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 开始,到任务真正跑起来,每一步都清楚。
看到没?整个流程就是:调用函数 → 分配内存 → 初始化 → 加入调度队列。每一步都可能失败,所以检查返回值真的很重要。
避坑指南
我踩过的三个坑
- 堆栈大小设太小:任务跑着跑着就崩了,查堆栈溢出才发现。后来我养成习惯,开发阶段把堆栈设大 50%,稳定后再调小。
- 优先级设太高:有个任务优先级设成最高,结果它一直占着 CPU,其他任务饿死了。记住,优先级越高责任越大。
- 忘记检查返回值:xTaskCreate 返回失败,我还以为任务创建成功了。结果系统里少了个任务,功能不全。
嗯,任务创建这块就这些。TCB 是身份证,堆栈是工作台,优先级是话语权。xTaskCreate 就是把这三样东西打包好,交给调度器去管理。