前回はECALLのtrapを入れてriscv-testsの判定を正式化し、メモリ命令まわりを直して49/54まで進めた。 今回は残りのjump系を片付けて52/54にし、検証まわりの整理をしたあと、例外情報をROBのentryから1個のレジスタに集約する作業に入る。
jalで止まる: link addressがPRFに書かれていない
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本しか残らない」という問題を抱えたままだったので、それも同時に解消する。
必要な処理
- 候補を集めて最も古いものを残す。候補はdispatch (decode時に分かる例外)、writeback (実行時に分かる例外)、すでに保持している1個。
- retire時に、各laneのrob_idと保持しているrob_idを比較して発火させる。
- flushでクリアする。
比較器の数はdispatch幅とwriteback port数の和に比例し、entry数には比例しない。 これが「entryごとに持たない」ことの効果になる。