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 ratestart_tx.shtells 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.