We are surrounded by rotary encoders without even realizing it, as they are used in so many everyday items, from printers and cameras to CNC machines and robots. The most common application of a rotary encoder is the volume knob on 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 can be used to determine which direction the knob is turned.
Rotary encoders are classified into two types: absolute and incremental. The absolute encoder reports the exact position of the knob in degrees, whereas the incremental encoder reports the number of 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.
Rotary encoders 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. Rotary encoders, on the other hand, 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 pin C, the common ground. It also has two contact pins A and B, as shown below.
When you turn the knob, A and B make contact with the common ground pin C in a specific order depending on which direction you turn the knob.
When they make contact with common ground, two signals are generated. These signals are 90° out of phase because one pin makes contact with common ground before the other. It is referred to as quadrature encoding.
When the knob is turned clockwise, the A pin connects to ground before the B pin. When the knob is turned anticlockwise, the B pin connects to ground before the A pin.
By monitoring when each pin connects or disconnects from ground, we can determine the direction in which the knob is being rotated. This can be accomplished by simply observing the state of B when A’s state changes.
When A changes state:
- if B != A, then the knob is turned clockwise.
- if B = A, the knob is turned counterclockwise.
Rotary Encoder Pinout
The pinout of the rotary encoder module is as follows:
GND is the ground connection.
VCC is the positive supply voltage, which is typically between 3.3 and 5 volts.
SW is the output of the push button switch (active low). When the knob is depressed, 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 used 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 understand how the rotary encoder works, it’s time to put it to use!
Let’s hook up the rotary encoder to the Arduino. The connections are quite simple. Begin by connecting the module’s +V pin to the Arduino’s 5V output and the GND pin to ground.
Now connect the CLK and DT pins to digital pins #2 and #3, respectively. Finally, connect the SW pin to digital pin #4.
The following image shows the wiring.
Arduino Example Code 1 – Reading Rotary Encoders
Our first example is very straightforward; it simply detects the rotation direction of the encoder and when the button is pressed.
First, try out the sketch, and then we’ll go over it 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);
}
You should see similar output in the serial monitor.
If the reported rotation 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 encoder’s CLK, DT, and SW pins are connected.
#define CLK 2
#define DT 3
#define SW 4
Following that, some variables are defined.
- The
counter
variable will increment each time the knob is rotated one detent (click). - The variables
currentStateCLK
andlastStateCLK
store the state of the CLK output and are used to calculate the amount of rotation. - A string named
currentDir
will be used to display the current rotational direction on the serial monitor. - The variable
lastButtonPress
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 rotary encoder connections as inputs, and then we enable the input pull-up on the SW pin. We also set up the serial monitor.
Finally, we read the current value of the CLK pin and store it in the variable lastStateCLK
.
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 to the lastStateCLK
value. If they differ, it indicates that the knob has been turned. We also check to see if currentStateCLK
is 1 in order to react to only one state change and avoid double counting.
currentStateCLK = digitalRead(CLK);
if (currentStateCLK != lastStateCLK && currentStateCLK == 1){
Within the if
statement, the direction of rotation is determined. To accomplish this, we simply read the DT pin and compare it to the current state of the CLK pin.
- If these two values differ, it indicates that the knob is turned anticlockwise. The
counter
is then decremented, and thecurrentDir
is set to “CCW”. - If these two values are identical, it indicates that the knob is turned clockwise. The
counter
is then incremented, and thecurrentDir
is set to “CW”.
if (digitalRead(DT) != currentStateCLK) {
counter --;
currentDir ="CCW";
} else {
counter ++;
currentDir ="CW";
}
The results are then printed to the serial monitor.
Serial.print("Direction: ");
Serial.print(currentDir);
Serial.print(" | Counter: ");
Serial.println(counter);
Following the if
statement, we update lastStateCLK
with the current state of CLK.
lastStateCLK = currentStateCLK;
The next step involves reading and debouncing the push button switch. We first read the current button state, and when it changes to LOW, we wait 50 ms for the push button to debounce.
If the button remains LOW for more than 50ms, it indicates that it has really been pressed. As a result, we print “Button pressed!” to the serial monitor.
int btnState = digitalRead(SW);
if (btnState == LOW) {
if (millis() - lastButtonPress > 50) {
Serial.println("Button pressed!");
}
lastButtonPress = millis();
}
Then we repeat the process.
Arduino Example Code 2 – Using Interrupts
To read the rotary encoder, we must constantly monitor changes in the DT and CLK signals.
One way to detect these changes is to poll them continuously, as we did in our previous sketch. It is, however, not the best solution for the following reasons.
- We must perform frequent checks to see if the value has changed. If the signal level does not change, there will be a waste of cycles.
- There is a possibility of latency between the time the event occurs and the time we check. If we need to react quickly, we will be delayed due to this latency.
- If the duration of the change is short, it is possible to completely miss the signal change.
One way to deal with this is to use interrupts.
With interrupts, there is no need to continuously poll for a specific event. This frees up the Arduino to perform other tasks without missing an event.
Wiring
Because most Arduino boards (including the Arduino UNO) only have two external interrupts, we can only monitor changes in the DT and CLK signals. Therefore, we will remove the SW pin connection.
Some boards (such as the Arduino Mega 2560) have more external interrupts than others. If you have one of these, you can keep the SW pin connection and modify the sketch below to include the push button code.
The updated layout of the wiring is as follows:
Arduino Code
Here’s an example of how to read a rotary encoder with 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;
}
Observe that the main loop of this program is left empty, so Arduino will be busy doing nothing.
Now, as you turn the knob, you should see similar output on the serial monitor.
Code Explanation:
This sketch simply monitors digital pins 2 (corresponding to interrupt 0) and 3 (corresponding to interrupt 1) for signal changes. In other words, it detects when the voltage changes from HIGH to LOW or LOW to HIGH as a result of turning the knob.
When a change occurs, the Arduino immediately detects it, saves its execution state, executes the function updateEncoder()
(also known as the Interrupt Service Routine or simply ISR), and then returns to whatever it was doing before.
The following two lines configure the interrupts. The attachInterrupt()
function instructs 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);
Arduino Example Code 3 – Controlling Servo Motor with Rotary Encoder
In the following example, we will use a rotary encoder to control the position of a servo motor.
This project can be very useful in a variety of situations. For example, if you want to operate a robotic arm, it can help you position the arm and its grip accurately.
If you are unfamiliar with servo motors, please read the following tutorial.
Wiring
Let’s include a servo motor in our project. Connect the red wire of the servo motor to the external 5V supply, the black/brown wire to ground, and the orange/yellow wire to PWM enabled digital pin 9.
You can, of course, use the Arduino’s 5V output, but keep in mind that the servo may induce electrical noise on the 5V supply line, which could damage your Arduino. Therefore, it is advised that you use an external power supply.
Arduino Code
Here is the code for using the rotary encoder to precisely control the servo motor. Each time the knob is rotated 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;
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 to our very first sketch, you will notice that, with a few exceptions, they are quite similar.
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 because servo motors only accept values within 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);