Overview
The KY-040 is an incremental rotary encoder module with a built-in pushbutton, commonly used for menu navigation, value adjustment, and user-input controls in Arduino, ESP32, Raspberry Pi, and Pico projects. It generates quadrature pulses on two output pins (CLK and DT) as you rotate the shaft, plus a momentary switch signal when you press the knob.
Unlike a potentiometer, the KY-040 has no end stops — you can spin it infinitely in either direction — and its output is digital, so it works directly with any microcontroller GPIO. It is the go-to control input for DIY synths, 3D printer menus, smart-home dimmers, retro game emulators, and any project that benefits from a turn-and-click interface.
At a Glance
Specifications
| Parameter | Value |
| Operating Voltage | 3.3V or 5V |
| Operating Current | < 10 mA |
| Encoder Type | Incremental, mechanical quadrature |
| Pulses per Revolution | 20 (one click = 2 pulses on CLK) |
| Detents per Revolution | 20 (tactile clicks) |
| Pushbutton | Momentary, normally-open, active LOW |
| Pull-up Resistors | 10 k onboard on CLK, DT, SW |
| Rotational Life | > 30,000 cycles (typical) |
| Switch Life | > 20,000 cycles (typical) |
| Operating Temperature | -10 to +70 C |
| Shaft Diameter | 6 mm (D-shaped) |
| PCB Dimensions | ~32 x 19 x 30 mm |
Pinout Diagram
Wiring Guide
Arduino Wiring
The KY-040 has onboard pull-up resistors, so no external resistors are needed. Connect CLK and DT to any two digital pins — use external interrupt pins (D2 or D3 on Uno) for the cleanest reads.
| KY-040 Pin | Arduino Pin |
|---|---|
| VCC (+) | 5V |
| GND | GND |
| CLK (Output A) | Digital Pin 2 |
| DT (Output B) | Digital Pin 3 |
| SW (Switch) | Digital Pin 4 |
ESP32 Wiring
The KY-040 works at 3.3V so it's natively compatible with ESP32 GPIO. All ESP32 GPIOs support interrupts, so you have flexibility in pin choice.
| KY-040 Pin | ESP32 Pin |
|---|---|
| VCC (+) | 3.3V |
| GND | GND |
| CLK | GPIO 18 |
| DT | GPIO 19 |
| SW | GPIO 21 |
Raspberry Pi Wiring
Raspberry Pi GPIO is 3.3V — power the encoder from 3.3V to keep signals within tolerance. Use a library like gpiozero or pigpio for event-driven reading.
| KY-040 Pin | Raspberry Pi Pin |
|---|---|
| VCC (+) | Pin 1 (3.3V) |
| GND | Pin 6 (GND) |
| CLK | Pin 11 (GPIO 17) |
| DT | Pin 13 (GPIO 27) |
| SW | Pin 15 (GPIO 22) |
Raspberry Pi Pico Wiring
Pico GPIO is 3.3V, matching the KY-040 when powered from the Pico's 3V3 (OUT) pin. All Pico GPIOs support interrupts via MicroPython callbacks.
| KY-040 Pin | Pico Pin |
|---|---|
| VCC (+) | 3V3 (OUT) |
| GND | GND |
| CLK | GP15 |
| DT | GP14 |
| SW | GP13 |
Code Examples
Arduino
// KY-040 Rotary Encoder - Arduino Example
// CLK = D2 (interrupt), DT = D3, SW = D4
const int CLK = 2;
const int DT = 3;
const int SW = 4;
volatile long counter = 0;
volatile int lastCLK = HIGH;
void readEncoder() {
int cur = digitalRead(CLK);
if (cur != lastCLK && cur == LOW) {
if (digitalRead(DT) != cur) counter++;
else counter--;
}
lastCLK = cur;
}
void setup() {
Serial.begin(9600);
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
pinMode(SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK), readEncoder, CHANGE);
}
void loop() {
static long last = 0;
if (counter != last) {
Serial.print("Count: ");
Serial.println(counter);
last = counter;
}
if (digitalRead(SW) == LOW) {
Serial.println("Button pressed!");
delay(200); // debounce
}
}
ESP32
// KY-040 Rotary Encoder - ESP32 Example
// CLK = GPIO 18, DT = GPIO 19, SW = GPIO 21
const int CLK = 18;
const int DT = 19;
const int SW = 21;
volatile long counter = 0;
volatile int lastCLK = HIGH;
void IRAM_ATTR readEncoder() {
int cur = digitalRead(CLK);
if (cur != lastCLK && cur == LOW) {
if (digitalRead(DT) != cur) counter++;
else counter--;
}
lastCLK = cur;
}
void setup() {
Serial.begin(115200);
pinMode(CLK, INPUT);
pinMode(DT, INPUT);
pinMode(SW, INPUT_PULLUP);
attachInterrupt(CLK, readEncoder, CHANGE);
}
void loop() {
static long last = 0;
if (counter != last) {
Serial.printf("Count: %ld\n", counter);
last = counter;
}
if (digitalRead(SW) == LOW) {
Serial.println("Button pressed!");
delay(200);
}
}
Raspberry Pi (Python)
#!/usr/bin/env python3
# KY-040 Rotary Encoder - Raspberry Pi Example
# CLK=GPIO17, DT=GPIO27, SW=GPIO22
from gpiozero import RotaryEncoder, Button
from signal import pause
rotor = RotaryEncoder(17, 27, max_steps=0)
button = Button(22, pull_up=True)
def turned():
print("Count: {}".format(rotor.steps))
def pressed():
print("Button pressed!")
rotor.when_rotated = turned
button.when_pressed = pressed
print("Turn the knob or press the button. Ctrl+C to quit.")
pause()
Raspberry Pi Pico (MicroPython)
# KY-040 Rotary Encoder - Pico MicroPython Example
# CLK=GP15, DT=GP14, SW=GP13
from machine import Pin
import time
clk = Pin(15, Pin.IN, Pin.PULL_UP)
dt = Pin(14, Pin.IN, Pin.PULL_UP)
sw = Pin(13, Pin.IN, Pin.PULL_UP)
counter = 0
last_clk = clk.value()
def on_rotate(pin):
global counter, last_clk
cur = clk.value()
if cur != last_clk and cur == 0:
if dt.value() != cur:
counter += 1
else:
counter -= 1
last_clk = cur
clk.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_rotate)
last_print = counter
while True:
if counter != last_print:
print("Count:", counter)
last_print = counter
if sw.value() == 0:
print("Button pressed!")
time.sleep_ms(200)
time.sleep_ms(10)