Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.

Rotary encoder with Raspberry Pi Pico 2 W — MicroPython interrupt decoding

Read a KY-040 rotary encoder on a Pico 2 W (CLK GP16, DT GP17, SW GP18) in MicroPython with pin interrupts and a quadrature table that ignores contact bounce.

MicroPythonVerified — micropython (mpy-cross v1.29.0), 3 Nov 2026Compile-only

Community draft — report a mistake

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 WOnly the pins used are shown · 3.3 V logicKY-040 rotary encoderGND3V3GP18GP17GP16CLKDTSW+GND
Every connection
Raspberry Pi Pico 2 WKY-040 rotary encoderWireNotes
GP16CLKyellowPhysical pin 21.
GP17DTgreenPhysical pin 22.
GP18SWbluePhysical pin 24. Internal pull-up on — the module has none on SW.
3V3+red3V3 is pin 36. The module’s CLK/DT pull-ups go to +, so they stay at 3.3 V.
GNDGNDblack

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

main.pyMicroPython
# 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

REPL / Thonny shellOutput
Rotary 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.

↑↓ to move · Enter to open