2、任务创建与管理:任务的概念、任务状态、任务优先级、任务创建函数xTaskCreate、任务删除函数vTaskDelete

好,咱们今天聊聊FreeRTOS里最核心的东西——任务。你想想看,一个嵌入式系统如果只能跑一个死循环,那能干的事太有限了。任务,就是让系统能同时处理多件事的基础。

任务到底是什么?

说白了,任务就是一段独立的程序,有自己的栈空间,有自己的执行流。我习惯把任务想象成一个独立的小人,每个小人都在干自己的活。有的在采集传感器数据,有的在刷新屏幕,有的在处理按键输入。这些小人在FreeRTOS的调度下,轮流使用CPU。

每个任务在代码里就是一个永不返回的C函数。嗯,这里要注意,任务函数里绝对不能有return语句,否则系统会崩溃。我刚开始学的时候就在这栽过跟头。

void vTaskFunction(void *pvParameters)
{
    // 任务初始化代码
    for(;;)
    {
        // 任务主体代码
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    // 这里绝对不能写return
}

任务的状态

任务在运行过程中,会在几种状态之间切换。我画个图你就能明白:

状态 说明 进入方式
运行态 正在使用CPU 调度器选中该任务
就绪态 可以运行,但CPU被别的任务占着 任务创建后、从阻塞/挂起恢复
阻塞态 等待某个事件或时间 调用vTaskDelay、等待队列/信号量
挂起态 暂停执行,不参与调度 调用vTaskSuspend

我在项目中遇到过一个问题:一个任务明明该执行了,但就是没反应。查了半天,发现是任务在等待一个永远不会到来的信号量,一直卡在阻塞态。所以调试的时候,先看看任务到底在哪个状态,能省不少时间。

任务优先级

优先级决定了任务谁先跑。数值越大,优先级越高。FreeRTOS支持抢占式调度,高优先级的任务就绪后,会立刻抢走CPU。

我个人习惯把实时性要求高的任务设高优先级,比如电机控制、通信处理。把界面刷新、日志输出这类任务设低优先级。但要注意,优先级不要设太多,一般3-5级就够了。我曾经见过一个项目设了20级优先级,调试起来简直噩梦。

避坑指南: 我曾经遇到过优先级反转的问题。一个低优先级任务拿了锁,高优先级任务等锁,中间优先级任务一直跑,低优先级任务拿不到CPU释放锁。结果高优先级任务活活被饿死。解决办法是用优先级继承协议,或者干脆避免在中断和任务间共享资源。

任务创建函数 xTaskCreate

这是最常用的任务创建方式。原型长这样:

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

返回值是pdPASS表示成功,pdFAIL表示失败(通常是栈不够)。

举个例子:

TaskHandle_t xLEDTaskHandle = NULL;

void vLEDTask(void *pvParameters)
{
    (void)pvParameters;  // 避免编译器警告
    for(;;)
    {
        GPIO_Toggle(LED_PIN);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void main(void)
{
    // 创建任务
    if(xTaskCreate(
        vLEDTask,
        "LED",
        128,        // 栈大小,单位是字
        NULL,
        2,          // 优先级
        &xLEDTaskHandle
    ) != pdPASS)
    {
        // 创建失败处理
        Error_Handler();
    }
    
    vTaskStartScheduler();
}
经验之谈: 栈大小怎么定?我一般先给个保守值,比如256字。然后用uxTaskGetStackHighWaterMark()函数在运行时查看实际使用量。这样能精确调整,既省内存又不会栈溢出。

任务删除函数 vTaskDelete

任务干完活了,或者出错了,可以用这个函数把它干掉。

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数传NULL表示删除自己。注意,删除自己后,vTaskDelete函数不会返回,后面的代码永远不会执行。

我建议在删除任务前,先释放它占用的资源,比如动态分配的内存、打开的硬件外设等。否则会造成资源泄漏。

void vCleanupTask(void *pvParameters)
{
    // 做一些清理工作
    for(;;)
    {
        // 收到删除指令
        if(xReceivedDeleteCommand)
        {
            // 先释放资源
            vReleaseHardware();
            vPortFree(pvBuffer);
            
            // 再删除自己
            vTaskDelete(NULL);
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
注意: 被删除的任务的栈空间和TCB(任务控制块)会被系统回收。但如果任务使用了动态分配的内存,系统不会自动释放,你得自己处理。我曾经有个项目,任务反复创建删除,内存越用越少,最后系统挂了。查了半天才发现是任务里malloc的内存没释放。

任务句柄的使用

任务句柄就像任务的身份证。创建任务时拿到句柄,后面就能用它来操作任务:

  • vTaskDelete(xTaskHandle) — 删除指定任务
  • vTaskSuspend(xTaskHandle) — 挂起任务
  • vTaskResume(xTaskHandle) — 恢复任务
  • uxTaskPriorityGet(xTaskHandle) — 获取任务优先级
  • vTaskPrioritySet(xTaskHandle, uxNewPriority) — 设置任务优先级

我个人习惯在创建任务时就把句柄保存成全局变量,方便其他地方引用。但要注意,如果任务被删除了,句柄就失效了,再用它操作任务会出问题。

总结一下

任务管理是FreeRTOS的基石。记住几个要点:

  1. 任务函数不能返回,必须是个死循环
  2. 优先级要合理设置,别搞太多级
  3. 栈大小要留够余量,用HighWaterMark来调优
  4. 删除任务前记得释放资源
  5. 任务句柄失效后别再用

嗯,这些就是我这些年用FreeRTOS做项目积累下来的经验。下一节咱们聊聊任务间的通信,那又是另一个有意思的话题了。