3、GAP与GATT:蓝牙通信的两大基石

好,咱们今天聊聊蓝牙协议栈里最核心的两个规范——GAP和GATT。说实话,我刚开始接触蓝牙开发时,也被这两个缩写搞得晕头转向。后来在项目里踩过几次坑,才真正理解了它们的分工。

简单来说,GAP负责“怎么找到对方、怎么连上”,GATT负责“连上之后怎么传数据”。一个是连接管理,一个是数据交互。你想想看,这不就跟两个人打电话一样吗?GAP是拨号、接听、挂断这些操作,GATT则是通话内容本身。

3.1 GAP:通用访问规范

GAP(Generic Access Profile)定义了蓝牙设备如何发现彼此、如何建立连接、以及设备在连接过程中的角色。我个人习惯把GAP理解成蓝牙设备的“社交礼仪”。

3.1.1 GAP的角色

在GAP规范里,设备分为两种角色:

  • Central(中心设备):主动发起扫描和连接的设备。比如你的手机。
  • Peripheral(外围设备):广播自身信息,等待被连接的设备。比如一个蓝牙手环。

嗯,这里要注意:一个设备可以同时扮演两种角色,但实际项目中很少这么干。我在做智能门锁项目时,就遇到过手机既要当Central连接门锁,又要当Peripheral广播自己的状态——结果功耗和连接稳定性都出了问题。

3.1.2 GAP的工作模式

GAP定义了四种工作模式,说白了就是设备在不同阶段的状态:

模式 说明 典型场景
广播模式 设备周期性发送广播包 蓝牙手环等待连接
扫描模式 设备监听周围的广播包 手机搜索附近设备
发起连接模式 主动向某个设备发起连接 手机点击“配对”按钮
连接模式 已建立连接,可以收发数据 数据传输中

我曾经在一个项目中,外围设备广播间隔设得太短(20ms),结果功耗飙升,电池两天就耗光了。后来我把广播间隔调到100ms,功耗降了60%,连接成功率也没受影响。所以广播间隔这个参数,一定要根据实际场景去调。

我的经验:广播数据包最多31字节,别想着塞太多东西。我一般只放设备名称、UUID和必要的标志位,剩下的数据等连接后再通过GATT传。

3.2 GATT:通用属性规范

GATT(Generic Attribute Profile)建立在GAP建立的连接之上,定义了数据如何组织和传输。说白了,GATT就是蓝牙设备之间的“数据字典”。

3.2.1 Client/Server模型

GATT采用经典的Client/Server模型:

  • GATT Server:存储数据的一方,提供属性(Attributes)。通常是传感器设备。
  • GATT Client:读取或写入数据的一方。通常是手机App。

你想想看,一个温度传感器作为GATT Server,它暴露了温度值这个属性。手机作为GATT Client,发一个读请求,Server就把温度值返回。就这么简单。

但实际项目中,角色可以互换。我记得做一个智能灯项目时,灯本身是Server,但灯也需要读取手机上的时间配置——这时候灯就临时充当了Client。嗯,GATT没有规定谁必须是Server,谁必须是Client,完全看业务需求。

3.2.2 Service与Characteristic

这是GATT里最重要的两个概念,也是初学者最容易搞混的地方。

Service(服务):一组相关功能的集合。每个Service有一个唯一的UUID标识。

Characteristic(特征):Service下的具体数据点。每个Characteristic也有自己的UUID,并且包含一个值(Value)和若干属性(Properties)。

举个例子,一个心率监测设备:

  • Service:心率服务(UUID: 0x180D)
  • Characteristic 1:心率测量值(UUID: 0x2A37),属性:Notify
  • Characteristic 2:心率传感器位置(UUID: 0x2A38),属性:Read

你看,一个Service下可以有多个Characteristic,每个Characteristic负责一个具体的数据点。

核心要点:Characteristic的属性决定了Client能对它做什么。常见的属性有:
  • Read:Client可以读取这个值
  • Write:Client可以写入这个值
  • Notify:Server主动通知Client值发生了变化
  • Indicate:类似Notify,但需要Client确认收到

3.2.3 实际开发中的Service设计

我在做蓝牙温湿度计项目时,设计了这样一个Service结构:

// 温湿度服务 UUID: 自定义 0xAAAA
//   - 温度特征 UUID: 0xAAA1, 属性: Read + Notify
//   - 湿度特征 UUID: 0xAAA2, 属性: Read + Notify
//   - 配置特征 UUID: 0xAAA3, 属性: Write
//     (用于设置上报间隔、单位切换等)

为什么温度用Notify?因为温度是实时变化的,我希望手机能第一时间收到更新。而配置特征用Write就够了,手机设置完参数后不需要频繁读取。

避坑指南:我曾经在一个项目中,把所有Characteristic都设成了Read + Write + Notify。结果手机端频繁读取,导致Server端CPU占用率飙升,设备发热严重。后来我仔细分析了每个特征的实际用途,只给必要的特征开了Notify,其他都用Read。功耗降了40%。

3.3 GAP与GATT的关系

最后总结一下两者的关系:

  • GAP:负责设备发现、连接建立、连接参数更新。没有GAP,两个设备根本见不着面。
  • GATT:负责连接建立后的数据交互。没有GATT,连上了也不知道传什么。

你想想看,GAP就像是你去参加一个聚会,先找到人、打招呼、握手。GATT则是握手之后你们开始聊天,聊什么内容、怎么聊,都由GATT来定义。

在实际开发中,我建议你先确定GAP的角色和参数(广播间隔、连接间隔等),再设计GATT的Service和Characteristic结构。顺序搞反了,后面调试起来会很痛苦。嗯,我就吃过这个亏。

我的建议:刚开始做蓝牙开发时,先用手机App(比如nRF Connect或LightBlue)去扫描和连接你的设备,看看广播包对不对、Service结构是否合理。这比直接写代码调试快得多。