第二十一項 ロータリーエンコーダとノイズ対策・割り込み

電子工作創作表現(2019/12/04)

スライドPDF

ロータリーエンコーダ

  • 無限回転できるツマミ
     
  • パラメータの選択などに使える
今回ロータリーエンコーダを紹介します。パーツとしては地味でちゃんと使おうとすると意外と込み入っているのですが、使えるようになっておくと便利なのでやっておこうと思います。

ロータリーエンコーダの機能

  • 無限に回転させられる
     
  • ボリュームのようなアナログ値ではなく、デジタル信号
パッと見ボリュームとほぼ同じ見た目ですが、ロータリーエンコーダは無限に回すことができるので、1回転以上する必要があるような場面で使われます。そのため、ボリュームのようなアナログの値ではなく一定の角度ごとにカウントされるデジタルの信号をやりとりすることになり、analogread()のように一行でサクサクとはいかず、ある程度プログラムを書いてあげる必要があります。

ロータリーエンコーダの用途

  • 選択肢が可変や大量にある時の選択ツールとして
     
  • 機械で動作する物の移動量を知る装置として
ミキサーのボリュームのように、パラメータの最小最大レンジが決まっているような場合はボリュームが使えますが、何かたくさんある選択肢の中から選ぶ機能のように、何回転もさせたい場合に使われます。また、機械に取り付けて回転を計測し、モーターや人がどのくらい物を動かしたかという事を検知する時にも使えます。

ロータリーエンコーダの種類

  • 機械式と光学式
     

  • インクリメンタルとアブソリュート

  • 主なメーカーはALPSとオムロン

エンコーダの種類には、まず機械式と光学式というものがあります。スイッチが中に入っていて、回るとスイッチのON/OFFが切り替わる方式が機械式、光センサーが中に入っていて、円盤の動きを読み取って状態を知るのが光学式で、光学式の方が比較的大きく高価ですが、信号が安定していて摩耗などの劣化もしにくいというメリットがあります。

回転の数え方にも種類があります。一定の角度動いたら1カウントするという相対的な数え方をするのがインクリメンタル、ボリュームに似ていますが今どの角度を向いているのかが分かるというのがアブソリュート方式です。

ALPS電気のEC12シリーズ

  • 秋月で80円ほど
     
  • クリック/ノンクリックタイプや、高さも違うものなど様々
今回は安価で手に入りやすい、ALPS電気の機械式インクリメンタルのエンコーダを実演します。秋月で手に入るもので、個人で入手でいるもので最もオーソドックスな部品です。

シンプルに接続

  • A相・B相から出力されるので、それぞれをプルアップで入力する
     
  • おおまかな値は取れるが、「チャタリング」ノイズが問題になりやすい
ロータリーエンコーダで値を正確に取るには、少し回路が複雑なので、順を追って説明します。まずシンプルにArduinoと直結しようとすると、真ん中のピンがCOM=GNDピンになるので、こうなります。今回2番3番ピンにつないでいますが、UNOの場合2番3番が良いです。理由は後述します。

これで信号は取れるのですが、機械式は「チャタリング」と言われるノイズが発生してしまうため、正確な回転数が取れません。

ロータリーエンコーダの信号

エンコーダから来る信号をグラフ化したものです。赤と青の線がそれぞれA相とB相のHIGH-LOWを表しており、回転させたときにこのように交互に上下する状態が理想です。

チャタリングノイズ

ところが、機械式のエンコーダでは動いた時にHIGHとLOWの境目で、信号がパタパタと暴れることがあります。これが「チャタリングノイズ」と呼ばれるもので、プログラムで回転数をカウントしようとした時にこのノイズのせいで余計にカウントされてしまいます。
参考:https://www.marutsu.co.jp/pc/static/large_order/1405_311_ph

ノイズ対策1:コンデンサ

それを回避するための方法として、まずひとつ目は信号線とGNDの間にコンデンサを入れてあげる方法です。コンデンサは電荷を貯めたり放出したりできる素子で、急峻なノイズをある程度吸収してくれるという性質があります。これを入れるだけでも大分信号が整ってきます。コンデンサにはどのくらい電気を貯められるかがF(ファラッド)という単位で示されていますが、このロータリーエンコーダの場合は0.01μF(マイクロファラッド)のコンデンサを入れるのがデータシートで推奨されています。

ノイズ対策2:シュミットトリガ

更に堅牢な作りにしたいという場合は、シュミットトリガを入れるとより信号が安定します。これはHIGH/LOWのしきい値にある程度バッファを持たせるためのもので、通常のデジタルピンは例えば5Vの半分である2.5V以上ならHIGH、それ以下ならLOWという判断をします。しかしそれだと、2.5V付近をちょっと行ったり来たりした場合またチャタリングのような信号のブレが出てしまうので、例えば4V以上ならHIGH、1V以下ならLOWに切り替わり、それ以外の時は以前の状態を維持するという振る舞いをさせることができるのがこのシュミットトリガで、そのような性質を「ヒステリシス」と呼んだりもします。

カウント用プログラム

  • プログラム内でカウントする

  • loopの中でカウントできるが、取りこぼしが起こる

このようにして回路を接続したら、以下のようなコードを書いて数をカウントしていきます。ロータリーエンコーダの方で数字を出してくれるわけではないので、Arduino側で処理する必要があります。2つの状態は0~3まで4種類あり、時計回りは1->3->0->2...となり、反時計周りは2->0->3->1...と繰り返されていきますので、現在の状態とその一個前の状態を比較することで、counterの値が増減するようになっています。

これでもある程度の速度はカウントできるのですが、実は取りこぼしが起きてしまいます。あくまで現在の状態を拾えているのはdigitalRead()を呼んでいる瞬間だけなので、素早く回したり、ロータリーエンコーダのカウント以外の処理が長くなったりすると、カウントを取りこぼすようになってしまいます。
bool pinA = false;
bool pinB = false;
byte current = 0;
byte previous = 0;
long counter = 0;

int cw[] = {1, 3, 0, 2};
int ccw[] = {2, 0, 3, 1};

void setup() {
  pinMode(2, INPUT);
  pinMode(3, INPUT);

  Serial.begin(115200);
}

void loop(){

  //現在の状態を取得
  pinA = digitalRead(2);
  pinB = digitalRead(3);

  //2ビットで0~3の数字に変換
  current = pinA + pinB * 2;

  //currentの値が、CW/CCWのどちらか期待する方向だったらカウントを増減
  if (current == cw[previous]) counter++;
  if (current == ccw[previous]) counter--;

  //直前の値として記憶
  previous = current;

  Serial.println(counter);
}

割り込みを入れる

  • loop中にReadするのではなく、ピンの変化で関数を呼び出す
     
  • 重い処理をしていても、中断して実行される
そういった時のために、実はArduinoには「割り込み」という機能がついています。UNOの場合は2番ピンと3番ピン、Arduino Leonardoの場合は0,1,2,3,7などのピンにその機能がついていて、そのピンのHIGH/LOWが変化した瞬間に用意されたプログラムを実行するというもので、取りこぼしたくない信号の変化を取る時にとても便利な機能です。

割り込みを入れた場合のコード

  • attachInterrupt()関数で宣言する
     
  • 割り込み内で使う変数にはvolatileをつける
先程のコードのloopにあった部分を、割り込み用関数rotaryとして別に定義します。2番ピン、3番ピンに変化があった時にすぐこの関数を呼び出すようattachInteerruptという命令を呼び出して、ピンと関数を関連付けます。こうすることで、loopの中でdelay関数のようにプログラムの流れを長時間専有する処理を加えたとしても、エンコーダが動くたびにカウント処理が挟まるので正確に動かした量を検知することができます。
volatile bool pinA = false;
volatile bool pinB = false;
volatile byte current = 0;
volatile byte previous = 0;
volatile long counter = 0;

int cw[] = {1, 3, 0, 2};
int ccw[] = {2, 0, 3, 1};

int input_A = 2;
int input_B = 3;

void setup() {
  pinMode(input_A, INPUT);
  pinMode(input_B, INPUT);
  Serial.begin(9600);

  //割り込みを宣言
  attachInterrupt(digitalPinToInterrupt(input_A), rotary, CHANGE);
  attachInterrupt(digitalPinToInterrupt(input_B), rotary, CHANGE);

  //初期位置を取得
  rotary();
}

void loop(){
  delay(500);
  Serial.println(counter);
}

void rotary()
{
  //現在の状態を取得
  pinA = digitalRead(input_A);
  pinB = digitalRead(input_B);

  //2ビットで0~3の数字に変換
  current = pinA + pinB * 2;

  //currentの値が、CW/CCWのどちらか期待する方向だったらカウントを増減
  if (current == cw[previous]) counter++;
  if (current == ccw[previous]) counter--;

  previous = current;
}