From ce7ce7b5a9e83657e55e9bc1cc41c2c20ba17639 Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Wed, 24 Jun 2026 15:15:13 +0800 Subject: [PATCH 1/4] util/sfdp: Port SFDP parser from legacy implementation Port the Serial Flash Discoverable Parameters (SFDP) parser library from the legacy implementation. Key modifications and fixes made during the port: - Time representation: Replaced the custom `Nanoseconds` type with new custom type based on `core::time::Duration` across all timing tables. - Legacy Bug Fixes: Corrected incorrect bitmasks in `ChipEraseTimeUnit::from_bits` and `DurationEnumAUnit::from_bits` from `& 0b1` to `& 0b11`, enabling the parser to correctly resolve all 4 states instead of triggering panics. - Default Value in Enums: Eliminated all `unreachable!()` wildcard arms in enum deserializers, replacing them with catch-all wildcards returning default or safe fallback states. - Underflow & Overflow Protection: - Added capacity bounds checks in `MemoryDensity::byte_len` to prevent subtraction underflow when bit length is less than 3. - Added shift boundary checks in `PowerOf2::value` to prevent undefined behavior/panics when exponent exceeds platform `usize` bit width. - Code Cleanups: Refactored `U24` to use `to_le_bytes`/`from_le_bytes` instead of raw bitwise shifts, and streamlined `MemoryDensity::from_byte_len` with pattern matching. Also includes original unit tests running against mock SFDP tables. Signed-off-by: Chia-Wei Liu --- util/sfdp/BUILD.bazel | 26 + util/sfdp/mod.rs | 2295 ++++++++++++++++++++++++++++++++++++++++ util/types/BUILD.bazel | 2 + util/types/lib.rs | 3 + util/types/time.rs | 29 + 5 files changed, 2355 insertions(+) create mode 100644 util/sfdp/BUILD.bazel create mode 100644 util/sfdp/mod.rs create mode 100644 util/types/time.rs 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) + } +} From 856bca8dc1a86b4417e17b7b0d5f7ae662e91bc7 Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Fri, 26 Jun 2026 07:25:52 +0800 Subject: [PATCH 2/4] earlgrey: drivers: Port SPI Host driver and add smoke test Key modifications and fixes made during the port: - Implements the `embedded-hal` `SpiDevice` trait, which includes: * Defines a related `SpiError` type. * Defines the `transaction` method in the struct, which accepts an array of operations, and `csaat` will be asserted during the entire operation series. * Marks the operations like `Transfer`, `TransferInPlace`, and `DelayNs` as unsupported explicitly, which is also originally unsupported. * Removes unsupported hardware optimization methods like `prefetch`, `drain_fifo`, and `aligned_drain_fifo` since they are not defined in `embedded_hal`. If we need the original optimization that only works on aligned words, we can branch on aligned/unaligned input and apply similar optimization when the input is aligned. However, we should stick with the input type defined in `embedded_hal`. - Defines a dedicated `SpiConfig` struct and a dedicated `configure` method to make SPI Host configurable. Also includes a smoke test validating SPI read transactions by checking the SFDP signature. Signed-off-by: Chia-Wei Liu --- target/earlgrey/drivers/BUILD.bazel | 14 + target/earlgrey/drivers/spi_host.rs | 259 ++++++++++++++++++ .../tests/drivers/spi_host/BUILD.bazel | 103 +++++++ .../tests/drivers/spi_host/spi_host.rs | 143 ++++++++++ .../tests/drivers/spi_host/system.json5 | 47 ++++ .../earlgrey/tests/drivers/spi_host/target.rs | 30 ++ 6 files changed, 596 insertions(+) create mode 100644 target/earlgrey/drivers/spi_host.rs create mode 100644 target/earlgrey/tests/drivers/spi_host/BUILD.bazel create mode 100644 target/earlgrey/tests/drivers/spi_host/spi_host.rs create mode 100644 target/earlgrey/tests/drivers/spi_host/system.json5 create mode 100644 target/earlgrey/tests/drivers/spi_host/target.rs 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_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); From 82c5b57923b3e5317f34da5a8dfd07356301a6be Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 30 Jun 2026 04:34:51 +0800 Subject: [PATCH 3/4] drivers: flash: Port generic SPI Flash driver and add smoke test Key modifications and fixes made during the port: - Makes the driver explicitly enter "Global 4-byte" mode when flash size >= 16 MB, which includes: * Update the command config in `from_sfdp()` & `from_sfdp_conservative()`. * Explicitly enters the global 4-byte mode at the end of initialization. - Skips the support of DUAL / QUAD in `SpiTxnWidth`. Also, inits the driver with non-conservative version for now. - Moves `transfer_req_resp` from legacy SPI Host driver to SPI Flash driver, as a helper method. - Moves `check_valid_size` from legacy Flash trait to SPI Flash driver, as a helper method. - Adds `is_busy` helper method as what we did for `BlockingFlash` trait, although we didn't implement this trait directly now. - Wraps legacy `page_size` and `size` into a `geometry` method required by `Flash` trait. Currently we hard-coded the erasable bitmap as 4KB and 64KB, and we may parse them in the future. - For `erase()` method, the `Flash` trait only accepts the size declared in the erasable bitmap, but here we uses the legacy implementation which will loop over the size to erase. However, we still only accepts power of 2 as the erase size due to `Flash` trait's contract. Also removes the original `erase_page()` method. - Drops `FlashStreamingRead` support (as we mentioned in SPI Host driver commit). The migration and refactoring for the unit tests of SPI driver is still WIP. Signed-off-by: Chia-Wei Liu --- drivers/flash/BUILD.bazel | 30 + drivers/flash/spi_flash.rs | 1982 +++++++++++++++++ .../tests/drivers/spi_flash/BUILD.bazel | 107 + .../tests/drivers/spi_flash/spi_flash.rs | 114 + .../tests/drivers/spi_flash/system.json5 | 41 + .../tests/drivers/spi_flash/target.rs | 29 + util/error/flash.rs | 3 + 7 files changed, 2306 insertions(+) create mode 100644 drivers/flash/BUILD.bazel create mode 100644 drivers/flash/spi_flash.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/drivers/spi_flash/spi_flash.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/drivers/spi_flash/target.rs diff --git a/drivers/flash/BUILD.bazel b/drivers/flash/BUILD.bazel new file mode 100644 index 00000000..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/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/util/error/flash.rs b/util/error/flash.rs index 976bfb4a..c16af41a 100644 --- a/util/error/flash.rs +++ b/util/error/flash.rs @@ -36,6 +36,9 @@ pub const FLASH_GENERIC_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(8, Error /// The erase size is invalid. pub const FLASH_GENERIC_ERASE_INVALID_SIZE: ErrorCode = FLASH_GENERIC.from_pw(9, Error::InvalidArgument); +/// The flash device is not initialized. +pub const FLASH_GENERIC_NOT_INITIALIZED: ErrorCode = + FLASH_GENERIC.from_pw(10, Error::FailedPrecondition); /// SFDP: Invalid memory density. pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode = From 804cfb5befd8136da83761792ff25a9f757faf1f Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 14 Jul 2026 08:09:48 +0800 Subject: [PATCH 4/4] earlgrey: tests: Add SPI Flash IPC integration test --- target/earlgrey/tests/spi_flash/BUILD.bazel | 148 ++++++++++++++++++ .../tests/spi_flash/combined_flash_server.rs | 142 +++++++++++++++++ .../tests/spi_flash/spi_flash_test.rs | 110 +++++++++++++ target/earlgrey/tests/spi_flash/system.json5 | 98 ++++++++++++ target/earlgrey/tests/spi_flash/target.rs | 29 ++++ 5 files changed, 527 insertions(+) create mode 100644 target/earlgrey/tests/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/spi_flash/combined_flash_server.rs create mode 100644 target/earlgrey/tests/spi_flash/spi_flash_test.rs create mode 100644 target/earlgrey/tests/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/spi_flash/target.rs diff --git a/target/earlgrey/tests/spi_flash/BUILD.bazel b/target/earlgrey/tests/spi_flash/BUILD.bazel new file mode 100644 index 00000000..114bc229 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/BUILD.bazel @@ -0,0 +1,148 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "combined_flash_server", + srcs = [ + "combined_flash_server.rs", + ], + codegen_crate_name = "combined_flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/registers:spi_host", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +rust_app( + name = "spi_flash_test", + srcs = [ + "spi_flash_test.rs", + ], + codegen_crate_name = "spi_flash_test_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:client", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/misc", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "spi_flash", + apps = [ + ":combined_flash_server", + ":spi_flash_test", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "spi_flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash", +) + +rust_binary_no_panics_test( + name = "no_panics", + apps = [ + "combined_flash_server", + "spi_flash_test", + ], + binary = ":spi_flash", +) diff --git a/target/earlgrey/tests/spi_flash/combined_flash_server.rs b/target/earlgrey/tests/spi_flash/combined_flash_server.rs new file mode 100644 index 00000000..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);