lynxeyedの電音鍵盤

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

Chiselを使ったRISC-Vの勉強(10. Load/Store全命令の実装)

実装中のRV32I RISC-V CPUですが、5段パイプラインのまま継続するか、6段パイプラインに増やすか見積もるため、Load/Storeを真面目に実装していませんでした。(今まで対応していたのは32bit長のlw/swのみ)
結論から申しますと、5段パイプラインのまま継続することにしました。詳細は後述する「パイプライン数の見積もり」をご覧ください。また、RV32Iとして一定の完成度になりつつあると思えてきたので、riscv-testsによるリグレッションテストも実施しました。

.dataセクションにも書き込めるようにする

Load/Store命令の実装がISA仕様を満たしているかをtestするにはriscv-testsのうち以下のものをクリアしなければいけません。

  • rv32ui-p-sb
  • rv32ui-p-sh
  • rv32ui-p-sw
  • rv32ui-p-lb
  • rv32ui-p-lbu
  • rv32ui-p-lh
  • rv32ui-p-lhu
  • rv32ui-p-lw

Dumpファイルを見れば分かりますが、上記のコードでは.dataセクションに初期値を置くため、データメモリが実装されていないとテストにパスしません。
今回製作したchiselプロジェクトではchisel-iotestersでコードを命令バス経由でメモリに書き込みます。
データメモリもこの命令バスからでもアクセスできるよう改造しました。


f:id:Lynx-EyED:20200708231604p:plain
なお、Avalon-MMに実装する際はデータバス、命令バスそれぞれから命令メモリ、データメモリをアクセスできるように接続します。データ/命令のアービトレーションは優先度に応じて切り替えられますし、優先度を同一にして優先権をラウンドロビンで移譲する実装もあるので問題とはなりません。
ですので、アンチグリッドロジックの実装は完全にAvalon-MM任せにできるので行いません。intel FPGA便利。

byte/halfword/wordの読み書きに対応する

データメモリ、命令メモリとも32bitアドレス/32bit幅のみのアクセスしかできていませんでしたが、RV32Iの場合load/store命令はbyte(8bit)/halfword(16bit)/word(32bit)のアクセスができないといけません。
これもAvalon-MMバス上ではbyteenable信号によりアラインメントの制約があるものの、8/16/32bitアクセスが可能です。
例えば、下位8bitのみ書き込みがしたい場合、

  • writedata = (32bitデータ)
  • byteenable = 0b’0001

とすれば32bitのうち、下位8bitのみ書き込まれます。

  • writedata = (32bitデータ)
  • byteenable = 0b’1100

とすれば32bitのうち、上位16bitのみ書き込まれます。
参考:
https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/manual/mnl_avalon_spec.pdf


f:id:Lynx-EyED:20200708232004p:plain
Avalon-MM有能です。
Chisel上でのメモリ側の実装は8bit x 4レーンに分け、byteenable信号によって書き込むレーンを選択するロジックを追加しAvalon-MM接続時と同等の環境を用意しました。
CPU側から、データメモリへの書き込みロジック(Store命令)は以下の通りです。

// send bus write size
    io.w_dmem_dat.byteenable := DontCare
    //  mem_rs(1)
    when(mem_ctrl.mem_wr === M_XWR) { // Store命令か?
        when(mem_ctrl.mask_type === MT_B) { // byteライト
            switch(mem_alu_out(1, 0)){ // アドレスの下位2bitでアラインを判断
                is("b00".U){
                    io.w_dmem_dat.byteenable := "b0001".U
                    io.w_dmem_dat.data := mem_rs(1)
                }
                is("b01".U){
                    io.w_dmem_dat.byteenable := "b0010".U
                    io.w_dmem_dat.data := mem_rs(1) << 8.U
                }
                is("b10".U){
                    io.w_dmem_dat.byteenable := "b0100".U
                    io.w_dmem_dat.data := mem_rs(1) << 16.U
                }
                is("b11".U){
                    io.w_dmem_dat.byteenable := "b1000".U
                    io.w_dmem_dat.data := mem_rs(1) << 24.U
                }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordライト
            switch(mem_alu_out(1, 0)){
                is("b00".U){
                    io.w_dmem_dat.byteenable := "b0011".U
                    io.w_dmem_dat.data := mem_rs(1)
                }
                is("b10".U){
                    io.w_dmem_dat.byteenable := "b1100".U
                    io.w_dmem_dat.data := (mem_rs(1) << 16.U)
                }
            }
        }.otherwise { // wordライト
            io.w_dmem_dat.byteenable := "b1111".U
            io.w_dmem_dat.data := mem_rs(1)
        }
    }

CPU側から、データメモリへの読み込みロジック(Load命令)は以下の通りです。

when(mem_ctrl.mem_wr === M_XRD) { // Load命令か?
        when(mem_ctrl.mask_type === MT_B) { // byteリード
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(7)), io.r_dmem_dat.data( 7, 0)) }
                is("b01".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(15)),io.r_dmem_dat.data(15, 8)) }
                is("b10".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(23)),io.r_dmem_dat.data(23,16)) }
                is("b11".U){ wb_dmem_read_data := Cat(Fill(24, io.r_dmem_dat.data(31)),io.r_dmem_dat.data(31,24)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_BU) { // byteリード符号なし
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data( 7, 0)) }
                is("b01".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(15, 8)) }
                is("b10".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(23,16)) }
                is("b11".U){ wb_dmem_read_data := Cat(0.U(24.W), io.r_dmem_dat.data(31,24)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_H) { // halfwordリード
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(15)), io.r_dmem_dat.data( 15, 0)) }
                is("b10".U){ wb_dmem_read_data := Cat(Fill(16, io.r_dmem_dat.data(31)), io.r_dmem_dat.data( 31, 16)) }
            }
        }.elsewhen(mem_ctrl.mask_type === MT_HU) { // halfwordリード符号なし
            switch(mem_alu_out(1, 0)){
                is("b00".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 15, 0 )) }
                is("b10".U){ wb_dmem_read_data := Cat(0.U(16.W), io.r_dmem_dat.data( 31, 16)) }
                // others
                is("b01".U){ wb_dmem_read_data := 0.U }
                is("b11".U){ wb_dmem_read_data := 0.U }
            }
        }.otherwise {
            wb_dmem_read_data := io.r_dmem_dat.data
        }
    }.otherwise {
        wb_dmem_read_data := io.r_dmem_dat.data
    }

パイプライン数の見積もり

上記実装の試行錯誤をしながらパイプライン数の見積もりをしていました。
結論として、

  • Load命令をStore命令にくらべて極端に多用する場合は6段パイプライン(たとえばGPIOの入力を絶えずポーリングするなど)
  • それ以外の場合は5段パイプラインで十分そう

Load命令が連続すると、ストール時間が増えます。加えて

(中略)
Load命令
分岐命令

という順序の場合、Load命令が分岐命令によってフラッシュされないようストール時間がさらに1クロック分増えます。この処理を多用するポーリング動作の場合は6段に拡張した方が処理速度は上がります。
が、低速な8bitマイコンではないのでこういう処理ばかりに専念させないと思われます。割り込み使うし。
加えていうのであれば、そこまでレイテンシ気にする場合はパイプラインマシンにとってコストが高い分岐命令を使うな、というところにまで発展するので。。。
こう言った結論から、5段パイプラインのままにしました。

riscv-testsを通してみた

RV32Iに要求されているテストのうち、以下がパスできたものです。

  • rv32ui-p-add
  • rv32ui-p-addi
  • rv32ui-p-and
  • rv32ui-p-andi
  • rv32ui-p-auipc
  • rv32ui-p-beq
  • rv32ui-p-bge
  • rv32ui-p-bgeu
  • rv32ui-p-blt
  • rv32ui-p-bltu
  • rv32ui-p-bne
  • rv32ui-p-jal
  • rv32ui-p-jalr
  • rv32ui-p-lb
  • rv32ui-p-lbu
  • rv32ui-p-lh
  • rv32ui-p-lhu
  • rv32ui-p-lui
  • rv32ui-p-lw
  • rv32ui-p-or
  • rv32ui-p-ori
  • rv32ui-p-sb
  • rv32ui-p-sh
  • rv32ui-p-simple
  • rv32ui-p-sll
  • rv32ui-p-slli
  • rv32ui-p-slt
  • rv32ui-p-slti
  • rv32ui-p-sltiu
  • rv32ui-p-sltu
  • rv32ui-p-sra
  • rv32ui-p-srai
  • rv32ui-p-srl
  • rv32ui-p-srli
  • rv32ui-p-sub
  • rv32ui-p-sw
  • rv32ui-p-xor
  • rv32ui-p-xori
  • rv32mi-p-breakpoint
  • rv32mi-p-csr
  • rv32mi-p-mcsr
  • rv32mi-p-sbreak
  • rv32mi-p-scall

パスできなかったもの

  • rv32ui-p-fence_i
  • rv32mi-p-ma_addr
  • rv32mi-p-ma_fetch
  • rv32mi-p-illegal
  • rv32mi-p-shamt

なおfence.i命令はこの記事執筆時点(ISA v.2.1)ではRV32/64/128の基本I命令からは外されており、"Zifencei"extensionという拡張扱いになっています。
実装は必須ではありません。命令キャッシュとデータキャッシュのコヒーレント性を保つ唯一の命令ですが、実装コストの大きさ、およびunix系OSで命令フェッチした際のコヒーレンス維持命令としては実用に耐えないと見たようです。
詳しくは以下の、p31,32を参照。
https://github.com/riscv/riscv-isa-manual/releases/download/draft-20200611-d08e29e/riscv-spec.pdf
あと、rv32mi-p-ma_*のテストコード、dumpファイルを見ると16bit命令が埋め込まれているので圧縮命令なのかな?(ma=misalignedってこと?)今回Cには対応しないのでこれはパスしなくていいかなという気持ち。あるいは例外吐く処理を入れるべきかな。。調査中
ミスアライン例外を起こすのが正解でした。
対応します。

その他のものはCSRの実装が現状で不完全なため動作が確認できていませんが、OpenOCDなどデバッガを接続した際に必須となるものばかりなので、実装していきます。

コード全文

github.com

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

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

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

brew install coreutils

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

nano src/sw/test.s