ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32CubeL5 TFM入门实战:从零跑通TrustZone安全世界

2026/8/29 14:18:07 拓冰建站 浏览量
STM32CubeL5 TFM入门实战:从零跑通TrustZone安全世界 最近在做一款基于STM32L552的物联网网关板子刚点亮的时候我还是用最原始的方式整个Flash裸奔外设随便初始化固件直接烧进去就跑。后来产品要过PSA Level 2认证才真正开始研究STM32CubeL5里集成的TFM方案。如果你也正在看这颗芯片应该知道STM32CubeL5是ST为L5系列提供的官方软件包而TFMTrusted Firmware-M是Arm主导的、面向Cortex-M平台的可信固件框架。两者配合之后芯片内部的TrustZone才真正被用起来。这篇文章我会从零开始完整记录一遍STM32CubeL5里TFM应用程序的入门过程包括底层概念、工程生成、安全与非安全两侧的代码结构、IPC调用方式以及一些官方文档里不会写的坑。适合刚拿到STM32L5、想跑通第一个TFM实例的开发者也适合那些之前只在MCU上做过裸机开发、第一次接触TrustZone的朋友。文章里用到的操作我都在NUCLEO-L552ZE-Q开发板上实际跑过理论上也适配其他STM32L5系列型号。1. 在做TFM之前先搞懂这几个底层概念1.1 STM32CubeL5软件包到底给了我们什么先说STM32CubeL5。它是ST针对STM32L5系列推出的嵌入式软件包基于STM32Cube HAL驱动层构建里面提供了外设驱动、中间件、例程工程以及低功耗管理组件。跟F4、H7这些老系列相比L5最大的区别在于内核换成了Arm Cortex-M33并且带TrustZone扩展这也就是ARMv8-M架构中最重要的特性之一。STM32CubeL5软件包本身在ST官网可以直接下载也可以通过STM32CubeMX在线拉取。安装好之后你会发现它的目录结构跟其他Cube包差不多有Drivers、Middlewares、Projects等几个大目录。不过多了一个非常关键的东西——Middlewares/ST/trustedfirmware拆开看就是TFM源码具体版本因包版本而异我手头这个版本对应TFM 1.5。也就是说ST已经帮我们移植好了TFM在L5上的运行环境你要做的不是从零移植而是在这个基础上做安全设计。这里有一个很多人容易忽视的点STM32CubeL5软件包里的TFM并不是一个独立的开发项目它和安全侧Secure工程、非安全侧Non-Secure工程三个部分是一套联动的关系。ST把整个系统分成了两个独立的CM33工程安全侧叫CM33_S非安全侧叫CM33_NSTFM位于安全侧内部。后面在CubeMX里我们会看到这个过程。1.2 TFM在STM32L5上扮演什么角色TFMTrusted Firmware-M是一个面向Cortex-M处理器的安全固件框架它提供了安全启动、安全存储、密码学算法、初始 attestation 等一堆安全服务。你可以把它理解成一套运行在“安全世界”里的系统固件普通用户代码跑在“非安全世界”两者通过硬件级别的TrustZone隔离。这个架构最终目标是对齐PSA认证体系如果产品需要过安全认证用TFM是最顺的路。在实际的STM32L5工程里TFM承担的具体工作是上电后它最先运行完成硬件初始化然后加载一组被称为“Secure Partition”的安全服务分区最后跳转到非安全世界把你写的Application拉起来。所以TFM不只是“安全启动”这几个字它还是整个系统的守护者。L5上常见的几种TFM运行模式包括IPC模型非安全侧应用通过IPC接口请求安全服务每个服务运行在独立分区中隔离程度高代码更规范是TFM官方主推的模式。Library模型非安全侧直接链接安全库调用安全函数实现简单但隔离性弱适合早期原型验证。API模型以C库函数形式暴露安全服务多数情况下需要配合IPC分区模型使用。STM32CubeL5里默认的TFM配置是IPC模型。我在第一次看到工程里那一堆分区代码时也头大但你不用把所有源码都吃透只要掌握调用边界在哪里就够了。1.3 TrustZone隔离模型为什么“安全世界”不是一句空话TrustZone这个词在手机上已经普及很多年了但在MCU里落地的方式不太一样。STM32L5上的TrustZone把Flash、SRAM、外设总线、中断都划分成安全与非安全两个区域由内核的SAU安全属性单元和芯片级的TZSC、TZPC等模块共同控制。我习惯用一个生活类比来解释整颗芯片就像一栋写字楼安全世界是核心机房普通程序只能在大堂和公共区域活动想去机房必须刷门禁卡。门禁卡就是NSNon-Secure调用机制核心机房里的设备比如带密钥的加密引擎、安全存储Flash区外人摸不到。硬件上M33内核有一条指令SG专门用来做安全切换非安全代码执行到特定地址时会触发异常安全侧响应后CPU才能进入安全世界执行。整个过程硬件保证安全性不像软件方案那样可以绕过。理解这个分层非常关键因为你不搞清“哪个固件跑在哪个世界”后面写代码很容易踩坑。比如我在调试时反复遇到的一个现象非安全侧代码访问某个外设寄存器直接报HardFault原因很简单——那个外设被安全侧独占非安全代码根本没有访问权限。这不是配置错了而是TrustZone隔离本来就该这样。2. 开发环境准备与工程生成2.1 硬件选型与开发板准备做TFM开发建议先准备一块官方开发板。ST的NUCLEO-L552ZE-Q是最常见的选择板载ST-LINK/V3调试器可以直接调试TrustZone的双世界。如果手头没有开发板用NUCLEO-L552ZE或NUCLEO-L4A6ZG其实也可以但要注意部分低容量型号内置Flash和SRAM较小生成TFM工程后可用空间会很紧张。开发板也不用特意买新的我这里是之前项目留下的NUCLEO-L552ZE-Q板载L552ZE1MB Flash256KB SRAM跑TFM默认配置完全没问题。如果要测试PSA高级特性比如安全存储、初始认证等也建议选512KB以上Flash的型号因为TFM固件本身就要吃掉大概几十KB的空间。连接ST-LINK到电脑之前建议先升级一下ST-LINK的固件。固件版本太旧时ST-LINK无法识别TrustZone相关的烧录区域会导致后面调试连不上。ST的STM32CubeProgrammer在连接时其实会主动提示更新但有些版本不提示所以我习惯自己动手更新。2.2 工具链版本选择开发工具链方面ST官方推荐STM32CubeIDE。这是我实测最省事的方式因为它集成了CubeMX工程生成、GCC编译工具链、ST-LINK调试并且直接支持TFM双工程的项目结构。版本上我强烈建议用较新的IDE。我最早用STM32CubeIDE 1.12配STM32CubeL5 1.4.0编译TFM时遇到了GCC链接脚本不兼容的问题折腾了半天才查出来是编译器版本太老。换成STM32CubeIDE 1.15以上之后GCC版本到了11.3问题就消失了。如果不想用STM32CubeIDE也可以用Keil或IAR但对新手来说配置复杂度会高一截不推荐第一次就碰。除了IDE还需要安装STM32CubeProgrammer一是用来烧录二是用来查看TrustZone相关选项字节后面调试安全配置会频繁用到。另外串口调试助手也是必需品因为TFM的日志输出和你的应用日志都会走串口我习惯用115200-8-N-1的配置。2.3 用CubeMX生成带TFM的初始工程打开STM32CubeMX在芯片选择界面输入STM32L552ZE选择NUCLEO-L552ZE-Q开发板即可。进入图形配置界面后最重要的配置在Project Manager里的TrustZone设置。第一步在左侧“Security”分类下找“TrustZone”确认使能选项已经打开。这个操作会初始化SAU、TZSC等模块的默认状态同时解锁安全内存布局编辑。第二步配置一个可选组件中间件里的TFM。CubeMX在“Middleware and Software Packs”里会显示“TF-M”选项勾选它会展开可选的TFM配置项。默认配置一般就够跑通我建议先保持默认不要在第一次就改安全分区大小和内存布局。这里有几个参数稍微解释一下TFM Profile影响安全服务的范围默认是PROFILE_MEDIUM已经包含加密、安全存储和初始化认证足够起步。最大分区数影响IPC调用时并发能力一般默认就行。安全侧堆栈大小影响TFM内部任务栈如果你在安全分区里写复杂逻辑后期可能需要增大。第三步如果你打算自己写非安全侧业务代码记得在Project Manager里勾选“Generate Underlying C files for Non-Secure Application”。其实现在CubeMX生成的工程默认就是双工程CM33_S和CM33_NS两个子工程一起生成不用额外设置。生成时会自动创建好两个工程文件再把两个工程链接到同一个workbench里编译顺序也会自动管理。生成完工程后你会在目录里看到类似这样的结构Project/ ├── STM32CubeIDE/ │ ├── project_CM33_S/ │ └── project_CM33_NS/ ├── Core/ ├── Secure/ │ ├── CM33_S/ │ └── CM33_NS/ └── ...这里Secure/CM33_S是安全工程Secure/CM33_NS是非安全工程。注意名字虽然都叫CM33但一个烧进安全区域一个烧进非安全区域千万不要混淆。顺带说一句CubeMX对TFM的可视化配置已经做得比较完善但在某些版本里你仍然需要在“.ioc”文件里手工维护一小部分内存映射。如果你在生成后打开工程发现SRAM地址错乱建议先检查是否选择了适合你芯片型号的Device配置。3. 从生成代码到第一个TFM实例跑起来3.1 认识TFM代码结构生成后的代码量很大直接打开会让人有劝退的冲动。不要慌你只需要先识别几个核心区域。安全侧工程CM33_S里app目录存放安全分区应用程序tfm目录是TFM的核心代码包括启动、中断管理、IPC机制等。以默认工程为例你会看到这些安全服务分区TFM Crypto提供加解密、哈希、签名等密码学API。TFM Secure Storage提供密钥和数据的持久化安全存储服务。TFM Initial Attestation提供设备身份证明服务返回一个token用于证明设备身份。TFM Platform / ITS内部可信存储、平台相关服务。这些分区之间通过TFM内部的消息分发机制通信外部非安全应用通过“SVC PendSV”机制触发安全调用请求最后走到对应分区执行。非安全侧工程CM33_NS里的代码就比较接近传统MCU开发了。你能看到main.c里既有注释说“This is the Non-Secure application”也有HAL初始化和外设配置。第一次跑的时候我会建议先把非安全侧所有外设配置简化只保留一个串口打印方便看日志。3.2 编译整个工程并理解输出文件TFM双工程的编译顺序很讲究。安全侧必须先生成好非安全侧依赖安全侧导出的接口定义和库文件。用STM32CubeIDE打开workbench后如果你用默认配置IDE已经配置好了CM33_S和CM33_NS的依赖关系直接Build All即可。如果你在命令行编译需要先执行# 以Makefile方式为例 make -C Secure/CM33_S nx_secure make -C Secure/CM33_NS 2/dev/null编译产物默认放在Secure/CM33_S/Debug/bin/tfm_s_signed.bin Secure/CM33_NS/Debug/bin/tfm_s_ns_signed.bin或者类似路径下。注意这里有两个关键文件tfm_s_signed.bin安全侧固件烧录到Flash起始地址通常0x0C0000或0x08000000取决于你的配置这个是安全世界的镜像。tfm_s_ns_signed.bin包含安全侧和非安全侧的完整镜像头部有签名信息。如果只想烧一个文件用这个最省事。我在第一次编译时差点烧错把tfm_s_signed.bin当成完整固件烧进去结果上电后非安全侧应用完全不执行。后来才意识到在TrustZone模式下非安全应用必须放在安全固件之后且入口地址有严格约定烧录顺序错了或者地址错位系统起不来是必然的。3.3 烧录与首次运行烧录时我建议直接用STM32CubeProgrammer而不是IDE自带的下载功能因为你能更清楚地看到烧录地址和Option Bytes。连接到开发板后先检查TrustZone相关选项字是否配置成“enabled”。在STM32CubeProgrammer的“Option Bytes”页面找到TrustZone使能区域确保它是启用状态。如果你用的是全新的板子默认可能是关闭的这时候直接烧TFM会出现奇怪的问题比如运行到安全入口就死掉或者调试器报“cannot access target”。我踩过这个坑所以现在每次拿到新板子都会先检查这一步。烧录方式有两种方式一烧录完整镜像推荐新手STM32_Programmer_CLI -c portSWD -w tfm_s_ns_signed.bin 0x08000000这个命令会把包含安全和非安全的整体镜像写入起始地址烧录后复位即可。方式二分步烧录STM32_Programmer_CLI -c portSWD -w tfm_s_signed.bin 0x0C0000 STM32_Programmer_CLI -c portSWD -w ns_app.bin 0x08040000分步烧录适合你只改了非安全侧代码、不想重新烧安全固件的场景能节省大量时间。烧录完成后复位开发板串口输出应该能看到TFM的启动日志默认包括TFM版本信息、固件签名状态、启动跳转信息最后非安全侧应用开始运行。如果串口能看到类似[INF] Starting bootloader、[INF] Jumping to non-secure image这样的输出就说明系统已经跑通了接下来就可以开始魔改自己的应用了。4. 非安全侧应用开发如何在你自己的代码里调用安全服务4.1 SCMI与IPC模型理解跑通默认工程后你会发现非安全侧main函数只是个空壳真正有价值的是它与安全侧通信的那套机制。TFM官方主推IPC模型所以搞清楚IPC怎么用基本等于拿到进入安全世界的钥匙。在STM32CubeL5的TFM实现里非安全侧应用通过一套头文件和库函数来调用安全服务。你不需要关心底层SVC调用细节只要调用上层接口API即可。以标准PSA接口为例比如psa_initial_attest_get_token()获取设备初始认证token。psa_import_key()、psa_sign_message()使用安全侧密钥管理服务。psa_storage_get()、psa_storage_set()安全存储数据读写。这些接口声明在psa/crypto.h、psa/initial_attestation.h、psa/storage_common.h等头文件里。编译时非安全工程会自动链接对应库你在代码里直接#include进去调用就行。调用流程其实可以归纳为四个步骤在非安全侧包含对应PSA接口头文件。初始化TFM通信上下文ST的示例代码中通常会调用tfm_ns_interface_dispatch()之类的封装函数不过新版CubeMX生成代码里这一步一般是自动完成的。调用具体PSA接口函数。校验返回值并处理数据。IPC模型底层是线程安全的消息队列所以你可以放心在非安全侧RTOS多线程里调用这些API不需要自己加锁。这一点我实际验证过在FreeRTOS两个任务里同时调用安全存储接口数据一致性没问题。4.2 编写非安全侧客户端的实操示例我用一个最简单的例子演示在非安全侧调用安全存储服务把一个传感器校准值写入内部安全存储。这个场景很典型校准数据不想被普通代码读取和篡改放到安全世界里正合适。#include psa/storage_common.h #include string.h void save_calibration(uint32_t value) { psa_storage_uid_t uid 0x1001; uint8_t data[4] {0}; psa_status_t status; data[0] (uint8_t)(value 0xFF); data[1] (uint8_t)((value 8) 0xFF); data[2] (uint8_t)((value 16) 0xFF); data[3] (uint8_t)((value 24) 0xFF); status psa_storage_set(uid, sizeof(data), data, 0); if (status ! PSA_SUCCESS) { /* 处理错误 */ } } uint32_t load_calibration(void) { psa_storage_uid_t uid 0x1001; uint8_t data[4] {0}; size_t length 0; psa_status_t status; status psa_storage_get(uid, 0, sizeof(data), data, length); if (status ! PSA_SUCCESS || length ! sizeof(data)) { return 0; } return (uint32_t)(data[0] | (data[1] 8) | (data[2] 16) | (data[3] 24)); }这段代码在main()里直接调用即可。如果你编译报错找不到psa_storage_common.h说明非安全工程的头文件路径没配置好需要在CM33_NS工程的Include Paths里把Middlewares/ST/trustedfirmware/interface/include等目录加进去。第一次跑这个例子时我犯了一个低级错误没有先初始化安全侧就在非安全侧main里调用psa_storage_set结果直接卡死。其实CubeMX生成的默认启动流程里非安全侧main执行前安全侧已经完成初始化了所以不需要也不会自动初始化但如果你把非安全侧工程单独拿出来编译并另外烧录那就必须保证安全侧镜像先跑起来。这一点很多人会漏。4.3 安全侧服务扩展的基本思路默认的四个安全服务分区覆盖了大多数安全需求但如果你要往安全侧加自定义逻辑比如把设备证书私钥放在安全世界用一个专属算法管理那就需要扩展安全分区。这个工作量比较大涉及TFM分区清单配置、分区内代码编写、非安全侧接口声明等步骤。STM32CubeL5的TFM工程里已经内置了自定义分区的模板在SECURE/CM33_S/app目录下你可以参考一个叫tfm_ps_test_service之类的测试分区复制它的配置方式。核心要改的文件包括tfm_manifest_list.yaml注册新分区名、分区ID、服务ID。tfm_protected_storage.yaml或类似yaml文件定义分区支持的API。分区源文件实现具体服务和消息处理逻辑。非安全侧接口头文件在interface/include下添加调用函数声明。这里我要提醒自定义安全分区时最好把内存布局先重新算一遍。TFM允许每个分区配置独立的栈空间如果配置过大超出了安全侧SRAM预算编译能过但运行时会踩内存越界表现为随机死机。调试这类问题非常痛苦远不如一开始就把栈大小定准。5. 常见问题与排查技巧实录5.1 启动就HardFault先查这四处TFM工程跑不起来很多情况下不是代码逻辑问题而是配置或烧录层面的问题。我把这段时间调试遇到的高频问题整理成一个速查表照着排查命中率很高。现象可能原因处理方式上电后DBG显示PC停在0xFFFFFFFE非安全入口地址错误检查生成的镜像是否包含非安全侧确认使用完整镜像烧录启动日志只有TFM bootloader信息没有跳转非安全侧非安全镜像未找到或无有效签名检查非安全镜像是否签名确认烧录地址和Linker脚本一致串口无任何输出时钟或串口引脚配置未使能确认非安全侧main.c中串口初始化代码也有可能是安全侧UART被隔离非安全侧无法访问调用PSA API时卡死IPC消息队列未初始化或分区未启动确认TFM固件版本和工程版本一致增加调试断点查看调用栈随机HardFault且SystemHandler在非安全侧安全侧分区栈溢出调大对应分区栈大小通常需要查YAML配置第一个“PC停在0xFFFFFFFE”的坑我自己印象最深刻。当时我改了非安全应用的链接脚本把FLASH起始地址往前挪了一点想着能省空间。结果烧录后系统连启动都过不了查了半天才发现TFM会验证非安全镜像入口地址地址不对就不会跳转。5.2 调试器连不上或烧录不进去TrustZone模式下调试比普通MCU麻烦不少。当你启用TrustZone后调试器的访问权限也会被安全属性限制。ST-LINK默认能访问全部内存但如果你在安全侧配置中把调试接口限制成“Secure Only”那么非安全侧代码在调试时可能无法访问安全内存区域也读不到安全侧寄存器。如果遇到调试器连不上目标板先用STM32CubeProgrammer确认连接如果能连上再检查Option Bytes里的调试相关配置。注意以下几点确保使用了ST-LINK/V3V2虽然也能用但在TrustZone调试时体验差一些经常断点失效。在IDE调试配置里勾选“Enable TrustZone”相关的选项否则调试器不知道目标区被安全划分。如果固件启用了防回读保护RDP Level 1或2调试器只能连接无法读Flash这种情况需要先解除保护但解除保护会擦除全片Flash慎用。我处理过一次客户寄回的问题板子死活连不上调试器结果发现对方把RDP级别调到了2任何连接都被拒绝只能进入Bootloader模式恢复。所以产品出厂前如果要启用RDP务必考虑好售后调试方案。5.3 性能、功耗和证书相关的经验TFM并不是零成本的。每一次PSA API调用都要经过SVC陷入、安全世界调度、参数校验、服务执行、返回这几个过程实测下来一次简单的PSA存储读写比直接写内部Flash慢2至3个数量级。所以设计上要想清楚安全服务适合低频、数据量小的操作如果非安全侧要高频处理大量数据比如传感器采样流放在安全侧并不现实。功耗方面TFM在运行安全服务时M33会进入安全世界并执行相对复杂的固件逻辑功耗自然高于纯裸机应用。低功耗产品要做功耗评估时不要把非安全侧的停止电流直接当整机电流还要看TZSC、TZPC等安全外设的配置是否会阻止低功耗模式退出。ST在STM32CubeL5的例程里提供了基于TFM的低功耗示例但我在移植到自己板子上时发现如果安全侧配置了非安全唤醒源需要同时检查安全侧异常处理代码否则唤醒后安全侧任务调度会乱。还有一个容易被忽略的点是固件签名和证书。TFM启动验证非安全镜像的签名时需要公钥烧录进安全侧。默认工程使用的是ST提供的测试密钥量产时必须替换成自己的密钥否则别人用ST的公开测试固件就能把你的设备刷成另一个版本。替换密钥后要重新签名非安全镜像这个流程建议在CI脚本里固化避免每次出包手动操作导致签名不一致。另外提一句在写安全侧代码时要特别注意堆的大小。TFM本身有内存保护机制但它是针对分区的不会阻止你自己把全局变量写爆。我用过一段时间的TFM后养成了一个习惯每次修改安全侧逻辑都会在编译后检查.map文件里安全SRAM的使用率超过70%我就会想办法精简逻辑防止后期集成时莫名其妙出Bug。整个项目跑通之后我最大的体会是TFM本身的复杂度远没有想象中高真正难的是理解CPU和安全世界之间的边界在哪以及在哪一层做隔离决策。刚接触时我总想着把所有东西都塞进安全世界后来才发现这不是好设计。安全世界只应该保存必须保密的密钥和核心逻辑其他能放到非安全侧的尽量放出去否则系统性能和功耗都会变得很难看。现在用STM32L5做TrustZone方案我的流程已经固化成了先跑官方TFM示例再按需裁剪分区最后才写自己的非安全业务整个过程一周左右就能完成一遍。希望这份笔记能让你少走一些我走过的弯路。