Effective Interfacing of Pushbuttons to Arduino Boards

In this post we will go back to basics and look at using pushbuttons. This is the first device covered in my book Making Sense of 37 Sensors.

A pushbutton or other switch is the simplest input device you can have in your microcontroller project, but it does have some pitfalls. Let’s look at a simple pushbutton switch.

There are two general ways to connect the switch. The circuit shown on the left has the switch connecting the digital input to ground when closed, and the digital input being pulled up to Vcc when open. The circuit on the right exchanges the positions of the switch and resistor, which now is a pull-down resistor. This circuit has the advantage of the input being high (or true) when closed. The circuit on the left requires inverting the input (with the logical NOT or “!” operator in C++) to have closed be true

However it has the electrical advantage in that the power supply voltage does not need to be provided to the switch, just the ground potential. Also, the pullup resistor can be eliminated by using the built-in pullup provided by the microcontroller

We will connect a pushbutton to the Arduino board and use it to control the LED that is built into the board. Here, the common Arduino Uno R3 (or here the Elegoo clone) is used, however nearly every Arduino model or clone will work. The pushbutton is connected between digital input 2 and ground.

Elegoo Uno R3 (Arduino clone) with pushbutton attached

Basic Operation

For the most basic example, we will light the LED while the button is pressed and extinguish the LED when the button is not pressed. This is done with the following program (or “sketch”):

#define BUTTON 2  // pin for button

void setup() {
  pinMode(BUTTON, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT); // See text!
}

void loop() {
  bool buttonState = !digitalRead(BUTTON);
  digitalWrite(LED_BUILTIN, buttonState);
}

The setup function configures the button input pin to have the the pullup enabled and the LED pin to be an output. On boards without the built-in LED, an LED will need to be attached to another digital pin, and then connect through a current limiting resistor to ground. 

In the loop function, the button is read, value complemented, and stored in the variable buttonState. This value is then written to the LED. Some models have the built-in LED connected to Vcc rather than ground. These require the output driven low to turn the LED on. For these the digitalWrite statement needs to be:

digitalWrite(LED_BUILTIN, !buttonState);

Note reading the button and writing the led happens continuously, regardless of whether the button state changes. While this does take time, it does not wear anything out.

Edge Triggered

The level sensitive input of the first example goes wrong when we need to know when the button is being depressed rather than is staying depressed. Imaging having the button as in input to a counter. We want to increment the count for each button press. In the previous program the counter will be incremented every time through loop while the button is depressed, which will be thousands of times per second!

In this example we want to toggle the led whenever the button is pressed, once per press.  This happens when we read the button as pressed, but the previous time we read the button it was not pressed. The new loop function is:

void loop() {
  static bool lastState = false;
  static bool ledState = false;
  bool buttonState = !digitalRead(BUTTON);

  if (buttonState && !lastState) {
    //toggle LED
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);
  }

  lastState = buttonState;
}

A new variable, lastState, has the button state in the previous loop iteration. So we do the toggle if buttonState is true and lastState is false. The current led state is saved in variable ledState and is toggled when we change the led state.

Debouncing

Switch contacts have a tendency to bounce on each other when the switch is opened and/or closed. This creates false presses that can cause additional counts in edge triggered applications. To solve the problem one needs to add a low-pass filter that blocks the high frequency bouncing but allows the regular presses to be seen. Debouncing can be performed by adding components to create a low pass filter, but it is much easier to add a low pass filter in software by simply reading the button at a slow enough rate. In this example the sampling rate, SAMPLE_TIME, is given the value 10, so that the button is only read every 10 milliseconds. The added code is shown in red.

void loop() {
  static bool lastState = false;
  static bool ledState = false;
  static unsigned long lastSampleTime = 0;

  unsigned long currentTime = millis();
  if (currentTime - lastSampleTime > SAMPLE_TIME) {
    lastSampleTime = currentTime;
    
    bool buttonState = !digitalRead(BUTTON);
    if (buttonState && !lastState) {
      //toggle LED
      ledState = !ledState;
      digitalWrite(LED_BUILTIN, ledState);
    }
    lastState = buttonState;
  }
}

Note that in the time comparison we always want to use relative times, the difference between two times, in the expression. If we instead used currentTime > lastSampleTime + SAMPLE_TIME, the comparison will be invalid if lastSampleTime+SAMPLE_TIME overflows.