Bench note
Encoder quadrature you'll decode without the phase chart
Why quadrature exists
An incremental rotary encoder outputs two square waves—A and B—offset by 90 degrees. That phase relationship tells you which way the shaft turned. If A leads B, you're spinning clockwise; if B leads A, counterclockwise. You count edges to measure distance.
Most hobby encoders are optical: a slotted disc interrupts two photointerrupters. Industrial versions use magnetic or capacitive sensing, but the output logic is identical. You get two digital signals and you have to interpret them without missing transitions.
Sample both channels on every edge
The reliable approach is edge-triggered interrupts on both A and B. When either pin changes, read the current state of both channels and compare against the last state you stored. A lookup table or a compact state machine maps those four bits to +1, -1, or 0 (invalid transition).
Polling works if your loop is fast enough—faster than half the shortest pulse width at maximum shaft speed. For a 600-pulse-per-revolution encoder spinning at 3000 RPM, that's a 100 µs pulse. Your poll interval needs to be under 50 µs, which is tight on an Arduino running other tasks. Interrupts let you sleep between edges.
Pull-ups or external resistors
Most encoders have open-collector outputs. Enable the microcontroller's internal pull-up resistors or add 4.7 kΩ externals to 3.3 V or 5 V, depending on your logic level. If the encoder datasheet specifies a maximum pull-up value, respect it—too weak and you'll see slow rise times that double-trigger your interrupt.
Some modules include onboard pull-ups. Check with a multimeter before you add your own, or you'll end up with a parallel resistance that shifts your logic thresholds.
Debounce in firmware, not hardware
Mechanical encoders (the clicky potentiometer kind) bounce. Optical encoders usually don't, but EMI near motors can inject noise. A simple debounce: ignore transitions that arrive within 1 ms of the last valid edge. Store a timestamp in your interrupt handler and reject changes that are too soon.
For high-resolution optical encoders on a servo, you want every pulse. In that case, add a small ceramic cap—10 nF to 100 nF—across each channel to ground, right at the microcontroller pin. That filters high-frequency glitches without slowing your real edges.
State tracking without the truth table
You can avoid a 16-entry lookup table with this pattern:
int8_t delta = 0;
if (A_new != A_old) {
delta = (A_new == B_new) ? -1 : +1;
} else if (B_new != B_old) {
delta = (B_new == A_new) ? +1 : -1;
}
position += delta;This checks which channel changed, then compares the new values. If A changed and now matches B, you moved backward; if they differ, forward. Swap the signs if your encoder runs the other way.
Absolute position from power-on
Incremental encoders don't know where they are when you boot. If you need a home position, add a limit switch or an index pulse (the Z channel some encoders provide once per revolution). On startup, drive the mechanism until you hit the switch, then zero your counter.
For a closed-loop control system, this homing sequence is your first state in the state machine. Don't assume the encoder starts at zero—store the offset and subtract it from every reading.
Log transitions during integration
When you first wire an encoder, log every A/B state change with a timestamp. Print A B position on one line per edge. Spin the shaft by hand and confirm the count increments smoothly in both directions. If you see the position jump by two or decrement when it should increment, you've swapped a wire or your edge logic is backwards.
I keep that debug output behind a compile flag so I can turn it on again when I move to a new encoder model. Saves an hour of confusion every time.
When to upgrade to a hardware counter
Most 32-bit micros have timer peripherals that decode quadrature in hardware. The STM32 family calls it "encoder mode"; the ESP32 has PCNT (pulse counter) modules. You configure two pins, and the peripheral maintains the count in a register. Your firmware just reads it.
Hardware counters never miss edges, even if your main loop stalls. Use them when you're tracking a fast motor or running other interrupt-heavy tasks. The setup is more involved—clock trees, pin remapping—but the result is a position value you can trust at any speed.
What you get
Quadrature decoding isn't exotic. It's two digital inputs, a bit of edge logic, and the discipline to test both directions before you close the enclosure. Once it works, you have reliable feedback for velocity estimation, position control, or just counting rotations. No timing diagram required.