Why Your Rotary Encoder Counts Wrong (And How to Fix It)
You connect a rotary encoder to your Arduino. You write the code. You rotate the shaft one click. The serial monitor shows three counts. You rotate it clockwise and the count goes down. You rotate counterclockwise and the count goes up. You add a delay. The count is still wrong.

Rotary encoder wiring — A and B channels must be debounced either in hardware (RC filter) or software (interrupt-based state table) to count correctly.
You assume the encoder is defective. You buy a new one. The same problem. You buy a new one. The same problem.
Rotary encoder problems are almost always about debouncing. And if the direction is wrong, the phase relationship between the two outputs is also wrong. Both are solvable.
How a Mechanical Rotary Encoder Works
A mechanical rotary encoder has two contacts, typically labeled A and B, that make contact with a common ground as the shaft rotates. The contacts are never perfectly synchronized — when A makes contact, B is either about to make contact or has just broken contact, depending on the rotation direction.
This creates a two-bit gray code sequence. For clockwise rotation: A rises, then B rises, then A falls, then B falls. For counterclockwise: A rises, then B falls, then A falls, then B rises.
The Arduino reads the state of A and B at each interrupt. The direction is determined by which transition happened and what the previous state was. This is the standard way to read an incremental rotary encoder.
The problem: mechanical contacts bounce. When A makes contact, it does not cleanly go from high to low or low to high. It bounces: high, low, high, low, high in the space of 1-5ms. Each bounce triggers an interrupt. Your Arduino counts each bounce as a separate step.
The solution is debouncing. Hardware debouncing uses a capacitor across the contact. Software debouncing uses a timer to ignore bounces.
Software Debouncing Without Blocking
The naive debounce approach uses delay(). This works but blocks the main loop and makes the sketch unresponsive to other inputs.
The correct approach uses a state-based debouncer with a timestamp:
#define ENCODER_A 2
#define ENCODER_B 3
volatile long count = 0;
volatile unsigned long lastChange = 0;
const unsigned long debounceTime = 1000; // microseconds
void encoderISR() {
unsigned long now = micros();
if (now - lastChange < debounceTime) return;
lastChange = now;
static int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
static uint8_t old_AB = 3;
uint8_t new_AB = (digitalRead(ENCODER_A) << 1) | digitalRead(ENCODER_B);
old_AB = (old_AB << 2) | new_AB;
count += enc_states[old_AB & 0x0F];
}
This uses a state transition table. The enc_states array maps each possible 4-bit transition to a count increment (+1), decrement (-1), or no change (0). This handles direction correctly and ignores invalid transitions.
Why Direction Is Wrong
If clockwise increases the count and counterclockwise decreases it, your A and B channels are wired correctly. If the direction is reversed, A and B are swapped.
This is not a code problem. It is a wiring problem. Swap the A and B wire connections and the direction will be correct.
Some encoders have different phase relationships. The state transition table above assumes a specific encoder type. If your encoder counts backward after swapping A and B, your encoder uses the opposite phase relationship. In that case, the enc_states table should be:
int8_t enc_states[] = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};
Try both tables until the direction is correct.
The Difference Between Quadrature and Gray Code Encoders
Quadrature encoders have two output channels (A and B) that are 90 degrees out of phase. Gray code encoders have multiple output channels that represent position in a binary-like code.
Most hobby rotary encoders are quadrature. The term "quadrature" refers to the 90-degree phase relationship, not the number of positions per revolution.
Gray code encoders are used in industrial absolute position sensing. They are more expensive and the reading method is completely different. If your encoder has more than 2 signal pins (besides ground), it might be gray code or an absolute encoder, and the quadrature reading method will not work.
What Resolution Do You Actually Need?
Encoders are sold by "pulses per revolution" (PPR) or "cycles per revolution" (CPR). These are different.
PPR (pulses per revolution) is the number of rising edges per channel per revolution. For a 30 PPR encoder, the A channel rises 30 times per revolution.
CPR (counts per revolution) is the number of counts after quadrature decoding. Each PPR creates 4 counts (rising A, rising B, falling A, falling B). So a 30 PPR encoder gives 120 CPR.
The encoder you buy might say 30 PPR, which gives 120 counts per revolution. After the state transition table, you still get 120 counts per revolution.
If you need finer resolution, buy a higher PPR encoder. But also consider whether you need that resolution. For a volume control on a menu, 30 PPR (120 counts per revolution) is more than enough. For a robot wheel encoder measuring small distances, you need higher resolution.
When Hardware Debouncing Is Better
Software debouncing with interrupts works for encoders that are turned by hand. For encoders on motors, especially high-speed motors, the interrupt approach becomes unreliable because the interrupts fire too fast.
For motor-shaft encoders, use hardware debouncing with an RC filter:
Encoder A ──────┬──────── Arduino pin
│
10kΩ resistor
│
100nF capacitor
│
GND
The RC filter slows the edges. The encoder signal goes through the resistor and charges the capacitor. The capacitor voltage rises slowly. When the encoder bounces, the capacitor smooths the bounce. The Arduino sees a clean transition.
The tradeoff: the RC filter introduces latency. At high motor speeds, this latency can cause missed counts. For high-speed applications, use an optical encoder with open-collector transistor outputs, which do not bounce.
The encoder is not broken. Mechanical contacts bounce, and the Arduino sees every bounce as a count. Debounce in hardware or software, and the count becomes correct.
For reliable rotary encoder reading:
Bournes PEC11R Mechanical Rotary Encoder — 30 PPR, panel mount, satisfying click feel. Good for menu navigation and volume controls. (Amazon)
Optical Rotary Encoder 600 PPR — For motor shaft speed and position measurement. No contact bounce, works at high RPM. (Amazon)
100nF Ceramic Capacitor Assortment — For hardware debouncing RC filters on encoder signal lines. (Amazon)
I earn from qualifying purchases.
Article #017, 2026-04-18. Content Farm pipeline, Run #017.
Top comments (0)