type
status
date
slug
summary
tags
category
icon
password
声明
该文章作为一生一芯官方学习记录的补充,部分代码实现思路以及思考仅代表个人观点。
本文不立足于撰写“详细教程”、“通关秘籍”等类型的文章,仅仅作为个人记录留存。
如果您正参加该项目,且尚未完成相应任务点,请自觉退出!该行为可能与“一生一芯”项目官方所推崇的独立解决能力培养理念相违背!
本文内容包括但不限于:
- 部分代码设计实现思路
- 踩坑记录
- 部分必做题、思考题观点整理
……
📝 预学习阶段
NVBoard
sim目标通过定义FST宏可自由生成不同格式的波形文件
一生一芯官方推荐版本为Verilator 5.008 2023-03-04 rev v5.008,该版本下—trace-vcd和—trace-fst不可用 新版本中已经弃用—trace参数,作者此处使用版本为Verilator 5.040 2025-08-30 rev v5.040-50-g0a9d9db5a具体请参考官方手册:
- 键盘检测实验中作者觉得讲义中所写状态机不好理解,遂自行实现,依照 HDLBits 网站中的状态机描述风格:
NEMU(PA1)
主要内容是实现一个简易调试器

sdb_mainloop()解析
rl_gets()函数将整行命令读取到str中,strtok函数按空格分割出目标命令,在查找表中逐个对比,一旦匹配则进入相应的cmd_*逻辑中继续执行。优雅的退出
主函数退出前会执行一个判断状态的函数:
故只需要在
cmd_q中设置 NEMU 状态为退出即可:表达式求值
- 隐式类型转换
完成表达式求值的核心任务就是不断处理各种非法表达式,确保计算结果格式与预期相符。
讲义中统一规定使用无符号格式进行计算,而C语言中执行字面量除法默认是有符号数除法,为了与测试脚本运行结果一致,将
val1和val2强转为有符号类型- 表达式生成器
讲义中建议为在主函数中加入测试,而作者在这里单独新增一个test指令,在 NEMU 中执行 test 命令将自动生成 500 条表达式(自动丢弃掉非法表达式,例如包含除以 0 的表达式)。
同时为了避免生成过长的表达式,对递归深度和生成表达式长度进行限制,否则有可能除法段错误:
测试结果:
断点Bug
完成监视点内容之后,作者使用
w $pc==0x8000000c设置断点到最后一条 ebreak 指令,结果如下:发现非法指令被执行。经过 gdb 调试发现,执行结束 ebreak 指令后,NEMU 状态会被设置为END后安全退出,而扫描监视点时,命中监视点会将 NEMU 状态设置为STOP,导致后续读取到非法指令触发错误,解决方案也很简单,加一层状态判断即可。
另外,此处是用监视点模拟断点,因此程序代码运行时产生了两次中断(旧值不等于新值)。
C阶段
理解一条指令在 NEMU 中的执行过程
exec_once(&s, cpu.pc);包含指令的取指、译码、执行、pc 更新,cpu 结构体保存全局状态,Decode s;为运行时上下文结构体,保存静态 pc、动态 pc、指令等相关元素。
- exec_once 前四行:
- 取指过程:
vaddr_ifetch函数到对应地址下读取32位长度的指令,然后将snpc+4,返回读取的指令。- 译码与执行过程:
这一长串宏直接使用 VSCode 插件自带的智能感知(Intellisence)展开为:(下文只保留一个INSTPAT)
有关于do {} while(0)的用法参考
有关于符号标记用法参考。从功能上来说,这种方式给出了一种高效快捷的代码跳转方式,一旦匹配则提前结束该代码块。
pattern_decode函数详解
这里
i表示处理指令字符串的下标。空格被自动忽略,除0、1、?之外的字符会抛出异常,下面以“??????? ????? ????? ??? ????? 00101 11”为例子:__key用于识别指令操作码,只对0和1感兴趣,最终值为0010111;__mask用于生成掩码,?对应位置设置为0,其他对应设置为1,表示?区域不应参与匹配。__shift对于一些不关心低位匹配的操作码,如110 ????,最终它的值为4,右移四位匹配。micro64(0); 展开为macro32(0);macro32(32);,继续展开为macro16(0);macro16(16);macro16(32);macro16(48); ,最终变成macro(0);macro(1);macro(2);……;marcro(64); ,这种宏展开可以在编译阶段直接优化,减少for循环方式造成的运行时开销。这种思路的可实现性在于指令是提前就加载到代码中的,即编译阶段指令代码的内容就是确定的。而对于等待用户输入是程序运行时才能解决的问题。
接下来这行代码
(((uint64_t)((s)->isa.inst) >> shift) & mask) == key 对指令进行匹配,匹配成功则提取操作码和操作数。decode_operand函数详解
这里
BITS宏进行位切片,提取出对应的寄存器。src1R()宏展开之后长这样,很显然是对相应寄存器的赋值操作:immI()宏展开之后长这样,提取I型指令的立即数,并做符号扩展:之后便可以完成代码的执行,结束后返回0。
实现更多的指令
运行测试用例 mul-longlong.c 时出现 HIT BAD TRAP 的提示:

使用 gdb 调试,调用栈如下:

该语句导致
halt_ret被置1:这说明,执行 ebreak 时,$a0 寄存器的值为 1。打印当前
s→pc的值为 0x80000140 接下来利用 sdb 在 NEMU 中设置对 $a0 的监视点:
查看反汇编,发现 check 函数执行导致 a0 被设置为 1
结合C代码和反汇编分析容易知道,
check函数负责检查乘法计算结果和预期结果是否一致,说明我们乘法指令的实现存在问题C11 标准(N1570)中指出: “When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.” 当一个整数类型的值被转换为除 _Bool 以外的另一种整数类型时,如果该值能够被新类型表示,则该值保持不变。 32位无符号数任意取值都能够被 64 位有符号类型的整数所表示,所以转换时值不变,高位作零扩展。如果作符号扩展,它的数值就被改变了。
此处实现方法的确有问题,对于
mulh这种指令,src1和src2如果是负数,类型转换后变成了正数,计算结果自然与实际不相符。于是对src1和src2先使用宏函数SEXT()进行符号扩展,最后进行运算。最终修改如下:再次运行成功通过:

完整测试如下:

使用 DPI-C 机制验证 RV32I 单周期 CPU
DPI-C 机制允许在 System Verilog 中直接调用 C 函数,或者导出 function 和 task 供 C 语言调用。
npc_trap();的实现:使用 verilator 进行仿真
while内至少top→eval();两次,第一次计算由时钟沿触发产生的时序逻辑更新(clk 0→1,eval()触发 pc_o 的更新),第二次计算触发寄存器更新导致的组合逻辑更新(由新的pc_o取出下一条指令)。这样波形上才能和事件驱动型仿真器一致。
top→eval();是根据输入激励计算输出tfp->dump(contextp->time());是按当前时间轴记录一次波形
contextp->timeInc(1);是当前记录时间加一,三者必须共同配合才能仿真出完整波形测试结果:

对 AM 的 Makefile 默认启动批处理模式NEMU
通过RTFSC得知,要在运行NEMU时传入参数
-b 作者此处直接使用 remake 工具单步调试,发现最终
ARGS变量的来源:对应 Makefile 源代码位置修改如下:
考虑到对测试代码使用 sdb 的需求,作如下调整:
添加递归 remake 支持
起因是由于使用 remake 单步调试时遇到子项目的 make 构建就会直接跳过,如果想要调试子目标的 Makefile 就要中途退出一次,然后使用 remake 调试 Makefile.xxx,不太方便。后来发现了 nemu.mk 文件使用了
$(MAKE) 这个内置变量,查询了该变量相关用法,于是做如下修改:修改前单步调试:

修改后单步调试:

内部的 Make 也会开启 Trace 模式,子目录的详细构建过程也被打印出来了。
参考文档内容
使用 C 语言解析 ELF 文件并实现 ftrace
主要完成:
- 使用 elf.h 解析 ELF 文件的符号表,建立针对 FUNC 类型的查找表
- 判断 call 指令和 ret 指令并打印跟踪日志
- 修改 Makefile 使得运行 tests 时可以自动传入
elf参数
ftrace 效果如下:

这里发现两个个问题:
- C 代码最开始是 f0 调用了 f3,但是 ftrace 显示是 f0 调用了 f2
- 讲义中提到的返回不对应问题
这里反汇编是 jalr x0, 0(a5),作为调用指令目的寄存器却不是 ra。
经过 STFW,该现象可能与尾调用有关。如果函数最后一步的操作是调用一个函数,无需把返回地址压入栈中,直接跳转到那个函数,由它直接返回。
参考资料The RISC-V Instruction Set Manual Volume I | © RISC-V2.5.1. Unconditional Jumps 另外,根据手册和查阅反汇编代码,函数调用指令一般情况目标寄存器为 ra 寄存器,ret 指令在 RV32I 和 RV64I 中扩展为jalr x0, 0(x1)
Difftest 实现
RISCV32 寄存器成员顺序定义:先是 32 位通用寄存器,然后是 PC 寄存器。
对比每个寄存器值和下一条指令地址是否一致即可,不过多赘述。
验证效果:尝试修改 inst.c 中几条指令的实现,比如先后修改 add、sub、sw、jal、bne,执行
add:
sub:
sw:
jal:
bne:
构建 NPC-Infra
参考资料
- Author:Hyacimond
- URL:http://hyacimond.top/%E9%A1%B9%E7%9B%AE/ysyx-record
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts










