Skip to content

generate_signal.py

Lives on: the Hackerman Pi, at ~/Desktop/hackrf project/generate_signal.py (see Hackerman for how to reach that Pi).

What it does

Generates a synthetic radio signal and writes it to signal.iq — a raw IQ (in-phase / quadrature) sample file in the format HackRF expects for transmission. start_tx.sh then takes this file and feeds it to the HackRF hardware.

import numpy as np

sample_rate = 2_000_000   # 2 MHz
duration = 10.0             # seconds
tone_freq = 100_000        # 100 kHz tone, offset from center freq

t = np.arange(0, duration, 1 / sample_rate)
iq = np.exp(1j * 2 * np.pi * tone_freq * t)

i_samples = (np.real(iq) * 127).astype(np.int8)
q_samples = (np.imag(iq) * 127).astype(np.int8)

interleaved = np.empty(i_samples.size * 2, dtype=np.int8)
interleaved[0::2] = i_samples
interleaved[1::2] = q_samples

interleaved.tofile("signal.iq")
print("Wrote signal.iq")

Walkthrough

Parameters (top of file):

  • sample_rate = 2_000_000 — 2 million samples per second. This has to match whatever sample rate start_tx.sh tells the HackRF to use, or the transmitted tone will land at the wrong frequency.
  • duration = 10.0 — the signal is 10 seconds long. At 2 MHz, that's 20 million samples.
  • tone_freq = 100_000 — the tone sits 100 kHz above whatever center frequency the HackRF is tuned to (e.g. if the HackRF transmits at 915 MHz center, the actual signal shows up at 915.1 MHz).

Building the tone:

t = np.arange(0, duration, 1 / sample_rate)
iq = np.exp(1j * 2 * np.pi * tone_freq * t)

t is a timestamp for every sample (0, 1/2,000,000, 2/2,000,000, ...). np.exp(1j * 2π * freq * t) is the standard way to represent a single-frequency tone as a complex exponential — this is what "IQ" means: each sample is a complex number whose real part is the "in-phase" (I) component and imaginary part is the "quadrature" (Q) component. Radios use this representation because it lets you shift a signal to any frequency just by multiplying by a complex tone, without needing separate math for amplitude and phase.

Converting to HackRF's format:

i_samples = (np.real(iq) * 127).astype(np.int8)
q_samples = (np.imag(iq) * 127).astype(np.int8)

The math above produces floating-point numbers between -1 and 1. HackRF expects 8-bit signed integers (range -128 to 127), so each sample is scaled by 127 and cast to int8.

Interleaving:

interleaved = np.empty(i_samples.size * 2, dtype=np.int8)
interleaved[0::2] = i_samples
interleaved[1::2] = q_samples

HackRF reads IQ files as alternating I, Q, I, Q, ... bytes — not as two separate blocks. [0::2] and [1::2] are Python's "every other element starting at index 0 (or 1)" slices, which is how the two arrays get zipped together into that alternating pattern.

Output:

interleaved.tofile("signal.iq")

Writes the raw bytes straight to signal.iq in the same folder, ready for start_tx.sh to transmit.