Skip to main content

I2C

The I2C (Inter-Integrated Circuit) interface provides two-wire synchronous serial communication. Supports standard (100 kHz), fast (400 kHz), and fast-plus (1 MHz) modes with configurable voltage levels from 1.8 V to 3.3 V.

Only master mode

Please note that the AT1000 device only supports master mode for I2C communication. This means that it can initiate communication with slave devices, but it cannot act as a slave itself.

Clock stretching

The AT1000 I2C master does not support clock stretching. A slave that holds SCL low to delay a transfer will not be waited for, so devices that rely on clock stretching need their own timing accommodation.

Multiplexing

Some interfaces share the same GPIO pins, so only one can be used at a time. Check out the pin alternate functions page for more details.

The PWM pins use the same channels: enabling I2C fails with 409 CONFIGURATION_CONFLICT while PWM is enabled on a pin of the same channel (DA17/DA18 for I2C 0, DA21/DA22 for I2C 1).

Enabling I2C also fails with 409 CONFIGURATION_CONFLICT while its SDA or SCL pin is configured as a GPIO. Reset the pin to its default analog input (reset() on the pin) before enabling I2C.

Pin-voltage check

Enabling I2C, or changing vcc while it is enabled, first measures its SDA and SCL pins. If any of them reads below −0.5 V or above vcc + 0.5 V, the request fails with 409 UNSAFE_PIN_VOLTAGE and the interface stays disabled. The error message names the pin and the voltage measured. Reconfiguring with an unchanged vcc does not repeat the check.

Quick Start​

The example code below shows how to enable the I2C bus, set the baud rate, and transmit/receive data.

import { AT1000 } from '@ikalogic/at1000';
let devices = await AT1000.findDevices(); // Find devices (returns host strings)
let tester = await AT1000.open(devices[0]); // Open the first detected device
await tester.reset(); // Reset the device to a known state
let i2c = tester.com.i2c(0); // Select the first I2C bus

await i2c.enable({ vcc: 3.3, baud_rate: 100000 }); // Enable I2C bus with 3.3V and 100kHz

// Define an I2C transaction
let i2c_transaction = {
address: 0x50, // I2C address of the slave device
start: true, // Start condition
stop: true, // Stop condition
nack_last_byte: false, // Don't NACK the last byte; the device default is true
};

// Write request to the slave device, registers 0x10 and 0x20
await i2c.tx({ ...i2c_transaction, data: [0x10, 0x20] });

i2c_transaction.nack_last_byte = true;
// Read 4 bytes from the slave device
let data = await i2c.rx({ ...i2c_transaction, length: 4 });

// Disable I2C bus on AT1000 master
await i2c.disable();

API Reference​

Accessing I2C​

const i2c = tester.com.i2c(id);

Parameters:

  • id (number / int / usize): The I2C interface identifier (0-based index)

Returns: I2c instance / Result<I2c, At1000Error> in Rust


Methods​

configure(config)​

Configures the I2C interface parameters without changing the enabled state.

await i2c.configure({
baud_rate: 400000,
vcc: 3.3
});

Parameters:

  • config (I2cConfigPatch): Configuration object with optional properties:
    • enabled (boolean / bool / Option<bool>, optional): Enable or disable the interface
    • baud_rate (number / int / Option<u32>, optional): I2C clock frequency - 100000, 400000, or 1000000
    • vcc (number / float / Option<f64>, optional): Logic level voltage in volts (1.8 to 3.3)

Valid baud rates:

  • 100000 (100 kHz): Standard mode
  • 400000 (400 kHz): Fast mode
  • 1000000 (1 MHz): Fast-plus mode

Default configuration (after tester.reset()): disabled, baud_rate 1000000, vcc 3.3 V.

Returns: Promise<I2cConfig> / I2cConfig / Result<I2cConfig, At1000Error> - The updated configuration

Exceptions:

  • Throws validation error if baud_rate is not one of the valid values
  • Throws validation error if vcc is outside the range [1.8, 3.3]
  • Throws error 409 UNSAFE_PIN_VOLTAGE if enabling, or changing vcc while enabled, finds SDA or SCL below −0.5 V or above vcc + 0.5 V
  • Throws error 409 CONFIGURATION_CONFLICT when enabling while SDA or SCL is configured as a GPIO (not an analog input), or while SPI, UART or PWM uses the same channel

enable([config])​

