330 WIP - changing to 45 degree rotation before demodulation.

This commit is contained in:
Denny
2018-02-20 05:27:33 -05:00
parent dda37a1198
commit 3d1ac30646
13 changed files with 360 additions and 220 deletions

View File

@@ -5,54 +5,86 @@ import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
public class RealInterpolator extends Interpolator
{
private final static Logger mLog = LoggerFactory.getLogger( RealInterpolator.class );
private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
private float mGain;
public RealInterpolator( float gain )
{
mGain = gain;
}
/**
* Calculates an interpolated value from the 8 samples beginning at the
* offset index. Uses the mu value to determine which of 128 filter kernels
* to use in order to closely approximate the ideal frequency response for
* the value of mu, ranging from 0.0 to 1.0, indicating the interpolated
* position in the sample set ranging from offset to offset + 7.
*
* @param samples - sample array of length at least offset + 7
*
* @param mu - interpolated sample position between 0 and 1.0
*
* @return - interpolated sample value
*/
public float filter( float[] samples, int offset, float mu )
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
private float mGain;
/* Identify the filter bank that corresponds to mu */
int index = (int)( NSTEPS * mu );
float accumulator = ( TAPS[ index ][ 7 ] * samples[ offset ] );
accumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ] );
accumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ] );
accumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ] );
accumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ] );
accumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ] );
accumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ] );
accumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ] );
return accumulator * mGain;
}
public Complex filter( float[] iSamples, float[] qSamples, int offset, float mu )
{
float i = filter( iSamples, offset, mu );
float q = filter( qSamples, offset, mu );
return new Complex( i, q );
}
/**
* Provides an interpolated sample point along an eight-sample waveform representation using 128 filters to
* provide approximate interpolation with a resolution of 1/128th of a sample.
*
* @param gain to apply to the interpolated sample
*/
public RealInterpolator(float gain)
{
mGain = gain;
}
/**
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
*
* @param samples - sample array of length at least offset + 7
* @param mu - interpolated sample position between 0.0 and 1.0
* @return - interpolated sample value
*/
public float filter(float[] samples, int offset, float mu)
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7);
/* Identify the filter bank that corresponds to mu */
int index = (int)(NSTEPS * mu);
float accumulator = (TAPS[index][7] * samples[offset]);
accumulator += (TAPS[index][6] * samples[offset + 1]);
accumulator += (TAPS[index][5] * samples[offset + 2]);
accumulator += (TAPS[index][4] * samples[offset + 3]);
accumulator += (TAPS[index][3] * samples[offset + 4]);
accumulator += (TAPS[index][2] * samples[offset + 5]);
accumulator += (TAPS[index][1] * samples[offset + 6]);
accumulator += (TAPS[index][0] * samples[offset + 7]);
return accumulator * mGain;
}
public Complex filter(float[] iSamples, float[] qSamples, int offset, float mu)
{
float i = filter(iSamples, offset, mu);
float q = filter(qSamples, offset, mu);
return new Complex(i, q);
}
public static void main(String[] args)
{
RealInterpolator interpolator = new RealInterpolator(1.0f);
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
double TWO_PI = Math.PI * 2.0;
float[] samples = new float[16];
for(int x = 0; x < 16; x++)
{
samples[x] = (float)Math.sin(TWO_PI * (double)x / 8.0);
}
mLog.debug("Samples: " + Arrays.toString(samples));
for(float x = 0.0f; x <= 1.01f; x += 0.1f)
{
mLog.debug(decimalFormat.format(x) + ": " + decimalFormat.format(interpolator.filter(samples, 1, x)));
}
}
}

View File

@@ -20,11 +20,20 @@ import io.github.dsheirer.sample.complex.Complex;
public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator
{
//Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / -4.0d);
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
@Override
public void adjust(Complex symbol)
{
symbol.multiply(DIFFERENTIAL_OFFSET);
}
@Override
public float getPhaseError(Complex symbol)
{
Complex copy = symbol.copy();
copy.multiply(DIFFERENTIAL_OFFSET);
return super.getPhaseError(copy);
}
}

View File

@@ -25,33 +25,34 @@ public class InterpolatingSymbolBuffer
private float[] mDelayLineInphase;
private float[] mDelayLineQuadrature;
private float mMu; //Sample Counter
private float mGainMu = 0.05f; //Sample Counter Gain
private float mOmega;
private float mGainOmega = 0.1f * mGainMu * mGainMu;
private float mOmegaRel = 0.005f;
private float mOmegaMid;
private float mSamplingPoint; //Sample Counter
private float mSampleCounterGain = 0.05f; //Sample Counter Gain
private float mSamplesPerSymbol;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaxDeviation = 0.005f;
private int mDelayLinePointer = 0;
private float mSamplesPerSymbol;
private int mTwiceSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
private SymbolDecisionData2 mSymbolDecisionData2;
public InterpolatingSymbolBuffer(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mMu = samplesPerSymbol;
mSamplingPoint = samplesPerSymbol;
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
mOmega = samplesPerSymbol;
mOmegaMid = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
mSamplesPerSymbol = samplesPerSymbol;
mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol);
}
public void receive(Complex currentSample)
{
mMu--;
mSymbolDecisionData2.receive(currentSample);
mSamplingPoint--;
//Fill up the delay line to use with the interpolator
mDelayLineInphase[mDelayLinePointer] = currentSample.inphase();
@@ -62,6 +63,7 @@ public class InterpolatingSymbolBuffer
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
@@ -70,7 +72,7 @@ public class InterpolatingSymbolBuffer
*/
public void increaseSampleCounter(float samplesToAdd)
{
mMu += samplesToAdd;
mSamplingPoint += samplesToAdd;
}
/**
@@ -81,13 +83,13 @@ public class InterpolatingSymbolBuffer
*/
public void resetAndAdjust(float symbolTimingError)
{
//mOmega is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
//mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per
//symbol
mOmega = mOmega + mGainOmega * -symbolTimingError;
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError;
mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation);
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
increaseSampleCounter((mOmega + (mGainMu * -symbolTimingError)));
increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError)));
}
/**
@@ -95,7 +97,7 @@ public class InterpolatingSymbolBuffer
*/
public boolean hasSymbol()
{
return mMu < 1.0f;
return mSamplingPoint < 1.0f;
}
/**
@@ -105,7 +107,7 @@ public class InterpolatingSymbolBuffer
public Complex getMiddleSample()
{
/* Calculate interpolated middle sample and current sample */
mMiddleSample.setValues(getInphase(mMu), getQuadrature(mMu));
mMiddleSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
return mMiddleSample;
}
@@ -116,17 +118,18 @@ public class InterpolatingSymbolBuffer
*/
public Complex getCurrentSample()
{
float half_omega = mOmega / 2.0f;
int half_sps = (int)Math.floor(half_omega);
float half_mu = mMu + half_omega - half_sps;
float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f;
int halfSamplingPointOffset = (int)Math.floor(halfDetectedSamplesPerSymbol);
float halfSamplingPoint = mSamplingPoint + halfDetectedSamplesPerSymbol - halfSamplingPointOffset;
if(half_mu > 1.0)
if(halfSamplingPoint > 1.0)
{
half_mu -= 1.0;
half_sps += 1;
halfSamplingPoint -= 1.0;
halfSamplingPointOffset += 1;
}
mCurrentSample.setValues(getInphase(half_mu, half_sps), getQuadrature(half_mu, half_sps));
mCurrentSample.setValues(getInphase(halfSamplingPointOffset, halfSamplingPoint),
getQuadrature(halfSamplingPointOffset, halfSamplingPoint));
return mCurrentSample;
}
@@ -136,11 +139,10 @@ public class InterpolatingSymbolBuffer
* be used to support an external eye-diagram chart.
* @return symbol decision data.
*/
public SymbolDecisionData getSymbolDecisionData()
public SymbolDecisionData2 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);
mSymbolDecisionData2.setSamplingPoint(mSamplingPoint);
return mSymbolDecisionData2;
}
/**
@@ -155,12 +157,13 @@ public class InterpolatingSymbolBuffer
/**
* Returns the interpolated inphase value for the specified offset
* @param delayLineOffset offset into the inphase delay line buffer
* @param interpolation into the buffer to calculate the interpolated sample
* @return inphase value of the interpolated sample
*/
public float getInphase(float interpolation, int indexOffet)
public float getInphase(int delayLineOffset, float interpolation)
{
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + indexOffet, interpolation);
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation);
}
/**
@@ -175,12 +178,13 @@ public class InterpolatingSymbolBuffer
/**
* Returns the interpolated quadrature value for the specified offset
* @param delayLineOffset offset into the quadrature delay line buffer
* @param interpolation into the buffer to calculate the interpolated sample
* @return quadrature value of the interpolated sample
*/
public float getQuadrature(float interpolation, int indexOffset)
public float getQuadrature(int delayLineOffset, float interpolation)
{
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + indexOffset, interpolation);
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation);
}
/**

View File

@@ -34,7 +34,7 @@ public class QPSKDemodulator implements ComplexSampleListener
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<Dibit> mDibitListener;
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<SymbolDecisionData2> mSymbolDecisionDataListener;
private Complex mPreviousSample = new Complex(0, 0);
private Complex mPreviousMiddleSample = new Complex(0, 0);
@@ -47,6 +47,7 @@ public class QPSKDemodulator implements ComplexSampleListener
private float mSymbolTimingError;
private double mSampleRate;
/**
* Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a
* phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier
@@ -129,6 +130,16 @@ public class QPSKDemodulator implements ComplexSampleListener
mMiddleSymbol.normalize();
mCurrentSymbol.normalize();
//Send to an external constellation symbol listener when registered
if(mSymbolListener != null)
{
mSymbolListener.receive(mCurrentSymbol);
}
//We rotate the symbol from star pattern to polar pattern to facilitate phase error calculation
// mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
// mSymbolPhaseErrorCalculator.adjust(mMiddleSymbol);
//Symbol timing error calculations
mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
@@ -138,20 +149,12 @@ 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);
mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f);
mPLL.adjust(mPhaseError);
if(mPLLErrorListener != null)
@@ -210,7 +213,7 @@ public class QPSKDemodulator implements ComplexSampleListener
mPLLFrequencyListener = listener;
}
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
{
mSymbolDecisionDataListener = listener;
}

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
* 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;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
public class SymbolDecisionData2
{
private ComplexCircularBuffer mBuffer;
private float mSamplingPoint;
public SymbolDecisionData2(int samplesPerSymbol)
{
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
}
public void receive(float inphase, float quadrature)
{
mBuffer.put(new Complex(inphase, quadrature));
}
public void receive(Complex complex)
{
mBuffer.put(complex.copy());
}
public void setSamplingPoint(float samplingPoint)
{
mSamplingPoint = samplingPoint;
}
public float getSamplingPoint()
{
return mSamplingPoint;
}
public Complex[] getDemodulated()
{
Complex[] samples = mBuffer.getAll();
int length = samples.length / 2;
Complex[] demodulated = new Complex[length];
for(int x = 0; x < samples.length / 2; x++)
{
Complex copy = samples[length + x].copy();
copy.multiply(samples[x].conjugate());
demodulated[x] = copy;
}
return demodulated;
}
}

View File

@@ -192,7 +192,12 @@ public class CostasLoop implements IPhaseLockedLoop
/* Limit frequency to +/- maximum loop frequency */
limitFrequency();
mErrorBuffer.put(phaseError);
// checkLoopBandwidth();
}
private void checkLoopBandwidth()
{
mErrorBuffer.put(mLoopFrequency);
double standardDeviation = mErrorBuffer.standardDeviation();

View File

@@ -22,9 +22,9 @@ public enum Tracking
// 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);
COARSE(200.0, 0.2, 0.4),
FINE(400.0, 0.1, 0.2),
LOCKED(600.0, 0.0, 0.1);
private double mLoopBandwidth;
private double mThresholdMinimum;