🦁

【電子工作】割り込み処理(ソフトウェア割り込み)

2022/12/18に公開

micropython(pymakr + RP2で動作確認)

  • コールバック関数は引数を取る必要があります。この引数はTimerオブジェクトです。
        ※ もし引数が不要な場合は_でもつけときましょう。
サンプルコード

5秒毎に繰り返し実行されます

from machine import Timer

def repeatCallback(payload):
  print(payload) # Timer(mode=PERIODIC, period=268685764, tick_hz=1000000)

Timer(period=5000, callback=repeatCallback)

Arduino(platformio + esp32で動作確認)

delayの副作用

  • 初心者向きでは、loop()の中で一定時間の間隔を取る場合(1秒おきや10分おきなど)、delay()を使っているかもしれません。
  • delay()で処理を停止している間は完全にCPUが停止しており、他の処理を受け付けることができません。そのため、もしdelay()で停止している間にボタン入力があったとしても、ボタン入力には反応できません
  • そこでタイマー割り込みを使い、一定時間ごとに特定の関数を実行するというやり方をとります。

最小構成のタイマー割り込み

サンプルコード

1秒ごとにLEDが点滅するプログラムです。

#define LED_PIN 10

// 1. タイマー宣言
hw_timer_t * timer = NULL;

// 6. タイマー呼び出しされるコールバック関数
void IRAM_ATTR onTimer() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  // 2. タイマー作成。0番のタイマーを80クロックで1カウント、カウントアップ
  timer = timerBegin(0, 80, true);
  // 3. 割り込みの設定。第一引数は設定するタイマー、第二引数は割り込み関数
  timerAttachInterrupt(timer, &onTimer, true);
  // 4. トリガー条件を設定。timerが1000,000カウントごとに割り込みを発生させる
  timerAlarmWrite(timer, 1000000, true);
  // 5. タイマーを有効化
  timerAlarmEnable(timer);
}

void loop() {
}

解説

  1. タイマー作成
    timerBeginの引数の型は以下の通りです。
hw_timer_t *timerBegin(uint8_t timer, uint16_t divider, bool countUp)

timerBegin()でタイマーを作成しています。

  • 1つ目timerはタイマーのIDです。ESP32はタイマーが4つあり、0-3までのタイマーを利用できます。
  • 2つ目dividerは何クロックでカウントをするかの数値になります。80の場合、80クロックで1カウントします(esp32は80クロックが1秒に相当します)。getApbFrequency()/1000000で、どのCPU周波数でも1マイクロ秒に固定化することができます。
  • 3つ目countはカウントアップする場合にはtrue。カウントダウンする場合にはfalseになります。通常はtrueのみしか使わないと思います。

戻り値は、hw_timer_t *型の値です。この戻り値を後から使います。

  1. timerAttachInterrupt()
void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(), bool edge)

タイマー割り込みが発生したときに実行する関数を登録します。

  • 1つめのtimerは、hw_timer_t型を当てはめます。hw_timer_t型とはtimerBegin()で返却された値です。
  • 2つ目fnには割り込み時に呼ぶ関数(コールバック関数)を当てはめます。この関数にはIRAM_ATTR属性を付与する必要があるのですが、これについては後述します。
  • 3つ目edgeは割り込み検知方法です。trueがエッジタイプ、falseがレベルタイプです。タイマーの場合にはカウントアップしていって、指定カウントに変わったところを検知するので、エッジトリガーのtrueを指定します。
  1. timerAlarmWrite()
void timerAlarmWrite(hw_timer_t *timer, uint64_t interruptAt, bool autoreload)

割り込みが発生したときの、トリガー条件を設定します。

  • 1つ目のtimerは設定するタイマー(hw_timer_t型)で、timerBegin()で返却された値です。
  • 2つ目interruptAtはカウント数。timerAttachInterrupt()で登録した関数を呼び出すまでの期間で、timerBegin()で設定した分周後の周期が単位となります。この例では、timerBeginで80クロックで1カウントしましたので、1カウントは1μ秒です。1,000,000カウントごと、つまり1秒ごとに割り込み関数を呼び出すよう設定しています。
  • 3つ目autoreloadは、trueの場合には定期実行、falseの場合には1ショットの実行になります。
  1. タイマーを有効化
timerAlarmEnable(timer);
  • 引数のtimer: hw_timer_t型はtimerBegin()で返却された値です。
  • timerAlarmDisable()という関数で停止させることもできます。
  1. タイマー呼び出しされるコールバック関数
void IRAM_ATTR onTimer() {

コールバック関数にはIRAM_ATTRを追加します。
(追加しなくても動作するみたいですが、基本はつけてください)

このオプションは関数を高速なIRAM上に読み込みことを保証させるもののようです。このオプションが無いと、フラッシュ上に配置される可能性があり、その場合には低速動作になり、結果的にクラッシュする可能性があるみたいです。

割り込み処理は別ファイルに分割

割り込み処理が長くなってしまう場合は、別ファイルに分割したほうがスッキリします

サンプルコード
main.cpp
#include <Arduino.h>
#include <onTimer.h>

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 500000, true);
  timerAlarmEnable(timer);
}

void loop() {
}

onTimer.cpp
#define LED_PIN 17

hw_timer_t * timer = NULL;

void IRAM_ATTR onTimer() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

割り込み関数でグローバル変数を書き換える

割り込み関数の外に設定した変数(グローバル変数)の値を書き換える方法です

サンプルコード

ここではtimeCounter1というグローバル変数を設定しています

#include <Arduino.h>

#define OnB_LED 2
boolean togLED = false;

// 1. グローバル変数の準備
volatile int timeCounter1;

hw_timer_t *timer1 = NULL; 
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

void IRAM_ATTR onTimer1(){
  // 2. 変数更新時の排他処理
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

/****************** Interrupt handler *******************/

void RoutineWork()
{
  Serial.print("interrupted   ");
  Serial.print("togLED = ");
  Serial.println(togLED);

  if (togLED == true) {
    digitalWrite(OnB_LED, LOW);
    togLED = false;
  }else{
    digitalWrite(OnB_LED, HIGH);
    togLED = true;
  }
}

void setup() {
  pinMode(OnB_LED, OUTPUT);
  Serial.begin(115200);
  timer1 = timerBegin(0, 80, true); // タイマーの初期化
  timerAttachInterrupt(timer1, &onTimer1, true); // 割り込み処理関数の設定
  timerAlarmWrite(timer1, 1000000, true); // タイマー動作間隔を設定
  timerAlarmEnable(timer1); //タイマーを開始

  Serial.println("Start!");
}

void loop() {
}
  1. グローバル変数(タイマー割り込み処理内で更新する変数)の設定
volatile int timeCounter1;

グローバル変数にはvolatileを定義します。
volatileとは変数を配置するアドレスを固定するためのもので、これを定義しとかないと外部参照した時に値が不安定になるそうです。

  1. タイマー割り込み関数 変数更新時の排他処理
void IRAM_ATTR onTimer1(){
  portENTER_CRITICAL_ISR(&timerMux); // 排他的処理開始
  timeCounter1++; 
  portEXIT_CRITICAL_ISR(&timerMux); // 排他的処理終了
}

グローバル変数を更新している間は他の処理を実行しなくするよう、更新する前後にportENTER_CRITICAL_ISR と portEXIT_CRITICAL_ISR を設定します。
データベースのトランズアクションみたいなもの?

参考

https://lang-ship.com/blog/work/esp32-timer/
https://garretlab.web.fc2.com/arduino/lab/timer/

Discussion