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.
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.