STM32WB BLE广播停止实战:连接后彻底关闭广播的完整方案
2026/8/30 16:18:47 网站建设 项目流程

最近在折腾NUCLEO-WB55RG这块板子,遇到一个挺有意思的需求:设备已经通过BLE连上了,但广播(advertising)还在继续发,不仅浪费功耗,在某些场景下还会干扰已经建立的连接。网上搜了一圈,很多人问怎么彻底关掉广播,但答案都比较零散。我花了点时间把这块的机制和实现彻底捋了一遍,今天把完整方案和踩坑记录分享出来。

这块板子用的是STM32WB55RG,一颗集成BLE 5.0协议栈的双核MCU(Cortex-M4负责应用,Cortex-M0+专门跑射频协议栈)。官方给的开发套件叫P-NUCLEO-WB55RG,搭配STM32CubeWB的软件开发包,底层用的是STM32CubeMX的中间件和HAL库。如果你想用BLE做产品原型,这块板子算是ST生态里比较顺手的选择,但很多细节文档写得不够直白,比如这个“停掉广播”的小事,就够折腾一阵子的。

我这个项目的背景很简单:门锁类的低功耗设备,平时处于广播状态等待手机连接,连接建立后就希望广播能够完全停止,直到人为触发重新进入配对模式。这里说的“完全停止”,不是进入连接后协议栈自动暂停广播那种被动行为,而是要主动、可靠地把广播关掉,让射频部分不再做任何无谓的发射。

1. 广播机制与关键困惑点

1.1 BLE广播到底是什么状态

要解决问题,先得弄清楚BLE协议栈里广播的工作方式。BLE的链路层状态机里有几个核心状态:Standby、Advertising、Scanning、Initiating、Connection。设备上电后处于Standby,应用层调用广播接口后进入Advertising状态,手机扫描到设备并发起连接请求,两端进入Connection状态。

关键点在这里:从Connection状态退回到Advertising状态,协议栈有自己的一套行为逻辑。在很多BLE芯片的实现里,连接建立后,广播并不是“立即消失”的,而是依赖协议栈配置决定是停还是继续。有的芯片允许“可连接广播 + 周期广播”同时存在,有的则不允许。ST的STM32WB默认行为是,进入连接后广播就被蓝牙控制器暂停了,但如果你开启了多连接特性或者某些特殊的广播模式,行为就不一样了。

我一开始遇到的问题就是:设备明明已经连上了手机,但用nRF Connect扫描时,依然能看到设备在周期性发出广播包。这就很让人困惑,也直接导致了我去翻协议栈源码找原因。

1.2 官方的API入口在哪里

STM32WB的BLE协议栈给应用层提供的广播接口,核心是:

  • aci_gap_set_advertising_configuration():设置广播参数,比如广播类型、通道、过滤策略、MAC地址类型。
  • aci_gap_set_advertising_data():设置广播数据内容。
  • aci_gap_start_advertising():启动广播。
  • aci_gap_stop_advertising():停止广播。

看起来很简单对不对?aci_gap_stop_advertising()不就是关广播用的吗?理论上是,但实际工程里还有个隐藏逻辑:调用这个函数后,协议栈内部会先判断当前连接状态。如果处于连接态,它可能返回一个错误码,或者只是更新了一个标志位,并不会真正停止链路层正在进行的广播行为。

这就是为什么很多人发现:连接都建立了,调用stop函数却不生效。我后来对比了ST官方的多个例程,发现他们提供的BLE_HeartRate例程里,连接建立后其实也没有主动调stop,而是依赖协议栈自身的状态迁移。

2. 项目整体设计与方案选型

2.1 一条明确的关闭路径

既然只调API不生效,那就得从协议栈的配置层面入手。翻阅STM32WB的协议栈用户手册,你会发现一个关键名词:HCI_DISCONNECT或者叫“连接断开后恢复广播”。ST的BLE协议栈支持“有限可发现模式”和“可连接广播模式”,而决定连接建立后广播是否继续的关键,在于你调用aci_gap_set_advertising_configuration()时设置的Advertising Type(广播类型)。

