Skip to content
Tutorial
10 min read
October 17, 2026

MicroPython GPIO on ESP32: Blink an LED and Read a Button

Learn machine.Pin from scratch: outputs, inputs, internal pull-ups, active-high vs active-low wiring and software debouncing, with a simple LED-and-button circuit you can build in minutes.

MicroPython GPIO on ESP32: Blink an LED and Read a Button

GPIO—general-purpose input/output—is the foundation of many ESP32 projects. A pin can read a digital input such as a pushbutton or drive a low-current output such as an indicator LED. With MicroPython, you can test these behaviours interactively and build toward sensors, relays, displays, and automation.

This guide walks through safe wiring, output configuration, LED blinking, button input with pull resistors, debouncing, and troubleshooting. ESP32 boards differ in their exposed pins and onboard circuits, so treat every GPIO number as an example until you verify the pinout for your exact board. ESP32 GPIO is generally 3.3 V logic and is not 5 V tolerant.

1. What GPIO means

A GPIO can often be configured as an input or output. An output drives a logic level; an input samples the logic level presented to the pin. Some pins have additional functions, such as ADC, SPI, I²C, UART, touch, or boot-strapping. Not every pin supports every function, and some pins are reserved for flash or connected to onboard hardware.

A HIGH output is near the board's logic supply and a LOW output is near ground, but neither state means a pin can supply unlimited current. GPIO pins are intended for logic signals and modest loads. Use a resistor with an LED, and use a suitable transistor, MOSFET, or driver module for motors, relay coils, strips, lamps, and other loads.

Before wiring, identify the module and consult the board schematic or official pinout. Physical header pin numbers are not GPIO numbers. Never assume that GPIO 2, 4, 5, or 15 has the same availability on every ESP32 variant or development board.

2. Parts and safe wiring

For the LED exercise, use an ESP32 board, a breadboard, an LED, a 220–1,000 Ω current-limiting resistor, jumper wires, and a USB data cable. With power disconnected, connect a verified output-capable GPIO through the resistor to the LED anode; connect the LED cathode to GND. The resistor may be placed on either side of the LED as long as it is in series. If your LED module already includes a resistor, check its documentation before adding another.

For a button, use a momentary pushbutton and connect it to a suitable input pin. The input must have a defined idle level; otherwise it can float and change unpredictably. The simplest method is to use the ESP32's internal pull-up and connect the button between the GPIO and GND. When released, the input reads HIGH; when pressed, it reads LOW. This is called active-low logic.

Check for shorts before connecting USB. Do not apply 5 V to a GPIO input. If a peripheral's output is 5 V, use a suitable level shifter or divider designed for the signal and its timing.

3. Confirm MicroPython is running

Connect the board using a data-capable USB cable, open Thonny, choose the ESP32 MicroPython interpreter and correct serial port, then press Enter in the Shell. Test the REPL:

print("Hello from SolderHub")
import sys
print(sys.platform)

If the prompt is missing, press Enter, stop a running script, and confirm that another serial monitor is not holding the port. Use a firmware build for the exact ESP32 family. Keep a local copy of your scripts before changing firmware or erasing the filesystem.

MicroPython code is interpreted by the firmware on the board. It is not the same as running desktop Python on the computer, and not every desktop package is available. The machine module provides hardware interfaces such as Pin on supported builds.

4. Blink an LED

Choose a GPIO that your board documentation confirms is suitable for output. GPIO 4 below is an example only; replace it if your board pinout recommends another pin.

from machine import Pin
from time import sleep

led = Pin(4, Pin.OUT)  # verify GPIO 4 before connecting
while True:
    led.value(1)
    sleep(0.5)
    led.value(0)
    sleep(0.5)

Save the script locally and run it from Thonny. The LED should turn on for half a second and off for half a second. If the LED is wired from 3.3 V to a GPIO instead of from GPIO to GND, its active logic may be inverted; follow the actual circuit rather than assuming HIGH means lit.

To stop the loop, use Thonny's stop control. A loop that runs forever is normal for a blink demo, but it can make the Shell busy. During debugging, keep the timing slow enough to observe and avoid flooding the serial console.

5. Use toggle and a built-in LED carefully

MicroPython builds commonly provide Pin.toggle() on supported ports, but API availability can vary. The explicit led.value(1) and led.value(0) pattern is easy to understand and widely applicable. If your firmware supports toggle, the same loop can be shortened:

from machine import Pin
from time import sleep
led = Pin(4, Pin.OUT)  # replace with a verified GPIO
while True:
    led.toggle()
    sleep(0.5)

An onboard LED can be convenient, but its pin and polarity vary between boards. Some ESP32 development boards have no user LED, and some variants connect the LED through additional circuitry. Check the board schematic or vendor documentation. Do not try random GPIO numbers when the onboard LED does not respond; use a known external LED circuit first.

If the board resets when the LED is connected, disconnect it and test the print-only script. Check resistor placement, supply and ground, and whether you accidentally shorted a pin. A GPIO should not be used as a power output for a high-current LED strip.

6. Read a button with an internal pull-up

Wire a normally open pushbutton between a verified input-capable GPIO and GND. Configure the pin with Pin.IN and Pin.PULL_UP. The input is HIGH while the button is released and LOW while pressed.

from machine import Pin
from time import sleep

button = Pin(18, Pin.IN, Pin.PULL_UP)  # verify GPIO 18
while True:
    if button.value() == 0:
        print("Pressed")
    else:
        print("Released")
    sleep(0.1)

GPIO 18 is only an example. Select a pin that is exposed and safe on your board. Avoid pins used by flash, boot configuration, or onboard peripherals. The internal pull-up is weak by design; if the wire is long or the environment noisy, an external resistor and better wiring may be needed.

The loop prints the state repeatedly while the button is held, which is useful for an initial test but not ideal for an application. Once the logic is verified, report only state changes or generate a single event per press.

7. Debounce the button

Mechanical contacts do not switch cleanly. When pressed or released, the contacts may rapidly open and close for a short period. A program that treats every transition as a real press can count one physical press several times. This is called switch bounce.

A simple time-based debounce method accepts a changed state only after it remains stable for a chosen interval. The following example is suitable for learning and slow user-interface inputs; very fast or safety-critical systems need a more carefully designed approach.

from machine import Pin
from time import sleep_ms, ticks_ms, ticks_diff
button = Pin(18, Pin.IN, Pin.PULL_UP)  # verify GPIO 18
last_raw = button.value()
stable = last_raw
changed_at = ticks_ms()
DEBOUNCE_MS = 40
while True:
    raw = button.value()
    now = ticks_ms()
    if raw != last_raw:
        last_raw = raw
        changed_at = now
    if raw != stable and ticks_diff(now, changed_at) >= DEBOUNCE_MS:
        stable = raw
        if stable == 0:
            print("One press event")
    sleep_ms(5)

The code detects a stable transition to the pressed state. It does not print repeatedly while the button remains held. Adjust the debounce interval based on the switch and desired responsiveness, and test both press and release behaviour.

8. Combine the button and LED

A useful next exercise is to turn an LED on while the button is held. This verifies both an output and an input in one program.

from machine import Pin
from time import sleep_ms
led = Pin(4, Pin.OUT)  # verified output pin
button = Pin(18, Pin.IN, Pin.PULL_UP)  # verified input pin
while True:
    led.value(1 if button.value() == 0 else 0)
    sleep_ms(10)

This simple version is responsive enough for a basic button demo. It uses active-low button logic and assumes the LED is active-high. If the LED is wired in the opposite direction or a board's onboard LED is active-low, invert the output logic accordingly. Do not assume a sensor module's signal polarity without checking its documentation.

If it does not work, test each component separately. First print the button value with the LED disconnected; then run the LED blink script with the button disconnected. Once each circuit works independently, combine them. This avoids debugging two unknowns at the same time.

9. Common GPIO mistakes

Wrong pin number: header positions and GPIO identifiers are different. Use the exact board pinout. Input floats: configure a pull-up or pull-down, or install an appropriate external resistor. LED never lights: check polarity, resistor, ground, selected pin, and whether the pin is configured as output. LED stays on: confirm the code is running and verify whether the circuit is active-low. Repeated button events: debounce the switch and trigger on a stable transition rather than printing the current level continuously.

Board fails to boot: remove external wiring and check whether you used a boot-strapping pin or created a short. Unstable readings: shorten wires, ensure common ground, and keep signals away from noisy motor or relay wiring. Damaged input: applying 5 V or a negative voltage can exceed the GPIO's electrical limits. Disconnect the circuit and verify all signal levels before reconnecting.

Do not drive relay coils, motors, solenoids, or other inductive loads directly from a GPIO. Use a correctly rated transistor or driver, flyback protection where appropriate, and a suitable power supply. A logic pin is not a substitute for a power stage.

10. Organize your project code

Once the circuit works, separate hardware setup from the main behaviour. Use meaningful variable names, comments that explain board-specific pins, and small functions when the project grows. Avoid copying the same GPIO number into multiple places; define pin constants at the top so the wiring and code can be reviewed together.

For example, a larger program might use a read_button() function to return a stable state and a set_led() function to update the output. Keep the loop responsive and avoid long blocking delays when you later add Wi-Fi, sensors, or a web server. For more complex projects, a state machine can make the behaviour easier to reason about than a large nest of if statements.

If the script should run after power-up, save it to the ESP32 as main.py. During development, use a separate filename and run it manually until you know it behaves safely. Always keep a copy on the computer. If a main.py loop prevents normal interaction, use Thonny's stop/recovery workflow or reset the board and replace the script with a minimal test.

Final checklist

  • Verified the board-specific GPIO pinout.
  • Kept every GPIO signal within permitted 3.3 V logic limits.
  • Used a series resistor with the external LED.
  • Gave the button a defined pull-up or pull-down state.
  • Confirmed the active-low logic in the button circuit.
  • Added debounce before counting button presses.
  • Tested input and output separately before combining them.
  • Kept a local script backup and a recovery method.

Consult the official MicroPython machine.Pin documentation, the ESP32 technical reference for your chip family, and the schematic for your development board. Pin availability, onboard LED wiring, and special-purpose pins vary, so official board-specific information takes precedence over a generic example.

SolderHub takeaway: GPIO projects are simple when wiring and logic levels are explicit. Verify the pinout, protect the LED with a resistor, give inputs a defined state, debounce mechanical switches, and use a driver for loads that need more current than a GPIO can provide.

Further testing before adding hardware

After the basic circuit works, test repeated button presses, a long press, and a reset while the button is released. Confirm that the LED returns to the intended state after reset and that the program does not count contact bounce as several presses. If the project will be battery powered, remember that an LED left on can use far more energy than the ESP32 in a low-power state. Turn indicators off when they are not needed and measure the complete assembly rather than estimating from the microcontroller alone.

For projects with longer wires, consider external pull resistors, better grounding, and routing signal wires away from motor and relay wiring. Add more hardware one item at a time and retest after each change.

Signal In

Built from what makers search for

The lookups piling up in SolderHub's search bar every day shape what we publish next — new component pages, board guides, and firmware notes, sent out as they go live.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.