diff --git a/MODULE.bazel b/MODULE.bazel index c91cc96fa..255a90922 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -22,7 +22,7 @@ local_path_override( git_override( module_name = "pigweed", - commit = "6f0aac30f8313cbd5103686097a1f6b3098e8aac", # roll:pigweed + commit = "f540ae6c33424230ed2d388599efa89b6baacf80", patch_args = ["-p1"], patches = [ # Program the VeeR external-interrupt redirect table (MEIVT) in diff --git a/hal/blocking/flash/driver.rs b/hal/blocking/flash/driver.rs index 378152bf0..9d1ce5517 100644 --- a/hal/blocking/flash/driver.rs +++ b/hal/blocking/flash/driver.rs @@ -23,12 +23,18 @@ pub trait FlashDriver { /// The error type returned by driver operations. type Error; - /// The default page size in bytes. - const PAGE_SIZE: usize; + /// The page size in bytes. + /// + /// Prefer the [`page_size`](Self::page_size) method which is flexible to serve + /// statically defined or runtime discovered values. + const PAGE_SIZE: usize = 0; /// The maximum size of a single program operation (write window). /// Program operations cannot span across boundaries aligned to this size. - const PROGRAM_WINDOW_SIZE: usize; + /// + /// Prefer the [`program_window_size`](Self::program_window_size) method + /// which is flexible to serve statically defined or runtime discovered values. + const PROGRAM_WINDOW_SIZE: usize = 0; /// The maximum size of a single read operation. const MAX_READ_SIZE: usize; @@ -42,6 +48,17 @@ pub trait FlashDriver { /// Returns the total size of the flash in bytes. fn size(&self) -> NonZero; + /// Page size in bytes. + fn page_size(&self) -> usize { + Self::PAGE_SIZE + } + + /// The maximum size of a single program operation (write window). + /// Program operations cannot span across boundaries aligned to this size. + fn program_window_size(&self) -> usize { + Self::PROGRAM_WINDOW_SIZE + } + /// Returns a bitmap of supported erase block sizes. /// /// Each bit `i` represents a supported erase block size of `2^i` bytes. diff --git a/hal/blocking/flash/flash.rs b/hal/blocking/flash/flash.rs index 072e83c7d..da717ed12 100644 --- a/hal/blocking/flash/flash.rs +++ b/hal/blocking/flash/flash.rs @@ -166,17 +166,18 @@ impl Flash for BlockingFlash Result<(), Self::Error> { + let program_window_size = self.driver.program_window_size(); assert!( - TDriver::PROGRAM_WINDOW_SIZE.count_ones() == 1, - "TDriver::PROGRAM_WINDOW_SIZE must be a power of 2" + program_window_size.count_ones() == 1, + "program_window_size() must be a power of 2" ); - let window_mask = TDriver::PROGRAM_WINDOW_SIZE - 1; + let window_mask = program_window_size - 1; let mut addr = start_addr; while !data.is_empty() { // Calculate bytes remaining in the current program window let chunk = &data[..min( data.len(), - TDriver::PROGRAM_WINDOW_SIZE - ((addr.offset() & window_mask as u32) as usize), + program_window_size - ((addr.offset() & window_mask as u32) as usize), )]; self.driver.start_program(addr, chunk)?; self.blocking.wait_for_notification(); @@ -263,15 +264,15 @@ mod test { data: &[u8], ) -> Result<(), Self::Error> { let start_addr = start_addr.offset() as usize; + let program_window_size = self.program_window_size(); assert!(start_addr.checked_add(data.len()).unwrap() <= self.data.len()); assert!( - data.len() <= Self::PROGRAM_WINDOW_SIZE, + data.len() <= program_window_size, "Program window violation" ); let end_addr = start_addr.wrapping_add(data.len()); assert!( - start_addr / Self::PROGRAM_WINDOW_SIZE - == (end_addr - 1) / Self::PROGRAM_WINDOW_SIZE, + start_addr / program_window_size == (end_addr - 1) / program_window_size, "Program window violation" ); for (dest, src) in self.data[start_addr..end_addr].iter_mut().zip(data) { 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(()) + } +} diff --git a/services/orchestrator/capabilities/BUILD.bazel b/services/orchestrator/capabilities/BUILD.bazel index 859f7a9c5..c1c4d8e41 100644 --- a/services/orchestrator/capabilities/BUILD.bazel +++ b/services/orchestrator/capabilities/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( "src/boot_watch.rs", "src/evidence.rs", "src/lib.rs", + "src/lockdown_latch.rs", "src/svn_floor.rs", ], edition = "2024", diff --git a/services/orchestrator/capabilities/src/lib.rs b/services/orchestrator/capabilities/src/lib.rs index 0a721eac5..cf71cde68 100644 --- a/services/orchestrator/capabilities/src/lib.rs +++ b/services/orchestrator/capabilities/src/lib.rs @@ -19,6 +19,9 @@ //! `BootWatch` is the seam the orchestrator polls: one device's boot walk, //! erased of every device-specific type, answering with a `WalkVerdict`. //! +//! `LockdownLatch` is the terminal capability: latch the platform into its safe +//! state, one-way, at the top of the escalation ladder. +//! //! This crate is a dependency-free leaf: it holds the capability contracts, //! and everything depends downward on it. Concrete adapters bind a capability //! to a signal source and live in their own crates, so naming a capability @@ -32,9 +35,11 @@ mod boot_control; mod boot_watch; mod evidence; +mod lockdown_latch; mod svn_floor; pub use boot_control::BootControl; pub use boot_watch::{BootWatch, FailureCause, WalkVerdict}; pub use evidence::{BootStatus, EvidenceReader}; +pub use lockdown_latch::LockdownLatch; pub use svn_floor::{Svn, SvnFloor}; diff --git a/services/orchestrator/capabilities/src/lockdown_latch.rs b/services/orchestrator/capabilities/src/lockdown_latch.rs new file mode 100644 index 000000000..f57686df2 --- /dev/null +++ b/services/orchestrator/capabilities/src/lockdown_latch.rs @@ -0,0 +1,88 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! The [`LockdownLatch`] terminal safe-state capability contract. + +/// Latch capability: put the platform into its terminal safe state. +/// +/// What the safe state is (gating every managed device, tripping a fuse, +/// parking straps) is board wiring and never leaks through this seam. +/// +/// The latch is one-way and idempotent: nothing short of a platform reset +/// unlatches it, and latching an already-latched platform succeeds. `Ok` +/// means the safe state is in force, not merely requested. A failed latch +/// is a hard fault: `Err` means the safe state is not in force and the +/// caller must not continue as if it were. How the platform escalates from +/// there is board policy, outside this contract. +pub trait LockdownLatch { + /// The error type of this platform's latch mechanism. + /// + /// Bounded by [`core::error::Error`] so the caller gets `Display` and a + /// `source()` cause chain. Error categories are implementation-defined. + type Error: core::error::Error; + + /// Latches the platform into the safe state. + fn latch(&mut self) -> Result<(), Self::Error>; +} + +#[cfg(test)] +mod tests { + use super::*; + + // Implements LockdownLatch with no HAL dependency: the contract must be + // satisfiable from any stack (mock, IPC proxy, simulator). A HAL-bound + // `Error` type would stop this compiling. + struct MockLatch { + latched: bool, + fail: bool, + } + + #[derive(Debug, PartialEq, Eq)] + struct MockFault; + + impl core::fmt::Display for MockFault { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("mock latch fault") + } + } + + impl core::error::Error for MockFault {} + + impl LockdownLatch for MockLatch { + type Error = MockFault; + + fn latch(&mut self) -> Result<(), MockFault> { + if self.fail { + return Err(MockFault); + } + self.latched = true; + Ok(()) + } + } + + #[test] + fn contract_is_implementable_without_the_hal() { + let mut dev = MockLatch { + latched: false, + fail: false, + }; + + dev.latch().expect("latch failed"); + dev.latch().expect("repeated latch failed"); // idempotent + + assert!(dev.latched); + } + + #[test] + fn errors_surface_through_the_generic_seam() { + let mut dev = MockLatch { + latched: false, + fail: true, + }; + + let err = dev.latch().expect_err("expected the latch fault"); + + // Display comes from the core::error::Error bound, not a Debug dump. + assert_eq!(err.to_string(), "mock latch fault"); + } +} diff --git a/services/orchestrator/driver/src/board.rs b/services/orchestrator/driver/src/board.rs index bf978f1e0..d1c2508c7 100644 --- a/services/orchestrator/driver/src/board.rs +++ b/services/orchestrator/driver/src/board.rs @@ -5,7 +5,8 @@ //! Boards (or test mocks) implement these. use openprot_orchestrator_sm::ComponentId; -use orchestrator_capabilities::BootControl; + +pub use orchestrator_capabilities::BootControl; /// Access to one component's active firmware image, however it is reached — /// interposed flash, a PLDM/MCTP transfer, a RAM copy in tests. diff --git a/services/orchestrator/driver/src/tests.rs b/services/orchestrator/driver/src/tests.rs index 0f2bc0ee2..5790489c9 100644 --- a/services/orchestrator/driver/src/tests.rs +++ b/services/orchestrator/driver/src/tests.rs @@ -76,15 +76,15 @@ impl ImageSource for MemImage { } #[derive(Debug)] -struct VerifierBroken; +struct VerifierError; -impl core::fmt::Display for VerifierBroken { +impl core::fmt::Display for VerifierError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("verifier broken") } } -impl core::error::Error for VerifierBroken {} +impl core::error::Error for VerifierError {} /// The magic + XOR-zero check as a board-supplied verifier, reading in /// chunks. @@ -93,17 +93,17 @@ struct XorVerifier { } impl Verifier for XorVerifier { - type Error = VerifierBroken; + type Error = VerifierError; fn verify( &mut self, _id: ComponentId, image: &mut impl ImageSource, - ) -> Result { + ) -> Result { if self.fault { - return Err(VerifierBroken); + return Err(VerifierError); } - let len = image.size().map_err(|_| VerifierBroken)?; + let len = image.size().map_err(|_| VerifierError)?; let mut magic = [0u8; 4]; let mut xor = 0u8; let mut offset = 0; @@ -112,7 +112,7 @@ impl Verifier for XorVerifier { let take = chunk.len().min(len - offset); image .read_at(offset, &mut chunk[..take]) - .map_err(|_| VerifierBroken)?; + .map_err(|_| VerifierError)?; if offset == 0 && take >= 4 { magic.copy_from_slice(&chunk[..4]); } @@ -388,3 +388,80 @@ fn execute_returns_the_verdict_event() { Ok(Some(Event::VerificationPassed(C0))) ); } + +/// Wraps [`XorVerifier`] and snapshots the reset line as the check runs, +/// so the test can see the line state inside the verification window. +struct LineWatchingVerifier { + inner: XorVerifier, + line: std::rc::Rc>, + held_during_verify: std::rc::Rc>, +} + +impl Verifier for LineWatchingVerifier { + type Error = VerifierError; + + fn verify( + &mut self, + id: ComponentId, + image: &mut impl ImageSource, + ) -> Result { + self.held_during_verify.set(self.line.get()); + self.inner.verify(id, image) + } +} + +struct WatchBoard; + +impl BoardCapabilities for WatchBoard { + type Image = MemImage; + type Verifier = LineWatchingVerifier; + type BootControl = MockReset; +} + +// The at-rest guarantee end to end: the component is still held while its +// image is verified, and the line is released only on the passing verdict. +#[test] +fn release_follows_verification() { + let control = MockReset::new(); + let held = control.held.clone(); + let held_during_verify = std::rc::Rc::new(core::cell::Cell::new(false)); + let mut driver = PlatformDriver::::new(Board { + images: [MemImage::holding(valid_image())], + verifier: LineWatchingVerifier { + inner: XorVerifier { fault: false }, + line: held.clone(), + held_during_verify: held_during_verify.clone(), + }, + boot_controls: [control], + }); + let mut orch = orchestrator(); + + orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); + + assert_eq!(orch.state(), State::Ready); + assert!( + held_during_verify.get(), + "held while its image was verified" + ); + assert!(!held.get(), "released after the verdict"); +} + +// A dead reset line is a failed actuation, not a verdict: the SM fails +// closed and the component stays quiesced. +#[test] +fn failed_release_fails_closed() { + let mut control = MockReset::new(); + control.fail = true; + let held = control.held.clone(); + let mut driver = PlatformDriver::::new(Board { + images: [MemImage::holding(valid_image())], + verifier: XorVerifier { fault: false }, + boot_controls: [control], + }); + let mut orch = orchestrator(); + + orch.dispatch(&mut driver, Event::PowerGood(PowerOnResult::Provisioned)); + + assert_eq!(orch.state(), State::Locked); + assert!(held.get(), "never left reset"); +} 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..07e9cf23f --- /dev/null +++ b/target/ast10x0/backend/flash/src/lib.rs @@ -0,0 +1,182 @@ +// 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::smc::{ + FlashConfig, FlashGeometry, FmcReady, FmcUninit, GeometrySource, SmcConfig, SmcController, + SmcError, SmcInstance, SmcTopology, SpiNorFlash, SpiNorFlashDevice, +}; +use hal_flash_driver::{FlashAddress, FlashDriver}; +use util_error::{self as error, ErrorCode}; +use util_types::{Blocking, PowerOf2Usize}; + +/// Compile-time descriptor for the wired FMC controller this backend drives. +struct FmcInstance; + +impl SmcInstance for FmcInstance { + const CONTROLLER: SmcController = SmcController::Fmc; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 50 }), + cs1: Some(FlashConfig { spi_clock_mhz: 50 }), + dma_enabled: false, + enable_interrupts: false, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; +} + +/// Geometry source for the served chip (CS1). `Pinned` here makes the reported +/// geometry a compile-time constant; `Discover` reports the SFDP-read value. +type Cs1Geometry = ::Cs1Geometry; + +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 flash driver. +pub struct Ast10x0FmcFlashDriver { + fmc: FmcReady, + geometry: FlashGeometry, +} + +/// 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) and its CS flash windows: with both CS0 and CS1 + /// present the 256 MiB aperture is split in half, so CS0 decodes at + /// 0x8000_0000 and the served CS1 flash at 0x8800_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 FMC hardware block per the contract above. + let uninit = unsafe { FmcUninit::::new() }.map_err(map_smc_error)?; + let mut fmc = uninit.init().map_err(map_smc_error)?; + // Geometry was discovered over SFDP during `init()`; read it back off the + // CS1 handle (no rediscovery, no recalibration). + let geometry = { + let cs1 = fmc.cs1().map_err(map_smc_error)?; + cs1.geometry() + }; + NonZero::new(geometry.capacity_bytes as usize) + .ok_or(error::FLASH_AST10X0_INVALID_CAPACITY)?; + Ok(Self { fmc, geometry }) + } + + fn device(&mut self) -> Result, ErrorCode> { + let cs = self.fmc.cs1().map_err(map_smc_error)?; + SpiNorFlash::new(cs).map_err(map_smc_error) + } +} + +impl FlashDriver for Ast10x0FmcFlashDriver { + type Error = ErrorCode; + + // PAGE_SIZE / PROGRAM_WINDOW_SIZE are defaulted to 0, geometry is discovered instead + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 1; + + fn size(&self) -> NonZero { + NonZero::new(Cs1Geometry::geometry(&self.geometry).capacity_bytes as usize) + .expect("capacity validated in new()") + } + + /// Default erase page: one SFDP-discovered sector. + fn page_size(&self) -> usize { + Cs1Geometry::geometry(&self.geometry).sector_size as usize + } + + /// SPI NOR program page: writes must not cross this boundary. + fn program_window_size(&self) -> usize { + Cs1Geometry::geometry(&self.geometry).page_size as usize + } + + fn erasable_sizes_bitmap(&mut self) -> Result { + // Only sector erase is implemented by the peripheral driver. + Ok(1u32 + << Cs1Geometry::geometry(&self.geometry) + .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() != self.geometry.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(()) + } +} diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index 7e2e827bb..6a2690210 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl @@ -36,6 +36,28 @@ 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] + + # fmc_model describes the QEMU FMC device uniformly (JEDEC ID + SFDP + # geometry), shared by both chip selects. The qemu_runner seeds fresh + # images at $TEST_TMPDIR/ and attaches each present CS image as + # FMC flash (if=mtd): cs0_image at index 0, cs1_image at index 1. + env = { + "AST10X0_FLASH_SIZE": str(ctx.attr.flash_size), + "AST10X0_FMC_MODEL": ctx.attr.fmc_model, + } + if ctx.attr.cs0_image: + env["AST10X0_CS0_IMAGE"] = ctx.attr.cs0_image + env["AST10X0_CS0_FILL"] = str(ctx.attr.cs0_fill) + if ctx.attr.cs1_image: + env["AST10X0_CS1_IMAGE"] = ctx.attr.cs1_image + env["AST10X0_CS1_FILL"] = str(ctx.attr.cs1_fill) + if ctx.attr.cs0_image or ctx.attr.cs1_image: + providers.append(RunEnvironmentInfo(environment = env)) + return providers + system_image_test = rule( implementation = _system_image_test_impl, test = True, @@ -56,3 +78,51 @@ system_image_test = rule( ), }, ) + +flash_system_image_test = rule( + implementation = _flash_system_image_test_impl, + test = True, + attrs = { + "cs0_fill": attr.int( + doc = "Byte value the qemu_runner seeds cs0_image with (default 0xFF, erased).", + default = 0xFF, + ), + "cs0_image": attr.string( + doc = "Basename of the SPI-NOR image seeded in $TEST_TMPDIR and " + + "attached as FMC CS0 flash (if=mtd, index=0).", + default = "", + ), + "cs1_fill": attr.int( + doc = "Byte value the qemu_runner seeds cs1_image with (default 0xFF, erased).", + default = 0xFF, + ), + "cs1_image": attr.string( + doc = "Basename of the SPI-NOR image seeded in $TEST_TMPDIR and " + + "attached as FMC CS1 flash (if=mtd, index=1).", + default = "", + ), + "flash_size": attr.int( + doc = "Size in bytes of each seeded flash image.", + default = 8 * 1024 * 1024, + ), + "fmc_model": attr.string( + doc = "QEMU fmc-model applied controller-wide (JEDEC ID + SFDP " + + "geometry). Default is the 1 MiB w25q80bl.", + default = "w25q80bl", + ), + "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..b6e2e3a28 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,53 @@ def _sentinel_watcher( print(f"Exception watching sentinel: {e}", file=sys.stderr) +def _seed_flash_image(path: str, size: int, fill: int = 0xFF) -> None: + """Create/overwrite `path` with `size` bytes of `fill` (0xFF = erased).""" + with open(path, "wb") as f: + f.write(bytes([fill & 0xFF]) * size) + + +def _resolve_flash_drives(args): + """Return a list of (index, path, size, fill) FMC backing images. + + index 0 -> FMC CS0, index 1 -> FMC CS1. Each image is re-seeded on every + run so tests start from a known device state. An explicit --flash-image + (manual runs) attaches at CS1. A flash_system_image_test sets + AST10X0_CS0_IMAGE / AST10X0_CS1_IMAGE (basenames) plus AST10X0_FLASH_SIZE + and per-CS AST10X0_CS0_FILL / AST10X0_CS1_FILL, resolved against + $TEST_TMPDIR so each run gets private, freshly-seeded images. + """ + base = os.environ.get("TEST_TMPDIR", tempfile.gettempdir()) + size = int(os.environ.get("AST10X0_FLASH_SIZE", str(args.flash_size))) + drives = [] + if args.flash_image: + drives.append((1, args.flash_image, size, 0xFF)) + cs0 = os.environ.get("AST10X0_CS0_IMAGE") + if cs0: + fill = int(os.environ.get("AST10X0_CS0_FILL", "255")) + drives.append((0, os.path.join(base, cs0), size, fill)) + cs1 = os.environ.get("AST10X0_CS1_IMAGE") + if cs1: + fill = int(os.environ.get("AST10X0_CS1_FILL", "255")) + drives.append((1, os.path.join(base, cs1), size, fill)) + return drives + + def _main(args) -> None: + drives = _resolve_flash_drives(args) + + machine = args.machine + if drives: + # ast1030-evb models one flash type for the whole FMC controller. + # w25q80bl matches internal flash on evb CS0, but is smaller than evb CS1. + # Both CS share this model — only the attached backing images differ. + model = os.environ.get("AST10X0_FMC_MODEL", "w25q80bl") + machine = f"{machine},fmc-model={model}" + qemu_args = [ _QEMU_ARM, "-machine", - args.machine, + machine, "-cpu", args.cpu, "-bios", @@ -136,6 +191,13 @@ def _main(args) -> None: args.image, ] + for index, path, size, fill in drives: + _seed_flash_image(path, size, fill) + qemu_args += [ + "-drive", + f"file={path},format=raw,if=mtd,index={index}", + ] + if args.qemu_args: qemu_args.extend(args.qemu_args) diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel index 394ccf29a..5a56927a3 100644 --- a/target/ast10x0/peripherals/BUILD.bazel +++ b/target/ast10x0/peripherals/BUILD.bazel @@ -95,6 +95,7 @@ rust_library( target_compatible_with = TARGET_COMPATIBLE_WITH, deps = [ "//hal/blocking", + "//util/sfdp", "@ast1060_pac", "@pigweed//pw_log/rust:pw_log", "@rust_crates//:bitflags", diff --git a/target/ast10x0/peripherals/lib.rs b/target/ast10x0/peripherals/lib.rs index 8d2303dda..49da5e68d 100644 --- a/target/ast10x0/peripherals/lib.rs +++ b/target/ast10x0/peripherals/lib.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 #![no_std] +#![feature(associated_type_defaults)] +#![feature(adt_const_params)] pub mod gpio; pub mod hace; diff --git a/target/ast10x0/peripherals/smc/controller.rs b/target/ast10x0/peripherals/smc/controller.rs index 545b02809..37734b061 100644 --- a/target/ast10x0/peripherals/smc/controller.rs +++ b/target/ast10x0/peripherals/smc/controller.rs @@ -7,14 +7,15 @@ use core::cell::UnsafeCell; use core::marker::PhantomData; use crate::smc::helpers::{ - encode_fmc_segment, encode_spi_segment, flash_capacity_bytes, get_mid_point_of_longest_one, - spi_calibration_enable, spi_freq_div, total_capacity_bytes, validate_dma_read, - validate_mapped_range, SPI_CTRL_FREQ_MASK, SPI_DMA_CALC_CKSUM, SPI_DMA_CALIB_MODE, - SPI_DMA_ENABLE, SPI_DMA_RAM_MAP_BASE, + encode_fmc_segment, encode_spi_segment, get_mid_point_of_longest_one, spi_calibration_enable, + spi_freq_div, validate_dma_read, validate_mapped_range, SMC_WINDOW_SIZE_BYTES, + SPI_CTRL_FREQ_MASK, SPI_DMA_CALC_CKSUM, SPI_DMA_CALIB_MODE, SPI_DMA_ENABLE, + SPI_DMA_RAM_MAP_BASE, }; use crate::smc::interrupts::{SmcInterrupt, SmcInterruptDecoder}; use crate::smc::registers::SmcRegisters; use crate::smc::types::*; +use util_sfdp::{decode_geometry, FlashGeometry}; /// Internal controller state #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -74,395 +75,268 @@ const fn spi_nor_addr_width_reg(current: u32, cs: ChipSelect, use_4b: bool) -> u } } +/// How a chip select's flash geometry is resolved at init. +/// +/// [`Smc::init`] resolves each present CS through the [`SmcInstance`] marker's +/// associated source type. Because an associated type is only code-generated +/// when a marker names it, a build whose markers are all fixed-geometry sources +/// never instantiates the [`Discover`] path, so the SFDP decode code below is +/// never generated. +pub trait GeometrySource { + /// Produce the flash geometry for `cs`. + /// + /// `baseline_ctrl` is the CS control-register value to derive user-mode + /// transfers from (used only by [`Discover`]). + fn resolve( + regs: &SmcRegisters, + cs: ChipSelect, + window_base: usize, + baseline_ctrl: u32, + ) -> Result; + + /// Geometry to report to callers: the SFDP-discovered value for + /// [`Discover`], the const `G` for [`Pinned`] (folded at compile time). + fn geometry(discovered: &FlashGeometry) -> FlashGeometry { + *discovered + } +} + +impl GeometrySource for Discover { + fn resolve( + regs: &SmcRegisters, + cs: ChipSelect, + window_base: usize, + baseline_ctrl: u32, + ) -> Result { + // NOTE: fixed 256-byte SFDP read. A device whose BFP table extends past + // offset 256 will fail decode (DeviceNotSupported); grow this if needed. + let mut image = [0u8; 256]; + transceive_user_raw( + regs, + cs, + window_base, + baseline_ctrl, + &[0x5A], + &[0, 0, 0, 0], + &mut image, + TransferMode::Mode111, + ); + decode_geometry(&image).map_err(|_| SmcError::DeviceNotSupported) + } +} + +/// Geometry-source marker carrying a fixed geometry as a const parameter. +/// +/// Use this instead of [`Discover`] for production buils when the wired flash +/// is sure not to change Its `resolve` just returns `G`, so a build that names +/// only `Pinned` never generates the SFDP decode path. +pub struct Pinned; + +impl GeometrySource for Pinned { + fn resolve( + _regs: &SmcRegisters, + _cs: ChipSelect, + _window_base: usize, + _baseline_ctrl: u32, + ) -> Result { + Ok(G) + } + + fn geometry(_discovered: &FlashGeometry) -> FlashGeometry { + G + } +} + /// Type-state marker: controller is constructed but not initialized. pub struct Uninitialized; /// Type-state marker: controller has completed hardware initialization. pub struct Ready; -/// Generic Static Memory Controller (SMC) +/// Lifecycle marker for the controller's init state. Empty: config is carried +/// as a compile-time property of the `SmcInstance` type parameter, not stored. +pub trait SmcMode {} + +impl SmcMode for Uninitialized {} + +impl SmcMode for Ready {} + +/// Compile-time-computed controller layout: memory-map config word, per-CS +/// aperture share, window bases, and encoded segment words. Computed inline in +/// the `const` block at the top of `Smc::init` from an [`SmcInstance`]'s const +/// config; an invalid config `panic!`s, turning it into a build error at the +/// instantiation site. +struct SmcLayout { + conf: u32, + region_size: usize, + window_base: [usize; 2], + cs0_present: bool, + cs1_present: bool, + cs0_segment: u32, + cs1_segment: u32, +} + +/// Per-CS state resolved once during [`Smc::init`] +#[derive(Clone, Copy)] +struct ResolvedCs { + geometry: FlashGeometry, + normal_read_ctrl: u32, + window_base: usize, +} + +const fn encode_segment_for( + ctrl: SmcController, + start: usize, + end: usize, +) -> Result { + match ctrl { + SmcController::Fmc => encode_fmc_segment(start, end), + SmcController::Spi1 | SmcController::Spi2 => encode_spi_segment(start, end), + } +} + +/// Spin a fixed number of times to let a hardware register settle. +#[inline(never)] +fn loop_delay(spin_cnt: u32) { + for _ in 0..spin_cnt { + core::hint::spin_loop(); + } +} + +/// Generic Static Memory Controller (SMC). /// -/// The `Mode` type parameter enforces init ordering at compile time. -pub struct Smc { +/// `I` names the wired controller and carries its config as compile-time consts +/// ([`SmcInstance`]); `Mode` enforces init ordering. Neither config nor +/// controller id is stored: both are read from `I`, and everything derived from +/// them is computed at compile time in `init`. +pub struct Smc { regs: SmcRegisters, - controller_id: SmcController, - config: SmcConfig, state: SmcState, - /// Per-CS normal-read control register values stored at init time. - /// Indexed by `ChipSelect as usize`. Restored unconditionally after every - /// user-mode transaction, matching aspeed-rust `deactivate_user()` behavior. - normal_read_ctrl: [u32; 2], - /// Per-CS AHB flash window base addresses. - /// CS0 starts at `controller_id.flash_window_address()`; - /// CS1 starts immediately after the CS0 segment. - flash_window_base: [usize; 2], + /// Geometry + calibration resolved at init; `None` until `Ready` (and for + /// any absent chip select). + cs0_resolved: Option, + cs1_resolved: Option, + _i: PhantomData I>, _mode: PhantomData Mode>, } /// Ergonomic alias for the uninitialized controller handle. -pub type UninitSmc = Smc; +pub type UninitSmc = Smc; /// Ergonomic alias for the initialized controller handle. -pub type ReadySmc = Smc; +pub type ReadySmc = Smc; -impl Smc { +impl Smc { /// Create a new SMC controller instance. /// /// # Safety /// Caller must ensure: /// - No other Smc instance exists for this hardware controller /// - The controller's base address points to valid hardware - pub unsafe fn new(config: SmcConfig) -> Result { - if config.cs0.is_none() && config.cs1.is_none() { - return Err(SmcError::InvalidCapacity); - } - - let base = config.controller_id.base_address() as *const _; + pub unsafe fn new() -> Result { + let base = I::CONTROLLER.base_address() as *const _; // SAFETY: Caller ensures base address is valid and no other instance exists. let regs = unsafe { SmcRegisters::new(base) }; Ok(Self { regs, - controller_id: config.controller_id, - config, state: SmcState::Idle, - normal_read_ctrl: [0; 2], - flash_window_base: [0; 2], + cs0_resolved: None, + cs1_resolved: None, + _i: PhantomData, _mode: PhantomData, }) } /// Initialize hardware and transition to `Ready` mode. - pub fn init(self) -> Result, SmcError> { - // Phase 3: Topology-aware initialization - // - // The SmcTopology enum encodes the controller's role and master_idx: - // - BootSpi { master_idx }: Boot firmware path (typically FMC, master_idx=0) - // - HostSpi { master_idx }: Host BMC SPI path (typically SPI1, master_idx=0) - // - NormalSpi { master_idx }: Normal user SPI path (typically SPI2, master_idx=2) - // - // Topology gates behavior in setup_segments() and configure_timing(): - // The topology is consulted via self.config.topology. - - // 1. Configure flash types and write-enable per CS - let mut conf = 0u32; - if self.config.cs0.is_some() { - conf |= 1 << 16; // CONF_ENABLE_W0 - conf |= 0x2 << 0; // FLASH_TYPE_SPI - } - if self.config.cs1.is_some() { - conf |= 1 << 17; // CONF_ENABLE_W1 - conf |= 0x2 << 2; // FLASH_TYPE_SPI - } - self.regs.write_config(conf); + pub fn init(self) -> Result, SmcError> { + // Derive the full controller layout from its static config. Splits the + // 256 MiB aperture evenly across present chip selects for now. + let layout = const { + let ctrl = I::CONTROLLER; + let cfg = I::CONFIG; + let cs0_present = cfg.cs0.is_some(); + let cs1_present = cfg.cs1.is_some(); + let present = cs0_present as usize + cs1_present as usize; + if present == 0 { + panic!("SMC config must configure at least one chip select"); + } + let region_size = SMC_WINDOW_SIZE_BYTES / present; + let cs0_size = if cs0_present { region_size } else { 0 }; + let base = ctrl.flash_window_address(); + let window_base = [base, base + cs0_size]; + + let mut conf = 0u32; + if cs0_present { + conf |= 1 << 16; // CONF_ENABLE_W0 + conf |= 0x2 << 0; // FLASH_TYPE_SPI + } + if cs1_present { + conf |= 1 << 17; // CONF_ENABLE_W1 + conf |= 0x2 << 2; // FLASH_TYPE_SPI + } - // 2. Set up segment addresses (memory mapping) - Self::setup_segments(&self)?; + let cs0_segment = if cs0_present { + match encode_segment_for(ctrl, 0, cs0_size) { + Ok(seg) => seg, + Err(_) => panic!("invalid CS0 segment for SMC config"), + } + } else { + 0 + }; + let cs1_segment = if cs1_present { + match encode_segment_for(ctrl, cs0_size, cs0_size + region_size) { + Ok(seg) => seg, + Err(_) => panic!("invalid CS1 segment for SMC config"), + } + } else { + 0 + }; + + SmcLayout { + conf, + region_size, + window_base, + cs0_present, + cs1_present, + cs0_segment, + cs1_segment, + } + }; - // Snapshot per-CS normal-read control register values after all init writes. - // CS1 value is captured even if cs1 is None (safe: register read is harmless). - let cs0_normal_read = self.regs.read_cs0_ctrl(); - let cs1_normal_read = self.regs.read_cs1_ctrl(); + // 1. Configure flash types and write-enable per CS. + self.regs.write_config(layout.conf); - // Compute per-CS AHB flash window base addresses. - let base = self.controller_id.flash_window_address(); - let cs0_size = flash_capacity_bytes(self.config.cs0).unwrap_or(0); - let flash_window_base = [base, base + cs0_size]; + // 2. Set up segment addresses (memory mapping). + if layout.cs0_present { + self.regs.write_cs0_segment(layout.cs0_segment); + } + if layout.cs1_present { + self.regs.write_cs1_segment(layout.cs1_segment); + } - Ok(Smc { + let mut smc = Smc:: { regs: self.regs, - controller_id: self.controller_id, - config: self.config, state: SmcState::Idle, - normal_read_ctrl: [cs0_normal_read, cs1_normal_read], - flash_window_base, + cs0_resolved: None, + cs1_resolved: None, + _i: PhantomData, _mode: PhantomData, - }) - } - fn encode_segment(&self, start: usize, end: usize) -> Result { - match self.config.controller_id { - SmcController::Fmc => encode_fmc_segment(start, end), - SmcController::Spi1 | SmcController::Spi2 => encode_spi_segment(start, end), - } - } + }; - fn setup_segments(&self) -> Result<(), SmcError> { - // Decode-range sizing is topology-aware. - // - // For BootSpi (FMC, master_idx=0): Full decode range from configured capacity. - // Used for boot firmware; exclusive access to flash; no shared-bus concerns. - // - // For HostSpi / NormalSpi when master_idx != 0: Potential shared-bus topology. - // When multiple masters multiplex a single SPI flash, decode ranges may need - // to be restricted. Phase 3+ may implement decode_range_reinit logic keyed - // on config.topology.master_idx() to prevent collisions. - // - // For now, all topologies use the full capacity from FlashConfig. - // Phase 3+: add conditional decode-range sizing based on topology + master_idx. - - let cs0_size = flash_capacity_bytes(self.config.cs0)?; - let cs1_size = flash_capacity_bytes(self.config.cs1)?; - total_capacity_bytes(self.config.cs0, self.config.cs1)?; - - if cs0_size > 0 { - let seg = self.encode_segment(0, cs0_size)?; - self.regs.write_cs0_segment(seg); + // 3. Resolve + calibrate each present chip select once. + if layout.cs0_present { + smc.cs0_resolved = Some(smc.resolve_cs::(ChipSelect::Cs0, &layout)?); } - - if cs1_size > 0 { - let seg = self.encode_segment(cs0_size, cs0_size + cs1_size)?; - self.regs.write_cs1_segment(seg); + if layout.cs1_present { + smc.cs1_resolved = Some(smc.resolve_cs::(ChipSelect::Cs1, &layout)?); } - Ok(()) + Ok(smc) } } -impl Smc { - fn flash_window_base(&self, cs: ChipSelect) -> usize { - match cs { - ChipSelect::Cs0 => self.flash_window_base[0], - ChipSelect::Cs1 => self.flash_window_base[1], - } - } - - fn normal_read_ctrl(&self, cs: ChipSelect) -> u32 { - match cs { - ChipSelect::Cs0 => self.normal_read_ctrl[0], - ChipSelect::Cs1 => self.normal_read_ctrl[1], - } - } - - fn set_normal_read_ctrl(&mut self, cs: ChipSelect, val: u32) { - match cs { - ChipSelect::Cs0 => self.normal_read_ctrl[0] = val, - ChipSelect::Cs1 => self.normal_read_ctrl[1] = val, - } - } - - /// Perform a programmed I/O read via memory window. - /// - /// Reads directly from the flash memory window. Hardware automatically - /// converts memory accesses to SPI transactions. - pub fn read(&self, cs: ChipSelect, offset: u32, buf: &mut [u8]) -> Result { - let cs_config = self.cs_config(cs)?; - let cs_capacity = flash_capacity_bytes(Some(cs_config))?; - let window = self.flash_window_base(cs) as *const u8; - let offset = validate_mapped_range(offset, buf.len(), cs_capacity)?; - let flash_ptr = window.wrapping_add(offset); - pw_log::debug!( - "read: offset0x{:08x}, size:0x{:08x}, flash ptr:0x{:08x}", - offset as u32, - buf.len() as u32, - flash_ptr as u32 - ); - // SAFETY: `flash_ptr` is derived from the controller's fixed MMIO flash - // window using `wrapping_add`, which avoids imposing Rust allocation - // provenance rules on the raw address arithmetic itself. The actual read - // below requires the requested `[offset, offset + buf.len())` range to be - // backed by the controller's mapped flash aperture, and `buf` provides a - // valid, writable destination that does not overlap this MMIO window. - unsafe { - core::ptr::copy_nonoverlapping(flash_ptr, buf.as_mut_ptr(), buf.len()); - } - - Ok(buf.len()) - } - #[inline(never)] - pub fn loop_delay(spin_cnt: u32) { - for _ in 0..spin_cnt { - core::hint::spin_loop(); - } - } - /// Initiate a DMA read operation (non-blocking). - pub fn dma_read( - &mut self, - cs: ChipSelect, - flash_offset: u32, - dram_addr: usize, - len: u32, - ) -> Result<(), SmcError> { - if self.state != SmcState::Idle { - return Err(SmcError::ControllerNotReady); - } - if !self.config.dma_enabled { - return Err(SmcError::DmaNotEnabled); - } - if cs == ChipSelect::Cs1 && self.config.cs1.is_none() { - return Err(SmcError::InvalidChipSelect); - } - self.regs.disable_dma(); - Self::loop_delay(0x1000); - - let cs_config = self.cs_config(cs)?; - let cs_capacity = flash_capacity_bytes(Some(cs_config))?; - pw_log::debug!( - "flash_offset: 0x{:08x}, cs_cap: 0x{:08x}", - flash_offset as u32, - cs_capacity as u32 - ); - - let validated = validate_dma_read( - flash_offset, - self.flash_window_base(cs), - cs_capacity, - dram_addr, - len, - )?; - pw_log::debug!( - "flash start: 0x{:08x}, cs_cap: 0x{:08x}, dram_addr: 0x{:08x} len: 0x{:08x} ", - validated.flash_start as u32, - cs_capacity as u32, - validated.dram_addr as u32, - validated.dma_len_reg as u32 - ); - - // Set CS0 control register to normal-read mode before programming DMA - // registers. The DMA engine reads the CSx control register to know which - // SPI command to issue; it must be in normal-read mode (not user mode) - // before the kick. Matches aspeed-rust fmccontroller.rs::read_dma - // ctrl construction: preserve frequency bits, set ASPEED_SPI_NORMAL_READ. - let ctrl_val = self.normal_read_ctrl(cs) | ASPEED_SPI_NORMAL_READ; - self.regs.write_cs_ctrl(cs, ctrl_val); - - // Acquire the DMA bus arbiter before programming any DMA registers. - // On SPI1/SPI2: writes SPI_DMA_GET_REQ_MAGIC and spins until DMAGrant - // (bit 30 of spi080) is set. On FMC: bits 20–31 are Reserved — the write - // is a no-op and the spin condition is immediately false. Safe to call - // unconditionally on all controllers, matching aspeed-rust's approach. - self.regs.acquire_dma_arbiter(); - pw_log::debug!("acquired dma bus arbiter"); - // Program DMA registers in the order used by aspeed-rust fmccontroller.rs::read_dma: - // fmc084 = flash side DMA address (R_DMA_FLASH_ADDR) - // = flash_window_base[cs] - SPI_DMA_FLASH_MAP_BASE + cs_offset - // (computed in validate_dma_read) - // fmc088 = DRAM/SRAM destination address (R_DMA_DRAM_ADDR) - // = physical_sram_addr + SPI_DMA_RAM_MAP_BASE - // fmc08c = transfer length - 1 (R_DMA_LEN) - self.regs.write_dma_flash_addr(validated.flash_start as u32); - self.regs - .write_dma_dram_addr(validated.dram_addr + SPI_DMA_RAM_MAP_BASE); - self.regs.write_dma_len(validated.dma_len_reg); - - // Enable the completion IRQ before kicking DMA. QEMU evaluates - // INTR_CTRL_DMA_EN exactly once at DMA-done time - // (`aspeed_smc_dma_done` in qemu/hw/ssi/aspeed_smc.c) and won't - // re-fire the IRQ if the bit is set after the fact; aspeed-rust - // arms the IRQ before starting DMA for the same reason - // (`spicontroller.rs::read_dma`). - if self.config.enable_interrupts { - pw_log::debug!("enable dma irq"); - self.regs.enable_dma_irq(); - } - - // Kick DMA via read-modify-write to preserve timing calibration - // bits (fmc080 bits 8-19), matching aspeed-rust fmccontroller.rs::read_dma. - pw_log::debug!("start dma read..."); - self.regs.kick_dma_read(); - self.state = SmcState::DmaInFlight; - Ok(()) - } - - /// Read raw DMA/interrupt status register bits (FMC008). - pub fn dma_status(&self) -> u32 { - self.regs.read_dma_status() - } - - /// Clear DMA-related status bits in the status register (FMC008). - /// - /// `clear_mask` is write-1-to-clear and should contain only relevant bits. - pub fn clear_dma_status(&self, clear_mask: u32) { - self.regs - .clear_dma_status(clear_mask & DMA_STATUS_RELEVANT_BITS); - } - - /// Decode status bits and transition controller state. - /// - /// Called by both `handle_dma_irq` (IRQ-driven) and `poll_dma_completion` - /// (polling). Assumes `status & DMA_STATUS_RELEVANT_BITS != 0`. - fn complete_dma(&mut self, status: u32) -> Result { - let relevant = status & DMA_STATUS_RELEVANT_BITS; - let dma_in_flight = self.state == SmcState::DmaInFlight; - let decoded = SmcInterruptDecoder::decode_with_context(status, dma_in_flight); - self.clear_dma_status(relevant); - - match decoded { - SmcInterrupt::DmaComplete => { - self.regs.disable_dma(); - self.state = SmcState::Idle; - Ok(decoded) - } - SmcInterrupt::DmaError => { - self.regs.disable_dma(); - self.state = SmcState::Idle; - Err(SmcError::DmaAborted) - } - SmcInterrupt::CommandAbort => { - self.state = SmcState::Faulted; - Err(SmcError::HardwareError) - } - SmcInterrupt::WriteProtected => { - self.state = SmcState::Faulted; - Err(SmcError::WriteProtected) - } - SmcInterrupt::Unknown => Err(SmcError::HardwareError), - } - } - - /// Decode and complete an in-flight DMA operation from an IRQ event. - /// - /// Returns the decoded interrupt cause when a completion/error event was - /// observed and processed. If no relevant status bits are set, returns - /// `SmcError::ControllerNotReady` to indicate no completion work was found. - pub fn handle_dma_irq(&mut self) -> Result { - self.regs.disable_dma_irq(); - let status = self.dma_status(); - pw_log::info!("SMC handle_dma_irq: status=0x{:08x}", status as u32); - if status & DMA_STATUS_RELEVANT_BITS == 0 { - return Err(SmcError::ControllerNotReady); - } - self.complete_dma(status) - } - - /// Poll for DMA completion without requiring an IRQ. - /// - /// Returns `Poll::Pending` while the transfer is still in progress. - /// Returns `Poll::Ready(Ok(()))` on success or `Poll::Ready(Err(SmcError))` - /// on failure. Returns `Poll::Ready(Err(SmcError::ControllerNotReady))` if - /// no DMA is in flight. - /// - /// Suitable for spin-poll loops in contexts where `enable_interrupts` is - /// false (e.g., QEMU tests without an IRQ handler): - /// ```ignore - /// loop { - /// match controller.poll_dma_completion() { - /// Poll::Ready(result) => break result, - /// Poll::Pending => {} - /// } - /// } - /// ``` - pub fn poll_dma_completion(&mut self) -> core::task::Poll> { - if self.state != SmcState::DmaInFlight { - return core::task::Poll::Ready(Err(SmcError::ControllerNotReady)); - } - let status = self.dma_status(); - if status & DMA_STATUS_RELEVANT_BITS == 0 { - return core::task::Poll::Pending; - } - core::task::Poll::Ready(self.complete_dma(status).map(|_| ())) - } - - pub fn poll_blocking_dma_completion(&self, timeout: u32) -> u32 { - let mut to = timeout; - - while (self.regs.read_dma_status() & DMA_STATUS_RELEVANT_BITS) == 0 { - if to == 0 { - return 0; - } - to -= 1; - } - return to; - } +impl Smc { /// Check if controller is ready for operations. pub fn is_ready(&self) -> bool { self.state == SmcState::Idle @@ -475,142 +349,138 @@ impl Smc { /// Get the controller identifier. pub fn controller_id(&self) -> SmcController { - self.controller_id + I::CONTROLLER } /// Get the configured master ID for this controller topology. pub fn master_idx(&self) -> u8 { - self.config.topology.master_idx() + I::CONFIG.topology.master_idx() } - /// Return configured total flash capacity for this controller in bytes. - pub fn capacity_bytes(&self) -> Result { - total_capacity_bytes(self.config.cs0, self.config.cs1) + /// Build a handle for CS0 from its init-resolved geometry and calibration. + /// + /// Returns `SmcError::InvalidChipSelect` if CS0 was not configured. + pub fn cs0(&mut self) -> Result, SmcError> { + self.build_cs(ChipSelect::Cs0) } - /// Return configured flash capacity in bytes for the given chip select. + /// Build a handle for CS1 from its init-resolved geometry and calibration. /// - /// Returns `SmcError::InvalidChipSelect` if the slot was not populated - /// at construction time. Used by the device facade to bounds-check - /// per-CS reads and to compute per-CS controller-window offsets. - pub fn cs_capacity_bytes(&self, cs: ChipSelect) -> Result { - crate::smc::helpers::cs_capacity_bytes(&self.config, cs) + /// Returns `SmcError::InvalidChipSelect` if CS1 was not configured. + pub fn cs1(&mut self) -> Result, SmcError> { + self.build_cs(ChipSelect::Cs1) } - /// Return the configured `FlashConfig` for the requested chip select. - /// - /// Returns `SmcError::InvalidChipSelect` if the slot was not populated at - /// construction time. Used by device-facade constructors to validate the - /// caller-supplied `FlashConfig` against the per-CS configuration the - /// controller was actually initialized with. - pub fn cs_config(&self, cs: ChipSelect) -> Result { - let slot = match cs { - ChipSelect::Cs0 => self.config.cs0, - ChipSelect::Cs1 => self.config.cs1, - }; - slot.ok_or(SmcError::InvalidChipSelect) + fn build_cs(&mut self, cs: ChipSelect) -> Result, SmcError> { + let resolved = match cs { + ChipSelect::Cs0 => self.cs0_resolved, + ChipSelect::Cs1 => self.cs1_resolved, + } + .ok_or(SmcError::InvalidChipSelect)?; + + Ok(Cs { + regs: &self.regs, + state: &mut self.state, + cs, + window_base: resolved.window_base, + normal_read_ctrl: resolved.normal_read_ctrl, + geometry: resolved.geometry, + controller_id: I::CONTROLLER, + master_idx: I::CONFIG.topology.master_idx(), + dma_enabled: I::CONFIG.dma_enabled, + enable_interrupts: I::CONFIG.enable_interrupts, + }) } - /// Execute a raw user-mode SPI transfer on CS0 for this controller. + /// Resolve a chip select's geometry and run calibration once, at init. /// - /// The `mode` parameter controls the IO width written to the CS control - /// register for each phase (cmd / addr+payload / rx), matching the - /// per-phase register update pattern used by aspeed-rust's - /// `spi_nor_transceive_user()`. - pub fn transceive_user( + /// Reads the reset-time CS control value as the SFDP baseline, resolves + /// geometry via `S` (pinned or SFDP discovery), rejects a device that + /// overflows its aperture share, then calibrates and returns the stored + /// per-CS state. + fn resolve_cs( &self, cs: ChipSelect, - cmd: &[u8], - tx_payload: &[u8], - rx: &mut [u8], - mode: TransferMode, - ) -> Result<(), SmcError> { - if self.state != SmcState::Idle { - return Err(SmcError::ControllerNotReady); - } - if cs == ChipSelect::Cs1 && self.config.cs1.is_none() { - return Err(SmcError::InvalidChipSelect); + layout: &SmcLayout, + ) -> Result { + let cfg = self.cs_config(cs)?; + let window_base = layout.window_base[cs as usize]; + // Reset-time CS control value; user-mode transfers (incl. SFDP) derive + // their frequency bits from this baseline. + let baseline_ctrl = self.regs.read_cs_ctrl(cs); + let geometry = S::resolve(&self.regs, cs, window_base, baseline_ctrl)?; + + if geometry.capacity_bytes as usize > layout.region_size { + return Err(SmcError::InvalidCapacity); } - // Derive user-mode base from the stored normal-read value: preserve - // frequency bits and replace mode type with ASPEED_SPI_USER. - let user_base = (self.normal_read_ctrl(cs) & !0x7) | ASPEED_SPI_USER; - let window = self.flash_window_base(cs) as *mut u32; + let normal_read_ctrl = self.calibrate_cs( + cs, + cfg.spi_clock_mhz, + geometry.capacity_bytes as usize, + window_base, + )?; - // Assert CS: inactive first, then active (matches aspeed-rust activate_user). - self.regs - .write_cs_ctrl(cs, user_base | ASPEED_SPI_USER_INACTIVE); - self.regs.write_cs_ctrl(cs, user_base); + Ok(ResolvedCs { + geometry, + normal_read_ctrl, + window_base, + }) + } - // SAFETY: user mode is active; the flash aperture is the hardware-defined - // byte-stream port for SPI command traffic while user mode is held. - unsafe { - // Command phase — always single-wire. - let cmd_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.cmd_io_bits(); - self.regs.write_cs_ctrl(cs, cmd_ctrl); - spi_write_data(window, cmd); - - // Address / TX payload phase. - let addr_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.addr_io_bits(); - self.regs.write_cs_ctrl(cs, addr_ctrl); - spi_write_data(window, tx_payload); - - // RX data phase. - let data_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.data_io_bits(); - self.regs.write_cs_ctrl(cs, data_ctrl); - spi_read_data(window as *const u32, rx); - } + /// Presence check: the configured `FlashConfig` for `cs`, or + /// `InvalidChipSelect` if the slot was not populated. + fn cs_config(&self, cs: ChipSelect) -> Result { + let slot = match cs { + ChipSelect::Cs0 => I::CONFIG.cs0, + ChipSelect::Cs1 => I::CONFIG.cs1, + }; + slot.ok_or(SmcError::InvalidChipSelect) + } - // Deassert CS, then restore the pre-computed normal-read configuration - // (matches aspeed-rust deactivate_user restoring cmd_mode[cs].normal_read). - self.regs - .write_cs_ctrl(cs, user_base | ASPEED_SPI_USER_INACTIVE); - self.regs.write_cs_ctrl(cs, self.normal_read_ctrl(cs)); - Ok(()) + fn poll_blocking_dma_completion(&self, timeout: u32) -> u32 { + let mut to = timeout; + + while (self.regs.read_dma_status() & DMA_STATUS_RELEVANT_BITS) == 0 { + if to == 0 { + return 0; + } + to -= 1; + } + return to; } - // - // MMIO access:: nor read init - // - //TODO: call from nordevice layer instead - pub fn spi_nor_read_init(&mut self, cs: ChipSelect) -> Result<(), SmcError> { + /// Program normal-read command/address width for `cs` and run (or skip) + /// timing calibration. Returns the final normal-read control value the + /// handle restores after each user-mode transfer. + fn calibrate_cs( + &self, + cs: ChipSelect, + spi_clock_mhz: u32, + capacity: usize, + window_base: usize, + ) -> Result { let mode: TransferMode = TransferMode::Mode114; let dummy: u32 = 0x1; - let cs_capacity = self.cs_capacity_bytes(cs)?; - let use_4b_addr = spi_nor_uses_4b_addr(cs_capacity); - let read_opcode = spi_nor_qread_cmd_for_capacity(cs_capacity); - //pw_log::info!("=== spi_read_init()==="); + let use_4b_addr = spi_nor_uses_4b_addr(capacity); + let read_opcode = spi_nor_qread_cmd_for_capacity(capacity); let read_cmd = mode.data_io_bits() | (read_opcode << 16) | (dummy << 6) | ASPEED_SPI_NORMAL_READ; self.regs.write_cs_ctrl(cs, read_cmd); let addr_width = spi_nor_addr_width_reg(self.regs.read_addr_width(), cs, use_4b_addr); self.regs.write_addr_width(addr_width); - self.set_normal_read_ctrl(cs, read_cmd); + if cs != ChipSelect::Cs0 { - // CS1 calibration can fault on boards where the secondary FMC flash - // is not ready for the calibration sweep. Keep CS1 on the same - // fixed timing path used after calibration and still program its - // normal-read command/address width above. - return self.configure_timing(cs, self.cs_config(cs)?.spi_clock_mhz); + // CS1 calibration can fault on boards where the secondary flash is + // not ready for the sweep. Keep CS1 on the fixed timing path and + // still program its normal-read command/address width above. + return self.configure_timing(cs, spi_clock_mhz); } - self.timing_calibration(cs) - } - - fn configure_timing(&mut self, cs: ChipSelect, spi_clock_mhz: u32) -> Result<(), SmcError> { - // Timing calibration is topology-aware. - // - // For BootSpi (FMC, master_idx=0): Full calibration sweep recommended. - // Boot firmware has exclusive access; full timing margin is priority. - // - // For HostSpi / NormalSpi when master_idx != 0: Shared-bus topology. - // When a secondary master shares the flash bus, calibration on CS1 may need - // to be skipped to avoid interfering with the primary master's calibration. - // Phase 3+: gate calibration logic on config.topology.master_idx(). - // - // For now, all topologies use a single divider lookup; no HCLK sweep. - // Phase 3+: add conditional calibration logic per topology and master_idx. - // pw_log::info!("=== configure_timing()==="); + self.timing_calibration(cs, spi_clock_mhz, window_base) + } + + fn configure_timing(&self, cs: ChipSelect, spi_clock_mhz: u32) -> Result { //TODO: need to get this from scu register let sysclk_mhz = 200u32; let encoded_div = spi_freq_div(sysclk_mhz, spi_clock_mhz)?; @@ -618,21 +488,23 @@ impl Smc { let reg = self.regs.read_cs_ctrl(cs); self.regs .write_cs_ctrl(cs, (reg & !SPI_CTRL_FREQ_MASK) | encoded_div); - self.set_normal_read_ctrl(cs, self.regs.read_cs_ctrl(cs)); - Ok(()) + Ok(self.regs.read_cs_ctrl(cs)) } - fn timing_calibration(&mut self, cs: ChipSelect) -> Result<(), SmcError> { - let cs_cfg = self.cs_config(cs)?; - + fn timing_calibration( + &self, + cs: ChipSelect, + spi_clock_mhz: u32, + window_base: usize, + ) -> Result { if self.regs.already_calibrated(cs) { pw_log::info!("already calibrated"); - return self.configure_timing(cs, cs_cfg.spi_clock_mhz); + return self.configure_timing(cs, spi_clock_mhz); } //SPI2 work around - if self.config.topology.master_idx() != 0 && cs != ChipSelect::Cs0 { - return self.configure_timing(cs, cs_cfg.spi_clock_mhz); + if I::CONFIG.topology.master_idx() != 0 && cs != ChipSelect::Cs0 { + return self.configure_timing(cs, spi_clock_mhz); } // TODO: add SPIM config /* @@ -643,7 +515,7 @@ impl Smc { self.regs.write_cs_ctrl(cs, ctrl_val); let check_buf = unsafe { &mut *CALIBRATION_SCRATCH.0.get() }; - let window = self.flash_window_base(cs) as *const u8; + let window = window_base as *const u8; // TODO: configure timing_calibration_start_offset beside be??? let timing_offset = 0x0; let flash_ptr = window.wrapping_add(timing_offset); @@ -652,23 +524,23 @@ impl Smc { } if !spi_calibration_enable(&check_buf[..])? { - return self.configure_timing(cs, cs_cfg.spi_clock_mhz); + return self.configure_timing(cs, spi_clock_mhz); } - let gold_checksum = self.spi_dma_checksum(cs, 0, 0); - self.run_timing_sweep(cs, cs_cfg, gold_checksum); + let gold_checksum = self.spi_dma_checksum(0, 0, window_base); + self.run_timing_sweep(cs, spi_clock_mhz, gold_checksum, window_base); - self.configure_timing(cs, cs_cfg.spi_clock_mhz) + self.configure_timing(cs, spi_clock_mhz) } - fn spi_dma_checksum(&mut self, cs: ChipSelect, div: u32, delay: u32) -> u32 { + fn spi_dma_checksum(&self, div: u32, delay: u32, window_base: usize) -> u32 { let timing_offset = 0x0; // Request DMA access self.regs.acquire_dma_arbiter(); // Set DMA flash start address - let flash_addr = self.flash_window_base(cs) + timing_offset; + let flash_addr = window_base + timing_offset; self.regs.write_dma_flash_addr(flash_addr as u32); // Set DMA length self.regs.write_dma_len(SPI_CALIB_LEN as u32); @@ -695,10 +567,16 @@ impl Smc { return checksum; } - fn run_timing_sweep(&mut self, cs: ChipSelect, cs_cfg: FlashConfig, gold_checksum: u32) { + fn run_timing_sweep( + &self, + cs: ChipSelect, + spi_clock_mhz: u32, + gold_checksum: u32, + window_base: usize, + ) { let hclk_masks = [7u32, 14, 6, 13]; let mut calib_res = [0u8; 6 * 17]; - let mut freq_to_use = cs_cfg.spi_clock_mhz; + let mut freq_to_use = spi_clock_mhz; let sysclk_div_table = [100u32, 66, 50, 40]; // 200 / [2, 3, 4, 5] for (i, &mask) in hclk_masks.iter().enumerate() { @@ -709,7 +587,7 @@ impl Smc { freq_to_use = freq; - self.spi_dma_checksum(cs, mask, 0); + self.spi_dma_checksum(mask, 0, window_base); calib_res.fill(0); @@ -717,7 +595,7 @@ impl Smc { for delay_ns in 0..=0xf { let reg_val = (1 << 3) | hcycle | (delay_ns << 4); - let checksum = self.spi_dma_checksum(cs, mask, reg_val); + let checksum = self.spi_dma_checksum(mask, reg_val, window_base); let pass = checksum == gold_checksum; let index = (hcycle * 17 + delay_ns) as usize; @@ -749,6 +627,280 @@ impl Smc { } // run_timing_sweep } +/// Per-chip-select handle vended by [`Smc::cs0`] / [`Smc::cs1`]. +/// +/// The chip select is baked into the handle, so reads and transfers take no +/// `ChipSelect` argument. The handle borrows the controller exclusively for its +/// lifetime: reads and transfers are `&self`, DMA is `&mut self`, so the +/// compiler enforces one operation on the controller at a time. +pub struct Cs<'a> { + /// Shared register access: MMIO writes go through `&self`, so a shared + /// borrow suffices. Kept disjoint from `state` so the handle can stay free + /// of the controller's `SmcInstance` type parameter. + regs: &'a SmcRegisters, + /// Exclusive borrow of the controller's operation state; this is what makes + /// the handle exclusive (only one `Cs` can exist at a time) and lets DMA + /// transitions mutate state without threading `Smc`. + state: &'a mut SmcState, + cs: ChipSelect, + window_base: usize, + normal_read_ctrl: u32, + geometry: FlashGeometry, + /// Copied from `I::CONTROLLER` / `I::CONFIG` at construction so the handle + /// needs no generic parameter. + controller_id: SmcController, + master_idx: u8, + dma_enabled: bool, + enable_interrupts: bool, +} + +impl Cs<'_> { + /// The chip select this handle drives. + pub fn chip_select(&self) -> ChipSelect { + self.cs + } + + /// Resolved flash geometry for this chip (SFDP-discovered or pinned). + pub fn geometry(&self) -> FlashGeometry { + self.geometry + } + + /// Flash capacity in bytes for this chip. + pub fn capacity_bytes(&self) -> usize { + self.geometry.capacity_bytes as usize + } + + /// The controller this chip is attached to. + pub fn controller_id(&self) -> SmcController { + self.controller_id + } + + /// The master index of the controller's topology (for SPIM mux routing). + pub fn master_idx(&self) -> u8 { + self.master_idx + } + + /// Perform a programmed I/O read via the memory window. + /// + /// Reads directly from the flash memory window. Hardware automatically + /// converts memory accesses to SPI transactions. + pub fn read(&self, offset: u32, buf: &mut [u8]) -> Result { + let offset = validate_mapped_range(offset, buf.len(), self.capacity_bytes())?; + let flash_ptr = (self.window_base as *const u8).wrapping_add(offset); + pw_log::debug!( + "read: offset0x{:08x}, size:0x{:08x}, flash ptr:0x{:08x}", + offset as u32, + buf.len() as u32, + flash_ptr as u32 + ); + // SAFETY: `flash_ptr` is derived from the controller's fixed MMIO flash + // window via `wrapping_add`; the validated `[offset, offset + buf.len())` + // range lies within this chip's mapped aperture, and `buf` is a valid, + // writable destination disjoint from the MMIO window. + unsafe { + core::ptr::copy_nonoverlapping(flash_ptr, buf.as_mut_ptr(), buf.len()); + } + Ok(buf.len()) + } + + /// Execute a raw user-mode SPI transfer on this chip. + /// + /// The `mode` parameter controls the IO width written to the CS control + /// register for each phase (cmd / addr+payload / rx). + pub fn transceive_user( + &self, + cmd: &[u8], + tx_payload: &[u8], + rx: &mut [u8], + mode: TransferMode, + ) -> Result<(), SmcError> { + if *self.state != SmcState::Idle { + return Err(SmcError::ControllerNotReady); + } + transceive_user_raw( + self.regs, + self.cs, + self.window_base, + self.normal_read_ctrl, + cmd, + tx_payload, + rx, + mode, + ); + Ok(()) + } + + /// Initiate a DMA read operation (non-blocking). + pub fn dma_read( + &mut self, + flash_offset: u32, + dram_addr: usize, + len: u32, + ) -> Result<(), SmcError> { + if *self.state != SmcState::Idle { + return Err(SmcError::ControllerNotReady); + } + if !self.dma_enabled { + return Err(SmcError::DmaNotEnabled); + } + self.regs.disable_dma(); + loop_delay(0x1000); + + let cs_capacity = self.capacity_bytes(); + let validated = + validate_dma_read(flash_offset, self.window_base, cs_capacity, dram_addr, len)?; + pw_log::debug!( + "flash start: 0x{:08x}, cs_cap: 0x{:08x}, dram_addr: 0x{:08x} len: 0x{:08x} ", + validated.flash_start as u32, + cs_capacity as u32, + validated.dram_addr as u32, + validated.dma_len_reg as u32 + ); + + // Set the CS control register to normal-read mode before programming DMA + // registers. The DMA engine reads the CSx control register to know which + // SPI command to issue; it must be in normal-read mode (not user mode) + // before the kick. Matches aspeed-rust fmccontroller.rs::read_dma. + let ctrl_val = self.normal_read_ctrl | ASPEED_SPI_NORMAL_READ; + self.regs.write_cs_ctrl(self.cs, ctrl_val); + + // Acquire the DMA bus arbiter before programming any DMA registers. + self.regs.acquire_dma_arbiter(); + self.regs.write_dma_flash_addr(validated.flash_start as u32); + self.regs + .write_dma_dram_addr(validated.dram_addr + SPI_DMA_RAM_MAP_BASE); + self.regs.write_dma_len(validated.dma_len_reg); + + // Arm the completion IRQ before kicking DMA (QEMU evaluates the enable + // once at DMA-done time and won't re-fire if set afterward). + if self.enable_interrupts { + self.regs.enable_dma_irq(); + } + + self.regs.kick_dma_read(); + *self.state = SmcState::DmaInFlight; + Ok(()) + } + + /// Poll for DMA completion without requiring an IRQ. + /// + /// Returns `Poll::Pending` while the transfer is still in progress, + /// `Poll::Ready(Ok(()))` on success, or `Poll::Ready(Err(..))` on failure / + /// when no DMA is in flight. + pub fn poll_dma_completion(&mut self) -> core::task::Poll> { + if *self.state != SmcState::DmaInFlight { + return core::task::Poll::Ready(Err(SmcError::ControllerNotReady)); + } + let status = self.dma_status(); + if status & DMA_STATUS_RELEVANT_BITS == 0 { + return core::task::Poll::Pending; + } + core::task::Poll::Ready(self.complete_dma(status).map(|_| ())) + } + + /// Decode and complete an in-flight DMA operation from an IRQ event. + pub fn handle_dma_irq(&mut self) -> Result { + self.regs.disable_dma_irq(); + let status = self.dma_status(); + pw_log::info!("SMC handle_dma_irq: status=0x{:08x}", status as u32); + if status & DMA_STATUS_RELEVANT_BITS == 0 { + return Err(SmcError::ControllerNotReady); + } + self.complete_dma(status) + } + + /// Read raw DMA/interrupt status register bits (FMC008). + pub fn dma_status(&self) -> u32 { + self.regs.read_dma_status() + } + + /// Clear DMA-related status bits (write-1-to-clear). + pub fn clear_dma_status(&self, clear_mask: u32) { + self.regs + .clear_dma_status(clear_mask & DMA_STATUS_RELEVANT_BITS); + } + + /// Decode status bits and transition controller state. + /// + /// Assumes `status & DMA_STATUS_RELEVANT_BITS != 0`. + fn complete_dma(&mut self, status: u32) -> Result { + let relevant = status & DMA_STATUS_RELEVANT_BITS; + let dma_in_flight = *self.state == SmcState::DmaInFlight; + let decoded = SmcInterruptDecoder::decode_with_context(status, dma_in_flight); + self.clear_dma_status(relevant); + + match decoded { + SmcInterrupt::DmaComplete => { + self.regs.disable_dma(); + *self.state = SmcState::Idle; + Ok(decoded) + } + SmcInterrupt::DmaError => { + self.regs.disable_dma(); + *self.state = SmcState::Idle; + Err(SmcError::DmaAborted) + } + SmcInterrupt::CommandAbort => { + *self.state = SmcState::Faulted; + Err(SmcError::HardwareError) + } + SmcInterrupt::WriteProtected => { + *self.state = SmcState::Faulted; + Err(SmcError::WriteProtected) + } + SmcInterrupt::Unknown => Err(SmcError::HardwareError), + } + } +} + +/// Raw user-mode SPI transfer (CS-assert → 3-phase → CS-restore) shared by +/// `Smc::transceive_user` and `init`'s SFDP read. Module-private and +/// guardless: assumes segments and the per-CS normal-read snapshot are set. +#[allow(clippy::too_many_arguments)] +fn transceive_user_raw( + regs: &SmcRegisters, + cs: ChipSelect, + window_base: usize, + normal_read_ctrl: u32, + cmd: &[u8], + tx_payload: &[u8], + rx: &mut [u8], + mode: TransferMode, +) { + // Derive user-mode base from the stored normal-read value: preserve + // frequency bits and replace mode type with ASPEED_SPI_USER. + let user_base = (normal_read_ctrl & !0x7) | ASPEED_SPI_USER; + let window = window_base as *mut u32; + + // Assert CS: inactive first, then active (matches aspeed-rust activate_user). + regs.write_cs_ctrl(cs, user_base | ASPEED_SPI_USER_INACTIVE); + regs.write_cs_ctrl(cs, user_base); + + // SAFETY: user mode is active; the flash aperture is the hardware-defined + // byte-stream port for SPI command traffic while user mode is held. + unsafe { + // Command phase — always single-wire. + let cmd_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.cmd_io_bits(); + regs.write_cs_ctrl(cs, cmd_ctrl); + spi_write_data(window, cmd); + + // Address / TX payload phase. + let addr_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.addr_io_bits(); + regs.write_cs_ctrl(cs, addr_ctrl); + spi_write_data(window, tx_payload); + + // RX data phase. + let data_ctrl = (user_base & SPI_CTRL_IO_MODE_MASK) | mode.data_io_bits(); + regs.write_cs_ctrl(cs, data_ctrl); + spi_read_data(window as *const u32, rx); + } + + // Deassert CS, then restore the pre-computed normal-read configuration + // (matches aspeed-rust deactivate_user restoring cmd_mode[cs].normal_read). + regs.write_cs_ctrl(cs, user_base | ASPEED_SPI_USER_INACTIVE); + regs.write_cs_ctrl(cs, normal_read_ctrl); +} + unsafe fn spi_read_data(ahb_addr: *const u32, read_arr: &mut [u8]) { let len = read_arr.len(); let (chunks, remainder) = read_arr.split_at_mut(len - len % 4); diff --git a/target/ast10x0/peripherals/smc/device/block_device.rs b/target/ast10x0/peripherals/smc/device/block_device.rs index e9112c215..cb6d31115 100644 --- a/target/ast10x0/peripherals/smc/device/block_device.rs +++ b/target/ast10x0/peripherals/smc/device/block_device.rs @@ -4,7 +4,7 @@ //! Contained block-device facade layered on top of `SpiNorFlash`. use crate::smc::device::flash::{JedecId, SpiNorFlash, SpiNorFlashDevice}; -use crate::smc::types::{FlashConfig, SmcError}; +use crate::smc::types::SmcError; /// Geometry and limits exposed by the block facade. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -18,27 +18,16 @@ pub struct BlockDeviceInfo { /// Minimal block-oriented facade over a `SpiNorFlash` device. pub struct SpiNorBlockDevice<'a, 'b> { flash: &'a mut SpiNorFlash<'b>, - cfg: FlashConfig, } impl<'a, 'b> SpiNorBlockDevice<'a, 'b> { - /// Build a block facade from an existing `SpiNorFlash` plus known config. - pub fn from_flash(flash: &'a mut SpiNorFlash<'b>, cfg: FlashConfig) -> Result { - let expected = cfg_capacity_bytes(cfg)?; - let actual = SpiNorFlashDevice::capacity_bytes(flash)?; - if expected != actual { + /// Build a block facade from an existing `SpiNorFlash`. + pub fn from_flash(flash: &'a mut SpiNorFlash<'b>) -> Result { + let g = flash.geometry(); + if g.page_size == 0 || g.sector_size == 0 || g.block_size == 0 { return Err(SmcError::InvalidCapacity); } - if cfg.page_size == 0 || cfg.sector_size == 0 || cfg.block_size == 0 { - return Err(SmcError::InvalidCapacity); - } - Ok(Self { flash, cfg }) - } - - /// Build a block facade by mapping a JEDEC ID to a known flash profile. - pub fn from_jedec_id(flash: &'a mut SpiNorFlash<'b>, jedec: JedecId) -> Result { - let cfg = cfg_from_jedec(jedec)?; - Self::from_flash(flash, cfg) + Ok(Self { flash }) } /// Read bytes from the block device. @@ -51,7 +40,7 @@ impl<'a, 'b> SpiNorBlockDevice<'a, 'b> { if data.is_empty() { return Ok(0); } - let page = self.cfg.page_size as usize; + let page = self.flash.geometry().page_size as usize; if (address as usize) % page != 0 { return Err(SmcError::InvalidCapacity); } @@ -63,7 +52,7 @@ impl<'a, 'b> SpiNorBlockDevice<'a, 'b> { if length == 0 { return Ok(()); } - let sector = self.cfg.sector_size; + let sector = self.flash.geometry().sector_size; if !address.is_multiple_of(sector) || !length.is_multiple_of(sector) { return Err(SmcError::InvalidCapacity); } @@ -72,11 +61,12 @@ impl<'a, 'b> SpiNorBlockDevice<'a, 'b> { /// Return block-device geometry. pub fn info(&self) -> Result { + let g = self.flash.geometry(); Ok(BlockDeviceInfo { - capacity_bytes: cfg_capacity_bytes(self.cfg)?, - page_size: self.cfg.page_size as usize, - sector_size: self.cfg.sector_size as usize, - block_size: self.cfg.block_size as usize, + capacity_bytes: g.capacity_bytes as usize, + page_size: g.page_size as usize, + sector_size: g.sector_size as usize, + block_size: g.block_size as usize, }) } @@ -85,24 +75,3 @@ impl<'a, 'b> SpiNorBlockDevice<'a, 'b> { self.flash.jedec() } } - -fn cfg_capacity_bytes(cfg: FlashConfig) -> Result { - (cfg.capacity_mb as usize) - .checked_mul(1024 * 1024) - .ok_or(SmcError::InvalidCapacity) -} - -fn cfg_from_jedec(jedec: JedecId) -> Result { - match (jedec.manufacturer, jedec.memory_type, jedec.capacity_code) { - (0xEF, 0x40, 0x17) => Ok(FlashConfig::winbond_w25q64()), - (0xEF, 0x40, 0x18) => Ok(FlashConfig { - capacity_mb: 16, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, - }), - (0xEF, 0x40, 0x19) => Ok(FlashConfig::winbond_w25q256()), - _ => Err(SmcError::DeviceNotSupported), - } -} diff --git a/target/ast10x0/peripherals/smc/device/flash.rs b/target/ast10x0/peripherals/smc/device/flash.rs index 3815cfef0..b725e1620 100644 --- a/target/ast10x0/peripherals/smc/device/flash.rs +++ b/target/ast10x0/peripherals/smc/device/flash.rs @@ -7,9 +7,9 @@ use core::ops::FnMut; use core::result::Result; use core::result::Result::{Err, Ok}; -use crate::smc::fmc::FmcReady; -use crate::smc::spi::SpiReady; -use crate::smc::types::{AddressWidth, ChipSelect, FlashConfig, SmcError, TransferMode}; +use crate::smc::controller::Cs; +use crate::smc::types::{AddressWidth, SmcError, TransferMode}; +use util_sfdp::FlashGeometry; /// Build a command byte array: opcode followed by address bytes selected by /// `width`. Returns a fixed-size buffer and the valid length. @@ -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`. @@ -176,11 +191,6 @@ fn poll_delay() { } } -enum FlashBackend<'a> { - Fmc(&'a FmcReady), - Spi(&'a SpiReady), -} - /// Decoded JEDEC identifier returned by `READ_ID` (`0x9F`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct JedecId { @@ -214,11 +224,10 @@ fn expect_jedec_match(actual: JedecId, expected: JedecId) -> Result { - backend: FlashBackend<'a>, + /// Per-chip handle; the chip select is baked in. + cs: Cs<'a>, // Validated metadata for Phase 3B alignment/policy checks. - cfg: FlashConfig, - /// Chip select this flash device sits on. - cs: ChipSelect, + cfg: FlashGeometry, /// IO mode used for all SPI command transactions (WREN, RDSR, PP, SE). /// Defaults to `Mode111` (single-wire cmd/addr/data). cmd_mode: TransferMode, @@ -229,46 +238,17 @@ pub struct SpiNorFlash<'a> { } impl<'a> SpiNorFlash<'a> { - /// Build a flash facade from an initialized FMC controller wrapper. - pub fn from_fmc(fmc: &'a mut FmcReady, cfg: FlashConfig) -> Result { - Self::from_fmc_cs(fmc, cfg, ChipSelect::Cs0) - } - - /// Build a flash facade from an initialized FMC controller wrapper with explicit CS. - pub fn from_fmc_cs( - fmc: &'a mut FmcReady, - cfg: FlashConfig, - cs: ChipSelect, - ) -> Result { + /// Build a flash facade from a per-chip [`Cs`] handle. + /// + /// The handle already carries the resolved flash geometry (pinned or + /// SFDP-discovered) and the chip select, so no `ChipSelect` argument is + /// needed here. + pub fn new(cs: Cs<'a>) -> Result { + let cfg = cs.geometry(); let addressing_policy = Self::default_addressing_for_cfg(cfg); - Self::validate_capacity_cfg(cfg, fmc.cs_config(cs)?)?; Ok(Self { - backend: FlashBackend::Fmc(fmc), - cfg, cs, - cmd_mode: TransferMode::Mode111, - addressing_policy, - command_profile: FlashCommandProfile::for_addressing(addressing_policy), - }) - } - - /// Build a flash facade from an initialized SPI1/SPI2 controller wrapper. - pub fn from_spi(spi: &'a mut SpiReady, cfg: FlashConfig) -> Result { - Self::from_spi_cs(spi, cfg, ChipSelect::Cs0) - } - - /// Build a flash facade from an initialized SPI1/SPI2 controller wrapper with explicit CS. - pub fn from_spi_cs( - spi: &'a mut SpiReady, - cfg: FlashConfig, - cs: ChipSelect, - ) -> Result { - let addressing_policy = Self::default_addressing_for_cfg(cfg); - Self::validate_capacity_cfg(cfg, spi.cs_config(cs)?)?; - Ok(Self { - backend: FlashBackend::Spi(spi), cfg, - cs, cmd_mode: TransferMode::Mode111, addressing_policy, command_profile: FlashCommandProfile::for_addressing(addressing_policy), @@ -383,21 +363,19 @@ impl<'a> SpiNorFlash<'a> { Ok(written) } - fn validate_capacity_cfg(cfg: FlashConfig, expected: FlashConfig) -> Result<(), SmcError> { - if cfg != expected { - return Err(SmcError::InvalidCapacity); - } - Ok(()) - } - - fn default_addressing_for_cfg(cfg: FlashConfig) -> FlashAddressingPolicy { - if cfg.capacity_mb > 16 { + fn default_addressing_for_cfg(cfg: FlashGeometry) -> FlashAddressingPolicy { + if cfg.capacity_bytes > 16 * 1024 * 1024 { FlashAddressingPolicy::FourByteCommands } else { FlashAddressingPolicy::ThreeByteOnly } } + /// Return the SFDP-discovered geometry for this device. + pub fn geometry(&self) -> FlashGeometry { + self.cfg + } + pub fn addr_width(&self) -> AddressWidth { self.addressing_policy.addr_width() } @@ -408,8 +386,8 @@ impl<'a> SpiNorFlash<'a> { /// Validate a device-local offset before handing it to the controller. /// - /// `FmcReady::read` / `SpiReady::read` already select the per-CS AHB - /// window from the chip-select argument, so offsets stay CS-local here. + /// The `Cs` handle already selects the per-CS AHB window, so offsets stay + /// CS-local here. fn device_to_controller_offset(&self, device_offset: u32) -> Result { let cs_cap = self.capacity_bytes()?; if (device_offset as usize) >= cs_cap { @@ -436,49 +414,28 @@ 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()) } fn issue_command(&mut self, cmd: &[u8], payload: &[u8]) -> Result<(), SmcError> { - let cs = self.cs; let mode = self.cmd_mode; - match &self.backend { - FlashBackend::Fmc(fmc) => fmc.transceive_user(cs, cmd, payload, &mut [], mode), - FlashBackend::Spi(spi) => spi.transceive_user(cs, cmd, payload, &mut [], mode), - } + self.cs.transceive_user(cmd, payload, &mut [], mode) } fn read_status_impl(&self) -> Result { - let cs = self.cs; let mode = self.cmd_mode; let opcode = self.command_profile().read_status; let mut status = [0u8; 1]; - match &self.backend { - FlashBackend::Fmc(fmc) => fmc.transceive_user(cs, &[opcode], &[], &mut status, mode)?, - FlashBackend::Spi(spi) => spi.transceive_user(cs, &[opcode], &[], &mut status, mode)?, - } + self.cs.transceive_user(&[opcode], &[], &mut status, mode)?; Ok(status[0]) } fn read_jedec_id_impl(&self) -> Result<[u8; 3], SmcError> { - let cs = self.cs; let mode = self.cmd_mode; let mut id = [0u8; 3]; - match &self.backend { - FlashBackend::Fmc(fmc) => { - fmc.transceive_user(cs, &[commands::READ_ID], &[], &mut id, mode)? - } - FlashBackend::Spi(spi) => { - spi.transceive_user(cs, &[commands::READ_ID], &[], &mut id, mode)? - } - } + self.cs + .transceive_user(&[commands::READ_ID], &[], &mut id, mode)?; Ok(id) } @@ -507,20 +464,13 @@ impl SpiNorFlashDevice for SpiNorFlash<'_> { // the controller-window address before issuing the segment-routed read. self.validate_range(offset, buf.len())?; let translated = self.device_to_controller_offset(offset)?; - let cs = self.cs; //TODO: need to add dma_read variant if buf.len() is above some threshold // and dma is enabled - match &self.backend { - FlashBackend::Fmc(fmc) => fmc.read(cs, translated, buf), - FlashBackend::Spi(spi) => spi.read(cs, translated, buf), - } + self.cs.read(translated, buf) } fn capacity_bytes(&self) -> Result { - match &self.backend { - FlashBackend::Fmc(fmc) => fmc.cs_capacity_bytes(self.cs), - FlashBackend::Spi(spi) => spi.cs_capacity_bytes(self.cs), - } + Ok(self.cs.capacity_bytes()) } fn erase_sector(&mut self, offset: u32) -> Result<(), SmcError> { @@ -566,7 +516,8 @@ mod tests { commands, compare_chunked, encode_addr_cmd, expect_jedec_match, FlashAddressingPolicy, FlashCommandProfile, JedecId, SpiNorFlash, }; - use crate::smc::types::{AddressWidth, FlashConfig, SmcError}; + use crate::smc::types::{AddressWidth, SmcError}; + use util_sfdp::FlashGeometry; #[test] fn encode_addr_cmd_none_emits_opcode_only() { @@ -612,23 +563,21 @@ mod tests { #[test] fn default_addressing_policy_derives_from_capacity() { assert_eq!( - SpiNorFlash::default_addressing_for_cfg(FlashConfig { - capacity_mb: 8, + SpiNorFlash::default_addressing_for_cfg(FlashGeometry { + capacity_bytes: 8 * 1024 * 1024, page_size: 256, sector_size: 4096, block_size: 65536, - spi_clock_mhz: 25, }), FlashAddressingPolicy::ThreeByteOnly ); assert_eq!( - SpiNorFlash::default_addressing_for_cfg(FlashConfig { - capacity_mb: 32, + SpiNorFlash::default_addressing_for_cfg(FlashGeometry { + capacity_bytes: 32 * 1024 * 1024, page_size: 256, sector_size: 4096, block_size: 65536, - spi_clock_mhz: 25, }), FlashAddressingPolicy::FourByteCommands ); @@ -695,4 +644,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) + ); + } } diff --git a/target/ast10x0/peripherals/smc/fmc.rs b/target/ast10x0/peripherals/smc/fmc.rs index bce687f5f..ecdd983bd 100644 --- a/target/ast10x0/peripherals/smc/fmc.rs +++ b/target/ast10x0/peripherals/smc/fmc.rs @@ -16,77 +16,57 @@ //! //! See [`crate::smc`] module-level documentation for the full taxonomy. -use crate::smc::controller::{ReadySmc, UninitSmc}; -use crate::smc::interrupts::SmcInterrupt; -use crate::smc::types::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, TransferMode, -}; +use crate::smc::controller::{Cs, ReadySmc, UninitSmc}; +use crate::smc::types::{SmcController, SmcError, SmcInstance}; /// FMC handle before hardware initialization. -pub struct FmcUninit { - inner: UninitSmc, +pub struct FmcUninit { + inner: UninitSmc, } /// FMC handle after hardware initialization. -pub struct FmcReady { - inner: ReadySmc, +/// +/// Per-chip operations (read, transfer, DMA) are reached through a [`Cs`] handle +/// vended by [`FmcReady::cs0`] / [`FmcReady::cs1`]; the chip select is baked into +/// that handle rather than passed on every call. +pub struct FmcReady { + inner: ReadySmc, } -impl FmcUninit { +impl FmcUninit { /// Construct an uninitialized FMC controller. /// /// # Safety /// Caller must ensure unique ownership of the FMC hardware block. - pub unsafe fn new(mut config: SmcConfig) -> Result { - config.controller_id = SmcController::Fmc; + pub unsafe fn new() -> Result { + const { + assert!( + matches!(I::CONTROLLER, SmcController::Fmc), + "FmcUninit requires an SmcInstance whose CONTROLLER is Fmc" + ); + } // SAFETY: Caller upholds controller ownership requirements. - let inner = unsafe { UninitSmc::new(config)? }; + let inner = unsafe { UninitSmc::::new()? }; Ok(Self { inner }) } /// Initialize FMC hardware and transition to ready state. - pub fn init(self) -> Result { + pub fn init(self) -> Result, SmcError> { Ok(FmcReady { inner: self.inner.init()?, }) } } -impl FmcReady { - /// Perform a programmed I/O read via the FMC flash window. - pub fn read(&self, cs: ChipSelect, offset: u32, buf: &mut [u8]) -> Result { - self.inner.read(cs, offset, buf) +impl FmcReady { + /// Build a handle for CS0 from its init-resolved geometry. + pub fn cs0(&mut self) -> Result, SmcError> { + self.inner.cs0() } - /// Initiate a DMA read operation. - pub fn dma_read( - &mut self, - cs: ChipSelect, - flash_offset: u32, - dram_addr: usize, - len: u32, - ) -> Result<(), SmcError> { - self.inner.dma_read(cs, flash_offset, dram_addr, len) - } - - /// Read raw DMA/interrupt status bits from FMC008. - pub fn dma_status(&self) -> u32 { - self.inner.dma_status() - } - - /// Clear DMA-related status bits in FMC008 (write-1-to-clear). - pub fn clear_dma_status(&self, clear_mask: u32) { - self.inner.clear_dma_status(clear_mask) - } - - /// Handle DMA completion/error from IRQ status and finalize controller state. - pub fn handle_dma_irq(&mut self) -> Result { - self.inner.handle_dma_irq() - } - - /// Poll for DMA completion without requiring an IRQ. See `Smc::poll_dma_completion`. - pub fn poll_dma_completion(&mut self) -> core::task::Poll> { - self.inner.poll_dma_completion() + /// Build a handle for CS1 from its init-resolved geometry. + pub fn cs1(&mut self) -> Result, SmcError> { + self.inner.cs1() } /// Check if FMC is ready for operations. @@ -94,65 +74,13 @@ impl FmcReady { self.inner.is_ready() } - /// Program memory-mapped SPI NOR read mode for the selected chip select. - pub fn spi_nor_read_init(&mut self, cs: ChipSelect) -> Result<(), SmcError> { - self.inner.spi_nor_read_init(cs) + /// Get the controller identifier. + pub fn controller_id(&self) -> SmcController { + self.inner.controller_id() } #[doc(hidden)] pub fn test_force_dma_in_flight(&mut self) { self.inner.test_force_dma_in_flight(); } - - /// Return configured flash capacity in bytes. - pub fn capacity_bytes(&self) -> Result { - self.inner.capacity_bytes() - } - - /// Return configured flash capacity in bytes for the given chip select. - pub fn cs_capacity_bytes(&self, cs: ChipSelect) -> Result { - self.inner.cs_capacity_bytes(cs) - } - - /// Return the configured `FlashConfig` for the requested chip select. - pub fn cs_config(&self, cs: ChipSelect) -> Result { - self.inner.cs_config(cs) - } - - /// Execute a raw user-mode SPI transfer on the selected FMC chip select. - /// - /// `cs` selects CS0 or CS1; `mode` controls the per-phase IO width. - /// Returns `SmcError::InvalidChipSelect` if CS1 is requested but not configured. - pub fn transceive_user( - &self, - cs: ChipSelect, - cmd: &[u8], - tx_payload: &[u8], - rx: &mut [u8], - mode: TransferMode, - ) -> Result<(), SmcError> { - self.inner.transceive_user(cs, cmd, tx_payload, rx, mode) - } - - /// Convenience wrapper: execute a user-mode transfer on CS0. - pub fn transceive_user_cs0( - &self, - cmd: &[u8], - tx_payload: &[u8], - rx: &mut [u8], - mode: TransferMode, - ) -> Result<(), SmcError> { - self.inner - .transceive_user(ChipSelect::Cs0, cmd, tx_payload, rx, mode) - } - - /// Access the underlying generic ready controller. - pub fn as_inner(&self) -> &ReadySmc { - &self.inner - } - - /// Mutable access to the underlying generic ready controller. - pub fn as_inner_mut(&mut self) -> &mut ReadySmc { - &mut self.inner - } } diff --git a/target/ast10x0/peripherals/smc/helpers.rs b/target/ast10x0/peripherals/smc/helpers.rs index 6328f0fc2..df1fd0849 100644 --- a/target/ast10x0/peripherals/smc/helpers.rs +++ b/target/ast10x0/peripherals/smc/helpers.rs @@ -5,12 +5,9 @@ use core::convert::TryFrom; -use crate::smc::types::ChipSelect; -use crate::smc::types::FlashConfig; -use crate::smc::types::SmcConfig; use crate::smc::types::SmcError; -const SMC_WINDOW_SIZE_BYTES: usize = 256 * 1024 * 1024; +pub(crate) const SMC_WINDOW_SIZE_BYTES: usize = 256 * 1024 * 1024; const DMA_MAX_TRANSFER_LENGTH: u32 = 0x20_0000; // 32MBytes pub(crate) const SPI_CTRL_FREQ_MASK: u32 = 0x0F00_0F00; @@ -64,41 +61,6 @@ pub(crate) struct ValidatedDmaRead { pub dma_len_reg: u32, } -pub(crate) fn flash_capacity_bytes(config: Option) -> Result { - match config { - Some(config) => (config.capacity_mb as usize) - .checked_mul(1024 * 1024) - .ok_or(SmcError::InvalidCapacity), - None => Ok(0), - } -} - -pub(crate) fn cs_capacity_bytes(config: &SmcConfig, cs: ChipSelect) -> Result { - let slot = match cs { - ChipSelect::Cs0 => config.cs0, - ChipSelect::Cs1 => config.cs1, - }; - match slot { - Some(_) => flash_capacity_bytes(slot), - None => Err(SmcError::InvalidChipSelect), - } -} - -pub(crate) fn total_capacity_bytes( - cs0: Option, - cs1: Option, -) -> Result { - let cs0_size = flash_capacity_bytes(cs0)?; - let cs1_size = flash_capacity_bytes(cs1)?; - let total = cs0_size - .checked_add(cs1_size) - .ok_or(SmcError::InvalidCapacity)?; - if total > SMC_WINDOW_SIZE_BYTES { - return Err(SmcError::InvalidCapacity); - } - Ok(total) -} - pub(crate) fn validate_mapped_range( offset: u32, len: usize, @@ -167,7 +129,7 @@ pub(crate) fn validate_dma_read( /// Encode an FMC memory segment into hardware register format. /// /// FMC decode fields use 512 KiB alignment. `end` is exclusive. -pub(crate) fn encode_fmc_segment(start: usize, end: usize) -> Result { +pub(crate) const fn encode_fmc_segment(start: usize, end: usize) -> Result { if end == 0 || end <= start { return Err(SmcError::InvalidCapacity); } @@ -180,7 +142,7 @@ pub(crate) fn encode_fmc_segment(start: usize, end: usize) -> Result Result { +pub(crate) const fn encode_spi_segment(start: usize, end: usize) -> Result { if end == 0 || end <= start { return Err(SmcError::InvalidCapacity); } @@ -345,27 +307,6 @@ mod tests { assert!(result.is_err()); } - #[test] - fn test_total_capacity_overflow() { - let result = total_capacity_bytes( - Some(FlashConfig { - capacity_mb: 128, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, - }), - Some(FlashConfig { - capacity_mb: 129, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, - }), - ); - assert!(result.is_err()); - } - #[test] fn test_validate_mapped_range_accepts_exact_fit() { let offset = validate_mapped_range(4092, 4, 4096).unwrap(); diff --git a/target/ast10x0/peripherals/smc/mod.rs b/target/ast10x0/peripherals/smc/mod.rs index f494922e7..a8d197a97 100644 --- a/target/ast10x0/peripherals/smc/mod.rs +++ b/target/ast10x0/peripherals/smc/mod.rs @@ -15,7 +15,9 @@ pub mod registers; pub mod spi; pub mod types; -pub use controller::{Ready, ReadySmc, Smc, UninitSmc, Uninitialized}; +pub use controller::{ + Cs, GeometrySource, Pinned, Ready, ReadySmc, Smc, SmcMode, UninitSmc, Uninitialized, +}; pub use device::{ BlockDeviceInfo, FlashAddressingPolicy, FlashCommandProfile, JedecId, SpiNorBlockDevice, SpiNorFlash, SpiNorFlashDevice, @@ -24,9 +26,10 @@ pub use fmc::{FmcReady, FmcUninit}; pub use interrupts::{SmcInterrupt, SmcInterruptDecoder}; pub use spi::{SpiReady, SpiTransaction, SpiUninit}; pub use types::{ - AddressWidth, ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcRetryable, - SmcTopology, TransferMode, + AddressWidth, ChipSelect, Discover, FlashConfig, SmcConfig, SmcController, SmcError, + SmcInstance, SmcRetryable, SmcTopology, TransferMode, }; +pub use util_sfdp::FlashGeometry; /// Result type for SMC operations pub type Result = core::result::Result; diff --git a/target/ast10x0/peripherals/smc/spi/spi.rs b/target/ast10x0/peripherals/smc/spi/spi.rs index 343b70873..7d272e482 100644 --- a/target/ast10x0/peripherals/smc/spi/spi.rs +++ b/target/ast10x0/peripherals/smc/spi/spi.rs @@ -12,20 +12,17 @@ //! //! The wrapper's role is **construction and delegation only**: //! - `SpiUninit::new()`: Type-check that controller_id is SPI1 or SPI2 (not FMC) -//! - `SpiReady` methods: Delegate to inner controller operations +//! - `SpiReady::cs0` / `cs1`: Vend a per-chip [`Cs`] handle //! -//! For BootSpi (FMC), use `Smc` directly. +//! For BootSpi (FMC), use the [`crate::smc::fmc`] wrapper. //! For HostSpi/NormalSpi (SPI1/SPI2), use this wrapper. -use crate::smc::controller::{ReadySmc, UninitSmc}; -use crate::smc::interrupts::SmcInterrupt; -use crate::smc::types::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, TransferMode, -}; +use crate::smc::controller::{Cs, ReadySmc, UninitSmc}; +use crate::smc::types::{SmcController, SmcError, SmcInstance}; /// SPI handle before hardware initialization. -pub struct SpiUninit { - inner: UninitSmc, +pub struct SpiUninit { + inner: UninitSmc, } /// SPI handle after hardware initialization. @@ -37,90 +34,55 @@ pub struct SpiUninit { /// programming) is handled by the controller layer, not here. This keeps the wrapper /// thin and the topology logic centralized. /// -/// # Example -/// -/// ```ignore -/// let mut spi = unsafe { SpiUninit::new(SmcController::Spi1, config)? }; -/// let spi = spi.init()?; // Topology-gated behaviors in controller.init() -/// spi.read(0, &mut buf)?; -/// ``` -pub struct SpiReady { - inner: ReadySmc, +/// Per-chip operations (read, transfer, DMA) are reached through a [`Cs`] handle +/// vended by [`SpiReady::cs0`] / [`SpiReady::cs1`]; SPIM mux bracketing is layered +/// on top of that handle by [`crate::smc::spi::SpiTransaction`]. +pub struct SpiReady { + inner: ReadySmc, } -impl SpiUninit { +impl SpiUninit { /// Construct an uninitialized SPI controller for SPI1 or SPI2. /// /// # Topology Requirements /// /// The SPI wrapper is for HostSpi and NormalSpi topologies only. - /// BootSpi (FMC) should use the generic Smc directly. + /// BootSpi (FMC) should use the [`crate::smc::fmc`] wrapper. /// /// # Safety /// Caller must ensure unique ownership of the selected SPI hardware block. - pub unsafe fn new( - controller_id: SmcController, - mut config: SmcConfig, - ) -> Result { - // Phase 3: Topology-aware SPI construction check. - // + pub unsafe fn new() -> Result { // The SPI wrapper is specialized for HostSpi and NormalSpi topologies. - // FMC (BootSpi topology) uses the generic controller with FmcRegisterBackend. - // This check enforces that constraint at construction time. - match controller_id { - SmcController::Fmc => return Err(SmcError::InvalidChipSelect), - SmcController::Spi1 | SmcController::Spi2 => {} + // FMC (BootSpi topology) uses the FMC wrapper. Enforce that here. + const { + assert!( + !matches!(I::CONTROLLER, SmcController::Fmc), + "SpiUninit requires an SmcInstance whose CONTROLLER is Spi1 or Spi2" + ); } - config.controller_id = controller_id; // SAFETY: Caller upholds controller ownership requirements. - let inner = unsafe { UninitSmc::new(config)? }; + let inner = unsafe { UninitSmc::::new()? }; Ok(Self { inner }) } /// Initialize SPI hardware and transition to ready state. - pub fn init(self) -> Result { + pub fn init(self) -> Result, SmcError> { Ok(SpiReady { inner: self.inner.init()?, }) } } -impl SpiReady { - /// Perform a programmed I/O read via the SPI flash window. - pub fn read(&self, cs: ChipSelect, offset: u32, buf: &mut [u8]) -> Result { - self.inner.read(cs, offset, buf) - } - - /// Initiate a DMA read operation. - pub fn dma_read( - &mut self, - cs: ChipSelect, - flash_offset: u32, - dram_addr: usize, - len: u32, - ) -> Result<(), SmcError> { - self.inner.dma_read(cs, flash_offset, dram_addr, len) - } - - /// Read raw DMA/interrupt status bits from FMC008. - pub fn dma_status(&self) -> u32 { - self.inner.dma_status() - } - - /// Clear DMA-related status bits in FMC008 (write-1-to-clear). - pub fn clear_dma_status(&self, clear_mask: u32) { - self.inner.clear_dma_status(clear_mask) +impl SpiReady { + /// Build a handle for CS0 from its init-resolved geometry. + pub fn cs0(&mut self) -> Result, SmcError> { + self.inner.cs0() } - /// Handle DMA completion/error from IRQ status and finalize controller state. - pub fn handle_dma_irq(&mut self) -> Result { - self.inner.handle_dma_irq() - } - - /// Poll for DMA completion without requiring an IRQ. See `Smc::poll_dma_completion`. - pub fn poll_dma_completion(&mut self) -> core::task::Poll> { - self.inner.poll_dma_completion() + /// Build a handle for CS1 from its init-resolved geometry. + pub fn cs1(&mut self) -> Result, SmcError> { + self.inner.cs1() } /// Get the controller identifier. @@ -137,61 +99,4 @@ impl SpiReady { pub fn is_ready(&self) -> bool { self.inner.is_ready() } - - /// Program memory-mapped SPI NOR read mode for the selected chip select. - pub fn spi_nor_read_init(&mut self, cs: ChipSelect) -> Result<(), SmcError> { - self.inner.spi_nor_read_init(cs) - } - - /// Return configured flash capacity in bytes. - pub fn capacity_bytes(&self) -> Result { - self.inner.capacity_bytes() - } - - /// Return configured flash capacity in bytes for the given chip select. - pub fn cs_capacity_bytes(&self, cs: ChipSelect) -> Result { - self.inner.cs_capacity_bytes(cs) - } - - /// Return the configured `FlashConfig` for the requested chip select. - pub fn cs_config(&self, cs: ChipSelect) -> Result { - self.inner.cs_config(cs) - } - - /// Execute a raw user-mode SPI transfer on the selected SPI chip select. - /// - /// `cs` selects CS0 or CS1; `mode` controls the per-phase IO width. - /// Returns `SmcError::InvalidChipSelect` if CS1 is requested but not configured. - pub fn transceive_user( - &self, - cs: ChipSelect, - cmd: &[u8], - tx_payload: &[u8], - rx: &mut [u8], - mode: TransferMode, - ) -> Result<(), SmcError> { - self.inner.transceive_user(cs, cmd, tx_payload, rx, mode) - } - - /// Convenience wrapper: execute a user-mode transfer on CS0. - pub fn transceive_user_cs0( - &self, - cmd: &[u8], - tx_payload: &[u8], - rx: &mut [u8], - mode: TransferMode, - ) -> Result<(), SmcError> { - self.inner - .transceive_user(ChipSelect::Cs0, cmd, tx_payload, rx, mode) - } - - /// Access the underlying generic ready controller. - pub fn as_inner(&self) -> &ReadySmc { - &self.inner - } - - /// Mutable access to the underlying generic ready controller. - pub fn as_inner_mut(&mut self) -> &mut ReadySmc { - &mut self.inner - } } diff --git a/target/ast10x0/peripherals/smc/spi/spi_transaction.rs b/target/ast10x0/peripherals/smc/spi/spi_transaction.rs index 02c2f2991..fb3e93e83 100644 --- a/target/ast10x0/peripherals/smc/spi/spi_transaction.rs +++ b/target/ast10x0/peripherals/smc/spi/spi_transaction.rs @@ -5,10 +5,10 @@ use core::task::Poll; -use super::spi::SpiReady; use crate::scu::{ScuRegisters, SpiMonitorInstance, SpiMonitorSource, SpimGpioOriVal}; +use crate::smc::controller::Cs; use crate::smc::interrupts::SmcInterrupt; -use crate::smc::types::{ChipSelect, SmcController, SmcError, TransferMode}; +use crate::smc::types::{SmcController, SmcError, TransferMode}; #[derive(Clone, Copy)] struct SpiMuxState { @@ -19,15 +19,18 @@ struct SpiMuxState { /// In-flight SPI transaction state. /// -/// Synchronous helpers hide this state entirely. DMA returns it so the mux can -/// stay enabled until DMA completion. +/// Wraps a per-chip [`Cs`] handle and brackets the operation with SPIM +/// internal-mux setup/restore. The chip select is baked into the handle, so no +/// `ChipSelect` argument is threaded through. Synchronous helpers hide the mux +/// state entirely; DMA returns the transaction so the mux stays enabled until +/// DMA completion. pub struct SpiTransaction<'a> { - spi: &'a mut SpiReady, + cs: Cs<'a>, previous_mux: Option, } impl<'a> SpiTransaction<'a> { - fn begin(spi: &'a mut SpiReady, spim: Option) -> Result { + fn begin(cs: Cs<'a>, spim: Option) -> Result { let mut mux_state = SpiMuxState { spim_present: spim.is_some(), internal_mux_active: false, @@ -39,8 +42,8 @@ impl<'a> SpiTransaction<'a> { scu.validate_spim_instance(spim) .map_err(|_| SmcError::HardwareError)?; - if spi.master_idx() != 0 { - scu.set_spim_internal_mux(Self::spim_source(spi)?, spim as u8 + 1) + if cs.master_idx() != 0 { + scu.set_spim_internal_mux(Self::spim_source(&cs)?, spim as u8 + 1) .map_err(|_| SmcError::HardwareError)?; mux_state.internal_mux_active = true; } @@ -49,89 +52,79 @@ impl<'a> SpiTransaction<'a> { } Ok(Self { - spi, + cs, previous_mux: Some(mux_state), }) } - pub fn read( - spi: &'a mut SpiReady, - cs: ChipSelect, - offset: u32, - buf: &mut [u8], - ) -> Result { - Self::read_with_spim(spi, None, cs, offset, buf) + pub fn read(cs: Cs<'a>, offset: u32, buf: &mut [u8]) -> Result { + Self::read_with_spim(cs, None, offset, buf) } pub fn read_with_spim( - spi: &'a mut SpiReady, + cs: Cs<'a>, spim: impl Into>, - cs: ChipSelect, offset: u32, buf: &mut [u8], ) -> Result { - let mut txn = Self::begin(spi, spim.into())?; - let result = txn.spi.read(cs, offset, buf); + let mut txn = Self::begin(cs, spim.into())?; + let result = txn.cs.read(offset, buf); txn.finish_result(result) } pub fn transceive_user( - spi: &'a mut SpiReady, - cs: ChipSelect, + cs: Cs<'a>, cmd: &[u8], tx_payload: &[u8], rx: &mut [u8], mode: TransferMode, ) -> Result<(), SmcError> { - Self::transceive_user_with_spim(spi, None, cs, cmd, tx_payload, rx, mode) + Self::transceive_user_with_spim(cs, None, cmd, tx_payload, rx, mode) } pub fn transceive_user_with_spim( - spi: &'a mut SpiReady, + cs: Cs<'a>, spim: impl Into>, - cs: ChipSelect, cmd: &[u8], tx_payload: &[u8], rx: &mut [u8], mode: TransferMode, ) -> Result<(), SmcError> { - let mut txn = Self::begin(spi, spim.into())?; - let result = txn.spi.transceive_user(cs, cmd, tx_payload, rx, mode); + let mut txn = Self::begin(cs, spim.into())?; + let result = txn.cs.transceive_user(cmd, tx_payload, rx, mode); txn.finish_result(result) } pub fn dma_read( - spi: &'a mut SpiReady, - cs: ChipSelect, + cs: Cs<'a>, flash_offset: u32, dram_addr: usize, len: u32, ) -> Result { - Self::dma_read_with_spim(spi, None, cs, flash_offset, dram_addr, len) + Self::dma_read_with_spim(cs, None, flash_offset, dram_addr, len) } pub fn dma_read_with_spim( - spi: &'a mut SpiReady, + cs: Cs<'a>, spim: impl Into>, - cs: ChipSelect, flash_offset: u32, dram_addr: usize, len: u32, ) -> Result { - let txn = Self::begin(spi, spim.into())?; - txn.spi.dma_read(cs, flash_offset, dram_addr, len)?; + let mut txn = Self::begin(cs, spim.into())?; + txn.cs.dma_read(flash_offset, dram_addr, len)?; Ok(txn) } pub fn poll_dma_completion(&mut self) -> Poll> { - match self.spi.poll_dma_completion() { + match self.cs.poll_dma_completion() { Poll::Pending => Poll::Pending, Poll::Ready(result) => Poll::Ready(result.and_then(|_| self.finish_restore())), } } pub fn handle_dma_irq(&mut self) -> Result { - let interrupt = self.spi.handle_dma_irq()?; + let interrupt = self.cs.handle_dma_irq()?; self.finish_restore()?; Ok(interrupt) } @@ -163,8 +156,8 @@ impl<'a> SpiTransaction<'a> { Ok(()) } - fn spim_source(spi: &SpiReady) -> Result { - match spi.controller_id() { + fn spim_source(cs: &Cs<'a>) -> Result { + match cs.controller_id() { SmcController::Spi1 => Ok(SpiMonitorSource::Spi1), SmcController::Spi2 => Ok(SpiMonitorSource::Spi2), SmcController::Fmc => Err(SmcError::InvalidChipSelect), diff --git a/target/ast10x0/peripherals/smc/types.rs b/target/ast10x0/peripherals/smc/types.rs index 675c8f3c8..c332a2454 100644 --- a/target/ast10x0/peripherals/smc/types.rs +++ b/target/ast10x0/peripherals/smc/types.rs @@ -5,6 +5,8 @@ use embedded_storage::nor_flash::{NorFlashError, NorFlashErrorKind}; +use crate::smc::controller::GeometrySource; + /// Terminal errors: operation failed, don't retry #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SmcError { @@ -40,7 +42,7 @@ pub enum SmcError { pub enum ChipSelect { /// Primary chip select (CS0) — always valid when any flash is configured. Cs0 = 0, - /// Secondary chip select (CS1) — valid only when `SmcConfig.cs1.is_some()`. + /// Secondary chip select (CS1) — valid only when `SmcConfig.cs1` is `Some`. Cs1 = 1, } @@ -151,7 +153,7 @@ pub enum SmcController { impl SmcController { /// Get the base hardware address for this controller - pub fn base_address(&self) -> usize { + pub const fn base_address(&self) -> usize { match self { Self::Fmc => 0x7E620000, Self::Spi1 => 0x7E630000, @@ -160,7 +162,7 @@ impl SmcController { } /// Get the memory-mapped flash window address - pub fn flash_window_address(&self) -> usize { + pub const fn flash_window_address(&self) -> usize { match self { Self::Fmc => 0x80000000, Self::Spi1 => 0x90000000, @@ -178,44 +180,21 @@ impl SmcController { } } -/// Configuration for a single flash device +/// Per-chip-select input configuration. Holds only the SPI clock; the flash +/// geometry is resolved once at init from the [`SmcInstance`]'s per-CS +/// [`GeometrySource`](crate::smc::controller::GeometrySource). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FlashConfig { - /// Device capacity in MB - pub capacity_mb: u32, - /// Page size in bytes (typically 256) - pub page_size: u32, - /// Sector size in bytes (typically 4096) - pub sector_size: u32, - /// Block size in bytes (typically 65536) - pub block_size: u32, /// Desired SPI clock frequency in MHz pub spi_clock_mhz: u32, } -impl FlashConfig { - /// Winbond W25Q64 (8 MB) configuration - pub const fn winbond_w25q64() -> Self { - Self { - capacity_mb: 8, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, - } - } - - /// Winbond W25Q256 (32 MB) configuration - pub const fn winbond_w25q256() -> Self { - Self { - capacity_mb: 32, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, - } - } -} +/// Geometry-source marker: discover the geometry by reading SFDP from the chip. +/// +/// This is the only place SFDP discovery lives; a build whose markers never name +/// `Discover` never links the SFDP decode code. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Discover; /// SPI controller topology: role and master index /// @@ -270,14 +249,15 @@ impl SmcTopology { } } -/// Per-controller configuration +/// Per-controller input configuration. +/// +/// A chip select is present when its slot is `Some`. Geometry is not stored +/// here; it is supplied per chip when the handle is built (`cs0`/`cs1`). #[derive(Clone, Copy, Debug)] pub struct SmcConfig { - /// Which controller to configure - pub controller_id: SmcController, - /// Optional configuration for CS0 flash device + /// Configuration for the CS0 flash device, if present pub cs0: Option, - /// Optional configuration for CS1 flash device + /// Configuration for the CS1 flash device, if present pub cs1: Option, /// Enable DMA transfers pub dma_enabled: bool, @@ -286,3 +266,39 @@ pub struct SmcConfig { /// Controller topology (role and master index) pub topology: SmcTopology, } + +/// Compile-time description of a wired SMC controller: which hardware block it +/// is and how it is configured. +/// +/// A target implements this on a zero-sized marker type, one per wired +/// controller. Because both fields are `const`, everything derivable from them — +/// segment layout, decode-region size, memory-map bases, flash-type config bits — +/// is computed at compile time, and an invalid configuration becomes a build +/// error rather than a runtime `Err`. The controller carries this marker as a +/// type parameter, so its configuration is never laundered through a runtime +/// value. +pub trait SmcInstance { + /// Which hardware controller this marker describes. + const CONTROLLER: SmcController; + /// The controller's configuration (chip selects, clocks, topology, …). + const CONFIG: SmcConfig; + /// How CS0's geometry is resolved at init: [`Discover`] to read SFDP, or a + /// target-defined [`GeometrySource`] returning a fixed geometry. Only + /// consulted when `CONFIG.cs0` is `Some`. Defaults to [`Discover`]. + /// + /// To pin geometry instead of discovering it, define the struct up top and + /// name it in the type slot: + /// ```ignore + /// const CS0_GEOM: FlashGeometry = FlashGeometry { + /// capacity_bytes: 0x0400_0000, + /// page_size: 256, + /// sector_size: 4096, + /// block_size: 65536, + /// }; + /// type Cs0Geometry = Pinned; + /// ``` + type Cs0Geometry: GeometrySource = Discover; + /// How CS1's geometry is resolved at init. Only consulted when `CONFIG.cs1` + /// is `Some`. Defaults to [`Discover`]. + type Cs1Geometry: GeometrySource = Discover; +} diff --git a/target/ast10x0/tests/flash/BUILD.bazel b/target/ast10x0/tests/flash/BUILD.bazel new file mode 100644 index 000000000..1dc8f872c --- /dev/null +++ b/target/ast10x0/tests/flash/BUILD.bazel @@ -0,0 +1,138 @@ +# 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", "flash_system_image_test") + +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", + "//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", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + ], +) + +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", + "//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", + ], +) + +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", + "//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"], +) + +# QEMU variant: the runner seeds 1 MiB erased images on both FMC chip selects +# (if=mtd), emulating the EVB where CS0 boot flash and the external W25Q512 on +# CS1 are both present. The flash service binds CS1, so program/erase/read-back +# runs there under emulation; CS0 is attached but untouched. +# bazelisk test --config=virt_ast10x0 //target/ast10x0/tests/flash:flash_qemu_test +flash_system_image_test( + name = "flash_qemu_test", + cs0_image = "cs0.img", + cs1_image = "cs1.img", + flash_size = 64 * 1024 * 1024, + fmc_model = "w25q512jv", + 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", + 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..3921abe19 --- /dev/null +++ b/target/ast10x0/tests/flash/client_main.rs @@ -0,0 +1,156 @@ +// 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. +/// 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; + +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() != 0x0400_0000 { + fail("total size"); + } + if page.get() != SECTOR { + fail("page size"); + } + if bitmap != 1 << 12 { + 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() + { + 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(total.get() as u32), &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 + .program(FlashAddress::new(TEST_OFFSET), &backup) + .is_err() + { + 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"); + 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 new file mode 100644 index 000000000..aa9bbb2e4 --- /dev/null +++ b/target/ast10x0/tests/flash/server_main.rs @@ -0,0 +1,54 @@ +// 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 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) => { + 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..944e96679 --- /dev/null +++ b/target/ast10x0/tests/flash/system.json5 @@ -0,0 +1,98 @@ +// 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: [ + // 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", + 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, + }, + { + // FMC CS1 memory-mapped flash read window (64 MiB). + // With both CS0 and CS1 present the 256 MiB FMC + // aperture is split in half, relocating the served + // CS1 window to 0x88000000. + name: "fmc_cs1_window", + type: "device", + start_address: 0x88000000, + size_bytes: 0x04000000, + }, + ], + }, + ], + }, + { + 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..e7cfabc3a --- /dev/null +++ b/target/ast10x0/tests/flash/target.rs @@ -0,0 +1,49 @@ +// 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 ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; +use ast10x0_peripherals::scu::ScuRegisters; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target {} + +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 {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/smc/dma_irq/target.rs b/target/ast10x0/tests/smc/dma_irq/target.rs index 0f311f388..04472d6df 100644 --- a/target/ast10x0/tests/smc/dma_irq/target.rs +++ b/target/ast10x0/tests/smc/dma_irq/target.rs @@ -25,8 +25,8 @@ use arch_arm_cortex_m::Arch; use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; use ast10x0_peripherals::scu::ScuRegisters; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcInterrupt, SmcTopology, - UninitSmc, + Cs, FlashConfig, FmcUninit, SmcConfig, SmcController, SmcError, SmcInstance, SmcInterrupt, + SmcTopology, }; use codegen as _; use console_backend::console_backend_write_all; @@ -41,13 +41,21 @@ use target_debug::{dump_smc_read, dump_smc_register}; pub struct Target {} -const FLASH_CFG: FlashConfig = FlashConfig { - capacity_mb: 16, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, -}; +/// Compile-time FMC descriptor: single CS0 device at 50 MHz on the boot-SPI +/// interface, DMA-completion interrupts enabled, geometry discovered over SFDP +/// at init. +struct FmcInstance; + +impl SmcInstance for FmcInstance { + const CONTROLLER: SmcController = SmcController::Fmc; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 50 }), + cs1: None, + dma_enabled: true, + enable_interrupts: true, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; +} const DMA_FLASH_OFFSET: u32 = 0x500; const DMA_DRAM_ADDR: usize = 0x0004_1000; @@ -90,14 +98,12 @@ pub fn fmc_dma_irq_handler(_kernel: K) { ); } -fn poll_dma_irq_completion( - controller: &mut ast10x0_peripherals::smc::ReadySmc, -) -> Poll> { +fn poll_dma_irq_completion(cs: &mut Cs<'_>) -> Poll> { if !FMC_DMA_IRQ_FIRED.swap(false, Ordering::AcqRel) { return Poll::Pending; } - match controller.handle_dma_irq() { + match cs.handle_dma_irq() { Ok(SmcInterrupt::DmaComplete) => Poll::Ready(Ok(())), Ok(_) => Poll::Ready(Err(SmcError::HardwareError)), Err(err) => Poll::Ready(Err(err)), @@ -108,19 +114,7 @@ fn run_dma_read_irq_test() -> Result<(), SmcError> { let scu = unsafe { ScuRegisters::new_global_unlocked() }; scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); - let config = SmcConfig { - controller_id: SmcController::Fmc, - cs0: Some(FLASH_CFG), - cs1: None, - dma_enabled: true, - enable_interrupts: true, - topology: SmcTopology::BootSpi { master_idx: 0 }, - }; - - let uninit = unsafe { UninitSmc::new(config)? }; - let mut controller = uninit.init()?; - - controller.spi_nor_read_init(ChipSelect::Cs0)?; + let mut controller = unsafe { FmcUninit::::new()? }.init()?; if !controller.is_ready() || controller.controller_id() != SmcController::Fmc { return Err(SmcError::HardwareError); @@ -136,7 +130,10 @@ fn run_dma_read_irq_test() -> Result<(), SmcError> { pw_log::info!("SMC DMA IRQ: starting DMA read"); dump_smc_register(0x7E62_0000, 8); dump_smc_register(0x7E62_0080, 8); - controller.dma_read(ChipSelect::Cs0, DMA_FLASH_OFFSET, DMA_DRAM_ADDR, DMA_LEN)?; + { + let mut cs0 = controller.cs0()?; + cs0.dma_read(DMA_FLASH_OFFSET, DMA_DRAM_ADDR, DMA_LEN)?; + } pw_log::info!("after calling dma_read()"); dump_smc_register(0x7E62_0000, 8); dump_smc_register(0x7E62_0080, 8); @@ -144,36 +141,47 @@ fn run_dma_read_irq_test() -> Result<(), SmcError> { return Err(SmcError::HardwareError); } - for _ in 0..DMA_IRQ_TIMEOUT { - match poll_dma_irq_completion(&mut controller) { - Poll::Ready(result) => { - result?; - if !controller.is_ready() { - return Err(SmcError::HardwareError); + let mut completed = false; + { + let mut cs0 = controller.cs0()?; + for _ in 0..DMA_IRQ_TIMEOUT { + match poll_dma_irq_completion(&mut cs0) { + Poll::Ready(result) => { + result?; + completed = true; + break; } - pw_log::info!("SMC DMA IRQ: DMA read completed via IRQ"); - let dma_buf = unsafe { - core::slice::from_raw_parts(DMA_DRAM_ADDR as *const u8, DMA_LEN as usize) - }; - dump_smc_register(0x7E62_0000, 8); - dump_smc_register(0x7E62_0080, 8); - dump_smc_read(dma_buf, DMA_LEN); - return Ok(()); + Poll::Pending => core::hint::spin_loop(), } - Poll::Pending => core::hint::spin_loop(), + } + if !completed { + pw_log::info!("dma Timeout"); + pw_log::info!( + "FMC IRQ count={}", + FMC_DMA_IRQ_COUNT.load(Ordering::Acquire) as u32 + ); + pw_log::info!( + "FMC DMA status at timeout=0x{:08x}", + cs0.dma_status() as u32 + ); } } - pw_log::info!("dma Timeout"); - pw_log::info!( - "FMC IRQ count={}", - FMC_DMA_IRQ_COUNT.load(Ordering::Acquire) as u32 - ); - pw_log::info!( - "FMC DMA status at timeout=0x{:08x}", - controller.dma_status() as u32 - ); - dump_nvic_irq_state(FMC_IRQ); - Err(SmcError::Timeout) + + if !completed { + dump_nvic_irq_state(FMC_IRQ); + return Err(SmcError::Timeout); + } + + if !controller.is_ready() { + return Err(SmcError::HardwareError); + } + pw_log::info!("SMC DMA IRQ: DMA read completed via IRQ"); + let dma_buf = + unsafe { core::slice::from_raw_parts(DMA_DRAM_ADDR as *const u8, DMA_LEN as usize) }; + dump_smc_register(0x7E62_0000, 8); + dump_smc_register(0x7E62_0080, 8); + dump_smc_read(dma_buf, DMA_LEN); + Ok(()) } codegen::declare_kernel_interrupt_handlers!(); diff --git a/target/ast10x0/tests/smc/read/BUILD.bazel b/target/ast10x0/tests/smc/read/BUILD.bazel index d437777e5..21739fefd 100644 --- a/target/ast10x0/tests/smc/read/BUILD.bazel +++ b/target/ast10x0/tests/smc/read/BUILD.bazel @@ -6,7 +6,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", @@ -185,3 +185,21 @@ rust_binary_no_panics_test( binary = ":smc_read_test", tags = ["kernel"], ) + +# QEMU variant: default 1 MiB w25q80bl model controller-wide, with both chip +# selects backed by distinctly-filled images (CS0 = 0x11, CS1 = 0x22) so a +# read/DMA dump reveals which CS the firmware decoded. The current test pins +# cs0: None, collapsing CS1 to 0x8000_0000, so it reads out 0x22 (CS1). +# bazelisk run --config=virt_ast10x0 //target/ast10x0/tests/smc/read:smc_read_qemu_test +flash_system_image_test( + name = "smc_read_qemu_test", + cs0_fill = 0x11, + cs0_image = "cs0.img", + cs1_fill = 0x22, + cs1_image = "cs1.img", + flash_size = 1 * 1024 * 1024, + image = ":smc_read_test", + tags = ["qemu_only"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) diff --git a/target/ast10x0/tests/smc/read/target.rs b/target/ast10x0/tests/smc/read/target.rs index 5bf0507f2..fbc0e9758 100644 --- a/target/ast10x0/tests/smc/read/target.rs +++ b/target/ast10x0/tests/smc/read/target.rs @@ -23,7 +23,8 @@ use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; use ast10x0_peripherals::scu::ScuRegisters; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, UninitSmc, + FlashConfig, FmcUninit, SmcConfig, SmcController, SmcError, SmcInstance, SmcTopology, + TransferMode, }; use console_backend::console_backend_write_all; use target_common::{declare_target, TargetInterface}; @@ -33,8 +34,29 @@ use {console_backend as _, entry as _}; mod target_debug; use target_debug::{dump_smc_read, dump_smc_register}; +/// Compile-time FMC descriptor: CS0 and CS1 driven on the EVB at 50 MHz, +/// geometry discovered over SFDP at init. +struct FmcInstance; + +impl SmcInstance for FmcInstance { + const CONTROLLER: SmcController = SmcController::Fmc; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 50 }), + cs1: Some(FlashConfig { spi_clock_mhz: 50 }), + dma_enabled: true, + enable_interrupts: false, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; +} + pub struct Target {} +/// DMA destination buffer. The FMC DMA engine (an AHB bus master) writes here, +/// so it must live in non-cached SRAM the engine and CPU observe coherently. +/// `.ram_nc` (0xA0000) is above both code and the kernel RAM region. +#[unsafe(link_section = ".ram_nc")] +static mut SMC_DMA_BUF: [u8; 256] = [0u8; 256]; + #[allow(dead_code)] fn run_smc_read_test() -> Result<(), SmcError> { // --- 1. Init --- @@ -42,45 +64,34 @@ fn run_smc_read_test() -> Result<(), SmcError> { let scu = unsafe { ScuRegisters::new_global_unlocked() }; scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); - let config = SmcConfig { - controller_id: SmcController::Fmc, - - cs0: Some(FlashConfig { - capacity_mb: 8, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, - }), - cs1: Some(FlashConfig { - capacity_mb: 64, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, - }), - dma_enabled: true, - enable_interrupts: false, - topology: SmcTopology::BootSpi { master_idx: 0 }, - }; pw_log::info!("=== AST10x0 smc read test ==="); - let controller = unsafe { UninitSmc::new(config)? }; - let mut controller = controller.init()?; - - let _ = match controller.spi_nor_read_init(ChipSelect::Cs0) { - Ok(v) => v, - Err(e) => { - pw_log::info!("Error:: spi_nor_read_init cs0"); - return Err(e); - } - }; - let _ = match controller.spi_nor_read_init(ChipSelect::Cs1) { - Ok(v) => v, - Err(e) => { - pw_log::info!("Error:: spi_nor_read_init cs1"); - return Err(e); - } - }; + let mut controller = unsafe { FmcUninit::::new()? }.init()?; + + let mut id = [0u8; 3]; + controller + .cs1()? + .transceive_user(&[0x9F], &[], &mut id, TransferMode::Mode111)?; + pw_log::info!( + "RDID cs1: mfr=0x{:02x} type=0x{:02x} cap=0x{:02x}", + id[0] as u32, + id[1] as u32, + id[2] as u32 + ); + + let mut sfdp = [0u8; 256]; + controller + .cs1()? + .transceive_user(&[0x5A], &[0, 0, 0, 0], &mut sfdp, TransferMode::Mode111)?; + dump_smc_read(&sfdp, 64); + + let g = controller.cs1()?.geometry(); + pw_log::info!( + "geom cs1: cap=0x{:08x} page={} sector={} block={}", + g.capacity_bytes as u32, + g.page_size as u32, + g.sector_size as u32, + g.block_size as u32 + ); pw_log::info!("=== Dump 0x7E62_0000 ==="); dump_smc_register(0x7E62_0000, 16); @@ -92,17 +103,9 @@ fn run_smc_read_test() -> Result<(), SmcError> { // --- 2. MMIO read — success path --- // Confirm the call succeeds and returns the correct byte count. Flash // content is not inspected so this is safe on both QEMU and silicon. - // TODO: need to add test CS1 - pw_log::info!("=== read test cs0==="); - let mut buf = [0u8; 64]; - let n = controller.read(ChipSelect::Cs0, 0x400, &mut buf)?; - if n != 64 { - return Err(SmcError::HardwareError); - } - dump_smc_read(&buf, 64); - pw_log::info!("=== read test cs1==="); - let n = controller.read(ChipSelect::Cs1, 0x400, &mut buf)?; + let mut buf = [0u8; 64]; + let n = controller.cs1()?.read(0x400, &mut buf)?; if n != 64 { return Err(SmcError::HardwareError); } @@ -110,23 +113,33 @@ fn run_smc_read_test() -> Result<(), SmcError> { pw_log::info!("=== read dma test==="); // --- 4. DMA --- - let tempbuf = unsafe { core::slice::from_raw_parts(0x41500 as *mut u8, 256) }; - - let _ = match controller.dma_read(ChipSelect::Cs0, 0x400, 0x41500 as usize, 256) { - Err(SmcError::InvalidCapacity) => Ok(()), - Err(other) => Err(other), - Ok(()) => Err(SmcError::HardwareError), - }; + // SAFETY: SMC_DMA_BUF is a non-cached SRAM static uniquely owned here for + // the duration of the DMA; the engine and CPU observe it coherently. + let dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SMC_DMA_BUF) }; + + // Poison the destination: an all-0xff readback then proves the DMA wrote it. + dma_buf.fill(0xAA); + pw_log::info!("=== dma dest preseed (expect AA) ==="); + dump_smc_read(dma_buf, 256); + + { + let mut cs1 = controller.cs1()?; + let _ = match cs1.dma_read(0x400, dma_buf.as_ptr() as usize, 256) { + Err(SmcError::InvalidCapacity) => Ok(()), + Err(other) => Err(other), + Ok(()) => Err(SmcError::HardwareError), + }; - loop { - match controller.poll_dma_completion() { - core::task::Poll::Pending => { - // still running - } - core::task::Poll::Ready(result) => { - result?; - pw_log::info!("dma completion is ready"); - break; + loop { + match cs1.poll_dma_completion() { + core::task::Poll::Pending => { + // still running + } + core::task::Poll::Ready(result) => { + result?; + pw_log::info!("dma completion is ready"); + break; + } } } } @@ -134,7 +147,7 @@ fn run_smc_read_test() -> Result<(), SmcError> { pw_log::info!("=== dma done= =="); dump_smc_register(0x7E62_0000, 8); dump_smc_register(0x7E62_0080, 8); - dump_smc_read(tempbuf, 256); + dump_smc_read(dma_buf, 256); Ok(()) } diff --git a/target/ast10x0/tests/smc/read/target_spi1.rs b/target/ast10x0/tests/smc/read/target_spi1.rs index 04d23f7e2..12d1779bb 100644 --- a/target/ast10x0/tests/smc/read/target_spi1.rs +++ b/target/ast10x0/tests/smc/read/target_spi1.rs @@ -11,7 +11,7 @@ use ast10x0_peripherals::scu::{ ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource, }; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiNorFlash, + FlashConfig, SmcConfig, SmcController, SmcError, SmcInstance, SmcTopology, SpiNorFlash, SpiNorFlashDevice, SpiTransaction, SpiUninit, }; use console_backend::console_backend_write_all; @@ -22,13 +22,20 @@ use {console_backend as _, entry as _}; mod target_debug; use target_debug::{dump_smc_read, dump_smc_register}; -const SPI_FLASH_CONFIG: FlashConfig = FlashConfig { - capacity_mb: 32, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, -}; +/// Compile-time SPI1 descriptor: single CS0 device at 50 MHz on the host-SPI +/// interface, geometry discovered over SFDP at init. +struct Spi1Instance; + +impl SmcInstance for Spi1Instance { + const CONTROLLER: SmcController = SmcController::Spi1; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 50 }), + cs1: None, + dma_enabled: true, + enable_interrupts: false, + topology: SmcTopology::HostSpi { master_idx: 0 }, + }; +} pub struct Target {} @@ -49,26 +56,15 @@ fn config_spi1_master_controller() -> Result<(), SmcError> { fn run_spi1_read_test() -> Result<(), SmcError> { config_spi1_master_controller()?; - let config = SmcConfig { - controller_id: SmcController::Spi1, - cs0: Some(SPI_FLASH_CONFIG), - cs1: None, - dma_enabled: true, - enable_interrupts: false, - topology: SmcTopology::HostSpi { master_idx: 0 }, - }; - pw_log::info!("=== AST10x0 SMC SPI1 read test ==="); - let spi = unsafe { SpiUninit::new(SmcController::Spi1, config)? }; - let mut spi = spi.init()?; - spi.spi_nor_read_init(ChipSelect::Cs0)?; + let mut spi = unsafe { SpiUninit::::new()? }.init()?; if !spi.is_ready() { return Err(SmcError::HardwareError); } let jedec = { - let flash = SpiNorFlash::from_spi_cs(&mut spi, SPI_FLASH_CONFIG, ChipSelect::Cs0)?; + let flash = SpiNorFlash::new(spi.cs0()?)?; flash.jedec_id()? }; pw_log::info!( @@ -88,13 +84,7 @@ fn run_spi1_read_test() -> Result<(), SmcError> { pw_log::info!("=== SPI1 read ==="); let mut buf = [0u8; 64]; - let n = SpiTransaction::read_with_spim( - &mut spi, - SpiMonitorInstance::Spim0, - ChipSelect::Cs0, - 0x0, - &mut buf, - )?; + let n = SpiTransaction::read_with_spim(spi.cs0()?, SpiMonitorInstance::Spim0, 0x0, &mut buf)?; if n != buf.len() { return Err(SmcError::HardwareError); } @@ -103,9 +93,8 @@ fn run_spi1_read_test() -> Result<(), SmcError> { pw_log::info!("=== SPI1 DMA read @ 0x00000000 ==="); let dma_buf = unsafe { core::slice::from_raw_parts_mut(0x41500 as *mut u8, 256) }; let mut dma_txn = SpiTransaction::dma_read_with_spim( - &mut spi, + spi.cs0()?, SpiMonitorInstance::Spim0, - ChipSelect::Cs0, 0x0, 0x41500usize, dma_buf.len() as u32, diff --git a/target/ast10x0/tests/smc/read/target_spi2.rs b/target/ast10x0/tests/smc/read/target_spi2.rs index e3407062b..daeef0d6f 100644 --- a/target/ast10x0/tests/smc/read/target_spi2.rs +++ b/target/ast10x0/tests/smc/read/target_spi2.rs @@ -12,7 +12,7 @@ use ast10x0_peripherals::scu::{ ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource, }; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiTransaction, + FlashConfig, SmcConfig, SmcController, SmcError, SmcInstance, SmcTopology, SpiTransaction, SpiUninit, TransferMode, }; use console_backend::console_backend_write_all; @@ -23,13 +23,20 @@ use {console_backend as _, entry as _}; mod target_debug; use target_debug::{dump_smc_read, dump_smc_register}; -const SPI_FLASH_CONFIG: FlashConfig = FlashConfig { - capacity_mb: 32, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 25, -}; +/// Compile-time SPI2 descriptor: single CS0 device at 25 MHz on the normal-SPI +/// interface (master_idx 2), geometry discovered over SFDP at init. +struct Spi2Instance; + +impl SmcInstance for Spi2Instance { + const CONTROLLER: SmcController = SmcController::Spi2; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 25 }), + cs1: None, + dma_enabled: true, + enable_interrupts: false, + topology: SmcTopology::NormalSpi { master_idx: 2 }, + }; +} pub struct Target {} @@ -59,19 +66,8 @@ fn config_spi2_master_controller() -> Result<(), SmcError> { fn run_spi2_read_test() -> Result<(), SmcError> { config_spi2_master_controller()?; - let config = SmcConfig { - controller_id: SmcController::Spi2, - cs0: Some(SPI_FLASH_CONFIG), - cs1: None, - dma_enabled: true, - enable_interrupts: false, - topology: SmcTopology::NormalSpi { master_idx: 2 }, - }; - pw_log::info!("=== AST10x0 SMC SPI2 read test ==="); - let spi = unsafe { SpiUninit::new(SmcController::Spi2, config)? }; - let mut spi = spi.init()?; - spi.spi_nor_read_init(ChipSelect::Cs0)?; + let mut spi = unsafe { SpiUninit::::new()? }.init()?; if !spi.is_ready() { return Err(SmcError::HardwareError); @@ -88,9 +84,8 @@ fn run_spi2_read_test() -> Result<(), SmcError> { dump_smc_register(0x7E78_0070, 2); let mut jedec = [0u8; 3]; SpiTransaction::transceive_user_with_spim( - &mut spi, + spi.cs0()?, SpiMonitorInstance::Spim2, - ChipSelect::Cs0, &[0x9f], &[], &mut jedec, @@ -110,13 +105,7 @@ fn run_spi2_read_test() -> Result<(), SmcError> { pw_log::info!("=== SPI2 read ==="); let mut buf = [0u8; 64]; - let n = SpiTransaction::read_with_spim( - &mut spi, - SpiMonitorInstance::Spim2, - ChipSelect::Cs0, - 0x0, - &mut buf, - )?; + let n = SpiTransaction::read_with_spim(spi.cs0()?, SpiMonitorInstance::Spim2, 0x0, &mut buf)?; if n != buf.len() { return Err(SmcError::HardwareError); } @@ -125,9 +114,8 @@ fn run_spi2_read_test() -> Result<(), SmcError> { pw_log::info!("=== SPI2 DMA read @ 0x00000000 ==="); let dma_buf = unsafe { core::slice::from_raw_parts_mut(0x41500 as *mut u8, 256) }; let mut dma_txn = SpiTransaction::dma_read_with_spim( - &mut spi, + spi.cs0()?, SpiMonitorInstance::Spim2, - ChipSelect::Cs0, 0x0, 0x41500usize, dma_buf.len() as u32, diff --git a/target/ast10x0/tests/smc/sfdp/BUILD.bazel b/target/ast10x0/tests/smc/sfdp/BUILD.bazel new file mode 100644 index 000000000..463744d3f --- /dev/null +++ b/target/ast10x0/tests/smc/sfdp/BUILD.bazel @@ -0,0 +1,78 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +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"], +) + +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", + "//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", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + +system_image( + name = "smc_sfdp_test", + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + userspace = False, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "smc_sfdp_evb_test", + image = ":smc_sfdp_test", + 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 = ":smc_sfdp_test", + tags = ["kernel"], +) diff --git a/target/ast10x0/tests/smc/sfdp/system.json5 b/target/ast10x0/tests/smc/sfdp/system.json5 new file mode 100644 index 000000000..5eb8441cf --- /dev/null +++ b/target/ast10x0/tests/smc/sfdp/system.json5 @@ -0,0 +1,17 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 kernel-only SMC FMC CS1 SFDP capacity-discovery test configuration. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 262144, + ram_start_address: 0x00040500, + ram_size_bytes: 391936, // ends at RAM_NC boundary (0x000A0000) + }, +} diff --git a/target/ast10x0/tests/smc/sfdp/target.rs b/target/ast10x0/tests/smc/sfdp/target.rs new file mode 100644 index 000000000..4a6a6112d --- /dev/null +++ b/target/ast10x0/tests/smc/sfdp/target.rs @@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 SMC FMC CS1 SFDP config-discovery test target. +//! +//! Drives `FmcUninit::new(config).init()`: `init()` discovers both driven chip +//! selects from SFDP and finalizes a controller whose per-CS geometry is +//! *derived* from each chip — no capacity is fed in, only `spi_clock_mhz`. +//! Asserts the discovered CS1 capacity (EVB's W25Q512) is 64 MiB. Read-only: +//! issues only READ_SFDP / READ_ID; nothing is erased or programmed. + +#![no_std] +#![no_main] + +#[allow(unused_imports)] +use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; +use ast10x0_peripherals::scu::ScuRegisters; +use ast10x0_peripherals::smc::{ + FlashConfig, FmcUninit, SmcConfig, SmcController, SmcError, SmcInstance, SmcTopology, + SpiNorFlash, SpiNorFlashDevice, +}; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +/// The one geometry field SFDP does not encode; everything else is discovered. +const SPI_CLOCK_MHZ: u32 = 50; + +/// Expected capacity of the EVB's W25Q512 on CS1. +const CS1_EXPECTED_CAPACITY_MB: u32 = 64; + +/// Compile-time FMC descriptor: both CS driven on the EVB at 50 MHz. Capacity +/// for each is discovered by `init()`, not fed in here. +struct FmcInstance; + +impl SmcInstance for FmcInstance { + const CONTROLLER: SmcController = SmcController::Fmc; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { + spi_clock_mhz: SPI_CLOCK_MHZ, + }), + cs1: Some(FlashConfig { + spi_clock_mhz: SPI_CLOCK_MHZ, + }), + dma_enabled: false, + enable_interrupts: false, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; +} + +pub struct Target {} + +fn run_smc_fmc_cs1_sfdp_test() -> Result<(), SmcError> { + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + scu.apply_pinctrl_group(PINCTRL_FMC_QUAD); + + pw_log::info!("=== AST10x0 SMC FMC CS1 SFDP config test ==="); + // Both CS are driven on the EVB; capacity for each is an OUTPUT of init(). + let mut fmc = unsafe { FmcUninit::::new()? }.init()?; + + if !fmc.is_ready() { + return Err(SmcError::HardwareError); + } + + // CS1 geometry was discovered over SFDP during `init()`; read it back. + let g = { + let cs1 = fmc.cs1()?; + cs1.geometry() + }; + pw_log::info!( + "derived CS1 geom: cap=0x{:08x} page={} sector={} block={}", + g.capacity_bytes as u32, + g.page_size as u32, + g.sector_size as u32, + g.block_size as u32 + ); + + // JEDEC ID for the record (W25Q512 = EF 40 20). Read-only. + let jedec = { + let flash = SpiNorFlash::new(fmc.cs1()?)?; + flash.jedec_id()? + }; + pw_log::info!( + "CS1 JEDEC ID: {:02x} {:02x} {:02x}", + jedec[0] as u32, + jedec[1] as u32, + jedec[2] as u32 + ); + + let cap_mb = (g.capacity_bytes / (1024 * 1024)) as u32; + if cap_mb == CS1_EXPECTED_CAPACITY_MB { + pw_log::info!("SFDP-derived CS1 capacity MATCHES 64 MiB"); + Ok(()) + } else { + pw_log::info!("SFDP-derived CS1 capacity DIFFERS from 64 MiB"); + Err(SmcError::DeviceNotSupported) + } +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 SMC FMC CS1 SFDP Test"; + + fn main() -> ! { + let sentinel = if run_smc_fmc_cs1_sfdp_test().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/smc/write/target.rs b/target/ast10x0/tests/smc/write/target.rs index cee181ff6..6578a964e 100644 --- a/target/ast10x0/tests/smc/write/target.rs +++ b/target/ast10x0/tests/smc/write/target.rs @@ -10,7 +10,7 @@ use ast10x0_peripherals::scu::pinctrl::PINCTRL_FMC_QUAD; use ast10x0_peripherals::scu::ScuRegisters; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, FmcUninit, SmcConfig, SmcController, SmcError, SmcTopology, + FlashConfig, FmcUninit, SmcConfig, SmcController, SmcError, SmcInstance, SmcTopology, SpiNorFlash, SpiNorFlashDevice, }; use console_backend::console_backend_write_all; @@ -21,21 +21,20 @@ use {console_backend as _, entry as _}; mod target_debug; use target_debug::{dump_smc_read, dump_smc_register}; -const CS0_CONFIG: FlashConfig = FlashConfig { - capacity_mb: 8, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, -}; +/// Compile-time FMC descriptor: both CS driven on the EVB at 50 MHz, geometry +/// discovered over SFDP at init. +struct FmcInstance; -const CS1_CONFIG: FlashConfig = FlashConfig { - capacity_mb: 64, - page_size: 256, - sector_size: 4096, - block_size: 65536, - spi_clock_mhz: 50, -}; +impl SmcInstance for FmcInstance { + const CONTROLLER: SmcController = SmcController::Fmc; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(FlashConfig { spi_clock_mhz: 50 }), + cs1: Some(FlashConfig { spi_clock_mhz: 50 }), + dma_enabled: true, + enable_interrupts: false, + topology: SmcTopology::BootSpi { master_idx: 0 }, + }; +} const TEST_OFFSET: u32 = 0x10_0000; const TEST_LEN: usize = 256; @@ -80,26 +79,15 @@ fn run_smc_fmc_cs1_write_test() -> Result<(), SmcError> { 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: Some(CS1_CONFIG), - dma_enabled: true, - enable_interrupts: false, - topology: SmcTopology::BootSpi { master_idx: 0 }, - }; - pw_log::info!("=== AST10x0 SMC FMC CS1 write test ==="); - let fmc = unsafe { FmcUninit::new(config)? }; - let mut fmc = fmc.init()?; - fmc.spi_nor_read_init(ChipSelect::Cs1)?; + let mut fmc = unsafe { FmcUninit::::new()? }.init()?; if !fmc.is_ready() { return Err(SmcError::HardwareError); } let jedec = { - let flash = SpiNorFlash::from_fmc_cs(&mut fmc, CS1_CONFIG, ChipSelect::Cs1)?; + let flash = SpiNorFlash::new(fmc.cs1()?)?; flash.jedec_id()? }; pw_log::info!( @@ -111,23 +99,21 @@ fn run_smc_fmc_cs1_write_test() -> Result<(), SmcError> { pw_log::info!("=== backup CS1 sector ==="); let original = unsafe { core::slice::from_raw_parts_mut(0x41000 as *mut u8, TEST_SECTOR_LEN) }; - fmc.dma_read( - ChipSelect::Cs1, - TEST_OFFSET, - 0x41000usize, - TEST_SECTOR_LEN as u32, - )?; - loop { - match fmc.poll_dma_completion() { - core::task::Poll::Pending => {} - core::task::Poll::Ready(result) => { - result?; - break; + { + let mut cs1 = fmc.cs1()?; + cs1.dma_read(TEST_OFFSET, 0x41000usize, TEST_SECTOR_LEN as u32)?; + loop { + match cs1.poll_dma_completion() { + core::task::Poll::Pending => {} + core::task::Poll::Ready(result) => { + result?; + break; + } } } } - let mut flash = SpiNorFlash::from_fmc_cs(&mut fmc, CS1_CONFIG, ChipSelect::Cs1)?; + let mut flash = SpiNorFlash::new(fmc.cs1()?)?; pw_log::info!("=== erase CS1 sector ==="); flash.erase_sector(TEST_OFFSET)?; diff --git a/target/ast10x0/tests/spimonitor/setup_all_spim.rs b/target/ast10x0/tests/spimonitor/setup_all_spim.rs index d1ae10ea6..0ac4b1eae 100644 --- a/target/ast10x0/tests/spimonitor/setup_all_spim.rs +++ b/target/ast10x0/tests/spimonitor/setup_all_spim.rs @@ -19,8 +19,8 @@ use ast10x0_peripherals::scu::{ pinctrl::PINCTRL_SPI1_QUAD, ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorSource, }; use ast10x0_peripherals::smc::{ - ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiReady, SpiUninit, - TransferMode, + ChipSelect, FlashConfig, FlashGeometry, Pinned, SmcConfig, SmcController, SmcError, + SmcInstance, SmcTopology, SpiReady, SpiUninit, TransferMode, }; use ast10x0_peripherals::spimonitor::registers::SpiMonitorRegisters; use ast10x0_peripherals::spimonitor::{ @@ -73,13 +73,30 @@ const BMC_CSIN_RECOVERY_TIMEOUT_US: u32 = 500_000; const BMC_RECOVERY_RETRY_DELAY_US: u32 = 1_000_000; const BMC_CSIN_MASK: u32 = (1 << 0) | (1 << 14); const ENABLE_RUNTIME_DEBUG_LOGS: bool = false; -const BMC_FLASH_CONFIG: FlashConfig = FlashConfig { - capacity_mb: 128, +const BMC_FLASH_CONFIG: FlashConfig = FlashConfig { spi_clock_mhz: 25 }; +const BMC_FLASH_GEOMETRY: FlashGeometry = FlashGeometry { + capacity_bytes: 0x0400_0000, page_size: 256, sector_size: 4096, block_size: 65536, - spi_clock_mhz: 25, }; + +/// Compile-time SPI1 descriptor for the two BMC flashes: both CS at 25 MHz, +/// geometry pinned so the pre-reset probe never issues an SFDP read. +struct Spi1Instance; + +impl SmcInstance for Spi1Instance { + const CONTROLLER: SmcController = SmcController::Spi1; + const CONFIG: SmcConfig = SmcConfig { + cs0: Some(BMC_FLASH_CONFIG), + cs1: Some(BMC_FLASH_CONFIG), + dma_enabled: false, + enable_interrupts: false, + topology: SmcTopology::HostSpi { master_idx: 0 }, + }; + type Cs0Geometry = Pinned; + type Cs1Geometry = Pinned; +} const ALLOW_COMMANDS: [u8; 32] = [ 0x03, 0x13, 0x0b, 0x0c, 0x6b, 0x6c, 0x01, 0x05, 0x35, 0x06, 0x04, 0x20, 0x21, 0x9f, 0x5a, 0xb7, 0xe9, 0x32, 0x34, 0xd8, 0xdc, 0x02, 0x12, 0x3b, 0x3c, 0x70, 0xbb, 0xbc, 0x50, 0xeb, 0xec, 0xc2, @@ -364,7 +381,7 @@ fn monitor_spim_violations( fn reset_one_bmc_flash( scu: &ScuRegisters, - spi: &SpiReady, + spi: &mut SpiReady, monitor: SpiMonitorInstance, chip_select: ChipSelect, ) -> Result<[u8; 3], SmcError> { @@ -373,14 +390,18 @@ fn reset_one_bmc_flash( let proprietary_state = scu.spim_proprietary_pre_config(); let result = (|| { - spi.transceive_user(chip_select, &[0x66], &[], &mut [], TransferMode::Mode111)?; + let cs = match chip_select { + ChipSelect::Cs0 => spi.cs0()?, + ChipSelect::Cs1 => spi.cs1()?, + }; + cs.transceive_user(&[0x66], &[], &mut [], TransferMode::Mode111)?; delay_us(10_000); - spi.transceive_user(chip_select, &[0x99], &[], &mut [], TransferMode::Mode111)?; + cs.transceive_user(&[0x99], &[], &mut [], TransferMode::Mode111)?; delay_us(50_000); - spi.transceive_user(chip_select, &[0xe9], &[], &mut [], TransferMode::Mode111)?; + cs.transceive_user(&[0xe9], &[], &mut [], TransferMode::Mode111)?; let mut jedec = [0u8; 3]; - spi.transceive_user(chip_select, &[0x9f], &[], &mut jedec, TransferMode::Mode111)?; + cs.transceive_user(&[0x9f], &[], &mut jedec, TransferMode::Mode111)?; Ok(jedec) })(); @@ -393,21 +414,13 @@ fn reset_one_bmc_flash( fn reset_bmc_flashes(scu: &ScuRegisters, log_jedec: bool) -> Result<(), SmcError> { scu.apply_pinctrl_group(PINCTRL_SPI1_QUAD); - let config = SmcConfig { - controller_id: SmcController::Spi1, - cs0: Some(BMC_FLASH_CONFIG), - cs1: Some(BMC_FLASH_CONFIG), - dma_enabled: false, - enable_interrupts: false, - topology: SmcTopology::HostSpi { master_idx: 0 }, - }; - let spi = unsafe { SpiUninit::new(SmcController::Spi1, config)? }.init()?; + let mut spi = unsafe { SpiUninit::::new()? }.init()?; for (index, monitor, chip_select) in [ (0u32, SpiMonitorInstance::Spim0, ChipSelect::Cs0), (1u32, SpiMonitorInstance::Spim1, ChipSelect::Cs1), ] { - let jedec = reset_one_bmc_flash(scu, &spi, monitor, chip_select)?; + let jedec = reset_one_bmc_flash(scu, &mut spi, monitor, chip_select)?; if log_jedec { pw_log::info!( "spi1@{} reset complete, JEDEC ID: {:02x} {:02x} {:02x}", diff --git a/third_party/crates_io/Cargo.lock b/third_party/crates_io/Cargo.lock index 92d486fdd..0e0e1e6c4 100644 --- a/third_party/crates_io/Cargo.lock +++ b/third_party/crates_io/Cargo.lock @@ -534,9 +534,9 @@ dependencies = [ [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fugit" @@ -684,19 +684,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "heapless" version = "0.8.0" @@ -751,7 +745,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown", ] [[package]] @@ -945,13 +939,13 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" dependencies = [ "crc32fast", "flate2", - "hashbrown 0.15.5", + "hashbrown", "indexmap", "memchr", "ruzstd", @@ -1403,9 +1397,9 @@ dependencies = [ [[package]] name = "ruzstd" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888" dependencies = [ "twox-hash", ] diff --git a/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index c2c679594..6e48c4c44 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml @@ -87,7 +87,7 @@ clap = { version = "4.5.40", features = ["derive", "env", "wrap_help"] } futures = "0.3" hex = "0.4.3" minijinja = { version = "2.12.0", features = ["loader"] } -object = { version = "0.37.1", features = ["build", "elf", "read", "write"] } +object = { version = "0.40.0", features = ["build", "elf", "read", "write"] } prost = "0.13" rustc-demangle = "0.1.25" serde_json5 = "0.2.1" 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); 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 { 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) + } +} diff --git a/util/sfdp/BUILD.bazel b/util/sfdp/BUILD.bazel new file mode 100644 index 000000000..9fb316773 --- /dev/null +++ b/util/sfdp/BUILD.bazel @@ -0,0 +1,22 @@ +# 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 = "sfdp", + srcs = [ + "lib.rs", + ], + crate_name = "util_sfdp", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + ], +) + +rust_test( + name = "sfdp_test", + crate = ":sfdp", +) diff --git a/util/sfdp/lib.rs b/util/sfdp/lib.rs new file mode 100644 index 000000000..cef9403f4 --- /dev/null +++ b/util/sfdp/lib.rs @@ -0,0 +1,501 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Target-agnostic JEDEC JESD216 (SFDP) decoder. +//! +//! Pure byte logic: callers perform the SPI reads (opcode `0x5A`) and hand the +//! bytes here. Transcribed from opentitanlib's `spiflash/sfdp.rs`, cross-checked +//! against Zephyr's `jesd216_bfp_*` helpers, with `no_std` byte reads and +//! `util_error` codes in place of `std::io`/`thiserror`. + +#![no_std] +#![feature(adt_const_params)] + +use core::marker::ConstParamTy; + +use util_error::{ + ErrorCode, FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY, FLASH_GENERIC_SFDP_INVALID_SIGNATURE, + FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND, FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT, + FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV, + FLASH_GENERIC_SFDP_UNSUPPORTED_PARAMS_MAJOR_REV, +}; + +/// SFDP signature: ASCII "SFDP" read little-endian from the first dword. +pub const SFDP_SIGNATURE: u32 = 0x5044_4653; +/// Length in bytes of the SFDP header. +pub const SFDP_HEADER_LEN: usize = 8; +/// Length in bytes of one parameter header. +pub const PARAM_HEADER_LEN: usize = 8; +/// The only header/parameter major revision this decoder understands. +pub const SUPPORTED_MAJOR_REV: u8 = 1; +/// Parameter-header ID (LSB/MSB) of the mandatory JEDEC Basic Flash table. +pub const BFP_ID_LSB: u8 = 0x00; +pub const BFP_ID_MSB: u8 = 0xFF; +/// Maximum number of erase types a BFP table defines (DW8/DW9). +pub const NUM_ERASE_TYPES: usize = 4; + +/// Read a little-endian u32 from a 4-byte window at `off`, or `None` if the +/// window runs past the end of `bytes`. +fn le_u32(bytes: &[u8], off: usize) -> Option { + let end = off.checked_add(4)?; + let arr: [u8; 4] = bytes.get(off..end)?.try_into().ok()?; + Some(u32::from_le_bytes(arr)) +} + +/// Extract a `size`-bit field at `offset` from `word` (size < 32). +const fn field(word: u32, offset: u32, size: u32) -> u32 { + let mask = if size >= 32 { + u32::MAX + } else { + (1u32 << size) - 1 + }; + word.wrapping_shr(offset) & mask +} + +/// The 8-byte SFDP header: signature, revision, and parameter-header count. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SfdpHeader { + pub signature: u32, + pub minor: u8, + pub major: u8, + /// Number of parameter headers minus one (as stored on the wire). + pub nph: u8, +} + +impl SfdpHeader { + /// Parse the SFDP header from the first [`SFDP_HEADER_LEN`] bytes. + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < SFDP_HEADER_LEN { + return Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT); + } + let signature = le_u32(bytes, 0).ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + if signature != SFDP_SIGNATURE { + return Err(FLASH_GENERIC_SFDP_INVALID_SIGNATURE); + } + let major = bytes[5]; + if major != SUPPORTED_MAJOR_REV { + return Err(FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV); + } + Ok(Self { + signature, + minor: bytes[4], + major, + nph: bytes[6], + }) + } + + /// Number of parameter headers that follow the SFDP header. + pub fn num_param_headers(&self) -> usize { + self.nph as usize + 1 + } +} + +/// One 8-byte parameter header: identifies a table and points at its dwords. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ParameterHeader { + pub id_lsb: u8, + pub minor: u8, + pub major: u8, + /// Table length in 32-bit dwords. + pub dwords: u8, + /// 24-bit byte offset of the table within SFDP space. + pub pointer: u32, + pub id_msb: u8, +} + +impl ParameterHeader { + /// Parse one parameter header from an 8-byte window. + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < PARAM_HEADER_LEN { + return Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT); + } + let major = bytes[2]; + if major != SUPPORTED_MAJOR_REV { + return Err(FLASH_GENERIC_SFDP_UNSUPPORTED_PARAMS_MAJOR_REV); + } + let pointer = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], 0]); + Ok(Self { + id_lsb: bytes[0], + minor: bytes[1], + major, + dwords: bytes[3], + pointer, + id_msb: bytes[7], + }) + } + + /// True for the mandatory JEDEC Basic Flash Parameter table. + pub fn is_basic_flash(&self) -> bool { + self.id_lsb == BFP_ID_LSB && self.id_msb == BFP_ID_MSB + } +} + +/// A single supported erase operation from BFP DW8/DW9. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EraseType { + /// Erase opcode. + pub opcode: u8, + /// Erased size in bytes (`2^exp`). + pub size: u32, +} + +/// Decoded Basic Flash Parameter table. +/// +/// Holds a borrow of the raw table bytes; accessors decode fields on demand. +#[derive(Clone, Copy, Debug)] +pub struct BasicFlashParams<'a> { + data: &'a [u8], + dwords: usize, +} + +impl<'a> BasicFlashParams<'a> { + /// Parse the BFP table. `data` must hold at least the density dword (DW2). + pub fn parse(data: &'a [u8]) -> Result { + let dwords = data.len() / 4; + if dwords < 2 { + return Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT); + } + Ok(Self { data, dwords }) + } + + /// Read the 1-based dword `idx` if present. + fn dword(&self, idx: usize) -> Option { + if idx == 0 || idx > self.dwords { + return None; + } + le_u32(self.data, (idx - 1) * 4) + } + + /// Device density in bits, decoded from DW2 (JESD216 rule). + pub fn density_bits(&self) -> Result { + let dw2 = self + .dword(2) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + if dw2 & (1 << 31) != 0 { + let exp = field(dw2, 0, 31); + 1u64.checked_shl(exp) + .ok_or(FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + } else { + Ok(1u64 + dw2 as u64) + } + } + + /// Device capacity in bytes (density / 8). + pub fn capacity_bytes(&self) -> Result { + Ok(self.density_bits()? / 8) + } + + /// Programmable page size in bytes from DW11; defaults to 256 pre-JESD216A. + pub fn page_size(&self) -> u32 { + match self.dword(11) { + Some(dw11) => 1u32.checked_shl(field(dw11, 4, 4)).unwrap_or(256), + None => 256, + } + } + + /// Decode erase type `idx` (1..=4) from DW8/DW9; `None` if unused/absent. + pub fn erase_type(&self, idx: usize) -> Option { + if idx == 0 || idx > NUM_ERASE_TYPES { + return None; + } + // Types 1,2 live in DW8; types 3,4 in DW9. Even indices are the upper half. + let dw = self.dword(8 + (idx - 1) / 2)?; + let half = if idx % 2 == 0 { dw >> 16 } else { dw }; + let exp = (half & 0xFF) as u8; + if exp == 0 { + return None; + } + let size = 1u32.checked_shl(exp as u32)?; + Some(EraseType { + opcode: ((half >> 8) & 0xFF) as u8, + size, + }) + } + + /// Smallest supported erase (the erasable "sector"), if any. + pub fn smallest_erase(&self) -> Option { + (1..=NUM_ERASE_TYPES) + .filter_map(|i| self.erase_type(i)) + .min_by_key(|e| e.size) + } + + /// Largest supported erase (the erasable "block"), if any. + pub fn largest_erase(&self) -> Option { + (1..=NUM_ERASE_TYPES) + .filter_map(|i| self.erase_type(i)) + .max_by_key(|e| e.size) + } +} + +/// Geometry a controller needs to configure a flash device, all derived from +/// SFDP. Fields SFDP does not encode (e.g. desired clock) are the caller's. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ConstParamTy)] +pub struct FlashGeometry { + pub capacity_bytes: u64, + pub page_size: u32, + pub sector_size: u32, + pub block_size: u32, +} + +/// Locate the JEDEC Basic Flash parameter header, given the already-read SFDP +/// `header` (>= 8 bytes) and the `param_headers` block (`num_param_headers` * 8 +/// bytes). The returned `pointer`/`dwords` tell the caller what BFP bytes to +/// read next; this is pure decode, no I/O. +pub fn find_bfp_header(header: &[u8], param_headers: &[u8]) -> Result { + let hdr = SfdpHeader::parse(header)?; + let count = hdr.num_param_headers(); + if param_headers.len() < count * PARAM_HEADER_LEN { + return Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT); + } + for i in 0..count { + let window = param_headers + .get(i * PARAM_HEADER_LEN..) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + let ph = ParameterHeader::parse(window)?; + if ph.is_basic_flash() { + return Ok(ph); + } + } + Err(FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND) +} + +/// Assemble [`FlashGeometry`] from a parsed BFP table. +pub fn geometry_from_bfp(bfp: &BasicFlashParams) -> Result { + let capacity_bytes = bfp.capacity_bytes()?; + let sector = bfp + .smallest_erase() + .ok_or(FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND)?; + let block = bfp + .largest_erase() + .ok_or(FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND)?; + Ok(FlashGeometry { + capacity_bytes, + page_size: bfp.page_size(), + sector_size: sector.size, + block_size: block.size, + }) +} + +/// Decode [`FlashGeometry`] from a raw SFDP image read from address 0. +/// +/// Walks the header, the parameter-header directory, and the Basic Flash +/// Parameter table by indexing into `image`, so the caller performs exactly one +/// SPI read (opcode `0x5A`) of a sufficiently large blob. Out-of-range pointers +/// and short tables return an error rather than panicking, so over-reading SFDP +/// space is safe. +pub fn decode_geometry(image: &[u8]) -> Result { + let header = image + .get(0..SFDP_HEADER_LEN) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + let params = image + .get(SFDP_HEADER_LEN..) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + let bfp = find_bfp_header(header, params)?; + let start = bfp.pointer as usize; + let end = start + .checked_add(bfp.dwords as usize * 4) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + let table = image + .get(start..end) + .ok_or(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT)?; + geometry_from_bfp(&BasicFlashParams::parse(table)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Each fixture below is a hand-built SFDP image whose every decoded field is + // justified against JEDEC JESD216 (the SFDP standard) + // Only the DWORDs this decoder reads are meaningful + // (DW2 density, DW8/DW9 erase types, DW11 page size); DWORDs it never touches + // are left zero. Encodings used below, per JESD216 Basic Flash Parameter table: + // * DW2 density: if bit 31 is clear, density_bits = value + 1 (linear form); + // if bit 31 is set, density_bits = 1 << (value & 0x7FFF_FFFF) (power-of-two). + // capacity_bytes = density_bits / 8. + // * DW11 page size: bits [7:4] are an exponent; page_size = 1 << exp. Absent + // (table shorter than 11 dwords) it defaults to 256 (pre-JESD216A rule). + // * DW8/DW9 erase types: each 16-bit half is `opcode << 8 | size_exponent`, + // erased size = 1 << exponent; exponent 0 marks an unused slot. Smallest + // erase -> sector_size, largest -> block_size. + + /// Write the 8-byte SFDP header: signature, revision, parameter-header count. + fn write_header(buf: &mut [u8], nph_minus_1: u8) { + buf[0..4].copy_from_slice(&SFDP_SIGNATURE.to_le_bytes()); + buf[4] = 0x06; // minor rev (not decoded beyond major) + buf[5] = SUPPORTED_MAJOR_REV; // major rev = 1 + buf[6] = nph_minus_1; // number of parameter headers minus one + buf[7] = 0xFF; // reserved + } + + /// Build one 8-byte parameter header pointing at `dwords` of table at `ptr`. + fn param_header(id_lsb: u8, id_msb: u8, dwords: u8, ptr: u32) -> [u8; 8] { + let p = ptr.to_le_bytes(); + [ + id_lsb, + 0x06, + SUPPORTED_MAJOR_REV, + dwords, + p[0], + p[1], + p[2], + id_msb, + ] + } + + /// Write little-endian `dwords` into `buf` starting at byte offset `at`. + fn put_dwords(buf: &mut [u8], at: usize, dwords: &[u32]) { + for (i, w) in dwords.iter().enumerate() { + buf[at + i * 4..at + i * 4 + 4].copy_from_slice(&w.to_le_bytes()); + } + } + + #[test] + fn linear_16mib_page256_three_erases() { + // 128 Mbit part, linear density: DW2 = 128 Mbit - 1 = 0x07FF_FFFF -> + // capacity 16 MiB. 256-byte page, 4K/32K/64K erases (common W25Q128 shape). + let mut img = [0u8; 60]; + write_header(&mut img, 0); // one parameter header + img[8..16].copy_from_slice(¶m_header(BFP_ID_LSB, BFP_ID_MSB, 11, 16)); + put_dwords( + &mut img, + 16, + &[ + 0, + 0x07FF_FFFF, // DW2 density: 128 Mbit - 1 (linear) + 0, + 0, + 0, + 0, + 0, + 0x520F_200C, // DW8 type1 4K/0x20, type2 32K/0x52 + 0x0000_D810, // DW9 type3 64K/0xD8, type4 unused + 0, + 0x0000_0080, // DW11 page exp 8 -> 256 + ], + ); + let g = decode_geometry(&img).unwrap(); + assert_eq!(g.capacity_bytes, 16 * 1024 * 1024); + assert_eq!(g.page_size, 256); + assert_eq!(g.sector_size, 4096); // smallest erase + assert_eq!(g.block_size, 65536); // largest erase + } + + #[test] + fn pow2_4gbit_page512_two_erases() { + // 4 Gbit part exercising the power-of-two density form: DW2 bit 31 set, + // exp = 32 -> 2^32 bits = 512 MiB. Varies the page size (512) and omits + // the 32K erase, so DW8's upper half is the "unused" slot (exponent 0) + // between two real types. + let mut img = [0u8; 60]; + write_header(&mut img, 0); + img[8..16].copy_from_slice(¶m_header(BFP_ID_LSB, BFP_ID_MSB, 11, 16)); + put_dwords( + &mut img, + 16, + &[ + 0, + 0x8000_0020, // DW2 density: bit31 set, exp 32 -> 4 Gbit + 0, + 0, + 0, + 0, + 0, + 0x0000_200C, // DW8 type1 4K/0x20, type2 unused + 0x0000_D810, // DW9 type3 64K/0xD8, type4 unused + 0, + 0x0000_0090, // DW11 page exp 9 -> 512 + ], + ); + let g = decode_geometry(&img).unwrap(); + assert_eq!(g.capacity_bytes, 512 * 1024 * 1024); + assert_eq!(g.page_size, 512); + assert_eq!(g.sector_size, 4096); + assert_eq!(g.block_size, 65536); + } + + #[test] + fn skips_vendor_header_and_defaults_page() { + // 32 Mbit part behind a non-BFP vendor header: the decoder must skip + // header 0, find the BFP in header 1, and follow its 0x20 pointer. The BFP + // is only 9 dwords (no DW11), so page size falls back to the default 256. + let mut img = [0u8; 72]; + write_header(&mut img, 1); // two parameter headers + img[8..16].copy_from_slice(¶m_header(0x81, 0x00, 4, 0)); // vendor, ignored + img[16..24].copy_from_slice(¶m_header(BFP_ID_LSB, BFP_ID_MSB, 9, 0x20)); + put_dwords( + &mut img, + 0x20, + &[ + 0, + 0x01FF_FFFF, // DW2 density: 32 Mbit - 1 -> 4 MiB + 0, + 0, + 0, + 0, + 0, + 0x520F_200C, // DW8 type1 4K/0x20, type2 32K/0x52 + 0x0000_D810, // DW9 type3 64K/0xD8 + ], + ); + let g = decode_geometry(&img).unwrap(); + assert_eq!(g.capacity_bytes, 4 * 1024 * 1024); + assert_eq!(g.page_size, 256); // no DW11 -> default + assert_eq!(g.sector_size, 4096); + assert_eq!(g.block_size, 65536); + } + + #[test] + fn bad_signature_rejected() { + let mut img = [0u8; 60]; + write_header(&mut img, 0); + img[0] = 0; // corrupt "SFDP" + assert_eq!( + SfdpHeader::parse(&img[0..8]), + Err(FLASH_GENERIC_SFDP_INVALID_SIGNATURE) + ); + } + + #[test] + fn unsupported_major_rev_rejected() { + let mut img = [0u8; 60]; + write_header(&mut img, 0); + img[5] = 2; // major rev != SUPPORTED_MAJOR_REV + assert_eq!( + SfdpHeader::parse(&img[0..8]), + Err(FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV) + ); + } + + #[test] + fn header_too_short_rejected() { + assert_eq!( + SfdpHeader::parse(&[0u8; 4]), + Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT) + ); + } + + #[test] + fn no_bfp_header_rejected() { + let mut img = [0u8; 60]; + write_header(&mut img, 0); + img[8..16].copy_from_slice(¶m_header(0x81, 0x00, 4, 16)); // vendor only + assert_eq!( + find_bfp_header(&img[0..8], &img[8..16]), + Err(FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND) + ); + } + + #[test] + fn invalid_pow2_density_rejected() { + // DW2 bit 31 set with exponent >= 64 is not a representable density. + let mut img = [0u8; 60]; + write_header(&mut img, 0); + img[8..16].copy_from_slice(¶m_header(BFP_ID_LSB, BFP_ID_MSB, 2, 16)); + put_dwords(&mut img, 16, &[0, 0x8000_0040]); // exp 64 + assert_eq!( + decode_geometry(&img), + Err(FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + ); + } +}