330 WIP - implements the new data-directed symbol timing and phase

error calculator.  After increasing the gain for symbol timing, this
new symbol evaluator class seems to track the signal very accurately
despite lots of noise and ISI.
This commit is contained in:
Denny
2018-03-04 11:42:23 -05:00
parent b563f2d11b
commit 45a6c5a0b6
9 changed files with 58 additions and 76 deletions

View File

@@ -29,11 +29,7 @@ public class DQPSKDemodulator implements ComplexSampleListener
private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected IPhaseLockedLoop mPLL;
protected IQPSKSymbolDecoder mSymbolDecoder;
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator;
private DQPSKTimingErrorDetector mSymbolTimingErrorDetector = new DQPSKTimingErrorDetector();
private DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
private Complex mReceivedSample = new Complex(0, 0);
private Complex mPreviousPrecedingSample = new Complex(0, 0);
private Complex mPreviousCurrentSample = new Complex(0, 0);
@@ -41,9 +37,6 @@ public class DQPSKDemodulator implements ComplexSampleListener
private Complex mCurrentSample = new Complex(0, 0);
private Complex mPrecedingSymbol = new Complex(0, 0);
protected Complex mCurrentSymbol = new Complex(0, 0);
protected float mPhaseError;
private float mSymbolTimingError;
private Listener<Dibit> mDibitListener;
/**
@@ -54,15 +47,11 @@ public class DQPSKDemodulator implements ComplexSampleListener
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL
* @param symbolDecoder to decode demodulated symbols into dibits
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
*/
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator,
IQPSKSymbolDecoder symbolDecoder, InterpolatingSampleBuffer interpolatingSampleBuffer)
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
{
mPLL = phaseLockedLoop;
mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator;
mSymbolDecoder = symbolDecoder;
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
}
@@ -128,13 +117,9 @@ public class DQPSKDemodulator implements ComplexSampleListener
mPrecedingSymbol.normalize();
mCurrentSymbol.normalize();
mSymbolEvaluator.setSymbol(mCurrentSymbol);
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
//Symbol timing error calculation
mSymbolTimingError = mSymbolTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol);
mSymbolTimingError = clip(mSymbolTimingError, 0.5f);
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError);
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
//Store current samples/symbols to use for the next period
mPreviousPrecedingSample.setValues(mPrecedingSample);
@@ -142,15 +127,11 @@ public class DQPSKDemodulator implements ComplexSampleListener
//Calculate the phase error of the current symbol relative to the expected constellation and provide
//feedback to the PLL
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
mLog.debug("Phase error - ORIG:" + mPhaseError + " NEW:" + mSymbolEvaluator.getPhaseError());
mPLL.adjust(mPhaseError);
mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
if(mDibitListener != null)
{
mDibitListener.receive(mSymbolDecoder.decode(mCurrentSymbol));
mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
}
}

View File

@@ -37,17 +37,14 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL
* @param symbolDecoder to decode demodulated symbols into dibits
* @param interpolatingSampleBuffer
* @param sampleRate of the incoming complex sample stream
*/
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator
symbolPhaseErrorCalculator, IQPSKSymbolDecoder symbolDecoder,
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
double sampleRate)
{
super(phaseLockedLoop, symbolPhaseErrorCalculator, symbolDecoder, interpolatingSampleBuffer);
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
}
@@ -78,7 +75,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mPhaseError);
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)

View File

