3. L2CAP协议数据单元(PDU)格式与通道管理

L2CAP(Logical Link Control and Adaptation Protocol)层位于HCI之上、上层协议(如RFCOMM、ATT、SMP)之下,承担数据封装、协议复用与服务质量(QoS)适配的核心职责。本章深入剖析L2CAP PDU的四种帧结构、通道标识符(CID)分配规则以及MTU/MPS概念,为后续的驱动调试与性能优化奠定基础。

工程提示:在Linux内核蓝牙驱动(net/bluetooth/l2cap_core.c)中,PDU的解析与组装是L2CAP层最频繁执行的路径。理解帧格式的每一个字段,是定位“粘包”、“断帧”、“CID冲突”等经典问题的前提。

3.1 L2CAP PDU帧类型总览

L2CAP定义了四种PDU帧类型,分别用于不同的数据传输与控制场景:

帧类型 全称 主要用途 CID范围
B-frame Basic Information Frame 面向无连接/面向连接的数据传输(Basic Mode) 动态CID(0x0040–0xFFFF)
I-frame Information Frame ERTM/SM模式下的数据重传与确认 动态CID
S-frame Supervisory Frame ERTM模式下的流量控制与状态报告(RR/RNR/REJ) 动态CID
C-frame Control Frame / Command Frame 信令通道:连接建立、配置、断开(Command Reject, Connection Request, Configuration Request等) 固定CID:0x0001(信令通道)
关键区分:B-frame是L2CAP最基础的帧格式,所有模式都支持;I-frame和S-frame仅在增强重传模式(ERTM)和流模式(SM)下使用;C-frame始终通过固定CID 0x0001传输,不依赖动态通道。

3.2 B-frame(Basic Information Frame)详细结构

B-frame是L2CAP最基本的PDU格式,结构最为简洁,由固定长度头部和可变长度载荷组成:

B-frame 格式(字节序:Little-Endian):
+--------+--------+--------+--------+------------------+
| 长度 (2B) | CID (2B)  | 载荷 (0–65535 B)       |
+--------+--------+--------+--------+------------------+
| 头部 (4B)  | 信息载荷 (Payload)          |
+--------+--------+--------+--------+------------------+

字段说明:
- 长度(Length):2字节,表示后续载荷的字节数(不含头部自身)。
- CID(Channel Identifier):2字节,标识目标通道。
- 载荷(Payload):0–65535字节,上层协议数据(如RFCOMM帧、ATT PDU)。
边界条件:B-frame的“长度”字段为0是合法的,表示空载荷帧,用于某些协议层的保活或探测。驱动中必须正确处理零载荷帧,避免缓冲区越界。
B-frame 结构示意图 长度 (2B) 0x0010 (16字节) CID (2B) 0x0040 (动态) 载荷 (Payload) 0–65535 字节 (上层协议数据) 固定头部 (4B) 信息载荷 B-frame头部仅4字节,开销最小,适用于Basic Mode下的高效数据传输

3.3 I-frame 与 S-frame:ERTM模式下的增强帧

在ERTM(Enhanced Retransmission Mode)和SM(Streaming Mode)下,L2CAP使用I-frame和S-frame替代B-frame,以支持序列号、确认、重传和流控。

3.3.1 I-frame(Information Frame)

I-frame在B-frame基础上增加了控制字(Control Word),用于携带发送序列号(TxSeq)和接收序列号(RxSeq):

I-frame 格式:
+--------+--------+--------+--------+--------+--------+------------------+
| 长度 (2B) | CID (2B)  | 控制字 (2B) | 载荷 (可变)                |
+--------+--------+--------+--------+--------+--------+------------------+
| 头部 (4B)  | 控制字段 (2B) | 信息载荷                    |
+--------+--------+--------+--------+--------+--------+------------------+

控制字位域(16位):
Bit 15–14: 帧类型 (01 = I-frame)
Bit 13:    保留
Bit 12–8:   TxSeq (发送序列号, 0–31)
Bit 7:      SAR (分段与重组标志)
Bit 6–0:    RxSeq (接收序列号, 0–31)

3.3.2 S-frame(Supervisory Frame)

S-frame不携带用户数据载荷,仅用于控制信令:

S-frame 格式:
+--------+--------+--------+--------+--------+--------+
| 长度 (2B) | CID (2B)  | 控制字 (2B) | 无载荷           |
+--------+--------+--------+--------+--------+--------+

控制字位域(16位):
Bit 15–14: 帧类型 (10 = S-frame)
Bit 13–12: 功能码 (00=RR, 01=RNR, 10=REJ, 11=SREJ)
Bit 11–8:  保留
Bit 7–0:    RxSeq (接收序列号)

功能码含义:
- RR (Receiver Ready):  确认已收到 N 号帧,准备接收更多。
- RNR (Receiver Not Ready): 确认已收到 N 号帧,但接收方忙。
- REJ (Reject):         拒绝 N 号帧,请求重传。
- SREJ (Selective Reject): 选择性拒绝 N 号帧,仅重传该帧。
调试要点:在Linux内核中,I-frame和S-frame的解析位于l2cap_data_channel()函数。当遇到l2cap_ertm_reassembly_sdu()报错时,应优先检查控制字中的TxSeq/RxSeq是否连续,以及SAR标志是否正确。
I-frame 与 S-frame 结构对比 I-frame (信息帧) 长度 (2B) CID (2B) 控制字 (2B) 载荷 (可变) TxSeq(5b) | SAR(1b) | RxSeq(7b) S-frame (监控帧) 长度 (2B) CID (2B) 控制字 (2B) 无载荷 功能码(2b) | 保留(4b) | RxSeq(8b) S-frame不携带用户数据,用于RR/RNR/REJ/SREJ流控与重传控制

3.4 C-frame(Control Frame / Command Frame)

C-frame用于L2CAP信令通道(CID = 0x0001),负责通道的建立、配置、断开等管理操作。其格式与B-frame类似,但载荷部分为命令/响应结构:

C-frame 格式(信令通道):
+--------+--------+--------+--------+------------------+
| 长度 (2B) | CID=0x0001 | 命令载荷 (可变)           |
+--------+--------+--------+--------+------------------+

命令载荷内部结构(可包含多个命令):
+--------+--------+--------+--------+------------------+
| 代码 (1B) | 标识符 (1B) | 数据长度 (2B) | 命令数据 (可变) |
+--------+--------+--------+--------+------------------+

常用命令代码:
- 0x01: Command Reject
- 0x02: Connection Request
- 0x03: Connection Response
- 0x04: Configure Request
- 0x05: Configure Response
- 0x06: Disconnection Request
- 0x07: Disconnection Response
- 0x08: Echo Request / Response
- 0x0A: Information Request / Response
工程实践:在l2cap_sig_channel()函数中,内核通过l2cap_build_conf_req()和l2cap_parse_conf_req()处理配置协商。调试时,可通过btmon或hcidump抓取C-frame,检查标识符(Identifier)是否匹配请求与响应。

3.5 CID(通道标识符)分配规则

CID是L2CAP层识别通道的唯一标识,分配规则严格遵循蓝牙核心规范:

CID范围 分配类型 用途 示例
0x0000 保留 无效通道(NULL Identifier) —
0x0001 固定 L2CAP信令通道(Signaling Channel) C-frame传输
0x0002 固定 无连接通道(Connectionless Channel) 广播数据
0x0003–0x000F 保留 未来规范使用 —
0x0010–0x003F 固定 AMP(Alternate MAC/PHY)管理通道 AMP Manager
0x0040–0xFFFF 动态 动态分配的数据通道(面向连接) RFCOMM、ATT、SMP
常见错误:在驱动开发中,若动态CID分配器(l2cap_global_chan_by_scid())返回NULL,通常是因为CID已被占用或未正确注册。检查l2cap_chan_add()调用时是否传入了正确的本地SCID和远程DCID。

3.6 MTU与MPS概念

MTU(Maximum Transmission Unit)和MPS(Maximum PDU Payload Size)是L2CAP层两个极易混淆但至关重要的参数:

参数 全称 定义 作用范围 典型值
MTU Maximum Transmission Unit L2CAP层单帧所能承载的最大上层SDU大小(含头部) 通道级别(per-channel) 默认672字节,最大65535字节
MPS Maximum PDU Payload Size L2CAP层单帧中载荷(Payload)的最大字节数(不含头部) 通道级别(per-channel) 默认672字节,受Controller Buffer限制

核心区别:

  • MTU 是上层视角:上层协议(如RFCOMM)一次最多能发送多少数据给L2CAP。
  • MPS 是底层视角:L2CAP一次最多能封装多少数据放入一个PDU中发送给Controller。
  • 当上层SDU大小 > MPS时,L2CAP必须进行分段(Segmentation)与重组(Reassembly)(SAR机制)。
配置协商:在L2CAP Configuration Request/Response中,双方交换MTU和MPS值。最终生效的MTU = min(本地MTU, 对端MTU);MPS = min(本地MPS, 对端MPS, Controller Buffer大小)。在l2cap_chan结构体中,对应字段为imtu(入向MTU)、omtu(出向MTU)和mps。
MTU 与 MPS 概念关系 上层SDU (如RFCOMM帧) — 最大 MTU 字节 SAR分段 (若SDU > MPS) L2CAP头部 (4B) 载荷 (≤ MPS 字节) 头部 PDU 1 载荷 (≤ MPS) MTU = 上层SDU最大长度(含头部) MPS = 单个PDU中载荷最大长度(不含头部) 当 SDU > MPS 时,L2CAP 自动分段为多个PDU

3.7 工程实践:PDU解析与调试要点

  1. 帧类型识别:在l2cap_chan_recv()中,通过bt_cb(skb)->l2cap.btf或直接解析控制字的高2位判断帧类型(01=I-frame, 10=S-frame, 00=B-frame)。
  2. CID合法性检查:在l2cap_global_chan_by_scid()中,若返回NULL,打印scid值并检查是否在0x0040–0xFFFF范围内。
  3. MTU/MPS越界:在l2cap_do_send()中,若SDU长度 > chan->omtu,应返回-EMSGSIZE;若PDU载荷 > chan->mps,必须触发SAR分段。
  4. 信令超时:C-frame的标识符(Identifier)必须唯一,且响应中的Identifier必须与请求一致。在l2cap_sig_timeout()中,超时后应重发或断开通道。
内核调试命令:
# cat /sys/kernel/debug/bluetooth/hci0/l2cap — 查看当前所有L2CAP通道的CID、MTU、MPS、状态。
# btmon -w trace.log — 抓取完整的HCI+L2CAP数据流,用于离线分析PDU格式。