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 I2CThe 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 themCross-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.
See the implementation status for the full matrix.
Known gaps
What the SDK itself lists as missing here, straight from the header:
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.
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.
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.
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
hal_status_t core_i3c_init(core_i3c_t * h, I3C_TypeDef * instance, uint32_t scl_hz);void core_i3c_deinit(core_i3c_t * h);hal_status_t core_i3c_rstdaa(core_i3c_t * h);hal_status_t core_i3c_setdasa(core_i3c_t * h, uint8_t static_addr, uint8_t dyn_addr);hal_status_t core_i3c_entdaa(core_i3c_t * h, uint8_t * found);uint8_t core_i3c_attach(core_i3c_t * h, uint8_t static_addr);hal_status_t core_i3c_get_pid(core_i3c_t * h, uint8_t dyn_addr, uint8_t pid[6]);hal_status_t core_i3c_get_bcr(core_i3c_t * h, uint8_t dyn_addr, uint8_t * bcr);hal_status_t core_i3c_get_dcr(core_i3c_t * h, uint8_t dyn_addr, uint8_t * dcr);hal_status_t core_i3c_get_status(core_i3c_t * h, uint8_t dyn_addr, uint16_t * status);hal_status_t core_i3c_enec(core_i3c_t * h, uint8_t dyn_addr, uint8_t events);hal_status_t core_i3c_disec(core_i3c_t * h, uint8_t dyn_addr, uint8_t events);hal_status_t core_i3c_write(core_i3c_t * h, uint8_t dyn_addr, const uint8_t * data, uint16_t len);hal_status_t core_i3c_read(core_i3c_t * h, uint8_t dyn_addr, uint8_t * buf, uint16_t len);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);hal_status_t core_i3c_read_reg(core_i3c_t * h, uint8_t dyn_addr, uint16_t reg, uint8_t * buf, uint16_t len);hal_status_t core_i3c_ibi_enable(core_i3c_t * h, uint8_t dyn_addr, hal_i3c_ibi_cb_t cb, void * ctx);hal_status_t core_i3c_ibi_disable(core_i3c_t * h, uint8_t dyn_addr);| Type | Kind | Description |
|---|---|---|
core_i3c_t | typedef | Core-level I3C handle. Alias for hal_i3c_t — use this in application code.typedef hal_i3c_t core_i3c_t; |
| Name | Value | Description |
|---|---|---|
I3C_12M5 | 12500000UL | SDR maximum (12.4 MHz at a 248 MHz kernel) |
Generated from core_i3c.h — tiles@77fd3f3.

