2. 任务调度基础:FreeRTOS任务状态机、任务优先级与抢占机制、任务创建与删除的API

好,咱们进入正题。任务调度,说白了就是操作系统怎么安排这些任务轮流用CPU。你想想看,一个单片机只有一个核,但我们要同时处理传感器读取、控制算法、通信上报……怎么做到的?就是靠调度器来回切换。

我个人习惯,在讲任何RTOS之前,先让团队把任务状态机搞明白。状态机看不懂,后面优先级、抢占、API全是空中楼阁。

2.1 FreeRTOS任务状态机:任务的一生

一个任务从创建到删除,会经历几个状态。FreeRTOS里主要有四种:运行态就绪态阻塞态挂起态

我画个简图给你看:

创建任务 → 就绪态 ←→ 运行态
                ↓         ↓
             阻塞态    挂起态
                ↑         ↓
             超时/事件   恢复

嗯,这里要注意:一个时刻只有一个任务处于运行态。其他任务要么等着被调度(就绪态),要么在等某个事件(阻塞态),要么被我们主动挂起了。

我在项目中遇到过一个问题:有个新手把传感器读取任务里加了个vTaskDelay(1000),然后抱怨说“为什么我的任务不跑了?”——其实任务没死,只是进入了阻塞态,等1秒后自动回到就绪态。这就是状态机没理解透。

核心记忆点:

  • 运行态 → 就绪态:被更高优先级任务抢占
  • 运行态 → 阻塞态:主动调用延时或等待信号量
  • 阻塞态 → 就绪态:延时结束或事件发生
  • 运行态 → 挂起态:调用vTaskSuspend()

2.2 任务优先级与抢占机制:谁先跑?

优先级,就是给任务排个队。数字越大,优先级越高。FreeRTOS支持从0到(configMAX_PRIORITIES - 1),一般配置成32或64级。

抢占机制是什么?说白了就是:高优先级任务一就绪,低优先级任务立马让路

举个例子:

任务A:优先级2,负责姿态解算,每10ms执行一次
任务B:优先级1,负责LED闪烁,每100ms执行一次

假设任务B正在运行,突然任务A因为定时器触发了。这时候调度器会立刻暂停任务B,把CPU交给任务A。等任务A跑完(或者主动阻塞),任务B才能继续。

我曾经踩过一个坑:把通信任务优先级设得比控制任务还高。结果通信一忙,控制任务被频繁抢占,姿态解算周期抖动得厉害,飞控直接炸了。嗯,从那以后我定了个规矩:实时性要求越高的任务,优先级越高

避坑指南:

我曾经见过一个团队,把所有任务优先级都设成一样。结果调度器只能按时间片轮转,关键任务得不到及时响应。记住:优先级是实时系统的命根子,别乱设。

2.3 任务创建与删除的API:动手实操

好,理论讲完了,咱们看看代码。任务创建最核心的API就一个:xTaskCreate()

2.3.1 创建任务

函数原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名字(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄,用来删除或挂起
);

我习惯这么用:

TaskHandle_t xControlTaskHandle = NULL;

void vControlTask(void *pvParameters)
{
    // 姿态控制循环
    for(;;)
    {
        // 读取传感器
        // 解算姿态
        // 输出控制量
        vTaskDelay(pdMS_TO_TICKS(5)); // 5ms周期
    }
}

void main(void)
{
    // 创建控制任务
    xTaskCreate(
        vControlTask,
        "Control",
        256,        // 栈大小,单位是字
        NULL,
        3,          // 优先级3
        &xControlTaskHandle
    );

    // 启动调度器
    vTaskStartScheduler();
}

这里有个细节:栈大小怎么定? 我一般先给个256字(1024字节),跑起来后用uxTaskGetStackHighWaterMark()看剩余量,再调整。别一开始就给太大,浪费RAM。

2.3.2 删除任务

删除任务用vTaskDelete()。参数传任务句柄,如果传NULL就是删除自己。

// 删除指定任务
vTaskDelete(xControlTaskHandle);

// 任务自己删除自己
vTaskDelete(NULL);

嗯,这里要注意:删除任务后,任务占用的栈和TCB会被释放,但任务自己申请的内存(比如malloc出来的)不会自动释放。我见过有人删任务前忘了free,内存泄漏了。

我的习惯:

创建任务时,我会在任务函数开头加个打印,确认栈大小够用。删除任务前,我会确保所有动态分配的内存已经释放。别嫌麻烦,嵌入式系统里内存泄漏查起来真要命。

2.4 实战中的调度策略选择

讲完基础,咱们聊聊实际项目中怎么用。FreeRTOS默认是抢占式调度,但也可以配成协作式调度。我99%的项目都用抢占式,因为实时性要求摆在那。

但抢占式有个问题:优先级反转。低优先级任务拿着高优先级任务需要的资源,高优先级任务反而被阻塞。解决方法是优先级继承互斥量。这个后面章节会细讲,你先有个印象。

我总结一下任务调度的几个原则:

原则 说明
关键任务高优先级 控制、安全相关的任务优先级最高
避免优先级相同 相同优先级只能时间片轮转,实时性不可控
任务不要死循环 每个循环里至少有一个阻塞点(delay或等待事件)
栈大小要留余量 至少留20%的余量,防止栈溢出

好了,这一章就到这里。任务调度是实时系统的骨架,搞懂了状态机和优先级,后面学信号量、队列、中断管理就轻松多了。下一章咱们聊聊任务间通信,那又是另一片天地。