blob: 0ee308c618400f3bfaf907612d15b9a4333240bb [file] [view]
<!-- Licensed under the Apache-2.0 license -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# AST10x0 PFR Crate (`ast10x0_pfr`)
This crate provides the AST10x0 PFR (Platform Firmware Resilience) building
blocks. Its current public surface is [`SwmbxCtrl`](./swmbx_ctrl.rs), 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.
---
## 1. The mental model
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) ---+
```
* A **port** is one host-side master. There are `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.
* An **address** (`u8`, `0x00..=0xFF`) selects one byte/register. Each address is
called a **node**; there are `SWMBX_NODE_COUNT = 256` nodes per port.
* Some addresses can be **remapped** away from the flat buffer to a **FIFO** so
that repeated writes to the same address stream into a queue instead of
overwriting one byte. There are `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).
---
## 2. State at a glance
```
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
```
### Global switches — `mbx_en`
A 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.
### Per-node policy — `SwmbxNode`
```rust
struct 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.
### A FIFO endpoint — `SwmbxFifo<N>`
```rust
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_flag` is a **bool event latch**,
> while `SwmbxFifo::notify_flag` is a **u8 configuration mask** of
> `SWMBX_FIFO_NOTIFY_START (1<<0)` / `SWMBX_FIFO_NOTIFY_STOP (1<<1)`. They are
> unrelated despite sharing a field name.
---
## 3. Configuration API
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 geometry
Protection 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.
---
## 4. The two data paths
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`.
---
## 5. Transaction lifecycle (FIFO path)
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`.
---
## 6. Write protection & notification (flat-buffer path)
When not in a FIFO transaction, `send_msg` runs the node policy:
* **Protection.** The byte is written to the buffer **unless** it is protected —
i.e. it is dropped only when *both* the node's `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.
* **Notification.** If the global `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_flag` on 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.
### Direct helpers
`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.
---
## 7. Shared buffer region and safety
`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.
* Once a controller has been constructed under that promise, the *accessor*
methods (`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).
### Concurrency model: single-context, fed serially over IPC
`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_ack`s, 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:
* All mutators take `&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.
* The two **ports** are *external* bus masters (e.g. BMC and PCH), not internal
threads. They race on the shared **buffer memory**, which is why `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.
---
## 8. Error model
`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.
---
## 9. Worked example
```rust
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)?;
```
---
## 10. Build & test
```sh
# 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
```
---
## 11. Quick reference: glossary
| 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. |
---
## 12. How SWMBX sits in Zephyr PFR
```
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 │
└──────────────────────────────────────────────────────────────────────────────┘
```
### Layering explained
1. **Application layer**
PFR state machine and MCTP code call `swmbx_*` APIs to configure behavior
(protect/notify/fifo) and move mailbox bytes.
2. **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.
3. **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.
4. **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.
5. **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**.