STM32单片机FreeRTOS移植与任务通信实战 - 第3节

任务控制块:任务的身份证

说到任务创建,我第一个想聊的就是任务控制块(TCB)。你想想看,系统里跑着好几个任务,FreeRTOS 怎么知道谁是谁?谁在运行、谁在睡觉、谁在等资源?

说白了,每个任务都有一个专属的「身份证」——TCB。它是一个结构体,存着任务的所有关键信息。

TCB 里到底存了什么?

  • 任务堆栈指针(当前栈顶在哪)
  • 任务优先级(谁抢谁)
  • 任务状态(运行、就绪、阻塞、挂起)
  • 任务名称(调试时方便看)
  • 事件列表项(用于同步和通信)

我在项目里就吃过亏。有一次任务莫名其妙不跑了,查了半天才发现是 TCB 被意外覆盖了。从那以后,我每次创建任务都会先检查 TCB 内存是否够用。

任务堆栈:每个任务的小仓库

每个任务都有自己的堆栈,用来存局部变量、函数调用信息、中断现场。这个堆栈大小怎么定?

我个人的习惯是:先给一个保守值,比如 128 字(注意是字,不是字节)。跑起来后用 uxTaskGetStackHighWaterMark() 看剩余量,再调整。

堆栈大小估算技巧

  • 简单任务(只跑几个变量):64-128 字
  • 中等任务(有数组、结构体):256-512 字
  • 复杂任务(有递归、大缓冲区):512-1024 字

STM32 的 RAM 通常够用,但别浪费。我见过有人给每个任务分配 1024 字,结果 5 个任务就把 20KB RAM 吃光了。

堆栈溢出是隐形杀手

堆栈溢出不会立刻报错,而是悄悄覆盖相邻内存。我曾经调试一个系统,任务跑着跑着就死机,查了两天才发现是堆栈溢出把 TCB 给毁了。

建议:开发阶段开启 configCHECK_FOR_STACK_OVERFLOW,让系统帮你检测。

任务优先级:谁说了算

FreeRTOS 支持抢占式调度。优先级高的任务,只要就绪了,就能把低优先级的踢出去。

优先级范围是 0 到 configMAX_PRIORITIES - 1。数值越大,优先级越高。

优先级 典型用途 说明
0 空闲任务 系统自动创建,最低优先级
1-2 后台任务 数据记录、状态检测
3-4 普通任务 用户交互、数据处理
5-6 实时任务 传感器读取、控制算法
7 紧急任务 故障处理、安全保护

我踩过一个坑:把两个任务设成相同优先级,结果它们轮流跑,但有一个任务总抢不到 CPU。后来发现是时间片轮转没开。记住,同优先级任务默认是协作式调度,得靠 configUSE_TIME_SLICING 开启时间片。

xTaskCreate() API 详解

这是创建任务的核心函数。我把它拆开揉碎了讲。

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

参数逐个说

  • pvTaskCode:任务函数名。函数必须是 void vTaskFunction(void *pvParameters) 这种形式,里面要有死循环。
  • pcName:名字随便起,但别太长。调试时用 vTaskList() 能看到它。
  • usStackDepth:堆栈大小,单位是字。STM32 一个字是 4 字节。比如 128 字 = 512 字节。
  • pvParameters:传给任务函数的参数。可以传数字、指针、结构体地址。
  • uxPriority:优先级,0 最低。
  • pxCreatedTask:返回的任务句柄,以后可以用它来删除、挂起任务。

返回值

  • pdPASS:创建成功
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:内存不够

我建议每次创建任务都检查返回值,别偷懒。

实战代码示例

TaskHandle_t xLEDTaskHandle = NULL;

void vLEDTask(void *pvParameters)
{
    (void)pvParameters;  // 参数未使用,避免警告
    
    while(1)
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vCreateTasks(void)
{
    BaseType_t xReturned;
    
    xReturned = xTaskCreate(
        vLEDTask,           // 任务函数
        "LED",              // 任务名称
        128,                // 堆栈大小(128字 = 512字节)
        NULL,               // 无参数
        2,                  // 优先级 2
        &xLEDTaskHandle     // 任务句柄
    );
    
    if(xReturned != pdPASS)
    {
        // 创建失败,处理错误
        Error_Handler();
    }
}

这段代码我用了无数次。注意 vTaskDelay 用的是 pdMS_TO_TICKS 宏,把毫秒转成系统节拍。我刚开始写的时候直接写 vTaskDelay(500),结果任务跑得飞快——因为 500 个节拍在 1ms 节拍下才 0.5 秒。

任务创建流程全景图

下面这张图把整个流程串起来了。从调用 xTaskCreate 开始,到任务真正跑起来,每一步都清楚。

任务创建完整流程 调用 xTaskCreate() 分配 TCB 内存 分配任务堆栈 初始化 TCB 字段 加入就绪任务列表 返回 pdPASS 注意 TCB 和堆栈都从 FreeRTOS 堆中分配 堆大小由 configTOTAL_HEAP_SIZE 控制

看到没?整个流程就是:调用函数 → 分配内存 → 初始化 → 加入调度队列。每一步都可能失败,所以检查返回值真的很重要。

避坑指南

我踩过的三个坑

  1. 堆栈大小设太小:任务跑着跑着就崩了,查堆栈溢出才发现。后来我养成习惯,开发阶段把堆栈设大 50%,稳定后再调小。
  2. 优先级设太高:有个任务优先级设成最高,结果它一直占着 CPU,其他任务饿死了。记住,优先级越高责任越大。
  3. 忘记检查返回值:xTaskCreate 返回失败,我还以为任务创建成功了。结果系统里少了个任务,功能不全。

嗯,任务创建这块就这些。TCB 是身份证,堆栈是工作台,优先级是话语权。xTaskCreate 就是把这三样东西打包好,交给调度器去管理。