第4章 BLE协议栈之L2CAP:数据快递的核心枢纽

L2CAP,全称逻辑链路控制与适配协议。名字挺长,但说白了,它就是BLE协议栈里的“数据快递公司”。

我在做第一个BLE项目时,对L2CAP的理解就停留在“它能把数据包切碎再拼回去”。后来踩了坑才发现,它的作用远不止这些。今天咱们就把它彻底聊透。

一句话总结:L2CAP位于HCI之上、ATT/GATT之下,负责把上层应用的数据,封装成适合底层传输的格式,并管理多个逻辑信道。

4.1 L2CAP层在协议栈中的位置

先看一张图,你就明白它站在哪了。

BLE协议栈分层结构 APP / Profile GATT / ATT L2CAP(逻辑链路控制与适配) ← 本章核心 HCI(主机控制器接口) Link Layer(链路层) PHY(物理层) L2CAP 是上层应用与底层传输之间的“适配层”,负责数据封装与信道管理

你看,L2CAP夹在ATT和HCI之间。上层应用的数据,先交给ATT,再交给L2CAP打包,最后通过HCI送到蓝牙芯片。反过来,收到的数据也是L2CAP先拆包,再往上送。

4.2 L2CAP的核心功能

L2CAP主要干四件事。我按重要程度排个序:

  1. 协议/信道复用——多个上层协议共用同一条物理链路
  2. 分段与重组——大包切小、小包拼大
  3. MTU管理——协商双方能接受的最大数据包尺寸
  4. 服务质量(QoS)——BLE里基本没用,但经典蓝牙里很重要

咱们重点聊前三个。QoS在BLE里几乎被砍掉了,我后面会解释。

4.3 面向连接与无连接信道

L2CAP支持两种信道类型。你想想看,这就像打电话和发广播的区别。

信道类型 特点 BLE中使用 典型场景
面向连接信道(COC) 一对一、可靠、有流量控制 ✅ 常用 ATT协议、数据透传
无连接信道(CL) 一对多、不可靠、无确认 ❌ 基本不用 广播信标(由Link Layer处理)

面向连接信道,说白了就是建立一条专属通道。数据按顺序到达,丢了会重传。BLE里ATT协议就跑在COC上。

无连接信道呢?发出去就不管了,对方收没收到都不知道。经典蓝牙里L2CAP支持这个,但BLE里几乎用不到——因为广播数据直接在Link Layer就处理了,根本不需要L2CAP插手。

我的经验:刚开始学BLE时,我总纠结“无连接信道在哪用”。后来发现,BLE的广播和扫描都在Link Layer完成,L2CAP层根本不需要操心。所以,你只要记住COC就够了。

4.4 MTU与分段重组

MTU,最大传输单元。这是L2CAP里最容易出坑的地方。

BLE的Link Layer规定,一个数据包最大只有251字节(实际上有效载荷更少)。但上层应用可能一次要发几百甚至几千字节。怎么办?L2CAP负责切。

举个例子:

  • 应用层要发1000字节
  • L2CAP把它切成4个包:247 + 247 + 247 + 259
  • 每个包加上L2CAP头(4字节),交给Link Layer发送
  • 接收端L2CAP收到后,按顺序拼回1000字节,再交给上层

这个过程叫分段与重组(SAR)。分段在发送端,重组在接收端。

关键点:BLE的L2CAP默认MTU是23字节。你没看错,只有23字节!这是为了兼容最老的BLE设备。但实际项目中,我们都会协商更大的MTU,比如247字节,甚至512字节。

MTU协商是怎么做的?通过L2CAP的连接参数更新请求。双方交换自己支持的MTU大小,取最小值作为最终值。

// 伪代码:MTU协商过程
发起方: 我支持 MTU = 247
响应方: 我支持 MTU = 512
结果: 双方使用 MTU = min(247, 512) = 247

我曾经在一个项目中,MTU没协商好,导致数据总是发不完整。查了两天才发现,对端设备只支持默认23字节,而我这边按247字节发,数据被截断了。从那以后,我每次做BLE项目,第一件事就是确认MTU协商结果。

避坑指南:MTU协商不是自动的!你需要主动发起L2CAP连接参数更新请求。很多低端BLE芯片默认不协商,一直用23字节。如果你发现传输速度上不去,先检查MTU。

4.5 L2CAP在BLE中的简化

经典蓝牙的L2CAP很复杂,支持重传、流控、QoS、多路复用等等。但BLE为了省电和简单,砍掉了大量功能。

具体砍了什么?

功能 经典蓝牙 BLE 原因
重传与流控 ✅ 完整支持 ❌ 砍掉 由Link Layer负责
QoS ✅ 支持 ❌ 砍掉 BLE不保证带宽
多路复用 ✅ 完整 ✅ 简化版 只支持5个固定信道
分段重组 ✅ 完整 ✅ 保留 核心功能
无连接信道 ✅ 支持 ❌ 砍掉 由Link Layer广播替代

你看,BLE的L2CAP就是个“瘦身版”。它只保留了最基本的功能:信道复用和分段重组。其他的,要么交给下层(Link Layer),要么直接砍掉。

我个人觉得,这种设计很聪明。BLE的目标就是低功耗、低成本,没必要把协议栈搞得太重。你想想看,一个智能灯泡,需要QoS吗?不需要。能亮、能关、能调色就够了。

4.6 L2CAP信道与CID

每个L2CAP信道都有一个唯一的标识符,叫CID(信道ID)。就像快递单号一样,用来区分不同的数据流。

BLE里预定义了5个固定CID:

  • 0x0004 — ATT协议(最常用)
  • 0x0005 — L2CAP信令信道(用于MTU协商等)
  • 0x0006 — 安全管理协议(SMP)
  • 0x0001~0x0003 — 保留/未使用

动态CID(0x0040~0x007F)用于自定义数据通道。比如你做一个数据透传应用,就可以申请一个动态CID。

小技巧:在nRF5 SDK或Zephyr中,你可以通过bt_l2cap_chan_register()注册自定义L2CAP信道。但大多数情况下,用ATT就够了,不需要直接操作L2CAP。

4.7 实际项目中的L2CAP配置

说了这么多理论,咱们看看实际代码里L2CAP怎么配。以Zephyr RTOS为例:

// 初始化L2CAP信道
struct bt_l2cap_chan my_chan;
struct bt_l2cap_chan_ops my_ops = {
    .recv = my_recv_cb,
    .connected = my_connected_cb,
    .disconnected = my_disconnected_cb,
};

// 注册信道
bt_l2cap_chan_register(&my_chan, &my_ops, BT_L2CAP_PSM_ATT);

// 发起MTU协商
bt_l2cap_chan_update_mtu(&my_chan, 247);

这段代码做了三件事:

  1. 定义信道和回调函数
  2. 注册到ATT协议(PSM=0x001F)
  3. 请求MTU升级到247字节

嗯,这里要注意:MTU协商是异步的。你调用bt_l2cap_chan_update_mtu()后,对端可能同意,也可能拒绝。结果通过回调函数通知你。

4.8 本章小结

L2CAP在BLE里虽然被简化了,但它的作用不可替代。没有它,上层应用的数据就没法被正确封装和传输。

记住几个关键点:

  • L2CAP负责信道复用和分段重组
  • BLE只用面向连接信道(COC)
  • 默认MTU只有23字节,必须主动协商
  • BLE的L2CAP是经典蓝牙的瘦身版