Problem using interrupts for handling switch activations

I built an Arduino controller for the grip heaters on my motorbike. When powered up it turns on a relay for 4 1/2 minutes so the grips heat up as quickly as possible - after that they default to being on for 6 "periods" and off for 10 "periods" (a "period" was originally about 1 second, but I've currently got it set to about 600 mS).

The controller has a couple of switches for increasing or decreasing the amount of time the grips are heated for each cycle; pressing either key during the initial 4 1/2 minute "auto" mode immediately drops it into "manual mode" - and an LCD display tells me what the current setting is.

Version 1 worked well, but I can't see the display during the day unless the backlight is on - and the backlight being on is annoyingly bright at night - so I'm re-writing the sketch to allow me to turn the backlight off and on by holding either switch down for more than 2 seconds; anything less than that may be treated as an instruction to increase or decrease the time the heaters are on (depending on how quick the release was).

I've been playing with the code for several hours; it's working reasonably well, but is still glitching a bit - and I'd really appreciate some help as to where I'm going wrong; the problem is that quite often I'm getting 2 changes in heat level for 1 press of a button. I strongly suspect key bounce is the culprit, but I'm not understanding why - or how to handle it. I'll pop the code for the whole project in below, but basically the switch inputs are active low - and use the internal pullup when not activated. The interrupt is set to trigged on a high to low transition - and the interrupt service routine basically:

  1. Assumes that we wouldn't be in either routine unless the relevant key was pressed

  2. Then waits about 1/4 second (this gives me time to release the key)

  3. Checks if the key is released and if so treats it as a momentary press

  4. If it's still held down after 1/4 second then it waits for 2 more seconds and checks again if it's still held down - if so then it toggles the boolean variable for the backlight display.

  5. Any switch activation between 1/4 and 2 seconds gets ignored (I've played with that 1/4 delay)

  6. I ASSUMED that an ISR for a key switch can't get interrupted by another activation of the same switch whilst in progress (may or may not be a valid assumption) - and even tried disabling interrupts firstly whilst the initial 1/4 delay was running and later whilst the entire ISR was running - still not getting anywhere.

Hoping someone can shed a bit of light for me - or point me to a better technique. The main heating/resting loops are slightly clunky in that I originally wrote them this way so I could check for a keypress every 100mS or so when it wasn't interrupt driven - and that "clunkiness is still there as this "v2" is an adaption of the "v1" code.

Many thanks.

Cheers,

Colin

/*  This program controls the heating of my motorbike handlebars. It powers up in "auto" mode where it powers the heaters at full power for 4 1/2 min before switching to "manual" mode, starting at 6/16 power.
 *  Any momentary switch activity during "auto" mode immediately switches the unit to "manual" mode. 
 *  Any switch held down for more than 2 seconds toggles day/night mode (for LCD backlight control).
 *  The current time is also read from a RTC module & displayed on an OLED display but this is commented out just at the moment
 */

#include <Wire.h>                                                                 // I2C library
#include <LiquidCrystal_I2C.h>                                                    // LCD library
//#include <Adafruit_SSD1306.h>                                                     // OLED library
//#include "RTClib.h"                                                               // RTC library

//const int SCREEN_WIDTH        = 128;                                              // OLED display width, in pixels
//const int SCREEN_HEIGHT       = 32;                                               // OLED display height, in pixels
//const int OLED_RESET          = 4;                                                // Reset pin # (or -1 if sharing Arduino reset pin)
//const int SCREEN_ADDRESS      = 0x3C;                                             // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32

const int downSwitch          = 2;                                                // Define pins
const int upSwitch            = 3;
const int heaterControl       = 4;

const int powerLevels         = 16;                                               // 16 Power levels + 0 (off)

int Power                     = 6 * 10;                                           // Define heater power variable and set to 60 out of 160 (37.5% duty cycle)
volatile int modeTimer        = 270 * 10;                                         // Run in auto mode for 2700 x 1/10th sec (4 min & 30 seconds). Zero indicates manual mode
volatile int loopCount        = powerLevels * 10;                                 // 16 heat levels (plus off) x 1/10th sec per iteration

int onTime;                                                                       // Initial on time (in 1/10ths of a second)
int offTime;                                                                      // Initial off time (in 1/10ths of a second)
int displayPower              = Power / 10;                                       // Initial display power

unsigned long lightOffTime    = 0;                                                // Backlight timer
volatile bool nightMode                = 0;                                                // Backlight on if day mode (0); backlight off if night mode (1)
volatile bool interruptHasOccurred     = 0;                                                // True = Interrupt has occurred so we need to update things
const int releaseTime         = 300;                                              // Key releaseTime buffer and key release period - in milliseconds

//Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);         // Define interfaces
//RTC_DS3231 rtc;                                                                   //
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2);                           //

// SETUP CODE

void setup()                                                                      // Setup code starts here (runs once)
{
  attachInterrupt(digitalPinToInterrupt(downSwitch), downSwitchISR, FALLING);     // Setup ISRs to handle switch activations
  attachInterrupt(digitalPinToInterrupt(upSwitch), upSwitchISR, FALLING);

  pinMode(heaterControl, OUTPUT);                                                 // Set heater relay control pin as output
  pinMode(upSwitch, INPUT_PULLUP);                                                // Set up switch pin as input
  pinMode(downSwitch, INPUT_PULLUP);                                              // Set down switch pin as input

  Serial.begin(57600);                                                            //
  
//  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);                            // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
//  display.setTextSize(5);                                                         // Big numbers
//  display.setTextColor(SSD1306_WHITE);                                            // White text

  lcd.init();                                                                     // Prep LCD
  lcd.backlight();                                                                // Backlight on
  
  lcd.print("Auto Mode      *");                                                  // Display "Auto Mode      *"
  lcd.setCursor(0,1);                                                             // Reposition cursor
  lcd.print("Grips Warming Up");                                                  // Indicate full power
}
        
// MAIN CODE

void loop()                                                                       // Main code starts here
{
  
// Auto Mode Section
  
  while (modeTimer > 0)                                                           // While in auto mode just count down with heaters on & update OLED clock
  {
//    updateOLED();                                                                 // Show time on OLED
    digitalWrite(heaterControl, HIGH);                                            // Turn heater on

    backlightCheck();                                                             // Ensure backlight is in correct mode

    delay (100);                                                                  // Wait 100 milliseconds (1/10th second)
    modeTimer--;                                                                  // Decrement the timer count
  }                                                                               // And go around the loop until timer hits zero or a key is pressed

// Manual mode starts here

  onTime = Power;                                                                 // Set on_time & off_time counters (in 1/10ths of a second)
  offTime = (powerLevels * 10) - Power;                                           //
  
  lcd.clear();                                                                    // Clear the screen
  lcd.print("Manual");                                                            // Now in manual mode
  updateDisplay();                                                                //

  while (loopCount > 0)                                                           // Process 10x 100mS states (ie "1 second") 16 times (one for each of the possible heat levels)
  {
//    updateOLED();                                                                 // Update time display
    
    if (onTime > 0)                                                               // If time on required then:
    {
      digitalWrite(heaterControl, HIGH);                                          // Turn on the relay
      onTime--;                                                                   // Decrement the counter by 1/10th of a second
      lcd.setCursor(15,0);                                                        // Position cursor to top right
      lcd.print("*");                                                             // Print an "*" to indicate heater is on
    }

    else                                                                          // If time off required then:
    {
      if (offTime > 0)                                                            // for as long as time off hasn't timed out:
      {
        digitalWrite(heaterControl, LOW);                                         // Turn off the relay
        offTime--;                                                                // Decrement the counter by 1/10th of a second 
        lcd.setCursor(15,0);                                                      // Position cursor to top right
        lcd.print(" ");                                                           // Clear the "*" heater on indicator
      }
    }   
    
    backlightCheck();                                                             // Try to turn off backlight if not needed (only allowed in night mode)
    delay(50);                                                                    // Wait 50 milliseconds - this sets the overall speed of the timing loop in normal operation - approx 10 sec for complete cycle
    loopCount--;                                                                  // Decrement the loop count
               
  }                                                                               // End of heat cycle loop

  loopCount = powerLevels * 10;                                                   // 160 iterations done ... so reset the counter and get back into the loop
            
}                                                                                 // End of Main Code Loop

// Subroutines

void updateDisplay()                                                              // Update the LCD display
{
  lcd.setCursor(0,1);                                                             // Cursor to bottom left
  lcd.print("                ");                                                  // Clear bottom row
  lcd.setCursor(0,1);                                                             // Move cursor back to beginning of bottom row

  displayPower = Power / 10;                                                      // Calculate correct value for loop

  while(displayPower > 0)                                                         // While display power isn't zero ...
  {
    lcd.print("*");                                                               // - Print a "*" and advance the cursor
    displayPower--;                                                               // - Decrement the loop count
  }                                                                               // - And go around the loop again
}

void backlightCheck()                                                             // Turn off backlight if night mode is true
{
  if (nightMode == 1)                                                             //
  {
    lcd.noBacklight();                                                            //
  }
  else                                                                            //
  {
    lcd.backlight();                                                              //
  }
}

//void updateOLED()                                                                 //
//{
//    DateTime now = rtc.now();                                                     //

//    display.clearDisplay();                                                       //
//    display.setCursor(0,0);                                                       // Start at top-left corner
  
//    if (now.hour() < 10)                                                          // Leading "0" required before hour?
//    {
//      display.println("0");                                                       //
//      display.setCursor(32,0);                                                    //
//    }
  
//    display.println(now.hour(), DEC);                                             // Display current hour
  
//    display.setCursor(66,0);                                                      // Setup for correct minutes position

//    if (now.minute() < 10)                                                        // Leading "0" required before minute?
//    {
//      display.println("0");                                                       //
//      display.setCursor(98,0);                                                    //
//    }
  
//    display.println(now.minute(), DEC);                                           // Display current minute
  
//    display.display();                                                            // I lied before - this ACTUALLY displays it
//}

// Interrupt Service Routines

void downSwitchISR()                                                                // This gets run whenever the normally-high pin 2 (down switch) is grounded by the switch closing
{
  interruptHasOccurred = 1;                                                         // Signal that we've had an interrupt so the main loop can update LCD display & backlight
  
  for (int16_t delayInISR = releaseTime; delayInISR > 0; delayInISR--)              // Can't use delay() in ISRs - so this loops 1000 microsecond delays the specified number of times
  {
    delayMicroseconds(1000);
  }

  if (digitalRead(downSwitch) == HIGH)                                              // If the switch has been released after our 1/4 second wait it was just a momentary press - so process as heat level change
  {
    modeTimer = 0;                                                                  // Signal end of auto mode (may or may not have been in that mode at this time)

    if (Power > 0)                                                                  // Lower heat setting - but only if not already at min power level
    {
      Power -= 10;                                                                  // Decrease heater power
      loopCount = 0;                                                                // Reset the timing loop so it takes effect straight away
    }
  }

  else                                                                              // Switch must be down for more than 1/4 second - so a toggle night mode might be required
  {
    for (int16_t delayInISR = 2000; delayInISR > 0; delayInISR--)                   // Wait 2 seconds
    {
      delayMicroseconds(1000);                                                      //
    }

    if (digitalRead(downSwitch) == LOW)                                             // Switch down for 2 seconds or more?
    {
      nightMode = !nightMode;                                                       // Toggle night mode flag
    }
  }
}

void upSwitchISR()                                                                  // This gets run whenever the normally-high pin 3 (up switch) is grounded by the switch closing
{
  interruptHasOccurred = 1;                                                         // Signal that we've had an interrupt so the main loop can update LCD display & backlight

  for (int16_t delayInISR = releaseTime; delayInISR > 0; delayInISR--)              // Can't use delay() in ISRs - so this loops 1000 microsecond delays the specified number of times
  {
    delayMicroseconds(1000);
  }
  
  if (digitalRead(upSwitch) == HIGH)                                                // If the switch has been released after our 1/4 second wait it was just a momentary press - so process as heat level change
  {
    modeTimer = 0;                                                                  // Signal end of auto mode (may or may not have been in that mode at this time)

    if (Power < powerLevels * 10)                                                   // Increase heat level - but only if not already at maximum
    {
      Power += 10;                                                                  // Increase heater power
      loopCount = 0;                                                                // Reset the timing loop so it takes effect straight away
    }
  }

  else                                                                              // Switch must be down for more than 1/4 second - so a toggle night mode might be required
  {
    for (int16_t delayInISR = 2000; delayInISR > 0; delayInISR--)                   // Wait 2 seconds
    {
      delayMicroseconds(1000);                                                      //
    }

    if (digitalRead(upSwitch) == LOW)                                               // Switch down for 2 seconds or more?
    {
      nightMode = !nightMode;                                                       //  Toggle night mode flag
    }
  }
}

Conventional wisdom says that you don't need interrupts for seeing something as slow as a button press from a human.

Also, delays are reliant on timer interrupts which cease when you're in an interrupt routine, so using delays in interrupt servicing code is counterproductive.

Since you want long less and short press to have different effects, I suggest that you restructure your sketch as a state machine.

Thanks Bill,

I'm not 100% sure what the latest is with delays in ISRs; I read that delay() didn't work because it in-turn relied on interrupts but delayMicroseconds() worked just fine (which is what I used), but I even found delay() worked too; I think I recall reading something recently that said there's been a recent change and it could now be used but didn't want to take the chance.

I figured that there's probably many ways to do it ... using interrupts seemed like a better idea than the one I was using (at the time). Can't say I've heard of state machines - will do some Googling on that now.

Even if interrupts aren't the best tool for the job I'd still like to understand why they're not working reliably; even if ultimately they're not needed on this project I have a feeling that it'll bite me in the bum later on if I don't understand what I'm doing wrong.

I think you're right. As I was typing that reply I had a nagging doubt about delayMicroseconds but didn't bother to verify.

I suspect that switch bounce may be causing a problem. IIRC, interrupts can be detected while you're processing one and will fire when you exit your interrupt routine, so it's likely that the bounce will cause a second firing. The delays you have (assuming they are working :grinning:) should mean it'll be just two.

Thanks - that's what it seems like. Having just said that, I would have thought that noInterrupts() would have knocked those on the head, but I haven't had any luck getting that to make any difference.

Thanks for the heads up on state machines; I did a quick Google - looks interesting ... will check it out more thoroughly when my brain isn't so tired (12:10am here now).

I don't think noInterrupts will help - they're off while you process an interrupt anyway, but the processor will still flag that one fired while you were working the previous one.

I wasn't 100% sure about that; I thought I read something about interrupts no longer being disabled during ISRs but I can't find it (which might explain why delay() seemed to work) - so I might have to rely on someone else's memory.

I might be able to so a quick fix by putting a capacitor across the contacts until I learn how to do a version 3 / state machine.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.