Rotary Encoders for Makers: Incremental vs Absolute, Debouncing, and Reading with Arduino/ESP32
A rotary encoder shows up everywhere once you start looking for it — volume knobs, 3D printer LCD menu selectors, CNC jog wheels, camera focus rings, and any project that needs a knob you can turn indefinitely in either direction (unlike a potentiometer, which has hard end stops). They're cheap, mechanically simple, and one of the more commonly mis-wired parts in a beginner's project because the signal they produce (quadrature pulses, not a clean digital level) trips people up. This guide covers the two encoder types you'll actually encounter, how the signal works, and reliable ways to read one without missing or double-counting steps.
Incremental vs Absolute Encoders
TypeWhat It ReportsPosition on Power-UpTypical UseTypical Cost IncrementalRelative movement (steps clockwise/counterclockwise) since power-onUnknown — must be homed or zeroedVolume knobs, menu navigation, jog wheels, DIY encoders$1–$10 AbsoluteExact angular position at all timesKnown immediately, no homing neededRobotics joints, CNC/machine axis feedback, anything needing position after a power cycle$15–$100+The cheap EC11-style knob encoders on nearly every hobby electronics site are incremental — they only tell you "one step happened, in this direction," not an absolute angle. If a project genuinely needs to know exact position after a power loss (a CNC axis, a robot joint), an incremental encoder alone isn't enough without a homing routine or an added absolute reference (a limit switch, a magnetic absolute encoder like an AS5600, or a stepper motor with known step count from a homed position). Most maker projects — menu selection, volume control, digital jog wheels — only need incremental, which is why it's by far the more common part.
How Incremental (Quadrature) Encoders Work
A mechanical incremental encoder has two output pins, commonly labeled A and B (or CLK and DT), plus a common/ground. As the shaft turns, A and B each toggle between high and low, but slightly out of phase with each other — this phase relationship, called quadrature, is what tells you direction, not just that movement happened. Turning clockwise, A transitions before B; turning counterclockwise, B transitions before A. Reading both channels and comparing their relative timing (or their combined 2-bit state: 00, 01, 11, 10, cycling in one order for CW and the reverse order for CCW) is how software determines direction, not just step count.
Cheap mechanical encoders (EC11 and similar) also have physical detents — the little clicks you feel turning the knob — and most produce one full quadrature cycle (all four states) per detent, so in code you typically only care about complete cycles, not raw pin transitions, to avoid registering partial or bouncy transitions as steps.
Wiring
Encoder PinConnects ToNotes A / CLKDigital input pin (interrupt-capable preferred)Use internal pull-up if the encoder is open-collector/switch type (most cheap ones are) B / DTDigital input pin (interrupt-capable preferred)Same as above C / GND / SW commonGroundCommon return for both channels SW (if present, push-button)Digital input pinMost knob encoders have an integrated pushbutton on a separate pin — needs its own debouncing, unrelated to the rotary signalOn ESP32 and most Arduino boards, enable the internal pull-up resistors on both A and B pins (INPUT_PULLUP) rather than adding external resistors, since cheap mechanical encoders are simple switches internally and pull-ups are all they need. Prefer pins that support hardware interrupts for both A and B if your board has enough of them free — polling in the main loop works for slow, deliberate turns but will miss steps on a fast spin if the loop is doing anything else at the same time.
Reading the Signal: Interrupt-Driven Quadrature Decoding
The reliable approach is an interrupt on both A and B that reads the current 2-bit state on every transition and compares it to the previous state to determine direction, rather than trying to time single-pin transitions:
volatile int encoderPos = 0; volatile uint8_t lastState = 0; const int PIN_A = 32, PIN_B = 33; // Valid quadrature transition table: index = (lastState << 2) | newState const int8_t transitionTable[16] = { 0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 }; void IRAM_ATTR handleEncoder() { uint8_t newState = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B); uint8_t index = (lastState << 2) | newState; encoderPos += transitionTable[index]; lastState = newState; } void setup() { pinMode(PIN_A, INPUT_PULLUP); pinMode(PIN_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIN_A), handleEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(PIN_B), handleEncoder, CHANGE); }This full quadrature-table approach counts every valid transition (four per detent), which gives smooth, high-resolution movement; for menu navigation where you only want one "click" per detent, divide the accumulated count by 4 before acting on it, or use a library that already handles this — the widely used Encoder library (PJRC) implements exactly this quadrature decoding and is the easier starting point for most projects rather than hand-rolling the interrupt handler above.
Mechanical Contact Bounce vs Signal Bounce
Cheap mechanical rotary encoders are, electrically, just switches, and switches bounce — the contacts can chatter for a few milliseconds during a transition, which a fast interrupt handler can misread as multiple rapid transitions instead of one clean edge. The quadrature transition-table approach above is naturally more bounce-resistant than a naive single-pin edge counter because invalid state transitions (bounce artifacts) simply don't appear in the lookup table and are ignored, but for a particularly noisy encoder, adding a small hardware debounce (a 100nF capacitor from each signal pin to ground) or a short software debounce window (ignore transitions faster than roughly 1–2ms apart) cleans up the rest. If a knob is reliably skipping or double-counting steps, mechanical bounce is almost always the cause, not a software logic error.
Encoders on I2C/SPI: Skipping the Interrupt Pin Budget
If a project is short on interrupt-capable GPIO pins (common on boards already busy with a display, SD card, and sensors), an I2C-based encoder breakout (several inexpensive ones exist built around a small microcontroller that handles the quadrature decoding itself and exposes position over I2C) trades a little cost and complexity for freeing up two direct GPIO pins and offloading debounce entirely to the breakout's firmware. Worth considering on I2C/SPI-display-heavy builds like a CNC jog pendant or a synth control panel where GPIO is the scarce resource.
Practical Notes
- The integrated pushbutton on knob-style encoders is a completely separate switch from the rotary signal and needs its own debounce (a simple 20–50ms software debounce on that pin is sufficient — it doesn't need quadrature logic).
- Detent count varies by part (commonly 15, 20, or 24 detents per revolution) — check your specific encoder's datasheet if a project needs a known number of steps per full rotation.
- For rapid rotation applications (a CNC jog wheel spun quickly), interrupt-driven reading is not optional — polling will miss steps under fast movement even with fast loop code.
- If you need absolute position and can't home on every power-up, look at magnetic absolute encoders (AS5600 and similar, read over I2C) rather than trying to make an incremental encoder do a job it isn't built for.
Related Guides
- Potentiometers and Trimmers for Makers: Types, Tapers, Wiring, and Pots vs Encoders
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers
- ESP32: Setting Up for Arduino IDE
- Arduino vs ESP32: Which Should You Use? A Practical Comparison
- Getting Started with ESP32: GPIO, WiFi, and Your First Project