3. 核心协议栈:BLE基础、配对绑定与GATT服务

各位同学,今天我们来啃一块硬骨头——BLE协议栈。说实话,我刚接触数字钥匙项目时,也被这一堆缩写搞得头大。但后来我发现,搞懂BLE,数字钥匙的底层逻辑就通了八成。

3.1 BLE(蓝牙低功耗)基础

BLE和经典蓝牙最大的区别是什么?说白了,就是「省电」二字。经典蓝牙像是个随时待命的保安,功耗高但响应快;BLE则像个巡逻员,大部分时间在睡觉,有事才醒一下。

在数字钥匙场景里,手机和车机之间的连接,大部分时间是不需要高频通信的。你想想看,用户靠近车门时,钥匙才需要工作。平时保持连接干嘛?费电。

BLE的物理层工作在2.4GHz ISM频段,有40个信道。其中3个是广播信道(37/38/39),剩下37个是数据信道。我刚开始做项目时,总搞混广播信道和数据信道的用途。后来我总结了一个记忆方法:

  • 广播信道:用于设备发现和连接建立。就像你在商场里喊「我在这儿」。
  • 数据信道:用于连接后的数据传输。就像你们找到彼此后开始聊天。

关键参数:

  • 连接间隔:7.5ms ~ 4s(数字钥匙通常用20-50ms)
  • 广播间隔:20ms ~ 10.24s(推荐100ms左右)
  • 最大传输单元(MTU):默认23字节,可协商至247字节

我在项目中遇到过一个问题:手机靠近车辆时,连接建立太慢。查了半天,发现是广播间隔设得太长了。嗯,这里要注意,广播间隔和功耗是直接挂钩的——间隔越短,发现越快,但功耗也越高。

3.2 配对与绑定流程

配对和绑定,这两个词经常被混用。我个人的理解是:

  • 配对(Pairing):建立安全连接的过程。就像两个人第一次见面,互相确认身份。
  • 绑定(Bonding):保存配对信息,下次可以直接用。就像交换了名片,下次见面不用再自我介绍。

BLE 4.2之后引入了LE Secure Connections,这是目前数字钥匙项目的主流选择。整个流程分三步:

  1. 配对特征交换:双方交换IO能力、认证要求等。我建议这里把IO能力设为「NoInputNoOutput」,因为数字钥匙不需要用户按键确认。
  2. 认证阶段:选择配对方式。数字钥匙通常用Just Works或Passkey Entry。Just Works虽然方便,但容易被中间人攻击。我个人更推荐Passkey Entry,虽然多了一步输入,但安全性高很多。
  3. 密钥分发:交换LTK(长期密钥)、IRK(身份解析密钥)等。绑定信息就保存在这一步。

避坑指南:我曾经在某个项目中,因为绑定信息存储不当,导致手机换系统后无法重新配对。后来我规定:绑定信息必须存储在安全区域(如SE或TEE),并且支持远程擦除。你想想看,如果手机丢了,车钥匙还在里面,多危险。

3.3 GATT服务定义

GATT(通用属性协议)是BLE的灵魂。它定义了数据如何组织和访问。在数字钥匙项目中,我们通常自定义一个GATT服务,包含多个特征(Characteristic)。

一个典型的数字钥匙GATT服务结构如下:

服务/特征 UUID 属性 说明
数字钥匙服务 0xFFF0 Primary Service 自定义服务
└ 钥匙状态特征 0xFFF1 Read/Notify 读取钥匙状态,如锁定/解锁
└ 控制命令特征 0xFFF2 Write/WriteWithoutResponse 发送控制指令,如解锁
└ 安全认证特征 0xFFF3 Write/Indicate 用于安全认证数据交换

我建议在定义GATT服务时,遵循以下原则:

  • 服务UUID:使用16位UUID(0xFFFF范围内),但要注意避免冲突。我习惯用0xFFF0-0xFFFF这段。
  • 特征属性:根据实际需求选择。比如控制命令用WriteWithoutResponse,因为不需要等待确认,响应更快。
  • 描述符:别忘了加Client Characteristic Configuration Descriptor(CCCD),否则Notify/Indicate没法用。

小技巧:在定义GATT服务时,我建议把安全相关的特征单独放在一个服务里。这样方便做权限控制——比如只有绑定过的设备才能访问安全服务。我在一个项目中就是这么做的,后来审计时省了不少事。

3.4 ATT数据包结构

ATT(属性协议)是GATT的底层传输协议。每个ATT数据包都有固定的格式。说实话,刚开始看协议栈源码时,我被这些字节排列搞得晕头转向。后来我画了个图,一下子就清楚了。

一个标准的ATT数据包结构如下:

+--------+--------+--------+--------+--------+--------+
| 操作码  | 句柄   | 偏移量  | 数据长度 | 数据    |
| (1字节) | (2字节)| (2字节)| (1字节) | (可变)  |
+--------+--------+--------+--------+--------+--------+

常见的操作码有:

  • 0x0A:Read Request(读请求)
  • 0x0B:Read Response(读响应)
  • 0x12:Write Request(写请求,需要响应)
  • 0x52:Write Command(写命令,不需要响应)
  • 0x1B:Handle Value Notification(通知)
  • 0x1D:Handle Value Indication(指示,需要确认)

举个例子,如果手机要读取钥匙状态特征(句柄0x0010),ATT层会这样交互:

手机 → 车机: [0x0A][0x10][0x00][0x00][0x00]
              (Read Request, 句柄0x0010, 偏移0, 长度0)
车机 → 手机: [0x0B][0x10][0x00][0x00][0x01][0x01]
              (Read Response, 句柄0x0010, 偏移0, 长度1, 数据0x01表示已解锁)

我在调试时发现一个常见问题:MTU协商没做好,导致大数据包被截断。比如你要发送一个256字节的证书,但MTU只有23字节,那就得拆成多个包发送。我建议在连接建立后,立即协商MTU到最大值(247字节),这样能减少很多麻烦。

ATT层注意事项:

  • 每个ATT请求都必须有对应的响应(除了Write Command和Notification)
  • 句柄是16位的,范围0x0001-0xFFFF,0x0000保留
  • 数据长度字段只有1字节,所以单包最大255字节(受MTU限制)

好了,这一章的内容就到这里。BLE协议栈虽然看起来复杂,但只要你理解了广播→连接→GATT服务→ATT数据包这条主线,剩下的就是细节问题了。下一章我们会讲如何用这些知识实现一个完整的数字钥匙通信流程。