跳转至

PHY MCS 切换

SLE PHY/MCS 配置、RSSI 监控
前置阅读:Hello SLERSSI 测距

学习目标

  • 理解 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 的关系

PHYMCS 是两层协同的物理层配置:

  • 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

两个关键原则:

  1. 先 PHY 后 MCS,不可反向:PHY 是整个物理层的"地基"。在 PHY 切换完成(收到 set_phy_cb 成功状态)之前调用 sle_set_mcs(),可能运行在旧的或不一致的 PHY 上,导致 MCS 配置无效。
  2. set_phy_cb 中触发 MCS 设置:这是最自然的位置——PHY 确认成功 → 立即设置配套的 MCS,一气呵成。

涉及 API

API 谁调用 用途
sle_connection_register_callbacks() 双方 注册连接管理回调,包括 set_phy_cb(PHY 设置结果通知)
sle_set_phy_param(conn_id, &param) 双方 异步设置 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 档位配套
切换时数据传输 协议栈自动排空队列 应用层无需介入

程序运行流程:

  1. Server 与 Client 建立 SLE 连接
  2. Server 注册连接回调(含 set_phy_cbread_rssi_cb
  3. Server 创建 RSSI 监控任务,周期 5 秒
  4. 监控任务调用 sle_read_remote_device_rssi() 读取信号强度
  5. read_rssi_cb 中滤波 RSSI,判断是否跨越迟滞区间
  6. 需要切换时:调用 sle_set_phy_param() → 在 set_phy_cb 中调用 sle_set_mcs()
  7. 打印切换日志(旧参数 → 新参数 + 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_cbread_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() 被调用后,协议栈内部流程为:

  1. 暂停向空中接口发送新的数据包
  2. 将发送队列中已排队的包发送完毕
  3. 与对端协商并切换到新 PHY
  4. 新 PHY 生效后,自动恢复发送队列

应用层只需在 set_phy_cb 中打印日志和设置 MCS,无需手动管理数据队列。如果你在应用层维护了自己的发送缓冲区,建议在切换期间(从 sle_set_phy_paramset_phy_cb)暂停新数据的投递,避免发送目标 PHY 与实际 PHY 不匹配。