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:
-
Assumes that we wouldn't be in either routine unless the relevant key was pressed
-
Then waits about 1/4 second (this gives me time to release the key)
-
Checks if the key is released and if so treats it as a momentary press
-
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.
-
Any switch activation between 1/4 and 2 seconds gets ignored (I've played with that 1/4 delay)
-
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
}
}
}