Arduino Uno Rotary Encoder: Precision HMI Controller
Professional 5-pin incremental optical encoder delivers 400 pulses per revolution (100PPR×4 quadrature) through pins 2(CLK)/3(DT) interrupt decoding achieving 0.9° angular resolution. Integrated SPST push-button enables menu select/confirm with 50ms debounce filtering.
Gray code quadrature (A/B phase shift) determines CW/CCW direction with 4x multiplication. 5V operation, 10mA consumption, 100kRPH durability. Bounds limiting prevents wraparound in menu systems.
How It Works
A rotary encoder reports change, not position. Inside, two contacts — A and B — open and close against a patterned disc as the shaft turns, producing two square waves offset by a quarter cycle. That offset is the whole trick: comparing which channel changes first tells you the direction of rotation.
This is quadrature encoding. Over one detent the pair A,B steps through 00, 01, 11, 10 clockwise, and the reverse sequence anticlockwise. Because it is relative, the shaft has no end stops and can spin indefinitely — unlike a potentiometer, which has a fixed range.
Mechanical encoders such as the ubiquitous KY-040 bounce badly. Each transition produces a burst of spurious edges, and a naive sketch reads a single click as several steps in random directions. This is the single most common complaint about them.
The robust fix is a state-table decoder: track the previous A,B pair, combine it with the current pair, and only accept transitions that are valid in the quadrature sequence. Invalid combinations — which is what bounce produces — are simply discarded.
Components Needed
- Arduino Uno
- KY-040 Rotary Encoder Module (400PPR)
- Male-to-male jumper wires (5 pieces)
- Arduino Uno
- 220Ω menu display LED
- I2C LCD/OLED for visual feedback
Wiring to the Arduino Uno
Connect CLK to D2, DT to D3, and SW to any spare digital pin. Declare all three as INPUT_PULLUP — the encoder contacts simply short to ground, so pull-ups are required.
On the Arduino Uno the interrupt-capable pins are D2 and D3 only. Putting at least channel A on one of those lets the decoder catch every transition even when the sketch is busy, which matters if the knob is turned quickly.
Adding a 100 nF capacitor from each of CLK and DT to ground provides hardware debouncing and noticeably improves reliability on cheap modules — often more effective than any software change.
| Module pin | Arduino Uno pin | Function |
|---|---|---|
| CLK (A) | D2 | Quadrature channel A |
| DT (B) | D3 | Quadrature channel B |
| SW | any digital pin | Integrated push switch (active LOW) |
| + / VCC | 5V | Supply |
| GND | GND | Common ground |
Example Code
State-table quadrature decoding that rejects contact bounce. Upload it with the board set to Arduino Uno and open the Serial Monitor at 9600 baud.
const int PIN_A = 2;
const int PIN_B = 3;
const int PIN_SW = 9;
// Valid quadrature transitions; invalid ones (bounce) score 0
const int8_t TABLE[16] = {
0, -1, 1, 0,
1, 0, 0, -1,
-1, 0, 0, 1,
0, 1, -1, 0
};
volatile uint8_t prevState = 0;
volatile int8_t subStep = 0;
volatile long position = 0;
void readEncoder() {
uint8_t curr = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
uint8_t idx = (prevState << 2) | curr;
subStep += TABLE[idx];
prevState = curr;
if (subStep > 3) { position++; subStep = 0; } // one full detent
else if (subStep < -3) { position--; subStep = 0; }
}
void setup() {
Serial.begin(9600);
pinMode(PIN_A, INPUT_PULLUP);
pinMode(PIN_B, INPUT_PULLUP);
pinMode(PIN_SW, INPUT_PULLUP);
prevState = (digitalRead(PIN_A) << 1) | digitalRead(PIN_B);
attachInterrupt(digitalPinToInterrupt(PIN_A), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_B), readEncoder, CHANGE);
}
void loop() {
static long shown = 0;
if (position != shown) {
shown = position;
Serial.print("position: ");
Serial.println(shown);
}
if (digitalRead(PIN_SW) == LOW) {
Serial.println("button pressed — reset");
position = 0;
delay(250);
}
}
Applications
A rotary encoder turns up in a wide range of projects. These are the uses where it is the right choice rather than a compromise:
- Menu navigation on devices with an OLED or LCD
- Setpoint adjustment that keeps its value when power cycles
- Volume and parameter control in audio projects
- Measuring shaft rotation and direction on motors
- Jog wheels for CNC and camera control
Working with the Arduino Uno
The Arduino Uno is built around the ATmega328P and runs on 5 V logic with 2 KB of SRAM and 32 KB of program flash. These details change how this circuit is wired and what the sketch can do, so they are worth stating plainly before you build.
The Uno runs at 5 V, so most hobby sensor modules connect directly with no level shifting.
With only 2 KB of SRAM, avoid large buffers and prefer the F() macro for constant strings.
The single hardware UART is shared with the USB connection, so heavy Serial printing competes with uploads.
| Arduino Uno characteristic | Value | Why it matters here |
|---|---|---|
| Logic voltage | 5 V | Matches most hobby modules directly |
| ADC resolution | 10-bit (0–1023) | Sets how finely an analog reading can be resolved |
| Analog inputs | A0–A5 (six channels) | Determines how many analog sensors can share the board |
| PWM outputs | D3, D5, D6, D9, D10 and D11 | Needed for brightness, speed and tone control |
| I²C pins | A4 (SDA) and A5 (SCL) | Fixed by hardware — wiring copied from another board may not match |
| Interrupt pins | D2 and D3 only | Required for counting fast or asynchronous events |
| Serial | a single hardware UART shared with USB | Monitor runs at 9600 baud by default |
Troubleshooting
Most problems with this module fall into a handful of categories. Work through these before suspecting the part itself:
- One click jumps several counts — bounce. Use the state table rather than reading DT on a CLK edge.
- Direction is reversed — swap CLK and DT, or negate the step.
- Counts are missed when turning fast — polling cannot keep up; use interrupts as shown.
- The count changes while nobody touches it — pull-ups are missing and the inputs are floating.
- It works but feels unresponsive — the detent produces four transitions; make sure you divide by four rather than counting every edge.
- Code written for an ESP board gives odd analog values — the Arduino Uno uses a 10-bit ADC returning 0–1023; rescale any constant taken from a 12-bit example.
- An I²C sensor is not found after copying wiring from another Arduino — on the Arduino Uno I²C is on A4 (SDA) and A5 (SCL).
Taking It Further on the Arduino Uno
Once the basic reading works, where you go next depends very much on which board you are using. These are the directions that suit the Arduino Uno specifically:
The Uno’s shield ecosystem is its real advantage. Once the circuit works on a breadboard, a prototyping shield turns it into something permanent that still stacks with a data-logging or Ethernet shield without rewiring.
Logging to an SD card via a shield is the natural next step, since the Uno has no onboard storage and no network. Timestamp each reading with a DS3231 real-time clock so the log survives power cuts with correct times.
Because SRAM is limited to 2 KB, keep logged strings short and write them out immediately rather than buffering. Building a long String in memory is the most common cause of an Uno sketch that runs for hours and then freezes.
Notes and Practical Limits
Keep the ISR to the decode only. Printing or doing arithmetic inside an interrupt handler loses transitions and can hang the board — note that the sketch prints from loop(), not from the ISR.
Optical and magnetic encoders avoid bounce entirely and are the right choice for motor feedback. The mechanical KY-040 is for human-operated knobs, where a few milliseconds of settling is irrelevant.