Resolves #663. Updates P25 phase 2 Decoder to detect PLL lock errors for +/-90 and 180 out-of-phase. Adds support for broadcasting phase 2 channel frequency error calculations to support tuner auto-correction for frequency errors. Removes adaptive PLL bandwidth support from Phase 1 and uses a fixed PLL bandwidth for both phase 1 and phase 2 decoders. Updates the Phase 2 symbol timing gain with results from extensive testing of PLL bandwidth and symbol timing combinations. (#666)

This commit is contained in:
Denny Sheirer
2020-02-22 20:09:24 +00:00
committed by GitHub
parent 44237bec5c
commit b3b2e74688
182 changed files with 1430 additions and 996 deletions

View File

@@ -0,0 +1,125 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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.FeedbackDecoder;
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 AdaptivePLLBandwidthMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLBandwidthMonitor.class);
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
private CostasLoop mCostasLoop;
private FeedbackDecoder mFeedbackDecoder;
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
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 feedbackDecoder to rebroadcast frequency error source events
*/
public AdaptivePLLBandwidthMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
{
mCostasLoop = costasLoop;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
mCostasLoop.setFrequencyErrorProcessor(this);
mFeedbackDecoder = feedbackDecoder;
}
/**
* Sync detection event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncDetected(int bitErrors)
{
mSyncCount++;
update();
}
/**
* Sync loss event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncLost(int bitsProcessed)
{
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;
}
PLLBandwidth gain = PLLBandwidth.fromSyncCount(mSyncCount);
//Only update the costas loop if the gain value changes
if(gain != mPLLBandwidth)
{
mPLLBandwidth = gain;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
}
}
/**
* Resets the monitor and commands the PLL to use the highest gain level.
*/
public void reset()
{
mSyncCount = 0;
mPLLBandwidth = PLLBandwidth.BW_400;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
}
@Override
public void processFrequencyError(long frequencyError)
{
//Only rebroadcast the costas loop frequency error if the gain level has progressed above
//a bandwidth of 300, meaning that we have received at least 4 sync events
if(mPLLBandwidth != PLLBandwidth.BW_400 && mPLLBandwidth != PLLBandwidth.BW_300)
{
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
}
}
}

View File

@@ -1,123 +0,0 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 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.phase1.P25P1Decoder;
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 P25P1Decoder mP25P1Decoder;
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 p25P1Decoder to receive frequency error source events
*/
public AdaptivePLLGainMonitor(CostasLoop costasLoop, P25P1Decoder p25P1Decoder)
{
mCostasLoop = costasLoop;
mCostasLoop.setPLLGain(mPLLGain);
mCostasLoop.setFrequencyErrorProcessor(this);
mP25P1Decoder = p25P1Decoder;
}
/**
* Sync detection event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncDetected(int bitErrors)
{
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)
{
mP25P1Decoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
}
}
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* 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.
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
*
* 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;
@@ -41,7 +47,7 @@ public class CostasLoop implements IPhaseLockedLoop
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private PLLGain mPLLGain = PLLGain.LEVEL_1;
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
private double mSymbolRate;
private double mSampleRate;
private IFrequencyErrorProcessor mFrequencyErrorProcessor;
@@ -63,8 +69,7 @@ public class CostasLoop implements IPhaseLockedLoop
}
/**
* Registers the listener to receive the measured frequency error as tracked by the PLL at one
* second intervals.
* Registers the listener to receive the measured frequency error as tracked by the PLL once per second
*
* @param listener to receive frequency error measurement.
*/
@@ -102,22 +107,25 @@ public class CostasLoop implements IPhaseLockedLoop
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mPLLGain.getLoopBandwidth();
double bandwidth = TWO_PI / mPLLBandwidth.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 PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta
* Sets the PLLBandwidth state for this costas loop. The bandwidth affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param PLLGain
* @param pllBandwidth
*/
public void setPLLGain(PLLGain PLLGain)
public void setPLLBandwidth(PLLBandwidth pllBandwidth)
{
mPLLGain = PLLGain;
updateLoopBandwidth();
if(mPLLBandwidth != pllBandwidth)
{
mPLLBandwidth = pllBandwidth;
updateLoopBandwidth();
}
}
/**
@@ -162,8 +170,7 @@ public class CostasLoop implements IPhaseLockedLoop
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
* Updates the costas loop frequency and phase to adjust for the phase error value
*
* @param phaseError - (-)= late and (+)= early
*/

