diff options
Diffstat (limited to 'dpd')
-rw-r--r-- | dpd/README.md | 49 | ||||
-rwxr-xr-x | dpd/iq_file_server.py | 1 | ||||
-rwxr-xr-x | dpd/main.py | 44 | ||||
-rwxr-xr-x | dpd/show_spectrum.py | 11 | ||||
-rw-r--r-- | dpd/src/Adapt.py | 119 | ||||
-rw-r--r-- | dpd/src/Measure.py | 18 |
6 files changed, 156 insertions, 86 deletions
diff --git a/dpd/README.md b/dpd/README.md index 173b4c6..b5a6b81 100644 --- a/dpd/README.md +++ b/dpd/README.md @@ -1,21 +1,56 @@ -Digital Predistortion for ODR-DabMod -==================================== +Digital Predistortion Calculation Engine for ODR-DabMod +======================================================= -This folder contains work in progress for digital predistortion. It requires: +This folder contains work in progress for digital predistortion. + +Concept +------- + +ODR-DabMod makes outgoing TX samples and feedback RX samples available for an external tool. This +external tool can request a buffer of samples for analysis, can calculate coefficients for the +polynomial predistorter in ODR-DabMod and load the new coefficients using the remote control. + +The *dpd/main.py* script is the entry point for the *DPD Calculation Engine* into which these +features will be implemented. The tool uses modules from the *dpd/src/* folder: + +- Sample transfer and time alignment with subsample accuracy is done by *Measure.py* +- Estimating the effects of the PA using some model and calculation of the updated + polynomial coefficients is done in *Model.py* +- Finally, *Adapt.py* loads them into ODR-DabMod. + +These modules themselves use additional helper scripts in the *dpd/src/* folder. + +Requirements +------------ - USRP B200. - Power amplifier. - A feedback connection from the power amplifier output, at an appropriate power level for the B200. - Usually this is done with a directional coupler. -- ODR-DabMod with enabled dpd_port, and with a samplerate of 8192000 samples per second. + Usually this is done with a directional coupler and additional attenuators. +- ODR-DabMod with enabled *dpd_port*, and with a samplerate of 8192000 samples per second. - Synchronous=1 so that the USRP has the timestamping set properly, internal refclk and pps are sufficient for this example. - A live mux source with TIST enabled. See dpd/dpd.ini for an example. +The DPD server port can be tested with the *dpd/show_spectrum.py* helper tool, which can also display +a constellation diagram. + +File format for coefficients +---------------------------- +The coef file contains the polynomial coefficients used in the predistorter. The file format is +very similar to the filtertaps file used in the FIR filter. It is a text-based format that can +easily be inspected and edited in a text editor. + +The first line contains the number of coefficients as an integer. The second and third lines contain +the real, respectively the imaginary parts of the first coefficient. Fourth and fifth lines give the +second coefficient, and so on. The file therefore contains 2xN + 1 lines if it contains N +coefficients. + TODO ---- -Implement a PA model that updates the predistorter. -Implement cases for different oversampling for FFT bin choice +Implement a PA model. +Implement cases for different oversampling for FFT bin choice. +Fix loads of missing and buggy aspects of the implementation. diff --git a/dpd/iq_file_server.py b/dpd/iq_file_server.py index 2a38151..7a4e570 100755 --- a/dpd/iq_file_server.py +++ b/dpd/iq_file_server.py @@ -4,7 +4,6 @@ # This example server simulates the ODR-DabMod's # DPD server, taking samples from an IQ file # -# Copyright (C) 2017 Matthias P. Braendli # http://www.opendigitalradio.org # Licence: The MIT License, see notice at the end of this file diff --git a/dpd/main.py b/dpd/main.py index c871879..98eeb84 100755 --- a/dpd/main.py +++ b/dpd/main.py @@ -1,8 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""This Python script calculates and updates the parameter of the digital -predistortion module of the ODR-DabMod. More precisely the complex -coefficients of the polynom which is used for predistortion.""" +# +# DPD Calculation Engine main file. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file + +"""This Python script is the main file for ODR-DabMod's DPD Computation Engine. +This engine calculates and updates the parameter of the digital +predistortion module of ODR-DabMod.""" import logging logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', @@ -15,12 +21,32 @@ import src.Measure as Measure import src.Model as Model import src.Adapt as Adapt -port = 50055 -port_rc = 9400 -coef_path = "/home/andreas/dab/ODR-DabMod/polyCoefsCustom" -num_req = 10240 +parser = argparse.ArgumentParser(description="DPD Computation Engine for ODR-DabMod") +parser.add_argument('--port', default='50055', + help='port of DPD server to connect to (default: 50055)', + required=False) +parser.add_argument('--rc-port', default='9400', + help='port of ODR-DabMod ZMQ Remote Control to connect to (default: 9400)', + required=False) +parser.add_argument('--samplerate', default='8192000', + help='Sample rate', + required=False) +parser.add_argument('--coefs', default='dpdpoly.coef', + help='File with DPD coefficients, which will be read by ODR-DabMod', + required=False) +parser.add_argument('--samps', default='10240', + help='Number of samples to request from ODR-DabMod', + required=False) + +cli_args = parser.parse_args() + +port = int(cli_args.port) +port_rc = int(cli_args.rc_port) +coef_path = cli_args.coefs +num_req = int(cli_args.samps) +samplerate = int(cli_args.samplerate) -meas = Measure.Measure(port, num_req) +meas = Measure.Measure(samplerate, port, num_req) adapt = Adapt.Adapt(port_rc, coef_path) coefs = adapt.get_coefs() model = Model.Model(coefs) @@ -31,7 +57,7 @@ adapt.set_coefs(coefs) # The MIT License (MIT) # -# Copyright (c) 2017 Andreas Steger +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/dpd/show_spectrum.py b/dpd/show_spectrum.py index 0ae24c2..95dbef9 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 diff --git a/dpd/src/Adapt.py b/dpd/src/Adapt.py index b86f604..47a5e8a 100644 --- a/dpd/src/Adapt.py +++ b/dpd/src/Adapt.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- +# +# DPD Calculation Engine: updates ODR-DabMod's predistortion block. +# +# http://www.opendigitalradio.org +# Licence: The MIT License, see notice at the end of this file """ This module is used to change settings of ODR-DabMod using the ZMQ remote control socket. """ import zmq -import exceptions import logging import numpy as np -port = 9400 - class Adapt: - """Uses the ZMQ remote control to change parameters - of the DabMod + """Uses the ZMQ remote control to change parameters of the DabMod Parameters ---------- @@ -27,36 +28,38 @@ class Adapt: self.port = port self.coef_path = coef_path self.host = "localhost" - self._connect() + self._context = zmq.Context() def _connect(self): """Establish the connection to ODR-DabMod using - a ZMQ socket that is in request mode (Client)""" - context = zmq.Context() - sock = context.socket(zmq.REQ) + a ZMQ socket that is in request mode (Client). + Returns a socket""" + sock = self._context.socket(zmq.REQ) sock.connect("tcp://%s:%d" % (self.host, self.port)) sock.send(b"ping") data = sock.recv_multipart() if data != ['ok']: - raise exceptions.RuntimeError( + raise RuntimeError( "Could not connect to server %s %d." % (self.host, self.port)) - self.sock = sock + return sock def send_receive(self, message): - """Used to send a message to the ODR-DabMod. It always - returns a answer it also receives the next message - from ODR-DabMod over the ZMQ remote control socket. + """Send a message to ODR-DabMod. It always + returns the answer ODR-DabMod sends back. + + An example message could be + "get uhd txgain" or "set uhd txgain 50" Parameter --------- message : str - The message string that will be sent to - the receiver. + The message string that will be sent to the receiver. """ + sock = self._connect() logging.info("Send message: %s" % message) msg_parts = message.split(" ") for i, part in enumerate(msg_parts): @@ -65,52 +68,62 @@ class Adapt: else: f = zmq.SNDMORE - self.sock.send(part.encode(), flags=f) + sock.send(part.encode(), flags=f) - data = self.sock.recv_multipart() + data = sock.recv_multipart() logging.info("Received message: %s" % message) return data def set_txgain(self, gain): - """Set a new txgain for the ORD-DabMod. + """Set a new txgain for the ODR-DabMod. Parameters ---------- gain : int - Value that will be set to be txgain + new TX gain, in the same format as ODR-DabMod's config file """ + # TODO this is specific to the B200 if gain < 0 or gain > 89: - raise exceptions.ValueError("Gain has to be in [0,89]") + raise ValueError("Gain has to be in [0,89]") return self.send_receive("set uhd txgain %d" % gain) def get_txgain(self): - """Get the txgain value in dB for the ORD-DabMod.""" + """Get the txgain value in dB for the ODR-DabMod.""" + # TODO handle failure return self.send_receive("get uhd txgain") def set_rxgain(self, gain): - """Set a new rxgain for the ORD-DabMod. + """Set a new rxgain for the ODR-DabMod. Parameters ---------- gain : int - Value that will be set to be rxgain + new RX gain, in the same format as ODR-DabMod's config file """ + # TODO this is specific to the B200 if gain < 0 or gain > 89: - raise exceptions.ValueError("Gain has to be in [0,89]") + raise ValueError("Gain has to be in [0,89]") return self.send_receive("set uhd rxgain %d" % gain) def get_rxgain(self): - """Get the rxgain value in dB for the ORD-DabMod.""" + """Get the rxgain value in dB for the ODR-DabMod.""" + # TODO handle failure return self.send_receive("get uhd rxgain") def _read_coef_file(self): + """Load the coefficients from the file in the format given in the README""" coefs_complex = [] f = open(self.coef_path, 'r') lines = f.readlines() - n_coefs = lines[0] + n_coefs = int(lines[0]) coefs = [float(l) for l in lines[1:]] + i = 0 for r, c in zip(coefs[0::2], coefs[1::2]): - coefs_complex.append(np.complex64(r + 1j * c)) + if i < n_coefs: + coefs_complex.append(np.complex64(r + 1j * c)) + else: + raise ValueError("Incorrect coef file format: too many coefficients") + i += 1 f.close() return coefs_complex @@ -118,36 +131,34 @@ class Adapt: return self._read_coef_file() def _write_coef_file(self, coefs_complex): - coef_path = "/home/andreas/dab/ODR-DabMod/polyCoefsCustom" - f = open(coef_path, 'w') - f.write(str(len(coefs_complex)) + "\n") + f = open(self.coef_path, 'w') + f.write("{}\n".format(len(coefs_complex)).encode()) for coef in coefs_complex: - f.write(str(coef.real) + "\n") - f.write(str(coef.imag) + "\n") + f.write("{}\n{}\n".format(coef.real, coef.imag).encode()) f.close() def set_coefs(self, coefs_complex): self._write_coef_file(coefs_complex) self.send_receive("set memlesspoly coeffile polyCoefsCustom") - # The MIT License (MIT) - # - # Copyright (c) 2017 Andreas Steger - # - # Permission is hereby granted, free of charge, to any person obtaining a copy - # of this software and associated documentation files (the "Software"), to deal - # in the Software without restriction, including without limitation the rights - # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - # copies of the Software, and to permit persons to whom the Software is - # furnished to do so, subject to the following conditions: - # - # The above copyright notice and this permission notice shall be included in all - # copies or substantial portions of the Software. - # - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - # SOFTWARE. +# The MIT License (MIT) +# +# Copyright (c) 2017 Andreas Steger, Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/dpd/src/Measure.py b/dpd/src/Measure.py index 76f17b2..2ba3b37 100644 --- a/dpd/src/Measure.py +++ b/dpd/src/Measure.py @@ -4,27 +4,29 @@ import sys import socket import struct import numpy as np -import matplotlib.pyplot as pp +import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation import argparse import os import time import logging -import Dab_Util as DU +import src.Dab_Util as DU import datetime class Measure: """Collect Measurement from DabMod""" - def __init__(self, port, num_samples_to_request): - """""" + def __init__(self, samplerate, port, num_samples_to_request): logging.info("Instantiate Measure object") + self.samplerate = samplerate self.sizeof_sample = 8 # complex floats self.port = port self.num_samples_to_request = num_samples_to_request def _recv_exact(self, sock, num_bytes): - """Interfaces the socket to receive a byte string - + """Receive an exact number of bytes from a socket. This is + a wrapper around sock.recv() that can return less than the number + of requested bytes. + Args: sock (socket): Socket to receive data from. num_bytes (int): Number of bytes that will be returned. @@ -77,8 +79,6 @@ class Measure: rxframe = np.array([], dtype=np.complex64) if logging.getLogger().getEffectiveLevel() == logging.DEBUG: - import matplotlib.pyplot as plt - txframe_path = ('/tmp/txframe_fft_' + datetime.datetime.now().isoformat() + '.pdf') @@ -108,7 +108,7 @@ class Measure: logging.debug("Disconnecting") s.close() - du = DU.Dab_Util(8192000) + du = DU.Dab_Util(samplerate) txframe_aligned, rxframe_aligned = du.subsample_align(txframe, rxframe) logging.info( |