BLE连接与GATT交互流程:连接建立、MTU协商、服务发现、特征句柄映射

本章聚焦BLE连接建立后的GATT交互完整流程。作为读写特征的核心前置步骤,开发者需要精确掌握连接建立参数控制、MTU协商时机、服务发现策略以及句柄映射原理。这四个阶段决定了后续特征读写的可靠性与效率。

4.1 连接建立

BLE连接由发起者(Initiator)根据广播包中的连接请求触发,关键参数包括:

  • 连接间隔(Connection Interval):7.5ms~4s,典型值30~50ms。
  • 从设备延迟(Slave Latency):允许跳过的连接事件数,降低功耗。
  • 监督超时(Supervision Timeout):100ms~32s,超时则连接断开。

主机(GATT Client)收到连接完成事件后,即可发起后续交互。

4.2 MTU协商

ATT_MTU默认值23字节,协商后可扩展至512字节(BLE 4.2+)。协商过程遵循请求-响应模式:

// 主机发送MTU请求(建议值247)
ATT_MTU_REQ (client_rx_mtu = 247)

// 从设备回复MTU响应(取min(双方MTU))
ATT_MTU_RSP (server_rx_mtu = 247)  // 协商结果MTU=247

性能影响:MTU越大,单包携带数据越多,可减少包数量并提高吞吐量。推荐在连接建立后立即发起协商。

4.3 服务发现

GATT服务发现流程分为主服务发现和包含服务/特征发现两类常用方式:

发现类型ATT操作典型用途
Discover All Primary ServicesRead By Group Type (UUID=0x2800)获取所有主服务的句柄范围
Discover CharacteristicsRead By Type (UUID=0x2803)遍历指定服务内的特征
Discover All Characteristic DescriptorsFind Information获取CCCD等描述符句柄

实际实现中常使用分组发现(针对每个服务)以提高效率。完成发现后需建立本地句柄映射表。

4.4 特征句柄映射

每个特征及其描述符在GATT服务器中拥有唯一16位句柄(Handle)。客户端需将逻辑特征UUID与物理句柄建立映射,才能执行后续读写:

// 典型映射结构(C伪代码)
typedef struct {
    uint16_t handle;          // 特征声明句柄(用于写CCCD)
    uint16_t value_handle;    // 特征值句柄(用于读写)
    uint16_t cccd_handle;     // 客户端特征配置描述符句柄(可选)
    uint8_t  properties;      // 属性(读/写/通知等)
} gatt_char_mapping_t;

// 服务发现完成后的映射示例
gatt_char_mapping_t battery_level = {
    .handle = 0x002E,
    .value_handle = 0x002F,
    .cccd_handle = 0x0030,
    .properties = GATT_CHAR_PROP_READ | GATT_CHAR_PROP_NOTIFY
};

映射表通常用链表或数组维护,在发现回调中动态填充。正确映射是后续特征读写和缓存更新的基础。

整体流程与关键数据包

下图展示了从连接建立到句柄映射就绪的完整状态机及典型ATT数据包交互:

BLE连接建立 → GATT交互就绪流程 连接建立 LL_CONNECT_IND MTU协商 ATT_EXCHANGE_MTU 服务发现 Read By Group Type 句柄映射 本地缓存完成 📦 各阶段关键ATT数据包 • 连接建立:发送 CONNECT_IND 后,Host 收到 HCI_LE_Connection_Complete 事件。 • MTU协商:主机主动发出 EXCHANGE_MTU_REQ(含 client_rx_mtu),从机回复 EXCHANGE_MTU_RSP。 • 服务发现:使用 Read By Group Type (UUID 0x2800) 获取服务列表,再逐服务获取特征和描述符。 • 句柄映射:在发现回调中将 UUID、属性、值句柄、CCCD句柄保存到本地映射表。 ⏱ 时序注意事项 • MTU协商应在服务发现之前完成,否则服务发现时仍使用23字节MTU,效率低。 • 服务发现期间应避免发起其他GATT请求,防止PDU冲突导致协议错误。 📊 MTU对吞吐量影响(理论值) MTU 单包有效数据(字节) 相对吞吐量 23 20 1x 247 244 12.2x

代码示例(基于Zephyr API)

以下代码展示了在连接回调中按顺序触发MTU协商和服务发现,并填充句柄映射表的典型模式:

 // 连接回调(在User app中注册)
 static void connected(struct bt_conn *conn, uint8_t err) {
     if (err) return;
     // 1. 发起MTU协商(建议值247)
     bt_gatt_exchange_mtu(conn, 247);
 }
 
 // MTU协商完成回调(可选)
 static void mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) {
     // 2. MTU就绪后启动服务发现
     // 发现所有主服务,并在发现回调中处理特征
     bt_gatt_discover_all_primary_services(conn, discover_cb, NULL);
 }
 
 // 发现回调(每个服务/特征/描述符触发一次)
 static uint8_t discover_cb(struct bt_conn *conn,
                            const struct bt_gatt_attr *attr,
                            struct bt_gatt_discover_params *params) {
     if (!attr) {
         // 发现完成,本地映射就绪
         return BT_GATT_ITER_STOP;
     }
     // 3. 根据attr填充映射表(此处简化)
     store_handle_mapping(attr->handle, attr->uuid, attr->user_data);
     return BT_GATT_ITER_CONTINUE;
 }
💡 最佳实践:服务发现应使用分组发现方式(如Zephyr的bt_gatt_discover),避免一次发现所有服务导致内存激增。同时建议在发现完成后立即保存映射表到非易失存储,实现快速重连。

调试与常见错误

阶段常见错误排查方式
连接建立连接间隔过大导致掉线检查GAP参数,确认从设备延迟设置合理
MTU协商协商超时或未触发在连接回调后100ms内调用MTU请求,使用Sniffer抓包验证ATT请求
服务发现发现不完整(特征缺失)检查UUID匹配,遍历完所有句柄范围,确认服务器端正确声明特征
句柄映射句柄与UUID错位在发现回调中打印handle和uuid,与nRF Connect抓取的列表比对
⚠️ 性能陷阱:不要在服务发现过程中发起其他GATT操作,否则可能导致ATT事务冲突或意外断连。建议使用串行队列或状态机控制流程顺序。