串口仿真协议(RFCOMM)
本笔记为作者再学习蓝牙Host协议栈的一些心得体会,如有不对的地方,请包含与谅解!
------------by wsoz
串口仿真协议(RFCOMM)
串口仿真协议(RFCOMM)是蓝牙协议栈中非常重要的一层,它在L2CAP之上模拟RS-232串口通信,为上层应用提供可靠的串行数据流。
服务模型定义
RFCOMM的服务模型定义了设备类型、通信场景以及协议提供的服务接口。
设备类型
RFCOMM定义了两种设备类型:
| 类型 | 名称 | 说明 |
|---|---|---|
| Type 1 | 纯蓝牙设备(通信终端) | 只通过蓝牙进行通信的终端设备,如蓝牙耳机、蓝牙键盘 |
| Type 2 | 网关设备(中转/网关) | 同时支持蓝牙和有线串口(RS-232)的设备,可作为蓝牙与传统串口设备之间的桥梁 |
图示:
Type 1 设备 Type 2 设备(网关)
┌─────────────┐ ┌─────────────────────┐
│ 应用层 │ │ 应用层 │
├─────────────┤ ├──────────┬──────────┤
│ RFCOMM │ │ RFCOMM │ 串口驱动 │
├─────────────┤ ├──────────┼──────────┤
│ L2CAP │ │ L2CAP │ ↓ │
├─────────────┤ ├──────────┤ RS-232 │
│ 蓝牙基带 │ │ 蓝牙基带 │ 接口 │
└─────────────┘ └──────────┴──────────┘
通信场景
根据设备类型的组合,存在三种通信场景:
场景A:Type 1 ↔ Type 1(纯蓝牙通信)
┌─────────┐ 蓝牙 ┌─────────┐
│ Type 1 │ ←------------→ │ Type 1 │
│ (耳机) │ │ (手机) │
└─────────┘ └─────────┘
场景B:Type 1 ↔ Type 2(蓝牙设备通过网关连接有线设备)
┌─────────┐ 蓝牙 ┌─────────┐ RS-232 ┌─────────┐
│ Type 1 │ ←------------→ │ Type 2 │ ←----------→ │ 传统串口│
│ (手机) │ │ (网关) │ │ 设备 │
└─────────┘ └─────────┘ └─────────┘
场景C:Type 2 ↔ Type 2(两个网关互联,实现无线串口延长)
┌────────┐ RS-232 ┌─────────┐ 蓝牙 ┌─────────┐ RS-232 ┌────────┐
│ 设备A │ ←------→ │ Type 2 │ ←----→ │ Type 2 │ ←------→ │ 设备B │
└────────┘ └─────────┘ └─────────┘ └────────┘
服务接口
RFCOMM向上层提供的服务类似于TCP Socket:
| 特性 | 说明 |
|---|---|
| 可靠传输 | 基于L2CAP的可靠传输机制 |
| 面向连接 | 使用前必须先建立连接 |
| 字节流 | 提供无边界的字节流服务(与TCP类似) |
| 多路复用 | 一条蓝牙链路上可同时运行多个虚拟串口通道 |
DLCI(数据链路连接标识符)
RFCOMM使用DLCI(Data Link Connection Identifier) 来标识不同的虚拟串口连接:
| 项目 | 说明 |
|---|---|
| 作用 | 区分同一蓝牙链路上的多个RFCOMM连接 |
| 范围 | 2-61(可用于应用),0和1保留用于控制 |
| 计算 | DLCI = Server Channel × 2 + Direction Bit |
简单理解:DLCI就像端口号,让一条蓝牙连接可以同时跑多个"虚拟串口"。
RFCOMM帧格式
RFCOMM的帧格式继承自GSM 07.10,所有二进制数字都按照从低位到高位的顺序,从左至右读,每一帧的整体结构如下:
简单概念
谁先发起RFCOMM会话,谁就是Initiator,对方就是Responder。
关键点:
- 判定条件是在**DLCI=0(控制通道)**上谁先发SABM
- 这个身份一旦确定就不变,整个RFCOMM会话期间都保持
- 这个身份会影响后续帧中C/R位的取值(前面帧格式中提到的)
帧整体结构
┌─────────┬─────────┬──────────┬─────────────┬──────┐
│ Address │ Control │ Length │ Information │ FCS │
│ (1字节) │ (1字节) │(1-2字节) │ (0-N字节) │(1字节)│
└─────────┴─────────┴──────────┴─────────────┴──────┘
| 字段 | 长度 | 说明 |
|---|---|---|
| Address | 1字节 | 地址字段,包含DLCI和控制位 |
| Control | 1字节 | 帧类型标识 |
| Length | 1或2字节 | Information字段的长度 |
| Information | 0~N字节 | 数据载荷(有些帧类型没有此字段) |
| FCS | 1字节 | 帧校验序列(Frame Check Sequence) |
字节序列
RFCOMM的字节和位传输顺序:
-
字节序:按从左到右的顺序发送(即帧结构图中从左到右)
-
位序 :每个字节从LSB(最低有效位,Bit 0) 开始发送
发送顺序:Address → Control → Length → Information → FCS
每个字节内的位发送顺序:
Bit 0 → Bit 1 → Bit 2 → ... → Bit 7
(LSB优先)
Address 字段(1字节)
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Server Channel (5 bits)│ D │ C/R│ EA │
│ S4 │ S3 │ S2 │ S1 │ S0 │ │ │ │
└────┴────┴────┴────┴────┴────┴────┴────┘
├──────── DLCI (6 bits) ─────────────┤
| 位 | 名称 | 说明 |
|---|---|---|
| Bit 0 | EA(扩展地址位) | RFCOMM中固定为 1(表示地址只有1字节) |
| Bit 1 | C/R(命令/响应位) | 区分命令帧和响应帧 |
| Bit 2 | D(Direction Bit) | 方向位,DLCI的最低位 |
| Bit 3-7 | Server Channel(5位) | 服务通道号,DLCI的高5位,即上层协议的rfcomm channel |
DLCI = D + (Server Channel << 1),共6位(Bit 2~7)。
D位规则(Initiator / Responder):
在DLCI=0上先发送SABM的一方为Initiator ,回复UA的一方为Responder。
| 场景 | D值 | DLCI计算 |
|---|---|---|
| Initiator连接Responder的服务 | D = 0 | DLCI = 0 + Server Channel << 1 |
| Responder连接Initiator的服务 | D = 1 | DLCI = 1 + Server Channel << 1 |
D位的作用:让同一个Server Channel在两个方向上产生不同的DLCI,避免冲突。
C/R位规则:
| 发送方 | 命令帧 | 响应帧 |
|---|---|---|
| Initiator | C/R = 1 | C/R = 0 |
| Responder | C/R = 0 | C/R = 1 |
发起方:最初建立RFCOMM会话(DLCI=0)的那一端。
Control 字段(1字节)
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 1 │ 2 │ 3 │ 4 │ P/F│ 6 │ 7 │ 8 │
└────┴────┴────┴────┴────┴────┴────┴────┘
↑
P/F 位
Control字段中Bit 4 为P/F位(Poll/Final),其余位组合定义帧类型。
P/F位含义:
| 帧方向 | 名称 | 含义 |
|---|---|---|
| 命令帧中 | P(Poll) | P=1 表示要求对方必须回复 |
| 响应帧中 | F(Final) | F=1 表示这是对P=1命令的回复 |
各帧类型中P/F的使用:
| 帧类型 | P/F值 | 说明 |
|---|---|---|
| SABM | P = 1 | 必须置1,要求对方回复UA或DM |
| DISC | P = 1 | 必须置1,要求对方回复UA或DM |
| UA | F = 1 | 必须置1,作为对SABM/DISC的回复 |
| DM | F = 1 | 必须置1,作为拒绝回复 |
| UIH | 通常 = 0 | 数据帧一般不需要强制回复;在Credit-Based流控中P/F=1有特殊含义(携带Credit) |
简单理解:P/F是一个"必须回话"机制。命令帧设P=1表示"你必须回我",响应帧设F=1表示"这就是我的回复"。
Control字段定义了帧的类型(去掉P/F位后的值),RFCOMM使用以下5种帧类型:
| 帧类型 | 基础值(P/F=0) | 实际值(P/F=1) | 全称 | 用途 |
|---|---|---|---|---|
| SABM | 0x2F | 0x3F | Set Asynchronous Balanced Mode | 请求建立连接 |
| UA | 0x63 | 0x73 | Unnumbered Acknowledgement | 确认连接建立/断开 |
| DM | 0x0F | 0x1F | Disconnected Mode | 拒绝连接或表示未连接 |
| DISC | 0x43 | 0x53 | Disconnect | 请求断开连接 |
| UIH | 0xEF | 0xFF | Unnumbered Information with Header check | 数据传输(最常用) |
计算方法:实际值 = 基础值 | 0x10(P/F位在Bit 4)。SABM/DISC/UA/DM的P/F必须为1,UIH的P/F通常为0(携带Credit时为1)。
典型交互流程:
建立连接: A --SABM--> B --UA--> A (请求 → 确认)
数据传输: A --UIH---> B (发送数据)
断开连接: A --DISC--> B --UA--> A (断开 → 确认)
拒绝连接: A --SABM--> B --DM--> A (请求 → 拒绝)
Length 字段(1或2字节)
Length字段表示后面Information字段的字节数,长度本身可以是1字节或2字节:
1字节格式(EA=1,长度 ≤ 127):
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Length (7 bits) │ EA │
│ L6 │ L5 │ L4 │ L3 │ L2 │ L1 │ L0 │ 1 │
└────┴────┴────┴────┴────┴────┴────┴────┘
2字节格式(EA=0,长度 ≤ 32767):
第1字节: 第2字节:
Bit: 7 ... 1 0 Bit: 7 ... 0
┌────────────┬────┐ ┌─────────────────┐
│ L6 ... L0 │ 0 │ │ L14 ... L7 │
└────────────┴────┘ └─────────────────┘
| EA位 | 长度字段大小 | 最大长度值 |
|---|---|---|
| EA = 1 | 1字节 | 127 (0x7F) |
| EA = 0 | 2字节 | 32767 (0x7FFF) |
判断规则:看Length第1字节的Bit 0(EA位),为1则只有1字节,为0则有2字节。
Information 字段(0~N字节)
数据载荷,内容取决于帧类型:
| 帧类型 | Information内容 |
|---|---|
| SABM | 无(长度为0) |
| UA | 无(长度为0) |
| DM | 无(长度为0) |
| DISC | 无(长度为0) |
| UIH | 用户数据 或 多路复用控制命令(当DLCI=0时) |
FCS 字段(1字节)
帧校验序列,用于校验帧的完整性。
校验范围因帧类型不同:
| 帧类型 | 校验范围 |
|---|---|
| UIH | 仅校验 Address + Control(2字节) |
| 其他帧(SABM/UA/DM/DISC) | 校验 Address + Control + Length(3或4字节) |
UIH帧只校验头部,不校验Length和数据,这样即使长度字段传错,校验也能通过------设计目的是优先保证数据传输效率,因为L2CAP底层已有CRC校验。
完整帧示例
以DLCI=10的UIH数据帧为例,发送"Hi"(0x48 0x69):
Address: 0x29 → DLCI=10, C/R=0, EA=1
二进制: 00101001
Bit 7-2: 001010 (DLCI=10)
Bit 1: 0 (C/R)
Bit 0: 1 (EA)
Control: 0xEF → UIH帧
Length: 0x05 → 长度=2, EA=1
二进制: 00000101
Bit 7-1: 0000010 (长度=2)
Bit 0: 1 (EA)
Information: 0x48 0x69 → "Hi"
FCS: 0xXX → 对 Address + Control 计算得出
完整帧:29 EF 05 48 69 XX
多路控制通道控制命令
当RFCOMM帧的DLCI=0 时,UIH帧的Information字段不再承载用户数据,而是承载多路复用控制命令(Multiplexer Control Commands)。这些命令用于管理RFCOMM会话本身,主要是用来控制连接。
控制命令的帧格式
控制命令嵌套在UIH帧的Information字段中,格式如下:

UIH帧(DLCI=0)的 Information 字段:
┌──────────┬──────────────┬──────────────┐
│ Type │ Length │ Value │
│(1-2字节) │ (1-2字节) │ (0-N字节) │
└──────────┴──────────────┴──────────────┘
Type字段:
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 命令类型 (6 bits) │ C/R│ EA │
│ T7 │ T6 │ T5 │ T4 │ T3 │ T2 │ │ │
└────┴────┴────┴────┴────┴────┴────┴────┘
- Bit 0(EA):固定为1
- Bit 1(C/R):1=命令,0=响应
- Bit 2-7:命令类型编码
Length字段:与帧格式中的Length字段规则相同(EA位决定1或2字节)。
控制命令类型
| 命令 | Type值 | 全称 | 用途 |
|---|---|---|---|
| PN | 0x20 | Parameter Negotiation | 协商DLC参数(帧大小、流控方式、Credit等) |
| MSC | 0x38 | Modem Status Command | 传递虚拟串口的控制信号(DTR/DSR/RTS/CTS等) |
| RPN | 0x24 | Remote Port Negotiation | 协商串口参数(波特率、数据位、停止位等) |
| RLS | 0x14 | Remote Line Status | 报告线路错误状态 |
| Test | 0x08 | Test Command | 测试连接(回环测试) |
| FCoff | 0x18 | Flow Control Off | 请求对方停止发送所有DLC的数据 |
| FCon | 0x28 | Flow Control On | 允许对方恢复发送数据 |
| NSC | 0x04 | Non-Supported Command | 回复不支持的命令类型、 |
**注意:**RFCOMM的断开需要先断开其他的channel之后,确保其他通道关闭之后才可以关闭DLCI=0的通道,因此比如要关闭SPP以及RFCOMM的话就需要进行2次断开交互。
常用命令详解
PN(参数协商)
在建立DLC(发送SABM)之前,通常先用PN命令协商该通道的参数。
PN命令的Value字段(固定8字节):
| 字节偏移 | 名称 | 说明 |
|---|---|---|
| 0 | DLCI | 要协商的通道号 |
| 1 | I/CL | 收敛层类型(决定流控方式) |
| 2 | Priority | 优先级(0-63) |
| 3 | T1 | 定时器(RFCOMM中未使用,填0) |
| 4-5 | N1 | 最大帧大小(小端序),默认127 |
| 6 | N2 | 最大重传次数(RFCOMM中未使用,填0) |
| 7 | K | 初始Credit值(对方可发送的帧数) |
I/CL字段(字节1):
| CL值(高4位) | 含义 |
|---|---|
| 0x0 | 不使用Credit流控 |
| 0xE | 请求使用Credit-Based流控 |
| 0xF | 确认使用Credit-Based流控(响应中使用) |
PN命令示例:协商DLCI=6,Credit流控,最大帧大小330,初始Credit=7
Type: 0x83 → PN命令(0x20), C/R=1(命令), EA=1
Bit 2-7: 100000 (PN=0x20)
Bit 1: 1 (C/R=命令)
Bit 0: 1 (EA)
Length: 0x11 → 长度=8, EA=1
Bit 1-7: 0001000 (长度=8)
Bit 0: 1 (EA)
Value (8字节):
偏移0: 0x06 → DLCI=6
偏移1: 0xE0 → CL=0xE(请求Credit流控), I=0x0
偏移2: 0x00 → Priority=0
偏移3: 0x00 → T1=0(未使用)
偏移4: 0x4A → N1低字节 (330 = 0x014A)
偏移5: 0x01 → N1高字节
偏移6: 0x00 → N2=0(未使用)
偏移7: 0x07 → K=7(初始Credit)
完整PN命令:83 11 06 E0 00 00 4A 01 00 07
对方PN响应格式相同,区别:Type的C/R=0(响应),CL=0xF(确认Credit流控)。
MSC(Modem状态命令)
模拟RS-232的控制信号,是RFCOMM仿真串口的核心命令:
| 信号 | 说明 |
|---|---|
| RTC(DTR/DSR) | 终端就绪 |
| RTR(RTS/CTS) | 请求发送/允许发送 |
| IC(RI) | 振铃指示 |
| DV(DCD) | 载波检测 |
MSC命令在DLC建立后必须发送,用于通知对方虚拟串口的初始状态。
RPN(串口参数协商)
RPN(Remote Port Negotiation)命令用于协商串口参数:
| 参数 | 默认值 |
|---|---|
| 波特率 | 9600 |
| 数据位 | 8 |
| 停止位 | 1 |
| 校验 | 无 |
| 流控 | 无 |
实际使用情况:
| 场景 | 是否需要RPN | 原因 |
|---|---|---|
| Type 1 ↔ Type 1(纯蓝牙) | 不需要 | 底层是蓝牙链路,波特率等参数无实际意义,速率由蓝牙决定 |
| 涉及Type 2(网关) | 可能需要 | 网关需要配置真实RS-232串口的参数 |
结论 :大多数蓝牙应用(如SPP串口透传)不发送RPN命令,直接使用默认值。RPN命令在实际抓包中很少见到。应用层自己知道数据格式,不需要RFCOMM层关心。
控制命令交互示例
建立一个RFCOMM通道(Server Channel=3)的完整流程:
设备A (Initiator) 设备B (Responder)
│ │
│ ① SABM (DLCI=0) │ 建立控制通道
│ ────────────────────────────────→ │
│ UA (DLCI=0) │
│ ←──────────────────────────────── │
│ │
│ ② PN命令 (协商DLCI=6的参数) │ 参数协商
│ ────────────────────────────────→ │ (DLCI=0+3<<1=6)
│ PN响应 │
│ ←──────────────────────────────── │
│ │
│ ③ SABM (DLCI=6) │ 建立数据通道
│ ────────────────────────────────→ │
│ UA (DLCI=6) │
│ ←──────────────────────────────── │
│ │
│ ④ MSC命令 (DLCI=6) │ 交换串口状态
│ ────────────────────────────────→ │
│ MSC响应 │
│ ←──────────────────────────────── │
│ MSC命令 │
│ ←──────────────────────────────── │
│ MSC响应 │
│ ────────────────────────────────→ │
│ │
│ ⑤ UIH (DLCI=6, 用户数据) │ 开始数据传输
│ ←─────────────────────────────→ │
连接步骤总结:
-
第①步:建立控制通道(DLCI=0)
Initiator发送SABM(DLCI=0)请求建立RFCOMM会话,Responder回复UA确认。此时双方确定了Initiator/Responder身份,控制通道建立完成。
-
第②步:参数协商(PN命令)
通过DLCI=0的控制通道,Initiator发送PN命令协商数据通道(DLCI=6)的参数,包括最大帧大小、是否使用Credit流控、初始Credit值等。Responder回复PN响应确认参数。
-
第③步:建立数据通道(DLCI=6)
参数协商完成后,Initiator发送SABM(DLCI=6)请求建立数据通道,Responder回复UA确认。此时虚拟串口通道建立完成。
-
第④步:交换串口状态(MSC命令)
双方通过MSC命令交换虚拟串口的控制信号状态(DTR/DSR/RTS/CTS等)。这一步是必须的,模拟真实串口的握手过程。
-
第⑤步:数据传输(UIH帧)
通道就绪后,双方可以通过UIH帧(DLCI=6)互相发送用户数据,实现串口通信。
Credit-Based流控机制(补充)
Credit-Based流控是RFCOMM推荐的流控方式,相比传统的FCon/FCoff流控更加高效和精确。
Credit字段的出现条件
Credit字段并非总是存在于UIH帧中,需要满足以下两个条件:
条件1:启用Credit-Based流控
在PN命令协商时,通过I/CL字段的CL值(高4位)决定是否使用Credit流控:
| CL值(高4位) | 含义 | Credit字段 |
|---|---|---|
| 0x0 | 不使用Credit流控 | UIH帧永远没有Credit字段 |
| 0xE | 请求使用Credit流控(Initiator在PN Command中使用) | 协商成功后UIH帧可能有Credit字段 |
| 0xF | 确认使用Credit流控(Responder在PN Response中使用) | 协商成功后UIH帧可能有Credit字段 |
条件2:UIH帧的P/F位=1
即使启用了Credit流控,也不是每个UIH帧都携带Credit字段:
P/F=0(Control=0xEF):不携带Credit
┌─────────┬─────────┬──────────┬─────────────┬──────┐
│ Address │ Control │ Length │ Information │ FCS │
│ 0x29 │ 0xEF │ 0x09 │ Data │ 0xXX │
└─────────┴─────────┴──────────┴─────────────┴──────┘
P/F=1(Control=0xFF):携带Credit
┌─────────┬─────────┬────────┬──────────┬─────────────┬──────┐
│ Address │ Control │ Credit │ Length │ Information │ FCS │
│ 0x29 │ 0xFF │ 0x05 │ 0x09 │ Data │ 0xXX │
└─────────┴─────────┴────────┴──────────┴─────────────┴──────┘
P/F=1 → Credit字段插入在Control和Length之间
头部长度计算:
c
/* RFCOMM头部长度宏定义 */
#define RFCOMM_HDR_LEN_1 3 // Address(1) + Control(1) + Length(1)
#define RFCOMM_HDR_LEN_2 4 // Address(1) + Control(1) + Length(2)
// 如果使用Credit流控且P/F=1,需要额外加1字节Credit
// 实际头部长度 = RFCOMM_HDR_LEN_X + (credit_based ? 1 : 0)
Credit机制原理
基本概念:
| 术语 | 说明 |
|---|---|
| Credit(信用值) | 发送方可以发送的帧数量(不是字节数),每发送1个UIH帧消耗1个Credit |
| 初始Credit | 在PN命令的K字段中协商,双方各自给对方分配初始Credit |
| Credit补充 | 接收方通过UIH帧(P/F=1)补充Credit给发送方 |
| 双向独立 | A→B方向的Credit由B管理,B→A方向的Credit由A管理,互不影响 |
完整流程示例:
设备A 设备B
│ │
│ ① PN Cmd (K=7) │ A请求初始Credit=7
│ ─────────────────────────────────────→│
│ ② PN Rsp (K=7) │ B同意,给A 7个Credit
│←─────────────────────────────────────│
│ │
│ A的Credit余额:7 │
│ B的Credit余额:7 │
│ │
│ ③ UIH (P/F=0, "Data1") │ A发送数据,消耗1个Credit
│ ─────────────────────────────────────→│
│ A的Credit余额:6 │
│ │
│ ④ UIH (P/F=0, "Data2") │ A继续发送
│ ─────────────────────────────────────→│
│ A的Credit余额:5 │
│ │
│ ... 继续发送 ... │
│ │
│ ⑩ UIH (P/F=0, "Data7") │ A发送第7帧
│ ─────────────────────────────────────→│
│ A的Credit余额:0 │
│ │
│ ← A必须停止发送,等待B补充Credit ← │
│ │
│ ⑪ UIH (P/F=1, Credit=5, "Response") │ B补充5个Credit给A
│←─────────────────────────────────────│
│ A的Credit余额:5 │
│ │
│ ⑫ UIH (P/F=0, "Data8") │ A可以继续发送了
│ ─────────────────────────────────────→│
│ A的Credit余额:4 │
Credit补充策略:
接收方可以根据自身缓冲区状态选择补充时机:
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 每收到N帧补充 | 固定收到N帧后补充N个Credit | 简单,可预测 | 可能频繁补充 |
| 缓冲区空闲时补充 | 缓冲区处理完后一次性补充较多Credit | 减少补充次数 | 可能让发送方等待 |
| Credit快耗尽时提前补充 | 检测到≤阈值时提前补充 | 避免发送方等待 | 需要额外逻辑 |
Credit流控的好处
好处1:防止缓冲区溢出
没有流控的问题:
发送方(快) 接收方(慢)
│ 发送速度:1000帧/秒 │ 处理速度:100帧/秒
│ ─────────────────────────────────→│ 缓冲区
│ ─────────────────────────────────→│ ┌──────┐
│ ─────────────────────────────────→│ │ 满了!│
│ │ └──────┘
│ │ 数据丢失!
有Credit流控:
发送方 接收方
│ Credit=7 │
│ ─────────────────────────────────→│ 缓冲区
│ ...(发送7帧) │ ┌──────┐
│ Credit=0,停止发送 │ │ 7帧 │
│ │ └──────┘
│ │ 处理数据...
│ Credit=5 │ ┌──────┐
│←─────────────────────────────────│ │ 空闲 │
│ 继续发送 │ └──────┘
好处2:端到端精确流控
传统FCon/FCoff的问题:
发送方 接收方
│ 发送数据... │ 缓冲区满
│ ─────────────────────────────────→│
│ FCoff │ 请求停止
│←─────────────────────────────────│
│ ← 但已经发送了很多帧在路上 ← │ 这些帧还是会到达
│ │ 可能导致溢出
Credit机制:
发送方 接收方
│ Credit=0 │
│ ← 立即停止,不会发送多余帧 ← │
│ 等待Credit补充... │
│ Credit=5 │
│←─────────────────────────────────│
│ 精确发送5帧 │
好处3:更高效
| 特性 | FCon/FCoff | Credit-Based |
|---|---|---|
| 控制粒度 | 全部停止/全部恢复 | 精确控制帧数 |
| 响应速度 | 需要等待FCon命令 | 可以提前补充 |
| 开销 | 需要额外的控制帧 | 可以在数据帧中携带 |
| 可预测性 | 不可预测何时恢复 | 明确知道能发多少帧 |
示例:数据和Credit一起发送
接收方既要发送数据,又要补充Credit:
方案1(FCon/FCoff):需要2个帧
① FCon Command(控制帧)
② UIH(数据帧)
方案2(Credit-Based):只需1个帧
① UIH (P/F=1, Credit=5, Data)
既发送了数据,又补充了Credit
Credit流控的启用时机
重要 :Credit-Based流控不是在HCI初始化时启用 ,而是在RFCOMM连接建立时 通过PN命令协商。
时序图:
HCI初始化 → ACL连接 → L2CAP连接 → RFCOMM DLCI=0
→ PN命令协商(这里!)→ RFCOMM DLCI=N → 数据传输
协商流程:
Initiator Responder
│ │
│ SABM (DLCI=0) │ 建立控制通道
│ ─────────────────────────────────→│
│ UA (DLCI=0) │
│←─────────────────────────────────│
│ │
│ PN Command │ ← 在这里协商!
│ ┌─────────────────────────┐ │
│ │ DLCI = 10 │ │
│ │ CL = 0xE (请求Credit) │ │
│ │ K = 7 (初始Credit=7) │ │
│ └─────────────────────────┘ │
│ ─────────────────────────────────→│
│ │
│ PN Response │
│ ┌─────────────────────────┐ │
│ │ DLCI = 10 │ │
│ │ CL = 0xF (确认Credit) │ │
│ │ K = 7 (同意Credit=7) │ │
│ └─────────────────────────┘ │
│←─────────────────────────────────│
│ │
│ 协商完成,使用Credit流控 │
配置位置:
在RFCOMM层配置,而非HCI层:
c
// RFCOMM层的配置
typedef struct {
bool use_credit_flow_control; // 是否使用Credit流控
uint8_t initial_credit; // 初始Credit值
uint16_t max_frame_size; // 最大帧大小
} rfcomm_config_t;
// 全局配置
rfcomm_config_t rfcomm_config = {
.use_credit_flow_control = true, // 默认启用
.initial_credit = 7, // 默认7个Credit
.max_frame_size = 330 // 默认330字节
};
协商结果的可能性:
| 场景 | Initiator请求 | Responder响应 | 结果 |
|---|---|---|---|
| 双方都支持 | CL=0xE, K=7 | CL=0xF, K=7 | 使用Credit流控 |
| Responder不支持 | CL=0xE, K=7 | CL=0x0, K=0 | 回退到FCon/FCoff流控 |
| Initiator不需要 | CL=0x0, K=0 | CL=0x0, K=0 | 使用FCon/FCoff流控 |
实现要点
发送方逻辑
c
// 发送方的Credit管理
typedef struct {
uint8_t tx_credit; // 当前可用Credit
uint8_t dlci; // 通道号
bool credit_based; // 是否启用Credit流控
} rfcomm_channel_t;
void on_pn_response(rfcomm_channel_t *channel, uint8_t initial_credit) {
channel->tx_credit = initial_credit; // 初始化Credit
}
bool can_send_frame(rfcomm_channel_t *channel) {
if (!channel->credit_based) {
return true; // 非Credit模式,总是可以发送
}
return channel->tx_credit > 0; // Credit模式,有Credit才能发送
}
void send_uih_frame(rfcomm_channel_t *channel, uint8_t *data, int len) {
if (!can_send_frame(channel)) {
// Credit耗尽,加入等待队列
queue_pending_data(channel, data, len);
return;
}
// 构造UIH帧(P/F=0,不携带Credit)
uint8_t frame[256];
frame[0] = make_address(channel->dlci);
frame[1] = 0xEF; // P/F=0
frame[2] = len;
memcpy(&frame[3], data, len);
// 发送帧
send_rfcomm_frame(frame, 3 + len);
// 消耗1个Credit
if (channel->credit_based) {
channel->tx_credit--;
}
}
void on_receive_credit(rfcomm_channel_t *channel, uint8_t credit) {
channel->tx_credit += credit; // 补充Credit
// 发送等待队列中的数据
send_pending_data(channel);
}
接收方逻辑
c
// 接收方的Credit管理
typedef struct {
uint8_t rx_credit_given; // 已给出的Credit
uint8_t rx_frames_received; // 已接收的帧数
uint8_t dlci;
bool credit_based;
} rfcomm_channel_t;
void on_pn_command(rfcomm_channel_t *channel, uint8_t initial_credit) {
channel->rx_credit_given = initial_credit; // 记录给出的Credit
}
void on_receive_uih_frame(rfcomm_channel_t *channel, uint8_t *data, int len) {
if (channel->credit_based) {
channel->rx_frames_received++;
}
// 处理数据
process_data(data, len);
// 检查是否需要补充Credit
if (channel->credit_based) {
int credit_used = channel->rx_frames_received;
int credit_remaining = channel->rx_credit_given - credit_used;
if (credit_remaining <= 2) { // Credit快用完了
// 补充Credit
int credit_to_give = 10;
send_uih_with_credit(channel, credit_to_give);
channel->rx_credit_given += credit_to_give;
}
}
}
void send_uih_with_credit(rfcomm_channel_t *channel, uint8_t credit) {
uint8_t frame[256];
frame[0] = make_address(channel->dlci);
frame[1] = 0xFF; // P/F=1,表示携带Credit
frame[2] = credit; // Credit字节
frame[3] = data_len; // Length字段
// ... 后续数据
send_rfcomm_frame(frame, frame_len);
}
与HCI流控的区别
容易混淆的是,HCI层也有流控机制,但那是完全不同的机制:
| 特性 | HCI Flow Control | RFCOMM Credit Flow Control |
|---|---|---|
| 层次 | HCI层(Host↔Controller) | RFCOMM层(设备↔设备) |
| 控制对象 | ACL数据包 | RFCOMM帧 |
| 配置时机 | HCI初始化时 | RFCOMM连接建立时 |
| 机制 | Number of Completed Packets事件 | Credit字段 |
| 目的 | 防止Controller缓冲区溢出 | 防止对端应用层缓冲区溢出 |
HCI Flow Control示例:
Host Controller
│ HCI ACL Data │
│ ─────────────────────────────────→│ ACL缓冲区
│ HCI ACL Data │ ┌──────┐
│ ─────────────────────────────────→│ │ 满了 │
│ │ └──────┘
│ Number of Completed Packets │ ← 通知Host已处理
│←─────────────────────────────────│
│ 继续发送... │
这是HCI层的流控,与RFCOMM的Credit流控是两个独立的机制,分别工作在不同的协议层。
总结
Credit-Based流控的核心要点:
- 启用条件:在PN命令协商时通过CL字段启用,不是在HCI初始化时
- Credit字段:只有当UIH帧的P/F=1时才携带Credit字段
- 双向独立:每个方向的Credit由接收方管理
- 精确控制:每发送1帧消耗1个Credit,Credit=0时必须停止
- 高效补充:可以在数据帧中携带Credit,减少控制开销
优势:
- 防止缓冲区溢出
- 端到端精确流控
- 减少控制帧开销
- 可预测的发送行为
与传统流控对比:
- FCon/FCoff:全局控制,粗粒度,需要额外控制帧
- Credit-Based:精确控制,细粒度,可在数据帧中携带
这就是为什么Credit-Based流控是RFCOMM推荐的流控方式!