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.S 的 LLE_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, ¬i, 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 心跳)调试顺序
- 裸机验证:关掉 RT-Thread,确认裸机 BLE 例程正常工作
- 单独验证:关掉 BLE,确认 RT-Thread 多任务调度正常
- 合入检查:在 SysTick 中断和 TMOS 处理中各加一个 GPIO 翻转,用逻辑分析仪观察时序
- 栈检查:用
list_thread检查 TMOS 任务栈使用量,确保没有溢出 - 临界区验证:在
LLE_IRQHandler的rt_hw_interrupt_disable/enable前后加 GPIO 翻转,确认保护区间覆盖了 BLE 库函数调用
其他 CH585 特有约束
以下来自 RT-Thread 移植 readme,除第 8、11、12 条外,也是在使用 BLE 时必须注意的:
- 关闭全局中断再初始化 BLE(readme 第 8 条):初始化蓝牙时需要先关闭全局中断,等初始化完成后再开启。蓝牙使用免表中断,
StartUP.S已将BB_IRQLibHandler和LLE_IRQLibHandler放入中断向量表 - 禁止裸开关中断(readme 第 12 条):不可使用
__enable_irq()/__disable_irq(),必须使用 RT-Thread 的rt_hw_interrupt_disable()/rt_hw_interrupt_enable() - mscratch 寄存器(readme 第 7 条):已被中断系统占用,用户代码不可使用
- 中断函数修饰(readme 第 4 条):统一入口中断(
unified_interrupt_entry)不需要__attribute__((interrupt(...)))修饰;非统一入口中断需要 - 统一入口中断无需手动调 enter/leave(readme 第 10 条):
unified_interrupt_entry中已调用rt_interrupt_enter和rt_interrupt_leave,用户中断函数内不要重复调用 - 中断栈(readme 第 5 条):所有统一入口中断会切换到独立中断栈(
_eusrstack),务必在 LD 脚本中预留足够 RAM,否则中断中栈溢出直接 HardFault - 中断函数放高速 RAM(readme 第 2 条):外部中断函数建议用
__attribute__((section(".highcode")))修饰 - BLE_LibInit 调用顺序:
TMOS_SystemProcess()必须在CH58x_BLEInit()(内部调用BLE_LibInit())成功之后才能调用,否则会访问未初始化的库内部数据结构
参考资料
- MX-WCH-RTOS-SDK — 社区维护的 CH585/CH582 RT-Thread Nano + BLE 移植参考
- WCH 论坛:RT-Thread 上使用 BLE 的一些疑惑
- WCH 论坛:rtt 工程加了蓝牙后,断连
- WCH 论坛:CH585M FreeRTOS tickless BLE
- CH585 EVT
EXAM/FreeRTOS/readme.txt— 官方 FreeRTOS 移植说明(RT-Thread 同理可参考)
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 版本中的实际代码为准。