2. 蓝牙基础协议栈:BLE协议栈架构、GAP与GATT角色与规范、广播与扫描流程、连接建立过程
各位同学,咱们今天聊聊BLE协议栈。说实话,我刚入行那会儿,看到蓝牙协议栈的文档,第一反应是——这玩意儿也太复杂了吧?一堆分层、一堆角色、一堆流程。但后来做项目多了,我发现一个道理:你不需要记住每一层的每一个细节,但你必须清楚数据是怎么从你的应用代码,跑到对端设备上去的。
这一节,我就带你把这个路径捋清楚。
2.1 BLE协议栈架构:Controller、Host、Application
BLE协议栈,说白了就是一套软件分层规范。它把蓝牙通信这件事,拆成了三个大块:Controller(控制器)、Host(主机)、Application(应用)。
我习惯把这三层想象成一个快递系统:
- Controller 是快递员,负责把包裹(数据)送到对方手里。它管的是射频、物理层、链路层这些硬核的东西。
- Host 是快递分拣中心,负责把包裹打包、拆包、贴标签。它管的是逻辑链路、属性协议、安全管理。
- Application 是寄件人/收件人,只关心包裹里装的是什么,不关心快递怎么送。
来看看官方的分层图,我把它画成了SVG,方便你对照理解:
关键点:Controller 和 Host 之间通过 HCI(主机控制器接口) 通信。HCI 可以是 UART、USB、SDIO 等物理接口。我在调试一款国产蓝牙芯片时,就遇到过 HCI 波特率不匹配导致连接失败的问题,折腾了两天才发现是 UART 时钟源配置错了。
2.2 GAP 与 GATT:两个核心规范
在 Host 层里,有两个规范你必须要搞懂:GAP 和 GATT。它们俩分工非常明确:
| 规范 | 全称 | 管什么 | 类比 |
|---|---|---|---|
| GAP | Generic Access Profile | 设备发现、连接建立、广播、扫描 | 两个人怎么打招呼、怎么握手 |
| GATT | Generic Attribute Profile | 连接后的数据读写、通知、服务发现 | 握手之后,怎么交换名片、怎么聊天 |
说白了,GAP 管的是“能不能连上”,GATT 管的是“连上之后干什么”。这两个阶段是串行的——必须先通过 GAP 建立连接,然后才能用 GATT 传数据。
2.2.1 GAP 角色
GAP 定义了四种角色,但实际项目中常用的就两种:
- Broadcaster(广播者):只发广播包,不接收连接。比如蓝牙信标(Beacon)。
- Observer(观察者):只扫描广播包,不发起连接。比如扫描仪。
- Peripheral(外设):发广播,等待连接。比如心率带、温度传感器。
- Central(中心):扫描并主动发起连接。比如手机、网关。
我的经验:在 Wi-Fi 模块 + BLE 的透传方案中,模块通常作为 Peripheral,手机 App 作为 Central。但如果你做的是网关类产品,模块可能需要同时支持 Central 和 Peripheral 两种角色——这就涉及到多角色切换,嗯,这里要注意,很多芯片不支持同时运行两个角色。
2.2.2 GATT 角色与规范
GATT 的角色更简单,就两个:
- Server(服务器):提供数据的一方。比如温度传感器,它把温度值放在一个“特征值”里。
- Client(客户端):读取或写入数据的一方。比如手机 App,它去读温度传感器的特征值。
GATT 的数据组织方式,我习惯叫它“三层嵌套”:
Profile(服务集合)
└── Service(服务,如“电池服务”)
└── Characteristic(特征,如“电池电量”)
├── Value(数据值,如 85%)
└── Descriptor(描述符,如“客户端特征配置”)
你想想看,一个设备可以有多个 Service,每个 Service 可以有多个 Characteristic。手机连上设备后,就是通过遍历这些 Service 和 Characteristic,找到它想要的数据。
2.3 广播与扫描流程
广播和扫描,是 BLE 连接的第一步。我把它拆成两个阶段来讲。
2.3.1 广播阶段(Peripheral 视角)
外设(Peripheral)会周期性地发送广播包。广播包分两种:
- 可连接广播(Connectable Advertisement):告诉周围设备“我可以被连接”。
- 不可连接广播(Non-connectable Advertisement):只发数据,不接受连接。Beacon 用的就是这种。
广播包的结构大致如下:
广播包(Adv PDU)
├── 报头(Header):类型、长度、信道
├── 广播地址(AdvA):设备 MAC 地址
└── 广播数据(AdvData)
├── AD Structure 1: 标志(Flags)
├── AD Structure 2: 本地名称(Local Name)
└── AD Structure 3: 厂商自定义数据(Manufacturer Specific Data)
避坑指南:我曾经遇到一个项目,设备广播出去了,手机死活扫不到。后来发现是广播间隔设置得太长(1000ms),手机扫描窗口又太短,两者刚好错开了。我建议你把广播间隔设在 100ms~200ms 之间,扫描窗口设在 30ms 以上,这样匹配成功率最高。
2.3.2 扫描阶段(Central 视角)
中心设备(Central)会在三个广播信道(37、38、39)上轮流监听。扫描有两种模式:
- 被动扫描(Passive Scanning):只收广播包,不发任何请求。
- 主动扫描(Active Scanning):收到广播包后,发送扫描请求(Scan Req),外设回复扫描响应(Scan Rsp),获取更多数据。
我画了一张流程图,帮你理清广播和扫描的交互:
2.4 连接建立过程
当 Central 决定连接某个 Peripheral 时,它会发送一个 连接请求(Connect Req)。这个请求里包含了一堆连接参数,我挑几个重要的说:
| 参数 | 说明 | 典型值 | 影响 |
|---|---|---|---|
| 连接间隔 | 两个连接事件之间的时间 | 30ms ~ 50ms | 间隔越小,功耗越高,延迟越低 |
| 延迟 | 设备可以跳过多少个连接事件 | 0 ~ 4 | 延迟越大,省电,但响应慢 |
| 监控超时 | 多久没收到包就认为连接断开 | 2000ms ~ 6000ms | 超时太短容易误判断连 |
实战建议:在 Wi-Fi 模块透传场景中,我通常把连接间隔设为 30ms,延迟设为 0,监控超时设为 4000ms。这样既能保证透传数据的实时性,又不会因为 Wi-Fi 信道干扰而频繁断连。如果你做的是低功耗传感器,可以把连接间隔拉到 100ms 以上,配合延迟 4,功耗能降一半。
连接建立后,双方就进入 连接状态。这时候,GATT 开始登场——Central 会发起服务发现,遍历 Peripheral 的 Service 和 Characteristic,然后开始读写数据。
嗯,这里有个坑我要提醒你:连接参数不是 Central 单方面决定的。Peripheral 可以在连接后发起 连接参数更新请求(Connection Parameter Update Request)。我遇到过一款心率带,它连上后立刻请求把连接间隔从 30ms 改成 100ms,结果手机 App 没处理这个请求,导致连接异常。所以,两端都要做好连接参数更新的处理逻辑。