<alp/i2c_regfile.h> — Register-File I²C Target
A helper that exposes a caller-owned RAM buffer as a register-mapped I²C target (slave).
The byte-granular target callbacks in <alp/peripheral.h> mirror the wire protocol faithfully — which means almost every target application layers the same "register-mapped peripheral" state machine on top of them. This header ships that state machine once, so applications stop re-pasting it.
Header
#include <alp/i2c_regfile.h>
The state machine
From the external controller's point of view, the device behaves like classic register-mapped silicon:
| Wire event | Effect |
|---|---|
| Controller write, byte 0 (after a (re)START) | Latches the register pointer, taken modulo the file length (EEPROM-style wraparound) |
| Controller write, byte 1… | Stores into the backing buffer at the pointer; auto-increment with wraparound |
| Controller read | Streams the backing buffer from the pointer; auto-increment with wraparound |
| STOP | Re-arms "next written byte is the pointer" for the following transaction |
Because the pointer wraps modulo len on every access, the controller can never index outside the buffer.
Quick example
static volatile uint8_t regs[32] = { [0] = 0xA5 }; // reg 0 = ID
alp_i2c_regfile_t *rf;
alp_status_t rc = alp_i2c_regfile_open(ALP_E1M_I2C0, /* own_addr_7bit */ 0x42,
regs, sizeof regs, &rf);
if (rc != ALP_OK) { /* degrade -- see availability below */ }
// Registers 0..1 stay read-only (ID / status); 2..31 are controller-writable.
alp_i2c_regfile_set_write_window(rf, /* first */ 2, /* count */ 30);
alp_i2c_regfile_stats_t st;
alp_i2c_regfile_stats(rf, &st); // st.writes_seen, st.reads_seen
alp_i2c_regfile_close(rf); // idempotent; buffer stays caller-owned
The buffer is the API
The backing buffer is caller-owned, which is the point of the design: firmware publishes state by plain (volatile) stores into it and observes controller writes by reading it back. There is no extra API between the ISR-context callbacks and the application thread.
:::caution Two rules the buffer imposes
- Prime
regsbefore callingopen. The callbacks start firing as soon as it returns — set your ID register and defaults first. - Declare it
volatilewhen the application thread polls it. The buffer is written from ISR context. :::
Functions
| Call | Returns |
|---|---|
alp_i2c_regfile_open(bus_id, own_addr_7bit, regs, len, &out) | ALP_OK; ALP_ERR_INVAL on NULL regs/out, len == 0, or an out-of-range address; ALP_ERR_NOMEM when the helper (or target) handle pool is exhausted; otherwise the alp_i2c_target_open failure code. bus_id is a studio-resolved instance id in the same space as alp_i2c_open; own_addr_7bit is 0x08..0x77; regs must stay valid until close. |
alp_i2c_regfile_set_write_window(rf, first, count) | ALP_OK; ALP_ERR_INVAL on NULL rf or a window that does not fit the register file. |
alp_i2c_regfile_stats(rf, &out) | ALP_OK; ALP_ERR_INVAL on NULL rf / out. |
alp_i2c_regfile_close(rf) | void. Idempotent on NULL. Unregisters the target; no callback touches the backing buffer after it returns, and the buffer stays caller-owned throughout. |
Write windows
The whole file is controller-writable by default. alp_i2c_regfile_set_write_window() restricts writes to count registers starting at first; everything outside becomes read-only. count == 0 makes the whole file read-only. Reads are never restricted.
Out-of-window writes are dropped silently, but the register pointer still auto-increments — mirroring real silicon with read-only ID / status registers.
Call it right after open, before controller traffic: the window is read from ISR context and is not updated atomically against an in-flight transaction.
Stats
alp_i2c_regfile_stats_t answers "is the controller talking to us at all":
| Field | Meaning |
|---|---|
writes_seen | uint32_t — payload bytes received (register-pointer bytes excluded) |
reads_seen | uint32_t — bytes streamed out to the controller |
Both start at zero at open and saturate only by uint32_t wraparound. They are updated from ISR context, so a snapshot taken mid-transaction may be one byte stale — fine for bench observability, not a transaction log.
Availability
Availability tracks alp_i2c_target_open exactly — this helper is a pure layer over the portable target API and degrades with the same status codes:
ALP_ERR_NOSUPPORT— the backend or controller driver lacks target mode. On Zephyr that meansCONFIG_I2C_TARGETplus a driver implementingtarget_register.ALP_ERR_NOT_READY— the bus alias is unset.
Applications must degrade cleanly on both.
:::note On native_sim, nothing drives the bus
The emulated controller accepts the registration, but nothing external ever talks to it — so the callbacks never fire and writes_seen / reads_seen stay at zero. That exercises the open/close path, not the state machine.
:::
ABI status
[ABI-EXPERIMENTAL] — new in v0.9. Tracks the [ABI-EXPERIMENTAL] alp_i2c_target_* surface it wraps. Pin your SDK to a specific commit if you depend on it.
See also
<alp/peripheral.h>: I²C target (slave) mode — the byte-granular surface this wraps- Examples: i2c-scanner — the controller-side counterpart