2. 蓝牙协议基础与GATT结构
蓝牙低功耗(BLE)协议栈采用分层架构,其中 GATT(Generic Attribute Profile) 是应用层与数据交互的核心接口。理解 GATT 的层次模型——服务(Service)、特征(Characteristic)、描述符(Descriptor)——是构建与测试 Mock 蓝牙设备的基石。本章从协议栈原理出发,深入拆解 GATT 结构,并给出可落地的实现、调试与优化指南。
2.1 蓝牙协议栈层次
BLE 协议栈按功能划分为三大层,GATT 位于 Host 层顶部,直接面向应用:
- Controller:物理层(PHY)+ 链路层(LL),负责射频收发、连接状态管理。
- Host:包含 L2CAP、SMP、ATT 和 GATT。其中 ATT(属性协议) 提供属性读写框架,GATT 在其上定义属性组织方式。
- Application:用户业务逻辑,通过 GATT API 与对端交互。
在 Mock 测试中,通常模拟 Host 层以上的 GATT 行为,以验证应用层的正确性。
2.2 GATT 结构核心概念
GATT 将数据组织为树形层次结构,从顶层到底层依次为:
- 服务(Service):逻辑功能单元,例如“电池服务”、“设备信息服务”。由 UUID 唯一标识。
- 特征(Characteristic):具体数据项,包含一个值和若干属性(读、写、通知等)。每个服务可包含多个特征。
- 描述符(Descriptor):特征的元数据或配置项,例如“客户端特征配置描述符(CCCD)”用于开启通知。每个特征可包含多个描述符。
所有元素均通过 UUID 区分,标准 UUID(16 位)由 Bluetooth SIG 定义,自定义 UUID(128 位)用于私有协议。
2.3 服务、特征、描述符对比
| 元素 | 作用 | 包含关系 | UUID 类型 | 典型示例 |
|---|---|---|---|---|
| Service | 逻辑功能分组 | 包含 1 个或多个 Characteristic | 16 位 / 128 位 | 0x180F(电池服务) |
| Characteristic | 数据承载单元 | 包含值 + 属性 + 多个 Descriptor | 16 位 / 128 位 | 0x2A19(电池电量) |
| Descriptor | 特征配置/元数据 | 隶属于单个 Characteristic | 16 位 / 128 位 | 0x2902(CCCD) |
2.4 UUID 与蓝牙规范
UUID 是 GATT 元素的唯一标识。Bluetooth SIG 定义的 16 位标准 UUID 对应通用服务/特征,128 位自定义 UUID 用于厂商私有扩展。Mock 设备应优先复用标准 UUID 以提高互操作性,仅在必要时定义私有 UUID。
// 标准 UUID 示例(16 位)
#define UUID_BATTERY_SERVICE 0x180F
#define UUID_BATTERY_LEVEL 0x2A19
// 自定义 UUID 示例(128 位,用于 Mock 私有服务)
#define UUID_MOCK_SERVICE "12345678-1234-5678-1234-56789abcdef0"
#define UUID_MOCK_DATA_CHAR "12345678-1234-5678-1234-56789abcdef1"
测试建议:在 Mock 设备中使用标准 UUID 可降低协议分析工具(如 nRF Connect、LightBlue)的解析难度,便于快速验证结构正确性。
2.5 实现流程与代码示例
实现 GATT 结构通常遵循以下步骤:
- 规划服务、特征、描述符的 UUID 和权限。
- 注册服务到 GATT 数据库。
- 为每个特征添加回调函数(读、写、通知)。
- 配置描述符(如 CCCD)以支持通知/指示。
- 启动广播或连接等待对端访问。
以下示例基于 Zephyr BLE 栈,展示一个包含“模拟传感器服务”的 GATT 结构定义:
/* 服务与特征 UUID */
#define BT_UUID_MOCK_SERVICE_VAL \
BT_UUID_128_ENCODE(0x00001234, 0x1234, 0x5678, 0x9abc, 0xdef012345678)
#define BT_UUID_MOCK_DATA_VAL \
BT_UUID_128_ENCODE(0x00001234, 0x1234, 0x5678, 0x9abc, 0xdef012345679)
static struct bt_gatt_attr mock_attrs[] = {
BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(BT_UUID_MOCK_SERVICE_VAL)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(BT_UUID_MOCK_DATA_VAL),
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ,
mock_data_read_cb, NULL, &mock_data_val),
BT_GATT_CCCD(&mock_ccc_cfg, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
BT_GATT_DESCRIPTOR(BT_UUID_DECLARE_16(0x2901),
BT_GATT_PERM_READ,
mock_user_desc_read_cb, NULL, "Mock Sensor Data"),
};
static struct bt_gatt_service mock_svc = BT_GATT_SERVICE(mock_attrs);
bt_gatt_service_register(&mock_svc);
客户端侧(Python + bleak)扫描并发现上述服务:
import asyncio
from bleak import BleakScanner, BleakClient
async def discover_gatt():
device = await BleakScanner.find_device_by_name("MockSensor")
async with BleakClient(device) as client:
for service in client.services:
print(f"Service: {service.uuid}")
for char in service.characteristics:
print(f" Characteristic: {char.uuid} properties: {char.properties}")
for desc in char.descriptors:
print(f" Descriptor: {desc.uuid}")
asyncio.run(discover_gatt())
2.6 调试 GATT 结构
- 协议分析仪:使用 nRF Connect(手机端)或 Wireshark(配合 BLE 嗅探器)查看完整 GATT 树。
- ATT 错误码:返回 0x02(无效句柄)通常表示 UUID 或句柄不匹配;0x08(权限不足)检查回调权限配置。
- 日志输出:在服务注册、读写回调中增加日志,确认 GATT 操作流向。
// 启用 Zephyr BLE 日志(prj.conf)
CONFIG_BT_DEBUG_ATT=y
CONFIG_BT_DEBUG_GATT=y
常见陷阱:CCCD(0x2902)必须独立配置为 Descriptor,且 CCCD 写回调需要正确处理订阅/取消订阅状态,否则通知功能无法正常工作。
2.7 常见错误与解决方案
| 错误现象 | 可能原因 | 解决措施 |
|---|---|---|
| 对端无法发现服务 | 服务未注册 / UUID 错误 | 确认 bt_gatt_service_register 被调用,UUID 字节序正确 |
| 特征读写无响应 | 权限与特征属性不匹配 | 检查 BT_GATT_CHRC_READ 与 BT_GATT_PERM_READ 是否同时设置 |
| 通知不触发 | CCCD 未配置或配置回调缺失 | 添加 BT_GATT_CCCD 并实现 bt_gatt_ccc_cfg_changed |
| 多个服务顺序混乱 | ATT 句柄分配冲突 | 使用 BT_GATT_SERVICE 宏保证句柄连续,或显式指定句柄范围 |
2.8 性能与稳定性优化
- 精简结构:单个 Service 包含 3~5 个 Characteristic 为宜,避免单个服务过深或过宽,降低 ATT 遍历耗时。
- 合并通知:多条数据可通过同一个 Characteristic 分批次发送,减少连接间隔内的数据包数量。
- MTU 适配:在连接后协商 MTU(最大传输单元),使单次 GATT 操作承载更多数据,减少交互次数。
- 描述符复用:多个特征共享同一描述符配置时(如相同的 CCCD 处理逻辑),使用
BT_GATT_CCC_CACHING减少内存占用。
// MTU 协商示例(Zephyr)
void mtu_updated(struct bt_conn *conn, uint16_t mtu) {
printk("MTU updated: %d\n", mtu);
}
// 注册回调
BT_CONN_CB_DEFINE(conn_callbacks) = {
.mtu_updated = mtu_updated,
};
Mock 测试价值:通过 Mock 设备模拟极端 GATT 结构(如包含 20 个特征的服务),可提前发现性能瓶颈与协议栈边界问题,避免在真实硬件上反复烧录调试。