blob: 8f9265046de8c18e8d81b7000ad062ecbd923703 [file] [view]
<!-- Licensed under the Apache-2.0 license -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# i2c userspace driver
Layered like `services/mctp`, but adapted — patterns that solve a
usart-specific problem (e.g. parked/deferred reads) are intentionally **not**
carried over. i2c is strict request → response, run-to-completion.
**Supports:**
- **Master mode** — `embedded_hal::i2c::I2c::transaction()` (all operations;
write to send MCTP packets). ✅ Complete.
- **Slave/target mode** — Interrupt-driven responder with event metadata for
SPDM/MCTP dual-role operation. ✅ Slave RX (data reception) complete;
⚠️ Slave TX (ReadRequest responses) **required for SPDM responder** —
see [SPDM responder implementation](#spdm-responder-implementation).
```
consumer (any embedded-hal driver)
│ embedded_hal::i2c::I2c ← the only seam consumers see
client/ I2cClient<T: Transport> — ALL wire marshalling, host-buildable
│ i2c_api::Transport (bytes in → bytes out, one shot)
├── client-ipc/ IpcTransport (production, cross-process, kernel)
└── server/ LoopbackTransport (host tests + early boot)
▼ i2c_server::dispatch — decodes, replays, scatters reads
any embedded_hal::i2c::I2c
target/ast10x0/backend/i2c/ ← platform-specific, lives under target/
thin adapter over ast10x0_peripherals::i2c::Ast1060I2c
```
`api`/`client`/`server` are platform-agnostic and never name silicon. The
SoC-specific backend is the **only** crate that does, so it lives under
`target/<soc>/backend/`, mirroring `target/ast10x0/backend/usart`.
## Crates
| Crate | Bazel target | Host? | Role |
|-------|--------------|-------|------|
| `api` | `//services/i2c/api:i2c_api` | ✅ | Wire protocol + `embedded_hal::i2c::I2c` seam + the `Transport` seam. **Slave ops:** `ConfigureSlave`, `EnableSlave`, `DisableSlave`, `EnableSlaveNotification`, `SlaveReceive`, `SlaveSetResponse`. **Event kinds:** `DataReceived`, `ReadRequest`, `Stop` (for responder state machines). Host wire-codec tests. |
| `client` | `//services/i2c/client:i2c_client` | ✅ | `I2cClient<T: Transport>` implements `I2c` (master); also exposes slave methods (`configure_slave()`, `enable_slave()`, `slave_receive()`, etc.). All marshalling, no kernel/IPC dep. |
| `client-ipc` | `//services/i2c/client-ipc:i2c_client_ipc` | ❌ embedded | `IpcTransport` (`channel_transact`). The one IPC-coupled client piece. |
| `server` | `//services/i2c/server:i2c_server` | ✅ | Pure `dispatch()` + `dispatch_slave()` + `LoopbackTransport`. Host dispatch + e2e tests (master + slave RX). |
| `server-runtime` | `//services/i2c/server-runtime:i2c_server_runtime` | ❌ embedded | The Pigweed WaitGroup wait/respond loop. One channel per bus. On slave-RX IRQ, latches buffer + metadata (event kind, source address) and raises `Signals::USER`. |
| `backend` (ast10x0) | `//target/ast10x0/backend/i2c:i2c_backend_ast10x0` (crate `i2c_backend`) | ❌ embedded | bus → reg-ptr map, `init_bus`, `open_bus`/`open_bus_dma`. Under `target/`. |
## Key invariants
- **Host-testable protocol (the structural-template property).** The client
is generic over `i2c_api::Transport`; the *same* encoders/decoders run in
production (`IpcTransport`) and in host tests (`LoopbackTransport` →
`dispatch` → mock bus). Verified by `//services/i2c/tests:i2c_loopback_test`
— consumer → client → loopback → dispatch → mock, **no kernel/QEMU**.
- **Atomicity preserved across the process boundary.** One client
`I2c::transaction` ⇒ one `Transport::transact` ⇒ one server-side
`I2c::transaction` ⇒ one response. Never fragmented per-op.
- **One IPC channel per bus.** Multi-bus lives entirely in the server:
`i2c_server_runtime::run` takes `&[Bus { channel, driver }]`; adding a bus
is one slice entry, no code change.
- **Server is backend-agnostic.** `dispatch`/`run` are generic over
`embedded_hal::i2c::I2c`; never depend on the SoC backend. Errors map via
the embedded-hal `ErrorKind` taxonomy.
- **Dual-role responder support.** Interrupt-driven slave RX with event
metadata (kind + source address). Client waits on `Signals::USER`, fetches
event via `slave_receive()`, stages response via `slave_set_response()`.
Enables SPDM responders, register-echo patterns, and multi-master state
tracking. See [Dual-role responder support](#dual-role-responder-support).
## Dual-role responder support
The template supports **SPDM requester and responder** dual-role operation via
the interrupt-driven slave API:
- **Master (requester):** `I2c::transaction()` for atomic request-response
(write-read with repeated-START). Fully supported.
- **Slave (responder):** Interrupt-driven event notification + event metadata:
- `enable_notification()` — arm IRQ; server raises `Signals::USER` on event
- `slave_receive()` — non-blocking fetch after signal, returns
`SlaveReceiveEvent` with `kind`, `source_address`, `data_len`
- `slave_set_response()` — pre-load TX buffer (one response at a time)
**Event metadata:**
- `event_kind` — `DataReceived`, `ReadRequest`, or `Stop` (for responder
state machines)
- `source_address` — 7-bit I2C address of the requester (supports multi-master
buses; may be `0xFF` if hardware doesn't track it)
**Responder pattern (SPDM, register-echo, etc.):**
```rust
client.configure_slave(0x50)?;
client.enable_slave()?;
client.enable_notification()?;
loop {
// Wait for Signals::USER on channel
object_wait_signal(&channel, USER_BIT)?;
let event = client.slave_receive(&mut buf)?;
// Use event.source_address for per-requester state
// Use event.kind to distinguish read/write/stop
// Stage response for master's next read
client.slave_set_response(&buf[..event.data_len])?;
}
```
**Source address:** Captured during slave RX IRQ drain. Currently defaults to
`0xFF` (unavailable) until the hardware backend extracts it from registers.
## Wiring binaries
Client app (one bus): `I2cClient::new(IpcTransport::new(handle::I2C_n))` — then
use it purely as `embedded_hal::i2c::I2c`.
Server binary:
1. once at boot: `ast10x0_board::Ast10x0Board::init()` — the **board crate**
owns *all* I2C bring-up: subsystem (pin-mux / SCU clock+reset /
`init_i2c_global`) **and** eager per-controller `init_bus` for every wired
bus in its `&'static [I2cBusCfg]` descriptor (DMA buses included).
2. per owned bus, re-wrap the already-initialized controller (no re-init):
`i2c_backend::open_bus(n, &cfg)` for BufferMode, or `open_bus_dma(n, &cfg,
ram_nc_buf)` for DMA (buffer owned by the binary,
`#[link_section=".ram_nc"]`). `&cfg` must be the **same** descriptor entry
the board used.
3. build `&mut [i2c_server_runtime::Bus::new(handle::I2C_n, driver_n), …]`;
4. `i2c_server_runtime::run(handle::WG, handle::I2C_IRQ, signals::I2C, buses)`
— the runtime also registers the i2c IRQ; on it, drains every
notification-armed bus's slave RX into a per-bus latch and raises
`Signals::USER` on that bus's channel.
The matching `system.json5` declares one `channel_handler` per bus, the i2c
interrupt object, and one `wait_group`. Deferred until a concrete board
target needs it — not fabricated speculatively.
## SPDM responder implementation
**Status:** ⚠️ **REQUIRED for ocp-emea demo — full SPDM responder, not a subset.**
**What's implemented:**
- ✅ Slave RX data reception
- ✅ Event metadata framework (fields exist but not populated)
- ✅ `slave_receive()` API (client-side, returns `SlaveReceiveEvent`)
- ✅ `slave_set_response()` API (wire protocol + server dispatch)
- ✅ Server-runtime interrupt drain + per-bus latch
- ✅ Wire protocol supports Stop events
**Critical gaps blocking full SPDM responder:**
- ❌ Event kind propagation — `rx_event_kind` hardcoded to `DataReceived`;
responder cannot distinguish `Stop` event (transaction boundary)
- ❌ Source address extraction — `rx_source` hardcoded to `0xFF`; responder
cannot identify which requester sent the message
- ❌ `try_next_slave_event()` missing from HAL — backend must return event kind
alongside rx length so server-runtime can store it
**Post-demo (not blocking):**
- ReadRequest event delivery — needed for responder state machines; baseline SPDM
works via pre-staged TX buffer
- Hardware EVB testing (`--config=k_ast1060_evb`) — currently QEMU-only
## Test matrix
| Test target | Tags | What it covers |
|-------------|------|----------------|
| `//services/i2c/api:i2c_api_test` | `host` | Wire-codec unit tests: `I2cRequestHeader`/`I2cResponseHeader` round-trips, `I2cOpDesc` encode/decode, error + opcode byte mapping stability, slave opcode round-trips (`ConfigureSlave`…`SlaveReceive`), `SlaveReceive` max-len in `op_count`, `NoData` status round-trip |
| `//services/i2c/server:i2c_server_test` | `host` | `dispatch()`: write+read round-trip through a mock bus, bus error → wire error-code mapping, short/malformed request rejected without panic. `dispatch_slave()`: configure/enable/disable apply to device, runtime-owned ops (`SlaveReceive`) and malformed requests rejected |
| `//services/i2c/tests:i2c_loopback_test` | `host` | End-to-end: consumer drives `I2cClient` purely through the `embedded_hal::i2c::I2c` seam; `LoopbackTransport` routes the **real** client encoders/decoders into `i2c_server::dispatch` onto an `EchoBus` mock. Verifies address, write payload, op ordering, read scatter — the exact marshalling path used in production |
## Status
Host tests (`bazel test`, no kernel/QEMU): `//services/i2c/api:i2c_api_test`
(wire codec incl. slave ops), `//services/i2c/server:i2c_server_test`
(`dispatch` + `dispatch_slave` + error map),
`//services/i2c/tests:i2c_loopback_test` (end-to-end client↔server marshalling).
Full kernel/ARM stack incl. the slave/notification path under
`--config=virt_ast10x0`.
```bash
# Host only:
bazelisk test //services/i2c/api:i2c_api_test \
//services/i2c/server:i2c_server_test \
//services/i2c/tests:i2c_loopback_test
# Kernel/ARM (requires --config=virt_ast10x0):
bazelisk build --config=virt_ast10x0 //services/i2c/... \
//target/ast10x0/backend/i2c/...
```