ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32WB5x BLE OTA卡死原因分析与分层超时复位策略

STM32WB5x BLE OTA卡死原因分析与分层超时复位策略 前几天又遇到一次典型的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 的 FUSFirmware 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_ACCESSM4 没有对应权限。这些错误码能帮你判断问题出在 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 启动层回退把最后一道防线放在bootloaderOTA 中最重要的一道防线不是“超时复位”而是“启动时验证”。无论你采用双 Bank 还是单 BankBootloader 都要在跳转到应用之前验证目标镜像的有效性。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传输层超时后中止升级但保持广告等待重连。结果设备能重新被发现手机端重新连接后可以重新传输虽然浪费时间但可靠。策略 CFUS 安装阶段从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 里把最后的现场数据读出来比盲猜高效得多。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表