add spi1 and spi2 read test. add spim wiring and monitor
diff --git a/target/ast10x0/board/BUILD.bazel b/target/ast10x0/board/BUILD.bazel
index 15a420c..4a4734e 100644
--- a/target/ast10x0/board/BUILD.bazel
+++ b/target/ast10x0/board/BUILD.bazel
@@ -8,6 +8,8 @@
     name = "ast10x0_board",
     srcs = [
         "src/lib.rs",
+        "src/monitor.rs",
+        "src/spim_wiring.rs",
     ],
     crate_name = "ast10x0_board",
     edition = "2024",
diff --git a/target/ast10x0/board/src/lib.rs b/target/ast10x0/board/src/lib.rs
index eb4f4db..46acb80 100644
--- a/target/ast10x0/board/src/lib.rs
+++ b/target/ast10x0/board/src/lib.rs
@@ -14,6 +14,12 @@
 use ast10x0_peripherals::scu::{ClockRegisterHalf, ScuRegisterHalf};
 use ast10x0_peripherals::scu::{PinctrlPin, ScuRegisters};
 
+pub mod monitor;
+pub mod spim_wiring;
+
+pub use monitor::Ast1060Monitor;
+pub use spim_wiring::{apply_spim_wiring, presets, SpimWiring, SpimWiringError};
+
 /// Board descriptor metadata for AST10x0 board initialization.
 #[derive(Clone, Debug)]
 pub struct Ast10x0BoardDescriptor {
diff --git a/target/ast10x0/board/src/monitor.rs b/target/ast10x0/board/src/monitor.rs
new file mode 100644
index 0000000..ae48623
--- /dev/null
+++ b/target/ast10x0/board/src/monitor.rs
@@ -0,0 +1,279 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! Board-level SPI Monitor orchestration.
+//!
+//! `Ast1060Monitor` is the concrete implementation of the `Monitor` trait, orchestrating
+//! both SCU (for external mux control) and SPIPF (for enforcement, filtering, and locks).
+//!
+//! This layer ensures no duplication: mux control delegates to SCU routing, and SPIPF
+//! operations delegate to SPI monitor registers.
+
+use ast10x0_peripherals::scu::registers::ScuRegisters;
+use ast10x0_peripherals::scu::types::{ScuExtMuxSelect, SpiMonitorInstance};
+use ast10x0_peripherals::spimonitor::registers::SpiMonitorRegisters;
+use ast10x0_peripherals::spimonitor::traits::Monitor;
+use ast10x0_peripherals::spimonitor::types::{
+    BootError, BootResult, MonitorInstance, MonitorStatus, MuxSelect, PrivilegeDirection,
+    PrivilegeOp,
+};
+
+/// Board-level Monitor orchestrator.
+///
+/// Holds mutable references to both SCU and SPIPF register blocks, allowing it to
+/// orchestrate mux operations (via SCU) and enforcement/filtering (via SPIPF) from
+/// a single unified interface.
+///
+/// # Example
+///
+/// ```ignore
+/// let mut board = Ast1060Board::init();
+/// let mut monitor = board.monitor();
+/// monitor.set_mux(MonitorInstance::Spim0, MuxSelect::RotControl)?;
+/// monitor.set_address_privilege(/*...*/)?;
+/// monitor.lock_policy(MonitorInstance::Spim0)?;
+/// ```
+pub struct Ast1060Monitor<'a> {
+    scu: &'a mut ScuRegisters,
+    spipf: &'a mut [SpiMonitorRegisters; 4],
+    read_blocked_region_count: u8,
+    #[allow(dead_code)]
+    write_blocked_region_count: u8,
+}
+
+impl<'a> Ast1060Monitor<'a> {
+    /// Create a new board-level Monitor with access to both SCU and SPIPF.
+    pub fn new(
+        scu: &'a mut ScuRegisters,
+        spipf: &'a mut [SpiMonitorRegisters; 4],
+        read_blocked_region_count: u8,
+        write_blocked_region_count: u8,
+    ) -> Self {
+        Self {
+            scu,
+            spipf,
+            read_blocked_region_count,
+            write_blocked_region_count,
+        }
+    }
+
+    /// Get the register accessor for the specified monitor instance.
+    #[inline]
+    fn regs(&self, instance: MonitorInstance) -> &SpiMonitorRegisters {
+        match instance {
+            MonitorInstance::Spim0 => &self.spipf[0],
+            MonitorInstance::Spim1 => &self.spipf[1],
+            MonitorInstance::Spim2 => &self.spipf[2],
+            MonitorInstance::Spim3 => &self.spipf[3],
+        }
+    }
+
+    /// Get mutable reference to register accessor (for write operations).
+    #[inline]
+    fn regs_mut(&mut self, instance: MonitorInstance) -> &mut SpiMonitorRegisters {
+        match instance {
+            MonitorInstance::Spim0 => &mut self.spipf[0],
+            MonitorInstance::Spim1 => &mut self.spipf[1],
+            MonitorInstance::Spim2 => &mut self.spipf[2],
+            MonitorInstance::Spim3 => &mut self.spipf[3],
+        }
+    }
+
+    /// Map MonitorInstance to SCU SpiMonitorInstance for routing operations.
+    fn instance_to_scu(instance: MonitorInstance) -> SpiMonitorInstance {
+        match instance {
+            MonitorInstance::Spim0 => SpiMonitorInstance::Spim0,
+            MonitorInstance::Spim1 => SpiMonitorInstance::Spim1,
+            MonitorInstance::Spim2 => SpiMonitorInstance::Spim2,
+            MonitorInstance::Spim3 => SpiMonitorInstance::Spim3,
+        }
+    }
+
+    /// Map MuxSelect to SCU external mux select value.
+    fn mux_to_scu(mux: MuxSelect) -> ScuExtMuxSelect {
+        match mux {
+            MuxSelect::RotControl => ScuExtMuxSelect::Mux0,
+            MuxSelect::HostControl => ScuExtMuxSelect::Mux1,
+        }
+    }
+
+    /// Map SCU external mux select back to MuxSelect.
+    fn scu_to_mux(scu_mux: ScuExtMuxSelect) -> MuxSelect {
+        match scu_mux {
+            ScuExtMuxSelect::Mux0 => MuxSelect::RotControl,
+            ScuExtMuxSelect::Mux1 => MuxSelect::HostControl,
+        }
+    }
+
+    /// Extract enforcement active flag from CTRL register.
+    /// Enforcement is active when passthrough bits are NOT set.
+    /// - SPIPF000[1] = enbl_single_bit_passthrough
+    /// - SPIPF000[2] = enbl_multiple_bit_passthrough
+    /// When both bits are 0, enforcement is active and SPI commands are filtered.
+    fn is_enforcement_active(ctrl: u32) -> bool {
+        let pass_bits = (ctrl >> 1) & 0x3;
+        pass_bits == 0 // Enforcement active when passthrough disabled
+    }
+
+    /// Extract policy lock flag from lock/status register.
+    /// Policy is locked when write-disable bits are set in SPIPF07C.
+    /// SPIPF07C bit 0: wr_dis_of_spipfwa (write disable for address privilege tables)
+    fn is_policy_locked(lock_status: u32) -> bool {
+        // Write-disable bit in SPIPF07C
+        // When this bit is set, policy tables cannot be modified
+        (lock_status >> 0) & 0x1 != 0
+    }
+}
+
+impl<'a> Monitor for Ast1060Monitor<'a> {
+    fn set_mux(&mut self, instance: MonitorInstance, mux: MuxSelect) -> BootResult<()> {
+        // External mux selection is controlled via SCU0F0 register.
+        // Delegate to SCU routing layer which has the actual register access.
+        let scu_instance = Self::instance_to_scu(instance);
+        let scu_mux = Self::mux_to_scu(mux);
+        self.scu.set_spim_ext_mux(scu_instance, scu_mux);
+        Ok(())
+    }
+
+    fn read_mux(&self, instance: MonitorInstance) -> BootResult<MuxSelect> {
+        // Read from SCU0F0 register via SCU routing layer.
+        let scu_instance = Self::instance_to_scu(instance);
+        let scu_mux = self.scu.get_spim_ext_mux(scu_instance);
+        Ok(Self::scu_to_mux(scu_mux))
+    }
+
+    fn soft_reset(&mut self, instance: MonitorInstance) -> BootResult<()> {
+        let regs = self.regs_mut(instance);
+        // Soft reset clears status/logs but preserves policy.
+        // NON-BLOCKING TODO 1: Verify soft reset bit position from AST10x0 datasheet.
+        // Current: uses bit 7 in SPIPF000 as placeholder. May need adjustment.
+        // NON-BLOCKING TODO 2: Implement polling/timeout after write.
+        // Pattern: poll SPIPF000 until reset bit clears or timeout (e.g., 100 microseconds).
+        // In aspeed-rust, hardware self-clears the bit after reset completes.
+        let mut ctrl = regs.read_ctrl();
+        ctrl |= 0x80; // Soft reset bit (placeholder - verify with datasheet)
+        regs.write_ctrl(ctrl);
+        // TODO: Poll until bit 7 clears, with timeout check
+        Ok(())
+    }
+
+    fn hardware_reset(&mut self, instance: MonitorInstance) -> BootResult<()> {
+        let regs = self.regs_mut(instance);
+        // Full hardware reset of all state (SPIPF and related SCU registers).
+        // NON-BLOCKING TODO 1: Verify hardware reset bit and sequence from AST10x0 datasheet.
+        // Current: uses bit 8 in SPIPF000 as placeholder.
+        // NON-BLOCKING TODO 2: Implement polling/timeout after write.
+        // Check if reset bit self-clears like soft_reset, or if a separate status poll is needed.
+        let mut ctrl = regs.read_ctrl();
+        ctrl |= 0x100; // Hardware reset bit (placeholder - verify with datasheet)
+        regs.write_ctrl(ctrl);
+        // TODO: Poll until bit 8 clears or wait for timeout, similar to soft_reset
+        Ok(())
+    }
+
+    fn set_address_privilege(
+        &mut self,
+        instance: MonitorInstance,
+        start_addr: u32,
+        end_addr: u32,
+        _direction: PrivilegeDirection,
+        _op: PrivilegeOp,
+    ) -> BootResult<()> {
+        if end_addr < start_addr {
+            return Err(BootError::InvalidAddress);
+        }
+
+        let regs = self.regs_mut(instance);
+
+        // Address filtering in AST10x0 uses 16KB blocks (ACCESS_BLOCK_UNIT from aspeed-rust).
+        // Each SPIPFWA register controls 32 blocks per register (32 * 16KB = 512KB per register).
+        // NON-BLOCKING TODO: Implement dynamic slot allocation instead of fixed slot 0.
+        // Phase C work: currently allocates all regions to slot 0 for simplicity.
+        // Phase D work: track used slots, allocate new regions to free slots, or reject if full.
+        let slot = 0;
+
+        // Address filter encoding: SPIPFWA stores address range in HW format.
+        // Placeholder uses start/end field assignment; actual format needs datasheet verification.
+        let entry = ((start_addr & 0xFFF0_0000) << 0) | ((end_addr & 0x0F_FFFF) << 0);
+        regs.write_addr_filter_slot(slot, entry);
+
+        Ok(())
+    }
+
+    fn read_region_count(&self, _instance: MonitorInstance) -> BootResult<u32> {
+        // Region count is tracked in memory (following aspeed-rust pattern).
+        // aspeed-rust stores read_blocked_region_num and write_blocked_region_num as struct fields.
+        // We return the read-blocked region count for now; write-blocked can be exposed via separate method if needed.
+        Ok(self.read_blocked_region_count as u32)
+    }
+
+    fn read_status(&self, instance: MonitorInstance) -> BootResult<MonitorStatus> {
+        let regs = self.regs(instance);
+        let ctrl = regs.read_ctrl();
+        let lock_status = regs.read_lock_status();
+
+        // Read actual mux state from SCU (not placeholder from SPIPF)
+        let scu_instance = Self::instance_to_scu(instance);
+        let scu_mux = self.scu.get_spim_ext_mux(scu_instance);
+        let mux = Self::scu_to_mux(scu_mux);
+
+        Ok(MonitorStatus {
+            mux,
+            policy_locked: Self::is_policy_locked(lock_status),
+            enforcement_active: Self::is_enforcement_active(ctrl),
+            violation_count: 0, // NON-BLOCKING TODO: Implement violation log register reading
+                                // Future: read from SPIPF violation count register if available
+        })
+    }
+
+    fn supports_policy_lock(&self) -> bool {
+        // Check hardware capability from a known register or constant.
+        // Placeholder: assume AST10x0 always supports policy lock.
+        true
+    }
+
+    fn lock_policy(&mut self, instance: MonitorInstance) -> BootResult<()> {
+        if !self.supports_policy_lock() {
+            return Err(BootError::LockedOutFromMonitor);
+        }
+
+        let regs = self.regs_mut(instance);
+        // Policy lock is controlled by SPIPF07C register.
+        // From aspeed-rust: bit 0 is `wr_dis_of_spipfwa` (write disable for address tables).
+        let mut lock_status = regs.read_lock_status();
+        lock_status |= 0x1; // Set wr_dis_of_spipfwa bit
+        regs.write_lock_status(lock_status);
+        Ok(())
+    }
+
+    fn verify_policy_locked(&self, instance: MonitorInstance) -> BootResult<()> {
+        if !self.supports_policy_lock() {
+            return Ok(());
+        }
+
+        let regs = self.regs(instance);
+        let lock_status = regs.read_lock_status();
+        if Self::is_policy_locked(lock_status) {
+            Ok(())
+        } else {
+            Err(BootError::PolicyVerificationFailed)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_enforcement_flag() {
+        assert!(!Ast1060Monitor::is_enforcement_active(0));
+        assert!(Ast1060Monitor::is_enforcement_active(1 << 4));
+    }
+
+    #[test]
+    fn test_lock_flag() {
+        assert!(!Ast1060Monitor::is_policy_locked(0));
+        assert!(Ast1060Monitor::is_policy_locked(1));
+    }
+}
diff --git a/target/ast10x0/board/src/spim_wiring.rs b/target/ast10x0/board/src/spim_wiring.rs
new file mode 100644
index 0000000..d33289d
--- /dev/null
+++ b/target/ast10x0/board/src/spim_wiring.rs
@@ -0,0 +1,180 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! 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:
+//! 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 ast10x0_peripherals::scu::{
+    ScuError, ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough,
+    SpiMonitorSource,
+};
+use ast10x0_peripherals::smc::SmcController;
+use ast10x0_peripherals::spimonitor::{
+    LockedSpiMonitor, MonitorPolicy, SpiMonitor, SpiMonitorController, SpiMonitorError,
+    Uninitialized,
+};
+
+/// Static SPIM wiring for one SPI controller.
+///
+/// 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.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct SpimWiring {
+    /// Monitor instance the master is routed through.
+    pub instance: SpiMonitorInstance,
+    /// Which SPI master is being routed.
+    pub source: SpiMonitorSource,
+    /// Passthrough enable for the chosen instance.
+    pub passthrough: SpiMonitorPassthrough,
+    /// External mux selection (board-specific).
+    pub ext_mux: ScuExtMuxSelect,
+    /// Whether to enable the SCU-controlled MISO multi-function pin.
+    pub miso_multi_func: bool,
+}
+
+impl SpimWiring {
+    /// Default wiring for `SmcController::Spi1` (aspeed-rust SPI0,
+    /// `master_idx = 0`) routed through `Spim0` (SPIPF1 @ `0x7E79_1000`).
+    #[must_use]
+    pub const fn default_spi1_via_spim0() -> Self {
+        Self {
+            instance: SpiMonitorInstance::Spim0,
+            source: SpiMonitorSource::Spi1,
+            passthrough: SpiMonitorPassthrough::Enabled,
+            ext_mux: ScuExtMuxSelect::Mux0,
+            miso_multi_func: true,
+        }
+    }
+
+    /// Default wiring for `SmcController::Spi2` (aspeed-rust SPI1,
+    /// `master_idx = 2`) routed through `Spim2` (SPIPF3 @ `0x7E79_3000`).
+    #[must_use]
+    pub const fn default_spi2_via_spim2() -> Self {
+        Self {
+            instance: SpiMonitorInstance::Spim2,
+            source: SpiMonitorSource::Spi2,
+            passthrough: SpiMonitorPassthrough::Enabled,
+            ext_mux: ScuExtMuxSelect::Mux0,
+            miso_multi_func: true,
+        }
+    }
+}
+
+/// Errors raised while applying SPIM wiring.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum SpimWiringError {
+    /// Caller asked for SPIM wiring on FMC, which has no SPIM path.
+    InvalidController,
+    /// `wiring.source` disagrees with the SPI controller being initialized.
+    RouteMismatch,
+    /// SCU-side validation rejected the requested instance.
+    Scu(ScuError),
+    /// SPIPF policy programming or lock failed.
+    Monitor(SpiMonitorError),
+}
+
+impl From<ScuError> for SpimWiringError {
+    fn from(value: ScuError) -> Self {
+        Self::Scu(value)
+    }
+}
+
+impl From<SpiMonitorError> for SpimWiringError {
+    fn from(value: SpiMonitorError) -> Self {
+        Self::Monitor(value)
+    }
+}
+
+/// 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
+/// `MonitorPolicy::empty()` combined with lock will brick the SPI bus until
+/// reset, so callers should pass a vetted preset (see [`presets`]).
+///
+/// # Safety
+/// Caller must hold exclusive access to the SCU register block and to the
+/// target SPIPF block for the lifetime of the returned `LockedSpiMonitor`.
+pub unsafe fn apply_spim_wiring(
+    scu: &ScuRegisters,
+    controller_id: SmcController,
+    wiring: SpimWiring,
+    policy: &MonitorPolicy,
+) -> Result<LockedSpiMonitor, SpimWiringError> {
+    validate_controller_for_source(controller_id, wiring.source)?;
+    scu.validate_spim_instance(wiring.instance)?;
+
+    scu.set_spim_internal_master_route(wiring.instance, wiring.source);
+    scu.set_spim_passthrough(wiring.instance, wiring.passthrough);
+    scu.set_spim_ext_mux(wiring.instance, wiring.ext_mux);
+    scu.set_spim_miso_multi_func(wiring.instance, wiring.miso_multi_func);
+
+    let monitor_controller = match wiring.instance {
+        SpiMonitorInstance::Spim0 => SpiMonitorController::Spim0,
+        SpiMonitorInstance::Spim1 => SpiMonitorController::Spim1,
+        SpiMonitorInstance::Spim2 => SpiMonitorController::Spim2,
+        SpiMonitorInstance::Spim3 => SpiMonitorController::Spim3,
+    };
+
+    // SAFETY: Caller upholds exclusive SPIPF block access for the chosen
+    // instance, mirroring the SCU exclusivity required above.
+    let monitor = unsafe { SpiMonitor::<Uninitialized>::new(monitor_controller) };
+    let configured = monitor.apply_policy(policy)?;
+    let locked = configured.lock()?;
+    Ok(locked)
+}
+
+fn validate_controller_for_source(
+    controller_id: SmcController,
+    source: SpiMonitorSource,
+) -> Result<(), SpimWiringError> {
+    match (controller_id, source) {
+        (SmcController::Fmc, _) => Err(SpimWiringError::InvalidController),
+        (SmcController::Spi1, SpiMonitorSource::Spi1) => Ok(()),
+        (SmcController::Spi2, SpiMonitorSource::Spi2) => Ok(()),
+        (SmcController::Spi1, SpiMonitorSource::Spi2)
+        | (SmcController::Spi2, SpiMonitorSource::Spi1) => Err(SpimWiringError::RouteMismatch),
+    }
+}
+
+/// Built-in `MonitorPolicy` presets vetted against the BMC's flash opcode set.
+pub mod presets {
+    use ast10x0_peripherals::spimonitor::MonitorPolicy;
+
+    /// Allow-list for the BMC's normal flash opcodes covering both 3-byte and
+    /// 4-byte addressing variants. Empty `regions` (no address-privilege
+    /// filter applied).
+    ///
+    /// Opcodes:
+    /// `READ` (`0x03`), `FAST_READ` (`0x0B`), `FAST_READ_4B` (`0x0C`),
+    /// `PP` (`0x02`), `PP_4B` (`0x12`),
+    /// `SE_4K` (`0x20`), `SE_4K_4B` (`0x21`),
+    /// `RDSR` (`0x05`), `WREN` (`0x06`), `WRDI` (`0x04`),
+    /// `RDID` (`0x9F`), `RSTEN` (`0x66`), `RST` (`0x99`).
+    #[must_use]
+    pub const fn bmc_default_policy() -> MonitorPolicy {
+        let mut p = MonitorPolicy::empty();
+        p.allow_commands[0] = 0x03; // READ
+        p.allow_commands[1] = 0x0B; // FAST_READ
+        p.allow_commands[2] = 0x0C; // FAST_READ_4B
+        p.allow_commands[3] = 0x02; // PP
+        p.allow_commands[4] = 0x12; // PP_4B
+        p.allow_commands[5] = 0x20; // SE_4K
+        p.allow_commands[6] = 0x21; // SE_4K_4B
+        p.allow_commands[7] = 0x05; // RDSR
+        p.allow_commands[8] = 0x06; // WREN
+        p.allow_commands[9] = 0x04; // WRDI
+        p.allow_commands[10] = 0x9F; // RDID
+        p.allow_commands[11] = 0x66; // RSTEN
+        p.allow_commands[12] = 0x99; // RST
+        p.allow_command_count = 13;
+        p
+    }
+}
diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel
index d418dac..bb3cefb 100644
--- a/target/ast10x0/peripherals/BUILD.bazel
+++ b/target/ast10x0/peripherals/BUILD.bazel
@@ -40,7 +40,9 @@
         "smc/interrupts.rs",
         "smc/mod.rs",
         "smc/registers.rs",
