Posted by
| Nick Gammon
Australia (23,133 posts) Bio
Forum Administrator |
Message
| 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.
[EDIT] See below. After a few hours the 2 x AA batteries failed to power it properly. I substituted 2 x AA NiZn batteries which have a higher nominal voltage. Or you could use 3 x AA batteries and change the voltage divider a bit (eg. change 1.5K resistor to 5.6K).
Code
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
// 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 = 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);
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 whole thing works surprisingly well, considering that both the GPS and the MAX7219 are running from around 3V.
[EDIT] Fixed code on 7 April 2013, to correct bug where the DST calculations did not take into account that the day may be out by one. For example, on 6 am on Sunday, if you are 10 hours ahead, it thought the day was still Saturday and thus did not change the clock.
Schematic

Notes
On the cable that comes with the GPS there may be a gray wire. On mine this was on the left when plugged in (pin 6). Looking at the GPS from above (see diagram) pin 1 is on the right.
It turned out I actually have a EM-411 which is wired identically except that Tx/Rx are swapped, which is a bit of a trap.
The daylight saving calculations are for Melbourne, Australia. You could adjust the "timeOffset" variable for different time-zones, and if you live in the Northern Hemisphere the daylight saving is likely to be reversed at the very least (as Summer is in June) and may also start and end on different dates.
Battery voltage
A few more hours of testing revealed that the device failed to start up once the batteries drained a bit. Maybe 2 x AA batteries was a bit marginal.
You might want to use 3 x AA batteries instead, and maybe adjust the voltage divider a bit (eg. change 1.5K resistor to 5.6K).
Or, this worked for me ... I got a couple of 1.6V NiZn batteries from eBay:

The extra voltage they put out has kept it running ... for now. ;)
Boost converter
Further testing indicates that even that NiZn batteries were not reliable after a while. Instead I inserted a boost-converter module from eBay. This particular one cost just over $1. Try searching for "1A 3V to 5V DC-DC Converter Step Up Boost Module" or similar.
Placing that between the battery and the input to the board boosted the output of the batteries enough for the device to work more reliably.
There are other modules available which supply less current but work at lower voltages. For example "1-5V to 5V 500mA DC-DC Converter Step Up Boost Module". These cost around $1.50. |
- Nick Gammon
www.gammon.com.au, www.mushclient.com | Top |
|