只需3个函数就够了:rv核心API速查指南(rv_init / rv_step / rv_irq 快速上手清单)
2026/8/26 14:25:31 网站建设 项目流程

只需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_initrv_steprv_irq三个函数,再加上你自己提供的一个内存回调,就能在一台"软件 CPU"上跑起真实程序。本文是面向新手的 rv 核心 API 速查清单。

rv 是什么?先花 30 秒了解

在写代码之前,你需要知道 rv 的定位:

特性说明
指令集RV32IMAC_Zicsr(含原子、压缩、乘除法扩展)
特权级支持 M-mode 与 S-mode,含完整 CSR 和 TLB
代码量约 800 行 C89,无内存分配,无 64 位整数
API2 个必调函数 + 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 的第一步,它做了三件事:

  1. 清零整个 CPU 状态——32 个通用寄存器r[32]、程序计数器、所有 CSR;
  2. 设置复位向量——PC 指向0x80000000(RISC-V 惯例的 RAM 基址);
  3. 写入身份特征——设置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_EBP3执行到ebreak断点
RV_EMECALL11M-mode 执行ecall(系统调用)
RV_ESECALL9S-mode 执行ecall
RV_EILL2非法指令

最常见的写法就是"跑到 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_CSI8Supervisor SoftwareSBI / OS 软件中断
RV_CTI128Supervisor TimerCLINT 定时器到点
RV_CEI512Supervisor ExternalPLIC 外部设备(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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询