KY-040 Rotary Encoder Module
The KY-040 is a rotary encoder module that provides digital signals corresponding to the rotational position and direction. It features continuous 360-degree rotation and includes a built-in push-button switch, making it suitable for various control applications.

On this page
KY-040 pinout
The KY-040 is a 5-pin rotary encoder module with integrated push button:
| Pin | Type | Description | Notes |
|---|---|---|---|
| CLK | Communication | Clock signal output | Pulses on rotation |
| DT | Communication | Data signal output | Phase-shifted pulses for direction detection |
| SW | Control | Switch/button signal | Active low when pressed |
| VCC | Power | Power supply | 3.3V or 5V |
| GND | Power | Ground connection |
Interface: Digital quadrature encoder (2-phase)
Rotation: Continuous 360° rotation (no limits)
Direction: CLK and DT phase relationship determines rotation direction
Button: Built-in push-button switch (active low)
Power: 3.3V or 5V operation
Debouncing: Software debouncing recommended for stable readings
Wiring the KY-040 to ESP32
To interface the KY-040 with an ESP32 for rotary input:
| KY-040 pin | ESP32 pin | Purpose |
|---|---|---|
| VCC | 3.3V | Power supply |
| GND | GND | Ground |
| CLK | GPIO18 | Clock input (any GPIO) |
| DT | GPIO19 | Data input (any GPIO) |
| SW | GPIO21 | Button input (any GPIO) · optional |
Interrupt Pins: Use interrupt-capable GPIO pins for best response
GPIO Selection: Any digital GPIO pins work, shown pins are examples
Voltage: Use 3.3V to avoid level shifting
Software: Implement debouncing and state machine for direction detection
Button Optional: SW pin not required if button functionality not needed
KY-040 code examples
KY-040 Arduino example
Copy#define CLK_PIN 18 // GPIO18, matches the wiring above
#define DT_PIN 19 // GPIO19
#define SW_PIN 21 // GPIO21
int counter = 0;
int currentStateCLK;
int lastStateCLK;
bool currentStateSW;
bool lastStateSW;
void setup() {
pinMode(CLK_PIN, INPUT);
pinMode(DT_PIN, INPUT);
pinMode(SW_PIN, INPUT_PULLUP);
lastStateCLK = digitalRead(CLK_PIN);
lastStateSW = digitalRead(SW_PIN);
Serial.begin(115200);
Serial.println("KY-040 Rotary Encoder Test");
}
void loop() {
currentStateCLK = digitalRead(CLK_PIN);
if (currentStateCLK != lastStateCLK) {
if (digitalRead(DT_PIN) != currentStateCLK) {
counter++;
} else {
counter--;
}
Serial.print("Position: ");
Serial.println(counter);
}
lastStateCLK = currentStateCLK;
currentStateSW = digitalRead(SW_PIN);
if (currentStateSW == LOW && lastStateSW == HIGH) {
Serial.println("Button pressed");
delay(50); // debounce
}
lastStateSW = currentStateSW;
}This Arduino code sets up the KY-040 rotary encoder using three pins: CLK (Clock), DT (Data), and SW (Switch). It initializes these pins as inputs and continuously monitors the rotation direction and button presses.
When the encoder knob is turned, the CLK and DT pins generate pulses. By comparing the sequence of these pulses, the code determines whether the encoder is rotating clockwise or counterclockwise:
- If the
DTsignal is opposite to theCLKsignal, the counter increments (clockwise rotation). - If the
DTsignal matches theCLKsignal, the counter decrements (counterclockwise rotation).
The position counter is updated accordingly and printed to the serial monitor.
The built-in push-button switch (SW) is also monitored. If pressed, a message is printed to the serial monitor.
A delay(50) is used to debounce the encoder readings, ensuring stable detection of rotation and button presses.
KY-040 ESP-IDF example
Copy#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define CLK_PIN GPIO_NUM_18
#define DT_PIN GPIO_NUM_19
#define SW_PIN GPIO_NUM_21
volatile int counter = 0;
volatile int lastStateCLK;
void IRAM_ATTR encoder_isr_handler(void* arg) {
int currentStateCLK = gpio_get_level(CLK_PIN);
if (currentStateCLK != lastStateCLK) {
if (gpio_get_level(DT_PIN) != currentStateCLK) {
counter++;
} else {
counter--;
}
printf("Position: %d\n", counter);
}
lastStateCLK = currentStateCLK;
}
void app_main(void) {
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_ANYEDGE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << CLK_PIN) | (1ULL << DT_PIN) | (1ULL << SW_PIN),
.pull_up_en = GPIO_PULLUP_ENABLE
};
gpio_config(&io_conf);
lastStateCLK = gpio_get_level(CLK_PIN);
gpio_install_isr_service(0);
gpio_isr_handler_add(CLK_PIN, encoder_isr_handler, NULL);
printf("KY-040 Rotary Encoder Test\n");
while (1) {
if (gpio_get_level(SW_PIN) == 0) {
printf("Button Pressed\n");
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}This ESP-IDF code configures the KY-040 rotary encoder on GPIO18 (CLK), GPIO19 (DT), and GPIO21 (SW). It uses an interrupt service routine (ISR) to detect changes in the encoder's rotation and update a counter. Additionally, it checks for button presses in the main loop and prints the status to the console.
KY-040 ESPHome example
Copysensor:
- platform: rotary_encoder
pin_a: GPIO18
pin_b: GPIO19
name: "KY-040 Rotary Encoder"
min_value: -1000
max_value: 1000
resolution: 1
binary_sensor:
- platform: gpio
pin:
number: GPIO21
mode: INPUT_PULLUP
name: "KY-040 Button"This ESPHome configuration sets up the KY-040 rotary encoder with GPIO18 and GPIO19 for rotation detection and GPIO21 for the push button. The encoder is configured with a minimum and maximum value range, and its resolution is set to 1 step per increment.
KY-040 PlatformIO example
Copy[env:esp32]
platform = espressif32
board = esp32dev
framework = arduino#include <Arduino.h>
#define CLK_PIN 18
#define DT_PIN 19
#define SW_PIN 21
volatile int counter = 0;
int lastStateCLK;
void IRAM_ATTR rotary_encoder() {
int currentStateCLK = digitalRead(CLK_PIN);
if (currentStateCLK != lastStateCLK) {
if (digitalRead(DT_PIN) != currentStateCLK) {
counter++;
} else {
counter--;
}
Serial.print("Position: ");
Serial.println(counter);
}
lastStateCLK = currentStateCLK;
}
void setup() {
pinMode(CLK_PIN, INPUT);
pinMode(DT_PIN, INPUT);
pinMode(SW_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CLK_PIN), rotary_encoder, CHANGE);
Serial.begin(115200);
Serial.println("KY-040 Rotary Encoder Test");
}
void loop() {
if (digitalRead(SW_PIN) == LOW) {
Serial.println("Button Pressed");
delay(100);
}
}This PlatformIO code configures GPIO18 (CLK), GPIO19 (DT), and GPIO21 (SW) for the KY-040 rotary encoder. It uses an interrupt to detect changes in rotation and updates a counter accordingly. The push button state is also monitored and printed to the serial monitor when pressed.
KY-040 MicroPython example
Copyimport machine
import time
CLK_PIN = machine.Pin(18, machine.Pin.IN, machine.Pin.PULL_UP)
DT_PIN = machine.Pin(19, machine.Pin.IN, machine.Pin.PULL_UP)
SW_PIN = machine.Pin(21, machine.Pin.IN, machine.Pin.PULL_UP)
counter = 0
last_state_clk = CLK_PIN.value()
while True:
current_state_clk = CLK_PIN.value()
if current_state_clk != last_state_clk:
if DT_PIN.value() != current_state_clk:
counter += 1
else:
counter -= 1
print("Position:", counter)
last_state_clk = current_state_clk
if SW_PIN.value() == 0:
print("Button Pressed")
time.sleep(0.1)
time.sleep(0.05)This MicroPython script configures GPIO18 (CLK), GPIO19 (DT), and GPIO21 (SW) for the KY-040 rotary encoder. It continuously monitors the encoder rotation and updates a counter while also detecting button presses.
KY-040 specifications
About the KY-040
The KY-040 is built around an EC11-style incremental rotary encoder: a rotating shaft drags a metal wiper across two offset contact tracks, producing the CLK and DT quadrature pulses that let software work out both how far and which way the knob turned. Twenty detents per revolution, matching the module’s own pulses-per-revolution spec, is typical for this contact-based design, and the board carries its own 10 kOhm pull-up resistors on all three signal lines (CLK, DT and SW) so it drops onto an ESP32 without any external resistors. Because the signal comes from a physical wiper scraping metal rather than an optical slot, contact bounce shows up on every click and every rotation step, not just the push-button - a debounced read (or a short settle delay after each edge) matters more here than “read two digital pins” makes it sound.
On the ESP32 side CLK and DT are best read with an interrupt on the CLK pin rather than pure polling, since a fast spin can produce edges quicker than a loop() running other work will notice; the direction then comes from comparing DT’s level at the moment CLK changes, exactly as shown in the wiring above. For a finished product built on the same idea rather than a bare module, the CrowPanel 2.1" rotary display wires its knob to ESPHome’s rotary_encoder platform the same way this module does, just packaged behind a screen instead of a breakout board.
KY-040 troubleshooting
Unexpected Behavior or No Response
›
Issue: The module does not respond or behaves erratically when the knob is rotated.
Solutions:
- Ensure all connections are secure and correctly wired according to the pinout diagram.
- Verify that the microcontroller's input pins are properly configured as inputs in the code.
- Check for proper power supply voltage (3.3V or 5V) to the module.
- Implement software debouncing to account for mechanical switch noise.
Incorrect Direction Detection
›
Issue: The detected rotation direction is opposite to the actual rotation.
Solutions:
- Swap the connections of the
CLKandDTpins to correct the direction detection. - Ensure that the code logic correctly interprets the sequence of pulses from the
CLKandDTpins.
Where to buy the KY-040

Resources
Similar sensors




