Difficulty: Intermediate | Time: 45 Minutes | Board Target: Raspberry Pi 5 (4GB) running Raspberry Pi OS (Bookworm/2026)

If you are wiring a raspberry pi rotary encoder for a volume knob, menu dial, or jog wheel, skip the cheap KY-040 modules and use a bare Bourns PEC11R-4015F-N0024 with a 0.1µF hardware debounce capacitor. Read it using Python's gpiozero library backed by the pigpio daemon. This combination eliminates the contact bounce and Linux OS jitter that cause skipped steps and backward counting in 90% of online tutorials.

The Decision Path: Which Raspberry Pi Rotary Encoder to Buy?

Not all encoders are created equal. Mechanical encoders suffer from contact bounce, while optical encoders eliminate it but cost more and require 5V logic level shifting. Use this decision matrix to select the right part for your workbench.

Scenario Module / Part Pros Cons
Prototyping on a strict budget KY-040 Breakout Module ~$1.50, includes PCB and pull-ups Terrible contact bounce; switch pin often lacks pull-up; 5V VCC risks Pi GPIO
Precision UI, Audio Knob, or Menu Dial Bourns PEC11R (Bare) 24 PPR, crisp mechanical detents, highly reliable Requires manual RC debounce circuit on a breadboard
High RPM motor feedback or industrial use CUI Devices AMT103 (Optical) Zero bounce, 2048 PPR, capacitive sensing ~$15+, outputs 5V logic (requires level shifter for Pi 3.3V)
The Verdict: If you are building a user interface, audio controller, or smart home dial, the Default Pick is the Bourns PEC11R-4015F-N0024 (24 Pulses Per Revolution, 15mm shaft). It provides the tactile feel of a commercial audio mixer and costs under $3.00 in single quantities.

Parts List and Pi 5 Pin Mapping

The Raspberry Pi 5 features updated GPIO power delivery and pin multiplexing, but the BCM (Broadcom) numbering for standard GPIO remains compatible with the Pi 4. Below is the exact bill of materials and pinout for a robust, jitter-free build.

Bill of Materials

  • Microcontroller: Raspberry Pi 5 (4GB or 8GB)
  • Encoder: Bourns PEC11R-4015F-N0024 (or equivalent 24 PPR mechanical quadrature encoder)
  • Resistors: 3x 10kΩ (for CLK, DT, and SW pull-ups)
  • Capacitors: 2x 0.1µF (104) ceramic capacitors (for hardware debouncing)
  • Wiring: 22 AWG solid core jumper wires

Pin Mapping Table (BCM Numbering)

Encoder Pin Pi 5 GPIO (BCM) Physical Pin Function & Notes
CLK (A) GPIO 17 Pin 11 Quadrature Clock (Requires 10k pull-up + 0.1µF cap to GND)
DT (B) GPIO 27 Pin 13 Quadrature Data (Requires 10k pull-up + 0.1µF cap to GND)
SW (Switch) GPIO 22 Pin 15 Pushbutton (Requires 10k pull-up, software debounce OK)
VCC 3.3V Pin 1 Power (Do NOT use 5V on bare mechanical encoders)
GND GND Pin 9 Common Ground (Crucial for RC filter reference)

Wiring and the Hardware Debounce Math

The most common reason a raspberry pi rotary encoder skips steps or counts backward is Linux OS jitter. The Raspberry Pi runs a general-purpose Linux kernel, not a Real-Time Operating System (RTOS). If the Pi is handling a background task (like logging or network polling), a 5ms software debounce window might be delayed to 15ms. By the time the Python script reads the GPIO pin, the mechanical contacts have already bounced open and closed, resulting in a missed or reversed quadrature edge.

To fix this, we push the debounce to the hardware layer using an RC (Resistor-Capacitor) low-pass filter.

The RC Filter Math

By placing a 10kΩ resistor in series with the signal and a 0.1µF capacitor from the GPIO pin to GND, we create a low-pass filter. The cutoff frequency ($f_c$) is calculated as:

$f_c = \frac{1}{2 \pi R C} = \frac{1}{2 \pi (10000)(0.0000001)} \approx 159 \text{ Hz}$

The time constant ($\tau = RC$) is 1 millisecond. This perfectly absorbs the 50-microsecond contact bounce of the Bourns switch without delaying the 10ms+ pulse width generated by a human turning the knob. The gpiozero RotaryEncoder documentation highly recommends clean signals when using hardware-timed sampling.

Step-by-Step Wiring Procedure

  1. De-energize the Pi: Disconnect power from the Raspberry Pi 5 before wiring GPIO pins to prevent accidental short circuits.
  2. Wire Power and Ground: Connect the encoder's common pin (C) to Pi Pin 1 (3.3V). Connect the encoder's ground/ground shield to Pi Pin 9 (GND).
  3. Build the CLK Filter: Connect a 10kΩ resistor between the encoder's CLK pin and Pi GPIO 17. Connect a 0.1µF capacitor between Pi GPIO 17 and GND.
  4. Build the DT Filter: Connect a 10kΩ resistor between the encoder's DT pin and Pi GPIO 27. Connect a 0.1µF capacitor between Pi GPIO 27 and GND.
  5. Wire the Pushbutton: Connect a 10kΩ resistor between the encoder's SW pin and Pi GPIO 22. (No capacitor needed here; software debounce is fine for slow human presses).
  6. Verify Connections: Use a multimeter in continuity mode to ensure no solder bridges or loose jumper wires are shorting 3.3V to GND.

Bulletproof Python Code (gpiozero + pigpio)

To read the encoder without jitter, we use the gpiozero library but force it to use the pigpio pin factory. pigpio uses the Pi's DMA (Direct Memory Access) and hardware PWM timers to sample the GPIO pins at the hardware level, completely bypassing Linux OS scheduling delays. You can read more about Pi hardware capabilities in the official Raspberry Pi hardware documentation.

Prerequisite: Install the pigpio daemon via terminal: sudo apt install pigpio python3-pigpio, then enable it: sudo systemctl enable pigpiod && sudo systemctl start pigpiod.

import sys
import os
from signal import pause
from gpiozero import RotaryEncoder, Button
from gpiozero.pins.pigpio import PiGPIOFactory

# --- PIN DEFINITIONS (BCM Numbering) ---
PIN_CLK = 17
PIN_DT = 27
PIN_SW = 22

def on_encoder_rotate():
    # encoder.steps tracks the absolute position based on quadrature decoding
    print(f'Encoder Value: {encoder.steps}')

def on_button_press():
    print('Button Pressed! Resetting counter to 0.')
    # Reset the step counter without destroying the object
    encoder.steps = 0

def main():
    global encoder
    
    try:
        # Force pigpio factory for hardware-timed DMA sampling
        # This eliminates Linux OS jitter and missed quadrature edges
        factory = PiGPIOFactory()
    except Exception as e:
        print('FATAL: Could not connect to pigpiod.')
        print('Exact Error: ConnectionError: [Errno 111] Connection refused')
        print('Fix: Run \'sudo systemctl start pigpiod\' in the terminal.')
        sys.exit(1)

    try:
        # Initialize RotaryEncoder. max_steps=0 allows infinite rotation
        # wrap=False prevents the value from resetting at a maximum limit
        encoder = RotaryEncoder(PIN_CLK, PIN_DT, max_steps=0, pin_factory=factory)
        
        # Initialize Pushbutton with internal pull-up as a fallback, 
        # though external 10k is physically wired.
        button = Button(PIN_SW, pull_up=True, bounce_time=0.05, pin_factory=factory)
        
        # Bind callbacks
        encoder.when_rotated = on_encoder_rotate
        button.when_pressed = on_button_press
        
        print('Raspberry Pi Rotary Encoder initialized. Turn the knob or press the switch.')
        print('Press Ctrl+C to exit.')
        
        # Keep the script alive to listen for hardware interrupts
        pause()
        
    except KeyboardInterrupt:
        print('\nScript terminated by user.')
        sys.exit(0)
    except Exception as e:
        print(f'Unexpected runtime error: {e}')
        sys.exit(1)

if __name__ == '__main__':
    main()

Troubleshooting: Connection Refused and Skipped Steps

When working with hardware interrupts on Linux, things will occasionally fail. Here is the exact decision path for the most common errors.

Exact Error String: ConnectionError: [Errno 111] Connection refused

This error occurs when the Python script attempts to instantiate the PiGPIOFactory but the background daemon isn't listening on port 8888.

  • Cause 1 (Most Likely): The pigpiod service is not running.
  • Fix: Open a terminal and run sudo systemctl start pigpiod. To make it permanent, run sudo systemctl enable pigpiod.
  • Cause 2: You are running the script in a virtual environment that lacks the pigpio Python bindings.
  • Fix: Activate your venv and run pip install pigpio.

The First 3 Things to Check When It Fails (Skipped Steps / Jitter)

If the code runs but the encoder counts backward, skips numbers, or jumps erratically, check these three physical layer issues:

  1. Are the RC capacitors physically present? Software debounce cannot save you from Linux scheduling delays. Verify the 0.1µF capacitors are seated between the GPIO pins and GND, not just floating in the breadboard.
  2. Is the Common Ground shared? If you are powering the encoder from an external 3.3V breadboard supply, the GND of that supply must be tied to the Raspberry Pi's GND. Without a shared reference, the Pi will read floating noise as quadrature edges.
  3. Are the pull-up resistors correctly valued? If you are using a KY-040 module, ensure the module's VCC is connected to the Pi's 3.3V pin (Pin 1), not the 5V pin. Feeding 5V into GPIO 17 will slowly degrade the Pi's silicon and cause erratic logic high readings before eventually killing the pin.

How to Extend or Simplify the Build

Depending on your project timeline and production goals, you may want to alter the complexity of this circuit.

Simplify: Use an I2C Encoder Module

If you want to eliminate breadboard wiring, RC filters, and DMA daemons entirely, swap the bare encoder for the SparkFun Qwiic Twist (DEV-15083). This module features an RGB-illuminated rotary encoder with an ATtiny85 microcontroller onboard. It handles all hardware debouncing and quadrature decoding internally, exposing the turn count and button state over I2C. You simply plug it into the Pi's I2C bus (Pins 3 and 5) and read a single register via Python's smbus2 library. It costs around $12, trading component cost for massive time savings.

Extend: Add a 16x2 I2C OLED Display

To turn this into a standalone smart-home thermostat dial or audio volume controller, wire an SSD1306 128x64 I2C OLED to the Pi's secondary I2C pins (GPIO 2/3 or GPIO 44/45 on the Pi 5). Update the on_encoder_rotate callback to push the encoder.steps value to the OLED via the luma.oled Python library. Because the OLED updates via I2C, it won't block the pigpio hardware interrupts, ensuring your dial remains perfectly responsive even while redrawing the screen.