The EN and BOOT buttons
Two buttons, one pin read at one instant. Everything about download mode, auto-reset and the hold-BOOT-tap-EN ritual follows from that single fact.
EN resets, GPIO 0 votes
EN is the chip's enable line, pulled high through a 10 k resistor. Pull it low and the chip stops; let it rise and the chip starts. That rising edge is a reset, and it is the only thing the EN button does.
GPIO 0 is an ordinary pin with one extra job: at the instant EN rises, the ROM bootloader reads it. High means run the sketch in flash. Low means wait for esptool. After that instant GPIO 0 is an ordinary pin again for the rest of the run.
Which is why the ritual is what it is
Hold BOOT, tap EN, release. The holding is only there to guarantee GPIO 0 is low at the edge, and you cannot see the edge, so you hold across it. Do it in the other order and GPIO 0 is high when it counts — the sketch starts, and by the time you press BOOT the vote has been counted.
The auto-reset circuit on a dev board performs exactly this sequence with two transistors driven by the serial port's DTR and RTS lines. When it works you never think about any of this; when the cable or the driver does not drive those lines, you do it by hand.
What that means for your wiring
GPIO 0 is a fine output. It is a bad input, and a terrible place for anything that pulls it low — a button to ground, a sensor that idles low, a module's interrupt line. All of those are invisible while the board runs and put it in download mode at the next reset, which looks exactly like a board that has stopped working.
The classic ESP32 has three more pins that are read at boot: GPIO 2, 12 and 15. The next page has the whole list.
The code
The chip records why it last started. Print it in setup and a mystery reboot stops being a mystery.
#include <esp_system.h>
void setup() {
Serial.begin(115200);
delay(200);
switch (esp_reset_reason()) {
case ESP_RST_POWERON: Serial.println("power on"); break;
case ESP_RST_SW: Serial.println("ESP.restart()"); break;
case ESP_RST_PANIC: Serial.println("crash"); break;
case ESP_RST_INT_WDT:
case ESP_RST_TASK_WDT: Serial.println("watchdog"); break;
case ESP_RST_BROWNOUT: Serial.println("brown-out"); break;
case ESP_RST_DEEPSLEEP:Serial.println("woke from sleep"); break;
default: Serial.println("other"); break;
}
}
void loop() {}A brown-out and a watchdog reset look identical from the outside — the board just restarts. This tells them apart in one line.
The same question, one call. machine.reset_cause() returns a constant with a name.
import machine
CAUSE = {
machine.PWRON_RESET: "power on",
machine.HARD_RESET: "EN pin",
machine.WDT_RESET: "watchdog",
machine.DEEPSLEEP_RESET: "woke from sleep",
machine.SOFT_RESET: "soft reset",
}
print(CAUSE.get(machine.reset_cause(), "other"))DEEPSLEEP_RESET is the one to check for first: it tells you this boot is a continuation rather than a fresh start, which changes what setup code needs to run.
When it does not work
The auto-reset circuit is not working. It is two transistors driven by the DTR and RTS lines of the USB chip, and a cable, a hub or a driver that does not assert them leaves the chip in run mode. Manual entry works forever, so this is an annoyance rather than a fault.
Something is holding GPIO 0 low — commonly a module wired to it, or a stuck BOOT button. The chip is waiting for another upload. Release the pin and tap EN.
EN has a 10 k pull-up and usually a small capacitor, and it is a high-impedance node. Long wires to it pick up noise. If you have brought EN out to a panel button, keep the run short and put 0.1 µF across it.
Brown-out, not boot mode. The transmit burst pulls the rail down until the detector fires. The reset reason above will say so in one word.
GPIO 0 is not the only pin with another job. About a third of the header is already spoken for, and the header does not say so.
The pins you cannot use →Edit this page — content/esp32/en-and-boot-pins.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.