3. FreeRTOS内核入门:任务创建、删除、挂起与恢复,任务状态机

好,咱们今天聊聊FreeRTOS里最核心的东西——任务管理。

说实话,我刚开始接触RTOS的时候,最困惑的就是任务到底是个什么玩意儿。后来在车上一跑,才真正明白:任务就是一段独立运行的代码,有自己的栈、有自己的优先级。说白了,就是让CPU在多个逻辑线程之间来回跳,看起来像同时干活。

3.1 任务状态机:五种状态,一个循环

FreeRTOS的任务状态机,我习惯把它画成一个五角星。每个角代表一种状态,箭头代表怎么跳转。

状态 英文 说明
运行态 Running CPU正在执行该任务
就绪态 Ready 任务可以运行,但CPU在忙别的
阻塞态 Blocked 任务在等某个事件(延时、信号量等)
挂起态 Suspended 任务被主动挂起,不参与调度
删除态 Deleted 任务已被销毁,资源待回收

为什么会这样设计?你想想看,如果所有任务都在抢CPU,那不乱套了?FreeRTOS通过状态机,把任务的“活”和“等”分得清清楚楚。

核心要点:任务从创建到删除,本质上是状态在五个节点之间流转。你调用API,就是在帮任务“换挡”。

3.2 任务创建:xTaskCreate 与 xTaskCreateStatic

创建任务,最常用的是 xTaskCreate。我一般在项目里用动态创建,省心。但车规级项目里,有些客户要求静态创建——怕动态分配内存出问题。

// 动态创建示例
TaskHandle_t myTaskHandle = NULL;

void vTaskFunction(void *pvParameters) {
    while(1) {
        // 任务主体
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

BaseType_t xReturn = xTaskCreate(
    vTaskFunction,       // 任务函数
    "MyTask",            // 任务名(调试用)
    256,                 // 栈深度(单位:字)
    NULL,                // 参数
    2,                   // 优先级(0最低,configMAX_PRIORITIES-1最高)
    &myTaskHandle        // 任务句柄
);

if (xReturn != pdPASS) {
    // 创建失败,处理错误
}

嗯,这里要注意:栈深度给多少?我踩过坑。有一次在ECU上跑一个CAN报文处理任务,给了128个字,结果跑着跑着就崩了。后来一查,是函数调用嵌套太深,栈溢出了。我的建议是:先给个保守值,比如512字,跑稳定了再调小。

个人经验:uxTaskGetStackHighWaterMark() 查看栈剩余空间。如果剩余不到20%,就该加栈了。

3.3 任务删除:vTaskDelete

删除任务很简单,但有个坑——你得确保任务自己把自己删了,或者别的任务删它时,它不在运行关键代码。

// 任务自己删除自己
void vTaskFunction(void *pvParameters) {
    // 做一些初始化工作
    // ...
    // 干完活,自我了断
    vTaskDelete(NULL);  // NULL表示删除当前任务
}

// 别的任务删除它
void vAnotherTask(void *pvParameters) {
    vTaskDelay(pdMS_TO_TICKS(1000));
    if (myTaskHandle != NULL) {
        vTaskDelete(myTaskHandle);
        myTaskHandle = NULL;  // 记得置空,别悬空指针
    }
}

我曾经在项目里犯过一个低级错误:删了任务后,没把句柄置空。结果另一个任务拿着这个野指针去操作,直接HardFault。嗯,血的教训。

警告:删除任务后,FreeRTOS会自动回收栈和TCB内存。但如果你用了静态创建,得自己手动释放资源。

3.4 任务挂起与恢复:vTaskSuspend 与 vTaskResume

挂起和阻塞不一样。阻塞是任务自己主动等,挂起是外部强制让它“闭嘴”。

// 挂起任务
vTaskSuspend(myTaskHandle);

// 恢复任务
vTaskResume(myTaskHandle);

// 从中断中恢复(注意:中断中不能用vTaskResume)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskResumeFromISR(myTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

我一般在什么场景用挂起?比如OTA升级的时候,先把所有非关键任务挂起,只留一个升级任务跑。升级完了再一个个恢复。这样能保证升级过程不被干扰。

避坑指南:我曾经在中断里直接调了 vTaskResume,结果调度器没及时切换,导致高优先级任务没立刻跑起来。后来改成 vTaskResumeFromISRportYIELD 才搞定。

3.5 任务优先级与调度策略

FreeRTOS默认是抢占式调度。高优先级任务就绪了,低优先级任务立马被踢出去。但有个细节:同优先级任务之间是时间片轮转。

配置项 说明 我常用的值
configUSE_PREEMPTION 是否启用抢占 1(必须开)
configUSE_TIME_SLICING 同优先级是否时间片轮转 1(默认开)
configTICK_RATE_HZ 系统时钟节拍 1000(车用推荐)

为什么车用推荐1000Hz?因为很多CAN报文周期是10ms、20ms,1000Hz的tick能精确到1ms,延时控制更准。但代价是CPU开销大一点。嗯,看你的MCU主频了,如果跑在200MHz以上,完全扛得住。

3.6 实战:一个简单的车灯控制任务

咱们来个实际的。假设你要控制一个LED灯,按一下按钮切换亮灭。用FreeRTOS实现:

TaskHandle_t xLedTaskHandle = NULL;

void vLedTask(void *pvParameters) {
    uint8_t ucLedState = 0;
    const uint32_t ulDebounceDelay = pdMS_TO_TICKS(50);

    for(;;) {
        // 等待按钮按下(假设用信号量通知)
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        // 消抖延时
        vTaskDelay(ulDebounceDelay);

        // 切换LED状态
        ucLedState = !ucLedState;
        GPIO_WriteBit(LED_PORT, LED_PIN, ucLedState);

        // 打印状态(调试用)
        // printf("LED state: %d\n", ucLedState);
    }
}

// 在main中创建
int main(void) {
    // 硬件初始化
    // ...

    xTaskCreate(vLedTask, "LedTask", 128, NULL, 1, &xLedTaskHandle);
    vTaskStartScheduler();

    // 正常情况下不会跑到这里
    while(1);
}

这个例子虽然简单,但包含了任务创建、阻塞等待、延时等核心操作。你想想看,如果把LED换成电机、把按钮换成CAN报文,是不是就活起来了?

我的习惯:每个任务都起个有意义的名字,比如"CanRxTask"、"DiagTask"。调试时用 vTaskList() 一看就知道谁在跑、谁在等。

3.7 小结

任务管理是FreeRTOS的基石。你掌握了创建、删除、挂起、恢复,再理解了状态机,基本上就能应付大部分车端应用了。

最后说一句:别怕任务多。我见过一个项目里跑了30多个任务,照样稳如老狗。关键是每个任务的栈大小要合理,优先级别乱设。嗯,今天就到这儿,下节课咱们聊队列和信号量。