4. 扫描策略:主动扫描 vs 被动扫描,扫描参数配置
说到 BLE 扫描,很多刚入行的朋友觉得不就是打开扫描器收广播包嘛。其实没那么简单。扫描策略选不对,轻则丢包,重则根本连不上设备。我这些年调试过的项目里,至少有一半的连不上问题,最后都追溯到扫描参数上。
这一节,咱们把主动扫描和被动扫描掰开揉碎了讲清楚。再聊聊那些看似不起眼、实则要命的扫描参数。
4.1 主动扫描 vs 被动扫描:核心区别
先看一张图,把两种扫描的流程画清楚。
说白了,被动扫描就是只听不说。扫描器在三个广播信道(37、38、39)上轮流监听,收到广播包就收下,不回复任何东西。主动扫描呢,收到广播包后还会发一个 SCAN_REQ 回去,外设收到后回复 SCAN_RSP,这样就能拿到更多数据。
我记得刚做第一个 BLE 项目时,产品经理说设备广播里要带设备名称和电量。我心想广播数据就那么点空间,哪塞得下?后来才发现,用主动扫描拿 SCAN_RSP 就能搞定。嗯,这个坑踩得值。
4.2 什么时候用主动扫描?什么时候用被动扫描?
这不是拍脑袋选的。我一般按下面这个逻辑来判断:
| 场景 | 推荐扫描方式 | 原因 |
|---|---|---|
| 仅发现设备,不需要额外信息 | 被动扫描 | 省电,减少空中冲突 |
| 需要获取设备名称、服务 UUID 等 | 主动扫描 | 广播包放不下,靠 SCAN_RSP 补充 |
| 低功耗设备(如纽扣电池传感器) | 被动扫描 | 减少外设应答次数,延长电池寿命 |
| 密集环境(多设备同时广播) | 被动扫描 | SCAN_REQ 会增加碰撞概率 |
| 需要快速获取完整设备信息 | 主动扫描 | 一次交互拿到全部数据,减少后续连接 |
4.3 扫描参数配置:这些参数决定了你能不能连上
参数配置才是真正的技术活。很多开发者拿着 SDK 默认参数就跑,结果发现要么扫描不到设备,要么扫描到了但连不上。你想想看,扫描窗口和扫描间隔这两个参数,直接决定了你的扫描器有多「勤快」。
4.3.1 扫描窗口 (Scan Window) 和 扫描间隔 (Scan Interval)
这两个参数是成对出现的。我习惯用一个比喻来解释:
- 扫描窗口:你每次「睁眼」看多久
- 扫描间隔:你每隔多久「睁一次眼」
举个例子:扫描间隔 100ms,扫描窗口 50ms。意思是每 100ms 里,有 50ms 在监听,剩下 50ms 在休息。
| 参数 | 范围 (BLE Spec) | 推荐值 (自动重连) | 说明 |
|---|---|---|---|
| 扫描间隔 | 2.5ms ~ 10.24s | 100ms ~ 300ms | 间隔越小,发现越快,但耗电 |
| 扫描窗口 | 1.25ms ~ 10.24s | 50ms ~ 150ms | 窗口越大,单次扫描覆盖越全 |
| 窗口/间隔比 | ≤ 1 | 0.5 ~ 0.8 | 窗口不能超过间隔,否则无效 |
4.3.2 扫描模式 (Scan Type)
这个参数决定了你是被动扫描还是主动扫描。在代码里通常是一个枚举值:
// 以 Nordic nRF5 SDK 为例
#define SCAN_PASSIVE 0 // 被动扫描
#define SCAN_ACTIVE 1 // 主动扫描
// 配置示例
ble_gap_scan_params_t scan_params = {
.active = SCAN_ACTIVE, // 主动扫描
.interval = MSEC_TO_UNITS(100, UNIT_0_625_MS), // 100ms
.window = MSEC_TO_UNITS(50, UNIT_0_625_MS), // 50ms
.timeout = 0, // 持续扫描
.scan_phys = BLE_GAP_PHY_1MBPS,
};
这里要注意,主动扫描虽然能拿到更多数据,但每次 SCAN_REQ/SCAN_RSP 交互都会占用空中时间。在设备密集的环境里,过多的 SCAN_REQ 反而会加剧碰撞,导致丢包。
4.3.3 扫描 PHY (Physical Layer)
现在 BLE 5.0 以上支持 1M PHY、2M PHY 和 Coded PHY。扫描时也要指定用哪个:
- 1M PHY:最通用,兼容性好,所有 BLE 设备都支持
- 2M PHY:速度快,但距离短,适合高速数据传输
- Coded PHY:距离远(可达 400 米+),但速率低,适合远距离场景
我个人在自动重连场景里,首选 1M PHY。为什么?因为绝大多数外设广播用的就是 1M PHY。你用 2M PHY 去扫描,根本收不到 1M 的广播包。
4.3.4 扫描超时 (Scan Timeout)
这个参数决定了扫描持续多久后自动停止。0 表示持续扫描,直到你手动停止。
在自动重连场景里,我建议设一个合理的超时值。比如 10 秒。如果 10 秒内没发现目标设备,就停下来,等一段时间再重试。这样比一直开着扫描要省电得多。
// 带超时的扫描配置示例
scan_params.timeout = MSEC_TO_UNITS(10000, UNIT_10_MS); // 10秒超时
// 扫描回调中处理超时事件
void scan_evt_handler(ble_evt_t const * p_ble_evt) {
switch (p_ble_evt->header.evt_id) {
case BLE_GAP_EVT_TIMEOUT:
// 扫描超时,停止扫描
sd_ble_gap_scan_stop();
// 启动定时器,30秒后重新扫描
app_timer_start(m_retry_timer, 30000, NULL);
break;
}
}
4.4 扫描参数的权衡:一张图看懂
参数配置其实就是一场权衡游戏。我画了张图,把几个核心关系理清楚:
4.5 实战建议:自动重连场景的扫描参数模板
说了这么多理论,最后给出一套我实际项目中验证过的参数模板。你可以直接拿去用,再根据具体场景微调:
| 参数 | 推荐值 | 适用场景 |
|---|---|---|
| 扫描间隔 | 100ms | 快速发现,适合近距离重连 |
| 扫描窗口 | 50ms | 与间隔匹配,兼顾功耗 |
| 扫描模式 | 主动扫描 | 需要获取设备名称和服务 UUID |
| 扫描 PHY | 1M PHY | 兼容绝大多数 BLE 外设 |
| 扫描超时 | 10s | 避免长时间空转 |
| 重试间隔 | 30s | 超时后等待 30s 再重试 |