For the love of Engineering

Electric Scooter Part 4 – The Software

Introduction

Writing code for the Arduino microcontroller is pretty straightforward if you have ever written any programs in C or java.  The language syntax is pretty similar to both those languages.   There are really only two functions that you have to create.  One function is called setup() and is just executed once on startup and the other function is called loop() which is just continually, repeatedly executed.

void setup() 
{
  // put your setup code here, to run once:
}

void loop() 
{
  // put your main code here, to run repeatedly:
}  

This page isn’t meant to show every piece of code. It is just pieces that I thought were interesting or things that might be useful for anyone reading. The reason for not showing all the code is that nobody is going to make the hardware that goes with it and use this same software. However, if you are interested, I don’t mind sharing, use the contact page to get in touch.

Setup

Generally, in the setup function, you set any pins as inputs/outputs.  You also need to setup any third party libraries, such as the LCD library and any timers that are needed.  I knew that because the interface would have some form of hierarchy, I wanted a state machine for the display and the user interaction.  I also required a state machine for the bike.  There was other I/O such as the 1-wire library for the temperature sensor.  I also wanted interrupts for the hall effect sensor for the speedo.

I created the state machine for the menus by setting up a structure which contained five entries per line.

Current Mode : Cancel Operation : Up Operation : Down Operation : Select Operation

Each mode in the state machine is represented by one byte and the table just contains data on what mode the state machine should be moved from and to as each key on the keypad is pressed.  Any specific changes such as number editing is handled locally in that mode.  One thing to remember when programming these microcontrollers is that the elegance of high level languages such as C++ or C# cannot always be represented.  I don’t just mean in the syntax, I mean it sometimes is necessary to take short cuts in order to get the code to fit into the memory space or to run fast enough.  These are not high level powerful CPUs. The state machine is shown below:

                                  // MODE                         CANCEL                UP                            DOWN                      SELECT
