2. 蓝牙基础协议栈:BLE协议栈架构、GAP与GATT角色与规范、广播与扫描流程、连接建立过程

各位同学,咱们今天聊聊BLE协议栈。说实话,我刚入行那会儿,看到蓝牙协议栈的文档,第一反应是——这玩意儿也太复杂了吧?一堆分层、一堆角色、一堆流程。但后来做项目多了,我发现一个道理:你不需要记住每一层的每一个细节,但你必须清楚数据是怎么从你的应用代码,跑到对端设备上去的。

这一节,我就带你把这个路径捋清楚。

2.1 BLE协议栈架构:Controller、Host、Application

BLE协议栈,说白了就是一套软件分层规范。它把蓝牙通信这件事,拆成了三个大块:Controller(控制器)、Host(主机)、Application(应用)。

我习惯把这三层想象成一个快递系统:

  • Controller 是快递员,负责把包裹(数据)送到对方手里。它管的是射频、物理层、链路层这些硬核的东西。
  • Host 是快递分拣中心,负责把包裹打包、拆包、贴标签。它管的是逻辑链路、属性协议、安全管理。
  • Application 是寄件人/收件人,只关心包裹里装的是什么,不关心快递怎么送。

来看看官方的分层图,我把它画成了SVG,方便你对照理解:

BLE 协议栈架构(分层模型) Application(应用层) 用户自定义 Profile / 业务逻辑 Host(主机层) GAP(通用访问规范) GATT(通用属性规范) ATT / L2CAP / SMP HCI(主机控制器接口) Controller(控制器层) 链路层(LL) / 物理层(PHY) / 射频(RF) 💡 实际芯片中,Controller 通常集成在蓝牙 SoC 内部,Host 可能在 MCU 上运行

关键点: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),获取更多数据。

我画了一张流程图,帮你理清广播和扫描的交互:

广播与扫描流程(主动扫描) Peripheral Central ① 广播包(Adv PDU) ② 扫描请求(Scan Req) ③ 扫描响应(Scan Rsp) ④ 连接请求(Connect Req) ✅ 连接建立成功

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 没处理这个请求,导致连接异常。所以,两端都要做好连接参数更新的处理逻辑。