A rotary encoder is the knob on menus, volume controls and 3D printers. It gives two square waves, CLK and DT, a quarter-cycle apart; their order tells you the direction. The Pico 2 W catches every edge with pin interrupts on GP16 and GP17.
Power the module from 3V3, and its push switch on GP18 resets the count. Same code runs on a Pico W — on the Pico 2 W use the RPI_PICO2_W MicroPython build.
Wiring
| Raspberry Pi Pico 2 W | KY-040 rotary encoder | Wire | Notes |
|---|---|---|---|
| GP16 | CLK | yellow | Physical pin 21. |
| GP17 | DT | green | Physical pin 22. |
| GP18 | SW | blue | Physical pin 24. Internal pull-up on — the module has none on SW. |
| 3V3 | + | red | 3V3 is pin 36. The module’s CLK/DT pull-ups go to +, so they stay at 3.3 V. |
| GND | GND | black |
3.3–5 V part, 3.3 V board. Power the module from your board’s logic voltage (3.3 V on ESP32/Pico) — its pull-ups go to the + pin, so CLK and DT will never exceed that voltage.
Board quirks
- Every edge on CLK or DT runs a small interrupt handler that looks up the change in a 16-entry quadrature table. Bounce produces +1/−1 pairs that cancel out, so no delays or capacitors are needed.
- The KY-040 rests with both lines HIGH between clicks; the count only changes when a full step lands back there, so one click = one count.
- Everything here uses pull-ups (module resistors on CLK/DT, internal on SW), so the RP2350 pull-down erratum E9 does not apply.
- If clockwise counts down, swap the CLK and DT wires (or the GP numbers in code).
Code
# KY-040 rotary encoder on a Raspberry Pi Pico 2 W (MicroPython) with interrupt-driven decoding.
# Wiring: CLK -> GP16 (pin 21), DT -> GP17 (pin 22), SW -> GP18 (pin 24), + -> 3V3 (pin 36), GND -> GND.
# CLK and DT have pull-ups on the module (to +); SW has none, so the internal pull-up is used.
from machine import Pin
import time
clk = Pin(16, Pin.IN)
dt = Pin(17, Pin.IN)
sw = Pin(18, Pin.IN, Pin.PULL_UP)
# Quadrature state table: index = (previous AB << 2) | current AB -> -1, 0 or +1
_TABLE = (0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0)
state = (clk.value() << 1) | dt.value()
sub = 0
position = 0
def on_edge(pin):
global state, sub, position
new = (clk.value() << 1) | dt.value()
sub += _TABLE[(state << 2) | new]
state = new
if new == 0b11: # detent position on the KY-040: both lines HIGH
if sub >= 2:
position += 1
elif sub <= -2:
position -= 1
sub = 0
clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
print("Rotary encoder: turn the knob, press to reset")
shown = None
pressed = False
while True:
if position != shown:
shown = position
print("Position:", shown)
if sw.value() == 0 and not pressed:
pressed = True
position = 0
print("Button - reset to 0")
elif sw.value() == 1:
pressed = False
time.sleep_ms(10)# KY-040 rotary encoder on a Raspberry Pi Pico 2 W (MicroPython) with interrupt-driven decoding.
# Wiring: CLK -> GP16 (pin 21), DT -> GP17 (pin 22), SW -> GP18 (pin 24), + -> 3V3 (pin 36), GND -> GND.
# CLK and DT have pull-ups on the module (to +); SW has none, so the internal pull-up is used.
from machine import Pin
import time
clk = Pin(16, Pin.IN)
dt = Pin(17, Pin.IN)
sw = Pin(18, Pin.IN, Pin.PULL_UP)
# Quadrature state table: index = (previous AB << 2) | current AB -> -1, 0 or +1
_TABLE = (0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0)
state = (clk.value() << 1) | dt.value()
sub = 0
position = 0
def on_edge(pin):
global state, sub, position
new = (clk.value() << 1) | dt.value()
sub += _TABLE[(state << 2) | new]
state = new
if new == 0b11: # detent position on the KY-040: both lines HIGH
if sub >= 2:
position += 1
elif sub <= -2:
position -= 1
sub = 0
clk.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
dt.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=on_edge)
print("Rotary encoder: turn the knob, press to reset")
shown = None
pressed = False
while True:
if position != shown:
shown = position
print("Position:", shown)
if sw.value() == 0 and not pressed:
pressed = True
position = 0
print("Button - reset to 0")
elif sw.value() == 1:
pressed = False
time.sleep_ms(10)Expected serial output
Rotary encoder: turn the knob, press to reset
Position: 1
Position: 2
Position: 3
Position: 2
Button - reset to 0
Position: 0Rotary encoder: turn the knob, press to reset Position: 1 Position: 2 Position: 3 Position: 2 Button - reset to 0 Position: 0
Common errors
Counts 2 or 4 per click
Your encoder has a different detent position. Change the new == 0b11 check to 0b00, or divide the count.
Count jumps around when not touched
A loose + wire: without it the module pull-ups float. Check 3V3 reaches the + pin.