diff --git a/hal/blocking/flash/BUILD.bazel b/hal/blocking/flash/BUILD.bazel new file mode 100644 index 00000000..6962a9c3 --- /dev/null +++ b/hal/blocking/flash/BUILD.bazel @@ -0,0 +1,42 @@ +# 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 = "driver", + srcs = [ + "driver.rs", + ], + crate_name = "hal_flash_driver", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/types", + "@rust_crates//:zerocopy", + ], +) + +rust_library( + name = "flash", + srcs = [ + "flash.rs", + ], + crate_name = "hal_flash", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":driver", + "//util/io", + "//util/types", + ], +) + +rust_test( + name = "flash_test", + crate = ":flash", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/hal/blocking/flash/driver.rs b/hal/blocking/flash/driver.rs new file mode 100644 index 00000000..5cc6a758 --- /dev/null +++ b/hal/blocking/flash/driver.rs @@ -0,0 +1,139 @@ +//! Low-level flash driver interface. + +#![no_std] + +use core::num::NonZero; + +use util_types::PowerOf2Usize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +/// Low-level flash driver interface. +/// +/// This trait defines the interface for interacting with flash hardware at a low level. +/// It supports asynchronous-style operations (start/is_busy/complete) but can also be +/// implemented for synchronous drivers. +pub trait FlashDriver { + type Error; + /// A bitmap of supported erase block sizes. + /// + /// Each bit i represents a supported erase block size of 2^i bytes. + const ERASABLE_SIZES_BITMAP: u32; + /// The maximum size of a single program operation. + const PROGRAM_WINDOW_SIZE: usize; + /// The maximum size of a single read operation. + const MAX_READ_SIZE: usize; + /// The alignment required for read operations. + const READ_ALIGNMENT: usize; + /// The alignment required for program operations. + const PROGRAM_ALIGNMENT: usize; + + /// Returns the total size of the flash in bytes. + fn size(&self) -> NonZero; + + /// Reads data from flash. + /// + /// # Arguments + /// * `start_addr`: The address to start reading from. + /// * `buf`: The buffer to read data into. + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error>; + + /// Starts an erase operation. + /// + /// # Arguments + /// * `start_addr`: The start address of the block to erase. + /// * `size`: The size of the block to erase. + fn start_erase( + &mut self, + start_addr: FlashAddress, + size: PowerOf2Usize, + ) -> Result<(), Self::Error>; + + /// Starts a program operation. + /// + /// # Arguments + /// * `start_address`: The address to start programming at. + /// * `data`: The data to program. + fn start_program( + &mut self, + start_address: FlashAddress, + data: &[u8], + ) -> Result<(), Self::Error>; + + /// Returns whether the driver is currently busy with an operation. + fn is_busy(&mut self) -> bool; + + /// Completes a pending operation and returns the result. + fn complete_op(&mut self) -> Result<(), Self::Error>; +} + +/// Represents an address in flash memory. +/// +/// A flash address consists of a device identifier and an offset within that +/// device's address space. +#[derive(Default, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable, FromBytes, KnownLayout)] +#[repr(C)] +pub struct FlashAddress { + device_id: u32, + offset: u32, +} + +impl core::fmt::Display for FlashAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}:0x{:08x}", self.device_id, self.offset) + } +} + +impl core::fmt::Debug for FlashAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } +} + +impl FlashAddress { + /// Creates a new `FlashAddress`. + pub const fn new(device_id: u32, offset: u32) -> Self { + Self { device_id, offset } + } + + /// Returns the device identifier. + pub fn device_id(&self) -> u32 { + self.device_id + } + + /// Returns the offset within the device's address space. + pub fn offset(&self) -> u32 { + self.offset + } +} + +impl core::ops::Add for FlashAddress { + type Output = Self; + fn add(self, other: usize) -> Self { + Self { + device_id: self.device_id, + offset: self.offset + other as u32, + } + } +} + +impl core::ops::AddAssign for FlashAddress { + fn add_assign(&mut self, other: usize) { + self.offset += other as u32; + } +} + +impl core::ops::BitAnd for FlashAddress { + type Output = Self; + fn bitand(self, other: usize) -> Self { + Self { + device_id: self.device_id, + offset: self.offset & other as u32, + } + } +} + +impl core::ops::BitAndAssign for FlashAddress { + fn bitand_assign(&mut self, other: usize) { + self.offset &= other as u32; + } +} diff --git a/hal/blocking/flash/flash.rs b/hal/blocking/flash/flash.rs new file mode 100644 index 00000000..e123ef15 --- /dev/null +++ b/hal/blocking/flash/flash.rs @@ -0,0 +1,394 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! High-level blocking flash interface. + +#![cfg_attr(not(test), no_std)] + +use core::{cmp::min, num::NonZero}; +pub use hal_flash_driver::FlashAddress; +use hal_flash_driver::FlashDriver; +use util_io::RandomRead; +use util_types::{Blocking, PowerOf2Usize}; + +/// High-level flash interface. +/// +/// This trait provides a simplified, blocking interface for flash operations. +pub trait Flash { + type Error; + /// Returns the geometry of the flash. + /// + /// Returns a tuple of (total_size, page_size, erasable_sizes_bitmap). + fn geometry(&self) -> (NonZero, PowerOf2Usize, u32); + + /// Reads data from flash. + /// + /// # Arguments + /// * `start_addr`: The address to start reading from. + /// * `buf`: The buffer to read data into. + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error>; + + /// Erases a block of flash. + /// + /// # Arguments + /// * `start_addr`: The start address of the block to erase. + /// * `size`: The size of the block to erase. + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), Self::Error>; + + /// Programs data into flash. + /// + /// # Arguments + /// * `start_addr`: The address to start programming at. + /// * `data`: The data to program. + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), Self::Error>; + + /// Returns a `RandomRead` implementation for this flash. + fn random_reader(&mut self) -> impl RandomRead + where + Self: Sized, + { + FlashRandomReader(self) + } +} + +impl Flash for &mut F { + type Error = F::Error; + #[inline(always)] + fn geometry(&self) -> (NonZero, PowerOf2Usize, u32) { + (**self).geometry() + } + #[inline(always)] + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error> { + (**self).read(start_addr, buf) + } + #[inline(always)] + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), Self::Error> { + (**self).program(start_addr, data) + } + #[inline(always)] + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), Self::Error> { + (**self).erase(start_addr, size) + } +} + +/// A trait that can be used to constrain the page-size of the flash. +/// +/// If you just need to read the page size at runtime, use `Flash::geometry()` instead. +pub trait FlashPageSize { + /// The size of a flash page in bytes. + const PAGE_SIZE: usize; +} + +/// A blocking flash implementation that wraps a `FlashDriver`. +/// +/// This struct uses a `Blocking` implementation to wait for asynchronous flash +/// operations to complete. +pub struct BlockingFlash { + /// The underlying flash driver. + pub driver: TDriver, + /// The blocking mechanism used to wait for operations. + pub blocking: TBlocking, +} + +impl FlashPageSize + for BlockingFlash +{ + const PAGE_SIZE: usize = 1 << (TDriver::ERASABLE_SIZES_BITMAP.trailing_zeros()); +} + +impl Flash for BlockingFlash { + type Error = TDriver::Error; + fn geometry(&self) -> (NonZero, PowerOf2Usize, u32) { + let page_size = + PowerOf2Usize::new(1 << (TDriver::ERASABLE_SIZES_BITMAP.trailing_zeros())).unwrap(); + ( + self.driver.size(), + page_size, + TDriver::ERASABLE_SIZES_BITMAP, + ) + } + fn read(&mut self, start_addr: FlashAddress, mut buf: &mut [u8]) -> Result<(), Self::Error> { + let mut addr = start_addr; + let align_skip_len = (addr.offset() & (TDriver::READ_ALIGNMENT as u32 - 1)) as usize; + if (align_skip_len) != 0 { + assert!(TDriver::READ_ALIGNMENT <= 16); + let mut tmp = [0_u8; 16]; + let prefix_count = min(TDriver::READ_ALIGNMENT - align_skip_len, buf.len()); + self.driver + .read(addr & !(TDriver::READ_ALIGNMENT - 1), &mut tmp)?; + buf[..prefix_count].copy_from_slice(&tmp[align_skip_len..][..prefix_count]); + buf = &mut buf[prefix_count..]; + addr += prefix_count; + } + for buf_chunk in buf.chunks_mut(TDriver::MAX_READ_SIZE) { + self.driver.read(addr, buf_chunk)?; + addr += buf_chunk.len(); + } + Ok(()) + } + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), Self::Error> { + self.driver.start_erase(start_addr, size)?; + self.blocking.wait_for_notification(); + self.driver.complete_op() + } + fn program(&mut self, start_addr: FlashAddress, mut data: &[u8]) -> Result<(), Self::Error> { + assert!( + TDriver::PROGRAM_WINDOW_SIZE.count_ones() == 1, + "TDriver::PROGRAM_WINDOW_SIZE must be a power of 2" + ); + let window_mask = TDriver::PROGRAM_WINDOW_SIZE - 1; + let mut addr = start_addr; + while !data.is_empty() { + let chunk = &data[..min( + data.len(), + TDriver::PROGRAM_WINDOW_SIZE - ((addr.offset() & window_mask as u32) as usize), + )]; + self.driver.start_program(addr, chunk)?; + self.blocking.wait_for_notification(); + self.driver.complete_op()?; + data = &data[chunk.len()..]; + addr += chunk.len(); + } + Ok(()) + } +} + +struct FlashRandomReader<'a, F: Flash>(&'a mut F); +impl RandomRead for FlashRandomReader<'_, F> { + type Error = F::Error; + fn read(&mut self, start_addr: usize, buf: &mut [u8]) -> Result<(), Self::Error> { + self.0.read(FlashAddress::new(0, start_addr as u32), buf) + } + fn size(&self) -> usize { + self.0.geometry().0.get() + } +} + +#[cfg(test)] +mod test { + use super::*; + + pub struct FakeBlocking(); + impl Blocking for FakeBlocking { + fn wait_for_notification(&self) {} + } + + #[derive(Debug, Clone, Copy)] + pub struct FakeDriverError; + #[derive(Clone)] + pub struct FakeFlashDriver { + pub data: Vec, + pub check_err_result: Result<(), FakeDriverError>, + } + impl FakeFlashDriver { + pub fn new(data: Vec) -> Self { + Self { + data, + check_err_result: Ok(()), + } + } + } + impl FlashDriver for FakeFlashDriver { + type Error = FakeDriverError; + const ERASABLE_SIZES_BITMAP: u32 = 1 << 11; + const PROGRAM_WINDOW_SIZE: usize = 64; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 8; + + fn size(&self) -> NonZero { + NonZero::new(self.data.len()).unwrap() + } + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error> { + let start_addr = start_addr.offset() as usize; + assert!(start_addr.checked_add(buf.len()).unwrap() <= self.data.len()); + assert!(buf.len() <= Self::MAX_READ_SIZE); + assert!(start_addr % Self::READ_ALIGNMENT == 0); + buf.copy_from_slice(&self.data[start_addr..][..buf.len()]); + Ok(()) + } + fn start_erase( + &mut self, + start_addr: FlashAddress, + size: PowerOf2Usize, + ) -> Result<(), Self::Error> { + let start_addr = start_addr.offset() as usize; + assert_eq!(size.get(), 2048); + assert!(start_addr.checked_add(size.get()).unwrap() <= self.data.len()); + assert!(start_addr % size.get() == 0); + self.data[start_addr..][..size.get()].fill(0xff); + Ok(()) + } + fn start_program( + &mut self, + start_addr: FlashAddress, + data: &[u8], + ) -> Result<(), Self::Error> { + let start_addr = start_addr.offset() as usize; + assert!(start_addr.checked_add(data.len()).unwrap() <= self.data.len()); + assert!( + data.len() <= Self::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, + "Program window violation" + ); + for (dest, src) in self.data[start_addr..end_addr].iter_mut().zip(data) { + *dest &= *src; + } + Ok(()) + } + fn is_busy(&mut self) -> bool { + false + } + fn complete_op(&mut self) -> Result<(), Self::Error> { + self.check_err_result + } + } + + #[test] + #[should_panic(expected = "Program window violation")] + pub fn test_fake_flash_program_window_violation_0() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver + .start_program(FlashAddress::new(0, 0x3c), &[0x42; 5]) + .unwrap(); + } + + #[test] + #[should_panic(expected = "Program window violation")] + pub fn test_fake_flash_program_window_violation_1() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver + .start_program(FlashAddress::new(0, 0x0), &[0; 68]) + .unwrap(); + } + + #[test] + pub fn test_fake_flash_full_program_window() { + let mut flash_driver = FakeFlashDriver::new((0..255).collect()); + flash_driver + .start_program(FlashAddress::new(0, 0x40), &[0; 0x40]) + .unwrap(); + assert_eq!(flash_driver.data[0x40..0x80], [0; 0x40]); + } + + #[test] + pub fn test_size() { + let flash_driver = FakeFlashDriver::new((0..255).collect()); + let mut flash = BlockingFlash { + driver: flash_driver, + blocking: FakeBlocking(), + }; + + assert_eq!(flash.geometry().0.get(), 255); + assert_eq!(flash.random_reader().size(), 255); + } + + #[test] + pub fn test_read() { + let flash_driver = FakeFlashDriver::new((0..255).collect()); + + let mut flash = BlockingFlash { + driver: flash_driver, + blocking: FakeBlocking(), + }; + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(0, 0), &mut buf).unwrap(); + assert_eq!(buf, [0_u8, 1, 2, 3]); + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(0, 1), &mut buf).unwrap(); + assert_eq!(buf, [1, 2, 3, 4]); + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(0, 2), &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + + let mut buf = [0_u8; 4]; + flash.random_reader().read(2, &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(0, 3), &mut buf).unwrap(); + assert_eq!(buf, [3, 4, 5, 6]); + + let mut buf = [0_u8; 6]; + flash.read(FlashAddress::new(0, 3), &mut buf).unwrap(); + assert_eq!(buf, [3, 4, 5, 6, 7, 8]); + + for i in 0..32 { + let mut buf = [0_u8; 32]; + flash.read(FlashAddress::new(0, 0), &mut buf[..i]).unwrap(); + assert_eq!(&buf[..i], &flash.driver.data[..i]); + } + + for i in 0..32 { + let mut buf = [0_u8; 32]; + flash + .read(FlashAddress::new(0, 32 - i as u32), &mut buf[..i]) + .unwrap(); + assert_eq!(&buf[..i], &flash.driver.data[32 - i..32]); + } + } + + #[test] + pub fn test_erase() { + let mut flash = BlockingFlash { + driver: FakeFlashDriver::new(vec![0x42; 0x4000]), + blocking: FakeBlocking(), + }; + flash + .erase( + FlashAddress::new(0, 0x0800), + PowerOf2Usize::new(2048).unwrap(), + ) + .unwrap(); + assert_eq!(flash.driver.data[0x0000..0x0800], [0x42; 0x0800]); + assert_eq!(flash.driver.data[0x0800..0x1000], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x1000..0x4000], [0x42; 0x3000]); + + flash + .erase( + FlashAddress::new(0, 0x3000), + PowerOf2Usize::new(2048).unwrap(), + ) + .unwrap(); + assert_eq!(flash.driver.data[0x0000..0x0800], [0x42; 0x0800]); + assert_eq!(flash.driver.data[0x0800..0x1000], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x1000..0x3000], [0x42; 0x2000]); + assert_eq!(flash.driver.data[0x3000..0x3800], [0xff; 0x0800]); + assert_eq!(flash.driver.data[0x3800..0x4000], [0x42; 0x0800]); + } + + #[test] + pub fn test_program() { + let mut flash = BlockingFlash { + driver: FakeFlashDriver::new(vec![0xff; 8192]), + blocking: FakeBlocking(), + }; + + flash + .program( + FlashAddress::new(0, 0x3c), + &[0x10, 0x11, 0x12, 0x13, 0x14, 0x15], + ) + .unwrap(); + assert_eq!( + flash.driver.data[0x38..0x44], + [0xff, 0xff, 0xff, 0xff, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0xff, 0xff] + ); + + flash + .program(FlashAddress::new(0, 0x40), &[0x24, 0x25]) + .unwrap(); + assert_eq!( + flash.driver.data[0x38..0x44], + [0xff, 0xff, 0xff, 0xff, 0x10, 0x11, 0x12, 0x13, 0x04, 0x05, 0xff, 0xff] + ); + } +} diff --git a/services/flash/BUILD.bazel b/services/flash/BUILD.bazel new file mode 100644 index 00000000..4195c54c --- /dev/null +++ b/services/flash/BUILD.bazel @@ -0,0 +1,57 @@ +# 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 = [ + "//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 00000000..f13a3b4c --- /dev/null +++ b/services/flash/README.md @@ -0,0 +1,86 @@ +# 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::IpcChannel; + +// 1. Connect to the flash service +let mut flash = FlashIpcClient::new(IpcChannel::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(0, 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 consists of: +- **Device ID / Partition**: Represents different address spaces (e.g., Data vs. Info). +- **Offset**: The byte offset within that partition. + +On platforms like Earlgrey, helper methods are provided to construct 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 00000000..cf070b4c --- /dev/null +++ b/services/flash/client.rs @@ -0,0 +1,99 @@ +//! 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; +use util_types::PowerOf2Usize; +use zerocopy::{FromZeros, IntoBytes}; + +/// This struct implements the `Flash` trait by sending IPC requests to a remote +/// flash server. +pub struct FlashIpcClient { + ipc: IpcChannel, + page_size: PowerOf2Usize, + total_size: NonZero, + erasable_sizes_bitmap: u32, +} + +impl FlashIpcClient { + /// Creates a new `FlashIpcClient` from an existing IPC channel. + /// + /// This constructor will perform an IPC transaction to retrieve flash + /// information (page size and total size) from the server. + pub fn new(ipc: IpcChannel) -> Result { + let mut info = FlashInfo::new_zeroed(); + let mut result = 0u32; + + ipc.transaction::<16>( + &[IPC_OP_FLASH_GET_INFO.as_bytes()], + &mut [result.as_mut_bytes(), info.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::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(&self) -> (NonZero, PowerOf2Usize, u32) { + (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 size_val = size.get() as u32; + self.ipc.transaction::<16>( + &[ + IPC_OP_FLASH_ERASE.as_bytes(), + start_addr.as_bytes(), + size_val.as_bytes(), + ], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + + fn program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc.transaction::<2064>( + &[IPC_OP_FLASH_PROGRAM.as_bytes(), start_addr.as_bytes(), data], + &mut [result.as_mut_bytes()], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + let mut result = 0u32; + let length = buf.len() as u32; + self.ipc.transaction::<2064>( + &[ + IPC_OP_FLASH_READ.as_bytes(), + start_addr.as_bytes(), + length.as_bytes(), + ], + &mut [result.as_mut_bytes(), buf], + Instant::MAX, + )?; + IpcChannel::check_status(result) + } +} diff --git a/services/flash/opcode.rs b/services/flash/opcode.rs new file mode 100644 index 00000000..54bfdcf0 --- /dev/null +++ b/services/flash/opcode.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared flash IPC opcodes and data structures. + +#![no_std] + +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, +} diff --git a/services/flash/server.rs b/services/flash/server.rs new file mode 100644 index 00000000..3fefb13c --- /dev/null +++ b/services/flash/server.rs @@ -0,0 +1,111 @@ +//! 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; +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 } + } + + fn handle_get_info<'a>(&self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + 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()) + } + + fn handle_erase<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (addr, data) = + FlashAddress::read_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let (size, data) = u32::read_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let Some(size) = PowerOf2Usize::new(size as usize) else { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + }; + self.flash.erase(addr, size)?; + Ok(&data[0..0]) + } + + fn handle_program<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let (addr, data) = + FlashAddress::mut_from_prefix(data).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.program(*addr, data)?; + Ok(&data[0..0]) + } + + fn handle_read<'a>(&mut self, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + let addr = + FlashAddress::read_from_bytes(&data[0..8]).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let length = + usize::read_from_bytes(&data[8..12]).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + self.flash.read(addr, &mut data[..length])?; + Ok(&data[..length]) + } + + fn handle_op<'a>(&mut self, opcode: Opcode, data: &'a mut [u8]) -> Result<&'a [u8], ErrorCode> { + match opcode { + IPC_OP_FLASH_GET_INFO => self.handle_get_info(data), + IPC_OP_FLASH_ERASE => self.handle_erase(data), + IPC_OP_FLASH_PROGRAM => self.handle_program(data), + IPC_OP_FLASH_READ => self.handle_read(data), + _ => Err(error::IPC_ERROR_UNKNOWN_OP), + } + } + + /// Handles a single IPC request. + /// + /// This method waits for a request on the given IPC channel, dispatches it + /// to the appropriate handler, and sends the response. + pub fn handle_one(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + //pw_log::info!("ipc_wait"); + ipc.wait_readable()?; + //pw_log::info!("ipc_read"); + let len = ipc.read(0, data)?; + //pw_log::info!("ipc_exec"); + if len < 4 { + return Err(error::IPC_ERROR_BAD_REQ_LEN); + } + let (op_status, reqrsp) = data.split_at_mut(4); + let opcode = Opcode::read_from_bytes(op_status).unwrap(); + let len = match self.handle_op(opcode, reqrsp) { + Ok(result) => { + op_status.copy_from_slice((0u32).as_bytes()); + result.len() + } + Err(e) => { + op_status.copy_from_slice(e.0.as_bytes()); + 0 + } + }; + //pw_log::info!("ipc_respond: {}", len as usize); + ipc.respond(&data[..4 + len])?; + Ok(()) + } + + /// Runs the flash IPC server. + /// + /// This method enters an infinite loop, handling IPC requests one by one. + pub fn run(&mut self, ipc: &IpcChannel, data: &mut [u8]) -> Result<(), ErrorCode> { + loop { + self.handle_one(ipc, data)?; + } + } +} diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel index 0646daa7..486757b2 100644 --- a/target/earlgrey/drivers/BUILD.bazel +++ b/target/earlgrey/drivers/BUILD.bazel @@ -28,6 +28,22 @@ rust_library( visibility = ["//visibility:public"], deps = [ "//target/earlgrey/registers", + ], +) + +rust_library( + name = "eflash_driver", + srcs = ["eflash_driver.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash:driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/util", + "//util/error", + "//util/regcpy", + "//util/types", + "@rust_crates//:zerocopy", "@ureg", ], ) diff --git a/target/earlgrey/drivers/eflash_driver.rs b/target/earlgrey/drivers/eflash_driver.rs new file mode 100644 index 00000000..1c932461 --- /dev/null +++ b/target/earlgrey/drivers/eflash_driver.rs @@ -0,0 +1,261 @@ +#![no_std] + +use core::num::NonZero; + +use earlgrey_util::{AsMubi, EarlgreyFlashAddress}; +use flash_ctrl_core::{self, regs::ControlWriteVal}; +use hal_flash_driver::{FlashAddress, FlashDriver}; +use util_error::{self as error, ErrorCode}; +use util_regcpy::{copy_from_reg_unaligned, copy_to_reg_unaligned}; +use util_types::PowerOf2Usize; + +pub struct Permission { + pub read: bool, + pub write: bool, + pub erase: bool, +} + +impl Permission { + pub const FULL_ACCESS: Permission = Permission { + read: true, + write: true, + erase: true, + }; + pub const READ_ONLY: Permission = Permission { + read: true, + write: false, + erase: false, + }; +} + +const FLASH_SIZE: NonZero = NonZero::new(1024 * 1024).unwrap(); + +pub struct EmbeddedFlash { + mmio: flash_ctrl_core::FlashCtrl, + busy: bool, +} +impl EmbeddedFlash { + // TODO: unify these with the trait consts. + const BYTES_PER_BANK: u32 = 0x80000; + const BYTES_PER_PAGE: u32 = 2048; + + pub fn new(mmio: flash_ctrl_core::FlashCtrl) -> Self { + Self { mmio, busy: false } + } + + pub fn new_with_interrupts(mut mmio: flash_ctrl_core::FlashCtrl) -> Self { + mmio.regs_mut().intr_state().write(|w| w.op_done_clear()); + mmio.regs_mut().intr_enable().write(|w| w.op_done(true)); + Self { mmio, busy: false } + } + + pub fn set_default_permission(&mut self, perm: Permission) { + let reg = self.mmio.regs_mut(); + reg.default_region().modify(|v| { + v.rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } + + pub fn set_info_permission( + &mut self, + address: FlashAddress, + perm: Permission, + ) -> Result<(), ErrorCode> { + if !address.is_info() { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let reg = self.mmio.regs_mut(); + if address.bank() == 0 { + reg.bank0_info0_page_cfg() + .at(address.page() as usize) + .modify(|v| { + v.en(true.as_mubi()) + .rd_en(perm.read.as_mubi()) + .rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } else { + reg.bank1_info0_page_cfg() + .at(address.page() as usize) + .modify(|v| { + v.en(true.as_mubi()) + .rd_en(perm.read.as_mubi()) + .prog_en(perm.write.as_mubi()) + .erase_en(perm.erase.as_mubi()) + }); + } + Ok(()) + } +} + +//fn u32_from_usize(val: usize) -> u32 { +// u32::try_from(val).unwrap() +//} +impl FlashDriver for EmbeddedFlash { + type Error = ErrorCode; + // BytesPerWord: 8 + // WordsPerPage: 256 + // BytesPerBank: 524288 + // program_resolution: 8 (max flash words to program at one time) + // RegBusPgmResBytes = 64 + const ERASABLE_SIZES_BITMAP: u32 = 1 << 11; + const PROGRAM_WINDOW_SIZE: usize = 64; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 8; + + fn size(&self) -> core::num::NonZero { + FLASH_SIZE + } + + #[inline(never)] + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error> { + if buf.is_empty() { + return Ok(()); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.earlgrey_offset() + } else { + start_addr.offset() + }; + + if (start_offset & 3) != 0 { + return Err(error::FLASH_GENERIC_BAD_ALIGNMENT); + } + if buf.len() > Self::MAX_READ_SIZE { + return Err(error::FLASH_GENERIC_READ_TOO_LONG); + } + + self.check_busy()?; + self.mmio.regs_mut().addr().write(|w| w.start(start_offset)); + self.start_op(|w| { + w.op(|s| s.read()) + .partition_sel(start_addr.is_info()) + .num((buf.len() as u32 + 3) / 4 - 1) + }); + copy_from_reg_unaligned(buf, &self.mmio.regs_mut().rd_fifo()); + while self.is_busy() {} + self.complete_op() + } + + fn start_erase( + &mut self, + start_addr: FlashAddress, + size: PowerOf2Usize, + ) -> Result<(), Self::Error> { + if size.get() != 2048 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.earlgrey_offset() + } else { + start_addr.offset() + }; + let start_offset = start_offset as usize; + + if start_offset & (size.get() - 1) != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR); + } + self.check_busy()?; + + self.mmio + .regs_mut() + .addr() + .write(|w| w.start(start_offset as u32)); + self.start_op(|w| { + w.op(|s| s.erase()) + .erase_sel(|s| s.page_erase()) + .partition_sel(start_addr.is_info()) + .start(true) + }); + Ok(()) + } + + fn start_program(&mut self, start_addr: FlashAddress, data: &[u8]) -> Result<(), Self::Error> { + if data.is_empty() { + return Ok(()); + } + if data.len() > Self::PROGRAM_WINDOW_SIZE { + return Err(error::FLASH_GENERIC_PROGRAM_EXCEEDS_WINDOW_SIZE); + } + let start_offset = if start_addr.is_info() { + start_addr.bank() * Self::BYTES_PER_BANK + + start_addr.page() * Self::BYTES_PER_PAGE + + start_addr.earlgrey_offset() + } else { + start_addr.offset() + }; + let start_offset = start_offset as usize; + let end_offset = start_offset.wrapping_add(data.len()); + + if start_offset / Self::PROGRAM_WINDOW_SIZE != (end_offset - 1) / Self::PROGRAM_WINDOW_SIZE + { + return Err(error::FLASH_GENERIC_PROGRAM_SPANS_WINDOW_BOUNDARY); + } + self.check_busy()?; + // reset the op status register + + self.mmio + .regs_mut() + .addr() + .write(|w| w.start(start_offset as u32)); + self.start_op(|w| { + w.op(|s| s.prog()) + .prog_sel(|s| s.normal_program()) + .partition_sel(start_addr.is_info()) + .num(((data.len() + 3) / 4) as u32 - 1) + }); + copy_to_reg_unaligned(&self.mmio.regs_mut().prog_fifo(), data); + Ok(()) + } + + fn is_busy(&mut self) -> bool { + if self.busy && self.mmio.regs_mut().intr_state().read().op_done() { + self.busy = false; + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.op_done_clear()); + } + self.busy + } + + fn complete_op(&mut self) -> Result<(), Self::Error> { + if self.is_busy() { + return Err(error::FLASH_GENERIC_BUSY); + } + let status = self.mmio.regs().op_status().read(); + if status.err() { + let err_code = u32::from(self.mmio.regs().err_code().read()); + Err(error::FLASH_OPENTITAN.error(err_code as u16)) + } else { + Ok(()) + } + } +} +impl EmbeddedFlash { + fn start_op(&mut self, f: impl FnOnce(ControlWriteVal) -> ControlWriteVal) { + self.busy = true; + self.mmio.regs_mut().err_code().write(|_| 0xff.into()); + self.mmio.regs_mut().op_status().write(|w| w); + self.mmio + .regs_mut() + .intr_state() + .write(|w| w.op_done_clear()); + self.mmio.regs_mut().control().write(|w| f(w).start(true)); + } + fn check_busy(&mut self) -> Result<(), ErrorCode> { + if self.is_busy() { + Err(error::FLASH_GENERIC_BUSY) + } else { + Ok(()) + } + } +} diff --git a/target/earlgrey/tests/eflash/BUILD.bazel b/target/earlgrey/tests/eflash/BUILD.bazel new file mode 100644 index 00000000..f18dcb9f --- /dev/null +++ b/target/earlgrey/tests/eflash/BUILD.bazel @@ -0,0 +1,157 @@ +# 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") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "flash_server", + srcs = [ + "flash_server.rs", + ], + codegen_crate_name = "flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_app( + name = "flash_test", + srcs = [ + "flash_test.rs", + ], + codegen_crate_name = "flash_test_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:client", + "//target/earlgrey/util", + "//util/console", + "//util/error", + "//util/ipc", + "@pigweed//pw_base64/rust:pw_base64", + "@pigweed//pw_kernel/lib/pw_assert", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "flash", + apps = [ + ":flash_server", + ":flash_test", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "flash_verilator_test", + timeout = "eternal", + interface = "verilator", + tags = [ + "nightly_test", + "verilator", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":flash", +) + +opentitan_test( + name = "flash_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + interface = "teacup", + tags = [ + "earlgrey_silicon", + "hardware", + ], + target = ":flash", +) diff --git a/target/earlgrey/tests/eflash/flash_server.rs b/target/earlgrey/tests/eflash/flash_server.rs new file mode 100644 index 00000000..5848bf5f --- /dev/null +++ b/target/earlgrey/tests/eflash/flash_server.rs @@ -0,0 +1,83 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_server_codegen::{handle, signals}; +use pw_status::Error; +use userspace::time::Instant; +use userspace::{entry, syscall}; + +use earlgrey_util::EarlgreyFlashAddress; +use eflash_driver::{EmbeddedFlash, Permission}; +use hal_flash::{BlockingFlash, FlashAddress}; +use services_flash_server::FlashIpcServer; +use util_error::ErrorCode; +use util_ipc::IpcChannel; +use util_types::Blocking; + +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + let w = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) + .unwrap(); + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn flash_server() -> Result<(), ErrorCode> { + let mut driver = + EmbeddedFlash::new_with_interrupts(unsafe { flash_ctrl_core::FlashCtrl::new() }); + driver.set_default_permission(Permission::FULL_ACCESS); + for i in 5..9 { + driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + let flash = BlockingFlash { + driver, + blocking: FlashCtrlInterrupt, + }; + let mut flash_server = FlashIpcServer::new(flash); + let mut buf = [0u8; 2064]; + let ipc = IpcChannel::new(handle::FLASH_SERVICE); + flash_server.run(&ipc, &mut buf) +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 RUNNING"); + let ret = flash_server(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ FAIL: {:08x}", u32::from(e) as u32); + Err(Error::Unknown) + } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + //let _ = syscall::debug_shutdown(ret); + //loop {} + ret +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/eflash/flash_test.rs b/target/earlgrey/tests/eflash/flash_test.rs new file mode 100644 index 00000000..f5a34a33 --- /dev/null +++ b/target/earlgrey/tests/eflash/flash_test.rs @@ -0,0 +1,138 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use flash_test_codegen::handle; +use pw_status::Error; +use userspace::entry; + +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::ErrorCode; +use util_ipc::IpcChannel; + +fn get_manifest(flash: &mut FlashIpcClient) -> Result<(), ErrorCode> { + let mut buf = [0u8; 1024]; + flash.read(FlashAddress::data(0), &mut buf)?; + pw_log::info!("ROM_EXT manifest header:"); + util_console::hexdump::hexdump(&buf); + Ok(()) +} + +/* +use earlgrey_util::PersoCertificate; +fn get_certificates(flash: &mut FlashIpcClient) -> Result<(), ErrorCode> { + const BEGIN_CERT: &'static str = "-----BEGIN CERTIFICATE-----"; + const END_CERT: &'static str = "-----END CERTIFICATE-----"; + + let mut buf = [0u8; 1024]; + let mut output = [0u8; 1200]; + + pw_log::info!("Reading UDS cert"); + // Read out the UDS and print it if it exists. + // The UDS (factory) cert is located in bank=0, page=9. + if flash.read(FlashAddress::info(0, 9, 0), &mut buf).is_ok() { + let cert = PersoCertificate::from_bytes(&buf); + if let Ok((uds, _rest)) = cert { + pw_log::info!( + "Certificate: {}\n{}\n{}\n{}", + uds.name, + BEGIN_CERT as &str, + pw_base64::encode_str(uds.certificate, &mut output) + .map_err(ErrorCode::kernel_error)? as &str, + END_CERT as &str, + ); + } + } + + pw_log::info!("Reading CDI certs"); + // Read out the CDI certificates and print them. + // The CDI (dice) certs are located in bank=1, page=9. + let mut offset = 0usize; + loop { + let sz = core::cmp::min(2048 - offset, buf.len()); + flash.read(FlashAddress::info(1, 9, offset as u32), &mut buf[..sz])?; + match PersoCertificate::from_bytes(&buf) { + Ok((cdi, _)) => { + pw_log::info!( + "Certificate: {}\n{}\n{}\n{}", + cdi.name, + BEGIN_CERT as &str, + pw_base64::encode_str(cdi.certificate, &mut output) + .map_err(ErrorCode::kernel_error)? as &str, + END_CERT as &str, + ); + offset += (cdi.obj_size + 7) & !7; + } + Err(_) => break, + } + } + Ok(()) +} +*/ + +fn erase_program_test(flash: &mut FlashIpcClient, addr: FlashAddress) -> Result<(), ErrorCode> { + let (_total_size, page_size, _erasable_sizes_bitmap) = flash.geometry(); + pw_log::info!("Erasing {:08x}", addr.offset()); + flash.erase(addr, page_size)?; + pw_log::info!("Reading {:08x}", addr.offset()); + let mut buf = [0u8; 32]; + flash.read(addr, &mut buf)?; + util_console::hexdump::hexdump(&buf); + + pw_log::info!("Programming {:08x}", addr.offset()); + flash.program(addr, b"This is a test.")?; + + pw_log::info!("Reading {:08x}", addr.offset()); + flash.read(addr, &mut buf)?; + util_console::hexdump::hexdump(&buf); + + Ok(()) +} + +fn flash_test() -> Result<(), ErrorCode> { + let mut flash = FlashIpcClient::new(IpcChannel::new(handle::FLASH_SERVICE))?; + + let (total_size, page_size, _erasable_sizes_bitmap) = flash.geometry(); + pw_log::info!("Flash size: {}", total_size.get() as usize); + pw_log::info!("Flash page size: {}", page_size.get() as usize); + + get_manifest(&mut flash)?; + //get_certificates(&mut flash)?; + + // We're currently executing in SlotA, so we should be able to access SlotB. + erase_program_test(&mut flash, FlashAddress::data(0x90000))?; + erase_program_test(&mut flash, FlashAddress::info(0, 5, 0))?; + Ok(()) +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 RUNNING"); + let ret = flash_test(); + + // Log that an error occurred so that the app that caused the shutdown is logged. + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ FAIL: {:08x}", u32::from(e) as u32); + Err(Error::Unknown) + } + }; + + // Since this is written as a test, shut down with the return status from `main()`. + //let _ = syscall::debug_shutdown(ret); + //loop {} + ret +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("FAIL: panic in {}", module_path!() as &str); + loop {} +} diff --git a/target/earlgrey/tests/eflash/system.json5 b/target/earlgrey/tests/eflash/system.json5 new file mode 100644 index 00000000..244068e6 --- /dev/null +++ b/target/earlgrey/tests/eflash/system.json5 @@ -0,0 +1,86 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "flash_server", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_server", + ram_size_bytes: 4096, + objects: [ + { + name: "flash_service", + type: "channel_handler", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + //{ name: "flash_ctrl_prog_empty", number: 160 }, + //{ name: "flash_ctrl_prog_lvl", number: 161 }, + //{ name: "flash_ctrl_rd_full", number: 162 }, + //{ name: "flash_ctrl_rd_lvl", number: 163 }, + { name: "flash_ctrl_op_done", number: 164 }, + //{ name: "flash_ctrl_corr_err", number: 165 }, + ], + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + } + ], + threads: [ + { + name: "flash_server_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + + { + name: "flash_test", + flash_size_bytes: 16384, + processes: [ + { + name: "flash_test", + ram_size_bytes: 6144, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "flash_service", + }, + ], + threads: [ + { + name: "flash_test_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + + ], +} diff --git a/target/earlgrey/tests/eflash/target.rs b/target/earlgrey/tests/eflash/target.rs new file mode 100644 index 00000000..2e253d37 --- /dev/null +++ b/target/earlgrey/tests/eflash/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel new file mode 100644 index 00000000..de288e51 --- /dev/null +++ b/target/earlgrey/util/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "util", + srcs = [ + "error.rs", + "flash.rs", + "lib.rs", + "mubi.rs", + ], + crate_name = "earlgrey_util", + edition = "2024", + deps = [ + "//hal/blocking/flash:driver", + "//util/error", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/target/earlgrey/util/error.rs b/target/earlgrey/util/error.rs new file mode 100644 index 00000000..7b203244 --- /dev/null +++ b/target/earlgrey/util/error.rs @@ -0,0 +1,11 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use pw_status::Error; +use util_error::{ErrorCode, ErrorModule}; + +// TODO: review the pw_status error codes. + +pub const EG_ERROR: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`. +pub const EG_ERROR_CERT_NOT_FOUND: ErrorCode = EG_ERROR.from_pw(1, Error::NotFound); +pub const EG_ERROR_CERT_BAD_NAME: ErrorCode = EG_ERROR.from_pw(2, Error::InvalidArgument); diff --git a/target/earlgrey/util/flash.rs b/target/earlgrey/util/flash.rs new file mode 100644 index 00000000..63a82515 --- /dev/null +++ b/target/earlgrey/util/flash.rs @@ -0,0 +1,67 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Earlgrey-specific flash address utilities. + +use hal_flash_driver::FlashAddress; + +/// Constants for Earlgrey flash device IDs. +pub const DEVICE_ID_DATA: u32 = 0; +pub const DEVICE_ID_INFO: u32 = 1; + +/// A trait for constructing and inspecting Earlgrey-specific flash addresses. +pub trait EarlgreyFlashAddress { + /// Constructs a flash address for flash data pages. + fn data(address: u32) -> FlashAddress; + + /// Constructs a flash address for flash info pages. + /// + /// NOTE: Currently this packs the bank and page into the offset logically. + /// In the future, we may optimize this to use the flash-controller native + /// address representation directly to simplify the driver implementation. + fn info(bank: u32, page: u32, offset: u32) -> FlashAddress; + + /// Returns whether the flash address is an info page address. + fn is_info(&self) -> bool; + + /// Returns the bank of a flash info page. + fn bank(&self) -> u32; + + /// Returns the page number of a flash info page. + fn page(&self) -> u32; + + /// Returns the flash offset. For data pages, this is the absolute flash + /// address. For info pages, this is the offset within the info page. + fn earlgrey_offset(&self) -> u32; +} + +impl EarlgreyFlashAddress for FlashAddress { + fn data(address: u32) -> FlashAddress { + FlashAddress::new(DEVICE_ID_DATA, address) + } + + fn info(bank: u32, page: u32, offset: u32) -> FlashAddress { + let offset = (bank & 0x7f) << 24 | (page & 0xff) << 16 | (offset & 0xFFFF); + FlashAddress::new(DEVICE_ID_INFO, offset) + } + + fn is_info(&self) -> bool { + self.device_id() == DEVICE_ID_INFO + } + + fn bank(&self) -> u32 { + (self.offset() >> 24) & 0x7f + } + + fn page(&self) -> u32 { + (self.offset() >> 16) & 0xff + } + + fn earlgrey_offset(&self) -> u32 { + if self.is_info() { + self.offset() & 0xFFFF + } else { + self.offset() + } + } +} diff --git a/target/earlgrey/util/lib.rs b/target/earlgrey/util/lib.rs new file mode 100644 index 00000000..7d48d731 --- /dev/null +++ b/target/earlgrey/util/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +pub mod error; +pub mod flash; +mod mubi; + +pub use flash::EarlgreyFlashAddress; +pub use mubi::AsMubi; diff --git a/target/earlgrey/util/mubi.rs b/target/earlgrey/util/mubi.rs new file mode 100644 index 00000000..51fa77a5 --- /dev/null +++ b/target/earlgrey/util/mubi.rs @@ -0,0 +1,13 @@ +pub trait AsMubi { + fn as_mubi(&self) -> u32; +} + +impl AsMubi for bool { + fn as_mubi(&self) -> u32 { + if *self { + 6 + } else { + 9 + } + } +} diff --git a/util/console/BUILD.bazel b/util/console/BUILD.bazel new file mode 100644 index 00000000..aa11f812 --- /dev/null +++ b/util/console/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") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "console", + srcs = [ + "hexdump.rs", + "lib.rs", + ], + crate_name = "util_console", + edition = "2024", + deps = [ + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:zerocopy", + ], +) diff --git a/util/console/hexdump.rs b/util/console/hexdump.rs new file mode 100644 index 00000000..11ba81f1 --- /dev/null +++ b/util/console/hexdump.rs @@ -0,0 +1,53 @@ +//! Hexdump and hexadecimal string conversion utilities. + +use zerocopy::{Immutable, IntoBytes}; + +const HEX: [u8; 16] = *b"0123456789ABCDEF"; + +/// Prints a hexdump of the given data to the log. +pub fn hexdump(data: &T) +where + T: IntoBytes + Immutable + ?Sized, +{ + let data = data.as_bytes(); + for (i, d) in data.chunks(16).enumerate() { + let mut buf = [b' '; 80]; + let mut offset = i * 16; + for j in 0..8 { + buf[7 - j] = HEX[offset & 15]; + offset = offset >> 4; + } + for (j, &byte) in d.iter().enumerate() { + buf[10 + j * 3] = HEX[(byte >> 4) as usize]; + buf[11 + j * 3] = HEX[(byte & 15) as usize]; + buf[60 + j] = if byte >= 0x20 && byte < 0x7f { + byte + } else { + b'.' + }; + } + let end = 60 + d.len(); + // SAFETY: The buffer contains only ASCII codepoints. + let line = unsafe { core::str::from_utf8_unchecked(&buf[..end]) }; + pw_log::info!("{}", line as &str); + } +} + +/// Converts the given data to a hexadecimal string. +/// +/// The hexadecimal string is written into the `dest` buffer, and a slice of +/// the populated part of `dest` is returned as a `&str`. +pub fn hexstr<'a, T>(dest: &'a mut [u8], data: &T) -> &'a str +where + T: IntoBytes + Immutable + ?Sized, +{ + let data = data.as_bytes(); + let mut i = 0; + for &byte in data.iter() { + dest[i] = HEX[(byte >> 4) as usize]; + dest[i + 1] = HEX[(byte & 15) as usize]; + i += 2; + } + // SAFETY: the hex chars emitted into `dest` are ASCII codepoints. + unsafe { core::str::from_utf8_unchecked(&dest[..i]) } +} diff --git a/util/console/lib.rs b/util/console/lib.rs new file mode 100644 index 00000000..2afd6534 --- /dev/null +++ b/util/console/lib.rs @@ -0,0 +1,8 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Console-related utilities. + +#![no_std] + +pub mod hexdump; diff --git a/util/error/BUILD.bazel b/util/error/BUILD.bazel new file mode 100644 index 00000000..eb7594ec --- /dev/null +++ b/util/error/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_doc", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "error", + srcs = [ + "flash.rs", + "ipc.rs", + "kernel.rs", + "lib.rs", + ], + crate_name = "util_error", + edition = "2024", + deps = [ + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_doc( + name = "error_doc", + crate = ":error", +) diff --git a/util/error/flash.rs b/util/error/flash.rs new file mode 100644 index 00000000..976bfb4a --- /dev/null +++ b/util/error/flash.rs @@ -0,0 +1,63 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Flash-specific error codes. + +use crate::{ErrorCode, ErrorModule}; +use pw_status::Error; + +// TODO: review the pw_status error codes. + +/// The generic flash error module. +pub const FLASH_GENERIC: ErrorModule = ErrorModule::new(0x464c); //ascii `FL`. +/// The flash device is busy. +pub const FLASH_GENERIC_BUSY: ErrorCode = FLASH_GENERIC.from_pw(0, Error::Unavailable); +/// The erase address is invalid (e.g., not page-aligned). +pub const FLASH_GENERIC_ERASE_INVALID_ADDR: ErrorCode = + FLASH_GENERIC.from_pw(1, Error::InvalidArgument); +/// The operation has bad alignment. +pub const FLASH_GENERIC_BAD_ALIGNMENT: ErrorCode = FLASH_GENERIC.from_pw(2, Error::InvalidArgument); +/// The read request is too long. +pub const FLASH_GENERIC_READ_TOO_LONG: ErrorCode = FLASH_GENERIC.from_pw(3, Error::InvalidArgument); +/// The program operation exceeds the hardware window size. +pub const FLASH_GENERIC_PROGRAM_EXCEEDS_WINDOW_SIZE: ErrorCode = + FLASH_GENERIC.from_pw(4, Error::InvalidArgument); +/// The program operation spans a hardware window boundary. +pub const FLASH_GENERIC_PROGRAM_SPANS_WINDOW_BOUNDARY: ErrorCode = + FLASH_GENERIC.from_pw(5, Error::InvalidArgument); +/// The address is out of bounds. +pub const FLASH_GENERIC_ADDR_OUT_OF_BOUNDS: ErrorCode = + FLASH_GENERIC.from_pw(6, Error::InvalidArgument); +/// The flash page size is invalid. +pub const FLASH_GENERIC_INVALID_PAGE_SIZE: ErrorCode = + FLASH_GENERIC.from_pw(7, Error::InvalidArgument); +/// The flash size is invalid. +pub const FLASH_GENERIC_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(8, Error::InvalidArgument); +/// The erase size is invalid. +pub const FLASH_GENERIC_ERASE_INVALID_SIZE: ErrorCode = + FLASH_GENERIC.from_pw(9, Error::InvalidArgument); + +/// SFDP: Invalid memory density. +pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode = + FLASH_GENERIC.from_pw(1024, Error::InvalidArgument); +/// SFDP: Invalid signature. +pub const FLASH_GENERIC_SFDP_INVALID_SIGNATURE: ErrorCode = + FLASH_GENERIC.from_pw(1025, Error::InvalidArgument); +/// SFDP: No valid parameter header found. +pub const FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND: ErrorCode = + FLASH_GENERIC.from_pw(1026, Error::InvalidArgument); +/// SFDP: Parameters are too short. +pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT: ErrorCode = + FLASH_GENERIC.from_pw(1027, Error::InvalidArgument); +/// SFDP: Unsupported header major revision. +pub const FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV: ErrorCode = + FLASH_GENERIC.from_pw(1028, Error::InvalidArgument); +/// SFDP: Unsupported parameters major revision. +pub const FLASH_GENERIC_SFDP_UNSUPPORTED_PARAMS_MAJOR_REV: ErrorCode = + FLASH_GENERIC.from_pw(1029, Error::InvalidArgument); +/// SFDP: Parameters are too long. +pub const FLASH_GENERIC_SFDP_PARAMETERS_TOO_LONG: ErrorCode = + FLASH_GENERIC.from_pw(1030, Error::InvalidArgument); + +/// The OpenTitan flash error module. +pub const FLASH_OPENTITAN: ErrorModule = ErrorModule::new(0x464f); //ascii `FO`. diff --git a/util/error/ipc.rs b/util/error/ipc.rs new file mode 100644 index 00000000..e86db794 --- /dev/null +++ b/util/error/ipc.rs @@ -0,0 +1,20 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! IPC-specific error codes. + +use crate::{ErrorCode, ErrorModule}; +use pw_status::Error; + +/// The IPC error module. +pub const IPC_ERROR: ErrorModule = ErrorModule::new(0x4943); // ascii `IC` +/// The IPC response has a bad length. +pub const IPC_ERROR_RSP_BAD_LEN: ErrorCode = IPC_ERROR.from_pw(1, Error::InvalidArgument); +/// The IPC response is too large. +pub const IPC_ERROR_RSP_TOO_LARGE: ErrorCode = IPC_ERROR.from_pw(2, Error::InvalidArgument); +/// The IPC request is bad. +pub const IPC_ERROR_BAD_REQ: ErrorCode = IPC_ERROR.from_pw(3, Error::InvalidArgument); +/// The IPC request has a bad length. +pub const IPC_ERROR_BAD_REQ_LEN: ErrorCode = IPC_ERROR.from_pw(4, Error::InvalidArgument); +/// The IPC opcode is unknown. +pub const IPC_ERROR_UNKNOWN_OP: ErrorCode = IPC_ERROR.from_pw(5, Error::Unknown); diff --git a/util/error/kernel.rs b/util/error/kernel.rs new file mode 100644 index 00000000..f0f82dbf --- /dev/null +++ b/util/error/kernel.rs @@ -0,0 +1,41 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Kernel-specific error codes. + +use crate::{ErrorCode, ErrorModule}; + +/// The kernel error module. +pub const KERNEL_ERROR: ErrorModule = ErrorModule::new(0x4b45); // ascii `KE` +/// The operation was cancelled. +pub const KERNEL_ERROR_CANCELLED: ErrorCode = KERNEL_ERROR.error(1); +/// An unknown error occurred in the kernel. +pub const KERNEL_ERROR_UNKNOWN: ErrorCode = KERNEL_ERROR.error(2); +/// An invalid argument was provided to a kernel call. +pub const KERNEL_ERROR_INVALID_ARGUMENT: ErrorCode = KERNEL_ERROR.error(3); +/// The deadline for the operation was exceeded. +pub const KERNEL_ERROR_DEADLINE_EXCEEDED: ErrorCode = KERNEL_ERROR.error(4); +/// The requested resource was not found. +pub const KERNEL_ERROR_NOT_FOUND: ErrorCode = KERNEL_ERROR.error(5); +/// The resource already exists. +pub const KERNEL_ERROR_ALREADY_EXISTS: ErrorCode = KERNEL_ERROR.error(6); +/// Permission was denied for the operation. +pub const KERNEL_ERROR_PERMISSION_DENIED: ErrorCode = KERNEL_ERROR.error(7); +/// Resources have been exhausted. +pub const KERNEL_ERROR_RESOURCE_EXHAUSTED: ErrorCode = KERNEL_ERROR.error(8); +/// A precondition for the operation failed. +pub const KERNEL_ERROR_FAILED_PRECONDITION: ErrorCode = KERNEL_ERROR.error(9); +/// The operation was aborted. +pub const KERNEL_ERROR_ABORTED: ErrorCode = KERNEL_ERROR.error(10); +/// The value is out of range. +pub const KERNEL_ERROR_OUT_OF_RANGE: ErrorCode = KERNEL_ERROR.error(11); +/// The operation is unimplemented. +pub const KERNEL_ERROR_UNIMPLEMENTED: ErrorCode = KERNEL_ERROR.error(12); +/// An internal kernel error occurred. +pub const KERNEL_ERROR_INTERNAL: ErrorCode = KERNEL_ERROR.error(13); +/// The service or resource is unavailable. +pub const KERNEL_ERROR_UNAVAILABLE: ErrorCode = KERNEL_ERROR.error(14); +/// Data loss has occurred. +pub const KERNEL_ERROR_DATA_LOSS: ErrorCode = KERNEL_ERROR.error(15); +/// The caller is unauthenticated. +pub const KERNEL_ERROR_UNAUTHENTICATED: ErrorCode = KERNEL_ERROR.error(16); diff --git a/util/error/lib.rs b/util/error/lib.rs new file mode 100644 index 00000000..f5094086 --- /dev/null +++ b/util/error/lib.rs @@ -0,0 +1,122 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Error code handling. + +#![no_std] + +use core::num::NonZero; + +mod flash; +mod ipc; +mod kernel; + +pub use flash::*; +pub use ipc::*; +pub use kernel::*; + +/// Represents an error module. +/// +/// An error module is a non-zero 16-bit identifier that categorizes a set of +/// error codes. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct ErrorModule(pub NonZero); + +impl ErrorModule { + /// Creates a new `ErrorModule`. + /// + /// # Panics + /// Panics if `val` is zero. + pub const fn new(val: u16) -> Self { + match NonZero::new(val) { + Some(val) => Self(val), + None => panic!("ErrorModule must be non-zero"), + } + } + + /// Creates an `ErrorCode` within this module. + /// + /// The resulting `ErrorCode` will have the module ID in the upper 16 bits + /// and the provided `code` in the lower 16 bits. + pub const fn error(self, code: u16) -> ErrorCode { + ErrorCode::new(((self.0.get() as u32) << 16) | (code as u32)) + } + + /// Creates an `ErrorCode` from a Pigweed status. + /// + /// This is a convenience method for creating error codes that incorporate + /// a Pigweed status. + pub const fn from_pw(self, code: u16, err: pw_status::Error) -> ErrorCode { + // pw_status::Error is 5 bits. + self.error((code << 5) | (err as u16)) + } +} + +/// A 32-bit error code. +/// +/// An error code consists of a 16-bit module ID and a 16-bit module-specific +/// error value. +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +pub struct ErrorCode(pub NonZero); + +impl ErrorCode { + /// Creates a new `ErrorCode`. + /// + /// # Panics + /// Panics if `val` is zero. + pub const fn new(val: u32) -> Self { + match NonZero::new(val) { + Some(val) => Self(val), + None => panic!("ErrorCode must be non-zero"), + } + } + + /// Creates a kernel error code from a Pigweed status. + pub fn kernel_error(e: pw_status::Error) -> Self { + KERNEL_ERROR.error(e as u16) + } +} + +impl From for u32 { + fn from(e: ErrorCode) -> u32 { + e.0.get() + } +} + +/* + * TODO: decide if we want ufmt or not. +use ufmt::{uDebug, uDisplay, uwrite}; +impl uDisplay for ErrorCode { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + uwrite!(f, "0x{:x}", self.0.get()) + } +} + +impl uDebug for ErrorCode { + fn fmt(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error> + where + W: ufmt::uWrite + ?Sized, + { + uDisplay::fmt(self, f) + } +} +*/ + +impl core::fmt::Display for ErrorCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "0x{:x}", self.0.get()) + } +} + +impl core::fmt::Debug for ErrorCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } +} + +impl core::error::Error for ErrorCode {} diff --git a/util/io/BUILD.bazel b/util/io/BUILD.bazel new file mode 100644 index 00000000..49000837 --- /dev/null +++ b/util/io/BUILD.bazel @@ -0,0 +1,20 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "io", + srcs = [ + "io.rs", + ], + crate_name = "util_io", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_test( + name = "io_test", + crate = ":io", +) diff --git a/util/io/io.rs b/util/io/io.rs new file mode 100644 index 00000000..f5ebe6e4 --- /dev/null +++ b/util/io/io.rs @@ -0,0 +1,75 @@ +//! Input/Output traits and utilities. + +#![no_std] + +use util_error::{ErrorCode, ErrorModule}; + +/// The generic IO error module. +pub const IO_GENERIC: ErrorModule = ErrorModule::new(0x494F); // ascii: IO +/// The read operation is out of bounds. +pub const IO_GENERIC_READ_OUT_OF_BOUNDS: ErrorCode = + IO_GENERIC.from_pw(1, pw_status::Error::OutOfRange); + +/// Trait for random access read operations. +pub trait RandomRead { + type Error; + /// Reads data from the source into the destination buffer. + /// + /// # Arguments + /// * `start_addr`: The offset from which to start reading. + /// * `dst`: The buffer to read data into. + fn read(&mut self, start_addr: usize, dst: &mut [u8]) -> Result<(), Self::Error>; + + /// Returns the total size of the readable data in bytes. + fn size(&self) -> usize; +} + +impl RandomRead for &[u8] { + type Error = ErrorCode; + fn read(&mut self, start_addr: usize, dst: &mut [u8]) -> Result<(), Self::Error> { + // Explicit wrapping add. Overflows are expected to + // be detected in the indexing operation + let end_addr = start_addr.wrapping_add(dst.len()); + let src = self + .get(start_addr..end_addr) + .ok_or(IO_GENERIC_READ_OUT_OF_BOUNDS)?; + dst.copy_from_slice(src); + Ok(()) + } + fn size(&self) -> usize { + self.len() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn should_read() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut dst: [u8; 3] = [0; 3]; + assert!(src.read(6, &mut dst).is_ok()); + assert_eq!(&dst, &[7, 8, 9]); + assert_eq!(RandomRead::size(&src), 9); + } + + #[test] + fn should_fail() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut dst: [u8; 3] = [0; 3]; + assert!(src.read(7, &mut dst).is_err()); + } + + #[test] + fn invalid_start_address_should_not_panic() { + let mut src: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8]; + let mut dst: [u8; 4] = [0; 4]; + // Set `start_addr` so that adding `dst.len()` causes it to + // wrap around and become smaller than `src.len()` + let start_addr: usize = usize::MAX - (dst.len() - 1); + // Should not panic + let result = src.read(start_addr, &mut dst); + assert!(result.is_err()); + } +} diff --git a/util/ipc/BUILD.bazel b/util/ipc/BUILD.bazel new file mode 100644 index 00000000..3315d538 --- /dev/null +++ b/util/ipc/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") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "ipc", + srcs = [ + "lib.rs", + ], + crate_name = "util_ipc", + edition = "2024", + deps = [ + "//util/error", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/util/ipc/lib.rs b/util/ipc/lib.rs new file mode 100644 index 00000000..74b6e7bf --- /dev/null +++ b/util/ipc/lib.rs @@ -0,0 +1,106 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! IPC utilities. + +#![no_std] +use userspace::syscall::{self, Signals}; +use userspace::time::Instant; + +use util_error::ErrorCode; + +/// Represents an IPC channel. +/// +/// This struct provides a high-level interface for performing transactions +/// and handling requests on an IPC channel. +pub struct IpcChannel(u32); + +impl IpcChannel { + /// Creates a new `IpcChannel` from a raw channel handle. + pub const fn new(channel: u32) -> Self { + IpcChannel(channel) + } + + /// Checks an IPC status code and converts it to a `Result`. + pub fn check_status(code: u32) -> Result<(), ErrorCode> { + if code == 0 { + Ok(()) + } else { + Err(ErrorCode::new(code)) + } + } + + /// Performs an IPC transaction (request and response). + /// + /// This method combines multiple request buffers into a single buffer of + /// size `N`, sends it over the channel, and then distributes the response + /// into the provided response buffers. + /// + /// # Arguments + /// * `request`: A slice of buffers containing the request data. + /// * `response`: A slice of mutable buffers to receive the response data. + /// * `deadline`: The time by which the transaction must complete. + pub fn transaction( + &self, + request: &[&[u8]], + response: &mut [&mut [u8]], + deadline: Instant, + ) -> Result { + if false { + //let _n = N; + //syscall::channel_transact_iovec(self.0, request, response, deadline) + Err(util_error::KERNEL_ERROR_UNIMPLEMENTED) + } else { + let mut buffer = [0u8; N]; + let mut offset = 0usize; + + for item in request.iter() { + let sz = offset + item.len(); + buffer[offset..sz].copy_from_slice(item); + offset = sz; + } + let req = unsafe { + // SAFETY: naughty creation of a const ref to the same slice + // so we can use the same buffer for send and recv. + core::slice::from_raw_parts(buffer.as_ptr(), offset) + }; + let rsplen = syscall::channel_transact(self.0, req, &mut buffer, deadline) + .map_err(ErrorCode::kernel_error)?; + + offset = 0usize; + let rsp = &buffer[..rsplen]; + for item in response.iter_mut() { + let sz = offset + item.len(); + // TODO: how to handle an incomplete response?. + if sz > rsp.len() { + break; + } + item.copy_from_slice(&rsp[offset..sz]); + offset = sz; + } + Ok(rsplen) + } + } + + /// Waits until the channel is readable. + pub fn wait_readable(&self) -> Result<(), ErrorCode> { + loop { + let w = syscall::object_wait(self.0, Signals::READABLE, Instant::MAX) + .map_err(ErrorCode::kernel_error)?; + if w.pending_signals.contains(Signals::READABLE) { + break; + } + } + Ok(()) + } + + /// Reads data from the channel. + pub fn read(&self, offset: usize, buffer: &mut [u8]) -> Result { + syscall::channel_read(self.0, offset, buffer).map_err(ErrorCode::kernel_error) + } + + /// Sends a response on the channel. + pub fn respond(&self, buffer: &[u8]) -> Result<(), ErrorCode> { + syscall::channel_respond(self.0, buffer).map_err(ErrorCode::kernel_error) + } +} diff --git a/util/types/BUILD.bazel b/util/types/BUILD.bazel new file mode 100644 index 00000000..5c2c7f77 --- /dev/null +++ b/util/types/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "types", + srcs = [ + "lib.rs", + "opcode.rs", + "power_of_2.rs", + ], + crate_name = "util_types", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "types_test", + crate = ":types", +) diff --git a/util/types/lib.rs b/util/types/lib.rs new file mode 100644 index 00000000..1af4db9f --- /dev/null +++ b/util/types/lib.rs @@ -0,0 +1,21 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Common utility types. + +#![no_std] + +mod opcode; +mod power_of_2; + +pub use opcode::Opcode; +pub use power_of_2::PowerOf2Usize; + +/// A trait for blocking on notifications. +/// +/// This trait is typically implemented by mechanisms that need to wait for +/// an event or notification from another part of the system (e.g., an interrupt). +pub trait Blocking { + /// Waits until a notification is received. + fn wait_for_notification(&self); +} diff --git a/util/types/opcode.rs b/util/types/opcode.rs new file mode 100644 index 00000000..68d91d13 --- /dev/null +++ b/util/types/opcode.rs @@ -0,0 +1,19 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! IPC opcode definition. + +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +/// A 32-bit IPC opcode. +/// +/// Opcodes are typically represented as 4-character ASCII strings. +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable)] +pub struct Opcode(u32); + +impl Opcode { + /// Creates a new `Opcode` from a 4-byte array. + pub const fn new(val: [u8; 4]) -> Self { + Opcode(u32::from_le_bytes(val)) + } +} diff --git a/util/types/power_of_2.rs b/util/types/power_of_2.rs new file mode 100644 index 00000000..ee140b4c --- /dev/null +++ b/util/types/power_of_2.rs @@ -0,0 +1,78 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use core::hint::assert_unchecked; + +/// Represents a `usize`` that is guaranteed to be a power-of-two. The compiler +/// can take advantage of this fact when optimizing (for example, using bitwise +/// arithmetic instead of division). +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub struct PowerOf2Usize(usize); +impl PowerOf2Usize { + // WARNING: Do not add any functions or derives (such as + // zerocopy::FromBytes) that make it possible to modify this value without + // confirming that it is still a power-of-two. As the compiler is relying on + // the power-of-two assertion for safety, any such changes are unsound. + + /// Creates a new `PowerOf2Usize`. + /// + /// Returns `Some` if `val` is a power of two, `None` otherwise. + #[inline(always)] + pub const fn new(val: usize) -> Option { + if !val.is_power_of_two() { + return None; + } + Some(Self(val)) + } + + /// Returns the underlying `usize` value. + /// + /// This method asserts to the compiler that the value is a non-zero + /// power of two, which can enable certain optimizations. + #[inline(always)] + pub const fn get(self) -> usize { + // SAFETY: These assertions are safe because self.0 can only be set by + // Self::new, and we check for the same preconditions there. + // (LLVM is too stupid to realize that is_power_of_two() implies != 0) + unsafe { assert_unchecked(self.0 != 0) }; + unsafe { assert_unchecked(self.0.is_power_of_two()) }; + self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::PowerOf2Usize; + + #[test] + pub fn test() { + assert_eq!(PowerOf2Usize::new(0), None); + assert_eq!(PowerOf2Usize::new(1).unwrap().get(), 1); + assert_eq!(PowerOf2Usize::new(2).unwrap().get(), 2); + assert_eq!(PowerOf2Usize::new(3), None); + assert_eq!(PowerOf2Usize::new(4).unwrap().get(), 4); + assert_eq!(PowerOf2Usize::new(5), None); + assert_eq!(PowerOf2Usize::new(6), None); + assert_eq!(PowerOf2Usize::new(7), None); + assert_eq!(PowerOf2Usize::new(8).unwrap().get(), 8); + assert_eq!(PowerOf2Usize::new(9), None); + assert_eq!(PowerOf2Usize::new(0x7fff_ffff), None); + assert_eq!(PowerOf2Usize::new(0x8000_0000).unwrap().get(), 0x8000_0000); + assert_eq!(PowerOf2Usize::new(0x8000_0001), None); + assert_eq!(PowerOf2Usize::new(0xc000_0000), None); + assert_eq!(PowerOf2Usize::new(0xffff_ffff), None); + + #[cfg(target_pointer_width = "64")] + { + assert_eq!(PowerOf2Usize::new(0x7fff_ffff_ffff_ffff), None); + assert_eq!( + PowerOf2Usize::new(0x8000_0000_0000_0000).unwrap().get(), + 0x8000_0000_0000_0000 + ); + assert_eq!(PowerOf2Usize::new(0x8000_0000_0000_0001), None); + assert_eq!(PowerOf2Usize::new(0xc000_0000_0000_0000), None); + assert_eq!(PowerOf2Usize::new(0xffff_ffff_ffff_ffff), None); + } + } +}