Arduino で遊ぶ


ロータリー・エンコーダ

ロータリー・エンコーダとは

ロータリー・エンコーダにはいくつか種類があるようですが、今回取り上げるのはインクリメンタル型です。ここに等価回路を示したいと思います。
この2つのスイッチがタイミングをずらしてONOFFする構造になります。

データシートがない

電子部品を購入する際はデータシートを入手することが重要です。しかし、データシートがないロータリー・エンコーダを入手してしまったらどうすれば良いのでしょうか?
このように正しく結線された場合、
取得できる信号は以下のようになります。
 (O:Open、S:Short)
ここから分かるようにロータリー・エンコーダを操作すると、31023102...となり、逆に回すと20132013...の順番に信号が取得できます。
A、Bの接続を入れ替えると、20132013...となり、逆に回すと31023102...の順番に信号が取得できます。
これはA、Bの接続を入れ替えれば回転方向を入れ替えられることを表しています。

一方、このように間違った結線をされた場合、
取得できる信号は以下のようになります。
 (O:Open、S:Short)
その結果33023302...となり、逆に回すと20332033...の順番に信号が取得されます。
A、Bの接続を入れ替えると、33013301...となり、逆に回すと10331033...の順番に信号が取得されます。

誤った結線でも回転方向を判断することは可能ですが、結線に合わせた判断を行う必要がありますし、回転自体を認識できないタイミングが存在していることがわかります。
また、正しく結線された場合には03までの全ての信号を検出できますが、間違った結線では1または2のいずれかの信号を検出できないこともわかります。
これらの違いを利用して、結線の間違いも検出できることを示しています。

Arduino で試してみる

それでは早速試してみましょう。
今回は、簡単に検証することができるArduino nanoを利用したいと思います。
以下のような RotaryEncoder クラス を作成しました。

RotaryEncoder.h
/**
 * @file RotaryEncoder.h
 * @author Masato Kobayashi (m.kobayashi.org@gmail.com)
 * @brief RotaryEncoder Class
 * @version 0.1
 * @date 2019-11-20
 * 
 * @copyright Copyright (c) 2019
 * 
 */
/**
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
#ifndef _ROTARY_ENCODER_H_
#define _ROTARY_ENCODER_H_

#include 
#include 

// Caller function type declaration
typedef void (*recb_f)(int16_t);

// Data buffer for connection inspection 
#define CONNECTION_CHECK_BUF_LENGTH         4
typedef uint16_t ConnectionCheck_t[CONNECTION_CHECK_BUF_LENGTH];

/**
 * @brief RotaryEncoder Class
 * 
 */
class RotaryEncoder
{
private:
    // Callback function
    recb_f fCallback = NULL;
    // IO port number
    uint16_t iIoportA;
    uint16_t iIoportB;
    // Previous value
    uint16_t iOld;
    //  
    bool bDoConnectionCheck = false;
    uint16_t* tCheckBuf = NULL;

public:
    RotaryEncoder(uint16_t, uint16_t, recb_f);
    ~RotaryEncoder();

    void CheckValue(void);

    // Connection check
    void BeginCheck(uint16_t*);
    void RefreshCheck(void);

};

#endif
RotaryEncoder.cpp
/**
 * @file RotaryEncoder.cpp
 * @author Masato Kobayashi (m.kobayashi.org@gmail.com)
 * @brief RotaryEncoder Class
 * @version 0.1
 * @date 2019-11-20
 * 
 * @copyright Copyright (c) 2019
 * 
 */
/**
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */
#include 
#include "RotaryEncoder.h"

//! Table for determining the rotation direction
const int16_t iMutations[] = {
  /* 0 -> 0 : 0 */  0,
  /* 0 -> 1 : + */  1,
  /* 0 -> 2 : - */ -1,
  /* 0 -> 3 : E */  0,
  /* 1 -> 0 : - */ -1,
  /* 1 -> 1 : 0 */  0,
  /* 1 -> 2 : E */  0,
  /* 1 -> 3 : + */  1,
  /* 2 -> 0 : + */  1,
  /* 2 -> 1 : E */  0,
  /* 2 -> 2 : 0 */  0,
  /* 2 -> 3 : - */ -1,
  /* 3 -> 0 : E */  0,
  /* 3 -> 1 : - */ -1,
  /* 3 -> 2 : + */  1,
  /* 3 -> 3 : 0 */  0,
};

