2025/05/25 電子書籍「Markdown記法大全+Pandoc変換マスター: 初心者から脱却!HTML・PDF・電子書籍を自在に出力する方法」を出版

ESP32のタイマー割り込み

ESP32

Arduino Core for ESP32の3.0系で、Timerは、新しいESP-IDFドライバーを使用するようにリファクタリングされています。

タイマー関係の関数の仕様が変更になっています。この記事では、その変更内容について説明します。

主な変更点

「Migration from 2.x to 3.0」に記述されている変更点です。V2.のタイマー関数については、「PlatformIOではじめるM5Stackプログラミング「入門編」」内の「8-2. タイマー処理」の項も参考にしてください。

各関数(API)についての説明は、こちらを参照してください。

削除された関数

  • timerGetConfig
  • timerSetConfig
  • timerSetDivider
  • timerSetCountUp
  • timerSetAutoReload
  • timerGetDivider
  • timerGetCountUp
  • timerGetAutoReload
  • timerAlarmEnable
  • timerAlarmDisable
  • timerAlarmWrite
  • timerAlarmEnabled
  • timerAlarmRead
  • timerAlarmReadMicros
  • timerAlarmReadSeconds
  • timerAttachInterruptFlag

新しいAPI

以下は、新しく追加されたAPIです。

API説明
timerAlarmタイマーのアラームを設定し、自動的に有効化します。timerAlarmWrite と timerAlarmEnable を統合したものです。
timerGetFrequencyタイマーの実際の周波数を取得します。
timerAttachInterruptArg引数を指定して、タイマーに割り込みハンドラーを登録します。

変更されたAPI

以下の2つの関数は、引数が簡略化されています。

API説明
timerBegin引数が1つ(周波数)のみに変更されました。指定した周波数を実現するため、異なるクロックソースを使用して分周器が自動的に計算されます。
timerAttachInterrupt引数が2つのみに変更されました。edge パラメーターは削除されま

コード例

以下、Timer で公開されているサンプルコードです。

  1. Repeat timer example
/*
 Repeat timer example

 This example shows how to use hardware timer in ESP32. The timer calls onTimer
 function every second. The timer can be stopped with button attached to PIN 0
 (IO0).

 This example code is in the public domain.
 */

#include <Arduino.h>

// Stop button is attached to PIN 0 (IO0)
#define BTN_STOP_ALARM 0

hw_timer_t *timer = NULL;
volatile SemaphoreHandle_t timerSemaphore;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

volatile uint32_t isrCounter = 0;
volatile uint32_t lastIsrAt = 0;

void ARDUINO_ISR_ATTR onTimer() {
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  isrCounter = isrCounter + 1;
  lastIsrAt = millis();
  portEXIT_CRITICAL_ISR(&timerMux);
  // Give a semaphore that we can check in the loop
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
  // It is safe to use digitalRead/Write here if you want to toggle an output
}

void setup() {
  Serial.begin(115200);

  // Set BTN_STOP_ALARM to input mode
  pinMode(BTN_STOP_ALARM, INPUT_PULLUP);

  // Create semaphore to inform us when the timer has fired
  timerSemaphore = xSemaphoreCreateBinary();

  // Set timer frequency to 1Mhz
  timer = timerBegin(1000000);

  // Attach onTimer function to our timer.
  timerAttachInterrupt(timer, &onTimer);

  // Set alarm to call onTimer function every second (value in microseconds).
  // Repeat the alarm (third parameter) with unlimited count = 0 (fourth parameter).
  timerAlarm(timer, 1000000, true, 0);
}

void loop() {
  // If Timer has fired
  if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE) {
    uint32_t isrCount = 0, isrTime = 0;
    // Read the interrupt count and time
    portENTER_CRITICAL(&timerMux);
    isrCount = isrCounter;
    isrTime = lastIsrAt;
    portEXIT_CRITICAL(&timerMux);
    // Print it
    Serial.print("onTimer no. ");
    Serial.print(isrCount);
    Serial.print(" at ");
    Serial.print(isrTime);
    Serial.println(" ms");
  }
  // If button is pressed
  if (digitalRead(BTN_STOP_ALARM) == LOW) {
    // If timer is still running
    if (timer) {
      // Stop and free timer
      timerEnd(timer);
      timer = NULL;
    }
  }
}
  • Watchdog timer example
