Debounced Button Reading - HELP!

Hi all! This is my first post… I have been reading for a while now and learning lots, but I have come across a problem that has me well and truly stumped. I hope someone can help…

I am trying to wire up a button to my Arduino which when I will press. Upon release of the button, the program should work out how long the button was pressed for, and then execute different functions based on this time.

The major problem I have is bouncing, so I have implemented some debounce code, which works well, but only on the button press. I seem to be falling down with the “how long was it pressed for?” part. Every approach I have taken seems to not work. Here is the code I have so far to show what I’m trying to do… I really hope someone can see the issue as I’m pulling my hair out! The code works in that the first function runs and the menu page is moved, but if I press and hold the second function to clear the page does not, and then the menu skips when it shouldn’t. Any ideas??

Thanks!

int buttonState;
int lastButtonState = LOW;
int ButtonPressTime;
int menu;


unsigned long lastDebounceTime = 0;
unsigned long DebounceDelay = 200;  
int PressHoldShort = 2000;

void setup() {
  pinMode(buttonPin, INPUT);

}

void loop() {
  int reading = digitalRead(MenuPin);


    if (reading != lastButtonState) {      
    
    lastDebounceTime = millis();          // reset the debouncing timer
    menuincrement = 1;
  } 
  
  if (reading == LOW && (millis() - lastDebounceTime) > DebounceDelay && (millis() - lastDebounceTime) < PressHoldShort  && menuincrement == 1) {    //compare current debounce time against value set
   
   menu++;
   
   menuincrement = 0;
      
   ResetState = reading;            
 
  }
  
  if (reading == LOW && (millis() - lastDebounceTime) > PressHoldShort && menuincrement == 1) {    //compare current debounce time against value set
    
   ResetState = reading;            
  
  clearstats();
  
  }

  if (menu == 14) menu = 1;
}

You are failing to split the “determine how long the button was pressed” and the decision making based on that.

As long as the button is still pressed you can not determine how long the button will be pressed in total. Aka, you can not preform an action until the button is also released. The exception is is the button is pressed longer than you specified for the longest pressed action.

So split the two. Make code to determine is the button was pressed (aka, i released again), store that time and set a flag. Next you can check what action to do.

And my tip to make debounce easy, grab a library like Bounce2 :slight_smile: But even then remember you van only decide it was a short press action after the button is released again.

  if (reading == LOW && (millis() - lastDebounceTime) > DebounceDelay && (millis() - lastDebounceTime) < PressHoldShort  && menuincrement == 1)      //compare current debounce time against value set

Whilst it is tempting to write big, compound tests like this it is much easier to debug if you write it as a series of separate tests because you are able to determine which one(s) return true when expected to, or not.

Personally I would take the test for how long the button had been pressed out of the compound test. Considering that you are dealing with a “short” button press of 2 seconds do you need to debounce the input at all ?

Save the value of millis() when the button becomes pressed and save it again when it becomes released. Subtract one from the other to determine how long the button has been pressed

Thanks both! Very much appreciate the input. I will split the code out and see where I get.

To the point of do I need it debouncing at all... I'm running at 96mhz and it seems to pick up a few debounces I think up to about 120ms. So the problem I face is that I'm struggling to figure out how to flag when the button is definitely pressed, and then when it is definitely unpressed, given that it will probably flicker on both events.

Can you please clarify what length of button press periods you are aiming to measure and how many different periods ?

If you were to depict what states you go through when you press the button and then release the button, this could look like this:

You start from being idle, you receive a press (so pin goes LOW because of the input pull-up), you enter a theoretical bouncing state for a certain time. If the pin goes back HIGH after that time it's a glitch, you can't consider the button was really pressed, but if the pin is still LOW, then you can consider the button to be pressed.
Then same goes upon release

The resulting code for this small state machine (with everything coded so that it's easy to read, no optimization / code factoring) could be:

enum : byte {BUTTON_IDLE, BUTTON_BOUCING_DOWN, BUTTON_PRESSED, BUTTON_BOUNCING_UP} buttonState;
enum buttonAction_t : byte {BUTTON_NOCHANGE, BUTTON_DOWN, BUTTON_GLITCH_DOWN, BUTTON_DOWN_CONFIRMED, BUTTON_UP, BUTTON_GLITCH_UP, BUTTON_UP_CONFIRMED};

const byte buttonPin = 7;
const unsigned long buttonTimeOut = 15; // ms

buttonAction_t testButton()
{
  static unsigned long startTime = 0;
  buttonAction_t result = BUTTON_NOCHANGE;

  byte bState = digitalRead(buttonPin);

  switch (buttonState) {

    case BUTTON_IDLE:
      if (bState == LOW) {
        startTime = millis();
        buttonState = BUTTON_BOUCING_DOWN;
        result = BUTTON_DOWN;
      }
      break;

    case BUTTON_BOUCING_DOWN:
      if (millis() - startTime >= buttonTimeOut) {
        if (bState == LOW) {
          result = BUTTON_DOWN_CONFIRMED;
          buttonState = BUTTON_PRESSED;
        } else {
          result = BUTTON_GLITCH_DOWN;
          buttonState = BUTTON_IDLE;
        }
      }
      break;

    case BUTTON_PRESSED:
      if (bState == HIGH) {
        startTime = millis();
        buttonState = BUTTON_BOUNCING_UP;
        result = BUTTON_UP;
      }
      break;

    case BUTTON_BOUNCING_UP:
      if (millis() - startTime >= buttonTimeOut) {
        if (bState == LOW) {
          result = BUTTON_GLITCH_UP;
          buttonState = BUTTON_PRESSED;
        } else {
          result = BUTTON_UP_CONFIRMED;
          buttonState = BUTTON_IDLE;
        }
      }
      break;
  }
  return result;
}

void setup()
{
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
}

void loop()
{
  buttonAction_t buttonState = testButton();
  switch (buttonState) {
    case BUTTON_NOCHANGE: break;
    case BUTTON_DOWN: Serial.println(F("BUTTON_DOWN")); break;
    case BUTTON_GLITCH_DOWN: Serial.println(F("BUTTON_GLITCH_DOWN")); break;
    case BUTTON_DOWN_CONFIRMED: Serial.println(F("BUTTON_DOWN_CONFIRMED")); break;
    case BUTTON_UP: Serial.println(F("BUTTON_UP")); break;
    case BUTTON_GLITCH_UP: Serial.println(F("BUTTON_GLITCH_UP")); break;
    case BUTTON_UP_CONFIRMED: Serial.println(F("BUTTON_UP_CONFIRMED")); break;
  }
}

UKHeliBob:
Can you please clarify what length of button press periods you are aiming to measure and how many different periods ?

Looking for:

  • short press (<1 second) for menu change
  • medium press (1>5 seconds) for partial screen reset
  • long press (>5 seconds) for full screen reset reset

The function called will then depend which menu page (1-13) the medium and long presses were recorded on. If the medium or long press are recorded then the page should not change.

Example (pseudo) code for Bounce2:

button.update();

//button handling
if(button.fell()){
  buttonPressedMillis = millis();
}

if(button.rose()){
  buttonPressedDuration = millis() - buttonPressedMillis;
}

//separate, button action
if(buttonPressedDuration){
  if(buttonPressedDuration < 1000){
    menuChange();
  }
  else if(buttonPressedDuration < 5000){
    partialScreenReset();
  }
  else{
    fullScreenReset();
  }
  
  //reset duration to only trigger once!
  buttonPressedDuration = 0;
}

Like I said, split the debounce (and maybe even the duration calculation) from the decision making. Especially, like you said, that can include other parameters as well (like menu item).

Ok thank you very much for your help! I am definitely getting somewhere, but am getting some weird results…

I have setup Bounce2 like this:

  pinMode( MenuPin, INPUT );
  // After setting up the button, setup the Bounce instance :
  menubutton.attach(MenuPin);
  menubutton.interval(80); // De-bounce time of x milliseconds

Then the main bit is below. I have added some screen display to show me what the button press duration is and I’m getting some odd results. Firstly, the screen shows up a number before I have released the button. Secondly, it seems to show the duration from the previous button press, so I’m one step out of phase… How is that happening??

   // Update the Bounce instance :
    menubutton.update();
    
    //button handling
    if(menubutton.fell()){
      buttonPressedMillis = millis();
    }
    
    if(menubutton.rose()){
      buttonPressedDuration = millis() - buttonPressedMillis;
    }
    
    //separate, button action
    if(buttonPressedDuration){
      if(buttonPressedDuration < 5000){
        if(millis()- LastMenuChange > 500){
        //menu++;
        LastMenuChange = millis();
            display.clearDisplay();
            display.setTextSize(1);
            display.setTextColor(WHITE);
            display.setCursor(30,0);
            display.println(buttonPressedDuration);
            display.display();
            delay(1000);
        }
      }
      else if(buttonPressedDuration >= 5000 &&buttonPressedDuration < 10000){
            display.clearDisplay();
            display.setTextSize(1);
            display.setTextColor(WHITE);
            display.setCursor(30,0);
            display.println(buttonPressedDuration);
            display.display();
            delay(1000);
        if (menu == 1)  cleartrip();
        if (menu == 11) clearstats();
        if (menu == 12) Best060 = 0;
    
      }
      else if(buttonPressedDuration >= 10000){
        if (menu == 1) clearODO();
        if (menu == 13) clearservice();
      }
      
      //reset duration to only trigger once!
      buttonPressedDuration = 0;
    }

Is your button connected between GND and using pull up? Or to Vcc and using a pull down? I did assume the more common GND with pull up method.

Also, get rid off ALL delays(). They will mess with stuff.

[edit]
Why are you introducing something like ‘LastMenuChange’?

      if(buttonPressedDuration < 5000){
        //stuff
      }
      else if(buttonPressedDuration >= 5000 &&buttonPressedDuration < 10000){

I did leave out the “buttonPressedDuration >= 5000” part with a reason :wink: Because the first part of the if, else if statement already checked for that. So at that point ‘buttonPressedDuration’ can never be < 5000 :wink:

450nick

I don't know if this helps any, but Martyn Currey has some good ideas.

http://www.martyncurrey.com/switching-things-on-and-off-with-an-arduino/

About a third of the way down the page is a heading called "Switch Bounce".
He tests for a press 3 times in succession and if all 3 readings are the same then assumes that it is a press and not bounce. I adopted his approach and get rock solid consistency for what I am doing.

Steve

If you just do 3 readings directly in sequence it will not be much of a debounce. It's all about how to determine the time passed between the same readings. There are multiple ways which can give you good result with pro's and cons. Good article about it on HackADay. But if you don't have specific requirement except if to work and be simple, most libraries will do. And in my opinion, especially in the "be simple" department Bounce2 is great.

I have the button connected as in this example: https://www.arduino.cc/en/tutorial/button

That is pull up right??

The delays were only temporary so I can read the screen - will delete all of that in the final code

LastMenuChange I put in as a test - without it, when pressing the button it seemed to often travel through more than 1 page - sometimes 2 or 3. So I raised the debounce interval to over 100 but the button became a bit slow to use then. So I experimented with a debounce of around 50 and a further argument to not move the page if it had already moved within say 250ms. Seemed to make it a little more stable.

I tried putting in the >= 5000 part because I was getting some odd results. If I pressed the button momentarily it would sometimes trigger the mid press case or the long press even with the durations set way high (40000).

450nick:
That is pull up right??

No, that's pull down. Still don't know why that crappy tutorial is up there. :confused: So that would explain some or the weirdness. fell() and rose() point to the electrical state of a pin, not the button state. A happy coincidence is that if corresponds to the button state as well if a normally open push button is wired with a pull up. Which is very common. :slight_smile:

If your circuit is still on a breadboard I would suggest to:

  • Remove the resistor
  • Place the button only between a pin and GND
  • call pinMode(pin, INPUT_PULLUP) in setup for that pin (or use button.attach(pin, INPUT_PULLUP) if using Bounce2).

450nick:
The delays were only temporary so I can read the screen - will delete all of that in the final code

Now would be a good time :wink: They will mess with the rest. And atm you only update after a button press (or at least that's what the snippets show and that's the trouble with snippets over a MCVE). So no need to randomly wait.

450nick:
LastMenuChange I put in as a test - without it, when pressing the button it seemed to often travel through more than 1 page - sometimes 2 or 3.

If the debounce is correct and you only cycle trough pages when a button became pressed/released (and not IF a button is pressed/or released) that should not happen. So remove it and fix the real problem :wink:

But I think for now, try to make a MCVE to get your head around everything. Drop the real menu's and just try to increment a "page" variable the way you like and trigger other prints with long and medium presses. Once you get the hang of it, it will be easy to implement in your project.

YOU HERO! The button was causing all of the problems... It works like a dream now, super pleased! I've been trying to figure that out for about 3 days. Thank you thank you thank you!

I'm glade to hear it worked out! :smiley: