rtthread:context环境切换
rtthread中对于多线程切换是通过优先级表搭配优先级组进行调度的,优先级表中存储切换的上下线程节点,优先级组用来判断当前的最高优先级;
rtthread在优先级表调度中,将需要切换的线程结构体地址和sp指针作为参数传递给了context环境切换函数;
实际上下文切换是在context环境中,通过pendsv中断来配置的;
1 临界段保护
cm3内核配置了pendsv中断来实现上下线程的切换,而代码处于线程切换的临界段时需要保护线程切换不被打断;
cm3内核又配置了中断屏蔽寄存器组和对应的CPS指令用来快速开关所有中断,这个操作称之为临界段保护;
临界段保护同时也可以保护实时性要求高的代码段执行的时候不被打断;
1.1 内核中断屏蔽寄存器组
1.1.1 PRIMASK 寄存器: 1bit的寄存器,初始值为0;置1后可屏蔽除NMI、hard fault之外的所有中断响应;
1.1.2 FAULTMASK 寄存器: 1bit的寄存器,初始值为0;置1后可屏蔽除NMI之外的所有中断响应;
1.1.3 BASEPRI 寄存器: 9bit的寄存器,初始值为0;可屏蔽所有优先级大于它的的中断响应;值为0则不屏蔽;
1.2 CPS指令
1.2.1 CPSIE:Change Processor State Interrupt or abort enable;
CPSIE I ;PRIMASK=0,也是初始值;除NMI、hard fault之外处理器中断使能;
CPSIE F ;FAULTMASK=0,也是初始值;除NMI之外处理器异常使能;
1.2.2 CPSID:Change Processor State Interrupt or abort disable;
CPSID I ;PRIMASK=1,屏蔽中断;除NMI、hard fault之外处理器中断禁能;
CPSID F ;FAULTMASK=1,屏蔽中断;除NMI之外处理器异常禁能;
1.3 例程
rt_hw_interrupt_disable PROC
EXPORT rt_hw_interrupt_disable
MRS r0, PRIMASK
CPSID I
BX LR ;子函数返回,将pc还给主调函数;
ENDP
rt_hw_interrupt_enable PROC
EXPORT rt_hw_interrupt_enable
MSR PRIMASK, r0
BX LR
ENDP
;把PRIMASK作为参数传递,这样重新开中断的时候返回的状态是关中断之前的状态;
;把PRIMASK作为参数传递,是为了防止中断嵌套的时候外层中断还没执行完,PRIMASK就被内层中断清0了;
2 context环境切换
在scheduler调度中只是从优先级组和优先级表中取出了就绪线程,然后将就绪线程的sp指针的地址作为参数调用context环境切换函数;
在context环境切换函数中配置pendsv异常,然后触发pendsv异常,在pendsv中断函数中执行堆栈的保存与切换;
感觉没啥用还是忍不住提一句,这个"context环境切换"就是许多文章和博客里的"上下文切换";
rtthread中的环境切换函数和pendsv中断函数都是汇编函数,存储在context_rvds.s文件中,本节对context_rvds.s文件解构;
;***context_rvds.s文件***
import rt_thread_switch_interrupt_flag
import rt_interrupt_from_thread
import rt_interrupt_to_thread
;STM32F10xxx Cortex-M3 programming manual SCB系统控制块
SCB_VTOR equ 0xE000ED08 ; 向量表偏移寄存器
NVIC_INT_CTRL equ 0xE000ED04 ; 中断控制状态寄存器
NVIC_SYSPRI2 equ 0xE000ED20 ; 系统优先级寄存器(2)
NVIC_PENDSV_PRI equ 0x00FF0000 ; PendSV 优先级值 (lowest)
NVIC_PENDSVSET equ 0x10000000 ; 触发PendSV exception的值
area |.text|, code, readonly, align=2
thumb
require8
preserve8
;......此处省略多个汇编函数;
;......省略的汇编函数在后面部分;
align 4
end
//cpuport.c;两个sp指针相关变量,一个标志量;
//在context_switch函数中对这三个变量处理,然后在pendsv中使用他们push和pop堆栈;
rt_uint32_t rt_interrupt_from_thread;
rt_uint32_t rt_interrupt_to_thread;
rt_uint32_t rt_thread_switch_interrupt_flag;
2.1 中断函数
见1.3节;
2.2 rt_hw_context_switch_to函数
rt_hw_context_switch_to proc
export rt_hw_context_switch_to
ldr r1, =rt_interrupt_to_thread
str r0, [r1] ;&to_thread->sp放入rt_interrupt_to_thread;
;ldr [r1], r0 ;ldr和str都是寄存器放中间,地址放后面的,所以这样不行;
mov r0, #0x00
ldr r1, =rt_interrupt_from_thread
str r0, [r1] ;#0放入rt_interrupt_from_thread表示第一次初始化;
mov r0, #0x01
ldr r1, =rt_thread_switch_interrupt_flag
str r0, [r1] ;#1放入rt_thread_switch_interrupt_flag作为pendSV的中断标志
;配置pendSV的优先级,开启pendSV异常;原理见NVIC_SCB部分,先放着;
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] ; 读 .W的意思是将ldr指令强制转换成32bit地址宽度的thumb-2指令,搞不懂为什么要特地转化,先放着;
ORR r1,r1,r2 ; 改
STR r1, [r0] ; 写
; 触发 PendSV 异常 (产生上下文切换)
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
cpsie F
cpsie I ;不明白这里为啥要开两个中断,可能为了防止其中一个没清0导致中断没开;
endp
2.3 rt_hw_context_switch函数
这里有个疑问,这个rt_hw_context_switch函数已经有自己的标记符了,为什么还要标记成rt_hw_context_switch_interrupt?
为什么rt_schedule中还对这两个函数进行判断使用,这不是一个函数吗?先放着吧,懒得找答案;
rt_hw_context_switch_interrupt
EXPORT rt_hw_context_switch_interrupt
rt_hw_context_switch proc
export rt_hw_context_switch
ldr r2, =rt_thread_switch_interrupt_flag
ldr r3, [r2]
cmp r3, #0x01
beq _reswitch ;如果**_interrupt_flag为1,那么from_thread和interrupt_flag都是前面的switch_to函数配置的,就跳过不配置了;
;否则顺序执行,配置rt_thread_switch_interrupt_flag和rt_interrupt_from_thread;
mov r3, #0x01
str r3, [r2] ;如果不为1,则rt_thread_switch_interrupt_flag置1;
ldr r2, =rt_interrupt_from_thread
str r0, [r2]
_reswitch
ldr r2, =rt_interrupt_to_thread
str r1, [r2]
;触发PendSV异常,实现上下文切换;原理见NVIC_SCB部分,先放着;
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
bx lr
endp
2.4 PendSV_Handler函数
PendSV_Handler proc
export PendSV_Handler
msr r2, primask
cpsid I
ldr r0, =rt_thread_switch_interrupt_flag
ldr r1, [r0]
cbz r1, pendsv_exit ;中断标志位为0,不是中断退出;
mov r1, #0x00
str r1, [r0] ;中断标志位位1,那么清0中断标志位后继续;
ldr r0, =rt_interrupt_from_thread
ldr r1, [r0]
cbz r1, switch_to_thread ;如果是等于0表示第一遍启动,不需要保存from_thread的栈参数;跳过下面部分
;push from_thread
mrs r1, psp ;psp此时为栈底r0地址,栈底的前几个寄存器已经自动push了;
stmfd r1!, {r4-r11} ;将r11-r4依次存入栈中;此时r1为r4地址
ldr r0, [r0] ;r0里的数据为 &rt_interrupt_from_thread,
;[r0]为将r0里的数据作为地址,对地址内的数据操作,数据为&from_thread->sp
str r1, [r0] ;地址内的数据即from_thread->sp,该数据为sp指针值,即栈底r4地址;
;保存r4的地址到from_thread->sp指针的地址;这个指针本来也指向栈r4地址呀,为什么还要再保存一遍呢?
;可能这个sp指针在其他地方可能被更改,所以这里又给它初始化一下;初始化成r4地址;
;pop to_thread
switch_to_thread
ldr r0, =rt_interrupt_to_thread
ldr r0, [r0] ;地址内的数据为 &to_thread->sp
ldr r0, [r0] ;地址内的数据为 to_thread->sp,该指针内的数据为栈底r4地址
ldmfd r0!, {r4-r11} ;加载完后,此时r0内的数据为栈底r0地址;
msr psp, r0 ;为什么要将栈底r0的地址放入psp指针呢?
pendsv_exit
msr primask, r2
orr lr, lr,#0x04 ;lr的bit[2]为1,异常返回的是psp指针;
bx lr
endp
3 小结
线程进行切换,堆栈的保存实现是使用pendSV_Handler中断实现的,对地址内的地址内的地址进行操作的操作在此特别提一下;
那个psp是在context_rvds.s的下文切换的时候,把线程栈地址MSR给psp的,第二次pendSV时,上文保存的psp就已经是线程栈的psp了;
CPSR程序状态字寄存器,SCB系统控制单元寄存器,占坑;