lynxeyedの電音鍵盤

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

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は各命令のテストを終えた後、

  1. 汎用レジスタgp(x3)に1(pass)またはそれ以外(fail)を格納
  2. 例外を発生(ecall)
  3. mtvecに格納されているトラップベクタへ
  4. 例外の発生要因と発生時の特権モードの確認
  5. 特権モードが予期しないモードだった場合、gp = gp | 1337を計算し格納。
  6. 無限ループへ(終了)

という動作をします。

実装方針

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で読み込むように指定します。

実行結果


f:id:Lynx-EyED:20200618204708p:plain
gp(x3) = 1となっているので成功です。and命令はちゃんと動くようだ。よかった。
まだ何十個もテスト残ってるけど。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-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で教えていただきました。ありがとうございます。


また、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命令を実装しない方向で進めています