3、BLE基础(二):GAP角色与模式、GATT服务与特征值、配对与绑定流程

好,我们接着聊BLE的核心机制。上一章我们把BLE的协议栈骨架过了一遍,这一章得往肉里挖了。说白了,你写BLE应用,天天打交道的就两个东西:GAP 和 GATT。一个管“谁是谁、怎么连”,一个管“连上了能干啥”。

我刚开始做BLE项目时,老把这两个搞混。后来带我的老工程师说了句话,我记到现在——GAP是社交礼仪,GATT是签合同。你想想看,两个人见面,先得确定谁主动、谁被动,打完招呼才能谈正事。GAP管的就是这个“打招呼”的过程,GATT管的是“谈正事”的内容。

核心理解:GAP定义设备如何被发现、如何建立连接;GATT定义连接建立后,数据如何组织、如何读写。

3.1 GAP——通用访问规范

GAP(Generic Access Profile)是BLE应用层最基础的规范。它定义了四种角色,以及设备在不同阶段所处的模式。

3.1.1 四种角色

角色 英文 谁主动 典型设备
广播者 Broadcaster 只发不连 信标、温度传感器
观察者 Observer 只收不连 扫描仪、网关
外围设备 Peripheral 被动连接 手环、心率带
中心设备 Central 主动连接 手机、平板

这里有个坑,我一开始也踩过。很多人以为“外围设备”就是低功耗的,“中心设备”就是高功耗的。其实不一定。我记得有个项目,我们用手机当中心设备去连一个手环,结果手环的功耗反而比手机高——因为手环要一直广播,而手机只在扫描时才开射频。

个人经验:如果你做的是电池供电的传感器,尽量让它当外围设备。广播间隔可以设到100ms以上,功耗能降不少。我曾经把一个温湿度标签的广播间隔从20ms改到200ms,续航直接从3天变成了3周。

3.1.2 广播模式与扫描模式

GAP定义了两种广播模式:

  • 可连接广播:外围设备用,允许中心设备发起连接。
  • 不可连接广播:广播者用,只发数据,不接受连接请求。

扫描模式也有两种:

  • 被动扫描:只收广播包,不发任何请求。
  • 主动扫描:收到广播包后,发一个扫描请求,获取更多数据。

嗯,这里要注意。主动扫描虽然能拿到更多信息,但会多耗电,而且会增加空中的碰撞概率。我在做室内定位项目时,就遇到过因为扫描太频繁导致丢包的问题。后来把扫描窗口从30ms调到10ms,问题就解决了。

3.2 GATT——通用属性规范

连接建立之后,GATT就登场了。GATT定义了一套客户端-服务器模型。外围设备通常是GATT服务器,中心设备是GATT客户端。

3.2.1 服务与特征值

GATT的数据结构分三层:

  1. 服务(Service):一组相关功能的集合。比如“电池服务”、“设备信息服务”。
  2. 特征值(Characteristic):服务下的具体数据点。比如“电池电量百分比”、“设备序列号”。
  3. 描述符(Descriptor):对特征值的补充说明。比如“客户端特征配置描述符(CCCD)”,用来使能通知。

每个服务和特征值都有一个唯一的UUID。标准服务用16位UUID(如0x180F表示电池服务),自定义服务用128位UUID。

避坑指南:我曾经在项目里直接用了一个16位的自定义UUID,结果跟TI的某个标准服务冲突了。从那以后,我所有的自定义服务都用128位UUID,虽然长一点,但绝对不会撞车。

3.2.2 特征值的属性与权限

每个特征值都有属性(Properties)和权限(Permissions)。属性告诉客户端“你能对这个值做什么”,权限告诉客户端“你凭什么做”。

属性 含义 典型用途
Read 客户端可以读 读取设备版本号
Write 客户端可以写 设置设备参数
Notify 服务器主动推送 实时心率数据
Indicate 带确认的推送 关键告警

Notify和Indicate的区别,说白了就是一个不确认、一个确认。Notify快但不可靠,Indicate慢但可靠。我一般把实时数据用Notify,关键事件用Indicate。

3.2.3 一个完整的GATT服务示例

