おしらせ
本記事は以前Qrunchで書いていた記事の復刻です。
はじめに
前回の記事で、XILINX社の Zynq-7000 の Cortex-A9 や ZynqMP の Cortex-R5 用に HOS-V4a(ITRON4.0仕様 RTOS) を移植しましたが、その際に両社に共通の割り込みコントローラ(GIC)であるPL390を少し調べたのでそのあたりを中心にARMの割り込み関連対応をメモ書きしておきます。 実際に弄りながら「多分こうだろう」と言う理解のもとでやってますので間違っている個所もあるかもしれませんが、ご参考までです。
情報ソース
まず、いろいろなところに情報がありますので、先にソースを張っておきます。 案外日本語で読める情報も多かったです。
ARM (ユーザー登録が必要なものあり)
- Generic Interrupt Controller(PL390) Technical Reference Manual [PDF]
- ARM Generic Interrupt Controller Architecture Specification
- ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition [日本語版]
- Cortex-A9 MPCore Technical Reference Manual [日本語版]
Zynq-7000
ZynqMP
- UG1085 Zynq UltraScale+ Device Technical Reference Manual [日本語版]
- UG1087 Zynq UltraScale+ Devices Register Reference [zipダウンロード版]
その他参考したサイト
割り込みコントローラの構造
今回取り扱ったARMの Cortex-A9 や Cortex-R5 ではどちらも同じくARM社の PL390 という割り込みコントローラが推奨されており、XILINX社の Zynq-7000 の Cortex-A9 用にも ZynqMP の Cortex-R5 用にも PL390 が搭載されており、これを理解すればよさそうです。 (余談ですが、ZynqMPの Cortex-A53の方には PL400というもっと新しいものが使われているようです)。
PL390 には2つのレジスタ郡があり、CPUコアごとに存在する CPUインターフェースを管理する ICC 部分と、各CPUに割り込みを分配する分配器(Distributor)としての ICD部があるようです。 そして厄介なことにこれら2個のレジスタアドレスはARM社では定めておらず、LSI実装に任されています。 実際に以下のように異なるアドレスに存在します(OSのポーティングを面倒にしています)。
Zynq-7000 Cortex-A9 の場合
レジスタ | ベースアドレス |
---|---|
ICC | 0xf8f00100 |
ICD | 0xf8f01000 |
ZynqMP Cortex-R5 の場合
レジスタ | ベースアドレス |
---|---|
ICC | 0xf9001000 |
ICD | 0xf9000000 |
また、操作時の 注意点としては、ICCはCPUコアの数だけあり、ICDは複数個アに対して1個だけ存在しているので、ICDに関しては各CPUコアで競合しないように制御する必要があります(非対称システムを組むときに気を付けておく必要があります)。
各レジスタ
割り込みにかかわるレジスタ構成は主に下記のとおりです。
CPUインターフェース
レジスタ | アドレス | 説明 |
---|---|---|
ICCICR | 0x000 | CPU インタフェース制御レジスタ |
ICCPMR | 0x004 | 割り込み優先度マスクレジスタ |
ICCBPR | 0x008 | 2進小数点レジスタ |
ICCIAR | 0x00C | 割り込み応答レジスタ |
ICCEOIR | 0x010 | 割り込み終了レジスタ |
ICCRPR | 0x014 | 実行優先度レジスタ |
ICCHPIR | 0x018 | 最優先保留割り込みレジスタ |
ICCABPR | 0x01c | エイリアスされた非セキュア2 進小数点レジスタ |
ICCIDR | 0x0fc | CPUインタフェース実装識別レジスタ |
Distributor
レジスタ | アドレス | 説明 |
---|---|---|
ICDDCR | 0x000 | 配器制御レジスタ |
ICDICTR | 0x004 | 割り込みコントローラタイプ レジスタ |
ICDIIDR | 0x008 | 分配器実装者識別レジスタ |
ICDISRn | 0x080~ | 割り込みセキュリティレジスタ |
ICDISERn | 0x100~ | 割り込みイネーブルセットレジスタ |
ICDICERn | 0x180~ | 割り込みイネーブルクリアレジスタ |
ICDISPRn | 0x200~ | 割り込み保留セットレジスタ |
ICDICPRn | 0x280~ | 割り込み保留クリアレジスタ |
ICDABRn | 0x300~ | アクティブビット レジスタ |
ICDIPRn | 0x400~ | 割り込み優先度レジスタ |
ICDIPTRn | 0x800~ | 割り込みプロセッサターゲットレジスタ |
ICDICFRn | 0xc00~ | 割り込み構成レジスタ |
ICDSGIR | 0xf00 | ソフトウェア生成割り込みレジスタ |
制御方法
初期化
ICDの初期化
ICC と ICD とで分かれているようですが、特性を考えるとまずシステム全体のどこか一ヶ所で、割り込みをどのCPUに割り当てるか決めてしまい、ICD の一部を先に初期化する方が良いと思われます。 (もちろん、マルチコアOSで動的に割り込みの割り充てを変えるなどでなければ、この限りではありません)。 ICDの中には複数CPUから操作することを考慮して作られているレジスタもありますが、そうでないものもあるので注意が必要です。
私が実装したICDの初期化の流れとしては
- ICDDCR に 0 を書き込んで Distributor を停止させる
- ICDIPTRn を設定して、各割り込みをどのCPUに割り当てるか決める
- ICDICFRn を設定して、割り込み構成を設定する
- ICDDCR に 1 を書き込んで Distributor を有効にする
としています。
ICDIPTRn は8bit単位で配置されており、どのCPUに割り込みを入れるかbitマスクになっています。ICDICFRn の方は Zynq の場合、2bit単位で配置されており、レベル割り込み(2'b01)とエッジ割り込み(2'b11)を指定するようです。 Zynq の場合、内蔵ペリフェラルに関しては初期値から変更する必要はなく(全部レベル割り込みの模様)、PLに構成する自分で作る回路に合わせてここだけ設定すればよいようです。 特に ICDICFRn は 2bit サイズなので、同じワード内にある設定を別CPUコアから操作すると競合する可能性があるので、システムユニークな場所で初期化するのが良いと思われます。
他にも、システムユニークな初期化であれば割り込みを一旦すべて無効化して(ICDICERn)、保留されている割り込みをクリア(ICDICPRn)したりしても良いのかもしれません。
HOSのサンプルでは main関数の最初に、OSカーネルの起動(vsta_knl呼び出し)の前に初期化してしまっています。
ICCの初期化
次に各CPUコアでの初期化ですが、
- ICCPMR に 0xf8 を設定 (32 support levels の場合)
- ICCICR に 0x07 を設定 (ちょっと自信なし)
としています。 ICCPMR は割り込み優先度マスクで、数値が小さいほど優先度が高く、設定されている値と同じかそれ以上の数値がICDIPRnに設定されている割り込みはマスクされるようです。 Zynq の場合、ICCPMR のマスクは 32レベルなので下位3bitは0固定で、0xf8 が最大値になります。 ICCICR の解釈はよくわからなかったのですが、XILINX のサンプルでこの設定値だったので真似てそのまま設定しました。
割り込み番号ごとの有効化
次にCPUでプログラムが動き始め、割り込みを発生させる元となるハードウェアの初期化などが終わるといよいよ割り込みを有効化する必要が出ます。そのための手順は
- ICDIPRn に優先度を設定
- ICDISERn の該当ビットに1を書き込む
となります。 なお、割り込みを無効にする場合には
- ICDICERn の該当ビットに1を書き込む
となります。 ICDISERn と ICDICERn は、bit単位の配置ですが、それぞれセットとクリアに専用レジスタがあるので、複数個コアから制御しても割り込み番号が異なれば競合しません。 ICDIPRn は 8bit 単位なので、バイトアクセスしておけば競合を回避できるはずです。
割り込み処理
CPUの方の cpsr の i ビット (irq のマスクレジスタ)はクリアしているときに、有効化している割り込みが発生すると IRQモードに移行して割り込みハンドラに移動します。 この時注意点として、SCTLR.V ビットが1だと、例外ベクタが 0x00000000 番地ではなく、0xFFFF0000番地になります。 ZynqMP の Cortex-R5 では初期値がそうなっているようで、ブートコード内で
/* SCTLR.V クリア(例外ベクタを0番地に戻す) */ mrc p15, 0, r0, c1, c0, 0 mvn r1, #0x2000 and r0, r0, r1 mcr p15, 0, r0, c1, c0, 0
としてクリアしています。
IRQが発生するとIRQハンドラに入ります。ここで必要に応じてレジスタを退避して割り込み処理に移行するのですが、VFPを使っている場合にはそのレジスタの退避も必要です。 Cortex-A9 では、32個のレジスタを伴う VFPv3 と NEON をサポートしており、gcc のコンパイルオプションは -mfpu=neon となり、d0~d31, fpscr, fpexc の退避が必要となります。 Cortex-R5では、16個のレジスタを伴う VFPv3 をサポートしており、gcc のコンパイルオプションは -mfpu=vfpv3-d16 となり、d0~d16, fpscr, fpexc の退避が必要となります。
ちなみにこれらVFPも有効化しないと扱うことができず。HOSの初期化時に
mrc p15, 0, r0, c1, c0, 2 /* CP アクセスレジスタを読み込む */ orr r0, r0, #0x00f00000 /* NEON/VFPへのフルアクセス権を有効にする */ mcr p15, 0, r0, c1, c0, 2 /* CP アクセスレジスタを書き込む */ isb mov r0, #0x40000000 /* VFP および NEON ハードウェアをオンにする */ vmsr fpexc, r0 /* FPEXC の EN ビットを設定する */
のような有効化コードを入れています。
ちなみに汎用レジスタについてはProcedure Call Standard for the ARM Architectureを参考に、C言語の関数を呼ぶ前に、破壊される可能性のあるレジスタ(r0-r3, r12, lr)を退避しておけば良いことになります。
さて、レジスタの退避が終わると、次は実際にPL390の割り込みコントローラの処理が必要です。 基本的には
- ICCIAR を読み出して発生した割り込み番号を得る
- 発生した割り込み番号の割り込み処理を行う
- ICCEOIR に割り込み番号を書き込んで完了する
という処理になります。 (なお、この時、割り込み発生元がレベル割り込みで、ICCIAR を読み出す前に割り込みレベルが消えてしまうと、ICCIARから割り込み範囲外の大きな数値が戻るようでした)
なお、HOSの場合はさらに多重割り込みに対応した割り込み処理にしており、
- ICCIAR を読み出して発生した割り込み番号を得る
- ICCPMR に発生した割り込み番号の優先度より高いもののみ割り込みを受けるようにマスク設定
- ICCEOIR に割り込み番号を書き込んで完了する
- 発生した割り込み番号の割り込み処理を行う
- ICCPMR を元に戻す
という処理にすることで、4の処理中にさらに優先度の高い割り込みが来たら多重割り込みするようにしています。
割り込み要因のクリア
ICDICPRn の対応するbitに1を書き込むことで、該当する割り込み要因をクリアできます。HOSの場合は vclr_intという独自サービスコールで実装しています。 これはPL390にかかわらず一般論ですが、
- レベル割り込みの場合は、ハードウェアの割り込み原因を解消してからクリアする
- エッジ割り込みの場合はクリアしてから、ハードウェアの割り込み原因を解消する
というのが基本になります。 レベル割り込みの場合、ハードウェアの割り込み原因を解消するまえにクリアしても、まだ割り込みレベル維持されているので、即座に同じ割り込みの要件がまた成立してしまいますし、エッジの場合は、後からクリアすると、ハードウェアの割り込み原因を解消とクリアの間に再度割り込み事象が起こると割り込みを取りこぼす為です。 ここは割り込み先のハードウェア依存なので、プログラマの方で仕組みを理解してケアする必要があります。
ソフトウェアから割り込みを起こす
ICDISPRn の対応ビットに書き込むことで、対応する割り込みをソフトウェアから起こすことができるようです。vset_intという独自サービスコールで実装してみました。
おわりに
昨今の高性能な ARM コアですと、LANやUSBの対応があたりまえで Linux などを搭載するのが当然になってきたのでなかなかユーザー側で割り込みコントローラまで触ることは少ないかと思います。 一方で ZynqMP のような APU(Cortex-A9)のほかに、RPU(Cortex-R5)がついており、さらにプログラマブルロジック(PL)までもったようなSoCだと、Linuxとは別空間でシステムプログラミングが楽しめます。
こういった深い部分を弄ってみたいという方の参考になれば幸いに思います。