串口仿真协议(RFCOMM)

串口仿真协议(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 4P/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流控的核心要点

  1. 启用条件:在PN命令协商时通过CL字段启用,不是在HCI初始化时
  2. Credit字段:只有当UIH帧的P/F=1时才携带Credit字段
  3. 双向独立:每个方向的Credit由接收方管理
  4. 精确控制:每发送1帧消耗1个Credit,Credit=0时必须停止
  5. 高效补充:可以在数据帧中携带Credit,减少控制开销

优势

  • 防止缓冲区溢出
  • 端到端精确流控
  • 减少控制帧开销
  • 可预测的发送行为

与传统流控对比

  • FCon/FCoff:全局控制,粗粒度,需要额外控制帧
  • Credit-Based:精确控制,细粒度,可在数据帧中携带

这就是为什么Credit-Based流控是RFCOMM推荐的流控方式!

相关推荐
恶魔泡泡糖3 小时前
stm32核心板子使用验证与串口下载
stm32·单片机·嵌入式硬件
会编程的小孩3 小时前
指针应用 单片机
单片机·嵌入式硬件
HalvmånEver3 小时前
Linux:基于 UDP Socket 的实战项目 --简单双向通信程序
linux·单片机·udp
FreakStudio12 小时前
不用装软件!这款MicroPython浏览器 IDE :让你在手机上也能调试树莓派 Pico
python·单片机·嵌入式·电子diy·tinyml
yuan1999715 小时前
STM32远程升级系统(Bootloader + 上位机)
stm32·单片机·嵌入式硬件
Heartache boy15 小时前
野火STM32_HAL库版课程笔记-ADC多通道采集热敏、光敏、反射传感器(轮询)
笔记·stm32·单片机
AI+程序员在路上16 小时前
嵌入式软件技术大全
linux·开发语言·arm开发·单片机
秀秀更健康17 小时前
STM32的程序下载不进去----VDDA悬空
stm32·单片机·嵌入式硬件
我在人间贩卖青春19 小时前
DMA的应用
单片机·dma·gpdma