mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 00:57:31 -05:00
Merge branch 'master' into 39-polyphase-channelizer
# Conflicts: # src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java # src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java # src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java # src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java # src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java # src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java # src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java # src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java # src/main/java/io/github/dsheirer/module/decode/Decoder.java # src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java # src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java # src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java # src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java # src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java # src/main/java/io/github/dsheirer/source/tuner/TunerModel.java # src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java # src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
This commit is contained in:
162
src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
Normal file
162
src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
Normal file
@@ -0,0 +1,162 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DQPSKDemodulator implements ComplexSampleListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
|
||||
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||
protected IPhaseLockedLoop mPLL;
|
||||
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);
|
||||
private Complex mPrecedingSample = new Complex(0, 0);
|
||||
private Complex mCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
private Listener<Dibit> mDibitListener;
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
|
||||
* adjust for any changes in symbol timing.
|
||||
*
|
||||
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
|
||||
{
|
||||
mPLL = phaseLockedLoop;
|
||||
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a (filtered) buffer containing complex samples for decoding
|
||||
* @param complexBuffer with complex samples
|
||||
*/
|
||||
public void receive(ComplexBuffer complexBuffer)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
receive(samples[x], samples[x + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
|
||||
* @param inphase value for the sample
|
||||
* @param quadrature value for the sample
|
||||
*/
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
//Update current sample with values
|
||||
mReceivedSample.setValues(inphase, quadrature);
|
||||
|
||||
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
|
||||
mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
|
||||
|
||||
//Store the sample in the interpolating buffer
|
||||
mInterpolatingSampleBuffer.receive(mReceivedSample);
|
||||
|
||||
//Calculate the symbol once we've stored enough samples
|
||||
if(mInterpolatingSampleBuffer.hasSymbol())
|
||||
{
|
||||
calculateSymbol();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a symbol from the interpolating buffer
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Get preceding sample and an interpolated current sample from the interpolating buffer
|
||||
mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample();
|
||||
mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample();
|
||||
|
||||
//Calculate preceding, current and following symbols as the delta rotation compared to the previous samples
|
||||
mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
|
||||
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
|
||||
mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
|
||||
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
|
||||
|
||||
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
|
||||
//Set gain to unity before we calculate the error value
|
||||
mPrecedingSymbol.normalize();
|
||||
mCurrentSymbol.normalize();
|
||||
|
||||
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
|
||||
|
||||
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//Store current samples/symbols to use for the next period
|
||||
mPreviousPrecedingSample.setValues(mPrecedingSample);
|
||||
mPreviousCurrentSample.setValues(mCurrentSample);
|
||||
|
||||
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
||||
//feedback to the PLL
|
||||
mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
|
||||
if(mDibitListener != null)
|
||||
{
|
||||
mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive dibit symbol decisions from this demodulator
|
||||
*/
|
||||
public void setDibitListener(Listener<Dibit> listener)
|
||||
{
|
||||
mDibitListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
public static float clip(float value, float maximum)
|
||||
{
|
||||
if(value > maximum)
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if(value < -maximum)
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
|
||||
{
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
|
||||
* adjust for any changes in symbol timing.
|
||||
*
|
||||
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer
|
||||
* @param sampleRate of the incoming complex sample stream
|
||||
*/
|
||||
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
{
|
||||
super(phaseLockedLoop, interpolatingSampleBuffer);
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent class symbol calculation to capture eye diagram data
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Eye diagram listener
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(
|
||||
((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
|
||||
}
|
||||
|
||||
super.calculateSymbol();
|
||||
|
||||
if(mSamplesPerSymbolListener != null)
|
||||
{
|
||||
mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
{
|
||||
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * Math.PI);
|
||||
|
||||
mPLLFrequencyListener.receive(loopFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive symbol decision data to produce an eye diagram.
|
||||
*/
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive decoded QPSK symbols
|
||||
*/
|
||||
public void setSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
|
||||
{
|
||||
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
|
||||
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
|
||||
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 float mPhaseError = 0.0f;
|
||||
private float mTimingError = 0.0f;
|
||||
private float mTimingErrorPolarity = 1.0f;
|
||||
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
|
||||
/**
|
||||
* Decision-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 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 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 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 preceding, Complex current)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
//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 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
|
||||
public float getPhaseError()
|
||||
{
|
||||
return mPhaseError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing error of the symbol relative to the nearest reference symbol.
|
||||
*
|
||||
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
|
||||
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
|
||||
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
|
||||
*/
|
||||
@Override
|
||||
public float getTimingError()
|
||||
{
|
||||
return mTimingError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference symbol that is closest to the transmitted/sampled symbol.
|
||||
*/
|
||||
@Override
|
||||
public Dibit getSymbolDecision()
|
||||
{
|
||||
return mSymbolDecision;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.complex.Complex;
|
||||
|
||||
public class GardnerDetector
|
||||
{
|
||||
private float mErrorInphase;
|
||||
private float mErrorQuadrature;
|
||||
private float mError;
|
||||
|
||||
/**
|
||||
* Gardner detector for recovering symbol timing error.
|
||||
*/
|
||||
public GardnerDetector()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
|
||||
*
|
||||
* @param previous symbol
|
||||
* @param middle symbol (ie midway between the previous and current)
|
||||
* @param current symbol
|
||||
* @return symbol timing error
|
||||
*/
|
||||
public float getError(Complex previous, Complex middle, Complex current)
|
||||
{
|
||||
mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase());
|
||||
mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature());
|
||||
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
|
||||
|
||||
return mError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
|
||||
*/
|
||||
public static float normalize(float error, float maximum)
|
||||
{
|
||||
if(Float.isNaN(error))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip(error, maximum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
public static float clip(float value, float maximum)
|
||||
{
|
||||
if(value > maximum)
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if(value < -maximum)
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
|
||||
public interface IDQPSKSymbolEvaluator
|
||||
{
|
||||
/**
|
||||
* Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
|
||||
*
|
||||
* @param preceding symbol used to detect (counter-)clock-wise rotation
|
||||
* @param symbol currently being sampled
|
||||
*/
|
||||
void setSymbol(Complex preceding, Complex symbol);
|
||||
|
||||
/**
|
||||
* Phase error of the symbol relative to the closest reference symbol
|
||||
* @return angular phase error in radians
|
||||
*/
|
||||
float getPhaseError();
|
||||
|
||||
/**
|
||||
* Timing error of the symbol relative to the closest reference symbol
|
||||
* @return angular timing error in radians where a positive value indicates early sampling and sample timing needs
|
||||
* to be increased and a negative value indicates late sampling where sample timing needs to be decreased.
|
||||
*/
|
||||
float getTimingError();
|
||||
|
||||
/**
|
||||
* Dibit decision indicating the reference symbol location closest to the symbol.
|
||||
* @return symbol decision
|
||||
*/
|
||||
Dibit getSymbolDecision();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public interface IQPSKSymbolDecoder
|
||||
{
|
||||
/**
|
||||
* Decodes the symbol and converts it into a Dibit
|
||||
* @param symbol to decode
|
||||
* @return decoded dibit
|
||||
*/
|
||||
Dibit decode(Complex symbol);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.complex.Complex;
|
||||
|
||||
public interface ISymbolPhaseErrorCalculator
|
||||
{
|
||||
/**
|
||||
* Calculates the phase error of the symbol relative to an alignment for the constellation
|
||||
* @param symbol to calculate error
|
||||
* @return error value
|
||||
*/
|
||||
float getPhaseError(Complex symbol);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.filter.interpolator.RealInterpolator;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class InterpolatingSampleBuffer
|
||||
{
|
||||
private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation
|
||||
|
||||
private Complex mPrecedingSample = new Complex(0,0);
|
||||
private Complex mCurrentSample = new Complex(0,0);
|
||||
|
||||
private float[] mDelayLineInphase;
|
||||
private float[] mDelayLineQuadrature;
|
||||
private int mDelayLinePointer = 0;
|
||||
private int mTwiceSamplesPerSymbol;
|
||||
|
||||
private float mSamplingPoint;
|
||||
private float mSampleCounterGain = 1.0f;
|
||||
private float mDetectedSamplesPerSymbol;
|
||||
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
|
||||
private float mMaximumSamplesPerSymbol;
|
||||
private float mMinimumSamplesPerSymbol;
|
||||
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
* @param samplesPerSymbol
|
||||
*/
|
||||
public InterpolatingSampleBuffer(float samplesPerSymbol)
|
||||
{
|
||||
mSamplingPoint = samplesPerSymbol;
|
||||
mDetectedSamplesPerSymbol = samplesPerSymbol;
|
||||
mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
|
||||
mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL);
|
||||
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
|
||||
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
|
||||
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the sample in the buffer and updates pointers.
|
||||
* @param sample
|
||||
*/
|
||||
public void receive(Complex sample)
|
||||
{
|
||||
mSamplingPoint--;
|
||||
|
||||
//Fill up the delay line to use with the interpolator
|
||||
mDelayLineInphase[mDelayLinePointer] = sample.inphase();
|
||||
mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase();
|
||||
mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature();
|
||||
mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature();
|
||||
|
||||
//Increment pointer and keep pointer in bounds
|
||||
mDelayLinePointer++;
|
||||
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal sample counter with additional samples to add to the symbol sample counter.
|
||||
* @param samplesToAdd to the sample counter
|
||||
*/
|
||||
public void increaseSampleCounter(float samplesToAdd)
|
||||
{
|
||||
mSamplingPoint += samplesToAdd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
|
||||
* symbol.
|
||||
*
|
||||
* @param symbolTimingError from a symbol timing error detector
|
||||
*/
|
||||
public void resetAndAdjust(float symbolTimingError)
|
||||
{
|
||||
//Adjust detected samples per symbol based on timing error
|
||||
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain);
|
||||
|
||||
//Constrain detected samples per symbol to min/max values
|
||||
if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol)
|
||||
{
|
||||
mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol;
|
||||
}
|
||||
|
||||
if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol)
|
||||
{
|
||||
mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol;
|
||||
}
|
||||
|
||||
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
|
||||
increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Current value of the detected samples per symbol
|
||||
*/
|
||||
public float getSamplingPoint()
|
||||
{
|
||||
return mSamplingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
|
||||
*/
|
||||
public boolean hasSymbol()
|
||||
{
|
||||
return mSamplingPoint < 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls
|
||||
* somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3.
|
||||
*/
|
||||
public Complex getPrecedingSample()
|
||||
{
|
||||
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
|
||||
return mPrecedingSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
|
||||
* complete symbol with the hasSymbol() method.
|
||||
*/
|
||||
public Complex getCurrentSample()
|
||||
{
|
||||
/* Calculate interpolated current sample */
|
||||
mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint));
|
||||
return mCurrentSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolated inphase value for the specified offset
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
* @return inphase value of the interpolated sample
|
||||
*/
|
||||
public float getInphase(float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolated quadrature value for the specified offset
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
* @return quadrature value of the interpolated sample
|
||||
*/
|
||||
public float getQuadrature(float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.complex.Complex;
|
||||
|
||||
public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
|
||||
{
|
||||
private SymbolDecisionData mSymbolDecisionData;
|
||||
|
||||
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
|
||||
{
|
||||
super(samplesPerSymbol);
|
||||
|
||||
mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
|
||||
}
|
||||
|
||||
public void receive(Complex sample)
|
||||
{
|
||||
super.receive(sample);
|
||||
mSymbolDecisionData.receive(sample);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
|
||||
return mSymbolDecisionData;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class LSMConstellationDecoder implements IQPSKSymbolDecoder
|
||||
{
|
||||
@Override
|
||||
public Dibit decode(Complex symbol)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,36 +2,37 @@ package io.github.dsheirer.dsp.psk;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014, 2015 Dennis Sheirer
|
||||
*
|
||||
* Copyright (C) 2014 - 2018 Dennis Sheirer
|
||||
*
|
||||
* Gardner detector, Costas loop and Interpolator derived from:
|
||||
*
|
||||
*
|
||||
* OP25 - gardner_costas_cc_impl.cc
|
||||
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
|
||||
*
|
||||
*
|
||||
* OP25 - scope.py
|
||||
* Copyright 2008-2011 Steve Glass
|
||||
* Copyright 2011, 2012, 2013 KA1RBI
|
||||
*
|
||||
*
|
||||
* GNURadio - control_loop and mpsk_receiver_cc
|
||||
* Copyright 2011,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
*
|
||||
* 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/>
|
||||
******************************************************************************/
|
||||
|
||||
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
@@ -48,19 +49,19 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
|
||||
* optimal sampling timing and a Costas Loop as a phase locked loop synchronized
|
||||
* Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
|
||||
* optimal sampling timing and a Costas Loop as a phase locked loop synchronized
|
||||
* with the incoming signal carrier frequency.
|
||||
*
|
||||
*
|
||||
* Sample Rate: 48000
|
||||
* Symbol Rate: 4800
|
||||
*/
|
||||
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
{
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)( Math.PI / 4.0d );
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)(Math.PI / 4.0d);
|
||||
|
||||
/* 45 degree point */
|
||||
public static final Complex POINT_45_DEGREES =
|
||||
@@ -73,46 +74,46 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
private CostasLoop mCostasLoop = new CostasLoop();
|
||||
|
||||
private ISourceEventListener mFrequencyChangeListener;
|
||||
|
||||
|
||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||
|
||||
|
||||
public LSMDemodulator()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSymbolListener = null;
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
|
||||
|
||||
public void setSymbolListener( Listener<Complex> listener )
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
|
||||
public void removeSymbolListener()
|
||||
{
|
||||
mSymbolListener = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void receive( float inphase, float quadrature )
|
||||
{
|
||||
mGardnerDetector.receive( inphase, quadrature );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the costas loop to correct when a
|
||||
* phase lock error is detected in the binary output stream.
|
||||
*
|
||||
*
|
||||
* @param correction - value in radians
|
||||
*/
|
||||
public void correctPhaseError( double correction )
|
||||
{
|
||||
mCostasLoop.correctPhaseError( correction );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
@@ -126,10 +127,10 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
@@ -145,37 +146,37 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
return clip( error, maximum );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
|
||||
public void removeListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the current loop frequency of the costas loop and broadcasts
|
||||
* tuner frequency offset adjustments 10 times a second as needed to keep
|
||||
* the frequency offset below half of the maximum frequency
|
||||
*/
|
||||
public class FrequencyControl
|
||||
{
|
||||
/* Set the trigger threshold at half of the maximum costas loop control
|
||||
* frequency */
|
||||
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
||||
( 2.0f * (float)Math.PI * 600.0f ) / 48000.0f;
|
||||
/**
|
||||
* Processes the current loop frequency of the costas loop and broadcasts
|
||||
* tuner frequency offset adjustments 10 times a second as needed to keep
|
||||
* the frequency offset below half of the maximum frequency
|
||||
*/
|
||||
public class FrequencyControl
|
||||
{
|
||||
/* Set the trigger threshold at half of the maximum costas loop control
|
||||
* frequency */
|
||||
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
||||
(2.0f * (float)Math.PI * 600.0f) / 48000.0f;
|
||||
|
||||
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer( 40 );
|
||||
|
||||
private long mFrequencyError = 0;
|
||||
|
||||
private int mCounter = 0;
|
||||
|
||||
public void receive( float frequency )
|
||||
{
|
||||
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
|
||||
|
||||
private long mFrequencyError = 0;
|
||||
|
||||
private int mCounter = 0;
|
||||
|
||||
public void receive(float frequency)
|
||||
{
|
||||
// mCounter++;
|
||||
//
|
||||
// if( mCounter >= 480 )
|
||||
@@ -212,377 +213,402 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
// {
|
||||
// mBuffer.get( frequency );
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gardner symbol timing detector
|
||||
*/
|
||||
public class GardnerSymbolTiming implements ComplexSampleListener
|
||||
{
|
||||
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
||||
/**
|
||||
* Gardner symbol timing detector
|
||||
*/
|
||||
public class GardnerSymbolTiming implements ComplexSampleListener
|
||||
{
|
||||
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
||||
|
||||
private float[] mDelayLineInphase = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
|
||||
private float[] mDelayLineQuadrature = new float[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
|
||||
private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||
private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||
|
||||
private int mDelayLinePointer = 0;
|
||||
private int mDelayLinePointer = 0;
|
||||
|
||||
/* Sampling point */
|
||||
private float mMu = 10.0f;
|
||||
private float mGainMu = 0.05f;
|
||||
|
||||
/* Samples per symbol */
|
||||
private float mOmega = 10.0f;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
private float mOmegaMid = 10.0f;
|
||||
/* Sampling point */
|
||||
private float mMu = 10.0f;
|
||||
private float mGainMu = 0.05f;
|
||||
|
||||
private RealInterpolator mInterpolator = new RealInterpolator( 1.0f );
|
||||
|
||||
private Complex mPreviousSample = new Complex( 0, 0 );
|
||||
private Complex mPreviousMiddleSample = new Complex( 0, 0 );
|
||||
private Complex mPreviousSymbol = new Complex( 0, 0 );
|
||||
|
||||
/**
|
||||
* Provides symbol sampling timing control
|
||||
*/
|
||||
public GardnerSymbolTiming()
|
||||
{
|
||||
}
|
||||
/* Samples per symbol */
|
||||
private float mOmega = 10.0f;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
private float mOmegaMid = 10.0f;
|
||||
|
||||
@Override
|
||||
public void receive( float inphase, float quadrature )
|
||||
{
|
||||
/* Count down samples per symbol until we calculate the symbol */
|
||||
mMu--;
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
|
||||
mCostasLoop.increment();
|
||||
|
||||
/* Mix incoming sample with costas loop to remove any rotation
|
||||
* that is present from a mis-tuned carrier frequency */
|
||||
|
||||
Complex costas = mCostasLoop.getCurrentVector();
|
||||
|
||||
float derotatedInphase = Complex.multiplyInphase( inphase, quadrature, costas.inphase(), costas.quadrature() );
|
||||
float derotatedQuadrature = Complex.multiplyQuadrature( inphase, quadrature, costas.inphase(), costas.quadrature() );
|
||||
|
||||
/* Fill up the delay line to use with the interpolator */
|
||||
mDelayLineInphase[ mDelayLinePointer ] = derotatedInphase;
|
||||
mDelayLineInphase[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedInphase;
|
||||
private Complex mPreviousSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
private Complex mPreviousSymbol = new Complex(0, 0);
|
||||
|
||||
mDelayLineQuadrature[ mDelayLinePointer ] = derotatedQuadrature;
|
||||
mDelayLineQuadrature[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = derotatedQuadrature;
|
||||
/**
|
||||
* Provides symbol sampling timing control
|
||||
*/
|
||||
public GardnerSymbolTiming()
|
||||
{
|
||||
}
|
||||
|
||||
/* Increment pointer and keep pointer in bounds */
|
||||
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
|
||||
|
||||
/* Calculate the symbol once we've stored enough samples */
|
||||
if( mMu <= 1.0f )
|
||||
{
|
||||
float half_omega = mOmega / 2.0f;
|
||||
int half_sps = (int)Math.floor( half_omega );
|
||||
float half_mu = mMu + half_omega - (float)half_sps;
|
||||
|
||||
if( half_mu > 1.0 )
|
||||
{
|
||||
half_mu -= 1.0;
|
||||
half_sps += 1;
|
||||
}
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
/* Count down samples per symbol until we calculate the symbol */
|
||||
mMu--;
|
||||
|
||||
/* Calculate interpolated middle sample and current sample */
|
||||
float middleSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, mDelayLinePointer, mMu );
|
||||
float middleSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, mDelayLinePointer, mMu );
|
||||
mCostasLoop.increment();
|
||||
|
||||
int index = mDelayLinePointer + half_sps;
|
||||
|
||||
float currentSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, index, half_mu );
|
||||
float currentSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, index, half_mu );
|
||||
/* Mix incoming sample with costas loop to remove any rotation
|
||||
* that is present from a mis-tuned carrier frequency */
|
||||
|
||||
/* Multiply current sample and conjugate (negated quadrature) of
|
||||
* previous sample to get symbols to use for gardner error feedback */
|
||||
Complex middleSymbol = Complex.multiply( middleSampleInphase,
|
||||
middleSampleQuadrature, mPreviousMiddleSample.conjugate() );
|
||||
Complex currentSymbol = Complex.multiply( currentSampleInphase,
|
||||
currentSampleQuadrature, mPreviousSample.conjugate() );
|
||||
Complex costas = mCostasLoop.getCurrentVector();
|
||||
|
||||
/* Set gain to unity */
|
||||
middleSymbol.normalize();
|
||||
currentSymbol.normalize();
|
||||
float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||
float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||
|
||||
/* Gardner timing error calculations */
|
||||
float errorInphase = ( mPreviousSymbol.inphase() -
|
||||
currentSymbol.inphase() ) * middleSymbol.inphase();
|
||||
/* Fill up the delay line to use with the interpolator */
|
||||
mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
|
||||
mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
|
||||
|
||||
float errorQuadrature = ( mPreviousSymbol.quadrature() -
|
||||
currentSymbol.quadrature() ) * middleSymbol.quadrature();
|
||||
mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
|
||||
mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
|
||||
|
||||
float gardnerError = normalize( errorInphase + errorQuadrature, 1.0f );
|
||||
|
||||
if( mEyeDiagramDataTap != null )
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData(
|
||||
Arrays.copyOfRange( mDelayLineInphase, 0, 20 ),
|
||||
Arrays.copyOfRange( mDelayLineQuadrature, 0, 20 ),
|
||||
mMu, (float)half_sps + half_mu, gardnerError ) );
|
||||
}
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * gardnerError;
|
||||
mOmega = mOmegaMid + clip( mOmega - mOmegaMid, mOmegaRel );
|
||||
/* Increment pointer and keep pointer in bounds */
|
||||
mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
|
||||
|
||||
/* Adjust sample timing based on error of current sample */
|
||||
mMu += mOmega + ( mGainMu * gardnerError );
|
||||
/* Calculate the symbol once we've stored enough samples */
|
||||
if(mMu <= 1.0f)
|
||||
{
|
||||
float half_omega = mOmega / 2.0f;
|
||||
int half_sps = (int)Math.floor(half_omega);
|
||||
float half_mu = mMu + half_omega - (float)half_sps;
|
||||
|
||||
/* Store current samples/symbols to use for the next period */
|
||||
mPreviousSample.setValues( currentSampleInphase, currentSampleQuadrature );
|
||||
mPreviousMiddleSample.setValues( middleSampleInphase, middleSampleQuadrature );
|
||||
mPreviousSymbol = currentSymbol;
|
||||
if(half_mu > 1.0)
|
||||
{
|
||||
half_mu -= 1.0;
|
||||
half_sps += 1;
|
||||
}
|
||||
|
||||
/* Update costas loop using phase error present in current
|
||||
* symbol. The symbol is rotated from star orientation to polar
|
||||
* orientation to simplify error calculation */
|
||||
mCostasLoop.receive( currentSymbol );
|
||||
/* Calculate interpolated middle sample and current sample */
|
||||
float middleSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, mDelayLinePointer, mMu);
|
||||
float middleSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, mDelayLinePointer, mMu);
|
||||
|
||||
int index = mDelayLinePointer + half_sps;
|
||||
|
||||
float currentSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, index, half_mu);
|
||||
float currentSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, index, half_mu);
|
||||
|
||||
/* Multiply current sample and conjugate (negated quadrature) of
|
||||
* previous sample to get symbols to use for gardner error feedback */
|
||||
Complex middleSymbol = Complex.multiply(middleSampleInphase,
|
||||
middleSampleQuadrature, mPreviousMiddleSample.conjugate());
|
||||
Complex currentSymbol = Complex.multiply(currentSampleInphase,
|
||||
currentSampleQuadrature, mPreviousSample.conjugate());
|
||||
|
||||
/* Set gain to unity */
|
||||
middleSymbol.normalize();
|
||||
currentSymbol.normalize();
|
||||
|
||||
/* Gardner timing error calculations */
|
||||
float errorInphase = (mPreviousSymbol.inphase() -
|
||||
currentSymbol.inphase()) * middleSymbol.inphase();
|
||||
|
||||
float errorQuadrature = (mPreviousSymbol.quadrature() -
|
||||
currentSymbol.quadrature()) * middleSymbol.quadrature();
|
||||
|
||||
float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
|
||||
|
||||
if(mEyeDiagramDataTap != null)
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData(
|
||||
Arrays.copyOfRange(mDelayLineInphase, 0, 20),
|
||||
Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
|
||||
mMu, (float)half_sps + half_mu, gardnerError));
|
||||
}
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * gardnerError;
|
||||
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||
|
||||
/* Adjust sample timing based on error of current sample */
|
||||
mMu += mOmega + (mGainMu * gardnerError);
|
||||
|
||||
/* Store current samples/symbols to use for the next period */
|
||||
mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
|
||||
mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
|
||||
mPreviousSymbol = currentSymbol;
|
||||
|
||||
/* Update costas loop using phase error present in current
|
||||
* symbol. The symbol is rotated from star orientation to polar
|
||||
* orientation to simplify error calculation */
|
||||
mCostasLoop.receive(currentSymbol);
|
||||
// mCostasLoop.receive(
|
||||
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
||||
|
||||
/* Dispatch the differentiated symbol to the registered listener */
|
||||
if( mSymbolListener != null )
|
||||
{
|
||||
mSymbolListener.receive( currentSymbol );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
||||
* the incoming signal to enable the signal to be mixed down to zero offset
|
||||
* for proper synchronization. The mFrequency value indicates the detected
|
||||
* frequency offset. We attempt to keep that value close to zero by issuing
|
||||
* frequency adjustments to the tuner channel source.
|
||||
*
|
||||
* Most of the costas loop code was ported from gnuradio/control_loop and
|
||||
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
||||
* OP25/cqpsk.py
|
||||
*/
|
||||
public class CostasLoop implements Listener<Complex>
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
private static final float MAXIMUM_FREQUENCY =
|
||||
( 2400.0f * (float)TWO_PI ) / 48000.0f;
|
||||
|
||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||
private float mDamping = (float)Math.sqrt( 2.0 ) / 2.0f;
|
||||
|
||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||
* loop control level */
|
||||
private float mLoopBandwidth = (float)( TWO_PI / 400.0d );
|
||||
|
||||
private float mAlphaGain = ( 4.0f * mDamping * mLoopBandwidth ) /
|
||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
||||
( mLoopBandwidth * mLoopBandwidth ) );
|
||||
|
||||
private float mBetaGain = ( 4.0f * mLoopBandwidth * mLoopBandwidth ) /
|
||||
( 1.0f + ( 2.0f * mDamping * mLoopBandwidth ) +
|
||||
( mLoopBandwidth * mLoopBandwidth ) );
|
||||
/* Dispatch the differentiated symbol to the registered listener */
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(currentSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float mLoopPhase = 0.0f;
|
||||
|
||||
private float mLoopFrequency = 0.0f;
|
||||
|
||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||
|
||||
public CostasLoop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the current phasor. Use this
|
||||
* method to apply correction when a +/- 90, or 180 degree phase error
|
||||
* is detected in the binary output stream.
|
||||
*
|
||||
* If the supplied correction value places the loop frequency outside
|
||||
* of the max frequency, then the frequency will be corrected 360
|
||||
* degrees in the opposite direction to maintain within the max
|
||||
* frequency bounds.
|
||||
*
|
||||
* @param correction - correction value in radians
|
||||
*/
|
||||
public void correctPhaseError( double correction )
|
||||
{
|
||||
mLoopFrequency += correction;
|
||||
|
||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mLoopPhase += mLoopFrequency;
|
||||
/**
|
||||
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
||||
* the incoming signal to enable the signal to be mixed down to zero offset
|
||||
* for proper synchronization. The mFrequency value indicates the detected
|
||||
* frequency offset. We attempt to keep that value close to zero by issuing
|
||||
* frequency adjustments to the tuner channel source.
|
||||
*
|
||||
* Most of the costas loop code was ported from gnuradio/control_loop and
|
||||
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
||||
* OP25/cqpsk.py
|
||||
*/
|
||||
public class CostasLoop implements Listener<Complex>, IPhaseLockedLoop
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
/* Keep the loop phase in bounds */
|
||||
unwrapPhase();
|
||||
}
|
||||
private static final float MAXIMUM_FREQUENCY =
|
||||
(2400.0f * (float)TWO_PI) / 48000.0f;
|
||||
|
||||
private void unwrapPhase()
|
||||
{
|
||||
while( mLoopPhase > TWO_PI )
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
while( mLoopPhase < -TWO_PI )
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current vector of the loop rotated by THETA degrees.
|
||||
*/
|
||||
public Complex getCurrentVector()
|
||||
{
|
||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||
private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
|
||||
|
||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||
* loop control level */
|
||||
private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
|
||||
|
||||
private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
|
||||
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||
(mLoopBandwidth * mLoopBandwidth));
|
||||
|
||||
private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
|
||||
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||
(mLoopBandwidth * mLoopBandwidth));
|
||||
|
||||
private float mLoopPhase = 0.0f;
|
||||
|
||||
private float mLoopFrequency = 0.0f;
|
||||
|
||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||
|
||||
public CostasLoop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the current phasor. Use this
|
||||
* method to apply correction when a +/- 90, or 180 degree phase error
|
||||
* is detected in the binary output stream.
|
||||
*
|
||||
* If the supplied correction value places the loop frequency outside
|
||||
* of the max frequency, then the frequency will be corrected 360
|
||||
* degrees in the opposite direction to maintain within the max
|
||||
* frequency bounds.
|
||||
*
|
||||
* @param correction - correction value in radians
|
||||
*/
|
||||
public void correctPhaseError(double correction)
|
||||
{
|
||||
mLoopFrequency += correction;
|
||||
|
||||
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mLoopPhase += mLoopFrequency;
|
||||
|
||||
/* Keep the loop phase in bounds */
|
||||
unwrapPhase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mLoopPhase = 0.0f;
|
||||
mLoopFrequency = 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjust(double error)
|
||||
{
|
||||
adjust(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void correctInversion(double correction)
|
||||
{
|
||||
correctPhaseError(correction);
|
||||
}
|
||||
|
||||
private void unwrapPhase()
|
||||
{
|
||||
while(mLoopPhase > TWO_PI)
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
while(mLoopPhase < -TWO_PI)
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current vector of the loop rotated by THETA degrees.
|
||||
*/
|
||||
public Complex getCurrentVector()
|
||||
{
|
||||
// return ComplexSample.fromAngle( mLoopPhase + THETA );
|
||||
return Complex.fromAngle( mLoopPhase );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Complex complex )
|
||||
{
|
||||
/* Calculate phase error */
|
||||
float phaseError = getPhaseError( complex );
|
||||
|
||||
Dibit d = QPSKPolarSlicer.decide( complex );
|
||||
return Complex.fromAngle(mLoopPhase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Complex incrementAndGetCurrentVector()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
/* Calculate phase error */
|
||||
float phaseError = getPhaseError(complex);
|
||||
|
||||
Dibit d = mQPSKSymbolDecoder.decode(complex);
|
||||
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
|
||||
|
||||
/* Adjust for phase error */
|
||||
adjust( phaseError );
|
||||
}
|
||||
/* Adjust for phase error */
|
||||
adjust(phaseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phase_error - (-)= late and (+)= early
|
||||
*/
|
||||
private void adjust( float phase_error )
|
||||
{
|
||||
mLoopFrequency += mBetaGain * phase_error;
|
||||
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phase_error - (-)= late and (+)= early
|
||||
*/
|
||||
private void adjust(float phase_error)
|
||||
{
|
||||
mLoopFrequency += mBetaGain * phase_error;
|
||||
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
||||
|
||||
/* Maintain phase between +/- 2 * PI */
|
||||
unwrapPhase();
|
||||
/* Maintain phase between +/- 2 * PI */
|
||||
unwrapPhase();
|
||||
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
}
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains the frequency within the bounds of +/- loop frequency
|
||||
*/
|
||||
private void limitFrequency()
|
||||
{
|
||||
/* Check for and issue tuner offset correction */
|
||||
mFrequencyControl.receive( mLoopFrequency );
|
||||
|
||||
if( mLoopFrequency > MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency = MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if( mLoopFrequency < -MAXIMUM_FREQUENCY )
|
||||
{
|
||||
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Constrains the frequency within the bounds of +/- loop frequency
|
||||
*/
|
||||
private void limitFrequency()
|
||||
{
|
||||
/* Check for and issue tuner offset correction */
|
||||
mFrequencyControl.receive(mLoopFrequency);
|
||||
|
||||
/**
|
||||
* Detects rotational error present when the costas loop is not aligned
|
||||
* with the carrier frequency. Provides error feedback to adjust
|
||||
* mixer frequency.
|
||||
*/
|
||||
public float getPhaseError( Complex complex )
|
||||
{
|
||||
float phase_error = 0;
|
||||
|
||||
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
|
||||
{
|
||||
if( complex.inphase() > 0 )
|
||||
{
|
||||
phase_error = -complex.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = complex.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( complex.quadrature() > 0 )
|
||||
{
|
||||
phase_error = complex.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = -complex.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
return phase_error;
|
||||
}
|
||||
}
|
||||
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency = MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
TapGroup group = new TapGroup( "LSM Demodulator" );
|
||||
|
||||
group.add( new EyeDiagramDataTap( "Eye Diagram", 0, 4800 ) );
|
||||
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
/**
|
||||
* Detects rotational error present when the costas loop is not aligned
|
||||
* with the carrier frequency. Provides error feedback to adjust
|
||||
* mixer frequency.
|
||||
*/
|
||||
public float getPhaseError(Complex complex)
|
||||
{
|
||||
float phase_error = 0;
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||
}
|
||||
}
|
||||
if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
|
||||
{
|
||||
if(complex.inphase() > 0)
|
||||
{
|
||||
phase_error = -complex.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = complex.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(complex.quadrature() > 0)
|
||||
{
|
||||
phase_error = complex.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = -complex.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
if( tap instanceof EyeDiagramDataTap )
|
||||
{
|
||||
mEyeDiagramDataTap = null;
|
||||
}
|
||||
}
|
||||
return phase_error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if(mAvailableTaps == null)
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
TapGroup group = new TapGroup("LSM Demodulator");
|
||||
|
||||
group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
|
||||
|
||||
mAvailableTaps.add(group);
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap(Tap tap)
|
||||
{
|
||||
if(tap instanceof EyeDiagramDataTap)
|
||||
{
|
||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap(Tap tap)
|
||||
{
|
||||
if(tap instanceof EyeDiagramDataTap)
|
||||
{
|
||||
mEyeDiagramDataTap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
@@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class QPSKStarSlicer implements Listener<Complex>
|
||||
public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
/**
|
||||
* Slices a ComplexSample representing a phase shifted symbol according to
|
||||
* the following constellation pattern:
|
||||
*
|
||||
* 00 | 01
|
||||
* ---|---
|
||||
* 10 | 11
|
||||
*/
|
||||
public QPSKStarSlicer()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
/**
|
||||
* Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
|
||||
*
|
||||
* 00 +3 | 01 +1
|
||||
* ------|------
|
||||
* 10 -1 | 11 -1
|
||||
*/
|
||||
public QPSKStarSlicer()
|
||||
{
|
||||
}
|
||||
|
||||
public void addListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
public void addListener(Listener<Dibit> listener)
|
||||
{
|
||||
mBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener<Dibit> listener)
|
||||
{
|
||||
mBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Complex sample)
|
||||
{
|
||||
mBroadcaster.broadcast(decode(sample));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dibit decode(Complex sample)
|
||||
{
|
||||
if(sample.inphase() > 0)
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Complex sample )
|
||||
{
|
||||
mBroadcaster.broadcast( decide( sample ) );
|
||||
}
|
||||
|
||||
public static Dibit decide( Complex sample )
|
||||
{
|
||||
if( sample.inphase() > 0 )
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/*******************************************************************************
|
||||
* 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.dsp.symbol.Dibit;
|
||||
@@ -5,24 +20,7 @@ import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014,2015 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/>
|
||||
******************************************************************************/
|
||||
public class QPSKPolarSlicer implements Listener<Complex>
|
||||
public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
@@ -37,7 +35,7 @@ public class QPSKPolarSlicer implements Listener<Complex>
|
||||
* / \
|
||||
* / 11 \
|
||||
*/
|
||||
public QPSKPolarSlicer()
|
||||
public QPSKSymbolDecoder()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -60,10 +58,10 @@ public class QPSKPolarSlicer implements Listener<Complex>
|
||||
@Override
|
||||
public void receive( Complex complex )
|
||||
{
|
||||
mBroadcaster.broadcast( decide( complex ) );
|
||||
mBroadcaster.broadcast( decode( complex ) );
|
||||
}
|
||||
|
||||
public static Dibit decide( Complex complex )
|
||||
public Dibit decode(Complex complex )
|
||||
{
|
||||
if( Math.abs( complex.inphase() ) > Math.abs( complex.quadrature() ) )
|
||||
{
|
||||
@@ -0,0 +1,57 @@
|
||||
/*******************************************************************************
|
||||
* 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.sample.complex.Complex;
|
||||
|
||||
/**
|
||||
* Calculates the phase error for a QPSK polar-orientation constellation
|
||||
*/
|
||||
public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
|
||||
{
|
||||
private float mPhaseError;
|
||||
|
||||
@Override
|
||||
public float getPhaseError(Complex symbol)
|
||||
{
|
||||
mPhaseError = 0.0f;
|
||||
|
||||
if(Math.abs(symbol.inphase()) > Math.abs(symbol.quadrature()))
|
||||
{
|
||||
if(symbol.inphase() > 0)
|
||||
{
|
||||
mPhaseError = -symbol.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
mPhaseError = symbol.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(symbol.quadrature() > 0)
|
||||
{
|
||||
mPhaseError = symbol.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
mPhaseError = -symbol.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
return mPhaseError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*******************************************************************************
|
||||
* 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 SymbolDecisionData
|
||||
{
|
||||
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
|
||||
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
|
||||
|
||||
private ComplexCircularBuffer mBuffer;
|
||||
private float mSamplingPoint;
|
||||
|
||||
/**
|
||||
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
|
||||
* @param samplesPerSymbol to size the buffer
|
||||
*/
|
||||
public SymbolDecisionData(int samplesPerSymbol)
|
||||
{
|
||||
mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the complex sample in the buffer
|
||||
*/
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
mBuffer.put(new Complex(inphase, quadrature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the complex sample in the buffer
|
||||
*/
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
mBuffer.put(complex.copy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
|
||||
*/
|
||||
public void setSamplingPoint(float samplingPoint)
|
||||
{
|
||||
mSamplingPoint = samplingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fractional sampling point
|
||||
*/
|
||||
public float getSamplingPoint()
|
||||
{
|
||||
return mSamplingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of demodulated samples representing the current symbol where each current sample is demodulated against
|
||||
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
|
||||
*
|
||||
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*******************************************************************************
|
||||
* 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 enum SymbolInversion
|
||||
{
|
||||
DEGREES_90_PLUS(Math.PI / 2.0),
|
||||
DEGREES_90_MINUS(Math.PI / -2.0),
|
||||
DEGREES_180(Math.PI);
|
||||
|
||||
private double mAngleRadians;
|
||||
|
||||
SymbolInversion(double angleRadians)
|
||||
{
|
||||
mAngleRadians = angleRadians;
|
||||
}
|
||||
|
||||
public double getError()
|
||||
{
|
||||
return mAngleRadians;
|
||||
}
|
||||
}
|
||||
182
src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
Normal file
182
src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java
Normal file
@@ -0,0 +1,182 @@
|
||||
/*******************************************************************************
|
||||
* 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.pll;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
/**
|
||||
* Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to
|
||||
* zeroize any frequency offset inherent in the signal due to either mis-tuning or carrier frequency drift.
|
||||
*
|
||||
* Costas loop structure was modeled from KA1RBI's OP25/cqpsk.py
|
||||
*
|
||||
* Loop bandwidth and alpha/beta gain formulas derived from:
|
||||
* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html
|
||||
*/
|
||||
public class CostasLoop implements IPhaseLockedLoop
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
private Complex mCurrentVector = new Complex(0, 0);
|
||||
private double mLoopPhase = 0.0;
|
||||
private double mLoopFrequency = 0.0;
|
||||
private double mMaximumLoopFrequency;
|
||||
private double mDamping = Math.sqrt(2.0) / 2.0;
|
||||
private double mAlphaGain;
|
||||
private double mBetaGain;
|
||||
private Tracking mTracking = Tracking.NORMAL;
|
||||
|
||||
/**
|
||||
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
|
||||
*
|
||||
* @param sampleRate of the incoming samples
|
||||
* @param symbolRate of the digital signal
|
||||
*/
|
||||
public CostasLoop(double sampleRate, double symbolRate)
|
||||
{
|
||||
mMaximumLoopFrequency = TWO_PI * (symbolRate / 2.0) / sampleRate;
|
||||
updateLoopBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects the current phase tracker when an inverted output is detected. The inversion will take the form of
|
||||
* +/- 90 degrees or +/- 180 degrees, although the latter correction is equal regardless of the sign.
|
||||
*
|
||||
* If the supplied correction value places the loop frequency outside of the max frequency, then the frequency will
|
||||
* be corrected 360 degrees in the opposite direction to maintain within the max frequency bounds.
|
||||
*
|
||||
* @param correction as measured in radians
|
||||
*/
|
||||
public void correctInversion(double correction)
|
||||
{
|
||||
mLoopFrequency += correction;
|
||||
|
||||
while(mLoopFrequency > mMaximumLoopFrequency)
|
||||
{
|
||||
mLoopFrequency -= 2.0 * mMaximumLoopFrequency;
|
||||
}
|
||||
|
||||
while(mLoopFrequency < -mMaximumLoopFrequency)
|
||||
{
|
||||
mLoopFrequency += 2.0 * mMaximumLoopFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the loop bandwidth and alpha/beta gains according to the current loop synchronization state.
|
||||
*/
|
||||
private void updateLoopBandwidth()
|
||||
{
|
||||
double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
|
||||
|
||||
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
|
||||
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tracking state for this costas loop. The tracking state affects the aggressiveness of the alpha/beta
|
||||
* gain values in synchronizing with the signal carrier.
|
||||
*
|
||||
* @param tracking
|
||||
*/
|
||||
public void setTracking(Tracking tracking)
|
||||
{
|
||||
mTracking = tracking;
|
||||
updateLoopBandwidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop for each sample received at the sample rate.
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mLoopPhase += mLoopFrequency;
|
||||
|
||||
/* Normalize phase between +/- 2 * PI */
|
||||
if(mLoopPhase > TWO_PI)
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
if(mLoopPhase < -TWO_PI)
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current vector of the loop. Note: this value is updated for the current angle in radians each time this method
|
||||
* is invoked.
|
||||
*/
|
||||
public Complex getCurrentVector()
|
||||
{
|
||||
mCurrentVector.setAngle(mLoopPhase);
|
||||
return mCurrentVector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Complex incrementAndGetCurrentVector()
|
||||
{
|
||||
increment();
|
||||
return getCurrentVector();
|
||||
}
|
||||
|
||||
public double getLoopFrequency()
|
||||
{
|
||||
return mLoopFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phaseError - (-)= late and (+)= early
|
||||
*/
|
||||
public void adjust(double phaseError)
|
||||
{
|
||||
mLoopFrequency += (mBetaGain * phaseError);
|
||||
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
|
||||
|
||||
/* Normalize phase between +/- 2 * PI */
|
||||
if(mLoopPhase > TWO_PI)
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
if(mLoopPhase < -TWO_PI)
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
if(mLoopFrequency > mMaximumLoopFrequency)
|
||||
{
|
||||
mLoopFrequency = mMaximumLoopFrequency;
|
||||
}
|
||||
|
||||
if(mLoopFrequency < -mMaximumLoopFrequency)
|
||||
{
|
||||
mLoopFrequency = -mMaximumLoopFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mLoopPhase = 0.0;
|
||||
mLoopFrequency = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* 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.pll;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
/**
|
||||
* Phase-Locked Loop (PLL) interface
|
||||
*/
|
||||
public interface IPhaseLockedLoop
|
||||
{
|
||||
/**
|
||||
* Increments the PLL by one sample period
|
||||
*/
|
||||
void increment();
|
||||
|
||||
/**
|
||||
* Applies an error value to the PLL.
|
||||
*
|
||||
* @param error value in the range of -PI <> +PI
|
||||
*/
|
||||
void adjust(double error);
|
||||
|
||||
/**
|
||||
* Adjusts the current phase of the loop to account for +/-90 or 180 degree inverted phase locks when detected
|
||||
* @param correction to apply to the phase of the loop
|
||||
*/
|
||||
void correctInversion(double correction);
|
||||
|
||||
/**
|
||||
* Current vector of the PLL to use in de-spinning the incoming samples.
|
||||
* @return complex vector
|
||||
*/
|
||||
Complex getCurrentVector();
|
||||
|
||||
/**
|
||||
* Increments the phase rotation and gets the current vector of the PLL to use in de-spinning the incoming samples.
|
||||
* @return complex vector
|
||||
*/
|
||||
Complex incrementAndGetCurrentVector();
|
||||
|
||||
/**
|
||||
* Reset tracking to 0
|
||||
*/
|
||||
void reset();
|
||||
}
|
||||
42
src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
Normal file
42
src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java
Normal file
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* 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.pll;
|
||||
|
||||
/**
|
||||
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
|
||||
*/
|
||||
public enum Tracking
|
||||
{
|
||||
FASTEST(50.0),
|
||||
FAST(100.0),
|
||||
NORMAL(200.0),
|
||||
SLOW(400.0);
|
||||
|
||||
private double mLoopBandwidth;
|
||||
|
||||
Tracking(double loopBandwidth)
|
||||
{
|
||||
mLoopBandwidth = loopBandwidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop bandwidth setting
|
||||
*/
|
||||
public double getLoopBandwidth()
|
||||
{
|
||||
return mLoopBandwidth;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user