PIC AVR 工作室別館 arduinoの館->TopPage->接続くん2->ロータリーエンコーダ

ロータリーエンコーダ

Arduinoを使って、ロータリーエンコーダの制御を行った件について、 以前ブログで触れましたが、 その内容を整理しつつ、補足します。

ロータリーエンコーダの入力

角度センサとロータリーエンコーダー

ロータリーエンコーダの使い方については、 ChaNさんのサイトで詳しく説明されています。 ここでは、インクリメンタル型について触れます。

ロータリーエンコーダは、回転の操作(回転角)を入力するデバイスで、 オーディオコンポの「ボリュームつまみ」や「ラジオのチューニングつまみ」などで使われています。 2本のデジタル線だけで接続することができます。

回転角を知るためのセンサとしては、可変抵抗もよく使われます。Arduinoならアナログ入力端子から信号を入力することで、 「回転角」を知ることができます。 可変抵抗は抵抗を利用しているので、回転角の範囲に限りがありますが、ロータリーエンコーダは無限に回転させることができます。

ロータリーエンコーダの制御

ロータリーエンコーダは、2本(相)のデジタル入力信号を使って、オンとオフの2パターン×2相=4通りの情報(信号)を入力して、 その信号パターンが時系列的にどう変化したかを元に、回転角の増減を検知します。

それぞれの相は、半分ずつずれている(位相が90度ずれている※)ので、回転すると、2個のセンサーからの入力信号は、 「00」→「01」→「11」→「10」→「00」…という具合に、4通りのビットパターンが繰り返されます。 このパターンを10進数で表記すると、「0」→「1」→「3」→「2」→…となります。 また、反対に回すとこのパターンが逆順に現れます。回転していない場合は、「00」→「00」のように同じパターンが出続けます。

※:90度と言っても、つまみの回転角90度分ではない(4つの信号パターン一組を360度と考えたときに、その1/4で90度という意味。 製品にもよるけど、たとえば1周24パルスの製品なら、つまみを1回転させる間にこの4つ一組のパターンが24回現れる)

グレイコード

このビットパターンの並びを良く見てみましょう。隣り合ったデータ同士を比較すると、常に一度に1箇所(1ビット)しか変化していません。

つまり、「00」→「11」のように同時に2ビット以上変化することがありません。 このように、 一度に1ビットしか変化しないような符号化方法を、 「グレイコード」 と呼びます。 (「グレイ」は、色ではなく、これを考案した人の名前です。ここでは2桁の2進数データだけ扱っていますが、 3桁、4桁…と多ビット化したグレイコードもあります)

ロータリーエンコーダのような信号を扱う場合、グレイコードを利用すると、変化するビットが常に1個(1ビット)だけになるので、 回転時の微妙なタイミングによって信号が化けることが無く、信号の変化を正確にとらえることができます。

もし2個以上の信号が変化するような場合…例えば、「00」→「01」→「10」→「11」→「00」…のように(普通の2進数表記のように)、 同時に2ビット変化するようなコードの場合…センサーからの信号が、完全に厳密に2ビット同時に変化する必要があるのですが、 当然、物理的にはそういう事はありえません。ほんの一瞬だけ、どちらかが先に変化して、続いてもう片方が遅れて変化することになります。 こうした信号を読み込むと、コードを誤認識してしまい、誤動作の原因となります。グレイコードを使うと、これを防ぐことができます。

グレイコードを使うと、同期信号(クロック信号)などを使わずとも、データ信号だけで、ロータリーエンコーダからの信号を扱えます。

なお、一旦入力したデータは、グレイコードのままだと少し扱いにくいので、 「0」→「1」→「3」→「2」は、「0」→「1」→「2」→「3」に読み替え(置き換え)しておいた方が、 後々の処理で何かと都合がよくなる場合もあります。(都合に合わせて、定数テーブルなどを使って読み替え処理をしてください)

信号の計算・処理方法

読み込んだ「パターンの変化」から、どっちにどのくらい回ったのか(角度の変化)を、以下のように計算できます。

計算方法1:テーブル処理

入力信号のパターン(前回分と今回分)を合わせて、その「変化パターン」と「回転方向」を「定数テーブル」などに格納しておけば、 テーブルサーチ処理によって、どっち方向に回転したのかを知ることができます。

4つの「今の状態」に対して、「次の状態」は「CW(時計回り)」「CCW(反時計回り)」「停止したまま」の3通りがありうるので、 テーブルには12通りのパターンを盛り込んでおけば最小限のスペックとしては足ります。

実際には、回転が速すぎて2パターン一気に進んだり、信号にノイズが載るなどすると、本来ありえない信号が入力されます。 そういう場合については、上記の12個要素ではテーブルに見つからないのでエラー処理(例えば静止状態と扱う)とするか、 もしくは、エラーとなるケースもテーブルに盛り込んでおいても良いでしょう。(それでも全16パターンあれば済みます)

こうした処理をハードウェア回路で実現しようとすると少し規模が大きくなってしまうのですが、 ソフトウェア処理であれば、これが一番素直なやり方になるでしょう。 (後々プログラムを修正する際に、理解しやすいプログラムに作れる)

このテーブルを使って、if文やswitch~case文などを使ってこの通りにロジックを書くのが、素直な処理と言えると思います。

計算方法2:間引き

もう少し手を抜く方法があります。

ChaNさんサイトのロータリーエンコーダのページの、「ロータリーエンコーダのコーディング例(2)」では、 1周の4パターンの繰り返しのうち、1ヶ所だけに焦点を当てて、その変化パターンを通過するときだけ、 1ステップ分「右回転/左回転」カウントする方法を採っています。

これなら、全入力パターンをテーブルに記述しておく必要がなく、ソフト上の処理も単純で極めて小規模に済ませられます。

ただし、1周の全ステップの信号を拾うわけではないので、上記の「方法1」と比較すると、検知できる回転角の精度(分解能)は、 1/4とか1/2とかになります。(つまり2倍とか4倍とか回さないと同じ量の角度変化として検知されない)

また、回転時にクリック感(デテント=戻り止め)があるロータリーエンコーダでは、場合によっては、クリックの数と実際の回転がシンクロせず、 違和感を感じるかもしれません。

処理方法

計算方法2について、処理方法を考えてみます。

2ビットの入力値は、正回転、逆回転それぞれで、このように並びます。

 正回転 00→01→11→10→…
 逆回転 00→10→11→01→…

これを踏まえ、前回拾った信号を上位2ビットに、今回拾った信号を下位2ビットにして繋ぎ、計4ビットのデータに纏めてみましょう。 すると、この4ビットのデータ値(太字部分を通過するときのデータの並び)は、次のようになります。
(00→00など変化がない場合は除外)

正回転の場合

 「0001」→「0111」→「1110」→「1000」→…

逆回転の場合

 「0010」→「1011」→「1101」→「0100」→…

これらの上位2ビット(前回)と下位2ビット(今回)を4ビットの数値として、10進数(16進数)になおしてみると、それぞれ、

正回転の場合

 1(0x1) → 7(0x7) → 14(0xE) → 8(0x8)→…

逆回転の場合

 2(0x2) → 11(0xB) → 13(0xD) → 4(0x4)→…

のパターンが現れます。

これらの変化パターンのうち、例えば「0x7」と「0xD」のパターンだけに着目し、 1周4ステップのうち、これらのパターンが現れた時だけ増減すれば、単純な処理ロジックで「正逆回転」の変化量がカウントができます。
(なぜこの2パターンなのかは、下記でチャタリングと合わせて後ほど触れます)

大雑把に言うと、「計算方法1」でテーブル化する変化パターンのうち、正転・逆転からそれぞれ1パターンずつだけ抜き出し、 他は読み捨てているともいえます。なお、読み捨てているため、実際の物理的な回転量に対して、拾える信号の数(分解能)は1/4になります)

ただし、デテントで停止するのは4つのパターン1組分を跨いだところなので、どちらの計算方法でも実用上はあまり違いは生じないでしょう。

スケッチ

ロータリーエンコーダ制御のスケッチ

この方法2を元に、Arduino用スケッチにしてみたのが、以下のものです。

int a_sig = 7;    // a signal on d7
int b_sig = 8;    // b signal on d8

volatile int rot = 0;    // rotation value (initial = 0)
volatile int diff = 0;    // difference value (initial =0)

#include <MsTimer2.h>


void interrupt0(){
  int a_in;
  int b_in;
  int new_val;    // now value
  static int old_val = 0;    // old value (initial=0)
  
  if (digitalRead(a_sig)==HIGH){
    a_in = 1;
  }else{
    a_in = 0;
  }
  if (digitalRead(b_sig)==HIGH){
    b_in = 1;
  }else{
    b_in = 0;
  }
  new_val = a_in * 2 + b_in;
  
  if (( (new_val<<2) + old_val) == ( (0b11 <<2) + 0b01) ){
    diff ++;
  }
  if (( (new_val<<2) + old_val) == ( (0b01 <<2) + 0b11) ){
    diff --;
  }
  
  old_val = new_val;
}


int rotary_value(){

  noInterrupts();
  rot += diff;
  diff = 0;
  interrupts();
  
  return rot;
}



