rtos任务切换与调度
1、STM32L的 LR 寄存器
- LR寄存器,即R14,全称为Link register,翻译过来就是链接寄存器,专门用于函数、中断返回使用。在函数调用开始时,LR寄存器的值就是PC的值(返回地址),是CPU自动计算的,不需要程序员去更新,可以简单的理解为当我们调用BL时,CPU就知道我们要执行函数调用操作了,它就会很贴心的更新LR值。函数执行完成后,当我们需要将压栈的LR出栈操作,可以直接将LR出栈到PC,此时PC就可以直接拿LR的值去执行,实现函数返回。所以在函数调用即返回过程中,LR是可以直接用来做返回地址的。
- 在中断发生时,CPU仍然会自动更新LR的值,只不过这里就不是将当前PC值更新到LR了,而是根据当前的模式、MSP/PSP使用等情况来查表生成一个EXC_RETURN值,将EXC_RETURN值更新到LR里。即此时的LR已经不是函数返回可以直接使用的PC值了。中断执行完毕后,我们会对LR进行出栈操作,出栈到PC,CPU发现PC的值是EXC_RETURN范围,就知道这是中断返回了,即此时的PC值并不会被真正执行,而只是一个提示而已,CPU会自动将进入中断时硬件压栈的栈帧数据进行出栈,出栈的信息里就包括了返回地址,将该地址,再次赋值给了PC。使得程序继续执行。
- 简单来说就是
-
普通函数调用
当执行 BL(Branch with Link)指令时,LR 自动保存下一条指令的地址(即返回地址)。
函数返回时通过 BX LR 或 POP {PC} 跳转回调用点。 -
异常/中断处理
进入异常时:硬件自动将返回地址(PC)和其他状态(如 xPSR)压入当前堆栈(MSP 或 PSP),并将 LR 设置为特殊值(如 0xFFFFFFF9),表示返回时使用 MSP 还是 PSP。
退出异常时:根据 LR 的值确定返回模式和堆栈指针,也就是EXC_RETURN:
LR = 0xFFFFFFF1 → 返回 Handler 模式,继续使用 MSP。
LR = 0xFFFFFFF9 → 返回 Thread 模式,使用 MSP。
LR = 0xFFFFFFFD → 返回 Thread 模式,使用 PSP(FreeRTOS 任务使用的模式)。
2、MSP 与 PSP 的区别及 FreeRTOS 中的应用
-
MSP(Main Stack Pointer)
默认堆栈指针:用于异常处理(如中断、SVC)和特权级代码。
静态分配:在系统启动时由启动文件初始化(如 _estack 符号)。在链接文件中有
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of "RAM" Ram type memory */
启动文件中_estack的值在0x08000000位置
**g_pfnVectors:** **.word** _estack **.word** Reset_Handler **.word** NMI_Handler **.word** HardFault_Handler **.word** MemManage_Handler **.word** BusFault_Handler **.word** UsageFault_Handler
-
PSP(Process Stack Pointer)
任务堆栈指针:用于用户任务(线程模式)的私有堆栈。
动态切换:FreeRTOS 每个任务有自己的 PSP,任务切换时更新 PSP。 -
FreeRTOS 中的堆栈管理
特权级与用户级分离:内核代码运行在 Handler 模式,使用 MSP。用户任务运行在 Thread 模式,使用 PSP。
任务上下文保存:任务切换时,将 R4-R11 手动保存到任务堆栈(通过 PSP),其余寄存器由硬件自动保存。要注意的是一旦开始任务调度就开始默认使用PSP,SVC异常中断是软件中断任务启动后就会使用PSP,但普通硬件中断时还会自动使用MSP;
3、FreeRTOS 任务切换实现分析
-
开始调度时,启动第一个任务:prvPortStartFirstTask
static void prvPortStartFirstTask( void ) { /* 启动第一个任务。此操作还清除了指示FPU(浮点单元)正在使用的位,以防在调度器启动之前使用了FPU, 如果不清除该位,会导致在SVC栈中多留出空间用于懒保存FPU寄存器。 */ __asm volatile( " ldr r0, =0xE000ED08 \n" /* 加载 VTOR 寄存器地址(存储向量表基址。 */ " ldr r0, [r0] \n" /* 读取该寄存器的值,获取向量表的基地址。 */ " ldr r0, [r0] \n" /* 再次读取,得到存储在向量表中的主栈指针(MSP)值。 */ " msr msp, r0 \n" /* 将MSP寄存器设置为从向量表中读取到的栈顶地址。 */ " mov r0, #0 \n" /* 清除指示FPU正在使用的位,确保不会为FPU寄存器保存预留空间。 */ " msr control, r0 \n" /* 设置CONTROL寄存器,清除FPU使用标志,同时切换到特权模式。 */ " cpsie i \n" /* 全局启用IRQ(中断)。通过设置'i'位,允许IRQ中断。 */ " cpsie f \n" /* 全局启用FPU中断。通过设置'f'位,允许FPU中断。 */ " dsb \n" /* 数据同步屏障(DSB):确保所有之前的指令完成后才继续执行。 */ " isb \n" /* 指令同步屏障(ISB):确保指令管线被刷新,且正确获取新指令。 */ " svc 0 \n" /* 触发系统服务调用(SVC),开始第一个任务的调度。此时将引发上下文切换。 */ " nop \n" /* 空操作:此指令不做任何事,通常用于占位或填补指令槽。 */ ); }
初始化 MSP:从向量表(0xE000ED08 为 VTOR 寄存器地址)获取主堆栈初始值,也就是0x0800000的值。
正常上电时默认使用MSP,在程序执行到这里时栈顶地址早以不是这个值,这里使用
msr msp, r0
,重新设置为初始主堆栈地址,是因为下一步就要在SVC开启任务调度,并开始使用PSP任务指针,原本MSP主堆栈中的数据也不会再用到了;
我在仿真测试时在执行这个函数 prvPortStartFirstTask前 sp值是0x2001ffa0 在进入SVC中断函数时变为了 0x2001ffe0 0x20020000-0x2001ffe0 =32
进入 SVC 中断时,处理器会自动将以下寄存器压入 MSP 栈:
程序状态寄存器(xPSR)
返回地址(PC)
链接寄存器(LR)
寄存器 R0-R3, R12如果使用 FPU,还会保存 FPU 上下文。
栈消耗量约为 32字节(无 FPU)
或 72字节(有 FPU,这里不确定)。触发 SVC 异常:通过 svc 0 进入内核模式,启动第一个任务。
-
SVC 中断服务函数:vPortSVCHandler
void vPortSVCHandler( void ) { __asm volatile ( " ldr r3, pxCurrentTCBConst2 \n" // 加载 pxCurrentTCB 的地址到 r3 " ldr r1, [r3] \n" // 获取当前 TCB 的地址(r1 = pxCurrentTCB) " ldr r0, [r1] \n" // 获取任务的栈顶指针(r0 = pxTopOfStack) " ldmia r0!, {r4-r11, r14}\n" // 从栈中恢复寄存器 r4-r11 和 LR(EXC_RETURN) " msr psp, r0 \n" // 设置 PSP 为任务栈顶 " isb \n" " mov r0, #0 \n" " msr basepri, r0 \n" // 解除中断屏蔽(BASEPRI = 0) " bx r14 \n" // 使用 EXC_RETURN 返回到线程模式(PSP 栈) " .align 4 \n" "pxCurrentTCBConst2: .word pxCurrentTCB \n" ); }
恢复任务上下文:从任务堆栈中恢复 R4-R11(其余寄存器由硬件自动恢复)。
设置 PSP:将任务堆栈指针赋值给 PSP。
配置返回模式:通过修改 LR 为 0xFFFFFFFD,确保退出异常后使用 Thread 模式(线程模式)。
使用 PSP 作为堆栈指针。
跳转执行:BX LR 触发返回,开始执行任务代码
-
PENDSV中断服务函数、
任务正常开始调度后,后续在PENDSV中断去切换任务,这里如果是从任务中正常启动了任务调度,使用PSP,自动将R0-R3 xPSR PC LR R12压入堆栈;
void xPortPendSVHandler( void ) { __asm volatile ( //应该是有自动的R0-R3 xPSR PC LR R12入栈 " mrs r0, psp \n" // 获取当前任务的 PSP 到 r0 " isb \n" //指令同步屏障 " ldr r3, pxCurrentTCBConst \n" // 加载 pxCurrentTCB 地址到 r3 " ldr r2, [r3] \n" // 获取当前 TCB 地址(r2 = pxCurrentTCB) " tst r14, #0x10 \n" // 检查 LR 的 bit4(是否需保存 FPU 寄存器) " it eq \n" //条件执行:如果 bit4=0(使用 FPU) " vstmdbeq r0!, {s16-s31} \n" // 若使用 FPU,保存高部分 FPU 寄存器 " stmdb r0!, {r4-r11, r14}\n" // 保存 r4-r11 和 LR(EXC_RETURN) " str r0, [r2] \n" // 更新 TCB 中的栈顶指针 " stmdb sp!, {r0, r3} \n" //此时r0是当前任务的栈指针 r3是pxCurrentTCB的地址,将 r0 和 r3 压入 MSP 栈(临时保存) " mov r0, %0 \n" //将 configMAX_SYSCALL_INTERRUPT_PRIORITY 加载到 r0 " msr basepri, r0 \n" // 屏蔽低优先级中断(configMAX_SYSCALL_INTERRUPT_PRIORITY) " dsb \n" " isb \n" " bl vTaskSwitchContext \n" // 调用调度器选择新任务 " mov r0, #0 \n" " msr basepri, r0 \n" // 解除中断屏蔽 " ldmia sp!, {r0, r3} \n" //从 MSP 栈恢复 r0 和 r3 ,vTaskSwitchContext中pxCurrentTCB指针的值已经被改变,变为了下一个控制块的地址 " ldr r1, [r3] \n" // 获取新任务的 TCB 地址 " ldr r0, [r1] \n" // 获取新任务的栈顶指针 " ldmia r0!, {r4-r11, r14}\n" // 恢复新任务的寄存器 r4-r11 和 LR " tst r14, #0x10 \n" // 检查是否需要恢复 FPU 寄存器 " it eq \n" " vldmiaeq r0!, {s16-s31} \n" // 恢复 FPU 高寄存器 " msr psp, r0 \n" // 更新 PSP 为新任务的栈顶 " isb \n" " bx r14 \n" // 返回到新任务(使用 EXC_RETURN) R14与LR是同一个 // 应该是有自动的R0-R3 xPSR PC LR R12出栈 " .align 4 \n" "pxCurrentTCBConst: .word pxCurrentTCB \n" //得到pxCurrentTCB的地址 ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) //立即数 configMAX_SYSCALL_INTERRUPT_PRIORITY ); }
4、时间片调度与任务切换
-
时间片轮转机制
SysTick 中断:FreeRTOS 使用 SysTick 定时器生成 1ms 时间片中断。
中断处理流程:
SysTick 中断触发,进入 xPortSysTickHandler。
检查是否需切换任务(如时间片用完)。
若需切换,触发 PendSV 异常,由 xPortPendSVHandler 执行上下文切换。
-
主动任务切换
taskYIELD()
是 FreeRTOS 中用于主动触发任务切换的一个宏。任务可以在任何时候调用taskYIELD()
来主动请求操作系统切换任务。调用taskYIELD()
后,系统会立即执行任务切换,通常是触发一个PendSV
中断来保存当前任务的上下文并恢复另一个任务的上下文。例如osDelay()
里面就有portYIELD()
注意#define taskYIELD() portYIELD()
。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构