4. 多设备数据通道建立:并发连接、MTU协商、数据分片、流控实现

当蓝牙中央设备需要同时与多个外围设备(如传感器节点、执行器节点)建立数据通道时,必须协调并发连接的生命周期、协商每对连接的最大传输单元、处理数据分片与重组,并实现有效的流控机制以防止接收缓冲区溢出。本章围绕这四个核心维度展开,提供可直接落地的实现方案。

多设备数据通道建立流程 中央(GATT Client) 设备 A 设备 B 设备 C MTU 协商(每对连接独立) 数据分片与重组 流控(基于 Credit / 窗口) 稳定双向数据通道 每个步骤针对每一对连接独立执行,中央端使用连接句柄索引管理

4.1 并发连接管理

BLE 5.x 规范允许中央设备同时与最多约 20 个外围设备保持连接(受具体芯片及协议栈限制)。并发管理的关键是维护每一条连接的句柄(Connection Handle)和状态机。

连接状态描述关键事件
IDLE未发起连接—
CONNECTING发起连接请求,等待响应BLE_GAP_EVT_CONNECT
CONNECTED连接建立成功,准备 MTU 协商BLE_GAP_EVT_CONNECTED
MTU_PENDING正在协商 MTUBLE_GATTC_EVT_EXCHANGE_MTU_RSP
READYMTU 协商完成,通道可用应用层标记
DISCONNECTED连接断开BLE_GAP_EVT_DISCONNECTED

实现时,典型的并发连接管理采用数组或哈希表存储每条连接的状态、MTU 值、待发送数据队列等信息。以下为 Nordic nRF5 SDK 中并发连接池的部分代码示例:

// 连接池结构定义
#define MAX_CONNECTIONS 8

typedef struct {
    uint16_t conn_handle;
    uint8_t  state;         // 见上述状态枚举
    uint16_t negotiated_mtu;
    uint8_t  buffer[512];
    uint16_t buf_len;
} conn_entry_t;

static conn_entry_t m_connections[MAX_CONNECTIONS];

// 根据 conn_handle 快速索引
conn_entry_t * find_connection(uint16_t conn_handle) {
    for (int i = 0; i < MAX_CONNECTIONS; i++) {
        if (m_connections[i].conn_handle == conn_handle &&
            m_connections[i].state != IDLE) {
            return &m_connections[i];
        }
    }
    return NULL;
}
实际项目中建议使用连接句柄作为数组索引(若协议栈保证句柄连续且不重复),可将查找复杂度降至 O(1)。

4.2 MTU 协商

MTU(Maximum Transmission Unit)决定了每个 BLE 数据包可携带的有效载荷字节数。默认 MTU 为 23 字节(包含 3 字节操作头),因此用户数据最多 20 字节。通过 MTU 协商可将有效载荷提升至 247 字节甚至更大(取决于芯片与协议栈支持)。

协商流程:中央设备(GATT Client)调用 sd_ble_gattc_exchange_mtu_request(),外围设备(GATT Server)响应自身的 MTU 支持值,双方取最小值作为最终 MTU。

// 发起 MTU 协商(中央端)
static void mtu_exchange_start(conn_entry_t * p_conn) {
    uint32_t err_code = sd_ble_gattc_exchange_mtu_request(p_conn->conn_handle, BLE_ATT_MTU_MAX);
    APP_ERROR_CHECK(err_code);
    p_conn->state = MTU_PENDING;
}

// 在 BLE_GATTC_EVT_EXCHANGE_MTU_RSP 事件中处理协商结果
void on_mtu_rsp(ble_evt_t * p_ble_evt) {
    uint16_t conn_handle = p_ble_evt->evt.gattc_evt.conn_handle;
    conn_entry_t * p_conn = find_connection(conn_handle);
    if (!p_conn) return;

    uint16_t server_mtu = p_ble_evt->evt.gattc_evt.params.exchange_mtu_rsp.server_rx_mtu;
    uint16_t final_mtu = MIN(BLE_ATT_MTU_MAX, server_mtu);
    p_conn->negotiated_mtu = final_mtu;
    p_conn->state = READY;

    // 例:将 MTU 写入日志并更新数据分片长度
    NRF_LOG_INFO("Connection 0x%x MTU = %d", conn_handle, final_mtu);
}
参数典型值说明
BLE_ATT_MTU_MAX247中央侧最大请求值(需配置协议栈)
SD_BLE_CFG_MTU_SIZE16、32、64、128、247SoftDevice 编译时设置
实际有效载荷final_mtu - 3减掉 ATT 操作头(Opcode + Handle)
并非所有外设都支持更大的 MTU。如果外设返回的 MTU 仅为 23,则后续数据分片必须按 20 字节/包进行。建议在连接建立后立即发起 MTU 协商,避免先发送大包导致丢包。

4.3 数据分片机制

