第4章:服务与特征发现——GATT数据库结构与发现流程

BLE 连接建立之后,Central 必须通过发现 GATT 数据库来了解 Peripheral 的能力。本章深入 GATT 层次模型,解析 Primary Service / Characteristic 的发现机制,并封装可复用的客户端发现代码。

GATT 数据库层次结构 & 发现流程 ATT 协议层 GATT 服务聚合(Service) Primary Service A Primary Service B Char 1 Char 2 Char 3 Char 4 Char 5 Descriptor (CCCD) 发现方向:Primary → Char → Desc

4.1 GATT 数据库结构

GATT 将属性(Attribute)组织为三层层次:Service → Characteristic → Descriptor。每个 Service 由一个 16-bit 或 128-bit UUID 标识,且分为 Primary Service(主要服务)和 Secondary Service(次要服务)。Characteristic 包含值、属性(Properties)和可选的描述符。

层级用途UUID 示例发现方式
Primary Service声明设备的主要功能模块(如电池、设备信息)0x180F (Battery Service)GATT Primary Service Discovery
Characteristic包含具体数据的可读/可写/可通知属性0x2A19 (Battery Level)Discover All Characteristics of a Service
Descriptor描述 Characteristic 的配置或说明(如 CCCD)0x2902 (CCCD)Discover All Descriptors of a Characteristic

核心规则:Central 必须逐层发现:先枚举 Primary Service,然后对每个 Service 枚举其 Characteristic,最后可选枚举 Characteristic 的 Descriptor。跳过某个层级将导致无法正确访问属性。

4.2 Primary Service 发现流程

BLE 规范定义了两种发现服务的请求:Read By Group Type 和 Find By Type Value。最常用的是 Read By Group Type 请求,UUID 为 0x2800(Primary Service)或 0x2801(Secondary Service)。

  1. Central 发送 ATT Read By Group Type 请求,属性类型设为 0x2800,起始句柄 0x0001,结束句柄 0xFFFF。
  2. Peripheral 返回一组服务记录,每条记录包含 起始句柄、结束句柄 和 服务 UUID。
  3. 如果响应中的服务句柄范围未覆盖所有句柄,Central 继续发送下一条请求(从上一响应末尾+1 开始)。
  4. 重复直到收到错误响应或句柄越界,服务发现结束。

性能提示:大多数设备会在一次响应中返回所有 Primary Service,但若 GATT 表较长(超过 MTU 承载能力),蓝牙协议栈会自动分片。封装发现函数时务必循环处理,不要假设一次完成。

4.3 Characteristic 发现流程

针对每个已发现的 Primary Service,Central 使用 Read By Type 或 Read By Group Type 请求来发现包含在该服务句柄范围内的 Characteristic。请求类型 UUID 为 0x2803(Characteristic Declaration)。

步骤操作ATT 命令返回内容
1设置句柄范围服务起始句柄 ~ 服务结束句柄—
2发送 Read By TypeUUID = 0x2803属性句柄 + 特性值句柄 + 属性 + UUID
3解析响应—提取 UUID、属性、值句柄
4迭代若响应未覆盖全范围,发送下一段继续发现后续 Characteristic

常见错误:假设所有 Characteristic 的 UUID 长度为 16-bit。部分自定义服务使用 128-bit UUID,发现时需检查 UUID 长度字段(2字节或16字节)。错误解析 UUID 会导致查找失败。

4.4 发现代码封装(C 伪代码 / API 示例)

以下基于标准 BLE Host API(如 Zephyr、BlueZ、NimBLE)封装一个通用的“发现所有 Primary Service 及其 Characteristic”函数。省略平台细节,突出流程。

// 定义服务与特征的回调结构
typedef struct {
    uint16_t  start_handle;
    uint16_t  end_handle;
    uint8_t   uuid[16];
    bool      uuid_is_16bit;
} service_t;

typedef struct {
    uint16_t  decl_handle;
    uint16_t  value_handle;
    uint8_t   properties;
    uint8_t   uuid[16];
    bool      uuid_is_16bit;
} characteristic_t;

// 发现 Primary Service 的回调
static void on_service_discovered(service_t *svc, void *user_data) {
    printf("Service discovered: start=0x%04x, end=0x%04x\n",
           svc->start_handle, svc->end_handle);
    // 在此服务范围内继续发现特征
    discover_characteristics(svc->start_handle, svc->end_handle);
}

// 发现 Characteristic 的回调
static void on_char_discovered(characteristic_t *chr, void *user_data) {
    printf("Char discovered: handle=0x%04x, props=0x%02x\n",
           chr->value_handle, chr->properties);
    // 可选:在此特征下发现描述符
}

// 主发现函数
void discover_gatt_database(uint16_t conn_handle) {
    // 第1步:发现 Primary Service
    // API 调用示例(Zephyr 风格):
    // bt_gatt_discover_all_services(conn_handle, on_service_discovered, NULL);
    // 第2步:在回调中触发特征发现
    // bt_gatt_discover_characteristics(conn_handle, start, end, on_char_discovered, NULL);
}

封装建议:将发现逻辑封装为状态机或异步队列,避免在回调中执行耗时操作。使用 discovery_state 枚举记录当前发现阶段(IDLE、SERVICE_DISCOVERY、CHAR_DISCOVERY、DESC_DISCOVERY、DONE)。

4.5 调试与常见问题

  • 问题:只发现部分 Service — 检查句柄范围是否被截断;确认 Peripheral 未使用 Secondary Service(UUID 0x2801)且你只请求了 Primary。
  • 问题:Characteristic 属性为空 — 很多芯片的 Characteristic Declaration 包含属性字节,解析时注意字节序(小端)。
  • 问题:发现过程中断 — 可能 MTU 协商未完成,导致响应分片丢失。确保在发现前已通过 Exchange MTU 提升链路吞吐。
  • 调试命令示例(使用 HCI 或 BlueZ 原始 ATT):
# 使用 btmon 或 hcidump 监视 ATT 流量
btmon -d hci0

# 手工发送发现请求(仅用于调试)
bluetoothctl
menu gatt
discover-services [device]
discover-characteristics [service-uuid]

4.6 性能与稳定性优化

优化项具体做法收益
批量发现一次 Read By Group Type 请求尽可能覆盖大的句柄范围减少 ATT 请求次数,降低延迟
缓存结果连接断开后保留已发现的 GATT 结构,重连时不重新发现节省至少 300~500ms
异步队列使用独立任务处理发现回调,不阻塞主循环避免丢包或超时
超时处理每条发现请求设置 5 秒超时,超时后重置状态机防止死锁
MTU 调优发现前协商 MTU 至 247 字节单次响应可携带更多记录

总结:GATT 发现是 BLE 开发中最基础也最容易踩坑的环节。牢记层次顺序 Primary → Characteristic → Descriptor,使用迭代方式处理分片响应,并封装成可重入的异步状态机,能显著提高代码稳定性和调试效率。