Chiselを使ったRISC-Vの勉強(9.CSR:外部割り込みの実装)
外部割り込みを実装しました。
RISC-Vに実装する割り込みはCPU外部からの割り込みだけ(あるいは各割り込み入力をマルチプレクスしてここへ入力、または割り込み信号の判別だけできるようにして入力を増やしていく感じで)でいいかな、と思ってきた次第です。
ターゲットFPGAはintel P.S.GのCyclone10LP/GX/MAX10シリーズを考えているのでペリフェラルの実装はPlatform Designerで行います。
各モジュールはCPUの外部かつAvalon-MM上に存在することになります。割り込みはこの外部からの信号のみ受け付けるようにします。
事の発端
タイマー割り込みの調査をしていました。
- mtime : 時間計測用。一定のクロックでカウントアップ。RV32/RV64でともに64bit長
- mtimecmp:時間比較用レジスタ。mtime >= mtimecmpでタイマー割り込み発動。RV32/RV64でともに64bit長
となっており、タイマー割り込み発火のためには以下のようにプログラムを記述します。
- mtvecに割り込みトラップ先のアドレスを格納
- mtimecmpに割り込み発動の時間を格納
- mie.mtie=1にしてタイマー割り込みを許可
なるほどねーと
アセンブラを書いてみました。
addi x1, x0, 0x3FF csrrw x0, mtimecmp, x1
コンパイルすると
Error: unknown CSR `mtimecmp'
???
Machine time register (memory-mapped control register). Machine time compare register (memory-mapped control register).
メモリーマップドなのかーい!
Privileged Architecture Version 1.7を見るとまだmtime
,mtimecmp
は32bit長でCSRレジスタになっており、v.1.9あたりからメモリマップドに変わっています。特にhart>=2であれば(いわゆるマルチコアであれば)割り込み関連のSFRはメモリマップドにする方が全てのhartのSFRに素早くアクセスができ、得策といえます。一見、当たり前のことのなのですが、ちょっとびっくり。
ここで方針を変更します。
CSRからメモリマップドIOにダイレクトにアクセスするのは得策とは言えません。
CPUとリソースの取り合いになるからです。割り込み発動も遅くなります。それにこんな事で調停回路つくるの超めんどくさい。
せっかくメモリーマップドなので、メモリーマップドなタイマーモジュールを作ります。
このモジュールにアクセスするのにCPUがmtime
,mtimecmp
またはそれ相当のSFRをRWするというわけです。
そしてこのモジュールがmtime >= mtimecmp
となったときにCSRレジスタMIP.MTIP=1となるようにします。
割り込み回路実装の検討
ここで外部割り込みでもタイマー割り込みでも仕組みが同じになってしまいました。
違いは割り込み要因が外部割り込みMIP.MEIP=1
かタイマー割り込みMIP.MTIP=1
かの違いです。
タイマー割り込み回路を作る前の素振りとして、外部割り込み回路を作ります。
外部割り込み回路実装
CPUへの実装をしていきます。
- 割り込み入力用の1ビット信号を用意
- 任意のタイミングで割り込みビット=1
- 割り込み発生
- mepcへ発生時のアドレスを格納
- mtvecのアドレスに分岐
簡単です。
このとき、割り込み発生時に分岐命令処理中ですと割り込み信号による例外が握り潰されてしまうことがあるので、pc更新の論理回路が例外発生時の処理を最優先にするよう修正しています。
mepcがゼロになるんだが
mepcは前回のブログでも取り上げた通り例外発生時にいたpcを格納するCSRレジスタです。
実はむずかしいのはmepcの実装でした。理由は以下の通りです。
マルチパイプラインCPUなので、分岐中は実際にデコードや演算処理しないけど便宜上フェッチする(あとでnopにするかフラッシュする)命令があったり、ストール中は一旦pcをゼロにして、分岐完了後に正しいカウンターをフェッチして再カウントしたりします。
という訳で、いつ発動するかわからない割り込みは、場合によっては有り得ないpcを戻り値としてmepcへ格納してしまうことになりかねません。
対策としては、パイプラインが処理中の内容を監視し、ストール中か分岐中かを判別する。もしどちらかの状態であれば、その時点で有効だった最後のpcの値を戻り値としてmepcに格納します。
判別用にpc_invalidという信号を用意しました。
// program counter check val pc_invalid: Bool = inst_kill_branch || (ex_pc === pc_ini)
割り込みが発動した場合かつ分岐中、ストール中(pc=0)はmepcにpcを格納しないようにしました。
この場合は最後に有効だったpcをmepcへ格納します。
それ以外の例外発生時、例えばecall命令などではどうなるかというと、自身が原因でストールが発生、次フェーズで例外要因の分岐が発動しているはずなので上記の処理したpcを戻り値として格納してはいけません。
例外発動するまえのpcの値が格納されるので正確ではなくなる。そのまま現在のpcをmepcへ格納するのが正しいといえます。
とまぁ、src/main/scala/core/csr.scalaを見てもらえればこの辺りは分かっていただけるかと思います。
実行結果
アセンブラを記述します。<0xXX>と書いてあるのはその命令のアドレスです。割り込みが発生すると_label_expc(=0x18番地に飛びます)
_label0: addi x1, x0, 0x18 # <0x00> csrrw x0, mtvec, x1 # <0x04> : mtvec = <0x18> lui x1, 0x01 srli x1, x1, 1 csrrw x0, mie, x1 # enable external interrupt(mie.mtie = 1) jal x0, _label1 # jump _label1 _label_expc: addi x2, x0, 0xAA # <0x18> csrr x3, mepc # csrr x4, mcause # jal x0, _label_expc # <0x24> : loop _label1: addi x5, x0, 0xBB # <0x28> _label2: addi x6, x0, 0xCC # <0x2C> jal x0, _label2 # <0x30>
さて今回は、アセンブラを書いただけだと割り込みが発生したかどうかわかりません。
chisel-iotestersでCPUに擬似的に任意の時間に割り込み信号を注入します。(今回はlp==96としました。割り込み許可フラグが1になった後であればどのタイミングでもOK)
// src/main/scala/core.scalaの578行目付近 (中略) if(lp == 96){ poke(signal = c.io.sw.w_interrupt_sig, value = true.B) } else{ poke(signal = c.io.sw.w_interrupt_sig, value = false.B) }
実行後。
x2 = 0xAA
となっているので無事割り込みトラッピングに成功しています。また
x3 = mepc = 0x2c
となっているので、割り込み発動時にaddi x6, x0, 0xCC
を実行中だったことがわかります。割り込み発動のタイミングによっては
x3 = mepc = 0x30
となる場合もあると思います。コード全文
コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.21 --depth 1 cd KyogenRV/
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。
brew install coreutils
makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。
nano src/sw/test.s