TinyEMU源码分析之中断处理


本文属于《 TinyEMU模拟器基础系列教程》之一,欢迎查看其它文章。
本文中使用的代码,均为伪代码,删除了部分源码。

本文,以TinyEMU中M模式下的时钟中断为例,进行说明。

1 触发中断

mtimer是实现在M模式下的定时器,它位于CLINT控制器内部。
并给该计时器,定义了两个64 位宽的寄存器mtime和mtimecmp。

  • mtime,用于反映当前计时器的计数值
  • mtimecmp,用于设置计时器的比较值

当mtime 中的计数值 >= mtimecmp 中设置的比较值时,计时器便会产生时钟中断

时钟中断,会一直拉高,直到软件重新写mtimecmp 寄存器的值,使得mtimecmp值大于mtime值,从而将计时器中断清除。

在TinyEMU源码,riscv_machine.c中riscv_machine_get_sleep_duration函数,如下:

static int riscv_machine_get_sleep_duration(VirtMachine *s1, int delay)
{
	delay1 = m->timecmp - rtc_get_time(m);
	if (delay1 <= 0) {
		riscv_cpu_set_mip(s, MIP_MTIP);
		delay = 0;
	} else {
		/* convert delay to ms */
		delay1 = delay1 / (RTC_FREQ / 1000);
		if (delay1 < delay)
			delay = delay1;
	}
	...
}

当mtimecmp >= 当前时间时,调用riscv_cpu_set_mip函数,将0x80写入mip寄存器(即mip.MTIP=1),表示M模式下时钟中断处于等待响应状态。

2 查询中断

在riscv_cpu_template.h中,取指、译码、执行主循环处理glue函数,如下:

static void no_inline glue(riscv_cpu_interp_x, XLEN)(RISCVCPUState *s,
                                                   int n_cycles1)
{
    for(;;) {
		// 获取PC
		s->pc = GET_PC(); 

		// check pending interrupts
		raise_interrupt(s);
		
		// 取指、译码、执行
		...
	}
}

调用riscv_cpu.c中raise_interrupt函数,来处理中断,如下:

static __exception int raise_interrupt(RISCVCPUState *s)
{
    mask = get_pending_irq_mask(s); // 检测是否有中断或异常
    if (mask == 0)
        return 0;
    irq_num = ctz32(mask); // mask转为中断号或异常号
    raise_exception(s, irq_num | CAUSE_INTERRUPT); // 处理中断或异常
    return -1;
}

在处理中断前,我们需要调用get_pending_irq_mask函数,来检查是否有中断需要处理,返回非0,表示有中断待处理。
接下来,介绍get_pending_irq_mask函数的具体实现。

2.1 查询中断使能与pending状态(mie和mip)

get_pending_irq_mask函数,如下所示:

static inline uint32_t get_pending_irq_mask(RISCVCPUState *s)
{
    uint32_t pending_ints, enabled_ints;

	// part1:查询mip和mie寄存器
    pending_ints = s->mip & s->mie;
    if (pending_ints == 0)
        return 0; // 未发生中断
    ...
}

mie寄存器,可使能和关闭中断(1为使能,0为关闭),如下所示:
在这里插入图片描述

  • SSIE:表示S模式下,软件中断使能位
  • MSIE:表示M模式下,软件中断使能位
  • STIE:表示S模式下,时钟中断使能位
  • MTIE:表示M模式下,时钟中断使能位
  • SEIE:表示S模式下,外部中断使能位
  • MEIE:表示M模式下,外部中断使能位

mip寄存器,可指示中断已发生(1为发生,0为未发生),如下所示:
在这里插入图片描述

  • SSIP:表示S模式下的,软件中断处于等待响应状态
  • MSIP:表示M模式下的,软件中断处于等待响应状态
  • STIP:表示S模式下的,时钟中断处于等待响应状态
  • MTIP:表示M模式下的,时钟中断处于等待响应状态
  • SEIP:表示S模式下的,外部中断处于等待响应状态
  • MEIP:表示M模式下的,外部中断处于等待响应状态

当M模式下时钟中断发生时,则:

  • mie.MTIE,必然为1;
  • mip.MTIP,必然也为1。

因此,只有当mie&mip不为0时,才表示发生了中断,需要进行中断处理。
这里代码中,pending_ints = 0x80,表明发生了M模式下时钟中断,该中断需要被处理。

2.2 查询中断总开关与委托(mstatus和mideleg)

查询委托,也是在get_pending_irq_mask函数,如下所示:

static inline uint32_t get_pending_irq_mask(RISCVCPUState *s)
{	
	// part2:查询mstatus和mideleg寄存器
    enabled_ints = 0;
    switch(s->priv) {
    case PRV_M:
        if (s->mstatus & MSTATUS_MIE)
            enabled_ints = ~s->mideleg;
        break;
    case PRV_S:
        enabled_ints = ~s->mideleg; // s->mideleg = 0x222,enabled_ints = 0xfffffddd
        if (s->mstatus & MSTATUS_SIE) // s->mstatus.sie = 1
            enabled_ints |= s->mideleg; // enabled_ints = 0xffffffff
        break;
    default:
    case PRV_U:
        enabled_ints = -1;
        break;
    }
    return pending_ints & enabled_ints;
}

接下来,分别介绍,各模式下的判断逻辑。

2.2.1 M模式

    case PRV_M:
        if (s->mstatus & MSTATUS_MIE)
            enabled_ints = ~s->mideleg;
        break;

mstatus寄存器的mie位域,表示M模式下,全局中断开关;只有打开时,才会处理中断,否则抛弃。
若当前运行,在M模式下时:

  • 若mideleg.mie关闭,则enabled_ints为0,表明在M模式下,接收到任何中断,都被抛弃。
  • 若mideleg.mie打开,表明允许处理M模式下中断,但是需排除mideleg中指定委托到S模式处理的中断,用取反操作,来屏蔽掉这些中断的bit位,并置位未委托的中断bit位。得到的enabled_ints,该值中bit位为1,对应的这些中断,就是需要在M模式下处理的。

最后,返回值为(pending_ints & enabled_ints),该值为非0时,表示在M模式下可处理的中断。

换言之,在M模式下,可处理的中断,必须满足:

  • mie中对应bit为1:表示打开xx模式yy中断开关
  • mip中对应bit为1:表示xx模式yy中断等待处理
  • mstatus.mie为1:表示打开M模式中断总开关
  • mideleg中对应bit为0:表示xx模式yy中断未委托给S模式处理

注意:
mie、mip、mideleg这三个寄存器的字段结构定义,是完全一样的,理解了这一点,有助于理解本函数,这些逻辑与或操作的含义。
在这里插入图片描述

2.2.2 S模式

    case PRV_S:
        enabled_ints = ~s->mideleg; // s->mideleg = 0x222,enabled_ints = 0xfffffddd
        if (s->mstatus & MSTATUS_SIE) // s->mstatus.sie = 1
            enabled_ints |= s->mideleg; // enabled_ints = 0xffffffff
        break;

mstatus寄存器的sie位域,表示S模式下,全局中断开关;只有打开时,才会处理中断,否则抛弃。
若当前运行,在S模式下时:

  • 若mideleg.sie为0,表示关闭S模式中断,因此委托到S模式的这些中断,统统不能处理,需要忽略。~s->mideleg表示只处理未委托的中断(默认在M模式处理),后续可从S陷入M,去处理这些中断。
  • 若mideleg.sie为1,表示打开S模式中断,因此委托到S模式的这些中断,可以处理;并且未委托的中断(默认在M模式处理),可通过后续从S陷入M,去处理的。这两类中断,都可以处理,因此使用enabled_ints |= s->mideleg

最后,返回值为(pending_ints & enabled_ints),该值为非0时,表示在S模式下可处理的中断。

换言之,在S模式下,可处理的中断,必须满足:

  • mie中对应bit为1:表示打开xx模式yy中断开关
  • mip中对应bit为1:表示xx模式yy中断等待处理
  • mstatus.sie
    (1) sie为0时,只能处理未委托的中断(mideleg对应bit为0),后续通过S陷入M处理。
    (2) sie为1时,可处理未委托的中断(mideleg对应bit为0),后续通过S陷入M处理;以及委托的中断(mideleg对应bit为1),就在S下直接处理。

