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模块。
来看一张协议栈架构图,帮你建立整体印象:
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广播分两种:
- 可连接广播(Connectable Advertising):广播者允许其他设备连接。用于Peripheral角色。
- 不可连接广播(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协议栈的骨架搭起来了。记住几个关键点:
- 协议栈分三层,我们主要工作在Host和Application层
- GAP管“能不能找到你”,GATT管“找到之后能干什么”
- 广播数据只有31字节,UWB定位数据要精打细算
- 广播信道只有3个,跳频是固定的
- 扫描参数要和UWB测距时序协调好,别打架