BLE Host层L2CAP -- 状态机

1. 为什么需要状态机?

L2CAP 的面向连接信道从生到死要经历一连串阶段:发起连接 → 等待应答 → 双向协商参数 → 传数据 → 拆链。不同阶段能处理哪些信令报文、收到报文后该干什么,如果没有统一的规定,各家芯片厂各写各的,互操作性就是灾难。

于是规范用状态机(State Machine) 来一锤定音:每条 L2CAP 面向连接信道对应一个独立的状态机实例 (收到一个 L2CAP_ConnectReq 就新建一个实例),状态机从 CLOSED 状态出生,描述它一生中每个状态下"来什么事件、做什么动作、去哪个状态"。

2. 六大状态总览

|----------------------|------------------------|--------------|
| 状态 | 一句话说明 | 类比 |
| CLOSED | 信道未连接 | 单身 |
| WAIT_CONNECT | 收到了连接请求,只能先回 "pending" | 被表白了,说"让我想想" |
| WAIT_CONNECT_RSP | 自己发出了连接请求,等对方答复 | 表白后等回复 |
| CONFIG | 双向协商配置选项(含多个子状态) | 谈判、签合同 |
| OPEN | 用户数据传输态 | 婚后正常生活 |
| WAIT_DISCONNECT | 已发出断链请求,等对方确认 | 提出离婚,等手续办完 |

整体流转图:

几个理解要点:

  • 进入 CONFIG 的三条路 :从 CLOSED(直接回 success)、从 WAIT_CONNECT(pending 之后决定接受)、从 WAIT_CONNECT_RSP(对方接受了你的请求),三条路统统先落到子状态 WAIT_CONFIG;
  • 离开 CONFIG 只有一条正路 :发起者路径(initiator path)和应答者路径(acceptor path)双双完成,才进 OPEN。一边谈妥了另一边还没谈,就得继续等;
  • 断链的两条路 :任何状态收到 DisconnectReq 都可以当场回 DisconnectRsp 然后直接 CLOSED(对方主动拆链不需要走 WAIT_DISCONNECT);只有自己主动 发起 CloseChannel_Req 才进 WAIT_DISCONNECT。

3. 逐状态事件表精讲

规范用"事件-条件-动作-下一状态"表格定义每个状态,下面挑干货讲:

3.1 CLOSED 状态

CLOSED 不是"什么都不理",它是信道的产房:

|----------------------------------------------------|---------------------------------------------------------------------------------------|
| 事件 | 处理 |
| OpenChannel_Req(本机上上层请求建链) | 发 ConnectReq → 进 WAIT_CONNECT_RSP |
| 收到 L2CAP_ConnectReq | 三选一:资源充足→回 success 进 CONFIG;需要时间→回 pending 进 WAIT_CONNECT;没资源/不批准→回 refused 留在 CLOSED |
| 收到 ConfigReq | 回 Command Reject(Invalid CID) |
| 收到 ConnectRsp / ConfigRsp / DisconnectRsp / Data | Ignore(静默丢弃) |

⚠️ 注意一个细节:CLOSED 状态收到 ConnectRsp 要忽略。因为 ConnectRsp 只对 WAIT_CONNECT_RSP 中的请求有意义,CLOSED 状态下收到它大概率是链路上残留的旧报文或对方状态错乱,不回绝、不报错,直接扔。

3.2 WAIT_CONNECT / WAIT_CONNECT_RSP

这两个状态是连接阶段的"等待位",有两条只在规范注释里、却非常实用的规则:

  • WAIT_CONNECT 状态下不处理 DisconnectReq 和 ConfigReq ------因为此时 Source CID / Destination CID 还没分配,报文无法关联到具体信道的状态机,自然无从处理;
  • WAIT_CONNECT_RSP 状态下不处理 DisconnectReq------同理。

其余信令(ConfigRsp、DisconnectRsp、Data)一律 Ignore。收到 ConnectRsp 时三分支:

  • success → 发出第一个 ConfigReq,进 CONFIG(WAIT_CONFIG 子状态);
  • pending → 继续留在 WAIT_CONNECT_RSP(同时应启动 ERTX 定时器,见第六节);
  • refused → 回 CLOSED,本次建链失败。

