前几天又遇到一次典型的STM32WB5x BLE OTA卡死问题:手机端显示固件包传输到一半,进度条不再前进;设备侧此时本来应该在收包、写Flash,但就是没有任何回包。等了几分钟,手机端BLE连接超时断开,之后不管怎么扫描都找不到设备广播。重新上电也一样——好像固件升级把整台设备送进了某个“黑洞”状态。
这个问题的标题很直接:STM32WB5x BLE OTA gets stuck mid-update with no way to reconnect — is a firmware-side timeout/reset the right approach? 我先说结论:不能用一套简单的 timeout + reset 逻辑去扫尾,但也不能完全不处理。关键是要找到卡在哪个阶段、谁在卡、以及复位后从哪里启动。这篇文章就围绕这个思路,把 STM32WB5x 上 BLE OTA 卡死的原因、排查手段、分层超时设计和可落地的复位策略完整拆开聊。
1. 卡死现场:从“传输中”到“彻底失联”的三种表现
1.1 下载阶段卡住:数据传着传着就没有ACK了
第一种卡死最容易被误判为“蓝牙断连”。现象是:手机APP通过 BLE 自定义服务向设备持续发送固件分片,前面几十个包都正常,设备也回了 Write Response 或 Notify ACK;在某个分片之后,设备突然不回任何 Confirm 了。手机端要么等本次 GATT 写操作超时,要么等到 BLE 连接事件丢失,最终报错断开。
这种卡在下载阶段的问题,根源几乎都不在协议栈,而在 M4 应用核。
STM32WB5x 的双核架构里,M0+ 核负责 BLE 协议栈,M4 核负责用户的业务代码。GATT 写请求来了,M0+ 会把数据通过 IPCC 放进共享内存,然后通知 M4 去取。M4 收到数据后要做的是把分片写入目标 Flash。如果这段 Flash 写入逻辑写得很“霸道”——比如在擦写整个扇区时长时间关中断,或者HAL_FLASH_Program一次写入后再做 CRC 校验,那么 M4 处理完这个分片的时间可能超过 BLE 的连接间隔,M0+ 侧的 ACK 队列就会越积越多。
积压到一定程度,M0+ 的接收缓冲区满,协议栈开始丢弃新的 GATT 写请求。手机端发出去的包没人应答,连接事件也会因为没有及时交换空包而触发链路监督超时。此时设备并没有死,只是 M4 忙到忘记喂 BLE 协议栈,看起来就像被卡死了。
1.2 安装阶段卡住:FUS 一张接管,设备直接消失
第二种卡死比下载阶段更迷惑人。所有固件分片都传完了,校验也通过了,然后 APP 端发送“开始安装”的命令;设备收到命令后可能回了一帧 ACK,然后连接断开。此后无论怎么扫描,设备再也不会广播。重新上电也一样,设备仿佛人间蒸发。
如果你用 ST-Link 接上芯片,很可能会发现 M4 还在跑,但 M0+ 停在某个 FUS 服务里——这时设备并不是“坏了”,而是进入了无线栈固件升级(Wireless Stack Update)的安装流程。
STM32WB5x 的 FUS(Firmware Upgrade Service)运行在 M0+ 核上,负责安全安装无线栈固件、管理密钥等。当 FUS 开始安装或者擦写无线栈区域时,BLE 协议栈必然停止运行,所有无线活动都会终止。这个阶段本来就不应该期望还能搜索到设备。问题在于,FUS 安装完成后可能需要几秒到几十秒,一旦安装失败或 FUS 卡在某个内部状态,设备就永远停留在“没有无线栈可跑”的局面,自然也不会有广播。
1.3 复位后卡住:看起来是醒了,其实在裸奔
第三种情况出现在“timeout/reset 已经写进固件”的设备上。工程师发现 OTA 卡住后,在 M4 侧加了一个看门狗,或者收到“开始安装”后倒计时 10 秒强制NVIC_SystemReset()。结果是:设备确实能重启,但重启后既不广播、也不进入正常应用,调试器连上去发现程序跑飞或死在 HardFault。
这通常是复位后的启动入口出了问题。OTA 过程如果已经把新的应用固件写入了当前运行分区,但复位后 bootloader 没有做“镜像有效性检查”,直接跳转到一个写了一半或者 CRC 校验失败的镜像,CPU 就会取指失败,卡在启动阶段。
还有更隐蔽的一种:OTA 期间使用独立看门狗 IWDG,但喂狗逻辑放在主循环里。当 M4 阻塞等待 FUS 回复时,主循环停摆,IWDG 超时复位。运气好时复位后旧应用还在,但升级标志被写坏;运气不好正赶上 Flash 擦写中途,复位导致 Flash 内容不完整,设备便再也没能起来。
2. 双核架构下的OTA:到底是谁在“卡”谁
2.1 先理清 M4、M0+ 和 FUS 的职责边界
要处理卡死问题,首先得把 STM32WB5x 里的三角关系搞清楚。M4 核跑应用逻辑,也就是你写的产品代码;M0+ 核跑 BLE 协议栈,对外呈现为“无线核”。两个核通过 IPCC 硬件单元和共享内存通信。M4 不需要知道 BLE 连接的底层细节,它只负责从共享内存里取数据和发送命令。
FUS 是 M0+ 上的一套系统服务,类似一个运行在无线核上的 mini bootloader。M4 可以通过 IPCC 向 FUS 发送命令,比如查询版本、安装无线栈、删除无线栈、管理安全密钥等。FUS 有自己的安全状态和错误码。当执行覆盖无线栈区域的写操作时,FUS 会暂停当前无线栈运行,因此 BLE 连接一定会断。
很多第一次做 STM32WB OTA 的人会以为“BLE OTA”就是把数据写到 Flash 然后跳转,但实际上要区分升级对象:
- 升级用户应用固件:数据写入应用分区,通常与 FUS 无关。
- 升级无线栈固件:必须通过 FUS 或类似机制,不仅要写 M0+ 的代码区域,还要更新蓝牙栈版本和配置。
- 同时升级两者:先升级用户应用,再升级无线栈,或反过来,中间需要明确的状态管理。
如果没有做这个区分,就会在应用 OTA 过程中试图调用 FUS,或者在无线栈 OTA 过程中关闭了 FUS,导致整个状态机错乱。
2.2 应用固件OTA和无线栈OTA是两条完全不同的路
应用固件 OTA 通常走双 Bank 方案:当前从 Bank1 启动,新固件写入 Bank2,写完后置位切换标志,复位后 bootloader 检查标志并尝试从 Bank2 启动;如果 Bank2 无效则自动回退 Bank1。这种方式的好处是即使新固件写坏了,Bootloader 还能救回来,前提是 Bank1 的旧固件仍然可用。
无线栈 OTA 就复杂一点。无线栈代码和 FUS 安全性绑定,不能由 M4 直接写入,只能让 FUS 去处理。M4 要做的是把无线栈固件包传给 FUS,再由 FUS 完成校验、擦写、安装。这个过程中 M4 与 FUS 通过 IPCC 异步交互,不能一边等 FUS 一边又把 BLE 协议栈杀掉。
从调试角度看,应用 OTA 卡住大多能在 M4 用户代码里找到原因;无线栈 OTA 卡住则要优先查 FUS 返回的错误码和 M0+ 状态。两者排查手段完全不同,所以“一堆 timeout 逻辑走天下”在双核 OTA 里根本行不通。
2.3 “无法重连”的深层原因往往不在BLE超参数,而在于状态机
遇到设备不能重新连接时,很多人的第一反应是调 BLE 广播参数:缩短广播间隔、打开可发现模式、延长连接超时。这些参数调整可能有效,但治标不治本。
真正的深层原因往往是没有把 OTA 状态持久化。比如:
- OTA 下载了一半,设备复位,旧应用还在,但 SFlash 里某个“升级进行中”的标志位被误写成了“升级完成”。
- 新固件写入 Bank2 后,由于复位发生在标志位写入之后、CRC 校验完成之前,Bootloader 检查标志时认为可以切换,结果跳到损坏的 Bank2。
- FUS 安装无线栈时,M4 因为超时执行了复位,复位后 FUS 尚未完成安装,M0+ 没有任何可运行的无线栈,设备自然无法广播。
这些问题不是靠“多等几秒”或“多复位几次”能解决的。你得有一个“升级状态机 + 启动验证 + 回退路径”,让设备无论什么时候掉电,都能从持久化状态里判断:该继续升级、该回退旧版本、还是进入恢复模式。
3. 排查套路:从复位原因到FUS状态都翻一遍
3.1 上ST-Link先看芯片还能不能连上调试口
遇到 OTA 卡死,别急着改代码,先把芯片接到 ST-Link,打开 STM32CubeProgrammer。如果连接成功,你能读出器件 ID、Flash 大小、当前 FUS 版本和无线栈版本。这一步能立刻确认设备是否“活”着,以及是哪个核在跑。
如果连接失败,优先检查目标板电源,以及在 CubeProgrammer 连接设置里选择正确的接口模式和复位模式。有时设备运行在不正常状态,需要把 BOOT0 拉高,进入系统 Bootloader 再连接。不过 STM32WB5x 的内部 Bootloader 走的是 USART/USB,不是 SWD,所以如果 SWD 连不上,通常说明芯片内部时钟或电源出了大问题,或者调试引脚被 M4 代码复用并配置成了模拟输入。
3.2 第二眼看RCC_CSR:这台设备是“怎么死的”
如果 SWD 能连上,下一步我会读取复位原因寄存器RCC_CSR。这个寄存器能告诉你最后一次复位是由谁触发的:
- 上电复位 / 欠压复位
- 外部复位引脚
- 独立看门狗 IWDG 复位
- 窗口看门狗 WWDG 复位
- 软件复位 NVIC_SystemReset
- 其他原因
在项目里用一个调试命令把复位原因打印出来,会非常有价值。比如你发现设备每次 OTA 卡死后的复位原因是 IWDG,那说明问题出在“没有及时喂狗”;如果复位原因是软件复位,说明你的超时逻辑真的执行了NVIC_SystemReset(),但显然它没有解决问题。
读取代码如下:
uint32_t csr = RCC->CSR; if (csr & RCC_CSR_RMVF_Msk) { RCC->CSR |= RCC_CSR_RMVF_Msk; // 清除复位标志 } // 判断 if (csr & RCC_CSR_WDGRSTF_Msk) { // IWDG复位 } else if (csr & RCC_CSR_SFTRSTF_Msk) { // 软件复位 }我在实际项目中遇到过很隐蔽的情况:设备上电后RCC_CSR里的 IWDG 复位标志一直存在,而代码里没有在启动早期清除它,导致 Bootloader 以为上一次 OTA 异常退出,不断触发回滚。这种“历史复位原因”没有清理干净,也会造成奇怪的启动行为。
3.3 第三步翻FUS状态和无线栈版本
STM32CubeProgrammer 有一个专门的 FUS 页面,打开后能直接看到 FUS 版本、无线栈版本、当前运行的安全状态,以及上一次 FUS 命令的执行状态。
如果上位机显示无线栈区域为空,或者 FUS 状态不是 Running,那基本可以判断设备是卡在无线栈升级的安装阶段。此时你要么重新通过脚本刷入一套完整的无线栈固件,要么用 Flash 全擦除后重建。注意,STM32WB5x 的无线栈区域有安全校验,单纯在 MDK 里随便写一个地址是没用的,必须走 FUS 或官方工具链。
在自定义 OTA 流程中,我建议把 FUS 命令的返回码纳入日志系统。比如:
FUS_ERROR_IMG_NOT_AUTHENTICATED:固件包签名校验失败。FUS_ERROR_OP_ABORTED:操作被中断。FUS_ERROR_NO_DEVICE_ACCESS:M4 没有对应权限。
这些错误码能帮你判断问题出在 OTA 包的合法性,还是出在 FUS 命令交互流程。
3.4 如果连SWD都连不上,优先怀疑Flash被写花
假设设备彻底没反应,SWD 也连不上,这时不要上来就怀疑芯片坏了。先强制进入系统 Bootloader,把调试口重新救出来。如果 STM32CubeProgrammer 能连接系统 Bootloader,再对 Flash 做整片擦除,擦除后一般都能通过 SWD 重新连接。
这种情况多发生在 FUS 安装过程中被 M4 侧硬复位打断。虽然 STM32WB5x 在 Flash 擦写上做了一些保护,但复位发生在闪存编程时序中间,仍可能导致无线栈区域处于不稳定状态。而一旦无线栈区域内容损坏,M0+ 无法启动协议栈,整颗芯片就表现为“BLE 完全消失”,但 M4 可能还在空转。
所以从设计层面讲,任何可能中断 Flash 编程的操作都必须格外小心。尤其是无线栈升级期间,不是万不得已不要用 M4 侧看门狗去打断 FUS 流程。
4. 固件侧timeout/reset,能解决什么,不能解决什么
4.1 超时机制的正确分层:传输层、安装层、启动层
回到标题里的问题:firmware-side timeout/reset 到底是不是正确方案?我的答案是:timeout 是必须的,但 reset 要分级、分层,不能一把梭。
可以把整个 OTA 流程分成三个层次:
| 层次 | 典型超时场景 | 正确处理方式 | 是否建议直接复位 |
|---|---|---|---|
| 传输层 | 长时间没有收到下一个固件分片 | 中止本次下载,保持广播,等待重连 | 通常不需要复位 |
| 安装层 | FUS 安装无线栈超时 | 查询 FUS 状态,记录错误,再决定复位 | 谨慎,需先保护标志 |
| 启动层 | Bootloader 检测到镜像无效 | 回退到上一分区或进入恢复模式 | 通过跳转逻辑完成,不属于普通复位 |
4.2 传输层超时:该中止,但不该急着复位
传输层的超时最简单。M4 收到固件分片后,维护一个last_rx_tick。如果超过某个阈值(比如 5 秒)没有收到新的分片,就认为本次传输已经失败。此时正确的做法是:
- 把 OTA 状态机切到
OTA_STATE_FAILED。 - 记录失败的偏移量和错误码。
- 释放本次 OTA 占用的缓存。
- 判断当前正在运行的应用是否有效:如果有效,继续跑旧应用,同时继续保持 BLE 可连接。
- 如果旧应用已经被部分覆盖(比如采用了单 Bank 原地升级),则应让设备停留在“可发现且可重新下载”的状态,等待手机端重连后重新把完整固件包传一遍。
为什么不要立即复位?因为传输层超时往往只是手机空中的射频干扰、BLE 连接参数不匹配、或手机端 APP 异常,此时设备本身没有挂。你复位自己,反而会让手机端彻底丢掉连接;不复位,至少手机端还能通过当前连接继续补发几包。
我在实测中发现,BLE OTA 传文件时,如果手机屏幕熄灭或系统进入省电模式,连接会进入 dormant 状态,几秒钟没有数据是很正常的。把传输超时设成 2 秒会误伤,设成 10 秒又会让卡死恢复太慢。一般建议根据实际数据包间隔设置,比如你的 APP 每 20ms 发一包,可以设 2 秒;如果每 100ms 发一包,可以设 4~5 秒。
4.3 安装层超时:需要“先确认状态,再决定是否复位”
安装层指的就是“固件传输完成,开始擦写/切换”这个阶段。如果是应用固件双 Bank 切换,整个过程很短,M4 直接操作 Flash,超时主要关注擦写时间是否异常。如果是 FUS 安装无线栈,过程可能跨越数秒到几十秒,此时 M4 与 FUS 基于 IPCC 异步交互。
这个阶段最忌讳的就是“M4 等不到回复后,直接调用NVIC_SystemReset()”。
为什么?因为 FUS 可能在擦写无线栈 Flash,此时硬复位可能会打断 FUS 的擦写流程。STM32WB5x 的 FUS 内部有一定恢复机制,但你在错误的时间点打断它,可能让无线栈区域处于“存在但校验失败”的状态。与其这样,不如在进入安装前就设计好:
- 先通过 FUS 查询当前状态,如果 FUS 还活着,让它继续跑,或者主动发送一个放弃命令。
- 如果查询 FUS 也得不到响应,才考虑复位,并且复位后要检查无线栈版本和 FUS 错误码,决定是否进入“恢复模式”。
4.4 启动层回退:把最后一道防线放在bootloader
OTA 中最重要的一道防线不是“超时复位”,而是“启动时验证”。无论你采用双 Bank 还是单 Bank,Bootloader 都要在跳转到应用之前验证目标镜像的有效性。
STM32WB5x 的双 Bank 结构非常适合做 A/B 升级。你可以在 Bootloader 里做以下检查:
- 读取用户分区头部的镜像 CRC 或签名。
- 如果 CRC 有效,正常跳转。
- 如果 CRC 无效,检查另一个 Bank 的 CRC。
- 如果另一个 Bank 有效,切换启动。
- 如果两个 Bank 都无效,进入串口或 BLE 恢复模式。
我在项目里把这条规则固化为“启动链”:Bootloader -> App A / App B。OTA 写入只写非运行 Bank,写完置位 “pending” 标志,复位后 Bootloader 根据标志和校验结果决定正式切换还是回滚。这样一来,即使安装阶段复位时机不对,只要旧 Bank 还完整,设备至少能回到旧版本,而不是“彻底失联”。
回退机制虽然不能替代超时复位,但它能在超时复位做错时兜底。很多“OTA 卡死无法重连”的最终原因,其实是 Bootloader 没有做镜像有效性检查,启动链断了。
5. 一套可以落地的OTA超时复位参考设计
5.1 状态标志和数据布局先行
在设计 OTA 逻辑之前,先确定状态标志放在哪。ST 官方的双 Bank 方案一般会使用几个 Flash 字或 Option Byte 作为升级标志。由于 Flash 写入要擦除,不建议频繁修改同一个字。更稳妥的做法是用一个独立的配置扇区,专门保存 OTA 状态。
一个简单的状态结构体大概长这样:
typedef struct { uint32_t magic; uint32_t ota_state; // 0: idle, 1: transferring, 2: pending_switch, 3: failure uint32_t transfer_offset; uint32_t target_bank; // 目标启动区域 uint32_t last_error_code; uint32_t crc32_of_struct; } OtaPersistentStatus;每次更新状态后,先计算整个结构体的 CRC32,再写入保存区。Bootloader 启动时先检查 magic 和 CRC32,如果校验失败,就认为状态不可信,强制走安全启动路径。别小看这一步,很多 OTA 卡死就是状态标志半写半不写造成的。
5.2 传输超时的计算和喂狗节奏
传输超时不能拍脑袋定。先量一下你的 BLE 连接实际带宽:手机端每隔多少毫秒收到一个 Write Response?如果平均间隔是 30ms,那 3 秒内没有任何包,就说明链路已经断了,或者对端已经放弃。
设计喂狗时也要注意:喂狗不能放在while(1)主循环里,因为 OTA 接收数据时主循环可能长时间阻塞。更好的做法是“事件驱动 + 后台任务轮询”:
void OTA_BleOnWrite(uint8_t *pData, uint16_t len) { // 接收更多数据 OTA_Context.rx_data_happened = true; OTA_Context.last_rx_tick = HAL_GetTick(); // 写Flash或者缓存 } void App_MainLoop(void) { uint32_t now = HAL_GetTick(); if (OTA_Context.state == OTA_STATE_TRANSFERRING) { if (now - OTA_Context.last_rx_tick > OTA_TRANSFER_TIMEOUT_MS) { OTA_AbortTransfer(); } } // 其他任务 HAL_IWDG_Refresh(&hiwdg); // 不要在Flash擦写期间长时间不进这里 }如果在接收回调里直接写 Flash,而且 Flash 擦写时间比较长,要注意在擦写前开一个足够宽容的看门狗窗口。或者干脆把接收缓冲做深一点,数据先放进 RAM,后台慢慢写 Flash。这样 BLE 协议栈能更及时地回 ACK,连接不容易断。
我在实测中比较推荐“RAM 缓冲 + 后台 Flash 写入”的模式。每次 BLE 收满一个扇区大小的数据,就把数据优先缓存到内部 SRAM 或外部 PSRAM,然后在主循环的批量写入阶段统一写 Flash。缺点是占用 RAM,但能极大降低传输层卡死的概率。
5.3 安装阶段复位策略的伪代码思路
当所有固件包接收完成,进入安装阶段时,我会把流程拆成三步:
void OTA_StartInstall(void) { // 1. 持久化状态:进入 install 阶段 OTA_SaveStatus(OTA_STATE_INSTALLING, target_bank); // 2. 等待FUS或Flash切换完成 uint32_t timeout = HAL_GetTick() + OTA_INSTALL_TIMEOUT_MS; while (HAL_GetTick() < timeout) { HAL_IWDG_Refresh(&hiwdg); // 对FUS安装流程:轮询FUS查询命令 FUS_Status_t st = FUS_QueryStatus(); if (st == FUS_STATUS_READY) { OTA_SaveStatus(OTA_STATE_COMPLETED, target_bank); return; } if (st == FUS_STATUS_ERROR) { OTA_HandleFusError(st); return; } } // 3. 超时处理:先记录错误,再考虑复位 OTA_SaveStatus(OTA_STATE_FAILURE, OTA_GetCurrentBank()); NVIC_SystemReset(); }注意,第 3 步的复位不是首选项。如果 FUS 查询命令能响应,我宁可用 FUS 命令去中止或确认,而不是直接系统复位。只有 FUS 已经无响应、且确认继续等下去没有任何进展时,才执行复位。复位后用 Bootloader 的镜像校验来兜底。
5.4 实测验证:哪些方案能“救回来”,哪些会“越救越死”
我实际测试过三种策略,结果差别很大:
- 策略 A:传输层超时后立即
NVIC_SystemReset()。结果:设备重启后能起来,但如果 OTA 有大量缓存没有落盘,导致写了一半,Bootloader 没有回退逻辑,设备卡死。越救越死。 - 策略 B:传输层超时后中止升级但保持广告,等待重连。结果:设备能重新被发现,手机端重新连接后可以重新传输,虽然浪费时间,但可靠。
- 策略 C:FUS 安装阶段从
HAL_GetTick()到 30 秒超时后硬复位。结果:有几次设备能恢复正常,有几次无线栈区域被写坏,需要用 ST-Link 全擦后重刷。风险很高。
综合来看,最可靠的设计是:传输层尽量不重启,安装层尽量不硬复位,启动层尽量依赖 Bootloader 自动回退。
6. 几个关键决策,和给后来者的建议
回到最开始的问题:firmware-side timeout/reset 是不是正确做法?我的看法是,timeout 必须放在 OTA 的每一层,但 reset 是最后手段,不能放在传输层“每等不到回包就复位”。正确姿势是把 OTA 设计成状态机,每个状态都有明确的超时行为、持久化标志和恢复路径。复位只是所有恢复路径都不生效时的最终兜底。
我在实际项目里踩过坑,尤其建议注意这几点:
第一,不要让 M4 在等待 FUS 回复时完全阻塞。哪怕是一个 5ms 的HAL_Delay,也要保证主循环仍然能喂狗、能处理其他后台任务。
第二,复位之前一定要保存错误信息。你可以把last_error_code和ota_state写到备份寄存器或者 Flash 的专用区域,这样复位后可以通过日志直接看到“上次是为什么复位”。没有这一步,你只能靠猜。
第三,升级无线栈之前,先确认当前 FUS 版本和无线栈版本,确保无线栈固件包与 FUS 兼容。版本不匹配导致 FUS 安装失败的情况,比硬件问题常见得多。使用官方 OTA 例程时也要看清示例中给的无线栈版本,不要随手拿别的版本刷进去。
最后再分享一个排查技巧:OTA 卡死时,用 STM32CubeMonitor 或一个简单的串口日志,把 M4 侧收到的 BLE 数据字节数和 FUS 返回的每一条命令状态都记录下来。我靠这招定位过很多“看起来像玄学”的升级失败——其实只是某个包在传输层丢了,但上层一直没等回来。把日志打印到 Flash 尾巴上,掉电也不丢。这样即使设备彻底失联,接上 ST-Link 还能从 Flash 里把最后的现场数据读出来,比盲猜高效得多。