From 5d5b221e830b74fefe323e4478e2ae7788790a27 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 5 Mar 2021 13:37:49 +0000 Subject: [PATCH] Add Costas Loop PLL in Channel Analyzer Add loop bandwidth and other PLL controls to Channel Analyzer GUI. Fix bug where PLL lock frequency would be incorrect by the decimation factor. --- plugins/channelrx/chanalyzer/chanalyzer.cpp | 4 + .../channelrx/chanalyzer/chanalyzergui.cpp | 124 ++++++- plugins/channelrx/chanalyzer/chanalyzergui.h | 5 + plugins/channelrx/chanalyzer/chanalyzergui.ui | 344 +++++++++++++++--- .../chanalyzer/chanalyzersettings.cpp | 12 + .../channelrx/chanalyzer/chanalyzersettings.h | 4 + .../channelrx/chanalyzer/chanalyzersink.cpp | 81 ++++- plugins/channelrx/chanalyzer/chanalyzersink.h | 8 +- .../chanalyzer/chanalyzerwebapiadapter.cpp | 16 + sdrbase/CMakeLists.txt | 2 + sdrbase/dsp/costasloop.cpp | 110 ++++++ sdrbase/dsp/costasloop.h | 120 ++++++ sdrbase/resources/webapi/doc/html2/index.html | 18 +- .../doc/swagger/include/ChannelAnalyzer.yaml | 12 + .../api/swagger/include/ChannelAnalyzer.yaml | 12 + swagger/sdrangel/code/html2/index.html | 18 +- .../qt5/client/SWGChannelAnalyzerSettings.cpp | 92 +++++ .../qt5/client/SWGChannelAnalyzerSettings.h | 24 ++ 18 files changed, 933 insertions(+), 73 deletions(-) create mode 100644 sdrbase/dsp/costasloop.cpp create mode 100644 sdrbase/dsp/costasloop.h diff --git a/plugins/channelrx/chanalyzer/chanalyzer.cpp b/plugins/channelrx/chanalyzer/chanalyzer.cpp index f6f57398c..0873811a7 100644 --- a/plugins/channelrx/chanalyzer/chanalyzer.cpp +++ b/plugins/channelrx/chanalyzer/chanalyzer.cpp @@ -139,7 +139,11 @@ void ChannelAnalyzer::applySettings(const ChannelAnalyzerSettings& settings, boo << " m_ssb: " << settings.m_ssb << " m_pll: " << settings.m_pll << " m_fll: " << settings.m_fll + << " m_costasLoop: " << settings.m_costasLoop << " m_pllPskOrder: " << settings.m_pllPskOrder + << " m_pllBandwidth: " << settings.m_pllBandwidth + << " m_pllDampingFactor: " << settings.m_pllDampingFactor + << " m_pllLoopGain: " << settings.m_pllLoopGain << " m_inputType: " << (int) settings.m_inputType; ChannelAnalyzerBaseband::MsgConfigureChannelAnalyzerBaseband *msg diff --git a/plugins/channelrx/chanalyzer/chanalyzergui.cpp b/plugins/channelrx/chanalyzer/chanalyzergui.cpp index 2ca683fa0..6ca79a07b 100644 --- a/plugins/channelrx/chanalyzer/chanalyzergui.cpp +++ b/plugins/channelrx/chanalyzer/chanalyzergui.cpp @@ -103,18 +103,85 @@ void ChannelAnalyzerGUI::displaySettings() void ChannelAnalyzerGUI::displayPLLSettings() { - if (m_settings.m_fll) - { - ui->pllPskOrder->setCurrentIndex(5); - } + if (m_settings.m_costasLoop) + ui->pllType->setCurrentIndex(2); + else if (m_settings.m_fll) + ui->pllType->setCurrentIndex(1); + else + ui->pllType->setCurrentIndex(0); + setPLLVisibility(); + + int i = 0; + for(; ((m_settings.m_pllPskOrder>>i) & 1) == 0; i++); + if (m_settings.m_costasLoop) + ui->pllPskOrder->setCurrentIndex(i==0 ? 0 : i-1); else - { - int i = 0; - for(; ((m_settings.m_pllPskOrder>>i) & 1) == 0; i++); ui->pllPskOrder->setCurrentIndex(i); - } ui->pll->setChecked(m_settings.m_pll); + ui->pllBandwidth->setValue((int)(m_settings.m_pllBandwidth*1000.0)); + QString bandwidthStr = QString::number(m_settings.m_pllBandwidth, 'f', 3); + ui->pllBandwidthText->setText(bandwidthStr); + ui->pllDampingFactor->setValue((int)(m_settings.m_pllDampingFactor*10.0)); + QString factorStr = QString::number(m_settings.m_pllDampingFactor, 'f', 1); + ui->pllDampingFactorText->setText(factorStr); + ui->pllLoopGain->setValue((int)(m_settings.m_pllLoopGain)); + QString gainStr = QString::number(m_settings.m_pllLoopGain, 'f', 0); + ui->pllLoopGainText->setText(gainStr); +} + +void ChannelAnalyzerGUI::setPLLVisibility() +{ + ui->pllToolbar->setVisible(m_settings.m_pll); + + // BW + ui->pllPskOrder->setVisible(!m_settings.m_fll); + ui->pllLine1->setVisible(!m_settings.m_fll); + ui->pllBandwidthLabel->setVisible(!m_settings.m_fll); + ui->pllBandwidth->setVisible(!m_settings.m_fll); + ui->pllBandwidthText->setVisible(!m_settings.m_fll); + ui->pllLine2->setVisible(!m_settings.m_fll); + + // Damping factor and gain + bool stdPll = !m_settings.m_fll && !m_settings.m_costasLoop; + ui->pllDamplingFactor->setVisible(stdPll); + ui->pllDampingFactor->setVisible(stdPll); + ui->pllDampingFactorText->setVisible(stdPll); + ui->pllLine3->setVisible(stdPll); + ui->pllLoopGainLabel->setVisible(stdPll); + ui->pllLoopGain->setVisible(stdPll); + ui->pllLoopGainText->setVisible(stdPll); + ui->pllLine4->setVisible(stdPll); + + // Order + ui->pllPskOrder->blockSignals(true); + ui->pllPskOrder->clear(); + if (stdPll) + { + ui->pllPskOrder->addItem("CW"); + ui->pllPskOrder->addItem("BPSK"); + ui->pllPskOrder->addItem("QPSK"); + ui->pllPskOrder->addItem("8PSK"); + ui->pllPskOrder->addItem("16PSK"); + } + else if (m_settings.m_costasLoop) + { + ui->pllPskOrder->addItem("BPSK"); + ui->pllPskOrder->addItem("QPSK"); + ui->pllPskOrder->addItem("8PSK"); + if (m_settings.m_pllPskOrder < 2) + m_settings.m_pllPskOrder = 2; + else if (m_settings.m_pllPskOrder > 8) + m_settings.m_pllPskOrder = 8; + } + int i = 0; + for(; ((m_settings.m_pllPskOrder>>i) & 1) == 0; i++); + if (m_settings.m_costasLoop) + ui->pllPskOrder->setCurrentIndex(i==0 ? 0 : i-1); + else + ui->pllPskOrder->setCurrentIndex(i); + ui->pllPskOrder->blockSignals(false); + arrangeRollups(); } void ChannelAnalyzerGUI::setSpectrumDisplay() @@ -212,9 +279,10 @@ void ChannelAnalyzerGUI::tick() if (ui->pll->isChecked()) { - double sampleRate = ((double) m_channelAnalyzer->getChannelSampleRate()) / m_channelAnalyzer->getDecimation(); + double sampleRate = (double) m_channelAnalyzer->getChannelSampleRate(); int freq = (m_channelAnalyzer->getPllFrequency() * sampleRate) / (2.0*M_PI); ui->pll->setToolTip(tr("PLL lock. Freq = %1 Hz").arg(freq)); + ui->pllLockFrequency->setText(tr("%1 Hz").arg(freq)); } } @@ -232,16 +300,48 @@ void ChannelAnalyzerGUI::on_pll_toggled(bool checked) } m_settings.m_pll = checked; + setPLLVisibility(); + applySettings(); +} + +void ChannelAnalyzerGUI::on_pllType_currentIndexChanged(int index) +{ + m_settings.m_fll = (index == 1); + m_settings.m_costasLoop = (index == 2); + setPLLVisibility(); applySettings(); } void ChannelAnalyzerGUI::on_pllPskOrder_currentIndexChanged(int index) { - if (index < 5) { + if (m_settings.m_costasLoop) + m_settings.m_pllPskOrder = (1<<(index+1)); + else m_settings.m_pllPskOrder = (1<pllBandwidthText->setText(bandwidthStr); + applySettings(); +} + +void ChannelAnalyzerGUI::on_pllDampingFactor_valueChanged(int value) +{ + m_settings.m_pllDampingFactor = value/10.0; + QString factorStr = QString::number(m_settings.m_pllDampingFactor, 'f', 1); + ui->pllDampingFactorText->setText(factorStr); + applySettings(); +} + +void ChannelAnalyzerGUI::on_pllLoopGain_valueChanged(int value) +{ + m_settings.m_pllLoopGain = value; + QString gainStr = QString::number(m_settings.m_pllLoopGain, 'f', 0); + ui->pllLoopGainText->setText(gainStr); applySettings(); } diff --git a/plugins/channelrx/chanalyzer/chanalyzergui.h b/plugins/channelrx/chanalyzer/chanalyzergui.h index e81019461..9a6987f9e 100644 --- a/plugins/channelrx/chanalyzer/chanalyzergui.h +++ b/plugins/channelrx/chanalyzer/chanalyzergui.h @@ -81,6 +81,7 @@ private: void applySettings(bool force = false); void displaySettings(); void displayPLLSettings(); + void setPLLVisibility(); void setSpectrumDisplay(); bool handleMessage(const Message& message); @@ -91,7 +92,11 @@ private slots: void on_deltaFrequency_changed(qint64 value); void on_rationalDownSamplerRate_changed(quint64 value); void on_pll_toggled(bool checked); + void on_pllType_currentIndexChanged(int index); void on_pllPskOrder_currentIndexChanged(int index); + void on_pllBandwidth_valueChanged(int value); + void on_pllDampingFactor_valueChanged(int value); + void on_pllLoopGain_valueChanged(int value); void on_useRationalDownsampler_toggled(bool checked); void on_signalSelect_currentIndexChanged(int index); void on_rrcFilter_toggled(bool checked); diff --git a/plugins/channelrx/chanalyzer/chanalyzergui.ui b/plugins/channelrx/chanalyzer/chanalyzergui.ui index 19aaf5e83..cfad3ab2a 100644 --- a/plugins/channelrx/chanalyzer/chanalyzergui.ui +++ b/plugins/channelrx/chanalyzer/chanalyzergui.ui @@ -29,9 +29,9 @@ 0 - 10 - 631 - 81 + 0 + 524 + 101 @@ -127,6 +127,15 @@ + + + + 26 + 26 + 26 + + + @@ -147,6 +156,15 @@ + + + + 26 + 26 + 26 + + + @@ -167,6 +185,15 @@ + + + + 26 + 26 + 26 + + + @@ -335,49 +362,6 @@ - - - - - 40 - 16777215 - - - - PLL PSK order (1 for CW) - - - - 1 - - - - - 2 - - - - - 4 - - - - - 8 - - - - - 16 - - - - - F - - - - @@ -592,6 +576,274 @@ + + + + + 0 + + + 2 + + + 0 + + + 0 + + + + + PLL type + + + + PLL + + + + + FLL + + + + + Costas Loop + + + + + + + + + 70 + 0 + + + + + 70 + 16777215 + + + + PLL PSK order (1 for CW) + + + + CW + + + + + BPSK + + + + + QPSK + + + + + 8PSK + + + + + 16PSK + + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 24 + 24 + + + + PLL loop bandwidth + + + 1 + + + 100 + + + 1 + + + 2 + + + + + + + 0.002 + + + + + + + Qt::Vertical + + + + + + + D + + + + + + + + 24 + 24 + + + + PLL damping factor + + + 1 + + + 10 + + + 1 + + + 5 + + + + + + + 0.5 + + + + + + + Qt::Vertical + + + + + + + G + + + + + + + + 24 + 24 + + + + PLL loop gain + + + 1 + + + 1000 + + + 1 + + + 10 + + + + + + + 10 + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Freq + + + + + + + + 60 + 0 + + + + PLL lock frequency + + + -100000Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/plugins/channelrx/chanalyzer/chanalyzersettings.cpp b/plugins/channelrx/chanalyzer/chanalyzersettings.cpp index f8a313509..51cd93f5b 100644 --- a/plugins/channelrx/chanalyzer/chanalyzersettings.cpp +++ b/plugins/channelrx/chanalyzer/chanalyzersettings.cpp @@ -42,9 +42,13 @@ void ChannelAnalyzerSettings::resetToDefaults() m_ssb = false; m_pll = false; m_fll = false; + m_costasLoop = false; m_rrc = false; m_rrcRolloff = 35; // 0.35 m_pllPskOrder = 1; + m_pllBandwidth = 0.002f; + m_pllDampingFactor = 0.5f; + m_pllLoopGain = 10.0f; m_inputType = InputSignal; m_rgbColor = QColor(128, 128, 128).rgb(); m_title = "Channel Analyzer"; @@ -71,6 +75,10 @@ QByteArray ChannelAnalyzerSettings::serialize() const s.writeString(15, m_title); s.writeBool(16, m_rrc); s.writeU32(17, m_rrcRolloff); + s.writeFloat(18, m_pllBandwidth); + s.writeFloat(19, m_pllDampingFactor); + s.writeFloat(20, m_pllLoopGain); + s.writeBool(21, m_costasLoop); return s.final(); } @@ -118,6 +126,10 @@ bool ChannelAnalyzerSettings::deserialize(const QByteArray& data) d.readString(15, &m_title, "Channel Analyzer"); d.readBool(16, &m_rrc, false); d.readU32(17, &m_rrcRolloff, 35); + d.readFloat(18, &m_pllBandwidth, 0.002f); + d.readFloat(19, &m_pllDampingFactor, 0.5f); + d.readFloat(20, &m_pllLoopGain, 10.0f); + d.readBool(21, &m_costasLoop, false); return true; } diff --git a/plugins/channelrx/chanalyzer/chanalyzersettings.h b/plugins/channelrx/chanalyzer/chanalyzersettings.h index b230c8752..5a3ac8deb 100644 --- a/plugins/channelrx/chanalyzer/chanalyzersettings.h +++ b/plugins/channelrx/chanalyzer/chanalyzersettings.h @@ -40,9 +40,13 @@ struct ChannelAnalyzerSettings bool m_ssb; bool m_pll; bool m_fll; + bool m_costasLoop; bool m_rrc; quint32 m_rrcRolloff; //!< in 100ths unsigned int m_pllPskOrder; + float m_pllBandwidth; + float m_pllDampingFactor; + float m_pllLoopGain; InputType m_inputType; quint32 m_rgbColor; QString m_title; diff --git a/plugins/channelrx/chanalyzer/chanalyzersink.cpp b/plugins/channelrx/chanalyzer/chanalyzersink.cpp index dfd1586dc..15a80ac14 100644 --- a/plugins/channelrx/chanalyzer/chanalyzersink.cpp +++ b/plugins/channelrx/chanalyzer/chanalyzersink.cpp @@ -30,6 +30,7 @@ ChannelAnalyzerSink::ChannelAnalyzerSink() : m_channelSampleRate(48000), m_channelFrequencyOffset(0), m_sinkSampleRate(48000), + m_costasLoop(0.002, 2), m_sampleSink(nullptr) { m_usb = true; @@ -38,7 +39,8 @@ ChannelAnalyzerSink::ChannelAnalyzerSink() : DSBFilter = new fftfilt(m_settings.m_bandwidth / m_channelSampleRate, 2*m_ssbFftLen); RRCFilter = new fftfilt(m_settings.m_bandwidth / m_channelSampleRate, 2*m_ssbFftLen); m_corr = new fftcorr(2*m_corrFFTLen); // 8k for 4k effective samples - m_pll.computeCoefficients(0.002f, 0.5f, 10.0f); // bandwidth, damping factor, loop gain + m_pll.computeCoefficients(m_settings.m_pllBandwidth, m_settings.m_pllDampingFactor, m_settings.m_pllLoopGain); + m_costasLoop.computeCoefficients(m_settings.m_pllBandwidth); applyChannelSettings(m_channelSampleRate, m_sinkSampleRate, m_channelFrequencyOffset, true); applySettings(m_settings, true); @@ -123,21 +125,28 @@ void ChannelAnalyzerSink::processOneSample(Complex& c, fftfilt::cmplx *sideband) if (m_settings.m_pll) { - if (m_settings.m_fll) + // Use -fPLL to mix (exchange PLL real and image in the complex multiplication) + if (m_settings.m_costasLoop) + { + m_costasLoop.feed(re, im); + mix = si * std::conj(m_costasLoop.getComplex()); + feedOneSample(mix, m_costasLoop.getComplex()); + } + else if (m_settings.m_fll) { m_fll.feed(re, im); - // Use -fPLL to mix (exchange PLL real and image in the complex multiplication) mix = si * std::conj(m_fll.getComplex()); + feedOneSample(mix, m_fll.getComplex()); } else { m_pll.feed(re, im); - // Use -fPLL to mix (exchange PLL real and image in the complex multiplication) mix = si * std::conj(m_pll.getComplex()); + feedOneSample(mix, m_pll.getComplex()); } } - - feedOneSample(m_settings.m_pll ? mix : si, m_settings.m_fll ? m_fll.getComplex() : m_pll.getComplex()); + else + feedOneSample(si, si); } } @@ -230,7 +239,11 @@ void ChannelAnalyzerSink::applySettings(const ChannelAnalyzerSettings& settings, << " m_ssb: " << settings.m_ssb << " m_pll: " << settings.m_pll << " m_fll: " << settings.m_fll + << " m_costasLoop: " << settings.m_costasLoop << " m_pllPskOrder: " << settings.m_pllPskOrder + << " m_pllBandwidth: " << settings.m_pllBandwidth + << " m_pllDampingFactor: " << settings.m_pllDampingFactor + << " m_pllLoopGain: " << settings.m_pllLoopGain << " m_inputType: " << (int) settings.m_inputType; bool doApplySampleRate = false; @@ -247,6 +260,7 @@ void ChannelAnalyzerSink::applySettings(const ChannelAnalyzerSettings& settings, { m_pll.reset(); m_fll.reset(); + m_costasLoop.reset(); } } @@ -257,11 +271,30 @@ void ChannelAnalyzerSink::applySettings(const ChannelAnalyzerSettings& settings, } } + if (settings.m_costasLoop != m_settings.m_costasLoop || force) + { + if (settings.m_costasLoop) { + m_costasLoop.reset(); + } + } + if (settings.m_pllPskOrder != m_settings.m_pllPskOrder || force) { if (settings.m_pllPskOrder < 32) { m_pll.setPskOrder(settings.m_pllPskOrder); } + if (settings.m_pllPskOrder < 16) { + m_costasLoop.setPskOrder(settings.m_pllPskOrder); + } + } + + if ((settings.m_pllBandwidth != m_settings.m_pllBandwidth) + || (settings.m_pllDampingFactor != m_settings.m_pllDampingFactor) + || (settings.m_pllLoopGain != m_settings.m_pllLoopGain) + || force) + { + m_pll.computeCoefficients(settings.m_pllBandwidth, settings.m_pllDampingFactor, settings.m_pllLoopGain); + m_costasLoop.computeCoefficients(settings.m_pllBandwidth); } if ((settings.m_rationalDownSample != m_settings.m_rationalDownSample) || @@ -280,15 +313,42 @@ void ChannelAnalyzerSink::applySettings(const ChannelAnalyzerSettings& settings, } } +bool ChannelAnalyzerSink::isPllLocked() const +{ + if (m_settings.m_pll) + return m_pll.locked(); + else + return false; +} + Real ChannelAnalyzerSink::getPllFrequency() const { - if (m_settings.m_fll) { + if (m_settings.m_costasLoop) + return m_costasLoop.getFreq(); + else if (m_settings.m_fll) return m_fll.getFreq(); - } else if (m_settings.m_pll) { + else if (m_settings.m_pll) return m_pll.getFreq(); - } else { + else return 0.0; - } +} + +Real ChannelAnalyzerSink::getPllPhase() const +{ + if (m_settings.m_costasLoop) + return m_costasLoop.getPhiHat(); + else if (m_settings.m_pll) + return m_pll.getPhiHat(); + else + return 0.0f; +} + +Real ChannelAnalyzerSink::getPllDeltaPhase() const +{ + if (m_settings.m_pll) + return m_pll.getDeltaPhi(); + else + return 0.0f; } int ChannelAnalyzerSink::getActualSampleRate() @@ -307,5 +367,6 @@ void ChannelAnalyzerSink::applySampleRate() setFilters(sampleRate, m_settings.m_bandwidth, m_settings.m_lowCutoff); m_pll.setSampleRate(sampleRate); m_fll.setSampleRate(sampleRate); + m_costasLoop.setSampleRate(sampleRate); RRCFilter->create_rrc_filter(m_settings.m_bandwidth / (float) sampleRate, m_settings.m_rrcRolloff / 100.0); } diff --git a/plugins/channelrx/chanalyzer/chanalyzersink.h b/plugins/channelrx/chanalyzer/chanalyzersink.h index 1e5269c19..21f5cf156 100644 --- a/plugins/channelrx/chanalyzer/chanalyzersink.h +++ b/plugins/channelrx/chanalyzer/chanalyzersink.h @@ -26,6 +26,7 @@ #include "dsp/fftfilt.h" #include "dsp/phaselockcomplex.h" #include "dsp/freqlockcomplex.h" +#include "dsp/costasloop.h" #include "audio/audiofifo.h" #include "util/movingaverage.h" @@ -46,10 +47,10 @@ public: double getMagSq() const { return m_magsq; } double getMagSqAvg() const { return (double) m_channelPowerAvg; } - bool isPllLocked() const { return m_settings.m_pll && m_pll.locked(); } + bool isPllLocked() const; Real getPllFrequency() const; - Real getPllDeltaPhase() const { return m_pll.getDeltaPhi(); } - Real getPllPhase() const { return m_pll.getPhiHat(); } + Real getPllDeltaPhase() const; + Real getPllPhase() const; void setSampleSink(BasebandSampleSink* sampleSink) { m_sampleSink = sampleSink; } static const unsigned int m_corrFFTLen; @@ -70,6 +71,7 @@ private: Real m_interpolatorDistanceRemain; PhaseLockComplex m_pll; FreqLockComplex m_fll; + CostasLoop m_costasLoop; DecimatorC m_decimator; fftfilt* SSBFilter; diff --git a/plugins/channelrx/chanalyzer/chanalyzerwebapiadapter.cpp b/plugins/channelrx/chanalyzer/chanalyzerwebapiadapter.cpp index 63bcab1f9..c6b63e824 100644 --- a/plugins/channelrx/chanalyzer/chanalyzerwebapiadapter.cpp +++ b/plugins/channelrx/chanalyzer/chanalyzerwebapiadapter.cpp @@ -55,9 +55,13 @@ void ChannelAnalyzerWebAPIAdapter::webapiFormatChannelSettings( response.getChannelAnalyzerSettings()->setSsb(settings.m_ssb ? 1 : 0); response.getChannelAnalyzerSettings()->setPll(settings.m_pll ? 1 : 0); response.getChannelAnalyzerSettings()->setFll(settings.m_fll ? 1 : 0); + response.getChannelAnalyzerSettings()->setCostasLoop(settings.m_costasLoop ? 1 : 0); response.getChannelAnalyzerSettings()->setRrc(settings.m_rrc ? 1 : 0); response.getChannelAnalyzerSettings()->setRrcRolloff(settings.m_rrcRolloff); response.getChannelAnalyzerSettings()->setPllPskOrder(settings.m_pllPskOrder); + response.getChannelAnalyzerSettings()->setPllBandwidth(settings.m_pllBandwidth); + response.getChannelAnalyzerSettings()->setPllDampingFactor(settings.m_pllBandwidth); + response.getChannelAnalyzerSettings()->setPllLoopGain(settings.m_pllLoopGain); response.getChannelAnalyzerSettings()->setInputType((int) settings.m_inputType); response.getChannelAnalyzerSettings()->setRgbColor(settings.m_rgbColor); response.getChannelAnalyzerSettings()->setTitle(new QString(settings.m_title)); @@ -190,9 +194,21 @@ void ChannelAnalyzerWebAPIAdapter::webapiUpdateChannelSettings( if (channelSettingsKeys.contains("pll")) { settings.m_pll = response.getChannelAnalyzerSettings()->getPll() != 0; } + if (channelSettingsKeys.contains("costasLoop")) { + settings.m_costasLoop = response.getChannelAnalyzerSettings()->getCostasLoop() != 0; + } if (channelSettingsKeys.contains("pllPskOrder")) { settings.m_pllPskOrder = response.getChannelAnalyzerSettings()->getPllPskOrder(); } + if (channelSettingsKeys.contains("pllBandwidth")) { + settings.m_pllBandwidth = response.getChannelAnalyzerSettings()->getPllBandwidth(); + } + if (channelSettingsKeys.contains("pllDampingFactor")) { + settings.m_pllDampingFactor = response.getChannelAnalyzerSettings()->getPllDampingFactor(); + } + if (channelSettingsKeys.contains("pllLoopGain")) { + settings.m_pllLoopGain = response.getChannelAnalyzerSettings()->getPllLoopGain(); + } if (channelSettingsKeys.contains("rgbColor")) { settings.m_rgbColor = response.getChannelAnalyzerSettings()->getRgbColor(); } diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index a055cbebf..09ecdf38b 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -97,6 +97,7 @@ set(sdrbase_SOURCES dsp/ctcssfrequencies.cpp dsp/channelsamplesink.cpp dsp/channelsamplesource.cpp + dsp/costasloop.cpp dsp/cwkeyer.cpp dsp/cwkeyersettings.cpp dsp/datafifo.cpp @@ -255,6 +256,7 @@ set(sdrbase_HEADERS dsp/channelsamplesink.h dsp/channelsamplesource.h dsp/complex.h + dsp/costasloop.h dsp/ctcssdetector.h dsp/ctcssfrequencies.h dsp/cwkeyer.h diff --git a/sdrbase/dsp/costasloop.cpp b/sdrbase/dsp/costasloop.cpp new file mode 100644 index 000000000..d3c5022f0 --- /dev/null +++ b/sdrbase/dsp/costasloop.cpp @@ -0,0 +1,110 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright 2006-2021 Free Software Foundation, Inc. // +// Copyright (C) 2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// Based on the Costas Loop from GNU Radio // +// // +// This program 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "costasloop.h" +#include + +// Loop bandwidth supposedly ~ 2pi/100 rads/sample +// pskOrder 2, 4 or 8 +CostasLoop::CostasLoop(float loopBW, unsigned int pskOrder) : + m_maxFreq(1.0f), + m_minFreq(-1.0f), + m_pskOrder(pskOrder) +{ + computeCoefficients(loopBW); + reset(); +} + +CostasLoop::~CostasLoop() +{ +} + +void CostasLoop::reset() +{ + m_y.real(1.0f); + m_y.imag(0.0f); + m_freq = 0.0f; + m_phase = 0.0f; + m_freq = 0.0f; + m_error = 0.0f; +} + +// 2nd order loop with critical damping +void CostasLoop::computeCoefficients(float loopBW) +{ + float damping = sqrtf(2.0f) / 2.0f; + float denom = (1.0 + 2.0 * damping * loopBW + loopBW * loopBW); + m_alpha = (4 * damping * loopBW) / denom; + m_beta = (4 * loopBW * loopBW) / denom; +} + +void CostasLoop::setSampleRate(unsigned int sampleRate) +{ + reset(); +} + +static float branchlessClip(float x, float clip) +{ + return 0.5f * (std::abs(x + clip) - std::abs(x - clip)); +} + +// Don't use built-in complex.h multiply to avoid NaN/INF checking +static void fastComplexMultiply(std::complex &out, const std::complex cc1, const std::complex cc2) +{ + float o_r, o_i; + + o_r = (cc1.real() * cc2.real()) - (cc1.imag() * cc2.imag()); + o_i = (cc1.real() * cc2.imag()) + (cc1.imag() * cc2.real()); + + out.real(o_r); + out.imag(o_i); +} + +void CostasLoop::feed(float re, float im) +{ + std::complex nco(std::cosf(-m_phase), std::sinf(-m_phase)); + + std::complex in, out; + in.real(re); + in.imag(im); + fastComplexMultiply(out, in, nco); + + switch (m_pskOrder) + { + case 2: + m_error = phaseDetector2(out); + break; + case 4: + m_error = phaseDetector4(out); + break; + case 8: + m_error = phaseDetector8(out); + break; + } + m_error = branchlessClip(m_error, 1.0f); + + advanceLoop(m_error); + phaseWrap(); + frequencyLimit(); + + m_y.real(-nco.real()); + m_y.imag(nco.imag()); +} diff --git a/sdrbase/dsp/costasloop.h b/sdrbase/dsp/costasloop.h new file mode 100644 index 000000000..77c2e6d79 --- /dev/null +++ b/sdrbase/dsp/costasloop.h @@ -0,0 +1,120 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright 2006-2021 Free Software Foundation, Inc. // +// Copyright (C) 2018 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// Based on the Costas Loop from GNU Radio // +// // +// This program 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRBASE_DSP_COSTASLOOP_H_ +#define SDRBASE_DSP_COSTASLOOP_H_ + +#include + +#include "dsp/dsptypes.h" +#include "export.h" + +/** Costas Loop for phase and frequency tracking. */ +class SDRBASE_API CostasLoop +{ +public: + CostasLoop(float loopBW, unsigned int pskOrder); + ~CostasLoop(); + + void computeCoefficients(float loopBW); + void setPskOrder(unsigned int pskOrder) { m_pskOrder = pskOrder; } + void reset(); + void setSampleRate(unsigned int sampleRate); + void feed(float re, float im); + const std::complex& getComplex() const { return m_y; } + float getReal() const { return m_y.real(); } + float getImag() const { return m_y.imag(); } + float getFreq() const { return m_freq; } + float getPhiHat() const { return m_phase; } + +private: + + std::complex m_y; + float m_phase; + float m_freq; + float m_error; + float m_maxFreq; + float m_minFreq; + float m_alpha; + float m_beta; + unsigned int m_pskOrder; + + void advanceLoop(float error) + { + m_freq = m_freq + m_beta * error; + m_phase = m_phase + m_freq + m_alpha * error; + } + + void phaseWrap() + { + while (m_phase > (2 * M_PI)) + m_phase -= 2 * M_PI; + while (m_phase < (-2 * M_PI)) + m_phase += 2 * M_PI; + } + + void frequencyLimit() + { + if (m_freq > m_maxFreq) + m_freq = m_maxFreq; + else if (m_freq < m_minFreq) + m_freq = m_minFreq; + } + + void setMaxFreq(float freq) + { + m_maxFreq = freq; + } + + void setMinFreq(float freq) + { + m_minFreq = freq; + } + + float phaseDetector2(std::complex sample) const // for BPSK + { + return (sample.real() * sample.imag()); + } + + float phaseDetector4(std::complex sample) const // for QPSK + { + return ((sample.real() > 0.0f ? 1.0f : -1.0f) * sample.imag() - + (sample.imag() > 0.0f ? 1.0f : -1.0f) * sample.real()); + }; + + float phaseDetector8(std::complex sample) const // for 8PSK + { + const float K = (sqrtf(2.0) - 1); + if (fabsf(sample.real()) >= fabsf(sample.imag())) + { + return ((sample.real() > 0.0f ? 1.0f : -1.0f) * sample.imag() - + (sample.imag() > 0.0f ? 1.0f : -1.0f) * sample.real() * K); + } + else + { + return ((sample.real() > 0.0f ? 1.0f : -1.0f) * sample.imag() * K - + (sample.imag() > 0.0f ? 1.0f : -1.0f) * sample.real()); + } + }; + +}; + +#endif /* SDRBASE_DSP_COSTASLOOP_H_ */ diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 2b28c539c..3726384ad 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -2586,6 +2586,10 @@ margin-bottom: 20px; "type" : "integer", "description" : "Boolean" }, + "costasLoop" : { + "type" : "integer", + "description" : "Boolean" + }, "rrc" : { "type" : "integer", "description" : "Boolean" @@ -2597,6 +2601,18 @@ margin-bottom: 20px; "pllPskOrder" : { "type" : "integer" }, + "pllBandwidth" : { + "type" : "number", + "format" : "float" + }, + "pllDampingFactor" : { + "type" : "number", + "format" : "float" + }, + "pllLoopGain" : { + "type" : "number", + "format" : "float" + }, "inputType" : { "type" : "integer", "description" : "see ChannelAnalyzerSettings::InputType" @@ -45623,7 +45639,7 @@ except ApiException as e:
- Generated 2021-03-01T10:47:56.898+01:00 + Generated 2021-03-05T14:04:36.302+01:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/ChannelAnalyzer.yaml b/sdrbase/resources/webapi/doc/swagger/include/ChannelAnalyzer.yaml index 377aa1af7..06b3b0749 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/ChannelAnalyzer.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/ChannelAnalyzer.yaml @@ -23,6 +23,9 @@ ChannelAnalyzerSettings: fll: description: Boolean type: integer + costasLoop: + description: Boolean + type: integer rrc: description: Boolean type: integer @@ -31,6 +34,15 @@ ChannelAnalyzerSettings: type: integer pllPskOrder: type: integer + pllBandwidth: + type: number + format: float + pllDampingFactor: + type: number + format: float + pllLoopGain: + type: number + format: float inputType: description: see ChannelAnalyzerSettings::InputType type: integer diff --git a/swagger/sdrangel/api/swagger/include/ChannelAnalyzer.yaml b/swagger/sdrangel/api/swagger/include/ChannelAnalyzer.yaml index c109ca69d..02ae5b61e 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelAnalyzer.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelAnalyzer.yaml @@ -23,6 +23,9 @@ ChannelAnalyzerSettings: fll: description: Boolean type: integer + costasLoop: + description: Boolean + type: integer rrc: description: Boolean type: integer @@ -31,6 +34,15 @@ ChannelAnalyzerSettings: type: integer pllPskOrder: type: integer + pllBandwidth: + type: number + format: float + pllDampingFactor: + type: number + format: float + pllLoopGain: + type: number + format: float inputType: description: see ChannelAnalyzerSettings::InputType type: integer diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index 2b28c539c..3726384ad 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -2586,6 +2586,10 @@ margin-bottom: 20px; "type" : "integer", "description" : "Boolean" }, + "costasLoop" : { + "type" : "integer", + "description" : "Boolean" + }, "rrc" : { "type" : "integer", "description" : "Boolean" @@ -2597,6 +2601,18 @@ margin-bottom: 20px; "pllPskOrder" : { "type" : "integer" }, + "pllBandwidth" : { + "type" : "number", + "format" : "float" + }, + "pllDampingFactor" : { + "type" : "number", + "format" : "float" + }, + "pllLoopGain" : { + "type" : "number", + "format" : "float" + }, "inputType" : { "type" : "integer", "description" : "see ChannelAnalyzerSettings::InputType" @@ -45623,7 +45639,7 @@ except ApiException as e:
- Generated 2021-03-01T10:47:56.898+01:00 + Generated 2021-03-05T14:04:36.302+01:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.cpp index 2c3735567..cda2e09ed 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.cpp @@ -46,12 +46,20 @@ SWGChannelAnalyzerSettings::SWGChannelAnalyzerSettings() { m_pll_isSet = false; fll = 0; m_fll_isSet = false; + costas_loop = 0; + m_costas_loop_isSet = false; rrc = 0; m_rrc_isSet = false; rrc_rolloff = 0; m_rrc_rolloff_isSet = false; pll_psk_order = 0; m_pll_psk_order_isSet = false; + pll_bandwidth = 0.0f; + m_pll_bandwidth_isSet = false; + pll_damping_factor = 0.0f; + m_pll_damping_factor_isSet = false; + pll_loop_gain = 0.0f; + m_pll_loop_gain_isSet = false; input_type = 0; m_input_type_isSet = false; rgb_color = 0; @@ -88,12 +96,20 @@ SWGChannelAnalyzerSettings::init() { m_pll_isSet = false; fll = 0; m_fll_isSet = false; + costas_loop = 0; + m_costas_loop_isSet = false; rrc = 0; m_rrc_isSet = false; rrc_rolloff = 0; m_rrc_rolloff_isSet = false; pll_psk_order = 0; m_pll_psk_order_isSet = false; + pll_bandwidth = 0.0f; + m_pll_bandwidth_isSet = false; + pll_damping_factor = 0.0f; + m_pll_damping_factor_isSet = false; + pll_loop_gain = 0.0f; + m_pll_loop_gain_isSet = false; input_type = 0; m_input_type_isSet = false; rgb_color = 0; @@ -122,6 +138,10 @@ SWGChannelAnalyzerSettings::cleanup() { + + + + if(title != nullptr) { delete title; } @@ -162,12 +182,20 @@ SWGChannelAnalyzerSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&fll, pJson["fll"], "qint32", ""); + ::SWGSDRangel::setValue(&costas_loop, pJson["costasLoop"], "qint32", ""); + ::SWGSDRangel::setValue(&rrc, pJson["rrc"], "qint32", ""); ::SWGSDRangel::setValue(&rrc_rolloff, pJson["rrcRolloff"], "qint32", ""); ::SWGSDRangel::setValue(&pll_psk_order, pJson["pllPskOrder"], "qint32", ""); + ::SWGSDRangel::setValue(&pll_bandwidth, pJson["pllBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&pll_damping_factor, pJson["pllDampingFactor"], "float", ""); + + ::SWGSDRangel::setValue(&pll_loop_gain, pJson["pllLoopGain"], "float", ""); + ::SWGSDRangel::setValue(&input_type, pJson["inputType"], "qint32", ""); ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); @@ -221,6 +249,9 @@ SWGChannelAnalyzerSettings::asJsonObject() { if(m_fll_isSet){ obj->insert("fll", QJsonValue(fll)); } + if(m_costas_loop_isSet){ + obj->insert("costasLoop", QJsonValue(costas_loop)); + } if(m_rrc_isSet){ obj->insert("rrc", QJsonValue(rrc)); } @@ -230,6 +261,15 @@ SWGChannelAnalyzerSettings::asJsonObject() { if(m_pll_psk_order_isSet){ obj->insert("pllPskOrder", QJsonValue(pll_psk_order)); } + if(m_pll_bandwidth_isSet){ + obj->insert("pllBandwidth", QJsonValue(pll_bandwidth)); + } + if(m_pll_damping_factor_isSet){ + obj->insert("pllDampingFactor", QJsonValue(pll_damping_factor)); + } + if(m_pll_loop_gain_isSet){ + obj->insert("pllLoopGain", QJsonValue(pll_loop_gain)); + } if(m_input_type_isSet){ obj->insert("inputType", QJsonValue(input_type)); } @@ -339,6 +379,16 @@ SWGChannelAnalyzerSettings::setFll(qint32 fll) { this->m_fll_isSet = true; } +qint32 +SWGChannelAnalyzerSettings::getCostasLoop() { + return costas_loop; +} +void +SWGChannelAnalyzerSettings::setCostasLoop(qint32 costas_loop) { + this->costas_loop = costas_loop; + this->m_costas_loop_isSet = true; +} + qint32 SWGChannelAnalyzerSettings::getRrc() { return rrc; @@ -369,6 +419,36 @@ SWGChannelAnalyzerSettings::setPllPskOrder(qint32 pll_psk_order) { this->m_pll_psk_order_isSet = true; } +float +SWGChannelAnalyzerSettings::getPllBandwidth() { + return pll_bandwidth; +} +void +SWGChannelAnalyzerSettings::setPllBandwidth(float pll_bandwidth) { + this->pll_bandwidth = pll_bandwidth; + this->m_pll_bandwidth_isSet = true; +} + +float +SWGChannelAnalyzerSettings::getPllDampingFactor() { + return pll_damping_factor; +} +void +SWGChannelAnalyzerSettings::setPllDampingFactor(float pll_damping_factor) { + this->pll_damping_factor = pll_damping_factor; + this->m_pll_damping_factor_isSet = true; +} + +float +SWGChannelAnalyzerSettings::getPllLoopGain() { + return pll_loop_gain; +} +void +SWGChannelAnalyzerSettings::setPllLoopGain(float pll_loop_gain) { + this->pll_loop_gain = pll_loop_gain; + this->m_pll_loop_gain_isSet = true; +} + qint32 SWGChannelAnalyzerSettings::getInputType() { return input_type; @@ -451,6 +531,9 @@ SWGChannelAnalyzerSettings::isSet(){ if(m_fll_isSet){ isObjectUpdated = true; break; } + if(m_costas_loop_isSet){ + isObjectUpdated = true; break; + } if(m_rrc_isSet){ isObjectUpdated = true; break; } @@ -460,6 +543,15 @@ SWGChannelAnalyzerSettings::isSet(){ if(m_pll_psk_order_isSet){ isObjectUpdated = true; break; } + if(m_pll_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_pll_damping_factor_isSet){ + isObjectUpdated = true; break; + } + if(m_pll_loop_gain_isSet){ + isObjectUpdated = true; break; + } if(m_input_type_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.h index 4c73e87a3..557c0ba77 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelAnalyzerSettings.h @@ -71,6 +71,9 @@ public: qint32 getFll(); void setFll(qint32 fll); + qint32 getCostasLoop(); + void setCostasLoop(qint32 costas_loop); + qint32 getRrc(); void setRrc(qint32 rrc); @@ -80,6 +83,15 @@ public: qint32 getPllPskOrder(); void setPllPskOrder(qint32 pll_psk_order); + float getPllBandwidth(); + void setPllBandwidth(float pll_bandwidth); + + float getPllDampingFactor(); + void setPllDampingFactor(float pll_damping_factor); + + float getPllLoopGain(); + void setPllLoopGain(float pll_loop_gain); + qint32 getInputType(); void setInputType(qint32 input_type); @@ -126,6 +138,9 @@ private: qint32 fll; bool m_fll_isSet; + qint32 costas_loop; + bool m_costas_loop_isSet; + qint32 rrc; bool m_rrc_isSet; @@ -135,6 +150,15 @@ private: qint32 pll_psk_order; bool m_pll_psk_order_isSet; + float pll_bandwidth; + bool m_pll_bandwidth_isSet; + + float pll_damping_factor; + bool m_pll_damping_factor_isSet; + + float pll_loop_gain; + bool m_pll_loop_gain_isSet; + qint32 input_type; bool m_input_type_isSet;