A push button is the simplest input, but the NodeMCU has a few pins where it behaves differently. On D5 with the internal pull-up, wired to GND, it just works — and with an interrupt the sketch catches every press even while Wi-Fi is busy.
The sketch also shows a NodeMCU trick: the FLASH button on the board is GPIO0 (D3), and once your sketch is running it is a free extra input.
Wiring
| NodeMCU ESP8266 | Push button (6 mm tactile switch) | Wire | Notes |
|---|---|---|---|
| D5 | 1 | yellow | GPIO14 with INPUT_PULLUP: reads HIGH, LOW while pressed. |
| GND | 2 | black | Use the diagonal legs of a 4-pin tactile switch so the wires sit on opposite sides. |
No supply — passive switch part, 3.3 V board. Wire the button between a GPIO and GND and use the internal pull-up. Never connect a button between a GPIO and 5 V on a 3.3 V board.
Board quirks
- Wire the button to GND and use
INPUT_PULLUP. Never wire a button to 5 V/VIN — the ESP8266 pins are 3.3 V only. - D0 (GPIO16) has no pull-up (only
INPUT_PULLDOWN_16) and no interrupts. A button on D0 needs an external 10 kΩ pull-up and polling. - D3 is GPIO0 and already has the board’s FLASH button on it. After boot it is a normal input — the sketch reads it, so you get a second button for free. Don’t hold it during reset, or the board enters download mode.
- D8 (GPIO15) is pulled down on the board, so a button there must go to 3V3, not GND, and must not be held at power-up.
- Interrupt handlers on the ESP8266 need
IRAM_ATTR. Without it the board crashes with “ISR not in IRAM!” as soon as the interrupt is attached.
Code
// Push button on a NodeMCU (ESP8266): one leg → D5 (GPIO14), other leg → GND, internal pull-up.
// Also reads the board's own FLASH button (GPIO0 = D3), which is free to use once the sketch runs.
// Each press of the D5 button toggles the on-board LED (D4 / GPIO2, LOW = on).
const uint8_t BUTTON_PIN = D5;
const uint8_t FLASH_PIN = D3; // on-board FLASH button, pulled up on the board
volatile uint32_t presses = 0;
volatile uint32_t lastEdgeUs = 0;
// Interrupt handlers on the ESP8266 must live in IRAM, or the board crashes when flash is busy.
void IRAM_ATTR onPress() {
uint32_t now = micros();
if (now - lastEdgeUs > 200000) presses++; // 200 ms debounce
lastEdgeUs = now;
}
void setup() {
Serial.begin(115200);
Serial.println();
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(FLASH_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH); // off
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
Serial.println("Press the button on D5, or the FLASH button");
}
void loop() {
static uint32_t seen = 0;
static bool flashWasDown = false;
noInterrupts();
uint32_t n = presses;
interrupts();
if (n != seen) {
seen = n;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
Serial.printf("D5 button pressed (%u)\n", n);
}
bool flashDown = digitalRead(FLASH_PIN) == LOW;
if (flashDown && !flashWasDown) Serial.println("FLASH button pressed");
flashWasDown = flashDown;
delay(10);
}// Push button on a NodeMCU (ESP8266): one leg → D5 (GPIO14), other leg → GND, internal pull-up.
// Also reads the board's own FLASH button (GPIO0 = D3), which is free to use once the sketch runs.
// Each press of the D5 button toggles the on-board LED (D4 / GPIO2, LOW = on).
const uint8_t BUTTON_PIN = D5;
const uint8_t FLASH_PIN = D3; // on-board FLASH button, pulled up on the board
volatile uint32_t presses = 0;
volatile uint32_t lastEdgeUs = 0;
// Interrupt handlers on the ESP8266 must live in IRAM, or the board crashes when flash is busy.
void IRAM_ATTR onPress() {
uint32_t now = micros();
if (now - lastEdgeUs > 200000) presses++; // 200 ms debounce
lastEdgeUs = now;
}
void setup() {
Serial.begin(115200);
Serial.println();
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(FLASH_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH); // off
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
Serial.println("Press the button on D5, or the FLASH button");
}
void loop() {
static uint32_t seen = 0;
static bool flashWasDown = false;
noInterrupts();
uint32_t n = presses;
interrupts();
if (n != seen) {
seen = n;
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
Serial.printf("D5 button pressed (%u)\n", n);
}
bool flashDown = digitalRead(FLASH_PIN) == LOW;
if (flashDown && !flashWasDown) Serial.println("FLASH button pressed");
flashWasDown = flashDown;
delay(10);
}Expected serial output
Press the button on D5, or the FLASH button
D5 button pressed (1)
D5 button pressed (2)
FLASH button pressedPress the button on D5, or the FLASH button D5 button pressed (1) D5 button pressed (2) FLASH button pressed
Common errors
“ISR not in IRAM!” and a reboot loop
The interrupt function is missing IRAM_ATTR. Full fix.
One press counts several times
Contact bounce. The sketch ignores edges within 200 ms; raise that for very noisy switches.
Board goes into download mode when the button is held at power-up
The button is on D3 (GPIO0) or D4 (GPIO2) — both must be HIGH at reset. Use D5 for buttons you might hold.