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

@@ -556,6 +556,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//ignore
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
mSourceEventBroadcaster.broadcast(sourceEvent);
break;
default:
mLog.error("Received unrecognized source event from polyphase channel source [" +
sourceEvent.getEvent() + "]");

View File

@@ -1,171 +0,0 @@
/*******************************************************************************
* 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.filter.channelizer;
import java.util.Arrays;
public class PolyphaseChannelResultsBuffer
{
private long mTimestamp;
private int mPointer = 0;
private int mMaxSize;
private float[][] mChannelResults;
/**
* Buffer containing an array of polyphase channelizer complex channel results.
* @param timestamp reference relative to the first channel results.
* @param maxSize of channel results to store in this buffer
*/
public PolyphaseChannelResultsBuffer(long timestamp, int maxSize)
{
mTimestamp = timestamp;
mMaxSize = maxSize;
}
/**
* Reference timestamp relative to the first channel results contained in this buffer.
* @return reference timestamp in milliseconds since epoch
*/
public long getTimestamp()
{
return mTimestamp;
}
/**
* Indicates if this buffer is full
*/
public boolean isFull()
{
return mPointer >= mMaxSize;
}
/**
* Indicates if this buffer is empty
*/
public boolean isEmpty()
{
return mPointer == 0;
}
/**
* Adds the channel results to this buffer
* @param channelResults to add
* @throws IllegalArgumentException if the buffer is full or if the size of the channel results array is a different
* length than any of the preceeding channel results that were added to this buffer
*/
public void add(float[] channelResults) throws IllegalArgumentException
{
if(mPointer >= mMaxSize)
{
throw new IllegalArgumentException("Buffer is full - cannot add new channel results");
}
if(mPointer == 0)
{
mChannelResults = new float[mMaxSize][channelResults.length];
}
else
{
int currentLength = mChannelResults[0].length;
if(channelResults.length != currentLength)
{
throw new IllegalArgumentException("Channel results length [" + channelResults.length + "] cannot be " +
"a different length than the channel results that are already contained in this buffer [" +
currentLength + "]");
}
}
mChannelResults[mPointer++] = channelResults;
}
/**
* Channel results array contained in this buffer
*/
public float[][] getChannelResults()
{
//If this buffer is full, return all of the channel results
if(mPointer == mMaxSize)
{
return mChannelResults;
}
//Otherwise, truncate the results to return
else
{
return Arrays.copyOf(mChannelResults, mPointer);
}
}
/**
* Extracts a single I/Q interleaved sample buffer from the channel results
* @param iChannelIndex to extract for the inphase sample. The q index is assumed to be one greater than i.
* @return float[] containing interleaved I and Q samples
* @throws IllegalArgumentException if this buffer doesn't contain any samples or if the requested channel is
* not within the channel range of the contained channel results.
*/
public float[] getChannel(int iChannelIndex)
{
if(isEmpty())
{
throw new IllegalArgumentException("Buffer is empty - cannot extract channel [" + iChannelIndex + "]");
}
if(!isValidChannelIndex(iChannelIndex))
{
throw new IllegalArgumentException("Channel [" + iChannelIndex + "] is not valid for the contained channel " +
"results -- max channel is " + getMaxChannelIndex());
}
float[] samples = new float[mChannelResults.length * 2];
int pointer = 0;
int qChannelIndex = iChannelIndex + 1;
for(int x = 0; x < mPointer; x++)
{
float[] channelResults = mChannelResults[x];
samples[pointer++] = channelResults[iChannelIndex];
samples[pointer++] = channelResults[qChannelIndex];
}
return samples;
}
/**
* Indicates if the channel argument is valid for the contained channel results.
* @param channelIndex number (0 to N) to check
* @return false if the buffer is empty or if the channel is greater than the contained channel results
*/
private boolean isValidChannelIndex(int channelIndex)
{
return channelIndex <= getMaxChannelIndex();
}
/**
* Maximum channel index represented in the channel results contained in this buffer.
* @return max channel number, or 0 if the buffer is empty
*/
private int getMaxChannelIndex()
{
if(isEmpty())
{
return 0;
}
return mChannelResults[0].length;
}
}

View File

@@ -237,4 +237,11 @@ public class PolyphaseChannelSource extends TunerChannelSource
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}
@Override
public String toString()
{
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
getTunerChannel().getFrequency();
}
}

View File

