ベアメタルRaspberry Piの例外処理の書き方

前回はRPiベアメタル版MicroPythonにPinクラスを追加しましたが、実装したのは入出力ができるだけの最低限の機能でした。
今後、GPIO周りの様々な機能を実装していくには、まず先に割り込みを実装しておくほうが良さそうです。
データ転送や並列処理は割り込みがあると無いとでは大違いなので、割り込み無しで実装しても、割り込み機能を追加したら結局大幅に書き直しになりそうだからです。

というわけで、インターフェース誌2017年2月号の割り込みの解説を読んでみました。

昔のイメージだと、割り込みというのは以下のような感じでした。

・アドレス空間の0番地または最高番地(最近は少ないですが、昔だと6502はそうでした)に割り込みベクタがある
・割り込みベクタはジャンプ先アドレスの一覧(ジャンプテーブル)になっており、割り込み種別に応じてジャンプテーブルのアドレスがプログラムカウンタ(PC)に読み込まれる
・レジスタの設定で割り込みの禁止・許可ができる

上記のインターフェース誌の記事を読んだ結果、以下のようなことが分かりました。

・ARMの割り込みベクタは0番地から置くか、あるいは任意のアドレスに置くことが可能(CP15コプロセッサのレジスタに設定)。サイズは32bit×8個
・割り込みベクタはアドレスではなく命令を書く(分岐命令など)。ジャンプ先等を表すオペランドは最大24bitという制限あり
・ARMで割り込みを使うには、CPUの動作モードを割り込みモード/高速割り込みモード/ユーザモードのいずれかにする必要がある(起動時はスーパーバイザモード)
・動作モードが変わるとスタックポインタも切り替わる
・Raspberry PiではARMの標準とは異なる割り込み制御が行われている(GPUと共有されているなど)
・割り込み要因を正確に知るには、割り込みハンドラの中でさらに関連レジスタの値を調べる必要がある

GitHubの公開されているコードで、Raspberry Piのベアメタルで割り込みを使っている例もいくつか見つかりました。
ベアメタルではおなじみのdwelch67さんのblinker05は、タイマ割り込みでLEDを点滅させます。

raspberrypi/blinker05 at master · dwelch67/raspberrypi

この例では、割り込みハンドラ(void c_irq_handler ( void ))がGPIOをオン・オフしています。
割り込みハンドラの設定はvectors.sの中で行われていますが、ここでちょっとトリッキーなことをしています。

ベアメタルのプログラムはブートローダによって0x8000へロードされますが、その0x8000に割り込みベクタが置かれています。
割り込みベクタは、上で書いたように、ジャンプ先アドレスではなくジャンプ命令が書かれています。
割り込みベクタの直後に32bit×8個のアドレステーブルがあり、ジャンプ命令はこのアドレステーブルに記載されたアドレスへジャンプするように書かれています。

プログラムが起動される0x8000に書かれている命令は、割り込みベクタの先頭=リセットベクタであり、その内容は

ldr pc,reset_handler

となっています。reset_handlerはリセットハンドラのアドレスです。
つまり、ブート直後に必ずリセットハンドラへ飛ぶようになっています。

リセットハンドラの先頭は

reset:
mov r0,#0x8000
mov r1,#0x0000
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}
ldmia r0!,{r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!,{r2,r3,r4,r5,r6,r7,r8,r9}

となっています。このコードは0x8000からの64バイト(割り込みベクタとジャンプテーブル)を0x0000へコピーしています。

これによって、割り込みベクタが初期化されますが、リセット処理で実行される内容は0x8000からブートされたときとリセットで0x0000から実行されたときで、全く同じになります。

ただ、このコードはr2を壊してしまっているので、このままだと以前紹介したARM Boot Tagsは使えません。

ジャンプテーブルのIRQハンドラは、以下のコードを指しており、この中からCの関数(c_irq_handler)が呼ばれるようになっています。

irq:
push {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
bl c_irq_handler
pop  {r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
subs pc,lr,#4

最後のPC←LR-4という処理ですが、割り込みが起こったときには、CPUは次に実行する命令の処理に取り掛かっています。割り込み処理後に実行を再開するときには、処理途中だった命令を再度やり直す必要がありますので、プログラムカウンタを1命令だけ巻き戻しています。

ちなみに、Raspberry PiではタイマーがCPU側に1つとGPU側に4つの2系統存在し、このサンプルではCPU側のタイマーを使用しています。

もう一つの例としてmrvnさんのものを見ていきます。「006-you-are-exceptional」で未定義命令などの例外処理を行っています。

mrvn/RaspberryPi-baremetal: Baremetal exampels for Raspberry Pis

この例では、割り込みベクタを0番地ではなく、別のアドレスに設定しています。この場合は、割り込みベクタを指定するためにCP15コプロセッサのVBAR (Vector Base Address Register)を使います。
その処理はexceptions.cの中の以下のコードで行われています。

void set_vbar(uint32_t *base) {
asm volatile ("mcr p15, 0, %[base], c12, c0, 0"
:: [base] "r" (base));
}

void exceptions_init(void) {
set_vbar(exception_vector);
}

このexception_vectorは、entry.Sの中でブランチ命令として以下のように定義されています。

exception_vector:
b   stub_reset
b   stub_undef
b   stub_svc
b   stub_prefetch_abort
b   stub_data_abort
b   stub_hypervisor_trap
b   stub_irq
b   stub_fiq

この実装ではジャンプテーブルは無く、直接stub_xxxというアドレスへ分岐するようになっています。
この分岐先からさらに、Cで書かれた割り込みハンドラ「handler_xxx」を呼び出すようになっています。

handlerを呼び出すためのstubのコードはentry.Sの中でマクロとして記述されています。

.macro  make, num, offset, name
.global stub_\name
stub_\name:
saveall \num, \offset
bl  handler_\name
restoreall
.endm

// num, offset, name
make 0, 0, reset
make 1, 4, undef
make 2, 0, svc
make 3, 4, prefetch_abort
make 4, 8, data_abort
make 5, 0, hypervisor_trap
make 6, 4, irq
make 7, 4, fiq

これによって、例えば

    .global stub_irq
stub_irq:
saveall 6, 4
bl  handler_irq
restoreall

といったコードが生成されます。
saveallとrestoreallもマクロで、numは割り込み要因、offsetは戻りアドレスの補正値です。

コメント