第3章:RTOS选型与移植 — FreeRTOS vs uC/OS-III vs RT-Thread

说实话,RTOS选型这件事,在麻醉机项目里比你想的重要得多。我见过不少团队,代码写到一半发现任务调度不过来,或者中断响应慢了那么几毫秒,结果整机测试时报警灯乱闪。嗯,选错了RTOS,后面全是坑。

这一章,我就结合自己十多年的经验,把FreeRTOS、uC/OS-III和RT-Thread这三个主流方案掰开揉碎讲清楚。你想想看,麻醉机这种生命支持设备,一个任务跑飞了可不是重启那么简单。

3.1 三大RTOS的核心对比

先看一张表,这是我个人习惯的对比方式,一目了然。

特性FreeRTOSuC/OS-IIIRT-Thread
内核大小极小(4-9KB)中等(6-12KB)较大(10-20KB)
任务数量无限制无限制无限制
优先级数可配置(通常32-256)无限制256
调度算法抢占式+时间片抢占式+时间片抢占式+时间片
商业许可MIT开源商业许可(收费)Apache 2.0开源
内存占用极低较低中等
生态组件中等丰富(文件系统、网络协议栈等)
医疗认证有(IEC 62304)有(DO-178B)部分认证

我的建议:如果项目资源紧张(比如只有64KB Flash),FreeRTOS是首选。如果预算充足且需要高可靠性,uC/OS-III更稳妥。RT-Thread适合功能复杂的设备,但要注意裁剪。

3.2 任务划分原则 — 麻醉机的实战经验

任务怎么划分?我刚开始做呼吸机时犯过一个错:把所有传感器采集塞到一个任务里。结果呢?压力传感器和流量传感器的采样周期不一样,一个50Hz,一个100Hz,硬生生把系统拖垮了。

后来我总结了一套原则,分享给你:

  1. 按功能域划分:通气控制、监测报警、人机交互、通信管理,各归各的任务。
  2. 按实时性要求划分:控制回路(1ms周期)放高优先级,UI刷新(50ms)放低优先级。
  3. 按资源依赖划分:共享同一外设的操作尽量放一个任务,减少互斥锁。
  4. 避免任务过细:任务太多,上下文切换开销反而大。我一般控制在8-12个任务以内。

避坑指南:我曾经在一个项目中把键盘扫描单独做了一个任务,结果每次按键都触发一次任务切换,CPU占用率飙升。后来改成中断+信号量方式,问题就解决了。

3.3 中断管理 — 别让中断成为噩梦

中断管理是RTOS里最容易出问题的地方。说白了,中断服务程序(ISR)要短平快,别在里面做复杂运算。

我见过有人把PID计算放在中断里,结果系统直接崩了。为什么?因为中断优先级太高,把其他任务饿死了。

正确的做法是:

  • ISR里只做标记、清标志、发信号量
  • 具体处理放到任务里做
  • 中断优先级要合理分配,别让低优先级中断永远得不到响应
// 错误示范:在ISR里做复杂运算
void TIM1_IRQHandler(void) {
    // 清中断标志
    // 执行PID计算 — 千万别这么干!
    pid_output = PID_Calculate(setpoint, feedback);
    // 设置PWM
}

// 正确做法:ISR只发信号量
void TIM1_IRQHandler(void) {
    // 清中断标志
    xSemaphoreGiveFromISR(xSemaphore, NULL);  // 通知任务处理
}

void ControlTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY)) {
            pid_output = PID_Calculate(setpoint, feedback);
            // 设置PWM
        }
    }
}

注意:FreeRTOS里,ISR中调用的API函数必须带“FromISR”后缀,否则会触发断言。我当年调试时因为这个卡了两天。

3.4 内存管理策略 — 别让堆碎片害了你

内存管理,嗯,这是麻醉机开发里最容易被忽视的环节。你想想看,一个设备连续运行72小时,如果内存碎片越来越多,最后malloc失败,系统就挂了。

我推荐的做法是:

  • 静态分配优先:任务栈、消息队列、信号量,全部在编译时分配好。
  • 避免动态分配:除非必要,别用malloc/free。FreeRTOS提供了heap_1到heap_5五种方案,我一般用heap_4(合并碎片)。
  • 内存池技术:对于固定大小的数据块(比如传感器数据包),用内存池管理,效率高且无碎片。
// 静态分配示例:任务栈和TCB
StackType_t taskStack[1024];
StaticTask_t taskTCB;

TaskHandle_t xHandle = xTaskCreateStatic(
    vTaskFunction,      // 任务函数
    "ControlTask",      // 任务名
    1024,               // 栈深度
    NULL,               // 参数
    5,                  // 优先级
    taskStack,          // 栈缓冲区
    &taskTCB            // TCB缓冲区
);

我的经验:在麻醉机项目中,我把所有任务栈都设为静态,并且用工具(比如FreeRTOS+Trace)监控栈使用率。如果某个任务栈使用率超过80%,我就加大栈空间。别省那几百字节,省出来的可能是设备死机。

3.5 移植实战 — 从STM32到国产MCU

移植RTOS其实不难,但有几个关键点要注意。我记得有一次把FreeRTOS从STM32F4移植到GD32F3上,折腾了一周才发现是时钟配置的问题。

移植步骤:

  1. 配置系统时钟(SysTick),提供1ms的时基
  2. 实现三个底层函数:vPortSetupTimerInterrupt、xPortPendSVHandler、vPortSVCHandler
  3. 配置中断优先级分组(一般用4位抢占优先级)
  4. 修改FreeRTOSConfig.h里的宏定义
// FreeRTOSConfig.h 关键配置
#define configUSE_PREEMPTION          1       // 抢占式调度
#define configCPU_CLOCK_HZ            ((unsigned long)168000000)  // 主频
#define configTICK_RATE_HZ            ((TickType_t)1000)          // 1ms时基
#define configMAX_PRIORITIES          32      // 最大优先级
#define configMINIMAL_STACK_SIZE      ((unsigned short)128)
#define configTOTAL_HEAP_SIZE         ((size_t)(20 * 1024))      // 20KB堆

小技巧:移植完成后,先跑一个简单的LED闪烁任务验证。如果LED不闪,八成是时钟或中断向量表的问题。别急着调复杂功能,先把基础打牢。

3.6 选型决策树

最后,我整理了一个简单的决策思路,帮你快速选型:

  • Flash < 128KB,RAM < 32KB → FreeRTOS
  • 需要商业支持,预算充足 → uC/OS-III
  • 功能复杂,需要文件系统、网络等组件 → RT-Thread
  • 团队经验丰富,追求极致性能 → FreeRTOS + 深度裁剪

说白了,没有最好的RTOS,只有最适合你项目的。我个人的习惯是:小项目用FreeRTOS,大项目用RT-Thread。uC/OS-III嘛,除非客户指定,否则我很少用——毕竟要花钱。

嗯,这一章就到这里。下一章我们聊聊任务间通信,那才是真正体现功力的地方。