Project tutorial
Car HUD - Windscreen Display for Speed & Compass

Car HUD - Windscreen Display for Speed & Compass © GPL3+

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

  • 9,111 views
  • 11 comments
  • 18 respects

Components and supplies

About this project

Description:

I wanted it! I have been invidious of my friend Taggi's big car showing on windscreen the speed. I had to have it, I had to make it by myself of course!

Software point of view:

Projecting the circuit the most difficult and confusing part have been the connection of the 7 segments displays to the BCD decoders because numbers are "inverted" (mirrored).

There are 3 buttons: + and - for increasing/decreasing brightness and S/H to switch between Speed in km/h and Heading in degrees; in case of Heading also the red LED is on, it means "degrees" of compass (1-360°) when moving. It isn't an electronic compass, it is a GPS and you need to move to have the right information of the direction you are moving to. The brightness is saved in EEPROM memory after a minute. Brightness is changed both on displays and on LED by the way of PWM pins.

An important part of the code is for collecting datas from GPS, mainly speed and direction, taking them out from every NMEA sentence. Even using String class, mostly for Serial NMEA sentences manipulation, the whole elaboration flow is stable and solid; it uses "serialEvent()" to receive data from GPS one time per second, than calls "nmeaExtractData()" and finally it checks data packet with "nmea0183_checksum() to be sure of data integrity. If you use another make and model GPS be sure the sentences have the same structure or you have to make some changes here. For example EM406A uses "$GPRMC" packet id, BT220 uses "$GNRMC" instead... just a small name change... A useful link can help you with checksum test: https://nmeachecksum.eqth.net - Here an example of a complete NMEA sentence, it contains: id, time, validity, latitude, longitude, speed, true course, date, variation and checksum.

$GNRMC, 095836.000, A, 4551.9676, N, 01328.7118, E, 2.09, 341.84, 280519,, *08

The sketch provides to enable the latch for every single display BCD decoder one at a time, set the code number on the binary 4 bits bus, disable the latch, and so on when value changes. Left side not significant zeros are blanked (not displayed).

Before loading a new sketch to the MCU remember to take out the Jumper: it is connected to Rx pin of Arduino and during loading it is for sure in conflict with GPS Tx. After software loading put again in place the Jumper to restore normal functionality.

Components list:

  • 1 x MCU Arduino Nano
  • 3 x 5161as seven segments displays, common cathode, red
  • 1 x Beitian BN-220 serial TTL GPS (1 Hz GNRMC sentences)
  • 1 x Jumper
  • 3 x buttons (normally open) + 3 x caps
  • 22 x 1/4W 220 ohm resistors
  • 1 x 3mm LED, red
  • 2 x 100n capacitors
  • 3 x 14511 BCD decoders + latch
  • 1 x USB "B" female
  • 1 x strip line 2x male pins (for Jumper)
  • 1 x strip line 4x male pins to bend to 90° (for GPS)
  • 1 x pigtail cable connecting GPS to 4 pins onboard
  • 22 x strip line male pins to join two PCB in a stack
  • 1 x piece of double sided adhesive to stick GPS on PCB
  • 6 x turned strip line 5x female pins (for displays)
  • 50 x copper 0.6mm rivets
  • 8 x M3 screws
  • 4 x M3 female towers 20mm high
  • 1 x plastic box + cover (look at my 3D files ready for printing, below)

PCB (printed circuit boards):

I used two double faced PCBs for that reason around 50 pass through rivets or pins are used to solve routes for whole circuits. There are also 5 alignment holes to make first. I designed on every PCB these 5 alignment spots. In download section you have all PCB files, components and solder faces, mirrored, for downloading and printing by the way of a laser printer on "yellow" or "blue" sheets; this time I used the blue ones but also the yellow ones are good and the price is lower. I have to say the blue sheets are better... When printing remember to disable toner saving settings, use instead 1200 dpi resolution to have deep real black result. The toner transfer process from magic sheets to PCB is made by the use of an hot iron... Around in the Net there are some tutorials showing how to produce a good PCB but remember these important points: clean perfectly and slightly brush the copper with a kitchen ruvid sponge, 5 minutes of ironing, water thermal shock, two faces alignment by the way of 5 holes (I used 5 pins on a large white LED surface to see holes), protect the other face during corrosion process. Printing also components face makes the project "professional" :-)

NOTES: the pictures are taken before the very last version, it means a few details can be different: i.e. the strange green wire around the PCBs in the final version disappears, or the Jumper has moved from PCB #1 to PCB #2. You should setup the GPS at 9600 bps speed, 1Hz GNRMC NMEA sentence only; this can be made using its own setup software. Finally, before solder the USB female connector, stick a small piece of insulating tape under it to avoid unwanted contact with a copper line, on the component side, just below it.

Giving power for the first time you will discover the numbers are "unreadable", because it is made to look at them reflected on the windscreen (mirror). Install it in the car in the front of driver's seat, find a comfortable place and fix it. I suggest to connect the USB power cable after the ignition position so it will switch on / off when engine on / off. That's all ladies and gentlemen!

News and Improvements:

  • 25.03.2021: to complete the project I prepared two 3D .STL models at this link: https://grabcad.com/library/car-hud-1 where you may download files regarding a shaped box and his cover panel for this project to print by the way of a 3D printer.
  • 20.07.2021: it is available a V2 (version two) of the software that together with two wires and one resistor you will have altitude information on the display as thousand of meters (i.e. 0.89 = 890m), just press another time the S/H button! Instructions: 1) setup GPS to output also $GNGGA nmea sentence by the way of its own software; 2) cut actual connection between d5 Arduino pwm pin and the rest of the circuit, connect it to central display decimal dot pin with a 220ohm resistor; 3) connect d13 Arduino digital pin to the circuit where d5 was connected; look at few pictures here below; 4) install V2 sketch on your Arduino Nano.
  • 01.11.2021: version V2.2 is ready for you. What new? About altitude (already present with V2) the hud shows full value up to 999m, starting from 1000m it shows values in thousand, i.e. 1.24 (1240m) or 2.02 (2020m). I added a LDR photoresistor GL5539, a 10kOhm resistor and 2 wires. Look at the pictures below for V2.2 mods and install V2.2 sketch on your Arduino. The LDR sensor will modify brightness automatically (3 levels) depending of the light level (day, cloudy or night). Buttons (+) and (-) are still working to manually modify brightness, they have priority; to return back to automatic just press together (+) and (-) buttons for a while. The internal EEPROM to store brightness level is not used anymore. Let do V2 mods first!

When driving, just drive. Drive safe!

Code

Car-HUD Arduino sketchArduino
/*
  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;
}

Schematics

Fritzing picture
Fritzing car hud svycvayl6f
Fritzing schematic diagram
car-hud_m5RwPQqpxH.fzz
3D Plastic container picture
Car hud 3d yymnibvhm1
PCB #1 overview
Carhud 1 overview odiozndkmd
PCB #2 overview
Carhud 2 overview 6td1y3xjke
PCB #1 (bottom side)
Pcb car hud bottom layer copper 1 bphz1yonsk
PCB #1 (top side)
Pcb car hud top layer copper 1 r9kscjyyjz
PCB #2 (bottom side)
Pcb car hud bottom layer copper 2 fo51exagqn
PCB #2 (top side)
Pcb car hud top layer copper 2 0jnohihkiv
PCB #1 (components face)
Pcb car hud serigrafia 1 pvmmc7zqdd
PCB #2 (components face)
Pcb car hud serigrafia 2 5kndj4ri9f
Sketch version 2 (read instructions first)
ard-carhud-v2_3N5756haNI.ino
Sketch version 2.2 (read instructions first)
ard-carhud-v2-2_XhXHFJu0T8.ino

Comments

Similar projects you might like

Voice Controlled Car

Project tutorial by Yug_Ajmera

  • 58,805 views
  • 13 comments
  • 39 respects

Biometric Car Entry - True Keyless Car

Project showcase by Rajeev Velikkal

  • 18,676 views
  • 13 comments
  • 56 respects

Gear Indicator Project

Project tutorial by JorisH

  • 9,869 views
  • 3 comments
  • 13 respects

RC Car (Arduino-Based 3D Resin Printed) RC_Car_RP

Project tutorial by devinnamaky

  • 6,429 views
  • 0 comments
  • 20 respects

Arduino Powered PLC Display (Cheapest Display for PLC)

Project tutorial by Ali Kaan Ünal

  • 2,204 views
  • 1 comment
  • 4 respects

Race Car Dashboard (Online Connectivity)

Project showcase by Team Jatayu

  • 46,856 views
  • 69 comments
  • 66 respects
Add projectSign up / Login