FPGA開発日記

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

RISC-V OoO CPUを実装する (21. 整形、assertion、参照ISSのコンテナ化)

前回で例外情報をレジスタ1個に集約し、riscv-testsは52/54で止まっている。 残り2本は機能が未実装なので、いったん検証側の整備に回った回。 RTL全体の整形、store経路へのassertion追加、参照ISSを動かすための環境作りの3つ。

RTL全体を自作フォーマッタで整形し直す

Emacs側のSystemVerilogパッケージを入れ替えたので、整形規則もそれに合わせた。 パッケージにはCLIが付いているので、リポジトリ全体に一括でかけられる。

find core/rtl common/rtl -name '*.sv' -print0 \
  | xargs -0 <sv-kit>/bin/sv-kit format --write

対象は64ファイル、差分は3932行の入れ替えになった。

zshは変数を単語分割しない

最初は素直に書いて失敗した。

SV=$(find core/rtl -name '*.sv')
sv-kit format --write $SV     # zsh ではファイル名 64 個が「1 つの引数」として渡る

bashなら単語分割されるが、zshはしない。 フォーマッタ側は「そんな名前のファイルは読めない」と言い、こちらは出力の末尾だけ見て成功したと勘違いしていた。 find -print0 | xargs -0に変えて解決した。

意味が変わっていないことをどう確認するか

整形は空白しか変えないはずだが、3932行の差分を目で追うのは現実的ではない。 機械的に確認できる。

for f in $(git diff --name-only); do
  a=$(git show HEAD:$f | tr -d '[:space:]' | md5sum)
  b=$(tr -d '[:space:]' < $f    | md5sum)
  [ "$a" != "$b" ] && echo "CONTENT DIFF: $f"
done

空白を全部落としたうえでハッシュを取り、整形前後で一致するかを見る。 64ファイルすべてで一致したので、トークンの削除や並べ替えは起きていない。 そのうえでunit TB 15本とriscv-testsを流し、数値が完全に同じであることも確認した。

「安全な機械的変更」だと分かっていても、確認手段を持っているかどうかで作業の気楽さが違う。

黙って壊れる経路にassertionを置く

前回までに、storeの経路で2件の問題を直していた。

  1. 分岐予測ミスのflushがLSUに届かず、予測ミスの経路のstoreがSTQに残る
  2. STQがFIFOの先頭を無条件に書き出すので、残ったentryがあると以降のstoreが1つずつずれる

1は配線の修正で済んだが、2は構造の問題が残っていた。 STQはi_cmt_rob_idを受け取っているのに、照合に使っていなかった。

rob_idで照合する

STQのentryにrob_idを持たせ、commitのrob_idと一致したときだけdequeueするようにした。 そのうえでassertionを置く。

assert property (@(posedge i_clk) disable iff (!i_rst_n)
  (i_cmt_valid && !i_flush_all) |->
    (r_stq[r_tail[STQ_W-1: 0]].valid &&
     (r_stq[r_tail[STQ_W-1: 0]].rob_id == i_cmt_rob_id)))
else $error("scariv_stq: commit rob_id %0d does not match tail entry", i_cmt_rob_id);

照合を足しただけだと、ずれたときは「dequeueしない」だけで、ROB側はcommitしたつもりで進む。 症状は数百cycle後に別の場所で出る。 assertionがあれば、ずれたその瞬間に止まる。

1 cycleに2つstoreがretireする場合

ROBのo_cmt_storeは1本で、同じcycleに2つのstoreがretireすると1つしかdequeueされない。 対処は2通りあった。

案 内容
A commitを複数同時に出せるようにし、STQも1 cycleに複数dequeueする
B retireの連鎖をstoreのところで切り、1 cycleに1つに制限する

Bにした。 DTCMのwrite portが1本なので、Aにしても結局1 cycleに1つしか書けない。 memory側のportが増える段階まで、この制約を持っておく方が構造が素直になる。

実装は、分岐や例外でretireの連鎖を切っているのと同じ形で条件を1つ足すだけ。 そのうえで前提をassertionに書く。

