technique
GPIO: buttons and LEDs
Driving outputs and reading inputs on ESP32 pins in all four languages, with pull-ups, debouncing, interrupts, and the pins you must not use.
Before this
This page assumes you are comfortable with:
- prerequisiteDigital logic and pull-up resistorsHow a pin decides between 0 and 1, why an unconnected input floats, and how pull-up and pull-down resistors fix it.
- prerequisitePolling and interruptsTwo ways a program notices that something happened: asking over and over, or being interrupted when it does.
- techniqueChoosing a languageMicroPython, CircuitPython, C with ESP-IDF, or assembly: what each is good at, what it costs, and the same uppercase echo in all four.
Why you need this
A button and an LED are the smallest useful gadget: one input, one output. Every bigger project still uses them, for a status light, a reset-to-setup button, or a screen's backlight. This page is stage 4 of the pipeline on the hub, "Talk to hardware". It shows the same two jobs in MicroPython, CircuitPython, C with ESP-IDF, and assembly, and it lists the pins that will stop your board from booting if you wire them carelessly.
The idea
GPIO means general-purpose input/output: a pin your program controls. Each pin is in one of two modes at a time.
- Output: your program sets the pin to 1 (3.3 V) or 0 (0 V). An LED with a resistor on the pin lights when current flows.
- Input: your program reads whether the voltage on the pin is high (1) or low (0).
An input with nothing driving it floats: it picks up noise and reads random values. Digital logic and pull-up resistors explains the fix: a pull-up resistor to 3.3 V holds the pin at 1 until a button connects it to ground, which makes it 0. That wiring is active low: pressed reads 0. Most ESP32 pins have an internal pull-up you can switch on in code, so a button needs no extra parts.
Outputs can be active low too. On the CYD (ESP32-2432S028), the author's pinout says the RGB LED's red, green, and blue sit on GPIO 4, GPIO 16, and GPIO 17, and each turns on when the pin is 0. The backlight on GPIO 21 is the opposite: active high, so 1 turns the screen's light on.
Bounce
A mechanical button does not close cleanly. Its contacts bounce for a short time, so one press can read as 0, 1, 0, 1, 0 before settling. Code that counts presses counts several. Debouncing in software means accepting a new value only after it has stayed the same for a while, a settle window you choose to be longer than the bounce you see.
Edges and interrupts
An edge is the moment a pin changes: a falling edge goes 1 to 0 (an active-low press), a rising edge goes 0 to 1. Instead of reading the pin in a loop (polling), you can ask the chip to run a short function, an interrupt handler, on an edge. Polling and interrupts covers the trade. The rule for handlers: set a flag and return; do the real work in the main loop.
Pins you must not use
These come from the author's board notes and pinouts for the classic ESP32.
| Pins | Why |
|---|---|
| GPIO 6 to 11 | wired to the module's flash chip; "do not use" |
| GPIO 0, 2, 5, 12, 15 | strapping pins: read at reset to pick the boot mode. GPIO 0 low at reset enters flash mode; GPIO 12 high at reset selects 1.8 V flash and the board will not boot |
| GPIO 34, 35, 36, 39 | input only, and no internal pull-ups or pull-downs |
| GPIO 1 and 3 | UART0, the serial console |
A strapping pin is one the chip samples during reset to decide how to start. A button or LED circuit that holds one at the wrong level at power-on changes how the board boots. Other chips differ: the author's P4 notes list strapping pins 34 to 38, so check the board's own notes every time.
Worked example
The task: an LED that lights while a button is held, on a WROOM-32 DevKit. From the author's DevKit pinout, GPIO 32 is a plain pin (not strapping, not input-only, not flash) for the button, and GPIO 26 is a plain pin for the LED.
The LED resistor. From Voltage, current, and Ohm's law: with a 2.0 V red LED at 10 mA from 3.3 V,
Round up to the next common value, 150 Ω: the current becomes mA, and the resistor dissipates mW.
The button. One side to GPIO 32, the other to GND, internal pull-up on. Released, the pin reads 1. Pressed, it reads 0. Espressif's ESP32 datasheet gives the internal pull-up as about 45 kΩ, so pressing draws about mA. (With an external 10 kΩ pull-up instead, pressing would draw mA.)
The debounce. Read the pin every 5 ms and accept a new value after 4 identical readings in a row, so a press must be steady for ms to count:
| Time (ms) | Raw read | Same-in-a-row count | Accepted value |
|---|---|---|---|
| 0 | 1 | 4 | 1 (released) |
| 5 | 0 | 1 | 1 |
| 10 | 1 | 1 | 1 (a bounce) |
| 15 | 0 | 1 | 1 |
| 20 | 0 | 2 | 1 |
| 25 | 0 | 3 | 1 |
| 30 | 0 | 4 | 0 (pressed) |
MicroPython
The author's simplest output is ESP_32_DAD's main.py, excerpted here (author's project, hardware-proven on a CYD). It blinks the CYD's active-low red LED.
from machine import Pin
import time
led = Pin(4, Pin.OUT, value=1) # GPIO 4 = red LED, start OFF (active low)
while True:
led.value(0) # ON
time.sleep_ms(500)
led.value(1) # OFF
time.sleep_ms(500)
None of the author's projects reads a plain button, so this button-and-LED version was written for this page (not hardware-tested). It debounces as in the table and also counts falling edges with an interrupt whose handler only bumps a counter.
from machine import Pin
import time
led = Pin(26, Pin.OUT, value=0)
btn = Pin(32, Pin.IN, Pin.PULL_UP)
presses = 0
def on_fall(pin):
global presses
presses += 1 # raw edges, bounces included
btn.irq(trigger=Pin.IRQ_FALLING, handler=on_fall)
stable, count, last = 1, 0, 1
while True:
v = btn.value()
count = count + 1 if v == last else 1
last = v
if count >= 4 and v != stable:
stable = v
led.value(1 if stable == 0 else 0) # pressed = 0 = LED on
time.sleep_ms(5)
Keep led and btn in named variables for the whole program. The author's notes warn that MicroPython can garbage collect a Pin object nobody holds, which is how a CYD backlight turns off at random.
CircuitPython
Written for this page (not hardware-tested), for an Adafruit Feather ESP32-S3, whose boot button is on GPIO 0 and red LED on GPIO 13 per the author's board notes:
import digitalio, microcontroller, time
led = digitalio.DigitalInOut(microcontroller.pin.GPIO13)
led.direction = digitalio.Direction.OUTPUT
btn = digitalio.DigitalInOut(microcontroller.pin.GPIO0)
btn.switch_to_input(pull=digitalio.Pull.UP)
while True:
led.value = not btn.value # pressed reads False
time.sleep(0.005)
C with ESP-IDF
Written for this page (not hardware-tested), using ESP-IDF's GPIO driver on the DevKit pins:
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void app_main(void) {
gpio_set_direction(GPIO_NUM_26, GPIO_MODE_OUTPUT);
gpio_set_direction(GPIO_NUM_32, GPIO_MODE_INPUT);
gpio_set_pull_mode(GPIO_NUM_32, GPIO_PULLUP_ONLY);
while (1) {
gpio_set_level(GPIO_NUM_26, gpio_get_level(GPIO_NUM_32) == 0);
vTaskDelay(pdMS_TO_TICKS(5));
}
}
Assembly
This listing is Xtensa LX6 assembly for the classic ESP32, excerpted from ESP_32_DAD's blink_asm, an assembly blink written for the CYD (author's project; the project's notes record its MicroPython blink running on the board, not this file specifically). It drives GPIO registers directly.
# GPIO registers (memory-mapped, ESP32 Technical Reference Manual):
# 0x3FF44024 GPIO_ENABLE_W1TS_REG - write 1s to enable GPIO output
# 0x3FF44008 GPIO_OUT_W1TS_REG - write 1s to drive pin HIGH
# 0x3FF4400C GPIO_OUT_W1TC_REG - write 1s to drive pin LOW
app_main:
entry sp, 32 # Xtensa function prologue, 32-byte frame
movi a2, 0x3FF44024 # GPIO_ENABLE_W1TS_REG
movi a3, 0x10 # bit 4 = GPIO4
s32i a3, a2, 0 # enable GPIO4 output direction
blink_loop:
movi a2, 0x3FF4400C # GPIO_OUT_W1TC_REG (clear = drive LOW)
s32i a3, a2, 0
movi a4, 40000000
delay_on:
addi a4, a4, -1
bnez a4, delay_on
...
W1TS means "write 1 to set" and W1TC "write 1 to clear": writing the mask 0x10 (, bit 4) changes only GPIO 4 and leaves every other pin alone. The delay is a counted loop: the file's comments estimate about 3 cycles per pass at 240 MHz, so 500 ms is cycles, or passes.
In an ESP32 project
Every one of the author's CYD projects starts by setting GPIO outputs before anything else: TeachingMachineCYD sets the backlight high and parks the RGB LED pins high (off, since they are active low). On the CYD the BOOT button is GPIO 0, active low, so it doubles as a user button once the board is running, as long as nothing holds it low at reset. The language changes the syntax, not the steps: pick the mode, set the pull, then read or write.
Common mistakes
- Floating input. Symptom: the LED flickers with no one touching the button. Turn on the pull-up.
- Button on a strapping pin. Symptom: the board enters flash mode or will not boot when powered with the button held, or with a circuit pulling the pin. Move it to a plain pin.
- Pull-up on GPIO 34 to 39. Symptom: the input still floats. These pins have no internal pulls; add a 10 kΩ resistor.
- Active-low confusion. Symptom: the CYD's LED is on when you expect it off. Writing 0 turns it on.
- Pin object collected. Symptom: in MicroPython, an LED or backlight turns off at a random time. Hold the Pin in a variable.
- No debounce. Symptom: one press counts as two or three.
Cost
GPIO costs almost nothing in memory: one pin object in Python, a few instructions in C or assembly. Speed differs by language. The assembly writes a register in one store instruction; MicroPython's led.value() goes through the interpreter and is far slower, which only matters for very fast signals. The polling debounce wakes every 5 ms; an interrupt lets the CPU idle between presses, which saves power on a battery. The busy-wait delay in the assembly blink keeps the CPU at full power doing nothing for the whole half second. Edit-to-run time is seconds in MicroPython and CircuitPython and a build-and-flash cycle in C and assembly.
Going further
- Digital logic and pull-up resistors, for the voltages behind the 0 and 1.
- Polling and interrupts, for what an interrupt handler may and may not do.
- Xtensa assembly on the ESP32 and ESP32-S3, for
entry,movi, ands32i. - The GPIO chapter of the ESP32 Technical Reference Manual, for the W1TS and W1TC registers.
- PWM ("LEDC" on the ESP32) for dimming an LED instead of switching it.
Back to ESP32 development: assembly, C, MicroPython, and CircuitPython