Interactive Mission

Multiple Buttons & State Detection

One debounced button is useful. Real projects often need several buttons, and each one needs its own clean state, timing, and purpose.

Mission 06FoundationBeginner25-30 minutesParent SafeTeacher Friendly
Mission 06

Read Several Buttons Reliably

The Story

Mission 05 taught your ESP32 to trust one button only after it settles. That is the foundation for real control panels.

Now the project grows up. Instead of one button, you will read three buttons. Each button has its own pin, its own state, its own debounce timer, and its own meaning. The goal is not just to add more wires. The goal is to keep several inputs clean, separate, and predictable.

Explain Like I'm 12

Imagine a piano. Pressing one key does not move the other keys. Each key has its own job.

ESP32 buttons should work the same way. Button A, Button B, and Button C are separate little worlds. The ESP32 checks each one quickly, remembers what each one was doing before, and notices when one of them changes.

That change is the important part. A button being held down is a state. A button that just became pressed is an event.

Mission Goal

Build a three-button input panel that detects clean press and release events for each button without repeated triggers while the button is held.

Estimated Time

25-30 min

Difficulty

Beginner

Prerequisites

Skills You'll Learn

  • Read more than one button without mixing up the inputs
  • Give every button its own pin, name, state, and debounce timer
  • Detect the difference between held state and new press event
  • Handle press and release actions separately
  • Use arrays and small helper functions to keep button code organized

Components Required

  • ESP32 DevKit boardReads three independent button inputs
  • Three push buttonsUse the same type if possible
  • BreadboardKeeps each button in its own rows
  • Jumper wiresFor GPIO25, GPIO26, GPIO27, and GND
  • USB data cableUpload code and watch Serial Monitor

Engineering Explanation

With one button, you tracked one raw reading, one stable state, and one last-change time. With three buttons, you need the same information three times.

The beginner mistake is to reuse one variable for everything. That mixes the buttons together. If Button A changes, it can overwrite the timing or state that Button B needed. Reliable multi-input code keeps each button independent.

This mission uses arrays. An array is a numbered list. buttonPins[0] stores the first GPIO pin, buttonPins[1] stores the second, and buttonPins[2] stores the third. Matching arrays store the raw readings, stable states, and debounce times for the same button number.

The loop checks each button one by one. For each button, the code reads the GPIO, restarts that button's debounce timer if the raw value changed, and accepts a new stable state only after the reading has stayed still long enough. When the accepted state changes to LOW, that is a press event. When it changes to HIGH, that is a release event.

This pattern scales. Three buttons, five buttons, and ten buttons use the same idea: separate identity, separate state, repeated clean logic.

Real-World Analogy

A teacher taking attendance does not ask, 'Is anyone here?' and stop. She checks each student by name: Ali, Sara, Mina, Omar. The ESP32 must do the same thing with buttons. It checks GPIO25, then GPIO26, then GPIO27, and remembers each result separately.

A piano is another good picture. Pressing C does not press D. Each key is independent, even though the musician can press several keys together. Your buttons should be wired and tracked the same way.

A traffic-light control panel also works like this. Each switch has a label and a job. If two switches share the wrong wire, one action can accidentally trigger another. Clean button systems keep the wiring and code labels matched.

Wiring Diagram

Follow these steps in order. Unplug USB before you change any wires.

Wiring Diagram ESP32 GPIO25, GPIO26, and GPIO27 each connected to one side of a push button, with the other side of each button connected to GND
  1. 1

    Unplug the ESP32 USB cable before wiring.

  2. 2

    Place three push buttons across the breadboard center gap.

  3. 3

    Connect one side of Button 1 to ESP32 GPIO25.

  4. 4

    Connect one side of Button 2 to ESP32 GPIO26.

  5. 5

    Connect one side of Button 3 to ESP32 GPIO27.

  6. 6

    Connect the opposite side of each button to GND.

  7. 7

    Do not add external resistors; the sketch uses INPUT_PULLUP for all three buttons.

  8. 8

    Plug USB back in, upload the sketch, and open Serial Monitor at 115200 baud.

GPIO Table

SignalESP32 PinModeNotes
Button 1GPIO25INPUT_PULLUPReleased reads HIGH. Pressed reads LOW.
Button 2GPIO26INPUT_PULLUPTracked separately from Button 1.
Button 3GPIO27INPUT_PULLUPUses its own debounce timer.
Common groundGNDGroundThe other side of every button connects to GND.

Arduino Code

Copy this into Arduino IDE, then click Upload.

multiple_buttons_state_detection.ino
const byte BUTTON_COUNT = 3;

const byte buttonPins[BUTTON_COUNT] = {25, 26, 27};
const char* buttonNames[BUTTON_COUNT] = {"START", "STOP", "MODE"};

const unsigned long DEBOUNCE_DELAY = 50;

int lastRawReading[BUTTON_COUNT] = {HIGH, HIGH, HIGH};
int stableState[BUTTON_COUNT] = {HIGH, HIGH, HIGH};
unsigned long lastChangeTime[BUTTON_COUNT] = {0, 0, 0};

void setup() {
  Serial.begin(115200);

  for (byte i = 0; i < BUTTON_COUNT; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }

  Serial.println("Mission 06: Multiple buttons ready");
  Serial.println("Press START, STOP, or MODE.");
}

void loop() {
  for (byte i = 0; i < BUTTON_COUNT; i++) {
    checkButton(i);
  }
}

void checkButton(byte index) {
  int rawReading = digitalRead(buttonPins[index]);

  if (rawReading != lastRawReading[index]) {
    lastChangeTime[index] = millis();
    lastRawReading[index] = rawReading;
  }

  bool readingHasSettled = (millis() - lastChangeTime[index]) > DEBOUNCE_DELAY;

  if (readingHasSettled && rawReading != stableState[index]) {
    stableState[index] = rawReading;

    if (stableState[index] == LOW) {
      Serial.print(buttonNames[index]);
      Serial.println(" pressed");
    } else {
      Serial.print(buttonNames[index]);
      Serial.println(" released");
    }
  }
}
  • Each button has its own pin, name, raw reading, stable state, and debounce timer.
  • INPUT_PULLUP means released is HIGH and pressed is LOW.
  • A press event is detected only when the debounced state changes from HIGH to LOW.
  • A release event is detected when the debounced state changes from LOW to HIGH.

Line-by-line Explanation

  • BUTTON_COUNT tells the program how many buttons are in the panel.
  • buttonPins stores the GPIO number for each button. Button 0 uses GPIO25, button 1 uses GPIO26, and button 2 uses GPIO27.
  • buttonNames stores human-friendly labels so Serial Monitor prints START, STOP, and MODE instead of only numbers.
  • lastRawReading keeps the latest immediate digitalRead() value for each button.
  • stableState keeps the trusted debounced state for each button.
  • lastChangeTime keeps a separate debounce timer for each button so one button's bounce does not affect another button.
  • setup() uses a for loop to set every button pin to INPUT_PULLUP.
  • loop() checks every button every time it runs. The ESP32 is fast enough that this feels instant for a small button panel.
  • checkButton(index) runs the same debounce logic for whichever button number is passed in.
  • When the debounced state becomes LOW, the code prints a press event. When it becomes HIGH, the code prints a release event.

Expected Behaviour

Press each button once. Serial Monitor should show one press event and one release event for the correct button name:

START pressed START released STOP pressed STOP released MODE pressed MODE released

If you hold START down, it should not print START pressed again and again. It prints once when the press begins, then once when the release happens.

Common Mistakes

  • Using one state variable for all buttons

    The code cannot remember three independent states in one variable.

  • Giving two buttons the same GPIO pin

    Copy-paste or wiring notes were not updated.

  • Forgetting a separate debounce timer

    One shared timer makes one button's bounce affect the others.

  • Triggering actions while the button is held

    The program reacts to current state every loop instead of reacting to the change event.

  • Mixing pull-up and pull-down wiring styles

    Some buttons are wired to GND and others to 3.3 V.

  • Testing all buttons at once first

    Multiple mistakes can hide each other.

Troubleshooting

Most ESP32 problems are wiring, power, library, or timing issues. Check these first.

  • Wrong button name appears

    Likely cause: The physical wire order does not match the buttonPins and buttonNames arrays.

    Fix: Trace each wire from the GPIO pin to the button and update either the wiring or the array order.

  • One button never prints

    Likely cause: That GPIO wire may be in the wrong breadboard row or the button may not cross the center gap.

    Fix: Move the button across the breadboard gap and test the pin with a single-button sketch if needed.

  • A button prints many presses

    Likely cause: Its debounce timer may not be separate, or the delay may be too short for that physical button.

    Fix: Confirm lastChangeTime is indexed and try DEBOUNCE_DELAY = 80.

  • Two buttons trigger together

    Likely cause: Their GPIO wires or ground rows may be accidentally connected.

    Fix: Separate the wires, check breadboard rows, and test each input one at a time.

  • Serial Monitor prints nothing

    Likely cause: The baud rate may be wrong, upload may have failed, or all buttons may be wired to the wrong side.

    Fix: Use 115200 baud, upload again, and confirm every button connects its GPIO pin to GND when pressed.

Engineer Tip

Give every physical input its own identity: pin number, name, purpose, state, and debounce timer. Professional firmware stays reliable because it never lets one input overwrite another input's memory.

Remember This Forever

A button state says what is true now.

A button event says what just changed.

Reliable projects act on events, remember states, and keep every input separate.

Mini Challenge

No wrong answers — experiment and have fun!

  • Add a fourth button on GPIO33 and name it RESET.
  • Press two buttons at the same time and describe what Serial Monitor reports.
  • Create a lastButtonPressed variable and print which button was pressed most recently.
  • Build a tiny password game where START, MODE, STOP must be pressed in that order.

FAQs

  • Can two buttons share one GPIO pin?

    Not if you want to know which button was pressed. Each normal button should use its own GPIO pin so the ESP32 can read it independently.

  • Do I need separate debounce timing for each button?

    Yes. Each mechanical button bounces on its own schedule, so each button needs its own last reading, stable state, and change time.

  • Will pressing two buttons at once damage the ESP32?

    No. With the INPUT_PULLUP wiring in this mission, pressing two buttons at once is safe. Your code simply needs to decide how to respond.

  • Is reading multiple buttons much slower than reading one?

    No. Reading a few GPIO pins takes a tiny amount of time. The bigger challenge is keeping the code organized and the states separate.

  • What is the difference between button state and button event?

    State means what the button is doing right now, such as pressed or released. Event means something just changed, such as a new press or a new release.

  • Why does my code trigger again while I hold the button?

    That usually happens when the code reacts to the current pressed state every loop instead of reacting only to the moment the state changes.

  • Can I copy the same debounce code three times?

    You can, but it becomes easy to make mistakes. Arrays and a small helper function keep the same logic consistent for every button.

  • Should all buttons use INPUT_PULLUP?

    For beginner projects, yes. Keeping all buttons wired the same way reduces confusion because released means HIGH and pressed means LOW for every button.

  • What happens if I mix up the wire order?

    The wrong button name will appear in Serial Monitor or the wrong action will run. Label your pins and test one button at a time.

  • Can I detect both press and release?

    Yes. A press event happens when the debounced state changes to LOW. A release event happens when it changes back to HIGH.

  • Why use arrays for buttons?

    Arrays let one piece of code handle several buttons. Instead of writing the same logic over and over, the loop can check each button by index.

  • Do I need interrupts for multiple buttons?

    No. For beginner button panels, checking the buttons in loop() is simpler, reliable, and easier to debug.

  • What if two buttons are pressed at exactly the same time?

    The ESP32 can detect both. Your program should define what that means, such as allowing both actions or giving one action priority.

  • Why not use the same variable for every button?

    One variable can only remember one thing at a time. If several buttons share the same state variable, one button can overwrite another button's information.

  • How should I name buttons in code?

    Use names that describe purpose, such as START, STOP, and MODE, instead of vague names like btn1 and btn2.

Previous Mission

Next Mission

Continue Learning