blob: a15c2ade8f3ce123ed4617bd2c2115bec727afa8 [file] [view]
# I2C Service Framework
## Overview
The I2C service is a microkernel-style peripheral server built on the Pigweed
kernel. A dedicated userspace **server** process owns the I2C hardware
registers and exposes operations to **client** processes over IPC channels.
Clients never touch hardware directly — they serialize requests into a compact
wire protocol, transact over a kernel channel, and deserialize the response.
```text
┌─────────────────────┐ IPC channel ┌─────────────────────┐
│ Client process │◄─────────────────────────►│ Server process │
│ (i2c_client lib) │ channel_transact() │ (i2c_server bin) │
│ │ │ │
│ IpcI2cClient │ │ AspeedI2cBackend │
│ .write() │ │ .write() │
│ .read() │ │ .read() │
│ .write_read() │ │ .write_read() │
│ .probe() │ │ .recover_bus() │
└─────────────────────┘ └──────────┬──────────┘
aspeed-ddk │ Ast1060I2c
┌──────────────────┐
│ I2C Controller │
│ (MMIO regs) │
└──────────────────┘
```
---
## Crate Layers
### 1. `i2c_api` — Wire Protocol & Types
Platform-independent crate defining the types shared between client and server.
| Item | Purpose |
|------|---------|
| `I2cRequestHeader` / `I2cResponseHeader` | Fixed-size headers serialized into IPC payloads |
| `I2cOp` | Enum: `Write`, `Read`, `WriteRead`, `Probe`, `Recover` |
| `ResponseCode` | Wire-level result codes (`Ok`, `NoDevice`, `Busy`, …) |
| `BusIndex`, `I2cAddress` | Validated newtypes for bus number and 7-bit address |
| `I2cClient` trait | `write`, `read`, `write_read`, `probe` |
| `encode_*` / `decode_*` | Request/response serialization helpers |
**No dependencies on kernel, IPC, or hardware.**
```
//services/i2c/api:i2c_api
deps: embedded-hal
```
### 2. `i2c_client` — IPC Client Library
Userspace library implementing `I2cClient` over Pigweed IPC.
Each method encodes a request, calls `channel_transact()`, and decodes the
server's response.
```
//services/i2c/client:i2c_client
deps: i2c_api, userspace, embedded-hal
```
Usage:
```rust
let client = IpcI2cClient::new(handle::I2C); // handle from app_package
client.write(BusIndex::BUS_0, addr, &data)?;
```
### 3. `i2c_backend_aspeed` — Hardware Backend
Server-side adapter wrapping `aspeed-ddk` to drive AST1060 I2C controllers.
**Two-layer initialization model:**
| Layer | Registers | When | Where |
|-------|-----------|------|-------|
| Platform | SCU reset, I2CG0C/I2CG10, SCU4xx pinmux | Boot (single-threaded) | `entry.rs` |
| Per-bus | I2CC00, timing, I2CM10/I2CM14 | Server startup | `backend.init_bus(n)` |
| Per-operation | None (zero register writes) | Each IPC request | `Ast1060I2c::from_initialized()` |
Platform init sets up shared SCU/global registers that cannot be safely
modified from multiple processes. Per-bus init configures the individual
controller assigned to this server. Per-operation access creates a transient
handle on the stack with no register writes.
```
//services/i2c/backend-aspeed:i2c_backend_aspeed
deps: i2c_api, aspeed-ddk, ast1060-pac, pw_log
```
### 4. `i2c_server` — Server Binary
Userspace process that owns the I2C hardware. Runs a single-threaded
dispatch loop:
1. `object_wait(handle::I2C, READABLE)` — block until client request
2. `channel_read()` — deserialize `I2cRequestHeader`
3. Dispatch to `AspeedI2cBackend` method
4. `channel_respond()` — serialize `I2cResponseHeader` + data
```
//services/i2c/server:i2c_server
deps: app_i2c_server, i2c_api, i2c_backend_aspeed,
syscall_user, userspace, pw_log, pw_status
```
### 5. `i2c_client_test` — Test Binary
Userspace process that exercises the server through the client library.
Runs a sequence of test operations (probe, write, read, invalid bus) and
calls `debug_shutdown()` with the result.
```
//services/i2c/tests:i2c_client_test
deps: app_i2c_client, i2c_api, i2c_client,
syscall_user, userspace, pw_log, pw_status
```
---
## Bazel Build Structure
### Dependency Graph
```text
target/ast1060-evb/i2c:i2c (system_image)
├── :target (rust_binary — kernel)
│ ├── :codegen (target_codegen)
│ ├── :linker_script (target_linker_script)
│ ├── //target/ast1060-evb:entry
│ └── @pigweed//pw_kernel/...
├── //services/i2c/server:i2c_server (rust_binary — app)
│ ├── :app_i2c_server (app_package)
│ ├── //services/i2c/api
│ └── //services/i2c/backend-aspeed
│ ├── //services/i2c/api
│ ├── @oot_crates_no_std//:aspeed-ddk
│ └── @oot_crates_no_std//:ast1060-pac
└── //services/i2c/tests:i2c_client_test (rust_binary — app)
├── :app_i2c_client (app_package)
├── //services/i2c/api
└── //services/i2c/client
```
### Key Bazel Rules
#### `system_image`
Combines kernel binary + app binaries into a single flashable image.
Applies a Bazel configuration transition that sets the target platform
and system config flag for all transitive dependencies.
```starlark
system_image(
name = "i2c",
apps = [
"//services/i2c/tests:i2c_client_test",
"//services/i2c/server:i2c_server",
],
kernel = ":target",
platform = "//target/ast1060-evb",
system_config = ":system_config",
)
```
#### `app_package`
Generates a Rust crate from `system.json5` containing typed handle constants.
Each process's `objects` array entry becomes a `pub const` in the generated
`handle` module (index 0 → `handle::I2C = 0`).
```starlark
app_package(
name = "app_i2c_server",
app_name = "i2c_server", # must match system.json5 app name
system_config = "//target/ast1060-evb/i2c:system_config",
)
```
The `system_config` label is **hardcoded** per target, not the generic Bazel
flag. The `system_image` rule's transition sets the flag independently.
#### `target_codegen`
Generates a `codegen` crate with a `start()` function that boots all
userspace processes. The kernel's `target.rs` calls `codegen::start()` to
hand off to userspace.
#### `target_linker_script`
Generates a linker script from `system.json5` memory layout definitions,
placing vector table, kernel code, app code, kernel RAM, and app RAM
in their configured regions.
---
## System Configuration (`system.json5`)
The system configuration declares the memory map, process layout, kernel
objects, and thread stacks for the entire image.
### Memory Layout (AST1060-EVB)
```
0x00000000 ┌──────────────────────┐
│ Vector Table + │ 1184 bytes (0x4A0)
│ Kernel Annotations │
0x000004A0 ├──────────────────────┤
│ Kernel Code │ ~126 KB
0x00020000 ├──────────────────────┤
│ I2C Server App │ 128 KB
0x00040000 ├──────────────────────┤
│ I2C Client App │ 128 KB
0x00060000 ├──────────────────────┤
│ Kernel RAM │ 128 KB
0x00080000 ├──────────────────────┤
│ Server RAM │ 64 KB
0x00090000 ├──────────────────────┤
│ Client RAM │ 64 KB
0x000A0000 └──────────────────────┘
Total: 640 KB / 768 KB SRAM
```
### IPC Channel Objects
Kernel objects are declared per-process and linked across apps:
```json5
// Server process
objects: [{ name: "I2C", type: "channel_handler" }]
// Client process
objects: [{
name: "I2C",
type: "channel_initiator",
handler_app: "i2c_server",
handler_object_name: "I2C",
}]
```
The `channel_handler` / `channel_initiator` pair creates a bidirectional IPC
channel. The kernel connects them at boot based on the `handler_app` and
`handler_object_name` references.
---
## Build Commands
```bash
# Build the complete system image
bazel build --config=k_ast1060_evb //target/ast1060-evb/i2c:i2c
# Run QEMU test (virtual target)
bazel test --config=virt_ast1060_evb //target/ast1060-evb/i2c:i2c_test
# Flash to physical board via UART
bazel run --config=k_ast1060_evb //target/ast1060-evb/i2c:upload_i2c
# Build API library and run host tests
bazel test //services/i2c/api:i2c_api_test
```
---
## Adding a New I2C Target
To create a system image for a different board:
1. Create `target/<board>/i2c/system.json5` with the board's memory map
and the same app/channel object structure.
2. Create `target/<board>/i2c/BUILD.bazel` with `system_image`, `codegen`,
`linker_script`, and `target` rules pointing to the board's platform
and entry crate.
3. Create `target/<board>/i2c/target.rs` with the board's `TargetInterface`
impl (typically just `codegen::start()` + `shutdown()`).
4. Update `app_package` rules in `services/i2c/server/BUILD.bazel` and
`services/i2c/tests/BUILD.bazel` if needed, or add per-board
`app_package` targets with the new `system_config` label.
The `services/i2c/api`, `services/i2c/client`, and
`services/i2c/backend-aspeed` crates are target-independent and shared
across all board configurations.