第三章:任务管理与调度——让多个任务“有序共舞”

各位同学,欢迎来到第三章。这一章,我们要聊的是RTOS的核心——任务管理与调度。

说白了,任务管理就是回答三个问题:任务怎么生?任务怎么死?任务之间怎么抢CPU?

我在做第一个RTOS项目时,就踩过不少坑。比如任务优先级设反了,结果低优先级的电机控制任务一直抢不到CPU,电机直接“抖”起来了。嗯,那场面,挺尴尬的。

3.1 任务创建与删除

任务创建,就是给RTOS“生”一个任务。每个任务本质上是一个无限循环的函数,加上自己的栈空间和任务控制块(TCB)。

以FreeRTOS为例,创建任务的API长这样:

// 任务函数
void vMotorControlTask(void *pvParameters) {
    while(1) {
        // 读取编码器
        // 计算PID
        // 输出PWM
        vTaskDelay(pdMS_TO_TICKS(1)); // 1ms周期
    }
}

// 创建任务
TaskHandle_t xMotorHandle = NULL;
xTaskCreate(
    vMotorControlTask,   // 任务函数
    "Motor Control",     // 任务名(调试用)
    256,                 // 栈深度(单位:字)
    NULL,                // 参数
    5,                   // 优先级(0最低,configMAX_PRIORITIES-1最高)
    &xMotorHandle        // 任务句柄
);

我的习惯:创建任务时,栈大小一定要留余量。我一般会多给20%。为什么?因为中断嵌套、函数调用链深了,栈可能爆掉。我曾经在量产前才发现某个任务的栈只差4个字节就溢出了...冷汗都出来了。

任务删除呢?就是让任务“退役”。

// 任务自己删除自己
vTaskDelete(NULL);

// 或者由其他任务删除
vTaskDelete(xMotorHandle);

注意:删除任务前,要确保它占用的资源(如动态分配的内存、信号量、队列等)已经释放。否则会造成资源泄漏。我见过一个同事删了任务但没释放互斥锁,结果其他任务永远等不到这个锁,系统直接“死锁”。

3.2 任务优先级与调度策略

RTOS的调度策略,说白了就是“谁有资格用CPU”。主要有两种:

3.2.1 抢占式调度

高优先级任务就绪时,立即打断低优先级任务。这是实时系统的灵魂。

举个例子:

  • 任务A:优先级3,正在运行
  • 任务B:优先级5,从阻塞态变为就绪态
  • 结果:任务A被挂起,任务B立即运行

为什么会这样?因为RTOS要保证高优先级任务的实时性。你想想看,如果电机过流了,保护任务优先级最高,它必须立刻执行,不能等。

3.2.2 时间片调度

同优先级的任务,轮流使用CPU。每个任务运行一个时间片(通常1-10ms),然后切换给下一个。

我一般这样用:

  • 高优先级任务:抢占式,用于实时性要求高的(如电流环、位置环)
  • 低优先级任务:时间片,用于非实时性任务(如日志打印、状态显示)

避坑指南:我曾经把两个电机控制任务设为相同优先级,结果它们互相抢时间片,导致每个电机的控制周期都不稳定。后来我把它们设为不同优先级,用抢占式调度,问题就解决了。

3.3 任务状态切换

RTOS中的任务有四种状态:

状态 说明 典型场景
运行态 正在使用CPU 当前正在执行的任务
就绪态 可以运行,但CPU被别的任务占着 等待高优先级任务释放CPU
阻塞态 等待某个事件(延时、信号量、队列等) vTaskDelay()、等待消息
挂起态 被vTaskSuspend()暂停,不参与调度 调试时暂停某个任务

状态切换图(用SVG绘制):

运行态 就绪态 阻塞态 挂起态 调度器选择 被抢占/时间片用完 等待事件 事件到达 vTaskSuspend() vTaskResume()

这张图我建议你记在脑子里。调试RTOS问题时,90%的bug都能通过分析任务状态切换找到原因。

3.4 空闲任务与钩子函数

空闲任务是RTOS自动创建的,优先级最低。当所有其他任务都阻塞或挂起时,空闲任务运行。

空闲任务能干啥?

  • 统计CPU使用率
  • 进入低功耗模式
  • 做一些后台清理工作

钩子函数(Hook Function)是RTOS留给用户的“后门”。你可以注册一个函数,在空闲任务每次循环时被调用。

// 空闲任务钩子函数
void vApplicationIdleHook(void) {
    // 进入低功耗模式
    __WFI(); // Wait For Interrupt
}

// 在FreeRTOSConfig.h中启用
#define configUSE_IDLE_HOOK 1

我的经验:在运动控制系统中,我经常在空闲钩子里做两件事:

  1. 统计CPU负载——如果空闲任务运行时间占比低于10%,说明CPU快满了,需要优化
  2. 喂看门狗——确保系统不会因为任务卡死而复位

警告:不要在空闲钩子里调用任何可能阻塞的API(如vTaskDelay()、等待信号量等)。因为空闲任务本身不能阻塞,否则系统就“死”了。我曾经犯过这个错,结果系统一启动就卡死,查了半天才发现是空闲钩子里调了vTaskDelay()。

3.5 实战中的调度策略选择

我总结了一个简单的选择原则:

场景 推荐策略 原因
电流环(10-50kHz) 最高优先级,抢占式 实时性要求极高,不能被打断
位置环(1-10kHz) 次高优先级,抢占式 需要及时响应,但可容忍少量延迟
通信任务(CAN/以太网) 中等优先级,抢占式 避免丢包,但可缓冲
日志打印 低优先级,时间片 不着急,慢慢来
人机界面 最低优先级,时间片 用户操作慢,不敏感

嗯,这一章的内容就到这里。任务管理是RTOS的基石,搞懂了它,后面的同步、通信、中断管理才能顺利展开。记住:优先级不是越高越好,合理分配才是王道。


公众号:蓝海资料掘金营,微信deep3321