4、RISC-V 中断机制入门:中断概念、异常与中断的区别、RISC-V 特权架构(Machine/Supervisor/User 模式)、mtvec 寄存器与中断向量表
好,咱们今天聊聊中断。说实话,中断这玩意儿,是嵌入式开发的灵魂。没有中断,你的CPU就得像个傻子一样轮询,白白浪费算力。我在做D1s裸机开发时,第一个要搞定的就是中断系统。你想想看,按键响应、定时器触发、外设数据收发,哪个离得开中断?
4.1 中断到底是什么?
中断,说白了就是CPU正在干一件事,突然被一个紧急事件打断,CPU停下当前工作,去处理这个紧急事件,处理完再回来接着干。就像你正在写代码,突然有人敲门——你放下键盘去开门,聊完回来继续写。
但这里有个关键点:中断是异步的。你不知道它什么时候来。它可能在你执行任何一条指令时发生。所以中断处理程序必须非常小心,不能破坏CPU当前的工作状态。
核心要点:中断是硬件触发的,与当前执行的指令无关。它随时可能发生,所以中断服务程序(ISR)必须保存和恢复现场。
4.2 异常与中断的区别
很多人把异常和中断混为一谈。其实它们有本质区别。我刚开始学RISC-V时也搞混过,后来踩了个坑才彻底明白。
| 特性 | 中断 | 异常 |
|---|---|---|
| 触发方式 | 外部硬件事件(定时器、GPIO、UART等) | 指令执行过程中产生(除零、非法指令、缺页等) |
| 同步性 | 异步(与指令流无关) | 同步(由当前指令引起) |
| 可预测性 | 不可预测,随时可能发生 | 可预测,执行特定指令时发生 |
| 返回地址 | 被中断指令的下一条 | 导致异常的指令本身(或下一条,取决于异常类型) |
| 典型例子 | 定时器溢出、按键按下 | 除零错误、非法指令、系统调用 |
嗯,这里要注意:异常是同步的。比如你执行了一条除零指令,CPU立刻就知道出错了,当场就触发异常。而中断是异步的,它可能在你执行加法指令时突然冒出来。
我曾经在调试D1s的UART驱动时,发现数据总是丢包。查了半天,原来是中断处理程序里不小心修改了某个寄存器,导致异常触发。异常处理程序又没处理好现场恢复,结果死循环了。从那以后,我写中断处理程序都格外小心——能不用全局变量就不用,能用汇编保存现场就绝不用C。
4.3 RISC-V 特权架构:Machine / Supervisor / User 模式
RISC-V的特权架构,说白了就是给CPU分了三个等级。等级越高,权限越大,能访问的资源越多。D1s用的是RISC-V 64位核心,支持这三个模式。
- Machine Mode(M模式):最高权限。能访问所有寄存器、所有内存、所有外设。裸机程序就跑在这个模式下。没有操作系统,你就是上帝。
- Supervisor Mode(S模式):中间权限。通常给操作系统内核用。能访问大部分资源,但有些关键寄存器(比如mtvec)碰不了。
- User Mode(U模式):最低权限。给应用程序用。只能访问自己的地址空间,想干点坏事?门都没有。
个人经验:在D1s裸机开发中,我们全程使用M模式。这样最简单,不用操心权限切换。但如果你将来要跑RTOS或者Linux,就得理解S模式和U模式的作用了。
为什么要有这些模式?说白了就是为了安全。你想想看,如果应用程序能直接修改中断向量表,那系统还不乱套了?所以关键资源只有M模式才能动。
4.4 mtvec 寄存器与中断向量表
好,重头戏来了。mtvec 是Machine模式下的陷阱向量基址寄存器。它告诉CPU:当发生中断或异常时,你该去哪里找处理程序。
mtvec 寄存器长这样:
mtvec 寄存器(Machine Trap-Vector Base-Address Register)
位宽:64位(RV64)或32位(RV32)
结构:
[63:2] BASE[63:2] —— 中断向量表的基地址(4字节对齐)
[1:0] MODE —— 模式选择
00: Direct模式(所有中断/异常都跳转到BASE地址)
01: Vectored模式(根据中断号偏移,跳转到 BASE + cause*4)
10: 保留
11: 保留
这里有两个模式,我重点说一下:
- Direct模式(MODE=00):所有中断和异常都跳转到同一个入口地址。你需要在那个入口处判断是什么中断,然后分发处理。简单,但效率低。
- Vectored模式(MODE=01):根据中断号自动计算偏移。比如定时器中断号是7,CPU就会跳转到 BASE + 7*4 处。这样每个中断都有自己的入口,效率高。
注意:在D1s上,我建议使用Direct模式。为什么?因为Vectored模式要求每个中断入口只有一条指令的空间(4字节),你只能放一条跳转指令。而Direct模式可以放更多指令,处理起来更灵活。我曾经在别的芯片上用过Vectored模式,结果中断太多,跳转表都放不下了,最后还得改回Direct模式。
设置mtvec的代码示例:
// 汇编代码示例:设置mtvec为Direct模式
// 假设中断向量表放在地址 0x80000000
.section .text.vector
.globl _vector_table
_vector_table:
// 这里放中断处理程序的入口
j handle_all_interrupts // Direct模式,所有中断都跳到这里
.section .text.init
.globl _start
_start:
// 设置mtvec寄存器
la t0, _vector_table // 加载向量表地址
li t1, 0x00 // Direct模式(MODE=0)
or t0, t0, t1 // 组合地址和模式
csrw mtvec, t0 // 写入mtvec寄存器
// 开启全局中断
csrsi mstatus, 0x08 // 设置MIE位(Machine Interrupt Enable)
// 现在中断已经使能了
// ... 其他初始化代码
嗯,这里要注意:mtvec 的地址必须4字节对齐。因为低2位被用来存模式了。如果你地址没对齐,CPU会直接报错。我刚开始写D1s代码时就犯过这个错,折腾了半天才发现是地址没对齐。
4.5 中断处理流程
当中断发生时,CPU自动做以下几件事:
- 保存当前PC到 mepc 寄存器
- 保存中断原因到 mcause 寄存器
- 关闭全局中断(MIE位清零)
- 根据mtvec的设置,跳转到对应的处理程序
- 执行中断处理程序
- 执行 mret 指令,恢复现场,返回被中断的程序
这里有个细节:mret 指令会从mepc恢复PC,同时重新使能中断。所以你的中断处理程序里,如果不想被其他中断打断,可以保持MIE关闭。如果想支持中断嵌套,就得手动操作mstatus寄存器。
避坑指南:我曾经在D1s上写中断处理程序时,忘记在返回前清除中断挂起位。结果一执行mret,中断又立刻触发了,直接死循环。所以记住:处理完中断后,一定要清除对应的中断挂起标志。
4.6 总结
好了,这一章的内容就这些。咱们捋一捋:
- 中断是异步的,异常是同步的,别搞混
- RISC-V有三个特权模式,裸机用M模式就够了
- mtvec寄存器告诉CPU中断向量表在哪,Direct模式更灵活
- 中断处理流程:保存现场 → 处理中断 → 清除标志 → mret返回
下一章,咱们会实际动手配置D1s的PLIC(平台级中断控制器),让外设真正能触发中断。到时候你会看到,理论归理论,实际写代码时还有不少坑等着你。不过别担心,我会把踩过的坑都告诉你。