ホームチュートリアルプログラム
1-セットアップ 4-サウンド 7-FIFO インベーダー
2-フレームバッファ 5-SRAM 8-割り込み DSEmu
3-キー入力 6-ファイルシステム 9-マイク  
10-拡張回転バックグラウンド

Homebrew任天堂DS開発 Part 8

割り込み

'割り込み' は任天堂DSのCPUの現在の実行を中断し、重要な関数を実行する方法です。その関数から戻ると、DSは中断される前に実行していたコードをそのまま実行し続けます。

何かが起きたときに現在実行中のプログラムに'暇ができたとき'に実行するより、確実に処理されるので割り込みは重要です。 それは重要な事柄を失うのを防ぎます。1つの非常に共通な割り込みは チュートリアル2 で議論した垂直ブランク割り込みです。

割り込みの別の有効な局面は割り込みが発生するまで CPU を低消費電力モードにできることです。 低消費電力モードでは、割り込みが発生するまで CPU は実質停止しています。割り込みハンドラで処理の大部分を行うなら、CPUは低消費電力モードで時間の大半をすごすことができます。これはDSのバッテリーの寿命を伸ばします。

割り込みレジスタ

割り込みとその操作に対応する3つの非常に重要なレジスタがあります。それらは以下の通りです。

名前アドレスサイズ記述
IME 0x04000208 16 bits Interrupt Master Enable レジスタ
IE 0x04000210 32 bits Interrupt Enable レジスタ
IF 0x04000214 32 bits Interrupt Flags レジスタ

各CPUには、それら自身のこれらのレジスタのコピーを持っていて、それら自身の割り込みハンドラがいます。

Interrupt Master Enable レジスタ

IME レジスタはすべての割り込みのハンドリングをオン・オフにすることを提供します。 ビット0をオフにするとすべての割り込みが停止します。ビット0をオンにすると割り込みが発生するようになります。

これは割り込み処理が完了する前に他の割り込みをブロックするのに有効です。 例えば、自分自身で割り込みレジスタの値を変更する場合です。すべての割り込みレジスタの値を変更したい場合、途中で他の割り込みが発生して予期しない状態が発生しないように、割り込みをオフにすべきです。以下のコードがこれを行っています。

  // 割り込みの禁止
  IME=0;

  [...処理の実行...]

  // 割り込みの許可
  IME=1

Interrupt Enable レジスタ

IE レジスタは個々の割り込みをオン・オフにすることができます。 レジスタのそれぞれのビットは発生する特定の割り込みに対応しています。これらは DSTek において ARM9 IE レジスタ と記載されています。

ビットlibnds の定義記述
0IRQ_VBLANK垂直ブランクの期間
1IRQ_HBLANK水平ブランクの期間
2IRQ_YTRIGGERREG_VCOUNT scanline reached
3IRQ_TIMER0タイマー 0 トリガ
4IRQ_TIMER1タイマー 1 トリガ
5IRQ_TIMER2タイマー 2 トリガ
6IRQ_TIMER3タイマー 3 トリガ
7IRQ_NETWORK不明
8IRQ_DMA0DMA 0
9IRQ_DMA1DMA 1
10IRQ_DMA2DMA 2
11IRQ_DMA3DMA 3
12IRQ_KEYSキーの押下
13IRQ_CARTGBA フラッシュカートのの取り出し
16ARM7 からの IPC 割り込み
17受信 FIFO が空でない
18送信 FIFO が空でない
19IRQ_CARDDS card data completed
20IRQ_CARD_LINEDS カード割り込み
21GFX FIFO 割り込み

ARM7 IE レジスタはDSTek によると他のいくつかの割り込みがあります。

ビットlibnds の定義記述
22パワーマネージメント割り込み
23SPI 割り込み
24WIFI 割り込み

Using the IE レジスタ is a simple matter of setting it to the OR'd value of the 割り込み you want to handle. このチュートリアルのサンプルでは垂直ブランク割り込みとキー割り込みを使用します。

  IE = IRQ_VBLANK | IRQ_KEYS;

