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

资讯详情

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

STM32WB双核架构下实现稳定BLE OTA的完全指南

STM32WB双核架构下实现稳定BLE OTA的完全指南 STM32WB 这颗芯片我第一次真正把 OTA无线固件更新做进量产产品时最大的感受就是它比普通单片机麻烦的地方全藏在双核架构里。应用核是 Cortex-M4射频核是 Cortex-M0蓝牙协议栈在 M0 上跑。你想给设备做无线固件更新不只要操心 M4 这边的 Flash 分区、跳转、校验还得照顾 M0 的协议栈状态、FUS 保留区以及两个核共享总线导致的并发问题。这篇文章不翻译官方应用笔记也不复读例程代码而是把我从“在开发板上跑通 OTA Demo”到“把 OTA 放到产品上稳定用”的整个经验整理出来。适合正准备在 STM32WB 系列上做 BLE 产品、又不想在 OTA 上翻车的朋友。文中会聊到双核架构的底细、传输通道怎么选、升级包怎么做、A/B 分区怎么落地、BLE 传输层有哪些坑最后还会有一个我实际遇到的失败案例排查链路。1. 先摸清 STM32WB 的双核底细OTA 到底在升级哪个“核”很多人拿到 STM32WB 的第一反应是把它当成“带蓝牙的 STM32L4”这句话只对了一半。它不是所谓的协处理器方案而是两个完全对等的 Arm 内核跑在同一个芯片里Cortex-M4跑你的应用代码、业务逻辑、OTA 状态机。Cortex-M0跑射频协议栈BLE、802.15.4/Zigbee/Thread以及无线相关的底层服务。两个核之间通过 IPCCInter-Processor Communication Controller收发消息RAM 有一部分是共享的。M0 运行的协议栈不是你自己写的而是 ST 发布的二进制固件需要通过 FUSFirmware Upgrade Service来安装或升级。这里就引出了第一个容易搞混的概念STM32WB 的固件更新其实分两个级别。第一级是协议栈更新。比如 BLE 协议栈从 1.x 升到 1.y或者产品需要从 BLE 切换到 Zigbee/Thread这个操作会动到 FUS 管理的专用区域普通用户应用无权直接写必须走 ST 定义的升级流程。这个级别的升级一般不会放在产品 OTA 里给终端用户批量执行量产之后极少碰协议栈除非你的射频功能本身要升级。第二级才是用户应用 OTA。也就是 M4 上的 App你的业务代码。绝大多数场景下讨论的 OTA 都是这个。设备从服务器、手机或者网关拿到新固件镜像写入 Flash 中的应用分区然后跳转执行。后面的内容全部围绕这一级展开。1.1 FUS、系统 Bootloader 和协议栈区有三块区域不能乱动STM32WB 的 Flash 因为有了 FUS 和协议栈比普通 STM32 多出不少限制。我刚开始规划 Flash 布局时犯过一个错误想当然地把 App 放在 0x08000000结果烧进去后上电跑不起来。后来查资料才搞明白Flash 开头有一部分区域被系统 Bootloader 和 FUS 相关代码占用用户不能直接覆盖。具体地址每个型号、每个协议栈版本不完全一样ST 的 CubeProgrammer 能显示完整布局。以 STM32WB55 系列 1MB Flash 为例用户 App 的起始地址一般要避开 Flash 最前面的系统保留区同时 Flash 后面还有一块协议栈区由 FUS 管理。你编译 App 时的链接脚本必须显式把这些区域排除在外。这一点直接影响 OTA 分区设计。你不能像在 F 系列上那样直接从 Flash 头划分 Bootloader 和 App而是得先去读一下当前芯片的完整 Flash 布局再决定 A/B 分区放在哪里。我见过一个同事把 F 系列的工程迁移过来Flash 地址几乎没改结果设备跑起来后无线功能时好时坏——因为他的 App 边界和协议栈区域重叠了。1.2 双核对 OTA 方案的影响不只在分区上双核带来的麻烦还不止分区。第一M4 在擦写 Flash 的时候M0 还在跑协议栈两个核共享 Flash 总线。如果 M4 擦写 Flash 的瞬间 M0 正好去取指或者读参数表轻则等待重则出错。稳一点的 OTA 实现里写 Flash 前会先给 M0 发一条消息让它暂停无线活动等工作完成再通知它恢复。这个细节在官方例程里有体现但很多人只抄代码不关心为什么遇到随机失败就懵了。第二你写的 App 如果要用蓝牙传固件数据进来走的还是 M0 协议栈。也就是说OTA 状态机跑在 M4但数据收发跑在 M0两者通过 IPC 和共享内存交互。调试的时候一个串口打印在 M4一个 BLE 抓包在 M0两边你都得看住。第三FUS 区升级协议栈和 App 升级用的跳转逻辑是完全不同的两套。做应用 OTA 时千万不要去碰 FUS 管理的协议栈区。我见过有人用一个全片擦除操作把好好的蓝牙设备变成只有系统 Bootloader 的砖头。虽然可以通过 STM32CubeProgrammer 重新烧回来但产线上一旦发生就是批量事故。2. 四种 OTA 传输路线选型BLE、USB、UART 与自定义无线通道STM32WB 本身没有以太网也没有 Wi-Fi所以它的 OTA 传输通道设计基本取决于你的产品形态。我在项目初期花了比较多时间做选型这里直接给一个对比表。传输路线使用通道开发成本适合场景BLE OTABLE GATT 服务低官方有例程手表、传感器、便携低功耗设备USB 路线USB DFU 或自定义 Bulk 端点中带 USB 口的网关、工具类产品UART 路线串口 Ymodem 或自定义协议低产线烧录、售后维修口自定义无线LoRa / Sub-GHz / 私有 2.4G高远距离、无蓝牙需求、专网场景2.1 BLE OTA大多数低功耗设备的默认选择BLE OTA 是 STM32WB 上最自然的路线。STM32CubeWB 里提供了 BLE OTA 的示例工程基本思路是设备端实现一个 GATT 服务手机或主机通过写特征值把固件分包发过来设备收到后写入 Flash。好处是手机端用现成的 nRF Connect 或者自己的 App 就能发数据不需要额外硬件。这个路线特别适合带电池的低功耗小设备。我自己做过一款传感器整机平均电流只有几十微安OTA 时功耗会临时升高但升级完马上降回待机水平对产品形态来说完全可接受。2.2 USB 和 UART调试和产线场景更顺手USB 路线可以跑 ST 的 USB DFU Class也可以用自己的 Bulk 端点。USB 的优势是速度快、传输稳定缺点是产品必须有一个 USB 口否则方案直接废弃。对有 USB 的网关类设备来说这是个很舒服的升级通道。UART 路线在调试阶段特别好用。我的习惯是先把 BLE 传输部分临时禁用用串口把完整的 OTA 流程验证一遍——分包、写入、校验、跳转——流程稳定之后再把 BLE 传输层接回去。这样能把“传输层的问题”和“Flash 层的问题”分开排查少走很多弯路。产线烧录时串口 OTA 也是通用的兜底方案。2.3 自定义无线通道和云端中转如果产品用 LoRa、Sub-GHz 或者私有 2.4G你就得自己实现类似 BLE 的分包、确认、重传机制工作量会上一个台阶。好处是不用和蓝牙协议栈纠缠传输逻辑完全由自己掌控。还有一种很常见的部署形态设备是 BLE 终端但产品要支持远程升级。这时候普遍做法是用手机 App 或者 BLE 网关从服务器拉取固件包再通过 BLE OTA 下发给设备。云端的用户策略、权限控制由后端管理设备端只需要把 BLE OTA 这套协议做得足够可靠。像 AWS IoT 这类平台的 OTA 服务主要面向有网络连接的设备STM32WB 这类 BLE 终端通常挂在网关下把网关拉到的固件“翻译”成 BLE OTA 推送给终端是一种很务实的组合。3. 制作升级包与签名在 Ubuntu 上打包带版本头和 CRC 的镜像OTA 第一步不是写设备端代码而是先把要传的 bin 文件打成一个带头部信息的升级包。我见过不少项目直接把裸 bin 文件通过 BLE 发送设备端根据固定 Flash 地址写入。这样虽然能跑但有个隐患如果传输校验只靠 BLE 链路层的 CRC单包丢失还能重传但固件版本不对、包体长度被改、甚至拿错产品的固件刷了进来设备端完全没有防御手段。一个自包含的升级包头部非常关键。我在多个项目里沿用的头部结构如下字段大小说明Magic4 字节固定 0x5A5AA55A用于识别是否为有效升级包Version4 字节版本号例如 0x0102 表示 V1.2Length4 字节固件数据长度CRC324 字节对固件数据部分计算 CRC32Product ID2 字节产品 ID多型号共用一套升级流程时很有用3.1 Python 打包脚本在 Ubuntu 上用 Python 脚本生成升级包非常方便也容易接进 CI。下面是一个可直接使用的示例脚本。import struct import zlib import sys def make_ota_package(bin_path, out_path, version0x0102, product_id0x01): with open(bin_path, rb) as f: payload f.read() header struct.pack( IIIIH, 0x5A5AA55A, # magic version, # 版本号 len(payload), # 固件长度 zlib.crc32(payload) 0xFFFFFFFF, # CRC32 product_id # 产品 ID ) with open(out_path, wb) as f: f.write(header) f.write(payload) if __name__ __
返回列表