Zbotic Logo Zbotic Logo
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

  • Shop
  • About Us
  • Contact Us
  • Reseller
  • Blogs
020 69134444
1800 209 0998
[email protected]
Help Desk
Facebook Twitter Instagram Linkedin YouTube
Zbotic Logo Zbotic Logo
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

All departments
  • 3D Print Service
  • 3D Printer
  • Batteries & Chargers
  • Development Boards
  • Drone Parts
  • EBike parts
  • Sensor Modules
  • Electronic Components
  • Electronic Modules
  • IoT and Wireless
  • Mechanical Parts and Workbench Tools
  • Motors & Drivers & Pumps & Actuators
  • DIY and Robot Kits
  • Show more
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
Return to previous page
Home Arduino & Microcontrollers

Arduino Rotary Encoder: Menu Navigation and Position Sensing

Arduino Rotary Encoder: Menu Navigation and Position Sensing

March 11, 2026 /Posted byJayesh Jain / 0

A rotary encoder is one of the most versatile input devices you can connect to an Arduino. Unlike a potentiometer, it has infinite rotation and generates digital pulses — making it ideal for both arduino rotary encoder tutorial applications like menu navigation, volume controls, and stepper motor positioning. This comprehensive guide covers the fundamentals of how encoders work, proper debouncing, interrupt-driven reading, and three practical projects that demonstrate the full range of encoder capabilities in real embedded systems.

Table of Contents

  • How Rotary Encoders Work
  • Types of Rotary Encoders
  • Wiring KY-040 to Arduino
  • Basic Encoder Reading with Polling
  • Interrupt-Driven Reading (Better Approach)
  • Debouncing Encoders Properly
  • Project 1: LCD Menu Navigation
  • Project 2: Absolute Position Tracking
  • Project 3: Stepper Motor Speed Control
  • Frequently Asked Questions

How Rotary Encoders Work

A mechanical rotary encoder contains two internal switches (called channels A and B) connected to a rotary disc with evenly spaced slots. As you turn the shaft, the slots alternately break and complete the circuit for each channel, producing a square wave output.

The key insight is in the phase relationship between channels A and B. When turning clockwise, channel A transitions before channel B — when turning counter-clockwise, B transitions before A. By reading which channel changes first, you determine rotation direction. The number of transitions tells you how far it has rotated.

A typical KY-040 module produces 20 pulses per revolution (called 20 PPR or 20 steps/rev). Each pulse consists of 4 state transitions (A rises, B rises, A falls, B falls), giving 80 total state changes per revolution if you count all edges. Most applications count only one edge per click, so the encoder produces 20 detents per revolution.

Types of Rotary Encoders

Mechanical encoders (KY-040, EC11): Use physical contacts. Inexpensive (₹30–80), but generate contact bounce that must be filtered. Best for human-interface applications where speed is low (<100 RPM). The KY-040 includes a pushbutton by pressing the shaft — this is the select/enter button in most UI applications.

Optical encoders: Use a slotted disc and IR LED/photodetector pair. No contact bounce, very high resolution (up to 10,000 PPR), excellent for motor feedback and CNC positioning. More expensive (₹500+) and require 5V supply rather than the 3.3V some MCUs use.

Magnetic encoders (AS5048, AS5600): Use a rotating magnet and Hall-effect sensor. Zero mechanical wear, immune to contamination, and output absolute position over I2C/SPI. Ideal for permanent installations where the encoder shaft connects to a motor.

This tutorial focuses on the KY-040 mechanical encoder, which is by far the most common in Arduino projects.

Recommended: Arduino Uno R3 Beginners Kit — Includes the Arduino Uno R3, breadboard, and jumper wires — the ideal platform for your first rotary encoder project.

Wiring KY-040 to Arduino

The KY-040 module has 5 pins:

KY-040 Pin Function Arduino Pin
CLK (A) Channel A output D2 (INT0)
DT (B) Channel B output D3 (INT1)
SW Push button D4
+ VCC 5V
GND Ground GND

Connect CLK to D2 and DT to D3 to use hardware interrupts (INT0 and INT1 on Uno). The KY-040 module includes pull-up resistors on CLK and DT, so no external resistors are needed. For the SW (button) pin, enable Arduino’s internal pull-up in code.

