Posts Issued on November 14, 2026

posted by sakurai on November 14, 2026 #1150

デコーダ、レジスタファイル、ALU(Step 3)

Step 3で、パイプラインを流れる命令が初めて計算をするようになりました。デコーダ、レジスタファイル、ALUを入れ、データハザードは<ID>で待たせます。メモリを読まないriscv-tests(rv32ui-p)30本が通るところまで来ました。

やったこと

  • <ID>:命令をデコードし、レジスタファイルから2つ読みます。
  • <EX>:ALUの入力のMuxを通して演算します。aはrs1かpc(auipc)、bはrs2(R形式)かimmです。分岐の判定と分岐先の計算もここで行います。
  • <WB>:レジスタファイルに書き戻します。
  • データハザード:読むレジスタを書く命令が下流に残っていれば、<ID>のruleが発火しません。フォワーディングはまだありません。

各段のruleは「部品を呼んで次段へ渡す」だけで、デコーダ、ALU、分岐判定は純関数、レジスタファイルは別モジュールです。

デコーダ

デコーダは、命令コードを受け取ってDecInstという構造体を返す純関数です。DecInstには、命令の種類、ALU演算、rs1/rs2/rd、符号拡張済みのimmなどが入ります。

パターンの一致にはBitPatを使いました。命令の形(R形式、I形式、B形式…)ごとに、DecInstを作る関数を1つずつ書きます。BitPatは、命令コードからvの位置のビットを切り出して、その関数の引数に渡します。以前に書いたBitPatの記事です。

when(pat(n(7'b0000000), v, v, n(3'b000), v, n(7'b0110011)), rType(Add)),   // add
when(pat(n(7'b0100000), v, v, n(3'b000), v, n(7'b0110011)), rType(Sub)),   // sub
when(pat(v, v, n(3'b000), v, n(7'b0010011)), iAlu(Add)),                    // addi
when(pat(v, v, v, v, n(3'b000), v, v, n(7'b1100011)), bType(Eq)),          // beq

1行が1命令で、命令表とほぼ同じ形で読めます。どれにも一致しない命令は、リストの最後に置いた「常に一致する候補」に当たり、不正命令になります。

なお、BitPatはbscに付属するライブラリではありません。CTSRD-CHERIのBitPatを、手を加えずにリポジトリに入れて使っています。

レジスタファイルとデータハザード

レジスタファイルは32本×32bitで、x0は常に0です。書き込みバイパスを付けたので、<WB>が書いた値を、同じサイクルの<ID>が読めます。ruleが<WB>から<ID>の順に動くので、このバイパスが使えます。なお、ファイル名はRFile.bsvにしました。BSVのライブラリにRegFileがあり、名前がぶつかるためです。

ハザードの判定は、設計方針どおり段の中身で行います。<ID>は、このサイクルの移動が終わった後に<MA>と<WB>の入口にいる命令のrdを調べ、読むレジスタと一致すれば発火しません。そのために、この2つの段間FIFOを中身を覗けるFIFO(mkStageQ)にしました。<EX>に命令が残っている場合は、<EX>の入口のFIFOが満杯なので、<ID>はどのみち発火しません。

次の表は、fiboの先頭をKanataで見たものです。

命令 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
80000000 auipc x2,0x10 PC IF ID EX MA WB
80000004 addi x2,x2,0 PC IF ID ID ID EX MA WB
80000008 jal x1,80002000 PC IF IF IF ID EX MA WB
8000000c PC IF ID
80000010 PC IF
80002000 lui x15,0x80002 PC IF ID EX MA WB
80002004 addi x14,x15,104 PC IF ID ID ID EX MA WB

2行目のaddiは、直前のauipcが書くx2を読みます。サイクル3と4は、auipcがこのサイクルの移動の後に<MA>、<WB>の入口にいるので、<ID>で待ちます。サイクル5は、auipcが<WB>で書く値をバイパスで読んで、<ID>を出ます。その上流の命令も、wait信号を書かなくても段間FIFOが満杯になって止まります。直前の命令の結果を使うと2サイクル待つことになりますが、これはStep 6のフォワーディングで縮めます。

分岐をStep 3に前倒し

設計方針では分岐はStep 5の予定でした。しかし、rv32uiの演算のテストは、bneでループし、最後にtohostへswで書いて合否を伝えます。分岐が無いとテストが動かないので、<EX>での分岐の解決をこのStepに入れました。取り消しのためのepochはStep 1からあるので、足したのは分岐条件の判定と分岐先の計算の数行だけです。ストアはまだメモリに書かず、コミットの記録に載せるだけにしています。

どう確かめたか

  • デコーダ単体:Step 2の逆アセンブルのテストと同じ9431命令をデコーダに通し、Pythonで独立に書いた期待値と、DecInstの全部の欄を比べました。
  • 命令の実行:メモリを読まないrv32ui-pの30本(演算、lui/auipc、jal/jalr、全分岐)が通りました。合否だけでなく、1命令ごとのコミットログ(書いたレジスタと値)も、自作のISSと1行ずつ一致しました。
  • タイミング:Step 1から続く、段をランダムに止めるテストも、全部通りました。

bsimとVerilatorの両方で確認しています。

できたこと

命令が実際に計算し、分岐し、結果がレジスタに残るようになりました。データハザードも、wait信号を1本も書かずに<ID>の発火条件だけで止められています。残るrv32ui-pのテストはロードとストアで、次のStep 4でデータメモリを入れます。


左矢前のブログ 次のブログ右矢