diff options
author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2017-11-02 14:11:26 +0100 |
---|---|---|
committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2017-11-02 14:11:26 +0100 |
commit | 450c1e1d29a08326f4a370005bacafd528cd25e7 (patch) | |
tree | d9c21863bf6c1c0e2024203a5e1f1731f7e36f18 /src/output | |
parent | 1c5372335338962ccbe9e876467b7e0ea46877ac (diff) | |
download | dabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.tar.gz dabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.tar.bz2 dabmod-450c1e1d29a08326f4a370005bacafd528cd25e7.zip |
Create new SDR output abstraction and port Soapy
Diffstat (limited to 'src/output')
-rw-r--r-- | src/output/SDR.cpp | 380 | ||||
-rw-r--r-- | src/output/SDR.h | 169 | ||||
-rw-r--r-- | src/output/Soapy.cpp | 223 | ||||
-rw-r--r-- | src/output/Soapy.h | 89 | ||||
-rw-r--r-- | src/output/UHD.cpp | 994 | ||||
-rw-r--r-- | src/output/UHD.h | 244 |
6 files changed, 2099 insertions, 0 deletions
diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp new file mode 100644 index 0000000..c1183c4 --- /dev/null +++ b/src/output/SDR.cpp @@ -0,0 +1,380 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + */ +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "output/SDR.h" + +#include "PcDebug.h" +#include "Log.h" +#include "RemoteControl.h" +#include "Utils.h" + +#include <cmath> +#include <iostream> +#include <assert.h> +#include <stdexcept> +#include <stdio.h> +#include <time.h> +#include <errno.h> +#include <unistd.h> +#include <pthread.h> + +using namespace std; + +namespace Output { + +// Maximum number of frames that can wait in frames +static constexpr size_t FRAMES_MAX_SIZE = 8; + +// If the timestamp is further in the future than +// 100 seconds, abort +static constexpr double TIMESTAMP_ABORT_FUTURE = 100; + +// Add a delay to increase buffers when +// frames are too far in the future +static constexpr double TIMESTAMP_MARGIN_FUTURE = 0.5; + +SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) : + ModOutput(), + RemoteControllable("sdr"), + m_config(config), + m_running(false), + m_device(device) +{ + m_device_thread = std::thread(&SDR::process_thread_entry, this); +} + +SDR::~SDR() +{ + stop(); +} + +void SDR::stop() +{ + m_running.store(false); + + FrameData end_marker; + end_marker.buf.resize(0); + m_queue.push(end_marker); + + if (m_device_thread.joinable()) { + m_device_thread.join(); + } +} + +int SDR::process(Buffer *dataIn) +{ + if (m_device) { + if (not m_device->is_clk_source_ok()) { + // Ignore frame + return dataIn->getLength(); + } + + FrameData frame; + frame.buf.resize(dataIn->getLength()); + + const uint8_t* pDataIn = (uint8_t*)dataIn->getData(); + std::copy(pDataIn, pDataIn + dataIn->getLength(), + frame.buf.begin()); + + m_eti_source->calculateTimestamp(frame.ts); + + // TODO check device running + + if (frame.ts.fct == -1) { + etiLog.level(info) << + "OutputUHD: dropping one frame with invalid FCT"; + } + else { + // TODO setup Feedback and set tx_frame + + size_t num_frames = m_queue.push_wait_if_full(frame, + FRAMES_MAX_SIZE); + etiLog.log(trace, "SDR,push %zu", num_frames); + } + } + else { + // Ignore frame + } + + return dataIn->getLength(); +} + +void SDR::process_thread_entry() +{ + // Set thread priority to realtime + if (int ret = set_realtime_prio(1)) { + etiLog.level(error) << "Could not set priority for SDR device thread:" << ret; + } + + set_thread_name("sdrdevice"); + + last_tx_time_initialised = false; + + size_t last_num_underflows = 0; + size_t pop_prebuffering = FRAMES_MAX_SIZE; + + while (m_running.load()) { + struct FrameData frame; + etiLog.log(trace, "SDR,wait"); + m_queue.wait_and_pop(frame, pop_prebuffering); + etiLog.log(trace, "SDR,pop"); + + if (m_running.load() == false or frame.buf.empty()) { + break; + } + + if (m_device) { + handle_frame(frame); + + const auto rs = m_device->get_run_statistics(); + + /* Ensure we fill frames after every underrun and + * at startup to reduce underrun likelihood. */ + if (last_num_underflows < rs.num_underruns) { + pop_prebuffering = FRAMES_MAX_SIZE; + } + else { + pop_prebuffering = 1; + } + + last_num_underflows = rs.num_underruns; + } + + } + + m_running.store(false); + etiLog.level(warn) << "SDR Device thread terminated"; +} + +const char* SDR::name() +{ + if (m_device) { + m_name = "OutputSDR("; + m_name += m_device->device_name(); + m_name += ")"; + } + else { + m_name = "OutputSDR(<no device>)"; + } + return m_name.c_str(); +} + +void SDR::setETISource(EtiSource *etiSource) +{ + m_eti_source = etiSource; +} + +void SDR::handle_frame(struct FrameData& frame) +{ + // Assumes m_device is valid + + constexpr double tx_timeout = 20.0; + + if (not m_device->is_clk_source_ok()) { + return; + } + + double device_time = m_device->get_real_secs(); + bool timestamp_discontinuity = false; + const auto& time_spec = frame.ts; + + if (sourceContainsTimestamp) { + // Tx time from MNSC and TIST + const uint32_t tx_second = frame.ts.timestamp_sec; + const uint32_t tx_pps = frame.ts.timestamp_pps; + + if (not frame.ts.timestamp_valid) { + /* We have not received a full timestamp through + * MNSC. We sleep through the frame. + */ + etiLog.level(info) << + "OutputSDR: Throwing sample " << frame.ts.fct << + " away: incomplete timestamp " << tx_second << + " / " << tx_pps; + return; + } + + if (last_tx_time_initialised) { + const size_t sizeIn = frame.buf.size() / sizeof(complexf); + uint64_t increment = (uint64_t)sizeIn * 16384000ul / + (uint64_t)m_config.sampleRate; + // samps * ticks/s / (samps/s) + // (samps * ticks * s) / (s * samps) + // ticks + + uint32_t expected_sec = last_tx_second + increment / 16384000ul; + uint32_t expected_pps = last_tx_pps + increment % 16384000ul; + + while (expected_pps >= 16384000) { + expected_sec++; + expected_pps -= 16384000; + } + + if (expected_sec != tx_second or expected_pps != tx_pps) { + etiLog.level(warn) << "OutputSDR: timestamp irregularity!" << + std::fixed << + " Expected " << + expected_sec << "+" << (double)expected_pps/16384000.0 << + "(" << expected_pps << ")" << + " Got " << + tx_second << "+" << (double)tx_pps/16384000.0 << + "(" << tx_pps << ")"; + + timestamp_discontinuity = true; + } + } + + last_tx_second = tx_second; + last_tx_pps = tx_pps; + last_tx_time_initialised = true; + + const double pps_offset = tx_pps / 16384000.0; + + etiLog.log(trace, "SDR,tist %f", time_spec.get_real_secs()); + + if (time_spec.get_real_secs() + tx_timeout < device_time) { + etiLog.level(warn) << + "OutputSDR: Timestamp in the past! offset: " << + std::fixed << + time_spec.get_real_secs() - device_time << + " (" << device_time << ")" + " frame " << frame.ts.fct << + ", tx_second " << tx_second << + ", pps " << pps_offset; + return; + } + + if (time_spec.get_real_secs() > device_time + TIMESTAMP_ABORT_FUTURE) { + etiLog.level(error) << + "OutputSDR: Timestamp way too far in the future! offset: " << + std::fixed << + time_spec.get_real_secs() - device_time; + throw std::runtime_error("Timestamp error. Aborted."); + } + } + else { // !sourceContainsTimestamp + if (m_config.muting or m_config.muteNoTimestamps) { + /* There was some error decoding the timestamp */ + if (m_config.muting) { + etiLog.log(info, + "OutputSDR: Muting sample %d requested\n", + frame.ts.fct); + } + else { + etiLog.log(info, + "OutputSDR: Muting sample %d : no timestamp\n", + frame.ts.fct); + } + return; + } + } + + if (timestamp_discontinuity) { + frame.ts.timestamp_refresh = true; + } + m_device->transmit_frame(frame); +} + +// ======================================= +// Remote Control +// ======================================= +void SDR::set_parameter(const string& parameter, const string& value) +{ + stringstream ss(value); + ss.exceptions ( stringstream::failbit | stringstream::badbit ); + + if (parameter == "txgain") { + ss >> m_config.txgain; + m_device->set_txgain(m_config.txgain); + } + else if (parameter == "rxgain") { + ss >> m_config.rxgain; + // TODO myUsrp->set_rx_gain(m_config.rxgain); + throw ParameterError("Parameter " + parameter + " is TODO."); + } + else if (parameter == "freq") { + ss >> m_config.frequency; + m_device->tune(m_config.lo_offset, m_config.frequency); + m_config.frequency = m_device->get_tx_freq(); + } + else if (parameter == "muting") { + ss >> m_config.muting; + } + else if (parameter == "underruns" or + parameter == "latepackets" or + parameter == "frames") { + throw ParameterError("Parameter " + parameter + " is read-only."); + } + else { + stringstream ss; + ss << "Parameter '" << parameter + << "' is not exported by controllable " << get_rc_name(); + throw ParameterError(ss.str()); + } +} + +const string SDR::get_parameter(const string& parameter) const +{ + stringstream ss; + if (parameter == "txgain") { + ss << m_config.txgain; + } + else if (parameter == "rxgain") { + ss << m_config.rxgain; + } + else if (parameter == "freq") { + ss << m_config.frequency; + } + else if (parameter == "muting") { + ss << m_config.muting; + } + else if (parameter == "underruns" or + parameter == "latepackets" or + parameter == "frames" ) { + if (not m_device) { + throw ParameterError("OutputSDR has no device"); + } + const auto stat = m_device->get_run_statistics(); + + if (parameter == "underruns") { + ss << stat.num_underruns; + } + else if (parameter == "latepackets") { + ss << stat.num_late_packets; + } + else if (parameter == "frames") { + ss << stat.num_frames_modulated; + } + } + else { + ss << "Parameter '" << parameter << + "' is not exported by controllable " << get_rc_name(); + throw ParameterError(ss.str()); + } + return ss.str(); +} + +} // namespace Output diff --git a/src/output/SDR.h b/src/output/SDR.h new file mode 100644 index 0000000..ab23aa1 --- /dev/null +++ b/src/output/SDR.h @@ -0,0 +1,169 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + Common interface for all SDR outputs +*/ + +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "ModPlugin.h" +#include "EtiReader.h" + +namespace Output { + +using complexf = std::complex<float>; + +enum refclk_lock_loss_behaviour_t { CRASH, IGNORE }; + +/* This structure is used as initial configuration for all SDR devices. + * It must also contain all remote-controllable settings, otherwise + * they will get lost on a modulator restart. */ +struct SDRDeviceConfig { + std::string device; + + long masterClockRate = 32768000; + unsigned sampleRate = 2048000; + double frequency = 0.0; + double lo_offset = 0.0; + double txgain = 0.0; + double rxgain = 0.0; + bool enableSync = false; + + // When working with timestamps, mute the frames that + // do not have a timestamp + bool muteNoTimestamps = false; + unsigned dabMode = 0; + unsigned maxGPSHoldoverTime = 0; + + /* allowed values for UHD : auto, int, sma, mimo */ + std::string refclk_src; + + /* allowed values for UHD : int, sma, mimo */ + std::string pps_src; + + /* allowed values for UHD : pos, neg */ + std::string pps_polarity; + + /* What to do when the reference clock PLL loses lock */ + refclk_lock_loss_behaviour_t refclk_lock_loss_behaviour; + + // muting can only be changed using the remote control + bool muting = false; + + // TCP port on which to serve TX and RX samples for the + // digital pre distortion learning tool + uint16_t dpdFeedbackServerPort = 0; +}; + +// Each frame contains one OFDM frame, and its +// associated timestamp +struct FrameData { + // Buffer holding frame data + std::vector<uint8_t> buf; + + // A full timestamp contains a TIST according to standard + // and time information within MNSC with tx_second. + struct frame_timestamp ts; +}; + + +// All SDR Devices must implement the SDRDevice interface +class SDRDevice { + public: + struct RunStatistics { + size_t num_underruns; + size_t num_late_packets; + size_t num_overruns; + size_t num_frames_modulated; //TODO increment + }; + + // TODO make some functions const + virtual void tune(double lo_offset, double frequency) = 0; + virtual double get_tx_freq(void) = 0; + virtual void set_txgain(double txgain) = 0; + virtual double get_txgain(void) = 0; + virtual void transmit_frame(const struct FrameData& frame) = 0; + virtual RunStatistics get_run_statistics(void) = 0; + virtual double get_real_secs(void) = 0; + + + // Return true if GPS and reference clock inputs are ok + virtual bool is_clk_source_ok(void) = 0; + + virtual const char* device_name(void) = 0; +}; + +class SDR : public ModOutput, public RemoteControllable { + public: + SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device); + SDR(const SDR& other) = delete; + SDR operator=(const SDR& other) = delete; + ~SDR(); + + virtual int process(Buffer *dataIn) override; + + virtual const char* name() override; + + void setETISource(EtiSource *etiSource); + + /*********** REMOTE CONTROL ***************/ + + /* Base function to set parameters. */ + virtual void set_parameter(const std::string& parameter, + const std::string& value) override; + + /* Getting a parameter always returns a string. */ + virtual const std::string get_parameter( + const std::string& parameter) const override; + + private: + void stop(void); + void process_thread_entry(void); + void handle_frame(struct FrameData &frame); + + SDRDeviceConfig& m_config; + + std::atomic<bool> m_running; + std::thread m_device_thread; + ThreadsafeQueue<FrameData> m_queue; + + std::shared_ptr<SDRDevice> m_device; + std::string m_name; + + EtiSource *m_eti_source = nullptr; + bool sourceContainsTimestamp = false; + bool last_tx_time_initialised = false; + uint32_t last_tx_second = 0; + uint32_t last_tx_pps = 0; +}; + +} + diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp new file mode 100644 index 0000000..d6b5953 --- /dev/null +++ b/src/output/Soapy.cpp @@ -0,0 +1,223 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + It is an output driver using the SoapySDR library that can output to + many devices. +*/ + +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "output/Soapy.h" + +#ifdef HAVE_SOAPYSDR + +#include <SoapySDR/Errors.hpp> +#include <chrono> +#include <cstdio> + +#include "Log.h" +#include "Utils.h" + +using namespace std; + +namespace Output { + +static constexpr size_t FRAMES_MAX_SIZE = 2; + +Soapy::Soapy(SDRDeviceConfig& config) : + SDRDevice(), + m_conf(config) +{ + etiLog.level(info) << + "Soapy:Creating the device with: " << + m_conf.device; + + try { + m_device = SoapySDR::Device::make(m_conf.device); + stringstream ss; + ss << "SoapySDR driver=" << m_device->getDriverKey(); + ss << " hardware=" << m_device->getHardwareKey(); + for (const auto &it : m_device->getHardwareInfo()) { + ss << " " << it.first << "=" << it.second; + } + } + catch (const std::exception &ex) { + etiLog.level(error) << "Error making SoapySDR device: " << + ex.what(); + throw std::runtime_error("Cannot create SoapySDR output"); + } + + m_device->setMasterClockRate(m_conf.masterClockRate); + etiLog.level(info) << "SoapySDR master clock rate set to " << + m_device->getMasterClockRate()/1000.0 << " kHz"; + + m_device->setSampleRate(SOAPY_SDR_TX, 0, m_conf.sampleRate); + etiLog.level(info) << "SoapySDR:Actual TX rate: " << + m_device->getSampleRate(SOAPY_SDR_TX, 0) / 1000.0 << + " ksps."; + + tune(m_conf.lo_offset, m_conf.frequency); + m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0); + etiLog.level(info) << "SoapySDR:Actual frequency: " << + m_conf.frequency / 1000.0 << + " kHz."; + + m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain); + etiLog.level(info) << "SoapySDR:Actual tx gain: " << + m_device->getGain(SOAPY_SDR_TX, 0); + + std::vector<size_t> channels; + channels.push_back(0); + m_stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels); + m_device->activateStream(m_stream); +} + +Soapy::~Soapy() +{ + if (m_device != nullptr) { + if (m_stream != nullptr) { + m_device->closeStream(m_stream); + } + SoapySDR::Device::unmake(m_device); + } +} + +void Soapy::tune(double lo_offset, double frequency) +{ + if (not m_device) throw runtime_error("Soapy device not set up"); + + SoapySDR::Kwargs offset_arg; + offset_arg["OFFSET"] = to_string(lo_offset); + m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency, offset_arg); +} + +double Soapy::get_tx_freq(void) +{ + if (not m_device) throw runtime_error("Soapy device not set up"); + + // TODO lo offset + return m_device->getFrequency(SOAPY_SDR_TX, 0); +} + +void Soapy::set_txgain(double txgain) +{ + m_conf.txgain = txgain; + if (not m_device) throw runtime_error("Soapy device not set up"); + m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain); +} + +double Soapy::get_txgain(void) +{ + if (not m_device) throw runtime_error("Soapy device not set up"); + return m_device->getGain(SOAPY_SDR_TX, 0); +} + +SDRDevice::RunStatistics Soapy::get_run_statistics(void) +{ + RunStatistics rs; + rs.num_underruns = underflows; + rs.num_overruns = overflows; + rs.num_late_packets = late_packets; + rs.num_frames_modulated = num_frames_modulated; + return rs; +} + + +double Soapy::get_real_secs(void) +{ + if (m_device) { + long long time_ns = m_device->getHardwareTime(); + return time_ns / 1e9; + } + else { + return 0.0; + } +} + +bool Soapy::is_clk_source_ok() +{ + // TODO + return true; +} + +const char* Soapy::device_name(void) +{ + return "Soapy"; +} + +void Soapy::transmit_frame(const struct FrameData& frame) +{ + if (not m_device) throw runtime_error("Soapy device not set up"); + + // TODO timestamps + + // The frame buffer contains bytes representing FC32 samples + const complexf *buf = reinterpret_cast<const complexf*>(frame.buf.data()); + const size_t numSamples = frame.buf.size() / sizeof(complexf); + if ((frame.buf.size() % sizeof(complexf)) != 0) { + throw std::runtime_error("Soapy: invalid buffer size"); + } + + // Stream MTU is in samples, not bytes. + const size_t mtu = m_device->getStreamMTU(m_stream); + + size_t num_acc_samps = 0; + while (num_acc_samps < numSamples) { + const void *buffs[1]; + buffs[0] = buf + num_acc_samps; + + const size_t samps_to_send = std::min(numSamples - num_acc_samps, mtu); + + int flags = 0; + + auto ret = m_device->writeStream(m_stream, buffs, samps_to_send, flags); + + if (ret == SOAPY_SDR_TIMEOUT) { + continue; + } + else if (ret == SOAPY_SDR_OVERFLOW) { + overflows++; + continue; + } + else if (ret == SOAPY_SDR_UNDERFLOW) { + underflows++; + continue; + } + + if (ret < 0) { + etiLog.level(error) << "Unexpected stream error " << + SoapySDR::errToStr(ret); + throw std::runtime_error("Fault in Soapy"); + } + + num_acc_samps += ret; + } +} + +} // namespace Output + +#endif // HAVE_SOAPYSDR + + diff --git a/src/output/Soapy.h b/src/output/Soapy.h new file mode 100644 index 0000000..97076b5 --- /dev/null +++ b/src/output/Soapy.h @@ -0,0 +1,89 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + It is an output driver using the SoapySDR library that can output to + many devices. +*/ + +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#ifdef HAVE_SOAPYSDR +#include <SoapySDR/Version.hpp> +#include <SoapySDR/Modules.hpp> +#include <SoapySDR/Registry.hpp> +#include <SoapySDR/Device.hpp> + +#include <string> +#include <memory> + +#include "output/SDR.h" +#include "ModPlugin.h" +#include "EtiReader.h" +#include "RemoteControl.h" +#include "ThreadsafeQueue.h" + +namespace Output { + +class Soapy : public Output::SDRDevice +{ + public: + Soapy(SDRDeviceConfig& config); + Soapy(const Soapy& other) = delete; + Soapy& operator=(const Soapy& other) = delete; + ~Soapy(); + + virtual void tune(double lo_offset, double frequency) override; + virtual double get_tx_freq(void) override; + virtual void set_txgain(double txgain) override; + virtual double get_txgain(void) override; + virtual void transmit_frame(const struct FrameData& frame) override; + virtual RunStatistics get_run_statistics(void) override; + virtual double get_real_secs(void) override; + + // Return true if GPS and reference clock inputs are ok + virtual bool is_clk_source_ok(void) override; + virtual const char* device_name(void) override; + + private: + SDRDeviceConfig& m_conf; + SoapySDR::Device *m_device = nullptr; + SoapySDR::Stream *m_stream = nullptr; + + size_t underflows = 0; + size_t overflows = 0; + size_t late_packets = 0; + size_t num_frames_modulated = 0; //TODO increment +}; + +} // namespace Output + +#endif //HAVE_SOAPYSDR + diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp new file mode 100644 index 0000000..4942f3f --- /dev/null +++ b/src/output/UHD.cpp @@ -0,0 +1,994 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + */ +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "OutputUHD.h" + +#ifdef HAVE_OUTPUT_UHD + +#include "PcDebug.h" +#include "Log.h" +#include "RemoteControl.h" +#include "Utils.h" + +#include <boost/thread/future.hpp> + +#include <uhd/utils/msg.hpp> + +#include <cmath> +#include <iostream> +#include <assert.h> +#include <stdexcept> +#include <stdio.h> +#include <time.h> +#include <errno.h> +#include <unistd.h> +#include <pthread.h> + +using namespace std; + +// Maximum number of frames that can wait in frames +static const size_t FRAMES_MAX_SIZE = 8; + +typedef std::complex<float> complexf; + +std::string stringtrim(const std::string &s) +{ + auto wsfront = std::find_if_not(s.begin(), s.end(), + [](int c){ return std::isspace(c);} ); + return std::string(wsfront, + std::find_if_not(s.rbegin(), + std::string::const_reverse_iterator(wsfront), + [](int c){ return std::isspace(c);} ).base()); +} + +void uhd_msg_handler(uhd::msg::type_t type, const std::string &msg) +{ + if (type == uhd::msg::warning) { + etiLog.level(warn) << "UHD Warning: " << msg; + } + else if (type == uhd::msg::error) { + etiLog.level(error) << "UHD Error: " << msg; + } + else { + // do not print very short U messages and such + if (stringtrim(msg).size() != 1) { + etiLog.level(debug) << "UHD Message: " << msg; + } + } +} + +static void tune_usrp_to( + uhd::usrp::multi_usrp::sptr usrp, + double lo_offset, + double frequency) +{ + if (lo_offset != 0.0) { + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Setting freq to " << frequency << + " with LO offset " << lo_offset << "..."; + + const auto tr = uhd::tune_request_t(frequency, lo_offset); + uhd::tune_result_t result = usrp->set_tx_freq(tr); + + etiLog.level(debug) << "OutputUHD:" << + std::fixed << std::setprecision(0) << + " Target RF: " << result.target_rf_freq << + " Actual RF: " << result.actual_rf_freq << + " Target DSP: " << result.target_dsp_freq << + " Actual DSP: " << result.actual_dsp_freq; + } + else { + //set the centre frequency + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Setting freq to " << frequency << "..."; + usrp->set_tx_freq(frequency); + } + + usrp->set_rx_freq(frequency); +} + +// Check function for GPS TIMELOCK sensor from the ODR LEA-M8F board GPSDO +bool check_gps_timelock(uhd::usrp::multi_usrp::sptr usrp) +{ + try { + std::string sensor_value( + usrp->get_mboard_sensor("gps_timelock", 0).to_pp_string()); + + if (sensor_value.find("TIME LOCKED") == std::string::npos) { + etiLog.level(warn) << "OutputUHD: gps_timelock " << sensor_value; + return false; + } + + return true; + } + catch (uhd::lookup_error &e) { + etiLog.level(warn) << "OutputUHD: no gps_timelock sensor"; + return false; + } +} + +// Check function for GPS LOCKED sensor from the Ettus GPSDO +bool check_gps_locked(uhd::usrp::multi_usrp::sptr usrp) +{ + try { + uhd::sensor_value_t sensor_value( + usrp->get_mboard_sensor("gps_locked", 0)); + if (not sensor_value.to_bool()) { + etiLog.level(warn) << "OutputUHD: gps_locked " << + sensor_value.to_pp_string(); + return false; + } + + return true; + } + catch (uhd::lookup_error &e) { + etiLog.level(warn) << "OutputUHD: no gps_locked sensor"; + return false; + } +} + + +OutputUHD::OutputUHD( + OutputUHDConfig& config) : + ModOutput(), + RemoteControllable("uhd"), + myConf(config), + // Since we don't know the buffer size, we cannot initialise + // the buffers at object initialisation. + myDelayBuf(0), + running(false) +{ + myConf.muting = true; // is remote-controllable, and reset by the GPS fix check + + // Variables needed for GPS fix check + first_gps_fix_check.tv_sec = 0; + last_gps_fix_check.tv_sec = 0; + time_last_frame.tv_sec = 0; + + + std::stringstream device; + device << myConf.device; + + if (myConf.masterClockRate != 0) { + if (device.str() != "") { + device << ","; + } + device << "master_clock_rate=" << myConf.masterClockRate; + } + + if (myConf.usrpType != "") { + if (device.str() != "") { + device << ","; + } + device << "type=" << myConf.usrpType; + } + + MDEBUG("OutputUHD::OutputUHD(device: %s) @ %p\n", + device.str().c_str(), this); + + /* register the parameters that can be remote controlled */ + RC_ADD_PARAMETER(txgain, "UHD analog daughterboard TX gain"); + RC_ADD_PARAMETER(rxgain, "UHD analog daughterboard RX gain for DPD feedback"); + RC_ADD_PARAMETER(freq, "UHD transmission frequency"); + RC_ADD_PARAMETER(muting, "Mute the output by stopping the transmitter"); + RC_ADD_PARAMETER(underruns, "Read-only counter of number of underruns"); + RC_ADD_PARAMETER(latepackets, "Read-only counter of number of late packets"); + RC_ADD_PARAMETER(frames, "Read-only counter of number of frames modulated"); + + uhd::msg::register_handler(uhd_msg_handler); + + uhd::set_thread_priority_safe(); + + etiLog.log(info, "OutputUHD:Creating the usrp device with: %s...", + device.str().c_str()); + + myUsrp = uhd::usrp::multi_usrp::make(device.str()); + + etiLog.log(info, "OutputUHD:Using device: %s...", + myUsrp->get_pp_string().c_str()); + + if (myConf.masterClockRate != 0.0) { + double master_clk_rate = myUsrp->get_master_clock_rate(); + etiLog.log(debug, "OutputUHD:Checking master clock rate: %f...", + master_clk_rate); + + if (fabs(master_clk_rate - myConf.masterClockRate) > + (myConf.masterClockRate * 1e-6)) { + throw std::runtime_error("Cannot set USRP master_clock_rate. Aborted."); + } + } + + MDEBUG("OutputUHD:Setting REFCLK and PPS input...\n"); + + if (myConf.refclk_src == "gpsdo-ettus") { + myUsrp->set_clock_source("gpsdo"); + } + else { + myUsrp->set_clock_source(myConf.refclk_src); + } + myUsrp->set_time_source(myConf.pps_src); + + if (myConf.subDevice != "") { + myUsrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t(myConf.subDevice), + uhd::usrp::multi_usrp::ALL_MBOARDS); + } + + etiLog.level(debug) << "UHD clock source is " << myUsrp->get_clock_source(0); + + etiLog.level(debug) << "UHD time source is " << myUsrp->get_time_source(0); + + myUsrp->set_tx_rate(myConf.sampleRate); + etiLog.log(debug, "OutputUHD:Set rate to %d. Actual TX Rate: %f sps...", + myConf.sampleRate, myUsrp->get_tx_rate()); + + if (fabs(myUsrp->get_tx_rate() / myConf.sampleRate) > + myConf.sampleRate * 1e-6) { + throw std::runtime_error("Cannot set USRP sample rate. Aborted."); + } + + tune_usrp_to(myUsrp, myConf.lo_offset, myConf.frequency); + + myConf.frequency = myUsrp->get_tx_freq(); + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Actual TX frequency: " << myConf.frequency; + + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Actual RX frequency: " << myUsrp->get_tx_freq(); + + myUsrp->set_tx_gain(myConf.txgain); + etiLog.log(debug, "OutputUHD:Actual TX Gain: %f", myUsrp->get_tx_gain()); + + etiLog.log(debug, "OutputUHD:Mute on missing timestamps: %s", + myConf.muteNoTimestamps ? "enabled" : "disabled"); + + // preparing output thread worker data + sourceContainsTimestamp = false; + + SetDelayBuffer(myConf.dabMode); + + myUsrp->set_rx_rate(myConf.sampleRate); + etiLog.log(debug, "OutputUHD:Actual RX Rate: %f sps.", myUsrp->get_rx_rate()); + + myUsrp->set_rx_antenna("RX2"); + etiLog.log(debug, "OutputUHD:Set RX Antenna: %s", + myUsrp->get_rx_antenna().c_str()); + + myUsrp->set_rx_gain(myConf.rxgain); + etiLog.log(debug, "OutputUHD:Actual RX Gain: %f", myUsrp->get_rx_gain()); + + uhdFeedback = std::make_shared<OutputUHDFeedback>( + myUsrp, myConf.dpdFeedbackServerPort, myConf.sampleRate); + + MDEBUG("OutputUHD:UHD ready.\n"); +} + +bool OutputUHD::refclk_loss_needs_check() const +{ + if (suppress_refclk_loss_check) { + return false; + } + return myConf.refclk_src != "internal"; +} + +bool OutputUHD::gpsfix_needs_check() const +{ + if (myConf.refclk_src == "internal") { + return false; + } + else if (myConf.refclk_src == "gpsdo") { + return (myConf.maxGPSHoldoverTime != 0); + } + else if (myConf.refclk_src == "gpsdo-ettus") { + return (myConf.maxGPSHoldoverTime != 0); + } + else { + return false; + } +} + +bool OutputUHD::gpsdo_is_ettus() const +{ + return (myConf.refclk_src == "gpsdo-ettus"); +} + +OutputUHD::~OutputUHD() +{ + stop_threads(); +} + +void OutputUHD::stop_threads() +{ + running.store(false); + uhd_thread.interrupt(); + uhd_thread.join(); + async_rx_thread.join(); +} + + +void OutputUHD::setETISource(EtiSource *etiSource) +{ + myEtiSource = etiSource; +} + +int transmission_frame_duration_ms(unsigned int dabMode) +{ + switch (dabMode) { + // could happen when called from constructor and we take the mode from ETI + case 0: return 0; + + case 1: return 96; + case 2: return 24; + case 3: return 24; + case 4: return 48; + default: + throw std::runtime_error("OutputUHD: invalid DAB mode"); + } +} + +void OutputUHD::SetDelayBuffer(unsigned int dabMode) +{ + // find out the duration of the transmission frame (Table 2 in ETSI 300 401) + myTFDurationMs = transmission_frame_duration_ms(dabMode); + + // The buffer size equals the number of samples per transmission frame so + // we calculate it by multiplying the duration of the transmission frame + // with the samplerate. + myDelayBuf.resize(myTFDurationMs * myConf.sampleRate / 1000); +} + +int OutputUHD::process(Buffer* dataIn) +{ + if (not gps_fix_verified) { + if (gpsfix_needs_check()) { + initial_gps_check(); + + if (num_checks_without_gps_fix == 0) { + set_usrp_time(); + gps_fix_verified = true; + myConf.muting = false; + } + } + else { + set_usrp_time(); + gps_fix_verified = true; + myConf.muting = false; + } + } + else { + if (first_run) { + etiLog.level(debug) << "OutputUHD: UHD initialising..."; + + // we only set the delay buffer from the dab mode signaled in ETI if the + // dab mode was not set in contructor + if (myTFDurationMs == 0) { + SetDelayBuffer(myEtiSource->getMode()); + } + + running.store(true); + uhd_thread = boost::thread(&OutputUHD::workerthread, this); + async_rx_thread = boost::thread( + &OutputUHD::print_async_thread, this); + + lastLen = dataIn->getLength(); + first_run = false; + etiLog.level(debug) << "OutputUHD: UHD initialising complete"; + } + + if (lastLen != dataIn->getLength()) { + // I expect that this never happens. + etiLog.level(emerg) << + "OutputUHD: Fatal error, input length changed from " << lastLen << + " to " << dataIn->getLength(); + throw std::runtime_error("Non-constant input length!"); + } + + sourceContainsTimestamp = myConf.enableSync and + myEtiSource->sourceContainsTimestamp(); + + if (gpsfix_needs_check()) { + try { + check_gps(); + } + catch (std::runtime_error& e) { + running.store(false); + etiLog.level(error) << e.what(); + } + } + + // Prepare the frame for the worker + UHDWorkerFrameData frame; + frame.buf.resize(dataIn->getLength()); + + std::copy(pInData, pInData + dataIn->getLength(), + frame.buf.begin()); + + myEtiSource->calculateTimestamp(frame.ts); + + if (not running.load()) { + uhd_thread.interrupt(); + uhd_thread.join(); + async_rx_thread.join(); + first_run = true; + + etiLog.level(error) << "OutputUHD UHD worker failed"; + throw std::runtime_error("UHD worker failed"); + } + + if (frame.ts.fct == -1) { + etiLog.level(info) << + "OutputUHD: dropping one frame with invalid FCT"; + } + else { + try { + uhdFeedback->set_tx_frame(frame.buf, frame.ts); + } + catch (const runtime_error& e) { + etiLog.level(warn) << + "OutputUHD: Feedback server failed, restarting..."; + + uhdFeedback = std::make_shared<OutputUHDFeedback>( + myUsrp, myConf.dpdFeedbackServerPort, myConf.sampleRate); + } + + size_t num_frames = frames.push_wait_if_full(frame, + FRAMES_MAX_SIZE); + etiLog.log(trace, "UHD,push %zu", num_frames); + } + } + + return dataIn->getLength(); +} + + +void OutputUHD::set_usrp_time() +{ + if (myConf.enableSync and (myConf.pps_src == "none")) { + etiLog.level(warn) << + "OutputUHD: WARNING:" + " you are using synchronous transmission without PPS input!"; + + struct timespec now; + if (clock_gettime(CLOCK_REALTIME, &now)) { + perror("OutputUHD:Error: could not get time: "); + etiLog.level(error) << "OutputUHD: could not get time"; + } + else { + myUsrp->set_time_now(uhd::time_spec_t(now.tv_sec)); + etiLog.level(info) << "OutputUHD: Setting USRP time to " << + std::fixed << + uhd::time_spec_t(now.tv_sec).get_real_secs(); + } + } + + if (myConf.pps_src != "none") { + /* handling time for synchronisation: wait until the next full + * second, and set the USRP time at next PPS */ + struct timespec now; + time_t seconds; + if (clock_gettime(CLOCK_REALTIME, &now)) { + etiLog.level(error) << "OutputUHD: could not get time :" << + strerror(errno); + throw std::runtime_error("OutputUHD: could not get time."); + } + else { + seconds = now.tv_sec; + + MDEBUG("OutputUHD:sec+1: %ld ; now: %ld ...\n", seconds+1, now.tv_sec); + while (seconds + 1 > now.tv_sec) { + usleep(1); + if (clock_gettime(CLOCK_REALTIME, &now)) { + etiLog.level(error) << "OutputUHD: could not get time :" << + strerror(errno); + throw std::runtime_error("OutputUHD: could not get time."); + } + } + MDEBUG("OutputUHD:sec+1: %ld ; now: %ld ...\n", seconds+1, now.tv_sec); + /* We are now shortly after the second change. */ + + usleep(200000); // 200ms, we want the PPS to be later + myUsrp->set_time_unknown_pps(uhd::time_spec_t(seconds + 2)); + etiLog.level(info) << "OutputUHD: Setting USRP time next pps to " << + std::fixed << + uhd::time_spec_t(seconds + 2).get_real_secs(); + } + + usleep(1e6); + etiLog.log(info, "OutputUHD: USRP time %f\n", + myUsrp->get_time_now().get_real_secs()); + } +} + +void OutputUHD::initial_gps_check() +{ + if (first_gps_fix_check.tv_sec == 0) { + etiLog.level(info) << "Waiting for GPS fix"; + + if (clock_gettime(CLOCK_MONOTONIC, &first_gps_fix_check) != 0) { + stringstream ss; + ss << "clock_gettime failure: " << strerror(errno); + throw std::runtime_error(ss.str()); + } + } + + check_gps(); + + if (last_gps_fix_check.tv_sec > + first_gps_fix_check.tv_sec + initial_gps_fix_wait) { + stringstream ss; + ss << "GPS did not show time lock in " << initial_gps_fix_wait << " seconds"; + throw std::runtime_error(ss.str()); + } + + if (time_last_frame.tv_sec == 0) { + if (clock_gettime(CLOCK_MONOTONIC, &time_last_frame) != 0) { + stringstream ss; + ss << "clock_gettime failure: " << strerror(errno); + throw std::runtime_error(ss.str()); + } + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + stringstream ss; + ss << "clock_gettime failure: " << strerror(errno); + throw std::runtime_error(ss.str()); + } + + long delta_us = timespecdiff_us(time_last_frame, now); + long wait_time_us = transmission_frame_duration_ms(myConf.dabMode); + + if (wait_time_us - delta_us > 0) { + usleep(wait_time_us - delta_us); + } + + time_last_frame.tv_nsec += wait_time_us * 1000; + if (time_last_frame.tv_nsec >= 1000000000L) { + time_last_frame.tv_nsec -= 1000000000L; + time_last_frame.tv_sec++; + } +} + +void OutputUHD::check_gps() +{ + struct timespec time_now; + if (clock_gettime(CLOCK_MONOTONIC, &time_now) != 0) { + stringstream ss; + ss << "clock_gettime failure: " << strerror(errno); + throw std::runtime_error(ss.str()); + } + + // Divide interval by two because we alternate between + // launch and check + if (gpsfix_needs_check() and + last_gps_fix_check.tv_sec + gps_fix_check_interval/2.0 < + time_now.tv_sec) { + last_gps_fix_check = time_now; + + // Alternate between launching thread and checking the + // result. + if (gps_fix_task.joinable()) { + if (gps_fix_future.has_value()) { + + gps_fix_future.wait(); + + gps_fix_task.join(); + + if (not gps_fix_future.get()) { + if (num_checks_without_gps_fix == 0) { + etiLog.level(alert) << + "OutputUHD: GPS Time Lock lost"; + } + num_checks_without_gps_fix++; + } + else { + if (num_checks_without_gps_fix) { + etiLog.level(info) << + "OutputUHD: GPS Time Lock recovered"; + } + num_checks_without_gps_fix = 0; + } + + if (gps_fix_check_interval * num_checks_without_gps_fix > + myConf.maxGPSHoldoverTime) { + std::stringstream ss; + ss << "Lost GPS Time Lock for " << gps_fix_check_interval * + num_checks_without_gps_fix << " seconds"; + throw std::runtime_error(ss.str()); + } + } + } + else { + // Checking the sensor here takes too much + // time, it has to be done in a separate thread. + if (gpsdo_is_ettus()) { + gps_fix_pt = boost::packaged_task<bool>( + boost::bind(check_gps_locked, myUsrp) ); + } + else { + gps_fix_pt = boost::packaged_task<bool>( + boost::bind(check_gps_timelock, myUsrp) ); + } + gps_fix_future = gps_fix_pt.get_future(); + + gps_fix_task = boost::thread(boost::move(gps_fix_pt)); + } + } +} + +void OutputUHD::workerthread() +{ + // Set thread priority to realtime + if (int ret = set_realtime_prio(1)) { + etiLog.level(error) << "Could not set priority for UHD worker:" << ret; + } + + set_thread_name("uhdworker"); + + last_tx_time_initialised = false; + + uhd::stream_args_t stream_args("fc32"); //complex floats + myTxStream = myUsrp->get_tx_stream(stream_args); + + md.start_of_burst = false; + md.end_of_burst = false; + + num_underflows = 0; + num_late_packets = 0; + + size_t last_num_underflows = 0; + size_t pop_prebuffering = FRAMES_MAX_SIZE; + + while (running.load()) { + md.has_time_spec = false; + md.time_spec = uhd::time_spec_t(0.0); + + struct UHDWorkerFrameData frame; + etiLog.log(trace, "UHD,wait"); + frames.wait_and_pop(frame, pop_prebuffering); + etiLog.log(trace, "UHD,pop"); + + handle_frame(&frame); + num_frames_modulated++; + + /* Ensure we fill frames after every underrun and + * at startup to reduce underrun likelihood. */ + if (last_num_underflows < num_underflows) { + pop_prebuffering = FRAMES_MAX_SIZE; + } + else { + pop_prebuffering = 1; + } + last_num_underflows = num_underflows; + } + running.store(false); + etiLog.level(warn) << "UHD worker terminated"; +} + +void OutputUHD::handle_frame(const struct UHDWorkerFrameData *frame) +{ + // Transmit timeout + static const double tx_timeout = 20.0; + + // Check for ref_lock + if (refclk_loss_needs_check()) { + try { + if (not myUsrp->get_mboard_sensor("ref_locked", 0).to_bool()) { + etiLog.log(alert, + "OutputUHD: External reference clock lock lost !"); + if (myConf.refclk_lock_loss_behaviour == CRASH) { + throw std::runtime_error( + "OutputUHD: External reference clock lock lost."); + } + } + } + catch (uhd::lookup_error &e) { + suppress_refclk_loss_check = true; + etiLog.log(warn, "OutputUHD: This USRP does not have mboard " + "sensor for ext clock loss. Check disabled."); + } + } + + double usrp_time = myUsrp->get_time_now().get_real_secs(); + bool timestamp_discontinuity = false; + + if (sourceContainsTimestamp) { + // Tx time from MNSC and TIST + uint32_t tx_second = frame->ts.timestamp_sec; + uint32_t tx_pps = frame->ts.timestamp_pps; + + if (!frame->ts.timestamp_valid) { + /* We have not received a full timestamp through + * MNSC. We sleep through the frame. + */ + etiLog.level(info) << + "OutputUHD: Throwing sample " << frame->ts.fct << + " away: incomplete timestamp " << tx_second << + " / " << tx_pps; + usleep(20000); //TODO should this be TM-dependant ? + return; + } + + if (last_tx_time_initialised) { + const size_t sizeIn = frame->buf.size() / sizeof(complexf); + uint64_t increment = (uint64_t)sizeIn * 16384000ul / + (uint64_t)myConf.sampleRate; + // samps * ticks/s / (samps/s) + // (samps * ticks * s) / (s * samps) + // ticks + + uint32_t expected_sec = last_tx_second + increment / 16384000ul; + uint32_t expected_pps = last_tx_pps + increment % 16384000ul; + + while (expected_pps >= 16384000) { + expected_sec++; + expected_pps -= 16384000; + } + + if (expected_sec != tx_second or + expected_pps != tx_pps) { + etiLog.level(warn) << "OutputUHD: timestamp irregularity!" << + std::fixed << + " Expected " << + expected_sec << "+" << (double)expected_pps/16384000.0 << + "(" << expected_pps << ")" << + " Got " << + tx_second << "+" << (double)tx_pps/16384000.0 << + "(" << tx_pps << ")"; + + timestamp_discontinuity = true; + } + } + + last_tx_second = tx_second; + last_tx_pps = tx_pps; + last_tx_time_initialised = true; + + double pps_offset = tx_pps / 16384000.0; + + md.has_time_spec = true; + md.time_spec = uhd::time_spec_t(tx_second, pps_offset); + etiLog.log(trace, "UHD,tist %f", md.time_spec.get_real_secs()); + + // md is defined, let's do some checks + if (md.time_spec.get_real_secs() + tx_timeout < usrp_time) { + etiLog.level(warn) << + "OutputUHD: Timestamp in the past! offset: " << + std::fixed << + md.time_spec.get_real_secs() - usrp_time << + " (" << usrp_time << ")" + " frame " << frame->ts.fct << + ", tx_second " << tx_second << + ", pps " << pps_offset; + return; + } + + if (md.time_spec.get_real_secs() > usrp_time + TIMESTAMP_ABORT_FUTURE) { + etiLog.level(error) << + "OutputUHD: Timestamp way too far in the future! offset: " << + std::fixed << + md.time_spec.get_real_secs() - usrp_time; + throw std::runtime_error("Timestamp error. Aborted."); + } + } + else { // !sourceContainsTimestamp + if (myConf.muting or myConf.muteNoTimestamps) { + /* There was some error decoding the timestamp */ + if (myConf.muting) { + etiLog.log(info, + "OutputUHD: Muting sample %d requested\n", + frame->ts.fct); + } + else { + etiLog.log(info, + "OutputUHD: Muting sample %d : no timestamp\n", + frame->ts.fct); + } + usleep(20000); + return; + } + } + + tx_frame(frame, timestamp_discontinuity); +} + +void OutputUHD::tx_frame(const struct UHDWorkerFrameData *frame, bool ts_update) +{ + const double tx_timeout = 20.0; + const size_t sizeIn = frame->buf.size() / sizeof(complexf); + const complexf* in_data = reinterpret_cast<const complexf*>(&frame->buf[0]); + + size_t usrp_max_num_samps = myTxStream->get_max_num_samps(); + size_t num_acc_samps = 0; //number of accumulated samples + while (running.load() and (not myConf.muting) and (num_acc_samps < sizeIn)) { + size_t samps_to_send = std::min(sizeIn - num_acc_samps, usrp_max_num_samps); + + uhd::tx_metadata_t md_tx = md; + + //ensure the the last packet has EOB set if the timestamps has been + //refreshed and need to be reconsidered. + md_tx.end_of_burst = ( + sourceContainsTimestamp and + (frame->ts.timestamp_refresh or ts_update) and + samps_to_send <= usrp_max_num_samps ); + + + //send a single packet + size_t num_tx_samps = myTxStream->send( + &in_data[num_acc_samps], + samps_to_send, md_tx, tx_timeout); + etiLog.log(trace, "UHD,sent %zu of %zu", num_tx_samps, samps_to_send); + + num_acc_samps += num_tx_samps; + + md_tx.time_spec = md.time_spec + + uhd::time_spec_t(0, num_tx_samps/myConf.sampleRate); + + if (num_tx_samps == 0) { + etiLog.log(warn, + "OutputUHD::workerthread() unable to write to device, skipping frame!\n"); + break; + } + } +} + +void OutputUHD::print_async_thread() +{ + while (running.load()) { + uhd::async_metadata_t async_md; + if (myUsrp->get_device()->recv_async_msg(async_md, 1)) { + const char* uhd_async_message = ""; + bool failure = false; + switch (async_md.event_code) { + case uhd::async_metadata_t::EVENT_CODE_BURST_ACK: + break; + case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW: + uhd_async_message = "Underflow"; + num_underflows++; + break; + case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR: + uhd_async_message = "Packet loss between host and device."; + failure = true; + break; + case uhd::async_metadata_t::EVENT_CODE_TIME_ERROR: + uhd_async_message = "Packet had time that was late."; + num_late_packets++; + break; + case uhd::async_metadata_t::EVENT_CODE_UNDERFLOW_IN_PACKET: + uhd_async_message = "Underflow occurred inside a packet."; + failure = true; + break; + case uhd::async_metadata_t::EVENT_CODE_SEQ_ERROR_IN_BURST: + uhd_async_message = "Packet loss within a burst."; + failure = true; + break; + default: + uhd_async_message = "unknown event code"; + failure = true; + break; + } + + if (failure) { + etiLog.level(alert) << + "Received Async UHD Message '" << + uhd_async_message << "' at time " << + md.time_spec.get_real_secs(); + + } + } + + auto time_now = std::chrono::steady_clock::now(); + if (last_print_time + std::chrono::seconds(1) < time_now) { + const double usrp_time = + myUsrp->get_time_now().get_real_secs(); + + if ( (num_underflows > num_underflows_previous) or + (num_late_packets > num_late_packets_previous)) { + etiLog.log(info, + "OutputUHD status (usrp time: %f): " + "%d underruns and %d late packets since last status.\n", + usrp_time, + num_underflows, num_late_packets); + } + + num_underflows_previous = num_underflows; + num_late_packets_previous = num_late_packets; + + last_print_time = time_now; + } + } +} + +// ======================================= +// Remote Control for UHD +// ======================================= +void OutputUHD::set_parameter(const string& parameter, const string& value) +{ + stringstream ss(value); + ss.exceptions ( stringstream::failbit | stringstream::badbit ); + + if (parameter == "txgain") { + ss >> myConf.txgain; + myUsrp->set_tx_gain(myConf.txgain); + } + else if (parameter == "rxgain") { + ss >> myConf.rxgain; + myUsrp->set_rx_gain(myConf.rxgain); + } + else if (parameter == "freq") { + ss >> myConf.frequency; + tune_usrp_to(myUsrp, myConf.lo_offset, myConf.frequency); + myConf.frequency = myUsrp->get_tx_freq(); + } + else if (parameter == "muting") { + ss >> myConf.muting; + } + else if (parameter == "underruns" or + parameter == "latepackets" or + parameter == "frames") { + throw ParameterError("Parameter " + parameter + " is read-only."); + } + else { + stringstream ss; + ss << "Parameter '" << parameter + << "' is not exported by controllable " << get_rc_name(); + throw ParameterError(ss.str()); + } +} + +const string OutputUHD::get_parameter(const string& parameter) const +{ + stringstream ss; + if (parameter == "txgain") { + ss << myConf.txgain; + } + else if (parameter == "rxgain") { + ss << myConf.rxgain; + } + else if (parameter == "freq") { + ss << myConf.frequency; + } + else if (parameter == "muting") { + ss << myConf.muting; + } + else if (parameter == "underruns") { + ss << num_underflows; + } + else if (parameter == "latepackets") { + ss << num_late_packets; + } + else if (parameter == "frames") { + ss << num_frames_modulated; + } + else { + ss << "Parameter '" << parameter << + "' is not exported by controllable " << get_rc_name(); + throw ParameterError(ss.str()); + } + return ss.str(); +} + +#endif // HAVE_OUTPUT_UHD + diff --git a/src/output/UHD.h b/src/output/UHD.h new file mode 100644 index 0000000..e3f8174 --- /dev/null +++ b/src/output/UHD.h @@ -0,0 +1,244 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2017 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + It is an output driver for the USRP family of devices, and uses the UHD + library. +*/ + +/* + This file is part of ODR-DabMod. + + ODR-DabMod is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + ODR-DabMod is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with ODR-DabMod. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#ifdef HAVE_OUTPUT_UHD + +#include <uhd/utils/thread_priority.hpp> +#include <uhd/utils/safe_main.hpp> +#include <uhd/usrp/multi_usrp.hpp> +#include <boost/thread.hpp> +#include <deque> +#include <chrono> +#include <memory> +#include <string> +#include <atomic> + +#include "Log.h" +#include "output/SDR.h" +#include "TimestampDecoder.h" +#include "RemoteControl.h" +#include "ThreadsafeQueue.h" +#include "OutputUHDFeedback.h" + +#include <stdio.h> +#include <sys/types.h> + +//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args) +#define MDEBUG(fmt, args...) + +// If the timestamp is further in the future than +// 100 seconds, abort +#define TIMESTAMP_ABORT_FUTURE 100 + +// Add a delay to increase buffers when +// frames are too far in the future +#define TIMESTAMP_MARGIN_FUTURE 0.5 + +namespace Output { + +enum refclk_lock_loss_behaviour_t { CRASH, IGNORE }; + +/* This structure is used as initial configuration for OutputUHD. + * It must also contain all remote-controllable settings, otherwise + * they will get lost on a modulator restart. */ +struct OutputUHDConfig { + std::string device; + std::string usrpType; // e.g. b100, b200, usrp2 + + // The USRP1 can accept two daughterboards + std::string subDevice; // e.g. A:0 + + long masterClockRate = 32768000; + unsigned sampleRate = 2048000; + double frequency = 0.0; + double lo_offset = 0.0; + double txgain = 0.0; + double rxgain = 0.0; + bool enableSync = false; + + // When working with timestamps, mute the frames that + // do not have a timestamp + bool muteNoTimestamps = false; + unsigned dabMode = 0; + unsigned maxGPSHoldoverTime = 0; + + /* allowed values : auto, int, sma, mimo */ + std::string refclk_src; + + /* allowed values : int, sma, mimo */ + std::string pps_src; + + /* allowed values : pos, neg */ + std::string pps_polarity; + + /* What to do when the reference clock PLL loses lock */ + refclk_lock_loss_behaviour_t refclk_lock_loss_behaviour; + + // muting can only be changed using the remote control + bool muting = false; + + // TCP port on which to serve TX and RX samples for the + // digital pre distortion learning tool + uint16_t dpdFeedbackServerPort = 0; +}; + +class OutputUHD: public ModOutput, public RemoteControllable { + public: + OutputUHD(OutputUHDConfig& config); + OutputUHD(const OutputUHD& other) = delete; + OutputUHD operator=(const OutputUHD& other) = delete; + ~OutputUHD(); + + int process(Buffer* dataIn); + + const char* name() { return "OutputUHD"; } + + void setETISource(EtiSource *etiSource); + + /*********** REMOTE CONTROL ***************/ + + /* Base function to set parameters. */ + virtual void set_parameter(const std::string& parameter, + const std::string& value); + + /* Getting a parameter always returns a string. */ + virtual const std::string get_parameter( + const std::string& parameter) const; + + protected: + EtiSource *myEtiSource = nullptr; + OutputUHDConfig& myConf; + uhd::usrp::multi_usrp::sptr myUsrp; + std::shared_ptr<boost::barrier> mySyncBarrier; + bool first_run = true; + bool gps_fix_verified = false; + std::shared_ptr<OutputUHDFeedback> uhdFeedback; + + private: + // Each frame contains one OFDM frame, and its + // associated timestamp + struct UHDWorkerFrameData { + // Buffer holding frame data + std::vector<uint8_t> buf; + + // A full timestamp contains a TIST according to standard + // and time information within MNSC with tx_second. + struct frame_timestamp ts; + }; + + // Resize the internal delay buffer according to the dabMode and + // the sample rate. + void SetDelayBuffer(unsigned int dabMode); + + // data + // The remote-controllable static delay is in the OutputUHDConfig + int myTFDurationMs; // TF duration in milliseconds + std::vector<complexf> myDelayBuf; + size_t lastLen = 0; + + // GPS Fix check variables + int num_checks_without_gps_fix = 1; + struct timespec first_gps_fix_check; + struct timespec last_gps_fix_check; + struct timespec time_last_frame; + boost::packaged_task<bool> gps_fix_pt; + boost::unique_future<bool> gps_fix_future; + boost::thread gps_fix_task; + + // Wait time in seconds to get fix + static const int initial_gps_fix_wait = 180; + + // Interval for checking the GPS at runtime + static constexpr double gps_fix_check_interval = 10.0; // seconds + + // Asynchronous message statistics + size_t num_underflows = 0; + size_t num_late_packets = 0; + size_t num_underflows_previous = 0; + size_t num_late_packets_previous = 0; + + size_t num_frames_modulated = 0; + + uhd::tx_metadata_t md; + bool last_tx_time_initialised = false; + uint32_t last_tx_second = 0; + uint32_t last_tx_pps = 0; + + // Used to print statistics once a second + std::chrono::steady_clock::time_point last_print_time; + + bool sourceContainsTimestamp = false; + + ThreadsafeQueue<UHDWorkerFrameData> frames; + + // Returns true if we want to verify loss of refclk + bool refclk_loss_needs_check(void) const; + bool suppress_refclk_loss_check = false; + + // Returns true if we want to check for the gps_timelock sensor + bool gpsfix_needs_check(void) const; + + // Return true if the gpsdo is from ettus, false if it is the ODR + // LEA-M8F board is used + bool gpsdo_is_ettus(void) const; + + std::atomic<bool> running; + boost::thread uhd_thread; + boost::thread async_rx_thread; + void stop_threads(void); + + uhd::tx_streamer::sptr myTxStream; + + // The worker thread decouples the modulator from UHD + void workerthread(); + void handle_frame(const struct UHDWorkerFrameData *frame); + void tx_frame(const struct UHDWorkerFrameData *frame, bool ts_update); + + // Poll asynchronous metadata from UHD + void print_async_thread(void); + + void check_gps(); + + void set_usrp_time(); + + void initial_gps_check(); +}; + +} // namespace Output + +#endif // HAVE_OUTPUT_UHD + |