lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

Chiselを使ったRISC-Vの勉強(7.ハザードの解決)

MEMステージの実装中です。
制御ハザードはすでに分岐命令を実装した時点で実装しており、パイプライン化をしても特筆すべき点はありません。
ここではデータハザードを直していきます。
データハザードの解決が終わると、MEMステージをこのままにするか、もう1ステージ増やして6段パイプラインにするかが見積もりやすくなるはずです。

データハザードの解決

命令は上から下に実行されていきます。横軸は時間で、各命令のどのステージが進行中かがわかるようになっています。


f:id:Lynx-EyED:20200531143535p:plain
ここで、正確な値の入力を必要とするのは、EXステージです。同じ汎用レジスタに連続でアクセスする場合、これが問題となることがあります。
例えばaddi命令。

  • IF ステージ:命令フェッチ
  • IDステージ:rs1,rs2決定
  • EXステージ:ALUの入力と命令が確定
  • MEMステージ:ALUからの演算結果(addiの場合は足し算の値)確定
  • WBステージ:rdへALUからの演算結果代入


f:id:Lynx-EyED:20200531144814p:plain
addiの結果代入先のrdが、次命令のrs1,rs2のいずれかになると大変困ります。
rd確定前に、次命令がrs1,rs2が確定しているべきステージ(IDステージ)を過ぎているからです。

例は少し変わりますが、以下のような命令が考えられます。

lui     x1,  0x08     # x1 = 0x08 << 12 = 0x8000
addi    x1,  x1, 0x04 # x1 = x1 + 0x04 = 0x8004となるべきだが.....?

このままだと、最終的なx1の値は0x04となってしまいます。x1の値がまだWBステージまで到達する前に次の命令のEXステージで計算が開始されてしまうからです。

データフォワーディング

ここでハザードが起こることは分かりました。
解決していきます。この場合はデータフォワーディングすることによってこの問題に対処します。
rdに結果が代入されるのはWBステージですが、一つ前のMEMステージで値は確定しています。ですのでMEMステージで次命令のEXステージに結果をバイパスします。


f:id:Lynx-EyED:20200531145458p:plain



他にデータフォワーディングが必要とされる状況が考えられるでしょうか。
データメモリにアクセスする命令でハザードが多発すると思われます
(例1)

addi  x2 x0, 0xAA  # x2 = 0xAA
sw    x2, 5(x31)   # dmem[x31+5] = x2
lw    x3, 5(x31)   # x3 = dmem[x31+5]

ここでx3は0xAAとなるべきです。先ほど、データハザードの対策を適切にしたはずなのでこの場合でも正しい値が出るはずです。
でも以下の場合はどうでしょうか。
(例2)

addi  x2 x0, 0xAA  # x1 = 0xAA
nop
sw    x2, 5(x31)   # dmem[x31+5] = x2
lw    x3, 5(x31)   # x3 = dmem[x31+5]

「nopも入れてるんだし当然動くでしょ」と思ってたのですが、x3=0。しばらくデータメモリの実装を疑いましたが異常はありませんでした。
というか(例1)で動いているのでデータメモリの実装がおかしいはずはないのですが。。。


f:id:Lynx-EyED:20200531151901p:plain
1行目の命令がWBステージに到達する前に3行目の命令はIDステージに突入しています。ですのでここではWBステージからMEMステージにデータフォワーディングしてあげる必要があります。


f:id:Lynx-EyED:20200531212006p:plain
この部分をコードで記述すると以下のようになりました。信号名は参考にしたrocket-chipのものと同等です。

val ex_reg_rs1_bypass: UInt = MuxCase(ex_rs(0), Seq(
        (ex_reg_raddr(0) =/= 0.U && ex_reg_raddr(0) === mem_reg_waddr && mem_ctrl.rf_wen === REN_1) -> mem_alu_out,
        (ex_reg_raddr(0) =/= 0.U && ex_reg_raddr(0) === wb_reg_waddr && wb_ctrl.rf_wen === REN_1 /*&& wb_ctrl.mem_en === MEN_1*/) -> io.r_dmem_dat.data
    ))
    val ex_reg_rs2_bypass: UInt = MuxCase(ex_rs(1), Seq(
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === mem_reg_waddr && mem_ctrl.rf_wen === REN_1) -> mem_alu_out,
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === wb_reg_waddr && wb_ctrl.rf_wen === REN_1 && wb_ctrl.mem_en === MEN_1) -> io.r_dmem_dat.data,
        (ex_reg_raddr(1) =/= 0.U && ex_reg_raddr(1) === wb_reg_waddr && ex_ctrl.rf_wen === REN_0 && ex_ctrl.mem_en === MEN_1) -> wb_alu_out
    ))