-        "smc/spi.rs",
+        "smc/spi/mod.rs",
+        "smc/spi/spi.rs",
+        "smc/spi/spi_transaction.rs",
         "smc/types.rs",
         "spimonitor/controller.rs",
         "spimonitor/mod.rs",
diff --git a/target/ast10x0/peripherals/scu/mod.rs b/target/ast10x0/peripherals/scu/mod.rs
index 31fae65..bc236ed 100644
--- a/target/ast10x0/peripherals/scu/mod.rs
+++ b/target/ast10x0/peripherals/scu/mod.rs
@@ -13,6 +13,7 @@
 
 pub use pinctrl::PinctrlPin;
 pub use registers::ScuRegisters;
+pub use routing::SpimGpioOriVal;
 pub use types::{
     ClockRegisterHalf, ScuError, ScuExtMuxSelect, ScuRegisterHalf, SpiMonitorInstance,
     SpiMonitorPassthrough, SpiMonitorSource,
diff --git a/target/ast10x0/peripherals/scu/pinctrl.rs b/target/ast10x0/peripherals/scu/pinctrl.rs
index a9d7b4c..7655161 100644
--- a/target/ast10x0/peripherals/scu/pinctrl.rs
+++ b/target/ast10x0/peripherals/scu/pinctrl.rs
@@ -551,8 +551,180 @@
 pub const PINCTRL_SPI1_QUAD: &[PinctrlPin] = &[PIN_SCU430_17, PIN_SCU430_18];
 
 /// SPI2 quad.
-pub const PINCTRL_SPI2_QUAD: &[PinctrlPin] = &[PIN_SCU41C_30, PIN_SCU41C_31, PIN_SCU430_0, PIN_SCU430_1, PIN_SCU430_2, PIN_SCU430_3, PIN_SCU430_4];
+pub const PINCTRL_SPI2_QUAD: &[PinctrlPin] = &[
+    PIN_SCU41C_30,
+    PIN_SCU41C_31,
+    PIN_SCU430_0,
+    PIN_SCU430_1,
+    PIN_SCU430_2,
+    PIN_SCU430_3,
+    PIN_SCU430_4,
+];
 
+/// SPIM1
+pub const PINCTRL_SPIM1_DEFAULT: &[PinctrlPin] = &[
+    // CSIN
+    CLR_PIN_SCU410_0,
+    CLR_PIN_SCU4B0_0,
+    PIN_SCU690_0,
+    // CLKIN
+    CLR_PIN_SCU410_1,
+    CLR_PIN_SCU4B0_1,
+    PIN_SCU690_1,
+    // MOSIIN
+    CLR_PIN_SCU410_2,
+    CLR_PIN_SCU4B0_2,
+    PIN_SCU690_2,
+    // MISOIN
+    CLR_PIN_SCU410_3,
+    CLR_PIN_SCU4B0_3,
+    PIN_SCU690_3,
+    // IO2IN
+    CLR_PIN_SCU410_4,
+    CLR_PIN_SCU4B0_4,
+    PIN_SCU690_4,
+    // IO3IN
+    CLR_PIN_SCU410_5,
+    CLR_PIN_SCU4B0_5,
+    PIN_SCU690_5,
+    // CSOUT
+    CLR_PIN_SCU410_6,
+    CLR_PIN_SCU4B0_6,
+    PIN_SCU690_6,
+];
+/// SPIM2
+pub const PINCTRL_SPIM2_DEFAULT: &[PinctrlPin] = &[
+    // CSIN
+    CLR_PIN_SCU410_14,
+    CLR_PIN_SCU4B0_14,
+    PIN_SCU690_14,
+    // CLKIN
+    CLR_PIN_SCU410_15,
+    CLR_PIN_SCU4B0_15,
+    PIN_SCU690_15,
+    // MOSIIN
+    CLR_PIN_SCU410_16,
+    CLR_PIN_SCU4B0_16,
+    PIN_SCU690_16,
+    // MISOIN
+    CLR_PIN_SCU410_17,
+    CLR_PIN_SCU4B0_17,
+    PIN_SCU690_17,
+    // IO2IN
+    CLR_PIN_SCU410_18,
+    CLR_PIN_SCU4B0_18,
+    PIN_SCU690_18,
+    // IO3IN
+    CLR_PIN_SCU410_19,
+    CLR_PIN_SCU4B0_19,
+    PIN_SCU690_19,
+    // CSOUT
+    CLR_PIN_SCU410_20,
+    CLR_PIN_SCU4B0_20,
+    PIN_SCU690_20,
+];
+/// SPIM3
+pub const PINCTRL_SPIM3_DEFAULT: &[PinctrlPin] = &[
+    // CSIN
+    CLR_PIN_SCU410_28,
+    CLR_PIN_SCU4B0_28,
+    PIN_SCU690_28,
+    // CLKIN
+    CLR_PIN_SCU410_29,
+    CLR_PIN_SCU4B0_29,
+    PIN_SCU690_29,
+    // MOSIIN
+    CLR_PIN_SCU410_30,
+    CLR_PIN_SCU4B0_30,
+    PIN_SCU690_30,
+    // MISOIN
+    CLR_PIN_SCU410_31,
+    CLR_PIN_SCU4B0_31,
+    PIN_SCU690_31,
+    // IO2IN
+    CLR_PIN_SCU414_0,
+    CLR_PIN_SCU4B4_0,
+    PIN_SCU694_0,
+    // IO3IN
+    CLR_PIN_SCU414_1,
+    CLR_PIN_SCU4B4_1,
+    PIN_SCU694_1,
+    // CSOUT
+    CLR_PIN_SCU414_2,
+    CLR_PIN_SCU4B4_2,
+    PIN_SCU694_2,
+    // CLKOUT
+    CLR_PIN_SCU414_3,
+    CLR_PIN_SCU4B4_3,
+    PIN_SCU694_3,
+    // MOSIOUT
+    CLR_PIN_SCU414_4,
+    CLR_PIN_SCU4B4_4,
+    PIN_SCU694_4,
+    // MISOOUT
+    CLR_PIN_SCU414_5,
+    CLR_PIN_SCU4B4_5,
+    PIN_SCU694_5,
+    // IO2OUT
+    CLR_PIN_SCU414_6,
+    CLR_PIN_SCU4B4_6,
+    PIN_SCU694_6,
+    // IO3OUT
+    CLR_PIN_SCU414_7,
+    CLR_PIN_SCU4B4_7,
+    PIN_SCU694_7,
+];
+/// SPIM4
+pub const PINCTRL_SPIM4_DEFAULT: &[PinctrlPin] = &[
+    // CSIN
+    CLR_PIN_SCU414_10,
+    CLR_PIN_SCU4B4_10,
+    PIN_SCU694_10,
+    // CLKIN
+    CLR_PIN_SCU414_11,
+    CLR_PIN_SCU4B4_11,
+    PIN_SCU694_11,
+    // MOSIIN
+    CLR_PIN_SCU414_12,
+    CLR_PIN_SCU4B4_12,
+    PIN_SCU694_12,
+    // MISOIN
+    CLR_PIN_SCU414_13,
+    CLR_PIN_SCU4B4_13,
+    PIN_SCU694_13,
+    // IO2IN
+    CLR_PIN_SCU414_14,
+    CLR_PIN_SCU4B4_14,
+    PIN_SCU694_14,
+    // IO3IN
+    CLR_PIN_SCU414_15,
+    CLR_PIN_SCU4B4_15,
+    PIN_SCU694_15,
+    // CSOUT
+    CLR_PIN_SCU414_16,
+    CLR_PIN_SCU4B4_16,
+    PIN_SCU694_16,
+    // CLKOUT
+    CLR_PIN_SCU414_17,
+    CLR_PIN_SCU4B4_17,
+    PIN_SCU694_17,
+    // MOSIOUT
+    CLR_PIN_SCU414_18,
+    CLR_PIN_SCU4B4_18,
+    PIN_SCU694_18,
+    // MISOOUT
+    CLR_PIN_SCU414_19,
+    CLR_PIN_SCU4B4_19,
+    PIN_SCU694_19,
+    // IO2OUT
+    CLR_PIN_SCU414_20,
+    CLR_PIN_SCU4B4_20,
+    PIN_SCU694_20,
+    // IO3OUT
+    CLR_PIN_SCU414_21,
+    CLR_PIN_SCU4B4_21,
+    PIN_SCU694_21,
+];
 /// I2C2 pin group: SCL3/SDA3 mux selection on SCU418[0:1].
 ///
 /// The SVD names these EnblSCL3FnPin/EnblSDA3FnPin, corresponding to
diff --git a/target/ast10x0/peripherals/scu/routing.rs b/target/ast10x0/peripherals/scu/routing.rs
index 546eb7d..0ed23f6 100644
--- a/target/ast10x0/peripherals/scu/routing.rs
+++ b/target/ast10x0/peripherals/scu/routing.rs
@@ -7,9 +7,27 @@
 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);
+        } else {
+            val |= 1 << $bit;
+        }
+        $reg.write(|w| unsafe { w.bits(val) });
+    }};
+}
 
 impl ScuRegisters {
-    /// Enable or disable passthrough for a SPI monitor instance.
+    /// Enable or disable passthrough for a SPI monitor instance. Uses SCU0F0[7:4].
     pub fn set_spim_passthrough(
         &self,
         instance: SpiMonitorInstance,
@@ -26,17 +44,16 @@
         });
     }
 
-    /// Route an internal SPI master through the selected SPI monitor path. bit3
+    /// Route an internal SPI master through the selected SPI monitor path.
     pub fn set_spim_internal_master_route(
         &self,
         instance: SpiMonitorInstance,
         source: SpiMonitorSource,
     ) {
         self.unlock_write_protection();
-        self.regs().scu0f0().modify(|_, w| unsafe {
-            w.select_int_spimaster_connection()
-                .bits(instance as u8 + 1)
-        });
+        self.regs()
+            .scu0f0()
+            .modify(|_, w| unsafe { w.select_int_spimaster_connection().bits(instance as u8 + 1) });
 
         let select_spi2 = matches!(source, SpiMonitorSource::Spi2);
         self.regs()
@@ -44,6 +61,27 @@
             .modify(|_, w| w.int_spimaster_sel().bit(select_spi2));
     }
 
