
本篇定位: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 读写指令二、M-mode 核心 CSR 全表2.1 trap 相关(④专题详讲,这里先认脸)2.2 计时器/计数器2.3 PMP(物理内存保护,⑥专题)2.4 调试(⑩专题)三、mstatus 详解(最重要,没有之一)关键位详解其他位(选读)四、mtvec:trap 入口Direct 模式(常用,你大概率用这)Vectored 模式五、mepc / mcause / mtval:死机三件套mepc(0x341)mcause(0x342)mtval(0x343)六、mscratch:中断栈切换的关键(重点)为什么需要切栈切栈标准手法七、mie / mip:中断使能与挂起八、S-mode / U-mode CSR(选读,知道边界)九、CSR 速查总表(打印贴墙)十、本篇小结速查表一、CSR 读写指令四个指令族,都是csr开头:csrrw rd, csr, rs1 # rd csr; csr rs1 (读旧值到rd,写新值) csrrs rd, csr, rs1 # rd csr; csr csr | rs1 (置位,rs10则只读) csrrc rd, csr, rs1 # rd csr; csr csr ~rs1 (清位,rs10则只读) 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地址作用关键位mstatus0x300机器状态MIE/MPIE/MPPmisa0x301CPU 支持的扩展MXL31:30 Extensions[25:0]mie0x304中断使能MSIE/MTIE/MEIO 各外部中断mip0x344中断挂起(只读)MSIP/MTIP/MEIPmtvec0x305trap 入口地址BASE MODE[1:0]mscratch0x340暂存寄存器全 32 位,你随便用mepc0x341trap 时的 PC死机指令地址mcause0x342trap 原因Interrupt[31] Code[30:0]mtval0x343trap 附加信息出错地址/指令编码mcause例外——见下表mtval———mhartid0xF14当前核 ID多核启动用2.2 计时器/计数器CSR作用mcycle/mcycleh周期计数器(低32/高32),性能剖析用minstret/minstreth退休指令计数器mtime实时时钟(CLINT 提供,内存映射不是 CSR)mtimecmp定时器比较寄存器(触发 MTI)2.3 PMP(物理内存保护,⑥专题)CSR作用pmpcfg0-1516 个 PMP 区的配置(权限/模式)pmpaddr0-1516 个 PMP 区的地址2.4 调试(⑩专题)CSR作用dcsr调试控制和状态dpc调试 trap 的 PCdscratch0/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 级全局中断总开关。MIE1,M 级中断可触发MIE0,M 级中断被屏蔽trap 进 M 级时,硬件自动 MIE→0(防中断嵌套),trap 前的值存 MPIEMPIE(bit 7):trap 前的 MIE 值。trap 发生:MPIE ← MIE,MIE ← 0mret 返回:MIE ← MPIE,MPIE ← 1这个机制保证 mret 后中断状态恢复到 trap 前MPP[12:11]:trap 前的特权模式。00U, 01S, 11Mtrap 发生:MPP ← 当前模式mret 返回:模式 ← MPP这是判断死机时在哪个模式的关键——读 mstatus.MPP 就知道MPP 的两个特殊用法:软件想强制切到 S 模式:写 mstatus.MPP01,然后 mret初始化第一个从 M 切 S 的跳转:同样设 MPP01 再 mret其他位(选读)FS[14:13]:浮点单元状态(Off/Initial/Clean/Dirty)。关 FPU 省电、或裸机不用浮点时设 OffVS[10: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(Interrupt0):Code含义你要做什么0指令地址对齐错查函数指针/jalr 目标对齐1指令访问 faultPC 指向不可执行区2非法指令反汇编 mepc 处,看是不是跑飞到数据区3Breakpointebreak 或调试器4Load 地址对齐错lw/sw 地址没对齐5Load 访问 fault最常见,野指针/未初始化指针6Store 地址对齐—7Store 访问 fault写只读区/野指针8U 模式 ecall用户态系统调用9S 模式 ecall—11M 模式 ecall—12-15栈/页表相关—中断 Code(Interrupt1):Code含义3MSI(机器软件中断,IPI)7MTI(机器定时器中断,FreeRTOS tick)11MEI(机器外部中断,经 PLIC,I2C/UART 等)死机排查标准流程读 mepc → addr2line 定位代码行读 mcause → 知道是哪种异常/中断读 mtval → 看出错地址读 mstatus.MPP → 知道死机时在 M/S/U 哪个模式组合判断:如 mcause5(load fault) mtval0xDEADBEEF 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 软件切。切栈标准手法# 初始化时: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.MIE1。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 级 CSRS 级对应用途mstatussstatus状态(S 级版)mtvecstvecS 级 trap 入口mepcsepc—mcausescause—mscratchsscratchS 栈切换mie/sipsie/sipS 级中断(无)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地址一句话mstatus0x300状态总开关(MIE/MPIE/MPP)misa0x301CPU 支持什么扩展mie0x304中断使能(MSIE/MTIE/MEIE)mtvec0x305trap 入口(BASEMODE)mscratch0x340暂存(切栈用)mepc0x341trap PC(死机指令地址)mcause0x342trap 原因(IntCode)mtval0x343trap 附加(出错地址)mip0x344中断挂起(只读)mhartid0xF14核 IDmcycle0xB00周期计数器(性能剖析)pmpcfg00x3A0PMP 配置 0pmpaddr00x3B0PMP 地址 0satp0x180S 级页表基址十、本篇小结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.MIEM 级能访问所有 CSR(M/S/U),S 级只能访问 S 和部分 US 级独有 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, (1bit)看中断挂起csrr t0, mip周期计数csrr t0, mcycle核 IDcsrr t0, mhartid技术之路漫漫分享是为了更好地交流。如果本文的内容对你有启发希望能得到你的点赞 和收藏 ⭐。如果你在调试过程中遇到了其他问题欢迎在评论区 留言我们一起探讨。也欢迎关注 我一起交流底层开发的那些事儿。