【Raspberry Pi Pico入門 – 12】タイマー割り込みをしてみる

2023年10月13日Raspberry Pi Pico入門,基本編Arduino,Raspberry Pi Pico

概要

今回もスターターキットのチュートリアルの内容から離れてしまいますが、タイマー割り込み機能を使ってみます。

マイコンでの割り込み処理とは、何かしらの出来事を引き金として実行中の処理を一旦ストップし、指定の処理を優先的に実行、その後ストップした処理を再開することです。日常生活でイメージするなら、「掃除をしている最中にインターホンが鳴ったので、掃除をいったん中止してお客さんの対応をして、その後掃除を途中から再開」といった処理になります。
何かしらの出来事は自分で設定するのですが、Arduinoでは「ボタンが押された」「一定時間が経過した」を使うことが多いです。今回は「一定時間が経過した」の方を試してみます。

割り込み処理はコントロールが難しく、はじめは予想外の動作をしてしまうことが多いです。次回以降の解説でも割り込み処理は極力入れないようにしますので、興味がなければ飛ばしても構いません。
ただ、自分でプログラムを書いてみたときに上から順番に処理する形では上手くいかない、と思った時に使ってみてください。

実行環境

IDE:Arduino IDE 2.2.1
MCU:Raspberry Pi Pico

回路

今回はRaspberry Pi Pico上のLEDを使うので回路は必要ありません。

コーディング

新しくスケッチを作成し、以下の内容をコピペしてください。

struct repeating_timer st_timer;
bool timer_flag = false;

/* タイマー割り込み処理 */
bool Timer(struct repeating_timer *t) {
  timer_flag = true;
  return true;
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  /* タイマーの初期化(割込み間隔はusで指定) */
  add_repeating_timer_us(1000000, Timer, NULL, &st_timer);
}

void loop() {
  /* timer_flagがtrueになる度にLEDをトグルする */
  if (timer_flag == true) {
    if (digitalRead(LED_BUILTIN) == 0) {
      digitalWrite(LED_BUILTIN, HIGH);
    } else {
      digitalWrite(LED_BUILTIN, LOW);
    }
    timer_flag = false;
  }
}

動作確認

LEDが1秒ごとにON/OFFすれば成功です。

解説

割り込み処理とは

概要でも少し説明しましたが、図を使って少し詳しく解説します。

割り込み処理をシーケンス図で表すと下図のようになります。
普段はloop()の中を無限ループして処理を実行しています。ここでタイマー割り込みを設定すると、設定した時間が経過したときにloop()内で何をしていようが一旦処理を中断し、Timer()にジャンプします。

Timer()の中身を1回実行したらloop()の中断した場所に戻ってきて処理を再開します。

タイマー割り込みについて

割り込み機能はマイコンが変わると使い方も大きく変わります。可能な限り統一化してあるArduino IDEでも使うマイコンによって使い方が変わります。今回のプログラムはRaspberry Pi Pico(厳密にはRP2040を使ったボード)でしか使えないので注意してください。

Arduinoではdelay()が使えるのでタイマー割り込みはいらないのでは?と感じたかもしれませんが、delay()を使って時間を測るとdelay()以外の処理をすると時間がズレてしまったり、delay()で待っている間は何も仕事が出来なくなってしまいます。
なので待ち時間に他の処理をしておきたい時や、正確に時間を測りたいときはタイマー割り込みが便利です。

初期設定

以下のプログラムを実行するとタイマーが起動して時間の計測が始まります。〇〇以外はライブラリ側で決められている形なので変更するとエラーになります。

struct repeating_timer 〇〇〇〇;
add_repeating_timer_us(〇〇〇〇, 〇〇〇〇, NULL, &〇〇〇〇);

第1引数にはどれだけの時間が経過すると割り込み処理を実行するかの時間を設定します。単位は[us](100万分の1秒)です。
第2引数には先ほど書いた割り込み処理の関数名を書きます。
第4引数には1行目のような形でインスタンスを生成して書きます。このインスタンスはグローバル変数で宣言してください。

慣れるまではサンプルコードをコピペして、add_repeating_timer_us()の1つ目の数字だけを変える使い方がおすすめです。(タイマーを二つ以上使うことはほとんどないので、サンプルコードをコピペして1つ準備すれば十分です)
例えば、下のようにサンプルコードから数字を一桁減らして1000000にすると、0.1秒ごとにLEDが点滅します。

  /* タイマーの初期化(割込み間隔はusで指定) */
  add_repeating_timer_us(100000, Timer, NULL, &st_timer);

タイマー割り込み時の処理内容の設定

先ずは一定時間が経過した時に実行する処理を書きます。以下の〇〇以外はライブラリ側で決められている形なので変更するとエラーになります。

bool 〇〇〇〇(struct repeating_timer *t) {
  〇〇〇〇〇〇〇;
  〇〇〇〇〇〇〇;
  〇〇〇〇〇〇〇;
  return true;
}

処理内容は計算やフラグを立てる等は可能ですが、シリアル通信等の一部の機能はストップしてしまっており使えないものもあります。これは通常時はloop()の裏で実行している処理や他の割り込み処理がストップしているからです。
その辺りのバグを減らすためには、割り込み処理内ではフラグを立てる処理だけ等、可能な限り処理を減らすのが効果的です。

割り込み処理に慣れるまでは、
・フラグ用変数を1にするだけの処理(定期的に何かを実行したいときに便利)
・カウントをするだけの処理(電源ONから〇〇秒後に実行したいとき等に便利)
等にしておくと変なバグが出にくいのでおすすめです。

/* フラグを1にする処理 */
bool timer_flag = false;

bool Timer(struct repeating_timer *t) {
  timer_flag = true;
  return true;
}
/* カウントの値を1つ増やす処理(10までカウントしたら0に戻る) */
const int TIMER_MAX = 10;
int timer_cnt = 0;

bool Timer(struct repeating_timer *t) {
  timer_cnt++;
  if (timer_cnt >= TIMER_MAX) {
    timer_cnt = 0;
  }
  return true;
}

処理Aを1秒ごとに、処理Bを10秒ごとに実行したい、というときは二つ目のようなカウントを使うと便利です。

loop()内の構造

通常時はtimer_flagがfalseなので何も実行せずにものすごい速度でグルグルと無限ループしています。1秒が経過するとこの無限ループを一旦ストップし、Timer()を実行します。Timer()を実行するとtimer_flagがtrueになり、loop()のストップした位置に戻ってきます。するとif文の中の処理が実行され、最後にtimer_flagをfalseにしているので無限ループに戻る、という形になっています。
処理が終わったらtimer_flagをfalseにしないとif文の中の処理を何度も実行してしまうので注意が必要です。

重要なのはtimer_flagをfalseにするタイミングです。if文の最初でやるか最後にやるかで意味が大きく変わってきます。
最初にやった場合、if文の中の処理の途中でtimer_flagがtrueに変わる可能性があります。処理が最後まで終わることよりも時間の正確さが重要な場合は最初にtimer_flagをfalseにして、処理の中でtimer_flagがtrueになっていないか監視してください。
逆に最後に置いた場合は、if文の処理中は常にtimer_flagがtrueになります。時間の正確さよりも処理を完了させることが重要な場合は最後に実行してください。