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.

┌─────────────────────┐        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.

ItemPurpose
I2cRequestHeader / I2cResponseHeaderFixed-size headers serialized into IPC payloads
I2cOpEnum: Write, Read, WriteRead, Probe, Recover
ResponseCodeWire-level result codes (Ok, NoDevice, Busy, …)
BusIndex, I2cAddressValidated newtypes for bus number and 7-bit address
I2cClient traitwrite, 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:

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:

LayerRegistersWhenWhere
PlatformSCU reset, I2CG0C/I2CG10, SCU4xx pinmuxBoot (single-threaded)entry.rs
Per-busI2CC00, timing, I2CM10/I2CM14Server startupbackend.init_bus(n)
Per-operationNone (zero register writes)Each IPC requestAst1060I2c::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

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.

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

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:

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

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