Arduinoでロータリーエンコーダ入力を扱う

 アルプス電気のインクリメンタル型ロータリエンコーダEC12E2420801を秋月の店頭で購入しました。
ロータリーエンコーダ(24クリックタイプ): パーツ一般 秋月電子通商 電子部品 ネット通販

 データシートはついておらず、通販ページにもダウンロードリンクが掲載されておらず、ピン配置が解りません。が、調べてみると製造元のALPSのサイトに掲載されていました。左からA相、Common、B層の順のようです。
EC12シリーズ|EC12E2420801|基本情報

 今回はこのロータリエンコーダArduinoの入力デバイスとして使ってみます。
ALPS EC12E2420801
 

インクリメンタル型ロータリエンコーダの出力

 今回使用するEC12E2420801に限らず、一般にA相、B相の2つの出力があります。ロータリエンコーダを回転させると下図のようにA相とB相で90度位相がズレた波形が得られます。逆回転させると逆方向にズレた波形が得られます。
Rotary Encoder Pulse

 現在の状態だけを見ても有意な情報は得られませんが、直前の状態と現在の状態を比較することで、どちらに回転させたかを識別することが可能です。
 この特性から、右回転なら+1、左回転なら-1といったように回転方向によってデータをインクリメント/デクリメントするような用途で使用されます。例えば、デジタルカメラの絞りやシャッタースピードを変更するダイヤルで設定値を増減させたり、デジタルシンセサイザーのデータ入力ホイールで値を増減させるような用途で使われています。

 以下、回転方向を検出する方法を具体的に記述します。
 

インクリメンタル型ロータリエンコーダの回転方向の識別

 先の図に、クリックストップ毎に色を付けたものが以下の図になります。波形の1周期毎にA相B相の信号状態の組み合わせには4つの状態が存在し、それが繰り返されることが解ります。
Rotary Encoder Pulse colored by click-stop

 ロータリエンコーダを時計回り(CW; Clockwise)に回転させた場合、図の右方向に波形が進み、反時計回り(CCW; Counter Clockwise)に回転させた場合に図の左方向に波形が進みます。これは下表のような状態変化の繰り返しで表現することができます(検出したいのは回転方向であるため、どの状態から始まるかは重要ではありません)。

  • CW時のパルス
Step #1 #2 #3 #4
Phase A 0 0 1 1
Phase B 0 1 1 0
  • CCW時のパルス
Step #1 #2 #3 #4
Phase A 1 1 0 0
Phase B 0 1 1 0

 
 Phase A/Bの状態の4通りの組み合わせはCW, CCWいずれの場合にも登場するため、現在の状態だけでは回転方向は識別できません。
 そこで、直前の状態と現在の状態を(Step#1と#2、#2と#3という具合で)組み合わせると、下表が得られます。

  • CW
Step #1 #2 #3 #4
Previous Phase A 1 0 0 1
Previous Phase B 0 0 1 1
Current Phase A 0 0 1 1
Current Phase B 0 1 1 0
Code 8 1 7 14
Step #1 #2 #3 #4
Previous Phase A 0 1 1 0
Previous Phase B 0 0 1 1
Current Phase A 1 1 0 0
Current Phase B 0 1 1 0
Code 2 11 13 4

 ここでCodeとしたのはPrevious Phase A/B, Current Phase A/Bの状態を2進数として捉えた場合の10進数表現値です。直前の状態と現在の状態の組み合わせはCW, CCWですべて異なり重複しません。故にこの組み合わせを用いれば回転方向を識別することが可能となります。

 CWの場合をインクリメント、CCWの場合をデクリメントとして真理値表に整理すれば、以下のようになります。

Code Prev.A Prev.B Curr.A CurrB INC DEC Note
0 0 0 0 0 0 0 THIS PATTERN IS NOT EXIST
1 0 0 0 1 1 0 CW
2 0 0 1 0 0 1 CCW
3 0 0 1 1 0 0 THIS PATTERN IS NOT EXIST
4 0 1 0 0 0 1 CCW
5 0 1 0 1 0 0 THIS PATTERN IS NOT EXIST
6 0 1 1 0 0 0 THIS PATTERN IS NOT EXIST
7 0 1 1 1 1 0 CW
8 1 0 0 0 1 0 CW
9 1 0 0 1 0 0 THIS PATTERN IS NOT EXIST
10 1 0 1 0 0 0 THIS PATTERN IS NOT EXIST
11 1 0 1 1 0 1 CCW
12 1 1 0 0 0 0 THIS PATTERN IS NOT EXIST
13 1 1 0 1 0 1 CCW
14 1 1 1 0 1 0 CW
15 1 1 1 1 0 0 THIS PATTERN IS NOT EXIST

 さらに、Codeを要素番号として配列表現すれば以下のようになります。

// Array to detect Inc/Dec of rotary encoder
// Note: Non-existing code value is treated as zero.
//                         Code : 0, 1,  2, 3,  4, 5, 6, 7, 8, 9,10, 11,12,13,14,15,16
    static const int iRotPtn[] = {0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0,-1, 1, 0, 0};

 ここまで整理できればソフトウェアから容易に扱うことができますね。
 

Arduinoロータリエンコーダを扱う実装例

 ロータリエンコーダの状態変化をトリガに割り込み処理として実装すると以下のようになります。

// Specify pin# where Rotary encoder connected to
#define pinRotEncA  2
#define pinRotEncB  3

// Array to detect Inc/Dec of rotary encoder
// Note: Non-existing code value is treated as zero.
//                     Code : 0, 1,  2, 3,  4, 5, 6, 7, 8, 9,10, 11,12,13,14,15,16
static const int iRotPtn[] = {0, 1, -1, 0, -1, 0, 0, 1, 1, 0, 0, -1, 0,-1, 1, 0, 0};

// Previous PhaseA/B state
volatile bool bPrevA = false;
volatile bool bPrevB = false;

// Value
volatile int val= 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Rotary encoder test");

  // Set pins as input for rotary encoder
  pinMode(pinRotEncA, INPUT);
  pinMode(pinRotEncB, INPUT);
  // Pull-up
  digitalWrite(pinRotEncA, HIGH);
  digitalWrite(pinRotEncB, HIGH);

  // Set Interrupt
  attachInterrupt(0, intRotEnc, CHANGE);
}

void loop() {
  // put your main code here, to run repeatedly:
}

void intRotEnc(){
  // Current PhaseA/B state
  bool bRotA, bRotB;

  // Pattern code
  unsigned int iPtnCd;

  // Get Current PhaseA/B state
  bRotA = digitalRead(pinRotEncA);
  bRotB = digitalRead(pinRotEncB);

  // Encode the states to Pattern code
  iPtnCd = 0x0f & (bPrevA << 3 | bPrevB << 2 | bRotA << 1 | bRotB);
  
  switch(iRotPtn[iPtnCd]){
    case -1:
      val++;
      break;
    case 1:
      val--;
      break;
    default:
      break;
  }

  Serial.println(val);
  
  // Store current state to previous
  bPrevA = bRotA;
  bPrevB = bRotB;
}

 とりあえずこれでロータリエンコーダ入力を扱うことができます。但し、シリアルコンソールを眺めていると同値が連続して表示され、存在しないパターンの状態変化が発生していることに気付くと思います。
 これはハードウェア的にチャタリング対策を行っていないために発生していると考えられます。
 EC12E2420801の場合3ms程度待てば安定するようですが、ソフトウェア的にチャタリング対策を施そうにもArduinoの割り込み処理関数中ではdelay関数は使えません。ロータリエンコーダの状態変化をloop()中で監視しハンドリングするような実装とすれば簡単に対策できそうではありますが、loop()内の処理内容によってはロータリエンコーダ操作の取りこぼしが発生してしまうため、一長一短といったところでしょうか。
 真面目に対策するならLPFを通すなどハードウェアで対策を行う必要がありますが、個人レベルで実用になればいいというレベルのデバイスの入力として使う分にはこのままでも十分*1かと思います。
 



以上。

*1:昔のSONYジョグダイヤルが劣化してきた感じに近い操作感。