Basic Encoder Reading with Polling

The simplest approach — polling — checks the encoder state in each loop iteration. This works for slow human-speed turning but misses steps if the loop contains other slow operations.

const int CLK = 2;
const int DT  = 3;
const int SW  = 4;

int lastClkState;
int counter = 0;

void setup() {
  pinMode(CLK, INPUT);
  pinMode(DT,  INPUT);
  pinMode(SW,  INPUT_PULLUP);
  Serial.begin(9600);
  lastClkState = digitalRead(CLK);
}

void loop() {
  int clkState = digitalRead(CLK);
  if (clkState != lastClkState) {
    if (digitalRead(DT) != clkState) {
      counter++;  // Clockwise
    } else {
      counter--;  // Counter-clockwise
    }
    Serial.print("Position: ");
    Serial.println(counter);
  }
  lastClkState = clkState;

  if (digitalRead(SW) == LOW) {
    Serial.println("Button pressed!");
    delay(200); // Simple debounce
  }
}

The direction detection works by comparing whether DT matches CLK on the transition. If they’re different (out of phase), the rotation is clockwise; if the same (in phase due to bouncing), it’s counter-clockwise. This is a simplified state check — more robust implementations use a 4-state lookup table.

Interrupt-Driven Reading (Better Approach)

Hardware interrupts fire immediately when a pin changes state, regardless of what the main loop is doing. This is the correct approach for encoders — you’ll never miss a step, even if the loop is busy with display updates or serial communication.

const int CLK = 2;
const int DT  = 3;

volatile int counter = 0;
volatile uint8_t lastAB = 0;

// 4-state transition table: -1, 0, or +1
const int8_t ENC_STATES[] = {0,-1,1,0, 1,0,0,-1, -1,0,0,1, 0,1,-1,0};

void encoderISR() {
  uint8_t AB = (digitalRead(CLK) << 1) | digitalRead(DT);
  uint8_t idx = (lastAB << 2) | AB;
  counter += ENC_STATES[idx];
  lastAB = AB;
}

void setup() {
  pinMode(CLK, INPUT);
  pinMode(DT,  INPUT);
  attachInterrupt(digitalPinToInterrupt(CLK), encoderISR, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DT),  encoderISR, CHANGE);
  Serial.begin(9600);
}

void loop() {
  static int lastCount = 0;
  int current = counter; // atomic copy
  if (current != lastCount) {
    Serial.println(current);
    lastCount = current;
  }
}

The 16-element ENC_STATES lookup table handles all 16 possible 4-state transitions and returns +1, -1, or 0 depending on valid vs. invalid (bounce) transitions. This is the most robust encoder decoding technique — used in professional motion control systems.

Debouncing Encoders Properly

Mechanical encoder contacts bounce — they make and break contact multiple times in the first few milliseconds of each click. Poor debouncing causes missed steps or counted steps in the wrong direction.

Hardware debouncing: Add a 10nF capacitor between each signal pin (CLK, DT) and GND, plus a 10kΩ resistor in series with the signal. The RC circuit low-passes the bounce, presenting a clean transition to the Arduino.

Software debouncing: The ENC_STATES lookup table approach above handles most bounce automatically by rejecting invalid state transitions. Alternatively, add a minimum time check between counted transitions (10–20ms is typical for mechanical encoders).

Using the Encoder library: Install the Encoder library by Paul Stoffregen from Library Manager. It automatically uses interrupts and handles debouncing:

#include <Encoder.h>
Encoder myEnc(2, 3);  // CLK on 2, DT on 3

void loop() {
  long newPos = myEnc.read();
  // Divide by 4 for detent-based counting
  // (encoder produces 4 transitions per click)
  Serial.println(newPos / 4);
}

The library returns counts in units of 4 (one per transition edge), so divide by 4 to get the number of physical detent clicks.

Recommended: 1.8 Inch SPI TFT LCD Display Module — Combine with your encoder for a colour menu display. Small enough for panel mounting in custom enclosures.

Project 1: LCD Menu Navigation

This is the classic rotary encoder application — scroll through menu items by rotating and press the shaft to select. We’ll use a 16×2 LCD with I2C backpack.

#include <Encoder.h>
#include <LiquidCrystal_I2C.h>

Encoder enc(2, 3);
LiquidCrystal_I2C lcd(0x27, 16, 2);

