rtos任务切换与调度

1、STM32L的 LR 寄存器

  1. LR寄存器,即R14,全称为Link register,翻译过来就是链接寄存器,专门用于函数、中断返回使用。在函数调用开始时,LR寄存器的值就是PC的值(返回地址),是CPU自动计算的,不需要程序员去更新,可以简单的理解为当我们调用BL时,CPU就知道我们要执行函数调用操作了,它就会很贴心的更新LR值。函数执行完成后,当我们需要将压栈的LR出栈操作,可以直接将LR出栈到PC,此时PC就可以直接拿LR的值去执行,实现函数返回。所以在函数调用即返回过程中,LR是可以直接用来做返回地址的。
  2. 在中断发生时,CPU仍然会自动更新LR的值,只不过这里就不是将当前PC值更新到LR了,而是根据当前的模式、MSP/PSP使用等情况来查表生成一个EXC_RETURN值,将EXC_RETURN值更新到LR里。即此时的LR已经不是函数返回可以直接使用的PC值了。中断执行完毕后,我们会对LR进行出栈操作,出栈到PC,CPU发现PC的值是EXC_RETURN范围,就知道这是中断返回了,即此时的PC值并不会被真正执行,而只是一个提示而已,CPU会自动将进入中断时硬件压栈的栈帧数据进行出栈,出栈的信息里就包括了返回地址,将该地址,再次赋值给了PC。使得程序继续执行。
  3. 简单来说就是
  • 普通函数调用
    当执行 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 中的应用

  1. 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
    
  2. PSP(Process Stack Pointer)
    任务堆栈指针:用于用户任务(线程模式)的私有堆栈。
    动态切换:FreeRTOS 每个任务有自己的 PSP,任务切换时更新 PSP。

  3. FreeRTOS 中的堆栈管理
    特权级与用户级分离:内核代码运行在 Handler 模式,使用 MSP。用户任务运行在 Thread 模式,使用 PSP。
    任务上下文保存:任务切换时,将 R4-R11 手动保存到任务堆栈(通过 PSP),其余寄存器由硬件自动保存。要注意的是一旦开始任务调度就开始默认使用PSP,SVC异常中断是软件中断任务启动后就会使用PSP,但普通硬件中断时还会自动使用MSP;

3、FreeRTOS 任务切换实现分析

  1. 开始调度时,启动第一个任务: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 进入内核模式,启动第一个任务。


  2. 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 触发返回,开始执行任务代码

  3. 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、时间片调度与任务切换

  1. 时间片轮转机制

    SysTick 中断:FreeRTOS 使用 SysTick 定时器生成 1ms 时间片中断。

    中断处理流程:

    SysTick 中断触发,进入 xPortSysTickHandler。

    检查是否需切换任务(如时间片用完)。

    若需切换,触发 PendSV 异常,由 xPortPendSVHandler 执行上下文切换。

  2. 主动任务切换

    taskYIELD() 是 FreeRTOS 中用于主动触发任务切换的一个宏。任务可以在任何时候调用 taskYIELD() 来主动请求操作系统切换任务。调用 taskYIELD() 后,系统会立即执行任务切换,通常是触发一个 PendSV 中断来保存当前任务的上下文并恢复另一个任务的上下文。例如osDelay() 里面就有portYIELD() 注意#define taskYIELD() portYIELD()

posted @   lpajsj  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示