/**
 * @brief Construct a new Rotary Encoder:: Rotary Encoder object
 * 
 * @param ioA 'A' port number
 * @param ioB 'B' port number
 * @param f Callback function
 */
RotaryEncoder::RotaryEncoder(uint16_t ioA, uint16_t ioB, recb_f f)
{
    // Store parameters
    iIoportA = ioA;
    iIoportB = ioB;
    fCallback = f;

    // IO port setting
    pinMode(iIoportA, INPUT_PULLUP);
    pinMode(iIoportB, INPUT_PULLUP);

    // Record previous value
    iOld = ((digitalRead(iIoportA) & 1) << 1) | (digitalRead(iIoportB) & 1);
}

/**
 * @brief Destroy the Rotary Encoder:: Rotary Encoder object
 * 
 */
RotaryEncoder::~RotaryEncoder()
{
}

/**
 * @brief Check the rotary encoder status and detect the direction of rotation
 * 
 * @details If the rotation direction is known, the Count function is called
 * 
 */
void RotaryEncoder::CheckValue()
{
    // Get current value
    uint16_t iNow = ((digitalRead(iIoportA) & 1) << 1) | (digitalRead(iIoportB) & 1);

    // Determine rotation direction from current location and previous value
    int16_t iMutation = iMutations[(iOld << 2) | iNow];
    // Save value this time
    iOld = iNow;

    // Call back direction of rotation
    if (fCallback && iMutation)
    {
        fCallback(iMutation);
    }

    // Connection check 
    if (tCheckBuf)
    {
        tCheckBuf[iNow]++;
    }
}

/////// Connection check
/**
 * @brief Clear connection confirmation buffer
 * 
 */
void RotaryEncoder::RefreshCheck()
{
    if (tCheckBuf) {
        for (int i = 0; CONNECTION_CHECK_BUF_LENGTH > i; i++)
        {
            tCheckBuf[i] = 0;
        }
    }
}

/**
 * @brief Connection confirmation start
 * 
 * @param buf Connection check data storage buffer pointer
 */
void RotaryEncoder::BeginCheck(uint16_t* buf)
{
    tCheckBuf = buf;
    RefreshCheck();
}

RotaryEncoder.cpp の 74行目より始まる CheckValue() メソッドでロータリー・エンコーダの信号を検出して回転方向を検出します。このメソッドでは 77行目にあるように GPIO を介して2つのスイッチの状態を読みだしています。
このメソッドを定期的に呼び出して、前回の状況と今回の状況を比較することで回転方向を判別することができます。
回転方向の判別は 80行目で行っています。前回と今回の全てのパターンを網羅したテーブルを作成して確認しています。( 19行目から始まる配列)
そして回転方向が確定した後、87行目でコールバックします。
また 93行目では03のどの信号が読みだされたのか、それぞれの回数をカウントしています。これらの値を検証することで、結線の正誤を確認できます。

このRotaryEncoder クラスは、次のようにして利用します。

main.ino
/**
 * @file main.ino
 * @author Masato Kobayashi (m.kobayashi.org@gmail.com)
 * @brief Sample rotary encoder library
 *        ロータリーエンコーダライブラリのサンプル
 * @version 0.1
 * @date 2019-11-20
 * 
 * @copyright Copyright (c) 2019
 * 
 */
/**
 * This software is released under the MIT License.
 * http://opensource.org/licenses/mit-license.php
 */

#include "Timer.h"
#include "RotaryEncoder.h"

#define RotaryEncoder_A   3
#define RotaryEncoder_B   2

uint32_t iCount = 0L;

Timer* timer;
RotaryEncoder* renc;

//! Interval timer interrupt flag (true: interrupt processing in progress)
//! インターバルタイマー割り込みフラグ(true:割り込み処理中) 
volatile bool bInInterval = false;

//! Rotary encoder connection check buffer
//! ロータリーエンコーダの結線確認用バッファ 
ConnectionCheck_t tCheck = {0,};

/**
 * @brief Interval timer interrupt handler
 *        インターバルタイマーによる割り込みのハンドラ
 * 
 * @details In interrupt processing, just set the flag to 
 *          minimize processing time.
 *          割り込み処理では処理時間が最小になるように
 *          フラグをセットするだけとする
 */
void Interval()
{
  bInInterval = true;
}

