diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/ConfigParser.cpp | 87 | ||||
-rw-r--r-- | src/ConfigParser.h | 17 | ||||
-rw-r--r-- | src/DabMod.cpp | 63 | ||||
-rw-r--r-- | src/DabModulator.cpp | 56 | ||||
-rw-r--r-- | src/OutputSoapy.cpp | 287 | ||||
-rw-r--r-- | src/OutputSoapy.h | 138 | ||||
-rw-r--r-- | src/OutputUHD.cpp | 1035 | ||||
-rw-r--r-- | src/OutputUHD.h | 250 | ||||
-rw-r--r-- | src/TimestampDecoder.h | 14 | ||||
-rw-r--r-- | src/Utils.cpp | 14 | ||||
-rw-r--r-- | src/Utils.h | 4 | ||||
-rw-r--r-- | src/output/Feedback.cpp (renamed from src/OutputUHDFeedback.cpp) | 80 | ||||
-rw-r--r-- | src/output/Feedback.h (renamed from src/OutputUHDFeedback.h) | 33 | ||||
-rw-r--r-- | src/output/SDR.cpp | 433 | ||||
-rw-r--r-- | src/output/SDR.h | 97 | ||||
-rw-r--r-- | src/output/SDRDevice.h | 136 | ||||
-rw-r--r-- | src/output/Soapy.cpp | 264 | ||||
-rw-r--r-- | src/output/Soapy.h | 98 | ||||
-rw-r--r-- | src/output/UHD.cpp | 474 | ||||
-rw-r--r-- | src/output/UHD.h | 129 | ||||
-rw-r--r-- | src/output/USRPTime.cpp | 280 | ||||
-rw-r--r-- | src/output/USRPTime.h | 116 |
22 files changed, 2215 insertions, 1890 deletions
diff --git a/src/ConfigParser.cpp b/src/ConfigParser.cpp index 2c93a57..0e641c0 100644 --- a/src/ConfigParser.cpp +++ b/src/ConfigParser.cpp @@ -29,15 +29,17 @@ # include "config.h" #endif +#include <cstdint> +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/ini_parser.hpp> + #include "ConfigParser.h" #include "porting.h" #include "Utils.h" #include "Log.h" #include "DabModulator.h" +#include "output/SDR.h" -#include <unistd.h> -#include <boost/property_tree/ptree.hpp> -#include <boost/property_tree/ini_parser.hpp> using namespace std; @@ -214,75 +216,84 @@ static void parse_configfile( } #if defined(HAVE_OUTPUT_UHD) else if (output_selected == "uhd") { - OutputUHDConfig outputuhd_conf; + Output::SDRDeviceConfig sdr_device_config; + + string device = pt.get("uhdoutput.device", ""); + const auto usrpType = pt.get("uhdoutput.type", ""); + if (usrpType != "") { + if (not device.empty()) { + device += ","; + } + device += "type=" + usrpType; + } + sdr_device_config.device = device; - outputuhd_conf.device = pt.get("uhdoutput.device", ""); - outputuhd_conf.usrpType = pt.get("uhdoutput.type", ""); - outputuhd_conf.subDevice = pt.get("uhdoutput.subdevice", ""); - outputuhd_conf.masterClockRate = pt.get<long>("uhdoutput.master_clock_rate", 0); + sdr_device_config.subDevice = pt.get("uhdoutput.subdevice", ""); + sdr_device_config.masterClockRate = pt.get<long>("uhdoutput.master_clock_rate", 0); - if (outputuhd_conf.device.find("master_clock_rate") != std::string::npos) { + if (sdr_device_config.device.find("master_clock_rate") != std::string::npos) { std::cerr << "Warning:" "setting master_clock_rate in [uhd] device is deprecated !\n"; } - if (outputuhd_conf.device.find("type=") != std::string::npos) { + if (sdr_device_config.device.find("type=") != std::string::npos) { std::cerr << "Warning:" "setting type in [uhd] device is deprecated !\n"; } - outputuhd_conf.txgain = pt.get("uhdoutput.txgain", 0.0); - outputuhd_conf.rxgain = pt.get("uhdoutput.rxgain", 0.0); - outputuhd_conf.frequency = pt.get<double>("uhdoutput.frequency", 0); + sdr_device_config.txgain = pt.get("uhdoutput.txgain", 0.0); + sdr_device_config.rxgain = pt.get("uhdoutput.rxgain", 0.0); + sdr_device_config.frequency = pt.get<double>("uhdoutput.frequency", 0); std::string chan = pt.get<std::string>("uhdoutput.channel", ""); - outputuhd_conf.dabMode = mod_settings.dabMode; + sdr_device_config.dabMode = mod_settings.dabMode; - if (outputuhd_conf.frequency == 0 && chan == "") { + if (sdr_device_config.frequency == 0 && chan == "") { std::cerr << " UHD output enabled, but neither frequency nor channel defined.\n"; throw std::runtime_error("Configuration error"); } - else if (outputuhd_conf.frequency == 0) { - outputuhd_conf.frequency = parseChannel(chan); + else if (sdr_device_config.frequency == 0) { + sdr_device_config.frequency = parseChannel(chan); } - else if (outputuhd_conf.frequency != 0 && chan != "") { + else if (sdr_device_config.frequency != 0 && chan != "") { std::cerr << " UHD output: cannot define both frequency and channel.\n"; throw std::runtime_error("Configuration error"); } - outputuhd_conf.lo_offset = pt.get<double>("uhdoutput.lo_offset", 0); + sdr_device_config.lo_offset = pt.get<double>("uhdoutput.lo_offset", 0); - outputuhd_conf.refclk_src = pt.get("uhdoutput.refclk_source", "internal"); - outputuhd_conf.pps_src = pt.get("uhdoutput.pps_source", "none"); - outputuhd_conf.pps_polarity = pt.get("uhdoutput.pps_polarity", "pos"); + sdr_device_config.refclk_src = pt.get("uhdoutput.refclk_source", "internal"); + sdr_device_config.pps_src = pt.get("uhdoutput.pps_source", "none"); + sdr_device_config.pps_polarity = pt.get("uhdoutput.pps_polarity", "pos"); std::string behave = pt.get("uhdoutput.behaviour_refclk_lock_lost", "ignore"); if (behave == "crash") { - outputuhd_conf.refclk_lock_loss_behaviour = CRASH; + sdr_device_config.refclk_lock_loss_behaviour = Output::CRASH; } else if (behave == "ignore") { - outputuhd_conf.refclk_lock_loss_behaviour = IGNORE; + sdr_device_config.refclk_lock_loss_behaviour = Output::IGNORE; } else { std::cerr << "Error: UHD output: behaviour_refclk_lock_lost invalid." << std::endl; throw std::runtime_error("Configuration error"); } - outputuhd_conf.maxGPSHoldoverTime = pt.get("uhdoutput.max_gps_holdover_time", 0); + sdr_device_config.maxGPSHoldoverTime = pt.get("uhdoutput.max_gps_holdover_time", 0); - outputuhd_conf.dpdFeedbackServerPort = pt.get<long>("uhdoutput.dpd_port", 0); + sdr_device_config.dpdFeedbackServerPort = pt.get<long>("uhdoutput.dpd_port", 0); - mod_settings.outputuhd_conf = outputuhd_conf; + mod_settings.sdr_device_config = sdr_device_config; mod_settings.useUHDOutput = 1; } #endif #if defined(HAVE_SOAPYSDR) else if (output_selected == "soapysdr") { - auto& outputsoapy_conf = mod_settings.outputsoapy_conf; + auto& outputsoapy_conf = mod_settings.sdr_device_config; outputsoapy_conf.device = pt.get("soapyoutput.device", ""); outputsoapy_conf.masterClockRate = pt.get<long>("soapyoutput.master_clock_rate", 0); outputsoapy_conf.txgain = pt.get("soapyoutput.txgain", 0.0); + outputsoapy_conf.lo_offset = pt.get<double>("soapyoutput.lo_offset", 0.0); outputsoapy_conf.frequency = pt.get<double>("soapyoutput.frequency", 0); std::string chan = pt.get<std::string>("soapyoutput.channel", ""); outputsoapy_conf.dabMode = mod_settings.dabMode; @@ -315,9 +326,9 @@ static void parse_configfile( } #if defined(HAVE_OUTPUT_UHD) - mod_settings.outputuhd_conf.enableSync = (pt.get("delaymanagement.synchronous", 0) == 1); - mod_settings.outputuhd_conf.muteNoTimestamps = (pt.get("delaymanagement.mutenotimestamps", 0) == 1); - if (mod_settings.outputuhd_conf.enableSync) { + mod_settings.sdr_device_config.enableSync = (pt.get("delaymanagement.synchronous", 0) == 1); + mod_settings.sdr_device_config.muteNoTimestamps = (pt.get("delaymanagement.mutenotimestamps", 0) == 1); + if (mod_settings.sdr_device_config.enableSync) { std::string delay_mgmt = pt.get<std::string>("delaymanagement.management", ""); std::string fixedoffset = pt.get<std::string>("delaymanagement.fixedoffset", ""); std::string offset_filename = pt.get<std::string>("delaymanagement.dynamicoffsetfile", ""); @@ -392,7 +403,7 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings) break; case 'F': #if defined(HAVE_OUTPUT_UHD) - mod_settings.outputuhd_conf.frequency = strtof(optarg, NULL); + mod_settings.sdr_device_config.frequency = strtof(optarg, NULL); #endif break; case 'g': @@ -400,7 +411,7 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings) break; case 'G': #if defined(HAVE_OUTPUT_UHD) - mod_settings.outputuhd_conf.txgain = strtod(optarg, NULL); + mod_settings.sdr_device_config.txgain = strtod(optarg, NULL); #endif break; case 'l': @@ -409,7 +420,7 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings) case 'o': mod_settings.tist_offset_s = strtod(optarg, NULL); #if defined(HAVE_OUTPUT_UHD) - mod_settings.outputuhd_conf.enableSync = true; + mod_settings.sdr_device_config.enableSync = true; #endif break; case 'm': @@ -427,10 +438,10 @@ void parse_args(int argc, char **argv, mod_settings_t& mod_settings) fprintf(stderr, "Options -u and -f are mutually exclusive\n"); throw std::invalid_argument("Invalid command line options"); } - mod_settings.outputuhd_conf.device = optarg; - mod_settings.outputuhd_conf.refclk_src = "internal"; - mod_settings.outputuhd_conf.pps_src = "none"; - mod_settings.outputuhd_conf.pps_polarity = "pos"; + mod_settings.sdr_device_config.device = optarg; + mod_settings.sdr_device_config.refclk_src = "internal"; + mod_settings.sdr_device_config.pps_src = "none"; + mod_settings.sdr_device_config.pps_polarity = "pos"; mod_settings.useUHDOutput = 1; #endif break; diff --git a/src/ConfigParser.h b/src/ConfigParser.h index 0be3558..3f64883 100644 --- a/src/ConfigParser.h +++ b/src/ConfigParser.h @@ -34,12 +34,9 @@ #include <string> #include "GainControl.h" #include "TII.h" -#if defined(HAVE_OUTPUT_UHD) -# include "OutputUHD.h" -#endif -#if defined(HAVE_SOAPYSDR) -# include "OutputSoapy.h" -#endif +#include "output/SDR.h" +#include "output/UHD.h" +#include "output/Soapy.h" #define ZMQ_INPUT_MAX_FRAME_QUEUE 500 @@ -85,12 +82,8 @@ struct mod_settings_t { // Settings for the OFDM windowing unsigned ofdmWindowOverlap = 0; -#if defined(HAVE_OUTPUT_UHD) - OutputUHDConfig outputuhd_conf; -#endif - -#if defined(HAVE_SOAPYSDR) - OutputSoapyConfig outputsoapy_conf; +#if defined(HAVE_OUTPUT_UHD) || defined(HAVE_SOAPYSDR) + Output::SDRDeviceConfig sdr_device_config; #endif }; diff --git a/src/DabMod.cpp b/src/DabMod.cpp index b6f2dee..4961910 100644 --- a/src/DabMod.cpp +++ b/src/DabMod.cpp @@ -37,12 +37,9 @@ #include "OutputFile.h" #include "FormatConverter.h" #include "FrameMultiplexer.h" -#if defined(HAVE_OUTPUT_UHD) -# include "OutputUHD.h" -#endif -#if defined(HAVE_SOAPYSDR) -# include "OutputSoapy.h" -#endif +#include "output/SDR.h" +#include "output/UHD.h" +#include "output/Soapy.h" #include "OutputZeroMQ.h" #include "InputReader.h" #include "PcDebug.h" @@ -133,15 +130,15 @@ static void printModSettings(const mod_settings_t& mod_settings) else if (mod_settings.useUHDOutput) { fprintf(stderr, " UHD\n" " Device: %s\n" - " Type: %s\n" + " Subdevice: %s\n" " master_clock_rate: %ld\n" " refclk: %s\n" " pps source: %s\n", - mod_settings.outputuhd_conf.device.c_str(), - mod_settings.outputuhd_conf.usrpType.c_str(), - mod_settings.outputuhd_conf.masterClockRate, - mod_settings.outputuhd_conf.refclk_src.c_str(), - mod_settings.outputuhd_conf.pps_src.c_str()); + mod_settings.sdr_device_config.device.c_str(), + mod_settings.sdr_device_config.subDevice.c_str(), + mod_settings.sdr_device_config.masterClockRate, + mod_settings.sdr_device_config.refclk_src.c_str(), + mod_settings.sdr_device_config.pps_src.c_str()); } #endif #if defined(HAVE_SOAPYSDR) @@ -149,8 +146,8 @@ static void printModSettings(const mod_settings_t& mod_settings) fprintf(stderr, " SoapySDR\n" " Device: %s\n" " master_clock_rate: %ld\n", - mod_settings.outputsoapy_conf.device.c_str(), - mod_settings.outputsoapy_conf.masterClockRate); + mod_settings.sdr_device_config.device.c_str(), + mod_settings.sdr_device_config.masterClockRate); } #endif else if (mod_settings.useZeroMQOutput) { @@ -208,18 +205,20 @@ static shared_ptr<ModOutput> prepare_output( #if defined(HAVE_OUTPUT_UHD) else if (s.useUHDOutput) { s.normalise = 1.0f / normalise_factor; - s.outputuhd_conf.sampleRate = s.outputRate; - output = make_shared<OutputUHD>(s.outputuhd_conf); - rcs.enrol((OutputUHD*)output.get()); + s.sdr_device_config.sampleRate = s.outputRate; + auto uhddevice = make_shared<Output::UHD>(s.sdr_device_config); + output = make_shared<Output::SDR>(s.sdr_device_config, uhddevice); + rcs.enrol((Output::SDR*)output.get()); } #endif #if defined(HAVE_SOAPYSDR) else if (s.useSoapyOutput) { /* We normalise the same way as for the UHD output */ s.normalise = 1.0f / normalise_factor; - s.outputsoapy_conf.sampleRate = s.outputRate; - output = make_shared<OutputSoapy>(s.outputsoapy_conf); - rcs.enrol((OutputSoapy*)output.get()); + s.sdr_device_config.sampleRate = s.outputRate; + auto soapydevice = make_shared<Output::Soapy>(s.sdr_device_config); + output = make_shared<Output::SDR>(s.sdr_device_config, soapydevice); + rcs.enrol((Output::SDR*)output.get()); } #endif #if defined(HAVE_ZEROMQ) @@ -321,16 +320,16 @@ int launch_modulator(int argc, char* argv[]) flowgraph.connect(modulator, output); } + if (false #if defined(HAVE_OUTPUT_UHD) - if (mod_settings.useUHDOutput) { - ((OutputUHD*)output.get())->setETISource(modulator->getEtiSource()); - } + or mod_settings.useUHDOutput #endif #if defined(HAVE_SOAPYSDR) - if (mod_settings.useSoapyOutput) { - ((OutputSoapy*)output.get())->setETISource(modulator->getEtiSource()); - } + or mod_settings.useSoapyOutput #endif + ) { + ((Output::SDR*)output.get())->setETISource(modulator->getEtiSource()); + } size_t framecount = 0; @@ -415,16 +414,16 @@ int launch_modulator(int argc, char* argv[]) flowgraph.connect(modulator, output); } + if (false #if defined(HAVE_OUTPUT_UHD) - if (mod_settings.useUHDOutput) { - ((OutputUHD*)output.get())->setETISource(modulator->getEtiSource()); - } + or mod_settings.useUHDOutput #endif #if defined(HAVE_SOAPYSDR) - if (mod_settings.useSoapyOutput) { - ((OutputSoapy*)output.get())->setETISource(modulator->getEtiSource()); - } + or mod_settings.useSoapyOutput #endif + ) { + ((Output::SDR*)output.get())->setETISource(modulator->getEtiSource()); + } inputReader->PrintInfo(); diff --git a/src/DabModulator.cpp b/src/DabModulator.cpp index 0818f4f..1ea06de 100644 --- a/src/DabModulator.cpp +++ b/src/DabModulator.cpp @@ -162,10 +162,13 @@ int DabModulator::process(Buffer* dataOut) } } - auto cifCicEq = make_shared<CicEqualizer>( + shared_ptr<CicEqualizer> cifCicEq; + if (useCicEq) { + cifCicEq = make_shared<CicEqualizer>( myNbCarriers, (float)mySpacing * (float)m_settings.outputRate / 2048000.0f, cic_ratio); + } shared_ptr<TII> tii; shared_ptr<PhaseReference> tiiRef; @@ -347,43 +350,24 @@ int DabModulator::process(Buffer* dataOut) myFlowgraph->connect(tii, cifSig); } - if (useCicEq) { - myFlowgraph->connect(cifSig, cifCicEq); - myFlowgraph->connect(cifCicEq, cifOfdm); - } - else { - myFlowgraph->connect(cifSig, cifOfdm); - } - myFlowgraph->connect(cifOfdm, cifGain); - myFlowgraph->connect(cifGain, cifGuard); - - auto cifOut = cifPoly ? - static_pointer_cast<ModPlugin>(cifPoly) : - static_pointer_cast<ModPlugin>(myOutput); - - if (cifFilter) { - myFlowgraph->connect(cifGuard, cifFilter); - if (cifRes) { - myFlowgraph->connect(cifFilter, cifRes); - myFlowgraph->connect(cifRes, cifOut); - } - else { - myFlowgraph->connect(cifFilter, cifOut); + shared_ptr<ModPlugin> prev_plugin = static_pointer_cast<ModPlugin>(cifSig); + const std::list<shared_ptr<ModPlugin> > plugins({ + static_pointer_cast<ModPlugin>(cifCicEq), + static_pointer_cast<ModPlugin>(cifOfdm), + static_pointer_cast<ModPlugin>(cifGain), + static_pointer_cast<ModPlugin>(cifGuard), + static_pointer_cast<ModPlugin>(cifFilter), // optional block + static_pointer_cast<ModPlugin>(cifRes), // optional block + static_pointer_cast<ModPlugin>(cifPoly), // optional block + static_pointer_cast<ModPlugin>(myOutput), + }); + + for (auto& p : plugins) { + if (p) { + myFlowgraph->connect(prev_plugin, p); + prev_plugin = p; } } - else { - if (cifRes) { - myFlowgraph->connect(cifGuard, cifRes); - myFlowgraph->connect(cifRes, cifOut); - } - else { - myFlowgraph->connect(cifGuard, cifOut); - } - } - - if (cifPoly) { - myFlowgraph->connect(cifPoly, myOutput); - } } //////////////////////////////////////////////////////////////////// diff --git a/src/OutputSoapy.cpp b/src/OutputSoapy.cpp deleted file mode 100644 index 699501c..0000000 --- a/src/OutputSoapy.cpp +++ /dev/null @@ -1,287 +0,0 @@ -/* - 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 "OutputSoapy.h" -#ifdef HAVE_SOAPYSDR - -#include <SoapySDR/Errors.hpp> -#include <deque> -#include <chrono> - -#include "Log.h" -#include "Utils.h" - -#include <stdio.h> - -static const size_t FRAMES_MAX_SIZE = 2; - - -using namespace std; - - - -OutputSoapy::OutputSoapy(OutputSoapyConfig& config) : - ModOutput(), - RemoteControllable("soapy"), - m_conf(config), - m_device(nullptr) -{ - RC_ADD_PARAMETER(txgain, "SoapySDR analog daughterboard TX gain"); - RC_ADD_PARAMETER(freq, "SoapySDR transmission frequency"); - RC_ADD_PARAMETER(overflows, "SoapySDR overflow count [r/o]"); - RC_ADD_PARAMETER(underflows, "SoapySDR underflow count [r/o]"); - - etiLog.level(info) << - "OutputSoapy:Creating the device with: " << - config.device; - try - { - m_device = SoapySDR::Device::make(config.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(config.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) << "OutputSoapySDR:Actual TX rate: " << - m_device->getSampleRate(SOAPY_SDR_TX, 0) / 1000.0 << - " ksps."; - - m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency); - m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0); - etiLog.level(info) << "OutputSoapySDR:Actual frequency: " << - m_conf.frequency / 1000.0 << - " kHz."; - - m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain); - etiLog.level(info) << "OutputSoapySDR:Actual tx gain: " << - m_device->getGain(SOAPY_SDR_TX, 0); - -} - -OutputSoapy::~OutputSoapy() -{ - m_worker.stop(); - if (m_device != nullptr) { - SoapySDR::Device::unmake(m_device); - } -} - -void SoapyWorker::stop() -{ - running = false; - queue.push({}); - if (m_thread.joinable()) { - m_thread.join(); - } -} - -void SoapyWorker::start(SoapySDR::Device *device) -{ - m_device = device; - underflows = 0; - overflows = 0; - running = true; - m_thread = std::thread(&SoapyWorker::process_start, this); -} - -void SoapyWorker::process_start() -{ - // Set thread priority to realtime - if (int ret = set_realtime_prio(1)) { - etiLog.level(error) << "Could not set priority for SoapySDR worker:" << ret; - } - - set_thread_name("soapyworker"); - - std::vector<size_t> channels; - channels.push_back(0); - auto stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels); - m_device->activateStream(stream); - process(stream); - m_device->closeStream(stream); - running = false; - etiLog.level(warn) << "SoapySDR worker terminated"; -} - -void SoapyWorker::process(SoapySDR::Stream *stream) -{ - while (running) { - struct SoapyWorkerFrameData frame; - queue.wait_and_pop(frame); - - // The frame buffer contains bytes representing FC32 samples - const complexf *buf = reinterpret_cast<complexf*>(frame.buf.data()); - const size_t numSamples = frame.buf.size() / sizeof(complexf); - if ((frame.buf.size() % sizeof(complexf)) != 0) { - throw std::runtime_error("OutputSoapy: invalid buffer size"); - } - - // Stream MTU is in samples, not bytes. - const size_t mtu = m_device->getStreamMTU(stream); - - size_t num_acc_samps = 0; - while (running && (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(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); - running = false; - } - - num_acc_samps += ret; - } - } -} - -int OutputSoapy::process(Buffer* dataIn) -{ - if (first_run) { - m_worker.start(m_device); - first_run = false; - } - else if (!m_worker.running) { - etiLog.level(error) << "OutputSoapy: worker thread died"; - throw std::runtime_error("Fault in OutputSoapy"); - } - - SoapyWorkerFrameData frame; - m_eti_source->calculateTimestamp(frame.ts); - - - if (frame.ts.fct == -1) { - etiLog.level(info) << - "OutputSoapy: dropping one frame with invalid FCT"; - } - else { - const uint8_t* pInData = reinterpret_cast<uint8_t*>(dataIn->getData()); - frame.buf.resize(dataIn->getLength()); - std::copy(pInData, pInData + dataIn->getLength(), - frame.buf.begin()); - m_worker.queue.push_wait_if_full(frame, FRAMES_MAX_SIZE); - } - - return dataIn->getLength(); -} - - -void OutputSoapy::setETISource(EtiSource *etiSource) -{ - m_eti_source = etiSource; -} - -void OutputSoapy::set_parameter(const string& parameter, const string& value) -{ - stringstream ss(value); - ss.exceptions ( stringstream::failbit | stringstream::badbit ); - - if (parameter == "txgain") { - ss >> m_conf.txgain; - m_device->setGain(SOAPY_SDR_TX, 0, m_conf.txgain); - } - else if (parameter == "freq") { - ss >> m_conf.frequency; - m_device->setFrequency(SOAPY_SDR_TX, 0, m_conf.frequency); - m_conf.frequency = m_device->getFrequency(SOAPY_SDR_TX, 0); - } - else if (parameter == "underflows") { - throw ParameterError("Parameter 'underflows' is read-only"); - } - else if (parameter == "overflows") { - throw ParameterError("Parameter 'overflows' is read-only"); - } - else { - stringstream ss_err; - ss_err << "Parameter '" << parameter - << "' is not exported by controllable " << get_rc_name(); - throw ParameterError(ss_err.str()); - } -} - -const string OutputSoapy::get_parameter(const string& parameter) const -{ - stringstream ss; - if (parameter == "txgain") { - ss << m_conf.txgain; - } - else if (parameter == "freq") { - ss << m_conf.frequency; - } - else if (parameter == "underflows") { - ss << m_worker.underflows; - } - else if (parameter == "overflows") { - ss << m_worker.overflows; - } - else { - ss << "Parameter '" << parameter << - "' is not exported by controllable " << get_rc_name(); - throw ParameterError(ss.str()); - } - return ss.str(); -} - -#endif // HAVE_SOAPYSDR - diff --git a/src/OutputSoapy.h b/src/OutputSoapy.h deleted file mode 100644 index 230f11b..0000000 --- a/src/OutputSoapy.h +++ /dev/null @@ -1,138 +0,0 @@ -/* - 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 "ModPlugin.h" -#include "EtiReader.h" -#include "RemoteControl.h" -#include "ThreadsafeQueue.h" - -typedef std::complex<float> complexf; - -/* This structure is used as initial configuration for the Soapy output. - * It must also contain all remote-controllable settings, otherwise - * they will get lost on a modulator restart. */ -struct OutputSoapyConfig { - std::string device; - - long masterClockRate = 32768000; - unsigned sampleRate = 2048000; - double frequency = 0.0; - double txgain = 0.0; - unsigned dabMode = 0; -}; - -// Each frame contains one OFDM frame, and its -// associated timestamp -struct SoapyWorkerFrameData { - // 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; -}; - -class SoapyWorker -{ - public: - ThreadsafeQueue<SoapyWorkerFrameData> queue; - SoapySDR::Device *m_device; - std::atomic<bool> running; - size_t underflows; - size_t overflows; - - SoapyWorker() {} - SoapyWorker(const SoapyWorker&) = delete; - SoapyWorker operator=(const SoapyWorker&) = delete; - ~SoapyWorker() { stop(); } - - void start(SoapySDR::Device *device); - void stop(void); - - private: - std::thread m_thread; - - void process_start(void); - void process(SoapySDR::Stream *stream); -}; - -class OutputSoapy: public ModOutput, public RemoteControllable -{ - public: - OutputSoapy(OutputSoapyConfig& config); - OutputSoapy(const OutputSoapy& other) = delete; - OutputSoapy& operator=(const OutputSoapy& other) = delete; - ~OutputSoapy(); - - int process(Buffer* dataIn); - - const char* name() { return "OutputSoapy"; } - - 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: - SoapyWorker m_worker; - EtiSource *m_eti_source; - OutputSoapyConfig& m_conf; - - SoapySDR::Device *m_device; - - bool first_run = true; -}; - - -#endif //HAVE_SOAPYSDR diff --git a/src/OutputUHD.cpp b/src/OutputUHD.cpp deleted file mode 100644 index b533075..0000000 --- a/src/OutputUHD.cpp +++ /dev/null @@ -1,1035 +0,0 @@ -/* - 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 - myConf.staticDelayUs = 0; // is remote-controllable - - // 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(staticdelay, "Set static delay (uS) between 0 and 96000"); - 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 - sync_and_ts_valid = 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!"); - } - - sync_and_ts_valid = 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()); - - // calculate delay and fill buffer - uint32_t noSampleDelay = (myConf.staticDelayUs * (myConf.sampleRate / 1000)) / 1000; - uint32_t noByteDelay = noSampleDelay * sizeof(complexf); - - const uint8_t* pInData = (uint8_t*)dataIn->getData(); - - uint8_t *pTmp = &frame.buf[0]; - if (noByteDelay) { - // copy remain from delaybuf - memcpy(pTmp, &myDelayBuf[0], noByteDelay); - // copy new data - memcpy(&pTmp[noByteDelay], pInData, dataIn->getLength() - noByteDelay); - // copy remaining data to delay buf - memcpy(&myDelayBuf[0], &pInData[dataIn->getLength() - noByteDelay], noByteDelay); - } - else { - 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 = m_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"); - m_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 (sync_and_ts_valid) { - // 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 { // !sync_and_ts_valid - 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 = ( - sync_and_ts_valid 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 == "staticdelay") { - int64_t adjust; - ss >> adjust; - if (adjust > (myTFDurationMs * 1000)) - { // reset static delay for values outside range - myConf.staticDelayUs = 0; - } - else - { // the new adjust value is added to the existing delay and the result - // is wrapped around at TF duration - int newStaticDelayUs = myConf.staticDelayUs + adjust; - if (newStaticDelayUs > (myTFDurationMs * 1000)) - myConf.staticDelayUs = newStaticDelayUs - (myTFDurationMs * 1000); - else if (newStaticDelayUs < 0) - myConf.staticDelayUs = newStaticDelayUs + (myTFDurationMs * 1000); - else - myConf.staticDelayUs = newStaticDelayUs; - } - } - else if (parameter == "underruns" or - parameter == "latepackets" or - parameter == "frames") { - throw ParameterError("Parameter " + parameter + " is read-only."); - } - else { - stringstream ss_err; - ss_err << "Parameter '" << parameter - << "' is not exported by controllable " << get_rc_name(); - throw ParameterError(ss_err.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 == "staticdelay") { - ss << myConf.staticDelayUs; - } - 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/OutputUHD.h b/src/OutputUHD.h deleted file mode 100644 index 9213183..0000000 --- a/src/OutputUHD.h +++ /dev/null @@ -1,250 +0,0 @@ -/* - 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 version is multi-threaded. A separate thread sends the data to - the device. - - Data between the modulator and the UHD thread are exchanged through a - threadsafe queue. -*/ - -/* - 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 "ModPlugin.h" -#include "EtiReader.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 - -typedef std::complex<float> complexf; - -// 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; -}; - -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; - - // static delay in microseconds - int staticDelayUs = 0; - - // 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: - // 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 sync_and_ts_valid = false; - - ThreadsafeQueue<UHDWorkerFrameData> m_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(); -}; - -#endif // HAVE_OUTPUT_UHD - diff --git a/src/TimestampDecoder.h b/src/TimestampDecoder.h index 943b241..2272fe0 100644 --- a/src/TimestampDecoder.h +++ b/src/TimestampDecoder.h @@ -43,7 +43,7 @@ struct frame_timestamp uint32_t timestamp_sec; uint32_t timestamp_pps; // In units of 1/16384000 s - bool timestamp_valid; + bool timestamp_valid = false; bool timestamp_refresh; frame_timestamp() = default; @@ -88,6 +88,18 @@ struct frame_timestamp return timestamp_pps / 16384000.0; } + double get_real_secs() const { + double t = timestamp_sec; + t += timestamp_pps; + return t; + } + + long long int get_ns() const { + long long int ns = timestamp_sec * 1000000000ull; + ns += llrint((double)timestamp_pps / 0.016384); + return ns; + } + void print(const char* t) { fprintf(stderr, diff --git a/src/Utils.cpp b/src/Utils.cpp index f423dc1..f113be3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -214,3 +214,17 @@ double parseChannel(const std::string& chan) } return freq; } + +int transmission_frame_duration_ms(unsigned int dabMode) +{ + switch (dabMode) { + case 1: return 96; + case 2: return 24; + case 3: return 24; + case 4: return 48; + default: + throw std::runtime_error("invalid DAB mode"); + } +} + + diff --git a/src/Utils.h b/src/Utils.h index 6a36baf..5d5831b 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -68,3 +68,7 @@ void set_thread_name(const char *name); // Convert a channel like 10A to a frequency double parseChannel(const std::string& chan); +// dabMode is either 1, 2, 3, 4, corresponding to TM I, TM II, TM III and TM IV. +// throws a runtime_error if dabMode is not one of these values. +int transmission_frame_duration_ms(unsigned int dabMode); + diff --git a/src/OutputUHDFeedback.cpp b/src/output/Feedback.cpp index 68783f2..f0bbd98 100644 --- a/src/OutputUHDFeedback.cpp +++ b/src/output/Feedback.cpp @@ -34,41 +34,41 @@ DESCRIPTION: # include <config.h> #endif -#ifdef HAVE_OUTPUT_UHD - #include <vector> #include <complex> #include <cstring> -#include <uhd/types/stream_cmd.hpp> #include <sys/socket.h> #include <errno.h> #include <poll.h> #include <boost/date_time/posix_time/posix_time.hpp> -#include "OutputUHDFeedback.h" +#include "output/Feedback.h" #include "Utils.h" #include "Socket.h" using namespace std; -typedef std::complex<float> complexf; -OutputUHDFeedback::OutputUHDFeedback( - uhd::usrp::multi_usrp::sptr usrp, +namespace Output { + +DPDFeedbackServer::DPDFeedbackServer( + std::shared_ptr<SDRDevice> device, uint16_t port, - uint32_t sampleRate) + uint32_t sampleRate) : + m_port(port), + m_sampleRate(sampleRate), + m_device(device) { - m_port = port; - m_sampleRate = sampleRate; - m_usrp = usrp; - if (m_port) { m_running.store(true); - rx_burst_thread = boost::thread(&OutputUHDFeedback::ReceiveBurstThread, this); - burst_tcp_thread = boost::thread(&OutputUHDFeedback::ServeFeedbackThread, this); + rx_burst_thread = boost::thread( + &DPDFeedbackServer::ReceiveBurstThread, this); + + burst_tcp_thread = boost::thread( + &DPDFeedbackServer::ServeFeedbackThread, this); } } -OutputUHDFeedback::~OutputUHDFeedback() +DPDFeedbackServer::~DPDFeedbackServer() { m_running.store(false); @@ -83,12 +83,12 @@ OutputUHDFeedback::~OutputUHDFeedback() } } -void OutputUHDFeedback::set_tx_frame( +void DPDFeedbackServer::set_tx_frame( const std::vector<uint8_t> &buf, const struct frame_timestamp &buf_ts) { if (not m_running) { - throw runtime_error("OutputUHDFeedback not running"); + throw runtime_error("DPDFeedbackServer not running"); } boost::mutex::scoped_lock lock(burstRequest.mutex); @@ -131,13 +131,10 @@ void OutputUHDFeedback::set_tx_frame( } } -void OutputUHDFeedback::ReceiveBurstThread() +void DPDFeedbackServer::ReceiveBurstThread() { try { - set_thread_name("uhdreceiveburst"); - - uhd::stream_args_t stream_args("fc32"); //complex floats - auto rxStream = m_usrp->get_rx_stream(stream_args); + set_thread_name("dpdreceiveburst"); while (m_running) { boost::mutex::scoped_lock lock(burstRequest.mutex); @@ -148,43 +145,40 @@ void OutputUHDFeedback::ReceiveBurstThread() if (not m_running) break; - uhd::stream_cmd_t cmd( - uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_DONE); - cmd.num_samps = burstRequest.num_samples; - cmd.stream_now = false; + const size_t num_samps = burstRequest.num_samples; - double pps = burstRequest.rx_pps / 16384000.0; - cmd.time_spec = uhd::time_spec_t(burstRequest.rx_second, pps); + frame_timestamp ts; + ts.timestamp_sec = burstRequest.rx_second; + ts.timestamp_pps = burstRequest.rx_pps; + ts.timestamp_valid = true; // We need to free the mutex while we recv(), because otherwise we block the // TX thread lock.unlock(); - const double usrp_time = m_usrp->get_time_now().get_real_secs(); - const double cmd_time = cmd.time_spec.get_real_secs(); - - rxStream->issue_stream_cmd(cmd); + const double device_time = m_device->get_real_secs(); + const double cmd_time = ts.get_real_secs(); - uhd::rx_metadata_t md; - - std::vector<uint8_t> buf(cmd.num_samps * sizeof(complexf)); + std::vector<uint8_t> buf(num_samps * sizeof(complexf)); const double timeout = 60; - size_t samples_read = rxStream->recv(&buf[0], cmd.num_samps, md, timeout); + size_t samples_read = m_device->receive_frame( + reinterpret_cast<complexf*>(buf.data()), + num_samps, ts, timeout); lock.lock(); burstRequest.rx_samples = std::move(buf); burstRequest.rx_samples.resize(samples_read * sizeof(complexf)); // The recv might have happened at another time than requested - burstRequest.rx_second = md.time_spec.get_full_secs(); - burstRequest.rx_pps = md.time_spec.get_frac_secs() * 16384000.0; + burstRequest.rx_second = ts.timestamp_sec; + burstRequest.rx_pps = ts.timestamp_pps; etiLog.level(debug) << "DPD: acquired " << samples_read << " RX feedback samples " << "at time " << burstRequest.tx_second << " + " << std::fixed << burstRequest.tx_pps / 16384000.0 << - " Delta=" << cmd_time - usrp_time; + " Delta=" << cmd_time - device_time; burstRequest.state = BurstRequestState::Acquired; @@ -205,7 +199,7 @@ void OutputUHDFeedback::ReceiveBurstThread() m_running.store(false); } -void OutputUHDFeedback::ServeFeedback() +void DPDFeedbackServer::ServeFeedback() { TCPSocket m_server_sock; m_server_sock.listen(m_port); @@ -335,9 +329,9 @@ void OutputUHDFeedback::ServeFeedback() } } -void OutputUHDFeedback::ServeFeedbackThread() +void DPDFeedbackServer::ServeFeedbackThread() { - set_thread_name("uhdservefeedback"); + set_thread_name("dpdfeedbackserver"); while (m_running) { try { @@ -359,4 +353,4 @@ void OutputUHDFeedback::ServeFeedbackThread() m_running.store(false); } -#endif +} // namespace Output diff --git a/src/OutputUHDFeedback.h b/src/output/Feedback.h index 80d287f..2cad508 100644 --- a/src/OutputUHDFeedback.h +++ b/src/output/Feedback.h @@ -36,11 +36,6 @@ DESCRIPTION: # 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 <memory> #include <string> @@ -48,6 +43,9 @@ DESCRIPTION: #include "Log.h" #include "TimestampDecoder.h" +#include "output/SDRDevice.h" + +namespace Output { enum class BurstRequestState { None, // To pending request @@ -56,7 +54,7 @@ enum class BurstRequestState { Acquired, // Both TX and RX frames are ready }; -struct UHDReceiveBurstRequest { +struct FeedbackBurstRequest { // All fields in this struct are protected mutable boost::mutex mutex; boost::condition_variable mutex_notification; @@ -83,21 +81,21 @@ struct UHDReceiveBurstRequest { }; // Serve TX samples and RX feedback samples over a TCP connection -class OutputUHDFeedback { +class DPDFeedbackServer { public: - OutputUHDFeedback( - uhd::usrp::multi_usrp::sptr usrp, - uint16_t port, + DPDFeedbackServer( + std::shared_ptr<SDRDevice> device, + uint16_t port, // Set to 0 to disable the Feedbackserver uint32_t sampleRate); - OutputUHDFeedback(const OutputUHDFeedback& other) = delete; - OutputUHDFeedback& operator=(const OutputUHDFeedback& other) = delete; - ~OutputUHDFeedback(); + DPDFeedbackServer(const DPDFeedbackServer& other) = delete; + DPDFeedbackServer& operator=(const DPDFeedbackServer& other) = delete; + ~DPDFeedbackServer(); void set_tx_frame(const std::vector<uint8_t> &buf, const struct frame_timestamp& ts); private: - // Thread that reacts to burstRequests and receives from the USRP + // Thread that reacts to burstRequests and receives from the SDR device void ReceiveBurstThread(void); // Thread that listens for requests over TCP to get TX and RX feedback @@ -107,13 +105,12 @@ class OutputUHDFeedback { boost::thread rx_burst_thread; boost::thread burst_tcp_thread; - UHDReceiveBurstRequest burstRequest; + FeedbackBurstRequest burstRequest; std::atomic_bool m_running; uint16_t m_port = 0; uint32_t m_sampleRate = 0; - uhd::usrp::multi_usrp::sptr m_usrp; + std::shared_ptr<SDRDevice> m_device; }; - -#endif // HAVE_OUTPUT_UHD +} // namespace Output diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp new file mode 100644 index 0000000..ac25061 --- /dev/null +++ b/src/output/SDR.cpp @@ -0,0 +1,433 @@ +/* + 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) +{ + // muting is remote-controllable + m_config.muting = false; + + time_last_frame.tv_sec = 0; + time_last_frame.tv_nsec = 0; + + m_device_thread = std::thread(&SDR::process_thread_entry, this); + + m_dpd_feedback_server = make_shared<DPDFeedbackServer>( + m_device, + m_config.dpdFeedbackServerPort, + m_config.sampleRate); +} + +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) { + 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) << + "SDR output: dropping one frame with invalid FCT"; + } + else { + try { + if (m_dpd_feedback_server) { + m_dpd_feedback_server->set_tx_frame(frame.buf, frame.ts); + } + } + catch (const runtime_error& e) { + etiLog.level(warn) << + "SDR output: Feedback server failed, restarting..."; + + m_dpd_feedback_server = std::make_shared<DPDFeedbackServer>( + m_device, + m_config.dpdFeedbackServerPort, + m_config.sampleRate); + } + + 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; + + m_running.store(true); + + 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::sleep_through_frame() +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + stringstream ss; + ss << "clock_gettime failure: " << strerror(errno); + throw 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 runtime_error(ss.str()); + } + } + + long delta_us = timespecdiff_us(time_last_frame, now); + long wait_time_us = transmission_frame_duration_ms(m_config.dabMode); + + if (wait_time_us - delta_us > 0) { + usleep(wait_time_us - delta_us); + } + + time_last_frame.tv_nsec += wait_time_us * 1000; + while (time_last_frame.tv_nsec >= 1000000000L) { + time_last_frame.tv_nsec -= 1000000000L; + time_last_frame.tv_sec++; + } +} + +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()) { + sleep_through_frame(); + 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..d3693da --- /dev/null +++ b/src/output/SDR.h @@ -0,0 +1,97 @@ +/* + 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" +#include "output/SDRDevice.h" +#include "output/Feedback.h" + +namespace Output { + +using complexf = std::complex<float>; + +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); + void sleep_through_frame(void); + + 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; + + std::shared_ptr<DPDFeedbackServer> m_dpd_feedback_server; + + 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; + + struct timespec time_last_frame; + +}; + +} + diff --git a/src/output/SDRDevice.h b/src/output/SDRDevice.h new file mode 100644 index 0000000..856233b --- /dev/null +++ b/src/output/SDRDevice.h @@ -0,0 +1,136 @@ +/* + 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 <cstdint> +#include <string> +#include <vector> +#include <complex> + +#include "TimestampDecoder.h" + +namespace Output { + +enum refclk_lock_loss_behaviour_t { CRASH, IGNORE }; + +using complexf = std::complex<float>; + +/* 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; + std::string subDevice; // For UHD + + 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; + virtual void set_rxgain(double rxgain) = 0; + virtual double get_rxgain(void) = 0; + virtual size_t receive_frame( + complexf *buf, + size_t num_samples, + struct frame_timestamp& ts, + double timeout_secs) = 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; +}; + +} // namespace Output diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp new file mode 100644 index 0000000..e0a6f10 --- /dev/null +++ b/src/output/Soapy.cpp @@ -0,0 +1,264 @@ +/* + 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); + + const std::vector<size_t> channels({0}); + m_tx_stream = m_device->setupStream(SOAPY_SDR_TX, "CF32", channels); + m_device->activateStream(m_tx_stream); + + m_rx_stream = m_device->setupStream(SOAPY_SDR_RX, "CF32", channels); +} + +Soapy::~Soapy() +{ + if (m_device != nullptr) { + if (m_tx_stream != nullptr) { + m_device->closeStream(m_tx_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; + } +} + +void Soapy::set_rxgain(double rxgain) +{ + m_device->setGain(SOAPY_SDR_RX, 0, m_conf.rxgain); + m_conf.rxgain = m_device->getGain(SOAPY_SDR_RX, 0); +} + +double Soapy::get_rxgain(void) +{ + return m_device->getGain(SOAPY_SDR_RX, 0); +} + +size_t Soapy::receive_frame( + complexf *buf, + size_t num_samples, + struct frame_timestamp& ts, + double timeout_secs) +{ + int flags = 0; + long long timeNs = ts.get_ns(); + const size_t numElems = num_samples; + + void *buffs[1]; + buffs[0] = buf; + + m_device->activateStream(m_rx_stream, flags, timeNs, numElems); + + auto ret = m_device->readStream(m_tx_stream, buffs, num_samples, flags, timeNs); + + m_device->deactivateStream(m_rx_stream); + + // TODO update effective receive ts + + if (ret < 0) { + throw runtime_error("Soapy readStream error: " + to_string(ret)); + } + + return ret; +} + + +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_tx_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_tx_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..c603193 --- /dev/null +++ b/src/output/Soapy.h @@ -0,0 +1,98 @@ +/* + 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; + + virtual void set_rxgain(double rxgain) override; + virtual double get_rxgain(void) override; + virtual size_t receive_frame( + complexf *buf, + size_t num_samples, + struct frame_timestamp& ts, + double timeout_secs) 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_tx_stream = nullptr; + SoapySDR::Stream *m_rx_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..55beacc --- /dev/null +++ b/src/output/UHD.cpp @@ -0,0 +1,474 @@ +/* + 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/UHD.h" + +#ifdef HAVE_OUTPUT_UHD + +//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args) +#define MDEBUG(fmt, args...) + +#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; + +namespace Output { + +// Maximum number of frames that can wait in frames +static const size_t FRAMES_MAX_SIZE = 8; + +static 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()); +} + +static 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; + } + } +} + + + +UHD::UHD( + SDRDeviceConfig& config) : + SDRDevice(), + m_conf(config), + m_running(false) +{ + std::stringstream device; + device << m_conf.device; + + if (m_conf.masterClockRate != 0) { + if (device.str() != "") { + device << ","; + } + device << "master_clock_rate=" << m_conf.masterClockRate; + } + + MDEBUG("OutputUHD::OutputUHD(device: %s) @ %p\n", + device.str().c_str(), this); + + /* TODO + RC_ADD_PARAMETER(rxgain, "UHD analog daughterboard RX gain for DPD feedback"); + */ + + 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()); + + m_usrp = uhd::usrp::multi_usrp::make(device.str()); + + etiLog.log(info, "OutputUHD:Using device: %s...", + m_usrp->get_pp_string().c_str()); + + if (m_conf.masterClockRate != 0.0) { + double master_clk_rate = m_usrp->get_master_clock_rate(); + etiLog.log(debug, "OutputUHD:Checking master clock rate: %f...", + master_clk_rate); + + if (fabs(master_clk_rate - m_conf.masterClockRate) > + (m_conf.masterClockRate * 1e-6)) { + throw std::runtime_error("Cannot set USRP master_clock_rate. Aborted."); + } + } + + MDEBUG("OutputUHD:Setting REFCLK and PPS input...\n"); + + if (m_conf.refclk_src == "gpsdo-ettus") { + m_usrp->set_clock_source("gpsdo"); + } + else { + m_usrp->set_clock_source(m_conf.refclk_src); + } + m_usrp->set_time_source(m_conf.pps_src); + + m_device_time = std::make_shared<USRPTime>(m_usrp, m_conf); + + if (m_conf.subDevice != "") { + m_usrp->set_tx_subdev_spec(uhd::usrp::subdev_spec_t(m_conf.subDevice), + uhd::usrp::multi_usrp::ALL_MBOARDS); + } + + etiLog.level(debug) << "UHD clock source is " << m_usrp->get_clock_source(0); + + etiLog.level(debug) << "UHD time source is " << m_usrp->get_time_source(0); + + m_usrp->set_tx_rate(m_conf.sampleRate); + etiLog.log(debug, "OutputUHD:Set rate to %d. Actual TX Rate: %f sps...", + m_conf.sampleRate, m_usrp->get_tx_rate()); + + if (fabs(m_usrp->get_tx_rate() / m_conf.sampleRate) > + m_conf.sampleRate * 1e-6) { + throw std::runtime_error("Cannot set USRP sample rate. Aborted."); + } + + tune(m_conf.lo_offset, m_conf.frequency); + + m_conf.frequency = m_usrp->get_tx_freq(); + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Actual TX frequency: " << m_conf.frequency; + + etiLog.level(info) << std::fixed << std::setprecision(3) << + "OutputUHD:Actual RX frequency: " << m_usrp->get_tx_freq(); + + m_usrp->set_tx_gain(m_conf.txgain); + m_conf.txgain = m_usrp->get_tx_gain(); + etiLog.log(debug, "OutputUHD:Actual TX Gain: %f", m_conf.txgain); + + etiLog.log(debug, "OutputUHD:Mute on missing timestamps: %s", + m_conf.muteNoTimestamps ? "enabled" : "disabled"); + + // preparing output thread worker data + // TODO sourceContainsTimestamp = false; + + m_usrp->set_rx_rate(m_conf.sampleRate); + etiLog.log(debug, "OutputUHD:Actual RX Rate: %f sps.", m_usrp->get_rx_rate()); + + m_usrp->set_rx_antenna("RX2"); + etiLog.log(debug, "OutputUHD:Set RX Antenna: %s", + m_usrp->get_rx_antenna().c_str()); + + m_usrp->set_rx_gain(m_conf.rxgain); + etiLog.log(debug, "OutputUHD:Actual RX Gain: %f", m_usrp->get_rx_gain()); + + /* TODO + uhdFeedback = std::make_shared<OutputUHDFeedback>( + m_usrp, m_conf.dpdFeedbackServerPort, m_conf.sampleRate); + */ + + const uhd::stream_args_t stream_args("fc32"); //complex floats + m_rx_stream = m_usrp->get_rx_stream(stream_args); + m_tx_stream = m_usrp->get_tx_stream(stream_args); + + m_running.store(true); + m_async_rx_thread = boost::thread(&UHD::print_async_thread, this); + + MDEBUG("OutputUHD:UHD ready.\n"); +} + +UHD::~UHD() +{ + stop_threads(); +} + +void UHD::tune(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 = m_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 << "..."; + m_usrp->set_tx_freq(frequency); + } + + // TODO configure LO offset also for RX + m_usrp->set_rx_freq(frequency); +} + +double UHD::get_tx_freq(void) +{ + return m_usrp->get_tx_freq(); +} + +void UHD::set_txgain(double txgain) +{ + m_usrp->set_tx_gain(txgain); + m_conf.txgain = m_usrp->get_tx_gain(); +} + +double UHD::get_txgain(void) +{ + return m_usrp->get_tx_gain(); +} + +void UHD::transmit_frame(const struct FrameData& frame) +{ + 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 = m_tx_stream->get_max_num_samps(); + size_t num_acc_samps = 0; //number of accumulated samples + while (m_running.load() and (not m_conf.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 = ( + frame.ts.timestamp_valid and + frame.ts.timestamp_refresh and + samps_to_send <= usrp_max_num_samps ); + + //send a single packet + size_t num_tx_samps = m_tx_stream->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/m_conf.sampleRate); + + if (num_tx_samps == 0) { + etiLog.log(warn, + "OutputUHD unable to write to device, skipping frame!"); + break; + } + } +} + + +SDRDevice::RunStatistics UHD::get_run_statistics(void) +{ + RunStatistics rs; + rs.num_underruns = num_underflows; + rs.num_overruns = num_overflows; + rs.num_late_packets = num_late_packets; + rs.num_frames_modulated = num_frames_modulated; + return rs; +} + +double UHD::get_real_secs(void) +{ + return m_usrp->get_time_now().get_real_secs(); +} + +void UHD::set_rxgain(double rxgain) +{ + m_usrp->set_rx_gain(m_conf.rxgain); + m_conf.rxgain = m_usrp->get_rx_gain(); +} + +double UHD::get_rxgain() +{ + return m_usrp->get_rx_gain(); +} + +size_t UHD::receive_frame( + complexf *buf, + size_t num_samples, + struct frame_timestamp& ts, + double timeout_secs) +{ + uhd::stream_cmd_t cmd( + uhd::stream_cmd_t::stream_mode_t::STREAM_MODE_NUM_SAMPS_AND_DONE); + cmd.num_samps = num_samples; + cmd.stream_now = false; + cmd.time_spec = uhd::time_spec_t(ts.timestamp_sec, ts.pps_offset()); + + m_rx_stream->issue_stream_cmd(cmd); + + uhd::rx_metadata_t md; + + constexpr double timeout = 60; + size_t samples_read = m_rx_stream->recv(buf, num_samples, md, timeout); + + // Update the ts with the effective receive TS + ts.timestamp_sec = md.time_spec.get_full_secs(); + ts.timestamp_pps = md.time_spec.get_frac_secs() * 16384000.0; + return samples_read; +} + +// Return true if GPS and reference clock inputs are ok +bool UHD::is_clk_source_ok(void) +{ + bool ok = true; + + if (refclk_loss_needs_check()) { + try { + if (not m_usrp->get_mboard_sensor("ref_locked", 0).to_bool()) { + ok = false; + + etiLog.level(alert) << + "OutputUHD: External reference clock lock lost !"; + + if (m_conf.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."); + } + } + + if (m_device_time) { + ok |= m_device_time->verify_time(); + } + + return ok; +} + +const char* UHD::device_name(void) +{ + return "UHD"; +} + + +bool UHD::refclk_loss_needs_check() const +{ + if (suppress_refclk_loss_check) { + return false; + } + return m_conf.refclk_src != "internal"; +} + +void UHD::stop_threads() +{ + m_running.store(false); + if (m_async_rx_thread.joinable()) { + m_async_rx_thread.join(); + } +} + + + +void UHD::print_async_thread() +{ + while (m_running.load()) { + uhd::async_metadata_t async_md; + if (m_usrp->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 = + m_usrp->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; + } + } +} + +} // namespace Output + +#endif // HAVE_OUTPUT_UHD + diff --git a/src/output/UHD.h b/src/output/UHD.h new file mode 100644 index 0000000..4d3eecc --- /dev/null +++ b/src/output/UHD.h @@ -0,0 +1,129 @@ +/* + 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 <chrono> +#include <memory> +#include <string> +#include <atomic> + +#include "Log.h" +#include "output/SDR.h" +#include "output/USRPTime.h" +#include "TimestampDecoder.h" +#include "RemoteControl.h" +#include "ThreadsafeQueue.h" + +#include <stdio.h> +#include <sys/types.h> + +// 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 { + +class UHD : public Output::SDRDevice +{ + public: + UHD(SDRDeviceConfig& config); + UHD(const UHD& other) = delete; + UHD& operator=(const UHD& other) = delete; + ~UHD(); + + 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; + + virtual void set_rxgain(double rxgain) override; + virtual double get_rxgain(void) override; + virtual size_t receive_frame( + complexf *buf, + size_t num_samples, + struct frame_timestamp& ts, + double timeout_secs) 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; + uhd::usrp::multi_usrp::sptr m_usrp; + uhd::tx_streamer::sptr m_tx_stream; + uhd::rx_streamer::sptr m_rx_stream; + std::shared_ptr<USRPTime> m_device_time; + + size_t num_underflows = 0; + size_t num_overflows = 0; + size_t num_late_packets = 0; + size_t num_frames_modulated = 0; //TODO increment + size_t num_underflows_previous = 0; + size_t num_late_packets_previous = 0; + + uhd::tx_metadata_t md; + + // Used to print statistics once a second + std::chrono::steady_clock::time_point last_print_time; + + // Returns true if we want to verify loss of refclk + bool refclk_loss_needs_check(void) const; + bool suppress_refclk_loss_check = false; + + // Poll asynchronous metadata from UHD + std::atomic<bool> m_running; + boost::thread m_async_rx_thread; + void stop_threads(void); + void print_async_thread(void); +}; + +} // namespace Output + +#endif // HAVE_OUTPUT_UHD + diff --git a/src/output/USRPTime.cpp b/src/output/USRPTime.cpp new file mode 100644 index 0000000..dcedeab --- /dev/null +++ b/src/output/USRPTime.cpp @@ -0,0 +1,280 @@ +/* + 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: + The part of the UHD output that takes care of the GPSDO. +*/ + +/* + 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/USRPTime.h" + +#ifdef HAVE_OUTPUT_UHD + +//#define MDEBUG(fmt, args...) fprintf(LOG, fmt , ## args) +#define MDEBUG(fmt, args...) + +namespace Output { + +using namespace std; + + +// Check function for GPS TIMELOCK sensor from the ODR LEA-M8F board GPSDO +static bool check_gps_timelock(uhd::usrp::multi_usrp::sptr& usrp) +{ + try { + const string sensor_value = + usrp->get_mboard_sensor("gps_timelock", 0).to_pp_string(); + + if (sensor_value.find("TIME LOCKED") == string::npos) { + etiLog.level(warn) << "OutputUHD: gps_timelock " << sensor_value; + return false; + } + + return true; + } + catch (const uhd::lookup_error &e) { + etiLog.level(warn) << "OutputUHD: no gps_timelock sensor"; + return false; + } +} + +// Check function for GPS LOCKED sensor from the Ettus GPSDO +static bool check_gps_locked(uhd::usrp::multi_usrp::sptr& usrp) +{ + try { + const 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 (const uhd::lookup_error &e) { + etiLog.level(warn) << "OutputUHD: no gps_locked sensor"; + return false; + } +} + + +USRPTime::USRPTime( + uhd::usrp::multi_usrp::sptr usrp, + SDRDeviceConfig& conf) : + m_usrp(usrp), + m_conf(conf), + time_last_check(timepoint_t::clock::now()) +{ + if (m_conf.enableSync and (m_conf.pps_src == "none")) { + set_usrp_time_from_localtime(); + } +} + +bool USRPTime::verify_time() +{ + if (not gpsfix_needs_check()) { + return true; + } + + /* During bootup, we say the gpsdo is not ok, and we poll the GPSDO until + * we reach lock. Then we sync time. If we do not reach lock in time, we + * crash. + * + * Once we are synced and we have lock, everything ok. If we lose lock for + * a number of seconds, we switch to the lost_fix state. + * + * In the lost fix state, we return false to get the TX muted, and we monitor. + * If the fix comes back, we unmute. If we reach the timeout, we crash. + */ + + check_gps(); + + const auto duration_without_fix = + gps_fix_check_interval * num_checks_without_gps_fix; + + switch (gps_state) { + case gps_state_e::bootup: + if (duration_without_fix > initial_gps_fix_wait) { + throw runtime_error("GPS did not fix in " + + to_string(initial_gps_fix_wait) + " seconds"); + } + + if (num_checks_without_gps_fix == 0) { + if (m_conf.pps_src != "none") { + set_usrp_time_from_pps(); + } + gps_state = gps_state_e::monitor_fix; + return true; + } + + return false; + + case gps_state_e::monitor_fix: + if (duration_without_fix > m_conf.maxGPSHoldoverTime) { + throw runtime_error("Lost GPS Fix for " + + to_string(duration_without_fix) + " seconds"); + } + + return true; + } + + throw logic_error("End of USRPTime::verify_time() reached"); +} + +void USRPTime::check_gps() +{ + timepoint_t time_now = timepoint_t::clock::now(); + + // Divide interval by two because we alternate between + // launch and check + const auto checkinterval = chrono::seconds(lrint(gps_fix_check_interval/2.0)); + + if (gpsfix_needs_check() and time_last_check + checkinterval < time_now) { + time_last_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; + } + } + } + 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, m_usrp) ); + } + else { + gps_fix_pt = boost::packaged_task<bool>( + boost::bind(check_gps_timelock, m_usrp) ); + } + gps_fix_future = gps_fix_pt.get_future(); + + gps_fix_task = boost::thread(boost::move(gps_fix_pt)); + } + } +} + +bool USRPTime::gpsfix_needs_check() const +{ + if (m_conf.refclk_src == "internal") { + return false; + } + else if (m_conf.refclk_src == "gpsdo") { + return (m_conf.maxGPSHoldoverTime != 0); + } + else if (m_conf.refclk_src == "gpsdo-ettus") { + return (m_conf.maxGPSHoldoverTime != 0); + } + else { + return false; + } +} + +bool USRPTime::gpsdo_is_ettus() const +{ + return (m_conf.refclk_src == "gpsdo-ettus"); +} + +void USRPTime::set_usrp_time_from_localtime() +{ + etiLog.level(warn) << + "OutputUHD: WARNING:" + " you are using synchronous transmission without PPS input!"; + + struct timespec now; + if (clock_gettime(CLOCK_REALTIME, &now)) { + etiLog.level(error) << "OutputUHD: could not get time :" << + strerror(errno); + } + else { + const uhd::time_spec_t t(now.tv_sec, (double)now.tv_nsec / 1e9); + m_usrp->set_time_now(t); + etiLog.level(info) << "OutputUHD: Setting USRP time to " << + std::fixed << t.get_real_secs(); + } +} + +void USRPTime::set_usrp_time_from_pps() +{ + /* 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 + m_usrp->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", + m_usrp->get_time_now().get_real_secs()); +} + + +} // namespace Output + +#endif // HAVE_OUTPUT_UHD diff --git a/src/output/USRPTime.h b/src/output/USRPTime.h new file mode 100644 index 0000000..7527f21 --- /dev/null +++ b/src/output/USRPTime.h @@ -0,0 +1,116 @@ +/* + 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: + The part of the UHD output that takes care of the GPSDO and setting device + time. +*/ + +/* + 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/usrp/multi_usrp.hpp> +#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 <stdio.h> +#include <sys/types.h> + +namespace Output { + +class USRPTime { + public: + USRPTime( uhd::usrp::multi_usrp::sptr usrp, + SDRDeviceConfig& conf); + + // Verifies the GPSDO state, that the device time is ok. + // Returns true if all ok. + // Should be called more often than the gps_fix_check_interval + bool verify_time(void); + + // 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 + + private: + enum class gps_state_e { + /* At startup, the LEA-M8F GPSDO gets issued a hotstart request to + * make sure we will not sync time on a PPS edge that is generated + * while the GPSDO is in holdover. In the bootup state, we wait for + * the first PPS after hotstart, and then sync time. + */ + bootup, + + /* Once the system is up, we check lock every now and then. If the + * fix is lost for too long, we crash. + */ + monitor_fix, + }; + + void check_gps(); + + uhd::usrp::multi_usrp::sptr m_usrp; + SDRDeviceConfig& m_conf; + + gps_state_e gps_state = gps_state_e::bootup; + int num_checks_without_gps_fix = 1; + + using timepoint_t = std::chrono::time_point<std::chrono::steady_clock>; + timepoint_t time_last_check; + + boost::packaged_task<bool> gps_fix_pt; + boost::unique_future<bool> gps_fix_future; + boost::thread gps_fix_task; + + // 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; + + void set_usrp_time_from_localtime(void); + void set_usrp_time_from_pps(void); +}; + +} // namespace Output + +#endif // HAVE_OUTPUT_UHD |