From e6e433761e0dc46c561e7c93e7100d958989a6ac Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Tue, 11 Aug 2026 20:47:37 +0800 Subject: [PATCH 1/4] earlgrey: Add SPI Device driver and smoke test - Implements the SpiDev driver using ureg RegisterBlock representation. - Provides configure_cmd_info for hardware opcode intercept slots (SFDP, JEDEC ID, Read, Program, Erase). - Configures intercept_en, jedec_cc, jedec_id, addr_mode, and SRAM egress buffer. - Provides set_sfdp, write_to_mbx, poll, retire_cmd, and read_addr_swap. - Adds single-process driver smoke test validating register and SRAM egress state. Signed-off-by: Anthony Chen --- drivers/flash/BUILD.bazel | 9 + drivers/flash/opcode.rs | 103 +++ drivers/flash/spi_flash.rs | 23 +- target/earlgrey/drivers/BUILD.bazel | 16 + target/earlgrey/drivers/spi_device.rs | 680 ++++++++++++++++++ .../tests/drivers/spi_device/BUILD.bazel | 103 +++ .../tests/drivers/spi_device/spi_device.rs | 113 +++ .../tests/drivers/spi_device/system.json5 | 41 ++ .../tests/drivers/spi_device/target.rs | 30 + 9 files changed, 1096 insertions(+), 22 deletions(-) create mode 100644 drivers/flash/opcode.rs create mode 100644 target/earlgrey/drivers/spi_device.rs create mode 100644 target/earlgrey/tests/drivers/spi_device/BUILD.bazel create mode 100644 target/earlgrey/tests/drivers/spi_device/spi_device.rs create mode 100644 target/earlgrey/tests/drivers/spi_device/system.json5 create mode 100644 target/earlgrey/tests/drivers/spi_device/target.rs diff --git a/drivers/flash/BUILD.bazel b/drivers/flash/BUILD.bazel index 884455692..f9399b770 100644 --- a/drivers/flash/BUILD.bazel +++ b/drivers/flash/BUILD.bazel @@ -3,6 +3,14 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +rust_library( + name = "opcode", + srcs = ["opcode.rs"], + crate_name = "spi_flash_opcode", + edition = "2024", + visibility = ["//visibility:public"], +) + rust_library( name = "spi_flash", srcs = ["spi_flash.rs"], @@ -13,6 +21,7 @@ rust_library( ], visibility = ["//visibility:public"], deps = [ + ":opcode", "//hal/blocking/flash", "//hal/blocking/flash:driver", "//util/error", diff --git a/drivers/flash/opcode.rs b/drivers/flash/opcode.rs new file mode 100644 index 000000000..7e2dc91c7 --- /dev/null +++ b/drivers/flash/opcode.rs @@ -0,0 +1,103 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Standard SPI Flash opcodes. + +#![no_std] + +use core::ops::Deref; + +/// Standard SPI Flash Command Opcode. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +#[repr(transparent)] +pub struct Opcode(pub u8); + +impl Opcode { + // Read commands + pub const READ: Self = Self(0x03); + pub const FAST_READ: Self = Self(0x0B); + pub const FAST_QUAD_READ: Self = Self(0x6B); + pub const READ_4B: Self = Self(0x13); + pub const FAST_QUAD_READ_4B: Self = Self(0x6C); + + // Program commands + pub const PAGE_PROGRAM: Self = Self(0x02); + pub const PAGE_PROGRAM_QUAD: Self = Self(0x32); + pub const PAGE_PROGRAM_4B: Self = Self(0x12); + pub const PAGE_PROGRAM_QUAD_4B: Self = Self(0x34); + + // Erase commands + pub const SECTOR_ERASE: Self = Self(0x20); + pub const SECTOR_ERASE_4B: Self = Self(0x21); + pub const BLOCK_ERASE_32K: Self = Self(0x52); + pub const BLOCK_ERASE_32K_4B: Self = Self(0x5C); + pub const BLOCK_ERASE_64K: Self = Self(0xD8); + pub const BLOCK_ERASE_64K_4B: Self = Self(0xDC); + pub const CHIP_ERASE: Self = Self(0xC7); + pub const CHIP_ERASE2: Self = Self(0x60); + + // Control and Status commands + pub const WRITE_ENABLE: Self = Self(0x06); + pub const WRITE_DISABLE: Self = Self(0x04); + pub const READ_STATUS: Self = Self(0x05); + pub const WRITE_STATUS: Self = Self(0x01); + pub const WRITE_EAR: Self = Self(0xC5); + pub const ENTER_4B_ADDR_MODE: Self = Self(0xB7); + pub const EXIT_4B_ADDR_MODE: Self = Self(0xE9); + pub const RESET_ENABLE: Self = Self(0x66); + pub const RESET: Self = Self(0x99); + + // Identification and Parameters commands + pub const SFDP: Self = Self(0x5A); + pub const JEDEC_ID: Self = Self(0x9F); +} + +impl Deref for Opcode { + type Target = u8; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Opcode { + fn from(val: u8) -> Self { + Self(val) + } +} + +impl From for u8 { + fn from(val: Opcode) -> Self { + val.0 + } +} + +impl From for u32 { + fn from(val: Opcode) -> Self { + val.0 as u32 + } +} + +// Backward-compatible u8 constants mapped to canonical Opcode definitions +pub const OP_STATUS: u8 = Opcode::READ_STATUS.0; +pub const OP_WRITE_EN: u8 = Opcode::WRITE_ENABLE.0; +pub const OP_WR_STATUS: u8 = Opcode::WRITE_STATUS.0; +pub const OP_WR_EAR: u8 = Opcode::WRITE_EAR.0; +pub const OP_READ: u8 = Opcode::READ.0; +pub const OP_QREAD: u8 = Opcode::FAST_QUAD_READ.0; +pub const OP_READ4B: u8 = Opcode::READ_4B.0; +pub const OP_QREAD4B: u8 = Opcode::FAST_QUAD_READ_4B.0; +pub const OP_CHIP_ERASE: u8 = Opcode::CHIP_ERASE.0; +pub const OP_ERASE_4K: u8 = Opcode::SECTOR_ERASE.0; +pub const OP_ERASE4B_4K: u8 = Opcode::SECTOR_ERASE_4B.0; +pub const OP_ERASE_64K: u8 = Opcode::BLOCK_ERASE_64K.0; +pub const OP_ERASE4B_64K: u8 = Opcode::BLOCK_ERASE_64K_4B.0; +pub const OP_PROGRAM: u8 = Opcode::PAGE_PROGRAM.0; +pub const OP_QPROGRAM: u8 = Opcode::PAGE_PROGRAM_QUAD.0; +pub const OP_PROGRAM4B: u8 = Opcode::PAGE_PROGRAM_4B.0; +pub const OP_QPROGRAM4B: u8 = Opcode::PAGE_PROGRAM_QUAD_4B.0; +pub const OP_SFDP_READ: u8 = Opcode::SFDP.0; +pub const OP_RESET_ENABLE: u8 = Opcode::RESET_ENABLE.0; +pub const OP_RESET: u8 = Opcode::RESET.0; +pub const OP_READ_JEDEC_ID: u8 = Opcode::JEDEC_ID.0; +pub const OP_ENTER_4B_ADDR_MODE: u8 = Opcode::ENTER_4B_ADDR_MODE.0; diff --git a/drivers/flash/spi_flash.rs b/drivers/flash/spi_flash.rs index d70c5647d..7a38e8406 100644 --- a/drivers/flash/spi_flash.rs +++ b/drivers/flash/spi_flash.rs @@ -462,28 +462,7 @@ pub struct Status { 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; +pub use spi_flash_opcode::*; /// A RandomRead implementation that can be used to access SFDP bytes. pub struct SfdpRandRead<'a, S: embedded_hal::spi::SpiDevice> { diff --git a/target/earlgrey/drivers/BUILD.bazel b/target/earlgrey/drivers/BUILD.bazel index 92c80c76b..f94690cbd 100644 --- a/target/earlgrey/drivers/BUILD.bazel +++ b/target/earlgrey/drivers/BUILD.bazel @@ -104,3 +104,19 @@ rust_library( "@ureg", ], ) + +rust_library( + name = "spi_device", + srcs = ["spi_device.rs"], + crate_name = "earlgrey_spi_device", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//drivers/flash:opcode", + "//target/earlgrey/registers:spi_device", + "//util/error", + "//util/regcpy", + "@rust_crates//:aligned", + "@ureg", + ], +) diff --git a/target/earlgrey/drivers/spi_device.rs b/target/earlgrey/drivers/spi_device.rs new file mode 100644 index 000000000..24204e823 --- /dev/null +++ b/target/earlgrey/drivers/spi_device.rs @@ -0,0 +1,680 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! OpenTitan SPI Device driver for Earlgrey. + +#![no_std] + +use aligned::{Aligned, A4}; +use util_error::ErrorCode; +use util_regcpy::{copy_from_reg_array, copy_to_reg_array}; + +// SRAM buffer constants are based upon the OpenTitan programmers guide: +// https://opentitan.org/book/hw/ip/spi_device/doc/programmers_guide.html + +// Egress buffer constants +pub const MAILBOX_START_WORDS: usize = 0x800 / 4; +pub const MAILBOX_LEN_WORDS: usize = 1024 / 4; +pub const SFDP_START_WORDS: usize = 0xC00 / 4; +pub const SFDP_LEN_WORDS: usize = 256 / 4; + +// Ingress buffer constants +pub const PAYLOAD_FIFO_START_WORDS: usize = 0; +pub const PAYLOAD_FIFO_LEN_WORDS: usize = 256 / 4; + +// Command info list slots +pub const CMD_INFO_READ_STATUS: u8 = 0; +pub const CMD_INFO_JEDEC: u8 = 3; +pub const CMD_INFO_SFDP: u8 = 4; +pub const CMD_INFO_READ: u8 = 5; +pub const CMD_INFO_FASTREAD: u8 = 6; +pub const CMD_INFO_READ4B: u8 = 7; +pub const CMD_INFO_FAST_QUAD_READ: u8 = 8; +pub const CMD_INFO_FAST_QUAD_READ4B: u8 = 9; +pub const CMD_INFO_PAGEPROGRAM: u8 = 11; +pub const CMD_INFO_PAGEPROGRAM4B: u8 = 12; +pub const CMD_INFO_SECTORERASE: u8 = 13; +pub const CMD_INFO_SECTORERASE4B: u8 = 14; +pub const CMD_INFO_BLOCKERASE32K: u8 = 15; +pub const CMD_INFO_BLOCKERASE32K4B: u8 = 16; +pub const CMD_INFO_BLOCKERASE64K: u8 = 17; +pub const CMD_INFO_BLOCKERASE64K4B: u8 = 18; +pub const CMD_INFO_CHIPERASE: u8 = 19; +pub const CMD_INFO_CHIPERASE2: u8 = 20; +pub const CMD_INFO_PAGEPROGRAMQUAD: u8 = 21; +pub const CMD_INFO_PAGEPROGRAMQUAD4B: u8 = 22; + +pub use spi_flash_opcode::Opcode as SpiFlashOpcode; + +pub struct SpiDev { + mmio: spi_device::RegisterBlock>, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum SpiPayloadIoCfg { + SingleIoIn, // Payload is sent on the MOSI line (IO[0]) + SingleIoOut, // Payload is returned on the MISO line (IO[1]) + DualIoIn, + DualIoOut, + QuadIoIn, + QuadIoOut, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum AddrMode { + CFG, + _3B, + _4B, +} + +pub struct SpiFlashCmdCfg { + pub opcode: SpiFlashOpcode, + pub upload: bool, + pub busy: bool, + pub payload_io: Option, + pub addr_mode: Option, + pub dummy_cyc: u8, + pub filter: bool, +} + +impl SpiFlashCmdCfg { + pub const JEDEC_ID: Self = Self { + opcode: SpiFlashOpcode::JEDEC_ID, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: None, + dummy_cyc: 0, + filter: true, + }; + + pub const READ_STATUS: Self = Self { + opcode: SpiFlashOpcode::READ_STATUS, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: None, + dummy_cyc: 0, + filter: false, + }; + + pub const READ: Self = Self { + opcode: SpiFlashOpcode::READ, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: false, + }; + + pub const FAST_READ: Self = Self { + opcode: SpiFlashOpcode::FAST_READ, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 8, + filter: false, + }; + + pub const FAST_QUAD_READ: Self = Self { + opcode: SpiFlashOpcode::FAST_QUAD_READ, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::QuadIoOut), + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 8, + filter: false, + }; + + pub const READ_4B: Self = Self { + opcode: SpiFlashOpcode::READ_4B, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: false, + }; + + pub const FAST_QUAD_READ_4B: Self = Self { + opcode: SpiFlashOpcode::FAST_QUAD_READ_4B, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::QuadIoOut), + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 8, + filter: false, + }; + + pub const SFDP: Self = Self { + opcode: SpiFlashOpcode::SFDP, + upload: false, + busy: false, + payload_io: Some(SpiPayloadIoCfg::SingleIoOut), + addr_mode: Some(AddrMode::_3B), + dummy_cyc: 8, + filter: true, + }; + + pub const PAGE_PROGRAM: Self = Self { + opcode: SpiFlashOpcode::PAGE_PROGRAM, + upload: true, + busy: true, + payload_io: Some(SpiPayloadIoCfg::SingleIoIn), + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: true, + }; + + pub const PAGE_PROGRAM_QUAD: Self = Self { + opcode: SpiFlashOpcode::PAGE_PROGRAM_QUAD, + upload: true, + busy: true, + payload_io: Some(SpiPayloadIoCfg::QuadIoIn), + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: true, + }; + + pub const PAGE_PROGRAM_4B: Self = Self { + opcode: SpiFlashOpcode::PAGE_PROGRAM_4B, + upload: true, + busy: true, + payload_io: Some(SpiPayloadIoCfg::SingleIoIn), + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: true, + }; + + pub const PAGE_PROGRAM_QUAD_4B: Self = Self { + opcode: SpiFlashOpcode::PAGE_PROGRAM_QUAD_4B, + upload: true, + busy: true, + payload_io: Some(SpiPayloadIoCfg::QuadIoIn), + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: true, + }; + + pub const SECTOR_ERASE: Self = Self { + opcode: SpiFlashOpcode::SECTOR_ERASE, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: true, + }; + + pub const SECTOR_ERASE_4B: Self = Self { + opcode: SpiFlashOpcode::SECTOR_ERASE_4B, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: true, + }; + + pub const BLOCK_ERASE_32K: Self = Self { + opcode: SpiFlashOpcode::BLOCK_ERASE_32K, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: true, + }; + + pub const BLOCK_ERASE_32K_4B: Self = Self { + opcode: SpiFlashOpcode::BLOCK_ERASE_32K_4B, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: true, + }; + + pub const BLOCK_ERASE_64K: Self = Self { + opcode: SpiFlashOpcode::BLOCK_ERASE_64K, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::CFG), + dummy_cyc: 0, + filter: true, + }; + + pub const BLOCK_ERASE_64K_4B: Self = Self { + opcode: SpiFlashOpcode::BLOCK_ERASE_64K_4B, + upload: true, + busy: true, + payload_io: None, + addr_mode: Some(AddrMode::_4B), + dummy_cyc: 0, + filter: true, + }; + + pub const CHIP_ERASE: Self = Self { + opcode: SpiFlashOpcode::CHIP_ERASE, + upload: true, + busy: true, + payload_io: None, + addr_mode: None, + dummy_cyc: 0, + filter: true, + }; + + pub const CHIP_ERASE2: Self = Self { + opcode: SpiFlashOpcode::CHIP_ERASE, + upload: true, + busy: true, + payload_io: None, + addr_mode: None, + dummy_cyc: 0, + filter: true, + }; +} + +/// JEP-106 Identification code config +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct JedecIdConfig { + /// Number of continuation codes + pub num_cc: u8, + /// Manufacturing ID + pub manf_id: u8, + /// Device ID + pub dev_id: u16, +} + +impl JedecIdConfig { + pub const GOOGLE: Self = Self { + num_cc: 0x8, + manf_id: 0x26, + dev_id: (0x17 << 8) | 0x31, + }; +} + +// The JEDEC Identity Continuation Code +pub const JEDEC_CC: u32 = 0x7F; + +pub struct SpiFlashCmd<'a> { + pub opcode: SpiFlashOpcode, + pub wel: bool, + pub busy: bool, + pub address: Option, + pub payload: Option<&'a mut Aligned>, +} + +pub use spi_device::enums::Mode; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct SpiDevCfg { + pub jedec: JedecIdConfig, + pub mailbox: Option, + pub mode: Mode, + pub initial_address_mode_4b: bool, +} + +impl Default for SpiDevCfg { + fn default() -> Self { + Self { + jedec: JedecIdConfig::GOOGLE, + mailbox: None, + mode: Mode::Flashmode, + initial_address_mode_4b: true, + } + } +} + +pub trait SpiDevice { + fn write_to_mbx(&mut self, payload: &Aligned); + fn poll<'a>(&mut self, payload_buf: &'a mut Aligned) -> Option>; + fn retire_cmd(&mut self); + fn set_mode(&mut self, mode: Mode); + /// Under passthrough mode, intercept read address and swap the high address to achieve bank switching. + fn read_addr_swap( + &mut self, + enable: bool, + swap_addr_mask: Option, + swap_addr_data: Option, + ); +} + +impl SpiDev { + /// Create a new SpiDev driver instance. + /// + /// # Safety + /// + /// The caller must ensure exclusive ownership of the `spi_device` peripheral register block. + pub unsafe fn new(mmio: spi_device::RegisterBlock>) -> Self { + Self { mmio } + } + + /// Initialize the SPI Device peripheral with the provided configuration. + pub fn init(&mut self, cfg: &SpiDevCfg) -> Result<(), ErrorCode> { + self.mmio.control().write(|w| w.with_mode(cfg.mode)); + + self.mmio + .egress_buffer() + .get_sub_array::(MAILBOX_START_WORDS) + .unwrap() + .fill(0xD15EA5ED); + + self.mmio + .jedec_cc() + .write(|w| w.cc(JEDEC_CC).num_cc(cfg.jedec.num_cc.into())); + + self.mmio + .jedec_id() + .write(|w| w.mf(cfg.jedec.manf_id.into()).id(cfg.jedec.dev_id.into())); + + self.mmio + .addr_mode() + .write(|w| w.addr_4b_en(cfg.initial_address_mode_4b)); + + self.mmio + .cmd_info_wren() + .write(|w| w.opcode(SpiFlashOpcode::WRITE_ENABLE.into()).valid(true)); + + self.mmio + .cmd_info_wrdi() + .write(|w| w.opcode(SpiFlashOpcode::WRITE_DISABLE.into()).valid(true)); + + self.mmio.cmd_info_en4_b().write(|w| { + w.opcode(SpiFlashOpcode::ENTER_4B_ADDR_MODE.into()) + .valid(true) + }); + + self.mmio.cmd_info_ex4_b().write(|w| { + w.opcode(SpiFlashOpcode::EXIT_4B_ADDR_MODE.into()) + .valid(true) + }); + + if let Some(mbx_addr) = cfg.mailbox { + self.mmio.cfg().write(|w| w.mailbox_en(true)); + self.mmio.mailbox_addr().write(|_| mbx_addr); + } + + self.mmio.intercept_en().write(|w| { + w.sfdp(true) + .jedec(true) + .status(true) + .mbx(cfg.mailbox.is_some()) + }); + + self.mmio + .intr_enable() + .write(|w| w.upload_cmdfifo_not_empty(true)); + + self.configure_cmd_info(CMD_INFO_READ_STATUS, &SpiFlashCmdCfg::READ_STATUS); + self.configure_cmd_info(CMD_INFO_JEDEC, &SpiFlashCmdCfg::JEDEC_ID); + self.configure_cmd_info(CMD_INFO_READ, &SpiFlashCmdCfg::READ); + self.configure_cmd_info(CMD_INFO_FASTREAD, &SpiFlashCmdCfg::FAST_READ); + self.configure_cmd_info(CMD_INFO_FAST_QUAD_READ, &SpiFlashCmdCfg::FAST_QUAD_READ); + self.configure_cmd_info(CMD_INFO_READ4B, &SpiFlashCmdCfg::READ_4B); + self.configure_cmd_info( + CMD_INFO_FAST_QUAD_READ4B, + &SpiFlashCmdCfg::FAST_QUAD_READ_4B, + ); + self.configure_cmd_info(CMD_INFO_SFDP, &SpiFlashCmdCfg::SFDP); + self.configure_cmd_info(CMD_INFO_PAGEPROGRAM, &SpiFlashCmdCfg::PAGE_PROGRAM); + self.configure_cmd_info(CMD_INFO_PAGEPROGRAMQUAD, &SpiFlashCmdCfg::PAGE_PROGRAM_QUAD); + self.configure_cmd_info(CMD_INFO_PAGEPROGRAM4B, &SpiFlashCmdCfg::PAGE_PROGRAM_4B); + self.configure_cmd_info( + CMD_INFO_PAGEPROGRAMQUAD4B, + &SpiFlashCmdCfg::PAGE_PROGRAM_QUAD_4B, + ); + self.configure_cmd_info(CMD_INFO_SECTORERASE, &SpiFlashCmdCfg::SECTOR_ERASE); + self.configure_cmd_info(CMD_INFO_SECTORERASE4B, &SpiFlashCmdCfg::SECTOR_ERASE_4B); + self.configure_cmd_info(CMD_INFO_BLOCKERASE32K, &SpiFlashCmdCfg::BLOCK_ERASE_32K); + self.configure_cmd_info( + CMD_INFO_BLOCKERASE32K4B, + &SpiFlashCmdCfg::BLOCK_ERASE_32K_4B, + ); + self.configure_cmd_info(CMD_INFO_BLOCKERASE64K, &SpiFlashCmdCfg::BLOCK_ERASE_64K); + self.configure_cmd_info( + CMD_INFO_BLOCKERASE64K4B, + &SpiFlashCmdCfg::BLOCK_ERASE_64K_4B, + ); + self.configure_cmd_info(CMD_INFO_CHIPERASE, &SpiFlashCmdCfg::CHIP_ERASE); + self.configure_cmd_info(CMD_INFO_CHIPERASE2, &SpiFlashCmdCfg::CHIP_ERASE2); + + Ok(()) + } + + /// Populate the SFDP table in the SRAM egress buffer. + pub fn set_sfdp(&mut self, sfdp: &Aligned) { + let sfdp_regs = self + .mmio + .egress_buffer() + .get_sub_array::(SFDP_START_WORDS) + .unwrap(); + + copy_to_reg_array(&sfdp_regs, sfdp); + } + + /// Configure a command slot in the CMD_INFO array and apply command filter if required. + pub fn configure_cmd_info(&mut self, slot: u8, cfg: &SpiFlashCmdCfg) { + self.mmio.cmd_info().at(slot.into()).write(|w| { + w.valid(true) + .opcode(cfg.opcode.into()) + .payload_dir(|w| match cfg.payload_io { + None + | Some(SpiPayloadIoCfg::SingleIoIn) + | Some(SpiPayloadIoCfg::DualIoIn) + | Some(SpiPayloadIoCfg::QuadIoIn) => w.payload_in(), + Some(SpiPayloadIoCfg::SingleIoOut) + | Some(SpiPayloadIoCfg::DualIoOut) + | Some(SpiPayloadIoCfg::QuadIoOut) => w.payload_out(), + }) + .upload(cfg.upload) + .payload_en(match cfg.payload_io { + None => 0, + Some(SpiPayloadIoCfg::SingleIoIn) => 0x01, + Some(SpiPayloadIoCfg::SingleIoOut) => 0x02, + Some(SpiPayloadIoCfg::DualIoIn) => 0x03, + Some(SpiPayloadIoCfg::DualIoOut) => 0x03, + Some(SpiPayloadIoCfg::QuadIoIn) => 0x0F, + Some(SpiPayloadIoCfg::QuadIoOut) => 0x0F, + }) + .addr_swap_en(false) + .busy(cfg.busy) + .addr_mode(|w| match cfg.addr_mode { + None => w.addr_disabled(), + Some(AddrMode::CFG) => w.addr_cfg(), + Some(AddrMode::_3B) => w.addr3_b(), + Some(AddrMode::_4B) => w.addr4_b(), + }) + .dummy_en(cfg.dummy_cyc > 0) + .dummy_size(cfg.dummy_cyc.wrapping_sub(1).into()) + .mbyte_en(false) + }); + + if cfg.filter { + let f_slot = cfg.opcode.0 / 32; + let idx: u32 = 1 << (cfg.opcode.0 % 32); + + match f_slot { + 0 => self + .mmio + .cmd_filter0() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 1 => self + .mmio + .cmd_filter1() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 2 => self + .mmio + .cmd_filter2() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 3 => self + .mmio + .cmd_filter3() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 4 => self + .mmio + .cmd_filter4() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 5 => self + .mmio + .cmd_filter5() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 6 => self + .mmio + .cmd_filter6() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + 7 => self + .mmio + .cmd_filter7() + .read_and_modify(|_w, r| (u32::from(r) | idx).into()), + _ => { + unreachable!("Error configuring cmd_filter") + } + } + } + } + /// Write a payload into the mailbox egress buffer. + pub fn write_to_mbx(&mut self, payload: &Aligned) { + let mailbox = self + .mmio + .egress_buffer() + .get_sub_array::(MAILBOX_START_WORDS) + .unwrap(); + + copy_to_reg_array(&mailbox, payload); + } + + /// Poll for uploaded SPI flash commands from host. + pub fn poll<'a>( + &mut self, + mut payload_buf: &'a mut Aligned, + ) -> Option> { + let upload_status = self.mmio.upload_status().read(); + if !upload_status.cmdfifo_notempty() { + return None; + } + + let uploadstatus2 = self.mmio.upload_status2().read(); + if uploadstatus2.payload_start_idx() != 0 { + // Payload overflow, drop the command + self.retire_cmd(); + return None; + } + + let upload_cmdfifo = self.mmio.upload_cmdfifo().read(); + + let opcode: u8 = upload_cmdfifo.data() as u8; + let addr = if upload_status.addrfifo_notempty() { + Some(self.mmio.upload_addrfifo().read()) + } else { + None + }; + + let payload_len = uploadstatus2.payload_depth() as u16; + if payload_len > 256 { + self.retire_cmd(); + return None; + } + + payload_buf = &mut payload_buf[..payload_len.into()]; + + let payload_fifo = self + .mmio + .ingress_buffer() + .get_sub_array::(PAYLOAD_FIFO_START_WORDS) + .unwrap(); + + copy_from_reg_array(payload_buf, &payload_fifo); + + self.mmio.intr_state().write(|w| { + w.upload_cmdfifo_not_empty_clear() + .upload_payload_overflow_clear() + .upload_payload_not_empty_clear() + }); + + Some(SpiFlashCmd { + opcode: SpiFlashOpcode(opcode), + wel: upload_cmdfifo.wel(), + busy: upload_cmdfifo.busy(), + address: addr, + payload: Some(payload_buf), + }) + } + + /// Clear busy and WEL in flash status. + pub fn retire_cmd(&mut self) { + self.mmio + .flash_status() + .write(|w| w.busy_clear().wel_clear()); + } + + /// Set SPI device operation mode. + pub fn set_mode(&mut self, mode: Mode) { + self.mmio.control().modify(|w| w.with_mode(mode)); + } + + /// Configure address swapping for read commands. + pub fn read_addr_swap( + &mut self, + enable: bool, + swap_addr_mask: Option, + swap_addr_data: Option, + ) { + // Temporarily disable address swapping for read commands before modifying registers. + for cmd in [CMD_INFO_READ, CMD_INFO_FASTREAD, CMD_INFO_READ4B] { + if let Some(reg) = self.mmio.cmd_info().get(usize::from(cmd)) { + reg.modify(|w| w.addr_swap_en(false)); + } + } + + if !enable { + return; + } + + if let Some(mask) = swap_addr_mask { + self.mmio.addr_swap_mask().write(|_| mask); + } + + if let Some(data) = swap_addr_data { + self.mmio.addr_swap_data().write(|_| data); + } + + // Re-enable address swapping for the relevant read commands. + for cmd in [CMD_INFO_READ, CMD_INFO_FASTREAD, CMD_INFO_READ4B] { + if let Some(reg) = self.mmio.cmd_info().get(usize::from(cmd)) { + reg.modify(|w| w.addr_swap_en(true)); + } + } + } +} + +impl SpiDevice for SpiDev { + fn write_to_mbx(&mut self, payload: &Aligned) { + self.write_to_mbx(payload) + } + + fn poll<'a>(&mut self, payload_buf: &'a mut Aligned) -> Option> { + self.poll(payload_buf) + } + + fn retire_cmd(&mut self) { + self.retire_cmd() + } + + fn set_mode(&mut self, mode: Mode) { + self.set_mode(mode) + } + + fn read_addr_swap( + &mut self, + enable: bool, + swap_addr_mask: Option, + swap_addr_data: Option, + ) { + self.read_addr_swap(enable, swap_addr_mask, swap_addr_data) + } +} diff --git a/target/earlgrey/tests/drivers/spi_device/BUILD.bazel b/target/earlgrey/tests/drivers/spi_device/BUILD.bazel new file mode 100644 index 000000000..e9efbf2f4 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_device/BUILD.bazel @@ -0,0 +1,103 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") + +rust_app( + name = "spi_device", + srcs = [ + "spi_device.rs", + ], + codegen_crate_name = "spi_device_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/drivers:spi_device", + "//target/earlgrey/registers:spi_device", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + ], +) + +system_image( + name = "spi_device_image", + apps = [ + ":spi_device", + ], + 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_device_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spi_device_image", +) + +opentitan_test( + name = "spi_device_qemu_test", + timeout = "moderate", + environment = "//target/earlgrey/env:qemu", + interface = "qemu", + tags = ["qemu"], + target = ":spi_device_image", +) diff --git a/target/earlgrey/tests/drivers/spi_device/spi_device.rs b/target/earlgrey/tests/drivers/spi_device/spi_device.rs new file mode 100644 index 000000000..a57d43cf7 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_device/spi_device.rs @@ -0,0 +1,113 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Smoke test for Earlgrey SPI Device driver. +//! +//! Initializes the SPI Device peripheral in Flash mode with JEDEC ID and SFDP interception, +//! and verifies hardware register configuration and SRAM buffer write operations. + +#![no_std] +#![no_main] + +use aligned::{Aligned, A4}; +use earlgrey_spi_device::{JedecIdConfig, Mode, SpiDev, SpiDevCfg, CMD_INFO_SFDP}; +use pw_status::{Error, Result}; +use spi_device::SpiDevice; +use userspace::entry; +use util_panic as _; + +fn run_test() -> Result<()> { + // 1. Initialize SPI Device driver. + // SAFETY: We have exclusive access to SPI_DEVICE in this test process. + let mut dev = unsafe { SpiDev::new(spi_device::RegisterBlock::new(SpiDevice::PTR)) }; + + let cfg = SpiDevCfg { + jedec: JedecIdConfig::GOOGLE, + mailbox: Some(0x7FF0000), + mode: Mode::Flashmode, + initial_address_mode_4b: true, + }; + + dev.init(&cfg).map_err(|_| { + pw_log::error!("SPI Device init failed"); + Error::Internal + })?; + + // 2. Verify register configuration. + // SAFETY: We have exclusive access to SPI_DEVICE registers in this test process. + let dev_raw = unsafe { SpiDevice::new() }; + let regs = dev_raw.regs(); + + let ctrl = regs.control().read(); + if ctrl.mode() != spi_device::enums::Mode::Flashmode { + pw_log::error!("FAIL: unexpected mode"); + return Err(Error::FailedPrecondition); + } + + let jedec_cc = regs.jedec_cc().read(); + if jedec_cc.cc() != 0x7F || jedec_cc.num_cc() != 8 { + pw_log::error!( + "FAIL: unexpected jedec_cc: cc=0x{:x}, num_cc={}", + jedec_cc.cc(), + jedec_cc.num_cc() + ); + return Err(Error::FailedPrecondition); + } + + let jedec_id = regs.jedec_id().read(); + if jedec_id.mf() != 0x26 || jedec_id.id() != ((0x17 << 8) | 0x31) { + pw_log::error!( + "FAIL: unexpected jedec_id: mf=0x{:x}, id=0x{:x}", + jedec_id.mf(), + jedec_id.id() + ); + return Err(Error::FailedPrecondition); + } + + let intercept = regs.intercept_en().read(); + if !intercept.sfdp() || !intercept.jedec() || !intercept.status() || !intercept.mbx() { + pw_log::error!("FAIL: intercept_en bits not set properly"); + return Err(Error::FailedPrecondition); + } + + let cmd_sfdp = regs.cmd_info().at(CMD_INFO_SFDP.into()).read(); + if !cmd_sfdp.valid() + || cmd_sfdp.opcode() != 0x5A + || !cmd_sfdp.dummy_en() + || cmd_sfdp.dummy_size() != 7 + { + pw_log::error!("FAIL: cmd_info SFDP slot not configured properly"); + return Err(Error::FailedPrecondition); + } + + // 3. Test SFDP table loading into egress buffer via set_sfdp. + let mut test_sfdp_table: Aligned = Aligned([0u8; 256]); + test_sfdp_table[0..4].copy_from_slice(b"SFDP"); // Signature 0x50444653 + test_sfdp_table[4] = 0x00; // Minor rev 0 + test_sfdp_table[5] = 0x01; // Major rev 1 + test_sfdp_table[6] = 0x00; // 1 parameter header (0-based) + test_sfdp_table[7] = 0xFF; // Access protocol legacy + + dev.set_sfdp(&test_sfdp_table); + + // 4. Test mailbox write into egress buffer. + let mbx_payload: Aligned = Aligned([0x5A; 64]); + dev.write_to_mbx(&mbx_payload); + + pw_log::info!("SPI Device driver smoke test passed successfully!"); + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + pw_log::info!("🔄 RUNNING SPI Device Smoke Test"); + let ret = run_test(); + + if ret.is_err() { + pw_log::error!("FAIL: Smoke test execution failed"); + } else { + pw_log::info!("✅ PASS"); + } + + ret +} diff --git a/target/earlgrey/tests/drivers/spi_device/system.json5 b/target/earlgrey/tests/drivers/spi_device/system.json5 new file mode 100644 index 000000000..a222640b3 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_device/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_device", + flash_size_bytes: 16384, + processes: [{ + name: "spi_device_process", + ram_size_bytes: 4096, + objects: [ + { + type: "thread", + name: "spi_thread", + kernel_stack_size_bytes: 2048, + }, + ], + memory_mappings: [ + { + name: "spi_device", + type: "device", + start_address: 0x40050000, + size_bytes: 0x2000, + }, + ], + }], + }, + ], +} diff --git a/target/earlgrey/tests/drivers/spi_device/target.rs b/target/earlgrey/tests/drivers/spi_device/target.rs new file mode 100644 index 000000000..968a7d0b2 --- /dev/null +++ b/target/earlgrey/tests/drivers/spi_device/target.rs @@ -0,0 +1,30 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +#![allow(clippy::empty_loop)] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey SPI Device test"; + + fn main() -> ! { + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code as u32), + }; + loop {} + } +} + +declare_target!(Target); From cb75f78c3c764784df9a6e943be3316ac782e8ce Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Tue, 11 Aug 2026 20:50:42 +0800 Subject: [PATCH 2/4] util/sfdp: Add default SFDP table generation helper and driver integration - Adds create_default_sfdp_table in util/sfdp generating standard JESD216 compliant SFDP headers and basic flash parameters table. - Re-exports SFDP generation in earlgrey_spi_device driver. - Adds unit test validating default SFDP table parsing with SfdpReader. Signed-off-by: Anthony Chen --- util/sfdp/BUILD.bazel | 1 + util/sfdp/mod.rs | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/util/sfdp/BUILD.bazel b/util/sfdp/BUILD.bazel index 96bbdded9..087868637 100644 --- a/util/sfdp/BUILD.bazel +++ b/util/sfdp/BUILD.bazel @@ -16,6 +16,7 @@ rust_library( "//util/error", "//util/io", "//util/types", + "@rust_crates//:aligned", "@rust_crates//:zerocopy", ], ) diff --git a/util/sfdp/mod.rs b/util/sfdp/mod.rs index 3bd8fba23..3147cff71 100644 --- a/util/sfdp/mod.rs +++ b/util/sfdp/mod.rs @@ -5,6 +5,7 @@ const _: () = assert!(cfg!(target_endian = "little")); +use aligned::{Aligned, A4}; use bitfield_struct::bitfield; use core::mem::offset_of; use util_error as error; @@ -1693,6 +1694,60 @@ impl>> SfdpReader { } } +pub const DEFAULT_SFDP_TABLE_SIZE: usize = 256; + +/// Generates a standard JESD216 SFDP table (256 bytes, aligned to 4 bytes) +/// for a flash of a specific size (in bytes). +pub fn create_default_sfdp_table( + flash_total_len: usize, +) -> Aligned { + let mut buf = Aligned([0xFFu8; DEFAULT_SFDP_TABLE_SIZE]); + + let header = SfdpHeader { + sig: SfdpSignature::EXPECTED_VALUE, + major_rev: 1, + minor_rev: 0, + access_protocol: AccessProtocol::LEGACY, + num_parameter_header: 0, // 0-based => 1 parameter header + }; + + let phdr = ParameterHeader { + parameter_id_lsb: 0x00, + minor_rev: 0, + major_rev: 1, + len_in_dwords: 23, + ptr: U24::new(16), + parameter_id_msb: 0xFF, + }; + + let mut bpt = BasicFlashParameterTable::new_zeroed(); + if let Ok(density) = MemoryDensity::from_byte_len(flash_total_len as u32) { + bpt.table_jesd216.memory_density = density; + } + bpt.table_jesd216.word1.set_supports_1s_1s_4s_read(true); + bpt.table_jesd216.word1.set_erase4k_instr(0x20); + bpt.table_jesd216 + .word1 + .set_legacy_erase_sizes(LegacyEraseSizes::Erase4k); + bpt.table_jesd216 + .word1 + .set_legacy_write_granularity(LegacyWriteGranularity::Buffer64); + bpt.table_jesd216 + .word1 + .set_addr_bytes(AddressBytes::_3Or4Byte); + bpt.table_jesd216a + .word15 + .set_quad_enable_requirements(QuadEnableRequirements::QeBit6SR1); + + buf[0..8].copy_from_slice(header.as_bytes()); + buf[8..16].copy_from_slice(phdr.as_bytes()); + let bpt_bytes = bpt.as_bytes(); + let bpt_copy_len = core::cmp::min(bpt_bytes.len(), 23 * 4); + buf[16..16 + bpt_copy_len].copy_from_slice(&bpt_bytes[..bpt_copy_len]); + + buf +} + #[cfg(test)] mod test { use error::FLASH_GENERIC_SFDP_PARAMETERS_TOO_SHORT; @@ -2292,4 +2347,19 @@ mod test { assert_eq!(table.table.word2.erase_type_1_instr(), 0x21); assert_eq!(table.table.word2.erase_type_3_instr(), 0xdc); } + + #[test] + fn test_create_default_sfdp_table() { + let table = create_default_sfdp_table(64 * 1024 * 1024); + let mut reader = SfdpReader::new(&table[..]).expect("Failed to parse default SFDP table"); + let header = reader.header().expect("Failed to get header"); + assert_eq!(header.sig, SfdpSignature::EXPECTED_VALUE); + assert_eq!(header.major_rev, 1); + assert_eq!(header.num_parameter_header, 0); + + let basic_table = reader + .read_table::() + .expect("Failed to read BFPT"); + assert_eq!(basic_table.table.table_jesd216.word1.erase4k_instr(), 0x20); + } } From 3e190c2f999f64d1d1790ae4aa9b97d535c70683 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Tue, 11 Aug 2026 20:52:41 +0800 Subject: [PATCH 3/4] earlgrey: Add spidev process to HWE firmware - Implements the spidev process in HWE firmware. - Configures SpiDev in Flash mode with Google JEDEC ID and standard JESD216 SFDP table in SRAM egress buffer. - Hooks up spidev process and logger_spidev IPC channel in system.json5 and logmgr.rs. - Adds spidev process to multi_process_app in BUILD.bazel. Signed-off-by: Anthony Chen --- target/earlgrey/firmware/hwe/BUILD.bazel | 25 ++++++++ target/earlgrey/firmware/hwe/logmgr.rs | 6 ++ target/earlgrey/firmware/hwe/spidev.rs | 72 +++++++++++++++++++++++ target/earlgrey/firmware/hwe/system.json5 | 38 +++++++++++- 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 target/earlgrey/firmware/hwe/spidev.rs diff --git a/target/earlgrey/firmware/hwe/BUILD.bazel b/target/earlgrey/firmware/hwe/BUILD.bazel index 9006cb2ff..926a1b68e 100644 --- a/target/earlgrey/firmware/hwe/BUILD.bazel +++ b/target/earlgrey/firmware/hwe/BUILD.bazel @@ -141,6 +141,30 @@ rust_process( ], ) +rust_process( + name = "spidev", + srcs = [ + "spidev.rs", + ], + codegen_crate_name = "spidev_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//target/earlgrey/drivers:spi_device", + "//target/earlgrey/registers:spi_device", + "//util/error", + "//util/ipc", + "//util/sfdp", + "//util/zfmt", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:aligned", + "@zfmt//zfmt", + ], +) + multi_process_app( name = "hwe", processes = [ @@ -149,6 +173,7 @@ multi_process_app( ":platform", ":flash_server", ":usbmgr", + ":spidev", ], tags = ["kernel"], template = "app_entry.rs.jinja", diff --git a/target/earlgrey/firmware/hwe/logmgr.rs b/target/earlgrey/firmware/hwe/logmgr.rs index 256012c84..77ec0a067 100644 --- a/target/earlgrey/firmware/hwe/logmgr.rs +++ b/target/earlgrey/firmware/hwe/logmgr.rs @@ -196,6 +196,12 @@ fn logmgr_server() -> Result<(), Error> { Signals::READABLE, handle::LOGGER_SYSMGR as usize, )?; + syscall::wait_group_add( + handle::LOGMGR_WAIT_GROUP, + handle::LOGGER_SPIDEV, + Signals::READABLE, + handle::LOGGER_SPIDEV as usize, + )?; let mut server = LogServer::<2048>::new(); let mut active_log = ActiveLog::new(); diff --git a/target/earlgrey/firmware/hwe/spidev.rs b/target/earlgrey/firmware/hwe/spidev.rs new file mode 100644 index 000000000..732ee2bfb --- /dev/null +++ b/target/earlgrey/firmware/hwe/spidev.rs @@ -0,0 +1,72 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPI Device service process for Earlgrey HWE firmware. + +#![no_std] +#![no_main] + +use aligned::{Aligned, A4}; +use earlgrey_spi_device::{JedecIdConfig, Mode, SpiDev, SpiDevCfg}; +use pw_status::Error; +use spi_device::SpiDevice; +use spidev_codegen::{handle, signals}; +use userspace::time::Instant; +use userspace::{process_entry, syscall}; +use util_error::{AsStatus, ErrorCode}; +use util_sfdp::create_default_sfdp_table; +use util_zfmt::messages::{ProcessExit, ProcessStart}; + +fn spidev_server() -> Result<(), ErrorCode> { + // SAFETY: the spidev process has exclusive access to the SPI Device peripheral. + let mut dev = unsafe { SpiDev::new(spi_device::RegisterBlock::new(SpiDevice::PTR)) }; + + let cfg = SpiDevCfg { + jedec: JedecIdConfig::GOOGLE, + mailbox: None, + mode: Mode::Flashmode, + initial_address_mode_4b: true, + }; + + dev.init(&cfg)?; + + let sfdp_table = create_default_sfdp_table(64 * 1024 * 1024); + dev.set_sfdp(&sfdp_table); + + util_zfmt::debug!("spidev: initialized SPI device in Flash mode with SFDP"); + + let mut payload_buf: Aligned = Aligned([0u8; 256]); + + loop { + let wait_result = syscall::object_wait( + handle::SPIDEV_INTERRUPTS, + signals::SPI_DEVICE_UPLOAD_CMDFIFO_NOT_EMPTY, + Instant::MAX, + ) + .map_err(|e| ErrorCode::kernel_error(e))?; + + while let Some(cmd) = dev.poll(&mut payload_buf) { + let opcode = cmd.opcode.0; + util_zfmt::debug!( + "spidev: received command opcode 0x{opcode:02x}", + opcode = opcode + ); + dev.retire_cmd(); + } + + let _ = syscall::interrupt_ack(handle::SPIDEV_INTERRUPTS, wait_result.pending_signals); + } +} + +#[process_entry("spidev")] +fn entry() -> Result<(), Error> { + util_zfmt::info!(ProcessStart { name: "spidev" }); + let ret = spidev_server(); + util_zfmt::error!(ProcessExit { + name: "spidev", + status: ret.as_status() + }); + + let status_res = ret.map_err(|_| Error::Unknown); + syscall::debug_shutdown(status_res) +} diff --git a/target/earlgrey/firmware/hwe/system.json5 b/target/earlgrey/firmware/hwe/system.json5 index ba9a9c0a0..29f1ff427 100644 --- a/target/earlgrey/firmware/hwe/system.json5 +++ b/target/earlgrey/firmware/hwe/system.json5 @@ -17,7 +17,7 @@ apps: [ { name: "hwe", - flash_size_bytes: 49152, + flash_size_bytes: 65536, processes: [ { name: "logmgr", @@ -43,6 +43,10 @@ name: "logger_usb", type: "channel_handler" }, + { + name: "logger_spidev", + type: "channel_handler" + }, { name: "uart0_interrupts", type: "interrupt", @@ -264,6 +268,38 @@ size_bytes: 0x1000 } ] + }, + { + name: "spidev", + ram_size_bytes: 4096, + objects: [ + { + name: "logger_spidev", + type: "channel_initiator", + handler_process: "logmgr", + handler_object_name: "logger_spidev" + }, + { + name: "spidev_interrupts", + type: "interrupt", + irqs: [ + { name: "spi_device_upload_cmdfifo_not_empty", number: 69 } + ] + }, + { + name: "spidev_thread", + kernel_stack_size_bytes: 2048, + type: "thread" + } + ], + memory_mappings: [ + { + name: "spi_device", + type: "device", + start_address: 0x40050000, + size_bytes: 0x2000 + } + ] } ] } From 58dcf2f6553fd135e2a128c66de06d7fc2365c17 Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Tue, 11 Aug 2026 22:16:19 +0800 Subject: [PATCH 4/4] earlgrey: Add SPI Device E2E test - Adds host_spidev_check host harness using opentitanlib to query the SPI device over the SPI interface (BOOTSTRAP): - Validates Opcode 0x9F (Read JEDEC ID) returning 8x continuation codes 0x7F, Google Manf ID 0x26, and Device ID 0x1731. - Validates Opcode 0x5A (Read SFDP) returning JESD216 signature 'SFDP' and basic flash parameter tables. - Adds spidev_hyper340_test executing on CW340 targeting hwe_firmware. Signed-off-by: Anthony Chen --- target/earlgrey/tests/spidev/BUILD.bazel | 39 ++++ .../tests/spidev/host_spidev_check.rs | 168 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 target/earlgrey/tests/spidev/BUILD.bazel create mode 100644 target/earlgrey/tests/spidev/host_spidev_check.rs diff --git a/target/earlgrey/tests/spidev/BUILD.bazel b/target/earlgrey/tests/spidev/BUILD.bazel new file mode 100644 index 000000000..e6ee8406f --- /dev/null +++ b/target/earlgrey/tests/spidev/BUILD.bazel @@ -0,0 +1,39 @@ +# 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("//third_party/lowrisc_opentitan:defs.bzl", "opentitan_rust_binary") + +opentitan_rust_binary( + name = "host_spidev_check", + srcs = ["host_spidev_check.rs"], + edition = "2024", + rustc_flags = [ + "-C", + "link-arg=-Wl,--allow-shlib-undefined", + ], + deps = [ + "//target/earlgrey/testutil", + "//third_party/lowrisc_opentitan:opentitanlib", + "@ot_crate_index//:anyhow", + "@ot_crate_index//:clap", + "@ot_crate_index//:humantime", + "@ot_crate_index//:log", + ], +) + +opentitan_test( + name = "spidev_hyper340_test", + clear_bitstream = True, + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = "//target/earlgrey/firmware/hwe:hwe_firmware", + test_cmd = "--logging=info", + test_harness = ":host_spidev_check", +) diff --git a/target/earlgrey/tests/spidev/host_spidev_check.rs b/target/earlgrey/tests/spidev/host_spidev_check.rs new file mode 100644 index 000000000..7d939d5e8 --- /dev/null +++ b/target/earlgrey/tests/spidev/host_spidev_check.rs @@ -0,0 +1,168 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-side test harness for SPI Device E2E verification. +//! +//! Uses opentitanlib to query the Earlgrey SPI Device over the SPI bus: +//! 1. Reads and verifies JEDEC ID (Opcode 0x9F) matching Google continuation codes and ID. +//! 2. Reads and verifies SFDP Table (Opcode 0x5A) matching JESD216 signature and headers. + +use anyhow::{ensure, Context, Result}; +use clap::Parser; +use std::time::Duration; + +use opentitanlib::io::spi::Transfer; +use opentitanlib::spiflash::SpiFlash; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; + +#[derive(Debug, Parser)] +struct Opts { + #[command(flatten)] + init: InitializeTest, + + /// SPI interface name. + #[arg(long, default_value = "BOOTSTRAP")] + spi: String, + + /// Console receive timeout. + #[arg(long, value_parser = humantime::parse_duration, default_value = "180s")] + timeout: Duration, +} + +fn read_sfdp(spi: &dyn opentitanlib::io::spi::Target, offset: u32) -> Result> { + let mut buf = vec![0u8; 256]; + spi.run_transaction(&mut [ + // READ_SFDP (0x5A) always takes a 3-byte address followed by 1 dummy byte. + Transfer::Write(&[ + SpiFlash::READ_SFDP, + (offset >> 16) as u8, + (offset >> 8) as u8, + offset as u8, + 0x00, // Dummy byte + ]), + Transfer::Read(&mut buf), + ])?; + Ok(buf) +} + +fn test_jedec_id(spi: &dyn opentitanlib::io::spi::Target) -> Result<()> { + log::info!("Testing JEDEC ID readout (Opcode 0x9F)..."); + let jedec = SpiFlash::read_jedec_id(spi, 11)?; + log::info!("Read JEDEC ID bytes: {:02x?}", jedec); + + ensure!( + jedec.len() >= 11, + "JEDEC ID response too short: expected 11 bytes, got {}", + jedec.len() + ); + + // Verify 8 continuation codes (0x7F) + for i in 0..8 { + ensure!( + jedec[i] == 0x7F, + "Continuation code mismatch at index {}: expected 0x7F, got 0x{:02x}", + i, + jedec[i] + ); + } + + // Verify Google Manufacturer ID (0x26) + ensure!( + jedec[8] == 0x26, + "Manufacturer ID mismatch: expected 0x26 (Google), got 0x{:02x}", + jedec[8] + ); + + // Verify Device ID (0x31, 0x17) + ensure!( + jedec[9] == 0x31 && jedec[10] == 0x17, + "Device ID mismatch: expected [0x31, 0x17], got [0x{:02x}, 0x{:02x}]", + jedec[9], + jedec[10] + ); + + log::info!("✅ JEDEC ID verified successfully: 8x 0x7F, Manf 0x26, Dev 0x1731"); + Ok(()) +} + +fn test_sfdp_table(spi: &dyn opentitanlib::io::spi::Target) -> Result<()> { + log::info!("Testing SFDP table readout (Opcode 0x5A)..."); + let sfdp = read_sfdp(spi, 0)?; + + // 1. Verify SFDP Header: Signature "SFDP" (0x50444653) + let sig = &sfdp[0..4]; + ensure!( + sig == b"SFDP", + "SFDP signature mismatch: expected b\"SFDP\", got {:?}", + sig + ); + + let minor_rev = sfdp[4]; + let major_rev = sfdp[5]; + let num_ph = sfdp[6]; + let access_protocol = sfdp[7]; + + log::info!( + "SFDP Header: Major {}, Minor {}, Param Headers {}, Access Protocol 0x{:02x}", + major_rev, + minor_rev, + num_ph + 1, + access_protocol + ); + + ensure!(major_rev >= 1, "Invalid SFDP major revision: {}", major_rev); + + // 2. Verify Parameter Header 0 (Basic Flash Parameters) + let param_id_lsb = sfdp[8]; + let _param_minor = sfdp[9]; + let _param_major = sfdp[10]; + let param_len_dwords = sfdp[11]; + let param_ptr = sfdp[12] as u32 | ((sfdp[13] as u32) << 8) | ((sfdp[14] as u32) << 16); + let param_id_msb = sfdp[15]; + + ensure!( + param_id_lsb == 0x00 && param_id_msb == 0xFF, + "Parameter ID mismatch: expected 0xFF00, got 0x{:02x}{:02x}", + param_id_msb, + param_id_lsb + ); + ensure!( + param_ptr == 16, + "Parameter table pointer mismatch: expected 16, got {}", + param_ptr + ); + ensure!( + param_len_dwords >= 9, + "Parameter table length too short: {} DWORDs", + param_len_dwords + ); + + log::info!("✅ SFDP Table header & parameter headers verified successfully"); + Ok(()) +} + +fn main() -> Result<()> { + let opts = Opts::parse(); + opts.init.init_logging(); + + let transport = opts.init.init_target()?; + let uart = transport.uart("console")?; + + log::info!("Waiting for target firmware message 'spidev: initialized SPI device in Flash mode with SFDP'..."); + UartConsole::wait_for( + &*uart, + r"spidev: initialized SPI device in Flash mode with SFDP", + opts.timeout, + ) + .context("Timeout waiting for SPI Device HWE firmware to boot")?; + log::info!("Target firmware is ready."); + + let spi = transport.spi(&opts.spi)?; + + test_jedec_id(&*spi)?; + test_sfdp_table(&*spi)?; + + log::info!("🎉 All SPI Device E2E tests passed successfully!"); + Ok(()) +}