Chapter: 5 (Week 6) Duration: 4 hr Substrate: GNU Radio + Python; ANTSDR E200 optional for live capture Points: 10
Overview
Implement two LoRa demodulators: a straightforward FIR correlate-then-FFT, and a polyphase filter bank variant. Compare performance against a known IQ capture (virtual path: lab5-lora-868.iq). This lab directly applies the Lyons Ch 7 filter-design-criterion framing.
Part 1: LoRa Signal Analysis (45 min)
Load the provided IQ capture lab5-lora-868.iq (centre freq: 868.1 MHz, sample rate: 1 MSPS, SF=7, BW=125 kHz):
import numpy as np
import matplotlib.pyplot as plt
# Load IQ capture
fs = 1e6 # 1 MSPS
samples = np.fromfile("lab5-lora-868.iq", dtype=np.complex64)
# Compute and display spectrogram
fig, ax = plt.subplots(figsize=(14, 5))
ax.specgram(samples[:50000], NFFT=256, Fs=fs, noverlap=192, cmap='viridis')
ax.set_xlabel('Time (s)'); ax.set_ylabel('Frequency (Hz)')
ax.set_title('LoRa IQ capture: spectrogram'); plt.colorbar(ax.get_images()[0], ax=ax)
plt.tight_layout(); plt.show()
# Instantaneous frequency
inst_phase = np.unwrap(np.angle(samples[:10000]))
inst_freq = np.diff(inst_phase) * (fs / (2 * np.pi))
plt.figure(figsize=(14, 3))
plt.plot(inst_freq)
plt.axhline(0, c='k', linewidth=0.5)
plt.xlabel('Sample'); plt.ylabel('Inst. frequency (Hz)')
plt.title('LoRa chirp: instantaneous frequency'); plt.grid(True); plt.show()
From the spectrogram and instantaneous frequency plot, confirm:
- Chirp direction (up-chirp or down-chirp?)
- Approximate chirp duration (number of samples from one sweep boundary to the next)
- Approximate bandwidth (frequency range of each chirp)
Part 2: Python LoRa Demodulator (Straightforward FIR) (60 min)
import numpy as np
SF = 7
BW = 125e3
FS = 1e6 # sample rate of the capture
M = 2**SF # 128 symbols per chirp
def lora_reference_downchirp(sf, bw, fs):
"""Generate the reference down-chirp for dechirping."""
M = 2**sf
T_s = M / bw
N = int(T_s * fs)
t = np.arange(N) / fs
# Down-chirp: frequency decreases from +bw/2 to -bw/2
f_t = bw/2 - bw * t / T_s
phase = 2 * np.pi * np.cumsum(f_t) / fs
return np.exp(1j * phase)
def demod_lora_symbol_fir(rx, downchirp, sf, bw, fs):
"""
Demodulate one LoRa symbol using correlate-then-FFT.
Returns the symbol value (0 to 2^sf - 1).
"""
N = len(downchirp)
dechirped = rx[:N] * np.conj(downchirp)
# FFT; find peak bin
spectrum = np.abs(np.fft.fft(dechirped, 2**sf))**2
return int(np.argmax(spectrum))
# Generate reference down-chirp
downchirp = lora_reference_downchirp(SF, BW, FS)
samples_per_symbol = len(downchirp)
print(f"SF={SF}, BW={BW:.0f} Hz, FS={FS:.0f} Hz")
print(f"Symbol duration: {samples_per_symbol/FS*1000:.3f} ms")
print(f"Samples per symbol: {samples_per_symbol}")
# Demodulate first 20 symbols (skip preamble estimation for now)
samples = np.fromfile("lab5-lora-868.iq", dtype=np.complex64)
# Rough preamble skip: LoRa preamble is 8 identical up-chirps
# Each preamble chirp = samples_per_symbol samples
preamble_samples = 8 * samples_per_symbol
payload_start = preamble_samples + 2 * samples_per_symbol # preamble + 2 sync words
decoded = []
for i in range(20):
start = payload_start + i * samples_per_symbol
if start + samples_per_symbol > len(samples):
break
sym = demod_lora_symbol_fir(samples[start:], downchirp, SF, BW, FS)
decoded.append(sym)
print(f" Symbol {i:2d}: {sym:4d} ({sym:08b})")
print(f"\nDecoded {len(decoded)} symbols")
Record the first 20 symbol values from the capture.
Part 3: GNU Radio LoRa Demodulator (Polyphase Variant) (90 min)
Install gr-lora-sdr:
# Ubuntu 24.04 / Kali
pip install pybind11
git clone https://github.com/tapparelj/gr-lora-sdr.git
cd gr-lora-sdr && mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j4 && sudo make install && sudo ldconfig
Open GNU Radio Companion and build the following flowgraph:
[File Source: lab5-lora-868.iq]
|
[Throttle] (sample_rate=1e6)
|
[gr-lora-sdr: Lora Receiver]
(SF=7, BW=125000, CR=4/5, has_crc=True, implicit_header=False,
soft_decoding=False, center_freq=868.1e6, samp_rate=1e6)
|
[Message Debug]
Run the flowgraph. The Message Debug block should print decoded LoRa frames.
Performance comparison: Run both demodulators on the same IQ archive. Compare:
- First 10 symbols decoded: do they agree?
- Frame decode success: does the polyphase GRC demodulator decode the CRC-valid frame? Does your Python FIR demodulator?
Part 4: Lyons Ch 7 Analysis (30 min)
Write a 1-page analysis applying the Lyons Ch 7 design-criterion framing to LoRa demodulation:
-
What is the primary performance criterion for the LoRa demodulator's filter stage?
- Passband ripple?
- Stopband attenuation?
- Group delay flatness?
- Computational cost?
-
Why does the polyphase form provide the specific group-delay characteristic LoRa demodulation requires?
-
At what SNR does your Python FIR demodulator fail (symbol errors > 10%)? How does this compare to the polyphase GRC demodulator? (Generate noisy IQ with
samples + noiseat decreasing SNR levels to measure this.)
Deliverables
- Spectrogram screenshot from Part 1 with chirp direction and duration annotated
- Python FIR demodulator output: first 20 symbol values
- GNU Radio flowgraph screenshot (polyphase/gr-lora-sdr)
- Comparison table: symbol values from FIR vs. polyphase (do they agree for symbols 1-20?)
- Lyons Ch 7 analysis (1 page)
Grading (10 points)
| Item | Points |
|---|---|
| Spectrogram screenshot with correct annotations | 1.5 |
| FIR demodulator: correct first-20 symbol values | 2.5 |
| GNU Radio polyphase flowgraph screenshot | 2 |
| FIR vs. polyphase comparison (agree on 18+/20 symbols) | 2 |
| Lyons analysis (addresses all three questions) | 2 |