Beginner needs help with debounce logic

The eventual project goal is to create an up / down counter (also known as a "totalizer") with a presettable integer start value, but before I get there I need to overcome a very basic problem: how to debounce simple pushbutton inputs.

I drew heavily from some example code, particularly

https://docs.arduino.cc/built-in-examples/digital/Debounce/

... but something is obviously wrong with my implementation of the debounce logic.

This code includes handling two different pushbutton inputs. The green pushbutton illuminates the green LED, no problem, but as you can see thousands of state changes occur because there is no debounce logic.

The red pushbutton incorporates the debounce logic... but the LED never gets illuminated.

The problem code lines are between 118 and 152.

Yes I am quite new at this, and I'm working in a vacuum with no one else to look at my code and tell me where I went astray.

Would someone kindly suggest what I'm missing?

Here is the project wokwi:

... and the code

// test file to include writing to / reading from EEPROM, blink the builtin LED without using Delay() 
// output to serial 
// read pushbutton inputs and illuminate red / green LEDs 

#include <LiquidCrystal_I2C.h>

#define I2C_ADDR    0x27
#define LCD_COLUMNS 20
#define LCD_LINES   2

LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);

#include <EEPROM.h>

// the following was copied from example EEPROM Read file
// initialize the starting address, a value (for each byte) and the count (unsigned long)

int memAddress = 0;
unsigned long memValue;
unsigned long count0 = 0x05;       // initial value to place in EEPROM
unsigned long count1 = count0 - 1; // initial value to place in EEPROM
unsigned long count2 = count0 - 2; // initial value to place in EEPROM
unsigned long count3 = count0 - 3; // initial value to place in EEPROM

// debounce values

unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 50;    // the debounce time; increase if the output flickers

// constants won't change. Used here to set a pin number:

const int ledOnboard = LED_BUILTIN;  // the board number of the LED pin
const int button1Pin = 8;   // the number of the pushbutton pin
const int button2Pin = 9;   // the number of the pushbutton pin
const int led1Pin = 2;      // the number of the red LED pin
const int led2Pin = 3;      // the number of the green LED pin

const long interval = 1000;  // interval at which to blink (milliseconds)

// Variables will change:

int led0State = LOW;          // led0State used to set the onboard LED
int led1State = LOW;          // led1State used to set the red LED
int led2State = LOW;          // led2State used to set the green LED
int button1State = LOW;       // variable for reading the pushbutton 1 status
int button2State = LOW;       // variable for reading the pushbutton 2 status
int lastButton1State = HIGH;  // previous state of button 1
int lastButton2State = HIGH;  // previous state of button 2


// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store

unsigned long previousMillis = 0;  // will store last time LED was updated

void setup() {

  // set digital pins as outputs for the LEDs:

  pinMode(ledOnboard, OUTPUT);
  pinMode(led1Pin, OUTPUT);
  pinMode(led2Pin, OUTPUT);

  // LCD Initialization
 
  lcd.init();
  lcd.backlight();

  // Print something

  lcd.setCursor(0, 0);
  lcd.print("Line 1");
  lcd.setCursor(0, 1);
  lcd.print("Line 2");

  Serial.begin(9600);

  // initialize the external pushbutton pins as input, and choose the internal pullup resistor:
  
  pinMode(button1Pin, INPUT_PULLUP);
  pinMode(button2Pin, INPUT_PULLUP);

// populate first four (long integer i.e. 4 byte) EEPROM storage locations
// use EEPROM.put if you want to explicitly write
// use EEPROM.update if you want to write only if previous contents will be changed - prevents wear...?

EEPROM.put(memAddress + 0 , count0);
EEPROM.update(memAddress + 0 , count0); 
EEPROM.put(memAddress + 4 , count1);
EEPROM.update(memAddress + 8 , count2);
EEPROM.update(memAddress + 12 , count3);


}

// define onboard LED toggle function - if onboard LED is off turn it on and vice-versa
// I think this could be written more concisely but the following code makes it obvious

void toggleLED() {
  if (led0State == LOW) {
    led0State = HIGH;
  } else {
    led0State = LOW;
  }
}

void loop() {

  // here is where you'd put code that needs to be running all the time.
  // serial echo

  if (Serial.available()) {       // If anything comes in Serial (USB)
    Serial.write(Serial.read());  // echo it back to Serial (USB)
  }

  // obtain the state of each button:

  button1State = digitalRead(button1Pin);
  button2State = digitalRead(button2Pin);

  // check each button for state change.
  // both button1Pin and button2Pin are configured to use the internal pullup
  // so their default state is HIGH
  // when pressed, they are connected to gnd, and the state becomes LOW

  if (button1State != lastButton1State)  {  // check for state change, due to noise or whatever
    lastDebounceTime = millis();            // reset the debounce timer
  }

   if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state

  if (button1State != lastButton1State)  {  // check for state change

  Serial.print("State change detected! ");  // THIS NEVER HAPPENS

    if (button1State == LOW) {             // was it pressed? LOW = pressed = turn LED on:

    digitalWrite(led1Pin, HIGH);

    count1++ ; 

    }
  }
  } else {
    // turn LED off:
    digitalWrite(led1Pin, LOW);

  }
    lastButton1State = button1State ;

  // same for button 2. 
  // this code does not apply any debounce logic


  if (button2State != lastButton2State) {
     if (button2State == LOW) {
// turn LED on:
    digitalWrite(led2Pin, HIGH);

    count2++; 
     }
   // turn LED on:
    digitalWrite(led2Pin, HIGH);
   count2++;
  } else {
    // turn LED off:
    digitalWrite(led2Pin, LOW);
  }

  // check to see if it's time to toggle the onboard LED; that is, if the difference
  // between the current time and last time you toggled the LED is larger than
  // the interval at which you want to toggle the LED.

  unsigned long currentMillis = millis();
  unsigned long currentSecs = currentMillis / 1000;

  if (currentMillis - previousMillis >= interval) {
    // save the last time you toggled the LED
    previousMillis = currentMillis;

    toggleLED();

// seconds elapsed time serial output

  Serial.print("Time ");
  Serial.print(currentSecs);
  Serial.print("\t");


// read a byte from the current address of the EEPROM

  memValue = EEPROM.get(memAddress, memValue);

// print the EEPROM memValue at memAddress

  Serial.print("Addr: ");
  Serial.print(memAddress);
  Serial.print(" ");
  Serial.print(memValue, HEX);
  Serial.print(" ");
  Serial.print("Count1: ");
  Serial.print(count1);    
  Serial.print(" ");
  Serial.print("Count2: ");
  Serial.print(count2);
  Serial.println();

  // set the onboard LED to the ledState of the variable:

  digitalWrite(ledOnboard, led0State);

  // Advance to the next address, when at the end restart at the beginning.

  memAddress = memAddress + 4;         // increment the memory address by 4 bytes

  if (memAddress >= EEPROM.length()) {
    memAddress = 0;
  }

 }
}

Ultimately, getting way ahead of myself, I would like those two pushbutton inputs to be used to increment or decrement the count one unit when pressed briefly, but "rapidly" when held down, and the longer the button is held down the more rapidly the count increases / decreases. It's like setting a digital clock, but the better kind that doesn't just advance at a fixed (slow) rate when holding a "set" button. It needs to go up or down. I would like to be able to a value anywhere between zero and about a million so I'll need to "accelerate" that rate of change, and holding the button depressed will do that.

A separate input (not yet incorporated) will be used to increment the "totalizer" count, which will be retained in EEPROM through reboots / power cycle. The value will be displayed on the LCD.

  • To de-bounce mechanical switches, all that’s needed is to scan them every 50ms at which time you look for a change in the switches state.

  • This looks odd.

if (button2State != lastButton2State) 
{
    if (button2State == LOW) 
    {   
       digitalWrite(led2Pin, HIGH);   // turn LED on:
       count2++; 
    }

    digitalWrite(led2Pin, HIGH);     // turn LED on:
    count2++;
} 


  • Format your code, place { and } on lines by themselves.

I’d suggest you check the code for one of the numerous button library such as Button in easyRun or OneButton or Toggle or EasyButton or Bounce2, ...

That should give you ideas !

Thousands?!? For each press?

That seems too many. I have never seen more than maybe 10~20 per press with no debounce logic.

look this over

// check multiple buttons and toggle LEDs

enum { Off = HIGH, On = LOW };

byte pinsLed [] = { 10, 11, 12 };
byte pinsBut [] = { A1, A2, A3 };
#define N_BUT   sizeof(pinsBut)

byte butState [N_BUT];

// -----------------------------------------------------------------------------
int
chkButtons ()
{
    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        byte but = digitalRead (pinsBut [n]);

        if (butState [n] != but)  {
            butState [n] = but;

            delay (40);     // debounce

            if (On == but)
                return n;
        }
    }
    return -1;
}

// -----------------------------------------------------------------------------
void
loop ()
{
    switch (chkButtons ())  {
    case 2:
        digitalWrite (pinsLed [2], ! digitalRead (pinsLed [2]));
        break;

    case 1:
        digitalWrite (pinsLed [1], ! digitalRead (pinsLed [1]));
        break;

    case 0:
        digitalWrite (pinsLed [0], ! digitalRead (pinsLed [0]));
        break;
    }
}

// -----------------------------------------------------------------------------
void
setup ()
{
    Serial.begin (9600);

    for (unsigned n = 0; n < sizeof(pinsBut); n++)  {
        pinMode (pinsBut [n], INPUT_PULLUP);
        butState [n] = digitalRead (pinsBut [n]);
    }

    for (unsigned n = 0; n < sizeof(pinsLed); n++)  {
        digitalWrite (pinsLed [n], Off);
        pinMode      (pinsLed [n], OUTPUT);
    }
}

My poor man's solution for debouncing is to simply add a small delay. For simple things it's sufficient. For more complex button handling I suggest you to use a library. I've had good experiences with the OneButton library that was mentioned in an earlier response.
A bit

Writing your own button switch debouncing routine is also a good exercise for a beginner.
The most common strategy is to watch for any change from the current state that has been stable for say 50mS and, if this condition is met, then update the current state and perform any action, say switching a led on etc. Usually this is best done in an non-blocking way.
There are other strategies also depending on the latency requirements and how electrically noisy the environment is.

@jg1 - welcome to the forum and kuudos on your first post: prose, code in tags and a wokwi, nice.

Agree. To that end, I have looked at the OP's code.

Please say where you found the algorithm you attempted to implement.

It looks like a common debounce pattern of code lines, but slightly "optimised", which wrecks the logic.

I am in transit and can't run this to ground efficiently, but for now, I suggest you to take a careful look at the source material or code that informed your effort. I think you will find you didn't quite nail this. Close.

Debouncing logic is subtle and it don't take much to break it.

This is why it gets coded around so often; ppl use libraries, some are good. Others use simple hacks or stick to simple obvious ideas.

But let us get the OP's original effort working.

a7

Hi @jg1 !

I modified one of my button classes in a way that you get “accelerated” returns of true from a function btn.down() while the button is pressed:

On Wokwi: https://wokwi.com/projects/453484771264915457

/*
   Forum: https://forum.arduino.cc/t/need-beginner-help-with-debounce-logic/1425853
   Wokwi: https://wokwi.com/projects/453484771264915457

   ec2021

   Example for reading buttons with "acceleration" of returned
   (btn.down() == True)


   AccelBtnClass provides the ability to set a minimum and maximum interval
   to receive true from the function btn.down() while the button is pressed.
   The time to return true is decremented by a given value "decrement" in btn.init(...).
   Once the button has been released the interval is reset to maxInterval
   again.


*/


constexpr byte buttonPin[] {2, 3};
constexpr int buttons = sizeof(buttonPin) / sizeof(buttonPin[0]);

