●Arduinoで割り込みを使う

Arduinoでは、ほぼ完全にハードウエアに近い部分は見えなくなっており、
割り込みベクターやレジスタをスタックに退避するなど、細かい事は考える必要が無く、非常に簡単になっています。
しかし、内部処理に割り込みが使用されている関数を割り込み処理の中で使うとうまく動作しないため注意が必要です。

millisやdelayのように、一定の時間経過を扱う関数や、シリアル通信などの外部の通信を行う関数には内部に割り込み処理が使われているため、
割り込み処理の中で使用するとうまく動作しない可能性があります。


▼割り込みピンの設定

使用可能な割り込みピンは、Arduino Megaボード以外のほとんどのボードでは デジタルピン2番3番が使用可能と思われます。
利用するボードにより違うかもしれませんので事前に調べる必要があります。

デジタルピン2番に割り込みを設定するには、

attachInterrupt(digitalPinToInterrupt(2),呼び出される関数,FALLING);

このように呼び出す必要があります。
一つ目の引数には、digitalPinToInterrupt(2)が内部に入っていますが、それはハードウエアの隠蔽が少々漏れており、
Arduinoでのピン番号とハードウエアの割り込み番号が実際には違うため、digitalPinToInterrupt(2)によりデジタルピン2番の番号をハードウエアのint0に変換しています。
二つ目の引数では、割り込み時に呼び出される関数を指定します。
三つ目の引数では、割り込みが発生するトリガーを指定します。

LOW     LOWのとき発生
CHANGE  状態が変化したときに発生
RISING  LOWからHIGHに変わったときに発生
FALLING HIGHからLOWに変わったときに発生


▼割り込みの無効化

有効にした割り込みを無効にしたい場合には、

detachInterrupt(digitalPinToInterrupt(2));

このようにしてデジタルピン2番の割り込みを無効にします。


▼割り込み禁止

noInterrupts();//割り込み停止
interrupts();//割り込み開始

割り込み停止と開始をセットで使うことにより割り込み禁止箇所を指定する事が出来ます。
interrupts() と noInterrupts() は、マクロで読み替えられていて、AVRを使ったArduinoだと、sei() cli()に変換されます。


▼volatile修飾子

割り込み関数の中と外で使用する変数には、コンパイラの最適化防止のために、volatile修飾子を取り付けています。
volatile修飾子を取り付けないと誤った動作をする可能性があります。


▼サンプルプログラム

■次のプログラムでは2番3番のピンに取り付けたスイッチを交互に押すことにより、基板上のLEDのON・OFFを行います。
二つのスイッチを接続するピンは内部プルアップを指定しており、GNDとデジタルピン2番、GNDとデジタルピン3番の間にスイッチを取り付けています。

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(2),func_on,FALLING);
  attachInterrupt(digitalPinToInterrupt(3),func_off,FALLING);
}

volatile int i;

void loop() {
  digitalWrite(LED_BUILTIN,i);
}

void func_on(){
  noInterrupts();
  i=1;
  interrupts();
}
void func_off(){
  noInterrupts();
  i=0;
  interrupts();
}

デジタルピン2番3番に取り付けたスイッチを交互に押すことによりLEDが点灯・消灯を繰り返します。




■割り込みにより待機状態を作る

▼禁断の割り込みハンドラ内での一時停止

通常一定時間プログラムを停止するにはdelayを使いますが、割り込みハンドラ内では一瞬で抜けてしまい止まりません。
そのため、割り込みハンドラ内でも動作するdelayMicrosecondsを使用します。

次のプログラムはデジタルピン2番の割り込みにより、500ミリ秒停止してLEDをON・OFFします。
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(2, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(2),func_on,FALLING);
}

void loop() {
}

void func_on(){
  digitalWrite(LED_BUILTIN,1);
  for(int j=0;j<500;j++){
    delayMicroseconds(1000);
  }
  digitalWrite(LED_BUILTIN,0);
}