assert property (@(posedge i_clk) disable iff (!i_rst_n)
  $countones(w_assert_ret_store) <= 1)
else $error("scariv_rob: %0d stores retired in one cycle", $countones(w_assert_ret_store));

今の条件は前のlaneとしか比較していないので、dispatch幅が2の今だけ厳密になる。 幅を広げたときにlane 0とlane 2がstoreだと通ってしまうが、そのときはこのassertionが検出する。 「今は正しいが前提に依存している」ことを、コメントではなく実行時に効く形で残せる。

参照ISSを動かすための環境作り

次の関門は参照ISSとの命令単位の突合わせで、完了基準は「100万命令でmismatch 0」。 まずISSを動かすところから始めた。

入手先が変わっている

設計ドキュメントにはWhisperのリポジトリをchipsalliance/riscv-whisperと書いていたが、これは404だった。 正しくはchipsalliance/VeeR-ISSで、Western DigitalのSweRV-ISSがVeeRに改称された流れをそのまま引きずっている。 ドキュメントも直した。

ホストに入れられない

Whisperはboost_program_optionsを要求するが、ホストにboostの開発パッケージが入っていない。 sudoが要るので、ここで止まる。

合成側はすでにDockerに逃がしてある (Yosys / OpenSTA / SKY130を焼き込んだイメージがある) ので、検証側も同じ形にした。 sim/dockerに、Verilator、Whisper、riscv64-unknown-elf gcc、elf2hexを入れたイメージを作る。 ELFのビルドからhex変換、54本の実行までコンテナ内で完結する。

ビルドで踏んだもの

素直には通らなかった。順に記録しておく。

  1. Ubuntu 24.04 (GCC 13) ではWhisperがコンパイルできない。 libstdc++のヘッダ内で「instantiation後の部分特殊化」というエラーが出る。 Whisperの更新は2023年で止まっているので、baseを22.04 (GCC 12) にした。ホストのコンパイラとも一致する。

  2. BOOST_DIR=/usrを渡すとビルドが壊れる。 makefileが-isystem $(BOOST_DIR)/includeを付けるため、-isystem /usr/includeとなり、libstdc++が使う#include_next <stdlib.h>の探索順が崩れる。 distroのboostは既定の探索パスにあるので、BOOST_DIRは空のままでよい。

  3. elf2hexに2系統ある。 テストのMakefileが呼んでいるのは位置引数の形式。

   elf2hex <width> <depth> <elf> [base]

一方、sifive/elf2hexは--bit-width / --inputの形式で、base addressも渡せない。 位置引数の方はfesvrに入っているものだった。

  1. Spikeも同じelf2hexを持っているので一度そちらで通そうとしたが、configureがboost_systemのリンクに失敗する。 Ubuntu 22.04のboost_systemはほぼheader-onlyで中身が空なので、リンクテストが通らない。 今回はfesvr単独をビルドした。副の参照ISSとしてSpikeを入れるのは、必要になった時点で改めて考える。

  2. fesvrは2019年で更新が止まっており、dtm.ccに#include <stdexcept>が無い。 昔は別のヘッダ経由で入っていたものが、新しいlibstdc++では入らない。 Dockerfile側で1行足してビルドした。

  3. ホストで作ったobj_dir_*が残っていると、コンテナ内のビルドが失敗する。 Verilatorの生成物に絶対パスが入るため。切り替えるときは消す。

結果

コンテナ内で、unit TB 15本の数値がホストと完全に一致し、riscv-testsも52/54で同じになった。

sim/docker/build.sh              # 初回のみ 15 分程度
sim/docker/run.sh make test-rob
sim/docker/run.sh make riscv-tests
sim/docker/run.sh whisper --help

依存を1つ増やす形ではあるが、boostのような「ホストに入れるには権限が要るもの」が出てきた時点で、環境ごと固める方が結局早い。 バージョンを固定してあるので、他のマシンやCIにもそのまま持って行ける。