summaryrefslogtreecommitdiffstats
path: root/dpd/show_spectrum.py
diff options
context:
space:
mode:
Diffstat (limited to 'dpd/show_spectrum.py')
-rwxr-xr-xdpd/show_spectrum.py132
1 files changed, 114 insertions, 18 deletions
diff --git a/dpd/show_spectrum.py b/dpd/show_spectrum.py
index e92c1d0..f23dba2 100755
--- a/dpd/show_spectrum.py
+++ b/dpd/show_spectrum.py
@@ -1,16 +1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP server
-# and get samples from there.
+# This is an example tool that shows how to connect to ODR-DabMod's dpd TCP
+# server and get samples from there.
#
-# Since the TX and RX samples are not perfectly aligned, the tool has to align them properly,
-# which is done in two steps: First on sample-level using a correlation, then with subsample
-# accuracy using a FFT approach.
+# Since the TX and RX samples are not perfectly aligned, the tool has to align
+# them properly, which is done in two steps: First on sample-level using a
+# correlation, then with subsample accuracy using a FFT approach.
#
# It requires SciPy and matplotlib.
#
-# Copyright (C) 2017 Matthias P. Braendli
# http://www.opendigitalradio.org
# Licence: The MIT License, see notice at the end of this file
@@ -21,9 +20,18 @@ import numpy as np
import matplotlib.pyplot as pp
from matplotlib.animation import FuncAnimation
import argparse
+from scipy.misc import imsave
SIZEOF_SAMPLE = 8 # complex floats
+# Constants for TM 1
+NbSymbols = 76
+NbCarriers = 1536
+Spacing = 2048
+NullSize = 2656
+SymSize = 2552
+FicSizeOut = 288
+
def main():
parser = argparse.ArgumentParser(description="Plot the spectrum of ODR-DabMod's DPD feedback")
parser.add_argument('--samps', default='10240', help='Number of samples to request at once',
@@ -31,18 +39,19 @@ def main():
parser.add_argument('--port', default='50055',
help='port to connect to ODR-DabMod DPD (default: 50055)',
required=False)
-
parser.add_argument('--animated', action='store_true', help='Enable real-time animation')
+ parser.add_argument('--constellation', action='store_true', help='Draw constellaton plot')
+ parser.add_argument('--samplerate', default='8192000', help='Sample rate',
+ required=False)
cli_args = parser.parse_args()
- port = int(cli_args.port)
- num_samps_to_request = int(cli_args.samps)
-
- if cli_args.animated:
- plot_spectrum_animated(port, num_samps_to_request)
+ if cli_args.constellation:
+ plot_constellation_once(cli_args)
+ elif cli_args.animated:
+ plot_spectrum_animated(cli_args)
else:
- plot_spectrum_once(port, num_samps_to_request)
+ plot_spectrum_once(cli_args)
def recv_exact(sock, num_bytes):
bufs = []
@@ -82,6 +91,7 @@ def get_samples(port, num_samps_to_request):
else:
txframe = np.array([], dtype=np.complex64)
+
print("Wait for RX metadata")
rx_second, rx_pps = struct.unpack("=II", recv_exact(s, 8))
rx_ts = rx_second + rx_pps / 16384000.0
@@ -98,7 +108,7 @@ def get_samples(port, num_samps_to_request):
return (tx_ts, txframe, rx_ts, rxframe)
-def get_spectrum(port, num_samps_to_request):
+def recv_rxtx(port, num_samps_to_request):
tx_ts, txframe, rx_ts, rxframe = get_samples(port, num_samps_to_request)
# convert to complex doubles for more dynamic range
@@ -107,6 +117,10 @@ def get_spectrum(port, num_samps_to_request):
print("Received {} & {} frames at {} and {}".format(
len(txframe), len(rxframe), tx_ts, rx_ts))
+ return tx_ts, txframe, rx_ts, rxframe
+
+def get_spectrum(port, num_samps_to_request):
+ tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request)
print("Calculate TX and RX spectrum assuming 8192000 samples per second")
tx_spectrum = np.fft.fftshift(np.fft.fft(txframe, fft_size))
@@ -116,12 +130,90 @@ def get_spectrum(port, num_samps_to_request):
rx_power = 20*np.log10(np.abs(rx_spectrum))
return tx_power, rx_power
+def remove_guard_intervals(frame, options):
+ """Remove the cyclic prefix. The frame needs to be aligned to the
+ end of the transmission frame. Transmission Mode 1 is assumed"""
+ oversample = int(int(options.samplerate) / 2048000)
+
+ # From the end, take 2048 samples, then skip 504 samples
+ frame = frame[::-1]
+
+ stride_len = Spacing * oversample
+ stride_advance = SymSize * oversample
+
+ # Truncate the frame to an integer length of strides
+ newlen = len(frame) - (len(frame) % stride_advance)
+ print("Truncating frame from {} to {}".format(len(frame), newlen))
+ frame = frame[:newlen]
+
+ # Remove the cyclic prefix
+ frame = frame.reshape(-1, stride_advance)[:,:stride_len].reshape(-1)
+
+ # Reverse again
+ return frame[::-1]
+
+
+def plot_constellation_once(options):
+ port = int(options.port)
+ num_samps_to_request = int(options.samps)
+
+ tx_ts, txframe, rx_ts, rxframe = recv_rxtx(port, num_samps_to_request)
+
+ frame = remove_guard_intervals(txframe, options)
+
+ oversample = int(int(options.samplerate) / 2048000)
+
+ n = Spacing * oversample # is also number of samples per symbol
+ if len(frame) % n != 0:
+ raise ValueError("Frame length doesn't contain exact number of symbols")
+ num_syms = int(len(frame) / n)
+ print("frame {} has {} symbols".format(len(frame), num_syms))
+ spectrums = np.array([np.fft.fftshift(np.fft.fft(frame[n*i:n*(i+1)], n)) for i in range(num_syms)])
+
+ def normalise(x):
+ """Normalise a real-valued array x to the range [0,1]"""
+ y = x + np.min(x)
+ return x / np.max(x)
+
+ imsave("spectrums.png", np.concatenate([
+ normalise(np.abs(spectrums)),
+ normalise(np.angle(spectrums))]))
+
+ # Only take bins that are supposed to contain energy
+ # i.e. the middle 1536 bins, excluding the bin at n/2
+ assert(n % 2 == 0)
+ n_half = int(n/2)
+ spectrums = np.concatenate(
+ [spectrums[...,n_half-768:n_half],
+ spectrums[...,n_half + 1:n_half + 769]], axis=1)
+
+ sym_indices = (np.tile(np.arange(num_syms-1).reshape(num_syms-1,1), (1,NbCarriers)) +
+ np.tile(np.linspace(-0.4, 0.4, NbCarriers), (num_syms-1, 1) ) )
+ sym_indices = sym_indices.reshape(-1)
+ diff_angles = np.mod(np.diff(np.angle(spectrums, deg=1), axis=0), 360)
+ #sym_points = spectrums[:-1].reshape(-1)
+ # Set amplitude and phase of low power points to zero, avoid cluttering diagram
+ #sym_points[np.abs(sym_points) < np.mean(np.abs(sym_points)) * 0.1] = 0
+
+ print("ix {} spec {} da {}".format(
+ sym_indices.shape, spectrums.shape, diff_angles.shape))
+
+ fig = pp.figure()
+
+ fig.suptitle("Constellation")
+ ax1 = fig.add_subplot(111)
+ ax1.set_title("TX")
+ ax1.scatter(sym_indices, diff_angles.reshape(-1), alpha=0.1)
+
+ pp.show()
-sampling_rate = 8192000
fft_size = 4096
-freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./sampling_rate))
-def plot_spectrum_once(port, num_samps_to_request):
+def plot_spectrum_once(options):
+ port = int(options.port)
+ num_samps_to_request = int(options.samps)
+ freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate)))
+
tx_power, rx_power = get_spectrum(port, num_samps_to_request)
fig = pp.figure()
@@ -134,7 +226,11 @@ def plot_spectrum_once(port, num_samps_to_request):
ax2.plot(freqs, rx_power, 'b')
pp.show()
-def plot_spectrum_animated(port, num_samps_to_request):
+def plot_spectrum_animated(options):
+ port = int(options.port)
+ num_samps_to_request = int(options.samps)
+ freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1./int(options.samplerate)))
+
fig, axes = pp.subplots(2, sharex=True)
line1, = axes[0].plot(freqs, np.ones(len(freqs)), 'r', animated=True)
axes[0].set_title("TX")