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/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; diff --git a/target/earlgrey/firmware/transport/BUILD.bazel b/target/earlgrey/firmware/transport/BUILD.bazel index c74631c7..cdb69080 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", @@ -85,10 +86,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 +101,7 @@ rust_process( "//util/zfmt", "@pigweed//pw_kernel/userspace", "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", "@zfmt//zfmt", ], ) @@ -111,9 +117,41 @@ rust_process( tags = ["kernel"], visibility = ["//visibility:public"], deps = [ + "//hal/blocking/flash", + "//services/flash:client", "//target/earlgrey/services/sysmgr:server", + "//target/earlgrey/util", "//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", @@ -145,6 +183,7 @@ multi_process_app( processes = [ ":logmgr", ":sysmgr", + ":updatemgr", ":platform", ":flash_server", ":usbmgr", 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/flash_server.rs b/target/earlgrey/firmware/transport/flash_server.rs index 0918274c..2ddd1411 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,90 @@ 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_UPDATEMGR_SERVICE, + syscall::Signals::READABLE, + 1, // token 1 = EFlash updatemgr + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::EFLASH_USB_SERVICE, + syscall::Signals::READABLE, + 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 ipc = IpcHandle::new(handle::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 { - 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_updatemgr_ipc, &mut buf)?; + } else if token == 2 { + 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 5373985c..89b4b57e 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,41 @@ 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: "flash_updatemgr", + type: "channel_initiator", + handler_process: "flash_server", + handler_object_name: "eflash_updatemgr_service" + }, + { + name: "updatemgr_thread", + kernel_stack_size_bytes: 4096, + type: "thread" } ] }, @@ -142,9 +189,25 @@ handler_object_name: "logger_flash" }, { - name: "flash_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_usb_service", type: "channel_handler" }, + { + name: "flash_wait_group", + type: "wait_group" + }, { name: "flash_interrupts", type: "interrupt", @@ -169,6 +232,12 @@ type: "device", start_address: 0x41000000, size_bytes: 0x200 + }, + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000 } ] }, @@ -215,13 +284,19 @@ name: "flash_usb", type: "channel_initiator", handler_process: "flash_server", - handler_object_name: "flash_service" + handler_object_name: "eflash_usb_service" + }, + { + name: "spi_flash_usb", + type: "channel_initiator", + handler_process: "flash_server", + 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/tests/dfu/BUILD.bazel b/target/earlgrey/firmware/transport/tests/dfu/BUILD.bazel index 354b9cc6..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,52 @@ 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 = " ".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", +) + +opentitan_test( + name = "dfu_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 = [ ":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", ) @@ -93,11 +140,49 @@ 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 = " ".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", +) + +opentitan_test( + name = "dfu_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 = [ ":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, 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(()) +} diff --git a/target/earlgrey/firmware/transport/updatemgr.rs b/target/earlgrey/firmware/transport/updatemgr.rs new file mode 100644 index 00000000..3f348429 --- /dev/null +++ b/target/earlgrey/firmware/transport/updatemgr.rs @@ -0,0 +1,297 @@ +// 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 earlgrey_util::tags::OwnershipUpdateMode; +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; +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! 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 { + rom_ext_staging_slot: u32, + rom_ext_offset: u32, + owner_staging_slot: u32, + owner_offset: u32, +} + +#[derive(Zfmt)] +#[zfmt(format = "Update manager attempt failed: 0x{status:08x}. Retrying in 1s...")] +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 {} +#[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()?; + 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 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; + + 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; + } + + 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, + 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 { + 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, + work_buf, + ) + .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, + work_buf, + ) + .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 + }); + + 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 {}); + } + + 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 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, + &mut spi_flash_client, + &update, + &mut work_buf, + ) { + Ok(()) => break, + Err(e) => { + util_zfmt::warn!(UpdateAttemptFailed { status: e.0.get() }); + 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(()) +} + +#[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/firmware/transport/usbmgr.rs b/target/earlgrey/firmware/transport/usbmgr.rs index bc76db32..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,7 +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 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); 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/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/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/tests/eeprom_programmer/host_usb_dfu_spi_flash.rs b/target/earlgrey/tests/eeprom_programmer/host_usb_dfu_spi_flash.rs new file mode 100644 index 00000000..2834d11c --- /dev/null +++ b/target/earlgrey/tests/eeprom_programmer/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 EEPROM Programmer 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/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/tests/spi_flash/BUILD.bazel b/target/earlgrey/tests/spi_flash/BUILD.bazel new file mode 100644 index 00000000..114bc229 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/BUILD.bazel @@ -0,0 +1,148 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "combined_flash_server", + srcs = [ + "combined_flash_server.rs", + ], + codegen_crate_name = "combined_flash_server_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:spi_flash", + "//hal/blocking/flash", + "//hal/blocking/flash:driver", + "//services/flash:server", + "//target/earlgrey/drivers:eflash_driver", + "//target/earlgrey/drivers:spi_host", + "//target/earlgrey/registers:flash_ctrl_core", + "//target/earlgrey/registers:spi_host", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/panic", + "//util/types", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + ], +) + +rust_app( + name = "spi_flash_test", + srcs = [ + "spi_flash_test.rs", + ], + codegen_crate_name = "spi_flash_test_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking/flash", + "//services/flash:client", + "//target/earlgrey/util", + "//util/error", + "//util/ipc", + "//util/misc", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "spi_flash", + apps = [ + ":combined_flash_server", + ":spi_flash_test", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_test( + name = "spi_flash_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_flash", +) + +opentitan_test( + name = "spi_flash_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_flash", +) + +rust_binary_no_panics_test( + name = "no_panics", + apps = [ + "combined_flash_server", + "spi_flash_test", + ], + binary = ":spi_flash", +) diff --git a/target/earlgrey/tests/spi_flash/combined_flash_server.rs b/target/earlgrey/tests/spi_flash/combined_flash_server.rs new file mode 100644 index 00000000..921c297c --- /dev/null +++ b/target/earlgrey/tests/spi_flash/combined_flash_server.rs @@ -0,0 +1,145 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use combined_flash_server_codegen::{handle, signals}; +use earlgrey_util::EarlgreyFlashAddress; +use eflash_driver::{EmbeddedFlash, Permission}; +use hal_flash::BlockingFlash; +use hal_flash_driver::FlashAddress; +use pw_status::Error; +use services_flash_server::FlashIpcServer; +use spi_flash::SpiFlash; +use spi_host::SpiHost0; +use userspace::time::Instant; +use userspace::{entry, syscall}; +use util_error::ErrorCode; +use util_ipc::IpcHandle; +use util_types::Blocking; + +// EFlash Interrupt Blocker +struct FlashCtrlInterrupt; + +impl Blocking for FlashCtrlInterrupt { + fn wait_for_notification(&self) { + loop { + if let Ok(w) = syscall::object_wait( + handle::FLASH_INTERRUPTS, + signals::FLASH_CTRL_OP_DONE, + Instant::MAX, + ) { + if w.pending_signals.contains(signals::FLASH_CTRL_OP_DONE) { + break; + } + } + } + let _ = syscall::interrupt_ack(handle::FLASH_INTERRUPTS, signals::FLASH_CTRL_OP_DONE); + } +} + +fn run_server() -> Result<(), ErrorCode> { + // 1. Initialize EFlash driver. + pw_log::info!("combined_server: initializing EFlash driver"); + // SAFETY: We have exclusive access to FlashCtrl in this test process. + let mut eflash_driver = + EmbeddedFlash::new_with_interrupts(unsafe { flash_ctrl_core::FlashCtrl::new() }); + eflash_driver.set_default_permission(Permission::FULL_ACCESS); + // Grant info page permissions as well (same as standard eflash server) + for i in 5..9 { + eflash_driver.set_info_permission(FlashAddress::info(0, i, 0), Permission::FULL_ACCESS)?; + eflash_driver.set_info_permission(FlashAddress::info(1, i, 0), Permission::FULL_ACCESS)?; + } + + let eflash = BlockingFlash { + driver: eflash_driver, + blocking: FlashCtrlInterrupt, + }; + let mut eflash_server = FlashIpcServer::new(eflash); + + // 2. Initialize SPI Host. + pw_log::info!("combined_server: initializing SPI Host"); + // SAFETY: We have exclusive access to SPI_HOST0 in this test process. + let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; + let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + pw_log::error!( + "combined_server: SPI Host init failed: 0x{:x}", + u32::from(ErrorCode::from(e)) + ); + return Err(ErrorCode::from(e)); + } + + // 3. Initialize SpiFlash driver. + pw_log::info!("combined_server: initializing SpiFlash driver"); + let mut spi_flash = SpiFlash::new(spi_host); + if let Err(e) = spi_flash.init() { + pw_log::error!( + "combined_server: SPI Flash init failed: 0x{:x}", + u32::from(e) + ); + return Err(e); + } + let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + // 4. Register wait group ports. + pw_log::info!("combined_server: registering wait group ports"); + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::EFLASH_SERVICE, + syscall::Signals::READABLE, + handle::EFLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_SERVICE, + syscall::Signals::READABLE, + handle::SPI_FLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + let mut buf = [0u8; 2064]; + let eflash_ipc = IpcHandle::new(handle::EFLASH_SERVICE); + let spi_flash_ipc = IpcHandle::new(handle::SPI_FLASH_SERVICE); + + // 5. Enter main wait_group loop. + pw_log::info!("combined_server: entering main wait_group loop"); + loop { + let wait_result = syscall::object_wait( + handle::FLASH_WAIT_GROUP, + syscall::Signals::READABLE, + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + + let token = wait_result.user_data; + if token == handle::EFLASH_SERVICE as usize { + eflash_server.handle_one(&eflash_ipc, &mut buf)?; + } else if token == handle::SPI_FLASH_SERVICE as usize { + spi_flash_server.handle_one(&spi_flash_ipc, &mut buf)?; + } + } +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 COMBINED FLASH SERVER START"); + let ret = run_server(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ COMBINED FLASH SERVER PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ COMBINED FLASH SERVER FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/spi_flash_test.rs b/target/earlgrey/tests/spi_flash/spi_flash_test.rs new file mode 100644 index 00000000..49a2edb8 --- /dev/null +++ b/target/earlgrey/tests/spi_flash/spi_flash_test.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use pw_status::Error; +use spi_flash_test_codegen::handle; +use userspace::entry; + +use earlgrey_util::EarlgreyFlashAddress; +use hal_flash::{Flash, FlashAddress}; +use services_flash_client::FlashIpcClient; +use util_error::{ErrorCode, KERNEL_ERROR_INTERNAL}; +use util_ipc::IpcHandle; + +fn erase_program_test( + flash: &mut FlashIpcClient, + addr: FlashAddress, + flash_type: &str, +) -> Result<(), ErrorCode> { + let (_total_size, page_size, _erasable_sizes_bitmap) = flash.geometry()?; + pw_log::info!( + "[{}] Erasing at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.erase(addr, page_size)?; + + pw_log::info!("[{}] Reading after erase...", flash_type); + let mut buf = [0u8; 32]; + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + for &byte in buf.iter() { + if byte != 0xFF { + pw_log::error!( + "[{}] Erase check failed: byte is 0x{:02x}, expected 0xFF", + flash_type, + byte + ); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + let payload = b"Dual Flash IPC Test Payload!!! "; // 32 bytes (aligned) + pw_log::info!( + "[{}] Programming 32 bytes at offset 0x{:08x}...", + flash_type, + addr.offset() + ); + flash.program(addr, payload)?; + + pw_log::info!("[{}] Reading back program results...", flash_type); + flash.read(addr, &mut buf)?; + util_misc::hexdump(&buf); + + if buf != *payload { + pw_log::error!("[{}] Verify failed: content mismatch", flash_type); + return Err(KERNEL_ERROR_INTERNAL); + } + pw_log::info!("[{}] Program verified successfully!", flash_type); + Ok(()) +} + +fn flash_test() -> Result<(), ErrorCode> { + pw_log::info!("--- Testing Internal EFlash ---"); + let mut eflash = FlashIpcClient::new(IpcHandle::new(handle::EFLASH_SERVICE))?; + let (total_size, page_size, _) = eflash.geometry()?; + pw_log::info!( + "EFlash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on Slot B area (offset 0x90000) + erase_program_test(&mut eflash, FlashAddress::data(0x0009_0000), "EFlash")?; + + pw_log::info!("--- Testing External SPI Flash ---"); + let mut spi_flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_SERVICE))?; + let (total_size, page_size, _) = spi_flash.geometry()?; + pw_log::info!( + "SPI Flash size: {} bytes, page size: {} bytes", + total_size.get(), + page_size.get() + ); + // Test on 1MB offset + erase_program_test(&mut spi_flash, FlashAddress::new(0x0010_0000), "SpiFlash")?; + + Ok(()) +} + +#[entry] +fn entry() -> Result<(), Error> { + pw_log::info!("🔄 DUAL FLASH TEST CLIENT START"); + let ret = flash_test(); + + let ret = match ret { + Ok(()) => { + pw_log::info!("✅ DUAL FLASH TEST CLIENT PASS"); + Ok(()) + } + Err(e) => { + pw_log::error!("❌ DUAL FLASH TEST CLIENT FAIL: {:08x}", u32::from(e)); + Err(Error::Unknown) + } + }; + + ret +} + +util_panic::make_panic_handler!(); diff --git a/target/earlgrey/tests/spi_flash/system.json5 b/target/earlgrey/tests/spi_flash/system.json5 new file mode 100644 index 00000000..e89a685b --- /dev/null +++ b/target/earlgrey/tests/spi_flash/system.json5 @@ -0,0 +1,98 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "combined_flash_server", + flash_size_bytes: 32768, + processes: [ + { + name: "combined_flash_server", + ram_size_bytes: 8192, + objects: [ + { + name: "eflash_service", + type: "channel_handler", + }, + { + name: "spi_flash_service", + type: "channel_handler", + }, + { + name: "flash_wait_group", + type: "wait_group", + }, + { + name: "flash_interrupts", + type: "interrupt", + irqs: [ + { name: "flash_ctrl_op_done", number: 164 }, + ], + }, + { + name: "server_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + memory_mappings: [ + { + name: "flash_ctrl_core", + type: "device", + start_address: 0x41000000, + size_bytes: 0x200, + }, + { + name: "spi_host0", + type: "device", + start_address: 0x40300000, + size_bytes: 0x1000, + } + ], + }, + ], + }, + + { + name: "spi_flash_test", + flash_size_bytes: 16384, + processes: [ + { + name: "spi_flash_test", + ram_size_bytes: 8192, + objects: [ + { + name: "flash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_flash_service", + }, + { + name: "eflash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "eflash_service", + }, + { + name: "spi_test_thread", + kernel_stack_size_bytes: 2048, + type: "thread", + } + ], + }, + ], + }, + ], +} diff --git a/target/earlgrey/tests/spi_flash/target.rs b/target/earlgrey/tests/spi_flash/target.rs new file mode 100644 index 00000000..ec71c87e --- /dev/null +++ b/target/earlgrey/tests/spi_flash/target.rs @@ -0,0 +1,29 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey SPI Flash test"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); diff --git a/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) +} 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) diff --git a/target/earlgrey/util/BUILD.bazel b/target/earlgrey/util/BUILD.bazel index a3011502..70264548 100644 --- a/target/earlgrey/util/BUILD.bazel +++ b/target/earlgrey/util/BUILD.bazel @@ -44,6 +44,22 @@ 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", + "//util/io", + "@rust_crates//:zerocopy", + ], +) + rust_library( name = "dfu", srcs = ["dfu.rs"], @@ -52,6 +68,7 @@ rust_library( target_compatible_with = TARGET_COMPATIBLE_WITH, visibility = ["//visibility:public"], deps = [ + ":fw_update", ":util", "//hal/blocking/flash", "//hal/blocking/usb:hal_usb", 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/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 new file mode 100644 index 00000000..24ac507d --- /dev/null +++ b/target/earlgrey/util/fw_update.rs @@ -0,0 +1,195 @@ +// 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::error::EG_ERROR_BOOT_SLOT_UNKNOWN; +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; +use zerocopy::FromBytes; + +/// 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, + /// 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 { + /// 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, + rom_ext_size: info.rom_ext.size as usize, + app_size: info.app.size as usize, + }) + } + + /// 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!(), + } + } + + /// 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); + } + + 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, + })) + } +} + +pub struct FirmwareBundle { + pub offset: usize, + pub rom_ext_len: usize, + pub owner_len: usize, + pub owner_block_offset: Option, +} 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")); +} 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 =