Merge pull request #797 from srcejon/chan_an_costas_loop

Add Costas loop PLL to channel analyzer
This commit is contained in:
Edouard Griffiths 2021-03-06 04:35:38 +01:00 committed by GitHub
commit 7b13abe0d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 910 additions and 73 deletions

View File

@ -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

View File

@ -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<<index);
}
applySettings();
}
m_settings.m_fll = (index == 5);
void ChannelAnalyzerGUI::on_pllBandwidth_valueChanged(int value)
{
m_settings.m_pllBandwidth = value/1000.0;
QString bandwidthStr = QString::number(m_settings.m_pllBandwidth, 'f', 3);
ui->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();
}

View File

@ -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);

View File

@ -29,9 +29,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>10</y>
<width>631</width>
<height>81</height>
<y>0</y>
<width>524</width>
<height>101</height>
</rect>
</property>
<property name="windowTitle">
@ -335,49 +335,6 @@
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="pllPskOrder">
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>PLL PSK order (1 for CW)</string>
</property>
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>4</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
<item>
<property name="text">
<string>16</string>
</property>
</item>
<item>
<property name="text">
<string>F</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
@ -592,6 +549,274 @@
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="pllToolbar" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="pllType">
<property name="toolTip">
<string>PLL type</string>
</property>
<item>
<property name="text">
<string>PLL</string>
</property>
</item>
<item>
<property name="text">
<string>FLL</string>
</property>
</item>
<item>
<property name="text">
<string>Costas Loop</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="pllPskOrder">
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>PLL PSK order (1 for CW)</string>
</property>
<item>
<property name="text">
<string>CW</string>
</property>
</item>
<item>
<property name="text">
<string>BPSK</string>
</property>
</item>
<item>
<property name="text">
<string>QPSK</string>
</property>
</item>
<item>
<property name="text">
<string>8PSK</string>
</property>
</item>
<item>
<property name="text">
<string>16PSK</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="Line" name="pllLine1">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllBandwidthLabel">
<property name="text">
<string>BW</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="pllBandwidth">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>PLL loop bandwidth</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllBandwidthText">
<property name="text">
<string>0.002</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="pllLine2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllDamplingFactor">
<property name="text">
<string>D</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="pllDampingFactor">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>PLL damping factor</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>10</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllDampingFactorText">
<property name="text">
<string>0.5</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="pllLine3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllLoopGainLabel">
<property name="text">
<string>G</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="pllLoopGain">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>PLL loop gain</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllLoopGainText">
<property name="text">
<string>10</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="pllLine4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="pllHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="pllLockFrequencyLabel">
<property name="text">
<string>Freq</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="pllLockFrequency">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>PLL lock frequency</string>
</property>
<property name="text">
<string>-100000Hz</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="spectrumContainer" native="true">

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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

112
sdrbase/dsp/costasloop.cpp Normal file
View File

@ -0,0 +1,112 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "costasloop.h"
#include <cmath>
// 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)
{
(void) 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<float> &out, const std::complex<float> cc1, const std::complex<float> 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<float> nco(::cosf(-m_phase), ::sinf(-m_phase));
std::complex<float> 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());
}

122
sdrbase/dsp/costasloop.h Normal file
View File

@ -0,0 +1,122 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef SDRBASE_DSP_COSTASLOOP_H_
#define SDRBASE_DSP_COSTASLOOP_H_
#include <QDebug>
#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<float>& 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<float> 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()
{
const float two_pi = (float)(2.0 * M_PI);
while (m_phase > two_pi)
m_phase -= two_pi;
while (m_phase < -two_pi)
m_phase += two_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<float> sample) const // for BPSK
{
return (sample.real() * sample.imag());
}
float phaseDetector4(std::complex<float> 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<float> 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_ */

View File

@ -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:
</div>
<div id="generator">
<div class="content">
Generated 2021-03-01T10:47:56.898+01:00
Generated 2021-03-05T14:04:36.302+01:00
</div>
</div>
</div>

View File

@ -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

View File

@ -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

View File

@ -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:
</div>
<div id="generator">
<div class="content">
Generated 2021-03-01T10:47:56.898+01:00
Generated 2021-03-05T14:04:36.302+01:00
</div>
</div>
</div>

View File

@ -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;
}

View File

@ -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;