第1章:任务基础与调度机制

大家好,我是你们的老朋友。今天咱们来聊聊FreeRTOS最核心的东西——任务。说实话,我做了这么多年嵌入式开发,见过太多人一上来就写任务,结果系统跑起来各种诡异问题。其实啊,把任务基础搞明白,后面就顺了。

1.1 任务状态机:任务到底有几种状态?

先问大家一个问题:一个任务在系统里,可能处于哪些状态?

我刚开始学RTOS时,以为任务就两种状态:运行和不运行。后来被坑了一次才明白,FreeRTOS的任务状态机其实有四种状态:

  • 运行态(Running):正在使用CPU,同一时刻只有一个任务处于此状态
  • 就绪态(Ready):能运行但没轮到,等着CPU分配
  • 阻塞态(Blocked):在等某个事件,比如延时、信号量、队列
  • 挂起态(Suspended):被vTaskSuspend()挂起,除非别人调用vTaskResume(),否则永远不运行

核心要点:任务状态切换是FreeRTOS调度器工作的基础。理解了这个,你才能预测任务的行为。

我画了一张状态机图,大家一看就明白:

运行态 就绪态 阻塞态 挂起态 时间片用完 调度器选择 等待事件 事件到达 vTaskSuspend() vTaskResume() 图例 运行态:正在使用CPU 就绪态:等待CPU 阻塞态:等待事件 挂起态:被暂停 实线:自动切换 虚线:API调用

我的经验:调试时最常用的就是看任务状态。我曾经遇到一个任务卡死,查了半天发现它一直在阻塞态等一个永远不会来的信号量。用vTaskList()打印状态,一眼就发现问题了。

1.2 任务优先级:谁先跑?谁后跑?

优先级这东西,说白了就是给任务排个队。数值越小优先级越低,0是最低,configMAX_PRIORITIES-1是最高。

我个人习惯把优先级分成几档:

优先级范围 典型用途 举例
0-2 后台任务、空闲任务 系统监控、日志打印
3-5 普通业务任务 传感器采集、数据处理
6-8 实时性要求高的任务 通信协议处理、控制算法
9以上 紧急任务 中断下半部、安全保护

注意:优先级不是越高越好。我曾经在一个项目里把所有任务都设成高优先级,结果低优先级任务永远得不到CPU时间,系统直接卡死。这叫「优先级反转」的极端情况。

FreeRTOS的调度规则很简单:

  • 高优先级任务就绪时,立即抢占低优先级任务
  • 同优先级任务轮流执行(时间片轮转)
  • 没有就绪的高优先级任务时,才执行低优先级任务

1.3 任务创建与删除:动手写代码

好了,理论说完了,咱们直接上代码。任务创建用xTaskCreate(),原型长这样:

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

我写一个最简单的例子:

// 任务函数
void vTask1(void *pvParameters)
{
    int count = 0;
    while(1)
    {
        count++;
        printf("Task1 running, count=%d\n", count);
        vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
    }
}

// 创建任务
TaskHandle_t xTask1Handle = NULL;
xTaskCreate(
    vTask1,          // 任务函数
    "Task1",         // 名字
    128,             // 栈大小
    NULL,            // 无参数
    1,               // 优先级1
    &xTask1Handle    // 保存句柄
);

关键点:任务函数绝对不能返回!必须用while(1)死循环。如果函数返回了,系统会调用vTaskDelete(NULL)把自己删掉,但栈资源可能没释放干净。

删除任务用vTaskDelete():

// 删除自己
vTaskDelete(NULL);

// 删除其他任务
vTaskDelete(xTask1Handle);

避坑指南:我曾经在删除任务后,还继续使用它的句柄,结果系统直接HardFault。记住:删除后句柄就失效了,最好把句柄置为NULL。

1.4 任务切换原理:调度器是怎么工作的?

任务切换,说白了就是CPU在任务之间跳来跳去。FreeRTOS用的是抢占式调度,加上时间片轮转。

切换过程分三步:

  1. 保存现场:把当前任务的寄存器、栈指针等压入自己的栈
  2. 选择下一个任务:调度器从就绪列表中挑一个优先级最高的
  3. 恢复现场:把新任务的寄存器从栈里弹出来,CPU继续执行

触发切换的时机有:

  • 任务主动调用vTaskDelay()或等待事件
  • 高优先级任务就绪(比如中断里释放了信号量)
  • 时间片用完(SysTick中断触发)

我画个流程图,大家感受一下:

任务A正在运行 触发切换条件 保存任务A的现场 选择下一个任务B 切换触发条件 • vTaskDelay() • 等待信号量/队列 • 高优先级任务就绪 • 时间片用完 • 中断中触发 切换由PendSV中断 完成,优先级最低

嗯,这里要注意:任务切换是在PendSV中断里完成的。为什么用PendSV?因为它的优先级最低,可以等所有其他中断处理完再切换,避免中断嵌套导致的问题。

总结一下:任务切换的核心就是「保存现场-选择任务-恢复现场」三步曲。你想想看,每次切换都要做这些操作,所以任务切换是有开销的。任务太多、切换太频繁,CPU时间都花在切换上了,实际干活的时间就少了。

我个人建议:任务数量控制在10个以内,切换频率别太高。我曾经在一个项目里创建了30多个任务,结果系统响应慢得不行。后来合并成8个任务,性能直接翻倍。

好了,这一章的内容就到这儿。任务基础打牢了,后面讲队列、信号量、互斥量这些通信机制时,你就能理解它们是怎么影响任务状态的。


专注资料整理