Polyphase Clock Synchroniser (polyphase_clock_synchroniser_s)
Introduction
A symbol clock synchroniser produces symbols from a stream of samples. The most basic clock synchroniser works by picking a single sample out of the stream at the correct symbol rate (this ratio is known as the samples per symbol, or SPS). The problem with this simple approach is knowing whether the chosen sample is at the optimal location within the sample stream. This problem is shown in Figure 289: below. The orange line shows the best sampling location, however if the stream was just down-sampled by SPS then you might end up actually sampling at the grey location. If the signal to noise ratio (SNR) is good, then this might be acceptable. However as the SNR drops, the chances of getting a bit error when decoding in this bad location increases.
Figure 289: Symbol Sampling Points
In order to allow the selection of the best sampling point there needs to be some method of determining where the peaks and the zeros are in the data. At the most basic this can be achieved through a peak detector running over the SPS number of points. However, this is only a useful method if there are a high number of samples per symbol. If, however, there are only a few samples per symbol, it is necessary to instead interpolate first to allow this selection of an optimum point.
Figure 290: Interpolated Symbol Sampling Points
The interpolation allows the selection of the optimum points, where the number of samples per symbol is too low to rely on a direct down-sampling. The interpolation also is preferable to down-sampling because the additional low pass filter used within the interpolation filter can be shaped to the samples per symbol which will provide a processing gain compared to a direct down-sample.
Design
The polyphase symbol clock synchroniser attempts to recover symbol timing for an incoming data stream. Symbols are output from the component.
The symbol synchronisation is undertaken through the stages shown in Figure 291: below. This block is intended to detect and synchronise to a symbol stream for simple zero crossing modulation schemes (i.e. FSK, PSK).
Figure 291: Synchronisation Stages
\(x(n)\) defines the incoming (unsynchronised) symbol stream (at 2x regenerated sampling period)
\(y(m)\) defines the outgoing symbol data (at regenerated sampling period)
\(e(m)\) defines the outgoing calculated symbol error (at 2x regenerated sampling period)
Each of these stages are detailed below.
The implementation handles bandwidth limited signals (i.e. that are known to have 2 incoming samples for a single outgoing symbol).
The timing error detection uses a Gardner zero crossing detection method; this is suited to lower orders of modulation.
Notes
The incoming data and the polyphase interpolation filter are assumed to be run at 2x the desired output symbol rate. This is to prevent ambiguity if the previous decimating filter / ADC was badly matched to the filter.
The down sample by 2 is a simple 1 in 2 sample selection (i.e. no low pass filter present) - this is acceptable as every other sample should be the zero crossing point (as needed for the Gardner loop).
Interpolation filter selection and loop filter should be free-running to allow for n-FSK functionality (i.e. if no zero crossing is detected then the error is ignored as opposed to being considered maximal).
The selection of sub symbol may undergo a step change due to clock synchronisation. This is limited to only a single step per loop cycle.
For FSK modulation, matched filtering does not really perform as desired (zero crossing RRC can lead to the need for quadrature modulation methods). Therefore, a Gaussian transmit pulse shaping will show better performance in a real only demodulator.
References
Symbol Synchronization for SDR Using a Polyphase Filterbank Based on an FPGA P. Fiala and R. Linhart (2015)
Multirate Digital Filters for Symbol Timing Synchronization in Software Defined Radios F. Harris, M. Rice (2001)
Samples to Digital Symbols [GRCON2017] A Walls (2017)
Combined Matching and Polyphase Interpolation Filter (Real)
The interpolation structure used within this component consists of a single multiply-accumulate (MAC) unit. This provides a resource efficient method, but limits the maximum possible throughput. As up to 2 polyphase branches might be calculated (if sample stuffing is occurring), the input throughput should be low enough that \(2 \times \texttt{taps_per_branch}\) multiply-accumulate calculations can occur for each input sample.
The tap values and the number of taps to use is run-time settable, however a hard limit is defined as well to limit the amount of memory required.
Figure 292: Polyphase Interpolation Filter
Design
New samples are inserted within the sample RAM, which acts as a simple circular buffer of the last N samples (where N is equal to the FIR tap count divided by the number of polyphase branches).
When a new sample is inserted…
The relevant polyphase branch b0 tap is multiplied by the new sample.
The modulo counter then jumps to the next tap for that branch (a jump of the up sampling ratio), and then multiplies with the z-1 incoming sample.
The modulo counter continues until all the taps for that branch have been through the MAC. The output is then registered and the output Sample Valid line set High.
The accumulator is then internally reset, awaiting the next incoming sample.
Gardner loop detector
The Timing Error detector implemented is a Gardner zero crossing detector. This works by comparing the zero crossing location against the difference between two symbols. Therefore an incoming sample rate of at least twice that of the output symbol rate is required.
The Gardner loop has the form: Error = Prompt x (Late - Early) This is shown within Figure 293: with the \(n_3\) value being used for the Prompt variable. However, as the data stream should be centred around zero, if shifted by one position, i.e. using \(n_3\), \(n_4\) and \(n_5\), the timing error detector would produce an error with the opposite sign.
As can be seen in Figure 294:, under a signal varying in the range (-1, 1) the error limits are also bounded to the same region (with the greatest error occurring at 45 degrees lag/lead - as there are 4 samples per cycle).
Figure 293: Symbol location for Early/Late samples
Figure 294: Error Variation with varying sample phase
Notes
A Gardner loop is sensitive to any DC bias upon the incoming samples (caused by the non-differentiated term).
If there is no hard decision on the incoming samples (i.e. there is a magnitude as opposed to being exactly -1 or 1), then additional timing errors can be introduced by a run of the same symbol. To get around this a sign comparison of all three terms is used to ensure only zero crossings are used.
If the input energy is varying with time (likely with an FM phase differential discriminator), then there should be an AGC block, stabilising this data. Without that, the output from the loop will not have the same proportional gain as is expected by the loop filter/PLL.
The coarse estimation of the number of samples per symbol is not part of this element, only the fine correction undertaken naturally through the timing estimation.
Loop Filter
The realistic output of the timing error detector is not a smooth curve as defined above, but instead a choppy set of values (caused by sampling clock errors, and slight frequency misalignment). As such a Phase Locked Loop (PLL) loop filter is desired to help smooth the variation seen. This should be a second order PI (Proportional-Integral) loop filter, as there is a need for phase recovery (sub sample alignment using the interpolator), as well as frequency recovery (clock misalignment, leading to drift, using the interpolator, and incoming sample stuff/skip).
The loop filter has the following equations:
\(\texttt{ted}\) is the timing error detector output, (multiplied by any scaling required)
\(K_p\) is the proportional filter constant
\(K_i\) is the integrator filter constant
\(E_i\) is the integral error value
\(Err\) is the resultant error value
This is shown below as the start of the tap selection logic.
Sample and Tap Selection
The tap and sample selection is shown in Figure 295: below. The output of the Timing Error Detector is filtered, then accumulated. This accumulated value provides the phase error. A wrapping detector allows for small frequency adjustment.
Figure 295: Sample and Tap Selection
The Gardner timing error detector produces an output which has a magnitude related to the distance of the sample away from the zero crossing. As this value is used to correct the sample timing error, this value is negated.
The output from this timing error could lead to the zero crossing and actual usable samples being the wrong way round. To help detect this, the output symbol forwarded is the symbol with the highest magnitude of the two.
This symbol selection method can handle phase offsets from any sampling phase and small frequency errors (i.e. less than 100 ppm). However, this will not correct for a coarse frequency error, without an additional output sample skip/stuff mechanism.
Figure 296: Phase correction with small initial symbol phase offset
Figure 297: Phase correction with large initial symbol offset
Figure 298: Frequency Correction (note ever changing branch)
Figure 299: Symbol Skipping (145)
Figure 300: Symbol Stuffing (162)
Interface
<?xml version="1.0"?>
<componentspec>
<property name="rounding_type" type="enum" parameter="true" enums="truncate,half_up,half_even" default="half_even"
description="Selects the sample rounding type"/>
<property name="max_number_of_taps" type="ushort" parameter="true" default="64"
description="Defines the maximum number of taps across all interpolation branches"/>
<property name="interpolation_factor" type="uchar" initial="true" writable="false" default="8"
description="Defines the number of branches that can be used for interpolation"/>
<property name="number_of_taps" type="ushort" initial="true" writable="false" default="max_number_of_taps"
description="Number of taps that are actually being used per interpolation branch"/>
<!-- LINT EXCEPTION: xml_012: 2:
Cannot set default as size varies with parameter values. -->
<property name="taps" type="short" arraylength="max_number_of_taps" writable="true" description="Polyphase interpolation taps"/>
<property name="loop_coefficients" arraylength="3" type="short" writable="true" default="1536,51,512"
description="Coefficients for the loop filter and NCO"/>
<port name="input" producer="false" protocol="short_timed_sample-prot"/>
<port name="output" producer="true" protocol="short_timed_sample-prot"/>
</componentspec>
Ports
Inputs:
input: Primary input samples port.Protocol:
short_timed_sample-protOptional:
False
Outputs:
output: Primary output samples port.Protocol:
short_timed_sample-protOptional:
False
Opcode handling
Time
Incoming: Resynchronises the internal input sample time.
Outgoing: This is used in conjunction with the Interpolator tap selection to calculate the outputted symbol time. Note that a change in Sample_Interval might have a delay before the correlating change is made to the outgoing time. Note also that if a Sample_Interval has not been received, the time for the next outputted symbol cannot be calculated, so the time is just passed straight through.
Sample_Interval
Incoming: Value is stored and used in the calculation of the input sample time counter (as detailed above).
Outgoing: Sample Interval is set to the incoming sample interval multiplied by 2 (i.e. the data rate is halved).
Flush
Zeros are fed in for a length of taps_per_branch (any produced samples are outputted).
Then, the Flush is forwarded to output.
Discontinuity
Forwarded to output.
The Sample RAM, Accumulator and Loop filter are reset.
Metadata
Forwarded to output.
Samples
Incoming: Samples are consumed, leading to the internal counter and buffers to step on each sample.
Outgoing: Symbols are produced at half the rate of the input data stream (i.e. the data is decimated by 2).
Properties
rounding_type: Selects the sample rounding type.Type:
enumAllowed values:
['truncate', 'half_up', 'half_even']
Access:
Parameter:
TrueWritable:
FalseInitial:
FalseVolatile:
False
Default value:
half_even
max_number_of_taps: Defines the maximum number of taps across all interpolation branches.Type:
ushortAccess:
Parameter:
TrueWritable:
FalseInitial:
FalseVolatile:
False
Default value:
64
interpolation_factor: Defines the number of branches that can be used for interpolation.Type:
ucharAccess:
Parameter:
FalseWritable:
FalseInitial:
TrueVolatile:
False
Default value:
8
number_of_taps: Number of taps that are actually being used per interpolation branch.Type:
ushortAccess:
Parameter:
FalseWritable:
FalseInitial:
TrueVolatile:
False
Default value:
max_number_of_taps
taps: Polyphase interpolation taps. The actual number of taps required is defined through thetaps_per_branch * max_interpolation. Tap values are in the range (-1, 1) and passed as fixed point values in S0.15 format (i.e. 1 sign bit and 15 fractional bits), and can be converted to float by dividing by \(2^{15}\).Type:
shortArray dimensions:
['max_number_of_taps']
Access:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
None
loop_coefficients: Coefficients for the loop filter and NCO. Structured as: [\(K_p\), \(K_i\), \(K_{nco}\)]. These are actually fixed point values in S5.10 format (i.e. 1 sign bit, 5 integer bits and 10 fractional bits), and can be converted to float by dividing by \(2^{10} = 1024\).Type:
shortArray dimensions:
['3']
Access:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1536,51,512
Rounding Methods
The following round methods are offered:
truncate: Truncates value towards zero (this will lead to a strong DC bias).
half_up: Round values away from zero (this will lead to a weak bias towards +/- infinity).
half_even: Round values towards nearest even - convergent rounding (this will lead to no overall direction of bias).
Implementations
Example application
<?xml version="1.0"?>
<application done="file_write">
<instance component="ocpi.core.file_read" connect="frequency_demodulator_xs_s">
<property name="filename" value="input.bin"/>
</instance>
<instance component="ocpi.comp.sdr.dsp.frequency_demodulator_xs_s" connect="polyphase_clock_synchroniser_s">
</instance>
<instance component="ocpi.comp.sdr.dsp.polyphase_clock_synchroniser_s" connect="threshold_s_b">
<property name="taps_per_branch" value="5"/>
<property name="taps" valuefile="taps.dat"/>
<property name="max_interpolation" value="32"/>
<property name="max_taps_per_branch" value="5"/>
<property name="rounding_type" value="half_even"/>
<property name="loop_coefficients" value="1536,51,512"/>
</instance>
<instance component="ocpi.comp.sdr.dsp.threshold_s_b" connect="file_write">
<property name="threshold" value="0"/>
<property name="invert_output" value="false"/>
</instance>
<instance component="ocpi.core.file_write">
<property name="filename" value="output.bin"/>
</instance>
</application>
Dependencies
The dependencies to other elements in OpenCPI are:
components/communication/common/gardner_loop/gardner_loop.hh
components/dsp/common/polyphase_interpolator/polyphase_interpolator.hh
There is also dependencies on:
ieee.std_logic_1164
ieee.numeric_std
ieee.math_real
Limitations
The synchroniser is designed to work on discriminated samples (i.e. after a discriminator), as opposed to raw ADC samples.
Initially the timing error detector is designed for 2 samples per symbol (if real data).
Initially this is designed to handle real only modulation order=2.
Testing
Component testing not completed.