本記事の概要
今回は、タイマー回路の信号から「割り込み」をかけて、LED点滅回路に一定のタイミングで点灯させられるように改良しました。
Digilent社のHDMI入出力のデモアプリケーションについて勉強を進めていたところ、「割り込み」をかける処理がありました。
「割り込み」ってどうやってソースコードを書いたらいいんだろう?なんだか難しそうだな。
そこで、Zynqのプロセッサ上で「割り込み」をかけるAPI(Application Programming Interface)の勉強も兼ねて、よりシンプルなLED点滅回路に立ち戻って、AXI Timerからの割り込みを組み込んだLED点滅のアプリケーションを作成しました。
本アプリケーションを通じて、VitisやXilinx SDKのAPIを用いた割り込みのかけ方について学んでいきたいと思います。
「LED点滅編(3)」で説明した「インスタンス」の概念を知っておくと、より理解しやすいかと思います。
それでは、興味のある方はぜひ最後までご覧ください!
本記事の目標
統合ソフトウェア開発環境VitisとFPGA設計環境Vivadoにおいて、
プロセッサに割り込みをかけるブロックデザインとアプリケーションプロジェクトの作成方法について理解する
デバイス構成
割り込みとは
CPUでは、基本的にメモリに書いてある順番に従って、シーケンシャルに命令を実行していきます。
このメインルーチンで処理を行っている間に、何らかのイベントをトリガとしてその処理を中断し、別のプログラムで別の処理を行うことは「割り込み」と呼ばれています。
このようなトリガとなるイベントは「割り込み要求」、呼び出される別のプログラムは「割り込みハンドラ」や「割り込みサービスルーチン」と呼ばれています。
本記事では、タイマーが一定時間経過するごとに割り込み要求をCPUに送り、周期的にLEDの点灯パターンを変えていく割り込み処理を組みたいと思っています。
タイマー処理LED点滅回路のZynq構成
図に示すのは、Zybo上のデバイス構成を模式的に描いたものです。
「LED点滅編(3)」では、CPUからGPIOに点灯パターンを送信し、一定の周期でLEDを点灯させました。
今回の記事では、一定時刻ごとにLEDの点灯パターンを変える割り込み処理を行うために、Programmable Logic(PL)にAXI Timerを追加します。
AXI Timerから一定時刻ごとに出力される信号は、Application Processor Unit (APU)内のGIC(Generic Interrupt Controller; 汎用割り込みコントローラ)を介して、CPUに割り込み要求をかけます。
各IPコアやGICといったデバイスへの制御信号はAXI4-Liteを通じて、メインルーチン上でCPUから予め送信するようにしました。
制御信号送信後、whileループを回し続け、割り込み要求を受けるたびに、LEDの点灯パターンを変えるようなプログラムをアプリケーションプロジェクト上で組みたいと思います。
Vivadoブロックデザインの作成
では、Vivadoのブロックデザインにタイマーを追加し、GICの共用ペリフェラル割り込み(SPI)端子にタイマーからの割り込み要求を接続しましょう。
「LED点滅編(3)」で作成した下図のようなブロックダイアグラムに、AXI TimerのIPコアを追加します。
ブロックダイアグラム上で右クリックをし、[Add IP]を選択、ポップアップ内で[axi timer]と入力して、[AXI Timer]を選択します。
次に、IPコア[ZYNQ7 Processing System]に、GICの共用ペリフェラル割り込み(SPI)端子を追加します。
[ZYNQ7 Processing System]をダブルクリックし、[Interrupts]のタブを選択します。
[Fabric Interrupts]にチェック、さらに[IRQ_F2P[15:0]]にチェックを入れます。
この操作により、PL部からの共用ペリフェラル割り込み端子をイネーブルすることができます。
最後に、AXI TimerのIPコアのinterrupt端子と追加したIRQ_F2P端子とを接続すれば、ブロックダイアグラムは完成です。
あとは、wrapperファイルの作成、論理合成、配置配線、XSAファイルの作成まで行なってしまいましょう。
次に、アプリケーションプロジェクトの作成をVitis上で行っていきます。
アプリケーションプロジェクトの構成
割り込みに関係するVitis(Xilinx SDK)のドライバAPI
ここからが、本記事で一番のポイントになります。
APIを利用して、どのようにアプリケーション上で割り込みの処理をかけるかについて、図解しながら述べていきたいと思います。
どのAPIを使えばタイマー割り込みがかけられるかを考えるにあたり、次のリンク先のFPGAプログラミング大全のタイマー割り込みに関する節を参考にしています。
基本的なプロジェクトの作成方法やシミュレーションの方法からAPIの活用方法の基礎的なとこrまでが網羅的に記載されているので、初学者がさらにステップアップするのに非常に素晴らしい書籍だと思います。
割り込みベクタテーブルについて
まず、割り込み要求をCPUが受けたときに、CPUはどのようにして割り込み処理のプログラム(割り込みハンドラ)を選定するのでしょうか?
割り込み要求を発生させる要因は一つではありません。
種々の割り込み要因に対して、常に同じプログラムで割り込み処理を行うわけではないので、割り込み要因に応じて別のプログラム(割り込みハンドラ)へと分岐することが必要です。
この対応する割り込みハンドラへ分岐するための手続きを記述したプログラムは、割り込みベクタテーブルと呼ばれています。
割り込みベクタテーブルには、予め「割り込みハンドラの関数ポインタ(Handler)」と「割り込みハンドラに送る引数(CallBackRef)」を登録しておきます。
割り込み要求を受けたアプリケーションは、その要求に含まれる割り込みの発生源を表すIDの情報に紐付いた割り込みハンドラを、割り込みベクタテーブルに従って参照します。
割り込みベクターテーブルに割り込みハンドラを追加するAPI関数
割り込みベクタテーブルに割り込みハンドラを登録するAPI関数が、Vitis/Xilinx SDKには用意されています。
xil_exception.cの[Xil_ExceptionRegisterHandler()]という関数です。
次のコードのように、Xil_ExceptionRegisterHandler()に、「割り込み発生源のID」、「割り込みハンドラの関数ポインタ」、「割り込みハンドラに送る引数」を代入し実行すると、割り込みベクタテーブルに割り込みハンドラを登録することができます。
// 0.1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler,
&GICInst);
GICなどの各インスタンスにも割り込みハンドラテーブルが用意されている
以上が、アプリケーションで一番最初に割り込み要求を受けたときに行う処理になります
ただし、割り込みベクタテーブルだけですべての管理を行おうとすると、割り込み処理が複雑になってしまいます。
その理由を、GICを例に見ていきましょう。
例えば、GICの説明で見たとおり、GICにはPLに存在するタイマー以外の周辺機器と接続することもできます。それ以外にもPSからの割り込み信号を受けることも可能です。
そのため、アプリケーションに入力される割り込み信号の分岐だけでは不十分で、GICの入力源に対しても割り込み要求を分類し、適切な割り込みハンドラに分岐することが必要です。
その分岐のために、GICインスタンスには、割り込みベクタテーブルと同様の、割り込みハンドラテーブルが用意されています。
下の図のように、GICインスタンスと実際のGICを紐付けておき、GICの入力源に従って対応する割り込みハンドラを登録しておけば、割り込み要求に応じて更に適切な割り込みハンドラへと分岐させることが可能になります。
図のように、割り込みハンドラを分岐させるテーブルを、インスタンスごとに階層構造にしておけば、可読性が高いだけでなく、後から第三者による変更が容易なプログラムを作成することが可能です。
GICインスタンスに割り込みハンドラを追加するAPI関数
GICインスタンスに割り込みハンドラを登録するAPI関数は、xscugic.cの[XScuGic_Connect()]という関数です。
名前から分かる通り、GICと接続したデバイスのインスタンスと、アプリケーション上でも接続する関数です。
次のコードのように、「GICインスタンスのポインタ」、「割り込み発生源のID」、「割り込みハンドラの関数ポインタ」、「割り込みハンドラに送る引数」をそれぞれ代入し実行すると、GICインスタンスにも割り込みハンドラを登録することができます。
// 1.2 GIC割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
status = XScuGic_Connect(&GICInst, INTC_TMR_INTERRUPT_ID,
(Xil_ExceptionHandler)XTmrCtr_InterruptHandler, (void *) &TMRInst);
以上が、割り込みハンドラをアプリケーション上で登録するための基本概念になります。では、実際のLED点滅回路のソースコードの具体例を見ながら、詳細について理解していきます。
ソースコードの具体例
以下のコードが実際に筆者が組んだソースコードです。
記事の最後にまとめた参考文献をもとに少し改変しました。
#include <stdio.h>
#include "xparameters.h"
#include "xgpio.h"
#include "xtmrctr.h"
#include "xscugic.h"
// Parameter definitions
#define LEDS_DEVICE_ID XPAR_AXI_GPIO_0_DEVICE_ID
#define INTC_DEVICE_ID XPAR_PS7_SCUGIC_0_DEVICE_ID
#define TMR_DEVICE_ID XPAR_TMRCTR_0_DEVICE_ID
#define INTC_TMR_INTERRUPT_ID XPAR_FABRIC_AXI_TIMER_0_INTERRUPT_INTR
#define TMR_LOAD 0xF8000000 // 100MHz
#define LED_CHANNEL 1
#define LED_DIRECTMASK 0x00
XGpio LEDInst;
XScuGic GICInst;
XTmrCtr TMRInst;
static int led_data;
// -------- function prototypes
static void TMR_Intr_Handler(void *baseaddr_p);
int main()
{
int status;
//-----------------------------------------------------
// 0. 例外処理
//-----------------------------------------------------
// 0.1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler,
&GICInst);
// ARMの割り込みをイネーブル
Xil_ExceptionEnable();
//-----------------------------------------------------
// 1. GIC(Generic Interrupt Controller)
//-----------------------------------------------------
// 1.1 GICの初期化
XScuGic_Config *IntcConfig;
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
status = XScuGic_CfgInitialize(&GICInst, IntcConfig,
IntcConfig->CpuBaseAddress);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 1.2 GIC割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
status = XScuGic_Connect(&GICInst, INTC_TMR_INTERRUPT_ID,
(Xil_ExceptionHandler)XTmrCtr_InterruptHandler, (void *) &TMRInst);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// GICの割り込みをイネーブル
XScuGic_Enable(&GICInst, INTC_TMR_INTERRUPT_ID);
//-----------------------------------------------------
// 2. AXI Timer
//-----------------------------------------------------
// 2.1 AXI Timerの初期化
status = XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 2.2 AXI Timer割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
XTmrCtr_SetHandler(&TMRInst, (XTmrCtr_Handler) TMR_Intr_Handler, &TMRInst);
// 2.3 AXI Timerのリセット値の設定
XTmrCtr_SetResetValue(&TMRInst, 0, TMR_LOAD);
// 2.4 AXI Timer optionの設定 (Interrupt Mode And Auto Reload )
XTmrCtr_SetOptions(&TMRInst, 0,
XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
//-----------------------------------------------------
// 3. GPIO
//-----------------------------------------------------
// 3.1 GPIOの初期化
status = XGpio_Initialize(&LEDInst, LEDS_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 3.2 GPIOのdirection設定
led_data = 0;
XGpio_SetDataDirection(&LEDInst, LED_CHANNEL, LED_DIRECTMASK);
//-----------------------------------------------------
// 4. タイマーのカウント開始
//-----------------------------------------------------
XTmrCtr_Start(&TMRInst, 0);
while(1);
//-----------------------------------------------------
return 0;
}
//-------------------------------------------------
// A. ユーザー割り込み処理関数の定義
//-------------------------------------------------
void TMR_Intr_Handler(void *data)
{
// A.0 タイマーが定時に達しているか?
if (XTmrCtr_IsExpired(&TMRInst, 0)) {
// A.1 タイマーのカウント停止
XTmrCtr_Stop(&TMRInst, 0);
// A.2 GPIO出力信号の変更
led_data = led_data + 1;
XGpio_DiscreteWrite(&LEDInst, LED_CHANNEL, led_data);
// A.3 タイマーのリセット
XTmrCtr_Reset(&TMRInst, 0);
// A.4 タイマーのカウント開始
XTmrCtr_Start(&TMRInst, 0);
}
}
ソースコードの解説
①インスタンスの宣言
まずはインスタンスを宣言しています。
インスタンスの初期化(実際のデバイスとの紐付け)は後からでもよいのですが、インスタンスを最初に宣言しておき、メモリ内に領域を確保しておきます。
XGpio LEDInst;
XScuGic GICInst;
XTmrCtr TMRInst;
②ARMプロセッサに割り込みが発生したときについて割り込みベクタテーブルを登録する
割り込みベクタテーブルに割り込みハンドラを登録します。
GICの入力源に応じて更に分岐ができるようにしました。
GICインスタンスの割り込みハンドラテーブルにアクセスして次の分岐を行えるように、関数「XScuGic_InterruptHandler」への関数ポインタを入力しています。
//-----------------------------------------------------
// 0. 例外処理
//-----------------------------------------------------
// 0.1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) XScuGic_InterruptHandler,
&GICInst);
// ARMの割り込みをイネーブル
Xil_ExceptionEnable();
③GICインスタンスの初期化と割り込みハンドラの登録
次に、GICインスタンスです。
GICインスタンスをまだ初期化していなかったので初期化を行い、割り込みハンドラテーブルに次に行う割り込み処理を登録しましょう。
登録する割り込みハンドラにはAXI Timerの割り込みハンドラテーブルでの分岐処理を登録します。
AXI Timerには2チャンネル設けることができ、実はタイマーの入力源によっても処理を分岐させることが可能です。
そのため、タイマーのインスタンスにも割り込みハンドラテーブルが用意されています。
複雑ですが、GICの割り込みハンドラテーブルには、タイマーインスタンスの割り込みハンドラテーブルを登録して更に分岐できるようにしておきます。
//-----------------------------------------------------
// 1. GIC(Generic Interrupt Controller)
//-----------------------------------------------------
// 1.1 GICの初期化
XScuGic_Config *IntcConfig;
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
status = XScuGic_CfgInitialize(&GICInst, IntcConfig, IntcConfig->CpuBaseAddress);
// 1.2 GIC割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
status = XScuGic_Connect(&GICInst, INTC_TMR_INTERRUPT_ID,
(Xil_ExceptionHandler)XTmrCtr_InterruptHandler, (void *) &TMRInst);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// GICの割り込みをイネーブル
XScuGic_Enable(&GICInst, INTC_TMR_INTERRUPT_ID);
④タイマーインスタンスの初期化と割り込みハンドラの登録
タイマーインスタンスについても同様です。
タイマーインスタンスの割り込みハンドラテーブルに、最終的に実行するユーザー定義関数の関数ポインタを登録します。
その他、タイマーに関する初期設定(何カウント回すか?モードは?など)を設定します。
ユーザー定義の割り込み処理関数は、単純にタイマーが一定時刻経過していたら、LEDに出力する値をカウントして、GPIO出力するというものです。
GPIOへのレジスタ書き込みの方法は「LED点滅編(3)」に記載した方法と同様です。
//-----------------------------------------------------
// 2. AXI Timer
//-----------------------------------------------------
// 2.1 AXI Timerの初期化
status = XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 2.2 AXI Timer割り込み発生時に呼び出されるインスタンス(今回はタイマー)の割り込みハンドラを登録
XTmrCtr_SetHandler(&TMRInst, (XTmrCtr_Handler) TMR_Intr_Handler, &TMRInst);
// 2.3 AXI Timerのリセット値の設定
XTmrCtr_SetResetValue(&TMRInst, 0, TMR_LOAD);
// 2.4 AXI Timer optionの設定 (Interrupt Mode And Auto Reload )
XTmrCtr_SetOptions(&TMRInst, 0,
XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
//-------------------------------------------------
// A. ユーザー割り込み処理関数の定義
//-------------------------------------------------
void TMR_Intr_Handler(void *data)
{
// A.0 タイマーが定時に達しているか?
if (XTmrCtr_IsExpired(&TMRInst, 0)) {
// A.1 タイマーのカウント停止
XTmrCtr_Stop(&TMRInst, 0);
// A.2 GPIO出力信号の変更
led_data = led_data + 1;
XGpio_DiscreteWrite(&LEDInst, LED_CHANNEL, led_data);
// A.3 タイマーのリセット
XTmrCtr_Reset(&TMRInst, 0);
// A.4 タイマーのカウント開始
XTmrCtr_Start(&TMRInst, 0);
}
}
⑤その他 -GPIOインスタンスの初期化や無限ループ(while(1))-
いつでも良かったのですが、最終段のGPIOのインスタンスも初期化しておくのを忘れないようにしないといけません。
最後に、タイマーのカウントを開始して無限ループを回せばプログラムは完成です。
//-----------------------------------------------------
// 3. GPIO
//-----------------------------------------------------
// 3.1 GPIOの初期化
status = XGpio_Initialize(&LEDInst, LEDS_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 3.2 GPIOのdirection設定
led_data = 0;
XGpio_SetDataDirection(&LEDInst, LED_CHANNEL, LED_DIRECTMASK);
//-----------------------------------------------------
// 4. タイマーのカウント開始
//-----------------------------------------------------
XTmrCtr_Start(&TMRInst, 0);
while(1);
//-----------------------------------------------------
かなりくどい感じになりましたが、ソースコードの解説は以上です。オブジェクト指向の考えでコードを整理してみるとわかりやすいと思います。こうして図解してみると、GUIベースのプログラミング環境も構築できそうですね。
動作確認
コード例での動作確認
では、上述したコードでビルド、Zynqへのコンフィグレーションを行いましょう。
問題なく、約1.3秒周期でLEDを点滅させることができました!
直接ユーザー定義処理関数を呼び出しても動作するのか?
発想
割り込みハンドラテーブルの分岐処理がややこしい。直接ユーザー定義割り込み処理関数にとばしたい!
この発想に基づいて、コードを変更してみました。
ただし、前述したとおり、インスタンスの概念に基づき、階層ごとに割り込みハンドラを登録するほうが、応用性の高いソースコードを書くことが可能ですので、大規模なプログラムを書くときには避けるほうが良いかと思います。
int main()
{
int status;
//-----------------------------------------------------
// 0. 例外処理
//-----------------------------------------------------
// 0.1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler) TMR_Intr_Handler, &TMRInst);
// ARMの割り込みをイネーブル
Xil_ExceptionEnable();
//-----------------------------------------------------
// 1. GIC(Generic Interrupt Controller)
//-----------------------------------------------------
// 1.1 GICの初期化
XScuGic_Config *IntcConfig;
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
status = XScuGic_CfgInitialize(&GICInst, IntcConfig,
IntcConfig->CpuBaseAddress);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// GICの割り込みをイネーブル
XScuGic_Enable(&GICInst, INTC_TMR_INTERRUPT_ID);
//-----------------------------------------------------
// 2. AXI Timer
//-----------------------------------------------------
// 2.1 AXI Timerの初期化
status = XTmrCtr_Initialize(&TMRInst, TMR_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 2.2 AXI Timerのリセット値の設定
XTmrCtr_SetResetValue(&TMRInst, 0, TMR_LOAD);
// 2.3 AXI Timer optionの設定 (Interrupt Mode And Auto Reload )
XTmrCtr_SetOptions(&TMRInst, 0,
XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
//-----------------------------------------------------
// 3. GPIO
//-----------------------------------------------------
// 3.1 GPIOの初期化
status = XGpio_Initialize(&LEDInst, LEDS_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 3.2 GPIOのdirection設定
led_data = 0;
XGpio_SetDataDirection(&LEDInst, LED_CHANNEL, LED_DIRECTMASK);
//-----------------------------------------------------
// 4. タイマーのカウント開始
//-----------------------------------------------------
XTmrCtr_Start(&TMRInst, 0);
while(1);
//-----------------------------------------------------
return 0;
}
結果
このソースコードでも問題なく動作しました!
まとめ
以上をまとめると次の通りです:
割り込みを勉強することで、かなりドライバAPIで用いられる概念について理解が深まったと思います。あくまでも私の所感になりますが、インスタンスを中心としたオブジェクト指向的な書き方でソースコードを書けば、わかりやすいと感じました。
ここまで読んでいただき、ありがとうございました!
参考:コードの動作を確認したシステムの構成
開発環境
開発ボード Zybo Zynq-7010評価ボード
参考文献
https://japan.xilinx.com/support/documentation/user_guides/j_ug585-Zynq-7000-TRM.pdf
・次のYoutubeの動画では、タイマーを使ったLED点滅アプリケーションの作成方法を動画で解説しており、実際の作業の仕方を知ることができます。
コメント