PHY MCS 切换
学习目标
- 理解 PHY 速率(1M/2M/4M)和 MCS 编码(0~12)对吞吐量、通信距离、抗干扰能力的多维 trade-off
- 掌握
sle_set_phy_param()与sle_set_mcs()的两步异步设置流程,理解为何必须先 PHY 后 MCS - 能够根据 RSSI 实时判断最优 PHY/MCS 组合,实现自适应切换策略
- 理解切换迟滞的必要性,能够配置迟滞参数防止边界抖动导致频繁切换
基本概念
为什么要自适应切换
无线通信环境不是静态的。设备移动、障碍物遮挡、同频干扰都会让信号强度剧烈波动。固定使用一种 PHY 和 MCS 组合,要么在信号好时浪费带宽,要么在信号差时频繁丢包甚至断连。
自适应切换的核心思想是:用 RSSI 作为信号质量的"温度计",根据它实时选择最合适的 PHY 和 MCS 组合。
flowchart TD
A[定时读取 RSSI] --> B{判断 RSSI 区间}
B -->|大于 -48 dBm| C[4M PHY + MCS10<br/>最大吞吐]
B -->|-48 至 -63 dBm| D[2M PHY + MCS8<br/>速率与抗干扰兼顾]
B -->|-63 至 -73 dBm| E[1M PHY + MCS4<br/>保证可靠性]
B -->|小于 -73 dBm| F[1M PHY + MCS0<br/>最远距离兜底]
C --> G[等待监控间隔]
D --> G
E --> G
F --> G
G --> A
四档策略的出发点:
| 档位 | RSSI 区间 | PHY | MCS | 典型吞吐 | 适用场景 |
|---|---|---|---|---|---|
| 高速档 | RSSI > -48 dBm | 4M | MCS10 | ~3 Mbps | 设备紧贴或 1m 内,如 OTA 升级 |
| 均衡档 | -48 ~ -63 dBm | 2M | MCS8 | ~1.5 Mbps | 3~5m 室内,如实时音频流 |
| 可靠档 | -63 ~ -73 dBm | 1M | MCS4 | ~375 Kbps | 5~10m,如传感器数据上报 |
| 兜底档 | RSSI < -73 dBm | 1M | MCS0 | ~125 Kbps | 10m+ / 穿墙,保持连接不断 |
阈值中包含 2 dBm 切换迟滞:从高速档切出需要 RSSI 低于 -50(不是 -48),从低速档切回需要 RSSI 高于 -46。这样避免了 RSSI 在 -48 附近小幅抖动时来回切换。
PHY 与 MCS 的关系
PHY 和 MCS 是两层协同的物理层配置:
- PHY 速率 决定物理层每秒钟发送多少个符号(symbols/s):1M、2M 或 4M。更高的 PHY 意味着更高的符号速率,但需要更强的信号来保证符号正确解调。
- MCS(Modulation and Coding Scheme,调制与编码方案) 决定每个符号携带多少有效比特。MCS 越高,每符号携带的比特越多,但需要更高的信噪比(SNR)。
- 两者没有固定的绑定关系——你可以用 4M PHY + MCS0 获得高符号速率但强抗干扰,也可以用 1M PHY + MCS10 获得较低符号速率但每符号高数据量。
PHY 与 MCS 组合的有效吞吐矩阵如下:
| MCS0 BPSK ¼ |
MCS4 BPSK 1 |
MCS8 QPSK 1 |
MCS10 8PSK ¾ |
|
|---|---|---|---|---|
| PHY 4M | 1.0 Mbps | 4.0 Mbps | 8.0 Mbps | ~3.0 Mbps 实测 |
| PHY 2M | 0.5 Mbps | 2.0 Mbps | 4.0 Mbps | ~1.5 Mbps 实测 |
| PHY 1M | 0.25 Mbps | 1.0 Mbps | 2.0 Mbps | ~0.75 Mbps 实测 |
| 对应距离 | >15m 穿墙 | 10~15m | 5~10m | <3m |
MCS10~12 虽然理论编码率高,但 8PSK 对信噪比要求苛刻。距离稍远时,高重传率导致有效吞吐反而低于 MCS4~8。表中"实测"值反映了重传开销后的真实可用带宽。
PHY 参数结构
SLE 的 PHY 配置通过结构体 sle_set_phy_t 传递,不只是选 1M/2M/4M 那么简单:
typedef struct {
uint8_t tx_format; // 发送无线帧类型(SLE_RADIO_FRAME_*)
uint8_t rx_format; // 接收无线帧类型
uint8_t tx_phy; // 发送 PHY(SLE_PHY_1M/2M/4M)
uint8_t rx_phy; // 接收 PHY
uint8_t tx_pilot_density; // 发送导频密度(4:1 / 8:1 / 16:1 / 无)
uint8_t rx_pilot_density; // 接收导频密度
uint8_t g_feedback; // 先发链路反馈类型(0-63)
uint8_t t_feedback; // 后发链路反馈类型(0-7)
} sle_set_phy_t;
其中导频密度值得关注——导频是收发双方已知的参考信号,用于信道估计。导频越密,信道估计越准确(抗干扰越好),但开销越大(有效吞吐越低)。远距离和复杂环境应使用更密的导频(4:1),近距离可以用稀疏导频(16:1 或无导频)以提升吞吐。
异步设置的必要性
PHY 切换不是本端单方面决定就生效的——协议栈需要与对端设备协商确认,因此 sle_set_phy_param() 采用异步回调模式:
sequenceDiagram
participant App as 应用任务
participant Stack as SLE 协议栈
participant Peer as 对端设备
App->>Stack: sle_set_phy_param conn_id param
Note over Stack,Peer: 发起 PHY 协商请求<br/>对端确认
Stack-->>App: set_phy_cb conn_id status param
Note over App: 检查 status 确认成功<br/>此时 PHY 已生效
App->>Stack: sle_set_mcs conn_id target_mcs
Note over Stack,Peer: MCS 通知对端并即时生效
Note over Peer: 数据传输使用新 PHY/MCS
两个关键原则:
- 先 PHY 后 MCS,不可反向:PHY 是整个物理层的"地基"。在 PHY 切换完成(收到
set_phy_cb成功状态)之前调用sle_set_mcs(),可能运行在旧的或不一致的 PHY 上,导致 MCS 配置无效。 - 在
set_phy_cb中触发 MCS 设置:这是最自然的位置——PHY 确认成功 → 立即设置配套的 MCS,一气呵成。
涉及 API
| API | 谁调用 | 用途 |
|---|---|---|
sle_connection_register_callbacks() |
双方 | 注册连接管理回调,包括 set_phy_cb(PHY 设置结果通知) |
sle_set_phy_param(conn_id, ¶m) |
双方 | 异步设置 PHY 参数,结果在 set_phy_cb 中返回 |
sle_set_mcs(conn_id, mcs) |
双方 | 设置 MCS 等级(0~12),应在 PHY 设置成功回调中调用 |
sle_read_remote_device_rssi(conn_id) |
双方 | 发起 RSSI 读取请求,结果在 read_rssi_cb 中返回 |
这四个 API 构成自适应切换的最小集合。
sle_set_phy_param+sle_set_mcs是切换动作,sle_read_remote_device_rssi是决策依据,回调注册是基础设施。
案例说明
做什么
两台 WS63 设备建立 SLE 连接后,Server 端启动 RSSI 监控任务,每 5 秒读取一次信号强度,根据 RSSI 区间自动将链路切换到最优 PHY/MCS 组合,并打印切换日志。
本案例是高吞吐传输的"智能版本":高吞吐传输 固定使用 4M+MCS10 追求极速,本案例演示的是根据环境动态切换以平衡速率和可靠性——两者对比,可以直观理解"固定配置"和"自适应策略"的区别。
规格与功能
| 规格项 | 配置值 | 说明 |
|---|---|---|
| 设备角色 | Server 发起切换,Client 被动响应 | 任一方向都可发起,本案例 Server 侧实现 |
| RSSI 监控周期 | 5 秒 | 避免过于频繁影响通信性能 |
| RSSI 分档 | 4 档(> -48 / -48~-63 / -63~-73 / < -73) | 含 2 dBm 迟滞 |
| PHY 范围 | 1M / 2M / 4M | 与 RSSI 档位对应 |
| MCS 范围 | MCS0 / MCS4 / MCS8 / MCS10 | 与 PHY 档位配套 |
| 切换时数据传输 | 协议栈自动排空队列 | 应用层无需介入 |
程序运行流程:
- Server 与 Client 建立 SLE 连接
- Server 注册连接回调(含
set_phy_cb和read_rssi_cb) - Server 创建 RSSI 监控任务,周期 5 秒
- 监控任务调用
sle_read_remote_device_rssi()读取信号强度 read_rssi_cb中滤波 RSSI,判断是否跨越迟滞区间- 需要切换时:调用
sle_set_phy_param()→ 在set_phy_cb中调用sle_set_mcs() - 打印切换日志(旧参数 → 新参数 + RSSI)
案例流程
sequenceDiagram
participant MT as 监控任务
participant App as 应用层
participant Stack as SLE 协议栈
participant Peer as 对端设备
Note over MT,Peer: ═══ 连接已建立,初始 PHY 1M / MCS4 ═══
loop 每 5 秒
MT->>Stack: sle_read_remote_device_rssi conn_id
Stack-->>MT: read_rssi_cb conn_id rssi status
Note over MT: 滑动滤波 + 迟滞判断
alt 需要切换 PHY/MCS
MT->>Stack: sle_set_phy_param conn_id new_phy
Note over Stack,Peer: 协议栈与对端协商 PHY
Stack-->>MT: set_phy_cb conn_id ERRCODE_SUCC param
MT->>Stack: sle_set_mcs conn_id new_mcs
Note over Stack,Peer: MCS 即时生效
MT->>App: osal_printk 切换日志
else 无需切换
Note over MT: 保持当前配置
end
end
案例操作指导
编译
本案例为概念性代码,无需编译实际 sample。如需运行验证,参考 Hello SLE 的编译配置,在此基础上添加本案例的 PHY/MCS 切换逻辑。
烧录
将编译生成的固件分别烧录到两块 WS63 开发板,其中一块作为 Server(运行切换逻辑),另一块作为 Client。
验证
预期现象:两块板子上电后,RSSI 监控任务每 5 秒输出一次信号强度。当你移动两块板子(靠近或拉远),串口输出反映 PHY/MCS 的自动切换:
[RSSI] conn=0x0000 rssi=-45dBm | mode=HIGH PHY=4M MCS=10
[RSSI] conn=0x0000 rssi=-44dBm | mode=HIGH PHY=4M MCS=10
[RSSI] conn=0x0000 rssi=-55dBm | mode=BALANCE PHY=2M MCS=8 ← 拉远后自动降档
[RSSI] conn=0x0000 rssi=-68dBm | mode=RELIABLE PHY=1M MCS=4 ← 继续拉远
[RSSI] conn=0x0000 rssi=-62dBm | mode=BALANCE PHY=2M MCS=8 ← 靠近后自动升档
关键配置
RSSI 阈值与 PHY/MCS 映射
/* RSSI 阈值定义 — 含 2 dBm 迟滞,与切换逻辑配套 */
#define RSSI_THRESH_HIGH (-48) // RSSI 高于此值使用高速档
#define RSSI_THRESH_BALANCE (-63) // RSSI 高于此值使用均衡档
#define RSSI_THRESH_RELIABLE (-73) // RSSI 高于此值使用可靠档(低于此值用兜底档)
#define RSSI_HYSTERESIS 2 // 迟滞量 dBm
RSSI 区间与档位映射表
| 当前档位 | 降到哪切出 | 升到哪切回 | PHY | MCS | 导频密度 | 典型吞吐 |
|---|---|---|---|---|---|---|
| 高速档 | < -50 | -46 | 4M | MCS10 | 16:1 | ~3 Mbps |
| 均衡档 | < -65 | -61 | 2M | MCS8 | 8:1 | ~1.5 Mbps |
| 可靠档 | < -75 | -71 | 1M | MCS4 | 4:1 | ~375 Kbps |
| 兜底档 | N/A | -71 | 1M | MCS0 | 4:1 | ~125 Kbps |
迟滞参数 2 dBm 是经验值:太小(<1 dBm)容易在边界频繁切换,太大(>5 dBm)则切换反应迟钝。2~3 dBm 对大多数室内环境是合适的平衡点。
配置速查表
| 参数 | 推荐值 | 可调范围 | 调大影响 | 调小影响 |
|---|---|---|---|---|
| 监控周期 | 5000 ms | 1000~30000 ms | 省电但反应慢 | 耗电但反应快 |
| 迟滞量 | 2 dBm | 1~5 dBm | 切换更保守 | 边界频繁切换 |
| 高速档阈值 | -48 dBm | -40~-55 | 更激进使用4M | 更保守用2M |
| 兜底档阈值 | -73 dBm | -70~-80 | 更早降速保证连接 | 坚持用高速但可能丢包 |
| RSSI 滤波窗口 | 3 样本 | 1~10 | 平滑但响应慢 | 响应快但易抖动 |
代码详解
1. 连接回调注册
这段代码解决的核心问题:把 set_phy_cb 和 read_rssi_cb 两个关键回调注册到 SLE 协议栈,让我们的切换逻辑能在 PHY 设置完成和 RSSI 读取完成时被通知。
static sle_connection_callbacks_t g_sle_conn_cbs = {
.read_rssi_cb = sample_read_rssi_callback, // RSSI 读取结果回调
.set_phy_cb = sample_set_phy_callback, // PHY 设置结果回调
// 其他回调按需填充,未使用的保持 NULL
};
void sample_register_connection_callbacks(void)
{
errcode_t ret = sle_connection_register_callbacks(&g_sle_conn_cbs);
if (ret != ERRCODE_SUCC) {
osal_printk("[PHY-MCS] register conn callbacks failed: 0x%x\r\n", ret);
}
}
回调注册必须在建立连接前完成。
set_phy_cb是整个自适应切换的核心——它通知我们 PHY 切换成功,然后我们才能在回调中紧接着设置 MCS。
2. PHY/MCS 异步设置流程
这段代码实现正式的"先 PHY 后 MCS"两步切换。不能在 set_phy_cb 之前调用 sle_set_mcs(),因为 MCS 需要在新 PHY 的基础上才能正确生效。
/* 根据目标档位填充 PHY 参数 */
static void sample_start_phy_switch(uint16_t conn_id, uint8_t target_tier)
{
sle_set_phy_t phy_param = {0};
phy_param.tx_format = SLE_RADIO_FRAME_1;
phy_param.rx_format = SLE_RADIO_FRAME_1;
switch (target_tier) {
case TIER_HIGH:
phy_param.tx_phy = SLE_PHY_4M;
phy_param.rx_phy = SLE_PHY_4M;
phy_param.tx_pilot_density = SLE_PHY_PILOT_DENSITY_16_TO_1; // 强信号用稀疏导频
phy_param.rx_pilot_density = SLE_PHY_PILOT_DENSITY_16_TO_1;
break;
case TIER_BALANCE:
phy_param.tx_phy = SLE_PHY_2M;
phy_param.rx_phy = SLE_PHY_2M;
phy_param.tx_pilot_density = SLE_PHY_PILOT_DENSITY_8_TO_1;
phy_param.rx_pilot_density = SLE_PHY_PILOT_DENSITY_8_TO_1;
break;
case TIER_RELIABLE:
case TIER_FALLBACK:
phy_param.tx_phy = SLE_PHY_1M;
phy_param.rx_phy = SLE_PHY_1M;
phy_param.tx_pilot_density = SLE_PHY_PILOT_DENSITY_4_TO_1; // 弱信号用密集导频
phy_param.rx_pilot_density = SLE_PHY_PILOT_DENSITY_4_TO_1;
break;
}
/* 触发异步 PHY 设置 — 结果在 set_phy_cb 中处理 */
errcode_t ret = sle_set_phy_param(conn_id, &phy_param);
if (ret != ERRCODE_SUCC) {
osal_printk("[PHY-MCS] set phy failed: 0x%x\r\n", ret);
}
}
/* set_phy_cb — PHY 设置成功后,紧接着设置 MCS */
static void sample_set_phy_callback(uint16_t conn_id, errcode_t status,
const sle_set_phy_t *param)
{
if (status != ERRCODE_SUCC) {
osal_printk("[PHY-MCS] set phy callback error: 0x%x\r\n", status);
return;
}
/* 只有在 PHY 确认成功后,才设置配套的 MCS */
sle_set_mcs(conn_id, g_target_mcs);
osal_printk("[PHY-MCS] PHY=%dM MCS=%d 已生效\r\n",
(param->tx_phy == SLE_PHY_4M) ? 4 :
(param->tx_phy == SLE_PHY_2M) ? 2 : 1,
g_target_mcs);
}
注意:
sle_set_mcs()在set_phy_cb回调上下文中调用。该回调运行于 SLE service 线程,不能阻塞或长时间等待——所以只做轻量的设置和日志打印。
3. RSSI 监控任务
这段代码创建独立的监控线程,以固定周期读取 RSSI,是整个自适应切换的"心跳"。选择独立线程而非定时器回调的原因是:切换逻辑涉及多步异步操作,在线程中更容易管理状态。
#define RSSI_MONITOR_INTERVAL_MS 5000 // 5 秒监控间隔
static void sample_rssi_monitor_task(void *arg)
{
uint16_t conn_id = *(uint16_t *)arg;
while (1) {
/* 发起异步 RSSI 读取 — 结果在 read_rssi_cb 中返回 */
errcode_t ret = sle_read_remote_device_rssi(conn_id);
if (ret != ERRCODE_SUCC) {
osal_printk("[PHY-MCS] read rssi request failed: 0x%x\r\n", ret);
}
/* 阻塞等待下一个监控周期,避免忙轮询 */
osal_msleep(RSSI_MONITOR_INTERVAL_MS);
}
}
/* 启动监控任务 — 在连接建立后调用 */
osal_task *sample_start_rssi_monitor(uint16_t conn_id)
{
osal_task *task = osal_kthread_create(
sample_rssi_monitor_task, // 任务函数
(void *)&conn_id, // 传递连接句柄
"rssi_monitor", // 任务名称
2048 // 栈大小 2KB
);
return task;
}
osal_msleep()让任务主动休眠,不占用 CPU 资源。选择 5 秒间隔是"感知延迟"和"通信开销"的折中——间隔太短会频繁抢占协议栈资源,间隔太长则对信号变化反应迟钝。
4. RSSI 回调与切换迟滞
这段代码是自适应切换的"大脑"——读取 RSSI 后进行滑动窗口滤波,再根据包含迟滞的阈值判断是否需要切换档位。
static int8_t g_rssi_buffer[3] = {0}; // 3 样本滑动窗口
static uint8_t g_rssi_buf_idx = 0;
static uint8_t g_current_tier = TIER_RELIABLE; // 初始默认可靠档
/* read_rssi_cb — RSSI 结果返回后的处理 */
static void sample_read_rssi_callback(uint16_t conn_id, int8_t rssi,
errcode_t status)
{
if (status != ERRCODE_SUCC) {
return; // RSSI 读取失败,跳过本次判断
}
/* 滑动窗口滤波 — 填满后才开始判断 */
g_rssi_buffer[g_rssi_buf_idx % 3] = rssi;
g_rssi_buf_idx++;
if (g_rssi_buf_idx < 3) {
return; // 尚未收集足够样本
}
/* 计算窗口均值 */
int16_t avg_rssi = (g_rssi_buffer[0] + g_rssi_buffer[1] +
g_rssi_buffer[2]) / 3;
/* 迟滞判断 — 根据当前档位使用不同的阈值 */
uint8_t new_tier = g_current_tier;
int8_t enter_threshold, exit_threshold;
switch (g_current_tier) {
case TIER_HIGH:
exit_threshold = RSSI_THRESH_HIGH - RSSI_HYSTERESIS; // -50
enter_threshold = 0; // 无需上升
if (avg_rssi < exit_threshold) {
new_tier = TIER_BALANCE; // 下降一档
}
break;
case TIER_BALANCE:
enter_threshold = RSSI_THRESH_HIGH + RSSI_HYSTERESIS; // -46
exit_threshold = RSSI_THRESH_BALANCE - RSSI_HYSTERESIS; // -65
if (avg_rssi > enter_threshold) {
new_tier = TIER_HIGH; // 上升一档
} else if (avg_rssi < exit_threshold) {
new_tier = TIER_RELIABLE; // 下降一档
}
break;
/* TIER_RELIABLE / TIER_FALLBACK 同理 */
}
/* 需要切换时触发 */
if (new_tier != g_current_tier) {
g_current_tier = new_tier;
sample_start_phy_switch(conn_id, new_tier);
osal_printk("[PHY-MCS] tier change: %d -> %d, RSSI=%d\r\n",
g_current_tier, new_tier, avg_rssi);
}
}
滑动窗口平均滤波消除 RSSI 瞬时抖动——单次 RSSI 读数可能因为天线瞬时遮挡或协议栈调度延时出现异常值。3 个样本的窗口在平滑度和响应速度之间取得了平衡。
5. 切换时正在传输数据的处理
这个问题不需要应用层写任何代码——协议栈自动处理。
当 sle_set_phy_param() 被调用后,协议栈内部流程为:
- 暂停向空中接口发送新的数据包
- 将发送队列中已排队的包发送完毕
- 与对端协商并切换到新 PHY
- 新 PHY 生效后,自动恢复发送队列
应用层只需在 set_phy_cb 中打印日志和设置 MCS,无需手动管理数据队列。如果你在应用层维护了自己的发送缓冲区,建议在切换期间(从 sle_set_phy_param 到 set_phy_cb)暂停新数据的投递,避免发送目标 PHY 与实际 PHY 不匹配。