From 9682271a92abe514f4bab7ee9ab461ab65c95cea Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 30 Jun 2026 04:34:51 +0800 Subject: [PATCH 01/13] drivers/flash: Port generic SPI Flash driver and add smoke test Key modifications and fixes made during the port: - Makes the driver explicitly enter "Global 4-byte" mode when flash size >= 16 MB, which includes: * Update the command config in `from_sfdp()` & `from_sfdp_conservative()`. * Explicitly enters the global 4-byte mode at the end of initialization. - Skips the support of DUAL / QUAD in `SpiTxnWidth`. Also, inits the driver with non-conservative version for now. - Moves `transfer_req_resp` from legacy SPI Host driver to SPI Flash driver, as a helper method. - Moves `check_valid_size` from legacy Flash trait to SPI Flash driver, as a helper method. - Adds `is_busy` helper method as what we did for `BlockingFlash` trait, although we didn't implement this trait directly now. - Wraps legacy `page_size` and `size` into a `geometry` method required by `Flash` trait. Currently we hard-coded the erasable bitmap as 4KB and 64KB, and we may parse them in the future. - For `erase()` method, the `Flash` trait only accepts the size declared in the erasable bitmap, but here we uses the legacy implementation which will loop over the size to erase. However, we still only accepts power of 2 as the erase size due to `Flash` trait's contract. Also removes the original `erase_page()` method. - Drops `FlashStreamingRead` support (as we mentioned in SPI Host driver commit). Mock SPI Device & Unit Test Refactoring: - Ports the legacy `FakeSpiHost` to OpenPRoT, renaming it to `FakeSpiDevice` under `drivers/mock/spi_device_fake.rs` and implementing standard `embedded_hal::spi::SpiDevice` and `ErrorType` traits. - Rewrites mock transmission logic to be stateless by removing `TxnState`, supporting multi-buffer transaction execution and automatic TX/RX clock log alignment and padding. - Introduces `assert_all_expectations_met()` to strictly assert that all preprogrammed SPI responses are executed, preventing silent test leaks. - Adapts unit tests to new `PowerOf2Usize` API constraints: * Erase lengths in mixed granularity and multiple page tests are changed to powers of 2 (e.g., 144KB -> 128KB, 12KB -> 16KB). * Obsolete `test_erase_zero_length` is removed as zero-length input is prevented at compile time. - Aligns mock expectations with Global 4-byte address mode: expected opcodes are updated from 4-byte-specific ones (e.g. `OP_READ4B`) to standard opcodes (e.g. `OP_READ`) with 4-byte address streams. - Removes obsolete unit tests for prefetch, FIFO draining, and Streaming Read. Signed-off-by: Chia-Wei Liu --- drivers/flash/BUILD.bazel | 33 + drivers/flash/spi_flash.rs | 2091 +++++++++++++++++ drivers/mock/BUILD.bazel | 21 + drivers/mock/spi_device_fake.rs | 320 +++ .../tests/drivers/spi_flash/BUILD.bazel | 107 + .../tests/drivers/spi_flash/spi_flash.rs | 118 + .../tests/drivers/spi_flash/system.json5 | 41 + .../tests/drivers/spi_flash/target.rs | 29 + util/error/flash.rs | 3 + 9 files changed, 2763 insertions(+) create mode 100644 drivers/flash/BUILD.bazel create mode 100644 drivers/flash/spi_flash.rs create mode 100644 drivers/mock/BUILD.bazel create mode 100644 drivers/mock/spi_device_fake.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/drivers/spi_flash/spi_flash.rs create mode 100644 target/earlgrey/tests/drivers/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/drivers/spi_flash/target.rs diff --git a/drivers/flash/BUILD.bazel b/drivers/flash/BUILD.bazel new file mode 100644 index 00000000..88445569 --- /dev/null +++ b/drivers/flash/BUILD.bazel @@ -0,0 +1,33 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "spi_flash", + srcs = ["spi_flash.rs"], + crate_name = "spi_flash", + edition = "2024", + proc_macro_deps = [ + "@rust_crates//:bitfield-struct", + ], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//util/error", + "//util/io", + "//util/sfdp", + "//util/types", + "@rust_crates//:embedded-hal", + "@rust_crates//:zerocopy", + ], +) + +rust_test( + name = "spi_flash_test", + crate = ":spi_flash", + deps = [ + "//drivers/mock:spi_device_fake", + ], +) diff --git a/drivers/flash/spi_flash.rs b/drivers/flash/spi_flash.rs new file mode 100644 index 00000000..d70c5647 --- /dev/null +++ b/drivers/flash/spi_flash.rs @@ -0,0 +1,2091 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use bitfield_struct::bitfield; +use core::cmp::min; +use core::convert::TryFrom; +use core::num::NonZero; +use core::prelude::v1::*; +use util_sfdp::{QuadEnableRequirements, SfdpReader}; +use util_types::PowerOf2Usize; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes}; + +use embedded_hal::spi::Operation; +use hal_flash::{Flash as FlashTrait, FlashAddress}; +use util_error::{self as error, ErrorCode}; +use util_io::RandomRead; + +// TODO(b/481400917): Replace with stronger "byte count" type. +const KIB: usize = 1024; +const MIB: usize = 1024 * KIB; + +// The maximum number of bytes the opcode + address + dummy bytes at the start +// of a transaction will need. +const MAX_PREFIX_LEN: usize = 6; +const MAX_3B_SIZE: usize = 16 * MIB; +const SECTOR_SIZE: usize = 4096; +const BLOCK_SIZE: usize = 64 * KIB; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiTxnWidth { + STANDARD = 0, + DUAL = 1, + QUAD = 2, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SfCmd { + pub opcode: u8, + pub width: SpiTxnWidth, + pub addr_mode: AddressingMode, +} + +impl SfCmd { + pub const READ: Self = Self { + opcode: OP_READ, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const PROGRAM: Self = Self { + opcode: OP_PROGRAM, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const ERASE: Self = Self { + opcode: OP_ERASE_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const READ4B: Self = Self { + opcode: OP_READ4B, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const PROGRAM4B: Self = Self { + opcode: OP_PROGRAM4B, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE4B: Self = Self { + opcode: OP_ERASE4B_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE64K: Self = Self { + opcode: OP_ERASE_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_3Byte, + }; + pub const ERASE4B_64K: Self = Self { + opcode: OP_ERASE4B_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const QREAD: Self = Self { + opcode: OP_QREAD, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_3ByteWithDummy, + }; + pub const QREAD4B: Self = Self { + opcode: OP_QREAD4B, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_4ByteWithDummy, + }; + pub const QPROGRAM: Self = Self { + opcode: OP_QPROGRAM, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_3Byte, + }; + pub const QPROGRAM4B: Self = Self { + opcode: OP_QPROGRAM4B, + width: SpiTxnWidth::QUAD, + addr_mode: AddressingMode::_4Byte, + }; + pub const READ4B_GLOBAL: Self = Self { + opcode: OP_READ, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const PROGRAM4B_GLOBAL: Self = Self { + opcode: OP_PROGRAM, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE4B_GLOBAL: Self = Self { + opcode: OP_ERASE_4K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; + pub const ERASE64K_GLOBAL: Self = Self { + opcode: OP_ERASE_64K, + width: SpiTxnWidth::STANDARD, + addr_mode: AddressingMode::_4Byte, + }; +} + +pub struct SpiFlashConfig { + pub read: SfCmd, + pub program: SfCmd, + pub erase4k: SfCmd, + pub erase64k: SfCmd, + + /// The total size of the flash in bytes + pub size: NonZero, + pub quad_enable_req: Option, +} + +impl SpiFlashConfig { + pub fn from_sfdp_conservative>( + sfdp_bytes: R, + ) -> Result { + let mut sfdp = SfdpReader::new(sfdp_bytes)?; + let table = sfdp.basic_flash_parameters()?; + + let size = usize::try_from(table.table_jesd216().memory_density.byte_len()?).unwrap(); + let size = NonZero::new(size).ok_or(error::FLASH_GENERIC_INVALID_SIZE)?; + let config = if size.get() <= MAX_3B_SIZE { + SpiFlashConfig { + size, + read: SfCmd::READ, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + program: SfCmd::PROGRAM, + quad_enable_req: None, + } + } else { + SpiFlashConfig { + size, + read: SfCmd::READ4B_GLOBAL, + erase4k: SfCmd::ERASE4B_GLOBAL, + erase64k: SfCmd::ERASE64K_GLOBAL, + program: SfCmd::PROGRAM4B_GLOBAL, + quad_enable_req: None, + } + }; + Ok(config) + } + + pub fn from_sfdp>(sfdp_bytes: R) -> Result { + let mut sfdp = SfdpReader::new(sfdp_bytes)?; + let table = sfdp.basic_flash_parameters()?; + + let size = usize::try_from(table.table_jesd216().memory_density.byte_len()?).unwrap(); + let size = NonZero::new(size).ok_or(error::FLASH_GENERIC_INVALID_SIZE)?; + + // TODO: Restore Quad SPI (QSPI) support once earlgrey_spi_host implements + // DUAL/QUAD transaction widths. + let config = if size.get() <= MAX_3B_SIZE { + SpiFlashConfig { + size, + read: SfCmd::READ, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + program: SfCmd::PROGRAM, + quad_enable_req: None, + } + } else { + SpiFlashConfig { + size, + read: SfCmd::READ4B_GLOBAL, + erase4k: SfCmd::ERASE4B_GLOBAL, + erase64k: SfCmd::ERASE64K_GLOBAL, + program: SfCmd::PROGRAM4B_GLOBAL, + quad_enable_req: None, + } + }; + Ok(config) + } +} + +/// "Driver" for SPI NOR flash. +pub struct SpiFlash { + spi: S, + config: SpiFlashConfig, + initialized: bool, +} + +impl SpiFlash { + pub fn new(spi: S) -> Self { + Self { + spi, + config: SpiFlashConfig { + read: SfCmd::READ, + program: SfCmd::PROGRAM, + erase4k: SfCmd::ERASE, + erase64k: SfCmd::ERASE64K, + size: NonZero::new(1).unwrap(), + quad_enable_req: None, + }, + initialized: false, + } + } + + pub fn init(&mut self) -> Result<(), ErrorCode> { + let sfdp_bytes = SfdpRandRead { spi: &mut self.spi }; + let config = SpiFlashConfig::from_sfdp(sfdp_bytes)?; + let qer = config.quad_enable_req; + + let mut status = Status::new_zeroed(); + self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes())?; + + if status.busy() { + self.wait_for_busy_to_clear()?; + } + + if let Some(qer) = qer { + match qer { + QuadEnableRequirements::NoQeBit => {} + QuadEnableRequirements::QeBit6SR1 => { + if !status.maybe_quad_en() { + status.set_maybe_quad_en(true); + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_WR_STATUS, status.into()], &mut [])?; + } + } + _ => { + self.config.read = SfCmd::READ4B; + } + } + } + + // TODO: Consider different options: + // Enter 4-byte mode and use READ4B_GLOBAL vs Use READ4B if available. + if config.size.get() > MAX_3B_SIZE { + self.enter_4byte_mode()?; + } + + self.config = config; + self.initialized = true; + Ok(()) + } + + fn enter_4byte_mode(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.spi + .write(&[OP_ENTER_4B_ADDR_MODE]) + .map_err(|_| error::FLASH_GENERIC_BUSY)?; + Ok(()) + } + + pub fn reset_device(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_RESET_ENABLE], &mut [])?; + self.transfer_req_resp(&[OP_RESET], &mut [])?; + Ok(()) + } + + pub fn read_jedec_id(&mut self, buf: &mut [u8]) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_READ_JEDEC_ID], buf) + } + + pub fn config(&self) -> &SpiFlashConfig { + &self.config + } + + pub fn set_ear(&mut self, bank: u8) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_WR_EAR, bank], &mut [])?; + Ok(()) + } + + /// Erase the entire chip. + pub fn erase_all(&mut self) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + self.transfer_req_resp(&[OP_CHIP_ERASE], &mut [])?; + self.wait_for_busy_to_clear() + } + + fn wait_for_busy_to_clear(&mut self) -> Result<(), ErrorCode> { + let mut status = Status::new_zeroed(); + loop { + self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes())?; + if !status.busy() { + return Ok(()); + } + } + } + + fn erase_cmd(&mut self, start_addr: usize, cmd: SfCmd) -> Result<(), ErrorCode> { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + + let mut buf = [0_u8; MAX_PREFIX_LEN]; + let op = cmd + .addr_mode + .write_prefix(&mut buf, cmd.opcode, start_addr)?; + self.transfer_req_resp(op, &mut [])?; + + self.wait_for_busy_to_clear() + } + + fn transfer_req_resp(&mut self, req: &[u8], resp: &mut [u8]) -> Result<(), ErrorCode> { + if resp.is_empty() { + self.spi.write(req).map_err(|_| error::FLASH_GENERIC_BUSY) + } else { + let mut ops = [Operation::Write(req), Operation::Read(resp)]; + self.spi + .transaction(&mut ops) + .map_err(|_| error::FLASH_GENERIC_BUSY) + } + } + + fn check_valid_size(&self, start_addr: usize, len: usize) -> Result<(), ErrorCode> { + let Some(end_addr) = start_addr.checked_add(len) else { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + }; + if end_addr > self.config.size.get() { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + Ok(()) + } + + pub fn is_busy(&mut self) -> bool { + let mut status = Status::new_zeroed(); + match self.transfer_req_resp(&[OP_STATUS], status.as_mut_bytes()) { + Ok(_) => status.busy(), + Err(_) => true, + } + } + + pub fn complete_op(&mut self) -> Result<(), ErrorCode> { + Ok(()) + } +} + +impl FlashTrait for SpiFlash { + type Error = ErrorCode; + + fn geometry(&mut self) -> Result<(NonZero, PowerOf2Usize, u32), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + // Support 4KB and 64KB erases + let bitmap = (1 << SECTOR_SIZE.trailing_zeros()) | (1 << BLOCK_SIZE.trailing_zeros()); + let page_size = PowerOf2Usize::new(SECTOR_SIZE).unwrap(); + Ok((self.config.size, page_size, bitmap)) + } + + fn read(&mut self, start_addr: FlashAddress, buf: &mut [u8]) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + read_common( + &mut self.spi, + start_addr.offset() as usize, + buf, + self.config.read.opcode, + self.config.read.width, + self.config.read.addr_mode, + self.config.size.get(), + ) + } + + fn program(&mut self, start_address: FlashAddress, mut data: &[u8]) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + // A single program transaction must not span 256-byte pages; writes + // that span multiple pages must be split into multiple chunks. + const PROGRAM_PAGE_LEN: usize = 256; + + let start_addr = start_address.offset() as usize; + self.check_valid_size(start_addr, data.len())?; + + // TODO: Eliminate this buffer once the drivers support vectored I/O. + let mut buf = [0_u8; MAX_PREFIX_LEN + PROGRAM_PAGE_LEN]; + + let mut addr = start_addr; + while !data.is_empty() { + self.transfer_req_resp(&[OP_WRITE_EN], &mut [])?; + let prefix_len = self + .config + .program + .addr_mode + .write_prefix( + <&mut [u8; MAX_PREFIX_LEN]>::try_from(&mut buf[..MAX_PREFIX_LEN]).unwrap(), + self.config.program.opcode, + addr, + )? + .len(); + + let chunk_len = min(data.len(), PROGRAM_PAGE_LEN - (addr % PROGRAM_PAGE_LEN)); + buf[prefix_len..][..chunk_len].copy_from_slice(&data[..chunk_len]); + self.transfer_req_resp(&buf[..prefix_len + chunk_len], &mut [])?; + + self.wait_for_busy_to_clear()?; + data = &data[chunk_len..]; + addr += chunk_len; + } + Ok(()) + } + + fn erase(&mut self, start_addr: FlashAddress, size: PowerOf2Usize) -> Result<(), ErrorCode> { + if !self.initialized { + return Err(error::FLASH_GENERIC_NOT_INITIALIZED); + } + let mut addr = start_addr.offset() as usize; + let mut len = size.get(); + + if addr % SECTOR_SIZE != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR); + } + if len % SECTOR_SIZE != 0 { + return Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE); + } + self.check_valid_size(addr, len)?; + + while len > 0 { + let can_erase_block = (addr % BLOCK_SIZE == 0) && (len >= BLOCK_SIZE); + let (cmd, erased) = if can_erase_block { + (self.config.erase64k, BLOCK_SIZE) + } else { + (self.config.erase4k, SECTOR_SIZE) + }; + self.erase_cmd(addr, cmd)?; + addr += erased; + len -= erased; + } + Ok(()) + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable)] +pub struct Status { + busy: bool, + write_en: bool, + bp0: bool, + bp1: bool, + bp2: bool, + bp3: bool, + maybe_quad_en: bool, + reserved7: bool, +} + +const OP_STATUS: u8 = 0x05; +const OP_WRITE_EN: u8 = 0x06; +const OP_WR_STATUS: u8 = 0x01; +const OP_WR_EAR: u8 = 0xC5; +const OP_READ: u8 = 0x03; +const OP_QREAD: u8 = 0x6B; +const OP_READ4B: u8 = 0x13; +const OP_QREAD4B: u8 = 0x6C; +const OP_CHIP_ERASE: u8 = 0xC7; +const OP_ERASE_4K: u8 = 0x20; +const OP_ERASE4B_4K: u8 = 0x21; +const OP_ERASE_64K: u8 = 0xD8; +const OP_ERASE4B_64K: u8 = 0xDC; +const OP_PROGRAM: u8 = 0x02; +const OP_QPROGRAM: u8 = 0x32; +const OP_PROGRAM4B: u8 = 0x12; +const OP_QPROGRAM4B: u8 = 0x34; +const OP_SFDP_READ: u8 = 0x5a; +const OP_RESET_ENABLE: u8 = 0x66; +const OP_RESET: u8 = 0x99; +const OP_READ_JEDEC_ID: u8 = 0x9f; +const OP_ENTER_4B_ADDR_MODE: u8 = 0xB7; + +/// A RandomRead implementation that can be used to access SFDP bytes. +pub struct SfdpRandRead<'a, S: embedded_hal::spi::SpiDevice> { + spi: &'a mut S, +} + +impl<'a, S: embedded_hal::spi::SpiDevice> SfdpRandRead<'a, S> { + pub fn new(spi: &'a mut S) -> Self { + Self { spi } + } +} + +const SFDP_MEM_SIZE: usize = 1 << 24; + +impl RandomRead for SfdpRandRead<'_, S> { + type Error = ErrorCode; + fn read(&mut self, start_addr: usize, buf: &mut [u8]) -> Result<(), Self::Error> { + read_common( + self.spi, + start_addr, + buf, + OP_SFDP_READ, + SpiTxnWidth::STANDARD, + AddressingMode::_3ByteWithDummy, + SFDP_MEM_SIZE, + ) + } + fn size(&mut self) -> Result { + Ok(SFDP_MEM_SIZE) + } +} + +fn read_common( + spi: &mut S, + start_addr: usize, + buf: &mut [u8], + opcode: u8, + _width: SpiTxnWidth, + addr_size: AddressingMode, + src_total_len: usize, +) -> Result<(), ErrorCode> { + let Some(end_addr) = start_addr.checked_add(buf.len()) else { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + }; + if end_addr > src_total_len { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let mut cmd_buf = [0; MAX_PREFIX_LEN]; + let cmd_buf = addr_size.write_prefix(&mut cmd_buf, opcode, start_addr)?; + + let mut ops = [Operation::Write(cmd_buf), Operation::Read(buf)]; + spi.transaction(&mut ops) + .map_err(|_| error::FLASH_GENERIC_BUSY) +} + +const _: () = assert!( + size_of::() >= size_of::(), + "on supported platforms, usize must be at least 32-bits" +); + +/// Describes how addresses should be formatting on the wire +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressingMode { + _3Byte = 0, + _4Byte = 1, + _3ByteWithDummy = 2, + _4ByteWithDummy = 3, +} + +impl AddressingMode { + /// Writes the opcode, address and (if needed) dummy byte to `buf`, and + /// returns a slice to the part of `buf` that should be sent as the initial + /// bytes of the transaction. + #[inline] + fn write_prefix( + self, + buf: &mut [u8; MAX_PREFIX_LEN], + opcode: u8, + addr: usize, + ) -> Result<&[u8], ErrorCode> { + buf[0] = opcode; + if !self.is_valid_addr(addr) { + return Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS); + } + let shift = if matches!(self, Self::_3Byte | Self::_3ByteWithDummy) { + 8 + } else { + 0 + }; + let addr_bytes = u32::try_from(addr << shift).unwrap().to_be_bytes(); + *<&mut [u8; 4]>::try_from(&mut buf[1..5]).unwrap() = addr_bytes; + let len = match self { + Self::_3Byte => 4, + Self::_4Byte => 5, + Self::_3ByteWithDummy => 5, + Self::_4ByteWithDummy => { + buf[5] = 0; + 6 + } + }; + Ok(&buf[..len]) + } + + /// Returns true if `addr` can be represented fully by this address mode. + #[inline] + fn is_valid_addr(self, addr: usize) -> bool { + if addr < MAX_3B_SIZE { + return true; + } + match self { + Self::_3Byte | Self::_3ByteWithDummy => addr < MAX_3B_SIZE, + Self::_4Byte | Self::_4ByteWithDummy => u32::try_from(addr).is_ok(), + } + } +} + +#[cfg(test)] +mod test { + extern crate std; + use super::*; + use drivers_mock_spi_device_fake::*; + use std::vec; + use std::vec::Vec; + use util_sfdp::*; + + const GIB: usize = 1024 * MIB; + + const STATUS_WIP_WEL: u8 = 0x03; + const STATUS_WIP: u8 = 0x01; + const STATUS_READY: u8 = 0x00; + // Note: Some parts use a different bit position. + // See "6.4.18 JEDEC Basic Flash Parameter Table: 15th DWORD" of JESD216 and + // `QuadEnableRequirements`. + // FIXME: STATUS_QE is currently unused because QSPI dynamic initialization is disabled. + #[allow(dead_code)] + const STATUS_QE: u8 = 0x40; + + fn preprogram_init(fake_spi: &FakeSpiDevice, size_bytes: usize) { + let sfdp = gen_sfdp(size_bytes); + preprogram_sfdp(fake_spi, &sfdp); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + if size_bytes > 16 * MIB { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_ENTER_4B_ADDR_MODE].into(), vec![].into()); + } + } + + /// Generates some generic SFDP for a flash of a specific size (in bytes). + fn gen_sfdp(flash_total_len: usize) -> Vec { + const SFDP_HEADER: SfdpHeader = SfdpHeader { + sig: SfdpSignature::EXPECTED_VALUE, + major_rev: 1, + minor_rev: 0, + access_protocol: AccessProtocol::LEGACY, + num_parameter_header: 0, // 0-based => 0 means 1 header + }; + const SFDP_PARAMETER_HEADER: ParameterHeader = ParameterHeader { + parameter_id_lsb: 0x00, + major_rev: 1, + minor_rev: 0, + len_in_dwords: 23, + ptr: U24::new(16), + parameter_id_msb: 0xff, + }; + let mut result = vec![]; + result.extend_from_slice(SFDP_HEADER.as_bytes()); + result.extend_from_slice(SFDP_PARAMETER_HEADER.as_bytes()); + let mut bpt = BasicFlashParameterTable::new_zeroed(); + bpt.table_jesd216.memory_density = + MemoryDensity::from_byte_len(u32::try_from(flash_total_len).unwrap()).unwrap(); + //FIXME: Add test cases for the rest of the SFDP Quad support variations + bpt.table_jesd216.word1.set_supports_1s_1s_4s_read(true); + bpt.table_jesd216a + .word15 + .set_quad_enable_requirements(QuadEnableRequirements::QeBit6SR1); + result.extend_from_slice(bpt.as_bytes()); + result + } + + fn preprogram_sfdp(fake_spi: &FakeSpiDevice, sfdp_bytes: &[u8]) { + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 0, 0].into(), + sfdp_bytes[..8].to_vec().into(), + ); + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 8, 0].into(), + sfdp_bytes[8..16].to_vec().into(), + ); + fake_spi.preprogram_data_response( + vec![OP_SFDP_READ, 0, 0, 16, 0].into(), + sfdp_bytes[16..].to_vec().into(), + ); + } + + #[test] + fn test_size_8mb() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 8 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + assert_eq!(8 * MIB, flash.geometry().unwrap().0.get()); + assert_eq!(8 * MIB, flash.random_reader().size().unwrap()); + fake_spi.assert_all_expectations_met(); + } + #[test] + fn test_size_128mb() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + assert_eq!(128 * MIB, flash.geometry().unwrap().0.get()); + assert_eq!(128 * MIB, flash.random_reader().size().unwrap()); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response( + (&[OP_READ, 0x12, 0x34, 0x56]).into(), + b"Hello World!".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + let mut buf = [0x55_u8; 12]; + flash + .read(FlashAddress::new(0x12_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello World!"); + assert_eq!( + flash.read(FlashAddress::new(0x1234_5678_u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + assert_eq!( + flash.read(FlashAddress::new((8 * MIB) as u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_4b() { + let fake_spi = FakeSpiDevice::new(); + // 32 MiB flash + preprogram_init(&fake_spi, 32 * MIB); + fake_spi + .preprogram_data_response((&[OP_READ, 0x01, 0x23, 0x45, 0x67]).into(), b"World".into()); + fake_spi + .preprogram_data_response((&[OP_READ, 0x00, 0x12, 0x34, 0x56]).into(), b"Hello".into()); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + let mut buf = [0x55_u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + flash + .read(FlashAddress::new(0x0123_4567_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"World"); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_read_4b_qspi() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + // Expect OP_QREAD4B + 4B Address + 1 Dummy Byte (0x00) + fake_spi.preprogram_data_response( + vec![OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00].into(), + b"Hello".to_vec().into(), + ); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD4B; + + let mut buf = [0u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + fake_spi.assert_all_expectations_met(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qread_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response( + (&[OP_QREAD, 0x12, 0x34, 0x56, 0x00]).into(), + b"Hello World!".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD; + let mut buf = [0x55_u8; 12]; + flash + .read(FlashAddress::new(0x12_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello World!"); + assert_eq!( + flash.read(FlashAddress::new(0x1234_5678_u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + assert_eq!( + flash.read(FlashAddress::new((8 * MIB) as u32), &mut buf), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qread_4b() { + let fake_spi = FakeSpiDevice::new(); + // 32 MiB flash + preprogram_init(&fake_spi, 32 * MIB); + fake_spi.preprogram_data_response( + (&[OP_QREAD4B, 0x01, 0x23, 0x45, 0x67, 0x00]).into(), + b"World".into(), + ); + fake_spi.preprogram_data_response( + (&[OP_QREAD4B, 0x00, 0x12, 0x34, 0x56, 0x00]).into(), + b"Hello".into(), + ); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.read = SfCmd::QREAD4B; + let mut buf = [0x55_u8; 5]; + flash + .read(FlashAddress::new(0x0012_3456_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"Hello"); + flash + .read(FlashAddress::new(0x0123_4567_u32), &mut buf) + .unwrap(); + assert_eq!(&buf, b"World"); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_3b() { + let fake_spi = FakeSpiDevice::new(); + // 16 MiB flash + preprogram_init(&fake_spi, 16 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0xba, 0x10, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP_WEL]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0xba_1000_u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 5..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase + tx: vec![OP_ERASE_4K, 0xba, 0x10, 0b00], + rx: vec![0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=1, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP_WEL], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001_u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((16 * MIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_4b() { + let fake_spi = FakeSpiDevice::new(); + // 1 GiB flash + preprogram_init(&fake_spi, GIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_4K, 0x1a, 0x5e, 0xb0, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP_WEL]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(0x1a5e_b000_u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase (4-byte addr) + tx: vec![OP_ERASE_4K, 0x1a, 0x5e, 0xb0, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=1, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP_WEL], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + assert_eq!( + flash.erase( + FlashAddress::new(0xba_1001_u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((GIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_last_page() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0x7f, 0xf0, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((8 * MIB - 4096) as u32), + PowerOf2Usize::new(4096).unwrap(), + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // erase (4-byte addr) + tx: vec![OP_ERASE_4K, 0x7f, 0xf0, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + + assert_eq!( + flash.erase( + FlashAddress::new((8 * MIB - 1) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + assert_eq!( + flash.erase( + FlashAddress::new((8 * MIB) as u32), + PowerOf2Usize::new(4096).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_page_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + const ADDRESS: usize = 0x1000; + const LEN: usize = 4 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_4K, 0x00, 0x10, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x10, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_page_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + const ADDRESS: usize = 0x1000; + const LEN: usize = 4 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_4K, 0x00, 0x00, 0x10, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x00, 0x10, 0x00], + rx: vec![0, 0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_block_64k_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + const ADDRESS: usize = 0x10000; + const LEN: usize = 64 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_ERASE_64K, 0x01, 0x00, 0x00]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_64K, 0x01, 0x00, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_single_block_64k_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + const ADDRESS: usize = 0x01230000; + const LEN: usize = 64 * KIB; + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_ERASE_64K, 0x01, 0x23, 0x00, 0x00]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_64K, 0x01, 0x23, 0x00, 0x00], + rx: vec![0, 0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_mixed_granularity_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Erase sequence: 15 pages, 1 block, 1 page. + const ADDRESS: usize = 4 * KIB; + const LEN: usize = 128 * KIB; + + let mut expectations = Vec::new(); + for i in 1..=15 { + expectations.push((OP_ERASE_4K, 0x00, (i * 0x10) as u8, 0x00)); + } + expectations.push((OP_ERASE_64K, 0x01, 0x00, 0x00)); + expectations.push((OP_ERASE_4K, 0x02, 0x00, 0x00)); + + for (op, a1, a2, a3) in expectations.clone() { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![op, a1, a2, a3].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, a1, a2, a3)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + assert_eq!(log[offset + 1].tx, vec![*op, *a1, *a2, *a3]); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_mixed_granularity_4b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + + // Erase sequence: 1 page, 1 block, 15 pages. + const ADDRESS: usize = MAX_3B_SIZE - 4 * KIB; + const LEN: usize = 128 * KIB; + + let mut expectations = Vec::new(); + expectations.push((OP_ERASE_4K, 0x00, 0xff, 0xf0, 0x00)); + expectations.push((OP_ERASE_64K, 0x01, 0x00, 0x00, 0x00)); + for i in 0..15 { + expectations.push((OP_ERASE_4K, 0x01, 0x01, (i * 0x10) as u8, 0x00)); + } + + for (op, a1, a2, a3, a4) in expectations.clone() { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![op, a1, a2, a3, a4].into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, a1, a2, a3, a4)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + assert_eq!(log[offset + 1].tx, vec![*op, *a1, *a2, *a3, *a4]); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_alignment_errors() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + // Start address not page aligned + assert_eq!( + flash.erase(FlashAddress::new(1_u32), PowerOf2Usize::new(4096).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_ADDR) + ); + + // Length not page aligned + assert_eq!( + flash.erase(FlashAddress::new(0_u32), PowerOf2Usize::new(1).unwrap()), + Err(error::FLASH_GENERIC_ERASE_INVALID_SIZE) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_out_of_bounds() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((16 * MIB - 4 * KIB) as u32), + PowerOf2Usize::new(8 * KIB).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + + // 4B flash (32 MiB) + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 32 * MIB); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + + assert_eq!( + flash.erase( + FlashAddress::new((32 * MIB) as u32), + PowerOf2Usize::new(4 * KIB).unwrap() + ), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_entire_flash_3b() { + let fake_spi = FakeSpiDevice::new(); + const SIZE: usize = 128 * KIB; + preprogram_init(&fake_spi, SIZE); + + let expectations = [ + (OP_ERASE_64K, vec![0x00, 0x00, 0x00]), + (OP_ERASE_64K, vec![0x01, 0x00, 0x00]), + ]; + + for (op, addr_bytes) in &expectations { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + let mut tx = vec![*op]; + tx.extend_from_slice(addr_bytes); + fake_spi.preprogram_data_response(tx.into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0_u32), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * expectations.len()..]; + for (i, (op, addr_bytes)) in expectations.iter().enumerate() { + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + let mut expected_tx = vec![*op]; + expected_tx.extend_from_slice(addr_bytes); + assert_eq!(log[offset + 1].tx, expected_tx); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_all() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * KIB); + + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_CHIP_ERASE]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.erase_all().unwrap(); + + let log = fake_spi.log(); + let log = &log[(log.len() - 4)..]; + assert_eq!( + log, + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_CHIP_ERASE], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, 0x01] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, 0x00] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_entire_flash_4b() { + let fake_spi = FakeSpiDevice::new(); + const SIZE: usize = 32 * MIB; + preprogram_init(&fake_spi, SIZE); + + const NUM_BLOCKS: usize = SIZE / (64 * KIB); + for i in 0..NUM_BLOCKS { + let addr = (i * 64 * KIB) as u32; + let addr_bytes = addr.to_be_bytes(); + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + let mut tx = vec![OP_ERASE_64K]; + tx.extend_from_slice(&addr_bytes); + fake_spi.preprogram_data_response(tx.into(), vec![].into()); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase(FlashAddress::new(0_u32), PowerOf2Usize::new(SIZE).unwrap()) + .unwrap(); + + let log = fake_spi.log(); + let log = &log[log.len() - 3 * NUM_BLOCKS..]; + for i in 0..NUM_BLOCKS { + let addr = (i * 64 * KIB) as u32; + let addr_bytes = addr.to_be_bytes(); + let offset = i * 3; + assert_eq!(log[offset].tx, vec![OP_WRITE_EN]); + let mut expected_tx = vec![OP_ERASE_64K]; + expected_tx.extend_from_slice(&addr_bytes); + assert_eq!(log[offset + 1].tx, expected_tx); + assert_eq!(log[offset + 2].tx, vec![OP_STATUS, 0x00]); + } + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_multiple_4k_pages_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Erase sequence: four 4K pages. + const ADDRESS: usize = 0x1000; + const LEN: usize = 16 * KIB; + + for i in 1..=4 { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response( + vec![OP_ERASE_4K, 0x00, (i * 0x10) as u8, 0x00].into(), + vec![].into(), + ); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new(ADDRESS as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 12..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x10, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x20, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x30, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x00, 0x40, 0x00], + rx: vec![0x00, 0x00, 0x00, 0x00] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_erase_block_aligned_small_len_3b() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 16 * MIB); + + // Address is 64K aligned, but length is < 64K. Should use 4K erases. + const ADDRESS: usize = 0x10000; + const LEN: usize = 8 * KIB; + + for i in 0..2 { + fake_spi.preprogram_data_response(vec![OP_WRITE_EN].into(), vec![].into()); + fake_spi.preprogram_data_response( + vec![OP_ERASE_4K, 0x01, (i * 0x10) as u8, 0x00].into(), + vec![].into(), + ); + fake_spi.preprogram_data_response(vec![OP_STATUS].into(), vec![STATUS_READY].into()); + } + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .erase( + FlashAddress::new((ADDRESS) as u32), + PowerOf2Usize::new(LEN).unwrap(), + ) + .unwrap(); + + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 6..], + &[ + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x01, 0x00, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + FakeSpiTransfer { + tx: vec![OP_WRITE_EN], + rx: vec![0x00] + }, + FakeSpiTransfer { + tx: vec![OP_ERASE_4K, 0x01, 0x10, 0x00], + rx: vec![0, 0, 0, 0] + }, + FakeSpiTransfer { + tx: vec![OP_STATUS, 0x00], + rx: vec![0, STATUS_READY] + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_1140_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_4b() { + let fake_spi = FakeSpiDevice::new(); + // 128 MiB flash + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x02, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program( + FlashAddress::new(0x0274_1140_u32), + &[0xba, 0x5e, 0xba, 0x11], + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x02, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_3b() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_1140_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_QPROGRAM, 0x74, 0x11, 0x40, 0xba, 0x5e, 0xba, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_4b() { + let fake_spi = FakeSpiDevice::new(); + // 128 MiB flash + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[ + OP_QPROGRAM4B, + 0x02, + 0x74, + 0x11, + 0x40, + 0xba, + 0x5e, + 0xba, + 0x11, + ]) + .into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM4B; + flash + .program( + FlashAddress::new(0x0274_1140_u32), + &[0xba, 0x5e, 0xba, 0x11], + ) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 4..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![ + OP_QPROGRAM4B, + 0x02, + 0x74, + 0x11, + 0x40, + 0xba, + 0x5e, + 0xba, + 0x11 + ], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_last_byte() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi + .preprogram_data_response((&[OP_PROGRAM, 0x7f, 0xff, 0xff, 0x42]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x7f_ffff_u32), &[0x42]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_PROGRAM, 0x7f, 0xff, 0xff, 0x42], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + // Programming past the end of the flash should result in an error + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff_u32), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_program_spans_pages() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_PROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi + .preprogram_data_response((&[OP_PROGRAM, 0x74, 0x12, 0x00, 0x11]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash + .program(FlashAddress::new(0x74_11fd_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 8..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program end of first page + tx: vec![OP_PROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program start of next page + tx: vec![OP_PROGRAM, 0x74, 0x12, 0x00, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_last_byte() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x7f, 0xff, 0xff, 0x42]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x7f_ffff_u32), &[0x42]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 3..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program + tx: vec![OP_QPROGRAM, 0x7f, 0xff, 0xff, 0x42], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + // Programming past the end of the flash should result in an error + assert_eq!( + flash.program(FlashAddress::new(0x7f_ffff_u32), &[0x42, 0x42]), + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS) + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_qprogram_spans_pages() { + let fake_spi = FakeSpiDevice::new(); + // 8 MiB flash + preprogram_init(&fake_spi, 8 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response( + (&[OP_QPROGRAM, 0x74, 0x12, 0x00, 0x11]).into(), + (&[]).into(), + ); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_WIP]).into()); + fake_spi.preprogram_data_response((&[OP_STATUS]).into(), (&[STATUS_READY]).into()); + + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.config.program = SfCmd::QPROGRAM; + flash + .program(FlashAddress::new(0x74_11fd_u32), &[0xba, 0x5e, 0xba, 0x11]) + .unwrap(); + assert_eq!( + &fake_spi.log()[fake_spi.log().len() - 8..], + &[ + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program end of first page + tx: vec![OP_QPROGRAM, 0x74, 0x11, 0xfd, 0xba, 0x5e, 0xba], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + FakeSpiTransfer { + // write-enable + tx: vec![OP_WRITE_EN], + rx: vec![0x00], + }, + FakeSpiTransfer { + // program start of next page + tx: vec![OP_QPROGRAM, 0x74, 0x12, 0x00, 0x11], + rx: vec![0x00, 0x00, 0x00, 0x00, 0x00], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0, WIP=1 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_WIP], + }, + FakeSpiTransfer { + // get-status: WRITE_EN=0,WIP=0 + tx: vec![OP_STATUS, 0x00], + rx: vec![0x00, STATUS_READY], + }, + ] + ); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn test_set_ear() { + let fake_spi = FakeSpiDevice::new(); + preprogram_init(&fake_spi, 128 * MIB); + fake_spi.preprogram_data_response((&[OP_WRITE_EN]).into(), (&[]).into()); + fake_spi.preprogram_data_response((&[OP_WR_EAR, 0x02]).into(), (&[]).into()); + let mut flash = SpiFlash::new(fake_spi.clone()); + flash.init().unwrap(); + flash.set_ear(0x02).unwrap(); + fake_spi.assert_all_expectations_met(); + } + + #[test] + fn address_size_is_valid_addr() { + assert!(AddressingMode::_3Byte.is_valid_addr(0)); + assert!(AddressingMode::_3Byte.is_valid_addr(MAX_3B_SIZE - 1)); + assert!(!AddressingMode::_3Byte.is_valid_addr(MAX_3B_SIZE)); + + assert!(AddressingMode::_4Byte.is_valid_addr(MAX_3B_SIZE)); + assert!(AddressingMode::_4Byte.is_valid_addr(0xffff_ffff)); + + #[cfg(target_pointer_width = "8")] + assert!(!AddressingMode::_4Byte.is_valid_addr(0x1_0000_0000)); + + #[cfg(target_pointer_width = "8")] + assert!(!AddressingMode::_4Byte.is_valid_addr(0xffff_ffff_ffff_ffff)); + } + + #[test] + fn address_size_write_prefix() { + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56].as_slice()), + AddressingMode::_3Byte.write_prefix(&mut buf, OP_READ, 0x12_3456) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x00].as_slice()), + AddressingMode::_3ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x12_3456) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x78].as_slice()), + AddressingMode::_4Byte.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + + let mut buf = [0xdd; MAX_PREFIX_LEN]; + assert_eq!( + Ok([OP_READ, 0x12, 0x34, 0x56, 0x78, 0x00].as_slice()), + AddressingMode::_4ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + + assert_eq!( + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS), + AddressingMode::_3Byte.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + assert_eq!( + Err(error::FLASH_GENERIC_ADDR_OUT_OF_BOUNDS), + AddressingMode::_3ByteWithDummy.write_prefix(&mut buf, OP_READ, 0x1234_5678) + ); + } +} diff --git a/drivers/mock/BUILD.bazel b/drivers/mock/BUILD.bazel new file mode 100644 index 00000000..bccfc1d4 --- /dev/null +++ b/drivers/mock/BUILD.bazel @@ -0,0 +1,21 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "spi_device_fake", + srcs = ["spi_device_fake.rs"], + crate_name = "drivers_mock_spi_device_fake", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//util/error", + "@rust_crates//:embedded-hal", + ], +) + +rust_test( + name = "spi_device_fake_test", + crate = ":spi_device_fake", +) diff --git a/drivers/mock/spi_device_fake.rs b/drivers/mock/spi_device_fake.rs new file mode 100644 index 00000000..ed1d4c5a --- /dev/null +++ b/drivers/mock/spi_device_fake.rs @@ -0,0 +1,320 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +extern crate std; + +use std::borrow::Cow; +use std::cell::Ref; +use std::cell::RefCell; +use std::ops::Deref; +use std::rc::Rc; +use util_error::ErrorCode; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FakeSpiError(pub ErrorCode); + +impl embedded_hal::spi::Error for FakeSpiError { + fn kind(&self) -> embedded_hal::spi::ErrorKind { + embedded_hal::spi::ErrorKind::Other + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FakeSpiTransfer { + pub tx: Vec, + pub rx: Vec, +} + +pub struct ResponseProgram<'a, TError> { + pub prefix: Cow<'a, [u8]>, + pub response: Response<'a, TError>, +} + +pub enum Response<'a, TError> { + Data(Cow<'a, [u8]>), + Error(TError), +} + +#[derive(Clone)] +pub struct FakeSpiDevice<'a> { + log: Rc>>, + response_programs: Rc>>>, +} + +impl<'a> FakeSpiDevice<'a> { + pub fn new() -> Self { + Self { + log: Rc::new(RefCell::new(vec![])), + response_programs: Rc::new(RefCell::new(vec![])), + } + } + + /// Adds a programmed response to the fake device. If the SPI request + /// matches `prefix`, then `response` will be received after `prefix.len()` + /// zeroes. + pub fn preprogram_data_response(&self, prefix: Cow<'a, [u8]>, response: Cow<'a, [u8]>) { + self.response_programs.borrow_mut().push(ResponseProgram { + prefix, + response: Response::Data(response), + }) + } + + /// Adds a programmed error response to the fake device. If the SPI request + /// matches `prefix`, then the supplied error will be returned. + pub fn preprogram_error_response(&self, prefix: Cow<'a, [u8]>, err: ErrorCode) { + self.response_programs.borrow_mut().push(ResponseProgram { + prefix, + response: Response::Error(FakeSpiError(err)), + }) + } + + pub fn log(&self) -> impl Deref + use<'a, '_> { + Ref::map(self.log.borrow(), |log| log.as_slice()) + } + + pub fn assert_all_expectations_met(&self) { + assert!( + self.response_programs.borrow().is_empty(), + "Not all expected SPI response programs were executed: remaining = {}", + self.response_programs.borrow().len() + ); + } +} + +impl Default for FakeSpiDevice<'_> { + fn default() -> Self { + Self::new() + } +} + +impl embedded_hal::spi::ErrorType for FakeSpiDevice<'_> { + type Error = FakeSpiError; +} + +impl embedded_hal::spi::SpiDevice for FakeSpiDevice<'_> { + fn transaction( + &mut self, + operations: &mut [embedded_hal::spi::Operation<'_, u8>], + ) -> Result<(), Self::Error> { + let mut tx = Vec::new(); + let mut read_ops = Vec::new(); + + for op in operations.iter_mut() { + match op { + embedded_hal::spi::Operation::Write(buf) => { + tx.extend_from_slice(buf); + } + embedded_hal::spi::Operation::Read(buf) => { + read_ops.push(buf); + } + embedded_hal::spi::Operation::Transfer(read_buf, write_buf) => { + tx.extend_from_slice(write_buf); + read_ops.push(read_buf); + } + embedded_hal::spi::Operation::TransferInPlace(buf) => { + tx.extend_from_slice(buf); + // TransferInPlace is both write and read. In SpiFlash this is not typically used, + // but we treat it as read mapping to the same buffer. + read_ops.push(buf); + } + embedded_hal::spi::Operation::DelayNs(_) => {} + } + } + + let mut response_programs = self.response_programs.borrow_mut(); + let Some(response_index) = response_programs + .iter() + .position(|resp| tx.starts_with(&resp.prefix)) + else { + panic!("Unexpected SPI transaction: tx = {tx:x?}"); + }; + + let program = response_programs.remove(response_index); + + match program.response { + Response::Data(data) => { + let skipped_resp_bytes = tx.len() - program.prefix.len(); + let total_rx_requested: usize = read_ops.iter().map(|buf| buf.len()).sum(); + + if data.len() != total_rx_requested + skipped_resp_bytes { + panic!( + "Expected transaction with prefix {:x?} to return {} bytes, but a response of len {} was requested ({} bytes skipped). Simulated data len = {}", + program.prefix, + data.len(), + total_rx_requested, + skipped_resp_bytes, + data.len() + ); + } + + let mut data_offset = skipped_resp_bytes; + for buf in read_ops { + let len = buf.len(); + buf.copy_from_slice(&data[data_offset..data_offset + len]); + data_offset += len; + } + + let mut rx = vec![0; program.prefix.len()]; + rx.extend_from_slice(&data); + + let mut logged_tx = tx.clone(); + if logged_tx.len() < rx.len() { + logged_tx.resize(rx.len(), 0); + } + + self.log + .borrow_mut() + .push(FakeSpiTransfer { tx: logged_tx, rx }); + Ok(()) + } + Response::Error(err) => Err(err), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use embedded_hal::spi::{Operation, SpiDevice}; + use util_error::FLASH_GENERIC_BUSY; + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_no_expectations() { + let mut spi = FakeSpiDevice::new(); + let mut rx = [0; 4]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_no_matching_expectations() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hola".into(), b"adios".into()); + let mut rx = [0; 4]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + fn test_matching_expectations_exact_req_len() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hola".into(), b"adios".into()); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + + let mut rx1 = [0; 7]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx1)]) + .unwrap(); + assert_eq!(b"goodbye", &rx1); + + let mut rx2 = [0; 5]; + spi.transaction(&mut [Operation::Write(b"hola"), Operation::Read(&mut rx2)]) + .unwrap(); + assert_eq!(b"adios", &rx2); + + assert_eq!( + *spi.log(), + [ + FakeSpiTransfer { + tx: b"hi\0\0\0\0\0\0\0".into(), + rx: b"\0\0goodbye".into(), + }, + FakeSpiTransfer { + tx: b"hola\0\0\0\0\0".into(), + rx: b"\0\0\0\0adios".into(), + } + ] + ); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_matching_expectations_nonexact_req_len() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + let mut rx = [0; 6]; + spi.transaction(&mut [Operation::Write(b"hi!"), Operation::Read(&mut rx)]) + .unwrap(); + assert_eq!(b"oodbye", &rx); + assert_eq!( + *spi.log(), + [FakeSpiTransfer { + tx: b"hi!\0\0\0\0\0\0".into(), + rx: b"\0\0goodbye".into(), + }] + ); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_matching_expectations_error() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_error_response(b"hi".into(), FLASH_GENERIC_BUSY); + let mut rx = [0; 6]; + assert_eq!( + spi.transaction(&mut [Operation::Write(b"hi!"), Operation::Read(&mut rx)]), + Err(FakeSpiError(FLASH_GENERIC_BUSY)) + ); + } + + #[test] + #[should_panic( + expected = "Expected transaction with prefix [68, 69] to return 7 bytes, but a response of len 5 was requested" + )] + fn test_wrong_response_size() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"goodbye".into()); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Write(b"hi"), Operation::Read(&mut rx)]) + .unwrap(); + } + + #[test] + fn test_transfer_success() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"hello".into()); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx, b"hi")]) + .unwrap(); + assert_eq!(b"hello", &rx); + spi.assert_all_expectations_met(); + } + + #[test] + fn test_transfer_multi_success() { + let mut spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"hi".into(), b"hello".into()); + spi.preprogram_data_response(b"bye".into(), b"world".into()); + let mut rx1 = [0; 5]; + let mut rx2 = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx1, b"hi")]) + .unwrap(); + spi.transaction(&mut [Operation::Transfer(&mut rx2, b"bye")]) + .unwrap(); + assert_eq!(b"hello", &rx1); + assert_eq!(b"world", &rx2); + spi.assert_all_expectations_met(); + } + + #[test] + #[should_panic(expected = "Unexpected SPI transaction")] + fn test_transfer_panic_on_missing_program() { + let mut spi = FakeSpiDevice::new(); + let mut rx = [0; 5]; + spi.transaction(&mut [Operation::Transfer(&mut rx, b"hi")]) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "Not all expected SPI response programs were executed: remaining = 1" + )] + fn test_unmet_expectation_panics() { + let spi = FakeSpiDevice::new(); + spi.preprogram_data_response(b"unused_cmd".into(), b"data".into()); + // We do NOT call transaction() to execute this preprogrammed response. + // Calling assert_all_expectations_met() must panic! + spi.assert_all_expectations_met(); + } +} diff --git a/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel b/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel new file mode 100644 index 00000000..793caf58 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/BUILD.bazel @@ -0,0 +1,107 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "spi_flash", + srcs = [ + "spi_flash.rs", + ], + codegen_crate_name = "spi_flash_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:spi_host", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +system_image( + name = "spi_flash_image", + apps = [ + ":spi_flash", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "spi_flash_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash_image", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash_image", +) diff --git a/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs b/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs new file mode 100644 index 00000000..5276e8ca --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/spi_flash.rs @@ -0,0 +1,118 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Smoke test for generic SPI Flash driver on Earlgrey. +//! +//! Initializes SPI Host, SpiFlash driver, wraps them in BlockingFlash, +//! and performs Erase -> Program -> Read -> Verify cycle. + +#![no_std] +#![no_main] + +use hal_flash::Flash; +use hal_flash_driver::FlashAddress; +use pw_status::{Error, Result}; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; +use userspace::entry; + +fn run_test() -> Result<()> { + // 1. Initialize SPI Host. + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + spi_host + .init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) + .map_err(|_| Error::Internal)?; + + // 2. Create SpiFlash driver. + let mut flash = SpiFlash::new(spi_host); + + // 3. Initialize SpiFlash driver. + flash.init().map_err(|_| Error::Internal)?; + + // 4. Print geometry. + let (size, page_size, erase_bitmap) = flash.geometry().map_err(|_| Error::Internal)?; + pw_log::info!( + "Flash geometry: size={} bytes, page_size={} bytes, erase_bitmap=0x{:x}", + size, + page_size.get(), + erase_bitmap + ); + + // 5. Test Erase -> Program -> Read -> Verify + // We use address 0x00100000 (1MB offset) which is aligned to 64KB block boundary. + let test_addr = FlashAddress::new(0x0010_0000); + + // Erase 4KB sector. + let erase_size = util_types::PowerOf2Usize::new(4096).unwrap(); + pw_log::info!("Erasing 4KB at offset {}...", test_addr); + flash + .erase(test_addr, erase_size) + .map_err(|_| Error::Internal)?; + pw_log::info!("Erase complete."); + + // Read back and verify it is erased (all bytes 0xFF). + let mut read_buf = [0u8; 256]; + flash + .read(test_addr, &mut read_buf) + .map_err(|_| Error::Internal)?; + for (i, &b) in read_buf.iter().enumerate() { + if b != 0xFF { + pw_log::error!( + "Erase verification failed at offset +{}: expected 0xFF, got 0x{:02x}", + i, + b + ); + return Err(Error::FailedPrecondition); + } + } + pw_log::info!("Erase verified (all 0xFF)."); + + // Program a page (256 bytes). + let mut write_buf = [0u8; 256]; + for (i, b) in write_buf.iter_mut().enumerate() { + *b = i as u8; + } + pw_log::info!("Programming 256 bytes at offset {}...", test_addr); + flash + .program(test_addr, &write_buf) + .map_err(|_| Error::Internal)?; + pw_log::info!("Program complete."); + + // Read back and verify program. + read_buf.fill(0); + flash + .read(test_addr, &mut read_buf) + .map_err(|_| Error::Internal)?; + for (i, (&w, &r)) in write_buf.iter().zip(read_buf.iter()).enumerate() { + if w != r { + pw_log::error!( + "Verification failed at offset +{}: wrote 0x{:02x}, read 0x{:02x}", + i, + w, + r + ); + return Err(Error::FailedPrecondition); + } + } + pw_log::info!("Program verified successfully!"); + + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + pw_log::info!("🔄 RUNNING SPI Flash Smoke Test"); + let ret = run_test(); + + if ret.is_err() { + pw_log::error!("❌ FAIL"); + } else { + pw_log::info!("✅ PASS"); + } + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/drivers/spi_flash/system.json5 b/target/earlgrey/tests/drivers/spi_flash/system.json5 new file mode 100644 index 00000000..4c9b30c3 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_flash/system.json5 @@ -0,0 +1,41 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "spi_flash", + flash_size_bytes: 24576, // Slightly larger because of SFDP parser inclusion + processes: [{ + name: "spi_flash_process", + ram_size_bytes: 8192, // Larger stack/heap just in case + objects: [ + { + type: "thread", + name: "spi_thread", + kernel_stack_size_bytes: 2048, + }, + ], + memory_mappings: [ + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/drivers/spi_flash/target.rs b/target/earlgrey/tests/drivers/spi_flash/target.rs new file mode 100644 index 00000000..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 3e68ba804ed7edfddc910c2e1c9f5fcac086ee24 Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 14 Jul 2026 08:09:48 +0800 Subject: [PATCH 02/13] earlgrey/tests: Add SPI Flash IPC integration test --- target/earlgrey/tests/spi_flash/BUILD.bazel | 148 ++++++++++++++++++ .../tests/spi_flash/combined_flash_server.rs | 145 +++++++++++++++++ .../tests/spi_flash/spi_flash_test.rs | 110 +++++++++++++ target/earlgrey/tests/spi_flash/system.json5 | 98 ++++++++++++ target/earlgrey/tests/spi_flash/target.rs | 29 ++++ 5 files changed, 530 insertions(+) create mode 100644 target/earlgrey/tests/spi_flash/BUILD.bazel create mode 100644 target/earlgrey/tests/spi_flash/combined_flash_server.rs create mode 100644 target/earlgrey/tests/spi_flash/spi_flash_test.rs create mode 100644 target/earlgrey/tests/spi_flash/system.json5 create mode 100644 target/earlgrey/tests/spi_flash/target.rs diff --git a/target/earlgrey/tests/spi_flash/BUILD.bazel b/target/earlgrey/tests/spi_flash/BUILD.bazel new file mode 100644 index 00000000..114bc229 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/BUILD.bazel @@ -0,0 +1,148 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "combined_flash_server", + srcs = [ + "combined_flash_server.rs", + ], + codegen_crate_name = "combined_flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/registers:spi_host", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +rust_app( + name = "spi_flash_test", + srcs = [ + "spi_flash_test.rs", + ], + codegen_crate_name = "spi_flash_test_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:client", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/misc", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "spi_flash", + apps = [ + ":combined_flash_server", + ":spi_flash_test", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "spi_flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash", +) + +rust_binary_no_panics_test( + name = "no_panics", + apps = [ + "combined_flash_server", + "spi_flash_test", + ], + binary = ":spi_flash", +) diff --git a/target/earlgrey/tests/spi_flash/combined_flash_server.rs b/target/earlgrey/tests/spi_flash/combined_flash_server.rs new file mode 100644 index 00000000..921c297c --- /dev/null +++ b/target/earlgrey/tests/spi_flash/combined_flash_server.rs @@ -0,0 +1,145 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use combined_flash_server_codegen::{handle, signals}; +use earlgrey_util::EarlgreyFlashAddress; +use eflash_driver::{EmbeddedFlash, Permission}; +use hal_flash::BlockingFlash; +use hal_flash_driver::FlashAddress; +use pw_status::Error; +use services_flash_server::FlashIpcServer; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; +use userspace::time::Instant; +use userspace::{entry, syscall}; +use util_error::ErrorCode; +use util_ipc::IpcHandle; +use util_types::Blocking; + +// EFlash Interrupt Blocker +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + if let Ok(w) = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) { + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn run_server() -> Result<(), ErrorCode> { + // 1. Initialize EFlash driver. + pw_log::info!("combined_server: initializing EFlash driver"); + // SAFETY: We have exclusive access to FlashCtrl in this test process. + let mut eflash_driver = + EmbeddedFlash::new_with_interrupts(unsafe { flash_ctrl_core::FlashCtrl::new() }); + eflash_driver.set_default_permission(Permission::FULL_ACCESS); + // Grant info page permissions as well (same as standard eflash server) + for i in 5..9 { + eflash_driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + eflash_driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + + let eflash = BlockingFlash { + driver: eflash_driver, + blocking: FlashCtrlInterrupt, + }; + let mut eflash_server = FlashIpcServer::new(eflash); + + // 2. Initialize SPI Host. + pw_log::info!("combined_server: initializing SPI Host"); + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + pw_log::error!( + "combined_server: SPI Host init failed: 0x{:x}", + u32::from(ErrorCode::from(e)) + ); + return Err(ErrorCode::from(e)); + } + + // 3. Initialize SpiFlash driver. + pw_log::info!("combined_server: initializing SpiFlash driver"); + let mut spi_flash = SpiFlash::new(spi_host); + if let Err(e) = spi_flash.init() { + pw_log::error!( + "combined_server: SPI Flash init failed: 0x{:x}", + u32::from(e) + ); + return Err(e); + } + let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + // 4. Register wait group ports. + pw_log::info!("combined_server: registering wait group ports"); + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::EFLASH_SERVICE, + syscall::Signals::READABLE, + handle::EFLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_SERVICE, + syscall::Signals::READABLE, + handle::SPI_FLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + let mut buf = [0u8; 2064]; + let eflash_ipc = IpcHandle::new(handle::EFLASH_SERVICE); + let spi_flash_ipc = IpcHandle::new(handle::SPI_FLASH_SERVICE); + + // 5. Enter main wait_group loop. + pw_log::info!("combined_server: entering main wait_group loop"); + loop { + let wait_result = syscall::object_wait( + handle::FLASH_WAIT_GROUP, + syscall::Signals::READABLE, + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + + let token = wait_result.user_data; + if token == handle::EFLASH_SERVICE as usize { + eflash_server.handle_one(&eflash_ipc, &mut buf)?; + } else if token == handle::SPI_FLASH_SERVICE as usize { + spi_flash_server.handle_one(&spi_flash_ipc, &mut buf)?; + } + } +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 COMBINED FLASH SERVER START"); + let ret = run_server(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ COMBINED FLASH SERVER PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ COMBINED FLASH SERVER FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/spi_flash_test.rs b/target/earlgrey/tests/spi_flash/spi_flash_test.rs new file mode 100644 index 00000000..49a2edb8 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/spi_flash_test.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use pw_status::Error; +use spi_flash_test_codegen::handle; +use userspace::entry; + +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::{ErrorCode, KERNEL_ERROR_INTERNAL}; +use util_ipc::IpcHandle; + +fn erase_program_test( + flash: &mut FlashIpcClient, + addr: FlashAddress, + flash_type: &str, +) -> Result<(), ErrorCode> { + let (_total_size, page_size, _erasable_sizes_bitmap) = flash.geometry()?; + pw_log::info!( + "[{}] Erasing at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.erase(addr, page_size)?; + + pw_log::info!("[{}] Reading after erase...", flash_type); + let mut buf = [0u8; 32]; + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + for &byte in buf.iter() { + if byte != 0xFF { + pw_log::error!( + "[{}] Erase check failed: byte is 0x{:02x}, expected 0xFF", + flash_type, + byte + ); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + let payload = b"Dual Flash IPC Test Payload!!! "; // 32 bytes (aligned) + pw_log::info!( + "[{}] Programming 32 bytes at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.program(addr, payload)?; + + pw_log::info!("[{}] Reading back program results...", flash_type); + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + + if buf != *payload { + pw_log::error!("[{}] Verify failed: content mismatch", flash_type); + return Err(KERNEL_ERROR_INTERNAL); + } + pw_log::info!("[{}] Program verified successfully!", flash_type); + Ok(()) +} + +fn flash_test() -> Result<(), ErrorCode> { + pw_log::info!("--- Testing Internal EFlash ---"); + let mut eflash = FlashIpcClient::new(IpcHandle::new(handle::EFLASH_SERVICE))?; + let (total_size, page_size, _) = eflash.geometry()?; + pw_log::info!( + "EFlash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on Slot B area (offset 0x90000) + erase_program_test(&mut eflash, FlashAddress::data(0x0009_0000), "EFlash")?; + + pw_log::info!("--- Testing External SPI Flash ---"); + let mut spi_flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_SERVICE))?; + let (total_size, page_size, _) = spi_flash.geometry()?; + pw_log::info!( + "SPI Flash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on 1MB offset + erase_program_test(&mut spi_flash, FlashAddress::new(0x0010_0000), "SpiFlash")?; + + Ok(()) +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 DUAL FLASH TEST CLIENT START"); + let ret = flash_test(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ DUAL FLASH TEST CLIENT PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ DUAL FLASH TEST CLIENT FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/system.json5 b/target/earlgrey/tests/spi_flash/system.json5 new file mode 100644 index 00000000..e89a685b --- /dev/null +++ b/target/earlgrey/tests/spi_flash/system.json5 @@ -0,0 +1,98 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "combined_flash_server", + flash_size_bytes: 32768, + processes: [ + { + name: "combined_flash_server", + ram_size_bytes: 8192, + objects: [ + { + name: "eflash_service", + type: "channel_handler", + }, + { + name: "spi_flash_service", + type: "channel_handler", + }, + { + name: "flash_wait_group", + type: "wait_group", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + { name: "flash_ctrl_op_done", number: 164 }, + ], + }, + { + name: "server_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + }, + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + } + ], + }, + ], + }, + + { + name: "spi_flash_test", + flash_size_bytes: 16384, + processes: [ + { + name: "spi_flash_test", + ram_size_bytes: 8192, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_flash_service", + }, + { + name: "eflash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "eflash_service", + }, + { + name: "spi_test_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + }, + ], + }, + ], +} diff --git a/target/earlgrey/tests/spi_flash/target.rs b/target/earlgrey/tests/spi_flash/target.rs new file mode 100644 index 00000000..ec71c87e --- /dev/null +++ b/target/earlgrey/tests/spi_flash/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey SPI Flash test"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); From 052e9bed435ed2b22239cc6739ca08bd62cc566e Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Thu, 16 Jul 2026 06:02:53 +0800 Subject: [PATCH 03/13] transport: Support external SPI flash in flash_server - Update system.json5 to split the original flash_service into eflash_service and a new spi_flash_service. - Map the spi_host0 memory region (0x40300000) into the flash_server process. - Implement a wait_group in flash_server.rs to handle incoming requests for both eflash_service and spi_flash_service. - Initialize the SPI Host and SPI Flash driver in flash_server.rs, logging errors using zfmt diagnostics. - Update usbmgr to connect to the renamed eflash_service and initialize the new spi_flash_usb initiator. - Add spi_flash and spi_host dependencies to the flash_server target in BUILD.bazel. Signed-off-by: Anthony Chen --- .../earlgrey/firmware/transport/BUILD.bazel | 5 ++ .../firmware/transport/flash_server.rs | 77 ++++++++++++++++--- .../earlgrey/firmware/transport/system.json5 | 24 +++++- target/earlgrey/firmware/transport/usbmgr.rs | 1 + 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/target/earlgrey/firmware/transport/BUILD.bazel b/target/earlgrey/firmware/transport/BUILD.bazel index c74631c7..04a09e08 100644 --- a/target/earlgrey/firmware/transport/BUILD.bazel +++ b/target/earlgrey/firmware/transport/BUILD.bazel @@ -85,10 +85,14 @@ rust_process( 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", @@ -96,6 +100,7 @@ rust_process( "//util/zfmt", "@pigweed//pw_kernel/userspace", "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", "@zfmt//zfmt", ], ) diff --git a/target/earlgrey/firmware/transport/flash_server.rs b/target/earlgrey/firmware/transport/flash_server.rs index 0918274c..149ccc43 100644 --- a/target/earlgrey/firmware/transport/flash_server.rs +++ b/target/earlgrey/firmware/transport/flash_server.rs @@ -10,14 +10,29 @@ use userspace::time::Instant; use userspace::{process_entry, syscall}; use util_error::{AsStatus, ErrorCode}; use util_zfmt::messages::{ProcessExit, ProcessStart}; +use zfmt::Zfmt; use earlgrey_util::EarlgreyFlashAddress; use eflash_driver::{EmbeddedFlash, Permission}; use hal_flash::{BlockingFlash, FlashAddress}; use services_flash_server::FlashIpcServer; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; use util_ipc::IpcHandle; use util_types::Blocking; +#[derive(Zfmt)] +#[zfmt(format = "SPI Host init failed: {code:08x}")] +struct SpiHostInitFailed { + code: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "SPI Flash init failed: {code:08x}")] +struct SpiFlashInitFailed { + code: u32, +} + struct FlashCtrlInterrupt; impl Blocking for FlashCtrlInterrupt { @@ -38,28 +53,68 @@ impl Blocking for FlashCtrlInterrupt { } fn flash_server() -> Result<(), ErrorCode> { - let mut driver = + let mut eflash_driver = EmbeddedFlash::new_with_interrupts(unsafe { flash_ctrl_core::FlashCtrl::new() }); - driver.set_default_permission(Permission::FULL_ACCESS); + eflash_driver.set_default_permission(Permission::FULL_ACCESS); for i in 5..9 { - driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; - driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + 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 flash = BlockingFlash { - driver, + let eflash = BlockingFlash { + driver: eflash_driver, blocking: FlashCtrlInterrupt, }; - let mut flash_server = FlashIpcServer::new(flash); + let mut eflash_server = FlashIpcServer::new(eflash); + + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + let code = u32::from(ErrorCode::from(e)); + util_zfmt::error!(SpiHostInitFailed { code }); + return Err(ErrorCode::from(e)); + } + + let mut spi_flash = SpiFlash::new(spi_host); + if let Err(e) = spi_flash.init() { + util_zfmt::error!(SpiFlashInitFailed { code: u32::from(e) }); + return Err(e); + } + let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + 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 ipc = IpcHandle::new(handle::FLASH_SERVICE); + let eflash_ipc = IpcHandle::new(handle::EFLASH_SERVICE); + let spi_flash_ipc = IpcHandle::new(handle::SPI_FLASH_SERVICE); + loop { - syscall::object_wait( - handle::FLASH_SERVICE, + let wait_result = syscall::object_wait( + handle::FLASH_WAIT_GROUP, syscall::Signals::READABLE, Instant::MAX, ) .map_err(ErrorCode::kernel_error)?; - flash_server.handle_one(&ipc, &mut buf)?; + + 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)?; + } } } diff --git a/target/earlgrey/firmware/transport/system.json5 b/target/earlgrey/firmware/transport/system.json5 index 5373985c..a4459ed6 100644 --- a/target/earlgrey/firmware/transport/system.json5 +++ b/target/earlgrey/firmware/transport/system.json5 @@ -142,9 +142,17 @@ handler_object_name: "logger_flash" }, { - name: "flash_service", + 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", @@ -169,6 +177,12 @@ type: "device", start_address: 0x41000000, size_bytes: 0x200 + }, + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000 } ] }, @@ -215,7 +229,13 @@ name: "flash_usb", type: "channel_initiator", handler_process: "flash_server", - handler_object_name: "flash_service" + handler_object_name: "eflash_service" + }, + { + name: "spi_flash_usb", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "spi_flash_service" }, { name: "sysmgr_usb", diff --git a/target/earlgrey/firmware/transport/usbmgr.rs b/target/earlgrey/firmware/transport/usbmgr.rs index bc76db32..91119d4b 100644 --- a/target/earlgrey/firmware/transport/usbmgr.rs +++ b/target/earlgrey/firmware/transport/usbmgr.rs @@ -229,6 +229,7 @@ fn handle_usb() -> Result<(), ErrorCode> { const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_BUILDER.eps().0, &CDC_BUILDER.eps().1); let flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_USB))?; + let _spi_flash = FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH_USB))?; let dfu_handler = EarlgreyDfuHandler::new(flash, sysmgr, &boot_info)?; let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, dfu_handler); From 04c5bd85533c88d56377b2a42f38d9c07705c504 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Thu, 16 Jul 2026 07:08:28 +0800 Subject: [PATCH 04/13] transport: Add DFU Alt 5 (SPI EEPROM 0) read/write support - Increase DFU alt settings to 6 and register Alt 5 interface in usbmgr. - Pass the external spi_flash client from usbmgr to EarlgreyDfuHandler. - Update dnload() in EarlgreyDfuHandler to query SPI flash geometry, align erase requests using driver page size, and program blocks to external SPI flash. - Update upload() to read chunks from external SPI flash. - Update manifest() to bypass manifestation reboot for Alt 5 download. Signed-off-by: Anthony Chen --- .../earlgrey/firmware/transport/BUILD.bazel | 1 + target/earlgrey/firmware/transport/dfu.rs | 68 ++++++++++++++++++- target/earlgrey/firmware/transport/usbmgr.rs | 33 ++++++--- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/target/earlgrey/firmware/transport/BUILD.bazel b/target/earlgrey/firmware/transport/BUILD.bazel index 04a09e08..26ed19c0 100644 --- a/target/earlgrey/firmware/transport/BUILD.bazel +++ b/target/earlgrey/firmware/transport/BUILD.bazel @@ -65,6 +65,7 @@ rust_process( "//target/earlgrey/util", "//util/error", "//util/ipc", + "//util/types", "//util/zfmt", "@pigweed//pw_kernel/userspace", "@pigweed//pw_status/rust:pw_status", diff --git a/target/earlgrey/firmware/transport/dfu.rs b/target/earlgrey/firmware/transport/dfu.rs index 5fcee7fe..f40b4d15 100644 --- a/target/earlgrey/firmware/transport/dfu.rs +++ b/target/earlgrey/firmware/transport/dfu.rs @@ -111,6 +111,13 @@ pub const DFU_CDI0_CERT: hal_usb::StringDescriptorRef = pub const DFU_CDI1_CERT: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDI1 Certificate").as_ref(); +pub const DFU_ALT_FIRMWARE: u8 = 0; +pub const DFU_ALT_UDS_CERT: u8 = 1; +pub const DFU_ALT_CDI0_CERT: u8 = 2; +pub const DFU_ALT_CDI1_CERT: u8 = 3; +pub const DFU_ALT_RESERVED: u8 = 4; // Access to OWNER_PAGE_1 +pub const DFU_ALT_SPI_EEPROM0: u8 = 5; + /// Retrieves a certificate from the info partition in flash. /// /// # Arguments @@ -247,21 +254,26 @@ impl FwUpdate { /// DFU handler for Earlgrey, managing firmware updates and certificate uploads. pub struct EarlgreyDfuHandler { flash: FlashIpcClient, + spi_flash: FlashIpcClient, sysmgr: SysmgrClient, update: FwUpdate, + alt_setting: Option, } impl EarlgreyDfuHandler { /// Creates a new DFU handler. pub fn new( flash: FlashIpcClient, + spi_flash: FlashIpcClient, sysmgr: SysmgrClient, info: &BootInfo, ) -> Result { Ok(EarlgreyDfuHandler { flash, + spi_flash, sysmgr, update: FwUpdate::new(info)?, + alt_setting: None, }) } @@ -394,8 +406,48 @@ impl EarlgreyDfuHandler { } Ok(()) } -} + fn flash_spi_eeprom0_block(&mut self, block_num: u32, data: &[u8]) -> Result<(), DfuStatus> { + let (total_size, page_size, _) = self + .spi_flash + .geometry() + .map_err(|_| DfuStatus::ErrUnknown)?; + let address = block_num * FLASH_BLOCK_SIZE as u32; + if address >= total_size.get() as u32 { + return Err(DfuStatus::ErrAddress); + } + if (address as usize) % page_size.get() == 0 { + self.spi_flash + .erase(FlashAddress::new(address), page_size) + .map_err(|_| DfuStatus::ErrErase)?; + } + self.spi_flash + .program(FlashAddress::new(address), data) + .map_err(|_| DfuStatus::ErrProg)?; + Ok(()) + } + + fn read_spi_eeprom0_block( + &mut self, + block_num: u32, + data: &mut [u8], + ) -> Result { + let (total_size, _, _) = self + .spi_flash + .geometry() + .map_err(|_| DfuStatus::ErrUnknown)?; + let address = block_num * FLASH_BLOCK_SIZE as u32; + let total_bytes = total_size.get() as u32; + if address >= total_bytes { + return Ok(0); + } + let read_len = core::cmp::min(data.len(), (total_bytes - address) as usize); + self.spi_flash + .read(FlashAddress::new(address), &mut data[..read_len]) + .map_err(|_| DfuStatus::ErrUnknown)?; + Ok(read_len) + } +} impl DfuHandler for EarlgreyDfuHandler { /// Handles a DFU download (DNLOAD) request. /// @@ -407,8 +459,11 @@ impl DfuHandler for EarlgreyDfuHandler { block: block_num, len: data.len() as u32, }); - if alt == 0 { + self.alt_setting = Some(alt); + if alt == DFU_ALT_FIRMWARE { self.flash_fw_block(block_num as u32, data) + } else if alt == DFU_ALT_SPI_EEPROM0 { + self.flash_spi_eeprom0_block(block_num as u32, data) } else { Err(DfuStatus::ErrFile) } @@ -424,8 +479,12 @@ impl DfuHandler for EarlgreyDfuHandler { block: block_num, len: data.len() as u32, }); + self.alt_setting = Some(alt); match alt { - 1 | 2 | 3 => get_certificate(&mut self.flash, alt - 1, data), + DFU_ALT_UDS_CERT | DFU_ALT_CDI0_CERT | DFU_ALT_CDI1_CERT => { + get_certificate(&mut self.flash, alt - DFU_ALT_UDS_CERT, data) + } + DFU_ALT_SPI_EEPROM0 => self.read_spi_eeprom0_block(block_num as u32, data), _ => Err(DfuStatus::ErrFile), } } @@ -436,6 +495,9 @@ impl DfuHandler for EarlgreyDfuHandler { /// slot and requests a reboot. fn manifest(&mut self) -> Result<(), DfuStatus> { util_zfmt::info!(DfuManifest); + if self.alt_setting == Some(DFU_ALT_SPI_EEPROM0) { + return Ok(()); + } if self.update.state == FwUpdateState::Done || self.update.state == FwUpdateState::Application || self.update.state == FwUpdateState::RomExt diff --git a/target/earlgrey/firmware/transport/usbmgr.rs b/target/earlgrey/firmware/transport/usbmgr.rs index 91119d4b..2acefbf8 100644 --- a/target/earlgrey/firmware/transport/usbmgr.rs +++ b/target/earlgrey/firmware/transport/usbmgr.rs @@ -29,7 +29,11 @@ use usb_driver::UsbConfig; use usb_stack::{DescriptorSource, UsbAction, UsbClass}; mod dfu; -use dfu::{EarlgreyDfuHandler, DFU_CDI0_CERT, DFU_CDI1_CERT, DFU_FIRMWARE, DFU_UDS_CERT}; +use dfu::{ + EarlgreyDfuHandler, DFU_ALT_CDI0_CERT, DFU_ALT_CDI1_CERT, DFU_ALT_FIRMWARE, DFU_ALT_RESERVED, + DFU_ALT_SPI_EEPROM0, DFU_ALT_UDS_CERT, DFU_CDI0_CERT, DFU_CDI1_CERT, DFU_FIRMWARE, + DFU_UDS_CERT, +}; use earlgrey_sysmgr_client::SysmgrClient; use protocol_usb_cdc_acm::{CdcAcm, CdcAcmBuilder}; use protocol_usb_dfu::{DfuBuilder, DfuClass}; @@ -50,6 +54,8 @@ const DFU_FIRMWARE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(6); const DFU_UDS_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(7); const DFU_CDI0_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(8); const DFU_CDI1_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(9); +const DFU_RESERVED_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(10); +const DFU_SPI_EEPROM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(11); // The serial number size is 2 bytes (USB descriptor header) + 32 bytes of // serial number * (2 for hex encoding) * (2 bytes per UTF16 character). @@ -57,7 +63,7 @@ const USB_SERIAL_SIZE: usize = 2 + 32 * 2 * 2; const DFU_BUILDER: DfuBuilder = DfuBuilder::new( 2, // interface_num (2, after CDC-ACM's 0 and 1) - 4, // alt_settings + 6, // alt_settings 2048, // transfer_size ); @@ -94,12 +100,14 @@ const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { &CDC_BUILDER.comm_endpoints(), ), CDC_BUILDER.data_interface(USB_CDC_DATA_HANDLE, &CDC_BUILDER.data_endpoints()), - DFU_BUILDER.interface(0, DFU_FIRMWARE_HANDLE, &[]), - DFU_BUILDER.interface(1, DFU_UDS_CERT_HANDLE, &[]), - DFU_BUILDER.interface(2, DFU_CDI0_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_FIRMWARE, DFU_FIRMWARE_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_UDS_CERT, DFU_UDS_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_CDI0_CERT, DFU_CDI0_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_CDI1_CERT, DFU_CDI1_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_RESERVED, DFU_RESERVED_HANDLE, &[]), DFU_BUILDER.interface( - 3, - DFU_CDI1_CERT_HANDLE, + DFU_ALT_SPI_EEPROM0, + DFU_SPI_EEPROM_HANDLE, &[DFU_BUILDER.functional_descriptor()], ), ], @@ -119,6 +127,9 @@ const USB_COMM: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDC Comm Interface").as_ref(); const USB_DATA: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("CDC Data Interface").as_ref(); +const DFU_RESERVED: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Reserved").as_ref(); +const DFU_SPI_EEPROM: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("SPI EEPROM 0").as_ref(); /// Implements `DescriptorSource` to provide USB descriptors. /// @@ -160,6 +171,10 @@ impl DescriptorSource for MyDescriptors<'_> { Some(DFU_CDI0_CERT) } else if h == DFU_CDI1_CERT_HANDLE.0 { Some(DFU_CDI1_CERT) + } else if h == DFU_RESERVED_HANDLE.0 { + Some(DFU_RESERVED) + } else if h == DFU_SPI_EEPROM_HANDLE.0 { + Some(DFU_SPI_EEPROM) } else { None } @@ -229,8 +244,8 @@ fn handle_usb() -> Result<(), ErrorCode> { const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_BUILDER.eps().0, &CDC_BUILDER.eps().1); let flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_USB))?; - let _spi_flash = FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH_USB))?; - let dfu_handler = EarlgreyDfuHandler::new(flash, sysmgr, &boot_info)?; + let spi_flash = FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH_USB))?; + let dfu_handler = EarlgreyDfuHandler::new(flash, spi_flash, sysmgr, &boot_info)?; let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, dfu_handler); let mut usb = usb_driver::Usb::new(unsafe { usbdev::Usbdev::new() }, USB_CONFIG); From 0b496e4b988130bd1e51d9174bf711d29d49b1a5 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Fri, 17 Jul 2026 01:18:10 +0800 Subject: [PATCH 05/13] dfu: Fix DFU upload ZLP hanging Only send ZLP in DfuClass::poll if the total transfer size is less than the DFU wTransferSize (2048 bytes), avoiding a redundant ZLP when returning a full block. Signed-off-by: Anthony Chen --- protocol/usb/dfu/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/usb/dfu/lib.rs b/protocol/usb/dfu/lib.rs index 3958d0a6..ba6ed914 100644 --- a/protocol/usb/dfu/lib.rs +++ b/protocol/usb/dfu/lib.rs @@ -212,9 +212,9 @@ where return; } if let Some(data) = self.buffer.get(self.transfer_offset..self.transfer_total) { - let n = driver.transfer_in_unaligned(0, data, true); + let zlp = self.transfer_total < self.config.transfer_size as usize; + let n = driver.transfer_in_unaligned(0, data, zlp); self.transfer_offset += n; - if self.transfer_offset == self.transfer_total { if self.transfer_total < self.config.transfer_size as usize { self.state = DfuState::DfuIdle; From 78b5ee752451b008300267142415115acbd8ad83 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Thu, 16 Jul 2026 07:10:25 +0800 Subject: [PATCH 06/13] transport: Add host-side E2E test for DFU Alt 5 SPI flash read/write - Implement host_usb_dfu_spi_flash.rs to download a payload to Alt 5 (SPI EEPROM 0) and upload it back for verification. - Add host_usb_dfu_spi_flash to target dependencies and define dfu_spi_flash_hyper{310,340}_test target in BUILD.bazel. Signed-off-by: Anthony Chen --- .../firmware/transport/tests/dfu/BUILD.bazel | 56 +++++++ .../tests/dfu/host_usb_dfu_spi_flash.rs | 151 ++++++++++++++++++ target/earlgrey/testutil/lib.rs | 40 +++++ 3 files changed, 247 insertions(+) create mode 100644 target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs diff --git a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel index 354b9cc6..93929236 100644 --- a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel +++ b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel @@ -101,3 +101,59 @@ opentitan_test( test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", test_harness = ":host_usb_dfu_owner_transfer", ) + +opentitan_rust_binary( + name = "host_usb_dfu_spi_flash", + srcs = ["host_usb_dfu_spi_flash.rs"], + edition = "2024", + rustc_flags = [ + "-C", + "link-arg=-Wl,--allow-shlib-undefined", + ], + deps = [ + "//target/earlgrey/testutil", + "//third_party/lowrisc_opentitan:opentitanlib", + "//third_party/lowrisc_opentitan:usb_test_helper", + "@ot_crate_index//:anyhow", + "@ot_crate_index//:clap", + "@ot_crate_index//:log", + ], +) + +opentitan_test( + name = "dfu_spi_flash_hyper310_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = "//target/earlgrey/firmware/transport:transport_firmware", + target_data = [ + ":bootinfo_signed_simple", + ], + test_cmd = "--logging=info --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_harness = ":host_usb_dfu_spi_flash", +) + +opentitan_test( + name = "dfu_spi_flash_hyper340_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = "//target/earlgrey/firmware/transport:transport_firmware", + target_data = [ + ":bootinfo_signed_simple", + ], + test_cmd = "--logging=info --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_harness = ":host_usb_dfu_spi_flash", +) diff --git a/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs b/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs new file mode 100644 index 00000000..76addea8 --- /dev/null +++ b/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs @@ -0,0 +1,151 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, Context, Result}; +use clap::Parser; +use std::time::Duration; + +use earlgrey_testutil::{ + get_dfu_transfer_size, print_uart, sequence_dfu_download, sequence_dfu_upload, DfuClient, +}; +use opentitanlib::app::TransportWrapper; +use opentitanlib::io::uart::Uart; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; +use usb::UsbOpts; + +#[derive(Parser, Debug)] +struct CmdArgs { + #[command(flatten)] + init: InitializeTest, + + #[command(flatten)] + usb: UsbOpts, + + #[arg( + long, + default_value = "target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin" + )] + firmware: String, +} + +fn run_dfu_spi_flash_test_inner( + transport: &TransportWrapper, + usb: &UsbOpts, + firmware_path: &str, + uart: &dyn Uart, +) -> Result<()> { + log::info!("Resetting target..."); + transport.reset(opentitanlib::app::UartRx::Clear)?; + + log::info!("waiting for Maize Welcome on console..."); + let _ = UartConsole::wait_for( + uart, + r"Welcome to Maize on Earlgrey Transport Firmware!", + Duration::from_secs(10), + )?; + + usb.apply_strappings(transport, true)?; + if usb.vbus_control_available() { + usb.enable_vbus(transport, true)?; + } + if usb.vbus_sense_available() { + if !usb.vbus_present(transport)? { + bail!("OT USB does not appear to be connected to a host (VBUS not detected)"); + } + } + + let usb_vid = usb.vid; + let usb_pid = usb.pid; + + log::info!( + "waiting for DFU device (VID={:04x}, PID={:04x})...", + usb_vid, + usb_pid + ); + let device = transport + .usb()? + .device_by_id_with_timeout(usb_vid, usb_pid, None, Duration::from_secs(10)) + .context("DFU device not found")?; + + log::info!("Claiming DFU interface..."); + let interface_num = 2; + device.claim_interface(interface_num)?; + + let transfer_size = get_dfu_transfer_size(&*device, interface_num)?; + log::info!("DFU Transfer Size (Block Size): {} bytes", transfer_size); + + // Set Alt setting 5 (SPI EEPROM 0) + log::info!("Setting USB DFU Alt setting to 5 (SPI EEPROM 0)..."); + device.set_alternate_setting(interface_num, 5)?; + + let dfu = DfuClient::new(&*device, interface_num); + + log::info!("Reading payload from '{}'...", firmware_path); + let test_data = std::fs::read(firmware_path)?; + + log::info!("Sequencing DFU Download (expect_reboot = false)..."); + sequence_dfu_download(&dfu, uart, &test_data, transfer_size, false)?; + + log::info!("Sequencing DFU Upload to read back payload..."); + let uploaded_data = sequence_dfu_upload(&dfu, test_data.len(), transfer_size)?; + + log::info!("Verifying integrity of uploaded data..."); + if uploaded_data != test_data { + log::error!("Data mismatch!"); + log::error!( + "Original len: {}, Uploaded len: {}", + test_data.len(), + uploaded_data.len() + ); + log::error!( + "Original (first 16 bytes): {:02x?}", + &test_data[..std::cmp::min(16, test_data.len())] + ); + log::error!( + "Uploaded (first 16 bytes): {:02x?}", + &uploaded_data[..std::cmp::min(16, uploaded_data.len())] + ); + if let Some(mismatch_idx) = test_data + .iter() + .zip(uploaded_data.iter()) + .position(|(a, b)| a != b) + { + log::error!( + "First mismatch at index {}: expected {:02x}, got {:02x}", + mismatch_idx, + test_data[mismatch_idx], + uploaded_data[mismatch_idx] + ); + } else { + log::error!("No mismatch found within zipped range (vectors have different lengths)."); + } + let _ = device.release_interface(interface_num); + bail!("Data mismatch! Uploaded data does not match the downloaded payload."); + } + log::info!("✅ Integrity verification passed (hashes/bytes match)!"); + + let _ = device.release_interface(interface_num); + log::info!("Test Execution Finished Successfully!"); + Ok(()) +} + +fn run_dfu_spi_flash_test( + transport: &TransportWrapper, + usb: &UsbOpts, + firmware_path: &str, +) -> Result<()> { + let uart = transport.uart("console")?; + let res = run_dfu_spi_flash_test_inner(transport, usb, firmware_path, &*uart); + print_uart(&*uart); + res +} + +fn main() -> Result<()> { + let args = CmdArgs::parse(); + args.init.init_logging(); + + let transport = args.init.init_target()?; + run_dfu_spi_flash_test(&transport, &args.usb, &args.firmware)?; + Ok(()) +} diff --git a/target/earlgrey/testutil/lib.rs b/target/earlgrey/testutil/lib.rs index 95a0d155..26e82ba6 100644 --- a/target/earlgrey/testutil/lib.rs +++ b/target/earlgrey/testutil/lib.rs @@ -165,3 +165,43 @@ pub fn sequence_dfu_download( // Removed print_uart(uart) to preserve telemetry for the test harness. Ok(()) } + +pub fn sequence_dfu_upload( + dfu: &DfuClient, + expected_len: usize, + transfer_size: u16, +) -> Result> { + // Ensure we start from a clean state + let status = dfu.get_status()?; + if status.state() == DfuState::Error { + log::info!("Clearing DFU error status..."); + dfu.clear_status()?; + } + + let mut uploaded_data = Vec::new(); + let mut block_num = 0; + let mut buf = vec![0u8; transfer_size as usize]; + while uploaded_data.len() < expected_len { + let n = dfu.upload(block_num, &mut buf)?; + log::info!("Uploaded block {block_num}, size {n}..."); + if n == 0 { + log::warn!("Upload returned 0 bytes early at block {block_num}"); + break; + } + let chunk_len = std::cmp::min(n, expected_len - uploaded_data.len()); + uploaded_data.extend_from_slice(&buf[..chunk_len]); + block_num += 1; + } + + let status = dfu.get_status()?; + if status.state() == DfuState::UpLoadIdle { + dfu.abort()?; + } else if status.state() != DfuState::Idle { + bail!( + "DFU upload finished in unexpected state: {:?}", + status.state() + ); + } + + Ok(uploaded_data) +} From 21b554598bb61562d4e8f4f08b5bca7f9f6a5a73 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Fri, 10 Jul 2026 08:18:19 +0800 Subject: [PATCH 07/13] util: extract FwUpdate tracker to independent library target Move the DFU/A-B staging slot tracking helper `FwUpdate` from `target/earlgrey/util/dfu.rs` to a standalone library target `target/earlgrey/util/fw_update.rs`. This decouples the firmware slot tracking logic from specific sysmgr updater server implementations, allowing it to be reused directly by other userspace processes (such as the new `updatemgr` transport process). Signed-off-by: Anthony Chen --- target/earlgrey/util/BUILD.bazel | 15 ++++++ target/earlgrey/util/dfu.rs | 80 +-------------------------- target/earlgrey/util/fw_update.rs | 90 +++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 target/earlgrey/util/fw_update.rs diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel index a3011502..c4fb6cff 100644 --- a/target/earlgrey/util/BUILD.bazel +++ b/target/earlgrey/util/BUILD.bazel @@ -44,6 +44,20 @@ rust_test( crate = ":util", ) +rust_library( + name = "fw_update", + srcs = ["fw_update.rs"], + crate_name = "earlgrey_fw_update", + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + ":util", + "//target/earlgrey/services/sysmgr:client", + "//util/error", + ], +) + rust_library( name = "dfu", srcs = ["dfu.rs"], @@ -53,6 +67,7 @@ rust_library( visibility = ["//visibility:public"], deps = [ ":util", + ":fw_update", "//hal/blocking/flash", "//hal/blocking/usb:hal_usb", "//protocol/usb/dfu", diff --git a/target/earlgrey/util/dfu.rs b/target/earlgrey/util/dfu.rs index f9807380..e39acd50 100644 --- a/target/earlgrey/util/dfu.rs +++ b/target/earlgrey/util/dfu.rs @@ -12,6 +12,7 @@ use earlgrey_sysmgr_client::{BootInfo, SysmgrClient}; use earlgrey_util::tags::BootSlot; use earlgrey_util::tags::ManifestIdentifier; use earlgrey_util::EarlgreyFlashAddress; +use earlgrey_fw_update::{FwUpdate, FwUpdateState}; use earlgrey_util::PersoCertificate; use hal_flash::{Flash, FlashAddress}; use services_flash_client::FlashIpcClient; @@ -114,85 +115,6 @@ fn get_certificate(flash: &mut FlashIpcClient, n: u8, data: &mut [u8]) -> Result Err(DfuStatus::ErrUnknown) } -/// State of the firmware update process. -#[derive(Clone, Copy, PartialEq, Eq)] -enum FwUpdateState { - /// Idle, waiting for the first block of firmware. - Idle, - /// Flashing ROM_EXT. - RomExt, - /// Flashing Application. - Application, - /// Firmware download complete. - Done, -} - -/// Helper struct to track the progress and target partitions for a firmware update. -/// -/// It uses an A/B partitioning scheme, targeting the inactive slot. -struct FwUpdate { - /// Current state of the update process. - state: FwUpdateState, - /// Next expected block number that triggers a partition erase. - next_erase: u32, - /// The block number where the current image (ROM_EXT or App) download started. - start_block: u32, - /// Target boot slot for ROM_EXT. - _rom_ext: BootSlot, - /// Start address of target ROM_EXT partition in flash. - rom_ext_start: u32, - /// End address of target ROM_EXT partition in flash. - rom_ext_end: u32, - /// Target boot slot for Application. - app: BootSlot, - /// Start address of target Application partition in flash. - app_start: u32, - /// End address of target Application partition in flash. - app_end: u32, -} - -impl FwUpdate { - /// Creates a new `FwUpdate` tracker. - /// - /// It queries the current boot info to determine the active slots, - /// and targets the *opposite* (inactive) slots for the update. - fn new(info: &BootInfo) -> Result { - let rom_ext = info - .rom_ext - .boot_slot - .opposite() - .ok_or(earlgrey_util::error::EG_ERROR_BOOT_SLOT_UNKNOWN)?; - let rom_ext_start = FwUpdate::addr(rom_ext); - let app = info - .app - .boot_slot - .opposite() - .ok_or(earlgrey_util::error::EG_ERROR_BOOT_SLOT_UNKNOWN)?; - let app_start = FwUpdate::addr(app) + info.rom_ext.size; - - Ok(FwUpdate { - state: FwUpdateState::Idle, - next_erase: 0, - start_block: 0, - _rom_ext: rom_ext, - rom_ext_start, - rom_ext_end: rom_ext_start + info.rom_ext.size, - app, - app_start, - app_end: app_start + info.app.size, - }) - } - - /// Returns the physical flash address offset for a given boot slot. - fn addr(slot: BootSlot) -> u32 { - match slot { - BootSlot::SlotA => 0, - BootSlot::SlotB => 0x80000, - _ => unreachable!(), - } - } -} - /// DFU handler for Earlgrey, managing firmware updates and certificate uploads. pub struct EarlgreyDfuHandler { flash: FlashIpcClient, diff --git a/target/earlgrey/util/fw_update.rs b/target/earlgrey/util/fw_update.rs new file mode 100644 index 00000000..05764b6d --- /dev/null +++ b/target/earlgrey/util/fw_update.rs @@ -0,0 +1,90 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Device Firmware Upgrade (DFU) tracker layout and state tracking helper. + +#![no_std] + +use earlgrey_sysmgr_client::BootInfo; +use earlgrey_util::tags::BootSlot; +use earlgrey_util::error::EG_ERROR_BOOT_SLOT_UNKNOWN; +use util_error::ErrorCode; + +/// State of the firmware update process. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum FwUpdateState { + /// Idle, waiting for the first block of firmware. + Idle, + /// Flashing ROM_EXT. + RomExt, + /// Flashing Application. + Application, + /// Firmware download complete. + Done, +} + +/// Helper struct to track the progress and target partitions for a firmware update. +/// +/// It uses an A/B partitioning scheme, targeting the inactive slot. +pub struct FwUpdate { + /// Current state of the update process. + pub state: FwUpdateState, + /// Next expected block number that triggers a partition erase. + pub next_erase: u32, + /// The block number where the current image (ROM_EXT or App) download started. + pub start_block: u32, + /// Target boot slot for ROM_EXT. + pub rom_ext: BootSlot, + /// Start address of target ROM_EXT partition in flash. + pub rom_ext_start: u32, + /// End address of target ROM_EXT partition in flash. + pub rom_ext_end: u32, + /// Target boot slot for Application. + pub app: BootSlot, + /// Start address of target Application partition in flash. + pub app_start: u32, + /// End address of target Application partition in flash. + pub app_end: u32, +} + +impl FwUpdate { + /// Creates a new `FwUpdate` tracker. + /// + /// It queries the current boot info to determine the active slots, + /// and targets the *opposite* (inactive) slots for the update. + pub fn new(info: &BootInfo) -> Result { + let rom_ext = info + .rom_ext + .boot_slot + .opposite() + .ok_or(EG_ERROR_BOOT_SLOT_UNKNOWN)?; + let rom_ext_start = FwUpdate::addr(rom_ext); + let app = info + .app + .boot_slot + .opposite() + .ok_or(EG_ERROR_BOOT_SLOT_UNKNOWN)?; + let app_start = FwUpdate::addr(app) + info.rom_ext.size; + + Ok(FwUpdate { + state: FwUpdateState::Idle, + next_erase: 0, + start_block: 0, + rom_ext, + rom_ext_start, + rom_ext_end: rom_ext_start + info.rom_ext.size, + app, + app_start, + app_end: app_start + info.app.size, + }) + } + + /// Returns the physical flash address offset for a given boot slot. + pub fn addr(slot: BootSlot) -> u32 { + match slot { + BootSlot::SlotA => 0, + BootSlot::SlotB => 0x80000, + _ => unreachable!(), + } + } +} From 405a90f40eae41fb60d62e5594df492287284f05 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Mon, 22 Jun 2026 13:54:56 +0800 Subject: [PATCH 08/13] updatemgr: Integrate external SPI flash update scanner Introduce the new `updatemgr` process under `target/earlgrey/firmware/transport/updatemgr.rs` to handle external firmware update scanning. - Implement the external firmware bundle scanner (`scan_firmware_bundle` and `try_read_bundle_at`) in `util/fw_update.rs` using the `RandomRead` trait to locate valid `ROM_EXT` and `APPLICATION` manifests on the external flash. Signed-off-by: Anthony Chen --- .../earlgrey/firmware/transport/BUILD.bazel | 32 ++++++ .../firmware/transport/flash_server.rs | 38 +++++-- target/earlgrey/firmware/transport/logmgr.rs | 6 ++ target/earlgrey/firmware/transport/sysmgr.rs | 33 ++++++- .../earlgrey/firmware/transport/system.json5 | 65 ++++++++++-- .../earlgrey/firmware/transport/updatemgr.rs | 98 +++++++++++++++++++ target/earlgrey/services/sysmgr/BUILD.bazel | 1 + target/earlgrey/util/BUILD.bazel | 4 +- target/earlgrey/util/error.rs | 9 ++ target/earlgrey/util/fw_update.rs | 89 ++++++++++++++++- 10 files changed, 352 insertions(+), 23 deletions(-) create mode 100644 target/earlgrey/firmware/transport/updatemgr.rs diff --git a/target/earlgrey/firmware/transport/BUILD.bazel b/target/earlgrey/firmware/transport/BUILD.bazel index 26ed19c0..684d5d38 100644 --- a/target/earlgrey/firmware/transport/BUILD.bazel +++ b/target/earlgrey/firmware/transport/BUILD.bazel @@ -117,9 +117,40 @@ rust_process( tags = ["kernel"], visibility = ["//visibility:public"], deps = [ + "//hal/blocking/flash", + "//services/flash:client", "//target/earlgrey/services/sysmgr:server", "//util/error", + "//util/io", "//util/ipc", + "//util/types", + "//util/zfmt", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + "@zfmt//zfmt", + ], +) + +rust_process( + name = "updatemgr", + srcs = [ + "updatemgr.rs", + ], + codegen_crate_name = "updatemgr_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/services/sysmgr:client", + "//target/earlgrey/util", + "//target/earlgrey/util:fw_update", + "//util/error", + "//util/io", + "//util/ipc", + "//util/types", "//util/zfmt", "@pigweed//pw_kernel/userspace", "@pigweed//pw_status/rust:pw_status", @@ -151,6 +182,7 @@ multi_process_app( processes = [ ":logmgr", ":sysmgr", + ":updatemgr", ":platform", ":flash_server", ":usbmgr", diff --git a/target/earlgrey/firmware/transport/flash_server.rs b/target/earlgrey/firmware/transport/flash_server.rs index 149ccc43..2ddd1411 100644 --- a/target/earlgrey/firmware/transport/flash_server.rs +++ b/target/earlgrey/firmware/transport/flash_server.rs @@ -83,23 +83,41 @@ fn flash_server() -> Result<(), ErrorCode> { syscall::wait_group_add( handle::FLASH_WAIT_GROUP, - handle::EFLASH_SERVICE, + handle::EFLASH_UPDATEMGR_SERVICE, syscall::Signals::READABLE, - 1, // token 1 = EFlash + 1, // token 1 = EFlash updatemgr ) .map_err(ErrorCode::kernel_error)?; syscall::wait_group_add( handle::FLASH_WAIT_GROUP, - handle::SPI_FLASH_SERVICE, + handle::EFLASH_USB_SERVICE, syscall::Signals::READABLE, - 2, // token 2 = SPI Flash + 2, // token 2 = EFlash usb + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_UPDATEMGR_SERVICE, + syscall::Signals::READABLE, + 3, // token 3 = SPI Flash updatemgr + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_USB_SERVICE, + syscall::Signals::READABLE, + 4, // token 4 = SPI Flash usb ) .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); + let eflash_updatemgr_ipc = IpcHandle::new(handle::EFLASH_UPDATEMGR_SERVICE); + let eflash_usb_ipc = IpcHandle::new(handle::EFLASH_USB_SERVICE); + let spi_flash_updatemgr_ipc = IpcHandle::new(handle::SPI_FLASH_UPDATEMGR_SERVICE); + let spi_flash_usb_ipc = IpcHandle::new(handle::SPI_FLASH_USB_SERVICE); loop { let wait_result = syscall::object_wait( @@ -111,9 +129,13 @@ fn flash_server() -> Result<(), ErrorCode> { let token = wait_result.user_data; if token == 1 { - eflash_server.handle_one(&eflash_ipc, &mut buf)?; + eflash_server.handle_one(&eflash_updatemgr_ipc, &mut buf)?; } else if token == 2 { - spi_flash_server.handle_one(&spi_flash_ipc, &mut buf)?; + eflash_server.handle_one(&eflash_usb_ipc, &mut buf)?; + } else if token == 3 { + spi_flash_server.handle_one(&spi_flash_updatemgr_ipc, &mut buf)?; + } else if token == 4 { + spi_flash_server.handle_one(&spi_flash_usb_ipc, &mut buf)?; } } } diff --git a/target/earlgrey/firmware/transport/logmgr.rs b/target/earlgrey/firmware/transport/logmgr.rs index 43d64f65..a43cf380 100644 --- a/target/earlgrey/firmware/transport/logmgr.rs +++ b/target/earlgrey/firmware/transport/logmgr.rs @@ -198,6 +198,12 @@ fn logmgr_server() -> Result<(), Error> { Signals::READABLE, handle::LOGGER_SYSMGR as usize, )?; + syscall::wait_group_add( + handle::LOGMGR_WAIT_GROUP, + handle::LOGGER_UPDATEMGR, + Signals::READABLE, + handle::LOGGER_UPDATEMGR as usize, + )?; let mut server = LogServer::<2048>::new(); let mut active_log = ActiveLog::new(); diff --git a/target/earlgrey/firmware/transport/sysmgr.rs b/target/earlgrey/firmware/transport/sysmgr.rs index 0a0317a4..55d058d8 100644 --- a/target/earlgrey/firmware/transport/sysmgr.rs +++ b/target/earlgrey/firmware/transport/sysmgr.rs @@ -17,16 +17,39 @@ use util_zfmt::messages::{ProcessExit, ProcessStart}; fn sysmgr_server() -> Result<(), ErrorCode> { // SysmgrServer::new() will read boot log from retram and log boot info. let mut server = SysmgrServer::new()?; - let service_channel = IpcHandle::new(handle::SYSMGR_SERVICE); + + syscall::wait_group_add( + handle::SYSMGR_WAIT_GROUP, + handle::SYSMGR_UPDATER_SERVICE, + Signals::READABLE, + 1, // token 1 = Updater + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::SYSMGR_WAIT_GROUP, + handle::SYSMGR_USB_SERVICE, + Signals::READABLE, + 2, // token 2 = USB + ) + .map_err(ErrorCode::kernel_error)?; + + let updater_channel = IpcHandle::new(handle::SYSMGR_UPDATER_SERVICE); + let usb_channel = IpcHandle::new(handle::SYSMGR_USB_SERVICE); let mut buf = [0u8; 1024]; loop { // Wait for incoming IPC request. - syscall::object_wait(handle::SYSMGR_SERVICE, Signals::READABLE, Instant::MAX) - .map_err(ErrorCode::kernel_error)?; + let wait_result = + syscall::object_wait(handle::SYSMGR_WAIT_GROUP, Signals::READABLE, Instant::MAX) + .map_err(ErrorCode::kernel_error)?; - // Process request. - server.handle_one(&service_channel, &mut buf)?; + let token = wait_result.user_data; + if token == 1 { + server.handle_one(&updater_channel, &mut buf)?; + } else if token == 2 { + server.handle_one(&usb_channel, &mut buf)?; + } } } diff --git a/target/earlgrey/firmware/transport/system.json5 b/target/earlgrey/firmware/transport/system.json5 index a4459ed6..9db27264 100644 --- a/target/earlgrey/firmware/transport/system.json5 +++ b/target/earlgrey/firmware/transport/system.json5 @@ -17,7 +17,7 @@ apps: [ { name: "transport", - flash_size_bytes: 49152, + flash_size_bytes: 65536, processes: [ { name: "logmgr", @@ -43,6 +43,10 @@ name: "logger_usb", type: "channel_handler" }, + { + name: "logger_updatemgr", + type: "channel_handler" + }, { name: "uart0_interrupts", type: "interrupt", @@ -82,13 +86,21 @@ handler_object_name: "logger_sysmgr" }, { - name: "sysmgr_service", + name: "sysmgr_updater_service", type: "channel_handler" }, + { + name: "sysmgr_usb_service", + type: "channel_handler" + }, + { + name: "sysmgr_wait_group", + type: "wait_group" + }, { name: "sysmgr_thread", + type: "thread", kernel_stack_size_bytes: 2048, - type: "thread" } ], memory_mappings: [ @@ -109,6 +121,35 @@ type: "device", start_address: 0x40140000, size_bytes: 0x100 + }, + ] + }, + { + name: "updatemgr", + ram_size_bytes: 8192, + objects: [ + { + name: "logger_updatemgr", + type: "channel_initiator", + handler_process: "logmgr", + handler_object_name: "logger_updatemgr" + }, + { + name: "sysmgr_updater_client", + type: "channel_initiator", + handler_process: "sysmgr", + handler_object_name: "sysmgr_updater_service" + }, + { + name: "spi_flash_updatemgr", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "spi_flash_updatemgr_service" + }, + { + name: "updatemgr_thread", + kernel_stack_size_bytes: 4096, + type: "thread" } ] }, @@ -142,11 +183,19 @@ handler_object_name: "logger_flash" }, { - name: "eflash_service", + name: "eflash_updatemgr_service", + type: "channel_handler" + }, + { + name: "eflash_usb_service", + type: "channel_handler" + }, + { + name: "spi_flash_updatemgr_service", type: "channel_handler" }, { - name: "spi_flash_service", + name: "spi_flash_usb_service", type: "channel_handler" }, { @@ -229,19 +278,19 @@ name: "flash_usb", type: "channel_initiator", handler_process: "flash_server", - handler_object_name: "eflash_service" + handler_object_name: "eflash_usb_service" }, { name: "spi_flash_usb", type: "channel_initiator", handler_process: "flash_server", - handler_object_name: "spi_flash_service" + handler_object_name: "spi_flash_usb_service" }, { name: "sysmgr_usb", type: "channel_initiator", handler_process: "sysmgr", - handler_object_name: "sysmgr_service" + handler_object_name: "sysmgr_usb_service" }, { name: "usbmgr_thread", diff --git a/target/earlgrey/firmware/transport/updatemgr.rs b/target/earlgrey/firmware/transport/updatemgr.rs new file mode 100644 index 00000000..9a20139d --- /dev/null +++ b/target/earlgrey/firmware/transport/updatemgr.rs @@ -0,0 +1,98 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use earlgrey_util::error::EG_ERROR_UPDATE_NOT_FOUND; +use hal_flash::Flash; +use pw_status::Error; +use services_flash_client::FlashIpcClient; +use updatemgr_codegen::handle; +use userspace::process_entry; +use userspace::time::{sleep_until, Instant}; +use util_error::{AsStatus, ErrorCode}; +use util_io::RandomRead; +use util_ipc::IpcHandle; +use util_zfmt::messages::{ProcessExit, ProcessStart}; +use zfmt::Zfmt; + +#[derive(Zfmt)] +#[zfmt(format = "SPI Flash detected. Size: {size} bytes")] +struct SpiFlashDetected { + size: u32, +} + +#[derive(Zfmt)] +#[zfmt( + format = "Update found! Staging slot: {staging_slot:c}, ROM_EXT offset: 0x{rom_ext_offset:x}, Owner offset: 0x{owner_offset:x}" +)] +struct UpdateTargetMapped { + staging_slot: u32, + rom_ext_offset: u32, + owner_offset: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Update manager attempt failed: 0x{status:08x}. Retrying in 1s...")] +struct UpdateAttemptFailed { + status: u32, +} + +fn try_update( + spi_flash_client: &mut FlashIpcClient, + update: &earlgrey_fw_update::FwUpdate, + work_buf: &mut [u8; 2048], +) -> Result<(), ErrorCode> { + let mut reader = spi_flash_client.random_reader(); + let size = reader.size()?; + util_zfmt::info!(SpiFlashDetected { size: size as u32 }); + + let Some(_bundle) = update.scan_firmware_bundle(&mut reader, work_buf)? else { + return Err(EG_ERROR_UPDATE_NOT_FOUND); + }; + + util_zfmt::info!(UpdateTargetMapped { + staging_slot: update.rom_ext.0, + rom_ext_offset: update.rom_ext_start, + owner_offset: update.app_start, + }); + + Ok(()) +} + +fn updatemgr_process() -> Result<(), ErrorCode> { + let sysmgr_client = + earlgrey_sysmgr_client::SysmgrClient::new(IpcHandle::new(handle::SYSMGR_UPDATER_CLIENT)); + + let info = sysmgr_client.get_boot_info()?; + let update = earlgrey_fw_update::FwUpdate::new(&info)?; + + let spi_flash_handle = IpcHandle::new(handle::SPI_FLASH_UPDATEMGR); + let mut spi_flash_client = FlashIpcClient::new(spi_flash_handle)?; + + let mut work_buf = [0u8; 2048]; + loop { + match try_update(&mut spi_flash_client, &update, &mut work_buf) { + Ok(()) => break, + Err(e) => { + util_zfmt::warn!(UpdateAttemptFailed { status: e.0.get() }); + break; + } + } + } + + let _ = sleep_until(Instant::MAX); + Ok(()) +} + +#[process_entry("updatemgr")] +fn entry() -> Result<(), Error> { + util_zfmt::info!(ProcessStart { name: "updatemgr" }); + let ret = updatemgr_process(); + util_zfmt::error!(ProcessExit { + name: "updatemgr", + status: ret.as_status() + }); + Err(Error::Unknown) +} diff --git a/target/earlgrey/services/sysmgr/BUILD.bazel b/target/earlgrey/services/sysmgr/BUILD.bazel index 45acdae0..e03878ce 100644 --- a/target/earlgrey/services/sysmgr/BUILD.bazel +++ b/target/earlgrey/services/sysmgr/BUILD.bazel @@ -37,6 +37,7 @@ rust_library( "//target/earlgrey/registers:rstmgr", "//target/earlgrey/util", "//util/error", + "//util/io", "//util/ipc", "//util/types", "//util/zfmt", diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel index c4fb6cff..70264548 100644 --- a/target/earlgrey/util/BUILD.bazel +++ b/target/earlgrey/util/BUILD.bazel @@ -55,6 +55,8 @@ rust_library( ":util", "//target/earlgrey/services/sysmgr:client", "//util/error", + "//util/io", + "@rust_crates//:zerocopy", ], ) @@ -66,8 +68,8 @@ rust_library( target_compatible_with = TARGET_COMPATIBLE_WITH, visibility = ["//visibility:public"], deps = [ - ":util", ":fw_update", + ":util", "//hal/blocking/flash", "//hal/blocking/usb:hal_usb", "//protocol/usb/dfu", diff --git a/target/earlgrey/util/error.rs b/target/earlgrey/util/error.rs index 831a46a6..0d7bada1 100644 --- a/target/earlgrey/util/error.rs +++ b/target/earlgrey/util/error.rs @@ -17,3 +17,12 @@ pub const EG_ERROR_CERT_BAD_NAME: ErrorCode = EG_ERROR.from_pw(2, Error::Invalid pub const EG_ERROR_BAD_BOOT_LOG: ErrorCode = EG_ERROR.from_pw(3, Error::Unknown); /// The boot slot is unknown or invalid. pub const EG_ERROR_BOOT_SLOT_UNKNOWN: ErrorCode = EG_ERROR.from_pw(4, Error::Unknown); + +/// Failed to configure SPI Mux control pin. +pub const EG_ERROR_SPI_MUX_CTRL_CONFIG_FAILED: ErrorCode = EG_ERROR.from_pw(5, Error::Internal); +/// Failed to configure SPI Mux enable pin. +pub const EG_ERROR_SPI_MUX_OE_CONFIG_FAILED: ErrorCode = EG_ERROR.from_pw(6, Error::Internal); +/// Failed to set SPI Mux pin states. +pub const EG_ERROR_SPI_MUX_SET_FAILED: ErrorCode = EG_ERROR.from_pw(7, Error::Internal); +/// The firmware update bundle was not found on external flash. +pub const EG_ERROR_UPDATE_NOT_FOUND: ErrorCode = EG_ERROR.from_pw(8, Error::NotFound); diff --git a/target/earlgrey/util/fw_update.rs b/target/earlgrey/util/fw_update.rs index 05764b6d..430e2759 100644 --- a/target/earlgrey/util/fw_update.rs +++ b/target/earlgrey/util/fw_update.rs @@ -6,9 +6,12 @@ #![no_std] use earlgrey_sysmgr_client::BootInfo; -use earlgrey_util::tags::BootSlot; use earlgrey_util::error::EG_ERROR_BOOT_SLOT_UNKNOWN; +use earlgrey_util::manifest::Manifest; +use earlgrey_util::tags::{BootSlot, ManifestIdentifier}; use util_error::ErrorCode; +use util_io::RandomRead; +use zerocopy::FromBytes; /// State of the firmware update process. #[derive(Clone, Copy, PartialEq, Eq)] @@ -45,6 +48,10 @@ pub struct FwUpdate { pub app_start: u32, /// End address of target Application partition in flash. pub app_end: u32, + /// Size of target ROM_EXT partition in bytes. + pub rom_ext_size: usize, + /// Size of target Application partition in bytes. + pub app_size: usize, } impl FwUpdate { @@ -76,6 +83,8 @@ impl FwUpdate { app, app_start, app_end: app_start + info.app.size, + rom_ext_size: info.rom_ext.size as usize, + app_size: info.app.size as usize, }) } @@ -87,4 +96,82 @@ impl FwUpdate { _ => unreachable!(), } } + + /// Scans the provided reader to find a compatible firmware bundle using a provided work buffer. + pub fn scan_firmware_bundle( + &self, + flash: &mut impl RandomRead, + buf: &mut [u8], + ) -> Result, ErrorCode> { + let flash_size = flash.size()?; + let step = self.rom_ext_size; + + let mut offset = 0; + while offset + self.rom_ext_size + self.app_size <= flash_size { + if let Some(bundle) = self.try_read_bundle_at(flash, offset, buf)? { + return Ok(Some(bundle)); + } + offset += step; + } + + Ok(None) + } + + fn try_read_bundle_at( + &self, + flash: &mut impl RandomRead, + offset: usize, + buf: &mut [u8], + ) -> Result, ErrorCode> { + let manifest_size = core::mem::size_of::(); + if buf.len() < manifest_size { + return Ok(None); + } + + buf[..manifest_size].fill(0); + if flash.read(offset, &mut buf[..manifest_size]).is_err() { + return Ok(None); + } + let Ok((rom_ext_hdr, _)) = Manifest::ref_from_prefix(&buf[..manifest_size]) else { + return Ok(None); + }; + if rom_ext_hdr.identifier != ManifestIdentifier::ROM_EXT { + return Ok(None); + } + let rom_ext_len = rom_ext_hdr.length as usize; + if rom_ext_len > self.rom_ext_size { + return Ok(None); + } + + buf[..manifest_size].fill(0); + if flash + .read(offset + self.rom_ext_size, &mut buf[..manifest_size]) + .is_err() + { + return Ok(None); + } + let Ok((owner_hdr, _)) = Manifest::ref_from_prefix(&buf[..manifest_size]) else { + return Ok(None); + }; + + if owner_hdr.identifier != ManifestIdentifier::APPLICATION { + return Ok(None); + } + let owner_len = owner_hdr.length as usize; + if owner_len > self.app_size { + return Ok(None); + } + + Ok(Some(FirmwareBundle { + offset, + rom_ext_len, + owner_len, + })) + } +} + +pub struct FirmwareBundle { + pub offset: usize, + pub rom_ext_len: usize, + pub owner_len: usize, } From 17280c35402484ea533467b6d160daaa67e93db9 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Mon, 22 Jun 2026 14:24:15 +0800 Subject: [PATCH 09/13] updatemgr: Implement internal EFLASH writing Implement EFLASH page-by-page erasing and programming logic (`flash_write_partition`) in `updatemgr.rs` to flash valid ROM_EXT and APPLICATION images from the external flash reader. Signed-off-by: Anthony Chen --- .../earlgrey/firmware/transport/BUILD.bazel | 1 + .../earlgrey/firmware/transport/system.json5 | 6 + .../earlgrey/firmware/transport/updatemgr.rs | 155 +++++++++++++++++- target/earlgrey/util/fw_update.rs | 20 ++- 4 files changed, 173 insertions(+), 9 deletions(-) diff --git a/target/earlgrey/firmware/transport/BUILD.bazel b/target/earlgrey/firmware/transport/BUILD.bazel index 684d5d38..cdb69080 100644 --- a/target/earlgrey/firmware/transport/BUILD.bazel +++ b/target/earlgrey/firmware/transport/BUILD.bazel @@ -120,6 +120,7 @@ rust_process( "//hal/blocking/flash", "//services/flash:client", "//target/earlgrey/services/sysmgr:server", + "//target/earlgrey/util", "//util/error", "//util/io", "//util/ipc", diff --git a/target/earlgrey/firmware/transport/system.json5 b/target/earlgrey/firmware/transport/system.json5 index 9db27264..89b4b57e 100644 --- a/target/earlgrey/firmware/transport/system.json5 +++ b/target/earlgrey/firmware/transport/system.json5 @@ -146,6 +146,12 @@ handler_process: "flash_server", handler_object_name: "spi_flash_updatemgr_service" }, + { + name: "flash_updatemgr", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "eflash_updatemgr_service" + }, { name: "updatemgr_thread", kernel_stack_size_bytes: 4096, diff --git a/target/earlgrey/firmware/transport/updatemgr.rs b/target/earlgrey/firmware/transport/updatemgr.rs index 9a20139d..675026ee 100644 --- a/target/earlgrey/firmware/transport/updatemgr.rs +++ b/target/earlgrey/firmware/transport/updatemgr.rs @@ -5,7 +5,8 @@ #![no_main] use earlgrey_util::error::EG_ERROR_UPDATE_NOT_FOUND; -use hal_flash::Flash; +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; use pw_status::Error; use services_flash_client::FlashIpcClient; use updatemgr_codegen::handle; @@ -25,11 +26,12 @@ struct SpiFlashDetected { #[derive(Zfmt)] #[zfmt( - format = "Update found! Staging slot: {staging_slot:c}, ROM_EXT offset: 0x{rom_ext_offset:x}, Owner offset: 0x{owner_offset:x}" + format = "Update found! ROM_EXT staging: {rom_ext_staging_slot:c} (offset: 0x{rom_ext_offset:x}), Owner staging: {owner_staging_slot:c} (offset: 0x{owner_offset:x})" )] struct UpdateTargetMapped { - staging_slot: u32, + rom_ext_staging_slot: u32, rom_ext_offset: u32, + owner_staging_slot: u32, owner_offset: u32, } @@ -39,7 +41,77 @@ struct UpdateAttemptFailed { status: u32, } +#[derive(Zfmt)] +#[zfmt(format = "Flashing {region} partition: EFLASH offset 0x{start:x} ({len} bytes)")] +struct FlashingRegion { + region: &'static str, + start: u32, + len: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Successfully wrote {region} partition to EFLASH!")] +struct FlashWriteSuccess { + region: &'static str, +} + +#[derive(Zfmt)] +#[zfmt(format = "Failed to write {region} partition! Status: 0x{status:08x}")] +struct FlashWriteFailed { + region: &'static str, + status: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Firmware update installation complete! Rebooting into the new slot...")] +struct UpdateComplete {} + +#[derive(Zfmt)] +#[zfmt(format = "Owner block found at SPI Flash offset: 0x{offset:x}")] +struct OwnerBlockFound { + offset: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Owner block NOT found on SPI Flash!")] +struct OwnerBlockNotFound {} +fn flash_write_partition( + flash_client: &mut FlashIpcClient, + spi_flash: &mut impl RandomRead, + src_offset: usize, + dest_offset: u32, + len: usize, +) -> Result<(), ErrorCode> { + // Get EFLASH geometry to find page size + let (_, page_size, _) = flash_client.geometry()?; + let page_len = page_size.get(); + + let mut erased = 0; + while erased < len { + let erase_addr = dest_offset + erased as u32; + flash_client.erase(FlashAddress::data(erase_addr), page_size)?; + erased += page_len; + } + + let mut page_buf = [0u8; 2048]; + let mut written = 0; + while written < len { + let chunk_len = core::cmp::min(len - written, page_len); + let src_addr = src_offset + written; + let dest_addr = dest_offset + written as u32; + + page_buf.fill(0); + spi_flash.read(src_addr, &mut page_buf[..chunk_len])?; + flash_client.program(FlashAddress::data(dest_addr), &page_buf)?; + + written += chunk_len; + } + + Ok(()) +} + fn try_update( + flash_client: &mut FlashIpcClient, spi_flash_client: &mut FlashIpcClient, update: &earlgrey_fw_update::FwUpdate, work_buf: &mut [u8; 2048], @@ -48,16 +120,67 @@ fn try_update( let size = reader.size()?; util_zfmt::info!(SpiFlashDetected { size: size as u32 }); - let Some(_bundle) = update.scan_firmware_bundle(&mut reader, work_buf)? else { + let Some(bundle) = update.scan_firmware_bundle(&mut reader, work_buf)? else { return Err(EG_ERROR_UPDATE_NOT_FOUND); }; util_zfmt::info!(UpdateTargetMapped { - staging_slot: update.rom_ext.0, + rom_ext_staging_slot: update.rom_ext.0, rom_ext_offset: update.rom_ext_start, + owner_staging_slot: update.app.0, owner_offset: update.app_start, }); + util_zfmt::info!(FlashingRegion { + region: "ROM_EXT", + start: update.rom_ext_start, + len: bundle.rom_ext_len as u32, + }); + flash_write_partition( + flash_client, + &mut reader, + bundle.offset, + update.rom_ext_start, + bundle.rom_ext_len, + ) + .map_err(|e| { + util_zfmt::error!(FlashWriteFailed { + region: "ROM_EXT", + status: e.0.get(), + }); + e + })?; + util_zfmt::info!(FlashWriteSuccess { region: "ROM_EXT" }); + + util_zfmt::info!(FlashingRegion { + region: "Owner", + start: update.app_start, + len: bundle.owner_len as u32, + }); + flash_write_partition( + flash_client, + &mut reader, + bundle.offset + update.rom_ext_size, + update.app_start, + bundle.owner_len, + ) + .map_err(|e| { + util_zfmt::error!(FlashWriteFailed { + region: "Owner", + status: e.0.get(), + }); + e + })?; + util_zfmt::info!(FlashWriteSuccess { region: "Owner" }); + + if let Some(offset) = bundle.owner_block_offset { + util_zfmt::info!(OwnerBlockFound { + offset: offset as u32 + }); + } else { + util_zfmt::warn!(OwnerBlockNotFound {}); + } + Ok(()) } @@ -72,17 +195,33 @@ fn updatemgr_process() -> Result<(), ErrorCode> { let mut spi_flash_client = FlashIpcClient::new(spi_flash_handle)?; let mut work_buf = [0u8; 2048]; + let flash_ipc_handle = IpcHandle::new(handle::FLASH_UPDATEMGR); + let mut flash_client = FlashIpcClient::new(flash_ipc_handle)?; + loop { - match try_update(&mut spi_flash_client, &update, &mut work_buf) { + match try_update( + &mut flash_client, + &mut spi_flash_client, + &update, + &mut work_buf, + ) { Ok(()) => break, Err(e) => { util_zfmt::warn!(UpdateAttemptFailed { status: e.0.get() }); - break; + let _ = sleep_until(Instant::MAX); } } } - let _ = sleep_until(Instant::MAX); + let policy = earlgrey_sysmgr_client::BootPolicy { + pref_slot: update.app, + next_slot: update.app, + }; + sysmgr_client.set_boot_policy(policy)?; + util_zfmt::info!(UpdateComplete {}); + + let _ = sysmgr_client.request_reboot(); + Ok(()) } diff --git a/target/earlgrey/util/fw_update.rs b/target/earlgrey/util/fw_update.rs index 430e2759..24ac507d 100644 --- a/target/earlgrey/util/fw_update.rs +++ b/target/earlgrey/util/fw_update.rs @@ -7,7 +7,7 @@ use earlgrey_sysmgr_client::BootInfo; use earlgrey_util::error::EG_ERROR_BOOT_SLOT_UNKNOWN; -use earlgrey_util::manifest::Manifest; +use earlgrey_util::manifest::{Manifest, ManifestExtHeader, MANIFEST_EXT_ID_OWNER_TRANSFER_BLOB}; use earlgrey_util::tags::{BootSlot, ManifestIdentifier}; use util_error::ErrorCode; use util_io::RandomRead; @@ -162,10 +162,27 @@ impl FwUpdate { return Ok(None); } + let mut owner_block_offset = None; + for entry in &owner_hdr.extensions.entries { + if entry.identifier == MANIFEST_EXT_ID_OWNER_TRANSFER_BLOB { + let ext_offset = entry.offset as usize; + if ext_offset > 0 { + owner_block_offset = Some( + offset + + self.rom_ext_size + + ext_offset + + core::mem::size_of::(), + ); + break; + } + } + } + Ok(Some(FirmwareBundle { offset, rom_ext_len, owner_len, + owner_block_offset, })) } } @@ -174,4 +191,5 @@ pub struct FirmwareBundle { pub offset: usize, pub rom_ext_len: usize, pub owner_len: usize, + pub owner_block_offset: Option, } From e66e46f7903428db6e3a5e51d8ec0805960eb8f9 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Sat, 4 Jul 2026 16:29:00 +0800 Subject: [PATCH 10/13] updatemgr: support flashing owner block to owner info page 1 Implement `flash_owner_block` helper in `updatemgr.rs` to write the scanned Owner Block to Owner Info Page 1. Signed-off-by: Anthony Chen --- .../earlgrey/firmware/transport/updatemgr.rs | 70 +++++++++++++++++-- target/earlgrey/util/tags.rs | 25 +++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/target/earlgrey/firmware/transport/updatemgr.rs b/target/earlgrey/firmware/transport/updatemgr.rs index 675026ee..3f348429 100644 --- a/target/earlgrey/firmware/transport/updatemgr.rs +++ b/target/earlgrey/firmware/transport/updatemgr.rs @@ -5,6 +5,7 @@ #![no_main] use earlgrey_util::error::EG_ERROR_UPDATE_NOT_FOUND; +use earlgrey_util::tags::OwnershipUpdateMode; use earlgrey_util::EarlgreyFlashAddress; use hal_flash::{Flash, FlashAddress}; use pw_status::Error; @@ -75,12 +76,34 @@ struct OwnerBlockFound { #[derive(Zfmt)] #[zfmt(format = "Owner block NOT found on SPI Flash!")] struct OwnerBlockNotFound {} +#[derive(Zfmt)] +#[zfmt(format = "Flashing owner block to Info Page 3 (OwnerSlot1)...")] +struct FlashingOwnerBlock {} + +#[derive(Zfmt)] +#[zfmt(format = "Successfully flashed owner block to Info Page 3!")] +struct FlashingOwnerBlockSuccess {} + +#[derive(Zfmt)] +#[zfmt(format = "Failed to flash owner block! Status: 0x{status:08x}")] +struct FlashingOwnerBlockFailed { + status: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Owner block update is not allowed in current state (mode = {mode}). Skipping.")] +struct OwnerBlockUpdateNotAllowed { + mode: earlgrey_util::tags::OwnershipUpdateMode, +} + +/// Helper function to erase and write a firmware partition page-by-page. fn flash_write_partition( flash_client: &mut FlashIpcClient, spi_flash: &mut impl RandomRead, src_offset: usize, dest_offset: u32, len: usize, + work_buf: &mut [u8; 2048], ) -> Result<(), ErrorCode> { // Get EFLASH geometry to find page size let (_, page_size, _) = flash_client.geometry()?; @@ -93,16 +116,15 @@ fn flash_write_partition( erased += page_len; } - let mut page_buf = [0u8; 2048]; let mut written = 0; while written < len { let chunk_len = core::cmp::min(len - written, page_len); let src_addr = src_offset + written; let dest_addr = dest_offset + written as u32; - page_buf.fill(0); - spi_flash.read(src_addr, &mut page_buf[..chunk_len])?; - flash_client.program(FlashAddress::data(dest_addr), &page_buf)?; + work_buf.fill(0); + spi_flash.read(src_addr, &mut work_buf[..chunk_len])?; + flash_client.program(FlashAddress::data(dest_addr), work_buf)?; written += chunk_len; } @@ -110,6 +132,38 @@ fn flash_write_partition( Ok(()) } +fn flash_owner_block( + flash_client: &mut FlashIpcClient, + spi_flash: &mut impl RandomRead, + src_offset: usize, + work_buf: &mut [u8; 2048], +) -> Result<(), ErrorCode> { + let mut update_mode_bytes = [0u8; 4]; + flash_client.read(FlashAddress::info(1, 2, 20), &mut update_mode_bytes)?; + let update_mode = OwnershipUpdateMode(u32::from_le_bytes(update_mode_bytes)); + + if update_mode != OwnershipUpdateMode::AnyVersion { + util_zfmt::warn!(OwnerBlockUpdateNotAllowed { mode: update_mode }); + return Ok(()); + } + + // kFlashCtrlInfoPageOwnerSlot1 + let dest_addr = FlashAddress::info(1, 3, 0); + let (_, page_size, _) = flash_client.geometry()?; + + util_zfmt::info!(FlashingOwnerBlock {}); + + flash_client.erase(dest_addr, page_size)?; + + work_buf.fill(0); + spi_flash.read(src_offset, work_buf)?; + + flash_client.program(dest_addr, work_buf)?; + + util_zfmt::info!(FlashingOwnerBlockSuccess {}); + Ok(()) +} + fn try_update( flash_client: &mut FlashIpcClient, spi_flash_client: &mut FlashIpcClient, @@ -142,6 +196,7 @@ fn try_update( bundle.offset, update.rom_ext_start, bundle.rom_ext_len, + work_buf, ) .map_err(|e| { util_zfmt::error!(FlashWriteFailed { @@ -163,6 +218,7 @@ fn try_update( bundle.offset + update.rom_ext_size, update.app_start, bundle.owner_len, + work_buf, ) .map_err(|e| { util_zfmt::error!(FlashWriteFailed { @@ -177,6 +233,10 @@ fn try_update( util_zfmt::info!(OwnerBlockFound { offset: offset as u32 }); + + if let Err(e) = flash_owner_block(flash_client, &mut reader, offset, work_buf) { + util_zfmt::error!(FlashingOwnerBlockFailed { status: e.0.get() }); + } } else { util_zfmt::warn!(OwnerBlockNotFound {}); } @@ -194,10 +254,10 @@ fn updatemgr_process() -> Result<(), ErrorCode> { let spi_flash_handle = IpcHandle::new(handle::SPI_FLASH_UPDATEMGR); let mut spi_flash_client = FlashIpcClient::new(spi_flash_handle)?; - let mut work_buf = [0u8; 2048]; let flash_ipc_handle = IpcHandle::new(handle::FLASH_UPDATEMGR); let mut flash_client = FlashIpcClient::new(flash_ipc_handle)?; + let mut work_buf = [0u8; 2048]; loop { match try_update( &mut flash_client, diff --git a/target/earlgrey/util/tags.rs b/target/earlgrey/util/tags.rs index 178cbafd..a9f3c74f 100644 --- a/target/earlgrey/util/tags.rs +++ b/target/earlgrey/util/tags.rs @@ -177,3 +177,28 @@ impl RetRamVersion { /// Retention RAM layout version 4 ('RR04'). pub const Version4: Self = Self(u32::from_le_bytes(*b"RR04")); } + +/// The ownership update mode configuration. +#[derive(Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Zfmt)] +#[zfmt(format = "{0:c}")] +#[repr(C)] +pub struct OwnershipUpdateMode(pub u32); + +impl OwnershipUpdateMode { + /// Update mode open: `OPEN` (unlock key has full power). + pub const Open: Self = Self(u32::from_le_bytes(*b"OPEN")); + /// Update mode self: `SELF` (unlock key only unlocks to UnlockedSelf). + pub const SelfMode: Self = Self(u32::from_le_bytes(*b"SELF")); + /// Update mode NewVersion: `NEWV` + /// (unlock key can't unlock; accept new owner configs from self-same owner + /// if the config_version is newer). + pub const NewVersion: Self = Self(u32::from_le_bytes(*b"NEWV")); + /// Update mode SelfVersion: `SELV` + /// (unlock key only unlocks to UnlockedSelf; accept new owner configs from + /// self-same owner if the config_version is newer). + pub const SelfVersion: Self = Self(u32::from_le_bytes(*b"SELV")); + /// Update mode AnyVersion: `ANYV` + /// (accept new owner configs as long as the config_version is newer, + /// or any config_version if it is a new owner (transfer)). + pub const AnyVersion: Self = Self(u32::from_le_bytes(*b"ANYV")); +} From 6b2683f2e4aa97381c58f8784de4632905150120 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Thu, 23 Jul 2026 07:45:32 +0800 Subject: [PATCH 11/13] transport: Add dedicated eeprom_programmer firmware target for DFU SPI flash tests In the transport_firmware, `updatemgr` continuously scans the external SPI flash for updates while `usbmgr` handles USB DFU read/write requests . Operating both processes concurrently can cause interference and race conditions on the external flash. To isolate DFU SPI EEPROM operations during testing, add a dedicated `eeprom_programmer_firmware` target that runs a subset of transport processes without `updatemgr`. Signed-off-by: Anthony Chen --- .../firmware/transport/tests/dfu/BUILD.bazel | 42 +-- .../tests/eeprom_programmer/BUILD.bazel | 192 +++++++++++ .../tests/eeprom_programmer/flash_server.rs | 57 ++++ .../host_usb_dfu_spi_flash.rs | 2 +- .../eeprom_programmer/programmer_system.json5 | 119 +++++++ .../eeprom_programmer/programmer_target.rs | 33 ++ .../tests/eeprom_programmer/usbmgr.rs | 318 ++++++++++++++++++ target/earlgrey/tooling/opentitan_runner.bzl | 2 + 8 files changed, 735 insertions(+), 30 deletions(-) create mode 100644 target/earlgrey/tests/eeprom_programmer/BUILD.bazel create mode 100644 target/earlgrey/tests/eeprom_programmer/flash_server.rs rename target/earlgrey/{firmware/transport/tests/dfu => tests/eeprom_programmer}/host_usb_dfu_spi_flash.rs (98%) create mode 100644 target/earlgrey/tests/eeprom_programmer/programmer_system.json5 create mode 100644 target/earlgrey/tests/eeprom_programmer/programmer_target.rs create mode 100644 target/earlgrey/tests/eeprom_programmer/usbmgr.rs diff --git a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel index 93929236..b9d1ac00 100644 --- a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel +++ b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel @@ -83,45 +83,27 @@ opentitan_test( ) opentitan_test( - name = "dfu_firmware_update_hyper310_test", + name = "dfu_owner_transfer_hyper340_test", timeout = "eternal", clear_bitstream = True, ecdsa_key = FPGA_ECDSA_KEY, - environment = "//target/earlgrey/env:hyper310", - interface = "hyper310", + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", tags = [ "hardware", - "hyper310", + "hyper340", ], target = "//target/earlgrey/firmware/transport:transport_firmware", target_data = [ ":bootinfo_signed_transfer", ":bootinfo_signed_simple", ], - test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_cmd = "--logging=info --expect-reboot --expect-app --expect-owner-transfer --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_transfer.app_prod_0.signed.bin", test_harness = ":host_usb_dfu_owner_transfer", ) -opentitan_rust_binary( - name = "host_usb_dfu_spi_flash", - srcs = ["host_usb_dfu_spi_flash.rs"], - edition = "2024", - rustc_flags = [ - "-C", - "link-arg=-Wl,--allow-shlib-undefined", - ], - deps = [ - "//target/earlgrey/testutil", - "//third_party/lowrisc_opentitan:opentitanlib", - "//third_party/lowrisc_opentitan:usb_test_helper", - "@ot_crate_index//:anyhow", - "@ot_crate_index//:clap", - "@ot_crate_index//:log", - ], -) - opentitan_test( - name = "dfu_spi_flash_hyper310_test", + name = "dfu_firmware_update_hyper310_test", timeout = "eternal", clear_bitstream = True, ecdsa_key = FPGA_ECDSA_KEY, @@ -133,14 +115,15 @@ opentitan_test( ], target = "//target/earlgrey/firmware/transport:transport_firmware", target_data = [ + ":bootinfo_signed_transfer", ":bootinfo_signed_simple", ], - test_cmd = "--logging=info --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", - test_harness = ":host_usb_dfu_spi_flash", + test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_harness = ":host_usb_dfu_owner_transfer", ) opentitan_test( - name = "dfu_spi_flash_hyper340_test", + name = "dfu_firmware_update_hyper340_test", timeout = "eternal", clear_bitstream = True, ecdsa_key = FPGA_ECDSA_KEY, @@ -152,8 +135,9 @@ opentitan_test( ], target = "//target/earlgrey/firmware/transport:transport_firmware", target_data = [ + ":bootinfo_signed_transfer", ":bootinfo_signed_simple", ], - test_cmd = "--logging=info --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", - test_harness = ":host_usb_dfu_spi_flash", + test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_harness = ":host_usb_dfu_owner_transfer", ) diff --git a/target/earlgrey/tests/eeprom_programmer/BUILD.bazel b/target/earlgrey/tests/eeprom_programmer/BUILD.bazel new file mode 100644 index 00000000..143b016b --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/BUILD.bazel @@ -0,0 +1,192 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:multi_process_app.bzl", "multi_process_app") +load("@pigweed//pw_kernel/tooling:rust_process.bzl", "rust_process") +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") +load("//third_party/lowrisc_opentitan:defs.bzl", "opentitan_rust_binary") + +package(default_visibility = ["//visibility:public"]) + +target_linker_script( + name = "programmer_linker_script", + system_config = ":programmer_system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "programmer_system_config", + srcs = ["programmer_system.json5"], +) + +target_codegen( + name = "programmer_codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + crate_name = "codegen", + system_config = ":programmer_system_config", +) + +rust_binary( + name = "programmer_target", + srcs = [ + "programmer_target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":programmer_codegen", + ":programmer_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_log/rust:pw_log", + ], +) + +rust_process( + name = "flash_server", + srcs = [ + "flash_server.rs", + ], + codegen_crate_name = "flash_server_codegen", + edition = "2024", + system_config = ":programmer_system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//services/flash:server", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:spi_host", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + ], +) + +rust_process( + name = "usbmgr", + srcs = [ + "usbmgr.rs", + ], + codegen_crate_name = "usbmgr_codegen", + edition = "2024", + system_config = ":programmer_system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//hal/blocking/usb:hal_usb", + "//protocol/usb/cdc_acm", + "//protocol/usb/dfu", + "//protocol/usb/stack", + "//services/flash:client", + "//target/earlgrey/drivers:usb_driver", + "//target/earlgrey/registers:lc_ctrl", + "//target/earlgrey/registers:pinmux", + "//target/earlgrey/registers:top_earlgrey", + "//target/earlgrey/registers:usbdev", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + "@rust_crates//:zerocopy", + ], +) + +multi_process_app( + name = "eeprom_programmer", + processes = [ + ":flash_server", + ":usbmgr", + ], + tags = ["kernel"], + template = "//target/earlgrey/firmware/transport:app_entry.rs.jinja", + visibility = ["//visibility:public"], + deps = ["//util/panic"], +) + +system_image( + name = "eeprom_programmer_firmware", + apps = [ + ":eeprom_programmer", + ], + kernel = ":programmer_target", + platform = "//target/earlgrey", + system_config = ":programmer_system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], +) + +opentitan_rust_binary( + name = "host_usb_dfu_spi_flash", + srcs = ["host_usb_dfu_spi_flash.rs"], + edition = "2024", + rustc_flags = [ + "-C", + "link-arg=-Wl,--allow-shlib-undefined", + ], + deps = [ + "//target/earlgrey/testutil", + "//third_party/lowrisc_opentitan:opentitanlib", + "//third_party/lowrisc_opentitan:usb_test_helper", + "@ot_crate_index//:anyhow", + "@ot_crate_index//:clap", + "@ot_crate_index//:log", + ], +) + +opentitan_test( + name = "dfu_spi_flash_hyper310_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":eeprom_programmer_firmware", + target_data = [ + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple", + ], + test_cmd = "--logging=info --firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple)", + test_harness = ":host_usb_dfu_spi_flash", +) + +opentitan_test( + name = "dfu_spi_flash_hyper340_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":eeprom_programmer_firmware", + target_data = [ + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple", + ], + test_cmd = "--logging=info --firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple)", + test_harness = ":host_usb_dfu_spi_flash", +) diff --git a/target/earlgrey/tests/eeprom_programmer/flash_server.rs b/target/earlgrey/tests/eeprom_programmer/flash_server.rs new file mode 100644 index 00000000..7814e985 --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/flash_server.rs @@ -0,0 +1,57 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use flash_server_codegen::handle; +use pw_status::Error; +use services_flash_server::FlashIpcServer; +use spi_host::{RegisterBlock, SpiHost0}; +use userspace::process_entry; +use userspace::syscall; +use userspace::syscall::Signals; +use userspace::time::Instant; +use util_error::ErrorCode; +use util_ipc::IpcHandle; + +fn flash_server() -> Result<(), ErrorCode> { + let mmio0 = unsafe { RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + return Err(ErrorCode::from(e)); + } + let mut spi_flash = spi_flash::SpiFlash::new(spi_host); + if let Err(e) = spi_flash.init() { + return Err(e); + } + let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_USB_SERVICE, + Signals::READABLE, + 1, + ) + .map_err(ErrorCode::kernel_error)?; + + let mut buf = [0u8; 2064]; + let spi_flash_usb_ipc = IpcHandle::new(handle::SPI_FLASH_USB_SERVICE); + + loop { + let wait_result = + syscall::object_wait(handle::FLASH_WAIT_GROUP, Signals::READABLE, Instant::MAX) + .map_err(ErrorCode::kernel_error)?; + + let token = wait_result.user_data; + if token == 1 { + let _ = spi_flash_server.handle_one(&spi_flash_usb_ipc, &mut buf); + } + } +} + +#[process_entry("flash_server")] +fn entry() -> Result<(), Error> { + let _ = flash_server(); + Err(Error::Unknown) +} diff --git a/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs b/target/earlgrey/tests/eeprom_programmer/host_usb_dfu_spi_flash.rs similarity index 98% rename from target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs rename to target/earlgrey/tests/eeprom_programmer/host_usb_dfu_spi_flash.rs index 76addea8..2834d11c 100644 --- a/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_spi_flash.rs +++ b/target/earlgrey/tests/eeprom_programmer/host_usb_dfu_spi_flash.rs @@ -41,7 +41,7 @@ fn run_dfu_spi_flash_test_inner( log::info!("waiting for Maize Welcome on console..."); let _ = UartConsole::wait_for( uart, - r"Welcome to Maize on Earlgrey Transport Firmware!", + r"Welcome to Maize on Earlgrey EEPROM Programmer Firmware!", Duration::from_secs(10), )?; diff --git a/target/earlgrey/tests/eeprom_programmer/programmer_system.json5 b/target/earlgrey/tests/eeprom_programmer/programmer_system.json5 new file mode 100644 index 00000000..9726594f --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/programmer_system.json5 @@ -0,0 +1,119 @@ +// 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: "eeprom_programmer", + flash_size_bytes: 65536, + processes: [ + { + name: "flash_server", + ram_size_bytes: 4096, + objects: [ + { + name: "spi_flash_usb_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: "flash_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: "usbmgr", + ram_size_bytes: 16384, + objects: [ + { + name: "usbdev_interrupts", + type: "interrupt", + irqs: [ + { name: "usbdev_pkt_received", number: 135 }, + { name: "usbdev_pkt_sent", number: 136 }, + { name: "usbdev_disconnected", number: 137 }, + { name: "usbdev_host_lost", number: 138 }, + { name: "usbdev_link_reset", number: 139 }, + { name: "usbdev_link_suspend", number: 140 }, + { name: "usbdev_link_resume", number: 141 }, + { name: "usbdev_av_out_empty", number: 142 }, + { name: "usbdev_rx_full", number: 143 }, + { name: "usbdev_av_overflow", number: 144 }, + { name: "usbdev_av_setup_empty", number: 152 } + ] + }, + { + name: "spi_flash_usb", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "spi_flash_usb_service" + }, + { + name: "usbmgr_thread", + kernel_stack_size_bytes: 2048, + type: "thread" + } + ], + memory_mappings: [ + { + name: "usbdev", + type: "device", + start_address: 0x40320000, + size_bytes: 0x1000 + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000 + }, + { + name: "lc_ctrl", + type: "device", + start_address: 0x40140000, + size_bytes: 0x100 + } + ] + } + ] + } + ] +} diff --git a/target/earlgrey/tests/eeprom_programmer/programmer_target.rs b/target/earlgrey/tests/eeprom_programmer/programmer_target.rs new file mode 100644 index 00000000..cc4791f7 --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/programmer_target.rs @@ -0,0 +1,33 @@ +// 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 EEPROM Programmer Firmware"; + + fn main() -> ! { + codegen::start(); + loop { + core::hint::spin_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 { + core::hint::spin_loop(); + } + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/eeprom_programmer/usbmgr.rs b/target/earlgrey/tests/eeprom_programmer/usbmgr.rs new file mode 100644 index 00000000..ba21ff01 --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/usbmgr.rs @@ -0,0 +1,318 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use aligned::{Aligned, A4}; +use hal_flash::{Flash, FlashAddress}; +use hal_usb::driver::UsbDriver; +use hal_usb::{ConfigDescriptor, DeviceDescriptor, StringDescriptorRef}; +use lc_ctrl::LcCtrl; +use pinmux::PinmuxAon; +use protocol_usb_cdc_acm::{CdcAcm, CdcAcmBuilder}; +use protocol_usb_dfu::{DfuBuilder, DfuClass, DfuHandler, DfuStatus}; +use pw_status::Error; +use services_flash_client::FlashIpcClient; +use usb_driver::UsbConfig; +use usb_stack::{DescriptorSource, UsbAction, UsbClass}; +use usbdev::Usbdev; +use usbmgr_codegen::{handle, signals}; +use userspace::time::Instant; +use userspace::{process_entry, syscall}; +use util_error::ErrorCode; +use util_ipc::IpcHandle; +use zerocopy::IntoBytes; + +const USB_VENDOR_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(1); +const USB_PRODUCT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(2); +const USB_SERIAL_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(3); +const USB_CDC_COMM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(4); +const USB_CDC_DATA_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(5); +const DFU_FIRMWARE_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(6); +const DFU_UDS_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(7); +const DFU_CDI0_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(8); +const DFU_CDI1_CERT_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(9); +const DFU_RESERVED_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(10); +const DFU_SPI_EEPROM_HANDLE: hal_usb::StringHandle = hal_usb::StringHandle(11); + +const DFU_ALT_FIRMWARE: u8 = 0; +const DFU_ALT_UDS_CERT: u8 = 1; +const DFU_ALT_CDI0_CERT: u8 = 2; +const DFU_ALT_CDI1_CERT: u8 = 3; +const DFU_ALT_RESERVED: u8 = 4; +const DFU_ALT_SPI_EEPROM0: u8 = 5; + +const DFU_BUILDER: DfuBuilder = DfuBuilder::new( + 2, // interface_num + 6, // alt_settings + 2048, // transfer_size +); + +const CDC_BUILDER: CdcAcmBuilder = CdcAcmBuilder::new( + 0, // comm_if + 1, // data_if + 1, // comm_ep + 2, // data_out_ep + 3, // data_in_ep +); + +const DEVICE_DESC: DeviceDescriptor = DeviceDescriptor { + device_class: hal_usb::DeviceClass::SPECIFIED_BY_INTERFACE, + device_sub_class: 0x00, + device_protocol: 0x00, + max_packet_size: 64, + vendor_id: 0x18d1, + product_id: 0x503a, + device_release_num: 0x0100, + manufacturer: USB_VENDOR_HANDLE, + product: USB_PRODUCT_HANDLE, + serial_num: USB_SERIAL_HANDLE, +}; + +const CONFIG_DESC: ConfigDescriptor = ConfigDescriptor { + configuration_value: 1, + max_power: 250, + self_powered: false, + remote_wakeup: false, + interfaces: &[ + CDC_BUILDER.comm_interface( + USB_CDC_COMM_HANDLE, + &CDC_BUILDER.comm_func_descs(), + &CDC_BUILDER.comm_endpoints(), + ), + CDC_BUILDER.data_interface(USB_CDC_DATA_HANDLE, &CDC_BUILDER.data_endpoints()), + DFU_BUILDER.interface(DFU_ALT_FIRMWARE, DFU_FIRMWARE_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_UDS_CERT, DFU_UDS_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_CDI0_CERT, DFU_CDI0_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_CDI1_CERT, DFU_CDI1_CERT_HANDLE, &[]), + DFU_BUILDER.interface(DFU_ALT_RESERVED, DFU_RESERVED_HANDLE, &[]), + DFU_BUILDER.interface( + DFU_ALT_SPI_EEPROM0, + DFU_SPI_EEPROM_HANDLE, + &[DFU_BUILDER.functional_descriptor()], + ), + ], +}; + +const STRING_DESC_0: hal_usb::StringDescriptor0 = hal_usb::StringDescriptor0 { langs: &[0x0409] }; + +const VENDOR_ID: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Google Inc.").as_ref(); +const PRODUCT_ID_DEFAULT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("OpenPRoT Earlgrey EEPROM Programmer").as_ref(); +const USB_COMM: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDC Comm Interface").as_ref(); +const USB_DATA: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDC Data Interface").as_ref(); +const DFU_FIRMWARE: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("Application / Transport Firmware").as_ref(); +const DFU_UDS_CERT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("UDS Certificate").as_ref(); +const DFU_CDI0_CERT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDI_0 Certificate").as_ref(); +const DFU_CDI1_CERT: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("CDI_1 Certificate").as_ref(); +const DFU_RESERVED: hal_usb::StringDescriptorRef = hal_usb::string_descriptor!("Reserved").as_ref(); +const DFU_SPI_EEPROM: hal_usb::StringDescriptorRef = + hal_usb::string_descriptor!("SPI EEPROM 0").as_ref(); + +struct MyDescriptors<'a> { + serial_desc_bytes: StringDescriptorRef<'a>, + product_desc_bytes: StringDescriptorRef<'a>, +} + +impl DescriptorSource for MyDescriptors<'_> { + const DEVICE_DESC_BYTES: &'static Aligned = &Aligned(DEVICE_DESC.serialize()); + const CONFIG_DESC_BYTES: &'static Aligned = + &Aligned(CONFIG_DESC.serialize::<{ CONFIG_DESC.total_size() }>()); + const STRING_DESC_0_BYTES: &'static Aligned = + &Aligned(STRING_DESC_0.serialize::<{ STRING_DESC_0.total_size() }>()); + const DEVICE_STATUS: Aligned = Aligned([1u8, 0]); + + fn get_string( + &self, + handle: hal_usb::StringHandle, + _lang: u16, + ) -> Option> { + let h = handle.0; + if h == USB_VENDOR_HANDLE.0 { + Some(VENDOR_ID) + } else if h == USB_PRODUCT_HANDLE.0 { + Some(self.product_desc_bytes) + } else if h == USB_SERIAL_HANDLE.0 { + Some(self.serial_desc_bytes) + } else if h == USB_CDC_COMM_HANDLE.0 { + Some(USB_COMM) + } else if h == USB_CDC_DATA_HANDLE.0 { + Some(USB_DATA) + } else if h == DFU_FIRMWARE_HANDLE.0 { + Some(DFU_FIRMWARE) + } else if h == DFU_UDS_CERT_HANDLE.0 { + Some(DFU_UDS_CERT) + } else if h == DFU_CDI0_CERT_HANDLE.0 { + Some(DFU_CDI0_CERT) + } else if h == DFU_CDI1_CERT_HANDLE.0 { + Some(DFU_CDI1_CERT) + } else if h == DFU_RESERVED_HANDLE.0 { + Some(DFU_RESERVED) + } else if h == DFU_SPI_EEPROM_HANDLE.0 { + Some(DFU_SPI_EEPROM) + } else { + None + } + } +} + +pub struct EepromDfuHandler { + spi_flash: FlashIpcClient, +} + +impl EepromDfuHandler { + pub fn new(spi_flash: FlashIpcClient) -> Self { + Self { spi_flash } + } +} + +impl DfuHandler for EepromDfuHandler { + fn dnload(&mut self, alt_setting: u8, block_num: u16, data: &[u8]) -> Result<(), DfuStatus> { + if data.is_empty() { + return Ok(()); + } + if alt_setting != DFU_ALT_SPI_EEPROM0 { + return Err(DfuStatus::ErrTarget); + } + let address = (block_num as u32) * 2048; + let (total_size, page_size, _) = self + .spi_flash + .geometry() + .map_err(|_| DfuStatus::ErrUnknown)?; + if address >= total_size.get() as u32 { + return Err(DfuStatus::ErrAddress); + } + if (address as usize) % page_size.get() == 0 { + self.spi_flash + .erase(FlashAddress::new(address), page_size) + .map_err(|_| DfuStatus::ErrErase)?; + } + self.spi_flash + .program(FlashAddress::new(address), data) + .map_err(|_| DfuStatus::ErrProg) + } + + fn upload( + &mut self, + alt_setting: u8, + block_num: u16, + data: &mut [u8], + ) -> Result { + if alt_setting != DFU_ALT_SPI_EEPROM0 { + return Err(DfuStatus::ErrTarget); + } + let address = (block_num as u32) * 2048; + let (total_size, _, _) = self + .spi_flash + .geometry() + .map_err(|_| DfuStatus::ErrUnknown)?; + if address >= total_size.get() as u32 { + return Ok(0); + } + let len = data.len().min((total_size.get() as u32 - address) as usize); + if len == 0 { + return Ok(0); + } + self.spi_flash + .read(FlashAddress::new(address), &mut data[..len]) + .map_err(|_| DfuStatus::ErrUnknown)?; + Ok(len) + } + + fn manifest(&mut self) -> Result<(), DfuStatus> { + Ok(()) + } + + fn abort(&mut self) {} +} + +fn handle_usb() -> Result<(), ErrorCode> { + let lc_ctrl = unsafe { LcCtrl::new() }; + let device_id: [u32; 8] = lc_ctrl.regs().device_id().read().into(); + let mut serial_num_buffer = Aligned::([0_u8; 130]); + let descriptors = MyDescriptors { + serial_desc_bytes: hal_usb::hex_utf16_descriptor_aligned( + &mut serial_num_buffer, + device_id.as_bytes(), + ) + .unwrap_or(PRODUCT_ID_DEFAULT), + product_desc_bytes: PRODUCT_ID_DEFAULT, + }; + + const USB_CONFIG: UsbConfig = UsbConfig::new(&CDC_BUILDER.eps().0, &CDC_BUILDER.eps().1); + + let spi_flash = FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH_USB))?; + + let dfu_handler = EepromDfuHandler::new(spi_flash); + let mut dfu = DfuClass::<_, 2048>::new(DFU_BUILDER, dfu_handler); + + let mut usb = usb_driver::Usb::new(unsafe { Usbdev::new() }, USB_CONFIG); + let mut ep0 = usb_stack::SimpleEp0::new(); + let mut cdc_acm = CdcAcm::<256, 256>::new(CDC_BUILDER); + + loop { + let _wait_return = syscall::object_wait( + handle::USBDEV_INTERRUPTS, + signals::USBDEV_PKT_RECEIVED + | signals::USBDEV_PKT_SENT + | signals::USBDEV_DISCONNECTED + | signals::USBDEV_HOST_LOST + | signals::USBDEV_LINK_RESET + | signals::USBDEV_LINK_SUSPEND + | signals::USBDEV_LINK_RESUME + | signals::USBDEV_AV_OUT_EMPTY + | signals::USBDEV_RX_FULL + | signals::USBDEV_AV_OVERFLOW + | signals::USBDEV_AV_SETUP_EMPTY, + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + + while let Some(event) = usb.poll() { + let mut action = match cdc_acm.handle_event(event) { + Ok(a) => a, + Err(event) => match dfu.handle_event(event) { + Ok(a) => a, + Err(e) => ep0.handle_event(e, &descriptors).unwrap_or(UsbAction::None), + }, + }; + action.run(&mut usb); + } + + while let Some(byte) = cdc_acm.rx_queue.pop() { + let _ = cdc_acm.tx_queue.push(byte); + } + + cdc_acm.poll_transmit(&mut usb); + dfu.poll(&mut usb); + } +} + +fn usb_setup_pinmux() { + use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn}; + let mut pinmux = unsafe { PinmuxAon::new() }; + + pinmux + .regs_mut() + .mio_periph_insel() + .at(PinmuxPeripheralIn::UsbdevSense as usize) + .modify(|_| (PinmuxInsel::ConstantOne as u32).into()); +} + +fn usbmgr_server() -> Result<(), ErrorCode> { + usb_setup_pinmux(); + handle_usb() +} + +#[process_entry("usbmgr")] +fn entry() -> Result<(), Error> { + let _ = usbmgr_server(); + loop {} +} diff --git a/target/earlgrey/tooling/opentitan_runner.bzl b/target/earlgrey/tooling/opentitan_runner.bzl index b3b92192..ff7f9640 100644 --- a/target/earlgrey/tooling/opentitan_runner.bzl +++ b/target/earlgrey/tooling/opentitan_runner.bzl @@ -141,6 +141,8 @@ exec {runner} {args} extra_args = " ".join(env.opentitantool_args) if env.opentitantool_args else "" test_cmd_str = ctx.attr.test_cmd + if test_cmd_str: + test_cmd_str = ctx.expand_location(test_cmd_str, targets = ctx.attr.target_data) if not is_custom_harness and not test_cmd_str: test_cmd_str = "console --non-interactive --exit-success='{}' --exit-failure='{}'".format(exit_success, exit_failure) From d93589930ce14d40f4a824b5a1222030f3935b63 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Thu, 23 Jul 2026 13:06:58 +0800 Subject: [PATCH 12/13] tests: Pre-flash eeprom_programmer_firmware to invalidate EEPROM0 before DFU tests Set the default target in DFU owner transfer and firmware update tests to `eeprom_programmer_firmware` to invalidate external EEPROM0 prior to running the test payload. Signed-off-by: Anthony Chen --- .../firmware/transport/tests/dfu/BUILD.bazel | 61 ++++++++++-- .../tests/dfu/host_usb_dfu_owner_transfer.rs | 98 +++++++++++++++++-- 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel index b9d1ac00..211fe7d7 100644 --- a/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel +++ b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel @@ -34,6 +34,13 @@ sign_bin( ecdsa_key = FPGA_ECDSA_KEY, ) +sign_bin( + name = "transport_firmware_signed", + basename = "transport_firmware_signed", + bin = "//target/earlgrey/firmware/transport:transport_firmware", + ecdsa_key = FPGA_ECDSA_KEY, +) + opentitan_rust_binary( name = "host_usb_dfu_owner_transfer", srcs = ["host_usb_dfu_owner_transfer.rs"], @@ -73,12 +80,22 @@ opentitan_test( "hardware", "hyper310", ], - target = "//target/earlgrey/firmware/transport:transport_firmware", + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", target_data = [ ":bootinfo_signed_transfer", ":bootinfo_signed_simple", + ":transport_firmware_signed", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin", ], - test_cmd = "--logging=info --expect-reboot --expect-app --expect-owner-transfer --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_transfer.app_prod_0.signed.bin", + test_cmd = " ".join([ + "--logging=info", + "--expect-reboot", + "--expect-app", + "--expect-owner-transfer", + "--rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--firmware=$(rootpath :bootinfo_signed_transfer)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), test_harness = ":host_usb_dfu_owner_transfer", ) @@ -93,12 +110,22 @@ opentitan_test( "hardware", "hyper340", ], - target = "//target/earlgrey/firmware/transport:transport_firmware", + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", target_data = [ ":bootinfo_signed_transfer", ":bootinfo_signed_simple", + ":transport_firmware_signed", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin", ], - test_cmd = "--logging=info --expect-reboot --expect-app --expect-owner-transfer --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_transfer.app_prod_0.signed.bin", + test_cmd = " ".join([ + "--logging=info", + "--expect-reboot", + "--expect-app", + "--expect-owner-transfer", + "--rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--firmware=$(rootpath :bootinfo_signed_transfer)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), test_harness = ":host_usb_dfu_owner_transfer", ) @@ -113,12 +140,21 @@ opentitan_test( "hardware", "hyper310", ], - target = "//target/earlgrey/firmware/transport:transport_firmware", + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", target_data = [ ":bootinfo_signed_transfer", ":bootinfo_signed_simple", + ":transport_firmware_signed", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin", ], - test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_cmd = " ".join([ + "--logging=info", + "--expect-reboot", + "--expect-app", + "--rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--firmware=$(rootpath :bootinfo_signed_simple)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), test_harness = ":host_usb_dfu_owner_transfer", ) @@ -133,11 +169,20 @@ opentitan_test( "hardware", "hyper340", ], - target = "//target/earlgrey/firmware/transport:transport_firmware", + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", target_data = [ ":bootinfo_signed_transfer", ":bootinfo_signed_simple", + ":transport_firmware_signed", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin", ], - test_cmd = "--logging=info --expect-reboot --expect-app --firmware=target/earlgrey/firmware/transport/tests/dfu/bootinfo_simple.app_prod_0.signed.bin", + test_cmd = " ".join([ + "--logging=info", + "--expect-reboot", + "--expect-app", + "--rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--firmware=$(rootpath :bootinfo_signed_simple)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), test_harness = ":host_usb_dfu_owner_transfer", ) diff --git a/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_owner_transfer.rs b/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_owner_transfer.rs index 061007c7..26658a88 100644 --- a/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_owner_transfer.rs +++ b/target/earlgrey/firmware/transport/tests/dfu/host_usb_dfu_owner_transfer.rs @@ -1,14 +1,17 @@ // Licensed under the Apache-2.0 license // SPDX-License-Identifier: Apache-2.0 -use anyhow::{bail, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use clap::Parser; +use std::path::Path; use std::time::Duration; use earlgrey_testutil::{get_dfu_transfer_size, print_uart, sequence_dfu_download, DfuClient}; use opentitanlib::app::TransportWrapper; +use opentitanlib::image::image::{Image, ImageAssembler}; use opentitanlib::test_utils::init::InitializeTest; use opentitanlib::uart::console::UartConsole; +use opentitanlib::util::file::FromReader; use usb::UsbOpts; #[derive(Parser, Debug)] @@ -19,12 +22,18 @@ struct CmdArgs { #[command(flatten)] usb: UsbOpts, + #[arg(long)] + rom_ext: String, + #[arg( long, default_value = "target/earlgrey/firmware/transport/tests/dfu/bootinfo_transfer.app_prod_0.signed.bin" )] firmware: String, + #[arg(long)] + transport_firmware: String, + #[arg(long, default_value = "false")] expect_reboot: bool, @@ -38,20 +47,23 @@ struct CmdArgs { fn run_dfu_owner_transfer_test( transport: &TransportWrapper, usb: &UsbOpts, + bootstrap: &opentitanlib::bootstrap::BootstrapOptions, + rom_ext_path: &str, firmware_path: &str, + transport_firmware_path: &str, expect_reboot: bool, expect_app: bool, expect_owner_transfer: bool, ) -> Result<()> { let uart = transport.uart("console")?; - log::info!("Resetting target..."); + log::info!("Resetting target running eeprom_programmer_firmware..."); transport.reset(opentitanlib::app::UartRx::Clear)?; - log::info!("waiting for Maize Welcome on console..."); + log::info!("Waiting for Maize Welcome on console..."); let _ = UartConsole::wait_for( &*uart, - r"Welcome to Maize on Earlgrey Transport Firmware!", + r"Welcome to Maize on Earlgrey EEPROM Programmer Firmware!", Duration::from_secs(10), )?; @@ -69,7 +81,7 @@ fn run_dfu_owner_transfer_test( let usb_pid = usb.pid; log::info!( - "waiting for DFU device (VID={:04x}, PID={:04x})...", + "Waiting for DFU device (VID={:04x}, PID={:04x})...", usb_vid, usb_pid ); @@ -78,7 +90,7 @@ fn run_dfu_owner_transfer_test( .device_by_id_with_timeout(usb_vid, usb_pid, None, Duration::from_secs(10)) .context("DFU device not found")?; - log::info!("Claiming DFU interface..."); + log::info!("Claiming DFU interface to invalidate EEPROM0..."); let interface_num = 2; device.claim_interface(interface_num)?; @@ -87,6 +99,77 @@ fn run_dfu_owner_transfer_test( let dfu = DfuClient::new(&*device, interface_num); + log::info!("Invalidating EEPROM0 with 132 KiB invalid payload on Alt 5..."); + device.set_alternate_setting(interface_num, 5)?; + let invalid_data = vec![0x00u8; 132 * 1024]; + sequence_dfu_download(&dfu, &*uart, &invalid_data, transfer_size, false)?; + + let _ = device.release_interface(interface_num); + + let owner_fw_image = + Image::read_from_file(Path::new(transport_firmware_path)).with_context(|| { + format!("Failed to read owner firmware image at '{transport_firmware_path}'") + })?; + let image_bytes = owner_fw_image.bytes(); + ensure!( + image_bytes.len() >= 816, + "Owner firmware image at '{transport_firmware_path}' is too small to contain a manifest header" + ); + + // Extract manifest_base_address directly from offset 812 (0x32c) of the owner firmware's manifest header + // because: + // 1) `opentitanlib::image::manifest::Manifest` (from external @lowrisc_opentitan) does not expose + // `manifest_base_address` as a public field on `&Manifest` in the pinned revision in MODULE.bazel. + // 2) `earlgrey_util::manifest::Manifest` (from //target/earlgrey/util) exposes `pub manifest_base_address`, + // but parsing it via zerocopy traits fails due to a crate version mismatch between zerocopy v0.8.50 + // (used by `earlgrey_util`) and v0.8.26 (imported via `@ot_crate_index`). + // TODO: Parse via Manifest struct once (1) opentitanlib is updated or (2) zerocopy crate versions are unified. + // Offset 812 is guaranteed by the Earlgrey/OpenTitan manifest binary specification. + let manifest_base_addr = u32::from_le_bytes(image_bytes[812..816].try_into().unwrap()); + let firmware_offset = if manifest_base_addr == 0xa5a5a5a5 { + 0x10000 + } else if manifest_base_addr >= 0x100000 { + (manifest_base_addr & 0xfffff) as usize + } else { + manifest_base_addr as usize + }; + + log::info!( + "Assembling transport_firmware image: ROM Ext ('{}') @ 0, Firmware ('{}') @ {:#x}...", + rom_ext_path, + transport_firmware_path, + firmware_offset + ); + let mut image_assembler = ImageAssembler::with_params(0x100000, true); + image_assembler.parse(&[ + format!("{}@0", rom_ext_path), + format!("{}@{:#x}", transport_firmware_path, firmware_offset), + ])?; + let payload = image_assembler.assemble()?; + + log::info!("Bootstrapping transport_firmware back to device..."); + let progress = opentitanlib::app::StagedProgressBar::new(); + opentitanlib::bootstrap::Bootstrap::update_with_progress( + transport, bootstrap, &payload, &progress, + )?; + + log::info!("Waiting for Transport Firmware reboot on console..."); + let _ = UartConsole::wait_for( + &*uart, + r"Welcome to Maize on Earlgrey Transport Firmware!", + Duration::from_secs(10), + )?; + + log::info!("Connecting to DFU device running Transport Firmware..."); + let device = transport + .usb()? + .device_by_id_with_timeout(usb_vid, usb_pid, None, Duration::from_secs(10)) + .context("DFU device not found")?; + device.claim_interface(interface_num)?; + + let transfer_size = get_dfu_transfer_size(&*device, interface_num)?; + let dfu = DfuClient::new(&*device, interface_num); + log::info!( "Reading Application firmware payload from '{}'...", firmware_path @@ -175,7 +258,10 @@ fn main() -> Result<()> { run_dfu_owner_transfer_test( &transport, &args.usb, + &args.init.bootstrap.options, + &args.rom_ext, &args.firmware, + &args.transport_firmware, args.expect_reboot, args.expect_app, args.expect_owner_transfer, From 0558263f80d513d461031a87075e103283db431d Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Tue, 21 Jul 2026 04:48:23 +0800 Subject: [PATCH 13/13] tests: add updatemgr e2e owner transfer & fw update tests Add end-to-end tests for `updatemgr` covering both ownership transfer and pure firmware update scenarios via external SPI flash. The full end-to-end update flow: 1. Initial Setup: Target starts up running `eeprom_programmer_firmware` 2. Flash External EEPROM0: Host uses DFU Alt 5 (`DFU_ALT_SPI_EEPROM0`) exposed by the programmer firmware to flash the update payload (`ROM_EXT` + app) into external flash. 3. Bootstrap Transport FW: Host boostraps the device with the `transport_firmware`. 4. Telemetry Verification: `transport_firmware` boots up, detects the payload on external EEPROM0, processes owner transfer / firmware update, and outputs success telemetry over UART. Signed-off-by: Anthony Chen --- .../transport/tests/updatemgr/BUILD.bazel | 144 +++++++++ .../updatemgr/host_eeprom_owner_transfer.rs | 295 ++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 target/earlgrey/firmware/transport/tests/updatemgr/BUILD.bazel create mode 100644 target/earlgrey/firmware/transport/tests/updatemgr/host_eeprom_owner_transfer.rs diff --git a/target/earlgrey/firmware/transport/tests/updatemgr/BUILD.bazel b/target/earlgrey/firmware/transport/tests/updatemgr/BUILD.bazel new file mode 100644 index 00000000..f85ced71 --- /dev/null +++ b/target/earlgrey/firmware/transport/tests/updatemgr/BUILD.bazel @@ -0,0 +1,144 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") +load("//target/earlgrey/tooling/signing:defs.bzl", "sign_bin") +load("//third_party/lowrisc_opentitan:defs.bzl", "opentitan_rust_binary") + +package(default_visibility = ["//visibility:public"]) + +sign_bin( + name = "transport_firmware_signed", + basename = "transport_firmware_signed", + bin = "//target/earlgrey/firmware/transport:transport_firmware", + ecdsa_key = FPGA_ECDSA_KEY, +) + +opentitan_rust_binary( + name = "host_eeprom_owner_transfer", + srcs = ["host_eeprom_owner_transfer.rs"], + edition = "2024", + rustc_flags = [ + "-C", + "link-arg=-Wl,--allow-shlib-undefined", + ], + deps = [ + "//target/earlgrey/testutil", + "//third_party/lowrisc_opentitan:opentitanlib", + "//third_party/lowrisc_opentitan:usb_test_helper", + "@ot_crate_index//:anyhow", + "@ot_crate_index//:clap", + "@ot_crate_index//:log", + ], +) + +opentitan_test( + name = "updatemgr_eeprom_owner_transfer_hyper310_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", + target_data = [ + ":transport_firmware_signed", + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_transfer", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin", + ], + test_cmd = " ".join([ + "--logging=info", + "--expect-owner-transfer", + "--transport-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--new-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--new-firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_transfer)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), + test_harness = ":host_eeprom_owner_transfer", +) + +opentitan_test( + name = "updatemgr_eeprom_owner_transfer_hyper340_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", + target_data = [ + ":transport_firmware_signed", + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_transfer", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin", + ], + test_cmd = " ".join([ + "--logging=info", + "--expect-owner-transfer", + "--transport-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--new-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--new-firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_transfer)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), + test_harness = ":host_eeprom_owner_transfer", +) + +opentitan_test( + name = "updatemgr_eeprom_firmware_update_hyper310_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", + target_data = [ + ":transport_firmware_signed", + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin", + ], + test_cmd = " ".join([ + "--logging=info", + "--transport-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--new-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw310.prod_key_0.signed.bin)", + "--new-firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), + test_harness = ":host_eeprom_owner_transfer", +) + +opentitan_test( + name = "updatemgr_eeprom_firmware_update_hyper340_test", + timeout = "eternal", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = "//target/earlgrey/tests/eeprom_programmer:eeprom_programmer_firmware", + target_data = [ + ":transport_firmware_signed", + "//target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple", + "@opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin", + ], + test_cmd = " ".join([ + "--logging=info", + "--transport-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--new-rom-ext=$(rootpath @opentitan_devbundle//:rom_ext/rom_ext_usbdfu_anyversion_fpga_cw340.prod_key_0.signed.bin)", + "--new-firmware=$(rootpath //target/earlgrey/firmware/transport/tests/dfu:bootinfo_signed_simple)", + "--transport-firmware=$(rootpath :transport_firmware_signed)", + ]), + test_harness = ":host_eeprom_owner_transfer", +) diff --git a/target/earlgrey/firmware/transport/tests/updatemgr/host_eeprom_owner_transfer.rs b/target/earlgrey/firmware/transport/tests/updatemgr/host_eeprom_owner_transfer.rs new file mode 100644 index 00000000..0c52fe0d --- /dev/null +++ b/target/earlgrey/firmware/transport/tests/updatemgr/host_eeprom_owner_transfer.rs @@ -0,0 +1,295 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, ensure, Context, Result}; +use clap::Parser; +use std::path::Path; +use std::time::Duration; + +use earlgrey_testutil::{get_dfu_transfer_size, print_uart, sequence_dfu_download, DfuClient}; +use opentitanlib::app::TransportWrapper; +use opentitanlib::image::image::{Image, ImageAssembler}; +use opentitanlib::io::uart::Uart; +use opentitanlib::io::usb::UsbDevice; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; +use opentitanlib::util::file::FromReader; +use usb::UsbOpts; + +#[derive(Parser, Debug)] +struct CmdArgs { + #[command(flatten)] + init: InitializeTest, + + #[command(flatten)] + usb: UsbOpts, + + #[arg(long)] + transport_rom_ext: String, + + #[arg(long)] + new_rom_ext: String, + + #[arg(long)] + new_firmware: String, + + #[arg(long)] + transport_firmware: String, + + #[arg(long, default_value = "false")] + expect_owner_transfer: bool, +} + +fn setup_usb(transport: &TransportWrapper, usb: &UsbOpts) -> Result<()> { + usb.apply_strappings(transport, true)?; + if usb.vbus_control_available() { + usb.enable_vbus(transport, true)?; + } + if usb.vbus_sense_available() { + if !usb.vbus_present(transport)? { + bail!("OT USB does not appear to be connected to a host (VBUS not detected)"); + } + } + Ok(()) +} + +fn connect_dfu_device(transport: &TransportWrapper, usb: &UsbOpts) -> Result> { + log::info!( + "Waiting for DFU device (VID={:04x}, PID={:04x})...", + usb.vid, + usb.pid + ); + transport + .usb()? + .device_by_id_with_timeout(usb.vid, usb.pid, None, Duration::from_secs(10)) + .context("DFU device not found") +} + +fn get_firmware_assembly_offset(firmware_path: &str) -> Result { + let owner_fw_image = Image::read_from_file(Path::new(firmware_path)) + .with_context(|| format!("Failed to read owner firmware image at '{firmware_path}'"))?; + let image_bytes = owner_fw_image.bytes(); + ensure!( + image_bytes.len() >= 816, + "Owner firmware image at '{firmware_path}' is too small to contain a manifest header" + ); + + // Extract manifest_base_address directly from offset 812 (0x32c) of the owner firmware's manifest header + // because: + // 1) `opentitanlib::image::manifest::Manifest` (from external @lowrisc_opentitan) does not expose + // `manifest_base_address` as a public field on `&Manifest` in the pinned revision in MODULE.bazel. + // 2) `earlgrey_util::manifest::Manifest` (from //target/earlgrey/util) exposes `pub manifest_base_address`, + // but parsing it via zerocopy traits fails due to a crate version mismatch between zerocopy v0.8.50 + // (used by `earlgrey_util`) and v0.8.26 (imported via `@ot_crate_index`). + // TODO: Parse via Manifest struct once (1) opentitanlib is updated or (2) zerocopy crate versions are unified. + // Offset 812 is guaranteed by the Earlgrey/OpenTitan manifest binary specification. + let manifest_base_addr = u32::from_le_bytes(image_bytes[812..816].try_into().unwrap()); + if manifest_base_addr == 0xa5a5a5a5 { + Ok(0x10000) + } else if manifest_base_addr >= 0x100000 { + Ok((manifest_base_addr & 0xfffff) as usize) + } else { + Ok(manifest_base_addr as usize) + } +} + +fn flash_eeprom_update_payload( + device: &dyn UsbDevice, + dfu: &DfuClient, + uart: &dyn Uart, + new_rom_ext_path: &str, + new_firmware_path: &str, + transfer_size: u16, + interface_num: u8, +) -> Result<()> { + log::info!("Setting USB DFU Alt setting to 5 (SPI EEPROM 0)..."); + device.set_alternate_setting(interface_num, 5)?; + + let firmware_offset = get_firmware_assembly_offset(new_firmware_path)?; + log::info!( + "Assembling image: ROM Ext ('{}') @ 0, Firmware ('{}') @ {:#x}...", + new_rom_ext_path, + new_firmware_path, + firmware_offset + ); + let mut image_assembler = ImageAssembler::with_params(0x100000, false); + image_assembler.parse(&[ + format!("{}@0", new_rom_ext_path), + format!("{}@{:#x}", new_firmware_path, firmware_offset), + ])?; + let mut test_data = image_assembler.assemble()?; + + if let Ok(image) = Image::from_reader(&test_data[..]) { + if let Ok(subimages) = image.subimages() { + if let Some(last_subimage) = subimages.last() { + let actual_end = last_subimage.offset + last_subimage.manifest.length as usize; + // Round up to next 2KiB (2048 bytes) alignment. + let aligned_len = (actual_end + 2047) & !2047; + if aligned_len < test_data.len() { + log::info!( + "Optimizing DFU payload size: found {} subimages. Truncating payload from {} bytes to {} bytes (actual payload end: 0x{:x}, 2KiB aligned)", + subimages.len(), + test_data.len(), + aligned_len, + actual_end + ); + test_data.truncate(aligned_len); + } + } + } + } + + log::info!( + "Sequencing DFU Download of assembled image ({} bytes) to EEPROM0...", + test_data.len() + ); + sequence_dfu_download(dfu, uart, &test_data, transfer_size, false) +} + +fn flash_transport_firmware( + transport: &TransportWrapper, + bootstrap: &opentitanlib::bootstrap::BootstrapOptions, + transport_rom_ext_path: &str, + transport_firmware_path: &str, +) -> Result<()> { + let firmware_offset = get_firmware_assembly_offset(transport_firmware_path)?; + log::info!( + "Assembling transport_firmware image: ROM Ext ('{}') @ 0, Firmware ('{}') @ {:#x}...", + transport_rom_ext_path, + transport_firmware_path, + firmware_offset + ); + let mut image_assembler = ImageAssembler::with_params(0x100000, true); + image_assembler.parse(&[ + format!("{}@0", transport_rom_ext_path), + format!("{}@{:#x}", transport_firmware_path, firmware_offset), + ])?; + let payload = image_assembler.assemble()?; + + log::info!("Bootstrapping transport_firmware back to device..."); + let progress = opentitanlib::app::StagedProgressBar::new(); + opentitanlib::bootstrap::Bootstrap::update_with_progress( + transport, bootstrap, &payload, &progress, + )?; + Ok(()) +} + +fn verify_telemetry(uart: &dyn Uart, expect_owner_transfer: bool) -> Result<()> { + log::info!("Waiting for Application Execution telemetry on UART..."); + if expect_owner_transfer { + let _ = UartConsole::wait_for(uart, r"ownership_transfers: 1", Duration::from_secs(20)) + .context("Failed to detect ownership_transfers: 1 in UART telemetry!")?; + log::info!("✅ Detected ownership_transfers: 1"); + + let _ = UartConsole::wait_for(uart, r"config_version: 1", Duration::from_secs(5)) + .context("Failed to detect config_version: 1 in UART telemetry!")?; + log::info!("✅ Detected config_version: 1"); + + let _ = UartConsole::wait_for( + uart, + r"update_mode: SELV \(0x564c4553\)", + Duration::from_secs(5), + ) + .context("Failed to detect update_mode: SELV in UART telemetry!")?; + log::info!("✅ Detected update_mode: SELV (0x564c4553)"); + } else { + let _ = UartConsole::wait_for(uart, r"ownership_transfers: 0", Duration::from_secs(20)) + .context("Failed to detect ownership_transfers: 0 in UART telemetry!")?; + log::info!("✅ Detected ownership_transfers: 0"); + + let _ = UartConsole::wait_for(uart, r"config_version: 1", Duration::from_secs(5)) + .context("Failed to detect config_version: 1 in UART telemetry!")?; + log::info!("✅ Detected config_version: 1"); + + let _ = UartConsole::wait_for( + uart, + r"update_mode: ANYV \(0x56594e41\)", + Duration::from_secs(5), + ) + .context("Failed to detect update_mode: ANYV in UART telemetry!")?; + log::info!("✅ Detected update_mode: ANYV (0x56594e41)"); + } + + let _ = UartConsole::wait_for(uart, r"✅ PASSED bootinfo test", Duration::from_secs(5)) + .context("Failed to detect ✅ PASSED bootinfo test in UART telemetry!")?; + log::info!("✅ Detected 'PASSED bootinfo test'!"); + + print_uart(uart); + Ok(()) +} + +fn run_dfu_eeprom_owner_transfer_test( + transport: &TransportWrapper, + usb: &UsbOpts, + bootstrap: &opentitanlib::bootstrap::BootstrapOptions, + transport_rom_ext_path: &str, + new_rom_ext_path: &str, + new_firmware_path: &str, + transport_firmware_path: &str, + expect_owner_transfer: bool, +) -> Result<()> { + let uart = transport.uart("console")?; + + log::info!("Resetting target running eeprom_programmer_firmware..."); + transport.reset(opentitanlib::app::UartRx::Clear)?; + + log::info!("Waiting for Maize Welcome on console..."); + let _ = UartConsole::wait_for( + &*uart, + r"Welcome to Maize on Earlgrey EEPROM Programmer Firmware!", + Duration::from_secs(10), + )?; + + setup_usb(transport, usb)?; + + let device = connect_dfu_device(transport, usb)?; + let interface_num = 2; + device.claim_interface(interface_num)?; + + let transfer_size = get_dfu_transfer_size(&*device, interface_num)?; + let dfu = DfuClient::new(&*device, interface_num); + + flash_eeprom_update_payload( + &*device, + &dfu, + &*uart, + new_rom_ext_path, + new_firmware_path, + transfer_size, + interface_num, + )?; + + let _ = device.release_interface(interface_num); + log::info!("EEPROM DFU download complete. Released DFU interface."); + + flash_transport_firmware( + transport, + bootstrap, + transport_rom_ext_path, + transport_firmware_path, + )?; + + verify_telemetry(&*uart, expect_owner_transfer)?; + + log::info!("Test Execution Finished Successfully!"); + Ok(()) +} + +fn main() -> Result<()> { + let args = CmdArgs::parse(); + args.init.init_logging(); + + let transport = args.init.init_target()?; + + run_dfu_eeprom_owner_transfer_test( + &transport, + &args.usb, + &args.init.bootstrap.options, + &args.transport_rom_ext, + &args.new_rom_ext, + &args.new_firmware, + &args.transport_firmware, + args.expect_owner_transfer, + )?; + Ok(()) +}