ted_x16 HDL Worker

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

Detail

The worker uses a single clock domain (ctl_in.clk). Reset is the logical OR of ctl_in.reset and the init writable property.

This worker is the stream-format boundary in the folded chain: it consumes the POSITIONAL sample-rate stream from cr_x16 and produces a TAGGED symbol-rate stream for dsd_x16. The format change is forced by the Gardner TED being rate-changing and per-channel asynchronous — a fixed 16-slot positional frame cannot carry decisions that arrive at different times for different channels.

Channel/valid pipeline

The 3-stage ch_p/vld_p sideband pipeline is gated on in_beat (the logical AND of input_in.ready, input_in.valid, and can_accept). Both the FIR products and the accumulator register are also gated on in_beat. This is the critical backpressure fix: a mid-burst stall freezes the entire datapath uniformly so that data, its products, and its channel/valid tags stay in lockstep. The earlier bug (un-gated vld_p shift) desynchronised the valid pipeline from the FIR accumulator during a stall, attributing each channel’s filtered output to the wrong (predecessor) channel. The Gardner FSM and the PI update are both referenced off ch_p(1) / vld_p(1) for the same reason: the FIR accumulator for a given input beat is valid one cycle after that beat, so the TED must read it at pipeline position 1, not position 2.

FIR interpolator (stages 0 and 1)

Constants: SAMPLE_W = 16, COEFF_W = 16, N_TAPS = 3, N_PHASES = 32, PROD_W = 32, FACC_W = 34.

On each accepted sample beat (in_beat and sample opcode):

  • The per-channel delay line dl_i[ch][0..2] / dl_q[ch][0..2] is read (OLD values) and multiplied by the 3 coefficients COEFF_ROM[phase_sel[ch]] to form three 32-bit signed products each for I and Q.

  • The new sample is then shifted into dl_i[ch][0] / dl_q[ch][0] (delay line advances after the multiply, preserving poly_filter semantics).

One clock later (stage 1, also gated on in_beat):

  • The three products are summed into acc_i/acc_q (34-bit wide to accommodate the carry growth of three 32-bit additions).

  • filt_i/filt_q are the combinational slice acc[PROD_W-2 : COEFF_W-1] = bits [30:15], restoring Q1.15 with unity-gain scaling.

The coefficient table COEFF_ROM is a flat (rectangular window) 32-phase fractional-delay interpolator. Phase 0 is identity (c0 = 32767, c1 = c2 = 0); phases 1..31 blend the current and adjacent samples to interpolate at 1/32-sample resolution. This matches poly_filter.vhd with USE_PULSE_SHAPING = false and is correct for the differential-detection path used by OPV; a half-sine ROM cannot resolve the ±90° MSK symbol structure.

Gardner TED (stage 2)

Timing NCO: 16-bit unsigned, per channel. The closed-loop step size is clamp(omega_nominal + adj[ch], 1, 65535); an overflow (17th bit of the unsigned addition) marks a decision instant; the mid-symbol point is detected when bit 15 transitions from 0 to 1 in the same addition without an overflow.

FSM states (tstate[ch]) and transitions:

  • ST_HUNT ("00") — awaits the first overflow; latches i_prev/q_prev, advances to ST_WAIT_MID.

  • ST_WAIT_MID ("01") — awaits mid-symbol crossing; if a second overflow arrives first i_prev/q_prev are refreshed; on mid-symbol latches i_mid/q_mid, advances to ST_WAIT_DEC.

  • ST_WAIT_DEC ("10") — on each mid-symbol crossing refreshes i_mid/q_mid; on overflow computes the Gardner error: e = i_mid*(i_cur - i_prev) + q_mid*(q_cur - q_prev) where differences are 17-bit signed and products are 33-bit signed; the two products are summed to a 34-bit result and the top 32 bits form ted_err_r (Q1.31 scaled). Simultaneously the decided symbol {filt_i, filt_q} and its channel id are pushed to the output FIFO; symcnt[ch] increments. The FSM then refreshes i_prev/q_prev from the current sample and returns to ST_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 following symbol period.

PI loop filter (stage 3)

Triggered by the ted_val_r / ted_ch_r handoff from the Gardner stage (one pulse per symbol decision). Constants: ACC_W = 32, GAIN_W = 24, NCO_W = 16.

p_val = (tl_p_gain[23:0] * terr) >> tl_p_shift, result narrowed to 16 bits. i_val = (i_acc[ch] * tl_i_gain[23:0]) >> tl_i_shift, result narrowed to 16 bits. i_acc[ch] is updated to the saturated sum clamp(i_acc[ch] + terr, MAX_ACC_NEG, MAX_ACC_POS). The tl_freeze property holds i_acc without zeroing. adj[ch] = p_val + i_val.

Default tl_i_shift = 30 keeps i_val at zero for typical 32-bit i_acc values, giving proportional-only (first-order) timing correction.

Output FIFO and FSM

The FIFO is 64 entries deep (6-bit address); each entry is 36 bits: 4-bit channel id concatenated with 32-bit IQ. can_accept drops when occupancy exceeds FIFO_DEPTH - 4 to leave room for the last in-flight symbol.

The output FSM (OST_IDLE / OST_M0 / OST_M1 / OST_M2 / OST_M3 / OST_S / OST_EOF) serialises each FIFO entry as five output words:

  1. OST_M0 — metadata opcode, SOM, data = 0 (id = 0).

  2. OST_M1 — metadata, data = 0 (pad).

  3. OST_M2 — metadata, data = 0x0000000 & cur_ch_o[3:0] (value[31:0] = channel).

  4. OST_M3 — metadata, EOM, data = 0 (value[63:32] = 0).

  5. OST_S — sample opcode, SOM+EOM, data = {sym_i[15:0], sym_q[15:0]}.

The FIFO entry is popped (rd_ptr advanced) only when the OST_S beat is accepted by the downstream consumer.

Latency from input sample to first output metadata word: variable, governed by the timing NCO convergence to the symbol boundary, typically 3..5 symbols after reset (ST_HUNT + ST_WAIT_MID + one ST_WAIT_DEC iteration).

Worker Properties

  • ocpi_buffer_size_output

    • Type: ushort

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: True

      • Volatile: False

      • Read back: False

    • Default value: 0

  • ocpi_blocked_output

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

      • Read back: False

    • Default value: None

  • ocpi_max_latency_output

    • Type: ushort

    • Access:

      • Parameter: True

      • Writable: False

      • Initial: False

      • Volatile: False

      • Read back: False

    • Default value: None

  • ocpi_latency_output

    • Type: ushort

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

      • Read back: False

    • Default value: None

  • ocpi_messages_output

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

      • Read back: False

    • Default value: None

  • ocpi_bytes_output

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

      • Read back: False

    • Default value: None

Worker Ports

    Inputs:

  • input

    • Protocol: complex_short_timed_sample

    • Optional: False

  • Outputs:

  • output

    • Protocol: complex_short_timed_sample

    • Optional: False

Worker Ports

Inputs:

  • input:

    • Type: StreamInterface

    • Protocol: complex_short_timed_sample-prot

    • ClockDirection: in

    • Data width: 32

Outputs:

  • output:

    • Type: StreamInterface

    • Protocol: complex_short_timed_sample-prot

    • Worker EOF: true

    • Clock: input

    • Data width: 32

Utilisation

Utilization reporting is not currently implemented.