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,用静态分配能精确控制每一字节。

小技巧:调试阶段,我习惯把栈大小设大一点(比如512),等稳定了再调小。曾经有一次我为了省内存把栈设成128,结果任务跑着跑着就栈溢出,查了三天才找到原因。

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 避坑指南:我踩过的那些坑

最后,分享几个实战中容易翻车的地方:

  1. 栈溢出:任务栈太小,跑着跑着就崩了。我建议在调试阶段开启栈溢出检测钩子函数 vApplicationStackOverflowHook
  2. 优先级反转:低优先级任务占着资源不放,高优先级任务干瞪眼。解决方案是使用互斥量(带优先级继承的)。
  3. 任务删除后句柄失效:删除了任务,但还拿着句柄去操作它。轻则无效操作,重则系统崩溃。我习惯在删除后把句柄置为NULL。
  4. 在中断里调用API:不是所有API都能在中断里用。比如 vTaskDelay 就不行。记得查文档,或者用带 FromISR 后缀的版本。

嗯,这一章的内容就到这里。任务创建与管理,说白了就是「创建-运行-删除/挂起」这个循环。但每个环节都有细节,细节决定成败。下一章我们会聊任务同步与通信,到时候你会看到任务通知和队列、信号量怎么配合使用。

记住,多实践,多调试。代码是写出来的,也是调出来的。