1. 项目概述:X_CUBE_SBSFU 到底解决了什么问题
做嵌入式开发的工程师,尤其是做过量产产品维护的,应该都有过这种经历:产品已经交付到客户手上了,结果发现固件有 bug,或者需要升级功能。这时候你面临两个问题——怎么把新固件安全可靠地送进设备里,以及怎么防止别人把恶意固件刷进去。X_CUBE_SBSFU 这个软件包,就是 ST 针对这两个痛点给出的官方解决方案。
SBSFU 全称是 Secure Boot and Secure Firmware Update,也就是安全启动和安全固件更新。它不是一个单点工具,而是一整套运行在 STM32 平台上的软件框架,从 Bootloader 到固件签名、加密、防回滚、生命周期管理,全给安排好了。你拿到这套框架之后,不需要从零去设计安全方案,只需要做配置和集成,就能给自己的产品加上一套相对完善的安全启动和远程升级能力。
这个软件包适合谁来用?两种人最适合。第一种是正在做量产产品、需要给设备加安全机制的嵌入式工程师,无论你做的是 IoT 设备、工业控制器还是消费电子,这套方案都能直接用。第二种是对安全启动机制感兴趣、想深入理解"信任根""签名链"这些概念具体怎么落到代码里的开发者。如果你只是想把固件加密一下防止别人读取,SBSFU 也能覆盖这个需求,但它的核心价值在启动验证和更新保护上。
我第一次接触这个软件包的时候,翻了半天文档才搞明白它内部是怎么转的,UM2262 这份应用笔记写得不算特别长,但信息密度很高,如果没有人帮你梳理一遍,很容易卡在某个细节上出不来。这篇文章就是把我踩过的坑、理清的思路、实际操作过的流程完整写出来,给你一条可以直接照着走的路径。
2. 整体设计思路拆解:SBSFU 的架构和运行机制
2.1 从一段代码到三重保护:SBSFU 的核心架构
看 SBSFU 的第一件事,是要跳出"写代码"的思维,先建立起"分层保护"的概念。这套软件包在 MCU 上跑起来之后,Flash 里不再只有一个孤零零的应用程序,而是被划分成了几个职能不同的区域。
首先是 Bootloader,这是系统上电后最先执行的代码,它很小,职责也很单纯——验证下一级固件的合法性和完整性。其次是 SFU(Secure Firmware Update)部分,负责接收、解密、校验新固件并写入指定区域。最后才是你真正要跑的业务代码,也就是 User Application。
这三层的关系相当于一所公寓的门禁系统:Bootloader 是小区大门,SFU 是单元门,User App 是你家的房门。三道门各有各的锁,而且钥匙都是独立的。就算恶意攻击者拿到了单元门的钥匙,没有小区大门的权限依然进不来。这就是 SBSFU 的核心理念——不把所有安全筹码押在某一层上。
2.2 信任根到底"根"在哪里
理解 SBSFU 绕不开一个概念——信任根(Root of Trust)。很多开发者第一次看到这个词的时候会觉得玄乎,其实说白了就是一句话:总得有一个"绝对可信"的起点,才能一级一级验证下去。
在 SBSFU 的设计里,这个信任根就是烧录在 STM32 OTP(One-Time Programmable)区域里的公钥。OTP 区域的特性是一次性写入、之后永远不可修改,所以只要出厂时把公钥烧进去了,这个信任根就不可能被恶意固件篡改。Bootloader 每次启动时,都会拿着这把公钥去验证固件签名。签名通过了,才往下走;签名不通过,直接停在这里,系统起不来。
这个设计思路很值得借鉴。很多做安全方案的人容易犯一个错误,就是把密钥直接藏在代码里或者放在普通 Flash 的某个角落。这种做法的安全强度等于零,因为只要攻击者能读 Flash,密钥就暴露了。SBSFU 把信任根放在 OTP 里,等于是从硬件层面锁死了攻击路径。
2.3 密钥体系:一把公钥和一把私钥的分工
再往下挖一层,就到了密钥管理。SBSFU 默认使用 ECC 非对称加密算法(部分器件支持 RSA),整个体系里有两种密钥需要搞清楚:私钥和公钥。
私钥保存在 PC 端,也就是你的开发电脑上,永远不应该出现在设备端。每次打包固件时,用私钥对固件进行签名。公钥则会被放进 OTP 区,即我们前面说的信任根。设备端验证固件时,用公钥解签名。如果签名是用正确私钥生成的,那么公钥一定能解出正确的哈希值,固件内容也和哈希值匹配,验证即通过。
我用一个生活中的场景来类比:私钥是你的印章,公钥是别人手上的印章印鉴。你盖了章的文件,别人拿印鉴一比对,一模一样,就知道这文件确实是你发出的。私钥丢了可以重新生成,但公钥一旦烧进 OTP 就改不了了,所以一定要保管好 PC 上的私钥文件,最好放到加密容器或者硬件安全模块里。
2.4 分区布局和生命周期:固件更新背后的策略
SBSFU 的 Flash 分区不是随意划分的,它定义了严格的地址布局。以 STM32L4 系列为例,Flash 从起始地址开始依次是:Bootloader 区域、SFU 区域、Slot 0 和 Slot 1(两个固件槽)、以及用户数据区。Slot 0 和 Slot 1 的存在是为了实现 A/B 双备份更新策略。
A/B 策略是什么意思?就是系统永远维护两份固件,一份是当前正在运行的,另一份是待更新的。升级时,新固件写入非活跃的 Slot,校验通过后切换启动指针,设备重启后运行新固件。如果新固件启动失败,系统自动回退到旧固件,更新流程不会把设备变砖。
在 STM32 上并非所有型号都有足够的 Flash 让你跑 A/B 方案,部分资源紧张的芯片可能需要使用单 Slot + 压缩传输的方案,但 SBSFU 的框架本身已经把这几种策略都考虑进去了。你选择的 MCU 型号不同,可用的安全策略也不同,这一点在选型时要提前确认。
3. 环境准备与安装流程:从 MCU 选型到软件包落地
3.1 硬件平台兼容性自查:别在第一步就掉坑
我见过太多人第一步就卡死——下载了 SBSFU 却发现自己的板子根本不支持。SBSFU 对硬件是有要求的,不是所有 STM32 都能跑,核心要求有两条:一是 MCU 要有硬件加密引擎(CRYP)或足够算力来做签名验证,二是 Flash 容量要够放 Bootloader 加两大份固件。
官方明确支持的系列包括 STM32L4、STM32L5、STM32H7、STM32U5 以及部分 STM32WB 系列。其中 L5 和 U5 因为带 TrustZone(ARM TrustZone 技术),可以实现更强的隔离效果。如果你用的是老的 STM32F1 或 F4,很遗憾,官方支持列表里没有这些系列。不过也别急着放弃,SBSFU 的框架是模块化的,理论上你可以移植到其他 MCU 上,但这需要比较多的底层适配工作,不适合入门阶段尝试。
另外一个关键点是开发板的选择。ST 官方的评估板基本都能直接跑 SBSFU 的示例工程,比如 NUCLEO-L4R5ZI、NUCLEO-H743ZI、NUCLEO-L552ZE 等。如果你用的是第三方板子,需要仔细核对板载 ST-Link 版本和 MCU 封装是否与示例匹配。我第一次就用了一块非官方板,折腾了半天才发现是 Flash 地址偏移的问题。
3.2 软件工具链清单:一次配齐,省得来回找
SBSFU 的入门需要准备以下几样工具,缺一不可:
- 集成开发环境:STM32CubeIDE 或者 Keil MDK,推荐用 STM32CubeIDE,ST 自家工具和 CubeMX/CubeProgrammer 配合最顺滑。
- STM32CubeProgrammer:用于烧录 Bootloader、生成密钥、烧写 OTP 配置。
- STM32CubeMX:用于配置 MCU 引脚和时钟,SBSFU 的示例工程通常会附带 .ioc 文件。
- ST 官方的 X_CUBE_SBSFU 软件包:可以从 ST 官网或者 GitHub 上获取,需要注册 ST 账号。
- Python 环境:SBSFU 的固件签名和打包脚本由 Python 编写,生成新固件时必须用。
3.3 获取并导入软件包:实操时的要点记录
在 ST 官网搜索 X_CUBE_SBSFU,下载最新版本(我写这篇文章时最新是 3.x 版本,建议用新不用旧,老版本在部分新器件上有兼容问题)。下载解压后,你会看到这些关键目录:
Projects:包含各评估板的示例工程Middlewares:SBSFU 核心代码,包括 Bootloader、SFU 模块和加密库Utilities:包含固件签名工具的端到端示意图、图像转换工具的 Python 脚本Documentation:API 参考和用户手册
导入工程时,用 STM32CubeIDE 打开对应评估板的.project文件即可。要注意有的示例工程同时包含 Bootloader 和 User App 两个子工程,在 Project Explorer 里看起来是分层的,你需要分别编译、分别烧录。
我个人的建议是,第一次上手时不要用 IDE 图形化的方式创建工程,而是直接打开 ST 提供的现成示例,先把它跑通了,再慢慢改成你自己的项目。你一旦基于示例修改,可以大幅缩短前期排错时间。
3.4 编译和烧录前需要理解的"三件套"概念
SBSFU 工程之所以让新手困惑,是因为它不像平时写 MCU 程序那样只有一个 .elf 文件。它涉及三个相互关联的镜像:
- Bootloader 镜像:系统启动的第一段代码,必须烧录在 Flash 起始地址,比如 0x08000000。
- SFU 固件更新模块镜像:集成在 Bootloader 中,负责固件的接收、验证和写入,所以单独提出来说。
- User App 镜像:你的业务代码,可以独立编译生成,但必须用签名工具处理过之后才能被 Bootloader 验证通过。
第二个需要注意的是地址分配。Bootloader 占用的 Flash 区域是固定的,User App 必须编译到指定地址,例如 0x08008000 或 0x08010000 之类,具体看芯片型号和工程配置。如果你在 User App 工程里看到链接脚本里的 Flash 起始地址不是 0x08000000,这是正常的,别试图改回 0x08000000,否则会覆盖掉 Bootloader,整个安全机制就失效了。
第三个概念是"项目分区"的配置。这一部分直接在 application 的链接脚本中定义,然后通过编译期的宏传递给 Bootloader 工程。当两边不一致时,就会出现"Bootloader 验证通过之后却跳转失败"的经典问题。后面我会专门讲这个现象。
4. 核心原理解析:安全启动和固件更新是怎么协同工作的
4.1 安全启动的完整执行链路
整个安全启动过程可以拆成七个步骤,每一步都不能省,省了就会留下安全漏洞。我按照实际执行的顺序梳理一遍,同时标注每一步对应的关键函数:
第一步,MCU 上电复位,硬件从 Flash 起始地址取出 Bootloader 的复位向量,开始执行 Bootloader。此时 CPU 时钟外设都处于默认状态,安全机制尚未开始,所以这个阶段的执行窗口越短越好。
第二步,Bootloader 初始化硬件加密引擎、时钟和随机数发生器。加密引擎的初始化很关键,因为后续所有签名验证都依赖它。这里需要注意的是,Bootloader 阶段的时钟配置可能会和 App 阶段不一样,所以 App 启动后的时钟重新配置是必须的,否则外设波特率、定时器频率都会错乱。
第三步,Bootloader 读取 OTP 中的公钥,加载到加密引擎的密钥寄存器中。这一步操作的是 STM32 的硬件安全模块,ST 的代码封装成了SFU_OB_Open加SFU_OB_GetPublicKey这类接口。
第四步,Bootloader 定位到 User App 所在分区(以 Slot 0 为例),读取固件头部信息。SBSFU 的固件格式是自定义的,在用户固件原始数据之前,会自动加上一段头部,里面包含了签名、版本号、固件大小、载荷加密标识等元数据。
第五步,Bootloader 对固件镜像做哈希计算,然后用 OTP 中的公钥去验证签名字段。对于固件的哈希,SBSFU 默认是 SHA256,签名默认是 ECDSA。验证通过则继续,不通过则根据配置进入错误处理流程。
第六步,验证通过后,检查固件版本号是否大于等于当前已安装版本。如果版本回滚,Bootloader 会拒绝启动,这是防回滚机制发挥作用的地方。
第七步,一切正常则跳转到 User App 的入口地址。跳转前需要做一件很重要的事——关闭当前打开的所有中断、反初始化外设、复位系统时钟(有些实现会保留部分设置),否则 App 启动会出现异常。这个细节在官方代码里是SFU_APP_Jump函数处理的,很多人自己写简易 Bootloader 时经常会漏掉中断向量表的处理。
4.2 固件更新流程的完整业务逻辑
固件更新流程比启动流程更复杂,因为涉及固件传输。SBSFU 本身不规定传输协议,它提供的是"接收固件 + 写入 Flash + 验证"的能力,具体怎么把固件从 PC 传到设备,由你自己选择。官方示例里有 UART 和 USB 两种传输方式,你可以基于它们扩展出 BLE、Wi-Fi 或 TCP/IP 的通道。
固件更新的完整流程大致是:
用户设备上的 App 收到新固件包(可能是一个 bin 文件),先把完整固件数据放到临时缓冲区或直接写入不活跃 Slot。每写一个块,就做一次 CRC 或哈希校验,确保写入过程中没有出错。所有数据写完成之后,App 或者 Bootloader 对完整固件做最终签名验证。验证通过后,设置一个"待切换"标志,然后系统复位。复位后 Bootloader 发现待切换标志,启动新固件。如果新固件正常运行,且其自身发出"启动成功"信号(通常是设置一个标志位表示已经正常跑起来了),Bootloader 则确认切换完成。如果新固件启动后没有正常工作(比如看门狗超时),Bootloader 检测到超时后回退到旧固件,并标记新固件不可用。
这一套逻辑里有两个核心点值得展开:一个是"待切换标志"的设计,它本质上是一个状态机,存放区在 SRAM 里、带电池备份的寄存器里、或者在 Flash 的特定区域,SBSFU 在 Flash 里专门留了区域存这些标志数据。另一个是回退机制,ST 的实现会在 Flash 的"安装状态"区记录固件运行状况,包括启动次数、运行成功标志等。
4.3 防回滚机制的实现细节
很多开发者容易忽略防回滚,或者觉得"反正固件是签名的,别人伪造不了,还要防回滚干什么"。但防回滚不是防攻击者的,是防误操作和攻击者利用旧版本漏洞的组合攻击。
举一个实际场景:假设你在新版本里修复了一个安全漏洞,攻击者从旧版本里逆向出漏洞利用方式,然后想办法让设备降级到旧版本。如果 SBSFU 不检查版本号,旧固件虽然签名合法,但带有已知漏洞,攻击者就能通过它拿到系统控制权。防回滚策略就是在签名验证通过之后,再检查固件头部的版本号字段,只有版本号不低于当前运行版本时才会被接受。
SBSFU 的防回滚有多重配置方式,我常用的做法是开启"版本检查 + 最低版本号"的组合。在固件签名工具的命令行里指定一个版本号,这个版本号会被写进固件头部。Bootloader 端有一个"最低允许安装版本"的配置项,所有低于这个版本的固件一律拒绝。
需要注意的是,防回滚机制一旦开启,你就失去了"降级固件"的能力。如果新版本有 bug 需要回退到旧版本排查,SBSFU 会拦截这次更新。这个问题在实际生产中经常遇到,所以建议你在正式量产前想清楚,是否需要在设备端提供一个"工程师模式"或"调试模式"来临时绕过版本检查,ST 的代码里确实保留了类似后门,但默认关闭,你需要根据项目需求慎重决定是否开启。
4.4 加密固件的解密流程
SBSFU 的另一个重要特性是支持固件加密。不只是签名,而是把整个固件内容用 AES 加密之后再分发。设备端在收到密文固件后,Bootloader 先解密,再验证签名,最后写入 Flash。
解密流程中密钥的管理方式和公钥不同。AES 密钥不能直接放在 OTP 里,因为它会被频繁使用,放在 OTP 里无法更新。SBSFU 的做法是:设备出厂时随机生成一个设备唯一密钥,存储在受保护的 Flash 区域或者硬件唯一标识(Unique ID)派生的密钥中,传输给设备的固件则是用这个设备密钥加密的。这要求生产流程中必须有一个步骤是把密钥信息写入设备,增加了一步操作,但也大幅提升了安全性。
如果只是在开发阶段用,你可以不启用加密功能,只保留签名验证。但进入量产前建议把加密打开,否则你的固件数据在传输过程中会被中间人抓包分析,你的产品逻辑就暴露了。我实测过,启用固件加密之后,对启动时间的影响大约增加几十到几百毫秒,具体取决于 MCU 的加密引擎速度和固件大小,对于大多数产品来说这个开销可以接受。
5. 实操过程与核心环节实现:一步步跑通完整流程
5.1 用 STM32CubeProgrammer 生成密钥和烧写 OTP
SBSFU 工程第一次编译通过之后,还不能直接烧录运行,因为 OTP 里还是空的。需要用 STM32CubeProgrammer 生成密钥对并烧写到开发板的 OTP 区域。
打开 STM32CubeProgrammer,选择对应 MCU 型号,连接开发板之后,在菜单栏找到 OTP Programming 选项。ST 在这个工具里集成了 SBSFU 密钥生成向导,你可以选择生成 ECC 密钥对,指定密钥位数(推荐 P-256 曲线),然后点击生成。
生成之后,工具会输出两个文件:private_key.pem和public_key.pem。把公钥内容复制到 OTP 编程界面中,填写 OTP 区域的起始地址(具体地址参考你所用 MCU 的参考手册,例如 STM32L4 系列通常从 OTP 地址 0x1FFF7000 开始的一个固定区域)。烧写前 STM32CubeProgrammer 会提示你确认,因为 OTP 一旦烧写不可恢复,建议在烧写前把公钥存好,日后还会多次用到。
有个细节要记住:OTP 烧写本身并不是永久的"写即生效",部分器件支持在烧写前重新配置选项字来改变 OTP 区域的锁定状态。但一旦操作完成,你就无法再修改。所以我的习惯是,先在一个空的新板子上做完整流程测试,确认无误后再批量烧录。
另外,ST 的示例工程里会默认带上一个开发用的公钥/私钥对。直接用这个默认密钥跑通流程没问题,但正式产品一定要换成自己生成的密钥,否则任何人只要拿到 ST 默认私钥就能给你的设备刷固件了。这个细节非常重要,我见过有团队用默认密钥直接量产,直到测试时发现攻击者能随便升级才意识到问题。
5.2 编译 Bootloader 和 User App:地址对齐与编译顺序
进入编译环节前,务必要确认两个子工程的 Flash 地址分配。打开 Bootloader 工程的链接脚本,你会看到:
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K这说明 Bootloader 占用的区域是 Flash 起始的 32KB。再看 User App 工程的链接脚本:
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 224KUser App 偏移到了 0x08008000 之后。两个区域没有重叠,Bootloader 跳转时才能正确定位。
编译顺序上,建议先编译 Bootloader,再编译 User App。原因是 User App 的代码里可能引用了 Bootloader 提供的一些共享函数或固件包格式定义,这些头文件路径在 User App 工程里配置为指向 Bootloader 工程目录。不过从依赖关系上说,两者是独立的,编译顺序其实不完全绝对。但如果两个工程共用一些生成的中间文件,则先编译 Bootloader 能避免路径不匹配导致的问题。
编译过程中如果遇到报错说syscfg文件找不到或版本不兼容,一般是 STM32CubeMX 版本不一致造成的。解决方法是重新打开.ioc文件,用你当前的 CubeMX 版本重新生成一遍配置,再导入 IDE 编译。
5.3 固件签名与打包:Python 脚本里的命令行实操
User App 编译完成之后,生成的是原始的.elf或.bin文件,这个文件不能直接用于更新,必须经过签名和打包步骤。SBSFU 提供了一套 Python 脚本,位于软件包的Utilities/Image_Gen目录下。
打开命令行终端,进入该目录,执行签名的基本命令格式如下:
python stm32_sbsfu_image_gen.py -a rsa3072 -p private_key.pem -v 1.0.0 -o output_image.bin input_app.bin参数说明:
-a指定签名算法,常用的是 ecc256 或 rsa3072,必须和你的密钥类型一致。-p指定私钥文件路径。-v指定固件版本号,用于防回滚检查。-o指定输出文件名。- 最后一个参数是输入的用户固件 bin。
如果需要启用固件加密,加一个参数:
python stm32_sbsfu_image_gen.py -a ecc256 -p private_key.pem -v 1.0.0 --encrypt-key <密钥文件> --encrypt-scheme aes128 -o output_image.bin input_app.bin--encrypt-key指向包含 AES 密钥的文件,这个密钥需要提前在 PC 上生成并配置到设备端。更贴合量产的方式是使用 ST 的keygen工具生成密钥包,然后在生产流程中写进设备。
执行成功后会看到终端打印出固件头部信息和签名结果,同时生成一个新的 bin 文件。这个 bin 文件就是你可以通过 UART / USB / 网络 推送进设备的"官方固件包"。
如果你在把固件包写入设备后收到"Image Signature Invalid"或类似错误提示,排查方向有两个:一是确认签名时用的私钥是否与 OTP 里的公钥匹配,二是确认固件写入过程是否完好,比如串口传输时丢帧导致数据损坏。数据损坏引起的签名失败很容易排查——重新传输一遍——但如果是密钥不匹配,就要仔细核对生产流程里是否用了同一套密钥文件。
5.4 生成并验证签名固件:串口更新与日志分析
签名固件生成后,通过串口把更新包发送给设备。在 SBSFU 示例工程里,Bootloader 内置了一段串口接收程序,它会在 Bootloader 模式检测串口是否有新固件包到来。具体操作方式是:开发板复位后立即通过串口工具(比如 STM32CubeProgrammer 自带的固件升级工具,或者用 Python/串口助手)发送签名固件 bin 文件。
发送过程中观察 Bootloader 的调试串口输出(如果你例程里启用了SFU_DEBUG宏),典型的日志流程是:
SFU: Bootloader started SFU: Verify image in slot 0 ... OK SFU: No upgrade requested, jump to application或者:
SFU: Upgrade request received SFU: Image receiving... complete SFU: Image signature verify ... OK SFU: Install image ... OK SFU: Reset to run new image通过串口日志可以很直观地看到当前处于哪个阶段。我在调试时习惯把SFU_DEBUG宏打开,虽然它会额外占用一点 Flash 空间并增加日志输出延迟,但排查问题效率高很多。正式发布时再关掉。
5.5 完整一次更新流程的板级实测演示
为了让你更直观地理解,我描述一次在实际开发板上的完整更新过程。
环境:NUCLEO-L4R5ZI 开发板、STM32CubeProgrammer、X_CUBE_SBSFU 示例工程。
第一步:初始化烧录。通过 STM32CubeProgrammer 烧录 Bootloader 镜像,地址 0x08000000。然后烧录 User App 初始版本(已经签名好的 v1.0),地址 0x08008000。再烧写 OTP 公钥。重启后,系统直接启动到 App,串口输出App v1.0 started。
第二步:升级准备。修改 User App 代码,比如在串口输出时把v1.0改成v1.1,重新编译。用前面的签名命令对新 bin 执行签名,指定版本号-v 1.1.0。
第三步:触发更新。在 App 运行时,通过开发板上的用户按键触发一次软件复位。Bootloader 启动后会先检查串口是否收到更新指令(部分示例使用跳线或特定引脚配置进入升级模式),在 bootloader 模式下用串口工具发送固件包。
第四步:观察结果。等待约 1~3 秒(取决于固件大小和串口波特率),Bootloader 完成接收和验证,输出日志后自动重启。此时 App 串口输出变为App v1.1 started,升级完成。
第五步:验证回滚。重新发一次 v1.0 固件,Bootloader 会因版本号低于当前版本而拒绝安装,日志显示Image version too low。
这一整条流程跑通后,你对 SBSFU 的信任链、固件格式、更新状态机就都有了直观的认知。
6. 常见问题与排查技巧实录
6.1 Bootloader 不跳转 App:官方手册里没写全的排查路径
这是遇到概率最高的问题:Bootloader 明明烧进去了,OTP 公钥也写了,但系统启动后一直卡在 Bootloader,不跳转到 User App。
排查路径我按优先级排一下:
第一步,检查跳转地址。User App 的链接脚本 Flash 起始地址必须与 Bootloader 中配置的 App 起始地址完全一致。例如 Bootloader 工程里定义宏APP_START_ADDRESS = 0x08008000,但 User App 链接脚本却是 0x08010000,那就永远跳不过去。
第二步,检查中断向量表偏移。Cortex-M 内核要求应用的中断向量表放在一个由SCB->VTOR指定的地址上,且一般要求对齐到 0x80 或 0x200 的整数倍。User App 启动代码里需要写一行:
SCB->VTOR = APP_START_ADDRESS;如果你是自己写的工程,这一行极容易漏。ST 的示例工程里已经处理好了,但如果你把代码移植到自己的工程里,需要重新确认。
第三步,检查串口日志。如果日志显示Image signature verify FAILED之类的信息,说明签名验证没通过。这通常和 OTP 公钥不一致有关,但还有一种容易忽略的情况:签名工具的密钥类型与 Bootloader 中编译的密钥类型不匹配。你在 Bootloader 工程里选择了 ECC256,但签名时用了 RSA 密钥,自然会验证失败。
6.2 签名验证失败:不一定是密钥不匹配
签名验证失败可以细分成几种情况,用了几次之后我总结了一个速查表:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 验证失败且日志无任何输出 | OTP 区域没有烧写公钥或公钥烧错地址 | 用 STM32CubeProgrammer 重新检查 OTP 内容 |
| 验证失败但日志能打出签名算法的名字 | 签名算法与编译配置不一致 | 修改 Bootloader 工程的SBSFU_CONFIG,使其与签名工具参数一致 |
| 验证失败且用 ST 默认密钥能通过 | 私钥文件路径指向错误,使用了不匹配的密钥对 | 重新生成密钥对并同时更新 OTP |
| 验证失败但重新烧录 OTP 后解决 | 烧写公钥时烧录了错误数据 | 生成新密钥对并重新烧录 |
另外一个坑是 OTP 烧写的字节序问题。公钥烧进 OTP 后的字节序和你 PC 上 PEM 文件里的字节序可能不同。SBSFU 的工具链会处理好大部分情况,但如果你自己写脚本烧写公钥,一定要确认大小端转换,否则会出现"同一个算法、同一个密钥,但就是验证不过"的诡异问题。
6.3 App 能启动但外设异常:时钟和外设所有权交接的坑
SBSFU 跑通之后,最常见的"半成功"状态是:App 能启动、主函数也执行了、串口能打印,但定时器、ADC、DMA 这些外设的表现不正常,比如 PWM 频率不对、ADC 采样率错乱。
这种情况十有八九是时钟配置问题。Bootloader 为了快速启动,可能只配置了内部 HSI 时钟,没有切换到外部高速晶振 HSE,也没有配置 PLL。但 App 启动代码默认你已经在系统初始化阶段做了完整的 Clock Tree 配置,它会按 PLL 已经锁定的状态去设置外设分频。
解决方法很简单:在 App 启动代码的SystemInit()里重新执行完整的时钟树初始化,之后紧跟着重新初始化所有用到的外设。ST 标准库生成的工程代码,SystemInit()里面会调用SystemClock_Config(),你只需要确认这个函数确实执行了,并且没有因为 Bootloader 跳转时的某些条件被跳过。
还有个细节,如果你的 App 里启用了 FreeRTOS,跳转前 Bootloader 未清理的中断可能在内核调度时产生不可预期的行为。稳妥的做法是,在跳转前把 Bootloader 用到的外设全部 DeInit,再将全局中断关闭,最后跳转。App 入口的第一个动作就是__enable_irq()。ST 的跳转代码已经实现了这些操作,但如果你改过跳转函数,注意别把这些收尾工作删了。
6.4 固件更新中断导致变砖:怎么自救
A/B 方案下,更新中途断电通常不会导致设备完全变砖,因为旧固件还在。但如果你是单 Slot 方案(适用于 Flash 较小的芯片),更新过程断电或者传输中断,设备里就只剩下一份写了一半的固件,系统将无法启动。
SBSFU 对这种情况的处理是在固件写入前对固件头做校验,写完后再次确认,并且整个更新过程在收到完整固件包并验证通过后才会真正跳转。然而如果你固定使用单 Slot 方案,SBSFU 无法同时保证拥有旧版本完整的固件,崩溃自救只能靠外部方式:
- 恢复模式:保留一段极小的恢复程序在 Bootloader 中,它会监听串口或 USB,始终等待新固件。即使 App 损坏,Bootloader 仍能运行,可以重新刷入一个有效的固件包。
- 外部看门狗加回退标志:在启动 App 前设置一个"正在启动新固件"的标志,App 正常运行一段时间后清除该标志。如果 App 崩溃导致看门狗复位,Bootloader 检测到标志没有被清除,自动回到升级模式等待新固件。
- 备份槽位:代价是 Flash 占用翻倍,但在关键设备上值得。我做过一个工业网关项目,客户要求固件更新期间不允许出现任何失联窗口,最终就是用 A/B 方案加独立恢复程序,三层保险。
6.5 更新后无法连接调试器:检查选项字设置
有一个非常经典的坑:Bootloader 里为了安全,会在启动后修改 Flash 选项字,禁用调试接口(JTAG/SWD)。这本来是为了防逆向,但如果你没有在开发阶段禁用这个功能,或者调试接口在量产时被意外打开,就会出大问题——更新完固件之后,你可能再也连不上调试器,无法调试、无法恢复。
ST 的 SBSFU 示例默认会在某个阶段配置选项字来禁用调试口,具体的宏通常在sfu_low_level_flash.h中,例如SFU_LOADER_DBG_DISABLE。开发初期请务必保持调试口开启,量产固件里再把它关掉。如果已经出现"连不上调试器"的情况,解决办法是使用 STM32CubeProgrammer 的复位模式,通过手动拉 BOOT0 引脚到高电平进入系统 Bootloader(System Memory 模式),再用 STM32CubeProgrammer 连接并重新配置选项字。注意每个 STM32 系列的 BOOT0 引脚配置略有差异,查阅对应型号的数据手册。
6.6 关于"安全启动里能不能做 X"的几个高频问题
除了上面这些报错类的问题,我还经常在社区里看到一些"能不能"类型的问题,挑几个有代表性的说一下。
问:SBSFU 能支持 OTA(Over-The-Air)升级吗? 答:SBSFU 本身不限制传输链路,它只负责固件接收后的校验和安装。OTA 的完整方案需要在 User App 里集成网络协议栈(比如 MQTT、CoAP),下载固件包后用 SBSFU 提供的SFU_InstallImage接口触发安装。ST 有配套的 SBSFU + 无线协议栈的参考实现,但主要针对 STM32WB 系列。
问:Secure Boot 意味着固件不能被读取吗? 答:不是。Secure Boot 的职责是保证启动的固件是可信的、没被篡改过的。它不等于代码加密或防读取。如果你确实不希望固件被逆向分析,需要配合 Read Protection(RDP)等级 2 或 TrustZone 这类硬件机制使用。SBSFU 的文档里也明确建议:安全启动 + RDP Level 2 一起使用才是完整方案。
问:可以完全用 SBSFU 替代商用安全解决方案吗? 答:取决于你的威胁模型和合规要求。SBSFU 提供的安全机制(签名、防回滚、加密传输)在多数商用产品里是足够的。但如果你的产品有明确的合规认证要求(比如金融级别的安全标准),可能还需要额外的安全芯片或额外的安全评估。评估这些要求不是 SBSFU 的能力边界,但它可以成为你整个安全架构的起点。
7. 工具选型与源码学习路径:从"能用"到"会用"
7.1 SBSFU vs 自己写 Bootloader:算清楚这比账
不少开发者在接触 SBSFU 之后的第一反应是:"这套机制我能不能自己写一个 Bootloader 来实现?" 我的看法是,你当然可以,但要想清楚两个层面的问题。
第一层是安全协议的正确性。签名验证、防回滚、密钥管理这些逻辑看起来简单,但真正要做好,需要考虑大量边界情况。比如公钥存储位置的选择、算法实现时对侧信道攻击的防护、内存清密的时机、固件头格式的兼容扩展性。这些细节如果没有人替你验证,很容易留下安全隐患。ST 的 SBSFU 至少经过了厂商级的测试和不少量产项目的验证,比你在项目周期内临时写一个安全 Bootloader 要可靠得多。
第二层是维护成本。安全攻击手段在进化,芯片工具链在更新,算法在升级。SBSFU 作为官方软件包,后续有持续的维护和更新,你只需要跟着升级软件包版本。自己写的 Bootloader,所有安全问题都得自己盯,遇到新漏洞还得自己分析修复。除非你的产品有极其特殊的需求,比如必须集成私有加密算法,否则我不建议从零造轮子。
我见过一个更务实的折中方案:用 SBSFU 做安全底座,但对外层的更新策略、传输协议、业务逻辑完全自定义。这样既拿到了安全机制的成熟实现,又保留了自己产品的差异化空间。
7.2 深入源码的路径:从哪几个文件开始看
如果你希望从"能跑通"进阶到"能定制",需要把源码的关键部分读一遍。不要一上来就全部铺开,按下面的顺序读,效率会高很多。
第一个要读的文件是sfu_boot.c。它会告诉你整个 Bootloader 的执行流程,从主函数入口到调用跳转函数。核心流程都写在注释和关键函数调用里,比如SFU_OB_Open、SFU_IMG_Load、SFU_IMG_Verify、SFU_APP_Jump。
第二个要读的是sfu_low_level_flash.c。里面包含了 Flash 读写、区域擦除、选项字配置等底层操作。如果你要适配不同的 Flash 布局或者更换 MCU 型号,这个文件基本要全面改写。
第三个要读的是sfu_low_level_security.c。签名验证、哈希计算、解密逻辑都在这一层,通常调用 STM32 的硬件加密库。如果你想换密钥算法、调整加密引擎配置,都是在这个文件里改。
读源码的时候有一点要特别提醒:SBSFU 的代码是面向所有支持型号的通用实现,宏开关非常多。如果你在某个函数里看到了#if defined (STM32L5xx)这样的条件编译,说明不同系列走的是不同的内部路径。阅读时先确定你的芯片型号对应的分支,不要被其他型号的代码干扰判断。
7.3 密钥管理和生命周期管理的最佳实践
把 SBSFU 真正用到量产环节时,最需要重视的不是代码,而是密钥管理流程。我见过不少项目轻视密钥管理,结果安全机制形同虚设。
第一,私钥的存放。私钥在签名工具和开发 PC 上是明文 PEM 文件,必须限制访问权限。更好的做法是把私钥放到离线机器上,或者用硬件安全模块(HSM)保管,只有签名时才调用。如果你无法负担 HSM 成本,最低限度也要把私钥放进加密压缩包里,并设置强密码。
第二,生产环境的密钥分发。如果你委托代工厂烧录固件,不需要把私钥交给代工厂。你只需要在 PC 端完成固件签名打包,把签名后的固件包给你信得过的烧录环节。OTP 公钥的烧写可以并到产线的烧录工序中,但公钥本身不敏感,可以适当放宽权限。
第三,设备生命周期的管理。设备可能需要经历"开发 → 试产 → 量产 → 售后返修"等不同阶段,每个阶段设备的 OTP 配置可能不同。返修设备需要恢复出厂状态时,如果你的 OTP 已经烧死了,需要确认 STM32 的 RDP 回退机制是否允许你调整安全等级。至少在选型和设计阶段,就要想好"设备在客户现场需要召回或返修时,我们如何重新烧录"这个问题。
7.4 评估板之外的选型思考:安全启动对 MCU 资源的影响
选型时如果你把 SBSFU 作为一种参考方案的组成部分,要专门确认 MCU 的资源余量。安全启动 + 固件更新机制本身会占用不少 Flash 空间。Bootloader 加 SFU 模块,在不同配置下通常需要 24KB 到 48KB 的 Flash。如果你选择的 MCU 总 Flash 只有 64KB,留给 App 的空间就会非常紧张,这会影响你后面的业务开发。
运行内存上,签名验证和固件接收过程需要较大的 RAM 缓冲。Bootloader 阶段不会像 App 那样大方地使用全部 RAM,但如果你固件包比较大,建议你评估一下串口接收缓冲区、解密缓冲区、哈希中间结果占用的内存。我记得在某个项目里用 STM32L4 的 64KB RAM 芯片时,Bootloader 阶段的 RAM 峰值达到 16KB 左右,比想象的高不少。
所以我建议选型时,如果计划采用 SBSFU,Flash 最少留出 256KB(保守值),RAM 最少 64KB,否则后续开发会很痛苦。当然如果你用的是带外部 Flash 的方案,可以灵活很多,但会引入"外部 Flash 如何保护"的新问题,这些内容 SBSFU 也有参考做法,不过复杂度和成本都会上升。
8. 安全组件之外的额外收获:SBSFU 对工程思维的启发
其实 SBSFU 给我最大的收获,倒不只是安全启动本身,而是它让我重新审视了嵌入式软件的工程化组织方式。它的代码用宏开关和分层架构把"芯片相关"和"业务相关"做了很彻底的解耦,你在不同 STM32 系列之间迁移时,大部分上层逻辑不需要改,只换底层驱动和配置即可。这种架构思路值得借鉴到自己的项目中。
另外,SBSFU 的信号机制、状态机设计和错误处理流程,也是很好的学习素材。它不会像普通示例代码那样"怎么简单怎么来",而是以产品化的标准来处理各种异常路径。读它的代码,你会发现一个简单"跳转 App"的操作,背后要考虑多少种异常情况。这种工程习惯,比单纯学会一个软件包有价值得多。
如果你希望更深入地掌握它,我建议拿到源码后,不要急着改代码,先跑通默认示例,再逐步修改配置和源码,观察现象如何变化。把文档里每个配置项都实际测一遍,比来回读十遍文档都管用。我在学习阶段就经常主动"破坏"配置——比如故意用错密钥、故意降低版本号、故意在更新中途断电——通过观察失败表现,真正理解每一层防护的边界在哪里。