@@ -25,74 +25,86 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private Complex mSymbolError = new Complex(0,0);
private Complex mSymbolError = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorSign = 1.0f;
private float mTimingErrorPolarity = 1.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
* Data-directed symbol phase and timing error detector and symbol decision slicer.
*
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
*
* Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
* relative to the immediately preceding symbol.
*/
public DQPSKSymbolEvaluator()
{
}
/**
* Sets the symbol to be evaluated and calculates the symbol decision and phase and timing errors of the symbol
* relative to the four QPSK reference symbol locations. After invoking this method, you can access the phase and
* timing errors and the symbol decision via their respective accessor methods.
* Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
* phase and timing errors and the symbol decision via their respective accessor methods.
*
* Phase and timing error values are calculated by determining the symbol's quadrant and multiplying the symbol
* by the complex conjugate of the reference symbol for that quadrant and then deriving the radian angle error
* value. Both phase and timing error use this angle error, however the timing error is corrected with the
* appropriate sign so that the error value indicates the correct error direction.
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
* relative to the angular vector rotation so that the error value indicates the correct error direction.
*
* @param complex symbol to be evaluated
* @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
@Override
public void setSymbol(Complex complex)
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(complex);
mSymbolError.setValues(current);
if(mSymbolError.quadrature() > 0.0f)
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_135);
}
mTimingErrorSign = 1.0f;
}
else
{
if(mSymbolError.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_135);
}
mTimingErrorSign = -1.0f;
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mSymbolError.quadrature();
//Timing error is the same as phase error with the sign corrected according to the symbol's hemisphere
mTimingError = mPhaseError * mTimingErrorSign;
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
}
/**
* Phase error of the symbol relative to the nearest reference symbol.
*
* @return phase error in radians of distance from the reference symbol.
*/
@Override

View File

@@ -23,9 +23,11 @@ public interface IDQPSKSymbolEvaluator
{
/**
* Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
* @param complex symbol
*
* @param preceding symbol used to detect (counter-)clock-wise rotation
* @param symbol currently being sampled
*/
void setSymbol(Complex complex);
void setSymbol(Complex preceding, Complex symbol);
/**
* Phase error of the symbol relative to the closest reference symbol

View File

@@ -31,7 +31,7 @@ public class InterpolatingSampleBuffer
private int mTwiceSamplesPerSymbol;
private float mSamplingPoint;
private float mSampleCounterGain = 0.05f;
private float mSampleCounterGain = 1.0f;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaximumSamplesPerSymbol;
@@ -130,7 +130,7 @@ public class InterpolatingSampleBuffer
*/
public Complex getPrecedingSample()
{
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]);
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
return mPrecedingSample;
}
@@ -152,7 +152,7 @@ public class InterpolatingSampleBuffer
*/
public float getInphase(float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation);
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
}
/**
@@ -162,6 +162,6 @@ public class InterpolatingSampleBuffer
*/
public float getQuadrature(float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation);
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
}
}

View File

@@ -175,7 +175,7 @@ public class P25Phase1Pane extends DecoderPane
{
if(mSamplesPerSymbolLineChart == null)
{
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 0.1, 40);
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40);
}
return mSamplesPerSymbolLineChart;

View File

@@ -21,12 +21,8 @@ import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
import io.github.dsheirer.dsp.psk.DQPSKDemodulator;
import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator;
import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
import io.github.dsheirer.dsp.psk.QPSKStarSlicer;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
@@ -43,8 +39,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
{
private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class);
protected static final double SYMBOL_RATE = 4800;
protected ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
protected IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer();
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
@@ -72,8 +66,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE);
mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
mInterpolatingSampleBuffer);
mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
//Message framer can trigger a symbol-inversion correction to the PLL when detected
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
@@ -91,6 +84,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
protected ComplexBuffer filter(ComplexBuffer complexBuffer)
{
return mBasebandFilter.filter(complexBuffer);
// return complexBuffer;
}
private double getSampleRate()

View File

@@ -61,8 +61,7 @@ public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE));
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop,
mSymbolPhaseErrorCalculator, mSymbolDecoder, instrumentedBuffer, mSampleRate);
DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate);
mQPSKDemodulator = instrumented;
instrumented.setSymbolListener(mSymbolListener);

View File

@@ -437,17 +437,14 @@ public class Complex implements Serializable
public static void main(String[] args)
{
double angle = Math.PI; //180 degrees
Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString());
Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle));
Complex tap = new Complex(1.0f, -0.1f);
Complex convolved = Complex.multiply(s1, tap);
mLog.debug("s: " + s1.toString() +
" t: " + tap.toString() +
" convolved: " + convolved.toString());
Complex symbol = new Complex(0.7665009f, -0.6422432f);
mLog.debug("SYM: " + symbol.toString());
symbol.multiply(ROTATE_FROM_MINUS_45);
mLog.debug("DER: " + symbol.toString());
}
}