
做低功耗蓝牙产品的朋友应该对STM32WB不陌生。这颗芯片最大的卖点是双核Cortex-M4跑业务应用Cortex-M0专门跑射频协议栈BLE、Zigbee、Thread这些无线协议栈都是官方编译好的固件你直接调用就行。但卖点往往也是痛点一旦牵扯到OTA普通单核MCU那套“bootloader APP分区 串口下载”的办法在这颗芯片上就不够用了因为要升级的不止是你的应用还有那颗M0上的协议栈固件搞不好还要升级FUS本身。ST官方的AN5247正是针对这个场景给出的一套完整参考方案怎么处理无线下载、怎么在双核之间协调、怎么保证升级过程安全和可回退。这篇文章就围绕这份应用笔记结合我实际在项目里调STM32WB OTA的经过把关键环节、设计思路和踩过的坑一次性讲清楚适合正在用STM32WB做BLE产品、准备上远程固件更新的开发者参考。1. 项目背景与整体设计思路为什么STM32WB的OTA比普通MCU更绕很多工程师第一次拿到STM32WB时会习惯性地按单核MCU的思路规划OTA结果很快撞墙。这里面的核心矛盾不是“无线传输怎么写”而是这颗芯片在物理层面就把固件拆成了多块每块的升级路径完全不同。1.1 双核架构带来的新问题先对比一下普通MCU和STM32WB在OTA上的差异。普通MCU的OTA流程一般是bootloader检查升级标志从串口、网口或无线模块接收固件写入APP分区校验后跳转执行。整个系统里只有一个可执行体升级逻辑相对线性。STM32WB不一样。它内部同时运行着两个独立的固件M4核上的用户应用固件也就是你写的业务代码M0核上的无线协议栈固件比如BLE Protocol Stack。这两个固件在系统Flash里是分开存放的。升级用户应用时不能动协议栈区否则正在跑的BLE连接会直接断掉反过来升级协议栈时也不能指望用户应用帮忙完成FUS的安装流程。更麻烦的是Flash里还可能有FUSFirmware Upgrade Service它是M0侧一个更底层的管理程序负责协议栈的安装、升级和安全校验。这就带来一个很实际的问题你要升级的“固件”到底是哪一层每一层的升级通道、校验方式、失败恢复策略都不一样。很多项目死磕半天调不通OTA归根结底是没把这三层的关系理清楚。1.2 AN5247方案的总体思路AN5247的整体思路不是让开发者从零写一套OTA协议而是充分利用STM32WB已经具备的系统Bootloader和FUS能力在应用层暴露一组基于BLE GATT的OTA服务把“手机或者网关通过蓝牙把固件传给设备”这件事跑通。整个方案里要区分三条升级链路升级对象执行者典型触发方式固件来源用户应用系统BootloaderGPIO/应用内OTA服务触发跳转手机App通过BLE下发无线协议栈FUSST工具或应用内FUS命令ST固件包内无线栈bin文件FUS本身FUS的专用升级流程ST工具ST固件包内FUS bin文件我在前期规划时最容易犯的一个错是把“无线协议栈升级”和“用户应用升级”当成一回事结果在代码里找FUS调用接口花了两天。实际上用户应用升级完全可以不碰FUS而协议栈升级又必须走FUS二者井水不犯河水。1.3 为什么复用官方链路而不是完全自研很多论坛上有人问“STM32WB能不能像普通MCU一样自己写个Bootloader放在用户Flash里然后从BLE收数据直接写应用区”。技术上可行但我不建议在量产初期就这么干。原因有几个系统Bootloader已经支持通过BLE进行固件下载这套链路是官方验证过的稳定性有保障自己写无线Bootloader意味着要在M0协议栈还没完全启动的情况下处理BLE连接难度和工作量会成倍增加FUS负责无线栈升级这套东西虽然文档写得晦涩但实测下来比手撸M0侧代码可靠得多。官方方案的问题是定制空间有限比如断点续传、差分升级这些需要自己扩展。所以更合理的策略是先用AN5247的方案把整个OTA流程跑通验证无线链路和Flash分区没问题之后再决定要不要在应用层叠加自己的升级协议。可以说AN5247最大的价值是给了我们一个“能跑的基线版本”。2. 核心机制拆解安全启动、FUS与无线下载链路AN5247这套方案能成立靠的是几个底层机制相互配合。如果不理解这些机制后面调试任何问题都会像在黑盒子里猜。2.1 安全启动与信任根的作用STM32WB出厂时ROM里就有一段不可修改的启动代码。芯片上电后会先执行这段代码再做安全校验然后才跳转到后续固件。FUS本身也承担着校验者的角色。简单理解这就是一个“信任根”机制芯片只认被正确签名的固件。我把这个机制类比成小区的门禁系统芯片是门禁FUS是保安固件包是访客。访客进门必须出示带正确签名的身份证件。没有这个签名无论访客说什么保安都不会放行。落实到OTA上这意味着开发者手里有一把私钥用来给发布的固件签名设备里烧录的是对应的公钥升级时Bootloader或FUS用公钥验证固件签名签名不通过固件直接拒绝写入。很多第一次接触ST安全方案的团队最容易犯的错误是把私钥随便存在共享网盘或者代码仓库里。私钥一旦泄露别人就能伪造你的固件签名整个设备的安全体系就形同虚设。我在项目里对私钥的管理要求是离线保存、双重备份、专人保管、绝不入库。2.2 无线下载的业务流从GATT服务到Flash写入AN5247中用户应用 OTA 的服务通常由一组GATT特征组成我习惯把它们分成三类控制特征Control外部设备写入特定值比如写0x01表示“进入OTA模式”数据传输特征Data用来接收固件内容大文件分块传输状态反馈特征Status/NotifyAPP上报升级进度、当前状态和错误码。完整流程大概是手机连接设备找到OTA服务用户点击“开始升级”手机向控制特征写0x01应用收到命令停止广播、断开当前连接、通知M0停止射频活动应用在Flash里写入升级标志然后跳转到系统Bootloader系统Bootloader启动后读取升级标志通过BLE重新与手机建立连接手机开始分包发送新固件Bootloader接收数据写入目标区域或外部Flash缓存全部数据接收完成校验通过后提交到用户应用区设备复位新固件启动。注意第8步这里有个关键设计官方典型方案里是先把固件数据缓存到外部Flash比如MX25R1635F这类SPI NOR Flash等完整接收并校验后再一次性拷贝到内部Flash。为什么不直接往内部Flash写因为BLE传输速率有限大固件传输需要很长时间如果边收边擦内部Flash一旦中途断连内部Flash里就会出现一个半残的固件轻则升级失败重则变砖。用外部Flash做暂存区可以先把数据完整接收下来校验通过后再整体搬运可靠性会高很多。2.3 FUS版本、无线栈版本和应用版本三者的匹配关系这是STM32WB项目里一个非常容易踩坑的地方普通MCU完全没有这个概念。FUS版本决定它能支持哪些无线栈版本。比如你想升级到某个新版本BLE Stack但设备的FUS版本太老FUS会直接拒绝安装甚至返回一个让人摸不着头脑的错误码。我之前调试时遇到过一次“无线栈升级失败提示存储区状态错误”查了半天资料才发现是FUS版本太低先升FUS问题就解决了。所以我在项目里维护了一张版本兼容性清单组件当前项目版本升级前必须满足的条件FUS0.7.x只有它升级后才能装新版无线栈BLE Stack1.14.x应用代码编译时依赖该版本API用户应用1.0.x与BLE Stack版本匹配不能跨版本混用这里提醒一下应用代码一旦升级了BLE Stack用户应用固件通常也需要同步重新编译和升级因为你在M4侧调的BLE API如果跟M0实际运行的协议栈版本不匹配轻则莫名其妙地复位重则直接跑不起来。3. 实操全过程从零搭好STM32WB的无线固件更新这一部分我按照自己实际操作的顺序来写尽量把每个步骤的关键点都展开。假设你手里有一块NUCLEO-WB55RG开发板或者自制的WB5x模块底板。3.1 准备工作与工具链我个人的建议工具清单如下STM32CubeProgrammer烧录、查看FUS版本、配置选项字节、升级无线栈都靠它STM32CubeWB固件包包含FUS固件、无线栈bin文件、示例工程和签名工具IAR或STM32CubeIDE编译M4侧用户应用手机App我用的是ST BLE Toolbox官方出品的工具可以直接用来验证OTA流程带SWD接口的调试器比如ST-Link前期调试绕不开。准备工作里第一件事不是急着写代码而是先把手上的芯片摸清楚。用CubeProgrammer连上芯片后切到FUS相关的标签页读一下出厂FUS版本号。这个信息决定你后面能不能直接升级到目标版本的无线栈。3.2 初次烧录、安全配置与密钥管理拿到一颗全新的STM32WB我建议按以下顺序初始化连接ST-Link打开STM32CubeProgrammer读取芯片信息确认型号、Flash大小、当前FUS版本在FUS会话下生成密钥对并保存好私钥文件。这一步生成的是用于固件签名验证的公私钥对设备端会保留公钥私钥必须下载到本地妥善保管使用ST固件包里的无线栈bin文件通过FUS接口安装目标BLE协议栈烧录预先编译好的用户应用最简单的方式是用官方示例工程直接编译一个带OTA服务的Demo配置选项字节使系统Bootloader可用或者确认用户应用可以通过软件跳转进入Bootloader。生成密钥这一步很多新手会因为“选哪个算法、密钥长度多少”而纠结其实按官方工具的默认选项走就行。关键点是私钥文件在生成之后最好立刻复制到加密U盘或者密码管理器里不要在开发机上裸奔。3.3 用户应用工程中的OTA服务集成STM32CubeWB固件包里的示例工程比如BLE_TransparentMode或专门的OTA相关例程已经帮你把大部分BLE服务搭好了。在此基础上我需要确认两件事第一OTA服务的GATT特征是否注册成功。如果手机App扫描不到OTA服务检查一下服务初始化代码是否在BLE启动之前被调用以及服务的UUID是否和工具端匹配。第二控制特征的回调里是否正确处理了“进入OTA模式”的命令。我在代码里做的逻辑大概是这样static void OTA_Control_Handler(uint8_t cmd) { switch (cmd) { case 0x01: /* 停止广播断开当前连接 */ APP_Stop_Advertising(); /* 让M0释放射频资源时间和顺序很关键 */ BLE_Stack_Stop(); /* 保存OTA标志然后跳转系统Bootloader */ Save_Ota_Flag(); Jump_To_SystemBootloader(); break; default: break; } }注意这里的BLE_Stack_Stop()不是随便调用就行的。我在第一次调试时省掉了这一步结果跳转到系统Bootloader后手机一直搜不到设备的OTA广播后来看了官方论坛才知道是M0的射频资源没释放被之前的连接占着系统Bootloader根本没法正常工作。3.4 跳转系统Bootloader的实现要点跳转系统Bootloader的代码网上能找到很多版本但直接照搬容易出问题。下面是典型实现static void Jump_To_SystemBootloader(void) { uint32_t boot_addr SYSTEM_BOOTLOADER_ADDRESS; void (*sys_boot_jump)(void) NULL; /* 关中断、清中断标志避免跳转后残留中断响应 */ __disable_irq(); HAL_RCC_DeInit(); HAL_DeInit(); /* 设置主栈指针 */ __set_MSP(*(volatile uint32_t *)boot_addr); /* 获取复位中断向量地址 */ sys_boot_jump (void (*)(void))(*((volatile uint32_t *)(boot_addr 4))); /* 跳转 */ sys_boot_jump(); }这里SYSTEM_BOOTLOADER_ADDRESS的具体值取决于你的器件型号不能随便填。你在AN5247或参考手册里能找到准确地址STM32WB系列一般是0x1FFFxxxx这种系统Flash区域。如果填错了跳转过去必定HardFault。另外跳转前建议把OTA标志保存在一个掉电不丢失、且Bootloader能读到的地方。我用的是用户应用所在Flash区域之外的一个专用扇区这样Bootloader启动时先检查这个扇区的标志再决定是直接跑用户应用还是进入OTA接收流程。3.5 使用手机App完成一次完整的无线升级验证当工程编译烧录完成也确认能进入系统Bootloader后就可以做端到端验证了。我用ST BLE Toolbox的操作路径大致是手机扫描设备并连接找到OTA服务核对面板上的控制按钮、数据传输通道和状态显示点击“进入OTA模式”此时设备会断开连接在工具里选择编译好的用户应用bin文件注意是M4侧应用固件不是无线栈文件工具通过BLE把固件分包发送设备端接收、校验、写入升级完成后设备自动重启重新开始广播上位机或手机读取新的版本号确认升级成功。第一次完整跑通这个流程时我只改了一个打印字符作为版本差异确保升级后能观察到变化。强烈建议你也这么做因为如果新老固件肉眼区分不出来你会怀疑自己到底有没有升级成功。3.6 无线协议栈升级的实操路径用户应用升级跑通后还有一个重要的升级场景是无线协议栈。这个升级不经过系统Bootloader而是走FUS接口。用CubeProgrammer操作时路径大概是连接SWD进入FUS会话选择对应的无线栈bin文件例如stm32wb5x_BLE_Stack_full_extended_fw.bin点击升级等待FUS返回成功状态复位后重新烧录一份与新版协议栈匹配的用户应用否则M4侧应用可能跑不了。如果是产品已经出货用户手里没有SWD调试器那协议栈升级就需要在应用层通过FUS命令实现AN5247里也描述了这套流程。不过这属于比较高级的玩法了涉及到M0侧的IPC命令交互建议先在开发板上把流程完全吃透再往产品里放。4. 实际踩坑记录与排查技巧STM32WB的OTA调试过程我估计每个认真做过的人都有过一段“怀疑人生”的时间。后面再遇到类似问题基本都能对照着快速定位了。这里把我踩过和见过的坑整理一下。4.1 高频故障速查表现象可能原因排查与解决手机扫描不到设备广播没开、广播参数被修改、应用没初始化BLE先确认示例工程能否正常广播再看代码是否提前进入低功耗进入OTA模式后一直等不到连接M0射频资源没释放、跳转地址错误跳转前调用协议栈停止函数检查Bootloader地址是否与芯片匹配固件传输到一半断开BLE连接参数过于激进、外部Flash擦写阻塞导致超时加大Supervision Timeout考虑先缓存到RAM再批量写外部Flash无线栈升级失败FUS版本过低、无线栈包与安全区配置不匹配先升级FUS再核对Flash安全区起始地址升级后设备起不来应用固件与无线栈版本不匹配、签名校验未通过重新编译配套应用检查签名公钥是否正确烧录升级后手机需要重新配对无线栈升级把安全数据库擦掉了这是正常现象产品设计时提前考虑重新绑定流程4.2 几个比较隐蔽的坑有几个问题不是照着官方文档操作就能避开的我单独拎出来说一下。第一个是外部Flash的大小和地址映射。ST官方的OTA例程里默认外部Flash的缓存区大小是固定的你的固件如果超过这个大小写入就会越界轻则升级失败重则把外部Flash里的其他数据抹掉。改代码前一定要算好固件文件大小、签名头大小、加密填充大小三者和缓存区的关系。第二个是断电恢复。官方方案虽然用外部Flash做缓存但如果设备在“外部Flash拷贝到内部Flash”的过程中断电还是会进入一个尴尬状态内部Flash里的新固件不完整外部Flash里的缓存数据又已经标记为“可用”。我建议在产品化时加一个简单的状态机判断每次启动时检查内存区的固件完整标志不完整就保持Bootloader模式等待重传。第三个是连接参数与升级速率的平衡。BLE的传输速率不是固定值它取决于连接间隔、从机延迟、MTU大小等一系列参数。很多人为了追求升级速度把连接间隔调得很小结果传输稳定性急剧下降。我的经验是先以保守参数跑通全流程再逐步优化速率每次优化后做至少连续10次升级测试确认稳定后再提效。4.3 测试流程方面的建议最后聊一点测试层面的经验。OTA功能一定要做专项的“断电测试”。我在实验室里放了一个可以远程控制通断的电源自动化跑升级流程时随机断电然后检测设备能不能恢复。这个测试能暴露很多靠正常流程根本发现不了的问题。版本管理也要提前规划。升级验证要有一个明确的方法判断当前跑的是哪个版本最方便的做法是在固件里固化版本号并通过BLE服务暴露给外部读取。自动化测试里直接通过GATT读版本号比每次用调试器看内存靠谱得多。还有一点是防呆设计。OTA功能一旦上线就会面临“旧版本App升级新版本固件”“新版本App升级旧版本固件”这类混搭问题。建议在服务端或App端维护一个固件兼容性列表设备端固件也要在接收升级包之前做一次版本号检查不支持的版本直接拒绝接收避免把设备刷成半砖。结尾回头看这个项目我最深的一点体会是STM32WB的OTA不是“调通一个下载功能”那么简单而是一套涉及双核协作、安全体系、版本兼容和异常恢复的系统工程。FUS、无线协议栈、用户应用这三层每一层都有自己的升级链路和约束条件任何一个环节没考虑到位都会在量产阶段集中爆发。所以我建议刚开始接触的朋友不要急着往自己的业务代码里塞OTA逻辑。先老老实实按照AN5247配合一款官方开发板和示例工程把用户应用升级完整跑一遍再升级一次无线协议栈最后再手动模拟一次升级中断和断电。这几步走完你对这套机制的理解会比读十遍文档都有用。最后分享一个小窍门把“进入OTA模式”做成一个可以被按钮或串口命令触发的功能不要只在App里点。因为后期调试时手机连接经常会因为各种原因失败有一个本地触发入口能帮你快速确认Bootloader链路本身是不是好的。这个习惯帮我省了大量排查时间你用了也会回来谢我。