The Raspberry Pi Encoder Problem (And How to Solve It)
If you are trying to read a standard quadrature raspberry pi encoder using raw GPIO pins and software interrupts, you will drop steps. This is not a coding error; it is a fundamental limitation of the Linux kernel. The Raspberry Pi runs a non-real-time operating system. When the CPU is busy handling background tasks, network stacks, or UI rendering, it cannot guarantee microsecond-level interrupt latency. A rotary encoder spinning at just 60 RPM generates hundreds of state changes per second. If the OS delays reading the GPIO pin by even a few milliseconds, the state machine loses track of the quadrature sequence, resulting in missed steps or erratic direction reporting.
The direct answer is to offload the real-time decoding to dedicated hardware. Below is the decision matrix for reading rotary encoders on a Pi.
| Reading Method | Max Reliable RPM | CPU Overhead | Step Accuracy | Verdict |
|---|---|---|---|---|
| GPIO Polling (Python loop) | < 10 RPM | 100% of 1 core | Poor | Reject |
| GPIO Interrupts (RPi.GPIO / gpiozero) | ~60 RPM | Medium (spikes) | Fair (drops under load) | Reject for precision |
| Hardware I2C Decoder (Seesaw/ATtiny) | > 3000 RPM | Negligible | Perfect | Default Pick |
For this build, we are using the Adafruit I2C QT Rotary Encoder (PID: 4991). This breakout board contains an onboard ATtiny microcontroller that handles the quadrature decoding, hardware debouncing, and step counting locally. The Raspberry Pi simply queries an I2C register over a standard bus to get the accumulated position, entirely eliminating OS latency issues.
Parts List & Hardware Specifications
This guide targets the Raspberry Pi 5 (4GB or 8GB) and Raspberry Pi 4 Model B running Raspberry Pi OS (Bookworm 64-bit). The code relies on the modern `board` and `busio` implementations in CircuitPython for SBCs.
| Component | Exact Variant / Model | Approx. Price (2026) | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB) | $60.00 | Pi 4 Model B works identically. |
| Encoder Module | Adafruit I2C QT Rotary Encoder (PID 4991) | $11.95 | Includes NeoPixel and ATtiny decoder. |
| Jumper Wires | 28 AWG Silicone Female-to-Female | $6.00 (pack) | Silicone prevents melting near Pi SoC. |
| Power Supply | 27W USB-C PD Power Supply (Official) | $12.00 | Required for Pi 5 peripheral headroom. |
Pin Mapping & Wiring Procedure
The I2C bus on the Raspberry Pi uses specific hardware pins. Do not use software I2C (bit-banging) for this; hardware I2C is required for stable clock stretching with the ATtiny chip on the encoder module.
| Encoder Pin (PID 4991) | Raspberry Pi GPIO Header | Physical Pin # | Wire Color (Suggested) |
|---|---|---|---|
| VIN | 3V3 Power | Pin 1 | Red |
| GND | Ground | Pin 6 | Black |
| SCL | GPIO 3 (SCL1) | Pin 5 | Yellow |
| SDA | GPIO 2 (SDA1) | Pin 3 | Blue |
Wiring Steps
- De-energize the Pi: Disconnect the USB-C power cable. Never hot-plug I2C devices on the Pi's primary header, as the I2C pull-up resistors are tied directly to the 3.3V rail.
- Connect Power: Route the Red wire from the encoder's VIN pin to Physical Pin 1 (3.3V) on the Pi. Route the Black wire from GND to Physical Pin 6.
- Connect Data: Route Yellow (SCL) to Physical Pin 5, and Blue (SDA) to Physical Pin 3.
- Verify I2C Pull-ups: The PID 4991 has built-in 10kΩ pull-up resistors on the SDA and SCL lines. If you are only using one encoder, no additional resistors are needed. If you daisy-chain more than three, you may need to cut the pull-up jumper on the back of the extra boards to prevent the bus capacitance from pulling the rise time out of spec.
- Boot and Enable I2C: Power on the Pi. Open a terminal and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi. - Verify Address: Run
sudo i2cdetect -y 1. You should see36in the grid. This confirms the ATtiny is alive and responding at its default I2C address (0x36).
Complete Python Implementation (Target: Pi 5 / Bookworm)
To communicate with the onboard ATtiny, we use Adafruit's Seesaw library, which abstracts the I2C register mapping. Install the required library via pip in your virtual environment (standard practice for Bookworm OS, which enforces PEP 668):
sudo apt update
sudo apt install python3-venv python3-pip
python3 -m venv ~/env
source ~/env/bin/activate
pip3 install adafruit-circuitpython-seesaw
Save the following code as encoder_monitor.py. This script includes robust error handling for I2C bus drops and explicitly defines the target hardware.
import board
from adafruit_seesaw.seesaw import Seesaw
import time
import sys
# ---------------------------------------------------------
# TARGET HARDWARE CONFIGURATION
# Board: Raspberry Pi 4 Model B / Raspberry Pi 5
# OS: Raspberry Pi OS (Bookworm 64-bit)
# Component: Adafruit I2C QT Rotary Encoder (PID: 4991)
# ---------------------------------------------------------
ENCODER_I2C_ADDR = 0x36
BUTTON_PIN = 24 # Internal Seesaw pin mapping for the PID 4991 switch
def init_hardware():
"""Initialize I2C bus and Seesaw encoder with error handling."""
try:
# board.I2C() automatically maps to Pi's hardware SDA/SCL pins
i2c_bus = board.I2C()
seesaw = Seesaw(i2c_bus, addr=ENCODER_I2C_ADDR)
# Verify product ID to ensure we aren't talking to a different I2C device
if seesaw.get_version() >> 16 != 0xE357:
print("[WARNING] Connected device does not match expected Seesaw encoder signature.")
seesaw.encoder_position = 0 # Zero out the hardware counter
return seesaw
except ValueError as e:
print(f"[FATAL] I2C hardware initialization failed. Is I2C enabled in raspi-config?\nDetails: {e}")
sys.exit(1)
except RuntimeError as e:
print(f"[FATAL] Cannot find encoder at I2C address {hex(ENCODER_I2C_ADDR)}. Check wiring.\nDetails: {e}")
sys.exit(1)
def main():
seesaw = init_hardware()
last_position = 0
button_debounce = False
print("Monitoring Raspberry Pi encoder. Press Ctrl+C to exit.")
try:
while True:
# 1. Read Quadrature Position (Hardware accumulated)
current_position = seesaw.encoder_position
if current_position != last_position:
delta = current_position - last_position
print(f"Position: {current_position:4d} | Delta: {delta:+2d}")
last_position = current_position
# 2. Read Pushbutton State (Active Low)
# digital_read returns True if HIGH, False if LOW (pressed)
button_state = seesaw.digital_read(BUTTON_PIN)
if not button_state and not button_debounce:
print(">> BUTTON PRESSED <<")
button_debounce = True
elif button_state:
button_debounce = False
# 10ms sleep prevents hammering the I2C bus while maintaining snappy UI response
time.sleep(0.01)
except KeyboardInterrupt:
print("\n[INFO] Exiting gracefully.")
except OSError as e:
# Catches mid-run I2C disconnects or clock stretch timeouts
print(f"\n[FATAL] I2C Bus dropped during runtime: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Debugging: "OSError: [Errno 121] Remote I/O error"
When working with I2C on the Raspberry Pi, you will eventually encounter the dreaded OSError: [Errno 121] Remote I/O error. This exact error string means the Linux I2C master (the Pi) sent a clock pulse, but the slave device (the encoder) failed to acknowledge (ACK) the transaction, or the bus was pulled low unexpectedly.
The First Three Things to Check
- Verify the Ground Connection: 90% of Errno 121 issues on a workbench are caused by a floating ground. If the GND wire between the Pi and the encoder is loose, the I2C voltage levels will reference incorrectly, causing the ATtiny to ignore the SCL clock. Reseat the black wire on both ends.
- Check for I2C Address Collisions: Run
i2cdetect -y 1. If you see multiple devices responding, or if the address36is missing but you seeUU(meaning a kernel driver has claimed it), you have a conflict. Ensure no other HATs are using 0x36. - Inspect Wire Length and Capacitance: The I2C specification limits bus capacitance to 400pF. If you are using cheap, unshielded ribbon cables longer than 30cm (12 inches) between the Pi and the encoder, the signal edges will round off, causing the hardware I2C controller on the Pi's BCM2712/BCM2711 SoC to time out. Keep I2C runs under 20cm.
Ranked Causes for Intermittent Errno 121 Drops
| Rank | Cause | Fix / Measurement Threshold |
|---|---|---|
| 1 | Loose Dupont / Breadboard contacts | Crimp proper JST-XH connectors or solder directly. Breadboards introduce 10-50mΩ contact resistance. |
| 2 | I2C Clock Stretching Timeout | The Pi's hardware I2C controller has a known bug with clock stretching. Add dtparam=i2c_vc=on to /boot/firmware/config.txt to force the VideoCore I2C bus, which handles stretching better. |
| 3 | Power Supply Brownout | If the Pi's 3.3V rail dips below 3.1V under load, the ATtiny on the encoder will reset mid-transaction. Measure the VIN pin with a multimeter; it must read > 3.2V while the Pi is under load. |
Extending and Simplifying the Build
Depending on your project requirements, you may need to scale this setup up for a complex control surface, or dumb it down for a simple volume knob.
How to Extend (Multi-Encoder Control Surfaces)
The Adafruit I2C QT Rotary Encoder features address-selection jumpers on the back of the PCB. By bridging the A0 and A1 solder pads, you can shift the I2C address. This allows you to daisy-chain up to four encoders on the exact same SDA/SCL bus without needing an I2C multiplexer (like the TCA9548A).
- No jumpers: 0x36 (Default)
- Bridge A0: 0x37
- Bridge A1: 0x38
- Bridge A0 + A1: 0x39
To implement this in the Python code above, simply instantiate an array of Seesaw objects, passing the respective hex addresses, and poll them sequentially in your while True loop.
How to Simplify (Single-Turn Potentiometer Alternative)
If your application only requires a single-turn dial (like a thermostat or volume control) and does not require infinite rotation or step-tracking, drop the I2C encoder entirely. Instead, use a standard 10kΩ linear potentiometer wired to an external ADC (Analog-to-Digital Converter) like the ADS1115. The Raspberry Pi 5 and Pi 4 do not have native analog input pins. Wiring a potentiometer directly to a GPIO pin will not work and risks shorting the 3.3V rail to ground. Using an ADS1115 over I2C provides 16-bit resolution, which is vastly smoother for audio volume mapping than the 20-detent mechanical clicks of a rotary encoder.






