GPIO Inputs
Floating, Pull-ups, Debouncing
Why it matters
A real button press should be one event. Without debouncing you get phantom presses, double-clicks, and flaky menus — even with perfect code everywhere else.
The idea
Goal
Turn a noisy mechanical switch into a clean digital signal you can trust.
What’s actually happening
A switch is two pieces of metal touching. When they first touch, they bounce for a few milliseconds: HIGH/LOW/HIGH/LOW… then finally settle. Your CPU is fast enough to see all those transitions.
flowchart LR
Raw[RawGPIO] --> Debounce[WaitForStability]
Debounce --> Clean[CleanState]
Floating Inputs
An unconnected GPIO input is floating — it picks up noise and randomly flips between HIGH and LOW.
- Pull-up: Resistor to VCC (makes input HIGH by default)
- Pull-down: Resistor to GND (makes input LOW by default)
- ESP32 has internal pull-ups you can enable in software
ESP‑WROOM‑32 wiring note
Use a button-to-GND wiring and enable an internal pull-up:
- GPIO → Button → GND
- Enable internal pull-up in software
Avoid using a strapping pin (e.g. GPIO0/2/12/15) for a button unless you know why. Also note: GPIO34–39 are input-only and typically require an external pull-up/down.
Mini-lab
Try a debounce window of 20–50ms for common tactile switches. If you still see false triggers, increase it. If the button feels “laggy”, decrease it.
Demo
The top timeline is the raw GPIO signal (with bounce). The bottom is the debounced output.
Use:
- Bounce Severity: how “messy” the button is
- Sample Rate: how often we check the pin
- Debounce Window: how long the signal must stay stable before we accept a change
Key takeaways
- Mechanical switches bounce for a few milliseconds
- A floating input will randomly flip — use pull-ups/pull-downs
- Debouncing is a small state machine: detect change → wait for stability → accept
- On ESP32, some pins are input-only (34–39) and some are strapping pins (boot-sensitive)
Going deeper
Hardware debounce (RC + Schmitt trigger) can reduce CPU work and improve EMI robustness. For event-driven code, interrupts still need debouncing — either by masking interrupts for a window, or by using a timer task to confirm stability.
Math details
Definitions:
sample_rate = N samples / second
debounce_window = W seconds
Rule of thumb:
W should be several times the worst-case bounce duration.
Simple debounce state machine:
if raw != pending:
pending = raw
stable_time = 0
else:
stable_time += dt
if stable_time >= W:
debounced = pending
Implementation
LLM Prompt: GPIO Debounce
Write Rust code for ESP32 GPIO debouncing using esp-hal.
Input: GPIO pin configured as input with pull-up.
Implement state machine: detect change → wait for stability → accept.
Include configurable debounce window (default 50ms). Handle both
polling and interrupt-driven modes.
Lab Exercise
- Connect button: GPIO → Button → GND
- Enable internal pull-up in code
- Implement debounce state machine
- Test with different debounce windows (20ms, 50ms, 100ms)
- Observe: too short = false triggers, too long = laggy feel