GATT 协议基础:GAP 与 GATT 的关系、角色与层级

聊蓝牙开发,GATT 是绕不开的核心。很多人一上来就啃协议栈,结果被各种术语搞晕。我个人习惯先把 GATT 和 GAP 的关系理清楚,再往下看角色和层级,这样思路会顺很多。

GAP 与 GATT:一个管连接,一个管数据

GAP(Generic Access Profile)和 GATT(Generic Attribute Profile)是蓝牙协议栈里两个最基础的概念。简单说:GAP 负责设备怎么被发现、怎么建立连接;GATT 负责连接建立之后,数据怎么交换。

我在项目中遇到过不少新手,把这两个混为一谈。调试时发现设备连上了但读不到数据,就怀疑是 GATT 的问题。其实很多时候是 GAP 层的广播参数没配好,导致连接不稳定。嗯,这里要注意:GAP 是前提,GATT 是后续。

维度 GAP GATT
职责 设备发现、连接建立、安全配对 连接后的数据读写、通知、指示
角色 Broadcaster / Observer / Peripheral / Central Server / Client
数据模型 广播数据包(ADV) Attribute 表(Service / Characteristic)
典型场景 手机扫描到蓝牙手环 手机读取手环的心率值
我的习惯:调试时先确认 GAP 层广播正常、连接稳定,再抓 GATT 层的交互日志。这样能快速定位问题出在哪一层。

Server / Client 角色:谁提供数据,谁消费数据

GATT 定义了两种角色:Server(服务端)和Client(客户端)。Server 持有数据,Client 发起读写请求。说白了,Server 是数据提供方,Client 是数据消费方。

举个例子:你做一个蓝牙温湿度计。温湿度计本身是 GATT Server,它把温度和湿度数据放在特定的 Characteristic 里。手机 App 是 GATT Client,它主动去读这些数据。反过来,如果手机想设置温湿度计的采样间隔,手机作为 Client 写数据到 Server 的某个 Characteristic 里。

我曾经踩过一个坑:把 Server 和 Client 的角色跟 GAP 的 Peripheral / Central 搞混了。Peripheral 通常是 Server,Central 通常是 Client,但这不是绝对的。有些设备既是 Peripheral 又是 Server,这很常见。但理论上,一个设备可以同时是 Server 和 Client,比如手机同时连接多个外设时。

关键点:角色是逻辑上的,不是物理上的。同一个设备可以同时扮演 Server 和 Client,只要它实现了对应的 GATT 功能。

Profile / Service / Characteristic 层级

这是 GATT 最核心的数据模型。我习惯把它比作一个文件系统:

  • Profile —— 相当于一个“应用规范”,它定义了一组 Service 的集合。比如心率 Profile(HRS)包含心率 Service 和设备信息服务。
  • Service —— 相当于一个“文件夹”,把相关的 Characteristic 归类在一起。每个 Service 有唯一的 UUID。
  • Characteristic —— 相当于一个“文件”,里面存放具体的数据值。Characteristic 也有 UUID,并且有属性(读、写、通知等)。

你想想看,蓝牙协议栈为什么要搞这么复杂?直接传数据不就行了?其实是为了标准化。不同厂商的设备只要遵循同一个 Profile,手机 App 就能通用。比如所有心率计都使用标准的心率 Service,App 不需要为每个品牌写不同的代码。

注意:Characteristic 本身不直接存数据,它通过 Value 属性存数据。但 Characteristic 还包含 Descriptor(描述符),用来描述 Value 的配置信息,比如通知开关、客户端特征配置(CCCD)等。

层级关系图

下面这张图展示了 GATT 的完整层级结构。我建议你把它存下来,开发时对照着看。

GATT 层级结构图 Profile(应用规范) Service(服务) Characteristic(特征) Value(值) Properties(属性) Descriptors(描述符) 说明 • Profile 是 Service 的集合,由蓝牙 SIG 或厂商定义 • Service 是 Characteristic 的容器,每个 Service 有唯一 UUID • Characteristic 包含 Value、Properties、Descriptors 三部分

实际开发中的层级操作

在代码层面,你通常需要做这几件事:

  1. 注册 Service —— 调用 API 创建一个 Service 实例,指定 UUID。
  2. 添加 Characteristic —— 在 Service 下创建 Characteristic,设置 UUID、属性(读/写/通知)和初始值。
  3. 配置 Descriptor —— 尤其是 CCCD(Client Characteristic Configuration Descriptor),它控制通知和指示的开关。
  4. 处理读写请求 —— 实现回调函数,响应 Client 的读写操作。

举个例子,创建一个简单的心率 Service:

// 伪代码示例
service = gatt_server_create_service(HEART_RATE_SERVICE_UUID);

// 添加心率测量 Characteristic
char_hr = gatt_server_add_characteristic(
    service,
    HEART_RATE_MEASUREMENT_UUID,
    GATT_PROP_NOTIFY,          // 属性:支持通知
    GATT_PERM_READ             // 权限:可读
);

// 添加 CCCD Descriptor
gatt_server_add_descriptor(
    char_hr,
    CLIENT_CHARACTERISTIC_CONFIG_UUID,
    GATT_PERM_READ | GATT_PERM_WRITE
);

// 启动服务
gatt_server_start_service(service);
避坑指南:我曾经忘记添加 CCCD Descriptor,结果手机 App 订阅通知时一直失败。后来查了规范才发现,只要 Characteristic 支持通知或指示,就必须包含 CCCD。这是蓝牙规范强制要求的。

标准 Service 与自定义 Service

蓝牙 SIG 定义了很多标准 Service,比如:

  • 心率 Service(0x180D) —— 用于传输心率数据
  • 电池 Service(0x180F) —— 用于报告电池电量
  • 设备信息服务(0x180A) —— 包含设备名称、序列号等

如果你的产品不属于这些标准类别,可以定义自定义 Service。自定义 Service 使用 128 位 UUID,格式如 0000xxxx-0000-1000-8000-00805F9B34FB,其中 xxxx 是自定义的 16 位值。

我个人建议:能用标准 Service 就用标准的。这样你的设备可以被更多通用 App 识别,兼容性更好。只有标准 Service 无法满足需求时,才考虑自定义。

总结一下:GATT 是蓝牙数据交换的核心协议。理解 GAP 和 GATT 的分工、Server/Client 的角色、以及 Profile/Service/Characteristic 的层级关系,是开发蓝牙应用的基础。把这些概念吃透,后面写代码会顺畅很多。