RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
1 相关API
函数和变量 | 描述 |
---|---|
void rt_hw_context_switch_to(rt_uint32_t to); | 没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用 |
void rt_hw_context_switch(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于线程和线程之间的切换 |
void rt_hw_context_switch_interrupt(rt_uint32_t from, rt_uint32_t to); | 从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用 |
rt_uint32_t rt_thread_switch_interrupt_flag; | 表示需要在中断里进行切换的标志 |
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread; | 在线程进行上下文切换时候,用来保存 from 和 to 线程的栈顶指针的指针(即保存&thread->sp ) |
2 实现上下文切换
2.1 为什么使用PendSV
完成上下文切换
Cortex-M的上下文切换由PendSV
异常完成,为什么不用systick
或其他异常,解释如下:
上图为两个任务轮转调度,但当产生Systick
异常时正在响应一个中断,则Systick
异常会抢占其中断服务例程IRQ,若在此时OS进行上下文切换,会导致中断请求被延时,这在实时系统是不允许出现的。对于M3/4,当存在活跃的异常时,设计默认不允许返回到线程模式(非基级的线程模式
例外),否则触发Usage fault。
PendSV
异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行(编程为最低优先级的异常)。如果OS检测到某IRQ正在活动并且被SysTick
抢占,它将悬起一个PendSV
异常,以便缓期执行上下文切换:
- 任务A呼叫SVC请求任务切换
- OS收到请求做好上下文切换准备,并悬起PendSV异常
- 当CPU退出SVC后,立即进入PendSV进行上下文切换
- 当PendSV执行完后,然后任务B,切换到线程模式
- 中断发生,ISR开始执行
- ISR执行过程中,发生systick异常,抢占了该ISR
- OS执行必要操作,然后悬起PendSV异常以做好上下文切换的准备
- 当systick退出后,被抢占的ISR继续执行
- ISR执行完毕,PendSV服务例程开始执行,并在其中进行上下文切换
- PendSV执行完毕,回到任务A,切换到线程模式
2.2 Cortex-M 的上下文切换
在 ARM9 等平台,rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 的实现并不一样,在IRQ里如果触发了线程的调度,调度函数里会调用 rt_hw_context_switch_interrupt() 触发上下文切换。IRQ里处理完中断事务之后,中断退出之前,检查 rt_thread_switch_interrupt_flag 变量,如果该变量的值为 1,就根据 rt_interrupt_from_thread 变量和 rt_interrupt_to_thread 变量,完成线程的上下文切换。
在 Cortex-M 处理器架构里,基于自动部分压栈和 PendSV
的特性,上下文切换可以实现地更加简洁,并且允许中断嵌套。
因此,在线程和中断里进行上下文切换没有区别, rt_hw_context_switch() 和 rt_hw_context_switch_interrupt() 实现一致。
- 线程环境下的上下文切换
- 中断环境下的上下文切换
硬件在进入 PendSV 中断之前自动保存了 from 线程的 PSR、PC、LR、R12、R3-R0 寄存器,然后 PendSV 里保存 from 线程的 R11~R4 寄存器,以及恢复 to 线程的 R4~R11 寄存器,最后硬件在退出 PendSV 中断之后,自动恢复 to 线程的 R0~R3、R12、LR、PC、PSR 寄存器。
2.2.1 rt_hw_context_switch_to()
rt_hw_context_switch_to() 只有目标线程,没有来源线程。只在第一次启动时在rt_system_scheduler_start()
中调用。
;/*
; * void rt_hw_context_switch_to(rt_uint32 to);
; * r0 --> to
; * 该函数用于开启第一次线程切换
; */
rt_hw_context_switch_to PROC
EXPORT rt_hw_context_switch_to
; set to thread
LDR r1, =rt_interrupt_to_thread ; 设置目的线程栈顶地址
STR r0, [r1] ; rt_interrupt_to_thread = &to_thread->sp 即线程栈顶地址sp的指针
; 设置 from 线程为0,表示不需要从保存 from 的上下文
LDR r1, =rt_interrupt_from_thread ; 第一次启动没有源线程,栈顶地址的指针初始化为0
MOV r0, #0x0
STR r0, [r1] ; rt_interrupt_from_thread = 0
; 设置标志为 1,表示需要切换,这个变量将在 PendSV 异常处理函数里切换的时被清零
LDR r1, =rt_thread_switch_interrupt_flag
MOV r0, #1
STR r0, [r1] ; rt_thread_switch_interrupt_flag = 1
; 设置 PendSV 异常优先级为最低优先级
LDR r0, =NVIC_SYSPRI2
LDR r1, =NVIC_PENDSV_PRI
LDR.W r2, [r0,#0x00] ; r2 = *NVIC_SYSPRI2
ORR r1,r1,r2 ; r1 |= r2
STR r1, [r0] ; *r0 = *NVIC_SYSPRI2 = r1
; 触发 PendSV 异常 (将执行 PendSV 异常处理程序)
LDR r0, =NVIC_INT_CTRL
LDR r1, =NVIC_PENDSVSET
STR r1, [r0] ; SCB_ICSR寄存器写0无效, 因为可以直接赋值
; 放弃芯片启动到第一次上下文切换之前的栈内容,将 MSP 设置启动时的值
; LDR r0, =SCB_VTOR ; 读取向量表相对基址的偏移字节
; LDR r0, [r0]
; LDR r0, [r0]
; MSR msp, r0
; 使能全局中断和全局异常,使能之后将进入 PendSV 异常处理函数
CPSIE F
CPSIE I
; 不会执行到这里
ENDP
rt_hw_context_switch_to函数主要功能:
- 获取to线程栈顶指针的指针,以便直接修改它们的值
- 将rt_interrupt_from_thread 置 0,表示不需要从保存 from 线程的上下文
- rt_thread_switch_interrupt_flag 置 1
- 设置
PendSV
异常优先级为最低优先级 - 触发
PendSV
异常
注意:rt_interrupt_to_thread/rt_interrupt_from_thread的的值是一个指针(地址),即&thread->sp
。
2.2.2 rt_hw_context_switch()/ rt_hw_context_switch_interrupt()
实现从 from 线程切换到 to 线程的功能。
;/*
; * 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
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] ; rt_thread_switch_interrupt_flag = 1
LDR r2, =rt_interrupt_from_thread ; set rt_interrupt_from_thread
STR r0, [r2] ; rt_interrupt_from_thread = &from_thread->sp
_reswitch
LDR r2, =rt_interrupt_to_thread ; set rt_interrupt_to_thread
STR r1, [r2] ; rt_interrupt_to_thread = &to_thread->sp
; 触发 PendSV 异常,将进入 PendSV 异常处理函数里完成上下文切换
LDR r0, =NVIC_INT_CTRL ; 触发PendSV 异常,实现上下文切换
LDR r1, =NVIC_PENDSVSET
STR r1, [r0]
BX LR
ENDP
rt_hw_context_switch/rt_hw_context_switch_interrupt函数主要功能:
- 更新from/to线程栈顶指针的指针,以便直接修改它们的值(rt_thread_switch_interrupt_flag 为0,则不会更新from线程)
- rt_thread_switch_interrupt_flag = 1
- 触发
PendSV
异常
注意:在调用rt_hw_context_switch_xx
时要用中断锁保护,以防被打断。
2.2.3 PendSV服务例程中实现真正的上下文切换
; 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
; 判断r1是否为0,为0则跳转到pendsv_exit, 表示PendSV已处理
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit
; 清零 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 ; 源线程sp的指针若为0(第一次启动为0),则跳过寄存器第一次保存现场
; 保存from源线程的现场: r4 - r11
MRS r1, psp ; get from thread stack pointer
STMFD r1!, {r4 - r11} ; push r4 - r11 register
LDR r0, [r0] ; r0 = &rt_interrupt_from_thread
STR r1, [r0] ; update from thread stack pointer
; 恢复to目标线程的现场,并更新当前psp为目标线程的栈指针
switch_to_thread
LDR r1, =rt_interrupt_to_thread ; rt_interrupt_to_thread全局变量保存的是thread sp的地址
LDR r1, [r1]
LDR r1, [r1] ; load thread stack pointer
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
MSR psp, r1 ; update stack pointer
pendsv_exit
; 恢复全局中断状态
MSR PRIMASK, r2
; 修改 lr 寄存器的 bit2,确保进程使用 PSP 堆栈指针
ORR lr, lr, #0x04 ; lr = EXC_RETURN, 第二位置1表示返回线程模式后使用PSP
BX lr ; psr, pc, lr, r12, r3, r2, r1, r0 will pop from [to_thread_stack]
ENDP
PendSV_Handler
主要功能:
- 进入ESR前,硬件将psr, pc, lr, r12, r3, r2, r1, r0 压入 [from] 线程栈中
- 进入ESR中,若rt_thread_switch_interrupt_flag为0则退出,反之将其清零
- 进入ESR中,软件将r4 - r11 压入 [from] 线程栈中
- 进入ESR中,更新当前psp为to线程的栈指针
- 进入ESR中,软件将 [to] 线程栈中的保存的r4 - r11 弹出给寄存器
- 进入ESR中,将lr =
EXC_RETURN
的 第二位置1表示返回线程模式后使用PSP - 退出ESR,硬件将[to] 线程栈中的保存的psr, pc, lr, r12, r3, r2, r1, r0弹出给寄存器,跳转到pc所在地址继续取指执行。
关于rt_thread_switch_interrupt_flag的作用:
-
线程环境中,主动进行任务调度,在
rt_schedule
中调用rt_hw_context_switch()
函数后,此时已经将from/to线程的栈顶指针地址保存到rt_interrupt_from_thread/rt_interrupt_to_thread变量中,并且将rt_thread_switch_interrupt_flag置1,然后触发PendSV异常。但在中断屏蔽期间,若又悬起了systick异常,中断使能后,会先跳转到systick的ESR中执行。在systick中会检查当前线程的时间片与系统定时器,若当前线程时间片已用完或线程定时器超时,则会调用
rt_schedule
从优先级链表中获取当前最高优先级的线程,此时to_thread
可能会改变,但是from_thread
不会改变,最后调用rt_hw_context_switch_interrupt()
函数(两者实现一样),此时rt_thread_switch_interrupt_flag已为1,故不会更新rt_interrupt_from_thread,同时也不会再次将rt_thread_switch_interrupt_flag置1。 -
PendSV_Handler
中的rt_thread_switch_interrupt_flag作用个人猜测,可能是在没有调用rt_hw_context_switch/rt_hw_context_switch_interrupt时,但意外触发了PendSV,此时会退出,无需切换。
END