GPS digital clock

As daylight savings ends here (in Australia) this Sunday, I thought it would be a good time to make up a GPS clock I could take around with me as I reset the 1000 or so clocks we have here. (Just joking, but there are a lot, once you allow for clocks in ovens, microwaves, VCRs, etc.).

I had a EM-406a GPS module lying around, purchased for a day like this. Here are its pinouts:

A lot of time was wasted trying to find suitable level-converters for it, since it outputs at 2.85V (at 4800 baud) until I realized that 2.85V would count as HIGH on a serial port, without needing a level converter. And then if the processor was run at around 3.3V then anything over around 1.65V would be HIGH, which was even better. So, no level converter. :slight_smile:

Then I grabbed the MAX7219 8-digit LED module that recently arrived from eBay for $10.

Connected together and assembled into a lunch box it looks like this:

The photo doesn’t do the digits justice, they look higher contrast in a normal room:

The LED module is stuck to the LID:

The overall effect:

Inside is a Real Bare Bones Board (from Modern Device):

And the GPS and battery box:

Connections to the RBBB:

The Rx line on the GPS is supposed to be held high, so I used a voltage divider (two resistors) to get around 2.8V from the Vcc line on the RBBB. (1.5K and 10K and divides 3.2V into 2.8V at the join of the resistors, with 10K being the one going to Gnd).

The whole thing is powered by 2 x AA alkaline batteries. They put out around 3.2V, which is enough for the processor (which is running at 8 MHz on the internal oscillator) and apparently sufficient for the GPS.

The code is below, if you want to reproduce it.

A fair bit of code is working out whether it is daylight savings time or not. You may need to adjust the calculations depending on the local rules. For us right now, from the first Sunday in April to the first Sunday in October, it is not daylight savings time.

The main loop is simply the standard “get data from serial and buffer it” stuff.

I use a regular expression to parse the GPS data because I was too lazy to work out a more sophisticated way.

Then the time is adjusted by the time-zone, and then daylight saving based on the date from the GPS.

The time is shown as a 12-hour clock with the final digit being “P” for PM.

The decimal point in the “blank” digit (next to the P) is on if we have a GPS fix, otherwise off.

// GPS clock with digital read-out
// Author: Nick Gammon
// Date:   4 April 2013

#include <SoftwareSerial.h>
#include <Regexp.h>
#include <SPI.h>

// Adjust for your time zone. Hours + or - from UTC.
const int timeOffset = 10;

// MAX7219 constants

const byte MAX7219_REG_NOOP        = 0x0;
// codes 1 to 8 are digit positions 1 to 8
const byte MAX7219_REG_DECODEMODE  = 0x9;
const byte MAX7219_REG_INTENSITY   = 0xA;
const byte MAX7219_REG_SCANLIMIT   = 0xB;
const byte MAX7219_REG_SHUTDOWN    = 0xC;
const byte MAX7219_REG_DISPLAYTEST = 0xF;

// For GPS input
SoftwareSerial GPSserial(2, 3); // RX, TX

// how much serial data we expect before a newline
const unsigned int MAX_INPUT = 100;

// send a digit or other data to the MAX7219
void sendByte (const byte reg, const byte data)
  {    
  digitalWrite (SS, LOW);
  SPI.transfer (reg);
  SPI.transfer (data);
  digitalWrite (SS, HIGH); 
  }  // end of sendByte


void setup()  
  {
  // Open serial communications and wait for port to open:
  Serial.begin(115200);

  // set the data rate for the SoftwareSerial port
  GPSserial.begin(4800);
  
  SPI.begin ();
  sendByte (MAX7219_REG_SCANLIMIT, 7);      // show 6 digits
  sendByte (MAX7219_REG_DECODEMODE, 0xFF);  // use digits (not bit patterns)
  sendByte (MAX7219_REG_DISPLAYTEST, 0);    // no display test
  sendByte (MAX7219_REG_INTENSITY, 10);      // character intensity: range: 0 to 15
  sendByte (MAX7219_REG_SHUTDOWN, 1);       // not in shutdown mode (ie. start it up)
  
  // send hyphens during start-up
  for (int digit = 0; digit < 8; digit++)
    sendByte (digit + 1, 0x0A);
  }  // end of setup

