3. 任务创建与管理:任务函数原型、任务创建API、任务删除与挂起、任务优先级、任务堆栈大小估算

好,咱们进入第三章。这一章讲的是RTOS里最基础、也最核心的操作——怎么把任务“生”出来,怎么管好它,以及万一出问题了怎么让它“休眠”或者“消失”。

我刚开始用RTOS做制氧机项目时,犯过一个低级错误:任务堆栈给得太小,结果系统跑着跑着就莫名其妙地死机了。排查了整整两天,最后发现是堆栈溢出了。从那以后,我对任务创建这件事就格外上心。今天我把这些经验都倒给你。

3.1 任务函数原型

每个任务本质上就是一个C函数。但RTOS对它有特殊要求。说白了,这个函数不能像普通函数那样随便return。为什么呢?因为任务一旦return,RTOS就不知道它该干嘛了,系统会直接挂掉。

任务函数的原型长这样:

void vTaskFunction(void *pvParameters)
{
    // 任务初始化代码
    for(;;)
    {
        // 任务主体代码
        // 等待事件、处理数据、控制外设等
        vTaskDelay(pdMS_TO_TICKS(10)); // 让出CPU
    }
}

注意看,函数返回类型是void,参数是一个void指针。这个指针可以传任何东西——整数、结构体、字符串,都行。我个人习惯用结构体传参,这样扩展性好。

还有一点:任务函数里必须有一个无限循环。为什么?因为任务被创建后,RTOS会一直调度它。如果它执行完了,RTOS会调用一个内部错误处理函数,默认就是挂起系统。嗯,这里要注意,千万别让任务跑到底。

小技巧: 如果你确实想让某个任务只执行一次就结束,可以在函数末尾调用 vTaskDelete(NULL) 把自己删掉。这样RTOS就知道这个任务已经完成了。

3.2 任务创建API

FreeRTOS里创建任务主要用两个API:xTaskCreate 和 xTaskCreateStatic。前者动态分配堆栈,后者用你预先准备好的静态内存。

我一般在制氧机项目里用动态创建,省心。但如果你对内存有严格限制,比如某些工业级控制器,那就得用静态创建。

来看看xTaskCreate的原型:

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

返回值是pdPASS表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示内存不够。我记得有一次调试,死活创建不了任务,最后发现是FreeRTOS的堆大小配置得太小了。

举个例子,创建一个控制氧气浓度的任务:

TaskHandle_t xO2ControlHandle = NULL;

void vO2ControlTask(void *pvParameters)
{
    uint16_t *pO2Setpoint = (uint16_t *)pvParameters;
    
    for(;;)
    {
        // 读取氧传感器
        // PID计算
        // 控制电磁阀
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

// 在main函数或初始化中创建
uint16_t usO2Setpoint = 93; // 目标氧浓度93%
xTaskCreate(
    vO2ControlTask,
    "O2Control",
    256,        // 堆栈大小,单位是字
    (void *)&usO2Setpoint,
    3,          // 优先级
    &xO2ControlHandle
);

3.3 任务删除与挂起

任务不是创建了就完事了。有时候我们需要暂停某个任务,或者彻底把它干掉。

任务删除:

用 vTaskDelete() 函数。传入任务句柄,RTOS就会把这个任务从调度列表中移除,并释放它占用的内存(如果是动态创建的)。

// 删除自己
vTaskDelete(NULL);

// 删除其他任务
vTaskDelete(xO2ControlHandle);
警告: 千万不要在中断服务函数里调用 vTaskDelete()!我曾经犯过这个错,结果系统直接崩溃。因为中断上下文里不允许进行任务切换。正确的做法是在中断里发一个信号量或消息,让一个高优先级任务来处理删除操作。

任务挂起与恢复:

挂起和删除不一样。挂起只是让任务暂时“睡着”,它还在内存里,随时可以唤醒。这在制氧机里很有用。比如,当系统检测到传感器故障时,可以挂起控制任务,等故障恢复后再唤醒。

// 挂起任务
vTaskSuspend(xO2ControlHandle);

// 恢复任务
vTaskResume(xO2ControlHandle);

还有一个常用的API是 vTaskSuspendAll(),它会挂起所有任务。我一般在做系统级配置更新时用这个,确保操作是原子的。

3.4 任务优先级

优先级是RTOS调度的核心。FreeRTOS支持0到(configMAX_PRIORITIES - 1)的优先级,数值越大优先级越高。

在制氧机里,我通常这样分配优先级:

优先级 任务名称 说明
5(最高) 安全监控任务 监测氧浓度、压力、流量,一旦超标立即报警并停机
4 传感器采集任务 定时读取氧传感器、压力传感器数据
3 控制算法任务 PID计算、阀门控制
2 人机交互任务 按键扫描、LCD显示更新
1 通信任务 与上位机或云平台通信
0(最低) 空闲任务 系统空闲时运行,通常用于低功耗管理

你想想看,如果安全监控任务的优先级不够高,万一氧浓度超标了,它抢不到CPU,那后果就严重了。所以安全相关的任务一定要给最高优先级。

核心原则: 实时性要求越高的任务,优先级越高。但要注意优先级反转问题——当一个低优先级任务持有高优先级任务需要的资源时,高优先级任务反而被阻塞。解决办法是使用互斥量(带优先级继承的)。

3.5 任务堆栈大小估算

这是最容易踩坑的地方。堆栈给大了浪费内存,给小了系统崩溃。我见过太多人在这上面翻车了。

堆栈大小取决于几个因素:

  • 函数调用深度: 任务里调用的函数越多、嵌套越深,需要的堆栈越大
  • 局部变量大小: 特别是数组、结构体这类占用空间大的变量
  • 中断嵌套: 中断服务函数也会使用当前任务的堆栈
  • RTOS API调用: 某些API内部会使用额外的堆栈空间

我有个估算方法,你可以参考:

  1. 先给一个保守值,比如256字(1024字节)
  2. 运行系统,用 uxTaskGetStackHighWaterMark() 函数查看实际使用量
  3. 根据结果调整,一般留出30%-50%的余量

举个例子:

// 在任务中调用,查看剩余堆栈空间
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
printf("剩余堆栈: %d 字\n", uxHighWaterMark);

如果剩余堆栈接近0,说明堆栈给得太小了。我曾经在一个项目中,把通信任务的堆栈从128字加到512字才稳定下来。原因是那个任务里用了一个很大的JSON解析缓冲区。

避坑指南: 我曾经在调试一个制氧机时,发现系统每隔几小时就死机一次。用 uxTaskGetStackHighWaterMark 一查,发现某个任务的堆栈剩余只有4个字了。原来是某个异常分支里分配了一个大数组。所以,一定要在开发阶段就检查堆栈使用情况,别等到现场出问题再排查。

好了,这一章的内容就这些。任务创建和管理是RTOS的基石,你把这些搞明白了,后面的状态机设计才能稳稳地往上搭。下一章我们讲任务间的同步与通信,那才是真正让多个任务协同工作的关键。