当应用层待发送数据长度超过 MTU 有效载荷时,必须进行分片发送;接收端需按顺序重组。分片协议通常包含:总片数、当前片序号、数据长度三个字段。

以下为一个轻量分片格式实现(头 4 字节 + 数据体):

// 分片包头结构
typedef struct {
    uint16_t total_fragments;   // 总片数,范围 1-65535
    uint16_t fragment_index;    // 当前片序号(从 0 开始)
    uint8_t  data[];            // 片内数据(长度 = MTU - 4)
} fragment_header_t;

// 发送示例:将 1000 字节数据分片发送(MTU=128, 有效载荷=125)
uint16_t send_fragmented(uint16_t conn_handle, uint8_t *data, uint16_t len) {
    uint16_t mtu = find_connection(conn_handle)->negotiated_mtu;
    uint16_t payload_size = mtu - 4;   // 保留分片头
    uint16_t total_frags = (len + payload_size - 1) / payload_size;

    for (uint16_t i = 0; i < total_frags; i++) {
        uint16_t offset = i * payload_size;
        uint16_t frag_len = MIN(payload_size, len - offset);

        // 构造发送缓冲区(分片头 + 数据)
        uint8_t send_buf[mtu];
        fragment_header_t * hdr = (fragment_header_t *)send_buf;
        hdr->total_fragments = total_frags;
        hdr->fragment_index = i;
        memcpy(hdr->data, data + offset, frag_len);

        // 通过 Notification 或 Write 发送
        ble_gatts_hvx_params_t hvx_params = {
            .handle = m_data_ccc_handle,
            .type   = BLE_GATT_HVX_NOTIFICATION,
            .offset = 0,
            .p_len  = &mtu,
            .p_data = send_buf,
        };
        sd_ble_gatts_hvx(conn_handle, &hvx_params);
    }
    return total_frags;
}

接收端需维护一个按连接句柄索引的重组缓冲区,在收到所有分片后组合成完整数据,并向上回调。

4.4 流控实现

BLE 协议栈原生没有像 TCP 窗口那样的动态流控,但可以利用 Notification Credit 机制(基于 L2CAP)或者应用层自定义确认来防止发送过快导致丢包或溢出。

  • Credit 流控:BLE 4.2+ 支持 L2CAP LE Credit Based Flow Control。中央端为每对连接配置初始 credits(如 8),每发送一个包消耗一个 credit,外设处理完毕后发送 SDU(服务数据单元)并归还 credit。
  • 应用层滑动窗口:在 GATT 层,可维护一个发送窗口(如最多 4 个未确认包)。每次收到应用层 ACK 后释放窗口,继续发送后续分片。

以下是一个简单的应用层流控实现片段(基于确认通知):

// 发送窗口结构
#define TX_WINDOW_SIZE 4

typedef struct {
    uint16_t next_seq;            // 下一个待发送的序列号
    uint16_t acked_seq;           // 已确认的最大序列号
    uint8_t  window[TX_WINDOW_SIZE]; // 用于重传的缓冲区
} flow_ctrl_t;

// 可发送条件
bool can_send(flow_ctrl_t *ctrl) {
    return (ctrl->next_seq - ctrl->acked_seq) < TX_WINDOW_SIZE;
}

// 收到外设的确认通知时推进窗口
void on_ack(uint16_t ack_seq) {
    if (ack_seq > ctrl->acked_seq) {
        ctrl->acked_seq = ack_seq;
        // 触发发送新数据
        tx_process();
    }
}
在实际产品测试中,不加流控时,若连续快速地发送 20 个以上的 Notification,部分低端外设会因缓冲区溢出断连。推荐在每次发送后至少等待外设响应一个 ACK(可通过自定义 GATT 特征实现),再发送下一个。

4.5 常见错误与调试

  • 连接句柄冲突:新连接断开后释放的句柄被复用,未清理旧状态导致内存泄漏。解决:在断开事件中将该连接池条目重置为 IDLE。
  • MTU 协商失败:外设不支持或协议栈未使能 MTU 交换事件。检查编译配置 BLE_GATTC_EXCHANGE_MTU_ENABLE。
  • 分片顺序错乱:当多连接并发发送 Notification 时,若使用同一缓冲区,会出现交叉混叠。解决:每条连接使用独立的发送缓冲区。
  • Credit 耗尽:确认外设处理速度慢导致 credits 被耗尽,中央端堵塞。可增大初始 credits 或优化外设处理回调。

4.6 性能与稳定性优化

优化目标手段效果
降低延迟启用 Data Length Extension (DLE),增大 MTU 至 247单包吞吐提升 10x
提升并发数使用连接参数更新(connection interval 30~50ms)支持 8~12 设备稳定连接
减少断连应用层流控 + 分片重传丢包率 < 0.1%
节省内存动态分配重组缓冲区(根据 MTU 大小预分配)每个连接最多 1KB 缓冲区