An antenna for your band
By the end of this chapter, you know how long an antenna should be for the band you scan, and what to make it from.
The folder so far
The radio folder as What a recording holds left it: Substation installed in an environment inside it, switched on, its defaults kept as defaults.yaml, your band in config.yaml with settings for each receiver you own, and four test signals made.
mkdir radio
cd radio
python3 -m venv .venv
source .venv/bin/activate
pip install "substation==0.8.2"
substation --init
mv config.yaml defaults.yaml
bands:
amateur_2m_simplex:
type: NFM
reception_class: general
freq_start: 145.2e+6
freq_end: 145.5625e+6
channel_spacing: 12.5e+3
sample_rate: 2.4e+6
snr_threshold_db: 4.5
activation_variance_db: 3
device_overrides:
hackrf:
sample_rate: 2.0e+6
sdr_gain_db: 36
snr_threshold_db: 6
airspy:
sample_rate: 2.5e+6
sdr_gain_elements:
LNA: 10
MIX: 5
VGA: 12
snr_threshold_db: 6
airspyhf:
sample_rate: 0.912e+6
snr_threshold_db: 6
# Makes test-signal.wav: eight seconds of the 2m band as a receiver hands it over,
# hiss with a transmission in the middle, for Substation to scan in place of a receiver.
import wave
import numpy
# The frequency the transmission is sent on, in hertz, and how many seconds it lasts.
frequency = 145_500_000
length = 3
# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000
# Two seconds of hiss first, while Substation measures it, and three after.
start = 2
seconds = start + length + 3
# The hiss is random, but the same every time the script runs.
noise = numpy.random.default_rng(1)
with wave.open("test-signal.wav", "wb") as file:
# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
file.setnchannels(2)
file.setsampwidth(2)
file.setframerate(rate)
# A second at a time, so the script needs little memory.
for second in range(seconds):
# The moment of each IQ sample, in seconds.
t = second + numpy.arange(rate) / rate
# The receiver's own hiss.
hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01
# A buzz like a voice: overtones of a low note, each softer than the one below.
voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))
# Louder and softer six times a second, like syllables.
loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))
# FM: the sound swings the transmission's frequency to either side of its own.
phase = 2 * numpy.pi * (frequency - centre) * t + loudness * voice
# The transmission, only while it is on, added to the hiss.
on = (t >= start) & (t < start + length)
iq = hiss + numpy.where(on, 0.3 * numpy.exp(1j * phase), 0)
# Written as pairs of 16-bit numbers, I then Q.
pairs = numpy.stack([iq.real, iq.imag], axis=1)
file.writeframes((pairs * 32767).astype("<i2").tobytes())
python make_signal.py
# Makes test-noise.wav: six seconds of the 2m band as a receiver hands it over,
# with three things in it strong enough to cross a threshold: a steady noise,
# a burst too short to be a transmission, and a transmission.
import wave
import numpy
# The frequency of each, in hertz.
steady = 145_300_000
burst = 145_400_000
transmission = 145_500_000
# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000
# Six seconds in all.
seconds = 6
# The hiss and the noise are random, but the same every time the script runs.
noise = numpy.random.default_rng(2)
# How far the steady noise has wandered, carried from one second to the next.
drift = 0.0
with wave.open("test-noise.wav", "wb") as file:
# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
file.setnchannels(2)
file.setsampwidth(2)
file.setframerate(rate)
# A second at a time, so the script needs little memory.
for second in range(seconds):
# The moment of each IQ sample, in seconds.
t = second + numpy.arange(rate) / rate
# The receiver's own hiss.
hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01
# The steady noise: a signal whose frequency wanders at random, carrying nothing.
wander = drift + numpy.cumsum(noise.standard_normal(rate)) * 0.02
drift = wander[-1]
steady_noise = 0.03 * numpy.exp(1j * (2 * numpy.pi * (steady - centre) * t + wander))
# The test signal's buzz, rising and falling, for the burst and the transmission.
voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))
loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))
# The burst, a twentieth of a second long, three seconds in.
bursting = (t >= 3) & (t < 3.05)
brief = numpy.where(bursting, 0.3 * numpy.exp(1j * (2 * numpy.pi * (burst - centre) * t + loudness * voice)), 0)
# The transmission, from two seconds in to four.
talking = (t >= 2) & (t < 4)
sent = numpy.where(talking, 0.3 * numpy.exp(1j * (2 * numpy.pi * (transmission - centre) * t + loudness * voice)), 0)
# All of it together, written as pairs of 16-bit numbers, I then Q.
iq = hiss + steady_noise + brief + sent
pairs = numpy.stack([iq.real, iq.imag], axis=1)
file.writeframes((pairs * 32767).astype("<i2").tobytes())
python make_noise.py
# Makes test-loud.wav: eight seconds of the 2m band as a receiver hands it over,
# with a transmission so strong it overloads the receiver, as one sent close by
# would with the gain turned up too far.
import wave
import numpy
# The frequency the transmission is sent on, in hertz, and how many seconds it lasts.
frequency = 145_500_000
length = 3
# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000
# Three seconds of hiss first, while Substation measures it, and two after.
start = 3
seconds = start + length + 2
# The hiss is random, but the same every time the script runs.
noise = numpy.random.default_rng(1)
with wave.open("test-loud.wav", "wb") as file:
# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
file.setnchannels(2)
file.setsampwidth(2)
file.setframerate(rate)
# A second at a time, so the script needs little memory.
for second in range(seconds):
# The moment of each IQ sample, in seconds.
t = second + numpy.arange(rate) / rate
# The receiver's own hiss.
hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01
# A buzz like a voice, louder and softer like syllables, as before.
voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))
loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))
phase = 2 * numpy.pi * (frequency - centre) * t + loudness * voice
# The transmission, ten times as strong as the one in test-signal.wav.
on = (t >= start) & (t < start + length)
iq = hiss + numpy.where(on, 3 * numpy.exp(1j * phase), 0)
# A receiver can hand over nothing stronger than its largest number,
# so everything stronger is cut off there, as an overloaded one does.
pairs = numpy.clip(numpy.stack([iq.real, iq.imag], axis=1), -1, 1)
file.writeframes((pairs * 32767).astype("<i2").tobytes())
python make_loud.py
# Makes test-tone.wav: nine seconds of the 2m band as a receiver hands it over,
# with a transmission like the one in test-signal.wav, carrying a low tone under
# its voice, as many radios send.
import wave
import numpy
# The frequency the transmission is sent on, in hertz, and how many seconds it lasts.
frequency = 145_500_000
length = 3
# The tone under the voice, in hertz.
tone = 88.5
# The frequency the recording is centred on, and how many IQ samples it holds
# a second, as a receiver set for the 2m band hands them over.
centre = 145_000_000
rate = 2_400_000
# Three and a half seconds of hiss first, so that Substation has measured it
# before the transmission starts, and nine seconds in all.
start = 3.5
seconds = 9
# The hiss is random, but the same every time the script runs.
noise = numpy.random.default_rng(3)
with wave.open("test-tone.wav", "wb") as file:
# Each IQ sample as two 16-bit numbers, I and Q, held as two audio channels.
file.setnchannels(2)
file.setsampwidth(2)
file.setframerate(rate)
# A second at a time, so the script needs little memory.
for second in range(seconds):
# The moment of each IQ sample, in seconds.
t = second + numpy.arange(rate) / rate
# The receiver's own hiss.
hiss = (noise.standard_normal(rate) + 1j * noise.standard_normal(rate)) * 0.01
# A buzz like a voice: overtones of a low note, each softer than the one below.
voice = sum(5.3 / n ** 2 * numpy.sin(2 * numpy.pi * 150 * n * t) for n in range(2, 7))
# Louder and softer six times a second, like syllables.
loudness = numpy.abs(numpy.sin(2 * numpy.pi * 3 * t))
# The tone, as steady as the voice is not.
under = 4 * numpy.sin(2 * numpy.pi * tone * t)
# FM: the sound swings the transmission's frequency to either side of its own.
phase = 2 * numpy.pi * (frequency - centre) * t + loudness * voice + under
# The transmission, only while it is on, added to the hiss.
on = (t >= start) & (t < start + length)
iq = hiss + numpy.where(on, 0.3 * numpy.exp(1j * phase), 0)
# Written as pairs of 16-bit numbers, I then Q.
pairs = numpy.stack([iq.real, iq.imag], axis=1)
file.writeframes((pairs * 32767).astype("<i2").tobytes())
python make_tone.py
Why the length matters
A radio wave travels at the speed of light, about 300 million metres a second, and its wavelength is how far it travels in one cycle. Divide 300 by a frequency in megahertz and you have its wavelength in metres: about two metres around 145 MHz. That is where the 2m band's name comes from, since amateur radio bands are named for their wavelength.
An antenna hears best the frequencies whose wavelength suits its length: about half a wavelength for a dipole, two arms in a line with the cable joined between them, and about a quarter for a single upright rod. It hears other frequencies too, less well.
For your band
Substation installs a second command, substation-antenna, which works out the lengths from a band in your configuration. Give it your band's name with --band:
substation-antenna --band amateur_2m_simplex
It works from the middle of the band, 145.381 MHz, whose wavelength is 2.06 m, and says how wide the band is beside it: 362 kHz, a quarter of one per cent of its centre, so one length suits the whole band. Then it gives four kinds of antenna:
- A half-wave dipole: two arms in a line, 49.0 cm each, with the cable joined in the middle, 98.0 cm from tip to tip.
- A quarter-wave vertical: one rod, 49.0 cm long, standing upright over a ground plane, a sheet of metal or three or four wires spreading out from its base.
- A 5/8-wave vertical: a longer rod, five eighths of a wavelength.
- A full-wave loop: a loop of wire one wavelength around.
The dipole, the quarter-wave vertical and the loop come out a little shorter than half, a quarter and one whole wavelength, because a wave travels a little more slowly along a real wire than through open air, and a wire's ends make it act a little longer than it is. The notes after them suggest antennas that suit this frequency.
Setting up a dipole kit
The dipole kit sold with the RTL-SDR Blog V3, which Your first receiver described, has two long telescopic arms, which extend from 23 cm to 1 m, and two short ones, from 5 to 13 cm. For your band, screw the long ones into its base and pull each out to 47 cm: its maker says to count the 2 cm or so of metal inside the base too, which makes each arm the 49.0 cm a leg should be. Both arms should be the same length.
Stand the arms up and down, rather than side to side. Most transmissions on the ground are vertically polarised, sent from upright antennas, its maker says, and an upright antenna hears them best. Nothing in this section is checked when this site is built, since it needs the kit in your hands.
For a frequency
--freq takes a frequency in hertz instead of a band, for a band you have not defined, or a frequency you heard a transmission on. The 70cm amateur band, from 430 to 440 MHz, is one Substation ships no band for. For the middle of it:
substation-antenna --freq 435e6
At these frequencies an antenna is short: 16.4 cm a leg for a dipole. Its notes say that almost any antenna works, and its wavelength, 68.9 cm, is where the 70cm band's name comes from.
A wide band
A band can be too wide for one length. An antenna cut for one frequency works well a little either side of it, and Substation's hf_amateur_80m, from 3.6 to 3.8 MHz, part of the 80m amateur band, spans more than one length suits:
substation-antenna --band hf_amateur_80m
At 3.7 MHz a dipole is over 38 m long, which few people have room for, and its notes say a receiver does not need one: a long wire, as long as you can manage, or one of the other antennas it names. Where a dipole is wanted, its last lines compare the band's two edges, and give three ways round its width: cut it for the lower edge, use a tuner, which matches an antenna to frequencies its length does not suit, or use an antenna made to hear a wide range.
--list lists the bands it can work out, which are the bands substation --list-bands shows, your own among them. The command-line reference gives every option.
The next chapter is for programmers: using Substation inside a Python program of your own. Nothing after it needs it, so skip it if you do not write Python.