Buttons and other electro-mechanical inputs (advanced)

Introduction
This tutorial follows on from Buttons and other electro-mechanical inputs (introduction) and should be read when you are comfortable with the concepts explained in that tutorial.

If you have not reviewed the tutorial ‘Buttons and electro-mechanical inputs (introduction)’ please do so.

In this tutorial more examples are offered covering things you can do with buttons as inputs to an Arduino. Examples are in order of increasing functionality and associated difficulty.

Examples presented

  • Output on press and output again on release
  • Output on press then repeat while button is held
  • Output on short press or long press
  • Multi button input
  • 4 buttons with separate button press detection, resulting action and display

Schematic for all examples using a single button

Here is what it looks like with a Nano Every on breadboard

Output on press and output again on release

This example sends “Button has been pressed” to the serial monitor when the button becomes pressed then sends “Button has been released” when the button has been released.

/* Simple debounce for a single push to make button wired between ground (0V) and buttonPin */
/* This version sends "Button has been pressed" to the serial monitor when the button becomes pressed */
/* and "Button has been released" when the button is released */
/* Tested on a Nano Every, should work on any Arduino */

const uint8_t BUTTONPIN = 2;          // Input for button. On many Arduinos pins 0 and 1 are used for serial making pin 2 the first free pin.

void setup() {
  Serial.begin(9600);                 // Start the serial monitor to see the results.
  char fileName[] = {__FILE__};
  Serial.println(fileName);           // Prints the name of the file the location this sketch is in.
  pinMode(BUTTONPIN, INPUT_PULLUP);   // Make the button pin an input with the internal pull up resistor enabled.
}

void loop() {
  Buttons();                          // Calls the function to read the button.
  // Your other code goes here, make sure it is non blocking (no delays, no loops that don't exit quickly)
}

/* This is the function that reads the state of the button, debounces it and prints to the serial monitor when it detects a press */
void Buttons() {
  #define buttonPressed LOW             // When the button is pressed the input will be low, this is to remove the confusion this migth cause.
  uint32_t currentMillis = millis();                      // Millis times uses to debounce the button
  static uint32_t lastMillis;                             // Start of the debounce timeout
  const uint32_t BOUNCETIMEOUT = 20;                      // Debounce time in milliseconds 
  bool currentButtonState = digitalRead(BUTTONPIN);       // Reads the current state of the button and saves the result in a bool
  static bool lastButtonState = HIGH;                     // Holds the previous debounced state of the button, initialised as HIGH because the input is high when the button is not pressed
  
  if (lastButtonState != currentButtonState) {            // Checks to see if the button has been pressed or released, at this point the button has not been debounced
    if (currentMillis - lastMillis >= BOUNCETIMEOUT) {    // Checks to see if the state of the button has been stable for at least bounceTimeout duration
      lastButtonState = currentButtonState;               // At this point the button has been debounced, so save the last state
      if (currentButtonState == buttonPressed) {          // The button might have been pressed or released, this make sure only presses are acted on, not releases
        Serial.println ("Button has been pressed");       // Here you put whatever code you want to take action when the button is pressed
      } else {
        Serial.println ("Button has been released");      // Here you put whatever code you want to take action when the button is released
      }
    }
  } else {
    lastMillis = currentMillis;                           // Saves the current value of millis in last millis so the debounce timer starts from current millis
  }
}

This is what you can expect on the serial monitor

Output on press then repeat while button is held

This code sends “Button has been pressed” to the serial monitor when the button becomes pressed then repeats while the button is held. To make it obvious what is happening I have included a counter that increments if the button is held down. You can vary the time before the repeating starts and you can vary the interval between each repeat. This version includes a state machine in the Buttons function.

/* Simple debounce for a single push to make button wired between ground (0V) and buttonPin */
/* This version sends "Button has been pressed" to the serial monitor when the button becomes pressed */
/* then repeats as long as the button is held down. Includes a counter to make it clear what is happening */
/* The function Buttons in this tutorial is constructed as a state machine */

const uint8_t BUTTONPIN = 2;          // Input for button. On many Arduinos pins 0 and 1 are used for serial making pin 2 the first free pin.

void setup() {
  Serial.begin(9600);                 // Start the serial monitor to see the results.
  char fileName[] = {__FILE__};
  Serial.println(fileName);           // Prints the name of the file the location this sketch is in.
  pinMode(BUTTONPIN, INPUT_PULLUP);   // Make the button pin an input with the internal pull up resistor enabled.
}

void loop() {
  Buttons();                          // Calls the function to read the button.
  // Your other code goes here, make sure it is non blocking (no delays, no loops that don't exit quickly)
}

/* This is the function that reads the state of the button, debounces it and prints to the serial monitor when it detects a press and then repeats after a delay */
void Buttons() {
  #define buttonPressed LOW                                     // When the button is pressed the input will be low, this is to remove the confusion this migth cause.
  uint32_t currentMillis = millis();                            // Millis times uses to for debouncing and timing repeats
  static uint32_t lastMillis;                                   // Start of timeouts
  const uint32_t BOUNCETIMEOUT = 20;                            // Debounce time in milliseconds
  const uint32_t REPEATDELAY = 750;                             // Delay before button repeats
  const uint32_t REPEATINTERVAL = 150;                          // Interval between each repeat
  bool currentButtonState = digitalRead(BUTTONPIN);             // Reads the current state of the button and saves the result in a bool
  static uint8_t state = 0;                                     // Holds the current state of the button as per states
  enum states {notPressed, debouncing, firstRepeat, repeating}; // Differnt states the button can be in
  static uint8_t counter = 0;                                   // Counter to show the repeating output when the button is held down

  if(currentButtonState == buttonPressed) {
    switch (state) {
      case notPressed:
        lastMillis = currentMillis;
        state = debouncing;
        break;
      case debouncing:
        if (currentMillis - BOUNCETIMEOUT >= lastMillis) {
          lastMillis = currentMillis;
          state = firstRepeat;
          Serial.print("Button pressed counter = ");            // First detection of button pressed after bounceTimeout time has expired, put your own code here
          Serial.println(counter);
          ++counter;
        }
        break;
      case firstRepeat:
        if (currentMillis - REPEATDELAY >= lastMillis) {
          lastMillis = currentMillis;
          state = repeating;
          Serial.print("First repeat counter = ");            // First repeat, put your own code here
          Serial.println(counter);
          ++counter;
        }
        break;
      case repeating:
        if (currentMillis - REPEATINTERVAL >= lastMillis) {
          lastMillis = currentMillis;
          Serial.print("Repeating counter = ");               // Second and subsequent repeats, put your own code here
          Serial.println(counter);
          ++counter;
        }
        break;
    }
  } else {
    state = notPressed;
  }
}

This is what you can expect on the serial monitor

Output on short press or long press

This code waits until you release the button then sends either “Short press” or “Long press” to the serial monitor, depending on how long the button was held down for. You can vary the time considered to be a short press, anything longer than that will be a long press.

/* Simple debounce for a single push to make button wired between ground (0V) and buttonPin */
/* This version times the press and prints "Short press" or "Long press" depending how long the button is held down */

const uint8_t BUTTONPIN = 2;          // Input for button. On many Arduinos pins 0 and 1 are used for serial making pin 2 the first free pin.

void setup() {
  Serial.begin(9600);                 // Start the serial monitor to see the results.
  char fileName[] = {__FILE__};
  Serial.println(fileName);           // Prints the name of the file the location this sketch is in.
  pinMode(BUTTONPIN, INPUT_PULLUP);   // Make the button pin an input with the internal pull up resistor enabled.
}

void loop() {
  Buttons();                          // Calls the function to read the button.
  // Your other code goes here, make sure it is non blocking (no delays, no loops that don't exit quickly)
}

/* This is the function that reads the state of the button, debounces it and prints either "short press" or "Long press" depending on how long the button is held for */
void Buttons() {
  #define buttonPressed LOW                                 // When the button is pressed the input will be low, this is to remove the confusion this migth cause.
  uint32_t currentMillis = millis();                        // Millis times uses to debounce the button
  static uint32_t lastMillis;                               // Start of the timeouts
  const uint32_t BOUNCETIMEOUT = 20;                        // Debounce time in milliseconds
  const uint32_t LONGPRESSTIME = 500;                       // Interval for a long press
  bool currentButtonState = digitalRead(BUTTONPIN);         // Reads the current state of the button and saves the result in a bool
  static bool lastButtonState = HIGH;                       // Holds the previous debounced state of the button
  
  if (lastButtonState != currentButtonState) {              // Checks to see if the button has been pressed or released, at this point the button has not been debounced
    lastButtonState = currentButtonState;                   // This is to ensure the following code only responds once to each press or release of the button
    if (currentButtonState == buttonPressed) {
      lastMillis = currentMillis;                           // Save the value of millis to start the timer
    } else {
      if (currentMillis - lastMillis >= BOUNCETIMEOUT) {    // Checks that at least the debounc time has elapsed since lastMillis was updated
        if (currentMillis - lastMillis >= LONGPRESSTIME) {  // Checks to see if longPressTime has been exceeded
          Serial.println("Long press");                     // LongPressTime has been exceeded
        } else {
          Serial.println("Short press");                    // LongPressTime has not been exceeded
        }
      }
    }
  }
}