运行在S模式下时,对于非委托中断,其默认处理方式,就是陷入M模式;因此在S模式下,对这些非委托中断,均做了放过处理,未拦截。

这里,处理M模式时钟中断时,当前运行在S模式下,所以应该走这条分支,以继续处理。

2.2.3 U模式

    case PRV_U:
        enabled_ints = -1; // enabled_ints = 0xffffffff
        break;

若当前运行,在U模式下时:

  • enabled_ints = 0xffffffff,处理接受所有中断。

最后,返回值为(pending_ints & enabled_ints),该值为非0时,表示在U模式下可处理的中断。

换言之,在U模式下,可处理的中断,必须满足:

  • mie中对应bit为1:表示打开xx模式yy中断开关
  • mip中对应bit为1:表示xx模式yy中断等待处理

在U模式下,仅检查上述2项条件,因为U模式本身不具备处理中断的能力,因此对于满足条件的这些中断,需要全部做放过处理。在后续,可通过检查mideleg进行委托到S处理,或者非委托陷入M模式处理。

3 处理中断

static __exception int raise_interrupt(RISCVCPUState *s)
{
    mask = get_pending_irq_mask(s); // 检测是否有中断或异常
    if (mask == 0)
        return 0;
    irq_num = ctz32(mask); // mask转为中断号或异常号
    raise_exception(s, irq_num | CAUSE_INTERRUPT); // 处理中断或异常
    return -1;
}

在调用get_pending_irq_mask函数,查询到mask为非0,下面进行中断的处理。

3.1 获取中断编号

然后,会调用ctz32函数,查询mask中,第几位为1。

static inline int ctz32(uint32_t a)
{
    int i;
    if (a == 0)
        return 32;
    for(i = 0; i < 32; i++) {
        if ((a >> i) & 1)
            return i;
    }
    return 32;
}

例如:
发生M模式时钟中断时,mask=0x80,那么irq_num=7,表示中断编号(Exception Code)为7。
那么,irq_num | CAUSE_INTERRUPT,结果为0x80000007。

3.2 检查委托

然后,会调用raise_exception函数,如下:

static void raise_exception(RISCVCPUState *s, uint32_t cause)
{
    raise_exception2(s, cause, 0);
}
static void raise_exception2(RISCVCPUState *s, uint32_t cause,
                             target_ulong tval)
{
    BOOL deleg;
    target_ulong causel;
    
	// part1 : check deleg
    if (s->priv <= PRV_S) {
        /* delegate the exception to the supervisor priviledge */
        if (cause & CAUSE_INTERRUPT)
            deleg = (s->mideleg >> (cause & (MAX_XLEN - 1))) & 1;
        else
            deleg = (s->medeleg >> cause) & 1;
    } else {
        deleg = 0;
    }
    ...
}

在raise_exception2函数中,首先判断当前模式,如果<=S,即U和S模式,那么才进行委托判断,也就是说:

  • 只有在U和S模式下,发生中断时,才能委托到S模式处理;
  • 在M模式下,发生中断时,不能委托,只能在M模式处理。

这里当前为S模式,因此会进入分支。
然后,再判断cause的最高位:

  • 为1,表示中断。
  • 为0,表示异常。

其实无论是中断,还是异常,都是从cause中取出Exception Code,并判断mideleg中第Exception Code位的值deleg:
如果deleg为0,表示不委托,会在M模式下处理此中断;
如果deleg为1,表示委托,此中断会被委托到S模式处理。

这里M模式时钟中断,对应deleg为0,即mideleg.MTIP=0。
因此,此中断需要在M模式下处理

3.3 进入中断

检查委托,得到deleg值。
然后会将cause扩展为64位,以便写入寄存器中,如下:

static void raise_exception2(RISCVCPUState *s, uint32_t cause,
                             target_ulong tval)
{
	...
	// part2 : enter interrupt
	// 将cause扩展为64位
	// 即0x80000007 => 0x8000000000000007
    causel = cause & 0x7fffffff;
    if (cause & CAUSE_INTERRUPT)
        causel |= (target_ulong)1 << (s->cur_xlen - 1);
    
    // 委托
    if (deleg) {
        s->scause = causel;
        s->sepc = s->pc;
        s->stval = tval;
        s->mstatus = (s->mstatus & ~MSTATUS_SPIE) |
            (((s->mstatus >> s->priv) & 1) << MSTATUS_SPIE_SHIFT);
        s->mstatus = (s->mstatus & ~MSTATUS_SPP) |
            (s->priv << MSTATUS_SPP_SHIFT);
        s->mstatus &= ~MSTATUS_SIE;
        set_priv(s, PRV_S);
        s->pc = s->stvec;
    } 
	// 不委托
	else {
        s->mcause = causel;
        s->mepc = s->pc;
        s->mtval = tval;
        s->mstatus = (s->mstatus & ~MSTATUS_MPIE) |
            (((s->mstatus >> s->priv) & 1) << MSTATUS_MPIE_SHIFT);
        s->mstatus = (s->mstatus & ~MSTATUS_MPP) |
            (s->priv << MSTATUS_MPP_SHIFT);
        s->mstatus &= ~MSTATUS_MIE;
        set_priv(s, PRV_M);
        s->pc = s->mtvec;
    }
}

当deleg为0时,表示不委托,在M模式处理中断。
进入中断服务程序之前,需要完成以下操作:

  • 更新mcause
  • 更新mepc
  • 更新mtval
  • 更新mstatus
  • 切换到M模式
  • pc = mtvec,跳转到M模式异常处理入口地址

当deleg为1时,表示委托,在S模式处理中断。
进入中断服务程序之前,需要完成以下操作:

  • 更新scause
  • 更新sepc
  • 更新stval
  • 更新mstatus
  • 切换到S模式
  • pc = stvec,跳转到S模式异常处理入口地址

更新这些寄存器,主要是做现场保存,比如进入中断处理前的PC,模式等,以便在退出中断处理后,可以恢复到中断前的状态(具体参考RISCV规范文档)。

这里有一个问题,mtvec或stvec,到底什么时候配置的,以及指向何处?
接下来,我们来解释这个问题。

3.3.1 配置mtvec

在Bootloader初始化过程中,会执行riscv-pk\machine\mentry.S中,如下代码:

  # write mtvec and make sure it sticks
  la t0, trap_vector			// t0 = &trap_vector
  csrw mtvec, t0				// mtvec = t0

也就是,把trap_vector地址,写入mtvec寄存器(配置M模式,异常处理入口地址)。
mentry.S中trap_vector地址处,代码如下:
在这里插入图片描述
当为了处理中断或异常,而进入M模式时,PC会跳转到M模式异常向量表trap_vector,开始执行第一条指令csrrw sp, mscratch, sp,直到处理完毕后(当然中间可能会有一些跳转),执行最后一条指令mret,返回之前的模式。硬件在响应mret指令时,会自动将PC跳转到发生异常前的位置。

第一条与最后一条指令之间,这段代码,我们可以理解为:M模式下的异常服务程序
在Bootloader初始化时,只有先配置了mtvec,后续M模式下的异常,才能正常响应。

3.3.2 配置stvec

在进入OS阶段,Linux初始化过程中,会执行arch/riscv/kernel/head.S中,如下代码:

relocate:
	/* Relocate return address */
	li a1, PAGE_OFFSET		// a1 = PAGE_OFFSET
	la a0, _start			// a0 = _start
	sub a1, a1, a0			// a1 = a1 - a0
	add ra, ra, a1			// ra = ra + a1

	/* Point stvec to virtual address of intruction after satp write */
	la a0, 1f				// a0 = 1f
	add a0, a0, a1			// a0 = a0 + a1
	csrw stvec, a0			// stvec = a0 (stvec = 1f + PAGE_OFFSET - _start)

也就是,把S模式异常处理入口地址(1f + PAGE_OFFSET - _start),写入stvec寄存器,(可参考《一篇分析RISC-V Linux汇编启动过程》,或者《内核代码分析(linux系统riscv架构)》)。

