2021-01-04
md
Detecting Multiple Button Clicks in an Arduino Sketch
Push Button and LED on a Single GPIO Pin->

Firmware that I am writing for an ESP8266 based device (a Sonoff if you must know) needs to monitor a tactile push button for user input. I wanted a routine to return the number of times the button was pressed in quick succession or to indicate that the button had been pressed for a long time. Since this would be part of an Arduino sketch, it seemed obvious that it would have to be based on interrupts or else it would have to be implemented as a state machine that would be polled at each iteration of the sketch loop. I decided on the latter.

I sketched out a state machine which made eminent sense to me. Half hour of coding and testing and that should be it. It's not the first time that I "debounced" a switch. Hubris! Of course, it did not work out as expected. The test sketch would work for the most part. But every now and then a click would be missed or the count of the number of consecutive clicks would be off. Sometimes a double click would be reported as two single clicks. After playing around far too long with the various delays without much success; a closer inspection of what was going was warranted.

In the end, I have come up with what I judge to be a very simple to use push button library for Arduino. It has been tested with two ESP8266 boards and a UNO clone but it should work with any board. Many button libraries are available, and probably more than one would have been adequate. It's always a trade off, the time and probability of finding a good enough solution has to be weighed against the effort to create one. Had I not underestimated the time it took to programme the library and to write it up, I would have been more eager to search for a ready-made solution.

If the whole involved story about what was involved in finding the problem and then creating the library is of no interest, jump to the last section.

Table of content

  1. What Could Go Wrong?
  2. Probing the Switch Signal Without an Oscilloscope
  3. Help from Others
  4. The Jack G. Ganssle Debounce Algorithm in an Arduino Sketch
  5. Debouncing with a Finite State Machine
  6. The mdButton Library
  7. Downloads

What Could Go Wrong? toc

First off, I thought I might have made a mistake. If the GPIO pin connected to the switch was left floating that might explain the inconsistent behaviour. I was using a Wemos D1 Mini as Sonoff emulator, so I checked and there is a 10K pull up resistor connected to pin D3 (GPIO0) on the board. The Sonoff itself uses a 1K pull up resistor. That should not make a difference, but just to improve things, the pin mode was changed from INPUT to INPUT_PULLUP. Unfortunately, that did not resolve the problem.

After a while it became clear that the signal was bouncing when the button was released. That sort of made sense. Before, I had only used the button to toggle something on and off and that was done immediately after (debouncing) the high to low transition on the GPIO pin. But in this instance, because the length of the button press has to be measured to identify long presses and the release has to be found to measure the time delay before the next button press when counting consecutive button presses in quick succession, the possibility of a messy low to high transition has to be considered. In other words, I was debouncing the front edge of the button signal when pressed, I should have been debouncing both edges.

Probing the Switch Signal Without an Oscilloscope toc

Before modifying the sketch, I wanted to investigate what both transitions looked like. But how can that be done without an oscilloscope? Best I can do is to use the ESP itself. The following sketch reads the state of the button after it has been pressed in a tight loop and saves each state in a large array. It does the same thing when the button is released. When that is done, both arrays are scanned to check for bounces and some timing calculations are done.

/* * debounce */ #define BUTTON_PIN  0 // Sonoff tactile switch is GPIO 0 or D3 (3) on NodeMCU/D1 mini #define COUNT 6400 byte pressed[COUNT]; byte released[COUNT]; unsigned long startTime, stopTime, startTime2, stopTime2; int i; int bounceCount(byte (&a)[COUNT]) {  int count = 0;  for (int i = 1; i < COUNT; i += 1  ) {     if (a[i] != a[i-1]) { count += 1; }  }  return int (count/2); }   int indexToFirstBounce(byte (&a)[COUNT]) {  int wanted = a[0];  for (int i = 1; i < COUNT; i += 1  ) {     if (a[i] != wanted) { return i; }  }  return -1; }   int indexToEndOfLastBounce(byte (&a)[COUNT]) {  int wanted = a[0];  for (int i = COUNT-1; i >= 0; i -= 1  ) {     if (a[i] != wanted) { return i; }  }  return -1; }   void setup() {  Serial.begin (115200);  pinMode(BUTTON_PIN, INPUT_PULLUP);  digitalWrite(BUTTON_PIN, HIGH);  delay(500);  Serial.println("\n\nApplication started");  Serial.println("GPIO initialized");  Serial.printf("Button is %d\n", digitalRead(BUTTON_PIN)); } void loop() {  pressed[0] = 0;  released[0] = 1;  startTime = micros();  if (digitalRead(BUTTON_PIN) == LOW) {    for (i = 1; i < COUNT; i = i + 1) {      pressed[i] = digitalRead(BUTTON_PIN);    }    stopTime = micros();        while (digitalRead(BUTTON_PIN) == LOW) {      yield();      startTime2 = micros();    }      for (i = 1; i < COUNT; i = i + 1) {      released[i] = digitalRead(BUTTON_PIN);    }    stopTime2 = micros();    Serial.println();    if (COUNT <= 6400) {      Serial.print("\nPressed: ");      for (i = 0; i < COUNT; i = i + 1) {        if ((i % 80) == 0) { Serial.println();}        Serial.print(pressed[i]);      }        Serial.println();        Serial.print("\nReleased: ");      for (i = 0; i < COUNT; i = i + 1) {        if ((i % 80) == 0) { Serial.println();}        Serial.print(released[i]);      }        Serial.println();    }        long testTime = (stopTime - startTime);    double avgReadTime = (1.0*testTime)/COUNT;    long testTime2 = (stopTime2 - startTime2);    double avgReadTime2 = (1.0*testTime2)/COUNT;          Serial.printf("\n%d samples were taken after the button was pressed during %d microseconds.\n", COUNT, testTime);    Serial.printf("The state of the button was sampled every %.1f microseconds on average.\n", avgReadTime);          Serial.printf("\n%d samples were taken after the button was pressed during %d microseconds.\n", COUNT, testTime2);    Serial.printf("The state of the button was sampled every %.1f microseconds on average.\n", avgReadTime2);          Serial.println("\nBounces");      int bc = bounceCount(pressed);    Serial.printf("  On pressed: %d\n", bc);    if (bc > 0) {      Serial.printf("    Time to first bounce: %.1f microseconds.\n", indexToFirstBounce(pressed)*avgReadTime);          int ss = indexToEndOfLastBounce(pressed);      if (ss < 0) {        Serial.printf("    Had not settled after %d microseconds.\n", stopTime - startTime);      } else {          Serial.printf("    Time to steady state: %.1f microseconds.\n", ss*avgReadTime);      }    }    int bc2 = bounceCount(released);    Serial.printf("  On released: %d\n", bc2);    if (bc2 > 0) {      Serial.printf("    Time to first bounce: %.1f microseconds.\n", indexToFirstBounce(released)*avgReadTime);          int ss2 = indexToEndOfLastBounce(released);      if (ss2 < 0) {        Serial.printf("    Had not settled after %d microseconds.\n", stopTime2 - startTime2);      } else {          Serial.printf("    Time to steady state: %.1f microseconds.\n", ss2*avgReadTime2);      }    }  }   }

Most times, when pressing the button in a deliberate fashion the application shows no bounce at all. Here is part of the output displayed in the Arduino IDE serial monitor when that happens.

Pressed: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 ... 00000000000000000000000000000000000000000000000000000000000000000000000000000000 Released: 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 ... 11111111111111111111111111111111111111111111111111111111111111111111111111111111 6400 samples were taken after the button was pressed during 7369 microseconds. The state of the button was sampled every 1.2 microseconds on average. 6400 samples were taken after the button was pressed during 7447 microseconds. The state of the button was sampled every 1.2 microseconds on average. Bounces On pressed: 0 On released: 0

I was surprised to see bounces when releasing the switch seemed more likely than when pressing the button. This is just an impression, I did not do an exhaustive statistical study.

Pressed: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 ... 00000000000000000000000000000000000000000000000000000000000000000000000000000000 Released: 11111111111111101111111111111111001111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 ... 11111111111111111111111111111111111111111111111111111111111111111111111111111111 6400 samples were taken after the button was pressed during 7364 microseconds. The state of the button was sampled every 1.2 microseconds on average. 6400 samples were taken after the button was pressed during 7443 microseconds. The state of the button was sampled every 1.2 microseconds on average. Bounces On pressed: 0 On released: 2 Time to first bounce: 17.3 microseconds. Time to steady state: 38.4 microseconds.

And again but this time it took almost 3 times longer to get to a stable off (high) state.

Bounces On pressed: 0 On released: 2 Time to first bounce: 26.1 microseconds. Time to steady state: 106.6 microseconds.

This is definitely not a consistent result. The time to a stable open state can take considerable longer, more than an order of magnitude longer, almost 2.5 millisecond.

3200 samples were taken after the button was pressed during 3681 microseconds. The state of the button was sampled every 1.2 microseconds on average. 3200 samples were taken after the button was pressed during 3720 microseconds. The state of the button was sampled every 1.2 microseconds on average. Bounces On pressed: 0 On released: 4 Time to first bounce: 1.2 microseconds. Time to steady state: 2419.2 microseconds.

Bouncing does occur on pressing the button, usually by hitting it off centre. But that seemed rarer.

9600 samples were taken after the button was pressed during 11044 microseconds. The state of the button was sampled every 1.2 microseconds on average. 9600 samples were taken after the button was pressed during 10204 microseconds. The state of the button was sampled every 1.1 microseconds on average. Bounces On pressed: 1 Time to first bounce: 85.1 microseconds. Time to steady state: 150.7 microseconds. On released: 0

And again.

6400 samples were taken after the button was pressed during 6881 microseconds. The state of the button was sampled every 1.1 microseconds on average. Bounces On pressed: 1 Time to first bounce: 95.7 microseconds. Time to steady state: 160.2 microseconds.

These tests were done with a 12x12mm switch, the ubiquitous orange, black and silver model, but the Sonoff has the much smaller black and silver 6x6 mm switch. These can be seen on the image. The Sonoff switch has a much longer (17 mm) push rod which makes is easy to press on the key in all sorts of "incorrect" ways, especially when the board is outside the case as during the tests shown below.

Pressed: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 ... 00000000000000000000000000000000000000000000000000000000000000000000000000000000 Released: 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111110000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 ... 11111111111111111111111111111111111111111111111111111111111111111111111111111111 6400 samples were taken after the button was pressed during 7364 microseconds. The state of the button was sampled every 1.2 microseconds on average. 6400 samples were taken after the button was pressed during 7444 microseconds. The state of the button was sampled every 1.2 microseconds on average. Bounces On pressed: 0 On released: 1 Time to first bounce: 250.8 microseconds. Time to steady state: 825.8 microseconds.

The following was a shocker.

Pressed: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 ... 00000000000000000000000000000000000000000000000000000000000000000000000000000000 Released: 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111110000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111111111111111111111111111111111111111111111111111111111111111111111 11111111111111100000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000 6400 samples were taken after the button was pressed during 7365 microseconds. The state of the button was sampled every 1.2 microseconds on average. 6400 samples were taken after the button was pressed during 7448 microseconds. The state of the button was sampled every 1.2 microseconds on average. Bounces On pressed: 0 On released: 1 Time to first bounce: 138.1 microseconds. Time to steady state: 7446.8 microseconds.

Clearly, there were at least two bounces when the key was released, not one, and the steady state had not yet been reached after 7 milliseconds. That prompted using a longer sampling period, but a similar result was not to be had, of course.

12800 samples were taken after the button was pressed during 14725 microseconds. The state of the button was sampled every 1.2 microseconds on average. 12800 samples were taken after the button was pressed during 13607 microseconds. The state of the button was sampled every 1.1 microseconds on average. Bounces On pressed: 1 Time to first bounce: 80.5 microseconds. Time to steady state: 142.6 microseconds. On released: 0 Bounces On pressed: 1 Time to first bounce: 65.6 microseconds. Time to steady state: 180.6 microseconds. On released: 0 Bounces On pressed: 0 On released: 0 Bounces On pressed: 0 On released: 0 Bounces On pressed: 1 Time to first bounce: 88.7 microseconds. Time to steady state: 201.5 microseconds. On released: 2 Time to first bounce: 1.2 microseconds. Time to steady state: 2010.2 microseconds.

Again the results are definitely not constant. Furthermore, none of the timing results should be considered accurate. I used an average value for the length of time to execute one iteration of the loop routine. But my testing of the ESP8266 showed that there are considerable differences between the length of time spent in the loop overhead. And that remains true if Wi-Fi is turned on or off as far as I can tell. I could investigate this subject at length, but at this juncture I do not think it would not be very fruitful because this is a question of microseconds while the switch debouncing times are in the order of milliseconds.

Help from Others toc

So what would be a good debouncing strategy? How long does it take for the signal to be stable after a switch is activated? Someone must have studied this topic. I remember reading discussions on the need to debounce switches which contained oscilloscope traces. A search of the Internet returned quite a few posts of that type but most were not that informative, or to be more precise, most did not provide information that I had not seen before.

An exception did turn up, A Guide to Debouncing (2004-2008) by Jack G. Gannsle. He is an author, I have cited before (in my post ). His discussion of watchdogs had been one of the most insightful I had read. It's the same for this topic. He actually tested each of 18 switches 300 times, logging the min and max amount of bouncing for both closing and opening of the contacts. Talk about mind-numbingly boring!. The emphasis on opening was mine; the author looked just as carefully at the behaviour of the switches when they were released as when they were pressed.

It looks very much like what I thought I observed is not uncommon. The bouncing occurring when switches A, C, E, G, and Q were released was significant and wondrous in its diversity. Quite a few of the switches seemed to have longer periods of bounce after a release, including switches I and J which look like the types of switches I tested. Furthermore, the software debouncing routine he proposed had a debouncing time after a button release that was an order of magnitude greater than after a button press. No wonder my initial routine was getting into trouble, its debouncing time for a button release was infinitely shorter than for the button press.

The range of results found is startling. The worst case was a switch that took once took 157 ms, (1/6 of a second) to settle down once released. When pressed it would fall to a steady 0 volts in under 20 microseconds in all cases (much faster than the ~1 ms resolution of my test rig). The average time it took for the signal to settle down was 1.56 ms the worst, after eliminating two outliers, was 6.2 ms.

Jack Gannsle's debouncing algorithm has a 10 millisecond debouncing time on a key press and 100 ms on the following key release. Here is the suggested routine. The external boolean function RawKeyPressed must return true when the key is closed.

#define CHECK_MSEC 5 // Read hardware every 5 msec #define PRESS_MSEC 10 // Stable time before registering pressed #define RELEASE_MSEC 100 // Stable time before registering released // This function reads the key state from the hardware. extern bool_t RawKeyPressed(); // This holds the debounced state of the key. bool_t DebouncedKeyPress = false; // Service routine called every CHECK_MSEC to // debounce both edges void DebounceSwitch1(bool_t *Key_changed, bool_t *Key_pressed) { static uint8_t Count = RELEASE_MSEC / CHECK_MSEC; bool_t RawState; *Key_changed = false; *Key_pressed = DebouncedKeyPress; RawState = RawKeyPressed(); if (RawState == DebouncedKeyPress) { // Set the timer which allows a change from current state. if (DebouncedKeyPress) { Count = RELEASE_MSEC / CHECK_MSEC; } else { Count = PRESS_MSEC / CHECK_MSEC; } } else { // Key has changed - wait for new state to become stable. if (--Count == 0) { // Timer expired - accept the change. DebouncedKeyPress = RawState; *Key_changed=true; *Key_pressed=DebouncedKeyPress; // And reset the timer. if (DebouncedKeyPress) { Count = RELEASE_MSEC / CHECK_MSEC; } else { Count = PRESS_MSEC / CHECK_MSEC; } } } }

When called, DebounceSwitch1 will return the current debounced state of the button in Key_pressed. Most times the returned value in Key_changed will be false; it will only be true if there was a change from off to on or vice versa.

The J. G. Ganssle Debounce Algorithm in an Arduino Sketch toc

It is not too difficult to adjust Jack's function to work in an Arduino sketch where it will be called at irregular intervals at each loop iteration instead of being called at constant 5 millisecond intervals. Replacing the counter with calculations of elapsed time will do.

#include "jgButton.h" #define PRESS_MSEC 10 // Stable time before registering pressed #define RELEASE_MSEC 100 // Stable time before registering released // This holds the GPIO pin and debounce delays int Pin = 0; long PressDelay = PRESS_MSEC; long ReleaseDelay = RELEASE_MSEC; void SetupDebounce(int pinValue, long PressDelayValue, long ReleaseDelayValue) { Pin = pinValue; PressDelay = PressDelayValue; ReleaseDelay = ReleaseDelayValue; pinMode(Pin, INPUT_PULLUP); digitalWrite(Pin, HIGH); } // This holds the debounced state of the key and // the time it was last changed. boolean DebouncedKeyPress = false; unsigned long eventTime; // Service routine called every CHECK_MSEC to // debounce both edges void DebounceSwitch(boolean *Key_changed, boolean *Key_pressed) { static long Delay = ReleaseDelay; byte RawState; *Key_changed = false; *Key_pressed = DebouncedKeyPress; RawState = !digitalRead(Pin); if (RawState == DebouncedKeyPress) { // Set the timer which allows a change from current state. if (DebouncedKeyPress) { Delay = ReleaseDelay; } else { Delay = PressDelay; } eventTime = millis(); } else { // Key has changed - wait for new state to become stable. if ( (millis() - eventTime) > Delay ) { // Timer expired - accept the change. DebouncedKeyPress = RawState; *Key_changed=true; *Key_pressed=DebouncedKeyPress; // And reset the timer. if (DebouncedKeyPress) { Delay = ReleaseDelay; } else { Delay = PressDelay; } eventTime = millis(); } } }

Of course the RawKeyPressed function had to be replaced. Because it must return true (anything other than 0) when the key is pressed, the negative of the value read on the button pin is returned. Recall that the switch is normally open, and when it is pressed it grounds the pin which is a value of 0 or false. Testing was done with a simple sketch.

#include "jgButton.h" void buttonModule(void) { boolean keyChanged; boolean keyPressed; DebounceSwitch(&keyChanged, &keyPressed); if (keyChanged) { if (keyPressed) { Serial.println("Key up"); } else { Serial.println("Key down"); } } } void setup() { Serial.begin (115200); Serial.println("\n\nApplication started"); SetupDebounce(0, 10, 30); Serial.println("setup() completed"); } void loop() { buttonModule(); }

Results were much better with a release debounce time of 30 ms. At 110 ms of total debounce time the algorithm could not keep up with rapid successive key presses. The article actually said on page 18 that fast typists could attain a rate superior to 10 keys a second. And of course, pressing the same key repetitively can be done even faster.

Debouncing with a Finite State Machine toc

It should be possible to record the time the key state changed to down and then back to up so that long presses could be detected and the time between presses could be calculated to identify the number of consecutive button clicks. While this function could be the base of a button library with the functionality that is needed, I decided to use another approach: a finite state machine.

Before going further, I should mention that while it might not be obvious, the Jack Ganssle algorithm implements a simple finite state machine where there are only two states, the key is up There are only two states: the key is down (DebouncedKeyPress = true) and the key is up (DebouncedKeyPress = false). The only other information that must be preserved by the state machine is the time at which a change in the raw state of the key was detected eventTime.

There are five states in the machine I have created. Their names are pretty self-explanatory.

  1. AWAIT_PRESS: Wait until the button is pressed.
  2. DEBOUNCE_PRESSb>: Wait until the button signal has settled down.
  3. AWAIT_RELEASE: Wait until the button is released.
  4. DEBOUNCE_RELEASE: Wait until the button signal has settled down.
  5. AWAIT_MULTI_PRESS: Wait for a follow-up button press in quick succession.

The state machine is queried with an integer valued status function that can return

-1 The button has been released after being held a "long time".
0 The button has yet to be pressed or a press has yet to be completed.
1 The button has been pressed once (for less than a "long time") and it is an isolated press.
n, where n > 1, the button has been pressed n consecutive times in quick succession.

If a sequence of button presses in quick succession is a followed rapidly by a button press held down for a long period, only the latter will be reported and the sequence will be ignored.

In addition to the state and the event times, the state machine must preserve the number of consecutive clicks of the button. Aside from the greater number of states, that is about the only complication compared with Jack Ganssle's routine.

The mdButton Library toc

Since it is entirely conceivable the more than one button could be connected to a device and since these could very have different mechanical, it made sense to implement the debounce algorithm as an object.

This is getting repetitious, but I am a neophyte when it comes to C and I am even more clueless with regards to C++. This is the first C++ object I have ever crafted so I present it, girding up my loins in advance of all criticisms and hoping that helpful comments could improve it.

Here is the header file.

#ifndef mdButton_h #define mdButton_h #include "Arduino.h" #define DEFAULT_ACTIVE LOW #define DEFAULT_DEBOUNCE_PRESS_TIME 15 #define DEFAULT_DEBOUNCE_RELEASE_TIME 30 #define DEFAULT_MULTI_CLICK_TIME 400 #define DEFAULT_HOLD_TIME 2000 class mdButton { public: mdButton(uint8_t pin, bool active = DEFAULT_ACTIVE); //constructor // Set attributes void setDebouncePressTime(uint16_t value); void setDebounceReleaseTime(uint16_t value); void setMultiClickTime(uint16_t value); void setHoldTime(uint16_t value); // status, number of clicks since last update // -1 = button held, 0 = button up, 1, 2, ... number of times button clicked int status(); private: uint8_t pin_; bool active_; uint16_t debouncePressTime_ = DEFAULT_DEBOUNCE_PRESS_TIME; uint16_t debounceReleaseTime_ = DEFAULT_DEBOUNCE_RELEASE_TIME; uint16_t multiClickTime_ = DEFAULT_MULTI_CLICK_TIME; uint16_t holdTime_ = DEFAULT_HOLD_TIME; // State variables long eventTime_; enum buttonState_t { AWAIT_PRESS, DEBOUNCE_PRESS, AWAIT_RELEASE, DEBOUNCE_RELEASE, AWAIT_MULTI_PRESS } buttonState_; int clicks_; }; #endif

