2. GATT 核心概念精讲
好,咱们今天来聊聊 GATT Profile 的结构。说白了,这就是 BLE 设备之间怎么“说话”的规矩。你设计一个蓝牙设备,不管是手环、体脂秤还是传感器,都得按照这个规矩来。我个人习惯把 GATT Profile 想象成一棵倒着长的树——根在最上面,叶子在最下面。
这棵树的顶层是 Profile,它定义了设备的完整功能。往下是 Service(服务),再往下是 Characteristic(特征)。每个 Characteristic 还有自己的 Value 和 Descriptor(描述符)。今天咱们重点啃 Service 和 Characteristic 这两层。
核心要点:GATT Profile 的层次结构是:Profile → Service → Characteristic → Value + Descriptor。每一层都有严格的 UUID 定义和属性规则。
2.1 Service 的 UUID 定义
每个 Service 都有一个唯一的标识符,就是 UUID。你想想看,蓝牙 SIG(Special Interest Group)定义了一堆标准 Service,比如电池服务(0x180F)、设备信息服务(0x180A)、心率服务(0x180D)。这些标准 Service 用的是 16 位 UUID,省空间、效率高。
但如果你做的是自家产品,比如一个智能咖啡机,那就得用 128 位 UUID。格式长这样:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX。我建议你直接用在线 UUID 生成器搞一个,别自己瞎编,万一跟别人的冲突了,那调试起来可够你喝一壶的。
| UUID 类型 | 长度 | 用途 | 示例 |
|---|---|---|---|
| 16 位 UUID | 2 字节 | 蓝牙 SIG 标准 Service | 0x180F(电池服务) |
| 128 位 UUID | 16 字节 | 自定义 Service | 0000xxxx-0000-1000-8000-00805F9B34FB |
我的小技巧:开发阶段先用标准 UUID 做原型验证,等产品定型了再换成自定义 128 位 UUID。这样前期用 nRF Connect 调试时,工具能自动识别服务名,省得你老去翻协议栈文档。
2.2 Primary Service 与 Secondary Service
Service 分两种:Primary Service(主要服务)和 Secondary Service(次要服务)。
Primary Service 是设备对外暴露的核心功能。比如一个心率带,它的 Primary Service 就是心率服务(0x180D)。每个设备至少得有一个 Primary Service,不然手机端根本发现不了你。
Secondary Service 呢?它是被 Primary Service 引用的辅助服务。说白了,它不能独立存在,必须挂在某个 Primary Service 下面。我在项目中遇到过一种情况:一个体脂秤,它的 Primary Service 是体脂测量服务,但里面引用了电池服务(Secondary Service)来上报电量。这样设计的好处是——电池服务可以被多个 Primary Service 复用。
注意:Secondary Service 不能单独被客户端发现。你必须在 Primary Service 的 Included Service 列表里显式引用它,手机端才能看到。我曾经有个同事忘了加这个引用,结果设备死活连不上,查了两天才发现是这个问题。
2.3 Included Service
Included Service 是 Service 之间的一种引用关系。一个 Service 可以把另一个 Service “包含”进来,形成层级结构。
举个例子:你的智能手表有一个“运动监测服务”(Primary Service),它里面包含了“心率服务”和“计步服务”作为 Included Service。这样客户端在访问运动监测服务时,就能顺藤摸瓜找到心率数据和步数数据。
Included Service 在 GATT 数据库里是用一个特殊的 Attribute 来表示的。它的结构长这样:
Attribute Handle: 0x0020
Type: 0x2802 (GATT Include Declaration)
Value: [Included Service UUID] + [起始句柄] + [结束句柄]
你想想看,这种设计有什么好处?模块化!你可以把通用的 Service(比如设备信息、电池)封装好,然后在不同的 Primary Service 里引用。代码复用率一下子就上去了。
2.4 Characteristic 的声明与属性
Characteristic 是 GATT 里真正干活的东西。Service 只是个容器,Characteristic 才是装数据的抽屉。
每个 Characteristic 由三部分组成:
- Characteristic 声明:一个特殊的 Attribute,类型是 0x2803。它里面包含了 Characteristic 的属性、值句柄和 UUID。
- Characteristic 值:实际存储数据的 Attribute。
- Characteristic 描述符:可选的附加信息,比如客户端特征配置描述符(CCCD)、用户描述等。
Characteristic 声明(0x2803)的 Value 格式如下:
+--------+--------+--------+--------+--------+--------+
| 属性 | 值句柄 | UUID (16位或128位) |
+--------+--------+--------+--------+--------+--------+
| 1 字节 | 2 字节 | 2 或 16 字节 |
+--------+--------+--------+--------+--------+--------+
这里的“属性”字段就是 Characteristic 的能力标识。它是个位掩码,每一位代表一种能力:
| 位 | 属性名 | 说明 |
|---|---|---|
| 0 | Broadcast | 特征值可被广播 |
| 1 | Read | 客户端可读取特征值 |
| 2 | Write Without Response | 客户端可无应答写入 |
| 3 | Write | 客户端可有应答写入 |
| 4 | Notify | 服务端可主动通知客户端 |
| 5 | Indicate | 服务端可主动指示(需确认) |
| 6 | Authenticated Signed Writes | 带签名的写入 |
| 7 | Extended Properties | 扩展属性标志 |
实战经验:Notify 和 Indicate 的区别你一定要搞清楚。Notify 只管发,不管对方收没收到;Indicate 会等对方确认。我做过一个血糖仪项目,数据必须可靠传输,所以用了 Indicate。但 Indicate 的吞吐量比 Notify 低,因为每次都要等确认。如果你做的是传感器数据流,用 Notify 就够了。
2.5 实战:定义一个简单的 Service
光说不练假把式。咱们用 Nordic nRF5 SDK 的代码风格,定义一个“环境传感器服务”。
// 自定义 128 位 UUID
#define BLE_UUID_ENV_SENSOR_SERVICE 0x0001
#define BLE_UUID_ENV_SENSOR_TEMP 0x0002
#define BLE_UUID_ENV_SENSOR_HUMID 0x0003
// 服务初始化
static uint32_t env_sensor_service_init(void)
{
uint32_t err_code;
ble_uuid_t ble_uuid;
// 注册自定义 UUID 类型
ble_uuid128_t base_uuid = {
.uuid128 = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
};
err_code = sd_ble_uuid_vs_add(&base_uuid, &ble_uuid.type);
APP_ERROR_CHECK(err_code);
// 设置 Service UUID
ble_uuid.uuid = BLE_UUID_ENV_SENSOR_SERVICE;
// 添加 Primary Service
err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
&ble_uuid,
&m_env_sensor.service_handle);
APP_ERROR_CHECK(err_code);
// 添加温度 Characteristic(支持 Read + Notify)
// ... 此处省略 Characteristic 添加代码
return NRF_SUCCESS;
}
这段代码做了三件事:注册自定义 UUID 类型、添加 Primary Service、预留 Characteristic 添加位置。你实际开发时,记得把 base_uuid 换成你自己生成的 128 位 UUID。
避坑指南:我曾经在量产阶段发现两个产品线用了相同的 base UUID,只是 16 位 UUID 不同。结果手机端把两个设备当成同一个服务,数据全乱了。从那以后,我要求每个产品线必须用独立的 128 位 base UUID,哪怕只是内部测试用。
2.6 小结
今天咱们把 GATT Profile 的骨架搭起来了。Service 是容器,Characteristic 是数据抽屉。Primary Service 是门面,Secondary Service 是幕后帮手。Included Service 让服务之间可以互相引用,实现模块化设计。