riscv 中断处理

中断(中断返回)本质上也是一种跳转,只不过还需要附加一些读写CSR寄存器的操作。

RISC-V中断分为两种类型,一种是同步中断,即ECALL、EBREAK等指令所产生的中断,另一种是异步中断,即GPIO、UART等外设产生的中断。

  1. 中断号保存在 mcause 寄存器中,最高位是 1 说明是同步异常,否则是中断
  2. mepc 储存中断前执行指令的地址,调用 mret 返回后会执行其中的地址
  3. 对于 RISCV 而言,当前运行的状态保存在mstatus 寄存器中
  • MPP 位记录当前机器模式的特权等级,0 是用户级,1 是内核级,2 保留,3 是机器级,权限最高
  • MPIE 记录触发中断前的MIE 位的值,MIE (Machine Interrupt Enable)位为 1 的时候,中断才会触发

RISCV 不支持中断嵌套,即中断触发之后会将 mstatus 的 mie 位置 0

中断处理的第一条指令地址存储在 mtvec 中,mie 寄存器(不是mstatus 寄存器中的mie位)控制哪些中断可以被触发,只有对应位置置一的中断号的中断会触发。

中断处理完成之后需要返回,从机器模式的中断返回需要调用 mret 指令,它会 将 PC 设置为 mepc,通过将 mstatus 的 MPIE 域复制到MIE 来恢复之前的中断使能设置,并将权限模式设置为 mstatus 的 MPP 域中的值。

 

 

 

对于中断模块设计,一种简单的方法就是当检测到中断(中断返回)信号时,先暂停整条流水线,设置跳转地址为中断入口地址,然后读、写必要的CSR寄存器(mstatus、mepc、mcause等),等读写完这些CSR寄存器后取消流水线暂停,这样处理器就可以从中断入口地址开始取指,进入中断服务程序。

下面看tinyriscv的中断是如何设计的。中断模块所在文件:rtl/core/clint.v

输入输出信号列表如下:

 

 

 先看中断模块是怎样判断有中断信号产生的,如下代码:

 

 

 

第3~4行,复位后的状态,默认没有中断要处理。

第6~7行,判断当前指令是否是ECALL或者EBREAK指令,如果是则设置中断状态为S_INT_SYNC_ASSERT,表示有同步中断要处理。

第8~9行,判断是否有外设中断信号产生,如果是则设置中断状态为S_INT_ASYNC_ASSERT,表示有异步中断要处理。

第10~11行,判断当前指令是否是MRET指令,MRET指令是中断返回指令。如果是,则设置中断状态为S_INT_MRET。

下面就根据当前的中断状态做不同处理(读写不同的CSR寄存器),代码如下:

 

 

 

 

 

 

 

第1023行,当CSR处于S_CSR_IDLE时,如果中断状态为S_INT_SYNC_ASSERT,则在第11行将CSR状态设置为S_CSR_MEPC,在第12行将当前指令地址保存下来。

在第1323行,根据不同的指令类型,设置不同的中断码(Exception Code),这样在中断服务程序里就可以知道当前中断发生的原因了。

第24~28行,目前tinyriscv只支持定时器这个外设中断。

第30~31行,如果是中断返回指令,则设置CSR状态为S_CSR_MSTATUS_MRET。

第34~48行,一个时钟切换一下CSR状态。

接下来就是写CSR寄存器操作,需要根据上面的CSR状态来写。

 

 

 

 

第11~15行,写mepc寄存器。

第17~21行,写mcause寄存器。

第23~27行,关闭全局异步中断。

第29~33行,写mstatus寄存器。

最后就是发出中断信号,中断信号会进入到执行阶段。

有两种情况需要发出中断信号,一种是进入中断,另一种是退出中断。

9~12行,写完mstatus寄存器后发出中断进入信号,中断入口地址就是mtvec寄存器的值。

第13~15行,发出中断退出信号,中断退出地址就是mepc寄存器的值。

 

编写一个 BOOT

 mret 指令

为了使 hart 跑在监管者模式下,我们必须使用 mret 。

 参考 RISC-V 的相关资料,在处理 mret 指令时,PC 值会从 mepc 寄存器取得。因此,我们必须将 main 函数的地址存入 mepc 寄存器。

mstatus 寄存器

 

刚开始执行代码一定是机器模式,但是我们总不能一直让 hart 在机器模式下运行;此外,全局中断使能位也需要我们控制。这些都可以在 mstatus 寄存器上找到,关于 mstatus 寄存器,RISC-V 特权架构 和 RISC-V 中文手册上都有详细介绍。在此就略写几句。

当进入 main 函数时,hart 最好要进入监管者模式。因为 main 函数事实上是我们操作系统内核最主要的函数之一,此外,我们也希望中断能被打开。对照 mstatus 寄存器的位图,我们可以在对应位域置 1 ,来打开中断或者记录信息等。

比如,我们想先打开机器模式的中断使能,那么我们需要:

将 mstatus.MIE 位置为 1 ,因为它代表机器模式全局下的中断使能
将 mstatus.MPIE 位置为 1 ,它代表了在中断/异常发生前,机器模式全局下的中断使能(我们肯定不想在中断/异常发生一次后,使能就失效了吧)
我们还要将 mstatus.MPP 位置为 01,它代表了中断/异常发生前,代码运行的模式。之所以置为 01(监管者模式),是为了在执行 mret 的时候进入监管者模式。结合之前所说的,写下如下代码:

    li   t0, (0b01 << 11) | (1 << 7) | (1 << 3)
    csrw mstatus, t0

 


 

    # 让其它(非0号)硬件线程挂起,跳转至 3
    csrr    t0, mhartid
    bnez    t0, 3f
    csrw    satp, zero   //关闭mmu

 

这里是读取处理器的核心号码(mhartid),我们只需要使用 0 号核心进行初始化操作,非 0 的核心会跳转到后面挂起

    # 先初始化
    li      t0, (0b11 << 13) | (0b11 << 11) | (1 << 7)
    csrw    mstatus, t0
    la      t1, kernel_init
    csrw    mepc, t1
    la      t2, m_trap_vector
    csrw    mtvec, t2
    li      t3, 0xaaa
    csrw    mie, t3
    la      ra, 4f
    mret

这里出现一个关键的指令 csrw 意思是写入状态控制寄存器。每个核心都有一系列状态控制寄存器,可以参考 RISCV 手册。下方列出的是 mstatus 状态寄存器的每个位的情况。

  • 使 FS 置位,可以开启浮点运算(不开启的话使用浮点数会报错)
  • 使 MPIE 置位,手册里的说法是,这个位储存中断前 MIE 的值,当我们从中断返回后 MPIE 会放到 MIE 中
  • 使 MPP 置 0b11, MMP 标志着当前的特权级别
  • mepc 放置 m_trap_vector 函数的地址,出发中断后会跳转到 m_trap_vector (放在 src/asm/trap.S 中)
  • 调用 mret 之后,会执行 mepc 中的地址,即 kernel_init 函数

Rust 初始化函数

#[no_mangle]
extern "C" fn kernel_init(){
​
}
​
#[no_mangle]
extern "C" fn kernel_start(){
​
}

 

RISC-V from scratch 6 

posted on 2021-12-13 12:08  tycoon3  阅读(4707)  评论(2编辑  收藏  举报

导航