const byte transitionNetwork[] = {        
                                    //Run mode
                                    MODE_MONITOR_RUN,             MODE_MONITOR_RUN,     MODE_DIAG,                    MODE_SLEEP,               MODE_MONITOR,
                                    
                                    //TOP level menu
                                    MODE_SETTINGS,                MODE_MONITOR_RUN,     MODE_SLEEP,                   MODE_DIAG,                MODE_SETTINGS_LIGHT,
                                    MODE_DIAG,                    MODE_MONITOR_RUN,     MODE_SETTINGS,                MODE_MONITOR,             MODE_DIAG_BATTVOLT,
                                    MODE_MONITOR,                 MODE_MONITOR_RUN,     MODE_DIAG,                    MODE_REVIEW,              MODE_MONITOR_RUN, 
                                    MODE_REVIEW,                  MODE_MONITOR_RUN,     MODE_MONITOR,                 MODE_SLEEP,               MODE_REVIEW_RUN, 
                                    MODE_SLEEP,                   MODE_MONITOR_RUN,     MODE_REVIEW,                  MODE_SETTINGS,            MODE_SLEEP_RUN, 
                                    MODE_SLEEP_RUN,               MODE_SLEEP,           MODE_SLEEP,                   MODE_SLEEP,               MODE_SLEEP,
                                    
                                    //Diag mode
                                    MODE_DIAG_BATTVOLT,           MODE_DIAG,            MODE_DIAG_TICK,               MODE_DIAG_BATTRAW,        MODE_DIAG_BATTVOLT,
                                    MODE_DIAG_BATTRAW,            MODE_DIAG,            MODE_DIAG_BATTVOLT,           MODE_DIAG_THROTTLE,       MODE_DIAG_BATTRAW,
                                    MODE_DIAG_THROTTLE,           MODE_DIAG,            MODE_DIAG_BATTRAW,            MODE_DIAG_TICK,           MODE_DIAG_THROTTLE,
                                    MODE_DIAG_TICK,               MODE_DIAG,            MODE_DIAG_THROTTLE,           MODE_DIAG_BATTVOLT,       MODE_DIAG_TICK,    
                                                                            
                                    //Settings mode
                                    //MODE_SETTINGS_STARTUP,        MODE_SETTINGS,        MODE_SETTINGS_TIME,           MODE_SETTINGS_LIGHT,      MODE_SETTINGS_STARTUP_CHANGE,
                                    MODE_SETTINGS_LIGHT,          MODE_SETTINGS,        MODE_SETTINGS_TIME,           MODE_SETTINGS_UNITS_DISTANCE,  MODE_SETTINGS_LIGHT_CHANGE,
                                    MODE_SETTINGS_UNITS_DISTANCE, MODE_SETTINGS,        MODE_SETTINGS_LIGHT,          MODE_SETTINGS_UNITS_SPEED,MODE_SETTINGS_UNITS_DISTANCE_CHANGE,
                                    MODE_SETTINGS_UNITS_SPEED,    MODE_SETTINGS,        MODE_SETTINGS_UNITS_DISTANCE, MODE_SETTINGS_UNITS_TEMP, MODE_SETTINGS_UNITS_SPEED_CHANGE,
                                    MODE_SETTINGS_UNITS_TEMP,     MODE_SETTINGS,        MODE_SETTINGS_UNITS_SPEED,    MODE_SETTINGS_THROTTLE,   MODE_SETTINGS_UNITS_TEMP_CHANGE,                       
                                    MODE_SETTINGS_THROTTLE,       MODE_SETTINGS,        MODE_SETTINGS_UNITS_TEMP,     MODE_SETTINGS_IDLE_TIMEOUT, MODE_SETTINGS_THROTTLE_MIN,
                                    MODE_SETTINGS_IDLE_TIMEOUT,   MODE_SETTINGS,        MODE_SETTINGS_THROTTLE,       MODE_SETTINGS_TIME,         MODE_SETTINGS_IDLE_TIMEOUT_CHANGE, 

                                    //Throttle Settings
                                    MODE_SETTINGS_THROTTLE_MIN,   MODE_SETTINGS_THROTTLE, MODE_SETTINGS_THROTTLE_AUTO, MODE_SETTINGS_THROTTLE_MAX, MODE_SETTINGS_THROTTLE_MIN_CHANGE,
                                    MODE_SETTINGS_THROTTLE_MAX,   MODE_SETTINGS_THROTTLE, MODE_SETTINGS_THROTTLE_NULL, MODE_SETTINGS_THROTTLE_NULL, MODE_SETTINGS_THROTTLE_MAX_CHANGE,
                                    MODE_SETTINGS_THROTTLE_NULL,  MODE_SETTINGS_THROTTLE, MODE_SETTINGS_THROTTLE_MAX,  MODE_SETTINGS_THROTTLE_AUTO, MODE_SETTINGS_THROTTLE_NULL_CHANGE,
                                    MODE_SETTINGS_THROTTLE_AUTO,  MODE_SETTINGS_THROTTLE, MODE_SETTINGS_THROTTLE_NULL, MODE_SETTINGS_THROTTLE_MIN, MODE_SETTINGS_THROTTLE_AUTO_CHANGE,
                                    
                                    //Time settings
                                    MODE_SETTINGS_TIME,           MODE_SETTINGS,        MODE_SETTINGS_IDLE_TIMEOUT,   MODE_SETTINGS_LIGHT,      MODE_SETTINGS_TIME_HOUR,
                                    MODE_SETTINGS_TIME_HOUR,      MODE_SETTINGS_TIME,   MODE_SETTINGS_TIME_HOUR,      MODE_SETTINGS_TIME_HOUR,  MODE_SETTINGS_TIME_MINUTES, 
                                    MODE_SETTINGS_TIME_MINUTES,   MODE_SETTINGS_TIME,   MODE_SETTINGS_TIME_MINUTES,   MODE_SETTINGS_TIME_MINUTES, MODE_SETTINGS_TIME_YEAR,
                                    MODE_SETTINGS_TIME_YEAR,      MODE_SETTINGS_TIME,   MODE_SETTINGS_TIME_YEAR,      MODE_SETTINGS_TIME_YEAR,   MODE_SETTINGS_TIME_MONTH,
                                    MODE_SETTINGS_TIME_MONTH,     MODE_SETTINGS_TIME,   MODE_SETTINGS_TIME_MONTH,     MODE_SETTINGS_TIME_MONTH,  MODE_SETTINGS_TIME_DATE,
                                    MODE_SETTINGS_TIME_DATE,      MODE_SETTINGS_TIME,   MODE_SETTINGS_TIME_DATE,      MODE_SETTINGS_TIME_DATE,   MODE_SETTINGS_TIME_APPLY,
                               
                                    //Review mode
                                    MODE_REVIEW_CHARGE,           MODE_REVIEW,          MODE_REVIEW_RUN,              MODE_REVIEW_RUN,          MODE_REVIEW_CHARGE_SUMMARY,
                                    MODE_REVIEW_RUN,              MODE_REVIEW,          MODE_REVIEW_CHARGE,           MODE_REVIEW_CHARGE,       MODE_REVIEW_RUN_SUMMARY,
                                    MODE_REVIEW_RUN_SUMMARY,      MODE_REVIEW_RUN,      MODE_REVIEW_RUN_IND_MAX,      MODE_REVIEW_RUN_IND_MIN,  MODE_REVIEW_RUN_SUMMARY,    //Doesn't need to go anywhere, display on other screen
                                    MODE_REVIEW_RUN_IND_MIN,      MODE_REVIEW_RUN,      MODE_REVIEW_RUN_SUMMARY,      MODE_REVIEW_RUN_IND_MAX,  MODE_REVIEW_RUN_IND_MIN,
                                    MODE_REVIEW_RUN_IND_MAX,      MODE_REVIEW_RUN,      MODE_REVIEW_RUN_IND_MIN,      MODE_REVIEW_RUN_SUMMARY,  MODE_REVIEW_RUN_IND_MAX,
                                    MODE_REVIEW_CHARGE_SUMMARY,   MODE_REVIEW_CHARGE,   MODE_REVIEW_CHARGE_IND_MAX,   MODE_REVIEW_CHARGE_IND_MIN, MODE_REVIEW_CHARGE_SUMMARY,
                                    MODE_REVIEW_CHARGE_IND_MIN,   MODE_REVIEW_CHARGE,   MODE_REVIEW_CHARGE_SUMMARY,   MODE_REVIEW_CHARGE_IND_MAX, MODE_REVIEW_CHARGE_IND_MIN,
                                    MODE_REVIEW_CHARGE_IND_MAX,   MODE_REVIEW_CHARGE,   MODE_REVIEW_CHARGE_IND_MIN,   MODE_REVIEW_CHARGE_SUMMARY, MODE_REVIEW_CHARGE_IND_MAX,
                                 
                                    //Dummy settings change mode
                                    MODE_SETTINGS_STARTUP_CHANGE, MODE_SETTINGS_STARTUP,MODE_SETTINGS_STARTUP_CHANGE, MODE_SETTINGS_STARTUP_CHANGE, MODE_SETTINGS_STARTUP_CHANGE_SET,
                                    MODE_SETTINGS_LIGHT_CHANGE,   MODE_SETTINGS_LIGHT,  MODE_SETTINGS_LIGHT_CHANGE,   MODE_SETTINGS_LIGHT_CHANGE, MODE_SETTINGS_LIGHT_CHANGE_SET,
                                    MODE_SETTINGS_UNITS_DISTANCE_CHANGE, MODE_SETTINGS_UNITS_DISTANCE, MODE_SETTINGS_UNITS_DISTANCE_CHANGE, MODE_SETTINGS_UNITS_DISTANCE_CHANGE, MODE_SETTINGS_UNITS_DISTANCE_SET,
                                    MODE_SETTINGS_UNITS_SPEED_CHANGE, MODE_SETTINGS_UNITS_SPEED, MODE_SETTINGS_UNITS_SPEED_CHANGE, MODE_SETTINGS_UNITS_SPEED_CHANGE, MODE_SETTINGS_UNITS_SPEED_SET,
                                    MODE_SETTINGS_UNITS_TEMP_CHANGE, MODE_SETTINGS_UNITS_TEMP, MODE_SETTINGS_UNITS_TEMP_CHANGE, MODE_SETTINGS_UNITS_TEMP_CHANGE, MODE_SETTINGS_UNITS_TEMP_SET,
                                    MODE_SETTINGS_THROTTLE_MIN_CHANGE, MODE_SETTINGS_THROTTLE_MIN, MODE_SETTINGS_THROTTLE_MIN_CHANGE, MODE_SETTINGS_THROTTLE_MIN_CHANGE, MODE_SETTINGS_THROTTLE_MIN_SET,
                                    MODE_SETTINGS_THROTTLE_MAX_CHANGE, MODE_SETTINGS_THROTTLE_MAX, MODE_SETTINGS_THROTTLE_MAX_CHANGE, MODE_SETTINGS_THROTTLE_MAX_CHANGE, MODE_SETTINGS_THROTTLE_MAX_SET,
                                    MODE_SETTINGS_THROTTLE_NULL_CHANGE, MODE_SETTINGS_THROTTLE_NULL, MODE_SETTINGS_THROTTLE_NULL_CHANGE, MODE_SETTINGS_THROTTLE_NULL_CHANGE, MODE_SETTINGS_THROTTLE_NULL_SET,
                                    MODE_SETTINGS_THROTTLE_AUTO_CHANGE, MODE_SETTINGS_THROTTLE_AUTO, MODE_SETTINGS_THROTTLE_AUTO_CHANGE, MODE_SETTINGS_THROTTLE_AUTO_CHANGE, MODE_SETTINGS_THROTTLE_AUTO_SET,
                                    MODE_SETTINGS_IDLE_TIMEOUT_CHANGE, MODE_SETTINGS_IDLE_TIMEOUT, MODE_SETTINGS_IDLE_TIMEOUT_CHANGE, MODE_SETTINGS_IDLE_TIMEOUT_CHANGE, MODE_SETTINGS_IDLE_TIMEOUT_SET,
                                    
                                    //Go nowhere startup mode
                                    MODE_STARTUP,                 MODE_STARTUP,         MODE_STARTUP,                 MODE_STARTUP,             MODE_STARTUP,
                                    MODE_STARTUP_ERROR,           MODE_MONITOR_RUN,     MODE_STARTUP_ERROR,           MODE_STARTUP_ERROR,       MODE_MONITOR_RUN,
                                    MODE_INVALID };


One, very useful mechanism for writing code with the Arduino is to use a serial channel that sends ascii characters back through the usb connection that can be monitored by the PC that it is connected to.  There is a Serial library for doing this.

Serial.begin(9600);
Serial.print("The have been ");
Serial.print(millis());
Serial.println(" microseconds since startup");

A section of code that I usually add is for non-critical regular timing operations. I do this so that some operations can happen roughly every second or half second or two seconds etc.

void everyHalfSecond()
{
}

void everyFiveMillisecs()
{
}

void everyTenthOfASecond()
{
}

void everyTwoSeconds()
{
}

void everyFiveMintues()
{
}

void processRegulars(unsigned long time)
{

   if (time<everyFiveMillisecsUpdate || time>everyFiveMillisecsUpdate+5)
   {
     everyFiveMillisecsUpdate = time;
     everyFiveMillisecs();
   }
 
   if (time<everyTenthOfASecondUpdate || time>everyTenthOfASecondUpdate+100)
   {
     everyTenthOfASecondUpdate = time;
     everyTenthOfASecond();
   }
  
   if (time<everyHalfSecondUpdate || time>everyHalfSecondUpdate+500)
   {
     everyHalfSecondUpdate = time;
     everyHalfSecond();
   }

   if (time<everyTwoSecondUpdate || time>everyTwoSecondUpdate+2000)
   {
     everyTwoSecondUpdate = time;
     everyTwoSeconds();
   }
   
   if (time<everyFiveMinutesUpdate || time>everyFiveMinutesUpdate+300000)
   {
     everyFiveMinutesUpdate = time;
     everyFiveMintues();
   }
   
}

