Posts Issued on November 6, 2026

posted by sakurai on November 6, 2026 #1144

Claudeと議論し、既に開発済みの命令デコーダとパイプライン制御を拡張してどのように設計するかの方針を検討しました。従来のwait/cancel制御から現代的なFIFOで構成する方式に変更しています。

構成

  • ISAはRV32I + Zicsr、特権はMモードのみ(予定)。
  • パイプラインは<PC>, <IF>, <ID>, <EX>, <MA>, <WB>の6段です。<PC>は独立した段で、命令のPCは<PC>の最後で確定し、以後パイプラインに沿って流れます。
  • ツールはbsc(bsim / Verilog出力)、iverilog、Verilator、Yosys、Vivado、gtkwaveです。

設計方針

  1. デコーダは構造体を返す純関数にする。 <ID>が decode(Bit#(32)) -> DecInst を呼び、結果を次段へ渡します。パターン一致にはBitPatを使います。命令ごとの関数がDecInstを返し、switchで得た候補のリストをpickに渡すと、一致した命令の値が返ります。どれにも一致しない場合は、リストの最後に常に真の候補を置いて不正命令にします。DecInstには命令種別、ALU演算、rs1/rs2/rd、符号拡張済みimm、分岐条件、ロード/ストア幅、CSR番号、不正命令フラグを入れます。逆アセンブル表示はデバッグ用の関数として別に持ちます。
  2. 段間はパイプラインFIFOでつなぐ。 段間はmkPipelineFIFOです。<PC>→<IF>だけは、中身を読み返すlastメソッドを持つ自作のmkPipeQにします。wait信号もバブルも書きません。段のwaitは「その段のruleが発火しない」ことで表し、ハザードやメモリの応答待ちはruleの発火条件に書きます。下流が詰まれば、上流はFIFOが満杯になって同じサイクルに止まります。
  3. 段間データは素の構造体にする。 <PC>→<IF>は {pc, epoch}、<IF>→<ID>は {pc, epoch, inst}、<ID>→<EX>は {pc, epoch, DecInst, rs1値, rs2値} のように段ごとの型を定めます。FIFOに入っていること自体が「有効」なので、validもMaybeも持ちません。
  4. <PC>はPCの箱から次のPCを作る。 <PC>→<IF>の段間FIFO(mkPipeQ)が、ブロック図のPCの箱です。<PC>はその中身(直前に確定したPCとepoch)をlastで読み、+4します。redirectがあれば分岐先を選び、epochを反転します。<PC>専用のpcレジスタやepochレジスタはありません。PCの箱のリセット値は「リセットベクタ−4」です。
  5. 取り消しはepochで行う。 各命令はepochを1bit運びます。<EX>は分岐が成立したらexEpochを反転し、redirectレジスタ(mkCReg)に分岐先を書きます。redirectは、<PC>が使うまでレジスタに残ります。<ID>と<EX>は、epochが合わない命令をdeqだけしてenqせずに捨てます。<ID>でも比べるのは、誤った命令がハザードで無駄にwaitしないためです。取り消しはハザード判定より先に行います(キャンセルはwaitに勝つ)。
  6. ruleの実行順序を前提に設計する。 パイプラインFIFOではdeqがenqより先なので、ruleは<WB>→<MA>→<EX>→<ID>→<IF>→<PC>の順にスケジュールされます。<EX>が書いたredirectやexEpochを、同じサイクルの<ID>や<PC>がmkCRegの後ろのポートで読みます。<WB>の書き込みを同じサイクルの読み出しで使うため、レジスタファイルは書き込みバイパス付きにします。
  7. バイパスとインターロックは段の中身で判定する。 FIFOでは段の間隔が一定ではない(バブルが詰まる)ので、「ちょうど1wait」のような間隔を前提にした判定は使いません。下流の各段にいる命令のrdを近い段から順に調べ、最初に一致した命令の値を使います。値がまだ無ければ(ロード結果の未着など)、使う側の段のruleが発火しません。見つからなければレジスタファイルの値を使います。この判定は、段の間隔によらず正しく動きます。
  8. メモリはBRAMで作り、キャッシュは同じインタフェースの裏に入れる。 req/data形式の同期BRAMを使い、<PC>でreq、<IF>でdataを受けます。データ側は<EX>でreq、<MA>でdataです。キャッシュと仮想記憶は、プロセッサの完成後に同じインタフェースの裏に入れます。ミス時の応答未着は、<IF>や<MA>のruleが発火しない理由になります。
  9. 検証では任意の段を止められるようにする。 waitの実際の発生源は、メモリの応答未着、マルチサイクル演算器、ハザードです。これとは別に、テストベンチから任意のサイクルで任意の段を止める条件を、各段の発火条件に足せるようにします。

ブロック分割

パイプライン本体(mkProcessor)は6段のruleと段間FIFOだけを持ち、計算や記憶はすべて別モジュールか純関数に出します。段のruleは「部品を呼んで次段へ渡す」だけになり、部品は単体で検証できます。

ブロック分割

灰色の帯が段間FIFOです。<EX>/<MA>から<ID>へのフォワーディングは図では省いています。

ファイル 中身 形 使う段
Types.bsv Addr, Data, Inst, DecInst, 段間構造体(IF2ID など) 型定義 全段
PipeQ.bsv lastメソッド付きの1段パイプラインFIFO(PCの箱) モジュール <PC>・<IF>
Decoder.bsv decode(Bit#(32)) -> DecInst、即値生成を含む 純関数 <ID>
RegFile.bsv 32x32bit、x0固定ゼロ、2読み1書き、書き込みバイパス モジュール <ID>読み、<WB>書き
Alu.bsv alu(op, a, b)、brCond(cond, a, b)、分岐先計算 純関数 <EX>
NextPC(mkProcessor内) PCの箱+4 / 分岐先 / トラップ先の選択、epochの反転 rule <PC>
Mem.bsv req/resp型のメモリIF、BRAMCoreによるIMem・DMem。DMemはバイトイネーブル付き モジュール <PC>・<IF>、<EX>・<MA>
Csr.bsv mstatus, mtvec, mepc, mcause など。ecall/mret、例外の一括判定 モジュール <EX>〜<WB>
Processor.bsv 6段のrule、段間FIFO、発火条件とepochの制御、ハザード検出 モジュール —
Tb.bsv ほか検証環境 プログラムのロード、終了判定、トレース出力、期待値比較 テストベンチ (Claude担当)

後から足す部品は、ICache/DCache(Mem.bsvと同じインタフェースの裏に入れる)、乗除算器、BTB(<PC>での予測)です。

設計順序

「命令が実際に流れる」状態を早く作り、以後は常にriscv-testsで回帰確認できる順にします。各ステップの終わりで確認し、次へ進みます。ハザードは最初はストールだけで正しく動かし、フォワーディングは性能改善として後から入れます。

  1. 検証環境(Claude)
    • RISC-V GCCでriscv-tests(rv32ui-p)とfibo.cをビルドし、$readmemh用の.memを作る。
    • Makefile、bsimとVerilatorの両実行、tohost書き込みでの合否判定、コミットログ出力を用意する。
    • 期待値はISS(Spike)のコミットログと照合する。
  2. Types.bsvと段間FIFO
    • 段間構造体、mkPipelineFIFO、mkPipeQで6段をつなぐ。各段を止めるテスト用の発火条件とepochもここで入れる。
    • 完了条件:任意の段を任意のサイクルで止めるテストで、期待どおりのタイミングになる。
  3. IMemと<PC>/<IF>
    • BRAMCoreでIMemを作り、<PC>でreq、<IF>で命令を受け取る。
    • 完了条件:<ID>で逆アセンブル表示を出し、fiboのobjdumpと一致する。
  4. Decoder、RegFile、ALU
    • decodeでDecInstを作り、<EX>で演算、<WB>で書き戻す。データハザードは<ID>の発火条件で止める。
    • 完了条件:rv32uiの演算系(add, addi, sub, and, or, xor, sll, srl, sra, slt, lui, auipc など)が通る。
  5. DMemとロード/ストア
    • <EX>でアドレス計算とreq、<MA>で受け取り、バイト/ハーフワードの整列と符号拡張。
    • 完了条件:lb/lh/lw/lbu/lhu/sb/sh/swが通る。
  6. 分岐とジャンプ
    • <EX>で分岐を解決し、<PC>へredirect。epoch不一致の命令は<ID>と<EX>で捨てる。予測は「常に非分岐」から始める。
    • 完了条件:rv32ui-p全テストとfiboが通る。
  7. フォワーディング
    • <EX>→<EX>、<MA>→<EX>のバイパスを、方針7の判定で入れる。
    • 完了条件:段をランダムに止めても結果が変わらず、サイクル数が減る(CPIを計測)。
  8. CSRとEIT
    • Zicsr、ecall/ebreak/mret、不正命令・アドレス例外を<MA>で一括判定する。
    • 完了条件:rv32mi-pの主要テストが通る。
  9. キャッシュ
    • プロセッサ完成後の別フェーズ。Memのインタフェースは変えずに裏をICache→DCacheに差し替え、その後に仮想記憶(TLB)を加える。
    • 完了条件:全テストがそのまま通り、ミス率とCPIを計測できる。
  10. 拡張(任意):M拡張、BTB、FPGA実装。

未決事項

  • [ ] ソースの置き場所。git svnでGitHubのprivateリポジトリへ移行する。リポジトリ名待ち
  • [ ] 対象ISAはRV32I + Zicsr、特権はMモードのみでよいか
  • [ ] 最終ターゲットのFPGA(Ultra96、Arty など)。BRAM容量とキャッシュの要否に効く

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