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. 初始化IPSS(Inter-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寄存器里的RMVF(Reset 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再复位M0+,M4已经跑飞了,后面的复位操作就没人执行了。
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() | 100 | 100 | 每次都失败,无一例外 |
| NVIC_SystemReset() + 手动重发BLE初始化 | 100 | 42 | 部分情况下可恢复,但不可靠 |
| 双核联动复位后走完整初始化 | 100 | 0 | 全部成功 |
这个数据挺说明问题的。方案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,建议还是把参考手册里关于两个核的复位、时钟、调试、电源那几章翻透再动手。我这次是踩了坑才补的课,你们可以先补课再干活,能省不少时间。