const char* menuItems[] = {"Temperature", "Humidity", "Pressure", "Settings", "About"};
const int MENU_SIZE = 5;

int menuIndex = 0;
long lastEncPos = 0;

void setup() {
  lcd.init();
  lcd.backlight();
  pinMode(4, INPUT_PULLUP); // SW pin
  displayMenu();
}

void displayMenu() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("> ");
  lcd.print(menuItems[menuIndex]);
  int nextIdx = (menuIndex + 1) % MENU_SIZE;
  lcd.setCursor(0, 1);
  lcd.print("  ");
  lcd.print(menuItems[nextIdx]);
}

void loop() {
  long encPos = enc.read() / 4;
  if (encPos != lastEncPos) {
    int delta = encPos - lastEncPos;
    menuIndex = (menuIndex + delta + MENU_SIZE) % MENU_SIZE;
    lastEncPos = encPos;
    displayMenu();
  }
  if (digitalRead(4) == LOW) {
    lcd.clear();
    lcd.print("Selected:");
    lcd.setCursor(0, 1);
    lcd.print(menuItems[menuIndex]);
    delay(1500); // Show selection
    displayMenu();
    delay(300); // Button debounce
  }
}

The modulo arithmetic (% MENU_SIZE) wraps the menu index so it rolls from the last item back to the first and vice versa — creating a circular menu. The cursor symbol > indicates the currently selected item, and the second line shows the next item for context.

Project 2: Absolute Position Tracking

For robotics and CNC applications, you need to track absolute position across multiple revolutions. This example tracks a rotating arm with home-position detection.

#include <Encoder.h>

Encoder enc(2, 3);
const int HOME_SWITCH = 5;  // Limit switch at home position
const int PPR = 20;          // Pulses per revolution

bool isHomed = false;
long homeOffset = 0;

float getAngleDegrees() {
  long rawCounts = enc.read() / 4;
  if (!isHomed) return 0;
  long countsFromHome = rawCounts - homeOffset;
  return (countsFromHome % PPR) * (360.0 / PPR);
}

void homeEncoder() {
  // Slowly rotate until home switch triggers
  // (Motor control code depends on your driver)
  while (digitalRead(HOME_SWITCH) == HIGH) {
    delay(10); // Wait for home switch
  }
  homeOffset = enc.read() / 4;
  isHomed = true;
  Serial.println("Homed successfully");
}

void setup() {
  Serial.begin(9600);
  pinMode(HOME_SWITCH, INPUT_PULLUP);
  homeEncoder();
}

void loop() {
  Serial.print("Angle: ");
  Serial.print(getAngleDegrees());
  Serial.println(" degrees");
  delay(100);
}

The homing sequence finds a known reference position (limit switch), then all subsequent angles are calculated relative to that home. This is exactly how CNC machines and 3D printers find their origin at startup.

Project 3: Stepper Motor Speed Control

Use the encoder as a variable speed control for a stepper motor — turning clockwise increases speed, counter-clockwise decreases it. The encoder’s push button starts/stops the motor.

#include <Encoder.h>
#include <AccelStepper.h>

Encoder enc(2, 3);
AccelStepper stepper(AccelStepper::DRIVER, 8, 9); // STEP, DIR

long lastEncPos = 0;
int motorSpeed = 0;
bool running = false;

void setup() {
  pinMode(4, INPUT_PULLUP); // Encoder button
  stepper.setMaxSpeed(1000);
  stepper.setAcceleration(200);
  Serial.begin(9600);
}

void loop() {
  long encPos = enc.read() / 4;
  if (encPos != lastEncPos) {
    motorSpeed = constrain(motorSpeed + (encPos - lastEncPos) * 50, -1000, 1000);
    lastEncPos = encPos;
    stepper.setSpeed(running ? motorSpeed : 0);
    Serial.print("Speed: "); Serial.println(motorSpeed);
  }
  if (digitalRead(4) == LOW) {
    running = !running;
    stepper.setSpeed(running ? motorSpeed : 0);
    delay(300);
  }
  stepper.runSpeed();
}

Each encoder click changes speed by 50 steps/second. The constrain() function limits speed to -1000/+1000 (negative for reverse direction). The AccelStepper library handles the step pulse timing, freeing the main loop to read the encoder continuously.

