google-site-verification: google3bd66dd162ef54c7.html

FlexiTimer2の挙動の調査(Arduino)

 ちょっとやってみたいことがあるので、Arduinoのタイマー割込みライブラリであるFlexiTimer2の使い方を試していました。何をやりたいのかは、もう少し目処が立ってから記事にしたいと思いますが、Arduinoで二種類のタイマー割り込みを使った機能を作りたいと思っています。

 ところで、Arduinoのタイマー割り込みで有名なのはMsTimer2です。私もよく使っていて、だいぶ前の記事でこのタイマーの精度を調べたことがあります。MsTimer2は使い易いのですが、割込み間隔が1ms 単位しか設定出来ないので、これより細かいタイミングを作ることが出来ません。

 もっと細かい時間が設定出来るようにするために、MsTimer2の上位互換のライブラリとしてFlexiTimer2があり、割込み周期をより細かく指定出来るようになっています。関数の形式は下記で、

 FlexiTimer2::set(unsigned long units, double resolution, void (*f)());

 最初の引数(units)には時間を、二番目の引数(resolution)には時間の単位を指定することになっていて、

 例えば、FlexiTimer2::set(1, 1.0/3000, IRQ);

 とやれば1秒間に3000回の割り込みがかかります。これ、Arduino Playground のFlexiTimer2の解説に書いてある通りで、これ以外の制約について何も書かれていません。ならば、第二引数に 1.0/1000000 と入れると1μs単位の値が設定出来そうな気配です。

 とは言ってもArduinoの時間は4μs単位でカウントされているので、そのあたりが限界になる?それと、第二引数のタイプがdouble(倍精度浮動小数点)になっているけど、この型をArduinoで使うのはちょっと違和感があるなー。

 なんて思いながら動かしてみると、FlexiTimer2はとんでもない挙動を示しました。

▼オシロで波形を確認しながら
波形
 同時にユニバーサルカウンタでパルスの周期を測りました。

▼測定に使ったスケッチ
/* FlexiTimer2のテスト
* 引数を変えた時のFlexiTimer2の挙動調査スケッチ
*/

#include <FlexiTimer2.h>

void setup() {
pinMode(13, OUTPUT);
IntervalSet(0.000036); // FlexiTimer2の条件を変えて
}

void loop() {
}

void IntervalSet(float data) { // 引数をFlexiTimer2に設定
FlexiTimer2::stop();
FlexiTimer2::set(1, data, IRQ_timer2); // 第二引数の値を変えて測定
FlexiTimer2::start();
}

void IRQ_timer2() { // FlexiTimer2の割込み処理
// PORTB |= B00100000; // sbi
// PORTB &= B11011111; // cbi
digitalWrite(13, HIGH);
digitalWrite(13, LOW);
}
 このスケッチの9行目にいろいろな値を入れてパルスの周期を測定してみました。

▼測定結果
FlexiTimer2の特性
 これはびっくり。設定値に対して実際に出力される値が一致するのは 40~1000μsの範囲(0.00004~0.001)だけでした。50μsで誤差が多くなっているのは、時間の単位である4μsで割り切れないので、こういう結果になったのだと思います。ちなみに4μの倍数で値を設定すると誤差が少なくなり、24μsあたりが少ない誤差で設定出来る下限でした。

 時間が短い方はタイミング的に苦しくなるのでまあこういう結果になっても仕方ないと思います。でも意外だったのは時間が長い方です。1ms以上の値を指定しても全く無視されています。ライブラリの説明にはそんなことは書かれていないのに、この結果はちょっとどうなんだかなーと思います。

▼測定結果のグラフ
FlexiTimer2、設定に対する出力周期
 設定出来るのは、このグラフに白抜き矢印で「使える範囲」と書いてある領域になります。なおこれは設定単位の話で、これに対する倍率を別途第一引数で指定出来るので、実際には1ms以上の割込み間隔を指定することが出来ます。
 FlexiTimer2はMsTimer2の構造を流用して作ったようなことがどこかに書いてあったような気がするので、上限の時間単位が1msになっているのかも知れません。

