STM32H563 TrustZone下NonSecure代码直接Bank Swap的可行性与工程实践
做 STM32H563 的朋友大概率迟早会被一个问题拦住我跑的是 NonSecure非安全代码到底能不能直接做 Bank Swap我最早是在评估系统 OTA 方案时被这个点卡住的当时查了不少资料也翻了参考手册和实际板子来回试。直接说结论能但条件很严格。能不能成功取决于你 TrustZone 分区怎么配、Flash 控制器开放给了谁。如果一开始就把 Flash 控制器划给 Secure 世界那 NonSecure 代码一碰寄存器就是 HardFault反过来如果明确把相关资源配成 NonSecure 可访问那么直接写交换控制位、触发复位是可以完成 Bank Swap 的。不过哪怕硬件允许我还是不建议你在 NonSecure 代码里裸操作寄存器工程上更稳的是把这个动作封装成一个安全的服务接口。这篇文章我会把整个问题的原理、权限模型、操作步骤和踩过的坑一并讲清楚。内容偏 STM32H563 实际落地适合正在做 OTA、双固件备份启动或者被 TrustZone 安全属性折腾到头疼的嵌入式工程师参考。1. 先想明白 Bank Swap 到底在做什么1.1 双 Bank 不是给你存两份数据的STM32H563 这种带双 Bank Flash 的芯片Flash 被切成两个独立的存储区比如 2MB 的型号Bank0 和 Bank1 各 1MB。很多人第一次接触双 Bank 时容易有一个误解以为这是两块“一模一样的存储空间”可以把数据交错着放。实际上设计双 Bank 的核心目的就是为了让系统能在两个独立固件版本之间切换。在正常应用里一个 Bank 放当前运行的固件另一个 Bank 放升级包或回滚版本。升级时先把新固件写进非活动 Bank校验没问题后触发 Bank Swap然后复位CPU 就会从另一个 Bank 启动。整个过程不涉及将 Flash 数据搬来搬去硬件只是换了一个“启动视图”。这个机制对 OTA 特别重要因为升级最怕的就是写到一半断电或者新固件不完整导致设备变砖。双 Bank 加 Swap 能保证任何时候至少有一个可用的固件版本。1.2 Swap 的关键复位后从哪个 Bank 取向量Bank Swap 不只是把两个 Bank 的名字换一换那么简单。真正生效的动作发生在复位时序里。在 STM32H563 上Flash 控制器内部有一个交换标志位标志的物理含义是“当前系统复位后默认从 Bank0 还是 Bank1 取复位向量”。当你把交换标志写进 Flash 控制器并触发一次系统复位芯片的启动逻辑就会按新的 Bank 映射关系执行。如果之前从 Bank0 启动Swap 后就会从 Bank1 启动同时 Bank0 的数据区映射到原来 Bank1 的位置。你可以把 Bank Swap 理解为电脑的“启动盘切换”。Windows 安装在 C 盘Linux 安装在 D 盘你现在设置 BIOS 从 D 盘启动重启后系统从 D 盘进来但 C 盘的数据并没有被移动。Flash Bank 也是这个道理只是这个 BIOS 设置开关被集成到了 Flash 控制器里还额外加了一堆安全管控。1.3 为什么 H563 特别强调 TrustZone 下的 SwapH563 是带 TrustZone 的 Cortex-M33 芯片跟老 M3/M4 芯片最大的区别是系统从硬件层面被分成了 Secure安全和 NonSecure非安全两个世界。TrustZone 会改变很多外设的访问规则。Flash 寄存器本身跟安全属性挂钩后NonSecure 代码能不能写交换标志就变成一个需要明确回答的问题。很多人在老芯片上习惯了直接操作FLASH-CR到 H563 上照搬代码结果要么 HardFault要么换个 Bank 后根本起不来。这件事的本质是Bank Swap 属于系统级控制能力。从安全角度讲它直接影响整个固件的可信根和启动链。TrustZone 的设计哲学是这类关键控制权限默认应该收在 Secure 世界手里除非你主动放权。所以“非安全代码能不能做”这个问题其实不是芯片硬件能不能而是你的系统设计允许不允许。2. TrustZone 下谁有权力动 Flash 控制器2.1 安全状态不是“一个标志位”那么简单TrustZone 在 Cortex-M33 上把 CPU 的运行状态分成 Secure 和 NonSecure。硬件访问总线时每个访问都携带一个安全属性这个属性决定了访问能不能被目标外设接受。理解这个概念时我建议不要把它当成一个简单的全局开关。CPU 在 Secure 状态可以访问所有资源包括 NonSecure 资源但 NonSecure 状态访问 Secure 资源时总线会直接拒绝并产生 fault。换句话说Secure 是最高权限NonSecure 是被限制的客人。安全属性由几条机制共同决定当前 CPU 处于哪个状态也就是处理器模式本身。地址的安全属性通过 IDAU 和 SAU 来定义。外设本身的安全分配比如某个外设被设置为 Secure那 NonSecure 访问就会被拦下来。这三者必须同时满足访问才能成功。要在 NonSecure 代码里操作 Flash 控制器Flash 控制器必须被标记为 NonSecure 可访问而且相关寄存器地址也不能落在 SAU 保护的 Secure 区域里。2.2 Flash 控制器到底算安全外设还是非安全外设在 H563 上Flash 控制器对外设的安全属性分配由 GTZC_TZSC 这类 TrustZone 安全控制模块管理。你可以在系统初始化时把 Flash 控制器配置成 Secure 或 NonSecure。问题在于直接把整个 Flash 控制器配成 NonSecure 风险不小。Flash 控制器里不只是 Bank Swap 一个功能还有擦写、保护、选项字节控制等功能。一旦整个控制器非安全可见NonSecure 代码理论上也能擦除或修改安全相关的选项字节这可能破坏安全启动和固件可信链。所以很多安全方案会把 Flash 控制器保留在 Secure 世界同时通过 Secure 工程导出一两个 NonSecure 可调用的接口比如“请求 Bank Swap”“读当前 Bank 状态”。NonSecure 代码只能通过接口间接完成操作拿不到直接操作寄存器的权限。2.3 默认状态下 NonSecure 代码会遇到什么STM32H563 上电复位后系统默认处于 Secure 状态。如果你的工程分了 Secure 和 NonSecure 两个工程Secure 工程会先启动完成必要的安全初始化后再跳转到 NonSecure 应用。如果你没有主动把 Flash 控制器开放给 NonSecure那么 NonSecure 代码执行到写 Flash 控制寄存器这一步时会触发总线错误并进入 HardFault。这个现象是最常见的也是很多人一开始说“NonSecure 不能做 Bank Swap”的直接来源。但实际不是不能做而是默认配置不允许。只要在 Secure 世界初始化时把 Flash 控制器的安全属性改成 NonSecureNonSecure 代码就可以直接访问。不过我这里建议谨慎放权尤其是当产品有安全需求时。3. NonSecure 代码直接做 Bank Swap能但有条件3.1 三种情况对应三种结果根据系统配置不同NonSecure 代码执行 Bank Swap 会遇到三种结果。第一种情况Flash 控制器被配置为 Secure。NonSecure 代码一访问 Flash 寄存器总线就报错典型表现是触发 HardFault。这种情况最安全也最保守。第二种情况Flash 控制器被配置为 NonSecure但地址安全属性或者 SAU 配置限制了访问区域。这类配置比较少见一旦碰到往往表现为代码能跑但读写被莫名拦截甚至会跳转异常。第三种情况Flash 控制器及其相关寄存器都被正确配置为 NonSecure 可访问。这种情况下NonSecure 代码可以直接写 Bank Swap 标志并触发复位功能正常。也就是说“能不能从 NonSecure 代码做 Bank Swap”的答案是能但前提是你主动把访问权交出来。这个决定权在系统配置表里不在 CPU 架构里。3.2 在 NonSecure 侧操作前必须完成的检查清单如果你决定在 NonSecure 侧直接操作那么我建议至少完成下面几项确认。确认 Flash 控制器的安全分配是 NonSecure而不是默认 Secure。确认相关寄存器地址没有被 SAU 配置为 Secure 区域。确认 Bank Swap 标志位本身可写而不是被硬件锁保护。确认两个 Bank 中目标 Bank 存在完整且有效的启动代码。确认切换后非安全应用向量表地址有效必要时重新配置 SCB-VTOR。确认复位方式选择正确Bank Swap 需要触发系统复位才能生效。确认安全状态与目标代码匹配如果新 Bank 是纯 NonSecure 应用要保证 CPU 复位后能从 NonSecure 状态启动。这些检查项看着多但在实际项目里漏掉任何一个都可能出诡异问题。最常见的是前两项没确认就写寄存器后几项没确认就换过去然后新固件跑不起来。3.3 工程上推荐的做法把 Swap 封装成 Safe/NS Callable 服务虽然 NonSecure 侧直接操作可行但我在量产项目里还是会走“Secure 封装”的路线。不是说不能直接操作而是直接操作太冒险。举个例子。NonSecure 代码可能因为程序 bug、内存踩踏、攻击者输入异常等不可控因素误触发 Bank Swap。直接把 Flash 寄存器开放给 NonSecure 世界相当于把一个系统级开关裸露在 UI 层。在产品上这绝不是一个好设计。更合理的做法是Secure 工程里实现一个 NonSecure Callable 函数内部做业务校验再执行交换。NonSecure 代码只负责发起“请切换固件”的请求具体能不能切、何时切、怎么切由 Secure 侧决定。这样做还有个额外好处就是后续如果换了芯片或改了 Flash 寄存器的访问规则只要 Secure 接口不变NonSecure 应用完全不用动。升级维护成本低很多。4. 实操在 STM32H563 上从 NonSecure 代码触发 Bank Swap4.1 第一步用 CubeMX 把 TrustZone 分区和安全属性配好STM32 的 TrustZone 工程一般分成 Secure 和 NonSecure 两个 Project。在 CubeMX 里创建工程时需要显式打开 TrustZone 支持并把 Flash 的存储区域按安全属性切分。如果你打算采用 Secure Proxy 方式Flash 控制器安全属性保持 Secure 即可无需开放给 NonSecure。CubeMX 里只需要把 NonSecure 工程需要的内存和中断配置好让 Secure 工程能正常调用 NonSecure 工程入口。如果你执意要在 NonSecure 侧直接操作 Flash 寄存器那么需要把 Flash 控制器的安全分配改成 NonSecure。这个过程在 CubeMX 的 TrustZone 配置器里完成。我个人建议还是用 Secure Proxy 方式下面实操也按这个方式展开。4.2 第二步Secure 侧提供可调用的交换接口Secure 工程里写一个 NonSecure Callable 函数并用cmse_nonsecure_entry属性标记这样 NonSecure 代码才能安全地通过函数指针调用它。函数里至少要做三件事参数校验、Flash 状态检查、触发交换。代码骨架大概是这样/* secure_app/src/secure_services.c */ #include cmsis_compiler.h #include stm32h5xx.h /* 假设 Flash 控制寄存器中有 Bank Swap 控制位 */ #define FLASH_CR_BANK_SWAP_MASK (0x1UL 8) __attribute__((cmse_nonsecure_entry)) uint32_t SECURE_RequestBankSwap(uint8_t bank) { uint32_t primask; /* 1. 参数校验只允许 Bank0 / Bank1 */ if (bank 1U) { return 0x00000001U; /* 参数无效 */ } /* 2. 业务校验检查目标 Bank 是否有有效固件 */ if (!SECURE_IsImageValid(bank)) { return 0x00000002U; /* 固件无效 */ } /* 3. 写 Bank Swap 标志等待 Flash 控制器就绪 */ primask __get_PRIMASK(); __disable_irq(); /* 防止操作过程中被打断 */ WRITE_REG(FLASH-CR, READ_REG(FLASH-CR) | FLASH_CR_BANK_SWAP_MASK); while ((READ_REG(FLASH-SR) FLASH_SR_BSY_MASK) ! 0U) { /* 等待 Flash 不忙 */ } __set_PRIMASK(primask); /* 4. 触发系统复位让新的 Bank 映射生效 */ NVIC_SystemReset(); /* 正常情况下不会走到这里 */ return 0x00000000U; }注意这里我把寄存器位名做了简化具体位名和偏移要以你手头 STM32H563 的参考手册为准。重点是模式Secure 侧函数有业务判断、有等待、有复位逻辑NonSecure 侧永远不会直接接触寄存器。4.3 第三步NonSecure 侧调用并完成复位验证NonSecure 工程里函数调用方式和普通函数不太一样。Cortex-M33 的 NonSecure 世界调用 Secure 函数需要通过 CMSE 的 NonSecure Callable 入口机制函数指针要用cmse_nonsecure_call属性声明。NonSecure 侧代码可以这样写/* nonsecure_app/src/ota.c */ #include cmsis_compiler.h /* 指向 Secure 侧导出的接口地址 */ typedef uint32_t (*secure_bank_swap_fn)(uint8_t) __attribute__((cmse_nonsecure_call)); #define SECURE_BANK_SWAP_ADDR ((secure_bank_swap_fn)0x10000FF1UL) uint32_t OTA_PerformBankSwap(uint8_t bank) { secure_bank_swap_fn request_fn; request_fn SECURE_BANK_SWAP_ADDR; if (request_fn 0U) { return 0xFFFFFFFFU; } return request_fn(bank); }这里的入口地址0x10000FF1UL是我举例用的真实项目中要去 Secure 工程的 Map 文件或链接脚本里找SECURE_RequestBankSwap的地址。这个地址的生成与 Secure 工程布局、CMSE 配置有关一定要根据实际链接结果来。调用流程完成后芯片会复位。如果一切正常复位后从新 Bank 启动你可以在 NonSecure 应用入口的最开始打印或记录下来当前 Bank。如果没能启动到新 Bank通常是前面检查清单里某些项出了问题。4.4 验证的细节不要以为函数跑完看到复位就认为 Bank Swap 成功。我建议至少验证三处。第一确认复位后代码运行地址落在目标 Bank 区域。可以通过读 PC 指针或者在两个 Bank 的入口各放一个不同的魔数启动后读出来确认当前 Bank。第二确认 Flash 控制器的 Swap 状态位。很多芯片提供反映当前映射关系的状态位查询到状态位变化后才能判断交换真的生效。第三做回切测试。从 Bank0 切到 Bank1再切回 Bank0反复做几十次确认每次都能稳定切换。这问题在单次测试中很难暴露但量产可靠性上必须覆盖。5. 踩坑实录常见问题与排查方向5.1 一写 Flash 控制寄存器就 HardFault这是最常见的启动问题几乎都是因为 NonSecure 世界访问了安全资源。如果你确认代码逻辑没问题先查 Flash 控制器的安全分配是否被配置为 NonSecure。排查思路是在 Secure 工程里检查 GTZC_TZSC 的配置确认对应位是否开放给 NonSecure。同时确认 SAU 配置里没有把 Flash 寄存器地址段划成 Secure。实在找不到就先用 Secure 侧接口调用不要执着于 NonSecure 直接访问。如果不想缩小权限干脆放弃 NonSecure 直接操作走 Secure Proxy 方案。这个方案能彻底绕开 HardFault 的问题而且更安全。5.2 交换标志写进去了复位后还是旧 Bank这个坑比 HardFault 更难排查因为代码没有报错但行为不对。第一步看复位类型。有些代码用软复位NVIC_SystemReset()是可以的但如果你的启动配置或低功耗模式已经提前改变了复位状态可能出现标志被写但复位后没有重新采样的现象。第二步看 Bank Swap 标志是否需要在特定状态才能写。有些器件要求在 Flash 不忙、没有正在进行的擦写操作时才能修改 Swap 标志。如果在 OTA 过程中后台还在写 Flash就会出现标志写不进去或写一半被清掉的情况。第三步看保护位。如果目标 Bank 有读写保护或者选项字节被锁定Swap 标志可能被忽略。检查 RDP 等级和写保护配置。第四步看启动配置。芯片启动不一定只依赖 Bank Swap有时 Boot0 引脚、Boot 选项字节也会参与决定启动 Bank。如果 BOOT 配置有更高优先级Bank Swap 标志可能不会生效。5.3 切换后程序直接跑飞程序跑飞但代码确实已经在目标 Bank 执行了通常是向量表和启动代码的问题。Cortex-M33 的复位向量表包含初始 MSP 和 Reset_Handler。Bank Swap 后如果你新 Bank 里的应用是一个独立的 NonSecure 工程它必须有自己独立的向量表。而向量表地址必须与当前实际存储位置一致。如果应用是从非零地址运行比如放在偏移 0x40000 的位置那启动时要正确设置SCB-VTOR。另外如果新 Bank 的代码是 NonSecure 应用但切换发生时 CPU 还停留在 Secure 状态复位后如果硬件安全属性没有按要求把入口映射为 NonSecure同样会跑飞。你需要在 Secure 工程中确认启动代码从 Secure 到 NonSecure 的跳转条件。5.4 调试器连不上或 RDP 等级引起的问题TrustZone 工程调试起来比老芯片麻烦得多。最常见的问题是启动 Secure 世界和 NonSecure 世界之间切换时调试器停错了目标。另一个典型问题是 RDP读保护等级。如果你的芯片 RDP 等级被加密到 Level 1 或更高级别调试器探测会受限NonSecure 工程的调试连接也会非常难。这种情况需要先通过调试接口解除保护或切换到合适的调试配置。如果你在 OTA 过程中出现设备反复重启并且调试器都连不上先检查是否已经被某个异常代码触发保护。很多时候不是 Bank Swap 的问题而是升级过程中安全状态没处理好导致整个系统锁定。结尾这个问题我在实际项目里反复验证过多次。我的体会是单纯问“NonSecure 代码能不能做 Bank Swap”答案并不能帮你避开所有问题。更重要的还是想清楚你的系统里谁该拥有 Flash 控制器这种级资源。把 Bank Swap 这样一个能决定“整机往哪个固件启动”的权限直接开放给 NonSecure 世界短期看写着方便长期看隐患很大。我最后给团队定的方案就是 Secure 侧封装接口NonSecure 侧只做业务请求。后续即使换了 Flash 寄存器细节NonSecure 应用一点不用动这个设计帮我们省了大量维护成本。如果你也在做 H563 的双 Bank OTA建议至少先在自己板子上把安全属性和状态位读一遍再决定要不要直接开放权限。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →