2. 任务创建与管理:任务状态机、优先级、句柄与API详解

各位同学,咱们今天聊聊FreeRTOS里最核心的东西——任务。说白了,整个系统就是靠任务在跑。你想想看,一个单片机要同时处理按键、显示、通信、控制,没有任务调度,那代码得写成什么样?

我个人习惯,拿到一个新项目,第一件事就是先把任务框架搭好。任务怎么创建、怎么管理、怎么切换,这些搞明白了,后面写代码就顺风顺水。

2.1 任务状态机:就绪、运行、阻塞、挂起

任务在FreeRTOS里不是一直傻跑的。它有四种状态,像个状态机一样来回切换。我刚开始学的时候,总觉得这玩意儿抽象,后来在项目里踩过坑才真正理解。

四种核心状态:

  • 就绪态(Ready):任务已经准备好,随时可以运行。就等着调度器翻牌子。
  • 运行态(Running):正在占用CPU。单核MCU同一时刻只有一个任务在跑。
  • 阻塞态(Blocked):任务在等某个事件。比如等延时结束、等信号量、等队列消息。
  • 挂起态(Suspended):任务被强制暂停。调用vTaskSuspend()就进去了,得用vTaskResume()才能唤醒。

这里有个关键点:阻塞态和挂起态的区别。阻塞是任务自己主动去等,比如调了vTaskDelay();挂起是被别人强行按住,比如别的任务调了vTaskSuspend()。我在项目中遇到过,有人把这两个搞混,结果任务一直不跑,查了半天才发现是挂起没恢复。

下面这张图,是我手绘的任务状态切换逻辑,你一看就明白:

FreeRTOS 任务状态机 就绪态 Ready 运行态 Running 阻塞态 Blocked 挂起态 Suspended 调度器选择 时间片用完/被抢占 等待事件/延时 事件到达/延时结束 vTaskSuspend() vTaskResume() vTaskSuspend() vTaskResume() 注:创建后任务进入就绪态;删除任务从任何状态直接进入终止态(未画出)

嗯,这里要注意:任务创建后,默认进入就绪态。调度器一跑,就从就绪队列里挑一个优先级最高的去运行。运行中如果调了延时函数,就进阻塞态;如果被别的任务挂起,就进挂起态。

2.2 任务优先级:谁抢谁?

优先级这玩意儿,说白了就是给任务排个队。数值越大,优先级越高。FreeRTOS支持从0到(configMAX_PRIORITIES - 1),一般配置成5或32就够用了。

避坑指南:我曾经在一个项目里,把两个关键任务的优先级设成了相同值。结果一个任务跑着跑着,另一个老抢不到CPU,导致系统响应变慢。后来改成不同优先级,问题就解决了。

优先级的使用原则,我总结了几条:

  • 实时性要求高的任务:给高优先级。比如电机控制、紧急停止。
  • 周期性任务:中等优先级。比如数据采集、状态刷新。
  • 后台任务:低优先级。比如日志打印、界面刷新。

你想想看,如果按键扫描和电机控制优先级一样高,那电机转的时候按键按了没反应,这能行吗?

2.3 任务句柄:任务的身份证

任务句柄,就是TaskHandle_t类型的变量。它指向任务控制块(TCB),里面存了任务的所有信息——栈指针、优先级、状态等等。

我习惯在创建任务时,把句柄保存成全局变量。这样后面想挂起、恢复、删除任务,直接拿句柄操作就行。

TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;

// 创建任务时传入句柄地址
xTaskCreate(vTask1, "Task1", 256, NULL, 1, &xTask1Handle);
xTaskCreate(vTask2, "Task2", 256, NULL, 2, &xTask2Handle);

// 后面用句柄操作
vTaskSuspend(xTask1Handle);   // 挂起任务1
vTaskResume(xTask2Handle);    // 恢复任务2

小技巧:句柄不要乱用。我见过有人把句柄当普通变量传来传去,结果任务删了句柄还留着,一操作就崩溃。记住:句柄有效的前提是任务还存在。

2.4 xTaskCreate() 与 xTaskCreateStatic() 详解

这两个函数,是创建任务的两种方式。一个动态分配内存,一个静态分配。说白了,就是栈空间谁来给的问题。

xTaskCreate():动态创建

这是最常用的方式。任务栈和TCB都由FreeRTOS内部用pvPortMalloc()分配。用起来简单,适合大多数场景。

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

返回值:pdPASS表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示内存不够。

实际案例:我在一个物联网项目里,用动态创建了5个任务。后来发现系统跑着跑着就卡死,查了半天,原来是某个任务栈设小了,导致栈溢出。从那以后,我每个任务都留够余量,至少256字起步。

xTaskCreateStatic():静态创建

这个函数需要你自己提供栈空间和TCB内存。好处是:

  • 不用动态内存分配,适合对实时性要求极高的场景
  • 内存占用完全可控,不会出现碎片
  • 适合裸机转FreeRTOS的老项目
StackType_t xTaskStack[256];       // 自己定义的栈
StaticTask_t xTaskTCB;             // 自己定义的TCB

TaskHandle_t xTaskHandle = xTaskCreateStatic(
    vTaskFunction,      // 任务函数
    "StaticTask",       // 任务名
    256,                // 栈深度
    NULL,               // 参数
    1,                  // 优先级
    xTaskStack,         // 栈指针
    &xTaskTCB           // TCB指针
);

如果返回NULL,说明创建失败。检查一下你给的栈空间和TCB是不是有效的。

对比项 xTaskCreate() xTaskCreateStatic()
内存来源 FreeRTOS内部动态分配 用户自己提供
栈空间 自动分配 用户定义数组
TCB空间 自动分配 用户定义StaticTask_t变量
适用场景 大多数项目,方便快捷 对实时性/内存有严格要求的场景
风险 可能内存不足或产生碎片 需要手动管理内存,代码稍多

我个人习惯,原型阶段用动态创建,快速验证功能。产品定型后,如果发现内存碎片问题,再改成静态创建。你想想看,一个跑了几万小时的产品,内存碎片积累多了,突然某天创建任务失败,那多尴尬。

重要提醒:静态创建时,栈数组和TCB变量必须保持有效,不能是局部变量。我见过有人把栈定义在函数内部,函数返回后栈被释放,任务直接跑飞。正确做法是定义成全局变量或静态变量。

好了,任务创建和管理这块,核心就是这些。状态机搞明白,优先级设对,句柄用好,创建函数选对,你的任务系统就稳了。下一节咱们聊聊任务间的通信,那又是另一番天地。


公众号:蓝海资料掘金营,微信deep3321