prerequisite

Digital logic and pull-up resistors

How a pin decides between 0 and 1, why an unconnected input floats, and how pull-up and pull-down resistors fix it.

Before this

This page assumes you are comfortable with:

Why you need this

A button wired to an ESP32 pin seems like it should just work, and often it reads random 0s and 1s instead. The cause is almost always an input with nothing holding it at a voltage. This page explains how a pin turns a voltage into a bit, why a pin left alone "floats", and how one resistor, often one already built into the chip, makes the reading solid.

The idea

High and low are ranges

A digital pin deals in two values: 1 (high) and 0 (low). The chip decides which by measuring the pin's voltage against two thresholds. Espressif's ESP32 Series Datasheet (version 5.3, Table 5-3, "DC Characteristics") gives these for the classic ESP32, where VDDV_{DD} is the pin's supply voltage, 3.3 V on every board here:

Symbol Meaning Rule At VDDV_{DD} = 3.3 V
VIHV_{IH} lowest voltage guaranteed to read 1 at least 0.75×VDD0.75 \times V_{DD} 2.475 V
VILV_{IL} highest voltage guaranteed to read 0 at most 0.25×VDD0.25 \times V_{DD} 0.825 V
highest voltage allowed on an input VDD+0.3V_{DD} + 0.3 3.6 V

So 0 V to 0.825 V reads 0, and 2.475 V to 3.3 V reads 1. Between 0.825 V and 2.475 V the datasheet promises nothing: the chip may read either value, and may change its mind from one read to the next. Well-designed circuits never leave a pin there. The ESP32-S3, C6, and P4 have their own datasheets; check yours before relying on a number.

Outputs and inputs

A pin set as an output drives its own voltage: near 3.3 V for a 1, near 0 V for a 0. A pin set as an input drives nothing. It only measures, and it draws almost no current: the same datasheet table lists at most 50 nA (50 billionths of an ampere) flowing into an input.

Floating inputs

That tiny current is the problem. An input connected to nothing has no path to 3.3 V or to GND, so nothing sets its voltage. Stray charge from your hand or a nearby wire decides it. The pin is floating, and it reads random values.

A button does not fix this on its own. A button from the pin to GND makes the pin 0 V when pressed. When released, the button connects the pin to nothing, and it floats again.

Pull-up and pull-down resistors

A pull-up resistor connects the pin to 3.3 V through a resistance, such as 10 kΩ. With nothing else connected, almost no current flows through it, so by Ohm's law (V=IRV = IR, the voltage across a resistor equals current times resistance) almost no voltage is lost across it, and the pin sits at 3.3 V: a solid 1. Press a button that connects the pin to GND, and the pin goes to 0 V: a solid 0. The resistor's job is to lose the argument when the button speaks and win it otherwise.

A pull-down resistor is the mirror image: pin to GND through a resistor, button to 3.3 V. Released reads 0, pressed reads 1.

Active low

With a pull-up and a button to GND, pressed reads 0. That is called active low: the "on" state is the low voltage. It is the most common way buttons are wired, because GND is available everywhere on a board.

The chip's own resistors

Most ESP32 pins have a pull-up and a pull-down built in that code can switch on. The datasheet table lists both at 45 kΩ typical on the classic ESP32. They are called weak because 45 kΩ holds the pin less firmly than an external 10 kΩ, which matters with long wires near noise. Some pins have none: the author's WROOM-32 DevKit notes mark GPIO 34, 35, 36, and 39 as input-only with no internal pull-up or pull-down.

Switch bounce

A mechanical button does not close cleanly. The metal contacts touch, spring apart, and touch again for a short time, so one press can read as several 0-to-1 changes. This is bounce. The fix is to ignore changes for a short time after the first one, in software or with a small capacitor; GPIO: buttons and LEDs shows the code.

Set the wiring to "No resistor" and leave the button released: the strip of reads fills with random values, one every 100 ms. Switch to pull-up and the strip goes solid; press and it drops to 0. Pull-down flips the meaning.

Worked example

A button from a GPIO pin to GND, with a 10 kΩ pull-up from the pin to 3.3 V.

Released. The only current is the input's leakage, at most 50 nA. The voltage lost across the resistor is V=IR=0.00000005×10,000=0.0005V = IR = 0.00000005 \times 10{,}000 = 0.0005 V, half a millivolt. The pin sits at about 3.2995 V, far above the 2.475 V needed for a 1. It reads 1.

Pressed. The button ties the pin directly to GND, so the pin is at 0 V, below 0.825 V. It reads 0. The whole 3.3 V is now across the resistor:

I=3.310,000=0.33 mAI = \frac{3.3}{10{,}000} = 0.33 \text{ mA}

State Pin voltage Reads Current through the pull-up
released about 3.3 V 1 at most 50 nA
pressed 0 V 0 0.33 mA

With the chip's internal 45 kΩ pull-up instead, released is still about 3.3 V (the leakage loses at most 0.00000005×45,000≈20.00000005 \times 45{,}000 \approx 2 mV), and pressed draws 3.3/45,000≈733.3 / 45{,}000 \approx 73 µA (microamperes, millionths of an ampere). The weaker resistor wastes less current while pressed and holds the pin less firmly while released.

No resistor at all. Pressed is still 0 V. Released, nothing sets the voltage, and the reading is whatever the pin happens to hold. That is the demo's random strip.

In an ESP32 project

Boards often wire the pull-up for you. The CYD's touch controller signals a touch on GPIO 36, a pin with no internal pull-up, so the board carries an external one. This MicroPython excerpt is from CYD_Riprap, which has run on real hardware; the pin is set as a plain input and the code treats 1 as "not touched":

touch_irq = Pin(36, Pin.IN)
...
    if touch_irq.value():
        return None

When a pin needs the chip's internal pull-up, you ask for it when you configure the pin. This C with ESP-IDF excerpt from ESP_32_Keyboard's USB host firmware, which has run on real hardware, sets up the BOOT button on GPIO 0 as an input with the pull-up on, and asks for an interrupt on the falling edge, the moment the pin goes from 1 to 0 as the button is pressed:

    const gpio_config_t input_pin = {
        .pin_bit_mask = BIT64(APP_QUIT_PIN),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };

Some pins matter at power-on. The author's notes list GPIO 0, 2, 5, 12, and 15 on the classic ESP32 as strapping pins: the chip reads them once at reset to choose how to boot. GPIO 0 low at reset enters download mode instead of running your program, which is exactly what the BOOT button does. A pull-down on the wrong strapping pin can stop a board from starting.

Common mistakes

  • No pull resistor on a button. Symptom: random presses, especially when you touch the board or a cable.
  • Expecting pressed to read 1. With a pull-up, pressed reads 0. Symptom: the program acts on release instead of press.
  • Internal pull-up requested on GPIO 34 to 39. Those pins have none. Symptom: the pin floats even though the code asked for a pull-up.
  • Pull-up and pull-down both on. They form a divider and park the pin near the middle, in the undefined band. Symptom: unreliable reads.
  • A pull resistor on a strapping pin. Symptom: the board boots into download mode or will not start until you unplug the button.
  • Ignoring bounce. Symptom: one press counts as two or three.

Cost

A pull-up costs one cheap resistor, or nothing if the chip's internal one is enough. Its energy cost is only while pressed: 0.33 mA with 10 kΩ, about 73 µA with the internal 45 kΩ. For a button pressed now and then that is negligible; for a contact held closed all day on a battery, pick the larger resistance. A floating pin costs more in debugging time than the resistor ever would, because the symptom looks like a software bug.

Going further

  • Open-drain outputs, which can only pull low and rely on a pull-up, the way I2C and the CYD's touch signal work.
  • Schmitt-trigger inputs and hysteresis, which make a pin resist chatter near the threshold.
  • GPIO: buttons and LEDs, for reading buttons in all four languages.
  • Serial communication basics, where high and low carry whole bytes.

Leads to

Back to ESP32 development: assembly, C, MicroPython, and CircuitPython