Enables the I2C interface and optionally applies configuration.

await i2c.enable({
baud_rate: 400000,
vcc: 3.3
});

Parameters:

  • config (I2cEnableConfig, optional; I2cConfigPatch in Rust, always passed, with enabled set to true by enable itself): Optional configuration to apply:
    • baud_rate (number / int / Option<u32>, optional): I2C clock frequency (100000, 400000, or 1000000)
    • vcc (number / float / Option<f64>, optional): Logic level voltage (1.8 to 3.3)

Returns: Promise<I2cConfig> / I2cConfig / Result<I2cConfig, At1000Error> - The updated configuration with enabled: true

Exceptions:

  • Throws validation error if parameters are outside valid ranges
  • Throws error 409 UNSAFE_PIN_VOLTAGE if SDA or SCL reads below −0.5 V or above vcc + 0.5 V
  • Throws error 409 CONFIGURATION_CONFLICT while SDA or SCL is configured as a GPIO (not an analog input), or while SPI, UART or PWM uses the same channel

disable()​

Disables the I2C interface.

await i2c.disable();

Returns: Promise<I2cConfig> / I2cConfig / Result<I2cConfig, At1000Error> - The updated configuration with enabled: false


tx(transaction)​

Writes data to an I2C device.

// Write to device at address 0x50
await i2c.tx({
address: 0x50,
data: [0x00, 0x10, 0xAA, 0xBB]
});

// Write with custom start/stop conditions
await i2c.tx({
address: 0x3C,
start: true,
stop: false, // No stop - for repeated start
data: [0x40, 0xFF]
});

Parameters:

  • transaction (I2cTransactionWrite): Write transaction configuration. In Rust it is passed by value and built with I2cTransactionWrite::new(address, data), with optional .start(bool) / .stop(bool) setters:
    • address (number / int / I2cAddress, required): 7-bit I2C device address
    • data (string | number[] / str | list[int] / &[u8], required): Data to write (max 65536 bytes)
    • start (boolean / bool / .start(bool), optional, default: true): Generate START condition
    • stop (boolean / bool / .stop(bool), optional, default: true): Generate STOP condition
    • nack_last_byte (boolean / bool, optional, default: true): Send NACK on last byte (ignored on writes; not available on Rust writes)

Returns: Promise<number> / int / Result<usize, At1000Error> - Number of data bytes the slave acknowledged

A NACK is not an error, in any language: tx returns the number of bytes the slave acknowledged before the first NACK, which is 0 when it never acknowledged its address. Compare the result with the data length to detect a NACK.

Exceptions:

  • Throws validation error if data exceeds 65536 bytes (the Rust SDK does not check the length itself)
  • Throws error 400 INVALID_CONFIGURATION if the interface is disabled

rx(transaction)​

Reads data from an I2C device.

// Read 16 bytes from device at address 0x50
const data = await i2c.rx({
address: 0x50,
length: 16
});

// Read with custom conditions
const data2 = await i2c.rx({
address: 0x3C,
length: 8,
start: true,
stop: true,
nack_last_byte: true
});

Parameters:

  • transaction (I2cTransactionRead): Read transaction configuration. In Rust it is passed by value and built with I2cTransactionRead::new(address, length), with optional .start(bool) / .stop(bool) / .nack_last_byte(bool) setters:
    • address (number / int / I2cAddress, required): 7-bit I2C device address
    • length (number / int / usize, required): Integer number of bytes to read (0 to 65536, inclusive)
    • start (boolean / bool / .start(bool), optional, default: true): Generate START condition
    • stop (boolean / bool / .stop(bool), optional, default: true): Generate STOP condition
    • nack_last_byte (boolean / bool / .nack_last_byte(bool), optional, default: true): Send NACK on last byte

Returns: Promise<SerialData> / SerialData / Result<Vec<u8>, At1000Error> - Received data as byte array (number[], Vec<u8> in Rust)

A NACK is not an error, in any language: when the slave does not acknowledge its address, rx returns an empty array (an empty vector in Rust) instead of length bytes.

Exceptions:

  • All three SDKs reject length locally unless it is an integer in [0, 65536], before sending a request.
  • Throws error 400 INVALID_CONFIGURATION if the interface is disabled

Types​

I2cConfig​

Complete configuration object returned by configuration methods.

{
enabled: boolean,
baud_rate: number, // 100000, 400000, or 1000000
vcc: number // 1.8 to 3.3
}