+    /// Select the internal SPI master source and SPIM output path.
+    ///
+    /// `output_select` is the raw SCU0F0[2:0] value. Zero disables the
+    /// internal SPI master path; values 1 through 4 select SPIM0 through SPIM3.
+    pub fn set_spim_internal_mux(&self, source: SpiMonitorSource, output_select: u8) -> Result<()> {
+        if output_select > 4 {
+            return Err(super::types::ScuError::InvalidMuxSelection);
+        }
+
+        self.unlock_write_protection();
+        let source_bit = if matches!(source, SpiMonitorSource::Spi2) {
+            0x8
+        } else {
+            0
+        };
+        let mut bits = self.regs().scu0f0().read().bits();
+        bits = (bits & !0xF) | source_bit | u32::from(output_select);
+        self.regs().scu0f0().write(|w| unsafe { w.bits(bits) });
+        Ok(())
+    }
+
     /// Disable any internal SPI-master detour route.
     pub fn clear_spim_internal_master_route(&self) {
         self.unlock_write_protection();
@@ -52,7 +90,127 @@
         self.regs().scu0f0().write(|w| unsafe { w.bits(bits) });
     }
 
-    /// Select the external mux signal for a SPI monitor instance.
+    /// Apply AST1060 SPIM proprietary pin setup before a transaction.
+    pub fn spim_proprietary_pre_config(&self) -> Option<SpimGpioOriVal> {
+        self.unlock_write_protection();
+
+        let scu = self.regs();
+        let gpio = unsafe { &*ast1060_pac::Gpio::ptr() };
+
+        let scu0f0 = scu.scu0f0().read().bits();
+        if scu0f0 & 0x7 == 0 {
+            return None;
+        }
+
+        let spim_idx = (scu0f0 & 0x7) - 1;
+        if spim_idx > 3 {
+            return None;
+        }
+
+        let mut gpio_ori_val = [0; 4];
+
+        for idx in 0..4 {
+            if idx as u32 == spim_idx {
+                continue;
+            }
+
+            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)
+    }
+
+    /// 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 spim_idx = (bits & 0x7) - 1;
+        if spim_idx > 3 {
+            return;
+        }
+
+        for idx in 0..4 {
+            if idx as u32 == spim_idx {
+                continue;
+            }
+
+            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);
+                }
+                _ => {}
+            }
+        }
+    }
+
+    /// Select the external mux signal for a SPI monitor instance. Uses SCU0F0[15:12].
     pub fn set_spim_ext_mux(&self, instance: SpiMonitorInstance, mux: ScuExtMuxSelect) {
         self.unlock_write_protection();
         let bit = mux.as_bool();
@@ -116,12 +274,6 @@
         self.regs().scu0f0().read().bits()
     }
 
