From 2b82f8606b2a45b3353690b98603707994216cbb Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 30 Jun 2026 04:34:51 +0800 Subject: [PATCH 1/2] drivers/flash: Port generic SPI Flash driver and add smoke test Key modifications and fixes made during the port: - Makes the driver explicitly enter "Global 4-byte" mode when flash size >= 16 MB, which includes: * Update the command config in `from_sfdp()` & `from_sfdp_conservative()`. * Explicitly enters the global 4-byte mode at the end of initialization. - Skips the support of DUAL / QUAD in `SpiTxnWidth`. Also, inits the driver with non-conservative version for now. - Moves `transfer_req_resp` from legacy SPI Host driver to SPI Flash driver, as a helper method. - Moves `check_valid_size` from legacy Flash trait to SPI Flash driver, as a helper method. - Adds `is_busy` helper method as what we did for `BlockingFlash` trait, although we didn't implement this trait directly now. - Wraps legacy `page_size` and `size` into a `geometry` method required by `Flash` trait. Currently we hard-coded the erasable bitmap as 4KB and 64KB, and we may parse them in the future. - For `erase()` method, the `Flash` trait only accepts the size declared in the erasable bitmap, but here we uses the legacy implementation which will loop over the size to erase. However, we still only accepts power of 2 as the erase size due to `Flash` trait's contract. Also removes the original `erase_page()` method. - Drops `FlashStreamingRead` support (as we mentioned in SPI Host driver commit). Mock SPI Device & Unit Test Refactoring: - Ports the legacy `FakeSpiHost` to OpenPRoT, renaming it to `FakeSpiDevice` under `drivers/mock/spi_device_fake.rs` and implementing standard `embedded_hal::spi::SpiDevice` and `ErrorType` traits. - Rewrites mock transmission logic to be stateless by removing `TxnState`, supporting multi-buffer transaction execution and automatic TX/RX clock log alignment and padding. - Introduces `assert_all_expectations_met()` to strictly assert that all preprogrammed SPI responses are executed, preventing silent test leaks. - Adapts unit tests to new `PowerOf2Usize` API constraints: * Erase lengths in mixed granularity and multiple page tests are changed to powers of 2 (e.g., 144KB -> 128KB, 12KB -> 16KB). * Obsolete `test_erase_zero_length` is removed as zero-length input is prevented at compile time. - Aligns mock expectations with Global 4-byte address mode: expected opcodes are updated from 4-byte-specific ones (e.g. `OP_READ4B`) to standard opcodes (e.g. `OP_READ`) with 4-byte address streams. - Removes obsolete unit tests for prefetch, FIFO draining, and Streaming Read. Signed-off-by: Chia-Wei Liu --- drivers/flash/BUILD.bazel | 33 + drivers/flash/spi_flash.rs | 2091 +++++++++++++++++ drivers/mock/BUILD.bazel | 21 + drivers/mock/spi_device_fake.rs | 320 +++ .../tests/drivers/spi_flash/BUILD.bazel | 107 + .../tests/drivers/spi_flash/spi_flash.rs | 118 + .../tests/drivers/spi_flash/system.json5 | 41 + .../tests/drivers/spi_flash/target.rs | 29 + util/error/flash.rs | 3 + 9 files changed, 2763 insertions(+) create mode 100644 drivers/flash/BUILD.bazel create mode 100644 drivers/flash/spi_flash.rs create mode 100644 drivers/mock/BUILD.bazel create mode 100644 drivers/mock/spi_device_fake.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/drivers/spi_flash/spi_flash.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/drivers/spi_flash/target.rs diff --git a/drivers/flash/BUILD.bazel b/drivers/flash/BUILD.bazel new file mode 100644 index 00000000..88445569 --- /dev/null +++ b/drivers/flash/BUILD.bazel @@ -0,0 +1,33 @@ +# 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 = "spi_flash", + srcs = ["spi_flash.rs"], + crate_name = "spi_flash", + edition = "2024", + proc_macro_deps = [ + "@rust_crates//:bitfield-struct", + ], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//util/error", + "//util/io", + "//util/sfdp", + "//util/types", + "@rust_crates//:embedded-hal", + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "spi_flash_test", + crate = ":spi_flash", + deps = [ + "//drivers/mock:spi_device_fake", + ], +) diff --git a/drivers/flash/spi_flash.rs b/drivers/flash/spi_flash.rs new file mode 100644 index 00000000..d70c5647 --- /dev/null +++ b/drivers/flash/spi_flash.rs @@ -0,0 +1,2091 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use bitfield_struct::bitfield; +use core::cmp::min; +use core::convert::TryFrom; +use core::num::NonZero; +use core::prelude::v1::*; +use util_sfdp::{QuadEnableRequirements, SfdpReader}; +use util_types::PowerOf2Usize; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes}; + +use embedded_hal::spi::Operation; +use hal_flash::{Flash as FlashTrait, FlashAddress}; +use util_error::{self as error, ErrorCode}; +use util_io::RandomRead; + +// TODO(b/481400917): Replace with stronger "byte count" type. +const KIB: usize = 1024; +const MIB: usize = 1024 * KIB; + +// The maximum number of bytes the opcode + address + dummy bytes at the start +// of a transaction will need. +const MAX_PREFIX_LEN: usize = 6; +const MAX_3B_SIZE: usize = 16 * MIB; +const SECTOR_SIZE: usize = 4096; +const BLOCK_SIZE: usize = 64 * KIB; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiTxnWidth { + STANDARD = 0, + DUAL = 1, + QUAD = 2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SfCmd { + pub opcode: u8, + pub width: SpiTxnWidth, + pub addr_mode: AddressingMode, +} + +impl SfCmd { + pub const READ: Self = Self { + opcode: OP_READ, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const PROGRAM: Self = Self { + opcode: OP_PROGRAM, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const ERASE: Self = Self { + opcode: OP_ERASE_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const READ4B: Self = Self { + opcode: OP_READ4B, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const PROGRAM4B: Self = Self { + opcode: OP_PROGRAM4B, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE4B: Self = Self { + opcode: OP_ERASE4B_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE64K: Self = Self { + opcode: OP_ERASE_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const ERASE4B_64K: Self = Self { + opcode: OP_ERASE4B_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const QREAD: Self = Self { + opcode: OP_QREAD, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_3ByteWithDummy, + }; + pub const QREAD4B: Self = Self { + opcode: OP_QREAD4B, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_4ByteWithDummy, + }; + pub const QPROGRAM: Self = Self { + opcode: OP_QPROGRAM, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_3Byte, + }; + pub const QPROGRAM4B: Self = Self { + opcode: OP_QPROGRAM4B, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_4Byte, + }; + pub const READ4B_GLOBAL: Self = Self { + opcode: OP_READ, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const PROGRAM4B_GLOBAL: Self = Self { + opcode: OP_PROGRAM, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE4B_GLOBAL: Self = Self { + opcode: OP_ERASE_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE64K_GLOBAL: Self = Self { + opcode: OP_ERASE_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; +} + +pub struct SpiFlashConfig { + pub read: SfCmd, + pub program: SfCmd, + pub erase4k: SfCmd, + pub erase64k: SfCmd, + + /// The total size of the flash in bytes + pub size: NonZero, + pub quad_enable_req: Option, +} + +impl SpiFlashConfig { + pub fn from_sfdp_conservative>( + sfdp_bytes: R, + ) -> Result { + let mut sfdp = SfdpReader::new(sfdp_bytes)?; + let table = sfdp.basic_flash_parameters()?; + + let size = usize::try_from(table.table_jesd216().memory_density.byte_len()?).unwrap(); + let size = NonZero::new(size).ok_or(error::FLASH_GENERIC_INVALID_SIZE)?; + let config = if size.get() <= MAX_3B_SIZE { + SpiFlashConfig { + size, + read: SfCmd::READ, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + program: SfCmd::PROGRAM, + quad_enable_req: None, + } + } else { + SpiFlashConfig { + size, + read: SfCmd::READ4B_GLOBAL, + erase4k: SfCmd::ERASE4B_GLOBAL, + erase64k: SfCmd::ERASE64K_GLOBAL, + program: SfCmd::PROGRAM4B_GLOBAL, + quad_enable_req: None, + } + }; + Ok(config) + } + + pub fn from_sfdp>(sfdp_bytes: R) -> Result { + let mut sfdp = SfdpReader::new(sfdp_bytes)?; + let table = sfdp.basic_flash_parameters()?; + + let size = usize::try_from(table.table_jesd216().memory_density.byte_len()?).unwrap(); + let size = NonZero::new(size).ok_or(error::FLASH_GENERIC_INVALID_SIZE)?; + + // TODO: Restore Quad SPI (QSPI) support once earlgrey_spi_host implements + // DUAL/QUAD transaction widths. + let config = if size.get() <= MAX_3B_SIZE { + SpiFlashConfig { + size, + read: SfCmd::READ, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + program: SfCmd::PROGRAM, + quad_enable_req: None, + } + } else { + SpiFlashConfig { + size, + read: SfCmd::READ4B_GLOBAL, + erase4k: SfCmd::ERASE4B_GLOBAL, + erase64k: SfCmd::ERASE64K_GLOBAL, + program: SfCmd::PROGRAM4B_GLOBAL, + quad_enable_req: None, + } + }; + Ok(config) + } +} + +/// "Driver" for SPI NOR flash. +pub struct SpiFlash { + spi: S, + config: SpiFlashConfig, + initialized: bool, +} + +impl SpiFlash { + pub fn new(spi: S) -> Self { + Self { + spi, + config: SpiFlashConfig { + read: SfCmd::READ, + program: SfCmd::PROGRAM, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + size: NonZero::new(1).unwrap(), + quad_enable_req: None, + }, + initialized: false, + } + } + + pub fn init(&mut self) -> Result<(), ErrorCode> { + let sfdp_bytes = SfdpRandRead { spi: &mut self.spi }; + let config = SpiFlashConfig::from_sfdp(sfdp_bytes)?; + let qer = config.quad_enable_req; + + let mut status = Status::new_zeroed(); + self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes())?; + + if status.busy() { + self.wait_for_busy_to_clear()?; + } + + if let Some(qer) = qer { + match qer { + QuadEnableRequirements::NoQeBit => {} + QuadEnableRequirements::QeBit6SR1 => { + if !status.maybe_quad_en() { + status.set_maybe_quad_en(true); + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_WR_STATUS, status.into()], &mut [])?; + } + } + _ => { + self.config.read = SfCmd::READ4B; + } + } + } + + // TODO: Consider different options: + // Enter 4-byte mode and use READ4B_GLOBAL vs Use READ4B if available. + if config.size.get() > MAX_3B_SIZE { + self.enter_4byte_mode()?; + } + + self.config = config; + self.initialized = true; + Ok(()) + } + + fn enter_4byte_mode(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.spi + .write(&[OP_ENTER_4B_ADDR_MODE]) + .map_err(|_| error::FLASH_GENERIC_BUSY)?; + Ok(()) + } + + pub fn reset_device(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_RESET_ENABLE], &mut [])?; + self.transfer_req_resp(&[OP_RESET], &mut [])?; + Ok(()) + } + + pub fn read_jedec_id(&mut self, buf: &mut [u8]) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_READ_JEDEC_ID], buf) + } + + pub fn config(&self) -> &SpiFlashConfig { + &self.config + } + + pub fn set_ear(&mut self, bank: u8) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_WR_EAR, bank], &mut [])?; + Ok(()) + } + + /// Erase the entire chip. + pub fn erase_all(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_CHIP_ERASE], &mut [])?; + self.wait_for_busy_to_clear() + } + + fn wait_for_busy_to_clear(&mut self) -> Result<(), ErrorCode> { + let mut status = Status::new_zeroed(); + loop { + self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes())?; + if !status.busy() { + return Ok(()); + } + } + } + + fn erase_cmd(&mut self, start_addr: usize, cmd: SfCmd) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + + let mut buf = [0_u8; MAX_PREFIX_LEN]; + let op = cmd + .addr_mode + .write_prefix(&mut buf, cmd.opcode, start_addr)?; + self.transfer_req_resp(op, &mut [])?; + + self.wait_for_busy_to_clear() + } + + fn transfer_req_resp(&mut self, req: &[u8], resp: &mut [u8]) -> Result<(), ErrorCode> { + if resp.is_empty() { + self.spi.write(req).map_err(|_| error::FLASH_GENERIC_BUSY) + } else { + let mut ops = [Operation::Write(req), Operation::Read(resp)]; + self.spi + .transaction(&mut ops) + .map_err(|_| error::FLASH_GENERIC_BUSY) + } + } + + fn check_valid_size(&self, start_addr: usize, len: usize) -> Result<(), ErrorCode> { + let Some(end_addr) = start_addr.checked_add(len) else { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + }; + if end_addr > self.config.size.get() { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + Ok(()) + } + + pub fn is_busy(&mut self) -> bool { + let mut status = Status::new_zeroed(); + match self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes()) { + Ok(_) => status.busy(), + Err(_) => true, + } + } + + pub fn complete_op(&mut self) -> Result<(), ErrorCode> { + Ok(()) + } +} + +impl FlashTrait for SpiFlash { + type Error = ErrorCode; + + fn geometry(&mut self) -> Result<(NonZero, PowerOf2Usize, u32), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + // Support 4KB and 64KB erases + let bitmap = (1 << SECTOR_SIZE.trailing_zeros()) | (1 << BLOCK_SIZE.trailing_zeros()); + let page_size = PowerOf2Usize::new(SECTOR_SIZE).unwrap(); + Ok((self.config.size, page_size, bitmap)) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + read_common( + &mut self.spi, + start_addr.offset() as usize, + buf, + self.config.read.opcode, + self.config.read.width, + self.config.read.addr_mode, + self.config.size.get(), + ) + } + + fn program(&mut self, start_address: FlashAddress, mut data: &[u8]) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + // A single program transaction must not span 256-byte pages; writes + // that span multiple pages must be split into multiple chunks. + const PROGRAM_PAGE_LEN: usize = 256; + + let start_addr = start_address.offset() as usize; + self.check_valid_size(start_addr, data.len())?; + + // TODO: Eliminate this buffer once the drivers support vectored I/O. + let mut buf = [0_u8; MAX_PREFIX_LEN + PROGRAM_PAGE_LEN]; + + let mut addr = start_addr; + while !data.is_empty() { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + let prefix_len = self + .config + .program + .addr_mode + .write_prefix( + <&mut [u8; MAX_PREFIX_LEN]>::try_from(&mut buf[..MAX_PREFIX_LEN]).unwrap(), + self.config.program.opcode, + addr, + )? + .len(); + + let chunk_len = min(data.len(), PROGRAM_PAGE_LEN - (addr % PROGRAM_PAGE_LEN)); + buf[prefix_len..][..chunk_len].copy_from_slice(&data[..chunk_len]); + self.transfer_req_resp(&buf[..prefix_len + chunk_len], &mut [])?; + + self.wait_for_busy_to_clear()?; + data = &data[chunk_len..]; + addr += chunk_len; + } + Ok(()) + } + + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + let mut addr = start_addr.offset() as usize; + let mut len = size.get(); + + if addr % SECTOR_SIZE != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR); + } + if len % SECTOR_SIZE != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + } + self.check_valid_size(addr, len)?; + + while len > 0 { + let can_erase_block = (addr % BLOCK_SIZE == 0) && (len >= BLOCK_SIZE); + let (cmd, erased) = if can_erase_block { + (self.config.erase64k, BLOCK_SIZE) + } else { + (self.config.erase4k, SECTOR_SIZE) + }; + self.erase_cmd(addr, cmd)?; + addr += erased; + len -= erased; + } + Ok(()) + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable)] +pub struct Status { + busy: bool, + write_en: bool, + bp0: bool, + bp1: bool, + bp2: bool, + bp3: bool, + maybe_quad_en: bool, + reserved7: bool, +} + +const OP_STATUS: u8 = 0x05; +const OP_WRITE_EN: u8 = 0x06; +const OP_WR_STATUS: u8 = 0x01; +const OP_WR_EAR: u8 = 0xC5; +const OP_READ: u8 = 0x03; +const OP_QREAD: u8 = 0x6B; +const OP_READ4B: u8 = 0x13; +const OP_QREAD4B: u8 = 0x6C; +const OP_CHIP_ERASE: u8 = 0xC7; +const OP_ERASE_4K: u8 = 0x20; +const OP_ERASE4B_4K: u8 = 0x21; +const OP_ERASE_64K: u8 = 0xD8; +const OP_ERASE4B_64K: u8 = 0xDC; +const OP_PROGRAM: u8 = 0x02; +const OP_QPROGRAM: u8 = 0x32; +const OP_PROGRAM4B: u8 = 0x12; +const OP_QPROGRAM4B: u8 = 0x34; +const OP_SFDP_READ: u8 = 0x5a; +const OP_RESET_ENABLE: u8 = 0x66; +const OP_RESET: u8 = 0x99; +const OP_READ_JEDEC_ID: u8 = 0x9f; +const OP_ENTER_4B_ADDR_MODE: u8 = 0xB7; + +/// A RandomRead implementation that can be used to access SFDP bytes. +pub struct SfdpRandRead<'a, S: embedded_hal::spi::SpiDevice> { + spi: &'a mut S, +} + +impl<'a, S: embedded_hal::spi::SpiDevice> SfdpRandRead<'a, S> { + pub fn new(spi: &'a mut S) -> Self { + Self { spi } + } +} + +const SFDP_MEM_SIZE: usize = 1 << 24; + +impl RandomRead for SfdpRandRead<'_, S> { + type Error = ErrorCode; + fn read(&mut self, start_addr: usize, buf: &mut [u8]) -> Result<(), Self::Error> { + read_common( + self.spi, + start_addr, + buf, + OP_SFDP_READ, + SpiTxnWidth::STANDARD, + AddressingMode::_3ByteWithDummy, + SFDP_MEM_SIZE, + ) + } + fn size(&mut self) -> Result { + Ok(SFDP_MEM_SIZE) + } +} + +fn read_common( + spi: &mut S, + start_addr: usize, + buf: &mut [u8], + opcode: u8, + _width: SpiTxnWidth, + addr_size: AddressingMode, + src_total_len: usize, +) -> Result<(), ErrorCode> { + let Some(end_addr) = start_addr.checked_add(buf.len()) else { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + }; + if end_addr > src_total_len { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let mut cmd_buf = [0; MAX_PREFIX_LEN]; + let cmd_buf = addr_size.write_prefix(&mut cmd_buf, opcode, start_addr)?; + + let mut ops = [Operation::Write(cmd_buf), Operation::Read(buf)]; + spi.transaction(&mut ops) + .map_err(|_| error::FLASH_GENERIC_BUSY) +} + +const _: () = assert!( + size_of::() >= size_of::(), + "on supported platforms, usize must be at least 32-bits" +); + +/// Describes how addresses should be formatting on the wire +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressingMode { + _3Byte = 0, + _4Byte = 1, + _3ByteWithDummy = 2, + _4ByteWithDummy = 3, +} + +impl AddressingMode { + /// Writes the opcode, address and (if needed) dummy byte to `buf`, and + /// returns a slice to the part of `buf` that should be sent as the initial + /// bytes of the transaction. + #[inline] + fn write_prefix( + self, + buf: &mut [u8; MAX_PREFIX_LEN], + opcode: u8, + addr: usize, + ) -> Result<&[u8], ErrorCode> { + buf[0] = opcode; + if !self.is_valid_addr(addr) { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let shift = if matches!(self, Self::_3Byte | Self::_3ByteWithDummy) { + 8 + } else { + 0 + }; + let addr_bytes = u32::try_from(addr << shift).unwrap().to_be_bytes(); + *<&mut [u8; 4]>::try_from(&mut buf[1..5]).unwrap() = addr_bytes; + let len = match self { + Self::_3Byte => 4, + Self::_4Byte => 5, + Self::_3ByteWithDummy => 5, + Self::_4ByteWithDummy => { + buf[5] = 0; + 6 + } + }; + Ok(&buf[..len]) + } + + /// Returns true if `addr` can be represented fully by this address mode. + #[inline] + fn is_valid_addr(self, addr: usize) -> bool { + if addr < MAX_3B_SIZE { + return true; + } + match self { + Self::_3Byte | Self::_3ByteWithDummy => addr < MAX_3B_SIZE, + Self::_4Byte | Self::_4ByteWithDummy => u32::try_from(addr).is_ok(), + } + } +} + +#[cfg(test)] +mod test { + extern crate std; + use super::*; + use drivers_mock_spi_device_fake::*; + use std::vec; + use std::vec::Vec; + use util_sfdp::*; + + const GIB: usize = 1024 * MIB; + + const STATUS_WIP_WEL: u8 = 0x03; + const STATUS_WIP: u8 = 0x01; + const STATUS_READY: u8 = 0x00; + // Note: Some parts use a different bit position. + // See "6.4.18 JEDEC Basic Flash Parameter Table: 15th DWORD" of JESD216 and + // `QuadEnableRequirements`. + // FIXME: STATUS_QE is currently unused because QSPI dynamic initialization is disabled. + #[allow(dead_code)] + const STATUS_QE: u8 = 0x40; + + fn preprogram_init(fake_spi: &FakeSpiDevice, size_bytes: usize) { + let sfdp = gen_sfdp(size_bytes); + preprogram_sfdp(fake_spi, &sfdp); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + if size_bytes > 16 * MIB { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_ENTER_4B_ADDR_MODE].into(), vec![].into()); + } + } + + /// Generates some generic SFDP for a flash of a specific size (in bytes). + fn gen_sfdp(flash_total_len: usize) -> Vec { + const SFDP_HEADER: SfdpHeader = SfdpHeader { + sig: SfdpSignature::EXPECTED_VALUE, + major_rev: 1, + minor_rev: 0, + access_protocol: AccessProtocol::LEGACY, + num_parameter_header: 0, // 0-based => 0 means 1 header + }; + const SFDP_PARAMETER_HEADER: ParameterHeader = ParameterHeader { + parameter_id_lsb: 0x00, + major_rev: 1, + minor_rev: 0, + len_in_dwords: 23, + ptr: U24::new(16), + parameter_id_msb: 0xff, + }; + let mut result = vec![]; + result.extend_from_slice(SFDP_HEADER.as_bytes()); + result.extend_from_slice(SFDP_PARAMETER_HEADER.as_bytes()); + let mut bpt = BasicFlashParameterTable::new_zeroed(); + bpt.table_jesd216.memory_density = + MemoryDensity::from_byte_len(u32::try_from(flash_total_len).unwrap()).unwrap(); + //FIXME: Add test cases for the rest of the SFDP Quad support variations + bpt.table_jesd216.word1.set_supports_1s_1s_4s_read(true); + bpt.table_jesd216a + .word15 + .set_quad_enable_requirements(QuadEnableRequirements::QeBit6SR1); + result.extend_from_slice(bpt.as_bytes()); + result + } + + fn preprogram_sfdp(fake_spi: &FakeSpiDevice, sfdp_bytes: &[u8]) { + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 0, 0].into(), + sfdp_bytes[..8].to_vec().into(), + ); + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 8, 0].into(), + sfdp_bytes[8..16].to_vec().into(), + ); + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 16, 0].into(), + sfdp_bytes[16..].to_vec().into(), + ); + } + + #[test] + fn test_size_8mb() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 8 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + assert_eq!(8 * MIB, flash.geometry().unwrap().0.get()); + assert_eq!(8 * MIB, flash.random_reader().size().unwrap()); + fake_spi.assert_all_expectations_met(); + } + #[test] + fn test_size_128mb() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + assert_eq!(128 * MIB, flash.geometry().unwrap().0.get()); + assert_eq!(128 * MIB, flash.random_reader().size().unwrap()); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response( + (&[OP_READ, 0x12, 0x34, 0x56]).into(), + b"Hello World!".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + let mut buf = [0x55_u8; 12]; + flash + .read(FlashAddress::new(0x12_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello World!"); + assert_eq!( + flash.read(FlashAddress::new(0x1234_5678_u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + assert_eq!( + flash.read(FlashAddress::new((8 * MIB) as u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_4b() { + let fake_spi = FakeSpiDevice::new(); + // 32 MiB flash + preprogram_init(&fake_spi, 32 * MIB); + fake_spi + .preprogram_data_response((&[OP_READ, 0x01, 0x23, 0x45, 0x67]).into(), b"World".into()); + fake_spi + .preprogram_data_response((&[OP_READ, 0x00, 0x12, 0x34, 0x56]).into(), b"Hello".into()); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + let mut buf = [0x55_u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + flash + .read(FlashAddress::new(0x0123_4567_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"World"); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_4b_qspi() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + // Expect OP_QREAD4B + 4B Address + 1 Dummy Byte (0x00) + fake_spi.preprogram_data_response( + vec![OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00].into(), + b"Hello".to_vec().into(), + ); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD4B; + + let mut buf = [0u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + fake_spi.assert_all_expectations_met(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qread_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response( + (&[OP_QREAD, 0x12, 0x34, 0x56, 0x00]).into(), + b"Hello World!".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD; + let mut buf = [0x55_u8; 12]; + flash + .read(FlashAddress::new(0x12_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello World!"); + assert_eq!( + flash.read(FlashAddress::new(0x1234_5678_u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + assert_eq!( + flash.read(FlashAddress::new((8 * MIB) as u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qread_4b() { + let fake_spi = FakeSpiDevice::new(); + // 32 MiB flash + preprogram_init(&fake_spi, 32 * MIB); + fake_spi.preprogram_data_response( + (&[OP_QREAD4B, 0x01, 0x23, 0x45, 0x67, 0x00]).into(), + b"World".into(), + ); + fake_spi.preprogram_data_response( + (&[OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00]).into(), + b"Hello".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD4B; + let mut buf = [0x55_u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + flash + .read(FlashAddress::new(0x0123_4567_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"World"); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_3b() { + let fake_spi = FakeSpiDevice::new(); + // 16 MiB flash + preprogram_init(&fake_spi, 16 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0xba, 0x10, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP_WEL]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0xba_1000_u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 5..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase + tx: vec![OP_ERASE_4K, 0xba, 0x10, 0b00], + rx: vec![0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=1, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP_WEL], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001_u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((16 * MIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_4b() { + let fake_spi = FakeSpiDevice::new(); + // 1 GiB flash + preprogram_init(&fake_spi, GIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_4K, 0x1a, 0x5e, 0xb0, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP_WEL]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x1a5e_b000_u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase (4-byte addr) + tx: vec![OP_ERASE_4K, 0x1a, 0x5e, 0xb0, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=1, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP_WEL], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001_u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((GIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_last_page() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0x7f, 0xf0, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((8 * MIB - 4096) as u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase (4-byte addr) + tx: vec![OP_ERASE_4K, 0x7f, 0xf0, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + + assert_eq!( + flash.erase( + FlashAddress::new((8 * MIB - 1) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((8 * MIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_page_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + const ADDRESS: usize = 0x1000; + const LEN: usize = 4 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0x00, 0x10, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x10, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_page_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + const ADDRESS: usize = 0x1000; + const LEN: usize = 4 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_4K, 0x00, 0x00, 0x10, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x00, 0x10, 0x00], + rx: vec![0, 0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_block_64k_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + const ADDRESS: usize = 0x10000; + const LEN: usize = 64 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_64K, 0x01, 0x00, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_64K, 0x01, 0x00, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_block_64k_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + const ADDRESS: usize = 0x01230000; + const LEN: usize = 64 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_64K, 0x01, 0x23, 0x00, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_64K, 0x01, 0x23, 0x00, 0x00], + rx: vec![0, 0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_mixed_granularity_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Erase sequence: 15 pages, 1 block, 1 page. + const ADDRESS: usize = 4 * KIB; + const LEN: usize = 128 * KIB; + + let mut expectations = Vec::new(); + for i in 1..=15 { + expectations.push((OP_ERASE_4K, 0x00, (i * 0x10) as u8, 0x00)); + } + expectations.push((OP_ERASE_64K, 0x01, 0x00, 0x00)); + expectations.push((OP_ERASE_4K, 0x02, 0x00, 0x00)); + + for (op, a1, a2, a3) in expectations.clone() { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![op, a1, a2, a3].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, a1, a2, a3)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + assert_eq!(log[offset + 1].tx, vec![*op, *a1, *a2, *a3]); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_mixed_granularity_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + // Erase sequence: 1 page, 1 block, 15 pages. + const ADDRESS: usize = MAX_3B_SIZE - 4 * KIB; + const LEN: usize = 128 * KIB; + + let mut expectations = Vec::new(); + expectations.push((OP_ERASE_4K, 0x00, 0xff, 0xf0, 0x00)); + expectations.push((OP_ERASE_64K, 0x01, 0x00, 0x00, 0x00)); + for i in 0..15 { + expectations.push((OP_ERASE_4K, 0x01, 0x01, (i * 0x10) as u8, 0x00)); + } + + for (op, a1, a2, a3, a4) in expectations.clone() { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![op, a1, a2, a3, a4].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, a1, a2, a3, a4)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + assert_eq!(log[offset + 1].tx, vec![*op, *a1, *a2, *a3, *a4]); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_alignment_errors() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + // Start address not page aligned + assert_eq!( + flash.erase(FlashAddress::new(1_u32), PowerOf2Usize::new(4096).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + + // Length not page aligned + assert_eq!( + flash.erase(FlashAddress::new(0_u32), PowerOf2Usize::new(1).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_out_of_bounds() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((16 * MIB - 4 * KIB) as u32), + PowerOf2Usize::new(8 * KIB).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + + // 4B flash (32 MiB) + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((32 * MIB) as u32), + PowerOf2Usize::new(4 * KIB).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_entire_flash_3b() { + let fake_spi = FakeSpiDevice::new(); + const SIZE: usize = 128 * KIB; + preprogram_init(&fake_spi, SIZE); + + let expectations = [ + (OP_ERASE_64K, vec![0x00, 0x00, 0x00]), + (OP_ERASE_64K, vec![0x01, 0x00, 0x00]), + ]; + + for (op, addr_bytes) in &expectations { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + let mut tx = vec![*op]; + tx.extend_from_slice(addr_bytes); + fake_spi.preprogram_data_response(tx.into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0_u32), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, addr_bytes)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + let mut expected_tx = vec![*op]; + expected_tx.extend_from_slice(addr_bytes); + assert_eq!(log[offset + 1].tx, expected_tx); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_all() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * KIB); + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_CHIP_ERASE]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.erase_all().unwrap(); + + let log = fake_spi.log(); + let log = &log[(log.len() - 4)..]; + assert_eq!( + log, + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_CHIP_ERASE], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, 0x01] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, 0x00] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_entire_flash_4b() { + let fake_spi = FakeSpiDevice::new(); + const SIZE: usize = 32 * MIB; + preprogram_init(&fake_spi, SIZE); + + const NUM_BLOCKS: usize = SIZE / (64 * KIB); + for i in 0..NUM_BLOCKS { + let addr = (i * 64 * KIB) as u32; + let addr_bytes = addr.to_be_bytes(); + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + let mut tx = vec![OP_ERASE_64K]; + tx.extend_from_slice(&addr_bytes); + fake_spi.preprogram_data_response(tx.into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0_u32), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * NUM_BLOCKS..]; + for i in 0..NUM_BLOCKS { + let addr = (i * 64 * KIB) as u32; + let addr_bytes = addr.to_be_bytes(); + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + let mut expected_tx = vec![OP_ERASE_64K]; + expected_tx.extend_from_slice(&addr_bytes); + assert_eq!(log[offset + 1].tx, expected_tx); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_multiple_4k_pages_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Erase sequence: four 4K pages. + const ADDRESS: usize = 0x1000; + const LEN: usize = 16 * KIB; + + for i in 1..=4 { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response( + vec![OP_ERASE_4K, 0x00, (i * 0x10) as u8, 0x00].into(), + vec![].into(), + ); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 12..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x10, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x20, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x30, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x40, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_block_aligned_small_len_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Address is 64K aligned, but length is < 64K. Should use 4K erases. + const ADDRESS: usize = 0x10000; + const LEN: usize = 8 * KIB; + + for i in 0..2 { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response( + vec![OP_ERASE_4K, 0x01, (i * 0x10) as u8, 0x00].into(), + vec![].into(), + ); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 6..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x01, 0x00, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x01, 0x10, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_1140_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_4b() { + let fake_spi = FakeSpiDevice::new(); + // 128 MiB flash + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x02, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program( + FlashAddress::new(0x0274_1140_u32), + &[0xba, 0x5e, 0xba, 0x11], + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x02, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_1140_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_QPROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_4b() { + let fake_spi = FakeSpiDevice::new(); + // 128 MiB flash + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[ + OP_QPROGRAM4B, + 0x02, + 0x74, + 0x11, + 0x40, + 0xba, + 0x5e, + 0xba, + 0x11, + ]) + .into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM4B; + flash + .program( + FlashAddress::new(0x0274_1140_u32), + &[0xba, 0x5e, 0xba, 0x11], + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![ + OP_QPROGRAM4B, + 0x02, + 0x74, + 0x11, + 0x40, + 0xba, + 0x5e, + 0xba, + 0x11 + ], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_last_byte() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi + .preprogram_data_response((&[OP_PROGRAM, 0x7f, 0xff, 0xff, 0x42]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x7f_ffff_u32), &[0x42]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x7f, 0xff, 0xff, 0x42], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + // Programming past the end of the flash should result in an error + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff_u32), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_spans_pages() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi + .preprogram_data_response((&[OP_PROGRAM, 0x74, 0x12, 0x00, 0x11]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_11fd_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 8..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program end of first page + tx: vec![OP_PROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program start of next page + tx: vec![OP_PROGRAM, 0x74, 0x12, 0x00, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_last_byte() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x7f, 0xff, 0xff, 0x42]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x7f_ffff_u32), &[0x42]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_QPROGRAM, 0x7f, 0xff, 0xff, 0x42], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + // Programming past the end of the flash should result in an error + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff_u32), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_spans_pages() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x12, 0x00, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_11fd_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 8..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program end of first page + tx: vec![OP_QPROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program start of next page + tx: vec![OP_QPROGRAM, 0x74, 0x12, 0x00, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_set_ear() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_WR_EAR, 0x02]).into(), (&[]).into()); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.set_ear(0x02).unwrap(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn address_size_is_valid_addr() { + assert!(AddressingMode::_3Byte.is_valid_addr(0)); + assert!(AddressingMode::_3Byte.is_valid_addr(MAX_3B_SIZE - 1)); + assert!(!AddressingMode::_3Byte.is_valid_addr(MAX_3B_SIZE)); + + assert!(AddressingMode::_4Byte.is_valid_addr(MAX_3B_SIZE)); + assert!(AddressingMode::_4Byte.is_valid_addr(0xffff_ffff)); + + #[cfg(target_pointer_width = "8")] + assert!(!AddressingMode::_4Byte.is_valid_addr(0x1_0000_0000)); + + #[cfg(target_pointer_width = "8")] + assert!(!AddressingMode::_4Byte.is_valid_addr(0xffff_ffff_ffff_ffff)); + } + + #[test] + fn address_size_write_prefix() { + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56].as_slice()), + AddressingMode::_3Byte.write_prefix(&mut buf, OP_READ, 0x12_3456) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x00].as_slice()), + AddressingMode::_3ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x12_3456) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x78].as_slice()), + AddressingMode::_4Byte.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x78, 0x00].as_slice()), + AddressingMode::_4ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + + assert_eq!( + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS), + AddressingMode::_3Byte.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + assert_eq!( + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS), + AddressingMode::_3ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + } +} diff --git a/drivers/mock/BUILD.bazel b/drivers/mock/BUILD.bazel new file mode 100644 index 00000000..bccfc1d4 --- /dev/null +++ b/drivers/mock/BUILD.bazel @@ -0,0 +1,21 @@ +# 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 = "spi_device_fake", + srcs = ["spi_device_fake.rs"], + crate_name = "drivers_mock_spi_device_fake", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "@rust_crates//:embedded-hal", + ], +) + +rust_test( + name = "spi_device_fake_test", + crate = ":spi_device_fake", +) diff --git a/drivers/mock/spi_device_fake.rs b/drivers/mock/spi_device_fake.rs new file mode 100644 index 00000000..ed1d4c5a --- /dev/null +++ b/drivers/mock/spi_device_fake.rs @@ -0,0 +1,320 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +extern crate std; + +use std::borrow::Cow; +use std::cell::Ref; +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use util_error::ErrorCode; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FakeSpiError(pub ErrorCode); + +impl embedded_hal::spi::Error for FakeSpiError { + fn kind(&self) -> embedded_hal::spi::ErrorKind { + embedded_hal::spi::ErrorKind::Other + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FakeSpiTransfer { + pub tx: Vec, + pub rx: Vec, +} + +pub struct ResponseProgram<'a, TError> { + pub prefix: Cow<'a, [u8]>, + pub response: Response<'a, TError>, +} + +pub enum Response<'a, TError> { + Data(Cow<'a, [u8]>), + Error(TError), +} + +#[derive(Clone)] +pub struct FakeSpiDevice<'a> { + log: Rc>>, + response_programs: Rc>>>, +} + +impl<'a> FakeSpiDevice<'a> { + pub fn new() -> Self { + Self { + log: Rc::new(RefCell::new(vec![])), + response_programs: Rc::new(RefCell::new(vec![])), + } + } + + /// Adds a programmed response to the fake device. If the SPI request + /// matches `prefix`, then `response` will be received after `prefix.len()` + /// zeroes. + pub fn preprogram_data_response(&self, prefix: Cow<'a, [u8]>, response: Cow<'a, [u8]>) { + self.response_programs.borrow_mut().push(ResponseProgram { + prefix, + response: Response::Data(response), + }) + } + + /// Adds a programmed error response to the fake device. If the SPI request + /// matches `prefix`, then the supplied error will be returned. + pub fn preprogram_error_response(&self, prefix: Cow<'a, [u8]>, err: ErrorCode) { + self.response_programs.borrow_mut().push(ResponseProgram { + prefix, + response: Response::Error(FakeSpiError(err)), + }) + } + + pub fn log(&self) -> impl Deref + use<'a, '_> { + Ref::map(self.log.borrow(), |log| log.as_slice()) + } + + pub fn assert_all_expectations_met(&self) { + assert!( + self.response_programs.borrow().is_empty(), + "Not all expected SPI response programs were executed: remaining = {}", + self.response_programs.borrow().len() + ); + } +} + +impl Default for FakeSpiDevice<'_> { + fn default() -> Self { + Self::new() + } +} + +impl embedded_hal::spi::ErrorType for FakeSpiDevice<'_> { + type Error = FakeSpiError; +} + +impl embedded_hal::spi::SpiDevice for FakeSpiDevice<'_> { + fn transaction( + &mut self, + operations: &mut [embedded_hal::spi::Operation<'_, u8>], + ) -> Result<(), Self::Error> { + let mut tx = Vec::new(); + let mut read_ops = Vec::new(); + + for op in operations.iter_mut() { + match op { + embedded_hal::spi::Operation::Write(buf) => { + tx.extend_from_slice(buf); + } + embedded_hal::spi::Operation::Read(buf) => { + read_ops.push(buf); + } + embedded_hal::spi::Operation::Transfer(read_buf, write_buf) => { + tx.extend_from_slice(write_buf); + read_ops.push(read_buf); + } + embedded_hal::spi::Operation::TransferInPlace(buf) => { + tx.extend_from_slice(buf); + // TransferInPlace is both write and read. In SpiFlash this is not typically used, + // but we treat it as read mapping to the same buffer. + read_ops.push(buf); + } + embedded_hal::spi::Operation::DelayNs(_) => {} + } + } + + let mut response_programs = self.response_programs.borrow_mut(); + let Some(response_index) = response_programs + .iter() + .position(|resp| tx.starts_with(&resp.prefix)) + else { + panic!("Unexpected SPI transaction: tx = {tx:x?}"); + }; + + let program = response_programs.remove(response_index); + + match program.response { + Response::Data(data) => { + let skipped_resp_bytes = tx.len() - program.prefix.len(); + let total_rx_requested: usize = read_ops.iter().map(|buf| buf.len()).sum(); + + if data.len() != total_rx_requested + skipped_resp_bytes { + panic!( + "Expected transaction with prefix {:x?} to return {} bytes, but a response of len {} was requested ({} bytes skipped). Simulated data len = {}", + program.prefix, + data.len(), + total_rx_requested, + skipped_resp_bytes, + data.len() + ); + } + + let mut data_offset = skipped_resp_bytes; + for buf in read_ops { + let len = buf.len(); + buf.copy_from_slice(&data[data_offset..data_offset + len]); + data_offset += len; + } + + let mut rx = vec![0; program.prefix.len()]; + rx.extend_from_slice(&data); + + let mut logged_tx = tx.clone(); + if logged_tx.len() < rx.len() { + logged_tx.resize(rx.len(), 0); + } + + self.log + .borrow_mut() + .push(FakeSpiTransfer { tx: logged_tx, rx }); + Ok(()) + } + Response::Error(err) => Err(err), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use embedded_hal::spi::{Operation, SpiDevice}; + use util_error::FLASH_GENERIC_BUSY; + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_no_expectations() { + let mut spi = FakeSpiDevice::new(); + let mut rx = [0; 4]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_no_matching_expectations() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hola".into(), b"adios".into()); + let mut rx = [0; 4]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + fn test_matching_expectations_exact_req_len() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hola".into(), b"adios".into()); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + + let mut rx1 = [0; 7]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx1)]) + .unwrap(); + assert_eq!(b"goodbye", &rx1); + + let mut rx2 = [0; 5]; + spi.transaction(&mut [Operation::Write(b"hola"), Operation::Read(&mut rx2)]) + .unwrap(); + assert_eq!(b"adios", &rx2); + + assert_eq!( + *spi.log(), + [ + FakeSpiTransfer { + tx: b"hi\0\0\0\0\0\0\0".into(), + rx: b"\0\0goodbye".into(), + }, + FakeSpiTransfer { + tx: b"hola\0\0\0\0\0".into(), + rx: b"\0\0\0\0adios".into(), + } + ] + ); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_matching_expectations_nonexact_req_len() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + let mut rx = [0; 6]; + spi.transaction(&mut [Operation::Write(b"hi!"), Operation::Read(&mut rx)]) + .unwrap(); + assert_eq!(b"oodbye", &rx); + assert_eq!( + *spi.log(), + [FakeSpiTransfer { + tx: b"hi!\0\0\0\0\0\0".into(), + rx: b"\0\0goodbye".into(), + }] + ); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_matching_expectations_error() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_error_response(b"hi".into(), FLASH_GENERIC_BUSY); + let mut rx = [0; 6]; + assert_eq!( + spi.transaction(&mut [Operation::Write(b"hi!"), Operation::Read(&mut rx)]), + Err(FakeSpiError(FLASH_GENERIC_BUSY)) + ); + } + + #[test] + #[should_panic( + expected = "Expected transaction with prefix [68, 69] to return 7 bytes, but a response of len 5 was requested" + )] + fn test_wrong_response_size() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + fn test_transfer_success() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"hello".into()); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx, b"hi")]) + .unwrap(); + assert_eq!(b"hello", &rx); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_transfer_multi_success() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"hello".into()); + spi.preprogram_data_response(b"bye".into(), b"world".into()); + let mut rx1 = [0; 5]; + let mut rx2 = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx1, b"hi")]) + .unwrap(); + spi.transaction(&mut [Operation::Transfer(&mut rx2, b"bye")]) + .unwrap(); + assert_eq!(b"hello", &rx1); + assert_eq!(b"world", &rx2); + spi.assert_all_expectations_met(); + } + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_transfer_panic_on_missing_program() { + let mut spi = FakeSpiDevice::new(); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx, b"hi")]) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "Not all expected SPI response programs were executed: remaining = 1" + )] + fn test_unmet_expectation_panics() { + let spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"unused_cmd".into(), b"data".into()); + // We do NOT call transaction() to execute this preprogrammed response. + // Calling assert_all_expectations_met() must panic! + spi.assert_all_expectations_met(); + } +} diff --git a/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel b/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel new file mode 100644 index 00000000..793caf58 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel @@ -0,0 +1,107 @@ +# 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") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "spi_flash", + srcs = [ + "spi_flash.rs", + ], + codegen_crate_name = "spi_flash_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:spi_host", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +system_image( + name = "spi_flash_image", + apps = [ + ":spi_flash", + ], + 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 = "spi_flash_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash_image", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash_image", +) diff --git a/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs b/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs new file mode 100644 index 00000000..5276e8ca --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs @@ -0,0 +1,118 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Smoke test for generic SPI Flash driver on Earlgrey. +//! +//! Initializes SPI Host, SpiFlash driver, wraps them in BlockingFlash, +//! and performs Erase -> Program -> Read -> Verify cycle. + +#![no_std] +#![no_main] + +use hal_flash::Flash; +use hal_flash_driver::FlashAddress; +use pw_status::{Error, Result}; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; +use userspace::entry; + +fn run_test() -> Result<()> { + // 1. Initialize SPI Host. + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + spi_host + .init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) + .map_err(|_| Error::Internal)?; + + // 2. Create SpiFlash driver. + let mut flash = SpiFlash::new(spi_host); + + // 3. Initialize SpiFlash driver. + flash.init().map_err(|_| Error::Internal)?; + + // 4. Print geometry. + let (size, page_size, erase_bitmap) = flash.geometry().map_err(|_| Error::Internal)?; + pw_log::info!( + "Flash geometry: size={} bytes, page_size={} bytes, erase_bitmap=0x{:x}", + size, + page_size.get(), + erase_bitmap + ); + + // 5. Test Erase -> Program -> Read -> Verify + // We use address 0x00100000 (1MB offset) which is aligned to 64KB block boundary. + let test_addr = FlashAddress::new(0x0010_0000); + + // Erase 4KB sector. + let erase_size = util_types::PowerOf2Usize::new(4096).unwrap(); + pw_log::info!("Erasing 4KB at offset {}...", test_addr); + flash + .erase(test_addr, erase_size) + .map_err(|_| Error::Internal)?; + pw_log::info!("Erase complete."); + + // Read back and verify it is erased (all bytes 0xFF). + let mut read_buf = [0u8; 256]; + flash + .read(test_addr, &mut read_buf) + .map_err(|_| Error::Internal)?; + for (i, &b) in read_buf.iter().enumerate() { + if b != 0xFF { + pw_log::error!( + "Erase verification failed at offset +{}: expected 0xFF, got 0x{:02x}", + i, + b + ); + return Err(Error::FailedPrecondition); + } + } + pw_log::info!("Erase verified (all 0xFF)."); + + // Program a page (256 bytes). + let mut write_buf = [0u8; 256]; + for (i, b) in write_buf.iter_mut().enumerate() { + *b = i as u8; + } + pw_log::info!("Programming 256 bytes at offset {}...", test_addr); + flash + .program(test_addr, &write_buf) + .map_err(|_| Error::Internal)?; + pw_log::info!("Program complete."); + + // Read back and verify program. + read_buf.fill(0); + flash + .read(test_addr, &mut read_buf) + .map_err(|_| Error::Internal)?; + for (i, (&w, &r)) in write_buf.iter().zip(read_buf.iter()).enumerate() { + if w != r { + pw_log::error!( + "Verification failed at offset +{}: wrote 0x{:02x}, read 0x{:02x}", + i, + w, + r + ); + return Err(Error::FailedPrecondition); + } + } + pw_log::info!("Program verified successfully!"); + + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + pw_log::info!("🔄 RUNNING SPI Flash Smoke Test"); + let ret = run_test(); + + if ret.is_err() { + pw_log::error!("❌ FAIL"); + } else { + pw_log::info!("✅ PASS"); + } + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/drivers/spi_flash/system.json5 b/target/earlgrey/tests/drivers/spi_flash/system.json5 new file mode 100644 index 00000000..4c9b30c3 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/system.json5 @@ -0,0 +1,41 @@ +// 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: "spi_flash", + flash_size_bytes: 24576, // Slightly larger because of SFDP parser inclusion + processes: [{ + name: "spi_flash_process", + ram_size_bytes: 8192, // Larger stack/heap just in case + objects: [ + { + type: "thread", + name: "spi_thread", + kernel_stack_size_bytes: 2048, + }, + ], + memory_mappings: [ + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/drivers/spi_flash/target.rs b/target/earlgrey/tests/drivers/spi_flash/target.rs new file mode 100644 index 00000000..de862061 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/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 SPI FLASH test"; + + 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/util/error/flash.rs b/util/error/flash.rs index 976bfb4a..c16af41a 100644 --- a/util/error/flash.rs +++ b/util/error/flash.rs @@ -36,6 +36,9 @@ pub const FLASH_GENERIC_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(8, Error /// The erase size is invalid. pub const FLASH_GENERIC_ERASE_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(9, Error::InvalidArgument); +/// The flash device is not initialized. +pub const FLASH_GENERIC_NOT_INITIALIZED: ErrorCode = + FLASH_GENERIC.from_pw(10, Error::FailedPrecondition); /// SFDP: Invalid memory density. pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode = From e04124d2d2bd72eaeb49d2bd2c098b17f9420e1c Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 14 Jul 2026 08:09:48 +0800 Subject: [PATCH 2/2] earlgrey/tests: Add SPI Flash IPC integration test --- target/earlgrey/tests/spi_flash/BUILD.bazel | 148 ++++++++++++++++++ .../tests/spi_flash/combined_flash_server.rs | 145 +++++++++++++++++ .../tests/spi_flash/spi_flash_test.rs | 110 +++++++++++++ target/earlgrey/tests/spi_flash/system.json5 | 98 ++++++++++++ target/earlgrey/tests/spi_flash/target.rs | 29 ++++ 5 files changed, 530 insertions(+) create mode 100644 target/earlgrey/tests/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/spi_flash/combined_flash_server.rs create mode 100644 target/earlgrey/tests/spi_flash/spi_flash_test.rs create mode 100644 target/earlgrey/tests/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/spi_flash/target.rs diff --git a/target/earlgrey/tests/spi_flash/BUILD.bazel b/target/earlgrey/tests/spi_flash/BUILD.bazel new file mode 100644 index 00000000..114bc229 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/BUILD.bazel @@ -0,0 +1,148 @@ +# 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("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "combined_flash_server", + srcs = [ + "combined_flash_server.rs", + ], + codegen_crate_name = "combined_flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/registers:spi_host", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +rust_app( + name = "spi_flash_test", + srcs = [ + "spi_flash_test.rs", + ], + codegen_crate_name = "spi_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/error", + "//util/ipc", + "//util/misc", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "spi_flash", + apps = [ + ":combined_flash_server", + ":spi_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 = "spi_flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash", +) + +rust_binary_no_panics_test( + name = "no_panics", + apps = [ + "combined_flash_server", + "spi_flash_test", + ], + binary = ":spi_flash", +) diff --git a/target/earlgrey/tests/spi_flash/combined_flash_server.rs b/target/earlgrey/tests/spi_flash/combined_flash_server.rs new file mode 100644 index 00000000..921c297c --- /dev/null +++ b/target/earlgrey/tests/spi_flash/combined_flash_server.rs @@ -0,0 +1,145 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use combined_flash_server_codegen::{handle, signals}; +use earlgrey_util::EarlgreyFlashAddress; +use eflash_driver::{EmbeddedFlash, Permission}; +use hal_flash::BlockingFlash; +use hal_flash_driver::FlashAddress; +use pw_status::Error; +use services_flash_server::FlashIpcServer; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; +use userspace::time::Instant; +use userspace::{entry, syscall}; +use util_error::ErrorCode; +use util_ipc::IpcHandle; +use util_types::Blocking; + +// EFlash Interrupt Blocker +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + if let Ok(w) = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) { + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn run_server() -> Result<(), ErrorCode> { + // 1. Initialize EFlash driver. + pw_log::info!("combined_server: initializing EFlash driver"); + // SAFETY: We have exclusive access to FlashCtrl in this test process. + let mut eflash_driver = + EmbeddedFlash::new_with_interrupts(unsafe { flash_ctrl_core::FlashCtrl::new() }); + eflash_driver.set_default_permission(Permission::FULL_ACCESS); + // Grant info page permissions as well (same as standard eflash server) + for i in 5..9 { + eflash_driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + eflash_driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + + let eflash = BlockingFlash { + driver: eflash_driver, + blocking: FlashCtrlInterrupt, + }; + let mut eflash_server = FlashIpcServer::new(eflash); + + // 2. Initialize SPI Host. + pw_log::info!("combined_server: initializing SPI Host"); + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + pw_log::error!( + "combined_server: SPI Host init failed: 0x{:x}", + u32::from(ErrorCode::from(e)) + ); + return Err(ErrorCode::from(e)); + } + + // 3. Initialize SpiFlash driver. + pw_log::info!("combined_server: initializing SpiFlash driver"); + let mut spi_flash = SpiFlash::new(spi_host); + if let Err(e) = spi_flash.init() { + pw_log::error!( + "combined_server: SPI Flash init failed: 0x{:x}", + u32::from(e) + ); + return Err(e); + } + let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + // 4. Register wait group ports. + pw_log::info!("combined_server: registering wait group ports"); + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::EFLASH_SERVICE, + syscall::Signals::READABLE, + handle::EFLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_SERVICE, + syscall::Signals::READABLE, + handle::SPI_FLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + let mut buf = [0u8; 2064]; + let eflash_ipc = IpcHandle::new(handle::EFLASH_SERVICE); + let spi_flash_ipc = IpcHandle::new(handle::SPI_FLASH_SERVICE); + + // 5. Enter main wait_group loop. + pw_log::info!("combined_server: entering main wait_group loop"); + loop { + let wait_result = syscall::object_wait( + handle::FLASH_WAIT_GROUP, + syscall::Signals::READABLE, + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + + let token = wait_result.user_data; + if token == handle::EFLASH_SERVICE as usize { + eflash_server.handle_one(&eflash_ipc, &mut buf)?; + } else if token == handle::SPI_FLASH_SERVICE as usize { + spi_flash_server.handle_one(&spi_flash_ipc, &mut buf)?; + } + } +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 COMBINED FLASH SERVER START"); + let ret = run_server(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ COMBINED FLASH SERVER PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ COMBINED FLASH SERVER FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/spi_flash_test.rs b/target/earlgrey/tests/spi_flash/spi_flash_test.rs new file mode 100644 index 00000000..49a2edb8 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/spi_flash_test.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use pw_status::Error; +use spi_flash_test_codegen::handle; +use userspace::entry; + +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::{ErrorCode, KERNEL_ERROR_INTERNAL}; +use util_ipc::IpcHandle; + +fn erase_program_test( + flash: &mut FlashIpcClient, + addr: FlashAddress, + flash_type: &str, +) -> Result<(), ErrorCode> { + let (_total_size, page_size, _erasable_sizes_bitmap) = flash.geometry()?; + pw_log::info!( + "[{}] Erasing at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.erase(addr, page_size)?; + + pw_log::info!("[{}] Reading after erase...", flash_type); + let mut buf = [0u8; 32]; + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + for &byte in buf.iter() { + if byte != 0xFF { + pw_log::error!( + "[{}] Erase check failed: byte is 0x{:02x}, expected 0xFF", + flash_type, + byte + ); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + let payload = b"Dual Flash IPC Test Payload!!! "; // 32 bytes (aligned) + pw_log::info!( + "[{}] Programming 32 bytes at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.program(addr, payload)?; + + pw_log::info!("[{}] Reading back program results...", flash_type); + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + + if buf != *payload { + pw_log::error!("[{}] Verify failed: content mismatch", flash_type); + return Err(KERNEL_ERROR_INTERNAL); + } + pw_log::info!("[{}] Program verified successfully!", flash_type); + Ok(()) +} + +fn flash_test() -> Result<(), ErrorCode> { + pw_log::info!("--- Testing Internal EFlash ---"); + let mut eflash = FlashIpcClient::new(IpcHandle::new(handle::EFLASH_SERVICE))?; + let (total_size, page_size, _) = eflash.geometry()?; + pw_log::info!( + "EFlash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on Slot B area (offset 0x90000) + erase_program_test(&mut eflash, FlashAddress::data(0x0009_0000), "EFlash")?; + + pw_log::info!("--- Testing External SPI Flash ---"); + let mut spi_flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_SERVICE))?; + let (total_size, page_size, _) = spi_flash.geometry()?; + pw_log::info!( + "SPI Flash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on 1MB offset + erase_program_test(&mut spi_flash, FlashAddress::new(0x0010_0000), "SpiFlash")?; + + Ok(()) +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 DUAL FLASH TEST CLIENT START"); + let ret = flash_test(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ DUAL FLASH TEST CLIENT PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ DUAL FLASH TEST CLIENT FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/system.json5 b/target/earlgrey/tests/spi_flash/system.json5 new file mode 100644 index 00000000..e89a685b --- /dev/null +++ b/target/earlgrey/tests/spi_flash/system.json5 @@ -0,0 +1,98 @@ +// 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: "combined_flash_server", + flash_size_bytes: 32768, + processes: [ + { + name: "combined_flash_server", + ram_size_bytes: 8192, + objects: [ + { + name: "eflash_service", + type: "channel_handler", + }, + { + name: "spi_flash_service", + type: "channel_handler", + }, + { + name: "flash_wait_group", + type: "wait_group", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + { name: "flash_ctrl_op_done", number: 164 }, + ], + }, + { + name: "server_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + }, + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + } + ], + }, + ], + }, + + { + name: "spi_flash_test", + flash_size_bytes: 16384, + processes: [ + { + name: "spi_flash_test", + ram_size_bytes: 8192, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_flash_service", + }, + { + name: "eflash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "eflash_service", + }, + { + name: "spi_test_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + }, + ], + }, + ], +} diff --git a/target/earlgrey/tests/spi_flash/target.rs b/target/earlgrey/tests/spi_flash/target.rs new file mode 100644 index 00000000..ec71c87e --- /dev/null +++ b/target/earlgrey/tests/spi_flash/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 SPI Flash test"; + + 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);