BERGSONNE

I3C (Core.ST.H5)

I3C is I2C’s faster successor: push-pull SCL up to 12.5 MHz, dynamic addresses, and in-band interrupts on the same two wires. The STM32H523 is the only Core with it, so core_i3c is Core.ST.H5 only. It is the controller side, SDR mode.

Overview

I3C1 sits on pads 4 (SCL) and 5 (SDA), I3C2 on pads 2 and 3, the same pads as I2C1 and I2C3. You don’t declare an I3C bus: you ask coregen to run an I2C bus as I3C, and it does so when every tile on the bus supports it. Transfers are polled and bounded by a timeout, returning I3C_OK, I3C_NACK, I3C_TIMEOUT or I3C_ERROR. Including core_i3c.h on any other Core is a build error.

From config.json

Add "i3c": true to an I2C bus in interfaces. Tiles keep "bus": "I2C1":

"pads":  { "4": "I2C1.CLK", "5": "I2C1.DAT" },
"interfaces": {
  "I2C1": { "speed": 400000, "i3c": true, "i3c_speed": 12500000 }
},
"tiles": [ { "tile": "Sense.I.6P6", "bus": "I2C1", "instance": 0 } ]

coregen switches the bus only when the MCU has I3C, both pads offer it, and every tile on the bus speaks it. Otherwise the bus stays I2C, exactly as without the key, and a NOTE says why. When it switches, it emits core_i3c1 instead of core_i2c1, and core_init() resets any dynamic addresses left from the last image (RSTDAA) and gives each tile its dynamic address (SETDASA, from 0x09 up). Tile drivers then run unchanged:

#include "core.h"            // includes core_i3c.h when a bus runs as I3C
#include "tile_sense_i_6p6.h"

tile_t imu;
tile_sense_i_6p6_init(core_tiles_pal(&core_i3c1), 0, &imu, NULL);   // same driver as on I2C

The bridge maps each driver’s static address to its dynamic one, and re-assigns a tile that loses its address on its own reset. SCL depends on the clock level: 12.4 MHz at max, 10.7 MHz at medium, 8 MHz at low. See Project config for the keys.

By hand

Bring a controller up yourself, reset the bus addresses, and attach a target by its static I2C address. core_i3c_attach() returns its dynamic address, or 0 when nothing answers:

#include "core_i3c.h"

core_i3c_t bus;
core_i3c_init(&bus, I3C1, I3C_12M5);         // pads already in their I3C function
core_i3c_rstdaa(&bus);                       // forget addresses from the last image
uint8_t da = core_i3c_attach(&bus, 0x69);    // SETDASA: static 0x69 -> dynamic 0x09

uint8_t who;
if (da && core_i3c_read_reg(&bus, da, 0x75, &who, 1) == I3C_OK) {
    // WHO_AM_I read over I3C
}

core_i3c_entdaa() assigns addresses to every target that has none and records each one’s PID, BCR and DCR. The CCC helpers (get_pid, get_bcr, get_dcr, get_status) identify a target; write_reg / read_reg take 8- or 16-bit register addresses.

In-band interrupts

A target can interrupt the controller over the bus itself, with no extra pad. Enable it on both ends; the target also has to route an event to IBI in its own registers. Up to four targets can raise IBIs:

static void on_ibi(void *ctx, uint8_t dyn_addr, const uint8_t *payload, uint8_t len)
{
    (void)ctx; (void)dyn_addr; (void)payload; (void)len;   // interrupt context: keep it short
}

core_i3c_ibi_enable(&bus, da, on_ibi, NULL);   // acknowledge this target's IBIs
core_i3c_enec(&bus, da, LL_I3C_EVT_INT);       // then let it raise them

Cross-architecture support

Bench-verified on Core.ST.H5 with a Sense.I.6P6 (2026-09-27): 12.4 MHz SCL, dynamic addressing, CCCs, private transfers, IBIs at 1 kHz, and the tile driver over the bridge. The other Cores have no I3C peripheral.

·L0M0+n/a·L4M4n/a·W5M33n/a●H5M336/7WCH (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:

mediumC APINo HDR modes

The H5 I3C peripheral is SDR-only (RM0481 §49.2): no HDR-DDR, HDR-TSL/TSP or HDR-BT, although targets such as the ICM-42686-P offer DDR.

mediumC APINo legacy I2C targets on an I3C bus

The peripheral can send legacy I2C messages (MTYPE 0100), but this SDK does not expose them: coregen keeps a bus on I2C unless every tile on it speaks I3C, so a mixed bus never happens.

lowC APINo DMA, hot-join or secondary controller

FIFOs are serviced by polling (the controller stalls SCL for up to ~100 µs while it waits, §49.16.21); hot-join requests are NACKed (HJACK 0) and controller-role requests are not acknowledged.

mediumDSLNot in the DSL / Studio yet

Nothing here carries `@studio expose`, and Studio cannot request I3C for a bus yet; a hand-written config.json can ("i3c": true).

From the @studio unsupported notes in core_i3c.h — tiles@77fd3f3.

API reference

Lower-level · Tier 1
hal_status_t core_i3c_init(core_i3c_t * h, I3C_TypeDef * instance, uint32_t scl_hz);
Bring an I3C instance up as bus controller at `scl_hz` (push-pull SCL). The kernel clock is the APB clock (rcc_pclk1 for I3C1, rcc_pclk3 for I3C2; both run at SYSCLK on the Cores), so the fastest SCL depends on the clock level: 12.4 MHz at "max" (248 MHz), 10.7 MHz at "medium" (64 MHz), 8 MHz at "low" (16 MHz). The open-drain phases get the longest low time the kernel can count, up to 2 µs. The pads must already be in their I3C alternate function, push-pull (coregen does this).
Returns: I3C_OK, or I3C_ERROR for a bad instance.
void core_i3c_deinit(core_i3c_t * h);
Disable an I3C instance and stop its clock.
hal_status_t core_i3c_rstdaa(core_i3c_t * h);
Broadcast RSTDAA: every target on the bus forgets its dynamic address. Tiles keep their state across a reflash (they stay powered, and flash-serial on the H5 starts the new image without a reset), so run this before assigning addresses.
hal_status_t core_i3c_setdasa(core_i3c_t * h, uint8_t static_addr, uint8_t dyn_addr);
SETDASA: give the target at I2C static address `static_addr` the dynamic address `dyn_addr` (0 = the first free one, 0x09 up). Never its static address: a target that loses its dynamic address answers the static one again as an I2C device, and the controller could not tell.
Returns: I3C_OK, I3C_NACK when no target answers at that static address, I3C_ERROR when dyn_addr is taken or equals static_addr.
hal_status_t core_i3c_entdaa(core_i3c_t * h, uint8_t * found);
ENTDAA: assign dynamic addresses to every target that has none, recording each one's PID, BCR and DCR in h->targets[].
Returns: I3C_OK, or I3C_NACK when no target took part.
uint8_t core_i3c_attach(core_i3c_t * h, uint8_t static_addr);
Dynamic address of the target known by its static address, giving it one with SETDASA first if it has none yet.
Returns: The dynamic address, or 0 if the target does not answer.
hal_status_t core_i3c_get_pid(core_i3c_t * h, uint8_t dyn_addr, uint8_t pid[6]);
GETPID: the target's 48-bit provisioned ID, MSB first (MIPI manufacturer ID in the top 15 bits).
hal_status_t core_i3c_get_bcr(core_i3c_t * h, uint8_t dyn_addr, uint8_t * bcr);
GETBCR: bus characteristics (bit 1 IBI capable, bit 2 IBI payload).
hal_status_t core_i3c_get_dcr(core_i3c_t * h, uint8_t dyn_addr, uint8_t * dcr);
GETDCR: device characteristics (MIPI device-type code).
hal_status_t core_i3c_get_status(core_i3c_t * h, uint8_t dyn_addr, uint16_t * status);
GETSTATUS (format 1): the target's 16-bit status word.
hal_status_t core_i3c_enec(core_i3c_t * h, uint8_t dyn_addr, uint8_t events);
ENEC: let one target (or all, dyn_addr 0x7E) raise the given events (LL_I3C_EVT_INT for IBIs).
hal_status_t core_i3c_disec(core_i3c_t * h, uint8_t dyn_addr, uint8_t events);
DISEC: stop one target (or all, 0x7E) raising the given events.
hal_status_t core_i3c_write(core_i3c_t * h, uint8_t dyn_addr, const uint8_t * data, uint16_t len);
Private write of `len` bytes (len >= 1) to a target.
hal_status_t core_i3c_read(core_i3c_t * h, uint8_t dyn_addr, uint8_t * buf, uint16_t len);
Private read of up to `len` bytes (len >= 1) from a target.
hal_status_t core_i3c_write_reg(core_i3c_t * h, uint8_t dyn_addr, uint16_t reg, const uint8_t * data, uint16_t len);
Write `len` bytes starting at register `reg` (8-bit, or 16-bit MSB first when > 0xFF), in one private write.
hal_status_t core_i3c_read_reg(core_i3c_t * h, uint8_t dyn_addr, uint16_t reg, uint8_t * buf, uint16_t len);
Read `len` bytes starting at register `reg`: a private write of the register address, a repeated start, a private read, in one frame.
hal_status_t core_i3c_ibi_enable(core_i3c_t * h, uint8_t dyn_addr, hal_i3c_ibi_cb_t cb, void * ctx);
Acknowledge IBIs from a target and call `cb` for each (interrupt context: keep it short). Then enable them on the target with core_i3c_enec(). The target also has to route an event to IBI in its own registers.
hal_status_t core_i3c_ibi_disable(core_i3c_t * h, uint8_t dyn_addr);
Stop acknowledging IBIs from a target.
Types
TypeKindDescription
core_i3c_ttypedefCore-level I3C handle. Alias for hal_i3c_t — use this in application code.
typedef hal_i3c_t core_i3c_t;
Constants
NameValueDescription
I3C_12M512500000ULSDR maximum (12.4 MHz at a 248 MHz kernel)

Generated from core_i3c.h — tiles@77fd3f3.