2. 实时操作系统(RTOS)基础:FreeRTOS任务管理、任务优先级、任务状态转换、调度策略
好,咱们直接进入正题。这一章聊的是RTOS的基石——任务管理。说白了,就是让单片机学会“多线程”思考。你想想看,一个医疗设备要同时处理按键扫描、屏幕刷新、数据采集、报警输出,如果还用裸机的大循环,代码很快就会变成一团乱麻。
我个人习惯,在开始任何医疗设备项目之前,先把任务骨架搭好。这就像盖房子先画结构图,后面才不会返工。FreeRTOS是我用得最多的内核,轻量、稳定,而且社区资源丰富。咱们今天就把它掰开揉碎了讲。
2.1 任务是什么?
任务,在RTOS里就是一个无限循环的函数。它有自己的栈空间、优先级和状态。每个任务都觉得自己独占了CPU,但实际上内核在飞快地切换它们。
void vTaskFunction(void *pvParameters)
{
// 初始化代码,只执行一次
for(;;)
{
// 任务主体代码
// 等待事件、处理数据、输出结果
vTaskDelay(pdMS_TO_TICKS(100)); // 让出CPU
}
}
嗯,这里要注意:任务函数绝对不能返回。一旦返回,系统就会调用vTaskDelete(NULL)把自己干掉。我在项目中遇到过新手把for(;;)写成while(1)然后忘记加延时,结果低优先级任务永远得不到执行——这就是典型的“饿死”现象。
2.2 任务优先级:谁更重要?
优先级是RTOS调度的心脏。FreeRTOS支持从0到(configMAX_PRIORITIES-1)的优先级,数值越大,优先级越高。
| 优先级 | 典型用途 | 实时性要求 |
|---|---|---|
| 最高 (N-1) | 中断响应、紧急停止 | 微秒级 |
| 高 | 传感器数据采集、控制算法 | 毫秒级 |
| 中 | 人机交互、通信协议 | 10-100毫秒 |
| 低 | 日志记录、看门狗喂狗 | 秒级 |
我曾经在一个输液泵项目上踩过这个坑。一个低优先级任务占用了互斥锁,中优先级任务一直在运行,导致高优先级任务拿不到锁,整个系统响应延迟。病人输液速度失控了3秒钟才恢复。从那以后,我强制要求所有医疗项目必须使用优先级继承协议,并且严格控制优先级数量,不超过5级。
2.3 任务状态转换:任务的一生
一个任务在生命周期里有四种状态:运行态、就绪态、阻塞态、挂起态。理解这个转换,是调试RTOS问题的基本功。
我习惯在任务循环里加一个GPIO翻转,用示波器看波形。如果某个任务的波形消失了,说明它卡在阻塞态或者被挂起了。这比翻日志快得多。
2.4 调度策略:谁先跑?
FreeRTOS用的是基于优先级的抢占式调度,加上同优先级任务的时间片轮转。说白了就是:高优先级的任务随时可以打断低优先级的任务;同优先级的任务轮流跑,每人一个时间片。
// 创建两个同优先级任务
xTaskCreate(vTask1, "Task1", 256, NULL, 2, NULL);
xTaskCreate(vTask2, "Task2", 256, NULL, 2, NULL);
// 创建高优先级任务
xTaskCreate(vHighTask, "High", 256, NULL, 3, NULL);
你想想看,如果vHighTask一直不阻塞,那Task1和Task2永远没机会跑。这就是为什么我反复强调:高优先级任务必须设计成“短小精悍”——干活快,然后主动阻塞等待下一个事件。
- 安全关键任务(如电机控制、报警输出)放在最高优先级
- 数据采集任务放在次高优先级,使用定时器触发
- 人机交互任务放在中等优先级,允许被数据采集打断
- 日志、统计等非实时任务放在最低优先级
- 所有任务必须包含阻塞点(延时、等待队列、等待信号量)
2.5 任务创建与删除:生与死
创建任务用xTaskCreate(),删除任务用vTaskDelete()。但要注意,删除任务时要确保它持有的资源已经释放,否则会造成内存泄漏。
TaskHandle_t xTaskHandle = NULL;
void vStartMonitorTask(void)
{
xTaskCreate(vMonitorTask, "Monitor", 512, NULL, 3, &xTaskHandle);
}
void vStopMonitorTask(void)
{
if(xTaskHandle != NULL)
{
vTaskDelete(xTaskHandle);
xTaskHandle = NULL; // 防止野指针
}
}
在一个心电监护仪项目中,我动态创建了一个数据压缩任务,用完后直接删除了。结果忘了这个任务里申请了一个DMA缓冲区。系统跑了72小时后,内存碎片导致新任务创建失败,设备死机。从那以后,我改用静态任务创建,并且所有任务在系统初始化阶段就全部创建好,运行期间只挂起/恢复,不删除。
2.6 任务堆栈:别让栈溢出了
每个任务都有自己的栈空间。栈大小设置是个技术活——设大了浪费RAM,设小了会溢出。医疗设备通常RAM有限,所以得精打细算。
| 任务类型 | 推荐栈大小 (words) | 说明 |
|---|---|---|
| 简单IO任务 | 128-256 | 只有几个局部变量和函数调用 |
| 通信协议任务 | 256-512 | 需要处理缓冲区、状态机 |
| 控制算法任务 | 512-1024 | 可能有浮点运算、矩阵计算 |
| GUI任务 | 1024-2048 | 字体渲染、窗口管理很吃栈 |
FreeRTOS提供了uxTaskGetStackHighWaterMark()函数,可以查看任务栈的剩余空间。我习惯在系统稳定运行24小时后,读取所有任务的水位标记,然后加上20%的余量作为最终栈大小。
2.7 实战经验:任务设计七步法
最后,分享一个我用了多年的任务设计流程。每次开始新项目,我都会走一遍这个流程,确保不遗漏关键点。
- 列出所有功能模块——按键、显示、采集、通信、报警、存储
- 确定实时性要求——哪些必须在1ms内响应?哪些可以容忍100ms?
- 分配优先级——按实时性从高到低排序,最高优先级不超过5个
- 设计任务间通信——用队列传数据,用信号量做同步,用互斥锁保护共享资源
- 估算栈大小——先给一个保守值,跑起来后用水位标记优化
- 编写任务函数——每个任务一个文件,函数名清晰表明用途
- 压力测试——模拟最坏情况,检查是否有任务饿死或栈溢出