lynxeyedの電音鍵盤

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

Chiselを使ったRISC-Vの勉強(9.CSR:外部割り込みの実装)

外部割り込みを実装しました。
RISC-Vに実装する割り込みはCPU外部からの割り込みだけ(あるいは各割り込み入力をマルチプレクスしてここへ入力、または割り込み信号の判別だけできるようにして入力を増やしていく感じで)でいいかな、と思ってきた次第です。

ターゲットFPGAintel 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'

???


f:id:Lynx-EyED:20200625193918p:plain

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)
 }

実行後。


f:id:Lynx-EyED:20200625202401p:plain
x2 = 0xAAとなっているので無事割り込みトラッピングに成功しています。
またx3 = mepc = 0x2cとなっているので、割り込み発動時にaddi x6, x0, 0xCCを実行中だったことがわかります。
割り込み発動のタイミングによってはx3 = mepc = 0x30となる場合もあると思います。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-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