Skip to main content

2.2 Button Debounce

The goal of this example is to show you how to make button presses more reliable and consistent by using a technique called debouncing.
When we press a physical button, its contacts can bounce rapidly, causing the microcontroller to detect multiple presses instead of one.
In this example, we'll use software debouncing with the millis() function to ensure that one press equals one toggle of the LED.

In this documentation you will learn:

  • How to read a button state with digitalRead().
  • What is button debouncing and how to handle it with millis().
  • How to toggle an LED on each button press.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x Push button
  • 1x LED (any color)
  • 1x 330 ohm resistor
  • 5x Jumper wires
  • 1x USB-C cable
ℹ️
The button half of this circuit is the same as 2.1 Button Counter. If you still have that one built, keep it wired up and skip ahead to step 5. Everything before that is a repeat.

Putting the components together

Follow the eight steps below. Each photo is taken from the same position, so you can compare it with the previous one and see exactly what changed.

1. Insert the NULA MINI board on the breadboard

ℹ️
This step assumes you know how a breadboard is wired inside and what its power rails are. For an introduction, see Breadboard Fundamentals documentation page.

Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel, with the chip facing down. Press it in evenly until all the pins are seated.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard

This example uses three pins. The names are printed along both edges of the board, and each pin sits in its own numbered row. In the photo the board occupies rows 25 to 30, which puts IO19 in row 27 on the a–e side, and IO4 in row 27 and GND in row 30 on the f–j side.

ℹ️
Your board may sit in different rows than the one in the photo. What matters is the row each pin lands in. Read the names printed on the board rather than copying the numbers.

2. Place the push button

Push the button into the middle of the breadboard so that it straddles the centre channel, a few rows clear of the board. In the photo its legs are in rows 15 and 17.

Push button placed across the centre channel
Step 2: the button straddling the centre channel
⚠️
A push button has four legs, but only two of them matter. The two legs on the same side of the button are permanently joined together inside it. Pressing the button connects one side to the other. So always use one leg from each side of the centre channel, one in the a–e strip and one in the f–j strip. If you take both wires from the same side, the two are already connected and the board will behave as if the button is held down forever.

3. Connect GND to the button

Ground goes across in two hops, using the blue rail along the edge of the breadboard as a shared ground line.

  • First jumper: from the row holding GND (row 30 in the photo) out to the blue rail.
  • Second jumper: from that same blue rail back to row 15, on the f–j side of the button.
Two jumper wires connecting GND to the button through the negative rail
Step 3: GND reaches the button through the blue − rail
ℹ️
Both wires have to go into the same rail column, the one running alongside the blue line. The rail next to the red line is a separate strip and is not connected to it.

4. Connect IO19 to the button

Run a single jumper from the row holding IO19 (row 27 on the a–e side) to row 17, on the a–e side of the button.

Jumper wire connecting IO19 to the left side of the button
Step 4: IO19 connected to the other side of the button

The two wires reach the button from opposite sides: row 15 on the f–j side, row 17 on the a–e side. That is the diagonal pair, and it is what makes the button actually switch something. The button is now finished; the rest of this page is the LED.

5. Bring IO4 out and add the resistor

The LED is driven by IO4, which sits in row 27 on the f–j side of the board, the same side as GND and the opposite side from IO19.

⚠️
IO4 and IO19 are both in row 27, on opposite sides of the centre channel. They are completely separate. All of the LED wiring happens on the f–j side.
  • The board covers most of that row, so use the outermost hole, j27, and run a jumper along to row 7.
  • Then place the 330 Ω resistor so it bridges row 7 to row 5.
Jumper from IO4 and the 330 ohm resistor in place
Step 5: IO4 brought out to row 7, with the 330 Ω resistor bridging rows 7 and 5
ℹ️
Resistors have no polarity, so it does not matter which way round it goes. The rows are only what the photo happens to use. Any free rows work, as long as the resistor and the LED end up in series between IO4 and ground.

6. Add the LED

The LED goes in next, continuing the chain: long leg (anode) into row 5, the same row the resistor ends in, and short leg (cathode) into row 3.

LED added, anode to the resistor and cathode toward ground
Step 6: the LED in place, long leg toward the resistor
⚠️
Get the LED the right way round. An LED only passes current in one direction. The long leg must face the resistor and IO4, the short leg must face ground. Backwards, it simply will not light. Nothing will break, but nothing will happen either.

7. Connect the LED to ground

One jumper closes the circuit: from row 3, where the LED's short leg sits, across to the blue rail, the same rail the button already uses for ground.

Jumper connecting the LED cathode to the negative rail
Step 7: the LED's short leg wired to the blue − rail

The full path is now IO4 → 330 Ω → LED → − rail → GND. Because the rail is shared, the button and the LED both take their ground from the same place.

8. Connect the board to your computer

The finished circuit powered over USB-C
Step 8: the finished circuit, powered over USB-C

Reading the button state

The function digitalRead(pin) reads the value from a specified digital pin and returns either HIGH or LOW as result. We call this function inside the loop():


void loop() {

//digitalRead() is a function that reads the value from a specified digital pin, either HIGH or LOW.
bool reading = digitalRead(BUTTON_PIN);
}
ℹ️
The reading is inverted from what you might expect. INPUT_PULLUP switches on a resistor inside the chip that holds the pin at 3.3 V, so the pin reads HIGH while the button is released. Pressing the button connects the pin to GND, so it reads LOW. A button wired this way is called active low, and it is why the code below watches for a change from HIGH to LOW.

Button debouncing

When you press a button, it changes from an open circuit to a closed circuit, which we treat as a single press. For us, it feels like one clean action because we react slowly compared to electronics. But to a microcontroller, which checks the button state thousands of times in that short moment, it looks very different.

As the button is pushed, two metal contacts inside touch each other. Since they're not perfectly smooth or aligned, they can quickly connect and disconnect a few times before settling. To the microcontroller, that looks like the button was pressed several times in a row, even though you only pressed it once.

Button debouncing visualized
Visualization of button debouncing

The simplest software-based strategy to handle this is the delay-based debouncer. The whole point of it is to wait out the debouncing window. This is where the millis() comes in handy.

The function millis() returns the number of milliseconds passed since the board began running the current program. We can use it to check if enough time has passed between last button state change and current time, if it has, we are prepared for the next press, if not, wait a bit more.


Toggling the LED

Each time a valid press is detected, we flip the LED's state with a simple toggle. This code is located inside the debouncer filter.

if (lastState == HIGH && reading == LOW) {

//We toggle the ledState
ledState = !ledState;

//We turn the LED on or off depending on the current ledState
if (ledState == true) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
}
lastState = reading;

Code

Below is the full example code for toggling an LED with a debounced button. It uses the millis() function to check if enough time has passed between button state changes.

/*
This is a variable to which we pass the number of pin that we had connected the BUTTON to.
The NULA board has a pin naming logic as follows: IO19, where 19 is the number that we give to the variable.
If you wish to use a different pin, make sure you are using a IO__ marked pin.
*/
const int BUTTON_PIN = 19;

/*
This is a variable to which we pass the number of pin that we had connected the LED to.
The NULA board has a pin naming logic as follows: IO4, where 4 is the number that we give to the variable.
If you wish to use a different pin, make sure you are using a IO__ marked pin.

Remember that the LED needs a 330 Ohm resistor in series with it. That resistor limits how much current flows, and
without it the LED draws more than either it or the pin is built for, so both can be damaged.
*/
const int LED_PIN = 4;

/*
Those are the variables used for button debouncing.
*/
bool ledState = false;
bool lastState = HIGH;
unsigned long lastChangeMs = 0;
const unsigned long debounceMs = 25;

void setup() {

/*
pinMode() is a function that configures the specified pin to behave either as an input or in this case as an output.
This simply means that the pin "listens" for the available data when in input mode, and "writes" data when in output mode.
As our pin needs to detect if the button has been pressed, we will put the pin in INPUT_PULLUP mode.
INPUT_PULLUP switches on a small resistor inside the chip that gently ties the pin to 3.3V, so the pin reads HIGH
while the button is released and the button only has to connect it to GND to read LOW.
*/
pinMode(BUTTON_PIN, INPUT_PULLUP);

/*
pinMode() is a function that configures the specified pin to behave either as an input or in this case as an output.
This simply means that the pin "listens" for available data when in input mode, and "writes" data when in output mode.
As our pin needs to turn on the LED, we will put the pin in OUTPUT mode.
*/
pinMode(LED_PIN, OUTPUT);
}

void loop() {

//digitalRead() is a function that reads the value from a specified digital pin, either HIGH or LOW.
bool reading = digitalRead(BUTTON_PIN);

/*
millis() is a function that returns the number of milliseconds passed since the board began running the current program.
In this example, we use this function to check out if the debouncing period has finished. This number will overflow
(go back to zero), after approximately 50 days.
*/
unsigned long now = millis();

/*
This is our debouncing logic, we check if the button has been pressed and if enough time has passed so that we don't get
false readings because of the noise in the signal.
*/
if (reading != lastState && (now - lastChangeMs) > debounceMs) {
lastChangeMs = now;

/*
As we are using the pull-up method for connecting the button, the readings are the other way around from what you
might expect: the pin reads HIGH while the button is released and LOW while it is pressed. So we need to toggle the
LED when the button state goes from HIGH to LOW.
*/
if (lastState == HIGH && reading == LOW) {

//We toggle the ledState
ledState = !ledState;

//We turn the LED on or off depending on the current ledState
if (ledState == true) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
}
lastState = reading;
}
}

What you should see

Upload the sketch. Nothing happens at first. The LED starts off. Press the button once and it turns on, press it again and it turns off.

The LED lit after a button press
One press, one toggle
ℹ️
The LED does not light while you hold the button. It toggles once per press. That is the whole point of the debouncer: without it, a single press would flip the LED several times and leave it in whichever state it happened to land on.

Full example

Check out the full example code on the link below:

2.2_Button_Debounce.ino

Example that toggles an LED with a debounced button press.