RT-Thread线程的首次切换源码剖析
1. rt_system_scheduler_start()
从RT-Thread的启动流程框图中可以看到,系统初始化的最后一步,是在准备第一次调度。
1.1 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_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.5. 触发PendSV异常
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
手动触发PendSV异常,这会导致处理器跳转到PendSV异常处理函数进行真正的上下文切换。
2.2.6. 恢复主堆栈指针
LDR r0, =SCB_VTOR
LDR r0, [r0]
LDR r0, [r0]
MSR msp, r0
这段汇编的意思是从中断向量表的第一个条目(复位时的初始堆栈指针)恢复主堆栈指针(MSP)。
2.2.7. 开启中断
CPSIE F ;使能异常
CPSIE I ;使能中断
开启中断与异常,准备进入PendSV_Handler。
2.2.8. 进入PendSV_Handler
2.2.8.1. 失能中断
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的位置(即硬件自动保存部分的起始地址)。
拓展:
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 还存储优先级、状态、名称等信息。
浙公网安备 33010602011771号