#include <Arduino.h>
#include "esp_system.h"
#include "rom/ets_sys.h"

const int button = 0;         //gpio to use to trigger delay
const int wdtTimeout = 3000;  //time in ms to trigger the watchdog
hw_timer_t *timer = NULL;

void ARDUINO_ISR_ATTR resetModule() {
  ets_printf("reboot\n");
  esp_restart();
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("running setup");

  pinMode(button, INPUT_PULLUP);                   //init control pin
  timer = timerBegin(1000000);                     //timer 1Mhz resolution
  timerAttachInterrupt(timer, &resetModule);       //attach callback
  timerAlarm(timer, wdtTimeout * 1000, false, 0);  //set time in us
}

void loop() {
  Serial.println("running main loop");

  timerWrite(timer, 0);  //reset timer (feed watchdog)
  long loopTime = millis();
  //while button is pressed, delay up to 3 seconds to trigger the timer
  while (!digitalRead(button)) {
    Serial.println("button pressed");
    delay(500);
  }
  delay(1000);  //simulate work
  loopTime = millis() - loopTime;

  Serial.print("loop time is = ");
  Serial.println(loopTime);  //should be under 3000
}

2.0系&3.0系で使用可能なタイマー割り込み

以下は、ESP_ARDUINO_VERSION による条件コンパイルを使用し、2.0系と3.0系の両方に対応したタイマー割り込み関数のサンプルです。

関数一覧

関数説明
void ARDUINO_ISR_ATTR onTimer()異タイマー割り込み
void InitTimerInt(uint64_t TimerInterval)タイマー割り込みの初期化関数
TimerInterval:タイマー割り込み間隔(ms)

ソースコード

  • TimerInt.cpp
#include <Arduino.h>
#include "TimerInt.h"

hw_timer_t *timer		= NULL;
volatile SemaphoreHandle_t timerSemaphore;
portMUX_TYPE timerMux	= portMUX_INITIALIZER_UNLOCKED;

volatile uint32_t isrCounter    = TIMER_INTERVAL;
volatile int32_t vibCounter    = -1;
volatile bool fTimer            = false;

void ARDUINO_ISR_ATTR onTimer() 
{
	// Increment the counter and set the time of ISR
	portENTER_CRITICAL_ISR(&timerMux);
	if(isrCounter != 0)
	{
	    isrCounter	-= 1;
	}
    if(vibCounter > 0)
    {
        vibCounter -= 1;
    }
	portEXIT_CRITICAL_ISR(&timerMux);
	// Give a semaphore that we can check in the loop
	xSemaphoreGiveFromISR(timerSemaphore, NULL);
	// It is safe to use digitalRead/Write here if you want to toggle an output
}

void InitTimerInt(uint64_t TimerInterval)
{
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
	// Create semaphore to inform us when the timer has fired
	timerSemaphore	= xSemaphoreCreateBinary();
	timer = timerBegin(1000000);			// Set timer frequency to 1Mhz
	timerAttachInterrupt(timer, &onTimer);	// Attach onTimer function to our timer.
	// Set alarm to call onTimer function every second (value in microseconds).
	// Repeat the alarm (third parameter) with unlimited count = 0 (fourth parameter).
	timerAlarm(timer, TimerInterval * 1000, true, 0);
#else
	// Create semaphore to inform us when the timer has fired
	timerSemaphore	= xSemaphoreCreateBinary();
	timer			= timerBegin(0, 80, true);			// タイマー番号:0 を使用
	timerAttachInterrupt(timer, &onTimer, true);		// Attach onTimer function to our timer.
	timerAlarmWrite(timer, TimerInterval * 1000, true);	// 10ms: 10 *1000us
	timerAlarmEnable(timer);							// Start an alarm
#endif
	fTimer			= true;
}
  • TimerInt.h
#ifndef Timer_INT_H
#define Timer_INT_H

#define TIMER_INTERVAL  20

extern hw_timer_t * timer;
extern volatile SemaphoreHandle_t timerSemaphore;
extern portMUX_TYPE timerMux;

extern volatile uint32_t isrCounter;
extern volatile int32_t vibCounter;
extern volatile bool fTimer;

void InitTimerInt(uint64_t TimerInterval);

#endif
  • main.cpp

Core2に対応したタイマー割り込み関数のサンプルコードです。

#include <Arduino.h>

#include <M5Unified.h>
#include "TimerInt.h"

const uint64_t TimerIntInterval	= 10;

void setup()
{
	auto cfg = M5.config();			// 設定用の構造体を取得。
	M5.begin(cfg);					// M5Unifiedを使用する準備をする。

	InitTimerInt(TimerIntInterval);	// タイマー割込のセットアップ
}

void loop()
{
    static bool fLED    = false;
    M5.update();                    // ボタン操作・タッチ操作の状態を更新する。

    // If Timer has fired
    if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE)
    {
        static uint32_t Count	= 0;
        uint32_t isrCount		= 0;
        // Read the interrupt count and time
        portENTER_CRITICAL(&timerMux);
        isrCount = isrCounter;
        portEXIT_CRITICAL(&timerMux);

        if(isrCount == 0)   // 1sec(10ms *100)毎に実行される
        {
            Count++;
            if(Count == 5)
            {
                Count   = 0;
                M5.Lcd.println("Timer 1sec");
            }
            M5.Power.setLed(fLED *128);
            fLED    = !fLED;
            
            portENTER_CRITICAL(&timerMux);
            isrCounter  = TIMER_INTERVAL;
            portEXIT_CRITICAL(&timerMux);
        }
    }

    if(M5.BtnA.wasClicked())
    {
        // if (fTimer) // Stop and free timer
        // {
        //     timerStop(timer);
        //     fTimer      = false;
        // } else {
        //     timerStart(timer);
        //     fTimer      = true;
        // }
		// -----------------------------------------
		vibCounter	= 10;
		M5.Power.setVibration(200);
    }
	portENTER_CRITICAL(&timerMux);
	if(vibCounter == 0)
	{
		vibCounter	= -1;
		M5.Power.setVibration(0);	// 停止
	}
	portEXIT_CRITICAL(&timerMux);

    M5.delay(1);
}

サンプルコードについて

主な機能

以下、サンプルコードの主な機能です。

  1. isrCounterを使用して、1秒毎に、”Timer 1sec”を画面に表示します。また、M5.Lcd.println関数の代わりにSerial.println関数を使用することで、シリアルモニターに表示する事ができます。
  2. 200ms毎に、電源用LED(緑)をON/OFFしています。
  3. vibCounter変数を使用して、ボタンAが押された時に200ms バイブレーションをONします。M5.Power.setVibration(0);で、バイブレーションをOFF出来ますが、残念ながら onTimer関数内では実行できない為、vibCounter変数を使用してメインループ内で実行しています。

割り込みの停止、再スタート

サンプルコードではコメントアウトしていますが、以下の関数を使用することで、タイマー割り込みの停止、再スタートが可能です。

  • 停止
    • timerStop(timer);
  • 再スタート
    • timerStart(timer);

タイマー割り込みの注意点

「PlatformIOではじめるM5Stackプログラミング「入門編」」内でも説明していますが、タイマー割込ルーティン(ハンドラ: ISR )を実装する際には、リアルタイム性や安定動作のためにいくつか重要な注意点がいくつかあります。

  1. 処理は最小限にする
    • ISR は非常に短時間で終了する必要があります。
    • 長時間ブロックすると、他の割り込み処理やシステム動作に悪影響が出ます。
    避けるべき処理例:
    • delay() (使えません)
    • Serial.print() (使わないでください)
    • 複雑な計算やループ処理
  2. 使用可能な関数は限られている。
    • ISR 内では “IRAM_ATTR“ 属性付きで定義された関数や変数しか安全ではありません。
    • フラッシュからの読み込みは禁止(割り込み中は一部のコード / データがフラッシュからロードできないためクラッシュの原因になります)。
  3. グローバル変数を使うときは volatile 修飾子を使う
    • ISR 内で読み書きする変数は volatile を付けて定義してください。
    • コンパイラによる最適化での意図しない動作を防ぎます。
  4. 割込の再入に注意
    • タイマー割込が再入( nested )されないように、重い処理はメインループなどで行うようにします。
    • ISR 内ではフラグだけ立てて、実際の処理は loop() 側で行うのが安全です。
  5. 割込の優先度と他の割込との競合
    • 高頻度で割込を発生させすぎると、他の割込処理やタスクが実行できなくなることがります。
    • 特に Wi-Fi 使用時は注意が必要です(タイミングがずれると通信が切れたりする)。

「Arduino Core for ESP32の3.0系について」の記事も参照ください。

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