该入口地址,其实位于arch/riscv/kernel/entry.S中trap_entry地址处,代码如下:
在这里插入图片描述
直到处理完毕后(当然中间可能会有一些跳转),执行最后一条指令sret,返回之前的模式。硬件在响应sret指令时,会自动将PC跳转到发生异常前的位置。

第一条与最后一条指令之间,这段代码,我们可以理解为:S模式下的异常服务程序
在Linux初始化时,只有先配置了stvec,后续S模式下的异常,才能正常响应。

3.4 执行中断服务程序

回到TinyEMU源码上来,看看如何M模式时钟中断。
在raise_exception2函数中,进入M模式,并跳转到mtvec指向的M模式异常处理入口地址,会执行riscv-pk\machine\mentry.S中,以下关键代码:

  # Yes.  Simply clear MTIE and raise STIP.
  li a0, MIP_MTIP					// a0 = MIP_MTIP
  csrc mie, a0						// mie &= ~a0\
  li a0, MIP_STIP					// a0 = MIP_STIP
  csrs mip, a0						// mip |= a0
  ...
  mret
  • mie.MTIP=0,关闭M模式时钟中断
  • mip.STIP=1,S模式时钟中断处于等待响应状态(中断注入)

然后,便通过mret退出,结束处理。
可以看出:

  • 中断服务程序,并没有特别处理此时钟中断,仅仅是切到M模式下,向S模式注入了一个时钟中断。
  • 类似于,实现了将M模式时钟中断,“委托”到S模式处理的效果。注入的STIP中断,与正常中断处理流程完全一致(下一轮,重新再走一遍“查询中断”=>“处理中断”,这些各个步骤)。

3.5 退出中断

由于退出中断时,固件/OS,往往会调用mret或sret指令,来恢复中断前的状态和模式。
我们看看TinyEMU,是如何响应mret和sret指令的。

3.5.1 处理mret指令

当TinyEMU执行mret指令时,会调用riscv_cpu.c中handle_mret函数,如下所示:

static void handle_mret(RISCVCPUState *s)
{
    int mpp, mpie;
    mpp = (s->mstatus >> MSTATUS_MPP_SHIFT) & 3;
    /* set the IE state to previous IE state */
    mpie = (s->mstatus >> MSTATUS_MPIE_SHIFT) & 1;
    s->mstatus = (s->mstatus & ~(1 << mpp)) |
        (mpie << mpp);
    /* set MPIE to 1 */
    s->mstatus |= MSTATUS_MPIE;
    /* set MPP to U */
    s->mstatus &= ~MSTATUS_MPP;
    set_priv(s, mpp);
    s->pc = s->mepc;
}

退出中断服务程序后,需要完成以下操作:

  • 恢复mstatus
  • M模式,切换到中断前的模式
  • pc = mepc,跳转中断前的程序PC地址

这些操作,都是做现场恢复(具体参考RISCV规范文档)。

3.5.2 处理sret指令

当TinyEMU执行sret指令时,会调用riscv_cpu.c中handle_sret函数,如下所示:

static void handle_sret(RISCVCPUState *s)
{
    int spp, spie;
    spp = (s->mstatus >> MSTATUS_SPP_SHIFT) & 1;
    /* set the IE state to previous IE state */
    spie = (s->mstatus >> MSTATUS_SPIE_SHIFT) & 1;
    s->mstatus = (s->mstatus & ~(1 << spp)) |
        (spie << spp);
    /* set SPIE to 1 */
    s->mstatus |= MSTATUS_SPIE;
    /* set SPP to U */
    s->mstatus &= ~MSTATUS_SPP;
    set_priv(s, spp);
    s->pc = s->sepc;
}

退出中断服务程序后,需要完成以下操作:

  • 恢复mstatus
  • S模式,切换到中断前的模式
  • pc = sepc,跳转中断前的程序PC地址

这些操作,都是做现场恢复(具体参考RISCV规范文档)。

4 RISCV中断处理流程图

中断查询,其流程图,如下所示:
在这里插入图片描述
中断处理,其流程图,如下所示:
在这里插入图片描述

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百里杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值