4、任务创建与管理:xTaskCreate、任务优先级、任务状态、任务删除
好,咱们进入正题。任务创建与管理,这是 FreeRTOS 里最基础、也最核心的操作。说白了,你写嵌入式程序,如果不用 RTOS,那就是一个大 while(1) 循环在那里转。但用了 RTOS 之后,你的代码就被拆成了一个个独立的任务,每个任务都有自己的“灵魂”。
我个人习惯,拿到一个新项目,第一件事就是先把任务框架搭起来。任务怎么建、优先级怎么分、什么时候删,这些搞清楚了,后面的开发会顺很多。今天我就把这几个关键点掰开揉碎了讲给你听。
4.1 xTaskCreate:任务的“出生证明”
每个任务在 FreeRTOS 里都是一个独立的执行流。要创建一个任务,就得用 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 当成字节数来填。比如想给 512 字节,就填 512。错了!在 32 位处理器上,一个“字”是 4 字节,所以 512 字其实是 2048 字节。我曾经在项目里见过有人填了 128,结果任务一跑就栈溢出,系统直接挂掉。排查了半天才发现是栈大小没算对。
举个例子,创建一个简单的 LED 闪烁任务:
void vLEDTask(void *pvParameters)
{
while(1)
{
GPIO_Toggle(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void)
{
TaskHandle_t xLEDHandle = NULL;
xTaskCreate(
vLEDTask, // 任务函数
"LED Task", // 任务名
128, // 栈大小:128字 = 512字节
NULL, // 无参数
2, // 优先级 2
&xLEDHandle // 保存句柄
);
vTaskStartScheduler();
}
你看,任务函数必须是一个无限循环。如果函数返回了,FreeRTOS 会调用 vTaskDelete(NULL) 把这个任务干掉。所以,任务函数里千万别写 return,除非你打算让它“自杀”。
uxTaskGetStackHighWaterMark() 查看实际使用量,然后精调。这样既不会浪费内存,也不会因为栈溢出而翻车。
4.2 任务优先级:谁先跑,谁后跑
优先级这个东西,说白了就是给任务排个队。数字越大,优先级越高。FreeRTOS 支持 0 到 configMAX_PRIORITIES - 1 的优先级范围。0 是最低优先级,一般留给空闲任务用。
我见过不少新手喜欢把所有任务都设成同一个优先级。结果呢?系统变成了“轮询调度”,每个任务轮流跑一个时间片。如果你的任务对实时性有要求,比如按键响应、通信处理,那这种设计就很容易出问题。
为什么会这样?因为 FreeRTOS 的调度策略是:高优先级任务就绪时,立即抢占低优先级任务。所以,你应该把对时间敏感的任务放在高优先级,把后台处理、数据记录这类任务放在低优先级。
举个例子,一个血氧仪的任务优先级分配:
| 任务名称 | 优先级 | 说明 |
|---|---|---|
| vSensorReadTask | 5 | 传感器数据采集,需要高实时性 |
| vDisplayUpdateTask | 3 | 屏幕刷新,中等优先级 |
| vLogDataTask | 1 | 数据记录到 Flash,可以慢慢来 |
| Idle Task | 0 | 系统空闲任务,最低 |
你看,传感器读取任务优先级最高,因为它要保证采样间隔稳定。显示任务可以稍微慢一点,人眼看不出来。日志任务嘛,只要不丢数据,晚几毫秒写进去都行。
vTaskDelay() 才解决。
4.3 任务状态:运行、就绪、阻塞、挂起
一个任务在 FreeRTOS 里,有四种状态。你想想看,就像一个人一天的状态:正在干活(运行)、等着干活(就绪)、在睡觉等闹钟(阻塞)、被关禁闭了(挂起)。
- 运行态(Running):任务正在占用 CPU。单核处理器上,同一时刻只有一个任务处于运行态。
- 就绪态(Ready):任务已经准备好,随时可以运行,但 CPU 被更高优先级的任务占着。
- 阻塞态(Blocked):任务在等待某个事件,比如延时到期、信号量可用、队列有数据。这是最常见的状态。
- 挂起态(Suspended):任务被
vTaskSuspend()挂起,只有调用vTaskResume()才能恢复。
我个人习惯,在设计任务时,尽量让任务在大部分时间处于阻塞态。为什么?因为阻塞态的任务不消耗 CPU 时间。如果一个任务一直在就绪态空转,那它就是在浪费电、浪费算力。
举个例子,一个按键扫描任务:
void vKeyScanTask(void *pvParameters)
{
while(1)
{
if(Key_Read() != KEY_NONE)
{
// 处理按键
}
// 关键:让任务阻塞 20ms,而不是一直轮询
vTaskDelay(pdMS_TO_TICKS(20));
}
}
你看,加了 vTaskDelay() 之后,这个任务每 20ms 才检查一次按键,其余时间都在睡觉。CPU 就可以去干别的事了。
4.4 任务删除:该放手时就放手
任务不是创建了就一定要跑到天荒地老。有些任务,干完活就该删掉。比如血氧仪开机时,有一个自检任务,检查完传感器、屏幕、存储都正常后,这个任务就没用了,可以删掉。
删除任务用 vTaskDelete() 函数:
void vSelfTestTask(void *pvParameters)
{
// 执行自检
if(SelfTest_Pass())
{
// 自检通过,删除自己
vTaskDelete(NULL); // NULL 表示删除当前任务
}
else
{
// 自检失败,进入错误处理
ErrorHandler();
}
}
这里有个细节:vTaskDelete(NULL) 是删除当前任务。如果你想删除其他任务,需要传入那个任务的句柄。比如:
TaskHandle_t xTestHandle = NULL;
void vMainTask(void *pvParameters)
{
// 创建自检任务
xTaskCreate(vSelfTestTask, "SelfTest", 256, NULL, 3, &xTestHandle);
// 等待一段时间后,如果自检任务还没结束,强制删除它
vTaskDelay(pdMS_TO_TICKS(5000));
if(xTestHandle != NULL)
{
vTaskDelete(xTestHandle);
xTestHandle = NULL;
}
}
嗯,到这里,任务创建、优先级、状态、删除这几个核心概念就讲完了。你想想看,其实任务管理就像管理一个团队:创建任务就是招人,优先级就是定岗位级别,状态就是看每个人在干什么,删除任务就是让人离职。搞清楚了这些,你的 RTOS 程序就有了一个稳固的骨架。
下一章,我会讲任务间的通信与同步——队列、信号量、互斥量。这些才是让任务之间“说话”的关键。到时候见。