|
14 |
RISC-Vプロセッサの設計 (39) |
デコーダ、レジスタファイル、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でデータメモリを入れます。
Leave a Comment