SNSへはこちら

FT90マイコンを使ってみよう(7・終) - 割り込みの仕様とか

今回で最終回にしたいと思います。最後に、割り込みに関してと雑多なことを述べて〆たいと思います。

割り込み

あらゆるマイコンをいじる上で地味に面倒な、割り込み処理と割り込みルーチンの登録。これについて述べます。
ただ、ブラックボックス化されている事が多いので、ふんわりとした理解になってしまっているのはご了承を。

GCC に頼った割り込み

ライブラリを導入しましたよね。その中に割り込みルーチンを登録する関数(interrupt_attach)があるのですが、具体的な中身を見てみたんですよ。するとエラー処理を省いたりした中身はこんな感じ。

int8_t interrupt_attach(interrupt_t interrupt, uint8_t priority, isrptr_t func)
{
...
    vector_table[priority] = func;
    INTERRUPT->IRQ[interrupt] = priority;
...
}

ここで、仮引数の interrupt は割り込み番号(整数値)であり、func は 戻り値 void, 引数 void の関数ポインタです。

どうやら vector_table というイカニモな配列と、割り込みに関するレジスタへ代入しているようです。
後者は良いとして、前者はどうなっているのでしょうか。調べるとこんな感じになっています。

#if defined(__GNUC__)
/* Soft ISR Vector table as defined in crt0.S */
extern isrptr_t vector_table[N_INTERRUPTS];
/* Dummy function as defined in crt0.S */
extern void nullvector(void);
#elif defined(__MIKROC_PRO_FOR_FT90x__)
/* Default interrupt handler. */
void nullvector(void) {}
/* Soft ISR Vector table as defined in crt0.S */
isrptr_t vector_table[N_INTERRUPTS] = { nullvector,
nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector,
nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector,
nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector,
nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector, nullvector,
};
/* Map interrupt handlers to vector table. */
void isr1(void) iv IVT_WDT_IRQ                 { vector_table[1](); }
void isr2(void) iv IVT_PM_IRQ                  { vector_table[2](); }
void isr3(void) iv IVT_EHCI_IRQ                { vector_table[interrupt_usb_host](); }
...
#endif

ごちゃごちゃ書いてありますが、gcc を使う場合は __GNUC__ マクロが定義されますから、上部の #if#elif 直前までの部分がビルドされるわけです。これだけじゃよく分かりませんよね。よく見るとコメントで、

/* Soft ISR Vector table as defined in crt0.S */

と書かれています。つまり、gcc にどっぷり依存した動作となっているわけですねえ。

これまでに作ったバイナリを覗いてみると、確かに vector_table が見つかります。そしてその中身は全て同じ関数をコールしています。

00800000 <vector_table>:
  800000:   2c 03 00 00     0000032c jmpx 0,$r28,0,cb0 <STARTUP_DFU_powermanagement_ISR+0x14>
  800004:   2c 03 00 00     0000032c jmpx 0,$r28,0,cb0 <STARTUP_DFU_powermanagement_ISR+0x14>
...
  800084:   2c 03 00 00     0000032c jmpx 0,$r28,0,cb0 <STARTUP_DFU_powermanagement_ISR+0x14>

これがこの後に説明する割り込みのためのベクタテーブルとなります。

割り込み時の挙動

ユーザーマニュアルによると、割り込み時はメモリ上の指定されたアドレスにジャンプするとのことです。つまりこれが本当のベクタテーブルです。上に示した vector_table はその後の挙動を司る、いわばサブ的な存在と思ってください。

例えばタイマー割り込み。この割り込み番号は 17 です。割り込み番号 x の割り込みでは、メモリの 0x08 + 4 * x 番地にジャンプすると決まっています。よってこの割り込みではアドレス 0x4C にジャンプします。そこには以下のような記述があります。

00000000 <_start>:
       0:   ff ff 30 00     0030ffff jmp 3fffc <EXITEXIT+0x20000>
       4:   ab 00 30 00     003000ab jmp 2ac <interrupt_33>
       8:   48 00 30 00     00300048 jmp 120 <interrupt_0>
       c:   4b 00 30 00     0030004b jmp 12c <interrupt_1>
...
      40:   72 00 30 00     00300072 jmp 1c8 <interrupt_14>
      44:   75 00 30 00     00300075 jmp 1d4 <interrupt_15>
      48:   78 00 30 00     00300078 jmp 1e0 <interrupt_16>
      4c:   7b 00 30 00     0030007b jmp 1ec <interrupt_17>
...

ということで、interrupt_17 に飛びます。ではそこに移って中身を見てみます。参考までに、interrupt_16 の逆アセンブル結果も示しておきました。

000001e0 <interrupt_16>:
     1e0:   00 00 00 84     84000000 push.l $r0
     1e4:   40 00 00 c4     c4000040 lda.l $r0,800040 <___ctors+0x40>
     1e8:   ae 00 30 00     003000ae jmp 2b8 <interrupt_common>

000001ec <interrupt_17>:
     1ec:   00 00 00 84     84000000 push.l $r0
     1f0:   44 00 00 c4     c4000044 lda.l $r0,800044 <___ctors+0x44>
     1f4:   ae 00 30 00     003000ae jmp 2b8 <interrupt_common>

何やら r0 レジスタに 800044 という値を入れていますね。これは先程上で見た vector_table です。つまりここまではアドレス等完全固定で、ソフト的に割り込みルーチンを書き換えることを想定しているというわけですね。実際、interrupt_attach では vector_table を書き換える挙動をしていましたね。
その後 interruput_common にジャンプしますが、ここの挙動はなんとなく察しが付きます。見てみましょう。

