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

资讯详情

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

STM32WB BLE OTA实战:双核架构下的无线固件更新

STM32WB BLE OTA实战:双核架构下的无线固件更新 嵌入式项目里OTA这三个字母一半是锦上添花另一半是提心吊胆。STM32WB系列的OTA还要加戏一颗SoC里有两个CPU核一个跑应用一个跑协议栈固件更新不再是刷个bin那么简单。AN5247是ST围绕这个主题发布的应用笔记也是我做过几轮BLE OTA之后回头看才真正读懂的文档。这篇文章不打算照搬AN5247的目录我会把STM32WB无线固件更新的设计思路、分区布局、BLE传输实现、双核协同以及实际调试中的坑串起来讲一遍适合正准备做FOTA或已经在填坑的工程师参考。1. 先搞清楚STM32WB的固件架构再谈OTA1.1 双核并存的更新复杂性STM32WB和普通MCU最大的区别在于它不是一个单纯的Cortex-M4控制器而是M4主核加M0协处理器的异构双核架构。M1? 不对是M4负责用户业务比如传感器数据采集、算法、图形界面M0则承担2.4GHz无线协议栈包括BLE、Zigbee、OpenThread、专有协议。两个核共享Flash和RAM通过特定IPC机制通信。这种架构带来的直接问题是升级时你更新谁更新M4的App还是更新M0里的无线栈很多第一次接触这颗芯片的人会把OTA理解成“把一个application.bin丢到Flash里然后复位”。放在普通MCU上够用但在STM32WB上如果你直接烧录Flash的0x08000000地址很可能把M0运行的无线栈覆盖掉设备直接变砖。STM32WB的Flash里同时放着一堆角色不同的固件包括FUS、无线协议栈、用户Bootloader、用户App它们各有各的职责。FUS是ST提供的固件升级服务跑在M0上默认出厂就有负责升级无线栈、管理安全和系统配置。无线栈是整个蓝牙协议栈的实现也跑在M0上用户App通过GAP/GATT接口调用它。所以做OTA之前先梳理清楚你要更新哪部分、保留哪部分、以及升级失败时还能不能回退。AN5247的核心价值就是围绕这个复杂场景给出了一套在STM32WB上做无线固件更新的推荐流程。它不是那种“点几下鼠标就完成”的教程更像是一张地图告诉你哪些地方能走、哪些地方要设路障。1.2 AN5247提出的整体思路AN5247把固件更新分成两个层面。第一个层面是用户应用程序的无线更新也就是通过BLE或者其他无线通道把M4核的App镜像传到设备里写入预先划分好的区域然后通过跳转机制让新App运行。第二个层面是系统级固件更新包括FUS和无线协议栈的更新这通常不推荐由用户App在运行时直接做而是通过ST官方配套工具或FUS服务来完成。主要原因在于无线栈与M0的安全模型深度绑定ST没有完全开放这部分源码。你如果硬要在用户态去升级无线栈一旦掉电、断连、地址写错M0那侧很可能没法恢复只能重新用ST-Link连接SWD来救砖。因此在产品设计阶段我会把“App OTA”和“系统组件升级”拆成两条路径前者做成用户可自助触发的功能后者则限制在工厂或售后维修模式。在实际工程里我见过不少团队只把AN5247里App OTA的部分拿来做却忽略了Bootloader的作用。实际上所有能稳定工作的无线OTA都至少需要一个最小Bootloader。这个Bootloader负责检查启动标志、校验新固件完整性、决定是从运行区启动还是从下载区搬运新固件。有了这一层即使OTA传输到一半断掉旧固件依然还能跑。AN5247的方案围绕的就是这种以Bootloader为中心的分区升级思路。2. 分区布局、启动链路与固件包格式动手前的必修课2.1 内存布局与A/B分区STM32WB系列根据具体型号可能有256KB到1MB的Flash但无论容量多大分区规划都直接决定OTA能不能做。官方SDK里通常给出了一个默认分区表如果你做AN5247的示例工程一般会看到类似下面的角色划分FUS区域、无线栈区域、用户Bootloader区域、App运行区、OTA下载区或备份区以及保留的数据区。我在自己项目里常用的一张表是区域起始地址/说明职责FUS由ST出厂配置占用Flash头部管理无线栈升级安全服务无线协议栈紧随FUS之后存储BLE/802.15.4栈M0运行用户App不可改用户Bootloader独立分区复位后首先执行检查启动标志校验签名跳转AppApp A当前用户应用运行区M4核业务主程序App B/下载区临时存放OTA接收的新镜像传输完成后校验并触发切换用户参数区独立小块保存升级状态、版本号、日志这种方案并不是严格意义上的双A/B镜像。A/B分区通常指两个完全对等的App区当前从A启动写入B下次切换后从B启动再升级时写A。而STM32WB这类内部Flash资源比较“精致”的芯片很多项目会采用“运行区临时下载区”的方式升级完成后由Bootloader把下载区的新固件整体搬到运行区或者通过地址重映射直接跳转。AN5247里也体现了类似的思想。不管哪一种核心都是保证“当前还能运行的程序”始终存在不会在升级过程中被覆盖。对于空间相对宽裕的型号我会优先选A/B方案因为回滚逻辑最简单。如果Flash吃紧可以用下载区方案但下载区必须大于最大固件体积并且Bootloader要支持擦除和复制操作。很多OTA诡异问题都是因为下载区和实际固件大小边界没算好写着写着越过了分区边界把别的区域冲掉。2.2 启动链路从Bootloader到AppSTM32WB复位后M4核会从Flash起始位置开始执行。如果有用户Bootloader系统先进入Bootloader。Bootloader要做什么首先检查是否收到了OTA升级请求比如某个升级标志位被置位或者下载区存在合法的新固件。如果有就做签名校验、版本号比较再决定是复制还是跳转。如果没有升级请求就直接跳到App区运行。这里有个容易被忽略的细节STM32WB的M0核需要M4核来释放启动。即使复位了M0无线栈也并不是上电就自动跑的而是由M4的代码通过“释放协处理器”的方式启动。所以Bootloader阶段如果做得太粗暴直接把整个Flash初始化掉有可能把无线栈的加载过程打断。我的做法是在Bootloader早期不碰M0需要的RAM和IPC标志仅在App里完成无线栈初始化。另一个关键是向量表偏移。用户App如果被烧录到0x08030000那么App里的中断向量表也要相应偏移。在Cortex-M上这通常是一个宏定义VECT_TAB_OFFSET需要在system_stm32wbxx.c里配置或者在代码里调用SCB-VTOR APP_START_ADDRESS。很多OTA之后无法进入中断或者进不了低功耗模式都是这个偏移没有写对。AN5247里反复强调App编译时必须使用和Bootloader约定一致的地址就是这个原因。2.3 OTA包格式和签名校验无线升级最容易出事的不是传输而是“传了一个错误的固件上去”。曾经有个项目因为版本号写错现场设备升级后反复重启。幸好当时做了回滚判断否则就是批量事故。所以OTA包格式必须在最初定义好至少包含Magic、版本号、固件长度、哈希值、签名和固件数据。在AN5247推荐的ST生态组合里通常会用STM32TrustedPackageCreator生成带签名的固件包。Bootloader在启动校验阶段会先验签名和哈希验不过就直接丢弃保持旧固件继续运行。这个过程对量产产品尤其重要OTA包在无线传输的过程中可能被干扰、被截断甚至被第三方抓包重放。只有做了签名校验才能保证设备不会被乱刷或误刷。哈希一般用SHA256签名可以用ECDSA或RSA。STM32WB内部带有硬件加密单元做SHA256的速度还行但Bootloader区域如果空间紧张你要权衡放哪些校验算法。我在一个BLE外设项目里只用SHA256加固定密钥做报文完整性校验签名验证放在Bootloader里做App升级时先验证Bootloader传来的镜像哈希。这样既保证安全又把复杂度控制住了。3. 基于BLE的OTA实现从协议栈到应用层3.1 为什么选BLE做无线传输通道STM32WB本身就是一颗原生支持2.4GHz的SoCBLE OTA是最顺理成章的方案。产品能通过蓝牙升级不需要额外接USB、串口或网线现场维护成本低。相比UART OTABLE的有效传输距离、抗干扰能力、配对鉴权机制都更适合消费类设备。像运动手环、穿戴设备、传感器节点、门锁基本都是靠BLE做FOTA。但BLE传输也有明显限制吞吐量有限且收到的数据是串行流不可能像SD卡拷贝那样一次写一大块。以STM32WB的BLE 5.0能力在DLE支持下单个Packet可以到244字节但实际吞吐还取决于连接间隔和射频环境。如果稳妥点按MTU为128字节来算100KB固件可能要传20到30秒。对于用户而言这个时间是可以接受的前提是你把进度反馈做好别让人盯着空白屏幕猜。正因为BLE传输“慢且容易断”所以OTA协议设计必须考虑断点续传或者至少失败重传机制。AN5247里给出的思路是在GATT层定义一个OTA服务通过不同Characteristic实现命令控制、数据流和状态反馈。实际操作中控制通道和数据通道分开能减少状态机混乱。3.2 OTA服务和协议设计STM32WB的BLE OTA服务可以自己定义也可以用SDK里现成的OTA Profile。典型结构是三个CharacteristicControl Point、Data Transfer、Status/Notification。Control Point用来发送开始升级、结束升级、获取版本等指令。Data Transfer用来传送固件二进制内容。Status用来让设备主动通知客户端当前状态比如“收到第几包”“校验失败”“正在擦除Flash”。一次完整OTA触发流程我习惯这么设计手机端连接设备发现OTA服务。设备端主动通过Status特征上报当前App版本和芯片剩余Flash空间。手机端选择固件文件解析固件包头通过Control Point发送“Start OTA”命令携带包长度、版本、SHA256摘要。设备端收到后先检查长度和地址边界再回复ACK。手机端等待设备端进入接收状态后开始通过Data Transfer特征分块发送数据。每发一块设备端写入下载区Flash然后回复状态或通知。所有数据块发送完毕手机端发送“Finish OTA”。设备端对下载区整个固件做二次校验校验OK后置位升级标志软复位。Bootloader启动后校验搬运或跳转新App完成后清除升级标志。这套流程看起来不复杂真正写代码时很容易在状态机上翻车。最常犯的错是手机端发了Start OTA但设备端还在处理上一个连接事件导致丢指令。因此我建议把所有OTA指令统一放入一个队列由M4核任务逐个处理而不是直接在BLE回调函数里执行。BLE回调里只做赋值和置标志一旦在里面做Flash擦写整个射频栈都会被阻塞连接就断。3.3 分块大小、超时重传与Flash擦写时序BLE MTU协商是最先要解决的事。默认23字节MTU时用户数据总共只有20字节传100KB固件需要接近5000包效率极低。STM32WB默认支持通过aci_gatt_update_characteristic_value等接口配置MTU手机端一般也会发起MTU交换。实测下来MTU从23改成247以后吞吐能提升好几倍。如果你做的是私有App建议初始化时把Preferred MTU设为最大并兼容到设备支持范围内的值。分块大小不能只看MTU还要看接收端的处理能力。我通常选200字节左右一块留一点缓冲。如果一次发送244字节设备端在写Flash时如果处理不过来蓝牙栈的Buffer可能溢出表现就是接收一半卡住。调试时可以用一段几百字节的测试固件反复传观察吞吐和丢包。Flash擦写是OTA里最大的“卡顿源”。STM32WB擦除一个扇区往往需要几十毫秒甚至更慢如果在擦除期间BLE连接事件到了数据包就会丢。常见做法是把擦除和写入分离在开始接收数据之前先把目标下载区所有扇区擦除一遍之后每收到一块数据如果对应扇区已擦好就纯写入。写入过程相对快。更稳一点可以在擦除阶段通过Status特征通知手机端“Erasing”让手机端暂停发送。当Flash特别大时这个暂停时间能到几百毫秒但总比断连好。3.4 双核协同M0收包、M4写Flash如何不打架STM32WB的BLE协议栈跑在M0而Flash控制器由两个核共享。双核对Flash同时操作会冲突所以SDK里通常会通过底层保护机制来处理。但如果你自己操作Flash寄存器必须小心M0正在处理蓝牙事件时M4如果去擦写Flash可能会导致RF事件丢失。反过来M0一旦在广播或连接事件中持续访问FlashM4的写操作也会被推迟。我的方案是建立一套“双核互斥”的业务规则OTA过程只在M4核上做Flash写入M0只负责RF数据收发。M0收到完整一包后放到RAM缓冲区通过IPC通知M4取走。M4写完Flash后清除标志再告诉M0可以继续发下一包。这样可以尽量避开M0在RF事件中直接操作Flash。AN5247的做法本质上也是基于这种分工它不鼓励用户在M0中断里处理大量数据。4. 实操过程搭环境、跑通一次完整OTA升级4.1 硬件准备和开发环境如果手里有一块NUCLEO-WB55RG直接拿它做实验最省事。没有板子的话自己做板子也可以但必须留出SWD接口最好再预留一个UART口打日志。OTA调试没有日志输出会非常痛苦。软件方面我建议安装STM32CubeIDE、STM32CubeProgrammer、STM32CubeWB固件包以及STM32TrustedPackageCreator。手机端可以直接用ST BLE Toolbox或自己写一个测试App。SDK版本尽量和AN5247匹配。STM32WB的固件演进较快不同SDK版本里Wireless Stack的地址和FUS版本可能有差异。如果你用的是某个旧应用笔记配套的工程却下载了最新版固件包可能会发现Flash布局不一致。我实际踩过这个坑照着老工程的链接脚本写结果把所有地址都偏移了0x4000一运行就HardFault。后来统一到同一个SDK版本重新生成了工程才解决。4.2 修改链接脚本与App启动地址打开SDK里的BLE OTA示例通常会有两个工程Bootloader和Application。如果你不想用自带的分区可以自己改链接脚本。关键点查看你的芯片Flash起始地址和大小然后给用户App单独分一块区域。假定我的App打算放在0x08030000那么链接脚本里的FLASH起始地址就写0x08030000长度则根据分区表固定。在App工程里除了链接脚本还要检查system_stm32wbxx.c是否定义VECT_TAB_OFFSET或者直接在主函数入口加一句SCB-VTOR 0x08030000UL;这句话必须在所有外设中断使用前执行否则程序一进中断就会去读原来的向量表。我见过不少人把它放在SystemInit之后结果GPIO中断一直不触发排查半天才发现向量表没改。4.3 生成带签名和校验的OTA镜像生成OTA升级包时我通常先把App编译生成的bin文件用STM32TrustedPackageCreator加上头部并签名。头部字段至少包括固件大小、版本号、目标地址、校验和。签名用的私钥由自己保存公钥和哈希算法则编译进Bootloader。这样即使别人拿到OTA包没有私钥也无法伪造。如果你只是本地测试可以先用STM32CubeProgrammer直接导出当前App的内存镜像作为升级文件再用脚本拼接一个简单头部。比如import struct, hashlib fw open(app.bin, rb).read() header bOTA1 struct.pack(I, len(fw)) hashlib.sha256(fw).digest() open(app_ota.bin, wb).write(header fw)这个例子只为说明格式量产时建议用官方签名工具和更完整的包里鉴权流程。4.4 实测一次完整升级流程把Bootloader、无线栈和初始App烧进板子后打开手机App观察广播包连接设备然后选择OTA升级包。我的测试步骤是先看设备端串口输出版本号再在手机端发送Start OTA确认设备回复ACK然后开始传输。等进度条走完设备会自动断开并复位。重新连接后读取App版本确认版本号已经变化。如果整个过程顺利一个100KB左右的App大约需要20到40秒。如果发现进度条卡住大概率是Flash擦写超时或MTU协商失败。此时不要急着改代码先看串口日志如果设备端已经收到全部数据但没有复位就去查升级标志是否写入成功、Bootloader是否进入。5. 常见问题与排查技巧实录5.1 升级后不跳转、固件跑飞优先查这4个地方OTA后最常见的症状是设备黑屏或反复复位。第一个要查的是App编译地址和实际烧录地址是否一致。第二个是Bootloader跳转前是否关闭了中断、是否清理了SysTick和NVIC。第三个是Vector table有没有设置正确最好在跳转前显式写一次SCB-VTOR。第四个是下载区和运行区如果重叠或者拷贝固件时把Bootloader地址覆盖了。我遇到过一种比较隐蔽的情况App本身没问题但Bootloader在跳转前保留了某些外设寄存器比如RTC或看门狗这些状态被新App继承后直接触发异常。解决办法是在Bootloader跳转前把所有外设Reset一遍再关闭全局中断最后设置好MSP和PC跳到App。另外如果OTA固件做了签名校验Bootloader校验失败时会回退到旧App但用户看到的现象同样是“没有升级成功”。这时需要把校验失败的原因打印出来比如CRC不匹配、版本过低、目标地址非法。AN5247里这些错误码通常是枚举类型最好直接映射成可读字符串。5.2 BLE传输断连、Flash写入失败、进度卡死BLE断连往往是距离或干扰导致但也有软件原因。比如设备端在Flash擦除时长时间不应答手机端认为超时断开。解决办法是擦除前通过通知告诉手机端“Busy”或者延长时间参数。我用STM32WB时会把连接间隔调到30到50ms周围环境嘈杂时有一定抗丢包能力。Flash写入失败通常和地址对齐有关。STM32WB的Flash写入要求半字或字对齐很多OTA包头部是任意struct拼的没做对齐写入时抛HardFault。因此我在协议解析层强制做memcpy到4字节对齐的RAM缓冲区再调用Flash写入函数。进度卡死还有一种常见原因是双核缓冲区的数据没及时消费。M0已经把数据塞满了IPC队列M4还在处理其他任务导致后续数据包没地方放。这种情况下我会把BLE连接事件里收到的数据直接放到一个大环形缓冲区由M4主循环里专门一个OTA状态机去消费缓冲区大小最好能覆盖3到5个BLE事件的数据量。5.3 调试FOTA的独家经验串口日志、断点与双核复位OTA调试一定要有日志而且要分级。Bootloader阶段打印“Enter Bootloader, flag...”App阶段打印“App version...”OTA服务打印“Receive packet idx...”。在无OS环境下打印过多会影响时序所以我通常把日志做成环形缓冲通过UART DMA输出。现场日志级别只开错误和状态调试时再开详细。用Keil或STM32CubeIDE调试双核有点麻烦因为M4和M0是独立的调试会话。我的做法是先用J-Link或ST-Link连接M4只在下M4侧打断点如果怀疑无线栈问题再用STM32CubeMonitor-RF查看射频连接事件不打断M0。升级完成后做一次全片读取对比Flash内容是否和固件包一致能快速定位是写入数据的问题还是传输的问题。6. 更进一步RT-Thread OTA与云平台OTA的集成思路6.1 本地BLE OTA与RT-Thread OTA的互补如果你已经在STM32WB上跑了RT-Thread会接触RT-Thread OTA组件它定义了分区表、固件包管理、下载和断点续传等一整套框架。RT-Thread OTA本来更多面向通过网口、WiFi模块、4G模块做固件升级但在STM32WB上也可以复用它的分区管理和升级流程把底层传输替换成BLE。也就是把BLE收下来的固件数据写入RT-Thread OTA定义的download分区然后调用Bootloader切换。这种方式的好处是业务代码和底层传输解耦。我试过一次把RT-Thread的ota_package解析逻辑直接放在BLE OTA服务后面手机端发来的固件仍然是二进制包但设备端会先用RT-Thread的固件包头解析它。这样以后如果产品增加了一个WiFi模块需要改成HTTP OTA只需要替换传输层分区和校验逻辑不用重写。6.2 云平台OTA会改变哪些环节像AWS IoT OTA这类平台会把固件分发、版本呈现和设备策略管理放到云端。设备端先从云端获取OTA任务再下载固件最后完成本地校验和切换。STM32WB如果接的是外部WiFi/以太网模块云平台OTA确实可以管理App镜像但无线栈和FUS更新通常仍然不通过云平台走。原因很简单无线栈的升级包和ST的签名体系绑定如果平台没法获取你的私钥和授权就会遇到限制。云平台OTA还会引入“用户策略”平台端可以配置哪些设备允许升级、升级哪个版本、分批次推送。这种策略对大批量设备很有价值。但从设备端视角无论云端怎么下发最终本地Bootloader要做的校验、签名、分区切换是一样的。所以我一般建议先本地把AN5247的OTA流程调通再上云否则云端任务下发后本地Flash布局没配好反而容易大面积失败。6.3 A/B分区是通用解法但别过度设计前面提到A/B分区在STM32WB上很实用。它不仅是本地OTA的保险杠在RT-Thread OTA和云平台OTA里也是通用做法。两个App区其中一个是激活区另一个是待写入区升级完成后通过记录在Bootloader里的标志位切换。这种方案的回滚很快如果新App启动5秒内没上报心跳Bootloader就切回旧App。对电池类产品尤其重要。但要提醒一点A/B分区需要双倍App空间Flash小的型号可能装不下。如果产品固件有128KB而整个用户区只有256KBA/B各128KB勉强可以但数据存储区就没有了。这时候可以选择“运行区下载区”方案升级过程中Bootloader做整体拷贝也算是伪A/B。关键还是根据产品实际容量和升级安全等级来权衡不要为了追求概念而把系统做复杂。最后说一点个人体会。OTA看起来是一键升级实际上是把分区、签名、双核、无线协议都串起来的系统工程。我在做STM32WB无线固件更新时光是启动跳转就折腾了半个星期想清楚向量表和Flash目标地址之后才顺起来。如果你也是第一次接触不要急着写代码先把AN5247里的内存分配和启动流程对着自己板子过一遍很多坑自然就绕开了。
返回列表