Thanks to visit codestin.com
Credit goes to lastminuteengineers.com

How Rotary Encoder Works and Interface It with Arduino

Have you ever turned the volume knob on a speaker, adjusted the settings on your microwave, or used the scroll wheel on a computer mouse? If so, you’ve already used a rotary encoder—maybe without even knowing it! These clever little devices are all around us, hidden inside gadgets like ovens, car dashboards, and digital cameras. They help us control things smoothly and precisely with just a simple twist.

In this guide, we’re going to explore how to hook up a rotary encoder module to an Arduino and use it in real-world projects. But before we jump into wiring and code, it’s important to understand what’s actually happening inside when you turn that knob.

Learning how a rotary encoder works will make everything else click into place—pun intended—and help you understand why they behave the way they do in your own projects. Once you know the basics, you’ll see why these simple-looking devices are so useful in so many different situations.

Let’s get started!

How Rotary Encoders Work?

Inside a rotary encoder, there’s a circular disc with evenly spaced slots. This disc is attached to the knob you turn. The disc connects to a pin called “C,” which serves as the common ground. The encoder also has two other important pins, called “A” and “B.” These pins will help us figure out which direction the knob is turning.

rotary encoder internal structure

When you turn the encoder’s knob, the slotted disc rotates along with it. As this happens, pins A and B repeatedly make contact with the common ground (pin C).

The important thing to understand is how they make contact. Because of the way the slots are arranged, pins A and B don’t touch the ground at the exact same time. One pin always touches just before the other. This creates two separate signals that are slightly out of sync. In technical terms, they are “90 degrees out of phase”.

rotary encoder working animation

So how do we figure out which way the knob is turning? We do this by watching the state of pin B at the exact moment pin A changes its state.

When pin A changes its state:

  • If pin B’s state is different from pin A (B ≠ A), then the knob is being turned clockwise.
    rotary encoder output pulses in clockwise rotation
  • If pin B’s state is the same as pin A (B = A), then the knob is being turned counterclockwise.
    rotary encoder output pulses in anticlockwise rotation

This method of tracking movement is called Quadrature Encoding.

Rotary Encoder Pinout

The pinout of the rotary encoder module is as follows:

rotary encoder module pinout

GND is the ground connection.

+ (VCC) supplies power to the encoder. Usually, you connect it to either the 5V or 3.3V pin on your Arduino.

SW (Switch) is connected to a built-in push-button inside the encoder. Normally, this pin is held “HIGH” (at a positive voltage) by using Arduino’s internal pull-up resistor, or by using an external pull-up resistor. When you press the button, the SW pin is pulled down to “LOW”.

CLK pin provides one of the two quadrature output signals used to detect rotation. You will connect this pin to a digital input pin on your Arduino.

DT pin provides the second quadrature output signal. This signal is similar to CLK, but it’s 90 degrees out of phase with it. Like the CLK pin, you connect DT to another digital input pin on your Arduino.

Wiring a Rotary Encoder to an Arduino

Now that we understand how a rotary encoder works, let’s connect it to an Arduino and put it to use! The wiring is simple and straightforward.

First, we need to provide power to the rotary encoder. Connect the encoder’s + (VCC) pin to the Arduino’s 5V and the GND pin to the Arduino’s GND.

Next, we’ll connect the signal pins. The CLK pin on the encoder should be connected to digital pin #2 on the Arduino, while the DT pin goes to digital pin #3.

Finally, we need to connect the switch pin. Connect the SW pin to digital pin #4 on the Arduino.

The following table lists the pin connections:

Rotary EncoderArduino
VCC (+)5V
GNDGND
SW4
DT3
CLK2

You can also refer to the wiring diagram below for a visual guide:

wiring rotary encoder with arduino uno

Once the connections are complete, your rotary encoder will be ready to use with the Arduino!

Arduino Example Code 1 – Reading Rotary Encoders

In this example, we’ll learn how to read the rotation of a rotary encoder and detect when its built-in button is pressed. This will help us understand how rotary encoders work and how we can use them in Arduino projects.

First, try out the sketch below, and then we’ll explore how it works in more detail.

// Rotary Encoder Inputs
#define CLK 2
#define DT 3
#define SW 4

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";
unsigned long lastButtonPress = 0;

void setup() {

  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(SW, INPUT_PULLUP);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);
}

void loop() {

  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;

  // Read the button state
  int btnState = digitalRead(SW);

  //If we detect LOW signal, button is pressed
  if (btnState == LOW) {
    //if 50ms have passed since last LOW pulse, it means that the
    //button has been pressed, released and pressed again
    if (millis() - lastButtonPress > 50) {
      Serial.println("Button pressed!");
    }

    // Remember last button press event
    lastButtonPress = millis();
  }

  // Put in a slight delay to help debounce the reading
  delay(1);
}

After uploading the code to your Arduino, open the Serial Monitor. Turn the knob in both directions and watch the counter values change on the screen. Try pressing the button and see if “Button pressed!” appears.

