3. embOS调度器启动与任务切换机制
embOS 的调度器并不是在 main() 里调用某个函数后就立刻开始工作的,它需要经历一个明确的启动过程:初始化内核数据结构、创建至少一个任务、然后通过 OS_Start() 把控制权从裸机环境交给调度器。理解这条路径,是掌握 embOS 实时行为的关键。本节围绕 OS_Start() 的执行流程、就绪表管理、任务切换触发条件,以及 Cortex-M 上 PendSV/SVC 的切换实现展开。
OS_Start() 执行流程
在调用 OS_Start() 之前,用户通常已经完成了 OS_Init() 和若干 OS_TASK_Create() 调用。此时系统中存在多个就绪任务,但还没有任何任务真正运行过。OS_Start() 的核心职责可以概括为三步:
- 选择最高优先级就绪任务:embOS 维护一个按优先级组织的就绪表,
OS_Start()会从中找出优先级最高的任务,将其设为当前任务(OS_CurTask)。 - 构造该任务的初始栈帧:为首次运行准备一个“看起来像刚从异常返回”的栈内容,使得后续通过异常返回机制进入任务函数。
- 触发首次上下文切换:在 Cortex-M 上通常通过
SVC指令进入异常,再由 PendSV 完成真正的栈切换,最终跳转到任务入口。
需要强调的是,OS_Start() 本身不会返回。它要么进入第一个任务,要么在无就绪任务时进入空闲处理。因此它之后的代码在正常调度下是不可达的。
就绪表管理
embOS 使用“就绪表 + 优先级位图”的方式管理任务。每个优先级对应一个就绪链表,任务通过 OS_TASK_Create() 插入对应链表。为了快速找到最高优先级,embOS 维护一个位图变量(通常是一个 32 位或按优先级数量扩展的整数),每一位代表一个优先级是否有就绪任务。
当任务被创建、被唤醒或被挂起时,调度器会更新对应优先级的链表和位图。查找最高优先级就绪任务时,只需对位图做一次“找最高置位”的操作,即可得到优先级编号,再从对应链表取出第一个任务。这种设计使得调度决策的时间复杂度接近常数,非常适合硬实时场景。
任务切换触发条件
任务切换并非随时发生,而是由明确的触发条件驱动。常见触发源包括:
- 任务主动让出:调用
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 中,硬件自动保存当前任务的寄存器到其栈(部分寄存器由硬件压栈,其余由软件保存),然后从新任务的栈恢复寄存器,最后通过异常返回指令跳转到新任务继续执行。
首次任务启动过程
首次任务启动与普通任务切换有一个重要区别:普通切换是“保存旧任务、恢复新任务”,而首次启动时并没有“旧任务”需要保存。因此 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 的协作方式,是后续学习任务间通信、中断处理和实时性能优化的基础。只有清楚“第一次切换是怎么发生的”,才能在调试中快速定位任务不运行、优先级反转或栈溢出等问题。