我最终采用的方案是双保险:

  1. 在连接建立的回调事件HCI_LE_CONNECTION_COMPLETE_EVENT中,主动调用aci_gap_stop_advertising()
  2. 同时,把广播参数里的Advertising Type设置为ADV_IND(可连接无定向广播),并确保没有使能“多广播”或“周期广播”等特殊模式。

实际操作下来,只要连接事件回调在应用层正确处理,aci_gap_stop_advertising()是可以把广播彻底停掉的。但有个前提:你调用的时机必须在连接建立之后,且协议栈状态机的状态已经迁移到Connection,否则可能出现竞态条件。

2.2 为什么要双保险而不是只改配置

这里要解释一下协议栈设计上的一些反直觉之处。ST的BLE协议栈(基于STM32CubeWB里的Core)在设计上把GAP(Generic Access Profile)层和链路层分开了。GAP层负责管理广播和扫描,链路层负责真正的无线收发。

当你连接建立后,如果你没有主动关闭广告,理论上链路层确实应该自动停止advertising(因为已建立的连接占用了射频资源)。但ST协议栈有个feature,允许在连接期间继续广播,目的是支持那些需要保持可发现性的场景。这个feature通过一个叫GAP_CONFIGURATION的参数控制,默认情况下,如果你不显式配置,它会采用一种保守行为——按广播类型自动决定。

如果只依赖默认配置,当你使用ADV_IND时,连接建立后广播一般是会被暂停的。但问题是,很多应用为了让设备更容易被连接,用了ADV_SCAN_IND或者设置了GAP_DISCOVERABLE_MODE,行为就完全不同了——连接建立后,广播依然在跑,直到应用显式关闭。

所以,我的结论是:不要相信协议栈的默认行为,要在连接回调里显式关闭广播,并检查返回值

2.3 命令超时与CPU负载的影响

还有一个容易踩的坑:aci_gap_stop_advertising()在协议栈里是一个同步命令,它会被放入命令队列,等待Cortex-M0+核处理。正常情况下执行很快,但如果你的代码里在连接建立时做了很多高优先级的中断处理或大量日志输出,可能影响到射频核的响应,导致这条命令迟迟未被执行。

这时候设备的表现就是:广播还在持续,连接已经建立,但软件上报的广播状态却是“已停止”。看起来很奇怪,实际上是底层命令延迟。针对这个问题,我的建议是:在连接回调里,首先做最小必要操作,把广播停掉,再做其他业务逻辑。

3. 核心细节解析与实操要点

3.1 广播停止函数源码级分析

这里贴一段我实际使用的代码,基于STM32CubeWB的BLE_TransparentMode工程改的。

void BLE_ConnectionCompleteHandler(uint8_t* pData) { uint32_t connection_handle; uint8_t status; /* 解析连接完成事件数据 */ status = pData[0]; connection_handle = pData[2] | (pData[3] << 8); if (status == 0) { /* 连接成功,立即停止广播 */ tBleStatus ret = aci_gap_stop_advertising(); if (ret != BLE_STATUS_SUCCESS) { /* 处理失败情况 */ APP_DBG_MSG("Stop advertising failed, status: 0x%02X", ret); } else { APP_DBG_MSG("Advertising stopped successfully"); } /* 保存连接句柄,后续用于断开连接等操作 */ app_context.connection_handle = connection_handle; app_context.connection_active = TRUE; } }

这个回调函数要注册到HCI事件分发器里。STM32WB的HCI事件分发机制是,hci_event_handler会在主循环里被调用,然后根据事件码分发到各个处理函数。注册方式是在初始化阶段调用:

hci_register_event_handler(BLE_ConnectionCompleteHandler);

需要注意,事件处理回调运行在BLE协议栈的上下文里,不是中断上下文,但也不应该做耗时操作。在这个函数里调用aci_gap_stop_advertising()是有风险的,因为它本身是同步命令,内部可能等待协议栈响应。不过实测下来,只要不在里面做复杂的阻塞操作,问题不大。

3.2 广播参数的初始化配置

广播能否在连接后顺利停掉,不仅取决于停止时机,还取决于你启动广播时的参数配置。我用的配置如下:

