
做嵌入式图形开发最怕的不是时序调不对而是你明明把屏点亮过无数次换了一颗带 TrustZone 的 MCU 之后LTDC 反而不工作了。寄存器写不进、DMA2D 搬图花屏、中断始终不触发这些现象单独看都像硬件问题实际上全是安全属性没配好。我自己第一次在带 TrustZone 的芯片上配置 LTDC 时就被这个组合狠狠教育了一顿所以今天把整套配置思路和踩坑记录整理出来希望对做安全产品、TFT 显示方案的朋友有帮助。LTDC 是 ST 芯片上的液晶显示控制器TrustZone 是 ARM 针对嵌入式设备做的硬件安全隔离方案。两者看起来一个管显示、一个管安全但放到同一颗 MCU 上就产生了一系列需要仔细处理的问题外设归哪个世界管、显存放在哪个区域、中断走哪一套 NVIC这些都要在写显示驱动之前搞定。这篇文章适合正在做带 GUI 的安全终端、支付设备、工业 HMI 的开发者也适合那些刚接触 TrustZone、想搞清楚它到底怎么影响裸机外设开发的初学者。1. 为什么 TrustZone 会管到显示驱动头上1.1 先理解 TrustZone 是怎么“管”外设的TrustZone 会把 CPU 的运行状态分成安全和非安全两个世界在 Cortex-M33、M55 这类内核上硬件层面不仅仅隔离了代码还隔离了内存和外设访问。具体来说芯片内部有一套硬件机制给每一块内存区域、每一个外设都打上“安全”或“非安全”的标签。安全世界的代码可以访问所有安全和非安全资源非安全世界的代码只能访问被标记为非安全的资源。这个机制听起来有点抽象我用一个生活化的类比来解释。你可以把 MCU 想象成一家公司安全世界是管理层办公室非安全世界是普通员工工区。管理层有门禁卡能进所有房间普通员工只能进自己的工区如果误闯了管理层的资料室报警器会响并且门会自动锁死。对应到 MCU 上就是非安全代码一旦访问了安全地址或安全外设寄存器会直接触发 HardFault 或者 Bus Error。这套隔离机制在外设维度上是由 GTZC带 TrustZone 的全局安全控制单元这类模块来管理的。每个外设都有默认的安全属性绝大多数外设复位之后默认挂在安全世界名下。也就是说只要芯片开了 TrustZoneLTDC 复位之后默认是“安全外设”非安全世界的代码去操作它的寄存器会直接被总线错误挡回来。“显示驱动跑不起来”往往就是这么来的。1.2 LTDC 的特殊性它是一条完整的数据通路LTDC 和普通的外设不一样普通外设比如 UART、SPI你操作它的寄存器、收发数据就够了数据链路很短。LTDC 则要复杂得多它一方面要通过 AHB/AXI 总线不断读取显存中的像素数据另一方面要产生 RGB 时序信号送给屏幕中间还可能要经过 DMA2D 做图形加速最终在中断里告诉你“这一帧已显示到哪一行了”。这意味着 LTDC 涉及的资源不止一个外设寄存器本身而是“LTDC 寄存器 显存区域 DMA2D 中断控制器”这一整条链路。而 TrustZone 的安全隔离恰好就是针对这种“跨模块跨内存”的访问场景设计的。每一个环节一旦安全属性不对整条显示通路就会断掉。我碰到过一个很典型的情况LTDC 已经能够正常配置屏幕也能点亮了但只要一开 DMA2D 做整屏搬图就开始花屏。排查到最后发现DMA2D 本身已经被我划到非安全侧了但它的目标地址——显存却还落在安全内存区域里。DMA2D 作为非安全外设根本没有权限往安全内存里写数据总线访问被自动拒绝但因为没有仔细检查错误标志代码还以为搬图成功了。这就是典型的“数据通路”问题。1.3 先做决定LTDC 到底放安全侧还是非安全侧在开始配置 TrustZone 之前你得先想清楚一个问题你的显示链路到底应该放在哪个世界。这个问题没有标准答案完全取决于产品需求。如果你的产品是工业 HMI、家用电器的彩屏界面、普通消费类仪表盘显示内容只是一些参数、图表、控制界面不涉及敏感数据那么推荐把 LTDC、DMA2D、显存区域都划到非安全世界。平时业务代码跑在非安全侧直接操作显示外设没有来回切换的开销开发效率也高。但如果你的产品带有安全属性比如金融支付终端的界面会显示密钥状态、二维码、用户敏感信息或者医疗设备需要在显示界面上体现安全提示那你可能要让安全世界的代码来控制显示链路避免非安全世界篡改屏幕内容。这种场景下LTDC 就要放在安全侧而完全禁止非安全侧直接访问。这两种方案在配置流程上差别比较大。下面我以“LTDC 放非安全侧”为主线来讲这是绝大多数项目的需求同时也会补充一些安全侧方案的注意事项。2. 配置前必须搞明白的四个核心机制2.1 启动流程安全世界永远先登场TrustZone 开启后CPU 复位后的第一条指令一定是执行安全世界代码。这是硬件设计决定的因为只有安全代码才能配置安全属性和内存隔离。所以哪怕你的产品业务代码全部跑在非安全侧也需要先有一个安全工程完成初始化之后再由安全代码跳转到非安全应用的入口点。这个启动顺序直接决定了 LTDC 配置的代码结构。你不能像以前那样在 main 函数里直接噼里啪啦地初始化时钟、配置 LTDC而是要拆成两个工程安全工程负责配置 TrustZone 相关机制并跳转非安全工程负责业务逻辑和显示驱动的实际初始化。这个跳转动作有讲究不是简简单单用函数指针调用一下就行的。安全代码需要设置好非安全世界的栈指针 MSP_NS、向量表 VTOR_NS然后通过某些方式跳转过去。在 Cortex-M33 上这通常涉及EXC_RETURN的用法和S、NS状态的切换。你可以用内联汇编配合特殊寄存器也可以直接用 ST 提供的库函数。无论哪种方式核心思想都是在跳转前你得先保证非安全代码能访问的 RAM 区域已经准备好并且是“非安全”的。我在实际调试中见过不少新手在这里翻车安全工程配置了 SAU 和 GTZC但忘了把非安全工程的向量表放在非安全可访问的 SRAM 区域导致跳转过去一执行中断就直接 HardFault。这一类问题往往不会在第一时间定位因为你看到的现象是屏幕没反应很难联想到是启动流程的问题。2.2 外设安全属性让 LTDC 和 DMA2D “划归”非安全外设的安全属性是需要显式配置的。在 ST 的带 TrustZone 芯片上有一个专门的模块负责分配外设的安全归属通常叫 GTZC 或者 TZSC。这个模块里面有一堆掩码寄存器每个 bit 对应一个外设或者一组外设把 bit 写成某个值就能把对应外设标记成安全或非安全。你需要重点关注的三个外设是LTDC 本身、DMA2D 图形加速器以及时钟控制的访问权限。LTDC 和 DMA2D 好理解都是显示链路的核心但很多人会忽略 RCC时钟控制也要能访问。你想想如果 RCC 还在安全侧手里非安全代码连使能 LTDC 时钟的寄存器都碰不到那后面配置时序、图层全是空谈。有些芯片的 RCC 安全属性是拆分的比如某些寄存器位保留给安全侧某些位可以配置成非安全访问。遇到这种芯片你需要仔细查阅参考手册中关于 RCC 安全属性的章节或者用 CubeMX 之类工具生成初始代码它会帮你把这些分散的属性配置好。还需要注意的是一旦你把 LTDC 和 DMA2D 划成了非安全外设安全世界的代码就再也无法直接访问这些外设寄存器了。这有时候会带来不便比如你的图形界面有一部分需要安全世界来动态改写这时候就得通过一个 NSCNon-Secure Callable函数做网关。安全世界编写一个可被非安全调用的函数在函数内部操作显示控制器的硬件资源非安全代码通过这个函数间接实现显示控制。这个函数本身要设立在带有 NSC 属性的内存区域里是不是有点绕但这是 TrustZone 下常见的软件架构设计值得记住。2.3 内存归属显存必须能被 LTDC 读到显存区域的安全属性是显示链路里面最容易被忽略、也最容易出怪问题的一环。LTDC 有个 DMA 引擎会持续从显存地址读取像素数据。如果 LTDC 被划成了非安全外设那么它通过 DMA 读取的地址也必须是“非安全”内存区域。哪怕只有一块显存落在安全区域读取时会得不到合法像素值表现就是花屏、黑屏、颜色错乱。SRAM 的安全属性分配在 Cortex-M33 这类芯片上由 SAU安全属性单元负责。你需要在安全工程里明确划分好哪些 SRAM 是安全的、哪些是非安全的。理想情况下把显存 Buffer 放在一段连片的非安全 SRAM 区域里同时确保链接脚本里这个区域的符号能被非安全工程正确引用。如果你的系统里用了 DMA2D那么 DMA2D 要访问的所有地址也都要遵循同样的规则。包括 DMA2D 的源地址比如片外 Flash 里的图片素材、目标地址显存 Buffer、以及设置顔色时访问的常量内存。只要有一个地址不合法DMA2D 传图就会出问题。更麻烦的是有些芯片的 DMA2D 错误不会直接报出来你的代码会像一切正常一样执行完毕但结果就是屏幕上的图像不对。这种“假成功”问题最浪费调试时间我在下文会讲具体的排查方法。2.4 中断归属NVIC 也要分成两个世界LTDC 会用到中断比如行中断Line Interrupt和全局中断Global Interrupt用来实现撕裂避免、同步刷新等高级功能。在 TrustZone 开启后NVIC 同样分成了安全和非安全两个部分。每个中断线都有安全属性默认情况下所有中断线都归安全世界管理。你把 LTDC 划成非安全外设之后还要记得把 LTDC 对应的中断线也改成非安全属性否则非安全侧的代码调用HAL_NVIC_EnableIRQ会被拒绝或者中断响应了却进不了非安全的处理函数。中断优先级也要注意。在 Cortex-M33 上安全代码可以配置中断优先级分组非安全代码只能使用非安全处理逻辑允许的优先级范围。如果安全侧把优先级分组设成了 3 bits 抢占优先级 1 bit 子优先级非安全侧的代码要在配合这个分组的情况下正确赋值。否则会出现中断使能了但不响应或者响应优先级跟预期完全不一致的情况。3. 完整配置示例将 LTDC 放在非安全世界的实操步骤3.1 安全工程部分的处理安全工程的核心职责是“搭好舞台”配置好 SAU、GTZC、中断属性然后让非安全应用登场。下面给出一段基于 Cortex-M33 架构、ST 带 TrustZone MCU 的示意代码实际寄存器名请以你所用芯片的参考手册为准。#include tz_s.h void TZ_Init(void) { /* 1. 配置 SAU把需要开放给非安全世界的内存区域标记为非安全 */ SAU_Disable(); SAU_ConfigRegion(0, 0x20000000, 0x00020000, 1); /* 示例将 128KB SRAM 划分给非安全 */ SAU_ConfigRegion(1, 0x08000000, 0x00080000, 1); /* 示例将 512KB Flash 划分为非安全 */ SAU_Enable(); /* 2. 配置 GTZC/TZSC把 LTDC 和 DMA2D 划为外设非安全 */ TZSC_ConfigPeripheral(TZSC_PERIPH_LTDC, TZSC_NS); TZSC_ConfigPeripheral(TZSC_PERIPH_DMA2D, TZSC_NS); /* 3. 配置 RCC 中与显示相关的时钟控制允许非安全访问 */ TZSC_ConfigPeripheral(TZSC_PERIPH_RCC, TZSC_NS); /* 4. 将 LTDC 中断线配置为非安全其他外设中断依需求配置 */ NVIC_ConfigTargetNonSecure(LTDC_IRQn); NVIC_ConfigTargetNonSecure(DMA2D_IRQn); } /* 安全跳转函数跳转到非安全世界 */ void TZ_JumpToNonSecure(void) { /* 设置非安全世界栈指针和向量表 */ uint32_t ns_msp *(volatile uint32_t *)NON_SECURE_START_ADDR; uint32_t ns_reset *(volatile uint32_t *)(NON_SECURE_START_ADDR 4); __TZ_set_MSP_NS(ns_msp); __TZ_set_VTOR_NS(NON_SECURE_START_ADDR); /* 切换到非安全模式并跳转 */ typedef void (*pFunction)(void); pFunction NonSecure_ResetHandler (pFunction)ns_reset; NonSecure_ResetHandler(); }这段代码里的关键点是SAU 区域配置一定要在跳转之前完成否则非安全代码拿不到可用内存。外设归属配置顺序要固定比如你先配 RCC 再配外设这样后续外设时钟使能才合法。跳转前要清掉安全侧未处理完的中断标志避免把中断带到非安全世界。很多用 STM32CubeMX 的开发者会习惯性忽略安全工程这一步。实际上 CubeMX 能在生成代码时就把这些 TrustZone 属性先配置好生成 nTZ非安全工程和 secure 工程。你只需要在 secure 工程的TZ_Security_Init类似函数里补上自己的配置即可。3.2 非安全工程配置 LTDC 的完整流程跳转到了非安全世界接下来的工作还是很熟悉的 LTDC 配置顺序只是你需要始终牢记当前代码只能访问非安全资源所有显示相关地址、寄存器、中断都必须落在非安全世界。配置流程如下void MX_LTDC_Init(void) { /* 使用 HAL 初始化 LTDC 基本时序 */ hltdc.Init.HorizontalSync 40; /* HSYNC 宽度单位像素时钟 */ hltdc.Init.VerticalSync 9; /* VSYNC 宽度单位行 */ hltdc.Init.AccumulatedHBP 139; /* HSYNC HBP水平后肩累计值 */ hltdc.Init.AccumulatedVBP 39; /* VSYNC VBP垂直后肩累计值 */ hltdc.Init.AccumulatedActiveW 539; /* 有效显示区宽度 左边累计 */ hltdc.Init.AccumulatedActiveH 339; /* 有效显示区高度 上边累计 */ hltdc.Init.TotalWidth 569; /* 水平周期总长 */ hltdc.Init.TotalHeigh 349; /* 垂直周期总高 */ hltdc.Init.BackColor.Blue 0; hltdc.Init.BackColor.Green 0; hltdc.Init.BackColor.Red 0; hltdc.Init.PixelClock 8000000; /* 像素时钟频率根据屏规格选择 */ hltdc.Init.HSPolarity LTDC_HSPOLARITY_AL; hltdc.Init.VSPolarity LTDC_VSPOLARITY_AL; hltdc.Init.DEPolarity LTDC_DEPOLARITY_AL; hltdc.Init.PCPolarity LTDC_PCPOLARITY_IPC; if (HAL_LTDC_Init(hltdc) ! HAL_OK) { Error_Handler(); } } void MX_LTDC_Layer1_Config(void) { /* 配置图层 1窗口位置、颜色格式、显存地址 */ pLayerCfg.WindowX0 0; pLayerCfg.WindowX1 480; pLayerCfg.WindowY0 0; pLayerCfg.WindowY1 272; pLayerCfg.PixelFormat LTDC_PIXEL_FORMAT_RGB888; pLayerCfg.FBStartAdress (uint32_t)frame_buffer; /* 必须是非安全 SRAM 地址 */ pLayerCfg.Alpha 255; pLayerCfg.Alpha0 0; pLayerCfg.BlendingFactor1 LTDC_BLENDING_FACTOR1_PAxCA; pLayerCfg.BlendingFactor2 LTDC_BLENDING_FACTOR2_PAxCA; pLayerCfg.ImageWidth 480; pLayerCfg.ImageHeight 272; if (HAL_LTDC_ConfigLayer(hltdc, pLayerCfg, 1) ! HAL_OK) { Error_Handler(); } }在这个阶段最容易犯的一个错误是显存地址用的是链接脚本自动分配的普通变量而这些变量可能落在了安全 SRAM 区域。你需要确保frame_buffer这个数组被放到了你在安全工程划分的非安全 SRAM 区域里面通常是用__attribute__((section(.non_secure_buffer)))或者在链接脚本里单独的段来做。因为很多 IDE 默认链接脚本会优先使用片内 SRAM 起点地址而安全工程通常是从地址 0 开始分配安全资源一不小心就撞车。其次像素时钟PixelClock这个参数要结合你的屏驱动 IC 允许的范围来选择不是越大越好。算像素时钟最简单的办法是总周期宽度乘以总周期高度再乘以目标帧率。比如 480x272 分辨率水平周期 569垂直周期 349目标帧率 60Hz那你至少需要 56934960 ≈ 11.9MHz 的像素时钟。你配置的时候可以留一定余量但也不要高出太多否则屏幕可能直接闪屏甚至白屏。3.3 帧缓冲与 DMA2D 的实际操作显存区域配好之后千万别急着往 LTDC 上挂画面。先用最简单的办法测一下链路直接把整块显存填成纯色比如全部写 0x00 或 0xFF看看屏幕有没有反应。如果不花屏说明 LTDC 的时序和内存通路都是通的这个时候再做 DMA2D 搬图才有意义。如果填充纯色就花屏或者完全黑屏你得回去检查内存归属和时钟配置。DMA2D 搬图在 TrustZone 下的额外要求就是遵守内存归属规则。这里有一个非常实用的排查技巧无论你用 DMA2D 做什么操作做完之后不要立刻去刷新用户界面而是先读回目标地址的头几个像素值跟期望值比对一下。如果读回来的值不是你要的颜色那几乎可以确定是 DMA2D 访问了安全内存地址或源地址未对齐。下面是一个 DMA2D 搬运 RGBA8888 图片到显存的示意代码void DMA2D_CopyImage(uint32_t *src, uint32_t *dst, uint32_t size) { /* 确保 src、dst 都是非安全内存地址否则这儿会静默失败或产生总线错误 */ DMA2D-CR DMA2D_M2M; /* 内存到内存模式 */ DMA2D-FGMAR (uint32_t)src; DMA2D-BGMAR (uint32_t)dst; DMA2D-FGOR 0; DMA2D-BGOR 0; DMA2D-FGPFCCR DMA2D_RGB888; DMA2D-BGPFCCR DMA2D_RGB888; DMA2D-NLR (uint32_t)(height 16) | (uint32_t)width; DMA2D-CR | DMA2D_CR_START; while ((DMA2D-ISR DMA2D_FLAG_TC) 0) { /* 等待传输完成 */ } DMA2D-IFCR DMA2D_FLAG_TC; /* 校验读回目标地址前 4 个像素 */ uint32_t check0 *(volatile uint32_t *)dst; uint32_t check1 *(volatile uint32_t *)dst 1; if (check0 ! src[0] || check1 ! src[1]) { /* 说明存在访问属性/总线问题做错误处理 */ } }注意这里我特别写了读回校验。在普通 MCU 开发里大家都不习惯做这种校验因为 DMA2D 一旦正常就是百分百正常的。但 TrustZone 开启后“正常”变成了“地址合法才行”所以多一次校验能帮你节省大量的排查时间。我自己的工程里Debug 版本会开启这套读回校验Release 版本再关掉既不损失性能又能快速发现问题。4. 常见问题与排查技巧实录4.1 白屏 / 黑屏问题的系统排查白屏或黑屏现象往往是显示链路的第一道大坎。按照下面的排查顺序来大概率能定位问题。首先看时钟。检查 RCC 里 LTDC 时钟有没有被使能像素时钟是否正常输出。如果 LTDC 时钟源还没配好屏幕不会有任何反应。可以用调试器直接读 RCC 寄存器和 LTDC 的 GCR 寄存器如果寄存器读出来全 0或者写入的值立即丢失说明寄存器访问权限有问题那大概率是外设安全属性没配好。其次看显存地址。把断点停在显示主循环里查看显存地址处的值确认填充的像素数据真的写进内存了。如果写入正常屏幕还是黑屏就要怀疑 LTDC 的图层配置比如窗口坐标是否合理、透明度 Alpha 是否为零、混合因子是否设置正确。这一部分仍然需要逐项核对。最后看时序参数。用示波器抓一下 LCD_CLK、HSYNC、VSYNC 引脚看是否有波形输出。如果有波形说明 LTDC 核心已经在工作了问题大概率出在图层配置或者数据通路如果完全没有波形就回到时钟和外设使能上排查。我个人的经验是白屏问题 80% 出在外设归属和内存归属15% 出在时钟树只有 5% 才是时序参数写错。所以别一上来就死磕屏参那样会浪费很多时间。4.2 DMA2D 花屏和“假成功”问题DMA2D 花屏有几个典型表现画面错位但颜色正常、颜色完全不对但图形结构清晰、画面只有一半正常另一半是噪点。逐个排除下来根因往往是地址访问属性或对齐问题。其中“画面错位但颜色正常”最可能是你的源图像宽度跟目标窗口宽度不匹配DMA2D 的偏移FGOR、BGOR设置不当前。这种情况与 TrustZone 关系不大。真正和 TrustZone 强相关的是“颜色不对但结构清晰”和“半屏噪点”这多半是 DMA2D 在搬运过程中访问了安全地址导致某些行数据没搬进来或者读回来的是总线上被拉高的垃圾数据。另一个常见的坑是 DMA2D 中断。如果你用了 DMA2D 完成中断要记得把 DMA2D_IRQn 也配置成非安全中断。不少人的代码把 LTDC 中断配置了但忽略 DMA2D 中断结果搬图动作是发起了但完成回调永远不执行程序一直卡在等待标志位的地方。这种问题看代码怎么等都找不出来只有看中断配置才能发现。4.3 中断相关问题的排查方向如果你配置了 LTDC 行中断但HAL_LTCD_LineEventCallback一直不触发请按下面这个顺序查检查 NVIC 中 LTDC 中断线是否被设置成非安全属性只有非安全中断线才能在非安全代码里使能。检查优先级分组。安全侧和非安全侧使用的优先级分组要一致否则非安全侧设置临界区、关中断时可能无法正确屏蔽安全侧中断。检查你使能的中断类型。LTDC 有行中断和全局中断两者在 NVIC 里可能是同一个 IRQn但触发来源不同。确保在LTDC_LIPCR里正确写入了比较行号且LTDC_IER里对应使能位已经置 1。最后确认你注册的回调函数是不是在非安全代码段里。如果回调函数被链接到了安全内存区域非安全中断向量表里指向它就会出问题。中断这一类问题最有效的调试手段是在中断入口打断点。如果你的调试器支持 TrustZone 调试模式能够同时调试安全和世界安全代码那是最好不过的。如果不行就在中断向量表处打断点看中断是否确实响应了。4.4 跳转非安全代码就 HardFault 的问题安全工程跑得好好的一跳转到非安全工程就 HardFault这是 TrustZone 开发中极其常见的现象但它也会导致显示功能完全不能工作因为非安全侧的显示任务根本没跑起来。排查优先级最高的项目是非安全代码的向量表和栈指针有没有正确设置。非安全代码在复位时需要读取首地址作为 MSP_NS读取偏移 4 的地址作为复位向量。如果这两个值没有正确初始化跳转过去之后 CPU 连第一条指令都执行不对。其次是 SAU 和内存属性。如果非安全代码的加载地址或者运行地址落在了安全区域CPU 去取指令时就会触发安全违规。记住不只是显存需要非安全所有的代码、数据、堆栈只要非安全代码会访问都必须放到非安全区域。还有一点容易忽略浮点单元 FPU。如果安全世界使用了 FPU而跳转时未正确保存浮点上下文或者非安全世界启用了 FPU 但状态不一致也会导致 HardFault。你可以查看 CPSR/CONTROL寄存器的 FPU 相关位来确认状态。提供一个经验法则跳转前把安全侧所有已使能的中断全部屏蔽清掉挂起中断标志等非安全世界完全初始化后再恢复中断。这样能最大限度避免因为中断残留导致的 HardFault。5. 调试 TrustZone 工程的一些额外心得5.1 用双工程结构管理代码配置 TrustZone 后你的软件架构必须拆成安全工程和非安全工程两个部分。安全工程里放的是系统初始化、安全服务、以及需要的 NSC 回调函数非安全工程里放的是业务、显示驱动、网络协议栈等普通应用代码。我个人推荐用 CMake 来管理这种双工程结构因为它能非常清晰地控制链接脚本和编译选项。下面是一个非常精简的目录结构示意project/ ├── CMakeLists.txt ├── secure/ │ ├── tz_s_init.c │ ├── tz_nsc_func.c │ └── secure_link.ld └── non_secure/ ├── main.c ├── ltdc_driver.c ├── dma2d_driver.c └── ns_link.ld安全工程的链接脚本要定义好非安全世界区域放在 CPU 起始地址的非安全向量表、可用的非安全 SRAM 区域、NSC 区域。非安全工程的链接脚本则不能使用安全区域所有内存段都必须落在安全工程划分出的非安全区域内。如果你用的 IDE比如 STM32CubeIDE你在创建 TrustZone 工程时它会帮你生成这套结构但底层原理还是上面说的这些。5.2 调试器的正确打开方式在 TrustZone 开启的芯片上调试你需要用支持 TrustZone 的调试器。ST-Link 搭配较新版本的 CubeIDE默认就能处理这类场景但你要有意识地区分你当前正在调试的代码是安全世界还是非安全世界。在调试器里查看SAU寄存器、GTZC 寄存器、内存访问属性能让你快速定位很多隐蔽问题。我的经验是遇到问题先别急着翻代码先在调试器里把以下几个寄存器的值读出来看一眼基本能确定问题范围SAU CTRL 寄存器确认是否使能。SAU 区域配置确认你划分的非安全区域是否还在。LTDC 的 GCR 寄存器确认 LTDC 是否已被使能。DMA2D 的 CR 和 ISR确认最后一个传输操作的返回值和处理状态。这些寄存器值一旦和你预期的不同问题就找到了如果全部正常再回到软件执行流程上排查。5.3 安全与非安全调试切换的小技巧你在实际操作中一定会遇到一个痛点代码断点打在了安全代码里但程序停在非安全世界断点不生效反过来也是一样。这是因为 TrustZone 下安全调试和非安全调试的断点资源是分开的。调试时尽量把断点打在对应世界的函数入口处而不是一条普通语句上。拿跳转函数举例你可以在安全工程的TZ_JumpToNonSecure函数入口打一个断点确认安全工程跑到这里时一切正常然后在非安全工程的main函数入口打一个断点看跳转过去后能不能顺利进入主程序。这样分段确认能快速定位是哪一步出了问题。另外一个实用技巧是在安全侧放一个专用的串口调试通道。你可以把某个 UART 配置成安全外设安全代码往这个串口打印日志非安全侧用另一个 UART 打印业务日志。这样两边世界各自输出各自的日志问题发生在哪一侧一目了然。如果没有条件用两路串口那就在一个串口上加上世界标识前缀比如 [S] 和 [NS]也能达到类似效果。5.4 关于安全侧处理显示回调的进阶方案有些项目虽然主要显示逻辑在非安全侧但安全侧希望在特定时刻去更新屏幕比如安全告警时需要立即全屏显示警告界面。这种情况下你不想让非安全代码直接操作显示外设又希望显示链路本身属于非安全侧怎么办我采用的方案是在安全侧写一个 NSC 函数函数内部实现一个针对显示控制器的“安全代理调用”。非安全侧调用这个函数时参数里带上你要写入的寄存器偏移和值或者更抽象一点带上你要执行的显示操作码比如“全屏填充红色”、“切换到安全页面”。安全侧的这个函数在执行时会主动切到非安全状态去操作 LTDC执行完再切回来。这样既保留了显示外设的非安全属性又实现了安全世界对显示内容的控制。当然这种设计需要你对状态切换和 NSC 机制非常熟悉否则容易埋坑。入门阶段我建议先用最简单的方案把 LTDC 完全放在非安全侧安全侧通过一个 NSC 函数向非安全侧发送“显示请求”由非安全侧的业务代码去实际执行。这种方案架构清晰、调试也方便等系统跑顺了再考虑更复杂的安全代理方案。写到这里文章也接近尾声了。最后再分享一个小技巧在真正调试 LTDC 和 TrustZone 组合之前先用厂家提供的示例代码把屏幕点亮确认开发板和屏幕硬件本身没有问题然后才把示例代码逐渐替换成自己的 TrustZone 配置。不要一上来就同时挑战 TrustZone 和显示驱动这两个大块那样出了问题你根本分不清是屏没配好还是安全属性没配好。我自己的习惯是先跑通纯色填充然后跑 DMA2D 搬图最后才上 GUI 框架每一步都确认无误再往下一步走才把带着 TrustZone 的显示链路稳稳当当地跑起来。