ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

riscv-tests实战指南:用标准测试集打造你的CPU验证流程

riscv-tests实战指南:用标准测试集打造你的CPU验证流程 1. 为什么你的CPU设计需要一套标准测试集先说个我见过很多次的场景课程设计做到后半段数据通路和控制信号都调通了自己写的testbench里加法、减法、跳转都过了心里美滋滋准备交差。结果把别人的测例拿过来一跑PC乱跳、内存写错、访存对齐方式不对直接心态爆炸。原因其实很简单自己写的testbench是“验证自己理解的正确性”不是“验证指令集的正确性”。你写testbench的时候脑子里已经预设了“这条指令应该这样工作”所以测试向量往往是沿着你的实现思路去构造的。而riscv-tests这类标准测试集不一样它是由RISC-V工具链社区维护的、面向指令集规范ISA Spec的逐条指令验证套件每条测试都在检查你的CPU是否严格符合规范而不是符合你脑子里的“差不多”。对于正在做CPU设计的人来说不管是Verilog写单周期、多周期、流水线还是用Logisim搭数据通路riscv-tests都能当成一面“照妖镜”。它能帮你快速定位到某一条指令、某一种寻址模式、某一个控制信号的问题省去你自己构造边界case的时间。这篇东西我会从环境搭建、编译流程、仿真接入、结果判定到常见坑一条龙讲清楚文末还会拆解一个ISA测试用例的内部结构让你知道这些.S文件到底在跑些什么东西。内容同样适用于做MIPS CPU设计的同学思路完全可以迁移只是汇编层面的具体指令不一样而已。2. 动手前先搞清楚的几个基础概念2.1 为什么测试套件能“检查”你的CPUriscv-tests的验证思想其实很朴素它不是在CPU运行完之后告诉你“对”或者“错”而是通过一套约定的机制让CPU在执行完测试后用写内存的方式“汇报”结果。这套机制的核心叫tohost。简单理解测试程序在内存里预留了一个固定地址当所有测试用例都通过时程序会往这个地址写一个特殊值只要有任何一条用例失败就往这个地址写一个代表失败标记的非零值。你的仿真testbench只需要盯着这个地址就能判断整轮测试有没有通过。至于失败在哪一条指令、哪一个周期则需要靠波形和调试手段去定位。这个概念为什么重要因为CPU验证和普通软件验证有本质区别——普通软件挂了会告诉你异常退出CPU可不会主动说话它只会默默地把PC推到奇怪的地方或者把不该写的数据写进寄存器。有了tohost这种“硬件可读的测试结果”你的仿真环境才能自动化地一轮一轮跑测试而不是靠肉眼盯波形。2.2 理解riscv-tests的目录结构与测试分类源码拿到手之后你会看到一个riscv-tests目录里面几个子目录各有分工isa/指令集测试这是核心按rv32ui、rv32um、rv32ua、rv64ui等方式分类。u表示用户态i表示整数指令m表示乘除法扩展a表示原子操作c表示压缩指令。每个目录下是一堆.S汇编文件比如add.S、sub.S、lw.S、sw.S。benchmarks/基准性能测试比如求阶乘、快速排序、多字节乘法等适合CPU功能已经稳定之后做性能评估。dummy_rocc/ROCCRocket Custom Coprocessor扩展接口的示例一般课程设计用不到。对于大多数自制CPU第一个目标是把rv32ui目录下的测试跑通。这个目录里的每条测试对应一条基础指令从加法、减法、逻辑运算到访存、跳转、比较非常全面。只有把这些基础指令全部跑通才敢说自己的CPU“吃饱了饭”。2.3 你需要准备的工具链和仿真环境跑通riscv-tests需要三样东西RISC-V交叉编译工具链、仿真工具iverilog/Verilator或你自己的模拟器、以及一份riscv-tests源码。工具链是第一个门槛。有两种选择直接下载官方预编译的riscv64-unknown-elf工具链或者自己用crosstool-NG编译。我的建议是除非你是在深度定制工具链否则不要自己编译——编译一次全工具链少说半小时多则两小时而且中间遇到缺依赖的情况极其劝退。直接用预编译版省下的时间够你跑十轮测试了。仿真工具方面如果只是跑教学级的小CPUiverilog完全够用如果CPU规模比较大或者想跑benchmarks推荐用Verilator做高速仿真。riscv-tests本身不挑仿真器它只需要你的testbench能加载二进制文件、能驱动时钟复位、能读取tohost地址。3. 从拉源码到跑通第一个测例的完整流程3.1 获取并编译riscv-tests假设你已经装好了riscv64-unknown-elf-gcc并且把工具链的bin目录加进了PATH接下来按这个流程操作git clone https://github.com/riscv-software-src/riscv-tests.git cd riscv-tests git submodule update --init --recursive这里要注意riscv-tests依赖riscv-gnu-toolchain子模块里的部分脚本所以submodule一定要拉全。拉完之后编译工作通常在isa目录下进行cd isa make XLEN32这个命令会生成所有的32位测试目标文件。如果你只想编译某个特定测试可以指定具体目标make XLEN32 rv32ui-p-add编译完成后isa/目录下会出现一堆rv32ui-p-*.elf、rv32ui-p-*.bin、rv32ui-p-*.dump文件。elf是完整的可执行文件dump是对应的反汇编文本后面调波形的时候会频繁用到。很多testbench需要的是十六进制格式的初始内存文件可以用objcopy转换riscv64-unknown-elf-objcopy -O verilog rv32ui-p-add.elf rv32ui-p-add.hex或者转成mem格式riscv64-unknown-elf-elf2hex --bit-width 32 --input rv32ui-p-add.elfelf2hex这个工具不是所有环境都自带如果没有就看testbench支持哪种格式实在不行直接用-O binary转成bin在testbench里用$readmemh配合十六进制文本加载。总之这一步的目的是一样的把elf变成你的仿真环境能理解的内存内容。3.2 编译参数里藏着哪些关键信息编译过程中会用到很多参数初学者容易被吓到咱们拆开看riscv64-unknown-elf-gcc -marchrv32i -mabiilp32 -nostdlib -static -Tlink.ld -o rv32ui-p-add rv32ui-p-add.S-marchrv32i表示只生成RV32I基础整数指令不带乘除法扩展。如果你的CPU实现了M扩展可以把-marchrv32im传给make。-nostdlib和-static表示不链接标准库因为riscv-tests里的测试代码不需要任何库函数所有功能靠宏展开的汇编实现。-Tlink.ld指定链接脚本riscv-tests自带了一个默认链接脚本会把代码段放到0x80000000。这个地址对许多自制CPU来说是有问题的——如果你的CPU复位地址是0x00000000那么直接加载elf是跑不起来的要么修改链接脚本把起始地址改成你的复位地址要么在testbench里做地址重映射。这里有个小技巧值得分享如果你不想纠结链接脚本可以直接看dump文件确认程序的真实起始地址然后在设计里把IMEM指令内存的基地址做成可配置的或者在SoC层面加一个地址转换逻辑。很多课程设计就是这么处理的——CPU本来就只有一小块内存没必要非得从0x80000000开始。3.3 写一个能接收测试结果的testbench有了测试镜像文件之后最关键的一步是写testbench。很多人卡在这一步不是因为不会驱动时钟复位而是不知道测试什么时候算结束。标准做法是测试程序跑完后会往tohost地址写结果你的testbench必须监测这个信号。以很多自制CPU的testbench为例reg [31:0] tohost; always (posedge clk) begin if (cpu_ready) begin tohost mem_read_data; // 假设CPU通过总线读到tohost地址 if (tohost ! 0) begin if (tohost 1) $display(TEST PASSED); else $display(TEST FAILED, tohost %0d, tohost); $finish; end end end注意这里只是示意实际实现取决于你的CPU访存方式。如果你的CPU每次只能执行一条指令并且访存是同步的那么判断tohost的时机要精确安排在“CPU正在读tohost所在地址”的那个周期。tohost的值怎么理解如果测试通过riscv-tests会写1如果测试失败会写一个非1的数值并且这个数值通常和失败的具体原因相关联。你不需要完全掌握所有失败码的含义但至少可以通过非1判断测试没过然后抓波形定位。另一个常见问题是设置仿真超时。如果CPU因为某个bug陷入死循环tohost永远不会被写仿真就会永远跑下去。建议在testbench里加一个周期计数器比如仿真超过10万周期就报超时并结束always (posedge clk) begin if (~reset_n) cycle_count 0; else if (cycle_count 200000) cycle_count cycle_count 1; else begin $display(TIMEOUT); $finish; end end3.4 完整跑通一个测例的实测记录我这边用一个小型单周期核做过测试从编译到跑通的完整过程大概是这样的cd riscv-tests/isa make XLEN32 rv32ui-p-add编译成功后会生成rv32ui-p-add.elf、rv32ui-p-add.dump。然后我用objcopy生成verilog格式的内存初始化文件加载到IMEM里。testbench驱动复位跑了几百个周期后在tohost地址读到1屏幕打印TEST PASSED。从复位到PASS大概只需要几百个周期。这个结论很重要——它说明riscv-tests不是那种动辄跑几百万周期的重型测试每一条指令用例都很轻巧非常适合在仿真环境里做迭代调试。如果测试失败了我的调试路径一般是这么走的先看tohost值确认是不是真的失败再看仿真波形重点看失败前的最后几条指令是什么PC有没有跳转到预期的地方最后对照dump文件看当前执行到的汇编是不是对应某条测试宏然后检查数据通路和控制器。4. 拆解一个ISA测试用例add.S的内部结构4.1 一个测试用例到底长什么样很多人第一次打开rv32ui-p-add.S会一脸懵因为文件内容几乎全是宏调用看不到传统的指令序列。这其实是riscv-tests的一贯风格它用宏封装了测试的基本框架实际测试逻辑在宏展开里。#include riscv_test.h #include test_macros.h RVTEST_RV32U RVTEST_CODE_BEGIN TEST_RR_OP( 2, add, 0x00000000, 0x00000000, 0x00000000 ); TEST_RR_OP( 3, add, 0x00000002, 0x00000001, 0x00000001 ); TEST_RR_OP( 4, add, 0x0000000a, 0x00000006, 0x00000004 ); TEST_PASSFAIL RVTEST_CODE_END .data RVTEST_DATA_BEGIN .align 4 TEST_DATA RVTEST_DATA_END宏调用TEST_RR_OP(3, add, 0x00000002, 0x00000001, 0x00000001)的含义是用寄存器传参把0x00000001和0x00000001分别加载到两个源寄存器执行add指令检查目标寄存器是否等于0x00000002。如果相等继续下一条用例如果不相等跳转到失败处理代码。每条测试宏背后其实做了一连串事情设置好源寄存器值、执行指令、然后做一次比较分支比较不通过就跳到fail标签。这些判断逻辑全都在test_macros.h里展开了切片式的汇编代码。4.2 测试宏展开后的真实指令流如果只是看.S文件还不过瘾建议直接打开.dump文件看反汇编那才是CPU真正执行的指令序列。比如TEST_RR_OP宏展开后大概会有这么几条指令加载立即数到源寄存器、执行被测指令、把结果和目标期望值比较、条件跳转。这套逻辑用汇编写出来并不复杂但值得注意的是这些宏内部大量使用了临时寄存器和跳转标签如果CPU的跳转指令如beq、bne、立即数加载指令如addi、lui、auipc有问题那么连最基础的测试宏框架都跑不起来更别说被测的那条add指令了。这里有一个很实用的排查经验如果你的CPU连rv32ui-p-add都跑不过而且PC乱飞、跳转错乱先别急着查加法器优先检查你的beq/bne指令和lui/addi加载立即数的逻辑。因为这些宏内部靠这些指令搭建判断框架框架不对后面的被测指令根本没有机会执行。4.3 立即数加载是怎么做到的RV32I的指令长度是32位而lui指令只能加载20位立即数那么宏里出现32位常量时CPU是怎么把完整的32位数加载到寄存器里的答案是组合多条指令先用lui加载高20位再用addi把低12位加进去。如果低12位的最高位是1addi会做符号扩展这时候还需要额外处理。riscv-tests的宏里面对这类情况做了精细处理保证任何32位立即数都能被正确加载。这个细节对CPU设计者来说是个很好的测试点如果你的lui和addi有边界bug比如符号扩展处理不对很多测试宏都会诡异失败。常见现象是测试add的某些正数用例能过但碰到负数或者边界值就挂。如果出现这种情况强烈建议先测lui和addi这两个基础指令再回头查add。4.4 PASS和FAIL的判定机制测试宏本身只负责产生“比较后跳转”的分支真正决定测试最终结果的是TEST_PASSFAIL宏。它会往tohost地址写入最终状态成功写1失败写非1的值。这段代码一般在riscv_test.h里定义。整个流程是所有测试宏都通过后执行流会自然走到TEST_PASSFAIL的PASS分支写入tohost1一旦某个宏失败执行流跳转到FAIL路径写入失败标记。所以你的testbench真的不需要额外做什么只要读tohost地址就够了。这个设计让riscv-tests能适配几乎所有CPU——只要你的CPU能执行sw指令并且能访问tohost对应地址就能参与测试。5. 常见问题与排查技巧实录5.1 指令跑飞、PC不递增这类基础问题症状仿真开始不久CPU的PC跳到奇怪地址或者一直停在某个循环里不出来。这类问题八成不是被测指令的问题而是测试框架本身跑不起来。排查顺序有讲究先查复位和初始PC值再查取指通路能不能正确从IMEM读出第一条指令然后查第一条lui或者addi是否能正确把立即数写入寄存器。riscv-tests程序的入口处通常会设置栈指针和全局指针如果你的CPU没实现la伪指令对应的auipcaddi序列或者寄存器堆写入使能信号有问题程序根本启动不了。这里插一句个人体会测试add之前先单独跑一个只有addi x1, x0, 0的裸程序确认最基本的指令写回正常再上riscv-tests。这种“先小后大”的思路能帮你把问题隔离到最小范围。5.2 链接地址对不上导致的“数据诡异”症状测试程序好像跑起来了但访存行为极其诡异一会儿读到全0一会儿读回乱码或者程序就是找不到tohost地址。大概率是链接脚本里的地址0x80000000和你的CPU实际内存基地址不匹配。处理方法有两种一是改riscv-tests的链接脚本把起始地址改成你CPU的复位地址二是在testbench里做一个地址偏移把CPU发出的0x80000000等地址映射到实际IMEM/DTEM的索引。我比较推荐第二种方式因为不需要重新编译测试程序改完testbench就能直接跑。映射逻辑就是一个简单的地址减法当CPU访问0x80000000 offset时实际读写内存的offset位置。有些同学觉得这对不上很麻烦但从验证角度想这反而是个好测试点如果你的地址转换逻辑写错了访存测试lw、sw会马上暴露问题比你自己写测试还快。5.3 没实现CSR指令导致测试卡死riscv-tests的某些测试会涉及CSR访问比如csrrw、csrr这些指令。如果你的CPU没有实现CSR寄存器组这些指令会变成非法指令产生意料之外的行为。这时候不要硬着头皮去跑全量测试先跑rv32ui-p-*里最基础的整数指令子集把不涉及CSR的部分全部跑通。如果确实需要跑涉及特权指令的测试可以考虑在RTL里加一个最小化的CSR模块至少处理mstatus、mtvec、mepc这几个寄存器。我的建议是课程设计阶段完全没必要在测试里追求全量覆盖先把rv32ui-p-*跑通已经能证明CPU的大部分功能没毛病了。等后面有空再针对缺失的CSR功能单独写定向测试。5.4 浮点寄存器、乘除法指令导致编译失败如果你的工具链默认生成64位架构代码编译32位测试时偶尔会遇到莫名其妙的错误。解决方法是显式指定架构make XLEN32 riscv_tests_isa_rv32ui有些环境还需要设置RISCV_ARCHrv32im之类的变量。要是编译过程中报浮点寄存器相关错误多半是工具链与测试的ABI不匹配记得确保-mabiilp32不要混用lp64。5.5 常见问题速查表现象可能原因排查方向编译报错找不到头文件submodule未拉取git submodule update --init --recursive程序跑飞、PC乱跳跳转指令或立即数加载指令有问题检查beq/bne、lui/addi、auipc实现数据读回全0链接地址、内存基地址不匹配检查地址映射和testbench加载规则无限循环不结束某条宏中的比较跳转条件不成立CPU进了失败分支但失败分支又跳不出去抓波形定位跳转处对照dump文件tohost一直没有变化CSRRW、mret等特权指令卡住跳过特权测试或实现最小CSR模块测试离线测试通过但仿真挂仿真器版本兼容、初始化文件格式问题检查hex/bin文件字节序确认$readmemh格式5.6 如何一步步扩大测试覆盖范围建议顺序是这样先跑rv32ui-p-add、rv32ui-p-sub这类纯运算指令再跑rv32ui-p-lw、rv32ui-p-sw这类访存指令然后跑rv32ui-p-beq、rv32ui-p-bne这类跳转指令最后跑rv32ui-p-jal、rv32ui-p-jalr。这个顺序基本和CPU数据通路的实现顺序一致每跑通一组都说明一个子模块没问题。等你把rv32ui-p-*全部跑绿可以试试rv32um-p-*乘除法测试前提是你的CPU实现了M扩展。每轮新跑一组测试前建议先跑一遍已通过的旧测试确认改动没有破坏之前的功能——这个习惯在CPU开发里比在软件里还重要因为硬件改一个信号牵扯的面太广了。最后建议把你跑过的测试结果做成一个表格哪个测试过了、哪个没过、失败时PC停在哪条指令都记录下来。调试CPU本来就是一场持久战记录能帮你发现规律比如“所有涉及lw负偏移的测试都挂”这种规律比单点排查快得多。6. 一些额外想说的大白话以上内容如果能顺利跑通你的CPU设计基本就过了最关键的功能验证关。riscv-tests不只是一个测试套件它更像是一份“可执行的指令集文档”每条测试用例都在告诉你某条指令规范要求的行为边界。很多设计上的死角比如addi的符号扩展边界、比较跳转的有符号/无符号差异、访存地址对齐要求都是被这些测试逼着改出来的。对正在做CPU课程设计的朋友我的建议很简单不要满足于自己写的那几个testbench用例尽早把riscv-tests引入你的验证流程。哪怕一开始跑不通对照波形和dump文件修CPU的过程本身就是对数据通路理解最深的时刻。在这里踩过的坑、修过的bug远比最后那个PASS标志值钱。最后分享一个我自己的小习惯每次修改CPU的RTL代码不管改动多小都会把rv32ui-p-*全量重新跑一遍。CPU设计最怕的不是改错而是改了一处关联的bug、其他模块跟着退化。自动化测试就是你回头看的底气这个习惯能帮你省下大量排查回归问题的时间。
返回列表