拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32WB55 BLE OTA中途卡死?从超时复位到双slot固件回退

这几天一直在调一个STM32WB55的BLE OTA卡死问题固件升级收到一半设备就像断线风筝一样从手机端消失既收不到包也搜不到广播必须用ST-Link强拉回来。网上搜了一圈很多人第一反应是“在固件里加个timeout超时直接reset不就行了”——我先说结论加超时是对的但直接reset是典型的治标不治本甚至可能把设备变成更大的砖头。这个帖子我会结合STM32WB5x双核架构、BLE连接参数和实际OTA流程把这个问题的根因拆开讲清楚并给出一套可以直接落地的firmware侧恢复方案。1. 先还原现场OTA卡在中间到底卡在哪1.1 升级中断开连接的三种典型表现我最早遇到这个问题时一开始并不知道是BLE连接断了还是flash写入失败。后来在几种不同主板上复现发现现象基本可以分为三类。第一种是最常见的手机端App进度条停在某个百分比比如67%、83%然后过一会儿直接变灰提示设备已断开。设备端没有复位还在运行但它的BLE广播已经没有在发了或者说发了也连不上。这时候你用手机重新扫描往往扫不到设备或者偶尔能扫到但连接超时。第二种是设备端直接死机OTA过程中屏幕或者LED不再闪烁调试串口没有任何输出看门狗好像也没起到作用。这种情况通常不是BLE本身的问题而是应用代码在等待某一包数据时陷入了一个没有出口的循环或者flash擦除期间阻塞了某些关键任务。第三种比较隐蔽升级流程走到了最后一步设备已经接收完整个镜像开始做校验和切换但校验不通过设备既没有回到旧固件也没有进入可升级状态就停在一个“半初始化”的bootloader里Android和iOS都连不上表现也是“卡死”。区分这三种现象很重要因为它们对应的修复方式完全不同。如果是第一种重点在BLE连接参数和链路层处理如果是第二种重点在应用层任务调度和timeout机制如果是第三种重点在bootloader的优先级管理和回退策略。1.2 “设备失联”的本质连接已经没了但代码还在跑STM32WB5x和普通MCU不太一样它是双核架构Cortex-M4跑应用代码Cortex-M0跑BLE协议栈。M4想要发蓝牙数据得通过IPC和邮箱机制向M0发命令。这个架构本身没有错但OTA引入了一个非常特殊的问题升级过程中要大量操作内部flash而flash擦写操作会阻塞M0处理蓝牙事件。这就好比你手机在下载大文件突然开始跑一个非常占CPU的压缩任务系统卡顿但下载还在继续。如果这个压缩任务持续太久网络的超时重传机制就会判定连接失效于是手机端直接断开。BLE也一样链路层有supervision timeout机制如果在一个超时窗口内没有收到连接事件连接就会被判定为丢失。更麻烦的是M4侧的固件代码并不知道链路层已经断了。大多数OTA示例程序都是循环等待“收到数据块→写flash→回复ACK”如果一直没收到下一块数据它就永远等在那里既不退出也不复位更不会重新发起广播。这就是标题里说的“gets stuck mid-update with no way to reconnect”的直接原因。所以拆解下来“设备失联”其实由两件事组成第一BLE链路断开导致再也收不到数据第二固件缺少超时机制不知道链路已经断了仍然在原地死等。2. timeout/reset是对的方向但裸复位是错的2.1 为什么不能直接NVIC_SystemReset很多人第一个想到的方案就是既然卡死了加一个超时超时后直接系统复位设备不就能重新跑起来了吗听起来逻辑通实际做起来会踩好几个坑。第一个坑是复位后bootloader不知道应该启动哪个固件。如果你的OTA流程是直接擦除当前运行固件的区域然后再写新固件那么在“擦除完成但写入未完成”的时刻复位设备进入bootloader后会试图从flash里找固件结果发现这一片是空的或者半个旧半个新直接启动失败。有些bootloader遇到这种情况会进入DFU模式这个还算好有些bootloader比较天真直接跳进去执行最终结果是HardFault设备彻底变砖。第二个坑是蓝牙协议栈状态没有恢复。STM32WB5x的BLE协议栈并不是复位后自动回到“可连接广播”状态的它需要M4侧重新调用初始化流程注册GATT服务设置广播参数然后调用aci_gap_start_advertising()。如果复位后你直接跑App主流程没有重新初始化蓝牙那设备确实在跑但手机搜不到和没跑没有任何区别。第三个坑最隐蔽如果OTA过程中修改了协议栈所在flash区域的内存数据而你没有正确保存和恢复复位后连接时可能触发GATT数据库不一致导致手机端报错或者连上之后就断开。我在某个项目里就遇到过设备重启后能广播手机能连上但一订阅notify就秒断原因正是Service Changed特征没有正常处理。所以裸复位不是不能用而是必须在复位前想清楚复位之后执行什么流程、启动哪个固件、蓝牙怎么恢复。这三个问题不解决reset只是把“卡死”变成“换个方式不正常”。2.2 真正的三件套超时 回退 可再连接我的建议是把“加超时”这件事做成一个完整的恢复机制而不是简单在中断里放一个复位。具体来说三个动作缺一不可。第一超时监控OTA接收过程必须有一个时间戳机制。每收到一个有效数据块就更新一次时间戳一个独立任务或定时器周期性检查当前时间与时间戳的差值超过阈值就判定为OTA超时。第二固件回退超时之后不是直接启动新固件而是回退到升级前的旧固件。这个设计的前提是——你的flash足够大能同时保留两个固件。如果你用的是能容纳两份固件的MCU建议优先采用双bank方案如果flash很小只能逐块覆盖升级那必须有强制的保底机制比如把固件先完整接收并写入外部flash全部校验通过后再更新内部flash。第三可再连接回退后设备必须马上进入一个“可被手机发现”的状态重新初始化BLE、打开广播让用户能重新连接。不够的话还要通过某种方式告诉手机端“上次升级失败了”这样App才能决定是重试升级还是恢复使用旧固件。这里有一个常见的误解超时时间设得越长越好这样可以给慢速网络更多时间。实际上BLE OTA不像TCP下载它的传输速率基本是稳定的连接参数定下来之后每秒钟能传的包数量是固定的。如果你的连接间隔是30ms一个连接事件传一个包那就算没有丢包一秒钟最多也就传30多个包再考虑每个包的MTU大小理论速率是可以算出来的。所以超时时间不是拍脑袋定的而是根据你设定的连接参数和预期数据包数量算出来的一般设成理论传输时间的2到3倍比较合理。3. 可落地的固件侧恢复机制3.1 双slot布局与boot标志要支持回退最稳妥的做法是双slot布局一个slot放当前运行版本另一个slot放新下载的版本。升级完成后再切换启动入口。这样即使新版本写入一半旧版本也在设备随时可以回退。我以STM32WB55内部flash为例给你一个参考布局。整片flash按应用镜像大小划分slot A放当前正式版本slot B放OTA新版本。BOOT_APP_FLAG放在某一个固定flash页末尾记录下一步应该启动slot A还是slot B。#define SLOT_A_ADDR 0x08008000 #define SLOT_B_ADDR 0x08080000 #define OTA_STATUS_FLAG_ADDR 0x0803F800 /* 放在slot A后预留区域 */ #define OTA_STATUS_APP_A 0xA5A5A5A5 /* 启动slot A */ #define OTA_STATUS_APP_B 0x5A5A5A5A /* 启动slot B */ #define OTA_STATUS_BUSY 0x00000000 /* OTA进行中 */ #define OTA_STATUS_INVALID 0xFFFFFFFF /* 无有效标记 */bootloader启动逻辑可以这样写int main(void) { uint32_t status *(volatile uint32_t *)OTA_STATUS_FLAG_ADDR; uint32_t crc_a crc32((uint8_t *)SLOT_A_ADDR, APP_MAX_SIZE); uint32_t crc_b crc32((uint8_t *)SLOT_B_ADDR, APP_MAX_SIZE); if (status OTA_STATUS_APP_B crc_b 0) { jump_to_app(SLOT_B_ADDR); } else if (crc_a 0) { jump_to_app(SLOT_A_ADDR); } else { /* 两个固件都不可用进入DFU等待重新升级 */ enter_dfu_mode(); } }这里有一个关键点OTA_STATUS_FLAG不是在收到整个文件后才写入而是在下载刚开始时就写入OTA_STATUS_BUSY然后整个接收过程中保持这个值。也就是说如果中途复位bootloader看到的是OTA_STATUS_BUSY它不会尝试跳转到任何一个slot的新固件而是检查旧固件是否完整完整就启动旧固件。这样即使升级中断设备也不是砖只是回到了旧版本。写入这个状态标志要注意页对齐。STM32WB5x内部flash的页大小按型号不同可能是2KB或者4KB你把标志放在一个独立页避免和代码区混用否则每次写标志都会触发整页擦除会擦掉你不想擦的数据。3.2 应用层OTA超时看门狗在应用层我建议实现一个轻量的超时看门狗。这个不是硬件看门狗而是一个状态机里轮询的分支但它的作用比硬件看门狗更精准因为它能区分“正常空闲”和“升级卡死”。我用的方法是在OTA状态机的每次循环中检查时间戳。typedef enum { OTA_IDLE 0, OTA_START, OTA_RECEIVING, OTA_VERIFY, OTA_READY, OTA_ERROR } ota_state_t; static ota_state_t ota_state; static uint32_t last_block_timestamp; #define OTA_RX_TIMEOUT_MS 5000 void ota_process(void) { switch (ota_state) { case OTA_RECEIVING: if ((HAL_GetTick() - last_block_timestamp) OTA_RX_TIMEOUT_MS) { /* 超时进入错误处理 */ ota_state OTA_ERROR; ota_handle_error(); } break; default: break; } } void on_ota_block_received(uint8_t *data, uint16_t len) { last_block_timestamp HAL_GetTick(); /* 继续处理数据块 */ }在ota_handle_error()里不要直接复位而是完成三件事static void ota_handle_error(void) { /* 1. 标记OTA失败bootloader回退旧固件 */ uint32_t status *(volatile uint32_t *)OTA_STATUS_FLAG_ADDR; if (status OTA_STATUS_APP_B) { write_flash_word(OTA_STATUS_FLAG_ADDR, OTA_STATUS_APP_A); } /* 2. 清理OTA临时变量停止相关定时器 */ ota_deinit(); /* 3. 复位由bootloader完成启动旧固件 */ NVIC_SystemReset(); }这里要注意写状态标志不能等到复位之前才写。因为如果你是在“旧固件运行时执行OTA”那你的状态标志写入要保证系统复位后bootloader能看到新值所以这个地方必须使用flash写操作而且要在复位前完成不能依赖RAM变量。超时阈值怎么定如果你用MTU 247、连接间隔30ms从手机往设备推一个64KB的镜像理想情况下一秒钟大概能传12KB到15KB那整个传输只需要4到6秒。这时候设5秒绝对不够至少要设15秒以上。我在实际项目中一般按理论时间的2倍再加5秒这样既不会因为抖动误判也不会在真正卡死的时候等太久。3.3 BLE连接参数先把断链概率压到最低如果你的OTA链路经常在传输中途断掉那么加再多timeout也只是善后。你要做的是降低断链概率让OTA过程尽量一次成功。STM32WB5x的BLE连接参数是从机peripheral向主机central发连接参数更新请求最终由主机决定。但你自己调试的时候可以用手机App配置连接参数也可以让设备主动请求一组更稳的参数。我实测下来对OTA最稳的参数组合是连接间隔30ms、slave latency 0、supervision timeout 400ms。如果你把连接间隔设得太小比如7.5ms手机和模块都容易因为时钟漂移而跟不上反而增加断链概率如果你把slave latency设成4设备在多个连接事件里可以不用回包省电但OTA期间主机发来的一堆数据就可能在发送窗口没打开时丢失造成重传甚至触发supervision timeout。supervision timeout的计算要满足一个约束它必须大于连接间隔和slave latency的组合值。BLE协议规定supervision timeout必须大于(1 slave_latency) * conn_interval * 2。在conn_interval30ms、latency0时这个最小值是60ms我设400ms就是留足了余量。如果设太短比如100ms一旦某个连接事件因为内部flash擦除延迟了一点点就会断链。还有一点容易忽略MTU大小。BLE 4.2以后支持数据长度扩展如果你的手机和模块都支持DLE每次可以发247字节。如果你只用了默认的23字节MTU那传输同样大小的文件包数量会多出10倍以上任何一个包丢失都会导致重传和整体时间拉长。STM32WB5x的BLE协议栈默认支持DLE建议在初始化后主动开启aci_hal_set_data_length(247, 247); aci_gap_update_adv_data(...);开启DLE之后手机端也要配合。Android手机上如果不手动开启DLE部分机型会降级到23字节MTU这个问题不是固件能完全解决的但你的设备端至少要先开好否则一定上不去速率。3.4 写flash时的行为约束STM32WB5x有一个很特殊的点M0核跑协议栈M4核跑应用两者共享flash但flash只有一个接口同一时刻只能有一条访问请求。当M4在写flash的时候M0如果想从flash取指令只能等待。如果M0正忙着处理BLE连接事件它发现flash被占用太久连接事件处理超时就会产生链路层错误最终断链。所以我在OTA过程中做了一个约束写flash的代码段不允许在BLE连接事件期间执行。具体做法是在写入前通过IPC通知M0暂停BLE调度或者把一个标志位置起来让BLE线程在即将处理重要事件时跳过数据发送。更简单的做法是每写完一个块主动调用一次HAL_Delay(2)给M0留出处理连接事件的时间窗口。这里要区分flash擦和flash写擦除一个页的时间远大于写入一个字的时间。STM32WB5x的flash页擦除时间大概在几毫秒到几十毫秒如果你的连接间隔是30ms一整个连接事件都在擦除页那服务器在那个连接事件里发来的包自然全部错过。如果要连续擦除多个页最稳妥的是每擦一页就退出flash操作让BLE跑两个连接事件再继续。另一个经验是如果升级过程中不依赖BLE实时交互你可以先关闭广播、停止连接在静默状态下完成升级升级完再重新广播。这会让用户体验差一点但成功率会大幅提高。很多商业产品在OTA时都会显示“正在升级请勿断开”其实设备端就是在静默写flash不处理连接事件。对STM32WB5x来说如果你希望升级过程中手机那边还保持一个“已连接”的提示那你的写入节奏必须放慢否则断开是必然的。4. 一些同样会坑到你的细节与排查实录4.1 这类问题最常见的三个假象第一个假象是“校验和错误导致卡死”。很多人看到FUS或者自己的bootloader里返回了CRC error就认为是数据传输出错于是加更多重传。实际上我遇到的情况往往是数据块本身没问题但写flash的地址因为偏移量算错写到了错误区域导致校验时读出来的数据永远对不上。排查方法是把启动地址、镜像大小、slot大小、保留区域大小画在一张图上重新核对一遍。第二个假象是“复位后手机扫不到设备肯定是蓝牙坏了”。我一度也这么以为后来发现问题是复位后M4没有重新初始化BLE协议栈。STM32WB5x的BLE协议栈初始化不是普通外设那样简单的HAL_UART_Init它需要一个完整的初始化序列包括aci_low_power_mgr_init、aci_gatt_init、aci_gap_init、注册服务、开启广播。如果你之前的代码里把BLE初始化和某些外设绑定在一起而Ota复位后跳过了这段初始化就会“假死”。第三个假象是“加个外部硬件看门狗就能解决问题”。硬件看门狗只能防止死循环不能防止错误分支。如果你的OTA状态机停在“等待新包”这个正常分支看门狗永远不会超时因为它判断条件只是“有没有跑”而不是“升级有没有进展”。所以需要的是我刚才说的自定义OTA超时而不是依赖硬件狗。4.2 现场排查操作速查表当你手头正好有一个卡死的设备按下面的顺序排查能省一小时。排查项操作方法判断标准确认设备是否还在跑查看调试串口/状态LED如果完全没输出大概率已HardFault确认BLE协议栈状态用ST-Link连上读M0状态寄存器检查是否有LL error或supervision timeout确认flash中固件完整性用STM32CubeProgrammer读取两个slot的内容CRC是否准确新固件是否写到一半确认OTA状态标志读取OTA_STATUS_FLAG_ADDR的值是否为OTA_STATUS_BUSY复测连接参数用nRF Connect或者抓包器对比实际连接参数看是否和预期值一致有一个很实用的小技巧如果设备卡死时能连上ST-Link但无法用串口打印信息可以先用STM32CubeProgrammer读一下设备ID和flash占用这能在不干扰现场的情况下判断MCU是否还在正常响应调试接口。如果调试接口也没反应基本可以判定MCU死机或时钟出了问题这种情况先查供电和时钟配置不要先怀疑OTA流程。4.3 踩坑清单与规避方法我踩过的坑挑几个印象最深的列出来。第一个坑升级中途手机切到后台。Android App一旦进入后台系统可能暂停BLE操作甚至CPU休眠设备端等不到数据块触发超时复位然后旧固件启动升级失败。这个不是设备端bug但设备端也要能处理这种现象——超时复位后要重新广播让用户能重连重试而不是干等。第二个坑双slot的大小预留不足。我把slot B设计成和slot A一样大结果新固件因为加了日志功能膨胀了一点点写到最后越界覆盖了OTA状态标志页导致复位后bootloader找不到有效标志进不了App。从那以后我在layout里强制规定slot大小必须按固件可能的最大体积算并且留至少一块空页做状态标志。第三个坑CRC计算区域和实际写入区域不一致。我用的是crc32((uint8_t *)SLOT_ADDR, APP_MAX_SIZE)但APP_MAX_SIZE是新固件实际大小而写入区域是slot的最大容量。如果写入时把尾部填充区也写了一遍CRC就永远对不上。解决办法是只对有效固件长度做计算并且确保写入时严格按有效长度写不填充满slot。第四个坑FUS和BLE协议栈版本不匹配。STM32WB5x的无线协议栈和FUS有对应关系如果你用STM32CubeProgrammer把无线协议栈升到了v1.16但FUS还是旧版升级过程中可能出现mid-update异常。检查方式是读FUS版本号和协议栈版本号确保匹配。5. 我的结论与扩展建议5.1 关于firmware-side timeout/reset的最终判断回到标题的问题我的答案是firmware-side timeout是必须的而且应该是OTA状态机的一部分reset不是不能用但必须配合回退和重新广播一起做。从实际效果来看一套可靠的OTA恢复机制应该满足三个条件第一升级过程中任何一步失败设备最终都能回到一个可执行的固件状态第二回到这个状态后设备能自动重新进入可被发现的广播状态让手机端可以重试第三整个过程不需要用户拆机、不需要调试器、不需要按复位按键。如果只是加一个while(1)超时然后NVIC_SystemReset这三个条件一个都不满足。而如果在超时处理里做状态标志切换、回退到旧固件、重新广播那就是一套能真正解决“stuck mid-update”问题的机制。以我手头这个STM32WB55项目为例最终实现的OTA流程是手机连上设备后先通过一个OTA控制特征值发送“开始升级”命令设备端把状态标志设为BUSY然后进入接收模式。接收过程中每收到一个有效块就刷新时间戳同时把新固件写入slot B。超过8秒没有新块就认为超时写状态标志为回退旧固件复位bootloader启动slot A然后初始化BLE并进入广播。整个流程里用户最多就是看到App提示“升级失败”然后重新试一次再也不用拆机刷bootloader。5.2 后续可以做的更强方案如果你已经把基础的超时回退做完了还可以往下走几步。一是断点续传把已接收的块地址记录在flash里重新连接后从断点继续传而不是整体重传。这个对设备算力要求不高但能极大提升大镜像升级的体验。二是版本强制校验新固件下载完成后bootloader在启动前除了CRC外再校验一个固件版本号如果新固件版本号小于旧固件不允许启动。这个能防止手滑把旧版本当成新版本推上去。三是远程日志上报在设备端记录每次OTA失败的错误码和阶段信息下一次连接时通过某个特征值上报给手机端。这个对定位实际用户场景里出现的偶发问题非常有帮助能直接看出是超时、CRC错误、flash写入失败还是协议栈错误。四是可以考虑在M0核的协议栈状态里加一个心跳机制M4定期通过IPC查询M0的状态如果发现M0没有正常响应主动触发一次协议栈重启而不是复位整个芯片。这个方案实现成本高一些但对于长时间无人值守的设备价值很大。最后再分享一个小技巧。我在代码里把OTA状态机的所有关键分支都打上了调试日志但只在Debug版里使能Release版关闭。这样复现卡死问题时用串口就能直接看到“接收到第几块数据之后没下文了”定位速度快很多。OTA调试非常依赖这种方式因为你面对的是一个传输中段链路断开的黑盒没有日志只能靠猜有了日志基本一眼就能看出来问题出在哪一层。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门