3. 任务创建与管理:任务函数原型、任务创建API、任务删除与挂起、任务优先级、任务堆栈大小估算
好,咱们进入第三章。这一章讲的是RTOS里最基础、也最核心的操作——怎么把任务“生”出来,怎么管好它,以及万一出问题了怎么让它“休眠”或者“消失”。
我刚开始用RTOS做制氧机项目时,犯过一个低级错误:任务堆栈给得太小,结果系统跑着跑着就莫名其妙地死机了。排查了整整两天,最后发现是堆栈溢出了。从那以后,我对任务创建这件事就格外上心。今天我把这些经验都倒给你。
3.1 任务函数原型
每个任务本质上就是一个C函数。但RTOS对它有特殊要求。说白了,这个函数不能像普通函数那样随便return。为什么呢?因为任务一旦return,RTOS就不知道它该干嘛了,系统会直接挂掉。
任务函数的原型长这样:
void vTaskFunction(void *pvParameters)
{
// 任务初始化代码
for(;;)
{
// 任务主体代码
// 等待事件、处理数据、控制外设等
vTaskDelay(pdMS_TO_TICKS(10)); // 让出CPU
}
}
注意看,函数返回类型是void,参数是一个void指针。这个指针可以传任何东西——整数、结构体、字符串,都行。我个人习惯用结构体传参,这样扩展性好。
还有一点:任务函数里必须有一个无限循环。为什么?因为任务被创建后,RTOS会一直调度它。如果它执行完了,RTOS会调用一个内部错误处理函数,默认就是挂起系统。嗯,这里要注意,千万别让任务跑到底。
3.2 任务创建API
FreeRTOS里创建任务主要用两个API:xTaskCreate 和 xTaskCreateStatic。前者动态分配堆栈,后者用你预先准备好的静态内存。
我一般在制氧机项目里用动态创建,省心。但如果你对内存有严格限制,比如某些工业级控制器,那就得用静态创建。
来看看xTaskCreate的原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位是字,不是字节)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值是pdPASS表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示内存不够。我记得有一次调试,死活创建不了任务,最后发现是FreeRTOS的堆大小配置得太小了。
举个例子,创建一个控制氧气浓度的任务:
TaskHandle_t xO2ControlHandle = NULL;
void vO2ControlTask(void *pvParameters)
{
uint16_t *pO2Setpoint = (uint16_t *)pvParameters;
for(;;)
{
// 读取氧传感器
// PID计算
// 控制电磁阀
vTaskDelay(pdMS_TO_TICKS(50));
}
}
// 在main函数或初始化中创建
uint16_t usO2Setpoint = 93; // 目标氧浓度93%
xTaskCreate(
vO2ControlTask,
"O2Control",
256, // 堆栈大小,单位是字
(void *)&usO2Setpoint,
3, // 优先级
&xO2ControlHandle
);
3.3 任务删除与挂起
任务不是创建了就完事了。有时候我们需要暂停某个任务,或者彻底把它干掉。
任务删除:
用 vTaskDelete() 函数。传入任务句柄,RTOS就会把这个任务从调度列表中移除,并释放它占用的内存(如果是动态创建的)。
// 删除自己
vTaskDelete(NULL);
// 删除其他任务
vTaskDelete(xO2ControlHandle);
任务挂起与恢复:
挂起和删除不一样。挂起只是让任务暂时“睡着”,它还在内存里,随时可以唤醒。这在制氧机里很有用。比如,当系统检测到传感器故障时,可以挂起控制任务,等故障恢复后再唤醒。
// 挂起任务
vTaskSuspend(xO2ControlHandle);
// 恢复任务
vTaskResume(xO2ControlHandle);
还有一个常用的API是 vTaskSuspendAll(),它会挂起所有任务。我一般在做系统级配置更新时用这个,确保操作是原子的。
3.4 任务优先级
优先级是RTOS调度的核心。FreeRTOS支持0到(configMAX_PRIORITIES - 1)的优先级,数值越大优先级越高。
在制氧机里,我通常这样分配优先级:
| 优先级 | 任务名称 | 说明 |
|---|---|---|
| 5(最高) | 安全监控任务 | 监测氧浓度、压力、流量,一旦超标立即报警并停机 |
| 4 | 传感器采集任务 | 定时读取氧传感器、压力传感器数据 |
| 3 | 控制算法任务 | PID计算、阀门控制 |
| 2 | 人机交互任务 | 按键扫描、LCD显示更新 |
| 1 | 通信任务 | 与上位机或云平台通信 |
| 0(最低) | 空闲任务 | 系统空闲时运行,通常用于低功耗管理 |
你想想看,如果安全监控任务的优先级不够高,万一氧浓度超标了,它抢不到CPU,那后果就严重了。所以安全相关的任务一定要给最高优先级。
3.5 任务堆栈大小估算
这是最容易踩坑的地方。堆栈给大了浪费内存,给小了系统崩溃。我见过太多人在这上面翻车了。
堆栈大小取决于几个因素:
- 函数调用深度: 任务里调用的函数越多、嵌套越深,需要的堆栈越大
- 局部变量大小: 特别是数组、结构体这类占用空间大的变量
- 中断嵌套: 中断服务函数也会使用当前任务的堆栈
- RTOS API调用: 某些API内部会使用额外的堆栈空间
我有个估算方法,你可以参考:
- 先给一个保守值,比如256字(1024字节)
- 运行系统,用 uxTaskGetStackHighWaterMark() 函数查看实际使用量
- 根据结果调整,一般留出30%-50%的余量
举个例子:
// 在任务中调用,查看剩余堆栈空间
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
printf("剩余堆栈: %d 字\n", uxHighWaterMark);
如果剩余堆栈接近0,说明堆栈给得太小了。我曾经在一个项目中,把通信任务的堆栈从128字加到512字才稳定下来。原因是那个任务里用了一个很大的JSON解析缓冲区。
好了,这一章的内容就这些。任务创建和管理是RTOS的基石,你把这些搞明白了,后面的状态机设计才能稳稳地往上搭。下一章我们讲任务间的同步与通信,那才是真正让多个任务协同工作的关键。