void setup() {
  Serial.begin(9600);
  
  pinMode(a_sig, INPUT); // a signal as input
  pinMode(b_sig, INPUT); // b signal as input
  
  MsTimer2::set(1, interrupt0); // 1ms period
  MsTimer2::start();
}


void loop() {

  int input_val;
  
  input_val = rotary_value();
  delay(100);
  Serial.println(input_val);
  
}

スケッチの処理内容

スケッチの処理内容の要点をざっくりまとめます。

(1)サンプリング処理

タイマ割り込みライブラリ「MsTimer2」を使って、周期的に信号を読みに行って、変化があった場合、 かつ上記の「0x7」「0xD」にあたる場合には、カウンタ(=回転位置の差分値)の増減を行います。 MsTimer2ライブラリを使用しているので、 setup関数内で、このライブラリの初期化を行っておく必要があります。 割り込み周期は、ロータリーエンコーダの場合、1m秒程度に設定しておくと良いかと思います。
(間隔が開きすぎると読み飛ばしが生じて値が上手く読み出せず、狭すぎるとチャタリングで少しばたつきます)

(2)読み出し方

関数「rotary_value」をメイン処理から呼び出すと、割り込みの都度拾っておいたカウンタ(変数diff:差分値)を元に、 積算処理を行って、実際の回転位置(角度)を計算します。 積算値は、グローバル変数rotとして定義してあるので、メイン処理側から任意のタイミングでクリアもできます。(rotにゼロを代入するなど)

暇のある方は、C++のオブジェクトに書き換えて、読み出すメソッドや、ゼロクリアするメソッドなどにしてみると使いやすいかもしれません。

なお、変数「diff」は割り込み処理内で更新されています。これを割り込み処理の外側で参照する場合、 一旦割り込み禁止にしておく必要があります。

変数「diff」はint型のため、メモリマップ上では複数バイトにまたがって配置されています。 Arduinoの場合、int型は2バイトです。アセンブラレベルで見ると、この2バイトは1バイトずつ別々に読み出しが行われます。 もし割り込みを禁止せずにアクセスすると、先に1バイト読み出してから、次にもう1バイト読み出す前に割り込みが発生してしまうと、 1バイト目と2バイト目に読み出されるデータは、別のデータとなってしまいます。 そのため、「noInterrupts()」「interrupts()」を使って、一時的に割り込み禁止状態でアクセスする必要があります。

関数「rotary_value()」の内部では、割り込み禁止で変数「diff」を参照しています。

ロータリーエンコーダ制御の課題

つまみを手で回すタイプのロータリーエンコーダは、センサーの接点が接触することで、通電/絶縁(オン/オフ)を制御しています。 そして、こうした制御には、グレイコードが役立っていることについては既に触れました。 しかし、接触センサ特有の問題を生じることがあります。

チャタリングに関すること

問題の一つは、チャタリングです。

接触センサといえば、代表的なものにタクトスイッチなどがありますが、こうしたパーツでは、オンとオフが切り替わるときに、 信号が安定するまでの微小時間にオンとオフが「バタバタ」と切り替わります。 これをチャタリング とかバウンシング(bouncing)とか呼びます。

チャタリングが発生しても、グレイコードを使っている場合、入力値が変化する1ビットのところだけがバタバタするだけで (短時間ばたつくことはあってもすぐに収まり)、コード値が致命的に誤認識されておかしな値を入力するといったことにはなりません。 また、一般的なチャタリング対策を行っておけば、短時間のばたつきについても特段気にする必要もないでしょう。

上記のプログラムでは、1ミリ秒ごとに割り込みでサンプリングすることで、チャタリングを除外しています。

(なお、「計算方法2」で間引く場合、正転・逆転の双方が、同じコードを跨ぐ値を採る必要があります。先ほどの0x7と0xDは、 正転側は「01→11」に、逆転側は「11→01」に変化している部分であり、お互いがちょうど逆向きの組み合わせと判ります。 このように同じコードを跨ぐ組み合わなら、チャタリングが発生してもお互いが補完し合うので、誤動作を防ぎます。 逆にもしこれが、0x7と0xB(0111←→1011)のように、同じコードを跨いでいない場合にチャタリングが起こると、 お互いが補完し合わないので、カウンタが片方向に勝手に複数回進んでしまい、誤動作を起こします。

逆に言えば、補完しあう組み合わせであれば、「0x7」「0xD」以外でもかまいません。 例えば、順方向「00→01」と、逆方向「01→00」を組み合わせた、「0x1」「0x4」の組み合わせでも同様に動きます。

チャタリングの影響は、回路構成やソフト側の読み取り処理の工夫で、このようにトラブルを未然に防ぐことが可能です。

接触不良に関すること

もう一つは、接触センサの寿命に関わる問題です。

接触センサは、物理的に接したり、離れたりすることで、信号のオン/オフが切り替えられます。 しかし、導電体が接触や摩擦を繰り返すと、いずれ擦り切れて、信号のオン/オフを上手く伝えられなくなってきます。 特に、本来は通電状態となる必要があるときに、上手く通電できない状態が発生することが生じます。

接触式のロータリーエンコーダの場合、本来通電している必要があるところ(オンのところ)で上手く接触ができなくなると、 正しいグレイコードが出力できなくなる恐れが生じます。すると、正しくカウント処理の結果がおかしくなり、誤動作を起こします。

接触センサであれば、これは原理上避けられません。いわゆる寿命というものです。 私が以前購入したオーディオコンポは、ボリュームつまみの内部でこのロータリーエンコーダが使われていますが、 新品で購入後、数年で接触が駄目になりました。(それ以降、リモコンでボリュームを設定しています)

寿命は、製品によって異なります。当然ながら、安いものは短く、長寿命のものは一般的に高価です。

信頼性が重要な用途の場合、やはり少し高くても、長寿命のものを選んでおくのが安全でしょう。 RSコンポーネンツで長寿命タイプを探してみました。

長寿命タイプとしては10万回転程度、一般的なものは3万回転程度のようです。

秋葉原の店頭などで、安価なものが簡単に手に入りますが、数万回転程度のものから、短いものでは数千回転程度、 モノによっては明示されていないものなどもありました。

用途に合わせて、どのくらいの寿命に設定するのかを考えて、選ぶ必要がありそうです。

光学式ロータリーエンコーダ

非接触な光学式ロータリーエンコーダ

なお、ロータリーエンコーダには、光学的に検知するタイプのものもあります。

光学タイプは、モータやギヤボックスの出力軸のように、常時回転している軸の回転角を正確に把握する用途で使われます。 ソフトの処理上は、接触式のものと基本的に変わりはありませんが、耐久性などの点で性格が異なります。

モータなどの回転軸では、数万回転、数十万回転はあっという間なので、接触センサは適しません。 また、接触式が毎秒数百~千回程度のセンスにしか対応できないのに対して、光学式は数桁速い速度まで追従します。

光学式は非接触センサなので、回転によって劣化することはないのですが、LEDと光センサの組み合わせで動作させるため、 LEDの発光寿命(通常時間で規定される)によって寿命が設定されます。LEDを常時点等させたままでも、数年~10年程度は動作可能です。

一方、LEDを点灯させる必要があるため、そのための電源が必要になります。

なお、オーディオコンポのボリュームのように、手で摘んで回転するといった用途には、光学センサのタイプは皆無です。

モータ以外にの用途

モータ以外にも、PC用のマウスの中にも、光学式のロータリーエンコーダが使われています。 初期のマウス(中にボールが入っているマウス)では、ボールの回転を検知する部分で使われていました。 最近のマウスでは、センターホイール(スクロールホイール)の内部で、光学式のものが使われています。

こちらのサイトでは、マウスを分解して、Arduinoで読み出す実験を行っていました。

ロータリーエンコーダの選び方

寿命

ロータリーエンコーダを選ぶ上でポイントとなるのは、まずはやはり寿命です。

人が手で触って回転させるため、センサ部の接触不良だけでなく、回転軸の機械的な強度や、回転軸自体の磨耗による劣化、 ハンダづけなどによる固定の強度などによって、モノ全体の寿命に影響します。

サイズと形状

さらに、寿命の長さの観点に加え、他にも選択の視点があります。

一つは、サイズやフットプリント(ハンダ付けする端子の配置や面積)、サイズ、基板に取り付けるための脚の寸法、 シャフトの長さや太さなどのバリエーションです。スルーホールなのか表面実装なのか、といった点もここに含められるでしょう。 どのような基板に取り付けて、どのようなケースに収めて、どのように操作して、 どのように力学的負荷が掛かるか…といったことを踏まえて選択します。

操作感

もう一つは、クリック感(デテント)があるかどうかといった点です。

手で操作するロータリーエンコーダは、操作している人が、その操作量を知覚しやすいように、 クリック感でフィードバックできるものと、そうしたクリック感がないものがあります。

クリック感があるタイプは、回転量にあわせてカチカチとフィードバックがあるのですが、 オーディオ機器のボリュームなどは、そうしたクリック感が無いほうが、高級感が得られます。

こうした観点から、「寿命」「サイズや形状」「クリック感有無」あたりを踏まえて選択します。

加えて、回転の重さや、クリック感の強度、回転軸の物理的強度、シャフトの太さや取り付けられるつまみの種類、 全体の質感なども、使い勝手などに影響するので、その辺りも加味しておくとよいでしょう。