データストールによる解決

他にどんな場合があるでしょうか。
WBステージでないと値が確定できない命令があります。Load命令全般です。
この場合はMEMステージから値だけもらってフォワーディングする、といった技が使えません。ストールを行うことで対処します。

(Load命令 && 次命令がLoad命令が使用したアドレスをrs1,rs2に使う) → ストール信号生成

ストール信号を発行するとpcの更新がとまり、ストールしたいステージのレジスタの更新が停止することになります。今回、IDステージを停止させるので、以下の図のようにIDステージが2回実行され、その時のEXステージはNOPとなります。


f:id:Lynx-EyED:20200531213000p:plain
しかしこの実装はあまり筋が良いとは言えません。データが次のステージで必ず読み込める前提になっているからです。
密結合メモリをFPGAの内部に作り込む前提である場合はここは問題とはなりませんが、FPGA外部のSDRAMなどにデータメモリを持つ場合はこの実装では動かないでしょう。
今回の実装の場合、メモリ側からackが帰ってくるまで待つのが正しい実装と言えます。ackが帰ってくるまで永久にIDステージを繰り返します。*1

f:id:Lynx-EyED:20200531214100p:plain
このストール信号の部分のコードの記述は以下のようになります。

var mem_stall: Bool = RegInit(false.B)

when (ex_ctrl.mem_wr === M_XRD) {
        mem_stall := true.B
} .elsewhen(io.r_dmem_dat.ack === true.B) {
        mem_stall := false.B
}
// メモリリード信号が発行されてから、メモリからACK信号(データ送信済み)がくるまでmem_stall信号をtrueにする。 

load_stall := ((ex_reg_waddr === id_raddr(0) || ex_reg_waddr === id_raddr(1)) && (ex_ctrl.mem_en === MEN_1) && (ex_ctrl.mem_wr === M_XRD)) || mem_stall

テストコード

今回の実装を包括的に網羅するアセンブラを記述します。

_label1:
        lui     x1,  0x08           # x1 = 0x08 << 12
        addi    x1,  x1, 0x04       # x1 = x1 + 0x04 = 0x8004
        addi    x2,  x1, 0x00      #  x2 = x1 

        sw      x2, 12(x31)        #  dmem[x31 + 12] = x2 = 0x8004
        lw      x3, 12(x31)        #  x3 = dmem[x31 + 12] => 0x8004になっているべき


        addi    x4, x0, 0xAA       # x4 = 0xAA
        sw      x4, 12(x31)        # dmem[x31 + 12] = x4 = 0xAA
        lw      x5, 12(x31)        # x5 = dmem[x31 + 12] = 0xAA

        beq    x4, x5, _label2   # x4 = x5 なら_label2へジャンプ
        addi    x10, x0, 0x44       
        addi    x11, x0, 0x55



 _label2:
        jal x0, _label2             # 無限ループ

実行結果


f:id:Lynx-EyED:20200531215028p:plain
実行できています。
x10,x11に値が代入されてしまっている場合は、beq命令が正しく評価されていない=>前命令のload命令でx4が格納される前に実行されてしまった可能性があります。


所感

ようやく5ステージパイプラインを持つCPUっぽくなってきました。さらにストールやフォワーディングが必要になった場合は今回のコードに追記する形でできると思いますので、そんなに辛くなさそう。
あと、ストールのところで「メモリリード信号が発行されてから、メモリからACK信号(データ送信済み)がくるまでmem_stall信号をtrueにする。」としていますが、ハザードは解決されたもののちょっとタイムロスが発生しているので、もう少し工夫してみようかなと思います。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.15 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

次回は?

この一ヶ月ほど、駆け足でやってきました。
コロナの影響で止まっていた製造関係も少しづつ再開が始まってきました。
通常作業がいつもの80%くらいまで復活してきています。*2


ですのでRISCV関係は少しペースは落としていかなければならないと思いますが、引き続き続けていきますのでよろしくお願いいたします。

*1:RAMが破壊されている場合も考慮した方がいい場合もあります。例外吐く処理を実装した方が最善かもしれません

*2:疫病の危機が去ったわけではありません。自分や他の人の健康や命に対し無頓着であってはなりません。しかし、このブログはそう言った話題に乗じてなにか政治的社会的プロパガンダを発したりはこれまでもしてきませんでしたしこれからも同様です。露ほども為にはならないかもしれませんが、面白いと思う人に面白いと思われるのならそれが本望です。うんこ。