Counting every click · 09 of 10

A state table

CLOCK and DATA together can only be in four states, and a turn walks round them in order. Interrupt on every change of either pin, look up the move in a sixteen-entry table, and add what it says: +1, -1 or 0. A bounce becomes a step forward and a step back, which cancel.

Four states, in order

Read CLOCK and DATA together and write them as two digits, CLOCK first, 1 for HIGH. There are only four possibilities: 11, 01, 00 and 10. At rest, with both contacts open, it is 11. Turning forward walks round them in that order, one digit changing at a time, and back to 11. Turning back walks the same circle the other way.

So every change of either pin is a move to a neighbour, and which neighbour says which way the knob went. That is all a state table is: for each of the sixteen pairs of old state and new state, write down +1, -1 or 0.

A state table
The run
Moves
0
Steps counted
0
Clicks at 4 a click
0
At rest, both contacts open: 11. Every change on either pin is a move to a neighbouring state, and the table says which way round it went.

Play a clean cycle first: four moves forward, +4. Then DATA bounces: DATA falls, springs back, and falls again. The table scores that +1, -1, +1, and the cycle still totals +4. Nothing in the sketch had to recognise it as bounce.

Why it copes where the first count did not

The first count only looked at one event, CLOCK falling, and trusted a single read of DATA to give the direction. A bounce on CLOCK counted as another click; a late read got the direction wrong.

The table looks at every change of both pins and scores each on its own. A bounce is a move and its undo, which add to nothing. A read that arrives too late to see a state, a missed state in the figure, is a jump across the square, which no single change can make; the table scores it 0 rather than guessing. It loses count honestly instead of counting backwards.

The capacitors on the board swallow the shortest bounces before any of this. The table deals with whatever gets past them.

Steps and clicks

The table counts four steps per cycle, twice as fine as it needs to be for a knob, so the sketch divides by STEPS_PER_CLICK. It is 4 because on many EC11 parts one click is one full cycle. This part's clicks have not been checked against its cycles, so turn the knob once round and compare: if every click moves the printed number by two, set it to 2.

Both pins interrupt, on CHANGE. On an Uno that needs D2 and D3, its only two external-interrupt pins, which is why the whole book puts CLOCK and DATA there.

The code

One handler, attached to both CLOCK and DATA on every change. It reads both pins, looks up the move from the last state to this one, and adds the result to steps. loop() divides by STEPS_PER_CLICK and prints clicks.

rotary_encoder_state_table.ino
/*
  Rotary Encoder - a state table                       TK06 / /p/tk06

  Wiring. Count from the square pad on the TinkerBlock board, knob
  up, header at the bottom:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
              (CLOCK, DATA and BTN all reach whatever VCC is)
    NC     -> nothing   (unconnected on the board)
    BTN    -> D4 on an Uno, GPIO 27 on an ESP32, GPIO 6 on an
              ESP32-S3, GP15 on a Raspberry Pi Pico
    CLOCK  -> D2, GPIO 25, GPIO 4, GP13   (the same four boards)
    DATA   -> D3, GPIO 26, GPIO 5, GP14

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    No library needed.
*/

// CLOCK, DATA and BTN, as GPIO numbers.
// Uno: 2, 3, 4. ESP32: 25, 26, 27. ESP32-S3: 4, 5, 6. Pico: 13, 14, 15.
const int CLOCK_PIN = 4;
const int DATA_PIN = 5;
const int BUTTON_PIN = 6;       // not used in this sketch
const int STEPS_PER_CLICK = 4;  // on many EC11 parts; count yours

#ifndef IRAM_ATTR
#define IRAM_ATTR               // only the ESP32 cores need it
#endif

// +1 a step forward, -1 a step back, 0 no move or a missed state.
// Index: old state * 4 + new state. A state is CLOCK * 2 + DATA.
const int8_t TABLE[16] = {0, -1, 1, 0, 1, 0, 0, -1,
                          -1, 0, 0, 1, 0, 1, -1, 0};

volatile uint8_t state = 3;     // both HIGH: at rest
volatile long steps = 0;

// Runs on every change of either pin.
void IRAM_ATTR onEither() {
  uint8_t now = (digitalRead(CLOCK_PIN) << 1) | digitalRead(DATA_PIN);
  steps += TABLE[(state << 2) | now];
  state = now;
}

void setup() {
  Serial.begin(115200);
  pinMode(CLOCK_PIN, INPUT);    // the block has its own pull-ups
  pinMode(DATA_PIN, INPUT);
  state = (digitalRead(CLOCK_PIN) << 1) | digitalRead(DATA_PIN);
  attachInterrupt(digitalPinToInterrupt(CLOCK_PIN), onEither, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DATA_PIN), onEither, CHANGE);
}

void loop() {
  static long shown = 0;

  noInterrupts();               // copy it in one piece
  long now = steps;
  interrupts();

  long clicks = now / STEPS_PER_CLICK;
  if (clicks != shown) {
    Serial.println(clicks);
    shown = clicks;
  }
}

A state is CLOCK × 2 + DATA, so 3 is both HIGH, at rest. The index into TABLE is the old state × 4 plus the new one. The sign convention matches the first count: CLOCK falling first is +1. STEPS_PER_CLICK is 4 on many EC11 parts; count yours.

When it does not work

Every click moves the count by 2, or by 4, or by a half.

The table counts four steps per cycle and the sketch divides by STEPS_PER_CLICK, 4, which is right if one click is one full cycle. That is so on many EC11 parts but has not been checked on this one. Turn the knob one full turn, compare the steps with the clicks you felt, and set STEPS_PER_CLICK to the ratio.

It counts the wrong way.

Swap the numbers in CLOCK_PIN and DATA_PIN. Which contact leads when turned clockwise is the part's choice. Swapping the pins reverses every entry of the table at once.

Why does the table have zeros for a jump of two states?

Going from 01 to 10 means both pins changed between two reads, and a state was missed. The move could have been two steps either way, so the table scores it 0 rather than guess. With both pins on interrupts, it should almost never happen.

Is this what encoder libraries do?

Most of the common ones use a state table of this kind, often with extra rules for where a click rests. Writing it once yourself shows what they are doing, and it is short enough to keep in a sketch.

Where this goes next

Six symptoms, and where to look first for each.

When the count goes wrong

Edit this page — content/books/rotary-encoder/a-state-table.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

Rotary Encoder

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum