4. 任务与调度核心概念

在 S32K 这类基于 ARM Cortex-M 内核的 MCU 上运行 RTOS,任务并不是“一直跑到底”的函数,而是被内核反复切换、暂停、恢复的执行实体。理解任务状态机、任务控制块(TCB)、优先级、就绪表、调度时机与上下文切换,是写出稳定实时系统的前提。本节以 FreeRTOS 在 S32K144 上的典型实现为主线,把这些概念串成一条完整的运行链路。

一、任务状态机:任务的一生

一个任务从创建到删除,会在若干状态之间迁移。FreeRTOS 中最核心的状态有四种:运行态(Running)、就绪态(Ready)、阻塞态(Blocked)和挂起态(Suspended)。单核 MCU 上同一时刻只有一个任务处于运行态,其余就绪任务排队等待 CPU。

  • 就绪态:任务已具备运行条件,只差 CPU 使用权,等待调度器选中。
  • 运行态:当前占用 CPU 的任务,正在执行指令。
  • 阻塞态:任务主动等待某个事件(延时到期、信号量、队列数据),带超时时间,超时后自动回到就绪态。
  • 挂起态:任务被显式挂起,不参与调度,也不会因超时自动恢复,必须由 vTaskResume() 唤醒。

状态迁移的触发点非常明确:调用 vTaskDelay() 进入阻塞;等待的信号量被释放则回到就绪;调用 vTaskSuspend() 进入挂起;被调度器选中则从就绪变为运行;时间片耗尽或被更高优先级任务抢占则从运行退回就绪。

FreeRTOS 任务状态机与迁移条件 就绪态 Ready 运行态 Running 阻塞态 Blocked 挂起态 Suspended 调度器选中 被抢占/时间片耗尽 vTaskDelay / 等事件 超时/事件到达 vTaskSuspend() vTaskResume() 关键:阻塞态带超时自动恢复;挂起态必须显式恢复;运行态唯一

二、任务控制块 TCB:任务的“身份证”

内核要管理任务,必须为每个任务保存一份数据结构,这就是任务控制块 TCB。在 FreeRTOS 中它由 TCB_t 定义,创建任务时由 xTaskCreate() 动态分配或使用静态分配。TCB 中保存了任务恢复运行所需的全部信息:

  • 栈指针:指向该任务私有栈的栈顶,上下文切换时最先保存和恢复。
  • 任务状态与优先级:当前状态、当前优先级、基础优先级。
  • 事件链表项:任务阻塞在哪个事件列表上,用于超时管理。
  • 就绪链表项:任务在就绪表中的挂接位置。
  • 任务名与句柄:调试和 API 调用使用。

可以把 TCB 理解为任务的“存档文件”:任务被切走时,CPU 寄存器写入它的栈,栈指针存入 TCB;任务被切回时,从 TCB 取出栈指针,再从栈中恢复寄存器。

三、优先级与就绪表:谁先上 CPU

FreeRTOS 默认采用抢占式优先级调度,优先级数值越大越高。S32K144 上通常配置 configMAX_PRIORITIES 为 5~10,数值越大越紧急。相同优先级的任务按时间片轮转。

就绪表是调度器的“候选名单”。FreeRTOS 用 pxReadyTasksLists[] 数组实现,每个优先级一个就绪链表。调度器只需从最高非空优先级链表取第一个任务即可,无需遍历所有任务。为了快速找到最高非空优先级,内核还维护一个位图 uxTopReadyPriority,用位操作在常数时间内定位。

概念数据结构/接口作用
任务控制块TCB_t保存任务栈指针、状态、优先级等上下文
就绪表pxReadyTasksLists[]按优先级组织的就绪任务链表数组
最高就绪优先级uxTopReadyPriority位图快速定位最高非空优先级
阻塞链表xDelayedTaskList按唤醒时间排序的阻塞任务
调度器vTaskSwitchContext()选出下一个运行任务

四、调度时机与上下文切换

调度不是随时发生的,而是在明确的“调度点”触发。常见调度时机包括:

  1. 任务调用阻塞 API,如 vTaskDelay()、xQueueReceive()。
  2. 中断服务程序中释放了信号量或发送了队列消息,且唤醒了更高优先级任务。
  3. 任务主动调用 taskYIELD() 让出 CPU。
  4. 系统节拍中断(SysTick)到来,检查时间片是否耗尽。
  5. 任务被创建、删除、挂起或恢复,导致就绪表变化。

上下文切换的本质是“保存现场、恢复现场”。以 Cortex-M 为例,硬件在异常入口自动压栈 R0-R3、R12、LR、PC、xPSR;PendSV 异常中,内核手动保存 R4-R11 到当前任务栈,把栈指针写入当前 TCB,然后调用调度器选出新任务,再从新任务 TCB 取出栈指针,恢复 R4-R11,最后异常返回自动恢复剩余寄存器。整个过程由 PendSV_Handler 完成,因此 PendSV 常被设置为最低优先级,避免打断其他中断。

/* S32K144 + FreeRTOS 典型任务创建与优先级配置 */
#define TASK_LED_PRIO     2
#define TASK_CAN_PRIO     4
#define TASK_LOG_PRIO     1

xTaskCreate(vTaskLed,  "LED",  256, NULL, TASK_LED_PRIO, NULL);
xTaskCreate(vTaskCan,  "CAN",  512, NULL, TASK_CAN_PRIO, NULL);
xTaskCreate(vTaskLog,  "LOG",  384, NULL, TASK_LOG_PRIO, NULL);

/* CAN 任务优先级最高,收到报文后可立即抢占 LED 与 LOG 任务 */
vTaskStartScheduler();

关键结论:优先级决定“谁先运行”,就绪表决定“谁在等待”,TCB 决定“怎么恢复”,PendSV 决定“怎么切换”。四者共同构成 RTOS 调度的最小闭环。

五、案例与常见误区

假设 S32K144 上有一个 CAN 接收任务(优先级 4)、一个 LED 闪烁任务(优先级 2)和一个日志任务(优先级 1)。当 CAN 中断收到报文并释放信号量时,CAN 任务从阻塞态变为就绪态,由于优先级最高,PendSV 触发后立即抢占 LED 任务。LED 任务被切回就绪态,等 CAN 任务再次阻塞后才继续运行。这就是抢占式调度的典型表现。

常见误区一:在中断中直接调用非 FromISR 版本的 API,导致就绪表被破坏或系统崩溃。中断中必须使用 xQueueSendFromISR()、xSemaphoreGiveFromISR() 等带 FromISR 后缀的接口。

常见误区二:把耗时操作放在高优先级任务中,导致低优先级任务长期得不到执行,表现为“系统卡死”。应遵循“高优先级任务短小、快进快出”的原则。

常见误区三:任务栈分配过小。上下文切换需要保存寄存器,若栈溢出会破坏相邻 TCB 或内存,建议开启 configCHECK_FOR_STACK_OVERFLOW 并在调试阶段使用 uxTaskGetStackHighWaterMark() 检查余量。

复盘时可以用一个简单清单检查调度设计是否合理:每个任务是否有明确的阻塞点?最高优先级任务是否足够短?中断是否只做“释放信号量”这类轻量动作?就绪表中是否存在长期不被调度的低优先级任务?把这些问题逐一确认,S32K 上的 RTOS 任务调度就能稳定可控。