<alp/i3c.h> — I3C Controller
Controller-mode I3C transfers against a studio-resolved bus index. New in v0.14.0.
The portable surface is deliberately narrow: open a bus, move bytes to or from a target address, close. Everything I3C adds over I²C that is topology rather than traffic — dynamic address assignment, target declaration, bus timing — lives in devicetree, not in this header.
Header
#include <alp/i3c.h>
Quick example
alp_i3c_t *bus = alp_i3c_open(&(alp_i3c_config_t){
.bus_id = 0u,
});
if (bus == NULL) {
printk("i3c open failed: %d\n", alp_last_error());
return;
}
const uint8_t reg[] = { 0x0Fu }; /* WHO_AM_I */
uint8_t id = 0u;
if (alp_i3c_write_read(bus, 0x08u, reg, sizeof(reg), &id, sizeof(id)) == ALP_OK) {
printk("who_am_i = 0x%02x\n", id);
}
alp_i3c_close(bus);
alp_i3c_close() is idempotent and safe on NULL.
Config struct
alp_i3c_t is an opaque handle; the config carries exactly one field.
| Field | Type | Notes |
|---|---|---|
bus_id | uint32_t | Studio-resolved I3C controller index on the active SoM. |
Default-initialiser macro
alp_i3c_config_t cfg = ALP_I3C_CONFIG_DEFAULT(0u);
ALP_I3C_CONFIG_DEFAULT(id) sets bus_id from id. There is nothing else in the struct to default.
See the shared macro contract for the C++ compound-literal caveat that applies to every ALP_*_CONFIG_DEFAULT.
Functions
| Call | Returns |
|---|---|
alp_i3c_open(const alp_i3c_config_t *cfg) | alp_i3c_t *, or NULL — read alp_last_error(). |
alp_i3c_write(bus, uint8_t addr, const uint8_t *data, size_t len) | alp_status_t — see the error table. |
alp_i3c_read(bus, uint8_t addr, uint8_t *data, size_t len) | alp_status_t — see the error table. |
alp_i3c_write_read(bus, uint8_t addr, const uint8_t *wdata, size_t wlen, uint8_t *rdata, size_t rlen) | alp_status_t — see the error table. Write-then-read against one target, the usual register-read shape. |
alp_i3c_close(bus) | void. Idempotent; NULL is a no-op. |
alp_i3c_capabilities(const alp_i3c_t *bus) | const alp_capabilities_t *. See instance capabilities. |
Errors
alp_i3c_open() returns NULL and sets alp_last_error() to one of:
| Code | Meaning |
|---|---|
ALP_ERR_INVAL | Bad config. |
ALP_ERR_NOT_READY | The controller exists but is not ready. |
ALP_ERR_NOT_PRESENT_ON_THIS_SOC | No I3C controller on this silicon (see the SoC counts below). |
ALP_ERR_NOSUPPORT | No backend can serve this bus. |
alp_i3c_write() / alp_i3c_read() / alp_i3c_write_read() return:
| Code | Meaning |
|---|---|
ALP_OK | Transfer completed. |
ALP_ERR_INVAL | Bad argument. |
ALP_ERR_NOT_READY | Handle or controller not ready. |
ALP_ERR_IO | Bus error — including a transfer to a target the devicetree never declared (see below). |
ALP_ERR_NOSUPPORT | The selected backend has no transfer path (Linux/Yocto and the software fallback). |
:::caution Targets must be declared in devicetree, and an undeclared one looks exactly like a NACK
A target device must be declared as a devicetree child of the controller node. A transfer to a target that is not DT-declared returns ALP_ERR_IO — indistinguishable from a bus NACK. If a device you know is on the wire returns ALP_ERR_IO on every transfer, check the devicetree before you reach for a scope.
:::
What is deliberately not here
The portable surface excludes, on purpose:
- IBI (in-band interrupt) — no registration, no delivery.
- Raw CCC escape hatch — no way to issue an arbitrary common command code.
- Explicit re-run-DAA — dynamic address assignment is not a callable operation.
If you need any of those, you are outside the portable contract and must drop to the backend's native driver.
Backends
| Backend | Applies to | Status |
|---|---|---|
zephyr_drv (priority 100, silicon_ref="*") | Synopsys DesignWare i3c_dw.c via the alp-i3cN devicetree alias, gated on CONFIG_I3C_CONTROLLER | Open proven on Alif Ensemble E8; live transfer unproven. |
yocto_drv (priority 100, silicon_ref="*", vendor linux) | Linux/Yocto — presence check only: stat()s /sys/bus/i3c/devices/i3c-<bus_id> | alp_i3c_write / alp_i3c_read / alp_i3c_write_read always return ALP_ERR_NOSUPPORT, because mainline Linux ships no userspace raw-transfer ABI for I3C. Also bench-unverified: the i3c-<N> naming convention has not been confirmed against a running V2N kernel. |
sw_fallback (priority 0) | native_sim / no controller present | All calls return ALP_ERR_NOSUPPORT. |
See the selection rules for how priority and silicon_ref pick a row.
Bench status
Stated plainly, because the gap matters:
- Controller init is bench-proven on E1M-AEN801 silicon (Flow C ITCM RAM-run, 2026-07-25):
device_is_ready()passes andalp_i3c_open()returns a handle. - A live transfer is unproven. No I3C target is populated on the bench carrier, so DAA finds zero targets; the
write/read/write_readpaths are reached but never ACKed.
Treat the transfer path as untested silicon-side code until a target is on the carrier.
Capability gating
v0.14.0 adds the capability id ALP_CAP_ID_HW_I3C to alp_cap_id_t, the capability macro ALP_CAP_HW_I3C, and a generated per-SoC ALP_SOC_I3C_COUNT.
#if ALP_HAS(HW_I3C)
/* … I3C path … */
#endif
| SoC | ALP_SOC_I3C_COUNT |
|---|---|
| Alif Ensemble E3 | 1 |
| Alif Ensemble E4 | 2 |
| Alif Ensemble E5 | 1 |
| Alif Ensemble E6 | 2 |
| Alif Ensemble E7 | 1 |
| Alif Ensemble E8 | 2 |
| Renesas RZ/V2N (N44) | 1 |
| NXP i.MX 93 | 0 |
| DEEPX DX-M1 | 0 |
See Capabilities for the compile-time / runtime pair.
ABI status
[ABI-EXPERIMENTAL] — new in v0.14.0. The transfer path is not bench-proven and the surface may still move. Pin your SDK to a specific commit if you depend on it.
See also
<alp/peripheral.h>— error model,alp_last_error(), and the shared config-macro contract<alp/cap.h>—ALP_HAS(HW_I3C)gating<alp/backend.h>— why one backend row wins over anotherboard.yamlreference — where the bus lands in your board description- API Overview — the full header index