今回使ってみるのは、ロータリエンコーダです。昔のメカニカルマウスや、マウスのホイールなどに使われている装置ですね。
本来は、データシートで仕様を確認することろなのですが、原理から入ってみましょう。
ロータリエンコーダとは、上図のような装置で、つまみの様に飛び出た部分(軸)をくるくる回すと、「どのくらい、どの方向に回転したか?」を、知ることができる装置のことです。Iduinoスタータキットに入っていたロータリエンコーダは、軸の絶対的な回転位置を知ることのできるタイプではなくて、相対的にどの方向にどれだけ回転したかを知ることができるタイプのものです。
このタイプのロータリエンコーダは、軸と一緒に回転する金属の円盤がついていて、円盤の周辺部がギザギザの歯車状になっています。この円周方向の、少しずれた位置に2つの電極がついていて、円盤と導通したかどうかが外部からわかるように配線が引き出されています(下図)。
2つの電極は、少しずれた位置にあるため、歯車状の円盤が回転したときに、導通するタイミングにもずれが生じます。
軸(円盤)が時計回りに回った時と、反時計回りに回った時の、端子CLKとDTの波形(ポンチ絵)が以下図です。
スイッチの物理的位置の差によって、スイッチのON/OFFタイミングにずれが生じているのですが、着目すべきは、CLKの立ち上がりエッジが生じた時の、DTの状態です。CLKの立ち上がりエッジに青い破線が引いてあります。時計回りに回っているときにはCLKの立ち上がりエッジでDT=LOW、反時計回りではDT=HIGHとなるので、回転方向を特定することが可能です。
そもそも、CLKの立ち上がりエッジの回数をカウントしておくと、どちら向きにどのくらい回転したか、を知ることができるという訳です。
原理がわかったので、プログラムは組めるのですが、実際の波形を取得して、確認しておきたいことがあります。
(クリックして拡大)
やっぱりそうですか……。上の波形は、ロータリエンコーダをグリ(時計回り)グリ(反時計回り)と回したときの物です。確かに、CLKの立ち上がりエッジでDTを見ると、初めの方は時計回り(DT=LOW)、後半は反時計回り(DT=HIGH)に見えます。しかし、やはり、チャタリングによる細い波形がそこかしこに見られます。綺麗な立ち上がりエッジに見えるところであっても、立ち上がり時間がダブって表示されているところは、複数回の立ち上がりエッジを検出した場所です。
筆者が使用したロータリエンコーダは、回すとカチカチと手ごたえ(クリックと呼ぶらしいです)があって、一周回すと、20回手ごたえをカウントできました。つまり、本来は、軸を一周、時計回りに回転させると、「時計回りに20クリック」という結果が得られるはずなのですが、チャタリングによるエッジをカウントしてしまうと、なにか間違った結果を返すかもしれませんね。あとでプログラムが完成したら、再度実験してみましょう。
この手のメカニカルなスイッチ類と付き合うためには、チャタリング除去とうまく付き合う必要があるみたいですね。もう少しちゃんとしたロータリエンコーダでは、回転する円盤にスリットが設けられていて、円盤を挟むようにLEDと受光素子が配置され、受光素子で受けた光量の変化によって回転数をカウントするタイプがある様です。そのタイプだと、チャタリングは起きないのでしょうか……?
手元のロータリエンコーダがメカニカルなのは間違いないので、言っても始まりませんが……。
まずは、こんな感じになるわけです(抜粋)。
void get_rotenc (
uchar *clk,
uchar *dt)
{
(*dt)=digitalRead (pin_DT);
(*clk)=digitalRead (pin_CLK);
}
void loop (void)
{
// check CLK rising edge
uchar clk;
uchar dt;
static uchar clk_prev=HIGH;
get_rotenc (&clk,&dt);
if (clk_prev==LOW && clk==HIGH)
{
// clk rising edge
if (dt==LOW)
cnt_cw++;
else
cnt_ccw++;
}
clk_prev=clk;
}
ArduinoのライブラリとCを使って組むと、これしかないですね。しかし、ちょっと、気持ち悪いわけですよ。だって、DTをサンプリングするタイミングと、CLKをサンプリングするタイミングって、「同時」じゃないですよね?本当に欲しいのは、「CLKが立ち上がった瞬間のDT」なのに、数100ns〜数μs手前のDTをサンプリングしてます。
Cライブラリの関数が、端子1本づつしか読み取ることができないので、どうしようもありません。
汎用性は失ってしまいますが、アセンブラを使う前提なら、解決策はあります。
Arduino UNOに搭載されている、ATmega328のデータシートを探してみてみると、端子状態を読み込み可能なポートは、PORT-B、C、Dがあるとわかります。続いて、Arduino HP ⇒ PRODUCTS ⇒ UNOからボードの回路図にアクセスすると、Arduino UNOのデジタル端子D1〜D8に引き出されているのはPORT-D0〜7、D8〜D13に引き出されているのはPORT-B0〜5です。PORT-Cはアナログ端子に割り当たっています。
ロータリエンコーダのCLKとDTを同じポート、例えば、PORT-D2とD3(デジタル端子のD2とD3に相当)に割り当てると、ポートレジスタPIND(8ビット)をリードすることで、同時にCLKとDT端子をサンプリングすることができます。
PINDのI/Oアドレスは0x09ですので、アセンブラで書くと、これだけ(↓)です。
IN R0, 0x09
1命令だけのことですので、なにかこう……、「インラインアセンブラ」みたいな機能があると、一発で解決なのですが。Arduinoが自前でコンパイラを開発したとも思えないし、どうせGCCなんでしょ?試してみましょう。
void setup (void)
{
int tmp=__GNUC__;
}
void loop (void)
{
}
コンパイルしてみます。ノーエラーでした、GCC確定ですね。だとすると、「gcc+inline+asm+arduino」辺りで検索すると、インラインアセンブラの使い方が見つかりますので、探してみてください。
インラインアセンブラを使うと、さっき作ったget_rotenc()関数が、以下の様に記述できます。
void get_rotenc (
uchar *clk,
uchar *dt)
{
// read port-d (PIND(0x09) register)
// clk=PIND2
// dt=PIND3
const uchar reg_PIND=0x09;
uchar pind;
__asm__ __volatile__ (
"in %0,%1"
:"=r"(pind)
:"I"(reg_PIND)
);
(*dt)=(pind&0x08)>>3;
(*clk)=(pind&0x04)>>2;
}
「どうせアセンブラを使うのだから、(*dt)とか、(*clk)の処理もアセンブラで書いたら速いんじゃない?」などと言う感覚は、もう大昔の感覚なのでしょうね……。8ビットの、しかもRISC MCUのアセンブラなんて、GCCと競ったところで勝てる気がしません。使う場所は必要最小限度にとどめるのが正しい。上の例では、本当にin命令1個を使わせるためだけにインラインアセンブラを使っています。
後は、GCCが勝手に全体最適化をかけてくれてますから。
さて、Arduinoには、ATmega以外の機体もありますし、同じATmegaでも、Leonardo(Micro)などに使われているATmega32U4などでは、ピンアサインが異なるため、このままでは動きません。以下のようなプリデファインマクロを使って、場合分けするのが親切でしょう。
他のMCUやボードについて知りたい方は、ググッてみてくださいm(1__)m。
アセンブラバージョンを使用可能な環境(UNO)で実行する場合にも、アセンブラバージョンと、C言語バージョンをユーザが選択可能なように、#defineを追加しておきます。
#define USE_ASM
:
:
#ifdef USE_ASM
__asm__ __volatile__ (……);
(*dt)=(pind&0x08)>>3;
(*clk)=(pind&0x04)>>2;
#else // USE_ASM
(*dt)=digitalRead (pin_DT);
(*clk)=digitalRead (pin_CLK);
#endif // USE_ASM
C言語バージョンを使いたい時には、#define USE_ASMをコメントにします。
※一応、アセンブラによる解決策を示したかっただけで、実はこの後議論するチャタリング除去を実施すると、もはや「CLKとDT端子の同時取り込み」には、それほど意味が無いことがわかります(1^^;)っ。ここは(アセンブラの)使い所ではなかったのかも知れない……。
ロータリエンコーダの各端子を以下の様に接続します。
Arduino | 接続 | ロータリエンコーダ |
GND | ⇔ | GND |
5V | → | + |
今回は未使用 | × | SW |
D3 | ← | DT |
D2 | ← | CLK |
お使いのロータリエンコーダによっては、端子名が違っているかもしれません。SWはスイッチのことで、回転する軸部分が押し込めるようになっており、押し込んだ事を検知するための信号線です。今回は使いません。
デュポンワイヤで直結するか、ブレッドボードに挿してジャンパワイヤで接続します。
実験用のプログラム全体をリンクしておきます。Arduinoへコンパイル&書き込みしたら、シリアルモニタを開いてください。
ロータリエンコーダ実験用プログラム:test_rotenc.ino
※Arduino UNO以外で実験する方は、プログラム先頭の
#define USE_ASMをコメントしないとコンパイルエラーになります。
"show"を送信すると、そこまでの間に検知した回転数(クリック数)を表示します。
実行サンプル:【 】内はシリアルモニタからの送信データ
【show】 …… 時計回りに1クリック後
OK: CW=1, CCW=0
【show】 …… 反時計回りに1クリック後
OK: CW=0, CCW=1
時計回り(CW)に1クリックと、反時計回り(CCW)に1クリックだけ、回転して"show"を送信した結果です。これだけ見ると良さそう。
こんどは、カチカチっと、2回分、連続でクリックしてみます。
実行サンプル:【 】内はシリアルモニタからの送信データ
【show】 …… 時計回りに2クリック後
OK: CW=2, CCW=1
【show】 …… 反時計回りに2クリック後
OK: CW=1, CCW=2
アレレ?何かおかしいですね……(1^^;)っ。さらに、一周360°時計回り。
【show】 …… 時計回りに360°一周(20クリック)後
OK: CW=36, CCW=5
だめだ<(1x_x)>。なんじゃこりゃ〜。バグかと思いましたが、違いました。チャタリングで誤判定している模様。期待値は「CW=20, CCW=0」ですから、ひどい……、それにしても酷い、ひたすらヒドイ。
チャタリング除去の前に、get_rotenc()の実行時間をmicros()で計測しておきます。
void setup (void)
{
// serial
Serial.begin (9600);
while (!Serial)
;
// test
uchar clk;
uchar dt;
unsigned long usec;
usec=micros ();
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
get_rotenc (&clk,&dt);
usec=micros () - usec;
Serial.println (usec/10., 1);
}
#define USE_ASMを定義した場合と、そうでない場合(C言語バージョン)の両方で計測します。
#define USE_ASMのケース:2.0[μs]
#define USE_ASMを定義しないケース:9.6[μs]
アセンブラを直接叩いた方が、×5倍近く高速でした。薄々思ってはいたのですが、digitalRead()って、遅すぎません?
ともあれ、USE_ASMを定義しなかった場合の方が、多少は、チャタリングによる誤判定が少ない様に思います。それでも、実用レベルには程遠い。USE_ASMの方がサンプリングスピードが速いので、よりチャタリングの悪影響を受け易いと考えられます。
問題は、サンプリング速度がだいぶ遅いUSE_ASM非定義の場合でも、完全にはチャタリングが除去しきれないということ。10μs以上に渡ってチャタリングの影響を受けているということですから、ちょっとやそっとでは除去しきれない(手強い)ことがわかりました。本腰を入れないとなりませんね。
clkの判定に、LPF(Low Pass Filter)をかけてみましょう。
LPF:clk256=(clk256*(N - 1) + ((clk==HIGH)? 255: 0))/N
Nは≧1の整数です。LOW(0)とHIGH(1)の端子状態を0〜255の256段階に引き伸ばしておいて(clk256)、過去のclk値を(N−1)/Nの強度で残していきます。以下図がNの値に対するステップ応答です。
clk256>127をHIGHと認識します。上のグラフでは、0.5を上回ったところがHIGHです。
N→大の方が、高周波数成分(チャタリング)を除去する能力が高くなりますが、N→大では応答が遅くなり、最悪、正規の(チャタリングではない)波形を取りこぼしてしまう可能性もあります。適切なNを設定することが必要です。
新しいプログラムでは、【flt】コマンドが追加してあり、Nの値をダイナミックに変更できるようになっています。Nの値を変えながら、最適値を探す実験をしてみましょう。
新しいプログラム:test_rotenc.ino
※先ほどのプログラムと同じです。既にLPFが組み込んでありますので、
先頭付近の#define USE_LPFを有効にしてください。
実行サンプル:【 】内はシリアルモニタからの送信データ
※USE_ASM&USE_LPFが有効
RDY
【flt,3】
OK: N=3
【show】 …… 時計回りに一周後
OK: CW=22, CCW=5
【flt,5】
OK: N=5
【show】 …… 時計回りに一周後
OK: CW=20, CCW=1
【flt,7】
OK: N=7
【show】 …… 時計回りに一周後
OK: CW=20, CCW=0
【flt,9】
OK: N=9
【show】 …… 時計回りに一周後
OK: CW=20, CCW=0
【show】 …… 反時計回りに一周後
OK: CW=0, CCW=20
筆者の環境だと、N=7でも、時々失敗しました。勿論、N=9でも、まだ失敗することはありますが、だいぶマシなレベルに向上しました。どこまで大きくしたらクロックを取りこぼすだろうか?と、N=32にしてみましたが、まだ取りこぼしはありません。N=64を設定したところ、誤判定が多発する様になりました。N=10〜30辺りで安定して使えるかもしれませんね。
int(16ビット)と、最大値256で処理しているので、Nの最大値は128です。これを超えると、intの演算がオーバフロウします。
なお、このフィルタには、時間の概念が入っていません。つまり、Nの値と、チャタリングを除去する能力との関係は、サンプリング速度が変わると、変わってしまいます。
例えば、USE_ASMの#defineをコメントしたり、実行環境を動作速度の速いMCUに変えたりして、サンプリング速度が変わると、同じNの値を使っても、元の環境と同じチャタリング除去能力にならず、再度調整が必要になります。
※もっと言うと、何か別なモジュールの制御が入って処理量が増えると、ロータリエンコーダのサンプリング速度が変わるので、その場合も、最適なN値は変わります。ただ、実験では、比較的安定して使用可能な範囲が広いので、それほどシビアに考えなくても良いかもしれません。
さらに対策を進めると、DTの波形にもノイズ(チャタリング)が乗っていましたので、DTにもCLKと同じ様なLPFをかけるべきかもしれません。今回は、ロータリエンコーダの試行が目的だったので、この辺で止めておきます。何か実用的なアプリケーションが見つかった時に対応したいと思います。プログラムは難しくないので、必要に応じて、追加してみてください。
モータの回転軸とロータリエンコーダの回転軸を繋いでおいて、モータの回転角度を知ることができないか?などと思っていましたが、このメカニカルなロータリエンコーダでは無理かもしれませんね……。チャタリングが除去しきれなかった場合、回転角度を間違えますから……。何か使い道を考えてみます。
※先の回からのフィードバック:他の処理量が増えてくると、CLKエッジを取りこぼすため、今回の方法ではチャタリングを除去しきれないケースが出てくることがわかりました。対処方法については「#2-10 ミニキーボード付きゲームパッド」をご覧ください。