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/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); 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 =