Resolves #373 - Adds support for Tuner frequency error PPM monitoring

and auto-correction for R820T tuners.

-Updated TunerModel and TunerViewPanel to add 2 new columns to
display the current tuner's PPM setting and (when available) the
measured frequency error averaged over 5 second intervals provided by
certain decoders (e.g. P25 C4FM and LSM).  Frequency error value from
P25 decoders is provided from the Costas Loop PLL once 2 or more P25
message sync events have been observed in a short period.
-Updated R820T tuner to auto-correct PPM when measured frequency error
exceeds the +/- 0.4PPM threshold for a period of 30 seconds or more.
Feature can be enabled/disabled by user in the tuner editor panel.

Resolved several issues with P25 C4FM/LSM decoders that was impacting
performance related to channel misalignment from tuner PPM offsets.
This commit is contained in:
Denny
2018-07-29 10:48:29 -04:00
parent 5c0d97d942
commit 0c47efea03
44 changed files with 1296 additions and 483 deletions

View File

@@ -149,7 +149,7 @@ public class C4FMSymbolFilter implements Listener<ReusableBuffer>, ISourceEventL
private static final int SAMPLE_RATE = 48000;
private static final int SYMBOL_RATE = 4800;
/* Tracking loop gain constant */
/* PLLGain loop gain constant */
private static final double K_SYMBOL_SPREAD = 0.0100;
/* Constraints on symbol spreading */

View File

@@ -132,7 +132,7 @@ public abstract class P25Decoder extends Decoder implements ISourceEventListener
* issue frequency correction requests to the channel source.
* @param sourceEvent to broadcast
*/
protected void broadcast(SourceEvent sourceEvent)
public void broadcast(SourceEvent sourceEvent)
{
if(mSourceEventListener != null)
{

View File

@@ -23,8 +23,8 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.Tracking;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
@@ -36,10 +36,12 @@ import java.util.Map;
public class P25DecoderC4FM extends P25Decoder
{
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class);
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected AdaptivePLLGainMonitor mPLLGainMonitor;
protected P25MessageFramer mMessageFramer;
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
@@ -54,7 +56,7 @@ public class P25DecoderC4FM extends P25Decoder
public P25DecoderC4FM(AliasList aliasList)
{
super(4800.0, aliasList);
setSampleRate(48000.0);
setSampleRate(25000.0);
}
public void setSampleRate(double sampleRate)
@@ -64,14 +66,16 @@ public class P25DecoderC4FM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mCostasLoop.setTracking(Tracking.FASTEST);
mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
//Message framer can issue a symbol-inversion correction request to the PLL when detected
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
//The Costas Loop receives symbol-inversion correction requests when detected.
//The PLL gain monitor receives sync detect/loss signals from the message framer
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
mMessageFramer.setListener(getMessageProcessor());
mMessageFramer.setSampleRate(sampleRate);
mQPSKDemodulator.setSymbolListener(mMessageFramer);
}
@@ -165,6 +169,10 @@ public class P25DecoderC4FM extends P25Decoder
mCostasLoop.reset();
setSampleRate(sourceEvent.getValue().doubleValue());
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
mCostasLoop.reset();
break;
}
}
@@ -183,5 +191,6 @@ public class P25DecoderC4FM extends P25Decoder
public void reset()
{
mCostasLoop.reset();
mPLLGainMonitor.reset();
}
}

View File

