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.
┌─────────────────────┐ 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) │ └──────────────────┘
i2c_api — Wire Protocol & TypesPlatform-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
i2c_client — IPC Client LibraryUserspace 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:
let client = IpcI2cClient::new(handle::I2C); // handle from app_package client.write(BusIndex::BUS_0, addr, &data)?;
i2c_backend_aspeed — Hardware BackendServer-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
i2c_server — Server BinaryUserspace process that owns the I2C hardware. Runs a single-threaded dispatch loop:
object_wait(handle::I2C, READABLE) — block until client requestchannel_read() — deserialize I2cRequestHeaderAspeedI2cBackend methodchannel_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
i2c_client_test — Test BinaryUserspace 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
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
system_imageCombines 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.
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_packageGenerates 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).
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_codegenGenerates 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_scriptGenerates 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.json5)The system configuration declares the memory map, process layout, kernel objects, and thread stacks for the entire image.
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
Kernel objects are declared per-process and linked across apps:
// 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 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
To create a system image for a different board:
Create target/<board>/i2c/system.json5 with the board's memory map and the same app/channel object structure.
Create target/<board>/i2c/BUILD.bazel with system_image, codegen, linker_script, and target rules pointing to the board's platform and entry crate.
Create target/<board>/i2c/target.rs with the board's TargetInterface impl (typically just codegen::start() + shutdown()).
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.