uint16_t iRight = 0, iLeft = 0;
uint16_t iRightOld = 0, iLeftOld = 0;
/**
 * @brief Function called after the direction of rotation is known
 *        回転方向が判明した後にコールされる関数
 * 
 * @param val A value indicating the direction of rotation
 *       回転方向を示す値
 *        1 : Clockwise
 *                 時計回り
 *            -1 : Counterclockwise
 *                 反時計回り
 */
void Count(int16_t val)
{
  if (val > 0)
  {
    iLeft++;
  }
  else
  {
    iRight++;
  }
}

/**
 * @brief Arduino Setup routine
 *        Arduinoセットアップルーチン
 * 
 */
void setup() {
  // initialize serial communication at 115200 bits per second:
  // シリアル通信を115200ビット/秒で初期化
  Serial.begin(115200);

  Serial.println("Start Arduino Rotary Encoder.");

  // Instantiating a rotary encoder object
  //     Specify signal input pins
  //     When the rotation direction can be determined, 
  //     the Count function is called
  // ロータリーエンコーダオブジェクトのインスタンス化
  //     信号の入力ピンを指定する 
  //     回転方向が判定できたら Count 関数がコールされる 
  renc = new RotaryEncoder(RotaryEncoder_A, RotaryEncoder_B, Count);

  // Started checking the connection of the rotary encoder
  // ロータリーエンコーダの結線確認を開始 
  renc->BeginCheck(tCheck);

  // Instantiating an interval timer object
  // インターバルタイマオブジェクトのインスタンス化 
  timer = new Timer();
  // Start interval timer
  //     Interval function is called every 0.5ms
  // インターバルタイマ開始 
  //     0.5ms 間隔で Interval 関数がコールされる 
  timer->attach_us(500L, Interval);
}

/**
 * @brief Arduino loop function
 *        Arduinoループ機能
 * 
 */
void loop() {
  // When an interval timer interrupt occurs
  // インターバルタイマ割り込みが発生した時 
  if (bInInterval)
  {
    // Check the rotary encoder status and detect 
    // the direction of rotation
    // If the rotation direction is known, 
    // the Count function is called
    // ロータリーエンコーダの状態を確認して 
    // 回転方向を検出します 
    // 回転方向が判明した場合は Count 関数がコールされます 
    renc->CheckValue();
    bInInterval = false;
  }
  // Output status regularly
  // 定期的に状況を出力する 
  if (!(iCount++ & 0x0fffff))
  {
    int16_t iRightDelta = iRight - iRightOld;
    int16_t iLeftDelta = iLeft - iLeftOld;

    Serial.print("Right:");
    Serial.print(iRight);
    Serial.print(" (");
    if (iRightDelta)
      Serial.print("+");
    Serial.print(iRightDelta);
    Serial.print("), Left:");
    Serial.print(iLeft);
    Serial.print(" (");
    if (iLeftDelta)
      Serial.print("+");
    Serial.print(iLeftDelta);
    Serial.print(")   ");

    for (int i = 0; CONNECTION_CHECK_BUF_LENGTH > i; i++)
    {
      if (i)
      {
        Serial.print(", ");
      }
      Serial.print(i);
      Serial.print(":");
      Serial.print(tCheck[i]);
    }

    Serial.println();

    iRightOld = iRight;
    iLeftOld = iLeft;
  }
}

94行目のrenc = new RotaryEncoder(RotaryEncoder_A, RotaryEncoder_B, Count);で RotaryEncoder クラスのインスタンスを生成します。第1、第2引数に、GPIO のピン番号を指定します。第3引数には、コールバック関数を指定します。
63行目からのvoid Count(int16_t val) {}関数で、ロータリー・エンコーダの回転方向が判明した際のコールバックを受け取ります。
127行目のrenc->CheckValue();でロータリー・エンコーダの回転方向を確認します。
このメソッドを呼び出すタイミングですが、タイマー割り込みを使て 500μ秒間隔で呼び出すようにしています。この間隔が長すぎると操作感が悪くなります。短いとロータリー・エンコーダの変位を検出できずに無駄にCPUを消費することになります。適切な間隔を見つけ出してください。
ちなみに、割り込み処理中は他の割り込み処理を行うことができないばかりか、いくつかの標準関数も機能しなくなります。そのため割り込み処理はできる限り短い時間で終わらせて制御を戻す必要があります。本スケッチの割り込み処理ではフラグを立てるのみで、void loop() {} の中で実際の処理を行うようにしています。

このプログラムの全体をGitHubに置きました。よろしければご覧ください。