google-site-verification: google3bd66dd162ef54c7.html

ウォッチドッグタイマー割込みを使ってロータリーエンコーダーを読む

 先にお断りしておきますが、この記事の内容ではArduinoを使ってロータリーエンコーダーを完璧に読むことは出来ません。この記事は、ウォッチドッグタイマー割り込みの使い方を解説するために書いています。

 Arduino UNO(ATmega328P)には timer0,timer1,timer2 の3つのタイマーがあります。このうちtimer0はシステムが使っているので、勝手に使うことはできません。いや、使ってもいいのですが、それをやるといろいろ面倒なことが起きるはずです。
 ということで、普通はTimer1とTimer2の割込みを使うことになります。普通のアプリならタイマー割り込みが二つあれば大丈夫でしょう。でももうすこし複雑なことをやらせたい場合、もう一つタイマー割り込みがあると助かります。

 そんなことを考えていたら、居酒屋ガレージ店主さんからコメントで、ATtiny2313を使ったパルスジェネレーターを紹介していただきました。この作品はアセンブラを駆使してCPUの資源をしゃぶりつくして作られているのですが、特に参考になったのは、ウォッチドッグタイマー割込みを使ってデジスイッチの読み取りをやっていることです。

 そうか、その手があったか。時間精度のいらない低頻度の割込みならウォッチドッグタイマーが使えます。このテクニックは組み込み系をやられている人には常識なのかも知れませんが、私にとっては目からウロコでした。

 ということで、早速Arduino UNOでやってみました。

▼回路図
Arduinoにロータリーエンコ-ダー
 Arduino UNOにロータリーエンコーダーを接続します。

▼スケッチ
/* ウォッチドッグタイマー割り込みで、ロータリーエンコーダ
* を読取る実験。(15ms間隔サンプリングなので早回しはダメ)
* 2016/03/31 ラジオペンチ http://radiopench.blog96.fc2.com/
*/

//#include <avr/wdt.h> // ウォッチドッグタイマーを使用

#define ECA 9 // エンコーダーA相 = D9
#define ECB 10 //       B相 = D10

volatile int X;
int data;

void setup() {
pinMode(ECA, INPUT_PULLUP); // ロータリーエンコーダーA
pinMode(ECB, INPUT_PULLUP); // ロータリーエンコーダーB

Serial.begin(9600);
Serial.println("start");
WDTimerStart(0); // 15ms間隔でウォッチドッグタイマー割込み開始
}

void loop() { // メインループ
if (X != 0) { // エンコーダーが動いていたら
data = data + X;
X = 0;
Serial.println(data);
}
}

void WDTimerStart(unsigned int ii) { // ウォッチドッグタイマーをセット。
// 引数はWDTCSRにセットするWDP0-WDP3の値。設定値と動作時間は概略下記
// 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
// 6=1sec, 7=2sec, 8=4sec, 9=8sec
byte bb;
if (ii > 9 ) ii = 9; // 変な値を排除
bb = ii & 7; // 下位3ビットをbbに
if (ii > 7) { // 7以上(8,9)なら
bb |= (1 << 5); // bbの5ビット目(WDP3)を1にする
}
bb |= ( 1 << WDCE );

MCUSR &= ~(1 << WDRF); // MCU Status RegのWatchdog Reset Flag ->0
// start timed sequence
WDTCSR |= (1 << WDCE) | (1 << WDE); // ウォッチドッグ変更許可(WDCEは4サイクルで自動リセット)
// set new watchdog timeout value
WDTCSR = bb; // 制御レジスタを設定
WDTCSR |= _BV(WDIE);
}

ISR(WDT_vect) { // WDT割込み発生時にロータリーエンコーダーを読む
static byte bp = 0; // ビットパターン記録バッファ
bp = bp << 1; // ピンの状態変化をbpに右詰めで記録 0b00ABABAB
if (digitalRead(ECA) == HIGH) bp |= 0x01;
bp = bp << 1;
if (digitalRead(ECB) == HIGH) bp |= 0x01;

bp &= 0x0F; // 下位4ビット残して上位を消す 0b0000ABAB
if (bp == 0b0111) X ++; // 一致していればインクリメント
if (bp == 0b1011) X --; // 一致していればデクリメント
}
 ロータリーエンコーダーを廻すと現在の値をシリアルモニターに表示するプログラムになっています。ロータリーエンコーダーの読取りは、51行目以降の割込み処理ルーチンで行っていますが、アルゴリズムは、以前の記事に書いたビットパターン一致の監視法です。CPUのレジスタを直接いじっているので、6行目の #include は使わないのでコメントアウトしています。

 20行目の WDTimerStart(0); でウォッチドッグタイマー割り込みを起動しています。引数によって割込み間隔が変わり、0で呼ぶと最高速である約15ms間隔で割り込みが発生します。ちなみにこの関数は以前記事にしたdelayWDT関数のプログラムで使った、delayWDT_setup(unsigned int ii) 関数の使い回しです。この割り込みを停止させるために、WDTimerStop()という関数も作った方がいいのですが、とりあえず止めないでも困らないのでまだ書いていません。

 これを動かした結果ですが、割り込みの速度が15ms間隔と遅いので、ロータリーエンコーダーをゆっくり廻さないと取りこぼしが発生します。まあこうなるのは想定の範囲内で、ゆっくりと動かすと正常に値が変化するので割り込みはうまくいっているようです。ちなみに、ロータリーエンコーダーを取りこぼし無く読むには、割込み間隔を1msくらいにする必要があります。

 そういう問題はありますが、ともかくウォッチドックタイマー割込みを使うプログラムはこれでいけそうです。人間が操作するスイッチの読み取りや、バッテリー電圧の監視くらいなら、ウォッチドックタイマの割込みで充分対応出来ると思います。

 あと、同じような機能はネットを探せばライブラリが出てくるので、それを使う手もあります。ただここからは私の意見ですが、Arduinoのユーザーライブラリには古いままメンテナンスされてないものがあって、最新版のIDEではコンパイラが通らないものが結構あります。ということで、よく確認しないでライブラリを使うと、かえってややこしいことになると思います。

◆まとめ
 ウォッチドッグタイマーは、プログラムの暴走など予期しない動きから確実に脱出するために用意されている仕掛けだと思います。でも、さほど信頼性が必要ない場合はこれを割込み源として使うのもアリだと思います。

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; } と書くべきでしょう。

 重箱の隅をつつくような話ですみません。なお、この件を気付かせていただいた、居酒屋ガレージ日記さんに感謝します。
カレンダー
02 | 2017/03 | 03
- - - 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コード