// http://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
// Devised by Tomohiko Sakamoto in 1993, it is accurate for any Gregorian date.
// Returns 0 = Sunday, 1 = Monday, etc.

int dayOfWeek (int d, int m, int y)
   {
   static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
   y -= m < 3;
   return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
   }  // end of dayOfWeek
   
   
// DST = Daylight Savings Time
boolean isDaylightTime (int day, int month, int year, int hour) 
  { 
  int firstSundayInApril;
  int firstSundayInOctober;

  for (firstSundayInApril = 1; firstSundayInApril <= 31; firstSundayInApril++)
    if (dayOfWeek (firstSundayInApril, 4, year) == 0)
      break;

  for (firstSundayInOctober = 1; firstSundayInOctober <= 31; firstSundayInOctober++)
    if (dayOfWeek (firstSundayInOctober, 10, year) == 0)
      break;

  // May to September: not DST
  if (month >= 5 && month <= 9) 
    return false;
  
  // January to March, and November to December: is DST
  if (month <= 3 || month >= 11)
    return true;  
   
  // In April, if not yet first Sunday, still DST
  if (month == 4 && day < firstSundayInApril)
    return true;

  // In April, on first Sunday, still DST before 2 am
  if (month == 4 && day == firstSundayInApril && hour < 2)
    return true;
    
  // In October, if after first Sunday, is DST
  if (month == 10 && day > firstSundayInOctober)
    return true;

  // In October, on first Sunday, is DST after 2 am
  if (month == 10 && day == firstSundayInOctober && hour >= 2)
    return true;
    
  // some date in April or October that did not pass the above tests
  return false; 
  } // end of isDaylightTime  

// here to process incoming serial data after a terminator received
void process_data (char * data)
  {
  // for now just display it
  Serial.println (data);

  MatchState ms;
  ms.Target (data);    
  
  char hour [5]; 
  char mins [5];  
  char secs [5]; 
  char valid [5];
  char day [5];
  char month [5];
  char year [5];

  char result = ms.Match ("^$GPRMC,(%d%d)(%d%d)(%d%d)%.%d+,(%a),.-,.-,.-,.-,.-,.-,(%d%d)(%d%d)(%d%d)");
//                                   HH    MM    SS    ms valid   lat  long spd crs  DD   MM    YY

  if (result != REGEXP_MATCHED)
    return;
    
   ms.GetCapture (hour, 0);
   ms.GetCapture (mins, 1);
   ms.GetCapture (secs, 2);
   ms.GetCapture (valid, 3);
   ms.GetCapture (day, 4);
   ms.GetCapture (month, 5);
   ms.GetCapture (year, 6);
   
   int iHour = atoi (hour);
   
   // make time local
   iHour += timeOffset;
   
   // allow for daylight savings
   if (isDaylightTime (atoi (day), atoi (month), atoi (year) + 2000, iHour))
     iHour += 1;
     
   // pull into range
   if (iHour >= 24)
     iHour -= 24;
   
   // work out AM/PM 
   boolean pm = false;
   if (iHour >= 12)
     pm = true;
     
   if (iHour > 12)
     iHour -= 12;
   
   char buf [8];
   sprintf (buf, "%02i%s%s", iHour, mins, secs); 
   
   // send all 6 digits
   for (byte digit = 0; digit < 6; digit++)
     {
     byte c = buf [digit];
     if (c == '0' && digit == 0)
       c = 0xF;  // code for a blank
     else
       c -= '0';
     if (digit == 1 || digit == 3)
       c |= 0x80;  // decimal place
     sendByte (8 - digit, c);  
     }   
     
  sendByte (2, 0xF | ((valid [0] == 'A') ? 0x80 : 0));  // space, add dot if valid
  
  if (pm)
    sendByte (1, 0xE);  // P    
  else
    sendByte (1, 0xF);    
  }  // end of process_data
  

