TERN is a narrowband digital mode for weak-signal HF communication, with a reference implementation in Fortran 2018. It transmits 77-bit messages in 50 Hz (Mode A, 27.2 s frame) or 25 Hz (Mode B, 54.4 s frame) and is designed around the statistics of the ionospheric channel rather than around a static AWGN assumption.
The physical layer combines:
- Orthogonal 16-FSK signaling with long symbols matched to the channel coherence time, detected without any phase-lock loop.
- Block-noncoherent detection with a Gauss-Markov channel smoother: the fading gain is modeled as an AR(1) process fitted to the Watterson autocorrelation, its posterior is computed by Kalman/RTS smoothing over pilot anchors, and per-tone likelihoods marginalize over that posterior. Detection interpolates continuously between coherent (near pilots) and square-law noncoherent (far from them), with decision-directed turbo iterations after the first decode attempt.
- A single CRC-aided polar codeword spanning the whole frame (N=256, K=91, CA-SCL list 8, Gaussian-approximation construction), harvesting the full time diversity of the frame.
- Three length-7 Costas arrays for unambiguous time-frequency acquisition and as coherent anchors of the channel smoother.
- An envelope soft limiter in front of the detector, statistically inert under Gaussian noise and bounding the influence of impulsive atmospheric noise (Middleton Class A model).
- Cross-frame combining: failed receptions retain their tone
log-likelihoods, and repeated transmissions are decoded from the
accumulated statistic, so a repeat protocol wastes no received energy
(
--repeatsin simulation,--stackin demodulation). - Constant-envelope GFSK synthesis (BT = 2), insensitive to PA nonlinearity.
The repository also contains the channel instrument used to qualify the
mode: an ITU-R F.1487 Watterson simulator with statistical self-validation,
exposed both to the internal Monte Carlo machinery and, through the
tern channel command, to captures of any external mode, so cross-mode
comparisons can be made under identical channel realizations.
All SNR figures are referenced to a 2500 Hz noise bandwidth (the WSJT-X
convention). Measured decode thresholds and the full calibration tables are
maintained in docs/VALIDATION.md; the design theory is in
docs/DESIGN.md. A complete reference manuscript — system model,
receiver derivations, validation methodology, results, and analysis,
with figures generated from the campaign data — is provided in
paper/tern.pdf (paper/tern.tex); it has not undergone journal peer
review, and every claim in it is reproducible from this repository.
tern/
app/tern_cli.f90 command-line interface
src/
core/ kinds, mode geometry, frame and code parameters
util/ RNG, bit/CRC utilities, cf32 and WAV audio I/O
codec/ polar encoder and CRC-aided SCL decoder
phy/ frame mapping, GFSK synthesis, receiver
channel/ ITU-R F.1487 Watterson model, AWGN, impairments
api/ high-level modem facade, frame simulator, scanner
test/ statistical channel validation and regression suite
docs/ design and validation documents
research/ Monte Carlo campaign tooling and results
gfortranwith Fortran 2018 supportfpm(Fortran Package Manager)make(optional convenience wrapper)python3(campaign driver only)
No external libraries are required.
make build # fpm build --profile release
make test # full suite: channel statistics, codec, audio, loopback
make install # install to ~/.local/binWaveform files are selected by extension: .cf32 is the 200 Hz complex
baseband (interleaved float32 I/Q), .wav is mono 16-bit PCM audio at
12 kHz with the signal at a configurable offset (1500 Hz by default) —
ready for an SSB transceiver audio interface.
# Mode geometry as JSON
tern info --mode A
# Modulate a message; WAV output is transmit-ready audio
tern mod --bits 1011001... --output frame.wav
# Demodulate a capture (cf32 or WAV)
tern demod --input capture.wav --f-search 20
# Decode every TERN signal in a busy audio passband
tern scan --input band_capture.wav --min-hz 200 --max-hz 2700
# Pass any capture (including another mode's WAV) through the channel
tern channel --input ft8.wav --output ft8_faded.wav \
--snr-db -20 --itu-profile mid_disturbed --seed 7
# Full TX -> ITU channel -> RX simulation, one JSON record per frame
tern simulate --snr-db -18 --itu-profile mid_disturbed --seed 7 \
--cfo-hz 1.1 --clock-ppm 20tern simulate and tern demod exit 0 on CRC success and 2 on failure,
so shell-level sweeps need no JSON parsing. The campaign driver in
research/monte_carlo.py produces threshold curves with Wilson confidence
intervals and a Markdown report from the same binary.
Every simulation is deterministic given (code revision, seed). The RNG is a
seed-scrambled xorshift64* whose 64-bit modular arithmetic is implemented
with bit-model operations only, avoiding signed-overflow undefined behavior,
so streams are identical across compilers and optimization levels. The
Watterson channel is validated statistically by test/test_watterson.f90
(Rayleigh envelope KS test, Doppler spectral width, autocorrelation shape,
tap independence, unit power gain) on every test run, before any modem
figure is trusted.
GPL-3.0-or-later. See LICENSE.