◆まとめ
 ということで、FlexiTimer2の使い方には条件があることが判りました。この記事に書いたような制約があることを理解して使わないと、思わぬトラブルに巻き込まれると思います。 というか、実際に私がトラブルに遭ったので詳しく調べた結果がこの記事です。

 こういう制約はライブラリの説明のどこかに書いておいて欲しいです。まあ私が見落としている、あるいは英語の文章の行間が読めなくて、こんな記事を書いてしまっているなら申し訳ないです。

 あと、この記事の結果は、ここで示しているスケッチを動かして得られた結果です (@Arduino
UNO 16MHz)。特に割込み処理ルーチン(21~26行)の中に何を書くかで結果が大きく変わってしまうはずなのでご注意下さい。

Arduinoのmillis()関数が返す値は不連続な場合がある

 居酒屋ガレージ日記さんが、Arduinoのタイマー処理という記事を書かれています。Arduinoでは時間の粒度は4μsが最小なのですが、それだけでは説明できないジッタが発生しているのが気になっていたのですが、そうか、タイマー0の割込みの影響だったんですね。

 ちょうどArduinoを使ってパルスジェネレーターでも作ろうかと考えていたところだったので、こういう情報はすごく助かります。

 ところで、プログラム開始からの時間をミリセカンド単位で返すmillis()関数の説明の中に、
>正確には、システムクロック周波数16MHzの1/64の1/256で「1.024ms」。
>  (タイマー割り込み内では端数をうまく処理してmillis関数での誤差が出ないようにしています)
 うまく処理して誤差が出ないようにしてあります。と書かれています。また、このあたりの詳細は記事の中のリンク先で詳しい解説があります。
 Arduinoでの時間管理 [Arduino]:放課後マイコンクラブ

 ここで気になったのが、この補正方法だとmillis()が返す値が不連続になってしまいそうなことです。つまり、そのままだと時計としては遅れてしまうので、これを補正するためには時々値を一つ以上飛ばす必要があるはずです。mills()の戻り値は整数で一つづつ順番に数値が増えると思っていたのですが、1段飛ばしになることももありそうです。

 ならば、実際にmillis()の値の変化がどうなっているのかを確認するプログラムを作ってみました。
/* millis()の値の変化を表示するプログラム
* 2016/02/20 ラジオペンチ http://radiopench.blog96.fc2.com/ 
*
* 解説:millis()の値は普通は1.024ms毎に1ずつ増加する。このままでは時計に
* 対して遅れてしまうので、約42ms(1000/24)毎に一度に2つ値を増加させることで
* 補正を行っている。
* このプログラムはmillsの値を最初の500回分シリアルに出力するので、約42回
* 置きに値が飛んでいる様子が確認出来る。(42,85,127,,が無い)
*/

unsigned long LASTmills; // 前回のmillisの値
unsigned int T[500]; // 数値記録バッファ
int n = 0;

void setup() {
Serial.begin(9600);
LASTmills = millis();
while (n < 500) { // 指定回数だけ
if (millis() != LASTmills) { // millisの値が前回より変化していたら
LASTmills = millis();
T[n] = LASTmills; // 配列に値を記録
n++;
}
}
for (n = 0; n < 500; n++) {
Serial.println(T[n]); // 記録された値をシリアルに出力
}
}

void loop() { // 何もしないで待つ
}
 シリアルに直接出力していたのではとても間に合わないので、500回分をメモリに記録し、後でゆっくりとシリアルに出力するようにしてあります。

▼実行結果
シリアル出力の末尾
 500回のループなのに値が512になっています。補正のために値を飛ばしているからこうなったのでしょう。ん、511が無い。

▼先頭付近
42が無い
 予想通り42が抜けています。

 millis()関数は普段は1.024ms置きに値をインクリメント。これでは時計に対して遅れてしまうので、42.666(1000/24)回に一回だけ値を2つ増やすことで誤差を補正していることが確認出来ました。

◆まとめ
 millis()関数が返す値は連続した整数ではなく、部分的に不連続になっていました。なので、
 if( millis() == 42 ) { hoge; } と書くのは危険で、ものすごく原因が判り難いバグになってしまいます。ここは、
 if( millis() >= 42 ) { hoge; } と書くべきでしょう。

 重箱の隅をつつくような話ですみません。なお、この件を気付かせていただいた、居酒屋ガレージ日記さんに感謝します。

