Skip to content

Buttons

A push button is your first input: instead of the code controlling the hardware, the hardware tells the code what's happening. You'll learn how the ESP32 reads a pin, why buttons "bounce", and how to package code into a module you can reuse in every project.

You'll need:

  • 1 × push button
  • The LED and resistor from the LEDs lesson
  • 2 × jumper wires

Hardware

Inside a push button: two pairs of legs that get connected when pressed

A push button has four legs, joined in two pairs. Pushing the button down connects the pairs, so electricity can flow through. Let go, and the connection opens again.

The simplest way to use one is to connect one leg to a GPIO pin and a leg on the other side of the button to GND. Plug the button across the gap in the middle of the breadboard so the pairs aren't shorted together.

Wiring

From To
ESP32 GPIO 14 One leg of the button
Button leg on the opposite side ESP32 GND

Keep the LED from the last lesson plugged in on GPIO 27. You'll use it shortly.

Pull-up resistors

When the button isn't pressed, the pin isn't connected to anything. A pin like that is "floating" and reads random 0s and 1s. To fix this, we switch on the ESP32's built-in pull-up resistor, which gently pulls the pin up to 3.3 V:

  • Not pressed: the pull-up holds the pin at 3.3 V, so it reads 1.
  • Pressed: the button connects the pin straight to GND, so it reads 0.

That might feel backwards: pressed is 0! It's the most common way to wire buttons, so you'll get used to it.

Note

GPIO 34–39 don't have pull-up resistors, so don't use them for buttons.

Software

Reading the pin

button_basic.py
# Print what a button's pin reads, 10 times a second
import time
from machine import Pin

BUTTON_PIN = 14  # Button A

# PULL_UP keeps the pin at 1 until the button connects it to GND
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)

while True:
    print(button.value())   # 1 = not pressed, 0 = pressed
    time.sleep(0.1)
  • Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP) sets the pin up as an input with the pull-up resistor switched on.
  • button.value() reads the pin: 1 or 0.

Run it and hold the button down. The stream of 1s should turn into 0s.

Debouncing

Say you want to count presses. The obvious idea is "if the pin changed to 0, add one". But when the metal contacts inside the button touch, they bounce against each other for a few milliseconds. To a fast microcontroller, one press can look like five!

The fix is called debouncing: once we see a change, we ignore the button for a moment until it has settled.

button_debounce.py
# Count button presses, ignoring the "bounces"
import time
from machine import Pin

BUTTON_PIN = 14

button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)

last_value = button.value()
presses = 0

while True:
    value = button.value()
    if value != last_value:      # the button just changed
        if value == 0:           # ...and it changed to pressed
            presses += 1
            print("Pressed! Total:", presses)
        last_value = value
        time.sleep_ms(20)        # ignore the bouncing for 20 ms
  • last_value remembers what the button read last time around the loop, so we only react when the value changes, not the whole time it's held.
  • time.sleep_ms(20) waits 20 milliseconds after every change, which is long enough for the bouncing to stop.

Try removing the sleep_ms line and pressing the button a few times. You'll probably see the count jump by more than one.

A reusable Button module

Almost every project uses buttons, and copying the debounce code every time gets messy (especially with three buttons!). So we've packaged it into a module: a Python file that other programs can import.

  1. Download button.py (or copy it from the box below).
  2. Save it onto your ESP32 as button.py. See Saving Programs to the Board if you need a reminder of how.
button.py
button.py
# button.py - a debounced push button you can reuse in any project.
#
# Wiring: one leg of the button to a GPIO pin, the other leg to GND.
# The internal pull-up resistor keeps the pin at 1 until the button
# is pressed, which connects it to GND and makes it read 0.
#
# Usage:
#   from button import Button
#   button = Button(14)
#   while True:
#       if button.was_pressed():
#           print("Pressed!")

import time
from machine import Pin


class Button:
    def __init__(self, pin_number, debounce_ms=30):
        self.pin = Pin(pin_number, Pin.IN, Pin.PULL_UP)
        self.debounce_ms = debounce_ms
        self._last_reading = self.pin.value()
        self._stable_value = self._last_reading
        self._changed_at = time.ticks_ms()
        self._pressed = False

    def _update(self):
        # Only trust a new reading once it has stayed the same for
        # debounce_ms. That ignores the "bounces" a button makes
        # for a few milliseconds when it is pushed or released.
        reading = self.pin.value()
        now = time.ticks_ms()
        if reading != self._last_reading:
            self._last_reading = reading
            self._changed_at = now
        elif (reading != self._stable_value
              and time.ticks_diff(now, self._changed_at) >= self.debounce_ms):
            self._stable_value = reading
            if reading == 0:
                self._pressed = True

    def was_pressed(self):
        # True once for each push, no matter how long it is held down.
        self._update()
        if self._pressed:
            self._pressed = False
            return True
        return False

    def is_down(self):
        # True for as long as the button is held down.
        self._update()
        return self._stable_value == 0

This version is a little smarter than our first one. Instead of pausing the whole program with sleep_ms, it notes the time of each change and only trusts the reading once it has stayed the same for 30 ms. Your program never has to stop and wait.

Now counting presses takes just a few lines:

button_module.py
# Count presses using the reusable Button class from button.py
# (copy button.py onto your ESP32 first)
from button import Button

button = Button(14)
presses = 0

while True:
    if button.was_pressed():
        presses += 1
        print("Pressed! Total:", presses)

The Button class gives you two methods:

Method Returns True… Use it for…
button.was_pressed() once for each press Toggling things, counting, menus
button.is_down() the whole time the button is held "Hold to move" controls

Keep your loop fast

The button is only checked when you call one of these methods. If your loop has a long time.sleep() in it, a quick press can slip by unnoticed. Keep sleeps short, or leave them out.

Button + LED

Time to combine what you've learned. Each press should flip the LED between on and off. Try writing it yourself first!

button_led_toggle.py
# Each press of Button A turns the LED on or off
from machine import Pin
from button import Button

LED_PIN = 27
BUTTON_PIN = 14

led = Pin(LED_PIN, Pin.OUT)
button = Button(BUTTON_PIN)

while True:
    if button.was_pressed():
        led.value(not led.value())   # flip the LED

Challenge

Make the LED light up only while the button is held down, but also print "Pressed!" once each time it's pushed.

Show a solution
button_hold.py
# The LED stays on only while Button A is held down,
# and a message is printed each time it's pressed
from machine import Pin
from button import Button

led = Pin(27, Pin.OUT)
button = Button(14)

while True:
    if button.was_pressed():
        print("Pressed!")
    led.value(button.is_down())

button.is_down() returns True or False, and led.value() treats those just like 1 and 0.

Next up: Potentiometers, an input that gives you a whole range of values instead of just on or off.