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 Services | Read By Group Type (UUID=0x2800) | 获取所有主服务的句柄范围 |
| Discover Characteristics | Read By Type (UUID=0x2803) | 遍历指定服务内的特征 |
| Discover All Characteristic Descriptors | Find 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数据包交互:
代码示例(基于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事务冲突或意外断连。建议使用串行队列或状态机控制流程顺序。