Arduino, シリアル出力の完了待ちはSerial.flush()で

 Arduinoでシリアルにデーターを出力した場合、ハードウェアのバックグラウンドで処理されます。つまりデーター送った後はソフトは何もしないでいいのですが、代償としてシリアルの出力が何時終わったか判りません(ソフトウエアシリアルは別です)。そういう事情があるので、シリアルの送信が全部終わった後で次のプログラムを動かしたい場合は、delay関数を使ってタイミングを調整することが行われます。

 ところで、このdelay関数でどれくらいの時間待たせればいいかは、文字数とボーレートから計算すればいいのですが、細かく計算するのが面倒なので多めに設定することが多いと思います。(私だけか?)

 あと、シリアルがバックグラウンドで実行されることを利用して、シリアルにデーターを投げておいて、その後で別の処理を開始することがあります。こうすると処理が並列に行われるので処理効率が上がります。ただ delay でどれくらい待たせればいいかが簡単に決まらなくなるので、多めにdelayの時間を設定することになるはずです。

 他にも色々なケースが考えられるますが、シリアル出力が終わるのをdelay関数で待つのは、あまり良い方法では無い気がします。

 この問題のうまい解決方法は無いかと、リファレンスを読んでいたら、Serial.flush()という関数がありました。この関数はArduino 1.0になる以前は、「受信バッファをクリアする」という動作だったらしいのですがArduino1.0以降では、「送信の完了を待つ」という仕様に変更になったそうです。まだ古いIDEの時代に書かれたスケッチがたくさん使われているためか、このSerial.flush()関数を使ったスケッチを私は見掛けたことがありません。

 ということで、Serial.flash()の効果を確認するスケッチを書いてみました。

// Serial.flush()のテスト 
// 2016/02/15 ラジオペンチ http://radiopench.blog96.fc2.com/

#include <avr/sleep.h> // スリープライブラリ

void setup() {
Serial.begin(9600);
Serial.println("Entering power save mode"); // メッセージ出力
// delay(10); // delay関数でシリアル出力完了を待つ
Serial.flush(); // Serial.flushでシリアル出力完了を待つ
set_sleep_mode(SLEEP_MODE_PWR_SAVE); // スリープモード指定
sleep_mode(); // ここでスリープ
}

void loop() {
}
 シリアルモニタにEntering power save mode と表示して、パワーセーブモードでスリープするプログラムです。スリープから復旧するルーチンが無いので一回実行されるだけです。もう一度実行するにはリセットする必要があります。

 このプログラムはSerial.flush(); を使うようになっています。実行するとシリアルモニタに以下のよう表示します。

▼Serial.flush()での実行結果
正常表示
 ちゃんと出力されています。

 ここで9行目のコメントアウトを外してdelay(10)を使い、10行目をコメントアウト、つまりSerial.flush()を使わないようにすると、

▼delay(10)を使った実行結果
表示不良
 文字が途中で途切れています。シリアル出力が全部終わる前にパワーセーブモードが始まってしまうので、こういうことが起きてしまいます。ここで、時間を延ばして、delay(30); とかに変更すれば文字の途切れは無くなります。

 でもそんなことするより、Serial.flush(); を使った方がスマートだと思います。

◆flashとflushの違い
 ところで、ちょうどいい機会なので flash と flush の違いについて説明します。というか自分も混乱したので、頭の整理を兼ねて文章にしておきます。ちなみに日本人にはどちらもフラッシュとしか読めませんが、英語の発音では違うのだそうです。

 ストロボのフラッシュはflashでピカリと光ること、ちなみにフラッシュメモリーもflashです。一方のflushには意味がいろいろあるようですが、「流し去る」というのが一番ピッタリすると思います。ちなみに水洗トイレの水を流すのはflushです。

 元のSerial.flush()という関数は、受信バッファの中身を消去するという機能だったのでflushという単語がピッタリします。でも、現在のように送信完了まで待つという仕様なら、Serial.wait()とでもしておいてもらった方が判り易かった気がします。まあネイティブな人達が決めたのでしょうから独り言なんですが。
カレンダー
04 | 2017/05 | 06
- 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31 - - -
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード