Subsystem Search

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"
…
Successfully installed … substation-…
$ substation --init
Created /home/keanu/radio/config.yaml
This is the full default configuration, fully commented. Edit it to suit
your hardware and bands, then start a scan, e.g. `substation --band amateur_2m`.
Any setting you delete falls back to the built-in default.
$ 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
Substation antenna calculator
Band: amateur_2m_simplex
Frequency range: 145.200 - 145.562 MHz (centre 145.381 MHz, span 362 kHz / 0.25%)
Wavelength at centre: 2.06 m

Antenna lengths (calculated for band centre):

  Half-wave dipole         total 98.0 cm   each leg 49.0 cm
  Quarter-wave vertical    49.0 cm
  5/8-wave vertical        1.29 m
  Full-wave loop           2.00 m  (perimeter)

Practical notes (receive only):

  Full-size antennas at this frequency are practical (49.0 cm quarter-wave).
  Good options:

    - Quarter-wave ground plane: simple to build from wire or a
      telescopic whip cut to length, with 3-4 radials.
    - Discone: wideband, covers multiple VHF/UHF bands at once.
    - Collinear vertical: higher gain if you want to focus on
      one band (e.g. airband, marine, 2m amateur).

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:

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
Substation antenna calculator
Frequency: 435.000 MHz
Wavelength: 0.69 m / 68.9 cm

Antenna lengths:

  Half-wave dipole         total 32.7 cm   each leg 16.4 cm
  Quarter-wave vertical    16.4 cm
  5/8-wave vertical        43.1 cm
  Full-wave loop           66.9 cm  (perimeter)

Practical notes (receive only):

  Antennas at this frequency are small (16.4 cm quarter-wave).
  Almost any antenna works well:

    - The whip antenna included with most SDR dongles is adequate.
    - A quarter-wave ground plane takes minutes to build from wire.
    - Discone: covers the full UHF range and beyond.

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
Substation antenna calculator
Band: hf_amateur_80m
Frequency range: 3.600 - 3.800 MHz (centre 3.700 MHz, span 200 kHz / 5.41%)
Wavelength at centre: 81.02 m

Antenna lengths (calculated for band centre):

  Half-wave dipole         total 38.49 m   each leg 19.24 m
  Quarter-wave vertical    19.24 m
  5/8-wave vertical        50.64 m
  Full-wave loop           78.59 m  (perimeter)

Practical notes (receive only):

  A full-size antenna at this frequency is large (19.24 m quarter-wave).
  For receive-only SDR scanning, you do NOT need a resonant antenna.
  Good alternatives:

    - Random wire: any length of wire, ideally outdoors and as
      long as you can manage (>19.24 m helps).  Feed via a 9:1 balun.
    - Active magnetic loop: compact (< 1m diameter), good for
      indoor use, rejects local noise.  Needs a preamp (built in
      on commercial units like the MLA-30+).
    - Shortened loaded vertical: mobile CB/HF whips (1-2m) with
      loading coils.  Less efficient but very practical.
    - Telescopic whip: basic but works for strong local signals.

NOTE: This band spans 5.4% of its centre frequency.  A single dipole
optimised for the centre will work acceptably across roughly:
  centre ± ~2%  →  3.626 - 3.774 MHz  (~4% useful bandwidth)
The full band is wider than this. If you want both the band edges and
the middle, consider:
  - cutting the dipole for the lower edge (longer = covers low freqs better)
  - using a tuner / matching network
  - using a wideband antenna (random wire, magnetic loop, discone)

Lengths at the band edges for comparison:
  3.600 MHz: dipole 39.56 m total
  3.800 MHz: dipole 37.47 m total

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.