rotary encoder output on serial monitor

If you notice that the rotation direction is opposite of what you expect, you can easily fix this by swapping the connections of the CLK and DT pins.

Code Explanation

In the beginning of our code, we define which Arduino pins connect to the encoder’s CLK, DT, and SW pins.

#define CLK 2
#define DT 3
#define SW 4

We also create several important variables.

  • The counter variable keeps track of how many steps the encoder has rotated.
  • The currentStateCLK and lastStateCLK variables store the state of the CLK pin at different times, which helps us detect movement.
  • The currentDir variable tells us whether the encoder is turning clockwise (CW) or counterclockwise (CCW).
  • Lastly, the lastButtonPress variable helps prevent false button presses by making sure we don’t count multiple presses when you only pushed the button once.
int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir ="";
unsigned long lastButtonPress = 0;

In the setup() section, we first set the rotary encoder’s pins as inputs so we can read their signals, then enable the input pull-up resistor on the SW pin. We also start the Serial Monitor so we can see what’s happening.

Finally, we read the initial state of the CLK pin and store it in lastStateCLK. This is important because it helps us detect when the knob moves.

pinMode(CLK,INPUT);
pinMode(DT,INPUT);
pinMode(SW, INPUT_PULLUP);

Serial.begin(9600);

lastStateCLK = digitalRead(CLK);

In the loop() section, we read the current state of the CLK pin and compare it with the previous state stored in lastStateCLK. If the state has changed, we know the knob has moved. To figure out which direction it’s moving, we check the DT pin.

If DT is different from CLK, the encoder is rotating counterclockwise (CCW), so we decrease the counter and set currentDir to “CCW”. If DT is the same as CLK, the encoder is turning clockwise (CW), so we increase the counter and set currentDir to “CW”.

if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {
  
  if (digitalRead(DT) != currentStateCLK) {
    counter--;
    currentDir = "CCW";
  } else {
    counter++;
    currentDir = "CW";
  }

We then display the direction and counter value on the Serial Monitor.

Serial.print("Direction: ");
Serial.print(currentDir);
Serial.print(" | Counter: ");
Serial.println(counter);

At this point, we update lastStateCLK to match the current state of CLK so that the next time the loop runs, we can detect new changes.

lastStateCLK = currentStateCLK;

For the button, we check if it is pressed (when SW is LOW). To avoid false readings from button bounce (tiny vibrations when pressing), we wait 50 milliseconds to make sure the button is truly pressed. If it stays pressed for that time, we show “Button pressed!” on the Serial Monitor. We also record when this press happened so we can properly detect future presses.

int btnState = digitalRead(SW);

if (btnState == LOW) {
    if (millis() - lastButtonPress > 50) {
        Serial.println("Button pressed!");
    }
    lastButtonPress = millis();
}

We end the loop with a small 1 millisecond delay to help stabilize our readings before starting again.

delay(1);

Arduino Example Code 2 – Using Interrupts

When we use a rotary encoder, we need to constantly check for changes in the DT and CLK signals to track its movement. In our first example, we did this by constantly checking these values over and over again in the main program loop. While this method works, it’s not the most efficient approach for a few reasons:

  1. The Arduino has to keep checking these signals all the time, even when nothing is happening. This is like repeatedly asking “Did something change yet?” This wastes valuable processing power.
  2. Sometimes there’s a small delay between when you turn the knob and when the Arduino notices. If the Arduino is busy with something else, it might not react immediately.
  3. If you turn the knob really fast, the Arduino might completely miss some of the movements because it wasn’t checking at exactly the right moment.

Luckily, there’s a better way! We can use Interrupts. Interrupts are like special alarms that immediately notify the Arduino when something important happens. When a signal changes on the rotary encoder, an interrupt can pause whatever the Arduino is doing, handle the change, and then return to its previous task. This approach is much more efficient because the Arduino doesn’t need to constantly check for changes – it can focus on other tasks and only react when necessary.

Wiring

The wiring for this project is almost the same as our previous example. The only difference is that we won’t connect the SW (button) pin this time. We already learned how to use the button before, so now we’ll focus just on tracking the knob’s rotation using interrupts.

control rotary encoder using interrupts with arduino uno

Arduino Code

After you’ve connected everything as shown in the diagram, upload this new code to your Arduino.

// Rotary Encoder Inputs
#define CLK 2
#define DT 3

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";

void setup() {
  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);

  // Call updateEncoder() when a change is seen on CLK pin
  attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);
}

void loop() {
  //Do some useful stuff here
}

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

When you run the code and turn the encoder knob, you’ll see the direction and counter values update instantly in the Serial Monitor. Because we’re using interrupts, the Arduino will react much more accurately than before.

rotary encoder interrupt output on serial monitor

Code Explanation

The code works similarly to our previous example, but with one key difference: instead of constantly checking the encoder’s state in the loop() function, we now use an interrupt to handle it.

At the beginning of the code, we define the pins for CLK and DT, along with some variables to keep track of the encoder’s movement, just like before.

#define CLK 2
#define DT 3

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";

In the setup() function, we configure the CLK and DT pins as inputs and start the Serial Monitor so we can see the output. Then, we read the initial state of the CLK pin and store it in a variable called lastStateCLK, which helps us detect future changes.

// Set encoder pins as inputs
pinMode(CLK, INPUT);
pinMode(DT, INPUT);

// Setup Serial Monitor
Serial.begin(9600);

// Read the initial state of CLK
lastStateCLK = digitalRead(CLK);

The most important part of this setup is the attachInterrupt() function. This special function tells the Arduino to watch the CLK pin closely and run our updateEncoder() function whenever the pin changes its state.

attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);

The attachInterrupt() function needs three pieces of information: the pin we want to monitor, the function we want to call when the interrupt occurs, and the condition that should trigger the interrupt. The condition we use here is CHANGE, which means the interrupt will be triggered whenever the signal switches from HIGH to LOW or from LOW to HIGH.

In our previous example, we had to check the encoder’s state over and over in the loop() function. Now, all that code has been moved to the updateEncoder() function.

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

One interesting thing about this new approach is that the loop() function is completely empty! This is because the interrupt system takes care of detecting changes automatically. The Arduino doesn’t need to constantly check the encoder’s state. Instead, it remains free to do other tasks until a signal change happens.

void loop() {
  //Do some useful stuff here
}

Arduino Example Code 3 – Controlling Servo Motor with Rotary Encoder

In this example, we’re going to control the position of a servo motor using a rotary encoder. This can be especially useful when you need precise control over movement, like when you’re building a robotic arm and want to carefully adjust the angle of a joint or the grip of a claw.

If you’re not already familiar with how servo motors work, it might be helpful to check out our basic tutorial before starting this project.

Wiring

We’re now adding a servo motor to our setup. You’ll need to connect the red wire from the servo to the 5V pin on the Arduino, the black or brown wire to GND, and the orange or yellow wire, which is the control signal, to digital pin 9 (a PWM-capable pin).

wiring for controlling servo motor with rotary encoder

Arduino Code

Here is the code for using the rotary encoder to precisely control the servo motor. Each time you rotate the knob one detent (click), the position of the servo arm changes by one degree.

// Include the Servo Library
#include <Servo.h>

// Rotary Encoder Inputs
#define CLK 2
#define DT 3

Servo servo;
int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";

void setup() {
  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Attach servo on pin 9 to the servo object
  servo.attach(9);
  servo.write(counter);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);

  // Call updateEncoder() when a change is seen on CLK pin
  attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);
}

void loop() {
  //Do some useful stuff here
}

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      if (counter < 0)
        counter = 0;
    } else {
      // Encoder is rotating CW so increment
      counter++;
      if (counter > 179)
        counter = 179;
    }
    // Move the servo
    servo.write(counter);
    Serial.print("Position: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

Code Explanation

Now, if you look at the code, you’ll see that it’s very similar to the previous interrupt-based rotary encoder sketch. However, we’ve added a few important changes to work with the servo motor.

The first major change is at the beginning of the sketch, where we include the Servo library. This library gives us the tools to easily control servo motors with Arduino. After that, we create a servo object to represent our servo motor.

#include <Servo.h>

Servo servo;

In the setup() section, we use the attach() function to connect our servo object to pin 9, where the servo’s control wire is connected. We also tell the servo to move to its starting position using the write() function and give it the current value of the counter, which starts at 0.

servo.attach(9);
servo.write(counter);

Inside the updateEncoder() function, we’ve added new code to control the servo motor as the encoder turns. Just like before, we check for changes in the CLK pin, and if a change is detected, we determine the direction of rotation by comparing the DT pin to the CLK pin. If the encoder is turning counterclockwise, we decrease the counter. If it’s turning clockwise, we increase the counter.

But here’s the new part: we’ve added limits to the counter. This is because servo motors only accept position values between 0 and 179 degrees. If the counter goes below 0, we reset it to 0. If it goes above 179, we cap it at 179. This ensures we’re only sending valid positions to the servo.

if (digitalRead(DT) != currentStateCLK) {
  counter--;
  if (counter < 0)
    counter = 0;
} else {
  counter++;
  if (counter > 179)
    counter = 179;
}

Once we have the correct value, we use servo.write(counter) to move the servo to that angle. The new position is also printed to the Serial Monitor, so you can see exactly where the servo is pointing each time you turn the encoder.

servo.write(counter);
Serial.print("Position: ");
Serial.println(counter);

The loop() function, just like in the last example, is left empty. That’s because we’re using interrupts to handle everything in real time. The Arduino doesn’t need to spend time constantly checking the encoder’s position—it only reacts when a change actually happens.

void loop() {
  //Do some useful stuff here
}