A Collection of Arduino Techniques that I've Found Useful

Two things on my motorbike really annoyed me; (1) the grip heaters had two settings: "too hot" and "too cold" and (2) I like to be able to see the time. So I mounted a small box with an LCD screen & a smaller OLED display visible through the clear lid - and inside I put an Arduino Pro Mini + a relay + a real time clock module + a power converter. Two buttons allow me to control the temperature (initial heat up + 16 heat levels) (and control the backlight) (info on the LCD screen) whilst the RTC time is displayed on the OLED display.

I've leaned heavily on the knowledge of others to accomplish my Arduino objectives and I wanted to try to give a little back.

As a programmer I (probably) suck, but I can say in my defence that (a) the following code works and (b) it contains the solutions to a surprising number of things I found challenging; things that I had to work out "what worked" and "what didn't" - and most importantly gain an understanding of "why".

I need to be clear that the vast majority of techniques that I've used were thought up by people far better at this than I am (some of their original comments are left in place); for their contributions I'm forever grateful - THANK YOU.

I've put the entire project code below in the hope that there's enough there for others to find solutions to some of the same issues I faced - and if there are any fellow motorcycle riders out there with heated grips that suck (like mine) then this may be all you need to get your fingers toasty warm (and give you the current time on a separate OLED display) :smiley:

The code contains solutions to the following things that I struggled with:

  • Using an LCD display with the I2C protocol
  • Using an OLED display with the I2C protocol
  • Getting the current time from a RTC (ie "all 3 at once")
  • Using interrupts to handle switch activations - including debouncing (what a journey that was!)
  • Handling different switch press durations to control different modes

Happy to receive any constructive criticism (and yes - I fully appreciate that there are a zillion ways to code anything).

Most importanly of all, I hope that someday someone finds a real-world solution to a problem they're having here (that's why I posted the entire project code -- so someone can see how it all hangs together).

Many thanks to all those who helped me bring this project to completion; my fingers thank you!

/*  This program controls the heating of my motorbike handlebars by switching a relay off and on. 
 *   
 *  It powers up in "auto" mode where it powers the heaters at full power for 4 1/2 min to get things warmed up before switching to "manual" mode, starting at 6/16 power.
 *  
 *  It powers up in "day" mode where the LCD backlight stays on.
 *  
 *  Any momentary switch activity during "auto" mode immediately switches the unit to "manual" mode. 
 *  
 *  Any switch held down for more than 1 second toggles day/night mode (for LCD backlight control).
 *  
 *  In night mode the LCD backlight remains off until a button is pressed - at which point it turns the backlight on for 3 seconds.
 *  
 *  The current time is read from an RTC module & displayed on the OLED display.
 *  
 *  The easiest way to set the reset the time if occasional adjustments are needed for drift or daylight savings changes is to simply remove the backup battery - power-up the unit at midnight - then reinsert the battery (the date isn't used).
 *  
 *  Be aware that there's a very common RTC module with a serious fault in that it tries to charge the 2032 battery (and tries to overcharge a rechargeable 2032 battery if powered at 5V) - be sure to remove the 201 ohm resister to disable the charging 
 *  circuit if using one of these.
 *  
 *  The main loop completes in about 60 mS. I did it this way originally so that key presses could be detected and the LCD screen updated in a timely manner, and although I've subsequently re-written the sketch to (rightly or wrongly) use interrupts 
 *  to handle button presses, I've stuck with this 60mS loop speed approach. Although it would be possible to turn the heaters off and on using fast loop speeds it would wear out the relay - so every "heat state period" actually consists 10 passes through the 
 *  main loop for that state - and in the end I found it easier to just treat these values as being 10 times the size inside the program that we think of them as when using the unit eg setting "******" on the display would turn on the heaters for 6 periods out of 16 but internally 
 *  the program uses "60" instead of "6"" for the "on time". It's also the reason (for example) increasing the heater power setting actually adds "10" not "1" to that variable. If I hadn't done it this way the heaters would still have been fine 
 *  but the LCD screen updates would be very sluggish because they'd only get to do their thing about once a second. So every cycle of "on time" plus "off time" (always a total of 16 time periods x 10) completes in about 10 seconds - which works well for the grip heaters
 *  and also to control my electric blanket!
 */

#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;                                                // Connected to down switch
const int upSwitch            = 3;                                                // Connected to up switch
const int heaterControl       = 4;                                                // Connected to relay

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). When we get to Zero (or below) we drop into manual mode (and stay there)
volatile int loopCount        = powerLevels * 10;                                 // 16 heat levels (plus off) x 1/10th sec per iteration

int onTime;                                                                       // Initial on time (in 60mS increments) (value set later)
int offTime;                                                                      // Initial off time (in 60mS increments) (value set later)
int displayPower              = Power / 10;                                       // Initial power level displayed on the LCD

volatile unsigned long lightOffTime    = 0;                                       // Backlight timer turn-off time (set by the ISRs)
volatile bool nightMode                = 0;                                       // Backlight on if day mode (0); backlight off if night mode (1) (Set by the ISRs)

const int releaseTime         = 500;                                              // Key releaseTime buffer / key release period - in milliseconds. A momentary key press needs to be less than this in duration to be counted for a heat level change
const int lcdTimeout          = 3 * 1000;                                         // How long to leave the backlight on for after a key press at night (in seconds) (converted to 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, LOW);         // Attach ISRs to handle switch activation interrupts
  attachInterrupt(digitalPinToInterrupt(upSwitch), upSwitchISR, LOW);

  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);                                                            // Initialise serial interface
  
  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();                                                                     // Initialise 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

    backlightControl();                                                           // Ensure backlight is in correct mode - as night mode can be turned on even while in (and without affecting) auto mode

    delay (100);                                                                  // Wait 100 milliseconds (1/10th second)
    modeTimer--;                                                                  // Decrement the timer count
  }                                                                               // And go around the loop until timer is at or below zero (either counted down or set by an ISR)

// Manual mode starts here

  onTime = Power;                                                                 // Set on_time & off_time counters (in 60mS increments)
  offTime = (powerLevels * 10) - Power;                                           // Off Time is 160 - On Time so the two always add up to 160 (16 states x 10 passes through the loop per state)
  
  lcd.clear();                                                                    // Clear the screen
  lcd.print("Manual Mode");                                                       // Now in manual mode
  updateDisplay();                                                                // Update bottom line of LCD display

  while (loopCount > 0)                                                           // Process 10x 60mS states (ie "just under 1 second") 16 times (one for each of the possible heat levels)
  {
    updateOLED();                                                                 // Update time display
    
    if (onTime > 0)                                                               // Let's deal with the time-on portion:
    {
      digitalWrite(heaterControl, HIGH);                                          // Turn on the relay
      onTime--;                                                                   // Decrement the counter by approx 60mS
      lcd.setCursor(15,0);                                                        // Position cursor to top right
      
      if (nightMode == 1)                                                         // If we're in night mode ...
      {
        lcd.print("N");                                                           // ... then print an "N" to indicate heater is on and we're in night mode
      }

      else                                                                        // Otherwise must be in day mode ...
      {
        lcd.print("*");                                                           // ... so just print the regular "*" in the top right-hand corner
      }
    }

    else                                                                          // Now let's deal with the time-off portion:
    {
      if (offTime > 0)                                                            // for as long as time off hasn't reached zero:
      {
        digitalWrite(heaterControl, LOW);                                         // Turn off the relay
        offTime--;                                                                // Decrement the counter by approx 60mS 
        lcd.setCursor(15,0);                                                      // Position cursor to top right
        lcd.print(" ");                                                           // Clear the "*" heater on indicator
      }
    }   
    
    backlightControl();                                                           // Try to turn off the LCD backlight if not needed (this will only succeed 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 set of 160 iterations
    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

  for (displayPower = Power / 10; displayPower > 0; displayPower--)               // Display correct number of stars on LCD
  {
    lcd.print("*");                                                               // - Print a "*" and advance the cursor
  }                                                                               //
}

void backlightControl()                                                           // Turn off backlight if night mode is true
{
  if (nightMode == 1)                                                             // If in night mode
  {
   if (millis() < lightOffTime)                                                   // and we haven't reached light off time then ...
   {
    lcd.backlight();                                                              // turn backlight on
   }

   else                                                                           // Otherwise we must have reached lights off time
   {
    lcd.noBacklight();                                                            // so turn backlight off
   }
  }

  if (nightMode == 0)                                                             // If not in night mode
  {
    lcd.backlight();                                                              // Turn on backlight
  }
}

void updateOLED()                                                                 // Write current time to OLED display
{
  DateTime now = rtc.now();                                                       // Get the current time from the RTC

  display.clearDisplay();                                                         // Clear the display so we don't get artifacts
  display.setCursor(0,0);                                                         // Start at top-left corner
  
  if (now.hour() < 10)                                                            // Leading "0" required before hour?
  {
    display.println("0");                                                         // If so then write it to the OLED buffer and ...
    display.setCursor(32,0);                                                      // Move the cursor for the next digit
  }
  
  display.println(now.hour(), DEC);                                               // Write the current hour to the OLED buffer
  
  display.setCursor(66,0);                                                        // Setup for correct minutes position

  if (now.minute() < 10)                                                          // Leading "0" required before minute?
  {
    display.println("0");                                                         // If so then write it to the OLED buffer
    display.setCursor(98,0);                                                      // and move the cursor for the next digit
  }
  
  display.println(now.minute(), DEC);                                             // Write the current minute to the buffer
  
  display.display();                                                              // Display the buffer contents on the OLED
}

// Interrupt Service Routines

void downSwitchISR()                                                              // This gets run whenever the normally-high pin 3 (up switch) is grounded by the switch closing
{
  static unsigned long lastInterruptTime = 0;                                     // Setup for key bounce handling
  unsigned long interruptTime = millis();                                         //
  
  if (interruptTime - lastInterruptTime > 150)                                    // If it's been at least 150 mS since last interrupt then proceed
  {
    lightOffTime = millis() + lcdTimeout;                                         // Work out what time the backlight needs to be turned off
    
    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)                                                              // 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 = 1000; delayInISR > 0; delayInISR--)               // Wait 1 second
      {
        delayMicroseconds(1000);                                                  //
      }

      if (digitalRead(downSwitch) == LOW)                                         // Switch down for 1 second or more?
      {
        nightMode = !nightMode;                                                   //  Toggle night mode flag
      }
    }
  }
  
  lastInterruptTime = interruptTime;                                              // Document last interrupt time for debounce code
}

void upSwitchISR()                                                                // This gets run whenever the normally-high pin 3 (up switch) is grounded by the switch closing
{
  static unsigned long lastInterruptTime = 0;                                     // Setup for key bounce handling
  unsigned long interruptTime = millis();                                         //

  if (interruptTime - lastInterruptTime > 150)                                    // If it's been at least 150 mS since last interrupt then proceed
  {
    lightOffTime = millis() + lcdTimeout;                                         // Work out what time the backlight needs to be turned off
    
    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 = 1000; delayInISR > 0; delayInISR--)               // Wait 1 seconds
      {
        delayMicroseconds(1000);                                                  //
      }

      if (digitalRead(upSwitch) == LOW)                                           // Switch down for 1 second or more?
      {
        nightMode = !nightMode;                                                   // Toggle night mode flag
      }
    }
  }
  lastInterruptTime = interruptTime;                                              // Document last interrupt time for debounce code
}
1 Like

where is the schematic?

Only in my head I'm afraid.

Electrically it's pretty simple; 12v from the bike goes into a buck converter and 5v comes out the other side to power everything.

Switches ground either of the 2 respective input pins when pressed - and the output pin connects to a relay input.

The OLED, LCD, and RTC all use the I2C protocol so are all connected in parallel to VCC, GND, and the two appropriate processor pins.

Hope that helped.

That should make drawing a schematic simple. What protection is in the 12V for the electronics. if the 2 respective input pins are connected to wires without protections another problem. Relays blow Arduino ports and Arduinos. There are other potential problems.

At one point I had an optoisolator between the Arduino output and the relay input, but that was just for level conversion as the output from the 3.3v Nano I was using at the time wasn't a high enough level to control the relay, but I took that out of the circuit for simplicity when I switched to a 5v Pro Mini.

So the short answer is "no protection" - and that's OK - the worst that could happen is a few components that cost approximately nothing get destroyed (which hasn't happened; in fact I've yet to destroy so much as a single component in any project which is probably some kind of record given where they mostly come from!)

The whole box is only powered when the ignition is on so the worst-case scenarios are nothing more than:

(a) riding home without the enjoyment that comes from toasty-warm hands if it fails off

(b) riding home with hot hands if it fails on (which isn't an issue - one just needs to change how they grip the bars) or

(c) the whole thing catches fire (highly unlikely but even that would be of no particular significance since it's connected to metal handlebars well away from anything else) (and keeping in mind that power is removed when the ignition is off - and I'm always on the bike when it's on) (and it's in a case designed to resist some degree of thermal insult).

I used a common relay that's available in 1, 2, or 4 relay configurations. Almost without exception the 2 & 4 relay versions have optoisolation whereas the single doesn't - but that doesn't mean it backfeeds 12 (or 230 volts if it were connected to a mains curcuit) - it just means that there are less safety layers. On a personal project using only 12v it's not a concern; in contrast there's no way in "heck" I switch mains voltage without optoisolation

In practice it's working really really well.

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