I've created a new ESP32 based version that syncs a DS2131 RTC with a GPS module to within a few microseconds. Its still very much a work in progress. It IS functional :-)
Syncing the DS3231 to the GPS is done with a small high level (level 5) interrupt handler in assembly. This generates a timestamp and offset in microseconds tracking the active edges of the GPS PPS and RTC SQW signals. This data is then fed in to a PID algorithm that will generate an offset value used to speed up and slow down the DS3231 RTC.
In a second version I'm thinking of using a larger touch LCD display and adding a DS3231 (RTC) disciplined to the GPS 1hz to handle timing. I've been playing with this using an esp32 and tracking the offset time between the GPS and RTCs 1h pulses as input to a PID algorithm and driving the DS3231s aging offset register to sync them.
The biggest issue I have run in to is the GPIO interrupt latency is typically just under 2us with occasional jumps to 10us! I'm currently averaging the last 10 deltas and using that as input for PID as a work around. With this I seem to be able to keep the RTC to within +/- 2us of the GPS 1hz signal.
I'm thinking about trying to use an esp8266 again. I switched to the esp32 because software serial at 9600 baud gave a lot of watchdog resets, not what you want in a time server. Software serial was needed because the esp8266 has only one usable UART and that is used for programming and debug messages. It has a second one but the receive pin is in use by on module flash making it unsuitable for receiving messages from a GPS module.
I've found that its possible to swap the pins used for UART0 to an alternate set of pins and I can connect the GPS there. You can also send debug messages to UART1 since its transmit pin is usable.
I'm interested to see if the GPIO interrupt latency is more consistent than I have found on the ESP32. There the latency varies between 4us and 38us. With wifi connected it tends to be on the higher side. With two cores, wifi using core0 and my app and GIPO interrupts using core1 I expected the ESP32 to be able to respond consistently.
I've moved to an ESP32, and renamed the project. I had too many issues with software serial on the ESP8266 failing to read properly at 9600 baud. Also I'm now using esp-idf instead of Arduino. I've added a small OLED display to show status and current UTC time.
Its running 3 "tasks":
gps_task - It's detects when the GPS unit is valid and uses the PPS signal as an interrupt, with 4us to 32us latency, to track current time in seconds once the time is set from one of the GPS records. Microseconds are interpolated based on measured internal microseconds between each PPS interrupt. A timer used as "watchdog" detects any missing PPS interrupts and invalidates the time.
ntp_task - listening on port 123 (NTP port) and responding to requests with current time.
dsp_task - manages the OLED display. It receives a "display" message from gps_task at each second interrupt and refreshes the display.
I threw together a quick prototype using a NodeMCU, small oled display and a gps unit. Then spent some time working with the software.
I had seen a number random crashes and found that this was due to power demands from the GPS module so I added a large-ish capacitor nearby.
Since the ESP8266 only has one USART I’m using a software serial implementation. I've still seen some crashes and decoding the stack trace they are happening on the interrupt service routine for this module. A quick look at the modules code shows a WAIT macro that may not be safe in an interrupt service routine,
Its responding to ntp requests and syncing with the PPS signal from the GPS unit. I am seeing a few times where the NMEA message from the GPS is parsed after the next second pulse from the GPS. Because I'm validating validating that the time I have is correct with each message I see it time warp back one second then timeworn forward one second a few times a day. I think that the ESP wifi housekeeping functions may sometimes delay the serial parsing. Or that rendering the display could be taking too long to render each second.
While I don't have a GPS module, I do have boards from SynchroClock that have the DS3231 real time clock 1hz routed to a GPIO pin on on the ESP8266. This lets me pretend I have a GPS (and I read the current time from the DS3231).
I used a strategy mentioned by @Nick Sayer, in the comments on one of his projects here, and used the ESP8266 builtin CPU cycle counter, 80Mhz, to interpolate the time between seconds. And it answers queries!
EDIT: It turns out that ESP.getCycleCount() is not very consistent. Using this I end up with up with well over 250us of jitter! Maybe something with idle sleep or some other power saving mode? In any case using micros() works much much better with jitter less than 20us!