TinyEMU源码分析之中断处理
- 1 触发中断
- 2 查询中断
- 2.1 查询中断使能与pending状态(mie和mip)
- 2.2 查询中断总开关与委托(mstatus和mideleg)
- 2.2.1 M模式
- 2.2.2 S模式
- 2.2.3 U模式
- 3 处理中断
- 3.1 获取中断编号
- 3.2 检查委托
- 3.3 进入中断
- 3.3.1 配置mtvec
- 3.3.2 配置stvec
- 3.4 执行中断服务程序
- 3.5 退出中断
- 3.5.1 处理mret指令
- 3.5.2 处理sret指令
- 4 总结
本文属于《 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(;;) {// 获取PCs->pc = GET_PC(); // check pending interruptsraise_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 = 0xfffffdddif (s->mstatus & MSTATUS_SIE) // s->mstatus.sie = 1enabled_ints |= s->mideleg; // enabled_ints = 0xffffffffbreak;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 = 0xfffffdddif (s->mstatus & MSTATUS_SIE) // s->mstatus.sie = 1enabled_ints |= s->mideleg; // enabled_ints = 0xffffffffbreak;
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 = 0xffffffffbreak;
若当前运行,在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 delegif (s->priv <= PRV_S) {/* delegate the exception to the supervisor priviledge */if (cause & CAUSE_INTERRUPT)deleg = (s->mideleg >> (cause & (MAX_XLEN - 1))) & 1;elsedeleg = (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 => 0x8000000000000007causel = 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 sticksla t0, trap_vector // t0 = &trap_vectorcsrw 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_OFFSETla a0, _start // a0 = _startsub a1, a1, a0 // a1 = a1 - a0add ra, ra, a1 // ra = ra + a1/* Point stvec to virtual address of intruction after satp write */la a0, 1f // a0 = 1fadd a0, a0, a1 // a0 = a0 + a1csrw 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_MTIPcsrc mie, a0 // mie &= ~a0\li a0, MIP_STIP // a0 = MIP_STIPcsrs 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 总结
中断查询,其流程图,如下所示:
中断处理,其流程图,如下所示: