| // 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::policy::{MonitorPolicy, MAX_REGION_SLOTS}; |
| use crate::spimonitor::registers::{SpiMonitorController, SpiMonitorRegisters}; |
| use crate::spimonitor::types::{ |
| ExtMuxSel, LockState, MonitorState, PassthroughMode, PrivilegeDirection, PrivilegeOp, Result, |
| SpiMonitorError, 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>; |
| |
| // --------------------------------------------------------------------------- |
| // 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 |
| // --------------------------------------------------------------------------- |
| |
| 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: &MonitorPolicy) -> 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); |
| } |
| |
| // Program command allow-list table. |
| for i in 0..policy.allow_command_count { |
| let cmd = policy.allow_commands[i] as u32; |
| self.regs.write_allow_cmd_slot(i, cmd); |
| } |
| |
| // Program address filter table. |
| for i in 0..policy.region_count { |
| if let Some(region) = policy.regions[i] { |
| let word = encode_addr_filter_slot( |
| region.start, |
| region.length, |
| region.direction, |
| region.op, |
| ); |
| self.regs.write_addr_filter_slot(i, word); |
| } |
| } |
| |
| Ok(SpiMonitor { |
| regs: self.regs, |
| controller: self.controller, |
| scu: self.scu, |
| _mode: PhantomData, |
| }) |
| } |
| |
| #[must_use] |
| pub const fn state(&self) -> MonitorState { |
| MonitorState::Uninitialized |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Configured state |
| // --------------------------------------------------------------------------- |
| |
| impl SpiMonitor<Configured> { |
| /// 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 |= CTRL_PASSTHROUGH_BIT, |
| PassthroughMode::Disabled => *bits &= !CTRL_PASSTHROUGH_BIT, |
| }); |
| } |
| |
| /// 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) |
| } |
| |
| /// 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>> { |
| // 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); |
| |
| Ok(SpiMonitor { |
| regs: self.regs, |
| controller: self.controller, |
| scu: self.scu, |
| _mode: PhantomData, |
| }) |
| } |
| |
| #[must_use] |
| pub const fn state(&self) -> MonitorState { |
| MonitorState::Configured |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Locked state |
| // --------------------------------------------------------------------------- |
| |
| impl SpiMonitor<Locked> { |
| /// 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). |
| /// 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) -> MonitorState { |
| MonitorState::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_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 |
| #[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() |
| |
| /// 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_max_sz() as usize / core::mem::size_of::<u32>(); |
| 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] |
| } |