aboutsummaryrefslogtreecommitdiffstats
path: root/dpd
diff options
context:
space:
mode:
Diffstat (limited to 'dpd')
-rw-r--r--dpd/README.md49
-rwxr-xr-xdpd/iq_file_server.py1
-rwxr-xr-xdpd/main.py44
-rwxr-xr-xdpd/show_spectrum.py11
-rw-r--r--dpd/src/Adapt.py119
-rw-r--r--dpd/src/Measure.py18
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(