FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

自作RISC-V CPUコアの検証用にDPI-Cを使用してSpikeを接続するための試行(Spikeの仕組み調査、続き)

DPI-Cを用いてSpikeとVerilator上のCPUを接続する構成の検討、実装を進めていった。

まず、RTL側だが、コミットが発生した時点でその情報をDPI-Cを経由してSpikeを制御するC++のコードに伝えなければならない。適当に以下のようにした。

always_ff @ (negedge i_clk, negedge i_msrh_reset_n) begin
  if (!i_msrh_reset_n) begin
  end else begin
    if (|(u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_entry_all_done)) begin
      for (int grp_idx = 0; grp_idx < msrh_pkg::DISP_SIZE; grp_idx++) begin
        if (committed_rob_entry.grp_id[grp_idx]) begin
          /* verilator lint_off WIDTH */
          step_spike ($time, longint'((committed_rob_entry.pc_addr << 1) + (4 * grp_idx)),
                      $clog2(u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_entry_all_done),
                      1 << grp_idx,
                      committed_rob_entry.inst[grp_idx].inst,
                      committed_rob_entry.inst[grp_idx].rd_valid,
                      committed_rob_entry.inst[grp_idx].rd_regidx,
                      w_physical_gpr_data[committed_rob_entry.inst[grp_idx].rd_rnid]);
        end
      end
    end
  end
end

本当はVerilatorのC++で作ったTB側で管理してもいいかもしれないが、Vivado SImと環境を共有したいためこのような実装にしておいた(後で考えてみるとVerilogのTB側をリコンパイルする必要があるので修正がかなり面倒くさいということが分かったが...)。 ここではstep_spike()という関数を用意しており、これがDPI-CでC++Verilogを接続するインタフェースとなる。

import "DPI-C" function void step_spike
  (
   input longint rtl_time,
   input longint rtl_pc,
   input int     rtl_cmt_id,
   input int     rtl_grp_id,
   input int     rtl_insn,
   input int     rtl_wr_valid,
   input int     rtl_wr_gpr,
   input longint rtl_wr_val
   );
extern "C" {
  void initial_spike (const char *filename);
  void step_spike(long long time, long long rtl_pc,
                  int rtl_cmt_id, int rtl_grp_id,
                  int rtl_insn,
                  int rtl_wr_valid, int rtl_wr_gpr_addr,
                  long long rtl_wr_val);
}

initial_spike()はシミュレーション前にSpikeのインスタンスを作成するためのもので、spike.ccをベースにいろいろと移植して作っている。sim_tというのがSpikeのシミュレーション環境本体のようなので、これをC++制御側でインスタンス化してSpikeのオブジェクトを作成した。

  spike_core = new sim_t(isa, priv, varch, nprocs, halted, real_time_clint,
                         initrd_start, initrd_end, bootargs, start_pc, mems, plugin_devices, htif_args,
                         std::move(hartids), dm_config, log_path, dtb_enabled, dtb_file);

で、かなり悩んだのだがこれだけではSpikeはシミュレーションを実行してくれない。どうも命令キャッシュの構成などが特殊になっておりHTIF経由でプログラムをロードしないとRAMが全く空の状態で実行されてしまう。多少汚いがSpike側のコードを改造し、初期化だけを行う以下のルーチンを作成してプログラムをロードすることにした。

  spike_core->spike_dpi_init();
// this class encapsulates the processors and memory in a RISC-V machine.
class sim_t : public htif_t, public simif_t
{
public:
...
  void spike_dpi_init() { htif_t::start();}

なんでこんなのが必要なのかというと、これは以下のコード断片からパクってきたものなのだが、load_program()を呼び出したりするのにいろいろと手順が必要らしい。private()のメソッドだったりして面倒だったので、結局手っ取り早くhtif::start()を実行するために独自の公開メソッドを作成したという訳だ。

int sim_t::run()
{
  host = context_t::current();
  target.init(sim_thread_main, this);
  return htif_t::run();
}

さらに空の命令を5回実行する。Spikeは本来は0x10000から実行が始まって(ここはROM)、そこからPayloadに飛ぶので最初の5命令は無視してよい。このため一致検証を始める前に5命令を飛ばしている。

  spike_core->get_core(0)->reset();
  spike_core->get_core(0)->step(5);

ここまでできれば、あとはRTL側がコミットを発生させるたびにSpikeを実行して結果を比較すればよい。そのためにstep_spike()を作成し、RTL側からレジスタの書き込み地やPCアドレスなどの情報を引き込んでくるようにした。

void step_spike(long long time, long long rtl_pc,
                int rtl_cmt_id, int rtl_grp_id,
                int rtl_insn,
                int rtl_wr_valid, int rtl_wr_gpr_addr,
                long long rtl_wr_val)
{
  processor_t *p = spike_core->get_core(0);
  p->step(1);

  fprintf(stderr, "%lld : PC=[%016llx] %s\n", time, rtl_pc,
          disasm->disassemble(rtl_insn).c_str());
  auto iss_pc = p->get_state()->prev_pc;
  if (iss_pc != rtl_pc) {
      fprintf(stderr, "==========================================\n");
      fprintf(stderr, "Wrong PC: RTL = %016llx, ISS = %016lx\n",
              rtl_pc, iss_pc);
      fprintf(stderr, "==========================================\n");
  }
  if (rtl_wr_valid) {
    int64_t iss_wr_val = p->get_state()->XPR[rtl_wr_gpr_addr];
    if (iss_wr_val != rtl_wr_val) {
      fprintf(stderr, "==========================================\n");
      fprintf(stderr, "Wrong GPR[%02d]: RTL = %016llx, ISS = %016lx\n",
              rtl_wr_gpr_addr, rtl_wr_val, iss_wr_val);
      fprintf(stderr, "==========================================\n");
    } else {
      fprintf(stderr, "GPR[%02d] <= %016llx", rtl_wr_gpr_addr, rtl_wr_val);
    }
  }
  fprintf (stderr, "\n");
}

Spike側はp->step(1)で1命令ずつ実行して、その結果のPCと汎用レジスタ値を取り出して一致比較を行っている。

実行結果。4命令しか実行していないが、どうにか加算結果と同じ命令を実行したSpikeで書き込んでいる汎用レジスタ値が一致することが確認できた。とりあえずまずは第1段階は終了かな。

10585 : PC=[0000000080000000] li      a0, 1
GPR[10] <= 0000000000000001
10585 : PC=[0000000080000004] addi    a1, a0, 2
GPR[11] <= 0000000000000003
10585 : PC=[0000000080000008] addi    a2, a1, 3
GPR[12] <= 0000000000000006
10585 : PC=[000000008000000c] addi    a3, a2, 4
GPR[13] <= 000000000000000a
f:id:msyksphinz:20210216205210p:plain
RTLとSpikeの動的一致検証環境動作の様子