Ryuz's tech blog

FPGAなどの技術ブログ

ARMの割り込みPL390(GIC)設定やHOS-V4aのZynq対応メモ(復刻記事)

おしらせ

本記事は以前Qrunchで書いていた記事の復刻です。

はじめに

前回の記事で、XILINX社の Zynq-7000 の Cortex-A9 や ZynqMP の Cortex-R5 用に HOS-V4a(ITRON4.0仕様 RTOS) を移植しましたが、その際に両社に共通の割り込みコントローラ(GIC)であるPL390を少し調べたのでそのあたりを中心にARMの割り込み関連対応をメモ書きしておきます。 実際に弄りながら「多分こうだろう」と言う理解のもとでやってますので間違っている個所もあるかもしれませんが、ご参考までです。

情報ソース

まず、いろいろなところに情報がありますので、先にソースを張っておきます。 案外日本語で読める情報も多かったです。

ARM (ユーザー登録が必要なものあり)

Zynq-7000

ZynqMP

その他参考したサイト

割り込みコントローラの構造

今回取り扱った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の初期化の流れとしては

  1. ICDDCR に 0 を書き込んで Distributor を停止させる
  2. ICDIPTRn を設定して、各割り込みをどのCPUに割り当てるか決める
  3. ICDICFRn を設定して、割り込み構成を設定する
  4. 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コアでの初期化ですが、

  1. ICCPMR に 0xf8 を設定 (32 support levels の場合)
  2. ICCICR に 0x07 を設定 (ちょっと自信なし)

としています。 ICCPMR は割り込み優先度マスクで、数値が小さいほど優先度が高く、設定されている値と同じかそれ以上の数値がICDIPRnに設定されている割り込みはマスクされるようです。 Zynq の場合、ICCPMR のマスクは 32レベルなので下位3bitは0固定で、0xf8 が最大値になります。 ICCICR の解釈はよくわからなかったのですが、XILINX のサンプルでこの設定値だったので真似てそのまま設定しました。

割り込み番号ごとの有効化

次にCPUでプログラムが動き始め、割り込みを発生させる元となるハードウェアの初期化などが終わるといよいよ割り込みを有効化する必要が出ます。そのための手順は

  1. ICDIPRn に優先度を設定
  2. ICDISERn の該当ビットに1を書き込む

となります。 なお、割り込みを無効にする場合には

  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の割り込みコントローラの処理が必要です。 基本的には

  1. ICCIAR を読み出して発生した割り込み番号を得る
  2. 発生した割り込み番号の割り込み処理を行う
  3. ICCEOIR に割り込み番号を書き込んで完了する

という処理になります。 (なお、この時、割り込み発生元がレベル割り込みで、ICCIAR を読み出す前に割り込みレベルが消えてしまうと、ICCIARから割り込み範囲外の大きな数値が戻るようでした)

なお、HOSの場合はさらに多重割り込みに対応した割り込み処理にしており、

  1. ICCIAR を読み出して発生した割り込み番号を得る
  2. ICCPMR に発生した割り込み番号の優先度より高いもののみ割り込みを受けるようにマスク設定
  3. ICCEOIR に割り込み番号を書き込んで完了する
  4. 発生した割り込み番号の割り込み処理を行う
  5. 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とは別空間でシステムプログラミングが楽しめます。

こういった深い部分を弄ってみたいという方の参考になれば幸いに思います。