ところで、RISC-V Rocket-Chipをシミュレーションするとき、何気なくmakeを叩いて、
make CONFIG=DefaultConfig output/rv64ui-p-add.out
とかして、勝手に実行されているけど、よく考えると具体的にどのようにして動作しているのか分からない。これを理解するためには、RocketChipがどのようにコンパイルされ、実行されているのかについて知らなければならない。
これを理解するためには、RISC-V FrontEnd Server (fesvr)について理解しなければならない。
RISC-V Rocket Chipが動作するまでの仕組み
まず、VCSシミュレータで実行する場合を見てみる。手元にVCSが実行できないのでmakeの中身を参照しながらだが、
cd vsim make CONFIG=DefaultConfig
とすると、Makefile群として以下が生成されるはずだ。
これらに、RocketChipを理解するための秘密が隠されている。
必要なファイル群
Makefragを参照すると、以下のファイルが必要なことが分かる。jtagはとりあえず無視してもOKだが、SimDTM.v, SimDTM.ccというのはRocketChipの全体制御を管理しているものだ。
bb_vsrcs = \ $(base_dir)/vsrc/jtag_vpi.v \ $(base_dir)/vsrc/plusarg_reader.v \ $(base_dir)/vsrc/ClockDivider2.v \ $(base_dir)/vsrc/ClockDivider3.v \ $(base_dir)/vsrc/AsyncResetReg.v \ sim_vsrcs = \ $(generated_dir)/$(long_name).v \ $(generated_dir)/$(long_name).behav_srams.v \ $(base_dir)/vsrc/$(TB).v \ $(base_dir)/vsrc/SimDTM.v \ $(bb_vsrcs) # C sources sim_csrcs = \ $(base_dir)/csrc/SimDTM.cc \ $(base_dir)/csrc/jtag_vpi.c
また、VCSのコンパイルオプションに、RISC-Vのライブラリが指定されていることが分かる。これがRISC-Vのフロントエンドサーバfesvrだ。
VCS_OPTS = -notice -line +lint=all,noVCDE,noONGS,noUI -error=PCWM-L -timescale=1ns/10ps -quiet \ +rad +v2k +vcs+lic+wait \ +vc+list -CC "-I$(VCS_HOME)/include" \ -CC "-I$(RISCV)/include" \ -CC "-std=c++11" \ -CC "-Wl,-rpath,$(RISCV)/lib" \ $(RISCV)/lib/libfesvr.so \ -sverilog \ +incdir+$(generated_dir) \ +define+CLOCK_PERIOD=1.0 $(sim_vsrcs) $(sim_csrcs) \ +define+PRINTF_COND=$(TB).printf_cond \ +define+STOP_COND=!$(TB).reset \ +define+RANDOMIZE_MEM_INIT \ +define+RANDOMIZE_REG_INIT \ +define+RANDOMIZE_GARBAGE_ASSIGN \ +define+RANDOMIZE_INVALID_ASSIGN \ +libext+.v \
RocketChipのシミュレーション時フロー
前回紹介したように、RocketChipの構造は以下のようになっている。
TestDriver.v
シミュレーションのオプションを管理する部分。$value$plusargs
の処理などはここで行われている。TestDriver内でTestHarnessがインスタンスされている。
TestHarness
TestHarnessの中にインスタンスされているものを具体的に列挙すると、
- ExampleRocketTop : RocketChip本体に当たる。
- SimAXIMem : mem_axi4 が接続されているRAMにあたる。おそらく通常の命令、データアクセスに当たる。
- SimAXIMem_1 : mmio_axi4が接続されているRAMにあたる。MMIOのデータアクセスに当たる。
- SimDTM : RTLのコントロール部になる。
ここではとりあえずRocketChipの本体とか、SimAXIMemはほおっておいて、SimDTMについて着目する。
SimDTM.vを参照する
SimDTM.vを参照してみる。
module SimDTM( input clk, input reset, ... __exit = debug_tick( __debug_req_valid, __debug_req_ready, __debug_req_bits_addr, __debug_req_bits_op, __debug_req_bits_data, __debug_resp_valid, __debug_resp_ready, __debug_resp_bits_resp, __debug_resp_bits_data );
このdebug_tick()
が、毎サイクル実行されるというわけだ。このdebug_tick()
は
if (!dtm) { s_vpi_vlog_info info; if (!vpi_get_vlog_info(&info)) abort(); dtm = new dtm_t(filter_argv_for_dtm(info.argc, info.argv)); }
として定義されており、このdtm_t
がfesvr
に定義されている。このdtmは riscv-tools/riscv-fesvr/fesvr/dtm.cc
に定義されており、ここではコンストラクタを呼んでいるので、
dtm_t::dtm_t(const std::vector<std::string>& args) : htif_t(args) { start_host_thread(); } ... htif_t::htif_t(const std::vector<std::string>& args) : mem(this), entry(DRAM_BASE), sig_addr(0), sig_len(0), tohost_addr(0), fromhost_addr(0), exitcode(0), stopped(false), syscall_proxy(this) { signal(SIGINT, &handle_signal); signal(SIGTERM, &handle_signal); signal(SIGABRT, &handle_signal); // we still want to call static destructors size_t i;
ここで、最初にload_program()
が呼ばれることで、ELFファイルがロードされる。
void htif_t::load_program() { std::string path; if (access(targs[0].c_str(), F_OK) == 0) path = targs[0]; else if (targs[0].find('/') == std::string::npos) { std::string test_path = PREFIX TARGET_DIR + targs[0]; if (access(test_path.c_str(), F_OK) == 0) path = test_path; } if (path.empty()) throw std::runtime_error("could not open " + targs[0]); std::map<std::string, uint64_t> symbols = load_elf(path.c_str(), &mem, &entry); if (symbols.count("tohost") && symbols.count("fromhost")) { tohost_addr = symbols["tohost"]; fromhost_addr = symbols["fromhost"]; } else { fprintf(stderr, "warning: tohost and fromhost symbols not in ELF; can't communicate with target\n"); } // detect torture tests so we can print the memory signature at the end if (symbols.count("begin_signature") && symbols.count("end_signature")) { sig_addr = symbols["begin_signature"]; sig_len = symbols["end_signature"] - sig_addr; } }