FreeRTOS任务调度

FreeRTOS任务调度的核心目标是确保就绪态中优先级最高的任务始终获得CPU执行权 。这一目标的实现,绝非简单的队列轮询,而是一个深度融合了软件调度逻辑与硬件底层机制(堆栈、中断、寄存器)的精密过程。其全貌可概括为两大阶段:系统的启动与第一个任务的运行,以及后续持续的任务切换与抢占。

1 双重堆栈与调度原则

1.1 双堆栈指针机制(MSP/PSP)

复制代码
主堆栈指针(MSP):服务于内核和所有中断/异常。当中断发生时,CPU自动使用MSP来保存上下文、执行中断服务例程(ISR)。内核关键数据也存放在MSP指向的区域。

进程堆栈指针(PSP):服务于每个用户任务。每个任务都有自己独立的栈空间,用于保存函数调用局部变量、任务挂起时的完整CPU现场(上下文)。任务在非中断的"线程模式"下运行时,使用PSP。

核心规则:中断(如SysTick, PendSV, SVC)必须使用MSP;普通任务代码使用PSP。这种隔离保证了内核的稳定性和任务间的独立性。

1.2 调度核心原则

复制代码
优先级抢占:一旦有更高优先级的任务进入就绪态,当前运行的任务应立即被抢占。

上下文即任务:任务的本质是其运行现场------所有CPU寄存器的值(包括PC程序计数器)。因此,任务切换的本质就是保存当前任务的寄存器集合(上下文)到其私有栈中,并将下一个任务的上下文从其私有栈恢复到CPU寄存器。

中断驱动:调度动作(启动、切换)由特定中断触发,以确保操作的原子性和对硬件的精确控制。

2 系统启动与第一个任务的诞生

这是从裸机到多任务的转折点,由**vTaskStartScheduler()**总领。

2.1 软件初始化

复制代码
创建空闲任务(Idle Task),它是一个最低优先级的后台任务,确保CPU永远有任务可执行。
若启用,创建定时器服务任务,用于处理软件定时器回调。
初始化全局变量和时基。

2.2 硬件适配与启动触发

复制代码
调用硬件层函数 xPortStartScheduler()。其核心工作包括:
    配置SysTick定时器:根据 configTICK_RATE_HZ 设置节拍中断,这是系统的心跳,周期性触发任务时间片检查。
    设置PendSV和SysTick中断为最低优先级:确保任务切换不会抢占其他紧急的中断处理。
    调用prvStartFirstTask()。

2.3 第一个任务的启动

复制代码
prvStartFirstTask()首先从向量表初始化MSP,因为即将发生的中断需要它。
然后,它触发一个SVC(系统服务调用)中断。SVC是一个特殊的异常,常用于从特权模式(如启动代码)请求系统服务。
SVC中断服务程序vPortSVCHandler()执行:
    通过pxCurrentTCB(一个始终指向当前运行任务的指针)找到创建好的、优先级最高的就绪任务(通常是您创建的第一个应用任务)。
    从该任务的任务控制块(TCB)中获取其栈顶指针。这个栈在任务创建时已被精心初始化,模拟了一个"保存好的现场"。
    执行关键的恢复现场操作:将该栈中预先存放的寄存器值(R4-R11, R14, 以及后续自动加载的R0-R3, R12, LR, PC, xPSR)弹出到CPU寄存器。
    将该栈顶地址赋值给PSP。
    通过一条BX R14指令(此时R14中是一个特殊的"异常返回值")退出中断。CPU检测到这个返回值后,会自动切换回线程模式,并改用PSP作为堆栈指针,同时继续从栈中恢复剩余的寄存器。最后,当程序计数器(PC)被恢复时,CPU就跳转到了该任务的入口函数,第一个任务从此开始运行。

3 任务切换与抢占的引擎

系统运行后,调度就交给了SysTick和PendSV。

3.1 触发源

复制代码
周期触发(SysTick):每个系统节拍,SysTick中断发生。在中断服务程序中,内核会检查:
    当前任务的时间片是否用完(对于同优先级轮转调度)。
    是否有更高优先级的任务因延时到期、信号量释放等事件而进入就绪态。
    如果需要切换,则触发一个PendSV中断。
    主动触发(API调用):当任务调用vTaskDelay(),taskYIELD(), 或释放信号量/队列等可能导致更高优先级任务就绪的API时,内核也会触发PendSV中断。

3.2 上下文切换的执行者(PendSV中断)

PendSV被设为最低优先级,可以确保所有紧急中断处理完毕后,再执行切换,因此它被称为惰性保存中断。
PendSV中断服务程序xPortPendSVHandler()执行:
保存当前任务(任务A)的现场:

复制代码
1.  读取当前的PSP。
2.  将CPU寄存器R4-R11,以及R14(LR)的值,手动压入任务A的栈(使用PSP)。
3.  将更新后的栈顶指针保存回任务A的TCB -> pxTopOfStack。此刻,任务A的完整运行状态已被"冷冻"在自己的堆栈中。

选择下一个任务:

复制代码
调用vTaskSwitchContext()。此函数是调度策略的核心,它会扫描就绪任务链表,通过优化算法(如前导零指令)快速找到最高优先级的就绪任务,并更新全局指针pxCurrentTCB指向它(任务B)。

恢复下一个任务(任务B)的现场:

复制代码
1.  从任务B的TCB -> pxTopOfStack 中获取其栈顶指针。
2.  从该栈中手动弹出之前保存的R4-R11和R14值到CPU寄存器。
3.  将栈顶指针更新到 PSP。

**通过BX R14指令退出中断。**CPU同样会自动切换回线程模式,使用PSP,并恢复剩余寄存器(R0-R3, R12, LR, PC, xPSR)。当PC被恢复时,CPU就从任务B上次被挂起的地方继续执行,仿佛从未中断过。

4 总结

SysTick: 作为调度触发器,提供节拍,定期检查调度需求。
PendSV: 作为上下文搬运工,专职负责执行实际的寄存器保存与恢复,因其低优先级特性确保了切换的安全性。
SVC: 作为系统启动器,仅在启动第一个任务时使用,完成从内核模式到用户任务模式的权限和栈指针切换。
**MSP/PSP:**作为内存空间管理者,严格隔离内核与任务、任务与任务之间的运行环境。

启动: 启动函数 → (初始化) → SVC中断 → 恢复任务1现场 → 任务1运行。
**切换:**SysTick或API → (判断需切换) → 触发PendSV → 保存任务A现场 → 选择任务B → 恢复任务B现场 → 任务B运行。

**核心思想:**任务被切换出去时,其完整状态被冻结在自己的私有堆栈中;任务被切换进来时,其状态从堆栈中解冻,精准地恢复到CPU。通过pxCurrentTCB这个指针的指引和双堆栈指针的自动切换,FreeRTOS高效地管理着多个并发的执行流,实现了基于优先级的实时抢占式调度。

相关推荐
辰哥单片机设计2 小时前
STM32项目分享:智能热水器
stm32·单片机·嵌入式硬件
___波子 Pro Max.2 小时前
序列化 vs 反序列化
stm32·单片机·嵌入式硬件
来生硬件工程师3 小时前
【信号完整性与电源完整性分析】什么是信号完整性?什么是电源完整性?
笔记·stm32·单片机·嵌入式硬件·硬件工程
@good_good_study3 小时前
FreeRTOS信号量
stm32
Zeku3 小时前
借助通用驱动spidev实现SPI全双工通信
stm32·freertos·linux驱动开发·linux应用开发
单片机系统设计4 小时前
基于STM32的宠物智能喂食系统
c语言·stm32·单片机·嵌入式硬件·物联网·毕业设计·宠物
雾削木4 小时前
STM32 HAL DS1302时钟模块
stm32·单片机·嵌入式硬件
沪漂的码农5 小时前
FlexCAN寄存器完全解读
stm32·单片机·嵌入式硬件·can
申克Lab17 小时前
STM32 FreeRTOS 消息队列
java·stm32·嵌入式硬件