Microcontrollers · #8 of 52

GPIO Inputs

Floating Pins, Pull Resistors, and Buttons You Can Trust

An ESP32 development board on a white background.
A GPIO pin looks like a software boolean only after the hardware has made a clean electrical promise. · Billie Grace Ward, CC BY 2.0

The first button on the environmental monitor feels innocent. Press it once and the display should wake. Instead the menu advances three screens, then sometimes wakes by itself overnight. The firmware prints pressed, released, pressed faster than any human finger could move.

The bug is not "the button is bad" and it is not "the CPU is confused." The pin is doing exactly what the outside world told it to do: it floated when nobody drove it, then it saw every little metal bounce when the switch contacts touched.

A GPIO input is the border between a messy physical object and a clean digital state. Your program wants true or false. The pin receives voltage, noise, leakage, contact bounce, cable pickup, ESD events, slow edges and sometimes the wrong boot strap. Good GPIO design is the craft of turning that mess into one reliable event.

By the end, you can

  1. Explain why a floating GPIO input can randomly read HIGH or LOW
  2. Choose pull-up and pull-down wiring for a switch, and compute the pull current
  3. Relate logic thresholds to real input voltage instead of treating HIGH/LOW as magic
  4. Explain mechanical switch bounce and why a fast CPU can see several transitions from one press
  5. Implement debounce as a small state machine with a measured stability window
  6. Avoid ESP32 pin traps: strapping pins, input-only pins, ADC2/Wi-Fi conflicts and weak internal pulls

A pin is a voltage judge

Inside the ESP32 module, a GPIO input is a transistor gate connected to a digital input buffer. The buffer does not know whether the voltage came from a button, a sensor, a wire, static charge or your finger. It only compares the pin voltage against internal thresholds and reports a logic state.

Close-up of an Espressif ESP-WROOM-32 module.
The module hides the input buffers, pull devices, boot straps and radio. The pins on the edge are not abstract variables; they are tiny analog nodes. · Brian Krent, CC BY-SA 4.0

For a 3.3 V logic family, a low voltage near ground is read as LOW and a high voltage near 3.3 V is read as HIGH. Between them is a forbidden gray region. Different chips publish different exact thresholds, but the engineering habit is the same: drive the pin decisively below the low threshold or above the high threshold. Do not design a button or sensor that parks near the middle and hope software can guess correctly.

The input itself draws almost no DC current. That sounds convenient, but it creates the first trap: if nothing drives the pin, the pin has no reason to be either high or low. It is a tiny capacitor connected to a high-impedance amplifier. It can hold old charge, pick up mains hum, respond to nearby fingers, and drift across thresholds. That is a floating input.

Give every input a default

The cure is simple: give the pin a weak, intentional default path. A pull-up resistor connects the input to 3.3 V, so the pin reads HIGH when nobody else drives it. A pull-down resistor connects it to ground, so the pin reads LOW by default.

The common ESP32 button wiring is button-to-ground with a pull-up:

  1. GPIO pin has a pull-up to 3.3 V.
  2. One side of the button connects to the GPIO pin.
  3. The other side of the button connects to ground.
  4. Not pressed means HIGH. Pressed means LOW.

That inverted logic is not a mistake. It is robust, it works with the ESP32's internal pull-ups, and it fails more predictably than a floating input.

A breadboard button circuit with a resistor used as a pull-down.
A pull resistor gives a button node a defined idle state. This photo uses pull-down wiring; button-to-ground with pull-up is the same idea inverted. · syvwlch, CC BY 2.0

Pull resistors are weak on purpose. If a 10 kΩ pull-up is connected to 3.3 V and the button shorts the pin to ground, the button current is

I=3.3 V10,000 Ω=0.33 mAI = \frac{3.3\ \text{V}}{10{,}000\ \Omega} = 0.33\ \text{mA}

That is enough to define the input, but small enough not to waste much power. A much larger pull, like 100 kΩ, wastes less current but makes the node easier to disturb and slower to settle when capacitance is present. A much smaller pull, like 1 kΩ, makes a stiffer node but wastes more current whenever the button is pressed.

Female-to-female jumper wires used for electronics prototyping.
Jumper wires are convenient antennas. Long loose leads make floating inputs and weak pulls easier to disturb. · Limor, CC BY 2.0

A button connects a GPIO pin to ground when pressed. The pin also has a 10 kΩ pull-up to 3.3 V. What does the pin read when the button is not pressed?

A button is not one perfect edge

A mechanical switch is two pieces of metal touching. When they first meet, they do not merge into one ideal conductor. They bounce, scrape and settle. The electrical node can flip HIGH and LOW several times before the contact finally rests.

Several small tactile pushbutton switches.
A tactile switch feels crisp to your finger, but the electrical contact can chatter for milliseconds. · walknboston, CC BY 2.0
A tactile switch mounted on a printed circuit board.
The switch is part of a circuit board, not just a UI object. Copper, ground reference, pull strength and nearby noise all shape what the MCU sees. · Raimond Spekking, CC BY-SA 4.0

Your finger may press once in 100 ms. The CPU can sample thousands or millions of times inside the first 5 ms. If your firmware increments a counter on every edge, one physical press can become five software events.

An oscilloscope display showing switch bounce after a transition.
Switch bounce is visible on a scope: one human action creates several electrical crossings before the node settles. · Tomoldbury, Public domain

This is why interrupts alone do not fix buttons. An interrupt makes the CPU notice an edge quickly. It does not prove the edge was the final edge. If bounce creates five edges, an interrupt can faithfully interrupt you five times.

Debounce is a state machine

Debounce means "accept a state change only after the raw signal has stayed stable long enough." It is not a delay sprinkled after a read. It is a small state machine:

  1. Keep the last accepted state.
  2. Watch the raw input.
  3. When raw input changes, mark it as pending and start a timer.
  4. If the raw input changes again before the timer expires, restart the timer.
  5. If it stays unchanged for the debounce window, accept it.

The debounce window is an engineering choice. For many tactile switches, 10 ms to 50 ms is a reasonable first test. Too short accepts bounce. Too long makes the UI feel laggy. The right value comes from the actual switch, wiring, pull strength and product feel.

Try these moves:

  1. Raise bounce severity and shorten the debounce window. Watch false raw chatter leak into accepted changes.
  2. Increase the debounce window to 50 ms. The output becomes steadier, but it responds later.
  3. Lower the sample rate. Notice that sampling is part of the design; a slow loop can miss short events.
  4. Imagine this is an interrupt handler. The raw edge still bounces, so the handler still needs a debounce policy.

Why can one physical button press create several software events?

Hardware debounce and Schmitt inputs

Software debounce is often enough for user buttons. Hardware debounce becomes useful when the line is noisy, the input is safety-related, the CPU sleeps deeply, or the edge must be clean before firmware starts running. The usual hardware move is an RC network plus a Schmitt trigger input.

The RC network slows the voltage change. The Schmitt trigger adds hysteresis: one threshold for rising edges and a different threshold for falling edges. That means a little noise around the middle does not repeatedly flip the output.

Otto Schmitt · 1913-1998 Invented the Schmitt trigger, a threshold circuit with hysteresis. It turns slow or noisy analog edges into clean digital transitions.

This matters because ordinary digital input buffers dislike slow edges. If a pin creeps through the threshold region, noise can make the buffer chatter. A Schmitt input is more tolerant because the decision point depends on direction. Many microcontrollers provide Schmitt behavior on some input pins, but you should verify the datasheet rather than assuming every pin has it.

A USB logic analyzer used for digital signal debugging.
A logic analyzer is the right instrument when the question is sequence and timing: how many edges happened, in what order, and how far apart? · Myself248, CC BY-SA 2.0

The scope shows voltage. The logic analyzer shows digital interpretation over time. For button work, both can be useful: the scope reveals bounce amplitude and edge shape, while the logic analyzer reveals what your firmware would count.

ESP32 pin choices are not interchangeable

The ESP32 lets many peripherals route to many pins, but that does not mean all pins are equally safe for buttons.

Strapping pins affect boot mode. GPIO0, GPIO2, GPIO12 and GPIO15 are classic pins to treat carefully. A button or external circuit that holds a strapping pin in the wrong state during reset can make the board boot into the wrong mode or fail to boot.

GPIO34 to GPIO39 are input-only. That is useful for sensing, but those pins do not have the same internal pull-up and pull-down options as the general pins. If you use them for a button, expect to add an external pull resistor.

