2. 开发环境搭建

做蓝牙开发,第一关就是搞懂协议栈的分层结构。很多初学者一上来就对着 GATT 猛啃,结果发现连广播都配不明白——说白了,就是没搞清楚每一层到底管什么事。

我个人习惯把 BLE 协议栈比作一个快递系统。你想想看,从寄件人到收件人,中间要经过打包、分拣、运输、派送好几个环节。每一层只干自己的活,上层不用管下层怎么跑。这就是分层设计的好处。

核心观点:BLE 协议栈不是铁板一块,而是由 HCI、L2CAP、GAP、GATT 四个核心层次组成的协作体系。搞懂它们的分工,你写代码时才知道「这个回调是谁触发的」、「那个参数该填什么值」。

整体架构:四层模型一眼看透

先看一张我手绘的架构图。这张图我每次培训都会拿出来讲,因为它把各层的关系说得最清楚。

BLE 协议栈四层架构 应用层 (Application) Profile / Service / 业务逻辑 GAP (通用访问规范) 广播 / 扫描 / 连接 / 配对 GATT (通用属性规范) 服务 / 特征 / 属性操作 L2CAP (逻辑链路控制与适配协议) 数据分片 / 重组 / 多路复用 / MTU 协商 HCI (主机控制器接口) 命令 / 事件 / 数据包传输 控制器 (Link Layer + PHY) 射频 / 跳频 / 包收发 Host 侧 (软件) Controller 侧 (硬件)

从图上你能看到,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 连接流程是这样的:

  1. Peripheral 调用 bt_le_adv_start() 开始广播
  2. Central 扫描到广播包,触发 recv() 回调
  3. Central 调用 bt_conn_create_le() 发起连接
  4. Peripheral 侧触发 connected() 回调
  5. 双方通过 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 之间,主要干三件事:

  1. 多路复用:区分上层是 GATT 还是其他协议(比如 ATT、SMP)
  2. 分片与重组:把大包切成适合链路层发送的小包
  3. 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)要读取温度值。整个过程是这样的:

  1. GAP 层:传感器广播「我是温度计」,手机扫描到广播包,发起连接
  2. L2CAP 层:连接建立后,L2CAP 为 ATT 协议分配一个 CID(通道 ID),协商 MTU
  3. GATT 层:手机发现传感器的温度 Service,找到温度 Characteristic,发起 Read 请求
  4. L2CAP 层:把 Read 请求分片,逐段交给 HCI
  5. HCI 层:把数据包传给 Controller,Controller 通过射频发出去
  6. 接收端:反向走一遍——HCI 收包、L2CAP 重组、GATT 解析、应用层拿到温度值

你看,每一层各司其职,互不干扰。这就是分层设计的魅力。

总结一下:

  • GAP:管广播、扫描、连接、配对——设备怎么被发现和连接
  • GATT:管服务、特征、属性操作——连上之后数据怎么交换
  • L2CAP:管数据分片、重组、多路复用——数据怎么打包传输
  • HCI:管 Host 和 Controller 的通信——软件和硬件怎么对话

搞懂这四层,你再看 Zephyr 的蓝牙 API,就不会觉得它是一团乱麻了。每一层都有对应的初始化函数、回调注册、参数配置。后面几章我们会逐一深入,从代码层面拆解每一层的实现细节。