This is what you can expect on the serial monitor

Multi button input

This version reads and debounces 4 buttons with code can easily be changed for more or fewer buttons.

Schematic for 4 buttons

Here is what it looks like with a Nano Every on breadboard

/* Debounce for a multiple push to make buttons wired between ground (0V) and buttonPin */
/* Tested on a Nano Every, should work on any Arduino */

const uint8_t BUTTONPIN[] = {2, 3, 4, 5}; // Inputs for buttons. On many Arduinos pins 0 and 1 are used for serial making pin 2 the first free pin.
const uint8_t BUTTONCOUNT = 4;            // Number of buttons.

void setup() {
  Serial.begin(9600);                     // Start the serial monitor to see the results.
  char fileName[] = {__FILE__};
  Serial.println(fileName);               // Prints the name of the file the location this sketch is in.
  for (uint8_t i = 0; i < BUTTONCOUNT; ++i) {
    pinMode(BUTTONPIN[i], INPUT_PULLUP);  // Make the button pins inputs with the internal pull up resistor enabled.
  }
}

void loop() {
  Buttons();                          // Calls the function to read the button.
  // Your other code goes here, make sure it is non blocking (no delays, no loops that don't exit quickly)
}

/* This is the function that reads the state of the button, debounces it and prints to the serial monitor when it detects a press */
void Buttons() {
  #define buttonPressed LOW                             // When the button is pressed the input will be low, this is to remove the confusion this migth cause.
  uint32_t currentMillis = millis();                    // Millis times uses to debounce the button
  static uint32_t lastMillis[BUTTONCOUNT];              // Start of the debounce timeout for each button
  const uint32_t BOUNCETIMEOUT = 20;                    // Debounce time in milliseconds 
  bool currentButtonState[BUTTONCOUNT];                 // Holds the current state of each button
  static bool lastButtonState[BUTTONCOUNT];             // Holds the previous debounced state of the button
  uint8_t i;
  
  for (i = 0; i < BUTTONCOUNT; ++i) {
    currentButtonState[i] = digitalRead(BUTTONPIN[i]);        // Reads the current state of each button and saves the result
    if (lastButtonState[i] != currentButtonState[i]) {        // Checks to see if each button has been pressed or released, at this point each button has not been debounced
      if (currentMillis - lastMillis[i] >= BOUNCETIMEOUT) {   // Checks to see if the state of each button has been stable for at least bounceTimeout duration
        lastButtonState[i] = currentButtonState[i];           // At this point the button has been debounced, so save the last state
        if (currentButtonState[i] == buttonPressed) {         // The button might have been pressed or released, this make sure only presses are acted on, not releases
          Serial.print ("Button ");                           // Here you put whatever code you want to take action when a button is pressed
          Serial.print (i);
          Serial.println (" has been pressed");
        }
      }
    } else {
      lastMillis[i] = currentMillis;                         // Saves the current value of millis in last millis so the debounce timer for each button starts from current millis
    }
  }
}

This is what you can expect on the serial monitor

4 buttons with separate button press detection, resulting action and display

This is the most complex version of the code presented here, and should only be studied when the earlier versions are properly understood. In larger sketches it can be useful to divide code up into smaller functions to make it tidy and make it easy to identify which bit of code does what. While not particularly necessary for the small sketches used here for demonstration purposes, this version of the code is split into 2 separate functions to deal with reading and debouncing the buttons then taking some action and displaying or printing the results. Each completed action is signalled to the next function by setting a flag. Only when the subsequent function detects a set flag does it carry out its task. This version uses the 4 button circuit in the previous version, and the output is exactly the same.

/* Debounce for multiple push to make buttons wired between ground (0V) and buttonPin */
/* This version separates the task of debouncing buttons from the subesqent action */
/* This is the most advanced version of the code and might be confusing if you are new to writing code */
/* Tested on a Nano Every, should work on any Arduino */

const uint8_t buttonPin[] = {2, 3, 4, 5}; // Inputs for buttons. On many Arduinos pins 0 and 1 are used for serial making pin 2 the first free pin.
const uint8_t buttonCount = 4;            // Number of buttons.
bool buttonPressedFlag[buttonCount];      // Flags to indicate that a button has been detected as pressed and debounced

typedef void (* functionPtrs) (void);
functionPtrs functions[4] = {&buttonZero, &buttonOne, &buttonTwo, &buttonThree};        // Pointers to the above 4 functions, this is how a different function is selected depending on which button is pressed

void setup() {
  Serial.begin(9600);                     // Start the serial monitor to see the results.
  char fileName[] = {__FILE__};
  Serial.println(fileName);               // Prints the name of the file the location this sketch is in.
  for (uint8_t i = 0; i < buttonCount; ++i) {
    pinMode(buttonPin[i], INPUT_PULLUP);  // Make the button pins inputs with the internal pull up resistor enabled.
  }
}

void loop() {
  Buttons();                          // Calls the function to read the button.
  doStuff();                          // Does something in response to the input
  // Your other code goes here, make sure it is non blocking (no delays, no loops that don't exit quickly)
}

/* This is the function that reads the state of the button, debounces it then sets a flag for the separate doStuff()function to do something */
void Buttons() {
  #define buttonPressed LOW                             // When the button is pressed the input will be low, this is to remove the confusion this migth cause.
  uint32_t currentMillis = millis();                    // Millis times uses to debounce the button
  static uint32_t lastMillis[buttonCount];              // Start of the debounce timeout for each button
  const uint32_t bounceTimeout = 20;                    // Debounce time in milliseconds 
  bool currentButtonState[buttonCount];                 // Holds the current state of each button
  static bool lastButtonState[buttonCount];             // Holds the previous debounced state of the button
  uint8_t i;
  
  for (i = 0; i < buttonCount; ++i) {
    currentButtonState[i] = digitalRead(buttonPin[i]);        // Reads the current state of each button and saves the result
    if (lastButtonState[i] != currentButtonState[i]) {        // Checks to see if each button has been pressed or released, at this point each button has not been debounced
      if (currentMillis - lastMillis[i] >= bounceTimeout) {   // Checks to see if the state of each button has been stable for at least bounceTimeout duration
        lastButtonState[i] = currentButtonState[i];           // At this point the button has been debounced, so save the last state
        if (currentButtonState[i] == buttonPressed) {         // The button might have been pressed or released, this make sure only presses are acted on, not releases
          buttonPressedFlag[i] = true;                        // Button press has been detected and debounced, set a flag to indicate to the next function that some action can be taken
        }
      }
    } else {
      lastMillis[i] = currentMillis;                         // Saves the current value of millis in last millis so the debounce timer for each button starts from current millis
    }
  }
}

/* Selects one of the 4 functions buttonZero() etc depending on which button was pressed */
void doStuff() {
  uint8_t i;
  for (i = 0; i < buttonCount; ++i) {
    if (buttonPressedFlag[i]) {
      buttonPressedFlag[i] = false;                         // Clear the flag to ensure the action only happens once
      functions[i]();                                       // Calls one of the 4 functions depending on which button was pressed
    }
  }
}

// 4 functions, which are called depending on which button has been pressed


void buttonZero() {
  Serial.println("Button 0 has been pressed");    // Your code for when button 0 is pressed goes here
}

void buttonOne() {
  Serial.println("Button 1 has been pressed");    // Your code for when button 1 is pressed goes here
}

void buttonTwo() {
  Serial.println("Button 2 has been pressed");    // Your code for when button 2 is pressed goes here
}

void buttonThree() {
  Serial.println("Button 3 has been pressed");    // Your code for when button 3 is pressed goes here
}

Further reading

Millis
I have used millis() in every example for timing the debounce time out, you can find out more about
using millis for timing here.

Non-blocking code
All the examples I have presented use non-blocking code themselves and rely on the other code in the sketch also being non-blocking. You can learn how to write code that does several things at once in this tutorial.

Keypad data entry
I have not covered using a keypad for data entry, if you want to learn how to do that read the keypad data entry tutorial.

State machines
Some of my examples introduced the idea of a state machine. I've not gone into any detail as to do would be too far from what this tutorial is about, however there are some excellent tutorials about state machines and how to use them, here are two:
Nick Gammon on state machines
Mike Cook on state machines

Comments and feedback
Buttons and other electro-mechanical inputs (comments)

Credits
I owe a great deal to others for their help in writing this tutorial:
Idahowalker and Robin2 for reading my drafts and providing lots of helpful suggestions.
larryd for revising my original text and providing numerous suggestions for improvements.
BallscrewBob for his support in getting my tutorial from an idea to the finished work.

My thanks to all of them.