|
6 |
RISC-Vプロセッサの設計 (33) |
Claudeと議論し、既に開発済みの命令デコーダとパイプライン制御を拡張してどのように設計するかの方針を検討しました。
blogから把握した現状
骨格はデコーダとPCパイプラインまでできていて、データパス(レジスタファイル、ALU、メモリ)はまだ繋がっていない、と読みました。この表は2026-11-03時点のblogの記録です。段間接続、ストール制御、valid bitは、下の「現状への意見」2〜4で置き換えます。
| 項目 | 現状 | 出典 |
|---|---|---|
| デコーダ | BitPatによる2段デコード(when節でパターン一致、命令ごとの関数で処理)。関数は逆アセンブル表示をする「ハードウェアインタプリター」型で、riscv-testsがひととおり通る | (2), (5), (6) |
| パイプライン段 | <PC>, <IF>, <ID>, <EX>, <MA>, <WB> の6段。<PC>を独立した段として扱う | (7), (8) |
| 段間接続 | 各段は1つのrule、段間はmkLFIFO(1段パイプラインFIFO)。流れるのはPC値だけ | (24), (27) |
| ストール制御 | pc/if/mc/ex/ma_wait。上流は同一サイクルで停止、下流にはバブルを流す。waitはTbからmkWireで入力 | (28), #677 |
| valid bit | Maybe#(int)を<IF>で試した段階。全段の制御パイプラインには未展開 | (26), (29) |
| 制御理論 | W(S-) = W(S) ∪ W(S+)、V(S+) = N(!W(S) ∪ W(S+)) ∩ !C(S+)。EITは<MA>で一括判定。バイパスは<EX>→<EX>、ロードユースは1waitで<MA>→<EX> | PP(19), PP(15), PP(10) |
| メモリ | プロセッサ側は未着手。別プロジェクトでBRAMCore(mkBRAMCore1Load、req/dataメソッド)によるROM/VRAMのBSV化を実施済み | メモリをBSVで書く(2) |
ツールはbsc(bsim / Verilog出力)、iverilog、Verilator、Yosys、Vivado、gtkwaveを使っています。
現状への意見
方針は妥当だと思います。次に進む前に、以下の7点を決めておくと手戻りが少なくなります。
- デコーダを「実行する関数」から「構造体を返す純関数」に変える。 今の実行ステージは逆アセンブル表示用の暫定品なので、BitPatのパターン一致(1段目)だけを使い、2段目の関数を作り直します(2026-11-03 確認済み)。逆アセンブル表示はデバッグ用の関数として残します。パイプラインでは、<ID>が
decode(Bit#(32)) -> DecInstを呼び、結果を次段へ渡す形が必要です。DecInstには命令種別、ALU演算、rs1/rs2/rd、符号拡張済みimm、分岐条件、ロード/ストア幅、CSR番号、不正命令フラグを入れます。BitPatのwhen節で値を返せるかは未確認なので、最初に試します。不可ならopcode/funct3/funct7のcase文で書き、既存のBitPat版との等価性を検証で担保します。 - 段間はmkPipelineFIFOでつなぎ、BSV流に書く。 wait信号も泡も書きません。段のwaitは「その段のruleが発火しない」ことで表し、ハザードなどは発火条件に書きます。上流はFIFOが満杯になって自然に止まります(PP(19)のwaitの式は、FIFOのハンドシェイクに含まれます)。取り消しは「deqしてenqしない」だけです。redirectは、<EX>がredirectレジスタ(mkCReg)に分岐先を書き、<PC>がそれを使って消します。<PC>専用のpcレジスタは持たず、<PC>→<IF>の段間FIFO(PCの箱)を読み返して+4します。そのためにlastメソッド付きの自作FIFO(mkPipeQ)を使います(2026-11-05 合意。レジスタ+wait案と比較し、同じタイミングでコードが約半分になるため採用)。
- 段間データを構造体にする。 <IF>→<ID>は {pc, epoch, inst}、<ID>→<EX>は {pc, epoch, DecInst, rs1値, rs2値} のように段ごとの型を定めます。FIFOに入っていること自体が「有効」なので、validもMaybeも持ちません。
- 「タグ」はepochビットで実装する。 epochはPCと一緒にPCの箱に入れ、各命令に付けて流します。<EX>は分岐が成立したらexEpochを反転し、<PC>はredirectを受けたら反転したepochを付けて出します。<EX>と<ID>は、epochが合わない命令をdeqだけしてenqせずに捨てます。<ID>で比べるのは、誤った命令がハザードで無駄にwaitしないためです。これはPP(19)のキャンセル信号C(S+)と等価ですが、キャンセルを全段に配線しなくて済み、FIFOで繋いだrule構成と相性が良いです。
- ルールの実行順序を前提に設計する。 パイプラインFIFOではdeqがenqより先なので、ruleは<WB>→<MA>→<EX>→<ID>→<IF>→<PC>の順に並びます。この順序は、FIFOが満杯で上流を同一サイクルに止めることや、<EX>のredirectを同じサイクルの<PC>や<ID>が受け取ること、<EX>/<MA>から<ID>へのバイパスと整合します。一方、<WB>の書き込みと<ID>の読み出しが同じサイクルに起きると、Regは「読み→書き」の順しか許さないので矛盾します。そのためレジスタファイルは、書き込み値を同一サイクルで読めるバイパス付き(RWire使用)で作ります。
- メモリはまずBRAMで作り、キャッシュは後から同じインタフェースの裏に入れる。 mkPromで使ったreq/data形式の同期BRAMは、<PC>でreq、<IF>でdataという(7)の<PC>/<IF>分離とそのまま合います。データ側も<EX>でreq、<MA>でdataです。キャッシュと仮想記憶は、プロセッサが完成した後にメモリの機能拡張として別途入れます(2026-11-03 合意済み)。その時、ミス時の「応答未着」が、<IF>や<MA>のruleが発火しない理由(waitの実体)になります。
- waitの発生源は内部の部品にする。 今のblogの構成ではTbからwaitを入れていますが、実際の発生源はメモリの応答未着、マルチサイクル演算器、ハザード検出で、どれも段のruleの発火条件になります。Tbからは、任意のタイミングで段を止める検証用の条件を発火条件に足せるようにしておきます。
ブロック分割案
パイプライン本体(mkProcessor)は6段のruleと段間FIFOだけを持ち、計算や記憶はすべて別モジュールか純関数に出します。段のruleは「部品を呼んで次段へ渡す」だけになり、部品は単体で検証できます。

灰色の帯が段間FIFO(mkPipelineFIFO)です。<EX>/<MA>から<ID>へのフォワーディングは図では省いています。
| ファイル | 中身 | 形 | 使う段 |
|---|---|---|---|
| Types.bsv | Addr, Data, Inst, DecInst, 段間構造体(IF2ID など) | 型定義 | 全段 |
| 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の箱(mkPipeQのlast)+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と同じIFの裏に入れる)、乗除算器(<EX>のwait)、BTB(<PC>での予測)です。
モジュール単位の設計順序
「命令が実際に流れる」状態を早く作り、以後は常にriscv-testsで回帰確認できる順にしました。各ステップの終わりで確認をいただき、次へ進みます。ハザードは最初はストールだけで正しく動かし、フォワーディングは性能改善として後から入れます。
- 検証環境(Claude)
- RISC-V GCCでriscv-tests(rv32ui-p)とfibo.cをビルドし、$readmemh用の.memを作る。
- Makefile、bsimとVerilatorの両実行、tohost書き込みでの合否判定、コミットログ出力を用意する。
- 期待値はISS(Spike)のコミットログと照合する。
- Types.bsv と段間FIFO化
- mkLFIFOをmkPipelineFIFOに、<PC>→<IF>はmkPipeQに置き換え、wait信号を発火条件に移す。epochフィールドもここで入れる。
- 完了条件:既存のwaitテストが新構成でも同じ波形になる。
- IMemと<PC>/<IF>
- BRAMCoreでIMemを作り、<PC>でreq、<IF>で命令を受け取る。
- 完了条件:<ID>で既存デコーダの逆アセンブル表示を出し、fiboのobjdumpと一致する。
- Decoderの関数化、RegFile、ALU
- decodeをDecInstを返す形にし、<EX>で演算、<WB>で書き戻す。データハザードはスコアボードで<ID>をストールさせる。
- 完了条件:rv32uiの演算系(add, addi, sub, and, or, xor, sll, srl, sra, slt, lui, auipc など)が通る。
- DMemとロード/ストア
- <EX>でアドレス計算とreq、<MA>で受け取り、バイト/ハーフワードの整列と符号拡張。
- 完了条件:lb/lh/lw/lbu/lhu/sb/sh/swが通る。
- 分岐とジャンプ
- <EX>で分岐を解決し、<PC>へredirect。epoch不一致の命令は<ID>と<EX>で捨てる(deqしてenqしない)。予測は「常に非分岐」から始める。
- 完了条件:rv32ui-p全テストとfiboが通る。
- フォワーディング
- <EX>→<EX>、<MA>→<EX>のバイパス。ロードユースは、値がまだ無ければ使う側の段が発火しない形で止める。
- FIFOでは段の間隔が一定でない(泡が詰まる)ので、「ちょうど1wait」のような間隔を前提にした判定(PP(10)の方式)は使わない。下流の各段にいる命令のrdを近い段から順に調べ、最初に一致した命令の値を使う。この判定なら間隔によらず正しい。
- 完了条件:結果は変わらず、サイクル数が減る(CPIを計測)。
- CSRとEIT
- Zicsr、ecall/ebreak/mret、不正命令・アドレス例外を<MA>で一括判定(PP(15)の方式)。
- 完了条件:rv32mi-pの主要テストが通る。
- キャッシュ
- プロセッサ完成後の別フェーズ。MemのIFは変えずに裏をICache→DCacheに差し替え、その後に仮想記憶(TLB)を加える。ミスが<IF>/<MA>のwaitの実体になる。
- 完了条件:全テストがそのまま通り、ミス率とCPIを計測できる。
- 拡張(任意):M拡張(<EX>のwait)、BTB(<PC>での予測)、FPGA実装。
次に確認したいこと
- [ ] ソースの置き場所。git svnでGitHubのprivateリポジトリへ移行すると決定済み(2026-11-03)。リポジトリ名待ち
- [ ] 対象ISAはRV32I + Zicsr、特権はMモードのみでよいか
- [ ] 最終ターゲットのFPGA(Ultra96、Arty など)。BRAM容量とキャッシュの要否に効きます
- [x] 「意見」の1〜7と、設計順序の可否(2026-11-04 確認済み)