BLE协议栈基础:架构、角色与广播扫描机制

做UWB定位与BLE适配,第一步得把BLE协议栈吃透。说实话,很多做UWB的工程师一开始都低估了BLE这层的复杂度——觉得不就是个无线开关嘛?等你真正要把厘米级定位数据和BLE广播包揉在一起的时候,才会发现协议栈里处处是坑。

这一章,我就带你从架构到实战,把BLE协议栈的核心骨架搭起来。

BLE协议栈架构:分层拆解

BLE协议栈分三层:Controller、Host、Application。我习惯把它想象成一个“汉堡包”——底层管射频,中间层管逻辑,顶层管业务。

核心分层:

  • Controller(控制器):物理层+链路层。负责收发数据包、跳频、时序控制。这部分通常是芯片硬件实现,我们一般不动它。
  • Host(主机):L2CAP、ATT、GATT、GAP、SM。这是软件协议栈的核心,我们做适配主要跟这层打交道。
  • Application(应用):你的业务代码。比如UWB测距结果怎么塞进广播包,就是在这层实现。

为什么这么分?我刚开始做BLE项目时,总想把所有逻辑都塞在应用层,结果广播间隔一调小,链路层就丢包。后来才明白——每一层都有自己的时序约束,跨层调用必须通过HCI接口。

个人经验: 调试UWB+BLE时,我建议先把Controller和Host的接口(HCI)日志打开。很多诡异问题,比如广播包发不出去、连接断断续续,其实都是HCI命令超时导致的。别一上来就怀疑UWB模块。

来看一张协议栈架构图,帮你建立整体印象:

BLE协议栈分层架构 Application(应用层) UWB测距数据封装 / 广播策略 / 连接管理 Host(主机层) GAP(通用访问规范) GATT(通用属性规范) ATT(属性协议) L2CAP(逻辑链路控制) SM(安全管理) HCI(主机控制器接口) Controller(控制器层:LL + PHY) 硬件实现,通常不动 通过HCI与Host通信

GAP与GATT:两个核心角色

GAP和GATT是BLE协议栈里最常打交道的两个规范。很多新手搞混它们——我简单说:GAP管“能不能找到你”,GATT管“找到之后能干什么”。

GAP(通用访问规范)

GAP定义了设备在无线环境中的角色和行为。说白了,就是决定你的设备是“喊话的”还是“听喊的”。

GAP角色 行为描述 UWB场景举例
Broadcaster(广播者) 只发广播,不建立连接 UWB标签持续广播位置信息
Observer(观察者) 只扫描广播,不发起连接 定位基站扫描标签广播
Peripheral(外设) 可被连接,通常发广播 UWB手环等待手机连接
Central(中心) 主动扫描并发起连接 手机APP连接手环读取数据

避坑指南: 我曾经在一个项目中把UWB标签设成了Peripheral角色,结果基站扫描时发现广播包时有时无。查了半天才发现——Peripheral角色在连接状态下会停止广播!如果你的UWB标签需要持续广播位置,请使用Broadcaster角色,或者做好连接状态下的广播策略切换。

GATT(通用属性规范)

GATT定义了连接建立后的数据交互方式。它基于ATT协议,用“属性(Attribute)”来表达数据。我习惯把GATT理解成一个“树形数据库”:

  • Service(服务):一组相关功能的集合。比如“UWB测距服务”
  • Characteristic(特征):具体的数据点。比如“距离值”、“置信度”
  • Descriptor(描述符):特征的额外信息。比如“单位是米”、“格式是float32”

举个例子,我在一个项目中定义了这样的GATT结构:

UWB Positioning Service (UUID: 0xFFE0)
  ├── Distance Characteristic (UUID: 0xFFE1)
  │   ├── Value: 2.35 (单位: 米)
  │   └── Client Characteristic Configuration Descriptor
  └── Confidence Characteristic (UUID: 0xFFE2)
      └── Value: 95 (单位: 百分比)

嗯,这里要注意——GATT的角色是独立的。GAP里你是Central,GATT里你可能是Server(提供数据的一方)。别搞混了。

BLE广播与扫描机制

广播和扫描是BLE最基础也最关键的机制。对于UWB定位来说,广播就是“喊出自己的位置”,扫描就是“听别人报位置”。

广播机制

BLE广播分两种:

  1. 可连接广播(Connectable Advertising):广播者允许其他设备连接。用于Peripheral角色。
  2. 不可连接广播(Non-connectable Advertising):只发数据,不接受连接。用于Broadcaster角色。

广播包的结构是这样的:

广播信道PDU:
  ├── 前导码 (1字节)
  ├── 访问地址 (4字节, 固定0x8E89BED6)
  ├── PDU头 (2字节)
  │   ├── PDU类型 (4位)
  │   ├── 发送地址类型 (1位)
  │   └── 长度 (6位)
  ├── 广播地址 (6字节)
  └── 广播数据 (0-31字节)
      ├── AD Structure 1
      │   ├── 长度 (1字节)
      │   ├── AD类型 (1字节)
      │   └── AD数据 (N字节)
      ├── AD Structure 2
      └── ...

广播数据最多31字节。对于UWB定位来说,这31字节就是你的“黄金空间”。我一般这样分配:

字段 字节数 说明
Flags 3 BLE标准必填
Local Name 6 设备标识,比如"UWB-TAG"
UWB Distance 4 float32,单位米
UWB Confidence 1 uint8,0-100%
Reserved 17 备用,可扩展

我的习惯: 广播间隔设置很关键。间隔太短(比如20ms)会大量占用信道,导致UWB测距受干扰;间隔太长(比如1s)又会导致定位延迟大。我一般从100ms起步,根据实际场景微调。记得在广播数据里加一个序列号字段,方便接收端判断是否丢包。

扫描机制

扫描分两种模式:

  • 被动扫描(Passive Scanning):只监听广播包,不发任何数据。省电,但无法获取扫描响应。
  • 主动扫描(Active Scanning):监听到广播后,发送扫描请求,获取扫描响应数据。数据量更大,但功耗也高。

对于UWB定位基站,我建议使用被动扫描。为什么?因为基站通常需要同时监听几十个标签,主动扫描会引发大量扫描请求,造成信道拥塞。你想想看,如果每个标签都回复扫描响应,那广播信道就炸了。

扫描窗口和扫描间隔也需要配合好:

// 扫描参数配置示例
// 扫描窗口: 30ms
// 扫描间隔: 100ms
// 这意味着每100ms内,有30ms在监听,70ms在休眠

struct ble_gap_disc_params scan_params = {
    .itvl = 100,    // 扫描间隔,单位0.625ms
    .window = 48,   // 扫描窗口,单位0.625ms (30ms)
    .filter_policy = BLE_GAP_DISC_FILT_NO_FILT,
    .limited = 0,
    .passive = 1    // 被动扫描
};

我曾经踩过的坑: 扫描窗口设得太大(比如100ms),结果UWB测距模块的SPI通信频繁被BLE中断,测距数据直接乱掉。后来我把扫描窗口压缩到20ms,并且错开UWB测距的时序,问题才解决。记住——BLE和UWB共用射频前端时,一定要做好时分调度。

广播信道与跳频

BLE在2.4GHz频段有40个信道,但广播只在3个信道上进行:37(2402MHz)、38(2426MHz)、39(2480MHz)。为什么只选3个?

原因很简单——避开Wi-Fi的常用信道。2.4GHz Wi-Fi通常用1、6、11信道,而BLE的37、38、39正好卡在Wi-Fi信道的间隙里。这样设计,广播包被Wi-Fi干扰的概率就大大降低了。

广播者会在3个信道上轮流发送广播包。接收端只需要在这3个信道上轮流扫描,就能保证不漏包。这个机制叫“跳频”,但广播阶段的跳频是固定的,不像连接后的跳频那么复杂。

BLE广播信道与跳频时序 CH 37 (2402 MHz) CH 38 (2426 MHz) CH 39 (2480 MHz) 广播事件时序(间隔 T_adv) CH37 CH38 CH39 CH37 CH38 CH39 CH37 CH38 CH39 一个广播事件 = 3个信道依次发送 T_adv 通常 20ms ~ 10.24s

小结

这一章我们把BLE协议栈的骨架搭起来了。记住几个关键点:

  • 协议栈分三层,我们主要工作在Host和Application层
  • GAP管“能不能找到你”,GATT管“找到之后能干什么”
  • 广播数据只有31字节,UWB定位数据要精打细算
  • 广播信道只有3个,跳频是固定的
  • 扫描参数要和UWB测距时序协调好,别打架