John Bradnam
Published © GPL3+

Car Windscreen HUD

A heads-up display for your car's windscreen showing your speed and/or heading.

IntermediateFull instructions provided8 hours1,320
Car Windscreen HUD

Things used in this project

Hardware components

Microchip ATtiny1614 microprocessor
14 pin SOIC package
×1
Maxim Integrated MAX7219 IC
24pin SOIC wide body
×1
4-Digit 7 segment Common Cathode Display 0.8in
I could only buy these in groups of 5. On eBay search for "5pcs 0.8" 4 digit segment led displays 7 seg with clock CC/CA type W/B/Y/G/R"
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
13mm shaft with button tops
×3
LM1117-5V Regulator
SOT223 variant
×1
Capacitor 47 µF
Capacitor 47 µF
47uF 16V 3528 Tantalum Capacitor
×1
Capacitor 100 nF
Capacitor 100 nF
0805 SMD Ceramic
×1
Resistor 10k ohm
Resistor 10k ohm
All 0805 1% 1 x 330 OHM, 1 x 4K7, 2 x 10K, 2 x 22K
×6
3 mm LED: Red
3 mm LED: Red
×1
DC Power Connector, Socket
DC Power Connector, Socket
Do a search on eBay for "DC Power Jack Socket Female Panel Mount Connector 3.5 mm x 1.35mm"
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

UPDI programmer
This is my home made UPDI programmer. See https://www.hackster.io/john-bradnam/create-your-own-updi-programmer-1e55f1
3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL files for 3D printing

0.2mm layer height, no supports

Schematics

Eagle Files

Schematic and PCB in Eagle format

Schematic

PCB

Code

HUD_V1.ino

C/C++
/**
 * Car Windscreen HUD
 * Original code: Marco Zonca, 10/2020
 * Updated code: John Bradnam, 12/2020
 *  - Changed code to run on ATtiny1614 processor
 *  - Modified electronics to use a MAX7219 display controller
 *  - Removed unused code
 * 
 * ATTiny1614 Pins mapped to Ardunio Pins
 *
 *             +--------+
 *         VCC + 1   14 + GND
 * (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
 *       1 PA5 + 3   12 + PA2 9  (MISO)
 * (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
 *       3 PA7 + 5   10 + PA0 11 (UPDI)
 * (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 * (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
 *             +--------+
 *             
 *   BOARD: ATtiny1614/1604/814/804/414/404/214/204
 *   Chip: ATtiny1614
 *   Clock Speed: 20MHz
 *   Support SerialEvent: Yes
 *   Programmer: jtag2updi (megaTinyCore)
*/

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

#define MAX7219_DATA 8 //(PA1)
#define MAX7219_CLK 10 //(PA3)
#define MAX7219_LOAD 3 //(PA7)
#define SWITCHES 0 //(PA4)
#define DEGREES_LED 2 //(PA6)

//LED order 2,3,1,0
#define DIGITS 3
int8_t digits[] = {2, 3, 1};

#define EEPROM_BRIGHTNESS 0
#define MEMCHECK_TIME 60000

String inputString = "";
int nmi_kmh = 0;
int nmi_truecourse = 0;
int8_t brightness = 4;
unsigned long memcheckTimeout = 0; // check to save "brightness" value in EEPROM every 60 seconds

bool stringComplete = false;
bool isKMH=true;

LedControl lc=LedControl(MAX7219_DATA,MAX7219_CLK,MAX7219_LOAD,1);


//--------------------------------------------------------------------------
// Hardware setup

void setup() 
{
  pinMode(SWITCHES,INPUT);
  pinMode(DEGREES_LED,OUTPUT);
  digitalWrite(DEGREES_LED, (isKMH) ? LOW : HIGH);

  //Set up RX pin to receive from GPS
  Serial.begin(9600);
  Wire.begin();
  inputString.reserve(200);

  //Read brightness from EEPROM
  brightness = EEPROM.read(EEPROM_BRIGHTNESS);
  memcheckTimeout = millis() + MEMCHECK_TIME;
  
  //The MAX72XX is in power-saving mode on startup,  we have to do a wakeup call
  lc.shutdown(0,false);
  lc.setIntensity(0,brightness);
  lc.clearDisplay(0);
  
}

//--------------------------------------------------------------------------
// Main program loop

void loop() 
{ 
  // GPS NMEA ------------------
  if (stringComplete == true) 
  {
    // received nmea sentence by serial port RX
    if (nmeaExtractData())
    {
      writeNumber((isKMH) ? nmi_kmh : nmi_truecourse);
    }
    inputString = "";
    stringComplete = false;
  }
  if (millis() > memcheckTimeout) 
  {  
    // put in memory brightness value (if modified)
    EEPROM.update(EEPROM_BRIGHTNESS, brightness);
    memcheckTimeout = millis() + MEMCHECK_TIME;
  }
  checkButtons();
}

//--------------------------------------------------------------------------
//Tests if buttons have been pressed
// - Buttons are on an analog pin. analogRead values for the buttons are:
//   HS = 781, - = 564, + = 0

void checkButtons()
{
  
  int v = analogRead(SWITCHES);
  if (v < 1000)
  {
    //debounce switch
    delay(10);
    if (v == analogRead(SWITCHES))
    {
      if (v > 650)
      {
        //H/S button pressed
        isKMH = !isKMH;
        digitalWrite(DEGREES_LED, (isKMH) ? LOW : HIGH);

        //Wait until switch is released
        while (v < 1000)
        {
          delay(100);
          v = analogRead(SWITCHES);
        }
      }
      else if (v < 100)
      {
        // + button is pressed
        if (brightness < 15)
        {
          brightness++;
          lc.setIntensity(0,brightness);
        }
        delay(100);
      }
      else
      {
        // - button is pressed
        if (brightness > 0)
        {
          brightness--;
          lc.setIntensity(0,brightness);
        }
        delay(100);
      }
    }
  }
}

//--------------------------------------------------------------------------
// Writes a 3-digit number to the display
//   num - number to display

void writeNumber(int num)
{
  num = min(num, 999);
  for (int i = 0; i < DIGITS; i++)
  {
    if (num > 0 || i == 0)
    {
      lc.setDigit(0, digits[i], num % 10, false);
    }
    else
    {
      lc.setChar(0, digits[i], ' ', false);
    }
    num = num / 10;
  }
}

//--------------------------------------------------------------------------
// extract data from nmea inputString
bool nmeaExtractData() 
{
  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))) 
  {
    //Get speed over ground
    float nmf_knots = getSubString(inputString, ',', 7).toFloat();
    nmi_kmh = int(nmf_knots * 1.852 + 0.5);

    //Get course over ground
    float nmf_truecourse = getSubString(inputString, ',', 8).toFloat();
    nmi_truecourse = int(nmf_truecourse + 0.5);
    
    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.
//  Make sure that "Support SerialEvent:" is set to "Yes" on the Tools menu in the IDE

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;
    }
  }
}

//--------------------------------------------------------------------------
//calculate checksum of nmea sentence
// nmea_data - String just received
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;
}


//--------------------------------------------------------------------------
//Returns nth substring in a given string for a given separator character
// data - Given String
// separator = character used as substring delimiter
// index - a zero based index for the sub-string to return

String getSubString(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length() - 1;
  
  for(int i = 0; i <= maxIndex && found <= index; i++)
  {
    if (data.charAt(i) == separator || i == maxIndex)
    {
        found++;
        strIndex[0] = strIndex[1] + 1;
        strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }

  return (found > index) ? data.substring(strIndex[0], strIndex[1]) : "";
}

Credits

John Bradnam

John Bradnam

145 projects • 179 followers
Thanks to marcozonca.

Comments