Arduino 公式フォーラムの “Is there a non blocking delay that’s as easy to use as the built in delay function?” で見つけた「邪道」なコードを掘り下げてみました。
例えば複数のタスクを異なる周期で起動する、次のようなスケッチがあるとします。
これを次のように書けたら、少し嬉しくありませんか?
DO_EVERY()
の正体は、次のようなマクロです。
このマクロを展開すると、次のようなコードになります。
for()
文の構造 for (初期化式; 条件式; 変化式) { 繰り返し処理; }
に当てはめれば、冒頭のコードとの違いは、次の通りとなります。
-
uint32_t previousMillis1 = 0;
などが static uint32_t prev = 0;
に置き換わる
-
if()
文による条件式が for()
文中の条件式に置き換わる
- 変化式の
prev = currentMillis
実行後に、条件式が繰り返される「ムダ」がある
特に最後の「ムダ」という一点でアレルギー反応を起こす人もいるかと思いますが、「邪道」と言われようが私は全然 OK で、むしろ読み易さの点において積極的に使いたい派です
ノンプリエンプティブ・マルチタスクの注意点
ウィキペディアの マルチタスク によれば、「ノンプリエンプティブ・マルチタスク」は次のように説明されています。
各タスク自身が、短い時間間隔でOSに処理を返す方式によって実現されているものを、ノンプリエンプティブなマルチタスク、協調的マルチタスクという。
この「短い時間間隔で」というのがミソです。この手の例題に用いられる digitalWrite()
や analogWrite()
で LED を点滅させたりサーボモータを駆動したりする、ナノ秒オーダーで完了する処理なら問題とはならないでしょう。
しかしサンプリング周期一定が好ましいフィルタリング処理(タイマ割り込みを使わない前提)で、浮動小数点演算でミリ秒オーダーの時間がかかったりすると、その周期性に問題が出始めます。
試しに冒頭のコードにおいて、各タスクの起動周期をそれぞれ 110msec、120msec、130msec に、処理時間が 1msec、2msec、3msec かかると想定した次のようなコードで、実際の周期性を観測してみます。
各タスクの処理時間がミリ秒単位でかかる場合の周期性を確認するコード
観測結果
次のグラフは、ExecTask()
の出力を 1 秒から 10 秒まで Arduino UNO R4 Minima で観測した結果です。明らかにタスク2はタスク1の、タスク3はタスク1と2の影響を受け、それぞれ ±1msec、±2msec のブレが観測されます。
もちろんこれがアプリケーションの性能に影響を与えなければ問題はありませんが、あちこちで紹介されているサンプルコードでは、こういう問題が生じ得るということは認識しておいた方が良いでしょう。
原因と改善策
タスクの周期性にブレが生じる原因は、各タスクの起動を判定する基準時刻を loop()
先頭の uint32_t currentMillis = millis();
で決めていることにあります。そこで、各タスクごとに基準時刻を更新することにします。
各タスクの処理時間がミリ秒単位でかかる場合の周期性を確認するコード(改善版)
もっと長い時間を観測すればブレが出てくる可能性はありますが、効果は「アリ」です。
改善版のマクロ
もちろん、この改善版にも「邪道」なマクロはあります。
どこで宣言されているかパッと見分からない変数が出てくる時点で、強いアレルギー反応を起こす人が続出かと思いますが、僕的には「アリ」なネタでした
オススメ文献
2014年11月とかなり古い文献なのでよくご存知の方もいると思いますが、Arduino でのマルチタスクについての優れた文献を紹介しておきます。複数のタスクを統制するための状態遷移やオブジェクト指向なコードなど、Blink Without Delay からさらにステップアップするのに最適と思います。
またこれらを短くまとめた「Arduinoのマルチタスクについて」も合わせて紹介しておきたいと思います。