Single, Double, Long Button click with Interrupts

Hi all :slight_smile:

I’m implementing Push-Button functionality with Interupts.
I found a nice piece of code from Jeff Saltzam, which enables four output events: Single-Click, Double-Click and Long-Hold and works great will polling.

But when implemented with Interrupts, the double-click appears to work fine but the single and Long-Hold don’t work.

Any suggestions or has anyone implemented Button functionality with interrupts?

// Button timing variables
int debounce =20;
int double_time=250;
int hold_time = 1000;
int longhold_time = 2500;

//Button variables
boolean button_value = HIGH; // value read from button
boolean button_last = HIGH; // buffered value of the button's previous state
boolean double_waiting = false; // whether we're waiting for a double click (down)
boolean double_next = false; // whether to register a double click on next release, or whether to wait and click
boolean single_ok = true;  // whether it's OK to do a single click
long button_downtime = -1; // time the button was pressed down
long button_releasetime = -1;   // time the button was released
boolean button_ignoreup = false; // whether to ignore the button release because the click+hold was triggered
boolean button_waitforup = false;   // when held, whether to wait for the up event
boolean hold_event = false; // whether or not the hold event happened already
boolean longhold_event = false; // whether or not the long hold event happened already

void checkButton()
{
  int event = 0;
  button_value = digitalRead(button);
  

// Button pressed down
  if(button_value == LOW && button_last == HIGH && (millis()-button_releasetime) > debounce)
   {
    button_downtime = millis();
    button_ignoreup = false;
    button_waitforup = false;
    single_ok = true;
    hold_event = false;
    longhold_event = false;
    
    if((millis()-button_releasetime) < double_time && double_next == false && double_waiting == true)
     {
      double_next = true;
     }
    else
     {
      double_next = false;
      double_waiting = false;
      }
    }
 // Button released
 else if (button_value == HIGH && button_last == LOW && (millis()-button_releasetime) > debounce )
  {
   //if(button_ignoreup == false)
   if(button_ignoreup == false)
   {
     button_releasetime = millis();
     if(double_next == false)
      {
       double_waiting = true;
       }
     else
      {
       event = 2;
       double_next = false;
       double_waiting = false;
       single_ok = false;
      }
     }
   
   }

   // Test for normal click event
if ( button_value == HIGH && (millis()-button_releasetime) >= double_time && double_waiting == true && double_next == false && single_ok == true && event !=2 )
  {
  event = 1;
  double_waiting = false;
   }

//Test for Hold
if (button_value == LOW && (millis()- button_downtime) >= hold_time)
  {
  if (hold_event == false)
   {
    event = 3;
    button_waitforup = true;
    button_ignoreup = true;
    double_next = false;
    double_waiting = false;
    hold_event = true;
    
   }
  if(millis() - button_downtime >= longhold_time)
   {
    if(longhold_event == false)
     {
      event = 4;
      longhold_event = true;
     }
   }
}
  
  button_last = button_value;
}

The code you posted appears to not use interrupts. If you more or less paste it into an interrupt routine, it won’t work because the value of millis() is frozen when an ISR is running.

Here’s the ISR implementation:

void Class:: setup()
{
     pinMode(2,INPUT_PULLUP)
    attachInterrupt(0,isr,CHANGE);
}

void Class::IsrButton()
{
	Class::GetObj( 0 )->HandleButtonInterrupt();
}

void Encoder::HandleButtonInterrupt()
{
       checkButton();
}

First I though, I won’t work because of ‘‘millis()’’, but it apears the double-click works and single-click with a longer press as well.

I understand that ‘‘millis()’’ is dependent on interrupts but is there a way to use it within an ISR?
Like declare a variable in the beginning equal to millis() and use that instead.

example: unsigned long now = millis();
And replace ‘‘now’’ in the code instead of ‘‘millis()’’ ?

So, what limitiations does millis() have regarding to the Interrupt Service Routines if used in one?

Inside an ISR, millis() won't change, that's all.

The value returned by micros() changes. However you are advised against doing lengthy things in an ISR.

The piece of code below, works for the Double-Click.
For the Single-Click it requires a longer press and won’t triger if pressed very fast.

It’s operation is using Interrupts.
It is strange why this happens, any insight regarding this?

button_state = digitalRead( pinButton );
	
	// Button pressed down
	// REMARK: "LOW" indicates the button is pressed and "HIGH" indicates the button is not pressed
	if( button_state == LOW && button_lastState == HIGH && ( millis() - button_PressedTime ) > debounce )
	{
		
		button_PressedTime = millis();  //button is pressed, start the time count
		if( ( millis() - button_releasetime ) < doubleClick_waitTime && double_next == false ) 
		{
			double_next = true;
		}
		else
		{
			double_next = false;
		}

	}
// Button released
	else if ( button_state == HIGH && button_lastState == LOW && ( millis() - button_releasetime ) > debounce )
	{
		button_releasetime = millis();
		if( double_next == true )
		{

			button_event = DOUBLECLICK;
			double_next = false;

		}

	}
// Test for normal click event

	if ( button_state == HIGH && button_lastState == LOW  && ( millis() - button_PressedTime ) >= doubleClick_waitTime ) 
	{
		//event = 1;		//event is 1, thus the single-click event is enabled
		button_event = SINGLECLICK;

	}

If I remember correctly, Windows handles it by separating and distinguishing between physical and logical mouse clicks. A physical press of the button is simply "buttondown" and "buttonup". The system then works on those two messages, depending on timestamps, to generate "mouse double click".

So a double click is

"button down" "button up" "mouse double click" "button up"

If you had clicked slowly it would be

"button down" "button up" "button down" "button up"

Which means that the system is obliged to give you the button down message no matter what because it cannot know what you will do next. That also means that your code that handles clicks and dblclicks must know that every dblclick has already been preceded by a single click. That means effectively it is very hard to have the two messages doing very different things.

For a silly example suppose you want "click" to do "cut" and "dblclick" to do a "copy" - by the time you receive the double you have already "cut"...

So, I tried to see how ''millis()'' varies inside the ISR. I saved its value before and after the button press(which triggers and interrupt).

It appears, the value is incremented while the button is pressed BUT the value of millis() before and after is the same?

This means, that during the interrupt its value is not increasing, right?

I'd much appreciate an answer for the issue I'm having :)

Memedhe: This means, that during the interrupt its value is not increasing, right?

That's right.