Arduinoでロータリーエンコーダを使いたい話

#概要
Arduinoでロータリーエンコーダが使いたいです。
外部割り込みを使ったコードは多々ありますが、Arduinoで外部割り込みが使えるのは2つのピンのみで、たくさん実装できないのです。なので、外部割り込みを使わないコーディングでチャタリング除去を目指します。一応ですけども、手を使って回すのを期待しているので、アホみたいに高速に回した場合は想定していませんのであしからず。

#コード1
##コード

```
//ピン番号宣言
int const ENCA = 2;
int const ENCB = 3;

//変数の宣言
byte valenc;
byte val;

void setup(void){
  pinMode(ENCA,INPUT_PULLUP);
  pinMode(ENCB,INPUT_PULLUP);
  Serial.begin(9600);
}
 
void loop(void){
  valenc = valenc<<2 & B00001100;
  valenc += (digitalRead(ENCA)<<1) + (digitalRead(ENCB));
  
/*
L 0010 1011 1101 0100
R 0001 0111 1110 1000
*/

  switch(valenc){
    //反時計回り
    case B00000010 : //fallthrough
    case B00001011 : //fallthrough
    case B00001101 : //fallthrough
    case B00000100 :
      if(val > 0){
      val--;
      }
      Serial.print("L ");
      Serial.println(val);      
      break;
      
    //時計回り
    case B00000001 : //fallthrough 
    case B00000111 : //fallthrough 
    case B00001110 : //fallthrough 
    case B00001000 : 
      if(val < 255){
      val++;
      }
      Serial.print("R ");
      Serial.println(val);      
      break;

    default: 
      break;
  }
}
```

##解説
基本的なコードはこんな感じ。意外?にも短いコードで読み取れるんだなぁと思いました()。
順に追って説明。

```
//ピン番号宣言
int const ENCA = 2;
int const ENCB = 3;

//変数の宣言
byte valenc;
byte val;

void setup(void){
  pinMode(ENCA,INPUT_PULLUP);
  pinMode(ENCB,INPUT_PULLUP);
  Serial.begin(9600);
}

void loop(void){
//...
```

AB2相のインクリメンタルロータリーエンコーダ(ノンクリックタイプ)を使いました。あとあとのコードでわかりますが、クリックタイプのものを使うと一度で4回分処理されてしまうので注意です。A相B相をそれぞれデジタルピンに接続します。今回は外部割り込みを使わないので、使うピンは何番でも大丈夫です。今回は2,3番を使いました。今回はArduinoで[内部プルアップ](http://mag.switch-science.com/2013/05/23/input_pullup/)を行なっているので、CピンはGNDに繋ぎます。
変数は2つ。valencはエンコーダの2相の状態を格納するための変数です。ここの処理が大事になってきます。valはただの変数で今回はエンコーダをまわすと0から255の値が動く、と言うためだけのものです。つまりvalencさえあればエンコーダは機能します。

```
  valenc = valenc<<2 & B00001100;
  valenc += (digitalRead(ENCA)<<1) + (digitalRead(ENCB));
  
```

loop()内でははじめにA相B相の状態を確認します。
2行目ではA相B相の状態を読み取ります。A相を1ビット上にあげているので、状態としては(BINで)00,01,11,10の4種類になります。ロータリーエンコーダが動いたかどうかの判断は、古い情報と新しい情報の比較をしないといけないので、1行目で前回読み取った値をoldの値として格納し直しています。なので、valencは0000から1111の状態になることになります。

```
/*
L 0010 1011 1101 0100
R 0001 0111 1110 1000
*/
```

ここで、ロータリーエンコーダの回転方向を確認します。下位3,4ビット目が古い情報、下位1,2ビット目が新しい情報です。(A,B相が) 00から10, 10から11, 11から01, 01から00になる方向が反時計回り、(A,B相が) 00から01, 01から11, 11から10, 10から00になる方向が時計回りになるということです。内部プルアップをした場合しない場合、AB相を逆につないだ場合はLRが逆になりますので適宜確認しながら調整していきます。
また、ここに書いていないものは2種類あり、0000や0101などは古い情報と新しい情報が同じなので動いていないということ、0011や1001などAB相がどちらも変わっているものは本来起こりえないもので情報の読み取りに取りこぼしがおきたということです。

```
  switch(valenc){
    //反時計回り
    case B00000010 : //fallthrough 
    case B00001011 : //fallthrough 
    case B00001101 : //fallthrough 
    case B00000100 : 
      if(val > 0){
      val--;
      }
      Serial.print("L ");
      Serial.println(val);      
      break;
      
    //時計回り
    case B00000001 : //fallthrough 
    case B00000111 : //fallthrough 
    case B00001110 : //fallthrough 
    case B00001000 : 
      if(val < 255){
      val++;
      }
      Serial.print("R ");
      Serial.println(val);      
      break;

    default: 
      break;
  }
}
```

条件分岐です。今回は高々8つの状態しかないので、caseを書き出しました。もっと多い場合はbit比較をすることになったり、まとめたりしますが。
反時計回りの場合はval--で値を減らし、時計回りの場合はval++で値を増やします。0-255におさめたいので、その場合に限ることをif文に入れ子にし、その後にSerial.printで情報を書き出します。
先述の通り、反時計回りでも時計回りでもない場合はdefaultで逃します。例えばここに```Serial.println("S ");``` (Stop)とでも入れておけばよりわかりやすい情報が見えるかもしれません。

##問題点と解決策
基本に忠実にコーディングすると意外にもちゃんと動いてくれてはいるのですが、時々時計回りにまわしても反時計回りに、その逆もまたしかり、LLRLLRLL...やRLRRRLR...といったようにチャタリングが起こってしまいます。そこでちょこっとだけ改良してチャタリングを最低限に抑える方向に持っていきます。

チャタリングに関しては01の変わり目で起こるので、LLLLRRLLのように逆方向が2回続くことは基本的にはありません。なので2個前までの情報から判断して、LLRとなる場合とRRLとになる場合だけを取り出して、これらを排除してやればOKということになります。この方法を用いるとLLLLSRSSS...となる場合や、LLLLSRLLLLとなる場合などが対処できないので完全に取り除くことは難しそうですが、ご愛嬌ということで。
LLRとRRLを排除すると、例えば「(意図的な)1目盛だけの微調整ができなくなるのでは?」というような疑問が起こるかもしれませんが、そんな微調整をするほどの時はエンコーダをゆっくり回すことになるので、LSSSSSRSSSSSLSSSSのようにStopが入るので、LLRやRRLの場合との区別がつくので問題ありません。loopサイクルの速さで起こるLLRやRRLとなる時だけ取り除けばいいのです。

#コード2
##コード

```
//ピン番号宣言
int const ENCA = 2;
int const ENCB = 3;

//変数の宣言
byte valenc;
byte val;

void setup(void){
  pinMode(ENCA,INPUT_PULLUP);
  pinMode(ENCB,INPUT_PULLUP);
  Serial.begin(9600);
}

void loop(void){
  valenc = valenc<<2 & B00111100;
  valenc += (digitalRead(ENCA)<<1) + (digitalRead(ENCB));

/*
L 0010 1011 1101 0100
R 0001 0111 1110 1000
*/

  switch(valenc & B00001111){
    //反時計回り
    case B00000010 : //fallthrough
    case B00001011 : //fallthrough
    case B00001101 : //fallthrough
    case B00000100 : 

      //RLとなる場合
      if((bitRead(valenc, 5) == bitRead(valenc, 1))&&(bitRead(valenc, 4) == bitRead(valenc, 0))){
      } else{
        if(val > 0){
        val--;
        }
        Serial.print("L ");
        Serial.println(val);     
      }
      break;

    //時計回り
    case B00000001 : //fallthrough
    case B00000111 : //fallthrough
    case B00001110 : //fallthrough
    case B00001000 : 

        //LRとなる場合
        if((bitRead(valenc, 5) == bitRead(valenc, 1))&&(bitRead(valenc, 4) == bitRead(valenc, 0))){
      } else{
        if(val < 255){
        val++;
        }
        Serial.print("R ");
        Serial.println(val);    
      }
      break;
        
    default: 
      break;
  }
}
```

##解説
変更点はほとんどありません。

```
void loop(void){
  valenc = valenc<<2 & B00111100;
//...
```
loop()1行目のoldを2つ前まで読み込めるようにしました。

```
  switch(valenc & B00001111){
    //反時計回り
//...
```

switchでの分岐時に1つ前と現在だけの比較にするために、& B00001111を追加します。

```
        if((bitRead(valenc, 5) == bitRead(valenc, 1))&&(bitRead(valenc, 4) == bitRead(valenc, 0))){
      } else{
//...
```

チャタリングがおきると、例えばLLLLR...となると、2つ前と現在のAB相の値が同じになります。なので、2つ前と現在を比較し、同じならば何もしない、違うならばカウントするというようにします。if条件を !=と||を使って逆にしてもいいです。そっちの方がスッキリするかもしれません。

##問題点と解決策2
これでだいぶチャタリング頻度は落ちましたが、完全ではありません。Serial.printをしているのでloop()処理の時間がかかっているので頻度が少ないように見えますが、これを除いたらもっと頻度が高くなる可能性もあります。逆にloop()内が長くなってくると、チャタリング頻度は低くなるものの、今度は取りこぼしがおおくなる可能性があります。
もう少し考えるべき点はありそうですけども、とりあえずはこれでokかな、と。

#関数化
##コード

```
//ピン番号宣言
int const ENCA = 2;
int const ENCB = 3;

int val;
int checkval;

//変数の宣言

void setup(){
  pinMode(ENCA,INPUT_PULLUP);
  pinMode(ENCB,INPUT_PULLUP);
  Serial.begin(9600);
}

void loop(){
  checkval = checkEnc(digitalRead(ENCA),digitalRead(ENCB));
  
  switch(checkval){
    case 2 : 
      val--;
      break;
      
    case 1 :
      val++;
      break;
      
    default : 
      break;
  }
  Serial.println(val);
}

int checkEnc(int x,int y){
  static byte valenc;
  byte state;

  valenc = valenc<<2 & B00111100;
  valenc += (x<<1) + (y);

  switch(valenc & B00001111){
    case B00000010 : //fallthrough
    case B00001011 : //fallthrough
    case B00001101 : //fallthrough
    case B00000100 : 

      if((bitRead(valenc, 5) == bitRead(valenc, 1))&&(bitRead(valenc, 4) == bitRead(valenc, 0))){
      } else{
        state = 2;
      }
      break;

    case B00000001 : //fallthrough
    case B00000111 : //fallthrough
    case B00001110 : //fallthrough
    case B00001000 : 

        if((bitRead(valenc, 5) == bitRead(valenc, 1))&&(bitRead(valenc, 4) == bitRead(valenc, 0))){
      } else{
      state = 1;  
      }
      break;
        
    default: 
      state = 0;
      break;
  }
  return state; //L = 2, R = 1, S = 0;
}
```

loop()内に書いていたコードをcheckEnc()という関数におさめてloop()を簡略化しました。
これでタイマー処理と同様に使うことができるので、チャタリング対策ももうすこしできそうです。