I/Oエキスパンダでロータリーエンコーダ入力

やりたいこと

I2C接続のI/OエキスパンダIC MCP23017 のI/Oポートにロータリーエンコーダ (機械式、2相出力、クリックありタイプ) をつなぎ、その回転をマイコンで検出したい。

ロータリーエンコーダの仕様と回転の判定方法

今回使うロータリーエンコーダは、C端子をGNDに接地し、A端子とB端子をそれぞれプルアップする。A端子とB端子がそれぞれA相とB相の出力ピンになる。クリックありタイプのため、(A相, B相) は安定状態では常に (High, High) であり、1クリック回転させると回転方向によって以下のように状態が変化する。

  • 時計回り :(H, H) → (H, L) → (L, L) → (L, H) → (H, H)
  • 反時計回り:(H, H) → (L, H) → (L, L) → (H, L) → (H, H)

よって、A相が H→L に変化したときに、B相がHであれば時計回り、Lであれば反時計回りと判定できる。


ポーリングでは取りこぼしで誤判定する

10msec程度の周期でポーリングした場合、状態変化を取りこぼしてしまい、回転方向を頻繁に誤判定する結果になった。たとえば時計回りのとき、(H, H) → (H, L) → (L, L) の (H, L) を取りこぼすと (H, H) → (L, L) という変化に見えてしまい、前述の判定方法では反時計回りと判定されてしまう。

割り込み信号の利用

MCP23017 には割り込み出力ピンがあり、予め設定したI/Oピンの状態が変化したときに割り込み信号を出力できる。この信号をマイコンで監視して、A相の状態が変化したときにすかさずA相・B相の状態を読みにいけば、無駄な通信をなくしつつ状態変化への応答も速くできる。

この方法でかなり誤判定は減ったが、依然としてしばしば誤判定は発生した。また、割り込み信号をマイコンで常時ポーリングするにはメインループを常にmsecオーダー以下で回さないといけないので実用的ではない。といって、割り込みルーチン内でI2C通信のような時間のかかる処理をするのも望ましくない。実際、M5Stackでそれをするとコアパニックが発生した。

割り込み発生時のピン状態を取得

MCP23017 には、割り込み発生時のI/Oピンの状態を記憶するレジスタがある。このレジスタを読めば、多少の遅延があっても状態を取りこぼすことはない。この方法で誤判定はほとんどなくなった。

中途半端な状態で止まった場合の対策

クリックありタイプのロータリーエンコーダは通常は安定状態 (H, H)で止まるが、たまに中途半端な状態で止まる場合もある。A相がLowのままというイレギュラーな状態は誤判定を招く。

MCP23017 には、最後に割り込み発生したピンを記憶するレジスタがある。このレジスタを読めば、最後に状態変化したピンが分かるので、A相がLowのまま止まっているロータリーエンコーダを誤判定することはなくなる。この方法で誤判定はなくなった。

コード

最後に、M5Stackのソースを示す。
この例ではMCP23017のポートAに4個のロータリーエンコーダを接続している。

#include <M5Stack.h>
#include <Adafruit_MCP23X17.h>

// ピン番号
#define PIN_SDA     21
#define PIN_SCL     22
#define PIN_PEG_INT 35

// I2Cスレーブアドレス
#define ADDR_PEG  0x22

// I/Oエキスパンダ
Adafruit_MCP23X17 mcp23017;

// ロータリーエンコーダのピン番号とビットマスク
// ポートA.0: エンコーダ[0]のA相,  ポートA.1: エンコーダ[0]のB相
// ポートA.2: エンコーダ[1]のA相,  ポートA.3: エンコーダ[1]のB相
// ポートA.4: エンコーダ[2]のA相,  ポートA.5: エンコーダ[2]のB相
// ポートA.6: エンコーダ[3]のA相,  ポートA.7: エンコーダ[3]のB相
static const uint8_t CLK_PIN [4] = {0, 2, 4, 6};
static const uint8_t CLK_MASK[4] = {0x01, 0x04, 0x10, 0x40};
static const uint8_t DT_MASK [4] = {0x02, 0x08, 0x20, 0x80};

void setup()
{
  M5.begin();
  // Serial.begin(115200);

  pinMode(PIN_SDA, INPUT_PULLUP);
  pinMode(PIN_SCL, INPUT_PULLUP);
  delay(100);

  if (!mcp23017.begin_I2C(ADDR_PEG)) {
    Serial.println("MCP23017 Error.");
    return;
  }else{
    Serial.println("MCP23017 OK.");
  }
  for(int i = 0; i < 16; i++){
    mcp23017.pinMode(i, INPUT_PULLUP);
  }

  mcp23017.setupInterrupts(true, false, LOW);
  for(int i = 0; i < 4; i++) {
    mcp23017.setupInterruptPin(CLK_MASK[i], CHANGE);
  }
  pinMode(PIN_PEG_INT, INPUT);
}

void loop()
{
  if (!digitalRead(PIN_PEG_INT)) {
    uint8_t last_pin = mcp23017.getLastInterruptPin(); 
    uint8_t val = (uint8_t)(mcp23017.getCapturedInterrupt() & 0x00FF);

    for(int i = 0; i < 4; i++) {
      if(last_pin == CLK_PIN[i]) {
        int clk = (val & CLK_MASK[i]) ? 1 : 0;
        int dt  = (val & DT_MASK [i]) ? 1 : 0;
        if(clk == LOW){
          if (dt == HIGH) {
            Serial.printf("Encoder %d +\n", i);
          } else {
            Serial.printf("Encoder %d -\n", i);
          }
        }
      }
    }
  }
}