Introduction: A Complete Arduino Rotary Solution

Rotary encoders are turnable control knobs for electronic projects, often used with Arduino family microcontrollers. They can be used to fine tune parameters, navigate menus, move objects on screen, set values of any kind. They are common replacements for potentiometers, because they can be rotated more accurately and infinitely, they increment or decrement one discrete value at a time, and often integrated with a pushable switch for selection kind functions. They come in all shapes and sizes, but the lowest price range is difficult to interface with as explained below.

There are countless articles about the working details and usage modes of Rotary encoders, and numerous sample codes and libraries on how to use them. The only problem is that none of them work 100% accurate with the lowest price range Chinese rotary modules.

Step 1: Rotary Encoders Inside

The rotary part of the encoder has three pins (and two more for the optional switch part). One is common ground (black GND), the other two is for determining direction when the knob is turned (they are often called blue CLK and red DT). Both these are attached to a PULLUP input pin of the microcontroller, making the level HIGH their default reading. When the knob is turned forward (or clockwise), first the blue CLK falls to level LOW, then red DT follows. Turning further, blue CLK rises back to HIGH, then as the common GND patch leaves both connection pins, red DT also rises back to HIGH. Thus completing one full tick FWD (or clockwise). Same goes the other direction BWD (or counter-clockwise), but now red falls first, and blue rises back last as shown in the two level images respectively.

Step 2: Misery That Causes Real Pain for Many

Common problem for Arduino hobbyists, that cheap Rotary encoder modules bounce extra changes in output levels, causing extra and wrong direction count readings. This prevents flawless counting and makes it impossible to integrate these modules into accurate rotary projects. These extra bounces are caused by the mechanical movements of the patches over the connection pins, and even applying extra capacitors cannot eliminate them completely. Bounces can appear anywhere in the full tick cycles, and are illustrated by real life scenarios on the images.

Step 3: Finite State Machine (FSM) Solution

The image shows the full state space of the possible level changes for the two pins (blue CLK and red DT), both for correct and false bounces. Based on this state machine a complete solution can be programmed that always works 100% accurate. Because no filtering delays are necessary in this solution, it is also the fastest possible. Another benefit of separating the pins' state space from the working mode is that one can apply both polling or interrupt modes to his own liking. Polling or interrupts can detect level changes on pins and a separate routine will calculate the new state based on current state and actual events of level changes.

Step 4: Arduino Code

The code below counts the FWD and BWD ticks on the serial monitor and also integrates the optional switch function.

// Peter Csurgay 2019-04-10
// Pins of the rotary mapped to Arduino ports
#define SW 21
#define CLK 22
#define DT 23
// Current and previous value of the counter tuned by the rotary
int curVal = 0;
int prevVal = 0;
// Seven states of FSM (finite state machine)
#define IDLE_11 0
#define SCLK_01 1
#define SCLK_00 2
#define SCLK_10 3
#define SDT_10 4
#define SDT_00 5
#define SDT_01 6
int state = IDLE_11;
void setup() {
// Level HIGH will be default for all pins
// Both CLK and DT will trigges interrupts for all level changes
  attachInterrupt(digitalPinToInterrupt(CLK), rotaryCLK, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DT), rotaryDT, CHANGE);
void loop() {
// Handling of the optional switch integrated into some rotary encoders
  if (digitalRead(SW)==LOW) {
// Any change in counter value is displayed in Serial Monitor
  if (curVal != prevVal) {
    prevVal = curVal;
// State Machine transitions for CLK level changes
void rotaryCLK() {
  if (digitalRead(CLK)==LOW) {
    if (state==IDLE_11) state = SCLK_01;
    else if (state==SCLK_10) state = SCLK_00;
    else if (state==SDT_10) state = SDT_00;
  else {
    if (state==SCLK_01) state = IDLE_11;
    else if (state==SCLK_00) state = SCLK_10;
    else if (state==SDT_00) state = SDT_10;
    else if (state==SDT_01) { state = IDLE_11; curVal--; }
// State Machine transitions for DT level changes
void rotaryDT() {
  if (digitalRead(DT)==LOW) {
    if (state==IDLE_11) state = SDT_10;
    else if (state==SDT_01) state = SDT_00;
    else if (state==SCLK_01) state = SCLK_00;
  else {
    if (state==SDT_10) state = IDLE_11;
    else if (state==SDT_00) state = SDT_01;
    else if (state==SCLK_00) state = SCLK_01;
    else if (state==SCLK_10) { state = IDLE_11; curVal++; }

Step 5: Flawless Integration

You can check in the attached video that the FSM solution works accurately and fast even in case of low range rotary encoders with various sporadic bounce effects.