1. RISC-V 架构基础:指令集、特权模式与汇编入门
各位同学,欢迎来到第一课。做嵌入式开发这么多年,我接触过 ARM、MIPS、x86,但 RISC-V 给我的感觉最特别。它不像一个商业产品,更像一套开放的标准——你完全可以按需裁剪。今天我们就从最核心的三个点入手:指令集长什么样、特权层级怎么分、汇编怎么写。
1.1 RISC-V 指令集概述
RISC-V 是一个模块化的指令集架构。什么叫模块化?说白了,它有一个最小的核心指令集(RV32I / RV64I),然后你可以往上加扩展模块,比如乘除法(M)、原子操作(A)、单精度浮点(F)、双精度浮点(D)等等。
| 扩展名 | 全称 | 典型指令数 | 适用场景 |
|---|---|---|---|
| RV32I | 32位整数基础集 | 47条 | 所有处理器必备 |
| M | 乘除法扩展 | 8条 | 需要硬件乘除的MCU |
| A | 原子操作扩展 | 11条 | 多核/多线程同步 |
| F/D | 单/双精度浮点 | 26/26条 | DSP、科学计算 |
| C | 压缩指令扩展 | 46条 | 代码密度敏感场景 |
核心要点:RV32I 只有 47 条指令,每条指令长度固定为 32 位。这意味着译码器可以做得非常简单——我在做第一个 RISC-V 软核时,指令译码只用了不到 200 行 Verilog。
指令格式上,RISC-V 只有 6 种基本类型:R 型(寄存器-寄存器)、I 型(立即数)、S 型(存储)、B 型(分支)、U 型(高位立即数)、J 型(跳转)。每种格式的指令字段位置是固定的,译码器不需要查表就能拆出操作码和寄存器号。
# R 型指令格式示例:ADD x1, x2, x3
# [funct7][rs2][rs1][funct3][rd][opcode]
# 0000000 00011 00010 000 00001 0110011
# 这条指令把 x2 和 x3 相加,结果写入 x1
1.2 特权架构与机器模式
RISC-V 定义了三种特权模式:机器模式(M-mode)、监管模式(S-mode)、用户模式(U-mode)。嵌入式系统里,我们最常打交道的是机器模式——它是权限最高的模式,也是芯片上电后默认进入的模式。
我的经验:在裸机开发中,你几乎全程都跑在 M-mode 下。只有跑 Linux 或 RTOS 时,才会用到 S-mode 和 U-mode。我刚开始做 RISC-V 移植时,一直搞不清 M-mode 和 S-mode 的中断处理差异,后来踩了个坑——M-mode 的中断默认不经过 stvec,而是直接走 mtvec。这个区别在写中断向量表时特别重要。
机器模式下,你有一组控制状态寄存器(CSR),用来配置处理器核心行为。常用的几个:
- mtvec:机器模式中断向量基地址
- mstatus:机器模式状态寄存器,控制全局中断使能
- mie:机器模式中断使能寄存器,按位控制每种中断
- mepc:异常返回地址,发生异常时硬件自动写入
- mcause:异常原因寄存器,告诉你发生了什么异常
为什么会需要这么多 CSR?因为 RISC-V 把异常处理、中断控制、内存保护等功能全部通过 CSR 暴露给软件。你想想看,没有这些寄存器,你连关个中断都做不到。
注意:访问 CSR 指令是特权指令。在 M-mode 下你可以读写所有 CSR,但在 U-mode 下访问大部分 CSR 会触发非法指令异常。我曾经在调试一个用户态程序时,忘记关掉对 mcycle 的读取,结果程序一跑就崩——查了半天才发现是权限问题。
1.3 RISC-V 汇编基础
写驱动离不开汇编。至少你要能看懂启动代码、中断入口、上下文切换这几段。RISC-V 的汇编语法跟 GNU Assembler 风格一致,指令格式是:
操作码 目标寄存器, 源寄存器1, 源寄存器2/立即数
来看几个最常用的指令:
| 指令 | 示例 | 含义 |
|---|---|---|
| add | add t0, t1, t2 | t0 = t1 + t2 |
| addi | addi t0, t1, 5 | t0 = t1 + 5 |
| lw | lw t0, 0(sp) | 从 sp+0 加载字到 t0 |
| sw | sw t0, 4(sp) | 将 t0 存储到 sp+4 |
| beq | beq t0, t1, label | 如果 t0==t1 跳转到 label |
| jal | jal ra, func | 跳转到 func,返回地址存 ra |
| csrr | csrr t0, mcause | 读取 mcause 到 t0 |
| csrw | csrw mtvec, t0 | 将 t0 写入 mtvec |
寄存器命名上,RISC-V 有 32 个通用寄存器 x0~x31。x0 是硬编码的零寄存器,读它永远得 0,写它无效。其他寄存器有 ABI 名称,写汇编时建议用 ABI 名,代码可读性会好很多。
# 一个简单的启动代码片段
.section .text.init
.globl _start
_start:
# 设置栈指针
la sp, _stack_top
# 设置中断向量表
la t0, trap_handler
csrw mtvec, t0
# 使能全局中断
csrsi mstatus, 0x8
# 跳转到 main
jal ra, main
# 死循环
loop:
j loop
避坑指南:写 RISC-V 汇编时,最容易犯的错误是忘记 x0 是只读零寄存器。我曾经在写一个 memcpy 的内联汇编时,不小心把 x0 当成了临时寄存器用——结果数据全被清零了。另外,RISC-V 没有专门的压栈出栈指令,你需要手动操作 sp 和内存,记得在函数入口和出口成对使用。
最后,我们来看一张 RISC-V 架构的整体关系图,把今天讲的内容串起来:
这张图把今天的内容串起来了。指令集是基础,特权架构是规则,汇编是工具。三者缺一不可。后面的章节里,我们会反复用到这些知识——写 GPIO 驱动时要用 CSR 配置中断,写 UART 驱动时要理解 M-mode 下的异常处理流程。