Learn
Reading Rotation Direction from a KY-040 Rotary Encoder on a Raspberry Pi 4B
Wire a KY-040 rotary encoder to a Raspberry Pi 4B and read real CLK/DT quadrature signals in Python to detect clockwise vs. counter-clockwise rotation — plus the SW push-switch, wired and read independently.
Note: the Rotary Encoder (KY-040) is a VIP-tier component in the simulator — you’ll need a VIP plan to use it there.
Try this directly in the free Raspberry Pi 4B simulator — no hardware or signup required. New to the GPIO header? Start with the interactive pinout guide.
What you’ll need
- Raspberry Pi 4B
- Rotary Encoder (KY-040)
Step by step
- Drag Raspberry Pi 4B onto the Canvas.
- Drag the Rotary Encoder (KY-040) onto the Canvas — a real 5-pin module. It connects via drawn wires only (its 5-in-a-row header can't be plugged directly into a breadboard without shorting the pins together).
- Hover over its 5 pins to confirm the labels, top to bottom: CLK, DT, SW, VCC, GND.
- Wire VCC → a Pi 3.3V pin and GND → a Pi GND pin. The encoder has onboard pull-up resistors on CLK/DT/SW — no external resistors needed. Wire VCC to 3.3V, not 5V: a 5V-referenced pull-up would present an unsafe voltage to the Pi's GPIO inputs, and the simulator's own overvoltage advisory will warn you in the Console if you do this.
- Wire CLK → a Pi GPIO input pin and DT → a different Pi GPIO input pin.
- (Optional) Wire SW → a third Pi GPIO input pin, if you also want to read the encoder's built-in push-switch (pressing straight down on the knob).
- Go to the Code tab and write a script that polls CLK and DT in a loop, comparing CLK's current reading against its own previous reading to detect a falling edge, then checking DT's value at that exact moment to determine direction — the standard real-world approach for this component:
import RPi.GPIO as GPIOimport asyncio GPIO.setmode(GPIO.BOARD)CLK, DT, SW = 37, 38, 40 # adjust to match your wiring GPIO.setup(CLK, GPIO.IN, pull_up_down=GPIO.PUD_UP)GPIO.setup(DT, GPIO.IN, pull_up_down=GPIO.PUD_UP)GPIO.setup(SW, GPIO.IN, pull_up_down=GPIO.PUD_UP) last_clk = GPIO.input(CLK) while True: clk_state = GPIO.input(CLK) if clk_state != last_clk: # CLK just changed — only act on the falling edge (1 -> 0) if clk_state == 0: if GPIO.input(DT) == 1: print("Clockwise") else: print("Counter-clockwise") last_clk = clk_state if GPIO.input(SW) == 0: print("Button pressed") await asyncio.sleep(0.001)- Click Start.
- Click the CW (top) arrow on the encoder's own body to simulate rotating it clockwise, one detent per click — click-and-hold auto-repeats every ~300ms, matching a continuous spin.
- Click the CCW (bottom) arrow to simulate rotating counter-clockwise.
- Click directly on the encoder's own body (not on either arrow) to press the built-in SW switch.
Watch the Console: each CW click should print "Clockwise" exactly once, each CCW click "Counter-clockwise" exactly once, and holding the body down should print "Button pressed" repeatedly for as long as it's held.
What “working correctly” looks like
- Clicking the CW arrow prints exactly one "Clockwise" line per click (and repeats automatically while held); the CCW arrow does the same for "Counter-clockwise" — matching the real KY-040's quadrature behavior, where CLK genuinely changes state before DT on a clockwise turn and after DT on a counter-clockwise turn.
- Rotation direction is correctly detected even by a tight polling loop with a very short sleep — the simulator holds each CLK/DT transition long enough (tens of milliseconds) to be reliably observed, not a true microsecond-scale pulse.
- The SW push-switch is completely independent of rotation — pressing the encoder's body works the same whether or not you've rotated it, and prints continuously for as long as it's held, just like the Tact Switch component.
- Wiring VCC to a Pi 5V pin instead of 3.3V logs the same overvoltage advisory a bare, undivided sensor wire would — a real reminder to use 3.3V here, since this component's pull-ups are wired to whatever you connect to VCC.
If something’s wrong
- GPIO.input(CLK)/GPIO.input(DT) always read HIGH, rotation isn't detected at all → confirm VCC is actually wired to a Pi power pin — with nothing wired to VCC, the onboard pull-up has nothing to pull up to, and the reading falls back to whatever pull_up_down bias your script configured.
- Direction reads backwards (Clockwise prints when you clicked CCW, or vice versa) → double-check which pin is wired to CLK and which to DT — swapping the two wires reverses which one your script checks first.
- A fast series of clicks (or holding an arrow) seems to miss a detent → make sure your polling loop's own asyncio.sleep() is short (0.001-0.01s) — a script that sleeps too long between reads can miss a transition, the same real-world limitation a genuine rotary encoder has if you don't poll fast enough or use an interrupt.
- SW never reads LOW → confirm SW is wired to the exact pin your script configures with GPIO.setup(pin, GPIO.IN), and confirm you're clicking directly on the encoder's own body, not on either arrow (which trigger rotation instead).