本篇定位 :RISC-V 专题第 3 篇。CSR(Control and Status Registers)是 M-mode 程序员的"控制面板"------所有配置(中断使能、trap 入口、模式切换、内存保护、性能计数器)都在 CSR 里。你写 bare-metal、配中断、切栈、调死机,本质就是读写 CSR。本篇给你一张完整 CSR 速查表和每个关键 CSR 的位级详解,重点是你 trap/中断要用到的 mstatus/mtvec/mepc/mcause/mtval/mscratch。
CSR 和通用寄存器的区别通用寄存器 x0-x31 是"数据寄存器",算术运算用。CSR 是"控制寄存器",不参与算术,只控制 CPU 行为。CSR 有独立的地址空间(0x000-0xFFF),用专门的 csr 指令读写,不能直接 add/lw。这点和 ARM 的"特殊寄存器靠 MSR/MRS 访问"类似,但 RISC-V 的 CSR 数量更多、分工更细。
目录
- [一、CSR 读写指令](#一、CSR 读写指令)
- [二、M-mode 核心 CSR 全表](#二、M-mode 核心 CSR 全表)
-
- [2.1 trap 相关(④专题详讲,这里先认脸)](#2.1 trap 相关(④专题详讲,这里先认脸))
- [2.2 计时器/计数器](#2.2 计时器/计数器)
- [2.3 PMP(物理内存保护,⑥专题)](#2.3 PMP(物理内存保护,⑥专题))
- [2.4 调试(⑩专题)](#2.4 调试(⑩专题))
- [三、mstatus 详解(最重要,没有之一)](#三、mstatus 详解(最重要,没有之一))
- [四、mtvec:trap 入口](#四、mtvec:trap 入口)
-
- [Direct 模式(常用,你大概率用这)](#Direct 模式(常用,你大概率用这))
- [Vectored 模式](#Vectored 模式)
- [五、mepc / mcause / mtval:死机三件套](#五、mepc / mcause / mtval:死机三件套)
- 六、mscratch:中断栈切换的关键(重点)
一、CSR 读写指令
四个指令族,都是 csr 开头:
csrrw rd, csr, rs1 # rd = csr; csr = rs1 (读旧值到rd,写新值)
csrrs rd, csr, rs1 # rd = csr; csr = csr | rs1 (置位,rs1=0则只读)
csrrc rd, csr, rs1 # rd = csr; csr = csr & ~rs1 (清位,rs1=0则只读)
csrrwi rd, csr, imm # 立即数版 csrrw
csrrsi rd, csr, imm # 立即数版 csrrs
csrrci rd, csr, imm # 立即数版 csrrc
伪指令(汇编常用):
csrr rd, csr # = csrrs rd, csr, x0 只读
csrw csr, rs1 # = csrrw x0, csr, rs1 只写
csrs csr, rs1 # = csrrs x0, csr, rs1 置位
csrc csr, rs1 # = csrrc x0, csr, rs1 清位
csrrs/csrrc 的妙用:原子位操作
csrrs/csrrc 是"读-改-写"原子操作------硬件保证读出旧值、改位、写回这个序列不被打断。所以改 CSR 单个位用 csrs/csrc,不要用 csrr+改+csrw 三步(三步中间可能被 trap 打断,造成丢失更新)。这点对中断使能位(mie)尤其关键。
二、M-mode 核心 CSR 全表
按功能分组:
2.1 trap 相关(④专题详讲,这里先认脸)
| CSR | 地址 | 作用 | 关键位 |
|---|---|---|---|
mstatus |
0x300 | 机器状态 | MIE/MPIE/MPP |
misa |
0x301 | CPU 支持的扩展 | MXL31:30 + Extensions25:0 |
mie |
0x304 | 中断使能 | MSIE/MTIE/MEIO + 各外部中断 |
mip |
0x344 | 中断挂起(只读) | MSIP/MTIP/MEIP |
mtvec |
0x305 | trap 入口地址 | BASE + MODE1:0 |
mscratch |
0x340 | 暂存寄存器 | 全 32 位,你随便用 |
mepc |
0x341 | trap 时的 PC | 死机指令地址 |
mcause |
0x342 | trap 原因 | Interrupt31 + Code30:0 |
mtval |
0x343 | trap 附加信息 | 出错地址/指令编码 |
mcause 例外 |
--- | --- | 见下表 |
mtval |
--- | --- | --- |
mhartid |
0xF14 | 当前核 ID | 多核启动用 |
2.2 计时器/计数器
| CSR | 作用 |
|---|---|
mcycle/mcycleh |
周期计数器(低32/高32),性能剖析用 |
minstret/minstreth |
退休指令计数器 |
mtime |
实时时钟(CLINT 提供,内存映射不是 CSR) |
mtimecmp |
定时器比较寄存器(触发 MTI) |
2.3 PMP(物理内存保护,⑥专题)
| CSR | 作用 |
|---|---|
pmpcfg0-15 |
16 个 PMP 区的配置(权限/模式) |
pmpaddr0-15 |
16 个 PMP 区的地址 |
2.4 调试(⑩专题)
| CSR | 作用 |
|---|---|
dcsr |
调试控制和状态 |
dpc |
调试 trap 的 PC |
dscratch0/1 |
调试暂存 |
三、mstatus 详解(最重要,没有之一)
mstatus 是机器状态的"总开关",trap/中断/模式切换全靠它。位布局(rv32):
mstatus[31:12]: SD/TSR/TW/TVM/MXR/SUM/MPRV/XS/FS/VS 等
mstatus[12:11]: MPP ← trap 前的特权模式(M/S/U)
mstatus[7]: MPIE ← trap 前的 MIE
mindex[6]: SPIE
mstatus[5]: UPIE
mstatus[3]: MIE ← M 级全局中断使能
mstatus[1]: SIE
mstatus[0]: UIE
关键位详解
MIE(Machine Interrupt Enable,bit 3):M 级全局中断总开关。
- MIE=1,M 级中断可触发
- MIE=0,M 级中断被屏蔽
- trap 进 M 级时,硬件自动 MIE→0(防中断嵌套),trap 前的值存 MPIE
MPIE(bit 7):trap 前的 MIE 值。
- trap 发生:MPIE ← MIE,MIE ← 0
- mret 返回:MIE ← MPIE,MPIE ← 1
- 这个机制保证 mret 后中断状态恢复到 trap 前
MPP12:11:trap 前的特权模式。
- 00=U, 01=S, 11=M
- trap 发生:MPP ← 当前模式
- mret 返回:模式 ← MPP
- 这是"判断死机时在哪个模式"的关键------读 mstatus.MPP 就知道
MPP 的两个特殊用法:
- 软件想强制切到 S 模式:写 mstatus.MPP=01,然后 mret
- 初始化第一个从 M 切 S 的跳转:同样设 MPP=01 再 mret
其他位(选读)
- FS14:13:浮点单元状态(Off/Initial/Clean/Dirty)。关 FPU 省电、或裸机不用浮点时设 Off
- VS10:9:向量单元状态(同 FS)
- MPRV(bit 17):置 1 时,M 模式的 load/store 用 MPP 指定的模式权限(内核访问用户内存用)
- TVM(bit 20):置 1 禁止 S 模式改页表(S mode 虚拟化用)
四、mtvec:trap 入口
mtvec[31:2]: BASE trap 入口地址(4字节对齐)
mtvec[1:0]: MODE 模式
00 = Direct 所有 trap 跳到 BASE
01 = Vectored 异常跳 BASE,中断跳 BASE + 4*cause
1x = 保留
Direct 模式(常用,你大概率用这)
所有 trap(异常+中断)都跳到同一个 BASE 地址。你的 trap handler 第一件事是读 mcause 判断是异常还是哪种中断,再分支处理。简单、灵活,MCU bare-metal 标准做法。
Vectored 模式
中断按 cause 走不同入口(BASE + 4*cause),异常仍进 BASE。省一次 mcause 读取,但入口表占用代码空间,且各中断入口都要写保存上下文的汇编。中断多且对延迟敏感时用。
怎么看你用的是哪种
启动汇编里
la t0, trap_vector; csrw mtvec, t0后,trap_vector 是不是数组?vectored 模式需要一段连续的入口表。大多数 MCU SDK(包括 FreeRTOS 移植)用 direct 模式 + 一个统一 handler 分发,简单可靠。
五、mepc / mcause / mtval:死机三件套
死机现场,这三个 CSR 是你的"黑匣子"。
mepc(0x341)
trap 发生时的 PC------就是出问题那条指令的地址(对于中断,是下一条要执行的指令)。
- 异常:返回后会重新执行那条指令(若已修复),或跳过
- 中断:返回后执行下一条
- 死机调试第一步:打 mepc,addr2line 看是哪行代码
mcause(0x342)
mcause[31]: Interrupt 1=中断, 0=异常
mcause[30:0]: Code 原因编号
异常 Code(Interrupt=0):
| Code | 含义 | 你要做什么 |
|---|---|---|
| 0 | 指令地址对齐错 | 查函数指针/jalr 目标对齐 |
| 1 | 指令访问 fault | PC 指向不可执行区 |
| 2 | 非法指令 | 反汇编 mepc 处,看是不是跑飞到数据区 |
| 3 | Breakpoint | ebreak 或调试器 |
| 4 | Load 地址对齐错 | lw/sw 地址没对齐 |
| 5 | Load 访问 fault | 最常见,野指针/未初始化指针 |
| 6 | Store 地址对齐 | --- |
| 7 | Store 访问 fault | 写只读区/野指针 |
| 8 | U 模式 ecall | 用户态系统调用 |
| 9 | S 模式 ecall | --- |
| 11 | M 模式 ecall | --- |
| 12-15 | 栈/页表相关 | --- |
中断 Code(Interrupt=1):
| Code | 含义 |
|---|---|
| 3 | MSI(机器软件中断,IPI) |
| 7 | MTI(机器定时器中断,FreeRTOS tick) |
| 11 | MEI(机器外部中断,经 PLIC,I2C/UART 等) |
死机排查标准流程
- 读 mepc → addr2line 定位代码行
- 读 mcause → 知道是哪种异常/中断
- 读 mtval → 看出错地址
- 读 mstatus.MPP → 知道死机时在 M/S/U 哪个模式
- 组合判断:如 mcause=5(load fault)+ mtval=0xDEADBEEF + mepc=某读指针行 → 野指针读 0xDEADBEEF
这五步是你调任何 RISC-V 死机的元动作,刻进肌肉记忆。
mtval(0x343)
trap 附加信息,内容随 mcause 变:
- 访问 fault(5/7):出错的内存地址
- 非法指令(2):那条指令的编码
- 页表 fault:出错地址
- 其他:可能为 0(实现定义)
mtval 让你能直接看到"哪个地址炸了",不用反推。你栈回溯笔记里没强调这个,但它和 mepc/mcause 一样重要,是死机第一现场。
六、mscratch:中断栈切换的关键(重点)
mscratch(0x340)是一个纯暂存寄存器 ,硬件不给它任何特殊语义,你随便用。但它有一个约定俗成的关键用途:中断栈切换。
为什么需要切栈
RTOS 里,任务跑在用户栈(PSP/线程栈),中断要跑内核栈(MSP/中断栈),避免中断污染任务栈、防止栈溢出时任务和中环互相破坏。ARM Cortex-M 硬件自动切 MSP/PSP(靠 EXC_RETURN bit 2);RISC-V 不自动切,你得用 mscratch 软件切。
切栈标准手法
asm
# 初始化时:M-mode sp 存内核栈,mscratch 存任务栈
# (或反过来,看约定)
trap_vector:
# 进入时:sp 是任务栈,mscratch 是内核栈
csrrw sp, mscratch, sp # 交换 sp 和 mscratch
# 现在 sp = 内核栈, mscratch = 任务栈
addi sp, sp, -CONTEXT_SIZE
sw ra, 0(sp)
sw t0, 4(sp)
# ... 保存其他寄存器到内核栈 ...
# 处理中断(C 调用)
call trap_handler_c
# 恢复
lw ra, 0(sp)
# ...
addi sp, sp, CONTEXT_SIZE
csrrw sp, mscratch, sp # 换回:sp=任务栈, mscratch=内核栈
mret
关键点:
csrrw sp, mscratch, sp原子交换两个 sp------sp 变成内核栈,mscratch 暂存任务栈- 中断处理全程在内核栈上
- 退出前再交换回来,sp 恢复任务栈
RISC-V 把切栈交给软件,灵活但每个细节你要负责。ARM 上这些硬件管,你写 handler 像写普通函数;RISC-V 上你必须在 trap entry/exit 显式处理。这是 ④trap 专题的核心难点,也是 RISC-V bare-metal 比 ARM 难的地方。
七、mie / mip:中断使能与挂起
mie(0x304,读写):
bit 3: MSIE 软件中断使能
bit 7: MTIE 定时器中断使能
bit 11: MEIE 外部中断使能
bit 8-12: 各特定外部中断(看实现)
mip(0x344,只读):
bit 3: MSIP 软件中断挂起
bit 7: MTIP 定时器中断挂起
bit 11: MEIP 外部中断挂起
- mie 是开关,mip 是状态。某中断要触发,需要 mie 对应位=1 且 mip 对应位=1 且 mstatus.MIE=1。
- mip 大部分位只读(由硬件/CLINT/PLIC 设),MSIP 可写(用于 IPI:一个核写另一个核的 MSIP 位触发软件中断)。
- 关全局中断:csrci mstatus, 0x8(清 MIE 位)。开:csrsi mstatus, 0x8。
嵌入式视角:这就是 FreeRTOS portDISABLE_INTERRUPTS 的实现
FreeRTOS RISC-V port 里
portDISABLE_INTERRUPTS()就是清 mstatus.MIE,portENABLE_INTERRUPTS()就是置 mstatus.MIE。你调的 ENTER_CRITICAL/EXIT_CRITICAL,底层就是这两条 csr 指令。临界区=临时关 MIE,保证不被中断打断。
八、S-mode / U-mode CSR(选读,知道边界)
S 级有自己一套 CSR(sstatus/stvec/sepc/scause/stval/sscratch/sie/sip/satp),结构对应 M 级但权限受限。U 级几乎没有 CSR(只有几个计时器读取)。
| M 级 CSR | S 级对应 | 用途 |
|---|---|---|
| mstatus | sstatus | 状态(S 级版) |
| mtvec | stvec | S 级 trap 入口 |
| mepc | sepc | --- |
| mcause | scause | --- |
| mscratch | sscratch | S 栈切换 |
| mie/sip | sie/sip | S 级中断 |
| (无) | satp | 页表基址(S 级 MMU 关键) |
satp 是 S 级独有,M 级没有(因为 M 不用页表)。跑 Linux 时 M 级 bootloader 切到 S 级前要配好 satp。你做 MCU bare-metal 不碰 satp,但 AEGIX 跑 Linux 要碰。
M 级能访问 S 级 CSR 吗
能。M 级全权,能读写所有级别的 CSR(M/S/U)。反过来 S 级只能读 S 和部分 U,不能读 M。这是特权架构的核心:高级别能管低级别,低级别受限。
九、CSR 速查总表(打印贴墙)
| CSR | 地址 | 一句话 |
|---|---|---|
| mstatus | 0x300 | 状态总开关(MIE/MPIE/MPP) |
| misa | 0x301 | CPU 支持什么扩展 |
| mie | 0x304 | 中断使能(MSIE/MTIE/MEIE) |
| mtvec | 0x305 | trap 入口(BASE+MODE) |
| mscratch | 0x340 | 暂存(切栈用) |
| mepc | 0x341 | trap PC(死机指令地址) |
| mcause | 0x342 | trap 原因(Int+Code) |
| mtval | 0x343 | trap 附加(出错地址) |
| mip | 0x344 | 中断挂起(只读) |
| mhartid | 0xF14 | 核 ID |
| mcycle | 0xB00 | 周期计数器(性能剖析) |
| pmpcfg0 | 0x3A0 | PMP 配置 0 |
| pmpaddr0 | 0x3B0 | PMP 地址 0 |
| satp | 0x180 | S 级页表基址 |
十、本篇小结
- CSR 是 M-mode 控制面板,用 csrrw/csrrs/csrrc 读写,改位用 csrrs/csrrc 原子操作
- mstatus 是核心:MIE(中断总开关)/MPIE(trap 前的 MIE)/MPP(trap 前的模式)
- mtvec 配 trap 入口,direct(统一入口)或 vectored(按 cause 分散)
- 死机三件套:mepc(地址)+mcause(原因)+mtval(附加),配 mstatus.MPP(模式)→ 排查五步法
- mscratch 暂存寄存器,核心用途是中断栈切换(csrrw sp,mscratch,sp)
- mie/mip 是中断使能/挂起,关中断清 mstatus.MIE
- M 级能访问所有 CSR(M/S/U),S 级只能访问 S 和部分 U
- S 级独有 satp(页表),跑 Linux 才用
速查表
| 想干啥 | CSR/指令 |
|---|---|
| 关全局中断 | csrci mstatus, 8(清 MIE bit3) |
| 开全局中断 | csrsi mstatus, 8 |
| 配 trap 入口 | csrw mtvec, &trap_vector |
| 读死机地址 | csrr t0, mepc |
| 读死机原因 | csrr t0, mcause |
| 读出错地址 | csrr t0, mtval |
| 读 trap 前模式 | csrr t0, mstatus; (>>11)&3 |
| 切中断栈 | csrrw sp, mscratch, sp |
| 置某中断使能 | csrs mie, (1<<bit) |
| 看中断挂起 | csrr t0, mip |
| 周期计数 | csrr t0, mcycle |
| 核 ID | csrr t0, mhartid |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。