1. BLE基础与GATT协议概述

各位同学好,我是老周。做嵌入式蓝牙开发十几年了,今天咱们聊聊BLE和GATT协议。

蓝牙低功耗,也就是BLE,跟经典蓝牙完全是两码事。我刚开始接触时也以为BLE只是经典蓝牙的省电版,后来踩了不少坑才明白——它俩连协议栈都不一样。

蓝牙低功耗技术简介

BLE为什么省电?说白了,它大部分时间都在睡觉。

经典蓝牙要持续保持连接,像打电话一样一直占着线。BLE不一样,它广播一下、传完数据、立马断开,然后继续睡。我做过一个温湿度传感器,一颗纽扣电池撑了两年多——这在经典蓝牙时代想都不敢想。

BLE的几个关键特性:

  • 峰值功耗低:发射电流仅几mA到十几mA
  • 连接速度快:从广播到建立连接,最快3ms搞定
  • 数据包小:一个包最多传251字节,适合传感器数据
  • 频段:2.4GHz ISM频段,40个信道
个人经验:做低功耗产品时,别光看芯片的休眠电流。我吃过一次亏——芯片休眠电流0.5µA,但广播间隔设成20ms,结果平均电流飙到200µA。广播间隔、连接间隔、数据量,这三者要一起算。

BLE协议栈架构

BLE协议栈分两层:Controller和Host。中间有个HCI接口隔开。

我画了一张图,帮你理清这个结构:

BLE协议栈架构 Host 主机层 GAP 通用访问规范 GATT 通用属性规范 SM 安全管理 ATT 属性协议 L2CAP 逻辑链路控制与适配 HCI 主机控制器接口 Controller 控制器层 LL 链路层 RF 射频层 PHY 物理层 应用层(App/Profile)运行在Host之上,通过GATT/GAP接口与协议栈交互

这张图你得记住。做BLE开发,大部分时间都在跟GAP和GATT打交道。

各层简单说:

  • PHY/射频:负责发0和1,1Mbps或2Mbps
  • LL链路层:管理广播、扫描、连接、重传
  • HCI:Host和Controller之间的通信桥梁
  • L2CAP:数据分包和重组,把大包切成小片
  • ATT:属性协议,GATT的底层传输机制
  • GATT:定义数据怎么组织和访问
  • GAP:定义设备怎么发现和连接

GATT与GAP的区别

很多新手搞混这两个。我简单说:

GAP管"怎么找到你",GATT管"找到你之后怎么读数据"。

举个例子:你进一个图书馆,GAP就是门口的导引牌,告诉你图书馆在哪、几点开门。GATT就是书架上的分类标签和索书号,告诉你哪本书在哪层哪列。

对比项 GAP GATT
全称 通用访问规范 通用属性规范
职责 设备发现、连接建立、广播 数据读写、通知、服务发现
角色 Broadcaster/Observer/Peripheral/Central Server/Client
典型场景 手机扫描到蓝牙手环 手机读取手环的心率值

关键点:GAP角色和GATT角色是独立的。一个Peripheral设备可以是GATT Server,也可以是GATT Client。我在项目中就做过一个网关,它作为Peripheral广播自己,同时又作为Client去读传感器数据。

GATT角色:Server与Client

GATT定义了两种角色:

  • Server:持有数据的一方。比如心率传感器,它存着心率值,等着别人来读。
  • Client:发起读写请求的一方。比如手机App,它主动去读心率传感器的数据。

嗯,这里要注意:Server和Client不是固定的。同一个设备可以同时是Server和Client。我做过一个数据采集器,它作为Server把采集到的数据暴露给手机,同时又作为Client去读另一个传感器的数据——这种场景很常见。

GATT通信的基本流程:

  1. Client发现Server有哪些Service
  2. Client发现Service里有哪些Characteristic
  3. Client读写Characteristic的值
  4. Server主动Notify/Indicate给Client
避坑指南:我曾经在项目里犯过一个低级错误——Client端没等Service发现完成就去读Characteristic,结果一直返回空值。记住:发现Service和Characteristic是异步的,必须等回调完成再操作。

Profile与Service的概念

这两个概念,我换个说法你就能懂:

Profile 是一份"说明书"。它规定了一个设备应该有哪些Service、怎么工作。比如心率Profile(Heart Rate Profile)规定:心率设备必须包含心率Service(Heart Rate Service)和设备信息服务(Device Information Service)。

Service 是具体的数据集合。一个Service包含多个Characteristic(特征值),每个Characteristic代表一个数据点。

举个例子:

心率Profile (Heart Rate Profile)
  └── 心率Service (Heart Rate Service) [UUID: 0x180D]
       ├── 心率测量特征 (Heart Rate Measurement) [UUID: 0x2A37]
       │   ├── 属性: Notify
       │   └── 值: 心率数值 (uint8)
       └── 传感器位置特征 (Body Sensor Location) [UUID: 0x2A38]
           ├── 属性: Read
           └── 值: 传感器位置 (uint8枚举)
  └── 设备信息服务 (Device Information Service) [UUID: 0x180A]
       ├── 制造商名称特征 [UUID: 0x2A29]
       └── 序列号特征 [UUID: 0x2A25]

你看,Service用UUID标识。标准Service用16位UUID(比如0x180D),自定义Service用128位UUID。

层级 说明 例子
Profile 规范文档,定义设备行为 心率Profile、电池Profile
Service 功能模块,包含多个特征 心率Service (0x180D)
Characteristic 单个数据点,可读/写/通知 心率测量值 (0x2A37)
Descriptor 特征的描述或配置 客户端特征配置描述符 (CCCD)
个人习惯:我设计自定义Service时,UUID喜欢用公司名+功能名的哈希值。比如"com.mycompany.temperature"转成128位UUID。这样既保证唯一性,又方便团队记忆。

最后说一句:GATT这套东西,说白了就是"属性表"的增删改查。你把它想象成一个Excel表格——Service是Sheet页,Characteristic是单元格,Descriptor是单元格的备注。理解了这一点,后面写代码就顺了。