class AccelBtnClass {
  private:
    byte pin;
    byte state;
    byte lastState;
    unsigned long lastChange;
    unsigned long lastPressTime;
    unsigned long lastCheckTime;
    unsigned long accelInterval;
    unsigned long minInterval;
    unsigned long maxInterval;
    unsigned long decrement;
  public:
    init(byte p, unsigned long minIntv, unsigned long maxIntv, unsigned long decr) {
      pin = p;
      pinMode(pin, INPUT_PULLUP);
      lastPressTime = 0;
      lastCheckTime = 0;
      minInterval = min(minIntv, maxIntv);
      maxInterval = max(minIntv, maxIntv);
      accelInterval = maxInterval;
      decrement     = decr;
    }
    unsigned long getLastPressTime() {
      return lastPressTime;
    };
    unsigned long getLastCheckTime() {
      return lastCheckTime;
    };
    void reset() {
      lastPressTime = 0;
    }
    boolean pressed() {
      byte actState = digitalRead(pin);
      if (actState != lastState) {
        lastChange = millis();
        lastState = actState;
      };
      if (actState != state && millis() - lastChange > 30) {
        state = actState;
        if (!state) lastPressTime = lastChange;
        return !state;
      }
      return false;
    }
    boolean down() {
      if (!digitalRead(pin)) {
        if (millis() - lastCheckTime >= accelInterval) {
          lastCheckTime = millis();
          if (accelInterval >= minInterval + decrement) {
            accelInterval -= decrement;
          }
          return true;
        } else {
          return false;
        }
      } else {
        accelInterval = maxInterval;
        return false;
      }
    }
};


AccelBtnClass button[buttons];

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < buttons; i++) {
    //      Pin No, minInterval, maxInterval, decrement
    button[i].init(buttonPin[i], 30, 330, 50);
  }
}

void loop() {
  doSomething();
}

void doSomething() {
  for (int i = 0; i < buttons; i++) {
    if (button[i].down()) {
      Serial.print("Button ");
      Serial.print(i);
      Serial.print(" down @ ");
      Serial.println(button[i].getLastCheckTime());
    }
  }

}

Hope it's of assistance for you ...
ec2021

@jg1 Here is your sketch with the debounce fixed.

I added another variabke as you were "overusing" the one state variable in your implementation.

The second variable tracks the state of the debouncer itself, which is different than the state of the button.

Look for my mark

//... 

in the comments, I think I tagged all the changes.

That was the big issue, and your "this never happens" comment aimed my attention.

There were other smaller issues around the overall structure.

Sry about deleting the comments. TBH I don't read or use comments too much, and here they just were visually in the way, so.

HTH

a7

If you want competent deep dives on this, pour yourself one and settle in with

and

both Gannsle and Gammon are worth knowing about, for all kindsa microprocessor stuff not just denouncing.

a7

You could try it like this?

-jim lee

@alto777 OMG that’s brilliant. Many thanks for looking over my code and implementing such a quick fix. Very impressive!

By way of explanation, the algorithm I was attempting to implement started with Debounce on a Pushbutton and I have no doubt I got lost along the way. It was frustrating attempting to find out what I was doing wrong on my own, and I am amazed how quickly you were able to decipher my code and the intent behind it.

More explanation — there is probably a more efficient or elegant way of doing what I seek, but yes I am trying to learn on my own, one step at a time.

But I don’t want to reinvent the wheel, so I sincerely appreciate @J-M-L suggesting I take advantage of those button libraries that probably do it a lot more elegantly. One or more of them is likely to offer a much simpler solution than my tortured code.

@PaulRB Yes… those thousands of state changes can be observed by momentarily pressing the green button and watching the resulting “count2” value on the serial output.

I know nothing about wokwi (except for the fact it’s cool!) but I assume that virtual component is intentionally designed to be “noisy” to illustrate a worst case situation. In actual practice I have never encountered a switch nearly that bad, but that’s ok because my goal here is to understand the logic involved and implement it in a way that can cope with less than ideal components.

I only found out about Wokwi yesterday. I’m astounded. That thing has already saved me a ton of time. I reached the point where I need to build things and ordered parts a couple days ago… they have yet to arrive.

@6v6gt yes, I experimented a bit with a 50 mS delay. It definitely mitigated the problem, but I got distracted with my frustration implementing a “real” debounce algorithm. I have future concerns as well, which brings me to @ec2021 suggestions of an “accelerating” algorithm. At first glance it looks promising. That ability to preselect the starting count value using up / down buttons that “accelerate” the count increase / decrease rate the longer they are pressed is my biggest concern. If someone already figured that out it will be marvelous. It’s hard enough for me to even explain!

@LarryD thank you for your formatting suggestions. It was becoming difficult for me to decipher my own code, so I’ll take all the help I can get!

@jimLee late edit… yes that looks promising too. A totalizer that uses up / down buttons to begin with a presettable value is what I’m seeking to implement.

Many thanks to everyone and I did not intent to omit anyone. Extremely grateful.

I'm sure there are hundreds of bounces when a button is pressed, but most Arduino are not fast enough to detect more than a few of them. Thousands? No way. Something else is going on here. It's like your edge-detection isn't working and the count is increasing at high speed as long as the button is pressed.

I think you can make three conclusions

  • not all code you find on the internet is good, or even works
  • Lotsa bad code comes from copying good code badly
  • It's hard to write code even when it is right in front of you

Harder to the extent that you are also bending it to your particular needs.

I recognized the "Limore Friedman" algorithm and as I said, your comment zeroed me in. If you trace the original with your finger, you will see exactly why the line you said never happened indeed would never, or maybe extreme rarely, happen. life is too short to look into that.

You can go a long way in pattern matching… I think that is the strongest skill I bring to this hobby. To read some code and be able to extract the part of it that does what I want, make it my own so to speak.

You might want to read the library code, and/or try a few of the other common simple denounce algorithms. Library code tends to be written at a higher level, so discerning the algorithm employed can be hard. But time spent just trying to read code you don't understand def pays off.

I use button libraries, among them my own not-suitable-for-publishing code. If it's one button I might just scribble out the code; another favorite dodge has been mentioned which is throttling the loop(), so it runs slow enough to miss any switch drama. This is often OK in sketches that don't do much that needs the higher speeds you are leaving off the table.

That can offend real programmers, who think deliberate slowing down of anything at any time is like a chef using deflavorants.

There is no need to use delay() for debouncing. I would not look at real algorithms for debouncing that include any calls to that function.

I don't much like interfaces with too few buttons and too many "long press", "double press" differences. Buttons are cheap enough, so. But if I was going to overuse the button, I would probably go with a library that claimed to take care of all that for me… and test the life out of it in the smallest sketch I could to make sure it delivered. Too many button libraries just suck, others are bad if you don't use them just right. Here the examples that come along for the ride when you install a library are invaluable. Code you neither wrote nor messed with first, a rule you could apply to any piece of hardware that is to become part of your project.

a7

@PaulRB you’re exactly right — maintaining a button press continued to increment the counter, so my edge detection wasn’t working to begin with.

I should have mentioned I copied that piece from State Change Detection (Edge Detection) for pushbuttons, demonstrating what @alto777 described as “good code copied badly” :grinning_face_with_smiling_eyes:

The fact de-selecting “bounce” in the wokwi pushbutton object made no difference was a clue. My OP includes my original code which I now regard as garbage not worth looking at any more.

@alto777 thanks again. I have a lot to learn but I’m learning quickly. It’s great to have help.

Yes I don’t like the idea of using delay() either. I’m far from a real programmer but it just seems like a crutch to me. Character flaw on my part I’m sure.

I don't much like interfaces with too few buttons and too many "long press", "double press" differences.

True… it can rapidly devolve into triple-press, press and hold, ad nauseam, and that just gets confusing. Fortunately my “totalizer” UI is going to be very simple: one on / off toggle switch corresponding to count / don’t count, and another three position spring return to center toggle for counter preset up / down. It’s the latter one with its “accelerating” feature that will prove interesting. @ec2021 ‘s Accel Button will get me started in the right direction. Its “acceleration” action is nearly perfect the way it is.

Here is an updated wokwi, essentially unchanged from yours. I merely applied the identical debounce logic to the second pushbutton and added some comments for reference:

Interestingly, decreasing the debounce delay to even five milliseconds works perfectly fine, so I was obviously way out in left field with my edge detection logic. Pressing either button as fast as humanly possible yields realistic results — perhaps four or five increments per second, max.

Mine uses no delay’s at all.

  • keep it simple
  • understand limitations
  • use more sophisitcated methods when needed

might find this helpful

can be dropped into the post #10 WokWi sim

// demonstrate
//    blink without delay()
//    debounce buttons w/o use of delay()
//    use of Serial
//    use of LCD
//    use of EEPROM

#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>

#define I2C_ADDR    0x27
#define LCD_COLUMNS 20
#define LCD_LINES   2

LiquidCrystal_I2C lcd (I2C_ADDR, LCD_COLUMNS, LCD_LINES);

// -------------------------------------
struct Cntr {
    const byte      PinLed;
    const byte      PinBut;
    const unsigned  EeAdr;
    const char     *desc;

    int             cnt;
    byte            butState;
}
cntr [] = {
#if (0)
    { 10, A1, 100,  "cntr-A" },
    { 11, A3, 102,  "cntr-B" },
#else
    {  2,  8, 100,  "cntr-A" },
    {  3,  9, 102,  "cntr-B" },
#endif
};
const int Ncntr = sizeof(cntr)/sizeof(Cntr);

// -------------------------------------
const byte PinLed = 7;

const unsigned long MsecDebounce = 50;
const unsigned long MsecDisplay  = 1000;

unsigned long msecBut;
unsigned long msecDisp;
unsigned long msec;

char s [90];

// -----------------------------------------------------------------------------
void cmds ()
{
    if (! Serial.available ())
        return;

    char buf [90];
    int n = Serial.readBytesUntil ('\n', buf, sizeof(buf)-1);
    buf [n] = '\0';

    if (strstr (buf, "clr"))  {
        for (int n = 0; n < Ncntr; n++)  {
            cntr [n].cnt = 0;
            EEPROM.put (cntr [n].EeAdr, cntr [n].cnt); 
        }
    }

    else if (strstr (buf, "save"))  {
        for (int n = 0; n < Ncntr; n++)
            EEPROM.put (cntr [n].EeAdr, cntr [n].cnt); 
    }
}

// -----------------------------------------------------------------------------
void chkButtons ()
{
    if (msec - msecBut < MsecDebounce)
        return;
    msecBut = msec;

    for (int n = 0; n < Ncntr; n++)  {
        byte but = digitalRead (cntr [n].PinBut);
        if (cntr [n].butState != but)  {
            cntr [n].butState  = but;
            msecBut            = msec;
            
            if (LOW == but)  {
                cntr [n].cnt++;
                digitalWrite (cntr [n].PinLed, ! digitalRead (cntr [n].PinLed));
            }
        }
    }
}

// -----------------------------------------------------------------------------
void display ()
{
    if (msec - msecDisp < MsecDisplay)
        return;
    msecDisp = msec;

    for (int n = 0; n < Ncntr; n++)  {
        sprintf (s, " %2d: %3d %s", n, cntr [n].cnt, cntr [n].desc);
        Serial.println (s);

        lcd.setCursor (0, n);
        lcd.print     (s);
    }

    digitalWrite (PinLed, ! digitalRead (PinLed));
}

// -----------------------------------------------------------------------------
void loop ()
{
    msec = millis ();

    cmds ();
    chkButtons ();
    display ();
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin   (9600);
    Serial.println ("ready");

    pinMode (PinLed, OUTPUT);

    for (int n = 0; n < Ncntr; n++)  {
        pinMode (cntr [n].PinLed, OUTPUT);
        pinMode (cntr [n].PinBut, INPUT_PULLUP);
        cntr [n].butState = digitalRead (cntr [n].PinBut);

        EEPROM.get (cntr [n].EeAdr, cntr [n].cnt); 
    }

    lcd.init      ();
    lcd.backlight ();
}