@@ -62,6 +62,12 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
}
@Override
public int getPolyphaseChannelIndexCount()
{
return mInputChannelCount;
}
public void dispose()
{
if(mChannelResultsQueue != null)

View File

@@ -59,6 +59,11 @@ public interface IPolyphaseChannelOutputProcessor
*/
void setPolyphaseChannelIndices(List<Integer> indexes);
/**
* List of current polyphase channel indices for the output processor
*/
int getPolyphaseChannelIndexCount();
/**
* Updates the synthesis filter taps for this output processor
* @param filter

View File

@@ -134,6 +134,12 @@ public class LTRDecoder implements Listener<ReusableBuffer>, ISyncDetectListener
mSynchronizationMonitor.syncDetected();
}
@Override
public void syncLost()
{
//no-op
}
/**
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is

View File

@@ -30,13 +30,13 @@ import org.slf4j.LoggerFactory;
*
* Synchronization States:
*
* -COARSE - default state. demoted from MEDIUM after one message period with no sync detection.
* -COARSE - default state. demoted from MEDIUM after one message period with no sync detection.
*
* -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE
* after two message periods with no sync detections.
* -MEDIUM - promoted from COARSE when one sync detection occurs after two message periods, or demoted from FINE
* after two message periods with no sync detections.
*
* -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more
* message periods.
* -FINE - promoted from MEDIUM after two or more consecutive sync detections within the preceding two or more
* message periods.
*/
public class SynchronizationMonitor implements ISyncDetectListener
{
@@ -50,6 +50,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
/**
* Constructs the synchronization monitor for the specified message symbol length.
*
* @param messageLength in symbols.
*/
public SynchronizationMonitor(int messageLength)
@@ -127,6 +128,12 @@ public class SynchronizationMonitor implements ISyncDetectListener
updateSyncState();
}
@Override
public void syncLost()
{
//no-op
}
/**
* Increments the symbol count for each decoded symbol.
*/

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* 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.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.module.decode.p25.P25Decoder;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Monitors decode sync events to adaptively control PLL loop gain.
*/
public class AdaptivePLLGainMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLGainMonitor.class);
private static final int MAX_SYNC_COUNT = 6;
private CostasLoop mCostasLoop;
private P25Decoder mP25Decoder;
private PLLGain mPLLGain = PLLGain.LEVEL_1;
private int mSyncCount;
/**
* Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
* level of the costas loop accordingly.
*
* @param costasLoop to receive adaptive gain updates.
* @param p25Decoder to receive frequency error source events
*/
public AdaptivePLLGainMonitor(CostasLoop costasLoop, P25Decoder p25Decoder)
{
mCostasLoop = costasLoop;
mCostasLoop.setPLLGain(mPLLGain);
mCostasLoop.setFrequencyErrorProcessor(this);
mP25Decoder = p25Decoder;
}
/**
* Sync detection event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncDetected()
{
mSyncCount++;
update();
}
/**
* Sync loss event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncLost()
{
mSyncCount -= 2;
update();
}
/**
* Updates the gain state for a sync detect or sync loss
*/
private void update()
{
if(mSyncCount < 0)
{
mSyncCount = 0;
return;
}
if(mSyncCount > MAX_SYNC_COUNT)
{
mSyncCount = MAX_SYNC_COUNT;
return;
}
PLLGain gain = PLLGain.fromSyncCount(mSyncCount);
//Only update the costas loop if the gain value changes
if(gain != mPLLGain)
{
mPLLGain = gain;
mCostasLoop.setPLLGain(mPLLGain);
}
}
/**
* Resets the monitor and commands the PLL to use the highest gain level.
*/
public void reset()
{
mSyncCount = 0;
mPLLGain = PLLGain.LEVEL_1;
mCostasLoop.setPLLGain(mPLLGain);
}
@Override
public void processFrequencyError(long frequencyError)
{
//Only rebroadcast the costas loop frequency error if the gain level has progressed above the
//default gain of 1, meaning that we have received at least 2 sync events
if(mPLLGain != PLLGain.LEVEL_1)
{
mP25Decoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
}
}
}

View File

