Code:
/*This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a
letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.*/
/*Original code by Tristan Fariss (aka bobski)
Modifications by */
//#define SAA1064display
#include <LiquidCrystal.h> //support library for character LCDs
#include <Wire.h> //support library for I2C
#ifdef SAA1064display
#include "SAA1064.ino" //support library for SA1064-driven LED display
#endif
#define primaryButtonArrayAnalogPin 0
#define numPrimaryButtons 5 //numPrimaryButtons should be 1 more than the number of hardware buttons to account for an idle state
#define numPrimaryPages 3 //the number of information/menu pages - used to define the size of the function pointer arrays
#define numSecondaryPages 2
#define numLEDpages 3
#define lcdBacklightPin 11
#define vssPulsesPerUnitDistance 4085 //vehicle-specific calibration information: how many VSS pulses to count as a unit of distance
#define vssTimeOut 250000 //how many microseconds to wait before deciding the car is stopped when receiving no VSS pulses (only affects displayed speed)
#define injectorOnePin 2
#define injectorTimeOut 125000 //how many microseconds to wait before deciding the injectors aren't firing
#define injectorOneActiveState LOW //whether the injector input is active high or low (valid values HIGH and LOW)
#define injectorMicrosecsPerUnitVolume 200000000 //microseconds of injector firing time per standard unit of volume (liters, gallons or whatever you like to use)
#define injectorFiringDelay -20
/* this is how many microseconds of discontinuity between the injector electrical signal and actual fuel flow time. this number gets subtracted from the measured
length of each injector firing event. if your injector closes faster than it opens, the fuel flow time will be shorter than the electrical pulse, and
injectorFiringDelay should be positive. if the injector opens faster than it closes, this value should be negative. */
#define distanceUnits "Mile"
#define distanceUnitsShort "Mi"
#define speedUnits "Miles/Hour"
#define speedUnitsShort "MPH"
#define volumeUnits "Gallons"
#define volumeUnitsShort "Gal"
#define flowUnits "Gal/Hour"
#define flowUnitsShort "GPH"
#define fuelEconomyUnits "Miles/Gal"
#define fuelEconomyUnitsShort "MPG"
#define engineSpeedUnits "Rev/Min"
#define engineSpeedUnitsShort "RPM"
typedef void (*fptype)(); //support for function pointers
int primaryButtonState = 0; //variables used for UI button handling
int primaryButtonPressed = 0;
/* buttonFunctions is an array of function pointers used to make the software functionality of the physical buttons
change depending on context. It is a 2D array, the first coordinate being the page number, the second being the
button functions (no buttons pressed, button1 pressed, button2 pressed and so on). When a particular page is being
displayed (a numPages element, currentPage in practice), it effectively selects a row of functions that correspond
to the page. The value returned by readAnalogButtonArray() (or some other function if something other than a
5-state voltage divider is being used to evaluate button input) determines which specific function in that row
will be executed. The first action in each row is an idle state that gets executed when no buttons are being
pressed.*/
const fptype primaryButtonFunctions [numPrimaryPages] [numPrimaryButtons] = { {&buttonVoidAction, &buttonVoidAction, &buttonVoidAction, &buttonLEDNextPage, &buttonPrimaryNextPage},
{&buttonVoidAction, &buttonResetInjCumulative, &buttonVoidAction, &buttonLEDNextPage, &buttonPrimaryNextPage},
{&buttonVoidAction, &buttonResetVssPulseCount, &buttonVoidAction, &buttonLEDNextPage, &buttonPrimaryNextPage} };
/* primaryRenderPage is an array of function pointers used to arbitrate control of the LCD display. The long variable
name is for the sake of allowing multiple displays on alternative hardware designs Each pointer in the array
corresponds to a page-print-function that can be called to print text and data to the display.*/
const fptype primaryRenderPage [numPrimaryPages] = {&mpgBasicPage162, &injectorBaseInfoPage162, &vssBaseInfoPage162};
unsigned int currentPrimaryPage = 0; //which page to actively display on the primary char display
unsigned long screenUpdatePeriod = 100; //the time (in ms) to wait between LCD updates
unsigned long lastScreenUpdate = 0; //when the screen was last updated
int displayBrightness = 512; //sets illumination brightness on displays, 0 (off) to 1023 (full bright)
const fptype LEDrenderPage [numLEDpages] = {&LEDnumBlankDisplay, &LEDnumOdoDistance, &LEDnumVehicleSpeed};
unsigned int currentLEDPage = 0;
unsigned long lastLedUpdate = 0;
unsigned long ledUpdatePeriod = 100;
const byte firstSaa1064Address = 0x70; //8-bit address with ADR tied to ground
const byte defaultSaa1064ControlByte = 0x37; //control byte: 9 mA drive current, no segment test, all digits active, multiplexed
unsigned long injOnToggleTime = 0; //variables used for injector pulsewidth and frequency measurement
unsigned long injCyclePeriod = 0;
unsigned long injLastOnToggleTime = 0;
unsigned long injOnInterval = 0;
unsigned long injCumulativeOnTime = 0;
unsigned long vssCyclePeriod = 0; //variables used for vehicle speed and distance measurement
unsigned long vssLastOnTime = 0;
unsigned long vssPulseCount = 0;
const float vssCyclePeriodToSpeedNumerator = 3600000000 / vssPulsesPerUnitDistance;
LiquidCrystal lcd(4, 5, 6, 7, 8, 12, 13);
/*===================================================================================================================*/
//The setup routine (runs at startup) and main program (loops constantly):
void setup()
{
attachInterrupt(0,singleInjectorInterrupt,CHANGE); //injector PWM measuring interrupt (pin 2)
attachInterrupt(1,basicVssInterrupt,RISING); //VSS measuring interrupt (pin 3)
pinMode(11,OUTPUT); //pin 11 is PWM backlight control for the LCD
Wire.begin(); //analog 4 and 5 are used for SDA and SCL of the I2C port
lcd.begin(16,2); //initialize the LCD as 16x2 characters
lcd.clear();
}
void loop()
{
primaryButtonState = readAnalogFourButtonArray(primaryButtonArrayAnalogPin);
if(primaryButtonState == 0) primaryButtonPressed = 0; //this section takes action once per button press
else if(primaryButtonPressed == 0)
{
primaryButtonFunctions[currentPrimaryPage][primaryButtonState]();
primaryButtonPressed = 1;
}
if(millis() >= lastScreenUpdate + screenUpdatePeriod) //this section updates the LCD screen at regular intervals
{
primaryRenderPage[currentPrimaryPage]();
analogWrite(lcdBacklightPin, (displayBrightness << 4));
lastScreenUpdate = millis();
}
if(millis() >= lastLedUpdate + ledUpdatePeriod)
{
LEDrenderPage[currentLEDPage]();
lastLedUpdate = millis();
}
}
/*===================================================================================================================*/
//The following routines are interrupts.
void singleInjectorInterrupt() //this interrupt tracks injector on time (flow rate) and cumulative on time (dispensed fuel volume)
{
if(digitalRead(injectorOnePin) == injectorOneActiveState)
{
injOnToggleTime = micros();
injCyclePeriod = injOnToggleTime - injLastOnToggleTime;
injLastOnToggleTime = injOnToggleTime;
}
else
{
injOnInterval = micros() - injOnToggleTime;
if(injOnInterval > 50000) { injOnInterval = 0; } //reject any erroneously long injector pulses
injCumulativeOnTime = injCumulativeOnTime + (injOnInterval - injectorFiringDelay);
}
}
void basicVssInterrupt() //this interrupt counts pulses (distance) and measures cycle time (speed)
{
unsigned long vssNow = micros();
vssCyclePeriod = vssNow - vssLastOnTime;
vssLastOnTime = vssNow;
vssPulseCount++;
}
/*===================================================================================================================*/
//The following are button-press functions, called to perform an action when a button is pressed.
void buttonVoidAction() { } // do nothing
void buttonPrimaryNextPage()
{
lcd.clear();
currentPrimaryPage++;
if(currentPrimaryPage >= numPrimaryPages) {currentPrimaryPage = 0;}
if(currentPrimaryPage < 0) {currentPrimaryPage = 0;}
}
void buttonLEDNextPage()
{
currentLEDPage++;
if(currentLEDPage >= numLEDpages) {currentLEDPage = 0;}
}
void buttonResetInjCumulative()
{
injCumulativeOnTime = 0;
}
void buttonResetVssPulseCount()
{
vssPulseCount = 0;
}
/*===================================================================================================================*/
/* The following are LCD page-render functions. Each function should contain the commands necessary to print a page of
information to the LCD display.*/
void injectorBaseInfoPage162() /*for 16x2 character displays. prints "### us" on the top line and "### ###" on the bottom.
cumulative microsecs, current pulse on microsecs and cycle microsecs, respectively*/
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print(injCumulativeOnTime);
lcd.setCursor(14,0);
lcd.print("us");
if(micros() < injOnToggleTime + injectorTimeOut)
{
lcd.setCursor(0,1);
lcd.print(injOnInterval);
lcd.setCursor(8,1);
lcd.print(injCyclePeriod);
}
else
{
lcd.setCursor(0,1);
lcd.print("---");
lcd.setCursor(8,1);
lcd.print("---");
}
}
void vssBaseInfoPage162() /*for 16x2 character displays. prints "### p ### Mi" on the top line and "### ### MPH" on
the bottom. pulses, miles, pulselength and speed respectively.*/
{
float vssSpeed = 0;
float distance = ( float(vssPulseCount) / float(vssPulsesPerUnitDistance) );
if(vssCyclePeriod != 0) {vssSpeed = ( vssCyclePeriodToSpeedNumerator / float(vssCyclePeriod) );}
if(micros() > vssLastOnTime + vssTimeOut) {vssSpeed = 0;}
lcd.clear();
lcd.setCursor(0,0);
lcd.print(vssPulseCount);
lcd.setCursor(6,0);
lcd.print("p");
lcd.setCursor(8,0);
lcd.print(distance);
lcd.setCursor(14,0);
lcd.print(distanceUnitsShort);
lcd.setCursor(0,1);
lcd.print(vssCyclePeriod);
lcd.setCursor(8,1);
lcd.print(vssSpeed);
lcd.setCursor(13,1);
lcd.print(speedUnitsShort);
}
void mpgBasicPage162() //for 16x2 character displays. prints "Trip: ### MPG" on the top line and "Inst: ### MPG" on the bottom
{
float tripMpg = ( (float(vssPulseCount) / vssPulsesPerUnitDistance) / (float(injCumulativeOnTime) / injectorMicrosecsPerUnitVolume) );
float instantMpg = ( (vssCyclePeriodToSpeedNumerator/float(vssCyclePeriod)) / float(3600000000 / (injectorMicrosecsPerUnitVolume)*(float(injOnInterval)/float(injCyclePeriod)))); //that's MPH/GPH fyi
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Trip: ");
lcd.print(tripMpg);
lcd.setCursor(13,0);
lcd.print(fuelEconomyUnitsShort);
lcd.setCursor(0,1);
lcd.print("Inst: ");
if(micros() < injOnToggleTime + injectorTimeOut) {lcd.print(instantMpg);}
else if(micros() > vssLastOnTime + vssTimeOut) {lcd.print("0.00");}
else{lcd.print("---");}
lcd.setCursor(13,1);
lcd.print(fuelEconomyUnitsShort);
}
/*===================================================================================================================*/
//The following are functions for and related to displaying data on LED displays (7-segment numerical, bar graph, etc)
void LEDnumBlankDisplay()
{
ledDisplayNumerical(firstSaa1064Address, 0, 0x00);
}
void LEDnumOdoDistance()
{
ledDisplayNumerical(firstSaa1064Address, (10*vssPulseCount)/vssPulsesPerUnitDistance, 0x32);
}
void LEDnumVehicleSpeed()
{
float vssSpeed = 0;
if(vssCyclePeriod != 0) {vssSpeed = ( vssCyclePeriodToSpeedNumerator / float(vssCyclePeriod) );} //make sure we aren't dividing by zero
if(micros() > vssLastOnTime + vssTimeOut) {vssSpeed = 0;} //zero out speed if the vehicle is stopped
ledDisplayNumerical(firstSaa1064Address, vssSpeed * 10, 0x32);
}
void saa1064Update(byte addy, byte ctrl, byte data0, byte data1, byte data2, byte data3) //This function controls an SAA1064 I2C LED driver
{
Wire.beginTransmission(addy >> 1); //bit-shift the 8-bit address to 7-bit for wire.h
Wire.write(0x00); //byte-address pointer
Wire.write(ctrl); //control register byte
Wire.write(data0); //first display output byte (7 segs + decimal, 8 discreet LEDs or whatever)
Wire.write(data1); //second display output byte
Wire.write(data2); //third display output byte
Wire.write(data3); //fourth display output byte
Wire.endTransmission();
}
void ledDisplayNumerical(byte addr, int value, byte forceDig_dpSelect) //This routine formats numerical variables for display on a 4-digit 7-segment display
{
byte ctrlByte = (defaultSaa1064ControlByte & 0x0F) | ((displayBrightness >> 3) & 0x70);
byte digitOne = findSegs(value % 10); //break value up into its component numerical digits and convert them to 7-segment font codes
byte digitTwo = findSegs((value % 100) / 10);
byte digitThree = findSegs((value % 1000) / 100);
byte digitFour = findSegs((value % 10000) / 1000);
if((value < 1000) && ((forceDig_dpSelect & 0x80) == 0)) {digitFour = 0x00;} //the upper four bits of forceDig_dpSelect disable leading zero blanking digit by digit
if((value < 100) && ((forceDig_dpSelect & 0x40) == 0)) {digitThree = 0x00;}
if((value < 10) && ((forceDig_dpSelect & 0x20) == 0)) {digitTwo = 0x00;}
if((value < 1) && ((forceDig_dpSelect & 0x10) == 0)) {digitOne = 0x00;}
digitOne = digitOne | ((forceDig_dpSelect << 7) & 0x80); //the lower four bits of forceDig_dpSelect activate the decimal points
digitTwo = digitTwo | ((forceDig_dpSelect << 6) & 0x80);
digitThree = digitThree | ((forceDig_dpSelect << 5) & 0x80);
digitFour = digitFour | ((forceDig_dpSelect << 4) & 0x80);
//send digits serially to display chip. digit order is a matter of hardware wiring. play around with it if yours isn't coming out right
saa1064Update(addr, ctrlByte, digitFour, digitTwo, digitThree, digitOne);
}
byte findSegs(int alpha) //this routine converts a single digit decimal into a byte to drive a 7-segment display
{
switch(alpha)
{
case 0:
return 0x3F; //These byte values are designed for a SAA1064 wired with P1-8 connected to a, b, c, d, e, f, g, dp of the first
//digit (or pair of digits if multiplexing), respectively. P9-16 are similarly connected to the second digit
case 1:
return 0x06;
case 2:
return 0x5B;
case 3:
return 0x4F;
case 4:
return 0x66;
case 5:
return 0x6D;
case 6:
return 0x7D;
case 7:
return 0x07;
case 8:
return 0x7F;
case 9:
return 0x6F;
default:
return 0x00;
}
}
/*===================================================================================================================*/
//The following are miscellaneous functions.
int readAnalogFourButtonArray(int analogPin) //this function converts the value from the button voltage divider to a useful value
{
int analogIn = analogRead(analogPin);
/* the following values are selected to work with a voltage divider consisting of four buttons lined up in parallel. one contact
of each button is connected to VCC (5V+). the remaining contact goes to a resistor. the first button has a 15k-ohm resistor,
the second has a 4.7k-ohm resistor, the third has a 1.5K-ohm resistor, and the fourth has a 0-ohm resistor (a plain wire). the
remaining lead of the resistors (and jumper) are connected together and tied to an analog pin of the user's choice. to complete
the voltage divider, a 4.7k-ohm resistor is connected between the resistors-analog pin connection and ground. this circuit
presents 0V when no buttons are being pressed, about 1.25V when button one is pressed, 2.5V when button two is pressed, 3.75V
for button three and 5V for button 4. the higher numbered button dominates if more than one button is pressed at the same time. */
if(analogIn > 120)
{
if(analogIn > 380)
{
if(analogIn > 640)
{
if(analogIn > 900)
{
return 4;
}
return 3;
}
return 2;
}
return 1;
}
return 0;
}