第1章:实时操作系统(RTOS)基础:FreeRTOS任务创建与调度、消息队列、信号量、在麻醉机中的应用场景

1.1 为什么麻醉机需要RTOS?

说实话,我最早接触麻醉机项目时,第一反应是「用裸机跑循环不就行了?」。但真正深入进去才发现,麻醉机这种设备,你同时要处理呼吸监测、参数显示、报警输出、数据记录……十几个任务挤在一个while(1)里,迟早要出问题。

举个例子:病人心率突然下降,报警必须在50毫秒内触发。但这时候如果主循环正在处理SD卡写入,那报警就延迟了。这在医疗设备里是致命的。

RTOS的核心价值,说白了就是「让该急的事先办」。FreeRTOS作为轻量级内核,在Cortex-M3/M4上跑得飞起,我个人的习惯是:只要项目超过3个并发任务,直接上RTOS,别犹豫。

关键认知: 麻醉机是实时系统,不是快就行,而是「确定性」——每个任务必须在规定时间内完成。FreeRTOS的任务调度器就是干这个的。

1.2 FreeRTOS任务创建——从零开始

任务在FreeRTOS里就是一个无限循环的函数。我见过不少新手把任务写成「执行一次就退出」,结果调度器直接报错。记住:任务函数永远不能返回。

先看一个最简单的任务创建:

// 任务函数:负责读取压力传感器
void vPressureTask(void *pvParameters)
{
    uint32_t pressure_val;
    while(1)
    {
        pressure_val = ADC_Read(PRESSURE_CH);
        // 处理数据...
        vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
    }
}

// 在主函数中创建任务
int main(void)
{
    xTaskCreate(
        vPressureTask,      // 任务函数指针
        "Pressure",         // 任务名(调试用)
        256,                // 栈深度(单位:字)
        NULL,               // 参数
        3,                  // 优先级(0最低,数值越大优先级越高)
        NULL                // 任务句柄
    );
    vTaskStartScheduler();  // 启动调度器
    while(1);               // 正常情况下不会执行到这里
}

这里有个坑:栈深度。我刚开始做时,给任务分配了128个字,结果跑着跑着就HardFault了。后来用uxTaskGetStackHighWaterMark()一查,栈用了90%以上。嗯,医疗设备我建议至少256字起步,关键任务给512字。

我的经验: 任务栈大小 = 局部变量 + 函数调用链 + 中断嵌套。保守点,先给大一点,调试稳定后再优化。我曾经因为栈溢出导致数据记录丢失,查了三天才找到原因。

1.3 任务调度——谁先跑?

FreeRTOS默认是抢占式调度。什么意思?高优先级任务就绪了,低优先级任务立刻被踢出去。你想想看,麻醉机里报警任务的优先级肯定比显示任务高,对吧?

我一般这样分配优先级:

任务名称 优先级 周期 说明
报警处理 5(最高) 事件触发 生命攸关,必须立即响应
传感器采集 4 10ms 流量、压力、浓度
数据记录 3 100ms 写入SD卡或Flash
UI显示 2 50ms 刷新屏幕,可容忍延迟
空闲任务 0 空闲时 系统自带,统计CPU使用率

注意:优先级不要超过硬件支持的范围。Cortex-M3支持256级,但FreeRTOS通常只用几十级。我建议最多用5-7个优先级,多了反而容易出问题。

避坑指南: 我曾经把两个任务设为相同优先级,结果它们轮流运行,导致传感器采集周期不稳定。后来改成不同优先级,配合vTaskDelay(),问题解决。记住:同优先级任务用时间片轮转,但医疗设备里最好别依赖这个。

1.4 消息队列——任务间的「快递员」

任务之间怎么传数据?用全局变量?别闹了,那会引发资源竞争。FreeRTOS的消息队列就是干这个的——一个任务往里放,另一个任务往外取,安全又高效。

在麻醉机里,传感器采集任务把数据打包成结构体,通过队列发给数据记录任务:

// 定义数据结构
typedef struct {
    uint32_t timestamp;
    uint16_t pressure;
    uint16_t flow;
    uint8_t  spo2;
} SensorData_t;

// 创建队列(容量10个元素)
QueueHandle_t xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));

// 采集任务(发送)
void vSensorTask(void *pvParameters)
{
    SensorData_t data;
    while(1)
    {
        data.timestamp = GetSysTick();
        data.pressure = ADC_Read(PRESSURE_CH);
        data.flow = ADC_Read(FLOW_CH);
        data.spo2 = SPO2_Read();
        
        // 发送到队列,等待100ms
        if(xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(100)) != pdPASS)
        {
            // 队列满了!记录错误日志
            Log_Error("Sensor queue overflow");
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// 记录任务(接收)
void vLogTask(void *pvParameters)
{
    SensorData_t data;
    while(1)
    {
        // 阻塞等待数据
        if(xQueueReceive(xSensorQueue, &data, portMAX_DELAY) == pdPASS)
        {
            // 写入SD卡
            SD_WriteLog(&data, sizeof(data));
        }
    }
}

这里有个细节:队列满了怎么办?我建议在发送时加超时,不要死等。如果队列持续溢出,说明采集速度大于处理速度,要么加大队列深度,要么优化接收任务。

实战要点: 队列深度 = 峰值数据量 × 1.5。比如传感器10ms发一次,记录任务20ms处理一次,峰值时队列里最多积压2个数据,设成5个就够。但为了安全,我通常设成10个。

1.5 信号量——协调资源的「红绿灯」

信号量分两种:二值信号量和计数信号量。二值信号量像一把锁,只能一个人用;计数信号量像停车场,能停几辆车就看初始值。

在麻醉机里,二值信号量最典型的应用是「中断与任务同步」。比如按键中断来了,唤醒UI任务去处理:

// 创建二值信号量
SemaphoreHandle_t xButtonSemaphore = xSemaphoreCreateBinary();

// 按键中断服务函数
void EXTI0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 给信号量(从ISR中调用)
    xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);
    // 如果唤醒的任务优先级更高,立即切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// UI任务(等待按键)
void vUITask(void *pvParameters)
{
    while(1)
    {
        // 等待信号量,阻塞
        if(xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdPASS)
        {
            // 处理按键事件
            uint8_t key = Key_Read();
            UI_HandleKey(key);
        }
    }
}

计数信号量呢?我用来管理「数据缓冲区池」。比如有5个缓冲区,任务用之前先「取」一个,用完再「还」回去:

// 创建计数信号量,初始值5
SemaphoreHandle_t xBufferSemaphore = xSemaphoreCreateCounting(5, 5);

// 使用缓冲区
void ProcessData(void)
{
    uint8_t *pBuffer;
    if(xSemaphoreTake(xBufferSemaphore, pdMS_TO_TICKS(1000)) == pdPASS)
    {
        pBuffer = GetFreeBuffer();
        // 处理数据...
        xSemaphoreGive(xBufferSemaphore); // 归还
    }
    else
    {
        Log_Error("No buffer available");
    }
}

我的习惯: 信号量名字要起好。我曾经用「sem1」「sem2」这种名字,三个月后自己都看不懂。现在我都用「xAlarmSem」「xBufferSem」这种带功能描述的命名。

1.6 麻醉机中的综合应用场景

好了,我们把上面这些串起来,看看一个完整的麻醉机数据流:

  1. 传感器采集任务(优先级4):每10ms读取压力、流量、氧浓度,打包成结构体,通过消息队列发给数据记录任务。
  2. 数据记录任务(优先级3):从队列取数据,写入SD卡。同时通过信号量通知显示任务「有新数据了」。
  3. UI显示任务(优先级2):等待信号量,收到后从共享缓冲区读取最新数据,刷新屏幕。
  4. 报警任务(优先级5):监测关键参数,一旦超限立即触发报警。这个任务用中断方式唤醒,延迟必须小于50ms。

你看,每个任务各司其职,通过队列和信号量协作。这就是RTOS的魅力——复杂系统变得清晰可控。

重要提醒: 医疗设备里,任务优先级不能随便改。报警任务必须最高,这是底线。我曾经见过一个团队把UI任务优先级设得比报警还高,结果报警延迟了200ms——这在麻醉机里是绝对不能接受的。

1.7 本章小结

这一章我们聊了FreeRTOS的四个核心概念:

  • 任务创建:用xTaskCreate(),注意栈大小和函数结构
  • 任务调度:抢占式,优先级决定谁先跑
  • 消息队列:任务间安全传数据,避免全局变量
  • 信号量:协调资源,中断与任务同步的好帮手

下一章,我们会深入麻醉机的数据记录模块,看看怎么用FreeRTOS实现可靠的数据存储。到时候我会分享一个我踩过的坑——SD卡写入时掉电,数据全丢了……嗯,那是个让人难忘的教训。

记住:RTOS不是银弹,但用好了,你的嵌入式系统会像瑞士钟表一样精准可靠。