FPGAボード(Sipeed Lichee Tang Primer)用に設計しているオリジナル16ビットCPUコアMoscoviumの割り込み機能を強化したいと思います。
今回解決しようとしている課題は、(1)ベクタ割り込み方式に対応させることと、(2)割り込みレベルを用いた多重割り込みに対応させること、の2点です。本来、組み込み用途のMCUに有利な割り込み機能を備えようとします。
ただし、後方互換性を維持しながら強化することにします。過去に作ったプログラムが動作しなくなってしまう様では、プログラムの修正や書き換えに要する労力がバカにならないためです。
今回の機能強化では、割り込みコントローラINTCとCPUコアに機能追加が必要となります。後方互換性の維持を前提として、INTCとCPUコアは、最新のデザインで統一してしまいます(旧デザインは破棄)。
※互換性維持の為に仕様が複雑化して行くのは好まないけど、互換性を維持することによって、旧バージョンを破棄できるのなら、保守の簡素化と差し引き0な感じです。
強化前、Moscoviumの割り込みは割り込み要因(原因、ソース)が何であっても、常に割り込みベクタivの指すアドレスに分岐します。割り込みによって実行されるプログラムを割り込みサービスルーチン(ISR;Interrupt Service Routine)と呼びます。
Moscoviumの場合、ISRで最初に実行すべき仕事は、何の要因で割り込みがかかったのか?を知ることです。
MoscoviumのINTC322Dコントローラでは、割り込みの原因となった割り込み要因を割り込み番号intnumレジスタの読み出しや、割り込み要因intfctレジスタを読み出すことで知ることができます。
割り込みは、優先度が高くて、割り込みの起きたその瞬間に処理すべき、重要な仕事を実行するために使われます。にもかかわらず、ISRの最初の仕事が、「え〜?何の要因で割り込み掛かったの〜?」って、調べるところから始まるのです。……のんびりし過ぎですね(1^^;)。
一方、多くの組み込み用マイコンでは、ベクタ割り込み方式が採用されています。
ベクタ割り込み方式では、割り込み要因毎に別々なISRアドレスを指定することができ、従って特定のISRに飛んできた時には、その割り込み要因は既知(自明)なので、すぐさま必要な処理を開始することができます。
ベクタ割り込み方式では、割り込みベクタivが指し示すアドレスにベクタテーブルを配置します。以下にベクタテーブルのサンプルを示します。
// ================================
// interrupt vector table
vectab:
bra interr // 00: (reserved)
datw lab_interr // 01: ICR1
datw lab_interr // 02: ICR2
datw lab_extint0 // 03: EIR0
datw lab_interr // 04: EIR1
datw lab_interr // 05: SRAR
datw lab_interr // 06: STAR
datw lab_interr // 07: MRAR
datw lab_interr // 08: MTER
datw lab_interr // 09: (reserved)
datw lab_interr // 10: CMBR0
datw lab_interr // 11: CMAR0
datw lab_interr // 12: OVFR0
datw lab_interr // 13: (reserved)
datw lab_interr // 14: CMBR1
datw lab_interr // 15: CMAR1
datw lab_interr // 16: OVFR1
datw lab_interr // 17: (reserved)
datw lab_interr // 18: (reserved)
datw lab_interr // 19: (reserved)
datw lab_interr // 20: (reserved)
datw lab_interr // 21: (reserved)
datw lab_interr // 22: LOFR
datw lab_interr // 23: RTCR
datw lab_interr // 24: CENR
datw lab_interr // 25: SMUR1
datw lab_interr // 26: SMRR1
datw lab_interr // 27: FTAR1
datw lab_interr // 28: FRAR1
datw lab_interr // 29: SMUR2
datw lab_interr // 30: SMRR2
datw lab_interr // 31: FTAR2
datw lab_interr // 32: FRAR2
// interrupt error routine
interr:
// something to do
rti
// EIR0 interrupt service routine
extint0:
// something to do
rti
こちらで各割り込み要因に割り当てた「割り込み番号」を、そのまま「ベクタ番号」と呼ぶこととします。割り込み番号の範囲は1〜32(※)としてあるので、ベクタ番号も同じ1から始まる番号となります。
※割り込み番号intnumレジスタから読み出される値は、割り込み番号(1〜32)×2です。割り込み要求が発生していなかった場合にintnum=0が読み出される仕様となっているため、割り込み番号は1以上の値に設定してあります。
各割り込み要因に対するISRアドレスを「割り込みベクタiv+ベクタ番号×2」のアドレスに格納した物が、ベクタテーブルです。
上に例示したベクタテーブルでは、外部端子割り込みEIR0だけに有効なISRアドレス(extint0)が設定されているので、EIR0割り込みを受け付けると正しく割り込み処理が実行されます。必要に応じて、割り込みを受け付けたいベクタに、有効なISRアドレスを設定してください。
また、ベクタ番号0番は存在しないため、Ivレジスタで指定したアドレス+0番地のデータ(1ワード、2バイト)は不問です。上のコード例では、エラー処理に飛ばすための「bra interr」を配置しています。データは不問なので、ダミーデータでもnop命令等でも問題ありません。
割り込みとは、他の処理よりも優先して処理しなければならない仕事を実行するために存在します。現時点(拡張前)では、Moscoviumの割り込みは許可状態と禁止状態の2状態しかありません。
もし、割り込みで処理すべき重要な仕事が複数あったとしても、その中の何れかの割り込みを処理している間、他の割り込みが多重にかかることはできません。つまり、割り込みで処理すべき仕事が複数あった場合、その優先順位は「受け付け順(あるいは、発生順)」なのです。
多くの組み込み用マイコンでは割り込みレベルを複数持っていて、より高い優先度の割り込みは、より低い優先度の割り込み処理(ISR)中からでも、多重に割り込むことができるようになっています。逆に、より高い優先度の割り込み処理(ISR)中に、より低い優先度の割り込み要因が発生しても、低い方は待たされます(下図)。
割り込みで実行すべき仕事を、優先度に従った割り込みレベルに割り当てることで、複数の割り込み処理を優先順位に従ってタイムリに実行することができる仕組みです。
今回Moscoviumでは、各割り込み要因に2ビットの割り込みレベルを割り当てようと思います。
2ビットなので、割り込みレベルは0〜3の4通りです。割り込み優先度の高い⇔低いを、割り込みレベル0⇔2の3通りに割り当てます。レベル3は、「割り込みマスク」状態ですので、割り込みを受け付けるレベルとしては3レベルとなります。
後ほど説明する割り込みレベルレジスタintlev0〜3に設定する値です。こちらは、各割り込み要因毎に設定する「割り込みレベル」です。
※以降、割り込みの優先度を議論する場合は、「割り込みレベルが高い/低い」と表現し、割り込みレベルの数値そのものを指す場合は、「割り込みレベルが小さい/大きい」と表現します。例として、「割り込みレベルが高い」とは、「割り込みレベルの数値が小さい」ことを指します。「高低」と「大小」の表現に注意して読んでください。
更に紛らわしいですが、もう一つ「割り込み許可レベル」と言う物もあります。今度はCPU側から見た割り込みを受け付けるレベルです。今しがた定義した「割り込みレベル」の内、CPUに設定した「割り込み許可レベル」より高い「割り込みレベル」の割り込みが、受け付けられます。
つまり、CPU側から見ると、割り込み許可レベル=0の状態が割り込み禁止状態、割り込み許可レベル=3だと、全ての割り込みを受け付ける状態となります。
後ほど説明する、状態レジスタsrのIEフィールドに設定する値です。
過去作ってきたプログラムをすべて修正しなくてはならなくなってしまうと大変な作業量となりますので、後方互換性の維持は絶対条件です。本節では、互換性を維持するための設定ビットについて考察します。
強化するのは、ベクタ割り込み方式と、割り込みレベル方式の2点なので、各々、有効化/無効化する設定ビットを持って、リセット解除後の初期値を「無効」にしておくと、積極的にビットを設定しない限り従来機能と変わらぬ割り込み機能を使用することができます。
この方式を主に主導するのはCPU側なので、CPUが設定ビットを持つのが妥当です。
CPUが割り込みを受け付けた時、割り込み先ISRの先頭アドレスを設定したり、ベクタ割り込み方式ではベクタテーブルの先頭アドレスを指定したりするのに、割り込みベクタivを使います。
Ivレジスタが指し示すのは、フェッチROM内のワードアドレス(偶数バイトアドレス)なので、Ivレジスタには偶数アドレスを設定しなければなりません。すると、Ivレジスタの最下位ビットは常に0であるはずなので、ここに、ベクタ割り込み方式有効ビット(VEビット)を割り当てます(→プログラマモデルで確認)。
過去のプログラムでは、Ivレジスタに偶数アドレスを設定していたはずですからVE=0であり、ベクタ割り込み方式が無効の、従来動作となります。
ベクタ割り込み方式を有効にするためには、Ivレジスタにベクタテーブルアドレスを設定する時に、以下の様にしてVEビット=1を設定する必要があります。
ldc iv,lab_vectab+ivec_ve // VE=1:ベクタ割り込み方式有効
ivec_veは、共通インクルードファイル(mcoc115.incl)で定義されています。「equ ivec_ve,0x0001」です。
割り込みレベルに従って、一番優先度の高い(割り込みレベルの数値としては小さい)割り込みを選び出してCPUに割り込み要求信号intc_irqを出力するのは、INTCユニットなので、この方式の有効/無効ビットは、INTCユニットの制御レジスタに設けます。
INTCユニットには適切なレジスタが無かったので、割り込み制御intctlレジスタを追加して、ここにLEVEビットを設けることにしました。新設したintctlレジスタを含めた、INTCユニットのI/Oレジスタ一覧は、こちらを更新しておきました。
過去のプログラムでは、今回新設したintctlレジスタに値を設定しているはずがないので、LEVEビットはリセット後の初期値0です。従って、割り込みレベル無効となって、従来通りの動作となります。
これにて、割り込みレベルを有効にするにはintctlレジスタのLEVEビット=1とすること、ベクタ割り込み方式を有効にするにはIvレジスタのVEビット=1とすること、が必要であり、それをしなければ、各々、従来の割り込み機能と互換動作となることが保証されました。
今年(2022年)は、深紅の大優勝旗⚾が初めて白川の関を越えたと話題となりました(その後、少しケチが付きましたが……)。
例えば高校野球32校が甲子園で戦うと仮定します。本当はもっと出場校は多いのでしょうが、都合により32校と仮定します。優勝校が決まるまで全部で何試合必要でしょうか?
とある1校が、残り全ての高校(31校)と対戦して全ての高校を負かすと、全31試合で優勝校が決まります。実は、トーナメントで行われる全試合数も同じ31(=全高校数−1)試合なのです。
トーナメントだと、第一回戦で2校づつ対戦して一方が勝ち上がるので、第二回戦に進むのは半分の16校です。同様に第二回戦、第三回戦、……と進んで行って、第五回戦が決勝戦となります。従って優勝校が戦う試合数は、全部で5試合=log2 (全高校数)です。
割り込みレベル付きの割り込みも同じです。
32個の割り込み要因があって、各々に割り込みレベルとベクタ番号が設定されています。ベクタ番号は固定ですが、割り込みレベルはユーザが自由に設定することができます。
割り込み要因をReq、割り込みレベルをLev、割り込みベクタ番号をVecとして、割り込みが勝ち上がって行くトーナメントを実施します(下図)。
※割り込み番号(ベクタ番号)と割り込み要因の対応はこちらに説明があります。
勝ち負けを決める条件は以下の通りです。
3つの条件を勝ち抜いて優勝するのは、割り込み要因32個の中で1個だけです。
もし、32個中1個も割り込み要因が発生していなかった場合、一番ベクタ番号の小さい要因が勝ち上がります。その場合は、CPUに対する割り込み要求信号intc_irqはアサートされません(∵要因が発生していない)。
旧方式では、優先度は「割り込み番号の小さい順」で固定だったので、単なるプライオリティエンコーダでした。RTLの記述は以下の通りです。
// priority encoder
reg [5:0] intnum;
always @(intfct[31:0] or intmsk[31:0])
begin
casex (intfct[31:0]&(~intmsk[31:0]))
32'bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1: intnum[5:0]=6'h01;
32'bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx10: intnum[5:0]=6'h02;
32'bxxxxxxxxxxxxxxxxxxxxxxxxxxxxx100: intnum[5:0]=6'h03;
32'bxxxxxxxxxxxxxxxxxxxxxxxxxxxx1000: intnum[5:0]=6'h04;
32'bxxxxxxxxxxxxxxxxxxxxxxxxxxx10000: intnum[5:0]=6'h05;
32'bxxxxxxxxxxxxxxxxxxxxxxxxxx100000: intnum[5:0]=6'h06;
32'bxxxxxxxxxxxxxxxxxxxxxxxxx1000000: intnum[5:0]=6'h07;
32'bxxxxxxxxxxxxxxxxxxxxxxxx10000000: intnum[5:0]=6'h08;
32'bxxxxxxxxxxxxxxxxxxxxxxx100000000: intnum[5:0]=6'h09;
32'bxxxxxxxxxxxxxxxxxxxxxx1000000000: intnum[5:0]=6'h0a;
32'bxxxxxxxxxxxxxxxxxxxxx10000000000: intnum[5:0]=6'h0b;
32'bxxxxxxxxxxxxxxxxxxxx100000000000: intnum[5:0]=6'h0c;
32'bxxxxxxxxxxxxxxxxxxx1000000000000: intnum[5:0]=6'h0d;
32'bxxxxxxxxxxxxxxxxxx10000000000000: intnum[5:0]=6'h0e;
32'bxxxxxxxxxxxxxxxxx100000000000000: intnum[5:0]=6'h0f;
32'bxxxxxxxxxxxxxxxx1000000000000000: intnum[5:0]=6'h10;
32'bxxxxxxxxxxxxxxx10000000000000000: intnum[5:0]=6'h11;
32'bxxxxxxxxxxxxxx100000000000000000: intnum[5:0]=6'h12;
32'bxxxxxxxxxxxxx1000000000000000000: intnum[5:0]=6'h13;
32'bxxxxxxxxxxxx10000000000000000000: intnum[5:0]=6'h14;
32'bxxxxxxxxxxx100000000000000000000: intnum[5:0]=6'h15;
32'bxxxxxxxxxx1000000000000000000000: intnum[5:0]=6'h16;
32'bxxxxxxxxx10000000000000000000000: intnum[5:0]=6'h17;
32'bxxxxxxxx100000000000000000000000: intnum[5:0]=6'h18;
32'bxxxxxxx1000000000000000000000000: intnum[5:0]=6'h19;
32'bxxxxxx10000000000000000000000000: intnum[5:0]=6'h1a;
32'bxxxxx100000000000000000000000000: intnum[5:0]=6'h1b;
32'bxxxx1000000000000000000000000000: intnum[5:0]=6'h1c;
32'bxxx10000000000000000000000000000: intnum[5:0]=6'h1d;
32'bxx100000000000000000000000000000: intnum[5:0]=6'h1e;
32'bx1000000000000000000000000000000: intnum[5:0]=6'h1f;
32'b10000000000000000000000000000000: intnum[5:0]=6'h20;
default: intnum[5:0]=6'h00;
endcase
end
割り込みレベルを使った方式では、上の様な簡単な回路(RTL記述)では処理できません。
各割り込み要因の優先度を、割り込みレベルとしてユーザが設定可能なので、割り込みレベルに従って勝ち抜き戦が必要となります。トーナメントで優勝を決めるのが、回路動作的には一番高速です。
要因Req(1ビット)、レベルLev(2ビット)、ベクタVec(6ビット)の入力AとBで戦って、勝者がCに出力される、以下図の様な勝ち抜き戦回路を作成します。
回路の動作は以下の通りとします。
この論理だと、割り込み要因Reqと割り込みレベルLevの条件が等しかった場合は、Aが優先されています。
割り込み要因Reqの有り/無し判定後、Levは大小比較回路(マグニチュードコンパレータ)を使って比較します。2ビットなので、そこまで大きな回路にならないと推定できます。Vecは6ビットかつ固定値なので大小比較は実施せず、常にA.Vec<B.Vecと見なして判定します。
この結果、ReqとLevが同条件だった場合、A側が優先的に勝利するので、A.Vec<B.Vecとなる様に信号線を接続して使わなくてはなりません。この様にすることで、6ビットの大小比較回路が省略可能となって、実行速度向上や論理規模縮小に貢献できます(※)。
割り込み要因A/Bから、勝ち残った一方がCとして出力されるので、この結果を次段の勝ち抜き戦回路に次々接続して行き、割り込み要因32個のトーナメントを実行します。
高校野球の例と同様に、5回戦(段)、全31試合(回路)のトーナメントで、一番割り込みレベルの高い割り込み要因が優勝(決定)します。
※上で示した勝ち抜き戦回路は、32要因なら31回路、もし64要因なら63回路インスタンスされて、論理規模に×31倍とか×63倍で利いてくるので、少しでも小さく作る努力をするのが正解です。
ここで、割り込みコントローラの仕様設計を完了しておきたいと思います。割り込みコントローラINTC322DVLのブロック図は、以下の通りです。
※薄黄色の箱はCPUからアクセス可能なI/Oレジスタです。二重になっているpencブロック、intmskレジスタなどの箱は、CPU毎に2組の回路が存在することを示しています。CPU毎のレジスタ選択は、メモリバスのbmst信号で切り分けます。
以下表に、今回追加するI/Oレジスタ一覧を示します。それ以外のレジスタは、旧INTCユニットの解説記事を更新しておきましたので、こちらを参照してください。
Addr | Name | R/W | Bit:15〜0 | Memo | |||||||||||||||
0xf130 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf132 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf134 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf136 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf138 | intlev3 | RW |
FRAL2 [1:0] |
FTAL2 [1:0] |
SMRL2 [1:0] |
SMUL2 [1:0] |
FRAL1 [1:0] |
FTAL1 [1:0] |
SMRL1 [1:0] |
SMUL1 [1:0] |
interrupt level 3 | ||||||||
0xf13a | intlev2 | RW |
CENL [1:0] |
RTCL [1:0] |
LOFL [1:0] |
− | − | − | − | − | − | − | − | − | − | interrupt level 2 | |||
0xf13c | intlev1 | RW |
OVFL1 [1:0] |
CMAL1 [1:0] |
CMBL1 [1:0] |
− | − |
OVFL0 [1:0] |
CMAL0 [1:0] |
CMBL0 [1:0] |
− | − | interrupt level 1 | ||||||
0xf13e | intlev0 | RW |
MTEL [1:0] |
MRAL [1:0] |
STAL [1:0] |
SRAL [1:0] |
EIL1 [1:0] |
EIL0 [1:0] |
ICL2 [1:0] |
ICL1 [1:0] |
interrupt level 0 |
BUSC出力のセレクト信号はbcs_int2_nです。上表の範囲でアサートされるので、INTCユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。
I/Oレジスタ仕様:
新割り込み方式では、割り込みコントローラINTCユニットが割り込み要求信号intc_irqと共に、割り込みベクタintc_vec[5:0]、割り込みレベルintc_lev[1:0]を出力してきます。CPUは割り込み要求intc_irqを受け付けたタイミングで、intc_vecとintc_levを取り込んで処理します。
後方互換性維持のため、従来方式とベクタ割り込み方式の切り替えを設定するVEビットを、割り込みベクタivの最下位ビットに設けます。こちらで解説済みです。
CPUが割り込み処理を実行する時のパイプライン実行図を以下図に示します。
通常(VE=0)の割り込みに比べて、ベクタ割り込み方式(VE=1)では、割り込み受付からISRに分岐するまでのサイクル数が+2サイクル増加します。ベクタテーブルからISR先頭アドレスをフェッチする2サイクルが追加となるためです。
従来、割り込み許可IEビットは1ビットでしたが、割り込み許可レベルを2ビットで指定できる様、IEビットも2ビットに拡張する必要があります。
以下に示すプログラマモデル図の様に、IE1,IE0の2ビットとします。
※Ivレジスタに追加したVEビットもご確認ください。
名称も、「割り込み許可(Interrupt Enable)」から「割り込み許可レベル(Interrupt Enable Level)」へ変更しました。
従来のIEビットは、新方式のIE1ビットと物理的に同じビットです。seie/clie等の組み込みマクロ命令を実行すると、IE1ビットだけが変化することに留意してください。IEフィールド={IE1,IE0}を変更するためのsrie組み込みマクロ命令を追加しておいたので、併せてご確認ください。
IEフィールドの示す割り込み許可レベルより、高い割り込みレベルを持った割り込み要求だけが受け付けられます。
従って、IEフィールド=0だと全ての割り込みが禁止されます。IE=1〜3では、割り込みレベルがIEより高い割り込みを受け付けます。実質、割り込みレベルは3段階となり、数値の小さい方が優先度としては高くなります。
CPUが割り込みを受け付けると、INTCユニットから通知された割り込みレベルintc_lev[1:0]が、IEフィールドに設定されます。これによって、たった今、受け付けた割り込み(intc_lev)と同じか、それよりレベルの低い割り込みは無視されて、多重に割り込みがかかることはありません。
今受け付けた割り込み(intc_lev)より高いレベルの割り込みが発生すれば、多重に割り込みがかかります。
割り込みコントローラINTCユニットも、CPUコアも、元のデザインを新しいデザインで置き換えたので、今回作成した割り込みベクタ/レベル方式は、過去のデザイン(ダウングレード版)含めて、全てのデザインで使用可能となっています。
新規のI/Oユニットが必要な訳ではないので、過去のデザインを使って検証すれば良かったのですが、Nihonium-SS(スーパスケイラ版)のデザインが、SDRAM搭載版1種類しかありませんでした。
SDRAM搭載版はPLLが高速クロックを生成するためかシミュレーションが遅いので、SDRAM非搭載版のNihonium-SSを作成して追加しました。型名はNSOC113VL0408です。
以下リンクより、最新のデザインをダウンロードすることができます。
FPGAボード(Sipeed Lichee Tang Primer)への書き込み手順はこちらを参照してください。
以下に、TD IDEでのコンパイル(論理合成・配置配線)結果をまとめておきます。
やっぱり、Nihonium-SSコア、大きいな……。最大LUT数に余裕がありません。将来、CPUやバスステートコントローラ、割り込みコントローラなどの共通ユニットに仕様を追加した場合、そのLUT増加分によって、最大LUT数を超えてしまう可能性もあります。
迂闊だった。CPU周りの機能追加は、やばい……。検証工数がとんでもないことになっています。
等々、検証すべきケースが多すぎて、全く、手が回りません。論理追加自体の手数は大したことは無かったのですが、検証は、もの凄く工数がかかるみたいです。
iverilogを使ったシミュレーションで、ざっと検証しましたが、十分とは言い切れないパタン数に留まっております。
個人の趣味の範囲なので止む無し。暫く、使いながら検証して行く感じになるでしょう……m(1__)m。