3.3 OPEN 状态

OPEN 是唯一能处理 L2CAP_Data 的状态,同时支持重配置(Reconfigure):

|--------------------------|-------------------------------------|-----------------------------|
| 事件 | 动作 | 下一状态 |
| SendData_Req | 按配置的模式(Basic/ERTM/Streaming)发数据 | OPEN |
| ReconfigureChannel_Req | 先把未发完的出向 SDU 传完 ,再发 ConfigReq | CONFIG(WAIT_CONFIG_REQ_RSP) |
| 收到 ConfigReq(参数可接受) | 传完 SDU,回 ConfigRsp(ok) | CONFIG(WAIT_SEND_CONFIG) |
| 收到 ConfigReq(不可接受) | 传完 SDU,回 ConfigRsp(fail) | OPEN(留在原地!) |
| 收到 DisconnectReq | 回 DisconnectRsp | CLOSED |

⭐ 两条容易被忽略的规则:

  1. "完成出向 SDU"是硬性要求 :进入重配置前,本地已拆包的所有 PDU 必须全部可靠送达并被对端确认,才允许切换到 CONFIG。也就是说重配置期间该方向数据流要被挂起(§7.1 也强调:重配置会话期间信道上所有数据流量应暂停,等配置结果出来再说);
  1. 收到不可接受的 ConfigReq 回 fail 后是回 OPEN 而不是断链------配合上一篇讲的"负响应可以带修改建议,触发新一轮配置请求",被拒不等于谈崩,只是这一轮没谈拢。

3.4 WAIT_DISCONNECT 状态

最简单的状态:只认 DisconnectReq(回 Rsp 后回 CLOSED)和 DisconnectRsp(回到 CLOSED),其余(ConnectRsp、ConfigRsp、Data)一律 Ignore。

4. CONFIG 状态的 8 个子状态

CONFIG 之所以复杂,是因为双向都要配置:每个设备同时扮演两个角色------

  • Initiator(发起者) :自己发 ConfigReq,等对方的 ConfigRsp;
  • Acceptor(应答者) :收对方的 ConfigReq,回 ConfigRsp。

两条路径可以先后进行,也可以完全并行,因此 CONFIG 内部拆成 8 个子状态:

|-------------------------|--------------------------------------------------------------|
| 子状态 | 含义(initiator 路径视角 / acceptor 路径视角) |
| WAIT_CONFIG | 已完成连接(或收到连接成功响应),但既没发配置请求,也没收到可接受的配置请求------一切尚未开始 |
| WAIT_SEND_CONFIG | acceptor 路径已收到可接受的请求并回成功;initiator 路径还没发起请求 |
| WAIT_CONFIG_REQ_RSP | initiator 已发出请求、还没等到成功响应;acceptor 还没收到可接受的请求 |
| WAIT_CONFIG_RSP | acceptor 路径已收尾(回过成功响应);initiator 还在等成功响应 |
| WAIT_CONFIG_REQ | initiator 路径已收尾(拿到成功响应);acceptor 还没收到可接受的请求 |
| WAIT_IND_FINAL_RSP | (仅 Lockstep)扩展流规范已交给本地 Controller,Controller 还没答复、最终配置响应也还没到 |
| WAIT_FINAL_RSP | (仅 Lockstep)Controller 已接受、自己也回了成功,就等对端的最终配置响应 |
| WAIT_CONTROL_IND | (仅 Lockstep)已收到对端成功响应,在等自己 Controller 接受或拒绝 |

主流程跳转图:

复制代码
WAIT_CONFIG ──(ConfigureChannel_Req: 发ConfigReq)──► WAIT_CONFIG_REQ_RSP
      │                                                      │
      │ (收到可接受的ConfigReq, 回ConfigRsp(success))           │
      ▼                                                      │
  WAIT_SEND_CONFIG ──(ConfigureChannel_Req: 发ConfigReq)──► WAIT_CONFIG_RSP
      │                                                      │
      │ (收到可接受的ConfigReq, 回ConfigRsp(success))           │ (收到对端成功ConfigRsp)
      ▼                                                      ▼
  WAIT_CONFIG_REQ ──────(收到可接受的ConfigReq)──────────────────────► OPEN ✅
                        (回ConfigRsp(success))

记忆口诀 :看子状态名里有没有 _REQ / _RSP 后缀------

  • 带 REQ_RSP:请求发出去了、响应没回来(initiator 未完成),同时 acceptor 侧也没就绪;
  • 带 _RSP:只剩"等对端的响应"(acceptor 已完成,initiator 未完成);
  • 带 _REQ:只剩"等对端的请求"(initiator 已完成,acceptor 未完成);
  • 两个都缺:WAIT_CONFIG / WAIT_SEND_CONFIG。

💡 通俗类比:谈婚论嫁得双方家长都点头。你这一家(initiator path)谈妥了、对方家(acceptor path)还没给话,你就在 WAIT_CONFIG_REQ 里干等;两家都点头(双向配置均成功),婚礼(OPEN)才开始。

非成功转换:负响应 ≠ 谈崩

CONFIG 内收到负面事件时,规范的处理相当"温和":

|----------------------------|-------------------------|------------|
| 事件 | 动作 | 状态变化 |
| 收到 ConfigReq 但参数不可接受 | 回 ConfigRsp(fail) | 留在原子状态 |
| 收到 ConfigRsp 且对方拒绝了自己的选项 | 修改参数重新发 ConfigReq | 留在原等待子状态 |
| 收到无关的 ConfigRsp | Ignore | 不变 |

这与上一篇的伏笔完全呼应:收到失败响应时,正确姿势是参照对方在负响应里附带的"能接受的参数"修改后再谈,而不是赌气断链。

与配置过程无关的事件

CONFIG 的任何子状态下:

  • CloseChannel_Req(内部要求放弃)→ 发 DisconnectReq → WAIT_DISCONNECT;
  • 收到 DisconnectReq → 回 DisconnectRsp → 直接 CLOSED(配置没谈完也可以被拆);
  • 收到 DisconnectRsp → Ignore;
  • 收到 L2CAP_Data → 照常处理 PDU,留在原子状态。

5. Standard 与 Lockstep:两种配置过程

CONFIG 子状态里的 WAIT_IND_FINAL_RSP / WAIT_FINAL_RSP / WAIT_CONTROL_IND 只在 Lockstep 过程出现。

|--------------|-------------------|-----------------------------------------------------------------------------------------------------------|
| 对比项 | Standard 过程 | Lockstep 过程 |
| 使用条件 | 默认过程 | 双方都支持 Extended Flow Spec 时必须使用 |
| ConfigReq 数量 | 可多轮、多次发送 | 每方只能发一次 ConfigReq,禁止多轮谈判 |
| 负响应后重新协商 | 允许(发新的 ConfigReq) | 不允许 |
| 配置响应 result | success / fail | 先回 pending(0x0004)(把双向流规范交给本地 Controller 仲裁),Controller 通过后再回最终 success;Controller 拒绝则回 fail (0x0005) |
| 特殊约束 | --- | 带了 ExtFlowSpec 就禁止再带 QoS 和 Flush Timeout 选项 |

Lockstep 的四步舞(双方对称执行):

复制代码
① 收到对端带 ExtFlowSpec 的 ConfigReq
② 回 ConfigRsp(pending)(可带对 ExtFlowSpec 的修改)
③ 自己发带 ExtFlowSpec 的 ConfigReq
④ 收到对端的 ConfigRsp(pending)
        ↓
   此时才把 (本地+对端) 两个流规范一起交给 Controller 仲裁
        ↓
   Controller 通过 → 发 ConfigRsp(success) → OPEN
   Controller 拒绝 → 发 ConfigRsp(fail)    → 回 WAIT_CONFIG

两条 Lockstep 的"死刑条款"(违者直接断链):

  • 收到 ConfigRsp(success) 却还没先收到过 pending → 断链(流程违规);
  • 自己发 Best Effort、对端要 Guaranteed(或反过来)→ 断链(上一篇讲过的"双向服务类型必须一致")。

6. 两个保命定时器:RTX 与 ERTX

状态机的稳定运行离不开两个超时定时器,规范特意把它们从状态表里独立出来