■多重割り込み

Arduinoではデフォルトで多重割り込みが出来ません。
というか、学習用マイコンのため多重割り込みの問題をなるべく隠蔽して簡単にする目的があるからだと思いますが、
プログラムを複数書いて色々推測した所、割り込みハンドラがコールされた時点で割り込み禁止状態になっているようです。

いわゆる、割り込み関数の最初に noInterrupts() がコールされた状態からスタートしているのではないかと思われます。
そのため、割り込み関数の中で、interrupts() を呼ぶとその時点から多重割り込みが出来る状態になります。

多重割り込みを許可すると、今まで使えなかったdelay関数も正常に動作するようになりました。
ただ、割り込みの中で割り込みが起きるため、重い処理を行ったりすると、一つの割り込み関数が何重にも呼び出されたりとか、
割り込みによりスタックが消費されてしまうなど、隠蔽されていた難しい所が表に出てきてしまいます。

隠すのがいいのか表に出すのがいいのかArduinoを開発している人たちは難しい判断をしてるのでしょうね。


■バグを出さないために

割り込みのプログラムが大きくなるとほんとに収集が付かなくなります。
割り込みに問題がある状態で実際に走らせると、いつおかしな動作が起きるのか予測不能なプログラムになり、
目でおってデバックなどほぼ不可能で、専用ツールを使って可能性をすべて洗い出し一つ一つ確認していくなど、物凄いエネルギーを消費することになります。

その状態を避けるため、処理の優先順位を決めて、割り込まれたら困る処理では事前に割り込み禁止を行い、変化したら困る変数は最初にローカル変数に確保する。
といった操作が必要になってきます。
まあ、それでも問題は起こるのですが、優先順位でどこまであきらめるかだと思います。


▼割り込み禁止

Arduinoでは最初は多重割り込み禁止になっていますが許可した場合には、

noInterrupts();
interrupts();

割り込み禁止と割り込み許可で重要な処理ははさむ必要があると思います。
ただ、その間の割り込みは無視されるのでそれなりにリスクはあります。


▼関数の最初で必要な変数を確保する

次のプログラムはデジタルピン2番の割り込みにより、500ミリ秒停止してLEDをON・OFFします
割り込みハンドラの外ではdelayが使えますが、次のプログラムでは void loop() 実行中に動作の要となる変数が変化してしまい、
値の一貫性が取れず実際にうまく動作しません。
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(2, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(2),func_on,FALLING);
}

volatile int i;

void loop() {
  digitalWrite(LED_BUILTIN,i);
  if(i){
    i=0;
    delay(500);
  }
}

void func_on(){
  i=1;
}

このプログラムを正常に動作させるには、
関数の動作に必要で、割り込みにより変化する可能性のある変数を関数の一番最初に確保する必要があります。
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(2, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(2),func_on,FALLING);
}

volatile int i;

void loop() {
  int pos=i;
  digitalWrite(LED_BUILTIN,pos);
  if(pos){
    i=0;
    delay(500);
  }
}

void func_on(){
  i=1;
}

割り込みにより変化する可能性のある変数 i をローカル変数 pos に確保することにより正常に動作します。


▼リード・モディファイ・ライト問題

プログラムは最終的にマシン語に変換されて、CPUで実行されます、CPU命令と内部の処理は基本的に リード・モディファイ・ライト動作( データを読む→読んだデータを処理する→データを書く)の繰り返しであり、 その処理の間に割り込みが入ると意図しない動作となります。

例えば、カウントアップする変数をリセットする割り込みで、count++ などのカウントアップ動作(メモリの値を読む→値を+1する→メモリに書き込む)の途中で 割り込みによるリセットをかけてもリセット後にメモリに書き込む動作が発生する可能性があるためうまく動いたり動かなかったりします。
この場合は処理の優先度を考えて割り込み禁止ではさんで回避します。

うーん、割り込みは難しいですね。


▲トップページ > マイコンなど