void loop()
{
static char input_line [MAX_INPUT];
static unsigned int input_pos = 0;

  if (GPSserial.available () > 0) 
    {
    char inByte = GPSserial.read ();

    switch (inByte)
      {

      case '\n':   // end of text
        input_line [input_pos] = 0;  // terminating null byte
        
        // terminator reached! process input_line here ...
        process_data (input_line);
        
        // reset buffer for next time
        input_pos = 0;  
        break;
  
      case '\r':   // discard carriage return
        break;
  
      default:
        // keep adding if not full ... allow for terminating null byte
        if (input_pos < (MAX_INPUT - 1))
          input_line [input_pos++] = inByte;
        break;

      }  // end of switch

  }  // end of incoming data


}  // end of loop

The whole thing works surprisingly well, considering that both the GPS and the MAX7219 are running from around 3V.

I think my cell phone has network based time - may be sourced from GPS too. Close enough anyway.

Aw, but where's the fun in that?

I was thinking that after I had posted 8)

Perhaps better to move to Queensland. No Daylight saving there!

[quote author=Nick Gammon link=topic=158296.msg1185367#msg1185367 date=1365047146] ...then if the processor was run at around 3.3V then anything over around 1.65V would be HIGH, which was even better. So, no level converter. :) [/quote]Brilliant shortcut. Good enough for home use.

[quote author=Nick Gammon link=topic=158296.msg1185367#msg1185367 date=1365047146] A fair bit of code is working out whether it is daylight savings time or not. You may need to adjust the calculations depending on the local rules. For us right now, from the first Sunday in April to the first Sunday in October, it is not daylight savings time. [/quote]Here we have last Sunday in March and October. I think the arguments for the "powersaving" and "higher efficiency (more work hours)" is something leftover from last century and highly overdue for removal?

[quote author=Nick Gammon link=topic=158296.msg1185367#msg1185367 date=1365047146] The time is shown as a 12-hour clock with the final digit being "P" for PM. [/quote]Why? ;)

The 8th digit, not the last digit of the time.

Or are you asking whether or not a normal person can tell the difference between 3 PM and 3 AM?

Update: the clock has failed after about 3 hours of use, I suspect the battery voltage might in fact be marginal. I'll investigate today.

I was right. My new AA batteries delivered enough volts ... for a while.

They dropped back to 1.4V each, giving 2.8V to the device, which (although the GPS looked happy) the rest wasn't.

By a truly amazing coincidence, a parcel arrived in the mail today. The NiZn batteries I had ordered from eBay. I hadn't even heard of NiZn until a week ago.

I ordered them on 29 March, and they arrived on 5 April. Not bad! $AUD 30.33 for 8 batteries plus a charger.

Close-up of a battery, in case you like this sort of thing:

I charged them in about 2 hours. Well it really took about 6 hours, the first 4 were because I didn't notice a plastic tab between the battery and the charger terminals. ;)

Put the new batteries in, and the GPS clock is back on the air! Hopefully for longer this time.

These batteries have a nominal voltage of 1.6V, which should be enough to keep the processor and MAX7219 happy for somewhat longer.

The whole thing works surprisingly well, considering that both the GPS and the MAX7219 are running from around 3V.

I have one of those ebay MAX71219's on the way. I'm happy to hear it works okay on 3V.

[quote author=Nick Gammon link=topic=158296.msg1186676#msg1186676 date=1365108123]
a normal person can tell the difference between 3 PM and 3 AM?
[/quote]Anybody can tell the difference between 0300 and 1500, that was the point :slight_smile:
The other convention involves letters and extra positions to show a 4 digit time. :roll_eyes:

What'd you pay for the ebay MAX7219? Taydaelectronics sells them for $1.25, delivered from CO in the US with quite inexpensive shipping.

[quote author=Papa G link=topic=158296.msg1188025#msg1188025 date=1365187023] I have one of those ebay MAX71219's on the way. I'm happy to hear it works okay on 3V. [/quote]

Well, let's say 3.2V, since it (and I'm not sure which part) failed when the voltage dropped to 2.8V.

Anybody can tell the difference between 0300 and 1500, that was the point

I know, but when you have to pick up the kids "at 3" it takes a moment's work to convert that to "at 15" each time.

What'd you pay for the ebay MAX7219? Taydaelectronics sells them for $1.25, delivered from CO in the US with quite inexpensive shipping.

Sure, I got 20 x MAX7219 for $9.60:

But for $10 I got the whole thing: PCB, MAX7219, chip socket, 2 X 4-digit LED display, edge connector, 2 caps and a resistor.

$9.60 for 20 is not bad. I have use besides whole digit board. You had some dot matrix board too, yes? Be more interested in that. Once I catch up to my design backlog and have some time. Not that I mind much being busy designing.

Yes, I got this:

That was $8 including shipping.

very cool Nick, nice write up too. Thanks for sharing

Do you have a listing for those?

I know it’s an old thread but you never know :slight_smile:

I have built an uno based 8 digit clock like this one

different gps module but same sketch

it acquires the gps signal and displays it to the LED display but every few seconds it stops for a while and then picks up, about 5 second pauses

the serial output shows a steady stream of data from the gps module every second, but something in the code is preventing it from running entirely smoothly

any ideas ?

No ideas without seeing your code and schematic.

// GPS clock with digital read-out
// Author: Nick Gammon
// Date:   4 April 2013
//http://www.gammon.com.au/forum/?id=11991&page=999
// NB: Compile for Lilypad Arduino (8 MHz clock)

// Version 2. Fixes problem with detecting daylight-saving time on the change-over day.

#include <SoftwareSerial.h>
#include <Regexp.h>
#include <SPI.h>

// Adjust for your time zone. Hours + or - from UTC.
const int DST_TIME_OFFSET = -1;

// MAX7219 constants

const byte MAX7219_REG_NOOP        = 0x0;
// codes 1 to 8 are digit positions 1 to 8
const byte MAX7219_REG_DECODEMODE  = 0x9;
const byte MAX7219_REG_INTENSITY   = 0xA;
const byte MAX7219_REG_SCANLIMIT   = 0xB;
const byte MAX7219_REG_SHUTDOWN    = 0xC;
const byte MAX7219_REG_DISPLAYTEST = 0xF;

// For GPS input
SoftwareSerial GPSserial(2, 3); // RX, TX

// how much serial data we expect before a newline
const unsigned int MAX_INPUT = 100;

// send a digit or other data to the MAX7219
void sendByte (const byte reg, const byte data)
  {    
  digitalWrite (SS, LOW);
  SPI.transfer (reg);
  SPI.transfer (data);
  digitalWrite (SS, HIGH); 
  }  // end of sendByte


void setup()  
  {
  // Open serial communications and wait for port to open:
  Serial.begin(115200);

  // set the data rate for the SoftwareSerial port
  GPSserial.begin(9600);
  
  SPI.begin ();
  sendByte (MAX7219_REG_SCANLIMIT, 7);      // show 6 digits
  sendByte (MAX7219_REG_DECODEMODE, 0xFF);  // use digits (not bit patterns)
  sendByte (MAX7219_REG_DISPLAYTEST, 0);    // no display test
  sendByte (MAX7219_REG_INTENSITY, 10);      // character intensity: range: 0 to 15
  sendByte (MAX7219_REG_SHUTDOWN, 1);       // not in shutdown mode (ie. start it up)
  
  // send hyphens during start-up
  for (int digit = 0; digit < 8; digit++)
    sendByte (digit + 1, 0x0A);
  }  // end of setup

// http://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week
// Devised by Tomohiko Sakamoto in 1993, it is accurate for any Gregorian date.
// Returns 0 = Sunday, 1 = Monday, etc.

int dayOfWeek (int d, int m, int y)
   {
   static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
   y -= m < 3;
   return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
   }  // end of dayOfWeek
   
   
// DST = Daylight Savings Time
boolean isDaylightTime (int day, int month, int year, int hour) 
  { 
  int firstSundayInApril;
  int firstSundayInOctober;

  for (firstSundayInApril = 1; firstSundayInApril <= 31; firstSundayInApril++)
    if (dayOfWeek (firstSundayInApril, 4, year) == 0)
      break;

  for (firstSundayInOctober = 1; firstSundayInOctober <= 31; firstSundayInOctober++)
    if (dayOfWeek (firstSundayInOctober, 10, year) == 0)
      break;

  // May to September: not DST
  if (month >= 5 && month <= 9) 
    return false;
  
  // January to March, and November to December: is DST
  if (month <= 3 || month >= 11)
    return true;  
   
  // In April, if not yet first Sunday, still DST
  if (month == 4 && day < firstSundayInApril)
    return true;

  // In April, on first Sunday, still DST before 2 am
  if (month == 4 && day == firstSundayInApril && hour < 2)
    return true;
    
  // In October, if after first Sunday, is DST
  if (month == 10 && day > firstSundayInOctober)
    return true;

  // In October, on first Sunday, is DST after 2 am
  if (month == 10 && day == firstSundayInOctober && hour >= 2)
    return true;
    
  // some date in April or October that did not pass the above tests
  return false; 
  } // end of isDaylightTime  

// here to process incoming serial data after a terminator received
void process_data (char * data)
  {
  // for now just display it
  Serial.println (data);

  MatchState ms;
  ms.Target (data);    
  
  char hour [5]; 
  char mins [5];  
  char secs [5]; 
  char valid [5];
  char day [5];
  char month [5];
  char year [5];

  char result = ms.Match ("^$GPRMC,(%d%d)(%d%d)(%d%d)%.%d+,(%a),.-,.-,.-,.-,.-,.-,(%d%d)(%d%d)(%d%d)");
//                                   HH    MM    SS    ms valid   lat  long spd crs  DD   MM    YY

  if (result != REGEXP_MATCHED)
    return;
    
   ms.GetCapture (hour, 0);
   ms.GetCapture (mins, 1);
   ms.GetCapture (secs, 2);
   ms.GetCapture (valid, 3);
   ms.GetCapture (day, 4);
   ms.GetCapture (month, 5);
   ms.GetCapture (year, 6);
   
   int iHour = atoi (hour);
   int iDay = atoi (day);
   
   // make time local
   iHour += DST_TIME_OFFSET;
   
   // if past midnight with time-zone offset adjust the day so the DST calculations are correct
   if (iHour >= 24)
     iDay++;
   else if (iHour < 0)
     iDay--;
   
   // allow for daylight savings
   if (isDaylightTime (iDay, atoi (month), atoi (year) + 2000, iHour))
     iHour++;
     
   // pull into range
   if (iHour >= 24)
     iHour -= 24;
   else
   if (iHour < 0)
     iHour += 24;
   
   // work out AM/PM 
   boolean pm = false;
   if (iHour >= 12)
     pm = true;
     
   if (iHour > 12)
     iHour -= 12;
   
   char buf [8];
   sprintf (buf, "%02i%s%s", iHour, mins, secs); 
   
   // send all 6 digits
   for (byte digit = 0; digit < 6; digit++)
     {
     byte c = buf [digit];
     if (c == '0' && digit == 0)
       c = 0xF;  // code for a blank
     else
       c -= '0';
     if (digit == 1 || digit == 3)
       c |= 0x80;  // decimal place
     sendByte (8 - digit, c);  
     }   
     
  sendByte (2, 0xF | ((valid [0] == 'A') ? 0x80 : 0));  // space, add dot if valid
  
  if (pm)
    sendByte (1, 0xE);  // P    
  else
    sendByte (1, 0xF);    
  }  // end of process_data
  

void loop()
{
static char input_line [MAX_INPUT];
static unsigned int input_pos = 0;

  if (GPSserial.available () > 0) 
    {
    char inByte = GPSserial.read ();

    switch (inByte)
      {

      case '\n':   // end of text
        input_line [input_pos] = 0;  // terminating null byte
        
        // terminator reached! process input_line here ...
        process_data (input_line);
        
        // reset buffer for next time
        input_pos = 0;  
        break;
  
      case '\r':   // discard carriage return
        break;
  
      default:
        // keep adding if not full ... allow for terminating null byte
        if (input_pos < (MAX_INPUT - 1))
          input_line [input_pos++] = inByte;
        break;

      }  // end of switch

  }  // end of incoming data


}  // end of loop

the schematic is broadly similar to Nick Gammons, (thanks Nick), same pins on the arduino

Very nice description and a very interesting project!

I wish more people would make their Exhibition entries this way.
(There are far too many with nothing in them but :" Come to my website/blog/instructable". Those are nothing but adds to draw traffic to an external site. They add no value to the forum)