組込み

【ZynqMP】2.2 GICv1の設定まわりを歩く

1.記事一覧

記事は複数回に分けて投稿する予定です。AMD/XilinxのARM SoC(ZynqMP Kria K26 SOM)で動作する、AMD/Xilinx公式認定Ubuntuやベアメタル開発を勉強していく覚書きです。

Zynq7000(armv7)版はこちら

2.GICv1(PL390)の設定処理を追いかける

以前の【ZynqMP】2.Cortex-R5でRTOS+GPIOで作業中に出会ったプチトラブルから始まり、それの謎解き記事である【ZynqMP】2.1 GPIO割込みが入らない?を前回書きました。

ILAで割込み線を計測した結果、原因がAXI-GPIOの方にあると判明したわけですが、新VitisやILAの扱いの不慣れもあり、実際はそこまできれいに解決まで進められたわけではありませんでした。寄り道として、Cortex-R5(CR5)のGICの仕様について調べ、GICの設定関数の中身を読み、レジスタ設定も読み出して確認したりしていました。これらのことを覚書として残しておこうと思います。

注意:この記事はGICの一般的な内容ではなく、XilinxのZynqMP SoCやBSP、IDE(Vitis)そして私の私見に偏った内容です。筆者はGICを扱うのは今回初めてです。

2-1. GIC(Generic Interrupt Controller)とは

GICとはARM Cortex-A/Rで利用される汎用割込みコントローラです。ZynqMPのCR5にはPL390(GICv1)、CA53にはGIC-400(GICv2)が搭載されています。ちなみにRaspberry Pi 4は GIC-400(GICv2)、NvidiaのJetsonではGIC-600(GICv3)を搭載したものもあるらしいです。

今回はPL390(GICv1)の仕様について調べながら、【ZynqMP】2.Cortex-R5でRTOS+GPIOで作成したサンプルアプリのGIC設定関数のレジスタ設定を見ていきます。まずはドキュメントです。

ARM

Xilinx

1つ目はPL390のTRM(Technical Reference Manual)で一見ページ数が少なくて読みやすそうなのですが、肝心のレジスタの仕様は2つ目を参照してねと書いてあるので、結局2つ目のGICアーキテクチャv2仕様書を読むことになります。

GICアーキテクチャv2仕様書はGICv2の仕様書という認識ですが、GICv1の内容も含まれていて差分内容も書かれています。特にレジスタ名がv1とv2で異なるので少しややこしいです。

XilinxのTRMではZynqMPのXilinx側設計部分を確認します。ARMのPL390は半導体ベンダの実装依存の箇所は定義されていないので、この部分はXilinxのドキュメントを見ることになります。

ZynqMPではGICは以下のような構造になっているようです。正直よくわかりません。どなたかで良いので矢印の説明を私にしてほしい…。

<引用Zynq UltraScale+ Device Technical Reference Manual>

上の図でPLからの割込みはSPI(Shared Peripheral Interrupts)と呼ばれる割込みの1つとして管理されます。GICの特徴の一つである、マルチコアに対応していて各コアに割込みイベントを振り分けるInterrupt Controller Distributor(ICD)があります。今回見ていくGICレジスタはこのICD関連のレジスタになります(レジスタ名がICD_xxxx)。

最後にGIC関連の勉強でお世話になった記事です。

2-2. SDT対応ベアメタルGICドライバ

Vitis 2023.2より、ビルドシステムが一新しハードウェアメタ情報はSDT(System Device Tree)を使用するようになりました。それに伴い、ベアメタル用GICドライバも新しいAPIが追加され、関数1つでGICの設定ができるようになっています。

公式WikiのGIC Standalone driverGithubのサンプルにはまだ新しいAPIの内容は書かれていないのですが(2024/1)、GPIOなど他のペリフェラルドライバのサンプルには新しいAPIが使われていますので、こちらを参考にすることになると思います。

GPIOのサンプルで使われているGIC設定関数はこちらです。

2-3. GIC設定関数を分解(XSetupInterruptSystem)

【ZynqMP】2.Cortex-R5でRTOS+GPIOで作成したサンプルアプリのGIC設定関数から、該当箇所を抜粋しました。この1関数でGICの設定が完了し、GPIO割込みが有効になります。

#include "xinterrupt_wrap.h"

XGpio_Config *ConfigPtr;
/* GPIOのパラメータ読出し */
ConfigPtr = XGpio_LookupConfig(XPAR_XGPIO_2_BASEADDR);
/* GPIOインスタンス生成 */
XGpio_Initialize(&user_sw_gpio, XPAR_XGPIO_2_BASEADDR);
/* GIC設定関数 */
XSetupInterruptSystem( &user_sw_gpio, //デバイスインスタンス
                       &GpioIntHandler, //割込みハンドラ
                       ConfigPtr->IntrId, //SPI割込み番号[11:0], トリガ種別[15:12]
                       ConfigPtr->IntrParent, //親割込みコントローラのベースアドレス(GIC)
                       XINTERRUPT_DEFAULT_PRIORITY-8); //割込み優先度をデフォから1段階上げる 0xA0 - 8

