P25 LSM work in progress. CQPSK demodulator is working now, but there is a slight phase error in the constellation (it's rotated right by 5-10 degrees). I'm not sure if this is an error coming from the decoder, or if it's reflecting a phase imbalance from the source tuner i/q data.

This commit is contained in:
dennis.sheirer
2015-02-16 23:14:23 +00:00
parent 78163caf4f
commit 7d948c4af4
6 changed files with 322 additions and 281 deletions

View File

@@ -39,24 +39,24 @@ public class CQPSKSlicer implements Listener<ComplexSample>
if( sample.real() > 0 )
{
if( sample.imaginary() > 0 )
{
mBroadcaster.broadcast( Dibit.D00_PLUS_1 );
}
else
{
mBroadcaster.broadcast( Dibit.D10_MINUS_1 );
}
}
else
{
if( sample.imaginary() > 0 )
{
mBroadcaster.broadcast( Dibit.D01_PLUS_3 );
}
else
{
mBroadcaster.broadcast( Dibit.D11_MINUS_3 );
}
}
else
{
if( sample.imaginary() > 0 )
{
mBroadcaster.broadcast( Dibit.D00_PLUS_1 );
}
else
{
mBroadcaster.broadcast( Dibit.D01_PLUS_3 );
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* 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
@@ -77,7 +77,6 @@ public class P25Decoder extends Decoder
/* Filters */
private ComplexFIRFilter mBasebandFilter;
private ComplexFIRFilter mRootRaisedCosineFilter;
private FloatFIRFilter mAudioFilter;
private C4FMSymbolFilter mSymbolFilter;
@@ -118,7 +117,7 @@ public class P25Decoder extends Decoder
if( mSourceSampleType == SampleType.COMPLEX )
{
mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass(
48000, 6500, 8000, 48, WindowType.HANNING, true ), 1.0 );
48000, 6500, 7500, 48, WindowType.HANNING, true ), 4.0 );
if( modulation == Modulation.CQPSK )
{
@@ -364,14 +363,16 @@ public class P25Decoder extends Decoder
public enum Modulation
{
C4FM( "C4FM" ),
CQPSK( "LSM SIMULCAST" );
C4FM( "C4FM", "C4FM" ),
CQPSK( "LSM SIMULCAST", "LSM" );
private String mLabel;
private String mShortLabel;
private Modulation( String label )
private Modulation( String label, String shortLabel )
{
mLabel = label;
mShortLabel = shortLabel;
}
public String getLabel()
@@ -379,6 +380,11 @@ public class P25Decoder extends Decoder
return mLabel;
}
public String getShortLabel()
{
return mShortLabel;
}
public String toString()
{
return getLabel();

View File

@@ -26,6 +26,7 @@ import settings.Setting;
import controller.channel.Channel;
import controller.state.ChannelState.ChangedAttribute;
import controller.state.ChannelStatePanel;
import decode.config.DecodeConfigP25Phase1;
public class P25Panel extends ChannelStatePanel
{
@@ -35,7 +36,7 @@ public class P25Panel extends ChannelStatePanel
private JLabel mSourceLabel;
private JLabel mChannelLabel;
private JLabel mProtocol = new JLabel( "P25" );
private JLabel mProtocol;
private JLabel mFrom = new JLabel( " " );
private JLabel mFromAlias = new JLabel( " " );
@@ -51,6 +52,12 @@ public class P25Panel extends ChannelStatePanel
public P25Panel( Channel channel )
{
super( channel );
DecodeConfigP25Phase1 p25Config =
(DecodeConfigP25Phase1)channel.getDecodeConfiguration();
mProtocol = new JLabel( "P25-1 " +
p25Config.getModulation().getShortLabel() );
init();
}

View File

@@ -2,8 +2,20 @@ package dsp.psk;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
* Copyright (C) 2014, 2015 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
@@ -26,153 +38,38 @@ import sample.Provider;
import sample.complex.ComplexSample;
import dsp.filter.interpolator.QPSKInterpolator;
/**
* Implements a CQPSK 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 CQPSKDemodulator implements Listener<ComplexSample>,
Provider<ComplexSample>
{
private final static Logger mLog =
LoggerFactory.getLogger( CQPSKDemodulator.class );
/* Sample Rate 48000 / Symbol Rate 4800 = Samples Per Symbol 10 */
public static final int SAMPLES_PER_SYMBOL = 10;
public static final int d_twice_sps = 20;
public static final int half_sps = 5;
public static final double TWO_PI = 2.0 * Math.PI;
/* CQPSK constellation is offset from normal QPSK by 45 degrees or pi/4 */
public static final float THETA = (float)( Math.PI / 4.0d );
private float d_mu = 10.5f;
private float d_gain_mu = 0.05f;
/* samples per symbol */
private float d_omega = 10.0f;
private float d_half_omega = 5.0f;
private float d_gain_omega = 0.1f * d_gain_mu * d_gain_mu;
private float d_omega_rel = 0.005f;
private float d_min_omega = d_omega * ( 1.0f - d_omega_rel );
private float d_max_omega = d_omega * ( 1.0f + d_omega_rel );
private float d_omega_mid = 10.0f;
private float d_phase = 0.0f;
private float d_freq = 0.0f;
private ComplexSample[] d_dl = new ComplexSample[ 40 ];
private int d_dl_index = 0;
private Listener<ComplexSample> mListener;
private ComplexSample[] mConstellationPoints;
private SymbolTracker mSymbolTracker = new SymbolTracker();
private QPSKPhaseErrorTracker mPhaseTracker = new QPSKPhaseErrorTracker();
private QPSKInterpolator mInterpolator = new QPSKInterpolator();
private ComplexSample d_last_sample = new ComplexSample( 0.0f, 0.0f );
private GardnerDetector mGardnerDetector = new GardnerDetector();
public CQPSKDemodulator()
{
createConstellationPoints();
/* prefill the delay line */
for( int x = 0; x < 40; x++ )
{
d_dl[ x ] = new ComplexSample( 0.0f, 0.0f );
}
}
private void createConstellationPoints()
{
mConstellationPoints = new ComplexSample[ 4 ];
for( int x = 0; x < 4; x++ )
{
mConstellationPoints[ x ] = ComplexSample.fromAngle(
( (float)( TWO_PI / 4.0f ) * (float)x ) );
}
}
@Override
public void receive( ComplexSample sample )
{
/* skip a number of symbols between sampling */
d_mu--;
/* increment phase based on the frequency of the rotation */
d_phase += d_freq;
/* keep phase clamped and not walk to infinity */
unwrapPhase();
/* NCO value for derotating the current sample */
ComplexSample nco = ComplexSample.fromAngle( d_phase + THETA );
/* Mixed/downconverted symbol */
ComplexSample symbol = ComplexSample.multiply( nco, sample );
/* Fill up the delay line for the interpolator */
d_dl[ d_dl_index ] = symbol;
d_dl[ d_dl_index + d_twice_sps ] = symbol;
/* Keep the delay line index in bounds */
d_dl_index = ( d_dl_index + 1 ) % d_twice_sps;
if( d_mu <= 1.0f )
{
ComplexSample interp_samp_mid = mInterpolator.filter( d_dl, d_dl_index, d_mu );
ComplexSample interp_samp = mInterpolator.filter( d_dl, d_dl_index + half_sps, d_mu );
float error_real = ( d_last_sample.real() - interp_samp.real() ) * interp_samp_mid.real();
float error_imag = ( d_last_sample.imaginary() - interp_samp.imaginary() ) * interp_samp_mid.imaginary();
ComplexSample diffdec = ComplexSample.multiply( interp_samp, d_last_sample.conjugate() );
d_last_sample = interp_samp;
float symbol_error = normalize( error_real + error_imag ); //Garner loop error
d_omega = d_omega + d_gain_omega * symbol_error * interp_samp.magnitude();
d_omega = d_omega_mid + clip( d_omega_mid, d_omega_rel );
d_mu += d_omega + d_gain_mu * symbol_error;
// mSymbolTracker.receive( interpolated );
mPhaseTracker.receive( diffdec );
if( mListener != null )
{
// mListener.receive( interp_samp );
mListener.receive( diffdec );
}
}
}
private float normalize( float symbol_error )
{
if( Float.isNaN( symbol_error ) )
{
return 0.0f;
}
if( symbol_error < -1.0 )
{
return -1.0f;
}
if( symbol_error > 1.0 )
{
return 1.0f;
}
return symbol_error;
mGardnerDetector.receive( sample );
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
private float clip( float value, float maximum )
public static float clip( float value, float maximum )
{
if( value > maximum )
{
@@ -185,186 +82,285 @@ public class CQPSKDemodulator implements Listener<ComplexSample>,
return value;
}
public static int getQPSKConstellationPoint( ComplexSample sample )
{
if( sample.real() >= 0.0f )
{
if( sample.imaginary() >= 0.0f )
{
return 0;
}
else
{
return 2;
}
}
else
{
if( sample.imaginary() >= 0.0f )
{
return 1;
}
else
{
return 3;
}
}
}
private void unwrapPhase()
{
while( d_phase > TWO_PI )
{
d_phase -= TWO_PI;
}
while( d_phase < -TWO_PI )
{
d_phase += TWO_PI;
}
}
/**
* Mueller & Mueller symbol timing tracker
* Gardner symbol timing detector
*/
public class SymbolTracker implements Listener<ComplexSample>
public class GardnerDetector implements Listener<ComplexSample>
{
ComplexSample mPoint2T = new ComplexSample();
ComplexSample mPoint1T = new ComplexSample();
ComplexSample mPoint0T = new ComplexSample();
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;
ComplexSample mConstellation2T = new ComplexSample();
ComplexSample mConstellation1T = new ComplexSample();
ComplexSample mConstellation0T = new ComplexSample();
private ComplexSample[] mDelayLine =
new ComplexSample[ 2 * TWICE_SAMPLES_PER_SYMBOL ];
private int mDelayLinePointer = 0;
/* Sampling point */
private float mMu = 10.5f;
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;
private CostasLoop mCostasLoop = new CostasLoop();
private QPSKInterpolator mInterpolator = new QPSKInterpolator();
private ComplexSample mPreviousSample = new ComplexSample( 0.0f, 0.0f );
public GardnerDetector()
{
for( int x = 0; x < ( 2 * TWICE_SAMPLES_PER_SYMBOL ); x++ )
{
mDelayLine[ x ] = new ComplexSample( 0.0f, 0.0f );
}
}
@Override
public void receive( ComplexSample sample )
{
mPoint2T = mPoint1T;
mPoint1T = mPoint0T;
mPoint0T = sample;
mConstellation2T = mConstellation1T;
mConstellation1T = mConstellation0T;
int currentConstellation = getQPSKConstellationPoint( mPoint0T );
mConstellation0T = mConstellationPoints[ currentConstellation ];
/* Count down samples per symbol until we calculate the symbol */
mMu--;
ComplexSample x = ComplexSample.multiply(
ComplexSample.subtract( mConstellation0T, mConstellation2T ),
mPoint1T.conjugate() );
ComplexSample y = ComplexSample.multiply(
ComplexSample.subtract( mPoint0T, mPoint2T ),
mConstellation1T.conjugate() );
mCostasLoop.increment();
ComplexSample u = ComplexSample.subtract( y, x );
/* Mix incoming sample with costas loop to remove any rotation */
ComplexSample unrotatedSample = ComplexSample
.multiply( mCostasLoop.getCurrentVector(), sample );
/* Real part of u contains error -- constrain to -1.0 to 1.0 */
float mm_error = clip( u.real(), 1.0f );
/* Fill up the delay line for the interpolator */
mDelayLine[ mDelayLinePointer ] = unrotatedSample;
mDelayLine[ mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL ] = unrotatedSample;
/* Keep the delay line pointer in bounds */
mDelayLinePointer = ( mDelayLinePointer + 1 ) % TWICE_SAMPLES_PER_SYMBOL;
/* Calculate the symbol once we've stored enough samples */
if( mMu <= 1.0f )
{
ComplexSample middleSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer, mMu );
middleSample.normalize();
ComplexSample currentSample = mInterpolator
.filter( mDelayLine, mDelayLinePointer +
HALF_SAMPLES_PER_SYMBOL, mMu );
/* Update omega (samples per symbol) based on loop error */
d_omega = d_omega + d_gain_omega * mm_error;
currentSample.normalize();
/* Gardner timing error calculations */
float errorReal = ( mPreviousSample.real() -
currentSample.real() ) * middleSample.real();
float errorImaginary = ( mPreviousSample.imaginary() -
currentSample.imaginary() ) * middleSample.imaginary();
float gardnerError = normalize( errorReal + errorImaginary );
mOmega = mOmega + mGainOmega * gardnerError;
mOmega = mOmegaMid + clip( mOmegaMid, mOmegaRel );
mMu += mOmega + mGainMu * gardnerError;
/* Calculate symbol as delta between previous and current samples */
ComplexSample symbol = ComplexSample.multiply(
currentSample, mPreviousSample.conjugate() );
mPreviousSample = currentSample;
/* Update costas loop phase error from error in current symbol */
mCostasLoop.receive( symbol );
/* Dispatch the symbol to the registered listener */
if( mListener != null )
{
mListener.receive( symbol );
}
}
}
/**
* Constrains timing error to +/- 1
*/
private float normalize( float error )
{
if( Float.isNaN( error ) )
{
return 0.0f;
}
/* Make sure we don't walk away */
d_omega = d_omega_mid + clip( d_omega - d_omega_mid, d_omega_rel );
if( error < -1.0 )
{
return -1.0f;
}
/* update mu based on loop error */
d_mu += d_omega + d_gain_mu * mm_error;
if( error > 1.0 )
{
return 1.0f;
}
return error;
}
}
/**
* Implements a control loop for QPSK
* Costas Loop - phase locked loop synchronized to the incoming signal.
*
* Ported to java from gnuradio/control_loop and gnuradio/mpsk_receiver_cc
* using initialization values from OP25/cqpsk.py
* Ported from gnuradio/control_loop and gnuradio/mpsk_receiver_cc
* using initialization values from KA1RBI's OP25/cqpsk.py
*/
public class QPSKPhaseErrorTracker implements Listener<ComplexSample>
public class CostasLoop implements Listener<ComplexSample>
{
private float d_loop_bw = 0.03f;
private float d_damping = (float)Math.sqrt( 2.0 ) / 2.0f;
private float d_alpha = 0.03f;
private float d_beta = 0.125f * d_alpha * d_alpha;
private float d_max_freq = ( 2.0f * (float)Math.PI * 1200.0f ) / 48000.0f;
public static final double TWO_PI = 2.0 * Math.PI;
public QPSKPhaseErrorTracker()
/* 45 degree rotation angle */
public static final float THETA = (float)( Math.PI / 4.0d );
/* 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 and 200 to adjust control level */
private float mLoopBandwidth = (float)( ( 2.0d * Math.PI ) / 150.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 float mMaximumFrequency = ( 2.0f * (float)Math.PI * 1200.0f ) /
48000.0f;
public CostasLoop()
{
}
/**
* Increments the phase of the loop
*/
public void increment()
{
mLoopPhase += mLoopFrequency;
/* Keep the loop phase in bounds */
unwrapPhase();
}
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 ComplexSample getCurrentVector()
{
return ComplexSample.fromAngle( mLoopPhase + THETA );
}
@Override
public void receive( ComplexSample sample )
{
float phase_error = -getPhaseError( sample );
/* Normalize magnitude of incoming signal to 1.0 */
sample.normalize();
advance_loop(phase_error, sample );
/* Calculate phase error */
float phaseError = getPhaseError( sample );
/* Adjust for phase error */
adjust( phaseError );
/* Maintain phase between +/- 2 * PI */
unwrapPhase();
frequency_limit();
/* Limit frequency to +/- maximum frequency */
limitFrequency();
}
/**
* 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;
}
private void advance_loop( float phase_error, ComplexSample sample )
private void limitFrequency()
{
float abs = sample.magnitude();
d_freq += d_beta * phase_error * abs;
d_phase += d_freq + d_alpha * phase_error * abs;
}
private void frequency_limit()
{
if( d_freq > d_max_freq )
if( mLoopFrequency > mMaximumFrequency )
{
d_freq = d_max_freq;
mLoopFrequency = mMaximumFrequency;
}
if( d_freq < -d_max_freq )
if( mLoopFrequency < -mMaximumFrequency )
{
d_freq = -d_max_freq;
mLoopFrequency = -mMaximumFrequency;
}
}
/**
* Calculates the phase error for a qpsk sample. Standard qpsk samples
* should be aligned along the x/y axis and any deviation from either
* axis represents an error. Whichever component, real or imaginary,
* that has the lesser absolute value, represents the error, since that
* component should be zero valued.
* Calculates the phase error as the difference between the real and
* imaginary values of the sample. When balanced, both absolute values
* should be equal to ~ 0.707. The difference between the real and
* imaginary values is used as the error signal. Mapping the sample
* vector to the correct quadrant allows us to change the sign of the
* error to correctly reflect early (-) or late (+) phase timing.
*
* For example, constellation point 0 is zero degrees and is represented
* by a sample that contains a real = +1 and an imaginary = 0 component.
* If we sample too early or too late, that will cause a +/- phase
* deviation in the resultant sample and the sample will reflect that
* error value in the imaginary component. That deviation is the error.
* Expects a normalized (to unit circle) sample value.
*
* The feedback error signal is small when the sample's phase is closely
* aligned with a perfect constellation and grows larger as the sample's
* phase diverges from alignment with a perfect constellation.
*/
public float getPhaseError( ComplexSample sample )
{
float error = Math.abs( Math.abs( sample.imaginary() ) -
Math.abs( sample.real() ) );
if( sample.realAbsolute() > sample.imaginaryAbsolute() )
{
if( sample.real() > 0 )
if( sample.imaginary() > 0 )
{
return -sample.imaginary();
return sample.real() > 0 ? -error : error;
}
else
{
return sample.imaginary();
return sample.real() > 0 ? error : -error;
}
}
else
{
if( sample.imaginary() > 0 )
if( sample.real() > 0 )
{
return sample.real();
return sample.real() > 0 ? error : -error;
}
else
{
return -sample.real();
return sample.real() > 0 ? -error : error;
}
}
}

View File

@@ -50,7 +50,7 @@ public class QPSKTapPanel extends TapViewPanel
mTap = tap;
mTap.addListener( getModel() );
mSampleCount = (int)( 500 * tap.getSampleRateRatio() );
mSampleCount = (int)( 100 * tap.getSampleRateRatio() );
getModel().setSampleCount( mSampleCount );
getModel().setDelay( tap.getDelay() );
@@ -78,9 +78,21 @@ public class QPSKTapPanel extends TapViewPanel
g2.setColor( Color.DARK_GRAY );
/* Vertical Line */
g2.drawLine( getWidth() / 2, 5, getWidth() / 2, getHeight() - 5 );
/* Horizontal Line */
g2.drawLine( getWidth() / 2 - (int)( middle ), getHeight() / 2,
getWidth() / 2 + (int)( middle ), getHeight() / 2 );
g2.setColor( Color.CYAN );
/* Crossed Lines */
g2.drawLine( getWidth() / 2 - (int)( middle ), 5,
getWidth() / 2 + (int)( middle ), getHeight() - 5 );
g2.drawLine( getWidth() / 2 + (int)( middle ), 5,
getWidth() / 2 - (int)( middle ), getHeight() - 5 );
if( mSamples != null &&
@@ -103,31 +115,35 @@ public class QPSKTapPanel extends TapViewPanel
g2.setColor( getForeground() );
}
float scale;
if( sample.real() != 0 && sample.imaginary() != 0 )
{
scale = 1.0f / sample.magnitude();
}
else if( sample.real() == 0 )
{
scale = 1.0f / sample.imaginary();
}
else
{
scale = 1.0f / sample.real();
}
// float scale;
//
// if( sample.real() != 0 && sample.imaginary() != 0 )
// {
// scale = 1.0f / sample.magnitude();
// }
// else if( sample.real() == 0 )
// {
// scale = 1.0f / sample.imaginary();
// }
// else
// {
// scale = 1.0f / sample.real();
// }
// float x = ( getWidth() / 2 ) +
// ( scale * sample.real() * middle * 0.75f * mVerticalZoom );
float x = ( getWidth() / 2 ) +
( scale * sample.real() * middle * 0.75f * mVerticalZoom );
( sample.real() * middle * 0.75f * mVerticalZoom );
if( Float.isNaN( x ) )
{
x = getWidth() / 2;
}
// float y = ( getHeight() / 2 ) +
// ( scale * sample.imaginary() * middle * 0.75f * mVerticalZoom );
float y = ( getHeight() / 2 ) +
( scale * sample.imaginary() * middle * 0.75f * mVerticalZoom );
( sample.imaginary() * middle * 0.75f * mVerticalZoom );
if( Float.isNaN( y ) )
{

View File

@@ -201,11 +201,27 @@ public class ComplexSample implements Serializable
return mLeft;
}
/**
* Absolute value of in-phase component
*/
public float inPhaseAbsolute()
{
return Math.abs( mLeft );
}
public float quadrature()
{
return mRight;
}
/**
* Absolute value of quadrature component
*/
public float quadratureAbsolute()
{
return Math.abs( mRight );
}
public float x()
{
return mLeft;