Arduinoの割り込み処理の関数をアセンブラ化する実験

UNO-ATmega328

Arduinoでのinterrupts()noInterrupts()関数は、割り込みの制御を行うために使用されます。これらの関数は特にタイミングが重要なタスクを処理する際に重宝します。
この関数をアセンブラで実装した場合のプログラムサイズについて実験してみました。

標準関数による処理

この関数は割り込みを有効にします。Arduinoが起動すると、割り込みはデフォルトで有効ですが、noInterrupts()によって無効にされた後は、このinterrupts()関数を使って再び有効にする必要があります。

interrupts()

この関数は割り込みを有効にします。Arduinoが起動すると、割り込みはデフォルトで有効ですが、noInterrupts()によって無効にされた後は、このinterrupts()関数を使って再び有効にする必要があります。

noInterrupts()

この関数は割り込みを無効にします。これにより、割り込みによって中断されることなく、コードの特定のセクションを安全に実行することができます。

サンプルプログラム

以下のサンプルプログラムは、外部割り込みを使ってLEDの点灯/消灯を制御するものです。外部ボタンが押されるたびにLEDの状態が変わります。

volatile bool ledState = false; // LEDの状態を格納する変数(割り込みで使用するためvolatileを指定)

void setup() {
  pinMode(2, INPUT_PULLUP); // ボタンのピン(ここでは2番ピン)を入力に設定、内部プルアップ抵抗を有効化
  pinMode(13, OUTPUT); // LEDのピン(ここでは13番ピン)を出力に設定
  attachInterrupt(digitalPinToInterrupt(2), toggleLED, FALLING); // 2番ピンに対する割り込みを設定、ボタンが押された時に反応(FALLINGエッジ)
}

void loop() {
  // ループ内での特別な処理は不要
}

void toggleLED() {
  noInterrupts(); // 割り込みを無効にして、割り込み処理中に他の割り込みが入らないようにする
  ledState = !ledState; // LEDの状態を切り替え
  digitalWrite(13, ledState); // 新しいLEDの状態をピンに書き込む
  interrupts(); // 割り込みを再び有効にする
}

最大32256バイトのフラッシュメモリのうち、スケッチが1016バイト(3%)を使っています。

このプログラムでは、noInterrupts()interrupts()toggleLED()関数内で使っています。これは、LEDの状態を切り替える間、他の割り込みが発生しないようにするためです。割り込みを無効にすることで、LEDの状態が切り替わるプロセスが安全に行われます。割り込みを再び有効にすることで、その他の割り込みも正常に処理できるようになります。

高速化に挑戦

Arduino標準のinterrrupts/noInterruptsと同じ機能で、実行速度が上がる新しい関数をアセンブラ使って書き直してみます。interrrupts/noInterrupts関数はハードウェアの割り込みを無効にする関数ですが、Arduinoコアライブラリのオーバーヘッドによりメモリー消費と実行時間が大きくなる傾向にあります。これに対して、ATmega328Pに限定して直接アセンブリ命令を利用することで、このオーバーヘッドを極力省くことを試みました。具体的には、AVRマイクロコントローラのSEI(割り込み許可)とCLI(割り込み不許可)命令を直接コードに埋め込みます。これにより関数呼び出しのコストを削減し、さらにオーバーヘッドも排除します。

AVRベースのArduinoでnoInterrupts()interrupts()をインラインアセンブラで書いて、割り込み制御を行うサンプルプログラムを再構築しましょう。以下のコードは、割り込みをインラインアセンブラで制御し、外部ボタンを使用してLEDの状態を切り替える方法を示しています。

インラインアセンブラを用いた割り込み制御関数

// 割り込みを無効にする関数
  void inline_noInterrupts() {
  asm volatile ("cli"); // cli命令で割り込みを無効にする
}

// 割り込みを有効にする関数
  void inline_interrupts() {
  asm volatile ("sei"); // sei命令で割り込みを有効にする
}
volatile bool ledState = false; // LEDの状態を格納する変数(割り込みで使用するためvolatileを指定)

void setup() {
  pinMode(2, INPUT_PULLUP); // ボタンのピン(ここでは2番ピン)を入力に設定、内部プルアップ抵抗を有効化
  pinMode(13, OUTPUT); // LEDのピン(ここでは13番ピン)を出力に設定
  attachInterrupt(digitalPinToInterrupt(2), toggleLED, FALLING); // 2番ピンに対する割り込みを設定、ボタンが押された時に反応(FALLINGエッジ)
}

void loop() {
  // ループ内での特別な処理は不要
}

void toggleLED() {
  inline_noInterrupts(); // 割り込みをインラインアセンブラで無効にする
  ledState = !ledState; // LEDの状態を切り替え
  digitalWrite(13, ledState); // 新しいLEDの状態をピンに書き込む
  inline_interrupts(); // 割り込みをインラインアセンブラで再び有効にする
}

最大32256バイトのフラッシュメモリのうち、スケッチが1016バイト(3%)を使っています。

このコードでは、noInterrupts()interrupts()をそれぞれinline_noInterrupts()inline_interrupts()に置き換えています。これにより、割り込み制御がアセンブリ言語で直接行われ、割り込みの管理がより直接的に行われます。これは割り込みのタイミングや挙動が極めて重要なアプリケーションにおいて有用です。

結果

どちらのサイズの1016バイトの消費量で全く変わりがありませんでした。
その英雄を知りたいため、各関数の中を覗いてみました。

interrupts()関数の中を覗く

#define sei() __asm__ __volatile__ ("sei" ::: "memory")

void interrupts() {
  sei();
}

interrupts() 関数の内部実装を見てみると、この関数は AVR ベースの Arduino プラットフォームにおいて、実際にはとてもシンプルなものです。この関数は通常、インラインアセンブリを使用して、Global Interrupt Enable (GIE) ビットをセットするだけです。このビットは AVR のステータスレジスタ(SREG)の 7番目のビットです。Arduino のオープンソースライブラリのソースコードを見ると、上記のように interrupts() 関数は実装されています(AVR マイクロコントローラの例)

この sei() マクロはアセンブリの sei 命令を使用して、Global Interrupt Enable (GIE) ビットをセットし、これにより割り込みを全体的に有効にします。__volatile__ キーワードはコンパイラに対して、この命令を最適化や移動を行わないように指示します。"memory" バリアはコンパイラに対して、この命令前後でメモリの読み書きの再順序を行わないように指示します。

これは非常に効率的な方法で、割り込みシステムの有効化を行います。ARMベースのマイクロコントローラや他のアーキテクチャを使用するArduinoプラットフォームでは、実装が異なる可能性があります。各アーキテクチャにおける具体的な命令セットやハードウェア特性に基づいた適切な方法で割り込みが有効または無効にされます。

nointerrupts()関数の中を覗く

#define cli() __asm__ __volatile__ ("cli" ::: "memory")
void noInterrupts() {
  cli();
}

noInterrupts() 関数は AVR ベースの Arduino プラットフォームで、割り込みを無効にするために使用されます。この関数も interrupts() 関数と同様に、インラインアセンブリを使用して特定の割り込み制御ビットを操作することで実装されています。

AVR マイクロコントローラにおける noInterrupts() 関数は、ステータスレジスタ(SREG)の Global Interrupt Enable (I) ビットをクリアすることで割り込みを無効にします。このビットは SREG の7番目のビットです。

Arduino のソースコードでは、上記のように noInterrupts() が実装されています。

この cli() マクロはアセンブリの cli 命令を使用して、Global Interrupt Enable (I) ビットをクリアし、これにより割り込みを全体的に無効にします。ここで使用される __volatile__ キーワードはコンパイラにこの命令を最適化しないよう指示しますし、"memory" バリアはメモリ操作の順序がこの命令によって影響を受けないことを保証します。

このような実装は、プログラム内で割り込みを正確に制御するために非常に重要です。特に、データの整合性を保つためや、特定の操作が割り込みによって中断されないようにする場合に用います。他のプロセッサアーキテクチャにおいても同様の機能がありますが、具体的な命令や実装は異なる場合があります。

 

コメント

タイトルとURLをコピーしました