
RISC-V 这几年的热度不用我多说从 MCU 到应用级 SoC从学术圈到工业界到处都能看到它的身影。但真正上手做 CPU 指令集验证的时候很多人第一步就卡住了——riscv-tests 这个官方测试套件看起来就是 clone 下来跑个 make 的事实际上手才发现工具链版本对不上、仿真环境跑不通、测试用例加载地址冲突、签名比对失败……各种问题一个接一个。我自己在带团队做 RV32I/RV64GC 核验证的时候前前后后踩了不下十几个坑有些坑甚至卡了整整两天才定位到根因。这篇内容就是把这些经验完整地梳理出来从环境搭建、工具链选型、riscv-tests 的编译机制、仿真环境对接到测试结果比对和常见报错的排查链路一步步讲清楚。不管你是刚接触 RISC-V 验证的新手还是已经跑过几轮回归测试但总有些 case 莫名其妙挂掉的老手应该都能从中找到有用的东西。1. 先搞清楚 riscv-tests 到底在测什么1.1 它不是普通的单元测试框架很多人第一次接触 riscv-tests会下意识把它当成类似 Google Test 或者 pytest 那样的测试框架。这个理解偏差会导致后面一系列困惑。riscv-tests 本质上是一组裸机bare-metal汇编测试用例每个测试用例都是一个独立的 RISC-V 汇编程序编译成 ELF 文件后放到目标 CPU 上运行。它不依赖操作系统不需要 C 运行时库甚至连栈都是自己设置的。每个测试用例的结构大致是这样的先做一些初始化设置栈指针、配置 CSR然后执行一系列指令操作最后把结果写到一个特定的内存地址或者寄存器里再通过ecall触发环境调用告诉测试框架“我跑完了结果是 pass 还是 fail”。这个机制的核心在于riscv-tests/env/目录下的环境代码。对于不同的目标比如p代表物理内存裸机环境v代表虚拟内存环境env 目录提供了不同的启动代码和 trap handler。你编译的时候选的--target参数实际上就是在选这套环境代码。1.2 测试用例的分类逻辑riscv-tests 的isa/目录下按指令集扩展分了很多子目录我列一下常见的目录测试内容典型用途rv32ui/rv64ui基础整数指令验证 RV32I/RV64I 基本功能rv32um/rv64um乘除法指令验证 M 扩展rv32ua/rv64ua原子指令验证 A 扩展rv32uf/rv64uf单精度浮点验证 F 扩展rv32ud/rv64ud双精度浮点验证 D 扩展rv32uc/rv64uc压缩指令验证 C 扩展rv32si/rv64si特权态指令验证 S 模式相关rv32mi/rv64mi机器态指令验证 M 模式 CSR 等每个目录下的测试用例命名也有规律。比如add.S测的是 ADD 指令的基本功能addi.S测 ADDIlw.S测 LW 加载。还有一些带后缀的比如jal-01.S、jal-02.S表示同一个指令的不同测试场景。这里有个容易忽略的点不是所有测试用例都适合你的核。比如你的核只实现了 RV32I那rv32um下面的乘除法测试你根本跑不了编译出来执行会直接触发非法指令异常。所以第一步要根据你的核实际支持的扩展筛选出对应的测试目录。1.3 签名比对机制的工作原理riscv-tests 判断测试通过与否靠的是一套叫signature comparison的机制。具体流程是这样的测试用例在运行过程中会把关键的计算结果按顺序写入一段连续的内存区域这段区域叫 signature 区。测试结束后仿真环境或者测试框架会把这段内存 dump 出来和预先计算好的参考签名reference signature做逐字节比对。如果完全一致说明所有指令的执行结果都正确如果有差异就说明某条指令的行为和预期不符。参考签名是怎么来的是在编译 riscv-tests 的时候用 SpikeRISC-V 官方参考模拟器跑一遍生成的。所以整个验证链路是Spike 跑出黄金参考结果 → 你的 DUT 跑出实际结果 → 两者比对。这个机制的好处是你不需要为每个测试用例单独写判断逻辑只要比对签名就行。但坏处也很明显一旦签名对不上你只知道“结果不对”不知道具体是哪条指令出了问题。后面我会讲怎么定位这种问题。2. 工具链选型别在这上面浪费时间2.1 为什么工具链是最容易出问题的环节RISC-V 的工具链生态目前比较分散有 GNU 工具链riscv64-unknown-elf-gcc、LLVM/Clang、还有各种厂商自己维护的版本。riscv-tests 官方推荐用 GNU 工具链但 GNU 工具链本身又有多个来源SiFive 的 freedom-tools、官方 riscv-gnu-toolchain 仓库、Ubuntu apt 里的版本、还有各种第三方预编译包。我踩过最坑的一次是用了 Ubuntu 22.04 apt 自带的gcc-riscv64-unknown-elf版本是 10.3编译 riscv-tests 的时候汇编器报了一堆奇怪的错误查了半天发现是这个版本的汇编器对某些伪指令的支持和 riscv-tests 源码不兼容。换成官方 riscv-gnu-toolchain 编译出来的 13.x 版本就一切正常。所以我的建议很明确不要用系统包管理器里的 RISC-V 工具链自己从源码编译官方 riscv-gnu-toolchain。虽然编译一次要等挺久取决于机器性能半小时到两小时不等但能省掉后面无数莫名其妙的兼容性问题。2.2 编译工具链的完整步骤和关键配置从源码编译 riscv-gnu-toolchain 的步骤大致如下# 安装依赖Ubuntu/Debian sudo apt-get install autoconf automake autotools-dev curl python3 \ libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex \ texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev # 克隆仓库注意要递归克隆子模块 git clone --recursive https://github.com/riscv/riscv-gnu-toolchain cd riscv-gnu-toolchain # 配置安装到 /opt/riscv目标为 RV64GC ./configure --prefix/opt/riscv --enable-multilib # 编译newlib 版本适合裸机 make -j$(nproc)这里有几个关键点需要说明--enable-multilib要不要开如果你只需要 RV32I 或者 RV64GC 中的一种可以不开编译会快很多。但如果你要同时验证 32 位和 64 位的核建议开启这样一套工具链就能覆盖多种架构组合。--prefix选哪里我习惯装到/opt/riscv然后把这个路径加到PATH里。不建议装到用户目录下因为团队协作的时候路径不统一会很麻烦。编译 newlib 还是 linux 版本riscv-tests 是裸机测试用 newlib 版本就够了。linux 版本编译时间更长而且 riscv-tests 用不上。编译完成后验证一下export PATH/opt/riscv/bin:$PATH riscv64-unknown-elf-gcc --version # 应该输出类似riscv64-unknown-elf-gcc (gc891d8dc23e) 13.2.02.3 Spike 和 pk 的安装除了工具链你还需要 Spike 模拟器。Spike 是 RISC-V 的官方参考实现用来生成黄金签名。安装步骤git clone https://github.com/riscv-software-src/riscv-isa-sim cd riscv-isa-sim mkdir build cd build ../configure --prefix/opt/riscv make -j$(nproc) make install如果你还需要跑 pkproxy kernel来运行更复杂的测试那还要装 riscv-pkgit clone https://github.com/riscv-software-src/riscv-pk cd riscv-pk mkdir build cd build ../configure --prefix/opt/riscv --hostriscv64-unknown-elf make -j$(nproc) make install注意Spike 和 pk 的版本最好和工具链保持同一时期避免因为 ABI 变化导致链接错误。我遇到过 Spike 版本太老、不支持某些新指令的情况导致签名生成阶段就失败了。3. 编译 riscv-testsMakefile 里的门道3.1 环境变量配置的正确姿势riscv-tests 的编译依赖几个环境变量设置不对就会各种报错。最核心的是这三个export RISCV/opt/riscv export PATH$RISCV/bin:$PATH export RISCV_TESTS/path/to/riscv-testsRISCV指向工具链安装目录PATH里要包含工具链的 bin 目录。有些版本的 riscv-tests Makefile 还会读RISCV_PREFIX默认是riscv64-unknown-elf-如果你用的是其他前缀比如riscv32-unknown-elf-需要显式设置。然后进入 riscv-tests 目录先初始化子模块cd riscv-tests git submodule update --init --recursive这一步很多人会忘导致env/目录是空的编译的时候报找不到链接脚本。3.2 编译特定测试集的命令拆解riscv-tests 的 Makefile 支持通过XLEN和RISCV_TARGET等变量来控制编译目标。比如我要编译 RV64I 的基础整数测试make XLEN64 RISCV_TARGETriscv64-unknown-elf isa/rv64ui这条命令背后做了这些事情遍历isa/rv64ui/目录下所有.S文件对每个文件调用riscv64-unknown-elf-gcc编译链接env/p/下的启动代码和链接脚本生成 ELF 文件到isa/rv64ui/目录下同时调用 Spike 生成参考签名文件.signature后缀如果你想编译所有测试集make XLEN64但这会编译 rv64ui、rv64um、rv64ua、rv64uf、rv64ud、rv64uc、rv64si、rv64mi 等所有目录时间比较长。建议按需编译。3.3 链接脚本和内存布局的坑riscv-tests 默认的链接脚本在env/p/link.ld它定义了测试程序的内存布局。默认的起始地址是0x80000000这是 RISC-V 裸机环境常见的内存起始地址。但你的 DUT 或者仿真环境的内存映射可能不是这个地址。比如有些 FPGA 原型验证平台把 DDR 映射到0x80000000有些映射到0x00000000还有些自定义的地址。如果链接地址和实际加载地址不一致程序跑起来会直接飞掉。解决办法有两种方法一修改链接脚本。直接改env/p/link.ld里的起始地址重新编译。这个方法的缺点是每次换平台都要改容易忘。方法二编译时覆盖。通过 Makefile 变量传入自定义链接脚本make XLEN64 RISCV_TARGETriscv64-unknown-elf \ RISCV_LINK_SCRIPT/path/to/your/link.ld isa/rv64ui我一般用方法二把不同平台对应的链接脚本放在独立目录下管理编译时按需指定。提示链接脚本里除了起始地址还要注意栈指针的初始值。riscv-tests 的启动代码会把栈指针设置到_end符号附近如果你的内存空间不够大栈可能会溢出到代码段。4. 仿真环境对接从 Spike 到你的 DUT4.1 用 Spike 做冒烟测试在把测试用例放到真实 DUT 之前强烈建议先用 Spike 跑一遍确认测试本身没问题。命令很简单spike /opt/riscv/riscv64-unknown-elf/bin/pk \ isa/rv64ui/add-01或者直接跑裸机 ELFspike isa/rv64ui/add-01Spike 会执行测试程序最后输出类似PASS或者具体的失败信息。如果 Spike 都跑不过那说明测试编译有问题先别急着上 DUT。4.2 对接自定义仿真环境的接口设计把 riscv-tests 对接到你自己的仿真环境比如 Verilator、VCS、或者 FPGA 原型核心是要实现一个testbench 侧的签名 dump 逻辑。具体来说仿真环境需要能加载 ELF 文件把代码段和数据段放到正确的内存地址需要监控测试程序是否执行到ecall指令这是测试结束的标志测试结束后从 signature 区读取数据和参考签名比对signature 区的起始地址和大小在链接脚本里定义通常是begin_signature和end_signature两个符号之间的区域。你可以在仿真环境里通过解析 ELF 文件的符号表获取这两个地址。一个典型的 testbench 签名 dump 流程// 伪代码示意 initial begin load_elf(isa/rv64ui/add-01); wait_for_ecall(); dump_signature(begin_signature_addr, end_signature_addr, add-01.signature); compare_signature(add-01.signature, add-01.reference_signature); end4.3 常见对接问题与排查对接过程中最常见的问题有这么几类问题一程序加载后 PC 不对。检查 ELF 的入口地址和你的 CPU reset vector 是否一致。riscv-tests 的入口通常是_start符号地址由链接脚本决定。问题二ecall 没有被正确捕获。有些仿真环境的 trap 处理逻辑不完善ecall 触发后 CPU 进入了异常处理但 testbench 没检测到。建议在 testbench 里直接监控指令总线上的 ecall 编码0x00000073。问题三签名区数据读出来全是 0 或者全是 X。这通常是内存初始化或者加载路径的问题。先确认 ELF 加载是否成功再检查 signature 区的地址是否被正确解析。5. 签名比对失败时的排查链路5.1 先确认是编译问题还是执行问题签名对不上第一步要区分是“测试本身编译错了”还是“DUT 执行错了”。方法很简单用 Spike 跑同一个 ELF看 Spike 的签名和参考签名是否一致。如果 Spike 也对不上那就是编译阶段的问题可能是工具链版本不兼容或者链接脚本配置错误。如果 Spike 对得上但 DUT 对不上那就是 DUT 的问题。5.2 逐指令定位差异点确认是 DUT 问题后下一步是定位具体是哪条指令执行错了。riscv-tests 的签名是按顺序写入的每个测试用例写入的字节数和顺序都是确定的。你可以通过比对签名文件中第一个不一致的字节位置反推出是哪条指令的结果出了问题。具体做法在 Spike 里开启指令 trace把每条指令的 PC、编码、执行结果都打印出来。然后在你的 DUT 仿真环境里也开同样的 trace两者逐条比对。第一条结果不一致的指令就是问题所在。# Spike 开启指令 trace spike --log-commits -l --logspike.log isa/rv64ui/add-01Spike 的 log 格式很清晰每行包含 PC、指令编码、寄存器读写信息。你的 DUT 仿真环境如果用的是 Verilator 或者 VCS也可以通过$display或者波形来获取类似信息。5.3 几类高频失败原因根据我的经验签名比对失败的原因分布大致是这样的失败原因占比典型表现指令译码错误35%特定指令结果全错流水线冒险处理不当25%结果间歇性错误CSR 读写行为不符15%特权态测试失败内存对齐处理错误10%load/store 相关测试失败异常处理逻辑错误10%trap 相关测试失败其他5%各种边界情况指令译码错误是最常见的尤其是新实现的指令或者不常用的指令变体。比如slli指令在 RV64 下移位量是 6 位在 RV32 下是 5 位如果译码逻辑写错了RV64 测试就会挂。流水线冒险问题比较隐蔽因为单条指令测试可能过但连续执行多条指令时就出问题。这类问题通常需要看波形才能定位。6. 让验证效率翻倍的几个实操技巧6.1 批量回归脚本的编写手动一个个跑测试用例效率太低写个批量脚本是必须的。我用 Python 写了一个简单的回归框架核心逻辑是import subprocess import os import glob def run_test(elf_path, simulator): 运行单个测试并返回结果 result subprocess.run( [simulator, elf_path], capture_outputTrue, textTrue, timeout60 ) return PASS in result.stdout def regression(test_dir, simulator): 批量回归 elf_files glob.glob(os.path.join(test_dir, *.elf)) passed, failed 0, [] for elf in elf_files: if run_test(elf, simulator): passed 1 else: failed.append(os.path.basename(elf)) print(f通过: {passed}/{len(elf_files)}) if failed: print(失败用例:, failed) return failed这个脚本可以对接 Spike也可以对接你自己的仿真器只要仿真器支持命令行加载 ELF 并输出 PASS/FAIL。6.2 用 CI 做持续验证如果团队有 Git 仓库建议把 riscv-tests 回归集成到 CI 流程里。每次 RTL 代码有改动自动触发一轮回归及时发现回归问题。GitLab CI 的配置大概长这样riscv-tests-regression: stage: test script: - export PATH/opt/riscv/bin:$PATH - cd riscv-tests make XLEN64 - python3 regression.py --dir isa/rv64ui --sim spike only: - merge_requests这样每次 MR 都会自动跑一遍基础指令集测试防止改坏已有功能。6.3 自定义测试用例的扩展方法riscv-tests 自带的用例覆盖了大部分标准指令但你的核可能有自定义扩展指令或者你想测试一些特定的边界场景。这时候就需要自己写测试用例。写自定义测试用例的步骤在isa/rv64ui/下新建一个.S文件比如mycustom.S参考现有用例的结构包含TEST_RR_OP或者TEST_IMM_OP等宏在 Makefile 的对应目录列表里加上你的文件名重新编译Spike 会自动生成参考签名宏定义在env/encoding.h和isa/macros/scalar/test_macros.h里常用的有TEST_RR_OP(name, inst, result, rs1, rs2)测试寄存器-寄存器型指令TEST_IMM_OP(name, inst, result, rs1, imm)测试立即数型指令TEST_LD_OP/TEST_ST_OP测试加载/存储指令提示自定义测试用例的签名生成依赖 Spike 支持你的自定义指令。如果 Spike 不支持你需要先扩展 Spike 的指令实现或者手动构造参考签名。7. 关于验证覆盖率和测试充分性的思考跑通 riscv-tests 只是指令集验证的起点不是终点。riscv-tests 的用例设计偏向功能正确性对边界条件、随机场景、并发冲突的覆盖有限。我在实际项目中的做法是分三层来做验证第一层是 riscv-tests 基础回归确保每条指令的基本功能正确。这一层是必须过的过不了后面都不用谈。第二层是随机指令流测试用工具生成大量随机指令序列在 Spike 和 DUT 上同时执行比对最终架构状态。这一层能发现很多 riscv-tests 覆盖不到的 corner case。第三层是定向边界测试针对特定模块比如乘法器的溢出、浮点数的舍入模式、CSR 的读写冲突写专门的测试。这一层需要根据你的核的具体实现来定制。riscv-tests 在第一层的作用不可替代但千万别以为跑通了 riscv-tests 就万事大吉了。我见过太多项目 riscv-tests 全过一跑真实程序就各种挂的情况。8. 几个让我印象深刻的踩坑实录说几个具体的踩坑经历都是真实发生过的希望能帮你少走弯路。坑一工具链版本导致的伪指令展开差异。有一次团队里两个人用不同版本的工具链编译同一份测试一个人跑出来 PASS另一个人跑出来 FAIL。查了半天发现是两个版本的汇编器对li伪指令的展开方式不同一个展开成luiaddi另一个展开成luiori。虽然功能等价但签名区写入的中间结果不同导致比对失败。解决办法就是统一工具链版本写进项目文档里。坑二链接地址冲突导致的静默错误。有一次测试程序加载后不报错但签名全是 0。查了两天才发现是链接脚本里的起始地址和仿真环境的内存映射差了一个偏移程序被加载到了错误的位置CPU 取到的全是空指令。这种问题不会报错只会静默失败特别难查。后来我养成了一个习惯每次换仿真环境先用一个最简单的add测试确认加载地址正确。坑三Spike 版本不支持新指令。在验证一个带自定义扩展的核时Spike 跑测试直接报非法指令。原因是 Spike 版本太老不认识我们自定义的指令编码。解决办法是扩展 Spike 的指令译码表重新编译。这个工作量不小但没办法参考模型必须支持所有待验证的指令。坑四ecall 处理逻辑的差异。有些测试用例在 Spike 上跑能正常结束在 DUT 上跑却卡死。排查发现是 DUT 的 ecall 异常处理逻辑和 Spike 不一致——Spike 把 ecall 当作正常的 trap 处理而 DUT 的 trap handler 实现有 bug进入异常后没有正确更新 mepc 和 mcause导致程序跑飞。这类问题需要对照 RISC-V 特权态规范逐条检查 CSR 行为。坑五多核场景下的签名区竞争。在验证多核 CPU 时多个核同时往同一个 signature 区写数据导致签名内容错乱。解决办法是给每个核分配独立的 signature 区或者在测试用例里加同步机制。riscv-tests 本身是为单核设计的多核场景需要自己扩展。这些坑的共同特点是表面现象相似都是签名对不上但根因完全不同。所以排查的时候一定要有系统性的方法从编译、加载、执行、比对四个环节逐一排除不要一上来就怀疑 RTL 代码。9. 环境搭建的自动化脚本最后分享一个我常用的环境搭建脚本把前面说的所有步骤串起来一键完成工具链编译、Spike 安装、riscv-tests 编译#!/bin/bash set -e INSTALL_DIR/opt/riscv WORK_DIR$HOME/riscv-work JOBS$(nproc) mkdir -p $WORK_DIR cd $WORK_DIR # 1. 编译工具链 if [ ! -d $INSTALL_DIR/bin ]; then git clone --recursive https://github.com/riscv/riscv-gnu-toolchain cd riscv-gnu-toolchain ./configure --prefix$INSTALL_DIR --enable-multilib make -j$JOBS cd $WORK_DIR fi export PATH$INSTALL_DIR/bin:$PATH # 2. 编译 Spike if [ ! -f $INSTALL_DIR/bin/spike ]; then git clone https://github.com/riscv-software-src/riscv-isa-sim cd riscv-isa-sim mkdir -p build cd build ../configure --prefix$INSTALL_DIR make -j$JOBS make install cd $WORK_DIR fi # 3. 编译 riscv-tests if [ ! -d riscv-tests ]; then git clone https://github.com/riscv-software-src/riscv-tests cd riscv-tests git submodule update --init --recursive make XLEN64 cd $WORK_DIR fi echo 环境搭建完成 echo 工具链路径: $INSTALL_DIR/bin echo 测试用例路径: $WORK_DIR/riscv-tests/isa这个脚本我用了很多次在新机器上从零开始搭建环境大概需要一到两个小时主要时间花在编译工具链上。建议把编译好的工具链打包备份换机器的时候直接解压省去重复编译的时间。环境搭好之后先用spike isa/rv64ui/add-01跑一个最简单的测试确认整条链路通畅然后再逐步接入你的 DUT 仿真环境。这个顺序很重要不要一上来就对接复杂环境出了问题很难定位是哪个环节的锅。