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。

避坑指南: 我曾经在某个产品中,任务栈只给了128字,结果任务跑着跑着就挂了。后来用 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+ 中断级任务 慎用,容易饿死低优先级任务

为什么会这样设计?因为高优先级任务就绪时,低优先级任务会被抢占。你想想看,如果有一个高优先级任务在死循环里跑,低优先级任务永远没机会执行。这就是所谓的“任务饿死”。

核心原则: 优先级不是越高越好。够用就行。我一般只设3-5个优先级层级,多了反而容易出问题。

我在项目中遇到过,一个同事把通信任务优先级设得比显示任务还高。结果通信一忙,屏幕卡死。用户投诉说“死机了”。其实不是死机,是显示任务抢不到CPU。后来我把显示任务优先级调高了一级,问题解决。

4.3 任务状态——任务在干嘛?

RTOS里的任务,有四种状态:运行态、就绪态、阻塞态、挂起态。

  • 运行态: 正在使用CPU。单核CPU同一时刻只有一个任务在运行。
  • 就绪态: 能跑,但CPU被别的任务占着。排队等调度。
  • 阻塞态: 在等某个事件。比如等延时、等信号量、等队列消息。
  • 挂起态:vTaskSuspend() 暂停了。除非别人调用 vTaskResume(),否则永远不跑。

说白了,任务状态就是任务当前在干什么。我经常用 vTaskGetInfo() 或者调试器查看任务状态,排查问题。

调试技巧: 如果任务一直处于就绪态但不运行,说明有更高优先级的任务在霸占CPU。如果任务一直阻塞,说明它等的事件没发生。这两个是排查系统卡顿的常用思路。

我记得有一次,系统跑着跑着就卡了。用调试器一看,所有任务都在阻塞态,就空闲任务在跑。原来是一个任务在等队列消息,但发消息的任务被挂起了。死锁了。嗯,这种问题很隐蔽,但状态一看就明白。

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。这个坑,新手很容易踩。

总结一下:任务通知是轻量级、高效率的通信方式。适合简单场景。如果通信逻辑复杂,别硬用,该上队列就上队列。