I saw a thread about someone who wanted a low power NTP stratum 1 server that could run off of batteries and could be put somewhere without an internet connection. That had me wondering how an esp8266 would do in that situation.
The hardware I put together was an esp8266 based Adafruit Huzzah, and an ublox NEO-7N. Because the esp8266 only has one usable uart and I didn't want to interfere with the uart bootloader, I used the uart's alternate pins on gpio 13 & 15 for the GPS. I attached a software serial object to the USB serial on gpio 1 & 3 for logging and debug messages.
For the software, I started with another project's codebase. Since that project was meant for an ESP32 and 100M ethernet, I only used their GPS parsing and Date parsing code.
My libraries were originally based on the millis() call, which wraps every 49 days but only has 1ms precision. I started by changing it to using micros(), which wraps every 71 minutes but has 1000x the precision.
In order to make it easier to verify the changes I made worked, I created unit tests based on the ArduinoFake mock system. This way I could simulate a local clock through tests and verify the results without having to wait to upload and run it on the esp8266.
After getting it all working in the test environment, I let it run on the actual hardware. I logged the clock offset between the local clock and the GPS PPS and graphed them.
The sawtooth pattern in this graph is from the limitation of 1 microsecond precision of micros(). The control software has detected that the local clock needs to be adjusted by about 75 nanoseconds every second, but the micros() return value only jumps to a new value when the drift adds up to 1us. The code is able to hold the local offset to within a few us, but I'd like more precision. More precision would make it easier to calculate the actual frequency difference between the local clock and the GPS PPS. A 1 us difference is 1 ppm at 1 second, and 0.004 ppm at 256 seconds.
So I switched to using the esp8266's cycle counter, which runs at 80MHz. This is 12.5 nanosecond precision. But this also brings counter wraps every 53.6 seconds, so I had to adjust my control timespan from 256 seconds to 48 seconds.
This looks much better, but there's occasional jumps downwards. let's zoom into them.
These jumps downward must be coming from the PPS interrupt occasionally being delayed. Perhaps something is disabling interrupts temporarily for 1us.
To help filter those out, I took 3 seconds worth of samples and used the median value.
This worked much better. Not every delayed sample is removed, but the vast majority are.
Now that I have the local clock in sync, I added an NTP server to serve the local time and setup a client on my network. As my NTP client, I chose a stratum 1 on gigabit ethernet connected to the AP. This way any latency and jitter added by Wifi would only happen once instead of twice as it would for a Wifi client. I compared it against another stratum 1 also connected via gigabit ethernet (stm32mp1).
The standard deviation of the offset between the two gigabit ethernet stratum 1s was 7.9us. The standard deviation between the gigabit ethernet stratum 1 and the esp8266 was 208.7us. You can visually see that the purple line has much less noise (jitter) on it. Round trip time had a mean of 298.9 us on gigabit, and 2,455.5 us on wifi.
Looking at power usage, it's around 150mA at 5V for both the esp8266 and the GPS module. 1 week at that usage would be around 25Ah. That's around 20 AA batteries, so that's too high. It's better than a Raspberry Pi, though.
For future consideration, power saving could be enabled and presend configured on the NTP client to have it send a NTP request to signal when to come out of power saving. The NTP client would then send a second NTP request a short time after which would hopefully be lower latency (1~2ms instead of 100~200ms).
Another future thing to try would be the esp32, which has two cores. This might make timestamps more accurate as one core would be dedicated to the network processing and it would be possible to poll for the PPS signal on the other core.