#373936 - 25/11/2022 00:18
Best practices for button debouncing?
|
carpal tunnel
Registered: 20/12/1999
Posts: 31600
Loc: Seattle, WA
|
I'm doing an LED strip lighting controller with an Arduino. It will have several pushbuttons to switch modes. I'm debouncing the buttons with code similar to the below code. This is Arduino code, but really my question is a general question that would apply to any programming language.
// Global variables
const int buttonPin3 = 19;
const unsigned long buttonDebounceMillis = 200;
volatile unsigned long buttonDebounce;
volatile bool lightsAreOn;
volatile int colorSelection;
// "Setup" is called once at Arduino bootup
void setup()
{
Serial.begin(115200);
buttonDebounce = millis();
pinMode(buttonPin3, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(buttonPin3), ProcessButton3, FALLING);
colorSelection = 4;
lightsAreOn = false;
}
// Pressing button 3 turns on the lights and randomizes the color pallete.
// "ProcessButton3" is called automatically when the hardware triggers an
// interrupt when the button state goes from high to low (pressed==low in
// this case because the button is configured with a built in pullup resistor).
void ProcessButton3()
{
if ((millis() - buttonDebounce) < buttonDebounceMillis)
{
// If the last time that a button falling happened very recently, interpret
// it as additional noise and return from the function without doing anything.
return;
}
else
{
// Accept this button press as valid. Update our debounce timer to the current time.
buttonDebounce = millis();
}
// Complete this button's tasks if the button press was valid.
lightsAreOn = true;
colorSelection = (int)random(1,5);
// Log the button press so that I can see whether the debounce worked.
Serial.println("Button 3 pressed.");
}
The code above works successfully to debounce the "press" of the button. However there is something very interesting that happens on the "release" of the button. Sometimes the release of the button has noise too, which causes the button to go low/high/low/high really fast. The initial low/high doesn't trigger the interrupt, but the next high/low does. If the user pressed and then released the button very briefly (within the 200ms debounce time) then the function still ignores the noise because the debounce code counts it within the 200ms range. But if the user presses the button and then releases it slowly, then there is a chance that the release of the button will cause the guts of the routine to unintentionally fire a second time during the release. I don't want to increase the 200ms debounce time. In fact, 200ms is still too slow. There will be several buttons on this controller, and some of them might do things where I want to press the button quickly multiple times in a row (such as turning the brightness up and down). Usually, the actual debounce noise happens at time scales well under 10ms, and really I'd love to set the value down that low. But I've found that if I want to get rid of the "release" noise too, then I have to increase the value to 500ms or more, so that the release is still within its debounce range when I release the button. Arduino's own example debounce code doesn't use interrupts, it just repeatedly polls the switch, but, I think their code might have the same problem (not sure): https://docs.arduino.cc/built-in-examples/digital/Debounce - My guess is that their code, since it depends on polling instead of hardware interrupts, will still have the problem but it will surface more rarely because the polling interval will be significantly slower than the hardware interrupt. Anyway, are there any "best practices" for handling this situation? I'm sure that keyboard key handlers do this all the time, and I'm wondering how they do it.
|
Top
|
|
|
|
#373937 - 25/11/2022 00:40
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 20/12/1999
Posts: 31600
Loc: Seattle, WA
|
Hmmmm. Ideas from this thread: https://arduino.stackexchange.com/questions/66761/debouncing-a-button-with-interruptInstead of triggering the interrupt on “falling”, I trigger the interrupt on “change”. Then I keep track of whether the button is being pushed down or being released. I debounce both states (pushing or releasing) and only perform the user’s desired activity when the button state has gone to “down” after the debounce. But not when it has gone to “up” after the debounce. If I’m not careful I could get into a stuck button state though.
|
Top
|
|
|
|
#373938 - 25/11/2022 01:04
Re: Best practices for button debouncing?
[Re: tfabris]
|
enthusiast
Registered: 11/01/2002
Posts: 211
Loc: Qc, Canada
|
There are a few libraries that do button handling and among other things, take care of debouncing. Search for "button" in the library manager. Button2 is the one I used in my last project
Edited by elperepat (25/11/2022 01:08)
_________________________
Patrick
|
Top
|
|
|
|
#373940 - 25/11/2022 01:35
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
Second try. This is what I have done in the past. No dangling timers either!
// Original code by Mark Lord; free for any use; no conditions.
#define BUTTON3_PIN 19
static bool lightsAreOn;
static int colorSelection;
// Get a non-zero timeout:
static inline long get_timeout (unsigned int t)
{
long m = millis() + t;
return m ? m : 1;
}
// Compare against current time, handling wraparound:
static inline bool time_after (long a, long b)
{
return (b - a) < 0;
}
#define time_before(a,b) time_after((b),(a))
struct button_s {
int pin;
bool pressed;
bool tmp_value;
long timer;
} button3 = {BUTTON3_PIN,0,0,0};
static bool debounce_button (struct button_s *b)
{
bool new_value = (digitalRead(b->pin) == LOW);
if (new_value != b->tmp_value) {
b->tmp_value = new_value;
b->timer = get_timeout(10);
return false; // no change (debouncing)
}
if (b->timer && time_after(millis(), b->timer)) {
b->timer = 0;
if (b->pressed != new_value) {
b->pressed = new_value;
return true; // button changed
}
}
return false; // no change (yet)
}
void loop ()
{
if (debounce_button(&button3)) {
lightsAreOn = button3.pressed;
if (lightsAreOn)
colorSelection = (int)random(1,5);
Serial.print("Pressed=");
Serial.println(button3.pressed ? "yes" : "no");
}
}
void setup ()
{
Serial.begin(115200);
pinMode(button3.pin, INPUT_PULLUP);
colorSelection = 4;
} not tested Tested. Works. Whenever I can code it myself in a small number of lines (eg. above), that's what I do. Too many Arduino libraries were written by amateurs, and I can do without all of those bugs.
Edited by mlord (25/11/2022 03:45)
|
Top
|
|
|
|
#373941 - 25/11/2022 02:06
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
..and here is a version of the same thing, with multiple buttons being handled simultaneously:
// Original code by Mark Lord; free for any use; no conditions.
#define BUTTON1_PIN 7
#define BUTTON2_PIN 8
#define BUTTON3_PIN 9
static bool lightsAreOn;
static int colorSelection;
// Get a non-zero timeout:
static inline long get_timeout (unsigned int t)
{
long m = millis() + t;
return m ? m : 1;
}
// Compare against current time, handling wraparound:
static inline bool time_after (long a, long b)
{
return (b - a) < 0;
}
#define time_before(a,b) time_after((b),(a))
struct button_s {
int pin;
bool pressed;
bool tmp_value;
long timer;
const char *name;
} buttons[]= {{BUTTON1_PIN,0,0,0,"FirstButton"},
{BUTTON2_PIN,0,0,0,"SecondButton"},
{BUTTON3_PIN,0,0,0,"ThirdButton"},
{-1,0,0,0,NULL}};
static bool debounce_button (struct button_s *b)
{
bool new_value = (digitalRead(b->pin) == LOW);
if (new_value != b->tmp_value) {
b->tmp_value = new_value;
b->timer = get_timeout(10);
return false; // no change (debouncing)
}
if (b->timer && time_after(millis(), b->timer)) {
b->timer = 0;
if (b->pressed != new_value) {
b->pressed = new_value;
return true; // button changed
}
}
return false; // no change (yet)
}
void loop ()
{
for (button_s *b = buttons; b->pin != -1; ++b) {
if (debounce_button(b)) {
Serial.print(b->name);
Serial.print("=");
Serial.println(b->pressed ? "on" : "off");
}
}
}
void setup ()
{
Serial.begin(115200);
for (button_s *b = buttons; b->pin != -1; ++b) {
pinMode(b->pin, INPUT_PULLUP);
}
}
Edited by mlord (25/11/2022 03:47)
|
Top
|
|
|
|
#373942 - 25/11/2022 02:22
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
Fixed a bug above. The idea of good debouncing is to continue the debounce interval until the button has stopped changing state for a specified period. Not to simply wait 200msecs and take the new value at the end of it all. So the code above is happy when the button has a stable state for at least 10msecs (or whatever you change that period to). It need not be a long time.
|
Top
|
|
|
|
#373943 - 25/11/2022 07:40
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 20/12/1999
Posts: 31600
Loc: Seattle, WA
|
Thanks so much, guys! Elperepat: I'll check out that button library, it looks like overkill for what I'm doing, but maybe I need that? For example it handles stuff like double and triple clicks. I didn't think I needed that, but the more I think about it, the more I think that sort of thing could be useful. Mark: The idea of good debouncing is to continue the debounce interval until the button has stopped changing state for a specified period. Not to simply wait 200msecs and take the new value at the end of it all. Yes, this exactly! Thanks so much for the code. I will try it out. Although your code polls in a loop for the button, I would need to convert it to work with interrupt-driven buttons and see how it does in that case. I've found that loop-polling the button detection is inherently less noisy than interrupt-driven buttons, but that basic concept of debouncing should work for both polling methods.
|
Top
|
|
|
|
#373945 - 25/11/2022 17:43
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
Whatever you do, pay attention to how the implementation (and/or library) uses timestamps ("millis()"). Nearly every Arduino example and library I've ever examined does it wrong, leaving the code susceptible to eventual timer wraparound which will yield false events -- either 25 or 49 days into the future.
Cheers
|
Top
|
|
|
|
#373946 - 25/11/2022 17:47
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
A simple test to demonstrate the debouncing, is to just load my sketch (above) into an Arduino, and use a jumper wire (grounded on one end) to tickle the appropriate "button" GPIO.
|
Top
|
|
|
|
#373951 - 26/11/2022 19:20
Re: Best practices for button debouncing?
[Re: mlord]
|
carpal tunnel
Registered: 20/12/1999
Posts: 31600
Loc: Seattle, WA
|
Indeed, I'm certainly concerned about the usage of millis() because I am aware of how a rollover could cause stuck buttons. Some implementations are smart so that the rollover could, at its worst, cause one noisy extra press, whereas, bad ones could cause completely stuck buttons requiring a device reset.
|
Top
|
|
|
|
#373953 - 26/11/2022 19:48
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 29/08/2000
Posts: 14496
Loc: Canada
|
The implementation above is "millis() proof". I also don't have a huge amount of confidence in any edge-interrupt based solutions. It is so easy to miss an edge..
|
Top
|
|
|
|
#373954 - 27/11/2022 10:46
Re: Best practices for button debouncing?
[Re: tfabris]
|
carpal tunnel
Registered: 13/07/2000
Posts: 4180
Loc: Cambridge, England
|
The problem with trying to implement "a button state is real iff it's constant for 10ms" solely in interrupt handlers, is that you can't do that with just GPIO interrupts -- you need a timer or tick interrupt too. Think about a button that goes down at 0ms, up at 800ms, down at 805ms, and up at 1200ms. It's only a t=815ms that you can know that you're dealing with two real presses and not a bounce -- but you don't get an interrupt at t=815ms because there's no transition then.
Peter
|
Top
|
|
|
|
#373955 - 28/11/2022 18:42
Re: Best practices for button debouncing?
[Re: peter]
|
carpal tunnel
Registered: 20/12/1999
Posts: 31600
Loc: Seattle, WA
|
Thanks, Peter. Yes, indeed that makes sense. As I'm seeing here, debouncing is hard work and full of surprises! I'm glad I brought it up here, because this is a great technical discussion. I knew I'd be talking to the right people. I'll bet it's been discussed here on the BBS previously as well (probably even outside the context of the empeg rotary encoder, too). The reason I wanted to use a hardware interrupt, instead of just polling the switch each time through the main loop, was because I wanted the button response on this controller to be super snappy. The code spends a lot of time in subroutines, sending data to the LED strands, and doing other things like color blending math, or looping through lists of LEDs. Though I'm doing my best to make sure that the main program loop doesn't get hung up, I was foreseeing a time when maybe the main loop might take a couple hundred milliseconds to come round. In the end, though, I have already given up on the hardware interrupts and gone back to loop polling, because it turns out that I need more total buttons than the Mega2560 will give me hardware interrupts for. Ultimately, I have switched to using the Button2 library that Elperepat linked. That library is amazing! It handles situations like long presses and hold-downs quite well. Those turned out to be things that I needed. Also, the library can interpret each of its behaviors differently for each button on my device, which also turns out to be something I needed. The library has been frequently updated over the last several years, and its most recent update was less than a month ago, so I feel like I can trust it. Its only drawback is that it depends on the main Loop function instead of button hardware interrupts, but, in my implementation it turned out that I couldn't use hardware interrupts in the end anyway. Thanks for that link, Elperepat! Mark, thanks so much for the debouncing code. It works well, and I only switched to the library because of my need for the complex special handling routines for long presses and hold downs. This has been excellent, guys, thanks so much.
|
Top
|
|
|
|
|
|