
不推荐用“能不能”来问这个问题因为它会给出一个让你误以为“这就完了”的答案。先说结论在 STM32H563 上TrustZone 开启之后从 non-secure 代码触发 Bank Swap硬件层面确实可行但你的产品能不能这么做、应不应该这么做完全取决于安全属性怎么配、切换目标 Bank 是否已经通过校验、以及你在安全启动策略里是否愿意放权给 non-secure 侧。这句结论听着像废话但实际调试中绝大多数人卡住的点不在“怎么写切换代码”而在“为什么我的 non-secure 代码压根碰不到 Flash 控制器”。这篇文章就不绕弯子了直接从 STM32H563 的 TrustZone 与双 Bank Flash 的配合机制讲起然后给你两条可落地的实现路径再把我踩过的坑和产品级 OTA 的安全切换方案一起列出来。适合正在做 H5 系列 OTA 或者准备把 Bootloader 放到 non-secure 侧的工程师参考。1. 先给结论能切但没你想的那么“自由”如果只回答标题的问题那答案就是能从 non-secure 代码物理上可以触发 Bank Swap但有三个前提条件。第一安全侧必须把 Flash 外设的安全属性开放给 non-secure 侧。如果 TZSC 里 Flash 外设还保持默认的 secure 属性那你从 non-secure 代码访问 FLASH 寄存器会在总线互连层就被拦下来。具体的表现有两种轻则读回全是 0重则直接进 HardFault 或者触发 Security Violation 错误。第二切换后的目标 Bank 里必须放着一套能通过安全侧校验的固件。这不是说“目标 Bank 烧了程序就行”而是指安全启动流程会检查安全镜像、版本号、签名甚至检查安全镜像与非安全镜像是不是配套的一对。你从 non-secure 侧直接切绕过了安全侧的“同意”流程目标 Bank 里万一放着一个旧版本或者不完整的安全镜像那启动阶段就会判断为非法切换设备会直接卡死在安全启动状态里。第三你的安全启动策略要能容忍 non-secure 代码拥有 Flash 控制器的控制权。这里说的不只是一个简单的“能不能切”而是包括读保护、写保护、选项字节编程等一连串敏感操作。如果你把这些全部向 non-secure 开放等于把系统安全的最后一道锁也交出去了。所以完整答案是硬件上允许安全策略上不一定允许产品设计上大多数时候不应该让 non-secure 直接切。这是我在多款 H5 项目里反复验证过的实践结论不是理论推导。2. Bank Swap 到底在切换什么不是内存搬家聊这个问题的前提是把 Bank Swap 的硬件机制理解透彻。很多应用层工程师第一次接触双 Bank Flash会以为它像 EEPROM 换页一样把两个 Bank 的数据整体搬移。实际上完全不是这样。H563 的 2MB Flash 分成两个独立 BankBank 1 的起始地址是 0x08000000Bank 2 的起始地址是 0x08100000。系统复位后默认从 0x08000000 取向量表也就是 Bank 1 作为启动 Bank。执行一次 Bank Swap 之后两个 Bank 在地址空间上的映射关系被交换原来映射到 0x08000000 的 Bank 1会被换到 0x08100000而 Bank 2 则映射到 0x08000000。CPU 复位后还是从 0x08000000 开始跑但物理上访问的已经是 Bank 2 了。整个过程不改动 Flash 里的任何数据只改“谁来担任启动 Bank”的映射开关。这个机制对 OTA 非常关键。你可以先用当前固件把新固件写入非启动 Bank写完之后触发一次 Bank Swap复位后系统就跑新固件了。因为写入过程发生在旧 Bank 之外擦写操作不会影响正在执行的代码天然规避了“程序跑着跑着把自己 Flash 区域擦掉了”的经典问题。触发 Bank Swap 的硬件操作大致是这样等 Flash 控制器空闲设置 Bank Swap 请求位再触发交换操作等待交换完成最后系统复位使新映射生效。不同系列寄存器命名略有差异但流程一致我用 STM32CubeH5 库时一般直接封装成 HAL_FLASHEx_SwapBank() 这类接口。这里有好几个工程师容易忽略的点我一个个说。2.1 交换生效需要复位热切换不现实Bank Swap 不是函数跳转不能像调用一个函数那样在运行时“热切换”。设置交换位之后当前代码仍然从旧地址执行只有等下一次系统复位完成后新的 Bank 映射才会被 Boot ROM 锁存。很多第一次做这块的同事会天真地以为设置完就能跳过去然后发现代码还停在原地接着开始怀疑寄存器没写进去。所以你在方案设计时一定要把“先写交换请求、然后复位”当成一个原子操作。而且要意识到复位后 Boot 代码的第一件事就是判断当前跑在哪个 Bank否则两个 Bank 里的代码会互相认为自己是启动方逻辑容易打结。一般做法是在一个固定的 RAM 地址或者备份寄存器里存一个启动标志Boot 阶段去读这个标志决定走哪条分支。2.2 交换不会自动做固件校验本质上Bank Swap 只是一个“地址映射重排”动作它不知道一个固件是好的还是坏的。当你切到新 Bank如果新 Bank 里的代码没有通过校验那系统就会启动失败。TrustZone 环境尤其严格安全侧启动会在 BootROM 阶段做一整套校验流程包括安全镜像的哈希、签名、版本号、回滚保护甚至检查非安全镜像与安全镜像是否匹配。校验不通过不是简单跳转到错误处理而是可能进入一个需要重新烧录才能恢复的状态。所以生产级 OTA 里Bank Swap 永远排在“镜像验证”之后。先验证再切换切换后还要再验证一遍这样才能保证设备不会因为一次半途而废的升级变成砖头。2.3 Flash 的双 Bank 划分与保护属性不是一层H563 的 Flash 在 TrustZone 开启后会进一步划分安全属性和非安全属性。也就是说两个 Bank 里都可以同时存在 Secure 区域和 Non-secure 区域它们之间有地址边界保护。设置 Bank Swap 之后这些属性定义会被重新映射到新的地址空间上。如果你的安全区域边界配置得不够清晰交换后可能出现“安全区域不完整”或者“非安全代码访问到安全区”的问题。这点在配置 Linker 脚本和 TZSC 时一定要提前规划好而不是在第一次交换失败后才开始排查。3. TrustZone 开启后Flash 控制器归谁管想回答“non-secure 能不能切”绕不开一个问题Flash 控制器这个外设在 TrustZone 开启后到底归哪个世界管ARMv8-M 的 TrustZone 不是一个简单的内存保护机制它是把整个系统划分成 Secure 和 Non-secure 两个平行世界。CPU 当前在哪个世界由执行状态决定地址空间里每一个区域的访问权限则由 SAU安全属性单元、IDAU实现定义安全属性单元和 TZSCTrustZone 安全控制器共同决定。CPU 发出的每次总线访问源是一种安全态目标地址又带一种安全属性两者不匹配就会被拦。STM32H563 的外设安全属性主要在 TZSC 里配置每个外设对应一个或多个安全配置位。你可以把某个外设整体配置为 Secure 或 Non-secure也可以通过额外机制配置它的中断、DMA 请求的安全属性。默认情况下Flash 控制器、RCC、PWR 这类基础外设往往被配置成 Secure毕竟它们是系统正常工作的命脉。所以如果你没有做任何配置就试图在 non-secure 代码里操作 FLASH 寄存器总线事务会带着 Non-secure 标签访问一个 Secure 外设。结果就是直接被拒绝。不是说你的代码写错了而是系统根本没放行。3.1 外设属性配置安全侧才能改把 Flash 外设从 Secure 改成 Non-secure这个动作只有安全侧代码能做。因为 TZSC 本身就是安全外设non-secure 代码没有权限碰它。所以就算你只想让 non-secure 直接操作 Flash也需要先有一段安全侧初始化代码把权限放开。这里要注意修改外设安全属性后最好做一次系统级的隔离刷新确保新的属性对后续总线访问完全生效。很多人的开发板上看似“什么都没配就能访问 Flash”是因为用了 ST 官方模板工程模板里已经把部分外设开放给了 non-secure。但你如果从零开始写 TrustZone 工程不加配置大概率会踩到总线访问被拦截的问题。3.2 开放外设不等于开放 Flash 存储区即使你把 Flash 控制器外设整体配置成 Non-secure 可访问也不意味着 non-se-secure 代码可以访问 Flash 里所有的地址。Flash 内部还有区域级的安全属性。假如某个 Flash 扇区被配置为 Secure而 non-secure 总线事务尝试读取或写入那个扇区Flash 控制器会把它标记为非法访问在 FLASH_SR 里置上错误位有一定概率触发更高优先级的安全错误中断。这个特性经常造成误解。你可能已经验证了“Flash 外设能访问了”但实际访问的是内存地址而不是外设寄存器地址。两者虽然同在 Flash 地址段但访问路径和保护逻辑不同。调试这种问题一定要把“Flash 外设寄存器”和“Flash 存储区”分开看待。3.3 一个容易被忽略的点非安全中断也会访问 FlashTrustZone 开启后中断也分成 Secure 和 Non-secure。non-secure 代码触发 Bank Swap 的过程里如果有一个非安全中断进来而中断服务程序里包含 Flash 读取操作就可能与正在进行的 Swap 操作产生竞争。我在调试时遇到过很典型的情况代码刚设置完交换位一个 SysTick 中断进来中断里碰巧访问了 FlashFlash 控制器还在忙碌状态于是触发 BSY 错误交换请求直接失败。处理方式不复杂在触发交换的临界区里关闭可屏蔽中断执行完交换请求和复位操作后再恢复中断。当然最稳妥的是直接触发复位让系统从头开始彻底避开中断残留问题。4. 从非安全代码触发 Bank Swap 的实现路径原理讲清楚了下面直接给可落地的路径。根据你的安全策略一般有两套方案非安全侧直接操作 Flash 控制器或者非安全侧调用安全侧提供的 Bank Swap 服务。这两条路我都实测过下面把细节讲透。4.1 路径1非安全侧直接操作 Flash 控制器这个方法最直观适合安全策略比较宽松的内部验证项目。第一步在安全侧初始化里把 FLASH 外设的安全属性修改为 Non-secure。一般是通过 TZSC 对应的外设安全配置位把它改成非安全属性。这一步做完non-secure 代码才能正常访问 FLASH 寄存器。第二步在 non-secure 侧实现交换逻辑。我常用的模板如下/* 非安全侧直接触发仅当 Flash 外设被配置为 Non-secure 且系统安全策略允许时 */ uint32_t primask __get_PRIMASK(); __disable_irq(); // 1. 关中断避免交换过程被打断 while (FLASH-SR FLASH_SR_BSY); // 2. 等 Flash 控制器空闲 FLASH-CR | FLASH_CR_BANK_SW; // 3. 设置 Bank Swap 请求位 FLASH-CR | FLASH_CR_START; // 4. 触发交换有些系列叫 SWAP_REQ while (FLASH-SR FLASH_SR_BSY); // 5. 等待交换完成 NVIC_SystemReset(); // 6. 复位让新映射生效用 STM32CubeH5 库的话里面的 HAL_FLASHEx_SwapBank() 会封装大部分步骤但通常不会帮你处理中断保护和复位外面还是要再套一层。这套方案有几个显而易见的优点代码量少、链路短、non-secure 侧完全自主。代价就是安全隐患明显。你把 Flash 控制器整体开放给了 non-secure那 non-secure 代码一旦被攻破攻击者可以直接操作 Flash 控制器有能力修改安全启动配置、擦除安全固件、改写保护位。这在安全认证导向的产品里基本是致命的。4.2 路径2非安全侧调用安全侧“切换服务”第二个方案更符合 TrustZone 的设计意图这也是我在量产项目里推荐的方式。敏感操作留在安全侧non-secure 侧只通过一个定义良好的 Non-secure CallableNSC接口发出请求。实现思路是安全侧将一个函数编译成 NSC 入口函数内部完成真正的 Bank Swap。ARMv8-M 的 Secure GatewaySG指令会负责从 non-secure 切到 secure 状态执行完再切回来。安全侧可以在函数里加各种检查比如验证升级标志、版本号、镜像哈希等再决定是否执行交换。安全侧代码大致长这样/* 安全侧放在 Non-secure Callable 区域 */ __attribute__((cmse_nonsecure_entry)) void secure_bank_swap(void) { /* 这里可以加校验升级标志、版本号、镜像状态检查 */ if (!is_upgrade_request_valid()) { return; } HAL_FLASHEx_SwapBank(); NVIC_SystemReset(); }non-secure 侧通过函数指针调用/* 非安全侧 */ typedef void (*bank_swap_fn)(void); bank_swap_fn do_swap (bank_swap_fn)SECURE_BANK_SWAP_ADDRESS; do_swap();这里最容易出错的是函数地址怎么拿到。NSC 函数地址不是随便从 RAM 里抄一个它必须位于 Non-secure Callable 区域内并且这个地址是在安全侧启动时被写入特定位置、然后通过合规的接口表传递给 non-secure 侧的。最常见的做法是安全侧在启动时把一组服务接口地址放在一个固定的 NSC 区域结构体里non-secure 侧通过查询这个结构体拿到函数指针。4.3 两种路径的对比与选型我一般用下面这个表格来快速判断选哪条路径判断维度直接非安全访问安全侧服务代理实现复杂度低几行寄存器操作中高需要 NSC 区域和接口设计安全风险高Flash 控制器整体开放低敏感操作仍然受控升级灵活性低non-secure 侧直接决定一切高安全侧可做策略校验适合场景内部验证、安全要求低的产品需要安全认证、远程升级的产品如果你的产品只是调试阶段验证功能直接用路径1跑通后对硬件行为建立直觉这是最高效的。但一旦进入量产阶段尤其是需要远程升级、防回滚、证书校验的产品先把路径1写进去再改路径2成本会比一开始就设计成路径2高得多。因为路径2需要安全侧提前规划 NSC 区域、接口地址、应用侧链接脚本这些在后期改起来牵一发动全身。5. 非安全侧直接切最容易踩的坑路径1虽然代码简单但坑一点都不少。我把自己在真实项目里踩过的、帮客户排查过的几个典型问题整理成下面几条。5.1 切换后安全镜像失效的连锁反应这是最隐蔽的坑。你可能已经确认 Bank 2 里烧了固件单独从 Bank 2 启动也能跑但切过去之后设备就是起不来。最后查下来发现TrustZone 环境里的启动不是简单的“找到向量表就跳”。BootROM 阶段会检查安全启动镜像的完整性、版本号、反回滚状态还会验证安全镜像和非安全镜像是不是配套的一对。如果你从 non-secure 侧直接切绕过了安全侧的“同意”流程但目标 Bank 里的安全镜像版本比当前 Bank 旧或者签名校验不过安全侧就会认为发生了非法切换进入安全失败状态。这个状态不是简单重新烧一版固件就能恢复的有些情况需要先擦除整个 Flash、清除保护标志再重新烧录。所以切 Bank 之前一定要先确认目标 Bank 的安全镜像和非安全镜像都通过了安全侧自己的校验流程而不是只看“有没有烧程序”。5.2 时序、复位与启动状态的边界第二个坑是把 Bank Swap 当成即时生效的跳转。前面强调了Swap 后要复位才能生效。但复位不是万能药一些项目在 non-secure 侧置位后立刻调 NVIC_SystemReset()结果发现启动后还是从旧 Bank 跑。排查下来通常是这几个原因叠加交换请求写了但没有等 Flash 控制器的忙标志清零复位前交换请求被丢弃。复位后 RCC 时钟配置回到默认值新固件如果依赖特定时钟配置可能在时钟初始化之前就卡死。交换过程中被中断打断Flash 控制器状态机没走完交换请求无效。我现在处理这类问题的方法是严格按“关中断 - 等 BSY 清零 - 设置交换位 - 等待交换完成 - 复位”的顺序执行在排查时还会在每一步翻转一个 GPIO用逻辑分析仪测量每一步的实际耗时很快能定位是哪一步没走完。5.3 中断、缓存与外设安全属性的连带问题第三个坑更细。如果你选择了路径2即安全侧代理服务那么 interrupt 的安全属性就特别重要。NSC 函数执行时中断可以在安全态或非安全态被触发。如果某个安全中断在 Bank Swap 过程中触发访问了 Flash 控制器而 Flash 控制器正忙就会产生 busy 错误。反过来如果你的 NVIC 里中断优先级分组配置不一致可能导致 switch 之后优先级翻转安全中断无法抢占系统行为异常。缓存方面H563 的 I-Cache/D-Cache 在 Bank Swap 后一般不会造成致命问题因为复位会清掉这些缓存和预取缓冲。但有一种做法很危险有人试图在 Bank Swap 后不复位直接跳转到新 Bank 的地址。这时候 CPU 还带着旧映射下的预取缓冲取指会错乱。再说一遍别试图绕过复位做“软切换”老老实实复位。另外一个连带问题是 DMA。假设你把 Flash 外设开放给了 non-secure但 DMA 控制器还是 secure 属性那么 non-secure 代码通过 DMA 访问 Flash 依然会被拒。TrustZone 的权限检查是逐层进行的任何一个环节的安全属性不匹配都会失败。因此配置的时候要把 Flash、DMA、中断控制器、相关时钟外设都查一遍。6. 产品级 OTA我建议的安全切换方案聊了这么多实验性的内容最后落到工程实践。如果你做的是一款要量产、要远程升级、要过安全认证的产品我的建议很直接不要放权给 non-secure 侧直接操作 Flash 控制器。安全侧应该掌握切换的最终决定权。6.1 为什么我不建议“裸奔”式非安全切换表面上看直接开放 Flash 外设给 non-secure 侧可以省掉 NSC 接口设计代码写起来也快。但代价是你把整个 Flash 控制器的控制权全部交给了 non-secure 固件。这等于告诉攻击者只要攻破你的应用程序就能随意摆弄 Flash 控制器包括改写保护位、擦除安全固件、篡改启动配置。这在 PSA、SESIP 这类安全认证里是过不去的在产品责任上也不可接受。远程升级场景尤其危险。攻击者一旦能控制 non-secure 固件他可以伪造一个升级包触发 Bank Swap然后通过一个未经验证的镜像把安全侧固件覆盖掉。这种攻击路径非常成熟安全设计不能只防“写错代码”的意外也要防“被攻击者利用”的恶意场景。6.2 推荐的双分区安全 OTA 流程我在这类产品上最终收敛得到一套流程简化为下面几步Boot 阶段安全侧运行极小、固定的 Boot 代码负责初始化 TrustZone划分 Secure/Non-secure 区域验证镜像然后跳转到 non-secure 应用。运行阶段non-secure 应用负责业务逻辑、网络下载、固件包落地。升级包下载到独立的存储区域后把版本号、校验值、升级标志写入安全侧共享的内存区域。升级请求non-secure 应用通过 NSC 接口向安全侧发起升级请求而不是直接写 Flash 寄存器。安全侧校验安全侧收到请求后做回滚保护检查、签名/哈希校验、目标 Bank 镜像存在性校验。全部通过才执行 Bank Swap 复位。启动校验复位后 Boot 再次执行安全校验这次校验的是新 Bank 的镜像。校验通过跳转到新应用校验失败可以回切旧 Bank。这套流程的好处是每个环节的安全责任归属非常清楚。non-secure 侧只负责业务和下载不掌握任何安全关键资源安全侧负责判断和切换即使判断失误最坏情况也就是升级失败回滚不会把安全固件暴露出去。6.3 关键配置与代码骨架如果你想在 H563 上实现这套方案配置上主要就几个关键点TZSC 里无需把 Flash 外设开放给 non-secure。Flash 控制器保持 secure只通过 NSC 接口暴露一个受控的 Bank Swap 服务。在 SAU/IDAU 里为 NSC 函数划分一块 Non-secure Callable 区域这段区域既能被 non-secure 代码调用又不会暴露安全内存。在 non-secure 工程里通过链接脚本把安全服务接口地址固定下来或者通过安全侧启动时发布的接口表动态获取。升级相关的共享变量版本号、升级标志、校验状态放在一块双方都可访问的共享内存区域最好是 SRAM 中标记为 Non-secure 的区域并保证数据一致性。代码骨架可以参考前面的 NSC 示例。安全侧只需要暴露一个函数比如 secure_request_ota_and_swap()内部完成校验和交换。non-secure 侧就不需要了解任何 Flash 寄存器的细节。这里再补充一个频繁踩坑的点接口表设计。有些项目中 non-secure 侧直接用一个固定的宏定义去跳转到某个地址结果地址一改就崩。安全侧应该把接口地址打包成一个结构体放在 NSC 区域的一个固定地址non-secure 启动早期就去读取这个结构体。这样安全固件升级后即使接口地址变了只要结构体位置不变non-secure 固件仍然可以找到正确的新函数。7. 调试建议和一点个人经验最后留点调试经验。如果你要做 H563 的 TrustZone Bank Swap开发阶段建议在安全侧加一个调试用的 NSC 函数专门会把当前 Flash Bank 状态、FLASH_SR 错误标志、TZSC/SAU 关键配置读出来传给 non-secure 侧。这样每次交换失败你能立刻知道是“Flash 控制器拒绝访问”、“交换请求没执行完”还是“镜像校验没通过”而不是靠猜。另外第一次在这个平台上做 Bank Swap 之前先用最简单的两个闪烁 LED 程序分别烧到 Bank 1 和 Bank 2在 Main 函数里通过 Bank 状态判断分别点亮不同 LED。确认手动切 Bank 没问题后再往 OTA 流程里集成。这招看起来基础但真的能省下大量定位时间。我在实际项目中还发现一个习惯很推荐把“Bank Swap 复位前”和“复位后”的安全日志分别记录到两个不同的备份寄存器区域。这样如果升级失败即使设备已经复位你也知道上一次是在哪个阶段出的问题而不至于两眼一抹黑。如果你只是想在开发板上验证 Bank Swap 这个功能先按路径1把最简单的交换流程跑通建立对硬件行为的直觉然后产品化时再收敛到路径2。这两步之间的衔接尽量在项目早期完成因为 NSC 区域划分、安全接口地址这些设计一旦定型后期重构代价会成倍增加。STM32H563 这套组合拳TrustZone 加双 Bank Flash本质上就是为了解决“既能安全启动又能安全升级”这个矛盾的。只要把安全属性和切换权限理清这套方案能在量产设备上跑得非常稳。