Interrupt Flags レジスタ

割り込みが発生するとき、IF レジスタはDSによって設定されます。 これはどの割り込みが発生したかのビットマスクを含んでいます。 複数の割り込みが発生したときは、IF は1ビット以上のビットがセットされていることに注意してください。 割り込みハンドラは IF レジスタをチェックして、どの割り込みを処理するかを判断します。

IF レジスタはまた、他の目的も持っています。割り込みルーチンの終わりでそれは実際に処理した割り込みのビットマスクを設定すべきです。 したがって、ハンドラに対するエントリでは、それはどの割り込みが発生したかのビットをセットしています。 そしてハンドラの終了時点では処理した割り込みに対するビットをセットする必要があります。 もし、既にビットがセットされていても、明示的にセットすべきです。 セットする目的はDSに対すテ割り込みをハンドルしたと記録させるシグナルです。

  if(IF & IRQ_VBLANK) {
    on_irq_vblank();
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;  
    IF |= IRQ_VBLANK;
  }
  
  if(IF & IRQ_KEYS) {
    on_irq_keys();
    VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS;  
    IF |= IRQ_KEYS;
  }

  [...その他のコード...]

この場合、IF レジスタ自身の値に垂直ブランク割り込みおよびキー割り込みを or します。 それとそれ自身の値を 'or' するば、割り込みハンドラのサブルーチンまたはネストされた割り込みでIFにいかなる他の変更も上書きしません。

割り込みハンドラ

割り込みが発生すると割り込みハンドラが呼ばれます。標準の割り込みハンドラは、任天堂DS の BIOS にあります。

ARM9 の割り込み

ARM9 ではコードは 0xFFFF0018 に位置しています。割り込みが発生するとプロセッサはいくつかの情報(リターンアドレスなど)を保存し、即時に 0xFFFF0018 にジャンプします。

0xFFFF0018 の命令は標準 BIOS 割り込みルーチンへのブランチ命令です。このルーチンは私達が書き込みできる特別なメモリアドレスに格納された関数にジャンプします。 このメモリアドレスに私達の関数へのポインタを格納することにより、私たち自身の関数で割り込みをハンドルさせることができます。

使用されるメモリアドレスは DTCM+0x3FFC として技術的に知られています。DTCM は様々な実際の物理アドレスを 'マップする' ARM9 の特別なメモリエリアです。 デフォルトで DevkitPRO のセットアップでは DTCM は 0x00800000 に位置しています。したがって、割り込みハンドラのアドレスは 0x00803FFC に格納されています。 これは libnds では以下のように定義されています。

#define IRQ_HANDLER (*(VoidFunctionPointer *)0x00803FFC)

libnds では ARM9 IRQ_HANDLER の物理アドレスをハードコードしていますが、もし、DTCM が異なったアドレスにマップされると、異なっている場合があります。 異なったセットアップを使用する場合は、このことを意識してください。

ARM7 の割り込み

ARM7 には DTCM メモリがありません。この割り込みハンドラのアドレスはメモリの 0x03FFFFFC に位置し、移動することはできません。 libnds での ARM7 定義は以下の通りです。

#define IRQ_HANDLER (*(VoidFunctionPointer *)(0x04000000-4))

ハンドラの設定

IRQ_HANDLER に関数へのポインタを割り当てると、あなた自身の割り込みハンドラを設定できます。 以下がこのサンプルプログラムでのセットアップコードです。

  IME = 0;
  IRQ_HANDLER = on_irq;
  IE = IRQ_VBLANK | IRQ_KEYS;
  IF = ~0;
  IME = 1;

最初にIMEに '0' をセットして割り込みを禁止します。 そして、'on_irq' を IRQ_HANDLER にセットします。'on_irq' は私達の割り込みハンドラ関数です。

