参考 官方文档 https://www.rt-thread.org/document/site/programming-manual/porting/porting/
线程上下文切换等功能,一般采用汇编格式编写,不同cpu架构实现方式肯定不同,为了使rt-thread系统能够在不同的CPU架构上都能运行,RT-thread提供了一套libcpu抽象层来适配不同的cpu,现在我们重点来说libcpu中的rt_hw_context_switch函数和rt_hw_context_switch_interrupt函数在cortex-m3架构上的实现。
rt_hw_context_switch函数:在线程环境下,从当前线程切换到目标线程
rt_hw_context_switch_interrupt 函数:在中断环境下,从当前线程切换到目标线程
rt_hw_context_switch()函数,可以马上进行上下文切换,rt_hw_context_switch_interrupt 需要中断上下文函数执行完之后,才能进行上下文切换。
在cortex-m3架构的cpu中,这两个函数的实现是相同的,因为,此架构的上下文切换功能都是基于PendSV中断实现的(详见另一篇文章https://blog.csdn.net/xiaoyink/article/details/101688645)
cortex-m3架构 这两个函数的实现如下
;/*
; * void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);
; * r0 --> from
; * r1 --> to
; */
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch PROC
EXPORT rt_hw_context_switch
; 检查 rt_thread_switch_interrupt_flag 变量是否为 1
; 如果变量为 1 就跳过更新 from 线程的内容
LDR r2, =rt_thread_switch_interrupt_flag
LDR r3, [r2]
CMP r3, #1
BEQ _reswitch
; 设置 rt_thread_switch_interrupt_flag 变量为 1
MOV r3, #1
STR r3, [r2]
; 从参数 r0 里更新 rt_interrupt_from_thread 变量
LDR r2, =rt_interrupt_from_thread
STR r0, [r2]
_reswitch
; 从参数 r1 里更新 rt_interrupt_to_thread 变量
LDR r2, =rt_interrupt_to_thread
STR r1, [r2]
; 触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
流程图如下:
rt_thread_switch_interrupt_flag 变量是指示 上下文切换动作 是否在PendSV中断中执行过,如果等于1,表示需要切换,需要在PendSV中断中执行,也就是还没有执行,那么也就不需要再更新rt_interrupt_from_thread变量,(在上一次调用过rt_hw_context_switch/rt_hw_context_switch_interrupt 函数之后还没来得及切换线程,即rt_interrupt_from_thread变量保存的起始线程仍然有效,所以不需再保存),其他没有什么难以理解的点,直接看汇编代码的注释即可。
PendSV_Handler实现(即Pend中断处理函数)
; r0 --> switch from thread stack
; r1 --> switch to thread stack
; psr, pc, lr, r12, r3, r2, r1, r0 are pushed into [from] stack
PendSV_Handler PROC
EXPORT PendSV_Handler
; 关闭全局中断
MRS r2, PRIMASK
CPSID I
; 检查 rt_thread_switch_interrupt_flag 变量是否为 0
; 如果为零就跳转到 pendsv_exit
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
; 清零 rt_thread_switch_interrupt_flag 变量
MOV r1, #0x00
STR r1, [r0]
; 检查 rt_interrupt_from_thread变量
; 如果为 0,就不进行 from 线程的上下文保存
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread
; 保存 from 线程的上下文
MRS r1, psp ; 获取 from 线程的栈指针
STMFD r1!, {r4 - r11} ; 将 r4~r11 保存到线程的栈里
LDR r0, [r0]
STR r1, [r0] ; 更新线程的控制块的 SP 指针
switch_to_thread
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; 获取 to 线程的栈指针
LDMFD r1!, {r4 - r11} ; 从 to 线程的栈里恢复 to 线程的寄存器值
MSR psp, r1 ; 更新 r1 的值到 psp
pendsv_exit
; 恢复全局中断状态
MSR PRIMASK, r2
; 修改 lr 寄存器的 bit2,确保进程使用 PSP 堆栈指针
ORR lr, lr, #0x04
; 退出中断函数
BX lr
ENDP
注意,官网上以上代码段有一个地方注释有误
; 检查 rt_interrupt_from_thread变量(官网写的是rt_thread_switch_interrupt_flag )
; 如果为 0,就不进行 from 线程的上下文保存
检查rt_interrupt_from_thread变量是为了在系统第一次调用上下文切换时,即调用rt_hw_context_switch_to函数(详见官网,这里没有写此函数),此时没有from线程,所以将rt_interrupt_from_thread变量设置为空。
线程上下文切换是,上下文保存在各个线程的栈空间中, 这里我们只需要在PendSV中手动保存和恢复r4-r11寄存器,因为其它寄存器在中断发生和中断返回的时候会自动保存和自动恢复(r0-r3、r12、lr、pc和xpsr寄存器)。
另外一个细节:rt_hw_context_switch 函数以及PendSV_Handler中断处理函数引入了一个全局变量rt_thread_switch_interrupt_flag ,主要的作用我用 以下场景说明:
rt_hw_context_switch 函数准备好from线程的数据后(此时rt_hw_context_switch函数可能运行在用户级线程中,也可能运行在中断处理函数中,特别是实时时钟的中断处理函数中),还没来得及触发PendSV中断,就被其他中断打断,在其他中断中又调用了rt_hw_context_switch 函数进行线程切换,这时候就不需要重新准备from线程数据,只需要准备to线程数据,并悬起PendSV中断,然后中断处理函数返回
此时如果之前的rt_hw_context_switch 如果运行在用户级线程模式,则先不返回rt_hw_context_switch 函数,而是响应之前悬起的PendSV中断,PendSV中断被触发,完成线程切换后再返回rt_hw_context_switch 函数,rt_hw_context_switch函数接着运行,在准备好to线程后,又一次悬起了PendSV中断,PendSV中断之后又被触发,但是此时不会再执行线程切换了,因为rt_thread_switch_interrupt_flag 全局变量已经在上一次PendSV中断处理函数执行中被清零,PendSV中断服务函数将直接退出,避免无效且错误的线程切换。
这就是rt_thread_switch_interrupt_flag全局变量的一个应用场景,但是我观察rtthread系统的代码发现,rt_hw_context_switch函数都是在rt_schedule()函数中调用的,而rt_schedule()函数会在调用rt_hw_context_switch 函数之前关闭全局中断,所以上述我说的场景并不会发生,rt_hw_context_switch函数执行一半的时候并不会被打断,所以实际上rt_thread_switch_interrupt_flag并没有发挥作用,但是实现的时候,还是加入了上述机制,注意,以上分析只是个人观点,完全有可能是错的。