lynxeyedの電音鍵盤

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

Chiselを使ったRISC-Vの勉強(5. 分岐命令の実装)

前回の記事と分けてしまったら内容がほとんど皆無になってしまいました。



ないようがないよう



(この辺りでみんなブラウザ閉じる)
年取るってこういうことなんだなぁ。



さて、本題です。
前回riscv-asからアセンブルした機械語をChiselプロジェクトにワンストップで導入する仕組みを構築しました。
分岐命令の相対アドレスを計算するのが大変なのと、immが命令タイプに応じて異なるため、ハンドアセンブルが厳しくなったからです。人為的ミスの低減にもなるはずです。

immの種類多すぎでは

今更感が非常に強いですが、immを整理しました。
命令セットのImmに対応するビット位置がまちまちなので、それをここで統一しました。
CSR関係のzimmもここで整理しました。なお、参考にしたのはこちら。

github.com

ざっとこんな感じです。riscv-miniとほとんど変わらない実装

object ImmGen {
    def apply(sel: UInt, inst: UInt): SInt = {
        val sign = Mux(sel === IMM_Z, 0.S, inst(31).asSInt)
        val b30_20 = Mux(sel === IMM_U, inst(30,20).asSInt, sign)
        val b19_12 = Mux(sel =/= IMM_U && sel =/= IMM_J, sign, inst(19,12).asSInt)
        val b11 = Mux(sel === IMM_U || sel === IMM_Z, 0.S,
            Mux(sel === IMM_J, inst(20).asSInt,
                Mux(sel === IMM_B, inst(7).asSInt, sign)))
        val b10_5 = Mux(sel === IMM_U || sel === IMM_Z, 0.U, inst(30,25))
        val b4_1 = Mux(sel === IMM_U, 0.U,
            Mux(sel === IMM_S || sel === IMM_B, inst(11,8),
                Mux(sel === IMM_Z, inst(19,16), inst(24,21))))
        val b0 = Mux(sel === IMM_S, inst(7),
            Mux(sel === IMM_I, inst(20),
                Mux(sel === IMM_Z, inst(15), 0.U)))

        Cat(sign, b30_20, b19_12, b11, b10_5, b4_1, b0).asSInt
    }

分岐命令

実装した分岐命令は以下に挙げる通りです。WLENは命令1ワードあたりのアドレスバイト長で、今回は4(=32bit)です。imm_XはXタイプの即値immという意味です。

疑似コード

rd = pc + WLEN, pc = pc + imm_J
  • jalr

疑似コード

rd = pc + WLEN, pc = rs1 + imm_I
  • beq

疑似コード

if (rs1 == rs2) then pc = pc + imm_B else pc = pc + WLEN
  • bne

疑似コード

if(rs1 =/= rs2)then pc = pc + imm_B else pc = pc + WLEN
  • blt

疑似コード

if(rs1 < rs2) then pc = pc + imm_B else pc = pc + WLEN
  • bltu

疑似コード

if(rs1.asUInt < rs2.asUInt) then pc = pc + imm_B else pc = pc + WLEN
  • bge

疑似コード

if(rs1 >= rs2)then pc = pc + imm_B else pc = pc + WLEN
  • bgeu

疑似コード

if(rs1.asUInt >= rs2.asUInt)then pc = pc + imm else pc = pc + WLEN

前回のコミットと比較し、すべての分岐命令においてALUを使うように修正しました。回路規模肥大化の抑制を期待できます。

バブルロジックの導入

next_inst_is_validが真の時はそのまま、偽の時は次アドレスの命令をバブルします。
JAL、JALR命令は特に条件なく分岐をしてくれるので、この命令があった場合は次にフェッチしている命令語をバブルします。
Bから始まる分岐命令の場合、条件が真となれば次段でフェッチしている命令をバブルします。偽の場合は分岐しませんので命令はそのまま残します。

// bubble logic
next_inst_is_valid := true.B
switch (id_ctrl.br_type) {

    is( BR_NE ) {
        when(val_rs1 =/= val_rs2) {
            next_inst_is_valid.:=(false.B)} // NEQ = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_EQ ) {
        when(val_rs1 === val_rs2) {
            next_inst_is_valid.:=(false.B)}  // EQ = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_GE ) {
        when(val_rs1 > val_rs2) {
            next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branc
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_GEU ) {
        when(val_rs1.asUInt > val_rs2.asUInt) {
            next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_LT ) {
        when(val_rs1 < val_rs2){
            next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_LTU ) {
        when(val_rs1.asUInt() < val_rs2.asUInt) {
            next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_J  ) { next_inst_is_valid.:=(false.B) }  // JAL
    is( BR_JR ) { next_inst_is_valid.:=(false.B) }  // JALR
}

命令語を間違えて解釈していた

Uが末尾につくアセンブラ命令。bltuやbgeu。
immがunsignedだと本気で誤解していました。

逝ってこいレジスタ。負が扱えないので帰ってこれない。すごい。作ってみてもいいかもですね。(ダメだろ)

正確には、rs1、rs2を比較時にunsignedとして扱う命令でした。ですよね。。immだったら、なんのためにauipc命令あるんだよという話になります。

JAL/JALR命令のテスト

そんなこんないろいろ間違いを治しつつ、テストコードを書きました。
(ここに掲載したのはjalとjalrだけです。bから始まる分岐命令も一応動作を確認済みです)
早くriscv-testを走らせられるレベルになりたい。

_start0:
    nop
    nop
_label1:
    addi  x1,  x0, 1    # x1 = x0 + 1 = 1
    addi  x2,  x0, 2    # x2 = x0 + 1 = 2
    addi  x3,  x0, 3    # x3 = x0 + 2 = 3
    jal   x4,  _label4  # x4に次アドレス(_lavel2)を格納し, _label4へジャンプ

_label2:
    addi  x7, x0, 7     # x7 = 7
    addi  x8, x0, 8     # x8 = 8 (RETURN HERE)
    addi  x9, x0, 9     # x9 = 9
    addi x10, x0, 10    # x10= 0x0A
    addi x11, x0, 11    # x11= 0x0B
_label3:
    jalr  x0, x5,0      # x5に格納されているアドレス(= _label5)へジャンプ

_label4:
    addi x12,x0,12      # x12= 0x0C
    jalr x5, x4, 4      # x5に次アドレス(_lavel5)を格納し, x4(= _label2) + 4のアドレス( addi  x8, x0, 8のところ)へジャンプ→(addi x7, x0, 7)は実行されない

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

結果を見てみます。intelliJであれば画面下部のterminalのタブで

make clean
make test

で動作します。
実行結果。


f:id:Lynx-EyED:20200508144407p:plain
この通り、x7に値は代入されていません。正しく動作していると言えるでしょう。

コード全文

github.com

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

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

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

brew install coreutils

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

nano src/sw/test.s

次回はデータRAMの実装に入ります。