For the sake of fun and learning, I'm trying to make the most accurate wall clock I possibly can, driven by GPS. I'm starting with a u-blox ZOE-M8Q breakout board I got from SparkFun, and a Raspberry Pi Pico microcontroller.

At a glance, things appear to work just fine. The GPS receiver ACKs my config messages, it sends the messages I want, the checksums match, the reported time and location seem close to right, the validity flags are good, it sees about 11 satellites, and the PPS signal is close to aligned with the top of the second (comparing by eye with my NTP-synced computer).

test setup and schematic

I want to time events with better resolution than a second, so I have the microcontroller counting the ticks of its 125 MHz clock between PPS pulses. I'm using a PIO state machine to do this, and can achieve a resolution of two clock cycles (the counter ticks at 62.5 MHz, and then I multiply by 2). As expected, the number of ticks per PPS pulse varies slightly. There's high frequency noise of about +/- 4 clock cycles, and gradual (over the course of hours) drift of +/- 200 clock cycles or so, but overall the cycle count per PPS pulse is remarkably close to 125,000,000. So far, so good.

Section 9.2 (Navigation Epochs) of the receiver description states:

when selecting the next navigation epoch, the receiver will always try to use the 1 kHz clock tick which it estimates to be closest to the desired fix period as measured in GNSS system time. Consequently the number of 1 kHz clock ticks between fixes will occasionally vary (so when producing one fix per second, there will normally be 1000 clock ticks between fixes, but sometimes, to correct drift away from GNSS system time, there will be 999 or 1001).

This is where the problem starts. Based on the above description, I expect to see two phenomena occasionally happen at the same time:

  1. The reported time in a UBX-NAV-TIMEGPS message will be ~999 ms or ~1001 ms after the previous time, instead of the usual ~1000 ms.
  2. The actual elapsed time measured in 125 MHz ticks of the microcontroller's clock between the previous two PPS pulses will be ~999 ms or ~1001 ms, instead of the usual ~1000 ms.

In actual fact, I observe phenomenon #1, but not phenomenon #2. I have double checked this with a logic analyzer to rule out a bug in my code. The actual elapsed time between pulses does not change when there is a 1 ms discontinuity in the reported times. This means that either the pulses before the discontinuity do not have accurate timestamps reported in the UBX-NAV-TIMEGPS message, or the pulses after the discontinuity do not (or more likely both are inaccurate). The UBX-NAV-TIMEGPS message is reporting a Time Accuracy Estimate of under 40 nanoseconds, so these results should be impossible.

Here is some data I captured. pps_duration is the number of 125 MHz cycles I counted, and the other fields are as described for a UBX-NAV-TIMEGPS message on page 397 of the receiver description. On that page, it specifies that "The precise GPS time of week in seconds is: (iTOW * 1e-3) + (fTOW * 1e-9)". You can see the discontinuity in the reported time when fTOW goes negative, but pps_duration does not budge.

pps_duration  week  iTOW      fTOW     tAcc  leapS  valid
125000008     2225  79723000   493880  32    18     0x07                                                                                
125000008     2225  79724000   494480  32    18     0x07                                                                                
125000010     2225  79725000   495079  32    18     0x07                                                                                
125000010     2225  79726000   495677  32    18     0x07                                                                                
125000008     2225  79727000   496276  32    18     0x07                                                                                
125000008     2225  79728000   496877  32    18     0x07                                                                                
125000008     2225  79729000   497476  32    18     0x07                                                                                
125000012     2225  79730000   498075  33    18     0x07                                                                                
125000006     2225  79731000   498676  33    18     0x07                                                                                
125000008     2225  79732000   499276  33    18     0x07                                                                                
125000008     2225  79733000   499876  34    18     0x07                                                                                
125000008     2225  79734001  -499524  34    18     0x07                                                                                
125000010     2225  79735000  -498924  34    18     0x07                                                                                
125000008     2225  79736000  -498324  34    18     0x07                                                                                
125000008     2225  79737000  -497724  35    18     0x07                                                                                
125000006     2225  79738000  -497123  35    18     0x07                                                                                
125000008     2225  79739000  -496523  35    18     0x07                                                                                
125000006     2225  79740000  -495921  35    18     0x07                                                                                
125000010     2225  79741000  -495321  35    18     0x07                                                                                
125000006     2225  79742000  -494722  36    18     0x07                                                                                
125000008     2225  79743000  -494122  36    18     0x07                                                                                
125000006     2225  79744000  -493522  36    18     0x07                                                                                
125000006     2225  79745000  -492923  36    18     0x07   

I had my logic analyzer running at the same time, and it saw the same thing (T0 marks the time of the pulse just before the first message reporting a negative fTOW).

logic analyzer output

Here is a graph of pps_duration and fTOW over time.

pps_duration and fTOW over time

Obviously I have misunderstood what this chip does, or I have misconfigured it in some way. Can you help me see where I have gone wrong? Can you suggest additional experiments I should perform? Do I need to buy one of the "Time & Frequency Sync products" mentioned in the receiver description, instead of the ZOE-M8Q?

  • \$\begingroup\$ +1 for a careful presentation. It isn't enough for me. But it's good enough for a nice start. And others may have more than they need. I've a question. Is the PPS pin supposed to provide a narrow pulse once per second? Is that what I'm reading from you? Or am I misunderstanding what it does? (And yes, I'm thinking that you are telling me also that it may be 999 ms or 1001 ms between PPS pulses. I just want to make sure I'm getting your words accurately.) \$\endgroup\$
    – jonk
    Commented Aug 29, 2022 at 3:55
  • \$\begingroup\$ The PPS pin provides a pulse of configurable width, once per second. I set it to pulse high for the first 100ms of each second. \$\endgroup\$ Commented Aug 29, 2022 at 4:00
  • \$\begingroup\$ And how often do you imagine that this 1 ms adjustment occurs? Once per year? Or once per hour? Or? I've no idea the time scale here. \$\endgroup\$
    – jonk
    Commented Aug 29, 2022 at 4:01
  • 1
    \$\begingroup\$ According to hobbs's answer, I guess it is not. \$\endgroup\$ Commented Aug 29, 2022 at 4:19
  • 1
    \$\begingroup\$ Yes, I 3D printed that and the SMA connector bracket. \$\endgroup\$ Commented Aug 29, 2022 at 12:33

1 Answer 1


The information in "Navigation Epochs" pertains to the time that position solutions are valid for; it doesn't have anything directly to do with the PPS output. You should probably be enabling and parsing the UBX-TIM-TP message, which gives timing information for the next event on the TIMEPULSE (PPS) pin, particularly the qErr field in that message.

However, you might find that entirely unnecessary, because the TIMEPULSE is not millisecond quantized the way that navigation solutions are. In fact it's quantized at 26MHz, and the error without additional correction is specified as 30ns RMS, which is close enough to your measurement resolution that you can probably not worry about it, depending on the application.

  • \$\begingroup\$ You might be right! The pulse times reported by UBX-TIM-TP are exactly at the top of the second (zero milliseconds, and zero submilliseconds). Only the qErr is varying from message to message, by +/- 11000 picoseconds, which seems about right. I'll let this run for a while, and if the data looks good tomorrow evening then I'll accept this answer. Thank you! \$\endgroup\$ Commented Aug 29, 2022 at 4:17
  • 2
    \$\begingroup\$ @NeilForrester no problem, good luck (I undertook a similar project in the past). In case it wasn't clear, basically all of the variation you're seeing in pps_duration beyond +/- 4 clocks is due to the Pi Pico's own oscillator drifting (mostly with temperature). \$\endgroup\$
    – hobbs
    Commented Aug 29, 2022 at 4:28
  • \$\begingroup\$ That's what I figured, but thank you for confirming. :) \$\endgroup\$ Commented Aug 29, 2022 at 4:33
  • 1
    \$\begingroup\$ Looking more closely at the data and the receiver description, you are definitely correct. Thank you for the help! \$\endgroup\$ Commented Aug 29, 2022 at 4:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.