void on_irq() 
{
  if(IF & IRQ_VBLANK) {
    on_irq_vblank();
    VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;  
    IF |= IRQ_VBLANK;
  }
  
  if(IF & IRQ_KEYS) {
    on_irq_keys();
    VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS;  
    IF |= IRQ_KEYS;
  }
}

割り込みが発生するつど、'on_irq' が呼び出されます。 この 'Interrupt Enable' レジスタでは、垂直ブランク割り込みとキー割り込みを処理されるように設定されます。. IF はデフォルト値が設定され、割り込みは続けられます。 turned back on.

垂直ブランク割り込み

グラフィックスハードウェアで DS スクリーンを更新する場合、それはスクリーンの左上から右下まで1行ずつ処理されます。 スクリーンメモリに書き込むときに、同じ行が描画されていると、表示がおかしくなる場合があります。 これに関しては チュートリアル2で解説しました。

これが起きるのを防ぐために、ハードウェアは水平ブランク割り込みをそれぞれの行の終わりに発生させます。あなたはここに処理を行う短い時間を得れます。 最後の行の終わりに、水平ブランク割り込みの後, 垂直ブランク割り込みが発生します。この時点でハードウェアは最後の行から最初の行に移動を行いますが、ここでスクリーンを書き直すコードが実行できます。

これが垂直ブランク割り込みでメモリに書き込むことができます。 このサンプルプログラムでは、私達が以前フレームバッファ チュートリアルでしたと同様に処理を行います。 四角形はスクリーン内を移動して描画されます。

また、垂直ブランク割り込み(VBI)も追加のセットアップが必要です。VBIをハンドルしたいなら私達が示したように適切な IE ビットをセットするだけでなく、 DISP_SR レジスタのビットもセットする必要があります。 このレジスタは水平ブランク割り込みもセットできますが、今回は不要なので、以下のコードのように libnds の定義を使用して、適切なビットをセットします。

  DISP_SR = DISP_VBLANK_IRQ;

私達の垂直ブランク割り込みフレームバッファのサンプルと同様にスクリーンに描画を行います。 また、X 座標と Y 座標を増加させますので、四角形はスクリーンの右下に移動します。

void on_irq_vblank() 
{	
  old_x = shape_x;
  old_y = shape_y;
  shape_x++;
  if(shape_x + shape_width >= SCREEN_WIDTH) {
    shape_x = 0;
    shape_y += shape_height;
    if(shape_y + shape_height >= SCREEN_HEIGHT) {
      shape_y = 0;
    }
  }      

  draw_shape(old_x, old_y, VRAM_A, RGB15(0, 0, 0));
  draw_shape(shape_x, shape_y, VRAM_A, RGB15(31, 0, 0));
  consoleClear();
  consolePrintf("Interrupt Count: %d\n", Interrupt_count);
  consolePrintf("SWI Count      : %d\n", swi_return_count);
}

またハンドラは2番目のDSスクリーンに何らかのテキストを 'コンソール' として表示するでしょう。 それはキー割り込みが発生した回数のカウンタ(Interrupt_count)とこのチュートリアルの後で説明するSWI BIOS 呼び出しが戻した回数(swi_return_count)表示します。

キー割り込み

私達のでもプログラムはキー押下による割り込みに対応します。 あなたは割り込みにおけるすべてのキールーチンをハンドルするわけではありませんが、いくつかのキーがそこで効果的にハンドルされます。 割り込みはすぐに呼び出されるので、'pause' キーや 'reset' キーのハンドラに対して有効だからです。

Aキー割り込みの欠点はそれが呼び出されるのはキーが押された時点だけで、キーが話された時点では呼び出しされません。 これは、よく見受けられる 'キーを押し続ける' といったシナリオを処理することを妨げます。 このため、キーハンドリングコードはアプリケーションのメインループ内に存在しています。

垂直ブランク割り込み(VBI)と同様に、キー割り込みの処理にも特別なセットアップコードが必要です。 どのキーが割り込みを発生させるか KEYS_CR レジスタにビットをセットする必要があります。ビットは以下の通りです。