-    /// Restore the raw SPI-monitor routing control register image.
-    pub fn restore_spim_route_ctrl(&self, route_ctrl: u32) {
-        self.unlock_write_protection();
-        self.regs().scu0f0().write(|w| unsafe { w.bits(route_ctrl) });
-    }
-
     /// Check that a SPI monitor instance can be represented in SCU routing
     /// operations.
     pub fn validate_spim_instance(&self, instance: SpiMonitorInstance) -> Result<()> {
@@ -132,4 +284,4 @@
             | SpiMonitorInstance::Spim3 => Ok(()),
         }
     }
-}
\ No newline at end of file
+}
diff --git a/target/ast10x0/peripherals/scu/types.rs b/target/ast10x0/peripherals/scu/types.rs
index c01d5a4..21ac7c7 100644
--- a/target/ast10x0/peripherals/scu/types.rs
+++ b/target/ast10x0/peripherals/scu/types.rs
@@ -10,6 +10,7 @@
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum ScuError {
     InvalidMonitorInstance,
+    InvalidMuxSelection,
 }
 
 /// Selects the lower or upper 32-bit control register half for reset domains.
diff --git a/target/ast10x0/peripherals/smc/controller.rs b/target/ast10x0/peripherals/smc/controller.rs
index 2f4618b..5f8139f 100644
--- a/target/ast10x0/peripherals/smc/controller.rs
+++ b/target/ast10x0/peripherals/smc/controller.rs
@@ -461,6 +461,11 @@
         self.controller_id
     }
 
+    /// Get the configured master ID for this controller topology.
+    pub fn master_idx(&self) -> u8 {
+        self.config.topology.master_idx()
+    }
+
     /// Return configured total flash capacity for this controller in bytes.
     pub fn capacity_bytes(&self) -> Result<usize, SmcError> {
         total_capacity_bytes(self.config.cs0, self.config.cs1)
diff --git a/target/ast10x0/peripherals/smc/mod.rs b/target/ast10x0/peripherals/smc/mod.rs
index 8ef133d..f494922 100644
--- a/target/ast10x0/peripherals/smc/mod.rs
+++ b/target/ast10x0/peripherals/smc/mod.rs
@@ -22,7 +22,7 @@
 };
 pub use fmc::{FmcReady, FmcUninit};
 pub use interrupts::{SmcInterrupt, SmcInterruptDecoder};
-pub use spi::{SpiReady, SpiUninit};
+pub use spi::{SpiReady, SpiTransaction, SpiUninit};
 pub use types::{
     AddressWidth, ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcRetryable,
     SmcTopology, TransferMode,
diff --git a/target/ast10x0/peripherals/smc/spi/mod.rs b/target/ast10x0/peripherals/smc/spi/mod.rs
new file mode 100644
index 0000000..af17359
--- /dev/null
+++ b/target/ast10x0/peripherals/smc/spi/mod.rs
@@ -0,0 +1,10 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! SPI1/SPI2 wrapper and transaction guard.
+
+mod spi;
+mod spi_transaction;
+
+pub use spi::{SpiReady, SpiUninit};
+pub use spi_transaction::SpiTransaction;
diff --git a/target/ast10x0/peripherals/smc/spi.rs b/target/ast10x0/peripherals/smc/spi/spi.rs
similarity index 95%
rename from target/ast10x0/peripherals/smc/spi.rs
rename to target/ast10x0/peripherals/smc/spi/spi.rs
index 6a17271..343b708 100644
--- a/target/ast10x0/peripherals/smc/spi.rs
+++ b/target/ast10x0/peripherals/smc/spi/spi.rs
@@ -123,6 +123,16 @@
         self.inner.poll_dma_completion()
     }
 
+    /// Get the controller identifier.
+    pub fn controller_id(&self) -> SmcController {
+        self.inner.controller_id()
+    }
+
+    /// Get the configured master ID for this controller topology.
+    pub fn master_idx(&self) -> u8 {
+        self.inner.master_idx()
+    }
+
     /// Check if SPI controller is ready for operations.
     pub fn is_ready(&self) -> bool {
         self.inner.is_ready()
diff --git a/target/ast10x0/peripherals/smc/spi/spi_transaction.rs b/target/ast10x0/peripherals/smc/spi/spi_transaction.rs
new file mode 100644
index 0000000..02c2f29
--- /dev/null
+++ b/target/ast10x0/peripherals/smc/spi/spi_transaction.rs
@@ -0,0 +1,191 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! Scoped SPI transaction wrapper.
+
+use core::task::Poll;
+
+use super::spi::SpiReady;
+use crate::scu::{ScuRegisters, SpiMonitorInstance, SpiMonitorSource, SpimGpioOriVal};
+use crate::smc::interrupts::SmcInterrupt;
+use crate::smc::types::{ChipSelect, SmcController, SmcError, TransferMode};
+
+#[derive(Clone, Copy)]
+struct SpiMuxState {
+    spim_present: bool,
+    internal_mux_active: bool,
+    proprietary_state: Option<SpimGpioOriVal>,
+}
+
+/// In-flight SPI transaction state.
+///
+/// Synchronous helpers hide this state entirely. DMA returns it so the mux can
+/// stay enabled until DMA completion.
+pub struct SpiTransaction<'a> {
+    spi: &'a mut SpiReady,
+    previous_mux: Option<SpiMuxState>,
+}
+
+impl<'a> SpiTransaction<'a> {
+    fn begin(spi: &'a mut SpiReady, spim: Option<SpiMonitorInstance>) -> Result<Self, SmcError> {
+        let mut mux_state = SpiMuxState {
+            spim_present: spim.is_some(),
+            internal_mux_active: false,
+            proprietary_state: None,
+        };
+
+        if let Some(spim) = spim {
+            let scu = unsafe { ScuRegisters::new_global_unlocked() };
+            scu.validate_spim_instance(spim)
+                .map_err(|_| SmcError::HardwareError)?;
+
+            if spi.master_idx() != 0 {
+                scu.set_spim_internal_mux(Self::spim_source(spi)?, spim as u8 + 1)
+                    .map_err(|_| SmcError::HardwareError)?;
+                mux_state.internal_mux_active = true;
+            }
+
+            mux_state.proprietary_state = scu.spim_proprietary_pre_config();
+        }
+
+        Ok(Self {
+            spi,
+            previous_mux: Some(mux_state),
+        })
+    }
+
+    pub fn read(
+        spi: &'a mut SpiReady,
+        cs: ChipSelect,
+        offset: u32,
+        buf: &mut [u8],
+    ) -> Result<usize, SmcError> {
+        Self::read_with_spim(spi, None, cs, offset, buf)
+    }
+
+    pub fn read_with_spim(
+        spi: &'a mut SpiReady,
+        spim: impl Into<Option<SpiMonitorInstance>>,
+        cs: ChipSelect,
+        offset: u32,
+        buf: &mut [u8],
+    ) -> Result<usize, SmcError> {
+        let mut txn = Self::begin(spi, spim.into())?;
+        let result = txn.spi.read(cs, offset, buf);
+        txn.finish_result(result)
+    }
+
+    pub fn transceive_user(
+        spi: &'a mut SpiReady,
+        cs: ChipSelect,
+        cmd: &[u8],
+        tx_payload: &[u8],
+        rx: &mut [u8],
+        mode: TransferMode,
+    ) -> Result<(), SmcError> {
+        Self::transceive_user_with_spim(spi, None, cs, cmd, tx_payload, rx, mode)
+    }
+
+    pub fn transceive_user_with_spim(
+        spi: &'a mut SpiReady,
+        spim: impl Into<Option<SpiMonitorInstance>>,
+        cs: ChipSelect,
+        cmd: &[u8],
+        tx_payload: &[u8],
+        rx: &mut [u8],
+        mode: TransferMode,
+    ) -> Result<(), SmcError> {
+        let mut txn = Self::begin(spi, spim.into())?;
+        let result = txn.spi.transceive_user(cs, cmd, tx_payload, rx, mode);
+        txn.finish_result(result)
+    }
+
+    pub fn dma_read(
+        spi: &'a mut SpiReady,
+        cs: ChipSelect,
+        flash_offset: u32,
+        dram_addr: usize,
+        len: u32,
+    ) -> Result<Self, SmcError> {
+        Self::dma_read_with_spim(spi, None, cs, flash_offset, dram_addr, len)
+    }
+
+    pub fn dma_read_with_spim(
+        spi: &'a mut SpiReady,
+        spim: impl Into<Option<SpiMonitorInstance>>,
+        cs: ChipSelect,
+        flash_offset: u32,
+        dram_addr: usize,
+        len: u32,
+    ) -> Result<Self, SmcError> {
+        let txn = Self::begin(spi, spim.into())?;
+        txn.spi.dma_read(cs, flash_offset, dram_addr, len)?;
+        Ok(txn)
+    }
+
+    pub fn poll_dma_completion(&mut self) -> Poll<Result<(), SmcError>> {
+        match self.spi.poll_dma_completion() {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(result) => Poll::Ready(result.and_then(|_| self.finish_restore())),
+        }
+    }
+
+    pub fn handle_dma_irq(&mut self) -> Result<SmcInterrupt, SmcError> {
+        let interrupt = self.spi.handle_dma_irq()?;
+        self.finish_restore()?;
+        Ok(interrupt)
+    }
+
+    fn finish_result<T>(&mut self, result: Result<T, SmcError>) -> Result<T, SmcError> {
+        let restore = self.finish_restore();
+        match (result, restore) {
+            (Ok(value), Ok(())) => Ok(value),
+            (Err(err), _) => Err(err),
+            (Ok(_), Err(err)) => Err(err),
+        }
+    }
+
+    fn finish_restore(&mut self) -> Result<(), SmcError> {
+        if let Some(state) = self.previous_mux.take() {
+            if state.spim_present {
+                let scu = unsafe { ScuRegisters::new_global_unlocked() };
+
+                if let Some(proprietary_state) = state.proprietary_state {
+                    scu.spim_proprietary_post_config(proprietary_state);
+                }
+
+                if state.internal_mux_active {
+                    scu.clear_spim_internal_master_route();
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn spim_source(spi: &SpiReady) -> Result<SpiMonitorSource, SmcError> {
+        match spi.controller_id() {
+            SmcController::Spi1 => Ok(SpiMonitorSource::Spi1),
+            SmcController::Spi2 => Ok(SpiMonitorSource::Spi2),
+            SmcController::Fmc => Err(SmcError::InvalidChipSelect),
+        }
+    }
+}
+
+impl Drop for SpiTransaction<'_> {
+    fn drop(&mut self) {
+        if let Some(state) = self.previous_mux.take() {
+            if state.spim_present {
+                let scu = unsafe { ScuRegisters::new_global_unlocked() };
+
+                if let Some(proprietary_state) = state.proprietary_state {
+                    scu.spim_proprietary_post_config(proprietary_state);
+                }
+
+                if state.internal_mux_active {
+                    scu.clear_spim_internal_master_route();
+                }
+            }
+        }
+    }
+}
diff --git a/target/ast10x0/tests/smc/BUILD.bazel b/target/ast10x0/tests/smc/BUILD.bazel
index 9d9937a..118a569 100644
--- a/target/ast10x0/tests/smc/BUILD.bazel
+++ b/target/ast10x0/tests/smc/BUILD.bazel
@@ -39,3 +39,33 @@
     name = "smc_write_evb_test",
     actual = "//target/ast10x0/tests/smc/write:smc_write_evb_test",
 )
+
+alias(
+    name = "smc_spi1_read_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi1_read_test",
+)
+
+alias(
+    name = "smc_spi1_read_evb_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi1_read_evb_test",
+)
+
+alias(
+    name = "smc_spi2_read_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi2_read_test",
+)
+
+alias(
+    name = "smc_spi2_read_evb_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi2_read_evb_test",
+)
+
+alias(
+    name = "smc_spi_cs0_read_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi_cs0_read_test",
+)
+
+alias(
+    name = "smc_spi_cs0_read_evb_test",
+    actual = "//target/ast10x0/tests/smc/read:smc_spi_cs0_read_evb_test",
+)
diff --git a/target/ast10x0/tests/smc/read/BUILD.bazel b/target/ast10x0/tests/smc/read/BUILD.bazel
index 1e649f7..d5df482 100644
--- a/target/ast10x0/tests/smc/read/BUILD.bazel
+++ b/target/ast10x0/tests/smc/read/BUILD.bazel
@@ -29,6 +29,28 @@
 )
 
 rust_binary(
+    name = "target_spi1",
+    srcs = [
+        "target_spi1.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/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 = [
         "target.rs",
@@ -52,6 +74,96 @@
 )
 
 system_image(
+    name = "smc_spi1_read_test",
+    kernel = ":target_spi1",
+    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_spi1_read_evb_test",
+    image = ":smc_spi1_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",
+    tags = ["kernel"],
+)
+
+alias(
+    name = "smc_spi_cs0_read_test",
+    actual = ":smc_spi1_read_test",
+    visibility = ["//visibility:public"],
+)
+
+alias(
+    name = "smc_spi_cs0_read_evb_test",
+    actual = ":smc_spi1_read_evb_test",
+    visibility = ["//visibility:public"],
+)
+
+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/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",
+    ],
+)
+
+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 = "spi2_no_panics_test",
+    binary = ":smc_spi2_read_test",
+    tags = ["kernel"],
+)
+
+system_image(
     name = "smc_read_test",
     kernel = ":target",
     platform = "//target/ast10x0",
diff --git a/target/ast10x0/tests/smc/read/target.rs b/target/ast10x0/tests/smc/read/target.rs
index 6915acb..09b2065 100644
--- a/target/ast10x0/tests/smc/read/target.rs
+++ b/target/ast10x0/tests/smc/read/target.rs
@@ -112,7 +112,7 @@
     // --- 4. DMA  ---
     let tempbuf = unsafe { core::slice::from_raw_parts(0x41000 as *mut u8, 256) };
 
-    let _ = match controller.dma_read(ChipSelect::Cs0, 0x500, 0x41000 as usize, 256) {
+    let _ = match controller.dma_read(ChipSelect::Cs0, 0x400, 0x41000 as usize, 256) {
         Err(SmcError::InvalidCapacity) => Ok(()),
         Err(other) => Err(other),
         Ok(()) => Err(SmcError::HardwareError),
diff --git a/target/ast10x0/tests/smc/read/target_spi1.rs b/target/ast10x0/tests/smc/read/target_spi1.rs
new file mode 100644
index 0000000..4da9be0
--- /dev/null
+++ b/target/ast10x0/tests/smc/read/target_spi1.rs
@@ -0,0 +1,142 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+//! AST10x0 SMC SPI1 read smoke test target.
+
+#![no_std]
+#![no_main]
+
+use ast10x0_peripherals::scu::{
+    pinctrl::{PINCTRL_SPI1_QUAD, PINCTRL_SPIM1_DEFAULT},
+    ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource,
+};
+use ast10x0_peripherals::smc::{
+    ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiTransaction,
+    SpiUninit,
+};
+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 core::ptr::write_volatile;
+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: 50,
+};
+
+pub struct Target {}
+
+fn config_spi1_master_controller() -> Result<(), SmcError> {
+    let scu = unsafe { ScuRegisters::new_global_unlocked() };
+    scu.apply_pinctrl_group(PINCTRL_SPIM1_DEFAULT);
+    scu.apply_pinctrl_group(PINCTRL_SPI1_QUAD);
+    // Configure the mux for the SPI master controller path.
+    scu.set_spim_internal_mux(SpiMonitorSource::Spi1, 1)
+        .map_err(|_| SmcError::HardwareError)?;
+    scu.set_spim_internal_master_route(SpiMonitorInstance::Spim0, SpiMonitorSource::Spi1);
+    scu.set_spim_passthrough(SpiMonitorInstance::Spim0, SpiMonitorPassthrough::Enabled);
+    scu.set_spim_ext_mux(SpiMonitorInstance::Spim0, ScuExtMuxSelect::Mux1);
+    pw_log::info!("SCU pinmux and SPIM routing configured for SPI1 monitoring");
+    Ok(())
+}
+
+fn run_spi1_read_test() -> Result<(), SmcError> {
+    config_spi1_master_controller()?;
+
+    let config = SmcConfig {
+        controller_id: SmcController::Spi1,
+        cs0: Some(SPI_FLASH_CONFIG),
+        cs1: None,
+        dma_enabled: true,
+        enable_interrupts: false,
+        topology: SmcTopology::HostSpi { master_idx: 0 },
+    };
+
+    pw_log::info!("=== AST10x0 SMC SPI1 read test ===");
+    let spi = unsafe { SpiUninit::new(SmcController::Spi1, config)? };
+    let mut spi = spi.init()?;
+    spi.spi_nor_read_init(ChipSelect::Cs0)?;
+
+    if !spi.is_ready() {
+        return Err(SmcError::HardwareError);
+    }
+
+    pw_log::info!("=== SPI1 controller register ===");
+    dump_smc_register(0x7E63_0000, 16);
+    dump_smc_register(0x7E63_0080, 16);
+    pw_log::info!("=== SCU QSPI Mux routing register ===");
+    dump_smc_register(0x7E6E_20F0, 4);
+    pw_log::info!("=== SPI1 controller/window ===");
+    dump_smc_register(0x9000_0000, 16);
+
+    unsafe {
+        write_volatile(0x7E63_0080 as *mut u32, 0x0);
+        write_volatile(0x7E63_0084 as *mut u32, 0x04220000);
+        write_volatile(0x7E63_0088 as *mut u32, 0x800b3640);
+        write_volatile(0x7E63_008c as *mut u32, 0x0);
+        write_volatile(0x7E63_0094 as *mut u32, 0x00790000);
+    }
+
+    pw_log::info!("=== SPI1 read ===");
+    let mut buf = [0u8; 64];
+    let n = SpiTransaction::read_with_spim(
+        &mut spi,
+        SpiMonitorInstance::Spim0,
+        ChipSelect::Cs0,
+        0x100000,
+        &mut buf,
+    )?;
+    if n != buf.len() {
+        return Err(SmcError::HardwareError);
+    }
+    dump_smc_read(&buf, buf.len() as u32);
+
+    pw_log::info!("=== SPI1 DMA read @ 0x00100000 ===");
+    let dma_buf = unsafe { core::slice::from_raw_parts_mut(0x41000 as *mut u8, 256) };
+    let mut dma_txn = SpiTransaction::dma_read_with_spim(
+        &mut spi,
+        SpiMonitorInstance::Spim0,
+        ChipSelect::Cs0,
+        0x100000,
+        0x41000usize,
+        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 SPI1 read Test";
+
+    fn main() -> ! {
+        let sentinel = if run_spi1_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/smc/read/target_spi2.rs b/target/ast10x0/tests/smc/read/target_spi2.rs
new file mode 100644
index 0000000..d1485dc
--- /dev/null
+++ b/target/ast10x0/tests/smc/read/target_spi2.rs
@@ -0,0 +1,142 @@
+// 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_peripherals::scu::{
+    pinctrl::{PINCTRL_SPI2_QUAD, PINCTRL_SPIM3_DEFAULT},
+    ScuExtMuxSelect, ScuRegisters, SpiMonitorInstance, SpiMonitorPassthrough, SpiMonitorSource,
+};
+use ast10x0_peripherals::smc::{
+    ChipSelect, FlashConfig, SmcConfig, SmcController, SmcError, SmcTopology, SpiTransaction,
+    SpiUninit,
+};
+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 core::ptr::write_volatile;
+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: 50,
+};
+
+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_SPI2_QUAD);
+    // Configure the mux for the SPI master controller path.
+    scu.set_spim_internal_mux(SpiMonitorSource::Spi2, 3)
+        .map_err(|_| SmcError::HardwareError)?;
+    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");
+    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, 4);
+    pw_log::info!("=== SPI2 controller/window ===");
+    dump_smc_register(0xB000_0000, 16);
+/*
+    unsafe {
+        write_volatile(0x7E64_0080 as *mut u32, 0x0);
+        write_volatile(0x7E64_0084 as *mut u32, 0x04220000);
+        write_volatile(0x7E64_0088 as *mut u32, 0x800b3640);
+        write_volatile(0x7E64_008c as *mut u32, 0x0);
+        write_volatile(0x7E64_0094 as *mut u32, 0x00790000);
+    }
+*/
+    pw_log::info!("=== SPI2 read ===");
+    let mut buf = [0u8; 64];
+    let n = SpiTransaction::read_with_spim(
+        &mut spi,
+        SpiMonitorInstance::Spim2,
+        ChipSelect::Cs0,
+        0x100000,
+        &mut buf,
+    )?;
+    if n != buf.len() {
+        return Err(SmcError::HardwareError);
+    }
+    dump_smc_read(&buf, buf.len() as u32);
+
+    pw_log::info!("=== SPI2 DMA read @ 0x00100000 ===");
+    let dma_buf = unsafe { core::slice::from_raw_parts_mut(0x41000 as *mut u8, 256) };
+    let mut dma_txn = SpiTransaction::dma_read_with_spim(
+        &mut spi,
+        SpiMonitorInstance::Spim2,
+        ChipSelect::Cs0,
+        0x100000,
+        0x41000usize,
+        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);