riscv 中断处理
中断(中断返回)本质上也是一种跳转,只不过还需要附加一些读写CSR寄存器的操作。
RISC-V中断分为两种类型,一种是同步中断,即ECALL、EBREAK等指令所产生的中断,另一种是异步中断,即GPIO、UART等外设产生的中断。
- 中断号保存在
mcause
寄存器中,最高位是 1 说明是同步异常,否则是中断 mepc
储存中断前执行指令的地址,调用mret
返回后会执行其中的地址- 对于
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(){ }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律