Release 2017.10.22 / Update 2018.9.25

ロータリーエンコーダを使うpart 3 : “Dual Encoder”

part 1part 2と、Arduinoでロータリエンコーダを使うためのスケッチを試行錯誤してきましたが、2つの記事で得た知恵を統合して、新たなスケッチを書いてみました。

名づけて「Dual Encoder」。

今回書いたスケッチで目指したのは、

  • デュアルエンコーダ(複数のエンコーダ値を読み取る)
  • 「外部割込み」ではなく「タイマー割り込み」で
  • クリック有り・無し対応

です。

前回の問題点解決に試行錯誤した結果、精度が程よく改善されたかと思います。特にクリックなしタイプに対しては、自分的に満足出来るレベルになりました。

ただし、完全にチャタリングを排除できたわけではないし、状況によっては他のタイマーと競合してスケッチ全体のレスポンスに影響を与える可能性もあります。また、クリックタイプへの対応は4の倍数で強制しているだけなので、バタつきも出ます。

なので、クリックタイプ&外部割込みで事足りるのであれば、そっちを使ったほうが安定しているかな、と(part 1 参照)。まあ、一度試して判断してもらえれば…。

本来ならライブラリにしたほうが便利なんでしょうけど、技量・時間不足なのでスケッチファイルです。コードを直接書き換えて使ってください。悪しからず。

回路図

このスケッチを試すのに必要なパーツは、

  • ArduinoUNO
  • ロータリーエンコーダ×2
  • (手元にあれば)128*64 OLED

ロータリーエンコーダは、2相とGNDの3ピンになっている安いタイプで大丈夫です。可能ならpart 2でやっていた「クリックなし」を用意できると、細かいエンコーダ操作が出来るので面白いか、と。

OLEDとu8glibの設定部分はこちらを参考にしてください。

あるいは、今回のスケッチはシリアルプリントにも返しているので、OLEDは用意しなくてもシリアルモニタでエンコーダ値を確認できます。

スケッチ

プロジェクトファイルをダウンロード、又は下記のコードをコピペするかして、Arduinoに書き込んでください。

Download – 「Dual encoder ver 1.0

//   jumbleat.com                                 //
//                                                //
//           dual encoder version 1.0             //
//                                   by jumbler   //
//                                   2017.10.20   //



#include <MsTimer2.h>
#define TIM MsTimer2

#define OLED_DRAW 1

#if(OLED_DRAW)

#include <U8glib.h>
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // OLED / I2C / TWI
// SDA 4/ SDL 5

#endif

#define ENC_NUM       2
#define ENC_TOLERANCE 25
#define TIMER_INTVAL  2
#define ENC_REPEAT    ENC_TOLERANCE * (TIMER_INTVAL / 2)

#define ENCA_1 4
#define ENCA_2 12
#define ENCB_1 7
#define ENCB_2 8
byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2};


#define ECUR B00000011  // current  enc position
#define EHOM B00001100  // previous enc position
#define ECHG B01000000  // flag that encoder has changed
#define ERES B10000000  // encoder resolution. '1' is for quarter resolution
volatile int  enc_count[ENC_NUM];
volatile int  enc_old[ENC_NUM];
volatile byte enc_status[ENC_NUM];



byte ELAYER(byte layer) {
  for (byte i = 0 ; i < 8 ; i++) if ((layer >> i) & B00000001) return i;
}
byte EMASK(byte val, byte layer) {
  byte tmp = (val & layer) >> ELAYER(layer);
  return tmp;
}

bool ENC_CHECK() {
  bool yes_no = false;
  for (byte i = 0 ; i < ENC_NUM ; i++) yes_no |= ENC_CHECK(i);
  return yes_no;
}

bool ENC_CHECK(byte pin) {
  bool yes_no = enc_status[pin] & ECHG;
  return yes_no;
}

void SET_ENC_RES(byte pin, bool res) {
  if (pin < ENC_NUM)
  {
    enc_status[pin] = (enc_status[pin] & ~ERES) | (res << ELAYER(ERES));
    enc_status[pin] &= ~ECHG;
  }
}

void ENC_RESET() {
  for (byte i = 0 ; i < 5 ; i++) ENC_READ();
  for (byte i = 0 ; i < (ENC_NUM) ; i++)
  {
    enc_status[i] |= ECHG;  // kick flag
    enc_count[i]   = 0;
    ENC_COUNT(i);
    enc_status[i] |= ECHG;  // kick flag
  }
}

int ENC_COUNT(byte pin) {

  int  enc_val = 0;
  char vec     = (enc_status[pin] & ERES) ? 4 : 1;

  if (enc_status[pin] & ECHG)
  {
    enc_val  = (enc_count[pin] - enc_old[pin]) / vec;

    enc_old[pin] = enc_count[pin];  //update as previous value
    enc_status[pin] &= ~ECHG;       // reset counting flags
  }
  return enc_val;
}


void ENC_READ() {
  static byte enc_gauge[ENC_NUM];

  // read pins value
  for (byte ii = 0 ; ii < ENC_REPEAT ; ii++)
  {
    short pin_val[ENC_NUM];

    // get current pins status of Encoder1
    pin_val[0]  = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1
    pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0);      // ENC A-2

#if(ENC_NUM > 1)

    // get current pins status of Encoder2
    pin_val[1]  = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1
    pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0);      // ENC B-2

#endif


    // for each encoder pins
    for (byte i = 0 ; i < ENC_NUM ; i++)
    {
      // modify order of pins value
      if (pin_val[i] < 2) pin_val[i] = 1 + (pin_val[i] * -1);
      enc_status[i] = (enc_status[i] & ~ECUR) + pin_val[i];

      short pos_old = EMASK(enc_status[i], EHOM);

      if (pin_val[i] != pos_old)
      {
        //gauging
        enc_gauge[i] = min(ENC_TOLERANCE + 1, enc_gauge[i]++);

        //counting
        if (enc_gauge[i] >= ENC_TOLERANCE)
        {
          // increase or decrease ?
          bool dir = (pin_val[i] > pos_old) ? 1 : 0;
          if (pin_val[i] == 0 && pos_old == 3) dir = 1;
          else if (pin_val[i] == 3 && pos_old == 0) dir = 0;

          // add count by the direction
          if (dir) enc_count[i]++;
          else enc_count[i]--;

          boolean change_flag = false;

          // forced count correction for Click-type
          if (enc_status[i] & ERES)
          {
            if (pin_val[i] == 3)
            {
              char rem = enc_count[i] % 4;
              if (rem != 0)
              {
                enc_count[i] = (enc_count[i] / 4) * 4;
                if (abs(rem) > 2)
                {
                  char vec = (enc_count[i] < 0) ? -1 : 1;
                  enc_count[i] += 4 * vec;
                }
              }
              change_flag = true;
            }
          } else {
            change_flag = true;
          }

          if (enc_count[i] == enc_old[i]) change_flag = false;

          // update current pos to home pos
          enc_status[i] = (enc_status[i] & ~EHOM) | (pin_val[i] << ELAYER(EHOM));
          // set count change flag
          enc_status[i] |= change_flag << ELAYER(ECHG);
          enc_gauge[i] = 0;
        }
      } else {
        enc_gauge[i] = max(0, enc_gauge[i]--);
      }
    }
  }
}



void setup() {
  for (byte i = 0 ; i < (ENC_NUM * 2) ; i++) pinMode(enc_pins[i], INPUT_PULLUP);

  SET_ENC_RES(0, 1);   // SET_ENC_RES(encoder number, click = 1 / non_click = 0)
  SET_ENC_RES(1, 1);   // SET_ENC_RES(encoder number, click = 1 / non_click = 0)

  TIM::set(TIMER_INTVAL, ENC_READ);
  TIM::start();

  Serial.begin(38400);

  char title[2][21] = {"Dual Encoder ver 1.0", "by jumbleat.com"};
  Serial.println(title[0]);
  Serial.println(title[1]);

#if(OLED_DRAW)

  u8g.setColorIndex(1);              // OLED pixel on
  u8g.setFont(u8g_font_orgv01r);     //719 Byte  / 11x11 /*

  u8g.firstPage();
  do {
    u8g.setPrintPos(15, 20);
    u8g.print(title[0]);
    u8g.setPrintPos(50, 55);
    u8g.print(title[1]);
  } while (u8g.nextPage());

#endif

  delay(3000);
  
  ENC_RESET();
}



void loop() {
  static int val[ENC_NUM];

  if (ENC_CHECK())
  {
    for (byte i = 0 ; i < ENC_NUM ; i++)
    {
      val[i] += ENC_COUNT(i);
      Serial.print((char)('A' + i));
      Serial.print(":");
      Serial.print(val[i]);
      Serial.print("  ");
    }
    Serial.println();


#if(OLED_DRAW)

    // --- OLED Draw
    int val_draw[ENC_NUM];
    for (byte i = 0 ; i < ENC_NUM ; i++) val_draw[i] = constrain(val[i], -64, 64);

    u8g.firstPage();
    do {
      for (byte i = 0 ; i < ENC_NUM ; i++)
      {
        byte space = 16;
        u8g.drawFrame(0, i * space, 127, 4);
        u8g.drawBox(min(64, 64 + val_draw[i]), i * space, abs(val_draw[i]), 4);
        u8g.setPrintPos(0, 10 + i * space);
        u8g.print(val[i]);
      }
    } while (u8g.nextPage());
    // --- OLED Draw end

#endif

  }
}

注意点

タイマー割り込み

このエンコーダ読み取りスケッチは「MsTimer2」というタイマー割り込み用のライブラリを使っています。(part 2を参考に)IDEへ組み込んでください。

もし、他のタイマーを使いたい場合、スケッチ内を書き換えれば対応できるとは思います。

#include <MsTimer2.h>
#define TIM MsTimer2

#define OLED_DRAW 1
void setup() {
  for (byte i = 0 ; i < (ENC_NUM * 2) ; i++) pinMode(enc_pins[i], INPUT_PULLUP);

  SET_ENC_RES(0, 1);   // SET_ENC_RES(encoder number, click = 1 / non_click = 0)
  SET_ENC_RES(1, 1);   // SET_ENC_RES(encoder number, click = 1 / non_click = 0)

  TIM::set(TIMER_INTVAL, ENC_READ);
  TIM::start();

ポート操作

今回のスケッチはdigitalRead関数ではなく、直接ポートを読みに行くようにしました。この方法だと割り込みに割く時間を節約できるからです。

なので、エンコーダに接続しているピン設定は基本変更出来ないと考えてください。また、ArduinoUNO系統(ATmega328)以外のマイコン、つまり、ポート配置が違うタイプも、このままでは動かない場合があると思います。

ピンの設定を変更したい場合は、自分で調べて、ポート読み取りの部分を書き換えてください。

PIN# & _BV(#)の部分です。

参考 ArduinoUNOのポート配置

void ENC_READ() {
  static byte enc_gauge[ENC_NUM];

  // read pins value
  for (byte ii = 0 ; ii < ENC_REPEAT ; ii++)
  {
    short pin_val[ENC_NUM];

    // get current pins status of Encoder1
    pin_val[0]  = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1
    pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0);      // ENC A-2

#if(ENC_NUM > 1)

    // get current pins status of Encoder2
    pin_val[1]  = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1
    pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0);      // ENC B-2

#endif

ちなみに、「ポート読み取り」にリンクしてませんが、最初に#define定義しているピンはpinModeの設定用なので、同様に書き換える必要があります。要注意。

#define ENC_NUM       2
#define ENC_TOLERANCE 25
#define TIMER_INTVAL  2
#define ENC_REPEAT    ENC_TOLERANCE * (TIMER_INTVAL / 2)

#define ENCA_1 4
#define ENCA_2 12
#define ENCB_1 7
#define ENCB_2 8
byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2};

使い方

諸々の関数は下記に詳細を書いておきますが、基本はENC_COUNT()の返り値を変更したい変数に投げるだけです。タイマー開始後なら好きなところに呼び出せます。

スケッチ内の仕組みや構造はこの記事では触れないので、気になる方はpart 1 / part 2を参考にしてください。

もしモサモサ感が気になるなら、それはOLEDを使うu8glibに起因するものなので、define定義した「OLED_DRAW」を0にするか、u8glibに絡む行をコメントアウトして、シリアルモニタだけの確認にしてください。

#include <MsTimer2.h>
#define TIM MsTimer2

#define OLED_DRAW 1

#if(OLED_DRAW)

#include <U8glib.h>
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE | U8G_I2C_OPT_DEV_0); // OLED / I2C / TWI
// SDA 4/ SDL 5

#endif

Dual Encoderの関数

void SET_ENC_RES (byte encoder num, bool resolution);

使うロータリーエンコーダの分解能を設定します。返り値はありません。

クリックのある一般的なエンコーダは、4パターンを1クリック(1カウント)として扱うので、trueにすると、それに倣ったカウントをします。

encoder num エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。
resolution 分解能の指定。
0(false) ノンクリックタイプの高分解能
1(true) クリックタイプ

例えば、1個目のエンコーダをクリックタイプとして扱いたい場合は、

SET_ENC_RES(0, true);

デフォルトの使用はノンクリックタイプを想定しているので、クリックタイプのロータリーエンコーダを使わない場合は、この関数で設定する必要はありません。

boolean ENC_CHECK (); / ENC_CHECK (byte encoder num);

エンコーダの増減があったかをチェックしにいきます。変化があれば1(true)、なければ0(false)を返してきます。

encoder num エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。

ENC_CHECK()場合、全部のエンコーダの中から1つでも増減があればtrueを返します。

ENC_CHECK(#)の場合は、指定したエンコーダのみの増減有無を返します。

この返り値は、ENC_COUNT()関数を呼ぶことでリセットされます。よって、リセットしたくない時、この関数で「増減有無の確認」だけすることができます。

int ENC_COUNT (byte encoder num);

実際のエンコーダ値を取得。前回読んでから増減した+-差分を返り値として返します。

encoder num エンコーダ番号の指定。1個目は(0)、2個目は(1)になります。

例えば、「val」という変数へ1個目のエンコーダ増減を足したいなら、

val += ENC_COUNT(0);

今回のスケッチではu8glibを組み込んでいますが、その中のdo~while分が全体のもたつきを作っていて、思うように更新されていないように見えます。ただ、(チャタリングによる取りこぼしがなければ)現実にロータリーエンコーダが回った分は拾っているハズなので、最終的な変更値はちゃんと得られると思います。

この関数を呼ぶことで、ENC_CHECK()の返り値はリセットされます。

void ENC_RESET()

エンコーダを読み取る上で、実際は「enc_count[]」という変数が裏で連続カウント値を記録しています。そのenc_count[]変数を0にリセットします。この変数はint型にしているので、よほどのことがなければオーバーフローしないと思いますが、使い方によってはその可能性もあるので、そういった危険がありそうな場合は、この関数を適宜組み込んでください。

参考 ) ArduinoUnoでint型が扱える数値範囲 -32768 ~ 32767

define定義値

ENC_NUM

ロータリーエンコーダの個数を指定をします。

1個しか使用しないのであれば、数を減らすことでタイマー割り込みに割く時間を節約できます。 ただし、その場合、ピン配列等も書き換えてください。

#define ENC_NUM       1
#define ENC_TOLERANCE 25
#define TIMER_INTVAL  2
#define ENC_REPEAT    ENC_TOLERANCE * (TIMER_INTVAL / 2)

#define ENCA_1 4
#define ENCA_2 12
//#define ENCB_1 7
//#define ENCB_2 8
byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2};

ENC_TOLERANCE

part 2に同じく、このスケッチは「ゲージ判定」でチャタリング対策しています。その閾値です。

この値はbyte型にしているので、255以上に設定しないでください。

TIMER_INTVAL

タイマー割り込みのインターバル時間(ms)です。

値を小さくするほど、正確な読み取りができるようになると思いますが、その分他のタスクを圧迫することになります。逆に、メインのタスクが上手く動かない場合に大きくすると、負荷が減るかと思います。ただし、だからと言って、大き過ぎてもエンコーダの読み取りが上手くいかなくなります。

ENC_REPEAT

1度のタイマー割り込みで1回のゲージ判定だと割り出しは難しいので、1回割り込んだら何回もピン状態を確認し、ゲージ分量を確保しています。つまり、1回の割り込み当たりに繰り返す「読み取り回数」の数値です。

数値を増やすことで、よりエンコーダの変化を細かく見れるようになりますが、その分他のタスクを圧迫することになります。

こちらも、255以上に設定しないでください。

 

ENC_TOLERANCE、TIMER_INTVAL、ENC_REPEAT、この3つの数値バランスでエンコーダの読み取り精度が変わってくるので、自分のプロジェクトに合わせ、上手く調整してください。

最後に

このスケッチは2つのエンコーダを同時に読めるよう目指したものですが、実は少し書き換えるだけで扱える個数を増やせます。

とりあえず、エンコーダ4個まで試しました。

ただし、割り込みに割く時間も増えるので、他のタスクに影響が出たり、取りこぼしが増えたりするかもしれません。特にPWMを扱うピンとのカブりが出てくると、憂慮することが増えるので厄介です。それでもよければ、と言う感じです。

例えば、3つ目のエンコーダをArduinoのA0、A1ピンに追加する場合、下記のように書き足します。

ピン定義

#define ENC_NUM       3
#define ENC_TOLERANCE 25
#define TIMER_INTVAL  2
#define ENC_REPEAT    ENC_TOLERANCE * (TIMER_INTVAL / 2)

#define ENCA_1 4
#define ENCA_2 12
#define ENCB_1 7
#define ENCB_2 8
#define ENCC_1 A0
#define ENCC_2 A1
byte enc_pins[ENC_NUM * 2] = {ENCA_1, ENCA_2, ENCB_1, ENCB_2, ENCC_1, ENCC_2};

ポート操作

    // get current pins status of Encoder1
    pin_val[0]  = ((PIND & _BV(4)) ? 1 : 0) << 1; // ENC A-1
    pin_val[0] |= ((PINB & _BV(4)) ? 1 : 0);      // ENC A-2

#if(ENC_NUM > 1)
    // get current pins status of Encoder2
    pin_val[1]  = ((PIND & _BV(7)) ? 1 : 0) << 1; // ENC B-1
    pin_val[1] |= ((PINB & _BV(0)) ? 1 : 0);      // ENC B-2

    pin_val[2]  = ((PINC & _BV(0)) ? 1 : 0) << 1; // ENC C-1
    pin_val[2] |= ((PINC & _BV(1)) ? 1 : 0);      // ENC C-2
#endif

後は上述に倣い、

int val += ENC_COUNT(2);

というように値を取得していくだけです。

 

ロータリーエンコーダを複数扱うようなプロジェクトは中々ないかもしれませんが、1個だけでも、充分機能してくれると思うので、是非お役立てください。

参考リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください