CH585 上强行同时跑 RT-Thread 和 BLE:能跑,但官方不推荐

先说结论:沁恒官方不推荐在 CH58x 系列上同时使用 RTOS 和 BLE。 BLE 协议栈是闭源库,深度绑定 TMOS 调度系统,无法剥离。RT-Thread 移植 readme 第 8 条明确写了「不建议使用蓝牙」,EVT 包里也找不到任何一个 RTOS + BLE 的例程。低功耗模式更是彻底不兼容(官方论坛明确回应「FreeRTOS + BLE low power consumption cannot coexist」)。

本文记录的是社区摸索出来的 workaround——在 RT-Thread 里创建一个低优先级任务循环调用 TMOS_SystemProcess(),配合一系列补丁让两个调度器勉强共存。技术上社区有成功的移植案例(如 MX-WCH-RTOS-SDK),但这不是官方支持的使用方式,生产环境需自行评估风险。


坑一:SysTick 被 BLE 初始化覆盖

现象:BLE 初始化完成后,RT-Thread 的 rt_tick 不再增长,系统调度停止;或者反过来,BLE 连接立即断开。

原因:RT-Thread 启动时会配置 SysTick 作为系统心跳,通常 1ms 一次中断。CH58X_BLEInit() 内部同样会调用 __SysTick_Config(SysTick_LOAD_RELOAD_Msk) 配置 SysTick,虽然紧接着就关了 SysTick 中断(PFIC_DisableIRQ(SysTick_IRQn)),但 SysTick 的配置寄存器已经被覆盖。TMOS 的时间基准来自 RTC(SYSTEM_TIME_MICROSEN 625us),不依赖 SysTick——但 BLE 初始化覆盖 SysTick 配置这个副作用仍然会让 RT-Thread 的系统心跳失效。

解决:在 CH58X_BLEInit() 内部找到 __SysTick_Config() 调用并注释掉,只保留 RT-Thread 的 SysTick 配置。BLE 初始化完成后,RT-Thread 需要重新配置一次 SysTick 以确保心跳参数正确。

// 在 CH58X_BLEInit() 内部:
// SysTick_Config(...);   // ← 注释掉,交给 RT-Thread 管理

坑二:LLE 中断内部做环境切换,RT-Thread 任务调度破坏上下文

现象:蓝牙广播和连接都正常,运行几秒到几分钟后系统死机或数据错乱,无固定规律。

原因:CH585 的蓝牙链路层中断 LLE_IRQHandler 在中断处理过程中,协议栈会在中断内部保存当前上下文,切换到另一个运行环境等待 BLE 硬件事件完成。如果这个过程中 SysTick 中断恰好触发了一次 RT-Thread 任务调度,调度器会尝试保存"当前任务"的上下文,但此时 CPU 正处于 BLE 协议栈的另一个运行环境中,上下文被错误保存,导致数据损坏甚至死机。

解决:在 ble_task_scheduler.SLLE_IRQHandler 入口加临界区保护,退出时恢复。

/* ↓ 示意代码,基于 EVT ble_task_scheduler.S 原始代码修改 ↓ */
LLE_IRQHandler:
    addi    sp, sp, -32*4            /* 分配 32 个寄存器保存空间 */

    sw      x1,  1 * 4(sp)           /* 保存 ra  */
    sw      x4,  2 * 4(sp)           /* 保存 tp  */
    sw      x5,  3 * 4(sp)
    /* ... x6~x30 按偏移依次保存 ... */
    sw      x31, 29 * 4(sp)

    /* ★ 关键:停掉 RT-Thread 调度,防止在 BLE 内部环境切换时切入 */
    call    rt_hw_interrupt_disable

    la      a1, g_LLE_IRQLibHandlerLocation
    lw      a0, 0(a1)
    jalr    x1, 0(a0)                /* 通过函数指针间接调用 BLE 库处理函数 */

    /* ★ 关键:恢复 RT-Thread 调度 */
    call    rt_hw_interrupt_enable

    lw      x1,  1 * 4(sp)
    lw      x4,  2 * 4(sp)
    /* ... x6~x30 按偏移依次恢复 ... */
    lw      x31, 29 * 4(sp)

    addi    sp, sp, 32*4
    mret

以上为示意代码,基于 EVT BLE/LIB/ble_task_scheduler.S 的原始代码修改,插入的 rt_hw_interrupt_disable / rt_hw_interrupt_enable 来自 RT-Thread 移植 readme 第 12 条的明确要求。实际修改时务必以你的 EVT 版本中的原始汇编为准。


坑三:TMOS 事件执行太久,BLE 连接超时

现象:在 GATT 写回调或 TMOS 定时事件中加了 Flash 写入、外设等待等操作后,BLE 连接不规则断开。

原因:TMOS 是单线程事件循环,所有 BLE 事件都在同一个循环中依次处理。BLE 对时序极其敏感,每个连接间隔内主从设备必须按约定时间窗口交换数据。如果某个 TMOS 事件回调执行了 20ms 的 Flash 擦写,连接间隔只有 30ms,对端设备连续几次收不到响应就会判定超时断开。

解决:单次 TMOS 事件执行时间控制在连接间隔的一半以内。耗时操作拆分成多个小事件,通过事件链逐步执行。

// ❌ 错误:在 GATT 回调中直接做耗时操作
void gatt_write_callback(uint8_t *data, uint16_t len)
{
    flash_erase_page(0x1000);        // 耗时 20ms+
    flash_write(0x1000, data, len);  // 直接阻塞 TMOS 循环
}

// ✅ 正确:只做数据缓存,通过事件链异步处理
void gatt_write_callback(uint8_t *data, uint16_t len)
{
    memcpy(g_rx_buffer, data, len);
    tmos_set_event(task_id, EVENT_FLASH_WRITE);  // 延迟到下一个 TMOS 周期
}

void flash_write_event_handler(void)
{
    flash_write_page(g_current_page++, g_rx_buffer + offset, 256);
    if (!finished) {
        tmos_set_event(task_id, EVENT_FLASH_WRITE);  // 继续下一批
    }
}

坑四:在中断里直接调用 BLE 函数

现象:在 GPIO 中断或定时器中断中调用 GATT_Notification() 等 BLE 发送函数,系统死机或数据发不出去。

原因:TMOS 不可重入,BLE 函数内部依赖 TMOS 事件机制,必须运行在任务上下文中。

解决:中断只置标志,在 RT-Thread 任务或 TMOS 事件回调中执行 BLE 操作。

// GPIO 中断
void GPIO_IRQHandler(void)
{
    if (GPIO_ReadITFlagPort() & GPIO_Pin_5) {
        g_send_flag = 1;                           // 只置标志
        rt_sem_release_from_isr(g_ble_sem);         // 通知任务
        GPIO_ClearITFlagBit(GPIO_Pin_5);
    }
}

// RT-Thread 任务
void ble_send_task(void *param)
{
    while (1) {
        rt_sem_take(g_ble_sem, RT_WAITING_FOREVER);
        if (g_send_flag) {
            g_send_flag = 0;

            attHandleValueNoti_t noti;
            noti.handle = g_char_handle;
            noti.len = len;
            tmos_memcpy(noti.value, data, len);
            GATT_Notification(conn_handle, &noti, FALSE);
        }
    }
}

坑五:任务优先级排错

现象:BLE 数据卡顿,或 TMOS 任务长期得不到执行。

推荐配置

任务 优先级 说明
TMOS 循环任务 仅比 IDLE 高一级 由高优先级任务抢占,空闲时立即处理 BLE
业务逻辑任务 中(高于 TMOS) 传感器采集、数据处理等
IDLE 最低 RT-Thread 空闲任务

RT-Thread Nano 中 IDLE 任务优先级通常为 31(最低)。TMOS 任务设置为优先级 23,业务任务设置为 46——即 TMOS 低于业务、远高于 IDLE。"仅比 IDLE 高一级"这个说法容易让人误解为设置为 30,实际上 readme 的意思是 TMOS 任务处于低优先级区间、不抢占业务即可。

这里容易有一个误解:TMOS 本身会主动让出 CPU(TMOS_SystemProcess() 处理完事件队列就返回,没有事件时几乎立即返回),并不会永久占据 CPU。优先级不建议过高的真正原因是 BLE 的硬实时保障在 LLE 中断层——连接间隔的严格时序由硬件中断保证,不依赖 TMOS 任务的优先级。TMOS 任务只负责消化中断产生的事件(GATT 回调、连接参数更新、广播刷新等),这些事件只要在下一个连接间隔到来之前处理完即可。因此 TMOS 任务可以设为低优先级,让传感器采集、电机控制等对延迟敏感的业务任务优先拿到 CPU,业务跑完后再由 TMOS 在空闲时间里收尾 BLE 事件。

rt_thread_t tmos_thread = rt_thread_create(
    "tmos",
    tmos_task_entry,
    RT_NULL,
    1024,    // 栈大小,建议 ≥ 1024 字节
    3,       // 优先级,低于用户业务任务
    5        // 时间片
);

坑六:自定义连接参数太紧凑

现象:RT-Thread 任务频繁被 BLE 中断打断,业务逻辑执行效率低。

EVT 官方 Peripheral 例程的默认连接参数本身已经比较宽松(MAX 间隔 125ms)。但很多开发者会为了低延迟把连接间隔改小,在 RTOS 场景下这会进一步挤压业务任务的 CPU 时间。

解决:如果需要自定义连接参数,保持宽松的配置。

// EVT Peripheral 默认值(参考 peripheral.c)
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL    6     // 7.5ms
#define DEFAULT_DESIRED_MAX_CONN_INTERVAL    100   // 125ms(本身已较宽松)
#define DEFAULT_DESIRED_SLAVE_LATENCY        0
#define DEFAULT_DESIRED_CONN_TIMEOUT         100   // 1s

// RTOS 场景建议值(如果默认值仍不够宽松)
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL    12    // 15ms
#define DEFAULT_DESIRED_MAX_CONN_INTERVAL    100   // 125ms
#define DEFAULT_DESIRED_SLAVE_LATENCY        4     // 可跳过 4 个间隔
#define DEFAULT_DESIRED_CONN_TIMEOUT         500   // 5s 超时

关键调整是增大 SLAVE_LATENCY(允许从机跳过若干连接间隔),在 MAX 间隔 125ms + 4 次延迟的最长窗口下,RT-Thread 有约 625ms 完整时间片执行业务任务。不要随意减小 MAX_CONN_INTERVAL——越小 BLE 中断越频繁,留给 RT-Thread 的时间越少。


整体架构

RT-Thread 调度器

├── 传感器采集任务(优先级 4)
├── 数据处理任务(优先级 5)
├── TMOS 循环任务(优先级 2,仅比 IDLE 高一级)
│     └── while(1) { TMOS_SystemProcess(); }
│           └── TMOS 内部:GATT 回调、连接管理、广播刷新
├── IDLE 任务

└── 中断层
      ├── LLE_IRQHandler(+ rt_hw_interrupt_disable/enable)
      └── SysTick_IRQ(RT-Thread 心跳)

调试顺序

  1. 裸机验证:关掉 RT-Thread,确认裸机 BLE 例程正常工作
  2. 单独验证:关掉 BLE,确认 RT-Thread 多任务调度正常
  3. 合入检查:在 SysTick 中断和 TMOS 处理中各加一个 GPIO 翻转,用逻辑分析仪观察时序
  4. 栈检查:用 list_thread 检查 TMOS 任务栈使用量,确保没有溢出
  5. 临界区验证:在 LLE_IRQHandlerrt_hw_interrupt_disable/enable 前后加 GPIO 翻转,确认保护区间覆盖了 BLE 库函数调用

其他 CH585 特有约束

以下来自 RT-Thread 移植 readme,除第 8、11、12 条外,也是在使用 BLE 时必须注意的:


参考资料


CH585 上 RT-Thread + BLE 的核心做法:TMOS 作为 BLE 的微调度器跑在 RT-Thread 的低优先级任务里,LLE 中断中以临界区保护阻止 RT-Thread 在 BLE 内部环境切换时切入,TMOS 事件执行时间控制在连接间隔的一半以内。

再次强调:这是社区摸索出来的 workaround,不是官方推荐方案。 低功耗模式完全不可用——BLE 需要 TMOS 运行,RTOS 的 tickless 低功耗与 TMOS 的 RTC 时间基准直接冲突,官方已明确回应无法共存。如果你的产品需要 BLE + 低功耗,纯 TMOS 是唯一选择。只有在确实需要 RTOS 的多任务能力、可以放弃低功耗、并且愿意承担踩坑成本时,才考虑本文的方案。

关于代码示例:文中的汇编和 C 代码为示意代码,基于 EVT 原始文件修改,未经过编译验证。实际移植时务必以你的 SDK 版本中的实际代码为准。