[...]

static void GpioIntHandler(void *CallbackRef)
{
  /*......*/
}

XGpio_LookupConfig()で割込み番号と親割込みコントローラの情報を取得し、XSetupInterruptSystem()に渡しています。SDTにより、親割込みコントローラの選択、割込み番号、トリガタイプのパラメータを内部で設定し、ユーザ側で選択する必要がなくなりました。

XSetupInterruptSystem()は新しく登場したAPIで、xinterrupt_wrap.cに実装されています。

/* in xinterrupt_wrap.c */
int XSetupInterruptSystem(void *DriverInstance, //デバイスインスタンス
                          void *IntrHandler, //割込みハンドラ
                          u32 IntrId, //SPI割込み番号[11:0], トリガ種別[15:12]
                          UINTPTR IntcParent, //GICのベースアドレス(GIC)
                          u16 Priority) //割込み優先度
{
	int Status;
	//GICインスタンス1つのみ生成.2回目以降の呼出しはスルー
	Status = XConfigInterruptCntrl(IntcParent);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}
#if defined (XPAR_SCUGIC)
	ScuGicInitialized = TRUE;
#endif
    //優先度とトリガを設定
	XSetPriorityTriggerType( IntrId, Priority, IntcParent);
    //割込みハンドラとハンドラ引数をIRQテーブルに登録
	Status = XConnectToInterruptCntrl(IntrId,
               (Xil_ExceptionHandler) IntrHandler,
                                      DriverInstance,
                                      IntcParent);
	if (Status != XST_SUCCESS) {
		return XST_FAILURE;
	}
[...略]
    //例外ベクタテーブルにIRQルートハンドラを登録
	XRegisterInterruptHandler(NULL, IntcParent);
    //割込み有効化と各CPUコアへのルート設定
	XEnableIntrId(IntrId, IntcParent);
	Xil_ExceptionInit(); //<--空実装。後方互換性のために残してあるらしい
    //例外IRQの有効化
	Xil_ExceptionEnable();
	return XST_SUCCESS;
}

XSetupInterruptSystem()の内容を上から順に見ていきます。

まずは以下の関数から。

/* in xinterrupt_wrap.c */
int XConfigInterruptCntrl(UINTPTR IntcParent);

GICドライバのデバイスインスタンスを生成します。AXI-INTC(Xilinx AXI割り込みコントローラIP,Microblaze向け)も対応していますが、ここでは触れません。インスタンスは1つのみ生成するようで、2回目以降の呼び出しは生成せずリターンします。GPIO割込みの設定時には既に生成済みのため、すぐリターンします。最初に生成したのはおそらくカーネルタイマ用のTTC周期割込みです。

インスタンス内にはGICドライバ内で定義されたIRQハンドラテーブルへのポインタを持つように設定されます。

2-3-A. 割込み優先度とトリガ設定

割込み優先度とトリガを設定する処理です。

/* in xinterrupt_wrap.c */
void XSetPriorityTriggerType( u32 IntrId, u8 Priority, UINTPTR IntcParent)
{
#if defined (XPAR_SCUGIC)
	u8 Trigger = (((XGet_TriggerType(IntrId) == 1) ||
		       (XGet_TriggerType(IntrId) == 2)) ? XINTR_IS_EDGE_TRIGGERED
		      : XINTR_IS_LEVEL_TRIGGERED);
	u16 IntrNum = XGet_IntrId(IntrId);
	u16 Offset = XGet_IntrOffset(IntrId);

	IntrNum += Offset;
	if (XGet_IntcType(IntcParent) == XINTC_TYPE_IS_SCUGIC) {
		XScuGic_SetPriorityTriggerType(&XScuGicInstance, IntrNum, Priority, Trigger);
	}
#endif
}

IntrIdは内部がSPI割込み番号[11:0], トリガタイプ[15:12]に分かれています。トリガタイプが1 or 2だった場合はエッジトリガ、それ以外はレベルトリガとなります。

AXI-GPIO IPはマニュアルではActive-Highのレベルセンシティブとあるので、それに一致する4が入っていました(なぜ4なのかは次章で)。そのためレベルトリガが設定されます。

続いてIntrIdからSPI割込み番号を取り出して、割込み番号にオフセットを足しています。オフセットとはGIC全体で扱う割込み番号とSPIのみで扱う場合の開始番号の差分を指すようです。

  • GIC 0~15 : SGI(Software Generated Interrupts)
  • GIC 16~31 : PPI(Private Peripheral Interrupts)
  • GIC 32~(max 1019) : SPI(Shared Peripheral Interrupts)

SPIは32番から始まるのですが、SPIのみを扱う場合に0からの番号を扱うことがあります。その違いを変換する場合にオフセットとして32という値が出てきます。

AXI-GPIOはPL to PS interruptポート(0~7)の0番に接続されていますので、以下の割当になります。

  • GIC IRQ ID : 121
  • SPI Number : 89 ((GIC IRQ ID) – 32)

XScuGic_SetPriorityTriggerType()で優先度とトリガを設定しています。

/* xscugic.c */
void XScuGic_SetPriorityTriggerType(XScuGic *InstancePtr, u32 Int_Id,
				    u8 Priority, u8 Trigger)
{
[...略]

RegValue = XScuGic_DistReadReg(InstancePtr,
				       XSCUGIC_PRIORITY_OFFSET_CALC(Int_Id));

	/*
	 * The priority bits are Bits 7 to 3 in GIC Priority Register. This
	 * means the number of priority levels supported are 32 and they are
	 * in steps of 8. The priorities can be 0, 8, 16, 32, 48, ... etc.
	 * The lower order 3 bits are masked before putting it in the register.
	 */
	LocalPriority = LocalPriority & (u8)XSCUGIC_INTR_PRIO_MASK;
	/*
	 * Shift and Mask the correct bits for the priority and trigger in the
	 * register
	 */
	RegValue &= ~((u32)XSCUGIC_PRIORITY_MASK << ((Int_Id % 4U) * 8U));
	RegValue |= (u32)LocalPriority << ((Int_Id % 4U) * 8U);

	/*
	 * Write the value back to the register.
	 */
	XScuGic_DistWriteReg(InstancePtr, XSCUGIC_PRIORITY_OFFSET_CALC(Int_Id),
			     RegValue);

/*
	 * Determine the register to write to using the Int_Id.
	 */
	RegValue = XScuGic_DistReadReg(InstancePtr,
				       XSCUGIC_INT_CFG_OFFSET_CALC(Int_Id));

	/*
	 * Shift and Mask the correct bits for the priority and trigger in the
	 * register
	 */
	RegValue &= ~((u32)XSCUGIC_INT_CFG_MASK << ((Int_Id % 16U) * 2U));
	RegValue |= (u32)Trigger << ((Int_Id % 16U) * 2U);

	/*
	 * Write the value back to the register.
	 */
	XScuGic_DistWriteReg(InstancePtr, XSCUGIC_INT_CFG_OFFSET_CALC(Int_Id),
			     RegValue);
[...略]
}

XScuGic_DistReadReg()とXScuGic_DistWriteReg()はICDxxxxレジスタへの読み書きをする関数マクロです。

ここでは割込み優先度レジスタICDIPRと割込みコンフィグレーションレジスタICDICRに設定しています。

割込み優先度レジスタICDIPR

<引用ARM Generic Interrupt Controller Architecture version 2.0 Architecture Specification B.b>

この32bitレジスタは8bitの優先度フィールドが計4フィールドあります。各フィールドがそれぞれIRQ番号に対応しているので1レジスタあたり4IRQ分相当です。右フィールド側がIRQ番号の小さい方です。ちなみにGICD_IPRIORITYRはGICv2の名前です。

サイズは8bitありますが、少なくとも[7:4]ビットを実装せよとARMの仕様書にあります。つまり有効bit長は半導体ベンダ側の実装依存ということだろうと思います。ちなみに有効bit[7:4]は16飛びの全16段階の優先度になります。

上記のGICドライバのコメントによるとZynqMP RPU GICではbit[7:3]が使えるようです。8飛びの32段階の優先度が実装されています。

実際に書き込まれたアドレスと値を見てみます。設定する優先度は0x98です。
これはXINTERRUPT_DEFAULT_PRIORITY=0xA0から8を引いた値です。優先度は値が小さいほど高く、bit[2:0]が無効なので8飛びごとの値になります。1段階優先度を上げるときは-8することになります。

ちなみに無効bit[2:0]にアクセスした場合は、RAZ/WIとあります。ryos36様の記事によると、ARMドキュメントの略語のようで、”Read As Zero/Writes Ignored”だそうです。(ARM語難しい)

書き込むアドレスはenable_priority_spi_INTID89 (PL390) Register(UG1087)です。32bitアクセスなので、addr=0xF9000479ではなくaddr=0xF9000478です。

レジスタ読み書きの直前にprintfでアドレスとデータを出力すると以下のようになりました。

  • 書込み前 addr=0xF9000478 value=0xA0A0A0A0
  • 書込み後 addr=0xF9000478 value=0xA0A098A0

アドレス0xF9000478 アクセスサイズ32bitなので、SPI 88~91番のうち、SPI89番bit[15:8]に0x98を書き込みました。もともと0xA0で初期化されてたので、これで1段階優先度が上がりました。

割込みコンフィグレーションレジスタICDICR

<引用ARM Generic Interrupt Controller Architecture version 2.0 Architecture Specification B.b>

この32bitレジスタは2bitのトリガ種別フィールドを16フィールド持ちます。各フィールドが各IRQに割り当てられています。

<引用ARM Generic Interrupt Controller Architecture version 2.0 Architecture Specification B.b>

2bitの領域の内、上位bitの設定値は以下のようになります。

  • 0 :レベルセンシティブ
  • 1 :エッジトリガ

下位bitはGICv1仕様発行前は役割があったらしいのですが、RPU GICはGICv1対応とZynqMP TRM (UG1085)にあるのでとりあえず気にしないことにします。

ただし、ドライバのコードでは下位bit=1を固定で入れているようです。

実際にレジスタに書き込まれたアドレスと値を見てみます。設定する値は0b01となります。

  • 書込み前 addr=0xF9000C1C value=0x55555555 (0b0101…0101)
  • 書込み後 addr=0xF9000C1C value=0x55555555 (0b0101…0101)

レジスタリファレンスにこのアドレスはなぜか載っていないですが、あるとすればenable_spi_config5 (PL390) Registerでしょうか(config4まではある)。Vitisのメモリダンプウィンドウで確認するとconfig5レジスタは存在して、0x55555555に初期化されています。初期化時の値が0b01なので、書き込まれているのかよくわかりませんでした。計算するとSPI89はbit[19:18]に入っているはずです。

2-3-B. 割込み番号やトリガ種別はどこで定義されているか

ところで、GIC設定関数に渡す割込み番号(SPI89)やレベルトリガ(4)という値がどこから来るのかについても少し調べました。

GIC設定関数を呼ぶ前にXGpio_LookupConfig()を呼んでGPIOパラメータを取り出しています。さらにその中ではXGpio_ConfigTableテーブルを参照しています。

/* in xgpio_g.c */
XGpio_Config XGpio_ConfigTable[] __attribute__ ((section (".drvcfg_sec"))) = {

	[......略]
	{
		"xlnx,axi-gpio-2.0", /* compatible */
		0x80020000, /* reg */
		0x1, /* xlnx,interrupt-present */
		0x0, /* xlnx,is-dual */
		0x4059, /* interrupts */
		0xf9000000, /* interrupt-parent */
		0x1 /* xlnx,gpio-width */
	},
	 {
		 NULL
	}
};
/* in xgpio.h */
typedef struct {
#ifndef SDT
	u16 DeviceId;		/**< Unique ID  of device */
#else
	char *Name;
#endif
	UINTPTR BaseAddress;	/**< Device base address */
	int InterruptPresent;	/**< Are interrupts supported in h/w */
	int IsDual;		/**< Are 2 channels supported in h/w */
#ifdef SDT
	u16 IntrId; /** Bits[11:0] Interrupt-id Bits[15:12] trigger type and level flags */
	UINTPTR IntrParent; /** Bit[0] Interrupt parent type Bit[64/32:1] Parent base address */
	u16 Width;  /** Gpio width */
#endif
} XGpio_Config;

トリガ種別の値4はu16 IntrId=0x4059からマスクして取り出した4というわけでした。ではテーブルを定義しているxgpio_g.cはどこの情報をもとにコード生成されたのかと言われれば、おそらくSDTになるのかなと思われます。(CMakeスクリプトを追えば詳細が分かると思いますが流石に面倒なので)

SDTでは以下のようになっています。

