Rotary encoders are useful for controlling servos and stepper motors, navigating menus on LCD displays, and also as speedometers that can measure how fast a wheel is rotating. In this article, we will look at how to set up a rotary encoder by building a simple program that counts up and down when you turn the knob.

Rotary encoders are used to measure angular rotation. They are similar in appearance to potentiometers but unlike potentiometers, rotary encoders can rotate around their axis without stopping. Also, while potentiometers output a resistance value, rotary encoders output a digital signal that can be used to determine the position of the knob.

Overview of a Rotary Encoder

Let’s take a closer look at a rotary encoder. The knob of a rotary encoder can rotate continuously around its axis without stopping. As you turn the knob, the rotary encoders has clicks, or “detents”. Each detent generates a digital signal that’s used by the Arduino to determine the position of the encoder knob. The number of detents per rotation defines the resolution of the encoder. Encoders with more detents have a greater resolution.

The Keyes KY-040 rotary encoder used in this article has 20 detents in a full rotation. This encoder also has a push button switch that can be controlled by pressing the knob down.

There are two types rotary encoders you’re likely to come across. One type is mounted to a PCB and has pins already attached like this one, the Keyes KY-040:

How to Setup and Program Rotary Encoders on the Arduino - Rotary Encoder

Another type is a stand alone version like this one:

How to Setup and Program Rotary Encoders on the Arduino - Standalone Rotary Encoder

Stand alone rotary encoders have the same pins as PCB mounted rotary encoders, plus an extra ground pin:

How Rotary Encoders Work

Inside the body of the rotary encoder is a metal ring that is attached to the knob and rotates with the knob. The metal ring has tabs on it, with each tab corresponding to one detent:

How a Rotary Encoder Works - Metal Disk.jpg

On the bottom half of the casing there are two sets of metal arms:

How a Rotary Encoder Works - Contact Arms.jpg

Both of the inner arms are connected to the ground pin:

How a Rotary Encoder Works - Inner Arms.jpg

The outer arm on this side is connected to the DT pin:

How a Rotary Encoder Works - DT Pin Arm.jpg

The outer arm on this side is connected to the CLK pin:

1 / 1 – How a Rotary Encoder Works - Clock Pin Arm.jpg

When the knob is turned the arms contact the metal tabs on the ring making a connection between the ground pin and the DT pin, or the ground pin and the CLK pin.

Let’s look at a waveform diagram to see how this works. When the encoder is rotating a square wave is output from both the DT pin and the CLK pin. When you turn the knob clockwise the DT pin goes HIGH first. At the same time the CLK pin is LOW:

Rotary Encoder Waveform CLOCKWISE WITH LINE.png

When you turn the knob counter-clockwise the CLK pin goes HIGH first and the DT pin is LOW:

Rotary Encoder Waveform COUNTER CLOCKWISE WITH LINE.png

By detecting which pin goes HIGH while the other is LOW, we can determine which direction the knob is turning. Let’s see this in action by building a project that counts up when the knob is turned clockwise and counts down when the knob is turned counter clockwise.

How to Connect a Rotary Encoder to the Arduino

Like other mechanical switches, rotary encoders are prone to switch bouncing. Therefore, we will need to use a Schmitt trigger to de-bounce the signals from the rotary encoder. The Schmitt trigger we will use is the SN74HC14.

Parts Needed:

  • Arduino Uno
  • Keyes KY-040 rotary encoder
  • SN74HC14 Schmitt trigger
  • (2) 1 µF capacitors

Follow this diagram to build the project:

Rotary Encoder Debounced With Schmitt Trigger - Wiring Diagram

How to Program a Rotary Encoder on the Arduino

Once the circuit is connected, upload this code to the Arduino:

int clkPin = 3;
int dtPin = 4;
int switchPin = 12;

int count = 0;
int clkPinLast = LOW;
int clkPinCurrent = LOW;


void setup() {
  pinMode(clkPin, INPUT);
  pinMode(dtPin, INPUT);
  pinMode(switchPin, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int switchState = digitalRead(switchPin);

  if (switchState == LOW) {
    count = 0;
  }

  clkPinCurrent = digitalRead(clkPin);

  if ((clkPinLast == LOW) && (clkPinCurrent == HIGH)) {
    if (digitalRead(dtPin) == HIGH) {
      count--;
    }
    else {
      count++;
    }

    Serial.println(count);
  }

  clkPinLast = clkPinCurrent;
}

Explanation of the Code

In the beginning of the sketch, we declare variables to store the readings from the CLK pin (clkPin), DT pin (dtPin), and SW pin (switchPin). Next we declare a variable called count to store the count number. The count will start at zero, so it’s initially set equal to zero. The clkPinLast variable keeps track of the last state of the CLK pin. It’s initially set equal to LOW. Then we declare another variable called clkPinCurrent that will store the current state of the CLK pin and set it equal to LOW as well.

In the setup() section, we use pin mode to set each pin as an INPUT. We don’t want the switchPin to float, so we use the internal pullup resistor. We will print the counts to the serial monitor so we initialize that too.

The first part of the loop() section is used to reset the count to zero when the rotary encoder switch is pressed. To do that, we first create a local variable called switchState, and set it equal to the digital read of the switchPin. The if statement on the next line sets count equal to zero when the switchPin goes LOW.

Now we take the clkPinCurrent variable and set it equal to the digital read from the clkPin. Then we have an if statement that says “if clkPinLast equals LOW, and clkPinCurrent equals HIGH” (the CLK pin went from LOW to HIGH since the last time around the loop), then enter the body of the first if statement and evaluate the nested if statement. The nested if statement asks whether or not the dtPin is HIGH. If it is HIGH, then we decrease the count variable by one. Both if statements will only be true if the knob is turning counter clockwise.

If the knob is not turning counter clockwise, then it must be turning clockwise. So we can use an else statement to increase the count variable by one if the above if statements are not true.

Next we print the value stored in count to the serial monitor. As a last step, we set the clkPinLast variable equal to the clkPinCurrent variable. That way, the program can tell if the state of the clkPin has changed in the next cycle through the loop.

After you build the circuit and upload the code, open up the serial monitor and rotate the encoder knob. The count should increase or decrease once for each click of the encoder.

Using Interrupts With Rotary Encoders

If you want to use the rotary encoder to control another device, the Arduino will need to balance taking reads from the encoder with sending signals to the other device. Unfortunately, the Arduino isn’t very good at doing multiple things at the same time.

For example, say you want to have an LED blinking on and off at the same time the encoder is counting up and down. Blinking an LED uses the delay() function, which causes the Arduino to stop what it’s doing for the duration of the delay. During that time it won’t be able to detect clicks from the rotary encoder.

Luckily there is a solution to this problem in the form of an interrupt service routine. By triggering an interrupt service routine with each signal from the rotary encoder, the Arduino can perform other tasks and only be interrupted when a signal from the rotary encoder is detected.

On the Arduino Uno, pins 2 and 3 are the only pins that are able to trigger an interrupt service routine.

Follow the diagram below to connect the example circuit:

Debounced Rotary Encoder With LED and Interrupts WIRING DIAGRAM

How to Program Interrupts With a Rotary Encoder

Once the circuit above is connected, upload this code to the Arduino:

volatile int clkPin = 2;
volatile int dtPin = 3;
int switchPin = 12;

volatile int count = 0;
volatile int clkPinLast = LOW;
volatile int clkPinCurrent = LOW;

int ledPin = 8;

void encoderInterrupt() {  
  clkPinCurrent = digitalRead(clkPin);
  if ((clkPinLast == LOW) && (clkPinCurrent == HIGH)) {
    if (digitalRead(dtPin) == HIGH) {
      count--;
    }
    else {
      count++;
    }
    Serial.println(count);
  }
  clkPinLast = clkPinCurrent;
}

void setup() {
  pinMode(clkPin, INPUT);
  pinMode(dtPin, INPUT);
  pinMode(switchPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(2), encoderInterrupt, CHANGE);
  attachInterrupt(digitalPinToInterrupt(3), encoderInterrupt, CHANGE);
  Serial.begin(9600);
}

void loop() {
  int switchState = digitalRead(switchPin);

  if (switchState == LOW) {
    count = 0;
  }
  
  digitalWrite(ledPin, HIGH);
  delay(100);
  digitalWrite(ledPin, LOW);
  delay(100);
}

Explanation of the Code

This sketch is very similar to the sketch in the previous project, with a couple differences. Since now we need to trigger interrupt service routines, we have to connect the clkPin and dtPin to pins 2 and 3 instead of pins 3 and 4 as in the last sketch.

We also need to add interrupt service routines for the clkPin and dtPin. Before variables can be used in interrupt service routines, they need to be declared as volatile. In this sketch count, clkPinLast, and clkPinCurrent are used in the interrupt service routine so they are declared as volatile variables. We also need to declare a variable for the LED pin (ledPin).

Next we create the interrupt service routine with void encoderInterrupt(). The code that goes inside the interrupt is the same code from the loop() section in the previous example that reads the dtPin and clkPin and increments and decrements the counter.

In the setup() section, we set the pin modes for each pin. The clkPin, dtPin, and switchPin are set the same as in the last example. But this time we also have an LED, so the ledPin variable is set as an OUTPUT.

The interrupt service routine needs to be called in the setup() section. To call the interrupt service routine, use the attachInterrupt() function.

The attachInterrupt() function takes three parameters – the digital pin to interrupt, the name of the interrupt service routine, and the mode of the interrupt. Each interrupt needs it’s own attachInterrupt() function, so in this sketch there is one for pin 2 and one for pin 3.

After you build the circuit and upload the code, open up the serial monitor. The LED should be blinking on and off. Now when you turn the rotary encoder knob, the count on the serial monitor should go up and down without any missed clicks.