Go Down

Topic: My digital speedometer project (Read 19 times) previous topic - next topic


I figured I might as well join the forum and share some info about the speedometer project I've been working on over the last few months.

As of now it's running on a Seeeduino V328 with 2 stacked protoshields and a 20-digit 2-line LCD. Vehicle movement is sensed via an ABS sensor on the left front wheel, which provides 54 pulses per wheel revolution. This allows the displayed speed to be valid down to 1 MPH. There is also a 5+1 digit odometer, a 3+1 digit trip odometer, and a power-fail circuit that saves the odometer data into EEPROM when a power failure is detected. The power fail circuit uses a MAX690 IC to trigger an interrupt when the incoming power supply voltage falls below 10 volts. An electrolytic capacitor keeps the circuit alive just long enough for the data to get written to the EEPROM...almost. I recently discovered that the last byte to get written isn't quite making it. (Time for a bigger capacitor)

Vehicle speed is displayed on the LCD in a 2-line block font, which was inspired by the MPGuino, and uses code that I adapted from an example posted by whosawhatsis.

I've been working on a menu to allow different options. The first is calibration, which can be used to dial in the accuracy while in use. The calibration value is also saved in EEPROM when exiting the calibration function. The second menu item brings up a distance meter that reads in feet. When I discovered that the ABS sensor sends a pulse for every 1.86 inches of travel, I thought "Why not?"

Eventually I plan to make a PCB for a DIP Atmega328 and all the additional circuitry, once I decide that I've tweaked the design enough. I also have a backlit 20-digit 4-line LCD that I intend to use, with a 3-line block font for the speed.
Inconveniencing electrons one drop at a time


Here's an early picture with the odometer in its infancy:

Here's a more recent one with updated odometer. No, I wasn't really driving that fast! :D I was feeding a fast square wave into the circuit to run a test.

One more showing the "footometer" mode (it rolls over at 100000 feet):

Inconveniencing electrons one drop at a time


Jan 02, 2011, 10:35 am Last Edit: Jan 02, 2011, 10:35 am by UnaClocker Reason: 1
I like it. :) Any chance you'll share your sketch?
Brian from Tacoma, WA
Arduino evangelist - since Dec, 2010.


Jan 03, 2011, 07:20 am Last Edit: Jan 03, 2011, 07:41 am by ElectricWater Reason: 1
I couldn't figure out another way to post the sketch other than cut it up into chunks, so here goes...

Code: [Select]
Speedometer / Odometer Arduino Sketch
by ElectricWater

dfowler / uchobby.com for Timer2 code
MPGuino for Large Font inspiration
whosawhatsis / Arduino forum for Large Font code

This sketch detects a square wave input using external interrupt 0.
The signal's frequency is proportional to the vehicle's speed.
The sketch also detects internal Timer2's overflow.
The two are used to calculate the vehicle's speed, and the square wave count is also
used to report the distance travelled.

Interrupt 0 (Digital pin 2) is connected to the vehicle's speed sensor.
An interrupt service routine triggered by a rising edge on Interrupt 0 increments
a counter (SensorCount).

Timer2 in the AVR is reconfigured to run at 125 KHz.
An interrupt service routine triggered by Timer2's overflow increments a counter
(TimerCount) by 8 and tests to see if the counter has reached a predetermined constant
value (CalValue - used for calibration). If so, 3 things happen:
- The value of SensorCount is copied intothe variable "Freq" for use in the main loop
- The "Trigger" flag gets set
- SensorCount and TimerCount get reset
TimerCount reaches CalValue at a very consistent rate, which is used to calculate the vehicle speed.
When CalValue is properly calibrated the value of Freq is equal to the actual speed in Miles per Hour.

The Timer2 ISR aslo causes digital pin 4 to toggle between high and low for testing purposes.

Note: The reason for incrementing TimerCount by 8 is that the sketch when first written
originally configured Timer2 to run at 2 MHz, which is 8 times faster than its current 250 KHz.
Slowing down Timer2 to 1/8 its original speed and having the ISR add 8 instead of 1
allows the AVR to spend less time servicing interrupts. This change resulted in no
noticeable degradation in speedometer function.

The main loop tests to see if the Trigger flag is set. If so, several things happen:
- The variable "Speed" is set to the average of the 3 most recent cycles of "Freq"
- Speed then gets printed on the LCD in large 2-line block font.
- Freq is used to update the odometer
- Odometer information is printed to the LCD...as long as we're not in a menu

The main odometer uses the variables TotalClicks, TotalTenths, and TotalMiles.
The trip odometer uses the variables DistanceClicks, DistanceTenths, and DistanceMiles.

TotalClicks is increased by the value of Freq each time Trigger is set, which means that it is
a running total of the speed sensor's pulses. CalDist is a calibration value that when adjusted
correctly is equal to the number of sensor pulses per 1/10 mile. Therefore, when TotalClicks
reaches CalDist the vehicle has travelled 1/10 mile, so TotalTenths is incremented and CalDist
is subtracted from TotalClicks. When TotalTenths reaches 10, TotalMiles is incremented and
TotalTenths is reset to zero. When TotalMiles reaches 100000 it gets rolled over back to zero.

The trip odometer then goes through the same process, except that DistanceMiles rolls over at 1000.

A trip odometer reset button is provided via a pushbutton switch connected to digital pin 6 (Button1).
Digital pin 6 has its internal pull-up resistor turned on and the pushbutton drives the pin low when pressed.
When a low is detected it resets DistanceClicks, DistanceTenths, and DistanceMiles to zero.

Interrupt 1 (Digital pin 3) is connected to a power-fail signal that goes low when the unregulated DC voltage
drops below ~10 volts. When ISR1 is called it copies all of the odometer data into EEPROM for use the next
time the circuit is powered up. Upon power-up the EEPROM is read back into the odometer variables.

An options menu is beginning to be implemented. Menu navigation is controlled by the 4
pushbutton switches connected to digital pin 6 and analog pins 0-2 (digital pins 14-16).
The A0 button (Button2) is used to enter the menu mode, and also to exit menu mode.
A1 (Button3) and A2 (Button4) is used to flip through the menus.
D6 (Button1) is used to select a menu option.
Once inside a menu option, Button3 and Button4 is used to change the data.
Button1  is used as an "OK" or "Enter" button, while Button2 is used as an "Exit" or "Escape" button.

The first menu option is to adjust the calibration value (CalDist) for the speed sensor count. (CalValue
is calculated from CalDist). When the CalDist is changed and "OK-ed" it will be written into EEPROM,
which is read again on power-up.

The second menu option is to display a temporary distance meter that reads in feet.

MenuMode is an integer variable that will change depending on which menu the user is in.
MenuMode = 0, no menu
MenuMode = 1, calibrate speedometer
MenuMode = 2, foot distance display mode

-----The circuit connections-----

Speed sensor input to Digital pin 2 (interrupt 0)
Power fail input to Digital pin 3 (interrupt 1)
Timer2 test output is on Digital pin 4

LCD hookups:
* LCD Vss pin (pin 1) to ground
* LCD Vcc pin (pin 2) to +5V
* LCD VO pin (pin 3) to ground
* LCD RS pin (pin 4) to Digital pin 7
* LCD R/W pin (pin 5) to Digital pin 5
* LCD Enable pin (pin 6) to Digital pin 8
* LCD D4 pin (pin 11) to Digital pin 9
* LCD D5 pin (pin 12) to Digital pin 10
* LCD D6 pin (pin 13) to Digital pin 11
* LCD D7 pin (pin 14) to Digital pin 12

Pushbutton switches:
Button1 to Digital pin 6
Button2 to Analog pin 0 (Digital pin 14)
Button3 to Analog pin 1 (Digital pin 15)
Button4 to Analog pin 2 (Digital pin 16)

Physical protoshield layout of the pushbuttons:
  ----  ----
  ----  ----


// include the EEPROM library code:
#include <EEPROM.h>

// include the library code:
#include <LiquidCrystal.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(7, 5, 8, 9, 10, 11, 12);

