Arduino program for bicycle speedometer

From ShawnReevesWiki
Revision as of 12:48, 23 May 2014 by Shawn (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

The following code can be used to program a bicycle-speedometer running on an Atmel 328P AVR micro-controller.

For a discussion and circuit-diagram, see bicycle speedometer.

 /*
 Arduino program for a bicycle speedometer using a Hall effect sensor switch
   and a 16-character LCD display, by Shawn Reeves, 2014.
 On timing:
 micros() returns the number of microseconds since the Arduino board
   began running the current program.
 micros() returns an unsigned long integer, so it overflows to zero
   after 4,294,967,295 µs.
 millis() is similar except it counts milliseconds.
 NOTA BENE:Both millis() and micros() clear interrupts while they read the
   timer, so use them sparingly!
 
 On interrupt routine:
 attachInterrupt(interrupt, ISR, mode)
   interrupt: 0 means digital pin 2 and 1 means digital pin 3.
   Mega2560, Leonardo, and Due boards offer it on more pins.
   ISR: routine to call when interrupt happens.
   mode: LOW, CHANGE, RISING, FALLING (DUE boards also allow HIGH)
 We'll have two interrupts:one for magnet switch
   and one for button to change wheel-diameter.
 
 On debouncing:
 The wheel-size-adjustment button is debounced by discarding signals that come too soon,
   but it can still be triggered unintentionally when the button is released.
 Either fix that with hardware (a capacitor, which would waste power) or
   use LOW instead of FALLING and make sure it's low a few cycles.
 */
 #include <LiquidCrystal.h>
 //HARDWARE PIN SELECTION:
 //The pins are selected to minimize spaghetti-crossing on the breadboard,
 //and so are dependent on the microcontroller's physical config.
 const int buttonPin = 2;//must be pin 2 or 3
 const int buttonPinInt = 0;//for 328-based arduinos, 0 means digital I/O pin 2
 const int switchPin = 3;//must be pin 2 or 3
 const int switchPinInt = 1;//for 328-based arduinos, 1 means digital I/O pin 3
 /*How to create an LCD object:
 Choose an object name and one of the following data formats:
 LiquidCrystal objectname(rs, enable, d4, d5, d6, d7)//4-bit, no read line.
 LiquidCrystal objectname(rs, rw, enable, d4, d5, d6, d7) //4-bit with read line.
 LiquidCrystal objectname(rs, enable, d0, d1, d2, d3, d4, d5, d6, d7) //8-bit, no read line.
 LiquidCrystal objectname(rs, rw, enable, d0, d1, d2, d3, d4, d5, d6, d7) //8-bit with read line.
 */
 //Assign RS to dp10, atmega328 pin 16; E to dp9, 328 pin15; Data4-Data7 to dp5-8, 328 pin11-14:
 LiquidCrystal lcd(10,9,5,6,7,8);
 
 //CONDITIONAL, CONSTANT PARAMETERS:
 const unsigned long debounceSwitchms = 5000;//Our routine will ignore signals that come closer together than this amount of microseconds
 const unsigned long maxTime = 10000000;//The amount of microseconds before going into waiting mode.
 const unsigned long debounce_delay = 40000;  //how many loops before allowing diameter button to respond again.
 const byte diameter_max = 99;  //If over 99, need to change display routine since "dia.##cm" takes 8 spaces and ### would take nine.
 const byte diameter_min = 1;  //Change to a larger amount if this is annoying.
 const float ten_thousand_pi = 31415.9;
 const byte updateSpeed = 1;
 const byte updateDiameter = 2;
 
 //VARIABLES:
 //It's recommended all variables used in the interrupt service routine are considered volatile,
 // so not kept in multi-purpose registers but in dedicated memory slots:
 volatile unsigned long hitTime;
 unsigned long lastTime;//Track the time of the previous signal so we can calculate time elapsed since.
 unsigned long microsSince;//for determining idleness. 0 <= ul <= 4,294,967,295
 unsigned long periodMicroSeconds;
 float speedFloat;//we use floating point math for maximum precision.
 float circumference;
 word speedInteger;//this we will convert to a string to display.
 byte precision;  //to keep track of roughly how precise the frequency will be.
 byte updateDisplay;  //True this byte when display should be updated.
 word delayCounter;//can count 0 to 65535
 volatile byte inputMode;
 volatile byte diameterCm = 47;//default diameter in centimeters.
 volatile byte debounceMe;//When this is not zero, we know it hasn't been very long since the last interrupt.
 
 //FUNCTIONS:
 void setup(){//This is the routine done only once when the board is reset, or first receives power.
   pinMode(switchPin, INPUT_PULLUP);//get switchPin ready for interrupt input, activating (internal) pullup resistor Hall Switch expects.
   pinMode(buttonPin, INPUT_PULLUP);//get buttonPin ready for interrupt input, activating (internal) pullup resistor.
   delayMicroseconds(50000);  //Wait 0.05s for display to power.
   lcd.begin(8,2);//Columns and rows setup. Our LCD is 16x1 but communicates as an 8x2.
   //lcd.cursor();//Turn on the cursor so we see LCD is working. Useful for debugging.
   lcd.display();//Turn on LCD.
   circumference = (float)diameterCm * ten_thousand_pi;//calculate circumference in micrometers whenever we change the diameter
   waitingMode();
   attachInterrupt(buttonPinInt, increaseDiameter, FALLING);//Go to increaseDiameter whenever interrupt buttonPinInt goes from high to low
   attachInterrupt(switchPinInt, recordTime, FALLING);//Go to recordTime whenever interrupt switchPinInt goes from high to low
 }
 
 void loop() {//this is the main routine that begins after setup() and repeats
   if(hitTime>0){//There's a 1 in 4 billion chance hitTime is zero but represents an actual reading.
     if (0==precision) {//this is first time switch, so no reference, so only set precision flag and record time.
       precision = 1;//so we don't end up here next time
       lastTime = hitTime;//record now for reference next time.
     }
     else
     { periodMicroSeconds = hitTime - lastTime; //if timer has overflowed once since last time, ok since this subtraction will overflow back.
       if (periodMicroSeconds > debounceSwitchms) {//debounce by rejecting too-fast times
         precision = 1;//Set our flag so we know we have a valid period
         updateDisplay = updateSpeed;//We have a new period, so let's update the display
         lastTime = hitTime;//record now for reference next time.
       }
     }
     hitTime=0;//skip the above routine until another hit.
     detachInterrupt(buttonPinInt);//Only allow changing diameter when stopped to prevent distracted bicycling.
     inputMode=0;
   }
   //We don't want to read micros every trip through this loop--Reading micros causes interrupt issues.
   // So, if we increase a 16-bit (word) variable by one every trip, it will only overflow back to zero once every 65536 (2^16) trips.
   if(delayCounter++ > debounce_delay){
     delayCounter=0;
     microsSince = micros() - lastTime;//Keep track of idleness.
     if(debounceMe>0){debounceMe--;}//let button-routine know time has passed
   }
   if(precision > 0 && microsSince > maxTime){//It's been more than maxTime since the last hit, so go back into waiting mode
     waitingMode();
   }
   switch (updateDisplay) {
     case updateDiameter:
       lcd.clear();
       lcd.print("dia=");lcd.print(diameterCm,DEC);lcd.print("cm");
       updateDisplay=0;
       circumference = (float)diameterCm * ten_thousand_pi;//calculate circumference in micrometers whenever we change the diameter
       break;
     case updateSpeed:
       lcd.clear();
       updateDisplay = 0;//Don't come back here until next update is requested by changing this flag back to 1
       //at this point, speedFloat is the circumference of the wheel in micrometers
       speedFloat = circumference / (float)periodMicroSeconds;//now it is speed in m/s
       speedInteger = (word)speedFloat;//round to a number between 0 and 65535
       lcd.print(speedInteger,DEC);lcd.print("m/s");
       lcd.setCursor(0,1);
       //convert mps to mph
       speedFloat = speedFloat * 2.23693;//Since the above leaves us with cm/microsecond
       speedInteger = (word)speedFloat;//round to a number between 0 and 65535
       lcd.print(speedInteger,DEC);lcd.print("m/hr");
       break;
   }
 }
 void recordTime(){//this is the interrupt service routine to be called whenever switchPin's voltage falls 
 //we want as few instructions and variables inside this routine as possible, so we just gather the time and go.
   hitTime = micros();//reads a clock in microseconds
 }
 void increaseDiameter(){//this is the interrupt service routine to be called whenever buttonPin's voltage falls 
 //we want as few instructions and variables inside this routine as possible
   if(debounceMe==0){//only if debounce time has passed since last visit here.
     if(inputMode==0){inputMode=1;}
     else{
       if(++diameterCm>diameter_max){diameterCm = diameter_min;}
     }
     updateDisplay=updateDiameter;
     debounceMe=2;//this will have to be decreased twice in the main loop before we can enter this routine again.
     //That way we'll know we've gone at least one full trip through the delayCounter loop.
   }
 }
 void waitingMode(){
   precision = 0;//We use 'precision' as a flag to mark whether we've already made one clock-reading.
   lcd.clear();  //clear display and go home
   lcd.print(readmVcc());lcd.print("mV");//Track battery voltage
   lcd.setCursor(0,1);//second octet of characters is treated by this LCD as second line
   lcd.print("dia=");lcd.print(diameterCm,DEC);lcd.print("cm");
   attachInterrupt(buttonPinInt, increaseDiameter, FALLING);//Go to increaseDiameter whenever interrupt buttonPinInt goes from high to low
 }
 long readmVcc() {
   //From https://code.google.com/p/tinkerit/wiki/SecretVoltmeter
   //May be consistently under-reporting by 10%, but will let you know when battery is failing.
   long result;
   // Read 1.1V reference against AVcc:
   ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
   delayMicroseconds(2000); // Wait for Vref to settle
   ADCSRA |= _BV(ADSC); // Convert
   while (bit_is_set(ADCSRA,ADSC));
   result = ADCL;//get low byte
   result |= ADCH<<8;//get high byte
   result = 1126400L / result; // Back-calculate AVcc in mV
   return result;
 }