16-Channel Folded Symbol Timing Recovery (ted_x16)

Performs polyphase matched-filter interpolation, Gardner timing-error detection, and PI loop filtering for 16 MSK channels simultaneously by time-sharing one physical datapath.

Tested platforms include xsim.

Design

ted_x16 is the second front-end fold in the 64-channel MSK demodulator chain, immediately downstream of cr_x16. It is the time-shared equivalent of 16 parallel (poly_filter + timing_loop) pairs from msk_demod_channel, collapsed into one physical datapath with all per-channel state — FIR delay line, TED NCO, TED FSM latches, PI integrator, and omega correction — held in 16-entry RAMs.

Input framing (positional, sample rate)

The input is the derotated positional stream from cr_x16: 16-sample complex_short_timed_sample messages at SPS = 4, SOM on channel 0, EOM on channel 15. The same 16-channel round / ~445-idle-clock sparsity argument as cr_x16 makes one folded datapath equivalent to 16 parallel instances.

Output framing (tagged, symbol rate)

The timing loop is rate-changing (one symbol decision per approximately 4 input samples) and each channel’s decision NCO overflows asynchronously, so a fixed positional 16-slot output frame cannot carry the symbol stream. Instead, for each symbol decision the worker emits a tagged pair: a metadata opcode (id = 0, value = channel index 0..15) followed immediately by a single sample opcode (sym_i in bits 31..16, sym_q in bits 15..0), both with SOM and EOM set. This tag-then-sample convention is the stream boundary between the POSITIONAL front end and the TAGGED back end of the folded chain; subsequent workers (dsd_x16, fs_x16, msk_decode_x16) carry the channel identity as metadata rather than as a slot position.

Polyphase interpolator

The FIR stage uses a 3-tap, 32-phase flat (rectangular) polyphase interpolator — the same COEFF_ROM_FLAT table as poly_filter.vhd with USE_PULSE_SHAPING = false. OPV uses differential detection (dsd_x16), so the matched filter at this stage needs only fractional-delay interpolation; a half-sine coherent matched filter would not resolve the ±90° differential symbol structure and would prevent the Gardner TED from locking. Each channel maintains a 3-sample deep delay line (I and Q independently) in a 16 × 3 RAM. On each accepted sample the per-channel delay line is multiplied by the 3 coefficients selected by phase_sel[ch] (top 5 bits of the timing NCO) and the three 32-bit products are accumulated to a 34-bit sum (FACC_W = 34), then sliced back to 16 bits at [PROD_W-2 : COEFF_W-1] = [30:15].

Gardner TED

The timing NCO counts at omega_nominal + adj[ch] per sample (16-bit unsigned wrap). An overflow (carry-out) marks a decision instant; the half-symbol point is detected when the NCO crosses the mid-scale threshold (bit 15 transition without an overflow in the same cycle). The FSM has three states:

  • HUNT — waits for the first decision overflow to establish i_prev/q_prev.

  • WAIT_MID — waits for the mid-symbol crossing to latch i_mid/q_mid.

  • WAIT_DEC — waits for the next decision overflow. At overflow the Gardner error e = i_mid*(i_cur - i_prev) + q_mid*(q_cur - q_prev) (full 34-bit product, top 32 bits used) is computed and passed to the PI filter, a tagged symbol is pushed to the output FIFO, and the FSM returns to WAIT_MID.

phase_sel[ch] is updated on each overflow to the top 5 bits of the post- advance NCO value, selecting the interpolation phase for the next symbol period.

PI loop filter

The timing error drives a PI controller with configurable proportional gain tl_p_gain / shift tl_p_shift and integral gain tl_i_gain / shift tl_i_shift. The default tl_i_shift = 30 leaves the integral term quiescent (proportional-only operation by default). The integral accumulator (32-bit signed) is saturated to [MAX_ACC_NEG, MAX_ACC_POS] (= ±2³⁰). The PI output adj[ch] (16-bit signed) is the omega correction added to omega_nominal each symbol period. tl_freeze holds all PI integrators without zeroing.

Output FIFO

A 64-entry synchronous FIFO buffers tagged symbols (4 bits channel + 32 bits IQ = 36 bits per entry) between the Gardner stage and the output FSM. The output FSM serialises each FIFO entry as four metadata words followed by one sample word, advancing only when the downstream consumer asserts ready.

Interface

<?xml version='1.0' encoding='utf-8'?>
<!-- ted_x16: FOLDED 16-channel symbol-timing recovery (polyphase matched-filter
     interpolator + Gardner TED + PI loop), the time-shared equivalent of 16
     parallel poly_filter+timing_loop pairs from msk_demod_channel.

     INPUT  (positional, sample rate): one 16-sample complex_short_timed_sample
       message per frame (SOM@pos0 .. EOM@pos15); sample at position k = channel
       k.  This is the carrier-corrected stream from cr_x16 at SPS=4.

     OUTPUT (tagged, symbol rate): the timing loop is RATE-CHANGING and each
       channel's symbol-decision NCO overflows asynchronously, so a positional
       16-slot frame cannot carry it.  Instead, per symbol decision the worker
       emits metadata(id=0, value=channel 0..15) followed by a 1-sample message
       carrying the decided symbol (sym_i in [31:16], sym_q in [15:0]).  At
       ~13.5 ksym/s/channel this tagging is cheap (on-chip, not the host DMA
       path that made per-sample metadata expensive).

     One shared poly_filter+TED+PI datapath; all per-channel state (FIR delay
     line, TED NCO/FSM/sample latches/phase_sel, PI integrator) lives in
     16-entry RAMs.  Valid because the stream is sparse (16 samp/frame, ~445
     idle clocks) and the loop latency is far below the 16-channel round, so each
     channel settles before its next sample, same argument as cr_x16.

     Defaults match msk_demod_frontend's fixed timing constants (SPS=4). -->
<ComponentSpec>

  <property name='init' type='bool' writable='true' default='false'
            description="Pulse high to reset the datapath and all 16 per-channel timing states"/>

  <!-- Timing-loop tuning (one shared set; applies to all 16 channels) -->
  <property name='omega_nominal' type='ushort' writable='true' default='16384'
            description="Nominal timing-NCO step = 2^16/SPS (16384 for SPS=4)"/>
  <property name='tl_p_gain' type='ulong' writable='true' default='1'
            description="Gardner PI proportional gain (shared)"/>
  <property name='tl_i_gain' type='ulong' writable='true' default='1'
            description="Gardner PI integral gain (shared)"/>
  <property name='tl_p_shift' type='uchar' writable='true' default='15'
            description="Gardner PI proportional shift (Kp). Default per timing_loop guide."/>
  <property name='tl_i_shift' type='uchar' writable='true' default='30'
            description="Gardner PI integral shift (Ki). 30 = proportional-only (integral quiescent)."/>
  <property name='tl_freeze' type='bool' writable='true' default='false'
            description="Freeze the PI integrators (hold omega per channel during silence)"/>

  <!-- Per-channel volatile status (index = channel 0..15) -->
  <property name='omega_current' type='ushort' volatile='true' arraylength='16'
            description="Current closed-loop timing-NCO step per channel"/>
  <property name='sym_count' type='ulong' volatile='true' arraylength='16'
            description="Symbols emitted per channel since reset"/>

  <!-- Ports: positional sample-rate IQ in, tagged symbol stream out -->
  <port name="input"  producer="false" protocol="complex_short_timed_sample-prot"/>
  <port name="output" producer="true"  protocol="complex_short_timed_sample-prot"/>

</ComponentSpec>

Ports

Inputs:

  • input: Derotated positional IQ stream from cr_x16.

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Outputs:

  • output: Tagged symbol stream — per symbol: metadata(id=0, value=channel).

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Opcode Handling

ted_x16 processes complex_short_timed_sample sample opcodes on the input. Non-sample opcodes are consumed without producing output. On the output the worker emits both metadata opcodes (for the channel tag) and sample opcodes (for the interpolated symbol); downstream workers must handle both. The EOF condition is propagated to the output after the output FSM returns to idle.

Properties

  • init: Pulse high to reset the datapath and all 16 per-channel timing states.

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • omega_nominal: Nominal timing-NCO step = 2^16/SPS (16384 for SPS=4).

    • Type: ushort

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 16384

  • tl_p_gain: Gardner PI proportional gain (shared).

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

  • tl_i_gain: Gardner PI integral gain (shared).

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

  • tl_p_shift: Gardner PI proportional shift (Kp). Default per timing_loop guide.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 15

  • tl_i_shift: Gardner PI integral shift (Ki). 30 = proportional-only (integral quiescent).

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 30

  • tl_freeze: Freeze the PI integrators (hold omega per channel during silence).

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • omega_current: Current closed-loop timing-NCO step per channel.

    • Type: ushort

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • sym_count: Symbols emitted per channel since reset.

    • Type: ulong

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

Implementations

  • ted_x16 (HDL)

    Time-shared (folded) implementation of polyphase matched-filter interpolation, Gardner timing-error detection, and PI loop filtering across 16 MSK channels.

Dependencies

Dependencies of ted_x16 are:

  • ieee.std_logic_1164

  • ieee.numeric_std

  • ocpi.types (OpenCPI type definitions)

The polyphase interpolator coefficient ROM, Gardner TED FSM, and PI loop-filter arithmetic are all inlined in the worker VHDL; no external primitive libraries are required.

Limitations

Limitations of ted_x16 are:

  • Loop gain properties (tl_p_gain, tl_i_gain, tl_p_shift, tl_i_shift) are shared across all 16 channels; per-channel tuning is not supported.

  • The polyphase interpolator is fixed at 3 taps and 32 phases (N_TAPS = 3, N_PHASES = 32); the coefficients are ROM constants compiled into the bitstream.

  • The input framing is fixed at exactly 16 samples per message (SPS = 4 positional encoding); other sample-per-symbol ratios require parameter changes and re-synthesis.

  • omega_nominal must be set to 2^16 / SPS before operation; the default 16384 is correct for SPS = 4.

  • The integral term is effectively disabled by the default tl_i_shift = 30. Users requiring a true PI loop must reduce tl_i_shift (e.g. to 32..36 for light integration).

Testing

The component unit test drives the positional input with a carrier-corrected 16-channel complex sinusoidal stimulus at SPS = 4 and verifies that the tagged symbol output converges to the correct symbol values and that sym_count increments for all 16 channels within a timeout period.

Tested platforms: None