自作CPUとかをやっていて、RTLとISS(命令セットシミュレータ)でどうしても誤差が生じる部分と言えば、真っ先に思いつくのがサイクル計測部分だ。 RISC-VのCPUとかを設計していると、ベンチマークを流している場合に必ずMCYCLEが一致しない問題にぶちあたる。
MCYCLEの比較を無視すればなんとかなるが、さらにMCYCLEの値を汎用レジスタに格納してメモリにストアしてしまったら大変。そこから先はロード命令や通常の命令ですら一致が取れなくなる。 そこでRTL側のMCYCLEの情報を抜き出してISSにバックポートするという方法を取る。RTLのMCYCLEの値をISSに転送して上書きすることでISSとRTLのつじつまを合わせるという作戦だ。
この方法はベンチマークソフトウェアなどでの検証を継続できるというメリットはあるものの、RTLのMCYCLEの検証はISSとの比較では決してできないという問題がある。常にRTL側を信頼するという前提に立っているので、一致検証以外の方法でMCYCLEの値は検証する必要がある。
それはさておき、SystemVerilogで書いている自作CPUと、SpikeなどのISSを接続する方法について調査した。
SystemVerilog側ではコミットが発生する度にDPI-Cを経由してC++コードを呼び出しており、Spikeを1ステップ実行しては一致比較する。
import "DPI-C" function void step_spike ( input longint rtl_time, input longint rtl_pc, input int rtl_priv, input longint rtl_mstatus, input int rtl_exception, input int rtl_exception_cause, 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 int rtl_wr_rnid, input longint rtl_wr_val ); /* ... 中略 ... */ /* verilator lint_off WIDTH */ step_spike ($time, longint'(committed_rob_entry.inst[grp_idx].pc_addr), int'(u_msrh_tile_wrapper.u_msrh_tile.u_msrh_csu.u_msrh_csr.r_priv), u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_sim_mstatus[u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_out_cmt_entry_id][grp_idx], u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_valid_except_grp_id[grp_idx], u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_except_type_selected, u_msrh_tile_wrapper.u_msrh_tile.u_rob.w_out_cmt_id, 1 << grp_idx, committed_rob_entry.inst[grp_idx].rvc_inst_valid ? committed_rob_entry.inst[grp_idx].rvc_inst : committed_rob_entry.inst[grp_idx].inst, committed_rob_entry.inst[grp_idx].rd_valid, committed_rob_entry.inst[grp_idx].rd_regidx, committed_rob_entry.inst[grp_idx].rd_rnid, w_physical_gpr_data[committed_rob_entry.inst[grp_idx].rd_rnid]);
引数は長いがとりあえず無視。コミットIDとPC、命令ビット、書き込み汎用レジスタアドレス、書き込み値などの情報が転送できていればよろしい。
Spike側だが、汎用レジスタなどの情報を取得して更新することのできるAPIが一応そろっているので、それを活用する。
if (rtl_wr_valid) { int64_t iss_wr_val = p->get_state()->XPR[rtl_wr_gpr_addr]; if ((((iss_insn.bits() & MASK_CSRRW) == MATCH_CSRRW) || ((iss_insn.bits() & MASK_CSRRS) == MATCH_CSRRS) || ((iss_insn.bits() & MASK_CSRRC) == MATCH_CSRRC) || ((iss_insn.bits() & MASK_CSRRWI) == MATCH_CSRRWI) || ((iss_insn.bits() & MASK_CSRRSI) == MATCH_CSRRSI) || ((iss_insn.bits() & MASK_CSRRCI) == MATCH_CSRRCI))) { if (((iss_insn.bits() >> 20) & 0x0fff) == CSR_MCYCLE) { p->set_csr(static_cast<int>(CSR_MCYCLE), static_cast<reg_t>(rtl_wr_val)); p->get_state()->XPR.write(rtl_wr_gpr_addr, rtl_wr_val); fprintf(compare_log_fp, "==========================================\n"); fprintf(compare_log_fp, "RTL MCYCLE Backporting to ISS.\n"); fprintf(compare_log_fp, "ISS MCYCLE is updated to RTL = %0*llx\n", g_rv_xlen / 4, rtl_wr_val); fprintf(compare_log_fp, "==========================================\n");
雑な実装ではあるが、iss_insn.bits()
というのはISSの実行した命令ビット、そしてMASK
とMATCH
の定数はSpikeがビルド時に勝手に用意してくれているので活用させて頂く。
そして対象となるCSRがMCYCLEの場合n以上県が成立し、p->get_state()->XPR.write()
でレジスタアドレスを更新するという訳だ。さらに元となるMCYCLE
のシステムレジスタもp->set_csr()
で更新する。
これをriscv-tests
のベンチマークで実行すると例えば以下のようなログが得られる。
142535 : RTL(23,1) Exception Cause = 27 142535 : 24552 : PC=[00000000800023b4] (23,01) b00027f3 csrr a5, mcycle ========================================== RTL MCYCLE Backporting to ISS. ISS MCYCLE is updated to RTL = 0000000000010e2d ========================================== 142563 : 24553 : PC=[00000000800023b8] (24,01) 00001717 auipc a4, 0x1 GPR[14](73) <= 00000000800033b8 142563 : 24554 : PC=[00000000800023bc] (24,02) ab870713 addi a4, a4, -1352 GPR[14](127) <= 0000000080002e70
RTLのMCYCLEの情報0x10e2d
をISSにバックポートして、MCYCLEの値とGPRの値が更新されたというログが得られた。
これで一応ベンチマークの継続実行ができるようになった。