概要
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系の関数がありますので後日調べてみたいと思います。
コメント