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
- What Could Go Wrong?
- Probing the Switch Signal Without an Oscilloscope
- Help from Others
- The Jack G. Ganssle Debounce Algorithm in an Arduino Sketch
- Debouncing with a Finite State Machine
- The mdButton Library
- Downloads
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.
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.
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.
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.
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.
- AWAIT_PRESS: Wait until the button is pressed.
- DEBOUNCE_PRESSb>: Wait until the button signal has settled down.
- AWAIT_RELEASE: Wait until the button is released.
- DEBOUNCE_RELEASE: Wait until the button signal has settled down.
- 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.
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.
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.