From c1a771cb4b017a0a8a81d877e132aaef9ea38599 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Mon, 20 Apr 2026 17:15:53 -0700 Subject: [PATCH 1/2] stm32: add GTZC and SAU drivers for TrustZone-capable families MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds initial support for the Global TrustZone Controller (GTZC) and Security Attribution Unit (SAU) on STM32 families with TrustZone: - WBA (gtzc_wba): TZSC + MPCBB + TZIC - U5/H5 (gtzc_v1): TZSC + MPCBB + TZIC + MPCWM (watermarks via PAC) - H503/L5: module stub, PAC sub-peripherals differ from wba/v1 embassy-stm32/src/gtzc/mod.rs: - `Mpcbb`: wraps pac::gtzc::Mpcbb; per-block and bulk secure/priv ops, raw bitmap read/write, global lock - `Tzic`: wraps pac::gtzc::Tzic; enable/clear/status per register group, convenience `enable_all(n_regs)` - `lock()` / `is_locked()`: lock TZSC config (cfg-gated per variant) embassy-stm32/src/sau/mod.rs: - `init(&[Region])`: programs up to 8 SAU regions, enables SAU and SecureFault via the cortex-m peripheral API - `disable()`: clears SAU enable bit examples/stm32wba6/src/bin/trustzone_setup.rs: - End-to-end example: SAU regions → MPCBB1/MPCBB2 → TZSC seccfgr → TZIC enable → lock Both gtzc and sau modules compile cleanly for stm32wba65ri (gtzc_wba) and stm32u585ai (gtzc_v1). --- embassy-stm32/src/gtzc/mod.rs | 293 ++++++++++++++++++ embassy-stm32/src/lib.rs | 4 + embassy-stm32/src/sau/mod.rs | 112 +++++++ examples/stm32wba6/src/bin/trustzone_setup.rs | 119 +++++++ 4 files changed, 528 insertions(+) create mode 100644 embassy-stm32/src/gtzc/mod.rs create mode 100644 embassy-stm32/src/sau/mod.rs create mode 100644 examples/stm32wba6/src/bin/trustzone_setup.rs diff --git a/embassy-stm32/src/gtzc/mod.rs b/embassy-stm32/src/gtzc/mod.rs new file mode 100644 index 0000000000..e95385f4e1 --- /dev/null +++ b/embassy-stm32/src/gtzc/mod.rs @@ -0,0 +1,293 @@ +//! Global TrustZone Controller (GTZC) driver. +//! +//! The GTZC provides hardware-enforced TrustZone security for STM32 microcontrollers. +//! It consists of three sub-units: +//! +//! - **TZSC** (TrustZone Security Controller): configures which peripherals are accessible +//! from the Non-Secure world and which require privileged access. +//! - **MPCBB** (Memory Protection Controller Block-Based): configures security and privilege +//! attributes for individual 512-byte SRAM blocks. +//! - **TZIC** (TrustZone Illegal access Controller): generates interrupts when a Non-Secure +//! or unprivileged access is made to a Secure or privileged-only resource. +//! +//! # Supported families +//! +//! | Variant | Family | TZSC | MPCBB | TZIC | MPCWM | +//! |-------------|---------------|--------|-------|------|-------| +//! | `gtzc_wba` | WBA | Full | Yes | Yes | No | +//! | `gtzc_v1` | U5 / H5 | Full | Yes | Yes | Yes | +//! | `gtzc_h503` | H503 | PrivOnly| Yes | No | BKPSRAM| +//! | `gtzc_l5` | L5 | Full | Yes | Yes | No | +//! +//! # Usage +//! +//! All GTZC configuration must be performed from the Secure world. After setup, call +//! [`lock()`] to prevent Non-Secure code from modifying the configuration. +//! +//! MPCBB and TZIC instances are accessed via chip-specific PAC constants +//! (e.g., `pac::GTZC_MPCBB1`, `pac::GTZC_TZIC`). Consult your device reference +//! manual for which instances are present. +//! +//! ```no_run +//! use embassy_stm32::{gtzc, pac}; +//! +//! // Configure SRAM1 blocks as Non-Secure (wba or v1) +//! let mpcbb1 = unsafe { gtzc::Mpcbb::new(pac::GTZC_MPCBB1) }; +//! mpcbb1.set_all_secure(/*n_regs=*/14, false); // SRAM1 = 28 blocks × 512 B on WBA65 +//! +//! // Enable TZIC to generate IRQs on illegal accesses (wba or v1) +//! let tzic = unsafe { gtzc::Tzic::new(pac::GTZC_TZIC) }; +//! tzic.enable_all(/*n_regs=*/4); +//! +//! // Lock TZSC so the Non-Secure world cannot reconfigure it (wba or v1) +//! unsafe { gtzc::lock(); } +//! ``` + +#[cfg(any(gtzc_wba, gtzc_v1))] +use crate::pac; + +// ──────────────────────────────────────────────────────────────────────────── +// MPCBB — Memory Protection Controller Block-Based +// ──────────────────────────────────────────────────────────────────────────── + +/// Memory Protection Controller Block-Based (MPCBB) wrapper. +/// +/// Controls security and privilege attributes of individual 512-byte SRAM blocks. +/// Each `seccfgr` / `privcfgr` register covers 32 contiguous blocks (= 16 KiB). +/// +/// Obtain a wrapper via [`Mpcbb::new`] with the chip-specific PAC constant for +/// the desired SRAM, e.g., `pac::GTZC_MPCBB1`, `pac::GTZC_MPCBB2`, etc. +#[cfg(any(gtzc_wba, gtzc_v1))] +pub struct Mpcbb { + inner: pac::gtzc::Mpcbb, +} + +#[cfg(any(gtzc_wba, gtzc_v1))] +impl Mpcbb { + /// Wrap a PAC MPCBB peripheral instance. + /// + /// # Safety + /// The caller must ensure no concurrent access to the same MPCBB instance and + /// that this code runs in the Secure world. + #[inline] + pub unsafe fn new(inner: pac::gtzc::Mpcbb) -> Self { + Self { inner } + } + + /// Globally lock this MPCBB's configuration. + /// + /// Once locked the security and privilege configuration cannot be changed until + /// the next hardware reset. + #[inline] + pub fn lock(&self) { + self.inner.cr().modify(|r| r.set_glock(true)); + } + + /// Returns `true` if the global lock has been set. + #[inline] + pub fn is_locked(&self) -> bool { + self.inner.cr().read().glock() + } + + /// Set the security attribute of a single 512-byte block. + /// + /// `block_idx` is a 0-based index into this SRAM region. + /// `secure = true` marks the block as Secure (Non-Secure accesses generate an IRQ); + /// `secure = false` makes it accessible from both worlds. + #[inline] + pub fn set_block_secure(&self, block_idx: usize, secure: bool) { + let reg = block_idx / 32; + let bit = block_idx % 32; + self.inner.seccfgr(reg).modify(|r| { + if secure { + r.set_sec(r.sec() | (1u32 << bit)); + } else { + r.set_sec(r.sec() & !(1u32 << bit)); + } + }); + } + + /// Set the privilege attribute of a single 512-byte block. + /// + /// `privileged = true` restricts access to privileged mode only. + #[inline] + pub fn set_block_privileged(&self, block_idx: usize, privileged: bool) { + let reg = block_idx / 32; + let bit = block_idx % 32; + self.inner.privcfgr(reg).modify(|r| { + if privileged { + r.set_priv_(r.priv_() | (1u32 << bit)); + } else { + r.set_priv_(r.priv_() & !(1u32 << bit)); + } + }); + } + + /// Write a raw security bitmap covering 32 contiguous blocks. + /// + /// `reg` selects which group of 32 blocks to configure (0-based). + /// Each bit in `bits` corresponds to one block: `1` = Secure, `0` = Non-Secure. + #[inline] + pub fn set_seccfgr(&self, reg: usize, bits: u32) { + self.inner.seccfgr(reg).write(|r| r.set_sec(bits)); + } + + /// Write a raw privilege bitmap covering 32 contiguous blocks. + /// + /// Each bit: `1` = Privileged-only, `0` = Unprivileged-accessible. + #[inline] + pub fn set_privcfgr(&self, reg: usize, bits: u32) { + self.inner.privcfgr(reg).write(|r| r.set_priv_(bits)); + } + + /// Read the raw security bitmap for a group of 32 blocks. + #[inline] + pub fn seccfgr(&self, reg: usize) -> u32 { + self.inner.seccfgr(reg).read().sec() + } + + /// Read the raw privilege bitmap for a group of 32 blocks. + #[inline] + pub fn privcfgr(&self, reg: usize) -> u32 { + self.inner.privcfgr(reg).read().priv_() + } + + /// Set all blocks as Secure or Non-Secure in one call. + /// + /// `n_regs` is the number of 32-block registers that cover this SRAM. + /// Calculate as `sram_size_bytes.div_ceil(16 * 1024)`. For example: + /// - 448 KiB SRAM (WBA65 SRAM1) → `n_regs = 14` + /// - 64 KiB SRAM (WBA65 SRAM2) → `n_regs = 4` + pub fn set_all_secure(&self, n_regs: usize, secure: bool) { + let bits = if secure { 0xFFFF_FFFF } else { 0 }; + for n in 0..n_regs { + self.inner.seccfgr(n).write(|r| r.set_sec(bits)); + } + } + + /// Set all blocks as Privileged-only or Unprivileged-accessible in one call. + /// + /// See [`set_all_secure`] for the `n_regs` calculation. + pub fn set_all_privileged(&self, n_regs: usize, privileged: bool) { + let bits = if privileged { 0xFFFF_FFFF } else { 0 }; + for n in 0..n_regs { + self.inner.privcfgr(n).write(|r| r.set_priv_(bits)); + } + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// TZIC — TrustZone Illegal access Controller +// ──────────────────────────────────────────────────────────────────────────── + +/// TrustZone Illegal access Controller (TZIC) wrapper. +/// +/// Generates an interrupt when a Non-Secure or unprivileged access is attempted +/// to a resource marked Secure or Privileged-only by TZSC or MPCBB. +/// +/// Each register covers a group of peripherals/blocks. Consult your device +/// reference manual for the number of groups available on your chip. +/// +/// Obtain a wrapper via [`Tzic::new`] with `pac::GTZC_TZIC`. +#[cfg(any(gtzc_wba, gtzc_v1))] +pub struct Tzic { + inner: pac::gtzc::Tzic, +} + +#[cfg(any(gtzc_wba, gtzc_v1))] +impl Tzic { + /// Wrap a PAC TZIC peripheral instance. + /// + /// # Safety + /// The caller must ensure no concurrent access and that this runs in Secure world. + #[inline] + pub unsafe fn new(inner: pac::gtzc::Tzic) -> Self { + Self { inner } + } + + /// Enable all illegal-access IRQs for register group `reg`. + #[inline] + pub fn enable_irqs(&self, reg: usize) { + self.inner.ier(reg).write(|r| r.set_ie(0xFFFF_FFFF)); + } + + /// Disable all illegal-access IRQs for register group `reg`. + #[inline] + pub fn disable_irqs(&self, reg: usize) { + self.inner.ier(reg).write(|r| r.set_ie(0)); + } + + /// Clear all pending illegal-access flags for register group `reg`. + #[inline] + pub fn clear_flags(&self, reg: usize) { + self.inner.fcr(reg).write(|r| r.set_cf(0xFFFF_FFFF)); + } + + /// Read pending illegal-access flags for register group `reg`. + /// + /// Each bit corresponds to one peripheral or SRAM block; `1` = access violation pending. + #[inline] + pub fn status(&self, reg: usize) -> u32 { + self.inner.sr(reg).read().f() + } + + /// Clear any pending flags then enable IRQs for all `n_regs` register groups. + /// + /// Typical call: + /// - WBA65: `tzic.enable_all(4)` + /// - U585: `tzic.enable_all(3)` + pub fn enable_all(&self, n_regs: usize) { + for reg in 0..n_regs { + self.clear_flags(reg); + self.enable_irqs(reg); + } + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// TZSC — TrustZone Security Controller (lock / raw access helpers) +// ──────────────────────────────────────────────────────────────────────────── + +/// Lock the TZSC security and privilege configuration until the next reset. +/// +/// After locking, no code (Secure or Non-Secure) can modify the TZSC register +/// contents. Typically called at the end of the Secure world's boot setup. +/// +/// For per-peripheral security/privilege configuration access the TZSC PAC registers +/// directly via `pac::GTZC_TZSC` — field names are chip-specific: +/// +/// - **WBA** (`gtzc_wba`): `pac::GTZC_TZSC.tzsc_seccfgr1().modify(|r| r.set_usart1sec(false))` +/// - **U5 / H5** (`gtzc_v1`): `pac::GTZC_TZSC.seccfgr1().modify(|r| r.set_usart1sec(false))` +/// +/// # Safety +/// Must be called from the Secure world. +#[cfg(any(gtzc_wba, gtzc_v1))] +pub unsafe fn lock() { + #[cfg(gtzc_wba)] + pac::GTZC_TZSC.tzsc_cr().modify(|r| r.set_lck(true)); + + #[cfg(gtzc_v1)] + pac::GTZC_TZSC.cr().modify(|r| r.set_lck(true)); +} + +/// Returns `true` if the TZSC configuration is locked. +#[cfg(any(gtzc_wba, gtzc_v1))] +pub fn is_locked() -> bool { + #[cfg(gtzc_wba)] + return pac::GTZC_TZSC.tzsc_cr().read().lck(); + + #[cfg(gtzc_v1)] + return pac::GTZC_TZSC.cr().read().lck(); +} + +// ──────────────────────────────────────────────────────────────────────────── +// MPCWM — Memory Protection Controller Watermark (gtzc_v1 only) +// ──────────────────────────────────────────────────────────────────────────── +// +// The MPCWM allows configuring security and privilege of sub-regions within +// external memories (FMC, OCTOSPI, BKPSRAM). Each watermark region A/B has: +// - CFGRx: SREN (sub-region enable), SRLOCK, SEC, PRIV +// - Rx: SUBA_START (granularity), SUBA_LENGTH +// +// Direct access via `pac::GTZC_TZSC.mpcwm1acfgr()` etc. is recommended until +// a higher-level MPCWM API is added. diff --git a/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index 05bf0416bd..841d86a40e 100644 --- a/embassy-stm32/src/lib.rs +++ b/embassy-stm32/src/lib.rs @@ -102,6 +102,8 @@ pub mod exti; pub mod flash; #[cfg(fmc)] pub mod fmc; +#[cfg(any(gtzc_wba, gtzc_v1, gtzc_h503, gtzc_l5))] +pub mod gtzc; #[cfg(hash)] pub mod hash; #[cfg(all(hrtim, feature = "stm32-hrtim"))] @@ -140,6 +142,8 @@ pub mod rtc; pub mod saes; #[cfg(sai)] pub mod sai; +#[cfg(any(gtzc_wba, gtzc_v1, gtzc_h503, gtzc_l5))] +pub mod sau; #[cfg(sdmmc)] pub mod sdmmc; #[cfg(spdifrx)] diff --git a/embassy-stm32/src/sau/mod.rs b/embassy-stm32/src/sau/mod.rs new file mode 100644 index 0000000000..f766b76d20 --- /dev/null +++ b/embassy-stm32/src/sau/mod.rs @@ -0,0 +1,112 @@ +//! Security Attribution Unit (SAU) driver. +//! +//! The SAU is a Cortex-M Security Extension core peripheral that defines which memory +//! regions are Secure, Non-Secure, or Non-Secure Callable. It is present on all +//! Cortex-M23 and Cortex-M33 processors that implement the TrustZone extension. +//! +//! Up to 8 configurable regions are supported. Memory not covered by any enabled region +//! is treated as **Secure** when the SAU is enabled. +//! +//! # Usage +//! +//! ```no_run +//! use embassy_stm32::sau::{Attribute, Region, init}; +//! +//! let regions = [ +//! Region { +//! base_address: 0x0808_0000, // Non-Secure Flash (must be 32-byte aligned) +//! end_address: 0x080F_FFFF, // Inclusive end (lower 5 bits must be 0x1F) +//! attribute: Attribute::NonSecure, +//! }, +//! Region { +//! base_address: 0x2007_0000, // Non-Secure SRAM2 +//! end_address: 0x2007_FFFF, +//! attribute: Attribute::NonSecure, +//! }, +//! Region { +//! base_address: 0x0C07_F000, // Non-Secure Callable veneer region +//! end_address: 0x0C07_FFFF, +//! attribute: Attribute::NonSecureCallable, +//! }, +//! ]; +//! +//! unsafe { init(®ions); } +//! ``` + +use cortex_m::peripheral::sau::{SauRegion, SauRegionAttribute}; +use cortex_m::peripheral::scb::Exception; + +/// SAU region security attribute. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Attribute { + /// Non-Secure: accessible from both Secure and Non-Secure worlds. + NonSecure = 0, + /// Non-Secure Callable: enables calls from Non-Secure world into Secure code + /// through a veneer (gate) function at this address. The NSC bit is set in RLAR. + NonSecureCallable = 1, +} + +/// SAU region definition. +/// +/// Both `base_address` and `end_address` must be 32-byte aligned: +/// - `base_address & 0x1F == 0` (lower 5 bits must be zero) +/// - `end_address & 0x1F == 0x1F` (lower 5 bits must be one) +/// +/// For example, to cover 0x0808_0000–0x080F_FFFF both constraints are naturally satisfied. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Region { + /// Base address of the region (inclusive). Must be 32-byte aligned. + pub base_address: u32, + /// End address of the region (inclusive). The lower 5 bits must be `0x1F`. + pub end_address: u32, + /// Security attribute for this region. + pub attribute: Attribute, +} + +/// Initialize and enable the SAU with the given region definitions. +/// +/// This function: +/// 1. Disables the SAU. +/// 2. Programs up to 8 regions from `regions` (extras are silently ignored). +/// 3. Re-enables the SAU. +/// 4. Enables the `SecureFault` exception so illegal TrustZone accesses surface as +/// a debuggable fault rather than escalating silently to `HardFault`. +/// +/// Memory not covered by any configured region is treated as **Secure**. +/// +/// # Safety +/// Must be called from the Secure world before Non-Secure code is started. +/// Regions must satisfy the 32-byte alignment constraints documented on [`Region`]. +pub unsafe fn init(regions: &[Region]) { + let mut core = unsafe { cortex_m::Peripherals::steal() }; + + for (i, region) in regions.iter().enumerate().take(8) { + let sau_region = SauRegion { + base_address: region.base_address & !0x1F, + limit_address: region.end_address | 0x1F, + attribute: match region.attribute { + Attribute::NonSecure => SauRegionAttribute::NonSecure, + Attribute::NonSecureCallable => SauRegionAttribute::NonSecureCallable, + }, + }; + // Ignore errors: out-of-range region numbers are guarded by `take(8)` above. + let _ = core.SAU.set_region(i as u8, sau_region); + } + + core.SAU.enable(); + + // Enable SecureFault so TrustZone violations produce a dedicated exception. + core.SCB.enable(Exception::SecureFault); +} + +/// Disable the SAU, making all memory Non-Secure accessible. +/// +/// # Safety +/// Must be called from the Secure world. After disabling, all memory is Non-Secure +/// accessible — call only if you intend to operate entirely in Non-Secure mode. +pub unsafe fn disable() { + let core = unsafe { cortex_m::Peripherals::steal() }; + // ALLNS=0, ENABLE=0: SAU disabled, all memory is Secure. + unsafe { core.SAU.ctrl.write(cortex_m::peripheral::sau::Ctrl(0)) }; +} diff --git a/examples/stm32wba6/src/bin/trustzone_setup.rs b/examples/stm32wba6/src/bin/trustzone_setup.rs new file mode 100644 index 0000000000..4bc62d48fe --- /dev/null +++ b/examples/stm32wba6/src/bin/trustzone_setup.rs @@ -0,0 +1,119 @@ +//! TrustZone configuration example for STM32WBA65. +//! +//! This example shows how to use the GTZC and SAU drivers to configure TrustZone +//! security boundaries on the WBA65 family. It must run from the Secure world +//! (i.e., the linker script must place this code in Secure Flash). +//! +//! In a real dual-image firmware: +//! 1. This Secure application starts, configures SAU + GTZC, then jumps to the +//! Non-Secure application at a known Non-Secure Flash address. +//! 2. The Non-Secure application runs with restricted access as configured below. +//! +//! IMPORTANT: The addresses and sizes used here are illustrative. Adjust them to +//! match your actual memory layout and linker scripts. + +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_stm32::gtzc::{self, Mpcbb, Tzic}; +use embassy_stm32::{Config, pac}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let config = Config::default(); + let _p = embassy_stm32::init(config); + + info!("TrustZone setup example (Secure world)"); + + // ── 1. Configure the SAU ──────────────────────────────────────────────── + // + // Define which memory regions are accessible from Non-Secure code. + // Memory NOT covered by any SAU region is Secure by default. + // + // STM32WBA65RI flash layout (example; adjust to your linker scripts): + // Secure Flash : 0x0800_0000 – 0x0807_FFFF (512 KiB) + // Non-Secure Flash: 0x0808_0000 – 0x080F_FFFF (512 KiB) + // NSC veneer : 0x0C07_F000 – 0x0C07_FFFF (4 KiB, in Secure alias) + // + // SRAM layout: + // Secure SRAM1 : first 448 KiB → blocks 0-27 of MPCBB1 + // Non-Secure SRAM2: 0x2007_0000 – 0x2007_FFFF (64 KiB) + + let sau_regions = [ + embassy_stm32::sau::Region { + base_address: 0x0808_0000, // Non-Secure Flash + end_address: 0x080F_FFFF, + attribute: embassy_stm32::sau::Attribute::NonSecure, + }, + embassy_stm32::sau::Region { + base_address: 0x2007_0000, // Non-Secure SRAM2 + end_address: 0x2007_FFFF, + attribute: embassy_stm32::sau::Attribute::NonSecure, + }, + embassy_stm32::sau::Region { + base_address: 0x0C07_F000, // Non-Secure Callable veneer + end_address: 0x0C07_FFFF, + attribute: embassy_stm32::sau::Attribute::NonSecureCallable, + }, + ]; + + unsafe { + embassy_stm32::sau::init(&sau_regions); + } + info!("SAU configured: {} regions", sau_regions.len()); + + // ── 2. Configure MPCBB1 (SRAM1 = 448 KiB = 28 × 16 KiB groups) ──────── + // + // WBA65RI SRAM1 is 448 KiB → 28 MPCBB registers (each covers 32 × 512 B = 16 KiB). + // Keep all SRAM1 blocks Secure. + let mpcbb1 = unsafe { Mpcbb::new(pac::GTZC_MPCBB1) }; + mpcbb1.set_all_secure(/*n_regs=*/ 14, true); + mpcbb1.set_all_privileged(/*n_regs=*/ 14, false); + info!("MPCBB1 (SRAM1): all blocks Secure"); + + // ── 3. Configure MPCBB2 (SRAM2 = 64 KiB = 4 × 16 KiB groups) ────────── + // + // Make SRAM2 fully Non-Secure so the Non-Secure application can use it. + let mpcbb2 = unsafe { Mpcbb::new(pac::GTZC_MPCBB2) }; + mpcbb2.set_all_secure(/*n_regs=*/ 4, false); + mpcbb2.set_all_privileged(/*n_regs=*/ 4, false); + info!("MPCBB2 (SRAM2): all blocks Non-Secure"); + + // ── 4. Configure TZSC peripheral security ─────────────────────────────── + // + // Mark USART1 as Non-Secure so the NS application can use it for logging. + // Other peripherals remain Secure (PAC::GTZC_TZSC field names are WBA-specific). + pac::GTZC_TZSC.tzsc_seccfgr2().modify(|r| r.set_usart1sec(false)); + info!("TZSC: USART1 → Non-Secure"); + + // ── 5. Enable TZIC (illegal access interrupts) ────────────────────────── + // + // WBA65RI has 4 TZIC register groups. + let tzic = unsafe { Tzic::new(pac::GTZC_TZIC) }; + tzic.enable_all(4); + info!("TZIC: enabled for all 4 register groups"); + + // ── 6. Lock the TZSC configuration ────────────────────────────────────── + // + // After locking, neither Secure nor Non-Secure code can modify the TZSC + // security/privilege registers until the next reset. + unsafe { gtzc::lock() }; + info!("TZSC locked. is_locked = {}", gtzc::is_locked()); + + // In a real application you would now jump to the Non-Secure application: + // let ns_entry = 0x0808_0000 as *const u32; + // let ns_vtor = *ns_entry as *const u32; + // let ns_sp = *ns_vtor; + // let ns_reset = *ns_vtor.add(1); + // // Set NS VTOR, MSP_NS, then BLX ns_reset + + info!("TrustZone setup complete — would jump to NS app here."); + + loop { + Timer::after_secs(1).await; + } +} From 94d5afffc88df23b282c09af89449f7f129ed665 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Mon, 20 Apr 2026 19:39:09 -0700 Subject: [PATCH 2/2] stm32: extend SAU/GTZC TrustZone drivers with IRQ routing, FPU, and NS boot - sau: add route_irq_to_nonsecure, enable_nonsecure_fpu, jump_to_nonsecure Raw register access used for NVIC ITNS (0xE000_E380) and SCB NSACR (0xE000_ED8C) pending cortex-m PR #647; TODO comments reference the PR. Fix disable() to set ALLNS=1 (was incorrectly writing 0). Fix doc example addresses to match WBA65RI memory map. - gtzc: add enable_clock, MPCBB superblock locking, srwiladis, invsecstate - trustzone_setup: rewrite example with correct WBA65RI addresses and all steps - Cargo.toml: patch cortex-m to leftger/cortex-m feat/trustzone for FPU.fpccr --- embassy-stm32/src/gtzc/mod.rs | 107 +++++++++++++- embassy-stm32/src/sau/mod.rs | 115 +++++++++++++-- examples/stm32wba6/src/bin/trustzone_setup.rs | 136 +++++++++++++----- 3 files changed, 308 insertions(+), 50 deletions(-) diff --git a/embassy-stm32/src/gtzc/mod.rs b/embassy-stm32/src/gtzc/mod.rs index e95385f4e1..e86584f8ea 100644 --- a/embassy-stm32/src/gtzc/mod.rs +++ b/embassy-stm32/src/gtzc/mod.rs @@ -17,11 +17,11 @@ //! | `gtzc_wba` | WBA | Full | Yes | Yes | No | //! | `gtzc_v1` | U5 / H5 | Full | Yes | Yes | Yes | //! | `gtzc_h503` | H503 | PrivOnly| Yes | No | BKPSRAM| -//! | `gtzc_l5` | L5 | Full | Yes | Yes | No | //! //! # Usage //! -//! All GTZC configuration must be performed from the Secure world. After setup, call +//! All GTZC configuration must be performed from the Secure world. **Enable the GTZC +//! clock first** via [`enable_clock()`], then configure MPCBB/TZIC, and finally call //! [`lock()`] to prevent Non-Secure code from modifying the configuration. //! //! MPCBB and TZIC instances are accessed via chip-specific PAC constants @@ -31,6 +31,9 @@ //! ```no_run //! use embassy_stm32::{gtzc, pac}; //! +//! // Enable GTZC clock before any register access. +//! unsafe { gtzc::enable_clock(); } +//! //! // Configure SRAM1 blocks as Non-Secure (wba or v1) //! let mpcbb1 = unsafe { gtzc::Mpcbb::new(pac::GTZC_MPCBB1) }; //! mpcbb1.set_all_secure(/*n_regs=*/14, false); // SRAM1 = 28 blocks × 512 B on WBA65 @@ -46,6 +49,36 @@ #[cfg(any(gtzc_wba, gtzc_v1))] use crate::pac; +// ──────────────────────────────────────────────────────────────────────────── +// Clock enable +// ──────────────────────────────────────────────────────────────────────────── + +/// Enable the GTZC peripheral clock. +/// +/// Must be called before accessing any GTZC register (TZSC, MPCBB, TZIC). +/// Failing to enable the clock causes hard faults on GTZC register reads/writes. +/// +/// # Safety +/// Must be called from the Secure world. +// WBA and U5 use `gtzc1en`; H5 (rcc_h5) names the same bit `tzsc1en`. +// H503 (rcc_h50) has no software clock gate for GTZC — always-on. +#[cfg(any(gtzc_wba, all(gtzc_v1, not(stm32h5))))] +pub unsafe fn enable_clock() { + pac::RCC.ahb1enr().modify(|r| r.set_gtzc1en(true)); +} + +/// Enable the GTZC peripheral clock. +/// +/// Must be called before accessing any GTZC register (TZSC, MPCBB, TZIC). +/// Failing to enable the clock causes hard faults on GTZC register reads/writes. +/// +/// # Safety +/// Must be called from the Secure world. +#[cfg(all(gtzc_v1, stm32h5))] +pub unsafe fn enable_clock() { + pac::RCC.ahb1enr().modify(|r| r.set_tzsc1en(true)); +} + // ──────────────────────────────────────────────────────────────────────────── // MPCBB — Memory Protection Controller Block-Based // ──────────────────────────────────────────────────────────────────────────── @@ -54,6 +87,8 @@ use crate::pac; /// /// Controls security and privilege attributes of individual 512-byte SRAM blocks. /// Each `seccfgr` / `privcfgr` register covers 32 contiguous blocks (= 16 KiB). +/// Every 32 blocks form one **superblock** that can be independently locked via +/// [`lock_superblock`]. /// /// Obtain a wrapper via [`Mpcbb::new`] with the chip-specific PAC constant for /// the desired SRAM, e.g., `pac::GTZC_MPCBB1`, `pac::GTZC_MPCBB2`, etc. @@ -89,6 +124,69 @@ impl Mpcbb { self.inner.cr().read().glock() } + /// Lock a single superblock (32 blocks = 16 KiB). + /// + /// `superblock` is a 0-based superblock index within this MPCBB. Each call locks + /// one bit in the CFGLOCK register; the lock is cleared only by hardware reset. + /// + /// Note: each bit in CFGLOCK corresponds to one 16 KiB superblock. On WBA, the + /// CFGLOCK register holds all superblock lock bits as a packed bitmask. + #[inline] + pub fn lock_superblock(&self, superblock: usize) { + self.inner + .cfglock() + .modify(|r| r.set_splck(r.splck() | (1u32 << superblock))); + } + + /// Lock the superblocks indicated by `mask`. + /// + /// Each bit `n` in `mask` corresponds to superblock `n`. Bits already set are + /// unaffected (lock is one-way). + #[inline] + pub fn lock_superblocks(&self, mask: u32) { + self.inner.cfglock().modify(|r| r.set_splck(r.splck() | mask)); + } + + /// Returns the current superblock lock bitmask from CFGLOCK. + #[inline] + pub fn superblock_lock_mask(&self) -> u32 { + self.inner.cfglock().read().splck() + } + + /// Configure the Secure Read/Write Illegal Access Disable (SRWILADIS) bit. + /// + /// When `true`, illegal accesses **from the Secure world** to Secure MPCBB blocks + /// do **not** generate a TZIC interrupt or flag. This is useful to suppress + /// spurious Secure-world self-access violations during initialization. + /// + /// Default after reset: `false` (all illegal accesses generate TZIC events). + #[inline] + pub fn set_srwiladis(&self, disabled: bool) { + self.inner.cr().modify(|r| r.set_srwiladis(disabled)); + } + + /// Returns the current SRWILADIS setting. + #[inline] + pub fn srwiladis(&self) -> bool { + self.inner.cr().read().srwiladis() + } + + /// Configure the Inverted Security State (INVSECSTATE) bit. + /// + /// When `true`, the security polarity of all blocks in this MPCBB is inverted: + /// a `seccfgr` bit of `0` means Secure, and `1` means Non-Secure. This is + /// an advanced option that is rarely needed; leave `false` for normal operation. + #[inline] + pub fn set_invsecstate(&self, inverted: bool) { + self.inner.cr().modify(|r| r.set_invsecstate(inverted)); + } + + /// Returns the current INVSECSTATE setting. + #[inline] + pub fn invsecstate(&self) -> bool { + self.inner.cr().read().invsecstate() + } + /// Set the security attribute of a single 512-byte block. /// /// `block_idx` is a 0-based index into this SRAM region. @@ -156,8 +254,9 @@ impl Mpcbb { /// /// `n_regs` is the number of 32-block registers that cover this SRAM. /// Calculate as `sram_size_bytes.div_ceil(16 * 1024)`. For example: - /// - 448 KiB SRAM (WBA65 SRAM1) → `n_regs = 14` - /// - 64 KiB SRAM (WBA65 SRAM2) → `n_regs = 4` + /// - 448 KiB SRAM (WBA65 SRAM1) → `n_regs = 28` + /// - 64 KiB SRAM (WBA65 SRAM2) → `n_regs = 4` + /// - 16 KiB SRAM (WBA65 SRAM6) → `n_regs = 1` pub fn set_all_secure(&self, n_regs: usize, secure: bool) { let bits = if secure { 0xFFFF_FFFF } else { 0 }; for n in 0..n_regs { diff --git a/embassy-stm32/src/sau/mod.rs b/embassy-stm32/src/sau/mod.rs index f766b76d20..6b2b7dd16e 100644 --- a/embassy-stm32/src/sau/mod.rs +++ b/embassy-stm32/src/sau/mod.rs @@ -14,8 +14,8 @@ //! //! let regions = [ //! Region { -//! base_address: 0x0808_0000, // Non-Secure Flash (must be 32-byte aligned) -//! end_address: 0x080F_FFFF, // Inclusive end (lower 5 bits must be 0x1F) +//! base_address: 0x0810_0000, // Non-Secure Flash (must be 32-byte aligned) +//! end_address: 0x081F_FFFF, // Inclusive end (lower 5 bits must be 0x1F) //! attribute: Attribute::NonSecure, //! }, //! Region { @@ -24,8 +24,8 @@ //! attribute: Attribute::NonSecure, //! }, //! Region { -//! base_address: 0x0C07_F000, // Non-Secure Callable veneer region -//! end_address: 0x0C07_FFFF, +//! base_address: 0x0C0F_E000, // Non-Secure Callable veneer region +//! end_address: 0x0C0F_FFFF, //! attribute: Attribute::NonSecureCallable, //! }, //! ]; @@ -53,7 +53,7 @@ pub enum Attribute { /// - `base_address & 0x1F == 0` (lower 5 bits must be zero) /// - `end_address & 0x1F == 0x1F` (lower 5 bits must be one) /// -/// For example, to cover 0x0808_0000–0x080F_FFFF both constraints are naturally satisfied. +/// For example, to cover 0x0810_0000–0x081F_FFFF both constraints are naturally satisfied. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Region { /// Base address of the region (inclusive). Must be 32-byte aligned. @@ -102,11 +102,108 @@ pub unsafe fn init(regions: &[Region]) { /// Disable the SAU, making all memory Non-Secure accessible. /// +/// Sets SAU CTRL.ALLNS=1, ENABLE=0: the SAU is off and all memory is treated as +/// Non-Secure (unless overridden by the IDAU). Use this only when you intend to +/// run entirely in Non-Secure mode with no security boundary enforcement. +/// /// # Safety -/// Must be called from the Secure world. After disabling, all memory is Non-Secure -/// accessible — call only if you intend to operate entirely in Non-Secure mode. +/// Must be called from the Secure world. pub unsafe fn disable() { let core = unsafe { cortex_m::Peripherals::steal() }; - // ALLNS=0, ENABLE=0: SAU disabled, all memory is Secure. - unsafe { core.SAU.ctrl.write(cortex_m::peripheral::sau::Ctrl(0)) }; + // ALLNS=1 (bit 1), ENABLE=0 (bit 0): SAU disabled, all memory Non-Secure. + unsafe { core.SAU.ctrl.write(cortex_m::peripheral::sau::Ctrl(0b10)) }; +} + +/// Route an interrupt to the Non-Secure world via the NVIC ITNS registers. +/// +/// By default all interrupts target the Secure world. Call this for every interrupt +/// that the Non-Secure application handles (e.g. USART1, TIM2, DMA channels). +/// +/// `irq_number` is the peripheral interrupt number as defined in your device's +/// interrupt vector table (0-based, excluding the 16 CPU exceptions). +/// +/// # Safety +/// Must be called from the Secure world before jumping to Non-Secure code. +pub unsafe fn route_irq_to_nonsecure(irq_number: u16) { + // TODO: replace with NVIC::route_to_nonsecure() once cortex-m PR #647 lands + // (https://github.com/rust-embedded/cortex-m/pull/647). + // + // NVIC ITNS registers start at 0xE000_E380 (ARMv8-M only). + // itns[n] covers interrupts [n*32 .. n*32+31]; setting bit k routes interrupt + // (n*32 + k) to Non-Secure world. + const NVIC_ITNS_BASE: *mut u32 = 0xE000_E380 as *mut u32; + let reg = usize::from(irq_number / 32); + let bit = irq_number as u32 % 32; + unsafe { + let ptr = NVIC_ITNS_BASE.add(reg); + ptr.write_volatile(ptr.read_volatile() | (1 << bit)); + } +} + +/// Enable FPU access from the Non-Secure world. +/// +/// Sets SCB->NSACR bits 10–11 (CP10/CP11) so Non-Secure code can use the FPU. +/// Also clears the FPCCR.TS bit so that lazy FP state preservation does not +/// treat FP registers as Secure (prevents accidental FP-register leakage across +/// the security boundary). +/// +/// Must be called before jumping to Non-Secure code if the NS application uses +/// floating-point operations. Without this, NS FPU use will raise a UsageFault. +/// +/// # Safety +/// Must be called from the Secure world. +pub unsafe fn enable_nonsecure_fpu() { + // TODO: replace raw SCB NSACR write with SCB::enable_nonsecure_fpu() once + // cortex-m PR #647 lands (https://github.com/rust-embedded/cortex-m/pull/647). + // + // SCB NSACR is at 0xE000_ED8C. Bits 10-11 (CP10/CP11) grant NS access to the FPU. + const SCB_NSACR: *mut u32 = 0xE000_ED8C as *mut u32; + const CP10_CP11: u32 = 0b11 << 10; + unsafe { SCB_NSACR.write_volatile(SCB_NSACR.read_volatile() | CP10_CP11) }; + + // FPCCR.TS (bit 26) = 0 means FP state is Non-Secure, preventing Secure FP register + // contents from leaking to NS code on context switches. + let core = cortex_m::Peripherals::steal(); + core.FPU.fpccr.modify(|v| v & !(1 << 26)); +} + +/// Transfer control to the Non-Secure application. Does not return. +/// +/// This performs the standard Secure→Non-Secure boot handoff: +/// 1. Sets `SCB_NS->VTOR` to `ns_vtor` so the NS world knows its vector table. +/// 2. Loads `MSP_NS` from the first word of the NS vector table (initial NS stack pointer). +/// 3. Reads the NS reset handler address from the second word of the vector table. +/// 4. Executes `BXNS` to atomically switch to Non-Secure state and jump to the handler. +/// +/// # Safety +/// - Must be called from the Secure world after all GTZC/SAU setup is complete. +/// - `ns_vtor` must be a valid Non-Secure vector table address (32-byte aligned per +/// Cortex-M33 requirements; in practice 64-byte or 128-byte alignment is typical). +/// - The Non-Secure reset handler at `*(ns_vtor + 4)` must be a valid Thumb function +/// address (i.e., bit 0 set in the vector table entry, as per ARM ABI convention). +pub unsafe fn jump_to_nonsecure(ns_vtor: u32) -> ! { + // Configure the Non-Secure vector table. + // SCB_NS->VTOR is the NS alias of the SCB VTOR register (0xE002_ED08). + const SCB_NS_VTOR: *mut u32 = 0xE002_ED08 as *mut u32; + SCB_NS_VTOR.write_volatile(ns_vtor); + + // Load initial NS stack pointer from the first word of the NS vector table. + let ns_sp = core::ptr::read_volatile(ns_vtor as *const u32); + core::arch::asm!( + "msr msp_ns, {sp}", + sp = in(reg) ns_sp, + options(nomem, nostack, preserves_flags), + ); + + // Read the NS reset handler address from the second word of the NS vector table. + // ARM convention: bit 0 = 1 in the vector table (Thumb mode marker). + // BXNS requires bit 0 = 0 or it raises SecureFault (SFSR.INVTRAN). + let ns_reset = core::ptr::read_volatile((ns_vtor as *const u32).add(1)); + + // BXNS atomically clears bit 0, switches to Non-Secure state, and jumps. + core::arch::asm!( + "bxns {entry}", + entry = in(reg) ns_reset & !1u32, + options(noreturn), + ); } diff --git a/examples/stm32wba6/src/bin/trustzone_setup.rs b/examples/stm32wba6/src/bin/trustzone_setup.rs index 4bc62d48fe..84530844bc 100644 --- a/examples/stm32wba6/src/bin/trustzone_setup.rs +++ b/examples/stm32wba6/src/bin/trustzone_setup.rs @@ -9,8 +9,20 @@ //! Non-Secure application at a known Non-Secure Flash address. //! 2. The Non-Secure application runs with restricted access as configured below. //! -//! IMPORTANT: The addresses and sizes used here are illustrative. Adjust them to -//! match your actual memory layout and linker scripts. +//! # WBA65RI Memory Layout +//! +//! Flash (2 MiB total, 0x0800_0000–0x081F_FFFF): +//! Secure Flash : 0x0800_0000 – 0x080F_FFFF (1 MiB) +//! Non-Secure Flash: 0x0810_0000 – 0x081F_FFFF (1 MiB) +//! NSC veneer alias: 0x0C0F_E000 – 0x0C0F_FFFF (8 KiB, in Secure alias space) +//! OTP/Info pages : 0x0BF9_0000 – 0x0BFB_7FFF (non-secure) +//! +//! SRAM: +//! SRAM1 (Secure) : 0x2000_0000 – 0x206F_FFFF (448 KiB, MPCBB1 28 regs) +//! SRAM2 (NS) : 0x2007_0000 – 0x2007_FFFF (64 KiB, MPCBB2 4 regs) +//! SRAM6 (Radio) : 0x4802_8000 – 0x4802_BFFF (16 KiB, MPCBB6 1 reg) +//! +//! Peripherals (Non-Secure): 0x4000_0000 – 0x4FFF_FFFF #![no_std] #![no_main] @@ -22,6 +34,9 @@ use embassy_stm32::{Config, pac}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; +/// Non-Secure application entry point address (start of NS Flash on WBA65RI). +const NS_VTOR: u32 = 0x0810_0000; + #[embassy_executor::main] async fn main(_spawner: Spawner) { let config = Config::default(); @@ -29,36 +44,56 @@ async fn main(_spawner: Spawner) { info!("TrustZone setup example (Secure world)"); + // ── 0. Enable GTZC clock ──────────────────────────────────────────────── + // + // GTZC registers are behind the AHB1 bus clock; accessing them without + // enabling the clock causes a hard fault. + unsafe { gtzc::enable_clock() }; + info!("GTZC clock enabled"); + // ── 1. Configure the SAU ──────────────────────────────────────────────── // // Define which memory regions are accessible from Non-Secure code. - // Memory NOT covered by any SAU region is Secure by default. - // - // STM32WBA65RI flash layout (example; adjust to your linker scripts): - // Secure Flash : 0x0800_0000 – 0x0807_FFFF (512 KiB) - // Non-Secure Flash: 0x0808_0000 – 0x080F_FFFF (512 KiB) - // NSC veneer : 0x0C07_F000 – 0x0C07_FFFF (4 KiB, in Secure alias) - // - // SRAM layout: - // Secure SRAM1 : first 448 KiB → blocks 0-27 of MPCBB1 - // Non-Secure SRAM2: 0x2007_0000 – 0x2007_FFFF (64 KiB) + // Memory NOT covered by any SAU region is Secure by default (when SAU is + // enabled). Five regions are configured here following the ST reference + // partition for WBA65: + // 0 – Non-Secure Flash + // 1 – OTP / Information pages (NS) + // 2 – Non-Secure Callable veneer (Secure alias, NSC) + // 3 – Non-Secure SRAM2 + // 4 – All peripherals (NS, so the NS app can use them) let sau_regions = [ + // Region 0: Non-Secure Flash (1 MiB, upper half of WBA65RI 2 MiB flash) embassy_stm32::sau::Region { - base_address: 0x0808_0000, // Non-Secure Flash - end_address: 0x080F_FFFF, + base_address: 0x0810_0000, + end_address: 0x081F_FFFF, attribute: embassy_stm32::sau::Attribute::NonSecure, }, + // Region 1: OTP / option bytes in NS — required for NS read of device info embassy_stm32::sau::Region { - base_address: 0x2007_0000, // Non-Secure SRAM2 - end_address: 0x2007_FFFF, + base_address: 0x0BF9_0000, + end_address: 0x0BFB_7FFF, attribute: embassy_stm32::sau::Attribute::NonSecure, }, + // Region 2: Non-Secure Callable veneer — Secure code callable from NS world embassy_stm32::sau::Region { - base_address: 0x0C07_F000, // Non-Secure Callable veneer - end_address: 0x0C07_FFFF, + base_address: 0x0C0F_E000, + end_address: 0x0C0F_FFFF, attribute: embassy_stm32::sau::Attribute::NonSecureCallable, }, + // Region 3: Non-Secure SRAM2 (64 KiB) + embassy_stm32::sau::Region { + base_address: 0x2007_0000, + end_address: 0x2007_FFFF, + attribute: embassy_stm32::sau::Attribute::NonSecure, + }, + // Region 4: All peripherals — NS app must access its peripherals + embassy_stm32::sau::Region { + base_address: 0x4000_0000, + end_address: 0x4FFF_FFFF, + attribute: embassy_stm32::sau::Attribute::NonSecure, + }, ]; unsafe { @@ -68,12 +103,13 @@ async fn main(_spawner: Spawner) { // ── 2. Configure MPCBB1 (SRAM1 = 448 KiB = 28 × 16 KiB groups) ──────── // - // WBA65RI SRAM1 is 448 KiB → 28 MPCBB registers (each covers 32 × 512 B = 16 KiB). - // Keep all SRAM1 blocks Secure. + // Keep all SRAM1 blocks Secure so NS code cannot read Secure data. let mpcbb1 = unsafe { Mpcbb::new(pac::GTZC_MPCBB1) }; - mpcbb1.set_all_secure(/*n_regs=*/ 14, true); - mpcbb1.set_all_privileged(/*n_regs=*/ 14, false); - info!("MPCBB1 (SRAM1): all blocks Secure"); + mpcbb1.set_all_secure(/*n_regs=*/ 28, true); + mpcbb1.set_all_privileged(/*n_regs=*/ 28, false); + // Suppress TZIC interrupts for Secure-world self-accesses to Secure blocks. + mpcbb1.set_srwiladis(true); + info!("MPCBB1 (SRAM1, 448 KiB): all blocks Secure"); // ── 3. Configure MPCBB2 (SRAM2 = 64 KiB = 4 × 16 KiB groups) ────────── // @@ -81,37 +117,63 @@ async fn main(_spawner: Spawner) { let mpcbb2 = unsafe { Mpcbb::new(pac::GTZC_MPCBB2) }; mpcbb2.set_all_secure(/*n_regs=*/ 4, false); mpcbb2.set_all_privileged(/*n_regs=*/ 4, false); - info!("MPCBB2 (SRAM2): all blocks Non-Secure"); + info!("MPCBB2 (SRAM2, 64 KiB): all blocks Non-Secure"); - // ── 4. Configure TZSC peripheral security ─────────────────────────────── + // ── 4. Configure MPCBB6 (Radio SRAM = 16 KiB = 1 × 16 KiB group) ────── // - // Mark USART1 as Non-Secure so the NS application can use it for logging. - // Other peripherals remain Secure (PAC::GTZC_TZSC field names are WBA-specific). + // SRAM6 is the 2.4 GHz radio TX/RX buffer at 0x4802_8000. + // Keep it Secure so NS code cannot tamper with radio payloads. + let mpcbb6 = unsafe { Mpcbb::new(pac::GTZC_MPCBB6) }; + mpcbb6.set_all_secure(/*n_regs=*/ 1, true); + mpcbb6.set_all_privileged(/*n_regs=*/ 1, false); + info!("MPCBB6 (SRAM6 radio, 16 KiB): all blocks Secure"); + + // ── 5. Configure TZSC peripheral security ─────────────────────────────── + // + // Mark specific peripherals as Non-Secure so the NS application can use them. + // PAC field names are WBA-specific (gtzc_wba variant). pac::GTZC_TZSC.tzsc_seccfgr2().modify(|r| r.set_usart1sec(false)); info!("TZSC: USART1 → Non-Secure"); - // ── 5. Enable TZIC (illegal access interrupts) ────────────────────────── + // ── 6. Enable TZIC (illegal access interrupts) ────────────────────────── // - // WBA65RI has 4 TZIC register groups. + // WBA65RI has 4 TZIC register groups. Clear any stale flags first, then + // enable IRQs so violations are reported via the GTZC interrupt. let tzic = unsafe { Tzic::new(pac::GTZC_TZIC) }; tzic.enable_all(4); info!("TZIC: enabled for all 4 register groups"); - // ── 6. Lock the TZSC configuration ────────────────────────────────────── + // ── 7. Lock the TZSC configuration ────────────────────────────────────── // // After locking, neither Secure nor Non-Secure code can modify the TZSC // security/privilege registers until the next reset. unsafe { gtzc::lock() }; info!("TZSC locked. is_locked = {}", gtzc::is_locked()); - // In a real application you would now jump to the Non-Secure application: - // let ns_entry = 0x0808_0000 as *const u32; - // let ns_vtor = *ns_entry as *const u32; - // let ns_sp = *ns_vtor; - // let ns_reset = *ns_vtor.add(1); - // // Set NS VTOR, MSP_NS, then BLX ns_reset + // ── 8. Route selected interrupts to the Non-Secure world ──────────────── + // + // By default all interrupts target Secure world. Route the interrupts that + // the NS application handles. IRQ numbers come from the device interrupt + // vector table (see stm32wba65ri PAC or reference manual). + // + // Example: route USART1 global interrupt (IRQ #37 on WBA65) to NS. + // unsafe { embassy_stm32::sau::route_irq_to_nonsecure(37); } + + // ── 9. Enable FPU access from Non-Secure world ────────────────────────── + // + // Allow NS code to use the Cortex-M33 FPU. Without this, any NS floating- + // point operation raises a UsageFault. + unsafe { embassy_stm32::sau::enable_nonsecure_fpu() }; + info!("FPU enabled for Non-Secure world"); + + // ── 10. Jump to the Non-Secure application ────────────────────────────── + // + // In a real dual-image build, uncomment the line below. The NS app must be + // linked to start at NS_VTOR (0x0810_0000) with a valid vector table. + // + // unsafe { embassy_stm32::sau::jump_to_nonsecure(NS_VTOR) }; - info!("TrustZone setup complete — would jump to NS app here."); + info!("TrustZone setup complete — would jump to NS app at {:#010x}.", NS_VTOR); loop { Timer::after_secs(1).await;