hal: add a hal for accessing flash memories Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/hal/blocking/flash/BUILD.bazel b/hal/blocking/flash/BUILD.bazel new file mode 100644 index 0000000..6962a9c --- /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/README.md b/hal/blocking/flash/README.md new file mode 100644 index 0000000..55823ea --- /dev/null +++ b/hal/blocking/flash/README.md
@@ -0,0 +1,55 @@ +# Blocking Flash HAL + +This HAL provides abstractions for interacting with flash memory. It defines traits for low-level drivers and high-level synchronous interfaces, along with a helper implementation to bridge them. + +## Key Abstractions + +### `FlashAddress` + +A transparent wrapper around a 32-bit offset (`u32`) representing a location in flash memory. It supports basic arithmetic operations (`Add`, `AddAssign`, `BitAnd`, `BitAndAssign`) to facilitate address calculations. + +Platform-specific extension traits (like `EarlgreyFlashAddress` in `earlgrey_util`) may use the offset bits to encode additional information (e.g., distinguishing between DATA and INFO partitions using the MSB). + +### `FlashDriver` Trait + +Defines the low-level interface for flash hardware. It is designed to support both synchronous and asynchronous hardware controllers using a start-poll-complete execution model for erase and program operations: + +1. **Start**: Initiated via `start_erase` or `start_program`. +2. **Poll**: Check completion status via `is_busy`, or wait for a hardware interrupt. +3. **Complete**: Finalize the operation and retrieve any execution errors via `complete_op`. + +Read operations (`read`) are synchronous for simplicity. + +Drivers also define hardware-specific constraints as constants: +* `ERASABLE_SIZES_BITMAP`: A bitmap where bit `i` is set if erasing blocks of size `2^i` is supported. +* `PROGRAM_WINDOW_SIZE`: The maximum size of a single write, and the boundary alignment constraint for writes. +* `MAX_READ_SIZE`: The maximum size of a single read. +* `READ_ALIGNMENT` / `PROGRAM_ALIGNMENT`: Address and size alignment requirements. + +### `Flash` Trait + +Provides a simplified, synchronous, blocking interface suitable for application use. It abstracts away the low-level start-poll-complete flow and hardware constraints (alignment, windows). + +### `BlockingFlash` + +A concrete implementation of the `Flash` trait that wraps a `FlashDriver` and a `Blocking` mechanism. It implements the blocking behavior and handles driver constraints automatically: + +```mermaid +graph TD + App[Application] -- "Flash::program()" --> BF[BlockingFlash] + BF -- "1. start_program()" --> Driver[FlashDriver] + BF -- "2. wait_for_notification()" --> Blocking[Blocking impl] + Blocking -- "wakes up (IRQ/Poll)" --> BF + BF -- "3. complete_op()" --> Driver +``` + +#### Read Alignment Handling + +If a read request is not aligned to `TDriver::READ_ALIGNMENT`, `BlockingFlash::read` will: +1. Read the aligned block containing the unaligned start address into a temporary buffer. +2. Copy the requested bytes from the temporary buffer. +3. Read the remaining data in aligned chunks directly into the destination buffer. + +#### Program Window Handling + +Hardware flash controllers often cannot program data that crosses a write window boundary (typically 64 bytes). `BlockingFlash::program` automatically detects these boundaries and splits a large or unaligned write into multiple smaller writes that fit within the program windows.
diff --git a/hal/blocking/flash/driver.rs b/hal/blocking/flash/driver.rs new file mode 100644 index 0000000..378152b --- /dev/null +++ b/hal/blocking/flash/driver.rs
@@ -0,0 +1,159 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! 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 an asynchronous-style execution model using a start-poll-complete pattern: +/// 1. Start the operation (`start_erase`, `start_program`). +/// 2. Poll status (`is_busy`) or wait for interrupt. +/// 3. Finalize and check errors (`complete_op`). +/// +/// Read operations (`read`) are assumed to be synchronous and blocking for simplicity. +pub trait FlashDriver { + /// The error type returned by driver operations. + type Error; + + /// The default page size in bytes. + const PAGE_SIZE: usize; + + /// 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; + + /// The maximum size of a single read operation. + const MAX_READ_SIZE: usize; + + /// The alignment required for read operations (addresses and lengths). + const READ_ALIGNMENT: usize; + + /// The alignment required for program operations (addresses and lengths). + const PROGRAM_ALIGNMENT: usize; + + /// Returns the total size of the flash in bytes. + fn size(&self) -> NonZero<usize>; + + /// Returns a bitmap of supported erase block sizes. + /// + /// Each bit `i` represents a supported erase block size of `2^i` bytes. + /// For example, if bit 11 is set, then 2048-byte erases are supported. + fn erasable_sizes_bitmap(&mut self) -> Result<u32, Self::Error>; + + /// Reads data from flash synchronously. + /// + /// # Arguments + /// * `start_addr`: The address to start reading from. Must be aligned to `READ_ALIGNMENT`. + /// * `buf`: The buffer to read data into. Must have size <= `MAX_READ_SIZE` and be aligned to `READ_ALIGNMENT`. + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), Self::Error>; + + /// Starts an erase operation asynchronously. + /// + /// The operation is not guaranteed to be complete until `complete_op` is called + /// and returns `Ok`. + /// + /// # Arguments + /// * `start_addr`: The start address of the block to erase. Must be aligned to the block size. + /// * `size`: The size of the block to erase. Must have its corresponding bit set in `ERASABLE_SIZES_BITMAP`. + fn start_erase( + &mut self, + start_addr: FlashAddress, + size: PowerOf2Usize, + ) -> Result<(), Self::Error>; + + /// Starts a program operation asynchronously. + /// + /// The operation is not guaranteed to be complete until `complete_op` is called + /// and returns `Ok`. + /// + /// The programmed region must not cross a `PROGRAM_WINDOW_SIZE` boundary. + /// + /// # Arguments + /// * `start_address`: The address to start programming at. Must be aligned to `PROGRAM_ALIGNMENT`. + /// * `data`: The data to program. Must have size <= `PROGRAM_WINDOW_SIZE` and be aligned to `PROGRAM_ALIGNMENT`. + 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 erase or program operation and returns the result. + /// + /// This should be called after `is_busy` returns false to check for errors and + /// reset the driver state. + fn complete_op(&mut self) -> Result<(), Self::Error>; +} + +/// Represents an address in flash memory. +/// +/// A flash address consists of an offset within the flash address space. +#[derive(Default, Clone, Copy, PartialEq, Eq, IntoBytes, Immutable, FromBytes, KnownLayout)] +#[repr(transparent)] +pub struct FlashAddress { + offset: u32, +} + +impl core::fmt::Display for FlashAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "0x{:08x}", 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(offset: u32) -> Self { + Self { offset } + } + + /// Returns the offset. + pub fn offset(&self) -> u32 { + self.offset + } +} + +impl core::ops::Add<usize> for FlashAddress { + type Output = Self; + fn add(self, other: usize) -> Self { + Self { + offset: self.offset + other as u32, + } + } +} + +impl core::ops::AddAssign<usize> for FlashAddress { + fn add_assign(&mut self, other: usize) { + self.offset += other as u32; + } +} + +impl core::ops::BitAnd<usize> for FlashAddress { + type Output = Self; + fn bitand(self, other: usize) -> Self { + Self { + offset: self.offset & other as u32, + } + } +} + +impl core::ops::BitAndAssign<usize> 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 0000000..072e83c --- /dev/null +++ b/hal/blocking/flash/flash.rs
@@ -0,0 +1,430 @@ +// 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, synchronous, blocking interface for flash operations. +/// It abstracts away the asynchronous execution model and hardware alignment/window +/// constraints of the underlying driver. +pub trait Flash { + /// The error type returned by flash operations. + type Error; + + /// Returns the geometry of the flash. + /// + /// # Returns + /// A tuple containing: + /// 1. The total size of the flash in bytes. + /// 2. The default/smallest page size (erase block size) in bytes. + /// 3. A bitmap of all supported erase block sizes. + fn geometry(&mut self) -> Result<(NonZero<usize>, PowerOf2Usize, u32), Self::Error>; + + /// Reads data from flash into the provided buffer. + /// + /// This method handles unaligned read addresses by performing partial reads + /// into a temporary buffer if necessary. + /// + /// # 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. + /// + /// This is a blocking operation that waits for the hardware erase to complete. + /// + /// # Arguments + /// * `start_addr`: The start address of the block to erase. Must be aligned to `size`. + /// * `size`: The size of the block to erase. Must be one of the supported sizes in `geometry().2`. + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), Self::Error>; + + /// Programs data into flash. + /// + /// This is a blocking operation that waits for the hardware program to complete. + /// It automatically handles programming data that spans across hardware program + /// window boundaries by splitting it into multiple aligned writes. + /// + /// # 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<Error = Self::Error> + where + Self: Sized, + { + FlashRandomReader(self) + } +} + +impl<F: Flash> Flash for &mut F { + type Error = F::Error; + #[inline(always)] + fn geometry(&mut self) -> Result<(NonZero<usize>, PowerOf2Usize, u32), Self::Error> { + (**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 implements the high-level `Flash` trait by wrapping a low-level +/// `FlashDriver` and using a `Blocking` mechanism (e.g., waiting for an interrupt +/// or polling) to block the calling thread until asynchronous driver operations +/// complete. +/// +/// It also handles address alignment for reads and program window constraints for writes. +pub struct BlockingFlash<TDriver: FlashDriver, TBlocking: Blocking> { + /// The underlying flash driver. + pub driver: TDriver, + /// The blocking mechanism used to wait for operations. + pub blocking: TBlocking, +} + +impl<TDriver: FlashDriver, TBlocking: Blocking> FlashPageSize + for BlockingFlash<TDriver, TBlocking> +{ + /// The default page size. + const PAGE_SIZE: usize = TDriver::PAGE_SIZE; +} + +impl<TDriver: FlashDriver, TBlocking: Blocking> Flash for BlockingFlash<TDriver, TBlocking> { + type Error = TDriver::Error; + fn geometry(&mut self) -> Result<(NonZero<usize>, PowerOf2Usize, u32), Self::Error> { + let bitmap = self.driver.erasable_sizes_bitmap()?; + let page_size = PowerOf2Usize::new(1 << (bitmap.trailing_zeros())).unwrap(); + Ok((self.driver.size(), page_size, bitmap)) + } + /// Reads data from flash. + /// + /// Handles unaligned `start_addr` by reading the aligned block containing it + /// into a temporary buffer first, copying the relevant bytes, and then reading + /// the remaining data in aligned chunks. + 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 { + // Read prefix up to alignment boundary + 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; + } + // Read remaining aligned chunks + for buf_chunk in buf.chunks_mut(TDriver::MAX_READ_SIZE) { + self.driver.read(addr, buf_chunk)?; + addr += buf_chunk.len(); + } + Ok(()) + } + /// Erases a block of flash. + /// + /// Starts the asynchronous erase operation and blocks the thread using `self.blocking` + /// until the operation completes. + 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() + } + /// Programs data into flash. + /// + /// Splits the data into chunks that fit within the hardware's `PROGRAM_WINDOW_SIZE` + /// and do not cross window boundaries. Each chunk is programmed asynchronously, + /// and the thread blocks until it completes before starting the next chunk. + 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() { + // 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), + )]; + 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<F: Flash> 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(start_addr as u32), buf) + } + fn size(&mut self) -> Result<usize, Self::Error> { + Ok(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<u8>, + pub check_err_result: Result<(), FakeDriverError>, + } + impl FakeFlashDriver { + pub fn new(data: Vec<u8>) -> Self { + Self { + data, + check_err_result: Ok(()), + } + } + } + impl FlashDriver for FakeFlashDriver { + type Error = FakeDriverError; + const PAGE_SIZE: usize = 2048; + const PROGRAM_WINDOW_SIZE: usize = 64; + const MAX_READ_SIZE: usize = 4096; + const READ_ALIGNMENT: usize = 4; + const PROGRAM_ALIGNMENT: usize = 8; + + fn erasable_sizes_bitmap(&mut self) -> Result<u32, Self::Error> { + Ok(1 << 11) + } + fn size(&self) -> NonZero<usize> { + 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(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(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(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().unwrap().0.get(), 255); + let mut reader = flash.random_reader(); + assert_eq!(reader.size().unwrap(), 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), &mut buf).unwrap(); + assert_eq!(buf, [0_u8, 1, 2, 3]); + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(1), &mut buf).unwrap(); + assert_eq!(buf, [1, 2, 3, 4]); + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(2), &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + + { + let mut reader = flash.random_reader(); + let mut buf = [0_u8; 4]; + reader.read(2, &mut buf).unwrap(); + assert_eq!(buf, [2, 3, 4, 5]); + } + + let mut buf = [0_u8; 4]; + flash.read(FlashAddress::new(3), &mut buf).unwrap(); + assert_eq!(buf, [3, 4, 5, 6]); + + let mut buf = [0_u8; 6]; + flash.read(FlashAddress::new(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), &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(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(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(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(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(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/util/io/BUILD.bazel b/util/io/BUILD.bazel new file mode 100644 index 0000000..9c68076 --- /dev/null +++ b/util/io/BUILD.bazel
@@ -0,0 +1,23 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "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 0000000..1b834b8 --- /dev/null +++ b/util/io/io.rs
@@ -0,0 +1,78 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! 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(&mut self) -> Result<usize, Self::Error>; +} + +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(&mut self) -> Result<usize, Self::Error> { + Ok(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(&mut src).unwrap(), 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()); + } +}