Understanding Rotary Encoders
What is a Rotary Encoder?
A rotary encoder is an electromechanical device that converts angular position or motion into digital signals. Unlike potentiometers (which are analog), encoders provide unlimited rotation and precise position tracking.
Types:
Incremental encoders have two output channels (A and B) that produce square wave signals 90° out of phase (quadrature signals). The phase relationship determines rotation direction.
A rotary encoder is an electromechanical device that converts angular position or motion into digital signals. Unlike potentiometers (which are analog), encoders provide unlimited rotation and precise position tracking.
Types:
- Incremental Encoders: Detect direction and amount of rotation (most common hobby type)
- Absolute Encoders: Provide exact position at all times (typically expensive)
Incremental encoders have two output channels (A and B) that produce square wave signals 90° out of phase (quadrature signals). The phase relationship determines rotation direction.
Quadrature Signal Timing
Clockwise Rotation:
A: ¯¯|___|¯¯¯|___|¯¯¯|___|¯¯¯|
B: ___|¯¯¯|___|¯¯¯|___|¯¯¯|___
A leads B by 90°
Counter-Clockwise:
A: ___|¯¯¯|___|¯¯¯|___|¯¯¯|___
B: ¯¯|___|¯¯¯|___|¯¯¯|___|¯¯¯|
B leads A by 90°
A: ¯¯|___|¯¯¯|___|¯¯¯|___|¯¯¯|
B: ___|¯¯¯|___|¯¯¯|___|¯¯¯|___
A leads B by 90°
Counter-Clockwise:
A: ___|¯¯¯|___|¯¯¯|___|¯¯¯|___
B: ¯¯|___|¯¯¯|___|¯¯¯|___|¯¯¯|
B leads A by 90°
State Transition Table
| Previous A | Previous B | Current A | Current B | Direction |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | CW |
| 0 | 1 | 1 | 1 | CW |
| 1 | 1 | 1 | 0 | CW |
| 1 | 0 | 0 | 0 | CW |
| 0 | 0 | 1 | 0 | CCW |
| 1 | 0 | 1 | 1 | CCW |
| 1 | 1 | 0 | 1 | CCW |
| 0 | 1 | 0 | 0 | CCW |
Demo 1: Volume Control Simulator
Circuit Diagram
Rotary Encoder (KY-040 or similar)
CLK (A) ---[10kΩ pull-up]--- +5V
|
+---> Pin 2 (INT0)
DT (B) ---[10kΩ pull-up]--- +5V
|
+---> Pin 3 (INT1)
SW ---[10kΩ pull-up]--- +5V
|
+---> Pin 4
GND ---> GND
+ ---> +5V (some modules)
Note: Many encoder modules have built-in pull-ups
Theory
Interrupt-Driven Design: Using hardware interrupts ensures no rotation pulses are missed, even when the main loop is busy. Arduino Uno has two external interrupts:
- INT0 on Pin 2
- INT1 on Pin 3
Calculations
Resolution:
Typical hobby encoders: 20 pulses per revolution (PPR)
With quadrature decoding: 20 × 4 = 80 counts per revolution
Angular Resolution:
360° / 80 counts = 4.5° per count
Debounce Time:
Typical mechanical bounce: 5-50ms
Software filter: Ignore transitions < 1ms apart
Minimum time = 1000 microseconds
Typical hobby encoders: 20 pulses per revolution (PPR)
With quadrature decoding: 20 × 4 = 80 counts per revolution
Angular Resolution:
360° / 80 counts = 4.5° per count
Debounce Time:
Typical mechanical bounce: 5-50ms
Software filter: Ignore transitions < 1ms apart
Minimum time = 1000 microseconds
Arduino Code
// Rotary Encoder Volume Control
const int PIN_A = 2; // CLK
const int PIN_B = 3; // DT
const int PIN_SW = 4; // Switch (button)
volatile int encoderPos = 50; // Start at 50% volume
volatile int lastEncoded = 0;
volatile unsigned long lastInterrupt = 0;
int volume = 50; // 0-100%
bool muted = false;
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
// Attach interrupts
attachInterrupt(digitalPinToInterrupt(PIN_A), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), updateEncoder, CHANGE);
Serial.begin(9600);
Serial.println("Volume Control Ready");
printVolume();
}
void loop() {
// Update volume from encoder position
static int lastPos = encoderPos;
if(encoderPos != lastPos) {
volume = constrain(encoderPos, 0, 100);
lastPos = encoderPos;
printVolume();
}
// Check for button press (mute/unmute)
static bool lastButton = HIGH;
bool currentButton = digitalRead(PIN_SW);
if(lastButton == HIGH && currentButton == LOW) {
delay(50); // Debounce
if(digitalRead(PIN_SW) == LOW) {
muted = !muted;
Serial.print("MUTED: ");
Serial.println(muted ? "ON" : "OFF");
}
}
lastButton = currentButton;
delay(10);
}
void updateEncoder() {
// Debouncing
unsigned long interruptTime = micros();
if(interruptTime - lastInterrupt < 1000) return;
lastInterrupt = interruptTime;
// Read current state
int MSB = digitalRead(PIN_A);
int LSB = digitalRead(PIN_B);
int encoded = (MSB < 1) | LSB;
int sum = (lastEncoded < 2) | encoded;
// Determine direction
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
encoderPos++;
}
else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
encoderPos--;
}
lastEncoded = encoded;
}
void printVolume() {
Serial.print("Volume: ");
Serial.print(volume);
Serial.print("% ");
// Visual bar
Serial.print("[");
for(int i = 0; i < 20; i++) {
if(i < volume / 5) Serial.print("=");
else Serial.print(" ");
}
Serial.println("]");
}
const int PIN_A = 2; // CLK
const int PIN_B = 3; // DT
const int PIN_SW = 4; // Switch (button)
volatile int encoderPos = 50; // Start at 50% volume
volatile int lastEncoded = 0;
volatile unsigned long lastInterrupt = 0;
int volume = 50; // 0-100%
bool muted = false;
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
// Attach interrupts
attachInterrupt(digitalPinToInterrupt(PIN_A), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), updateEncoder, CHANGE);
Serial.begin(9600);
Serial.println("Volume Control Ready");
printVolume();
}
void loop() {
// Update volume from encoder position
static int lastPos = encoderPos;
if(encoderPos != lastPos) {
volume = constrain(encoderPos, 0, 100);
lastPos = encoderPos;
printVolume();
}
// Check for button press (mute/unmute)
static bool lastButton = HIGH;
bool currentButton = digitalRead(PIN_SW);
if(lastButton == HIGH && currentButton == LOW) {
delay(50); // Debounce
if(digitalRead(PIN_SW) == LOW) {
muted = !muted;
Serial.print("MUTED: ");
Serial.println(muted ? "ON" : "OFF");
}
}
lastButton = currentButton;
delay(10);
}
void updateEncoder() {
// Debouncing
unsigned long interruptTime = micros();
if(interruptTime - lastInterrupt < 1000) return;
lastInterrupt = interruptTime;
// Read current state
int MSB = digitalRead(PIN_A);
int LSB = digitalRead(PIN_B);
int encoded = (MSB < 1) | LSB;
int sum = (lastEncoded < 2) | encoded;
// Determine direction
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
encoderPos++;
}
else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
encoderPos--;
}
lastEncoded = encoded;
}
void printVolume() {
Serial.print("Volume: ");
Serial.print(volume);
Serial.print("% ");
// Visual bar
Serial.print("[");
for(int i = 0; i < 20; i++) {
if(i < volume / 5) Serial.print("=");
else Serial.print(" ");
}
Serial.println("]");
}
Use Cases
- Audio Equipment: Volume, bass, treble controls
- User Interfaces: Menu navigation, value adjustment
- Robotics: Manual positioning, speed control
- 3D Printers: Manual axis control
Demo 2: Digital Position Counter with Direction Display
Circuit Diagram
Same as Demo 1, plus 3 LEDs:
Pin 9 ---[220Ω]---LED(Green)---|>|---GND (CW indicator)
Pin 10 ---[220Ω]---LED(Red)-----|>|---GND (CCW indicator)
Pin 11 ---[220Ω]---LED(Blue)----|>|---GND (Position zero)
Theory
Absolute Position Tracking: By counting all encoder transitions, we can track absolute position from a known starting point (zero reference).
Applications in Motion Control:
Applications in Motion Control:
- CNC Machines: Track tool position
- Robotics: Joint angle tracking
- Automated Systems: Linear actuator positioning
Calculations
Position Calculation:
If encoder has 20 PPR and we want to track degrees:
Counts per revolution = 20 × 4 = 80
Degrees per count = 360° / 80 = 4.5°
Position (degrees) = count × 4.5°
Linear Motion Example:
If encoder drives a lead screw with 2mm pitch:
Linear distance = (count / 80) × 2mm = count × 0.025mm
If encoder has 20 PPR and we want to track degrees:
Counts per revolution = 20 × 4 = 80
Degrees per count = 360° / 80 = 4.5°
Position (degrees) = count × 4.5°
Linear Motion Example:
If encoder drives a lead screw with 2mm pitch:
Linear distance = (count / 80) × 2mm = count × 0.025mm
Arduino Code
// Position Counter with Direction Display
const int PIN_A = 2;
const int PIN_B = 3;
const int PIN_SW = 4;
const int LED_CW = 9; // Green - Clockwise
const int LED_CCW = 10; // Red - Counter-clockwise
const int LED_ZERO = 11; // Blue - At zero position
volatile long encoderCount = 0;
volatile int lastEncoded = 0;
volatile unsigned long lastInterrupt = 0;
volatile int lastDirection = 0; // 1=CW, -1=CCW, 0=stopped
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
pinMode(LED_CW, OUTPUT);
pinMode(LED_CCW, OUTPUT);
pinMode(LED_ZERO, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PIN_A), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), updateEncoder, CHANGE);
Serial.begin(9600);
Serial.println("Position Counter Ready");
Serial.println("Press button to reset position");
}
void loop() {
// Update display
static long lastCount = -999;
if(encoderCount != lastCount) {
displayPosition();
lastCount = encoderCount;
}
// Update direction LEDs
if(lastDirection > 0) {
digitalWrite(LED_CW, HIGH);
digitalWrite(LED_CCW, LOW);
} else if(lastDirection < 0) {
digitalWrite(LED_CW, LOW);
digitalWrite(LED_CCW, HIGH);
} else {
digitalWrite(LED_CW, LOW);
digitalWrite(LED_CCW, LOW);
}
// Zero indicator
digitalWrite(LED_ZERO, encoderCount == 0 ? HIGH : LOW);
// Reset button
if(digitalRead(PIN_SW) == LOW) {
delay(50);
if(digitalRead(PIN_SW) == LOW) {
encoderCount = 0;
Serial.println("\n*** POSITION RESET TO ZERO ***\n");
delay(200);
}
}
delay(100);
}
void updateEncoder() {
unsigned long interruptTime = micros();
if(interruptTime - lastInterrupt < 1000) return;
lastInterrupt = interruptTime;
int MSB = digitalRead(PIN_A);
int LSB = digitalRead(PIN_B);
int encoded = (MSB < 1) | LSB;
int sum = (lastEncoded < 2) | encoded;
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
encoderCount++;
lastDirection = 1;
}
else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
encoderCount--;
lastDirection = -1;
}
lastEncoded = encoded;
}
void displayPosition() {
// Calculate degrees (assuming 20 PPR encoder)
float degrees = (encoderCount % 80) * 4.5;
int revolutions = encoderCount / 80;
Serial.print("Count: ");
Serial.print(encoderCount);
Serial.print(" | Rev: ");
Serial.print(revolutions);
Serial.print(" | Angle: ");
Serial.print(degrees, 1);
Serial.print("° | Dir: ");
Serial.println(lastDirection > 0 ? "CW" : (lastDirection < 0 ? "CCW" : "STOP"));
}
const int PIN_A = 2;
const int PIN_B = 3;
const int PIN_SW = 4;
const int LED_CW = 9; // Green - Clockwise
const int LED_CCW = 10; // Red - Counter-clockwise
const int LED_ZERO = 11; // Blue - At zero position
volatile long encoderCount = 0;
volatile int lastEncoded = 0;
volatile unsigned long lastInterrupt = 0;
volatile int lastDirection = 0; // 1=CW, -1=CCW, 0=stopped
void setup() {
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
pinMode(LED_CW, OUTPUT);
pinMode(LED_CCW, OUTPUT);
pinMode(LED_ZERO, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PIN_A), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), updateEncoder, CHANGE);
Serial.begin(9600);
Serial.println("Position Counter Ready");
Serial.println("Press button to reset position");
}
void loop() {
// Update display
static long lastCount = -999;
if(encoderCount != lastCount) {
displayPosition();
lastCount = encoderCount;
}
// Update direction LEDs
if(lastDirection > 0) {
digitalWrite(LED_CW, HIGH);
digitalWrite(LED_CCW, LOW);
} else if(lastDirection < 0) {
digitalWrite(LED_CW, LOW);
digitalWrite(LED_CCW, HIGH);
} else {
digitalWrite(LED_CW, LOW);
digitalWrite(LED_CCW, LOW);
}
// Zero indicator
digitalWrite(LED_ZERO, encoderCount == 0 ? HIGH : LOW);
// Reset button
if(digitalRead(PIN_SW) == LOW) {
delay(50);
if(digitalRead(PIN_SW) == LOW) {
encoderCount = 0;
Serial.println("\n*** POSITION RESET TO ZERO ***\n");
delay(200);
}
}
delay(100);
}
void updateEncoder() {
unsigned long interruptTime = micros();
if(interruptTime - lastInterrupt < 1000) return;
lastInterrupt = interruptTime;
int MSB = digitalRead(PIN_A);
int LSB = digitalRead(PIN_B);
int encoded = (MSB < 1) | LSB;
int sum = (lastEncoded < 2) | encoded;
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
encoderCount++;
lastDirection = 1;
}
else if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
encoderCount--;
lastDirection = -1;
}
lastEncoded = encoded;
}
void displayPosition() {
// Calculate degrees (assuming 20 PPR encoder)
float degrees = (encoderCount % 80) * 4.5;
int revolutions = encoderCount / 80;
Serial.print("Count: ");
Serial.print(encoderCount);
Serial.print(" | Rev: ");
Serial.print(revolutions);
Serial.print(" | Angle: ");
Serial.print(degrees, 1);
Serial.print("° | Dir: ");
Serial.println(lastDirection > 0 ? "CW" : (lastDirection < 0 ? "CCW" : "STOP"));
}
Use Cases
- Motor Position Feedback: Closed-loop control systems
- Robotic Arms: Joint angle measurement
- Scientific Instruments: Precision positioning
- Gaming Controllers: Infinite rotation input devices