Reasonably percice off-grid timekeeping with $35 of hardware
For whatever reason, I’ve long been interested in time synchronization technology.
NTP in particular—being a fairly easily graspable ecosystem of networked servers—has been floating around in my head for a while, and nearly for the same amount of time, the idea of having my own Stratum 0 time source at home has intrigued me.
At the end of last year, I reserved a little money to spend on “2026 projects”. The first of which being to finally get a reliable time service running for myself without depending on external sources over the internet. To do this, I chose the cheapest option, GPS-disciplined time.
Hardware
I’ve had a spare Raspberry Pi Pico laying on my desk for six-ish months, so to make use of it, I constraine…
Reasonably percice off-grid timekeeping with $35 of hardware
For whatever reason, I’ve long been interested in time synchronization technology.
NTP in particular—being a fairly easily graspable ecosystem of networked servers—has been floating around in my head for a while, and nearly for the same amount of time, the idea of having my own Stratum 0 time source at home has intrigued me.
At the end of last year, I reserved a little money to spend on “2026 projects”. The first of which being to finally get a reliable time service running for myself without depending on external sources over the internet. To do this, I chose the cheapest option, GPS-disciplined time.
Hardware
I’ve had a spare Raspberry Pi Pico laying on my desk for six-ish months, so to make use of it, I constrained myself to using Pi-Pico-compatible electronics for this project.
Through the power of ~15 seconds of searching a parts database and picking something that looks good, I settled on pairing my Pi Pico with a Waveshare Pico-GPS-L76B, as it is both cheap, and I enjoy using Waveshare’s products.
The Pico-GPS-L76B is a “hat” for the Pi Pico that contains a Quectel L76B-M33 GNSS module. In retrospect the L76T (T for Time) would have been a smarter choice, but I did only do about 15 seconds of research on my hardware.
The L76B interfaces with the Pi Pico over a UART interface, and talks in NMEA 0183-compliant messages with support for MediaTek’s proprietary $PMTK configuration commands.
NMEA 0183
“…what?” — I said to myself.
Up until I’d plugged my new components into each other, my only experience with GPS modules had been using pre-built libraries and tools like gpsd to interact with them.
Out of my refusal to use MicroPython in a latency-sensitive application, I completely ditched Waveshare’s L76x library, and directly attached to the L76B’s UART interface. Doing so presented me with a “garbled mess” of strings:
$GNRMC,000502.103,V,,,,,0.00,0.00,060180,,,N*59
$GNVTG,0.00,T,,M,0.00,N,0.00,K,N*2C
$GNGGA,000502.103,,,,,0,0,,,M,,M,,*53
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$BDGSA,A,1,,,,,,,,,,,,,,,*0F
$GPGSV,1,1,00*79
$BDGSV,1,1,00*68
$GNGLL,,,,,000502.103,V,N*61
I soon learned that said “garbled mess” was a set of National Marine Electronics Association (NMEA) standard 0183 messages. The format being a simple CR/LF-deliminated plaintext protocol that transmits parameterized messages with a checksum at 4800 baud over UART.
Thanks to a few PDFs I found floating around online from the chip manufacturer, I was able to get a pretty good grasp on these messages, and the types of commands I could send back to the GPS module.
As I mentioned at the end of the previous section, the L76B accepts $PMTK commands, of which there are a few interesting ones.
To start, I programmed the Pi Pico to send this pair of messages to the GPS to effectively reset it to its “default” state:
$PMTK314,-1*04 # Use default PMTK_API_SET_NMEA_OUTPUT configuration
$PMTK886,0*28 # Set PMTK_FR_MODE to "Normal"
The second command came to be very important, as my GPS kept going to “sleep” after a few seconds of inactivity because it was trying to save power, assuming it was in “aviation mode”.
Finally, I sent a second PMTK_API_SET_NMEA_OUTPUT command to actually select the messages I wanted to receive (and their transmit rate).
PMTK314,0,1,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*29
This string tells the GPS to send the bare minimum information required to keep gpsd happy, once per update cycle (a configurable period).
From RF to NTP
Speaking of gpsd, what am I actually doing with this data?
I chose to use the Pi Pico to perform two tasks:
- Perform initial configuration of the L76B
- Format the NMEA strings into USB CDC-ACM packets for gpsd to handle
This means that my setup looks like this:
[Antenna] -> [L76B] <--> [Pi Pico] -> [Host]
Where the “host” is a machine running gpsd and chrony, two tools I’m very familiar with.
I then have both gpsd and chrony exposing their respective sockets to my personal network for use in some upcoming radio projects, where precise timing and positional data will be important.
My gpsd “configuration” is really just a LaunchDaemon that executes:
gpsd -nN --readonly /dev/tty.usbmodem11
Then, my chrony config contains a refclock directive that reads from gpsd, and compensates for the slight delay induced by the UART->USB translation in my hardware stack:
refclock SOCK <path to gpsd socket> refid gpsd poll 1 offset 0.3 trust prefer
Thanks to the L76B being able to produce PPS-locked NMEA sentences, and very predictable latency in my firmware for the Pi Pico, this actually works out to be a very reliable clock source (as long as I set that 300ms offset). A quick search online also leads me to believe that this is a reasonable setup, and it should be fine to trust in the long run.
Next time I have some brain space for GPS, I plan to get a proper PPS signal in to gpsd, but for now, my home network has been running well with my makeshift Stratum 0 time source.