任务创建与管理:任务状态机、优先级设计与堆栈优化

大家好,我是你们的老朋友。今天我们来聊聊RTOS里最核心的东西——任务管理。说白了,任务就是你的程序里一个个独立执行的小单元。在除颤仪这种设备里,一个任务负责心电采样,一个任务负责高压充放电,还有一个任务负责用户界面。它们怎么跑、怎么切换、怎么不出乱子,这就是我们今天要啃的硬骨头。

一、任务状态机:就绪、运行、阻塞、挂起

任务不是一直占着CPU跑的。它像人一样,有各种状态。我习惯把任务状态机理解成四个房间:

  • 运行态:任务正在使用CPU,真正在执行代码。同一时刻只有一个任务能在这个房间。
  • 就绪态:任务啥都准备好了,就等CPU空闲下来。说白了就是排队等叫号。
  • 阻塞态:任务在等某个事件,比如等一个信号量、等延时结束、等数据从外设来。这时候它不消耗CPU。
  • 挂起态:任务被强制暂停,通常是被其他任务或中断调用了挂起函数。它不参与调度,直到被恢复。

我在项目中遇到过一个问题:有个新手把除颤仪的充电任务写成了死循环轮询,结果CPU一直被占着,心电采样任务根本得不到执行。嗯,这就是典型的没有用好阻塞态。正确的做法是:充电任务在等待电压达到目标值时,应该调用延时或等待事件,主动让出CPU。

核心要点:任务状态切换是由RTOS内核管理的。你写代码时,只需要调用API函数(比如vTaskDelay、xSemaphoreTake),内核会自动帮你完成状态迁移。千万别自己用while(1)去轮询,那是裸机思维。

二、任务优先级设计:别让高优先级饿死低优先级

优先级设计,说白了就是给任务排个座次。在FreeRTOS里,数值越大优先级越高。但这里有个坑——优先级反转和优先级继承。

我记得有一次调试除颤仪的通信模块,发现一个低优先级的打印任务偶尔会卡住整个系统。查了半天,原来是它占着一个互斥锁,高优先级的控制任务在等这个锁,而中优先级的其他任务又抢占了CPU。这就是经典的优先级反转。

我的建议是:

  • 关键实时任务(比如心电采样、高压控制)给最高优先级。这些任务必须准时响应,延迟几毫秒都可能出人命。
  • 中等优先级给通信、数据处理这类任务。它们重要,但可以容忍少量延迟。
  • 低优先级给界面刷新、日志打印、自检等后台任务。这些任务跑慢点没关系。

避坑指南:我曾经把界面刷新任务设成了最高优先级,结果采样任务被饿死了。你想想看,界面刷新再快,病人也看不出60帧和30帧的区别。但采样延迟1ms,波形就失真了。所以,优先级一定要按实时性需求来排,别按代码重要性来排。

另外,我建议你使用优先级继承协议。很多RTOS都支持,它能临时提升持有锁的任务的优先级,避免反转问题。在FreeRTOS里,使用互斥量(xSemaphoreCreateMutex)就自动带优先级继承功能。

三、任务堆栈大小估算与优化

任务堆栈,说白了就是任务的临时工作台。每个任务都有自己的栈空间,用来存局部变量、函数调用时的返回地址、中断现场等。堆栈设小了,任务跑着跑着就栈溢出,系统崩溃。设大了,RAM浪费,成本上升。

在除颤仪这种医疗设备里,RAM是寸土寸金的。我见过一个项目,每个任务都给了2KB的栈,结果8个任务吃掉了16KB RAM,而实际只需要不到一半。嗯,这里要注意,堆栈大小不是拍脑袋定的。

3.1 估算方法

我个人习惯用以下方法估算:

  1. 静态分析:列出任务里所有函数的局部变量大小,加上函数调用深度(嵌套层数),再加上中断嵌套的栈开销。比如一个任务调用了3层函数,每层有100字节局部变量,加上中断可能用掉200字节,那至少需要500字节。
  2. 动态测量:在任务里写一个栈填充函数,比如把所有栈空间初始化为0xAA,跑一段时间后检查栈空间被用掉了多少。FreeRTOS提供了uxTaskGetStackHighWaterMark()函数,能告诉你任务栈还剩多少。