In version 0.3 of the library, the active parameter was added to the constructor. By default it is set to LOW which means that when the push button is pressed it connects the GPIO pin to ground. This is what was assumed in the previous versions of the library. But it is now possible to use the library with a push button that will connect Vcc when pressed. In version 0.4 the enumerated type buttonState_t was moved to the private part of the mdButton object.

The source code is just as short.

#include "mdButton.h" mdButton::mdButton(uint8_t pin) { pin_ = pin; pinMode(pin_, INPUT_PULLUP); digitalWrite(pin_, HIGH); buttonState_ = AWAIT_PRESS; } //Set attributes void mdButton::setDebouncePressTime(uint16_t value){debouncePressTime_ = value;} void mdButton::setDebounceReleaseTime(uint16_t value){debounceReleaseTime_ = value;} void mdButton::setMultiClickTime(uint16_t value){multiClickTime_ = value;} void mdButton::setHoldTime(uint16_t value){holdTime_ = value;} int mdButton::status(void) { if (buttonState_ == AWAIT_PRESS) { clicks_ = 0; if (digitalRead(pin_) == LOW) { buttonState_ = DEBOUNCE_PRESS; eventTime_ = millis(); } } else if (buttonState_ == DEBOUNCE_PRESS) { if ((millis() - eventTime_) > debouncePressTime_) { buttonState_ = AWAIT_RELEASE; eventTime_ = millis(); } } else if (buttonState_ == AWAIT_RELEASE) { if (digitalRead(pin_) == HIGH) { if ((millis() - eventTime_) > holdTime_) { clicks_ = -1; } buttonState_ = DEBOUNCE_RELEASE; eventTime_ = millis(); } } else if (buttonState_ == DEBOUNCE_RELEASE) { if ((millis() - eventTime_) > debounceReleaseTime_) { if (clicks_ < 0) { buttonState_ = AWAIT_PRESS; return -1; } clicks_ += 1; buttonState_ = AWAIT_MULTI_PRESS; eventTime_ = millis(); } } else { // (buttonState == AWAIT_MULTI_PRESS) if (digitalRead(pin_) == LOW) { buttonState_ = DEBOUNCE_PRESS; eventTime_ = millis(); } else if ((millis() - eventTime_) > multiClickTime_) { buttonState_ = AWAIT_PRESS; return clicks_; } } return 0; }

To my way of thinking, it would be difficult to have something simpler to use.

It may be necessary change the default time attributes to handle different types of switches or even differences in individual switches of the same type. Jack Ganssle reports that a ratio of 1 to 2 in bounce times is not uncommon with switches of the same type. There are methods to do that and typically they would be invoked in the setup() part of the sketch.

There is a small sketch called tune.ino to marginally help in finding the best time attributes. There is no intelligence to it, timing values have to be plucked out of the air, as it were, and tested. However, searching in a systematic way does help in finding appropriate values in a reasonable amount of time.

Downloads toc

To repeat, this is a first attempt at creating an Arduino library. I only offer it here as a starting point for someone who would want to do something better. Other items discussed above are also available.

mdPushButton Library
A second version (2021-01-04) of the Arduino library suitable for installation with the library manager in the IDE. This newer version adds callback functions and better debugging routines for the state machine. The archive contains three example sketches plus the above mentionned sketch (renamed push_button_tune.ino) to help in setting the delay times for a given switch.
mdButton.zip (2021-01-04)
The original Arduino library suitable for installation with the library manager in the IDE. It contains two example sketches including tune.ino .
jgButton.zip
The Arduino version of the Jack Ganssle software debounce algorithm.
debounce.ino
The Arduino sketch to report the state of the signal from a pressed and then released button.
Push Button and LED on a Single GPIO Pin->