尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32WB BLE广播停止实战:连接后彻底关闭广播的完整方案

STM32WB BLE广播停止实战:连接后彻底关闭广播的完整方案 最近在折腾NUCLEO-WB55RG这块板子遇到一个挺有意思的需求设备已经通过BLE连上了但广播advertising还在继续发不仅浪费功耗在某些场景下还会干扰已经建立的连接。网上搜了一圈很多人问怎么彻底关掉广播但答案都比较零散。我花了点时间把这块的机制和实现彻底捋了一遍今天把完整方案和踩坑记录分享出来。这块板子用的是STM32WB55RG一颗集成BLE 5.0协议栈的双核MCUCortex-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广播类型。我最终采用的方案是双保险在连接建立的回调事件HCI_LE_CONNECTION_COMPLETE_EVENT中主动调用aci_gap_stop_advertising()。同时把广播参数里的Advertising Type设置为ADV_IND可连接无定向广播并确保没有使能“多广播”或“周期广播”等特殊模式。实际操作下来只要连接事件回调在应用层正确处理aci_gap_stop_advertising()是可以把广播彻底停掉的。但有个前提你调用的时机必须在连接建立之后且协议栈状态机的状态已经迁移到Connection否则可能出现竞态条件。2.2 为什么要双保险而不是只改配置这里要解释一下协议栈设计上的一些反直觉之处。ST的BLE协议栈基于STM32CubeWB里的Core在设计上把GAPGeneric 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, reason0x%02X, handle, reason); /* 断开连接后默认不恢复广播等待上层指令 */ aci_gap_stop_advertising(); app_set_state(APP_STATE_DISCONNECTED); }这个设计里广播的启停完全由应用层状态机控制不依赖协议栈默认行为。你可以在这个基础上扩展低功耗逻辑比如进入低功耗前确保广播已停唤醒后根据需要重新启动广播。这里要特别强调停止广播的返回值一定要检查。aci_gap_stop_advertising()返回BLE_STATUS_SUCCESS才算真正执行成功。如果返回BLE_STATUS_TIMEOUT或BLE_STATUS_ERROR说明协议栈状态不对广播可能还没停。处理方式是重试或记录错误日志而不是简单忽略。4.3 参数选择与功耗实测数据在低功耗场景下广播不关就等同于设备长期处于高频发射状态。我实测过一组数据状态平均电流说明连接建立后广播未停止约2.5mA受广播间隔影响广播间隔100ms时连接建立后广播已停止约0.8mA保持连接等待数据断开连接且广播停止约5uA进入低功耗模式后这个数据在电池供电的设备上是决定性的。如果你做的是纽扣电池供电的设备广播不关意味着工作时间直接缩短40%以上。另外广播参数里的间隔值也值得注意。GAP_ADV_FAST对应的实际间隔是30ms到60msGAP_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_advertising和aci_gap_stop_advertising而是封装成APP_StartAdvertising、APP_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看着简单但实际行为受参数配置、事件时序、句柄数量等多方面影响一不留神就出问题。希望这篇文章能让大家少走弯路一次就把广播管利索。
返回列表