4. 多任务创建与管理:用 xTaskCreate 搭建售货机的“三驾马车”
好,咱们进入实战环节。上一章我们聊了 FreeRTOS 的任务调度机制,这一章直接上手——用 xTaskCreate 创建出货、支付、UI 刷新这三个核心任务。
我个人习惯,拿到一个新项目,先不急着写业务逻辑。先把任务框架搭起来,让系统先“跑起来”。你想想看,任务都跑不顺,后面加功能全是坑。
4.1 任务创建:三行代码搞定?没那么简单
先看代码。这是我在一个自动售货机项目里实际用过的任务创建模板:
// 任务句柄
TaskHandle_t xHandleDispense = NULL;
TaskHandle_t xHandlePayment = NULL;
TaskHandle_t xHandleUI = NULL;
void AppTaskCreate(void)
{
// 创建出货任务
xTaskCreate(
vTaskDispense, // 任务函数
"Dispense", // 任务名(调试用)
256, // 栈深度(单位:字,不是字节!)
NULL, // 参数
3, // 优先级(0最低,configMAX_PRIORITIES-1最高)
&xHandleDispense // 任务句柄
);
// 创建支付任务
xTaskCreate(vTaskPayment, "Payment", 512, NULL, 2, &xHandlePayment);
// 创建UI刷新任务
xTaskCreate(vTaskUI, "UI_Refresh", 128, NULL, 1, &xHandleUI);
}
嗯,这里要注意。栈深度我写的是 256、512、128,单位是 字(Word)。在 32 位 MCU 上,一个字 = 4 字节。所以 256 字 = 1024 字节。我见过有人直接写 256 字节,结果任务一跑就栈溢出,查了半天。
uxTaskGetStackHighWaterMark() 一查,剩余栈空间只剩 8 个字。所以建议:先给大一点,稳定后再优化。
4.2 任务状态切换:运行、就绪、阻塞、挂起
任务创建后,它不会一直跑。FreeRTOS 的任务有四种状态,我画个表格帮你理清:
| 状态 | 含义 | 典型场景 |
|---|---|---|
| 运行 | 当前正在使用 CPU | 单核 MCU 同一时刻只有一个任务在运行 |
| 就绪 | 可以运行,但 CPU 被更高优先级任务占着 | 高优先级任务在跑,低优先级任务排队等 |
| 阻塞 | 等待某个事件(延时、信号量、队列等) | vTaskDelay() 等待、等待按键按下 |
| 挂起 | 被主动暂停,不参与调度 | vTaskSuspend() 暂停某个任务 |
说白了,任务就像公司里的员工:运行是正在干活,就绪是活干完了等新任务,阻塞是在等快递或等审批,挂起是被老板叫停。
4.3 实战:让三个任务“动”起来
光创建不行,得让任务有实际行为。我写个简化版的售货机任务逻辑:
// 出货任务
void vTaskDispense(void *pvParameters)
{
for(;;)
{
// 等待出货信号(来自支付任务)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 执行出货动作
Motor_Start();
vTaskDelay(pdMS_TO_TICKS(2000)); // 出货耗时2秒
Motor_Stop();
// 通知UI刷新
xTaskNotifyGive(xHandleUI);
}
}
// 支付任务
void vTaskPayment(void *pvParameters)
{
for(;;)
{
// 轮询支付状态(实际项目用中断+队列更合理)
if(Coin_Detect())
{
// 扣款逻辑...
// 通知出货任务
xTaskNotifyGive(xHandleDispense);
}
vTaskDelay(pdMS_TO_TICKS(50)); // 每50ms检查一次
}
}
// UI刷新任务
void vTaskUI(void *pvParameters)
{
for(;;)
{
// 等待刷新通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 刷新屏幕显示
LCD_Update();
}
}
4.4 任务挂起与恢复:什么时候用?
挂起状态我用的不多,但有一个场景特别典型——系统待机。
我记得有一次做户外售货机,要求 30 秒无操作自动进入低功耗模式。这时候不能删除任务(删除再创建开销大),直接挂起:
void EnterLowPowerMode(void)
{
vTaskSuspend(xHandlePayment); // 暂停支付检测
vTaskSuspend(xHandleUI); // 关闭屏幕刷新
// 出货任务本来就在阻塞,不用管
}
void ExitLowPowerMode(void)
{
vTaskResume(xHandlePayment);
vTaskResume(xHandleUI);
}
挂起的任务不参与调度,不消耗 CPU 时间。恢复时从挂起点继续执行,就像什么都没发生过。
vTaskSuspendAll() 暂停所有任务,然后单步检查全局变量。但注意,这个函数不能在中断里调用,否则会死锁。
4.5 任务优先级设计:别让“饿死”发生
优先级分配是个艺术。我一般遵循两个原则:
- 实时性要求高的给高优先级——比如支付任务,用户投币必须立刻响应。
- 耗时长的给低优先级——比如 UI 刷新,慢个 100ms 用户感觉不到。
但有个坑:如果高优先级任务一直不阻塞,低优先级任务会“饿死”。
我曾经犯过这个错。把支付任务优先级设得最高,结果它在一个死循环里轮询硬币状态,CPU 被它占满,UI 任务根本得不到执行。屏幕卡死,用户以为机器坏了。
解决方案?让高优先级任务主动让出 CPU:
void vTaskPayment(void *pvParameters)
{
for(;;)
{
if(Coin_Detect())
{
// 处理支付...
}
else
{
// 没有硬币?让出CPU给其他任务
taskYIELD();
}
}
}
或者更优雅的做法:用 vTaskDelay() 或阻塞在信号量上,而不是轮询。
4.6 总结:任务管理的“三板斧”
好了,这一章的内容就这些。总结一下我的经验:
- 创建任务:栈大小给够,优先级想清楚。
- 状态切换:多用阻塞,少用轮询。让 CPU 歇着。
- 挂起恢复:低功耗场景的利器,别滥用。
下一章我们会深入任务间通信——队列和信号量。到时候你会看到,任务之间怎么“聊天”才不会打架。