BERGSONNE

I2C

Two wires, many devices. core_i2c is a blocking I2C master — probe for a device, then read and write its registers by 7-bit address. The hal_i2c and ll_i2c layers sit underneath. Use the Core / HAL / LL toggle at the top of the sidebar to switch.

Overview

You pick an I2C instance (I2C1, I2C3, …) and a bus speed — I2C_100K (standard), I2C_400K (fast), or I2C_1M (fast-mode plus). The bus timing for your kernel clock is computed for you. Everything is blocking and 7-bit addressed.

core_i2c is handle-based (Tier 1) with a full register API. A small Tier 2 *_bus surface (byte read/write/probe by config-declared bus id) is what Studio emits.

Probe & scan

probe sends an address and checks for an ACK — the quickest way to confirm a device is alive. scan walks the whole bus:

#include "core.h"

  core_i2c_t i2c;
  core_i2c_init(&i2c, I2C1, I2C_400K);

  if (core_i2c_probe(&i2c, 0x68) == I2C_OK) {
      // device acked at 0x68
  }

  uint8_t found[16];
  int n = core_i2c_scan(&i2c, found, /*count*/ 0, sizeof(found));

Reading & writing registers

Most I2C devices are register files. The *_reg / *_byte helpers do the write-address-then-read dance (repeated START) for you. Register addresses can be 8- or 16-bit:

uint8_t who;
  core_i2c_read_byte(&i2c, 0x68, 0x75, &who);   // read WHO_AM_I

  core_i2c_write_byte(&i2c, 0x68, 0x6B, 0x00);  // wake the device

  uint8_t accel[6];
  core_i2c_read_reg(&i2c, 0x68, 0x3B, accel, sizeof(accel));  // burst read
Studio is byte-level
The Tier 2 *_bus surface does single-byte register read/write/probe only — bulk transfers and scan are Tier 1.

Cross-architecture support

Standard and fast mode are the verified baseline everywhere they apply; fast-mode plus (1 MHz) needs stronger drivers and isn’t on every Core. Interrupt/DMA and slave mode are still ahead:

●L0M0+3/7●L4M44/8●W5M333/7●H5M334/8WCH (RISC-V) · Nordic (nRF54) — in development

See the implementation status for the full matrix.

Known gaps

What the SDK itself lists as missing here, straight from the header:

mediumDSLTier 2 is byte-level only — no bulk / scan

The current Tier 2 surface is write_byte_bus / read_byte_bus / probe_bus. DSL programs that need to push a multi-byte payload (display init sequences, IMU FIFO drains) or discover what's on a bus (`scan -> int[]`) drop back to Tier 1 with a hal_i2c_t handle. Adds need the array-IN / array-OUT host-call ABI prototyped on the tile-driver side — track with the DSL Capability Coverage close.

highC APIInterrupt-driven / non-blocking I/O

All current operations are polled. A non-blocking variant (with completion callback or status poll) would let DSL programs interleave bus traffic without stalling the main loop. Tracked on the SDK roadmap as 'I2C interrupt-driven'.

highC APIDMA transfers

No DMA path for bulk reads/writes. Long FIFO drains (e.g., IMU water-level batch reads) currently block on polled byte loops.

mediumDSLTarget mode is C-only

Target (device) mode landed at Tier 1: core_i2c_target_init() lets a Core answer as an I2C device on a host bus, using a register-file model with an optional ISR callback. There is no Tier 2 / DSL surface, because a register buffer and a callback have no representation in the DSL host-call ABI. A Studio-level version would need a declarative "expose these variables as registers" binding, close to what the BLE characteristic binding does.

lowC APITarget mode on H5 I2C2 / I2C3

startup_stm32h523xx.s carries vectors for I2C1_EV, I2C1_ER and I2C2_EV only. There is no I2C2_ER or I2C3 entry, and slot 58 is labelled as I2C2 Error shared with USART1, so target mode is restricted to I2C1 on Core.ST.H5. I2C1 + I2C3 both work on Core.ST.L4 and Core.ST.W5; Core.ST.L0 has only I2C1.

mediumC API10-bit addressing

API takes uint8_t addr — 7-bit only. No path for 10-bit-addressed peripherals (rare in practice but in spec).

lowC APISMBus / PMBus extensions

No PEC, no Alert Response, no block read/write protocol framing. Not requested by any current tile.

From the @studio unsupported notes in core_i2c.h — tiles@6af026f.

API reference

Default-instance · Tier 2
hal_status_t core_i2c_write_byte_bus(uint8_t bus, uint8_t addr, uint16_t reg, uint8_t value);
Write a single byte to a register on a device on `bus`. Returns I2C_OK on success, I2C_NACK / I2C_ERROR on bus failure, or I2C_ERROR if `bus` isn't declared in config.json.
int core_i2c_read_byte_bus(uint8_t bus, uint8_t addr, uint16_t reg);
Read a single byte from a register on a device on `bus`. Returns the byte value (0..255) on success, or -1 on any error (bus undeclared, NACK, timeout). The signed return lets DSL programs branch on `< 0` without an out-pointer.
int core_i2c_probe_bus(uint8_t bus, uint8_t addr);
Check if a device responds at `addr` on `bus`. Returns 1 if the device ACKs, 0 on NACK / timeout / undeclared bus.
Lower-level · Tier 1
uint32_t _core_i2c_timing(uint32_t speed_hz);
Resolve the TIMINGR value for a given speed using the compile-time I2C kernel clock (I2C_KERNEL_CLK_MHZ from core_config.h). Returns 0 if no pre-computed value exists.
hal_status_t core_i2c_init(core_i2c_t * h, I2C_TypeDef * instance, uint32_t speed_hz);
Initialize an I2C bus with automatic timing resolution. core_i2c_t bus; core_i2c_init(&bus, I2C1, I2C_400K); Resolves the TIMINGR value from the compile-time kernel clock. Enables FMP mode automatically when speed is I2C_1M.
hal_status_t core_i2c_init_cfg(core_i2c_t * h, I2C_TypeDef * instance, const hal_i2c_config_t * cfg);
Initialize I2C with explicit config struct (advanced). For most use cases, prefer core_i2c_init(h, instance, speed_hz).
hal_status_t core_i2c_write(core_i2c_t * h, uint8_t addr, const uint8_t * data, uint32_t len);
Write data to a 7-bit address device.
hal_status_t core_i2c_read(core_i2c_t * h, uint8_t addr, uint8_t * buf, uint32_t len);
Read data from a 7-bit address device.
hal_status_t core_i2c_write_reg(core_i2c_t * h, uint8_t addr, uint16_t reg, const uint8_t * data, uint32_t len);
Write to a device register (8 or 16-bit register address).
hal_status_t core_i2c_read_reg(core_i2c_t * h, uint8_t addr, uint16_t reg, uint8_t * buf, uint32_t len);
Read from a device register (repeated START).
hal_status_t core_i2c_write_byte(core_i2c_t * h, uint8_t addr, uint16_t reg, uint8_t value);
Write a single byte to a register.
hal_status_t core_i2c_read_byte(core_i2c_t * h, uint8_t addr, uint16_t reg, uint8_t * value);
Read a single byte from a register.
hal_status_t core_i2c_probe(core_i2c_t * h, uint8_t addr);
Check if a device responds at addr. Returns I2C_OK or I2C_NACK.
void core_i2c_scan(core_i2c_t * h, uint8_t * found, uint8_t * count, uint8_t max_count);
Scan the I2C bus (0x08–0x77). Fills found[] with responding addresses.
hal_i2c_t * core_i2c_handle_for_bus(uint8_t bus);
The I2C handle for a bus id declared in config.json, or NULL if the project doesn't declare that bus. Emitted per project by coregen into core_init.c (when the project declares any I2C bus); the *_bus helpers below resolve their handle through it. Forward-declared here rather than `#include "core_init.h"` so this header compiles without a project (val tests, examples without config.json). The natives-side caller is gated on CORE_HAS_I2C_BUSES, so the linker never asks for the symbol unless the dispatcher exists.
hal_status_t core_i2c_target_init(core_i2c_target_t * t, I2C_TypeDef * instance, uint8_t addr, uint32_t speed, uint8_t * regs, uint16_t n_regs, hal_i2c_target_cb_t cb, void * ctx);
Bring up an I2C peripheral as a read-only target at `addr`.
hal_status_t core_i2c_target_init_rw(core_i2c_target_t * t, I2C_TypeDef * instance, uint8_t addr, uint32_t speed, uint8_t * regs, uint16_t n_regs, uint16_t first, uint16_t count, hal_i2c_target_cb_t cb, void * ctx);
As core_i2c_target_init, but accepts host writes to the register range [first, first + count). Everything outside stays read-only.
void core_i2c_target_deinit(core_i2c_target_t * t);
Stop answering on the bus.

Generated from core_i2c.h — tiles@f70bca2.