Rotary Encoder Interrupt Double Click Detection

Hi,
I am working on a project that uses a rotary encoder to navigate a menu system. All of the code snippets I have found are for polling the button, but I am trying to use interrupts to save processor time.

Moving side to side on a given level of the menu by turning the knob works perfectly.

I want to use a single click to move down a menu level and a double click to move up a menu level, however I’m having limited success.

My though process is as follows:
When a click is detected check the time elapsed since the last click. If the time is less than a predetermined interval, then this is a double click.
Otherwise if the time elapsed since the last click is greater than the predetermined interval, then this is a single click

I am getting mixed results, sometimes a double click takes the menu up a level, other times it goes down a level and back up.

Here are the relevant code snippets:

#include <PinChangeInt.h> // necessary otherwise we get undefined reference errors.

int doubleClickInterval = 1000;
unsigned long currentMillis = 0;
unsigned long lastMillis = 0;

void setup(){
    pinMode(enterButton, INPUT);     //set the enterButton to input
    digitalWrite(enterButton, HIGH); //use the internal pullup resistor
    PCintPort::attachInterrupt(enterButton, buttonPress, RISING); // attach a PinChange Interrupt to our pin on the rising edge

}
void loop(){
    

}

void buttonPress()
{
    //pressCounter++;   // increment the counter  // used for debugging
    currentMillis = millis();   // check the time
  
    if ((currentMillis - lastMillis)<doubleClickInterval){ // double click
       menu.moveUp();
    }
    else if ((currentMillis - lastMillis) > doubleClickInterval){ // single click
      menu.moveDown();
    }
    lastMillis = currentMillis;   // save these for next time
    lastCounter = pressCounter;   //  a button press is detected
}

I am working on a project that uses a rotary encoder to navigate a menu system. All of the code snippets I have found are for polling the button, but I am trying to use interrupts to save processor time.

Push Buttons should not require an interrupt. If your code is not responsive to button input there are blocking operations and you should address those, rather than trying to implement an interrupt.

My though process is as follows: When a click is detected check the time elapsed since the last click. If the time is less than a predetermined interval, then this is a double click. Otherwise if the time elapsed since the last click is greater than the predetermined interval, then this is a single click

Your logic is faulty. The first click of a double click is likely to be >doubleClickInterval from the last time a click was sensed, and will be considered a single click. That's when

other times it goes down a level and back up.

Thanks for your reply.

Your logic is faulty. The first click of a double click is likely to be >doubleClickInterval from the last time a click was sensed, and will be considered a single click. That's when Quote other times it goes down a level and back up.

How would I remedy that?

Its more complicate than that - your state machine will need to debounce as well as detect double clicks, so you will need a DEBOUNCE_INTERVAL as well as a DOUBLE_CLICK_INTERVAL.

Learn to draw state-transition diagrams for state machines, its a very useful thing to have mastered.

Have a look at this blog article for an explanation of how to think about the problem https://arduinoplusplus.wordpress.com/2016/04/11/switches-as-user-input-devices/.

I would not be using interrupts. If you are not processing switches fast enough, look at what else seems to be taking time as it will cause you other problems in the long run.

cattledog: Push Buttons should not require an interrupt.

marco_c: I would not be using interrupts.

I'm curious why people are recommending to not use interrupts? I was under the impression that since the button gets used so little when the code is running that polling it each time through the loop would be a waste of processor cycles.

Switches bounce, sometimes several times very rapidly. Dealing with bounces is more difficult with interrupts than with polling.

Interrupts are generally reserved for fast events that need to be captured reliably. Anything that happens on a human scale (like a button press) is not fast.

Interrupt routines should also work quickly and have limitations on what they can do without screwing up other interrupts. So an interrupt routine is also not likely to be processing the consequences of the button press, but is usually setting a flag to say that the event has happened. The main loop will then check for this flag. At this point you may as well be check for the switch input during the loop in the first place.

And, as jremington pointed out, debouncing is a lot harder in the ISR. If you want to use an ISR for button presses you should also be debouncing using hardware - the signal needs to be clean or you will get multiple interrupts for each keypress.

I’m curious why people are recommending to not use interrupts?
I was under the impression that since the button gets used so little when the code is running that polling it each time through the loop would be a waste of processor cycles.

The processor gets really bored running around the loop at 16 Mhz with nothing to do. :slight_smile:

If you have speed concerns with digitalRead(); you can always use Direct Port Manipulation. It’s only one or two clock cycles to directly read the state of a pin.

