任务管理基础:任务创建与删除
各位同学,今天我们来聊聊FreeRTOS最核心的东西——任务管理。说白了,任务就是RTOS里干活的最小单元。你想想看,一个单片机要同时处理LED闪烁、串口打印、按键扫描,没有任务管理,全靠一个大循环硬撑,那代码写起来得多痛苦?
我个人习惯,拿到一个新项目,第一件事就是先把任务框架搭好。任务怎么创建、怎么删除、优先级怎么配、堆栈给多大,这些基础打不牢,后面调试起来会非常头疼。我记得刚用FreeRTOS那会儿,就因为堆栈配小了,程序跑着跑着就莫名其妙复位,查了整整两天才找到原因。
xTaskCreate:任务的诞生
创建任务,用的是 xTaskCreate() 这个API。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度,单位是字
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄,用来后续操作
);
这里有个坑,我必须要提醒你。usStackDepth 的单位是「字」,不是字节!在32位MCU上,一个字是4字节。所以如果你写 128,实际分配的堆栈大小是 128 * 4 = 512 字节。我曾经见过有人把这个当字节写,结果堆栈溢出,程序跑飞,查了半天才反应过来。
uxTaskGetStackHighWaterMark() 查看实际使用量,再调整到合适的值。这样既省内存,又不会溢出。
vTaskDelete:任务的终结
有创建就有删除。vTaskDelete() 用来删除一个任务。调用后,任务会被从就绪列表、阻塞列表等所有内核数据结构中移除。但要注意,任务自己占用的堆栈和TCB(任务控制块)所占的内存,不会自动释放!
为什么会这样?因为FreeRTOS不知道你用的是动态内存分配还是静态内存分配。如果你是用 pvPortMalloc 分配的,那删除任务后需要手动调用 vPortFree 释放。嗯,这里要特别小心,我曾经在一个项目中频繁创建和删除任务,结果内存泄漏,跑了三天后系统挂了。
// 创建任务
TaskHandle_t xLEDTaskHandle = NULL;
xTaskCreate(vLEDTask, "LED", 128, NULL, 1, &xLEDTaskHandle);
// 删除任务
if (xLEDTaskHandle != NULL) {
vTaskDelete(xLEDTaskHandle);
xLEDTaskHandle = NULL; // 记得置空,避免野指针
}
任务优先级与堆栈配置
优先级,说白了就是任务的「插队权」。数值越大,优先级越高。FreeRTOS支持抢占式调度,高优先级任务就绪时,会立即抢占低优先级任务的CPU使用权。
配置优先级时,我建议你遵循一个原则:够用就好。不要为了省事把所有任务都设成同一个优先级,也不要搞太多优先级层级。一般来说,3-5个优先级就够用了。优先级太多,调度开销会变大,而且容易出优先级反转的问题。
| 优先级 | 典型用途 | 堆栈建议 |
|---|---|---|
| 最高(如5) | 时间敏感任务(如电机控制) | 256-512字 |
| 中等(如3) | 周期性任务(如LED闪烁) | 128-256字 |
| 最低(如1) | 后台任务(如串口打印) | 128-256字 |
堆栈配置这块,我踩过的坑最多。你想想看,任务函数里调用的函数、局部变量、中断嵌套,都会消耗堆栈。特别是用printf这类函数,堆栈消耗非常大。我一般会给串口打印任务多分配一些堆栈,比如256字起步。
任务状态转换:运行、就绪、阻塞、挂起
FreeRTOS的任务有四种状态,理解它们之间的转换关系,是掌握任务调度的关键。
- 运行态(Running):正在使用CPU。单核MCU上,同一时刻只有一个任务处于运行态。
- 就绪态(Ready):任务可以运行,但CPU被更高优先级的任务占着。说白了就是「排队等CPU」。
- 阻塞态(Blocked):任务在等待某个事件,比如延时到期、队列有数据、信号量可用。此时任务不参与调度,不消耗CPU。
- 挂起态(Suspended):通过
vTaskSuspend()主动让任务休眠。只有调用vTaskResume()才能唤醒。
状态转换的典型场景是这样的:
- 任务A创建后进入就绪态。
- 调度器选中任务A,它进入运行态。
- 任务A调用
vTaskDelay(100),进入阻塞态。 - 100ms后延时到期,任务A回到就绪态。
- 如果此时没有更高优先级的任务,任务A再次进入运行态。
嗯,这里要注意一个细节:vTaskDelay() 是相对延时,从调用时刻开始算。而 vTaskDelayUntil() 是绝对延时,适合做周期性任务,不会因为任务执行时间抖动而累积误差。
实战:创建LED闪烁与串口打印任务
光说不练假把式。我们直接上手写一个实际例子。这个项目里,我创建两个任务:一个负责LED闪烁,一个负责串口打印。LED任务优先级设为2,串口任务优先级设为1。
#include "FreeRTOS.h"
#include "task.h"
// LED闪烁任务
void vLEDTask(void *pvParameters) {
(void)pvParameters; // 避免编译器警告
// 初始化LED引脚(假设PA0)
// GPIO_Init();
for (;;) {
// 点亮LED
// GPIO_SetBits(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
// 熄灭LED
// GPIO_ResetBits(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
}
}
// 串口打印任务
void vUARTTask(void *pvParameters) {
(void)pvParameters;
// 初始化串口
// UART_Init(115200);
uint32_t ulCount = 0;
for (;;) {
printf("系统运行时间: %lu 秒\r\n", ulCount++);
vTaskDelay(pdMS_TO_TICKS(1000)); // 每秒打印一次
}
}
int main(void) {
// 硬件初始化
// SystemClock_Config();
// GPIO_Init();
// UART_Init();
// 创建任务
xTaskCreate(vLEDTask, "LED", 128, NULL, 2, NULL);
xTaskCreate(vUARTTask, "UART", 256, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for (;;);
}
- LED任务优先级高(2),串口任务优先级低(1)。当LED任务阻塞时,串口任务才能运行。
- 串口任务的堆栈给了256字,因为printf比较吃堆栈。LED任务128字就够了。
- 两个任务都用了
vTaskDelay()实现周期性执行,没有浪费CPU空转。
你可能会问:为什么LED任务优先级要设得比串口高?其实这取决于你的需求。如果LED闪烁的实时性要求高,比如做呼吸灯效果,那优先级就得高一些。如果只是简单指示,优先级低一点也无所谓。我个人习惯,把时间敏感的任务优先级设高,把后台打印类的任务优先级设低。
好了,任务管理的基础就讲到这里。下一章我们会深入任务间的通信机制,到时候你会看到,任务之间怎么传数据、怎么同步,那才是RTOS真正发挥威力的地方。