A pushbutton that toggles an LED

Connect a button and LED, recognise one press as one event, and debounce the input using a 30 ms stability check.

Getting started with ESP32 and ArduinoBeginner60 min

What you will learn

  • Connect GPIO27 through a pushbutton to GND and enable INPUT_PULLUP.
  • Distinguish a raw reading, stable state and falling edge.
  • Explain debouncing with millis() and a 30 ms interval.
  • Toggle an LED once per accepted press without repeating during a hold.
  • Check pressing, releasing, holding and resetting against expected behaviour.

Before you start

Complete courses 01 and 02: uploading Arduino sketches, using Serial Monitor and connecting an LED with a series resistor.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

Build a light that switches on with one press and off with the next. Releasing or holding the button causes no repeated switching. The LED starts off. Learn why mechanical contacts can produce several events from one press and how software prevents that.

Learning objectives

  • Connect a pushbutton to a digital input with an internal pull-up resistor.
  • Distinguish an input level from a press event.
  • Accept a change after the reading remains stable for 30 ms.
  • Preserve the LED state between calls to loop().
  • Check short presses, releases and long holds.

Prerequisites

Complete courses 01 and 02: uploading, Serial Monitor and LED wiring. Recognise setup(), loop(), HIGH and LOW. We introduce timing here.

Equipment

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
Red LED1Anode A and cathode K; identify polarity on the actual part.
220 Ω resistor1One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors.
Momentary pushbutton1Use two terminals connected only when pressed; two legs on the same side may already be joined.
Jumper wires5Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity.

Use a classic ESP32/ESP32-WROOM-32 board. Follow GPIO labels, not physical pin positions. ESP32-C3, S2 and S3 need a different wiring profile.

How the button and program work

Pressing the button connects GPIO27 to ground. INPUT_PULLUP enables an internal resistor that keeps the released input HIGH. A pressed button makes it LOW. This arrangement needs no additional external pull-up resistor. See the Espressif GPIO documentation for input configuration and reading functions.

Mechanical contacts can briefly bounce when pressed. The program therefore separates the raw reading from the accepted, stable state. Each change in the raw reading restarts a timer. A new state is accepted only after the reading stays unchanged for at least 30 ms. This is called debouncing; the Arduino Debounce example demonstrates the principle.

We switch the LED only when the accepted state changes from HIGH to LOW, called a falling edge. A steady LOW during a hold is not another event. Releasing the button returns the state to HIGH, ready for the next press.

Wiring and assembly

Wiring diagram

The drawing shows electrical connections by GPIO label, not the board’s physical header layout. Power the board through USB.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.GPIO25R1.1Control the LED branch.
R1.2D1.AConnect the 220 Ω resistor to the LED anode.
D1.KESP32.GNDConnect the cathode to common ground.
ESP32.GPIO27SW1.CONTACT_ADigital input configured as INPUT_PULLUP.
SW1.CONTACT_BESP32.GNDThe selected contact pair closes only while pressed.
  1. Disconnect USB before moving wires. Connect ESP32 GND to your breadboard ground connection.
  2. Connect GPIO25 through a 220 Ω resistor to the LED anode. Connect the cathode to GND. The anode is usually the longer lead of a new LED; the flat case edge identifies the cathode side. Check the actual component if its leads have been cut.
  3. Identify the button contacts. A typical four-leg tactile switch has two internally connected pairs; pressing joins those pairs. Confirm the layout from documentation or a continuity measurement with the switch disconnected.
  4. Choose two contacts that are disconnected when released and connected when pressed. Wire one to GPIO27 and the other to GND.
  5. Check for a permanent connection from the input to ground that bypasses the switch. Do not apply 5 V to GPIO; power the board through its USB cable.

Choosing two already connected legs produces a permanently pressed input. Also check the breadboard contact groups: placing both connections in one connected group bypasses the button.

Arduino setup

In Arduino IDE 2.x, install esp32 by Espressif Systems, target version 3.3.12, following the official installation guide. Select ESP32 Dev Module and your board's port. No extra libraries are required.

