debouncing a switch on an interrupt pin

I am using pin 2 (or 3) on a Nano to sense if a momentary push button is pressed and increment a counter when sensed. The counter skips a beat here and there and on the serial plotter the values coming in DO skip. I suspect the switch is a real "scratchy" one with minor pulses that the interrupt is picking up.

How do I debounce a switch on an interrupt since delay() and millis() don't work on interrupts? I probably can do it with a resistor and capacitor to filter out the spikes but would rather filter with software. The switch won't be pushed often nor rapidly.

Bricoleur:
The switch won’t be pushed often nor rapidly.

Then don’t use an interrupt for it. There’s a debounce example in the IDE. There’s also the Bounce2 LIbrary. You could also write your own debouncer.

This is a common subject on these forums, so I did some tests. I connected a simple, cheap push to make button to a resistor and a power supply and connected an oscilloscope across the button. With quick presses I typically measured a closed time for the button of around 150ms or more, with a bounce time of around 125μs. Maybe different people using different buttons will get different times, but I’ll go with what I have.
In detecting a button press you are looking for an event that lasts at least 150ms, this is an absolute age in terms of processor time. If you program is not getting completely round its loop in 10ms or less then you need to be asking why, and looking to either improve the code or using a faster processor. To be sure you are dealing with bounce you need to check the button input at least twice per press event; the first time you see it’s pressed, the second time you see it’s still pressed so you act on the press. 3 checks would be even better. So, given that a button press lasts at least 150ms, even if your code is rather slow and takes 50ms per loop, it is still going to be able to check the button input at least twice per loop, possibly 3 times.
An interrupt is what you use when you need something to be dealt with faster than your loop time. If you have an event that cannot wait 5ms or 10ms or 50ms or however long your loop takes THEN you use an interrupt. However, an interrupt is steeling time from your loop, so an interrupt has to be over and done very quickly, preferably in a time period measured in microseconds and quite possibly in less than the 125μs bounce I measured with my cheap button. Maybe another button will have even longer bounce. You should not have an interrupt taking this amount of time away from the loop. An interrupt is the wrong tool for button inputs and if you use an interrupt to detect and de-bounce a button press you are asking for trouble.
You can find a tutorial for de-bouncing at https://www.arduino.cc/en/Tutorial/Debounce, or you can try the code I have included below.

/* Simple button debounce for 4 buttons. Increments a count and sends the updated count to the serial monitor once per button press */
/* Tested on an Uno */
/* Connect simple push to make buttons between 0V and pin 2, 0V and pin 3, 0V and pin 4 and between 0V and pin 5 */

#define noOfButtons 4     //Exactly what it says; must be the same as the number of elements in buttonPins
#define bounceDelay 20    //Minimum delay before regarding a button as being pressed and debounced
#define minButtonPress 3  //Number of times the button has to be detected as pressed before the press is considered to be valid

const int buttonPins[] = {2, 3, 4, 5};      // Input pins to use, connect buttons between these pins and 0V
uint32_t previousMillis[noOfButtons];       // Timers to time out bounce duration for each button
uint8_t pressCount[noOfButtons];            // Counts the number of times the button is detected as pressed, when this count reaches minButtonPress button is regared as debounced 
uint8_t testCount[noOfButtons];             //Test count, incremented once per button press

void setup() {
  uint8_t i;
  uint32_t baudrate = 115200;
  Serial.begin(baudrate);
  Serial.println("");
  Serial.print("Serial port connected: ");
  Serial.println(baudrate);
  for (i = 0; i < noOfButtons; ++i) {
    pinMode(buttonPins[i], INPUT_PULLUP);
    Serial.print("Testcount ");
    Serial.print(i);
    Serial.print(" = ");
    Serial.println(testCount[i]);
  }
}

void loop() {
  debounce();
  delay(10);     //Your other code goes here instead of this delay. DO NOT leave this delay here, it's ONLY for demonstration.
}

void debounce() {
  uint8_t i;
  uint32_t currentMillis = millis();
  for (i = 0; i < noOfButtons; ++i) {
    if (digitalRead(buttonPins[i])) {             //Input is high, button not pressed or in the middle of bouncing and happens to be high
        previousMillis[i] = currentMillis;        //Set previousMillis to millis to reset timeout
        pressCount[i] = 0;                        //Set the number of times the button has been detected as pressed to 0
      } else {
      if (currentMillis - previousMillis[i] > bounceDelay) {
        previousMillis[i] = currentMillis;        //Set previousMillis to millis to reset timeout
        ++pressCount[i];
        if (pressCount[i] == minButtonPress) {
          doStuff(i);                             //Button has been debounced. Call function to do whatever you want done.
        }
      }
    }
  }
}

// Function to do whatever you want done once the button has been debounced.
// In this example it increments a counter and send the count to the serial monitor.
// Put your own functions here to do whatever you like.
void doStuff(uint8_t buttonNumber) {
  ++testCount[buttonNumber];
  Serial.print("Button ");
  Serial.print(buttonNumber);
  Serial.print(" testcount = ");
  Serial.println (testCount[buttonNumber]);
}

If, however, you do want to continue using an interrupt, you can do the debouncing as follows.
When you get a button press which you accept, you record the value of millis(). This is the time of the last accepted button press. The next time the interrupt service routine is triggered, you check if sufficient time (say 50mS) has elapsed since the last accepted button press. If not, then you ignore it as a bounce. Otherwise accepted it and record the time as before.
You can use millis() in an ISR. what you can’t do is wait while it increments.

Thanks for the info on debouncing. I was just about to hook it up to my scope but I used it successfully and can increment a counter with the button presses. The part that I don't understand is how does the program know when I'll push the button? It doesn't. I'd have to litter the program with the debounce function- void debounce(); and if I have a delay(), which I will, the button push could be and most likely will be missed.

I even tried calling the debounce function - void debounce(); with an interrupt hoping it would analyze itself (!) but it didn't work.

Any ideas how to always look for a button press AND debounce?

https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

Note
Inside the attached function, delay() won’t work and the value returned by millis() will not increment. Serial data received while in the function may be lost.

See more at the link.

Why are you using use interrupts?

All you need to do is scan your switches every ~50 milli seconds and look for switch change in state.

Do you understand the above?

6v6gt:
If, however, you do want to continue using an interrupt, you can do the debouncing as follows.
When you get a button press which you accept, you record the value of millis(). This is the time of the last accepted button press. The next time the interrupt service routine is triggered, you check if sufficient time (say 50mS) has elapsed since the last accepted button press. If not, then you ignore it as a bounce. Otherwise accepted it and record the time as before.

I don't see how that works. Maybe the button doesn't bounce, so the very first detection is acceptable but there won't be another interrupt to check against, so the press will be missed. Maybe the button bounces for longer and for more times that expected, how do you know which is the last bounce? How do you know if the next time you see and interrupt if it is yet another bounce from the first press or the beginning of a new press?

PerryBebbington:
I don't see how that works. Maybe the button doesn't bounce, so the very first detection is acceptable but there won't be another interrupt to check against, so the press will be missed. Maybe the button bounces for longer and for more times that expected, how do you know which is the last bounce? How do you know if the next time you see and interrupt if it is yet another bounce from the first press or the beginning of a new press?

I think what he means is that if the current millis value is more than, say, 250 greater than the LastValue recorded, then you consider the interrupt to be a new valid button press, and you go ahead and process it. And you also update LastValue to equal the current millis value. On the next interrupt, if the new current millis is less than 250 greater than LastValue, you consider it to be bouncing, and you don't process it. But you do update LastValue to equal the new current millis value.

The result is that you process the first contact immediately, but you don't recognize another one until at least 250ms have passed without any bouncing. Extended bouncing is still ignored because every time you get a bounce, you start over counting. Any contact more than 250ms after the previous valid contact or bounce will be recognized as a new valid contact.

Sherman,

Thanks, that makes sense :slight_smile:

++Karma;

A few points:

  • 250ms is excessive. 25-50ms is probably sufficient.

  • Let’s say that we use 50ms as this “lockout period”. There is a (very, very, very small) chance that this interrupt-based technique will cause a problem. Assume that the final “bounce” has occurred and its millis time recorded. Now, if nothing happens until the user presses the button again and that happens between 4,292,967,296 and 4,294,967,346 ms later (50ms window), then that new press will be seen as a “bounce” from the press ~47 days ago and it will be ignored.

  • The real issue is that the loop() code needs to process the results from the button press as sensed by the interrupt anyway. Otherwise, nothing useful can come of the button press. So, you might as well just poll it in the loop() and be done with it. Every time I’ve seen someone post on the forum claiming that they need to use interrupts for buttons because the loop() was too slow, it turned out that their code was poorly structured and implemented.

I found a work around using an interrupt and setting a flag (buttonpush = 1; for example). I know it raises the angst level for using an interrupt but I still think it has a powerful use. I don’t have to monitor a switch until I need to and the information has been stored for me if it was pushed or not. I only look to see if the switch has been pushed every second or so and even then, sometimes I can’t because I’m in a delay. The flag takes care of the bouncing switch - it doesn’t care if there are extraneous pulses as it gets reset back to where it was (buttonpush =1;). I then set the flag to 0, use the information that the button was pushed and start again.

The interrupt is like an employee sitting there doing nothing. Why not use it? I could write a complicated routine to figure out how to sweep the floor but why not hand the interrupt a broom and have them go to it? It might not be elegant but I have a swept floor and my routine was simple.

PerryBebbington:
I don't see how that works. Maybe the button doesn't bounce, so the very first detection is acceptable but there won't be another interrupt to check against, so the press will be missed. Maybe the button bounces for longer and for more times that expected, how do you know which is the last bounce? How do you know if the next time you see and interrupt if it is yet another bounce from the first press or the beginning of a new press?

@ShermanP got it right, but I'll also add this explanation regarding button debouncing

In a normal (not electrically noisy environment), you can assume that any pulse was a consequence of a deliberate press of the button. Simply accept the first and reject any further pulses that happen to come in the next say 50 mS.

In an electrically noisy environment, however, it is different. You cannot assume that every pulse was derived from a deliberate press of the button. Then the debouncing is a bit more complex to implement because you then have to watch for a stable state (say looking for the button being pressed continuously for 25mS) to filter out spurious pulse.

Your comment looks like you were considering only the second case.

I only look to see if the switch has been pushed every second or so and even then, sometimes I can't because I'm in a delay

The solution is not to use delay()

Bricoleur:
I only look to see if the switch has been pushed every second or so and even then, sometimes I can't because I'm in a delay.

UKHeliBob:
The solution is not to use delay()

As I said in Reply #10 .... "poorly structured and implemented".

But, it is @Bricoleur's project. Most people find a noticeable delay between pushing a button and the subsequent action to be a poor user experience. But, if Bricoleur is OK with that, I guess that's all that matters.

Another method that comes in handy sometimes is to detachInterrupt() within the ISR itself. Then the Loop can attach it again after doing whatever processing and debouncing it wants to do, and it's ready to receive a new button press. I think I used this once for a rotary encoder - in the ISR you immediately switch the interrupt over to the other pin, which should be stable, so any bouncing on the original pin produces no interrupt.

UKHeliBob:
The solution is not to use delay()

It IS working for me and it's simple coding but you have piqued my curiosity. How would I not use a delay()? I'm turning on and off a group of sets of lights that needs a specific timing interval. Use millis() instead?
And by the way, thank you to every who chimed in on my problem!

Use millis() instead?

Yes. See Using millis() for timing. A beginners guide, Several things at the same time and look at the BlinkWithoutDelay example in the IDE.

It IS working for me

Maybe it is but now you are beginning to learn why it causes problems. If you were not using delay you would not have a problem with debouncing as your loop would not take ages and ages, it would take a few milliseconds.

You have piqued my curiosity

GOOOOOOODDDDD!!!!

Use millis() instead?

Yes.
Start with the blink without delay tutorial.

[EDIT] Bob beat me to it.

Thanks for the tip on using millis() for a delay. Two questions arose though

  1. What is the published measured speed of instructions of a processor? E.I. how fast can I toggle a pin or read a pin. The processor speed for an UNO, Atmel 328P, is up to 16MIPS throughput at 16MHz, but here must be some latency and feet shuffling. I'd like to know what's the smallest event I can control or read.

  2. In part 2, the millis() fade example, you have set the blinkLedPin as an output pinMode(blinkLedPin, OUTPUT); yet you read from it later. I thought a pin had to be an input in order to read it. Not true?