/* bsp/hw_artifacts/sdt.dts */
[...略]
axi_gpio_2: axi_gpio@80020000 {
        #interrupt-cells = <0x2>;
        interrupts = <0x0 0x59 0x4>;
        xlnx,gpio-board-interface = "Custom";
        compatible = "xlnx,axi-gpio-2.0", "xlnx,xps-gpio-1.00.a";
        xlnx,all-outputs = <0x0>;
        xlnx,gpio-width = <0x1>;
        interrupt-parent = <&imux>;
[...略]

interrupts = <0x0 0x59 0x4>;の項目が該当します。Linux device treeと同じルールであれば、以下のはずです。

  • 第1項目:SPI=0 or PPI=1
  • 第2項目:SPI or PPI割込み番号 <– 0x59=SPI89番(0番始まり)
  • 第3項目:上がりエッジ1or下がりエッジ2orHighレベル4orLowレベル8

2-3-C. IRQハンドラの登録

割込み優先度とトリガの設定の次はIRQハンドラ関数の登録です。以下の関数呼び出し順でIRQハンドラテーブルへの登録を行っています。

/* xinterrupt_wrap.c */
int XConnectToInterruptCntrl(u32 IntrId, void *IntrHandler, void *CallBackRef, UINTPTR IntcParent);
>>/* xscugic.c */
>>s32  XScuGic_Connect(XScuGic *InstancePtr, u32 Int_Id,Xil_InterruptHandler Handler, void *CallBackRef)

XScuGic_Connect()の中身です。

s32  XScuGic_Connect(XScuGic *InstancePtr, u32 Int_Id,
		     Xil_InterruptHandler Handler, void *CallBackRef)
{
[......略]
	/*
	 * The Int_Id is used as an index into the table to select the proper
	 * handler
	 */
	InstancePtr->Config->HandlerTable[Int_Id].Handler = (Xil_InterruptHandler)Handler;

	/*
	 * The Int_Id is used as an index into the table to select the proper
	 * CallBackRef
	 */
	InstancePtr->Config->HandlerTable[Int_Id].CallBackRef = CallBackRef;

	/* Return statement */
	return XST_SUCCESS;
}

GICインスタンスが持つIRQハンドラテーブルポインタを通じて、テーブルにハンドラとハンドラ引数を登録します。引数CallBackRefはデバイスインタンスのポインタです(今回はAXI-GPIO)。

IRQハンドラテーブル本体のXScuGic_ConfigTableはxscugic_g.cで宣言されていました。gic-400とありますが、合っているのでしょうか…。少なくとも本記事の内容の範囲ではGICv1とGICv2の差はありません。

/* xscugic_g.c */
XScuGic_Config XScuGic_ConfigTable[] __attribute__ ((section (".drvcfg_sec"))) = {

	{
		"arm,gic-400", /* compatible */
		0xf9000000,
		0xf9001000, /* reg */
		{{0U}} /* Handler-table */
	},
	 {
		 NULL
	}
};

定義はこちら

/* xscugic.h */
typedef struct
{
#ifndef SDT
	u16 DeviceId;		/**< Unique ID  of device */
	u32 CpuBaseAddress;	/**< CPU Interface Register base address */
	u32 DistBaseAddress;	/**< Distributor Register base address */
#else
	char *Name;		/**< Compatible string */
	u32 DistBaseAddress;	/**< Distributor Register base address */
	u32 CpuBaseAddress;	/**< CPU Interface Register base address */
#endif
	XScuGic_VectorTableEntry HandlerTable[XSCUGIC_MAX_NUM_INTR_INPUTS];/**<
				 Vector table of interrupt handlers */
} XScuGic_Config;

Cortex-M系でNVICを扱っていた身としては、割込みハンドラテーブル==ベクタテーブルの認識なのですが、Cortex-A/Rはベクタテーブルと割込みハンドラテーブルは別物であるというところが少し混乱しました。

Cortex-A/R(armv7)にはそもそもベクタテーブルに割込み処理目的の例外ハンドラ(IRQ例外ハンドラ)を1つしか持ちません。これを便宜上IRQルートハンドラと呼ぶことにします。ルートハンドラに入った後、GICレジスタで保留中のIRQ番号を取得し、そこから割込みハンドラテーブル経由で登録したIRQハンドラを呼び出すという仕組みのようです。

ソフトウェアでハンドラに分岐する分、オーバーヘッドが掛かりそうですが、IRQ周りの実装の自由度が高くなるので、後者を優先したというところでしょうか。

2-3-D. ベクタテーブルへのIRQルートハンドラの登録

前章でベクタテーブルにはIRQ例外ハンドラは1つしか無いという話をしました。このハンドラをIRQルートハンドラと便宜上呼ぶことにします。

前提について

再度になりますが、今回追いかけているAXI-GPIOの割込み設定処理は以前の記事の【ZynqMP】2.Cortex-R5でRTOS+GPIOの話をしています。FreeRTOSの環境の話なので、ベクタテーブルもRTOS用のものが使われます。ゆえにこれから見ていく、ベアメタル用ベクタテーブルにIRQルートハンドラを登録する処理は意味がない処理です。最後にFreeRTOSの実装側もちらっと見てみます。

以上の前提を頭の隅において処理を見ていきます。

/* xinterrupt_wrap.c  */
void XRegisterInterruptHandler(void *IntrHandler,  UINTPTR IntcParent)
{
	if (XGet_IntcType(IntcParent) == XINTC_TYPE_IS_SCUGIC) {
#if defined (XPAR_SCUGIC)
		if (IntrHandler == NULL) {
			Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, \
						     (Xil_ExceptionHandler) XScuGic_InterruptHandler,
						     &XScuGicInstance);
		} else {
			Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, \
						     (Xil_ExceptionHandler) IntrHandler,
						     &XScuGicInstance);

		}
#endif
[......略]
}

