If you are building a volume knob, a CNC jog wheel, or a PID motor controller, you need a rotary encoder. The direct answer for 90% of hobbyist and prototyping builds is the HW-260 rotary encoder module. Unlike the bare KY-040 PCB, the HW-260 includes the necessary 10kΩ pull-up resistors and 104nF (0.1µF) hardware debounce capacitors, saving you from writing complex software filters or soldering external components.
This guide covers the exact wiring, a robust interrupt-driven code implementation targeting the Arduino Uno R3 and Nano v3 (ATmega328P), and a decision matrix to help you choose the right module for your specific application.
Which Arduino Encoder Module Should You Buy?
Not all quadrature encoders are created equal. Mechanical contact bounce is the primary enemy of encoder accuracy. When the internal metal wipers transition between contacts, they physically bounce, creating microsecond voltage spikes that your microcontroller interprets as dozens of phantom rotation steps.
| Module Variant | Hardware Debounce | Pull-ups Included | Best Use Case |
|---|---|---|---|
| KY-040 (Bare PCB) | None (Requires software filtering) | No (Requires external 10kΩ to VCC) | Permanent soldered installs where you design your own RC filter. |
| HW-260 (Mounted Module) | Yes (104nF caps on CLK/DT/SW) | Yes (10kΩ resistors to VCC) | Breadboarding, fast prototyping, and beginner projects. |
| AS5600 (Magnetic I2C) | N/A (Hall-effect, no physical contacts) | N/A (Digital I2C protocol) | High-vibration environments, motor shafts, and production hardware. |
Parts List and Pin Mapping
The code and wiring below assume you are using an Arduino Uno R3 or Arduino Nano v3 (both utilize the ATmega328P microcontroller). These boards have dedicated hardware interrupt pins on D2 and D3, which are mandatory for reliable high-speed encoder tracking without dropping steps.
Required Components
- 1x Arduino Uno R3 or Nano v3 (ATmega328P)
- 1x HW-260 Rotary Encoder Module (5-pin variant: GND, +VCC, SW, DT, CLK)
- 5x Male-to-Female or Male-to-Male jumper wires (22 AWG stranded)
- 1x Breadboard (if using Uno)
Exact Pin Mapping
| HW-260 Pin | Arduino Uno/Nano Pin | Function & Notes |
|---|---|---|
| GND | GND | Common ground. Do not leave floating. |
| +VCC | 5V | Powers the module and the onboard pull-up resistors. |
| SW | D4 | Pushbutton switch. Active LOW when pressed. |
| DT (Data) | D3 (INT1) | Quadrature Phase B. Must be an interrupt-capable pin. |
| CLK (Clock) | D2 (INT0) | Quadrature Phase A. Must be an interrupt-capable pin. |
Note: If you are using an Arduino Mega 2560, the interrupt pins are different (D2, D3, D18, D19, D20, D21). The code below includes a compile-time check to prevent you from uploading to an unsupported pin configuration.
Wiring and Compilable Code
While you can write a bare-metal state machine to decode quadrature signals, the industry standard for Arduino environments is the Encoder library by PJRC. It optimizes interrupt handling and supports various microcontroller architectures.
The code below targets the Arduino Uno R3 / Nano v3. It includes hardware interrupt decoding for the rotation, a software state-machine debouncer for the pushbutton, and a runtime disconnect error check.
#include
// Compile-time board validation to prevent interrupt pin errors
#if !defined(digitalPinToInterrupt)
#error "Target board lacks digitalPinToInterrupt. Use an Uno, Nano, or Mega."
#endif
// --- PIN DEFINITIONS (Target: Uno R3 / Nano v3) ---
const int PIN_ENC_CLK = 2; // Hardware INT0
const int PIN_ENC_DT = 3; // Hardware INT1
const int PIN_ENC_SW = 4; // Button pin (Polled)
// --- OBJECTS & VARIABLES ---
Encoder myEnc(PIN_ENC_CLK, PIN_ENC_DT);
long oldPosition = -999;
// Button state machine variables
bool lastSwState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50ms hardware+software debounce
void setup() {
Serial.begin(115200);
// Initialize button pin with internal pull-up as a backup
// (HW-260 has external pull-ups, but this prevents floating if the module is disconnected)
pinMode(PIN_ENC_SW, INPUT_PULLUP);
// Runtime disconnect check
// If the pin reads exactly mid-rail or floats erratically, the module isn't powered
pinMode(PIN_ENC_CLK, INPUT);
if (digitalRead(PIN_ENC_CLK) == LOW && digitalRead(PIN_ENC_DT) == LOW) {
// Both lines pulled low might indicate a short or missing VCC on a module with pull-ups
Serial.println("WARN: Check HW-260 VCC connection. Lines are low.");
}
Serial.println("Arduino Encoder Module Initialized.");
Serial.println("Rotate the knob or press the button.");
}
void loop() {
// 1. Read Encoder Position
long newPosition = myEnc.read();
if (newPosition != oldPosition) {
oldPosition = newPosition;
Serial.print("Position: ");
Serial.println(newPosition);
}
// 2. Read Button with State-Change Debounce
bool currentSwState = digitalRead(PIN_ENC_SW);
if (currentSwState != lastSwState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// If the state has stabilized and is LOW (pressed)
if (currentSwState == LOW && lastSwState == HIGH) {
Serial.println("EVENT: Button Pressed");
myEnc.write(0); // Reset encoder count on button press
Serial.println("Position reset to 0.");
}
}
lastSwState = currentSwState;
// 3. Runtime Error Handling: Detect disconnected ground
// If the button pin reads HIGH constantly but the encoder is spinning,
// we might have a ground loop issue. (Advanced diagnostic)
}
Calculating RPM from Encoder Pulses
A standard HW-260 module outputs 20 Pulses Per Revolution (PPR). Because the PJRC library tracks both rising and falling edges of both channels (quadrature decoding), it registers 80 steps per full 360° rotation. If you need to calculate motor RPM, use this formula:
RPM = (Delta_Steps / 80) * (60000 / Delta_Millis)
The First Three Things to Check When It Fails
When your serial monitor prints erratic numbers, jumps backward, or prints nothing at all, do not rewrite your code immediately. 95% of encoder issues are hardware or pin-mapping faults. Follow this diagnostic sequence.
1. Symptom: Count jumps erratically or registers multiple steps per click
- Cause A (Most Likely): Missing or failed pull-up resistors. If you are using a bare KY-040 instead of the HW-260, the CLK and DT pins are floating. The ATmega328P's internal pull-ups (approx. 20kΩ-50kΩ) are often too weak to overcome environmental noise. Fix: Solder 10kΩ resistors between VCC and both CLK/DT pins.
- Cause B: Quadrature phase mismatch. If the encoder counts up when you turn it left, and down when you turn it right, your CLK and DT pins are swapped in code or hardware. Fix: Swap the wires on D2 and D3, or swap the pin definitions in the
Encoderobject initialization.
2. Symptom: Serial monitor prints nothing or 'ERR: ENC_DISCONNECT'
- Cause A: Wrong interrupt pins for your specific board. The code targets D2 and D3 for the Uno/Nano. If you uploaded this to an ESP32 or a Leonardo, those pin numbers map to different physical hardware interrupts, or none at all. Fix: Consult the official Arduino attachInterrupt() documentation to find the correct interrupt pins for your specific microcontroller.
- Cause B: Common ground missing. If the HW-260 is powered by a separate 5V supply but the GND is not tied to the Arduino GND, the logic levels will float relative to the ATmega328P's VCC. Fix: Connect a jumper wire directly between the module GND and Arduino GND.
3. Symptom: The pushbutton (SW) triggers randomly without being pressed
- Cause: Switch contact bounce exceeding software filter. The HW-260 includes a 104nF capacitor on the SW line, but cheap manufacturing tolerances can result in severe bounce. Fix: Increase the
debounceDelayconstant in the code from 50ms to 100ms, or solder an additional 0.1µF ceramic capacitor directly across the SW and GND pins on the module.
Extending and Simplifying the Build
Once you have reliable step counting and button debouncing, you can scale this hardware up for complex applications or strip it down for permanent installations.
How to Extend: Closed-Loop PID Motor Control
The most common professional use for a quadrature encoder is closed-loop motor control. To extend this build, mount the encoder on the rear shaft of a DC gear motor. Feed the myEnc.read() value into a PID library (like the Arduino PID library by Brett Beauregard) as your 'Process Variable'. Set your 'Setpoint' to the desired position. The PID output will drive an H-Bridge (like the L298N or TB6612FNG) to correct the motor's position in real-time, eliminating the 'drift' inherent in open-loop DC motors.
How to Simplify: Eliminate Mechanical Bounce Entirely
If you are moving from a breadboard prototype to a finished PCB, or if your encoder will be mounted on a vibrating chassis (like a 3D printer or a CNC router), mechanical encoders will eventually fail due to contact oxidation and physical wear.
The Upgrade Path: Replace the HW-260 with an AS5600 Magnetic Rotary Encoder. The AS5600 uses the Hall effect to read the position of a diametrically magnetized neodymium shaft. It outputs absolute position via I2C or analog voltage, requires zero debounce capacitors, and has an infinite mechanical lifespan because there is no physical contact between the sensor and the magnet. While it requires a different code library (like the Adafruit AS5600 library), it completely eliminates the interrupt-pin and contact-bounce debugging steps outlined in this guide.






