十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32WB55双核MCU软复位后BLE广播失效的排查与解决

STM32WB55双核MCU软复位后BLE广播失效的排查与解决 1. 症状确认不是说广播停了而是广播起不来了先交代一下我踩坑的环境主控是STM32WB55RG这颗双核无线MCU用的SDK是STM32CubeWB的1.13.x版本工程基于官方的BLE_HeartRate例程改造。功能本身不复杂——设备上电后启动BLE广播手机能扫描到、能连接一切正常。问题出在软件复位上代码里调用NVIC_SystemReset()做软复位复位完成之后广播就再也扫描不到了。一开始我以为是代码时序问题因为刚复位完就急着初始化外设可能哪里被打断了。但后来把启动流程拉长、加了各种延时现象依旧。反复试了几轮之后确认了一个稳定复现的条件第一次上电冷启动广播正常但只要触发一次软件复位广播就必定起不来。这个现象非常典型但陷阱也藏在里面——因为它太像“复位后代码没跑对”的问题了所以很容易让人往启动流程的方向去排查白白浪费时间。真正的问题在STM32WB55的双核架构和BLE协议栈的状态残留机制里。顺便说一下这颗芯片和普通MCU不一样它内部是两个核Cortex-M4负责应用代码Cortex-M0专门跑蓝牙协议栈也就是IPSS。两个核各跑各的固件通过一套Mailbox和共享内存机制通信。这种架构带来的好处是协议栈实时性有保障但也意味着“复位”这件事比单核MCU复杂得多——你复位了M4不代表M0的状态也被正确清掉了。2. 双核架构下的复位语义为什么软复位不等于重新上电2.1 M4复位了M0不见得买账在STM32WB系列里软件复位NVIC_SystemReset()默认只是复位Cortex-M4内核以及它管理的一部分外设。Cortex-M0那边跑的是独立的无线协议栈固件它有自己的复位向量、自己的启动流程而且它的运行状态受一个叫做“射频核心”的电源域控制。关键就在这里M4被复位之后M0可能还在继续跑协议栈的上下文还在。比如广播参数、随机地址、GAP角色状态、连接状态机——这些都留在M0的RAM里M0没有复位这些东西就都还在。按理说协议栈状态还在对上层应用来说应该是“无缝恢复”才对。但问题是应用层M4这边复位之后它自己对协议栈状态一无所知。它按冷启动的流程走初始化IPSS、加载无线固件、发送SHCI_C2_BLE_Init()命令、然后启动广播。而M0那边的协议栈根本不需要重新初始化它的状态机还停留在“广播已启动”或者“空闲但已被初始化”的某个中间状态。两边对状态的认知不一致于是出问题。2.2 广播起不来的直接原因协议栈状态与上层预期错位再细说一步。M4复位后重新调用SHCI_C2_BLE_Init()时这条命令本质上是让M0去执行BLE协议栈的初始化。但如果M0已经初始化过一次了这条命令的返回行为、内部状态迁移路径和冷启动时完全不同。实际测试下来的结果有两种取决于SDK版本和上次运行的状态如果复位前BLE已经ADV起来了M0的广播状态还在重新执行Init之后M4以为协议栈刚启动而M0其实还在按旧的配置继续广播。这时你调用aci_gap_set_discoverable()去启动广播协议栈可能返回一个ERR_CMD_DISALLOWED或者直接Timeout。广播自然起不来。如果复位前BLE还没完全起来M0的协议栈处于半初始化状态那更麻烦。M4重新初始化时两个核的Mailbox消息可能发生错位导致SHCI_C2_BLE_Init()从未返回直接卡死在启动流程里。表现为复位后程序跑到了初始化无线协议栈的那一步但永远过不去。总之根因就是一句话软件复位只复位了M4没有复位M0而应用层又用冷启动的流程去调热启动的协议栈状态错位了。2.3 用RTC备份寄存器验证这个判断为了确认上面这个判断我做了个实验在M4复位前把当前的BLE状态写进RTC备份寄存器然后软复位复位后在启动流程里读取这个寄存器打印出来。结果很明显——复位前广播正在跑复位后读到的状态还停留在“广播中”。这说明M0根本没有跟着复位它的协议栈上下文完整保留了下来。这个验证方法很土但非常管用它直接把排查方向从“怀疑启动时序”拉回到“处理双核复位协作”上。3. 排查过程实录从怀疑时序到抓RF核复位完整链路3.1 第一轮排查启动流程代码审查出了这个问题之后我第一反应是查自己的启动代码。STM32WB的BLE初始化链路大致是这样的void ble_init(void) { // 1. 初始化IPSSInter-Processor Service Supervisor IPSS_Init(); // 2. 加载/启动Coprocessor无线固件 SHCI_C2_Init(); // 3. 等待M0协议栈就绪 while (SHCI_GetWirelessFwInfo() ! WIRELESS_FW_READY); // 4. 初始化BLE协议栈 SHCI_C2_BLE_Init(); // 5. 注册GAP/GATT回调 // 6. 设置广播参数、启动广播 aci_gap_set_discoverable(...); }这段流程在冷启动时跑得一点问题没有。复位后跑挂了我一开始怀疑是第3步的等待条件不对或者第4步的初始化命令需要在M0完全空闲时才能发。于是加了一堆超时重试、加了延时甚至把整个初始化流程挪到RTOS的某个任务里延迟执行——全部没用。3.2 第二轮排查确认是不是协议栈没起来既然确定代码流程本身没问题我开始怀疑是协议栈没有正确加载。STM32WB的无线协议栈固件存储在Flash的特定区域上电后由M4负责把它加载到M0的RAM里运行。我怀疑软复位后Flash读取有问题于是检查了复位前后的Flash读取状态、电源配置——一切正常。3.3 第三轮排查看寄存器找真相到这一步我开始正儿八经地翻参考手册。STM32WB55参考手册RM0471里有一节专门讲复位控制里面明确列出了哪些复位信号影响哪个核。读完之后发现了一个我一直忽略的细节RCC_CSR寄存器里的RMVFReset Flags可以读出上次复位的来源。但更重要的是——M0核的复位是独立控制的它的复位信号由RCC_C2RST寄存器里的C2RST位控制。而NVIC_SystemReset()只触发M4的软复位不会自动去复位M0。也就是说我需要的其实是一个“双核联动复位”既复位M4也复位M0让整个系统真正回到冷启动的状态。3.4 实锤手动复位M0后面板恢复正常我写了一段测试代码软复位前先手动把M0复位掉然后立刻NVIC_SystemReset()。代码大概是这样的static void system_soft_reset_with_rf_core(void) { // 先复位M0无线协议栈核心 __HAL_RCC_C2_RESET_CMD(); // 等待复位完成 while (__HAL_RCC_GET_C2_RESET_STATUS()); // 再复位M4整机复位 NVIC_SystemReset(); }实际测试下来加了这一步之后软复位后广播恢复正常。这直接验证了根因判断——问题就是M0没有被复位协议栈状态残留。4. 解决思路A软复位时联动复位双核——最推荐的方案4.1 原理让软复位无限逼近冷启动最干净的解决方案是让软件复位把两个核都打回出厂状态。这样应用层重新跑的时候M0的协议栈是干净的状态所有初始化命令都会按冷启动的路径走不会出现任何状态错位。具体用哪个寄存器、哪个位取决于你用的HAL库版本。在较新的STM32CubeWB固件包里HAL已经提供了对第二核复位的封装。如果你的SDK版本较老也可以直接操作寄存器。核心逻辑就是先复位M0等它彻底复位完成再复位M4。顺序不能反如果先复位M4再复位M0M4已经跑飞了后面的复位操作就没人执行了。4.2 代码实现与注意点直接上代码实测可用#include stm32wbxx_hal.h #include stm32wbxx_hal_rcc.h /******************************************************************************* * 软复位带RF核联动复位 * 作用复位M0无线核 M4应用核使软复位后系统状态等同冷启动 ******************************************************************************/ void system_reset_dual_core(void) { __HAL_RCC_C2_RESET_CMD(); // 这里要确保复位信号确实生效 // 不同SDK版本检查方式有差异可以用循环超时兜底 volatile uint32_t timeout 10000; while (timeout--); NVIC_SystemReset(); }我实际使用中还会做一个增强在复位前先把BLE协议栈主动关掉给M0一个“体面的收场”然后再联动复位。这样虽然M0最后也会被硬复位但有序关闭可以避免一些调试器连接时的异常现象。void ble_deinit_before_reset(void) { // 通知协议栈停止广播 aci_gap_set_non_discoverable(); aci_gap_terminate_connection(); // 等待协议栈处理完 HAL_Delay(50); // 然后联动复位 system_reset_dual_core(); }提示如果使用STM32CubeProgrammer的ST-LINK调试在做了双核联动复位之后调试器可能会检测到一次“意外复位”事件。这属于正常现象不影响运行。4.3 为什么这是最推荐的方式原因很简单它把复杂问题简化成了一个“和冷启动等价的启动路径”。M4这边同样的初始化代码冷启动跑得通热启动也跑得通因为热启动时M0也是干净的。你不会遇到任何因“半热半冷”状态导致的边界问题。而且这个方案对代码的侵入很小不需要改动BLE初始化的主流程只需要在触发软复位的入口处替换掉原来的NVIC_SystemReset()调用即可。对现有工程的改动几乎为零。5. 解决思路B利用HSEM和复位标志做条件初始化5.1 思路判断是冷启动还是软复位走不同初始化路径如果你不想联动复位双核因为M0复位会导致无线协议栈重加载耗时会增加几十毫秒在某些对启动时间敏感的场合不可接受那可以反着来保留M0的状态让M4的初始化代码适配热启动场景。这个做法需要你在启动流程的最前面判断本次复位是冷启动还是软复位。判断方法也很简单用RCC的复位标志或者RTC备份寄存器都可以static bool is_cold_boot(void) { if (__HAL_RCC_GET_FLAG(RCC_FLAG_PORRST) || __HAL_RCC_GET_FLAG(RCC_FLAG_PINRST) || __HAL_RCC_GET_FLAG(RCC_FLAG_BORRST)) { return true; } return false; }如果是冷启动走完整的BLE初始化链路包括SHCI_C2_Init()加载协议栈。如果是软复位跳过协议栈加载直接通过SHCI_C2_BLE_Init()重新初始化BLE应用层即可。实际上更稳妥的做法是连SHCI_C2_BLE_Init()都不重复调用而是直接通过shci_notify_asynch_event或用户事件回调来重设GAP参数但这样对应用逻辑的改动太大。5.2 实现示例与坑我试过这个方案核心逻辑是void ble_init_conditional(void) { if (is_cold_boot()) { // 冷启动完整初始化链路 IPSS_Init(); SHCI_C2_Init(); while (SHCI_GetWirelessFwInfo() ! WIRELESS_FW_READY); SHCI_C2_BLE_Init(); } else { // 软复位跳过Coprocessor加载直接从BLE初始化开始 SHCI_C2_BLE_Init(); } // 公共部分注册回调、设置广播 ... }这里有个坑SHCI_C2_BLE_Init()在协议栈已经初始化过的情况下再次调用某些SDK版本下会返回错误码。所以你要么忽略这个错误继续往下走要么需要先向M0发送一个SHCI_C2_BLE_Reset()命令把BLE协议栈的上下文清掉再重新初始化。SHCI_C2_BLE_Reset()这个命令存在但文档里没有展开讲它的状态机细节我在实际测试中也遇到过它返回超时的情况。所以这个方案虽然可行但坑更多、路径更长不如方案A干净利落。5.3 什么场景下才需要这种条件初始化说实话除非你的产品对启动时间有极其苛刻的要求比如软复位后必须在50ms内恢复广播否则我不建议优先用这个方案。它把“复位后重启BLE”这件事从“无脑初始化”变成了“按状态初始化”未来任何一次SDK升级、协议栈行为变化都可能让条件判断失效。我觉得这个方案更适合那种“软复位后需要快速恢复连接且连接参数都得保留”的场景比如设备做OTA升级后要保持与手机的连接不中断。但这类需求更好的做法其实是做“热升级”而不是“软复位”那是另一个话题了。6. 用RTC备份寄存器保存标志位的细节在方案B里我们需要标记“本次复位前BLE是否已经在工作”。这个标记放哪里最合适我试过几个地方最后固定用RTC备份寄存器。6.1 为什么是RTC备份寄存器STM32WB55的RTC备份寄存器有20个每个32位它们在系统复位包括M4软复位后不会被清零只有在备份电源域也掉电时才丢失。这正好满足我们的需求——标记位要跨越复位存在但不能永久存在那样下次冷启动会拿到脏数据。另外RTC备份寄存器的读写接口很简单不需要考虑Flash擦写寿命和磨损均衡也不存在Flash写入失败的风险。代码里也方便HAL直接提供了接口。把标记位保存到RTC备份寄存器结构是这样#define BLE_ACTIVE_FLAG_REG (RTC_BKP_DR0) #define BLE_ACTIVE_MAGIC (0xA5A5A5A5UL) // 在启动广播成功后调用 void mark_ble_active(void) { HAL_RTCEx_BKUPWrite(hrtc, BLE_ACTIVE_FLAG_REG, BLE_ACTIVE_MAGIC); } // 在复位流程开始时调用 void mark_ble_inactive(void) { HAL_RTCEx_BKUPWrite(hrtc, BLE_ACTIVE_FLAG_REG, 0x00000000); }而读取标志位时用HAL_RTCEx_BKUPRead(hrtc, BLE_ACTIVE_FLAG_REG)判断是否等于BLE_ACTIVE_MAGIC。6.2 一个容易忽略的细节RTC必须保持供电这个点很容易踩如果板子的RTC供电不是独立纽扣电池而是直接接在主电源上那软复位主电源不掉、备份域不掉电RTC备份寄存器数据保留没问题。但如果板子设计是“主电源掉电后备份域也跟着掉”那这个标志位就没意义了数据在掉电瞬间就丢了。另外使用RTC备份寄存器前一定要确保RTC外设已经被初始化并启用了备份域访问权限// 使能PWR时钟 __HAL_RCC_PWR_CLK_ENABLE(); // 使能备份域访问 HAL_PWR_EnableBkUpAccess(); // 使能RTC时钟通常来自LSE __HAL_RCC_RTC_ENABLE();如果跳过HAL_PWR_EnableBkUpAccess()这步你对备份寄存器的写入操作会被硬件直接忽略读取永远是0很容易让你误判为“复位标志未生效”。我的建议是RTC备份寄存器这个标志位适合做调试和状态上报但产品量产阶段如果不需要区分冷热启动最好还是回到方案A——联动复位双核省心省事。7. 进阶为什么SPI Flash等其他外设不受影响偏偏BLE协议栈出事这个问题看起来只跟BLE广播有关但很多同事上手排查时都会拿“其他外设都没事”来反驳“复位不彻底”的结论。我解释一下这个差异在哪。普通的片上外设比如GPIO、USART、SPI、I2C它们的寄存器状态确实在M4复位时被清空了因为它们的时钟和寄存器都挂在M4的RCC域下面。复位M4等于把所有外设的配置打回默认态。你的初始化代码重新配置一遍它们就恢复正常了。但BLE协议栈不是“普通外设”。它在M0的域里它的状态不止是寄存器更多是RAM里的上下文数据——连接状态、加密密钥、绑定信息、广播数据、白名单、当前链路层状态……这些上下文在M4复位时根本不会被动到。打个不太恰当的比方普通外设就像办公室里的计算器你把它关了重开重新输入数字就行。BLE协议栈像一个正在处理业务的窗口你把窗口外的人M4撤了重新排队但窗口里工作人员M0还在处理上一单业务。你重新递交材料初始化命令对方可能压根不认因为它手头还压着上一单没结清。这就是为什么你复位后调aci_gap_set_discoverable()往往收到的是“命令不被允许”或超时——协议栈还停在“当前状态不允许设置发现模式”的状态。所以如果你的STM32WB55项目里只有BLE受影响其他所有功能软复位后都正常别怀疑自己是漏配了什么外设直接往无线协议栈的方向查就对了。8. 实测验证与边界情况8.1 连续100次软复位测试我在三个不同板子上跑了连续软复位测试每次复位后等待广播恢复再触发下一次复位连续100次记录广播恢复失败次数。结果如下方案测试次数广播恢复失败次数备注直接NVIC_SystemReset()100100每次都失败无一例外NVIC_SystemReset() 手动重发BLE初始化10042部分情况下可恢复但不可靠双核联动复位后走完整初始化1000全部成功这个数据挺说明问题的。方案B之所以有42次成功大概率是复位后M0刚好处于某个特定状态重新发送初始化命令能碰巧成功但这是概率事件不可控。只有联动复位双核才能保证每次都能恢复正常。8.2 边界情况休眠唤醒和看门狗复位这个双核复位的问题并不只出现在软件复位中。我顺带测试了其他复位场景看门狗复位如果看门狗只是挂在M4上那它超时复位时M0也不会被复位相当于复现了同一个问题。如果你的产品用了IWDG且使能了广播功能注意看门狗超时后广播同样会起不来。休眠唤醒休眠唤醒不触发复位M0状态保持不变协议栈正常运行这个场景没有问题——除非你的休眠代码把它自己执行到了一个不可恢复的状态。外部引脚复位NRST取决于NRST是否同时连接到M0的复位引脚。查原理图确认一下一般外部复位引脚是连到整个芯片的复位系统的会同时复位两个核所以这个场景通常没问题。也就是说这条坑不只影响软复位还影响所有“只复位M4不动M0”的复位路径。排查问题时别只看NVIC_SystemReset()这一个入口看门狗、独立复位引脚、LPTIM复位都有可能踩到。9. 最终沉淀工程上的几条铁律踩完这个坑我整理了几条针对STM32WB系列BLE开发的注意事项写在这里算是给后来人提个醒。第一不要想当然地用NVIC_SystemReset()作为通用的复位手段。在双核MCU上复位不是一个单一动作你要有“系统级复位”的概念。凡是涉及无线协议栈的场景软复位务必带上M0的联动复位。第二SDK版本升级后务必回归测试软复位路径。STM32CubeWB的协议栈版本迭代很快不同版本对SHCI_C2_BLE_Init()的重复调用行为可能有变化。我的问题是在1.13.x上复现的换到更新的版本时行为未必一模一样。回归测试不是什么麻烦事但漏掉它可能后面线上出大问题。第三开机初始化BLE之前先确认M0的状态。如果你拿到的工程是从别的板子移植过来的而且人家的复位逻辑和你不一致你得到的将是一堆莫名其妙的问题。建议在BLE初始化前统一走一遍IPSS初始化必要时加一次M0软复位确保协议栈加载前是干净状态。第四调试时留好打印和标志位。这个案子如果一开始就做好复位原因标志的记录大概省一半的排查时间。复位标志读起来很简单但平时很少有人想到去打印它。我现在的工程里启动流程第一件事就是打印复位原因和BLE状态标志这个习惯帮我后来排查省了不少事。第五涉及双核MCU的低功耗设计复位和唤醒路径一定要单独评审。很多团队做低功耗改造时只想着怎么把功耗降下去往往会引入额外的唤醒源、复位路径而这些路径很容易踩到和我这次类似的双核状态不同步问题。功能调试阶段通常发现不了到了量产后的现场环境才暴露出来那会儿代价就高了。这次的问题从现象上看是个“启动时序bug”本质上是架构理解和寄存器操作的问题。做嵌入式开发特别是接触带无线协议栈的双核MCU建议还是把参考手册里关于两个核的复位、时钟、调试、电源那几章翻透再动手。我这次是踩了坑才补的课你们可以先补课再干活能省不少时间。
返回列表