📖

自作仮想CPUのパイプライン処理を改良した その2

2024/04/03に公開

はじめに

chiselで仮想CPUを自作しています。スーパスカラ実行の実現を目指しています。
今回は、前回に引き続きその前段階の準備として取り組んだ内容をまとめます。

今回の課題

今回の取り組んだ内容は、「1演算を複数サイクルかけて実行する」です。
chiselで足し算、掛け算などの演算を行う場合は1サイクルで演算は終了しますが、実際の回路ではそれらは複数サイクルかかります。これをchiselでも模擬できるようにしようというのが目的です。
このために、以下を行いました。

  • EXステージ:演算が完了するまでに指定のサイクルかかるようにする
  • それ以外のステージ:演算が完了するまでは処理を止める

以下で詳細を説明します。

EXステージの改良

上述の通り、演算を完了するまでに複数サイクルかけられるようにします。このためには以下が必要です。

  1. 完了までのサイクル数を指定
  2. 実行開始条件が整ったら演算開始
  3. 指定のサイクル経過したら演算を終了し、「完了フラグ」を立てる
  4. 実行中はIDステージからの指令を受け取らない

1 完了までのサイクル数の指定

演算の開始と終了を判定する部品ExecControllerを作成し、ALUの一部とします。
ExecControllerはインスタンス生成時に完了までのサイクル数を引数としてとるようにします。

2 実行開始条件が整ったら演算開始

「実行開始」を指示する入力startExecをExecControllerのIOに追加します。
条件が整ったらstartExecを立てます。

実行開始できない場合とは、

  • 開始直後で最初の指令がEXステージまで到達していない
  • その演算ユニットに非対応の指令(ALUに対するmul指令など)

そこで、各ユニットは、演算部に渡す指令に対して

  • 初期値はBUBBLE
  • IDから受け取った指令が自分のユニットで処理できるものの場合、それで置き換え
    ということをし、演算部は上記指令がBUBBLEでない場合は「実行開始」と判断します。

複数の演算器で使いまわせるように、この判定を行う部分をクラス化します。

class StartJudgeCircuit extends Module{
  val io = IO(new Bundle{
    val exe_fun = Input(UInt(WORD_LEN.W))
    val start = Output(Bool())
  })
  
  io.start := Mux(!(io.exe_fun === BUBBLE), true.B, false.B)
}

 ---
  val aluStartJudge = Module(new StartJudgeCircuit())
  aluStartJudge.io.exe_fun := alu.io.exe_fun
  alu.io.startExec := aluStartJudge.io.start

ExecControllerは、startExecが立つと、カウンタを初期化します。

3 指定のサイクル経過したら演算を終了し、「完了フラグ」を立てる

ExecControllerは、counterを1サイクルに1ずつ増やしていきます。
引数で与えられたサイクル数に到達すると、「完了フラグ」を立てます。
なお、レジスタの性質上counterの値が1増えるのは次のサイクルからなので、counter + 1を比較対象としています。

class ExecController(execCycle: Int) extends Module {
  val io = IO(new Bundle {
    val startExec = Input(Bool()) 
    val execComplete = Output(Bool())
  })

  val onExec = WireDefault(false.B)
  val counter = RegInit(0.U(log2Ceil(execCycle + 1).W))

  onExec := io.startExec

  when(io.startExec && !onExec) {
    counter := 0.U
  }

  when(onExec) {
    counter := counter + 1.U
  }

  val execComp = io.startExec && ((counter + 1.U) === execCycle.U)
  when(execComp) {
    onExec := false.B
  }

  io.execComplete := execComp
}

ExecControllerを用いたALUは以下のようになります。

class ALU(execCycle: Int) extends Module{

  val io = IO(
   new Bundle{
	 // 略 指令値(Input)及び結果(Output)
     val execComplete = Output(Bool())
     val startExec = Input(Bool())
   }
 )

  val cntlr = Module(new ExecController(execCycle))
  cntlr.io.startExec := io.startExec

  //略 実際の演算処理

  io.execComplete := cntlr.io.execComplete
}

4 実行中はIDステージからの指令を受け取らない

これは指令の受け渡しにキューを用いることで実現します。
「完了フラグ」が立ったらキューから次の指令を受け取ります。
キューインスタンスのio.deq.readyを落とせば指令は取り出されなくなります。

val instBuf = Module(new Queue(gen = new EXInSignals, entries = INST_BUF_DEPTH))
  instBuf.io.enq.bits.pc := id_reg_pc
//略 指令を入れる処理

instBuf.io.deq.ready := eXUnit.io.aluExecState.execComplete
eXUnit.io.input := instBuf.io.deq.bits

それ以外のステージの改良

EXステージが実行中の場合、それ以外のステージは処理を止める必要があります。

IF、IDステージ

EXステージが実行中の場合、stallするようにします。
変数stall_flagを立てるとstallする仕組みは作成済みですので、これを利用します。

ex_wait_flag := eXUnit.io.aluExecState.onExec
//略
stall_flag := (id_rs1_data_hazard || id_rs2_data_hazard || ex_wait_flag)

また、IDステージは、stall中はEXステージに指令を渡さないようにします。でないと同じ指令が連続して処理されてしまいます。

instBuf.io.enq.valid := !stall_flag

MEM、WBステージ

今のつくりだと、EXステージは例えば完了に2サイクルかかるならば2回分の指令をMEMステージに渡してしまいます。
ID-EX間のようにキューにしてもいいですが、今回はEXステージが実行中の場合は処理を無効化するような対応とします。
MEMやWBの処理を行うか否かのフラグmem_wen、rf_wenが既に存在しますので、EXステージからMEMステージに受け渡す際に、実行完了していない場合はこれらを「処理実行しない」(MEN_X、REN_X)に設定します。

  io.result.mem_wen := MEN_X
  io.result.rf_wen := REN_X
  when(io.aluExecState.execComplete){
     io.result.mem_wen := reg_mem_wen  //IDから受け取った値
     io.result.rf_wen := reg_rf_wen
  }

結果

以下のプログラムを実行します。

00000000 <main>:
   0:	00300513          	li	a0,3
   4:	00200593          	li	a1,2
   8:	00a58633          	add	a2,a1,a0
   c:	c0001073          	unimp
  10:	00000513          	li	a0,0
  14:	00008067          	ret

ALUの実行サイクルは2とします。

val alu = Module(new ALU(2))

以下のようになりました。
id_reg_pc: IDステージが処理しているpc
deq_ready: EXステージが指令を取り出せるか
exec_pc: EXステージが処理しているpc
op1_data、op2_data: EXステージが処理しているオペランドのデータ
start: EXステージが処理開始か
execComplete: EXステージで処理完了か

各行2サイクルで処理しており、2回目でexecCompleteとdeq_readyが立つことが確認できました!

参考

細かい話は以下に書きました。
https://www.rm48.net/post/chisel-queueについて
https://www.rm48.net/post/chisel-queueに入れたデータを取り出すのが1サイクル遅れる

最後に

準備が整ったので次はいよいよスーパスカラ実行に取り掛かります

Discussion