Super-Accurate (1ms/yr) GPS-Corrected RTC Clock - *without* Internet NTP

Project goal: Create a super-accurate (<1ms drift per year) standalone 7-segment LED clock whose time stays accurate and “sets itself” without access to the Internet or a host computer (i.e., without using NTP servers) by updating time via GPS time signals. Since GPS satellites have their own onboard atomic clocks, this is an extremely stable and accurate time source! The code only uses SPI.h and Wire.h comm libraries; everything else (interfacing with GPS module, Max7219, RTC DS3231, time calculations) is coded “manually” with NO OTHER additional libraries (as a challenge and learning experience). So the total code base ends up being quite light (about 7,300 bytes). This sketch maintains super-accurate time in an area where there is no access to internet but there is access to a GPS signal.

Project design: Arduino controller (Mega for prototyping, Nano for deployment); 8-digit 7-segment LED display module with Max 7219 controller chip; Ublox Neo-6M (clone) GPS module; DS3231 RTC module. External power supply via 5V USB phone charger.

Requirements: RTC clock time appears on the 7-segment LED display. RTC clock time is updated hourly by syncing with GPS module’s 1Hz PPS signal (this achieves the <1ms accuracy)… so that the clock “sets itself” on a permanent basis without user intervention. User-defined UTC offsets (in code) display time in local time zone. User-defined daylight-savings rules, including fractional hour offsets (need to make sure it can work in Nepal!). If power is lost, unit maintains time through RTC battery backup.

[note: I know there are lots of libraries and solutions already on the shelf to handle these things, but I set myself a challenge to code without them!]

How it works:

  • There are 2 ways to pull time and date from the GPS module. One is by reading various NMEA sentences and parsing them (this appears to be the most common approach if you check out other GPS clock projects). BUT… there is quite a bit of latency in the time it takes for the NMEA sentence to be sent, read, and parsed, especially at 9600 baud. I measured about 450ms (almost half a second!) latency when I compared the time set parsing NMEA sentences to the highly accurate 1Hz time-pulse output of the GPS module. We can do better than 450ms, right? YOU BET! Read on…
  • The magic happens in that the Neo 6M GPS module is capable of outputting a 1Hz “Pulse Per Second” (PPS) signal. This signal is precisely synced to the UTC time received from the GPS satellites once the GPS module has a fix. So we will set time with this PPS reference (via digital reads), and NOT the NMEA sentences. (Make sure to get a module board that has the PPS pin on it… not all modules do!). Do we really need this absurd level of accuracy? Of course not! But… it’s fun.
  • To further minimize latency, when my code prepares to sync the RTC to GPS, it reads a standard NMEA sentence, then increments the second to be “primed and ready” for the next PPS signal. That way, as soon as the PPS signal is received, we immediately writes the new second to the time registers on the RTC
  • When syncing with this PPS method, I get a 20 microsecond difference between the RTC squarewave output and the GPS’s PPS signal right after doing the syncing. By doing the sync every hour, we assure a super-accurate RTC time register is maintained. The RTC 1Hz squarewave output in turn triggers an update of the RTC time to the LED display. Accounting for the time it takes to read the RTC time registers and then output those data to the LED module, I’ve calculated a total latency of about 400 microseconds best case and, as the RTC’s accuracy drifts over the hour, around 1.2ms worst case.

Note: this was a terrific learning experience, showing how to program and control the various modules “manually” without libraries. The intricacies of setting time, and dealing with UTC offsets and daylight savings, were also huge coding and mental challenges. I know my code can certainly be improved in areas, but it does appear to work!

IMPLEMENTATION NOTES [SEE FOLLOW-UP POST FOR MORE ON THESE]

A) GPS Module and super accurate time-setting.
B) UTC Offsets for local time - without a library!
C) Daylight savings - without a library!
D) RTC modules - hidden hardware traps!
E) Interfacing with the Max7219 - without a library!
F) Interfacing with the DS3231 RTC chip - without a library!
G) IR Remote Features - without a library!

PROJECT PHASES
Phase 1: Prototype hardware and code on a Mega. [done]
Phase 2: Migrate to Nano. [done]
Phase 3: IR remote interface to toggle date/time, and toggle local/UTC time. [done]
Phase 4: Build enclosure for tabletop display.

CODE (Github):
https://github.com/00steve00/Super-Accurate-Arduino-Clock/tree/master

IMPLEMENTATION NOTES [continued from above]

A) GPS Module and super accurate time-setting. This whole project hinges on the ability of the GPS receiver to output a 1 Hz PPS (pulse-per-second) signal based on the UTC time received from the GPS satellites. The documentation for the U-blox Neo-6M GPS receiver is formidable. (My clone seems to have the same functionality.) The full documentation is 200+ pages: U-blox 6 Receiver Description and Protocols.

The important part for our application is the timepulse feature. The unit has a timepulse output that defaults to 1Hz pulses (goes high for 100ms every 1000ms) when the GPS receiver has a GPS fix lock. (In the documentation, see Appendix Section A.17 for the timepulse default settings on the 6M.) The timepulse goes high at the "top of the second" -- so the rising edge is what we want to watch for on the module PPS pin in order to set our RTC as precisely as possible. On the modules commonly available for purchase, the timepulse output is on the PPS pin. As mentioned above, make sure the module you purchase actually has the PPS pin. Some Neo-6M modules for sale out there don't have it. Check pictures carefully or ask the seller. You're paying for the whole GPS receiver chip, you should be able to access all its functions!

B) UTC Offsets for local time - without a library! Implementing the offset is fairly straightforward for whole hour offsets, but more complicated for fractional hours. There are many places around the world with fractional hour offsets from UTC. My code allows the user to specify the offset in both hours and minutes relative to UTC.

C) Daylight savings - without a library! The tricky part here is that in the US and Europe, for example, the begin and end dates of "daylight savings" or "summer time" are defined not as specific dates each year, but in terms of "second Sunday of March" or "first Sunday of November". So the actual start/end dates move around year to year. My code allows the user to specify the formula in a format of "nth" day of the week for a given month, at a given time. The code then determines on the fly whether DST is in effect at any given time based on the settings. If a location doesn't observe DST, then simply put an arbitrary start/end time and make sure the offset hours are the same.

D) RTC modules - hidden traps! There are loads of very cheap DS3231 RTC modules out there. But there are some things to watch out for! First, not every DS3231 chip is the same on these modules. There is the higher accuracy (+/- 2ppm) DS3231-SN chip, and the lower accuracy (+/- 5ppm) DS3231-M chip. Might as well spend a few extra pennies and get the good one. I had to ask several sellers till I found one that promised it was the SN chip.

Here's the other trip for buying these modules. See my forum post here. But if you intend to use the RTC with a battery backup to keep time with power off, be aware: these modules ship set up to put a charge current on the + side of the battery! So you can't use a standard CR2032 battery in this case. The hack is to remove a certain diode or resistor on the module, and then you're good to go. See this post here and here for info on this battery issue and hacking the module board.

E) Interfacing with the Max7219 - without a library! I owe this all to @CrossRoads. In researching how to work with the Max7219 for LED control, I came across this post: MAX7219 Library selection and @CrossRoads made the point that it's easy to control the Max7219 with some basic SPI commands using the SPI.h library. This really opened up my world. I had a lot of fun digging into the Max7219 datasheet, and really understanding how it works. If I had relied solely on one of the various LED control libraries, I would not have the same understanding of the chip.

F) Interfacing with the DS3231 RTC chip - without a library! After being inspired to control the Max above, I thought let's do the same with the DS3231. And it turns out controlling this was pretty straightforward as well. Once again, it required digging into the DS3231 datasheet to really understand how to control the chip. Then I dug into some of the source code of some of the DS3231 libraries to see how manage the I2C interface using Wire.h library commands.

G) IR Remote Features - without a library! This code also takes input from an IR demodulator receiver to decode commands sent by an infrared remote control, in this case a Sony remote. Again, the code does not rely on libraries. This code assumes a 20-bit Sony remote protocol. The code could easily be adapted to other protocols.

Karma+!!!

Code updated on github for a few enhancements and 1 fix related to the date syncing to local or UTC time, instead of only UTC time.

CODE (Github):

Project starred and watched!!!!

thanks to the moderator for the bump

Thank you Steve for posting this code and the description. I have your code running on my set-up here on the bench - same hardware as yours - even the TSOP38238.