void loop() 
{
   unsigned long time = millis();
   processRegulars(time);
  ...
}

Key presses

As is always the case when you are interfacing with a physical system you have to take into account physical effects. So for example, there is some code that handles the debouncing of the switches in the keys. When pressing any key, even on the keyboard on a computer, in reality the switch doesn’t go just from off to on, it bounces between the two states until settling on the final one. Any CPU is quick enough to detect this. You can solve this problem with hardware but actually it is just as easy to do with software. The way to do this is just to only assume a new state if the state hasn’t changed for some time to take into account the bounce time.

Another mechanism I added was to fake the hardware key presses by reading keys back from the serial port. That way I could debug the software without having the physical keyboard connected.

For the hard keys, I wanted to only send a signal out when a key press was released.

int processKeys()
{
  int key = readKey();

  if (key == lastKey)
  {
    if (keyConsistentCount<=DEBOUNCE_LIMIT)
    {
      keyConsistentCount++;
    }
  }
  else
  {
    keyConsistentCount = 0;
  }
  lastKey = key;

  if (keyConsistentCount>DEBOUNCE_LIMIT)
  {
    lastSentKey = key;    
  }
  return lastSentKey;
}

int processManagedKey()
{
  //Sends out a single key press per key up event
  int key;
  int managedKey = KEY_NONE;
  if (Serial.available())
  {
    int serialKey = processKeyFromSerial();
    if (serialKey!=KEY_NONE)
    {
      return serialKey;
    }
  }
  key = processKeys();
  managedKey = KEY_NONE;

  if (lastManagedKey!=KEY_NONE && key == KEY_NONE)
  {
   managedKey = lastManagedKey;
  }

  lastManagedKey = key;
  if (managedKey>KEY_SELECT)
  {
     Serial.print("Unknown Key - ");
     Serial.println(managedKey);
  }
  return managedKey; 
}

Battery measurement

Because I wanted to shrink the hardware, I ended up using fixed resistors for the opto-isolators. This meant that I had to calibrate the voltages in software. So, I ended up building a test rig that could change the voltages for each cell (on the bench) and then I wrote a program to calculate the curve of the line so that I could get accurate voltages on each cell. I used the raw readings at three different voltages to calculate this. The response was almost linear but not quite. I would calculate the curve then cache the parameters so that the voltages could be calculated quickly. The table ended up like this:

#define BATT_1 2.48
#define BATT_2 3.20
#define BATT_3 3.96

//Following are individual Analog readings at 2.5 and 3.25 and 4v respectively for each battery 
const int battery_lines[20][3] =
{
  89,169,265,    //1
  96,184,288,    //2
  96,183,371,    //3
  127,238,307,    //4
  103,196,272,    //5
  91,173,386,    //6
  132,247,386,    //7
  91,172,271,    //8
  94,178,279,    //9
  143,266,416,    //10
  105,198,311,    //11
  98,185,292,    //12
  95,179,282,    //13
  97,184,290,    //14
  92,174,274,    //15
  133,249,390,    //16
  95,178,281,    //17
  93,174,274,    //18
  129,239,375,    //19
  102,190,300     //20
};

Real-time clock

Having a clock on the bike (shown on the dashboard) was kind of essential for me. Obviously, I was using this bike for commuting so knowing what time it was as I was riding along would tell me if I would make it to work on time or not! The realtime clock I used had a Adafruit PCF8523 which used a simple cell battery to keep the time – good for five years before replacing the battery! This has a library that does all the hard work that you can use with the arduino. I made sure that you could adjust the time and date using the interface – it is in the state table. Getting the time is pretty straightforward using the library. I also put in a warning that would tell me when the battery was getting low! Weirdly, it took more code to set the time because I had to write something that worked out how many days there were in each month based on leap years etc.

#include <Time.h>
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;

void setupRTC()
{
  Wire.begin();
  delay(1000);
  RTC.begin();
  if (! RTC.isrunning()) 
  {
    Serial.println("RTC is NOT running!");
    // following line sets the RTC to the date & time this sketch was compiled
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
}

//Time String is expected to be at least 5 chars and a null
void getCurrentTime( char *timeString)
{
  DateTime now = RTC.now();
  sprintf(timeString,"%0.2d:%0.2d",now.hour(), now.minute());
  timeString[5]=0;
}

//Returns millivolts
unsigned long getRTCBatteryVoltage()
{
  unsigned long val = analogRead(ANALOG_RTC_BAT);
  return (val*5000)/1023;
}

void getWholeTime(int *yr, int *mnth, int *date, int *hr, int *mn)
{
  DateTime now = RTC.now();
  *yr = now.year();
  *mnth = now.month();
  *date = now.day();
  *hr = now.hour();
  *mn = now.minute();
}

void setWholeTime(int yr, int mnth, int date, int hr, int mn)
{
  setTime(hr,mn,0,date,mnth,yr); 
  RTC.adjust(now());
}

Display

On the dashboard, I had two LCD displays. On one I wanted to put the speed, total voltage, time, temperature, ignition status, throttle position etc. On the second LCD, which only had one line I put only the mileage unless the user was going through the menus, in which case it displayed the menu that you were currently in. One of the issues with the big display was trying to show large numbers that could be seen during riding without having to concentrate too much. Luckily, each LCD has 8 customisable characters. This means that I could do custom shapes for each number to show the corners etc.

//These are simple character fragments that are used to build up the numbers

#define lcs 0 //Large char custom ascii start
#define bot lcs
#define top lcs+1
#define bth lcs+2

#define ROTATING_TENTH 1 //Which char to use as a tenth of a mile
#define UNIT_GCHAR  2 //Which char to use as the unit char

uint8_t largeCustomDigitChars[3][8] = 
{
 {B00000,  B00000,  B00000,  B00000,  B11111,  B11111,  B11111,  B11111}, //Bottom Mid
 {B11111,  B11111,  B11111,  B11111,  B00000,  B00000,  B00000,  B00000 }, //Top 
 {B11111,  B11111,  B00000,  B00000,  B00000,  B00000,  B11111,  B11111 } //Top and Bottom
};

byte largeDigitCharPatterns[10][12] =
{
  bot,bot,bot,255,32,255,255,32,255,top,top,top, //0	
  bot,bot,32,32,255,32,32,255,32,top,top,top,    //1	
  bot,bot,bot,bot,bot,255,255,32,32,top,top,top, //2
  bot,bot,bot,0,bot,255,32,32,255,top,top,top,   //3
  bot,32,32,255,bot,bot,32,255,32,32,top,32,     //4
  bot,bot,bot,255,bot,bot,32,32,255,top,top,top, //5
  bot,bot,bot,255,bot,bot,255,32,255,top,top,top,//6
  bot,bot,bot,32,bot,255,32,255,32,32,top,32,    //7
  bot,bot,bot,255,bot,255,255,32,255,top,top,top,//8
  bot,bot,bot,255,bot,255,32,32,255,32,32,top    //9
};


byte midDigitCharPatterns[10][6] = 
{
  255,top,255,255,bot,255,	 //0
  top,255,32,bot,255,bot,	 //1
  top,bth,255,255,bth,bot,	 //2
  bth,bth,255,bth,bth,255,	 //3
  255,bot,bot,32,255,32,	 //4
  255,bth,top,bot,bth,255,	 //5
  255,bth,top,255,bth,255,	 //6
  top,top,255,32,255,32,	 //7
  255,bth,255,255,bth,255,	 //8
  255,bth,255,32,32,255		 //9
};

byte scroller_data[]=
{
  B01110,  B10001,  B10011,  B10101,  B11001,  B10001,  B01110,  B00000,  B00000,  B00000, //0
  B00100,  B01100,  B00100,  B00100,  B00100,  B00100,  B01110,  B00000,  B00000,  B00000, //1 
  B01110,  B10001,  B00001,  B00010,  B00100,  B01000,  B11111,  B00000,  B00000,  B00000, //2
  B11111,  B00010,  B00100,  B00010,  B00001,  B10001,  B01110,  B00000,  B00000,  B00000, //3
  B00010,  B00110,  B01010,  B10010,  B11111,  B00010,  B00010,  B00000,  B00000,  B00000, //4
  B11111,  B10000,  B11110,  B00001,  B00001,  B10001,  B01110,  B00000,  B00000,  B00000, //5
  B00110,  B01000,  B10000,  B11110,  B10001,  B10001,  B01110,  B00000,  B00000,  B00000, //6
  B11111,  B00001,  B00010,  B00100,  B01000,  B01000,  B01000,  B00000,  B00000,  B00000, //7
  B01110,  B10001,  B10001,  B01110,  B10001,  B10001,  B01110,  B00000,  B00000,  B00000, //8
  B01110,  B10001,  B10001,  B01111,  B00001,  B00010,  B01100,  B00000,  B00000,  B00000, //9
  B01110,  B10001,  B10011,  B10101,  B11001,  B10001,  B01110,  B00000,  B00000,  B00000  //0 again - wrap around

};

Each LCD also has a backlight that can be turned on and off. I made this light software controlled and was initially intending to sense the status of the headlights and turn them on and off with the headlights. However, in the end, it seemed better to just turn this on and leave it on when the system was running.

Settings

The system needs to store not just settings for the user interface but also the current mileage and historical information about the batteries during a run phase and a charge phase. If there were any errors that happened during charge (when the bike was unattended), this would also be stored. Probably the most critical one is the mileage. Of course, this cannot ever be forgotten. The Arduino has a section of flash ROM where information like this can be permanently stored. One of the issues is that it can only be written to a fixed number of times, so I couldn’t just continually write to it. The actual mileage (internally) was stored in meters, even though the smallest unit displayed was .1 of whatever unit was currently being used (miles or km). As I stated before though, I had a software controlled battery backup. This meant that I could write the mileage at the point the ignition was turned off. The unit stays on for 1 to 10 minutes (configurable) after ignition off and if the bike is plugged in then the unit stays on to monitor the battery charging process. All this information is stored and can be reviewed using the interface on the dash. Any low voltage batteries can be checked.

All these settings were stored in a structure that is written to the flash ROM. The first byte stored is always the version of the settings. I upgraded the settings version 7 times during the development process and I wanted to make sure no settings were lost. Especially if I wanted to add features after installation.

struct storedSettings 
{
  byte configVersion;  //version of settings	
  byte startupMode;    //startup in run mode
  byte lightMode;      //light on/off or auto
  byte speedUnit;      //miles or km
  byte distanceUnit;   //miles of km
  unsigned long metres;  //actual mileage reading
  int minThrottle;  //min throttle sense value
  int maxThrottle;  //max throttle sense value
  unsigned long lastChargeTime;  //How long did the charge cycle last for
  unsigned long lastRunTime;     //How long did the run cycle last for
  unsigned long lastRunDistance; //How far was ridden during the last run
  batteryInfo_t chargeBatteries[20]; //Battery charge information	
  batteryInfo_t runBatteries[20];    //Battery discharge information
  byte temperatureUnit;	//degrees F or degrees C
  int nullThrottleMin;  //How much tolerance before we assume throttle is 0
  int idleTimeout; //In mimutes - how much time in idle before switching off?
}

Temperature

As I mentioned before, I added a temperature sender which used the 1-wire interface to communicate. The 1-wire interface basically is a “slow” interface standard that can be used to communicate between multiple devices using only one wire. However, the DS18B20 device that I used actually has three. A power wire, ground and communications. One of the issues with the software was that all the demo code that used the OneWire library and the DallasTemperature library had a built in pause to wait for information to come back from the temperature sensor. Obviously, I was using this in a real-time system, I could not afford to pause the CPU while waiting. I ended up creating an asynchronous communication mechanism so I could fire off a command to the temperature sensor then every other second alternate between reading the temperature and requesting another temperature read (remember the “everySecond” function from earlier). This is real-time enough for a temperature sensor!

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(PIN_TEMPERATURE);
// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);
float m_temperature = 0.0;
bool requestPhase = true;

void setupTemperature()
{
  sensors.begin();
  //Initially wait for first one
  sensors.setWaitForConversion(true);
  sensors.requestTemperatures();
  m_temperature = readTemperatureByUnit(getTemperatureScale());
  sensors.setWaitForConversion(false);  
}

//This function shouldn't be called from outside
float readTemperatureByUnit(byte unit)
{
  float temp = 0;
  if (unit == UNIT_C)
  {
    temp = sensors.getTempCByIndex(0);  
  }
  else
  {
    temp = sensors.getTempFByIndex(0);  
  }
  return temp;
}

float getTemperature()
{
  return m_temperature;
}

void processTemperature()
{
    //Should be called about once a second or longer
    if (requestPhase)
    {
      requestPhase = false;
      sensors.requestTemperatures();    
    }
    else
    {
      requestPhase = true;
      m_temperature = readTemperatureByUnit(getTemperatureScale());
    }
}

Speed

One slightly tricky issue was reading the speed. I had set up an interrupt connected to the line from the hall effect sensor from the hub motor. I worked out how many windings there were in the wheel (I only used one of the three phases). Then I calculated the distance covered by one rotation of the outer part of the tyre and got a value for number of pulses per meter. When I tested this is turned out to be spot on. This wasn’t the tricky part though. The problem is the Arduino is essentially an 8-bit microcontroller and the number I am using to count the number of ticks is a 32-bit number. So, when the CPU reads this value, it essentially has to do 4 fetches from memory to read the number. Normally, this wouldn’t be a problem but because I am using interrupts it is entirely possible for that number to change while I am halfway through reading it, if the hall effect sensor just happens to change at that time. I tried a few different ways to fix this and in the end the simplest way was to just read the value more than once until it was the same value for two consecutive reads (up to a max of 10!). This worked.

volatile unsigned long speedo_ticks = 0;
double speedoTicksPerMetre = 28.0/1.468;  //Guess
unsigned long initialMetreCount = 53227000;

unsigned long lastTimeTicksRead = 0;
unsigned long lastTickReading = 0;

#define MILES_PER_KM 0.621371

void setupSpeedo()
{
  pinMode(PIN_SPEEDO, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_SPEEDO), speedoTick, FALLING);
  initialMetreCount = getMetres();
}

void speedoTick()
{
  speedo_ticks++;
}
unsigned long getLastTimeTicksRead()
{
  return lastTimeTicksRead;
}

unsigned long getSpeedoTicks()
{
  //Have a go at reading a potentially volatile value
  int i;
  unsigned long ticks=speedo_ticks;
  for (i=0;i<10;i++)
  {
    if (speedo_ticks == ticks)
    {
      break;
    }
    ticks = speedo_ticks;
  }
  return ticks;
}

int getCurrentSpeed(byte unit)
{
  double spd = 0;
  unsigned long ticks = getSpeedoTicks();
  unsigned long tim = millis();
  spd = (double)(ticks-lastTickReading)/(double) (tim-lastTimeTicksRead);
  spd = spd/speedoTicksPerMetre;
  lastTickReading = ticks;
  lastTimeTicksRead = tim;
  //Speed is currently in metres per millisecond
  //which just happens to be the same as km per second
  //convert it to km per hour
  spd = spd *60.0 * 60.0;

  if (unit == UNIT_MILE)
  {
    spd = spd * MILES_PER_KM;
  }
  return (int) spd;  
}

unsigned long getCurrentDistance()
{
  //returns distance in metres
  unsigned long totalMetres = initialMetreCount + (unsigned long) ((double) getSpeedoTicks()/speedoTicksPerMetre);    
  return totalMetres;
}