View File

@@ -0,0 +1,110 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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.FeedbackDecoder;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Monitors decode sync events to adaptively control frequency correction broadcasts.
*/
public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(FrequencyCorrectionSyncMonitor.class);
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
private FeedbackDecoder mFeedbackDecoder;
private CostasLoop mCostasLoop;
private int mSyncCount;
/**
* Constructs an adaptive monitor to monitor the sync state of a decoder
*
* @param costasLoop to receive adaptive gain updates.
* @param feedbackDecoder to rebroadcast frequency error source events
*/
public FrequencyCorrectionSyncMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
{
mCostasLoop = costasLoop;
mCostasLoop.setFrequencyErrorProcessor(this);
mFeedbackDecoder = feedbackDecoder;
}
/**
* Sync detection event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncDetected(int bitErrors)
{
mSyncCount++;
update();
}
/**
* Sync loss event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncLost(int bitsProcessed)
{
mSyncCount -= 2;
update();
}
/**
* Updates the sync count
*/
private void update()
{
if(mSyncCount < 0)
{
mSyncCount = 0;
return;
}
if(mSyncCount > MAX_SYNC_COUNT)
{
mSyncCount = MAX_SYNC_COUNT;
return;
}
}
/**
* Resets the monitor
*/
public void reset()
{
mSyncCount = 0;
}
@Override
public void processFrequencyError(long frequencyError)
{
//Only rebroadcast the frequency error if the sync count is more than 2
if(mSyncCount > 2)
{
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
}
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 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
@@ -21,37 +21,45 @@
*/
package io.github.dsheirer.dsp.psk.pll;
import java.util.Map;
import java.util.TreeMap;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
*/
public enum PLLGain
public enum PLLBandwidth
{
//NOTE: static gain level of 200 produced the best results for releases prior to 0.3.4b2
//NOTE: static bandwidth of 200 produced the best results for P25P1 releases prior to 0.3.4b2
LEVEL_0(100.0, 0, 0),
LEVEL_1(150.0, 0, 1),
LEVEL_2(170.0, 2, 4),
LEVEL_3(190.0, 5, 6),
LEVEL_4(200.0, 7, 8),
LEVEL_5(200.0, 9, 10),
LEVEL_6(250.0, 11, 12),
LEVEL_7(275.0, 12,13),
LEVEL_8(325.0, 14, 15),
LEVEL_9(350.0, 16,17),
LEVEL_10(375.0, 18, 19),
LEVEL_11(400.0, 19,20);
BW_400(400.0, 0, 1),
BW_300(300.0, 2, 3),
BW_250(250.0, 4, 5),
BW_200(200.0, 6, 7);
private double mLoopBandwidth;
private int mRangeStart;
private int mRangeEnd;
PLLGain(double loopBandwidth, int start, int end)
PLLBandwidth(double loopBandwidth, int start, int end)
{
mLoopBandwidth = loopBandwidth;
mRangeStart = start;
mRangeEnd = end;
}
private static Map<Integer,PLLBandwidth> LOOKUP_MAP = new TreeMap<>();
static
{
for(PLLBandwidth pllBandwidth : PLLBandwidth.values())
{
for(int x = pllBandwidth.getRangeStart(); x <= pllBandwidth.getRangeEnd(); x++)
{
LOOKUP_MAP.put(x, pllBandwidth);
}
}
}
/**
* Loop bandwidth setting
*/
@@ -76,27 +84,13 @@ public enum PLLGain
* @param syncCount 0-10
* @return PLL gain appropriate for the sync detection value
*/
public static PLLGain fromSyncCount(int syncCount)
public static PLLBandwidth fromSyncCount(int syncCount)
{
if(syncCount <= LEVEL_1.getRangeEnd())
if(LOOKUP_MAP.containsKey(syncCount))
{
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;
return LOOKUP_MAP.get(syncCount);
}
return BW_200;
}
}

View File

@@ -22,20 +22,26 @@
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.dsp.symbol.Dibit;
/**
* Utility for calculating phase inverted sync pattern derivatives for QPSK constellations
*/
public class QPSKPhaseInversionCalculator
{
public static String[] LOOKUP_MINUS_90 = {"A","8","B","9","2","0","3","1","E","C","F","D","6","4","7","5"};
public static String[] LOOKUP_PLUS_90 = {"5","7","4","6","D","F","C","E","1","3","0","2","9","B","8","A"};
public static String[] LOOKUP_180 = {"F","E","D","C","B","A","9","8","7","6","5","4","3","2","1","0"};
public static void main(String[] args)
{
//DMR sync patterns
String[] syncs = {"755FD7DF75F7", "DFF57D75DF5D", "7F7D5DD57DFD", "D5D7F77FD757", "77D55F7DFD77",
"5D577F7757FF", "F7FDD5DDFD55", "7DFFD5F55D5F", "D7557F5FF7F5", "DD7FF5D757DD"};
// String[] syncs = {"755FD7DF75F7", "DFF57D75DF5D", "7F7D5DD57DFD", "D5D7F77FD757", "77D55F7DFD77",
// "5D577F7757FF", "F7FDD5DDFD55", "7DFFD5F55D5F", "D7557F5FF7F5", "DD7FF5D757DD"};
//P25 Phase 2 sync pattern
String[] syncs = {"575D57F7FF"};
//P25 Phase 1 sync pattern
// String[] syncs = {"5575F5FF77FF"};
for(String sync: syncs)
{
@@ -56,10 +62,11 @@ public class QPSKPhaseInversionCalculator
{
value = letter - 48;
}
sbNorm.append(letter);
sbP90.append(LOOKUP_PLUS_90[value]);
sbM90.append(LOOKUP_MINUS_90[value]);
sb180.append(LOOKUP_180[value]);
sbP90.append(rotate(value, Dibit.Rotation.PLUS90));
sbM90.append(rotate(value, Dibit.Rotation.MINUS90));
sb180.append(rotate(value, Dibit.Rotation.INVERT));
}
System.out.println("NORMAL: " + sync);
@@ -68,4 +75,59 @@ public class QPSKPhaseInversionCalculator
System.out.println(" 180: " + sb180.toString());
}
}
/**
* Converts the hexadecimal value to an integer, decomposes it into high and low order dibits, rotates the dibits
* in the specified direction, and recombines the rotated dibits into a 4-bit value, returned as a hex character.
* @param value to rotate
* @param direction direction
* @return rotated hex character
*/
public static String rotate(int value, Dibit.Rotation direction)
{
try
{
Dibit dibitHigh = getHighDibit(value);
Dibit dibitLow = getLowDibit(value);
return getHex(dibitHigh.rotate(direction), dibitLow.rotate(direction));
}
catch(Exception e)
{
//ignore
}
return "-";
}
/**
* Combines the bit values from the two dibits and converts the integer value to a hex character
* @param d1 containing the high order bits
* @param d2 containing the low order bits
* @return string hex character representing the value of these dibits combined
*/
public static String getHex(Dibit d1, Dibit d2)
{
int value = d1.getHighValue() + d2.getLowValue();
return Integer.toHexString(value).toUpperCase();
}
/**
* Calculates the dibit that represents the bits in the 0x8 and 0x4 bit positions of the value
* @param value
* @return a dibit
*/
public static Dibit getHighDibit(int value)
{
return Dibit.fromValue((value >> 2) & 3);
}
/**
* Calculates the dibit that represents the bits in the 0x2 and 0x1 bit positions of the value
* @param value
* @return a dibit
*/
public static Dibit getLowDibit(int value)
{
return Dibit.fromValue(value & 3);
}
}