3. 低功耗蓝牙协议栈架构
在 nRF52 系列上开发低功耗蓝牙产品,最先要建立的一个认知是:蓝牙协议栈并不是“一堆库函数”,而是一块被 Nordic 预先编译、烧录并受保护的二进制固件——SoftDevice。应用代码(App)与 SoftDevice 之间通过一套固定的 API 和事件回调交互,二者在 Flash 与 RAM 上严格分区,运行时通过中断与调度器协作。理解这套分区与分层,是后续做低功耗、做量产、做协议栈版本升级的前提。
SoftDevice 的定位与 S140/S132/S113 的角色划分
SoftDevice 是 Nordic 提供的、经过蓝牙 SIG 认证的协议栈二进制,它把从物理层到 GATT 的整条链路都封装起来,对外只暴露 sd_* 开头的 API。应用不能直接访问协议栈内部结构,只能通过 API 调用和事件回调与之通信。这种“黑盒 + 接口”的设计换来了认证一致性和稳定性,代价是应用必须遵守它的内存布局与调度规则。
三个常见型号的差异,本质上是“支持的角色数量”和“协议特性”的差异,而不是性能高低:
- S132:支持 Central(主机)、Peripheral(从机)、Observer、Broadcaster 全部四种 GAP 角色,可同时做多连接的主从混合,适合网关、多连接采集器。
- S140:同样支持全部四种角色,但面向 nRF52840 等更大 Flash/RAM 的器件,支持蓝牙 5 的长距离编码 PHY、2M PHY、扩展广播等特性,是当前新项目的主流选择。
- S113:只支持 Peripheral 和 Broadcaster 角色,即“只能做从机/广播者”,代码体积和 RAM 占用最小,适合单一从机产品,如传感器节点、手环。
选型判断很直接:产品是否需要主动扫描、主动发起连接、是否需要同时连多个设备?需要就选 S132/S140;只做被连接的外设,S113 足够且更省资源。注意 SoftDevice 型号与芯片型号、SDK 版本、协议栈版本三者必须匹配,量产时固件包要锁定这三者的组合。
分层模型:从射频到应用的数据通路
蓝牙协议栈自下而上可分为 Controller 与 Host 两大部分,SoftDevice 把两者都包含在内。应用位于最上层,通过 GATT/GAP API 与协议栈交互。各层职责如下:
- PHY / Link Layer:负责射频收发、广播与连接事件的时序、跳频、CRC、加密的链路层部分。连接间隔、从机延迟、监督超时这些低功耗关键参数都在这一层生效。
- L2CAP:逻辑链路控制与适配,负责把上层数据分段/重组,管理信道(CID)。BLE 中 ATT 就承载在固定的 L2CAP CID 0x0004 上。MTU 协商的结果直接影响 ATT 每次能传多少字节。
- SM(Security Manager):负责配对与密钥分发,决定配对方式(Just Works、Passkey、OOB)、是否绑定、密钥如何存储。它和 GAP 的广播/连接流程紧密耦合。
- ATT:属性协议,定义 Client/Server 角色和 Read/Write/Notify/Indicate 等操作,是 GATT 的传输基础。
- GATT:在 ATT 之上组织 Service、Characteristic、Descriptor 的层次结构,规定数据如何被组织和发现。
- GAP:负责广播、扫描、发起连接、连接参数管理,是“设备如何被看见、如何建立连接”的层。
数据方向可以这样理解:应用调用 sd_ble_gatts_hvx() 发送通知,数据经 GATT → ATT → L2CAP 分段 → Link Layer 排队,在下一个连接事件发出;对端收到后反向逐层上传,最终以 BLE_GATTC_EVT_HVX 事件交给对端应用。这条通路上的每一层都可能成为吞吐瓶颈,例如 MTU 太小、连接间隔太长、通知队列满。
事件驱动机制:应用如何与协议栈协作
SoftDevice 采用事件驱动模型。协议栈在内部完成射频收发、协议解析后,把结果封装成 ble_evt_t 结构,通过 ble_evt_handler() 回调交给应用。应用必须在这个回调里根据 evt_id 做分支处理,例如:
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED: // 连接建立,保存 conn_handle
m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
break;
case BLE_GAP_EVT_DISCONNECTED: // 断开,重新开始广播
m_conn_handle = BLE_CONN_HANDLE_INVALID;
advertising_start();
break;
case BLE_GATTS_EVT_WRITE: // 对端写入特征值
// 解析 p_ble_evt->evt.gatts_evt.params.write
break;
case BLE_GATTC_EVT_HVX: // 收到通知/指示
break;
default:
break;
}
}
这里有几个工程上必须注意的点。第一,回调运行在协议栈的调度上下文,不能在里面做耗时操作(如长延时、大块 Flash 擦写),否则会阻塞协议栈、丢连接事件。第二,conn_handle 是连接的唯一标识,多连接场景下所有 GATT 操作都要带上它,不能用一个全局变量假设只有一条连接。第三,事件是异步的:调用 sd_ble_gap_adv_start() 只是“请求开始广播”,真正开始广播会以 BLE_GAP_EVT_ADV_SET_TERMINATED 或连接事件的形式反馈,应用不能假设调用返回即已生效。
分层与角色的对应关系
把分层和角色放在一起看,会更容易理解 API 的组织方式。GAP 层决定设备是 Peripheral 还是 Central,这直接决定你调用的是 sd_ble_gap_adv_start() 还是 sd_ble_gap_connect()。GATT 层决定设备是 Server 还是 Client:Peripheral 通常做 GATT Server,把自己的传感器数据组织成 Service 供对端读取或订阅;Central 通常做 GATT Client,去发现和读写对端的服务。ATT 的 Client/Server 角色与 GATT 的 Client/Server 角色一致,而 SM 则在连接建立后按需触发配对流程。
一个典型的从机传感器产品,其数据流是:上电 → 配置 GAP 广播参数(设备名、UUID、发射功率)→ 开始广播 → 手机连接 → 触发 BLE_GAP_EVT_CONNECTED → 手机发现服务 → 手机写 CCCD 订阅通知 → 应用定时采集传感器 → 调用 sd_ble_gatts_hvx() 发送通知 → 手机收到数据。整条链路里,应用只负责“采集 + 调 API + 处理事件”,协议栈负责其余全部细节。
ble_evt_handler 入口打印 evt_id 和 conn_handle,可以快速看清连接生命周期;用 nRF Connect 手机 App 观察服务发现和 CCCD 写入过程,能直观对应到 GATT/ATT 层的行为。掌握这套分层与事件模型后,后续的连接参数优化、MTU 协商、配对绑定、低功耗设计,都可以落到具体的层和具体的 API 上,而不是停留在“调一下协议栈”的模糊层面。