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/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/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..925228a6 --- /dev/null +++ b/drivers/flash/api/README.md @@ -0,0 +1,228 @@ +# 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 | +| `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. `BackendError::WouldBlock` is +treated as backend-internal and maps to `FlashError::Busy` at the wire +boundary. + +## 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..4271ddd0 --- /dev/null +++ b/drivers/flash/api/src/backend.rs @@ -0,0 +1,160 @@ +// 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. `WouldBlock` is backend-internal +//! and is not encoded on the wire. + +use crate::protocol::{FlashError, FlashGeometry}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BackendError { + InvalidOperation, + InvalidAddress, + InvalidLength, + BufferTooSmall, + Busy, + Timeout, + /// 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, + /// 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::Busy, + 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..d2a59b24 --- /dev/null +++ b/drivers/flash/api/src/protocol.rs @@ -0,0 +1,530 @@ +// 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, + /// Underlying media reported an I/O error (e.g. flash program failure). + IoError = 0x07, + /// Address/length straddles a region the backend refuses to touch + /// (e.g. write-protected partition). + NotPermitted = 0x08, + 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::IoError, + 0x08 => 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::IoError), + (0x08, 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); + } + assert_eq!(FlashError::from(0x09), 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::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..77f98369 --- /dev/null +++ b/drivers/flash/client/README.md @@ -0,0 +1,169 @@ +# 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 | +| `IoError` | Media-level failure | +| `NotPermitted` | Blocked by backend policy/protection | +| `InternalError` | Unclassified server fault | + +Client policy should treat `Busy` as the retryable contention signal. + +## 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..2efd4f5a --- /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::{Clock, 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) +}