第2章 GAP层详解:广播与扫描、连接建立流程、连接参数更新、体温计广播数据包设计
各位同学,欢迎来到第二章。上一章我们聊了蓝牙协议栈的整体架构,今天咱们深入GAP层——说白了,GAP就是蓝牙设备之间怎么“打招呼”、“互相认识”、“建立关系”的那套规矩。做体温计这类低功耗设备,GAP层玩得转不转,直接决定了你的产品能不能稳定工作。
2.1 广播与扫描——设备怎么“刷存在感”
先说说广播。广播是什么?就是你的体温计不停地对外喊:“我在这儿!我是体温计!我能测体温!” 这个过程不需要建立连接,谁都能听到。
我个人习惯把广播分成两种:
- 可连接广播:允许其他设备来连接我。体温计默认就用这种。
- 不可连接广播:只发数据,不接连接。比如一些信标设备。
广播包的结构其实很简单,核心就三部分:
- 前导码:接收端用来同步时钟的,固定8位。
- 访问地址:固定值0x8E89BED6,这是蓝牙规范定的。
- PDU:真正有用的数据,包含广播数据和扫描响应数据。
嗯,这里要注意一个坑:广播间隔。你想想看,如果广播间隔设得太短,比如20ms,设备功耗会飙升。我见过一个项目,工程师为了“响应快”,把广播间隔设成了30ms,结果体温计电池两天就耗光了。后来改成200ms,功耗降了80%,用户体验也没差多少。
再说扫描。扫描就是手机或者其他中心设备在“听广播”。扫描有两种模式:
- 被动扫描:只听,不发任何东西。
- 主动扫描:听到广播后,可以发一个扫描请求,让设备回复更多数据。
我在项目中遇到过一个问题:手机主动扫描时,体温计回复了扫描响应数据,但手机端却解析不出来。查了半天,发现是扫描响应数据里放了太多厂商自定义字段,超过了31字节的限制。记住,广播数据和扫描响应数据各自最多31字节,别超了。
2.2 连接建立流程——从“打招呼”到“牵手”
广播和扫描只是前戏,真正干活还得建立连接。连接建立的流程,说白了就是三步:
- 发起者发送连接请求:手机说“我想连你”。
- 广播者接受连接:体温计说“好的”。
- 双方交换连接参数:约定好通信的节奏。
这里有个关键点:连接请求里包含了连接参数,比如连接间隔、延迟、超时时间。这些参数一旦确定,后续通信就按这个节奏走。
为什么会这样设计?因为蓝牙低功耗(BLE)的核心思想就是“能省则省”。连接间隔越大,设备休眠时间越长,功耗越低。但代价是数据延迟变高。体温计这种场景,数据量小,延迟要求不高,所以连接间隔可以设大一点。
2.3 连接参数更新——动态调整通信节奏
连接建立后,参数还能改吗?当然可以。这就是连接参数更新(Connection Parameter Update)的作用。
什么时候需要更新参数?我总结了几种场景:
- 刚连接时:手机可能用默认参数连上来,但体温计希望用自己的参数。
- 数据传输阶段:如果数据量突然变大,可以临时缩短连接间隔。
- 低功耗模式:数据传完了,把连接间隔拉大,省电。
参数更新的流程是这样的:
- 设备A发送连接参数更新请求(LL_CONNECTION_UPDATE_REQ)。
- 设备B收到后,如果同意,就回复确认。
- 双方在指定的时间点切换到新参数。
嗯,这里有个坑:不是所有手机都支持参数更新。我遇到过一款国产手机,体温计发参数更新请求,手机直接忽略。后来我加了个超时重试机制,如果3秒内没收到回复,就保持原参数不变。
2.4 体温计广播数据包设计——实战案例
好了,理论讲完了,咱们来点实战。体温计的广播数据包怎么设计?
先看广播数据包的结构。一个典型的广播包包含以下AD Structure(AD = Advertising Data):
- Flags:标志位,告诉手机这个设备支持什么模式。
- Local Name:设备名称,比如“Thermometer_01”。
- Service UUID:服务UUID,告诉手机这个设备支持什么服务。
- Manufacturer Specific Data:厂商自定义数据,可以放体温值、电池电量等。
我个人习惯把体温值直接放在广播包里。这样手机不需要连接就能看到温度,用户体验好很多。但要注意,广播包只有31字节,得精打细算。
下面是一个体温计广播数据包的示例:
// 广播数据包示例(十六进制)
// 02 01 06 —— Flags: LE General Discoverable Mode
// 09 09 54 68 65 72 6D 6F 6D 65 74 65 72 —— Local Name: "Thermometer"
// 03 02 0A 18 —— Service UUID: 0x180A (Device Information Service)
// 05 FF 12 34 56 78 —— Manufacturer Data: 厂商ID=0x1234, 温度值=0x5678
uint8_t adv_data[] = {
0x02, 0x01, 0x06, // Flags
0x09, 0x09, 'T', 'h', 'e', 'r', 'm', 'o', 'm', 'e', 't', 'e', 'r', // Name
0x03, 0x02, 0x0A, 0x18, // Service UUID
0x05, 0xFF, 0x34, 0x12, 0x78, 0x56 // Manufacturer Data
};
你看,这个包总共用了2+1+1+13+1+1+2+1+1+2 = 25字节,还剩6字节可以放其他数据。如果温度值需要更高精度,可以用4字节表示浮点数。
我在项目中遇到过一个问题:手机扫描时,广播包里的温度值总是显示错误。后来发现是字节序搞反了。蓝牙协议里多字节数据默认是小端序(Little Endian),但我的代码里用了大端序。记住,温度值要按小端序存放。
最后,给大家一个体温计广播数据包设计的检查清单:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| Flags | 3 | 必选,标识设备模式 |
| Local Name | 可变(≤13) | 建议不超过13字节,留空间给其他数据 |
| Service UUID | 3或5 | 16位UUID用3字节,128位用17字节 |
| Manufacturer Data | 可变(≤26) | 放温度值、电池电量等 |
好了,这一章的内容就到这里。GAP层看似简单,但细节很多。广播间隔、连接参数、数据包设计,每一个点都可能成为你项目中的“坑”。下一章我们聊GATT层,看看数据怎么在连接建立后传输。到时候见!