4. 扫描策略:主动扫描 vs 被动扫描,扫描参数配置

说到 BLE 扫描,很多刚入行的朋友觉得不就是打开扫描器收广播包嘛。其实没那么简单。扫描策略选不对,轻则丢包,重则根本连不上设备。我这些年调试过的项目里,至少有一半的连不上问题,最后都追溯到扫描参数上。

这一节,咱们把主动扫描和被动扫描掰开揉碎了讲清楚。再聊聊那些看似不起眼、实则要命的扫描参数。

4.1 主动扫描 vs 被动扫描:核心区别

先看一张图,把两种扫描的流程画清楚。

主动扫描 vs 被动扫描 流程对比 被动扫描 (Passive Scanning) 扫描器监听广播信道 (37/38/39) 接收 ADV_IND / ADV_NONCONN_IND 仅接收,不发送任何请求 主动扫描 (Active Scanning) 监听广播信道 (37/38/39) 收到 ADV_IND 后,发送 SCAN_REQ 外设回复 SCAN_RSP(额外数据) 获取完整广播数据 + 扫描响应数据 被动扫描:只听不说,省电但信息少 主动扫描:一问一答,信息完整但多一次空中交互

说白了,被动扫描就是只听不说。扫描器在三个广播信道(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 窗口不能超过间隔,否则无效
我曾经踩过的坑:有一次把扫描窗口设得比扫描间隔还大,结果芯片直接不工作了。查了半天手册才发现,有些芯片对 window > interval 的处理是直接报错。所以一定要保证 window ≤ interval。

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 的广播包。

关键点:扫描 PHY 必须与外设广播 PHY 匹配。外设用 1M 广播,你就得用 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 扫描参数的权衡:一张图看懂

参数配置其实就是一场权衡游戏。我画了张图,把几个核心关系理清楚:

扫描参数权衡关系 扫描参数配置 扫描间隔 ↓ 发现更快,但更耗电 扫描窗口 ↑ 单次覆盖更全,功耗增加 主动扫描 信息完整,空中碰撞↑ 扫描超时 ↑ 等待更久,功耗更高 核心原则:在功耗和发现速度之间找到平衡点 自动重连场景建议:间隔 100ms,窗口 50ms,主动扫描,超时 10s

4.5 实战建议:自动重连场景的扫描参数模板

说了这么多理论,最后给出一套我实际项目中验证过的参数模板。你可以直接拿去用,再根据具体场景微调:

参数 推荐值 适用场景
扫描间隔 100ms 快速发现,适合近距离重连
扫描窗口 50ms 与间隔匹配,兼顾功耗
扫描模式 主动扫描 需要获取设备名称和服务 UUID
扫描 PHY 1M PHY 兼容绝大多数 BLE 外设
扫描超时 10s 避免长时间空转
重试间隔 30s 超时后等待 30s 再重试
一个小技巧:如果设备在移动中(比如车载蓝牙),可以把扫描间隔缩短到 50ms,窗口保持 25ms。这样能更快捕捉到广播信号。代价是功耗会翻倍,自己权衡。