3. FreeRTOS 内核入门:任务创建与删除、任务状态机、任务优先级、vTaskDelay与调度机制

好,咱们今天正式进入 FreeRTOS 的内核世界。说实话,很多初学者一上来就被各种概念砸晕了——任务、队列、信号量、互斥量……其实你想想看,RTOS 最核心的东西就一个:任务。任务搞明白了,后面那些都是锦上添花。

我个人习惯把 FreeRTOS 比作一个公司。任务就是员工,调度器就是老板。老板怎么安排员工干活?谁优先级高谁先干?谁在摸鱼(阻塞)就换人?这就是我们今天要聊的内容。

本章核心: 掌握任务的创建与删除、理解任务状态机、搞懂优先级与调度机制、熟练使用 vTaskDelay。

3.1 任务创建与删除

先看怎么创建一个任务。FreeRTOS 提供了两个 API:xTaskCreate 和 xTaskCreateStatic。前者动态分配任务栈,后者静态分配。我建议新手先用动态的,省心。

// 任务函数原型
void vTaskFunction(void *pvParameters);

// 创建任务
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。嗯,这里要注意:一定要检查返回值。我在项目中遇到过好几次,堆栈分配失败导致任务没创建成功,程序却继续往下跑,最后莫名其妙死机。排查了半天才发现是堆空间不够。

经验之谈: 任务栈大小怎么定?我一般先给一个较大的值(比如 256 字),跑起来后用 uxTaskGetStackHighWaterMark() 查看剩余栈空间,再逐步调小。别一开始就抠门,容易踩坑。

删除任务更简单:

// 删除当前任务
vTaskDelete(NULL);

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

注意:任务删除后,它占用的栈和TCB(任务控制块)会被释放,但如果你用了静态创建,需要自己处理内存释放。另外,千万别在中断服务函数里调用 vTaskDelete,会出问题。

3.2 任务状态机

FreeRTOS 的任务有四种状态:运行态、就绪态、阻塞态、挂起态。我画了一张图,帮你理清它们之间的关系:

FreeRTOS 任务状态机 运行态 (Running) 就绪态 (Ready) 阻塞态 (Blocked) 挂起态 (Suspended) 调度器选择 时间片用完/被抢占 vTaskDelay/等待事件 延时到/事件发生 vTaskSuspend vTaskResume vTaskResumeFromISR 状态说明: • 运行态:当前正在使用 CPU 的任务,单核 MCU 上同一时刻只有一个任务处于运行态 • 就绪态:任务已准备好运行,但 CPU 被更高优先级或同优先级任务占用 • 阻塞态:任务在等待某个事件(延时、信号量、队列等),不参与调度 • 挂起态:任务被显式挂起,只有调用 vTaskResume 才能恢复

说白了,任务就是在这些状态之间跳来跳去。你写代码的时候,心里要清楚:当前任务在哪个状态?下一步会跳到哪个状态? 这对调试特别有帮助。

3.3 任务优先级

FreeRTOS 支持抢占式调度,优先级高的任务会抢 CPU。优先级数值越小,优先级越低(0 是最低)。

优先级值 含义 典型用途
0 最低优先级(空闲任务) 空闲任务、后台低功耗任务
1 ~ 3 低优先级 普通业务逻辑、数据显示
4 ~ 6 中优先级 通信处理、协议栈
7 ~ configMAX_PRIORITIES-1 高优先级 实时控制、紧急响应
我曾经踩过的坑: 把两个任务设成相同优先级,结果发现它们轮流跑,但其中一个任务里有个 while(1) 死循环,另一个任务永远得不到 CPU。后来才意识到,同优先级任务靠时间片轮转,但前提是每个任务都要主动让出 CPU(比如调用 vTaskDelay 或等待事件)。否则,一个任务霸占 CPU,其他同优先级任务就饿死了。

优先级设计有个原则:够用就好,别贪多。我一般只用 3~5 个优先级层级。优先级太多,调度开销会变大,而且容易搞混。

3.4 vTaskDelay 与调度机制

vTaskDelay 是 FreeRTOS 里最常用的延时函数。它和 HAL_Delay 最大的区别是:vTaskDelay 会让任务进入阻塞态,把 CPU 让给其他任务。

// 延时 1000 个 tick(假设 configTICK_RATE_HZ = 1000,则延时 1 秒)
vTaskDelay(pdMS_TO_TICKS(1000));

// 或者直接写 tick 数
vTaskDelay(1000);  // 延时 1000 个 tick

这里有个细节:vTaskDelay 指定的是从当前时刻开始,等待多少个 tick。如果你想让任务以固定周期运行,用 vTaskDelayUntil 更合适,它能避免累积误差。

TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(100);  // 100ms 周期

while(1) {
    // 等待下一个周期
    vTaskDelayUntil(&xLastWakeTime, xFrequency);
    
    // 执行周期性任务
    // ...
}
小技巧: 调试时可以用 uxTaskGetSystemState() 查看所有任务的状态和运行时间。我曾经用这个函数抓到一个任务跑飞的问题——某个任务一直处于运行态,其他任务全被饿死了。

调度机制说白了就两句话:

  • 抢占式调度: 高优先级任务就绪时,立即抢占低优先级任务。
  • 时间片轮转: 同优先级任务轮流使用 CPU,每个任务跑一个时间片(默认 1 个 tick)。

你想想看,如果没有抢占式调度,一个高优先级任务要等低优先级任务跑完才能执行,那实时性从何谈起?所以 FreeRTOS 默认就是抢占式,这也是大多数 RTOS 的选择。

3.5 实战:创建两个任务交替运行

最后,咱们写个简单的例子。创建两个任务,一个每 500ms 打印 "Task1",另一个每 1000ms 打印 "Task2"。

void vTask1(void *pvParameters) {
    while(1) {
        printf("Task1 running\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vTask2(void *pvParameters) {
    while(1) {
        printf("Task2 running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void) {
    // 硬件初始化
    HAL_Init();
    SystemClock_Config();
    
    // 创建任务
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 128, NULL, 1, NULL);
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 正常情况下不会跑到这里
    while(1);
}

运行效果:Task1 每 500ms 打印一次,Task2 每 1000ms 打印一次。两个任务同优先级,靠时间片轮转和 vTaskDelay 让出 CPU。

核心要点回顾:
  • 任务创建用 xTaskCreate,记得检查返回值
  • 任务有四种状态:运行、就绪、阻塞、挂起
  • 优先级数值越小优先级越低,0 是空闲任务
  • vTaskDelay 让任务阻塞,释放 CPU
  • 同优先级任务靠时间片轮转,高优先级任务抢占低优先级

嗯,这一章的内容就到这儿。任务管理是 FreeRTOS 的基石,搞懂了这些,后面学队列、信号量、互斥量就会轻松很多。记住我的一句话:写 RTOS 代码,心里时刻装着任务状态机。