4. 多设备数据通道建立:并发连接、MTU协商、数据分片、流控实现
当蓝牙中央设备需要同时与多个外围设备(如传感器节点、执行器节点)建立数据通道时,必须协调并发连接的生命周期、协商每对连接的最大传输单元、处理数据分片与重组,并实现有效的流控机制以防止接收缓冲区溢出。本章围绕这四个核心维度展开,提供可直接落地的实现方案。
4.1 并发连接管理
BLE 5.x 规范允许中央设备同时与最多约 20 个外围设备保持连接(受具体芯片及协议栈限制)。并发管理的关键是维护每一条连接的句柄(Connection Handle)和状态机。
| 连接状态 | 描述 | 关键事件 |
|---|---|---|
| IDLE | 未发起连接 | — |
| CONNECTING | 发起连接请求,等待响应 | BLE_GAP_EVT_CONNECT |
| CONNECTED | 连接建立成功,准备 MTU 协商 | BLE_GAP_EVT_CONNECTED |
| MTU_PENDING | 正在协商 MTU | BLE_GATTC_EVT_EXCHANGE_MTU_RSP |
| READY | MTU 协商完成,通道可用 | 应用层标记 |
| 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;
}
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_MAX | 247 | 中央侧最大请求值(需配置协议栈) |
| SD_BLE_CFG_MTU_SIZE | 16、32、64、128、247 | SoftDevice 编译时设置 |
| 实际有效载荷 | final_mtu - 3 | 减掉 ATT 操作头(Opcode + Handle) |
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();
}
}
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 缓冲区 |