引数の*IntrHandlerにはNULLが入っていますので、Xil_ExceptionRegisterHandler()第2引数にはXScuGic_InterruptHandler()というGICドライバで定義されたIRQルートハンドラ関数が渡されます。このIRQルートハンドラもベアメタル用なので使われません。

Xil_ExceptionRegisterHandler()の中身です。ベクタテーブルに登録しています。

/* xil_exception.c */
void Xil_ExceptionRegisterHandler(u32 Exception_id,
				    Xil_ExceptionHandler Handler,
				    void *Data)
{
[......略]
	XExc_VectorTable[Exception_id].Handler = Handler;
	XExc_VectorTable[Exception_id].Data = Data;
}

ベクタテーブル本体です。とてもシンプルですね。これを0番地に順に実装すれば完成ですが、FreeRTOS側がやっているのでありません。

/* xil_exception.c */
XExc_VectorTableEntry XExc_VectorTable[XIL_EXCEPTION_ID_LAST + 1] =
{
	{Xil_ExceptionNullHandler, NULL},
	{Xil_UndefinedExceptionHandler, NULL},
	{Xil_ExceptionNullHandler, NULL},
	{Xil_PrefetchAbortHandler, NULL},
	{Xil_DataAbortHandler, NULL},
	{Xil_ExceptionNullHandler, NULL},
	{Xil_ExceptionNullHandler, NULL},
};

ベアメタルのベクタテーブル

Cortex-R5 Technical Reference Manual r1p2を参考にベクタテーブルの内容を整理してみました。0x18のIRQの要素に登録されるということですね。ちなみに0x14が飛んでいますが、予約領域だそうです。

Offset例外の種別
0x00リセット
0x04未定義命令
0x08ソフトウェア割込み(SVC) ←FreeRTOSのコンテキストスイッチで使用
0x0Cプリフェッチアボート
0x10データアボート
0x18IRQ ←FreeRTOSのカーネルタイマ(TTC)、AXI-GPIO
0x1CFIQ

最後にGICドライバ内で定義されている、IRQルートハンドラXScuGic_InterruptHandler()を見てみます。中で前章で出てきたIRQハンドラテーブルを参照しています。

/* xscugic_intr.c */
void XScuGic_InterruptHandler(XScuGic *InstancePtr)
{
    u32 InterruptID;
    XScuGic_VectorTableEntry *TablePtr;
[...略]
/* GICから保留中の最高優先度のIRQ番号を取得 */
/* ICCIAR:割込みアクノリッジレジスタから読み出す */
    IntIDFull = XScuGic_CPUReadReg(InstancePtr, XSCUGIC_INT_ACK_OFFSET);
    InterruptID = IntIDFull & XSCUGIC_ACK_INTID_MASK;
[...略]
/* IRQ番号を元にIRQハンドラテーブルからハンドラを呼び出す */
    TablePtr = &(InstancePtr->Config->HandlerTable[InterruptID]);
    if (TablePtr != NULL) {
        TablePtr->Handler(TablePtr->CallBackRef);
    }
[...略]
/* 処理が終了したらGICに終了を通知する */
/* ICCEOIR:割込み終了レジスタ */
    XScuGic_CPUWriteReg(InstancePtr, XSCUGIC_EOI_OFFSET, IntIDFull);
}

IRQルートハンドラの登録処理は以上です。

せっかくなのでFreeRTOSの実装も見ていきます。

FreeRTOSのベクタテーブル

ベクタテーブルはアセンブリで定義されています。Cortex-M系のようなベクタ割込みに慣れていると、割込みハンドラテーブルをそのまま配置するイメージが染み付いてますが、違います。LDR(ロード命令)などでPC(Program Counter)に例外ハンドラのアドレスをロードする処理を順に記述していきます。

/*port_asm_vectors.S*/ 
[...略]
.section .vectors,"a"
_vector_table:
	ldr	pc,=_boot
	ldr	pc,=Undefined
	ldr   pc, _swi
	ldr	pc,=PrefetchAbortHandler
	ldr	pc,=DataAbortHandler
	NOP	/* Placeholder for address exception vector*/
	ldr   pc, _irq
	ldr	pc,=FIQHandler

_irq:   .word FreeRTOS_IRQ_Handler
_swi:   .word FreeRTOS_SWI_Handler
[...略]

Vitisのリンカ設定を見てみます。.vectorsセクションはATCM(密結合メモリA)に置かれます。この環境ではベクタは0番地配置なのでZynqMPのRPUではATCMになります。

次にIRQルートハンドラを見ていきます。”_irq”=”FreeRTOS_IRQ_Handler”と上記に定義されています。FreeRTOS_IRQ_Handlerは以下portASM.Sにアセンブリ定義されています。

