mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
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:
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user