实战技巧:我通常先给一个保守值(比如1KB),跑完所有功能后,用高水位标记函数查看实际使用量,然后加上20%的安全余量。比如实际用了600字节,我就设成768字节(0.75KB)。这样既不浪费,又留了余地。

3.2 优化策略

堆栈优化不是让你去抠字节,而是从设计上减少栈使用:

  • 避免深层函数调用:递归调用在RTOS里是大忌。我见过有人用递归做菜单导航,结果栈直接爆了。改成状态机实现,栈使用量从1KB降到了200字节。
  • 大数组用静态或堆分配:比如一个512字节的采样缓冲区,如果定义成局部变量,每次调用函数都会压栈。改成全局数组或动态分配,栈压力就小多了。
  • 中断服务程序要短:中断里尽量只做标记,实际处理放到任务里。因为中断用的是任务栈,中断嵌套多了,栈很容易溢出。

警告:千万不要为了省RAM而把栈设得刚刚好。我曾经在除颤仪原型机上把栈设到只比实际用量多10%,结果一次异常中断嵌套直接栈溢出,系统复位。医疗设备里,栈溢出可能导致误放电或漏放电,这是人命关天的事。安全余量至少20%,我建议30%。

四、实战示例:一个除颤仪的任务设计

下面是我在课程里用的一个简化示例,展示如何创建和管理任务:

// 任务优先级定义
#define PRIO_SAMPLE     5   // 心电采样,最高
#define PRIO_CHARGE     4   // 高压充电
#define PRIO_COMM       3   // 通信处理
#define PRIO_DISPLAY    2   // 界面刷新
#define PRIO_LOG        1   // 日志记录

// 任务堆栈大小(单位:字,32位系统一个字=4字节)
#define STACK_SAMPLE    256 // 约1KB
#define STACK_CHARGE    128 // 约0.5KB
#define STACK_COMM      192 // 约0.75KB
#define STACK_DISPLAY   128 // 约0.5KB
#define STACK_LOG       64  // 约0.25KB

// 创建任务
xTaskCreate(vTaskSample, "Sample", STACK_SAMPLE, NULL, PRIO_SAMPLE, NULL);
xTaskCreate(vTaskCharge, "Charge", STACK_CHARGE, NULL, PRIO_CHARGE, NULL);
xTaskCreate(vTaskComm,   "Comm",   STACK_COMM,   NULL, PRIO_COMM,   NULL);
xTaskCreate(vTaskDisplay,"Display",STACK_DISPLAY, NULL, PRIO_DISPLAY,NULL);
xTaskCreate(vTaskLog,    "Log",    STACK_LOG,    NULL, PRIO_LOG,    NULL);

你看,每个任务都有明确的优先级和栈大小。采样任务最高优先级,栈也最大,因为它要处理中断和缓冲区。日志任务最低优先级,栈最小,因为它只是偶尔写个字符串。

在实际项目中,我还会加一个看门狗任务,优先级设为最高(比采样还高),专门监控其他任务是否正常运行。如果某个任务超过预定时间没响应,看门狗就触发系统复位。这在医疗设备里是标配。

五、总结一下

任务创建与管理,说白了就是三件事:

  • 状态机:让任务该跑的时候跑,该等的时候等,别占着CPU不放。
  • 优先级:按实时性需求排座次,别让高优先级任务饿死低优先级,也别让低优先级任务堵住高优先级。
  • 堆栈:估算要准,余量要足,优化要巧。别为了省几字节RAM而牺牲系统稳定性。

我记得刚入行时,带我的老工程师说过一句话:「RTOS不是让你把裸机代码搬过来加个任务壳子,而是让你重新思考程序的执行逻辑。」这句话我一直记到现在。你想想看,是不是这个道理?

好,今天就聊到这里。下一章我们讲任务间通信——信号量、消息队列、事件标志组,这些都是让任务之间「说话」的工具。到时候见。