4、任务管理:任务创建与删除、任务优先级、任务状态、任务通知
4.1 任务创建与删除——别让任务“生而不养”
任务创建,是RTOS里最基础的操作。说白了,就是给CPU找活儿干。
我个人习惯用 xTaskCreate() 这个API。它的参数看着多,其实就五样东西:任务函数名、任务名字(调试用)、栈大小、参数、优先级、任务句柄。
// 创建一个LED闪烁任务
TaskHandle_t xLedTaskHandle = NULL;
void vLedTask(void *pvParameters)
{
while(1)
{
GPIO_Toggle(LED_PIN);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 在main函数中创建
xTaskCreate(
vLedTask, // 任务函数
"LED Task", // 任务名,调试时有用
128, // 栈大小,单位是字(4字节)
NULL, // 参数
2, // 优先级,数值越大优先级越高
&xLedTaskHandle // 任务句柄,后续删除或通知用
);
嗯,这里要注意:栈大小别给少了。我在项目中遇到过,一个任务莫名其妙跑飞,查了两天才发现是栈溢出。你想想看,任务里如果用了 printf() 或者局部大数组,栈需求会暴涨。我建议至少给256字起步,复杂任务给512甚至1024。
uxTaskGetStackHighWaterMark() 一查,剩余栈空间只剩4个字!差点就踩雷了。
删除任务呢?用 vTaskDelete()。但记住,任务自己可以删自己,传NULL就行。别的任务删它,要传句柄。
// 任务自己删除自己
void vSelfDeleteTask(void *pvParameters)
{
// 干完活,自己走人
vTaskDelete(NULL);
}
// 其他任务删除这个任务
vTaskDelete(xLedTaskHandle);
删除后,任务占用的栈和TCB(任务控制块)会被释放。但如果你用了动态内存分配,记得在任务退出前自己释放,RTOS不会帮你擦屁股。
4.2 任务优先级——谁急谁先跑
优先级,是RTOS调度的心脏。FreeRTOS里,数值越大优先级越高。0是最低,configMAX_PRIORITIES-1是最高。
| 优先级 | 典型用途 | 说明 |
|---|---|---|
| 0 | 空闲任务 | 系统自带,啥也不干时跑它 |
| 1-3 | 后台任务 | 数据记录、状态上报等 |
| 4-6 | 普通任务 | 业务逻辑、UI刷新等 |
| 7-9 | 实时任务 | 传感器读取、控制算法 |
| 10+ | 中断级任务 | 慎用,容易饿死低优先级任务 |
为什么会这样设计?因为高优先级任务就绪时,低优先级任务会被抢占。你想想看,如果有一个高优先级任务在死循环里跑,低优先级任务永远没机会执行。这就是所谓的“任务饿死”。
我在项目中遇到过,一个同事把通信任务优先级设得比显示任务还高。结果通信一忙,屏幕卡死。用户投诉说“死机了”。其实不是死机,是显示任务抢不到CPU。后来我把显示任务优先级调高了一级,问题解决。
4.3 任务状态——任务在干嘛?
RTOS里的任务,有四种状态:运行态、就绪态、阻塞态、挂起态。
- 运行态: 正在使用CPU。单核CPU同一时刻只有一个任务在运行。
- 就绪态: 能跑,但CPU被别的任务占着。排队等调度。
- 阻塞态: 在等某个事件。比如等延时、等信号量、等队列消息。
- 挂起态: 被
vTaskSuspend()暂停了。除非别人调用vTaskResume(),否则永远不跑。
说白了,任务状态就是任务当前在干什么。我经常用 vTaskGetInfo() 或者调试器查看任务状态,排查问题。
我记得有一次,系统跑着跑着就卡了。用调试器一看,所有任务都在阻塞态,就空闲任务在跑。原来是一个任务在等队列消息,但发消息的任务被挂起了。死锁了。嗯,这种问题很隐蔽,但状态一看就明白。
4.4 任务通知——轻量级的“传话”机制
任务通知,是FreeRTOS里一个很巧妙的设计。它比信号量、队列都快,而且省内存。
每个任务都有一个32位的通知值。你可以用 xTaskNotifyGive() 给任务发通知,任务用 ulTaskNotifyTake() 等通知。
// 任务A:等待通知
void vWaitingTask(void *pvParameters)
{
uint32_t ulNotifiedValue;
while(1)
{
// 等待通知,超时时间设为最大值
ulNotifiedValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if(ulNotifiedValue > 0)
{
// 收到通知,干活!
ProcessData();
}
}
}
// 任务B:发送通知
void vSenderTask(void *pvParameters)
{
while(1)
{
// 模拟数据到达
vTaskDelay(pdMS_TO_TICKS(100));
// 给任务A发通知
xTaskNotifyGive(xWaitingTaskHandle);
}
}
你想想看,如果用信号量,你得先创建、再获取、再释放。任务通知直接内置在TCB里,省掉了这些开销。我实测过,任务通知比二进制信号量快30%左右。
我在项目中遇到过,用任务通知做中断下半部处理。中断里发通知,任务里处理数据。这样中断服务程序执行时间极短,任务里慢慢处理。既保证了实时性,又不会阻塞中断。
嗯,这里要注意:任务通知不能用于中断服务程序和任务之间的通信?其实可以。FreeRTOS提供了 xTaskNotifyFromISR() 版本,专门给中断用。但记得检查返回值,防止通知队列满了。
xTaskNotifyGive(),结果编译报错。后来才发现,中断里必须用带 FromISR 后缀的API。这个坑,新手很容易踩。
总结一下:任务通知是轻量级、高效率的通信方式。适合简单场景。如果通信逻辑复杂,别硬用,该上队列就上队列。