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的数据结构分三层:
- 服务(Service):一组相关功能的集合。比如“电池服务”、“设备信息服务”。
- 特征值(Characteristic):服务下的具体数据点。比如“电池电量百分比”、“设备序列号”。
- 描述符(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之后的配对流程分三个阶段:
- 配对特征交换:双方交换IO能力、认证要求、密钥大小等信息。
- 短期密钥(STK)生成:根据配对方法生成临时密钥。
- 传输特定密钥:分发长期密钥(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的关系、以及配对绑定的流程串起来了。我建议你把它存下来,写代码时对照着看。
我的习惯:每次开始一个新项目,我都会先画一张类似的图。把GAP角色、GATT服务结构、配对方式定下来,再开始写代码。这样能避免后期返工。尤其是配对方式,一旦产品量产了再想改,代价非常大。