Skip to main content

Down to one Hertz.

Frequency deviation of a few dozen Hz rarely causes practical problems. But it is fun to push the envelope and reach a precision comparable with the short-term frequency stability of incoming short-wave signals. It can be done with nothing more than readily available equipment and a bit of patience.

As a curious person, I want to know how precise my TRX displays frequencies. Furthermore, in a hidden corner of its configuration menu, my rig has a frequency calibration value that I can set to a useful value. Only: How to find such a value?

Plan of action

The idea is rather straightforward: Use a transmitter with stable, precise, and known frequency. Measure that frequency with your rig. If the result deviates from the transmitter’s known frequency, the calibration of the rig isn’t optimal and needs adjustment (if possible).

Highly precise transmitters

AM transmitters (with always transmitted carriers) lend themselves to measuring their frequency. The highly precise time signal transmitters are particularly useful. Such transmit on exactly 5, 10, 15, 20, and 25 MHz. For our purpose, the USA’s or, for example, their Chinese counterparts can be used equally well. If they can be listened to, they are first choice.

But often they are not to be heard, in particular not in Europe. For us Europeans, the Russian RWM time signal transmitters work well. They can be heard on 4996, 9996, and 14996 kHz; in Europe, almost always at least one of them is receivable. Unfortunately, the signal can be used for our purpose only during the first 7:55 minutes of each half hour, namely, when they transmit a carrier. At other times, they transmit impulses, which are not as suitable. The following waterfall screenshot shows the change from impulses to carrier.

Waterfall display by the WSJT-X program: At the bottom, a spectrum with many lines is still to be seen, generated by the impulse sequence. This signal is not very useful for frequency calibration. At precisely 10:00:00, a steady carrier started to be transmitted. The waterfall shows a single, clear, steady line.

The long wave broadcast BBC transmitter with its highly precise carrier frequency of 198 kHz is also quite useful.

Reception can usually be done with whatever amateur radio antennas are available anyway. While they may not be designed to operate on the transmitters’ frequencies, they typically suffice to get the job done: Our rigs generally have a lot of sensitivity to spare on the pertinent frequencies.

Warm ‘em up!

Before you really start, warm up your rig. Some internet sources recommend warm-up times of hours or even days. My IC-705 needs a mere 40 minutes to stabilize, as the following graph shows. The y-axis is deviation in Hz, the x-axis time since measurement start in minutes. The graph records the warm-up during a reception of a 10 MHz time signal.

Graph of frequency deviation over time. The initial deviation is about 5 Hz. 10 minutes later, it has decreased to 1.5 Hz, 20 minutes, below 1 Hz. After about 40 minutes, only noise of about ±0.5 Hz remains. The whole time, noise of about ±0.7 Hz has been superimposed on the data. A second curve shows the warming up as a smoothed, exponential decay.

Frequency comparison, from simple to precise

How to measure the frequency of a distant transmitter?

The simplest way: Switch your TRX to USB and search for zero beat. A precision of maybe 20 Hz can be reached this way.

To do better, one could use a customary 440 Hz tuning fork. When the tone from the speaker beats the note it emits when struck, the TRX frequency display should be exactly 440 Hz below the carrier frequency received. Precision of one or two Hz is easily obtainable via this simple trick.

WSJT-X

We can do even better, if a computer is available that can run the well-known FLOSS program WSJT-X. WSJT-X is obtainable via https://wsjt.sourceforge.io/wsjtx.html. This software runs under Linux, on Mac, and even under Windows.

If you have a working setup for FT8 with WSJT-X, you’re all set. But if your shack is reasonably quiet, you don’t necessarily have to wire up your rig with your computer: Acoustic coupling from the radio’s speaker to a computer’s microphone or a headset’s usually works, too.

FreqCal: the king’s path

