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 establishi_prev/q_prev.WAIT_MID— waits for the mid-symbol crossing to latchi_mid/q_mid.WAIT_DEC— waits for the next decision overflow. At overflow the Gardner errore = 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 toWAIT_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 fromcr_x16.Protocol:
complex_short_timed_sample-protOptional:
False
Outputs:
output: Tagged symbol stream — per symbol: metadata(id=0, value=channel).Protocol:
complex_short_timed_sample-protOptional:
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:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
omega_nominal: Nominal timing-NCO step = 2^16/SPS (16384 for SPS=4).Type:
ushortAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
16384
tl_p_gain: Gardner PI proportional gain (shared).Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1
tl_i_gain: Gardner PI integral gain (shared).Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1
tl_p_shift: Gardner PI proportional shift (Kp). Default per timing_loop guide.Type:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
15
tl_i_shift: Gardner PI integral shift (Ki). 30 = proportional-only (integral quiescent).Type:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
30
tl_freeze: Freeze the PI integrators (hold omega per channel during silence).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
omega_current: Current closed-loop timing-NCO step per channel.Type:
ushortArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
sym_count: Symbols emitted per channel since reset.Type:
ulongArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
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_nominalmust be set to2^16 / SPSbefore 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 reducetl_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