Documentation

KY-040 Rotary Encoder Module for Arduino with Demo Code | ShillehTek Product Manual
Documentation / KY-040 Rotary Encoder Module for Arduino with Demo Code | ShillehTek Product Manual

KY-040 Rotary Encoder Module for Arduino with Demo Code | ShillehTek Product Manual

EncoderESP32InputKnobKY-040ky-040-rotary-encoder-module-for-arduino-with-demo-codemanualPicoRaspberry PiRotary Encodershillehtek

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

Operating Voltage
3.3V - 5V
Output Type
Digital quadrature + switch
Detents
20 per revolution
Pulses per Revolution
20 (2 per detent)
Pushbutton
Active LOW switch
Pins
CLK, DT, SW, VCC, GND

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

KY-040 rotary encoder module pinout showing CLK (Output A), DT (Output B), SW (Switch), +VCC (+5V), and GND pins on the 5-pin header

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
Tip: Putting CLK on a hardware-interrupt pin (D2 or D3 on Uno) lets you catch every pulse even during slow loops or LCD updates. Without interrupts, fast rotation may skip counts.

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
Tip: Power the module from 3.3V (not 5V) on ESP32. The 10k pull-ups on the board reference VCC, so feeding 5V would push the digital high level above ESP32's 3.3V GPIO tolerance.

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)
Warning: Do not power the KY-040 from the Pi's 5V rail. The board's pull-ups would pull CLK / DT high to 5V, which can damage the Pi's 3.3V GPIO. Always use the 3.3V pin.

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
Tip: Use the machine.Pin IRQ trigger to read encoder steps reliably even when the main loop is busy with display updates. Falling-edge on CLK works well for this mechanical encoder.

Code Examples

Arduino

ky040_encoder.ino
// 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

ky040_esp32.ino
// 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)

ky040_rpi.py
#!/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)

ky040_pico.py
# 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)

Frequently Asked Questions

What's the difference between CLK and DT?
CLK and DT are the two quadrature outputs of the encoder. When you rotate the knob, they produce square waves that are 90 degrees out of phase. By comparing which signal leads the other, your code determines direction: if DT differs from CLK at a falling CLK edge, it's one direction; if they match, it's the other.
Why does my count sometimes jump or go the wrong direction?
Mechanical encoders bounce — the contacts produce noisy edges as they make and break. Use interrupts to catch every transition, add a tiny RC filter (10 nF cap to GND on CLK and DT) for hardware debouncing, or use a library like Encoder.h (Arduino) or RotaryEncoder (gpiozero) that handles debouncing internally.
How do I know how far the user turned the knob?
Each detent (tactile click) produces 2 pulses on CLK. So if your code counts every CLK edge, divide the count by 2 to get "clicks." Most makers find counting only on falling CLK edges gives one count per detent, which feels more natural for menu navigation.
Does the pushbutton need a debounce resistor?
The board has a built-in 10k pull-up on SW, so the switch is already pulled HIGH when not pressed and goes LOW when pressed. For software debouncing, add a short delay (10 - 50 ms) after detecting a press, or use a library like Button2 / Bounce2 (Arduino) or gpiozero.Button which debounces automatically.
Can I use this with a 3.3V microcontroller?
Yes — power VCC from your board's 3.3V rail and the onboard pull-ups will reference 3.3V. The encoder is purely mechanical (no active electronics other than pull-up resistors), so it works at any voltage from 3V to 5V.
What library should I use for the cleanest code?
Arduino: Paul Stoffregen's Encoder.h library handles interrupts and debouncing for you. Raspberry Pi: gpiozero's RotaryEncoder class is the simplest. ESP32: ESP32Encoder library is well-maintained. Pico MicroPython: use machine.Pin IRQ directly as shown in the example above, or the `rotary` PyPI package for higher-level handling.