From bf568254560be049739b3dcfad7a13b07792e4f0 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 6 Jun 2026 13:02:36 -0700 Subject: [PATCH 01/11] util: IPC abstraction Signed-off-by: Chris Frantz --- util/ipc/BUILD.bazel | 26 ++++++++++ util/ipc/host.rs | 121 +++++++++++++++++++++++++++++++++++++++++++ util/ipc/lib.rs | 53 +++++++++++++++++++ util/ipc/target.rs | 40 ++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 util/ipc/BUILD.bazel create mode 100644 util/ipc/host.rs create mode 100644 util/ipc/lib.rs create mode 100644 util/ipc/target.rs diff --git a/util/ipc/BUILD.bazel b/util/ipc/BUILD.bazel new file mode 100644 index 000000000..c748e9212 --- /dev/null +++ b/util/ipc/BUILD.bazel @@ -0,0 +1,26 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "ipc", + srcs = [ + "host.rs", + "lib.rs", + "target.rs", + ], + crate_name = "util_ipc", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@pigweed//pw_status/rust:pw_status", + ] + select({ + "@platforms//os:none": [ + "@pigweed//pw_kernel/userspace", + ], + "//conditions:default": [ + "@pigweed//pw_time/rust:pw_time", + ], + }), +) diff --git a/util/ipc/host.rs b/util/ipc/host.rs new file mode 100644 index 000000000..9abb9caf1 --- /dev/null +++ b/util/ipc/host.rs @@ -0,0 +1,121 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use super::{IpcChannel, IpcHandle}; + +pub trait AsSyscallBuffer { + fn as_raw(&self) -> (*const u8, usize); + fn as_raw_mut(&mut self) -> (*mut u8, usize); + fn total_size(&self) -> usize; +} + +// Converts a simple u8 slice. +impl AsSyscallBuffer for [u8] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr(), self.len()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr(), self.len()) + } + fn total_size(&self) -> usize { + self.len() + } +} + +// Converts a simple u8 array. +impl AsSyscallBuffer for [u8; N] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr(), self.len()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr(), self.len()) + } + fn total_size(&self) -> usize { + self.len() + } +} + +// Converts a slice of u8 slices. +impl AsSyscallBuffer for [&[u8]] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr().cast::(), self.len().wrapping_neg()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr().cast::(), self.len().wrapping_neg()) + } + fn total_size(&self) -> usize { + self.iter().fold(0, |total, item| total + item.len()) + } +} + +impl AsSyscallBuffer for [&mut [u8]] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr().cast::(), self.len().wrapping_neg()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr().cast::(), self.len().wrapping_neg()) + } + fn total_size(&self) -> usize { + self.iter().fold(0, |total, item| total + item.len()) + } +} + +// Converts an array of u8 slices. +impl AsSyscallBuffer for [&[u8]; N] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr().cast::(), self.len().wrapping_neg()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr().cast::(), self.len().wrapping_neg()) + } + fn total_size(&self) -> usize { + self.iter().fold(0, |total, item| total + item.len()) + } +} + +impl AsSyscallBuffer for [&mut [u8]; N] { + fn as_raw(&self) -> (*const u8, usize) { + (self.as_ptr().cast::(), self.len().wrapping_neg()) + } + fn as_raw_mut(&mut self) -> (*mut u8, usize) { + (self.as_mut_ptr().cast::(), self.len().wrapping_neg()) + } + fn total_size(&self) -> usize { + self.iter().fold(0, |total, item| total + item.len()) + } +} + +pub type Instant = pw_time::Instant; + +impl IpcChannel for IpcHandle { + fn transact( + &self, + _send_data: &BufSend, + _recv_data: &mut BufRecv, + _deadline: Instant, + ) -> pw_status::Result + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized, + { + panic!("IpcHandle cannot be used on host"); + } + + fn read(&self, _offset: usize, _buffer: &mut Buf) -> pw_status::Result + where + Buf: AsSyscallBuffer + ?Sized, + { + panic!("IpcHandle cannot be used on host"); + } + + fn respond(&self, _buffer: &Buf) -> pw_status::Result<()> + where + Buf: AsSyscallBuffer + ?Sized, + { + panic!("IpcHandle cannot be used on host"); + } + + fn set_peer_user_signal(&self, _set: bool) -> pw_status::Result<()> { + panic!("IpcHandle cannot be used on host"); + } +} diff --git a/util/ipc/lib.rs b/util/ipc/lib.rs new file mode 100644 index 000000000..7861452b0 --- /dev/null +++ b/util/ipc/lib.rs @@ -0,0 +1,53 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use pw_status::Result; + +/// Trait wrapping basic IPC operations on a channel. +pub trait IpcChannel { + fn transact( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + deadline: Instant, + ) -> Result + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized; + + fn read(&self, offset: usize, buffer: &mut Buf) -> Result + where + Buf: AsSyscallBuffer + ?Sized; + + fn respond(&self, buffer: &Buf) -> Result<()> + where + Buf: AsSyscallBuffer + ?Sized; + + /// Set (set=true) or clear (set=false) Signals::USER on the paired peer. + fn set_peer_user_signal(&self, set: bool) -> Result<()>; +} + +/// Transparent wrapper around a raw IPC handle. +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct IpcHandle { + pub handle: u32, +} + +impl IpcHandle { + pub const fn new(handle: u32) -> Self { + Self { handle } + } +} + +#[cfg(target_os = "none")] +mod target; +#[cfg(target_os = "none")] +pub use target::{AsSyscallBuffer, Instant}; + +#[cfg(not(target_os = "none"))] +mod host; +#[cfg(not(target_os = "none"))] +pub use host::{AsSyscallBuffer, Instant}; diff --git a/util/ipc/target.rs b/util/ipc/target.rs new file mode 100644 index 000000000..d278d073d --- /dev/null +++ b/util/ipc/target.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use super::{IpcChannel, IpcHandle}; + +pub use userspace::buffer::AsSyscallBuffer; +pub use userspace::time::Instant; + +impl IpcChannel for IpcHandle { + fn transact( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + deadline: Instant, + ) -> pw_status::Result + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_transact(self.handle, send_data, recv_data, deadline) + } + + fn read(&self, offset: usize, buffer: &mut Buf) -> pw_status::Result + where + Buf: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_read(self.handle, offset, buffer) + } + + fn respond(&self, buffer: &Buf) -> pw_status::Result<()> + where + Buf: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_respond(self.handle, buffer) + } + + fn set_peer_user_signal(&self, set: bool) -> pw_status::Result<()> { + userspace::syscall::object_set_peer_user_signal(self.handle, set) + } +} From 9b48c73649b75928fe731823dfda157b4b3707de Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sun, 7 Jun 2026 14:59:40 -0700 Subject: [PATCH 02/11] services: flash client/server Signed-off-by: Chris Frantz --- services/flash/BUILD.bazel | 58 ++++++++++++++++ services/flash/README.md | 88 ++++++++++++++++++++++++ services/flash/client.rs | 109 ++++++++++++++++++++++++++++++ services/flash/opcode.rs | 51 ++++++++++++++ services/flash/server.rs | 134 +++++++++++++++++++++++++++++++++++++ 5 files changed, 440 insertions(+) create mode 100644 services/flash/BUILD.bazel create mode 100644 services/flash/README.md create mode 100644 services/flash/client.rs create mode 100644 services/flash/opcode.rs create mode 100644 services/flash/server.rs diff --git a/services/flash/BUILD.bazel b/services/flash/BUILD.bazel new file mode 100644 index 000000000..e4a6babd3 --- /dev/null +++ b/services/flash/BUILD.bazel @@ -0,0 +1,58 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "opcode", + srcs = [ + "opcode.rs", + ], + crate_name = "services_flash_opcode", + edition = "2024", + deps = [ + "//hal/blocking/flash", + "//util/types", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "client", + srcs = [ + "client.rs", + ], + crate_name = "services_flash_client", + edition = "2024", + deps = [ + ":opcode", + "//hal/blocking/flash", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "server", + srcs = [ + "server.rs", + ], + crate_name = "services_flash_server", + edition = "2024", + deps = [ + ":opcode", + "//hal/blocking/flash", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:zerocopy", + ], +) diff --git a/services/flash/README.md b/services/flash/README.md new file mode 100644 index 000000000..e352c54a8 --- /dev/null +++ b/services/flash/README.md @@ -0,0 +1,88 @@ +# Flash Service + +The Flash Service provides a centralized interface for userspace applications to interact with on-chip and external flash memory. This is achieved via an IPC-based client-server architecture. + +## Overview + +Applications interact with flash through the `Flash` trait, typically using the `FlashIpcClient` implementation. All operations are blocking from the perspective of the caller. + +### Key Features +- **Partition Support**: Access to both primary Data partitions and auxiliary Info partitions. +- **Flexible Erase**: Support for multiple erase granularities (e.g., page vs. block) as reported by the hardware. +- **Unified Addressing**: A logical `FlashAddress` system that abstracts hardware-specific bank and page layouts. + +## Usage + +To use the flash service, initialize a `FlashIpcClient` with a handle to the flash service: + +```rust +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_ipc::IpcHandle; + +// 1. Connect to the flash service +let mut flash = FlashIpcClient::new(IpcHandle::new(FLASH_SERVICE_HANDLE))?; + +// 2. Retrieve device geometry +let (total_size, page_size, erasable_bitmap) = flash.geometry(); + +// 3. Erase a block (using the default page size) +let addr = FlashAddress::new(0x1000); +flash.erase(addr, page_size)?; + +// 4. Program data +flash.program(addr, b"Hello, Flash!")?; + +// 5. Read data back +let mut buf = [0u8; 13]; +flash.read(addr, &mut buf)?; +``` + +## The `Flash` Trait + +The primary interface for flash operations: + +- `geometry() -> (NonZero, PowerOf2Usize, u32)`: Returns the total capacity, the default/smallest page size, and a bitmap of all supported erase block sizes. +- `read(addr, buf)`: Reads data from the specified address. +- `erase(addr, size)`: Erases a block of the specified size. The size must be one of the values supported in the `erasable_bitmap`. +- `program(addr, data)`: Writes data to the specified address. Flash must be erased before programming. + +### Understanding `erasable_bitmap` +The `erasable_bitmap` is a `u32` where each set bit `i` indicates that an erase block size of `2^i` bytes is supported. +- Bit 11 set (`0x800`) -> 2048-byte erase supported. +- Bit 16 set (`0x10000`) -> 64KB erase supported. + +## Addressing + +Flash memory is addressed using the `FlashAddress` type, which wraps a single 32-bit `offset`. + +On platforms like Earlgrey, the most significant bit (MSB) of this offset is used to distinguish between different partitions: +- **DATA partition**: MSB is 0 (offset < 0x80000000). +- **INFO partition**: MSB is 1 (offset >= 0x80000000). + +The `EarlgreyFlashAddress` trait (from `earlgrey_util`) provides helper methods to construct and inspect addresses: +- `FlashAddress::data(offset)`: Accesses the main data partition. +- `FlashAddress::info(bank, page, offset)`: Accesses specific info pages. + +## Implementation Details + +The service is built on several layers of abstraction: + +### IPC Layer +- **`FlashIpcServer`**: Wraps a hardware-backed `Flash` implementation and dispatches IPC requests. +- **`FlashIpcClient`**: Implements the `Flash` trait by proxying calls to the server. + +### Hardware Abstraction +- **`FlashDriver` Trait**: Defines the low-level, often asynchronous, interface for hardware drivers. +- **`BlockingFlash`**: A wrapper that converts a `FlashDriver` into a synchronous `Flash` implementation using a provided blocking mechanism. + +### Component Diagram + +```mermaid +graph TD + Client[Userspace Application] -- "Flash Trait" --> IPC_Client[FlashIpcClient] + IPC_Client -- "IPC" --> IPC_Server[FlashIpcServer] + IPC_Server -- "Flash Trait" --> BlockingFlash[BlockingFlash] + BlockingFlash -- "FlashDriver Trait" --> HardwareDriver[e.g., EmbeddedFlash] + HardwareDriver --> HW[Flash Controller] +``` diff --git a/services/flash/client.rs b/services/flash/client.rs new file mode 100644 index 000000000..fbd6fec18 --- /dev/null +++ b/services/flash/client.rs @@ -0,0 +1,109 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Flash IPC client implementation. + +#![no_std] +use core::num::NonZero; + +use hal_flash::{Flash, FlashAddress}; +use services_flash_opcode::*; +use userspace::time::Instant; +use util_error::{self as error, ErrorCode}; +use util_ipc::{IpcChannel, IpcHandle}; +use util_types::PowerOf2Usize; +use zerocopy::{FromZeros, IntoBytes}; + +/// An IPC-based client for the flash service. +/// +/// This struct implements the `Flash` trait by proxying requests to a remote +/// flash server via an IPC handle. +pub struct FlashIpcClient { + ipc: IpcHandle, + page_size: PowerOf2Usize, + total_size: NonZero, + erasable_sizes_bitmap: u32, +} + +impl FlashIpcClient { + /// Creates a new `FlashIpcClient` using the provided IPC handle. + /// + /// This constructor will perform an IPC transaction to retrieve flash + /// geometry and capabilities from the server. + pub fn new(ipc: IpcHandle) -> Result { + let mut info = FlashInfo::new_zeroed(); + let mut result = 0u32; + + ipc.transact( + &[IPC_OP_FLASH_GET_INFO.as_bytes()], + &mut [result.as_mut_bytes(), info.as_mut_bytes()], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result)?; + + let Some(page_size) = PowerOf2Usize::new(info.page_size as usize) else { + return Err(error::FLASH_GENERIC_INVALID_PAGE_SIZE); + }; + let Some(total_size) = NonZero::new(info.total_size as usize) else { + return Err(error::FLASH_GENERIC_INVALID_SIZE); + }; + Ok(Self { + ipc, + page_size, + total_size, + erasable_sizes_bitmap: info.erasable_sizes_bitmap, + }) + } +} + +impl Flash for FlashIpcClient { + type Error = ErrorCode; + fn geometry(&mut self) -> Result<(NonZero, PowerOf2Usize, u32), ErrorCode> { + Ok((self.total_size, self.page_size, self.erasable_sizes_bitmap)) + } + + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), ErrorCode> { + let mut result = 0u32; + let op = EraseOp { + address: start_addr, + size: size.get() as u32, + }; + self.ipc + .transact( + &[IPC_OP_FLASH_ERASE.as_bytes(), op.as_bytes()], + &mut [result.as_mut_bytes()], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result) + } + + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc + .transact( + &[IPC_OP_FLASH_PROGRAM.as_bytes(), start_addr.as_bytes(), data], + &mut [result.as_mut_bytes()], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + let op = ReadOp { + address: start_addr, + length: buf.len() as u32, + }; + self.ipc + .transact( + &[IPC_OP_FLASH_READ.as_bytes(), op.as_bytes()], + &mut [result.as_mut_bytes(), buf], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result) + } +} diff --git a/services/flash/opcode.rs b/services/flash/opcode.rs new file mode 100644 index 000000000..ef6805eef --- /dev/null +++ b/services/flash/opcode.rs @@ -0,0 +1,51 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared flash IPC opcodes and data structures. + +#![no_std] + +use hal_flash::FlashAddress; +use util_types::Opcode; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +/// IPC opcode for erasing a flash block. +pub const IPC_OP_FLASH_ERASE: Opcode = Opcode::new(*b"FLET"); +/// IPC opcode for programming flash. +pub const IPC_OP_FLASH_PROGRAM: Opcode = Opcode::new(*b"FLWR"); +/// IPC opcode for reading from flash. +pub const IPC_OP_FLASH_READ: Opcode = Opcode::new(*b"FLRD"); +/// IPC opcode for retrieving flash information. +pub const IPC_OP_FLASH_GET_INFO: Opcode = Opcode::new(*b"FLIN"); + +/// Information about the flash device. +#[derive(FromBytes, Immutable, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct FlashInfo { + /// The size of a single flash page in bytes. + pub page_size: u32, + /// The total size of the flash in bytes. + pub total_size: u32, + /// A bitmap of supported erase block sizes. + pub erasable_sizes_bitmap: u32, +} + +/// Arguments for the `IPC_OP_FLASH_ERASE` request. +#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct EraseOp { + /// The start address of the block to erase. + pub address: FlashAddress, + /// The size of the block to erase in bytes. + pub size: u32, +} + +/// Arguments for the `IPC_OP_FLASH_READ` request. +#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)] +#[repr(C)] +pub struct ReadOp { + /// The start address to read from. + pub address: FlashAddress, + /// The number of bytes to read. + pub length: u32, +} diff --git a/services/flash/server.rs b/services/flash/server.rs new file mode 100644 index 000000000..296bbbe8b --- /dev/null +++ b/services/flash/server.rs @@ -0,0 +1,134 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Flash IPC server implementation. + +#![no_std] + +use hal_flash::{Flash, FlashAddress}; +use services_flash_opcode::*; +use util_error::{self as error, ErrorCode}; +use util_ipc::{IpcChannel, IpcHandle}; +use util_types::{Opcode, PowerOf2Usize}; +use zerocopy::{FromBytes, IntoBytes}; + +/// A flash server that handles flash IPC requests. +/// +/// This struct wraps an object implementing the `Flash` trait and provides +/// an IPC interface to it. +pub struct FlashIpcServer { + flash: TFlash, +} + +impl> FlashIpcServer { + /// Creates a new `FlashIpcServer` wrapping the given flash implementation. + pub fn new(flash: TFlash) -> Self { + Self { flash } + } + + /// Handles the `IPC_OP_FLASH_GET_INFO` request. + /// + /// Writes the flash geometry into the provided buffer and returns it. + fn handle_geometry<'a>( + &mut self, + data: &'a mut [u8], + reqsz: usize, + ) -> Result<&'a [u8], ErrorCode> { + if reqsz != 0 { + return Err(error::IPC_ERROR_BAD_REQ_LEN); + } + let (info, _rest) = + FlashInfo::mut_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let (total_size, page_size, erasable_sizes_bitmap) = self.flash.geometry()?; + info.page_size = page_size.get() as u32; + info.total_size = total_size.get() as u32; + info.erasable_sizes_bitmap = erasable_sizes_bitmap; + Ok(info.as_bytes()) + } + + /// Handles the `IPC_OP_FLASH_ERASE` request. + /// + /// Parses the `EraseOp` from the input data and erases the specified block. + fn handle_erase<'a>( + &mut self, + data: &'a mut [u8], + reqsz: usize, + ) -> Result<&'a [u8], ErrorCode> { + let req_data = data.get(..reqsz).ok_or(error::IPC_ERROR_BAD_REQ_LEN)?; + let op = EraseOp::read_from_bytes(req_data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let Some(size) = PowerOf2Usize::new(op.size as usize) else { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + }; + self.flash.erase(op.address, size)?; + Ok(&data[0..0]) + } + + /// Handles the `IPC_OP_FLASH_PROGRAM` request. + /// + /// Parses the start address and data from the input, then programs it. + fn handle_program<'a>( + &mut self, + data: &'a mut [u8], + reqsz: usize, + ) -> Result<&'a [u8], ErrorCode> { + let req_data = data.get(..reqsz).ok_or(error::IPC_ERROR_BAD_REQ_LEN)?; + let (addr, program_data) = + FlashAddress::read_from_prefix(req_data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.program(addr, program_data)?; + Ok(&data[0..0]) + } + + /// Handles the `IPC_OP_FLASH_READ` request. + /// + /// Parses the `ReadOp` from the input, reads the data from flash into the + /// buffer, and returns the read slice. + fn handle_read<'a>(&mut self, data: &'a mut [u8], reqsz: usize) -> Result<&'a [u8], ErrorCode> { + let req_data = data.get(..reqsz).ok_or(error::IPC_ERROR_BAD_REQ_LEN)?; + let op = ReadOp::read_from_bytes(req_data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let length = op.length as usize; + if length > data.len() { + return Err(error::FLASH_GENERIC_INVALID_SIZE); + } + self.flash.read(op.address, &mut data[..length])?; + Ok(&data[..length]) + } + + fn handle_op<'a>( + &mut self, + opcode: Opcode, + data: &'a mut [u8], + reqsz: usize, + ) -> Result<&'a [u8], ErrorCode> { + match opcode { + IPC_OP_FLASH_GET_INFO => self.handle_geometry(data, reqsz), + IPC_OP_FLASH_ERASE => self.handle_erase(data, reqsz), + IPC_OP_FLASH_PROGRAM => self.handle_program(data, reqsz), + IPC_OP_FLASH_READ => self.handle_read(data, reqsz), + _ => Err(error::IPC_ERROR_UNKNOWN_OP), + } + } + + /// Handles a single IPC request. + /// + /// This method performs a non-blocking read on the IPC handle. The caller + /// must ensure the handle is readable (e.g., by calling `syscall::object_wait`) + /// before calling this method. + pub fn handle_one(&mut self, ipc: &IpcHandle, data: &mut [u8]) -> Result<(), ErrorCode> { + let len = ipc.read(0, data).map_err(ErrorCode::kernel_error)?; + let (opcode, reqrsp) = data.split_at_mut(core::mem::size_of::()); + let opcode = Opcode::read_from_bytes(opcode).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let len = len.saturating_sub(core::mem::size_of::()); + + let mut status = 0u32; + let result = match self.handle_op(opcode, reqrsp, len) { + Ok(result) => result, + Err(e) => { + status = e.0.get(); + &[] + } + }; + ipc.respond(&[status.as_bytes(), result]) + .map_err(ErrorCode::kernel_error)?; + Ok(()) + } +} From a62696e178dda30aee8aa9385730797a0608ba05 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 05:11:15 -0700 Subject: [PATCH 03/11] util/error: add ErrorCode::check_status The flash-service branch's services/flash/client.rs calls ErrorCode::check_status, which was missing from util/error on this branch; restore it so the baseline compiles. --- util/error/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/util/error/lib.rs b/util/error/lib.rs index f5094086f..b2ee850e2 100644 --- a/util/error/lib.rs +++ b/util/error/lib.rs @@ -77,6 +77,15 @@ impl ErrorCode { pub fn kernel_error(e: pw_status::Error) -> Self { KERNEL_ERROR.error(e as u16) } + + /// Checks a wire status word: zero is success, any non-zero value is the + /// corresponding error code. + pub const fn check_status(status: u32) -> Result<(), ErrorCode> { + match NonZero::new(status) { + None => Ok(()), + Some(val) => Err(ErrorCode(val)), + } + } } impl From for u32 { From d5d322ecef351104adc9db2af5183b979350d56f Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 05:14:07 -0700 Subject: [PATCH 04/11] target/ast10x0: allow intra-page program starts in SpiNorFlash --- .../ast10x0/peripherals/smc/device/flash.rs | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/target/ast10x0/peripherals/smc/device/flash.rs b/target/ast10x0/peripherals/smc/device/flash.rs index 3815cfef0..3e6917bee 100644 --- a/target/ast10x0/peripherals/smc/device/flash.rs +++ b/target/ast10x0/peripherals/smc/device/flash.rs @@ -45,6 +45,21 @@ fn encode_addr_cmd(opcode: u8, offset: u32, width: AddressWidth) -> ([u8; 5], us } } +/// Validate a single page-program operation. +/// +/// SPI NOR page program wraps at page boundaries, so a write that crosses one +/// would silently corrupt the start of the page; reject it instead. Unaligned +/// starts *within* a page are legal. +fn validate_page_program_bounds(page_size: usize, offset: u32, len: usize) -> Result<(), SmcError> { + if page_size == 0 || len == 0 || len > page_size { + return Err(SmcError::InvalidCapacity); + } + if (offset as usize) % page_size + len > page_size { + return Err(SmcError::InvalidCapacity); + } + Ok(()) +} + /// Minimal SPI NOR flash device API. pub trait SpiNorFlashDevice { /// Read bytes from flash at `offset` into `buf`. @@ -436,13 +451,7 @@ impl<'a> SpiNorFlash<'a> { } fn validate_page_program(&self, offset: u32, data: &[u8]) -> Result<(), SmcError> { - let page_size = self.cfg.page_size as usize; - if page_size == 0 || data.is_empty() || data.len() > page_size { - return Err(SmcError::InvalidCapacity); - } - if (offset as usize) % page_size != 0 { - return Err(SmcError::InvalidCapacity); - } + validate_page_program_bounds(self.cfg.page_size as usize, offset, data.len())?; self.validate_range(offset, data.len()) } @@ -695,4 +704,43 @@ mod tests { Err(SmcError::HardwareError) ); } + + #[test] + fn page_program_bounds_accepts_aligned_full_page() { + assert_eq!(super::validate_page_program_bounds(256, 0x100, 256), Ok(())); + } + + #[test] + fn page_program_bounds_accepts_unaligned_within_page() { + assert_eq!(super::validate_page_program_bounds(256, 0x105, 37), Ok(())); + assert_eq!(super::validate_page_program_bounds(256, 0x1ff, 1), Ok(())); + } + + #[test] + fn page_program_bounds_rejects_page_crossing() { + assert_eq!( + super::validate_page_program_bounds(256, 0x1ff, 2), + Err(SmcError::InvalidCapacity) + ); + assert_eq!( + super::validate_page_program_bounds(256, 0x10, 256), + Err(SmcError::InvalidCapacity) + ); + } + + #[test] + fn page_program_bounds_rejects_empty_and_oversized() { + assert_eq!( + super::validate_page_program_bounds(256, 0x100, 0), + Err(SmcError::InvalidCapacity) + ); + assert_eq!( + super::validate_page_program_bounds(256, 0x100, 257), + Err(SmcError::InvalidCapacity) + ); + assert_eq!( + super::validate_page_program_bounds(0, 0x100, 16), + Err(SmcError::InvalidCapacity) + ); + } } From 4360336a48c6f31b4c7a922804fafc827ac6ca1e Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 05:17:06 -0700 Subject: [PATCH 05/11] util/error: add AST10x0 flash error module --- util/error/flash.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/util/error/flash.rs b/util/error/flash.rs index 976bfb4ab..b5c8360e3 100644 --- a/util/error/flash.rs +++ b/util/error/flash.rs @@ -61,3 +61,36 @@ pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_LONG: ErrorCode = /// The OpenTitan flash error module. pub const FLASH_OPENTITAN: ErrorModule = ErrorModule::new(0x464f); //ascii `FO`. + +/// The AST10x0 SMC/FMC flash error module. +pub const FLASH_AST10X0: ErrorModule = ErrorModule::new(0x4641); //ascii `FA`. + +/// Hardware-level failure reported by the SMC controller. +pub const FLASH_AST10X0_HARDWARE_ERROR: ErrorCode = FLASH_AST10X0.from_pw(0, Error::Internal); +/// Timed out waiting for a flash operation to complete. +pub const FLASH_AST10X0_TIMEOUT: ErrorCode = FLASH_AST10X0.from_pw(1, Error::DeadlineExceeded); +/// DMA transfer aborted. +pub const FLASH_AST10X0_DMA_ABORTED: ErrorCode = FLASH_AST10X0.from_pw(2, Error::Aborted); +/// DMA transfer length mismatch. +pub const FLASH_AST10X0_DMA_LENGTH_MISMATCH: ErrorCode = FLASH_AST10X0.from_pw(3, Error::DataLoss); +/// Invalid chip select. +pub const FLASH_AST10X0_INVALID_CHIP_SELECT: ErrorCode = + FLASH_AST10X0.from_pw(4, Error::InvalidArgument); +/// Invalid or unsupported capacity/range. +pub const FLASH_AST10X0_INVALID_CAPACITY: ErrorCode = FLASH_AST10X0.from_pw(5, Error::OutOfRange); +/// Attached flash device not supported. +pub const FLASH_AST10X0_DEVICE_NOT_SUPPORTED: ErrorCode = + FLASH_AST10X0.from_pw(6, Error::Unimplemented); +/// Flash is write-protected. +pub const FLASH_AST10X0_WRITE_PROTECTED: ErrorCode = + FLASH_AST10X0.from_pw(7, Error::PermissionDenied); +/// A write is already in progress. +pub const FLASH_AST10X0_WRITE_IN_PROGRESS: ErrorCode = FLASH_AST10X0.from_pw(8, Error::Unavailable); +/// Controller not in the Ready lifecycle state. +pub const FLASH_AST10X0_CONTROLLER_NOT_READY: ErrorCode = + FLASH_AST10X0.from_pw(9, Error::FailedPrecondition); +/// DMA requested but not enabled in the controller config. +pub const FLASH_AST10X0_DMA_NOT_ENABLED: ErrorCode = + FLASH_AST10X0.from_pw(10, Error::FailedPrecondition); +/// A read returned fewer bytes than requested. +pub const FLASH_AST10X0_SHORT_READ: ErrorCode = FLASH_AST10X0.from_pw(11, Error::DataLoss); From badbb40c2dd053adcacd8199de272ba398d290f9 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 05:21:00 -0700 Subject: [PATCH 06/11] target/ast10x0: add FMC flash service backend --- target/ast10x0/backend/flash/BUILD.bazel | 20 +++ target/ast10x0/backend/flash/src/lib.rs | 166 +++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 target/ast10x0/backend/flash/BUILD.bazel create mode 100644 target/ast10x0/backend/flash/src/lib.rs diff --git a/target/ast10x0/backend/flash/BUILD.bazel b/target/ast10x0/backend/flash/BUILD.bazel new file mode 100644 index 000000000..e731dea69 --- /dev/null +++ b/target/ast10x0/backend/flash/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_backend_ast10x0", + srcs = ["src/lib.rs"], + crate_name = "flash_backend", + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash:driver", + "//target/ast10x0/peripherals", + "//util/error", + "//util/types", + ], +) diff --git a/target/ast10x0/backend/flash/src/lib.rs b/target/ast10x0/backend/flash/src/lib.rs new file mode 100644 index 000000000..5167eb52d --- /dev/null +++ b/target/ast10x0/backend/flash/src/lib.rs @@ -0,0 +1,166 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 FMC backend for the generic flash service. +//! +//! Adapts the SMC/FMC SPI-NOR peripheral driver to `hal_flash_driver::FlashDriver` +//! so it can be wrapped by `hal_flash::BlockingFlash` and served over IPC by +//! `services_flash_server::FlashIpcServer`. + +#![no_std] + +use core::num::NonZero; + +use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; +use ast10x0_peripherals::scu::ScuRegisters; +use ast10x0_peripherals::smc::{ + ChipSelect, FlashConfig, FmcReady, FmcUninit, SmcConfig, SmcController, SmcError, SmcTopology, + SpiNorFlash, SpiNorFlashDevice, +}; +use hal_flash_driver::{FlashAddress, FlashDriver}; +use util_error::{self as error, ErrorCode}; +use util_types::{Blocking, PowerOf2Usize}; + +/// CS0 flash device configuration (W25Q64-class, 8 MiB). +/// +/// Matches the hardware-verified configuration used by +/// //target/ast10x0/tests/smc/write. +const CS0_CONFIG: FlashConfig = FlashConfig { + capacity_mb: 8, + page_size: 256, + sector_size: 4096, + block_size: 65536, + spi_clock_mhz: 50, +}; + +fn map_smc_error(e: SmcError) -> ErrorCode { + match e { + SmcError::HardwareError => error::FLASH_AST10X0_HARDWARE_ERROR, + SmcError::Timeout => error::FLASH_AST10X0_TIMEOUT, + SmcError::DmaAborted => error::FLASH_AST10X0_DMA_ABORTED, + SmcError::DmaLengthMismatch => error::FLASH_AST10X0_DMA_LENGTH_MISMATCH, + SmcError::InvalidChipSelect => error::FLASH_AST10X0_INVALID_CHIP_SELECT, + SmcError::InvalidCapacity => error::FLASH_AST10X0_INVALID_CAPACITY, + SmcError::DeviceNotSupported => error::FLASH_AST10X0_DEVICE_NOT_SUPPORTED, + SmcError::WriteProtected => error::FLASH_AST10X0_WRITE_PROTECTED, + SmcError::WriteInProgress => error::FLASH_AST10X0_WRITE_IN_PROGRESS, + SmcError::ControllerNotReady => error::FLASH_AST10X0_CONTROLLER_NOT_READY, + SmcError::DmaNotEnabled => error::FLASH_AST10X0_DMA_NOT_ENABLED, + } +} + +/// No-op `Blocking` impl paired with this driver. +/// +/// FMC user-mode SPI-NOR commands have no completion interrupt; the peripheral +/// driver polls the device's WIP status bit to completion inside +/// `program_page`/`erase_sector`, so `start_*` below return with the operation +/// already finished and there is nothing to wait for. +pub struct NoWaitBlocking; + +impl Blocking for NoWaitBlocking { + fn wait_for_notification(&self) {} +} + +/// FMC CS0 flash driver. +pub struct Ast10x0FmcFlashDriver { + fmc: FmcReady, +} + +/// Stable alias used by the server binary for compile-time backend selection. +pub type Backend = Ast10x0FmcFlashDriver; + +impl Ast10x0FmcFlashDriver { + /// Initialize the FMC and return a ready driver. + /// + /// # Safety + /// The calling process must be the sole owner of the FMC controller + /// (MMIO 0x7e62_0000), its CS0 flash window (0x8000_0000), and must have + /// the SCU (0x7e6e_2000) mapped for pinctrl, per the system.json5 of the + /// image this runs in. Call at most once per process. + pub unsafe fn new() -> Result { + // SAFETY: sole ownership of the SCU mapping per the contract above. + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); + + let config = SmcConfig { + controller_id: SmcController::Fmc, + cs0: Some(CS0_CONFIG), + cs1: None, + dma_enabled: false, + enable_interrupts: false, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; + // SAFETY: sole ownership of the FMC hardware block per the contract above. + let uninit = unsafe { FmcUninit::new(config) }.map_err(map_smc_error)?; + let mut fmc = uninit.init().map_err(map_smc_error)?; + fmc.spi_nor_read_init(ChipSelect::Cs0).map_err(map_smc_error)?; + Ok(Self { fmc }) + } + + fn device(&mut self) -> Result, ErrorCode> { + SpiNorFlash::from_fmc_cs(&mut self.fmc, CS0_CONFIG, ChipSelect::Cs0).map_err(map_smc_error) + } +} + +impl FlashDriver for Ast10x0FmcFlashDriver { + type Error = ErrorCode; + + /// Default erase page: one 4 KiB sector. + const PAGE_SIZE: usize = CS0_CONFIG.sector_size as usize; + /// SPI NOR program page: writes must not cross a 256-byte boundary. + const PROGRAM_WINDOW_SIZE: usize = CS0_CONFIG.page_size as usize; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 1; + + fn size(&self) -> NonZero { + NonZero::new(CS0_CONFIG.capacity_mb as usize * 1024 * 1024).unwrap() + } + + fn erasable_sizes_bitmap(&mut self) -> Result { + // Only 4 KiB sector erase is implemented by the peripheral driver. + Ok(1u32 << CS0_CONFIG.sector_size.trailing_zeros()) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error> { + let len = buf.len(); + let n = self + .device()? + .read(start_addr.offset(), buf) + .map_err(map_smc_error)?; + if n != len { + return Err(error::FLASH_AST10X0_SHORT_READ); + } + Ok(()) + } + + fn start_erase( + &mut self, + start_addr: FlashAddress, + size: PowerOf2Usize, + ) -> Result<(), Self::Error> { + if size.get() != CS0_CONFIG.sector_size as usize { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + } + // Blocks until the device's WIP bit clears; see `NoWaitBlocking`. + self.device()? + .erase_sector(start_addr.offset()) + .map_err(map_smc_error) + } + + fn start_program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), Self::Error> { + // Blocks until the device's WIP bit clears; see `NoWaitBlocking`. + self.device()? + .program_page(start_addr.offset(), data) + .map_err(map_smc_error)?; + Ok(()) + } + + fn is_busy(&mut self) -> bool { + false + } + + fn complete_op(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} From 8ea6e5d75fb17e4f4bbb31893b3de2039b5e6bbc Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 06:02:26 -0700 Subject: [PATCH 07/11] target/ast10x0: add flash service server image scaffolding Adds the AST10x0 flash-service test image's system config, generic kernel target, and server app (flash_server_bin). The server process owns the SCU (pinctrl), FMC register, and CS0 AHB read-window device mappings, and serves services_flash_server::FlashIpcServer requests over the "flash" channel using the Task 3 FMC backend. The client app, system_image, and tests are added by the next task. --- target/ast10x0/tests/flash/BUILD.bazel | 69 +++++++++++++++++ target/ast10x0/tests/flash/server_main.rs | 53 +++++++++++++ target/ast10x0/tests/flash/system.json5 | 93 +++++++++++++++++++++++ target/ast10x0/tests/flash/target.rs | 39 ++++++++++ 4 files changed, 254 insertions(+) create mode 100644 target/ast10x0/tests/flash/BUILD.bazel create mode 100644 target/ast10x0/tests/flash/server_main.rs create mode 100644 target/ast10x0/tests/flash/system.json5 create mode 100644 target/ast10x0/tests/flash/target.rs diff --git a/target/ast10x0/tests/flash/BUILD.bazel b/target/ast10x0/tests/flash/BUILD.bazel new file mode 100644 index 000000000..56c855560 --- /dev/null +++ b/target/ast10x0/tests/flash/BUILD.bazel @@ -0,0 +1,69 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@rust_crates//:cortex-m-semihosting", + ], +) + +rust_app( + name = "flash_server_bin", + srcs = ["server_main.rs"], + codegen_crate_name = "app_flash_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//hal/blocking/flash:flash", + "//services/flash:server", + "//target/ast10x0/backend/flash:flash_backend_ast10x0", + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/target/ast10x0/tests/flash/server_main.rs b/target/ast10x0/tests/flash/server_main.rs new file mode 100644 index 000000000..02b45442b --- /dev/null +++ b/target/ast10x0/tests/flash/server_main.rs @@ -0,0 +1,53 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_flash_server::handle; +use flash_backend::{Backend, NoWaitBlocking}; +use hal_flash::BlockingFlash; +use services_flash_server::FlashIpcServer; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::Instant; +use util_ipc::IpcHandle; + +/// IPC buffer: must hold the largest request/response. Reads are bounded by +/// the client's buffer and this size; 4 KiB of payload + opcode/status headroom. +const IPC_BUF_SIZE: usize = 4352; + +#[entry] +fn entry() { + // SAFETY: this process is the sole owner of the SCU/FMC/CS0-window + // mappings declared in system.json5, and this runs once. + let driver = match unsafe { Backend::new() } { + Ok(d) => d, + Err(e) => { + pw_log::error!("flash server: FMC init failed: {:08x}", e.0.get() as u32); + let _ = syscall::debug_shutdown(Err(pw_status::Error::Internal)); + loop {} + } + }; + let flash = BlockingFlash { + driver, + blocking: NoWaitBlocking, + }; + let mut server = FlashIpcServer::new(flash); + let mut buf = [0u8; IPC_BUF_SIZE]; + + pw_log::info!("flash server: ready"); + loop { + if syscall::object_wait(handle::FLASH, Signals::READABLE, Instant::MAX).is_err() { + continue; + } + if let Err(e) = server.handle_one(&IpcHandle::new(handle::FLASH), &mut buf) { + pw_log::error!("flash server: request failed: {:08x}", e.0.get() as u32); + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/flash/system.json5 b/target/ast10x0/tests/flash/system.json5 new file mode 100644 index 000000000..c290bd77d --- /dev/null +++ b/target/ast10x0/tests/flash/system.json5 @@ -0,0 +1,93 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 Flash Service Configuration +// ARM Cortex-M4 @ 200 MHz with 768KB SRAM (0x00000000 - 0x000BFFFF) +// NOTE: AST10x0 does not support XIP - firmware executes from RAM. +// +// PMSAv7-Friendly Memory Layout (same as ../usart/system.json5): +// 0x00000000 - 0x00000500: Vector table (1280 bytes) +// 0x00000500 - 0x00020000: Kernel code (~126KB, ends at 128KB boundary) +// 0x00020000 - 0x00060000: Flash server + client app flash (256KB) +// 0x00060000 - 0x00080000: Kernel RAM (128KB) +// 0x00080000 - 0x000A0000: App RAM (128KB) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, // 0x500 + }, + kernel: { + flash_start_address: 0x00000500, // After vector table + flash_size_bytes: 129792, // ~126KB (ends at 0x00020000) + ram_start_address: 0x00060000, // After all flash regions + ram_size_bytes: 131072, // 128KB + }, + apps: [ + { + name: "flash_server_bin", + flash_size_bytes: 131072, // 128KB for server code + processes: [ + { + name: "flash_server_process", + ram_size_bytes: 65536, // 64KB RAM (holds the 4KB+ IPC buffer) + objects: [ + { + name: "flash", + type: "channel_handler", // Server side of client→server channel + }, + { + type: "thread", + name: "flash_server_thread", + kernel_stack_size_bytes: 4096, // 4KB stack + },], + memory_mappings: [ + { + // SCU — needed once at init for FMC pinctrl. + name: "scu", + type: "device", + start_address: 0x7e6e2000, + size_bytes: 0x1000, + }, + { + // FMC controller registers. + name: "fmc_regs", + type: "device", + start_address: 0x7e620000, + size_bytes: 0x1000, + }, + { + // FMC CS0 memory-mapped flash read window (8 MiB). + name: "fmc_cs0_window", + type: "device", + start_address: 0x80000000, + size_bytes: 0x800000, + }, + ], + }, + ], + }, + { + name: "flash_client_app", + flash_size_bytes: 65536, // 64KB for client code + processes: [ + { + name: "flash_client_process", + ram_size_bytes: 32768, // 32KB RAM + objects: [ + { + name: "flash", + type: "channel_initiator", + handler_process: "flash_server_process", + handler_object_name: "flash", + }, + { + type: "thread", + name: "flash_client_thread", + kernel_stack_size_bytes: 2048, + },], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/flash/target.rs b/target/ast10x0/tests/flash/target.rs new file mode 100644 index 000000000..5be5ec228 --- /dev/null +++ b/target/ast10x0/tests/flash/target.rs @@ -0,0 +1,39 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 Flash Service Target +//! +//! This target runs the flash server as a userspace process. +//! Clients can communicate with it over an IPC channel. + +#![no_std] +#![no_main] + +use cortex_m_semihosting::debug::{exit, EXIT_FAILURE, EXIT_SUCCESS}; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 Flash Service"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + let status = if code == 0 { + EXIT_SUCCESS + } else { + EXIT_FAILURE + }; + exit(status); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); From d4080276965ca97235a2c75d71a4038d5d99c026 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 06:24:03 -0700 Subject: [PATCH 08/11] target/ast10x0: add flash service client test and system image --- target/ast10x0/tests/flash/BUILD.bazel | 53 ++++++++++ target/ast10x0/tests/flash/client_main.rs | 118 ++++++++++++++++++++++ target/ast10x0/tests/flash/server_main.rs | 5 +- target/ast10x0/tests/flash/system.json5 | 9 +- target/ast10x0/tests/flash/target.rs | 10 ++ 5 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 target/ast10x0/tests/flash/client_main.rs diff --git a/target/ast10x0/tests/flash/BUILD.bazel b/target/ast10x0/tests/flash/BUILD.bazel index 56c855560..9fa65cae9 100644 --- a/target/ast10x0/tests/flash/BUILD.bazel +++ b/target/ast10x0/tests/flash/BUILD.bazel @@ -40,6 +40,7 @@ rust_binary( ":codegen", ":linker_script", "//target/ast10x0:entry", + "//target/ast10x0/peripherals", "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", "@pigweed//pw_kernel/kernel", "@pigweed//pw_kernel/subsys/console:console_backend", @@ -67,3 +68,55 @@ rust_app( "@pigweed//pw_status/rust:pw_status", ], ) + +rust_app( + name = "flash_client_app", + srcs = ["client_main.rs"], + codegen_crate_name = "app_flash_client", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//hal/blocking/flash:flash", + "//services/flash:client", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "flash", + apps = [ + ":flash_server_bin", + ":flash_client_app", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +# FMC flash is not modeled under QEMU in this repo (same restriction as +# //target/ast10x0/tests/smc/...): run on EVB hardware. +system_image_test( + name = "flash_evb_test", + image = ":flash", + tags = ["hardware"], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":flash", + tags = ["kernel"], +) diff --git a/target/ast10x0/tests/flash/client_main.rs b/target/ast10x0/tests/flash/client_main.rs new file mode 100644 index 000000000..b50e87055 --- /dev/null +++ b/target/ast10x0/tests/flash/client_main.rs @@ -0,0 +1,118 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_flash_client::handle; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use userspace::entry; +use userspace::syscall; +use util_ipc::IpcHandle; + +/// 1 MiB in: same offset the on-hardware smc write test uses; clear of code. +const TEST_OFFSET: u32 = 0x0010_0000; +const SECTOR: usize = 4096; + +fn fail(msg: &str) -> ! { + pw_log::error!("flash client FAIL: {}", msg as &str); + let _ = syscall::debug_shutdown(Err(pw_status::Error::Internal)); + loop {} +} + +fn pattern(i: usize) -> u8 { + (i as u8).wrapping_mul(31).wrapping_add(7) +} + +#[entry] +fn entry() { + let mut flash = match FlashIpcClient::new(IpcHandle::new(handle::FLASH)) { + Ok(c) => c, + Err(_) => fail("connect/geometry"), + }; + + // 1. Geometry matches the backend's CS0 config. + let Ok((total, page, bitmap)) = flash.geometry() else { + fail("geometry"); + }; + if total.get() != 8 * 1024 * 1024 { + fail("total size"); + } + if page.get() != SECTOR { + fail("page size"); + } + if bitmap != 1 << 12 { + fail("erase bitmap"); + } + + // 2. Erase one sector, verify it reads back erased. + if flash.erase(FlashAddress::new(TEST_OFFSET), page).is_err() { + fail("erase"); + } + let mut buf = [0u8; 64]; + if flash.read(FlashAddress::new(TEST_OFFSET), &mut buf).is_err() { + fail("read after erase"); + } + if buf.iter().any(|&b| b != 0xff) { + fail("not erased"); + } + + // 3. Unaligned program crossing a 256-byte program-page boundary: + // starts at +250, 300 bytes -> exercises BlockingFlash window + // splitting and the intra-page start relaxation. + let mut data = [0u8; 300]; + for (i, b) in data.iter_mut().enumerate() { + *b = pattern(i); + } + if flash + .program(FlashAddress::new(TEST_OFFSET + 250), &data) + .is_err() + { + fail("program"); + } + + // 4. Read back and verify, including the untouched prefix. + let mut rb = [0u8; 600]; + if flash.read(FlashAddress::new(TEST_OFFSET), &mut rb).is_err() { + fail("read back"); + } + if rb[..250].iter().any(|&b| b != 0xff) { + fail("prefix clobbered"); + } + for i in 0..300 { + if rb[250 + i] != pattern(i) { + fail("data mismatch"); + } + } + if rb[550..].iter().any(|&b| b != 0xff) { + fail("suffix clobbered"); + } + + // 5. Error paths: bad erase size, out-of-bounds read. + if flash + .erase( + FlashAddress::new(TEST_OFFSET), + util_types::PowerOf2Usize::new(512).unwrap(), + ) + .is_ok() + { + fail("erase size not rejected"); + } + let mut oob = [0u8; 16]; + if flash + .read(FlashAddress::new(0x0100_0000), &mut oob) + .is_ok() + { + fail("oob read not rejected"); + } + + pw_log::info!("flash client PASS"); + let _ = syscall::debug_shutdown(Ok(())); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/flash/server_main.rs b/target/ast10x0/tests/flash/server_main.rs index 02b45442b..aa9bbb2e4 100644 --- a/target/ast10x0/tests/flash/server_main.rs +++ b/target/ast10x0/tests/flash/server_main.rs @@ -19,8 +19,9 @@ const IPC_BUF_SIZE: usize = 4352; #[entry] fn entry() { - // SAFETY: this process is the sole owner of the SCU/FMC/CS0-window - // mappings declared in system.json5, and this runs once. + // SAFETY: this process is the sole owner of the FMC/CS0-window mappings + // declared in system.json5, the kernel target applied the FMC pinmux + // before starting any process, and this runs once. let driver = match unsafe { Backend::new() } { Ok(d) => d, Err(e) => { diff --git a/target/ast10x0/tests/flash/system.json5 b/target/ast10x0/tests/flash/system.json5 index c290bd77d..4e6b5cfa2 100644 --- a/target/ast10x0/tests/flash/system.json5 +++ b/target/ast10x0/tests/flash/system.json5 @@ -42,13 +42,8 @@ kernel_stack_size_bytes: 4096, // 4KB stack },], memory_mappings: [ - { - // SCU — needed once at init for FMC pinctrl. - name: "scu", - type: "device", - start_address: 0x7e6e2000, - size_bytes: 0x1000, - }, + // FMC pinctrl is applied by the kernel target's + // pre-task init, so the server does not map the SCU. { // FMC controller registers. name: "fmc_regs", diff --git a/target/ast10x0/tests/flash/target.rs b/target/ast10x0/tests/flash/target.rs index 5be5ec228..406f25b60 100644 --- a/target/ast10x0/tests/flash/target.rs +++ b/target/ast10x0/tests/flash/target.rs @@ -9,6 +9,8 @@ #![no_std] #![no_main] +use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; +use ast10x0_peripherals::scu::ScuRegisters; use cortex_m_semihosting::debug::{exit, EXIT_FAILURE, EXIT_SUCCESS}; use target_common::{declare_target, TargetInterface}; use {console_backend as _, entry as _}; @@ -19,6 +21,14 @@ impl TargetInterface for Target { const NAME: &'static str = "AST10x0 Flash Service"; fn main() -> ! { + // Static pinmux configuration, applied before any process starts so + // no task ever needs SCU access (avoids cross-task RMW races on the + // shared pinctrl registers). + // SAFETY: kernel main() runs once, single-threaded, with exclusive + // hardware ownership. + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); + codegen::start(); #[expect(clippy::empty_loop)] loop {} From c64533ea79ffefc487d8830dd4542ea6b90c9e42 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 06:25:25 -0700 Subject: [PATCH 09/11] target/ast10x0: move FMC pinmux from flash backend to kernel target init Completes the pinmux-ownership change already present in the flash test image (system.json5 no longer maps the SCU into the server process): the backend driver no longer touches the shared SCU; PINCTRL_FMC_QUAD is applied once by the kernel target before any process starts. --- target/ast10x0/backend/flash/src/lib.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/target/ast10x0/backend/flash/src/lib.rs b/target/ast10x0/backend/flash/src/lib.rs index 5167eb52d..6bca640c2 100644 --- a/target/ast10x0/backend/flash/src/lib.rs +++ b/target/ast10x0/backend/flash/src/lib.rs @@ -11,8 +11,6 @@ use core::num::NonZero; -use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; -use ast10x0_peripherals::scu::ScuRegisters; use ast10x0_peripherals::smc::{ ChipSelect, FlashConfig, FmcReady, FmcUninit, SmcConfig, SmcController, SmcError, SmcTopology, SpiNorFlash, SpiNorFlashDevice, @@ -74,14 +72,12 @@ impl Ast10x0FmcFlashDriver { /// /// # Safety /// The calling process must be the sole owner of the FMC controller - /// (MMIO 0x7e62_0000), its CS0 flash window (0x8000_0000), and must have - /// the SCU (0x7e6e_2000) mapped for pinctrl, per the system.json5 of the - /// image this runs in. Call at most once per process. + /// (MMIO 0x7e62_0000) and its CS0 flash window (0x8000_0000), per the + /// system.json5 of the image this runs in. The FMC pinmux + /// (`PINCTRL_FMC_QUAD`) must already have been applied by the kernel + /// target's pre-task init; this driver never touches the shared SCU. + /// Call at most once per process. pub unsafe fn new() -> Result { - // SAFETY: sole ownership of the SCU mapping per the contract above. - let scu = unsafe { ScuRegisters::new_global_unlocked() }; - scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); - let config = SmcConfig { controller_id: SmcController::Fmc, cs0: Some(CS0_CONFIG), @@ -93,7 +89,8 @@ impl Ast10x0FmcFlashDriver { // SAFETY: sole ownership of the FMC hardware block per the contract above. let uninit = unsafe { FmcUninit::new(config) }.map_err(map_smc_error)?; let mut fmc = uninit.init().map_err(map_smc_error)?; - fmc.spi_nor_read_init(ChipSelect::Cs0).map_err(map_smc_error)?; + fmc.spi_nor_read_init(ChipSelect::Cs0) + .map_err(map_smc_error)?; Ok(Self { fmc }) } From d7cf97453b4e3589a8444103c546041deede21b2 Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 17:38:19 -0700 Subject: [PATCH 10/11] ast10x0: run flash service test under QEMU via fmc-model=w25q64 + seeded mtd image --- target/ast10x0/defs.bzl | 42 ++++++++++++++++++++ target/ast10x0/harness/qemu_runner.py | 54 +++++++++++++++++++++++++- target/ast10x0/tests/flash/BUILD.bazel | 18 +++++++-- 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index 7e2e827bb..a8624574a 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl @@ -36,6 +36,18 @@ def _system_image_test_impl(ctx): runfiles = runfiles, )] +def _flash_system_image_test_impl(ctx): + default_info = _system_image_test_impl(ctx)[0] + providers = [default_info] + if ctx.attr.flash_image: + # The qemu_runner seeds a fresh erased image at $TEST_TMPDIR/ + # and attaches it as the FMC CS0 flash (if=mtd). + providers.append(RunEnvironmentInfo(environment = { + "AST10X0_FLASH_IMAGE": ctx.attr.flash_image, + "AST10X0_FLASH_SIZE": str(ctx.attr.flash_size), + })) + return providers + system_image_test = rule( implementation = _system_image_test_impl, test = True, @@ -56,3 +68,33 @@ system_image_test = rule( ), }, ) + +flash_system_image_test = rule( + implementation = _flash_system_image_test_impl, + test = True, + attrs = { + "flash_image": attr.string( + doc = "Basename of the SPI-NOR image the qemu_runner seeds in " + + "$TEST_TMPDIR and attaches as FMC CS0 flash (if=mtd).", + default = "cs0.img", + ), + "flash_size": attr.int( + doc = "Size in bytes of the seeded flash image.", + default = 8 * 1024 * 1024, + ), + "image": attr.label( + doc = "The system_image target to test.", + mandatory = True, + providers = [SystemImageInfo], + executable = True, + cfg = "target", + ), + "slave_image": attr.label( + doc = "Optional slave system_image for paired two-device tests.", + mandatory = False, + default = None, + providers = [SystemImageInfo], + cfg = "target", + ), + }, +) diff --git a/target/ast10x0/harness/qemu_runner.py b/target/ast10x0/harness/qemu_runner.py index 198698a8e..ef842a0f9 100644 --- a/target/ast10x0/harness/qemu_runner.py +++ b/target/ast10x0/harness/qemu_runner.py @@ -55,6 +55,19 @@ def _parse_args(): parser.add_argument( "--qemu-args", nargs="*", help="Extra arguments to pass to qemu" ) + parser.add_argument( + "--flash-image", + type=str, + help="Path to a raw SPI-NOR image to attach as the FMC CS0 flash " + "(if=mtd). Re-seeded to an erased (0xFF) state of --flash-size bytes " + "on every run so tests start from a known device state.", + ) + parser.add_argument( + "--flash-size", + type=int, + default=8 * 1024 * 1024, + help="Size in bytes of the --flash-image backing store (default: 8 MiB).", + ) parser.add_argument( "--timeout", type=int, @@ -118,11 +131,43 @@ def _sentinel_watcher( print(f"Exception watching sentinel: {e}", file=sys.stderr) +def _seed_flash_image(path: str, size: int) -> None: + """Create/overwrite `path` with `size` bytes of 0xFF (erased NOR state).""" + with open(path, "wb") as f: + f.write(b"\xff" * size) + + +def _resolve_flash_image(args): + """Return (path, size) for the FMC CS0 backing image, or (None, 0). + + An explicit --flash-image wins. Otherwise a flash_system_image_test sets + AST10X0_FLASH_IMAGE (basename) + AST10X0_FLASH_SIZE, resolved against + $TEST_TMPDIR so each test run gets a private, freshly-seeded image. + """ + if args.flash_image: + return args.flash_image, args.flash_size + name = os.environ.get("AST10X0_FLASH_IMAGE") + if not name: + return None, 0 + base = os.environ.get("TEST_TMPDIR", tempfile.gettempdir()) + size = int(os.environ.get("AST10X0_FLASH_SIZE", str(args.flash_size))) + return os.path.join(base, name), size + + def _main(args) -> None: + flash_path, flash_size = _resolve_flash_image(args) + + machine = args.machine + if flash_path: + # ast1030-evb defaults its FMC CS0 chip to a 1 MiB w25q80bl; the FMC + # backend drives an 8 MiB W25Q64-class device, so model the matching + # chip (and size the backing image to it below). + machine = f"{machine},fmc-model=w25q64" + qemu_args = [ _QEMU_ARM, "-machine", - args.machine, + machine, "-cpu", args.cpu, "-bios", @@ -136,6 +181,13 @@ def _main(args) -> None: args.image, ] + if flash_path: + _seed_flash_image(flash_path, flash_size) + qemu_args += [ + "-drive", + f"file={flash_path},format=raw,if=mtd", + ] + if args.qemu_args: qemu_args.extend(args.qemu_args) diff --git a/target/ast10x0/tests/flash/BUILD.bazel b/target/ast10x0/tests/flash/BUILD.bazel index 9fa65cae9..60d32a7db 100644 --- a/target/ast10x0/tests/flash/BUILD.bazel +++ b/target/ast10x0/tests/flash/BUILD.bazel @@ -7,7 +7,7 @@ load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "flash_system_image_test") filegroup( name = "system_config", @@ -59,7 +59,7 @@ rust_app( tags = ["kernel"], target_compatible_with = TARGET_COMPATIBLE_WITH, deps = [ - "//hal/blocking/flash:flash", + "//hal/blocking/flash", "//services/flash:server", "//target/ast10x0/backend/flash:flash_backend_ast10x0", "//util/ipc", @@ -78,7 +78,7 @@ rust_app( tags = ["kernel"], target_compatible_with = TARGET_COMPATIBLE_WITH, deps = [ - "//hal/blocking/flash:flash", + "//hal/blocking/flash", "//services/flash:client", "//util/ipc", "//util/types", @@ -115,6 +115,18 @@ system_image_test( visibility = ["//visibility:public"], ) +# QEMU variant: the runner seeds an 8 MiB erased image and attaches it as the +# FMC CS0 flash (if=mtd), so program/erase/read-back can run under emulation. +# bazelisk test --config=virt_ast10x0 //target/ast10x0/tests/flash:flash_qemu_test +flash_system_image_test( + name = "flash_qemu_test", + flash_image = "cs0.img", + image = ":flash", + tags = ["qemu_only"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + rust_binary_no_panics_test( name = "no_panics_test", binary = ":flash", From 063b91f6b24ea0dfd3d1dd3b00d1d3de3f30d64e Mon Sep 17 00:00:00 2001 From: Anthony Rocha Date: Wed, 19 Aug 2026 18:24:14 -0700 Subject: [PATCH 11/11] ast10x0: make flash test non-destructive with sector backup/restore (mirrors smc/write) --- target/ast10x0/tests/flash/client_main.rs | 43 ++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/target/ast10x0/tests/flash/client_main.rs b/target/ast10x0/tests/flash/client_main.rs index b50e87055..15ab68a20 100644 --- a/target/ast10x0/tests/flash/client_main.rs +++ b/target/ast10x0/tests/flash/client_main.rs @@ -12,6 +12,9 @@ use userspace::syscall; use util_ipc::IpcHandle; /// 1 MiB in: same offset the on-hardware smc write test uses; clear of code. +/// The test is non-destructive on hardware: the whole sector is backed up +/// before the first erase and restored + verified at the end, exactly as +/// //target/ast10x0/tests/smc/write does. const TEST_OFFSET: u32 = 0x0010_0000; const SECTOR: usize = 4096; @@ -46,12 +49,25 @@ fn entry() { fail("erase bitmap"); } + // Back up the whole sector before any destructive op so the test restores + // the original contents on real hardware (mirrors smc/write). + let mut backup = [0u8; SECTOR]; + if flash + .read(FlashAddress::new(TEST_OFFSET), &mut backup) + .is_err() + { + fail("backup read"); + } + // 2. Erase one sector, verify it reads back erased. if flash.erase(FlashAddress::new(TEST_OFFSET), page).is_err() { fail("erase"); } let mut buf = [0u8; 64]; - if flash.read(FlashAddress::new(TEST_OFFSET), &mut buf).is_err() { + if flash + .read(FlashAddress::new(TEST_OFFSET), &mut buf) + .is_err() + { fail("read after erase"); } if buf.iter().any(|&b| b != 0xff) { @@ -100,11 +116,30 @@ fn entry() { fail("erase size not rejected"); } let mut oob = [0u8; 16]; + if flash.read(FlashAddress::new(0x0100_0000), &mut oob).is_ok() { + fail("oob read not rejected"); + } + + // Restore the original sector contents and verify (mirrors smc/write's + // restore_sector: erase -> program original -> read-back compare). + if flash.erase(FlashAddress::new(TEST_OFFSET), page).is_err() { + fail("restore erase"); + } if flash - .read(FlashAddress::new(0x0100_0000), &mut oob) - .is_ok() + .program(FlashAddress::new(TEST_OFFSET), &backup) + .is_err() { - fail("oob read not rejected"); + fail("restore program"); + } + let mut restored = [0u8; SECTOR]; + if flash + .read(FlashAddress::new(TEST_OFFSET), &mut restored) + .is_err() + { + fail("restore read"); + } + if restored != backup { + fail("restore verify"); } pw_log::info!("flash client PASS");