diff options
Diffstat (limited to 'src/output')
-rwxr-xr-x | src/output/BladeRF.cpp | 28 | ||||
-rwxr-xr-x | src/output/BladeRF.h | 17 | ||||
-rw-r--r-- | src/output/Dexter.cpp | 691 | ||||
-rw-r--r-- | src/output/Dexter.h | 138 | ||||
-rw-r--r-- | src/output/Feedback.cpp | 2 | ||||
-rw-r--r-- | src/output/Feedback.h | 2 | ||||
-rw-r--r-- | src/output/Lime.cpp | 37 | ||||
-rw-r--r-- | src/output/Lime.h | 14 | ||||
-rw-r--r-- | src/output/SDR.cpp | 281 | ||||
-rw-r--r-- | src/output/SDR.h | 13 | ||||
-rw-r--r-- | src/output/SDRDevice.h | 30 | ||||
-rw-r--r-- | src/output/Soapy.cpp | 24 | ||||
-rw-r--r-- | src/output/Soapy.h | 12 | ||||
-rw-r--r-- | src/output/UHD.cpp | 33 | ||||
-rw-r--r-- | src/output/UHD.h | 18 | ||||
-rw-r--r-- | src/output/USRPTime.cpp | 9 |
16 files changed, 1106 insertions, 243 deletions
diff --git a/src/output/BladeRF.cpp b/src/output/BladeRF.cpp index a6ad0cc..c16b64d 100755 --- a/src/output/BladeRF.cpp +++ b/src/output/BladeRF.cpp @@ -239,13 +239,10 @@ double BladeRF::get_bandwidth(void) const return (double)bw; } -SDRDevice::RunStatistics BladeRF::get_run_statistics(void) const +SDRDevice::run_statistics_t BladeRF::get_run_statistics(void) const { - RunStatistics rs; - rs.num_underruns = underflows; - rs.num_overruns = overflows; - rs.num_late_packets = late_packets; - rs.num_frames_modulated = num_frames_modulated; + run_statistics_t rs; + rs["frames"] = num_frames_modulated; return rs; } @@ -269,14 +266,14 @@ double BladeRF::get_rxgain(void) const size_t BladeRF::receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp &ts, + frame_timestamp &ts, double timeout_secs) { // TODO return 0; } -bool BladeRF::is_clk_source_ok() const +bool BladeRF::is_clk_source_ok() { // TODO return true; @@ -287,24 +284,23 @@ const char *BladeRF::device_name(void) const return "BladeRF"; } -double BladeRF::get_temperature(void) const +std::optional<double> BladeRF::get_temperature(void) const { if (not m_device) throw runtime_error("BladeRF device not set up"); float temp = 0.0; - int status = bladerf_get_rfic_temperature(m_device, &temp); - if (status < 0) - { + if (status >= 0) { + return (double)temp; + } + else { etiLog.level(error) << "Error getting BladeRF temperature: %s " << bladerf_strerror(status); + return std::nullopt; } - - return (double)temp; } - -void BladeRF::transmit_frame(const struct FrameData &frame) // SC16 frames +void BladeRF::transmit_frame(struct FrameData&& frame) // SC16 frames { const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t)); diff --git a/src/output/BladeRF.h b/src/output/BladeRF.h index bc6db38..fa3419e 100755 --- a/src/output/BladeRF.h +++ b/src/output/BladeRF.h @@ -74,8 +74,8 @@ class BladeRF : public Output::SDRDevice virtual double get_txgain(void) const override; virtual void set_bandwidth(double bandwidth) override; virtual double get_bandwidth(void) const override; - virtual void transmit_frame(const struct FrameData& frame) override; - virtual RunStatistics get_run_statistics(void) const override; + virtual void transmit_frame(struct FrameData&& frame) override; + virtual run_statistics_t get_run_statistics(void) const override; virtual double get_real_secs(void) const override; virtual void set_rxgain(double rxgain) override; @@ -83,14 +83,14 @@ class BladeRF : public Output::SDRDevice virtual size_t receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) override; // Return true if GPS and reference clock inputs are ok - virtual bool is_clk_source_ok(void) const override; + virtual bool is_clk_source_ok(void) override; virtual const char* device_name(void) const override; - virtual double get_temperature(void) const override; + virtual std::optional<double> get_temperature(void) const override; private: @@ -99,14 +99,9 @@ class BladeRF : public Output::SDRDevice bladerf_channel m_channel = BLADERF_CHANNEL_TX(0); // channel TX0 //struct bladerf_stream* m_stream; /* used for asynchronous api */ - size_t underflows = 0; - size_t overflows = 0; - size_t late_packets = 0; size_t num_frames_modulated = 0; - //size_t num_underflows_previous = 0; - //size_t num_late_packets_previous = 0; }; } // namespace Output -#endif // HAVE_BLADERF
\ No newline at end of file +#endif // HAVE_BLADERF diff --git a/src/output/Dexter.cpp b/src/output/Dexter.cpp new file mode 100644 index 0000000..26472e8 --- /dev/null +++ b/src/output/Dexter.cpp @@ -0,0 +1,691 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2023 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + It is an output driver using libiio targeting the PrecisionWave DEXTER board. +*/ + +/* + 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/Dexter.h" + +#ifdef HAVE_DEXTER + +#include <chrono> +#include <limits> +#include <cstdio> +#include <iomanip> + +#include "Log.h" +#include "Utils.h" + +using namespace std; + +namespace Output { + +static constexpr uint64_t DSP_CLOCK = 2048000uLL * 80; + +static constexpr uint64_t IIO_TIMEOUT_MS = 1000; + +static constexpr size_t TRANSMISSION_FRAME_LEN_SAMPS = (2656 + 76 * 2552) * /* I+Q */ 2; +static constexpr size_t IIO_BUFFERS = 2; +static constexpr size_t IIO_BUFFER_LEN_SAMPS = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; + +static string get_iio_error(int err) +{ + char dst[256]; + iio_strerror(-err, dst, sizeof(dst)); + return string(dst); +} + +static void fill_time(struct timespec *t) +{ + if (clock_gettime(CLOCK_REALTIME, t) != 0) { + throw std::runtime_error(string("Failed to retrieve CLOCK_REALTIME") + strerror(errno)); + } +} + +Dexter::Dexter(SDRDeviceConfig& config) : + SDRDevice(), + m_conf(config) +{ + etiLog.level(info) << "Dexter:Creating the device"; + + m_ctx = iio_create_local_context(); + if (not m_ctx) { + throw std::runtime_error("Dexter: Unable to create iio context"); + } + + int r; + if ((r = iio_context_set_timeout(m_ctx, IIO_TIMEOUT_MS)) != 0) { + etiLog.level(error) << "Failed to set IIO timeout " << get_iio_error(r); + } + + m_dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx"); + if (not m_dexter_dsp_tx) { + throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device"); + } + + m_ad9957 = iio_context_find_device(m_ctx, "ad9957"); + if (not m_ad9957) { + throw std::runtime_error("Dexter: Unable to find ad9957 iio device"); + } + + m_ad9957_tx0 = iio_context_find_device(m_ctx, "ad9957_tx0"); + if (not m_ad9957_tx0) { + throw std::runtime_error("Dexter: Unable to find ad9957_tx0 iio device"); + } + + // TODO make DC offset configurable and add to RC + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc0", 0)) != 0) { + throw std::runtime_error("Failed to set dexter_dsp_tx.dc0 = false: " + get_iio_error(r)); + } + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "dc1", 0)) != 0) { + throw std::runtime_error("Failed to set dexter_dsp_tx.dc1 = false: " + get_iio_error(r)); + } + + if (m_conf.sampleRate != 2048000) { + throw std::runtime_error("Dexter: Only 2048000 samplerate supported"); + } + + tune(m_conf.lo_offset, m_conf.frequency); + // TODO m_conf.frequency = m_dexter_dsp_tx->getFrequency(SOAPY_SDR_TX, 0); + etiLog.level(info) << "Dexter:Actual frequency: " << + std::fixed << std::setprecision(3) << + m_conf.frequency / 1000.0 << " kHz."; + + // skip: Set bandwidth + + // skip: antenna + + // The FIFO should not contain data, but setting gain=0 before setting start_clks to zero is an additional security + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) { + throw std::runtime_error("Failed to set dexter_dsp_tx.gain0 = 0 : " + get_iio_error(r)); + } + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_flush_fifo_trigger", 1)) != 0) { + throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_flush_fifo_trigger = 1 : " + get_iio_error(r)); + } + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) { + throw std::runtime_error("Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " + get_iio_error(r)); + } + + constexpr int CHANNEL_INDEX = 0; + m_tx_channel = iio_device_get_channel(m_ad9957_tx0, CHANNEL_INDEX); + if (m_tx_channel == nullptr) { + throw std::runtime_error("Dexter: Cannot create IIO channel."); + } + + iio_channel_enable(m_tx_channel); + + m_buffer = iio_device_create_buffer(m_ad9957_tx0, IIO_BUFFER_LEN_SAMPS, 0); + if (not m_buffer) { + throw std::runtime_error("Dexter: Cannot create IIO buffer."); + } + + // Flush the FPGA FIFO + { + constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; + constexpr size_t buflen = buflen_samps * sizeof(int16_t); + + memset(iio_buffer_start(m_buffer), 0, buflen); + ssize_t pushed = iio_buffer_push(m_buffer); + if (pushed < 0) { + etiLog.level(error) << "Dexter: init push buffer " << get_iio_error(pushed); + } + this_thread::sleep_for(chrono::milliseconds(200)); + } + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) { + etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain << + " : " << get_iio_error(r); + } + + m_running = true; + m_underflow_read_thread = std::thread(&Dexter::underflow_read_process, this); +} + +void Dexter::channel_up() +{ + int r; + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", m_conf.txgain)) != 0) { + etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = " << m_conf.txgain << + " : " << get_iio_error(r); + } + + m_channel_is_up = true; + etiLog.level(debug) << "DEXTER CHANNEL_UP"; +} + +void Dexter::channel_down() +{ + int r; + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0)) != 0) { + etiLog.level(error) << "Failed to set dexter_dsp_tx.gain0 = 0: " << get_iio_error(r); + } + + // Setting stream0_start_clocks to 0 will flush out the FIFO, but we need to wait a bit before + // we "up" the channel again + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", 0)) != 0) { + etiLog.level(warn) << "Failed to set dexter_dsp_tx.stream0_start_clks = 0 : " << get_iio_error(r); + } + + long long underflows_old = 0; + + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_old)) != 0) { + etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r); + } + + long long underflows = underflows_old; + + // Limiting to 10*96ms is just a safety to avoid running into an infinite loop + for (size_t i = 0; underflows == underflows_old && i < 10; i++) { + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows)) != 0) { + etiLog.level(warn) << "Failed to read dexter_dsp_tx.buffer_underflows0 : " << get_iio_error(r); + } + + this_thread::sleep_for(chrono::milliseconds(96)); + } + + if (underflows == underflows_old) { + etiLog.level(warn) << "DEXTER CHANNEL_DOWN, no underflow detected! " << underflows; + } + + m_channel_is_up = false; + etiLog.level(debug) << "DEXTER CHANNEL_DOWN"; +} + +void Dexter::handle_hw_time() +{ + /* + * On startup, wait until `gpsdo_locked==1` and `pps_loss_of_signal==0`, + * then do the clocks alignment and go to normal state. + * + * In normal state, if `pps_loss_of_signal==1`, go to holdover state. + * + * If we've been in holdover state for longer than the configured time, or + * if `pps_loss_of_signal==0` stop the mod and restart. + */ + int r; + + switch (m_clock_state) { + case DexterClockState::Startup: + { + long long gpsdo_locked = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gpsdo_locked", &gpsdo_locked)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.gpsdo_locked: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + long long pps_loss_of_signal = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + if (gpsdo_locked == 1 and pps_loss_of_signal == 0) { + /* Procedure: + * Wait 200ms after second change, fetch pps_clks attribute + * idem at the next second, and check that pps_clks incremented by DSP_CLOCK + * If ok, store the correspondence between current second change (measured in UTC clock time) + * and the counter value at pps rising edge. */ + + etiLog.level(info) << "Dexter: Waiting for second change..."; + + struct timespec time_at_startup; + fill_time(&time_at_startup); + time_at_startup.tv_nsec = 0; + + struct timespec time_now; + do { + fill_time(&time_now); + this_thread::sleep_for(chrono::milliseconds(1)); + } while (time_at_startup.tv_sec == time_now.tv_sec); + this_thread::sleep_for(chrono::milliseconds(200)); + + long long pps_clks = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + time_t tnow = time_now.tv_sec; + etiLog.level(info) << "Dexter: pps_clks " << pps_clks << " at UTC " << + put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S"); + + time_at_startup.tv_sec = time_now.tv_sec; + do { + fill_time(&time_now); + this_thread::sleep_for(chrono::milliseconds(1)); + } while (time_at_startup.tv_sec == time_now.tv_sec); + this_thread::sleep_for(chrono::milliseconds(200)); + + long long pps_clks2 = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_clks", &pps_clks2)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_clks: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + tnow = time_now.tv_sec; + etiLog.level(info) << "Dexter: pps_clks increased by " << pps_clks2 - pps_clks << " at UTC " << + put_time(std::gmtime(&tnow), "%Y-%m-%d %H:%M:%S"); + + if ((uint64_t)pps_clks + DSP_CLOCK != (uint64_t)pps_clks2) { + throw std::runtime_error("Dexter: Wrong increase of pps_clks, expected " + to_string(DSP_CLOCK)); + } + + m_utc_seconds_at_startup = time_now.tv_sec; + m_clock_count_at_startup = pps_clks2; + m_holdover_since = chrono::steady_clock::time_point::min(); + m_holdover_since_t = 0; + m_clock_state = DexterClockState::Normal; + etiLog.level(debug) << "Dexter: switch clock state Startup -> Normal"; + } + } + break; + case DexterClockState::Normal: + { + long long pps_loss_of_signal = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + if (pps_loss_of_signal == 1) { + m_holdover_since = chrono::steady_clock::now(); + m_holdover_since_t = chrono::system_clock::to_time_t(chrono::system_clock::now()); + m_clock_state = DexterClockState::Holdover; + etiLog.level(debug) << "Dexter: switch clock state Normal -> Holdover"; + } + } + break; + case DexterClockState::Holdover: + { + using namespace chrono; + + long long pps_loss_of_signal = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "pps_loss_of_signal", &pps_loss_of_signal)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.pps_loss_of_signal: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + const duration<double> d = steady_clock::now() - m_holdover_since; + const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime); + if (d > max_holdover_duration or pps_loss_of_signal == 0) { + m_clock_state = DexterClockState::Startup; + m_utc_seconds_at_startup = 0; + m_clock_count_at_startup = 0; + m_holdover_since = chrono::steady_clock::time_point::min(); + m_holdover_since_t = 0; + etiLog.level(debug) << "Dexter: switch clock state Holdover -> Startup"; + } + } + break; + } +} + +void Dexter::tune(double lo_offset, double frequency) +{ + // lo_offset is applied to the DSP, and frequency is given to the ad9957 + + long long freq = frequency; + int r = 0; + if ((r = iio_device_attr_write_longlong(m_ad9957, "center_frequency", freq)) != 0) { + etiLog.level(warn) << "Failed to set ad9957.center_frequency = " << freq << " : " << get_iio_error(r); + } + + long long lo_offs = lo_offset; + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "frequency0", lo_offs)) != 0) { + etiLog.level(warn) << "Failed to set dexter_dsp_tx.frequency0 = " << lo_offs << " : " << get_iio_error(r); + } +} + +double Dexter::get_tx_freq(void) const +{ + long long lo_offset = 0; + int r = 0; + + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "frequency0", &lo_offset)) != 0) { + etiLog.level(warn) << "Failed to read dexter_dsp_tx.frequency0: " << get_iio_error(r); + return 0; + } + + long long frequency = 0; + if ((r = iio_device_attr_read_longlong(m_ad9957, "center_frequency", &frequency)) != 0) { + etiLog.level(warn) << "Failed to read ad9957.center_frequency: " << get_iio_error(r); + return 0; + } + + return frequency + lo_offset; +} + +void Dexter::set_txgain(double txgain) +{ + int r = 0; + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", txgain)) != 0) { + etiLog.level(warn) << "Failed to set dexter_dsp_tx.gain0 = " << txgain << ": " << get_iio_error(r); + } + + long long txgain_readback = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) { + etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r); + } + else { + m_conf.txgain = txgain_readback; + } +} + +double Dexter::get_txgain(void) const +{ + long long txgain_readback = 0; + int r = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "gain0", &txgain_readback)) != 0) { + etiLog.level(warn) << "Failed to read dexter_dsp_tx.gain0: " << get_iio_error(r); + } + return txgain_readback; +} + +void Dexter::set_bandwidth(double bandwidth) +{ + return; +} + +double Dexter::get_bandwidth(void) const +{ + return 0; +} + +SDRDevice::run_statistics_t Dexter::get_run_statistics(void) const +{ + run_statistics_t rs; + { + std::unique_lock<std::mutex> lock(m_attr_thread_mutex); + rs["underruns"].v = underflows; + } + rs["latepackets"].v = num_late; + rs["frames"].v = num_frames_modulated; + + rs["in_holdover_since"].v = 0; + rs["remaining_holdover_s"].v = nullopt; + switch (m_clock_state) { + case DexterClockState::Startup: + rs["clock_state"].v = "startup"; break; + case DexterClockState::Normal: + rs["clock_state"].v = "normal"; break; + case DexterClockState::Holdover: + rs["clock_state"].v = "holdover"; + rs["in_holdover_since"].v = m_holdover_since_t; + { + using namespace std::chrono; + const auto max_holdover_duration = seconds(m_conf.maxGPSHoldoverTime); + const duration<double> remaining = max_holdover_duration - (steady_clock::now() - m_holdover_since); + rs["remaining_holdover_s"].v = (ssize_t)duration_cast<seconds>(remaining).count(); + } + break; + } + + return rs; +} + +double Dexter::get_real_secs(void) const +{ + long long clks = 0; + int r = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + switch (m_clock_state) { + case DexterClockState::Startup: + return 0; + case DexterClockState::Normal: + case DexterClockState::Holdover: + return (double)m_utc_seconds_at_startup + (double)(clks - m_clock_count_at_startup) / (double)DSP_CLOCK; + } + throw std::logic_error("Unhandled switch"); +} + +void Dexter::set_rxgain(double rxgain) +{ + // TODO +} + +double Dexter::get_rxgain(void) const +{ + // TODO + return 0; +} + +size_t Dexter::receive_frame( + complexf *buf, + size_t num_samples, + frame_timestamp& ts, + double timeout_secs) +{ + // TODO + return 0; +} + + +bool Dexter::is_clk_source_ok() +{ + if (m_conf.enableSync) { + handle_hw_time(); + return m_clock_state != DexterClockState::Startup; + } + else { + return true; + } +} + +const char* Dexter::device_name(void) const +{ + return "Dexter"; +} + +std::optional<double> Dexter::get_temperature(void) const +{ + std::ifstream in("/sys/bus/i2c/devices/1-002f/hwmon/hwmon0/temp1_input", std::ios::in | std::ios::binary); + if (in) { + double tbaseboard; + in >> tbaseboard; + return tbaseboard / 1000.0; + } + else { + return {}; + } +} + +void Dexter::transmit_frame(struct FrameData&& frame) +{ + constexpr size_t frame_len_bytes = TRANSMISSION_FRAME_LEN_SAMPS * sizeof(int16_t); + if (frame.buf.size() != frame_len_bytes) { + etiLog.level(debug) << "Dexter::transmit_frame Expected " << + frame_len_bytes << " got " << frame.buf.size(); + throw std::runtime_error("Dexter: invalid buffer size"); + } + + const bool require_timestamped_tx = (m_conf.enableSync and frame.ts.timestamp_valid); + + if (not m_channel_is_up) { + if (require_timestamped_tx) { + if (m_clock_state == DexterClockState::Startup) { + return; // not ready + } + else { + constexpr uint64_t TIMESTAMP_PPS_PER_DSP_CLOCKS = DSP_CLOCK / 16384000; + // TIMESTAMP_PPS_PER_DSP_CLOCKS=10 because timestamp_pps is represented in 16.384 MHz clocks + uint64_t frame_start_clocks = + // at second level + ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK + m_clock_count_at_startup + + // at subsecond level + (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS; + + const double margin_s = frame.ts.offset_to_system_time(); + + long long clks = 0; + int r = 0; + if ((r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "clks", &clks)) != 0) { + etiLog.level(error) << "Failed to get dexter_dsp_tx.clks: " << get_iio_error(r); + throw std::runtime_error("Dexter: Cannot read IIO attribute"); + } + + const double margin_device_s = (double)(frame_start_clocks - clks) / DSP_CLOCK; + + etiLog.level(debug) << "DEXTER FCT " << frame.ts.fct << " TS CLK " << + ((int64_t)frame.ts.timestamp_sec - (int64_t)m_utc_seconds_at_startup) * DSP_CLOCK << " + " << + m_clock_count_at_startup << " + " << + (uint64_t)frame.ts.timestamp_pps * TIMESTAMP_PPS_PER_DSP_CLOCKS << " = " << + frame_start_clocks << " DELTA " << margin_s << " " << margin_device_s; + + // Ensure we hand the frame over to HW with a bit of margin + if (margin_s < 0.2) { + etiLog.level(warn) << "Skip frame short margin " << margin_s; + num_late++; + return; + } + + if ((r = iio_device_attr_write_longlong(m_dexter_dsp_tx, "stream0_start_clks", frame_start_clocks)) != 0) { + etiLog.level(warn) << "Skip frame, failed to set dexter_dsp_tx.stream0_start_clks = " << frame_start_clocks << " : " << get_iio_error(r); + num_late++; + return; + } + m_require_timestamp_refresh = false; + } + } + + channel_up(); + } + + if (m_require_timestamp_refresh) { + etiLog.level(debug) << "DEXTER REQUIRE REFRESH"; + channel_down(); + m_require_timestamp_refresh = false; + } + + // DabMod::launch_modulator ensures we get int16_t IQ here + //const size_t num_samples = frame.buf.size() / (2*sizeof(int16_t)); + //const int16_t *buf = reinterpret_cast<const int16_t*>(frame.buf.data()); + + if (m_channel_is_up) { + for (size_t i = 0; i < IIO_BUFFERS; i++) { + constexpr size_t buflen_samps = TRANSMISSION_FRAME_LEN_SAMPS / IIO_BUFFERS; + constexpr size_t buflen = buflen_samps * sizeof(int16_t); + + memcpy(iio_buffer_start(m_buffer), frame.buf.data() + (i * buflen), buflen); + ssize_t pushed = iio_buffer_push(m_buffer); + if (pushed < 0) { + etiLog.level(error) << "Dexter: failed to push buffer " << get_iio_error(pushed) << + " after " << num_buffers_pushed << " bufs"; + num_buffers_pushed = 0; + channel_down(); + break; + } + num_buffers_pushed++; + } + num_frames_modulated++; + } + + { + std::unique_lock<std::mutex> lock(m_attr_thread_mutex); + size_t u = underflows; + lock.unlock(); + + if (u != 0 and u != prev_underflows) { + etiLog.level(warn) << "Dexter: underflow! " << prev_underflows << " -> " << u; + } + + prev_underflows = u; + } +} + +void Dexter::underflow_read_process() +{ + m_underflow_ctx = iio_create_local_context(); + if (not m_underflow_ctx) { + throw std::runtime_error("Dexter: Unable to create iio context for underflow"); + } + + auto dexter_dsp_tx = iio_context_find_device(m_ctx, "dexter_dsp_tx"); + if (not dexter_dsp_tx) { + throw std::runtime_error("Dexter: Unable to find dexter_dsp_tx iio device"); + } + + set_thread_name("dexter_underflow"); + + while (m_running) { + this_thread::sleep_for(chrono::seconds(1)); + long long underflows_attr = 0; + + int r = iio_device_attr_read_longlong(m_dexter_dsp_tx, "buffer_underflows0", &underflows_attr); + + if (r == 0) { + size_t underflows_new = underflows_attr; + + std::unique_lock<std::mutex> lock(m_attr_thread_mutex); + if (underflows_new != underflows and underflows_attr != 0) { + underflows = underflows_new; + } + } + } + m_running = false; +} + +Dexter::~Dexter() +{ + m_running = false; + if (m_underflow_read_thread.joinable()) { + m_underflow_read_thread.join(); + } + + if (m_ctx) { + if (m_dexter_dsp_tx) { + iio_device_attr_write_longlong(m_dexter_dsp_tx, "gain0", 0); + } + + if (m_buffer) { + iio_buffer_destroy(m_buffer); + m_buffer = nullptr; + } + + if (m_tx_channel) { + iio_channel_disable(m_tx_channel); + } + + iio_context_destroy(m_ctx); + m_ctx = nullptr; + } + + if (m_underflow_ctx) { + iio_context_destroy(m_underflow_ctx); + m_underflow_ctx = nullptr; + } +} + +} // namespace Output + +#endif // HAVE_DEXTER diff --git a/src/output/Dexter.h b/src/output/Dexter.h new file mode 100644 index 0000000..d4f425f --- /dev/null +++ b/src/output/Dexter.h @@ -0,0 +1,138 @@ +/* + Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the + Queen in Right of Canada (Communications Research Center Canada) + + Copyright (C) 2023 + Matthias P. Braendli, matthias.braendli@mpb.li + + http://opendigitalradio.org + +DESCRIPTION: + It is an output driver using libiio targeting the PrecisionWave DEXTER board. +*/ + +/* + 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_DEXTER +#include "iio.h" + +#include <string> +#include <memory> +#include <ctime> +#include <mutex> +#include <thread> +#include <variant> + +#include "output/SDR.h" +#include "ModPlugin.h" +#include "EtiReader.h" +#include "RemoteControl.h" + +namespace Output { + +enum class DexterClockState { + Startup, + Normal, + Holdover +}; + +class Dexter : public Output::SDRDevice +{ + public: + Dexter(SDRDeviceConfig& config); + Dexter(const Dexter& other) = delete; + Dexter& operator=(const Dexter& other) = delete; + virtual ~Dexter(); + + virtual void tune(double lo_offset, double frequency) override; + virtual double get_tx_freq(void) const override; + virtual void set_txgain(double txgain) override; + virtual double get_txgain() const override; + virtual void set_bandwidth(double bandwidth) override; + virtual double get_bandwidth() const override; + virtual void transmit_frame(struct FrameData&& frame) override; + virtual run_statistics_t get_run_statistics() const override; + virtual double get_real_secs() const override; + + virtual void set_rxgain(double rxgain) override; + virtual double get_rxgain() const override; + virtual size_t receive_frame( + complexf *buf, + size_t num_samples, + frame_timestamp& ts, + double timeout_secs) override; + + // Return true if GPS and reference clock inputs are ok + virtual bool is_clk_source_ok() override; + virtual const char* device_name() const override; + + virtual std::optional<double> get_temperature() const override; + + private: + void channel_up(); + void channel_down(); + void handle_hw_time(); + + bool m_channel_is_up = false; + + SDRDeviceConfig& m_conf; + + struct iio_context* m_ctx = nullptr; + struct iio_device* m_dexter_dsp_tx = nullptr; + + struct iio_device* m_ad9957 = nullptr; + struct iio_device* m_ad9957_tx0 = nullptr; + struct iio_channel* m_tx_channel = nullptr; + struct iio_buffer *m_buffer = nullptr; + + /* Underflows are counted in a separate thread */ + struct iio_context* m_underflow_ctx = nullptr; + std::atomic<bool> m_running = ATOMIC_VAR_INIT(false); + std::thread m_underflow_read_thread; + void underflow_read_process(); + mutable std::mutex m_attr_thread_mutex; + size_t underflows = 0; + + size_t prev_underflows = 0; + size_t num_late = 0; + size_t num_frames_modulated = 0; + + size_t num_buffers_pushed = 0; + + DexterClockState m_clock_state = DexterClockState::Startup; + + // Only valid when m_clock_state is not Startup + uint64_t m_utc_seconds_at_startup = 0; + uint64_t m_clock_count_at_startup = 0; + + // Only valid when m_clock_state Holdover + std::chrono::steady_clock::time_point m_holdover_since = + std::chrono::steady_clock::time_point::min(); + std::time_t m_holdover_since_t = 0; +}; + +} // namespace Output + +#endif //HAVE_DEXTER + diff --git a/src/output/Feedback.cpp b/src/output/Feedback.cpp index 88d8319..d112b5a 100644 --- a/src/output/Feedback.cpp +++ b/src/output/Feedback.cpp @@ -84,7 +84,7 @@ DPDFeedbackServer::~DPDFeedbackServer() void DPDFeedbackServer::set_tx_frame( const std::vector<uint8_t> &buf, - const struct frame_timestamp &buf_ts) + const frame_timestamp &buf_ts) { if (not m_running) { throw runtime_error("DPDFeedbackServer not running"); diff --git a/src/output/Feedback.h b/src/output/Feedback.h index aef86b0..b31347f 100644 --- a/src/output/Feedback.h +++ b/src/output/Feedback.h @@ -94,7 +94,7 @@ class DPDFeedbackServer { ~DPDFeedbackServer(); void set_tx_frame(const std::vector<uint8_t> &buf, - const struct frame_timestamp& ts); + const frame_timestamp& ts); private: // Thread that reacts to burstRequests and receives from the SDR device diff --git a/src/output/Lime.cpp b/src/output/Lime.cpp index 593cddb..47045e7 100644 --- a/src/output/Lime.cpp +++ b/src/output/Lime.cpp @@ -318,13 +318,14 @@ double Lime::get_bandwidth(void) const return bw; } -SDRDevice::RunStatistics Lime::get_run_statistics(void) const +SDRDevice::run_statistics_t Lime::get_run_statistics(void) const { - RunStatistics rs; - rs.num_underruns = underflows; - rs.num_overruns = overflows; - rs.num_late_packets = late_packets; - rs.num_frames_modulated = num_frames_modulated; + run_statistics_t rs; + rs["underruns"] = underflows; + rs["overruns"] = overflows; + rs["dropped_packets"] = dropped_packets; + rs["frames"] = num_frames_modulated; + rs["fifo_fill"] = m_last_fifo_fill_percent * 100; return rs; } @@ -348,14 +349,14 @@ double Lime::get_rxgain(void) const size_t Lime::receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp &ts, + frame_timestamp &ts, double timeout_secs) { // TODO return 0; } -bool Lime::is_clk_source_ok() const +bool Lime::is_clk_source_ok() { // TODO return true; @@ -366,25 +367,23 @@ const char *Lime::device_name(void) const return "Lime"; } -double Lime::get_temperature(void) const +std::optional<double> Lime::get_temperature(void) const { if (not m_device) throw runtime_error("Lime device not set up"); - float_type temp = numeric_limits<float_type>::quiet_NaN(); - if (LMS_GetChipTemperature(m_device, 0, &temp) < 0) - { + float_type temp = 0; + if (LMS_GetChipTemperature(m_device, 0, &temp) >= 0) { + return temp; + } + else { etiLog.level(error) << "Error getting LimeSDR temperature: %s " << LMS_GetLastErrorMessage(); + return std::nullopt; } - return temp; } -float Lime::get_fifo_fill_percent(void) const -{ - return m_last_fifo_fill_percent * 100; -} -void Lime::transmit_frame(const struct FrameData &frame) +void Lime::transmit_frame(struct FrameData&& frame) { if (not m_device) throw runtime_error("Lime device not set up"); @@ -406,7 +405,7 @@ void Lime::transmit_frame(const struct FrameData &frame) LMS_GetStreamStatus(&m_tx_stream, &LimeStatus); overflows += LimeStatus.overrun; underflows += LimeStatus.underrun; - late_packets += LimeStatus.droppedPackets; + dropped_packets += LimeStatus.droppedPackets; #ifdef LIMEDEBUG etiLog.level(info) << LimeStatus.fifoFilledCount << "/" << LimeStatus.fifoSize << ":" << numSamples << "Rate" << LimeStatus.linkRate / (2 * 2.0); diff --git a/src/output/Lime.h b/src/output/Lime.h index 72a018e..4510bf2 100644 --- a/src/output/Lime.h +++ b/src/output/Lime.h @@ -66,8 +66,8 @@ class Lime : public Output::SDRDevice virtual double get_txgain(void) const override; virtual void set_bandwidth(double bandwidth) override; virtual double get_bandwidth(void) const override; - virtual void transmit_frame(const struct FrameData &frame) override; - virtual RunStatistics get_run_statistics(void) const override; + virtual void transmit_frame(struct FrameData&& frame) override; + virtual run_statistics_t get_run_statistics(void) const override; virtual double get_real_secs(void) const override; virtual void set_rxgain(double rxgain) override; @@ -75,15 +75,14 @@ class Lime : public Output::SDRDevice virtual size_t receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp &ts, + frame_timestamp &ts, double timeout_secs) override; // Return true if GPS and reference clock inputs are ok - virtual bool is_clk_source_ok(void) const override; + virtual bool is_clk_source_ok(void) override; virtual const char *device_name(void) const override; - virtual double get_temperature(void) const override; - virtual float get_fifo_fill_percent(void) const; + virtual std::optional<double> get_temperature(void) const override; private: SDRDeviceConfig &m_conf; @@ -95,11 +94,10 @@ class Lime : public Output::SDRDevice std::vector<complexf> interpolatebuf; std::vector<short> m_i16samples; std::atomic<float> m_last_fifo_fill_percent = ATOMIC_VAR_INIT(0); - size_t underflows = 0; size_t overflows = 0; - size_t late_packets = 0; + size_t dropped_packets = 0; size_t num_frames_modulated = 0; }; diff --git a/src/output/SDR.cpp b/src/output/SDR.cpp index b0c09b6..75a7ee3 100644 --- a/src/output/SDR.cpp +++ b/src/output/SDR.cpp @@ -2,7 +2,7 @@ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the Queen in Right of Canada (Communications Research Center Canada) - Copyright (C) 2022 + Copyright (C) 2023 Matthias P. Braendli, matthias.braendli@mpb.li http://opendigitalradio.org @@ -25,7 +25,9 @@ */ #include "output/SDR.h" +#include "output/UHD.h" #include "output/Lime.h" +#include "output/Dexter.h" #include "PcDebug.h" #include "Log.h" @@ -46,17 +48,16 @@ using namespace std; namespace Output { -// Maximum number of frames that can wait in frames -static constexpr size_t FRAMES_MAX_SIZE = 8; +// Maximum number of frames that can wait in frames. +// Keep it low when not using synchronised transmission, in order to reduce delay. +// When using synchronised transmission, use a 6s buffer to give us enough margin. +static constexpr size_t FRAMES_MAX_SIZE_UNSYNC = 8; +static constexpr size_t FRAMES_MAX_SIZE_SYNC = 250; // 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(), ModMetadata(), RemoteControllable("sdr"), m_config(config), @@ -77,20 +78,40 @@ SDR::SDR(SDRDeviceConfig& config, std::shared_ptr<SDRDevice> device) : RC_ADD_PARAMETER(txgain, "TX gain"); RC_ADD_PARAMETER(rxgain, "RX gain for DPD feedback"); RC_ADD_PARAMETER(bandwidth, "Analog front-end bandwidth"); - RC_ADD_PARAMETER(freq, "Transmission frequency"); + RC_ADD_PARAMETER(freq, "Transmission frequency in Hz"); + RC_ADD_PARAMETER(channel, "Transmission frequency as channel"); RC_ADD_PARAMETER(muting, "Mute the output by stopping the transmitter"); RC_ADD_PARAMETER(temp, "Temperature in degrees C of the device"); RC_ADD_PARAMETER(underruns, "Counter of number of underruns"); RC_ADD_PARAMETER(latepackets, "Counter of number of late packets"); RC_ADD_PARAMETER(frames, "Counter of number of frames modulated"); - RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO"); - RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss"); + RC_ADD_PARAMETER(synchronous, "1 if configured for synchronous transmission"); + RC_ADD_PARAMETER(max_gps_holdover_time, "Max holdover duration in seconds"); + +#ifdef HAVE_OUTPUT_UHD + if (std::dynamic_pointer_cast<UHD>(device)) { + RC_ADD_PARAMETER(gpsdo_num_sv, "Number of Satellite Vehicles tracked by GPSDO"); + RC_ADD_PARAMETER(gpsdo_holdover, "1 if the GPSDO is in holdover, 0 if it is using gnss"); + } +#endif // HAVE_OUTPUT_UHD + + RC_ADD_PARAMETER(queued_frames_ms, "Number of frames queued, represented in milliseconds"); #ifdef HAVE_LIMESDR if (std::dynamic_pointer_cast<Lime>(device)) { RC_ADD_PARAMETER(fifo_fill, "A value representing the Lime FIFO fullness [percent]"); } #endif // HAVE_LIMESDR + +#ifdef HAVE_DEXTER + if (std::dynamic_pointer_cast<Dexter>(device)) { + RC_ADD_PARAMETER(in_holdover_since, "DEXTER timestamp when holdover began"); + RC_ADD_PARAMETER(remaining_holdover_s, "DEXTER remaining number of seconds in holdover"); + RC_ADD_PARAMETER(clock_state, "DEXTER clock state: startup/normal/holdover"); + } +#endif // HAVE_DEXTER + + } SDR::~SDR() @@ -104,6 +125,11 @@ SDR::~SDR() } } +void SDR::set_sample_size(size_t size) +{ + m_size = size; +} + int SDR::process(Buffer *dataIn) { if (not m_running) { @@ -125,6 +151,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn) if (m_device and m_running) { FrameData frame; frame.buf = std::move(m_frame); + frame.sampleSize = m_size; if (metadataIn.empty()) { etiLog.level(info) << @@ -138,7 +165,7 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn) * This behaviour is different to earlier versions of ODR-DabMod, * which took the timestamp from the latest ETI frame. */ - frame.ts = *(metadataIn[0].ts); + frame.ts = metadataIn[0].ts; // TODO check device running @@ -157,9 +184,12 @@ meta_vec_t SDR::process_metadata(const meta_vec_t& metadataIn) 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); + + const auto max_size = m_config.enableSync ? FRAMES_MAX_SIZE_SYNC : FRAMES_MAX_SIZE_UNSYNC; + auto r = m_queue.push_overflow(std::move(frame), max_size); + etiLog.log(trace, "SDR,push %d %zu", r.overflowed, r.new_size); + + num_queue_overflows += r.overflowed ? 1 : 0; } } else { @@ -180,16 +210,13 @@ void SDR::process_thread_entry() last_tx_time_initialised = false; - size_t last_num_underflows = 0; - size_t pop_prebuffering = FRAMES_MAX_SIZE; - m_running.store(true); try { while (m_running.load()) { struct FrameData frame; etiLog.log(trace, "SDR,wait"); - m_queue.wait_and_pop(frame, pop_prebuffering); + m_queue.wait_and_pop(frame); etiLog.log(trace, "SDR,pop"); if (m_running.load() == false) { @@ -197,20 +224,7 @@ void SDR::process_thread_entry() } 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; + handle_frame(std::move(frame)); } } } @@ -236,46 +250,19 @@ const char* SDR::name() return m_name.c_str(); } -void SDR::sleep_through_frame() -{ - using namespace std::chrono; - - const auto now = steady_clock::now(); - if (not t_last_frame_initialised) { - t_last_frame = now; - t_last_frame_initialised = true; - } - - const auto delta = now - t_last_frame; - const auto wait_time = transmission_frame_duration(m_config.dabMode); - - if (wait_time > delta) { - this_thread::sleep_for(wait_time - delta); - } - - t_last_frame += wait_time; -} - -void SDR::handle_frame(struct FrameData& frame) +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; } const auto& time_spec = frame.ts; - if (m_config.enableSync and m_config.muteNoTimestamps and - not time_spec.timestamp_valid) { - sleep_through_frame(); - etiLog.log(info, - "OutputSDR: Muting sample %d : no timestamp\n", - frame.ts.fct); + if (m_config.enableSync and m_config.muteNoTimestamps and not time_spec.timestamp_valid) { + etiLog.log(info, "OutputSDR: Muting sample %d : no timestamp\n", frame.ts.fct); return; } @@ -298,11 +285,12 @@ void SDR::handle_frame(struct FrameData& frame) } if (frame.ts.offset_changed) { + etiLog.level(debug) << "TS offset changed"; m_device->require_timestamp_refresh(); } if (last_tx_time_initialised) { - const size_t sizeIn = frame.buf.size() / sizeof(complexf); + const size_t sizeIn = frame.buf.size() / frame.sampleSize; // Checking units for the increment calculation: // samps * ticks/s / (samps/s) @@ -341,7 +329,7 @@ void SDR::handle_frame(struct FrameData& frame) etiLog.log(trace, "SDR,tist %f", time_spec.get_real_secs()); - if (time_spec.get_real_secs() + tx_timeout < device_time) { + if (time_spec.get_real_secs() < device_time) { etiLog.level(warn) << "OutputSDR: Timestamp in the past at FCT=" << frame.ts.fct << " offset: " << std::fixed << @@ -350,6 +338,7 @@ void SDR::handle_frame(struct FrameData& frame) " frame " << frame.ts.fct << ", tx_second " << tx_second << ", pps " << pps_offset; + m_device->require_timestamp_refresh(); return; } @@ -363,13 +352,12 @@ void SDR::handle_frame(struct FrameData& frame) } if (m_config.muting) { - etiLog.log(info, - "OutputSDR: Muting FCT=%d requested", - frame.ts.fct); + etiLog.log(info, "OutputSDR: Muting FCT=%d requested", frame.ts.fct); + m_device->require_timestamp_refresh(); return; } - m_device->transmit_frame(frame); + m_device->transmit_frame(std::move(frame)); } // ======================================= @@ -397,21 +385,33 @@ void SDR::set_parameter(const string& parameter, const string& value) m_device->tune(m_config.lo_offset, m_config.frequency); m_config.frequency = m_device->get_tx_freq(); } + else if (parameter == "channel") { + try { + const double frequency = parse_channel(value); + + m_config.frequency = frequency; + m_device->tune(m_config.lo_offset, m_config.frequency); + m_config.frequency = m_device->get_tx_freq(); + } + catch (const std::out_of_range& e) { + throw ParameterError("Cannot parse channel"); + } + } else if (parameter == "muting") { ss >> m_config.muting; } - else if (parameter == "underruns" or - parameter == "latepackets" or - parameter == "frames" or - parameter == "gpsdo_num_sv" or - parameter == "gpsdo_holdover" or - parameter == "fifo_fill") { - throw ParameterError("Parameter " + parameter + " is read-only."); + else if (parameter == "synchronous") { + uint32_t enableSync = 0; + ss >> enableSync; + m_config.enableSync = enableSync > 0; + } + else if (parameter == "max_gps_holdover_time") { + ss >> m_config.maxGPSHoldoverTime; } else { stringstream ss_err; ss_err << "Parameter '" << parameter - << "' is not exported by controllable " << get_rc_name(); + << "' is read-only or not exported by controllable " << get_rc_name(); throw ParameterError(ss_err.str()); } } @@ -432,6 +432,16 @@ const string SDR::get_parameter(const string& parameter) const else if (parameter == "freq") { ss << m_config.frequency; } + else if (parameter == "channel") { + const auto maybe_freq = convert_frequency_to_channel(m_config.frequency); + + if (maybe_freq.has_value()) { + ss << *maybe_freq; + } + else { + throw ParameterError("Frequency is outside list of channels"); + } + } else if (parameter == "muting") { ss << m_config.muting; } @@ -439,55 +449,57 @@ const string SDR::get_parameter(const string& parameter) const if (not m_device) { throw ParameterError("OutputSDR has no device"); } - const double temp = m_device->get_temperature(); - if (std::isnan(temp)) { - throw ParameterError("Temperature not available"); + const std::optional<double> temp = m_device->get_temperature(); + if (temp) { + ss << *temp; } else { - ss << temp; - } - } - 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; + throw ParameterError("Temperature not available"); } } - else if (parameter == "gpsdo_num_sv") { - const auto stat = m_device->get_run_statistics(); - ss << stat.gpsdo_num_sv; + else if (parameter == "queued_frames_ms") { + ss << m_queue.size() * + chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode)) + .count(); } - else if (parameter == "gpsdo_holdover") { - const auto stat = m_device->get_run_statistics(); - ss << (stat.gpsdo_holdover ? 1 : 0); + else if (parameter == "synchronous") { + ss << m_config.enableSync; } -#ifdef HAVE_LIMESDR - else if (parameter == "fifo_fill") { - const auto dev = std::dynamic_pointer_cast<Lime>(m_device); - - if (dev) { - ss << dev->get_fifo_fill_percent(); - } - else { - ss << "Parameter '" << parameter << - "' is not exported by controllable " << get_rc_name(); - throw ParameterError(ss.str()); - } + else if (parameter == "max_gps_holdover_time") { + ss << m_config.maxGPSHoldoverTime; } -#endif // HAVE_LIMESDR else { + if (m_device) { + const auto stat = m_device->get_run_statistics(); + try { + const auto& value = stat.at(parameter).v; + if (std::holds_alternative<string>(value)) { + ss << std::get<string>(value); + } + else if (std::holds_alternative<double>(value)) { + ss << std::get<double>(value); + } + else if (std::holds_alternative<ssize_t>(value)) { + ss << std::get<ssize_t>(value); + } + else if (std::holds_alternative<size_t>(value)) { + ss << std::get<size_t>(value); + } + else if (std::holds_alternative<bool>(value)) { + ss << (std::get<bool>(value) ? 1 : 0); + } + else if (std::holds_alternative<std::nullopt_t>(value)) { + ss << ""; + } + else { + throw std::logic_error("variant alternative not handled"); + } + return ss.str(); + } + catch (const std::out_of_range&) { + } + } + ss << "Parameter '" << parameter << "' is not exported by controllable " << get_rc_name(); throw ParameterError(ss.str()); @@ -495,4 +507,39 @@ const string SDR::get_parameter(const string& parameter) const return ss.str(); } +const json::map_t SDR::get_all_values() const +{ + json::map_t stat = m_device->get_run_statistics(); + + stat["txgain"].v = m_config.txgain; + stat["rxgain"].v = m_config.rxgain; + stat["freq"].v = m_config.frequency; + stat["muting"].v = m_config.muting; + stat["temp"].v = std::nullopt; + + const auto maybe_freq = convert_frequency_to_channel(m_config.frequency); + + if (maybe_freq.has_value()) { + stat["channel"].v = *maybe_freq; + } + else { + stat["channel"].v = std::nullopt; + } + + if (m_device) { + const std::optional<double> temp = m_device->get_temperature(); + if (temp) { + stat["temp"].v = *temp; + } + } + stat["queued_frames_ms"].v = m_queue.size() * + (size_t)chrono::duration_cast<chrono::milliseconds>(transmission_frame_duration(m_config.dabMode)) + .count(); + + stat["synchronous"].v = m_config.enableSync; + stat["max_gps_holdover_time"].v = (size_t)m_config.maxGPSHoldoverTime; + + return stat; +} + } // namespace Output diff --git a/src/output/SDR.h b/src/output/SDR.h index 33477bf..960de0c 100644 --- a/src/output/SDR.h +++ b/src/output/SDR.h @@ -2,7 +2,7 @@ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the Queen in Right of Canada (Communications Research Center Canada) - Copyright (C) 2022 + Copyright (C) 2023 Matthias P. Braendli, matthias.braendli@mpb.li http://opendigitalradio.org @@ -51,6 +51,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable { SDR operator=(const SDR& other) = delete; virtual ~SDR(); + virtual void set_sample_size(size_t size); virtual int process(Buffer *dataIn) override; virtual meta_vec_t process_metadata(const meta_vec_t& metadataIn) override; @@ -66,15 +67,17 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable { virtual const std::string get_parameter( const std::string& parameter) const override; + virtual const json::map_t get_all_values() const override; + private: void process_thread_entry(void); - void handle_frame(struct FrameData &frame); - void sleep_through_frame(void); + void handle_frame(struct FrameData&& frame); SDRDeviceConfig& m_config; std::atomic<bool> m_running = ATOMIC_VAR_INIT(false); std::thread m_device_thread; + size_t m_size = sizeof(complexf); std::vector<uint8_t> m_frame; ThreadsafeQueue<FrameData> m_queue; @@ -86,9 +89,7 @@ class SDR : public ModOutput, public ModMetadata, public RemoteControllable { bool last_tx_time_initialised = false; uint32_t last_tx_second = 0; uint32_t last_tx_pps = 0; - - bool t_last_frame_initialised = false; - std::chrono::steady_clock::time_point t_last_frame; + size_t num_queue_overflows = 0; }; } diff --git a/src/output/SDRDevice.h b/src/output/SDRDevice.h index b599f5a..378829c 100644 --- a/src/output/SDRDevice.h +++ b/src/output/SDRDevice.h @@ -2,7 +2,7 @@ Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Her Majesty the Queen in Right of Canada (Communications Research Center Canada) - Copyright (C) 2022 + Copyright (C) 2023 Matthias P. Braendli, matthias.braendli@mpb.li http://opendigitalradio.org @@ -38,6 +38,9 @@ DESCRIPTION: #include <string> #include <vector> #include <complex> +#include <variant> +#include <optional> +#include <unordered_map> #include "TimestampDecoder.h" @@ -98,32 +101,25 @@ struct SDRDeviceConfig { struct FrameData { // Buffer holding frame data std::vector<uint8_t> buf; + size_t sampleSize = sizeof(complexf); // A full timestamp contains a TIST according to standard // and time information within MNSC with tx_second. - struct frame_timestamp ts; + frame_timestamp ts; }; // All SDR Devices must implement the SDRDevice interface class SDRDevice { public: - struct RunStatistics { - size_t num_underruns = 0; - size_t num_late_packets = 0; - size_t num_overruns = 0; - size_t num_frames_modulated = 0; - - int gpsdo_num_sv = 0; - bool gpsdo_holdover = false; - }; + using run_statistics_t = json::map_t; virtual void tune(double lo_offset, double frequency) = 0; virtual double get_tx_freq(void) const = 0; virtual void set_txgain(double txgain) = 0; virtual double get_txgain(void) const = 0; - virtual void transmit_frame(const struct FrameData& frame) = 0; - virtual RunStatistics get_run_statistics(void) const = 0; + virtual void transmit_frame(struct FrameData&& frame) = 0; + virtual run_statistics_t get_run_statistics(void) const = 0; virtual double get_real_secs(void) const = 0; virtual void set_rxgain(double rxgain) = 0; virtual double get_rxgain(void) const = 0; @@ -132,14 +128,14 @@ class SDRDevice { virtual size_t receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) = 0; - // Returns device temperature in degrees C or NaN if not available - virtual double get_temperature(void) const = 0; + // Returns device temperature in degrees C + virtual std::optional<double> get_temperature(void) const = 0; // Return true if GPS and reference clock inputs are ok - virtual bool is_clk_source_ok(void) const = 0; + virtual bool is_clk_source_ok(void) = 0; virtual const char* device_name(void) const = 0; diff --git a/src/output/Soapy.cpp b/src/output/Soapy.cpp index 684a9a4..7931860 100644 --- a/src/output/Soapy.cpp +++ b/src/output/Soapy.cpp @@ -180,13 +180,13 @@ double Soapy::get_bandwidth(void) const return m_device->getBandwidth(SOAPY_SDR_TX, 0); } -SDRDevice::RunStatistics Soapy::get_run_statistics(void) const +SDRDevice::run_statistics_t Soapy::get_run_statistics(void) const { - RunStatistics rs; - rs.num_underruns = underflows; - rs.num_overruns = overflows; - rs.num_late_packets = late_packets; - rs.num_frames_modulated = num_frames_modulated; + run_statistics_t rs; + rs["underruns"].v = underflows; + rs["overruns"].v = overflows; + rs["timeouts"].v = timeouts; + rs["frames"].v = num_frames_modulated; return rs; } @@ -216,7 +216,7 @@ double Soapy::get_rxgain(void) const size_t Soapy::receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) { int flags = 0; @@ -254,7 +254,7 @@ size_t Soapy::receive_frame( } -bool Soapy::is_clk_source_ok() const +bool Soapy::is_clk_source_ok() { // TODO return true; @@ -265,14 +265,14 @@ const char* Soapy::device_name(void) const return "Soapy"; } -double Soapy::get_temperature(void) const +std::optional<double> Soapy::get_temperature(void) const { // TODO Unimplemented // LimeSDR exports 'lms7_temp' - return std::numeric_limits<double>::quiet_NaN(); + return std::nullopt; } -void Soapy::transmit_frame(const struct FrameData& frame) +void Soapy::transmit_frame(struct FrameData&& frame) { if (not m_device) throw runtime_error("Soapy device not set up"); @@ -320,6 +320,7 @@ void Soapy::transmit_frame(const struct FrameData& frame) m_tx_stream, buffs, samps_to_send, flags, timeNs); if (num_sent == SOAPY_SDR_TIMEOUT) { + timeouts++; continue; } else if (num_sent == SOAPY_SDR_OVERFLOW) { @@ -349,6 +350,7 @@ void Soapy::transmit_frame(const struct FrameData& frame) SoapySDR::errToStr(ret_deact)); } m_tx_stream_active = false; + m_require_timestamp_refresh = false; } if (eob_because_muting) { diff --git a/src/output/Soapy.h b/src/output/Soapy.h index 4ee53ca..4fce11a 100644 --- a/src/output/Soapy.h +++ b/src/output/Soapy.h @@ -65,8 +65,8 @@ class Soapy : public Output::SDRDevice virtual double get_txgain(void) const override; virtual void set_bandwidth(double bandwidth) override; virtual double get_bandwidth(void) const override; - virtual void transmit_frame(const struct FrameData& frame) override; - virtual RunStatistics get_run_statistics(void) const override; + virtual void transmit_frame(struct FrameData&& frame) override; + virtual run_statistics_t get_run_statistics(void) const override; virtual double get_real_secs(void) const override; virtual void set_rxgain(double rxgain) override; @@ -74,14 +74,14 @@ class Soapy : public Output::SDRDevice virtual size_t receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) override; // Return true if GPS and reference clock inputs are ok - virtual bool is_clk_source_ok(void) const override; + virtual bool is_clk_source_ok(void) override; virtual const char* device_name(void) const override; - virtual double get_temperature(void) const override; + virtual std::optional<double> get_temperature(void) const override; private: SDRDeviceConfig& m_conf; @@ -91,9 +91,9 @@ class Soapy : public Output::SDRDevice SoapySDR::Stream *m_rx_stream = nullptr; bool m_rx_stream_active = false; + size_t timeouts = 0; size_t underflows = 0; size_t overflows = 0; - size_t late_packets = 0; size_t num_frames_modulated = 0; }; diff --git a/src/output/UHD.cpp b/src/output/UHD.cpp index ac34ce4..094e021 100644 --- a/src/output/UHD.cpp +++ b/src/output/UHD.cpp @@ -315,7 +315,7 @@ double UHD::get_bandwidth(void) const return m_usrp->get_tx_bandwidth(); } -void UHD::transmit_frame(const struct FrameData& frame) +void UHD::transmit_frame(struct FrameData&& frame) { const double tx_timeout = 20.0; const size_t sizeIn = frame.buf.size() / sizeof(complexf); @@ -350,6 +350,7 @@ void UHD::transmit_frame(const struct FrameData& frame) frame.ts.timestamp_valid and m_require_timestamp_refresh and samps_to_send <= usrp_max_num_samps ); + m_require_timestamp_refresh = false; //send a single packet size_t num_tx_samps = m_tx_stream->send( @@ -359,7 +360,7 @@ void UHD::transmit_frame(const struct FrameData& frame) num_acc_samps += num_tx_samps; - md_tx.time_spec += uhd::time_spec_t(0, num_tx_samps/m_conf.sampleRate); + md_tx.time_spec += uhd::time_spec_t::from_ticks(num_tx_samps, (double)m_conf.sampleRate); if (num_tx_samps == 0) { etiLog.log(warn, @@ -376,18 +377,22 @@ void UHD::transmit_frame(const struct FrameData& frame) } -SDRDevice::RunStatistics UHD::get_run_statistics(void) const +SDRDevice::run_statistics_t UHD::get_run_statistics(void) const { - 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; + run_statistics_t rs; + rs["underruns"].v = num_underflows; + rs["overruns"].v = num_overflows; + rs["late_packets"].v = num_late_packets; + rs["frames"].v = num_frames_modulated; if (m_device_time) { const auto gpsdo_stat = m_device_time->get_gnss_stats(); - rs.gpsdo_holdover = gpsdo_stat.holdover; - rs.gpsdo_num_sv = gpsdo_stat.num_sv; + rs["gpsdo_holdover"].v = gpsdo_stat.holdover; + rs["gpsdo_num_sv"].v = gpsdo_stat.num_sv; + } + else { + rs["gpsdo_holdover"].v = true; + rs["gpsdo_num_sv"].v = 0; } return rs; } @@ -411,7 +416,7 @@ double UHD::get_rxgain() const size_t UHD::receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) { uhd::stream_cmd_t cmd( @@ -434,7 +439,7 @@ size_t UHD::receive_frame( } // Return true if GPS and reference clock inputs are ok -bool UHD::is_clk_source_ok(void) const +bool UHD::is_clk_source_ok(void) { bool ok = true; @@ -471,13 +476,13 @@ const char* UHD::device_name(void) const return "UHD"; } -double UHD::get_temperature(void) const +std::optional<double> UHD::get_temperature(void) const { try { return std::round(m_usrp->get_tx_sensor("temp", 0).to_real()); } catch (const uhd::lookup_error &e) { - return std::numeric_limits<double>::quiet_NaN(); + return std::nullopt; } } diff --git a/src/output/UHD.h b/src/output/UHD.h index 29867fb..9891c7a 100644 --- a/src/output/UHD.h +++ b/src/output/UHD.h @@ -55,14 +55,6 @@ DESCRIPTION: #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 @@ -79,8 +71,8 @@ class UHD : public Output::SDRDevice virtual double get_txgain(void) const override; virtual void set_bandwidth(double bandwidth) override; virtual double get_bandwidth(void) const override; - virtual void transmit_frame(const struct FrameData& frame) override; - virtual RunStatistics get_run_statistics(void) const override; + virtual void transmit_frame(struct FrameData&& frame) override; + virtual run_statistics_t get_run_statistics(void) const override; virtual double get_real_secs(void) const override; virtual void set_rxgain(double rxgain) override; @@ -88,14 +80,14 @@ class UHD : public Output::SDRDevice virtual size_t receive_frame( complexf *buf, size_t num_samples, - struct frame_timestamp& ts, + frame_timestamp& ts, double timeout_secs) override; // Return true if GPS and reference clock inputs are ok - virtual bool is_clk_source_ok(void) const override; + virtual bool is_clk_source_ok(void) override; virtual const char* device_name(void) const override; - virtual double get_temperature(void) const override; + virtual std::optional<double> get_temperature(void) const override; private: SDRDeviceConfig& m_conf; diff --git a/src/output/USRPTime.cpp b/src/output/USRPTime.cpp index d1197ec..5a11851 100644 --- a/src/output/USRPTime.cpp +++ b/src/output/USRPTime.cpp @@ -46,11 +46,14 @@ USRPTime::USRPTime( m_conf(conf), time_last_check(timepoint_t::clock::now()) { - if (m_conf.pps_src == "none") { + if (m_conf.refclk_src == "internal" and m_conf.pps_src != "none") { + etiLog.level(warn) << "OutputUHD: Unusal refclk and pps source settings. Setting time once, no monitoring."; + set_usrp_time_from_pps(); + } + else if (m_conf.pps_src == "none") { if (m_conf.enableSync) { etiLog.level(warn) << - "OutputUHD: WARNING:" - " you are using synchronous transmission without PPS input!"; + "OutputUHD: you are using synchronous transmission without PPS input!"; } set_usrp_time_from_localtime(); |