ast10x0_pfr)This crate provides the AST10x0 PFR (Platform Firmware Resilience) building blocks. Its current public surface is SwmbxCtrl, the in-memory controller for the software mailbox (swmbx).
SwmbxCtrl models a small block of shared memory that two host agents poke at over SMBus/I²C, and layers three behaviors on top of that memory: write protection, change notification, and FIFO-backed registers.
The crate is no_std and allocation-free: all state lives in fixed-size arrays sized by compile-time constants.
Picture a 256-byte register file that is visible to more than one master:
address 0x00 ........................... 0xFF
+----+----+----+----+-- --+----+----+----+
buffer: | b0 | b1 | b2 | b3 | ... | | |bFF | <- flat shared memory
+----+----+----+----+-- --+----+----+----+
^ (lives at `buffer_base`)
|
port 0 (e.g. BMC) ---+
+---> both ports see the SAME 256 bytes
port 1 (e.g. PCH) ---+
SWMBX_DEV_COUNT = 2 of them (think BMC and PCH/CPU). Both ports share the one underlying buffer, but each port has its own per-address policy and its own transaction state.u8, 0x00..=0xFF) selects one byte/register. Each address is called a node; there are SWMBX_NODE_COUNT = 256 nodes per port.SWMBX_FIFO_COUNT = 4 FIFOs, each up to SWMBX_FIFO_DEPTH = 256 entries deep.SwmbxCtrl is the policy/state layer only. It does not own a real bus; the shared memory is reached through a caller-supplied buffer address (see §7).
SwmbxCtrl ├── mbx_en: u8 // GLOBAL feature switches ├── node: [[SwmbxNode; 256]; 2] // per-port, per-address policy ├── fifo: [SwmbxFifo<256>; 4] // the four FIFO endpoints ├── mbx_fifo_execute: [bool; 2] // per-port: in a FIFO transaction? ├── mbx_fifo_addr: [u8; 2] // per-port: address that opened it ├── mbx_fifo_idx: [u8; 2] // per-port: which FIFO is active ├── buffer_base: usize // address of the 256-byte buffer └── buffer_size: usize // length of the buffer region
mbx_enA bitmask of three independent features, toggled with enable_behavior:
| Flag | Bit | Meaning when set globally |
|---|---|---|
SWMBX_PROTECT | 1 << 0 | Per-node write protection is armed |
SWMBX_NOTIFY | 1 << 1 | Per-node / FIFO notifications are armed |
SWMBX_FIFO | 1 << 2 | FIFO remapping is armed |
A feature only takes effect when both the global switch and the relevant per-node bit are set. This two-level gating lets firmware arm/disarm a whole class of behavior with one write without losing the per-address configuration.
SwmbxNodestruct SwmbxNode { notify_flag: bool, // a pending "this node changed" event enabled_flags: u8, // per-node PROTECT / NOTIFY bits }
enabled_flags reuses the SWMBX_PROTECT / SWMBX_NOTIFY bit positions, but at per-address granularity. notify_flag is the latched event for that node.
SwmbxFifo<N>struct SwmbxFifo<N> { queue: Queue<SwmbxFifoEntry, N>, // heapless SPSC ring of bytes notify_flag: u8, // CONFIG: which of START/STOP notifies are enabled notify_start: bool, // runtime: has START notify already fired this txn? fifo_write: bool, // runtime: was anything written this txn? fifo_offset: u8, // the address this FIFO is bound to enabled: bool, // is this FIFO active? msg_index: usize, // write cursor max_msg_count: usize, // effective depth (<= N) }
Note the name collision:
SwmbxNode::notify_flagis a bool event latch, whileSwmbxFifo::notify_flagis a u8 configuration mask ofSWMBX_FIFO_NOTIFY_START (1<<0)/SWMBX_FIFO_NOTIFY_STOP (1<<1). They are unrelated despite sharing a field name.
These set up policy before traffic flows:
| Method | Effect |
|---|---|
new_with_regions(size, buf) | Construct, recording the buffer region address. unsafe (§7). |
enable_behavior(flag, enable) | Flip one or more global switches in mbx_en. |
update_protect(port, addr, enable) | Set/clear the per-node PROTECT bit for one address. |
apply_protect(port, bitmap, start_idx) | Bulk-set protection from packed 32-bit words (see below). |
update_notify(port, addr, enable) | Set/clear the per-node NOTIFY bit for one address. |
update_fifo(idx, addr, depth, notify, enable) | Bind FIFO idx to addr, set depth + notify config, (re)enable. |
flush_fifo(idx) | Drain a FIFO and reset its transient flags. |
apply_protect and the bitmap geometryProtection for all 256 nodes fits in 256 / 32 = 8 words of 32 bits each (PROTECT_WORD_COUNT = 8, PROTECT_BITS_PER_WORD = 32). Each word covers a contiguous block of 32 addresses; start_idx says which word the slice begins at:
node address = (start_idx + word_index) * 32 + bit_index
└─ PROTECT_WORD_SHIFT = log2(32) = 5
Bit set → node protected, bit clear → node unprotected. The range is validated with checked arithmetic, so an out-of-range or overflowing start_idx returns InvalidProtectRange instead of panicking or wrapping.
Every read/write resolves to one of two backends:
┌─────────────────────────── send_msg / get_msg ───────────────────────────┐
│ │
in a FIFO txn AND │ else │
SWMBX_FIFO armed? │ │
yes ─────┤ no ──┤
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ FIFO path │ │ flat-buffer path │
│ append/pop │ │ write/read byte │
│ on fifo[i] │ │ at buffer_base │
└─────────────┘ └──────────────────┘
The selector is: mbx_fifo_execute[port] && (mbx_en & SWMBX_FIFO) != 0.
FIFO routing is per transaction, bracketed by send_start / send_stop:
send_start(port, addr) │ check_fifo(addr): is there an ENABLED fifo whose fifo_offset == addr? ├─ yes → mbx_fifo_execute[port] = true │ mbx_fifo_addr[port] = addr // remember which node opened it │ mbx_fifo_idx[port] = i // remember which FIFO └─ no → mbx_fifo_execute[port] = false // this txn uses the flat buffer │ ▼ send_msg(port, addr, val) (repeatable) │ FIFO path → fifo[i].append_write(val) │ • on first write, if NOTIFY armed + node NOTIFY set + │ fifo START-notify configured → latch node notify_flag, │ mark notify_start so it fires only once │ • on FIFO full → latch node notify_flag, return FifoFull │ get_msg(port, addr) (repeatable) │ FIFO path → fifo[i].dequeue() // removes (pops) the oldest byte │ ▼ send_stop(port) │ if STOP-notify configured + something was written this txn + │ node NOTIFY set → latch node notify_flag │ reset notify_start / fifo_write, clear mbx_fifo_* for this port
So a typical streamed-register exchange is: send_start → send_msg × N → (peer) get_msg × N → send_stop.
When not in a FIFO transaction, send_msg runs the node policy:
PROTECT bit and the global SWMBX_PROTECT switch are set. A protected write is silently discarded (the call still returns Ok); the buffer keeps its old value.SWMBX_NOTIFY switch and the node‘s NOTIFY bit are set, the node’s notify_flag is latched to true.get_msg on this path simply reads the byte from the buffer.
Current limitation:
notify_flagon a node is written but the crate exposes no API to read or consume it, so notifications are presently unobservable from outside. Wiring up a consumer (e.g.take_notify(port, addr)) is a known follow-up.
swmbx_write(fifo, addr, val) / swmbx_read(fifo, addr) bypass transaction state entirely: with fifo = true they act on the FIFO mapped to addr (error FifoNotMapped if none), otherwise straight on the flat buffer. These are the “the firmware itself wants to poke the mailbox” path, as opposed to modeling a host master.
SwmbxCtrl never embeds the mailbox memory; it stores the buffer address (buffer_base) and reaches it through SharedRegion<T>, a thin wrapper that does volatile reads/writes through a NonNull<T>.
Why this exists: on the target, the buffer is a fixed physical/SRAM region shared with other masters, so the controller must reference it by address rather than own it.
Because raw addresses are involved, the soundness boundary is explicit:
SharedRegion::from_addr and SwmbxCtrl::new_with_regions are unsafe — the caller promises the address is valid, aligned, mapped, and uniquely owned. Each has a # Safety section spelling out the contract.send_msg, get_msg, swmbx_read/write, the update_* family) are safe — using the mailbox requires no further unsafe.from_addr additionally asserts non-null and correct alignment. For a compile-time-constant address these asserts become compile errors; for a runtime address they are runtime panics (a null/misaligned base is treated as an unrecoverable configuration bug, not a returnable error).SwmbxCtrl is not internally synchronized, and deliberately so. It is driven from a single execution context: the i2c service runs a cooperative event loop (object_wait over a WaitGroup) in which the hardware interrupt is just another wake-up — the IRQ only drains the slave-RX latch, interrupt_acks, and signals the client. The mailbox itself is then driven serially by one consumer (one IPC channel per bus); the IRQ never reentrantly touches controller state. Because of this:
&mut self; mbx_en is a plain u8 (no Cell, no atomics, no lock). The type is intentionally !Sync, so any accidental attempt to share one controller across contexts is a compile error rather than a silent data race.SharedRegion uses volatile accesses; the controller's own fields (mbx_en, node, fifo) live in firmware RAM and are touched only by the single consumer.SwmbxError is a #[repr(u16)] enum with stable diagnostic codes (code() → u16) suitable for firmware telemetry:
| Variant | Code | Raised when … |
|---|---|---|
InvalidPort | 0x1001 | port >= SWMBX_DEV_COUNT |
InvalidAddress | 0x1002 | address outside the buffer / node range |
InvalidFifoIndex | 0x1003 | index >= SWMBX_FIFO_COUNT |
InvalidFifoDepth | 0x1004 | configured depth is 0 or > SWMBX_FIFO_DEPTH |
InvalidFlagMask | 0x1005 | enable_behavior flag has no known bits |
InvalidProtectRange | 0x1006 | apply_protect slice out of range / overflows |
FifoFull | 0x1007 | append to a full FIFO |
FifoEmpty | 0x1008 | read from an empty FIFO |
| (0x1009 retired) | — | was NullRegionBase; null base is now a panic |
FifoNotMapped | 0x100A | direct FIFO access to an unmapped address |
Codes are an external ABI: 0x1009 is intentionally left reserved so existing code values don't shift.
const BUF_LEN: usize = 256; let mut backing = [0u8; BUF_LEN]; // SAFETY: `backing` is live, uniquely-owned memory for the controller's life. let mut ctrl = unsafe { SwmbxCtrl::new_with_regions(BUF_LEN, backing.as_mut_ptr() as usize, 0) }; // --- Flat register with write-protect + notify ------------------------------- ctrl.enable_behavior(SWMBX_PROTECT | SWMBX_NOTIFY, true)?; ctrl.update_protect(0, 0x10, true)?; // node 0x10 is read-only to port 0 ctrl.send_msg(0, 0x10, 0xAA)?; // dropped: stays 0x00 ctrl.update_protect(0, 0x10, false)?; ctrl.send_msg(0, 0x10, 0xAA)?; // now written assert_eq!(ctrl.get_msg(0, 0x10)?, 0xAA); // --- Streaming register backed by a FIFO ------------------------------------- ctrl.enable_behavior(SWMBX_FIFO, true)?; ctrl.update_fifo(0, 0x0D, 4, SWMBX_FIFO_NOTIFY_START | SWMBX_FIFO_NOTIFY_STOP, true)?; ctrl.send_start(0, 0x0D)?; // 0x0D resolves to FIFO 0 ctrl.send_msg(0, 0x0D, 0x11)?; ctrl.send_msg(0, 0x0D, 0x22)?; assert_eq!(ctrl.get_msg(0, 0x0D)?, 0x11); // pops in order assert_eq!(ctrl.get_msg(0, 0x0D)?, 0x22); assert_eq!(ctrl.get_msg(0, 0x0D), Err(SwmbxError::FifoEmpty)); ctrl.send_stop(0)?;
# Build the (target-only) library bazel build //target/ast10x0/pfr:pfr # Run the host unit tests bazel test //target/ast10x0/pfr:swmbx_ctrl_host_test
| Term | In code | Meaning |
|---|---|---|
| Port / device | port: usize, SWMBX_DEV_COUNT | One host master (e.g. BMC, PCH). |
| Node | SwmbxNode, indexed by addr: u8 | Per-address policy + event latch. |
| Buffer | buffer_base + SharedRegion | The flat 256-byte shared register file. |
| FIFO endpoint | SwmbxFifo, fifo[idx] | A queue bound to one address. |
| Transaction | send_start … send_stop | The window during which FIFO routing applies. |
| Global switch | mbx_en bits | Arms a whole feature class. |
| Per-node bit | SwmbxNode::enabled_flags | Arms a feature for one address. |
ASPEED Zephyr PFR (apps/aspeed-pfr)
┌──────────────────────────────────────────────────────────────────────────────┐
│ Application logic │
│ - AspeedStateMachine.c │
│ - mctp/plat_mctp.c, mctp/mctp_smbus.c │
│ | │
│ | calls swmbx_* API (enable/update/read/write/send_start/stop) │
│ v │
│ include/zephyr/drivers/i2c/pfr/swmbx.h │
└────────┬─────────────────────────────────────────────────────────────────────┘
│ API contract
v
┌──────────────────────────────────────────────────────────────────────────────┐
│ SW mailbox controller driver │
│ drivers/i2c/pfr/swmbx.c │
│ │
│ swmbx_ctrl_data: │
│ - mbx_en │
│ - node[2][256] (protect/notify bits + sem ptr) │
│ - fifo[4] (addr mapping, queue state, sem ptr) │
│ - buffer ptr -> SWMBX_BUF_BASE │
│ │
│ global indirection: │
│ swmbx_info @ SWMBX_INFO_BASE --> pointer to swmbx_ctrl_data │
└────────┬─────────────────────────────────────────────────────────────────────┘
│
│ data path (flat mailbox bytes)
v
┌──────────────────────────────────────────────────────────────────────────────┐
│ Shared mailbox memory │
│ SWMBX_BUF_BASE = 0x7e7b0e00 │
│ 256-byte register window (node 0x00..0xFF) │
└────────┬─────────────────────────────────────────────────────────────────────┘
│
│ transport path (I2C/SMBus target endpoints)
v
┌──────────────────────────────────────────────────────────────────────────────┐
│ I2C target side │
│ drivers/i2c/target/swmbx_target.c │
│ dts bindings: aspeed,swmbx-ctrl + aspeed,swmbx-dev │
│ board overlays instantiate swmbx0/swmbx1 endpoints │
└──────────────────────────────────────────────────────────────────────────────┘
Application layer PFR state machine and MCTP code call swmbx_* APIs to configure behavior (protect/notify/fifo) and move mailbox bytes.
Driver policy/state layer (swmbx.c) Holds all policy and runtime state (mbx_en, per-node flags, FIFO mapping, transaction state). This is where address-level protection, notify latching, and FIFO transaction semantics are implemented.
Shared memory layer (SWMBX_BUF_BASE) Backing storage for the flat mailbox register file (0x00..0xFF). Non-FIFO accesses read/write this byte array directly.
Transport layer (I2C target endpoints) swmbx_target.c and DT overlays expose SWMBX endpoints on SMBus/I2C so external masters (for example BMC and CPU/PCH) can drive mailbox traffic.
Cross-context handoff (SWMBX_INFO_BASE) The C implementation stores a pointer to swmbx_ctrl_data in shared info memory so non-device-handle entry points (swmbx_send_*) can locate the active controller state.
In short: application calls API -> driver enforces policy -> bytes land in shared mailbox memory -> transport exposes that memory to external SMBus/I2C masters.