000002b8 <interrupt_common>:
     2b8:   00 80 00 84     84008000 push.l $r1
     2bc:   00 00 01 84     84010000 push.l $r2
     2c0:   00 80 01 84     84018000 push.l $r3
     2c4:   00 00 02 84     84020000 push.l $r4
     2c8:   00 80 02 84     84028000 push.l $r5
     2cc:   00 00 03 84     84030000 push.l $r6
     2d0:   00 80 03 84     84038000 push.l $r7
     2d4:   00 00 04 84     84040000 push.l $r8
     2d8:   00 80 04 84     84048000 push.l $r9
     2dc:   00 00 05 84     84050000 push.l $r10
     2e0:   00 80 05 84     84058000 push.l $r11
     2e4:   00 00 06 84     84060000 push.l $r12
     2e8:   00 00 0f 84     840f0000 push.l $r30
     2ec:   00 00 34 08     08340000 calli $r0
     2f0:   00 00 e0 8d     8de00000 pop.l $r30
     2f4:   00 00 c0 8c     8cc00000 pop.l $r12
     2f8:   00 00 b0 8c     8cb00000 pop.l $r11
     2fc:   00 00 a0 8c     8ca00000 pop.l $r10
     300:   00 00 90 8c     8c900000 pop.l $r9
     304:   00 00 80 8c     8c800000 pop.l $r8
     308:   00 00 70 8c     8c700000 pop.l $r7
     30c:   00 00 60 8c     8c600000 pop.l $r6
     310:   00 00 50 8c     8c500000 pop.l $r5
     314:   00 00 40 8c     8c400000 pop.l $r4
     318:   00 00 30 8c     8c300000 pop.l $r3
     31c:   00 00 20 8c     8c200000 pop.l $r2
     320:   00 00 10 8c     8c100000 pop.l $r1
     324:   00 00 00 8c     8c000000 pop.l $r0
     328:   00 00 00 a4     a4000000 reti

長ったらしいですが、やっていることは単純で次の処理をしています。

  • r0 を除く全レジスタの値をスタックに待避
  • r0 に格納されていた値をアドレスとして関数コール
    • vector_table を差していましたね
    • vector_table では、interrupt_attach によって書き換えられた値(ユーザーの指定した割り込みルーチン)をコールする
  • 全レジスタを復帰
  • reti 命令を呼ぶ

なるほど、これで挙動がわかりました。要するに回りくどいことをしていますが結局関数を呼んでいるだけだということです。そして最後に reti を実行して割り込み状態から抜けると言う感じでしょうかね。

でも何故こんな回りくどいやり方を...

実はこの gcc、__attribute__((interrupt)) などという割り込み属性の指定が通用しません。無理やり記述しても ignore されます。

もし対応していたら、レジスタの退避の話や reti の実行が完全コンパイラ任せで出来たでしょう(割り込み属性を付けない通常の関数だとただの return を呼ぶことになっていつまで経っても割り込みから復帰できない)。
こういう理由があって、このような回りくどい構成にしているのだと思われます。取り敢えずアセンブリで事前に レジスタ待避・関数コール・レジスタ復帰・ reti 実行の雛形を書いてしまって、後からコールする関数を当てはめるという感じにしたのですね。
でも何で attibute を使わない方針にしたのかな...gcc の言語依存が嫌だったのか??

実際の割り込み登録

要は interrupt_attach を呼ぶだけなのですが、実用上はグローバル割り込みも有効化する必要があります。なので一例を出しておきますね。

#include <ft900.h>

void timerISR(void) {
    GPIO->GPIO00_31_VAL ^= (1 << 13);
    TIMER->TIMER_INT |= 1 << 0;
}

void timer_irq_init(void) {
    INTERRUPT->IRQ_CTRL = 1 << 7; // enable global interrupt
    interrupt_attach(17, 0, timerISR);

    TIMER->TIMER_CONTROL_0 = 1 << 1; // block enabled
    TIMER->TIMER_CONTROL_2 = 1 << 4; // enable preslacer for timerA
    TIMER->TIMER_CONTROL_3 = 0 << 4; // down count for A
    TIMER->TIMER_INT = 1 << 1; // enable IRQ for A
    TIMER->TIMER_PRESC_LS = 50000 & 0xFF;
    TIMER->TIMER_PRESC_MS = 50000 >> 8; // 100MHz -> 2kHz
    TIMER->TIMER_WRITE_LS = (2000 - 1) & 0xFF;
    TIMER->TIMER_WRITE_MS = (2000 - 1) >> 8;

    TIMER->TIMER_CONTROL_1 = 1 << 0; // start A
}

その他雑多

delayms と delayus

CPU クロックに応じて、いい感じにディレイをやってくれるユーティリティです。整数で値を入れます。
引数型は 32bit 整数なので超えないように注意。

asm類

以前の記事でリバースエンジニアリングをしましたが、中でも memset とか memcpy とか面白い CPU 命令が有りました。これらの命令を呼ぶ asm volatile 的なアレがマクロとしてライブラリ中に存在しましたので列挙しておきます。

ただ、このマクロ関数の引数は C 言語のそれとは異なることに注意です。せっかくCっぽい命令なんだから合わせてくれ!とは思いますが。

#define asm_memcpy8(src, dst, size) MEMCPY_B(src, dst, size)
#define asm_memcpy16(src, dst, size) MEMCPY_S(src, dst, size)
#define asm_memcpy32(src, dst, size) MEMCPY_L(src, dst, size)

#define asm_memset8(val, dst, size) MEMSET_B(val, dst, size)
#define asm_memset16(val, dst, size) MEMSET_S(val, dst, size)
#define asm_memset32(val, dst, size) MEMSET_L(val, dst, size)

#define asm_strcpy(src, dst) STRCPY_B(src, dst)
などなど...

これらは ft900_asm.h にありますのでチェキしてみてください。