Classroom Glossary Public page

Lab 5: LoRa Demodulator Pair (FIR + Polyphase)

384 words

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:

  1. First 10 symbols decoded: do they agree?
  2. 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:

  1. What is the primary performance criterion for the LoRa demodulator's filter stage?

    • Passband ripple?
    • Stopband attenuation?
    • Group delay flatness?
    • Computational cost?
  2. Why does the polyphase form provide the specific group-delay characteristic LoRa demodulation requires?

  3. 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 + noise at 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