chiselでスーパスカラを実装 その5
はじめに
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個もストールさせるのはちょっと、、、)
コードは以下に置きました
Discussion