ロータリーエンコーダの読み取り実装

設定つまみや回転検出に使われるロータリーエンコーダは、実装が単純だと「回したのに値が飛ぶ」「たまに逆方向に動く」といった不具合が起きやすいデバイスです。本記事では、A相・B相の仕組みから、誤カウントに強い状態遷移テーブル方式の実装、メカ式(機械接点式)つまみ特有の注意点までを解説します。

A相・B相の仕組み:2ビットのグレイコード遷移で方向がわかる

インクリメンタル型のロータリーエンコーダは、回転に応じてA相・B相という2本のパルスを出力します。両者は電気角で90°の位相差を持っており、どちらの相が先に変化するかで回転方向を判別できます。定義した時計回り(CW)方向についてA相とB相のどちらが先行するかは、品種・配線・取り付け基準によって異なるため、後述の手順で必ず実機を回して確認してください。1回転に1度だけパルスを出すZ相(原点信号)を持つ品種もあります。

A相・B相の2ビットの組み合わせは「00→01→11→10→00…」のように、一度に1ビットだけ変化する順序(グレイコード)で遷移します。この性質が、後述する誤カウント対策の鍵になります(この例はB相が先に変化する並びです。A相先行のエンコーダではシーケンスが逆転するため、後述するテーブルの符号を反転させて対応します)。実装上はA相・B相の2ビット状態(00/01/11/10)の遷移として扱い、正常な遷移(1ビットのみ変化)だけを有効とするのが定石です。

安易な実装はなぜ誤動作するのか

よくある実装は「A相の立ち上がりエッジ割り込みでB相のレベルを見る」方式です。この方式には2つの問題があります。理想的な波形を仮定すれば成立しますが、メカ式エンコーダの接点はチャタリング(接点バウンス)で短時間に何度もエッジを出すため、(1) 1クリック(ディテント)回しただけで複数カウントされたり、(2) エッジの瞬間にB相の値が不安定になって方向を誤判定したりします。コンデンサによるフィルタだけでは、条件によって取りこぼしや遅延、残留バウンスが生じ得るため、ソフトウェア側の対策も併用します。

定石:状態遷移テーブル方式

堅牢な実装の定石は、A相・B相の2ビットの「前回値→今回値」の全16通りをテーブルで判定する方式です。グレイコード順に沿った正しい遷移だけを+1または−1のカウントとし、2ビットが同時に変化する遷移は、理想的なクアドラチャ検波では発生しないため無効として捨てます。機械式では一瞬そのように観測される場合もありますが、多くはバウンスやサンプリングの瞬間的なぶれであり、無効化する方が誤カウントを抑えられます。チャタリングで生じる行き戻りは+1と−1が相殺し、結果としてデバウンス(チャタリング除去)の役割も果たします。次のコード例では、read_pin_a()・read_pin_b() が各相のレベルを0または1で返すことを前提としています。

#include <stdint.h>
/* 状態は (A<<1)|B の2bit値。インデックスは (前回<<2)|今回 */
static const int8_t enc_table[16] = {
     0, +1, -1,  0,
    -1,  0,  0, +1,
    +1,  0,  0, -1,
     0, -1, +1,  0
};
static uint8_t prev_state;
static volatile int32_t enc_count;
void encoder_poll(void)            /* 1 ms周期で呼び出す */
{
    uint8_t state = (uint8_t)((read_pin_a() << 1) | read_pin_b());
    static uint8_t inited = 0u;
    if (!inited) { prev_state = state; inited = 1u; return; }  /* 初回は現在値で初期化 */
    enc_count += enc_table[(prev_state << 2) | state];
    prev_state = state;
}

カウントの+方向が時計回りになるかどうかは、A相・B相の配線とビット割り当てで反転します。必ず実機で回して確認し、逆なら符号を入れ替えます。

クリックとカウントの関係

カチカチと手応えのあるメカ式つまみでは、1クリックの間にA相・B相が4状態進む(1周期分変化する)品種が多く、その場合はカウントを4で割った値が「クリック数」になります。1クリックで2状態の品種もあり、クリックの位置と電気周期が一致しない機種もあるため、データシートで確認するか、実機で1クリック回してカウント値を見て決めます。また、プルアップの有無、シュミットトリガ入力の有無、外付けRCフィルタといった入力回路でも安定性が変わります。可能ならシュミットトリガ入力やデジタルフィルタの併用も検討してください。割り切れない端数が残らないよう、クリック境界(両相とも安定する状態)で確定させる実装にすると操作感が安定します。

読み取り周期と実装形態

  • 手で回すつまみ:1 ms程度の周期ポーリングで十分です。人間の操作速度であれば、多くの用途では状態の取りこぼしは問題になりにくいです(詳しいサンプリング周波数の目安は後述します)。タイマ割り込みやRTOSの周期タスクからencoder_poll()を呼びます。
  • モータ軸などの高速回転:ポーリングでは取りこぼすため、マイコンのタイマが持つエンコーダモード(位相計数機能)やハードウェアカウンタを使います。ソフトウェアの役割は周期的なカウンタ読み出しと差分計算になります。
  • サンプリング周波数の目安:A相・B相の最大エッジ周波数の4〜10倍以上を確保するのが実用的な目安です(各相の変化を取りこぼさないためには理屈上は標本化定理(ナイキストの定理)により2倍が下限ですが、タイミングジッタや割り込みレイテンシを考慮して余裕を持たせます)。具体的な値は、分解能・4逓倍カウント・最大回転速度から最大エッジ周波数を算出して決めます。
  • 割り込み方式を使う場合:両相の両エッジで割り込みをかけ、ハンドラ内はテーブル判定とカウントだけの最小処理にします。なお、8ビットや16ビットマイコンなどでは、32ビット変数の読み出しが1命令で完了しない場合があるため、volatile指定だけでは排他になりません。メインループ側でenc_countを読むときは、割り込み禁止などのクリティカルセクションでローカル変数へコピーしてから使います。

ハードウェア側の注意点

  • プルアップ:メカ式は接点が開閉するだけのため、プルアップ抵抗(マイコン内蔵でも可)が必要です。
  • フィルタ:小容量コンデンサ(数nF程度)でエッジを鈍らせると、テーブル方式と併用してさらに安定します。ただし高速回転用途では波形なまりに注意します。
  • クリック間での停止:つまみが中間位置で止まると、振動で1ビットだけ行き来することがあります。テーブル方式なら誤カウントせず相殺されます。

まとめ

ロータリーエンコーダは「90°位相差のグレイコード」という性質を活かし、状態遷移テーブルで読むのが定石です。エッジ割り込み+レベル判定で不安定さに悩んでいるなら、テーブル方式への置き換えだけで見違えるほど安定します。

組み込みソフトの世界 トップへ戻る