6.1 RTX(Response Timeout eXpired)

  • 触发 :每发出一个信令请求(包括 L2CAP_EchoReq),就为该请求启动一个 RTX;
  • 超时 :可以重发一次重复的 Request (此时 RTX 重置为至少上一次两倍的新值),也可以直接拆链;
  • 初始值 :实现自定义,但最小 1 秒、最大 60 秒 ;从计时开始到拆链的总时长不得超过 60 秒;
  • 每条未决请求都要有自己的 RTX(每个 outstanding request 一个);
  • 收到重复的 Request 时,要按第一次收到它时的状态上下文处理(不能当作新事件)。

💡 LE 上的补充规则 :LE 传输中,Peripheral 侧的 Host 可以在 RTX 超时后直接断链------参考信令链路的 flush timeout:flush timeout 越长(物理层重传越多、越可靠),L2CAP 层需要的重传就越少;若 flush timeout 是无限的,L2CAP 层干脆不做任何重传。

6.2 ERTX(Extended RTX)

  • 触发 :对方对请求回了 "pending" (如 ConnectRsp(0x0001 connect pending)),说明对方还在"内部处理"(比如等上层应用决定接不接受),RTX 的量程可能不够,换更长的 ERTX;
  • 超时 :同样可重发 Request 或拆链;若选择重发,ERTX 消失、换一个新的 RTX,计时从头来;
  • 初始值 :最小 60 秒、最大 300 秒;总时长不得超过 300 秒;
  • 每条收到 pending 的未决请求一个 ERTX,且每条请求同一时刻只能有一个(RTX 或 ERTX)。

💡 通俗类比:RTX 是"外卖 30 分钟没到就催单";ERTX 是"商家点了'出餐中',那就再宽限到 2 小时";真超时了就直接退款关单(直接 CLOSED,不再走礼貌性道别流程)。

7. 写在最后

L2CAP 面向连接信道的完整生命周期是:

复制代码
ConnectReq/Rsp(确定 CID、PSM)
      ↓
CONFIG 状态:双向各自跑 initiator + acceptor 两条路径
      ↓      (选项怎么填?→ 上一篇;流程怎么走?→ 本篇)
OPEN 状态:按协商好的模式(Basic/ERTM/Streaming)收发数据
      ↓      (可 Reconfigure 回 CONFIG)
DisconnectReq/Rsp → CLOSED

三个最重要的心法:

  1. 配置是双向独立、且必须双向都完成------单向谈妥不等于信道可用;
  2. Ignore ≠ Reject------很多状态下信令会被静默丢弃,排错时别把"没回包"当成"没收到";
  3. 定时器是协议健壮性的地基------RTX/ERTX 的倍增重传与上限设计,保证了对端无响应时资源不被永久占用。
相关推荐
小莞尔1 小时前
【51单片机】【protues仿真】基于51单片机超声波测距仪系统
c语言·单片机·嵌入式硬件·物联网·51单片机
星恒讯工业路由器1 小时前
工业网关固件升级:远程OTA支持与防变砖机制详解
网络·物联网·工业网关·固件升级·远程ota·防变砖机制·双系统备份
辛迪聊物业数字化1 小时前
中小物业SaaS系统架构设计:从收费、巡检到IoT对接的落地实践
物联网·系统架构
Yiran_G2 小时前
ZigBee通信正常,数据却是错的?嵌入式开发中的数据链路排查
网络·嵌入式硬件·物联网·智能家居
honsor2 小时前
ModbusTCP、SNMP、HTTP 怎么选?以太网温湿度变送器协议选型实战
网络·物联网·网络协议·tcp/ip·http
启博网络2 小时前
智慧档案库房工业组网:MR-1280 边缘大脑,实现温湿度、漏水、防火防盗设备智能联动教程
运维·网络·物联网·网络协议·网络安全·信息与通信·sdwan
码流子2 小时前
高速公路视频AI分析,算力该放哪:边缘、中心还是云边协同?
大数据·数据库·人工智能·物联网·音视频
wdfk_prog2 小时前
lwip教程 31:SLIP vs PPPoS——串口 IP Framing、错误检测、协商与工程边界
网络协议·tcp/ip·php
ITxiaobing20233 小时前
AppsFlyer Protect360 判假之后:如何系统排查 IP 维度
网络·网络协议·tcp/ip