Skip to main content

<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.

#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.

FieldTypeNotes
bus_iduint32_tStudio-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

CallReturns
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:

CodeMeaning
ALP_ERR_INVALBad config.
ALP_ERR_NOT_READYThe controller exists but is not ready.
ALP_ERR_NOT_PRESENT_ON_THIS_SOCNo I3C controller on this silicon (see the SoC counts below).
ALP_ERR_NOSUPPORTNo backend can serve this bus.

alp_i3c_write() / alp_i3c_read() / alp_i3c_write_read() return:

CodeMeaning
ALP_OKTransfer completed.
ALP_ERR_INVALBad argument.
ALP_ERR_NOT_READYHandle or controller not ready.
ALP_ERR_IOBus error — including a transfer to a target the devicetree never declared (see below).
ALP_ERR_NOSUPPORTThe 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_IOindistinguishable 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

BackendApplies toStatus
zephyr_drv (priority 100, silicon_ref="*")Synopsys DesignWare i3c_dw.c via the alp-i3cN devicetree alias, gated on CONFIG_I3C_CONTROLLEROpen 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 presentAll 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 and alp_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_read paths 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
SoCALP_SOC_I3C_COUNT
Alif Ensemble E31
Alif Ensemble E42
Alif Ensemble E51
Alif Ensemble E62
Alif Ensemble E71
Alif Ensemble E82
Renesas RZ/V2N (N44)1
NXP i.MX 930
DEEPX DX-M10

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

Questions about this page? Discuss in Community Forum