本文部分内容参考了这篇文章,感谢前人栽树!
https://zhuanlan.zhihu.com/p/108624018
IBEX Simple System介绍
IBEX的Simple System系统除了挂载ram之外,还挂载有simulator_ctrl和timer两个外设,并且有一个tracing模块用于记录ibex core读取总线数据的记录。
由于verilator只能编译可综合的设计,所以对于RAM我们必须提供一个可综合的model,RAM的可综合model非常简单,用二维数组就可以实现。但是我们必须解决如何把一个初始的RAM镜像load进去,如果不考虑灵活性的话,直接指定一个镜像文件也是可行的。但如果要让testbench指定的话,就必须提供一个接口,可供C++调用(这与Verilator中的实现机制有密切的关系,在testbench的cpp文件中也要关于ram进行配置)。
simulator ctrl从bus上读取写地址和写数据。如果写地址为0x20000,则向file中写数据。如果向0x20008中写1,sim_finish便置为1,经过若干时钟周期后,调用$finish,终止仿真。
always_ff @(posedge clk_i or negedge rst_ni) begin
if (~rst_ni) begin
rvalid_o <= 0;
sim_finish <= 'b0;
end else begin
// Immeditely respond to any request
rvalid_o <= req_i;
if (req_i & we_i) begin
case (ctrl_addr)
CHAR_OUT_ADDR: begin
if (be_i[0]) begin
$fwrite(log_fd, "%c", wdata_i[7:0]);
if(FlushOnChar) begin
$fflush(log_fd);
end
end
end
SIM_CTRL_ADDR: begin
if ((be_i[0] & wdata_i[0]) && (sim_finish == 'b0)) begin
$display("Terminating simulation by software request.");
sim_finish <= 3'b001;
end
end
default: ;
endcase
end
end
if (sim_finish != 'b0) begin
sim_finish <= sim_finish + 1;
end
if (sim_finish >= 3'b010) begin
$finish;
end
end
timer根据Machine Timer Registers,为core产生中断。中断的输出信号直接被接往CPU core。timer从bus上接受地址和数据,若读地址为0x30000-0x3000c,则返回对应timer register的值。
Bus上负责cpu core与ram,simulator ctrl,timer之间的交流。在担任总线功能的同时,也必须要完成仲裁器的任务。根据地址,选择要启动的外设。
Address | Description |
---|---|
0x20000 | ASCII Out, write ASCII characters here that will get output to the log file |
0x20008 | Simulator Halt, write 1 here to halt the simulation |
0x30000 | RISC-V timer mtime register |
0x30004 | RISC-V timer mtimeh register |
0x30008 | RISC-V timer mtimecmp register |
0x3000C | RISC-V timer mtimecmph register |
0x100000 – 0x1FFFFF | 1 MB memory for instruction and data. Execution starts at 0x100080, exception handler base is 0x100000 |
配置环境
略,IBEX文档中有详细介绍 。
FuseSoC建立仿真
有关于FuseSoC的介绍可以查看手册https://fusesoc.readthedocs.io/en/stable/user/cli.html
总的来说,FuseSoC是一个用于HDL代码的 包管理+建立系统 的工具,他的基本单元有各种.core文件构成:A FuseSoC core is a reasonably self-contained, reusable piece of IP, such as a FIFO implementation. core类似于package的概念,具有层次化结构。里面说明了这个core包含的HDL文件,包含的其他的低层次core,以及许多配置信息。这些配置信息例如:选择之后需要使用的EDA工具(Tool Flows),选择需要仿真、综合、静态分析(Targets)。FuseSoC希望能隐藏这些工具使用细节上的差异。
运行如下命令:
fusesoc --cores-root=. run --target=sim --setup --build lowrisc:ibex:ibex_simple_system --RV32E=0 --RV32M=ibex_pkg::RV32MFast
FuseSoC建立了lowrisc:ibex:ibex_simple_system命名的core,它的位置在examples/simple_system。它所包含的core的依赖关系图如下所示。
ibex_top继续包含ibex中的各个组件。
命令行中指定target为sim,即是选择了如下的目标tool flow的配置。
命令还对RV32E以及RV32M这两个参数进行显式的配置。还有许多其他的参数,都使用默认default值。
执行完该命令之后,会出现build文件夹。rtl目录下存放了core包含的全部文件,sim-verilator目录下存放使用verilator编译好的cpp文件以及其对应的.o, .d文件。Verilator把一个仿真,包括DUT和testbench编译成了Linux下的一个可执行文件。
生成待测程序
make -C examples/sw/simple_system/hello_test
通过之前配置完成的RISC-V工具链,将程序编译为elf文件。同时会生成.o .d .bin .vmem等文件。
hello_test.vmem是由srecord工具生成,相当于把代码空间objcopy到一个文件中。
在hello_test.c中,有一系列以putchar为基础的输出函数。putchar是通过对特定地址0x2000写一个字符来实现。simulator ctrl外设会将这一特定地址的写操作转化为对log file的写。
//simple_system_regs.h
#define SIM_CTRL_BASE 0x20000
#define SIM_CTRL_OUT 0x0
#define SIM_CTRL_CTRL 0x8
//simple_system_common.h
#define DEV_WRITE(addr, val) (*((volatile uint32_t *)(addr)) = val)
simple_system_common.c
int putchar(int c) {
DEV_WRITE(SIM_CTRL_BASE + SIM_CTRL_OUT, (unsigned char)c);
return c;
}
之后hello_test.c中pcount_enable,timer_enable等操作,涉及riscV特权级以及中断等指令的实现,这里暂时不作讨论。
CPU上电后初始PC地址0x80,在simple_system/sw/simple_system/common/crt0.s中定义了如下的vectors段。从0x00到0x7c是向量表,0x80放置了CPU执行的第一条指令,跳转到reset_handler中。
.section .vectors, "ax"
.option norvc;
// All unimplemented interrupts/exceptions go to the default_exc_handler.
.org 0x00
.rept 7
jal x0, default_exc_handler
.endr
jal x0, timer_handler
.rept 23
jal x0, default_exc_handler
.endr
// reset vector
.org 0x80
jal x0, reset_handler
程序跳转到reset_handler执行,重置所有寄存器为0。指令运行到main_entry,跳转到main(也就是hello_test.cpp中的main),执行用户程序。
main_entry:
/* jump to main program entry point (argc = argv = 0) */
addi x10, x0, 0
addi x11, x0, 0
jal x1, main
运行仿真器
./build/lowrisc_ibex_ibex_simple_system_0/sim-verilator/Vibex_simple_system [-t] --meminit=ram,<sw_elf_file>
这个可执行文件的入口地址在examples/simple_system/ibex_simple_system_main.cc
#include "ibex_simple_system.h"
int main(int argc, char **argv) {
SimpleSystem simple_system(
"TOP.ibex_simple_system.u_ram.u_ram.gen_generic.u_impl_generic",
1024 * 1024);
return simple_system.Main(argc, argv);
}
有关SimpleSystem的声明如下。其中ibex_simple_system这个类就是由Verilator编译好的DUT model。
class SimpleSystem {
public:
SimpleSystem(const char *ram_hier_path, int ram_size_words);
virtual ~SimpleSystem() {}
virtual int Main(int argc, char **argv);
// Return an ISA string, as understood by Spike, for the system being
// simulated.
std::string GetIsaString() const;
protected:
ibex_simple_system _top;
VerilatorMemUtil _memutil;
MemArea _ram;
virtual int Setup(int argc, char **argv, bool &exit_app);
virtual void Run();
virtual bool Finish();
};
调用的SimpleSystem的Main函数如下所示。分别是对于仿真系统进行SetUp和Run。
SimpleSystem::SimpleSystem(const char *ram_hier_path, int ram_size_words)
: _ram(ram_hier_path, ram_size_words, 4) {}
int SimpleSystem::Main(int argc, char **argv) {
bool exit_app;
int ret_code = Setup(argc, argv, exit_app);
if (exit_app) {
return ret_code;
}
Run();
if (!Finish()) {
return 1;
}
return 0;
}
在SetUp中,首先会将待测DUT的model同testbench相连。
之后指定MemArea,其基本原理是把某个memory的对象的hierarchy(这里是TOP.ibex_simple_system.u_ram.u_ram.gen_generic.u_impl_generic)绑定到一个名称上,如"ram",之后便可以调用RAM model里的函数把数据写入到对应"ram"的软件模型中。
在SetUp函数中,最终会调用Verilator的Verilated::commandArgs(argc, argv), 根据命令中提供的参数,对仿真进行配置。这里我们传入了–meminit=ram,<sw_elf_file>,verilator会将elf文件load进"ram"中,作为初始内存的状态。
int SimpleSystem::Setup(int argc, char **argv, bool &exit_app) {
VerilatorSimCtrl &simctrl = VerilatorSimCtrl::GetInstance();
simctrl.SetTop(&_top, &_top.IO_CLK, &_top.IO_RST_N,
VerilatorSimCtrlFlags::ResetPolarityNegative);
_memutil.RegisterMemoryArea("ram", 0x0, &_ram);
simctrl.RegisterExtension(&_memutil);
exit_app = false;
return simctrl.ParseCommandArgs(argc, argv, exit_app);
}
在Run()函数里面,最终会完成对DUT的激励。在while循环中,反转clk,对模型eval,Trace。
//vendor\lowrisc_ip\dv\verilator\simutil_verilator\cpp\verilator_sim_ctrl.cc
while (1) {
...
*sig_clk_ = !*sig_clk_;
...
top_->eval();
time_++;
Trace();
...
}
}
最终结果
trace_core_000000.log中记录了CPU执行的全部指令。可以与crt0.s以及hello_test.cpp等程序对照进行查看。
Verilator显示出仿真运行的整体状况。
结语
经过几天的配环境和学习,终于大致上摸清了仿真simple system的过程,也恶补了许多之前忽略的计组的知识。simple system虽然小巧,但是涉及了CPU仿真的大部分环节,也加深了我对于RISC-V核的认识。
后续我会进一步了解Verilator更多的内容,学习IBEX提供的另一套co-simulation的UVM testbench。