Polyphase Decimator (polyphase_decimator_xs)

A polyphase decimator is a resource efficient method for decimating a signal.

Introduction

To decimate by a factor of N, a normal decimator operates by first filtering the incoming data stream with a low pass filter (with a maximum normalised frequency of 1/N), forwarding a sample to the output, and then discarding N-1 samples.

This is resource intensive, as it means the filtering is running at the faster sample rate of the two (i.e. the input rate). In addition, a lot of the samples passing through this filter are actually going to be discarded and therefore are unneeded.

Instead with a Polyphase Decimator the filter is split into sub-filters, each set relating to a single input sample. The input then steps through these sub-filters, a sample at a time. Once the last sub-filter output value has been calculated, all of the sub-filter outputs are summed together to provide the output sample value. Following this the input sample steps back to the first sub-filter, and the process continues.

For example, consider decimating the following example stream by a factor of three:

Input Samples

A

B

C

D

E

F

G

H

I

J

K

L

M

with a set of taps:

Filter Taps

0

1

2

3

4

5

6

7

8

9

Traditional Decimator:

Output sample 1

A0 + B1 + C2 + D3 + E4 + F5 + G6 + H7 + I8 + J9

Output sample 2

B0 + C1 + D2 + E3 + F4 + G5 + H6 + I7 + J8 + K9
(This sample is then discarded by decimation)

Output sample 3

C0 + D1 + E2 + F3 + G4 + H5 + I6 + J7 + K8+ L9
(This sample is then discarded by decimation)

Polyphase Decimator:

Step 0

A0 + D3 + G6 + J9

Step 1

B1 + E4 + H7

Step 2

C2 + F5 + I8

Output

Step 0 + Step 1 + Step 2

Design

The Polyphase Decimator is designed to allow runtime variable decimation rates, along with customisable low pass filter taps. The user of this component should ensure that the provided taps are correctly scaled, e.g. to provide a unity gain, as the taps are not scaled within this component.

Taps should be generated on the high-sample rate, as a low pass filter with a normalised cut-off frequency of 1/decimation_factor. If desired then additional filter shaping can be added.

Input values and taps should be scaled appropriately to ensure that saturation or overflow does not occur.

The internal data flow follows the following pseudo-states:

Wait for Data

  • Wait until a new input sample is received.

  • Once a sample is received, add it to the current filter-bank’s input data buffer. (This buffer shall have a minimum size of taps_per_branch).

  • This branch can then be processed.

Processing Sample

  • Multiply the taps and incoming sample data for the current filter bank.

  • If this is the last filter-bank:

    • Accumulate the outputs of all the filter-banks

    • Output this accumulation as a sample from the component.

    • Reset to the 0th filter-bank.

    • Return to Wait for Data.

  • If not the last filter-bank:

    • Increment to the next filter-bank

    • Return to Wait for Data.

Time Message Alignment

In order to help align messages in time, the decimator allows the group delay of the built-in filter to be entered as a property. If no group delay is defined, then this value is defaulted to zero.

The group delay is subtracted as a time offset from any incoming timestamps (this is the equivalent of delaying the timestamp relative to the samples).

The group delay is defined with two properties; group_delay_seconds for seconds, group_delay_fractional for fractional delay - these properties having the same format as the timestamp and sample interval opcodes.

If the subtraction of the group delay leads to a time value which is less than zero, then the value is wrapped around by the maximum value the timestamp field can hold (UQ32.64).

Interface

<?xml version="1.0"?>
<componentspec>
  <property name="decimation_factor" type="uchar" initial="true" writable="false" default="1"
    description="Decimation factor (number of branches)"/>
  <property name="taps_per_branch" type="uchar" initial="true" writable="false" default="1"
    description="Number of taps for each branch"/>
  <property name="tap_array_length" type="ushort" parameter="true" writable="false" default="65535" description="Size of tap array"/>
  <property name="rounding_type" type="enum" initial="true" writable="false" enums="truncate,half_up,half_even" default="half_even"
    description="Rounding type"/>
  <property name="group_delay_seconds" type="ulong" default="0" writable="true" description="Group delay of the filter in seconds"/>
  <property name="group_delay_fractional" type="ulonglong" default="0" writable="true"
    description="Group delay of the filter fractional"/>
  <!-- LINT EXCEPTION: xml_012: 1: Cannot specify default value on an array -->
  <property name="taps" type="short" arraylength="tap_array_length" writable="true" description="Array of tap values"/>
  <port name="input" producer="false" protocol="complex_short_timed_sample-prot"/>
  <port name="output" producer="true" protocol="complex_short_timed_sample-prot"/>
</componentspec>

Opcode Handling

  • Samples

    • Apply decimation to these samples and send to output port.

    • If there is not sufficient samples to produce a decimated sample, then no messages are forwarded.

    • If the number of samples is not a multiple of the decimation factor, then the number of output samples will be \(\lfloor{n_{input} \over N} \rfloor\), where \(n_{input}\) is the number of input samples, and \(N\) is the decimation factor. Any remaining samples are left in the worker input buffer.

  • Flush

    • If any data received since last reset:

      • Input taps_per_branch * decimation_factor zeros into the sample buffer, forwarding any decimated output.

      • Reset sample buffer, and sub-filter offset.

      • Forward Flush opcode to output port.

    • Otherwise, forward Flush opcode to output port.

  • Discontinuity

    • Reset input buffer.

    • Forward to output port.

  • Metadata

    • Forward to output port.

  • Sample_Interval

    • Calculate output sample interval as input sample_interval * decimation_factor.

    • Forward calculated value to output port.

  • Time

    • Resynchronises the internal input sample time (as long as there is not a previous time currently being processed).

    • This value is used, along with the incoming sample_interval and the group delay properties, to calculate the output sample time.

    • If the component is not processing samples (i.e. if no data samples have been received by the worker) then the calculated time is passed through.

    • If the component is currently processing samples and a time opcode is received:

      • The current decimated sample is completed and outputted.

      • The calculated time, plus sample_interval multiplied by samples since the new time was received, is outputted.

    • Notes:

      • The time opcode is affected by the group delay of the FIR filter, this is detailed above.

      • If a sample_interval has not been received, the time for the next outputted symbol cannot be calculated. In this case, the time is just passed straight through, resulting in a slight inaccuracy in the downstream time.

      • If a time opcode occurs in a processing block, there is no error checking for discontinuity

      • If sample_interval and time changes in the middle of a processing block then the outputted time and sample interval will occur after the end of processing the current sample block, however the ordering of these two messages might lead to a slight inaccuracy (if time is updated before sample_interval).

Properties

  • decimation_factor: Decimation factor (number of branches). N.B. A value of 1 means no decimation.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: True

      • Volatile: False

    • Default value: 1

  • taps_per_branch: Number of taps for each branch.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: True

      • Volatile: False

    • Default value: 1

  • tap_array_length: Size of tap array.

    • Type: ushort

    • Access:

      • Parameter: True

      • Writable: False

      • Initial: False

      • Volatile: False

    • Default value: 65535

  • rounding_type: Rounding type.

    • Type: enum

      • Allowed values: ['truncate', 'half_up', 'half_even']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: True

      • Volatile: False

    • Default value: half_even

  • group_delay_seconds: Group delay of the filter in seconds.

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 0

  • group_delay_fractional: Group delay of the filter fractional.

    • Type: ulonglong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 0

  • taps: Array of tap values. The actual number of taps required is taps_per_branch * decimation_factor.

    • Type: short

      • Array dimensions: ['tap_array_length']

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: None

Note. The precision of the taps property depends on the input protocol type:

For char and complex_char inputs:

  • the array data type is char

  • these are 8bit (S0.7) (i.e. in the range -1, 1).

  • sign bit | Fractional bits (7 bits)

  • To convert from float multiply by \(2^7\)

For short and complex_short inputs:

  • the array data type is short

  • these are 16bit (S0.15) (i.e. in the range -1, 1).

  • sign bit | Fractional bits (15 bits)

  • To convert from float multiply by \(2^{15}\)

For long and complex_long inputs:

  • the array data type is long

  • these are 32bit (S0.31) (i.e. in the range -1, 1).

  • sign bit | Fractional bits (31 bits)

  • To convert from float multiply by \(2^{31}\)

The array is intended to be entered “Branch First”, therefore the following shape is used. (Note that this shape is the natural shape for the taps as produced by the low pass filter calculated at the interpolated sampling rate.)

branch

0

1

2

3

0

1

2

3

tap_addr

0

0

0

0

1

1

1

1

Ports

Inputs:

  • input: Primary input samples port.

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Outputs:

  • output: Primary output samples port.

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Implementations

Example Application

<?xml version="1.0"?>
<application done="file_write">
  <instance component="ocpi.core.file_read" connect="polyphase_decimator_xs">
    <property name="filename" value="input.bin"/>
  </instance>
  <instance component="ocpi.comp.sdr.dsp.polyphase_decimator_xs" connect="file_write">
    <property name="decimation_factor" value="16"/>
    <property name="taps_per_branch" value="8"/>
    <property name="taps" valuefile="taps.data"/>
    <property name="rounding_type" value="half_even"/>
  </instance>
  <instance component="ocpi.core.file_write">
    <property name="filename" value="output.bin"/>
  </instance>
</application>

Dependencies

The dependencies to other elements in OpenCPI are:

There is also a dependency on:

  • ieee.std_logic_1164

  • ieee.numeric_std

  • ieee.math_real

Limitations

Limitations of polyphase_decimator_xs are:

  • None.

Testing

Tested platforms: None

Component testing not completed.