Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.

Rotary encoder with ESP32 DevKit V1 — interrupt code, no library

Read a KY-040 rotary encoder on an ESP32 DevKit V1 with IRAM_ATTR interrupts and a bounce-proof quadrature table, powered from 3V3, plus the push switch.

Arduino C++Verified — core esp32 3.3.12, 3 Nov 2026Compile-only

Community draft — report a mistake

A rotary encoder is the knob on menus, volume controls and 3D-printer screens: it clicks round endlessly and reports each step and its direction on two pins, CLK and DT. The KY-040 module adds a push switch.

On the ESP32 DevKit V1, both outputs get an interrupt, and a small lookup table decides whether each change is a real step or contact bounce — no library needed. Power the module from 3V3 so its pull-ups keep the signals at 3.3 V. The sketch prints the position and direction, and pressing the knob resets it to zero.

Wiring

ESP32 DevKit V1Only the pins used are shown · 3.3 V logicKY-040 rotary encoderD32D33D25GND3V3CLKDTSW+GND
Every connection
ESP32 DevKit V1KY-040 rotary encoderWireNotes
3V3+redThe module’s pull-ups go to +, so 3V3 keeps CLK and DT at 3.3 V. Not VIN.
GNDGNDblack
D32CLKyellowEncoder output A.
D33DTgreenEncoder output B.
D25SWbluePush switch to GND; the sketch turns on the internal pull-up.

3.3–5 V part, 3.3 V board. Power the module from your board’s logic voltage (3.3 V on ESP32/Pico) — its pull-ups go to the + pin, so CLK and DT will never exceed that voltage.

Board quirks

  • The sketch uses no library. Paul Stoffregen’s Encoder library is great on AVR, but its ESP32 support depends on the core version; an interrupt plus a 16-entry lookup table is short, portable and ignores contact bounce.
  • Interrupt handlers on the ESP32 must be marked IRAM_ATTR so they run from RAM; without it the board can crash with “Cache disabled but cached memory region accessed” during flash writes.
  • Don’t use D34–D39 for CLK/DT/SW unless you are sure your module has pull-ups on all three — those pins have no internal pull-ups, and SW usually has none on the module.
  • Most KY-040 encoders give 4 transitions per click (20 clicks per turn). If yours moves 2 per click, change / 4 to / 2.
  • Power from 3V3. On VIN the module’s 10 kΩ pull-ups would lift CLK and DT to 5 V.

Code

rotary-encoder-esp32.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// KY-040 style rotary encoder on an ESP32 DevKit V1 — no library, interrupt-driven.
// + → 3V3, GND → GND, CLK → D32 (GPIO32), DT → D33 (GPIO33), SW → D25 (GPIO25).
// A quadrature lookup table rejects contact bounce: invalid transitions count as zero.

constexpr uint8_t CLK_PIN = 32;
constexpr uint8_t DT_PIN = 33;
constexpr uint8_t SW_PIN = 25;

// Index = (previous AB << 2) | current AB. +1 / -1 for valid steps, 0 for bounce.
const int8_t QUAD[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};

volatile uint8_t lastAB = 0;
volatile int32_t quarterSteps = 0;

void IRAM_ATTR onEncoder() {
  uint8_t ab = (digitalRead(CLK_PIN) << 1) | digitalRead(DT_PIN);
  quarterSteps += QUAD[(lastAB << 2) | ab];
  lastAB = ab;
}

void setup() {
  Serial.begin(115200);
  pinMode(CLK_PIN, INPUT_PULLUP);  // the module has 10 kΩ pull-ups to +; these just help
  pinMode(DT_PIN, INPUT_PULLUP);
  pinMode(SW_PIN, INPUT_PULLUP);   // SW usually has no pull-up on the module
  lastAB = (digitalRead(CLK_PIN) << 1) | digitalRead(DT_PIN);
  attachInterrupt(digitalPinToInterrupt(CLK_PIN), onEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(DT_PIN), onEncoder, CHANGE);
  Serial.println("Rotary encoder: CLK 32, DT 33, SW 25 — turn or press the knob");
}

void loop() {
  static int32_t lastPos = 0;
  static bool lastSw = HIGH;
  static uint32_t swChanged = 0;

  noInterrupts();
  int32_t pos = quarterSteps / 4;  // KY-040: 4 transitions per detent
  interrupts();
  if (pos != lastPos) {
    Serial.printf("Position %ld (%s)\n", (long)pos, pos > lastPos ? "clockwise" : "anticlockwise");
    lastPos = pos;
  }

  bool sw = digitalRead(SW_PIN);
  if (sw != lastSw && millis() - swChanged > 30) {
    swChanged = millis();
    lastSw = sw;
    if (sw == LOW) {
      Serial.println("Button pressed — position reset to 0");
      noInterrupts();
      quarterSteps = 0;
      interrupts();
      lastPos = 0;
    }
  }
}

Library: Encoder (Paul Stoffregen).

Expected serial output

Serial Monitor · 115200Output
Rotary encoder: CLK 32, DT 33, SW 25 — turn or press the knob
Position 1 (clockwise)
Position 2 (clockwise)
Position 3 (clockwise)
Position 2 (anticlockwise)
Button pressed — position reset to 0

Common errors

Counts go the wrong way

Swap the CLK and DT wires, or swap the pin numbers in the sketch.

Position jumps by 2 or skips

Your encoder has a different number of transitions per detent. Try / 2, and make sure both CLK and DT have interrupts attached.

Board resets with a Guru Meditation error when turning the knob

An ISR without IRAM_ATTR, or Serial.print inside the ISR. Keep the handler as short as the one in the sketch and print from loop().

↑↓ to move · Enter to open