ビットlibnds の定義記述
0KEY_AA ボタン
1KEY_BB ボタン
2KEY_SELECTSelect ボタン
3KEY_STARTStart ボタン
4KEY_RIGHTRight ボタン
5KEY_LEFTLeft ボタン
6KEY_UP上方向キー
7KEY_DOWN下方向キー
8KEY_R右方向キー
9KEY_L左方向キー
10未使用
11未使用
12未使用
13未使用
14キー割り込みの許可 (0=禁止, 1=許可)
15割り込み状況 (0=or, 1=and)

キー割り込みを有効にするにはビット 14 をオンに設定する必要があります。ビット 15がオフならビットマスクのどれかのキーが押されると割り込みが発生します。 またオンなら、すべてのキーが押されると割り込みが発生します。サンプルプログラムではユーザーが A と B の両方のキーが押された時に割り込みを発生させたいので以下のコードとなります。

  // A と B の両者が押された時に割り込みが発生
  // ビット 15 は両者のキーが押された判定を行うので、オンに設定する
  KEYS_CR = KEY_A | KEY_B | (1<<14) | (1<<15);

キーハンドラは、割り込みが何回発生したかを表示するためにカウンタを増加させます。 このカウンタは垂直ブランク割り込み期間中に2番目のDSスクリーンに表示します。

void on_irq_keys() 
{
  // キー割り込みの発生
  Interrupt_count++;
}

割り込み待機、BIOS および SWI 呼び出し

割り込みが発生するのを待機する BIOS ルーチンがあります。 このルーチンは ARM9 を '低消費電力' モードに切り替えて、命令を処理するのを停止し、いくつかのメモリバンクをパワーダウンします。

他のチュートリアルで 'swiWaitForVBlank' 呼び出しとして間接的に使用されるのを見たはずです。 これは垂直ブランク割り込み(VBI)を待機しながら、ARM9 を派ワダウンします。 VBIの結果として割り込みハンドラが呼び出された後、ARM9 のパワーは元の状態に戻り、処理が継続されます。 このルーチンを呼び出すことによりパワーを節約できるので、DSのバッテリーの状態を長持ちさせることになります。 これは 'SWI 5' として知られています。幸い、ibnds は C 言語から BIOS を呼び出せる関数 swiWaitForVBlank を持っています。

'SWI' はプロセッサが特別内地のメモリ (BIOS) にジャンプする ARM 機械語命令です。 それは SWI に続く数値をインデックスとして、アドレス配列に指定されたアドレスにジャンプします。

指定したビットマスクに適合したどんな割り込みも待機できる汎用的な BIOS ルーチンがあります。 これで、どんな特定の割り込みの発生を待機することが可能となり、パワーダウンすることができます。これは 'SWI 4' として知られています。

このデモでは、キー割り込みを待機するために、より汎用的な BIOS 機能をどのように呼び出すか解説するつもりです。 それは ARM インライン アセンブラを使用します。

void swiWaitForKeys() 
{
  asm("mov r0, #1");
  asm("mov r1, #4096");
  asm("swi #262144");
}

最初の2つの命令が ARM レジスタの R0 と R1 をセットアップします。 R0 の '1' は、次の割り込みを待機することを意味します。R1 の 4096 という値は IRQ_KEYS です。SWI 渡された '262144' は、10進数 '4' を左に 16回シフトしました。 '4' は割り込み待機 BIOS ルーチンの SWI 番号です。

この関数を呼び出すことにより、BIOS 中の割り込み待機ルーチンが呼び出されます。実際の割り込みルーチンは以下の通りです。

   while(!(WAIT_FOR_INTR & R1)) {
     ; ARM9 を低消費電力モードに切り替え、割り込みを待機
     MCR     p15, 0, LR,c7,c0, 4

     ; 割り込みが発生し、割り込みハンドラから復帰したのでループ状態に戻る 
   }
   ; SWIからの復帰