Rust: ikalogic_at1000::I2cConfig

pub struct I2cConfig {
pub enabled: bool,
pub baud_rate: u32,
pub vcc: f64,
}

I2cConfigPatch​

Partial configuration for the configure() method. All properties are optional.

{
enabled?: boolean,
baud_rate?: number, // 100000, 400000, or 1000000
vcc?: number // 1.8 to 3.3
}

Rust: ikalogic_at1000::I2cConfigPatch

pub struct I2cConfigPatch {
pub enabled: Option<bool>,
pub baud_rate: Option<u32>,
pub vcc: Option<f64>,
}

I2cEnableConfig​

Optional configuration for the enable() method.

{
baud_rate?: number, // 100000, 400000, or 1000000
vcc?: number // 1.8 to 3.3
}

Rust has no separate enable type: enable takes I2cConfigPatch and sets enabled: true itself.

I2cTransactionWrite​

Write transaction parameters.

{
address: number, // 7-bit I2C address
data: string | number[], // Data to write (max 65536 bytes)
start?: boolean, // Generate START (default: true)
stop?: boolean, // Generate STOP (default: true)
nack_last_byte?: boolean // NACK last byte (default: true; ignored on writes)
}

Rust: ikalogic_at1000::I2cTransactionWrite. The fields are private: build the transaction with a constructor and setters. It is Copy and is passed to tx by value. The payload is borrowed for the duration of the call, and the address is a validated I2cAddress built with I2cAddress::try_from(0x50)?, which rejects anything above 0x7f. Rust writes have no nack_last_byte.

impl<'a> I2cTransactionWrite<'a> {
pub const fn new(address: I2cAddress, data: &'a [u8]) -> Self; // An empty payload is permitted
pub const fn start(self, start: bool) -> Self; // Generate START (device default: true)
pub const fn stop(self, stop: bool) -> Self; // Generate STOP (device default: true)
}

I2cTransactionRead​

Read transaction parameters.

{
address: number, // 7-bit I2C address
length: number, // Integer number of bytes to read (0-65536)
start?: boolean, // Generate START (default: true)
stop?: boolean, // Generate STOP (default: true)
nack_last_byte?: boolean // NACK last byte (default: true)
}

Rust: ikalogic_at1000::I2cTransactionRead. The fields are private: build the transaction with a constructor and setters. It is Copy and is passed to rx by value.

impl I2cTransactionRead {
pub const fn new(address: I2cAddress, length: usize) -> Self;
pub const fn start(self, start: bool) -> Self; // Generate START (device default: true)
pub const fn stop(self, stop: bool) -> Self; // Generate STOP (device default: true)
pub const fn nack_last_byte(self, nack_last_byte: bool) -> Self; // NACK last byte (device default: true)
}

SerialData​

Output data type, always returned as a byte array.

number[] // Array of bytes (0-255)

In Rust, data is a Vec<u8>.


Usage Example​

import { AT1000 } from '@ikalogic/at1000';

// Find and connect to device
const devices = await AT1000.findDevices();
const tester = await AT1000.open(devices[0]);
await tester.reset(); // Reset the device to a known state

// Access I2C interface 0
const i2c = tester.com.i2c(0);

// Enable with 400 kHz clock and 3.3V logic
await i2c.enable({
baud_rate: 400000,
vcc: 3.3
});

// Write to EEPROM at address 0x50
await i2c.tx({
address: 0x50,
data: [0x00, 0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F] // "Hello" at address 0x0000
});

// Wait for write cycle to complete
await new Promise(resolve => setTimeout(resolve, 10));

// Read back from EEPROM
// First, set read address
await i2c.tx({
address: 0x50,
data: [0x00, 0x00],
stop: false // No stop for repeated start
});

// Then read data
const data = await i2c.rx({
address: 0x50,
length: 5
});

console.log("Read:", Buffer.from(data).toString()); // "Hello"

// Disable interface
await i2c.disable();

Common Patterns​

Register Read (Single Byte)​

// Read register 0x75 from device at 0x68 (e.g., IMU WHO_AM_I register)
await i2c.tx({
address: 0x68,
data: [0x75],
stop: false
});

const whoAmI = await i2c.rx({
address: 0x68,
length: 1
});

console.log("WHO_AM_I:", whoAmI[0].toString(16));

Register Write​

// Write 0x80 to register 0x6B (e.g., PWR_MGMT_1 register)
await i2c.tx({
address: 0x68,
data: [0x6B, 0x80]
});

Multi-Byte Register Read​

// Read 6 bytes starting from register 0x3B (e.g., accelerometer data)
await i2c.tx({
address: 0x68,
data: [0x3B],
stop: false
});

const accelData = await i2c.rx({
address: 0x68,
length: 6
});

// Parse as 16-bit signed integers
const accelX = (accelData[0] << 8) | accelData[1];
const accelY = (accelData[2] << 8) | accelData[3];
const accelZ = (accelData[4] << 8) | accelData[5];

EEPROM Operations​

// Write to EEPROM (24C256 example)
const eepromAddr = 0x50;
const memAddr = [0x00, 0x10]; // Address 0x0010
const writeData = [0xAA, 0xBB, 0xCC, 0xDD];

await i2c.tx({
address: eepromAddr,
data: [...memAddr, ...writeData]
});

// Wait for write cycle (5ms typical)
await new Promise(resolve => setTimeout(resolve, 10));

// Read back
await i2c.tx({
address: eepromAddr,
data: memAddr,
stop: false
});

const readData = await i2c.rx({
address: eepromAddr,
length: 4
});

Scanning I2C Bus​

Each address is probed with a 1-byte read: a slave that acknowledges its address returns one byte, an absent one returns an empty array. A write with no data cannot be used for this, because its result is 0 whether or not the address was acknowledged. Some devices act on a read (for example by popping a FIFO), so scan only buses where that is harmless.

console.log("Scanning I2C bus...");
const foundDevices = [];

for (let addr = 0x08; addr < 0x78; addr++) {
// An absent device NACKs its address and the read returns no bytes
const data = await i2c.rx({ address: addr, length: 1 });
if (data.length > 0) {
foundDevices.push(addr);
console.log(`Found device at 0x${addr.toString(16).padStart(2, '0')}`);
}
}

console.log(`Found ${foundDevices.length} device(s)`);

Repeated Start Transaction​

// Combined write-read transaction with repeated start
// (More efficient than separate transactions)

// Write register address
await i2c.tx({
address: 0x3C,
data: [0x00],
stop: false // Keep bus active for repeated start
});

// Immediately read without releasing bus
const data = await i2c.rx({
address: 0x3C,
length: 16,
start: true, // Repeated START
stop: true
});

embedded-hal Support (Rust)​

The Rust SDK implements the embedded-hal 1.0 traits, so existing driver crates can talk to a device wired to the AT1000. Add embedded-hal = "1" to your dependencies to call the trait methods directly.

