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 NodeMCU ESP8266 — interrupt code

Read a KY-040 rotary encoder on a NodeMCU ESP8266 with IRAM_ATTR interrupts on D5/D6, push button on D7, and a bounce-proof quadrature table.

Arduino C++Verified — core esp8266 3.1.2, 3 Nov 2026Compile-only

Community draft — report a mistake

A rotary encoder is the usual knob for menus and set-points — a thermostat, a timer, a pump run time. The KY-040 module gives two out-of-phase signals (CLK and DT) plus a push button.

On the NodeMCU, read it with interrupts on D5 and D6, mark the handler IRAM_ATTR as the ESP8266 requires, and put the button on D7 with the internal pull-up. D0 is out — it has no interrupt support.

Wiring

NodeMCU ESP8266Only the pins used are shown · 3.3 V logicKY-040 rotary encoderGND3V3D5D6D7CLKDTSW+GND
Every connection
NodeMCU ESP8266KY-040 rotary encoderWireNotes
3V3+redThe module’s CLK/DT pull-ups go to +, so 3V3 keeps them at the ESP8266’s level.
GNDGNDblack
D5CLKyellowD5 = GPIO14, interrupt on change.
D6DTgreenD6 = GPIO12, interrupt on change.
D7SWblueD7 = GPIO13, internal pull-up; press = LOW.

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

  • Every interrupt handler on the ESP8266 must be marked IRAM_ATTR. Without it the code runs from flash, and the board crashes with an “ISR not in IRAM” error as soon as it boots.
  • D0 (GPIO16) can’t take interrupts, so it is no use for CLK or DT. D3, D4 and D8 can, but a turned knob can leave a contact closed at reset and pull a boot pin the wrong way. D5/D6/D7 are the safe trio.
  • Interrupts fire on both CLK and DT, and a 16-entry table accepts only valid quadrature steps. Contact bounce produces invalid transitions that count as zero — so no capacitors or delays are needed.
  • A KY-040 gives 4 transitions per click, hence position / 4. Some cheap encoders give 2 — change the divisor if one click moves the count by 2 or by half.
  • The sketch copies position with interrupts off. A 32-bit read is atomic on the ESP8266, but the habit matters once you read more than one shared variable.

Code

rotary-encoder-nodemcu.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// KY-040 rotary encoder on a NodeMCU (ESP8266), read with interrupts.
// + → 3V3, GND → GND, CLK → D5 (GPIO14), DT → D6 (GPIO12), SW → D7 (GPIO13).
const uint8_t ENC_CLK = D5;
const uint8_t ENC_DT = D6;
const uint8_t ENC_SW = D7;

volatile int32_t position = 0;  // in quarter-steps
volatile uint8_t lastState = 0;

// ISRs must sit in IRAM on the ESP8266, or the board crashes when flash is busy.
void IRAM_ATTR onEncoder() {
  // Quadrature table: +1 / -1 for valid transitions, 0 for contact bounce.
  static const int8_t table[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
  uint8_t s = (digitalRead(ENC_CLK) << 1) | digitalRead(ENC_DT);
  position += table[(lastState << 2) | s];
  lastState = s;
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  pinMode(ENC_CLK, INPUT);        // the module has 10 kΩ pull-ups on CLK and DT
  pinMode(ENC_DT, INPUT);
  pinMode(ENC_SW, INPUT_PULLUP);  // many modules leave the SW pull-up unfitted
  lastState = (digitalRead(ENC_CLK) << 1) | digitalRead(ENC_DT);
  attachInterrupt(digitalPinToInterrupt(ENC_CLK), onEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_DT), onEncoder, CHANGE);
  Serial.println("Turn the knob; press it to reset to 0");
}

void loop() {
  static int32_t shown = 0;
  noInterrupts();
  int32_t steps = position / 4;  // one detent = 4 quarter-steps on a KY-040
  interrupts();
  if (steps != shown) {
    shown = steps;
    Serial.printf("Position %d\n", shown);
  }
  if (digitalRead(ENC_SW) == LOW) {
    noInterrupts();
    position = 0;
    interrupts();
    shown = 0;
    Serial.println("Reset to 0");
    while (digitalRead(ENC_SW) == LOW) delay(10);  // wait for release; delay() feeds the watchdog
  }
}

Library: Encoder (Paul Stoffregen).

Expected serial output

Serial Monitor · 115200Output
Turn the knob; press it to reset to 0
Position 1
Position 2
Position 3
Position 2
Reset to 0

Common errors

Crash at boot: “ISR not in IRAM!”

The handler is missing IRAM_ATTR. See the full fix.

Count goes the wrong way

Swap the CLK and DT wires (or the pin constants).

Button reads pressed all the time

SW is on D3/D4/D8 or not connected. Use D7 with INPUT_PULLUP as in the sketch.

↑↓ to move · Enter to open