4. 任务状态与切换:运行态、就绪态、阻塞态、挂起态

各位做运动控制的朋友,咱们今天聊聊任务调度里最核心的东西——任务状态。说白了,一个任务从生到死,到底经历了哪些阶段?

我刚开始用FreeRTOS做多轴运动控制时,就吃过状态切换的亏。当时一个伺服电机在高速运动中突然卡顿,查了半天,发现是某个任务被意外挂起了,导致位置更新没跟上。嗯,从那以后,我对任务状态的理解就深刻多了。

4.1 四大核心状态

在实时系统中,任务有四种基本状态。你想想看,就像一个人一天的状态:正在干活(运行态)、等着干活(就绪态)、被什么事卡住了(阻塞态)、被叫去开会暂停工作(挂起态)。

状态 含义 运动控制中的典型场景
运行态 任务正在占用CPU执行 正在计算插补轨迹
就绪态 任务已准备好,等待调度器分配CPU 等待执行位置环计算
阻塞态 任务因等待某事件而暂停 等待编码器数据到达
挂起态 任务被主动暂停,不参与调度 调试时暂停某个轴的控制

关键点:一个CPU在任意时刻只能运行一个任务。其他任务要么等着,要么被卡住,要么被挂起。

4.2 状态转换图

下面这张图是我自己画的,把四种状态之间的转换关系讲清楚了。我个人习惯把这张图贴在工位旁边,调试时一眼就能看出任务卡在哪个环节。

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

我的经验:调试运动控制程序时,我经常用这个状态图来排查问题。如果电机不动了,先看控制任务是不是在阻塞态等数据,还是在挂起态被谁suspend了。

4.3 vTaskDelay 与 vTaskDelayUntil

这两个API是任务延时用的,但用法完全不同。我见过很多新手把两者混用,结果定时精度一塌糊涂。

vTaskDelay:相对延时

这个函数让任务阻塞指定的tick数。注意,它是相对延时——从调用时刻开始算起。

// 让任务延时100个tick
vTaskDelay(100);

// 实际阻塞时间 = 100 tick + 调度延迟
// 如果被高优先级任务抢占,实际延时可能更长

我在项目中遇到过一个问题:用vTaskDelay做周期性任务,比如每10ms执行一次位置更新。结果发现周期越来越长。为什么?因为vTaskDelay只保证最小延时,不保证精确周期。

警告:vTaskDelay不适合做精确的周期性调度。每次延时从当前时间开始算,加上任务执行时间,周期会逐渐漂移。

vTaskDelayUntil:绝对延时

这个函数就聪明多了。它指定一个绝对时间点,任务阻塞到那个时间点再唤醒。说白了,就是「我不管现在几点,反正到点叫我」。

TickType_t xLastWakeTime;
const TickType_t xFrequency = 100;  // 100 tick周期

// 初始化
xLastWakeTime = xTaskGetTickCount();

while(1) {
    // 等待下一个周期
    vTaskDelayUntil(&xLastWakeTime, xFrequency);
    
    // 执行周期性任务
    update_motor_position();
}

你想想看,用vTaskDelayUntil,不管任务执行了多久,下次唤醒时间都是固定的。这就保证了运动控制的周期性——对于伺服驱动来说,位置环的更新周期必须严格一致,否则电机就会抖动。

特性 vTaskDelay vTaskDelayUntil
延时方式 相对延时 绝对延时
周期精度 低,会漂移 高,固定周期
适用场景 一次性延时、非周期性等待 周期性任务(如控制环)
受任务执行时间影响

核心建议:做运动控制,周期性任务一律用vTaskDelayUntil。我曾经在一个四轴联动项目中,因为用了vTaskDelay导致各轴不同步,机器跑出来的轨迹全是锯齿。换成vTaskDelayUntil后,问题立刻解决。

4.4 状态切换的避坑指南

我总结几个实际项目中容易踩的坑,分享给大家:

  • 任务饿死:高优先级任务一直占用CPU,低优先级任务永远得不到运行。我曾经在调试时发现一个数据采集任务始终不执行,就是因为有个中断服务任务优先级太高,一直占着CPU不放。
  • 死锁:两个任务互相等待对方释放资源。比如任务A等信号量S1,任务B等信号量S2,但A持有S2,B持有S1。嗯,这种问题在运动控制中很致命,会导致电机突然停止。
  • 优先级反转:低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。我建议使用互斥量(Mutex)配合优先级继承机制来解决。

调试技巧:用vTaskList()和vTaskGetRunTimeStats()可以查看所有任务的状态和CPU占用率。我每次调试运动控制程序,第一件事就是打印任务状态表,看看有没有任务卡在阻塞态出不来。

好了,任务状态这块就讲到这里。记住一句话:状态切换是调度的灵魂,搞懂了状态,你就掌握了实时系统的命脉。


专注资料整理