  • I2c implements embedded_hal::i2c::I2c with 7-bit addresses. A transaction sends START before the first operation, a repeated START when the direction changes, and STOP after the last operation
  • A NACK fails with At1000Error::NoAcknowledge, whose embedded-hal kind is ErrorKind::NoAcknowledge. This differs from the tx() method, which returns the number of acknowledged bytes
  • Spi implements embedded_hal::spi::SpiBus. The bus has no chip select of its own: drive one from a DigitalIo output, for example with ExclusiveDevice from the embedded-hal-bus crate
  • DigitalIo implements OutputPin and InputPin, once configured as a digital output or input
use embedded_hal::i2c::I2c as _;
use ikalogic_at1000::I2cConfigPatch;

let mut i2c = tester.com.i2c(0)?;
i2c.enable(I2cConfigPatch { vcc: Some(3.3), baud_rate: Some(400_000), ..Default::default() })?;

// Read register 0x75 from device at 0x68 (e.g., IMU WHO_AM_I register), with a repeated START
let mut buf = [0u8; 1];
i2c.write_read(0x68, &[0x75], &mut buf)?;
println!("WHO_AM_I: {:02x}", buf[0]);

Notes​

  • I2C addresses are 7-bit values (0 to 127 / 0x00 to 0x7F); 0x08 to 0x77 is the non-reserved address range
  • Pull-up resistors are typically required on SDA and SCL lines (often integrated)
  • Clock stretching is not supported by the underlying hardware
  • Use nack_last_byte: true (.nack_last_byte(true) in Rust) when reading to signal end of transaction
  • Read transactions accept an integer length from 0 to 65536 bytes, inclusive, with local validation in all three SDKs. Write data may be up to 65536 bytes per transaction