How Do You Interface a Rotary Encoder to a Microcontroller?
Last updated 8 July 2026 · 11 min read
Direct Answer
An incremental rotary encoder outputs two square waves, A and B, offset by 90 degrees (in quadrature) as the shaft turns — the phase relationship between them tells you direction, and each transition tells you the shaft has moved one increment. There are two standard ways to decode this on a microcontroller: a software quadrature state machine, where both channels are read on every edge interrupt and a small lookup table converts the (previous state, new state) pair into +1, -1, or invalid, or a hardware quadrature decoder, where a general-purpose timer (STM32's Encoder Interface Mode) or a dedicated peripheral (ESP32's PCNT) counts and decodes the pulses entirely in hardware, freeing the CPU from every edge and eliminating missed-count risk at high shaft speed. Contact-type (mechanical) encoders also bounce like a switch, but the fix is different from a simple RC filter or delay-based debounce: because valid quadrature transitions are a known, small set, an invalid (previous, new) state pair is simply rejected by the same lookup table used for decoding, rather than needing a separate debounce stage.
Detailed Explanation
Rotary encoders show up constantly in embedded designs (motor position/speed feedback, manual UI controls such as volume knobs and jog dials, and machine-axis position tracking), but nearly every mention of "encoder" on this site so far has been a passing reference in a motor-control, robotics, or real-time co-processor context. This page covers the interfacing problem itself: what the signals look like, how to decode them correctly, and where the real engineering trade-offs are.
Incremental vs Absolute Encoders
This page focuses on incremental encoders, by far the most common type in cost-sensitive embedded designs, which output relative motion (pulses per increment of rotation) rather than an absolute angular position. An incremental encoder has no memory of position across a power cycle; the host must track a running count and, if absolute position matters, home the system against a known reference at startup.
Absolute encoders (single-turn or multi-turn) output a unique digital code for every shaft position, typically over SPI, SSI, or a serial protocol like BiSS-C, and retain position through a power cycle. They cost substantially more than incremental encoders and are used where knowing absolute position immediately after power-up is a hard requirement (robotic joints, CNC axes without a homing cycle). That interfacing problem is different enough (digital position word, not pulse counting) to be outside this page's scope. A related but distinct contactless option is a magnetic rotary position sensor IC (AS5600/AS5048A-class parts), which measures a rotating magnet's field angle directly rather than counting or decoding pulses at all.
The Quadrature Signal
An incremental encoder's sensing element, whether optical (LED/photodiode through a slotted disc), magnetic (Hall-effect or magnetoresistive against a pole wheel), or mechanical (a wiper contact against a segmented track), produces two digital channels, A and B, offset by a quarter cycle (90 electrical degrees) from each other. Many encoders also provide a third channel, Z (or "index"), which pulses once per revolution and is used to establish an absolute reference point after homing.
The 90-degree phase offset is the entire trick: which channel leads the other tells you rotation direction, and both channels together define four distinct states per cycle:
| A | B | State |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 1 | 1 | 2 |
| 0 | 1 | 3 |
Rotating one way steps through these states in order (0→1→2→3→0…); rotating the other way steps through them in reverse. A decoder that tracks the previous state and compares it to the new state on every transition can therefore determine both that the shaft moved and which way.
Software Decoding: The Quadrature State Machine
The simplest correct approach configures both A and B as GPIO inputs with an interrupt on every edge (both rising and falling) on both pins. On each interrupt, the ISR reads both pin states, forms a 2-bit "new state" value, and looks up the transition against the previous 2-bit state in a small table:
/* Indexed by (old_state << 2) | new_state. +1 = forward, -1 = reverse, 0 = invalid/bounce */
static const int8_t qtable[16] = {
0, -1, +1, 0,
+1, 0, 0, -1,
-1, 0, 0, +1,
0, +1, -1, 0
};
void encoder_isr(void) {
static uint8_t old_state = 0;
uint8_t new_state = (gpio_read(PIN_A) << 1) | gpio_read(PIN_B);
position += qtable[(old_state << 2) | new_state];
old_state = new_state;
}
Any (old, new) pair that isn't a valid single-step transition (e.g. 0→2, a "diagonal" jump) returns 0 rather than ±1. This is what rejects contact bounce and missed edges without a separate debounce stage, as covered in more depth in how do you debounce a button or switch?
This approach costs one interrupt per edge. For a 4x decode (see below) that's up to four interrupts per encoder line-count, which becomes a real CPU-loading concern at high shaft speed or with a high-resolution encoder. It's the right choice when the encoder is slow relative to the MCU (a manual UI knob, a low-speed position sensor) and no other peripheral budget is available.
Hardware Decoding: Timer Encoder Mode and PCNT
Most mainstream microcontroller families offload quadrature decoding to hardware entirely, which is the better choice whenever the peripheral is available:
STM32 general-purpose timers (TIM1, TIM2, TIM3, TIM4, TIM5, TIM8 on most families; check the specific part's timer table) support a dedicated Encoder Interface Mode: route the A and B signals to the timer's two input capture channels, configure TIMx_SMCR for encoder mode, and the timer's counter register automatically increments or decrements on every valid edge with no CPU involvement at all. Reading the current position is a single register read (TIMx_CNT); the timer optionally raises an interrupt only when the counter under/overflows, not on every pulse. This is the standard approach for motor-control feedback where a control loop needs the current position at every sample tick with zero jitter from software decode timing. Encoder mode is a distinct configuration from the same timer channels' general input capture mode. See STM32 timer configuration for PWM, input capture, and output compare for how those other channel modes work when the same TIMx peripheral isn't being used for encoder decoding.
ESP32's PCNT (Pulse Counter) peripheral provides equivalent hardware quadrature decoding: two PCNT channels can be configured with control-signal inputs so that one channel counts up and the other counts down based on the phase relationship, with a configurable watchpoint interrupt (e.g. fire only every N counts, or on overflow) instead of an interrupt per edge. Other platforms (many PIC, AVR, and Microchip parts with a dedicated QEI, or Quadrature Encoder Interface, peripheral) provide the same capability under a different peripheral name; check the specific part's peripheral list before assuming software decoding is required.
Where no dedicated encoder/QEI peripheral exists but a general-purpose timer's dual input-capture channels are free, DMA-driven capture of edge timestamps is a middle-ground option, with more setup complexity than encoder mode but still far lighter than a per-edge ISR, though this is uncommon in practice since most parts with any motor-control ambition include native encoder mode.
Resolution and 4x Decoding
An encoder's datasheet PPR (pulses per revolution) or CPR (cycles per revolution) rating describes one full A/B cycle, not the number of transitions a quadrature decoder can resolve. Because each of the four states in the table above is a distinct, individually detectable transition, a full quadrature decoder (whether software or hardware) achieves 4x resolution multiplication: a 100 PPR encoder yields 400 counts per revolution. Some designs deliberately decode only on the rising edge of channel A (1x decoding) or on both edges of A alone (2x decoding) to reduce interrupt load in a software implementation, at the cost of the resolution and, in the 1x case, more sensitivity to a single missed edge. Hardware encoder-mode timers on most platforms support configuring 1x/2x/4x modes directly; default to 4x unless a specific reason (e.g. deliberately matching a legacy pulse count) argues otherwise.
Choosing Software vs Hardware Decoding
| Factor | Software (ISR + state machine) | Hardware (timer encoder mode / PCNT) |
|---|---|---|
| CPU load | One interrupt per valid edge | Effectively zero (counter runs in hardware) |
| Missed-count risk at high speed | Rises with shaft speed and other interrupt load | Essentially eliminated within the peripheral's max input frequency |
| Peripheral availability | None required (any two GPIOs) | Requires a timer/PCNT channel not needed elsewhere |
| Position read | Software variable, always current | Single register read of the hardware counter |
| Multiple encoders | Scales linearly with interrupt load | Limited by the number of encoder-capable timer/PCNT instances on the part |
For anything beyond a slow manual control input, prefer hardware decoding if the part has the peripheral free. It removes an entire class of high-speed missed-count bugs rather than requiring careful ISR-latency budgeting to avoid them.
For high-speed shaft encoders feeding a tight motor-control loop on a platform without hard real-time guarantees (a Linux-based Raspberry Pi, for example), see does a Raspberry Pi need a real-time co-processor?. Encoder counting at speed is one of the concrete cases where scheduler jitter on a general-purpose OS can lose counts that a dedicated MCU timer would not.
Zeus Design designs the sensor interfacing and firmware for motor-feedback and motion-control products as part of full-stack electronics and firmware development.
Design Considerations
- Add pull-up (or pull-down) resistors on A and B unless the encoder has push-pull outputs. Open-collector/open-drain encoder outputs (common on lower-cost mechanical and optical encoders) float without an external pull resistor, producing erratic decode results that look like intermittent bounce rather than a wiring fault.
- Size the hardware timer's counter width against the application's homing strategy. A 16-bit hardware counter wraps every 65,536 counts, which is fine if the application re-zeros regularly or only needs relative motion between reads, but a design that expects the raw counter to represent absolute position over a long run needs to handle wraparound explicitly in software.
- Budget interrupt latency against maximum shaft speed for software decoding. At the encoder's maximum RPM and PPR, calculate the worst-case time between valid edges and confirm the ISR (including anything else that can delay it, such as higher-priority interrupts or critical sections) always completes well within that window; missing an edge under load corrupts the position count silently rather than raising an error.
- Use the Z/index channel for absolute reference, not for ongoing position tracking. The index pulse only fires once per revolution and is meant for a homing routine at startup or after a fault, not as a substitute for continuous A/B decoding.
- Fuse encoder counts with an IMU for wheeled-robot dead reckoning. Wheel encoder counts alone drift under wheel slip and don't capture heading directly; a mobile robot design commonly combines encoder-derived distance travelled with an accelerometer and gyroscope (IMU) for heading correction between odometry updates.
Common Mistakes
- Debouncing a mechanical encoder with an RC filter or a fixed delay, the way a push-button is debounced. This slows or distorts the quadrature edges themselves and can cause direction errors under fast rotation. A state-machine decoder that rejects invalid transitions handles contact bounce correctly without touching the signal edges. See how do you debounce a button or switch? for why the two cases need different fixes.
- Reading only one channel with a simple edge counter. This counts pulses but cannot determine direction, and a shaft that oscillates back and forth near a boundary (common in manual UI knobs) produces a runaway count in one direction instead of settling back to zero.
- Assuming every edge interrupt fires in time under system load. A software decoder starved of CPU time by a higher-priority interrupt or a long critical section elsewhere in the firmware silently drops counts. Position drifts without any error indication, which is far harder to diagnose in the field than a hard fault.
- Ignoring supply and signal integrity on long encoder cable runs. Encoders mounted at the end of a metre or more of cable (common on machine axes) are susceptible to noise-induced false edges on unshielded, un-terminated lines; differential line-driver encoder outputs (RS-422-style) exist specifically for this case and are worth the extra cost over single-ended outputs on long runs.
- Mixing up PPR and post-decode counts-per-revolution when specifying encoder resolution to a customer or in a requirements document. A "1000 PPR" encoder yields 4000 counts/rev after 4x quadrature decoding. Stating the wrong number leads to a 4x error in any downstream distance-per-count or velocity calculation.
Frequently Asked Questions
- Can you read a rotary encoder with just one channel instead of both A and B?
- You can count pulses on a single channel, but you lose direction information entirely — a single-channel edge counter can only tell you that the shaft moved, not which way, and any back-and-forth jitter near a position boundary (common with manual UI knobs) accumulates as a one-directional runaway count rather than settling back to zero. Reading both A and B, and decoding the quadrature relationship between them, is what makes direction detection possible; single-channel counting is only appropriate for applications that genuinely don't care about direction, such as a simple RPM/frequency measurement on a motor shaft.
- Is the STEP/DIR-style interface used for stepper motors the same as an encoder's A/B output?
- No — they look superficially similar (two digital signals related to motion) but serve opposite roles and are not interchangeable. STEP/DIR is a command interface: the host tells a stepper driver IC to move, as covered in how do you drive a stepper motor with a driver IC. An encoder's A/B quadrature output is a feedback interface: the encoder tells the host how far a shaft has actually moved, after the fact. A closed-loop motion system commonly uses both together — STEP/DIR to command a stepper (or a PWM signal to command a DC/BLDC motor) and a separate encoder's A/B output to confirm the commanded motion actually happened.
- Do absolute encoders use the same quadrature decoding described here?
- No. This page covers incremental encoders, which is what the quadrature A/B/Z signal description applies to. Absolute encoders report position as a digital code (commonly over SPI, SSI, or BiSS-C) rather than as relative pulses, so there is no quadrature decoding step at all — the host simply reads a position word directly. The trade-off is cost and complexity: absolute encoders are more expensive and need a digital serial interface driver, but retain position through a power cycle, which an incremental encoder cannot do without a separate homing routine.
References
Related Questions
How Do You Debounce a Button or Switch?
Mechanical switch contacts bounce for milliseconds. Compare hardware debounce (RC + Schmitt trigger) with firmware algorithms, and pick the right window.
How Do GPIO Pins Work on a Microcontroller?
GPIO (General Purpose Input/Output) pins let a microcontroller read digital signals and drive outputs. Learn how push-pull, open-drain, and pull resistors work.
What Are Interrupts in Embedded Systems and How Do They Work?
Interrupts let a microcontroller respond to hardware events instantly without polling. Learn how ISRs, NVIC priority, and interrupt latency work.
How Do You Drive a DC Motor with an H-Bridge?
How an H-bridge drives a brushed DC motor in both directions — topology, shoot-through and dead time, PWM decay modes, driver ICs, and current sensing.
Real-Time Control on Raspberry Pi: PREEMPT-RT and the Co-Processor Pattern
Linux scheduler jitter makes hard real-time impractical on a bare Raspberry Pi. Learn when PREEMPT-RT helps and when to add an MCU co-processor.
How Do You Configure the STM32 ADC?
Configure the STM32 ADC: regular vs injected channels, single/continuous/scan modes, sampling time, calibration, oversampling, and internal channels.