第4章 任务创建与管理:从零开始搭建你的RTOS应用
好,咱们进入正题。任务,是RTOS里最核心的概念。说白了,整个系统就是一堆任务在轮流干活。这一章,我就带你手把手把任务创建、参数传递、优先级设置、堆栈分配这些基本功练扎实。
我记得刚接触FreeRTOS那会儿,总觉得任务创建就是个函数调用,没什么大不了的。结果第一次做项目,就因为堆栈设小了,程序跑着跑着就莫名其妙死机。嗯,这种坑,咱们今天一并填上。
4.1 任务创建函数 xTaskCreate 深度解析
任务创建的入口函数就是 xTaskCreate。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(仅用于调试)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度,单位是字(word)
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级(0 ~ configMAX_PRIORITIES-1)
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值只有两种可能:pdPASS 表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示内存不够。我建议你每次调用后都检查返回值,别偷懒。
关键点:任务函数必须是一个永不返回的死循环。如果函数执行到了末尾,FreeRTOS 会调用 vTaskDelete(NULL) 自动删除该任务。但我不推荐依赖这个行为——显式控制更安全。
4.2 任务参数传递:不只是传个数字那么简单
任务参数 pvParameters 是一个 void * 指针。这意味着你可以传任何东西进去:整数、结构体指针、字符串,甚至另一个任务的句柄。
我常用的几种传参方式:
- 传整数:直接把整数强转为
void *,任务里再转回来。适合简单的ID或标志位。 - 传结构体指针:多个参数打包成一个结构体,传指针。注意结构体生命周期必须覆盖任务运行期间。
- 传字符串:传
char *,任务里直接使用。小心字符串存储位置——别传局部变量。
来看个实际例子:
typedef struct {
uint8_t led_id;
uint16_t blink_rate_ms;
} led_param_t;
led_param_t led1_param = { .led_id = 1, .blink_rate_ms = 500 };
void vLEDTask(void *pvParameters) {
led_param_t *param = (led_param_t *)pvParameters;
while(1) {
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
vTaskDelay(pdMS_TO_TICKS(param->blink_rate_ms));
}
}
xTaskCreate(vLEDTask, "LED1", 128, &led1_param, 2, NULL);
我曾经踩过的坑:传局部变量地址给任务。任务还没开始跑,局部变量已经出栈了。结果任务拿到的全是垃圾数据。解决办法:要么用全局变量,要么用动态分配的堆内存,要么用静态分配的结构体。
4.3 任务优先级与堆栈设置:平衡的艺术
优先级决定了任务什么时候能抢到CPU。FreeRTOS 里,数值越大优先级越高。0 是最低优先级,configMAX_PRIORITIES - 1 是最高。
| 优先级范围 | 典型用途 | 注意事项 |
|---|---|---|
| 0 ~ 2 | 后台任务、空闲任务 | 空闲任务优先级固定为0 |
| 3 ~ 5 | 普通业务任务 | 大多数任务放这个区间 |
| 6 ~ 7 | 实时性要求高的任务 | 小心优先级反转 |
堆栈大小怎么定?这是新手最容易懵的地方。我一般按这个经验来:
- 简单任务(只调几个函数,无局部大数组):128 ~ 256 字
- 中等任务(有局部数组、调用printf等):256 ~ 512 字
- 复杂任务(有递归、大结构体):512 ~ 1024 字
STM32L4 是 Cortex-M4 内核,一个字是 4 字节。所以 128 字 = 512 字节。你想想看,如果设成 64 字,才 256 字节,稍微嵌套几层函数调用就溢出了。
我的调试习惯:先用 uxTaskGetStackHighWaterMark() 查看任务堆栈剩余量。如果剩余量小于堆栈总量的 20%,我就加大堆栈。别靠猜,用数据说话。
4.4 任务删除与任务句柄:善始善终
任务删除用 vTaskDelete()。参数传任务句柄,传 NULL 表示删除自己。
TaskHandle_t xTask1Handle = NULL;
void vTask1(void *pvParameters) {
while(1) {
// 干点活...
if(should_exit) {
vTaskDelete(NULL); // 删除自己
}
}
}
// 在其他任务中删除 task1
vTaskDelete(xTask1Handle);
任务句柄 TaskHandle_t 是个好东西。有了它,你可以在任务外部控制这个任务:挂起、恢复、删除、获取状态。创建任务时把句柄存起来,就像给任务办了个身份证。
注意:任务删除后,它占用的堆栈和TCB内存并不会自动释放回堆——除非你用的是动态内存管理并且开启了 configUSE_TRASH_FREE。在 STM32L4 这种资源受限的芯片上,频繁创建删除任务可能导致内存碎片。我建议:能复用就别删。
4.5 知识体系总览
下面这张图把任务创建与管理的核心流程串起来了。你可以把它当作本章的思维导图:
4.6 实战建议:从简单开始
如果你刚开始用 FreeRTOS,我建议你这样做:
- 先创建两个任务,一个闪灯,一个打印串口。优先级都设成 1。
- 用
uxTaskGetStackHighWaterMark()看堆栈余量,调整到合理值。 - 试着改优先级,观察任务执行顺序的变化。
- 最后试试用句柄删除和恢复任务。