任务(Task)是 FreeRTOS 中最核心的概念。每个任务都是一个独立的执行流,有自己的栈空间和优先级,调度器负责决定哪个任务获得 CPU。

任务状态

任务永远处于以下四种状态之一:

状态含义
运行态(Running)当前正在占用 CPU 执行
就绪态(Ready)可以运行,正在等待 CPU
阻塞态(Blocked)等待某个事件(延时到期、队列数据、信号量等)
挂起态(Suspended)被显式挂起,调度器完全不考虑它

状态转换图

         创建
          │
          ▼
       ┌──────┐  等待事件   ┌────────┐
       │ 就绪  │──────────▶│ 阻塞    │
       │(Ready)│◀──────────│(Blocked)│
       └──┬───┘  事件到达   └────────┘
          │                     │
   调度器选中│                     │(被抢占/时间片到)
          ▼                     │
       ┌──────┐                 │
       │ 运行  │─────────────────┘
       │(Run) │
       └──┬───┘
          │ vTaskSuspend()
          ▼
       ┌────────┐
       │  挂起   │  vTaskResume()
       │(Suspend)│──────────▶ 就绪态
       └────────┘

阻塞态的两种等待

任务阻塞时,通常在等两类事件:

  1. 时间事件:延时到期(如 vTaskDelay(500)),或到达绝对时间
  2. 同步事件:数据到达队列、信号量被释放、事件组 bit 被置位、收到任务通知等

任务可以同时等两种事件:设置超时时间。例如”等队列数据最多等 100ms”,100ms 内数据到了就处理,到了还没到就超时退出阻塞。

挂起态

进入挂起态只能通过 vTaskSuspend(),退出只能通过 vTaskResume()xTaskResumeFromISR()。大多数应用不使用挂起态。

创建任务

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针(函数名)
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(字数,不是字节数)
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 优先级(0最低,configMAX_PRIORITIES-1最高)
    TaskHandle_t *pxCreatedTask     // 出参:任务句柄(后续操作用)
);

返回值:pdPASS 创建成功;errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(即 pdFAIL)堆内存不足。

栈深度单位

usStackDepth 的单位是 字(word),不是字节。在 32 位 MCU(如 Cortex-M)上 1 字 = 4 字节。usStackDepth=128 实际分配 512 字节栈空间。

六种创建 API

API内存分配方式说明
xTaskCreate()动态分配(堆)最常用
xTaskCreateStatic()静态分配(用户提供内存)不使用堆
xTaskCreateRestricted()动态+MPU限制需要内存保护单元
xTaskCreateRestrictedStatic()静态+MPU限制
xTaskCreateAffinitySet()动态+指定核心SMP 多核用
xTaskCreateStaticAffinitySet()静态+指定核心SMP 多核用

名称带 Static 的版本需要你预先提供栈和 TCB 的内存,不从堆分配;不带 Static 的从堆里动态分配。

任务函数模板

void vMyTask(void *pvParameters)
{
    // pvParameters 就是创建时传入的参数,可以强转成需要的类型
    int myParam = (int)pvParameters;

    for(;;)  // 任务必须是死循环,不能 return
    {
        // 任务逻辑
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

    // 如果确实要退出,必须调用 vTaskDelete(NULL) 删除自己
    vTaskDelete(NULL);
}

空闲任务(Idle Task)

调用 vTaskStartScheduler() 时,内核自动创建一个优先级为 0 的空闲任务,它永远处于就绪态(死循环),保证系统至少有一个任务能运行。

空闲任务的职责:

  1. 释放被删除任务的 TCB 和栈内存(如果任务自己删除自己,内存由空闲任务回收)
  2. 执行空闲钩子函数(如果配置了)
  3. 如果没有其他任务就绪,CPU 就跑空闲任务(可进入低功耗模式)

空闲钩子函数

通过 vApplicationIdleHook() 可以在空闲任务每次循环中插入自定义逻辑:

void vApplicationIdleHook(void)
{
    // 每次空闲任务循环都会调用这里
    // 可以放:低功耗进入、简单指示灯、CPU 利用率统计
}

需要在 FreeRTOSConfig.h 中设置 configUSE_IDLE_HOOK = 1

空闲钩子的红线

  • 绝不能阻塞或挂起(否则系统可能没有可运行任务)
  • 如果应用任务调用了 vTaskDelete() 删除自己,空闲钩子必须尽快返回,让空闲任务有机会清理内存
  • 尽量短小,不要做耗时操作

优先级管理

// 获取任务优先级,传 NULL 表示获取当前任务自己的优先级
UBaseType_t uxPriority = uxTaskPriorityGet(NULL);

// 设置任务优先级
vTaskPrioritySet(xTask1Handle, uxPriority + 1);
  • 优先级范围:0(最低)到 configMAX_PRIORITIES - 1(最高)
  • 超过最大值会被自动截断到 configMAX_PRIORITIES - 1
  • FreeRTOS 支持任务在运行中动态修改自己或其他任务的优先级

删除任务

// 需要 INCLUDE_vTaskDelete = 1
vTaskDelete(xTask1Handle);  // 删除指定任务
vTaskDelete(NULL);          // 删除自己

内存释放

  • 内核自动分配的内存(TCB+栈)由空闲任务负责回收
  • 任务自己分配的内存(pvPortMalloc、打开的文件、外设句柄等)不会自动释放,必须在删除前手动释放
  • 因此,频繁创建/删除任务容易导致内存碎片,建议用”任务池”模式或静态创建

调度算法

FreeRTOS 是固定优先级抢占式调度器,通过两个配置项控制调度行为:

配置宏效果
configUSE_PREEMPTION1启用抢占(高优先级任务就绪后立即抢占低优先级)
configUSE_PREEMPTION0协作式(任务必须主动让出 CPU)
configUSE_TIME_SLICING1同优先级时间片轮转
configUSE_TIME_SLICING0同优先级不轮转,直到任务主动阻塞或让出

三种调度组合:

调度方式抢占时间片特点
抢占式+时间片默认配置,最常用
抢占式+无时间片高优先级立即抢占,同优先级不自动切换
协作式任意必须任务主动让出,不推荐新项目使用

抢占式调度的关键规则

  1. 高优先级永远先执行:只要有高优先级任务处于就绪态,低优先级任务就不会获得 CPU
  2. 抢占随时发生:不仅在 Tick 中断中,中断退出时、释放信号量时、发送队列时都可能触发抢占
  3. 同优先级轮转:开启时间片后,同优先级任务每个 Tick 切换一次(时间片长度 = 1 / configTICK_RATE_HZ)

延时:vTaskDelay vs vTaskDelayUntil

// 相对延时:从调用时起,延迟 N 个 tick
vTaskDelay(pdMS_TO_TICKS(500));

// 绝对延时:保证固定周期执行,不会漂移
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500));

周期性任务用 vTaskDelayUntil

vTaskDelay(500) 是”这次执行完,再等 500ms”,如果任务执行本身花了时间,周期会越来越漂移。 vTaskDelayUntil() 记录上次唤醒时间,下次唤醒以这个时间点为基准计算,保证绝对周期精度。


下一步:03-队列

相关:04-中断处理09-内存管理10-FreeRTOSConfig配置返回目录