/* portASM.S */
[...略]
.align 4
.type FreeRTOS_IRQ_Handler, %function
FreeRTOS_IRQ_Handler:

	[...略]

	/* Call the interrupt handler. */
	PUSH	{r0-r4, lr}
	LDR		r1, vApplicationIRQHandlerConst
	BLX		r1
	POP		{r0-r4, lr}
	ADD		sp, sp, r2
	
	[...略]
	vApplicationIRQHandlerConst: .word vApplicationIRQHandler
  • CPU動作モード切替
  • ネスティング管理
  • CPUコンテキスト退避
  • GIC保留割込み番号読み取り(GICアクノリッジレジスタ)
  • vApplicationIRQHandler呼び出し
  • GIC割込み終了レジスタ書込み
  • CPUコンテキスト復帰orコンテキストスイッチ+復帰

以上の様な処理が実装されていました。vApplicationIRQHandlerは…なんて言えば良いか迷いますが、とりあえずセカンダリIRQルートハンドラとでも呼びましょう。その実装が以下になります。

/* portZynqUltrascale.c */
void vApplicationIRQHandler( uint32_t ulICCIAR )
{
extern XScuGic_Config XScuGic_ConfigTable[];
static const XScuGic_VectorTableEntry *pxVectorTable = XScuGic_ConfigTable[ XPAR_SCUGIC_SINGLE_DEVICE_ID ].HandlerTable;
uint32_t ulInterruptID;
const XScuGic_VectorTableEntry *pxVectorEntry;

	/* The ID of the interrupt is obtained by bitwise ANDing the ICCIAR value
	with 0x3FF. */
	ulInterruptID = ulICCIAR & 0x3FFUL;
	if( ulInterruptID < XSCUGIC_MAX_NUM_INTR_INPUTS )
	{
		/* Call the function installed in the array of installed handler
		functions. */
		pxVectorEntry = &( pxVectorTable[ ulInterruptID ] );
		pxVectorEntry->Handler( pxVectorEntry->CallBackRef );
	}
}

GICアクノリッジレジスタのIRQ番号でIRQハンドラテーブルの登録ハンドラに飛んでいます。IRQハンドラテーブルはGICドライバのインスタンスが指しているのと同じXScuGic_ConfigTable[]になっています。テーブル参照先が同じになっているので、ベアメタル用のGIC設定関数がFreeRTOS環境でも使えるわけですね。

2-3-E. 割込みの有効化とルーティング

マルチコア向けのため、どのコアに割込みを伝えるかを設定する必要があります。以下の呼び出し順で設定しています。

/* intr/xinterrupt_wrap.c*/
void XEnableIntrId( u32 IntrId, UINTPTR IntcParent);
>>/*xscugic.c*/
>>void XScuGic_Enable(XScuGic *InstancePtr, u32 Int_Id)
>>>>/*xscugic.c*/
>>>>void XScuGic_InterruptMaptoCpu(XScuGic *InstancePtr, u8 Cpu_Identifier, u32 Int_Id);

XScuGic_InterruptMaptoCpu()を見てみます。

/*xscugic.c*/
void XScuGic_InterruptMaptoCpu(XScuGic *InstancePtr, u8 Cpu_Identifier, u32 Int_Id)
{
	u32 RegValue;
[...略]
    u8 Cpu_CoreId;
    u32 Offset;
[...略]
    RegValue = XScuGic_DistReadReg(InstancePtr,XSCUGIC_SPI_TARGET_OFFSET_CALC(Int_Id));

    Offset = (Int_Id & 0x3U);
    Cpu_CoreId = (0x1U << Cpu_Identifier);

    RegValue |= (u32)(Cpu_CoreId) << (Offset * 8U);
    XScuGic_DistWriteReg(InstancePtr,
            XSCUGIC_SPI_TARGET_OFFSET_CALC(Int_Id),
            RegValue);
[...略]
}

この関数はSPI割込みイベントをどのCPUコアに送るかのルートを設定します。引数のCpu_Identifierには今回0が入っています。Cortex-R5の0番コアで実行しているからです。

設定するレジスタは以下になります。

割込みプロセッサターゲットレジスタICDIPTR

<引用ARM Generic Interrupt Controller Architecture version 2.0 Architecture Specification B.b>

32bitレジスタで8bitのフィールドを4つ持ちます。各フィールドがIRQに対応します。

フィールド内の設定内容は以下のように、各bitが各CPUに対応します。最大8コアまで対応できるということみたいです。今回はCPU interface 0になります。

レジスタの実際の書込みを見てみました。書込み値は0x01になります。

  • 書込み前 addr=0xF9000878 value=0x00000000
  • 書込み後 addr=0xF9000878 value=0x00000100

SPI89はenable_targets_spi_INTID89 (PL390) Register=0xF9000879なので、しっかりとbit[15:8]に書き込めているようです。

