mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
330 WIP - updates the demodulator viewer with an eye diagram and symbol
chart. Testing various configurations to see which yields the best PLL and symbol timing locks.
This commit is contained in:
@@ -33,11 +33,13 @@ public class InterpolatingSymbolBuffer
|
||||
private float mOmegaMid;
|
||||
|
||||
private int mDelayLinePointer = 0;
|
||||
private float mSamplesPerSymbol;
|
||||
private int mTwiceSamplesPerSymbol;
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
|
||||
public InterpolatingSymbolBuffer(float samplesPerSymbol)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mMu = samplesPerSymbol;
|
||||
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
|
||||
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
|
||||
@@ -58,7 +60,8 @@ public class InterpolatingSymbolBuffer
|
||||
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature();
|
||||
|
||||
//Increment pointer and keep pointer in bounds
|
||||
mDelayLinePointer = (mDelayLinePointer + 1) % mTwiceSamplesPerSymbol;
|
||||
mDelayLinePointer++;
|
||||
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,11 +83,11 @@ public class InterpolatingSymbolBuffer
|
||||
{
|
||||
//mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
|
||||
//symbol
|
||||
mOmega = mOmega + mGainOmega * symbolTimingError;
|
||||
mOmega = mOmega + mGainOmega * -symbolTimingError;
|
||||
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||
|
||||
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
|
||||
increaseSampleCounter((mOmega + (mGainMu * symbolTimingError)));
|
||||
increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +131,18 @@ public class InterpolatingSymbolBuffer
|
||||
return mCurrentSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
|
||||
* be used to support an external eye-diagram chart.
|
||||
* @return symbol decision data.
|
||||
*/
|
||||
public SymbolDecisionData getSymbolDecisionData()
|
||||
{
|
||||
//TODO: I think that delay line pointer needs to have +4 added to it since the filter is 8 taps long
|
||||
return new SymbolDecisionData(mDelayLineInphase, mDelayLineQuadrature,
|
||||
mSamplesPerSymbol, mDelayLinePointer, mMu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolated inphase value for the specified offset
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -31,7 +32,9 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private GardnerDetector mGardnerDetector = new GardnerDetector();
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<Dibit> mDibitListener;
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
|
||||
private Complex mPreviousSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
@@ -42,6 +45,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
private Complex mCurrentSymbol = new Complex(0, 0);
|
||||
private float mPhaseError;
|
||||
private float mSymbolTimingError;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
|
||||
@@ -63,6 +67,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
"] must be at least 2 x symbol rate [" + symbolRate + "]");
|
||||
}
|
||||
|
||||
mSampleRate = sampleRate;
|
||||
mPLL = phaseLockedLoop;
|
||||
mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator;
|
||||
mQPSKSymbolDecoder = symbolDecoder;
|
||||
@@ -108,6 +113,12 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample();
|
||||
mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample();
|
||||
|
||||
//Eye diagram listener
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData());
|
||||
}
|
||||
|
||||
//Calculate middle and current symbols as the delta between the previous and current samples
|
||||
mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
@@ -127,12 +138,20 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPreviousMiddleSample.setValues(mMiddleSample);
|
||||
mPreviousSymbol.setValues(mCurrentSymbol);
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//Adjust the current symbol as necessary (e.g. for differential encoding)
|
||||
mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
|
||||
|
||||
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
||||
//feedback to the PLL
|
||||
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
|
||||
|
||||
mPhaseError = GardnerDetector.clip(mPhaseError, 0.2f);
|
||||
mPLL.adjust(mPhaseError);
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
@@ -140,18 +159,21 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
mPLLErrorListener.receive((double)mPhaseError);
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * Math.PI);
|
||||
|
||||
mPLLFrequencyListener.receive(loopFrequency);
|
||||
}
|
||||
|
||||
//Decode the dibit from the symbol and send to the listener
|
||||
if(mDibitListener != null)
|
||||
{
|
||||
mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol));
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
//TODO: Assemble dibits here and broadcast
|
||||
}
|
||||
}
|
||||
@@ -179,4 +201,18 @@ public class QPSKDemodulator implements ComplexSampleListener
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* 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, either 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 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/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
|
||||
public class SymbolDecisionData
|
||||
{
|
||||
private float[] mSamplesInphase;
|
||||
private float[] mSamplesQuadrature;
|
||||
private float mSamplesPerSymbol;
|
||||
private int mSampleIndex;
|
||||
private float mSampleIndexOffset;
|
||||
|
||||
/**
|
||||
* Symbol decision data - samples and timing decision for a single symbol decision instant.
|
||||
*
|
||||
* @param samplesInphase
|
||||
* @param samplesQuadrature
|
||||
* @param samplesPerSymbol
|
||||
* @param sampleIndex
|
||||
* @param sampleIndexOffset
|
||||
*/
|
||||
public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol,
|
||||
int sampleIndex, float sampleIndexOffset)
|
||||
{
|
||||
mSamplesInphase = samplesInphase;
|
||||
mSamplesQuadrature = samplesQuadrature;
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mSampleIndex = sampleIndex;
|
||||
mSampleIndexOffset = sampleIndexOffset;
|
||||
}
|
||||
|
||||
public float[] getInphaseSamples()
|
||||
{
|
||||
return mSamplesInphase;
|
||||
}
|
||||
|
||||
public float[] getQuadratureSamples()
|
||||
{
|
||||
return mSamplesQuadrature;
|
||||
}
|
||||
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return mSamplesPerSymbol;
|
||||
}
|
||||
|
||||
public int getSampleIndex()
|
||||
{
|
||||
return mSampleIndex;
|
||||
}
|
||||
|
||||
public float getSampleIndexOffset()
|
||||
{
|
||||
return mSampleIndexOffset;
|
||||
}
|
||||
}
|
||||
@@ -194,20 +194,21 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
|
||||
mErrorBuffer.put(phaseError);
|
||||
|
||||
double variance = mErrorBuffer.variance();
|
||||
double standardDeviation = mErrorBuffer.standardDeviation();
|
||||
|
||||
switch(mTracking)
|
||||
{
|
||||
case SEARCHING:
|
||||
if(Tracking.COARSE.meetsThreshold(variance))
|
||||
if(Tracking.COARSE.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.COARSE);
|
||||
}
|
||||
break;
|
||||
case COARSE:
|
||||
if(Tracking.COARSE.meetsThreshold(variance))
|
||||
if(Tracking.COARSE.contains(standardDeviation))
|
||||
{
|
||||
if(Tracking.FINE.meetsThreshold(variance))
|
||||
//Promote if possible - otherwise remain
|
||||
if(Tracking.FINE.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.FINE);
|
||||
}
|
||||
@@ -218,9 +219,10 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
break;
|
||||
case FINE:
|
||||
if(Tracking.FINE.meetsThreshold(variance))
|
||||
if(Tracking.FINE.contains(standardDeviation))
|
||||
{
|
||||
if(Tracking.LOCKED.meetsThreshold(variance))
|
||||
//Promote if possible - otherwise remain
|
||||
if(Tracking.LOCKED.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.LOCKED);
|
||||
}
|
||||
@@ -231,7 +233,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
break;
|
||||
case LOCKED:
|
||||
if(!Tracking.LOCKED.meetsThreshold(variance))
|
||||
if(!Tracking.LOCKED.contains(standardDeviation))
|
||||
{
|
||||
setTracking(Tracking.FINE);
|
||||
}
|
||||
|
||||
@@ -17,32 +17,55 @@ package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
public enum Tracking
|
||||
{
|
||||
SEARCHING(50.0, Math.PI),
|
||||
COARSE(100.0, 0.01),
|
||||
FINE(150.0, 0.001),
|
||||
LOCKED(200.0, 0.0001);
|
||||
// SEARCHING(50.0, 0.01, Math.PI),
|
||||
// COARSE(100.0, 0.001, 0.01),
|
||||
// FINE(150.0, 0.0001, 0.001),
|
||||
// LOCKED(200.0, 0.0, 0.0001);
|
||||
SEARCHING(100.0, 0.4, Math.PI),
|
||||
COARSE(150.0, 0.2, 0.4),
|
||||
FINE(175.0, 0.1, 0.2),
|
||||
LOCKED(200.0, 0.0, 0.1);
|
||||
|
||||
private double mLoopBandwidth;
|
||||
private double mVarianceThreshold;
|
||||
private double mThresholdMinimum;
|
||||
private double mThresholdMaximum;
|
||||
|
||||
Tracking(double loopBandwidth, double varianceThreshold)
|
||||
Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum)
|
||||
{
|
||||
mLoopBandwidth = loopBandwidth;
|
||||
mVarianceThreshold = varianceThreshold;
|
||||
mThresholdMinimum = thresholdMinimum;
|
||||
mThresholdMaximum = thresholdMaximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop bandwidth setting
|
||||
*/
|
||||
public double getLoopBandwidth()
|
||||
{
|
||||
return mLoopBandwidth;
|
||||
}
|
||||
|
||||
public double getVarianceThreshold()
|
||||
/**
|
||||
* Minimum error value for this entry
|
||||
*/
|
||||
public double getThresholdMinimum()
|
||||
{
|
||||
return mVarianceThreshold;
|
||||
return mThresholdMinimum;
|
||||
}
|
||||
|
||||
public boolean meetsThreshold(double variance)
|
||||
/**
|
||||
* Maximum error value for this entry
|
||||
*/
|
||||
public double getThresholdMaximum()
|
||||
{
|
||||
return variance < getVarianceThreshold();
|
||||
return mThresholdMaximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the error value is within the range for this entry
|
||||
*/
|
||||
public boolean contains(double standardDeviation)
|
||||
{
|
||||
return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user