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 pinArduino Uno pinFunction
CLK (A)D2Quadrature channel A
DT (B)D3Quadrature channel B
SWany digital pinIntegrated push switch (active LOW)
+ / VCC5VSupply
GNDGNDCommon 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.

State-table quadrature decoding that rejects contact bounce
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 characteristicValueWhy it matters here
Logic voltage5 VMatches most hobby modules directly
ADC resolution10-bit (0–1023)Sets how finely an analog reading can be resolved
Analog inputsA0–A5 (six channels)Determines how many analog sensors can share the board
PWM outputsD3, D5, D6, D9, D10 and D11Needed for brightness, speed and tone control
I²C pinsA4 (SDA) and A5 (SCL)Fixed by hardware — wiring copied from another board may not match
Interrupt pinsD2 and D3 onlyRequired for counting fast or asynchronous events
Seriala single hardware UART shared with USBMonitor 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.