BLE 协议栈核心概念:GAP、GATT、L2CAP 与 HCI 分层解析
做蓝牙开发,第一关就是搞懂协议栈的分层结构。很多初学者一上来就对着 GATT 猛啃,结果发现连广播都配不明白——说白了,就是没搞清楚每一层到底管什么事。
我个人习惯把 BLE 协议栈比作一个快递系统。你想想看,从寄件人到收件人,中间要经过打包、分拣、运输、派送好几个环节。每一层只干自己的活,上层不用管下层怎么跑。这就是分层设计的好处。
核心观点:BLE 协议栈不是铁板一块,而是由 HCI、L2CAP、GAP、GATT 四个核心层次组成的协作体系。搞懂它们的分工,你写代码时才知道「这个回调是谁触发的」、「那个参数该填什么值」。
整体架构:四层模型一眼看透
先看一张我手绘的架构图。这张图我每次培训都会拿出来讲,因为它把各层的关系说得最清楚。
从图上你能看到,HCI 是一条分界线。HCI 以上是 Host(主机),跑在 CPU 上,属于软件层;HCI 以下是 Controller(控制器),通常是蓝牙芯片的固件,处理射频和链路层逻辑。Zephyr 里我们主要跟 Host 侧打交道,但理解 HCI 能帮你定位很多疑难杂症。
GAP:管「谁找谁」和「怎么连」
GAP 全称是 Generic Access Profile。名字很绕,但它的职责其实很单纯——定义设备如何被发现、如何建立连接、以及连接后的安全策略。
我在项目中遇到过最典型的 GAP 问题:设备广播了,手机死活扫不到。查了半天,发现是广播间隔设得太短,导致射频冲突。嗯,GAP 的参数就是这么敏感。
GAP 的核心角色
BLE 设备在 GAP 层有四种角色,Zephyr 里通过 bt_le_adv_start() 和 bt_conn_cb_register() 来配置:
| 角色 | 职责 | Zephyr 典型 API |
|---|---|---|
| Broadcaster | 只发广播,不连接 | bt_le_adv_start() |
| Observer | 只扫描,不连接 | bt_le_scan_start() |
| Peripheral | 可被连接,通常作为从设备 | BT_LE_ADV_CONN |
| Central | 主动发起连接,通常作为主设备 | bt_le_scan_cb + bt_conn_create_le() |
我的经验:Zephyr 里 Peripheral 和 Central 的代码结构差异很大。Peripheral 侧重广播配置和连接回调,Central 侧重扫描过滤和连接发起。初学者建议先从 Peripheral 入手,因为大部分传感器设备都是这个角色。
GAP 事件流
一个典型的 GAP 连接流程是这样的:
- Peripheral 调用
bt_le_adv_start()开始广播 - Central 扫描到广播包,触发
recv()回调 - Central 调用
bt_conn_create_le()发起连接 - Peripheral 侧触发
connected()回调 - 双方通过
bt_conn_security()启动配对
你看,GAP 管的是「谁找谁」和「怎么连」这两件事。至于连上之后干什么,那是 GATT 的活。
GATT:管「连上之后做什么」
GATT 全称是 Generic Attribute Profile。它建立在 GAP 连接之上,定义了数据如何组织、如何读写、如何通知。
说白了,GATT 就是一套「属性数据库」的规范。每个属性有一个 UUID、一组权限、一段值。多个属性组成 Service,多个 Service 组成 Profile。
GATT 的核心概念
| 概念 | 说明 | Zephyr 示例 |
|---|---|---|
| Service | 一组相关功能的集合 | BT_GATT_SERVICE_DEFINE |
| Characteristic | 一个带值的属性,可读/写/通知 | BT_GATT_CHARACTERISTIC |
| Descriptor | 描述特征值的元数据 | BT_GATT_CCC (客户端特征配置) |
| Attribute | GATT 数据库的最小单元 | bt_gatt_attr_register() |
注意:GATT 操作是有 MTU 限制的。默认 MTU 是 23 字节,其中 3 字节是协议头,实际有效载荷只有 20 字节。如果你要发超过 20 字节的数据,必须先做 MTU 协商。我见过有人直接发 100 字节,结果数据被截断,排查了一整天。
GATT 操作类型
GATT 定义了四种基本操作,Zephyr 里都有对应的回调:
- Read:Client 读取 Server 的属性值
- Write:Client 写入 Server 的属性值
- Notify:Server 主动推送数据,不需要 Client 确认
- Indicate:Server 主动推送数据,Client 必须确认
我个人习惯把 Notify 用于高频数据(比如心率、温度),Indicate 用于关键事件(比如报警、状态变更)。因为 Indicate 有确认机制,可靠性更高,但吞吐量会下降。
L2CAP:管「数据怎么打包」
L2CAP 全称是 Logical Link Control and Adaptation Protocol。它位于 GATT/GAP 和 HCI 之间,主要干三件事:
- 多路复用:区分上层是 GATT 还是其他协议(比如 ATT、SMP)
- 分片与重组:把大包切成适合链路层发送的小包
- MTU 协商:通信双方协商最大传输单元
你想想看,GATT 层发一个 100 字节的写请求,L2CAP 会把它切成 20 字节一段,逐段交给 HCI。接收端再拼回去。这个过程对上层完全透明。
关键点:Zephyr 里 L2CAP 的 MTU 协商是自动的,但你可以通过 CONFIG_BT_L2CAP_TX_MTU 和 CONFIG_BT_L2CAP_RX_MTU 来调整。如果你的应用需要传输大块数据(比如 OTA 升级),建议把 MTU 设到 512 以上,否则传输效率会很低。
HCI:管「Host 和 Controller 怎么说话」
HCI 全称是 Host Controller Interface。它是软件和硬件之间的通信桥梁。Zephyr 支持三种 HCI 传输方式:
| 传输方式 | 适用场景 | Zephyr 配置 |
|---|---|---|
| UART | 外挂蓝牙模块(如 nRF52 系列) | CONFIG_BT_HCI_UART |
| SPI | 高速传输场景 | CONFIG_BT_HCI_SPI |
| USB | USB 蓝牙适配器 | CONFIG_BT_HCI_USB |
HCI 的报文分为三类:命令(Host→Controller)、事件(Controller→Host)、数据(双向)。Zephyr 的 HCI 驱动层会帮你处理好这些报文的封装和解析,你基本不用直接操作 HCI 命令——除非你要做底层调试。
避坑指南:我曾经在调试一个蓝牙断连问题时,用 HCI 日志抓到了 Disconnection Complete 事件,发现原因是 Controller 侧的超时参数 connSupervisionTimeout 设得太小。这个参数在 GAP 层配置,但最终是通过 HCI 命令下发给 Controller 的。所以,当你遇到莫名其妙的断连,先开 HCI 日志看看事件码。
四层协作:一个完整的通信例子
假设你有一个温度传感器(Peripheral),手机 App(Central)要读取温度值。整个过程是这样的:
- GAP 层:传感器广播「我是温度计」,手机扫描到广播包,发起连接
- L2CAP 层:连接建立后,L2CAP 为 ATT 协议分配一个 CID(通道 ID),协商 MTU
- GATT 层:手机发现传感器的温度 Service,找到温度 Characteristic,发起 Read 请求
- L2CAP 层:把 Read 请求分片,逐段交给 HCI
- HCI 层:把数据包传给 Controller,Controller 通过射频发出去
- 接收端:反向走一遍——HCI 收包、L2CAP 重组、GATT 解析、应用层拿到温度值
你看,每一层各司其职,互不干扰。这就是分层设计的魅力。
总结一下:
- GAP:管广播、扫描、连接、配对——设备怎么被发现和连接
- GATT:管服务、特征、属性操作——连上之后数据怎么交换
- L2CAP:管数据分片、重组、多路复用——数据怎么打包传输
- HCI:管 Host 和 Controller 的通信——软件和硬件怎么对话
搞懂这四层,你再看 Zephyr 的蓝牙 API,就不会觉得它是一团乱麻了。每一层都有对应的初始化函数、回调注册、参数配置。后面几章我们会逐一深入,从代码层面拆解每一层的实现细节。