吞吐量测试
使用技术:SLE SSAP 通知/写入、速率统计
前置阅读:高吞吐传输
学习目标
- 理解 SLE 吞吐量测试的科学方法——滑动窗口统计、不同配置下的 A/B 对比
- 掌握有效吞吐量(去协议头)和物理层速率的区别
- 能够设计一组配置矩阵测试,找到最优 PHY/MCS/连接间隔组合
- 理解丢包率与 RSSI 的关系,判断链路质量
基本概念
有效吞吐量 vs 物理层速率
物理层 4M PHY = 4M symbols/s,但这个数字是"空中符号率",远不等于实际能传输的有效数据速率。实际有效吞吐量需要层层扣除:
flowchart TD
PHY[4M PHY 物理层速率<br/>4 Mbps] --> L1[减 L1 开销<br/>前导码 + CRC + 接入地址]
L1 --> L2[减 L2 开销<br/>GAP 帧头 + 链路层控制]
L2 --> IDLE[减连接间隔空闲<br/>两次连接事件之间的等待]
IDLE --> RETX[减重传开销<br/>丢包重传消耗的带宽]
RETX --> EFF[有效吞吐量<br/>实测约 2~3 Mbps]
style PHY fill:#f9f,stroke:#333
style EFF fill:#9f9,stroke:#333
| 损耗来源 | 大约占比 | 说明 |
|---|---|---|
| L1 开销(前导码、CRC、接入地址) | ~15% | 每包固定的物理层开销 |
| L2 开销(GAP 帧头、链路层控制) | ~10% | 链路层协议开销 |
| 连接间隔空闲 | ~20% | 取决于连接间隔参数 |
| 重传 | ~5-15% | 取决于信号质量 |
| 有效载荷 | ~50-60% | 最终应用层可见的数据 |
测试方法:单变量法
每次测试固定一个组合(PHY/MCS/连接间隔/MTU),发送固定数量包(如 10000 包),用滑动窗口计算 Kbps/Mbps。改变一个变量,其他变量不变——这是科学实验的基本原则。
丢包率统计
每包带递增序号 → 接收方检测序号不连续 → 记录丢失序号。丢包率 = 丢失包数 / 总发包数。丢包率 > 1% 说明当前配置不适合当前信号环境。
flowchart LR
TX[发送方<br/>seq=0,1,2,3,4,5...]
AIR[空中传输]
RX[接收方]
TX --> AIR --> RX
subgraph 丢包检测
RX2[收到 seq: 0,1,3,4,5]
CHECK[检测: 2 缺失]
LOSS[丢包计数 +1]
end
RX --> RX2
RX2 --> CHECK
CHECK --> LOSS
丢包不一定是真的"丢失"——可能是 CRC 校验失败被协议栈丢弃,也可能是发送时协议栈缓冲区满没有真正发出。前者属于链路质量问题,后者属于流控问题。区分它们需要同时观察发送方的流控状态和接收方的 RSSI。
涉及 API
| API | 谁调用 | 用途 |
|---|---|---|
sle_set_phy_param() |
双方 | 设置 PHY |
sle_set_mcs() |
双方 | 设置 MCS |
sle_set_data_len() |
Server | 设置载荷长度 |
ssaps_notify_indicate() |
Server | 发送测试数据包 |
sle_read_remote_device_rssi() |
Client | 读取 RSSI 辅助分析 |
osal_get_cur_time_ms() |
Client | 计时用于速率计算 |
案例说明
做什么
Server 发包 / Client 收包统计,轮换配置矩阵(PHY x MCS x 连接间隔)→ 记录每个组合的吞吐量和丢包率 → 输出最优配置。
测试结果表格模板
| PHY | MCS | 连接间隔 | 理论速率 | 实测吞吐 | 丢包率 | RSSI | 距离 |
|---|---|---|---|---|---|---|---|
| 1M | MCS0 | 12.5ms | 0.25 Mbps | ??? | ??? | ??? | ??? |
| 1M | MCS4 | 12.5ms | 1.0 Mbps | ??? | ??? | ??? | ??? |
| 2M | MCS6 | 12.5ms | 2.0 Mbps | ??? | ??? | ??? | ??? |
| 4M | MCS8 | 2.5ms | 2.0 Mbps | ??? | ??? | ??? | ??? |
| 4M | MCS10 | 2.5ms | 3.0 Mbps | ??? | ??? | ??? | ??? |
案例流程说明
sequenceDiagram
participant S as Server
participant C as Client
Note over S,C: 连接 + 配对 + MTU 协商
S->>C: sle_set_phy_param(4M)
C->>S: sle_set_phy_param(4M)
S->>S: sle_set_mcs(MCS10)
S->>S: sle_set_data_len(1500)
Note over S: 开始第 1 轮测试: 4M + MCS10 + 2.5ms
S->>S: pkt_count = 0
loop 发送 10000 包
S->>C: Notification (1200B, seq=pkt_count)
Note right of C: 检测 seq 连续性
Note right of C: total_bytes += 1200
S->>S: pkt_count++
alt pkt_count % 1000 == 0
Note right of C: 打印瞬时吞吐 + 丢包
end
end
Note over S: 切换到下一组配置...
Note over S: 第 2 轮: 4M + MCS8 + 5ms
案例操作指导
第一步:编译固件
在 .config 中设置:
第二步:烧录并运行
Server 固件烧录到板子 A,Client 固件烧录到板子 B。Server 先上电(启动广播),Client 后上电(扫描连接)。
第三步:观察测试输出
Server 预期输出:
[throughput server] connected, conn_id=0
[throughput server] test round 1: PHY=4M MCS=10 interval=2.5ms
[throughput server] sending 10000 packets...
[throughput server] round 1 done. wait for switch...
Client 预期输出:
[throughput client] connected, conn_id=0
[throughput client] test round 1: PHY=4M MCS=10 interval=2.5ms
[throughput client] [ 0- 999] 285.3 KB/s lost=0 rssi=-45
[throughput client] [1000-1999] 290.1 KB/s lost=1 rssi=-44
[throughput client] [2000-2999] 287.8 KB/s lost=0 rssi=-45
...
[throughput client] === round 1 summary ===
[throughput client] avg: 288.2 KB/s total lost: 3/10000 (0.03%)
第四步:收集数据
按照配置矩阵逐轮测试,将每轮的测试结果填入表格。最终找出在当前环境下吞吐量最高且丢包率可接受(< 1%)的配置组合。
关键配置
测试参数
/* 测试包数:10000 包 / 轮。
足够消除启动波动(前几十包可能受连接建立影响)。 */
#define TEST_PACKETS_PER_ROUND 10000
/* 包大小:1200 字节。
接近 MTU 上限(1500),最大化有效载荷比例。
减去 20 字节的序列号 + 时间戳头 = 1180 字节纯载荷。 */
#define TEST_PACKET_SIZE 1200
/* 统计窗口:1000 包。
平衡统计精度和时效性——1000 包约 2~3 秒完成,
观察者可及时看到吞吐变化趋势。 */
#define STATS_WINDOW_SIZE 1000
配置矩阵
| 变量 | 取值 | 说明 |
|---|---|---|
| PHY | 1M / 2M / 4M | 物理层速率 |
| MCS | MCS0 / MCS4 / MCS6 / MCS8 / MCS10 | 调制编码方案 |
| 连接间隔 | 2.5ms / 5ms / 7.5ms / 12.5ms | 调度密度 |
| MTU | 520 / 1024 / 1500 | 最大传输单元 |
不需要遍历全部 4x5x4x3 = 240 种组合——大多数组合没有意义。例如 MCS10 只在 4M PHY 下有效,1M PHY 配 MCS10 没有意义。实际测试通常控制在 15~20 个有代表性的组合。
配置矩阵(常用组合)
typedef struct {
uint8_t phy; // SLE_PHY_1M / SLE_PHY_2M / SLE_PHY_4M
uint8_t mcs; // SLE_MCS_0 ~ SLE_MCS_10
uint16_t conn_interval; // 连接间隔(单位 125us)
uint16_t mtu; // MTU 大小
} throughput_test_config_t;
static const throughput_test_config_t test_matrix[] = {
/* PHY, MCS, 间隔, MTU */
{SLE_PHY_1M, SLE_MCS_4, 0x64, 520 }, // 基线:1M 标准模式
{SLE_PHY_2M, SLE_MCS_6, 0x64, 1024 }, // 2M 中速
{SLE_PHY_2M, SLE_MCS_8, 0x32, 1024 }, // 2M 高速
{SLE_PHY_4M, SLE_MCS_8, 0x32, 1500 }, // 4M 中速
{SLE_PHY_4M, SLE_MCS_10, 0x14, 1500 }, // 4M 最高吞吐
{SLE_PHY_4M, SLE_MCS_10, 0x32, 1500 }, // 4M 高速 + 标准间隔
};
代码详解
数据包结构
/* 每包数据的结构——前 20 字节是元数据,后面是载荷 */
typedef struct {
uint32_t seq; // 递增序号(用于丢包检测)
uint32_t timestamp_ms; // 发包时刻(用于计算单向延迟)
uint32_t magic; // 魔数 0x5LE544B(用于数据完整性校验)
uint32_t reserved; // 保留
uint8_t test_data[1180]; // 测试载荷(填充 0xA5 模式)
} throughput_packet_t;
/* 打包一个测试包 */
static void fill_test_packet(throughput_packet_t *pkt, uint32_t seq)
{
pkt->seq = seq;
pkt->timestamp_ms = osal_get_cur_time_ms();
pkt->magic = 0x5LE544B;
pkt->reserved = 0;
memset(pkt->test_data, 0xA5, sizeof(pkt->test_data));
}
速率统计公式
核心公式非常简单——关键在于统计窗口的选择和计时精度:
/* 速率统计。
bps = total_data_bytes × 8000 / elapsed_ms
为什么要 × 8000?
- 字节 → 比特需要 × 8
- 毫秒 → 秒需要 × 1000
- 合并: × 8 × 1000 = × 8000
为什么用 total_data_bytes 而不是 total_packets?
- 因为不同测试轮次的包大小可能不同
- 用字节数统一度量,便于跨轮对比
*/
static uint32_t calc_throughput_bps(uint32_t total_bytes, uint32_t elapsed_ms)
{
if (elapsed_ms == 0) return 0;
return (total_bytes * 8000) / elapsed_ms;
}
Server 端发送循环
static void throughput_send_task(void *arg)
{
throughput_packet_t pkt;
uint32_t seq = 0;
uint32_t round_start_time = osal_get_cur_time_ms();
while (seq < TEST_PACKETS_PER_ROUND) {
/* 1. 检查连接状态 */
if (g_conn_id == 0) {
osal_sleep_ms(10);
continue;
}
/* 2. 填充数据包 */
fill_test_packet(&pkt, seq);
/* 3. 发送(Notification 模式——不等待对端确认) */
errcode_t ret = ssaps_notify_indicate(
g_conn_id, g_property_handle,
SSAP_PROPERTY_TYPE_VALUE,
(uint8_t *)&pkt, sizeof(pkt),
SSAP_NOTIFY_TYPE_NOTIFICATION);
if (ret == ERRCODE_SUCC) {
seq++;
} else if (ret == ERRCODE_SLE_BUSY) {
/* 协议栈忙:等待 QoS 恢复。
高速发送时流控是常态——缓冲区满了就要等。
不休眠直接重试会 CPU 空转;休眠 1ms 是经验值,
太短没意义(协议栈不会在微秒级排空缓冲区),
太长会降低吞吐。 */
osal_sleep_ms(1);
} else {
printf("[tp srv] send error: %d at seq=%d\n", ret, seq);
osal_sleep_ms(10);
}
osal_task_yield();
}
uint32_t elapsed = osal_get_cur_time_ms() - round_start_time;
uint32_t total_bytes = seq * sizeof(throughput_packet_t);
uint32_t bps = calc_throughput_bps(total_bytes, elapsed);
printf("[tp srv] round done: %d pkts in %d ms, avg %d KB/s\n",
seq, elapsed, bps / 8000);
}
Client 端接收与统计
static uint32_t g_total_bytes = 0;
static uint32_t g_pkt_in_window = 0;
static uint32_t g_window_start_ms = 0;
static uint32_t g_last_seq = 0;
static uint32_t g_lost_packets = 0;
static bool g_first_pkt = true;
static void throughput_notification_cb(uint16_t conn_id, uint16_t handle,
uint8_t *data, uint16_t data_len)
{
throughput_packet_t *pkt = (throughput_packet_t *)data;
/* 1. 数据完整性校验 */
if (pkt->magic != 0x5LE544B) {
printf("[tp cli] corrupted packet, magic mismatch\n");
return;
}
/* 2. 窗口初始化 */
if (g_pkt_in_window == 0) {
g_window_start_ms = osal_get_cur_time_ms();
}
/* 3. 丢包检测(基于 seq 连续性) */
if (!g_first_pkt) {
if (pkt->seq != g_last_seq + 1) {
/* seq 不连续——中间有包丢失 */
uint32_t lost = pkt->seq - g_last_seq - 1;
g_lost_packets += lost;
}
} else {
g_first_pkt = false;
}
g_last_seq = pkt->seq;
/* 4. 累计字节数 */
g_total_bytes += data_len;
g_pkt_in_window++;
/* 5. 每 STATS_WINDOW_SIZE 个包打印一次统计 */
if (g_pkt_in_window >= STATS_WINDOW_SIZE) {
uint32_t elapsed = osal_get_cur_time_ms() - g_window_start_ms;
uint32_t bps = calc_throughput_bps(g_total_bytes, elapsed);
int8_t rssi = sle_read_remote_device_rssi(conn_id);
printf("[tp cli] [%5d-%5d] %d KB/s lost=%d rssi=%d\n",
g_last_seq - STATS_WINDOW_SIZE + 1, g_last_seq,
bps / 8000, g_lost_packets, rssi);
/* 重置窗口 */
g_pkt_in_window = 0;
g_total_bytes = 0;
}
}
丢包原因分析
丢包率高时,结合 RSSI 判断根因:
static void analyze_packet_loss(uint32_t lost, uint32_t total, int8_t rssi)
{
uint32_t loss_rate = (lost * 1000) / total; // 千分比
if (loss_rate < 10) {
return; // < 1%: 正常,无需关注
}
printf("[tp cli] !! high loss rate: %d.%d%%\n",
loss_rate / 10, loss_rate % 10);
if (rssi < -75) {
/* RSSI 弱 → 链路质量差 → 降低 PHY 或缩小距离 */
printf("[tp cli] cause: weak signal (rssi=%d). "
"try lower PHY or shorter distance.\n", rssi);
} else if (rssi < -60) {
/* RSSI 中等 → 可能是干扰 → 切换信道试试 */
printf("[tp cli] cause: possible interference. "
"try different channel.\n");
} else {
/* RSSI 强但丢包 → 可能是流控问题,发送方堆积 */
printf("[tp cli] cause: likely flow control. "
"check server side QoS status.\n");
}
}