Recommended: 3D Printer Controller Board RAMPS 1.4 for Arduino Mega — RAMPS 1.4 includes stepper driver slots and encoder headers — perfect for CNC and 3D printer projects that combine rotary encoders with stepper motors.
Recommended: Arduino Mega 2560 R3 Board — When using multiple encoders for multi-axis positioning (X/Y/Z), the Mega provides the interrupt pins and processing power needed.

Frequently Asked Questions

Why does my encoder skip steps or count in the wrong direction?

Skipping is usually caused by contact bounce without proper debouncing, or by polling-based reading when the loop is too slow. Switch to interrupt-driven reading with the Encoder library or the ENC_STATES lookup table approach. Wrong direction is simply the A/B channels being swapped — swap the CLK and DT wires or multiply the counter by -1 in code.

Can I use a rotary encoder on any Arduino pin?

For polling, yes — any digital pin works. For interrupt-driven reading, you need interrupt-capable pins. On Arduino Uno, only D2 and D3 support hardware interrupts. On Nano, same: D2 and D3. On Mega 2560, pins 2, 3, 18, 19, 20, and 21 support interrupts — allowing up to 3 encoders. The Encoder library also supports pin-change interrupts on any pin (slower but works).

What’s the difference between incremental and absolute encoders?

An incremental encoder (like KY-040) outputs pulses as it rotates but doesn’t know its absolute position — it counts from wherever it started. If you power-cycle it, position is lost. An absolute encoder (like AS5048) outputs a unique code for every shaft position and retains absolute position even without power. Incremental encoders are cheaper; absolute encoders are essential for applications where home calibration is impractical.

How do I use multiple encoders with one Arduino?

On Arduino Mega, use interrupt-capable pins (2, 3, 18, 19, 20, 21) for up to 3 encoders simultaneously. For more, use PCF8574 I2C GPIO expanders with the Encoder library’s software pin-change mode. Each encoder needs its own Encoder object in code.

Why does my encoder’s button have contact bounce too?

Yes, the push button inside a KY-040 bounces just like any mechanical switch. Use a 200ms delay after detecting a press (as in the menu project above), or use a proper software debounce routine that requires the pin to be stable for 50ms before registering a press. The Bounce2 library handles this elegantly.

Ready to start your rotary encoder project? Shop our complete range of Arduino boards and modules at Zbotic — quality components with fast delivery across India.

Tags: Arduino, encoder library, KY-040, menu navigation, position sensing, rotary encoder, tutorial
Share Post
  • Facebook
  • Linkedin
  • Whatsapp
Arduino OLED Display SSD1306: ...
blog arduino oled display ssd1306 menu and graphics tutorial 594720
blog arduino i2c deep dive multiple sensors on two wires 594724
Arduino I2C Deep Dive: Multipl...

Related posts

Svg%3E
Read more

Arduino Batch Programming: Flash Multiple Boards Quickly

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Based Radar System with Ultrasonic Sensor

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Automatic Plant Monitor: Sunlight, Moisture, Temperature

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Lie Detector: GSR Sensor Polygraph Project

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Metal Detector: Build a Treasure Finder

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading

Add comment Cancel reply

Your email address will not be published. Required fields are marked

Facebook Twitter Instagram Pinterest Linkedin Youtube

Get the latest deals and more.

Download on Google Play Download on the App Store

Call us: 020 69134444 / 1800 209 0998

Monday - Saturday 09:30 AM - 06:00 PM
For Technical Supports Email: [email protected]
For Sales / Enquiries Email: [email protected]

  • My Account

    • Cart

    • Wishlist

    • Checkout

    • My Orders

    • Track Order

    • My Account

  • Information

    • FAQs

    • Blogs

    • Career

    • About Us

    • Contact Us

    • Payment Options

  • Policies

    • Privacy Policy

    • Terms & Conditions

    • GST Input Tax Credit

    • Shipping Return Policy

    • E-Waste Collection Points

    • Our Sitemap

© Zbotic.in is registered trademark of Moxie Supply Pvt Ltd – All Rights Reserved
Login
Use Phone Number
Use Email Address
Not a member yet? Register Now
Reset Password
Use Phone Number
Use Email Address
Register
Already a member? Login Now