真正上手之后才发现,BLE 和经典蓝牙是完全不同的两套东西。
常见误解
我以前以为 BLE 只是省电版的蓝牙。其实 BLE 是专门为小数据量、周期性上报、靠纽扣电池长期工作设计的协议,它的省电来自绝大多数时间都在睡眠的连接机制,而不是物理层射频比经典蓝牙更省电。
我以前还觉得 BLE 传输速率太低传不了数据。BLE 4.2 的实际有效吞吐通常在每秒几 KB 到几十 KB,上报温湿度绰绰有余,但用来做固件升级就比较吃力,所以做 BLE OTA 必须专门设计 Flash 擦写时序和流控。
还有个坑是以为 BLE 连上就能直接收发数据。实际上能不能读某个数据、能不能收到主动推送,取决于设备端有没有暴露对应服务,以及手机客户端有没有往特征值的 CCC 描述符(0x2902)里写值打开通知开关。手机 App 连上设备却收不到数据,九成是没开这个通知开关。
协议栈分层
BLE 协议栈自上而下可以分为六层,和常见的网络协议对比着记更容易理解:
| 层级 | 作用 | 类比 |
|---|---|---|
| GAP | 设备怎么被发现、怎么建立连接、扮演什么角色 | TCP/IP 里的编址与会话管理 |
| GATT | 数据怎么组织,包含服务、特征值、描述符 | 应用层数据模型 |
| ATT | GATT 的传输载体,定义读、写、通知等操作原语 | 类似 HTTP 的请求与响应报文格式 |
| L2CAP | 协议复用、分包与重组 | TCP/UDP 端口复用与分段 |
| Link Layer | 广播、连接状态机、跳频与空中时序 | 链路层 |
| PHY | 2.4 GHz 频段,划分 40 个信道(3 个广播 + 37 个数据) | 物理层 |
GAP(Generic Access Profile)负责管连接过程,决定设备是发广播的外围设备(Peripheral)还是主动扫描的中心设备(Central),定义广播包里带什么名字、服务 UUID、能不能被连接,以及连上之后用多长的间隔通信。
GATT(Generic Attribute Profile)是写应用代码打交道最多的一层。设备端把所有数据组织成一棵树,客户端连上后按这棵树的结构来读写:
graph TD
Dev["BLE 设备"] --> S1["服务 Service
UUID 0x181A 环境传感"]
Dev --> S2["服务 Service
UUID 0xFFF0 厂商自定义"]
S1 --> C1["特征值 Characteristic
UUID 0x2A6E 温度"]
S1 --> C2["特征值 Characteristic
UUID 0x2A6F 湿度"]
C1 --> D1["描述符 CCC
0x2902 通知开关"]
C1 --> D2["描述符 用户描述
0x2901 名称"]
C2 --> D3["描述符 CCC
0x2902 通知开关"]
S2 --> C3["特征值 Characteristic
UUID 0xFFF1 控制命令"]
C3 --> D4["描述符 用户描述
0x2901 名称"]
这棵树里的三类节点各司其职:
- 服务(Service)把一组相关的数据打包在一起,靠 UUID 区分。UUID 分 16 位(蓝牙联盟定义的标准服务,比如电池服务
0x180F、环境传感0x181A)和 128 位(厂商自己随便定义)两种。 - 特征值(Characteristic)是真正装数据的节点,包含读写通知等权限属性和具体的字节数组,下面可以挂多个描述符。
- 描述符(Descriptor)用来存放特征值的附加配置。最常用的是
CCC(Client Characteristic Configuration,UUID0x2902),客户端往里写1开启通知(Notification),写2开启指示(Indication),写0关闭推送。
这套结构和 Modbus 寄存器映射很像,服务相当于功能码分组,特征值相当于保持寄存器,描述符相当于配置字。区别在于 Modbus 寄存器表是双方提前写死的静态表,而 BLE 的 GATT 表是设备端自己定义好,客户端连上后通过服务发现(Service Discovery)动态读出来的,客户端事先不需要知道表长什么样。
连接与数据流
BLE 通信分为广播和连接两个阶段,搞清楚这两个阶段分别干什么,遇到连上了没数据就知道从哪查起。
在广播阶段,外围设备在 37、38、39 三个广播信道上周期性发广播包,里面装着设备名称、标志位和部分服务 UUID。中心设备扫到广播后,感兴趣就发起连接请求。广播间隔通常设在 100 ms ~ 1 s 之间,间隔拉得越长越省电,但手机扫到它的平均等待时间也越久。
进入连接阶段后,双方按照约定好的连接间隔(Connection Interval,范围 7.5 ms ~ 4 s)周期性醒来收发数据。GATT 层的读、写、打开通知开关,全都在这个阶段通过 ATT 报文完成:
sequenceDiagram
participant C as 中心设备 (App)
participant P as 外围设备 (传感器)
P->>C: 广播包 (设备名 + 服务 UUID)
C->>P: 发起连接请求
Note over C,P: 连接建立
C->>P: 服务发现 (读取 GATT 表)
P-->>C: 返回服务 / 特征值 / 描述符列表
C->>P: 写 CCC (0x2902) = 1 使能通知
P-->>C: 写入成功响应
loop 每个连接间隔
P-->>C: 通知 Notification (推送特征值新数据)
end
Note over C,P: 双方均可主动断开连接
主动推送数据时的通知(Notification)和指示(Indication)有一点核心差别:
- 通知是发出去就完事了,不需要客户端回复,速度快但极端情况下可能丢包;
- 指示是发出去后必须等客户端回一个确认包,设备端收到确认才能发下一条,可靠但吞吐慢。平时上报温湿度等数据用通知,关键控制命令的回执用指示。
连接参数与功耗
一颗纽扣电池能不能撑一年,主要看下面三个连接参数怎么配:
- 连接间隔(Connection Interval)决定了两次通信事件相隔多久,范围
7.5 ms ~ 4 s。假设每次唤醒收发耗时 5 ms,间隔设为 1 s 时射频占空比只有 0.5%,设成 100 ms 占空比就涨到 5%,功耗直接差出十倍。 - 从机延迟(Slave Latency)允许外围设备在没有数据要发时直接跳过若干个连接事件继续睡觉。比如设成 4,代表设备最多可以连续跳过 4 次唤醒,到第 5 个连接间隔再起来跟主机打个招呼,平时没数据时能省下大量电。
- 监管超时(Supervision Timeout)定义了双方多久没收到任何回应才判定断开连接,一般设在 5 s 以上,必须大于
(1 + 从机延迟) × 连接间隔 × 2。
这三个参数的调整权在中心设备(比如手机或网关)手里,外围设备只能发连接参数更新请求表达偏好,同不同意由中心设备决定。这也是为什么同一个蓝牙设备连不同手机或网关时功耗有差异。
在实际物联网项目里,比较省电的做法是平时让设备请求 500 ms ~ 1 s 的长间隔加上 4 的从机延迟,应付日常慢速上报;等到要读历史记录或升固件时,由手机或网关主动把连接间隔切到 15 ~ 30 ms,传完大块数据再切回长间隔。
比较经典的实现是用 做的Airtag,每两秒才向周围的iPhone发送一次广播,一个纽扣电池可以维持一年的设备续航
实际吞吐量计算
BLE 4.2 默认单包有效载荷很短,ATT_MTU 默认只有 23 字节,扣掉 3 字节的 ATT 协议头,一包最多只能塞 20 字节的用户数据(即便打开底层数据长度扩展 DLE 把物理包扩到 251 字节,上层依然受 ATT_MTU 限制)。如果按默认 20 字节一包、连接间隔 100 ms 来传 4 KB 的固件,光来回发 200 多包就要耗掉 20 多秒。
提升手段有三个:
- 连接建立后立刻协商把
ATT_MTU调大(BLE 4.2 及以上最高支持 517 字节),这样 4 KB 数据发 8 包左右就能传完。 - 传大文件期间把连接间隔缩短到
15 ~ 30 ms。 - 每次发通知尽量把单个 MTU 塞满,别拆成三五个字节发一次。
到了 BLE 5.0 引入 2M PHY 后,物理层符号速率翻倍,配合大 MTU 和短间隔,实际传输速度能跑到上百 KB/s。不过 2M PHY 发射功耗更高、通信距离也会缩短,按实际需求开启即可。
配对与安全
BLE 4.0 和 4.1 的旧版配对机制存在容易被抓包破解的漏洞,到了 BLE 4.2 引入基于 ECDH 密钥交换的 LE Secure Connections 后才算真正安全。在单片机端写代码时需要注意四个细节:
- 配对方式有三种:Just Works(只加密不认证,防不住中间人攻击);一方带屏幕或键盘可以用 6 位数字配对码(Passkey Entry);双方都有屏幕则用数字比对(Numeric Comparison),安全性最高。
- 开启绑定(Bonding)后,配对交换的长期密钥(LTK)要存进片上 Flash,下次重连直接恢复加密跳过配对。写 Flash 时一定要加校验,防止掉电写坏半截数据导致后续永远连不上。
- 敏感特征值要在 GATT 表里配置访问权限(比如要求链路加密或带认证后才允许读写),中心设备没完成加密就来读写时协议栈会自动拒绝并返回认证不足错误。
- BLE 链路层加密只保护空中无线电这一段,数据进了手机操作系统就是明文。如果要传敏感业务配置或密钥,最好在应用层载荷里再套一层对称加密或签名。
硬件集成方案选型
往产品里加 BLE 功能主要有三种硬件形态:
| 方案 | 典型硬件 | 优势 | 局限 |
|---|---|---|---|
| 外挂串口 AT 模块 | 常见串口透传模块,主控 MCU 靠 UART 控制 | 不用学蓝牙协议栈,几天就能调通,射频认证现成 | 蓝牙连接状态不由主控直接管理,功耗受休眠机制制约,只能用固定 AT 指令集 |
| 双芯片架构(MCU + 蓝牙芯片) | 主控 MCU 搭配 BlueNRG、nRF52 或 ESP32-C3 | 主控算力与蓝牙分离,GATT 表与低功耗完全可控 | 两颗芯片物料成本高,需要维护两套固件 |
| 单芯片 SoC 直接跑业务 | nRF52、ESP32-C3、杰理或沁恒蓝牙 SoC | 物料成本最低,外围器件最精简 | 业务代码不能阻塞蓝牙底层协议栈时序 |
如果只是给现有的成熟单片机产品加个手机近场配参功能,外挂串口 AT 模块最省事,不用动原来的工程架构,通过串口发几条指令就能透传数据:
1 | // MCU 通过串口初始化 BLE 透传模块的典型流程 |
用串口 AT 模块主要防四个坑。第一是蓝牙状态不是主控自己管理的,到底连上没有、什么时候断开了、当前处于指令模式还是透传模式,全靠解析串口上报的字符串(比如 +CONNECTED)或者读状态引脚来猜,一旦串口丢字节或者透传数据里刚好撞上状态关键字,主控和模块的状态机就会失步,同步状态跟玄学一样;第二是模块休眠后串口唤醒有延迟,主控发的第一条指令很容易丢,发数据前要先拉唤醒脚或做应答握手;第三是空中蓝牙接收突发速率可能比串口波特率快,大批下发数据时要做好分包流控;第四是同一款模块不同批次可能刷了不同版本的固件,量产采购前必须让厂家锁死固件版本。
如果是从头画板子做新产品,或者追求极致的成本和纽扣电池续航,直接选单颗带 BLE 的 SoC(比如沁恒 CH58x/CH59x、杰理、ESP32-C3 或 nRF52)把业务代码和协议栈跑在一起划算得多,没必要为了省几行代码多花钱买两颗芯片。
常用调试工具
调 BLE 协议和排查断连问题,平时备好这三样工具就够了:
- 手机端装一个 nRF Connect(Android / iOS)或 LightBlue。连上板子后能直接看到整棵 GATT 树,手动点读写、打开
0x2902通知开关、改 MTU、看信号强度。一旦联调出问题,先用 nRF Connect 试一下,一分钟就能分清到底是单片机固件没发出来还是手机 App 代码写错了。 - 遇到偶发断连或通知收不到时,直接打开 Android 手机开发者选项里的「启用蓝牙 HCI 信息收集日志」,复现问题后把日志文件导出来丢进 Wireshark,所有的 ATT 读写请求、连接参数更新请求和断开原因码都能看得一清二楚。
- 只有在排查底层广播包发不出来、跳频异常或双方握手死活连不上这类射频与链路层问题时,才需要用到专门的硬件蓝牙抓包器(比如基于 nRF52840 Dongle 刷官方 Sniffer 固件配 Wireshark,几十块钱就能搞定,没必要去买几万块的商业协议分析仪)。