WAIT_FOR_INTR は割り込みがハンドルされたビットセットを含むビットマスクを格納するメモリドレスです。それは DTCM+0x3FF8 に位置しています。 これは以前の DTCM に関するディスカッションにしたがって、DevKitPRO の ARM9 ではこれは 0x00803FF8 です。libnds ではこれは VBLANK_INTR_WAIT_FLAGS と定義されています。 これは名前の示す通りの垂直ブランク割り込みだけではありません。

重要なことは割り込みを待機するのに SWI ルーチンを使用するなら、VBLANK_INTR_WAIT_FLAGS に適切なビットをセットしなければならないということです。 そうしないと ARM9 プロセッサはハングしているように見えるでしょう。

したがって、swiWaitForVBlank を使用するなら、垂直ブランク割り込みハンドラのコードは以下のようなコードを含まなければいけません。

  VBLANK_INTR_WAIT_FLAGS |= IRQ_VBLANK;  

私達は swiWaitForKeys を使用する予定なので、キーのために以下のコードが必要です。

  VBLANK_INTR_WAIT_FLAGS |= IRQ_KEYS;  

私はフォーラムの 'swiWaitForVBlank' に関する報告で、それが壊れていてハングしているというのを見たことがあります。 VBLANK_INTR_WAIT_FLAGS を設定していないという理由は十分にありそうです。.

libnds と割り込み

libnds は割り込みハンドラを設定するルーチンがあり少し簡単です。私がこのルーチンを使用しなかったのには2つの理由があります。 最初は基本的なレジスタを使用することで、それらがどのように動作するかを理解できるようにするためです。 2番目はBIOSのSWI割り込み待機を使用すると、libnds ルーチンに関する微妙な問題があるということです。

'libnds' のデフォルトの割り込みハンドラのコードは以下の通りです。

void irqDefaultHandler(void)
{
	int i = 0;

	for (i = 0; i < 32; i++)
	{
		if(IF & (1 << i) )irqTable[i]();
	}
	
	VBLANK_INTR_WAIT_FLAGS = IF | IE;
}

'VBLANK_INTR_WAIT_FLAGS' の割り当てに注目してください。 これには、IE レジスタの値とIF レジスタの値の or を代入しています。 IE レジスタはハンドルしたいすべての割り込みのビットマスクだということを思い出してください。 したがって、どんな割り込みにたいしても、このコードは BIOS の SWI 割り込み待機がすべての割り込みをハンドリングしたと通知するでしょう。

この結果、VBIが発生し、キー割り込みが発生していない場合でも、BIOS はキー割り込みが発生したと判断し、私達の 'swiWaitForKeys' ルーチンにから出るでしょう。 libnds コードは以下のようにするべきでしょう。

  VBLANK_INTR_WAIT_FLAGS |= IF;

もしあなたが libnds のデフォルトの割り込みハンドラを使用する場合には、あなたは残念ながらVBI割り込み以外の割り込みに対してBIOS の割り込み待機ルーチンを使用して応答することができないでしょう。

IF と VBLANK_INTR_WAIT_FLAGS の設定

IF と VBLANK_INTR_WAIT_FLAGS を設定するとき注意すべきことがいくつかあります。 もし、ハンドラで発生したすべての割り込みを処理したということを通知するために、'IF' を VBLANK_INTR_WAIT_FLAGS に割り当てる場合は、ハンドラは以下の順に設定する必要があります。

  VBLANK_INTR_WAIT_FLAGS = IF;  
  IF = IF;

'IF' への書き込みはそれをリセットします、したがって、最初に IF に書き込み、それから VBLANK_INTR_WAIT_FLAGS に IF を書き込むと、 あなたは間違った値を VBLANK_INTR_WAIT_FLAGS に書き込むことになります。その結果、BIOS の SWI 割り込み待機は多分ハングするでしょう。

メイン ループ

