RISC-V 32架构实践专题九(从零开始写操作系统-trap机制)

本文详细解释了RISC-V体系结构中的异常和中断处理机制,包括trap过程、trap相关寄存器的作用、trap流程以及如何在trap时保存和恢复上下文。讨论了中断嵌套、trap处理函数的实现和异常验证案例。
摘要由CSDN通过智能技术生成

        在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寄存器

  1. 更新mcause寄存器,将产生trap的原因自动更新到mcause寄存器中;
  2. 更新mepc寄存器,当trap原因为异常时,将mepc自动更新为遇到trap时的pc寄存器值;当trap原因为中断时,将mepc自动更新为pc寄存器值+4;(这样做的原因是,发生异常时,允许我们进行异常处理后,再一次执行异常时的指令;如果异常此时被修复,则可以再次正常执行下去;常见的应用场景为操作系统中的缺页异常。)
  3. 更新mtval寄存器,将产生异常的辅助信息保存到此寄存器中;
  4. 更新mstatus寄存器,主要是进行xPIE域与xPP域值的更新;(需要注意的是,在riscv原始架构下,同时还会将xIE清零,关闭全局中断。所以原始的riscv架构硬件上是默认不支持中断嵌套的。)
  5. 同时将更新特权模式到机器模式。(其实还能使用托管到监管者模式)

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

上述汇编代码实现的功能是:

  1. 保存当前执行流的上下文环境;
  2. 跳转到c代码中,进行异常处理;
  3. 恢复当前执行流的上下文环境。
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:

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值