2、蓝牙核心规范解读:蓝牙5.1/5.2/5.3/5.4协议栈架构、HCI层详解、LL层详解、GAP层详解、GATT层详解
各位同学,咱们今天聊聊蓝牙核心规范。说实话,很多开发者一上来就调API,对协议栈底层一知半解。我个人觉得,搞定位和测向,不懂协议栈架构,就像开车不看仪表盘——能跑,但迟早要出问题。
蓝牙从5.1到5.4,每一版都在演进。但核心的协议栈骨架没变。咱们一层层剥开来看。
2.1 蓝牙协议栈整体架构
蓝牙协议栈,说白了就是一套分层软件。从下往上分别是:
- Controller(控制器):负责射频、基带、链路管理。这是硬件和底层固件的事。
- Host(主机):跑在应用处理器上,负责逻辑链路控制、属性协议、安全管理等。
- HCI(主机控制器接口):连接Host和Controller的桥梁。
我刚开始做蓝牙项目时,总搞不清Host和Controller的分界线。后来踩了个坑——在Controller里塞了太多逻辑,结果升级固件特别麻烦。嗯,这里要注意:Host和Controller的分离,是为了让蓝牙模块可以独立升级,互不影响。
核心要点:蓝牙5.1引入AoA/AoD测向,5.2新增LE Audio,5.3优化了连接更新和信道分类,5.4则带来了带响应的周期性广播(PAwR)和加密广播数据。但底层协议栈架构基本一致。
2.2 HCI层详解——Host和Controller的“翻译官”
HCI层,全称Host Controller Interface。它定义了Host和Controller之间怎么通信。你想想看,Host发一个“扫描设备”的命令,Controller收到后开始扫描,然后通过HCI事件把结果返回给Host。
HCI支持三种传输方式:
- UART:最常见,成本低,但速率有限。
- USB:高速场景用,比如蓝牙dongle。
- SDIO/SPI:嵌入式设备里常见。
我记得有一次调试定位精度,发现测向数据总是丢包。查了半天,原来是HCI UART的波特率设得太低,数据来不及传。换成921600后,问题解决了。
避坑指南:我曾经在HCI层踩过一个坑——HCI命令和事件的超时处理。如果Controller没及时响应,Host会重发命令,导致命令队列混乱。建议在HCI驱动里加一个命令完成事件队列,按序处理。
HCI命令格式很简单:OpCode(操作码)+ 参数长度 + 参数。比如发送LE Set Extended Scan Parameters命令,OpCode是0x0041,参数包括扫描类型、扫描间隔、扫描窗口等。
// HCI命令示例:设置扩展扫描参数
// OpCode: 0x0041 (LE Set Extended Scan Parameters)
// 参数: 扫描类型(0x01=主动扫描), 扫描间隔(0x0060), 扫描窗口(0x0030)
uint8_t hci_cmd[] = {
0x01, // 扫描类型: 主动扫描
0x60, 0x00, // 扫描间隔: 96 * 0.625ms = 60ms
0x30, 0x00, // 扫描窗口: 48 * 0.625ms = 30ms
0x00, // 自身地址类型: 公共地址
0x00 // 扫描策略: 接受所有广播
};
2.3 LL层详解——链路层的“硬核”操作
LL层,Link Layer,是蓝牙最底层的逻辑控制。它负责设备发现、连接建立、数据包收发、加密等。说白了,LL层就是蓝牙的“交通警察”,指挥数据包怎么走、什么时候走。
LL层有几个关键概念:
- 状态机:Standby、Advertising、Scanning、Initiating、Connection。设备在不同状态间切换。
- 数据包格式:Preamble(前导码)+ Access Address(接入地址)+ PDU(协议数据单元)+ CRC(循环冗余校验)。
- 连接事件:主从设备在约定的时间窗口内交换数据。
做定位开发时,LL层的连接参数特别重要。比如连接间隔、从机延迟、监督超时。我见过一个项目,连接间隔设成7.5ms,结果功耗爆炸。后来改成30ms,定位精度没降多少,续航翻了一倍。
注意:LL层的连接参数直接影响定位延迟和功耗。连接间隔越小,定位数据更新越快,但功耗越高。建议根据应用场景平衡:室内定位用20-50ms,室外追踪用100-200ms。
蓝牙5.1在LL层引入了CTE(Constant Tone Extension,恒定音调扩展)。这是测向的核心。CTE是一段连续的未调制载波,接收端通过IQ采样来估算信号到达角度。
// CTE参数配置示例
// 在LL层,CTE通过HCI命令 LE Set Connection CTE Transmit Parameters 配置
// 参数包括:CTE类型(AoA/AoD)、CTE长度(8-160us)、CTE间隔
typedef struct {
uint8_t cte_type; // 0x00=AoA, 0x01=AoD_1us, 0x02=AoD_2us
uint8_t cte_length; // CTE长度,单位8us,范围1-20
uint8_t cte_count; // 每个连接事件的CTE数量
uint8_t switching_pattern_length; // 天线切换模式长度
uint8_t switching_pattern[0x3F]; // 天线切换序列
} le_cte_params_t;
2.4 GAP层详解——设备如何“打招呼”
GAP层,Generic Access Profile,定义了设备如何发现彼此、如何建立连接。你想想看,手机打开蓝牙,扫描周围设备,这就是GAP层在干活。
GAP层定义了四种角色:
- Broadcaster(广播者):只发广播,不连接。比如信标。
- Observer(观察者):只收广播,不连接。比如扫描仪。
- Peripheral(外设):可被连接,通常是小设备。比如手环。
- Central(中心):主动连接外设。比如手机。
做定位系统时,我习惯把信标设成Broadcaster,手机或网关设成Observer。这样功耗最低,而且可以同时接收多个信标的信号。
GAP层还定义了广播数据格式。广播包最多31字节,包含多个AD Structure(广告数据结构)。每个AD Structure由长度、类型、数据组成。
// 广播数据示例:包含完整本地名称和Tx Power Level
// AD Structure 1: 完整本地名称 "BLE_BEACON"
0x0B, // 长度: 11字节
0x09, // 类型: Complete Local Name
'B','L','E','_','B','E','A','C','O','N',
// AD Structure 2: Tx Power Level
0x02, // 长度: 2字节
0x0A, // 类型: Tx Power Level
0xC5, // Tx Power: -59 dBm (补码表示)
个人经验:广播数据里一定要包含Tx Power Level。做RSSI测距时,没有这个值,你根本算不出距离。我曾经接手一个项目,信标没发Tx Power,结果定位误差大得离谱。后来加上后,精度从10米提升到3米。
2.5 GATT层详解——数据怎么“聊”
GATT层,Generic Attribute Profile,定义了连接建立后,设备之间怎么交换数据。GATT基于属性协议(ATT),把数据组织成Service(服务)和Characteristic(特征值)。
GATT的角色:
- GATT Server:提供数据的一方。比如传感器设备。
- GATT Client:读取/写入数据的一方。比如手机App。
做定位开发时,我通常把测向数据(IQ样本、角度估算结果)放在一个自定义Service里。客户端通过Read或Notify来获取数据。
GATT的通信方式:
- Read:客户端主动读取。
- Write:客户端写入数据。
- Notify/Indicate:服务器主动推送数据。Notify不需要确认,速度快;Indicate需要确认,更可靠。
我记得有个项目,定位数据需要实时更新。用Read方式,客户端轮询,功耗高、延迟大。后来改成Notify,服务器每100ms推送一次,延迟降到50ms以内,功耗也降了30%。
关键点:GATT层的MTU(最大传输单元)大小影响数据吞吐量。默认是23字节,但可以通过MTU Exchange协商到更大值。做测向时,IQ样本数据量大,建议把MTU设到247字节,这样一次Notify就能传更多数据。
GATT的Service和Characteristic用UUID标识。标准Service有16位UUID,自定义Service用128位UUID。比如测向Service的UUID可以这样定义:
// 自定义测向Service UUID
#define UUID_DIRECTION_FINDING_SERVICE 0x1800 // 示例,实际用128位
#define UUID_CHAR_IQ_SAMPLE 0x2A37 // 示例,实际用128位
#define UUID_CHAR_ANGLE_RESULT 0x2A38 // 示例,实际用128位
// Characteristic属性
#define PROPERTY_NOTIFY 0x10
#define PROPERTY_READ 0x02
#define PROPERTY_WRITE 0x08
好了,这一章的内容就到这。协议栈的每一层都有自己的职责,搞清楚了,后面做定位开发才能得心应手。下一章咱们聊聊蓝牙5.1测向原理,也就是AoA和AoD到底是怎么算出来的。