16-Channel Folded Carrier Recovery (cr_x16)
Applies a 4th-power Costas carrier-recovery loop to 16 MSK channels simultaneously by time-sharing one physical datapath across all channels.
Tested platforms include xsim.
Design
cr_x16 is the first front-end fold in the 64-channel MSK demodulator chain.
It is the time-shared equivalent of 16 parallel carrier_correction_4p instances
(as used in the monolithic msk_demod worker), collapsed into one physical
datapath with per-channel loop state held in 16-entry RAMs.
The worker consumes a positional sample-rate stream produced by the channelizer and decimation stage. Each message carries exactly 16 complex samples, one per channel: the sample arriving with SOM is assigned to channel 0, and the sample arriving with EOM is assigned to channel 15. At a nominal 100 MHz clock and 216.8 kHz frame rate this leaves approximately 445 idle clock cycles between consecutive 16-sample frames.
Because the frame is sparse, only one physical copy of the full Costas loop datapath is required. Channels inject into it in round-robin slot order (0, 1, …, 15) and each channel’s cross-sample loop state — 4x NCO phase accumulator, derotation NCO phase accumulator, 4x frequency word (PI output), PI integral accumulator, IIR low-pass-filter accumulator, and lock detector accumulator — is read from and written back to a 16-entry RAM indexed by the channel id that travels with the in-flight data through the pipeline. The pipeline is 9 stages deep; because this is far shorter than the 16-channel round (16 clocks minimum), every channel’s state is fully committed before that channel’s next sample arrives, making the fold bit-identical to 16 independent parallel loops.
The loop error path follows the same arithmetic as carrier_correction_4p:
the input IQ is raised to the 4th power by two successive complex squarers
(16-bit inputs, full-precision intermediate products, MSB-slice truncation
between stages), mixed against a 4x tracking NCO whose phase is looked up in a
shared combinational sin_cos_lut ROM (10-bit phase, 12-bit sinusoid), the
resulting cross-product forms the phase error, which is filtered by a first-order
IIR low-pass (shift by LPF_ALPHA = 10) and then driven through a PI loop
filter with configurable proportional gain cc_lpf_p_gain / shift
cc_lpf_p_shift and integral gain cc_lpf_i_gain / shift cc_lpf_i_shift.
The PI output updates the per-channel 4x NCO frequency word.
The derotation path is independent and shorter (2 pipeline stages). On every
accepted input sample the 1x derotation NCO phase (derived by dividing the 4x
frequency word by 4) is looked up in a second sin_cos_lut instance, the raw
IQ is multiplied by cos/sin, and the result is pushed into a 128-deep output FIFO.
The FIFO decouples the fixed-latency derotation pipeline from downstream
backpressure and also acts as the guard that prevents accepting new samples when
there is insufficient output room.
The output stream is identically framed to the input: 16-sample positional
messages (SOM on slot 0, EOM on slot 15) carrying derotated IQ, ready to feed
ted_x16 or any carrier-correction-free downstream worker.
Per-channel lock status (cc_lock, 16-element boolean array) and estimated
1x carrier offset NCO frequency word (cc_freq_word, 16-element signed long
array) are exposed as volatile read-only properties indexed by channel position.
Loop gain is shared across all 16 channels; the cc_lpf_freeze and
cc_lpf_zero control properties freeze or zero all per-channel PI integrators
simultaneously. Defaults cc_lpf_p_shift = 16 and cc_lpf_i_shift = 22
were tuned for Charlie-to-Charlie over-the-air operation.
Interface
<?xml version='1.0' encoding='utf-8'?>
<!-- cr_x16: 16-channel FOLDED 4th-power Costas carrier recovery / derotation.
Consumes the channelizer/decimate3 POSITIONAL stream: one 16-sample
`complex_short_timed_sample` message per frame (SOM on position 0, EOM on
position 15); the sample at position k belongs to channel k. Applies the
same 4th-power Costas carrier-offset correction as msk_demod's
carrier_correction_4p, but TIME-SHARES one physical datapath across all 16
channels instead of 16 parallel copies, with per-channel loop state held in
16-entry RAMs. Emits the derotated IQ on an identically-framed positional
stream (16 samples/message, SOM..EOM), ready to feed a carrier-correction-
LESS msk_demod_x16.
Why this exists: the 16 parallel carrier_correction_4p blocks are the
dominant LUT cost (~21k of msk_demod_x16's 76k). Folding collapses them to
one datapath + state RAM, which is what lets four x16 chains (64 channels)
fit a ZCU102. The fold is bit-identical to 16 parallel instances because
the stream is sparse (16 samples/frame, ~445 idle cycles between frames) and
the loop latency (~6 cycles) is far below the 16-channel round, so each
channel's loop state fully settles before its next sample, exactly as it
does in the parallel front-ends.
Tuning properties mirror msk_demod_x16's cc_* set (one shared set feeds all
16 channels). -->
<ComponentSpec>
<!-- Init / control -->
<property name='init' type='bool' writable='true' default='false'
description="Pulse high to reset the folded datapath and all 16 per-channel loop states"/>
<!-- Shared 4th-power Costas PI loop tuning (one set; applies to all 16 channels) -->
<property name='cc_lpf_p_gain' type='ulong' writable='true' default='1'
description="4th-power Costas PI proportional gain (shared across all 16 channels)"/>
<property name='cc_lpf_i_gain' type='ulong' writable='true' default='1'
description="4th-power Costas PI integral gain (shared)"/>
<property name='cc_lpf_p_shift' type='uchar' writable='true' default='16'
description="4th-power Costas PI proportional shift. Default tuned Charlie-to-Charlie OTA."/>
<property name='cc_lpf_i_shift' type='uchar' writable='true' default='22'
description="4th-power Costas PI integral shift. Rule of thumb: i_shift = p_shift + 4..8."/>
<property name='cc_lpf_freeze' type='bool' writable='true' default='false'
description="Freeze Costas loops (hold current per-channel frequency estimates)"/>
<property name='cc_lpf_zero' type='bool' writable='true' default='false'
description="Zero Costas loop accumulators (force all de-rotation NCOs to 0 Hz)"/>
<!-- Per-channel volatile status arrays (index = position/channel 0..15) -->
<property name='cc_lock' type='bool' volatile='true' arraylength='16'
description="Carrier-recovery lock status per channel (position 0..15)"/>
<property name='cc_freq_word' type='long' volatile='true' arraylength='16'
description="Estimated 1x carrier offset NCO frequency word per channel (signed)"/>
<!-- Ports: positional 16-sample IQ in, derotated positional 16-sample IQ 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: Carrier-uncorrected positional IQ stream from the channelizer/decimator.Protocol:
complex_short_timed_sample-protOptional:
False
Outputs:
output: Derotated positional IQ stream (same framing), ready forted_x16.Protocol:
complex_short_timed_sample-protOptional:
False
Opcode Handling
cr_x16 processes complex_short_timed_sample sample opcodes only. All
other opcodes (including metadata) are consumed from the input without producing
output words; the EOF opcode is propagated to the output after the FIFO drains.
Properties
init: Pulse high to reset the folded datapath and all 16 per-channel loop states.Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lpf_p_gain: 4th-power Costas PI proportional gain (shared across all 16 channels).Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1
cc_lpf_i_gain: 4th-power Costas PI integral gain (shared).Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1
cc_lpf_p_shift: 4th-power Costas PI proportional shift. Default tuned Charlie-to-Charlie OTA.Type:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
16
cc_lpf_i_shift: 4th-power Costas PI integral shift. Rule of thumb: i_shift = p_shift + 4..8.Type:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
22
cc_lpf_freeze: Freeze Costas loops (hold current per-channel frequency estimates).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lpf_zero: Zero Costas loop accumulators (force all de-rotation NCOs to 0 Hz).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lock: Carrier-recovery lock status per channel (position 0..15).Type:
boolArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
cc_freq_word: Estimated 1x carrier offset NCO frequency word per channel (signed).Type:
longArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
Implementations
cr_x16(HDL)Time-shared (folded) implementation of a 4th-power Costas carrier-recovery and derotation loop operating across 16 MSK channels with one physical datapath.
Dependencies
Dependencies of cr_x16 are:
ieee.std_logic_1164
ieee.numeric_std
ocpi.types(OpenCPI type definitions)
sin_cos_lut(combinational sin/cos ROM from the projectdspprimitive library — the same instance used bycarrier_correction_4p)
Limitations
Limitations of cr_x16 are:
Loop gain properties (
cc_lpf_p_gain,cc_lpf_i_gain,cc_lpf_p_shift,cc_lpf_i_shift) are shared across all 16 channels; per-channel tuning is not supported.The input framing is fixed at exactly 16 samples per message (positional encoding); arbitrary-length sample streams are not supported.
Lock detection (
cc_lock) uses a first-order IIR energy tracker on the filtered phase error; the lock threshold is a compile-time constant and is not software-configurable.
Testing
The component unit test drives the positional input with a 16-channel complex sinusoidal stimulus, varying carrier offsets per channel, and verifies that the derotated output magnitude and residual phase error converge to within expected bounds after a settling period.
Tested platforms: None