??本文的目的是分析RT-Thread的底层汇编,以Cortex-M3内核为例分析,在RT-Thread的kernel源码中,这部分内容属于libcpu,在libcpu中,有不同cpu架构的底层实现,如riscv、arm等。kernel中的这部分代码负责线程堆栈的初始化、上下文切换(也可以叫线程切换)、HardFault异常处理、全局中断的控制。掌握这部分代码需要理解三个中断,一个过程。三个中断指的是全局中断、PendSV中断、HardFault中断。一个过程指的是线程切换的过程。读完文章后,你一定对RT-Thread底层做的那些与CPU架构相关的内容有了了解,之后根据不同架构的指令集按照三个中断、一个过程的分析方法都能分析和理解,可以将RT-Thread移植到任意内核的cpu上。后面的内容就是按照三个中断、一个过程进行展开。
一、内核寄存器
??cortex-m3的内核寄存器如上图。R0-R15是通用寄存器,其中的R13-R15都有各自的含义,SP是堆栈指针。每个线程都有自己独立的堆栈,LR是返回寄存器,保存返回的地址,PC是程序计数寄存器,保存下一条需要执行的指令。最下面的那五个特殊功能寄存器在中断和异常中有作用。后面用到的时候会加以说明。
二、三个中断
2.1 第一个关键中断:全局中断
.global rt_hw_interrupt_disable
.type rt_hw_interrupt_disable, %function
rt_hw_interrupt_disable:
MRS R0, PRIMASK
CPSID I
BX LR
.global rt_hw_interrupt_enable
.type rt_hw_interrupt_enable, %function
rt_hw_interrupt_enable:
MSR PRIMASK, R0
BX LR
内核寄存器说明
PRIMASK(Priority Mask Register 优先级掩码寄存器) 寄存器作用:PRIMASK寄存器屏蔽具有可配置优先级的所有异常的激活,关闭除了HMI异常外的所有中断。从内核寄存器的架构可以看到PRIMASK属于特殊功能寄存器,可以通过指令MRS和MSR操作。
指令说明
MRS:Rd, 读取特殊功能寄存器的值到通用寄存器 MSR:,Rn, 把通用寄存器的值写入特殊功能寄存器 理解:M是MOV,R是通用寄存器R0-R15,S是特殊功能寄存器,汇编一般左边是目标,右边是源,MRS按顺序展开就是MRS:Rd, CPSID I 设置PRIMASK寄存器的值为1,关闭中断 中断处理过程 关闭全局中断:把PRIMASK的值0写入R0,调用CPSID I关闭全局中断 使能全局中断:把R0的值0写入PRIMASK,使能全局中断 关闭全局中断的意义 由于关闭了systick中断,系统节拍暂停,相当于时间禁止,系统停止调度,当前只有这个线程有MCU的控制权,有效处理多线程对共享资源的使用。
2.2 第二个关键中断:PendSV异常
2.2.1 PendSV异常介绍
??PendSV全称Pendable request for system service,SV是server服务的缩写,PendSV用于操作系统的上下文切换,上下文从内存的角度看就是线程的堆栈,在初始化或者创建线程的时候都需要设置线程的堆栈,上下文指的就是这个堆栈,每个线程都有独立的线程堆栈,在线程的堆栈中保存了内核寄存器的内容,如PC、LR、R0-R12的内容,PC、LR、R0-R12的内容保存了一个时刻的现场,这个现场就是上下文,上下文保存了线程切出和切入的现场信息。 ??区别于SVC,二者的区别是PendSV是异步的,SVC是同步的。挂起(pending)的中断请求会等待比它优先级更高的中断处理完才能处理,这就用到可嵌套中断控制器NVIC了,NVIC将挂起的中断根据优先级的大小逐个执行。而PendSV异常被设置为优先级最小,所以PendSV会在所有中断都处理完后才去执行,这就保证上下文的切换不会影响到中断。我们知道中断需要快进快出,所以上下文切换不能在Systick中断里处理,把它给放到优先级最低的PendSV做处理。
2.2.2 代码分析
.global PendSV_Handler
.type PendSV_Handler, %function
PendSV_Handler:
MRS R2, PRIMASK
CPSID I
LDR R0, =rt_thread_switch_interrupt_flag
LDR R1, [R0]
CBZ R1, pendsv_exit
MOV R1, #0
STR R1, [R0]
LDR R0, =rt_interrupt_from_thread
LDR R1, [R0]
CBZ R1, switch_to_thread
MRS R1, PSP
STMFD R1!, {R4 - R11}
LDR R0, [R0]
STR R1, [R0]
switch_to_thread:
LDR R1, =rt_interrupt_to_thread
LDR R1, [R1]
LDR R1, [R1]
LDMFD R1!, {R4 - R11}
MSR PSP, R1
pendsv_exit:
MSR PRIMASK, R2
ORR LR, LR, #0x04
BX LR
??在rt_hw_context_switch或者rt_hw_context_switch_to中设置rt_thread_switch_interrupt_flag标志,表示要处理PendSV异常,在进入PendSV后先清除rt_thread_switch_interrupt_flag标志,这种用法是裸机里面很常用的前后台处理,在中断外面设置一个标志,然后在中断中先清除标志再做处理。看到下方的流程图,清除完标志后先保存from线程的上下文,再切出to线程的上下文。有一种情况是没有from线程的,那就是rt_hw_context_switch_to,这个线程切换函数用在系统刚开始调度的时候rt_system_scheduler_start,所以对rt_interrupt_from_thread做了一个判空,如果为空则直接切换到to线程的上下文。下面的汇编代码的模式用于获取变量的值,在汇编代码里用得比较多。
LDR R0, =rt_thread_switch_interrupt_flag
LDR R1, [R0]
2.3 第三个关键中断:HardFault异常
2.3.1 HardFault介绍
2.3.2 代码分析
??PSP是进程堆栈,MSP是主堆栈,在系统调度之前使用的是MSP,异常处理程序(例如 PendSV)可以通过改变其在退出时使用的 EXC_RETURN 值来改变使用哪个堆栈。在pendsv_exit中通过 ORR LR, LR, #0x04将使用的堆栈设置为进程栈。 ??在HarldFalut中通过TST lr, #0x04判断是MSP还是PSP,把上下文入栈后通过rt_hw_hard_fault_exception把上下文中的内容打印到终端,根据dump的寄存器值判断触发系统HardFault的原因。下见面的代码。
.global HardFault_Handler
.type HardFault_Handler, %function
HardFault_Handler:
MRS r0, msp
TST lr, #0x04
BEQ _get_sp_done
MRS r0, psp
_get_sp_done:
STMFD r0!, {r4 - r11}
STMFD r0!, {lr}
TST lr, #0x04
BEQ _update_msp
MSR psp, r0
B _update_done
_update_msp:
MSR msp, r0
_update_done:
PUSH {LR}
BL rt_hw_hard_fault_exception
POP {LR}
ORR LR, LR, #0x04
BX LR
三、RT-Thread线程切换过程
3.1 寄存器说明
3.2 线程切换过程
SysTick_Handler->rt_tick_increase->rt_schedule->rt_hw_context_switch((rt_ubase_t)&from_thread->sp,(rt_ubase_t)&to_thread->sp);
??在systick中断里面判断时间片是否用完来决定是否需要切换线程,如果需要切换线程则执行rt_hw_context_switch,在rt_hw_context_switch中把from线程和to线程的栈顶地址保存在全局变量rt_interrupt_from_thread和rt_interrupt_to_thread,最后触发PendSV异常。在适当时机处理PendSV异常(等待其它中断都处理完),PendSV中保存from线程的现场,切出to线程的现场。总结就是始于systick,终于MSR PSP, R1。见如下汇编代码和程序流程图
.global rt_hw_context_switch_interrupt
.type rt_hw_context_switch_interrupt, %function
.global rt_hw_context_switch
.type rt_hw_context_switch, %function
rt_hw_context_switch_interrupt:
rt_hw_context_switch:
LDR R2, =rt_thread_switch_interrupt_flag
LDR R3, [R2]
CMP R3, #1
BEQ _reswitch
MOV R3, #1
STR R3, [R2]
LDR R2, =rt_interrupt_from_thread
STR R0, [R2]
_reswitch:
LDR R2, =rt_interrupt_to_thread
STR R1, [R2]
LDR R0, =ICSR
LDR R1, =PENDSVSET_BIT
STR R1, [R0]
BX LR
四、在arm与riscv上的差异
1、riscv中没有类似于PendSV的中断,上下文切换只有一个过程,而在arm中有两个过程:一是rt_hw_context_switch,二是PendSV异常处理。 2、在arm中,rt_hw_context_switch_interrupt和rt_hw_context_switch是一样的,因为上下文切换是在PendSV中进行的,切换过程不会对中断造成影响。而在riscv中没有类似于PendSV的机制,所以这两个切换函数需要做区分,在中断中只能使用rt_hw_context_switch_interrupt。除了arm以外的其它cpu内核,都与riscv类似,rt_hw_context_switch_interrupt和rt_hw_context_switch需要做区分。
|