WSJT-X sports well-known functionality such as FT8 and WSPR, but also provides an option to calibrate frequency. It works similar to the tuning fork method: The receiver is set to USB and tuned a bit below the frequency one wants to measure. This time, the difference should not be 440 Hz, but 1500 Hz (typically; you could configure WSJT-X to use another value if you wanted to). With its DSP algorithms, WSJT-X measures the frequency of the resulting audio. If the receiver is precise, an audio of exactly 1500 Hz should result.

How to

The WSJT-X main guide available from the WSJT-X web page instructs how to actually use the frequency calibration mode.

Some additional advice on top of what that main guide has:

If “measure” is active (highlighted with the yellow eclipse in the screenshot below), frequencies as measured are logged to a file fmt.all. To find the directory containing that file, navigate “File” / “Open log directory”.

For the beginning of a measurement campaign, I recommend to briefly set “measure” and unset it quite soon, after just a few measurements. This just ensures the file exists, so you can find it. Next thing, delete it so that no old leftover measurements can confuse things.

It is convenient to only activate “measure” whenever a suitable transmitter gives usable measurement values. Alternatively, it is also possible to open the file with a text editor and delete all lines that reflect dubious measurements values. (Incidentally, the file can also be read with spreadsheet software.)

Deviating from what the guide says, I recommending against activating “tools” / “execute calibration cycle”. When that is activated, the receiver will cycle through configured frequencies, staying half a minute at each. Of course, this can work only if some CAT link connects computer and rig. But even with working CAT, I rather recommend to choose frequencies manually. This a particularly reasonable choice if the RWM transmitters are used, which emit a signal useful for our purposes only during the first about 8 minutes of each half hour.

The red “OOB” (“out of band”) just warns that amateur radio transmissions are not allowed on the present frequency. As we don’t want to transmit, this is irrelevant for us.

If you want to follow the guide’s recommendation to delete frequencies where no signal can be heard, you may want to first change to a new configuration that you created for the present frequency calibration purpose.

Tune the rig

If your equipment has that functionality, you can now adjust its frequency calibration. Precision down to a single Hertz across the entire short wave spectrum is quite in reach.

Any change to your rig’s frequency calibration effectively invalidates your fmt.all file, so you should delete it. Fresh new measurements can then asses the success of your configuration change.

Reward of the effort

Immediately after a calibration effort that went particularly well, the program reported a frequency deviation below 0.01 ppm, which amounts to less of 1/3 Hz across the entire short wave range:

The background shows the WSJT-X calibration screen. A frequency of 14996 kHz was received May 1, 2024. The measured deviation is about -0.08 or -0.09 Hz. "Measure" is activated. On top of that picture, the popup "good calibration solution" has been copied. The frequency line slope is calculated to -0,006±0,002 ppm, the x-axis intercept to 0,00±0,02 Hz, at N=9 (not sure what "N" is here) and a standard deviation of 0.04 Hz.

To check a calibration in this fashion, a time signal transmitter can be received. The “DF” column should show values in the ±1 Hz range; the above screenshot even reaches better than ±0.1 Hz. If the fmt.all files features measurements at at least two different frequencies, “tools” / “solve for calibration parameters” shows the popup that has been shown in the above picture. (But it is always placed on top of the middle of the main window. The above picture is a collage.) If slope is within ±0.030 ppm and intercept within ±0.10 Hz, the goal “precision one Hz across the shortwave spectrum” has been reached.

The popup offers an “apply” button. With that, the data calculated will be used for correcting frequencies reported by the rig, but only within WSJT-X. This is mostly useful if the rig itselfs offers no calibration.

The FMT competition and sources of measurement errors

What precision can be expected?

The ARRL regularly organizes a “frequency measurement test” (FMT). This friendly competition challenges amateurs to measure as precisely as possible the frequencies of four carriers, each of which is transmitted for exactly one minute.

(The WSJT-X website offers an outdated description “FMT User” showing how to compete in FMT successfully with software that has once been available within the WSJT (precursor of WSJT-X) software package. The functionality is now largely available in WSJT-X itself. I’m not sure it is worth the effort to still read that description.)

