💨

chiselでスーパスカラを実装 その5

2024/08/15に公開

はじめに

chiselで仮想CPUのスーパスカラ実行を実装しています。
前回でMEMステージの並列化までできました。
今回はいよいよ最終段階WBステージの並列化を行います。

構成

概要

  • 前段(MEMステージ)から2つの信号が渡される
  • この入力を受け取るキュー(入力キュー)を揃える。MEMステージの実行が完了したら信号をキューに入れる
  • レジスタへの書き込みユニットを2つ備える。それぞれが入力キューを持つ。
  • 各入力に対し、それを処理するユニットを決め、そのユニットの入力キューに入れる。
  • 書き込みユニットは、前の指令の処理が完了したら割り当てられた新しい指令を入力キューから取り出して処理
  • 双方のユニットで処理が完了したら「完了信号」を立てる

構成自体はMEMステージとほとんど同じです。

詳細 書き込みユニット

書き込みユニットが行うのは

  • 書き込むレジスタのアドレスを計算
  • レジスタに書き込み
  • 書き込みが終わったら完了信号を立て、次の指令を取り出し
    です。
    ただ、レジスタについては、読み込みは他のステージ(IDステージ)から行うため、WBステージの中に閉じ込めてしまうことはできません。かといってアドレス計算のようなことはなるべくWBステージの中に閉じ込めてしまいたいです。

ということでレジスタの読み書きを管理するレジスタ管理ユニットを作成しました。

  • 書き込むアドレスや値の計算はWBユニットで行う
  • 書き込みユニットからアドレスと値をレジスタ管理ユニットに入力。書き込みはレジスタ管理ユニットで行う
  • レジスタ管理ユニットの書き込みが完了したら書き込みユニットの完了信号も立てる

詳細 レジスタ管理ユニット

  • 2つの書き込みポート、4つの読み込みポートを備える
    • 読み込みは1指令で2個読む場合があるため
  • 読み込みポートに入力されたアドレス値を出力
  • 書き込みポートに入力されたアドレスに、入力された値を書き込み
  • ポートごとに実行状況を出力
    • 実行中、実行完了、など

コード

class WBUnitMgr() extends Module{

  val io = IO(
    new Bundle{
      val in = Input(Vec(FETCH_NUM, new MemOutSignals))
      val prevComplete = Input(Bool())
      val wbInfs = Output(Vec(WB_UNIT_NUM, new WbInf))
      val unitExecStates = Input(Vec(WB_UNIT_NUM, new EXOpState))
      val complete = Output(Bool())
      val out = Output(Vec(FETCH_NUM, new WbOutSignals))
    }
  )
  //Input to buffer
  val inBufs = Seq.fill(FETCH_NUM)(Module(new Queue(gen = new WbInSignals, entries = INST_BUF_DEPTH)))
  for(i <- 0 until FETCH_NUM){
    inBufs(i).io.enq.bits := WBUnit.createInSignals(io.in(i))
    inBufs(i).io.enq.valid := io.prevComplete
  }
  
  //Init router
  val wbInRouter = Module(new WBInRouter())
  val wbUnits:Seq[WBUnit] = Seq.fill(FETCH_NUM)(Module(new WBUnit()))

  Seq.tabulate(FETCH_NUM){
    i => {      
      wbInRouter.io.inputs(i) := inBufs(i).io.deq.bits  //1
      inBufs(i).io.deq.ready := true.B
      
      val unitInf = Wire(new RoutUnitInf)
      unitInf.name := i.U
      unitInf.instInBuf := wbUnits(i).io.bufCount
      wbInRouter.io.outUnitInfs(i) := unitInf

      wbUnits(i).io.in := wbInRouter.io.results(i).signal
      Seq.tabulate(FETCH_NUM){
        j => {
          val unitIdx = wbInRouter.io.results(j).unitName  //2
          when(i.U === unitIdx){
            wbUnits(i).io.in := wbInRouter.io.results(j).signal
          }
        }
      }

      wbUnits(i).io.unitExecState := io.unitExecStates(i)

      io.out(i) := wbUnits(i).io.out
    }
  }

  io.complete := wbUnits.map(_.io.execState.execComplete).reduce(_ && _)  //3
}

1, 2:入力信号と書き込みユニットを接続。wbInRouterはj番目の信号が割り当てられるユニット番号をio.results(j).unitNameに出力

3: すべてのwbUnitでio.execState.execCompleteがtrueになったらWBステージとしての完了信号(io.complete)を立てる

