FPGA開発日記

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

RISC-V OoO CPUを実装する (20. link addressの落とし穴と、例外レジスタへの集約)

前回はECALLのtrapを入れてriscv-testsの判定を正式化し、メモリ命令まわりを直して49/54まで進めた。 今回は残りのjump系を片付けて52/54にし、検証まわりの整理をしたあと、例外情報をROBのentryから1個のレジスタに集約する作業に入る。

rv64ui-p-jalはtest_2で止まっていた。 commit traceを見る限り、値は正しい。

commit pc=80000198 rd=x4 data=000000008000019c   <- jal tp, target_2
commit pc=800001a8 rd=x2 data=00000000800001a8   <- auipc sp
commit pc=800001ac rd=x2 data=000000008000019c   <- addi sp, sp, -12

止まっているのは次のbne sp,tpで、IQの中身を見ると片方のoperandが永久にreadyにならない。

IQ[2] pc=00800001b0 robid=2 cat=1 rdy=10 prs1=25 prs2=23
prf_ready[23] = 0

prs2はjalが書くはずの物理レジスタだった。

原因はtopのBRU writebackにあった。

w_wb_result[1] = 'h0;
w_wb_result[1].valid         = w_iss_valid[1];
w_wb_result[1].rob_id        = w_iss_uop[1].rob_id;
w_wb_result[1].prd           = w_iss_uop[1].prd;
w_wb_result[1].reg_type      = w_iss_uop[1].uop.rd_type;
w_wb_result[1].branch_taken  = w_bru_branch_taken;
w_wb_result[1].branch_target = w_bru_branch_target;
w_wb_result[1].result        = w_bru_wb_result;

wb_enへの代入が無い。 structを'h0で初期化しているので、常に0のままだった。

wb_enが0だと、PRFへの書き込みも、IQのwakeupも起きない。 条件分岐はrdを持たないので0で正しく、これまで通っていたテストにはlink registerを読む命令が無かったため、表に出ていなかった。

traceが正しく見えた理由

commit traceにはlink addressが正しく出ていた。 これはtraceがROBに載せた検証用の値で、PRFの値ではないからである。 実機で不要なfieldをifdefで囲ってあるので、実装上の分岐はそこで閉じているが、「traceが正しい」ことは「PRFが正しい」ことを意味しない。 traceだけを見て正しいと判断すると、この種のbugは素通りする。

修正はrdを持つ場合だけwb_enを立てる1行で、jalとjalrに加えてauipcも通った。 auipcのテストはjalで飛んでからpc相対の値を確認する構成だったので、link addressが直った時点で一緒に解決した。

52/54になった。

残り2本は未実装の機能

残ったのはfence_iとma_dataで、どちらも配線の誤りではない。

  • fence_i: 自己書き換えcodeのテスト。今はITCMとDTCMが別の配列で、TBが同じimageを両方にロードしている。DTCM側に書いた命令をITCMから読む構成になっていないので、原理的に通らない。次の段階でL1I/L1Dを入れるときに、まとめて整理する必要がある。
  • ma_data: misalignedアクセスのテスト。RISC-Vはhardwareで処理するか、address misaligned例外を出すかのどちらかを選べる。今はどちらでもない。例外を出す実装にしても、このテスト自体は「hardwareで処理する」前提なので通らない。

どちらも「実装方針を決める」作業なので、いったんここで止めて検証側を整えることにした。

検証まわりの整理

検証用trace値の経路

ROBのwriteback側では、bankごとに1組の信号に集約してからpayloadに書いていた。 前回、doneと分岐情報は別経路に分けたが、検証用のtrace値は集約のままだった。 そのため、同じbankに複数portが同時に書き戻すと、別の命令の結果がtraceに出る。

実際、ld_stの調査中にli t2,-35のtraceが別の値になっているのを見ていた。 entryごとに全portを比較するdoneの側に相乗りさせて解決した。

参照ISSとの命令単位の突合わせに進む前に、ここは直しておく必要がある。 traceが別の命令の値になっていると、RTLが正しくてもmismatchが出て、切り分けが難しくなる。

unit testbenchの追従

一連の修正で、3本のTBが動かない状態になっていた。

  • 発行キューのTB: 新しく足したissue readyのportが未接続でbuildできない
  • ROBのTB: flush portの名前変更に追従していない
  • RATのTB: 参照モデルが、修正前のRTLと同じ誤った転送規則を実装していた

3番目が厄介で、参照モデルがRTLと同じ思い込みを共有していると、テストは通るのに両方間違っている状態になる。 今回は参照モデル側を「前のlaneからのみ転送する」形に直し、あわせて2つのケースを足した。

  • 自分のlaneの書き込みは見えないこと
  • 前のlaneが、後ろのlaneの書き込みを見ないこと

これで15本すべてが通る状態に戻った。

名前の整理

参照ISSとの突合わせをこれまでDIFFTESTと呼んでいたが、これはXiangShanのco-simulation frameworkの固有名である。 一般名詞としてのdifferential testingはあるものの、CPU検証でこの綴りを使うと、まずあの仕組みを指す。 自分の実装に他プロジェクトの固有名を使うのは筋が悪いので、COSIMに改名した。

  • defineはCOSIM_ENABLE
  • 設計ドキュメントの用語も統一。ただしXiangShanのDiffTestに言及している箇所は固有名として残す
  • 公開済みの記事はそのままにした

例外情報を1個のレジスタに集約する

ここから設計の整理に入る。

なぜ1個で足りるか

現状、ROBのentryごとにexcpt_validとexcpt_causeを持っている。 しかしretire時にtrapを出すと、その時点でpipelineは全flushされる。 つまり「最も古い例外」だけが意味を持ち、それより若い例外はどのみち消える。

ROB全体で1個のレジスタに集約できる。

構成 entryごと 単一レジスタ
現在 (32 entry) 5 bit × 32 = 160 bit 約12 bit
最終形 (256 entry) 5 bit × 256 = 1280 bit 約15 bit

pcはレジスタに持たせなくてよい。 trapを出す時点でその命令のentryはまだROBにあるので、payloadのpcを読めばよい。

副次的な効果として、writeback側の例外の書き込み経路が消える。 この経路は「同じbankに複数portが書き戻すと1本しか残らない」という問題を抱えたままだったので、それも同時に解消する。

必要な処理

  1. 候補を集めて最も古いものを残す。候補はdispatch (decode時に分かる例外)、writeback (実行時に分かる例外)、すでに保持している1個。
  2. retire時に、各laneのrob_idと保持しているrob_idを比較して発火させる。
  3. flushでクリアする。

比較器の数はdispatch幅とwriteback port数の和に比例し、entry数には比例しない。 これが「entryごとに持たない」ことの効果になる。