第2章 GAP层详解:广播与扫描、连接建立流程、连接参数更新、体温计广播数据包设计

各位同学,欢迎来到第二章。上一章我们聊了蓝牙协议栈的整体架构,今天咱们深入GAP层——说白了,GAP就是蓝牙设备之间怎么“打招呼”、“互相认识”、“建立关系”的那套规矩。做体温计这类低功耗设备,GAP层玩得转不转,直接决定了你的产品能不能稳定工作。

2.1 广播与扫描——设备怎么“刷存在感”

先说说广播。广播是什么?就是你的体温计不停地对外喊:“我在这儿!我是体温计!我能测体温!” 这个过程不需要建立连接,谁都能听到。

我个人习惯把广播分成两种:

  • 可连接广播:允许其他设备来连接我。体温计默认就用这种。
  • 不可连接广播:只发数据,不接连接。比如一些信标设备。

广播包的结构其实很简单,核心就三部分:

  • 前导码:接收端用来同步时钟的,固定8位。
  • 访问地址:固定值0x8E89BED6,这是蓝牙规范定的。
  • PDU:真正有用的数据,包含广播数据和扫描响应数据。

嗯,这里要注意一个坑:广播间隔。你想想看,如果广播间隔设得太短,比如20ms,设备功耗会飙升。我见过一个项目,工程师为了“响应快”,把广播间隔设成了30ms,结果体温计电池两天就耗光了。后来改成200ms,功耗降了80%,用户体验也没差多少。

我的建议:体温计这类设备,广播间隔设在100ms~300ms之间比较合理。既要保证手机能快速发现,又不能太费电。

再说扫描。扫描就是手机或者其他中心设备在“听广播”。扫描有两种模式:

  • 被动扫描:只听,不发任何东西。
  • 主动扫描:听到广播后,可以发一个扫描请求,让设备回复更多数据。

我在项目中遇到过一个问题:手机主动扫描时,体温计回复了扫描响应数据,但手机端却解析不出来。查了半天,发现是扫描响应数据里放了太多厂商自定义字段,超过了31字节的限制。记住,广播数据和扫描响应数据各自最多31字节,别超了。

2.2 连接建立流程——从“打招呼”到“牵手”

广播和扫描只是前戏,真正干活还得建立连接。连接建立的流程,说白了就是三步:

  1. 发起者发送连接请求:手机说“我想连你”。
  2. 广播者接受连接:体温计说“好的”。
  3. 双方交换连接参数:约定好通信的节奏。

这里有个关键点:连接请求里包含了连接参数,比如连接间隔、延迟、超时时间。这些参数一旦确定,后续通信就按这个节奏走。

为什么会这样设计?因为蓝牙低功耗(BLE)的核心思想就是“能省则省”。连接间隔越大,设备休眠时间越长,功耗越低。但代价是数据延迟变高。体温计这种场景,数据量小,延迟要求不高,所以连接间隔可以设大一点。

避坑指南:我曾经在一个项目中,把连接间隔设成了7.5ms(最小值),结果体温计和手机通信时频繁断连。后来发现是手机蓝牙芯片处理不过来。记住,不是所有手机都能支持极短的连接间隔。保守一点,设到30ms以上比较稳妥。

2.3 连接参数更新——动态调整通信节奏

连接建立后,参数还能改吗?当然可以。这就是连接参数更新(Connection Parameter Update)的作用。

什么时候需要更新参数?我总结了几种场景:

  • 刚连接时:手机可能用默认参数连上来,但体温计希望用自己的参数。
  • 数据传输阶段:如果数据量突然变大,可以临时缩短连接间隔。
  • 低功耗模式:数据传完了,把连接间隔拉大,省电。

参数更新的流程是这样的:

  1. 设备A发送连接参数更新请求(LL_CONNECTION_UPDATE_REQ)。
  2. 设备B收到后,如果同意,就回复确认。
  3. 双方在指定的时间点切换到新参数。

嗯,这里有个坑:不是所有手机都支持参数更新。我遇到过一款国产手机,体温计发参数更新请求,手机直接忽略。后来我加了个超时重试机制,如果3秒内没收到回复,就保持原参数不变。

注意:参数更新请求不能发得太频繁。蓝牙规范建议两次请求之间至少间隔20秒。我曾经见过一个工程师每5秒发一次请求,结果手机蓝牙协议栈直接崩溃了。

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层,看看数据怎么在连接建立后传输。到时候见!