【一生一芯03】verilator仿真框架搭建
目录
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 bash
unset VERILATOR_ROOT # For bash
cd verilator
git pull # Make sure git repository is up-to-date
git 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 version
autoconf # Create ./configure script
./configure # Configure and create Makefile
make -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;
end
endmodule
sim_main.cpp文件
#include <verilated.h>// 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 = verilator
else
export VERILATOR_ROOT
VERILATOR = $(VERILATOR_ROOT)/bin/verilator
endif
default:
@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/Vour
Hello 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 <verilated_dpi.h>
//glibc
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// Difftest
#include <dlfcn.h>
//readline
#include <readline/readline.h>
#include <readline/history.h>
//system time
#include <sys/time.h>
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波形设置 start
Verilated::traceEverOn(true);
tfp = new VerilatedVcdC;
top->trace(tfp, 0);
tfp->open("wave.vcd");
//VCD波形设置 end
//initial data
pmem_init();
cpu_init();
#ifdef CONFIG_DIFFTEST
init_difftest();
#endif
sdb_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 %dn",top->clk);
// top->mem_inst = pmem_read(top->mem_addr);
// printf("excute addr:0x%08lx inst:0x%08xn",top->mem_addr,top->mem_inst);
top->eval();
tfp->dump(main_time);
main_time ++;
top->clk = 1;
//printf("======clk should be 1 now %dn",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_DIFFTEST
difftest_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_DIFFTEST
void 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/include
sim:
$(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/Vtop
gtkwave wave.vcd
count:
find . -name "sim_main.cpp" -or -name "*.[vc]" | xargs wc -l
clean:
rm -rf obj_dir
rm wave.vcd
include ../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 instn");
printf(COLOR_END);
hit_exit(cpu_gpr[10]);
}
//ebreak in verilog
import "DPI-C" function void ebreak();
module EBREAK(
input wire [31:0] inst_i
);
always @(*) begin
if(inst_i == `INST_EBREAK)
ebreak();
end
endmodule
首先在c++中定义ebreak函数,打印执行指令,并调用hit_exit函数判断输出状态。verilog中,将函数import,当检测到ebreak时,就会调用c++的函数执行,实现仿真的结束。
3.2 env
env的实现思路和ebreak是一致的,主要用于取到不在译码列表中的指令时通知仿真环境结束仿真,并报出“invalid inst”的信息。在前期书写riscv指令时,方便debug。确认指令实现完整且正确后可以注释掉。
//env
extern "C" void env(){
printf(COLOR_RED);
printf("invalid instn");
printf(COLOR_END);
hit_exit(NPC_BAD);
}
3.3 访存
由于单周期处理器设计时尚未接入总线,因此访存也是通过Dpi-C机制实现。其原理和ebreak一致,只不过添加了输入输出的信号,一生一芯讲义中已经给出了模板和伪代码,将其内容补全即可。实现过程中发现rdata信号会存在UNoptflat的警告,该警告会在另一个笔记中总结,这里使用/*verilator split_var*/进行消除。后续在实现输入输出及运行马里奥,也需要在c++的函数中书写mmio。
//Dpi-C in c++
//memory read
extern "C" void pmem_read(addr_t raddr, addr_t *rdata) {
//mmio-rtc
if(raddr == RTC_ADDR) {}
//memory
else { *rdata = ret;}
}
//memory write
extern "C" void pmem_write(addr_t waddr, addr_t wdata, char wmask) {
if (waddr < CONFIG_MBASE) return;
//memory
else if((waddr >= CONFIG_MBASE) && (waddr < CONFIG_MAX)) {
wdata >>= 8, wmask >>= 1, pt++;
}
}
//mmio-serial_port
else if(waddr == SERIAL_PORT) {}
}
Dpi-C in verilog
import "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 EXU
input wire[63:0] raddr,
input wire[63:0] waddr,
input wire[63:0] wdata,
input wire[7:0] wmask,
input wire ren,
input wire wen,
//to EXU
output reg[63:0] rdata/*verilator split_var*/
);
//reg [63:0] rdata_buf;
always @(*) begin
if (ren) pmem_read(raddr, rdata);
else rdata = 64'b0;
if (wen) pmem_write(waddr, wdata, wmask);
else pmem_write(waddr, wdata, 0);
end
endmodule
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 info
uint64_t *cpu_gpr = NULL;
extern "C" void set_gpr_ptr(const svOpenArrayHandle r) {
cpu_gpr = (uint64_t *)(((VerilatedDpiOpenVar*)r)->datap());
}
//gpr dpi-c in verilog
import "DPI-C" function void set_gpr_ptr(input logic [63:0] a []);
initial set_gpr_ptr(rf); // rf为通用寄存器的二维数组变量
参考资料: