skip to Main Content

We interrupt this program to bring you something of key importance ;)

TL:dr “Some Arduino / C++ code to handle switch activations & calculate press durations using interrupts”

I spent almost the entire last weekend trying to get a piece of Arduino code working properly for a project. It sounds like a simple enough thing; I want the program to start when I press a button the first time – stop when I press the same button a second time – and reset when I hold the button down for more than a couple of seconds. And I want it to use interrupts.

In microcontroller / microprocessor / microcomputer parlance an interrupt is a bit like a fire alarm; it stays quiet and out of the way leaving you free to go about your daily business (you don’t need to check for a fire every few seconds) … but when there’s a fire it interrupts whatever you’re doing (the interrupt) and from that point you “grab your warden’s jacket / get everyone out of the building to an assembly area / call the fire brigade / do a head count etc” – in computer terms those pre-defined actions would be called the “interrupt service routine”.

To contine the analogy, after a period of time – when it’s been confirmed that there’s no longer a fire and it’s safe to return – the all-clear is given and people are free to return to whatever they were doing before they got interrupted.

In program terms my device is set to generate an interrupt as soon as a switch is pressed … and another when the key is released; that way we can know (a) when it was pressed and (b) how long it was pressed for. So every time I press and release the button it’ll generate 2 interrupts right? Wrong. In testing that it actually generated an average of around 40 interrupts per press & release! Huh? How come? In reality switches don’t close cleanly; in what seems like well defined motions that occur in a fraction of a second to us slow humans – to an incredibly fast micro device this takes an eternity – and in that time it’s able to record all of the times the contacts come together – break apart – make a connection – break a connection – before finally making first a solid connection – and then a solid disconnection (the same thing happens when the button is released).

From a programming point of view we have to think about how we want to handle this; to continue the fire alarm analogy, it’s a bit like having the alarm activated by a faulty switch which turns the alarm on – and then off again – multiple times. In that context we need to decide “do we want people to take action the moment they hear the first momentary alarm” (to give them the greatest opportunity to exit the building safely) – or do we want them to ignore the alarm until it’s been going for a set period of time so that they know it’s not just a brief test?

In the context of my project – since the button press started a stopwatch – I really wanted the program to start the stopwatch on the very first interrupt (if a 100m sprint were being timed we wouldn’t want to lose several hundreths of a second waiting for a solid closure) – but I didn’t want it to stop a few microseconds later due to contact bounce – only to start again and stop again a few microseconds later for the next few milliseconds. Much the same as with a fire alarm where you wouldn’t want people to leave their desks – then return 3 seconds later when the alarm stopped – only to leave again 2 seconds later – and return again 4 seconds later (repeat 20 times).

So key bouncing is something we have to work around – and the approach we take depends on what we need it to do; if the press is starting a stopwatch we want that timing process to begin at the first opportunity – but if pressing the button launches a nuclear missile then you probably wouldn’t want it to be triggered by what might turn out to be a momentary glitch!

Other factors come into play as well; you mgiht think that if one interrupt is generated when contacts close – and another when they open (even if it’s only briefly during a bounce) then eventually when the dust has settled we’ll always end up with an even number of interrupts occuring right? Wrong again. In practice I ended up with quite a few odd interrupt counts. In theory that shouldn’t happen, but in practice I suspect that some of those interrupts occurred so soon after a previous one that they occurred at a time where it was ignored because the processor was still processing a previous interrupt. In a fire alarm context it would be a bit like telling people to evacuate the first time the alarm goes off – and return when the all clear is signalled by it going off a 2nd time. Sounds good in theory, but programs are very pedantic; unless you cater for situations like someone forgetting to sound the all clear you’ll end up in a situation where they stay away from their desks for months … until there’s a fire … and then return to work. So clearly we have to cater for that eventuality by including logic like “if it’s been X amount of time … and there’s no fire … and the brigade has left then lets just assume that the event is over”.

My code – that I finally got working correctly – has 3 main parts to it:

(1) The ISR (Interrupt Service Routine)

(2) Some “watchdog” code that monitors for buttons that are still flagged as being pressed, but can be seen to be open. This code also calculates the switch press duration since there’s no guarantee that there will always be a interrupt that’s interpreted as a switch-release interrupt generated (it may occur, but if things are out of sync at that point it can just get treated as another button press – not a release).

(3) The supporting code that defines the variables used – and calls the “watchdog” code.

At this point it’s probably worth mentioning that there are a few in the Arduino community who appear to be strongly opposed to using interrupts to handle key presses. By my observation they seem to be misinterpreting “some things occur so quickly that you need to use an interrupt to guarantee that you’ll catch the event” (such as every time a compressed fuel/air mixture ignites in an engine) as meaning “only rapidly occurring events can/should use interrupts”. In my opinion there’s no right or wrong; if you want a key press to be recorded the moment it occurs which then frees you to handle it at a later time when it’s more convenient then that’s a perfectly valid position to take.

So – philosophy aside – here’s the code:

/* Any process that acts on the sw1Pressed flag must immediately clear the flag to prevent other portions of code acting on it.

Once it's been cleared it won't be set again until there's been a key release flagged. 
The actual key release flag set happens in the handler not the ISR.

Any process that acts on sw1PressDuration must immediately reset it to zero to prevent some other portion of code acting on it.
*/

const byte    sw1                 = 2;                                          // Switch to pin assignments
const byte    sw1Debounce         = 30;                                         // Sw1 debounce time in ms

volatile bool sw1Pressed          = false;                                      // Switch state flags
volatile bool sw1Released         = true;

volatile long sw1PressTimestamp   = 0;                                          // Switch time stamp variables
volatile long sw1ReleaseTimestamp = 0;
volatile long sw1PressDuration    = 0;

void setup()
{
  attachInterrupt(digitalPinToInterrupt(sw1), sw1ISR, CHANGE);                  // Attach interrupt handler
}

void loop()
{
  sw1Handler();                                                                 // Sw1 state maintenance
}

void sw1Handler()                                                               // Maintain valid sw1 states
 {
   if (!sw1Released)                                                            // Update duration if switch flagged closed
   {
     sw1PressDuration = millis() - sw1PressTimestamp;                           // Calculate the press duration to date
   }

   // Switch still flagged as not released but release time has passed & switch is open?
   if (!sw1Released && millis() >= sw1ReleaseTimestamp && digitalRead(sw1))
   {
     sw1Released      = true;                                                             // Reset the flag
     sw1PressDuration = millis() - sw1PressTimestamp;                                     // Calculate press duration
     digitalWrite(LED_BUILTIN, LOW);                                                      // Turn off LED
   }
 }

 void sw1ISR()                                                                            // Switch 1 interrupt handler
 {
    delayMicroseconds(500);                                                               // 0.5ms for levels to settle   

// If not currently flagged as pressed and previously flagged as released and currently closed then we  
// can treat as a valid press. If already flagged as pressed then just ignore.
   if (!sw1Pressed && sw1Released && !digitalRead(sw1))                                   // Typically caused by an
                                                                                          // interrupt triggered by a
                                                                                          // FALLING level.
   {
     sw1Pressed        = true;                                                            // Flag sw1 as pressed
     sw1Released       = false;                                                           // Flag sw1 as not released
     sw1PressTimestamp = millis();                                                        // Record time pressed
     digitalWrite(LED_BUILTIN, HIGH);                                                     // Turn on LED
   }

   // Switch not flagged as released & more than debounce period since pressed & switch now open?
   // Typically caused by an interrupt triggered by a RISING level

   if (!sw1Released && millis() - sw1PressTimestamp >= sw1Debounce && digitalRead(sw1))
   {
     sw1ReleaseTimestamp = millis() + sw1Debounce;                        // Record time switch can be flagged as released
                                                                          // (checked & handled by sw1Handler process)
   }
 }

Happy to discuss this with anyone should they so desire. It’s also important to point out a couple of things:

(1) The routine is designed to assume that a signal level is high when the switch is open – and is pulled down to ground when it’s closed. Since the Arduino Mega processors have an internal pullup resistor that can be turned on this makes circuit design a bit easier as one doesn’t then need an external pullup resister (pins can “float” to any value without one).

(2) Because us humans are “slow” in computer speed terms, I’ve assumed that if a key press or key release has been initiated then we can safely ignore any other state changes that occur during the key debounce periods. In essence this “limits” the algorithm to “only” around 16 key presses per second – whereas in reality we can’t even manage 10 – so it’s not a problem. Actual bounce periods vary; typically it’s “all over” within about 10mS but I’m using some particularly poor quality switches – so had to increase the period.

Finally – if someone would like me to eMail them a copy of the code then I’d be happy to oblige; it should be possible to copy/paste from the above, but in reality I had to do a whole lot of ugly things to the formatting to make it display correctly in the blog; my apologies in advance if that means a copy/paste ends up with poor formatting.

This Post Has 0 Comments

Leave a Reply

Your email address will not be published.

Back To Top