ESP32のタイマークラス Tickerを調べる

概要

ESP32でタイマー処理を行うクラスのTickerを調べてみました。

タイマー追加の内部処理

Arduino15\packages\esp32\hardware\esp32\1.0.4\libraries\Ticker\src\Ticker.cpp

void Ticker::_attach_ms(uint32_t milliseconds, bool repeat, callback_with_arg_t callback, uint32_t arg) {
  esp_timer_create_args_t _timerConfig;
  _timerConfig.arg = reinterpret_cast<void*>(arg);
  _timerConfig.callback = callback;
  _timerConfig.dispatch_method = ESP_TIMER_TASK;
  _timerConfig.name = "Ticker";
  if (_timer) {
    esp_timer_stop(_timer);
    esp_timer_delete(_timer);
  }
  esp_timer_create(&_timerConfig, &_timer);
  if (repeat) {
    esp_timer_start_periodic(_timer, milliseconds * 1000ULL);
  } else {
    esp_timer_start_once(_timer, milliseconds * 1000ULL);
  }
}

タイマー追加はすべて、上記関数へのラッパーでした。

構造体に設定をつめて、esp_timer_create()関数を呼び出してから、単発だったらesp_timer_start_once()関数、リピートの場合にはesp_timer_start_periodic()関数を呼び出しています。

esp_timer_系関数はマイクロ秒単位で引数を取りますが、Tickerクラスはミリ秒単位で管理しているようです。

タイマー停止の内部処理

Arduino15\packages\esp32\hardware\esp32\1.0.4\libraries\Ticker\src\Ticker.cpp

void Ticker::detach() {
  if (_timer) {
    esp_timer_stop(_timer);
    esp_timer_delete(_timer);
    _timer = nullptr;
  }
}

停止時にはesp_timer_stop()してからesp_timer_delete()しているだけでした。

ラッパークラス 単発系(once)

単発実行は以下の4関数あります。

  • void once(float seconds, callback_t callback)
  • void once(float seconds, void (*callback)(TArg), TArg arg)
  • void once_ms(uint32_t milliseconds, callback_t callback)
  • void once_ms(uint32_t milliseconds, void (*callback)(TArg), TArg arg)

_msとついているのは受け取ったミリ秒をそのまま渡して、ついていないものは秒で受け取ったものを1000倍してミリ秒にしてから_attach_ms()を呼び出しています。

基本的には_ms系だけ使うほうがいい気がします。

あとはコールバック関数と、コールバック関数用の変数を渡しています。変数無しの関数は0を渡していました。

  template<typename TArg>
  void once(float seconds, void (*callback)(TArg), TArg arg)
  {
    static_assert(sizeof(TArg) <= sizeof(uint32_t), "attach() callback argument size must be <= 4 bytes");
    uint32_t arg32 = (uint32_t)(arg);
    _attach_ms(seconds * 1000, false, reinterpret_cast<callback_with_arg_t>(callback), arg32);
  }
  template<typename TArg>
  void once_ms(uint32_t milliseconds, void (*callback)(TArg), TArg arg)
  {
    static_assert(sizeof(TArg) <= sizeof(uint32_t), "attach_ms() callback argument size must be <= 4 bytes");
    uint32_t arg32 = (uint32_t)(arg);
    _attach_ms(milliseconds, false, reinterpret_cast<callback_with_arg_t>(callback), arg32);
  }

複数のタイマーを同じコールバック関数で動かしたい場合には、変数で区別するのが便利そうです。用途別に別のコールバック関数を準備するのであれば変数はいらないと思います。

スケッチ例を見てみる

Ticker/Blinker

#include <Arduino.h>
#include <Ticker.h>
// attach a LED to pPIO 21
#define LED_PIN 21
Ticker blinker;
Ticker toggler;
Ticker changer;
float blinkerPace = 0.1;  //seconds
const float togglePeriod = 5; //seconds
void change() {
  blinkerPace = 0.5;
}
void blink() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void toggle() {
  static bool isBlinking = false;
  if (isBlinking) {
    blinker.detach();
    isBlinking = false;
  }
  else {
    blinker.attach(blinkerPace, blink);
    isBlinking = true;
  }
  digitalWrite(LED_PIN, LOW);  //make sure LED on on after toggling (pin LOW = led ON)
}
void setup() {
  pinMode(LED_PIN, OUTPUT);
  toggler.attach(togglePeriod, toggle);
  changer.once(30, change);
}
void loop() {
  
}

サンプルスケッチはLEDがGPIO21に接続されている想定ですので、各自環境に合わせて書き換えてください。私はM5StickCで動かしたので10に変更しました。(M5StickCはLOWにすると光るので点滅が逆転します)

このスケッチには3つのタイマークラスが登場しています。

Ticker toggler;

toggler.attach(togglePeriod, toggle);

上記で設定されています。togglePeriodは5秒なので、5秒間隔でtoggle()関数を呼び出しています。

Ticker changer;

  changer.once(30, change);

上記で設定されています。30秒後に一度だけchange()関数を呼び出しています。

Ticker blinker;

    blinker.attach(blinkerPace, blink);

上記で設定されています。blinkerPaceは当初0.1秒なので、0.1秒間隔でblink()関数を呼び出しています。

    blinker.detach();

また、5秒間隔で呼ばれるtoggle()関数の中でタイマーの無効化も呼ばれています。

スケッチの動き

5秒間隔で動くblinkerタイマーにより、Lチカするblinkerタイマーの起動と停止を交互に実行することで、5秒間隔で当初0.1秒間隔のLチカと、LOWのモードを切り替えています。

changerタイマーが30秒後に1度実行され、その中でLチカする間隔を制御するblinkerPace変数を0.1秒から0.5秒に書き換えています。

そのため30秒後からは0.5秒間隔でLチカするのと、LOWのモードを切り替える動きになります。

ちょっと詰め込みすぎなスケッチ例ですね。

Ticker/Arguments

#include <Arduino.h>
#include <Ticker.h>
// attach a LED to GPIO 21
#define LED_PIN 21
Ticker tickerSetHigh;
Ticker tickerSetLow;
void setPin(int state) {
  digitalWrite(LED_PIN, state);
}
void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(1, LOW);
  
  // every 25 ms, call setPin(0) 
  tickerSetLow.attach_ms(25, setPin, 0);
  
  // every 26 ms, call setPin(1)
  tickerSetHigh.attach_ms(26, setPin, 1);
}
void loop() {
}

引数ありバージョンのスケッチ例です。こちらの方がシンプルなスケッチですが、動かしてみないとよくわからないと思います。こちらもLEDの接続しているGPIOに書き換えてください。

digitalWrite(1, LOW);の行は1じゃなくてLED_PINな気もします。

タイマーは2つあり、25ミリ秒間隔でsetPin(0)を呼び出すタイマーと、26ミリ秒間隔でsetPin(1)を呼び出すタイマーです。

この2つの間隔が違うタイマーでLEDをON/OFFしているので、周期がずれた点滅を繰り返すと思います。

備考

このタイマーで利用されているesp_timer_系の関数は、精度が悪いようです。

上記スライドか、以下の書籍にて詳しく紹介されています。

ざっくりと説明すると、Wi-Fi接続などの優先度の高い処理が動くと、タイマー処理が遅延して実行されるとのことです。

ただし、esp_timer_系のタイマーは本来はマイクロ秒単位の精度ですが、Tickerクラスはミリ秒単位で動作しています。上記実験では最大12ミリ秒程度の遅延がありましたので、その程度の遅延が発生しても問題がない用途であれば、Tickerクラスを利用しても問題はないと思います。

まとめ

0.1秒以上のタイマーであれば、十分な精度があるように思います。ミリ秒単位での遅延が問題になる場合には、別のtimer系の関数がありますので後日調べてみたいと思います。

コメント