blob: 3815cfef014f0cc5ac6a3c58aebff55afb1d7859 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! SPI NOR facade with shared logic for FMC and SPI1/SPI2 backends.
use core::ops::FnMut;
use core::result::Result;
use core::result::Result::{Err, Ok};
use crate::smc::fmc::FmcReady;
use crate::smc::spi::SpiReady;
use crate::smc::types::{AddressWidth, ChipSelect, FlashConfig, SmcError, TransferMode};
/// Build a command byte array: opcode followed by address bytes selected by
/// `width`. Returns a fixed-size buffer and the valid length.
///
/// The caller slices `&buf[..len]` to get the exact command bytes.
/// This mirrors aspeed-rust's explicit `AddressWidth` selection rather than
/// relying on implicit `to_be_bytes()[1..]` slicing.
fn encode_addr_cmd(opcode: u8, offset: u32, width: AddressWidth) -> ([u8; 5], usize) {
let be = offset.to_be_bytes();
match width {
AddressWidth::None => {
let mut buf = [0u8; 5];
buf[0] = opcode;
(buf, 1)
}
AddressWidth::ThreeByte => {
let mut buf = [0u8; 5];
buf[0] = opcode;
buf[1] = be[1];
buf[2] = be[2];
buf[3] = be[3];
(buf, 4)
}
AddressWidth::FourByte => {
let mut buf = [0u8; 5];
buf[0] = opcode;
buf[1] = be[0];
buf[2] = be[1];
buf[3] = be[2];
buf[4] = be[3];
(buf, 5)
}
}
}
/// Minimal SPI NOR flash device API.
pub trait SpiNorFlashDevice {
/// Read bytes from flash at `offset` into `buf`.
fn read(&self, offset: u32, buf: &mut [u8]) -> Result<usize, SmcError>;
/// Return configured flash capacity in bytes.
fn capacity_bytes(&self) -> Result<usize, SmcError>;
/// Erase one sector at `offset`.
fn erase_sector(&mut self, offset: u32) -> Result<(), SmcError>;
/// Program one page at `offset`.
fn program_page(&mut self, offset: u32, data: &[u8]) -> Result<usize, SmcError>;
/// Verify flash content against `expected` bytes.
fn verify(&self, offset: u32, expected: &[u8]) -> Result<bool, SmcError>;
/// Read status register.
fn status(&self) -> Result<u8, SmcError>;
/// Read JEDEC ID bytes (manufacturer + 2-byte device ID).
fn jedec_id(&self) -> Result<[u8; 3], SmcError>;
}
/// Standard SPI NOR opcodes used by Phase 3B operations.
pub mod commands {
pub const WRITE_ENABLE: u8 = 0x06;
pub const ERASE_SECTOR_4K: u8 = 0x20;
pub const ERASE_SECTOR_4K_4B: u8 = 0x21;
pub const PAGE_PROGRAM: u8 = 0x02;
pub const PAGE_PROGRAM_4B: u8 = 0x12;
pub const READ_FAST: u8 = 0x0B;
pub const READ_FAST_4B: u8 = 0x0C;
pub const READ_STATUS: u8 = 0x05;
pub const READ_ID: u8 = 0x9F;
}
const STATUS_WIP: u8 = 0x01;
const WRITE_COMPLETE_MAX_POLLS: u32 = 1_000_000;
const STATUS_POLL_SPIN_DELAY: u32 = 64;
/// Addressing policy for SPI NOR command transactions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FlashAddressingPolicy {
/// Use 3-byte addressing command set.
ThreeByteOnly,
/// Use 4-byte addressing command set.
FourByteCommands,
}
impl FlashAddressingPolicy {
pub const fn addr_width(self) -> AddressWidth {
match self {
Self::ThreeByteOnly => AddressWidth::ThreeByte,
Self::FourByteCommands => AddressWidth::FourByte,
}
}
}
/// SPI NOR opcode profile selected by addressing policy.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlashCommandProfile {
pub read_fast: u8,
pub page_program: u8,
pub erase_sector_4k: u8,
pub read_status: u8,
pub write_enable: u8,
}
impl FlashCommandProfile {
const fn for_addressing(policy: FlashAddressingPolicy) -> Self {
match policy {
FlashAddressingPolicy::ThreeByteOnly => Self {
read_fast: commands::READ_FAST,
page_program: commands::PAGE_PROGRAM,
erase_sector_4k: commands::ERASE_SECTOR_4K,
read_status: commands::READ_STATUS,
write_enable: commands::WRITE_ENABLE,
},
FlashAddressingPolicy::FourByteCommands => Self {
read_fast: commands::READ_FAST_4B,
page_program: commands::PAGE_PROGRAM_4B,
erase_sector_4k: commands::ERASE_SECTOR_4K_4B,
read_status: commands::READ_STATUS,
write_enable: commands::WRITE_ENABLE,
},
}
}
}
/// Compare `expected` against bytes produced by `read`, in chunks of at most
/// `chunk` bytes. `read(offset, dst)` fills `dst` with bytes at the given
/// device-local offset. Returns `Ok(true)` on full equality, `Ok(false)` on the
/// first mismatch, and propagates any error from `read`. Bounded scratch use:
/// the comparison is performed in stack-resident chunks (`chunk` bytes, capped
/// at 256) regardless of `expected.len()`.
fn compare_chunked<F>(
mut read: F,
offset: u32,
expected: &[u8],
chunk: usize,
) -> Result<bool, SmcError>
where
F: FnMut(u32, &mut [u8]) -> Result<usize, SmcError>,
{
let mut scratch = [0u8; 256];
let step = core::cmp::min(chunk, scratch.len());
if step == 0 {
return Err(SmcError::InvalidCapacity);
}
let mut cursor = 0usize;
while cursor < expected.len() {
let n = core::cmp::min(step, expected.len() - cursor);
let chunk_offset = offset
.checked_add(cursor as u32)
.ok_or(SmcError::InvalidCapacity)?;
read(chunk_offset, &mut scratch[..n])?;
if scratch[..n] != expected[cursor..cursor + n] {
return Ok(false);
}
cursor += n;
}
Ok(true)
}
fn poll_delay() {
for _ in 0..STATUS_POLL_SPIN_DELAY {
core::hint::spin_loop();
}
}
enum FlashBackend<'a> {
Fmc(&'a FmcReady),
Spi(&'a SpiReady),
}
/// Decoded JEDEC identifier returned by `READ_ID` (`0x9F`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct JedecId {
pub manufacturer: u8,
pub memory_type: u8,
pub capacity_code: u8,
}
impl JedecId {
#[must_use]
pub const fn from_bytes(raw: [u8; 3]) -> Self {
Self {
manufacturer: raw[0],
memory_type: raw[1],
capacity_code: raw[2],
}
}
#[must_use]
pub const fn as_bytes(self) -> [u8; 3] {
[self.manufacturer, self.memory_type, self.capacity_code]
}
}
fn expect_jedec_match(actual: JedecId, expected: JedecId) -> Result<JedecId, SmcError> {
if actual != expected {
return Err(SmcError::HardwareError);
}
Ok(actual)
}
/// Wrapper-aware SPI NOR flash facade.
pub struct SpiNorFlash<'a> {
backend: FlashBackend<'a>,
// Validated metadata for Phase 3B alignment/policy checks.
cfg: FlashConfig,
/// Chip select this flash device sits on.
cs: ChipSelect,
/// IO mode used for all SPI command transactions (WREN, RDSR, PP, SE).
/// Defaults to `Mode111` (single-wire cmd/addr/data).
cmd_mode: TransferMode,
/// Addressing policy selected for SPI NOR command construction.
addressing_policy: FlashAddressingPolicy,
/// Opcodes selected for the current addressing policy.
command_profile: FlashCommandProfile,
}
impl<'a> SpiNorFlash<'a> {
/// Build a flash facade from an initialized FMC controller wrapper.
pub fn from_fmc(fmc: &'a mut FmcReady, cfg: FlashConfig) -> Result<Self, SmcError> {
Self::from_fmc_cs(fmc, cfg, ChipSelect::Cs0)
}
/// Build a flash facade from an initialized FMC controller wrapper with explicit CS.
pub fn from_fmc_cs(
fmc: &'a mut FmcReady,
cfg: FlashConfig,
cs: ChipSelect,
) -> Result<Self, SmcError> {
let addressing_policy = Self::default_addressing_for_cfg(cfg);
Self::validate_capacity_cfg(cfg, fmc.cs_config(cs)?)?;
Ok(Self {
backend: FlashBackend::Fmc(fmc),
cfg,
cs,
cmd_mode: TransferMode::Mode111,
addressing_policy,
command_profile: FlashCommandProfile::for_addressing(addressing_policy),
})
}
/// Build a flash facade from an initialized SPI1/SPI2 controller wrapper.
pub fn from_spi(spi: &'a mut SpiReady, cfg: FlashConfig) -> Result<Self, SmcError> {
Self::from_spi_cs(spi, cfg, ChipSelect::Cs0)
}
/// Build a flash facade from an initialized SPI1/SPI2 controller wrapper with explicit CS.
pub fn from_spi_cs(
spi: &'a mut SpiReady,
cfg: FlashConfig,
cs: ChipSelect,
) -> Result<Self, SmcError> {
let addressing_policy = Self::default_addressing_for_cfg(cfg);
Self::validate_capacity_cfg(cfg, spi.cs_config(cs)?)?;
Ok(Self {
backend: FlashBackend::Spi(spi),
cfg,
cs,
cmd_mode: TransferMode::Mode111,
addressing_policy,
command_profile: FlashCommandProfile::for_addressing(addressing_policy),
})
}
/// Override the IO mode used for all SPI command transactions.
///
/// Applies to WREN, RDSR, PAGE_PROGRAM, and SECTOR_ERASE paths.
/// The memory-mapped read path always uses the segment-routed AHB window
/// and is unaffected by this setting.
pub fn with_cmd_mode(mut self, mode: TransferMode) -> Self {
self.cmd_mode = mode;
self
}
/// Override the addressing policy used for command-profile selection.
pub fn with_addressing_policy(mut self, policy: FlashAddressingPolicy) -> Self {
self.addressing_policy = policy;
self.command_profile = FlashCommandProfile::for_addressing(policy);
self
}
/// Read and decode the three-byte JEDEC ID tuple.
pub fn jedec(&self) -> Result<JedecId, SmcError> {
Ok(JedecId::from_bytes(self.read_jedec_id_impl()?))
}
/// Read JEDEC ID and require an exact match.
pub fn expect_jedec(&self, expected: JedecId) -> Result<JedecId, SmcError> {
expect_jedec_match(self.jedec()?, expected)
}
/// Program an arbitrary-length buffer by issuing page-sized writes.
///
/// The starting `offset` must be page-aligned. The final chunk may be
/// shorter than a full page.
pub fn program(&mut self, offset: u32, data: &[u8]) -> Result<usize, SmcError> {
if data.is_empty() {
return Ok(0);
}
let page_size = self.cfg.page_size as usize;
if page_size == 0 || (offset as usize) % page_size != 0 {
return Err(SmcError::InvalidCapacity);
}
self.validate_range(offset, data.len())?;
let mut written = 0usize;
while written < data.len() {
let chunk_len = core::cmp::min(page_size, data.len() - written);
let chunk_offset = offset
.checked_add(written as u32)
.ok_or(SmcError::InvalidCapacity)?;
self.program_page(chunk_offset, &data[written..written + chunk_len])?;
written += chunk_len;
}
Ok(written)
}
/// Erase all sectors intersecting the requested byte range.
///
/// The erased interval is rounded outward to sector boundaries.
pub fn erase_range(&mut self, offset: u32, len: usize) -> Result<(), SmcError> {
if len == 0 {
return Ok(());
}
self.validate_range(offset, len)?;
let sector_size = self.cfg.sector_size as usize;
if sector_size == 0 {
return Err(SmcError::InvalidCapacity);
}
let start = (offset as usize / sector_size) * sector_size;
let end = (offset as usize)
.checked_add(len)
.ok_or(SmcError::InvalidCapacity)?;
let end_aligned = end
.checked_add(sector_size - 1)
.ok_or(SmcError::InvalidCapacity)?
/ sector_size
* sector_size;
let mut current = start;
while current < end_aligned {
self.erase_sector(current as u32)?;
current += sector_size;
}
Ok(())
}
/// Perform a correctness-first flash update of the requested region.
///
/// This helper erases all sectors intersecting the target range, programs
/// the supplied bytes page-by-page, then verifies the written contents.
/// Returns the number of bytes programmed on success.
pub fn update_region(&mut self, offset: u32, data: &[u8]) -> Result<usize, SmcError> {
if data.is_empty() {
return Ok(0);
}
self.erase_range(offset, data.len())?;
let written = self.program(offset, data)?;
if !self.verify(offset, data)? {
return Err(SmcError::HardwareError);
}
Ok(written)
}
fn validate_capacity_cfg(cfg: FlashConfig, expected: FlashConfig) -> Result<(), SmcError> {
if cfg != expected {
return Err(SmcError::InvalidCapacity);
}
Ok(())
}
fn default_addressing_for_cfg(cfg: FlashConfig) -> FlashAddressingPolicy {
if cfg.capacity_mb > 16 {
FlashAddressingPolicy::FourByteCommands
} else {
FlashAddressingPolicy::ThreeByteOnly
}
}
pub fn addr_width(&self) -> AddressWidth {
self.addressing_policy.addr_width()
}
pub fn command_profile(&self) -> FlashCommandProfile {
self.command_profile
}
/// Validate a device-local offset before handing it to the controller.
///
/// `FmcReady::read` / `SpiReady::read` already select the per-CS AHB
/// window from the chip-select argument, so offsets stay CS-local here.
fn device_to_controller_offset(&self, device_offset: u32) -> Result<u32, SmcError> {
let cs_cap = self.capacity_bytes()?;
if (device_offset as usize) >= cs_cap {
return Err(SmcError::InvalidCapacity);
}
Ok(device_offset)
}
fn validate_range(&self, offset: u32, len: usize) -> Result<(), SmcError> {
let start = offset as usize;
let end = start.checked_add(len).ok_or(SmcError::InvalidCapacity)?;
if end > self.capacity_bytes()? {
return Err(SmcError::InvalidCapacity);
}
Ok(())
}
fn validate_sector_erase(&self, offset: u32) -> Result<(), SmcError> {
let sector_size = self.cfg.sector_size as usize;
if sector_size == 0 || (offset as usize) % sector_size != 0 {
return Err(SmcError::InvalidCapacity);
}
self.validate_range(offset, sector_size)
}
fn validate_page_program(&self, offset: u32, data: &[u8]) -> Result<(), SmcError> {
let page_size = self.cfg.page_size as usize;
if page_size == 0 || data.is_empty() || data.len() > page_size {
return Err(SmcError::InvalidCapacity);
}
if (offset as usize) % page_size != 0 {
return Err(SmcError::InvalidCapacity);
}
self.validate_range(offset, data.len())
}
fn issue_command(&mut self, cmd: &[u8], payload: &[u8]) -> Result<(), SmcError> {
let cs = self.cs;
let mode = self.cmd_mode;
match &self.backend {
FlashBackend::Fmc(fmc) => fmc.transceive_user(cs, cmd, payload, &mut [], mode),
FlashBackend::Spi(spi) => spi.transceive_user(cs, cmd, payload, &mut [], mode),
}
}
fn read_status_impl(&self) -> Result<u8, SmcError> {
let cs = self.cs;
let mode = self.cmd_mode;
let opcode = self.command_profile().read_status;
let mut status = [0u8; 1];
match &self.backend {
FlashBackend::Fmc(fmc) => fmc.transceive_user(cs, &[opcode], &[], &mut status, mode)?,
FlashBackend::Spi(spi) => spi.transceive_user(cs, &[opcode], &[], &mut status, mode)?,
}
Ok(status[0])
}
fn read_jedec_id_impl(&self) -> Result<[u8; 3], SmcError> {
let cs = self.cs;
let mode = self.cmd_mode;
let mut id = [0u8; 3];
match &self.backend {
FlashBackend::Fmc(fmc) => {
fmc.transceive_user(cs, &[commands::READ_ID], &[], &mut id, mode)?
}
FlashBackend::Spi(spi) => {
spi.transceive_user(cs, &[commands::READ_ID], &[], &mut id, mode)?
}
}
Ok(id)
}
fn write_enable(&mut self) -> Result<(), SmcError> {
let profile = self.command_profile();
self.issue_command(&[profile.write_enable], &[])
}
fn wait_write_complete(&self, max_polls: u32) -> Result<(), SmcError> {
let mut polls = 0u32;
while polls < max_polls {
let sr = self.read_status_impl()?;
if (sr & STATUS_WIP) == 0 {
return Ok(());
}
poll_delay();
polls += 1;
}
Err(SmcError::Timeout)
}
}
impl SpiNorFlashDevice for SpiNorFlash<'_> {
fn read(&self, offset: u32, buf: &mut [u8]) -> Result<usize, SmcError> {
// Bounds-check against the selected CS's capacity, then translate to
// the controller-window address before issuing the segment-routed read.
self.validate_range(offset, buf.len())?;
let translated = self.device_to_controller_offset(offset)?;
let cs = self.cs;
//TODO: need to add dma_read variant if buf.len() is above some threshold
// and dma is enabled
match &self.backend {
FlashBackend::Fmc(fmc) => fmc.read(cs, translated, buf),
FlashBackend::Spi(spi) => spi.read(cs, translated, buf),
}
}
fn capacity_bytes(&self) -> Result<usize, SmcError> {
match &self.backend {
FlashBackend::Fmc(fmc) => fmc.cs_capacity_bytes(self.cs),
FlashBackend::Spi(spi) => spi.cs_capacity_bytes(self.cs),
}
}
fn erase_sector(&mut self, offset: u32) -> Result<(), SmcError> {
self.validate_sector_erase(offset)?;
let profile = self.command_profile();
let width = self.addr_width();
self.write_enable()?;
let (cmd, len) = encode_addr_cmd(profile.erase_sector_4k, offset, width);
self.issue_command(&cmd[..len], &[])?;
self.wait_write_complete(WRITE_COMPLETE_MAX_POLLS)
}
fn program_page(&mut self, offset: u32, data: &[u8]) -> Result<usize, SmcError> {
self.validate_page_program(offset, data)?;
let profile = self.command_profile();
let width = self.addr_width();
self.write_enable()?;
let (cmd, len) = encode_addr_cmd(profile.page_program, offset, width);
self.issue_command(&cmd[..len], data)?;
self.wait_write_complete(WRITE_COMPLETE_MAX_POLLS)?;
Ok(data.len())
}
fn verify(&self, offset: u32, expected: &[u8]) -> Result<bool, SmcError> {
self.validate_range(offset, expected.len())?;
compare_chunked(|o, b| self.read(o, b), offset, expected, 256)
}
fn status(&self) -> Result<u8, SmcError> {
self.read_status_impl()
}
fn jedec_id(&self) -> Result<[u8; 3], SmcError> {
self.read_jedec_id_impl()
}
}
#[cfg(test)]
mod tests {
use super::{
commands, compare_chunked, encode_addr_cmd, expect_jedec_match, FlashAddressingPolicy,
FlashCommandProfile, JedecId, SpiNorFlash,
};
use crate::smc::types::{AddressWidth, FlashConfig, SmcError};
#[test]
fn encode_addr_cmd_none_emits_opcode_only() {
let (buf, len) = encode_addr_cmd(0x06, 0x1234_5678, AddressWidth::None);
assert_eq!(len, 1);
assert_eq!(&buf[..len], &[0x06]);
}
#[test]
fn encode_addr_cmd_three_byte_emits_low_24_bits() {
let (buf, len) = encode_addr_cmd(0x20, 0x1234_5678, AddressWidth::ThreeByte);
assert_eq!(len, 4);
assert_eq!(&buf[..len], &[0x20, 0x34, 0x56, 0x78]);
}
#[test]
fn encode_addr_cmd_four_byte_emits_full_32_bits() {
let (buf, len) = encode_addr_cmd(0x13, 0x1234_5678, AddressWidth::FourByte);
assert_eq!(len, 5);
assert_eq!(&buf[..len], &[0x13, 0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn command_profile_three_byte_uses_legacy_opcodes() {
let profile = FlashCommandProfile::for_addressing(FlashAddressingPolicy::ThreeByteOnly);
assert_eq!(profile.read_fast, commands::READ_FAST);
assert_eq!(profile.page_program, commands::PAGE_PROGRAM);
assert_eq!(profile.erase_sector_4k, commands::ERASE_SECTOR_4K);
assert_eq!(profile.read_status, commands::READ_STATUS);
assert_eq!(profile.write_enable, commands::WRITE_ENABLE);
}
#[test]
fn command_profile_four_byte_uses_4b_opcodes() {
let profile = FlashCommandProfile::for_addressing(FlashAddressingPolicy::FourByteCommands);
assert_eq!(profile.read_fast, commands::READ_FAST_4B);
assert_eq!(profile.page_program, commands::PAGE_PROGRAM_4B);
assert_eq!(profile.erase_sector_4k, commands::ERASE_SECTOR_4K_4B);
assert_eq!(profile.read_status, commands::READ_STATUS);
assert_eq!(profile.write_enable, commands::WRITE_ENABLE);
}
#[test]
fn default_addressing_policy_derives_from_capacity() {
assert_eq!(
SpiNorFlash::default_addressing_for_cfg(FlashConfig {
capacity_mb: 8,
page_size: 256,
sector_size: 4096,
block_size: 65536,
spi_clock_mhz: 25,
}),
FlashAddressingPolicy::ThreeByteOnly
);
assert_eq!(
SpiNorFlash::default_addressing_for_cfg(FlashConfig {
capacity_mb: 32,
page_size: 256,
sector_size: 4096,
block_size: 65536,
spi_clock_mhz: 25,
}),
FlashAddressingPolicy::FourByteCommands
);
}
fn fixture_1kb() -> [u8; 1024] {
let mut out = [0u8; 1024];
let mut i = 0;
while i < out.len() {
out[i] = (i as u8).wrapping_mul(31).wrapping_add(7);
i += 1;
}
out
}
#[test]
fn compare_chunked_matches_buffer_above_scratch_size() {
let expected = fixture_1kb();
let backing = expected;
let read = |offset: u32, dst: &mut [u8]| -> Result<usize, SmcError> {
let off = offset as usize;
dst.copy_from_slice(&backing[off..off + dst.len()]);
Ok(dst.len())
};
assert_eq!(compare_chunked(read, 0, &expected, 256), Ok(true));
}
#[test]
fn compare_chunked_detects_mismatch_in_later_chunk() {
let expected = fixture_1kb();
let mut backing = expected;
backing[800] = backing[800].wrapping_add(1);
let read = |offset: u32, dst: &mut [u8]| -> Result<usize, SmcError> {
let off = offset as usize;
dst.copy_from_slice(&backing[off..off + dst.len()]);
Ok(dst.len())
};
assert_eq!(compare_chunked(read, 0, &expected, 256), Ok(false));
}
#[test]
fn compare_chunked_propagates_read_error() {
let expected = fixture_1kb();
let read =
|_offset: u32, _dst: &mut [u8]| -> Result<usize, SmcError> { Err(SmcError::Timeout) };
assert_eq!(
compare_chunked(read, 0, &expected, 256),
Err(SmcError::Timeout)
);
}
#[test]
fn expect_jedec_match_returns_actual_on_exact_match() {
let jedec = JedecId::from_bytes([0xEF, 0x40, 0x18]);
assert_eq!(expect_jedec_match(jedec, jedec), Ok(jedec));
}
#[test]
fn expect_jedec_match_rejects_mismatch() {
let expected = JedecId::from_bytes([0xEF, 0x40, 0x18]);
let actual = JedecId::from_bytes([0xC2, 0x20, 0x19]);
assert_eq!(
expect_jedec_match(actual, expected),
Err(SmcError::HardwareError)
);
}
}