The results of this competition yield some idea how precise short wave signal frequencies can be measured with common amateur means. During the April 2024 edition, more than 80% of the participants were able to measure all frequencies more precisely than 1 Hz. But only a third measured all four frequencies to a precision below ½ Hz.

The main source of error is probably propagation. The ionosphere is not at one stable height, but its actual (apparent) height keeps changing. Via the Doppler effect, height fluctuations cause frequency fluctuations. The warm-up graph above shows such fluctuations, they seem to be in the order of a several tenths of Hz.

A second source of error, though smaller ones, hides in the time base of whatever sound card is used. WSJT-X boldly displays frequency values with 0.001 Hz precision, which shouldn’t be taken at face value. Should you desire to venture into sub-Hz precision and take the sample rate of your sound card into account, the “fldigi” software contains functionality to measure it. An article by JC Gibbons (N8OBJ) explains how to do that.

How about the transmitter?

Apparently, modern rigs are sufficiently “transceive”, that is, they transmit on the same frequency they receive. This coincides with my every-day experience on the bands. Also, the WSJT-X guide does not discuss this, even though frequency precision within a WSPR band, is equally a concern for both receive and transmit - in particular if frequencies near band edge are used. Each such band is a mere 200 Hz wide.

A simple experiment for this needs two stations, A and B, who work each other in FT8. B changes its frequency to that A is using. If A then sees B on its own frequency, chances are both are transceive.

Strictly speaking and theoretically, it could happen that both are not transceive, but the deviations happen to cancel each other. Now everyday experience on the bands show that such maneuvers consistently results in A and B transmitting on the same frequency within 1 Hz or so. So this is not likely.

A nit-picky method would use two additional stations B and C to find out whether A is transceive: B first sends a carrier. A receives that carrier, measures its frequency, and sets itself up to transmit on that exact frequency. This could be done with “tune” in WSJT-X, running mode WSPR or FT8. Next, A and B transmit on that frequency (B unchanged) in alternating fashion, each for, say, half a minute. C listens to both, measures both frequencies, and compares.

This text is an English translation and an adaption for this blog of material that I originally published via the DARC’s magazine CQ DL. (When I originally send in the article, I had negotiated non-exclusivity after an initial grace period, which is over by now.)

If you have a Fediverse account and want to discuss this, I recommend answering my pertinent announcement toot.

Going live!

This blog has been dormant for more than a year.

In the meantime, I suggested to the Nikola project a change or two. The cooperation was quite pleasing.

The last half year, other things rightfully pulled my attention from getting this blog off the ground.

But now, I’ve reappeared on the blogging stage. Things have now reached a state that can serve as my personal “minimal viable project” for blog publishing with Nikola instead of Pelican. (Incidentally, Pelican has not looked at my PR.)

There’s a lot that could be improved, but what’s in the box right now (using Nikola’s base-jinja theme unchanged), is good enough to get going.

So: Venture out into the world, little blog!

My new blog engine!

It’s about time a get a blog going for myself. Well… Here it is!

Since at least 1997, I’ve been dabbling with software generating HTML from other formats. Today, being the “pipeline person” I am, I enjoy a decent static site generator when I see one.

Besides a pipeline person, I’m also a software developer. Retired by now, but you get the idea. I sometimes entertain an idea how to enhance stuff with a bit of home-grown code. So a plugin-enticing site generator is what I want.

A few years ago, I switched to Python as my main language for small private projects. For two previous sites, I had been using Pelican. But now, it is:

Good bye to Pelican!

Why?

The plugin interface problem

The interface to a Pelican plugin is not too well-specified in the pertinent documentation. I had to find out quite a bit of stuff via trial and error. In the end, I managed to write plugins that did what I intended them to do. But I remained unsure whether they would continue to function flawlessly in the future, with new Pelican versions. Was I using stable API or ephemeral implementation detail? The uncertainty didn’t feel good.

Broken error handling

Then came the day when I was starting yet another plugin. I coded the initial version to simply raise some error. To my horror, Pelican just went ahead and built the site as if nothing were wrong.

Now these two ideas are important to me:

Fail early
Once a failure is detected, processing should stop, lest later processing tries to feed on state that earlier processing failed to provide.
Fail loudly
When the result the user reasonably expects can not be produced by the software, the software should confess that error (e.g., by a non-zero exit value).

Some experimentation and study of code turned out Pelican follows neither of those two ideas. Instead:

  • It specifically contains “fail late” functionality. Errors are caught and logged and otherwise ignored. To remember the error becomes the responsibility of the logger object. To me, that’s a 🤦.
  • Pelican fails silently. Errors are ignored out of the box. The pelican CLI happily reports “no error” via its exit value. Doesn’t matter whether the HTML generated is or isn’t what the user wanted. 🤦
  • One has to specifically provide --fatal errors to pelican to get “fail loudly”. (In passing: I have not found out how to do --fatal errors via the API used by tasks.py. Didn’t find it in the documentation, and didn’t look very hard in the code.) Software that wants me to like it works the other way around: Fail loudly by default, and it may provide a special command line switch to suppress error handling. Though I’m unlikely to use it.
  • I duly reported the silent failure as a bug, and provided a fix (that reads the logger info and fails cleanly on error). The Pelican maintainer seriously argued “Read the log to find out whether something is wrong”. Two months after my initial bug report, there is no indication “silent failure by default” is seen as a problem worthy of a fix.

High time to move on.

Nikola - you’re next!

After a few hours of research, I found Nikola and decided to try liking it.

  • Architecture seems cleaner.
  • Documentation apparently comprehensive.
  • Auto-reloading actually seems to work - never had that working under Pelican.
  • Build on top of the task runner Doit, which I also like - seems to be an improvement over the straightforward invoke I’ve been using.
  • I also investigated Nikola’s issue tracker briefly and found their issue handling reasonable.

Other choices investigated

I also looked into other static page generators powered by Python:

  • blag Too simple. No plugin support I could find.
  • Hyde “Currently hyde is only supported on python 2.7.x. Python 3.x support is in progress … .”
  • makesite More a cooking recipe how to build a site generator yourself than a site generator.
  • Stapy Not well documented. Somehow I had a funny feeling when investigating this one. Also, part of the text of the repo’s README is actually a screen shot.
  • Tinkerer declares itself dead and recommends Baku in its will.
  • Baku uses its own, home-grown templating engine, “to avoid dependencies”. – To paraphrase a well-known saying: “It is easy to remain small standing on no-ones shoulders.”
  • Lektor came in close second. I mostly chose Nikola over Lektor as I was impressed by Doit and like the prospect of partial and parallel rebuilds.
  • I also happened to stumble over Staticman. This isn’t a static site generator, but an interesting approach at a home-grown system for users to leave comments: There is no database, instead, a git repo is used. Not sure I consider that approach viable, but it is interesting.

Nikola’s peculiarities

Out of the box, Nikola produces a tree with relative links only. So if you write ![foo](/images/bar.png), what you get might actually be <img alt="foo" src="../../images/bar.png"> depending on the level of the file that’s generated from your markdown.

I applaud that idea. It means you give an archive of your output folder to a friend and have them point a browser at it, and it will just work. Without a server.

But it can lead to confusion. For example, if you have a reference to something on your own server outside the material that Nikola generates.

This can be changed by setting a configuration variable URL_TYPE = "full_path". This produces src="/blog/images/bar.png". So far, so good. Another configuration variable can be used to configure a non-root URI, under which the blog lives. This blog does this, via SITE_URL = "https://dj3ei.famsik.de/blog/". If you do both, the build in development server no longer works. It serves the files below http://localhost:8000/, not http://localhost:8000/blog/. This breaks links.

2024-12-21 addition: I fixed this and offered my fix as a PR to the Nikola project. Of course, I’m also using my own fix. So nikola serve works for me.