3、RTOS选型与移植:FreeRTOS vs Zephyr vs RT-Thread

做医疗贴片固件,选RTOS这件事,我踩过的坑比你们想象的多。说实话,市面上能用的RTOS不少,但真正适合医疗贴片这种资源受限、又要求高可靠性的场景,其实就那么几个。今天我就把FreeRTOS、Zephyr和RT-Thread这三家的底裤扒一扒,讲讲我的真实感受。

3.1 三大RTOS的硬核对比

先给个结论:没有最好的RTOS,只有最合适的。医疗贴片通常用Cortex-M0/M4内核,Flash 64KB-256KB,RAM 8KB-64KB。在这个范围内,三者的表现天差地别。

维度 FreeRTOS Zephyr RT-Thread
最小ROM占用 ~6KB ~20KB ~8KB
最小RAM占用 ~1KB ~4KB ~2KB
任务切换时间 ~1.5μs (72MHz) ~2.5μs ~1.8μs
驱动生态 弱(需自研) 强(Linux风格) 中等(国产生态)
认证友好度 高(IEC 62304)

我个人习惯,如果项目Flash在64KB以下,直接选FreeRTOS。为什么?因为它轻啊。我在一个血糖贴片项目里,FreeRTOS只占了6KB Flash,剩下的空间全给了算法和协议栈。Zephyr虽然功能全,但它的抽象层太重了,20KB起步,小片子根本扛不住。

核心结论:

  • Flash < 64KB:FreeRTOS(别无选择)
  • Flash 64KB-128KB:RT-Thread(平衡之选)
  • Flash > 128KB:Zephyr(生态碾压)

3.2 任务调度模型怎么选?

医疗贴片的任务模型,说白了就三种:周期性采集、事件驱动处理、紧急报警。这三种任务对调度的要求完全不同。

我一般这样设计:

  • 周期性任务(如ADC采样、心率计算):用定时器触发,优先级中等。我习惯把采样率设为100Hz,用FreeRTOS的vTaskDelayUntil()保证精确周期。
  • 事件驱动任务(如按键、通信接收):用队列或信号量触发,优先级低。为什么?因为事件可以缓冲,没必要抢CPU。
  • 紧急任务(如电池欠压、异常检测):最高优先级,抢占式调度。我曾经在一个项目中,把电池检测放在低优先级,结果电池没电了系统还在跑算法,数据全丢了。嗯,血的教训。
// 我常用的任务优先级分配方案
#define PRIO_EMERGENCY    3   // 紧急任务(电池、异常)
#define PRIO_PERIODIC     2   // 周期性任务(采样、计算)
#define PRIO_EVENT        1   // 事件驱动任务(通信、UI)
#define PRIO_IDLE         0   // 空闲任务

// 创建任务示例
xTaskCreate(emergency_task, "Emergency", 256, NULL, PRIO_EMERGENCY, NULL);
xTaskCreate(sampling_task, "Sampling", 512, NULL, PRIO_PERIODIC, NULL);
xTaskCreate(comm_task, "Comm", 1024, NULL, PRIO_EVENT, NULL);

我的经验:医疗贴片里,不要用时间片轮转。时间片轮转看似公平,但会导致采样抖动。我做过对比测试,用抢占式调度,采样抖动在±5μs以内;用时间片轮转,抖动能到±200μs。对于心率变异性分析来说,这个误差是致命的。

3.3 移植要点:从裸机到RTOS

移植RTOS这件事,说难不难,说简单也不简单。我总结了几条铁律:

3.3.1 中断处理要改

裸机时代,中断里想干啥就干啥。但上了RTOS,中断里不能调用阻塞API。比如vTaskDelay()xQueueReceive()这些,在中断里调用会死锁。

// 错误示范:中断里调用阻塞API
void ADC_IRQHandler(void) {
    uint32_t data = ADC_GetData();
    xQueueSend(adc_queue, &data, portMAX_DELAY);  // 死锁!
}

// 正确做法:使用中断安全版本
void ADC_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint32_t data = ADC_GetData();
    xQueueSendFromISR(adc_queue, &data, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

3.3.2 堆栈大小要算准

这是新手最容易翻车的地方。我见过一个项目,任务堆栈设了128字节,结果跑着跑着就HardFault。为什么?因为任务里调用了printf(),这个函数本身就要占200多字节的栈。

避坑指南:我曾经在移植Zephyr到STM32L0时,发现任务栈溢出导致系统随机重启。排查了两天才发现,是Zephyr的日志系统默认开了CONFIG_LOG,每个任务多了256字节的栈开销。所以移植时,先关掉所有调试功能,跑通后再逐个打开

我一般这样估算堆栈:

  • 基础任务:256字节(只做简单状态机)
  • 通信任务:512-1024字节(有协议栈)
  • 算法任务:1024-2048字节(有浮点运算)

3.3.3 时钟节拍要匹配

FreeRTOS默认是1000Hz(1ms节拍),但医疗贴片里,我建议改成100Hz(10ms节拍)。为什么?因为节拍太快,上下文切换开销太大。我实测过,1000Hz节拍下,CPU有5%的时间花在切换上;改成100Hz后,这个开销降到0.5%。

// FreeRTOSConfig.h 中修改
#define configTICK_RATE_HZ  ((TickType_t)100)  // 100Hz,10ms节拍

// 注意:如果用了vTaskDelayUntil(),要重新计算延时
// 原来延时10ms:vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(10));
// 现在延时10ms:vTaskDelayUntil(&xLastWakeTime, 1);  // 1个tick = 10ms

3.4 我的最终建议

如果你现在要做一个医疗贴片,我的选择顺序是:

  1. 资源极度受限(Flash < 64KB):FreeRTOS + 自研驱动层。虽然累点,但可控。
  2. 中等资源(64KB-128KB):RT-Thread Nano。它保留了RT-Thread的IPC机制,但裁剪了设备框架,很轻。
  3. 资源充裕(Flash > 128KB):Zephyr。它的蓝牙协议栈和传感器驱动是现成的,能省大量开发时间。

最后说一句:移植RTOS不是终点,而是起点。真正花时间的,是后续的任务划分、优先级调优、以及低功耗管理。这些内容,我们后面的章节会详细讲。

一句话总结:选RTOS看资源,调调度看实时性,移植看细节。别贪大求全,够用就好。