ESP32 Button ISRs Need a Defined Idle Level and Deferred Logging

Karen Mitchell8 min read
Other ManufacturerOther TopicTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

On this ESP32, the button input on GPIO0 has no defined idle level, and its interrupt handler both toggles GPIO2 and calls formatted printf. Those are the first two issues to separate: an unstable input can generate false edges, while formatted output inside an interrupt can block or take too long.

What does the screen output tell you about the failure?

The project builds and flashes, so the next diagnosis belongs to runtime: determine whether a real button transition reaches the handler and whether the LED output changes. The current handler prints after toggling, but that print is not a reliable diagnostic in an ISR. It can delay interrupt handling, and a blocking UART implementation can hold the handler while characters transmit.

Observed behavior Likely area to inspect First check
Repeated messages or LED toggles without a press Floating input or switch bounce Give the input a defined idle level, then debounce.
No message and no LED change Input wiring, interrupt setup, selected edge, or GPIO choice Check the pin level at rest and while pressing; check GPIO API return values.
Messages appear but LED does not visibly change Output wiring, LED polarity, or GPIO2 selection Measure GPIO2 and confirm the board LED's active level.
Erratic behavior associated with reset or startup GPIO0 boot-strapping behavior on some ESP32 boards Check the specific board documentation and startup wiring.

Temporarily remove the ISR's printf before judging whether the input or output path works. Check this stage by confirming the symptom changes or remains when logging is removed; do not treat missing ISR text as proof that the edge never occurred.

What level does GPIO0 have while the button is idle?

The input configuration selects GPIO0 through (1ULL << 0), sets GPIO_MODE_INPUT, disables both internal pulls, and selects GPIO_INTR_POSEDGE. A disconnected or open switch therefore leaves the input without an internally defined idle state. Electrical noise or the nearby hand and wiring can produce transitions that look like button presses.

Choose the pull and edge from the actual circuit, not from the software intention:

Button wiring Idle level Configuration direction
Button connects GPIO0 to 3.3 V when pressed Low Enable pull-down and use a rising-edge interrupt for the press transition.
Button connects GPIO0 to ground when pressed High Enable pull-up and use a falling-edge interrupt for the press transition.

These choices assume the GPIO is wired directly to the stated rail when pressed. Confirm the board's circuit and allowed GPIO voltage before applying them. If the board already provides a suitable external resistor, account for that circuit rather than enabling a conflicting internal pull.

The source uses BUTTON_PIN as a definition but hard-codes zero in the mask and ISR registration. Replace those literals with the same pin identifier so configuration and handler registration cannot drift apart. Check by inspecting the configured mask, pull setting, edge, and registered handler pin together.

Could GPIO0 affect startup as well as button presses?

GPIO0 is a boot-strapping pin on some ESP32 variants or boards. Its level during reset can affect startup mode, depending on the device and board circuit; that is distinct from a floating input causing runtime interrupts. A build and flash that succeed do not prove the button wiring is harmless during reset or that every board treats GPIO0 identically.

  1. Identify the exact ESP32 board and read its pinout or schematic to determine whether GPIO0 is a strapping pin and what circuitry is already connected.
  2. Check the GPIO0 level during reset and normal operation, including whether the button can hold it at an unintended level during startup.
  3. If the board's use of GPIO0 conflicts with the button circuit, move the button to a suitable GPIO and update the pin definition, input mask, and interrupt registration consistently.

Check this stage by confirming normal reset and boot with the button released and pressed as appropriate. Do not infer the board-specific strap behavior from the GPIO number alone.

Why should the ISR stop printing and doing the full response?

An ISR should do the minimum work needed to record an event and leave longer processing to task context. printf performs integer formatting for the %d value in this program; that work can be expensive even if the output path buffers data. If output uses a blocking UART write, transmission time adds directly to interrupt latency. A slow handler can interfere with other time-sensitive work.

Remove the print from button_isr_handler. Have the ISR signal a task, then let the task update diagnostic output. A minimal flag can work for a simple experiment, but a single flag can collapse multiple edges into one event. For event counting or reliable transfer between interrupt and task contexts, use the appropriate ISR-safe FreeRTOS synchronization mechanism for the project. Do not assume that declaring a shared variable volatile alone makes all concurrent access safe.

Also decide where the LED state changes. If the ISR toggles the output and the task only logs, one press can still produce multiple toggles from bounce. A cleaner path is for the ISR to signal an event and for the task to apply one accepted press after debounce, update the LED, and log the resulting state. Check by verifying that removing diagnostic output does not change the intended event behavior and that output happens outside the ISR.

How should the button be debounced?

A mechanical button does not necessarily make one clean transition per press. Its contacts can bounce between open and closed, creating several edges from a single physical action. With the present edge interrupt, each qualifying rising edge toggles led; a burst of an even number of edges can appear as no change, while an odd number can appear as one change.

  1. First give the input a stable idle state and select the edge matching the wired press transition.
  2. In the interrupt path, record or signal that an edge occurred; avoid formatting or lengthy processing there.
  3. In task context, accept one press only after the input has remained in the pressed state for a debounce interval selected for the switch and application. Read the actual input state again rather than treating every edge as a press.
  4. Re-arm press detection only after the button returns to its released state, applying the corresponding release handling for the selected pull and edge strategy.

The evidence does not specify a switch model or timing, so select the interval from measurement and the application's response requirement instead of copying an arbitrary delay. Check with repeated deliberate presses and releases: each action should register once, with no idle-state events.

How do you confirm the GPIO output path?

The program resets GPIO2 and sets it to output, while the global led value starts at zero. That confirms the intended software path only if the selected pin is connected to the LED and its active polarity matches the assumed logic. Some board LEDs are active-low, and a board may assign its LED elsewhere; check its schematic or measure the pin rather than assuming GPIO2 drives the LED.

Test the output independently of the button by setting GPIO2 high and low from task context or a temporary controlled test. If the pin voltage changes but the LED does not, inspect the LED path and polarity. If the voltage does not change, inspect the pin selection, output setup, GPIO call return values, and whether another board function uses that pin.

Check this stage by confirming that the physical LED state corresponds to each commanded logic level. Then restore the event-driven update and confirm that the software state and visible output agree after each accepted press.

What proves the complete button-to-LED path is fixed?

After the electrical level, pin selection, ISR behavior, debounce, and output path are checked independently, test them together. A successful test requires more than seeing a log line: the input must idle steadily, one physical press must create one accepted event, and the output must reach the expected state.

  1. With the button released, observe GPIO0 and confirm it stays at the selected idle level without repeated events.
  2. Press and release the button repeatedly; confirm one accepted press per action and no toggles caused by bounce.
  3. Confirm GPIO2 changes once per accepted press and the LED follows the board's actual active polarity.
  4. Repeat through reset with the button released, and check startup behavior if GPIO0 is used on this board.
  5. Keep diagnostic logging in task context and confirm that enabling it does not reintroduce missed or extra button events.

What happens if the LED still does not respond after debouncing?

What happens if the LED still does not respond after debouncing?

Test GPIO2 independently and measure its voltage. A changing pin with a dark LED points to LED wiring or active polarity; an unchanged pin points back to output setup, pin selection, or a failed GPIO call.

What happens if GPIO0 still generates events with the button released?

Measure the idle level and inspect the pull resistor and wiring. A floating or noisy input still needs a defined bias; also verify that the selected edge matches the press transition.

What happens if the program works after removing printf?

Keep formatted output out of the ISR. Signal a task from the interrupt and print from task context, because formatting and a blocking UART can extend interrupt handling.

What should I verify before calling the ESP32 button fix complete?

Run repeated press/release cycles and confirm a stable idle input, one accepted event per press, one GPIO2 transition per event, correct LED polarity, and normal reset behavior with the button released.

Back to blog