ARM Cortex-M0 での割込サービスルーチン(ISR)の秘密

投稿者: | 2015年3月4日

We don’t need so-called ‘interrupt‘ modifier with an interrupt service routine function definition.

そうそう。一つだけ書き忘れてました。

マイクロプロセッサによっては、C 言語の関数定義で割込サービスルーチン(ISR、あるいは割込ハンドラ)を書こうとすると、特殊な宣言が必要なものがあります。例えば TI C6000 DSP では、割込ベクタから直接呼び出される関数を C で書く場合には、interrupt 修飾子が必要になります。例えば、こんな具合です。(余談: DSP/BIOS, SYS/BIOS 等の OS で、割込ディスパッチャという仕組を使う場合は、OS が ISR にラッパを被せてくれるので interrupt 修飾子は不要です。)

interrupt void an_isr(void)
{
	// something to do
}

結論から言うと、このような宣言は ARM Cortex-M0 では不要です。簡単に言えば、第一の理由は「ISR の中で破壊される可能性のあるレジスタ等は全てハードウェアで待避されるから」であり、第二は「割込から通常のリターン命令で戻っても、ハードウェアが割込からのリターンと認識できるから」です。

もう少し詳しく見てみましょう。

マイクロプロセッサのコードを C 言語で書く場合、関数呼出しの前後におけるレジスタ待避にはルールが設けられています。例えば TI C6000 DSP では、A0〜A31、B0〜B31 という汎用レジスタがありますが、これらの待避は

  • [AB]0〜9: 関数を呼び出す側(parent)が待避
  • [AB]10〜15: 呼び出された関数(child)が待避
  • [AB]16〜31: 関数を呼び出す側(parent)が待避

というルールになっています。例えばある関数がレジスタ [AB]10〜15 を使っていなければ、その関数の出入り口では一切の汎用レジスタを待避する必要がない、ということになります。

ところが C6000 DSP で ISR を C言語で書く場合には事情が異なってきます。それは、割込の発生時にどのような実行コンテキストで、どのようなレジスタが使われているか分からないからです。そのため、interrupt 修飾子なしで ISR を書いてしまうと、[AB]0〜9 および [AB]16〜31 が ISR 内部で破壊されてしまう可能性があります。(Cコンパイラが関数をアセンブリコードを生成するときに、関数の呼び出し元がそれらを待避してくれると期待しているため。)

もう一つの理由ですが、C6000 DSP では通常関数のリターンに対して、割込からのリターンで B IRP という専用のニーモニックを実行しなくてはならないことがあります。(IRP とは、Interrupt Return Pointer の略です。) この命令を実行しないと、プロセッサは割込コンテキストからの復帰をと認識できないのです。

話を ARM Cortex-M0 に戻します。繰り返しになりますが、このような interrupt 修飾子は Cortex-M0 では不要です。その理由の一つは、必要なレジスタ待避をハードウェアで実現しているためです。実際には、ARM Cortex-M0 の register convention では次のように定義されています。(The Definitive Guide to the ARM Cortex-M0 の初版ではこれを記述した表に誤植があります。)

  • R0〜R3, R12: 関数を呼び出す側(parent)が待避
  • R4〜R11: 呼び出された関数(child)が待避

ここで、C6000 DSP と同様の仕組であれば、ISR では R0〜R3, R12 を追加で待避しなくてはいけないのですが、Cortex-M0 では割込時に、ハードウェアで R0〜R3, R12 を自動的に待避する仕組があるため、通常の関数コードを ISR として利用することができるのです。

もう一つの理由ですが、ARM Cortex-M0 では割込リターンに「通常のリターン命令」を使っても、それを「割込からのリターン」だとハードウェアが認識できる仕組があります。その仕掛ですが、「割込からのリターン」を命令やレジスタで区別しているのではなく、リターン先として格納される「アドレス値」によって区別するというアイデアです。

Cortex-M0 では通常、関数呼出し時のリターンアドレスを R14 (別名 LR) に入れて関数のエントリにジャンプしますが、割込発生時には、ハードウェアが R14 に EXC_RETURN と呼ばれる特殊なビットパターンを格納します。この値は通常のリターンアドレスと区別できるようになっているため(具体的には最下位ビットが 1 になっている。Cortex-M0 には 8ビット幅命令はないので、リターン先アドレスの最下位ビットが 1 になることはない)、ハードウェアはこのリターンが割込コンテキストからの復帰だと理解できる、という訳です。

まとめますと、このような 2つの理由により ARM Cortex-M0 のコンパイラ(あるいは register convention)では、割込サービスルーチンの C 言語での定義において、特殊なキーワードでコンパイラに情報を与える必要はない、ということになります。つまり、C言語で書いたどのような関数も、割込サービスルーチンとして利用することができるのです。

おしまい。

追記

ちなみに ARMCC(コンパイラ)は interrupt という修飾子を理解しません。これはまだ ISO 非標準のようです。ARMCC で伝統的な ARM (ARM7TDMI とか) のための ISR を書くためには __irq という修飾子があります。一方 GCC ARM だと、__attribute__((interrupt("IRQ"))) とか書くようです。

Cortex-M0 ではこれらの修飾子は不要ですが、書いても無害のようです。ISR であることを強調するために __irq などと書くのは良い習慣だと思います。ただし、割込サービスルーチンの適切な実装というのはアーキテクチャやプラットフォームにかなり依存しますので、アプリケーションの他のコードと分けて設計および実装したほうが良いように思います。