今回で最終回にしたいと思います。最後に、割り込みに関してと雑多なことを述べて〆たいと思います。
割り込み
あらゆるマイコンをいじる上で地味に面倒な、割り込み処理と割り込みルーチンの登録。これについて述べます。
ただ、ブラックボックス化されている事が多いので、ふんわりとした理解になってしまっているのはご了承を。
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
にありますのでチェキしてみてください。