ルーティングが設定できたら、次は割込み許可です。関数呼び出しを1段戻ったXScuGic_Enable()で許可をしています。

/* xscugic.c */
void XScuGic_Enable(XScuGic *InstancePtr, u32 Int_Id)
{
	u32 Mask;
	u8 Cpu_Identifier = (u8)CpuId;
[...略]↓さっきのルーティングの処理
XScuGic_InterruptMaptoCpu(InstancePtr, Cpu_Identifier, Int_Id);
[...略]
	Mask = (u32)0x00000001U << (Int_Id % 32U);
	/*
	 * Enable the selected interrupt source by setting the
	 * corresponding bit in the Enable Set register.
	 */
	XScuGic_DistWriteReg(InstancePtr, (u32)XSCUGIC_ENABLE_SET_OFFSET +
			     ((Int_Id / 32U) * 4U), Mask);
[...略]
}

以下のレジスタで割込み許可を設定します。

割込みイネーブルレジスタICDISER

<引用ARM Generic Interrupt Controller Architecture version 2.0 Architecture Specification B.b>

32bitレジスタで各1bitがIRQに割り当てられています。

1を書き込むと割込みが有効になります。0を書き込むのは無意味になります。読み込むと設定された値が読めます。

割込みを無効にする場合は別の割込みクリアイネーブルレジスタICDICERに1を書き込む必要があります。レジスタが別だとアトミックな操作をする必要が無いですね。

実際のレジスタの書込み値を見てみます。

  • 書込み前 addr=0xF900010C value=0x00000000
  • 書込み後 addr=0xF900010C value=0x02000000

enable_spi_enable_set2 (PL390) Register=0x0xF900010C はSPI64~95までに対応しているので、bit25がSPI89になります。よって正しいデータが書き込まれているようです。

2-3-F. 例外の許可(IRQ)

最後にIRQ例外の許可を行います。AXI-GPIOの割込みを設定する時点ではFreeRTOSが動作しているので、既に許可されているはずです。

Xil_ExceptionEnable()では各例外のうち、IRQを許可します。この関数はマクロ定義で、やっていることはステータス・レジスタCPSRのIRQ許可ビット(bit7)を0(=有効)にセットしています。

カレントプログラムステータスレジスタCPSR

<引用ARM Cortex -R Series Version: 1.0 Programmer’s Guide>

7bit目のIビットがIRQの許可ビットになっており、0で有効になります。

Xil_ExceptionEnable()マクロを展開すると、CPSRを読み出して許可ビット操作して書き戻しています。

/* xil_exception.h */
#define Xil_ExceptionEnable() Xil_ExceptionEnableMask(XIL_EXCEPTION_IRQ)
↓
↓
#define Xil_ExceptionEnableMask(Mask)	mtcpsr(mfcpsr() & ~ ((Mask) & XIL_EXCEPTION_ALL))
↓
↓
mtcpsr(mfcpsr() & (~((0x80)&	(0x40 | 0x80))))
以下はインラインアセンブリ実装
mfcpsr() : cpsrレジスタRead値を返す(MRS命令)
mtcpsr() : cpsrレジスタに値をWriteする(MSR命令)
↓
↓
要約すると
r_cpsr = CPSR;
 w_cpsr = r_cpsr & ~0x80; <--IRQ許可bit(7)を有効
 CPSR = w_cpsr;

3.最後に

GICには大きく分けてディストリビュータ(ICD)とCPUインターフェース(ICC)に分かれていて、今回は前者のみのレジスタ設定を扱いました(と言ってもまだ一部)。ICCやGICv2はまたそのうち深めていこうと思います。

私がGICに触るのは初めての経験でした。設定関数の実装も読もうと思えば読める程度の量で、かつ、FreeRTOSやGPIO割込みを使う動くコードが手元に合ったことで、いろいろ試していじったり出来ました。このおかげで沼に落ちること無く、中身を理解することが出来たと思います。FreeRTOSのポーティング部分の実装も結構よく出来ていて、いい勉強になったと思います。

GICドライバのソースを追いかけていく中で、GICv3のコンパイルスイッチがいろいろな箇所にあります。AMD/Xilinxさんは現在未発表ですが、Versal-NETと呼ばれるらしい新Versalシリーズの片鱗がBSPのコードの中に見えています。Cortex-A78AEとCortex-R52の組み合わせのようです。そうなると実装されるのはGIC-600AE(GICv3)あたりでしょうか。(オープンソースなのは良いのですが、ビジネス的に次期新製品バレバレなの大丈夫なのかな…)

ABOUT ME
sh-goto
組込エンジニア. 最近の遊びはLinuxの低レイヤいじりとXilinxのZynqとFPGAを使った電子工作
関連記事