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
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| Red LED | 1 | Anode A and cathode K; identify polarity on the actual part. |
| 220 Ω resistor | 1 | One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors. |
| Momentary pushbutton | 1 | Use two terminals connected only when pressed; two legs on the same side may already be joined. |
| Jumper wires | 5 | Use 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.
| From | To | Connection |
|---|---|---|
ESP32.GPIO25 | R1.1 | Control the LED branch. |
R1.2 | D1.A | Connect the 220 Ω resistor to the LED anode. |
D1.K | ESP32.GND | Connect the cathode to common ground. |
ESP32.GPIO27 | SW1.CONTACT_A | Digital input configured as INPUT_PULLUP. |
SW1.CONTACT_B | ESP32.GND | The selected contact pair closes only while pressed. |
- Disconnect USB before moving wires. Connect ESP32 GND to your breadboard ground connection.
- 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.
- 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.
- Choose two contacts that are disconnected when released and connected when pressed. Wire one to GPIO27 and the other to GND.
- 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
#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 off | Expected result |
|---|---|
| One normal press | LED on. |
| Second press | LED off. |
| Third press, held for two seconds | LED stays on during the hold. |
| Release after that hold | LED stays on. |
| Reset with the button released | LED off. |
Record compilation and board observations separately. Pulses shorter than the stable interval may be ignored.
Troubleshooting
| Symptom | Check and action |
|---|---|
| LED never lights | Check polarity, resistor, GPIO25 and shared ground; repeat the LED exercise from course 02. |
| Button causes no change | Check GPIO27 and the selected contacts; their continuity must change when pressed. |
| Input seems permanently active | Check for wires on already connected switch legs or one breadboard contact group. |
| One press causes several changes | Restore the original 30 ms debounce program and check loose connections. |
| Unreadable messages | Set 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
- Why does a released button read
HIGH? - Why should we avoid switching the LED for every
LOWreading? - 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.
