只需3个函数就够了:rv核心API速查指南(rv_init / rv_step / rv_irq 快速上手清单)
【免费下载链接】rv32-bit RISC-V CPU in ~800 lines of C89项目地址: https://gitcode.com/gh_mirrors/rv/rv
rv是一个用约 800 行 C89 代码实现的 32 位 RISC-V CPU 模拟器(RV32IMAC),完整支持 M 模式与 S 模式,甚至可以直接引导启动 RISC-V 32 位 Linux。它的 API 设计极简:rv_init、rv_step、rv_irq三个函数,再加上你自己提供的一个内存回调,就能在一台"软件 CPU"上跑起真实程序。本文是面向新手的 rv 核心 API 速查清单。
rv 是什么?先花 30 秒了解
在写代码之前,你需要知道 rv 的定位:
| 特性 | 说明 |
|---|---|
| 指令集 | RV32IMAC_Zicsr(含原子、压缩、乘除法扩展) |
| 特权级 | 支持 M-mode 与 S-mode,含完整 CSR 和 TLB |
| 代码量 | 约 800 行 C89,无内存分配,无 64 位整数 |
| API | 2 个必调函数 + 1 个中断函数 + 1 个你写的内存回调 |
| 战绩 | 通过 riscv-tests 全部支持项,可启动 RISC-V32 Linux |
核心文件只有两个:接口定义在 rv.h,全部实现在 rv.c。
rv_init 初始化:一行完成 CPU 复位
void rv_init(rv *cpu, void *user, rv_bus_cb bus_cb);这是使用 rv 的第一步,它做了三件事:
- 清零整个 CPU 状态——32 个通用寄存器
r[32]、程序计数器、所有 CSR; - 设置复位向量——PC 指向
0x80000000(RISC-V 惯例的 RAM 基址); - 写入身份特征——设置
misa寄存器声明支持 I/M/A/C 扩展和 U/S 模式,初始特权级为 M-mode。
两个参数怎么传:
user:一个不透明指针,会在每次内存访问时原样传回你的回调函数,通常传内存数组的首地址;bus_cb:你自己实现的总线回调函数(下文详解)。
💡 小技巧:对同一个cpu再次调用rv_init就是一次完整的 CPU 复位,调试死循环或挂死状态时非常有用。
rv_step 单步执行:模拟一颗"软 CPU"的心脏
rv_u32 rv_step(rv *cpu);调用一次 = CPU 执行一条指令。它内部完成取指(含 C 扩展半字取指)、译码、执行、访存全流程,并自动:
- 累加
cycle计数器; - 处理页表翻译(satp/TLB);
- 若
mip有挂起中断,按从高到低优先级自动响应(见 rv.c 中的rv_service)。
返回值速查表
| 返回值 | 含义 |
|---|---|
RV_TRAP_NONE | 正常执行完毕,继续下一步 |
RV_TRAP_WFI | 指令是wfi(等待中断),CPU 在此"打盹" |
RV_E* | 发生异常,编码即 rv.h 中定义的异常号 |
常用异常号一览(完整列表见 rv.h):
| 宏 | 编号 | 场景 |
|---|---|---|
RV_EBP | 3 | 执行到ebreak断点 |
RV_EMECALL | 11 | M-mode 执行ecall(系统调用) |
RV_ESECALL | 9 | S-mode 执行ecall |
RV_EILL | 2 | 非法指令 |
最常见的写法就是"跑到 ecall 为止":
while (rv_step(&cpu) != RV_EMECALL) { }rv_irq 触发中断:给 CPU 按响铃
void rv_irq(rv *cpu, rv_cause cause);RISC-V 的中断不是立即打断执行的——rv_irq只是置位mip寄存器(机器中断挂起),真正响应发生在下一条rv_step中,且需要对应mie使能位打开才会陷入。三种中断源:
| 宏 | 值 | 名称 | 典型来源 |
|---|---|---|---|
RV_CSI | 8 | Supervisor Software | SBI / OS 软件中断 |
RV_CTI | 128 | Supervisor Timer | CLINT 定时器到点 |
RV_CEI | 512 | Supervisor External | PLIC 外部设备(UART 等) |
中断优先级由rv_service按编号从大到小扫描(Timer > External > Software),并根据mideleg决定陷入 S-mode 还是 M-mode。
唯一的"第四函数":内存回调 rv_bus_cb
rv 自己不管理内存,所有读写都交给你的回调:
rv_res bus_cb(void *user, rv_u32 addr, rv_u8 *data, rv_u32 is_store, rv_u32 width);- 读:从
addr拷贝width字节到data;写:反向拷贝; - 地址越界返回
RV_BAD,rv 会将其转化为相应的 load/store fault 异常。
完整可运行的最小示例在 tools/example/example.c:一块 64KB 内存 + 2 条机器码(add a7, a7, 42后接ecall),约 30 行就把 CPU 跑起来了。编译它只需gcc -o example example.c rv.c。
三个函数串起来:完整执行循环
rv cpu; rv_init(&cpu, mem, bus_cb); /* 1. 复位 */ memcpy(mem, program, size); /* 加载程序到 0x80000000 */ for (;;) { rv_u32 res = rv_step(&cpu); /* 2. 执行一条指令 */ if (res == RV_EMECALL) break; /* 3. 捕获系统调用 */ if (res == RV_TRAP_WFI) /* 4. CPU 休眠时喂中断 */ rv_irq(&cpu, RV_CTI); }这套init → step 循环 → irq 喂中断的骨架,正是 tools/linux/mach.c 引导 Linux 的全部原理——配合 CLINT、PLIC、UART 三个小设备驱动(同目录),就能跑起真实的 RISC-V32 Linux,文档见 tools/linux/README.md。
新手上手清单
- ✅ 定义一块内存数组 + 一个越界检查版
bus_cb - ✅
rv_init传入内存首地址 - ✅ 把机器码 / 二进制镜像加载到
0x80000000 - ✅
while循环里调用rv_step,用返回值判断异常 - ✅ 需要中断时(wfi、定时器、UART)调用
rv_irq - ✅ 调试卡死:重调
rv_init复位,或直接打印cpu.pc/cpu.csr.mepc
总结:rv 的 API 哲学就是"CPU 状态全在一个rv结构体里,你能做的只有初始化、单步、给中断"。三个函数加一个回调,约 800 行代码,是你理解 RISC-V 指令执行、CSR、中断与页表机制的最佳入口。
【免费下载链接】rv32-bit RISC-V CPU in ~800 lines of C89项目地址: https://gitcode.com/gh_mirrors/rv/rv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考