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