目录
1 verilator介绍
1.1 简介
1.2 安装
1.3 hello,world
2 npc仿真框架搭建
2.1 sim_main.cpp
2.1.1 头文件引用
2.1.2 仿真环境
2.1.3 主函数
2.1.4 执行函数
2.1.5 内存初始化
2.1.6 基础设施
2.2 Makefile文件构建
3 Dpi-C机制
3.1 ebreak
3.2 env
3.3 访存
3.4 寄存器
1 verilator介绍
verilator详细内容可以查看官方手册Overview — Verilator 5.003 documentation
1.1 简介
Verilator是一种开源的Verilog/SystemVerilog仿真器,可用于编译代码以及代码在线检查,Verilator能够读取Verilog或者SystemVerilog文件,并进行lint checks(基于lint工具的语法检测),并最终将其转换成C++的源文件.cpp和.h。
Verilator不直接将Verilog HDL转换为C++或者SystemC,反之Verilator将代码编译成更快的优化过的并且支持多线程的模型,该模型被依次包装在(wrapped)在C++/SystemC模型中。这样就生成一个编译的Verilog模型,其功能和Verilog是一致的,但效率由于基于C++即使是单线程模型也可以10倍快于SystemC,100倍快于基于解释Verilog的仿真器,并且通过多线程可以进一步加速。
1.2 安装
# Prerequisites:#sudo apt-get install git perl python3 make autoconf g++ flex bison ccache#sudo apt-get install libgoogle-perftools-dev numactl perl-doc#sudo apt-get install libfl2# Ubuntu only (ignore if gives error)#sudo apt-get install libfl-dev# Ubuntu only (ignore if gives error)#sudo apt-get install zlibc zlib1g zlib1g-dev# Ubuntu only (ignore if gives error)git clone https://github.com/verilator/verilator # Only first time# Every time you need to build:unsetenv VERILATOR_ROOT# For csh; ignore error if on bashunset VERILATOR_ROOT# For bashcd verilatorgit pull # Make sure git repository is up-to-dategit tag# See what versions exist#git checkout master# Use development branch (e.g. recent bug fixes)#git checkout stable# Use most recent stable release#git checkout v{version}# Switch to specified release versionautoconf # Create ./configure script./configure# Configure and create Makefilemake -j `nproc`# Build Verilator itself (if error, try just 'make')sudo make install
1.3 hello,world
安装好verilator后可以在文件目录下找到官方提供的example。以make_hello_c为例
top.v文件
module top; initial begin$display("Hello World!");$finish; endendmodule
sim_main.cpp文件
#include // verilator官方库#include "Vtop.h"//top.v会被封装为头文件供c++调用int main(int argc, char** argv, char** env) {if (false && argc && argv && env) {}Vtop* top = new Vtop;// 构建verilator模型,可以通过类型调用top中的参数while (!Verilated::gotFinish()) {// 开始仿真直到$finish top->eval();// Evaluate model}top->final();//结束仿真delete top;// 清除模型 return 0;// Return good completion status}
Makefile文件
ifeq ($(VERILATOR_ROOT),)VERILATOR = verilatorelseexport VERILATOR_ROOTVERILATOR = $(VERILATOR_ROOT)/bin/verilatorendifdefault:@echo "-- Verilator hello-world simple example"@echo "-- VERILATE & BUILD --------"$(VERILATOR) -Wall -cc --exe --build -j top.v sim_main.cpp@echo "-- RUN ---------------------"obj_dir/Vtop@echo "-- DONE --------------------"@echo "Note: Once this example is understood, see examples/make_tracing_c."@echo "Note: Also see the EXAMPLE section in the verilator manpage/document."
Makefile用于文件构建,主要的语句只有
$(VERILATOR) -cc --exe --build -j top.v sim_main.cpp
-Wall
:让verilator
执行强类型警告--cc
:得到C++
输出--exe
:和wrapper
文件一起,为了创建一个可执行文件--build
:让verilator
能让自己执行- -j :创建多线程编译,提高编译速度
运行程序,可以看到命令行中打印出”Hello World”
./obj_dir/VourHello World- our.v:2: Verilog $finish
事实上,这只是一个最简单的案例,在example下还有一个真正的案例tracing可以实现波形的输出,目录结构如下:
❯ tree -d.├── cmake_hello_c├── cmake_hello_sc├── cmake_protect_lib├── cmake_tracing_c├── cmake_tracing_sc├── make_hello_c│ └── obj_dir├── make_hello_sc├── make_protect_lib├── make_tracing_c├── make_tracing_sc└── xml_py
2 npc仿真框架搭建
2.1 sim_main.cpp
2.1.1 头文件引用
头文件需要提供仿真所需内容,包含:
- verilator官方库:生成仿真模型和波形,提供dpi-c接口
- 基础设施:difftest的动态链接,sdb的readline,rtc的sys/time
- c++相关库函数:仿真文件本身依旧是c++文件,可以调用c/c++库函数
#include "verilated_vcd_c.h" //用于生成波形#include "Vtop.h"#include "verilated.h"//dpi-c#include "Vtop__Dpi.h"#include //glibc#include #include #include // Difftest#include //readline#include #include //system time#include
2.1.2 仿真环境
在仿真环境中,定义全局变量 top 实例化模块,其中包含两个变量top->clk,top->rst;定义上下文指针 contextp;定义波形指针 tfp;定义仿真时间 main_time;定义ref寄存器(用于difftest)
//================= Environment ===============VerilatedContext* contextp;Vtop* top;VerilatedVcdC* tfp;vluint64_t main_time = 0;//initial 仿真时间double sc_time_stamp(){return main_time;}uint64_t ref_regs[33];void hit_exit(int status) {}
2.1.3 主函数
//============ Main ============int main(int argc, char** argv, char** env) {contextp = new VerilatedContext;contextp->commandArgs(argc, argv);top = new Vtop{contextp};//VCD波形设置startVerilated::traceEverOn(true);tfp = new VerilatedVcdC;top->trace(tfp, 0);tfp->open("wave.vcd");//VCD波形设置end//initial datapmem_init();cpu_init();#ifdef CONFIG_DIFFTESTinit_difftest();#endifsdb_mainloop();return 0;}
2.1.4 执行函数
在执行函数内实现单步运行,初始化后将复位信号拉高,时钟每周期变更一次。要注意每次eval后都要用dump函数来记录波形,不然wave中会按照之前的状态输出。
//================= Exec =====================void cpu_init() {//cpu_gpr[32] = CONFIG_MBASE;top -> clk = 0;top -> rst_n = 0;top -> eval();tfp->dump(main_time);main_time ++;top -> clk = 1;top -> rst_n = 0;top -> eval();tfp->dump(main_time);main_time ++;top -> rst_n = 1;}void exec_once(VerilatedVcdC* tfp) {top->clk = 0;//printf("======clk shoule be 0 now %d\n",top->clk);// top->mem_inst = pmem_read(top->mem_addr);// printf("excute addr:0x%08lx inst:0x%08x\n",top->mem_addr,top->mem_inst);top->eval();tfp->dump(main_time);main_time ++;top->clk = 1;//printf("======clk should be 1 now %d\n",top->clk); top->eval(); tfp->dump(main_time);main_time ++;}void cpu_exec(uint64_t n) {for(int i; i < n; i++){exec_once(tfp);#ifdef CONFIG_DIFFTESTdifftest_exec_once();#endif}}
2.1.5 内存初始化
//================= Memory ====================addr_t img_size = 0;uint8_t pmem[10485760] = {0};uint8_t* cpu2mem(addr_t addr) {}void pmem_init() {char image_path[] = "/home/springkiss/ysyx-workbench/npc/image.bin";}
2.1.6 基础设施
基础设施主要包含各种trace工具,difftest和sdb。
itrace需要借助dpi-c读取出当前正在执行的指令,再链接llvm库进行反汇编输出;
difftest是一生一芯项目中最重要+好用的工具,是处理器调试的一大杀手锏。具体实现方式可以参考讲义内容;
sdb可以参考nemu的实现,能够进行单步运行和寄存器打印我认为就足够支持处理器的debug。
//================== Itrace ==================// extern "C" void itrace(int itrace_data,addr_t itrace_addr){// printf("excute inst %016x: %08x",itrace_addr,itrace_data);// }//================= Difftest =================#ifdef CONFIG_DIFFTESTvoid init_difftest() {}void checkregs(uint64_t *ref_regs){}void difftest_exec_once(){}#endif//=================== Sdb ====================void gpr_display() {}static int cmd_c(char *args) {}static int cmd_q(char *args) {}static int cmd_help(char *args);static int cmd_si(char *args) {}static int cmd_info(char *args) {}#define NR_CMD ARRLEN(cmd_table)static int cmd_help(char *args) {}void sdb_mainloop() {}
2.2 Makefile文件构建
以下是完成仿真框架时自己的Makefile构建,仅供参考。
- sim:开启仿真
- wave:记录波形
- count:统计代码行数
all:@echo "Write this Makefile by your self."VSRCS = $(shell find $(./vsrc ) -name "*.v")# CSRCS = $(shell find $(./csrc ) -name "*.c" -or -name "*.cc" -or -name "*.cpp")INCLUDE = ./vsrc/includesim:$(call git_commit, "sim RTL") # DO NOT REMOVE THIS LINE!!!@echo $(VSRCS)verilator --trace --cc --exe --build \--top-module top \-I$(INCLUDE) ./csrc/sim_main.cpp $(VSRCS) \-LDFLAGS -"lreadline"wave: sim./obj_dir/Vtopgtkwave wave.vcdcount:find . -name "sim_main.cpp" -or -name "*.[vc]" | xargs wc -lclean:rm -rf obj_dirrm wave.vcdinclude ../Makefile
3 Dpi-C机制
Verilator支持systemverilog直接编程接口导入和导出语句。通过Dpi-C机制,可以实现仿真用c++文件和RTL文件的交互,基于此可以实现ebreak,env来通知仿真环境结束仿真,以及在实现总线之前的访存行为。
3.1 ebreak
通常的仿真文件会定义MAX_SIMTIME来决定仿真何时结束。但是在处理器设计中,我们并不知道程序会执行多少条指令,因此可以设置ebreak指令:当程序执行到ebreak指令时,通知仿真环境结束仿真,并通过寄存器a0的值来判定程序执行是pass还是fail
//ebreak in c++extern "C" void ebreak(){printf(COLOR_GREEN);printf("excute the ebreak inst\n");printf(COLOR_END);hit_exit(cpu_gpr[10]);}//ebreak in verilogimport "DPI-C" function void ebreak();module EBREAK(input wire [31:0] inst_i);always @(*) beginif(inst_i == `INST_EBREAK) ebreak(); endendmodule
首先在c++中定义ebreak函数,打印执行指令,并调用hit_exit函数判断输出状态。verilog中,将函数import,当检测到ebreak时,就会调用c++的函数执行,实现仿真的结束。
3.2 env
env的实现思路和ebreak是一致的,主要用于取到不在译码列表中的指令时通知仿真环境结束仿真,并报出“invalid inst”的信息。在前期书写riscv指令时,方便debug。确认指令实现完整且正确后可以注释掉。
//envextern "C" void env(){printf(COLOR_RED);printf("invalid inst\n");printf(COLOR_END);hit_exit(NPC_BAD);}
3.3 访存
由于单周期处理器设计时尚未接入总线,因此访存也是通过Dpi-C机制实现。其原理和ebreak一致,只不过添加了输入输出的信号,一生一芯讲义中已经给出了模板和伪代码,将其内容补全即可。实现过程中发现rdata信号会存在UNoptflat的警告,该警告会在另一个笔记中总结,这里使用/*verilator split_var*/进行消除。后续在实现输入输出及运行马里奥,也需要在c++的函数中书写mmio。
//Dpi-C in c++//memory readextern "C" void pmem_read(addr_t raddr, addr_t *rdata) {//mmio-rtcif(raddr == RTC_ADDR) {}//memoryelse { *rdata = ret;}}//memory writeextern "C" void pmem_write(addr_t waddr, addr_t wdata, char wmask) {if (waddr = CONFIG_MBASE) && (waddr >= 8, wmask >>= 1, pt++;}}//mmio-serial_portelse if(waddr == SERIAL_PORT) {} }Dpi-C in verilogimport "DPI-C" function void pmem_read(input longint raddr, output longint rdata);import "DPI-C" function void pmem_write(input longint waddr, input longint wdata, input byte wmask);module MEM(//from EXUinputwire[63:0] raddr,inputwire[63:0] waddr,inputwire[63:0] wdata,inputwire[7:0]wmask,inputwire ren,inputwire wen,//to EXUoutputreg[63:0] rdata/*verilator split_var*/);//reg [63:0] rdata_buf;always @(*) beginif (ren) pmem_read(raddr, rdata);else rdata = 64'b0;if (wen) pmem_write(waddr, wdata, wmask);else pmem_write(waddr, wdata, 0);endendmodule
3.4 寄存器
根据讲义内容
在verilog中,通用寄存器一般会用二维数组实现。但是由于Dpi-C的二维数组机制较为复杂,因此可以使用一种高性能的实现方式:引用传递。
具体地,首先在c++中定义一个set_gpr_ptr函数,该函数接受一个类型为 svOpenArrayHandle 的参数,并将全局变量 cpu_gpr 设置为数组句柄的数据指针。这样,就可以通过 cpu_gpr 全局变量访问 svOpenArrayHandle 句柄表示的数组中的值。
接着,在 SystemVerilog 中导入了 set_gpr_ptr() 函数,并在 initial 块中调用了该函数,将 rf 数组作为参数传递给它。通过这种方式,就可以在 SystemVerilog 中调用 set_gpr_ptr() 函数,并将 rf 数组中的值作为参数传递给该函数,从而通过 cpu_gpr 全局变量访问 rf 数组中的值。
//================= Dpi-c =====================//gpr infouint64_t *cpu_gpr = NULL;extern "C" void set_gpr_ptr(const svOpenArrayHandle r) {cpu_gpr = (uint64_t *)(((VerilatedDpiOpenVar*)r)->datap());}//gpr dpi-c in verilogimport "DPI-C" function void set_gpr_ptr(input logic [63:0] a []);initial set_gpr_ptr(rf);// rf为通用寄存器的二维数组变量
参考资料:
Overview — Verilator 5.003 documentation
用RTL实现最简单的处理器 | 官方文档