3. ThreadX 任务状态机:六态转换与 API 触发条件

任务状态机,说白了就是 RTOS 调度器的「交通规则」。每个任务在生命周期里,都会在几个状态之间跳来跳去。我刚开始用 ThreadX 时,总觉得状态图是理论派画出来唬人的,直到有一次产品死锁,查了两天才发现是一个任务卡在了「挂起态」没出来……嗯,从那以后我再看状态机,就像看地图一样认真。

ThreadX 的任务状态比很多 RTOS 多两个——休眠态和完成态。一共六个状态,咱们一个一个捋清楚。

3.1 六态总览

先看这张图,你脑子里就有个框架了:

ThreadX 任务六态转换图 就绪态 (Ready) 运行态 (Running) 挂起态 (Suspended) 休眠态 (Sleeping) 终止态 (Terminated) 完成态 (Completed) 调度器选中 时间片用完/抢占 tx_thread_suspend tx_thread_resume tx_thread_sleep 休眠时间到 tx_thread_terminate tx_thread_reset 函数返回 tx_thread_reset 说明:休眠态和完成态是 ThreadX 区别于其他 RTOS 的独特设计,用于低功耗管理和任务生命周期终结。 所有状态转换均由 API 调用或调度器内部事件触发,不存在「自动跳转」。
我的习惯:调试时我总在状态转换点加 GPIO 翻转,用示波器看任务切换的时序。比 printf 靠谱多了,不干扰实时性。

3.2 六个状态,逐个拆解

3.2.1 就绪态(Ready)

任务已经准备好运行,只等调度器「翻牌子」。所有就绪任务按优先级排在一个链表里——就绪链表。调度器每次从这里挑一个优先级最高的去执行。

  • 进入条件:任务创建后自动进入;从挂起/休眠/终止/完成态被恢复时也会进入。
  • 离开条件:调度器选中它,变成运行态。
  • 相关 API:无直接 API,由调度器内部管理。

3.2.2 运行态(Running)

CPU 正在执行这个任务的代码。同一时刻,单核芯片上只有一个任务处于运行态。你想想看,这就是「皇帝」状态,但随时可能被「篡位」。

  • 进入条件:调度器从就绪链表中选中当前最高优先级任务。
  • 离开条件:时间片用完、被更高优先级任务抢占、主动挂起/休眠/终止/完成。
  • 相关 API:无直接 API,由调度器触发。
我曾经踩过的坑:在一个中断里调用了 tx_thread_sleep(),结果系统直接挂掉。记住——中断上下文不能调用任何可能引起任务切换的 API!运行态只属于任务,不属于 ISR。

3.2.3 挂起态(Suspended)

任务被「人为暂停」,不参与调度。挂起态是主动的、显式的暂停,不是等信号量那种阻塞。我习惯用它来做「任务级别的暂停/恢复」控制。

  • 进入条件:调用 tx_thread_suspend()。
  • 离开条件:调用 tx_thread_resume()。
  • 相关 API:
    UINT tx_thread_suspend(TX_THREAD *thread_ptr);
    UINT tx_thread_resume(TX_THREAD *thread_ptr);

3.2.4 休眠态(Sleeping)

任务主动「睡一会儿」,时间到了自动醒来。这是 ThreadX 实现低功耗的关键——当所有任务都休眠时,系统可以进入深度睡眠模式。

  • 进入条件:调用 tx_thread_sleep()。
  • 离开条件:休眠计时到期,自动回到就绪态。
  • 相关 API:
    UINT tx_thread_sleep(ULONG timer_ticks);
低功耗实战:我在一个电池供电的传感器节点上,把主任务设计成「采集→处理→休眠→唤醒→采集」的循环。休眠期间 CPU 进入 STOP 模式,电流从 50mA 降到 5µA。你想想看,这省下来的电有多可观。

3.2.5 终止态(Terminated)

任务被「强制结束」,但它的控制块和栈还在。终止态不是删除——任务还可以被重置后重新运行。这有点像「暂停营业,但店铺没拆」。

  • 进入条件:调用 tx_thread_terminate()。
  • 离开条件:调用 tx_thread_reset() 回到就绪态。
  • 相关 API:
    UINT tx_thread_terminate(TX_THREAD *thread_ptr);
    UINT tx_thread_reset(TX_THREAD *thread_ptr);

3.2.6 完成态(Completed)

任务的入口函数执行完毕,自然返回。这是 ThreadX 独有的状态——任务「寿终正寝」,但控制块保留,可以重置复用。

  • 进入条件:任务函数执行 return 语句。
  • 离开条件:调用 tx_thread_reset() 回到就绪态。
  • 相关 API:无直接进入 API,由任务返回自动触发;离开使用 tx_thread_reset()。

3.3 状态转换速查表

当前状态 触发 API / 事件 下一状态 备注
就绪态 调度器选中 运行态 自动发生,无需 API
运行态 时间片用完 / 被抢占 就绪态 调度器内部触发
运行态 tx_thread_suspend() 挂起态 不能自己挂自己
挂起态 tx_thread_resume() 就绪态 恢复后重新参与调度
运行态 tx_thread_sleep() 休眠态 参数为 tick 数
休眠态 计时到期 就绪态 自动唤醒
运行态 tx_thread_terminate() 终止态 强制结束,资源保留
终止态 tx_thread_reset() 就绪态 重置后从头运行
运行态 函数 return 完成态 自然结束
完成态 tx_thread_reset() 就绪态 复用任务控制块

3.4 几个容易混淆的点

挂起态 vs 休眠态:挂起是「别人叫醒你」,休眠是「闹钟叫醒你」。挂起需要另一个任务或 ISR 调用 resume,休眠是内核定时器自动唤醒。我一般在需要外部事件触发恢复时用挂起,在周期性任务中用休眠。

终止态 vs 完成态:终止是「被枪毙」,完成是「寿终正寝」。但两者都能通过 reset 复活。区别在于——终止态的任务可能在任何代码位置被掐断,完成态的任务已经执行完所有代码。如果你在终止态做 reset,任务会从入口函数重新开始;完成态也一样。

能不能自己挂起自己?不能。tx_thread_suspend() 不能挂起当前正在运行的任务。如果你想让当前任务暂停,用 tx_thread_sleep() 或者等待一个信号量/队列。我曾经看到新手在任务里写 tx_thread_suspend(tx_thread_identify()),结果直接触发断言——嗯,ThreadX 内部会检查这个。

调试小技巧:用 tx_thread_info_get() 可以实时查看任务当前状态。我在调试时会在串口终端每秒打印一次所有任务的状态,一眼就能看出哪个任务「卡住」了。

3.5 状态机与低功耗的关系

ThreadX 的低功耗模式,说白了就是利用「休眠态」来做文章。当所有任务都处于休眠态或挂起态时,就绪链表为空,调度器无事可做。这时 ThreadX 会调用一个钩子函数——tx_low_power_enter(),让芯片进入 STOP 或 STANDBY 模式。

我在一个项目中,把不用的任务全部挂起,只留一个主任务做周期性采集。主任务采集完数据就休眠 100ms,这 100ms 里 CPU 几乎不耗电。整个系统的平均功耗从 30mA 降到了 0.5mA——你想想看,电池寿命从 3 天变成了 6 个月。

关于低功耗的具体配置,咱们后面有专门章节细讲。这里你只要记住:状态机是低功耗的基础,休眠态是省电的开关。