从零实现BLE协议栈(6-1)数据信道 PDU 中的应答和流控

数据信道 PDU 中的应答和流控

前提知识

阅读本文需要具备以下基础:

  • 理解连接事件的永动循环(第 4-2 篇有详细讲解):SN/NESN 的收发判定发生在每个 Connection Event 内部的 RX→TX 流程中。不理解事件循环,就不知道这套判定逻辑在哪里执行。
  • 理解 T_IFS 的 150 µs 约束(第 2-2 篇有详细讲解):BLE 在一个 T_IFS 窗口内只收发一对包。这意味着在飞行的未确认包最多只有一个,正好满足 Stop-and-Wait 的前提。
  • 理解 CRC 校验的作用(第 1-2 篇有详细讲解):CRC 错误是触发重传的根因之一,SN/NESN 的"不翻转"行为就是对 CRC 失败的协议级响应。

不需要提前了解 L2CAP 分片或 HCI 数据格式。本文只解决一个问题:BLE 链路层如何实现可靠传输。


一、广播信道 PDU 与数据信道 PDU

image

在 BLE(蓝牙低功耗)中,PDU(Protocol Data Unit,协议数据单元) 是链路层(Link Layer)通信的基本单位。根据设备所处的时刻(是在广播还是已经连接),PDU 主要分为两大类:广播信道 PDU数据信道 PDU

1.1广播信道 PDU (Advertising Channel PDUs)

这些 PDU 在 37, 38, 39 三个广播信道上传输,用于发现设备、建立连接或传输无连接数据。
image

在第 1、2 章中,我们涉及到了以下这些 PDU :

  • ADV_IND: 通用可连接非定向广播。最常见的类型,允许任何设备扫描或连接。
  • ADV_NONCONN_IND: 不可连接非定向广播。仅用于通过广播发送数据(如 Beacon 室内定位)。
  • SCAN_REQ: 扫描请求。观察者(Observer)或主机(Central)发现可扫描广播后,请求更多数据。
  • SCAN_RSP: 扫描响应。广播者对 SCAN_REQ 的回复,通常包含设备名称、发射功率等。
  • CONNECT_IND: 连接指示。主机(Central)向从机(Peripheral)发送的指令,一旦发送成功,双方即进入“连接态”。
    还有其他很多种类,可以在 BLE Spec 的 Volume 6, Part B, Section 2.3 中找到完整列表。

1.2 数据信道 PDU (Data Channel PDUs)

一旦连接建立,双方就在 0-36 号数据信道上通过跳频通信。数据 PDU 的结构如下:
image

根据 Header 中的 LLID 字段,数据 PDU 分为以下两类:

1.2.1 LL Data PDU (LLID = 01 b 或 10 b)

这是搬运用户数据的“货车”。

  • Start PDU (LLID=10 b):代表一个 L 2 CAP 帧的开始。
  • Continuation PDU (LLID=01 b):代表长数据的后续分片。如果 Payload 长度为 0,则称为 Empty PDU(纯 ACK 包)。

1.2.2 LL Control PDU (LLID = 11 b)

这是链路层用来管理连接的“指挥车”。它们不往上层发,而是由链路层直接执行。常见包括

  • LL_CONNECTION_UPDATE_IND: 更改连接间隔、超时时间等。
  • LL_CHANNEL_MAP_IND: 告知对方哪些信道干扰严重,需要屏蔽。
  • LL_VERSION_IND: 交换版本信息。
  • LL_FEATURE_REQ / RSP: 询问对方支持哪些高级特性(如 2 M PHY、DLE 等)。
  • LL_LENGTH_REQ / RSP: 协商单包最大长度(Data Length Extension)。

二、应答和流控(Acknowledgment and Flow Control)机制

由于 2.4 GHz 存在非常多的干扰,建立连接之后,即使有跳频机制,也无法避免出现被干扰导致丢包的情况,因此,BLE 连接态内置了一套可靠的传输机制,即应答和流控(Acknowledgment and Flow Control)机制。

在 LL Data PDU 中,前 2 字节的 Header 承载了流控的核心逻辑:

image

字段 位宽 含义 作用
LLID 2 bit 逻辑链路 ID 区分是数据起始(Start)、续传(Cont)还是控制帧(Control)
NESN 1 bit 下一个期望序列号 充当 ACK/NACK。告诉对方:我希望你下一包发什么。
SN 1 bit 序列号 标记本包的身份。
MD 1 bit 更多数据 1 表示本 Event 内还有后续数据,维持射频开启。
Length 8 bit 有效载荷长度 PDU 长度(0~255)。

SN/NESN

传统 TCP 序列号长达 32 bit,因为其窗口大,允许大量包在飞行。而 BLE 的传输受限于 \(T\_IFS\),其本质是 Stop-and-Wait ARQ(停止等待协议)

  1. Master 发一包
  2. 等 150 µs(T_IFS)
  3. Slave 收到后回一包
  4. 如果双方都还有数据(MD=1),再来一个来回

在“只有当前包”和“下一个包”两种状态切换时,1 bit(0 或 1)交替翻转足以区分这是否是一个新包。

SN/NESN 的三条判定规则

image

整套 ARQ 机制可以归结为三条判定规则。理解了这三条,就理解了 BLE 链路层可靠传输的全部。

规则一:判断收到的是新包还是重传

接收方维护一个本地变量 rx_nesn,代表"我期望收到的下一个包的序列号"。

  • 如果收到包的 SN == rx_nesn:这是新包。接收方处理数据,然后把 rx_nesn 翻转(0→1 或 1→0),表示"下次我期望另一个序列号"。
  • 如果收到包的 SN != rx_nesn:这是重传。数据丢弃(避免重复上交),但仍然要回 ACK。

规则二:判断自己发出的包是否被确认

发送方维护一个本地变量 tx_sn,代表"我当前发出的包的序列号"。

  • 如果收到对方回包的 NESN != tx_sn:说明对方的 NESN 翻转了,即对方已经确认收到了我发出的包。发送方更新 tx_sn,可以准备发下一个新包。
  • 如果收到对方回包的 NESN == tx_sn:说明对方的 NESN 没有翻转,即我发出的包未被确认(可能因为 CRC 错误或丢包)。发送方重传同一个包。

规则三:初始值必须为 0

每次新建连接时,双方的 tx_snrx_nesn必须重置为 0

如果不重置会怎样?假设上一条连接断开时 Slave 的 rx_nesn = 1,新连接建立后 Master 按规范发出第一包 SN=0,但 Slave 的 rx_nesn 还停留在 1。SN(0) != rx_nesn(1),Slave 判定为重传包,丢弃数据。连接还没传出第一个有效字节就已经开始闷声丢包了。

LLID

SN/NESN 解决了"可靠不可靠"的问题,但接收方还需要知道包里装的是什么。这由 PDU Header 中的 LLID(Logical Link Identifier) 字段决定:

LLID 值 含义 用途
01 (1) Continuation L 2 CAP 大帧的后续分片
10 (2) Start L 2 CAP 帧的第一个分片或完整小包
11 (3) LL Control 链路层控制包(VERSION_IND、FEATURE_REQ 等)
00 (0) Reserved 保留,不使用

接收方的分流逻辑非常简单:看 LLID,走不同的处理路径。LL Control 去控制层解析,Start/Continue 去 L 2 CAP 重组模块。

MD(More Data)

用于告诉对方:"这个 Connection Event 内我还有更多包要发,别急着关射频。"

正常情况下,一个 Connection Event 内只收发一对包(Master→Slave + Slave→Master),然后双方休眠到下一个 Anchor Point。但如果发送方还有数据排在队列里,就把 MD=1 写进当前包头。接收方看到 MD=1 后,知道对方后面还有料,不会立刻关闭射频,而是继续等下一个 T_IFS 窗口。

这样,在一个 Connection Event 内可以连续多次来回,把积压数据一口气清完。代价是这个 Event 会持续更久,可能挤占其他连接或扫描任务的射频时间。

在当前的 06_phy_data_channel Demo 里,MD 始终设为 0(每 Event 只收发一对包),更符合基础学习的需要。

二、代码实操:06_phy_data_channel 的 SN/NESN 处理

06_phy_data_channelconn.c 中,SN/NESN 处理在每次 RX 成功后立刻执行:

/* conn.c: conn_event() — SN/NESN 流控 */

/* 规则二:检查对方是否确认了我发出的包 */
if (pdu_data_rx.nesn != tx_sn) {
    /* 对方 NESN 翻转 → 我的上一包已被确认 */
    tx_sn = pdu_data_rx.nesn;
}

/* 规则一:判断收到的是新包还是重传 */
bool new_packet = false;
if (pdu_data_rx.sn == rx_nesn) {
    /* SN 匹配 → 新包,处理数据 */
    new_packet = true;
    rx_nesn ^= 1;              /* 翻转期望值,等下一个新包 */
}

紧接着,根据 new_packetLLID 决定数据走向:

/* conn.c: conn_event() — LLID 分流 */
if (new_packet && pdu_data_rx.len > 0) {
    if (pdu_data_rx.ll_id == PDU_DATA_LLID_CTRL) {
        /* LL Control: 版本请求、特性请求等 */
        tx_pdu_pending = handle_ll_control(&pdu_data_rx);
    } else if (pdu_data_rx.ll_id == PDU_DATA_LLID_DATA_START ||
               pdu_data_rx.ll_id == PDU_DATA_LLID_DATA_CONTINUE) {
        /* L2CAP 数据分片 → 送入重组模块(下一篇详解) */
        bool complete = l2cap_rx_fragment(
            pdu_data_rx.ll_id,
            pdu_data_rx.lldata,
            pdu_data_rx.len);

        if (complete) {
            l2cap_process_complete();
        }
    }
}

最后在 TX 阶段,无论发什么包,都把当前的 tx_snrx_nesn 写进包头:

/* ll_pdu.c: build_empty_pdu() — 组装空 PDU(纯 ACK) */
void build_empty_pdu(struct pdu_data *pdu)
{
    memset(pdu, 0, sizeof(*pdu));
    pdu->ll_id = PDU_DATA_LLID_DATA_CONTINUE;
    pdu->nesn  = rx_nesn;  /* 告诉对方"我期望的下一个 SN" */
    pdu->sn    = tx_sn;    /* 标记自己这包的序列号 */
    pdu->md    = 0;        /* 本 Demo 不使用 More Data */
    pdu->len   = 0;        /* 空包,没有 Payload */
}

运行 06_phy_data_channel,从中心设备向 Slave 写入一段数据,串口会打印完整的收发流控过程:

[CONN] #0 ch=3 RX OK, TX empty PDU sent
[CONN] #1 ch=8 RX OK (new), LLID=2 len=11 → L2CAP start
[CONN] #2 ch=13 RX OK, TX empty PDU sent

当出现干扰导致 CRC 错误时,SN 不翻转,下一个 Event 会看到重传:

[CONN] #5 ch=28 RX CRC error → NESN not flipped
[CONN] #6 ch=33 RX OK (retransmit, SN mismatch) → data discarded, ACK sent

本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪

posted @ 2026-04-02 10:15  ixbwer  阅读(80)  评论(0)    收藏  举报