@@ -22,20 +22,17 @@ import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKGardnerDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.AdaptivePLLGainMonitor;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.Tracking;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class P25DecoderLSM extends P25Decoder
{
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class);
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
protected static final float SAMPLE_COUNTER_GAIN = 0.3f;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter2 mBasebandFilter;
@@ -43,6 +40,7 @@ public class P25DecoderLSM extends P25Decoder
protected DQPSKGardnerDemodulator mQPSKDemodulator;
protected P25MessageFramer mMessageFramer;
protected CostasLoop mCostasLoop;
protected AdaptivePLLGainMonitor mPLLGainMonitor;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
/**
@@ -54,11 +52,12 @@ public class P25DecoderLSM extends P25Decoder
public P25DecoderLSM(AliasList aliasList)
{
super(4800.0, aliasList);
setSampleRate(48000.0);
setSampleRate(25000.0);
}
/**
* Sets or changes the channel sample rate
*
* @param sampleRate in hertz
*/
public void setSampleRate(double sampleRate)
@@ -68,14 +67,17 @@ public class P25DecoderLSM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mCostasLoop.setTracking(Tracking.NORMAL);
mPLLGainMonitor = new AdaptivePLLGainMonitor(mCostasLoop, this);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
//Message framer can issue a symbol-inversion correction request to the PLL when detected
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
//The Costas Loop receives symbol-inversion correction requests when detected.
//The PLL gain monitor receives sync detect/loss signals from the message framer
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop, mPLLGainMonitor);
mMessageFramer.setListener(getMessageProcessor());
mMessageFramer.setSampleRate(sampleRate);
mQPSKDemodulator.setSymbolListener(mMessageFramer);
}
@@ -97,13 +99,18 @@ public class P25DecoderLSM extends P25Decoder
/**
* Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
*
* @param reusableComplexBuffer to filter
* @return filtered complex buffer
*/
protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
{
//No additional filtering of the channel is currently needed, since the polyphase channelizer
//provides the filtering.
return reusableComplexBuffer;
//User accounting of the incoming buffer is handled by the filter
return mBasebandFilter.filter(reusableComplexBuffer);
// return mBasebandFilter.filter(reusableComplexBuffer);
}
/**
@@ -119,6 +126,10 @@ public class P25DecoderLSM extends P25Decoder
setSampleRate(sourceEvent.getValue().doubleValue());
mCostasLoop.reset();
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Reset the PLL if/when the tuner PPM changes so that we can re-lock
mCostasLoop.reset();
break;
}
}
@@ -129,6 +140,7 @@ public class P25DecoderLSM extends P25Decoder
public void reset()
{
mCostasLoop.reset();
mPLLGainMonitor.reset();
}
public void dispose()
@@ -163,7 +175,7 @@ public class P25DecoderLSM extends P25Decoder
if(filter == null)
{
filter = FilterFactory.getLowPass(48000, 7250, 8000, 60,
filter = FilterFactory.getLowPass(getSampleRate(), 7250, 8000, 60,
WindowType.HANN, true);
mBasebandFilters.put(getSampleRate(), filter);

View File

@@ -36,11 +36,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class P25MessageFramer implements Listener<Dibit>
{
private final static Logger mLog =
LoggerFactory.getLogger(P25MessageFramer.class);
private final static Logger mLog = LoggerFactory.getLogger(P25MessageFramer.class);
/* Determines the threshold for sync pattern soft matching */
private static final int SYNC_MATCH_THRESHOLD = 2;
@@ -49,9 +49,10 @@ public class P25MessageFramer implements Listener<Dibit>
/* Costas Loop phase lock error correction values. A phase lock error of
* 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
* error of 180 degrees requires a correction of 1/2 of the symbol rate */
public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
public static final double DEFAULT_SAMPLE_RATE = 25000.0;
public static final double FREQUENCY_PHASE_CORRECTION_90_DEGREES = 1200.0;
public static final double FREQUENCY_PHASE_CORRECTION_180_DEGREES = 2400.0;
public static final int TSBK_BEGIN = 64;
public static final int TSBK_CRC_START = 144;
public static final int TSBK_END = 260;
@@ -71,9 +72,12 @@ public class P25MessageFramer implements Listener<Dibit>
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD);
private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher(48);
private ArrayList<P25MessageAssembler> mAssemblers =
new ArrayList<P25MessageAssembler>();
private PLLPhaseInversionDetector mInversionDetector90CW;
private PLLPhaseInversionDetector mInversionDetector90CCW;
private PLLPhaseInversionDetector mInversionDetector180;
private MultiSyncPatternMatcher mMatcher;
private List<P25MessageAssembler> mAssemblers = new ArrayList<P25MessageAssembler>();
private Listener<Message> mListener;
private AliasList mAliasList;
@@ -88,10 +92,13 @@ public class P25MessageFramer implements Listener<Dibit>
* to the message length, and then broadcast that bit buffer to the registered
* listener.
*/
public P25MessageFramer(AliasList aliasList)
public P25MessageFramer(AliasList aliasList, ISyncDetectListener syncDetectListener)
{
mAliasList = aliasList;
//Assign the sync detect listener to the matcher with a sync loss threshold equal to the longest message length
mMatcher = new MultiSyncPatternMatcher(syncDetectListener, DataUnitID.LDU1.getMessageLength(), 48);
mPrimarySyncDetector.setListener(new ISyncDetectListener()
{
@Override
@@ -106,6 +113,12 @@ public class P25MessageFramer implements Listener<Dibit>
}
}
}
@Override
public void syncLost()
{
//no-op
}
});
mMatcher.add(mPrimarySyncDetector);
@@ -122,25 +135,41 @@ public class P25MessageFramer implements Listener<Dibit>
mAssemblers.add(new P25MessageAssembler());
}
public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop)
public P25MessageFramer(AliasList aliasList, IPhaseLockedLoop phaseLockedLoop, ISyncDetectListener syncDetectListener)
{
this(aliasList);
this(aliasList, syncDetectListener);
if(phaseLockedLoop != null)
{
//Add additional sync pattern detectors to detect when we get 90/180 degree out of phase sync pattern
//detections so that we can apply correction to the phase locked loop
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
phaseLockedLoop, PHASE_CORRECTION_90_DEGREES));
mInversionDetector90CW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_90_DEGREES);
mMatcher.add(mInversionDetector90CW);
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CW,
phaseLockedLoop, -PHASE_CORRECTION_90_DEGREES));
mInversionDetector90CCW = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_90_CCW,
phaseLockedLoop, DEFAULT_SAMPLE_RATE, -FREQUENCY_PHASE_CORRECTION_90_DEGREES);
mMatcher.add(mInversionDetector90CCW);
mMatcher.add(new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
phaseLockedLoop, PHASE_CORRECTION_180_DEGREES));
mInversionDetector180 = new PLLPhaseInversionDetector(FrameSync.P25_PHASE1_ERROR_180,
phaseLockedLoop, DEFAULT_SAMPLE_RATE, FREQUENCY_PHASE_CORRECTION_180_DEGREES);
mMatcher.add(mInversionDetector180);
}
}
/**
* Updates the incoming sample stream sample rate to allow the PLL phase inversion detectors to
* recalculate their internal phase correction values.
*
* @param sampleRate of the incoming sample stream
*/
public void setSampleRate(double sampleRate)
{
mInversionDetector180.setSampleRate(sampleRate);
mInversionDetector90CW.setSampleRate(sampleRate);
mInversionDetector90CCW.setSampleRate(sampleRate);
}
public void dispose()
{
for(P25MessageAssembler a : mAssemblers)
@@ -671,34 +700,46 @@ public class P25MessageFramer implements Listener<Dibit>
}
/**
* Sync pattern detector to listen for costas loop phase lock errors and
* apply a phase correction to the costas loop so that we don't miss any
* messages.
* Sync pattern detector to listen for costas loop phase lock errors and apply a phase correction to the costas
* loop so that we don't miss any messages.
*
* When the costas loop locks with a +/- 90 degree or 180 degree phase
* error, the slicer will incorrectly apply the symbol pattern rotated left
* or right by the phase error. However, we can detect these rotated sync
* patterns and apply immediate phase correction so that message processing
* can continue.
* When the costas loop locks with a +/- 90 degree or 180 degree phase error, the slicer will incorrectly apply
* the symbol pattern rotated left or right by the phase error. However, we can detect these rotated sync patterns
* and apply immediate phase correction so that message processing can continue.
*/
public class PLLPhaseInversionDetector extends SyncDetector
{
private FrameSync mFrameSync;
private IPhaseLockedLoop mPhaseLockedLoop;
private double mCorrection;
private double mSampleRate;
private double mFrequencyCorrection;
private double mPllCorrection;
public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double correction)
/**
* Constructs the PLL phase inversion detector.
*
* @param frameSync pattern to monitor for detecting phase inversion errors
* @param phaseLockedLoop to receive phase correction values
* @param sampleRate of the incoming sample stream
* @param frequencyCorrection to apply to the PLL. Examples:
* QPSK +/-90 degree correction: +/-SYMBOL RATE / 4.0
* QPSK 180 degree correction: SYMBOL RATE / 2.0
*/
public PLLPhaseInversionDetector(FrameSync frameSync, IPhaseLockedLoop phaseLockedLoop, double sampleRate,
double frequencyCorrection)
{
super(frameSync.getSync());
mFrameSync = frameSync;
mPhaseLockedLoop = phaseLockedLoop;
mCorrection = correction;
mFrequencyCorrection = frequencyCorrection;
setSampleRate(sampleRate);
setListener(new ISyncDetectListener()
{
@Override
public void syncDetected()
{
mPhaseLockedLoop.correctInversion(mCorrection);
mPhaseLockedLoop.correctInversion(mPllCorrection);
/* Since we detected a sync pattern, start a message assembler */
for(P25MessageAssembler assembler : mAssemblers)
@@ -710,7 +751,24 @@ public class P25MessageFramer implements Listener<Dibit>
}
}
}
@Override
public void syncLost()
{
//no-op
}
});
}
/**
* Sets or adjusts the sample rate so that the phase inversion correction value can be recalculated.
* @param sampleRate
*/
public void setSampleRate(double sampleRate)
{
mSampleRate = sampleRate;
mPllCorrection = 2.0 * Math.PI * mFrequencyCorrection / mSampleRate;
}
}
}