From 0bf80356a0939bf576889ddf60e6028109847805 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Mon, 4 May 2026 21:05:51 -0700 Subject: [PATCH 1/2] flash: consolidate flash API review updates --- MODULE.bazel.lock | 2 +- drivers/flash/api/BUILD.bazel | 23 ++ drivers/flash/api/README.md | 227 +++++++++++++ drivers/flash/api/src/backend.rs | 156 +++++++++ drivers/flash/api/src/lib.rs | 9 + drivers/flash/api/src/protocol.rs | 535 ++++++++++++++++++++++++++++++ drivers/flash/client/BUILD.bazel | 20 ++ drivers/flash/client/README.md | 173 ++++++++++ drivers/flash/client/src/lib.rs | 390 ++++++++++++++++++++++ 9 files changed, 1534 insertions(+), 1 deletion(-) create mode 100644 drivers/flash/api/BUILD.bazel create mode 100644 drivers/flash/api/README.md create mode 100644 drivers/flash/api/src/backend.rs create mode 100644 drivers/flash/api/src/lib.rs create mode 100644 drivers/flash/api/src/protocol.rs create mode 100644 drivers/flash/client/BUILD.bazel create mode 100644 drivers/flash/client/README.md create mode 100644 drivers/flash/client/src/lib.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0ecaa0c0..367e137c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -2698,7 +2698,7 @@ "bzlTransitiveDigest": "3kVt1CoIJVJiOv5hhXwKcnZWdFwwoEOhc/OeC2GrMQU=", "usagesDigest": "/Nz9QH+EBlDXoVXWiAEH8owsAo1cu+GkXjlRdolG7Sk=", "recordedFileInputs": { - "@@//third_party/crates_io/Cargo.lock": "bbc7b14d634194e289a36caa99b437e0c1b88162219fa5aa719a915fbe86c261", + "@@//third_party/crates_io/Cargo.lock": "3e43f044148e1cf344654ede226b070849ed6b56be5b1a013ee0a63c56718d85", "@@//third_party/crates_io/Cargo.toml": "e8006d3d6342dc0bf39342da3c6359fafa48e5660e085347f89a01cba529dbd0", "@@caliptra_deps+//crates_io/embedded/Cargo.lock": "d6c0101f48da22f2bc2d339f358de79bad3dd03218c6db29a14099b9f7757691", "@@caliptra_deps+//crates_io/embedded/Cargo.toml": "8f9f4ed2721db13476b12fdac045dac2142b38f189a8abb5f4c446dc0c6ac3dd", diff --git a/drivers/flash/api/BUILD.bazel b/drivers/flash/api/BUILD.bazel new file mode 100644 index 00000000..a8d75593 --- /dev/null +++ b/drivers/flash/api/BUILD.bazel @@ -0,0 +1,23 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "flash_api", + srcs = [ + "src/backend.rs", + "src/lib.rs", + "src/protocol.rs", + ], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "flash_api_test", + crate = ":flash_api", +) diff --git a/drivers/flash/api/README.md b/drivers/flash/api/README.md new file mode 100644 index 00000000..e57cd899 --- /dev/null +++ b/drivers/flash/api/README.md @@ -0,0 +1,227 @@ +# flash_api + +Shared wire protocol and backend trait for the OpenPRoT flash driver. + +Bazel target: `//drivers/flash/api:flash_api` + +## Purpose + +`flash_api` is the contract crate consumed by both sides of the flash +IPC boundary: + +- the userspace IPC facade ([`drivers/flash/client`](../client/)), which + serializes requests and parses responses, +- the platform server (out of tree in this review repo), which dispatches + opcodes onto a `FlashBackend` impl. + +It owns the on-wire byte layout, the opcode set, the error code map, +the discovery value types, and the backend trait surface. No transport, +no syscalls, no platform code — pure data definitions plus one trait. + +## Layer position + +``` +Application task + │ + ▼ +FlashClient ─────────► flash_api ◄───────── FlashServer + (wire types, + backend trait) + │ + ▼ + PlatformFlashBackend + │ + ▼ + SMC / FMC +``` + +## Glossary + +A few domain terms are used throughout this crate, the client, and the +server: + +**Backend** — the platform-side code that actually talks to flash +silicon. Implements the `FlashBackend` trait. There is exactly one +backend per physical controller (e.g. an `Ast10x0FlashBackend` for the +AST10x0 SMC/FMC). The backend is what gives meaning to a `Read` or an +`Erase`; the wire protocol just shuttles the request to it. + +**Geometry** — the *static shape* of a flash device, described by the +`FlashGeometry` value type. The wire payload exposes exactly these +fields: `capacity`, `page_size`, `erase_sizes` (bitmap), +`min_erase_align`, `address_width`, and opaque `flags`. + +**Route key** — backend-local selector type (`FlashBackend::RouteKey`) +used by the server runtime to route a channel-bound request to the +correct flash target. + +**Flags** — an opaque `u8` field in `FlashGeometry` reserved for +backend-defined hints. Clients should treat these bits as raw values +rather than stable protocol semantics. + +## Wire protocol + +### Frame layout + +Every request frame is a `FlashRequestHeader` (16 bytes, little-endian, +packed) followed by an opcode-specific payload of up to +`MAX_PAYLOAD_SIZE` (256) bytes. Every response frame is a +`FlashResponseHeader` (8 bytes, little-endian, packed) followed by an +opcode-specific payload of up to `MAX_PAYLOAD_SIZE` bytes. + +```rust +#[repr(C, packed)] +pub struct FlashRequestHeader { + pub op_code: u8, + pub flags: u8, + pub payload_len: u16, + pub address: u32, + pub length: u32, + pub reserved: u32, +} // = 16 bytes + +#[repr(C, packed)] +pub struct FlashResponseHeader { + pub status: u8, // 0 = Success; otherwise FlashError + pub reserved: u8, + pub payload_len: u16, + pub value: u32, // op-specific (capacity, byte count, ...) +} // = 8 bytes +``` + +Both headers derive `zerocopy::{FromBytes, IntoBytes, Immutable, +KnownLayout}` and ship `new`/`success`/`error` builders plus +little-endian-aware accessors (`address_value()`, `length_value()`, +`value_word()`, `payload_length()`, …) so neither side needs to +hand-roll byte twiddling. + +### Opcodes + +| Op | Value | Request shape | Response shape | +|---|---|---|---| +| `Exists` | 0x01 | header only | `value` = 0/1 | +| `GetCapacity` | 0x02 | header only | `value` = bytes | +| `Read` | 0x03 | header (`address`, `length`) | `value` = byte count, payload = bytes read | +| `Write` | 0x04 | header (`address`, `length`, `payload_len`) + payload | `value` = byte count | +| `Erase` | 0x05 | header (`address`, `length`) | empty | +| `GetGeometry` | 0x06 | header only | payload = `FlashGeometry` (24 B) | + +`MAX_PAYLOAD_SIZE` is a protocol constant: every backend honours the +same value, so clients reference it directly rather than querying for +it. + +## Discovery value types + +### `FlashGeometry` (24 B) + +Returned in the `GetGeometry` response payload. + +```rust +pub struct FlashGeometry { + pub capacity: u32, + pub page_size: u32, // write granularity (typically 256) + pub erase_sizes: u32, // bitmap; bit n set => 1 << n bytes supported + pub min_erase_align: u32, + pub address_width: u8, // 3 or 4 + pub flags: u8, // opaque backend-defined bits + pub _rsv: [u8; 6], +} +``` + +`erase_sizes` as a bitmap lets the client pick the largest aligned +erase opcode per stride (e.g. 4 KiB | 32 KiB | 64 KiB = +`(1<<12) | (1<<15) | (1<<16)`). + +`flags` is intentionally opaque at the protocol layer. + +## Backend traits + +```rust +pub trait FlashBackend { + type RouteKey: Copy; + + fn info(&self, key: Self::RouteKey) -> FlashInfo; + + fn exists(&mut self, key: Self::RouteKey) + -> Result; // default Ok(true) + + fn read (&mut self, key: Self::RouteKey, address: u32, out: &mut [u8]) + -> Result; + fn write(&mut self, key: Self::RouteKey, address: u32, data: &[u8]) + -> Result; + fn erase(&mut self, key: Self::RouteKey, address: u32, length: u32) + -> Result<(), BackendError>; + + fn enable_interrupts (&mut self) -> Result<(), BackendError>; + fn disable_interrupts(&mut self) -> Result<(), BackendError>; +} + +pub trait FlashGeometryProvider: FlashBackend { + fn geometry(&self, key: Self::RouteKey) + -> Result; // default derives from info() +} +``` + +`FlashBackend` is the minimal data-plane contract. Geometry discovery +is split into the composable `FlashGeometryProvider` extension trait. +Server code that serves `GetGeometry` should bound by +`FlashGeometryProvider`. + +Discovery methods (`info`, `geometry`) take `&self` — they report +static authoring on the server side and don't need exclusive access. +`geometry` ships a default impl so existing single-erase-granule +backends stay source-compatible without writing boilerplate. + +`RouteKey` is an associated type. Single-CS backends set it to `()`; +multi-CS controllers set it to a chip-select index. Channel-implicit +routing keeps the wire header free of routing fields — each +`FlashClient` is bound to one CS via its IPC handle, and the server +maps channel → backend → `RouteKey`. + +## Errors + +`FlashError` is the wire status code carried in +`FlashResponseHeader::status`: + +| Variant | Code | Meaning | +|---|---|---| +| `Success` | 0x00 | OK | +| `InvalidOperation` | 0x01 | Unknown opcode | +| `InvalidAddress` | 0x02 | Address out of range | +| `InvalidLength` | 0x03 | Length zero, overflow, or misaligned | +| `BufferTooSmall` | 0x04 | Server-side buffer constraint | +| `Busy` | 0x05 | Backend busy | +| `Timeout` | 0x06 | Operation timed out | +| `WouldBlock` | 0x07 | Could not complete synchronously; retry after IRQ | +| `IoError` | 0x08 | Media-level failure | +| `NotPermitted` | 0x09 | Blocked by backend policy/protection | +| `InternalError` | 0xFF | Unclassified server fault | + +`BackendError` is the trait-level error backends produce; an `impl +From for FlashError` provides the canonical mapping for +the server's response-encoding path. + +## Tests + +Host-side unit tests cover each wire type at the encoder/decoder +level: opcode and error-code round-trips (known values + unknown-byte +fallthrough), `new`-and-accessor round-trips for the request and +response headers as well as `FlashGeometry`, +explicit little-endian byte-position asserts, and short-buffer +rejection on header decode. + +``` +bazel test //drivers/flash/api:flash_api_test +``` + +## Constraints + +- `no_std` — no allocator, no I/O. +- Pure data + one trait. No syscalls, no clocks, no platform deps. +- Host-buildable — picked up by the CI `//...` wildcard. + +## Dependencies + +| Crate | Role | +|---|---| +| `zerocopy` | `FromBytes` / `IntoBytes` derives on wire structs | diff --git a/drivers/flash/api/src/backend.rs b/drivers/flash/api/src/backend.rs new file mode 100644 index 00000000..3a9cd873 --- /dev/null +++ b/drivers/flash/api/src/backend.rs @@ -0,0 +1,156 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Backend trait that platform flash drivers implement. +//! +//! The shape mirrors the `FlashStorage` HIL from caliptra-mcu-sw but is +//! synchronous and buffer-borrowing rather than callback-based: the +//! server runtime drives concurrency, so backends only need to expose +//! a blocking-or-`WouldBlock` surface. + +use crate::protocol::{FlashError, FlashGeometry}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BackendError { + InvalidOperation, + InvalidAddress, + InvalidLength, + BufferTooSmall, + Busy, + Timeout, + /// Backend cannot complete synchronously at this time; the server + /// runtime should retry after `OPERATION_COMPLETE` fires. + WouldBlock, + /// Media-level failure (program/erase verify fail, ECC uncorrectable, …). + IoError, + /// Operation is blocked by backend policy or protection state. + NotPermitted, + InternalError, +} + +impl From for FlashError { + fn from(value: BackendError) -> Self { + match value { + BackendError::InvalidOperation => FlashError::InvalidOperation, + BackendError::InvalidAddress => FlashError::InvalidAddress, + BackendError::InvalidLength => FlashError::InvalidLength, + BackendError::BufferTooSmall => FlashError::BufferTooSmall, + BackendError::Busy => FlashError::Busy, + BackendError::Timeout => FlashError::Timeout, + BackendError::WouldBlock => FlashError::WouldBlock, + BackendError::IoError => FlashError::IoError, + BackendError::NotPermitted => FlashError::NotPermitted, + BackendError::InternalError => FlashError::InternalError, + } + } +} + +/// Static description of the flash device a backend exposes. Reported +/// to clients via `GetCapacity`. +/// +/// The per-call payload cap is *not* part of `FlashInfo`: it is fixed +/// by the protocol (`MAX_PAYLOAD_SIZE`) and the same for every +/// backend. Clients reference the constant directly. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FlashInfo { + /// Total addressable bytes [0, capacity). + pub capacity: u32, + /// Smallest erasable unit, in bytes. Erase requests must be aligned + /// and sized in multiples of this value. + pub erase_size: u32, +} + +/// Core backend operations required by the flash server runtime. +/// +/// This trait is intentionally minimal: it captures the data-plane +/// operations and minimal static information (`FlashInfo`) needed to +/// service read/write/erase traffic. +pub trait FlashBackend { + /// Per-call routing key. Single-CS backends set this to `()`; multi-CS + /// backends set it to a CS index (e.g. `ChipSelect`) so the server + /// runtime can dispatch each channel to the right device on a shared + /// controller. + type RouteKey: Copy; + + /// Static layout/features of the device selected by `key`. + fn info(&self, key: Self::RouteKey) -> FlashInfo; + + /// Probe whether the flash device selected by `key` is present and + /// responsive. + /// + /// Default implementation assumes presence so existing backends remain + /// source-compatible until they opt into a hardware-backed probe. + fn exists(&mut self, _key: Self::RouteKey) -> Result { + Ok(true) + } + + /// Read up to `out.len()` bytes from the device selected by `key`, + /// starting at the device-relative `address`, into `out`. Returns the + /// number of bytes actually read. + fn read( + &mut self, + key: Self::RouteKey, + address: u32, + out: &mut [u8], + ) -> Result; + + /// Write `data` to the device selected by `key`, starting at the + /// device-relative `address`. Returns the number of bytes actually + /// written. + fn write( + &mut self, + key: Self::RouteKey, + address: u32, + data: &[u8], + ) -> Result; + + /// Erase `length` bytes on the device selected by `key`, starting at + /// the device-relative `address`. Both must be multiples of + /// `FlashInfo::erase_size`. + fn erase( + &mut self, + key: Self::RouteKey, + address: u32, + length: u32, + ) -> Result<(), BackendError>; + + fn enable_interrupts(&mut self) -> Result<(), BackendError>; + + fn disable_interrupts(&mut self) -> Result<(), BackendError>; +} + +/// Optional geometry-discovery extension for [`FlashBackend`]. +/// +/// Server code that exposes the `GetGeometry` opcode should bound the +/// backend type by this trait. A blanket impl provides a default +/// geometry derived from [`FlashBackend::info`], so simple backends do +/// not need to add any extra code. +pub trait FlashGeometryProvider: FlashBackend { + /// Wire-shaped geometry for the device selected by `key`. Powers the + /// `GetGeometry` opcode. + /// + /// Default derives from `info()`: a single erase granularity (the + /// one already advertised in `FlashInfo`), 256-byte page, + /// address-width inferred from capacity, no opaque flags. A + /// backend that supports multiple erase granules (4 K + 64 K, etc.) + /// or has backend-defined opaque flag bits should override. + fn geometry(&self, key: Self::RouteKey) -> Result { + let info = self.info(key); + let erase_bitmap = if info.erase_size != 0 && info.erase_size.is_power_of_two() { + info.erase_size + } else { + 0 + }; + let address_width: u8 = if info.capacity > 0x0100_0000 { 4 } else { 3 }; + Ok(FlashGeometry::new( + info.capacity, + 256, + erase_bitmap, + info.erase_size, + address_width, + 0, + )) + } +} + +impl FlashGeometryProvider for T {} diff --git a/drivers/flash/api/src/lib.rs b/drivers/flash/api/src/lib.rs new file mode 100644 index 00000000..51fd720a --- /dev/null +++ b/drivers/flash/api/src/lib.rs @@ -0,0 +1,9 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +pub mod backend; +pub mod protocol; + +pub use protocol::*; diff --git a/drivers/flash/api/src/protocol.rs b/drivers/flash/api/src/protocol.rs new file mode 100644 index 00000000..31a207ec --- /dev/null +++ b/drivers/flash/api/src/protocol.rs @@ -0,0 +1,535 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Wire protocol for the flash driver IPC channel. +//! +//! The operation set mirrors the flash storage HIL used in caliptra-mcu-sw +//! (`runtime/kernel/drivers/flash`), reframed as an opcode + packed-header +//! protocol matching the conventions of the other OpenPRoT userspace +//! drivers (see `drivers/usart/api`). + +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +/// Maximum payload bytes carried in a single request or response. +/// +/// Larger logical I/O is split into chunks by the client. This is a +/// protocol constant — every backend honours the same value, so clients +/// can reference it directly rather than querying the server. +pub const MAX_PAYLOAD_SIZE: usize = 256; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FlashOp { + /// Probe the driver. Response carries no value. + Exists = 0x01, + /// Total bytes of flash exposed by this backend. Result in `value`. + GetCapacity = 0x02, + /// Read `length` bytes starting at `address`. Response payload carries + /// the bytes read; `value` is the byte count. + Read = 0x03, + /// Write the request payload (`payload_len` bytes) starting at + /// `address`. `length` must equal `payload_len`. `value` returns the + /// byte count actually written. + Write = 0x04, + /// Erase `length` bytes starting at `address`. + Erase = 0x05, + /// Discover device geometry — capacity, page size, supported erase + /// granularities, address width, opaque flags. Response carries + /// one `FlashGeometry` record in the payload. The response-header + /// `value` word is unused for this opcode (set to 0 and ignored). + GetGeometry = 0x06, +} + +impl TryFrom for FlashOp { + type Error = FlashError; + + fn try_from(value: u8) -> Result { + match value { + 0x01 => Ok(Self::Exists), + 0x02 => Ok(Self::GetCapacity), + 0x03 => Ok(Self::Read), + 0x04 => Ok(Self::Write), + 0x05 => Ok(Self::Erase), + 0x06 => Ok(Self::GetGeometry), + _ => Err(FlashError::InvalidOperation), + } + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FlashError { + Success = 0x00, + InvalidOperation = 0x01, + InvalidAddress = 0x02, + InvalidLength = 0x03, + BufferTooSmall = 0x04, + Busy = 0x05, + Timeout = 0x06, + /// Operation cannot complete right now; the server/runtime may defer + /// completion until the backend signals progress via interrupt. + WouldBlock = 0x07, + /// Underlying media reported an I/O error (e.g. flash program failure). + IoError = 0x08, + /// Address/length straddles a region the backend refuses to touch + /// (e.g. write-protected partition). + NotPermitted = 0x09, + InternalError = 0xFF, +} + +impl From for FlashError { + fn from(value: u8) -> Self { + match value { + 0x00 => Self::Success, + 0x01 => Self::InvalidOperation, + 0x02 => Self::InvalidAddress, + 0x03 => Self::InvalidLength, + 0x04 => Self::BufferTooSmall, + 0x05 => Self::Busy, + 0x06 => Self::Timeout, + 0x07 => Self::WouldBlock, + 0x08 => Self::IoError, + 0x09 => Self::NotPermitted, + _ => Self::InternalError, + } + } +} + +/// Request header on the wire. 16 bytes, little-endian, packed. +/// +/// `address` and `length` are interpreted per `op_code`; see `FlashOp`. +/// `payload_len` is the number of bytes that immediately follow this +/// header in the request frame (zero for read/erase/probe ops). +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct FlashRequestHeader { + pub op_code: u8, + pub flags: u8, + pub payload_len: u16, + pub address: u32, + pub length: u32, + pub reserved: u32, +} + +impl FlashRequestHeader { + pub const SIZE: usize = 16; + + pub fn new(op: FlashOp, address: u32, length: u32, payload_len: u16) -> Self { + Self { + op_code: op as u8, + flags: 0, + payload_len: payload_len.to_le(), + address: address.to_le(), + length: length.to_le(), + reserved: 0, + } + } + + pub fn operation(&self) -> Result { + FlashOp::try_from(self.op_code) + } + + pub fn address_value(&self) -> u32 { + u32::from_le(self.address) + } + + pub fn length_value(&self) -> u32 { + u32::from_le(self.length) + } + + pub fn payload_length(&self) -> usize { + u16::from_le(self.payload_len) as usize + } +} + +/// Response header on the wire. 8 bytes, little-endian, packed. +/// +/// `value` is a per-op return word — capacity, chunk size, bytes +/// processed, etc. `payload_len` counts bytes that follow this header +/// (non-zero only for `Read`). +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct FlashResponseHeader { + pub status: u8, + pub reserved: u8, + pub payload_len: u16, + pub value: u32, +} + +impl FlashResponseHeader { + pub const SIZE: usize = 8; + + pub fn success(value: u32, payload_len: u16) -> Self { + Self { + status: FlashError::Success as u8, + reserved: 0, + payload_len: payload_len.to_le(), + value: value.to_le(), + } + } + + pub fn error(error: FlashError) -> Self { + Self { + status: error as u8, + reserved: 0, + payload_len: 0, + value: 0, + } + } + + pub fn is_success(&self) -> bool { + self.status == FlashError::Success as u8 + } + + pub fn error_code(&self) -> FlashError { + FlashError::from(self.status) + } + + pub fn value_word(&self) -> u32 { + u32::from_le(self.value) + } + + pub fn payload_length(&self) -> usize { + u16::from_le(self.payload_len) as usize + } +} + +/// Static device geometry returned in the `GetGeometry` response payload. +/// 24 bytes, little-endian, packed. +/// +/// `erase_sizes` is a bitmap: bit `n` set means the backend supports an +/// erase opcode of `1 << n` bytes (e.g. 4 KiB | 32 KiB | 64 KiB = +/// `(1<<12) | (1<<15) | (1<<16)`). +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct FlashGeometry { + pub capacity: u32, + pub page_size: u32, + pub erase_sizes: u32, + pub min_erase_align: u32, + pub address_width: u8, + pub flags: u8, + pub _rsv: [u8; 6], +} + +impl FlashGeometry { + pub const SIZE: usize = 24; + + pub fn new( + capacity: u32, + page_size: u32, + erase_sizes: u32, + min_erase_align: u32, + address_width: u8, + flags: u8, + ) -> Self { + Self { + capacity: capacity.to_le(), + page_size: page_size.to_le(), + erase_sizes: erase_sizes.to_le(), + min_erase_align: min_erase_align.to_le(), + address_width, + flags, + _rsv: [0; 6], + } + } + + pub fn capacity_value(&self) -> u32 { + u32::from_le(self.capacity) + } + + pub fn page_size_value(&self) -> u32 { + u32::from_le(self.page_size) + } + + pub fn erase_sizes_bitmap(&self) -> u32 { + u32::from_le(self.erase_sizes) + } + + pub fn min_erase_align_value(&self) -> u32 { + u32::from_le(self.min_erase_align) + } + + pub fn raw_flags(&self) -> u8 { + self.flags + } +} + +#[cfg(test)] +mod tests { + use super::*; + extern crate std; + use zerocopy::{FromBytes, IntoBytes}; + + #[test] + fn flash_op_try_from_known_codes() { + let cases = [ + (0x01u8, FlashOp::Exists), + (0x02, FlashOp::GetCapacity), + (0x03, FlashOp::Read), + (0x04, FlashOp::Write), + (0x05, FlashOp::Erase), + (0x06, FlashOp::GetGeometry), + ]; + for (byte, op) in cases { + assert_eq!(FlashOp::try_from(byte).unwrap(), op); + assert_eq!(op as u8, byte); + } + } + + #[test] + fn flash_op_try_from_unknown_is_invalid_operation() { + for byte in [0x00u8, 0x07, 0x08, 0x09, 0x10, 0x42, 0xFF] { + assert_eq!( + FlashOp::try_from(byte).unwrap_err(), + FlashError::InvalidOperation + ); + } + } + + #[test] + fn flash_error_from_known_codes_round_trip() { + let cases = [ + (0x00u8, FlashError::Success), + (0x01, FlashError::InvalidOperation), + (0x02, FlashError::InvalidAddress), + (0x03, FlashError::InvalidLength), + (0x04, FlashError::BufferTooSmall), + (0x05, FlashError::Busy), + (0x06, FlashError::Timeout), + (0x07, FlashError::WouldBlock), + (0x08, FlashError::IoError), + (0x09, FlashError::NotPermitted), + (0xFF, FlashError::InternalError), + ]; + for (byte, err) in cases { + assert_eq!(FlashError::from(byte), err); + assert_eq!(err as u8, byte); + } + } + + #[test] + fn flash_error_from_unknown_byte_is_internal_error() { + for byte in [0x0Au8, 0x10, 0x42, 0x80, 0xFE] { + assert_eq!(FlashError::from(byte), FlashError::InternalError); + } + } + + #[test] + fn request_header_size_matches_const_and_wire_layout() { + assert_eq!(core::mem::size_of::(), 16); + assert_eq!(FlashRequestHeader::SIZE, 16); + let hdr = FlashRequestHeader::new(FlashOp::Read, 0, 0, 0); + assert_eq!(hdr.as_bytes().len(), 16); + } + + #[test] + fn request_header_new_then_accessors() { + let hdr = FlashRequestHeader::new(FlashOp::Read, 0xDEAD_BEEF, 0x10, 0); + assert_eq!(hdr.operation().unwrap(), FlashOp::Read); + assert_eq!(hdr.address_value(), 0xDEAD_BEEF); + assert_eq!(hdr.length_value(), 0x10); + assert_eq!(hdr.payload_length(), 0); + + let hdr = FlashRequestHeader::new(FlashOp::Write, 0x4000, 0x80, 0x80); + assert_eq!(hdr.operation().unwrap(), FlashOp::Write); + assert_eq!(hdr.address_value(), 0x4000); + assert_eq!(hdr.length_value(), 0x80); + assert_eq!(hdr.payload_length(), 0x80); + } + + #[test] + fn request_header_encodes_little_endian_on_wire() { + let hdr = FlashRequestHeader::new(FlashOp::Read, 0x0403_0201, 0x0807_0605, 0x0A09); + let bytes = hdr.as_bytes(); + assert_eq!(bytes[0], FlashOp::Read as u8); + assert_eq!(bytes[1], 0); + assert_eq!(&bytes[2..4], &[0x09, 0x0A]); + assert_eq!(&bytes[4..8], &[0x01, 0x02, 0x03, 0x04]); + assert_eq!(&bytes[8..12], &[0x05, 0x06, 0x07, 0x08]); + assert_eq!(&bytes[12..16], &[0, 0, 0, 0]); + } + + #[test] + fn request_header_round_trip_through_bytes() { + let original = FlashRequestHeader::new(FlashOp::Erase, 0xCAFE_BABE, 0x1000, 0); + let bytes = original.as_bytes(); + let decoded = FlashRequestHeader::ref_from_bytes(bytes).unwrap(); + assert_eq!(decoded.operation().unwrap(), FlashOp::Erase); + assert_eq!(decoded.address_value(), 0xCAFE_BABE); + assert_eq!(decoded.length_value(), 0x1000); + assert_eq!(decoded.payload_length(), 0); + } + + #[test] + fn request_header_decode_invalid_op_byte_surfaces_error() { + let mut bytes = [0u8; 16]; + bytes[0] = 0xAB; + let hdr = FlashRequestHeader::ref_from_bytes(&bytes[..]).unwrap(); + assert_eq!(hdr.operation().unwrap_err(), FlashError::InvalidOperation); + } + + #[test] + fn request_header_decode_short_buffer_fails() { + let bytes = [0u8; 15]; + assert!(FlashRequestHeader::ref_from_bytes(&bytes[..]).is_err()); + } + + #[test] + fn response_header_size_matches_const_and_wire_layout() { + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(FlashResponseHeader::SIZE, 8); + let hdr = FlashResponseHeader::success(0, 0); + assert_eq!(hdr.as_bytes().len(), 8); + } + + #[test] + fn response_header_success_builder_matches_accessors() { + let hdr = FlashResponseHeader::success(0x1234_5678, 0x80); + assert!(hdr.is_success()); + assert_eq!(hdr.error_code(), FlashError::Success); + assert_eq!(hdr.value_word(), 0x1234_5678); + assert_eq!(hdr.payload_length(), 0x80); + } + + #[test] + fn response_header_error_builder_zeroes_payload_and_value() { + let hdr = FlashResponseHeader::error(FlashError::IoError); + assert!(!hdr.is_success()); + assert_eq!(hdr.error_code(), FlashError::IoError); + assert_eq!(hdr.value_word(), 0); + assert_eq!(hdr.payload_length(), 0); + } + + #[test] + fn response_header_encodes_little_endian_on_wire() { + let hdr = FlashResponseHeader::success(0x0807_0605, 0x0403); + let bytes = hdr.as_bytes(); + assert_eq!(bytes[0], FlashError::Success as u8); + assert_eq!(bytes[1], 0); + assert_eq!(&bytes[2..4], &[0x03, 0x04]); + assert_eq!(&bytes[4..8], &[0x05, 0x06, 0x07, 0x08]); + } + + #[test] + fn response_header_round_trip_through_bytes() { + for err in [ + FlashError::Success, + FlashError::InvalidAddress, + FlashError::WouldBlock, + FlashError::NotPermitted, + FlashError::InternalError, + ] { + let original = if matches!(err, FlashError::Success) { + FlashResponseHeader::success(0xAA, 0x55) + } else { + FlashResponseHeader::error(err) + }; + let bytes = original.as_bytes(); + let decoded = FlashResponseHeader::ref_from_bytes(bytes).unwrap(); + assert_eq!(decoded.error_code(), err); + assert_eq!(decoded.is_success(), matches!(err, FlashError::Success)); + if !matches!(err, FlashError::Success) { + assert_eq!(decoded.value_word(), 0); + assert_eq!(decoded.payload_length(), 0); + } + } + } + + #[test] + fn get_geometry_response_header_uses_payload_not_value_word() { + let hdr = FlashResponseHeader::success(0, FlashGeometry::SIZE as u16); + assert!(hdr.is_success()); + assert_eq!(hdr.value_word(), 0); + assert_eq!(hdr.payload_length(), FlashGeometry::SIZE); + } + + #[test] + fn response_header_decode_short_buffer_fails() { + let bytes = [0u8; 7]; + assert!(FlashResponseHeader::ref_from_bytes(&bytes[..]).is_err()); + } + + #[test] + fn max_payload_size_is_protocol_constant() { + assert_eq!(MAX_PAYLOAD_SIZE, 256); + } + + #[test] + fn flash_geometry_size_matches_const_and_wire_layout() { + assert_eq!(core::mem::size_of::(), 24); + assert_eq!(FlashGeometry::SIZE, 24); + let geom = FlashGeometry::new(0, 0, 0, 0, 3, 0); + assert_eq!(geom.as_bytes().len(), 24); + } + + #[test] + fn flash_geometry_new_then_accessors() { + let geom = FlashGeometry::new( + 0x0100_0000, + 256, + (1u32 << 12) | (1u32 << 15) | (1u32 << 16), + 4096, + 3, + 0x03, + ); + assert_eq!(geom.capacity_value(), 0x0100_0000); + assert_eq!(geom.page_size_value(), 256); + assert_eq!( + geom.erase_sizes_bitmap(), + (1u32 << 12) | (1u32 << 15) | (1u32 << 16) + ); + assert_eq!(geom.min_erase_align_value(), 4096); + assert_eq!(geom.address_width, 3); + assert_eq!(geom.raw_flags(), 0x03); + } + + #[test] + fn flash_geometry_encodes_little_endian_on_wire() { + let geom = FlashGeometry::new( + 0x0403_0201, + 0x0807_0605, + 0x0C0B_0A09, + 0x100F_0E0D, + 0x11, + 0x01, + ); + let bytes = geom.as_bytes(); + assert_eq!(&bytes[0..4], &[0x01, 0x02, 0x03, 0x04]); + assert_eq!(&bytes[4..8], &[0x05, 0x06, 0x07, 0x08]); + assert_eq!(&bytes[8..12], &[0x09, 0x0A, 0x0B, 0x0C]); + assert_eq!(&bytes[12..16], &[0x0D, 0x0E, 0x0F, 0x10]); + assert_eq!(bytes[16], 0x11); // address_width + assert_eq!(bytes[17], 0x01); // flags = DMA_ELIGIBLE + assert_eq!(&bytes[18..24], &[0; 6]); + } + + #[test] + fn flash_geometry_round_trip_through_bytes() { + let original = FlashGeometry::new( + 0x0080_0000, + 512, + 1u32 << 16, + 65536, + 4, + 0x02, + ); + let bytes = original.as_bytes(); + let decoded = FlashGeometry::ref_from_bytes(bytes).unwrap(); + assert_eq!(decoded.capacity_value(), 0x0080_0000); + assert_eq!(decoded.page_size_value(), 512); + assert_eq!(decoded.erase_sizes_bitmap(), 1u32 << 16); + assert_eq!(decoded.min_erase_align_value(), 65536); + assert_eq!(decoded.address_width, 4); + assert_eq!(decoded.raw_flags(), 0x02); + } + + #[test] + fn flash_geometry_raw_flags_round_trip_unknown_bits() { + let mut bytes = [0u8; FlashGeometry::SIZE]; + bytes[17] = 0xFF; // all flag bits including unknown + let geom = FlashGeometry::ref_from_bytes(&bytes[..]).unwrap(); + assert_eq!(geom.raw_flags(), 0xFF); + } + +} diff --git a/drivers/flash/client/BUILD.bazel b/drivers/flash/client/BUILD.bazel new file mode 100644 index 00000000..8e80b37d --- /dev/null +++ b/drivers/flash/client/BUILD.bazel @@ -0,0 +1,20 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +rust_library( + name = "flash_client", + srcs = ["src/lib.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash/api:flash_api", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:zerocopy", + ], +) diff --git a/drivers/flash/client/README.md b/drivers/flash/client/README.md new file mode 100644 index 00000000..3630ead4 --- /dev/null +++ b/drivers/flash/client/README.md @@ -0,0 +1,173 @@ +# flash_client + +Userspace IPC client facade for the flash driver. + +Bazel target: `//drivers/flash/client:flash_client` + +## Purpose + +`flash_client` gives any userspace task a simple, blocking API to read, +write, erase, and discover flash over a Pigweed IPC channel. It handles +all request serialization, response parsing, and error mapping. It has +no knowledge of the underlying hardware — that lives in the platform +backend on the server side. + +## Layer position + +``` +Application task + │ + ▼ +FlashClient ← this crate + │ channel_transact (Pigweed IPC) + ▼ +flash_server → PlatformFlashBackend → SMC / FMC controller +``` + +See [`drivers/flash/api`](../api/) for the wire types and backend trait +shared with the server. The api README also defines the domain +vocabulary used here (*backend*, *geometry*, *route key*, +*flags*). + +## API + +### Construction + +```rust +let mut flash = FlashClient::new(handle); // no default timeout +// or with a per-instance default: +let mut flash = FlashClient::with_default_timeout( + handle, + Some(Duration::from_secs(2)), +); +flash.set_default_timeout(Some(Duration::from_millis(500))); +``` + +`handle: u32` is a Pigweed IPC channel handle the platform binding +hands the task. The client takes `&mut self` on every method that +touches the wire — the synchronous one-call-in-flight invariant is +enforced at the type level. Each instance owns 1 KiB of scratch +(req + resp); per-call stack overhead is ~0. + +The default-timeout knob is the policy applied by every method that +doesn't accept an explicit timeout. `None` means block until the server +responds; any concrete `Duration` bounds the call. The OS clock type +does not appear on the public API surface — only `core::time::Duration`. + +### Probe + +```rust +if flash.exists()? { + // backend reports a responsive device on this handle +} +``` + +Returns `Ok(true)` when the backend reports a present device, +`Ok(false)` when it reports absence. + +### Geometry + +```rust +let bytes = flash.capacity()?; // total flash size in bytes +let chunk = FlashClient::chunk_size(); // protocol const, no IPC + +let geom = flash.geometry()?; +let granules = geom.erase_sizes_bitmap(); // bit n set => 1 << n is supported +let smallest = geom.min_erase_align_value(); +let width = geom.address_width; // 3 or 4 +let flags = geom.raw_flags(); // opaque u8 +``` + +`chunk_size()` is a `const fn` returning `MAX_PAYLOAD_SIZE` — the +per-call payload cap is a protocol constant, identical for every +backend, so no IPC is issued. + +`geometry()` issues one IPC and returns the full `FlashGeometry` +record (capacity, page size, supported-erase-size bitmap, address +width, opaque flags). A client that needs to support multiple +flash chip vendors (Macronix, Winbond, Micron, ISSI, …) consumes +`erase_sizes_bitmap()` to pick the largest aligned erase opcode per +stride, instead of hard-coding the granule per board. + +### Read + +```rust +let mut buf = [0u8; 256]; +let n = flash.read(address, &mut buf)?; +// or with an explicit timeout for this one call: +let n = flash.read_with_timeout(address, &mut buf, Some(Duration::from_millis(50)))?; +``` + +`buf.len()` must be ≤ `FlashClient::chunk_size()`. For reads larger +than one chunk, the caller is responsible for issuing multiple calls +and advancing the address. + +### Write + +```rust +let written = flash.write(address, &data[..])?; +// or with an explicit timeout: +let written = flash.write_with_timeout(address, &data[..], Some(Duration::from_millis(50)))?; +``` + +`data.len()` must be ≤ `FlashClient::chunk_size()`. + +### Erase + +```rust +flash.erase(address, length)?; +// or with an explicit timeout: +flash.erase_with_timeout(address, length, Some(Duration::from_secs(1)))?; +``` + +Both `address` and `length` must be aligned to and a multiple of one of +the granules advertised by `flash.geometry()?.erase_sizes_bitmap()`. + +## Error handling + +```rust +pub enum ClientError { + IpcError(pw_status::Error), // transport-level failure + ServerError(FlashError), // server returned a flash error code + InvalidResponse, // response frame is malformed + BufferTooSmall, // caller buffer exceeds MAX_PAYLOAD_SIZE +} +``` + +`FlashError` variants (defined in `flash_api`): + +| Variant | Meaning | +|---|---| +| `InvalidOperation` | Unrecognised opcode | +| `InvalidAddress` | Address out of range | +| `InvalidLength` | Length zero, overflow, or misaligned | +| `BufferTooSmall` | Server-side buffer constraint | +| `Busy` | Backend busy | +| `Timeout` | Operation timed out | +| `WouldBlock` | Operation could not be completed immediately | +| `IoError` | Media-level failure | +| `NotPermitted` | Blocked by backend policy/protection | +| `InternalError` | Unclassified server fault | + +### `Busy` vs `WouldBlock` + +- **`Busy`**: The backend is actively performing another operation and cannot accept new requests. Retry the entire call after waiting. +- **`WouldBlock`**: This specific operation could not be completed immediately due to resource constraints or contention, but is not blocked by global device activity. Callers can implement smarter retry logic or adjust request parameters. + +## Constraints + +- `no_std` — targets Pigweed kernel userspace tasks only. +- Bazel `target_compatible_with` is scoped to AST10x0 targets (`TARGET_COMPATIBLE_WITH`). +- Single call is limited to `MAX_PAYLOAD_SIZE` (256 bytes) per transaction. +- All calls are synchronous / blocking on `channel_transact`. +- One in-flight call per `FlashClient` instance (enforced by `&mut self`). + +## Dependencies + +| Crate | Role | +|---|---| +| `flash_api` | Wire types (`FlashOp`, headers, `FlashGeometry`, `FlashError`) | +| `userspace` (Pigweed) | `syscall::channel_transact`, internal kernel-deadline conversion | +| `pw_status` | IPC transport error type | +| `zerocopy` | Zero-copy header / geometry deserialization | +| `core::time::Duration` | Public-surface timeout type (no OS clock leaks into the API) | diff --git a/drivers/flash/client/src/lib.rs b/drivers/flash/client/src/lib.rs new file mode 100644 index 00000000..d451de04 --- /dev/null +++ b/drivers/flash/client/src/lib.rs @@ -0,0 +1,390 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use core::time::Duration; + +use flash_api::{ + FlashError, FlashGeometry, FlashOp, FlashRequestHeader, FlashResponseHeader, +}; +pub use flash_api::MAX_PAYLOAD_SIZE; +use userspace::syscall; +use userspace::time::{Duration as KDuration, Instant, SystemClock}; +use zerocopy::FromBytes; + +/// Minimum buffer size required for Flash protocol messages. +/// Clients must provide request and response buffers of at least this size. +pub const MIN_BUFFER_SIZE: usize = 512; + +/// Convenience macro to call a `_with_timeout` method using the client's default timeout. +/// +/// Usage: `with_default_timeout!(self, method_name, arg1, arg2, ...)` +macro_rules! with_default_timeout { + ($self:expr, $method:ident) => {{ + let timeout = $self.default_timeout; + $self.$method(timeout) + }}; + ($self:expr, $method:ident, $($arg:expr),+) => {{ + let timeout = $self.default_timeout; + $self.$method($($arg,)* timeout) + }}; +} + +/// Convert a public `Option` into the kernel +/// `Instant` deadline used by the IPC syscall. `None` and any duration +/// that would overflow the clock both saturate to `Instant::MAX` +/// (block-forever). Kept private so the kernel clock type does not +/// appear in the public API. +fn deadline_from(timeout: Option) -> Instant { + let Some(d) = timeout else { return Instant::MAX }; + let millis = d.as_millis().min(i64::MAX as u128) as i64; + let kd = KDuration::from_millis(millis); + SystemClock::now() + .checked_add_duration(kd) + .unwrap_or(Instant::MAX) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClientError { + IpcError(pw_status::Error), + ServerError(FlashError), + InvalidResponse, + BufferTooSmall, +} + +impl From for ClientError { + fn from(e: pw_status::Error) -> Self { + Self::IpcError(e) + } +} + +/// Flash driver client for performing read, write, erase, and discovery operations. +/// +/// This client uses caller-provided buffers for request and response messages, +/// giving you full control over memory allocation and lifetime. This eliminates +/// hidden stack or heap allocations and is ideal for embedded and no_std environments. +/// +/// # Buffer Requirements +/// You must provide two buffers of at least [`MIN_BUFFER_SIZE`] bytes each: +/// - `req`: Used to serialize outgoing requests to the server +/// - `resp`: Used to receive and parse responses from the server +/// +/// Both buffers may be reused across multiple client operations. They do not need +/// to persist between calls. Consider a static buffer or placing them on the stack +/// in a scope that encompasses all flash operations. +/// +/// # Timeout Behavior +/// A default timeout can be set at construction or updated later with +/// [`set_default_timeout`](Self::set_default_timeout). All methods without an explicit +/// timeout parameter will use this default. `None` means block until the server responds. +/// +/// # Examples +/// ```ignore +/// let mut req = [0u8; MIN_BUFFER_SIZE]; +/// let mut resp = [0u8; MIN_BUFFER_SIZE]; +/// let mut client = FlashClient::new(FLASH_HANDLE, &mut req, &mut resp); +/// +/// let capacity = client.capacity()?; +/// let mut data = [0u8; 256]; +/// client.read(0x1000, &mut data)?; +/// ``` +pub struct FlashClient<'a> { + handle: u32, + req: &'a mut [u8], + resp: &'a mut [u8], + default_timeout: Option, +} + +impl<'a> FlashClient<'a> { + /// Create a new client with caller-provided buffers and no default timeout. + /// + /// # Panics + /// Panics if either `req` or `resp` is smaller than [`MIN_BUFFER_SIZE`]. + pub fn new(handle: u32, req: &'a mut [u8], resp: &'a mut [u8]) -> Self { + debug_assert!( + req.len() >= MIN_BUFFER_SIZE && resp.len() >= MIN_BUFFER_SIZE, + "buffers must be at least {} bytes", + MIN_BUFFER_SIZE + ); + Self { + handle, + req, + resp, + default_timeout: None, + } + } + + /// Create a client with caller-provided buffers and a default timeout. + /// + /// # Panics + /// Panics if either `req` or `resp` is smaller than [`MIN_BUFFER_SIZE`]. + pub fn with_default_timeout( + handle: u32, + req: &'a mut [u8], + resp: &'a mut [u8], + timeout: Option, + ) -> Self { + debug_assert!( + req.len() >= MIN_BUFFER_SIZE && resp.len() >= MIN_BUFFER_SIZE, + "buffers must be at least {} bytes", + MIN_BUFFER_SIZE + ); + Self { + handle, + req, + resp, + default_timeout: timeout, + } + } + + /// Update the default timeout used by `read`, `write`, `erase`, and + /// the discovery calls when no explicit timeout is supplied. + pub fn set_default_timeout(&mut self, timeout: Option) { + self.default_timeout = timeout; + } + + /// Probe flash presence through the server. + /// + /// Returns `Ok(true)` when backend reports responsive flash, + /// `Ok(false)` when backend reports no device present. + pub fn exists(&mut self) -> Result { + let to = self.default_timeout; + self.call_value(FlashOp::Exists, 0, 0, to).map(|v| v != 0) + } + + /// Total bytes of flash exposed by the backend. + pub fn capacity(&mut self) -> Result { + let to = self.default_timeout; + self.call_value(FlashOp::GetCapacity, 0, 0, to) + } + + /// Wire-side geometry: capacity, page size, supported erase + /// granularities (bitmap), address width, opaque flags. Uses + /// the client's default timeout. + pub fn geometry(&mut self) -> Result { + with_default_timeout!(self, geometry_with_timeout) + } + + pub fn geometry_with_timeout( + &mut self, + timeout: Option, + ) -> Result { + let hdr = FlashRequestHeader::new(FlashOp::GetGeometry, 0, 0, 0); + self.req[..FlashRequestHeader::SIZE] + .copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr)); + + let resp_len = syscall::channel_transact( + self.handle, + &self.req[..FlashRequestHeader::SIZE], + &mut self.resp[..], + deadline_from(timeout), + )?; + + parse_geometry_response(&self.resp[..resp_len]) + } + + /// Largest single read or write the backend will accept. Larger + /// requests must be issued by the caller as a sequence of + /// chunk-sized operations. + /// + /// This is a protocol constant (`MAX_PAYLOAD_SIZE`); no IPC is + /// issued. The value is the same for every backend. + pub const fn chunk_size() -> usize { + MAX_PAYLOAD_SIZE + } + + /// Read up to `out.len()` bytes starting at `address`, applying the + /// client's default timeout. The caller is responsible for ensuring + /// `out.len() <= chunk_size()`. + pub fn read(&mut self, address: u32, out: &mut [u8]) -> Result { + with_default_timeout!(self, read_with_timeout, address, out) + } + + /// Read up to `out.len()` bytes starting at `address`, bounded by + /// `timeout`. `None` means block until the server responds. + pub fn read_with_timeout( + &mut self, + address: u32, + out: &mut [u8], + timeout: Option, + ) -> Result { + if out.len() > MAX_PAYLOAD_SIZE { + return Err(ClientError::BufferTooSmall); + } + + let hdr = FlashRequestHeader::new(FlashOp::Read, address, out.len() as u32, 0); + self.req[..FlashRequestHeader::SIZE] + .copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr)); + + let resp_len = syscall::channel_transact( + self.handle, + &self.req[..FlashRequestHeader::SIZE], + &mut self.resp[..], + deadline_from(timeout), + )?; + + parse_payload_response(&self.resp[..resp_len], out) + } + + /// Write `data` starting at `address`, applying the client's default + /// timeout. The caller is responsible for ensuring + /// `data.len() <= chunk_size()`. + pub fn write(&mut self, address: u32, data: &[u8]) -> Result { + with_default_timeout!(self, write_with_timeout, address, data) + } + + /// Write `data` starting at `address`, bounded by `timeout`. `None` + /// blocks until the server responds. The caller is responsible for + /// ensuring `data.len() <= chunk_size()`. + pub fn write_with_timeout( + &mut self, + address: u32, + data: &[u8], + timeout: Option, + ) -> Result { + if data.len() > MAX_PAYLOAD_SIZE { + return Err(ClientError::BufferTooSmall); + } + if FlashRequestHeader::SIZE + data.len() > self.req.len() { + return Err(ClientError::BufferTooSmall); + } + + let hdr = FlashRequestHeader::new( + FlashOp::Write, + address, + data.len() as u32, + data.len() as u16, + ); + self.req[..FlashRequestHeader::SIZE] + .copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr)); + self.req[FlashRequestHeader::SIZE..FlashRequestHeader::SIZE + data.len()] + .copy_from_slice(data); + + let resp_len = syscall::channel_transact( + self.handle, + &self.req[..FlashRequestHeader::SIZE + data.len()], + &mut self.resp[..], + deadline_from(timeout), + )?; + + parse_value_response(&self.resp[..resp_len]).map(|n| n as usize) + } + + /// Erase `length` bytes starting at `address`, applying the client's + /// default timeout. Both must be aligned to and a multiple of the + /// backend's erase granule. + pub fn erase(&mut self, address: u32, length: u32) -> Result<(), ClientError> { + with_default_timeout!(self, erase_with_timeout, address, length) + } + + /// Erase `length` bytes starting at `address`, bounded by `timeout`. + /// `None` blocks until the server responds. Both must be aligned to + /// and a multiple of the backend's erase granule. + pub fn erase_with_timeout( + &mut self, + address: u32, + length: u32, + timeout: Option, + ) -> Result<(), ClientError> { + let hdr = FlashRequestHeader::new(FlashOp::Erase, address, length, 0); + self.req[..FlashRequestHeader::SIZE] + .copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr)); + + let resp_len = syscall::channel_transact( + self.handle, + &self.req[..FlashRequestHeader::SIZE], + &mut self.resp[..], + deadline_from(timeout), + )?; + + parse_value_response(&self.resp[..resp_len]).map(|_| ()) + } + + fn call_value( + &mut self, + op: FlashOp, + address: u32, + length: u32, + timeout: Option, + ) -> Result { + let hdr = FlashRequestHeader::new(op, address, length, 0); + self.req[..FlashRequestHeader::SIZE] + .copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr)); + + let resp_len = syscall::channel_transact( + self.handle, + &self.req[..FlashRequestHeader::SIZE], + &mut self.resp[..], + deadline_from(timeout), + )?; + + parse_value_response(&self.resp[..resp_len]) + } +} + +fn parse_value_response(resp: &[u8]) -> Result { + if resp.len() < FlashResponseHeader::SIZE { + return Err(ClientError::InvalidResponse); + } + + let hdr_bytes = &resp[..FlashResponseHeader::SIZE]; + let Some(hdr) = zerocopy::Ref::<_, FlashResponseHeader>::from_bytes(hdr_bytes).ok() else { + return Err(ClientError::InvalidResponse); + }; + + if hdr.is_success() { + Ok(hdr.value_word()) + } else { + Err(ClientError::ServerError(hdr.error_code())) + } +} + +fn parse_geometry_response(resp: &[u8]) -> Result { + if resp.len() < FlashResponseHeader::SIZE { + return Err(ClientError::InvalidResponse); + } + + let hdr_bytes = &resp[..FlashResponseHeader::SIZE]; + let Some(hdr) = zerocopy::Ref::<_, FlashResponseHeader>::from_bytes(hdr_bytes).ok() else { + return Err(ClientError::InvalidResponse); + }; + + if !hdr.is_success() { + return Err(ClientError::ServerError(hdr.error_code())); + } + + let len = hdr.payload_length(); + if len != FlashGeometry::SIZE + || resp.len() < FlashResponseHeader::SIZE + FlashGeometry::SIZE + { + return Err(ClientError::InvalidResponse); + } + + let geom_bytes = + &resp[FlashResponseHeader::SIZE..FlashResponseHeader::SIZE + FlashGeometry::SIZE]; + FlashGeometry::read_from_bytes(geom_bytes).map_err(|_| ClientError::InvalidResponse) +} + +fn parse_payload_response(resp: &[u8], out: &mut [u8]) -> Result { + if resp.len() < FlashResponseHeader::SIZE { + return Err(ClientError::InvalidResponse); + } + + let hdr_bytes = &resp[..FlashResponseHeader::SIZE]; + let Some(hdr) = zerocopy::Ref::<_, FlashResponseHeader>::from_bytes(hdr_bytes).ok() else { + return Err(ClientError::InvalidResponse); + }; + + if !hdr.is_success() { + return Err(ClientError::ServerError(hdr.error_code())); + } + + let len = hdr.payload_length(); + if len > out.len() || resp.len() < FlashResponseHeader::SIZE + len { + return Err(ClientError::InvalidResponse); + } + + out[..len].copy_from_slice(&resp[FlashResponseHeader::SIZE..FlashResponseHeader::SIZE + len]); + Ok(len) +} From 51b95508864655bb780ac43998820116104f06bd Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Tue, 5 May 2026 08:49:14 -0700 Subject: [PATCH 2/2] flash: remove WouldBlock from wire status and document server-internal retry semantics --- drivers/flash/README.md | 151 ++++++++++++++++++++++++++++++ drivers/flash/api/README.md | 9 +- drivers/flash/api/src/backend.rs | 12 ++- drivers/flash/api/src/protocol.rs | 19 ++-- drivers/flash/client/README.md | 6 +- drivers/flash/client/src/lib.rs | 2 +- 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 drivers/flash/README.md diff --git a/drivers/flash/README.md b/drivers/flash/README.md new file mode 100644 index 00000000..74efca24 --- /dev/null +++ b/drivers/flash/README.md @@ -0,0 +1,151 @@ +# Flash Driver Model + +This document describes the architecture of the layered flash userspace driver +under drivers/flash and how it integrates with platform server bindings. + +## 1. Layer Overview + +``` +┌──────────────────────────────────────────────────────────┐ +│ Application / Client Task │ +│ FlashClient (drivers/flash/client) │ +│ channel_transact(request) -> response │ +└────────────────────────┬─────────────────────────────────┘ + │ Pigweed IPC channel + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Server Binary (platform binding) │ +│ rust_app wires handles + backend + runtime loop │ +└────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Server Library (platform binding) │ +│ dispatch_request: protocol -> backend translation │ +│ runtime loop: channel_read -> dispatch -> respond │ +└────────────────────────┬─────────────────────────────────┘ + │ FlashBackend trait + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Platform Backend (platform binding) │ +│ PlatformFlashBackend : FlashBackend │ +└────────────────────────┬─────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────┐ +│ Controller Driver (SMC/FMC or equivalent) │ +│ Raw MMIO or HAL-level flash controller implementation │ +└──────────────────────────────────────────────────────────┘ +``` + +## 2. Crate Map + +| Bazel target | Crate | Role | +|---|---|---| +| //drivers/flash/api:flash_api | flash_api | Wire protocol, error/status model, geometry types, backend trait contract | +| //drivers/flash/client:flash_client | flash_client | Userspace IPC facade for read/write/erase/discovery | + +The API/client layers are target-agnostic within Pigweed kernel userspace +targets. + +## 3. Wire Protocol (flash_api::protocol) + +Operations are encoded by FlashOp in FlashRequestHeader (16 bytes, +repr(C, packed), little-endian), with an optional payload up to +MAX_PAYLOAD_SIZE (256 bytes). + +| Op | Value | Request shape | Response shape | +|---|---|---|---| +| Exists | 0x01 | header only | value = 0/1 | +| GetCapacity | 0x02 | header only | value = capacity bytes | +| Read | 0x03 | address + length | payload = bytes read, value = byte count | +| Write | 0x04 | address + length + payload | value = byte count | +| Erase | 0x05 | address + length | success/error only | +| GetGeometry | 0x06 | header only | payload = FlashGeometry | + +FlashResponseHeader (8 bytes) carries status (FlashError), payload length, +and an op-specific value word. + +## 4. Backend Contract (flash_api::backend) + +The server-side backend contract is defined by FlashBackend: + +- info(route_key) -> FlashInfo +- exists(route_key) -> Result +- read(route_key, address, out) -> Result +- write(route_key, address, data) -> Result +- erase(route_key, address, length) -> Result<(), BackendError> +- enable_interrupts() / disable_interrupts() + +Geometry discovery is exposed via FlashGeometryProvider, which can derive a +default geometry from info() or be overridden by backends that need richer +semantics. + +## 5. Client Library (flash_client) + +FlashClient is a synchronous userspace facade that: + +- serializes request headers/payloads into caller-provided request buffers, +- performs channel_transact with optional timeout, +- parses/validates response headers and payload lengths, +- maps transport and wire errors into ClientError. + +Current client behavior and constraints: + +- no_std, blocking IPC calls. +- single call in flight per client instance (&mut self API). +- per-call data cap is FlashClient::chunk_size() (MAX_PAYLOAD_SIZE). +- explicit support for discovery calls: exists, capacity, geometry. + +For detailed usage and method-level behavior, see: + +- drivers/flash/api/README.md +- drivers/flash/client/README.md + +## 6. Error Surface + +Wire-level status is FlashError. Common categories: + +- protocol misuse: InvalidOperation, InvalidAddress, InvalidLength +- runtime contention: Busy, Timeout +- media/policy failures: IoError, NotPermitted +- fallback: InternalError + +ClientError wraps three classes of failure: + +- IpcError(pw_status::Error): transport syscall failure +- ServerError(FlashError): valid response reporting flash-level failure +- InvalidResponse: malformed/truncated response frame +- BufferTooSmall: local request/response buffer constraints + +## 7. Integration Model + +drivers/flash is split so protocol and client can evolve independently from +platform backend/server bring-up: + +- Keep wire schema and trait contract stable in flash_api. +- Keep userspace ergonomics and parsing hardening in flash_client. +- Implement server runtime and concrete backend per target tree. + +This separation allows host-side validation of protocol and client logic before +hardware-specific bindings are ready. + +## 8. Extension Points + +- New backend: implement FlashBackend (and optionally FlashGeometryProvider) + in a platform crate, then bind server channel handles to route keys. +- New operation: add FlashOp variant, define header/payload semantics, extend + backend trait and server dispatch. +- New geometry flags semantics: preserve wire compatibility by treating flags + as opaque at protocol level and documenting interpretation at backend level. + +## 9. Testing Focus + +Recommended tests by layer: + +- flash_api: wire layout, endian correctness, enum/status round-trips, + short-buffer decode rejection. +- flash_client: response validation paths, timeout behavior, chunk-size checks, + and retry handling for Busy. +- platform server/backend: operation dispatch, alignment rules, erase granule + correctness, and hardware fault mapping to FlashError. diff --git a/drivers/flash/api/README.md b/drivers/flash/api/README.md index e57cd899..925228a6 100644 --- a/drivers/flash/api/README.md +++ b/drivers/flash/api/README.md @@ -192,14 +192,15 @@ maps channel → backend → `RouteKey`. | `BufferTooSmall` | 0x04 | Server-side buffer constraint | | `Busy` | 0x05 | Backend busy | | `Timeout` | 0x06 | Operation timed out | -| `WouldBlock` | 0x07 | Could not complete synchronously; retry after IRQ | -| `IoError` | 0x08 | Media-level failure | -| `NotPermitted` | 0x09 | Blocked by backend policy/protection | +| `IoError` | 0x07 | Media-level failure | +| `NotPermitted` | 0x08 | Blocked by backend policy/protection | | `InternalError` | 0xFF | Unclassified server fault | `BackendError` is the trait-level error backends produce; an `impl From for FlashError` provides the canonical mapping for -the server's response-encoding path. +the server's response-encoding path. `BackendError::WouldBlock` is +treated as backend-internal and maps to `FlashError::Busy` at the wire +boundary. ## Tests diff --git a/drivers/flash/api/src/backend.rs b/drivers/flash/api/src/backend.rs index 3a9cd873..4271ddd0 100644 --- a/drivers/flash/api/src/backend.rs +++ b/drivers/flash/api/src/backend.rs @@ -6,7 +6,8 @@ //! The shape mirrors the `FlashStorage` HIL from caliptra-mcu-sw but is //! synchronous and buffer-borrowing rather than callback-based: the //! server runtime drives concurrency, so backends only need to expose -//! a blocking-or-`WouldBlock` surface. +//! a blocking-or-`WouldBlock` surface. `WouldBlock` is backend-internal +//! and is not encoded on the wire. use crate::protocol::{FlashError, FlashGeometry}; @@ -18,8 +19,11 @@ pub enum BackendError { BufferTooSmall, Busy, Timeout, - /// Backend cannot complete synchronously at this time; the server - /// runtime should retry after `OPERATION_COMPLETE` fires. + /// Operation cannot complete synchronously now. + /// + /// This is an internal backend/server scheduling signal. The server + /// runtime should defer and retry after a progress signal, typically + /// a completion interrupt, rather than exposing it directly on the wire. WouldBlock, /// Media-level failure (program/erase verify fail, ECC uncorrectable, …). IoError, @@ -37,7 +41,7 @@ impl From for FlashError { BackendError::BufferTooSmall => FlashError::BufferTooSmall, BackendError::Busy => FlashError::Busy, BackendError::Timeout => FlashError::Timeout, - BackendError::WouldBlock => FlashError::WouldBlock, + BackendError::WouldBlock => FlashError::Busy, BackendError::IoError => FlashError::IoError, BackendError::NotPermitted => FlashError::NotPermitted, BackendError::InternalError => FlashError::InternalError, diff --git a/drivers/flash/api/src/protocol.rs b/drivers/flash/api/src/protocol.rs index 31a207ec..d2a59b24 100644 --- a/drivers/flash/api/src/protocol.rs +++ b/drivers/flash/api/src/protocol.rs @@ -66,14 +66,11 @@ pub enum FlashError { BufferTooSmall = 0x04, Busy = 0x05, Timeout = 0x06, - /// Operation cannot complete right now; the server/runtime may defer - /// completion until the backend signals progress via interrupt. - WouldBlock = 0x07, /// Underlying media reported an I/O error (e.g. flash program failure). - IoError = 0x08, + IoError = 0x07, /// Address/length straddles a region the backend refuses to touch /// (e.g. write-protected partition). - NotPermitted = 0x09, + NotPermitted = 0x08, InternalError = 0xFF, } @@ -87,9 +84,8 @@ impl From for FlashError { 0x04 => Self::BufferTooSmall, 0x05 => Self::Busy, 0x06 => Self::Timeout, - 0x07 => Self::WouldBlock, - 0x08 => Self::IoError, - 0x09 => Self::NotPermitted, + 0x07 => Self::IoError, + 0x08 => Self::NotPermitted, _ => Self::InternalError, } } @@ -297,9 +293,8 @@ mod tests { (0x04, FlashError::BufferTooSmall), (0x05, FlashError::Busy), (0x06, FlashError::Timeout), - (0x07, FlashError::WouldBlock), - (0x08, FlashError::IoError), - (0x09, FlashError::NotPermitted), + (0x07, FlashError::IoError), + (0x08, FlashError::NotPermitted), (0xFF, FlashError::InternalError), ]; for (byte, err) in cases { @@ -313,6 +308,7 @@ mod tests { for byte in [0x0Au8, 0x10, 0x42, 0x80, 0xFE] { assert_eq!(FlashError::from(byte), FlashError::InternalError); } + assert_eq!(FlashError::from(0x09), FlashError::InternalError); } #[test] @@ -416,7 +412,6 @@ mod tests { for err in [ FlashError::Success, FlashError::InvalidAddress, - FlashError::WouldBlock, FlashError::NotPermitted, FlashError::InternalError, ] { diff --git a/drivers/flash/client/README.md b/drivers/flash/client/README.md index 3630ead4..77f98369 100644 --- a/drivers/flash/client/README.md +++ b/drivers/flash/client/README.md @@ -144,15 +144,11 @@ pub enum ClientError { | `BufferTooSmall` | Server-side buffer constraint | | `Busy` | Backend busy | | `Timeout` | Operation timed out | -| `WouldBlock` | Operation could not be completed immediately | | `IoError` | Media-level failure | | `NotPermitted` | Blocked by backend policy/protection | | `InternalError` | Unclassified server fault | -### `Busy` vs `WouldBlock` - -- **`Busy`**: The backend is actively performing another operation and cannot accept new requests. Retry the entire call after waiting. -- **`WouldBlock`**: This specific operation could not be completed immediately due to resource constraints or contention, but is not blocked by global device activity. Callers can implement smarter retry logic or adjust request parameters. +Client policy should treat `Busy` as the retryable contention signal. ## Constraints diff --git a/drivers/flash/client/src/lib.rs b/drivers/flash/client/src/lib.rs index d451de04..2efd4f5a 100644 --- a/drivers/flash/client/src/lib.rs +++ b/drivers/flash/client/src/lib.rs @@ -10,7 +10,7 @@ use flash_api::{ }; pub use flash_api::MAX_PAYLOAD_SIZE; use userspace::syscall; -use userspace::time::{Duration as KDuration, Instant, SystemClock}; +use userspace::time::{Clock, Duration as KDuration, Instant, SystemClock}; use zerocopy::FromBytes; /// Minimum buffer size required for Flash protocol messages.