digitalPin8_value= PIND & B00000001

I think that people recommend staying away from interrupts when not needed, is that they can be mishandled in a variety of subtle ways, and lead to problems.

Another important reason is that there are issues with debouncing mechanical buttons/switches in software when using the interrupts. Some of the standard techniques won’t work at all, and others may leave an extra interrupt queued up which will be executed.

Jeff Salzman has written some really nice code to deal with single click, double click, and long presses on buttons. http://forum.arduino.cc/index.php?topic=14479.0

Here is a demonstration version of his code with Serial output.

/* 4-Way Button:  Click, Double-Click, Press+Hold, and Press+Long-Hold Test Sketch

By Jeff Saltzman
Oct. 13, 2009

http://forum.arduino.cc/index.php?topic=14479.0

To keep a physical interface as simple as possible, this sketch demonstrates generating four output events from a single push-button.
1) Click:  rapid press and release
2) Double-Click:  two clicks in quick succession
3) Press and Hold:  holding the button down
4) Long Press and Hold:  holding the button for a long time 
*/

#define buttonPin 4        // digital input pin

// Button timing variables
int debounce = 50;          // ms debounce period to prevent flickering when pressing or releasing the button
int DCgap = 500;            // max ms between clicks for a double click event
int holdTime = 2000;        // ms hold period: how long to wait for press+hold event
int longHoldTime = 5000;    // ms long hold period: how long to wait for press+hold event

// Button variables
boolean buttonVal = HIGH;   // value read from button
boolean buttonLast = HIGH;  // buffered value of the button's previous state
boolean DCwaiting = false;  // whether we're waiting for a double click (down)
boolean DConUp = false;     // whether to register a double click on next release, or whether to wait and click
boolean singleOK = true;    // whether it's OK to do a single click
long downTime = -1;         // time the button was pressed down
long upTime = -1;           // time the button was released
boolean ignoreUp = false;   // whether to ignore the button release because the click+hold was triggered
boolean waitForUp = false;        // when held, whether to wait for the up event
boolean holdEventPast = false;    // whether or not the hold event happened already
boolean longHoldEventPast = false;// whether or not the long hold event happened already


void setup() {
   // Set button input pin
   pinMode(buttonPin, INPUT_PULLUP);
   Serial.begin(115200);
   Serial.println("Jeff Salzman Button Mult-Function Demo");
   Serial.println("http://forum.arduino.cc/index.php?topic=14479.0");
}

void loop() {
   // Get button event and act accordingly
   int b = checkButton();
   if (b == 1) Serial.println("single click");
   if (b == 2) Serial.println("double click");
   if (b == 3) Serial.println("press and hold");
   if (b == 4) Serial.println("long press and hold");
}

int checkButton() {    
   int event = 0;
   buttonVal = digitalRead(buttonPin);
   // Button pressed down
   if (buttonVal == LOW && buttonLast == HIGH && (millis() - upTime) > debounce)
   {
       downTime = millis();
       ignoreUp = false;
       waitForUp = false;
       singleOK = true;
       holdEventPast = false;
       longHoldEventPast = false;
       if ((millis()-upTime) < DCgap && DConUp == false && DCwaiting == true)  DConUp = true;
       else  DConUp = false;
       DCwaiting = false;
   }
   // Button released
   else if (buttonVal == HIGH && buttonLast == LOW && (millis() - downTime) > debounce)
   {        
       if (not ignoreUp)
       {
           upTime = millis();
           if (DConUp == false) DCwaiting = true;
           else
           {
               event = 2;
               DConUp = false;
               DCwaiting = false;
               singleOK = false;
           }
       }
   }
   // Test for normal click event: DCgap expired
   if ( buttonVal == HIGH && (millis()-upTime) >= DCgap && DCwaiting == true && DConUp == false && singleOK == true && event != 2)
   {
       event = 1;
       DCwaiting = false;
   }
   // Test for hold
   if (buttonVal == LOW && (millis() - downTime) >= holdTime) {
       // Trigger "normal" hold
       if (not holdEventPast)
       {
           event = 3;
           waitForUp = true;
           ignoreUp = true;
           DConUp = false;
           DCwaiting = false;
           //downTime = millis();
           holdEventPast = true;
       }
       // Trigger "long" hold
       if ((millis() - downTime) >= longHoldTime)
       {
           if (not longHoldEventPast)
           {
               event = 4;
               longHoldEventPast = true;
           }
       }
   }
   buttonLast = buttonVal;
   return event;
}