3. embOS调度器启动与任务切换机制

embOS 的调度器并不是在 main() 里调用某个函数后就立刻开始工作的,它需要经历一个明确的启动过程:初始化内核数据结构、创建至少一个任务、然后通过 OS_Start() 把控制权从裸机环境交给调度器。理解这条路径,是掌握 embOS 实时行为的关键。本节围绕 OS_Start() 的执行流程、就绪表管理、任务切换触发条件,以及 Cortex-M 上 PendSV/SVC 的切换实现展开。

OS_Start() 执行流程

在调用 OS_Start() 之前,用户通常已经完成了 OS_Init() 和若干 OS_TASK_Create() 调用。此时系统中存在多个就绪任务,但还没有任何任务真正运行过。OS_Start() 的核心职责可以概括为三步:

  1. 选择最高优先级就绪任务:embOS 维护一个按优先级组织的就绪表,OS_Start() 会从中找出优先级最高的任务,将其设为当前任务(OS_CurTask)。
  2. 构造该任务的初始栈帧:为首次运行准备一个“看起来像刚从异常返回”的栈内容,使得后续通过异常返回机制进入任务函数。
  3. 触发首次上下文切换:在 Cortex-M 上通常通过 SVC 指令进入异常,再由 PendSV 完成真正的栈切换,最终跳转到任务入口。

需要强调的是,OS_Start() 本身不会返回。它要么进入第一个任务,要么在无就绪任务时进入空闲处理。因此它之后的代码在正常调度下是不可达的。

就绪表管理

embOS 使用“就绪表 + 优先级位图”的方式管理任务。每个优先级对应一个就绪链表,任务通过 OS_TASK_Create() 插入对应链表。为了快速找到最高优先级,embOS 维护一个位图变量(通常是一个 32 位或按优先级数量扩展的整数),每一位代表一个优先级是否有就绪任务。

当任务被创建、被唤醒或被挂起时,调度器会更新对应优先级的链表和位图。查找最高优先级就绪任务时,只需对位图做一次“找最高置位”的操作,即可得到优先级编号,再从对应链表取出第一个任务。这种设计使得调度决策的时间复杂度接近常数,非常适合硬实时场景。

embOS 默认优先级数值越小优先级越高(0 为最高)。在创建任务时指定的优先级必须唯一或允许共享,具体取决于配置。共享优先级时,同优先级任务按时间片轮转。

任务切换触发条件

任务切换并非随时发生,而是由明确的触发条件驱动。常见触发源包括:

  • 任务主动让出:调用 OS_TASK_Delay()、OS_TASK_WaitEvent()、OS_TASK_Suspend() 等阻塞型 API,当前任务从就绪表移除,调度器选择新的最高优先级任务。
  • 事件唤醒:中断或其它任务通过 OS_EVENT_Set()、OS_TASK_Resume() 等使某个更高优先级任务就绪,若其优先级高于当前任务,则触发切换。
  • 时间片到期:同优先级任务轮转时,系统节拍中断(SysTick)递减时间片,到期后触发切换。
  • 中断返回:在中断服务程序结束时,如果中断唤醒了更高优先级任务,则通过 PendSV 延迟切换,保证中断响应不被破坏。

这些条件最终都汇聚到同一个动作:设置 PendSV 挂起位,等待当前异常或中断处理完成后执行上下文切换。

Cortex-M 上的 PendSV/SVC 切换实现

Cortex-M 内核提供了两个非常适合 RTOS 的异常:SVC(Supervisor Call)和 PendSV(Pendable Service Call)。embOS 利用它们实现任务切换:

  • SVC:用于从任务上下文主动请求内核服务,例如 OS_Start() 首次启动、任务调用阻塞 API 时。SVC 异常优先级通常配置为较高,确保内核调用及时响应。
  • PendSV:用于实际的上下文切换。它被设置为最低优先级,这样当多个中断同时发生时,PendSV 会推迟到所有高优先级中断处理完毕后再执行,避免在中断中做繁重的栈切换。

切换过程大致如下:当需要切换时,内核将 PendSV 挂起位置 1;当前异常或中断返回时,若 PendSV 处于挂起状态且优先级允许,则进入 PendSV 处理程序。在 PendSV 中,硬件自动保存当前任务的寄存器到其栈(部分寄存器由硬件压栈,其余由软件保存),然后从新任务的栈恢复寄存器,最后通过异常返回指令跳转到新任务继续执行。

embOS 启动与任务切换路径(Cortex-M) OS_Init() 初始化内核数据结构 OS_TASK_Create() 创建任务,插入就绪表 OS_Start() 选择最高优先级任务 就绪表管理(优先级位图 + 链表) Prio 0 Task A Prio 1 Task B Prio 2 Task C Prio 3 空闲 位图:0b00000111 最高置位 → Prio 0 切换触发条件 Delay / WaitEvent / Suspend 事件唤醒 / 时间片到期 / 中断返回 SVC 异常 任务上下文请求内核服务 设置 PendSV 挂起位 PendSV 处理 保存当前任务上下文 恢复新任务上下文 首次任务启动:OS_Start() → SVC → PendSV → 异常返回 → 任务入口函数 任务栈中预置的 PC 指向任务函数,LR 指向任务退出处理,PSR 设置 Thumb 状态

首次任务启动过程

首次任务启动与普通任务切换有一个重要区别:普通切换是“保存旧任务、恢复新任务”,而首次启动时并没有“旧任务”需要保存。因此 OS_Start() 需要为新任务手工构造一个初始栈帧,使其看起来像是刚刚被切换进来一样。

在 Cortex-M 上,这个初始栈帧通常包含:

  • xPSR:设置 Thumb 位(bit 24),确保异常返回后处于 Thumb 状态。
  • PC:指向任务入口函数,例如 TaskA。
  • LR:指向任务退出处理函数,防止任务函数返回后跑飞。
  • R12、R3、R2、R1:通常清零或设为约定值。
  • R0:可作为任务参数传入。

随后 OS_Start() 触发 SVC,SVC 处理程序设置 PendSV 挂起位。当异常返回时,PendSV 被激活,执行上下文恢复:从新任务栈中弹出上述寄存器,最后执行异常返回指令。此时 CPU 的 PC 被设置为任务入口函数地址,任务正式开始运行。

首次启动的关键判断:如果 OS_Start() 调用时没有任何就绪任务,embOS 会进入空闲循环或触发断言。因此必须至少创建一个任务,且该任务优先级有效。

实践中的观察与验证

在实际调试中,可以通过以下方式验证调度器启动是否正常:

  • 在任务入口函数第一行设置断点,确认 OS_Start() 后能命中。
  • 查看 OS_CurTask 指针是否指向预期任务控制块。
  • 检查就绪表位图变量,确认最高优先级位与预期任务一致。
  • 在 PendSV 处理程序中设置断点,观察上下文保存与恢复的寄存器变化。

例如,创建两个任务:TaskA 优先级 0,TaskB 优先级 1。调用 OS_Start() 后,应首先运行 TaskA。若 TaskA 调用 OS_TASK_Delay(100),则 TaskA 从就绪表移除,位图最高位置变为优先级 1,TaskB 开始运行。100 个系统节拍后,TaskA 被唤醒,若其优先级高于 TaskB,则再次触发切换。

理解 OS_Start() 的执行流程、就绪表的位图管理、切换触发条件以及 PendSV/SVC 的协作方式,是后续学习任务间通信、中断处理和实时性能优化的基础。只有清楚“第一次切换是怎么发生的”,才能在调试中快速定位任务不运行、优先级反转或栈溢出等问题。