diff --git a/drivers/flash/BUILD.bazel b/drivers/flash/BUILD.bazel new file mode 100644 index 00000000..5f5dd257 --- /dev/null +++ b/drivers/flash/BUILD.bazel @@ -0,0 +1,30 @@ +# 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", +) diff --git a/drivers/flash/spi_flash.rs b/drivers/flash/spi_flash.rs new file mode 100644 index 00000000..1d4a8fbc --- /dev/null +++ b/drivers/flash/spi_flash.rs @@ -0,0 +1,1982 @@ +// 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; + } + } + } + + if config.size.get() > 16 * MIB { + 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 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; + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum FakeSpiOp { + Write(Vec), + Read(usize), + Transaction(Vec), + } + + pub struct FakeSpiDevice { + expected_ops: Vec<(FakeSpiOp, Vec)>, + next_op_idx: usize, + } + + impl FakeSpiDevice { + pub fn new(expected: Vec<(FakeSpiOp, Vec)>) -> Self { + Self { + expected_ops: expected, + next_op_idx: 0, + } + } + + pub fn verify(&self) { + assert_eq!( + self.next_op_idx, + self.expected_ops.len(), + "Not all expected SPI operations were executed" + ); + } + } + + impl embedded_hal::spi::ErrorType for FakeSpiDevice { + type Error = core::convert::Infallible; + } + + impl embedded_hal::spi::SpiDevice for FakeSpiDevice { + fn transaction( + &mut self, + operations: &mut [embedded_hal::spi::Operation<'_, u8>], + ) -> Result<(), Self::Error> { + let mut transaction_ops = Vec::new(); + for op in operations.iter() { + match op { + embedded_hal::spi::Operation::Write(buf) => { + transaction_ops.push(FakeSpiOp::Write(buf.to_vec())); + } + embedded_hal::spi::Operation::Read(buf) => { + transaction_ops.push(FakeSpiOp::Read(buf.len())); + } + _ => unimplemented!(), + } + } + + assert!( + self.next_op_idx < self.expected_ops.len(), + "Unexpected SPI transaction" + ); + let (expected_op, response) = &self.expected_ops[self.next_op_idx]; + self.next_op_idx += 1; + + if let FakeSpiOp::Transaction(expected_sub_ops) = expected_op { + assert_eq!( + transaction_ops.len(), + expected_sub_ops.len(), + "Transaction sub-operations count mismatch" + ); + let mut resp_idx = 0; + for (i, op) in operations.iter_mut().enumerate() { + match op { + embedded_hal::spi::Operation::Write(buf) => { + if let FakeSpiOp::Write(expected_buf) = &expected_sub_ops[i] { + assert_eq!(buf, expected_buf, "Transaction write content mismatch"); + } else { + panic!("Expected Write op, got {:?}", expected_sub_ops[i]); + } + } + embedded_hal::spi::Operation::Read(buf) => { + if let FakeSpiOp::Read(expected_len) = expected_sub_ops[i] { + assert_eq!( + buf.len(), + expected_len, + "Transaction read length mismatch" + ); + let chunk = &response[resp_idx..resp_idx + expected_len]; + buf.copy_from_slice(chunk); + resp_idx += expected_len; + } else { + panic!("Expected Read op, got {:?}", expected_sub_ops[i]); + } + } + _ => unimplemented!(), + } + } + } else { + panic!("Expected transaction op, got {:?}", expected_op); + } + + Ok(()) + } + + fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + assert!( + self.next_op_idx < self.expected_ops.len(), + "Unexpected SPI write" + ); + let (expected_op, _) = &self.expected_ops[self.next_op_idx]; + self.next_op_idx += 1; + + if let FakeSpiOp::Write(expected_buf) = expected_op { + assert_eq!(buf, expected_buf, "Write content mismatch"); + } else { + panic!("Expected Write, got {:?}", expected_op); + } + Ok(()) + } + + fn read(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { + assert!( + self.next_op_idx < self.expected_ops.len(), + "Unexpected SPI read" + ); + let (expected_op, response) = &self.expected_ops[self.next_op_idx]; + self.next_op_idx += 1; + + if let FakeSpiOp::Read(expected_len) = expected_op { + assert_eq!(buf.len(), *expected_len, "Read length mismatch"); + buf.copy_from_slice(response); + } else { + panic!("Expected Read, got {:?}", expected_op); + } + Ok(()) + } + } + + /// 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 + } + + /// Configure the expected operations to playback the expected SFDP read pattern + /// when requested. + fn preprogram_sfdp(expected_ops: &mut Vec<(FakeSpiOp, Vec)>, size_bytes: usize) { + let sfdp = gen_sfdp(size_bytes); + expected_ops.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_SFDP_READ, 0, 0, 0, 0]), + FakeSpiOp::Read(8), + ]), + sfdp[..8].to_vec(), + )); + expected_ops.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_SFDP_READ, 0, 0, 8, 0]), + FakeSpiOp::Read(8), + ]), + sfdp[8..16].to_vec(), + )); + expected_ops.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_SFDP_READ, 0, 0, 16, 0]), + FakeSpiOp::Read(92), + ]), + sfdp[16..].to_vec(), + )); + } + + /// Configure the expected operations to playback the entire initialization sequence + /// (SFDP read, status register check, and optional 4-byte address mode transition). + fn preprogram_init(expected_ops: &mut Vec<(FakeSpiOp, Vec)>, size_bytes: usize) { + preprogram_sfdp(expected_ops, size_bytes); + expected_ops.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_READY], + )); + if size_bytes > 16 * MIB { + expected_ops.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected_ops.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + } + } + + #[test] + fn test_size_8mb() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 8 * MIB); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + assert_eq!(flash.config.size.get(), 8 * MIB); + flash.spi.verify(); + } + + #[test] + fn test_size_128mb() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 128 * MIB); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + assert_eq!(flash.config.size.get(), 128 * MIB); + flash.spi.verify(); + } + + #[test] + fn test_read_3b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_READ, 0x12, 0x34, 0x56]), + FakeSpiOp::Read(12), + ]), + b"Hello World!".to_vec(), + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + let mut buf = [0u8; 12]; + flash.read(FlashAddress::new(0x12_3456), &mut buf).unwrap(); + assert_eq!(&buf, b"Hello World!"); + + // Out of bounds tests + assert_eq!( + flash.read(FlashAddress::new((8 * MIB - 6) as 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) + ); + + flash.spi.verify(); + } + + #[test] + fn test_read_4b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 32 * MIB); + + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_READ, 0x00, 0x12, 0x34, 0x56]), + FakeSpiOp::Read(5), + ]), + b"Hello".to_vec(), + )); + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_READ, 0x01, 0x23, 0x45, 0x67]), + FakeSpiOp::Read(5), + ]), + b"World".to_vec(), + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + let mut buf = [0u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + flash + .read(FlashAddress::new(0x0123_4567), &mut buf) + .unwrap(); + assert_eq!(&buf, b"World"); + flash.spi.verify(); + } + + #[test] + fn test_read_4b_qspi() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 32 * MIB); + + // Expect OP_QREAD4B + 4B Address + 1 Dummy Byte (0x00) + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00]), + FakeSpiOp::Read(5), + ]), + b"Hello".to_vec(), + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + 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"); + flash.spi.verify(); + } + + #[test] + fn test_qread_3b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 8 * MIB); + // Expect OP_QREAD + 3B Address + 1 Dummy Byte (0x00) + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_QREAD, 0x12, 0x34, 0x56, 0x00]), + FakeSpiOp::Read(5), + ]), + b"Hello".to_vec(), + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD; + + let mut buf = [0u8; 5]; + flash.read(FlashAddress::new(0x12_3456), &mut buf).unwrap(); + assert_eq!(&buf, b"Hello"); + flash.spi.verify(); + } + + #[test] + fn test_qread_4b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 32 * MIB); + + // Expect OP_QREAD4B + 4B Address + 1 Dummy Byte (0x00) + expected.push(( + FakeSpiOp::Transaction(vec![ + FakeSpiOp::Write(vec![OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00]), + FakeSpiOp::Read(5), + ]), + b"Hello".to_vec(), + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + 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"); + flash.spi.verify(); + } + + #[test] + fn test_erase_3b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, 16 * MIB); + + // write-enable + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + // erase + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0xba, 0x10, 0x00]), + vec![], + )); + // get-status: WRITE_EN=1, WIP=1 + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_WIP_WEL], + )); + // get-status: WRITE_EN=0, WIP=1 + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_WIP], + )); + // get-status: WRITE_EN=0, WIP=0 + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_READY], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + flash + .erase( + FlashAddress::new(0xba_1000), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001), + 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) + ); + } + + #[test] + fn test_erase_4b() { + let mut expected = Vec::new(); + preprogram_init(&mut expected, GIB); + + // write-enable + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + // erase (new driver uses OP_ERASE_4K in 4B mode) + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x1a, 0x5e, 0xb0, 0x00]), + vec![], + )); + // get-status: WRITE_EN=1, WIP=1 + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_WIP_WEL], + )); + // get-status: WRITE_EN=0, WIP=0 + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![STATUS_READY], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + flash + .erase( + FlashAddress::new(0x1a5e_b000), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001), + 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) + ); + } + + #[test] + fn test_erase_last_page() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x7f, 0xf0, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + flash + .erase( + FlashAddress::new((8 * MIB - 4096) as u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + + 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) + ); + flash.spi.verify(); + } + + #[test] + fn test_erase_single_page_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x00, 0x10, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0x1000), PowerOf2Usize::new(4096).unwrap()) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_single_page_4b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 32 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x00, 0x00, 0x10, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0x1000), PowerOf2Usize::new(4096).unwrap()) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_single_block_64k_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_64K, 0x01, 0x00, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x10000), + PowerOf2Usize::new(65536).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_single_block_64k_4b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 32 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_64K, 0x01, 0x23, 0x00, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x01230000), + PowerOf2Usize::new(65536).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_mixed_granularity_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // Mixed test: We use a size of 128KB (Power of 2). + // Starting at 56KB, to verify it uses page erase at 56KB, 60KB, and 64KB block erase. + let ops = [ + (OP_ERASE_4K, vec![0x00, 0xE0, 0x00]), // 56KB + (OP_ERASE_4K, vec![0x00, 0xF0, 0x00]), // 60KB + (OP_ERASE_64K, vec![0x01, 0x00, 0x00]), // 64KB + (OP_ERASE_4K, vec![0x02, 0x00, 0x00]), // 128KB + (OP_ERASE_4K, vec![0x02, 0x10, 0x00]), // 132KB + (OP_ERASE_4K, vec![0x02, 0x20, 0x00]), // 136KB + (OP_ERASE_4K, vec![0x02, 0x30, 0x00]), // 140KB + (OP_ERASE_4K, vec![0x02, 0x40, 0x00]), // 144KB + (OP_ERASE_4K, vec![0x02, 0x50, 0x00]), // 148KB + (OP_ERASE_4K, vec![0x02, 0x60, 0x00]), // 152KB + (OP_ERASE_4K, vec![0x02, 0x70, 0x00]), // 156KB + (OP_ERASE_4K, vec![0x02, 0x80, 0x00]), // 160KB + (OP_ERASE_4K, vec![0x02, 0x90, 0x00]), // 164KB + (OP_ERASE_4K, vec![0x02, 0xA0, 0x00]), // 168KB + (OP_ERASE_4K, vec![0x02, 0xB0, 0x00]), // 172KB + (OP_ERASE_4K, vec![0x02, 0xC0, 0x00]), // 176KB + (OP_ERASE_4K, vec![0x02, 0xD0, 0x00]), // 180KB + ]; + + for (op, addr) in &ops { + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + let mut cmd = vec![*op]; + cmd.extend_from_slice(addr); + expected.push((FakeSpiOp::Write(cmd), vec![])); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((56 * KIB) as u32), + PowerOf2Usize::new(128 * KIB).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_mixed_granularity_4b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 32 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + // Mixed test: We use a size of 128KB (Power of 2). + // Starting at MAX_3B_SIZE - 8KB, which is 16MB - 8KB. + let ops = [ + (OP_ERASE_4K, vec![0x00, 0xFF, 0xE0, 0x00]), + (OP_ERASE_4K, vec![0x00, 0xFF, 0xF0, 0x00]), + (OP_ERASE_64K, vec![0x01, 0x00, 0x00, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x00, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x10, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x20, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x30, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x40, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x50, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x60, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x70, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x80, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0x90, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0xA0, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0xB0, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0xC0, 0x00]), + (OP_ERASE_4K, vec![0x01, 0x01, 0xD0, 0x00]), + ]; + + for (op, addr) in &ops { + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + let mut cmd = vec![*op]; + cmd.extend_from_slice(addr); + expected.push((FakeSpiOp::Write(cmd), vec![])); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((MAX_3B_SIZE - 8 * KIB) as u32), + PowerOf2Usize::new(128 * KIB).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_alignment_errors() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + assert_eq!( + flash.erase(FlashAddress::new(1), PowerOf2Usize::new(4096).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + // 4095 is not a power of 2, so it would fail compile or fail new(). + // We test with PowerOf2Usize::new(1) which is a power of 2 but invalid size (not multiple of 4K). + assert_eq!( + flash.erase(FlashAddress::new(0), PowerOf2Usize::new(1).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE) + ); + } + + #[test] + fn test_erase_out_of_bounds() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((16 * MIB - 4096) as u32), + PowerOf2Usize::new(8192).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + + // 4B flash (32 MiB) + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 32 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((32 * MIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + } + + #[test] + fn test_erase_entire_flash_3b() { + let mut expected = Vec::new(); + const SIZE: usize = 128 * KIB; + preprogram_sfdp(&mut expected, SIZE); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let ops = [ + (OP_ERASE_64K, vec![0x00, 0x00, 0x00]), + (OP_ERASE_64K, vec![0x01, 0x00, 0x00]), + ]; + + for (op, addr) in &ops { + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + let mut cmd = vec![*op]; + cmd.extend_from_slice(addr); + expected.push((FakeSpiOp::Write(cmd), vec![])); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_all() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 128 * KIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_CHIP_ERASE]), vec![])); + // status busy check loop + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], // busy + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], // ready + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.erase_all().unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_entire_flash_4b() { + let mut expected = Vec::new(); + const SIZE: usize = 32 * MIB; + preprogram_sfdp(&mut expected, SIZE); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + let num_blocks = SIZE / (64 * KIB); + for i in 0..num_blocks { + let addr = (i * 64 * KIB) as u32; + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + let mut cmd = vec![OP_ERASE_64K]; // OP_ERASE_64K in B7 mode + cmd.extend_from_slice(&addr.to_be_bytes()); + expected.push((FakeSpiOp::Write(cmd), vec![])); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_multiple_4k_pages_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // 16KB is a valid power of 2 (erases four 4K pages) + for i in 1..=4 { + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x00, (i * 0x10) as u8, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x1000), + PowerOf2Usize::new(16 * KIB).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_erase_block_aligned_small_len_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 16 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // 8KB is a power of 2, erases two 4K pages + for i in 0..2 { + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_ERASE_4K, 0x01, (i * 0x10) as u8, 0x00]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + } + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x10000), + PowerOf2Usize::new(8 * KIB).unwrap(), + ) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_program_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_PROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], // busy + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], // ready + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_1140), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_program_4b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 128 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + // Expect OP_PROGRAM (0x02) but with 4 address bytes under B7 mode + expected.push(( + FakeSpiOp::Write(vec![ + OP_PROGRAM, 0x02, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11, + ]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x0274_1140), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_qprogram_3b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_QPROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_1140), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_qprogram_4b() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 128 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![ + OP_QPROGRAM4B, + 0x02, + 0x74, + 0x11, + 0x40, + 0xba, + 0x5e, + 0xba, + 0x11, + ]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM4B; + flash + .program(FlashAddress::new(0x0274_1140), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_program_last_byte() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_PROGRAM, 0x7f, 0xff, 0xff, 0x42]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x7f_ffff), &[0x42]) + .unwrap(); + + // Check overflow past end of flash + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + flash.spi.verify(); + } + + #[test] + fn test_program_spans_pages() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // Page 1 Write + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_PROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // Page 2 Write + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_PROGRAM, 0x74, 0x12, 0x00, 0x11]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_11fd), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_qprogram_last_byte() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_QPROGRAM, 0x7f, 0xff, 0xff, 0x42]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x7f_ffff), &[0x42]) + .unwrap(); + + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + flash.spi.verify(); + } + + #[test] + fn test_qprogram_spans_pages() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 8 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // Page 1 + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_QPROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + // Page 2 + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push(( + FakeSpiOp::Write(vec![OP_QPROGRAM, 0x74, 0x12, 0x00, 0x11]), + vec![], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x01], + )); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_11fd), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + flash.spi.verify(); + } + + #[test] + fn test_set_ear() { + let mut expected = Vec::new(); + preprogram_sfdp(&mut expected, 128 * MIB); + expected.push(( + FakeSpiOp::Transaction(vec![FakeSpiOp::Write(vec![OP_STATUS]), FakeSpiOp::Read(1)]), + vec![0x00], + )); + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_ENTER_4B_ADDR_MODE]), vec![])); + + expected.push((FakeSpiOp::Write(vec![OP_WRITE_EN]), vec![])); + expected.push((FakeSpiOp::Write(vec![OP_WR_EAR, 0x02]), vec![])); + + let spi = FakeSpiDevice::new(expected); + let mut flash = SpiFlash::new(spi); + flash.init().unwrap(); + flash.set_ear(0x02).unwrap(); + flash.spi.verify(); + } + + #[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)); + } + + #[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) + ); + } +} diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel index 76820225..3fde61f7 100644 --- a/target/earlgrey/drivers/BUILD.bazel +++ b/target/earlgrey/drivers/BUILD.bazel @@ -86,3 +86,17 @@ rust_library( "@ureg", ], ) + +rust_library( + name = "spi_host", + srcs = ["spi_host.rs"], + crate_name = "earlgrey_spi_host", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/registers:spi_host", + "//util/regcpy", + "@rust_crates//:embedded-hal", + "@ureg", + ], +) diff --git a/target/earlgrey/drivers/spi_host.rs b/target/earlgrey/drivers/spi_host.rs new file mode 100644 index 00000000..2f4c5526 --- /dev/null +++ b/target/earlgrey/drivers/spi_host.rs @@ -0,0 +1,259 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPI Host driver for Earlgrey. +//! +//! This driver implements the `embedded-hal` 1.0 SPI traits for the Earlgrey SPI Host controller. + +#![no_std] + +/// SPI Host driver errors. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SpiError { + /// Invalid transaction parameters or state. + InvalidTransaction, + /// TX FIFO overflow. + FifoOverflow, + /// RX FIFO underflow. + FifoUnderflow, + /// Operation timed out. + Timeout, + /// Hardware error reported by the controller. + HardwareError, +} + +impl embedded_hal::spi::Error for SpiError { + fn kind(&self) -> embedded_hal::spi::ErrorKind { + // Map all errors to Other for now. + embedded_hal::spi::ErrorKind::Other + } +} + +/// Configuration for the SPI Host driver. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct SpiConfig { + /// Clock divider to adjust transfer speed. + /// Formula: f_sck = f_core / (2 * (clkdiv + 1)) + pub clkdiv: u32, + /// SPI clock polarity (false for low when idle, true for high when idle). + pub cpol: bool, + /// SPI clock phase (false to sample on first edge, true on second edge). + pub cpha: bool, + /// Chip Select ID line (0 for CS0, 1 for CS1, etc.) + pub csid: u32, +} + +impl SpiConfig { + /// Default configuration for SPI Host 0 (SPI0). + /// SPI0 clock domain is 96MHz. clkdiv 1 provides 24MHz SCK. + pub const DEFAULT_SPI0: Self = Self { + clkdiv: 1, + cpol: false, + cpha: false, + csid: 0, + }; + + /// Default configuration for SPI Host 1 (SPI1). + /// SPI1 clock domain is 48MHz. clkdiv 0 provides 24MHz SCK. + pub const DEFAULT_SPI1: Self = Self { + clkdiv: 0, + cpol: false, + cpha: false, + csid: 0, + }; +} + +/// Earlgrey SPI Host driver. +pub struct SpiHost { + mmio: spi_host::RegisterBlock>, +} + +impl SpiHost { + /// Create a new SpiHost instance using the peripheral ownership token for SpiHost0. + pub fn new_spi_host0(token: spi_host::SpiHost0) -> Self { + let _ = token; + Self { + mmio: unsafe { spi_host::RegisterBlock::new(spi_host::SpiHost0::PTR) }, + } + } + + /// Create a new SpiHost instance using the peripheral ownership token for SpiHost1. + pub fn new_spi_host1(token: spi_host::SpiHost1) -> Self { + let _ = token; + Self { + mmio: unsafe { spi_host::RegisterBlock::new(spi_host::SpiHost1::PTR) }, + } + } + + /// Dynamically reconfigure the SPI Host speed, mode, and chip select. + pub fn configure(&mut self, config: &SpiConfig) -> Result<(), SpiError> { + self.mmio.configopts().write(|w| { + w.clkdiv(config.clkdiv) + .cpol(config.cpol) + .cpha(config.cpha) + .fullcyc(true) + .csnlead(0) + .csnidle(2) + .csntrail(1) + }); + self.mmio.csid().write(|_| config.csid); + Ok(()) + } + + /// Initialize the SPI Host peripheral. + /// + /// Configures the clock, SPI mode (0), CS timings, performs a reset, + /// and enables the peripheral. + pub fn init(&mut self, config: &SpiConfig) -> Result<(), SpiError> { + self.configure(config)?; + self.mmio.control().write(|w| w.sw_rst(true)); + loop { + let status = self.mmio.status().read(); + if status.txempty() && status.rxempty() { + break; + } + } + self.mmio.control().write(|w| w.sw_rst(false).spien(true)); + self.mmio.csid().write(|_| config.csid); + + Ok(()) + } + + fn is_active(&self) -> bool { + self.mmio.status().read().active() + } + + fn is_ready(&self) -> bool { + let status = self.mmio.status().read(); + status.ready() && !status.active() + } + + /// Send a write command segment. + fn write_cmd( + &mut self, + mut data: &[u8], + speed: u32, + final_csaat: bool, + ) -> Result<(), SpiError> { + self.mmio.control().modify(|w| w.output_en(true)); + + while !data.is_empty() { + while !self.is_ready() { + // TODO: we should block here for an interrupt that would signal the hardware is ready. + } + + let chunk_len = core::cmp::min(data.len(), MAX_TX_CHUNK_LEN); + let (chunk, rest) = data.split_at(chunk_len); + data = rest; + + util_regcpy::copy_to_reg_unaligned(&self.mmio.txdata(), chunk); + + let chunk_csaat = !data.is_empty() || final_csaat; + + self.mmio.command().write(|w| { + w.speed(speed) + .csaat(chunk_csaat) + .direction(DIR_TXONLY) + .len(chunk_len.saturating_sub(1) as u32) + }); + } + Ok(()) + } + + /// Send a read command segment and receive data into `dest`. + fn read_cmd( + &mut self, + mut len: usize, + speed: u32, + final_csaat: bool, + mut dest: &mut [u8], + ) -> Result<(), SpiError> { + self.mmio.control().modify(|w| w.output_en(true)); + + while len > 0 { + while !self.is_ready() { + // TODO: we should block here for an interrupt that would signal the hardware is ready. + } + + let chunk_len = core::cmp::min(len, MAX_RX_CHUNK_LEN); + len -= chunk_len; + + let chunk_csaat = len > 0 || final_csaat; + + self.mmio.command().write(|w| { + w.speed(speed) + .csaat(chunk_csaat) + .direction(DIR_RXONLY) + .len(chunk_len.saturating_sub(1) as u32) + }); + + if chunk_len > dest.len() { + return Err(SpiError::InvalidTransaction); + } + // Split dest into the chunk we want to read now, and the rest for later. + let (mut chunk_dest, rest) = dest.split_at_mut(chunk_len); + dest = rest; + + while !chunk_dest.is_empty() { + let status = self.mmio.status().read(); + let rxqd = status.rxqd() as usize; // rxqd is in 32-bit words + let bytes_in_fifo = rxqd * 4; + + if bytes_in_fifo > 0 { + let drain_len = core::cmp::min(bytes_in_fifo, chunk_dest.len()); + let (drain_chunk, remaining) = chunk_dest.split_at_mut(drain_len); + util_regcpy::copy_from_reg_unaligned(drain_chunk, &self.mmio.rxdata()); + chunk_dest = remaining; + } + } + } + Ok(()) + } +} + +const MAX_RX_CHUNK_LEN: usize = 256; +const MAX_TX_CHUNK_LEN: usize = 288; +const DIR_RXONLY: u32 = 1; +const DIR_TXONLY: u32 = 2; + +impl embedded_hal::spi::ErrorType for SpiHost { + type Error = SpiError; +} + +impl embedded_hal::spi::SpiDevice for SpiHost { + fn transaction( + &mut self, + operations: &mut [embedded_hal::spi::Operation<'_, u8>], + ) -> Result<(), Self::Error> { + let op_count = operations.len(); + for (i, op) in operations.iter_mut().enumerate() { + let is_last = i == op_count - 1; + let csaat = !is_last; + + match op { + embedded_hal::spi::Operation::Read(buf) => { + self.read_cmd(buf.len(), 0, csaat, buf)?; + } + embedded_hal::spi::Operation::Write(buf) => { + self.write_cmd(buf, 0, csaat)?; + } + embedded_hal::spi::Operation::Transfer(_, _) + | embedded_hal::spi::Operation::TransferInPlace(_) => { + // Full-duplex SPI transfer is not supported by this simple driver yet. + return Err(SpiError::InvalidTransaction); + } + embedded_hal::spi::Operation::DelayNs(_) => { + // Delay operation is not supported. + return Err(SpiError::InvalidTransaction); + } + } + } + + // Wait for the controller to finish the last command segment. + while self.is_active() {} + // Disable output buffers to release the bus. + self.mmio.control().modify(|w| w.output_en(false)); + + Ok(()) + } +} 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..d2561bda --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs @@ -0,0 +1,114 @@ +// 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<()> { + let spi_host0 = unsafe { SpiHost0::new() }; + let mut spi_host = earlgrey_spi_host::SpiHost::new_spi_host0(spi_host0); + spi_host + .init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) + .map_err(|_| Error::Internal)?; + + // 2. Create SpiFlash driver. + let mut flash = SpiFlash::new(spi_host); + 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..2e253d37 --- /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 Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/drivers/spi_host/BUILD.bazel b/target/earlgrey/tests/drivers/spi_host/BUILD.bazel new file mode 100644 index 00000000..f0d73929 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_host/BUILD.bazel @@ -0,0 +1,103 @@ +# 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_host", + srcs = [ + "spi_host.rs", + ], + codegen_crate_name = "spi_host_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:spi_host", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +system_image( + name = "spi_host_image", + apps = [ + ":spi_host", + ], + 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_host_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_host_image", +) + +opentitan_test( + name = "spi_host_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_host_image", +) diff --git a/target/earlgrey/tests/drivers/spi_host/spi_host.rs b/target/earlgrey/tests/drivers/spi_host/spi_host.rs new file mode 100644 index 00000000..40927330 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_host/spi_host.rs @@ -0,0 +1,143 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Smoke test for Earlgrey SPI Host driver. +//! +//! Initializes the SPI Host, reads the JEDEC SFDP signature of the +//! external Flash, and verifies it matches the standard "SFDP" string. + +#![no_std] +#![no_main] + +use earlgrey_spi_host::{SpiConfig, SpiHost}; +use embedded_hal::spi::SpiDevice; +use pw_status::{Error, Result}; +use spi_host::{SpiHost0, SpiHost1}; +use userspace::entry; + +fn run_test() -> Result<()> { + // 1. Initialize SPI Host (SPI_HOST0). + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let spi_host0 = unsafe { SpiHost0::new() }; + let mut spi = SpiHost::new_spi_host0(spi_host0); + spi.init(&SpiConfig::DEFAULT_SPI0).map_err(|_| { + pw_log::error!("SPI init failed"); + Error::Internal + })?; + + // 2. Read SFDP Signature (Command 0x5A, Address 0x000000, 8 Dummy Cycles -> 1 Dummy Byte). + // The TX buffer contains: [Opcode, Addr[23:16], Addr[15:8], Addr[7:0], Dummy] + let tx_buf = [0x5A, 0x00, 0x00, 0x00, 0x00]; + let mut rx_buf = [0u8; 4]; + + let mut ops = [ + embedded_hal::spi::Operation::Write(&tx_buf), + embedded_hal::spi::Operation::Read(&mut rx_buf), + ]; + + spi.transaction(&mut ops).map_err(|_| { + pw_log::error!("SPI transaction 1 failed"); + Error::Internal + })?; + + pw_log::info!( + "SFDP signature read 1: {:02x} {:02x} {:02x} {:02x}", + rx_buf[0], + rx_buf[1], + rx_buf[2], + rx_buf[3] + ); + + // Read SFDP Signature again to verify FIFO is not corrupted by padded write remainders. + let mut rx_buf2 = [0u8; 4]; + let mut ops2 = [ + embedded_hal::spi::Operation::Write(&tx_buf), + embedded_hal::spi::Operation::Read(&mut rx_buf2), + ]; + + spi.transaction(&mut ops2).map_err(|_| { + pw_log::error!("SPI transaction 2 failed"); + Error::Internal + })?; + + pw_log::info!( + "SFDP signature read 2: {:02x} {:02x} {:02x} {:02x}", + rx_buf2[0], + rx_buf2[1], + rx_buf2[2], + rx_buf2[3] + ); + + // 3. Verify SFDP Signature ("SFDP" -> [0x53, 0x46, 0x44, 0x50]) + let expected_sig = [0x53, 0x46, 0x44, 0x50]; // "SFDP" in ASCII (little-endian bytes) + if rx_buf != expected_sig { + pw_log::error!( + "FAIL: Unexpected SFDP signature in read 1: [{:02x}, {:02x}, {:02x}, {:02x}] (expected [53, 46, 44, 50])", + rx_buf[0], + rx_buf[1], + rx_buf[2], + rx_buf[3] + ); + return Err(Error::FailedPrecondition); + } + if rx_buf2 != expected_sig { + pw_log::error!( + "FAIL: Unexpected SFDP signature in read 2: [{:02x}, {:02x}, {:02x}, {:02x}] (expected [53, 46, 44, 50])", + rx_buf2[0], + rx_buf2[1], + rx_buf2[2], + rx_buf2[3] + ); + return Err(Error::FailedPrecondition); + } + + pw_log::info!("SFDP signature verified successfully!"); + + // 4. Exercise SPI_HOST1 to verify multi-instance capability. + // SAFETY: We have exclusive access to SPI_HOST1 in this test process. + let spi_host1 = unsafe { SpiHost1::new() }; + let mut spi1 = SpiHost::new_spi_host1(spi_host1); + spi1.init(&SpiConfig::DEFAULT_SPI1).map_err(|_| { + pw_log::error!("SPI1 init failed"); + Error::Internal + })?; + + // Perform a dummy transaction. MISO is likely floating or unmapped, so we expect + // garbage bytes (usually 0x00 or 0xFF), but the transaction itself must succeed (no timeout/hang). + let mut rx_buf_dummy = [0u8; 4]; + let mut ops_dummy = [ + embedded_hal::spi::Operation::Write(&tx_buf), + embedded_hal::spi::Operation::Read(&mut rx_buf_dummy), + ]; + + spi1.transaction(&mut ops_dummy).map_err(|_| { + pw_log::error!("SPI1 transaction failed"); + Error::Internal + })?; + + pw_log::info!( + "SPI1 dummy transaction returned: {:02x} {:02x} {:02x} {:02x}", + rx_buf_dummy[0], + rx_buf_dummy[1], + rx_buf_dummy[2], + rx_buf_dummy[3] + ); + + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + pw_log::info!("🔄 RUNNING SPI Host Smoke Test"); + let ret = run_test(); + + if ret.is_err() { + pw_log::error!("FAIL: Smoke test execution failed"); + } else { + pw_log::info!("✅ PASS"); + } + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/drivers/spi_host/system.json5 b/target/earlgrey/tests/drivers/spi_host/system.json5 new file mode 100644 index 00000000..23c1d5d8 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_host/system.json5 @@ -0,0 +1,47 @@ +// 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_host", + flash_size_bytes: 16384, + processes: [{ + name: "spi_host_process", + ram_size_bytes: 4096, + objects: [ + { + type: "thread", + name: "spi_thread", + kernel_stack_size_bytes: 2048, + }, + ], + memory_mappings: [ + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + }, + { + name: "spi_host1", + type: "device", + start_address: 0x40310000, + size_bytes: 0x1000, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/drivers/spi_host/target.rs b/target/earlgrey/tests/drivers/spi_host/target.rs new file mode 100644 index 00000000..c8a2aa45 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_host/target.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(clippy::empty_loop)] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/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..58337b64 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/combined_flash_server.rs @@ -0,0 +1,142 @@ +// 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, KERNEL_ERROR_INTERNAL}; +use util_ipc::IpcHandle; +use util_types::Blocking; + +// 1. 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> { + pw_log::info!("combined_server: initializing EFlash driver"); + 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); + + pw_log::info!("combined_server: initializing SPI Host"); + let spi_host0 = unsafe { SpiHost0::new() }; + let mut spi_host = earlgrey_spi_host::SpiHost::new_spi_host0(spi_host0); + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + let err_num = match e { + earlgrey_spi_host::SpiError::InvalidTransaction => 1, + earlgrey_spi_host::SpiError::FifoOverflow => 2, + earlgrey_spi_host::SpiError::FifoUnderflow => 3, + earlgrey_spi_host::SpiError::Timeout => 4, + earlgrey_spi_host::SpiError::HardwareError => 5, + }; + pw_log::error!("combined_server: SPI Host init failed: {}", err_num); + return Err(KERNEL_ERROR_INTERNAL); + } + + 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); + + pw_log::info!("combined_server: registering wait group ports"); + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::EFLASH_SERVICE, + syscall::Signals::READABLE, + 1, // token 1 = EFlash + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_SERVICE, + syscall::Signals::READABLE, + 2, // token 2 = SPI Flash + ) + .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); + + 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 == 1 { + eflash_server.handle_one(&eflash_ipc, &mut buf)?; + } else if token == 2 { + 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..2e253d37 --- /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 Userspace UART"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/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 = diff --git a/util/sfdp/BUILD.bazel b/util/sfdp/BUILD.bazel new file mode 100644 index 00000000..96bbdded --- /dev/null +++ b/util/sfdp/BUILD.bazel @@ -0,0 +1,26 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "sfdp", + srcs = ["mod.rs"], + crate_name = "util_sfdp", + edition = "2024", + proc_macro_deps = [ + "@rust_crates//:bitfield-struct", + ], + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "//util/io", + "//util/types", + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "sfdp_test", + crate = ":sfdp", +) diff --git a/util/sfdp/mod.rs b/util/sfdp/mod.rs new file mode 100644 index 00000000..3bd8fba2 --- /dev/null +++ b/util/sfdp/mod.rs @@ -0,0 +1,2295 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(not(test), no_std)] + +const _: () = assert!(cfg!(target_endian = "little")); + +use bitfield_struct::bitfield; +use core::mem::offset_of; +use util_error as error; +use util_error::ErrorCode; +use util_io::RandomRead; +use util_types::{MultiplyDuration, Nanoseconds}; +use zerocopy::FromBytes; +use zerocopy::FromZeros; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, Debug)] +#[repr(transparent)] +pub struct SfdpSignature(pub u32); +impl SfdpSignature { + pub const EXPECTED_VALUE: Self = Self(0x50444653); + + pub const fn new() -> Self { + Self::EXPECTED_VALUE + } + pub fn is_valid(self) -> bool { + self == Self::EXPECTED_VALUE + } +} +impl Default for SfdpSignature { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, Debug)] +#[repr(transparent)] +pub struct AccessProtocol(pub u8); +impl AccessProtocol { + pub const XSPI_NAND_CLASS2: Self = Self(241); + pub const SPI_NAND_CLASS_1: Self = Self(244); + pub const SPI_NAND_CLASS_2: Self = Self(245); + pub const XSPI_NOR_PROFILE_2_5B: Self = Self(250); + pub const XSPI_NOR_PROFILE_1_OCTAL_3B_8W: Self = Self(252); + pub const XSPI_NOR_PROFILE_1_OCTAL_4B_20W: Self = Self(253); + pub const XSPI_NOR_PROFILE_1_OCTAL_4B_8W: Self = Self(254); + pub const LEGACY: Self = Self(255); +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, Debug)] +#[repr(C)] +pub struct SfdpHeader { + pub sig: SfdpSignature, + pub minor_rev: u8, + pub major_rev: u8, + pub num_parameter_header: u8, + pub access_protocol: AccessProtocol, +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(transparent)] +pub struct U24([u8; 3]); +impl U24 { + pub const fn new(val: u32) -> Self { + let [b0, b1, b2, _] = val.to_le_bytes(); + Self([b0, b1, b2]) + } + pub const fn as_usize(&self) -> usize { + const { assert!(size_of::() >= size_of::()) } + self.as_u32() as usize + } + pub const fn as_u32(&self) -> u32 { + let [b0, b1, b2] = self.0; + u32::from_le_bytes([b0, b1, b2, 0]) + } +} +impl core::fmt::Debug for U24 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.as_u32(), f) + } +} +impl core::fmt::Display for U24 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.as_u32(), f) + } +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, Debug)] +#[repr(C)] +pub struct ParameterHeader { + pub parameter_id_lsb: u8, + pub minor_rev: u8, + pub major_rev: u8, + pub len_in_dwords: u8, + pub ptr: U24, + pub parameter_id_msb: u8, +} + +#[derive(Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct FunctionSpecificParameterTableIdAssignments(pub u16); +impl FunctionSpecificParameterTableIdAssignments { + pub const BASIC_FLASH: Self = Self(0xFF00); + pub const _4B_ADDRESS_INSTRUCTION_TABLE: Self = Self(0xFF84); +} + +impl ParameterHeader { + pub fn parameter_id(&self) -> FunctionSpecificParameterTableIdAssignments { + let val = ((self.parameter_id_msb as u16) << 8) | self.parameter_id_lsb as u16; + FunctionSpecificParameterTableIdAssignments(val) + } +} + +#[derive(Debug)] +pub enum LegacyEraseSizes { + Reserved0 = 0, + Erase4k = 1, + Reserved2 = 2, + Erase4kNotAvailable = 3, +} +impl LegacyEraseSizes { + const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::Reserved0, + 0b01 => Self::Erase4k, + 0b10 => Self::Reserved2, + _ => Self::Erase4kNotAvailable, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[derive(Debug)] +pub enum LegacyWriteGranularity { + SingleByte = 0, + Buffer64 = 1, +} +impl LegacyWriteGranularity { + const fn from_bits(bits: u8) -> Self { + match bits & 0x1 { + 0 => Self::SingleByte, + _ => Self::Buffer64, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[derive(Debug)] +pub enum AddressBytes { + _3ByteOnly = 0, + _3Or4Byte = 1, + _4ByteOnly = 2, + Reserved3 = 3, +} +impl AddressBytes { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::_3ByteOnly, + 0b01 => Self::_3Or4Byte, + 0b10 => Self::_4ByteOnly, + _ => Self::Reserved3, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord1 { + #[bits(2)] + pub legacy_erase_sizes: LegacyEraseSizes, + + #[bits(1)] + pub legacy_write_granularity: LegacyWriteGranularity, + + #[bits(1)] + pub block_protect_is_volatile: bool, + + #[bits(1)] + pub status_write_requires_write_enable: bool, + + #[bits(3)] + __: u8, + + pub erase4k_instr: u8, + + pub supports_1s_1s_2s_read: bool, + + #[bits(2)] + pub addr_bytes: AddressBytes, + + pub dtr_clocking_supported: bool, + + pub supports_1s_2s_2s_read: bool, + + pub supports_1s_4s_4s_read: bool, + + pub supports_1s_1s_4s_read: bool, + + #[bits(9)] + __: u16, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord3 { + #[bits(5)] + pub fast_read_1s_4s_4s_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_4s_4s_mode_clocks: u8, + + pub fast_read_1s_4s_4s_instr: u8, + + #[bits(5)] + pub fast_read_1s_1s_4s_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_1s_4s_mode_clocks: u8, + + pub fast_read_1s_1s_4s_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord4 { + #[bits(5)] + pub fast_read_1s_1s_2s_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_1s_2s_mode_clocks: u8, + + pub fast_read_1s_1s_2s_instr: u8, + + #[bits(5)] + pub fast_read_1s_2s_2s_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_2s_2s_mode_clocks: u8, + + pub fast_read_1s_2s_2s_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord5 { + pub fast_read_2x_2s_2s_supported: bool, + + #[bits(3)] + __: u8, + + pub fast_read_4x_4s_4s_supported: bool, + + #[bits(27)] + __: u32, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord6 { + #[bits(16)] + __: u16, + + #[bits(5)] + pub fast_read_2s_2s_2s_wait_states: u8, + + #[bits(3)] + pub fast_read_2s_2s_2s_mode_clocks: u8, + + pub fast_read_2s_2s_2s_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord7 { + #[bits(16)] + __: u16, + + #[bits(5)] + pub fast_read_4s_4s_4s_wait_states: u8, + + #[bits(3)] + pub fast_read_4s_4s_4s_mode_clocks: u8, + + pub fast_read_4s_4s_4s_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord8 { + #[bits(8)] + pub erase_type_1_size: PowerOf2, + + pub erase_type_1_instr: u8, + + #[bits(8)] + pub erase_type_2_size: PowerOf2, + + pub erase_type_2_instr: u8, +} + +#[bitfield(u8)] +#[derive(Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct PowerOf2 { + pub n: u8, +} +impl PowerOf2 { + pub fn value(&self) -> Option { + let n = self.n(); + if n < (usize::BITS as u8) { + Some(1 << n) + } else { + None + } + } +} + +#[bitfield(u8)] +#[derive(Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct SmallPowerOf2 { + #[bits(4)] + pub n: u8, + #[bits(4)] + __: u8, +} +impl SmallPowerOf2 { + pub fn value(&self) -> usize { + 1 << self.n() + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord9 { + #[bits(8)] + pub erase_type_3_size: PowerOf2, + + pub erase_type_3_instr: u8, + + #[bits(8)] + pub erase_type_4_size: PowerOf2, + + pub erase_type_4_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord10 { + #[bits(4)] + pub multiplier_max_erase_time: u8, + + #[bits(7)] + pub erase_type_1_time: EraseTime, + #[bits(7)] + pub erase_type_2_time: EraseTime, + #[bits(7)] + pub erase_type_3_time: EraseTime, + #[bits(7)] + pub erase_type_4_time: EraseTime, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct EraseTime { + #[bits(5)] + pub count: u8, + + #[bits(2)] + pub units: EraseTimeUnit, + + __: bool, +} + +impl From for Nanoseconds { + fn from(value: EraseTime) -> Self { + Nanoseconds::from(value.units()).mul(value.count() as i64 + 1) + } +} + +#[cfg(test)] +mod duration_tests { + use super::*; + + #[test] + fn test_duration() { + let et = EraseTime::from_bits(0b1000010); + assert_eq!(Nanoseconds::from(et), Nanoseconds::from_millis(384)); + } +} + +#[derive(Debug)] +pub enum EraseTimeUnit { + _1ms = 0b00, + _16ms = 0b01, + _128ms = 0b10, + _1s = 0b11, +} + +impl EraseTimeUnit { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::_1ms, + 0b01 => Self::_16ms, + 0b10 => Self::_128ms, + _ => Self::_1s, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +impl From for Nanoseconds { + fn from(value: EraseTimeUnit) -> Self { + match value { + EraseTimeUnit::_1ms => Nanoseconds::from_millis(1), + EraseTimeUnit::_16ms => Nanoseconds::from_millis(16), + EraseTimeUnit::_128ms => Nanoseconds::from_millis(128), + EraseTimeUnit::_1s => Nanoseconds::from_secs(1), + } + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord11 { + #[bits(4)] + pub multiplier_max_page_program: u8, + + #[bits(4)] + pub page_size: SmallPowerOf2, + + #[bits(6)] + pub page_program_time: PageProgramTime, + + #[bits(5)] + pub byte_program_first_time: ByteProgramTime, + + #[bits(5)] + pub byte_program_additional_time: ByteProgramTime, + + #[bits(7)] + pub chip_erase_time: ChipEraseTime, + + __: bool, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct PageProgramTime { + #[bits(5)] + pub count: u8, + + #[bits(1)] + pub units: PageProgramTimeUnit, + + #[bits(2)] + __: u8, +} + +impl From for Nanoseconds { + fn from(value: PageProgramTime) -> Self { + Nanoseconds::from(value.units()).mul(value.count() as i64 + 1) + } +} + +#[derive(Debug)] +pub enum PageProgramTimeUnit { + _8us = 0b0, + _64us = 0b1, +} + +impl PageProgramTimeUnit { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b1 { + 0b0 => Self::_8us, + _ => Self::_64us, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +impl From for Nanoseconds { + fn from(value: PageProgramTimeUnit) -> Self { + match value { + PageProgramTimeUnit::_8us => Nanoseconds::from_micros(8), + PageProgramTimeUnit::_64us => Nanoseconds::from_micros(64), + } + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ByteProgramTime { + #[bits(4)] + pub count: u8, + #[bits(1)] + pub unit: ByteProgramTimeUnit, + #[bits(3)] + __: u8, +} + +impl From for Nanoseconds { + fn from(value: ByteProgramTime) -> Self { + Nanoseconds::from(value.unit()).mul(value.count() as i64 + 1) + } +} + +#[derive(Debug)] +pub enum ByteProgramTimeUnit { + _1us = 0b0, + _8us = 0b1, +} + +impl ByteProgramTimeUnit { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b1 { + 0b0 => Self::_1us, + _ => Self::_8us, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +impl From for Nanoseconds { + fn from(value: ByteProgramTimeUnit) -> Self { + match value { + ByteProgramTimeUnit::_1us => Nanoseconds::from_micros(1), + ByteProgramTimeUnit::_8us => Nanoseconds::from_micros(8), + } + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ChipEraseTime { + #[bits(4)] + pub count: u8, + #[bits(2)] + pub units: ChipEraseTimeUnit, + #[bits(2)] + __: u8, +} + +impl From for Nanoseconds { + fn from(value: ChipEraseTime) -> Self { + Nanoseconds::from(value.units()).mul(value.count() as i64 + 1) + } +} + +#[derive(Debug)] +pub enum ChipEraseTimeUnit { + _16ms = 0b00, + _256ms = 0b01, + _4s = 0b10, + _64s = 0b11, +} + +impl ChipEraseTimeUnit { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::_16ms, + 0b01 => Self::_256ms, + 0b10 => Self::_4s, + _ => Self::_64s, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +impl From for Nanoseconds { + fn from(value: ChipEraseTimeUnit) -> Self { + match value { + ChipEraseTimeUnit::_16ms => Nanoseconds::from_millis(16), + ChipEraseTimeUnit::_256ms => Nanoseconds::from_millis(256), + ChipEraseTimeUnit::_4s => Nanoseconds::from_secs(4), + ChipEraseTimeUnit::_64s => Nanoseconds::from_secs(64), + } + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord12 { + #[bits(4)] + pub prohibited_ops_during_program_suspend: ProhibitedOpsDuringProgramSuspend, + + #[bits(4)] + pub prohibited_ops_during_erase_suspend: ProhibitedOpsDuringEraseSuspend, + + __: bool, + + #[bits(4)] + pub program_resume_suspend_interval: Interval64us, + + #[bits(7)] + pub suspend_in_progress_program_max_latency: DurationEnumA, + + #[bits(4)] + pub erase_resume_suspend_interval: Interval64us, + + #[bits(7)] + pub suspend_in_progress_erase_max_latency: DurationEnumA, + + pub suspend_resume_supported: bool, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ProhibitedOpsDuringProgramSuspend { + pub erase_nesting_permitted: bool, + pub program_nesting_permitted: bool, + pub no_read: bool, + pub erase_and_program_restrictions_are_sufficient: bool, + #[bits(4)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ProhibitedOpsDuringEraseSuspend { + pub erase_nesting_permitted: bool, + pub program_nesting_permitted: bool, + pub no_read: bool, + pub erase_and_program_restrictions_are_sufficient: bool, + #[bits(4)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Interval64us { + #[bits(4)] + pub counts_64ms: u8, + #[bits(4)] + __: u8, +} + +impl From for Nanoseconds { + fn from(value: Interval64us) -> Self { + Nanoseconds::from_millis(64).mul(value.counts_64ms() as i64) + } +} + +/// This duration type is used in several places +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct DurationEnumA { + #[bits(5)] + pub count: u8, + + #[bits(2)] + pub units: DurationEnumAUnit, + + __: bool, +} + +#[derive(Debug)] +pub enum DurationEnumAUnit { + _128ns = 0b00, + _1us = 0b01, + _8us = 0b10, + _64us = 0b11, +} + +impl DurationEnumAUnit { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::_128ns, + 0b01 => Self::_1us, + 0b10 => Self::_8us, + _ => Self::_64us, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +impl From for Nanoseconds { + fn from(value: DurationEnumAUnit) -> Self { + match value { + DurationEnumAUnit::_128ns => Nanoseconds::from_nanos(128), + DurationEnumAUnit::_1us => Nanoseconds::from_micros(1), + DurationEnumAUnit::_8us => Nanoseconds::from_micros(8), + DurationEnumAUnit::_64us => Nanoseconds::from_micros(64), + } + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord13 { + pub program_resume_instr: u8, + + pub program_suspend_instr: u8, + + pub resume_instr: u8, + + pub suspend_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord14 { + #[bits(2)] + __: u8, + + #[bits(6)] + pub status_register_polling_device_busy: StatusRegisterPollingDeviceBusy, + + #[bits(7)] + pub exit_deep_powerdown_next_op_delay: DurationEnumA, + + pub exit_deep_powerdown_instr: u8, + + pub enter_deep_powerdown_instr: u8, + + pub deep_powerdown_supported: bool, +} + +#[bitfield(u8)] +pub struct StatusRegisterPollingDeviceBusy { + pub legacy_polling_supported: bool, + + pub bit_7_polled_any_time: bool, + + #[bits(6)] + __: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord15 { + #[bits(4)] + pub mode_disable_sequence_4s_4s_4s: ModeDisableSequence4S4S4S, + + #[bits(5)] + pub mode_enable_sequence_4s_4s_4s: ModeEnableSequence4S4S4S, + + pub mode_0_4_4_supported: bool, + + #[bits(6)] + pub mode_exit_method_0_4_4: ModeExitMethod044, + + #[bits(4)] + pub mode_entry_method_0_4_4: ModeEntryMethod044, + + #[bits(3)] + pub quad_enable_requirements: QuadEnableRequirements, + + pub hold_or_reset_disable: bool, + + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ModeDisableSequence4S4S4S { + pub issue_ff_instr: bool, + pub issue_f5_instr: bool, + pub read_mod_write_seq: bool, + pub issue_soft_reset: bool, + #[bits(4)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ModeEnableSequence4S4S4S { + pub set_qe: bool, + pub issue_38_instr: bool, + pub issue_35_instr: bool, + pub read_mod_write_seq1: bool, + pub read_mod_write_seq2: bool, + #[bits(3)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ModeExitMethod044 { + pub mod_bits_00: bool, + pub input_io1: bool, + __: bool, + pub input_io2: bool, + pub mod_bits_not_ax: bool, + #[bits(3)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct ModeEntryMethod044 { + pub mod_bits_a5: bool, + pub instr_85: bool, + pub mod_bits_ax: bool, + #[bits(5)] + __: u8, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum QuadEnableRequirements { + NoQeBit = 0b000, + QeBit1SR2A = 0b001, + QeBit6SR1 = 0b010, + QeBit7SR2B = 0b011, + QeBit1SR2C = 0b100, + QeBit1SR2D = 0b101, + QeBit1SR2E = 0b110, + Reserved, +} + +impl QuadEnableRequirements { + pub const fn from_bits(bits: u8) -> Self { + match bits & 0b111 { + 0b000 => Self::NoQeBit, + 0b001 => Self::QeBit1SR2A, + 0b010 => Self::QeBit6SR1, + 0b011 => Self::QeBit7SR2B, + 0b100 => Self::QeBit1SR2C, + 0b101 => Self::QeBit1SR2D, + 0b110 => Self::QeBit1SR2E, + _ => Self::Reserved, + } + } + pub const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord16 { + #[bits(7)] + pub volatile_non_register_write_enable_instr: VolatileNonRegisterWriteEnableInstr, + + __: bool, + + #[bits(6)] + pub soft_reset_rescue_seq_support: SoftResetRescureSeqSupport, + + #[bits(10)] + pub exit_4_b_addressing: Exit4BAddressing, + + #[bits(8)] + pub enter_4_b_addressing: Enter4BAddressing, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct VolatileNonRegisterWriteEnableInstr { + pub non_volatile_reg1_last_written_we06: bool, + pub volatile_reg1_last_written_we06: bool, + pub volatile_reg1_last_written_we50: bool, + pub volatile_non_reg1_last_written_we0650: bool, + pub mix_volatile_we06: bool, + #[bits(3)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct SoftResetRescureSeqSupport { + pub drive0_f8_clk: bool, + pub drive0_f10_clk: bool, + pub drive0_f16_clk: bool, + pub instr_f0: bool, + pub instr66_then99: bool, + pub exit044_first: bool, + #[bits(2)] + __: u8, +} + +#[bitfield(u16)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Exit4BAddressing { + pub instr_e9: bool, + pub instr_06_then_e9: bool, + pub ear: bool, + pub bank_reg: bool, + pub conf_reg: bool, + pub hardware_reset: bool, + pub software_reset: bool, + pub power_cycle: bool, + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Enter4BAddressing { + pub intr_b7: bool, + pub instr_06_then_b7: bool, + pub ear: bool, + pub bank_reg: bool, + pub conf_reg: bool, + pub dedicated_add: bool, + pub always_4b: bool, + __: bool, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord17 { + #[bits(5)] + pub fast_read_wait_states_1s_8s_8s: u8, + + #[bits(3)] + pub fast_read_mode_clocks_1s_8s_8s: u8, + + pub fast_read_instr_1s_8s_8s: u8, + + #[bits(5)] + pub fast_read_wait_states_1s_1s_8s: u8, + + #[bits(3)] + pub fast_read_mode_clocks_1s_1s_8s: u8, + + pub fast_read_instr_1s_1s_8s: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord18 { + #[bits(18)] + __: u32, + + #[bits(5)] + pub output_driver_str: OuputDriverStr, + + pub jedec_spi_protocol_reset_implemented: bool, + + #[bits(2)] + pub data_strobe_waveform_str: DataStrobeWaveformStr, + + pub data_strobe_support_qpi_str_4s_4s_4s: bool, + + pub data_strobe_support_qpi_dtr_4s_4d_4d: bool, + + __: bool, + + #[bits(2)] + pub octal_dtr_8d_8d_8d_cmd: OctalDtrCmd, + + #[bits(1)] + pub octal_dtr_8d_8d_8d_mode: ByteOrder8D8D8D, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct OuputDriverStr { + pub type0_supported: bool, + pub type1_supported: bool, + pub type2_supported: bool, + pub type3_supported: bool, + pub type4_supported: bool, + #[bits(3)] + __: u8, +} + +#[derive(Debug)] +pub enum DataStrobeWaveformStr { + Reserved = 0b00, + DataStartRisingDS = 0b01, + RisingDSMiddleData = 0b10, + RisingDsBeforeData = 0b11, +} + +impl DataStrobeWaveformStr { + const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::Reserved, + 0b01 => Self::DataStartRisingDS, + 0b10 => Self::RisingDSMiddleData, + _ => Self::RisingDsBeforeData, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[derive(Debug)] +pub enum OctalDtrCmd { + CmdExtSameAsCmd = 0b00, + CmdExtInverseCmd = 0b01, + Reserved = 0b10, + CmdAndCmdExtMake16b = 0b11, +} + +impl OctalDtrCmd { + const fn from_bits(bits: u8) -> Self { + match bits & 0b11 { + 0b00 => Self::CmdExtSameAsCmd, + 0b01 => Self::CmdExtInverseCmd, + 0b10 => Self::Reserved, + _ => Self::CmdAndCmdExtMake16b, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[derive(Debug)] +pub enum ByteOrder8D8D8D { + SameAs1S1S1S = 0b0, + Swapped = 0b1, +} + +impl ByteOrder8D8D8D { + const fn from_bits(bits: u8) -> Self { + match bits & 0b1 { + 0b0 => Self::SameAs1S1S1S, + _ => Self::Swapped, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord19 { + #[bits(4)] + pub mode_8s_8s_8s_disable_sequences: Mode8s8s8sDisableSequences, + + #[bits(5)] + pub mode_8s_8s_8s_enable_sequences: Mode8s8s8sEnableSequences, + + pub mode_0_8_8_supported: bool, + + #[bits(6)] + pub mode_0_8_8_exit_method: Mode088ExitMethod, + + #[bits(4)] + pub mode_0_8_8_entry_method: Mode088EntryMethod, + + #[bits(3)] + pub octal_enable_requirements: OctalEnableReqs, + + #[bits(9)] + __: u16, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Mode8s8s8sDisableSequences { + pub instr_06_then_ff: bool, + #[bits(2)] + __: u8, + pub soft_reset_66_99: bool, + #[bits(4)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Mode8s8s8sEnableSequences { + __: bool, + pub instr_06_then_e8: bool, + pub instr_06_then_72: bool, + #[bits(5)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Mode088ExitMethod { + pub mode_bits: bool, + pub ff_on_dq: bool, + #[bits(6)] + __: u8, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Mode088EntryMethod { + pub read_conf_reg: bool, + #[bits(7)] + __: u8, +} + +#[derive(Debug)] +pub enum OctalEnableReqs { + NoOctalEnableBit = 0b000, + OctalEnableIsBit3 = 0b001, + Reserved0 = 0b010, + Reserved1 = 0b100, + Reserved2 = 0b101, + Reserved3 = 0b110, + Reserved4 = 0b111, +} + +impl OctalEnableReqs { + const fn from_bits(bits: u8) -> Self { + match bits & 0b111 { + 0b000 => Self::NoOctalEnableBit, + 0b001 => Self::OctalEnableIsBit3, + 0b010 => Self::Reserved0, + 0b100 => Self::Reserved1, + 0b101 => Self::Reserved2, + 0b110 => Self::Reserved3, + _ => Self::Reserved4, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord20 { + #[bits(4)] + pub max_speed_4s_4s_4s_no_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_4s_4s_4s_with_data_strobe: MaxSpeed4s4s4sWithDataStrobe, + + #[bits(4)] + pub max_speed_4s_4d_4d_no_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_4s_4d_4d_with_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_8s_8s_8s_no_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_8s_8s_8s_with_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_8d_8d_8d_no_data_strobe: MaxSpeed, + + #[bits(4)] + pub max_speed_8d_8d_8d_with_data_strobe: MaxSpeed, +} + +#[derive(Debug)] +pub enum MaxSpeed { + Reserved0 = 0b0000, + _33Mhz = 0b0001, + _50Mhz = 0b0010, + _66Mhz = 0b0011, + _80Mhz = 0b0100, + _100Mhz = 0b0101, + _133Mhz = 0b0110, + _166Mhz = 0b0111, + _200Mhz = 0b1000, + _250Mhz = 0b1001, + _266Mhz = 0b1010, + _333Mhz = 0b1011, + _400Mhz = 0b1100, + Reserved1 = 0b1101, + NotCharacterized = 0b1110, + NotSupported = 0b1111, +} + +impl MaxSpeed { + const fn from_bits(bits: u8) -> Self { + match bits & 0b1111 { + 0b0000 => Self::Reserved0, + 0b0001 => Self::_33Mhz, + 0b0010 => Self::_50Mhz, + 0b0011 => Self::_66Mhz, + 0b0100 => Self::_80Mhz, + 0b0101 => Self::_100Mhz, + 0b0110 => Self::_133Mhz, + 0b0111 => Self::_166Mhz, + 0b1000 => Self::_200Mhz, + 0b1001 => Self::_250Mhz, + 0b1010 => Self::_266Mhz, + 0b1011 => Self::_333Mhz, + 0b1100 => Self::_400Mhz, + 0b1101 => Self::Reserved1, + 0b1110 => Self::NotCharacterized, + _ => Self::NotSupported, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[derive(Debug)] +pub enum MaxSpeed4s4s4sWithDataStrobe { + Reserved0 = 0b0000, + _33Mhz = 0b0001, + _50Mhz = 0b0010, + _66Mhz = 0b0011, + _80Mhz = 0b0100, + _100Mhz = 0b0101, + _133Mhz = 0b0110, + _166Mhz = 0b0111, + Reserved1 = 0b1000, + Reserved2 = 0b1001, + Reserved3 = 0b1010, + Reserved4 = 0b1011, + Reserved5 = 0b1100, + Reserved6 = 0b1101, + NotCharacterized = 0b1110, + NotSupported = 0b1111, +} + +impl MaxSpeed4s4s4sWithDataStrobe { + const fn from_bits(bits: u8) -> Self { + match bits & 0b1111 { + 0b0000 => Self::Reserved0, + 0b0001 => Self::_33Mhz, + 0b0010 => Self::_50Mhz, + 0b0011 => Self::_66Mhz, + 0b0100 => Self::_80Mhz, + 0b0101 => Self::_100Mhz, + 0b0110 => Self::_133Mhz, + 0b0111 => Self::_166Mhz, + 0b1000 => Self::Reserved1, + 0b1001 => Self::Reserved2, + 0b1010 => Self::Reserved3, + 0b1011 => Self::Reserved4, + 0b1100 => Self::Reserved5, + 0b1101 => Self::Reserved6, + 0b1110 => Self::NotCharacterized, + _ => Self::NotSupported, + } + } + const fn into_bits(self) -> u8 { + self as u8 + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord21 { + pub support_1s_1d_1d_fast_read: bool, + + pub support_1s_2d_2d_fast_read: bool, + + pub support_1s_4d_4d_fast_read: bool, + + pub support_4s_4d_4d_fast_read: bool, + + #[bits(28)] + __: u32, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord22 { + #[bits(5)] + pub fast_read_1s_1d_1d_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_1d_1d_mode_clocks: u8, + + pub fast_read_1s_1d_1d_instr: u8, + + #[bits(5)] + pub fast_read_1s_2d_2d_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_2d_2d_mode_clocks: u8, + + pub fast_read_1s_2d_2d_instr: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BasicFlashWord23 { + #[bits(5)] + pub fast_read_1s_4d_4d_wait_states: u8, + + #[bits(3)] + pub fast_read_1s_4d_4d_mode_clocks: u8, + + pub fast_read_1s_4d_4d_instr: u8, + + #[bits(5)] + pub fast_read_4s_4d_4d_wait_states: u8, + + #[bits(3)] + pub fast_read_4s_4d_4d_mode_clocks: u8, + + pub fast_read_4s_4d_4d_instr: u8, +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, Debug)] +pub struct MemoryDensity(u32); +impl MemoryDensity { + pub const fn from_byte_len(byte_len: u32) -> Result { + if byte_len == 0 { + return Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY); + } + match byte_len.checked_mul(8) { + Some(bit_len) if bit_len <= 0x8000_0000 => Ok(Self(bit_len - 1)), + _ => { + if !byte_len.is_power_of_two() { + return Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY); + } + let shift = byte_len.trailing_zeros(); + Ok(Self(0x8000_0000 | (shift + 3))) + } + } + } + pub const fn byte_len(&self) -> Result { + if (self.0 & 0x8000_0000) == 0 { + return Ok((self.0 + 1) / 8); + } + // -3 to convert bits to bytes + let bits_power = self.0 & !0x8000_0000; + if bits_power < 3 { + return Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY); + } + let power = bits_power - 3; + if power > 31 { + return Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY); + } + Ok(1 << power) + } +} + +#[cfg(test)] +mod density_tests { + use zerocopy::FromBytes; + + use super::*; + + #[test] + fn small() { + let density: MemoryDensity = + MemoryDensity::read_from_bytes(&[0xFF, 0xFF, 0xFF, 0x00]).unwrap(); + assert_eq!(density.byte_len().unwrap(), 16 * 1024 * 1024 / 8); + assert_eq!( + MemoryDensity::from_byte_len(2 * 1024 * 1024) + .unwrap() + .as_bytes(), + &[0xFF, 0xFF, 0xFF, 0x00] + ); + } + #[test] + fn big() { + let density: MemoryDensity = + MemoryDensity::read_from_bytes(&[0x21, 0x00, 0x00, 0x80]).unwrap(); + assert_eq!(density.byte_len().unwrap(), 1024 * 1024 * 1024); + assert_eq!( + MemoryDensity::from_byte_len(1024 * 1024 * 1024) + .unwrap() + .as_bytes(), + &[0x21, 0x00, 0x00, 0x80] + ); + } + #[test] + fn too_big() { + let density: MemoryDensity = + MemoryDensity::read_from_bytes(&[0x24, 0x00, 0x00, 0x80]).unwrap(); + assert_eq!( + density.byte_len(), + Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + ); + } + + #[test] + fn from_byte_len() { + assert_eq!( + MemoryDensity::from_byte_len(0), + Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + ); + assert_eq!(MemoryDensity::from_byte_len(1), Ok(MemoryDensity(0x7))); + assert_eq!(MemoryDensity::from_byte_len(2), Ok(MemoryDensity(0xf))); + assert_eq!(MemoryDensity::from_byte_len(3), Ok(MemoryDensity(0x17))); + assert_eq!(MemoryDensity::from_byte_len(4), Ok(MemoryDensity(0x1f))); + assert_eq!( + MemoryDensity::from_byte_len(1024), + Ok(MemoryDensity(0x1fff)) + ); + assert_eq!( + MemoryDensity::from_byte_len(1025), + Ok(MemoryDensity(0x2007)) + ); + assert_eq!( + MemoryDensity::from_byte_len(1024 * 1024), + Ok(MemoryDensity(0x7f_ffff)) + ); + assert_eq!( + MemoryDensity::from_byte_len(256 * 1024 * 1024), + Ok(MemoryDensity(0x7fff_ffff)) + ); + assert_eq!( + MemoryDensity::from_byte_len(256 * 1024 * 1024 + 1), + Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + ); + assert_eq!( + MemoryDensity::from_byte_len(512 * 1024 * 1024), + Ok(MemoryDensity(0x8000_0020)) + ); + assert_eq!( + MemoryDensity::from_byte_len(1024 * 1024 * 1024), + Ok(MemoryDensity(0x8000_0021)) + ); + assert_eq!( + MemoryDensity::from_byte_len(2048 * 1024 * 1024), + Ok(MemoryDensity(0x8000_0022)) + ); + assert_eq!( + MemoryDensity::from_byte_len(u32::MAX), + Err(error::FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY) + ); + } +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, Debug)] +#[repr(C)] +pub struct BasicFlashParameterTable { + pub table_jesd216: BasicFlashParameterTableJESD216, + pub table_jesd216a: BasicFlashParameterTableJESD216A, + pub table_jesd216c: BasicFlashParameterTableJESD216C, + pub table_jesd216f: BasicFlashParameterTableJESD216F, +} + +trait ParameterTable { + const FUNCTION: FunctionSpecificParameterTableIdAssignments; + const MIN_SIZE: usize; +} + +impl ParameterTable for BasicFlashParameterTable { + const FUNCTION: FunctionSpecificParameterTableIdAssignments = + FunctionSpecificParameterTableIdAssignments::BASIC_FLASH; + const MIN_SIZE: usize = size_of::(); +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, Debug)] +#[repr(C)] +pub struct BasicFlashParameterTableJESD216 { + pub word1: BasicFlashWord1, + pub memory_density: MemoryDensity, + pub word3: BasicFlashWord3, + pub word4: BasicFlashWord4, + pub word5: BasicFlashWord5, + pub word6: BasicFlashWord6, + pub word7: BasicFlashWord7, + pub word8: BasicFlashWord8, + pub word9: BasicFlashWord9, +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, Debug)] +#[repr(C)] +pub struct BasicFlashParameterTableJESD216A { + pub word10: BasicFlashWord10, + pub word11: BasicFlashWord11, + pub word12: BasicFlashWord12, + pub word13: BasicFlashWord13, + pub word14: BasicFlashWord14, + pub word15: BasicFlashWord15, + pub word16: BasicFlashWord16, +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, Debug)] +#[repr(C)] +pub struct BasicFlashParameterTableJESD216C { + pub word17: BasicFlashWord17, + pub word18: BasicFlashWord18, + pub word19: BasicFlashWord19, + pub word20: BasicFlashWord20, +} + +#[derive(Clone, Copy, Eq, PartialEq, FromBytes, IntoBytes, Immutable, Debug)] +#[repr(C)] +pub struct BasicFlashParameterTableJESD216F { + pub word21: BasicFlashWord21, + pub word22: BasicFlashWord22, + pub word23: BasicFlashWord23, +} + +macro_rules! field_opt_getter { + ($param1:ident, $param2:ty) => { + pub fn $param1(&self) -> Option<&$param2> { + if self.header.len_in_dwords as usize * size_of::() + >= offset_of!(BasicFlashParameterTable, $param1) + size_of::<$param2>() + { + Some(&self.table.$param1) + } else { + None + } + } + }; +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, KnownLayout)] +pub struct _4BInstructionTableWord1 { + pub _1s_1s_1s_read_support: bool, + pub _1s_1s_1s_fast_read_support: bool, + pub _1s_1s_2s_fast_read_support: bool, + pub _1s_2s_2s_fast_read_support: bool, + pub _1s_1s_4s_fast_read_support: bool, + pub _1s_4s_4s_fast_read_support: bool, + pub _1s_1s_1s_page_program_support: bool, + pub _1s_1s_4s_page_program_support: bool, + pub _1s_4s_4s_page_program_support: bool, + pub erase_type_1_support: bool, + pub erase_type_2_support: bool, + pub erase_type_3_support: bool, + pub erase_type_4_support: bool, + pub _1s_1d_1d_dtr_read_support: bool, + pub _1s_2d_2d_dtr_read_support: bool, + pub _1s_4d_4d_dtr_read_support: bool, + pub volatile_sector_lock_read_support: bool, + pub volatile_sector_lock_write_support: bool, + pub non_volatile_sector_lock_read_support: bool, + pub non_volatile_sector_lock_write_support: bool, + pub _1s_1s_8s_fast_read_support: bool, + pub _1s_8s_8s_fast_read_support: bool, + pub _1s_8d_8d_dtr_read_support: bool, + pub _1s_1s_8s_page_program_support: bool, + pub _1s_8s_8s_page_program_support: bool, + #[bits(7)] + __: u8, +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, KnownLayout)] +pub struct _4BInstructionTableWord2 { + pub erase_type_1_instr: u8, + pub erase_type_2_instr: u8, + pub erase_type_3_instr: u8, + pub erase_type_4_instr: u8, +} + +#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout)] +#[repr(C)] +pub struct _4BInstructionTable { + pub word1: _4BInstructionTableWord1, + pub word2: _4BInstructionTableWord2, +} + +impl ParameterTable for _4BInstructionTable { + const FUNCTION: FunctionSpecificParameterTableIdAssignments = + FunctionSpecificParameterTableIdAssignments::_4B_ADDRESS_INSTRUCTION_TABLE; + const MIN_SIZE: usize = size_of::<_4BInstructionTable>(); +} + +#[derive(Debug, PartialEq, Eq)] +pub struct HeaderAndTable { + pub header: ParameterHeader, + table: T, +} + +impl HeaderAndTable { + pub fn table_jesd216(&self) -> BasicFlashParameterTableJESD216 { + self.table.table_jesd216 + } + field_opt_getter! {table_jesd216a, BasicFlashParameterTableJESD216A} + field_opt_getter! {table_jesd216c, BasicFlashParameterTableJESD216C} + field_opt_getter! {table_jesd216f, BasicFlashParameterTableJESD216F} +} + +pub struct SfdpReader { + reader: R, + basic_flash_header: Option, + _4b_instructions_header: Option, +} + +impl>> SfdpReader { + pub fn new(reader: R) -> Result { + let mut sfdp_reader = Self { + reader, + basic_flash_header: None, + _4b_instructions_header: None, + }; + sfdp_reader.parse_headers()?; + Ok(sfdp_reader) + } + + pub fn header(&mut self) -> Result { + let mut header = SfdpHeader::new_zeroed(); + self.reader.read(0, header.as_mut_bytes())?; + Ok(header) + } + + pub fn parameter_header(&mut self, index: u8) -> Result { + let mut parameter_header = ParameterHeader::new_zeroed(); + self.reader.read( + size_of::() * (index as usize + 1), + parameter_header.as_mut_bytes(), + )?; + Ok(parameter_header) + } + + fn parse_headers(&mut self) -> Result<(), R::Error> { + let sfdp_header = self.header()?; + if !sfdp_header.sig.is_valid() { + return Err(error::FLASH_GENERIC_SFDP_INVALID_SIGNATURE.into()); + } + if sfdp_header.major_rev != 1 { + return Err(error::FLASH_GENERIC_SFDP_UNSUPPORTED_HEADER_MAJOR_REV.into()); + } + + // TODO: b/403297476 - Use the most up to date version we support + // Right now it will just overwrite with the latest header that matches the function + for index in 0..=sfdp_header.num_parameter_header { + let Ok(parameter_header) = self.parameter_header(index) else { + // Failed to parse this header. + continue; + }; + if parameter_header.major_rev != 1 { + // unsupported header, but maybe we can find another supported one + continue; + } + let h = match parameter_header.parameter_id() { + FunctionSpecificParameterTableIdAssignments::BASIC_FLASH => { + &mut self.basic_flash_header + } + FunctionSpecificParameterTableIdAssignments::_4B_ADDRESS_INSTRUCTION_TABLE => { + &mut self._4b_instructions_header + } + _ => { + // Skip header + continue; + } + }; + *h = Some(parameter_header); + } + Ok(()) + } + + fn read_table( + &mut self, + ) -> Result, R::Error> { + let Some(parameter_header) = (match T::FUNCTION { + FunctionSpecificParameterTableIdAssignments::BASIC_FLASH => self.basic_flash_header, + FunctionSpecificParameterTableIdAssignments::_4B_ADDRESS_INSTRUCTION_TABLE => { + self._4b_instructions_header + } + _ => return Err(error::FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND.into()), + }) else { + return Err(error::FLASH_GENERIC_SFDP_NO_VALID_PARAMETER_HEADER_FOUND.into()); + }; + let mut table_inner = T::new_zeroed(); + + self.read_table_common(parameter_header, T::MIN_SIZE, table_inner.as_mut_bytes())?; + + Ok(HeaderAndTable { + header: parameter_header, + table: table_inner, + }) + } + + fn read_table_common( + &mut self, + parameter_header: ParameterHeader, + min_bytes: usize, + target_slice: &mut [u8], + ) -> Result<(), R::Error> { + let bytes_len = (parameter_header.len_in_dwords as usize) + .checked_mul(size_of::()) + .ok_or(error::FLASH_GENERIC_SFDP_PARAMETERS_TOO_LONG)? + .min(target_slice.len()); + if bytes_len < min_bytes { + return Err(error::FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT.into()); + } + + let target = &mut target_slice[..bytes_len]; + self.reader.read(parameter_header.ptr.as_usize(), target)?; + Ok(()) + } + + pub fn basic_flash_parameters( + &mut self, + ) -> Result, R::Error> { + self.read_table::() + } + + pub fn _4b_instructions_parameters( + &mut self, + ) -> Result, R::Error> { + self.read_table::<_4BInstructionTable>() + } +} + +#[cfg(test)] +mod test { + use error::FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT; + + use super::*; + + #[test] + fn too_long() { + let sfdp_bytes_params_too_long: &[u8] = &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x00, 0xff, 0x00, 0x05, 0x01, + /*param len =*/ 0x20, 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe5, 0x20, 0xf9, 0xff, 0xff, 0xff, + 0xff, 0x07, 0x44, 0xeb, 0x08, 0x6b, 0x08, 0x3b, 0x42, 0xbb, 0xfe, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x40, 0xeb, 0x0c, 0x20, 0x0f, 0x52, 0x10, 0xd8, + 0x00, 0x00, 0x36, 0x02, 0xa6, 0x00, 0x82, 0xea, 0x14, 0xc9, 0xe9, 0x63, 0x76, 0x33, + 0x7a, 0x75, 0x7a, 0x75, 0xf7, 0xa2, 0xd5, 0x5c, 0x19, 0xf7, 0x4d, 0xff, 0xe9, 0x30, + 0xf8, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]; + let mut sfdp_reader: SfdpReader<&[u8]> = + SfdpReader::new(sfdp_bytes_params_too_long).unwrap(); + let params = sfdp_reader.basic_flash_parameters().unwrap(); + assert!(params.table_jesd216f().is_some()); + } + + #[test] + fn too_short() { + let sfdp_bytes_params_too_short: &[u8] = &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x00, 0xff, 0x00, 0x05, 0x01, + /*param len =*/ 0x02, 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe5, 0x20, 0xf9, 0xff, 0xff, 0xff, + 0xff, 0x07, 0x44, 0xeb, 0x08, 0x6b, 0x08, 0x3b, 0x42, 0xbb, 0xfe, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x40, 0xeb, 0x0c, 0x20, 0x0f, 0x52, 0x10, 0xd8, + 0x00, 0x00, 0x36, 0x02, 0xa6, 0x00, 0x82, 0xea, 0x14, 0xc9, 0xe9, 0x63, 0x76, 0x33, + 0x7a, 0x75, 0x7a, 0x75, 0xf7, 0xa2, 0xd5, 0x5c, 0x19, 0xf7, 0x4d, 0xff, 0xe9, 0x30, + 0xf8, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]; + let mut sfdp_reader: SfdpReader<&[u8]> = + SfdpReader::new(sfdp_bytes_params_too_short).unwrap(); + assert_eq!( + sfdp_reader.basic_flash_parameters(), + Err(FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT) + ); + } + + #[test] + fn print_params() { + let sfdp_bytes: &[u8] = &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x00, 0xff, 0x00, 0x05, 0x01, 0x10, 0x80, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xe5, 0x20, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x07, 0x44, 0xeb, 0x08, 0x6b, + 0x08, 0x3b, 0x42, 0xbb, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x40, 0xeb, 0x0c, 0x20, 0x0f, 0x52, 0x10, 0xd8, 0x00, 0x00, 0x36, 0x02, 0xa6, 0x00, + 0x82, 0xea, 0x14, 0xc9, 0xe9, 0x63, 0x76, 0x33, 0x7a, 0x75, 0x7a, 0x75, 0xf7, 0xa2, + 0xd5, 0x5c, 0x19, 0xf7, 0x4d, 0xff, 0xe9, 0x30, 0xf8, 0x80, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + ]; + + let mut sfdp_reader: SfdpReader<&[u8]> = SfdpReader::new(sfdp_bytes).unwrap(); + + let sfdp_header = sfdp_reader.header().unwrap(); + assert!(sfdp_header.sig.is_valid()); + assert_eq!(sfdp_header.access_protocol, AccessProtocol::LEGACY); + assert_eq!(sfdp_header.major_rev, 1); + assert_eq!(sfdp_header.minor_rev, 5); + assert_eq!(sfdp_header.num_parameter_header, 0); + + let basic_flash = sfdp_reader.basic_flash_parameters().unwrap(); + + assert_eq!( + basic_flash.header.parameter_id(), + FunctionSpecificParameterTableIdAssignments::BASIC_FLASH + ); + assert_eq!(basic_flash.header.major_rev, 1); + assert_eq!(basic_flash.header.minor_rev, 5); + assert_eq!(basic_flash.header.len_in_dwords, 16); + assert_eq!(basic_flash.header.ptr.as_u32(), 128); + + assert_eq!( + format!("{:#?}", basic_flash.table_jesd216()), + r#"BasicFlashParameterTableJESD216 { + word1: BasicFlashWord1 { + legacy_erase_sizes: Erase4k, + legacy_write_granularity: Buffer64, + block_protect_is_volatile: false, + status_write_requires_write_enable: false, + erase4k_instr: 32, + supports_1s_1s_2s_read: true, + addr_bytes: _3ByteOnly, + dtr_clocking_supported: true, + supports_1s_2s_2s_read: true, + supports_1s_4s_4s_read: true, + supports_1s_1s_4s_read: true, + }, + memory_density: MemoryDensity( + 134217727, + ), + word3: BasicFlashWord3 { + fast_read_1s_4s_4s_wait_states: 4, + fast_read_1s_4s_4s_mode_clocks: 2, + fast_read_1s_4s_4s_instr: 235, + fast_read_1s_1s_4s_wait_states: 8, + fast_read_1s_1s_4s_mode_clocks: 0, + fast_read_1s_1s_4s_instr: 107, + }, + word4: BasicFlashWord4 { + fast_read_1s_1s_2s_wait_states: 8, + fast_read_1s_1s_2s_mode_clocks: 0, + fast_read_1s_1s_2s_instr: 59, + fast_read_1s_2s_2s_wait_states: 2, + fast_read_1s_2s_2s_mode_clocks: 2, + fast_read_1s_2s_2s_instr: 187, + }, + word5: BasicFlashWord5 { + fast_read_2x_2s_2s_supported: false, + fast_read_4x_4s_4s_supported: true, + }, + word6: BasicFlashWord6 { + fast_read_2s_2s_2s_wait_states: 0, + fast_read_2s_2s_2s_mode_clocks: 0, + fast_read_2s_2s_2s_instr: 0, + }, + word7: BasicFlashWord7 { + fast_read_4s_4s_4s_wait_states: 0, + fast_read_4s_4s_4s_mode_clocks: 2, + fast_read_4s_4s_4s_instr: 235, + }, + word8: BasicFlashWord8 { + erase_type_1_size: PowerOf2 { + n: 12, + }, + erase_type_1_instr: 32, + erase_type_2_size: PowerOf2 { + n: 15, + }, + erase_type_2_instr: 82, + }, + word9: BasicFlashWord9 { + erase_type_3_size: PowerOf2 { + n: 16, + }, + erase_type_3_instr: 216, + erase_type_4_size: PowerOf2 { + n: 0, + }, + erase_type_4_instr: 0, + }, +}"# + ); + assert_eq!( + format!("{:#?}", basic_flash.table_jesd216a().unwrap()), + r#"BasicFlashParameterTableJESD216A { + word10: BasicFlashWord10 { + multiplier_max_erase_time: 6, + erase_type_1_time: EraseTime { + count: 3, + units: _16ms, + }, + erase_type_2_time: EraseTime { + count: 0, + units: _128ms, + }, + erase_type_3_time: EraseTime { + count: 9, + units: _16ms, + }, + erase_type_4_time: EraseTime { + count: 0, + units: _1ms, + }, + }, + word11: BasicFlashWord11 { + multiplier_max_page_program: 2, + page_size: SmallPowerOf2 { + n: 8, + }, + page_program_time: PageProgramTime { + count: 10, + units: _64us, + }, + byte_program_first_time: ByteProgramTime { + count: 3, + unit: _8us, + }, + byte_program_additional_time: ByteProgramTime { + count: 2, + unit: _1us, + }, + chip_erase_time: ChipEraseTime { + count: 9, + units: _16ms, + }, + }, + word12: BasicFlashWord12 { + prohibited_ops_during_program_suspend: ProhibitedOpsDuringProgramSuspend { + erase_nesting_permitted: true, + program_nesting_permitted: false, + no_read: false, + erase_and_program_restrictions_are_sufficient: true, + }, + prohibited_ops_during_erase_suspend: ProhibitedOpsDuringEraseSuspend { + erase_nesting_permitted: false, + program_nesting_permitted: true, + no_read: true, + erase_and_program_restrictions_are_sufficient: true, + }, + program_resume_suspend_interval: Interval64us { + counts_64ms: 1, + }, + suspend_in_progress_program_max_latency: DurationEnumA { + count: 19, + units: _1us, + }, + erase_resume_suspend_interval: Interval64us { + counts_64ms: 7, + }, + suspend_in_progress_erase_max_latency: DurationEnumA { + count: 19, + units: _1us, + }, + suspend_resume_supported: false, + }, + word13: BasicFlashWord13 { + program_resume_instr: 122, + program_suspend_instr: 117, + resume_instr: 122, + suspend_instr: 117, + }, + word14: BasicFlashWord14 { + status_register_polling_device_busy: StatusRegisterPollingDeviceBusy { + legacy_polling_supported: true, + bit_7_polled_any_time: false, + }, + exit_deep_powerdown_next_op_delay: DurationEnumA { + count: 2, + units: _1us, + }, + exit_deep_powerdown_instr: 171, + enter_deep_powerdown_instr: 185, + deep_powerdown_supported: false, + }, + word15: BasicFlashWord15 { + mode_disable_sequence_4s_4s_4s: ModeDisableSequence4S4S4S { + issue_ff_instr: true, + issue_f5_instr: false, + read_mod_write_seq: false, + issue_soft_reset: true, + }, + mode_enable_sequence_4s_4s_4s: ModeEnableSequence4S4S4S { + set_qe: true, + issue_38_instr: false, + issue_35_instr: false, + read_mod_write_seq1: false, + read_mod_write_seq2: true, + }, + mode_0_4_4_supported: true, + mode_exit_method_0_4_4: ModeExitMethod044 { + mod_bits_00: true, + input_io1: false, + input_io2: true, + mod_bits_not_ax: true, + }, + mode_entry_method_0_4_4: ModeEntryMethod044 { + mod_bits_a5: true, + instr_85: false, + mod_bits_ax: true, + }, + quad_enable_requirements: QeBit1SR2C, + hold_or_reset_disable: false, + }, + word16: BasicFlashWord16 { + volatile_non_register_write_enable_instr: VolatileNonRegisterWriteEnableInstr { + non_volatile_reg1_last_written_we06: true, + volatile_reg1_last_written_we06: false, + volatile_reg1_last_written_we50: false, + volatile_non_reg1_last_written_we0650: true, + mix_volatile_we06: false, + }, + soft_reset_rescue_seq_support: SoftResetRescureSeqSupport { + drive0_f8_clk: false, + drive0_f10_clk: false, + drive0_f16_clk: false, + instr_f0: false, + instr66_then99: true, + exit044_first: true, + }, + exit_4_b_addressing: Exit4BAddressing { + instr_e9: false, + instr_06_then_e9: false, + ear: false, + bank_reg: false, + conf_reg: false, + hardware_reset: true, + software_reset: true, + power_cycle: true, + }, + enter_4_b_addressing: Enter4BAddressing { + intr_b7: false, + instr_06_then_b7: false, + ear: false, + bank_reg: false, + conf_reg: false, + dedicated_add: false, + always_4b: false, + }, + }, +}"# + ); + + assert!(basic_flash.table_jesd216c().is_none()); + assert!(basic_flash.table_jesd216f().is_none()); + } + + struct TestSample<'a> { + name: &'a str, + sfdp_bytes: &'a [u8], + expected_mem_density: u32, + expected_page_size_from_jesd216a: Option, + expected_4b_table: bool, + } + + const TEST_SAMPLES: &[TestSample] = &[ + TestSample { + name: "test_1", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x01, 0xFF, 0x00, 0x05, 0x01, 0x10, 0x18, 0x00, + 0x00, 0xFF, 0x26, 0x00, 0x01, 0x04, 0x58, 0x00, 0x00, 0xFF, 0xF5, 0x20, 0x85, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x08, 0x3B, 0x00, 0x00, 0xEE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x0C, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x81, 0xEF, 0xFF, 0xE1, 0x00, 0x01, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFF, + 0x82, 0x00, 0x00, 0x40, 0x47, 0x4F, 0x4F, 0x47, 0x00, 0x00, 0x70, 0x00, 0x00, 0x04, + 0x00, 0x00, 0xED, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ], + expected_mem_density: 64 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: false, + }, + TestSample { + name: "test_2", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x00, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, + 0x00, 0xFF, 0xC2, 0x00, 0x01, 0x04, 0x60, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x20, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, + 0x44, 0xEB, 0x08, 0x6B, 0x08, 0x3B, 0x04, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0x44, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x36, + 0x00, 0x27, 0x9D, 0xF9, 0xC0, 0x64, 0x85, 0xCB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ], + expected_mem_density: 32 * 1024 * 1024, + expected_page_size_from_jesd216a: None, // doesn't support JESD216A + expected_4b_table: false, + }, + TestSample { + name: "test_3", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x01, 0xFF, 0x00, 0x05, 0x01, 0x10, 0x18, 0x00, + 0x00, 0xFF, 0x26, 0x00, 0x01, 0x04, 0x58, 0x00, 0x00, 0xFF, 0xF5, 0x20, 0x85, 0xFF, + 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x08, 0x3B, 0x00, 0x00, 0xEE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x0C, 0x20, 0x10, 0xD8, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x81, 0xEF, 0xFF, 0xE1, 0x00, 0x01, + 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xFF, + 0x82, 0x00, 0x00, 0x40, 0x47, 0x4F, 0x4F, 0x47, 0x00, 0x00, 0x90, 0x00, 0x00, 0x04, + 0x00, 0x00, 0xEF, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ], + expected_mem_density: 32 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: false, + }, + TestSample { + name: "test_4", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x00, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, + 0x00, 0xFF, 0xC2, 0x00, 0x01, 0x04, 0x60, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x20, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, + 0x44, 0xEB, 0x08, 0x6B, 0x08, 0x3B, 0x04, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0x44, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x36, + 0x00, 0x27, 0x9D, 0xF9, 0xC0, 0x64, 0x85, 0xCB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ], + expected_mem_density: 64 * 1024 * 1024, + expected_page_size_from_jesd216a: None, // doesn't support JESD216A + expected_4b_table: false, + }, + TestSample { + name: "test_5", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x02, 0xFF, // + // v=1.6, l=0x10, ptr=0x30, f=0xff00 + 0x00, 0x06, 0x01, 0x10, 0x30, 0x00, 0x00, 0xFF, // + // v=1.0, l=0x04, ptr=0x10, f=0xffc2 + 0xC2, 0x00, 0x01, 0x04, 0x10, 0x01, 0x00, 0xFF, // + // v=1.0, l=0x02, ptr=0xC0, f=0xff84, the data is outside of this sample data + 0x84, 0x00, 0x01, 0x02, 0xC0, 0x00, 0x00, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xE5, 0x20, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, // + 0x44, 0xEB, 0x08, 0x6B, 0x08, 0x3B, 0x04, 0xBB, // + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // + 0xFF, 0xFF, 0x44, 0xEB, 0x0C, 0x20, 0x0F, 0x52, // + 0x10, 0xD8, 0x00, 0xFF, 0xD6, 0x49, 0xC5, 0x00, // + 0x81, 0xDF, 0x04, 0xE3, 0x44, 0x03, 0x67, 0x38, // + 0x30, 0xB0, 0x30, 0xB0, 0xF7, 0xBD, 0xD5, 0x5C, // + 0x4A, 0x9E, 0x29, 0xFF, 0xF0, 0x50, 0xF9, 0x85, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + ], + expected_mem_density: 64 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: true, + }, + TestSample { + name: "test_6", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x02, 0xFF, 0x00, 0x06, 0x01, 0x10, 0x30, 0x00, + 0x00, 0xFF, 0xC2, 0x00, 0x01, 0x04, 0x10, 0x01, 0x00, 0xFF, 0x84, 0x00, 0x01, 0x02, + 0xC0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x20, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0x44, 0xEB, 0x08, 0x6B, 0x08, 0x3B, 0x04, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0x44, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0xFF, + 0xD6, 0x49, 0xC5, 0x00, 0x85, 0xDF, 0x04, 0xE3, 0x44, 0x03, 0x67, 0x38, 0x30, 0xB0, + 0x30, 0xB0, 0xF7, 0xBD, 0xD5, 0x5C, 0x4A, 0x9E, 0x29, 0xFF, 0xF0, 0x50, 0xF9, 0x85, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ], + expected_mem_density: 128 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: true, + }, + TestSample { + name: "test_7", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x02, 0xFF, 0x00, 0x06, 0x01, 0x10, 0x30, 0x00, + 0x00, 0xFF, 0xC2, 0x00, 0x01, 0x04, 0x10, 0x01, 0x00, 0xFF, 0x84, 0x00, 0x01, 0x02, + 0xC0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x20, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0x44, 0xEB, 0x08, 0x6B, 0x08, 0x3B, 0x04, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0xFF, 0xFF, 0xFF, 0x44, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0xFF, + 0xD6, 0x49, 0xC5, 0x00, 0x85, 0xDF, 0x04, 0xE3, 0x44, 0x03, 0x67, 0x38, 0x30, 0xB0, + 0x30, 0xB0, 0xF7, 0xBD, 0xD5, 0x5C, 0x4A, 0x9E, 0x29, 0xFF, 0xF0, 0x50, 0xF9, 0x85, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, + ], + expected_mem_density: 128 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: true, + }, + TestSample { + name: "test_8", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x05, 0x01, 0x00, 0xFF, 0x00, 0x05, 0x01, 0x10, 0x80, 0x00, + 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE5, 0x20, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x44, 0xEB, 0x08, 0x6B, + 0x08, 0x3B, 0x42, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0x40, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0x00, 0x36, 0x02, 0xA6, 0x00, + 0x82, 0xEA, 0x14, 0xC9, 0xE9, 0x63, 0x76, 0x33, 0x7A, 0x75, 0x7A, 0x75, 0xF7, 0xA2, + 0xD5, 0x5C, 0x19, 0xF7, 0x4D, 0xFF, 0xE9, 0x30, 0xF8, 0x80, + ], + expected_mem_density: 16 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: false, + }, + TestSample { + name: "test_9", + sfdp_bytes: &[ + 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x01, 0xFF, 0x00, 0x06, 0x01, 0x10, 0x80, 0x00, + 0x00, 0xFF, 0x84, 0x00, 0x01, 0x02, 0xD0, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x01, 0x02, + 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE5, 0x20, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x44, 0xEB, 0x08, 0x6B, + 0x08, 0x3B, 0x42, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0x40, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0x00, 0x36, 0x02, 0xA6, 0x00, + 0x82, 0xEA, 0x14, 0xE2, 0xE9, 0x63, 0x76, 0x33, 0x7A, 0x75, 0x7A, 0x75, 0xF7, 0xA2, + 0xD5, 0x5C, 0x19, 0xF7, 0x4D, 0xFF, 0xE9, 0x70, 0xF9, 0xA5, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, + 0xF0, 0xFF, 0x21, 0xFF, 0xDC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + ], + expected_mem_density: 128 * 1024 * 1024, + expected_page_size_from_jesd216a: Some(256), + expected_4b_table: true, + }, + ]; + + #[test] + fn test_all_samples() { + for td in TEST_SAMPLES { + println!("Sample name: {}", td.name); + let mut sfdp_reader: SfdpReader<&[u8]> = SfdpReader::new(td.sfdp_bytes).unwrap(); + let basic_flash = sfdp_reader.basic_flash_parameters().unwrap(); + assert_eq!( + basic_flash + .table_jesd216() + .memory_density + .byte_len() + .unwrap(), + td.expected_mem_density + ); + assert_eq!( + basic_flash + .table_jesd216a() + .map(|t| t.word11.page_size().value()), + td.expected_page_size_from_jesd216a + ); + if td.expected_4b_table { + assert!(sfdp_reader._4b_instructions_header.is_some()); + } else { + assert_eq!(sfdp_reader._4b_instructions_header, None); + } + } + } + + #[test] + fn get_4b_table() { + let sfdp_bytes: &[u8] = &[ + 0x53, 0x46, 0x44, 0x50, 0x06, 0x01, 0x01, 0xFF, 0x00, 0x06, 0x01, 0x10, 0x80, 0x00, + 0x00, 0xFF, 0x84, 0x00, 0x01, 0x02, 0xD0, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x01, 0x02, + 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xE5, 0x20, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x44, 0xEB, 0x08, 0x6B, + 0x08, 0x3B, 0x42, 0xBB, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0x40, 0xEB, 0x0C, 0x20, 0x0F, 0x52, 0x10, 0xD8, 0x00, 0x00, 0x36, 0x02, 0xA6, 0x00, + 0x82, 0xEA, 0x14, 0xE2, 0xE9, 0x63, 0x76, 0x33, 0x7A, 0x75, 0x7A, 0x75, 0xF7, 0xA2, + 0xD5, 0x5C, 0x19, 0xF7, 0x4D, 0xFF, 0xE9, 0x70, 0xF9, 0xA5, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, + 0xF0, 0xFF, 0x21, 0xFF, 0xDC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + ]; + + let mut sfdp_reader: SfdpReader<&[u8]> = SfdpReader::new(sfdp_bytes).unwrap(); + let table = sfdp_reader.read_table::<_4BInstructionTable>().unwrap(); + assert!(table.table.word1.erase_type_1_support()); + assert!(table.table.word1.erase_type_3_support()); + assert_eq!(table.table.word2.erase_type_1_instr(), 0x21); + assert_eq!(table.table.word2.erase_type_3_instr(), 0xdc); + } +} diff --git a/util/types/BUILD.bazel b/util/types/BUILD.bazel index 407a06af..ef0b9045 100644 --- a/util/types/BUILD.bazel +++ b/util/types/BUILD.bazel @@ -9,11 +9,13 @@ rust_library( "lib.rs", "opcode.rs", "power_of_2.rs", + "time.rs", ], crate_name = "util_types", edition = "2024", visibility = ["//visibility:public"], deps = [ + "@pigweed//pw_time/rust:pw_time", "@rust_crates//:zerocopy", ], ) diff --git a/util/types/lib.rs b/util/types/lib.rs index 1af4db9f..c6c8dedb 100644 --- a/util/types/lib.rs +++ b/util/types/lib.rs @@ -7,9 +7,12 @@ mod opcode; mod power_of_2; +mod time; pub use opcode::Opcode; pub use power_of_2::PowerOf2Usize; +pub use time::MultiplyDuration; +pub use time::Nanoseconds; /// A trait for blocking on notifications. /// diff --git a/util/types/time.rs b/util/types/time.rs new file mode 100644 index 00000000..ca6814e8 --- /dev/null +++ b/util/types/time.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug)] +pub struct NanoClock; + +impl pw_time::Clock for NanoClock { + const TICKS_PER_SEC: u64 = 1_000_000_000; + + // NanoClock is only used as a compile-time tick configuration for Duration conversion. + // now() is implemented with a dummy value only to satisfy the Clock trait requirement. + fn now() -> pw_time::Instant { + pw_time::Instant::from_ticks(0) + } +} + +pub type Nanoseconds = pw_time::Duration; + +/// A trait for multiplying duration types by an integer factor. +pub trait MultiplyDuration { + /// Multiplies the duration by the specified integer factor. + fn mul(self, factor: i64) -> Self; +} + +impl MultiplyDuration for Nanoseconds { + fn mul(self, factor: i64) -> Self { + Self::from_nanos(self.ticks() * factor) + } +}