Repository navigation
Guide Self calibration
A cheap radio's oscillator is off by tens of ppm: at 1 GHz that is tens of kHz. The selfcal module (core/src/selfcal, dect2/selfcal/selfcal.h)
measures that error against a signal whose frequency is known and gives the number for the radio's ppm setting.
Sign: positive ppm = the radio's clock runs fast, a signal shows up below its real frequency (same as radioPpm() in source.h).
One oscillator sets the LO and the sample clock, so a tone of true frequency fnom is seen at fnom / (1 + ppm 1e-6):
ppm = (fnom / (centerHz + basebandHz) - 1) * 1e6.
| Reference | How | Typical accuracy |
|---|---|---|
| Steady carrier or pilot (ATSC pilot, a CW beacon, a test oscillator) |
ToneMeter on the IQ |
the station's own, ATSC <= 0.05 ppm in practice |
| FM stereo pilot, 19 kHz |
FmPilotMeter (discriminator, then a tone meter in the MPX) |
most stations lock it to a good reference, the standard only promises +-2 Hz (100 ppm), so use several stations |
| GSM / LTE / NR cell carrier | the cell decoder (core/src/cell, nr, nbiot) reports its measured carrier error, fromFreqError() converts it |
about 0.05 ppm |
| GPS L1 C/A carrier | the gnss decoder's carrier after removing the Doppler of the tracked satellite, fromFreqError()
|
well under 0.1 ppm |
| DVB-T / DVB-T2 pilots, other digital TV | same, fromFreqError()
|
depends on the broadcaster |
| Time signals | not used |
combine() takes several estimates (for example three FM stations and a cell): it drops a reference that is more than 3 sigma (or 0.3 ppm) from
the median and weights the others by 1 / sigma^2. A single odd station therefore cannot move the result.
- The IQ is mixed down by the tone's expected place and low-passed with a triangular (sinc^2) filter, hop = decimation, so that only the search window (+-100 ppm of the carrier by default) is left. A strong neighbour (FM audio around the pilot) is suppressed well.
- The first 0.5 s are searched with a zero-padded Hann FFT; the peak, refined by a parabola on the log spectrum, gives the offset to about 1 Hz.
- The signal is then summed in 50 ms blocks after a mixer at that offset. The phase of the block sums is unwrapped and a line is fitted;
its slope is the remaining frequency error. The uncertainty comes from the residual of the fit (
sigma_f = sigma_phi / sqrt(Sxx) / (2 pi Tblock)), so it falls with time^1.5, and no result is given when the phase scatter exceeds 1 rad (no steady tone).
-
Storekeeps the history per radio serial in a text file (utc, ppm, sigma, temperature if known, which reference).drift()gives the warm-up slope in ppm per minute;value()gives the newest measurement while the clock is still moving and the weighted mean of the last hour once it has settled (under 0.01 ppm per minute). -
applyPpm(settings.radio, ppm)writes theppmkey the radio panel and every driver read (and removes it for 0). -
Wizardis the state machine of the Calibrate dialog: Searching -> Integrating (progress from the current uncertainty against the 0.2 ppm target) -> Done, or Failed after 120 s without a usable reference. A result worse than the target but under 1 ppm is accepted at the time limit. - A radio whose oscillator drifts with temperature should be calibrated after warm-up (5 to 10 minutes); the stored history shows how far it moves.
tests/selfcal/test_selfcal_core.cpp (ctest name selfcal_core): synthetic carriers at -50 .. +50 ppm (100 MHz, 584 MHz ATSC pilot, 800 MHz at 0 dB
SNR) and a synthetic FM station with audio, pilot and a stereo subcarrier at 240 ksps all come back within 0.2 ppm; noise alone gives no result;
combine, store (save/load, drift, value) and the wizard are checked.
Radio settings -> Calibrate... (shown for radios that have the ppm setting; not with a converter). The dialog offers the FM stereo pilot of
the station you are tuned to (LiveCal::FmStation: the station is mixed to the centre of the raw capture, filtered to about 200 kHz and given to
FmPilotMeter; it works with offset tuning and any rate from 128 ksps up) or a carrier of known frequency (LiveCal::Carrier). The engine hands
the raw samples over through Engine::setIqTap() (the point where the recorder takes them). The measurement is the error left on top of the ppm
already set, so Apply writes the old setting plus the measurement. Every finished measurement goes into the history (kept in the preferences
under selfcal.hist, per radio), shown as a graph with the warm-up drift.
fromCell() and fromGpsCarrier() turn a cell's carrier offset (CellTelemetry::cfoHz) and a tracked GPS carrier residual into estimates, and
combine() merges them with the others; the dialog itself does not use the cell and GPS decoders yet (it needs their modes running on the same
capture).
See also: Tutorials and guides, FAQ, Troubleshooting.
Getting started
Using OnAir
Tools and guides (0.2.7)
- Band survey
- Message database
- Watchlists
- TV recorder
- Time machine
- Pass scheduler
- Plugin SDK
- Headless server
- All guides
Radios
Modes by group
- TV
- Broadcast radio
- Aviation
- Maritime
- Satellite & navigation
- Two-way radio
- Cellular & rail
- Pagers
- IoT & sensors
- Weather & science
- Amateur: digital
- Amateur: voice & packet
- HF utility
- General & tools
Tutorials
- DVB-T2 TV with a HackRF
- FM radio with RDS
- Aircraft with ADS-B and the map
- Ships with AIS
- DAB+ digital radio
- Weather satellite images
- HF digital: FT8 and WSPR
- Digital voice: DMR, D-STAR, P25
- Pagers: POCSAG and FLEX
- Time stations and the clock
- Record IQ, then the Signal workbench
- The signal sample library
- Control a rig with Hamlib
- Two receivers and the multi-decoder
Reference
Help
Legal and privacy
More