BLE 广播包结构详解:从一个真实抓包说起
- 一、先建立整体图景
- 1.1 一个广播包的完整封装
- 1.2 Preamble 长度随 PHY 变
- 二、PDU Header:16 个 bit 决定一切
- 三、Payload 里装什么:AD Structure
- 3.1 一次完整的"扫码-应答"
- 3.2 实例 ① :Flags
- 3.3 实例 ② :16-bit 服务 UUID 列表
- 3.4 实例 ③ :完整设备名
- 3.5 为什么设备名必须放到 SCAN_RSP?
- 四、从广播到连接:跳频与 Channel Selection
- 4.1 主从怎么同步跳?
- 4.2 Channel Map 由谁维护?
- 五、速查手册
- 5.1 PDU Type 全表
- 5.2 常用 AD Type
- 5.3 三种 PHY
本文以 Zephyr 的
peripheral_hr为例,把「源码里写的ad[]」和「Wireshark 里抓到的字节」逐层对应起来。
读完之后,拿到任意一段 BLE 广播包的十六进制,都能自己拆开看。
一、先建立整体图景
BLE 广播涉及三个层次:
| 层次 | 负责什么 |
|---|---|
| Link Layer(链路层) | 包的物理外壳:怎么在 2.4 GHz 上传一帧 |
| GAP | Payload 里「广播数据」的格式约定,即 AD Structure |
| 应用层 | 广播什么内容 |
1.1 一个广播包的完整封装
+-----------+------------------+--------------+-----------+ | Preamble | Access Address | PDU | CRC | | 1 byte | 4 bytes | 2~39 bytes | 3 bytes | +-----------+------------------+--------------+-----------+ │ ▼ +------------------+-----------------------+ | Header | Payload | | 2 bytes | 0~37 bytes | +------------------+-----------------------+Access Address 在广播信道上固定为0x8E89BED6(低字节先发,所以空口上是D6 BE 89 8E)。它有两个作用:
- 帧同步—— 接收机在噪声中滑动匹配这个 32-bit 序列,一旦匹配上就知道「后面跟着一个有效包」。
- 区分包类型—— 广播信道(37/38/39)上的包固定用它;数据信道(0~36)上的连接包用随机32-bit 值,每次连接都不同。
1.2 Preamble 长度随 PHY 变
| PHY | Preamble | 内容 |
|---|---|---|
| LE 1M | 8 bits(1 字节) | 如01010101 |
| LE 2M | 16 bits(2 字节) | 如0101010101010101 |
| LE Coded | 80 个符号 | 10 组00111100,时长 80 µs |
以上尺寸是传统广播(Legacy Advertising,LE 1M)的情况。扩展广播的 PDU 尺寸不同:Length 字段扩到 8 bits,单包 Payload 最多 255 字节,靠 chaining 可拼到 1650 字节。
二、PDU Header:16 个 bit 决定一切
PDU Header 是 16 bits。BLE 4.x 和 BLE 5.x 看它的角度略有不同 —— 差别只在 bit 5。
BLE 4.x bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 ├─ RFU ─├────── Length ──────┤ RxAdd │ TxAdd │ RFU │ PDU Type │ BLE 5.x(bit 5 从 RFU 变成 ChSel) bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 ├─ RFU ─├────── Length ──────┤ RxAdd │ TxAdd │ ChSel │ RFU │ PDU Type │| 字段 | bit 位置 | 长度 | 含义 |
|---|---|---|---|
| PDU Type | 0~3 | 4 bits | 包类型(ADV_IND / SCAN_RSP / CONNECT_IND …) |
| RFU | 4 | 1 bit | 保留未用 |
| ChSel | 5 | 1 bit | BLE 5.0 起:连接后使用 CSA #1(=0)还是 CSA #2(=1) |
| TxAdd | 6 | 1 bit | 发送方地址类型:0 = Public,1 = Random |
| RxAdd | 7 | 1 bit | 接收方地址类型:0 = Public,1 = Random |
| Length | 8~13 | 6 bits | Payload 长度。字段本身可表示 0~63,传统广播实际最大 37 |
| RFU | 14~15 | 2 bits | 保留未用 |
关于 ChSel:只有双方都支持CSA #2 时才会用 #2,否则降级到 #1。这是规范保证的兼容性 —— 广播者在
ADV_IND里宣告自己是否支持,发起者在CONNECT_IND里宣告,两边都置 1 才启用。
三、Payload 里装什么:AD Structure
peripheral_hr广播时一共发了两个包,Payload 里装了三个 AD 结构:
| 包 | 源码数组 | 装了什么 | 字节数 |
|---|---|---|---|
| ADV_IND | ad[] | Flags + 3 个 16-bit UUID | 11 |
| SCAN_RSP | sd[] | 完整设备名 | 25 |
每个 AD 结构的通用格式是:
┌──────────┬───────────┬──────────────────────┐ │ Length │ AD Type │ AD Data │ │ 1 byte │ 1 byte │ (Length - 1) bytes │ └──────────┴───────────┴──────────────────────┘Length只统计它后面的字节数(即 Type + Data),不含 Length 字段自己。一个AdvData里可以串联多个 AD Structure。
3.1 一次完整的"扫码-应答"
三个 AD 结构分别装在两个包里。它们是这样被手机拿到的:
手机 板子 │ │ │ ◄──────── ADV_IND ───────────────────────┤ 广播包:Flags + UUID 列表 │ │ ├───────── SCAN_REQ ──────────────────────►│ 手机:"我想知道你是谁" │ │ │ ◄──────── SCAN_RSP ──────────────────────┤ 扫描响应:完整设备名 │ │
下面把这三个 AD 结构逐个拆开——每个都从源码一路看到空口字节。
3.2 实例 ① :Flags
源码:
BT_DATA_BYTES(BT_DATA_FLAGS,(BT_LE_AD_GENERAL|BT_LE_AD_NO_BREDR))空口字节:
02 01 06 │ └┬┘ │ └─── AD Type = 0x01(Flags),AD Data = 0x06 └─────── Length = 0x02解码:BT_LE_AD_GENERAL = 0x02(bit 1)、BT_LE_AD_NO_BREDR = 0x04(bit 2),两者相或得0x06
| Bit | 含义 | 值 |
|---|---|---|
| 0 | LE Limited Discoverable Mode | 0 |
| 1 | LE General Discoverable Mode | 1✅ |
| 2 | BR/EDR Not Supported | 1✅ |
| 3~4 | Simultaneous LE + BR/EDR | 0 |
| 5~7 | Reserved | 0 |
3.3 实例 ② :16-bit 服务 UUID 列表
源码:
BT_DATA_BYTES(BT_DATA_UUID16_ALL,BT_UUID_16_ENCODE(BT_UUID_HRS_VAL),// 0x180DBT_UUID_16_ENCODE(BT_UUID_BAS_VAL),// 0x180FBT_UUID_16_ENCODE(BT_UUID_DIS_VAL))// 0x180A空口字节:
07 03 0d 18 0f 18 0a 18 │ │ └───────┬───────┘ │ │ └─── AD Data(6 字节 = 3 个 UUID) │ └───────────── AD Type = 0x03(Complete List of 16-bit Service Class UUIDs) └──────────────── Length = 0x07| 空口字节 | UUID | 服务 |
|---|---|---|
0d 18 | 0x180D | Heart Rate Service(HRS) |
0f 18 | 0x180F | Battery Service(BAS) |
0a 18 | 0x180A | Device Information Service(DIS) |
UUID 在空口上是小端的,所以0d 18还原成0x180D。
3.4 实例 ③ :完整设备名
源码:
BT_DATA(BT_DATA_NAME_COMPLETE,CONFIG_BT_DEVICE_NAME,sizeof(CONFIG_BT_DEVICE_NAME)-1)空口字节:
18 09 5a 65 70 68 79 72 20 48 65 61 72 74 72 61 74 65 20 53 65 6e 73 6f 72 │ │ └────────────────────────────────────────────────────────────────────┘ │ │ AD Data(23 字节,ASCII) │ └───── AD Type = 0x09(Complete Local Name) └──────── Length = 0x18 = 24(Type + Data 共 24 字节)解码:AD Data 是 23 字节 ASCII,即Zephyr Heartrate Sensor。
源码里的sizeof(CONFIG_BT_DEVICE_NAME) - 1正是去掉字符串末尾的\0,得到 23 字节。
3.5 为什么设备名必须放到 SCAN_RSP?
Legacy 广播的AdvData/ScanRspData上限都只有31 字节。本例中:
Flags 的 AD 结构 = 1(Length) + 1(Type) + 1(Data) = 3 字节 UUID 列表 AD 结构 = 1(Length) + 1(Type) + 6(3 个 UUID) = 8 字节 设备名 AD 结构 = 1(Length) + 1(Type) + 23(名字) = 25 字节 ────────────────────────────────────────────────────────────────── 全部塞进 AdvData 36 字节 > 31 ✗超了, 所以源码把设备名挪到SCAN_RSP,广播包只留 Flags + UUID(11 字节)。
这是规范约束,不是「习惯」。
四、从广播到连接:跳频与 Channel Selection
为什么要跳频?因为 2.4 GHz 是公共频段,WiFi、蓝牙、Zigbee、微波炉都挤在一起。如果 BLE 一直用某个信道而它正好被 WiFi 占着,就会持续丢包。跳频把丢包风险分摊到 37 个信道上—— 某些信道差,其他信道还能用。
| 阶段 | 信道 | 数量 | 特点 |
|---|---|---|---|
| 广播阶段 | 37、38、39 | 3 个 | 固定,所有人都知道 |
| 连接阶段 | 0~36 | 37 个 | 跳频 |
4.1 主从怎么同步跳?
不能靠 “再发个包商量”,必须用双方都知道的确定性算法:
| 项目 | CSA #1 | CSA #2 |
|---|---|---|
| 引入版本 | BLE 4.0 | BLE 5.0 |
| 空口 ChSel 值 | 0 | 1 |
| 算法本质 | 简单递增取模 | 伪随机 + 位运算 |
| 核心公式(简化) | next = (last + hop) mod 37 | 基于channelIdentifier + counter的伪随机映射 |
| 信道图受限时的表现 | ⚠️ 分布不均,部分信道被过度使用 | ✅ 保持均匀 |
| 抗干扰能力 | 一般 | 显著更强 |
| 最少可用信道要求 | ≥ 2 | ≥ 2 |
CSA #2 是可选特性:如果一方不支持,连接时自动降级到 CSA #1,不会出现「连不上」,只是性能差异。
谁决定?:广播者在ADV_IND的 ChSel 位宣告,发起者在CONNECT_IND里也带 ChSel,双方都支持才用 #2。
4.2 Channel Map 由谁维护?
主设备(通常是手机)。它在CONNECT_IND里把信道图交给从机,之后要改就通过LL_CHANNEL_MAP_IND下发。手机做 WiFi 共存时,会把被 WiFi 占用的信道标记为「不可用」,BLE 就只在剩下的信道里跳。
从机只能被动接受,没有拒绝的权利。规范要求信道图里至少要有 2 个可用信道 —— 即使干扰极严重,也要保留最少 2 个才能维持跳频。
五、速查手册
5.1 PDU Type 全表
| 二进制 | 十六进制 | PDU Type | 说明 |
|---|---|---|---|
| 0000 | 0x0 | ADV_IND | 可连接 + 可扫描 + 非定向广播 |
| 0001 | 0x1 | ADV_DIRECT_IND | 可连接定向广播(高速重连) |
| 0010 | 0x2 | ADV_NONCONN_IND | 不可连接、不可扫描(Beacon 常用) |
| 0011 | 0x3 | SCAN_REQ | 扫描请求(由中心设备发出) |
| 0100 | 0x4 | SCAN_RSP | 扫描响应 |
| 0101 | 0x5 | CONNECT_IND | 连接请求 |
| 0110 | 0x6 | ADV_SCAN_IND | 可扫描但不可连接 |
| 0111 | 0x7 | ADV_EXT_IND | 扩展广播指示(BLE 5.0) |
| 1000 | 0x8 | AUX_ADV_IND 等 | 辅助广播信道包(BLE 5.0) |
| 1001~1111 | 0x9~0xF | Reserved | 保留 |
5.2 常用 AD Type
| AD Type | 名称 | 说明 |
|---|---|---|
| 0x01 | Flags | 发现模式 / BR/EDR 支持 |
| 0x02 / 0x03 | 16-bit UUID 列表 | 不完整 / 完整 |
| 0x04 / 0x05 | 32-bit UUID 列表 | 不完整 / 完整 |
| 0x06 / 0x07 | 128-bit UUID 列表 | 不完整 / 完整 |
| 0x08 / 0x09 | 设备名 | 截断版 / 完整版 |
| 0x0A | Tx Power Level | 发射功率 |
| 0x12 | Peripheral Connection Interval Range | 从机期望的连接间隔范围 |
| 0x16 | Service Data (16-bit UUID) | 服务数据 |
| 0x19 | Appearance | 外观(决定手机显示什么图标) |
| 0x1A | Advertising Interval | 广播间隔 |
| 0x24 | URI | 网址 |
| 0xFF | Manufacturer Specific Data | 厂商自定义数据(iBeacon 核心) |
5.3 三种 PHY
| PHY | 调制速率 | 编码 | 数据速率 | 特点 |
|---|---|---|---|---|
| LE 1M | 1 Msym/s | 无 | 1 Mb/s | BLE 4.0 原始 PHY,兼容性最好 |
| LE 2M | 2 Msym/s | 无 | 2 Mb/s | BLE 5.0 新增,快一倍,距离更近 |
| LE Coded | 1 Msym/s | S=8 | 125 kb/s | BLE 5.0 新增,距离最远 |
| LE Coded | 1 Msym/s | S=2 | 500 kb/s | 速率与距离的折中 |
S= 每个 bit 用几个符号。S 越大,纠错越强、传得越远,但速率越低。
主广播信道(37/38/39)不能用 LE 2M,这是为了向后兼容老设备。
传统广播(ADV_IND / SCAN_RSP / CONNECT_IND)固定用 LE 1M;
扩展广播(ADV_EXT_IND)则可以在主信道上使用 LE Coded PHY。