Publish FT8 second-by-second post with tighter, snarkier prose

This commit is contained in:
Graham McIntire 2026-04-27 13:09:32 -05:00
parent 3b7f7b39c9
commit 1a9f846719
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -7,95 +7,59 @@ date = 2026-04-27
<div class="scene-host" data-scene-host>
The most popular digital mode in amateur radio sounds, to a human ear, like a small chorus of out-of-tune piccolos warming up. It runs for exactly twelve and a bit seconds, repeats every fifteen, and somehow squeezes a complete contact between two stations into less audio than a ringtone. Unlike the dial-up handshake, which serenaded a generation of pre-broadband households with a sequence whose every wail and chirp meant something specific, FT8 is too quiet, too brief, and too dense to dissect by ear. The information is in there. Your auditory cortex just doesn't have the right decoder.
FT8 sounds like a chorus of out-of-tune piccolos having a brief panic attack. Twelve and a bit seconds of warbling, repeating every fifteen, somehow containing an entire contact between two stations on opposite sides of the planet. Dial-up modems at least had the decency to be loud about what they were doing. FT8 is too quiet, too brief, and too dense for any of it to be obvious. The information is in there. Your ears just aren't the right tool.
What follows is what those tones are actually doing, second by second, from the moment the operator clicks "Enable TX" to the moment a similar-looking string of characters lights up on someone else's screen 8000 km away.
So here's what those tones are doing, second by second, from the moment someone clicks "Enable TX" to the moment a near-identical string lights up on a screen 8000 km away.
<div id="scene-ft8-spectrum"></div>
The whole transmission, laid out as the spectrogram a receiver would see. Time runs left to right across 12.64 seconds. The vertical axis is the eight 8-FSK tones, spaced 6.25 Hz apart for a total signal bandwidth of about 50 Hz. The 79 tones are not pseudo-random fill; they are the actual output of an FT8 encoder running in the browser on the message "CQ W5ISP EM13", carrying 91 bits of source code + CRC and 83 bits of LDPC parity. Drag the time cursor (or flip auto-play) to step through symbol by symbol; the readout below tells you which phase you're in and what those three bits, or that sync symbol, are doing. The amber Costas-array pattern (3, 1, 4, 0, 6, 5, 2) appears three times, at start, middle, and end, and is the scaffold the rest of the protocol hangs on.
The whole transmission as a receiver sees it. 12.64 seconds wide, eight 8-FSK tones tall, spaced 6.25 Hz apart. The 79 tones aren't pseudo-random fill, they're the literal output of an FT8 encoder running in your browser on "CQ W5ISP EM13". Drag the cursor or flip auto-play to step through. The amber staircase (3, 1, 4, 0, 6, 5, 2) is the [Costas array](https://en.wikipedia.org/wiki/Costas_array), repeated three times. Everything else hangs off it.
## Before t = 0: the computer does the hard part
Long before any RF leaves the antenna, your computer takes the message you want to send and beats it into a shape the radio knows how to handle. FT8 messages are 77 bits. That's the entire payload budget. There is no negotiation. Want to send "CQ W5ISP EM13"? That fits, with room to spare. Want to send a paragraph of pleasantries? You cannot. The protocol is constitutionally incapable of small talk.
The radio's job in FT8 is mostly to stay out of the way. The actual work happens in software, where your message gets compressed into 77 bits and not one bit more. "CQ W5ISP EM13" fits comfortably. A heartfelt paragraph about your antenna does not. The protocol is constitutionally incapable of small talk, which depending on your view of amateur radio is either a tragedy or its single greatest feature.
The 77 bits are produced by a [source encoder](https://en.wikipedia.org/wiki/Source_coding) that knows the structure of an amateur QSO and exploits it ruthlessly. Standard callsigns get hashed into compact integer indices. Grid squares get packed into 15 bits. The signal report is a small integer offset from a known base. The full taxonomy of FT8 message types (standard, contest, EU VHF, ARRL Field Day, free-text, telemetry) is encoded in the first three bits, with three more bits for sub-type, so the decoder knows how to interpret the rest.
A [source encoder](https://en.wikipedia.org/wiki/Source_coding) that knows the shape of a QSO does the squeezing: callsigns hash to integer indices, grids pack into 15 bits, signal reports are small offsets from a base, and three bits up front declare which of the message-type schemas applies. From there:
Once the message is 77 bits of densely packed binary, three more steps happen in quick succession:
1. [CRC-14](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) over the 77 bits, appended. 91 bits.
2. [LDPC(174,91)](https://en.wikipedia.org/wiki/Low-density_parity-check_code) doubles that to 174 bits. The extra 83 are parity, smeared across the codeword so noise has to chew through a lot of it before anything is unrecoverable. This is the part that buys you decodes at -24 dB SNR.
3. Group the 174 bits three at a time, [Gray-code](https://en.wikipedia.org/wiki/Gray_code) the result, and you have 58 symbols numbered 0 through 7.
1. A [CRC-14](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) is computed over the 77 bits and appended, giving 91 bits. The CRC is purely a sanity check; it does not protect against errors, it only detects them.
2. The 91 bits get fed through a rate-1/2 [LDPC(174,91) encoder](https://en.wikipedia.org/wiki/Low-density_parity-check_code). 174 bits come out. The 83 added bits are parity, distributed sparsely across the codeword in a pattern that lets the decoder recover the original even when many of the 174 bits arrive wrong. LDPC is the reason FT8 decodes down at -24 dB SNR while you can't hear a thing in the speaker.
3. The 174 bits get grouped three at a time into 58 [octal symbols](https://en.wikipedia.org/wiki/Octal), each one a number from 0 to 7. Each symbol becomes one of eight tones. The mapping is [Gray-coded](https://en.wikipedia.org/wiki/Gray_code) so an error to an adjacent tone (the most common kind) only flips one bit.
Then the symbol stream gets sandwiched between three copies of the famous [Costas 7x7 array](https://en.wikipedia.org/wiki/Costas_array): the sequence [3, 1, 4, 0, 6, 5, 2]. One copy at the start, one in the middle, one at the end. These 21 sync symbols carry no payload data. Their sole job is to be unambiguously findable by a receiver that doesn't know exactly when the transmitter started.
Total: 7 + 29 + 7 + 29 + 7 = 79 symbols. At 160 ms per symbol, that's 12.64 seconds of audio. The numbers are not coincidences; the symbol rate (6.25 baud) and the tone spacing (6.25 Hz) are deliberately equal so the eight tones are mutually orthogonal at the symbol boundaries. This means a matched filter looking at any one tone gets zero contribution from the other seven, which is the cleanest possible situation for a demodulator working at sub-noise-floor SNRs.
Three [Costas arrays](https://en.wikipedia.org/wiki/Costas_array) get spliced in (start, middle, end), the sequence [3, 1, 4, 0, 6, 5, 2] each time. 7 + 29 + 7 + 29 + 7 = 79 symbols, 160 ms each, 12.64 seconds total. The symbol rate and tone spacing are both 6.25, which is not an accident: it makes the eight tones orthogonal at symbol boundaries, which is the cleanest possible situation for the demodulator on the other end.
<div id="scene-ft8-pipeline"></div>
The same pipeline that produced the spectrogram above, with bit counts laid out so the geometry tells the story. "CQ W5ISP EM13" packs to 77 bits, gains 14 bits of CRC, gets doubled to 174 bits by LDPC, packs into 58 octal symbols, and finally has three Costas arrays spliced in to make the 79-symbol on-air stream. The bottom row is the same spectrogram as above, miniaturized.
Same pipeline, drawn so the bit counts tell the story. 77 in, 91 after CRC, 174 after LDPC, 58 octal symbols, 79 on-air after Costas insertion. The miniaturized spectrogram at the bottom is what actually goes out.
## t = 0 ms: the carrier appears
The transmission is timed to start exactly 500 ms after the start of an even or odd UTC 15-second slot, depending on which slot you've chosen. The first 160 ms contains the first sync symbol, but with one twist: the PA's drive level ramps up from zero over a small fraction of that first symbol to avoid splattering keyclicks across the band. Aggressive ramping makes nearby operators love you. Lazy ramping makes them complain about your signal on the next 25 Hz bin over.
A receiver listening on the right slot sees the first tone arrive, and starts the long process of figuring out where exactly in time and frequency it landed.
The transmission starts exactly 500 ms into a 15-second UTC slot. PA drive ramps from zero over the first few ms of symbol one, because the alternative is splattering keyclicks across the band and earning the eternal contempt of the operator on the next 25 Hz bin over.
## t = 0 ms to 1.12 s: first Costas array
Seven symbols of [3, 1, 4, 0, 6, 5, 2]. To a spectrogram, this looks like seven brightly lit pixels in a staircase pattern that doesn't repeat under any combination of time or frequency shifts. Costas arrays are mathematical objects with the specific property that their auto-correlation has exactly one peak, and that peak is sharp.
The receiver doesn't know your transmit frequency to within 6.25 Hz. It doesn't know your start time to within 160 ms. The Costas array exists to fix both at once. The receiver slides a Costas template across the entire spectrogram in both axes, computing a correlation score at every (frequency, time) offset. There will be exactly one place where the score is high. That's your signal. Within milliseconds the receiver has nailed down where to start counting symbols and what frequency to mix down to.
The reason Costas arrays beat simpler sync patterns is that the mode is meant to work when the signal is well below the noise floor. A repetitive sync sequence (like a tone burst) would be impossible to find when buried in noise because the auto-correlation has multiple peaks at every offset corresponding to the period. The Costas property guarantees a single, unambiguous peak even in heavy noise, which is the only way the rest of the protocol gets to do its job.
Seven symbols of [3, 1, 4, 0, 6, 5, 2], chosen for the property that no time-or-frequency shift of the pattern looks like itself. The receiver, which does not know your transmit frequency to within 6.25 Hz or your start time to within 160 ms, slides this template across the spectrogram in both axes. There is exactly one place where the score spikes, and that place is your signal. Sync done.
## t = 1.12 to 5.76 s: first data block
29 symbols, each one of eight tones, each carrying 3 bits, each lasting 160 ms. To a spectrogram, this looks like a sequence of 29 horizontal bars hopping seemingly at random between the eight available tone slots. To the receiver, this is the first half of the LDPC(174,91) codeword: 87 bits worth of "your message, plus parity, plus more parity, all interleaved."
The bits are not in obvious payload order. The LDPC encoder produces a codeword where the original 91 message bits are mixed with 83 parity bits in a pattern designed to maximize how spread out the parity protection is. Even if a contiguous chunk of the data is destroyed by a noise burst, the remaining bits carry enough redundancy to reconstruct the original. The encoder's matrix is sparse (the "low density" part of LDPC), which is what makes the decoder fast enough to run in software on a typical computer.
While this block transmits, the receiver is already demodulating it. Each 160 ms symbol gets fed through a bank of eight matched filters, one per tone, and the receiver computes a soft-decision likelihood for each of the three bits the symbol represents. Hard decisions (just picking the loudest tone) lose 2-3 dB of decoding margin. FT8 always keeps the soft values around because LDPC's whole superpower is using probabilistic information from the channel.
29 symbols carrying 87 bits of LDPC codeword, hopping between the eight tone slots in a pattern that looks random because, post-LDPC, it is. The receiver runs eight matched filters in parallel and keeps soft confidence values for every bit, because giving the decoder hard yes/no decisions throws away 2-3 dB of margin and FT8 has zero dB to spare.
## t = 5.76 to 6.88 s: middle Costas array
Another seven symbols of [3, 1, 4, 0, 6, 5, 2]. The receiver already found the signal during the first Costas, so why repeat it? Two reasons.
First, the second Costas lets the receiver re-estimate timing and frequency drift. If the transmitter's clock is a few ppm off, or yours is, or there's a slow Doppler shift on the path because the ionosphere is doing one of its slower acrobatic routines, the symbols later in the transmission will arrive a bit early or late. The middle Costas gives the demodulator a fresh fix.
Second, in heavy noise, the first Costas might have been noise-destroyed and missed entirely. The middle one offers a second chance for initial sync. Robustness through redundancy.
The receiver's symbol clock recovery is now locked in for the next half of the transmission.
Same seven symbols again. The receiver uses this for two things: a fresh fix on timing and frequency in case the ionosphere is being twitchy, and a second shot at initial sync if the first Costas got eaten by a noise burst.
## t = 6.88 to 11.52 s: second data block
Another 29 symbols, the remaining 87 bits of the LDPC codeword. The pattern looks just as random as the first block because, statistically, it is.
Here's the elegant part: by the time the second block finishes, the receiver has 174 noisy bit estimates, each with a soft confidence value. It hands these to the LDPC decoder, which begins running its [belief propagation](https://en.wikipedia.org/wiki/Belief_propagation) algorithm even before the transmission is over. Belief propagation works by passing messages between bit nodes and parity-check nodes in the LDPC graph, iteratively refining each bit's estimate based on what its parity-check neighbors think it should be.
After typically 20 to 50 iterations, the decoder either converges on a valid codeword (one where every parity check is satisfied) or gives up. If it converges, you have your 91 candidate bits: 77 of message, 14 of CRC.
The other 87 bits. Once the second block lands, the receiver has 174 soft-valued bit estimates and starts running [belief propagation](https://en.wikipedia.org/wiki/Belief_propagation) on the LDPC graph. After 20 to 50 iterations it either converges on a valid codeword or it doesn't. If it does, you have 91 candidate bits.
## t = 11.52 to 12.64 s: final Costas array, then silence
Seven more sync symbols, then the PA ramps down to zero over the last few milliseconds. The transmission is over.
The receiver doesn't strictly need the final Costas. By now, sync is locked in. But it's there for symmetric design and as a final cross-check, and it lets the receiver attempt forensic recovery of transmissions where the first one or two Costas arrays got blown away by interference. With three Costas arrays distributed across the message, FT8 can typically still decode if any one of them survives and at least one of the data blocks is mostly intact.
Seven sync symbols, PA ramps to zero, transmission ends. By now sync is long since locked, but the third Costas is there for symmetry and so the decoder can still recover the message if either of the other two Costas blocks got obliterated.
## t > 12.64 s: the receiver does the rest
The transmission is done, but the work isn't. The receiver runs the CRC check on the decoded 91 bits. If the 14-bit CRC matches what the source said it should be, the message is real. If it doesn't, the LDPC decoder's apparent success was a false positive (which happens, rarely, when the noise happens to land on a valid codeword that isn't the one transmitted) and the message gets thrown out.
CRC check on the 91 bits. If it passes, the source decoder reads the i3 message-type bits, unpacks the matching schema, looks up callsign hashes against its recently-heard table, expands the grid, and "CQ W5ISP EM13" pops up in WSJT-X with a frequency offset and an SNR. If the CRC fails, the decoder's apparent success was a false alarm (the noise happened to look like some other valid codeword), and the result gets dropped on the floor.
If the CRC checks out, the source decoder reverses the message-type encoding: pulls the three i3 bits, decides what kind of message this is, and unpacks the corresponding fields. Callsign hashes get looked up in the receiver's running database of recently-heard callsigns. Grids get expanded back to four characters. The result is a string like "CQ W5ISP EM13" that gets dropped into the WSJT-X main window with a frequency offset and an SNR estimate.
The whole decode, from end-of-transmission to displayed message, typically takes a fraction of a second on modern hardware. WSJT-X scans the entire 2500 Hz audio passband for FT8 signals at every 6.25 Hz offset, in parallel, and surfaces every one that decodes successfully. On a busy 20-meter evening at solar maximum, a single 15-second cycle might produce 30 or 40 decoded messages from stations all over the planet.
## Why this works at -24 dB SNR
The math, done bluntly: FT8 transmits 174 bits of LDPC codeword in 12.64 seconds across 50 Hz of bandwidth. Channel capacity in that bandwidth at -24 dB SNR (using Shannon's formula) is around 6 bits/sec, or 76 bits in 12.64 seconds. FT8 sends 91 effective bits (77 message + 14 CRC) of useful information in that same time, slightly above Shannon's theoretical limit for the assumed AWGN channel.
It cheats slightly by counting the Costas overhead as separate from "data" and by relying on the receiver's coherent detection of orthogonal tones, but the bottom line is that FT8 is operating very close to the absolute information-theoretic limit of what's possible in that channel. There is not much room left for improvement without sacrificing something else (longer transmissions, narrower bandwidth, simpler messages).
The dial-up modem worked for the same reason any modulation scheme works: it found a clever way to push data through a channel with known constraints. The constraints there were 4 kHz of phone-line bandwidth, ~30 dB SNR, and analog hand-off equipment. The constraints for FT8 are 50 Hz of HF bandwidth, sometimes -24 dB SNR, and a propagation channel that occasionally swallows entire seconds whole. Different problem, similar elegance, both worth pulling apart to see what each beep is for.
WSJT-X does this for every 6.25 Hz bin in a 2500 Hz passband, in parallel, every 15 seconds. On a good 20m evening, one slot can produce 30 or 40 decodes from stations scattered across the planet. The dial-up modem had it easy: 4 kHz of clean phone line and 30 dB SNR. FT8 has 50 Hz of HF and sometimes -24 dB. Same idea, harder problem, weirder noises.
</div>