Download the Arduino example (.zip), extract it and open button_toggle/button_toggle.ino. The complete course package keeps this folder under arduino/. Keep folder and sketch names matched. Check GPIO25 for the LED and GPIO27 for the button. Set Serial Monitor to 115200 baud.

Complete Arduino program

button_toggle.ino Arduino / C++
Download .ino

#include <Arduino.h>

constexpr uint8_t LED_PIN = 25;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 30;

bool ledOn = false;
int lastReading = HIGH;
int stableButtonState = HIGH;
uint32_t lastChangeMs = 0;

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // A button held during startup must be released before the first toggle.
  lastReading = digitalRead(BUTTON_PIN);
  stableButtonState = lastReading;
  lastChangeMs = millis();
  Serial.println("WB Maker Bridge - Press the button to toggle the LED.");
  Serial.println("LED: OFF");
}

void loop() {
  const uint32_t now = millis();
  const int reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {
    lastReading = reading;
    lastChangeMs = now;
  }

  if (now - lastChangeMs >= DEBOUNCE_MS && reading != stableButtonState) {
    stableButtonState = reading;

    // INPUT_PULLUP means LOW is pressed; only a new press toggles the LED.
    if (stableButtonState == LOW) {
      ledOn = !ledOn;
      digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
      Serial.print("LED: ");
      Serial.println(ledOn ? "ON" : "OFF");
    }
  }
}

Run the displayed, downloadable example unchanged first.

Code walkthrough

In setup(), the LED starts off and the button gets its pull-up. ledOn stores the LED state. Other variables store raw and accepted readings and the last change time. DEBOUNCE_MS sets the 30 ms interval.

Each pass through loop() reads the input and checks elapsed time. A raw change records a new timestamp. After 30 ms of stability, the new state is accepted. If that newly accepted state is LOW, the program reverses the stored LED state and updates the output.

Timing uses millis() and the difference between timestamps. The program keeps checking the button during the 30 ms interval. It does not pause execution for that interval, so filtering an event does not require a blocking delay.

Run and observe

Click Verify, then Upload. Open Serial Monitor and release the button. Reset the ESP32 to establish a repeatable starting point. The LED should be off. Press and release normally: it turns on. The next press turns it off.

Holding for two seconds changes the LED once; release causes no change. Serial Monitor reports LED: ON or LED: OFF. Reset clears the LED state. A button held during reset must be released before the first press.

Test, starting with the LED offExpected result
One normal pressLED on.
Second pressLED off.
Third press, held for two secondsLED stays on during the hold.
Release after that holdLED stays on.
Reset with the button releasedLED off.

Record compilation and board observations separately. Pulses shorter than the stable interval may be ignored.

Troubleshooting

SymptomCheck and action
LED never lightsCheck polarity, resistor, GPIO25 and shared ground; repeat the LED exercise from course 02.
Button causes no changeCheck GPIO27 and the selected contacts; their continuity must change when pressed.
Input seems permanently activeCheck for wires on already connected switch legs or one breadboard contact group.
One press causes several changesRestore the original 30 ms debounce program and check loose connections.
Unreadable messagesSet Serial Monitor to 115200 baud.

Independent challenge

Starting from reset, make five separate presses and predict the state before the sixth. Then change the debounce interval from 30 to 250 ms. Compare a quick touch with a half-second press. Record which action is accepted and whether you notice a delay. Restore 30 ms afterwards.

Check your understanding

  1. Why does a released button read HIGH?
  2. Why should we avoid switching the LED for every LOW reading?
  3. Is release a falling edge in this circuit?

Answer guidance

After five accepted presses the LED is on; the sixth turns it off. Each pair returns it to its starting state. With a 250 ms interval, a roughly 200 ms press is too short to be accepted. A half-second press is accepted after about a quarter-second of stable input.

The internal pull-up establishes the released HIGH. Reacting to every LOW would repeatedly switch the LED during a hold. Pressing changes HIGH to LOW; releasing changes LOW to HIGH. A release must also be accepted before another press can be registered.

Downloads

Primary references