レジスタ管理ユニット(regMgr)との接続は以下の通り

  Seq.tabulate(MEM_UNIT_NUM){
    i =>     
    regMgr.io.wrPorts(i).start := wbUnitMgr.io.out(i).fetch_id > 0.U
    regMgr.io.wrPorts(i).addr := wbUnitMgr.io.out(i).wb_addr  //1
    regMgr.io.wrPorts(i).val_in := wbUnitMgr.io.out(i).wb_data

    wbUnitMgr.io.unitExecStates(i) := regMgr.io.wrPorts(i).execState  //2
  }

1: 書き込みアドレス、書き込み値は書き込みユニットが計算した値を入力
2: regMgrの実行状況を書き込みユニットの実行状況とする

テスト

ユニットテスト

実行完了信号の立つタイミングが正しいを確認

    "Output signals" should "be correct" in {
		test(new WBUnitMgr()){ mgr =>

			mgr.io.in(0).pc.poke(4.U)
			mgr.io.in(1).pc.poke(8.U)
			mgr.io.in(0).fetch_id.poke(1.U)
			mgr.io.in(1).fetch_id.poke(1.U)
			mgr.io.in(0).rf_wen.poke(REN_S)
			mgr.io.in(1).rf_wen.poke(REN_X)
			mgr.io.prevComplete.poke(true.B)
			mgr.io.unitExecStates(0).onExec.poke(false.B)
			mgr.io.unitExecStates(1).onExec.poke(false.B)
						
			mgr.io.complete.expect(false.B)

			mgr.clock.step(1)  //inBufsから取り出し
			mgr.io.complete.expect(false.B)

			mgr.clock.step(1)  //wbUnitに入力
			mgr.io.unitExecStates(0).execComplete.poke(true.B)
			mgr.io.unitExecStates(1).execComplete.poke(true.B)
			mgr.io.complete.expect(true.B)
		}
	}

他にもregMgrと接続し、レジスタに値が書き込まれることも確認しました

全体

他のユニットとも接続してシステム全体をテストします。要所要所で信号値をprintfで出力して確認します。
実行するコードは以下です。

int main() { 

  asm volatile("li a0, 3");
  asm volatile("li a1, 2");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("add a2, a1, a0");

  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");
  asm volatile("nop");

  asm volatile("unimp"); 

  return 0;
}

3行目以降のnopは1、2行目の結果がレジスタに書き込まれるの待ち、最後のnop一群はadd指令が最後のステージまで行くの待ちで入れています。(まだハザード解決はできていません)

以下がダンプ結果です

00000000 <main>:
   0:	00300513          	li	a0,3
   4:	00200593          	li	a1,2
   8:	00000013          	nop
   c:	00000013          	nop
  10:	00000013          	nop
  14:	00000013          	nop
  18:	00000013          	nop
  1c:	00000013          	nop
  20:	00000013          	nop
  24:	00000013          	nop
  28:	00000013          	nop
  2c:	00000013          	nop
  30:	00000013          	nop
  34:	00000013          	nop
  38:	00000013          	nop
  3c:	00000013          	nop
  40:	00000013          	nop
  44:	00000013          	nop
  48:	00000013          	nop
  4c:	00000013          	nop
  50:	00000013          	nop
  54:	00000013          	nop
  58:	00000013          	nop
  5c:	00000013          	nop
  60:	00a58633          	add	a2,a1,a0
  64:	00000013          	nop

実行結果は以下のようになりました。

~略~

***WB***
wb0_out.pc: 0x00000000
wb1_out.pc: 0x00000004
wb0_out.wb_data: 0x00000003
wb0_out.wb_adr: 0x0a
wb1_out.wb_data: 0x00000002
wb1_out.wb_adr: 0x0b

~略~

***HazardResolver***
hr_0_out.pc: 0x00000060
hr_1_out.pc: 0x00000064
id_rs_datas(0): 0x00000002
id_rs_datas(1): 0x00000003

~略~

ex0_unit_out.pc: 0x00000060
ex1_unit_out.pc: 0x00000064
ex0_unit_in.op1_data: 0x00000002
ex0_unit_in.op2_data: 0x00000003
ex1_unit_in.op1_data: 0x00000000
ex1_unit_in.op2_data: 0x00000000
ex0_unit_out.alu_out: 0x00000005
ex1_unit_out.alu_out: 0x00000000

まずpc=0、4の結果(2、3)がwbステージでレジスタに書き込まれ、pc=60でaddを実行する際、それらのレジスタ値が読み込まれています。
そしてEXステージにて正しい計算(2+3=5)が行われています!

最後に

長いこと取り組んできたスーパスカラ実行が(色々制限事項はつけましたが)できるようになって一段落です!

今後は制限の解除およびパフォーマンスの向上を目指していきます。
まずやるのはハザード解決、フォワーディングですかね(さすがにliとaddの間に20個もストールさせるのはちょっと、、、)

コードは以下に置きました
https://github.com/mr16048/mycpu/tree/superscala_WB

Discussion