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 coefficientsCOEFF_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_qare the combinational sliceacc[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; latchesi_prev/q_prev, advances toST_WAIT_MID.ST_WAIT_MID("01") — awaits mid-symbol crossing; if a second overflow arrives firsti_prev/q_prevare refreshed; on mid-symbol latchesi_mid/q_mid, advances toST_WAIT_DEC.ST_WAIT_DEC("10") — on each mid-symbol crossing refreshesi_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 formted_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 refreshesi_prev/q_prevfrom the current sample and returns toST_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:
OST_M0—metadataopcode, SOM,data= 0 (id= 0).OST_M1—metadata,data= 0 (pad).OST_M2—metadata,data=0x0000000&cur_ch_o[3:0](value[31:0]= channel).OST_M3—metadata, EOM,data= 0 (value[63:32]= 0).OST_S—sampleopcode, 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_outputType:
ushortAccess:
Parameter:
FalseWritable:
FalseInitial:
TrueVolatile:
FalseRead back:
False
Default value:
0
ocpi_blocked_outputType:
ulongAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
TrueRead back:
False
Default value:
None
ocpi_max_latency_outputType:
ushortAccess:
Parameter:
TrueWritable:
FalseInitial:
FalseVolatile:
FalseRead back:
False
Default value:
None
ocpi_latency_outputType:
ushortAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
TrueRead back:
False
Default value:
None
ocpi_messages_outputType:
ulongAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
TrueRead back:
False
Default value:
None
ocpi_bytes_outputType:
ulongAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
TrueRead back:
False
Default value:
None
Worker Ports
inputProtocol:
complex_short_timed_sampleOptional:
False
outputProtocol:
complex_short_timed_sampleOptional:
False
Inputs:
Outputs:
Worker Ports
Inputs:
input:Type:
StreamInterfaceProtocol:
complex_short_timed_sample-protClockDirection:
inData width:
32
Outputs:
output:Type:
StreamInterfaceProtocol:
complex_short_timed_sample-protWorker EOF:
trueClock:
inputData width:
32
Utilisation
Utilization reporting is not currently implemented.