Rotary Encoder (KY-040)
A rotary encoder is an electromechanical transducer that converts angular rotation into a pair of quadrature-encoded digital signals (A and B). By decoding the relative phase of these signals, the direction and amount of rotation can be determined. The KY-040 module also includes a built-in push button (switch) on the shaft. Rotary encoders are used for volume knobs, menu navigation, motor speed control, and any application requiring infinite rotation (unlike potentiometers which have limited travel).
For this interfacing you need the following components:
- Arduino board (Uno, Nano, Mega, etc.)
- KY-040 rotary encoder module (or similar EC11 / PEC11 encoder)
- Breadboard and jumper wires
- USB cable to connect Arduino to your computer
Schematic
KY-040 Module Arduino
------------- -------
CLK (A) --> Digital Pin 2 (interrupt-capable)
DT (B) --> Digital Pin 3 (interrupt-capable)
SW (Switch) --> Digital Pin 4
VCC --> 5V
GND --> GND
Pins 2 and 3 are the only interrupt-capable pins on the Arduino Uno/Nano. Use them for low-latency rotation tracking.
Pin Map
| Module Pin | Name | Arduino Connection |
|---|---|---|
| CLK | Output A (clock) | Pin 2 |
| DT | Output B (data) | Pin 3 |
| SW | Push button (active LOW) | Pin 4 |
| VCC | Power | 5V |
| GND | Ground | GND |
- CLK and DT have internal pull-up resistors on the module. When the shaft rotates, they produce a 2-bit Gray code.
- SW is connected to GND internally when the shaft is pressed (no external pull-up needed if using
INPUT_PULLUP).
Install necessary Library
Install the Encoder library by Paul Stoffregen via the Library Manager (Tools > Manage Libraries).
Alternatively, using arduino-cli:
arduino-cli lib install "Encoder"
This library uses hardware interrupts on pins 2 and 3 for the Uno. For the Rotary library (by Ben Buxton) β a lighter alternative that works on any pin via polling:
arduino-cli lib install "Rotary"
Code with complete explanation
This sketch tracks encoder rotation position with acceleration and detects the built-in button press.
#include <Encoder.h>
#define ENC_CLK 2
#define ENC_DT 3
#define ENC_SW 4
Encoder enc(ENC_CLK, ENC_DT);
long lastPosition = -999;
long position = 0;
void setup()
{
Serial.begin(9600);
pinMode(ENC_SW, INPUT_PULLUP);
Serial.println("Rotary Encoder Test");
}
void loop()
{
// Read button
if (digitalRead(ENC_SW) == LOW)
{
Serial.println("Button pressed β resetting position");
enc.write(0);
// Simple debounce
delay(200);
}
// Read position
long newPosition = enc.read();
position = newPosition;
if (position != lastPosition)
{
long delta = position - lastPosition;
int direction = (delta > 0) ? 1 : -1;
Serial.print("Position: ");
Serial.print(position);
Serial.print(" Delta: ");
Serial.print(delta);
Serial.print(" Direction: ");
Serial.println(direction == 1 ? "CW" : "CCW");
lastPosition = position;
}
}
Interrupt-based tracking with acceleration
// Using the Rotary library for polling (no interrupts)
#include <Rotary.h>
Rotary r = Rotary(2, 3);
int counter = 0;
void setup()
{
Serial.begin(9600);
}
void loop()
{
unsigned char result = r.process();
if (result == DIR_CW)
{
counter++;
Serial.print("CW Counter: ");
Serial.println(counter);
}
else if (result == DIR_CCW)
{
counter--;
Serial.print("CCW Counter: ");
Serial.println(counter);
}
}
Manual decoding (without library)
#define CLK 2
#define DT 3
int lastCLK = HIGH;
int counter = 0;
void setup()
{
Serial.begin(9600);
pinMode(CLK, INPUT_PULLUP);
pinMode(DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK), onEncoderChange, CHANGE);
}
void loop()
{
// counter is updated by interrupt
Serial.println(counter);
delay(100);
}
void onEncoderChange()
{
int clk = digitalRead(CLK);
int dt = digitalRead(DT);
if (clk != lastCLK)
{
if (dt != clk)
{
counter++;
}
else
{
counter--;
}
lastCLK = clk;
}
}
Code breakdown
Encoder enc(pinA, pinB)β creates an encoder object using interrupts on the specified pins.enc.read()β returns the current position count. Positive = CW rotation, negative = CCW.enc.write(position)β resets the position counter to an arbitrary value.- The Rotary library polls the encoder state in
loop()without interrupts β suitable for applications where the encoder is read only occasionally. - Manual decoding uses
attachInterrupt()on the CLK pin and examines the DT pin to determine direction. - The push button (
SW) is read as a standard digital input withINPUT_PULLUP(LOW = pressed).
Adding acceleration (fast rotation = larger steps)
unsigned long lastTime = 0;
void loop()
{
unsigned long now = millis();
unsigned long dt = now - lastTime;
long newPos = enc.read();
long delta = newPos - position;
if (delta != 0)
{
// Acceleration: if rotation is fast, multiply the step
int multiplier = (dt < 50) ? 5 : (dt < 150) ? 2 : 1;
position += delta * multiplier;
enc.write(position);
Serial.print("Position (accel): ");
Serial.println(position);
lastTime = now;
lastPosition = newPos;
}
}
Steps to perform this interfacing
- Connect the KY-040 rotary encoder to the Arduino as shown in the schematic.
- Install the Encoder library by Paul Stoffregen via the Library Manager.
- Copy the code into the Arduino IDE.
- Select the correct board and port (
Tools > BoardandTools > Port). - Upload the sketch to the Arduino.
- Open the Serial Monitor (
Tools > Serial Monitor, set baud rate to 9600). - Rotate the knob clockwise β the position increases and βCWβ is printed.
- Rotate counterclockwise β the position decreases and βCCWβ is printed.
- Press the shaft (push button) β the position resets to 0.
Caution
- Interrupt pins only: The Encoder library requires pins 2 and 3 on Uno/Nano for hardware interrupts. On Mega, pins 2, 3, 18, 19, 20, 21 are interrupt-capable. On Arduino Due, MKR, or SAMD boards, any pin can be an interrupt. If you use non-interrupt pins with the Encoder library, it falls back to polling β which works but may miss steps at fast rotation speeds.
- Contact bounce: Mechanical rotary encoders produce contact bounce on the CLK and DT outputs, especially at low rotation speeds. The Encoder library handles this internally via a state machine, but very bouncy encoders may still produce false counts. Add external 10β100 nF capacitors from CLK and DT to GND to suppress bounce in hardware.
- Detent vs smooth rotation: The KY-040 has 20 detents (physical click stops) per revolution, but the encoder produces 40 pulses per revolution (both rising and falling edges on both channels). The count therefore increments by 2 per detent. If you want 1 count per detent, divide
enc.read()by 2. - Pull-up resistors: The KY-040 module includes onboard 10 kΞ© pull-up resistors on CLK and DT. If using a bare EC11 encoder without a module, add external 10 kΞ© pull-up resistors from CLK to VCC and DT to VCC.
- High-speed rotation: At fast rotation speeds (> 1000 RPM), the encoder may produce pulses faster than the interrupt service routine can process, causing missed steps. For high-speed applications, use a dedicated rotary decoder IC (e.g., LS7083/84) or reduce the interrupt handlerβs overhead.
- Switch debounce: The shaft push button (SW) is a mechanical switch and bounces like any other button. The code uses a simple
delay(200)for debounce β this blocksloop()for 200 ms. For production code, use the Bounce2 library or a non-blocking debounce withmillis(). - EMI sensitivity: Long wires between the encoder and Arduino can pick up electrical noise, generating false encoder counts. Use shielded twisted-pair cable for runs > 30 cm, or add 10 nF capacitors at the Arduino end of CLK and DT lines to GND.