キー割り込みを待つのを示すために、メインループでは私達の 'swiWaitForKeys' ルーチンを使用します。

  while(1) {
    swiWaitForKeys();
    ++swi_return_count;
  }

その呼び出しから戻ったとき、垂直ブランク割り込み(VBI)期間中に2番目のスクリーンに表示するカウンタを増加させます。 これはいつSWI呼び出しから戻ったかを表示しています。

以前使用したメインループの大半は 'swiWaitForVBlank' でした。これは1秒間に60回呼び出されます。 実際にメインループでは何もしないので、いつVBIから戻ってくるかについて気にかけません。 その代わりにキー割り込みで待機しています。キーが押されたときにだけ、これが発生します。おそらく数秒に1回でしょう。

2番目のスクリーン

このデモはキーが押された回数やSWI呼び出しの結果のデバック情報を表示するために2番目のスクリーンを使用します。 これは以下のコードで使用のための設定をしています。

  videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE);
  vramSetBankC(VRAM_C_SUB_BG);
  SUB_BG0_CR = BG_MAP_BASE(31);
  BG_PALETTE_SUB[255] = RGB15(31,31,31);
  consoleInitDefault((u16*)SCREEN_BASE_BLOCK_SUB(31), (u16*)CHAR_BASE_BLOCK_SUB(0), 16);

これはコンソール使用のための他のスクリーン設定コード(例えば チュートリアル1)と非常に似ています。 異なるのは、私達が 'videoSetMode' の代わりに 'videoSetModeSub' を使用している点です。サブスクリーンは2番目のスクリーンです。 あなたは、さまざまなレジスタが 'MAIN' バージョンの代わりに 'SUB' バージョンを使用している事に気づくでしょう。 私は今後のチュートリアルで2番目のスクリーンの設定及び使用方法に関してカバーするつもりですが、ここでは基本的な考え方についてのみ解説しました。

サンプルアプリケーションの使用方法

私が徹底的にテストしたサンプルアプリケーションは Interrupts_demo1 と呼ばれます。 同時に'A' と 'B' ボタンを押すと、キー割り込みのトリガとなります。この割り込みはそれが起きたことを示すために2番目のスクリーンに表示するカウンタを増加します。 また、割り込みが起きると割り込みからの復帰を待機する BIOSルーチンを呼び出し、2番目のスクリーンに表示される2番目のカウンタを増加します。

これが起きると、小さな赤い四角がスクリーンを横切り、下に行きます。 ARM9 でのプログラムの実行の大半は低消費電力モードです。割り込み中だけ ARM9 は本当にアクティブです。

デモのビルド

完全なサンプルプログラムは 'interrupts_demo1'です。 ARM9 のコードは arm9_main.cppにあります。 これは チュートリアル1 と同様に、情報を表示するためにコンソールルーチンを使用します。 ARM7 のコードは arm7_main.cpp にあります。すべてをビルドするための Makefile ファイルです。

完全なソースコードは Interrupts_demo1.zip で、エミュレータまたは実機で動作する Interrupts_demo1.ndsInterrupts_demo1.nds.gba をダウンロードできます。DSEmu 0.4.4 またはそれ以上のバージョンではこのデモを問題なく実行できます。

結論

このチュートリアルではどのように割り込みと効果的に割り込みを待機するためのBIOS呼び出しについて解説しました。 ここで示したアプローチはバッテリーの寿命を長くするでしょう。そして、ARM9 がいつオフにされるかをビジーループで監視しているエミュレータは、効果的にCPUリソースを使用できるようになり効果的です。

将来のチュートリアルではもっと割り込みを使用するでしょう。それが今回割り込みを扱おうと決めた理由です。 ただし、キー割り込みがしばしば使われるわけではありません。それは FIFO、IPCおよびタイマーを使用すことを学ぶ方が重要です。

いつものように、いろいろなコメントや提案を歓迎します。 以下の私の連絡先を見てください。