A rotary encoder converts shaft rotation into digital pulses. Unlike a potentiometer, it has no end stops and produces absolute position information in its incremental form -- you count pulses, not voltage levels. This makes it the right choice for menu navigation, motor position feedback, tuning controls, and any application where you need to track relative rotation reliably.
The KY-040 is the most common breakout module for maker projects. Under the hood it is a mechanical incremental encoder with an integrated push switch. This guide covers the quadrature output, pull-up resistors, debouncing, and how to read it correctly on an Arduino.
How an Incremental Rotary Encoder Works
Inside a mechanical rotary encoder, two rows of contacts sit 90 degrees out of phase with each other relative to the mechanical step angle. These produce two output signals, A and B (also called CLK and DT on breakout modules).
When you rotate the shaft clockwise, A leads B:
- A rises before B → step is clockwise
- A falls before B → step is clockwise (other edge)
Counterclockwise rotation reverses this:
- B rises before A → step is counterclockwise
This two-channel quadrature arrangement is what allows direction detection from a purely incremental (pulse-counting) sensor. A single-channel encoder can count steps but cannot determine direction.
Resolution
Resolution is specified in PPR (pulses per revolution) or detents. A typical KY-040 has 20 detents per revolution and produces 20 pulses per revolution per channel. Some encoders produce pulses between detents as well, giving effective 4x resolution per revolution with quadrature decoding.
KY-040 Pinout
| Pin Label | Function |
|---|---|
| + (or VCC) | 3.3--5V supply |
| GND | Ground |
| SW | Push switch output (active LOW) |
| DT (B) | Quadrature output channel B |
| CLK (A) | Quadrature output channel A |
The KY-040 breakout module includes onboard 10kΩ pull-up resistors on CLK, DT, and SW connected to VCC. When outputs are open (shaft at rest between contacts), all three pins read HIGH. Contact closure pulls them LOW.
If you are using a bare encoder (without the KY-040 breakout), you must add your own pull-up resistors -- see below.
Pull-Up Resistors: Required
Encoder contacts are simple mechanical switches that connect the output to GND when closed and float when open. Without pull-up resistors, the "open" state is not a clean logic level -- the pin floats and picks up noise.
For a bare encoder:
- Connect CLK and DT to VCC through 10kΩ resistors each.
- Connect SW to VCC through a 10kΩ resistor.
- Encoder common pin connects to GND.
Arduino's internal pull-ups (~20kΩ--50kΩ) can substitute for external ones: use pinMode(pin, INPUT_PULLUP). At slow rotation speeds, internal pull-ups are fine. For high-speed counting, external 10kΩ pull-ups are more reliable because the impedance is lower and pull up faster.
Connecting KY-040 to Arduino
| KY-040 Pin | Arduino Pin |
|---|---|
| + (VCC) | 5V |
| GND | GND |
| CLK | D2 (interrupt pin) |
| DT | D3 (interrupt pin) |
| SW | D4 (or any digital pin) |
Using interrupt-capable pins (D2, D3 on Arduino Uno) for CLK and DT is important at higher rotation speeds. Polling works for a menu knob turned slowly by hand, but polling in the main loop will miss pulses if the code is doing anything else.
Reading the Encoder: Code Structure
The cleanest approach for direction detection reads both channels on a CLK interrupt:
const int clkPin = 2;
const int dtPin = 3;
volatile int position = 0;
void setup() {
pinMode(clkPin, INPUT_PULLUP);
pinMode(dtPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(clkPin), encoderISR, CHANGE);
}
void encoderISR() {
int clk = digitalRead(clkPin);
int dt = digitalRead(dtPin);
if (clk != dt) {
position++; // clockwise
} else {
position--; // counterclockwise
}
}
This triggers on every CLK edge (CHANGE). At each interrupt, compare CLK and DT: if they differ, increment; if they match, decrement. The logic follows from the quadrature phase relationship.
For applications where you need 4x resolution (detecting both edges of both channels), attach an interrupt to DT as well, with the same comparison reversed.
Debouncing
Mechanical encoders bounce. Each contact transition produces multiple brief open/close events before settling. Without debouncing, one physical detent click can register as 2--5 pulses.
Software debouncing approaches:
- Timeout: After detecting a transition, ignore any further transitions for 1--5ms.
- State machine: Track the full four-state quadrature sequence (00 → 01 → 11 → 10 → 00 for CW) and only count complete valid sequences. Invalid transitions (skipped states, bounce) are discarded.
- Hardware RC filter: A 10kΩ resistor in series with each encoder output and a 100nF capacitor to GND forms a low-pass filter that absorbs bounce spikes. The KY-040 breakout does not include this -- add it on a breadboard if you see spurious counts.
For menu navigation (slow rotation), a 5ms timeout debounce in software is usually sufficient. For motor feedback encoders, use a hardware RC filter or a dedicated encoder IC (LS7366R).
Push Switch (SW Pin)
The SW pin on the KY-040 is active LOW -- pressing the shaft down pulls SW to GND. The KY-040 includes a pull-up, so the idle state is HIGH.
On Arduino, read it with digitalRead() and check for LOW:
if (digitalRead(swPin) == LOW) {
// button pressed -- debounce as needed
}
Add software debouncing (check that LOW persists for > 20ms) to avoid multiple triggers from a single press.
Common Wiring Mistakes
Swapping CLK and DT: The encoder works but direction is reversed. Swap the wires or invert the direction in firmware.
No pull-up resistors on a bare encoder: Outputs float and register constant noise. Always add pull-ups.
Polling instead of interrupts for fast rotation: Pulses are missed. Use interrupts for anything faster than manual hand rotation.
Connecting both CLK and DT to non-interrupt pins on Uno: D2 and D3 are the two interrupt pins on Arduino Uno and Nano. If both are already used, consider using an ATmega328P with pin-change interrupts instead, or switch to a Teensy or STM32 which have many more interrupt inputs.
Simulating the Circuit
Before finalizing the encoder wiring in a project, sketch the full connection in CircuitDiagramMaker -- encoder symbol, pull-up resistors, optional RC debounce filter, and the Arduino connections. For complex encoder interfaces (multiple encoders, dedicated counter ICs like the LS7366R), a clear circuit diagram is essential to verify signal routing before soldering.
Create Your Own Rotary Encoder Circuit Diagram
- Place the encoder symbol with CLK, DT, SW, VCC, GND pins labeled
- Show pull-up resistors (with values) or note that the KY-040 module includes them
- Add optional hardware RC debounce filters
- Wire to Arduino with interrupt pin assignments documented
- Export the diagram for your build notes or project README
Create your own rotary encoder circuit diagram -- free
Key Takeaways
- Incremental rotary encoders use two quadrature channels (A/B, or CLK/DT) that are 90 degrees out of phase; direction is determined by which channel leads the other.
- The KY-040 breakout has onboard 10kΩ pull-ups; bare encoders require external pull-ups to VCC or
INPUT_PULLUPmode on Arduino. - Connect CLK and DT to interrupt-capable pins (D2, D3 on Uno) to avoid missing pulses at moderate rotation speeds.
- Mechanical encoders bounce -- use a software timeout, a state machine debounce, or an RC hardware filter to prevent spurious counts.
- The SW push switch is active LOW; the idle state is HIGH through the pull-up resistor.
- For high-speed motor feedback or multi-encoder systems, consider a dedicated counter IC like the LS7366R rather than software counting.
Originally published at https://circuitdiagrammaker.app/blog/rotary-encoder-circuit-diagram.
Top comments (0)