Chiselを使ったRISC-Vの勉強(8.CSR:例外の実装と割り込みの準備)
riscv-testsをパスしたい
RV32Iと謳うにはriscv-testsのテストパタンセットのうちRV32I ISAに要求されているリグレッションテストをパスするのが望ましいと言えます。CSR周りはその限りではありません。(picorv32などの特権命令非搭載かつ例外処理が独自、という実装もある)
github.com
このパターンセットをパスすることにより、RISC-Vのアーキテクチャとして正しく実装されているという指標になります。
ここではriscv-testsのrv32ui-p-and.dump
からどのような挙動をするのかをみてみます。
実装が正しければ最終的にpass,正しくない実装があるとfailにジャンプします*1。
開始アドレスはリンカスクリプトlink.ldを書き換えて0x0000_0000からはじまるようにしています。(詳細後述)
(略) 0000062c <fail>: 62c: 0ff0000f fence 630: 00018063 beqz gp,630 <fail+0x4> 634: 00119193 slli gp,gp,0x1 638: 0011e193 ori gp,gp,1 63c: 05d00893 li a7,93 640: 00018513 mv a0,gp 644: 00000073 ecall 00000648 <pass>: 648: 0ff0000f fence 64c: 00100193 li gp,1 650: 05d00893 li a7,93 654: 00000513 li a0,0 658: 00000073 ecall 65c: c0001073 unimp 660: 0000 unimp 662: 0000 unimp
pass
でもfail
でもecall命令を使い例外を発生させています。なおpass
するとgp(x3) = 1となります。
ecallはmtvecに格納されているベクタアドレスに遷移します。
118: 00000297 auipc t0,0x0 11c: eec28293 addi t0,t0,-276 # 4 <trap_vector> 120: 30529073 csrw mtvec,t0
上記のように、mtvecにはtrap_vector
アドレスが格納されていることがわかります。
00000004 <trap_vector>: 4: 34202f73 csrr t5,mcause 8: 00800f93 li t6,8 c: 03ff0863 beq t5,t6,3c <write_tohost> 10: 00900f93 li t6,9 14: 03ff0463 beq t5,t6,3c <write_tohost> 18: 00b00f93 li t6,11 1c: 03ff0063 beq t5,t6,3c <write_tohost> 20: 00000f13 li t5,0 24: 000f0463 beqz t5,2c <trap_vector+0x28> 28: 000f0067 jr t5 2c: 34202f73 csrr t5,mcause 30: 000f5463 bgez t5,38 <handle_exception> 34: 0040006f j 38 <handle_exception> 00000038 <handle_exception>: 38: 5391e193 ori gp,gp,1337 0000003c <write_tohost>: 3c: 00001f17 auipc t5,0x1 40: fc3f2223 sw gp,-60(t5) # 1000 <tohost> 44: ff9ff06f j 3c <write_tohost>
trap_vector
に遷移したのち例外発生要因mcause
を調べています。
- mcause = 8:ユーザーモードでのecall
- mcause = 9:スーパーバイザーモードでのecall
- mcause = 11:マシンモードでのecall
となります。今回はマシンモードでの暫定実装ですのでecallでの遷移時は11以外は格納されません。
それ以外の場合、
gp = gp | 1337
が実行されます。ですのでtestに成功してもmcauseの要因がいい加減ですとgp=1が得られなく、テストは失敗に終わります。
なんで1337なんだろ。まあいいか。
なお、割り込み時もmcauseのMSBビット(RV32の場合はmcause[31]が1になる)以外、挙動は同じです。
と、ここまで考えてきたように、CSRを最低限riscv-testsをパスできるレベルで実装しなければいけなくなってきました。
riscv.org
もう一度まとめると、
riscv-testsは各命令のテストを終えた後、
- 汎用レジスタgp(x3)に1(pass)またはそれ以外(fail)を格納
- 例外を発生(ecall)
- mtvecに格納されているトラップベクタへ
- 例外の発生要因と発生時の特権モードの確認
- 特権モードが予期しないモードだった場合、gp = gp | 1337を計算し格納。
- 無限ループへ(終了)
という動作をします。
実装方針
CSRのうち、以下を実装しなきゃいけないことがわかります。めんどくさいなぁ。
・ecall命令
・mtvec
・mepc(例外発生したpcを格納)
・mcause
・mstatus(最低限。マシンモードのみ実装)
「めんどくさい」というのはここまで実装しておいて今更ですが、CSR周りはPrivileged ISAのバージョンアップ毎にガラリと仕様が変わる為、あまり頑張りたくない感じなのです。
これまでの先人たちの実装をgithubで探すと、ある機能を果たすレジスタ名が同じでアドレスだけ異なっていたり、読み書きの権限が異なっていたりとなかなかカオスです。
ただし、例外を実装してしまえば(誤解を恐れずにいうのであれば)汎用組込みマイコンとしてマストな機能である割り込みの実装ができたようなものなので…それをモチベーションに頑張ります。
参考にしたのはいつも通り、riscv-miniです。
github.com
しかし、今回目指している実装はPrivileged Architecture Version 1.11に準拠していますが、この実装はVersion 1.7に準拠している為、このままの実装はできません。(mtvecは読み込みしかできない実装だったし)
あと、いま制作しているプロジェクトは5ステージパイプラインですが、riscv-miniは3ステージです。データフォワーディングなどの仕組みが若干異なります。
riscv-testsの導入
基本的にREADMEにあるようにすれば導入できます。
git clone https://github.com/riscv/riscv-tests cd riscv-tests
ここで命令メモリ開始アドレスが異なっている場合は修正します。
nano env/p/link.ld
自分の場合は開始アドレスが0x0000_0000
だったので下記のように変更しました。
SECTIONS { . = 0x00000000; # ->ここを修正した .text.init : { *(.text.init) } . = ALIGN(0x1000); .tohost : { *(.tohost) } . = ALIGN(0x1000); .text : { *(.text) } . = ALIGN(0x1000); .data : { *(.data) } .bss : { *(.bss) } _end = .; }
保存後ビルドします。
git submodule update --init --recursive autoconf ./configure --prefix=$(インストールしたいフォルダ) make make install
prefixで指定したフォルダに何やらたくさん入りますが、欲しいバイナリは(インストールフォルダ)/share/riscv-tests/isa
に格納されています。
今回はand命令のコードをテストしようと思います。拡張子なしでrv32ui-p-and
というファイル名が存在していると思いますが、実態はelfファイルです。自分のプロジェクトで読み込めるようにオリジナルhexファイルに変換します。
riscv64-unknown-elf-objcopy --gap-fill 0 -O binary rv32ui-p-and rv32ui-p-and.bin od -An -v -tx4 -w4 rv32ui-p-and.bin > rv32ui-p-and.hex
これをsrc/main/scala/core/core.scala
で読み込むように指定します。
実行結果
まだ何十個もテスト残ってるけど。
コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.20 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s
mepcはどのアドレスを格納すべきなのか
分岐命令とジャンプ命令では、
rd = (分岐が起こったアドレス) + 4.U
を脊髄反射で格納していました。mepcも同じように(例外発生アドレス)+4.Uを格納して良いのでしょうか。
twitterで教えていただきました。ありがとうございます。
MEPCは「例外が発生したPC」を格納するという定義ですので値を勝手に進めたりしません。しかも+4 進めてしまうとC命令で例外が発生すると再実行できなくなります。
— msyksphinz_dev (@dev_msyksphinz) 2020年6月14日
また、specificationにも以下の記述があるとslackでも教えていただいきました。
When a trap is taken into M-mode, mepc is written with the virtual address of the instruction that was interrupted or that encountered the exception. Otherwise, mepc is never written by the implementation, though it may be explicitly written by software.
トラップが発生しマシンモードに遷移すると、mepcには割り込み命令や、例外を発生させた命令の仮想アドレスが書き込まれます。それ以外の場合は、ソフトウェアによって明示的に代入されることはあるかもしれませんが、実装によってmepcがwriteされることはありません。
なるほどー。なので、もし復帰する際は自分で例外発生したアドレスに戻るか、その次の命令のアドレスにいくか記述してやる必要があるとのこと。
おしまい。
*1:Compress命令はまた異なる挙動になります。今回C命令を実装しない方向で進めています