3. 任务创建与管理:任务函数、任务创建API、任务删除与挂起、任务通知
好,咱们进入第三章。任务创建与管理,这可以说是RTOS开发的「基本功」。我见过不少新手,一上来就堆任务,结果系统跑起来跟老牛拉破车似的。说白了,任务管理没做好,后面全是坑。
这一章,我会把任务函数怎么写、API怎么用、任务怎么删怎么挂起,还有那个很实用的任务通知,都给你捋一遍。嗯,咱们开始。
3.1 任务函数:任务的「灵魂」
每个任务,本质上就是一个永不返回的C函数。为什么说永不返回?因为任务一旦启动,它就在一个死循环里跑,直到你主动把它干掉。
我个人习惯,任务函数长这样:
void vTaskFunction(void *pvParameters)
{
// 1. 初始化部分(只执行一次)
init_sensor();
// 2. 任务主循环
for(;;)
{
// 处理业务逻辑
read_data();
process_data();
// 别忘了一定要加阻塞!
vTaskDelay(pdMS_TO_TICKS(100));
}
}
这里有个关键点:任务函数里绝对不能有return。我在项目中遇到过有人把任务函数写成普通函数,执行完就返回了,结果系统直接崩溃。你想想看,RTOS调度器发现任务「消失了」,它怎么处理?
vTaskDelete(NULL) 主动自杀,而不是让函数自然返回。
3.2 任务创建API:xTaskCreate 与 xTaskCreateStatic
创建任务,最常用的就是 xTaskCreate。它的参数看着多,其实用熟了也就那么回事。
| 参数 | 说明 | 我的建议 |
|---|---|---|
| pvTaskCode | 任务函数指针 | 直接写函数名就行 |
| pcName | 任务名字(调试用) | 别太长,16字节以内 |
| usStackDepth | 栈大小(单位是字) | 宁大勿小,我一般给256起步 |
| pvParameters | 传给任务的参数 | 可以传结构体指针 |
| uxPriority | 任务优先级 | 0是最低,数值越大优先级越高 |
| pxCreatedTask | 任务句柄(可选) | 后面操作任务要用到 |
举个例子:
TaskHandle_t xHandle = NULL;
BaseType_t xReturn = xTaskCreate(
vTaskFunction, // 任务函数
"Sensor Task", // 任务名
256, // 栈大小
NULL, // 参数
2, // 优先级
&xHandle // 句柄
);
if (xReturn == pdPASS)
{
// 创建成功
printf("任务创建成功!\n");
}
else
{
// 创建失败,多半是内存不够
printf("任务创建失败,检查堆大小\n");
}
还有一个 xTaskCreateStatic,用于静态分配内存。我一般在资源紧张的场景才用它。比如某次做电池供电的设备,RAM只有64KB,用静态分配能精确控制每一字节。
3.3 任务删除与挂起:该放手时就放手
任务不是创建了就完事了。有时候需要暂停它,有时候需要彻底干掉它。
3.4.1 任务删除
用 vTaskDelete。注意,删除后任务占用的内存会被回收,但如果你用了动态分配,记得自己释放。
// 删除自己
vTaskDelete(NULL);
// 删除其他任务(需要句柄)
vTaskDelete(xHandle);
我曾经犯过一个错:在中断服务函数里调用了 vTaskDelete。结果呢?系统直接挂掉。记住,任务删除不能在中断里调用。
3.4.2 任务挂起与恢复
挂起和删除不一样。挂起只是让任务「睡觉」,随时可以叫醒。删除是彻底「消失」。
// 挂起任务
vTaskSuspend(xHandle);
// 恢复任务
vTaskResume(xHandle);
我有个实际案例:做温控系统时,温度采集任务平时不需要跑那么快,我就把它挂起,等用户按下「开始测量」按钮再恢复。这样既省电又省CPU。
- 删除:任务彻底消失,资源回收。适合一次性任务。
- 挂起:任务暂停,资源保留。适合需要频繁启停的任务。
3.4 任务通知:轻量级的「传话员」
任务之间怎么通信?很多人第一反应是队列和信号量。但我要告诉你,任务通知是FreeRTOS里最轻量、最高效的通信方式。
为什么这么说?因为任务通知直接操作任务控制块(TCB),不需要创建额外的队列或信号量对象。说白了,就是直接在任务身上「贴纸条」。
3.4.1 发送通知
// 发送通知(不带值)
xTaskNotifyGive(xHandle);
// 发送通知(带32位值)
xTaskNotify(xHandle, 0x01, eSetBits);
3.4.2 接收通知
uint32_t ulNotifiedValue;
// 阻塞等待通知
ulNotifiedValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 或者用这个
xTaskNotifyWait(0x00, ULONG_MAX, &ulNotifiedValue, portMAX_DELAY);
我记得有一次做多传感器数据融合,三个传感器任务各自采集数据,然后通过任务通知告诉主控任务「数据准备好了」。主控任务收到通知后,一次性读取所有数据。整个过程没有用队列,没有用信号量,代码简洁得让人舒服。
- 一个任务通知另一个任务「干活」
- 中断服务程序通知任务「事件发生」
- 轻量级的二值信号量替代
3.5 避坑指南:我踩过的那些坑
最后,分享几个实战中容易翻车的地方:
- 栈溢出:任务栈太小,跑着跑着就崩了。我建议在调试阶段开启栈溢出检测钩子函数
vApplicationStackOverflowHook。 - 优先级反转:低优先级任务占着资源不放,高优先级任务干瞪眼。解决方案是使用互斥量(带优先级继承的)。
- 任务删除后句柄失效:删除了任务,但还拿着句柄去操作它。轻则无效操作,重则系统崩溃。我习惯在删除后把句柄置为NULL。
- 在中断里调用API:不是所有API都能在中断里用。比如
vTaskDelay就不行。记得查文档,或者用带FromISR后缀的版本。
嗯,这一章的内容就到这里。任务创建与管理,说白了就是「创建-运行-删除/挂起」这个循环。但每个环节都有细节,细节决定成败。下一章我们会聊任务同步与通信,到时候你会看到任务通知和队列、信号量怎么配合使用。
记住,多实践,多调试。代码是写出来的,也是调出来的。