I have an oscilloscope probing the PPS (from the GPS) and the SQW (from the DS3231) signals as well as decoding the I2C protocol to the RTC. After setting up and the GPS getting a 3D fix, the two 1HZ signals were off by 60 to 250 milliseconds. However, after some time (I didn't measure it), the two signals are now aligned and the seconds counter is in sync with my shortwave receiver tuned to WWV (by that I mean the seconds change in synchrony to the audio tone at the top of each minute). I guess I was expecting the RTC to get GPS time as soon as the GPS got a fix; but I was not able to capture that traffic with the protocol analyzer. Can you explain more about how this works? I can read the code but I don't completely understand it - that's my limitation, not a defect in the code.

As a relative newbie I tend to rely on libraries - I admire your skill at hand-coding much of this. In this case I would be keen to use an LCD to add dates, day-of-week, 12-hour display, PDT/PST, etc. while maintaining the accuracy; but that's out of scope for this conversation.

Regarding IR codes; can you give a generalization of the buttons on a remote that correspond to these hex codes? I.e. "TV channel up", "TV mute" or something like that? I have some experience with SIRC (I have used Ken's blog as a reference and it is an excellent resource) but still, grabbing some remotes in the workshop and giving it go, would be nice to get a general idea of which remote might have the best chance to work.

I really dig the whole, "super accurate" thing and the idea of a GPS with an RTC is appealing (I have developed several NTP clocks around the house). Thanks again for posting this.

Rob

I believe that it is explained here, in the GitHub repo

It gets an NMEA sentence and read the seconds, for example 34.
So it knows that the next PPS pulse will be for second 35.
When it sees the PPS pulse it writes immediately the right time with second 35 in the RTC registers so it doesn't waste time reading and parsing the next time NMEA sentence.
This is done every hour.

Thank you zoomx.

I cannot get the code to update the date in the RTC. I have confirmed I am getting the correct messages from the GPS.

Can you help me understand when the date gets updated to the RTC? I see that it checks for a new date, but I can’t tell how often it does this. How can I initialize an RTC that has not yet been set to the correct date?

Thanks.

Line 866 in the original source on GitHub the routine that does the synchronization.

Line 791 says that it is done at 15 minutes of every hour.

Thank you for your reply.

Yes, I saw those lines; in fact I changed it to every minute (for testing purposes). But it’s not updating the date in the RTC. It appears from monitoring the I2C traffic that the code is not sending the date to the RTC.

Point to note, GPS satellites do not transmit UTC time.

GPS satellites transmit GPS time which is based on the time in January 1980.

To arrive at the time UTC, a GPS receiver must account for the current number of leap seconds, which is currently 18.

A GPS receiver does adjust for the difference but it can take some time from power up before you can be sure a GPS has the correct value of leap seconds and unless the GPS knows the current value of leap seconds, the time it is sending out is not correct.

Have fun.

If I take a DS3231 RTC breakout board (module) with a coin battery installed and program it with a given time and date using a Nano and a ready-to-use library (there are several to choose from), and then remove it and insert it into this circuit (also a Nano with a GPS) running this code, it somehow overwrites/corrupts the date in the module.

The module performs as expected and does not exhibit this behavior in a circuit using example sketches for RTC libraries.

It's always possible that there is something wrong with my RTC module. I have several and they all do the same thing. They were sourced off-shore so I can't vouch for their authenticity, other than to say, they work with other libraries.

It would be helpful to get some clarity on the IR codes regarding the type of remote (TV, CD player..?) and the function each code represents. My efforts to reverse-engineer these have been unsuccessful so far.

Thanks for creating this project, it is a great start to what I'm looking to build. I originally tried to build something similar with a Raspberry Pi Zero, GPS hat and seven segment LED display hat but couldn't get the accuracy high enough for my desires, primarily due to Linux not being a RTOS. I'm now redesigning it with an Adafruit Huzzah32, Adafruit Ultimate GPS and DS3231 "featherwing", still using the same MAX7219-driven display hat.

I'm new to Arduino projects and electronics in general so this question may be a bit obvious: Why not use an interrupt on a GPIO pin to determine when the PPS goes high? From reading the code, I think it's checking to see if the PPS has gone high every time the main loop runs. Wouldn't an interrupt potentially be more accurate?

n6rob:
I have an oscilloscope probing the PPS (from the GPS) and the SQW (from the DS3231) signals as well as decoding the I2C protocol to the RTC. After setting up and the GPS getting a 3D fix, the two 1HZ signals were off by 60 to 250 milliseconds. However, after some time (I didn't measure it), the two signals are now aligned and the seconds counter is in sync with my shortwave receiver tuned to WWV (by that I mean the seconds change in synchrony to the audio tone at the top of each minute). I guess I was expecting the RTC to get GPS time as soon as the GPS got a fix; but I was not able to capture that traffic with the protocol analyzer. Can you explain more about how this works? I can read the code but I don't completely understand it - that's my limitation, not a defect in the code.

What is done to synchronize the DS3231 1HZ pulse with the GPS PPS? Is the pulse timing reset when the RTC time is set?
Thanks, bill

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.