@@ -41,10 +41,12 @@ public class CostasLoop implements IPhaseLockedLoop
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private Tracking mTracking = Tracking.FASTEST;
private PLLGain mPLLGain = PLLGain.LEVEL_1;
private double mSymbolRate;
private double mSampleRate;
private int mCounter;
private IFrequencyErrorProcessor mFrequencyErrorProcessor;
private long mFrequencyError;
private int mBaudCounter;
/**
* Costas Loop for tracking and correcting frequency error in a received carrier signal.
@@ -60,6 +62,17 @@ public class CostasLoop implements IPhaseLockedLoop
updateLoopBandwidth();
}
/**
* Registers the listener to receive the measured frequency error as tracked by the PLL at one
* second intervals.
*
* @param listener to receive frequency error measurement.
*/
public void setFrequencyErrorProcessor(IFrequencyErrorProcessor listener)
{
mFrequencyErrorProcessor = listener;
}
/**
* 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.
@@ -89,21 +102,21 @@ public class CostasLoop implements IPhaseLockedLoop
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mTracking.getLoopBandwidth();
double bandwidth = TWO_PI / mPLLGain.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
* Sets the PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param tracking
* @param PLLGain
*/
public void setTracking(Tracking tracking)
public void setPLLGain(PLLGain PLLGain)
{
mTracking = tracking;
mPLLGain = PLLGain;
updateLoopBandwidth();
}
@@ -159,7 +172,7 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopFrequency += (mBetaGain * phaseError);
mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError);
/* Normalize phase between +/- 2 * PI */
//Normalize phase between +/- 2 * PI
if(mLoopPhase > TWO_PI)
{
mLoopPhase -= TWO_PI;
@@ -170,7 +183,7 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopPhase += TWO_PI;
}
/* Limit frequency to +/- maximum loop frequency */
//Limit frequency to +/- maximum loop frequency
if(mLoopFrequency > mMaximumLoopFrequency)
{
mLoopFrequency = mMaximumLoopFrequency;
@@ -181,17 +194,25 @@ public class CostasLoop implements IPhaseLockedLoop
mLoopFrequency = -mMaximumLoopFrequency;
}
//TODO: broadcast channel offset to the tuner to use in auto-adjusting tuner PPM
// mCounter++;
//
// if(mCounter > (5 * mSymbolRate))
// {
// mCounter = 0;
// mLog.info("Current Costas Loop Frequency: " + mLoopFrequency + " [" +
// (int)(mSampleRate / TWO_PI * mLoopFrequency) + " Hz Offset] sample rate [" + mSampleRate + "]");
// }
//Broadcast current frequency error measurement once a second to an external listener
mBaudCounter++;
if(mBaudCounter > mSymbolRate)
{
mBaudCounter = 0;
mFrequencyError = (long)(mSampleRate / TWO_PI * mLoopFrequency);
if(mFrequencyErrorProcessor != null)
{
mFrequencyErrorProcessor.processFrequencyError(mFrequencyError);
}
}
}
/**
* Resets the PLL internal tracking values
*/
@Override
public void reset()
{

View File

@@ -16,27 +16,14 @@
package io.github.dsheirer.dsp.psk.pll;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
* Interface for passing frequency error measurements.
*/
public enum Tracking
public interface IFrequencyErrorProcessor
{
FASTEST(50.0),
FAST(100.0),
NORMAL(200.0),
SLOW(400.0);
private double mLoopBandwidth;
Tracking(double loopBandwidth)
{
mLoopBandwidth = loopBandwidth;
}
/**
* Loop bandwidth setting
* Provides a frequency error measurement to the listener.
*
* @param error value as measured by the provider
*/
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
void processFrequencyError(long error);
}

View File

@@ -0,0 +1,87 @@
/*******************************************************************************
* 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 PLLGain
{
LEVEL_1(50.0, 0, 1),
LEVEL_2(75.0, 2, 4),
LEVEL_3(100.0, 5, 6),
LEVEL_4(125.0, 7, 8),
LEVEL_5(150.0, 9, 10);
private double mLoopBandwidth;
private int mRangeStart;
private int mRangeEnd;
PLLGain(double loopBandwidth, int start, int end)
{
mLoopBandwidth = loopBandwidth;
mRangeStart = start;
mRangeEnd = end;
}
/**
* Loop bandwidth setting
*/
public double getLoopBandwidth()
{
return mLoopBandwidth;
}
public int getRangeStart()
{
return mRangeStart;
}
public int getRangeEnd()
{
return mRangeEnd;
}
/**
* Look up PLL gain from a running sync detection count.
*
* @param syncCount 0-10
* @return PLL gain appropriate for the sync detection value
*/
public static PLLGain fromSyncCount(int syncCount)
{
if(syncCount <= LEVEL_1.getRangeEnd())
{
return LEVEL_1;
}
else if(syncCount <= LEVEL_2.getRangeEnd())
{
return LEVEL_2;
}
else if(syncCount <= LEVEL_3.getRangeEnd())
{
return LEVEL_3;
}
else if(syncCount <= LEVEL_4.getRangeEnd())
{
return LEVEL_4;
}
else
{
return LEVEL_5;
}
}
}

View File

@@ -15,7 +15,13 @@
******************************************************************************/
package io.github.dsheirer.dsp.symbol;
/**
* Listener interface to be notified each time a sync pattern has been detected and/or when the sync
* has been lost.
*/
public interface ISyncDetectListener
{
void syncDetected();
void syncDetected();
void syncLost();
}