广播PDU结构:PDU类型与报头载荷
上一章我们聊了广播通道和物理层的基本概念。这一章,咱们得钻进数据包内部,看看广播PDU到底长什么样。
说白了,BLE广播就是设备在三个广播通道上不断往外扔小数据包。这个小数据包,就叫广播PDU(Protocol Data Unit)。你想想看,如果连这个包的结构都不清楚,那调试广播问题的时候,基本就是两眼一抹黑。
我记得刚入行那会儿,有个项目设备老是连不上手机。我拿着抓包工具看了半天,发现广播包类型配错了——该用可连接广播的,配成了不可连接广播。手机扫描到了,但就是不发连接请求。从那以后,我对PDU类型就特别敏感。
广播PDU的整体结构
一个完整的广播PDU,由两部分组成:
- 报头(Header):2字节,描述这个包的类型、长度、发送地址类型等信息
- 载荷(Payload):最多37字节,真正携带的数据内容
嗯,这里要注意。BLE 4.0/4.1时代,载荷最大是31字节。到了4.2,扩展广播把载荷上限提到了255字节。但咱们这一章先聚焦传统广播,也就是31字节载荷的老规矩。
核心要点:广播PDU = 2字节报头 + 最多31字节载荷。总长度不超过33字节(不含前导码和接入地址)。
报头(Header)详解
报头这2个字节,信息量很大。我习惯把它拆成几个字段来看:
| 字段 | 位数 | 说明 |
|---|---|---|
| PDU类型 | 4 bit | 决定这个广播包是干啥用的 |
| RFU | 2 bit | 保留位,必须填0 |
| TxAdd | 1 bit | 发送地址类型:0=公共地址,1=随机地址 |
| RxAdd | 1 bit | 接收地址类型(仅用于定向广播) |
| 长度 | 6 bit | 载荷的字节数,范围0~31 |
| RFU | 2 bit | 保留位 |
你看,报头里最重要的就是那4个bit的PDU类型。它决定了整个广播包的“性格”。
四种核心广播PDU类型
BLE规范定义了四种传统广播PDU类型。我一个个说。
1. ADV_IND —— 可连接无定向广播
这是最常见的广播类型。设备在三个广播通道上轮流发送,任何扫描设备都能收到,而且收到后可以发起连接请求。
特点:
- 可被扫描
- 可被连接
- 不指定目标设备(无定向)
我在项目中90%的场景用的都是ADV_IND。比如智能灯泡、温湿度传感器,这些需要被手机发现并连接的设备,都用这个类型。
2. ADV_DIRECT_IND —— 可连接定向广播
这个类型比较特殊。它指定了目标设备的地址,只有那个设备能响应。而且它只在三个广播通道中的一个通道上发送一次,然后就停了。
特点:
- 不可被扫描
- 可被连接(仅限指定设备)
- 定向发送,效率高
- 发送一次即结束
我的经验:ADV_DIRECT_IND常用于已经配对过的设备快速重连。比如蓝牙耳机从充电仓拿出来,它知道手机的地址,直接发定向广播,手机收到后立刻发起连接,速度比ADV_IND快很多。
3. ADV_NONCONN_IND —— 不可连接无定向广播
这种广播只发数据,不接受连接。说白了就是“我只管说,你爱听不听,但别想连我”。
特点:
- 可被扫描
- 不可被连接
- 常用于信标(Beacon)类设备
你想想看,苹果的iBeacon、谷歌的Eddystone,用的就是ADV_NONCONN_IND。它们只需要广播一个UUID和电量信息,不需要手机去连接它们。
4. ADV_SCAN_IND —— 可扫描无定向广播
这个类型有点中间态。它允许扫描请求,但不允许连接请求。
特点:
- 可被扫描(可以响应扫描请求)
- 不可被连接
说实话,这个类型我用得不多。它存在的意义是:设备想提供更多数据给扫描者,但又不想被连接。扫描者发一个扫描请求,设备回复一个扫描响应数据包,这样就能传递更多信息。
避坑指南:我曾经在一个项目中,把信标设备配成了ADV_IND。结果手机扫描到后,总弹出连接提示,用户体验极差。后来改成ADV_NONCONN_IND,问题解决。所以,不需要连接的设备,一定用ADV_NONCONN_IND或ADV_SCAN_IND。
四种类型对比表
| PDU类型 | 可扫描 | 可连接 | 定向 | 典型场景 |
|---|---|---|---|---|
| ADV_IND | ✓ | ✓ | ✗ | 传感器、灯泡、通用外设 |
| ADV_DIRECT_IND | ✗ | ✓(仅指定设备) | ✓ | 已配对设备快速重连 |
| ADV_NONCONN_IND | ✓ | ✗ | ✗ | 信标、广播器 |
| ADV_SCAN_IND | ✓ | ✗ | ✗ | 需要额外数据的广播器 |
载荷(Payload)结构
说完报头,咱们看看载荷。载荷最多31字节,里面放的是真正的广播数据。
广播数据的组织方式,遵循一个简单的规则:AD Structure。每个AD Structure由三部分组成:
- 长度(1字节):表示后面数据和类型的总长度
- 类型(1字节):AD Type,标识这段数据的含义
- 数据(N字节):实际数据内容
举个例子,一个完整的广播包载荷可能是这样的:
02 01 06 03 02 12 34 0A 09 48 65 6C 6C 6F
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ └─ 'o'
│ │ │ │ │ │ │ │ │ │ │ │ └──── 'l'
│ │ │ │ │ │ │ │ │ │ │ └─────── 'l'
│ │ │ │ │ │ │ │ │ │ └────────── 'e'
│ │ │ │ │ │ │ │ │ └───────────── 'H'
│ │ │ │ │ │ │ │ └──────────────── 长度=10 (0x0A)
│ │ │ │ │ │ │ └─────────────────── AD Type=0x09 (Complete Local Name)
│ │ │ │ │ │ └────────────────────── 数据=0x1234
│ │ │ │ │ └───────────────────────── AD Type=0x02 (Incomplete 16-bit UUIDs)
│ │ │ │ └──────────────────────────── 长度=3
│ │ │ └─────────────────────────────── AD Type=0x01 (Flags)
│ │ └────────────────────────────────── 长度=2
│ └───────────────────────────────────── 第一个AD Structure开始
你看,载荷就是由一串AD Structure拼接而成的。每个Structure之间没有分隔符,靠长度字段来划分边界。
小技巧:解析广播数据时,我习惯用while循环,每次读1字节长度,然后跳转到下一个Structure。如果长度字段为0,说明数据结束或者有问题,直接退出。这样能避免解析到垃圾数据。
广播PDU结构总览图
下面这张图,把整个广播PDU的结构和四种类型的关系画清楚了。
实际项目中的选择建议
说了这么多理论,咱们来点实际的。你在项目中到底该选哪种广播类型?
我个人习惯按这个思路来:
- 设备需要被手机连接吗? → 需要就用ADV_IND,不需要就看下一步
- 设备需要响应扫描请求吗? → 需要就用ADV_SCAN_IND,不需要就用ADV_NONCONN_IND
- 设备需要快速重连已知主机吗? → 用ADV_DIRECT_IND
就这么简单。别把问题搞复杂了。
注意:ADV_DIRECT_IND有个坑——它只发一次。如果目标设备没收到,这次广播就浪费了。所以重连场景下,我通常会先发几次ADV_IND做“预热”,再切到ADV_DIRECT_IND。这样成功率更高。
小结
这一章我们把广播PDU的结构拆了个底朝天。报头里的PDU类型决定了广播包的“性格”,载荷里的AD Structure决定了广播包的内容。四种类型各有各的用途,选对了,项目就顺了。