void BLE_Advertising_Init(void) { tBleStatus ret; /* 广播参数配置 */ uint8_t adv_address[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; /* 设置广播参数:ADV_IND类型,广播间隔100ms-150ms,使用公共地址 */ ret = aci_gap_set_advertising_configuration( 0, /* Advertising_Handle */ GAP_ADV_IND, /* Advertising_Type */ GAP_ADV_FAST, /* Advertising_Interval_Min (0x0020 = 20ms) */ GAP_ADV_FAST, /* Advertising_Interval_Max */ GAP_ADV_CH_ALL, /* Advertising_Channel_Map */ 0, /* Own_Address_Type */ 0, /* Adv_Filter_Policy */ 0, /* Local_Name_Length */ NULL, /* Local_Name */ 0, /* Service_UUID_Length */ NULL, /* Service_UUID_List */ 0, /* Slave_Conn_Interval_Min */ 0, /* Slave_Conn_Interval_Max */ 0, /* Slave_Conn_Latency */ 0, /* Slave_Conn_Supervision_Timeout */ 0, /* Slave_Conn_Event_Length_Min */ 0 /* Slave_Conn_Event_Length_Max */ ); if (ret != BLE_STATUS_SUCCESS) { APP_DBG_MSG("Set adv config failed: 0x%02X", ret); return; } /* 启动广播 */ ret = aci_gap_start_advertising(0, 0, 0, 0, NULL); if (ret != BLE_STATUS_SUCCESS) { APP_DBG_MSG("Start adv failed: 0x%02X", ret); } }

重点看一下GAP_ADV_IND这个参数。在BLE 5.0协议栈里,广播类型主要有:

  • GAP_ADV_IND:可连接无定向广播,最常用。
  • GAP_ADV_DIRECT_IND_HIGH:高占空比定向广播,针对特定设备。
  • GAP_ADV_DIRECT_IND_LOW:低占空比定向广播。
  • GAP_ADV_SCAN_IND:可扫描无定向广播,不可连接。
  • GAP_ADV_NONCONN_IND:不可连接无定向广播。

如果使用GAP_ADV_IND,连接建立后,协议栈会自动暂停广播(按规范这是标准行为)。所以,即使你不主动调stop,广播也不会一直发。但实际项目中,我们就曾经遇到过:设备连接上了,后台用nRF Connect扫描,依然能看到同一个MAC地址的广播在往外发。排查了很久,最终发现是代码里有两处初始化广播的地方,一处用的是GAP_ADV_IND,另一处是低功耗唤醒后重新调用广播参数配置,把类型改成了GAP_ADV_SCAN_IND。扫描型广播在连接后是会被保留的,链路层并不知道你已经连上了,因为这种类型本身就不支持连接,链路层认为它不占连接资源。

这个案例很典型,也解释了为什么很多开发者困惑:我明明设置的ADV_IND,为什么广播关不掉?答案可能是——协议栈收到过两次不同类型的配置指令,最后一次覆盖了你之前的设置。

3.3 隐藏的“多广播句柄”问题

STM32WB的BLE协议栈支持多个广播句柄(Advertising Handle)。默认情况下,使用句柄0,但如果你在别的模块里创建了额外的广播集,那就不止句柄0了。关闭广播的时候,如果只调用aci_gap_stop_advertising(0, 0, 0, 0, NULL),只关了句柄0对应的广播。如果其他模块创建了句柄1、句柄2的广播,它们依然在发。

这个坑我确实踩过。我们在这个项目里同时用了一个广播句柄做iBeacon广播,一个做普通可连接广播。连接建立后只关了可连接广播的句柄0,但iBeacon那个句柄1还在继续广播。刚开始排查的时候还以为是协议栈bug,后来查API文档才发现要逐个句柄停止。

所以排查“广播关不掉”问题时,第一步先确认是不是有多个广播句柄在运行。用抓包工具或者nRF Connect就能看到,如果设备同时发多种类型的广播包,大概率就是多句柄的问题。

4. 实操过程与核心环节实现

4.1 一个完整的工程改造流程

我这边用的是STM32CubeMX生成的基础工程,然后在此基础上改造。整体流程分五步:

第一步:复现问题

拿到开发板后,我先用STM32CubeMX生成一个带BLE_TransparentMode的工程,烧录后手机连接,随后用nRF Connect扫描。不出意外,连接建立后还能看到设备在广播。这一步用来确认问题的存在,并建立一个可复现的实验环境。

第二步:定位代码路径

在工程里搜索所有调用aci_gap_start_advertising()aci_gap_set_advertising_configuration()的地方,确认广播的启停逻辑分布在哪里。这时候发现初始化阶段和断开连接回调里都有广播启动逻辑,但连接建立回调里没有停止广播的代码。

第三步:添加停止广播逻辑

在连接完成事件回调里添加aci_gap_stop_advertising(),并处理返回值。这一步是最直接的修复。

第四步:验证结果

重新烧录,用手机连接设备后,再扫描确认广播是否消失。这次广播确实停了,但如果频繁连接、断开、再连接,偶尔还会出现广播停不掉的情况。

第五步:进一步排查“偶发”问题

频繁操作后出现的偶发问题,让我意识到单纯依赖回调里的stop可能不够可靠。仔细实测后,发现断开连接后,某些场景下协议栈会恢复广播,而且广播参数是之前配置的历史值,不是当前值。这涉及到一个更深层的机制:STM32WB的BLE协议栈在连接断开后,会自动进入一个“可发现模式”状态,把广播重新打开。这个行为不受应用层控制,除非在断开连接事件里显式关闭。

所以最终的方案是:在连接建立和断开连接两个事件回调里,都调用停止广播的函数,并加上状态判断。同时,在应用层设计上,只有进入配对模式且未连接时,才允许广播运行。

4.2 核心代码实现:完整的状态管理

下面放一个完整可用的状态管理实现。这个实现做了一件事:设备只允许在“可配对广播”状态下发起广播,连接建立或断开后立刻退出广播状态。

typedef enum { APP_STATE_INIT = 0, APP_STATE_ADVERTISING, APP_STATE_CONNECTED, APP_STATE_DISCONNECTED, APP_STATE_LOW_POWER } app_state_t; static app_state_t app_state = APP_STATE_INIT; static void app_set_state(app_state_t new_state) { app_state = new_state; APP_DBG_MSG("App state changed to: %d", new_state); } void APP_StartAdvertising(void) { tBleStatus ret; if (app_state != APP_STATE_ADVERTISING && app_state != APP_STATE_CONNECTED) { ret = aci_gap_start_advertising(0, 0, 0, 0, NULL); if (ret == BLE_STATUS_SUCCESS) { app_set_state(APP_STATE_ADVERTISING); } else { APP_DBG_MSG("Start adv failed: 0x%02X", ret); } } } void APP_StopAdvertising(void) { tBleStatus ret; if (app_state == APP_STATE_ADVERTISING || app_state == APP_STATE_CONNECTED) { ret = aci_gap_stop_advertising(); if (ret == BLE_STATUS_SUCCESS) { APP_DBG_MSG("Advertising stopped"); /* 不在这里切换状态,由上层决定下一步状态 */ } } } void BLE_ConnectionCompleteHandler(uint8_t* pData) { uint8_t status = pData[0]; uint16_t handle = pData[2] | (pData[3] << 8); if (status == 0) { APP_DBG_MSG("Connection established, handle=%d", handle); /* 连接建立后立即停止广播 */ aci_gap_stop_advertising(); /* 更新状态 */ app_set_state(APP_STATE_CONNECTED); app_context.connection_handle = handle; } } void BLE_DisconnectionHandler(uint8_t* pData) { uint8_t status = pData[0]; uint16_t handle = pData[2] | (pData[3] << 8); uint8_t reason = pData[4]; APP_DBG_MSG("Disconnected, handle=%d, reason=0x%02X", handle, reason); /* 断开连接后,默认不恢复广播,等待上层指令 */ aci_gap_stop_advertising(); app_set_state(APP_STATE_DISCONNECTED); }

这个设计里,广播的启停完全由应用层状态机控制,不依赖协议栈默认行为。你可以在这个基础上扩展低功耗逻辑,比如进入低功耗前确保广播已停,唤醒后根据需要重新启动广播。

这里要特别强调:停止广播的返回值一定要检查aci_gap_stop_advertising()返回BLE_STATUS_SUCCESS才算真正执行成功。如果返回BLE_STATUS_TIMEOUTBLE_STATUS_ERROR,说明协议栈状态不对,广播可能还没停。处理方式是重试或记录错误日志,而不是简单忽略。

4.3 参数选择与功耗实测数据

在低功耗场景下,广播不关就等同于设备长期处于高频发射状态。我实测过一组数据:

状态平均电流说明
连接建立后,广播未停止约2.5mA(受广播间隔影响)广播间隔100ms时
连接建立后,广播已停止约0.8mA保持连接,等待数据
断开连接且广播停止约5uA进入低功耗模式后

这个数据在电池供电的设备上是决定性的。如果你做的是纽扣电池供电的设备,广播不关意味着工作时间直接缩短40%以上。

另外,广播参数里的间隔值也值得注意。GAP_ADV_FAST对应的实际间隔是30ms到60ms,GAP_ADV_SLOW是1s到2.5s。间隔越短,被手机发现得越快,但功耗越高。在不需要快速连接的应用里,优先用慢广播,进一步降低平均电流。但对于门锁这种需要秒级连接体验的应用,还是得用快广播,然后在连接建立后快速关闭。

5. 常见问题与排查技巧实录

5.1 广播停止失败,返回错误码 0x42

这是BLE_STATUS_PARAM_ERROR。最常见的原因是:调用aci_gap_stop_advertising()时,传入的广播句柄不匹配。默认语句是aci_gap_stop_advertising(),其实这个函数第二个参数开始有句柄相关参数,在不同版本的库里有差异。如果使用的是较新的 HAL 版本,调用时要确认API签名,传入正确的句柄。

看一下官方定义,STM32WB 的aci_gap_stop_advertising()原型是:

tBleStatus aci_gap_stop_advertising(void);

注意,这个函数在ST的库里没有参数。如果你在代码里看到自己在aci_gap_stop_advertising()里传了参数,比如aci_gap_stop_advertising(0, 0, 0, 0, NULL),说明你用的是旧版API或者是其他芯片平台的移植代码,这种情况需要核对库版本。

多句柄场景下,需要调用aci_gap_stop_advertising()配合句柄参数的重载版本,具体看你的库目录下的ble_gap_aci.h头文件里怎么声明的。我们项目用的1.15版本库,声明就是无参数版本,一次只能停一个广播集。

5.2 连接后广播不停,但代码里确实调用了stop

这种情况,我用两招排查:

第一,在stop调用前加延时看协议栈状态。因为连接完成事件刚触发时,协议栈内部可能还在处理链路层状态迁移,应用层的stop命令虽然进了队列,但被协议栈内部的高优先级任务挤到后面。我试验过,在连接回调里加5ms延时再调stop,成功率明显提高。但延时不能太长,否则用户会感知到连接速度变慢。

第二,打印调用时的hci_le_get_advertising_state()返回值。用这个接口查一下当前广播状态寄存器,如果返回值是GAP_Adv_State_Enabled,说明链路层确实还在广播。这时候再调一次stop,一般就能停掉。如果链路层状态显示已停止,但射频还有信号出来,那就不是广播的问题,可能是其他射频活动,比如正在发送一条命令响应。

5.3 断开连接后,广播自动恢复,状态机混乱

这个问题在协议栈版本较老的固件上更容易出现。断开连接事件里,如果不主动停广播,协议栈可能会依据内部的“恢复广播”策略自动重新启动广播。这是ST为了开发者体验而设计的:设备断开连接后,默认回到可被连接的状态。

但我们的应用不想要这个默认行为,因为断开连接后可能进入低功耗或者等待用户操作。解决办法是:在HCI_DISCONNECTION_COMPLETE_EVENT回调里调用aci_gap_stop_advertising()

注意,这个回调的上下文里协议栈状态可能还是“连接中”,所以stop调用同样可能失败。建议用HAL_Delay(2)加一个短延时再执行。实测下来,2ms延时在绝大多数情况下足够让协议栈完成状态迁移。

5.4 连接建立后用嗅探器抓包看不到广播,但手机能看到

这里有个容易混淆的概念:手机扫描时看到的“广播”其实分两种:

  • 连接建立前的广播包(ADV_IND / ADV_SCAN_IND)
  • 连接建立后,从设备周期性发送的“连接事件”数据包

有些蓝牙嗅探器(尤其是只监听固定信道37/38/39的),只能看到广播包,看不到连接事件。而手机作为中心设备,如果在扫描窗口里碰巧收到了连接事件的数据包,会把它误认为广播。这种情况下,你以为广播没停,实际它已经是连接通信了。

要区分这两种情况,建议用支持跟连接事件的专业抓包工具,比如nRF Sniffer for Bluetooth LE,或者Ellisys的蓝牙分析仪。单纯用手机App做判断,不够精确。

5.5 低功耗模式下射频唤醒异常,广播又被触发

这是一种特别隐蔽的情况。设备进入低功耗停止模式后,外部引脚唤醒或定时器唤醒,代码里如果执行了初始化广播的代码路径,广播会自动恢复。很多工程会在所有启动路径里都调用一下广播初始化,包括唤醒路径。这本身没问题,但如果唤醒后没判断连接状态,就会在连接已经存在的情况下再次启动广播。

这个问题的排查方法是:在广播启动代码里加断点,或者打印调用栈,跟踪是哪条路径触发了广播启动。然后在该路径加状态判断。

6. 防坑清单与设计建议

6.1 代码层的防坑总结

根据实际踩坑经验,整理了一张自查表:

  • 检查调用aci_gap_stop_advertising()时是否返回BLE_STATUS_SUCCESS,失败时打印错误码。
  • 检查是否有多套广播句柄,逐个确认是否全部停止。
  • 检查连接建立和断开连接两个事件回调里,是否都有停止广播逻辑。
  • 检查广播参数里的Advertising_Type是否为GAP_ADV_IND,避免使用会导致连接后广播保留的类型。
  • 检查断开连接后,是否进入了重新广播的默认路径,手动停止广播。
  • 检查代码里是否存在多个广播初始化调用点(如唤醒路径、按键路径),统一由状态机控制。

6.2 架构层面的建议

如果你打算用STM32WB做产品,建议从一开始就把BLE状态机抽象出来。不要在各处直接调用aci_gap_start_advertisingaci_gap_stop_advertising,而是封装成APP_StartAdvertisingAPP_StopAdvertising这类接口,内部统一管理状态标志和回调。后续排查问题时会轻松很多。

另外,把连接事件、断开连接事件、广播状态变化事件都统一到一个事件处理模块里,通过一个回调函数对外通知。应用层根据这些事件驱动UI、传感器采集、低功耗切换等业务逻辑。这样广播行为就在一个可控的地方被集中管理,不会散落各处导致状态混乱。

6.3 关于ST例程的参考价值

ST官方例程虽然很多,但BLE相关的例程往往偏“底层验证”型,对产品化场景的覆盖不够。例如官方例程中连接建立后并不会显式关闭广播,而是依赖协议栈行为。这在开发阶段没问题,但到了产品化阶段就埋了坑。

我的建议是:官方例程可以参考连接流程、服务配置等基础代码,但广播策略、功耗策略、状态管理这些,一定要根据自己的应用场景单独设计,不要直接照搬。

7. 一个实用的广播状态自检函数

最后分享一个调试用的自检函数,可以在主循环里定期调用,实时监控广播状态,在异常时自动恢复。

void BLE_Advertising_Monitor(void) { uint8_t adv_state = 0; tBleStatus ret; /* 查询当前广播状态 */ ret = aci_gap_get_advertising_state(0, &adv_state); if (ret == BLE_STATUS_SUCCESS) { /* adv_state == 0x00 表示停止,0x01 表示进行中 */ if (adv_state != 0x00 && app_state == APP_STATE_CONNECTED) { /* 已连接但广播还在跑,强制停止 */ APP_DBG_MSG("Abnormal: adv state = 0x%02X while connected", adv_state); aci_gap_stop_advertising(); } } }

这个函数可以在低功耗模式下禁用,或者在进入低功耗前调用一次,确认广播已经停止。如果检测到异常,可以记录下来,在下次启动连接时给出警示。对于大批量出货的产品,这种自检逻辑能在早期发现协议栈异常,避免现场问题。

当然,自检函数只是兜底,核心还是要把广播启停逻辑做对。BLE广播这块的状态管理其实是整个STM32WB开发里最容易被低估的环节。协议栈API看着简单,但实际行为受参数配置、事件时序、句柄数量等多方面影响,一不留神就出问题。希望这篇文章能让大家少走弯路,一次就把广播管利索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询