第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 麻醉机中的综合应用场景
好了,我们把上面这些串起来,看看一个完整的麻醉机数据流:
- 传感器采集任务(优先级4):每10ms读取压力、流量、氧浓度,打包成结构体,通过消息队列发给数据记录任务。
- 数据记录任务(优先级3):从队列取数据,写入SD卡。同时通过信号量通知显示任务「有新数据了」。
- UI显示任务(优先级2):等待信号量,收到后从共享缓冲区读取最新数据,刷新屏幕。
- 报警任务(优先级5):监测关键参数,一旦超限立即触发报警。这个任务用中断方式唤醒,延迟必须小于50ms。
你看,每个任务各司其职,通过队列和信号量协作。这就是RTOS的魅力——复杂系统变得清晰可控。
重要提醒: 医疗设备里,任务优先级不能随便改。报警任务必须最高,这是底线。我曾经见过一个团队把UI任务优先级设得比报警还高,结果报警延迟了200ms——这在麻醉机里是绝对不能接受的。
1.7 本章小结
这一章我们聊了FreeRTOS的四个核心概念:
- 任务创建:用xTaskCreate(),注意栈大小和函数结构
- 任务调度:抢占式,优先级决定谁先跑
- 消息队列:任务间安全传数据,避免全局变量
- 信号量:协调资源,中断与任务同步的好帮手
下一章,我们会深入麻醉机的数据记录模块,看看怎么用FreeRTOS实现可靠的数据存储。到时候我会分享一个我踩过的坑——SD卡写入时掉电,数据全丢了……嗯,那是个让人难忘的教训。
记住:RTOS不是银弹,但用好了,你的嵌入式系统会像瑞士钟表一样精准可靠。