unsigned long getDistanceInUnit(unsigned long distance, byte unit)
{
  if (unit == UNIT_MILE)
  {
    return (unsigned long) ((double) distance * MILES_PER_KM/10);   
  }
  return (distance /10);   
}
//Warning distance is returned as 100x the unit
unsigned long getCurrentDistanceInUnit(byte unit)
{
  return getDistanceInUnit(getCurrentDistance(), unit);
} 

String getUnitString(int unit)
{
  if (unit==UNIT_KM)
  {
    return ("KM");
  }
  return ("Miles");
}

void saveDistanceOffset()
{
  //resets the tick offset count and stores the actual mileage
  unsigned long distance = getCurrentDistance();
  Serial.print("distance =");
  Serial.println(distance);
  setMetres(distance);
  detachInterrupt(digitalPinToInterrupt(PIN_SPEEDO));
  speedo_ticks = 0;
  attachInterrupt(digitalPinToInterrupt(PIN_SPEEDO), speedoTick, RISING);   
  lastTimeTicksRead = millis();
  lastTickReading = 0;
  initialMetreCount = distance;
}

Summary

As I said at the start of this page, I have just included the parts of code that I thought was interesting or solved a particular problem. There is a lot of code I haven’t shown but there is probably not much interesting in there unless you are recreating the entire project. I usually start any software at version 0.1 then as I add major features I increase by 0.1, or bug fixes by 0.01. At the time the software was running fully in the bike, it was at version 1.20. It eventually made it to 1.21 but that story is on the next page! I had to learn a lot to create the software, not necessarily about software engineering but certainly about the nuances of the hardware of the arduino, the peripherals and the PCB that I had made. Just as a reminder of all the systems that had to work together, not just with the software but also the hardware:

  1. Multiplexed battery interface
  2. Real time clock
  3. Temperature sensor
  4. Bike status sensors (throttle, lights, ignition)
  5. Speed sensor and odometer
  6. User Keyboard interface
  7. 2 LCDs
  8. Software controlled battery backup system
  9. Buzzer debug interface
  10. Battery performance history and storage

All these systems had to survive vibration, cold, heat, water even snow on a few occasions. For the installation into the scooter, please see the next page.

Previous

Leave a comment

Your email address will not be published. Required fields are marked *