Cortex-M以STM32H750为代表,RISC-V以芯来为代表
RTOS版本为RT-Thread 4.1.1
关于STORE
x4, 4(sp)这种寄存器前面带数字的写法,其意思为将x4的值存入sp+4这个地址,即前面的数字表示偏移的意思
反之LOAD
表示从内存里面取值
地址加载 (Load Address). 伪指令(Pseudoinstruction), RV32I and RV64I.
la rd, symbol x[rd] = &symbol
例如将函数irq_entry()的地址放入t0中
la t0, irq_entry
移动(Move). 伪指令(Pseudoinstruction), RV32I and RV64I.
mv rd, rs1 x[rd] = x[rs1]
把寄存器 x[rs1]复制到 x[rd]中。实际被扩展为 addi rd, rs1, 0
加立即数(Add Immediate). I-type, RV32I and RV64I.
把符号位扩展的立即数加到寄存器 x[rs1]上,结果写入 x[rd]。忽略算术溢出。
addi rd, rs1, immediate x[rd] = x[rs1] + sext(immediate)
加 (Add). R-type, RV32I and RV64I.
把寄存器 x[rs2]加到寄存器 x[rs1]上,结果写入 x[rd]。忽略算术溢出。
add rd, rs1, rs2 x[rd] = x[rs1] + x[rs2]
压缩形式:c.add rd, rs2; c.mv rd, rs2
Cortex-M
在启动文件中会初始化一个中断向量表,所有的异常和中断根据这个表的地址跳转
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog interrupt ( wwdg1_it)
DCD PVD_AVD_IRQHandler ; PVD/AVD through EXTI Line detection
RISC-V
的异常会跳入所有异常共享的异常处理程序入口mtvec,在启动文件中的初始化如下:
exc_entry由汇编编写,其中又会调用core_exception_handler()
/*
* Set Exception Entry MTVEC to exc_entry
* Due to settings above, Exception and NMI
* will share common entry.
*/
la t0, exc_entry
csrw CSR_MTVEC, t0
ECLIC 的每个中断源均可以设置成向量或者非向量处理(通过寄存器 clicintattr[i]的 shv 域),其要点如下:
当 mtvec.MODE != 6’b000011 时,处理器使用默认中断模式
mtvec.MODE = 6’b000011 时,处理器使用ECLIC 中断模式
,推荐使用此模式。
/* Set the interrupt processing mode to ECLIC mode */
la t0, 0x3f
csrc CSR_MTVEC, t0
csrs CSR_MTVEC, 0x3
如果被配置成为向量处理模式,则该中断被处理器内核响应后,处理器直接跳入该中断的向量入口(Vector Table Entry)存储的目标地址
如果被配置成为非向量处理模式,则该中断被处理器内核响应后,处理器直接跳入所有中断共享的入口地址
默认情况下异常和所有非向量中断共享入口地址(不推荐),推荐将 CSR 寄存器 mtvt2 的最低位设置为 1,则所有非向量中断共享的入口地址由 CSR 寄
存器 mtvt2 的值(忽略最低 2 位的值)指定
/*
* Set ECLIC non-vector entry to be controlled
* by mtvt2 CSR register.
* Intialize ECLIC non-vector interrupt
* base address mtvt2 to irq_entry.
*/
la t0, irq_entry
csrw CSR_MTVT2, t0
csrs CSR_MTVT2, 0x1
进入irq_entry
后会自动关闭中断MIE
保存完上下文之后会调用对应中断服务程序,在跳入中断服务程序的同时,硬件也会同时打开中断的全局使能
中断服务程序执行完成之后需要将中断全局使能再次关闭,保证恢复上下文的原子性
MPIE会记录异常发生前得MIE值,以便异常结束时恢复到原来的值
csrrw ra, CSR_JALMNXTI, ra
在跳入中断服务程序的同时,“csrrw ra, CSR_JALMNXTI, ra”指令还会达到 JAL(Jump and Link)的效果,硬件同时更新 Link 寄存器的值为该指令的 PC 自身作为函数调用的返回地址。因此,从中断服务程序函数返回后会回到该“csrrw ra,CSR_JALMNXTI, ra”指令重新执行,重新判断是否还有中断在等待(Pending),从而达到中断咬尾的效果。如果没有中断在等待(Pending),则该指令相当于是个 Nop 指令不做任何操作。
对于中断嵌套,会重新从irq_entry
进入
当Cortex-M开始响应一个中断时,会自动完成如下操作:
入栈: 自动把8个寄存器的值压入栈
取向量:从向量表中找出对应的服务程序入口地址
选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
压栈顺序如下:
地址(设原SP为 N-0) | 寄存器 | 被保存的顺序 |
---|---|---|
N-4 | xPSR | 2 |
N-8 | PC | 1 |
N-12 | LR | 8 |
N-16 | R12 | 7 |
N-20 | R3 | 6 |
N-24 | R2 | 5 |
N-28 | R1 | 4 |
N-32 (新SP也指向这里) | R0 | 3 |
RISC-V
架构的处理器在进入和退出中断处理模式时没有硬件自动保存和恢复上下文(通用寄存器)的操作,因此需要软件明确地使用(汇编语言编写的)指令进行上下文的保存和恢复。根据中断是向量处理模式还是非向量处理模式,上下文的保存和恢复涉及到的内容会有所差异
在上述异常exc_entry和中断irq_entry中,都有SAVE_CONTEXT
和RESTORE_CONTEXT
来保存上下文和恢复上下文
.macro SAVE_CONTEXT
csrrw sp, CSR_MSCRATCHCSWL, sp
/* Allocate stack space for context saving */
addi sp, sp, -20*REGBYTES
STORE x1, 0*REGBYTES(sp)
STORE x4, 1*REGBYTES(sp)
STORE x5, 2*REGBYTES(sp)
STORE x6, 3*REGBYTES(sp)
STORE x7, 4*REGBYTES(sp)
STORE x10, 5*REGBYTES(sp)
STORE x11, 6*REGBYTES(sp)
STORE x12, 7*REGBYTES(sp)
STORE x13, 8*REGBYTES(sp)
STORE x14, 9*REGBYTES(sp)
STORE x15, 10*REGBYTES(sp)
STORE x16, 14*REGBYTES(sp)
STORE x17, 15*REGBYTES(sp)
STORE x28, 16*REGBYTES(sp)
STORE x29, 17*REGBYTES(sp)
STORE x30, 18*REGBYTES(sp)
STORE x31, 19*REGBYTES(sp)
.endm
.macro RESTORE_CONTEXT
LOAD x1, 0*REGBYTES(sp)
LOAD x4, 1*REGBYTES(sp)
LOAD x5, 2*REGBYTES(sp)
LOAD x6, 3*REGBYTES(sp)
LOAD x7, 4*REGBYTES(sp)
LOAD x10, 5*REGBYTES(sp)
LOAD x11, 6*REGBYTES(sp)
LOAD x12, 7*REGBYTES(sp)
LOAD x13, 8*REGBYTES(sp)
LOAD x14, 9*REGBYTES(sp)
LOAD x15, 10*REGBYTES(sp)
LOAD x16, 14*REGBYTES(sp)
LOAD x17, 15*REGBYTES(sp)
LOAD x28, 16*REGBYTES(sp)
LOAD x29, 17*REGBYTES(sp)
LOAD x30, 18*REGBYTES(sp)
LOAD x31, 19*REGBYTES(sp)
addi sp, sp, 20*REGBYTES
csrrw sp, CSR_MSCRATCHCSWL, sp
.endm
还有SAVE_CSR_CONTEXT
和RESTORE_CSR_CONTEXT
来保存和恢复这三个MCAUSE
MEPC
MSUBM
CSR寄存器
例如下面的第一条命令表示把MCAUSE
存到SP+11*4的位置,正好上面留了三个位置给CSR寄存器
/**
* \brief Macro for save necessary CSRs to stack
* \details
* This macro store MCAUSE, MEPC, MSUBM to stack.
*/
.macro SAVE_CSR_CONTEXT
/* Store CSR mcause to stack using pushmcause */
csrrwi x5, CSR_PUSHMCAUSE, 11
/* Store CSR mepc to stack using pushmepc */
csrrwi x5, CSR_PUSHMEPC, 12
/* Store CSR msub to stack using pushmsub */
csrrwi x5, CSR_PUSHMSUBM, 13
.endm
.macro RESTORE_CSR_CONTEXT
LOAD x5, 13*REGBYTES(sp)
csrw CSR_MSUBM, x5
LOAD x5, 12*REGBYTES(sp)
csrw CSR_MEPC, x5
LOAD x5, 11*REGBYTES(sp)
csrw CSR_MCAUSE, x5
.endm
Cortex-M有主栈MSP和线程栈PSP自动切换,默认是使用的MSP,那么疑问来了,怎么使用线程栈呢?
PendSV_Handler PROC
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
pendsv_exit
; restore interrupt
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr
ENDP
在线程切换的时候,会把线程的sp——rt_interrupt_to_thread
赋给psp
在进入异常服务程序后,LR的值被自动更新为特殊的EXC_RETURN,所以只需要在异常中将LR的bit2置1就可以切换PSP了
EXC_RETURN会根据进入异常前的模式和SP使用情况生成(保持进入异常前的值),理论上只用第一次线程切换时手动把MSP改成PSP
EXC_RETURN位段 | 含义 |
---|---|
[31:4] | EXC_RETURN的标识:必须全为1 |
3 | 0=返回后进入Handler模式 1=返回后进入线程模式 |
2 | 0=从主堆栈中做出栈操作,返回后使用MSP, 1=从进程堆栈中做出栈操作,返回后使用PSP |
1 | 保留,必须为0 |
0 | 0=返回ARM状态。 1=返回Thumb状态。在CM3中必须为1 |
LR在函数调用时会自动更新,对于函数的返回,将LR出栈给PC即可
在异常退出时,也会将LR赋给PC,但是很显然这不是代码空间的地址,系统会根据标识检测到这是一条EXC_RETURN命令,进一步根据进入中断时入栈的PC进行返回
在保存上下文和恢复上下文中都有如下语句:
csrrw sp, CSR_MSCRATCHCSWL, sp
mscratchcswl 寄存器用于在多个中断 level 间切换时,交换目的寄存器与 mscratch 的值来加速中断处理
使用带读操作的 CSR 指令访问 mscratchcsw,当特权模式不变,在出现中断程序和应用程序的切换时,有以下伪指令所示的寄存器操作:
mcause.mpil
表示前一个中断级别
mintstatus.mil
表示Machine Mode 的有效中断级别
csrrw rd, mscratchcswl, rs1
// Pseudocode operation.
// 栈指针的切换只在中断中操作,mintstatus.mil肯定不为0
// 如果mcause.mpil==0表示从线程中进入的中断(待验证),即判断成立,使用mscratch和SP交换来加载主栈
// RESTORE_CONTEXT中再次调用即再次交互,把主栈存入mscratch并把线程栈交换到SP中
if ( (mcause.mpil==0) != (mintstatus.mil == 0) )
{
t = rs1; rd = mscratch; mscratch = t;
}
else
{ // 中断嵌套使用同一个栈,不需要改变
rd = rs1; // mscratch unchanged.
}
// Usual use: csrrw sp, mscratchcswl, sp
看到这里,就会有一个疑问,第一次进中断时,mscratch中的内容从哪里来呢?
rt_hw_context_switch_to
表示没有来源即第一次切换线程(在开始OS调度时调用,不是在中断切换)
所以第一次切换线程时会将主栈存入mscratch,之后就不需要再管了
之后便线程栈赋值给sp
rt_hw_context_switch_to:
/* Setup Interrupt Stack using
The stack that was used by main()
before the scheduler is started is
no longer required after the scheduler is started.
Interrupt stack pointer is stored in CSR_MSCRATCH */
la t0, _sp
csrw CSR_MSCRATCH, t0
LOAD sp, 0x0(a0) /* Read sp from first TCB member(a0) */
在进入中断时,mepc 寄存器被同时更新,以反映当时遇到中断时的 PC 值。软件必须使用 mret指令退出中断,执行 mret 指令后处理器将从 mepc 定义的 pc 地址重新开始执行。通过这个机制,意味着 mret 指令执行后处理器回到了当时遇到中断时的 PC 地址,从而可以继续执行之前被中止的程序流。