GPSDO FreeRTOS v1.05 — multi-level accumulator working, 24-bit
dithered PWM, and a measurement that says our threshold scaling was wrong
v1.05 of the STM32F411 FreeRTOS GPSDO is up. Two substantial changes and one
result I think is worth more than either of them.
**Algorithm 12 — the multi-level accumulator, after MIS42N's Budget GPSDO.**
Readings accumulate into levels, level n covering 2^n seconds; a correction
fires at the *lowest* level whose error exceeds that level's limit. A large
error acts within two seconds, a small one waits for a longer average, and
there is no loop time constant to choose. The levels come out of the bit
pattern of a seconds counter rather than an array of buffers — eleven levels,
2 s to 2048 s, in 22 bytes.
It shipped in v1.04 with the arithmetic right and five faults in the machinery
around it, each hiding the next. With all five fixed, 23 hours:
```
phase RMS, settled 5-8 ns (best previous: 10-23 ns)
|phase| < 10 ns 86.7% of samples
picDIV re-arms 1 (121 without the TIM2 frequency trim)
10 000 s frequency 4e-12 (previous: 1.4e-11)
```
**24-bit control voltage from a dithered PWM,** also Alan's idea: run the PWM
at fewer bits than you need and vary the duty from period to period so the
average carries the rest. The gain is the carrier, not the resolution — 13 bits
at 12.2 kHz instead of 16 bits at 2 kHz lets the filter corner move from 0.7 Hz
to 4.2 Hz for the same ripple, a six-fold shorter time constant, and filter
delay goes straight into a control loop as phase lag. Alan dithers in a timer
interrupt because a PIC has no DMA; on this part that would be 12 000
interrupts a second competing with the 1PPS capture, so the pattern — which is
periodic for a constant value — is computed once into a table and replayed by
DMA. 0.012% CPU, none of it in an interrupt, and the average is exact by
construction. The loop now keeps the fraction of each correction rather than
truncating it, which takes one step from 3.2e-11 to 1.25e-13.
**The measurement.** I tried to derive the per-level thresholds from the
measured phase noise rather than setting them by hand. The obvious way is to
assume the phase is white, so that averaging 2^L samples reduces the test
statistic by 2^(L/2), and scale the thresholds accordingly.
That is wrong, and not marginally. Measured on two boards of the same design —
same PCB, same OCXO, different rooms — the exponent is **0.95 and 1.03, not
0.50**. The phase these loops fight is a slow wander with an autocorrelation of
0.96 at 60 s and 0.64 at 300 s; averaging barely reduces it. Spread over a 1 s
window: 26.6 ns. Over 1024 s: still 20.6 ns.
The consequence compounds with level. The white-noise table understates the
real spread about five-fold at level 0 and more than a hundred-fold at level
10, so it falls 32x across the hierarchy where the phase itself falls by 1.3x.
On a quiet board the crossing still lands somewhere sensible and the loop
behaves; on the same board in a noisier room it lands at the bottom of the
hierarchy, where each correction has the least averaging behind it.
If you are deriving thresholds from a noise measurement in your own design,
this is worth checking on your hardware: measure the spread of your test
statistic at more than one averaging time and see whether it actually falls the
way white noise says it should. On this design it does not, and a first
difference at 1 s tells you nothing about it — differencing is exactly what
removes the low-frequency part you care about.
v1.05 measures the exponent per level and fits the table to it. Selectable, and
honestly not always an improvement: on the quieter board the measured table
corrects a third as often and doubles the phase RMS, because it asks "is this
deviation unusual for this board" where the white-noise version asks "is this
deviation above the measurement noise" — and for control the second question is
often the more useful one. Which is right depends on the site, so it is a
setting rather than a replacement.
**Also in this release:** a cold-start hang (a `vTaskDelay()` reached before
the FreeRTOS scheduler exists — a hard fault with no output, and it only bit
when the GPS receiver was still booting, so a reset always appeared to fix it);
the frequency digits now follow the loop's own lock state instead of waiting
for a 1000 s average; and the 320x240 and 480x320 screen layouts finally match.
Credit where it belongs: algorithm 12, the zero-crossing correction, the
dithered PWM and the self-assessment idea are all **MIS42N's**, from his Budget
GPSDO. Measurements against a rubidium reference on algorithms 10 and 11 by Dan
Wiering. PCB by Scrachi. Original GPSDO v0.06c by André Balsa.
Source, three-language manual and a changelog that explains the reasoning
behind each change:
https://github.com/jmnlabs/GPSDO_FreeRTOS