Electronics Design AU
Sensors

How Do You Interface a Rotary Encoder to a Microcontroller?

Last updated 6 July 2026 · 10 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 (volume knobs, 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.

The Quadrature Signal

An incremental encoder's sensing element — 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:

ABState
000
101
112
013

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.

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 — 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 — 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

FactorSoftware (ISR + state machine)Hardware (timer encoder mode / PCNT)
CPU loadOne interrupt per valid edgeEffectively zero — counter runs in hardware
Missed-count risk at high speedRises with shaft speed and other interrupt loadEssentially eliminated within the peripheral's max input frequency
Peripheral availabilityNone required (any two GPIOs)Requires a timer/PCNT channel not needed elsewhere
Position readSoftware variable, always currentSingle register read of the hardware counter
Multiple encodersScales linearly with interrupt loadLimited 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 — 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 — higher-priority interrupts, 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

Related Forum Discussions