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 架构的整体关系图,把今天讲的内容串起来:

RISC-V 架构核心知识体系 指令集架构 RV32I 基础集 (47条) M/A/F/D/C 扩展 6种指令格式 固定长度 32位 模块化可裁剪 特权架构 M-mode (机器模式) S-mode (监管模式) U-mode (用户模式) CSR 控制寄存器组 异常与中断处理 汇编基础 32个通用寄存器 ABI 命名规范 指令操作码 CSR 读写指令 启动代码模板 三者关系 指令集定义处理器能执行哪些操作 特权架构决定不同模式下能访问哪些资源 汇编则是用指令实现控制逻辑的桥梁 典型应用场景 裸机驱动开发 → M-mode + RV32IMC RTOS 移植 → M-mode + S-mode + 原子扩展

这张图把今天的内容串起来了。指令集是基础,特权架构是规则,汇编是工具。三者缺一不可。后面的章节里,我们会反复用到这些知识——写 GPIO 驱动时要用 CSR 配置中断,写 UART 驱动时要理解 M-mode 下的异常处理流程。