下面是我在UWB+BLE融合项目里用过的自定义服务。这个服务用来传输UWB测距结果。

// 自定义服务UUID:0xAAAA
// 特征值1:测距结果(Notify)
//    UUID: 0xAAA1
//    属性: Notify
//    长度: 4字节(float)
// 特征值2:测距配置(Read/Write)
//    UUID: 0xAAA2
//    属性: Read, Write
//    长度: 1字节(配置模式)

// 初始化代码(基于Zephyr)
static struct bt_gatt_attr attrs[] = {
    BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_16(0xAAAA)),
    BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(0xAAA1),
                           BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_NONE,
                           NULL, NULL, NULL),
    BT_GATT_CCC(&ccc_cfg),
    BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(0xAAA2),
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
                           read_config, write_config, &config_value),
};

这段代码里,我定义了一个服务,里面有两个特征值。第一个是Notify类型的,用来实时推送测距结果;第二个是可读可写的,用来配置工作模式。CCCD(客户端特征配置描述符)是Notify必须的,不然客户端收不到推送。

3.3 配对与绑定流程

配对(Pairing)和绑定(Bonding)是BLE安全机制的核心。很多人把这两个词混着用,其实它们有本质区别。

  • 配对:一次性的安全连接建立过程。断开后,下次连接需要重新配对。
  • 绑定:配对后,双方保存密钥。下次连接时自动恢复加密连接。

说白了,配对是“临时认识一下”,绑定是“存个联系方式,下次直接打电话”。

3.3.1 配对流程的三个阶段

BLE 4.2之后的配对流程分三个阶段:

  1. 配对特征交换:双方交换IO能力、认证要求、密钥大小等信息。
  2. 短期密钥(STK)生成:根据配对方法生成临时密钥。
  3. 传输特定密钥:分发长期密钥(LTK)、身份解析密钥(IRK)等。

配对方法取决于双方的IO能力:

IO能力组合 配对方法 安全性
无输入/无输出 Just Works 低(无MITM保护)
一方有显示,一方有按键 Passkey Entry 高(有MITM保护)
双方都有显示 Numeric Comparison 高(有MITM保护)
一方有显示,一方有确认 Out of Band 最高(通过外部信道)

重要提醒:Just Works虽然方便,但没有任何中间人攻击防护。如果你的产品涉及门锁、支付等安全敏感场景,千万别用Just Works。我见过一个智能门锁项目,出厂默认用Just Works,结果被安全团队一测就攻破了。后来改成Passkey Entry,虽然用户体验差了点,但安全等级上去了。

3.3.2 绑定与密钥存储

绑定成功后,设备会保存以下密钥:

  • LTK(长期密钥):用于后续连接的加密。
  • IRK(身份解析密钥):用于解析可解析私有地址。
  • CSRK(连接签名解析密钥):用于数据签名。

这些密钥必须存储在非易失性存储器中。我习惯把它们放在Flash的专用分区里,并加上CRC校验。有一次,一个设备在OTA升级后绑定的设备全丢了,就是因为密钥分区被擦除了。从那以后,我每次做OTA都会检查密钥分区是否被保留。

3.4 本章知识体系

下面这张图把GAP和GATT的关系、以及配对绑定的流程串起来了。我建议你把它存下来,写代码时对照着看。

BLE GAP与GATT知识体系 BLE GAP 与 GATT 知识体系 GAP - 通用访问规范 “社交礼仪” 四种角色 广播/观察/外围/中心 广播模式 可连接/不可连接 扫描模式 被动/主动 连接建立 连接参数协商 连接建立后 GATT - 通用属性规范 “签合同” 服务 (Service) 功能集合 特征值 (Char) 数据点 描述符 (Desc) 补充说明 属性/权限 读/写/通知/指示 配对与绑定流程 阶段1:特征交换 IO能力/认证要求 阶段2:STK生成 临时密钥 阶段3 密钥分发 绑定:保存LTK/IRK/CSRK 下次连接自动恢复加密

我的习惯:每次开始一个新项目,我都会先画一张类似的图。把GAP角色、GATT服务结构、配对方式定下来,再开始写代码。这样能避免后期返工。尤其是配对方式,一旦产品量产了再想改,代价非常大。