在riscv体系架构中,异常和中断的过程被统称为trap。广义的来说,中断也属于异常的一部分;不管发生异常或是中断,微处理器的硬件行为是一致的,微处理器暂停当前程序,转向异常或中断处理程序,处理完成后返回之前暂停的程序。
一、异常类型
对于青稞V4内核来说,其异常类型如下图所示:
其中interrupt位为1的属于中断,为0则属于异常。
通常来讲,异常是属于同步的,在正常的程序执行流中,执行到某条指令后,触发异常,所以是同步的(由自身引起的);而中断则属于异步的,一般在程序执行过程中,由外部硬件触发中断,打断正常执行流程,所以是异步的(由外部引起的)。
二、trap的过程分析
要弄清楚riscv架构的trap流程,则不得不了解几个重要的CSR寄存器;在trap发生时,硬件会进行一系列操作来处理trap,其现象就是表现为硬件自动对某些CSR寄存器进行硬件置位;完成trap的硬件操作后,才会跳转到异常入口进行异常处理函数的执行。
2.1 trap相关寄存器
需要了解的CSR寄存器如下所示:
- mstatus寄存器
xIE域,用于x模式下的全局中断使能;
xPIE域,在发生trap时,保存之前xIE位的值;trap结束后,将值恢复给xIE位;
xPP域,在发生trap时,保存之前的特权级模式;trap结束后,将特权模式恢复为此域的值。
- mtvec 寄存器
机器陷阱向量基地址寄存器主要用于存储异常向量基地址,通过最后两个bit位来选择trap异常模式:
- 0,异常统一入口地址模式;无论发生什么异常,都跳转到此寄存器中设置的地址;
- 1,异常向量表模式;当发生异常时(所有的异常),都跳转到此寄存器中的地址;当发生中断时,跳转到【中断号 * 4 + 此寄存器中的地址】地址处;
- mepc寄存器
当trap发生时,此寄存器自动保存pc寄存器的值;当trap退出后,mepc的值将返回给pc。
- mcause寄存器
此寄存器为机器模式下的trap原因寄存器;当发生trap时,将产生原因保存到此寄存器当中。
此寄存器中的interrupt位为0时,表示trap原因为异常;为1时,表示trap原因为中断。
- mtval
mtval寄存器用于提供更为详细的异常原因信息,其实现与硬件强相关。由cpu厂家各自行实现,具体可参考各cpu厂商的内核手册。(此寄存器值就是异常产生的更细致的原因)
2.2 trap流程
当程序正常运行,此时发生trap时,将进行如下步骤的处理。
2.2.1 自动更新CSR寄存器
- 更新mcause寄存器,将产生trap的原因自动更新到mcause寄存器中;
- 更新mepc寄存器,当trap原因为异常时,将mepc自动更新为遇到trap时的pc寄存器值;当trap原因为中断时,将mepc自动更新为pc寄存器值+4;(这样做的原因是,发生异常时,允许我们进行异常处理后,再一次执行异常时的指令;如果异常此时被修复,则可以再次正常执行下去;常见的应用场景为操作系统中的缺页异常。)
- 更新mtval寄存器,将产生异常的辅助信息保存到此寄存器中;
- 更新mstatus寄存器,主要是进行xPIE域与xPP域值的更新;(需要注意的是,在riscv原始架构下,同时还会将xIE清零,关闭全局中断。所以原始的riscv架构硬件上是默认不支持中断嵌套的。)
- 同时将更新特权模式到机器模式。(其实还能使用托管到监管者模式)
2.2.2 进入异常处理函数
根据mtvec中的值,将跳转到异常处理函数的入口地址开始执行。
这里需要注意的是,当mtvec设置为统一入口地址时,不管时中断还是异常,都将跳转到同一个地址开始执行;当mtvec设置为向量表模式时,所有的异常都是跳转到向量表的基地址处开始执行,而中断则从基地址开始,加上中断号 * 4的偏移。
最后需要注意的是,在进行trap时,是需要进行上下文context保存与恢复的。因为trap发生的时候我们是无法明确其发生时间点的,所以对于caller save的通用寄存器我们就无法进行及时的保存,所以需要在trap时,自行完成上下文context保存与恢复。
对于硬件上实现了硬件压栈的riscv cpu,则可以不用软件实现trap时上下文context的保存与恢复;对于没有在硬件上实现压栈功能的riscv cpu,则只能在软件上实现上下文context的保存与恢复。
2.2.3 mret退出异常
异常或中断处理程序完成之后,需要从服务程序中退出。进入异常和中断后,微处理器由用户模式进入机器模式,异常和中断的处理也在机器模式下完成,当需要退出异常和中断时,需要使用 mret 指令进行返回。
此时,微处理器硬件将自动执行如下操作:
- PC 指针恢复为 CSR 寄存器 mepc 的值,即从 mepc 保存的指令地址处开始执行。需要注意异常处理完成后对 mepc 的偏移操作。
- 更新 CSR 寄存器 mstatus,MIE 恢复为 MPIE,MPP 用于恢复之前的微处理器的特权模式。
三、trap的实现
3.1 trap处理函数
首先需要实现一个trap处理功能,此功能由汇编与c语言代码组合而成。其中汇编代码用于保存当前执行流的上下文环境,c代码则用于处理相关发生的异常。
由于青稞V4F支持中断嵌套,所以它与原始riscv架构此处有一定区别;我们为了避免因中断嵌套而产生的一些未知错误,在trap阶段,使用软件关闭全局中断,禁止发生中断嵌套。
# 将如下代码链接到.init.trap段
.section .init.trap
.global trap_vec
# 地址进行4字节对齐
.balign 4
trap_vec:
# 关闭全局中断,禁止中断嵌套
csrci mstatus, 0x08
# 完成当前上下文的保存 --- trap发生时,软件自己进行保存
csrrw t6, mscratch, t6
store_reg t6
mv t5, t6
csrrw t6, mscratch, t6
sw t6, 120(t5)
# 为即将调用的函数传入2个形参,分别是异常地址和异常原因
csrr a0, mepc
csrr a1, mcause
# 伪指令,实现长跳转,跳入c代码中进行异常处理
call trap_handle
# 将返回的地址重新写入mepc
csrw mepc, a0
# 恢复正常执行流的上下文环境
csrr a0, mscratch
restore_reg a0
lw a0, 36(a0)
# 退出机器模式,硬件自动使能全局中断
mret
上述汇编代码实现的功能是:
- 保存当前执行流的上下文环境;
- 跳转到c代码中,进行异常处理;
- 恢复当前执行流的上下文环境。
void trap_init(void)
{
uint32_t vec = (uint32_t)trap_vec & 0xfffffffc;
// 设置mtvec异常向量入口地址,且模式为统一入口地址模式
asm volatile("csrw mtvec, %0" : : "r"(vec));
}
uint32_t trap_handle(uint32_t epc, uint32_t cause)
{
uint32_t return_pc = epc;
uint32_t cause_code = cause & 0xfff;
// 判断trap发生原因是异常还是中断
if (cause & 0x80000000) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 12:
usart1_puts("systick interruption!\n\r");
break;
case 14:
usart1_puts("software interruption!\n\r");
break;
default:
usart1_puts("unknown async exception!\n\r");
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions!, code = %d\n\r", cause_code);
//panic("OOPS! What can I do!");
// 发生异常时可以对mepc的值+4,来跳过异常指令继续执行下去
//return_pc += 4;
}
return return_pc;
}
上述c代码,实现了对异常的处理:
当发生异常时,将运行到else分支中去;如果不对mepc进行+4处理,则后续运行会一直跳入此分支;如果对mepc进行了+4处理,则程序执行流将正常运行。
同时需要注意的是,mstatus寄存器的xIE只是使能全局中断,仅对中断有效;对异常和非屏蔽中断来说,是无效的(也就是说,当异常发生后,肯定是会进行trap的,无论全局中断是否开启)
四、异常验证
void taks_fun2(void)
{
while(1)
{
printf("taks_fun2 --- running!\n\r");
led2_ctrl(0);
mdelay(1000);
// 故意进行地址不对齐访问,产生人为异常
*(uint32_t *)2 = 0x12345678;
schedule_task();
}
}
void mdelay(uint32_t m)
{
uint32_t i = 0;
for(i = 0; i < m * 6000; i++)
{
;
}
}
void start_kernel(void)
{
uint32_t task_id[2] = {0};
// 设置sysclk系统时钟为96MHz
clock_hse_96Mhz();
// 设置usart1,并初始化配置为115200,8,none,1
usart1_init();
printf("\r======== hello RVOS ========\n\r");
// 初始化内存页分配器
page_init();
// 初始化trap
trap_init();
printf("trap_init done!\n\r");
task_id[0] = task_create(taks_fun1);
printf("create task id: %d!\n\r", task_id[0]);
task_id[1] = task_create(taks_fun2);
printf("create task id: %d!\n\r", task_id[1]);
schedule_task();
while (1) {} // stop here!
}
将任务2中,进行地址不对齐访问,造成人为异常,然后运行代码,查看现象。
4.1 当异常处理不进行mepc+4时
4.2 当异常处理进行mepc+4时
异常编号为6: