Marco Zonca
Published © GPL3+

Car HUD - Windscreen Display for Speed+Compass+Alt.

Drive without distracting your sight, take care of speed limits taking under control your car speed on windscreen! (2nd version available)

IntermediateFull instructions provided12,590
Car HUD - Windscreen Display for Speed+Compass+Alt.

Things used in this project

Story

Read more

Schematics

Fritzing picture

Fritzing schematic diagram

3D Plastic container picture

PCB #1 overview

PCB #2 overview

PCB #1 (bottom side)

PCB #1 (top side)

PCB #2 (bottom side)

PCB #2 (top side)

PCB #1 (components face)

PCB #2 (components face)

Sketch version 2 (read instructions first)

Sketch version 2.2 (read instructions first)

Code

Car-HUD Arduino sketch

Arduino
/*
  This sketch acts as car windscreen HUD (head up display), by Marco Zonca, 10/2020
  Arduino Nano as CPU, GPS BT-220 nmea every 1 sec, 3 x buttons, 3 x seven segment displays common cathode,
  3 x 14511 BCD latch decoder, MPU EEPROM memory (1 byte) and many resistors;

  WARNING:
  =======
  Before updating software let disconnect RX pin on Arduino (TX from GPS) by the way of the JUMPER
*/

#include <Wire.h>
#include <EEPROM.h>

String inputString = "";
String nm_time = "00:00:00";
String nm_validity = "V";
String nm_latitude = "dd°mm.mmmm'N";
String nm_longitude = "ddd°mm.mmmm'E";
String nm_knots = "0.0kn";
float nmf_knots = 0.0;
float nmf_kmh = 0.0;
int nmi_kmh = 0;
String nm_truecourse = "360";
float nmf_truecourse = 360;
String nm_date = "dd/mm/yyyy";
int nmi_truecourse = 0;
byte kCent = 0;
byte kDeci = 0;
byte kUnit = 0;
byte tCent = 0;
byte tDeci = 0;
byte tUnit = 0;
byte brightness = 120;
byte latch_off = HIGH;
byte latch_on = LOW;
int n=0;
unsigned long lastmemcheck = 0;
unsigned long memcheck = 60000;  // check to save "brightness" value in EEPROM every 60 seconds

bool stringComplete = false;
bool isKMH=true;
bool ret = false;

const int disp001 = 2;  // units display latch
const int disp010 = 8;  // tens display latch
const int disp100 = 12;  // undreds display latch

const int disp001dim = 9;  // units display dimmer/off pin
const int disp010dim = 10;  // tens display dimmer/off pin
const int disp100dim = 11;  // undreds display dimmer/off pin

const int button_kt = 14;  // kmh/truecourse button
const int button_more = 15;  // brightness + button
const int button_less = 16;  // brightness - button

const int degreesLED = 3;  // degrees LED

const int bit_3 = 7;  // bit 3
const int bit_2 = 6;  // bit 2
const int bit_1 = 5;  // bit 1
const int bit_0 = 4;  // bit 0

const int dly = 10;  // delay latch m/sec
const byte off = 0;  // the same as brightness=0
const int addr = 0; // EEPROM address for brightness value

const byte numbers[10] [4] = {{0,0,0,0},{1,0,0,0},{0,1,0,0},{1,1,0,0},{0,0,1,0},
                              {1,0,1,0},{0,1,1,0},{1,1,1,0},{0,0,0,1},{1,0,0,1}};  // bits 0,1,2,3

void setup() {
  Serial.begin(9600);
  Wire.begin();
  inputString.reserve(200);
  brightness = EEPROM.read(addr);
  if (brightness > 250 || brightness < 10) { brightness=120; }  // avoid crazy values from 1st EEPROM read
  pinMode(disp001, OUTPUT);
  pinMode(disp010, OUTPUT);
  pinMode(disp100, OUTPUT);
  pinMode(degreesLED, OUTPUT);
  pinMode(button_kt, INPUT_PULLUP);
  pinMode(button_less, INPUT_PULLUP);
  pinMode(button_more, INPUT_PULLUP);
  pinMode(bit_3, OUTPUT);
  pinMode(bit_2, OUTPUT);
  pinMode(bit_1, OUTPUT);
  pinMode(bit_0, OUTPUT);  
  
  analogWrite(disp001dim, off);  // off and zero displays
  analogWrite(disp010dim, off);
  analogWrite(disp100dim, off);
  analogWrite(degreesLED, off);
  
  setBusNr(0);
  digitalWrite(disp001, latch_on);
  digitalWrite(disp010, latch_on);
  digitalWrite(disp100, latch_on);
  delay(dly);
  digitalWrite(disp001, latch_off);
  digitalWrite(disp010, latch_off);
  digitalWrite(disp100, latch_off);

  analogWrite(disp001dim, brightness);  // on display
  analogWrite(disp010dim, brightness);
  analogWrite(disp100dim, brightness);
}  // setup()

void loop() {
  // GPS NMEA ------------------
  if (stringComplete == true) {  // received nmea sentence by serial port RX
    ret = nmeaExtractData();
    inputString = "";
    stringComplete = false;
    if (ret == true) {
      kCent=nmi_kmh/100;
      n=nmi_kmh-(kCent*100);
      kDeci=n/10;
      n=nmi_kmh-(kCent*100)-(kDeci*10);
      kUnit=n;
      tCent=nmi_truecourse/100;
      n=nmi_truecourse-(tCent*100);
      tDeci=n/10;
      n=nmi_truecourse-(tCent*100)-(tDeci*10);
      tUnit=n;
      display();
    }
  }
  if (millis() > (lastmemcheck+memcheck)) {  // put in memory brightness value (if modified)
    EEPROM.update(addr,brightness);
    lastmemcheck=millis();
  }
  checkButtons();
}

void display() {
  if (isKMH == true) {  // speed in km/h (isKMH=true)
    analogWrite(degreesLED, off);
    setBusNr(kUnit);
    digitalWrite(disp001, latch_on);
    delay(dly);
    digitalWrite(disp001, latch_off);
    if (kDeci > 0 || kCent > 0) {  // set tens off if tens=0 (and also undreds=0)
      setBusNr(kDeci);
      digitalWrite(disp010, latch_on);
      delay(dly);
      digitalWrite(disp010, latch_off);
      analogWrite(disp010dim, brightness);
    } else {
      analogWrite(disp010dim, off);
    }
    if (kCent > 0) {  // set undreds off if=0
      setBusNr(kCent);
      digitalWrite(disp100, latch_on);
      delay(dly);
      digitalWrite(disp100, latch_off);
      analogWrite(disp100dim, brightness);
    } else {
      analogWrite(disp100dim, off);
    }
  } else {  // true bearing in degrees (isKMH=false) 
    analogWrite(degreesLED, brightness);
    setBusNr(tUnit);
    digitalWrite(disp001, latch_on);
    delay(dly);
    digitalWrite(disp001, latch_off);
    if (tDeci > 0 || tCent > 0) {  // set tens off if tens=0 (and also undreds=0)
      setBusNr(tDeci);
      digitalWrite(disp010, latch_on);
      delay(dly);
      digitalWrite(disp010, latch_off);
      analogWrite(disp010dim, brightness);
    } else {
      analogWrite(disp010dim, off);
    }
    if (tCent > 0) {  // set undreds off if=0
      setBusNr(tCent);
      digitalWrite(disp100, latch_on);
      delay(dly);
      digitalWrite(disp100, latch_off);
      analogWrite(disp100dim, brightness);
    } else { 
      analogWrite(disp100dim, off);
    }
  }
}  // display()

void checkButtons(){
  if (digitalRead(button_kt) == LOW) {
    if (isKMH == true) {
      isKMH=false;
    } else {
      isKMH=true;
    }
    delay(250);
  }
  if (digitalRead(button_more) == LOW) {
    if (brightness <= 240) {
      brightness=brightness+10;
    }
    analogWrite(disp001dim, brightness);
    analogWrite(disp010dim, brightness);
    analogWrite(disp100dim, brightness);
    delay(100);
  }
  if (digitalRead(button_less) == LOW) {
    if (brightness >= 20) {
      brightness=brightness-10;
    }
    analogWrite(disp001dim, brightness);
    analogWrite(disp010dim, brightness);
    analogWrite(disp100dim, brightness);
    delay(100);
  }
}  // checkButtons()

void setBusNr(int number) {  // sets 4 bits bus
  for (byte b=0; b<=3; b++) {
    if (numbers[number][b]==0) {
      if (b==0) {digitalWrite(bit_0, LOW);}
      if (b==1) {digitalWrite(bit_1, LOW);}
      if (b==2) {digitalWrite(bit_2, LOW);}
      if (b==3) {digitalWrite(bit_3, LOW);}
    } else {
      if (b==0) {digitalWrite(bit_0, HIGH);}
      if (b==1) {digitalWrite(bit_1, HIGH);}
      if (b==2) {digitalWrite(bit_2, HIGH);}
      if (b==3) {digitalWrite(bit_3, HIGH);}
    }
  }
}  // setBusNr()

// extract data from nmea inputString
bool nmeaExtractData() {
  int d=0;
  int s=0;
  int y=0;
  int z=0;
  float t=0;
  bool ret = false;  //true if nmea sentence = $GNRMC and valid CHKSUM
  if ((inputString.substring(0,6) == "$GNRMC") && (inputString.substring(inputString.length()-4,inputString.length()-2) == nmea0183_checksum(inputString))) {
    y=0;
    for (s = 1; s < 11; s ++) { 
      y=inputString.indexOf(",",y);
      switch (s) {
      case 1: //-----------------------time
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_time=inputString.substring(y+1,y+2+1)+":"+inputString.substring(y+1+2,y+4+1)+":"+inputString.substring(y+1+4,y+6+1);
        }
        y=z;
        break;
      case 2: //-----------------------validity
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_validity=inputString.substring(y+1,y+1+1);
        }
        y=z;
        break;
      case 3: //-----------------------latitude
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_latitude=inputString.substring(y+1,y+2+1)+"°"+inputString.substring(y+1+2,y+10+1)+"'";
        }
        y=z;
        break;
      case 4: //-----------------------north/south
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_latitude=nm_latitude + inputString.substring(y+1,y+1+1);
        }
        y=z;
        break;
      case 5: //-----------------------longitude
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_longitude=inputString.substring(y+1,y+3+1)+"°"+inputString.substring(y+1+3,y+11+1)+"'";
        }
        y=z;
        break;
      case 6: //-----------------------east/west
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_longitude=nm_longitude + inputString.substring(y+1,y+1+1);
        }
        y=z;
        break;
      case 7:  //-----------------------speed knots
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nmf_knots=inputString.substring(y+1,z).toFloat();
          t=roundOneDec(nmf_knots);
          nm_knots=String(t,1)+"kn";
          nmf_kmh=roundTwoDec(nmf_knots * 1.852);
          nmi_kmh=roundZeroDec(nmf_knots * 1.852);
        }
        y=z;
        break;
      case 8: //-----------------------true course
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nmf_truecourse=inputString.substring(y+1,z).toFloat();
          t=roundZeroDec(nmf_truecourse);
          nmi_truecourse=t;
          d=t;
          nm_truecourse=d;
        }
        y=z;
        break;
      case 9: //-----------------------date
        z=inputString.indexOf(",",y+1);
        if (z>(y+1)) {
          nm_date=inputString.substring(y+1,y+2+1)+"/"+inputString.substring(y+1+2,y+4+1)+"/20"+inputString.substring(y+1+4,y+6+1);
        }
        y=z;
        break;
      case 10:
        // statements n.u.
        break;
      default:
        // statements n.u.
        break;
      }
    }
    ret=true;
  }
  return ret;
}  // nmeaExtractData()

/*
  SerialEvent occurs whenever a new data comes in the hardware serial RX. This
  routine is run between each time loop() runs, so using delay inside loop can
  delay response. Multiple bytes of data may be available.
*/
void serialEvent() {
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}  // serialEvent()

//calculate checksum of nmea sentence
String nmea0183_checksum(String nmea_data) {
    int crc = 0;
    String chSumString = "";
    int i;
    // ignore the first $ sign, checksum in sentence
    for (i = 1; i < (nmea_data.length()-5); i ++) { // remove the - 5 if no "*" + cksum + cr + lf are present
        crc ^= nmea_data[i];
    }
    chSumString = String(crc,HEX);
    if (chSumString.length()==1) {
      chSumString="0"+chSumString.substring(0,1);
    }
    chSumString.toUpperCase();
    return chSumString;
}  // nmea0183_checksum(String nmea_data)

// round zero decimals
float roundZeroDec(float f) {
  float y, d;
  y = f*1;
  d = y - (int)y;
  y = (float)(int)(f*1)/1;
  if (d >= 0.5) {
    y += 1;
   } else {
    if (d < -0.5) {
    y -= 1;
  }
  }
  return y;
}

// round one decimal
float roundOneDec(float f) {
  float y, d;
  y = f*10;
  d = y - (int)y;
  y = (float)(int)(f*10)/10;
  if (d >= 0.5) {
    y += 0.1;
   } else {
    if (d < -0.5) {
    y -= 0.1;
  }
  }
  return y;
}

// round two decimals
float roundTwoDec(float f) {
  float y, d;
  y = f*100;
  d = y - (int)y;
  y = (float)(int)(f*100)/100;
  if (d >= 0.5) {
    y += 0.01;
   } else {
    if (d < -0.5) {
    y -= 0.01;
  }
  }
  return y;
}

Credits

Marco Zonca

Marco Zonca

12 projects • 43 followers
"From an early age I learned to not use pointers"

Comments