I’ve been working on a little project to create a stratum one time server running on a PCjr. For those of you not familiar with the technology, a stratum one time server is one hop removed from the source of time, which in my case would be the GPS satellite system.
Getting time from a GPS is fairly simple - with the right model receiver it is as simple as reading data from the serial port and parsing it. GPS receivers generally send data to a computer once a second. Sending only the timestamp data on my receiver (a Garmin 18X LVC) at 4800 bps takes around 150 milliseconds and it is highly variable, so you can’t get great timing resolution just by using the serial port.
There is an elegant fix for this. Some receivers (including the 18X LVC) include a 1PPS (one pulse per second) line, which goes high for 20 milliseconds at the top of each second once a GPS fix is made. So once you read the time you can wait for the pulse to know when the next second has just started. The 1PPS is often wired to the Carrier Detect pin on a PC, which is a pin that can be programmed to generate an interrupt.
Fast forward, I have the code that parses the GPS timestamp data and takes an interrupt from the 8250 UART when the 1PPS signal trips the carrier detect line. This is great, but knowing the top of each second isn’t good enough on this machine. Here is why.
The clock crystal used on the original IBM PC runs at 1,193,180 Hz . This is connected to timer 0 on the 8253 timer, which is programmed to fire an interrupt every 64K pulses from the clock. That results in an interrupt 18.2 times per second, or once every 54.92ms. That interrupt is wired to IRQ 0 and has an interrupt handler to maintain the date and time on the machine. So the timing resolution on the machine is at best around 55ms.
One approach to get better timing resolution is to reprogram the 8253 to interrupt at a faster rate. If you interrupt 64 times faster you can get an interrupt every 0.85 milliseconds. Of course you chain the standard interrupt handler so that the machine keeps the correct date and time. This approach is simple but it slows the machine down due to the extra interrupt overhead.
Another approach is to try to latch the counter in the 8253, and then use that counter and the standard BIOS ticks to compute the time. In this setup the BIOS ticks gives you 55ms resolution while the counter in the 8253 gives you microsecond resolution.
I’ve tried both approaches. The latter approach is great on paper, and I can get timings as accurate as 270 micro seconds with my C code. However, every once in a while my timings are off by 55ms. Why? There is a fundamental flaw … to get the time accurately you have to freeze both the 8253 counter and the BIOS ticks counter at the same time. And there is no way to do this. Even if you are clever and disable interrupts at the right time there is still a race condition that can lead to an extra BIOS tick getting recorded. I’ve minimized the window and I’m still having it happen 1.5% of the time.
And of course then I find out that Michael Abrash writes about this problem in his Graphics Programming Black Book. I should have read that first. ;-0
As for the PCjr, what a rotten machine. ;-0 It works hard to keep up with a 4800 bps serial stream. Touching the keyboard fires the non-maskable interrupt which can delay the serial port and the 1PPS interrupt. And screen writes through the BIOS - awfully slow! I timed a BIOS call to move the cursor at around 300 microseconds alone.
It’s a fun project but I think I’ll settle for 1ms of accuracy and call it done. Which means going back to the first technique, where the 8253 interrupts 64x faster. It’s not great, but I don’t have the fundamental problem of trying to latch two counts atomically.
My next step: get the TCP/IP code worked in so that it can actually serve the time. Don’t worry, I won’t advertise it as a stratum one time server on the internet.