diff --git a/target/ast10x0/board/BUILD.bazel b/target/ast10x0/board/BUILD.bazel index e5baf596..34075dad 100644 --- a/target/ast10x0/board/BUILD.bazel +++ b/target/ast10x0/board/BUILD.bazel @@ -18,5 +18,6 @@ rust_library( deps = [ "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", "//target/ast10x0/peripherals", + "@ast1060_pac", ], ) diff --git a/target/ast10x0/board/src/lib.rs b/target/ast10x0/board/src/lib.rs index e12c05d7..f9924eab 100644 --- a/target/ast10x0/board/src/lib.rs +++ b/target/ast10x0/board/src/lib.rs @@ -18,7 +18,12 @@ pub mod monitor; pub mod spim_wiring; pub use monitor::Ast1060Monitor; -pub use spim_wiring::{apply_spim_wiring, presets, SpimWiring, SpimWiringError}; +pub use spim_wiring::{ + apply_spim_external_mux, apply_spim_pinctrl, apply_spim_wiring, apply_spim_wiring_with_log, + bmc_spim_csin_levels, bmc_spim_path_debug, enable_flash_power, presets, + release_spi_flash_resets, set_bmc_resets, spim_external_mux_state, BmcSpimPathDebug, + SpimWiring, SpimWiringError, +}; pub use ast10x0_peripherals::i2c::{I2cConfig, I2cError}; @@ -115,7 +120,7 @@ impl Ast10x0Board { /// This is a placeholder; production code should use a proper timer or delay provider. /// Spins for approximately `micros` microseconds. #[inline] -fn delay_us(micros: u32) { +pub fn delay_us(micros: u32) { // Very rough approximation: ~16 cycles per microsecond on Cortex-M4 @ ~50MHz // This is calibration-free but inaccurate; improve for production. for _ in 0..(micros * 16) { diff --git a/target/ast10x0/board/src/spim_wiring.rs b/target/ast10x0/board/src/spim_wiring.rs index d33289db..5ad45869 100644 --- a/target/ast10x0/board/src/spim_wiring.rs +++ b/target/ast10x0/board/src/spim_wiring.rs @@ -4,32 +4,36 @@ //! Static SPI-monitor wiring for AST10x0 boards. //! //! Composes the `scu::routing` mux helpers with the `spimonitor::controller` -//! typestate to apply once-per-process SPIM routing and SPIPF policy at -//! backend init time. Per-transaction reroutes are explicitly out of scope: +//! typestate to apply once-per-process external-monitor routing and SPIPF +//! policy at backend init time. Per-transaction reroutes are explicitly out of scope: //! the SPIPF lock is one-way, and the design doc //! (`peripherals/spimonitor/planning/overview-and-usage-model.md`) calls for //! "configure early, validate, lock, and operate under that locked policy." +use ast1060_pac as device; use ast10x0_peripherals::scu::{ + pinctrl::{ + PINCTRL_GPIOL2, PINCTRL_GPIOL3, PINCTRL_SPIM1_DEFAULT, PINCTRL_SPIM2_DEFAULT, + PINCTRL_SPIM3_DEFAULT, PINCTRL_SPIM4_DEFAULT, + }, ScuError, ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource, }; use ast10x0_peripherals::smc::SmcController; use ast10x0_peripherals::spimonitor::{ - LockedSpiMonitor, MonitorPolicy, SpiMonitor, SpiMonitorController, SpiMonitorError, - Uninitialized, + LockedSpiMonitor, MonitorPolicy, PassthroughMode, SpiMonitor, SpiMonitorController, + SpiMonitorError, Uninitialized, }; -/// Static SPIM wiring for one SPI controller. +/// Static wiring for one external SPI monitor path. /// -/// Captures the four SCU0F0 fields plus the MISO multi-function pin choice -/// that together determine which monitor instance a given SPI master is -/// routed through. +/// The `source` identifies the external flash bus associated with this policy. +/// It does not enable the AST1060 internal SPI-master detour. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SpimWiring { - /// Monitor instance the master is routed through. + /// Monitor instance connected to the external bus. pub instance: SpiMonitorInstance, - /// Which SPI master is being routed. + /// External SPI bus associated with the monitor policy. pub source: SpiMonitorSource, /// Passthrough enable for the chosen instance. pub passthrough: SpiMonitorPassthrough, @@ -92,10 +96,265 @@ impl From for SpimWiringError { } } +/// Apply the default SCU pinctrl group for a SPI monitor instance. +/// +/// The instance numbering follows the hardware SPIPF blocks: `Spim0` uses +/// the device-tree `spim1` pins, through `Spim3` using the `spim4` pins. +pub fn apply_spim_pinctrl(scu: &ScuRegisters, instance: SpiMonitorInstance) { + let group = match instance { + SpiMonitorInstance::Spim0 => PINCTRL_SPIM1_DEFAULT, + SpiMonitorInstance::Spim1 => PINCTRL_SPIM2_DEFAULT, + SpiMonitorInstance::Spim2 => PINCTRL_SPIM3_DEFAULT, + SpiMonitorInstance::Spim3 => PINCTRL_SPIM4_DEFAULT, + }; + scu.apply_pinctrl_group(group); +} + +/// Configure the external flash mux controls described by the board schematic. +/// +/// SPIM1/2 use GPIO A-D pin 12 plus SGPIOM output 0. +/// SPIM3/4 use GPIO E-H pin 8 plus SGPIOM output 2. +pub fn apply_spim_external_mux(instance: SpiMonitorInstance, mux: ScuExtMuxSelect) { + let high = matches!(mux, ScuExtMuxSelect::Mux1); + let (gpio_group, gpio_mask, sgpio_select) = match instance { + SpiMonitorInstance::Spim0 | SpiMonitorInstance::Spim1 => { + (ExternalMuxGpioGroup::Abcd, 1 << 12, 1 << 0) + } + SpiMonitorInstance::Spim2 | SpiMonitorInstance::Spim3 => { + (ExternalMuxGpioGroup::Efgh, 1 << 8, 1 << 2) + } + }; + + let gpio = unsafe { &*device::Gpio::ptr() }; + let scu = unsafe { &*device::Scu::ptr() }; + scu.scu41c().modify(|_, w| { + w.enbl_sgpiomaster_ckfn_pin() + .set_bit() + .enbl_sgpiomaster_ldfn_pin() + .set_bit() + .enbl_sgpiomaster_dofn_pin() + .set_bit() + .enbl_sgpiomaster_difn_pin() + .set_bit() + }); + match gpio_group { + ExternalMuxGpioGroup::Abcd => { + gpio.gpio000() + .modify(|r, w| unsafe { w.bits(update_bit(r.bits(), gpio_mask, high)) }); + gpio.gpio004() + .modify(|r, w| unsafe { w.bits(r.bits() | gpio_mask) }); + } + ExternalMuxGpioGroup::Efgh => { + gpio.gpio020() + .modify(|r, w| unsafe { w.bits(update_bit(r.bits(), gpio_mask, high)) }); + gpio.gpio024() + .modify(|r, w| unsafe { w.bits(r.bits() | gpio_mask) }); + } + } + + let sgpio = unsafe { &*device::Sgpiom::ptr() }; + sgpio.gpio554().modify(|_, w| unsafe { + w.enbl_of_serial_gpio() + .set_bit() + .numbers_of_serial_gpiopins() + .bits(16) + .serial_gpioclk_division() + .bits(24) + }); + let latch = sgpio.gpio570().read().bits(); + sgpio + .gpio500() + .write(|w| unsafe { w.bits(update_bit(latch, sgpio_select, high)) }); + + crate::delay_us(1_000); +} + +/// Read back the board-level external mux selection. +#[must_use] +pub fn spim_external_mux_state(instance: SpiMonitorInstance) -> Option { + let (gpio_group, gpio_mask, sgpio_select) = match instance { + SpiMonitorInstance::Spim0 | SpiMonitorInstance::Spim1 => { + (ExternalMuxGpioGroup::Abcd, 1 << 12, 1 << 0) + } + SpiMonitorInstance::Spim2 | SpiMonitorInstance::Spim3 => { + (ExternalMuxGpioGroup::Efgh, 1 << 8, 1 << 2) + } + }; + let gpio = unsafe { &*device::Gpio::ptr() }; + let gpio_high = match gpio_group { + ExternalMuxGpioGroup::Abcd => gpio.gpio000().read().bits() & gpio_mask != 0, + ExternalMuxGpioGroup::Efgh => gpio.gpio020().read().bits() & gpio_mask != 0, + }; + let sgpio = unsafe { &*device::Sgpiom::ptr() }; + let latch = sgpio.gpio570().read().bits(); + let sgpio_high = latch & sgpio_select != 0; + if gpio_high != sgpio_high { + None + } else if gpio_high { + Some(ScuExtMuxSelect::Mux1) + } else { + Some(ScuExtMuxSelect::Mux0) + } +} + +/// Release the active-low CPU and BMC SPI flash reset outputs. +/// +/// This mirrors `ast1060_prot_gpio_post_init()` in the Zephyr board support. +#[must_use] +pub fn release_spi_flash_resets() -> bool { + const CPU_SPI_RESET_N: u32 = 1 << 6; + const BMC_SPI_RESET_N: u32 = 1 << 7; + const RESET_MASK: u32 = CPU_SPI_RESET_N | BMC_SPI_RESET_N; + + let sgpio = unsafe { &*device::Sgpiom::ptr() }; + let latch = sgpio.gpio570().read().bits(); + sgpio + .gpio500() + .write(|w| unsafe { w.bits(latch | RESET_MASK) }); + crate::delay_us(1_000); + + sgpio.gpio570().read().bits() & RESET_MASK == RESET_MASK +} + +/// Raw board routing state for the external BMC SPI1/2 paths. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BmcSpimPathDebug { + pub scu410: u32, + pub scu4b0: u32, + pub scu690: u32, + pub scu610: u32, + pub gpio_data: u32, + pub gpio_direction: u32, + pub sgpio_latch: u32, + pub sgpio_config: u32, +} + +/// Capture the pinmux and external-mux state used by BMC SPI1/2. +#[must_use] +pub fn bmc_spim_path_debug() -> BmcSpimPathDebug { + let scu = unsafe { &*device::Scu::ptr() }; + let gpio = unsafe { &*device::Gpio::ptr() }; + let sgpio = unsafe { &*device::Sgpiom::ptr() }; + + BmcSpimPathDebug { + scu410: scu.scu410().read().bits(), + scu4b0: scu.scu4b0().read().bits(), + scu690: scu.scu690().read().bits(), + scu610: scu.scu610().read().bits(), + gpio_data: gpio.gpio000().read().bits(), + gpio_direction: gpio.gpio004().read().bits(), + sgpio_latch: sgpio.gpio570().read().bits(), + sgpio_config: sgpio.gpio554().read().bits(), + } +} + +/// Sample the active-low BMC SPI chip-select inputs. +/// +/// SPIM1 CSIN is GPIOA0 and SPIM2 CSIN is GPIOB6. The GPIO data-read +/// value register reflects the physical input levels while the pins are +/// assigned to their SPIM alternate functions. GPIO0C0 is the output-latch +/// readback and must not be used for these input signals. +#[must_use] +pub fn bmc_spim_csin_levels() -> u32 { + const SPIM1_CSIN: u32 = 1 << 0; + const SPIM2_CSIN: u32 = 1 << 14; + + let gpio = unsafe { &*device::Gpio::ptr() }; + gpio.gpio000().read().bits() & (SPIM1_CSIN | SPIM2_CSIN) +} + +/// Enable the flash-power outputs required by older AST1060 demo boards. +#[must_use] +pub fn enable_flash_power(scu: &ScuRegisters) -> bool { + scu.apply_pinctrl_group(PINCTRL_GPIOL2); + scu.apply_pinctrl_group(PINCTRL_GPIOL3); + + const FLASH_POWER_MASK: u32 = (1 << 26) | (1 << 27); + let gpio = unsafe { &*device::Gpio::ptr() }; + gpio.gpio070() + .modify(|r, w| unsafe { w.bits(r.bits() | FLASH_POWER_MASK) }); + gpio.gpio074() + .modify(|r, w| unsafe { w.bits(r.bits() | FLASH_POWER_MASK) }); + crate::delay_us(1_000); + + gpio.gpio0c8().read().bits() & FLASH_POWER_MASK == FLASH_POWER_MASK +} + +/// Assert or release the active-low BMC reset outputs. +/// +/// The prot board routes BMC_SRST to SGPIOM output 8 and BMC_EXTRST to +/// SGPIOM output 9. On assertion EXTRST is driven low first; on release SRST +/// is driven high first. +#[must_use] +pub fn set_bmc_resets(asserted: bool) -> bool { + const BMC_SRST_MASK: u32 = 1 << 8; + const BMC_EXTRST_MASK: u32 = 1 << 9; + + let scu = unsafe { &*device::Scu::ptr() }; + scu.scu41c().modify(|_, w| { + w.enbl_sgpiomaster_ckfn_pin() + .set_bit() + .enbl_sgpiomaster_ldfn_pin() + .set_bit() + .enbl_sgpiomaster_dofn_pin() + .set_bit() + .enbl_sgpiomaster_difn_pin() + .set_bit() + }); + + let sgpio = unsafe { &*device::Sgpiom::ptr() }; + sgpio.gpio554().modify(|_, w| unsafe { + w.enbl_of_serial_gpio() + .set_bit() + .numbers_of_serial_gpiopins() + .bits(16) + .serial_gpioclk_division() + .bits(24) + }); + + let output_high = !asserted; + let first_mask = if asserted { + BMC_EXTRST_MASK + } else { + BMC_SRST_MASK + }; + let second_mask = if asserted { + BMC_SRST_MASK + } else { + BMC_EXTRST_MASK + }; + + for mask in [first_mask, second_mask] { + let latch = sgpio.gpio570().read().bits(); + sgpio + .gpio500() + .write(|w| unsafe { w.bits(update_bit(latch, mask, output_high)) }); + crate::delay_us(10_000); + } + + let latch = sgpio.gpio570().read().bits(); + let reset_mask = BMC_SRST_MASK | BMC_EXTRST_MASK; + (latch & reset_mask == reset_mask) == output_high +} + +#[derive(Clone, Copy)] +enum ExternalMuxGpioGroup { + Abcd, + Efgh, +} + +const fn update_bit(value: u32, mask: u32, set: bool) -> u32 { + if set { + value | mask + } else { + value & !mask + } +} + /// Apply static SPIM wiring at controller-init time. /// -/// Order: validate → SCU route → passthrough → ext-mux → MISO multi-func → -/// SPIPF policy → SPIPF lock. The lock is one-way; an empty +/// Order: validate → pinctrl → SCU route → passthrough → ext-mux → +/// MISO multi-func → SPIPF policy → SPIPF lock. The lock is one-way; an empty /// `MonitorPolicy::empty()` combined with lock will brick the SPI bus until /// reset, so callers should pass a vetted preset (see [`presets`]). /// @@ -107,14 +366,36 @@ pub unsafe fn apply_spim_wiring( controller_id: SmcController, wiring: SpimWiring, policy: &MonitorPolicy, +) -> Result { + unsafe { apply_spim_wiring_with_log(scu, controller_id, wiring, policy, None) } +} + +/// Apply static SPIM wiring and optionally configure violation-log DMA before +/// the policy registers are locked. +/// +/// # Safety +/// The caller must satisfy [`apply_spim_wiring`] ownership requirements. A +/// supplied log buffer must remain exclusively owned by the monitor forever. +pub unsafe fn apply_spim_wiring_with_log( + scu: &ScuRegisters, + controller_id: SmcController, + wiring: SpimWiring, + policy: &MonitorPolicy, + log_buffer: Option<&'static mut [u32]>, ) -> Result { validate_controller_for_source(controller_id, wiring.source)?; scu.validate_spim_instance(wiring.instance)?; - scu.set_spim_internal_master_route(wiring.instance, wiring.source); + apply_spim_pinctrl(scu, wiring.instance); + scu.disable_spim_cs_internal_pull_down(wiring.instance); + // SPIPF monitors the external BMC/host pins. Keep SCU0F0[3:0] clear so + // neither AST1060 internal SPI controller is detoured into the monitor. + scu.set_spim_internal_mux(wiring.source, 0)?; scu.set_spim_passthrough(wiring.instance, wiring.passthrough); scu.set_spim_ext_mux(wiring.instance, wiring.ext_mux); + apply_spim_external_mux(wiring.instance, wiring.ext_mux); scu.set_spim_miso_multi_func(wiring.instance, wiring.miso_multi_func); + scu.set_spim_filter(wiring.instance, true); let monitor_controller = match wiring.instance { SpiMonitorInstance::Spim0 => SpiMonitorController::Spim0, @@ -127,6 +408,12 @@ pub unsafe fn apply_spim_wiring( // instance, mirroring the SCU exclusivity required above. let monitor = unsafe { SpiMonitor::::new(monitor_controller) }; let configured = monitor.apply_policy(policy)?; + if let Some(buffer) = log_buffer { + configured.configure_log(buffer)?; + } + configured.set_push_pull(true); + configured.set_passthrough(PassthroughMode::Disabled); + configured.enable(); let locked = configured.lock()?; Ok(locked) } @@ -146,7 +433,9 @@ fn validate_controller_for_source( /// Built-in `MonitorPolicy` presets vetted against the BMC's flash opcode set. pub mod presets { - use ast10x0_peripherals::spimonitor::MonitorPolicy; + use ast10x0_peripherals::spimonitor::{ + profile, MonitorPolicy, PrivilegeDirection, PrivilegeOp, + }; /// Allow-list for the BMC's normal flash opcodes covering both 3-byte and /// 4-byte addressing variants. Empty `regions` (no address-privilege @@ -177,4 +466,18 @@ pub mod presets { p.allow_command_count = 13; p } + + /// Policy matching the supplied Zephyr SPIM nodes: full command list and + /// write protection over flash addresses `0x0000_0000..0x0800_0000`. + #[must_use] + pub fn zephyr_spim_policy() -> MonitorPolicy { + let mut policy = profile::zephyr_default(); + let _ = policy.add_region( + 0, + 0x0010_0000, + PrivilegeDirection::Write, + PrivilegeOp::Disable, + ); + policy + } } diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel index e7318f26..08a0b726 100644 --- a/target/ast10x0/peripherals/BUILD.bazel +++ b/target/ast10x0/peripherals/BUILD.bazel @@ -1,7 +1,7 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") package(default_visibility = ["//visibility:public"]) @@ -51,6 +51,7 @@ rust_library( "smc/spi/spi.rs", "smc/spi/spi_transaction.rs", "smc/types.rs", + "spimonitor/commands.rs", "spimonitor/controller.rs", "spimonitor/mod.rs", "spimonitor/policy.rs", @@ -79,3 +80,15 @@ rust_library( "@rust_crates//:nb", ], ) + +rust_test( + name = "spimonitor_commands_test", + srcs = ["spimonitor/commands.rs"], + crate_name = "spimonitor_commands_test", + edition = "2024", + tags = ["do_not_build"], + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) diff --git a/target/ast10x0/peripherals/scu/pinctrl.rs b/target/ast10x0/peripherals/scu/pinctrl.rs index d3997168..203fe2ac 100644 --- a/target/ast10x0/peripherals/scu/pinctrl.rs +++ b/target/ast10x0/peripherals/scu/pinctrl.rs @@ -537,6 +537,13 @@ paste! { gen_pin_pairs!(SCU6B0, 0x6B0, 31); } +// GPIO +pub const PINCTRL_GPIOH2: &[PinctrlPin] = + &[CLR_PIN_SCU414_26, CLR_PIN_SCU4B4_26, CLR_PIN_SCU694_26]; +pub const PINCTRL_GPIOL2: &[PinctrlPin] = &[CLR_PIN_SCU418_26]; +pub const PINCTRL_GPIOL3: &[PinctrlPin] = &[CLR_PIN_SCU418_27]; +pub const PINCTRL_GPIOM5: &[PinctrlPin] = &[CLR_PIN_SCU41C_5]; + /// I2C1 pin group: SCL2/SDA2 mux selection on SCU414[30:31]. /// /// The SVD names these EnblSCL2FnPin/EnblSDA2FnPin, but they correspond to diff --git a/target/ast10x0/peripherals/scu/routing.rs b/target/ast10x0/peripherals/scu/routing.rs index 0ed23f63..b99d1bd7 100644 --- a/target/ast10x0/peripherals/scu/routing.rs +++ b/target/ast10x0/peripherals/scu/routing.rs @@ -3,30 +3,183 @@ //! SCU routing and mux helpers for SPI monitor integration. +use ast1060_pac as device; + use super::registers::ScuRegisters; use super::types::{ Result, ScuExtMuxSelect, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource, }; -const PIN_SPIM0_CLK_OUT_BIT: u32 = 7; -const PIN_SPIM1_CLK_OUT_BIT: u32 = 21; -const PIN_SPIM2_CLK_OUT_BIT: u32 = 3; -const PIN_SPIM3_CLK_OUT_BIT: u32 = 17; - -pub type SpimGpioOriVal = [u32; 4]; - -macro_rules! modify_reg { - ($reg:expr, $bit:expr, $clear:expr) => {{ - let mut val: u32 = $reg.read().bits(); - if $clear { - val &= !(1 << $bit); + +#[derive(Clone, Copy)] +pub struct SpimGpioOriVal { + clk_gpio_ori_val: [u32; 4], +} + +#[derive(Clone, Copy)] +struct SpimGpioInfo { + scu_register: SpimScuRegister, + scu_bit_mask: u32, + gpio_group: GpioGroup, + gpio_bit_mask: u32, +} + +#[derive(Clone, Copy)] +enum SpimScuRegister { + Scu690, + Scu694, +} + +#[derive(Clone, Copy)] +enum GpioGroup { + Abcd, + Efgh, +} + +fn gpio_regs() -> &'static device::gpio::RegisterBlock { + // SAFETY: The PAC supplies the GPIO register block base address. + unsafe { &*device::Gpio::ptr() } +} + +fn gpio_direction(group: GpioGroup) -> u32 { + match group { + GpioGroup::Abcd => gpio_regs().gpio004().read().bits(), + GpioGroup::Efgh => gpio_regs().gpio024().read().bits(), + } +} + +fn gpio_set_direction(group: GpioGroup, mask: u32, output: bool) { + let update = |bits: u32| { + if output { + bits | mask } else { - val |= 1 << $bit; + bits & !mask } - $reg.write(|w| unsafe { w.bits(val) }); - }}; + }; + + match group { + GpioGroup::Abcd => gpio_regs() + .gpio004() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + GpioGroup::Efgh => gpio_regs() + .gpio024() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + }; +} + +fn gpio_set_data(group: GpioGroup, mask: u32, high: bool) { + let update = |bits: u32| { + if high { + bits | mask + } else { + bits & !mask + } + }; + + match group { + GpioGroup::Abcd => gpio_regs() + .gpio000() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + GpioGroup::Efgh => gpio_regs() + .gpio020() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + }; +} + +fn gpio_set_output(group: GpioGroup, mask: u32, high: bool) { + gpio_set_data(group, mask, high); + gpio_set_direction(group, mask, true); +} + +// Literal translation of g_ast1060_spim_clk_gpio[] in spi_aspeed.c. +const AST1060_SPIM_CLK_GPIO: [SpimGpioInfo; 4] = [ + SpimGpioInfo { + scu_register: SpimScuRegister::Scu690, + scu_bit_mask: 1 << 7, + gpio_group: GpioGroup::Abcd, + gpio_bit_mask: 1 << 7, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu690, + scu_bit_mask: 1 << 21, + gpio_group: GpioGroup::Abcd, + gpio_bit_mask: 1 << 21, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu694, + scu_bit_mask: 1 << 3, + gpio_group: GpioGroup::Efgh, + gpio_bit_mask: 1 << 3, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu694, + scu_bit_mask: 1 << 17, + gpio_group: GpioGroup::Efgh, + gpio_bit_mask: 1 << 17, + }, +]; + +// Literal translation of g_ast1060_spim_cs_gpio[] in spi_aspeed.c. +const AST1060_SPIM_CS_GPIO: [SpimGpioInfo; 4] = [ + SpimGpioInfo { + scu_register: SpimScuRegister::Scu690, + scu_bit_mask: 1 << 1, + gpio_group: GpioGroup::Abcd, + gpio_bit_mask: 1 << 6, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu690, + scu_bit_mask: 1 << 20, + gpio_group: GpioGroup::Abcd, + gpio_bit_mask: 1 << 20, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu694, + scu_bit_mask: 1 << 2, + gpio_group: GpioGroup::Efgh, + gpio_bit_mask: 1 << 2, + }, + SpimGpioInfo { + scu_register: SpimScuRegister::Scu694, + scu_bit_mask: 1 << 16, + gpio_group: GpioGroup::Efgh, + gpio_bit_mask: 1 << 16, + }, +]; + +fn ast1060_spim_op_idx(scu0f0: u32) -> Option { + if scu0f0 & 0x7 == 0 { + return None; + } + + match (scu0f0 & 0x7) - 1 { + 2 => Some(3), + 3 => Some(2), + _ => None, + } } impl ScuRegisters { + fn set_spim_pin_function(&self, pin: SpimGpioInfo, enabled: bool) { + let update = |bits: u32| { + if enabled { + bits | pin.scu_bit_mask + } else { + bits & !pin.scu_bit_mask + } + }; + + match pin.scu_register { + SpimScuRegister::Scu690 => self + .regs() + .scu690() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + SpimScuRegister::Scu694 => self + .regs() + .scu694() + .modify(|r, w| unsafe { w.bits(update(r.bits())) }), + }; + } + /// Enable or disable passthrough for a SPI monitor instance. Uses SCU0F0[7:4]. pub fn set_spim_passthrough( &self, @@ -44,6 +197,41 @@ impl ScuRegisters { }); } + /// Enable or disable the SCU-side SPI monitor filter path. Uses SCU0F0[11:8]. + pub fn set_spim_filter(&self, instance: SpiMonitorInstance, enable: bool) { + self.unlock_write_protection(); + self.regs().scu0f0().modify(|_, w| match instance { + SpiMonitorInstance::Spim0 => w.enbl_filter_of_spipf1().bit(enable), + SpiMonitorInstance::Spim1 => w.enbl_filter_of_spipf2().bit(enable), + SpiMonitorInstance::Spim2 => w.enbl_filter_of_spipf3().bit(enable), + SpiMonitorInstance::Spim3 => w.enbl_filter_of_spipf4().bit(enable), + }); + } + + /// Disable the internal pull-down on the monitor CS output pin. + pub fn disable_spim_cs_internal_pull_down(&self, instance: SpiMonitorInstance) { + self.unlock_write_protection(); + match instance { + SpiMonitorInstance::Spim0 => { + self.regs() + .scu610() + .modify(|r, w| unsafe { w.bits(r.bits() | (1 << 6)) }); + } + SpiMonitorInstance::Spim1 => { + self.regs() + .scu610() + .modify(|r, w| unsafe { w.bits(r.bits() | (1 << 20)) }); + } + // SPIM3's corresponding pin cannot have its pull-down disabled. + SpiMonitorInstance::Spim2 => {} + SpiMonitorInstance::Spim3 => { + self.regs() + .scu614() + .modify(|r, w| unsafe { w.bits(r.bits() | (1 << 16)) }); + } + } + } + /// Route an internal SPI master through the selected SPI monitor path. pub fn set_spim_internal_master_route( &self, @@ -94,120 +282,69 @@ impl ScuRegisters { pub fn spim_proprietary_pre_config(&self) -> Option { self.unlock_write_protection(); - let scu = self.regs(); - let gpio = unsafe { &*ast1060_pac::Gpio::ptr() }; + let scu0f0 = self.regs().scu0f0().read().bits(); + pw_log::debug!("SPIM pre-config: SCU0F0=0x{:08x}", scu0f0 as u32); + let op_idx = match ast1060_spim_op_idx(scu0f0) { + Some(idx) => idx, + None => return None, + }; + pw_log::debug!( + "SPIM pre-config: active SPIM index {}, op index {}", + ((scu0f0 & 0x7) - 1) as u32, + op_idx as u32 + ); - let scu0f0 = scu.scu0f0().read().bits(); - if scu0f0 & 0x7 == 0 { - return None; - } + let clk = AST1060_SPIM_CLK_GPIO[op_idx]; + let cs = AST1060_SPIM_CS_GPIO[op_idx]; + let mut clk_gpio_ori_val = [0u32; 4]; + // Change the paired SPIM CLKOUT pin to GPIO mode. + self.set_spim_pin_function(clk, false); - let spim_idx = (scu0f0 & 0x7) - 1; - if spim_idx > 3 { - return None; - } + // Save its GPIO direction bit, then configure it as an input. + clk_gpio_ori_val[op_idx] = gpio_direction(clk.gpio_group) & clk.gpio_bit_mask; + gpio_set_direction(clk.gpio_group, clk.gpio_bit_mask, false); - let mut gpio_ori_val = [0; 4]; + // Drive the paired SPIM CSOUT GPIO high and configure it as output. + gpio_set_output(cs.gpio_group, cs.gpio_bit_mask, true); - for idx in 0..4 { - if idx as u32 == spim_idx { - continue; - } + // Change the paired SPIM CSOUT pin to GPIO mode. + self.set_spim_pin_function(cs, false); - match idx { - 0 => { - modify_reg!(scu.scu690(), PIN_SPIM0_CLK_OUT_BIT, true); - gpio_ori_val[0] = gpio.gpio004().read().bits(); - modify_reg!(gpio.gpio004(), PIN_SPIM0_CLK_OUT_BIT, true); - } - 1 => { - modify_reg!(scu.scu690(), PIN_SPIM1_CLK_OUT_BIT, true); - gpio_ori_val[1] = gpio.gpio004().read().bits(); - modify_reg!(gpio.gpio004(), PIN_SPIM1_CLK_OUT_BIT, true); - } - 2 => { - modify_reg!(scu.scu694(), PIN_SPIM2_CLK_OUT_BIT, true); - gpio_ori_val[2] = gpio.gpio024().read().bits(); - modify_reg!(gpio.gpio024(), PIN_SPIM2_CLK_OUT_BIT, true); - } - 3 => { - modify_reg!(scu.scu694(), PIN_SPIM3_CLK_OUT_BIT, true); - gpio_ori_val[3] = gpio.gpio024().read().bits(); - modify_reg!(gpio.gpio024(), PIN_SPIM3_CLK_OUT_BIT, true); - } - _ => {} - } - } - - Some(gpio_ori_val) + pw_log::debug!( + "SPIM pre-config: op index {}, saved CLK direction mask 0x{:08x}", + op_idx as u32, + clk_gpio_ori_val[op_idx] as u32 + ); + Some(SpimGpioOriVal { clk_gpio_ori_val }) } /// Restore AST1060 SPIM proprietary pin state after a transaction. pub fn spim_proprietary_post_config(&self, gpio_ori_val: SpimGpioOriVal) { self.unlock_write_protection(); - let scu = self.regs(); - let gpio = unsafe { &*ast1060_pac::Gpio::ptr() }; - - let bits = scu.scu0f0().read().bits(); - if bits.trailing_zeros() >= 3 { - return; - } + let scu0f0 = self.regs().scu0f0().read().bits(); + let op_idx = match ast1060_spim_op_idx(scu0f0) { + Some(idx) => idx, + None => return, + }; + let clk = AST1060_SPIM_CLK_GPIO[op_idx]; + let cs = AST1060_SPIM_CS_GPIO[op_idx]; + pw_log::debug!( + "SPIM post-config: SCU0F0=0x{:08x}, op index {}, saved CLK direction mask 0x{:08x}", + scu0f0 as u32, + op_idx as u32, + gpio_ori_val.clk_gpio_ori_val[op_idx] as u32 + ); - let spim_idx = (bits & 0x7) - 1; - if spim_idx > 3 { - return; - } + // Restore the paired CLKOUT GPIO direction bit. + let was_output = gpio_ori_val.clk_gpio_ori_val[op_idx] != 0; + gpio_set_direction(clk.gpio_group, clk.gpio_bit_mask, was_output); - for idx in 0..4 { - if idx as u32 == spim_idx { - continue; - } + // Return paired CLKOUT and CSOUT pins to SPIM mode. + self.set_spim_pin_function(clk, true); + self.set_spim_pin_function(cs, true); - match idx { - 0 => { - let ori_val = gpio_ori_val[0]; - gpio.gpio004().modify(|r, w| unsafe { - let mut current = r.bits(); - current &= !(1 << PIN_SPIM0_CLK_OUT_BIT); - current |= ori_val; - w.bits(current) - }); - modify_reg!(scu.scu690(), PIN_SPIM0_CLK_OUT_BIT, false); - } - 1 => { - let ori_val = gpio_ori_val[1]; - gpio.gpio004().modify(|r, w| unsafe { - let mut current = r.bits(); - current &= !(1 << PIN_SPIM1_CLK_OUT_BIT); - current |= ori_val; - w.bits(current) - }); - modify_reg!(gpio.gpio004(), PIN_SPIM1_CLK_OUT_BIT, false); - } - 2 => { - let ori_val = gpio_ori_val[2]; - gpio.gpio024().modify(|r, w| unsafe { - let mut current = r.bits(); - current &= !(1 << PIN_SPIM2_CLK_OUT_BIT); - current |= ori_val; - w.bits(current) - }); - modify_reg!(scu.scu694(), PIN_SPIM2_CLK_OUT_BIT, false); - } - 3 => { - let ori_val = gpio_ori_val[3]; - gpio.gpio024().modify(|r, w| unsafe { - let mut current = r.bits(); - current &= !(1 << PIN_SPIM3_CLK_OUT_BIT); - current |= ori_val; - w.bits(current) - }); - modify_reg!(scu.scu694(), PIN_SPIM3_CLK_OUT_BIT, false); - } - _ => {} - } - } + pw_log::debug!("SPIM post-config: restore complete"); } /// Select the external mux signal for a SPI monitor instance. Uses SCU0F0[15:12]. diff --git a/target/ast10x0/peripherals/smc/controller.rs b/target/ast10x0/peripherals/smc/controller.rs index eaf74dea..545b0280 100644 --- a/target/ast10x0/peripherals/smc/controller.rs +++ b/target/ast10x0/peripherals/smc/controller.rs @@ -618,9 +618,7 @@ impl Smc { let reg = self.regs.read_cs_ctrl(cs); self.regs .write_cs_ctrl(cs, (reg & !SPI_CTRL_FREQ_MASK) | encoded_div); - let val = self.normal_read_ctrl(cs) & ((!SPI_CTRL_FREQ_MASK) | encoded_div); - self.set_normal_read_ctrl(cs, val); - + self.set_normal_read_ctrl(cs, self.regs.read_cs_ctrl(cs)); Ok(()) } diff --git a/target/ast10x0/peripherals/spimonitor/commands.rs b/target/ast10x0/peripherals/spimonitor/commands.rs new file mode 100644 index 00000000..aa71f779 --- /dev/null +++ b/target/ast10x0/peripherals/spimonitor/commands.rs @@ -0,0 +1,201 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Hardware command-table descriptors for the AST10x0 SPI monitor. + +/// A decoded command descriptor before the valid/lock state is applied. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CommandDescriptor { + pub opcode: u8, + pub generic: bool, + pub write: bool, + pub read: bool, + pub memory: bool, + pub data_width: u8, + pub dummy_cycles: u8, + pub program_size: u8, + pub address_len: u8, + pub address_width: u8, +} + +impl CommandDescriptor { + #[must_use] + pub const fn encode(self) -> u32 { + ((self.generic as u32) << 29) + | ((self.write as u32) << 28) + | ((self.read as u32) << 27) + | ((self.memory as u32) << 26) + | ((self.data_width as u32) << 24) + | ((self.dummy_cycles as u32) << 16) + | ((self.program_size as u32) << 13) + | ((self.address_len as u32) << 10) + | ((self.address_width as u32) << 8) + | self.opcode as u32 + } +} + +#[derive(Clone, Copy)] +struct CommandFlags { + generic: bool, + write: bool, + read: bool, + memory: bool, +} + +const GENERIC_READ_MEMORY: CommandFlags = CommandFlags { + generic: true, + write: false, + read: true, + memory: true, +}; +const GENERIC_WRITE_MEMORY: CommandFlags = CommandFlags { + generic: true, + write: true, + read: false, + memory: true, +}; +const GENERIC_NONE: CommandFlags = CommandFlags { + generic: true, + write: false, + read: false, + memory: false, +}; +const GENERIC_READ: CommandFlags = CommandFlags { + generic: true, + write: false, + read: true, + memory: false, +}; +const GENERIC_WRITE: CommandFlags = CommandFlags { + generic: true, + write: true, + read: false, + memory: false, +}; +const RESERVED_NONE: CommandFlags = CommandFlags { + generic: false, + write: false, + read: false, + memory: false, +}; +const RESERVED_WRITE: CommandFlags = CommandFlags { + generic: false, + write: true, + read: false, + memory: false, +}; + +const fn command( + opcode: u8, + flags: CommandFlags, + data_width: u8, + dummy_cycles: u8, + program_size: u8, + address_len: u8, + address_width: u8, +) -> CommandDescriptor { + CommandDescriptor { + opcode, + generic: flags.generic, + write: flags.write, + read: flags.read, + memory: flags.memory, + data_width, + dummy_cycles, + program_size, + address_len, + address_width, + } +} + +/// Return the descriptor used by the Zephyr AST10x0 SPI monitor driver. +#[must_use] +pub const fn descriptor(opcode: u8) -> Option { + let entry = match opcode { + 0x03 => command(opcode, GENERIC_READ_MEMORY, 1, 0, 0, 3, 1), + 0x13 => command(opcode, GENERIC_READ_MEMORY, 1, 0, 0, 4, 1), + 0x0b => command(opcode, GENERIC_READ_MEMORY, 1, 8, 0, 3, 1), + 0x0c => command(opcode, GENERIC_READ_MEMORY, 1, 8, 0, 4, 1), + 0x3b => command(opcode, GENERIC_READ_MEMORY, 2, 8, 0, 3, 1), + 0x3c => command(opcode, GENERIC_READ_MEMORY, 2, 8, 0, 4, 1), + 0xbb => command(opcode, GENERIC_READ_MEMORY, 2, 4, 0, 3, 2), + 0xbc => command(opcode, GENERIC_READ_MEMORY, 2, 4, 0, 4, 2), + 0x6b => command(opcode, GENERIC_READ_MEMORY, 3, 8, 0, 3, 1), + 0x6c => command(opcode, GENERIC_READ_MEMORY, 3, 8, 0, 4, 1), + 0xeb => command(opcode, GENERIC_READ_MEMORY, 3, 6, 0, 3, 3), + 0xec => command(opcode, GENERIC_READ_MEMORY, 3, 6, 0, 4, 3), + 0x02 => command(opcode, GENERIC_WRITE_MEMORY, 1, 0, 1, 3, 1), + 0x12 => command(opcode, GENERIC_WRITE_MEMORY, 1, 0, 1, 4, 1), + 0x32 => command(opcode, GENERIC_WRITE_MEMORY, 3, 0, 1, 3, 1), + 0x34 => command(opcode, GENERIC_WRITE_MEMORY, 3, 0, 1, 4, 1), + 0x20 => command(opcode, GENERIC_WRITE_MEMORY, 0, 0, 1, 3, 1), + 0x21 => command(opcode, GENERIC_WRITE_MEMORY, 0, 0, 1, 4, 1), + 0xd8 => command(opcode, GENERIC_WRITE_MEMORY, 0, 0, 5, 3, 1), + 0xdc => command(opcode, GENERIC_WRITE_MEMORY, 0, 0, 5, 4, 1), + 0x06 | 0x04 | 0x50 | 0x66 | 0x99 => command(opcode, GENERIC_NONE, 0, 0, 0, 0, 0), + 0x05 | 0x35 | 0x15 | 0x70 | 0x9f => command(opcode, GENERIC_READ, 1, 0, 0, 0, 0), + 0x01 | 0x31 => command(opcode, GENERIC_WRITE, 1, 0, 0, 0, 0), + 0x5a => command(opcode, GENERIC_READ, 1, 8, 0, 3, 1), + 0xb7 | 0xe9 => command(opcode, RESERVED_NONE, 0, 0, 0, 0, 0), + 0xc5 => command(opcode, RESERVED_WRITE, 1, 0, 0, 0, 0), + 0xc2 => command(opcode, GENERIC_WRITE, 1, 0, 0, 0, 0), + _ => return None, + }; + Some(entry) +} + +pub const VALID: u32 = 1 << 30; +pub const VALID_ONCE: u32 = 1 << 31; +pub const LOCKED: u32 = 1 << 23; + +#[must_use] +pub const fn table_value(opcode: u8, valid_once: bool) -> Option { + match descriptor(opcode) { + Some(entry) => Some(entry.encode() | if valid_once { VALID_ONCE } else { VALID }), + None => None, + } +} + +#[must_use] +pub const fn fixed_slot(opcode: u8) -> Option { + match opcode { + 0xb7 => Some(0), + 0xe9 => Some(1), + 0xc5 => Some(31), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::{fixed_slot, table_value}; + + #[test] + fn fast_read_4b_matches_zephyr_encoding() { + assert_eq!(table_value(0x0c, false), Some(0x6d08_110c)); + } + + #[test] + fn reserved_commands_have_fixed_slots() { + assert_eq!(fixed_slot(0xb7), Some(0)); + assert_eq!(fixed_slot(0xe9), Some(1)); + assert_eq!(fixed_slot(0xc5), Some(31)); + } + + #[test] + fn winbond_die_select_matches_zephyr_encoding() { + assert_eq!(table_value(0xc2, false), Some(0x7100_00c2)); + } + + #[test] + fn complete_zephyr_allow_list_is_supported() { + let commands = [ + 0x03, 0x13, 0x0b, 0x0c, 0x6b, 0x6c, 0x01, 0x05, 0x35, 0x06, 0x04, 0x20, 0x21, 0x9f, + 0x5a, 0xb7, 0xe9, 0x32, 0x34, 0xd8, 0xdc, 0x02, 0x12, 0x3b, 0x3c, 0x70, 0xbb, 0xbc, + 0x50, 0xeb, 0xec, 0xc2, + ]; + for command in commands { + assert!(table_value(command, false).is_some()); + } + } +} diff --git a/target/ast10x0/peripherals/spimonitor/controller.rs b/target/ast10x0/peripherals/spimonitor/controller.rs index 39b1a1ee..252f8f38 100644 --- a/target/ast10x0/peripherals/spimonitor/controller.rs +++ b/target/ast10x0/peripherals/spimonitor/controller.rs @@ -7,6 +7,7 @@ use core::marker::PhantomData; use crate::scu::registers::ScuRegisters; use crate::scu::types::{ScuExtMuxSelect, SpiMonitorInstance}; +use crate::spimonitor::commands::{fixed_slot, table_value, LOCKED as COMMAND_LOCKED}; use crate::spimonitor::policy::{MonitorPolicy, MAX_REGION_SLOTS}; use crate::spimonitor::registers::{SpiMonitorController, SpiMonitorRegisters}; use crate::spimonitor::types::{ @@ -36,38 +37,6 @@ pub type ConfiguredSpiMonitor = SpiMonitor; /// Ergonomic alias for a locked SPI monitor handle. pub type LockedSpiMonitor = SpiMonitor; -// --------------------------------------------------------------------------- -// Encoding helpers (hardware-format definitions) -// --------------------------------------------------------------------------- - -/// Encode one address-filter slot word from a region policy entry. -/// -/// Hardware slot format (pending datasheet confirmation): -/// - bits[31:14] : region base address >> 14 (18-bit granule) -/// - bit[13] : direction (0 = read, 1 = write) -/// - bit[12] : op (0 = enable/allow, 1 = disable/block) -/// - bits[11:0] : length in 4 KiB units (length >> 12), clamped to 12 bits -/// -/// TODO: replace with confirmed SPIPF register field encoding once available. -fn encode_addr_filter_slot( - start: u32, - length: u32, - direction: PrivilegeDirection, - op: PrivilegeOp, -) -> u32 { - let addr_field = (start >> 14) & 0x3_FFFF; - let dir_bit: u32 = match direction { - PrivilegeDirection::Read => 0, - PrivilegeDirection::Write => 1, - }; - let op_bit: u32 = match op { - PrivilegeOp::Enable => 0, - PrivilegeOp::Disable => 1, - }; - let len_field = (length >> 12) & 0xFFF; - (addr_field << 14) | (dir_bit << 13) | (op_bit << 12) | len_field -} - // --------------------------------------------------------------------------- // Uninitialized state // --------------------------------------------------------------------------- @@ -99,23 +68,84 @@ impl SpiMonitor { if policy.region_count > MAX_REGION_SLOTS { return Err(SpiMonitorError::InvalidRegion); } + if self.regs.read_lock_status() & LOCK_STATUS_REQUIRED != 0 { + return Err(SpiMonitorError::Locked); + } + for slot in 0..COMMAND_TABLE_SLOTS { + if self.regs.read_allow_cmd_slot(slot) & COMMAND_LOCKED != 0 { + return Err(SpiMonitorError::Locked); + } + } - // Program command allow-list table. + let general_slot_limit = + if policy.allow_commands[..policy.allow_command_count].contains(&0xc5) { + LAST_GENERAL_COMMAND_SLOT_EXCLUSIVE + } else { + COMMAND_TABLE_SLOTS + }; + let mut general_command_count = 0usize; + for opcode in policy.allow_commands[..policy.allow_command_count] + .iter() + .copied() + { + if table_value(opcode, false).is_none() { + return Err(SpiMonitorError::UnsupportedCommand(opcode)); + } + if fixed_slot(opcode).is_none() { + general_command_count += 1; + } + } + if general_command_count > general_slot_limit - FIRST_GENERAL_COMMAND_SLOT { + return Err(SpiMonitorError::NoCommandSlot); + } + for region in policy.regions[..policy.region_count].iter().flatten() { + validate_privilege_region(region.start, region.length)?; + } + + for slot in 0..COMMAND_TABLE_SLOTS { + self.regs.write_allow_cmd_slot(slot, 0); + } + + let mut next_slot = FIRST_GENERAL_COMMAND_SLOT; + // Slots 0 and 1 are reserved for EN4B and EX4B; slot 31 is reserved + // for WREAR when it is present. Otherwise slot 31 can hold a generic + // command, allowing the supplied 32-command Zephyr policy to fit. for i in 0..policy.allow_command_count { - let cmd = policy.allow_commands[i] as u32; - self.regs.write_allow_cmd_slot(i, cmd); + let opcode = policy.allow_commands[i]; + let value = + table_value(opcode, false).ok_or(SpiMonitorError::UnsupportedCommand(opcode))?; + let slot = match fixed_slot(opcode) { + Some(slot) => slot, + None => { + if next_slot >= general_slot_limit { + return Err(SpiMonitorError::NoCommandSlot); + } + let slot = next_slot; + next_slot += 1; + slot + } + }; + self.regs.write_allow_cmd_slot(slot, value); + if self.regs.read_allow_cmd_slot(slot) != value { + return Err(SpiMonitorError::VerificationFailed); + } } - // Program address filter table. + // Empty policy means unrestricted access, matching the Zephyr driver: + // initialize both 256 MiB privilege maps to all-allowed, then apply + // the requested deny/allow regions. + initialize_privilege_table(&self.regs, PrivilegeDirection::Read)?; + initialize_privilege_table(&self.regs, PrivilegeDirection::Write)?; + for i in 0..policy.region_count { if let Some(region) = policy.regions[i] { - let word = encode_addr_filter_slot( + configure_privilege_region( + &self.regs, region.start, region.length, region.direction, region.op, - ); - self.regs.write_addr_filter_slot(i, word); + )?; } } @@ -131,6 +161,14 @@ impl SpiMonitor { pub const fn state(&self) -> MonitorState { MonitorState::Uninitialized } + + /// Pulse the SPIPF software-reset bit for at least 5 microseconds. + pub fn software_reset(&self) { + let ctrl = self.regs.read_ctrl() & !CTRL_SW_RESET_BIT; + self.regs.write_ctrl(ctrl | CTRL_SW_RESET_BIT); + delay_cycles(SW_RESET_DELAY_CYCLES); + self.regs.write_ctrl(ctrl); + } } // --------------------------------------------------------------------------- @@ -138,6 +176,128 @@ impl SpiMonitor { // --------------------------------------------------------------------------- impl SpiMonitor { + /// Reset the SPIPF transaction parser while preserving programmed policy. + /// + /// Zephyr pulses this reset immediately before handing flash ownership + /// from the RoT master to the external BMC/PCH master. + pub fn reset_filter_state(&self) { + let ctrl = self.regs.read_ctrl() & !CTRL_SW_RESET_BIT; + self.regs.write_ctrl(ctrl | CTRL_SW_RESET_BIT); + delay_cycles(SW_RESET_DELAY_CYCLES); + self.regs.write_ctrl(ctrl); + } + + /// Add or re-enable one command, matching the Zephyr shell `cmd add`. + pub fn add_command(&self, opcode: u8, valid_once: bool) -> Result { + let value = + table_value(opcode, valid_once).ok_or(SpiMonitorError::UnsupportedCommand(opcode))?; + + for slot in 0..COMMAND_TABLE_SLOTS { + let current = self.regs.read_allow_cmd_slot(slot); + if current & 0xff == u32::from(opcode) && current & COMMAND_LOCKED == 0 { + self.regs.write_allow_cmd_slot(slot, value); + return verify_command_slot(&self.regs, slot, value); + } + } + + let slot = if let Some(slot) = fixed_slot(opcode) { + let current = self.regs.read_allow_cmd_slot(slot); + if current != 0 && current & 0xff != u32::from(opcode) { + return Err(SpiMonitorError::NoCommandSlot); + } + slot + } else { + (FIRST_GENERAL_COMMAND_SLOT..COMMAND_TABLE_SLOTS) + .find(|slot| self.regs.read_allow_cmd_slot(*slot) == 0) + .ok_or(SpiMonitorError::NoCommandSlot)? + }; + if self.regs.read_allow_cmd_slot(slot) & COMMAND_LOCKED != 0 { + return Err(SpiMonitorError::Locked); + } + self.regs.write_allow_cmd_slot(slot, value); + verify_command_slot(&self.regs, slot, value) + } + + /// Disable every unlocked entry matching an opcode. + pub fn remove_command(&self, opcode: u8) -> Result { + let mut count = 0; + for slot in 0..COMMAND_TABLE_SLOTS { + let current = self.regs.read_allow_cmd_slot(slot); + if current & 0xff == u32::from(opcode) { + if current & COMMAND_LOCKED != 0 { + return Err(SpiMonitorError::Locked); + } + self.regs.write_allow_cmd_slot(slot, 0); + verify_command_slot(&self.regs, slot, 0)?; + count += 1; + } + } + if count == 0 { + return Err(SpiMonitorError::UnsupportedCommand(opcode)); + } + Ok(count) + } + + /// Lock every command-table entry matching an opcode. + pub fn lock_command(&self, opcode: u8) -> Result { + let mut count = 0; + for slot in 0..COMMAND_TABLE_SLOTS { + let current = self.regs.read_allow_cmd_slot(slot); + if current & 0xff == u32::from(opcode) { + let updated = current | COMMAND_LOCKED; + self.regs.write_allow_cmd_slot(slot, updated); + verify_command_slot(&self.regs, slot, updated)?; + count += 1; + } + } + if count == 0 { + return Err(SpiMonitorError::UnsupportedCommand(opcode)); + } + Ok(count) + } + + /// Update an address privilege region, matching the Zephyr shell + /// `addr read/write enable/disable` operations. + pub fn configure_region( + &self, + start: u32, + length: u32, + direction: PrivilegeDirection, + op: PrivilegeOp, + ) -> Result<()> { + let lock = self.regs.read_lock_status(); + let locked = match direction { + PrivilegeDirection::Read => lock & LOCK_READ_PRIVILEGE != 0, + PrivilegeDirection::Write => lock & LOCK_WRITE_PRIVILEGE != 0, + }; + if locked { + return Err(SpiMonitorError::Locked); + } + configure_privilege_region(&self.regs, start, length, direction, op) + } + + /// Read one word from the selected privilege bitmap. + pub fn privilege_word(&self, direction: PrivilegeDirection, index: usize) -> Result { + if index >= PRIVILEGE_TABLE_WORDS { + return Err(SpiMonitorError::InvalidSlot); + } + select_privilege_table(&self.regs, direction); + Ok(self.regs.read_addr_filter_slot(index)) + } + + /// Lock one privilege bitmap, matching the shell `addr lock read/write`. + pub fn lock_privilege_table(&self, direction: PrivilegeDirection) -> Result<()> { + let mask = match direction { + PrivilegeDirection::Read => LOCK_READ_PRIVILEGE, + PrivilegeDirection::Write => LOCK_WRITE_PRIVILEGE, + }; + self.regs.modify_lock_status(|bits| *bits |= mask); + if self.regs.read_lock_status() & mask != mask { + return Err(SpiMonitorError::LockFailed); + } + Ok(()) + } + /// Enable the monitor filter (SPIPF000 bit 0). /// /// Mirrors Zephyr's `spim_monitor_enable(dev, true)`. @@ -160,8 +320,13 @@ impl SpiMonitor { /// Mirrors Zephyr's `spim_passthrough_config`. pub fn set_passthrough(&self, mode: PassthroughMode) { self.regs.modify_ctrl(|bits| match mode { - PassthroughMode::Enabled => *bits |= CTRL_PASSTHROUGH_BIT, - PassthroughMode::Disabled => *bits &= !CTRL_PASSTHROUGH_BIT, + PassthroughMode::Enabled => { + *bits = (*bits & !CTRL_PASSTHROUGH_MASK) | CTRL_SINGLE_PASSTHROUGH_BIT + } + PassthroughMode::MultiEnabled => { + *bits = (*bits & !CTRL_PASSTHROUGH_MASK) | CTRL_MULTI_PASSTHROUGH_BIT + } + PassthroughMode::Disabled => *bits &= !CTRL_PASSTHROUGH_MASK, }); } @@ -212,6 +377,57 @@ impl SpiMonitor { drain_log_impl(&self.regs, buf) } + /// Configure a caller-owned, static DMA buffer for violation logging. + /// + /// The hardware stores one 32-bit violation record per entry. + pub fn configure_log(&self, buffer: &'static mut [u32]) -> Result<()> { + if buffer.is_empty() || buffer.len() > MAX_LOG_ENTRIES { + return Err(SpiMonitorError::InvalidLogBuffer); + } + let address = buffer.as_mut_ptr() as usize; + if address & 0x3 != 0 || address > u32::MAX as usize { + return Err(SpiMonitorError::InvalidLogBuffer); + } + buffer.fill(0); + self.regs + .write_log_config(address as u32, buffer.len() as u32); + Ok(()) + } + + /// Use push-pull signaling for the monitor output path. + pub fn set_push_pull(&self, enable: bool) { + self.regs.modify_ctrl2(|bits| { + if enable { + *bits |= CTRL2_PUSH_PULL; + } else { + *bits &= !CTRL2_PUSH_PULL; + } + }); + } + + /// Enable command, write, and read violation interrupts in SPIPF004. + /// + /// Platform code must install and enable the corresponding NVIC handler. + pub fn enable_violation_interrupts(&self) { + self.regs + .modify_ctrl2(|bits| *bits |= CTRL2_VIOLATION_IRQ_ENABLE_MASK); + } + + /// Return currently pending command/write/read violation status bits. + #[must_use] + pub fn pending_violations(&self) -> u32 { + self.regs.read_ctrl2() & CTRL2_VIOLATION_STATUS_MASK + } + + /// Acknowledge all currently pending violation status bits. + pub fn acknowledge_violations(&self) -> u32 { + let pending = self.pending_violations(); + if pending != 0 { + self.regs.modify_ctrl2(|bits| *bits |= pending); + } + pending + } + /// Lock monitor policy registers and transition to `Locked`. /// /// Activates all write-protection bits to prevent further policy changes. @@ -220,9 +436,31 @@ impl SpiMonitor { /// - Lock all command table entries /// - Write-disable SPIPF000, SPIPF004, SPIPF010, SPIPF014 pub fn lock(self) -> Result> { - // Placeholder: This single bit write is incomplete. - // Full lock requires SPIPF07C write-disable bits per aspeed-rust pattern. - self.regs.modify_ctrl(|bits| *bits |= CTRL_LOCK_BIT); + for slot in 0..COMMAND_TABLE_SLOTS { + let value = self.regs.read_allow_cmd_slot(slot); + self.regs.write_allow_cmd_slot(slot, value | COMMAND_LOCKED); + } + + self.regs + .modify_ctrl(|bits| *bits |= CTRL_BLOCK_FIFO_LOCK | CTRL_SW_RESET_LOCK); + self.regs.modify_lock_status(|bits| { + *bits |= LOCK_CTRL + | LOCK_IRQ_CTRL + | LOCK_LOG_BASE + | LOCK_LOG_CTRL + | LOCK_WRITE_PRIVILEGE + | LOCK_READ_PRIVILEGE; + }); + + let lock_status = self.regs.read_lock_status(); + if lock_status & LOCK_STATUS_REQUIRED != LOCK_STATUS_REQUIRED { + return Err(SpiMonitorError::LockFailed); + } + for slot in 0..COMMAND_TABLE_SLOTS { + if self.regs.read_allow_cmd_slot(slot) & COMMAND_LOCKED == 0 { + return Err(SpiMonitorError::LockFailed); + } + } Ok(SpiMonitor { regs: self.regs, @@ -243,17 +481,6 @@ impl SpiMonitor { // --------------------------------------------------------------------------- impl SpiMonitor { - /// Configure passthrough mode in locked state. - /// - /// Passthrough is intentionally available post-lock because it is used - /// during mux ownership transitions at runtime (e.g., BMC boot-hold/release). - pub fn set_passthrough(&self, mode: PassthroughMode) { - self.regs.modify_ctrl(|bits| match mode { - PassthroughMode::Enabled => *bits |= CTRL_PASSTHROUGH_BIT, - PassthroughMode::Disabled => *bits &= !CTRL_PASSTHROUGH_BIT, - }); - } - /// Select the external SPI mux routing in locked state. /// /// Available post-lock for mux ownership transitions at runtime (e.g., BMC boot-hold/release). @@ -329,20 +556,139 @@ impl SpiMonitor { /// /// Confirmed from aspeed-rust implementation (src/spimonitor/hardware.rs). /// Register field names from ast1060_pac provide safe typed accessors. -const CTRL_MONITOR_ENABLE_BIT: u32 = 1 << 0; // enbl_filter_fn() in SPIPF000[0] -const CTRL_PASSTHROUGH_BIT: u32 = 1 << 1; // enbl_single_bit_passthrough() in SPIPF000[1] -#[allow(dead_code)] -const CTRL_SW_RESET_BIT: u32 = 1 << 0; // sweng_rst() in SPIPF000[?] - uses PAC field +const CTRL_SINGLE_PASSTHROUGH_BIT: u32 = 1 << 0; +const CTRL_MULTI_PASSTHROUGH_BIT: u32 = 1 << 1; +const CTRL_PASSTHROUGH_MASK: u32 = (1 << 0) | (1 << 1); +const CTRL_MONITOR_ENABLE_BIT: u32 = 1 << 2; #[allow(dead_code)] -const CTRL_EXT_MUX_SEL_BIT: u32 = 1 << 2; // PLACEHOLDER - NOT in SPIPF000! See note below. -#[allow(dead_code)] -const CTRL_LOCK_BIT: u32 = 1 << 31; // PLACEHOLDER - NOT in SPIPF000! See note below. - // - // NOTE: CTRL_EXT_MUX_SEL and CTRL_LOCK are NOT in SPIPF000 register: - // - ExtMux is controlled via SCU0F0 register (ext_mux_select_sig_of_spipfN bits) - // See aspeed-rust: spim_ext_mux_config() - // - Lock is controlled via SPIPF07C write-disable bits and individual command - // table entry lock bits. See aspeed-rust: spim_lock_common(), spim_lock_rw_priv_table() +const CTRL_SW_RESET_BIT: u32 = 1 << 15; +const CTRL_BLOCK_FIFO_LOCK: u32 = 1 << 22; +const CTRL_SW_RESET_LOCK: u32 = 1 << 23; +const CTRL2_VIOLATION_STATUS_MASK: u32 = 0x7; +const CTRL2_VIOLATION_IRQ_ENABLE_MASK: u32 = 0x7 << 16; +const CTRL2_PUSH_PULL: u32 = 1 << 31; +const COMMAND_TABLE_SLOTS: usize = 32; +const FIRST_GENERAL_COMMAND_SLOT: usize = 2; +const LAST_GENERAL_COMMAND_SLOT_EXCLUSIVE: usize = 31; +const PRIVILEGE_TABLE_WORDS: usize = 512; +const PRIVILEGE_BLOCK_SIZE: u32 = 16 * 1024; +const PRIVILEGE_ADDRESS_LIMIT: u32 = 256 * 1024 * 1024; +const PRIVILEGE_READ_SELECT: u32 = 0x5200_0000; +const PRIVILEGE_WRITE_SELECT: u32 = 0x5700_0000; +const MAX_LOG_ENTRIES: usize = 0x7_ffff; +const SW_RESET_DELAY_CYCLES: u32 = 1_000; + +const LOCK_CTRL: u32 = 1 << 0; +const LOCK_IRQ_CTRL: u32 = 1 << 1; +const LOCK_LOG_BASE: u32 = 1 << 4; +const LOCK_LOG_CTRL: u32 = 1 << 5; +const LOCK_WRITE_PRIVILEGE: u32 = 1 << 30; +const LOCK_READ_PRIVILEGE: u32 = 1 << 31; +const LOCK_STATUS_REQUIRED: u32 = LOCK_CTRL + | LOCK_IRQ_CTRL + | LOCK_LOG_BASE + | LOCK_LOG_CTRL + | LOCK_WRITE_PRIVILEGE + | LOCK_READ_PRIVILEGE; + +fn select_privilege_table(regs: &SpiMonitorRegisters, direction: PrivilegeDirection) { + let selection = match direction { + PrivilegeDirection::Read => PRIVILEGE_READ_SELECT, + PrivilegeDirection::Write => PRIVILEGE_WRITE_SELECT, + }; + regs.modify_ctrl(|bits| *bits = (*bits & 0x00ff_ffff) | selection); +} + +fn verify_command_slot(regs: &SpiMonitorRegisters, slot: usize, expected: u32) -> Result { + if regs.read_allow_cmd_slot(slot) != expected { + return Err(SpiMonitorError::VerificationFailed); + } + Ok(slot) +} + +fn initialize_privilege_table( + regs: &SpiMonitorRegisters, + direction: PrivilegeDirection, +) -> Result<()> { + select_privilege_table(regs, direction); + for index in 0..PRIVILEGE_TABLE_WORDS { + regs.write_addr_filter_slot(index, u32::MAX); + } + if regs.read_addr_filter_slot(0) != u32::MAX + || regs.read_addr_filter_slot(PRIVILEGE_TABLE_WORDS - 1) != u32::MAX + { + return Err(SpiMonitorError::VerificationFailed); + } + Ok(()) +} + +fn configure_privilege_region( + regs: &SpiMonitorRegisters, + start: u32, + length: u32, + direction: PrivilegeDirection, + op: PrivilegeOp, +) -> Result<()> { + validate_privilege_region(start, length)?; + let end = start + .checked_add(length) + .ok_or(SpiMonitorError::InvalidLength)? + .min(PRIVILEGE_ADDRESS_LIMIT); + let aligned_start = start / PRIVILEGE_BLOCK_SIZE * PRIVILEGE_BLOCK_SIZE; + let aligned_end = end + .checked_add(PRIVILEGE_BLOCK_SIZE - 1) + .ok_or(SpiMonitorError::InvalidLength)? + / PRIVILEGE_BLOCK_SIZE + * PRIVILEGE_BLOCK_SIZE; + let mut block = aligned_start / PRIVILEGE_BLOCK_SIZE; + let end_block = aligned_end / PRIVILEGE_BLOCK_SIZE; + + select_privilege_table(regs, direction); + while block < end_block { + let word_index = (block / 32) as usize; + let bit_index = block % 32; + let remaining = end_block - block; + let updated = if bit_index == 0 && remaining >= 32 { + block += 32; + match op { + PrivilegeOp::Enable => u32::MAX, + PrivilegeOp::Disable => 0, + } + } else { + block += 1; + let value = regs + .read_addr_filter_slot_checked(word_index) + .ok_or(SpiMonitorError::InvalidAddress)?; + match op { + PrivilegeOp::Enable => value | (1 << bit_index), + PrivilegeOp::Disable => value & !(1 << bit_index), + } + }; + if !regs.write_addr_filter_slot_checked(word_index, updated) { + return Err(SpiMonitorError::InvalidAddress); + } + if regs.read_addr_filter_slot_checked(word_index) != Some(updated) { + return Err(SpiMonitorError::VerificationFailed); + } + } + Ok(()) +} + +fn validate_privilege_region(start: u32, length: u32) -> Result<()> { + if start >= PRIVILEGE_ADDRESS_LIMIT { + return Err(SpiMonitorError::InvalidAddress); + } + if length == 0 || start.checked_add(length).is_none() { + return Err(SpiMonitorError::InvalidLength); + } + Ok(()) +} + +fn delay_cycles(cycles: u32) { + for _ in 0..cycles { + core::hint::spin_loop(); + } +} /// Shared drain-log implementation used by both `Configured` and `Locked`. fn drain_log_impl<'a>( @@ -350,7 +696,7 @@ fn drain_log_impl<'a>( buf: &'a mut [ViolationLogEntry], ) -> &'a [ViolationLogEntry] { let log_base = regs.log_ram_base_addr(); - let max_entries = regs.read_log_max_sz() as usize / core::mem::size_of::(); + let max_entries = regs.read_log_capacity_entries() as usize; let write_idx = regs.read_log_idx_reg() as usize; let available = write_idx.min(max_entries); diff --git a/target/ast10x0/peripherals/spimonitor/mod.rs b/target/ast10x0/peripherals/spimonitor/mod.rs index 8f649e14..4fb11754 100644 --- a/target/ast10x0/peripherals/spimonitor/mod.rs +++ b/target/ast10x0/peripherals/spimonitor/mod.rs @@ -3,6 +3,7 @@ //! AST10x0 SPI monitor (SPIPF) module. +pub mod commands; pub mod controller; pub mod policy; pub mod profile; @@ -10,6 +11,7 @@ pub mod registers; pub mod traits; pub mod types; +pub use commands::{descriptor as command_descriptor, table_value as command_table_value}; pub use controller::{ Configured, ConfiguredSpiMonitor, Locked, LockedSpiMonitor, SpiMonitor, UninitSpiMonitor, Uninitialized, diff --git a/target/ast10x0/peripherals/spimonitor/profile.rs b/target/ast10x0/peripherals/spimonitor/profile.rs index 4e498dfe..e3f35e33 100644 --- a/target/ast10x0/peripherals/spimonitor/profile.rs +++ b/target/ast10x0/peripherals/spimonitor/profile.rs @@ -30,3 +30,16 @@ pub const fn firmware_update_window() -> MonitorPolicy { p.allow_command_count = 6; p } + +/// Full command allow-list used by the AST1060 Zephyr device tree. +#[must_use] +pub const fn zephyr_default() -> MonitorPolicy { + let mut p = MonitorPolicy::empty(); + p.allow_commands = [ + 0x03, 0x13, 0x0b, 0x0c, 0x6b, 0x6c, 0x01, 0x05, 0x35, 0x06, 0x04, 0x20, 0x21, 0x9f, 0x5a, + 0xb7, 0xe9, 0x32, 0x34, 0xd8, 0xdc, 0x02, 0x12, 0x3b, 0x3c, 0x70, 0xbb, 0xbc, 0x50, 0xeb, + 0xec, 0xc2, + ]; + p.allow_command_count = p.allow_commands.len(); + p +} diff --git a/target/ast10x0/peripherals/spimonitor/registers.rs b/target/ast10x0/peripherals/spimonitor/registers.rs index 3fcf02e7..01fd6151 100644 --- a/target/ast10x0/peripherals/spimonitor/registers.rs +++ b/target/ast10x0/peripherals/spimonitor/registers.rs @@ -118,6 +118,17 @@ impl SpiMonitorRegisters { self.regs().spipf004().write(|w| unsafe { w.bits(value) }); } + pub fn modify_ctrl2(&self, f: F) + where + F: FnOnce(&mut u32), + { + self.regs().spipf004().modify(|r, w| { + let mut bits = r.bits(); + f(&mut bits); + unsafe { w.bits(bits) } + }); + } + /// SPIPF07C: Lock/status register. pub fn read_lock_status(&self) -> u32 { self.regs().spipf07c().read().bits() @@ -127,6 +138,17 @@ impl SpiMonitorRegisters { self.regs().spipf07c().write(|w| unsafe { w.bits(value) }); } + pub fn modify_lock_status(&self, f: F) + where + F: FnOnce(&mut u32), + { + self.regs().spipf07c().modify(|r, w| { + let mut bits = r.bits(); + f(&mut bits); + unsafe { w.bits(bits) } + }); + } + /// SPIPFWT[n]: Allow-command table entry. pub fn read_allow_cmd_slot(&self, index: usize) -> u32 { self.regs().spipfwt(index).read().bits() @@ -138,6 +160,14 @@ impl SpiMonitorRegisters { .write(|w| unsafe { w.bits(value) }); } + /// Read an allow-command slot without panicking for an invalid index. + pub fn read_allow_cmd_slot_checked(&self, index: usize) -> Option { + self.regs() + .spipfwt_iter() + .nth(index) + .map(|register| register.read().bits()) + } + /// SPIPFWA[n]: Address filter table entry. pub fn read_addr_filter_slot(&self, index: usize) -> u32 { self.regs().spipfwa(index).read().bits() @@ -149,45 +179,60 @@ impl SpiMonitorRegisters { .write(|w| unsafe { w.bits(value) }); } + /// Read an address-filter slot without panicking for an invalid index. + pub fn read_addr_filter_slot_checked(&self, index: usize) -> Option { + self.regs() + .spipfwa_iter() + .nth(index) + .map(|register| register.read().bits()) + } + + /// Write an address-filter slot, returning false for an invalid index. + #[must_use] + pub fn write_addr_filter_slot_checked(&self, index: usize, value: u32) -> bool { + let Some(register) = self.regs().spipfwa_iter().nth(index) else { + return false; + }; + register.write(|w| unsafe { w.bits(value) }); + true + } + // ----------------------------------------------------------------------- // Violation log registers - // - // TODO: confirm SPIPF register offsets for log control from the AST10x0 - // datasheet once available. Offsets below are placeholders consistent with - // known Aspeed SPIPF register map patterns. // ----------------------------------------------------------------------- - /// Current violation log write index (number of entries written so far). - /// - /// Maps to the SPIPF log index register (placeholder offset 0x080). + /// Current violation log write index (number of 32-bit entries written). pub fn read_log_idx_reg(&self) -> u32 { - // SAFETY: raw offset read within the known SPIPF register block page. - unsafe { - let ptr = (self.base as *const u8).add(0x080) as *const u32; - core::ptr::read_volatile(ptr) - } + self.regs() + .spipf018() + .read() + .block_log_dmawr_pointer() + .bits() } - /// Maximum violation log capacity in bytes. - /// - /// Maps to the SPIPF log size register (placeholder offset 0x084). - pub fn read_log_max_sz(&self) -> u32 { - // SAFETY: same as above. - unsafe { - let ptr = (self.base as *const u8).add(0x084) as *const u32; - core::ptr::read_volatile(ptr) - } + /// Maximum violation log capacity in 32-bit entries. + pub fn read_log_capacity_entries(&self) -> u32 { + self.regs() + .spipf014() + .read() + .size_of_block_log_dmabuffer() + .bits() } /// Base address of the violation log RAM region. - /// - /// Returns a `usize` suitable for casting to `*const u32` by the caller. - /// Maps to the SPIPF log RAM address register (placeholder offset 0x088). pub fn log_ram_base_addr(&self) -> usize { - // SAFETY: same as above. - unsafe { - let ptr = (self.base as *const u8).add(0x088) as *const u32; - core::ptr::read_volatile(ptr) as usize - } + (self.regs().spipf010().read().bits() & !0x3) as usize + } + + /// Configure the violation-log DMA buffer. + pub fn write_log_config(&self, base_addr: u32, entries: u32) { + self.regs() + .spipf010() + .write(|w| unsafe { w.bits(base_addr & !0x3) }); + self.regs() + .spipf014() + .write(|w| unsafe { w.bits(LOG_DMA_ENABLE | entries) }); } } + +const LOG_DMA_ENABLE: u32 = 1 << 31; diff --git a/target/ast10x0/peripherals/spimonitor/types.rs b/target/ast10x0/peripherals/spimonitor/types.rs index 4be2d823..5c00cec7 100644 --- a/target/ast10x0/peripherals/spimonitor/types.rs +++ b/target/ast10x0/peripherals/spimonitor/types.rs @@ -28,7 +28,10 @@ pub enum PrivilegeOp { /// Mirrors Zephyr's `spim_passthrough_config`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PassthroughMode { + /// Enable single-bit passthrough. Enabled, + /// Enable multi-bit passthrough. + MultiEnabled, Disabled, } @@ -127,8 +130,15 @@ impl ViolationLogEntry { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SpiMonitorError { InvalidRegion, + InvalidAddress, + InvalidLength, InvalidSlot, + InvalidLogBuffer, + UnsupportedCommand(u8), + NoCommandSlot, Locked, + LockFailed, + VerificationFailed, InvalidTransition, } diff --git a/target/ast10x0/tests/smc/read/BUILD.bazel b/target/ast10x0/tests/smc/read/BUILD.bazel index d740fafe..d437777e 100644 --- a/target/ast10x0/tests/smc/read/BUILD.bazel +++ b/target/ast10x0/tests/smc/read/BUILD.bazel @@ -50,6 +50,29 @@ rust_binary( ], ) +rust_binary( + name = "target_spi2", + srcs = [ + "target_spi2.rs", + "//target/ast10x0/tests/smc:target_debug.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + rust_binary( name = "target", srcs = [ @@ -95,6 +118,28 @@ system_image_test( visibility = ["//visibility:public"], ) +system_image( + name = "smc_spi2_read_test", + kernel = ":target_spi2", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + userspace = False, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "smc_spi2_read_evb_test", + image = ":smc_spi2_read_test", + tags = ["hardware"], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], +) + rust_binary_no_panics_test( name = "spi1_no_panics_test", binary = ":smc_spi1_read_test", diff --git a/target/ast10x0/tests/smc/read/target_spi2.rs b/target/ast10x0/tests/smc/read/target_spi2.rs new file mode 100644 index 00000000..e3407062 --- /dev/null +++ b/target/ast10x0/tests/smc/read/target_spi2.rs @@ -0,0 +1,166 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 SMC SPI2 read smoke test target. + +#![no_std] +#![no_main] +use ast10x0_board::{apply_spim_external_mux, enable_flash_power}; +#[allow(unused_imports)] +use ast10x0_peripherals::scu::{ + pinctrl::{PINCTRL_SPI2_QUAD, PINCTRL_SPIM3_DEFAULT, PINCTRL_SPIM4_DEFAULT}, + ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource, +}; +use ast10x0_peripherals::smc::{ + ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiTransaction, + SpiUninit, TransferMode, +}; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +#[path = "../target_debug.rs"] +mod target_debug; +use target_debug::{dump_smc_read, dump_smc_register}; + +const SPI_FLASH_CONFIG: FlashConfig = FlashConfig { + capacity_mb: 32, + page_size: 256, + sector_size: 4096, + block_size: 65536, + spi_clock_mhz: 25, +}; + +pub struct Target {} + +fn config_spi2_master_controller() -> Result<(), SmcError> { + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + scu.apply_pinctrl_group(PINCTRL_SPIM3_DEFAULT); + scu.apply_pinctrl_group(PINCTRL_SPIM4_DEFAULT); + scu.apply_pinctrl_group(PINCTRL_SPI2_QUAD); + if !enable_flash_power(&scu) { + pw_log::info!("GPIOL2/GPIOL3 flash power readback failed"); + return Err(SmcError::HardwareError); + } + apply_spim_external_mux(SpiMonitorInstance::Spim2, ScuExtMuxSelect::Mux1); + + // Configure the mux for the SPI master controller path. + scu.set_spim_internal_master_route(SpiMonitorInstance::Spim2, SpiMonitorSource::Spi2); + scu.set_spim_passthrough(SpiMonitorInstance::Spim2, SpiMonitorPassthrough::Enabled); + scu.set_spim_ext_mux(SpiMonitorInstance::Spim2, ScuExtMuxSelect::Mux1); + pw_log::info!("SCU pinmux and SPIM routing configured for SPI2 monitoring"); + + for _ in 0..1_000_000 { + core::hint::spin_loop(); + } + Ok(()) +} + +fn run_spi2_read_test() -> Result<(), SmcError> { + config_spi2_master_controller()?; + + let config = SmcConfig { + controller_id: SmcController::Spi2, + cs0: Some(SPI_FLASH_CONFIG), + cs1: None, + dma_enabled: true, + enable_interrupts: false, + topology: SmcTopology::NormalSpi { master_idx: 2 }, + }; + + pw_log::info!("=== AST10x0 SMC SPI2 read test ==="); + let spi = unsafe { SpiUninit::new(SmcController::Spi2, config)? }; + let mut spi = spi.init()?; + spi.spi_nor_read_init(ChipSelect::Cs0)?; + + if !spi.is_ready() { + return Err(SmcError::HardwareError); + } + + pw_log::info!("=== SPI2 controller register ==="); + dump_smc_register(0x7E64_0000, 16); + dump_smc_register(0x7E64_0080, 16); + pw_log::info!("=== SCU QSPI Mux routing register ==="); + dump_smc_register(0x7E6E_20F0, 2); + dump_smc_register(0x7E6E_2418, 2); + dump_smc_register(0x7e6e_2694, 2); + dump_smc_register(0x7E78_0020, 2); + dump_smc_register(0x7E78_0070, 2); + let mut jedec = [0u8; 3]; + SpiTransaction::transceive_user_with_spim( + &mut spi, + SpiMonitorInstance::Spim2, + ChipSelect::Cs0, + &[0x9f], + &[], + &mut jedec, + TransferMode::Mode111, + )?; + pw_log::info!( + "SPI2 CS0 JEDEC ID: {:02x} {:02x} {:02x}", + jedec[0] as u32, + jedec[1] as u32, + jedec[2] as u32 + ); + + if jedec[0] == 0xff { + pw_log::info!("SPI2 CS0 JEDEC manufacturer is 0xff; skipping read test"); + return Ok(()); + } + + pw_log::info!("=== SPI2 read ==="); + let mut buf = [0u8; 64]; + let n = SpiTransaction::read_with_spim( + &mut spi, + SpiMonitorInstance::Spim2, + ChipSelect::Cs0, + 0x0, + &mut buf, + )?; + if n != buf.len() { + return Err(SmcError::HardwareError); + } + dump_smc_read(&buf, buf.len() as u32); + + pw_log::info!("=== SPI2 DMA read @ 0x00000000 ==="); + let dma_buf = unsafe { core::slice::from_raw_parts_mut(0x41500 as *mut u8, 256) }; + let mut dma_txn = SpiTransaction::dma_read_with_spim( + &mut spi, + SpiMonitorInstance::Spim2, + ChipSelect::Cs0, + 0x0, + 0x41500usize, + dma_buf.len() as u32, + )?; + + loop { + match dma_txn.poll_dma_completion() { + core::task::Poll::Pending => {} + core::task::Poll::Ready(result) => { + result?; + break; + } + } + } + dump_smc_read(dma_buf, dma_buf.len() as u32); + + Ok(()) +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 SMC SPI2 read Test"; + + fn main() -> ! { + let sentinel = if run_spi2_read_test().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/spimonitor/BUILD.bazel b/target/ast10x0/tests/spimonitor/BUILD.bazel new file mode 100644 index 00000000..8618bbf1 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/BUILD.bazel @@ -0,0 +1,49 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") +load(":spimonitor_test.bzl", "spimonitor_setup_all", "spimonitor_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +spimonitor_test(1, TARGET_COMPATIBLE_WITH) + +spimonitor_test(2, TARGET_COMPATIBLE_WITH) + +spimonitor_test(3, TARGET_COMPATIBLE_WITH) + +spimonitor_test(4, TARGET_COMPATIBLE_WITH) + +spimonitor_setup_all(TARGET_COMPATIBLE_WITH) + +alias( + name = "target", + actual = ":target_spim1", + visibility = ["//visibility:public"], +) + +alias( + name = "spimonitor_test", + actual = ":spimonitor1_test", + visibility = ["//visibility:public"], +) diff --git a/target/ast10x0/tests/spimonitor/setup_all_spim.rs b/target/ast10x0/tests/spimonitor/setup_all_spim.rs new file mode 100644 index 00000000..b6ba08cc --- /dev/null +++ b/target/ast10x0/tests/spimonitor/setup_all_spim.rs @@ -0,0 +1,576 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Configure SPIM1 through SPIM4 and keep all monitors active. + +#![no_std] +#![no_main] + +use core::cell::UnsafeCell; + +#[path = "test_common.rs"] +mod test_common; + +use ast10x0_board::{ + apply_spim_external_mux, bmc_spim_csin_levels, bmc_spim_path_debug, delay_us, + enable_flash_power, release_spi_flash_resets, set_bmc_resets, spim_external_mux_state, +}; +use ast10x0_peripherals::scu::{ + pinctrl::PINCTRL_SPI1_QUAD, ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorSource, +}; +use ast10x0_peripherals::smc::{ + ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiReady, SpiUninit, + TransferMode, +}; +use ast10x0_peripherals::spimonitor::registers::SpiMonitorRegisters; +use ast10x0_peripherals::spimonitor::{ + ConfiguredSpiMonitor, LockedSpiMonitor, MonitorPolicy, PrivilegeDirection, PrivilegeOp, + SpiMonitorController, ViolationLogEntry, +}; +use target_common::{declare_target, TargetInterface}; +use test_common::TestConfig; +use {console_backend as _, entry as _}; + +struct Spim1Config; +struct Spim2Config; +struct Spim3Config; +struct Spim4Config; + +impl test_common::TestConfig for Spim1Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim0; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim0; +} + +impl test_common::TestConfig for Spim2Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim1; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim1; +} + +impl test_common::TestConfig for Spim3Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim2; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim2; +} + +impl test_common::TestConfig for Spim4Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim3; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim3; +} + +#[repr(align(16))] +struct LogRam(UnsafeCell<[u32; test_common::LOG_RAM_WORDS]>); + +// SAFETY: Each buffer is exclusively assigned to one SPIPF instance. +unsafe impl Sync for LogRam {} + +static SPIM1_LOG: LogRam = LogRam(UnsafeCell::new([0; test_common::LOG_RAM_WORDS])); +static SPIM2_LOG: LogRam = LogRam(UnsafeCell::new([0; test_common::LOG_RAM_WORDS])); +static SPIM3_LOG: LogRam = LogRam(UnsafeCell::new([0; test_common::LOG_RAM_WORDS])); +static SPIM4_LOG: LogRam = LogRam(UnsafeCell::new([0; test_common::LOG_RAM_WORDS])); + +const WRITE_PROTECTED_LENGTH: u32 = 0x0010_0000; +const MONITOR_POLL_INTERVAL_US: u32 = 10_000; +const BMC_CSIN_RECOVERY_TIMEOUT_US: u32 = 500_000; +const BMC_RECOVERY_RETRY_DELAY_US: u32 = 1_000_000; +const BMC_CSIN_MASK: u32 = (1 << 0) | (1 << 14); +const ENABLE_RUNTIME_DEBUG_LOGS: bool = false; +const BMC_FLASH_CONFIG: FlashConfig = FlashConfig { + capacity_mb: 128, + page_size: 256, + sector_size: 4096, + block_size: 65536, + spi_clock_mhz: 25, +}; +const ALLOW_COMMANDS: [u8; 32] = [ + 0x03, 0x13, 0x0b, 0x0c, 0x6b, 0x6c, 0x01, 0x05, 0x35, 0x06, 0x04, 0x20, 0x21, 0x9f, 0x5a, 0xb7, + 0xe9, 0x32, 0x34, 0xd8, 0xdc, 0x02, 0x12, 0x3b, 0x3c, 0x70, 0xbb, 0xbc, 0x50, 0xeb, 0xec, 0xc2, +]; + +fn log_buffer(log: &'static LogRam) -> &'static mut [u32] { + // SAFETY: The caller assigns each static buffer to exactly one monitor. + unsafe { &mut *log.0.get() } +} + +fn raw_log_word(log: &'static LogRam, index: usize) -> u32 { + // SAFETY: The index comes from the bounded SPIPF log pointer. + unsafe { core::ptr::read_volatile((*log.0.get()).as_ptr().add(index)) } +} + +fn log_violation(id: u32, index: u32, word: u32) { + match ViolationLogEntry::parse(word) { + ViolationLogEntry::BlockedCommand(command) => pw_log::info!( + "SPIM{} BLOCKED COMMAND log[{}]: cmd=0x{:02x}, raw=0x{:08x}", + id as u32, + index as u32, + command as u32, + word as u32 + ), + ViolationLogEntry::BlockedWriteAddr(address) => pw_log::info!( + "SPIM{} BLOCKED WRITE log[{}]: address=0x{:08x}, raw=0x{:08x}", + id as u32, + index as u32, + address as u32, + word as u32 + ), + ViolationLogEntry::BlockedReadAddr(address) => pw_log::info!( + "SPIM{} BLOCKED READ log[{}]: address=0x{:08x}, raw=0x{:08x}", + id as u32, + index as u32, + address as u32, + word as u32 + ), + ViolationLogEntry::Invalid(raw) => pw_log::info!( + "SPIM{} INVALID LOG log[{}]: raw=0x{:08x}", + id as u32, + index as u32, + raw as u32 + ), + } +} + +fn log_violation_status(id: u32, status: u32) { + pw_log::info!( + "SPIM{} status=0x{:08x}: command_blocked={}, write_blocked={}, read_blocked={}", + id as u32, + status as u32, + (status & (1 << 0) != 0) as u32, + (status & (1 << 1) != 0) as u32, + (status & (1 << 2) != 0) as u32 + ); +} + +fn acknowledge_violations(regs: &SpiMonitorRegisters) -> u32 { + let pending = regs.read_ctrl2() & 0x7; + if pending != 0 { + regs.modify_ctrl2(|bits| *bits |= pending); + } + pending +} + +macro_rules! runtime_debug { + ($($arg:tt)*) => { + if ENABLE_RUNTIME_DEBUG_LOGS { + pw_log::info!($($arg)*); + } + }; +} + +fn recover_bmc_path( + scu: &ScuRegisters, + spim1: &ConfiguredSpiMonitor, + spim2: &ConfiguredSpiMonitor, +) { + runtime_debug!("BMC recovery: asserting BMC reset"); + if !set_bmc_resets(true) { + runtime_debug!("BMC recovery: reset assertion readback failed"); + } + + runtime_debug!("BMC recovery: routing SPIM1/2 flashes to RoT"); + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux1); + delay_us(1_000); + + loop { + if !set_bmc_resets(true) { + runtime_debug!("BMC recovery: reset assertion readback failed"); + } + if spim_external_mux_state(Spim1Config::INSTANCE) != Some(ScuExtMuxSelect::Mux1) { + runtime_debug!( + "BMC recovery: RoT mux readback failed; retrying in {} us", + BMC_RECOVERY_RETRY_DELAY_US as u32 + ); + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux1); + delay_us(BMC_RECOVERY_RETRY_DELAY_US); + continue; + } + spim1.reset_filter_state(); + spim2.reset_filter_state(); + + runtime_debug!("BMC recovery: resetting and probing spi1@0 and spi1@1"); + if reset_bmc_flashes(scu, false).is_err() { + runtime_debug!( + "BMC recovery: flash reset/probe failed; retrying in {} us", + BMC_RECOVERY_RETRY_DELAY_US as u32 + ); + delay_us(BMC_RECOVERY_RETRY_DELAY_US); + continue; + } + + spim1.reset_filter_state(); + spim2.reset_filter_state(); + acknowledge_violations(spim1.regs()); + acknowledge_violations(spim2.regs()); + + runtime_debug!("BMC recovery: handing SPIM1/2 flashes back to BMC"); + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux0); + delay_us(60_000); + if spim_external_mux_state(Spim1Config::INSTANCE) != Some(ScuExtMuxSelect::Mux0) { + runtime_debug!( + "BMC recovery: BMC mux readback failed; retrying in {} us", + BMC_RECOVERY_RETRY_DELAY_US as u32 + ); + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux1); + delay_us(BMC_RECOVERY_RETRY_DELAY_US); + continue; + } + + if !set_bmc_resets(false) { + runtime_debug!( + "BMC recovery: reset release readback failed; retrying in {} us", + BMC_RECOVERY_RETRY_DELAY_US as u32 + ); + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux1); + delay_us(BMC_RECOVERY_RETRY_DELAY_US); + continue; + } + + runtime_debug!("BMC recovery: complete"); + return; + } +} + +fn monitor_spim_violations( + spim1: &ConfiguredSpiMonitor, + spim2: &ConfiguredSpiMonitor, + spim3: &LockedSpiMonitor, + spim4: &LockedSpiMonitor, +) -> ! { + let monitors = [ + (1u32, spim1.regs(), &SPIM1_LOG), + (2u32, spim2.regs(), &SPIM2_LOG), + (3u32, spim3.regs(), &SPIM3_LOG), + (4u32, spim4.regs(), &SPIM4_LOG), + ]; + let mut log_indices = [ + spim1.regs().read_log_idx_reg(), + spim2.regs().read_log_idx_reg(), + spim3.regs().read_log_idx_reg(), + spim4.regs().read_log_idx_reg(), + ]; + let mut poll_count = 0u32; + let mut bmc_csin_low_us = 0u32; + + if ENABLE_RUNTIME_DEBUG_LOGS { + for (id, regs, _) in monitors { + pw_log::info!( + "SPIM{} ctrl/status/lock=0x{:08x}/0x{:08x}/0x{:08x}", + id as u32, + regs.read_ctrl() as u32, + regs.read_ctrl2() as u32, + regs.read_lock_status() as u32 + ); + } + pw_log::info!("Monitoring BMC and host traffic; SPIPF logs blocked transactions only"); + } + + loop { + for (slot, (id, regs, log)) in monitors.iter().enumerate() { + let next_index = regs.read_log_idx_reg(); + if next_index != log_indices[slot] { + let status = regs.read_ctrl2(); + pw_log::info!( + "SPIM{} NEW VIOLATION: log {} -> {}", + *id as u32, + log_indices[slot] as u32, + next_index as u32 + ); + log_violation_status(*id, status); + + let end = next_index.min(test_common::LOG_RAM_WORDS as u32); + for index in log_indices[slot]..end { + log_violation(*id, index, raw_log_word(log, index as usize)); + } + + let acknowledged = acknowledge_violations(regs); + pw_log::info!( + "SPIM{} acknowledged status 0x{:08x}", + *id as u32, + acknowledged as u32 + ); + log_indices[slot] = next_index; + } + } + + let csin_levels = bmc_spim_csin_levels(); + if csin_levels != BMC_CSIN_MASK { + bmc_csin_low_us = bmc_csin_low_us.saturating_add(MONITOR_POLL_INTERVAL_US); + if bmc_csin_low_us >= BMC_CSIN_RECOVERY_TIMEOUT_US { + runtime_debug!( + "BMC CSIN timeout: levels=0x{:08x}, low for {} us", + csin_levels as u32, + bmc_csin_low_us as u32 + ); + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + recover_bmc_path(&scu, spim1, spim2); + log_indices[0] = spim1.regs().read_log_idx_reg(); + log_indices[1] = spim2.regs().read_log_idx_reg(); + bmc_csin_low_us = 0; + } + } else { + bmc_csin_low_us = 0; + } + + poll_count = poll_count.wrapping_add(1); + if ENABLE_RUNTIME_DEBUG_LOGS && poll_count % 100 == 0 { + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + let mux = match spim_external_mux_state(Spim1Config::INSTANCE) { + Some(ScuExtMuxSelect::Mux0) => 0u32, + Some(ScuExtMuxSelect::Mux1) => 1u32, + None => 0xffff_ffff, + }; + let host_mux = match spim_external_mux_state(Spim3Config::INSTANCE) { + Some(ScuExtMuxSelect::Mux0) => 0u32, + Some(ScuExtMuxSelect::Mux1) => 1u32, + None => 0xffff_ffff, + }; + let path = bmc_spim_path_debug(); + pw_log::info!( + "SPIM health: SCU0F0=0x{:08x}, mux BMC/host={}/{}, CSIN=0x{:08x}", + scu.route_control_raw() as u32, + mux as u32, + host_mux as u32, + bmc_spim_csin_levels() as u32 + ); + pw_log::info!( + "SPIM log_idx 1/2/3/4={}/{}/{}/{}", + monitors[0].1.read_log_idx_reg() as u32, + monitors[1].1.read_log_idx_reg() as u32, + monitors[2].1.read_log_idx_reg() as u32, + monitors[3].1.read_log_idx_reg() as u32 + ); + pw_log::info!( + "SPIM status 1/2/3/4=0x{:08x}/0x{:08x}/0x{:08x}/0x{:08x}", + monitors[0].1.read_ctrl2() as u32, + monitors[1].1.read_ctrl2() as u32, + monitors[2].1.read_ctrl2() as u32, + monitors[3].1.read_ctrl2() as u32 + ); + pw_log::info!( + "BMC path: SCU410/4B0/690=0x{:08x}/0x{:08x}/0x{:08x}, SCU610=0x{:08x}", + path.scu410 as u32, + path.scu4b0 as u32, + path.scu690 as u32, + path.scu610 as u32 + ); + pw_log::info!( + "BMC path: GPIO000/004=0x{:08x}/0x{:08x}, SGPIOM570/554=0x{:08x}/0x{:08x}", + path.gpio_data as u32, + path.gpio_direction as u32, + path.sgpio_latch as u32, + path.sgpio_config as u32 + ); + } + delay_us(MONITOR_POLL_INTERVAL_US); + } +} + +fn reset_one_bmc_flash( + scu: &ScuRegisters, + spi: &SpiReady, + monitor: SpiMonitorInstance, + chip_select: ChipSelect, +) -> Result<[u8; 3], SmcError> { + scu.set_spim_internal_mux(SpiMonitorSource::Spi1, monitor as u8 + 1) + .map_err(|_| SmcError::HardwareError)?; + let proprietary_state = scu.spim_proprietary_pre_config(); + + let result = (|| { + spi.transceive_user(chip_select, &[0x66], &[], &mut [], TransferMode::Mode111)?; + delay_us(10_000); + spi.transceive_user(chip_select, &[0x99], &[], &mut [], TransferMode::Mode111)?; + delay_us(50_000); + spi.transceive_user(chip_select, &[0xe9], &[], &mut [], TransferMode::Mode111)?; + + let mut jedec = [0u8; 3]; + spi.transceive_user(chip_select, &[0x9f], &[], &mut jedec, TransferMode::Mode111)?; + Ok(jedec) + })(); + + if let Some(state) = proprietary_state { + scu.spim_proprietary_post_config(state); + } + scu.clear_spim_internal_master_route(); + result +} + +fn reset_bmc_flashes(scu: &ScuRegisters, log_jedec: bool) -> Result<(), SmcError> { + scu.apply_pinctrl_group(PINCTRL_SPI1_QUAD); + let config = SmcConfig { + controller_id: SmcController::Spi1, + cs0: Some(BMC_FLASH_CONFIG), + cs1: Some(BMC_FLASH_CONFIG), + dma_enabled: false, + enable_interrupts: false, + topology: SmcTopology::HostSpi { master_idx: 0 }, + }; + let spi = unsafe { SpiUninit::new(SmcController::Spi1, config)? }.init()?; + + for (index, monitor, chip_select) in [ + (0u32, SpiMonitorInstance::Spim0, ChipSelect::Cs0), + (1u32, SpiMonitorInstance::Spim1, ChipSelect::Cs1), + ] { + let jedec = reset_one_bmc_flash(scu, &spi, monitor, chip_select)?; + if log_jedec { + pw_log::info!( + "spi1@{} reset complete, JEDEC ID: {:02x} {:02x} {:02x}", + index as u32, + jedec[0] as u32, + jedec[1] as u32, + jedec[2] as u32 + ); + } + if matches!(jedec, [0x00, 0x00, 0x00] | [0xff, 0xff, 0xff]) { + return Err(SmcError::HardwareError); + } + } + Ok(()) +} + +fn production_policy() -> MonitorPolicy { + let mut policy = MonitorPolicy::empty(); + policy.allow_commands.copy_from_slice(&ALLOW_COMMANDS); + policy.allow_command_count = ALLOW_COMMANDS.len(); + let _ = policy.add_region( + 0, + WRITE_PROTECTED_LENGTH, + PrivilegeDirection::Write, + PrivilegeOp::Disable, + ); + policy +} + +fn setup_all_spim() -> Result<(), test_common::TestError> { + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + let policy = production_policy(); + //let spim4_policy = spim4_test_policy(); + + pw_log::info!("=== GPIO flash power ==="); + if !enable_flash_power(&scu) { + pw_log::info!("FAIL: GPIOL2/GPIOL3 flash power readback"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: GPIOL2 and GPIOL3 set high"); + + pw_log::info!("=== Release SPI flash reset outputs ==="); + if !release_spi_flash_resets() { + pw_log::info!("FAIL: SGPIOM CPU/BMC SPI reset outputs did not release"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: SGPIOM outputs 6 and 7 released high"); + + pw_log::info!("=== Hold BMC in reset ==="); + if !set_bmc_resets(true) { + pw_log::info!("FAIL: SGPIOM BMC reset outputs did not assert"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: SGPIOM outputs 8 and 9 asserted low"); + + pw_log::info!("=== SPIM1 ==="); + test_common::configure_wiring::(&scu)?; + let spim1 = test_common::initialize_monitor_with_policy::( + log_buffer(&SPIM1_LOG), + &policy, + )?; + test_common::dump_policy(&spim1, WRITE_PROTECTED_LENGTH)?; + test_common::validate_one_mib_write_protection(&spim1)?; + test_common::validate_filtering(&spim1)?; + + pw_log::info!("=== SPIM2 ==="); + test_common::configure_wiring::(&scu)?; + let spim2 = test_common::initialize_monitor_with_policy::( + log_buffer(&SPIM2_LOG), + &policy, + )?; + test_common::dump_policy(&spim2, WRITE_PROTECTED_LENGTH)?; + test_common::validate_one_mib_write_protection(&spim2)?; + test_common::validate_filtering(&spim2)?; + + pw_log::info!("=== SPIM3 ==="); + test_common::configure_wiring::(&scu)?; + let spim3 = test_common::initialize_monitor_with_policy::( + log_buffer(&SPIM3_LOG), + &policy, + )?; + test_common::dump_policy(&spim3, WRITE_PROTECTED_LENGTH)?; + test_common::validate_one_mib_write_protection(&spim3)?; + test_common::validate_filtering(&spim3)?; + let spim3 = test_common::lock_monitor(spim3)?; + + pw_log::info!("=== SPIM4 ==="); + test_common::configure_wiring::(&scu)?; + let spim4 = test_common::initialize_monitor_with_policy::( + log_buffer(&SPIM4_LOG), + &policy, + )?; + test_common::dump_policy(&spim4, WRITE_PROTECTED_LENGTH)?; + test_common::validate_one_mib_write_protection(&spim4)?; + test_common::validate_filtering(&spim4)?; + let spim4 = test_common::lock_monitor(spim4)?; + + if scu.route_control_raw() & 0x0fff_0000 != 0 { + pw_log::info!( + "FAIL: unexpected SCU flash-reset routing: 0x{:08x}", + scu.route_control_raw() as u32 + ); + return Err(test_common::TestError::Check); + } + pw_log::info!("All SPIM1-4 monitors are filtering traffic"); + pw_log::info!("SPIM3/4 policies are locked; SPIM1/2 match unlocked Zephyr runtime"); + pw_log::info!( + "SCU0F0 after SPIM setup: 0x{:08x}", + scu.route_control_raw() as u32 + ); + + pw_log::info!("=== Reset BMC flashes through internal SPI1 ==="); + if reset_bmc_flashes(&scu, true).is_err() { + pw_log::info!("FAIL: BMC flash reset/readback through SPI1"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: spi1@0 and spi1@1 are in a known SPI state"); + + pw_log::info!("=== Hand BMC flash mux to external BMC master ==="); + spim1.reset_filter_state(); + spim2.reset_filter_state(); + test_common::validate_filtering(&spim1)?; + test_common::validate_filtering(&spim2)?; + pw_log::info!( + "SPIM1/2 pre-release ctrl=0x{:08x}/0x{:08x}", + spim1.regs().read_ctrl() as u32, + spim2.regs().read_ctrl() as u32 + ); + + apply_spim_external_mux(Spim1Config::INSTANCE, ScuExtMuxSelect::Mux0); + if spim_external_mux_state(Spim1Config::INSTANCE) != Some(ScuExtMuxSelect::Mux0) { + pw_log::info!("FAIL: SPIM1/2 external BMC mux handoff"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: SPIM1/2 mux switched from RoT=1 to BMC=0"); + + pw_log::info!("Waiting 60 ms for flash routing to settle"); + delay_us(60_000); + + pw_log::info!("=== Release BMC reset ==="); + if !set_bmc_resets(false) { + pw_log::info!("FAIL: SGPIOM BMC reset outputs did not release"); + return Err(test_common::TestError::Check); + } + pw_log::info!("PASS: SGPIOM outputs 8 and 9 released high"); + + pw_log::info!("Firmware will remain active until the user stops or resets it"); + monitor_spim_violations(&spim1, &spim2, &spim3, &spim4) +} + +struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 Setup All External SPI Monitors"; + + fn main() -> ! { + if setup_all_spim().is_err() { + pw_log::info!("FAIL: setup all SPIM monitors"); + } + + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/spimonitor/spimonitor_test.bzl b/target/ast10x0/tests/spimonitor/spimonitor_test.bzl new file mode 100644 index 00000000..e229ed10 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/spimonitor_test.bzl @@ -0,0 +1,100 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +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") + +def spimonitor_test(index, target_compatible_with): + target_name = "target_spim{}".format(index) + image_name = "spimonitor{}_test".format(index) + + rust_binary( + name = target_name, + srcs = [ + "test_common.rs", + "test_spim{}.rs".format(index), + ], + crate_root = "test_spim{}.rs".format(index), + edition = "2024", + tags = ["kernel"], + target_compatible_with = target_compatible_with, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], + ) + + system_image( + name = image_name, + kernel = ":" + target_name, + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = target_compatible_with, + userspace = False, + visibility = ["//visibility:public"], + ) + + system_image_test( + name = "spimonitor{}_evb_test".format(index), + image = ":" + image_name, + tags = ["hardware"], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], + ) + + rust_binary_no_panics_test( + name = "spimonitor{}_no_panics_test".format(index), + binary = ":" + image_name, + tags = ["kernel"], + ) + +def spimonitor_setup_all(target_compatible_with): + rust_binary( + name = "setup_all_spim", + srcs = [ + "setup_all_spim.rs", + "test_common.rs", + ], + crate_root = "setup_all_spim.rs", + edition = "2024", + tags = ["kernel"], + target_compatible_with = target_compatible_with, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], + ) + + system_image( + name = "setup_all_spim_image", + kernel = ":setup_all_spim", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = target_compatible_with, + userspace = False, + visibility = ["//visibility:public"], + ) + + rust_binary_no_panics_test( + name = "setup_all_spim_no_panics_test", + binary = ":setup_all_spim_image", + tags = ["kernel"], + ) diff --git a/target/ast10x0/tests/spimonitor/system.json5 b/target/ast10x0/tests/spimonitor/system.json5 new file mode 100644 index 00000000..669c9c50 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/system.json5 @@ -0,0 +1,17 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 kernel-only SPI monitor smoke test configuration. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 262144, + ram_start_address: 0x00040500, + ram_size_bytes: 393216, + }, +} diff --git a/target/ast10x0/tests/spimonitor/test_common.rs b/target/ast10x0/tests/spimonitor/test_common.rs new file mode 100644 index 00000000..4b57a1cc --- /dev/null +++ b/target/ast10x0/tests/spimonitor/test_common.rs @@ -0,0 +1,327 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared AST10x0 external SPI monitor configuration tests. + +#![allow(dead_code)] + +use core::cell::UnsafeCell; + +use ast10x0_board::{apply_spim_external_mux, apply_spim_pinctrl, spim_external_mux_state}; +use ast10x0_peripherals::scu::{ + ScuError, ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, + SpiMonitorSource, +}; +use ast10x0_peripherals::spimonitor::{ + ConfiguredSpiMonitor, LockState, MonitorPolicy, MonitorState, PassthroughMode, + PrivilegeDirection, PrivilegeOp, SpiMonitor, SpiMonitorController, SpiMonitorError, + Uninitialized, +}; + +pub const PROTECTED_LENGTH: u32 = 0x0010_0000; +const LOG_RAM_BYTES: usize = 0x200; +pub const LOG_RAM_WORDS: usize = LOG_RAM_BYTES / core::mem::size_of::(); +const COMMAND_VALID_MASK: u32 = (1 << 30) | (1 << 31); +const COMMAND_LOCKED: u32 = 1 << 23; +const LOCK_REQUIRED: u32 = (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 30) | (1 << 31); + +pub trait TestConfig { + const INSTANCE: SpiMonitorInstance; + const CONTROLLER: SpiMonitorController; +} + +#[repr(align(16))] +struct LogRam(UnsafeCell<[u32; LOG_RAM_WORDS]>); + +// SAFETY: Each test image is single-threaded and owns one SPIPF log buffer. +unsafe impl Sync for LogRam {} + +static LOG_RAM: LogRam = LogRam(UnsafeCell::new([0; LOG_RAM_WORDS])); + +#[derive(Clone, Copy)] +pub enum TestError { + Monitor, + Scu, + Check, +} + +impl From for TestError { + fn from(_: SpiMonitorError) -> Self { + Self::Monitor + } +} + +impl From for TestError { + fn from(_: ScuError) -> Self { + Self::Scu + } +} + +macro_rules! test_check { + ($condition:expr, $message:literal) => { + if !$condition { + pw_log::info!($message); + return Err(TestError::Check); + } + }; +} + +fn log_buffer() -> &'static mut [u32] { + // SAFETY: The test is single-threaded and calls this exactly once. + unsafe { &mut *LOG_RAM.0.get() } +} + +pub fn configure_wiring(scu: &ScuRegisters) -> Result<(), TestError> { + pw_log::info!("START: external monitor pinctrl, routing, and mux"); + apply_spim_pinctrl(scu, C::INSTANCE); + scu.disable_spim_cs_internal_pull_down(C::INSTANCE); + + // SPIPF observes external BMC/host traffic. Do not detour SPI1 or SPI2. + scu.set_spim_internal_mux(SpiMonitorSource::Spi1, 0)?; + test_check!( + scu.route_control_raw() & 0x0f == 0, + "FAIL: internal SPI master detour is enabled" + ); + + // SCU passthrough enables the external signal path through SPIPF. + scu.set_spim_passthrough(C::INSTANCE, SpiMonitorPassthrough::Enabled); + scu.set_spim_miso_multi_func(C::INSTANCE, true); + scu.set_spim_filter(C::INSTANCE, true); + + // This board provides ext-mux-sel-gpios. Match the Zephyr driver by + // driving those GPIOs only; SCU0F0[15:12] is used only when no external + // mux GPIOs are described. + apply_spim_external_mux(C::INSTANCE, ScuExtMuxSelect::Mux1); + test_check!( + spim_external_mux_state(C::INSTANCE) == Some(ScuExtMuxSelect::Mux1), + "FAIL: external mux 1 GPIO readback" + ); + + test_check!( + scu.route_control_raw() & 0x0f == 0, + "FAIL: external monitor setup changed internal SPI routing" + ); + pw_log::info!("PASS: external monitor pinctrl, routing, and mux"); + Ok(()) +} + +pub fn build_policy() -> MonitorPolicy { + let mut policy = MonitorPolicy::empty(); + policy.allow_commands[..9] + .copy_from_slice(&[0x9f, 0x05, 0x06, 0x04, 0x02, 0x12, 0x20, 0x21, 0x0c]); + policy.allow_command_count = 9; + let _ = policy.add_region( + 0, + PROTECTED_LENGTH, + PrivilegeDirection::Write, + PrivilegeOp::Disable, + ); + policy +} + +pub fn initialize_monitor( + buffer: &'static mut [u32], +) -> Result { + initialize_monitor_with_policy::(buffer, &build_policy()) +} + +pub fn initialize_monitor_with_policy( + buffer: &'static mut [u32], + policy: &MonitorPolicy, +) -> Result { + pw_log::info!("START: monitor reset, policy, and log RAM"); + let monitor = unsafe { SpiMonitor::::new(C::CONTROLLER) }; + monitor.software_reset(); + test_check!( + monitor.regs().read_ctrl() & (1 << 15) == 0, + "FAIL: software reset did not deassert" + ); + + let configured = monitor.apply_policy(policy)?; + test_check!( + configured.state() == MonitorState::Configured, + "FAIL: monitor did not enter configured state" + ); + + configured.configure_log(buffer)?; + test_check!( + configured.regs().read_log_capacity_entries() as usize == LOG_RAM_WORDS, + "FAIL: 0x200-byte log RAM configuration" + ); + test_check!( + configured.regs().read_log_idx_reg() == 0, + "FAIL: violation log pointer was not reset" + ); + + configured.set_push_pull(true); + configured.set_passthrough(PassthroughMode::Disabled); + configured.enable(); + test_check!( + configured.regs().read_ctrl() & (1 << 2) != 0, + "FAIL: monitor filter is not enabled" + ); + pw_log::info!("PASS: monitor reset, policy, and log RAM"); + Ok(configured) +} + +#[allow(dead_code)] +pub fn dump_policy( + configured: &ConfiguredSpiMonitor, + write_protected_length: u32, +) -> Result<(), TestError> { + pw_log::info!("allow-command table:"); + for slot in 0..32 { + let value = configured.regs().read_allow_cmd_slot(slot); + if value != 0 { + pw_log::info!( + " slot {:02}: cmd=0x{:02x}, raw=0x{:08x}", + slot as u32, + (value & 0xff) as u32, + value as u32 + ); + } + } + + pw_log::info!( + "write-protected region: start=0x00000000, length=0x{:08x}", + write_protected_length as u32 + ); + Ok(()) +} + +#[allow(dead_code)] +pub fn validate_filtering(configured: &ConfiguredSpiMonitor) -> Result<(), TestError> { + test_check!( + configured.regs().read_ctrl() & 0x7 == 0x4, + "FAIL: SPIPF filtering mode readback" + ); + pw_log::info!("PASS: SPIPF filtering mode"); + Ok(()) +} + +#[allow(dead_code)] +pub fn validate_one_mib_write_protection( + configured: &ConfiguredSpiMonitor, +) -> Result<(), TestError> { + test_check!( + configured.privilege_word(PrivilegeDirection::Read, 0)? == u32::MAX + && configured.privilege_word(PrivilegeDirection::Read, 1)? == u32::MAX + && configured.privilege_word(PrivilegeDirection::Read, 2)? == u32::MAX, + "FAIL: read privilege table is not unrestricted" + ); + test_check!( + configured.privilege_word(PrivilegeDirection::Write, 0)? == 0 + && configured.privilege_word(PrivilegeDirection::Write, 1)? == 0, + "FAIL: first 1 MiB is not write protected" + ); + test_check!( + configured.privilege_word(PrivilegeDirection::Write, 2)? == u32::MAX, + "FAIL: writes at and above 0x00100000 are not enabled" + ); + pw_log::info!("PASS: reads allowed; writes below 0x00100000 blocked"); + pw_log::info!("PASS: writes at 0x00110000 allowed"); + Ok(()) +} + +#[allow(dead_code)] +pub fn lock_monitor( + configured: ConfiguredSpiMonitor, +) -> Result { + Ok(configured.lock()?) +} + +fn test_passthrough_control(configured: &ConfiguredSpiMonitor) -> Result<(), TestError> { + pw_log::info!("START: passthrough control readback"); + configured.set_passthrough(PassthroughMode::Enabled); + test_check!( + configured.regs().read_ctrl() & 0x3 == 0x1, + "FAIL: single-bit passthrough readback" + ); + configured.set_passthrough(PassthroughMode::MultiEnabled); + test_check!( + configured.regs().read_ctrl() & 0x3 == 0x2, + "FAIL: multi-bit passthrough readback" + ); + configured.set_passthrough(PassthroughMode::Disabled); + test_check!( + configured.regs().read_ctrl() & 0x3 == 0, + "FAIL: passthrough disable readback" + ); + pw_log::info!("PASS: passthrough control readback"); + Ok(()) +} + +fn test_command_policy(configured: &ConfiguredSpiMonitor) -> Result<(), TestError> { + pw_log::info!("START: command table add and remove"); + configured.remove_command(0x05)?; + let status_slot = configured.add_command(0x05, false)?; + let status_value = configured + .regs() + .read_allow_cmd_slot_checked(status_slot) + .ok_or(TestError::Check)?; + test_check!( + status_value & COMMAND_VALID_MASK != 0, + "FAIL: command add readback" + ); + configured.remove_command(0x05)?; + let status_value = configured + .regs() + .read_allow_cmd_slot_checked(status_slot) + .ok_or(TestError::Check)?; + test_check!(status_value == 0, "FAIL: command remove did not clear slot"); + configured.add_command(0x05, false)?; + pw_log::info!("PASS: command table add and remove"); + Ok(()) +} + +fn test_address_policy(configured: &ConfiguredSpiMonitor) -> Result<(), TestError> { + pw_log::info!("START: protected and unprotected address policy"); + test_check!( + configured.privilege_word(PrivilegeDirection::Write, 0)? == 0, + "FAIL: protected write region programming" + ); + test_check!( + configured.privilege_word(PrivilegeDirection::Write, 2)? == u32::MAX, + "FAIL: unprotected write region programming" + ); + pw_log::info!("PASS: protected and unprotected address policy"); + Ok(()) +} + +fn test_policy_locking(configured: ConfiguredSpiMonitor) -> Result<(), TestError> { + pw_log::info!("START: policy locking and readback"); + configured.lock_command(0x9f)?; + let locked = configured.lock()?; + test_check!( + locked.lock_state() == LockState::Locked, + "FAIL: monitor lock state" + ); + test_check!( + locked.regs().read_lock_status() & LOCK_REQUIRED == LOCK_REQUIRED, + "FAIL: lock register readback" + ); + let slot = locked.regs().read_allow_cmd_slot(2); + locked.regs().write_allow_cmd_slot(2, 0); + test_check!( + locked.regs().read_allow_cmd_slot(2) == slot && slot & COMMAND_LOCKED != 0, + "FAIL: command lock did not prevent modification" + ); + pw_log::info!("PASS: policy locking and readback"); + Ok(()) +} + +pub fn run() -> Result<(), TestError> { + pw_log::info!("=== AST10x0 external SPI monitor configuration test ==="); + + let scu = unsafe { ScuRegisters::new_global_unlocked() }; + configure_wiring::(&scu)?; + let configured = initialize_monitor::(log_buffer())?; + test_passthrough_control(&configured)?; + test_command_policy(&configured)?; + test_address_policy(&configured)?; + test_policy_locking(configured)?; + + pw_log::info!("External traffic blocking requires a BMC/host stimulus"); + pw_log::info!("=== all SPI monitor configuration tests passed ==="); + Ok(()) +} diff --git a/target/ast10x0/tests/spimonitor/test_spim1.rs b/target/ast10x0/tests/spimonitor/test_spim1.rs new file mode 100644 index 00000000..0aa6a20b --- /dev/null +++ b/target/ast10x0/tests/spimonitor/test_spim1.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +#[path = "test_common.rs"] +mod test_common; + +use ast10x0_peripherals::scu::SpiMonitorInstance; +use ast10x0_peripherals::spimonitor::SpiMonitorController; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +struct Spim1Config; + +impl test_common::TestConfig for Spim1Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim0; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim0; +} + +struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 External SPIM1 Configuration Test"; + + fn main() -> ! { + let sentinel = if test_common::run::().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/spimonitor/test_spim2.rs b/target/ast10x0/tests/spimonitor/test_spim2.rs new file mode 100644 index 00000000..cae247c6 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/test_spim2.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +#[path = "test_common.rs"] +mod test_common; + +use ast10x0_peripherals::scu::SpiMonitorInstance; +use ast10x0_peripherals::spimonitor::SpiMonitorController; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +struct Spim2Config; + +impl test_common::TestConfig for Spim2Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim1; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim1; +} + +struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 External SPIM2 Configuration Test"; + + fn main() -> ! { + let sentinel = if test_common::run::().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/spimonitor/test_spim3.rs b/target/ast10x0/tests/spimonitor/test_spim3.rs new file mode 100644 index 00000000..e6ce1597 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/test_spim3.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +#[path = "test_common.rs"] +mod test_common; + +use ast10x0_peripherals::scu::SpiMonitorInstance; +use ast10x0_peripherals::spimonitor::SpiMonitorController; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +struct Spim3Config; + +impl test_common::TestConfig for Spim3Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim2; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim2; +} + +struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 External SPIM3 Configuration Test"; + + fn main() -> ! { + let sentinel = if test_common::run::().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/target/ast10x0/tests/spimonitor/test_spim4.rs b/target/ast10x0/tests/spimonitor/test_spim4.rs new file mode 100644 index 00000000..3d437157 --- /dev/null +++ b/target/ast10x0/tests/spimonitor/test_spim4.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +#[path = "test_common.rs"] +mod test_common; + +use ast10x0_peripherals::scu::SpiMonitorInstance; +use ast10x0_peripherals::spimonitor::SpiMonitorController; +use console_backend::console_backend_write_all; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +struct Spim4Config; + +impl test_common::TestConfig for Spim4Config { + const INSTANCE: SpiMonitorInstance = SpiMonitorInstance::Spim3; + const CONTROLLER: SpiMonitorController = SpiMonitorController::Spim3; +} + +struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 External SPIM4 Configuration Test"; + + fn main() -> ! { + let sentinel = if test_common::run::().is_ok() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);