SXSBJSXYT

保持热爱,奔赴山海

 

RT-Thread线程的首次切换源码剖析

1. rt_system_scheduler_start()

图1.1-RT-Thread的启动流程

        从RT-Thread的启动流程框图中可以看到,系统初始化的最后一步,是在准备第一次调度。

1.1 rt_system_scheduler_start源码

图1.2-rt_system_scheduler_start源码

        rt_system_scheduler_start函数的内容主要是从就绪列表中取出优先级最高的TCB(线程控制块),然后将TCB的sp传入rt_hw_context_switch_to函数中,准备线程切换。

 拓展:为什么往rt_hw_context_switch_to中入sp?

        线程切换的本质是 保存当前线程的现场,并 恢复目标线程的现场。而线程的“现场”(即上下文)全部保存在其栈中,切换 sp 即切换了线程的执行环境,因此切换的核心就是 切换栈指针

2. rt_hw_context_switch_to()

图2.1-rt_system_scheduler_start()源码
图2.1-rt_hw_context_switch_to源码

2.1函数功能

  rt_hw_context_switch_to 用于执行系统中的第一次线程切换,通常在操作系统启动时调用。

2.2 代码逐步分析

2.2.1. 设置目标线程

LDR     r1, =rt_interrupt_to_thread
STR     r0, [r1]

        1. 将rt_interrupt_to_thread的地址加载到r1寄存器;(在ARM汇编中,这里的 = 表示 获取 rt_interrupt_to_thread 变量的地址

        2. 将r0寄存器中的值写到r1寄存器所保存的地址中去。既rt_interrupt_to_thread全局变量的值等于0x20000120。

2.2.2. 清除源线程

LDR     r1, =rt_interrupt_from_thread
MOV     r0, #0x0
STR     r0, [r1]

        将 rt_interrupt_from_thread 的值设置为0,表示这是第一次切换,没有源线程。

2.2.3. 设置切换标志

LDR     r1, =rt_thread_switch_interrupt_flag
MOV     r0, #1
STR     r0, [r1]

        设置rt_thread_switch_interrupt_flag(线程切换中断标志)为1,通知系统需要进行线程切换。

2.2.4. 配置PendSV优先级

LDR     r0, =NVIC_SYSPRI2
LDR     r1, =NVIC_PENDSV_PRI
LDR.W   r2, [r0,#0x00]
ORR     r1,r1,r2
STR     r1, [r0]

        这部分设置PendSV异常的优先级。PendSV通常设置为最低优先级,确保在所有其他中断处理完成后再进行任务切换。

拓展1:关于LDR.W指令

拓展2:为什么要把PendSV的优先级设置为最低? 

        PendSV异常会自动延迟上下文切换的请求,直到其它的ISR(中断服务程序)都处理完后再执行,为了实现这个机制,需要把PendSV的优先级设置为最低。

图2.2-PendSV控制上下文切换流程图
图2.3-PendSV控制上下文切换流程解释

2.2.5. 触发PendSV异常

LDR     r0, =NVIC_INT_CTRL
LDR     r1, =NVIC_PENDSVSET
STR     r1, [r0]

        手动触发PendSV异常,这会导致处理器跳转到PendSV异常处理函数进行真正的上下文切换。 

图2.4-将NVIC_INT_CTRL的值加载到R0寄存器
图2.5-将NVIC_PENDSVSET的值加载到R1寄存器

2.2.6. 恢复主堆栈指针

LDR     r0, =SCB_VTOR
LDR     r0, [r0]
LDR     r0, [r0]
MSR     msp, r0

       这段汇编的意思是从中断向量表的第一个条目(复位时的初始堆栈指针)恢复主堆栈指针(MSP)。

图2.6-将SCB_VTOR的值加载到R0寄存器
图2.7-获取向量表的基地址(flash启动,0x08000000)
图2.8-读取基地址处的值(即初始MSP值)
图2.9-将初始栈顶值写入MSP寄存器

2.2.7. 开启中断

CPSIE   F    ;使能异常
CPSIE   I    ;使能中断

        开启中断与异常,准备进入PendSV_Handler。

图2.10-开关中断/异常指令(CM3权威手册)
图2.11-屏蔽寄存器组(CM3权威手册)

 2.2.8. 进入PendSV_Handler

图2.12-PendSV_Handler源码
  2.2.8.1.  失能中断
图2.13-关闭中断
   2.2.8.2.  获取rt_thread_switch_interrupt_flag
    ; get rt_thread_switch_interrupt_flag
    LDR     r0, =rt_thread_switch_interrupt_flag
    LDR     r1, [r0]
    CBZ     r1, pendsv_exit         ; pendsv already handled

    ; clear rt_thread_switch_interrupt_flag to 0
    MOV     r1, #0x00
    STR     r1, [r0]

         这里解释一下CBZ     r1, pendsv_exit,这句汇编的意思是,r1的值如果为0就跳转到pendsv_exit语句执行,类似于C语言的goto。显然这里不会跳转,因为在2.2.3.设置切换标志的时候已经将rt_thread_switch_interrupt_flag置1。然后将rt_thread_switch_interrupt_flag置0,清除标志位。

拓展:

   2.2.8.3.  跳转switch_to_thread
    LDR     r0, =rt_interrupt_from_thread
    LDR     r1, [r0]
    CBZ     r1, switch_to_thread    ; skip register save at the first time

    MRS     r1, psp                 ; get from thread stack pointer
    STMFD   r1!, {r4 - r11}         ; push r4 - r11 register
    LDR     r0, [r0]
    STR     r1, [r0]                ; update from thread stack pointer

         这里第三行汇编会判断rt_interrupt_from_thread的值是否为0,为0则会跳转到switch_to_thread语句。系统首次启动调度,切换线程,已经在2.2.2. 清除源线程中将rt_interrupt_from_thread的值置0了。所以接下来程序会直接执行switch_to_thread语句。

 2.2.8.4.  switch_to_thread函数
switch_to_thread
    LDR     r1, =rt_interrupt_to_thread
    LDR     r1, [r1]
    LDR     r1, [r1]                ; load thread stack pointer

    LDMFD   r1!, {r4 - r11}         ; pop r4 - r11 register
    MSR     psp, r1                 ; update stack pointer

        这段汇编,先把rt_interrupt_to_thread的地址加载到r1,然后把rt_interrupt_to_thread的值加载到r1寄存器,rt_interrupt_to_thread 是一个全局变量,存储 即将切换到的线程的TCB(Thread Control Block)地址,TCB 的第一个字段通常存储的是该线程的 栈指针(SP),所以 LDR r1, [r1] 两次解引用,最终得到线程的栈顶地址。

        LDMFD   r1!, {r4 - r11}这句汇编表示从栈中弹出r4~r11寄存器,同时栈指针r1自动更新(“!”表示回写)。在 ARM Cortex-M 架构中,硬件自动保存部分寄存器(R0-R3, R12, LR, PC, xPSR),而 R4-R11 需要手动保存/恢复(这就是为什么这里只恢复这些寄存器)。

| High Address |
|--------------|
|      xPSR    |  <-- 程序状态寄存器
|      PC      |  <-- 入口函数(任务恢复后执行的指令)
|      LR      |  <-- 返回位置
|      R12     |
|      R3      |
|      R2      |
|      R1      |
|      R0      |  <-- 线程入口参数
|--------------|
|      R11     |  <-- 手动保存的寄存器
|      R10     |
|      ...     |
|      R4      |  <-- `LDMFD r1!, {r4-r11}` 从这里恢复
| Low Address  |  <-- 栈顶(初始 `r1` 指向这里)

        最后,MSR psp, r1 将 r1(当前线程的栈顶)写入 PSP(Process Stack Pointer),这样后续的 POP 操作会从该栈继续恢复剩余寄存器(如 PC、LR 等)。切记,r1现在指向目标任务栈中R0的位置(即硬件自动保存部分的起始地址)

图2.14-入栈操作(CM3权威手册)
图2.15-弹栈恢复R4~R11寄存器
图2.16-更新线程栈顶指针

拓展:

MRS/MSR特殊指令:

 

2.2.8.5. 返回线程模式,使用线程堆栈
pendsv_exit
    ; restore interrupt
    MSR     PRIMASK, r2

    ORR     lr, lr, #0x04
    BX      lr
    ENDP

        这段汇编先是恢复了中断, 然后修改 LR(EXC_RETURN 值),强制 CPU 在异常返回时使用 进程栈指针(PSP) 而非主栈指针(MSP)。最后跳转到lr寄存器指向的地址。

 2.2.9. 退出PendSV_Handler

        在 PendSV 处理程序中,已通过 MSR psp, r1将 PSP 设置为目标任务的栈顶。当执行2.2.8.5.返回线程模式中的BX lr时硬件会自动将之前自动压入PSP栈中的寄存器弹出,然后切换栈指针,根据EXC_RETURN 的 bit 2 决定使用哪个栈指针,然后切换模式,从 Handler 模式(异常处理)退出,回到 Thread 模式(普通任务执行)。最后跳转到PC指向的地址(PC 的值是目标任务被切换前保存的下一条指令地址),从而继续执行。

3.线程首次切换总结

4. 常见问题

问题1: 中断切换时 sp 会被修改吗?

        不会,中断(如 SysTick)使用 MSP(主栈指针),而任务线程使用 PSP(进程栈指针),两者独立。

问题2: 为什么不直接传递栈地址,而是通过 TCB?

        TCB 是线程的管理中心,除了 sp,TCB 还存储优先级、状态、名称等信息。

posted on 2025-06-08 12:48  SXSBJSXYT  阅读(13)  评论(0)    收藏  举报  来源

导航