BlueNRG-LP/LPS OTA无线固件升级:架构设计与工程实践

📅 发布时间:2026/8/29 16:34:06
BlueNRG-LP/LPS OTA无线固件升级:架构设计与工程实践 1. 项目概述BlueNRG-LP/LPS 无线固件升级到底在做什么1.1 芯片定位BlueNRG-LP 与 BlueNRG-LPS 的关系和差异做蓝牙低功耗BLE产品开发的朋友应该都有过这种经历——产品已经量产出货了结果发现一个固件 bug或者客户提了一个需要改协议参数的需求怎么办返厂成本高得离谱让用户用有线调试口升级消费电子产品几乎不会给你留出 SWD 接口。这时候OTAOver-The-Air空中固件升级就成了一种“早知道要做、但真正重视的人不多”的核心能力。ST 的 BlueNRG 系列在 BLE SoC 市场里属于一套非常成熟的方案。BlueNRG-LP 是 ST 主推的低功耗 BLE 5.2 SoC内置 Arm Cortex-M0 内核主频最高 64 MHzFlash 容量从 192 KB 到 320 KB 不等具体看型号后缀RAM 则有 64 KB 到 96 KB 可选。BlueNRG-LPS 是它的低成本版本本质上是一颗经过精简的变体Flash 和 RAM 规格缩水主要面向成本敏感、功能相对简单的产品比如 Beacon、电子价签、传感器节点这一类。但两者在 OTA 升级的机制上是一脉相承的开发接口和 SDK 结构基本一致所以这次的应用笔记核心思路可以同时覆盖这两颗芯片。很多刚接触 BlueNRG-LP 的开发者会把它和 ST 另外一类产品搞混BlueNRG-2 是“射频协议栈分离”时代的产品而 BlueNRG-LP/LPS 已经变成“射频协议栈MCU 一体”的 SoC。这意味着你的应用代码、BLE 协议栈、Bootloader 都跑在同一颗芯片上Flash 排布和启动流程也因此变得更有讲究OTA 的设计也完全不同于以前 MCU 独立蓝牙芯片的双芯片方案。1.2 为什么无线升级是 BLE 产品的刚需先说一个真实案例。我做过一个智能照明项目第一批货发出去之后客户反馈说某个特定品牌的手机连不上设备查了两周最后定位到是芯片协议栈里一个广播参数的兼容性坑。按照传统思路这种问题要么召回、要么让用户寄回来刷固件但我们是做海外市场的召回成本没人扛得住。好在当初提前做了 OTA 功能远程推送了一版新固件三天内 90% 的设备都静默升级成功问题就这么圆过去了。从产品角度OTA 的典型价值主要有这几个修复出厂后暴露的缺陷包括协议栈兼容问题、射频参数调优、应用逻辑 bug功能迭代和合规性更新比如新的蓝牙规范、新 Profile 定义、新传感器算法减少售后成本省掉邮寄、返厂、人工刷写的费用提升产品生命周期管理能力尤其是物联网设备你永远不知道出货之后会碰到什么场景。所以只要你的产品里有 BLE 芯片、有 Flash 空间余量我建议都要考虑把 OTA 做进去。BlueNRG-LP/LPS 在硬件层面已经为这件事铺好了路芯片内部 PKAPublic Key Accelerator可以做固件签名校验Flash 支持双 Bank 切换还有专门为 OTA 设计的 Bootloader 可以参考。你要做的不是从零造轮子而是理解这套机制、在你的工程里正确启用它。1.3 与有线升级的差异不只是“省一根线”有线升级SWD/JTAG和无线升级本质上都是往芯片 Flash 里写数据但工程实现上差别非常大。有线升级时调试器直接通过 SWD 接口访问内核和 Flash 控制器你不需要考虑“当前程序正在跑、Flash里还存着当前程序”的尴尬问题随时可以全片擦除、重写。OTA 就不一样了升级过程运行时芯片正通过 BLE 协议栈接收数据、把数据写入 Flash一旦 Flash 操作不当跑着的代码本身可能被破坏设备直接变砖。所以 OTA 的第一个设计原则是在升级过程中绝不破坏正在执行的代码区。这决定了 BlueNRG-LP/LPS 的 OTA 必须采用“Bootloader 引导 双 Slot 存储”的架构。Bootloader 是一段独立于主应用的启动代码它负责校验、解包、跳转应用区被分成两份一份是当前运行区Slot A一份是备份/接收区Slot B。新固件先写到非工作区校验通过后切换标志位重启后由 Bootloader 决定启动哪个区域。这套思路在很多蓝牙 SoC 上都能看到比如 Nordic 的 DFU 也是类似逻辑。有线升级还有一个隐含优势——调试器可以在任意时刻停止内核、查看寄存器、单步执行。OTA 出问题时你只靠 LED 和 BLE 日志来推断排查起来更依赖设计阶段留的诊断手段。所以做 OTA不只是把“升级功能”实现出来还得把“异常路径”设计出来比如升级中断怎么回滚、版本校验失败怎么处理、日志怎么记录。2. 整体架构设计OTA 是怎么跑起来的2.1 双 Bank 方案与 Flash 分区细节BlueNRG-LP 的 Flash 布局在参考设计里是有明确建议的我这里以一个典型的 256 KB Flash 型号为例给出一种我在实际项目中验证过的分区方式区域起始地址大小内容Bootloader 区0x1000000032 KB启动引导、OTA 管理、BLE DFU 服务部分App 区 ASlot A0x1000800080 KB当前运行的应用固件App 区 BSlot B0x1001600080 KB新固件接收区用户数据/参数区0x1001E0008 KB版本号、Flag、升级状态、配对信息注意地址最前面的 0x10000000 不是笔误BlueNRG-LP 的 Flash 基地址就是 0x10000000和 STM32 的 0x08000000 不同。很多人第一次烧录 Bootloader 时把地址写错导致跳转失败这个要格外留意。Slot A 和 Slot B 的大小直接决定了你能升级多大的固件。如果你的 App 编译出来只有 40 KB那 Slot 分区 80 KB 是完全够用的但如果你的代码超过 70 KB就得考虑把两个 Slot 都放大或者把 Bootloader 压缩一下。Flash 是一种“尺寸换安心”的资源我建议留足 20% 的余量给后续功能迭代留空间。双 Bank 的切换原理其实不复杂Bootloader 在启动时读一个“启动标志”变量比如写在用户数据区的 0x55AA 表示启动 Slot A0xAA55 表示启动 Slot B然后跳转到对应地址。升级完成后并不需要物理拷贝数据只需要改写这个标志位重启后 Bootloader 就会从新分区启动。这就是“双 Bank 切换”的核心——不搬数据只改指针。2.2 Bootloader、协议栈与 App 的协作关系在 BlueNRG-LP/LPS 的 OTA 体系里Bootloader 并不是一个“只在启动时存在瞬间”的角色。ST 默认提供的 Bootloader 工程是一个完整的、独立的小程序它自己带 BLE 协议栈独立运行一个叫做“DFU Service”的 GATT 服务。手机或者主机 MCU 连上设备后通过这个服务发送新固件包Bootloader 负责把数据写入 Slot B。注意这个 DFU 服务跑在 Bootloader 里的不是跑在 App 里。这就引出一个关键设计决策升级过程是由 Bootloader 接管 BLE 连接还是在 App 里接管ST 官方推荐方案是 Bootloader 模式App 收到升级指令后保存必要参数、断开 BLE 连接然后软复位跳转到 BootloaderBootloader 启动后广播一个特殊的设备名通常带 “DFU” 后缀等待主机重新连接、开始传输固件。这种做法的好处是升级代码和业务代码完全解耦App 里不需要集成一大坨传输解包逻辑Bootloader 的 Flash 写入流程可以做得更专注、更稳定。但这里有一个容易被忽略的坑因为 Bootloader 也运行 BLE 协议栈它广播时的 MAC 地址、连接参数、GATT 服务 UUID 和 App 是完全不同的。你的手机端工具如果默认连接原来的设备名升级时会发现怎么都搜不到设备。解决办法是在 App 里触发升级前用某种方式通知用户“接下来设备会变成 DFU 模式请等待重新搜索”或者干脆在手机端做自动化检测到设备断开就自动开始搜索带 DFU 特征的新设备。2.3 无线升级的数据通路从 BLE Air 到 Flash 的完整流程整个 OTA 升级的数据流我拆成 8 步来说明App 端当前运行的应用接收到升级指令这个指令可以通过私有 GATT 服务、UART 指令、按键触发等方式下发。App 保存必要的上下文比如需要保持的配对信息、升级完要恢复的状态然后清空 BLE 白名单、断开连接。App 写一个“请求进入 DFU”的标志然后调用系统复位跳转到 Bootloader。Bootloader 启动初始化自己的 BLE 协议栈以 DFU 设备名广播、等待连接。主机手机 App 或网关 MCU连接上来通过 DFU Service 下发固件包。固件包被切成 MTU 大小的分块每块带序列号和 CRC 校验。Bootloader 逐块接收、校验、写入 Slot B块与块之间回发确认帧主机收到确认才发下一块。整个固件包传输完毕Bootloader 对 Slot B 的整体镜像做完整校验CRC32 或 SHA256取决于配置校验通过后改写启动标志。Bootloader 复位新的应用从 Slot B 启动完成升级。这段链路里第 4 到第 7 步是最核心的。很多 OTA 失败都发生在第 6 步——如果 BLE 连接不稳定或者 MTU 协商过大导致丢包率高传输过程就会卡住。我之前实测过把 MTU 从默认的 23 字节提高到 247 字节后传输一个 80 KB 的固件从原来的一分多钟缩短到十几秒但代价是丢包率略微上升。所以实际的 MTU 设置需要根据信道质量来权衡这点在后面的实操部分我会再展开。3. 实操配置从零搭建可用的 OTA 工程3.1 开发环境准备与 SDK 版本选择做 BlueNRG-LP/LPS 开发官方推荐的 IDE 是 STM32CubeIDE配合 ST 提供的 BlueNRG-LP SDK。这个 SDK 里有完整的 Bootloader 工程、App 工程和 OTA 服务代码。版本选择上我建议直接用 ST 官网最新的 Release 版本因为早期版本里 OTA 相关的 bug 不少很多是在后续维护版修复的。如果你用的是某个旧 SDK 做量产的项目也要重点关注 SDK Release Note 里关于 DFU 的修复条目。需要准备的硬件和环境如下一块 BlueNRG-LP/LPS 的开发板或者你自己的最小系统板ST-Link 调试器用于首次烧录 Bootloader 和 App一份手机端的 BLE 调试工具可以选用 ST 官方的 ST BLE Toolbox或者第三方的 LightBlue / nRF Connect安装好 STM32CubeIDE 和对应的 SDK 包。注意一个细节如果你的板子上已经烧录过程序要烧 Bootloader 之前需要先做一次全片擦除Full Flash Erase否则旧的 App 代码可能影响 Bootloader 正常运行。我在调试时遇到过几次奇怪的现象——Bootloader 明明编译烧录成功了复位后却跳不进 DFU 模式最后发现是 Flash 里残留的 App 向量表把启动流程搞乱了。3.2 Bootloader 工程配置的核心参数ST 的 SDK 里通常会提供otp或bl相关的示例工程打开之后重点配置这几个参数Flash 分区起始地址。这组地址必须和你的链接脚本Linker Script完全一致。如果 Bootloader 的地址是 0x10000000那么 App 的 FLASH_ORIGIN 就要设置成 0x10008000。一旦错位Bootloader 跳转后直接 hardfault。这个配置同时要改.ld文件里的两处一个是 Flash 的 ORIGIN一个是 RAM 的起始位置App 的 RAM 布局也要避开 Bootloader 占用的部分否则启动后变量区互相踩踏会出现极其诡异的随机 bug。DFU Service 的 UUID 与特征值。ST 默认的 DFU Service 定义了一套标准特征值包含固件数据特征、控制点特征、版本信息特征。如果你打算用自己的手机 App 来对接可以沿用这套 UUID省去改 Bootloader 的麻烦如果希望加密升级需要额外配置密钥校验ST 的 SDK 支持 AES 密钥和签名那么必须在 Bootloader 里烧入对应的公钥App 端用私钥给固件包签名。BLE 连接参数。Bootloader 模式下不建议开启过于激进的信道优化策略默认的连接间隔、监督超时在多数场景下就够用。因为 DFU 最重要的不是连接速度而是稳定性。信道跳频策略选默认即可不用额外改。配置完这些编译烧录 Bootloader然后通过串口日志观察它是否正常广播。这一步如果顺利说明底层的 Flash 分区已经对了可以继续配置 App。3.3 App 端集成 DFU 触发与生成可升级固件App 工程需要做的事情比 Bootloader 少很多但有几个关键点值得讲清楚。第一App 的链接脚本必须把 Flash 起始地址改为 Slot A 的起始地址比如 0x10008000中断向量表偏移也要同步设置。在 Cortex-M0 上向量表偏移是通过在 startup 代码里设置 SystemInit 或直接在NVIC_SetVectorTable完成的。很多人只在链接脚本里改了地址忘了配置向量表偏移结果 Bootloader 跳转过去之后第一个中断就跑飞。第二App 里需要加一个“请求进入 DFU”的入口。最直接的方式是定义一个私有 GATT 服务手机端往某个特征值写入 0x01App 收到后做下面三件事擦除或保存关键数据例如绑定密钥、产品配置延时几百毫秒等待 BLE 协议栈完成断开连接的通知设置一个特殊寄存器值或 RAM 标志然后调用 NVIC_SystemReset()。跳转到 Bootloader 只是复位怎么知道复位之后应该进 DFU 而不是正常启动呢答案就在 RAM 标志或专门的启动标志位里。Bootloader 启动后会检查这个标志如果是“请进入 DFU”就直接走 DFU 流程不再校验 Slot A如果没设置就按正常的“校验并启动 App”流程走。这个机制理解透了你就知道为什么 App 里要在复位前写标志位了。第三生成可升级的固件包。App 编译生成的.bin文件不能直接发给设备因为 Bootloader 需要知道这个固件包的格式、版本、目标地址和校验值。SDK 里通常会提供一个固件打包工具比如 ST BLE Toolbox 里的 DFU 文件生成器你可以把.bin、版本号、目标地址填进去生成.dfu或.ota格式的升级文件。重点强调必须把升级文件的目标地址设置为 Slot B 的地址而不是 App 的 Slot A 地址。如果你用默认的 0x08000000 之类地址Bootloader 会把数据写到错误的地方轻则升级失败重则把 Bootloader 或参数区覆盖掉。3.4 手机端升级实测与传输速率调优手机端我实测过几种工具体验最好的是 ST 官方的 ST BLE Toolbox。它的 DFU 界面设计得比较完整能显示当前设备和 DFU 设备的切换、升级进度、失败重试。用 LightBlue 或 nRF Connect 也可以做但需要你把 DFU Service 的各特征值弄明白手动去走流程比较繁琐适合调试时不方便换设备的场景。实际升级操作流程手机连接 App 的 GATT 服务写入触发 DFU 的指令观察设备断开在 BLE 扫描列表里找到带 “DFU” 字样的设备点击连接选择本地的.dfu/.ota固件包点击升级等待进度条走完设备重启扫描列表里出现正常的应用设备名升级完成。这里有一个调优的关键传输速率。之前提到的 MTU 协商在 Bootloader 里是自动处理的。如果手机端支持更大的 MTU比如 Android 上通过requestMtu(247)Bootloader 在连接后会自动协商到最大值。实测下来MTU 从 23 提到 247传输 80 KB 固件从约 90 秒降到 12 秒左右。如果你觉得速度还不够可以调整 Bootloader 工程里的CONNECTION_INTERVAL和SLAVE_LATENCY。以我实测的数据连接间隔从 15 ms 缩短到 7.5 ms 后吞吐量会提升 15% 左右但功耗也上去了且如果射频环境不好重传率会明显增加。4. 常见问题与排查经验实录4.1 Flash Bank 切换失败的典型表现升级失败里概率最高的一类是数据写进去了、校验也过了重启后却还是旧固件。这种“平静的失败”最让人抓狂因为看起来一切正常但结果不对。我遇到过一次反复排查后定位到是启动标志位的存储位置问题。我在 App 里用了芯片内 Flash 的最后一页存产品参数而 Bootloader 里也把启动标志写到了同一页的不同偏移。两边默认配置不一致导致 App 正常跑起来后会把 Bootloader 刚写好的启动标志覆盖掉。解决办法是统一参数区的地址或者在 App 中把那块区域标记为“保留”禁止写操作。另一类典型问题Bootloader 检查 Slot B 的镜像完整性时发现 CRC 校验失败于是启动 Slot A 的旧固件。表面现象是升级失败一次后设备还能正常工作但下次进入 DFU 时可能会遇到旧固件中的状态残留。我的建议是在 Bootloader 的校验失败分支里除了回滚还要给主机发一个明确的状态码比如 0x03 表示校验失败这样手机端就能把这个错误直接展示给用户而不是让用户看到一个“连接断了”的模糊提示。4.2 升级过程中断连导致的半砖状态升级到一半 BLE 断连这是另一个高频事故。如果断连发生在数据块传输过程中Slot B 里的镜像是不完整的Bootloader 不会改启动标志重启后依然进旧 App设备不会变砖。最危险的是断连发生在“所有数据块传输完成、Bootloader 正在做整体校验”的瞬间——这时候 Flash 里已经有了一份不完整或损坏的镜像如果校验逻辑写得不够严谨可能误判校验通过导致启动时跳进坏代码。预防这个问题的硬性要求是整体校验逻辑必须对 Slot B 的完整镜像做强校验不能只依赖每个分块的 CRC。每个分块的 CRC 只能保证数据块在传输过程中没问题不能保证整份 firmware 的逻辑完整性。我习惯的做法是在固件打包时在文件头写入整个镜像的 CRC32 或 SHA256 摘要Bootloader 全部收完后对 Slot B 实际内容重算一次摘要比对文件头里的摘要值完全一致才允许切换启动标志。另外实测中我发现一个体验上的坑手机端在升级过程中如果切到后台Android 系统可能暂停或杀掉 App 的 BLE 连接这是 BLE 开发的老问题。所以在设计手机端时最好用前台服务的方式持有 BLE 连接并做好断点续传或重连重传的交互逻辑。设备端这边Bootloader 的广播超时时间里可以设置得长一点比如 3 分钟给手机端留足重连的时间窗口。4.3 启动校验与回滚机制的最佳实践完整的 OTA 方案不能只考虑“升级成功”路径还得把“升级失败怎么回滚”设计得明明白白。BlueNRG-LP/LPS 没有硬件上的“双 Flash Bank 自动切换”能力回滚逻辑必须在 Bootloader 软件层面实现。我常用的回滚策略有三层版本号比对固件包头携带一个单调递增的版本号Bootloader 比较新镜像和当前镜像的版本号。如果新镜像版本低于或等于当前版本直接拒绝升级。这样做可以防止误操作刷入旧版本也能在升级异常时避免“降级回滚导致版本变老”的混乱。启动失败自动回滚Bootloader 里设置一个“启动计数”变量。每次从 Slot B 启动时先把这个计数加 1。App 正常运行 30 秒后通过一个命令告诉 Bootloader“启动成功”清零计数。如果计数大于等于 3说明新固件无法正常运行可能启动即崩溃Bootloader 自动将启动标志切回 Slot A。这套机制就像一个“看门狗”能兜住很多运行时崩溃的极端情况。保留最后一份正常配置在参数区里保存每次成功启动的固件版本和关键参数镜像回滚时如果发现参数区也被破坏了Bootloader 可以将参数恢复到上一次成功启动的状态。要注意的是自动回滚计数器的实现有一点细节计数器写到 Flash 后不能频繁擦写否则会损耗 Flash 寿命。我建议不要每次启动都写 Flash而是只在第一次启动 Slot B 时写后续的 30 秒“确认窗口”内只在 RAM 里计数。只有确认窗口超时后才真正触发 Flash 擦写操作把计数清零或标记成功。4.4 典型问题速查表问题现象可能原因排查/解决方式烧录 Bootloader 后不广播 DFUFlash 有旧 App 残留启动向量冲突全片擦除后重烧 Bootloader手机搜不到 DFU 设备广播名称被改、广播超时时间过短、设备没有正确进入 DFU 模式检查 Bootloader 广播配置用 nRF Connect 扫描所有设备升级到 50% 提示失败/断开距离过远、BLE 被干扰、MTU 过大导致芯片处理不过来降低 MTU缩短连接间隔建议近距离重试升级完成后重启仍是旧固件启动标志没写成功、App 的 CRC 校验失败、Bootloader 跳转地址错误检查参数区 Flash 地址、检查 Slot B 的链接脚本地址新固件启动后随机 hardfault中断向量表偏移未设置在 App 启动代码中设置向量表偏移到 0x10008000固件包无法生成目标地址填写错误固件打包时目标地址填 Slot B 地址不是 Slot A 地址升级一次后功耗异常偏高升级后的 App 未重新初始化外设确认 App 从复位流程正常走完外设初始化不要依赖之前的中断状态5. 应用场景与扩展思考BlueNRG-LP/LPS 的 OTA 方案技术核心是统一的但放到不同产品形态里落地的侧重差别很大。简单梳理几个我接触过的场景消费电子智能照明、智能门锁这些设备的用户没有技术背景升级过程必须“静默化”。建议把触发点隐藏在产品日常交互中比如门锁在低电量提示时顺带检查有没有新固件。升级过程中如果用户操作设备必须做好状态冲突处理不能让用户觉得“这把锁升级的时候怎么按不动”。医疗/工业传感器这类设备对数据安全要求高升级必须防篡改、防伪固件。ST 的 Bootloader 支持代码签名校验可以把公钥烧在 Bootloader 里App 和固件包用私钥签名这样即使有人截获了空中数据包也无法伪造固件。这个安全机制值得专门花时间测试验证。大批量部署的 Beacon/标签这类设备数量动辄上千逐台用手机升级不现实。可行的方案是让设备端不依赖手机而是通过一个集中式的网关用私有协议同时管理多个 BLE 设备的固件版本和升级任务。BlueNRG-LP 的 OTA 机制本身就可以支持这种“网关批量升级”的方式只是主控端需要自己实现多连接的调度和并发控制。OTA 后续的数据分析升级上线只是开始还要关注升级成功率的监控和分析。可以在设备端上报固件版本、升级失败错误码、连接重试次数等数据汇总到后台用来判断 OTA 策略是否需要调整。没有这一步OTA 就只是个“能用”的功能无法闭环。6. 一些经验性的补充建议最后聊几个我在实际项目中反复验证过的体会不算什么高深理论但关键时刻能省下大量排查时间。第一把 Bootloader 和 App 的编译输出分开管理。生产烧录时Bootloader 一般只烧一次App 会通过 OTA 频繁更新。如果两者混在一个工程里每次编译都要小心 Bootloader 是否被误改误烧。我在项目里会用 CI 脚本把 Bootloader 作为独立固件归档只在有明确需求时才重新编译和验证。第二升级过程一定要有详细的日志出口。设备正常运行时你可能用不到但一旦现场反馈升级失败仅靠设备端闪烁的 LED 根本不足以定位问题。我在 Bootloader 里保留了 UART 日志输出即使量产机没有引 UART 排针调试板子上也留着测试点把关键步骤都打印出来——广播启动、连接建立、分块接收、CRC 结果、跳转地址——这会让你在排查问题时直接从“盲猜”变成“看日志说话”。第三对 OTA 的触发条件做保护。不是任何时候都适合升级。如果设备电量低于阈值或者正在执行关键任务门锁正在开锁、医疗设备正在测量应该拒绝进入 DFU。这个保护要给用户明确的反馈同时也让系统更安全。我在一个项目中就因为没有做电量保护用户升级到一半设备断电导致 Flash 数据错乱虽然回滚机制兜住了但体验非常差。第四别忽略射频环境对升级的影响。OTA 传输大文件时对射频链路的稳定性要求比普通小数据交互高得多。我在一个金属外壳的产品里测试发现升级成功率只有 60%后来做了天线匹配优化把 RSSI 从 -75 dBm 提到 -60 dBm 左右成功率才恢复到 98% 以上。如果你的产品外壳是金属材质或者天线距离结构件较近一定要在真实环境里反复测试 OTA 成功率而不是只在开发板上验证。BlueNRG-LP 和 BlueNRG-LPS 的无线固件升级底层机制是成熟的真正的难度在于把机制和你的产品形态、用户体验、安全要求融合到一起。按照上面这套思路把 Bootloader、App、打包工具、手机端都跑通你的 BLE 产品就能真正具备“可远程修复、可远程进化”的能力。