【从零开始】手写BLE协议栈(2-1)主动扫描(Active Scanning)
主动扫描:ADV_IND、SCAN_REQ 与 SCAN_RSP 的三包握手
本章示例代码:https://github.com/ixbwer/write-BLE-stack-from-scratch/tree/master/02_active_scan
前提知识
阅读本文需要具备以下基础:
- 理解 BLE 广播帧结构(1-2 篇有详细讲解):1-2 篇中的广播者使用 ADV_NONCONN_IND(不可扫描),本文升级为 ADV_IND(可扫描)。你需要理解 PDU 格式——Header、AdvA 和 AdvData——以及广播使用哪三个信道(37/38/39)。
- 理解 RADIO 状态机(1-1 有详细讲解):主动扫描涉及 RX→TX→RX 状态切换——先接收 ADV_IND,再发送 SCAN_REQ,再接收 SCAN_RSP,不理解状态机就看不懂这个切换过程。
本文只关注广播→扫描阶段的三包交互和两包之间有 150 µs 约束",不深入分析其成因,成因在 2-2 篇展开。
一、广播不是独白,是开放式提问
在 1-2 篇里,我们让 nRF52 每隔 100 ms 在信道 37/38/39 上发送一次 ADV_NONCONN_IND 包——一种不可连接、不可扫描的广播。它的作用就是一张名片:把设备名广而告之,不关心谁在听。但 ADV_NONCONN_IND 只能"贴名片",不允许扫描器反问。本章的广播者将升级为 ADV_IND(可连接可扫描广播),这样扫描器才有资格发出 SCAN_REQ。
但名片的正面空间有限——ADV_IND 的 AdvData 字段最多只有 31 字节。设想一台蓝牙温湿度传感器,它要在名片里放设备名(8 字节)、公司标识(4 字节)、电池电量和传感器数值(10 字节),31 字节很快就用完了。如果扫描方想知道这台传感器的固件版本?装不下。
BLE 规范给扫描器提供了一种"翻名片背面"的机制:主动扫描(Active Scanning)。流程是这样的——
扫描器收到 ADV_IND 后,向广播者发送一个 SCAN_REQ 包,意思是"我注意到你了,请把你的附加信息也给我"。广播者收到后,用 SCAN_RSP 作为回应,同样携带最多 31 字节的额外数据(ScanRspData)。两包加在一起,扫描器最多可获得 62 字节的设备信息。
这就是 ADV_IND → SCAN_REQ → SCAN_RSP 三包握手的全貌。
与之对应的,被动扫描(Passive Scanning) 是扫描器只收听 ADV_IND,对感兴趣的设备静默记录——什么也不回发。它的优点是扫描器自身对周围完全"无感知",不会暴露任何自身信息,在功耗敏感或隐私敏感的场景里常用。

二、角色分工:广播者(Advertiser) 与扫描器(Scanner)
三包握手有两个设备参与,但角色不对称:
广播者(Advertiser) 是发出 ADV_IND 的一方。它在 ADV_IND 的 Header 中把 PDU 类型设为 ADV_IND(而不是 ADV_NONCONN_IND),就等于贴出一块告示牌:"来者可问"。
BLE Core Spec(Vol 6, Part B, Section 2.3.1)把广播 PDU 类型分为四种,其中只有两种接受 SCAN_REQ:
| PDU 类型 | 类型值 | 可连接 | 可扫描(接受 SCAN_REQ) | 定向 |
|---|---|---|---|---|
| ADV_IND | 0b0000 |
✓ | ✓ | ✗ |
| ADV_DIRECT_IND | 0b0001 |
✓ | ✗ | ✓ |
| ADV_NONCONN_IND | 0b0010 |
✗ | ✗ | ✗ |
| ADV_SCAN_IND | 0b0110 |
✗ | ✓ | ✗ |
扫描器(Scanner) 是监听广播信道、接收 ADV_IND 的一方。扫描器收到广播包之后,第一件事是检查 PDU 类型。如果类型是 ADV_IND 或 ADV_SCAN_IND,说明对方允许被扫描,可以发出 SCAN_REQ。如果是 ADV_NONCONN_IND 或 ADV_DIRECT_IND(定向广播,针对特定设备),扫描器就算发出 SCAN_REQ 也不会收到回应——规范明确禁止广播者在这两种类型下回复 SCAN_RSP。
信道 在三包交互中保持不变。ADV_IND、SCAN_REQ 和 SCAN_RSP 都在同一个广播信道(37、38 或 39 之一)上完成,不切换信道。这是 BLE 规范的硬性要求(Core Spec Vol 6, Part B, Section 4.4.2)——广播者在哪个信道被"问"到,就在哪个信道作答。

