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(;;) {
// 获取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 总结
中断查询,其流程图,如下所示:
中断处理,其流程图,如下所示: