How Rotary Encoder Works and Interface It with Arduino

Rotary encoders are used in the myriad of common devices we see every day. From printers and photographic lenses to CNC machines and robotics, rotary encoders are closer to us than we think. The most popular example of the use of a rotary encoder in everyday life is the volume control knob of a car radio.

A rotary encoder is a type of position sensor that converts the angular position (rotation) of a knob into an output signal that is used to determine which direction the knob is being turned.

There are two types of rotary encoders – absolute and incremental. The absolute encoder reports the exact position of the knob in degrees while the incremental encoder reports how many increments the shaft has moved.

The rotary encoder used in this tutorial is of the incremental type.

Rotary Encoders Vs Potentiometers

Rotary encoders are the modern digital equivalent of potentiometers and are more versatile than them.

They can rotate 360° without stopping whereas potentiometers can only rotate 3/4 of the circle.

Potentiometers are used in situations where you need to know the exact position of the knob. Whereas, rotary encoders are used in situations where you need to know the change in position rather than the exact position.

How Rotary Encoders Work?

Inside the encoder is a slotted disc that is connected to the common ground pin C. It also contains two contact pins A and B, as shown below.

rotary encoder internal structure

When you turn the knob, A and B come into contact with the common ground pin C in a particular order according to the direction in which you are turning the knob.

When they come into contact with common ground, signals are generated. These signals are 90° out of phase with each other as one pin comes into contact with the common ground before the other pin. This is called quadrature encoding.

rotary encoder working animation.gif

When you turn the knob clockwise the A pin connects to the ground before the B pin. And when you turn the knob counterclockwise, the B pin connects to the ground before the A pin.

By tracking when each pin connects to or disconnects from ground, we can determine which direction the knob is being rotated. You can do this by simply observing the state of B when A changes state.

When A changes state:

  • if B != A, then the knob is turned clockwise.
    rotary encoder output pulses in clockwise rotation
  • if B = A, the knob is turned counterclockwise.
    rotary encoder output pulses in anticlockwise rotation

Rotary Encoder Pinout

The pinouts of the rotary encoder are as follows:

rotary encoder module pinout

GND is the ground connection.

VCC is the positive supply voltage, typically 3.3 – 5 volts.

SW is the active low push button switch output. When the knob is pressed down, the voltage goes LOW.

DT (Output B) is similar to CLK output, but it lags behind CLK by a 90° phase shift. This output is used to determine the direction of rotation.

CLK (Output A) is the primary output pulse to determine the amount of rotation. Each time the knob is turned in either direction by just one detent (click), the ‘CLK’ output goes through one cycle of going HIGH and then LOW.

Wiring a Rotary Encoder to an Arduino

Now that we know everything about the rotary encoder it’s time to put it to use!

Let’s connect the rotary encoder to the Arduino. The connections are quite simple. Start by connecting the +V pin on the module to 5V on the Arduino and the GND pin to Ground.

Now connect the CLK and DT pins to digital pins #2 and #3 respectively. Lastly connect the SW pin to digital pin #4.

The following illustration shows the wiring.

wiring rotary encoder with arduino uno

Arduino Code – Reading Rotary Encoders

Now that you have connected your encoder you will need a sketch to make it work.

The following sketch detects which direction the encoder is being rotated and when the button is being pushed.

Try this sketch first and then we will go into detail about it.

// 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);
}

If all goes well, you will see an output like this on the serial monitor.

rotary encoder output on serial monitor

If the rotation being reported is the opposite of what you expect, try swapping the CLK (Output A) and DT (Output B) pins.

Code Explanation:

The sketch begins by declaring the Arduino pins to which the CLK, DT and SW pins of the encoder are connected.

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

After this some integers are defined.

  • The counter variable will count each time the knob is rotated one detent (click).
  • The currentStateCLK and lastStateCLK variables hold the state of the CLK output and are used to determine the amount of rotation.
  • A string named currentDir will be used to print the current direction of rotation on the serial monitor.
  • The lastButtonPress variable is used to debounce a switch.
int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir ="";
unsigned long lastButtonPress = 0;

In the setup section, we first configure the connections to the rotary encoder as inputs, then we enable the input pullup resistor on the SW pin. We also setup the serial monitor.

Finally we read the current value of the CLK pin and store it in the lastStateCLK variable.

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

Serial.begin(9600);

lastStateCLK = digitalRead(CLK);

In the loop section, we check the CLK state again and compare it with the lastStateCLK value. If they are different it means the knob has turned. We also check if currentStateCLK is 1 to react to only one state change and to avoid double counting.

currentStateCLK = digitalRead(CLK);

if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

Inside the if statement we find out the direction of rotation. To do this we simply read the DT pin and compare it with the current state of the CLK pin.

  • If these two values ​​are different, it means that the knob is turned counterclockwise. We then decrement the counter and set the currentDir to “CCW”.
  • If these two values ​​are equal, it means that the knob is turned clockwise. We then increment the counter and set the currentDir to “CW”.
if (digitalRead(DT) != currentStateCLK) {
    counter --;
    currentDir ="CCW";
} else {
    counter ++;
    currentDir ="CW";
}

Then we print our results on the serial monitor.

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

Outside the if statement, we update lastStateCLK with the current state of CLK.

lastStateCLK = currentStateCLK;

Next comes the logic of reading and debouncing the push button switch. We first read the current button state, when it goes LOW, we wait 50ms for the push button to debounce.

If the button remains LOW for more than 50ms, it means that the button is really pressed. So we print the message “Button pressed!” on the serial monitor.

int btnState = digitalRead(SW);

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

Then we do it all over again.

Arduino Code – Using Interrupts

In order for the rotary encoder to work, we need to continuously monitor changes in the DT and CLK signals.

One way to know when these changes happen is to continuously poll them (like we did in our last sketch). However, it is not the best solution for the following reasons.

  • We have to busily perform checking to see if the value has changed. There will be a waste of cycles if signal level does not change.
  • There will be latency from the time the event occurs to the time we check. If we need to react quickly, we will be delayed because of this latency.
  • It is possible to miss the signal change entirely, if the duration of the change is short.

A widely adopted solution is the use of interrupts.

With interrupts you don’t need to continuously poll the specific event. This frees up the Arduino to do some other work while not missing the event.

Wiring

Since most Arduino boards (including Arduino UNO) have only two external interrupts, we can only monitor changes in the DT and CLK signals. So remove the connection for the SW pin from the previous wiring diagram.

So now the wiring looks like this:

control rotary encoder using interrupts with arduino uno

Some boards (like the Arduino Mega 2560) have more external interrupts. If you have one of those, you can keep the connection for the SW pin and extend the sketch below to include the code for the push button.

Arduino Code

Here is a sketch that demonstrates how to read a rotary encoder using interrupts.

// 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 any high/low changed seen
	// on interrupt 0 (pin 2), or interrupt 1 (pin 3)
	attachInterrupt(0, updateEncoder, CHANGE);
	attachInterrupt(1, 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;
}

Notice that the main loop of this program is kept empty so Arduino will be busy doing nothing.

Now try turning the knob. You will see an output like this on the serial monitor.

rotary encoder interrupt output on serial monitor

Code Explanation:

This program watches digital pin 2 (corresponding to interrupt 0) and digital pin 3 (corresponding to interrupt 1) for a change in value. In other words, it looks for a voltage change going from HIGH to LOW or LOW to HIGH, which happens when you turn the knob.

When a change occurs, the function updateEncoder() (known as the Interrupt Service Routine or just ISR) is called automatically. The code within this function is executed and then the program returns back to what it was doing before.

The following two lines are responsible for all this. The function attachInterrupt() tells the Arduino which pin to monitor, which ISR to execute when the interrupt is triggered and what type of trigger to look for.

attachInterrupt(0, updateEncoder, CHANGE);
attachInterrupt(1, updateEncoder, CHANGE);

Control Servo Motor with Rotary Encoder

For our next project we will be using a rotary encoder to control the position of a servo motor.

This project can be very useful in many situations, for example, if you want to operate a robotic arm it will help you to accurately position the arm and its grip.

If you’re not familiar with servo motors, consider reading (at least skimming) the tutorial below.

Wiring

Let’s add a servo motor to our project. Connect the servo motor’s red wire to the external 5V supply, the black/brown wire to ground, and the orange/yellow wire to the PWM enabled pin 9.

Of course you can use the Arduino’s 5V output, but be aware that the servo may induce electrical noise on the 5V line, which could probably damage your Arduino. Therefore it is recommended that you use an external power supply.

wiring for controlling servo motor with rotary encoder

Arduino Code

Here is the sketch to precisely control the servo motor with the rotary encoder. Each time the knob is rotated one detent (click) the position of the servo arm will change 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;

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);
}

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 --;
			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:

If you compare this sketch with our very first sketch, you will see many similarities except for a few things.

In the beginning we include the built-in Arduino Servo library and create a servo object to represent our servo motor.

#include <Servo.h>

Servo servo;

In the setup, we attach the servo object to pin 9 (to which the control pin of the servo motor is connected).

servo.attach(9);

In the loop, we limit the counter to the range 0 to 179, as a servo motor only accepts a value between this range.

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

Finally the counter value is used to position the servo motor.

servo.write(counter);