1. BLE 协议栈概述

各位好,我是老李。调试蓝牙项目这么多年,有个问题一直让我头疼——GATT 特征读写回调用着用着就内存泄漏了。你排查半天,最终发现根子往往埋在整个协议栈的哪一层里。所以今天第一课,咱们先把 BLE 协议栈的底子铺清楚,尤其是 GATT 层到底在哪儿干活的。

1.1 从物理到应用——分层不是闹着玩的

BLE 协议栈,说白了就是一套把数据从芯片天线搬到 App 里面去的规矩。我习惯把它理解成「四层楼」:

  • Controller(控制器)——最底层,管射频、定时、收包发包。
  • Host(主机)——中间核心,管连接、拆包、安全、服务发现。
  • Application(应用)——你的代码,读写特征值、处理通知。
  • HCI(主机控制器接口)——Controller 和 Host 之间的通道,实际可以是 UART、USB 或者 SPI。

你想想看,如果你直接怼物理层写应用,那不得疯?分层把复杂度藏起来了,但每层之间的回调、缓冲区、内存分配也就成了泄漏的温床。

核心观点:GATT 位于 Host 层之上,它不直接管蓝牙怎么连,只管「怎么读怎么写」这台蓝牙设备上的数据。

BLE协议栈四层结构 Application(应用) 你的GATT回调代码 GATT(通用属性配置文件) 特征读写/通知/发现服务 ATT(属性协议) 属性操作命令与响应 L2CAP(逻辑链路与适配协议) 分片/重组/流量控制 HCI(主机控制器接口) 🔍 关注重点 GATT回调泄漏 常发生在此处 及上方应用层

1.2 每一层都在干什么?

我列个表,方便你对照着看。曾有同事问我:「我写个 notify 回调,怎么也跟 LL 层有关系?」——你看了表就明白了。

协议层 主要职责 泄漏风险点
PHY(物理层) 2.4G 射频调制、解调 一般无泄漏(硬件直接处理)
LL(链路层) 连接状态机、重传、跳频 少量的 packet buffer 泄漏
HCI 命令/事件透传、数据分片 HCI command buffer 未释放
L2CAP 分段重组、通道管理 SDU 缓冲区滞留
ATT 属性操作(Read/Write/Notify) 属性值实际占用内存
GATT 服务/特征发现、回调事件分发 【经典泄漏点】注册回调后忘记注销、上下文对象未释放

1.3 GATT 层到底特殊在哪?

你从表里能看出来,GATT 层是个「协管员」。它自己不存数据,但它负责调度数据的走向。举个例子:

  • 当手机发来一个「Write Request」——GATT 收到 ATT 层的 ATT_WRITE_REQ 事件,然后根据 handle 找到对应的特征,调用你注册的 write_callback。
  • 如果你的 callback 里 malloc 了缓冲区,却因为流程异常没 free——泄漏了。而且 GATT 层在粘合时可能会额外分配内存来拼装参数,这点很容易被忽略。

我个人的排查习惯:遇到回调泄漏,先看 GATT 层的事件处理函数——它有没有把参数值深拷贝一份传给你?那份拷贝是谁负责释放?很多芯片 SDK 的文档都写得很模糊。

1.4 避坑指南:协议栈版本差异

不同厂商的 BLE 协议栈实现略有不同。我踩过的坑是:某家芯片把 GATT 回调里的 connection_handle 做成了指针,你如果不按文档说的 memcpy 出来,它就会在下一帧被覆盖。结果你以为回调参数没变,实际内存早乱了。

  • TI CC13xx/CC26xx:GATT 回调在 ICall 上下文中执行,需要特别注意堆栈大小和消息释放。
  • Nordic nRF5:softdevice 自动管理 ATT 缓冲区,但 GATT 事件中的 ble_gatts_evt_t 是栈上变量,必须在回调退出前复制或用完。
  • ESP32 Bluedroid:GATT 事件分派在主循环,注意锁、队列和动态分配。

看清你的芯片,再谈排查。

1.5 为什么一定要理解这层架构?

因为泄漏不会只出现在你写的回调函数里。你想想看:

  1. 调用 sd_ble_gatts_hvx() 通知数据——这个 api 内部会走 GATT → ATT → L2CAP → HCI,每层都可能分配临时内存。
  2. 如果中途连接断开了,系统是否清理了正在传输中的通知缓冲区?有些协议栈只清理了 ATT 层,GATT 层还在等 ACK,结果泄漏了。
  3. 你的 write callback 里收到一个大数据包,GATT 层可能做了分片合并,合完后的那块内存是否交给应用了?如果应用没释放,那就算协议栈的 bug。

警告: 千万不要以为「GATT回调泄漏」只是应用层的事。我曾遇到过因为 L2CAP 的 MTU 协商失败导致 ATT 缓冲区一直挂在队列里,最后 GATT 层的 pending request 永远无法释放。查了两天才定位到。

好了,这一章就聊到这儿。协议栈分层好比你的工具箱——每层工具不同,但只有搞清楚哪个螺丝刀对应哪个螺丝,漏油了才知道拧哪里。下一课咱们会深入 GATT 的特征注册机制,看看回调到底是怎么被「栓」上去的。