Chiselを使ったRISC-Vの勉強(6.パイプライン化とALUを使用する命令の見直し)
突然ですがいままでシングルラインで組んでいたCPUプロジェクトをいきなり5ステージパイプラインにしました。
いきなり!ステージ
パイプライン化に向けての準備
本来でしたら、シングルステージでまだ実装が終わっていないMEMステージまで実装後複数段パイプラインにするのが筋だとおもいます。
しかしプロジェクトを進めていく上で問題が発生していました。
- 機能が増えていくにつれて各ステージの境目がわからなくなってきていた
- バス上にメモリマップドなメモリがあり、そこから命令の読み込み、データ読み書きをする前提である
- シングルクロック未満(=非同期)でメモリへの読み込み、書き込みは現実的に不可能。例えばアドレスを与える→データを受け取るを考えても最低2クロック必要。
- したがってメモリアクセスが終了するまで次の命令に移行しないようストール機能が必須
- 併せて各機能で分離して動作させるためステージングも必要
- RV32Iの場合、なお、データメモリは8bit/16bit/32bit単位でアクセスができないといけない
- データメモリは32bitバスを前提としているのでバス幅調整に1ステージ分追加するかも見積もる必要がある。
したがってこの時点でシングルパイプラインで実装し続けるメリットは皆無であり、一旦パイプライン化してからデータメモリ、その他機能を追加し全容を把握するのが最適と言えます。なおMEMステージで実装するデータメモリは命令メモリと基本的RWは変わらないので32bit幅のアクセスだけできるようかなり大雑把に実装しています。
予定していたマルチパイプライン化にこの段階で取り組むことにしました。
参考にしたコード
rocket/src/main/scala/dpath.scala · fe9adfe71b90046d7d6b84d0c9fe033b06c9229d · CoDEx / rocket-chip · GitLab
本家のRocket-Chipのブランチから探しても同じものが取得できると思います。
ステージごとにかなり簡潔に記述されているので、自分のプロジェクトに導入する際にも分かりやすかったです。
パイプライン化をしたからと言って、いままでの資産が無駄になることはなく、必要とされるデータのレジスタをステージ分用意するだけです。
例えばCPUのパイプラインが5ステージあって、任意のデータが最終ステージまで必要とされているのであれば、データ x5 のレジスタが必要になるだけです。
任意のデータが3段目のステージまで必要とされているのであれば、データ x3のレジスタが必要、といった具合です。
また各ステージで前段の処理が終わっていないと次のステージに移行できないようであれは適宜ストール信号を発行できるようにします。
この部分は未完成な部分があるので、次回以降に取り扱いたいと思います。
行数がいきなり増えるのでびっくりしますが、各ステージで分けて考えられるようになるので、シングルステージよりも考えやすいと思います。
ここでパイプライン化する際に必須ではないですが併せて修正した部分を取り上げます。
ALUの変更
いままで算術、論理命令だけの演算をしていました。しかし条件分岐もおなじALUを使う事ができるはずです。実際同じ演算を行いますし、ALUと別に条件分岐ごとに比較演算機を外部に作るのは論理回路の浪費に思われます。
条件分岐の処理中に算術演算命令を並行して行うようなハザードが発生するステージはないからです。
そういう並行して動作する構成も面白そうですけどそれはまたの機会に。
というわけで、少し処理を追加する必要がありました。
以下の4つの演算です
- ALU_SEQ (入力された2つの値(in.a, in.b)は等しいか?)
- ALU_SNE (入力された2つの値(in.a, in.b)は等しくないか?)
- ALU_SGE (in.a >= in.bか?)
- ALU_SGEU (UInt(in.a) >= UInt(in.b)か?)
※ALU_SGE (io.op1 >= io.op2か?)、SGEU(SGEの符号なし)はすでに実装済みです。
以下のような実装です。
object ALU { def ALU_ADD: UInt = 0.U(5.W) def ALU_SLL: UInt = 1.U(5.W) def ALU_SEQ: UInt = 2.U(5.W) def ALU_SNE: UInt = 3.U(5.W) def ALU_XOR: UInt = 4.U(5.W) def ALU_SRL: UInt = 5.U(5.W) def ALU_OR: UInt = 6.U(5.W) def ALU_AND: UInt = 7.U(5.W) def ALU_COPY1: UInt = 8.U(5.W) def ALU_COPY2: UInt = 9.U(5.W) def ALU_SUB: UInt = 10.U(5.W) def ALU_SRA: UInt = 11.U(5.W) def ALU_SLT: UInt = 12.U(5.W) def ALU_SGE: UInt = 13.U(5.W) def ALU_SLTU: UInt = 14.U(5.W) def ALU_SGEU: UInt = 15.U(5.W) def ALU_X: UInt = 0.U(5.W) // BitPat("b????") def isSub(op: UInt): Bool = op >= ALU_COPY2 // need sub? def isCmp(op: UInt): Bool = op >=ALU_SLT // Compare op? def isCmpU(op: UInt): Bool = op >= ALU_SLTU // Compare unsigned? def isCmpI(op: UInt): Bool = op(0) // need inverse for compare? //noinspection ScalaStyle def isCmpEq(op: UInt): Bool = !op(3) // EQ or NEQ compare operation? } import ALU._ //noinspection ScalaStyle class ALU extends Module { val io = IO { new Bundle { val op1: UInt = Input(UInt(32.W)) val op2: UInt = Input(UInt(32.W)) val alu_op: UInt = Input(UInt(5.W)) val out: UInt = Output(UInt(32.W)) val cmp_out = Output(Bool()) } } // Shift val op2_inv: UInt = Mux(isSub(io.alu_op), ~io.op2, io.op2).asUInt() val sum: UInt = io.op1 + op2_inv + isSub(io.alu_op) val shamt: UInt = io.op2(4,0).asUInt val shin: UInt = Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL,io.op1,Reverse(io.op1)) val shift_r: UInt = (Cat(isSub(io.alu_op) & shin(31), shin).asSInt >> shamt)(31, 0) val shift_l: UInt = Reverse(shift_r) val slt: Bool = Mux(io.op1(31) === io.op2(31), sum(31), Mux(isCmpU(io.alu_op), io.op2(31), io.op1(31))) val cmp = isCmpI(io.alu_op) ^ Mux(isCmpEq(io.alu_op), (io.op1 ^ io.op2) === 0.U, slt) val w_out = Mux(io.alu_op === ALU_ADD || io.alu_op === ALU_SUB, sum, Mux(io.alu_op === ALU_SLT || io.alu_op === ALU_SLTU, cmp, Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL, cmp, Mux(io.alu_op === ALU_SLL, shift_l, Mux(io.alu_op === ALU_AND, io.op1 & io.op2, Mux(io.alu_op === ALU_OR, io.op1 | io.op2, Mux(io.alu_op === ALU_XOR, io.op1 ^ io.op2, Mux(io.alu_op === ALU_COPY1, io.op1 , io.op2)))))))) io.out := w_out io.cmp_out := cmp } }
実装にあたってはいつものriscv-miniを参考にしました。
github.com
今回追加した機能はmini-riscvには含まれてはいませんので、自前で追加実装する必要があります。
ド・モルガンの法則がわかってればなんてことはないでしょ、、、ってここを実装するのすんごい時間かかった。
高専の数学1の最後の方を20年ぶりくらいに必死に勉強し直す始末に。
みんな社会人になっても宇宙人になっても仙人になってもおじいさんおばあさんになっても数学の勉強やろうね。趣味すらできなくなるから。
chisel-iotestersでプローブする信号の追加
今のところ必要としている信号は以下のものです。適宜追加していきます。
- IF/IDステージ: pc , 機械語
- EXステージ:rs1, rs2, imm
- MEMステージ:ALU演算結果
- WBステージ: ALU演算結果とrd
アセンブラを組む
前回のものに少し手を加えただけです。
今回の動作確認における実質的な内容は変わらないので各行の日本語のコメントは省略します。
test.s
に以下を記述します
_start0: addi x29, x0, 10 # x29 = x0 + 10 = 10 addi x31, x29, 0xAA # x31 = x29 + 0xAA = 180 (= 0xB4) _label1: addi x1, x0, 1 # x1 = x0 + 1 = 1 addi x2, x0, 2 # x2 = x0 + 2 = 2 addi x3, x0, 3 # x3 = x0 + 3 = 3 addi x4, x0, 4 # x4 = x0 + 4 = 4 addi x5, x0, 5 # x5 = x0 + 5 = 5 addi x6, x0, 6 # x6 = x0 + 6 = 6 addi x7, x6, 7 # x7 = x6 + 7 = 13 (= 0x0D) addi x8, x0, 8 # x8 = x0 + 8 = 8 jal x4, _label4 # x4 => address(_label2), jump _label4 _label2: addi x9, x0, 9 # x9 = 9 addi x10, x0, 10 # x10= 0x0A addi x11, x0, 11 # x11= 0x0B _label3: jalr x0, x5,0 # jump to x5 (= _label5) _label4: addi x12, x0, 12 # x12= 0x0C jalr x5, x4, 4 # x5 => _label5, jump x4+4 (= _label2+4) _label5: jal x0, _label5 # forever loop nop
動作確認
make clean make
で動作確認を行う事ができます。IntelliJのユーザはTerminalで一旦上記コマンドを実行してからrunしてみてください。(runだけだと機械語ファイルがない場合エラーになる)
ここではおもにアセンブラファイル2行目のaddi x31, x29, 0xAA
に注目したいと思います。
x29の値(0xAが格納されている)とimmの値(0xAA)の足し算の結果をx31に書き戻すという命令です。
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.8 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s
次回はハザードの解決に取り組みます。