广播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的结构和四种类型的关系画清楚了。

广播PDU结构总览 广播PDU 前导码(1B) + 接入地址(4B) + PDU(2~33B) + CRC(3B) 报头 Header 2字节 载荷 Payload 0~31字节(传统广播) 报头字段分解 PDU类型(4bit) | RFU(2bit) | TxAdd(1bit) RxAdd(1bit) | 长度(6bit) | RFU(2bit) 共16bit = 2字节 四种广播PDU类型 ADV_IND ADV_DIRECT_IND ADV_NONCONN_IND ADV_SCAN_IND 关键说明 • 传统广播PDU总长度不超过33字节(报头2B + 载荷最多31B) • 载荷由多个AD Structure拼接而成,每个Structure包含:长度(1B) + 类型(1B) + 数据(NB) • ADV_IND:可扫描+可连接,最常用 • ADV_DIRECT_IND:定向连接,快速重连场景 • ADV_NONCONN_IND:不可连接,信标场景 • ADV_SCAN_IND:可扫描但不可连接,需要额外数据的广播

实际项目中的选择建议

说了这么多理论,咱们来点实际的。你在项目中到底该选哪种广播类型?

我个人习惯按这个思路来:

  1. 设备需要被手机连接吗? → 需要就用ADV_IND,不需要就看下一步
  2. 设备需要响应扫描请求吗? → 需要就用ADV_SCAN_IND,不需要就用ADV_NONCONN_IND
  3. 设备需要快速重连已知主机吗? → 用ADV_DIRECT_IND

就这么简单。别把问题搞复杂了。

注意:ADV_DIRECT_IND有个坑——它只发一次。如果目标设备没收到,这次广播就浪费了。所以重连场景下,我通常会先发几次ADV_IND做“预热”,再切到ADV_DIRECT_IND。这样成功率更高。

小结

这一章我们把广播PDU的结构拆了个底朝天。报头里的PDU类型决定了广播包的“性格”,载荷里的AD Structure决定了广播包的内容。四种类型各有各的用途,选对了,项目就顺了。