4. BLE协议栈架构与空口包结构
做低功耗蓝牙开发,我见过不少工程师一上来就调应用层代码,遇到连不上、断连、丢包就抓瞎。说白了,你不懂协议栈怎么分层、空口包长什么样,抓包数据摆在你面前也看不懂。这一章,我们就从底层往上捋一遍。
4.1 BLE协议栈分层
BLE协议栈分三层:Controller、Host、Application。Controller跑在MCU的射频内核里,Host跑在主核或OS上,Application就是你的业务代码。我习惯把Controller比作「邮局」,Host比作「信件分拣中心」,Application就是「写信的人」。
| 层级 | 核心子层 | 我的理解 |
|---|---|---|
| Controller | PHY、LL | 负责发比特、收比特、状态机、重传 |
| Host | L2CAP、ATT、GATT、SM | 负责拆包、组包、属性读写、安全配对 |
| Application | Profiles、Services | 你的业务逻辑,比如心率、温湿度 |
这里重点说几个我经常在抓包时关注的子层。
4.1.1 LL(Link Layer)
LL是BLE的「心脏」。它管理五个状态:Standby、Advertising、Scanning、Initiating、Connection。我在项目中遇到过最典型的坑:设备在Advertising状态待太久,功耗下不来。后来发现是LL的Advertising间隔设得太短,导致射频一直开着。你想想看,间隔20ms和100ms,平均电流差好几倍。
4.1.2 L2CAP(Logical Link Control and Adaptation Protocol)
L2CAP干的事很简单:把上层的大包拆成LL能发的小包,或者把LL收上来的小包拼回去。它有一个关键参数——MTU。默认是23字节,但很多芯片支持协商到247甚至更大。我建议你在抓包时留意L2CAP层的Length字段,如果发现数据被频繁分段,说明MTU太小,可以尝试协商大MTU来提升吞吐。
4.1.3 ATT(Attribute Protocol)与GATT(Generic Attribute Profile)
ATT定义了「属性」的读写规则,GATT定义了这些属性怎么组织成Service和Characteristic。说白了,ATT是「怎么读怎么写」,GATT是「读什么写什么」。抓包时,你看到的Read Request、Write Command、Notification,都是ATT层的PDU。
4.2 空口包格式
BLE空口包分四种:Advertising包、Data包、LL Control包、Periodic包。但不管哪种,基本结构都一样。我画了一张图,你一看就明白。
4.2.1 Preamble
Preamble就1个字节,广播包是0xAA,数据包是0x55。它的作用就是让接收端的射频电路锁定频率和相位。嗯,这里要注意:如果你抓包时发现Preamble不对,大概率是抓包工具离设备太远,信号太弱。
4.2.2 Access Address
Access Address(AA)是BLE包的「身份证」。广播包的AA是固定的——0x8E89BED6。数据包的AA在连接建立时由LL随机生成。我曾经在调试一个多连接场景时,发现两个连接的AA竟然一样,导致数据串扰。后来查了芯片手册,发现是随机数种子初始化没做好。从那以后,我每次抓包都会先确认AA是否唯一。
4.2.3 PDU
PDU是包的核心,里面装着真正的数据。广播PDU和Data PDU的格式不一样,我列个表对比一下。
| PDU类型 | Header长度 | Payload最大长度 | 典型用途 |
|---|---|---|---|
| 广播PDU | 2字节 | 37字节 | 广播、扫描、发起连接 |
| 数据PDU | 2字节 | 255字节 | 连接后的数据收发 |
| LL Control PDU | 2字节 | 可变 | 连接参数更新、加密、调频 |
4.2.4 CRC
CRC是3字节的校验码,用多项式0x100065B计算。接收端收到包后会重新算一遍CRC,如果对不上就丢弃。我习惯在抓包时看CRC状态:如果CRC错误率超过5%,说明射频环境很差,需要检查天线匹配或降低发射功率。
4.3 抓包视角下的连接建立过程
连接建立是BLE最核心的流程,也是抓包调试的高频场景。我拆成三步来讲。
4.3.1 第一步:广播与扫描
外设(Peripheral)发ADV_IND包,里面包含设备地址和广播数据。主机(Central)发SCAN_REQ,外设回SCAN_RSP。这一步我经常用来排查「设备搜不到」的问题。如果抓包看不到ADV_IND,说明外设没在广播;如果看到ADV_IND但没看到SCAN_REQ,说明主机没在扫描。
4.3.2 第二步:发起连接
主机发CONNECT_IND包,里面包含连接参数:Connection Interval、Slave Latency、Supervision Timeout。外设收到后回复一个空包,连接就算建立了。我建议你在抓包时重点关注CONNECT_IND里的Interval字段,它直接决定了功耗和延迟。
4.3.3 第三步:数据交互
连接建立后,双方按Connection Interval定时交换数据。每个Interval里,主机先发,外设再回。如果外设没数据,就回一个空包。我在项目中遇到过一个问题:外设偶尔不回复空包,导致主机认为连接断了。后来发现是外设的LL状态机在某个中断里卡住了,没来得及发空包。
抓包小技巧: 用抓包工具看连接建立过程时,我习惯先过滤出CONNECT_IND包,然后看它前后的广播包。如果CONNECT_IND之后没有数据包,说明连接没建立成功。这时候检查CONNECT_IND里的Access Address和CRC,大概率能找到原因。
好了,这一章的内容就这些。协议栈分层和空口包结构是抓包调试的基础,你把这些搞清楚了,后面分析具体问题就会轻松很多。