3. FreeRTOS 内核入门:任务创建与删除、优先级与调度策略

各位同学,欢迎来到第三章。从这一章开始,我们正式进入 FreeRTOS 的内核世界。

很多初学者刚接触 RTOS 时,总觉得它很神秘。其实说白了,RTOS 就是一个高效的任务调度器。它帮你决定「哪个任务先跑、哪个任务等一等」。我刚开始用 FreeRTOS 时,也踩过不少坑,比如任务莫名其妙不运行了,或者优先级设错了导致系统卡死。今天我们就从最基础的任务管理讲起。

本章核心知识点:任务创建与删除、任务优先级与调度策略(抢占式/协作式)、任务状态机(运行、就绪、阻塞、挂起)、空闲任务与阻塞延时(vTaskDelay)。

FreeRTOS 任务管理核心知识体系 FreeRTOS 任务管理 任务创建与删除 优先级与调度 任务状态机 空闲任务与延时 xTaskCreate vTaskDelete 抢占式调度 协作式调度 运行/就绪/阻塞 挂起状态 空闲任务钩子 vTaskDelay 💡 核心要点 任务 = 无限循环函数 + 独立栈空间 + 优先级 + 状态

3.1 任务创建与删除

在 FreeRTOS 中,任务本质上是一个永远不会返回的 C 函数。它通常是一个无限循环,里面放着你要执行的代码。

创建任务使用 xTaskCreate() 函数。它的原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(4字节)
    void *pvParameters,             // 传递给任务的参数
    UBaseType_t uxPriority,         // 任务优先级(0 ~ configMAX_PRIORITIES-1)
    TaskHandle_t *pxCreatedTask     // 返回的任务句柄
);

返回值:pdPASS 表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示内存不足。

个人经验:我习惯在创建任务后立即检查返回值。曾经有一次我忘了检查,结果任务没创建成功,系统静默地跑着,但该干的事一件没干。排查了半天才发现是堆栈大小设得太小了。

下面是一个典型的任务创建示例:

void vTask1(void *pvParameters)
{
    while(1)
    {
        // 任务代码
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void main(void)
{
    TaskHandle_t xTask1Handle = NULL;
    
    BaseType_t xReturn = xTaskCreate(
        vTask1,           // 任务函数
        "Task1",          // 任务名
        128,              // 栈大小(128字 = 512字节)
        NULL,             // 参数
        2,                // 优先级
        &xTask1Handle     // 句柄
    );
    
    if(xReturn != pdPASS)
    {
        // 处理错误
        Error_Handler();
    }
    
    vTaskStartScheduler();  // 启动调度器
}

删除任务用 vTaskDelete()。注意:被删除的任务会进入「僵尸」状态,它的栈空间和 TCB 会被空闲任务回收。

// 删除自己
vTaskDelete(NULL);

// 删除其他任务(需要句柄)
vTaskDelete(xTask1Handle);

⚠️ 重要提醒:千万不要在中断服务函数中调用 vTaskDelete()!FreeRTOS 的 API 函数分为「FromISR」版本和普通版本,中断中只能调用带 FromISR 后缀的函数。

3.2 任务优先级与调度策略

FreeRTOS 支持两种调度策略:抢占式调度和协作式调度。默认是抢占式,这也是绝大多数项目的选择。

特性 抢占式调度 协作式调度
切换时机 任何时候,只要更高优先级任务就绪 只有当前任务主动让出 CPU(如调用 vTaskDelay)
实时性 高 低
适用场景 大多数嵌入式系统 任务间协作紧密、共享资源多的场景
配置方式 configUSE_PREEMPTION = 1 configUSE_PREEMPTION = 0

优先级数值越小,优先级越低。0 是最低优先级,configMAX_PRIORITIES-1 是最高优先级。

抢占式调度的核心规则很简单:

  • 如果高优先级任务就绪了,低优先级任务立刻被「抢走」CPU
  • 同优先级任务之间采用时间片轮转(Round-Robin)
  • 每个时间片长度由 configTICK_RATE_HZ 决定

避坑指南:我曾经在一个项目中把三个任务的优先级都设成了相同值,结果发现某个关键任务总是被其他任务「挤占」。后来才意识到,同优先级任务之间是时间片轮转,没有优先级高低之分。如果你的任务有实时性要求,一定要给它分配更高的优先级。

3.3 任务状态机

每个任务在任意时刻都处于以下四种状态之一:

  • 运行态(Running):正在使用 CPU。单核系统中,同一时刻只有一个任务处于运行态。
  • 就绪态(Ready):可以运行,但 CPU 被更高优先级或同优先级的任务占用着。
  • 阻塞态(Blocked):等待某个事件。比如等待延时结束、等待信号量、等待队列消息。
  • 挂起态(Suspended):被 vTaskSuspend() 挂起,只有调用 vTaskResume() 才能恢复。

状态转换关系如下:

创建 → 就绪 → 运行 → 阻塞 → 就绪 → 运行 ...
                ↓
              挂起 → 就绪(通过 vTaskResume)

这里有个关键点:只有就绪态的任务才能被调度器选中进入运行态。阻塞态和挂起态的任务不会参与调度。

我的习惯:调试时我喜欢用 uxTaskGetNumberOfTasks() 和 vTaskList() 来查看当前所有任务的状态。这能帮你快速定位「哪个任务卡住了」。

3.4 空闲任务与阻塞延时

当你调用 vTaskStartScheduler() 后,FreeRTOS 会自动创建一个空闲任务(Idle Task)。它的优先级是 0,也就是最低优先级。

空闲任务的作用:

  • 当没有其他任务需要运行时,空闲任务获得 CPU
  • 回收被删除任务的栈空间和 TCB
  • 进入低功耗模式(如果配置了 configUSE_IDLE_HOOK)

你可以通过空闲任务钩子(Idle Hook)在空闲时执行一些低优先级的后台操作,比如系统状态统计、进入睡眠模式等。

阻塞延时 vs 空转延时

很多初学者会问:为什么不用 HAL_Delay() 或者自己写一个空循环延时?

答案很简单:空转延时浪费 CPU。当你在一个任务中执行 for(i=0;i<1000000;i++) 时,CPU 一直在跑,其他任务根本没机会运行。

而 vTaskDelay() 会让任务主动进入阻塞态,把 CPU 让给其他任务。这才是 RTOS 的正确用法。

// ❌ 错误做法:空转延时
void vTaskBad(void *pvParameters)
{
    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        for(uint32_t i=0; i<500000; i++);  // CPU 被白白占用
    }
}

// ✅ 正确做法:阻塞延时
void vTaskGood(void *pvParameters)
{
    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500));  // 任务进入阻塞态,CPU 让出
    }
}

vTaskDelay() 的参数单位是「时钟节拍数」。使用 pdMS_TO_TICKS(毫秒) 宏可以方便地将毫秒转换为节拍数。

⚠️ 注意:vTaskDelay() 的延时是「相对延时」,它从调用时刻开始计时。如果你需要精确的周期性执行,建议使用 vTaskDelayUntil(),它能消除任务执行时间带来的累积误差。

3.5 实战要点总结

好了,这一章的内容就到这里。我们来快速回顾一下关键点:

  • 任务创建用 xTaskCreate(),记得检查返回值
  • 任务删除用 vTaskDelete(),不要在中断中调用
  • 优先级数值越小优先级越低,抢占式调度下高优先级任务会立即抢占 CPU
  • 任务有四种状态:运行、就绪、阻塞、挂起
  • 空闲任务优先级最低,负责回收资源和进入低功耗
  • 用 vTaskDelay() 代替空转延时,让 CPU 做更有意义的事