尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32WB OTA与无线固件更新实战:从BLE Bootloader到FUS完整方案

STM32WB OTA与无线固件更新实战:从BLE Bootloader到FUS完整方案 最近在给一款基于 STM32WB55 的物联网产品做远程维护方案产品已经小批量铺出去了结果客户现场反馈了一个只有特定场景才会触发的偶发问题。这种问题最头疼板子焊死在设备里不可能拆壳用 ST-Link只能走无线升级。于是我把 STM32WB 的 OTA 和无线固件更新完整跑了一遍从 Flash 分区、Bootloader、BLE 传输链路到协议栈自升级、签名校验、防回滚整个链路梳理下来踩了不少坑。这篇文章把我的方案、代码思路和实测经验全部整理出来给正准备做 STM32WB OTA 的朋友一个可以直接抄作业的参考。1. 为什么STM32WB的OTA是一个两层问题1.1 双核架构决定了升级方式的特殊性做 OTA 之前必须先搞清楚 STM32WB 和普通 MCU 的本质区别。STM32WB 是一颗双核芯片Cortex-M4 内核跑用户应用逻辑Cortex-M0 内核专门跑无线协议栈比如 BLE、Zigbee、Thread。这两个核之间通过 IPCC核间通信处理器交换消息Mailbox 机制传递命令和事件内存上有一块共享的 SRAM 区域用于数据交换。这意味着OTA这个词在 STM32WB 上不是单一概念。用户应用固件需要升级无线协议栈固件也需要升级。两者的升级路径和机制完全不同应用固件App跑在 M4 上可以由 Bootloader 引导你自定义的升级流程比如通过 BLE 下载新固件、写入 Flash、校验、跳转。无线协议栈固件Stack跑在 M0 上由 ST 提供的 FUSFirmware Upgrade Service机制管理不能由用户随便往 Flash 里写就完事必须走 FUS 的命令接口。很多第一次接触 STM32WB 的朋友会在这一步卡住以为像 STM32F1 那样编译一个 bin 文件直接刷进去就行。结果自己写完应用固件后把协议栈地址覆盖了板子直接变砖只能通过 ST-Link 连 STM32CubeProgrammer 的 FUS 模式恢复。所以做方案设计时第一步不是写代码而是把我到底要升级什么、谁负责升级、升级到哪块 Flash这件事理清楚。1.2 应用固件升级与协议栈升级的本质区别回到实际场景。产品用 BLE 传输数据手机 App 连接设备设备采集传感器数据并上报。这种场景下日常维护大概率只需要升级应用固件比如修一个数据处理 bug、优化一段算法。这类升级走自定义 Bootloader BLE 通道就可以解决。但有些场景必须升级协议栈固件。举个例子产品早期烧录的是 BLE 5.0 的 full stack后期发现项目需要同时支持 Zigbee 协议或者需要修复 ST 官方栈的一个 errata这时候就必须更新 M0 侧的无线协议栈固件。而栈固件的升级不能由你的 App 代码直接写 Flash 完成必须把控制权交给 FUS由 FUS 完成擦除、写入、安全校验。还有一个容易忽略的点是 M4 和 M0 之间的通信。升级过程中M0 可能因为栈固件被擦除而停止响应此时如果 M4 侧还在通过 API 等待 BLE 事件程序就会死等。所以我做升级流程时第一步会把 M4 和 M0 的通信任务暂停等待升级完成后再重新初始化服务。这个顺序如果反了升级过程中会频繁出现 HardFault。下面这张图是我习惯画的升级流程概览虽然没按正式流程图绘制但能帮助理解两个核的分工M4 应用核 M0 网络核 | | |--- 正常业务通信 -----------| | | |--- 收到升级指令 -----------| | | | 进入升级模式 | | 暂停 BLE 服务 | | | |--- FUS 触发栈升级 ---------| | | | | 擦除并重写 Stack | | | 用户 App 升级 | | 下载 → 校验 → 跳转 |2. Flash布局与分区设计OTA的地基2.1 一张表看懂STM32WB55的Flash划分OTA 方案里最核心、最不能出错的就是 Flash 分区。STM32WB55 的 Flash 有 1MB 和 2MB 两个变体常见的是 1MB 版本。整个 Flash 从低地址到高地址大致可以分为以下区域以我实际工程为例具体偏移以你的链接脚本为准区域用途起始地址示例大小示例说明FUS / 系统 Bootloader0x0800000064KB出厂自带或由 ST 工具写入负责安全启动和栈固件安装用户 Bootloader0x0801000032KB自己写的引导程序负责接收新固件并引导 App用户 App 区0x08018000256KB当前运行的应用固件下载暂存区0x08058000256KB接收新固件时暂存校验通过后拷贝到 App 区NVDS / 用户数据区0x08098000剩余空间存储配对信息、校准数据、升级标志位无线协议栈Stack高地址区域具体由 ST 栈镜像决定约 256KB由 FUS 管理用户不要直接写Flash 地址分配不是拍脑袋定的有几个约束条件需要满足第一用户 Bootloader 和 App 的起始地址必须和链接脚本、中断向量表偏移一致。App 工程的VECT_TAB_OFFSET必须改成 App 区的偏移量否则中断向量表错位程序跑起来必死。第二下载暂存区和 App 区的尺寸必须一致至少不能小于 App 区。因为升级流程是先把新固件完全下载到暂存区做完完整性校验后再一次性拷贝到 App 区。这样做的最大好处是容错下载过程中断了、丢包了旧 App 还好好地躺在 App 区断电重启后 Bootloader 发现暂存区不完整直接跳转旧 App设备不会变砖。第三无线协议栈固件的地址不要和用户区重叠。这块由 FUS 管理但你在规划用户分区时一定要给栈固件预留空间比如 1MB Flash 的芯片栈固件可能会占掉最后的 256KB那用户 App 区、Bootloader、暂存区总共只能用 768KB。2.2 双备份方案与临时下载区方案的取舍热词里有人提到 AB 分区、stm32 keil ota 的 ab 分区其实就是双备份策略Flash 里有 A、B 两个 App 区一个运行一个空闲OTA 时把新固件写入空闲区校验通过后切换启动标志下次启动直接跑新版本。这种方案的优点是升级过程不打断当前运行A 运行时 B 可以后台下载而且理论上永远不会出现升级到一半旧固件没了的情况Android 系统的 OTA 底层思路也是这个。但在 STM32WB 上我建议大多数项目用暂存区 拷贝方案不要盲目上 AB 分区。原因很简单Flash 空间不够。1MB 的 Flash栈固件吃掉 256KBBootloader 和 NVDS 再吃掉 96KB剩下约 672KB。如果做 AB 分区每个 App 区只能分到 336KB而 STM32WB55 的 BLE 应用随随便便就能编译出 150KB~250KB 的固件空间非常紧张。再加上栈固件和 FUS 需要对齐地址AB 分区的地址规划难度会明显上升。暂存区 拷贝方案虽然升级时不能跑业务需先停机但代码简单、Flash 占用低、移植速度最快。对于大多数传感器采集类产品OTA 本来就会让设备暂停工作停机几十秒完全可接受。如果你的产品对可用性要求极高比如医疗设备、工业控制器必须做到 OTA 期间业务不间断那再考虑 AB 分区。STM32 其他系列可以参考IAP APP APP的方式实现但在 STM32WB 上要额外小心拷贝/切换标志位要在 NVDS 或独立 Flash 扇区里保存不能放在 App 区内部否则升级时整个扇区擦除会把标志位一起擦掉。2.3 用户Bootloader的设计边界用户 Bootloader 的职责要尽量简单我见过太多人把 Bootloader 写成一个大工程功能模块一堆结果一个小小的 bug 就导致整机变砖。我的 Bootloader 只承担四个功能检查是否有升级请求标志保存在 NVDS。如果有则初始化 BLE 外设等待手机连接并传输固件。数据接收完成后做完整性校验CRC32 SHA256。校验通过后用 Flash 接口把暂存区数据拷到 App 区然后跳转执行。除此之外什么都不做。日志打印、传感器初始化、云连接逻辑统统丢给 App。原因在于 Bootloader 意味着反悔的机会它越简单出错概率越低升级失败时至少还能通过再次进入 Bootloader 恢复。跳转 App 时的细节也不能马虎。跳转前要关闭全局中断、关闭已初始化的外设时钟、把系统时钟恢复到默认状态然后设置 MSP 为 App 区首地址存的值再设置 PC 指向App起始地址 4。很多人会漏掉__set_PRIMASK(1)关中断这一步结果跳转后 App 里跑飞排查半天找不到原因。3. BLE OTA传输链路从Service设计到断点续传3.1 自定义OTA Service的特征定义BLE 传输是整个 OTA 链路中最容易出问题、也最需要定制化开发的部分。BLE 的 GATT 服务模型天然适合做命令下发和数据块传输我参考了 ST 官方 OTA 例程的思路实现了自己的 OTA Service。整个 OTA Service 使用 4 个特征值版本号特征只读App 端读取当前固件版本判断是否需要对端设备升级。控制点特征可写App 端下发升级指令比如开始升级、擦除暂存区、提交固件、重启。数据块特征可写App 端依次写入固件内容每包最大 240 字节。状态通知特征可通知/可写设备端把升级进度、校验结果、错误码主动推给 App。为什么控制点和数据块要分开因为控制指令是命令性质必须保证可靠性而数据块是批量传输如果混在一个特征里协议解析逻辑会变得很乱。加上 STM32WB 的 BLE 协议栈对同时写多个特征值有一定时序要求分开设计后实测出包率更高也更容易做单元测试。服务端的代码逻辑是一个状态机每个状态下只接受特定指令。比如空闲态收到开始升级指令后进入下载态此时数据块特征才允许写入如果收到固件清空指令则先把暂存区所有扇区擦除一遍。这个设计能有效避免 App 端乱序操作导致 Flash 数据错乱。3.2 MTU协商、分包机制与传输效率计算BLE 4.2/5.0 默认 ATT_MTU 是 23 字节也就是说单包用户数据最多只能传 20 字节。如果按 20 字节一包传 250KB 固件需要 12800 包哪怕每包间隔 7.5ms光传输就要 96 秒加上协议开销整个升级过程可能要 3~5 分钟体验很差。所以 OTA 传输前必须先做 MTU 协商。STM32WB 的协议栈支持在连接后通过 GATT Exchange MTU 把 ATT_MTU 提升到 247 字节这样单包最多可以携带 244 字节的有效数据。但如果数据格式里还要带包序号和标志位实际可用负载就是 244 字节减掉头部字节数。我自己的协议格式是 2 字节包序号 2 字节长度字段 N 字节数据每包实际数据量取 240 字节。计算公式也不复杂固件大小 256KB每包 240 字节大约需要 1093 包。在连接间隔 30ms、每个连接间隔传 3 包的条件下理论传输时间约为1093 包 / 3 包每间隔 * 30ms 10930ms ≈ 11 秒实际测试因为偶尔有丢包重传、BLE 调度抖动完整升级流程在 20~30 秒左右这个体验已经可以接受了。发送频率不是越高越好。BLE 的链路层有自己的重传机制如果你一个连接事件内塞太多包对端处理不过来反而触发拥塞丢包。我在 STM32WB 上实测MTU 协商到 247 后每个连接间隔发送 3~4 包是稳定区间超过 5 包开始出现BLE_ATT_ERR_INSUFFICIENT_RESOURCES错误需要把发送窗口降下来。3.3 断点续传与失败重传的工程实现在产品现场无线传输不可能百分百可靠。手机信号干扰、用户走远、系统断连都可能让升级中断。如果没有断点续传一次失败就要从第一包重新传用户体验极差尤其固件越大越不能接受。我的实现方式是两种机制组合第一逐包 ACK。设备端每收到一包数据写入暂存区后更新本地接收计数并周期性通过状态通知特征上报当前记录的下一包序号。App 端发现上报的序号和自己发送序号不一致就重发对应序号的数据包。第二进度记录落盘。每收到 100 包把当前接收进度写入 NVDS。这个频率要权衡 Flash 寿命NVDS 的擦写次数有限如果每包都写 Flash几万次擦写后 Flash 就废了。实测每 100 包落盘一次整个升级周期大约落盘 11 次完全在合理范围内。断连恢复流程是设备端进入 Bootloader 后上报当前记录的位置App 从这个位置继续传输。但如果暂存区第一次写入就校验失败则从头开始。判定的标准是暂存区头部额外存一个 16 字节的 magic 标记只有完整擦除并写入好第一包后才写这个标记否则认为暂存区无效。4. 无线协议栈固件升级绕不开的FUS4.1 FUS到底是什么聊完用户 App 的 OTA再来说协议栈升级。STM32WB 的无线协议栈固件不在用户 Flash 的普通区域内它由 ST 提供STM32WB5x_BLE_Stack_full_fw.bin等镜像文件这些镜像文件必须通过 FUS 服务安装到 Flash 的栈区域。FUS 是烧录在 STM32WB 系统区域的一段固件负责三类事情启动校验、无线栈安装/更新、以及密钥管理Secure Key Storage。用户代码通过一个特定内存地址FUS_FW_IMAGE等接口与 FUS 通信FUS 会接管系统自动完成栈固件的擦写写完后复位系统。为什么不能自己写栈固件区因为栈固件包含 M0 侧的代码和数据M4 侧无法直接访问 M0 的私有资源而且栈固件有签名机制普通写入的内容 FUS 启动时校验不通过会直接拒绝运行。这一点和用户 App 区完全不一样是 STM32WB 的硬约束。4.2 通过用户App触发Stack升级的操作流程协议栈升级的触发方式有两种。第一种是用 STM32CubeProgrammer 通过 ST-Link 连接用 FUS Tool 手动刷入栈固件这种方式适合产线不适合现场。第二种是产品出厂后在用户 App 中通过代码触发 FUS 升级这才是真正意义上的无线固件更新。流程大致是先通过 BLE 或者串口把新的栈固件文件下发到设备暂存在用户暂存区或专用区域。用户 App 调用 FUS 接口设置栈固件路径并触发升级。FUS 校验固件签名随后把控制权交给系统 Bootloader系统擦除旧栈并写入新栈。整个升级过程 M0 不可用设备会复位。复位后系统重新启动用户 App 重新初始化无线服务检查栈版本是否与预期一致。这里要注意一个细节FUS 升级栈固件时用户 App 代码还在 Flash 里不会被动但 M0 服务会失效。所以升级前必须先把 BLE 连接断开、清理通知回调否则复位后 App 端可能残留旧的连接状态造成 GATT 数据库不一致。4.3 升级顺序与版本匹配的坑栈固件和用户 App 之间存在版本匹配关系。ST 的无线协议栈和驱动stm32wbxx_hal_ble是一一对应的升级栈固件后你的 App 工程里编译链接的 HAL BLE 库也必须更新到对应版本否则会出现函数接口不一致、内存布局不匹配、甚至编译期就报错的问题。我在实际项目中遇到的典型问题是栈固件升级到新版后App 端调用HAL_BLE_Init返回错误码BLE_STATUS_INVALID_PARAMS。查了 ST 社区才发现新版栈对 M4 与 M0 之间的消息池大小有不同要求必须在编译时通过链接脚本的BLE_POOL_SIZE配置同步调整。版本匹配策略我强烈建议做成捆绑升级App 固件和新版栈固件作为一个发布包设备端先升级 App再升级栈或者先升级栈再升级 App但绝对不要只升级其中一个。我曾经为了省流量只下发了栈固件结果 App 里的旧 HAL 驱动和栈新版本不兼容设备运行几分钟后就死机最后只能重新走一遍完整升级流程才恢复。5. 安全机制签名、加密与防回滚5.1 固件签名校验从CRC到非对称签名OTA 通道天然是不安全的尤其 BLE 是无线广播信道任何人用手机或嗅探器都能截获你的传输内容。如果你只做 CRC32 校验攻击者可以轻易伪造一个带合法 CRC 的恶意固件包刷进去后设备就完全失控了。所以一个正经的 OTA 方案必须做非对称签名。我的做法是采用 ECDSA P-256 签名算法。编译产线固件时用私有密钥对固件 bin 文件计算签名签名值附在固件包的尾部。设备端 Bootloader 里保存的是对应的公钥在跳转 App 之前对固件包做签名验证验证通过才允许启动。这里有个选型的原因ECDSA 签名是 64 字节尾巴开销很小验签计算量也远小于 RSA。STM32WB 虽然有硬件加速 AES但 ECDSA 验签是软件实现的实测验签一次大约耗时 200ms 左右放在 OTA 启动阶段完全可以接受。公钥的管理要特别谨慎。设备端放了公钥就意味着任何人都能通过静态分析拿到它这没关系因为公钥本来就不怕泄露。怕的是私钥泄露一旦泄露攻击者就能给任何固件签名。所以私钥必须放在离线构建机或 HSM 里不能进代码仓库。5.2 加密传输与防回滚设计防止固件被分析传输内容还要加密。STM32WB 硬件自带 AES 加解密引擎我在传输协议中对每包数据做了 AES-CBC 加密密钥在出厂时写入设备并把密文存入安全存储区。这样即使攻击者抓包拿到完整的加密固件流也无法直接解析出明文。防回滚是另一个容易被忽视的点。攻击者可能会保留一个旧版本固件可能含安全漏洞然后把设备刷回旧版本。所以固件包头部必须带版本号Bootloader 在升级前比较新版本号是否大于当前运行版本号如果不大于就拒绝写入。版本号建议用递增的整数不要用字符串 v1.2字符串解析容易出错整数比较最稳。另外安全启动链条也要打通。STM32WB 的系统 Bootloader/FUS 本身具备安全启动能力如果你产品的安全等级要求很高还应配置 RDP读保护等级为 Level 1 或 Level 2防止通过调试口直接 dump Flash 内容。RDP Level 2 是不可逆的一旦设置调试口永久关闭所以产线设置时务必先确认所有测试都已完成避免返工时设备锁死。6. 实测踩坑与调试心得6.1 常见问题速查表整个 OTA 方案我从零搭起来前后花了两周其中一半时间在调各种不按文档走的问题。整理了一张速查表都是实际遇到的希望对你有帮助问题现象直接原因解决方案升级完成后设备无法连接 BLE升级后没有重新初始化 GATT 服务Bootloader/App 跳转后必须完整调用HAL_BLE_Init并注册服务Flash 写操作失败暂存区地址被栈固件占用重新核对 Flash 分区表确保暂存区在栈固件地址范围之外升级过程中设备 HardFaultM4 和 M0 通信任务未暂停进入升级模式后先关闭所有 BLE/GATT 任务升级完成再恢复固件明明传完了跳转后白屏跑飞中断向量表偏移未配置检查 App 工程的VECT_TAB_OFFSET和链接脚本的 Flash 基地址BLE 传输到一半断流连接事件内发包太多调小发送窗口每个连接间隔不超过 4 包升级后栈版本显示还是旧版触发了 FUS 升级但未等系统复位FUS 升级完成后必须等待系统重启不能在同一会话内继续调用栈 API验签失败但固件明明是用正确密钥签的固件包尾部的签名拼接位置错误统一约定签名跟在 bin 文件末尾设备端按固定偏移读取6.2 调试工具与排查思路OTA 相关的 bug 往往横跨 BLE、Flash、Bootloader 三层排查起来比较费劲。我实际用下来最有效的三样工具第一串口日志。在 Bootloader 和 App 中都通过 UART 输出升级流程的关键节点比如进入 Bootloader、开始擦除、下载第 N 包、校验通过、跳转。这是最朴素但最高效的手段很多问题看日志一眼就能定位。第二STM32CubeProgrammer 的 Memory and File 视图。可以实时看 Flash 各区域的十六进制内容用来确认分区表是否按预期写入、暂存区和 App 区数据是否正确。我排查过一次升级完 App 区数据全 0xFF的问题就是靠它确认是擦除函数传参错误导致把整个扇区擦没了。第三使用 STM32CubeMonitor-RF 配合开发板抓取 BLE 空中数据包。分析数据链路层重传、连接间隔、MTU 协商是否真正生效。有一次传输速度上不去用这个工具一看才发现手机端并没有协商到 247 字节 MTU实际每个包还是 20 字节白白浪费了大量时间。调试时的耐心也很关键。OTA 升级涉及固件下载、Flash 写入、签名验证、跳转等多个环节任何一个环节出错现象都是设备不工作。我的建议是从简到繁逐步验证先在开发板上用串口升级打通单机链路再切换为 BLE 通道最后才接入签名和加密逻辑。分步验证能把问题锁定在更小范围内不会出现多个变量叠加导致无从下手的情况。最后分享一个我个人的习惯OTA 功能完成 Basic 验证后务必做一次极端测试。把手机放在距离设备 30 米的地方升级或者升级过程中故意断电几次看设备是否能恢复到可用状态。这些测试虽然不愉快但能暴露出正常情况下根本发现不了的问题。我就是在一次断电测试中发现暂存区 magic 标记的擦除逻辑有漏洞才补上了断点续传的关键一环。OTA 做的是最后一道防线只有经过这种极端场景的考验才敢把它用到量产产品上。
返回列表