【一生一芯03】verilator仿真框架搭建

目录

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 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->clktop->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为通用寄存器的二维数组变量

 

参考资料:

Overview — Verilator 5.003 documentation

用RTL实现最简单的处理器 | 官方文档

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>