STM32 TrustZone下手写UART中断:从安全配置到HAL回调全解析

发布时间:2026/8/31 22:00:28
STM32 TrustZone下手写UART中断:从安全配置到HAL回调全解析 上周处理一个 STM32L552 的项目客户在已有 TrustZone 分区方案的前提下要求给非安全侧新增一路 USART1 中断收发还被特别要求不能重新跑 CubeMX 生成。原因很直接工程里已经手工改过链接脚本、SAU 配置和安全侧初始化代码CubeMX 只要一重新生成这些人工改动就会被覆盖项目直接没法编译。于是我对着参考手册和 HAL 源码逐层去翻最后在不打开 CubeMX 的情况下把 UART 中断完整跑通了。整个过程踩了不少坑尤其是 TrustZone 对中断链路的影响比普通 STM32F1/F4 项目复杂得多。这篇内容就是这次实践的全记录适合已经有 STM32 HAL 基础、刚开始碰 TrustZone或者正被 CubeMX 生成代码折腾的人参考。先说清楚一个前提我用的芯片是 STM32L552Cortex-M33 内核硬件支持 TrustZone。但同样的逻辑在 STM32U5、STM32H5 系列上也成立只是寄存器名称和安全配置项的位位置略有差别。下面几节我会把“为什么不能直接用 CubeMX 生成”、“TrustZone 对 UART 中断配置到底卡在哪几层”、“完整的手写配置步骤”以及“实测遇到的坑”依次讲透。1. 为什么要在 TrustZone 下手写 UART 中断不用 CubeMX 的真实场景很多人听到“不用 CubeMX”第一反应是“装高手”其实不是。CubeMX 在 TrustZone 工程里做的事非常多但也正因为多它和已有工程之间经常打架。1.1 正常 CubeMX TrustZone 流程到底做了什么在带有 TrustZone 的 STM32 上CubeMX 会默认生成两个工程一个 Secure 工程一个 Non-Secure 工程。它帮你完成的事情包括配置 Option Bytes确定 Flash 里 Secure 和 Non-Secure 区域的边界。在 Secure 工程初始化代码里设置 SAU 和 GTZC给外设和内存区域分配安全属性。生成两个独立的向量表Secure 侧用VTOR_SNon-Secure 侧用VTOR_NS。配置好 Secure 工程的初始中断和目标态保证启动阶段能正常跳到 Non-Secure 应用。生成两个独立工程各自的链接脚本分别指向不同的 Flash/RAM 区域。最后在 Non-Secure 工程的main里才允许你写业务代码UART、GPIO、定时器这些外设初始化CubeMX 全都帮你铺好了。如果你是从零开始一个新项目这套流程的确省事。但问题也恰恰出在这里它把“安全分区”和“应用功能”耦合得太死。你一旦在 Secure 工程里手工维护了 SAU 配置或者像我们项目一样用了 TF-M 风格的启动流程CubeMX 重新生成代码时那一堆MX_..._Init()函数会毫不犹豫地覆盖你手工改过的安全属性初始化。1.2 必须手工写的几种现实场景我这次遇到的情况属于最典型的一种工程里已经有自己的安全侧分区方案并且 Non-Secure 工程是 CMake 管理的编译系统CubeMX 的代码生成完全不适应这种构建方式。这种情况下你没有选择只能手写。还有几种场景也特别常见你需要在运行过程中动态切换外设的安全属性。CubeMX 生成的是启动时一次性配置运行时要改 GTZC 或者 TZSC你依然得自己写寄存器操作。你在做安全侧与普通应用侧的通信比如 Non-Secure 工程通过安全服务调用 Secure 侧的 UART接口没法定成 CubeMX 模板的样子。内存分区是非标准的。CubeMX 的链接脚本只覆盖它自己定义的那几种分区模板一旦你用了自定义的 LMA/VMA 布局生成代码没法用。CI/CD 构建环境要求全部脚本化。CubeMX 是 GUI 工具在 Linux CI 环境里自动化生成代码虽然可行但每次刷新配置都会导致大量无意义的 git diff不利于代码审查。所以说搞清楚手写方案不是要抛弃工具而是当工具不适合项目现状时你要能接管底层。1.3 手写方案的实际收益手动写一遍之后你会对整条中断链路有完全不一样的理解。比如你会发现普通 STM32 上只要配置 GPIO 复用、使能外设时钟、调用HAL_UART_Receive_IT就能跑通的 UART 中断在 TrustZone 上其实暗藏着一层“这个外设归谁所有、中断能够去哪个世界”的问题。而且从工程维护角度看手写代码是可以进版本管理、可以 code review、可以写单测的。CubeMX 生成的代码虽然也能 review但很多配置隐藏在与 IDE 绑定的.ioc文件里review 起来反而不直观。后面我会给出完整的手写配置流程并解释每一步为什么必须这么做。2. TrustZone 对 UART 中断链路的影响必须手动打通的三层安全配置TrustZone 环境下UART 中断能不能正常触发、触发后能不能进入正确的处理函数取决于三层安全属性是否一致。这三层分别是外设本身的安全属性、NVIC 中断目标态、向量表归属。任何一层配置不一致问题就会以各种诡异的方式出现。2.1 第一层外设安全属性GTZC 里的 TZSC/TZPCCortex-M33 的 TrustZone 把系统分成了 Secure 世界和 Non-Secure 世界。判断一个内存地址属于哪个世界靠的是 SAU 和 IDAU。SAU 是可编程的由 Secure 代码在启动时配置IDAU 是芯片厂家固化好的实现定义属性。在 STM32L5 上IDAU 之前还有一层 GTZC也就是 Global TrustZone Controller它负责给外设和 GPIO 分配安全属性。GTZC 内部又拆成几个部分和 UART 直接相关的是TZSC负责 AHB/APB 外设的安全属性比如 USART1 是 Secure 还是 Non-Secure。TZPC负责 GPIO 端口的安全属性比如 GPIOA 是 Secure 还是 Non-Secure。TZIC负责捕获那些“非法中断”或者“非法访问”事件调试时非常有用。以 USART1 为例复位默认值是 Secure。也就是说Non-Secure 代码在没做任何配置的情况下直接访问 USART1 寄存器会触发 SecureFault。同理USART1 的引脚如果所在的 GPIO 端口还是 Secure 属性Non-Secure 代码操作这个引脚也会出问题。所以你要手动处理的第一个配置就是把 UART 实例和所在 GPIO 端口的安全属性改成和目标使用世界一致。/* 以 STM32L5 为例Secure 世界启动早期执行 */ /* 将 USART1 配置为 Non-Secure */ TZSC-SECCFGR1 ~TZSC_SECCFGR1_USART1_S; /* 将 GPIOA 配置为 Non-Secure */ TZPC-DECSECCFGR1 ~TZPC_DECSECCFGR1_DECSECA;注意这两个寄存器在复位后处于锁定状态吗并不是但强烈建议在 Secur 侧启动早期配完就不要再动防止 Non-Secure 侧通过非法路径尝试修改。不同型号的 GTZC 寄存器布局不同U5 和 H5 的位定义有差异一定要对着对应参考手册的 “GTZC registers” 章节核对。2.2 第二层NVIC 中断目标态ITNS 寄存器外设被标注为 Secure 或者 Non-Secure 只是第一步紧跟其后的是中断目标态。Cortex-M33 的 NVIC 里每个中断源都有一个“Target Non-Secure”属性位集中在NVIC-ITNS寄存器组里。这一位决定了中断触发后CPU 是进入 Secure Handler 还是 Non-Secure Handler。ITNS 的配置规则很简单也有点坑它必须和外设本身的安全属性一致。如果外设是 Non-Secure那这个中断源在 ITNS 里必须置 1如果外设是 SecureITNS 必须保持 0。如果不一致轻则中断不触发重则直接把系统引导到错误状态。/* Secure 侧配置 USART1 中断目标态为 Non-Secure */ NVIC_SetTargetState(USART1_IRQn, 1); /* 或者直接操作寄存器 */ NVIC-ITNS[0] | (1UL USART1_IRQn);这里有个重要细节ITNS 寄存器只在 Secure 世界可见Non-Secure 代码是看不到也写不了的。所以哪怕你的 UART 完全属于 Non-Secure 应用使用这个 ITNS 配置也必须由 Secure 侧提前做好。换句话说多了一个“Secure 侧要为 Non-Secure 侧铺路”的步骤这是以前做标准库/HAL 时完全不需要考虑的事。2.3 第三层向量表归属VTOR_S 与 VTOR_NSCortex-M33 针对于 Secure 和 Non-Secure 世界各有一个向量表偏移寄存器分别是SCB-VTOR_S和SCB-VTOR_NS。中断触发后CPU 会按照中断目标态去读取对应世界的向量表。中断目标是 Non-Secure则从 Non-Secure 向量表取中断服务函数地址目标是 Secure则从 Secure 向量表取。这一层最容易出问题的地方在于你在 Keil/IAR/CMake 工程里写了USART1_IRQHandler但这个函数被编译到了错误的链接区段。比如 Non-Secure 工程里定义了 Hander但 Non-Secure 向量表在链接脚本里指向的地址和我们实际放置函数的位置不一致那么中断一旦触发CPU 取到的是错误地址轻则跑飞重则直接 HardFault。所以手写方案里你必须明确知道当前这个 UART 中断要交给哪个世界处理然后把中断服务函数放在对应的向量表区段里。如果你用的是真正的双工程方案Secure 侧一个工程、Non-Secure 侧一个工程这个区分是天然的。如果你用了什么单工程双区域链接的骚操作那就特别容易在这里踩坑。3. 手写 UART 中断的完整代码从 RCC 时钟到 HAL_Callback前面讲了这么一大堆原理现在进入实操。我这次用的方案是USART1 属于 Non-Secure 世界Non-Secure 应用直接使用 HAL 完成中断收发Secure 侧只负责启动阶段的安全属性配置。这个结构最简单也是大多数项目实际采用的形态。3.1 安全属性预置与时钟、GPIO 初始化Secure 侧的启动代码必须在跳到 Non-Secure 应用之前完成 UART 和 GPIO 的安全属性预置。否则 Non-Secure 应用连寄存器都摸不到。下面这段代码放在 Secure 工程的main早期或者对应的SystemInit之后执行。/* Secure 侧启动早期执行 */ void Secure_Init_NS_UART(void) { /* 1. 把 USART1 切到 Non-Secure */ TZSC-SECCFGR1 ~TZSC_SECCFGR1_USART1_S; /* 2. 把 GPIOA 切到 Non-Secure */ TZPC-DECSECCFGR1 ~TZPC_DECSECCFGR1_DECSECA; /* 3. 设置 USART1 中断目标态为 Non-Secure */ NVIC_SetTargetState(USART1_IRQn, 1); }这里有个容易忽略的点STM32 的 GPIO 默认上电后大部分是模拟输入并且 GPIOA 这个端口在 GTZC 里默认是 Secure 的。如果你只清了 USART1 的安全属性没清 GPIO 端口的安全属性那 USART1 的 TX/RX 引脚依然无法被 Non-Secure 应用正常操作因为引脚对应的 GPIO 寄存器还是 Secure 保护状态。时钟这边RCC 配置也有安全属性概念。在 STM32L5 上RCC 寄存器同样受 TrustZone 控制。为了让 Non-Secure 的HAL_UART_Init能正常执行我建议在 Secure 侧启动阶段直接把所需外设时钟打开或者把 RCC 里对应的时钟使能权限也放开。实际工程里我习惯直接把时钟全部配置好Non-Secure 侧拿到的是一个时钟树已经稳定的系统省去很多排查时间。/* Secure 侧启动时钟配置 */ void Secure_Init_Clock(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); }时钟配置在 Secure 侧完成后Non-Secure 侧的 HAL 也能访问这个外设了因为外设总线时钟一旦使能不会因为世界切换而关闭。3.2 GPIO 复用与 UART 初始化Non-Secure 工程的main里初始化代码和普通 STM32 项目看起来差别不大但每一步的背景都多了一层“这个资源已经归我所有”的意味。/* Non-Secure 侧 */ UART_HandleTypeDef huart1; uint8_t rx_byte; void NS_UART_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; huart1.AdvancedInit.AdvFeatureInit UART_ADVFEATURE_NO_INIT; HAL_UART_Init(huart1); /* 配置 NVIC 中断优先级并打开中断 */ HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART1_IRQn); }如果你之前用 CubeMX 生成过代码会发现这段代码和生成的MX_USART1_UART_Init()基本一模一样。区别在于CubeMX 帮你搞定了几层安全配置而我们现在是手动在前面铺好的。3.3 启动接收并实现回调初始化完成后启动中断接收void NS_UART_Start_Receive(void) { HAL_UART_Receive_IT(huart1, rx_byte, 1); }同时实现中断服务函数和 HAL 回调。注意这个USART1_IRQHandler必须编译到 Non-Secure 工程的向量表里。/* Non-Secure 侧 */ void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 收到一个字节做业务处理 */ HAL_UART_Transmit(huart1, rx_byte, 1, 100); /* 继续等待下一个字节 */ HAL_UART_Receive_IT(huart1, rx_byte, 1); } }到这里一个最简单的 Non-Secure 侧 UART 中断收发就串起来了数据到达 - NVIC 收到中断 - 因为 ITNS 置 1CPU 切到 Non-Secure 状态取 Non-Secure 向量表 - 进入USART1_IRQHandler- HAL 处理 - 回调函数。3.4 如果 UART 必须给 Secure 侧使用有些场景下 UART 必须留在 Secure 世界比如安全日志输出、安全数据通道。这时候代码结构会反过来UART_Init、USART1_IRQHandler、HAL_UART_RxCpltCallback全部放在 Secure 工程里ITNS 保持 0。Non-Secure 世界需要收发数据时不能直接碰这个 UART只能通过你单独写的安全服务函数比如 SVC 或者 NSC 区域调用间接使用。/* Secure 侧服务接口供 Non-Secure 侧调用 */ uint8_t Secure_UART_ReceiveByte(uint8_t *byte) { if (HAL_UART_Receive(Secure_huart, byte, 1, 100) HAL_OK) return 0; return 1; }这个设计在传统 STM32 上完全没有等价物是 TrustZone 项目才特有的架构取舍。到底把外设放哪个世界需要和安全架构一起决策不是单纯看哪个代码好用。4. 初始化顺序与中断优先级分组TrustZone 下最隐蔽的联动很多手写 TrustZone 工程跑不起来不是配置写错了而是顺序不对。TrustZone 的初始化顺序本质上是由“Secure 世界要先把安全属性发牌Non-Secure 世界才能拿到牌”这个逻辑决定的。4.1 启动阶段的顺序链如果你认真读过启动汇编会发现 CM33 复位后CPU 默认从 Secure 世界取向量表。整个启动链大致是复位向量进入 Secure 侧启动代码。Secure 侧配置 SAU、GTZC/TZSC/TZPC、NVIC ITNS。Secure 侧配置时钟、系统外设。Secure 侧设置 Non-Secure 向量表地址VTOR_NS。Secure 侧用BLXNS或者 SVC 机制跳到 Non-Secure 复位向量。Non-Secure 侧开始执行自己的main初始化 UART 等业务外设。我把第 2 步提前强调是因为很多人会先在 Secure 侧把 UART 初始化了再在 Non-Secure 侧初始化一次结果两边配置互相干扰。正确的做法是安全属性一旦定好UART 实例归属哪个世界就由那个世界独立初始化另一个世界不要重复碰它。4.2 SAU 区域的潜在影响SAU 负责把整个地址空间划分成 Secure 和 Non-Secure。如果你在 Secure 侧配置 SAU 时把 USART1 的寄存器地址区间错误地标注成了 Secure那么即使 TZSC 里 USART1 已经写成 Non-SecureNon-Secure 侧访问 UART 寄存器照样会被拦截因为 SAU 的优先级高于外设安全控制。反过来如果你把某个 SRAM 区域配置成 Secure而 Non-Secure 侧 UART 接收缓冲区恰好放在这个 Secure SRAM 里那 Non-Secure 代码操作这个缓冲区也会触发安全违规。所以手写工程里你要画一张表明确列出来资源归属世界配置位置USART1 实例Non-SecureTZSCGPIOANon-SecureTZPCUSART1 中断目标态Non-SecureNVIC ITNSRX 缓冲区所在 SRAMNon-SecureSAU / 链接脚本UART 时钟Secure 已使能RCC这张表在排查问题时特别有用。一旦出现 SecureFault先对表检查资源归属能省一半时间。4.3 中断优先级分组和特权级别Cortex-M33 的中断优先级寄存器IPR有 Secure/Non-Secure 两套视图。Secure 侧可以通过AIRCR.PRIS锁定当前优先级分组让 Non-Secure 侧只能用被允许的优先级范围。这看起来像安全特性但实际项目里经常引发奇怪现象Non-Secure 侧调用HAL_NVIC_SetPriority设置一个超出允许范围的优先级写操作会被静默忽略中断优先级保持默认值。如果复位后优先级分组是NVIC_PRIORITYGROUP_4之类的默认值Non-Secure 侧通常不会出问题。但你如果在 Secure 侧设置了更复杂的优先级分组然后又期望 Non-Secure 侧用同样分组配置 UART 中断就得确保 Non-Secure 侧的优先级范围匹配。我踩过的一个具体例子是Secure 侧把 PRIS 设成了NVIC_PRIORITYGROUP_2Non-Secure 侧用HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)结果优先级值被裁剪UART 中断被一个低频定时器中断一直抢占回调经常延迟好几毫秒。排查了很久才发现是优先级分组的问题。所以在手写 TrustZone 工程时Secure 侧的优先级设置和 Non-Secure 侧的优先级调用要保持一致否则宁可全用默认分组。5. 实测踩坑记录SecureFault、ITNS 不一致、回调失踪前面给出的代码虽然是能跑通的但手写过程中不会一帆风顺。我把这次实操里踩过的坑按现象整理出来每个都附上根因和排查手段下次你遇到类似问题可以直接抄作业。5.1 现象一Non-Secure 代码一访问 UART 就进 SecureFault这是最常见的坑也是最容易定位的。现象就是 Non-Secure 侧调用HAL_UART_Init时程序直接进入SecureFault_Handler或者复位循环。排查手段首先是看异常状态寄存器。Cortex-M33 的SCB-SFSR会记录安全错误的原因重点看这几个位SFSR 位段含义排查方向INVEP无效异常入口向量表配置问题INVTRAN无效状态转换BLXNS/安全函数调用问题AUVIOL属性违规外设/内存安全属性不一致LSPERR浮点延迟保存错误安全侧浮点上下文问题实际中 AUVIOL 位最常见说明就是安全属性没配置对。按 4.2 节的表格逐项检查八成是 TZSC 或者 TZPC 漏配了。/* Secure 侧调试用读取并打印 SFSR */ uint32_t sfsr SCB-SFSR; if (sfsr SCB_SFSR_AUVIOL_Msk) { /* 属性违规重点查 TZSC/TZPC/SAU */ }5.2 现象二ITNS 配置和向量表不匹配中断永远不执行这个坑更隐蔽。外设和 NVIC 看起来都配好了中断也确实触发了但程序就是不进你写的USART1_IRQHandler。根因通常是两种情况。第一种ITNS 设成了 Non-Secure但 Non-Secure 向量表里对应位置是空的或者指向了一个 Secure 地址。第二种ITNS 设成了 Secure但 USART1 外设本身是 Non-Secure这时中断行为可能变得很蹊跷甚至触发 SecureFault。排查手段是在调试器里同时检查VTOR_S、VTOR_NS和向量表地址处的值。/* 在调试器里观察 */ SCB-VTOR_NS; /* Non-Secure 向量表基地址 */ *((uint32_t *)(SCB-VTOR_NS USART1_IRQn * 4 16)); /* IRQ 向量 */IRQ 向量的偏移规律是初始栈顶 Reset NMI HardFault MemManage BusFault UsageFault ... 16 个系统异常之后才是 IRQ0。USART1 的 IRQ 号根据芯片不同一般在 20 左右对应向量表索引大概是USART1_IRQn 16。我常在调试器里直接打印这个值和反汇编窗口里USART1_IRQHandler的地址对比。如果看到的是0xFFFFFFFF说明链接脚本没把中断函数放到向量表里得去查工程里向量表的符号导出。5.3 现象三HAL_UART_RxCpltCallback 不触发但中断确实进来了这个现象是我在调试优先级分组时发现的。中断服务函数里打断点能看到HAL_UART_IRQHandler执行了但回调就是没反应。HAL 的 UART 接收中断流程里如果出现 Overrun ErrorORE标志HAL_UART_IRQHandler会优先处理错误进入HAL_UART_ErrorCallback此时HAL_UART_RxCpltCallback自然不会触发。而在 TrustZone 环境下如果 Non-Secure 侧对 UART 的访问频率和数据流速率不一致ORE 很容易置位。void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-ErrorCode HAL_UART_ERROR_ORE) { /* 清掉 ORE 标志重启接收 */ __HAL_UART_CLEAR_OREFLAG(huart); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }另外别忽略一种情况你的回调函数名写对了没。HAL 库的回调都是 weak 函数如果你在 Secure 侧也定义了一个HAL_UART_RxCpltCallback而实际中断是进 Non-Secure 世界处理的那 Non-Secure 侧链接器可能不会选到你的 Non-Secure 版本两个工程各自链接各自的回调看起来就像“回调失踪”。所以双工程方案里回调函数的归属世界必须和中断处理世界严格一致。5.4 现象四Secure 侧初始化 UART 后跳到 Non-SecureHAL 状态错乱如果你的 Secure 侧在把 UART 交给 Non-Secure 之前自己先调用过HAL_UART_Init然后 Non-Secure 侧又调用HAL_UART_InitHAL 内部的gState和Lock很可能已经不在初始状态导致第二次初始化直接返回HAL_BUSY。这是因为 HAL 的状态机是基于内存变量维护的Secure 侧和 Non-Secure 侧如果各自有一份UART_HandleTypeDef变量它们并不共享同一个对象。更稳妥的做法是同一个 UART 实例在它的归属世界里只初始化一次另一个世界不管。如果确实需要交接Secure 侧初始化完就把gState改回HAL_UART_STATE_RESET或者干脆让 Secure 侧完全不碰这个 UART把时钟打开即可。5.5 一个最小复现实验如果你也是第一次做这件事我建议不要直接往大工程里加代码先搭一个最小双工程验证环境。Secure 工程只做三件事配时钟、把 USART1 和 GPIOA 置为 Non-Secure、设 ITNS 后跳到 Non-Secure。Non-Secure 工程只做 UART 回显。实验步骤在 Non-Secure 工程的main里初始化 UART 并启动HAL_UART_Receive_IT。用 USB 转串口连接 PA9/PA10。串口助手发送 0x41。预期串口助手收到 0x41 回显。如果没回显按优先级排查先看有没有进 SecureFault再看有没有进USART1_IRQHandler最后看有没有进HAL_UART_RxCpltCallback。这三步观察点都打上断点很快就能定位是哪一层断了。6. 最后的工程建议与调试心得这个项目做完之后我自己最大的体会是TrustZone 环境下手写外设中断本质上是在和一个“资源归属权”系统打交道。传统 MCU 编程里你写完初始化就能直接用外设TrustZone 里你必须先把外设“分发”到正确的世界然后那个世界的代码才能像以前一样使用它。这个思维转换很重要。几个具体建议把所有安全属性配置集中放到 Secure 侧一个独立的Secure_Init_函数簇里不要散落在多个文件里。这样 Non-Secure 侧代码不会碰任何安全寄存器审查时也只需要看这一处。在 Secure 侧加一个SecureFault_Handler进去后把SFSR的值保存到固定内存地址方便后续调试。别让空循环把现场丢掉。向量表是最容易踩坑的地方。每次构建后用nm或者调试器查看USART1_IRQHandler的段地址确认它在正确的世界区域里。优先级的设置一定要统一。Secure 侧和 Non-Secure 侧如果都配中断优先级最好使用相同的分组策略否则低优先级中断抢高优先级这种诡异时序会把你折磨疯。最后再分享一个小技巧调试时我习惯在 Secure 侧把 TZIC 的中断状态寄存器读出来看看。TZIC 会记录哪些“非法事件”违反了安全规则它给出的违规外部中断源编号能直接告诉你哪个中断配置错了。这个信息比在 Non-Secure 侧瞎猜要高效得多。如果你手头正好有带 TrustZone 的板子完全可以按这篇文章的流程走一遍。即使你最终还是会回到 CubeMX这一遍手写也会让你在遇到生成代码和实际需求冲突时知道去哪里改、怎么改。