// build 2-line digit font data array
// Digits are 3 characters wide.
byte bignums[10][2][3] = {
// Define which characters to use for each number. 255 is a solid block; 254 is a space  
// The format is { {TopLeft, TopMiddle, TopRight}, {BottomLeft, BottomMiddle, BottomRight} }
{ {255, 0, 255}, {255, 1, 255} },        // data to display "0"
{ {0, 255, 254}, {1, 255, 1} },          // data to display "1"
{ {2, 2, 255}, {255, 1, 1} },            // data to display "2"
{ {0, 2, 255}, {1, 1, 255} },            // data to display "3"
{ {255, 1, 255}, {254, 254, 255} },      // data to display "4"
{ {255, 2, 2}, {1, 1, 255} },            // data to display "5"
{ {255, 2, 2}, {255, 1, 255} },          // data to display "6"
{ {0, 0, 255}, {254, 255, 254} },        // data to display "7"
{ {255, 2, 255}, {255, 1, 255} },        // data to display "8"
{ {255, 2, 255}, {254, 254, 255} }       // data to display "9"

int CalDist;                              // Calibration value for the odometer
                                         // Increase to increment slower, decrease to increment faster
                                         // Equal to the number of speed sensor pulses per 1/10 mile

int CalValue;                             // Calibration value for the speedometer
                                         // Gets calculated from CalDist
                                         // Increase to read faster, decrease to read slower
/*   ---Time and distance math---

 Timer2 runs at 250 KHz and counts from 0 to 255 before overflow.
 Each overflow triggers an interrupt adding +8 to TimerCount. This happens 976.5625 times per second.
 Therefore, TimerCount increases by an average of 7812.5 per second. We'll call these increases in value "counts".
 We'll call one pulse from the speed sensor 1 click.
 CalValue "counts" = 1 mile/hour
 Formulas for relating CalValue to CalDist are as follows:
                                  7812.5 counts       1 click       1 hour   3600 sec      1 mile       2812500
 CalDist (clicks per 1/10 mile) = ------------- x --------------- x ------ x -------- x ------------ = --------
                                      1 sec       CalValue counts   1 mile    1 hour    10 1/10 mile   CalValue
                     7812.5 counts   3600 sec   1 hour      1 mile      1 1/10 mile    2812500
 CalValue (counts) = ------------- x -------- x ------ x ------------ x ----------- = ---------
                         1 sec        1 hour    1 mile   10 1/10 mile     CalDist      CalDist  
Inconveniencing electrons one drop at a time


Code: [Select]

const byte SqWavePin = 4;                 // Square wave genereated from digital pin 4
const byte Button1 = 6;                   // Pushbutton switch D6, connected to ground
const byte Button2 = 14;                  // Pushbutton switch A0, connected to ground
const byte Button3 = 15;                  // Pushbutton switch A1, connected to ground
const byte Button4 = 16;                  // Pushbutton switch A2, connected to ground

byte MenuMode = 0;                        // MenuMode 0 means not in a menu

boolean MenuModeActive = false;           // Only goes true when a menu item has been selected

unsigned long ButtonsOffTime;             // How long since last button was released

boolean Button1State;                     // State of Button1 from last cycle
boolean Button2State;                     // State of Button2 from last cycle
boolean Button3State;                     // State of Button3 from last cycle
boolean Button4State;                     // State of Button4 from last cycle
boolean ButtonPressed = false;            // Becomes true when a button press is detected
boolean ButtonPressedPrev = false;        // Previous value of ButtonPressed

int CalDistPrev;                          // Previous value of CalDist during calibration

int SensorCount = 0;                      // Speed Sensor pulse count
int TimerCount = 0;                       // Timer2 overflow count
int Freq;                                 // Copy of SensorCount, and is the actual vehicle speed
boolean Trigger = false;                  // When it becomes true it triggers the print cycle
boolean SqWaveOut = false;                // Used to toggle output state of pin 4
int FreqPrev1 = 0;                        // Previous value of Freq, used for averaging speed
int FreqPrev2 = 0;                        // Another previous value of Freq, used for averaging speed
int Speed;                                // Average of all 3 Freq variables, and is what is displayed on LCD

int DistanceMiles;                        // Cumulative distance travelled in miles
int DistanceTenths;                       // Cumulative distance travelled, tenths digit
int DistanceClicks;                       // Cumulative speed sensor pulses received, reduced by CalDist each 1/10 mile
unsigned long TotalMiles;                 // Total distance travelled in miles
int TotalTenths;                          // Total distance travelled, tenths digit
int TotalClicks;                          // Total speed sensor pulses received, reduced by CalDist each 1/10 mile

long DistanceFeet;                        // Cumulative distance travelled in feet, temporary display option
unsigned int DistanceFeetTenths;          // Cumulative distance travelled in feet, tenths digit, temporary display option
int FeetScaleFactor;                      // Scale Factor used to calculate feet from speed sensor pulses

ISR(TIMER2_OVF_vect) {                    // This is the Interrupt Service Routine, called when Timer2 overflows

 TimerCount +=8;                         // Increment TimerCount by 8
 if (TimerCount > CalValue)  {           // Check to see if it's time to display the count
   Freq = SensorCount;                   // Copies the interrupt 0 count into variable "Freq"
   Trigger = true;                       // sets "Trigger" to let the main loop know it's time to print data
   SensorCount = 0;                      // Reset the interrupt 0 count
   TimerCount -= CalValue;               // Reset Timer2 count, but keep the excess for next time around
 SqWaveOut = !SqWaveOut;                 // Toggle SqWaveOut to its opposite state
 digitalWrite(SqWavePin, SqWaveOut);     // Set pin 4 to the new state of SqWaveOut


void setup()  {
// turn on DigitalPin 13 LED to signal that the custom characters have been loaded into the LCD  
 pinMode(13, OUTPUT);
 digitalWrite(13, HIGH);

 pinMode(SqWavePin, OUTPUT);                  // Output on pin 4 will be oscillating
 pinMode(Button1, INPUT);                     // Set Button1 pin as input
 pinMode(Button2, INPUT);                     // Set Button2 pin as input
 pinMode(Button3, INPUT);                     // Set Button3 pin as input
 pinMode(Button4, INPUT);                     // Set Button4 pin as input
 digitalWrite(Button1, HIGH);                 // Turn on Button1's internal pull-up resistor
 digitalWrite(Button2, HIGH);                 // Turn on Button2's internal pull-up resistor
 digitalWrite(Button3, HIGH);                 // Turn on Button3's internal pull-up resistor
 digitalWrite(Button4, HIGH);                 // Turn on Button4's internal pull-up resistor
 CalDist = (int(EEPROM.read(101)) * 256) + (int(EEPROM.read(102)));  // read CalDist out of EEPROM
 CalValue = int((2812500.0 / CalDist) + .5);               // calculate CalValue from CalDist

 // set up the LCD's number of columns and rows:

 // read total mileage out of EEPROM
 TotalMiles = (EEPROM.read(5) * 65536) + (EEPROM.read(6) * 256) + (EEPROM.read(7));
 TotalTenths = EEPROM.read(8);
 TotalClicks = (EEPROM.read(9) * 256) + (EEPROM.read(10));
 // read Distance out of EEPROM
 DistanceMiles = (EEPROM.read(0) * 256) + (EEPROM.read(1));
 DistanceTenths = EEPROM.read(2);
 DistanceClicks = (EEPROM.read(3) * 256) + (EEPROM.read(4));
// print the speed in big font
   lcd.setCursor(0, 0);                  // set the cursor to (13,0)
   Freq = Freq % 1000;                   // drop any digits above 999
   printbigchar(int(Freq / 100),0);      // print the speed hundreds
   Freq = Freq % 100;                    // drop any digits above 99
   printbigchar(int(Freq / 10),1);       // print the speed tens
   Freq = Freq % 10;                     // drop any digits above 9
   printbigchar(Freq,2);                 // print the speed ones
 // write Distance to LCD
 attachInterrupt(0, AddSensorCount, RISING);  // Interrupt 0 is on digital pin 2
 attachInterrupt(1, EEPROMwrite, FALLING);    // Interrupt 1 is on digital pin 3

 //  -------------------------------------------------------------------------------------
 //  This bit of code is adapted from an article by dfowler at uchobby.com
 //  http://www.uchobby.com/index.php/2007/11/24/arduino-interrupts/
 //Timer2 Settings: Timer Prescaler / 64, mode 0
 //Timmer clock = 16 MHz / 64 = 250 KHz or 0.5us
 TCCR2A = 0;
 TCCR2B = 1<<CS22 | 0<<CS21 | 0<<CS20;      // Set Timer2 frequency to 250 KHz
                                            // Used to be 010 for 2 MHz clock

 //Timer2 Overflow Interrupt Enable  
 TIMSK2 = 1<<TOIE2;
 //  -------------------------------------------------------------------------------------
 ButtonsOffTime = millis();                 // Set ButtonStateTime to current time
Inconveniencing electrons one drop at a time

Go Up