ADC2 conflicts with Wi-Fi on many ESP32 workflows. If a pin is also an ADC2 input and your firmware uses Wi-Fi, analog reads may be unavailable or unreliable while the radio is active. For simple digital buttons that may not matter, but for sensor inputs it matters a lot.

Internal pulls are weak. ESP32 internal pull-ups and pull-downs are convenient for short local buttons, but they are not a shield against long cables, wet connectors or noisy enclosures. External 4.7 kΩ to 10 kΩ pulls are common when the input must be physically robust.

A bench checklist for one reliable button

Start with hardware:

  1. Pick a non-strapping GPIO unless you have a reason not to.
  2. Wire the button to ground and use a pull-up.
  3. Keep the wire short. If it leaves the board, add protection and a stronger external pull.
  4. Measure the idle and pressed voltage at the pin, not just at the button.
  5. Look at the edge with a scope or logic analyzer if the behavior is surprising.

Then write firmware:

  1. Convert the raw active-low read into a named logical state.
  2. Debounce state changes with a measured window.
  3. Generate events only when the debounced state changes.
  4. Separate "button is held" from "button was just pressed."
  5. Test rapid taps, long holds, power-up with the button held and sleep/wake behavior.
Interrupt debounce without lying to yourself

Interrupts are useful when the CPU sleeps or the event must be noticed quickly, but they do not eliminate bounce. A solid interrupt design usually does one of three things: disable further interrupts for a short debounce window and confirm the pin later; wake a timer task that samples until stable; or use hardware filtering before the interrupt pin.

The trap is doing real application work in the first interrupt. The first edge may be the first bounce, not the real press. Treat the interrupt as "something changed, start verification," not "the button event is proven." That habit also scales to reed switches, limit switches, encoder contacts and wake pins.

Practice 1 warm-up

A button uses a 4.7 kΩ pull-up to 3.3 V and shorts the pin to ground when pressed. How much current flows through the pull-up while the button is held?

Show worked solution

Use Ohm's law:

I=3.3 V4700 Ω0.00070 AI = \frac{3.3\ \text{V}}{4700\ \Omega} \approx 0.00070\ \text{A}

That is about 0.70 mA while pressed. It is small for a bench project, but it can matter in a battery product if the button may be held for long periods.

Practice 2 core

A raw button input changes from HIGH to LOW, then bounces HIGH/LOW for 8 ms, then stays LOW. Your debounce window is 20 ms. When should firmware report "pressed"?

Show worked solution

The firmware should not report the press on the first falling edge. It should mark LOW as pending, restart the timer each time bounce changes the raw state, then accept LOW only after the raw input has remained LOW for 20 ms.

Since the last bounce ends at about 8 ms, the accepted press happens around 28 ms after the first edge. That is the price of proving the state is stable.

Practice 3 stretch

You need a wake button on an ESP32 product. The button is on a small daughterboard connected by a 20 cm cable. Name three design choices that make the input more reliable.

Show worked solution

Good choices include:

  1. Avoid boot strapping pins so the button cannot change reset behavior.
  2. Use a real external pull-up or pull-down near the MCU, not only a weak internal pull.
  3. Add RC filtering or a Schmitt-trigger buffer if the cable is noisy or the wake edge is slow.
  4. Add ESD protection if the button is user-accessible.
  5. Route the signal with a nearby ground return so the cable is not a big loop antenna.
  6. Debounce after wake before treating the first edge as a user command.

The important point is that the cable makes this no longer a tiny local bench button. The input needs an electrical default, noise control and a firmware verification step.

Key takeaways

  • A GPIO input is a voltage judge, not a magic boolean.
  • A floating input has no defined state. Every input needs an intentional driver, pull-up or pull-down.
  • Pull current is ordinary Ohm's law: I=V/RI = V/R.
  • Mechanical switches bounce. One press can produce several threshold crossings.
  • Debounce is a state machine: pending state, stability timer, accepted state.
  • On ESP32, pin choice matters. Avoid strapping traps, know input-only pins, and do not assume internal pulls are strong enough.

A good button is humble engineering. One resistor gives the pin a default. One state machine gives the signal time to prove itself. One careful pin choice keeps reset from becoming a mystery. Only after those details are handled does the firmware get the simple thing it wanted all along: one human action, one trusted event.

full glossary →