三、SCAN_REQ:扫描器的一张请帖
SCAN_REQ 是扫描器发向广播者的一个纯粹的"请求包"——它不携带任何数据,只是告诉广播者两件事:"我是谁(ScanA)" 和 "我想找你(AdvA)"。
BLE Core Spec Vol 6, Part B, Section 2.3.2.1 规定了 SCAN_REQ 的 PDU 格式:

ScanA 是扫描器自身的蓝牙地址,广播者收到后可以用它来决定"要不要回应这台设备"(比如隐私保护场景下,广播者可能只回应白名单里的地址)。
AdvA 是从刚才收到的 ADV_IND 包中提取的广播者地址。广播者收到 SCAN_REQ 后,先核对 AdvA 是否等于自己的地址——如果不匹配,说明这条 SCAN_REQ 是发往别人的,广播者静默忽略,不回应。
SCAN_REQ 没有 AdvData、没有负载,格式固定,长度永远是 12 字节(只有链路层 PDU 头 + ScanA + AdvA)。它的意义就是一张请帖:拿着广播者的名字去敲门,同时亮出自己的身份。
四、SCAN_RSP:广播者的名片背面
广播者在收到合法的 SCAN_REQ(AdvA 匹配,且本设备当前允许被扫描)后,发出 SCAN_RSP 作为回应。SCAN_RSP 的格式(Core Spec Vol 6, Part B, Section 2.3.2.2)如下:

ScanRspData 的格式与 ADV_IND 中的 AdvData 完全相同——由一系列 AD Structure 串联构成:

示例:完整设备名"ThermoPro"
09 09 54 68 65 72 6D 6F 50 72 6F
│ │ └─ "T""h""e""r""m""o""P""r""o"
│ └─ Type = 0x09(完整设备名)
└─ Length = 9(Type + 8字节名称)
广播者通常把高频查询的信息(设备名、服务 UUID)放进 ADV_IND,把低频引用的信息(固件版本、制造商私有数据、配置序列号)放进 SCAN_RSP。这样既不占用固定广播包的宝贵空间,又允许真正"有需要的"扫描器按需索取。
两包合计,扫描器最多能拿到 62 字节的设备信息(ADV_IND 的 31 + SCAN_RSP 的 31)。
五、三包交互的精确时序
三包握手并不是"各方自己决定什么时候发"——每两包之间都有严格的时序约束:T_IFS(Inter-Frame Space,帧间间隔)= 150 µs ±2 µs。
这个约束是双向的,谁是回包方、谁就负责这 150 µs 的计时:

两个关键约束点:
-
ADV_IND → SCAN_REQ:扫描器在收到 ADV_IND 的最后一个 bit 后,必须恰好在 150 µs 内让 SCAN_REQ 的第一个 bit 飞出天线。如果扫描器"慢了",广播者的 RX 接收窗口会超时关闭,这次扫描完全失败。
-
SCAN_REQ → SCAN_RSP:广播者在收到 SCAN_REQ 的最后一个 bit 后,再用恰好 150 µs 把 SCAN_RSP 的第一个 bit 发出去。如果广播者"慢了",等待中的扫描器会超时放弃,SCAN_RSP 永远无法被接收。

注意"恰好"这两个字——T_IFS 不是"不超过 150 µs",而是精确等于 150 µs ±2 µs。2 µs 的容忍范围非常窄,用 CPU 软件计时根本无法满足,必须依赖硬件机制。这个问题的完整分析在 2-2 篇,nRF52 的硬件实现方案在 2-3 篇。
本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪
本文版权归作者:ixbwer所有,转载请注明原文链接:https://www.cnblogs.com/ixbwer/p/19786057,否则保留追究法律责任的权利。

浙公网安备 33010602011771号