| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! SPI monitor controller facade. |
| |
| 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::{SpiMonitorPolicy, MAX_REGION_SLOTS}; |
| use crate::spimonitor::registers::{SpiMonitorController, SpiMonitorRegisters}; |
| use crate::spimonitor::types::{ |
| ExtMuxSel, LockState, PassthroughMode, PrivilegeDirection, PrivilegeOp, Result, |
| SpiMonitorError, SpiMonitorState, ViolationLogEntry, |
| }; |
| |
| /// Typestate: monitor is created but policy is not yet applied. |
| pub struct Uninitialized; |
| /// Typestate: policy tables are programmed and can still be changed. |
| pub struct Configured; |
| /// Typestate: policy is locked and runtime-mutating APIs are unavailable. |
| pub struct Locked; |
| |
| /// Generic SPI monitor instance with typestate-enforced lifecycle. |
| pub struct SpiMonitor<Mode> { |
| regs: SpiMonitorRegisters, |
| controller: SpiMonitorController, |
| scu: ScuRegisters, |
| _mode: PhantomData<fn() -> Mode>, |
| } |
| |
| /// Ergonomic alias for an uninitialized SPI monitor handle. |
| pub type UninitSpiMonitor = SpiMonitor<Uninitialized>; |
| /// Ergonomic alias for a configured-but-unlocked SPI monitor handle. |
| pub type ConfiguredSpiMonitor = SpiMonitor<Configured>; |
| /// Ergonomic alias for a locked SPI monitor handle. |
| pub type LockedSpiMonitor = SpiMonitor<Locked>; |
| |
| // --------------------------------------------------------------------------- |
| // Uninitialized state |
| // --------------------------------------------------------------------------- |
| |
| impl SpiMonitor<Uninitialized> { |
| /// Construct a new controller facade for a specific monitor instance. |
| /// |
| /// # Safety |
| /// Caller must guarantee exclusive ownership of the target SPIPF block and SCU. |
| pub const unsafe fn new(controller: SpiMonitorController) -> Self { |
| Self { |
| regs: unsafe { SpiMonitorRegisters::new_for_controller(controller) }, |
| controller, |
| scu: unsafe { ScuRegisters::new_global() }, |
| _mode: PhantomData, |
| } |
| } |
| |
| /// Program command-table and address-filter policy, then transition to |
| /// `Configured`. |
| /// |
| /// Returns `Err(InvalidSlot)` if `allow_command_count` exceeds the command |
| /// table length. Returns `Err(InvalidRegion)` if `region_count` exceeds |
| /// `MAX_REGION_SLOTS`. |
| pub fn apply_policy(self, policy: &SpiMonitorPolicy) -> Result<SpiMonitor<Configured>> { |
| if policy.allow_command_count > policy.allow_commands.len() { |
| return Err(SpiMonitorError::InvalidSlot); |
| } |
| 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); |
| } |
| } |
| |
| 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 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); |
| } |
| } |
| |
| // 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] { |
| configure_privilege_region( |
| &self.regs, |
| region.start, |
| region.length, |
| region.direction, |
| region.op, |
| )?; |
| } |
| } |
| |
| Ok(SpiMonitor { |
| regs: self.regs, |
| controller: self.controller, |
| scu: self.scu, |
| _mode: PhantomData, |
| }) |
| } |
| |
| #[must_use] |
| pub const fn state(&self) -> SpiMonitorState { |
| SpiMonitorState::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); |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Configured state |
| // --------------------------------------------------------------------------- |
| |
| impl SpiMonitor<Configured> { |
| /// 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<usize> { |
| 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<usize> { |
| 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<usize> { |
| 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<u32> { |
| 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)`. |
| pub fn enable(&self) { |
| self.regs |
| .modify_ctrl(|bits| *bits |= CTRL_MONITOR_ENABLE_BIT); |
| } |
| |
| /// Disable the monitor filter (SPIPF000 bit 0). |
| /// |
| /// Mirrors Zephyr's `spim_monitor_enable(dev, false)`. |
| pub fn disable(&self) { |
| self.regs |
| .modify_ctrl(|bits| *bits &= !CTRL_MONITOR_ENABLE_BIT); |
| } |
| |
| /// Configure passthrough mode (SPIPF000 passthrough bit). |
| /// |
| /// When `PassthroughMode::Enabled`, SPI traffic bypasses the filter. |
| /// Mirrors Zephyr's `spim_passthrough_config`. |
| pub fn set_passthrough(&self, mode: PassthroughMode) { |
| self.regs.modify_ctrl(|bits| match mode { |
| 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, |
| }); |
| } |
| |
| /// Select the external SPI mux routing. |
| /// |
| /// Mirrors Zephyr's `spim_ext_mux_config`. Platform code maps `Sel0`/`Sel1` |
| /// to ROT vs BMC/PCH roles. |
| /// |
| /// Correctly uses SCU0F0 register (ext_mux_select_sig_of_spipfN bits) |
| /// for each SPIPF instance, following the aspeed-rust pattern. |
| pub fn set_ext_mux(&self, sel: ExtMuxSel) { |
| use crate::scu::types::{ScuExtMuxSelect, SpiMonitorInstance}; |
| |
| let mux_sel = match sel { |
| ExtMuxSel::Sel0 => ScuExtMuxSelect::Mux0, |
| ExtMuxSel::Sel1 => ScuExtMuxSelect::Mux1, |
| }; |
| |
| let instance = match self.controller { |
| SpiMonitorController::Spim0 => SpiMonitorInstance::Spim0, |
| SpiMonitorController::Spim1 => SpiMonitorInstance::Spim1, |
| SpiMonitorController::Spim2 => SpiMonitorInstance::Spim2, |
| SpiMonitorController::Spim3 => SpiMonitorInstance::Spim3, |
| }; |
| |
| self.scu.set_spim_ext_mux(instance, mux_sel); |
| } |
| |
| /// Query the current external SPI mux selection. |
| #[must_use] |
| pub fn get_ext_mux(&self) -> ExtMuxSel { |
| let instance = match self.controller { |
| SpiMonitorController::Spim0 => SpiMonitorInstance::Spim0, |
| SpiMonitorController::Spim1 => SpiMonitorInstance::Spim1, |
| SpiMonitorController::Spim2 => SpiMonitorInstance::Spim2, |
| SpiMonitorController::Spim3 => SpiMonitorInstance::Spim3, |
| }; |
| match self.scu.get_spim_ext_mux(instance) { |
| ScuExtMuxSelect::Mux0 => ExtMuxSel::Sel0, |
| ScuExtMuxSelect::Mux1 => ExtMuxSel::Sel1, |
| } |
| } |
| |
| /// Drain violation log entries into `buf`. Returns the filled slice. |
| /// |
| /// Available in `Configured` state for diagnostic use during bring-up. |
| pub fn drain_log<'a>(&self, buf: &'a mut [ViolationLogEntry]) -> &'a [ViolationLogEntry] { |
| 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. |
| /// See aspeed-rust::spim_lock_common() for complete lock sequence: |
| /// - Write-disable SPIPFWA/SPIPFRA (address filter tables) |
| /// - Lock all command table entries |
| /// - Write-disable SPIPF000, SPIPF004, SPIPF010, SPIPF014 |
| pub fn lock(self) -> Result<SpiMonitor<Locked>> { |
| 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, |
| controller: self.controller, |
| scu: self.scu, |
| _mode: PhantomData, |
| }) |
| } |
| |
| #[must_use] |
| pub const fn state(&self) -> SpiMonitorState { |
| SpiMonitorState::Configured |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Locked state |
| // --------------------------------------------------------------------------- |
| |
| impl SpiMonitor<Locked> { |
| /// Select the external SPI mux routing in locked state. |
| /// |
| /// Available post-lock for mux ownership transitions at runtime (e.g., BMC boot-hold/release). |
| /// Uses SCU0F0 register following the aspeed-rust pattern. |
| pub fn set_ext_mux(&self, sel: ExtMuxSel) { |
| let mux = match sel { |
| ExtMuxSel::Sel0 => ScuExtMuxSelect::Mux0, |
| ExtMuxSel::Sel1 => ScuExtMuxSelect::Mux1, |
| }; |
| let instance = match self.controller { |
| SpiMonitorController::Spim0 => SpiMonitorInstance::Spim0, |
| SpiMonitorController::Spim1 => SpiMonitorInstance::Spim1, |
| SpiMonitorController::Spim2 => SpiMonitorInstance::Spim2, |
| SpiMonitorController::Spim3 => SpiMonitorInstance::Spim3, |
| }; |
| self.scu.set_spim_ext_mux(instance, mux); |
| } |
| |
| /// Query the current external SPI mux selection in locked state. |
| #[must_use] |
| pub fn get_ext_mux(&self) -> ExtMuxSel { |
| let instance = match self.controller { |
| SpiMonitorController::Spim0 => SpiMonitorInstance::Spim0, |
| SpiMonitorController::Spim1 => SpiMonitorInstance::Spim1, |
| SpiMonitorController::Spim2 => SpiMonitorInstance::Spim2, |
| SpiMonitorController::Spim3 => SpiMonitorInstance::Spim3, |
| }; |
| match self.scu.get_spim_ext_mux(instance) { |
| ScuExtMuxSelect::Mux0 => ExtMuxSel::Sel0, |
| ScuExtMuxSelect::Mux1 => ExtMuxSel::Sel1, |
| } |
| } |
| |
| /// Drain violation log entries into `buf`. Returns the filled slice. |
| /// |
| /// Caller is responsible for synchronization and log-pointer reset. |
| pub fn drain_log<'a>(&self, buf: &'a mut [ViolationLogEntry]) -> &'a [ViolationLogEntry] { |
| drain_log_impl(&self.regs, buf) |
| } |
| |
| #[must_use] |
| pub const fn lock_state(&self) -> LockState { |
| LockState::Locked |
| } |
| |
| #[must_use] |
| pub const fn state(&self) -> SpiMonitorState { |
| SpiMonitorState::Locked |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // State-independent accessors |
| // --------------------------------------------------------------------------- |
| |
| impl<Mode> SpiMonitor<Mode> { |
| #[must_use] |
| pub fn regs(&self) -> &SpiMonitorRegisters { |
| &self.regs |
| } |
| |
| #[must_use] |
| pub const fn controller(&self) -> SpiMonitorController { |
| self.controller |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Internal helpers |
| // --------------------------------------------------------------------------- |
| |
| /// SPIPF000 bit positions. |
| /// |
| /// Confirmed from aspeed-rust implementation (src/spimonitor/hardware.rs). |
| /// Register field names from ast1060_pac provide safe typed accessors. |
| 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_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<usize> { |
| 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>( |
| regs: &SpiMonitorRegisters, |
| buf: &'a mut [ViolationLogEntry], |
| ) -> &'a [ViolationLogEntry] { |
| let log_base = regs.log_ram_base_addr(); |
| 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); |
| let count = available.min(buf.len()); |
| |
| for i in 0..count { |
| // SAFETY: log_base is a hardware RAM address validated by the PAC |
| // base-address mapping. Offset stays within [0, max_entries) words. |
| let word = unsafe { core::ptr::read_volatile((log_base as *const u32).add(i)) }; |
| buf[i] = ViolationLogEntry::parse(word); |
| } |
| |
| &buf[..count] |
| } |