Arduino program for bicycle speedometer
From ShawnReevesWiki
Jump to navigationJump to searchThe 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;
}