MSK Demod x16 (msk_demod_x16)
Full 16-channel OPV MSK demodulator: 16 parallel front-ends feeding one time-shared Viterbi decoder at 16:1.
Tested platforms include xsim.
Design
msk_demod_x16 is a resource-efficient folded implementation of 16
independent MSK demodulator chains sharing a single opv_frame_decoder
(Viterbi, rate-1/2 k=7). It is intended to consume exactly one output port of
a polyphase channelizer carrying 16 frequency bins.
The input protocol is complex_short_timed_sample. Each 16-sample sample
message carries one IQ pair per bin in positional order: word 0 (SOM) is bin 0,
word 15 (EOM) is bin 15, with Q in bits 31:16 and I in bits 15:0. No per-sample
metadata is used; slot identity is determined by position within the message.
The absolute channel identifier reported on the output is
abs_bin = port_index*16 + slot, allowing a 64-channel system to use four
instances (port_index 0..3) each consuming one of four channelizer ports.
The input demux process maintains a 4-bit position counter that resets on SOM.
Each sample word dispatches to the corresponding msk_demod_frontend instance
(fe_data_valid(slot) is asserted for exactly one cycle per word, with
fe_i_in / fe_q_in driven from the current word).
Sixteen msk_demod_frontend instances run in parallel. Each front-end
incorporates:
A fourth-power Costas carrier-correction loop (
carrier_correction_4p) with a 32-bit NCO, PI loop filter, and programmable proportional/integral gains (cc_lpf_p_gain/cc_lpf_i_gain) and shifts (cc_lpf_p_shift/cc_lpf_i_shift). The gains and shifts are shared across all 16 front-ends. Defaults are tuned for Charlie-to-Charlie over-the-air operation:p_shift=16,i_shift=22.A burst detector (
burst_detectfrom thedspprimitive library) that generates an energy-proportional squelch signal.A matched-filter timing-recovery channel (
msk_demod_channel) with a Gardner timing-error detector and proportional-integral loop filter.A 3-bit soft quantizer (
soft_quantizer).A frame synchronizer (
frame_sync) that assertsframe_starton the first payload symbol of each detected frame andpayload_validfor all subsequent payload symbols in VERIFY and LOCKED states.
The soft collect and back-end architecture is shared with msk_decode_x16.
Per-channel double-buffered BRAM soft stores (32 banks of 4K x 4-bit each,
mapped to 32 BRAM18 primitives via a flat 1-D array with ram_style="block")
accumulate the 2144 payload soft decisions per frame. When a buffer fills the
16:1 scheduler re-drives all 2144 symbols to the single opv_frame_decoder
at one symbol per clock. Input is stalled (input_out.take de-asserted) for
the duration of each Viterbi decode to prevent overrun.
Each decoded frame produces a 16-byte metadata message (abs_bin in value
byte 0) followed by a 134-byte payload sample message on the 8-bit
uchar_timed_sample output bus.
Timing at 100 MHz: Viterbi decode takes approximately 70,881 clocks (~0.71 ms); 16 channels x 0.71 ms = 11.4 ms against a 40 ms OPV frame period, yielding 28.5% Viterbi utilisation.
Interface
<?xml version='1.0' encoding='utf-8'?>
<!-- OCS for msk_demod_x16: 16-parallel-channel MSK demodulator.
Consumes one channelizer output port carrying 16 channels interleaved;
each IQ sample is preceded by a metadata(id=0, value=abs_bin 0..63) message.
Runs 16 parallel front-ends and time-shares one Viterbi decoder 16:1.
Emits decoded frames on uchar_timed_sample output, each frame tagged
with metadata(id=0, value=port_index*16 + local_slot). -->
<ComponentSpec>
<!-- Init / control -->
<property name='init' type='bool' writable='true' default='false'
description="Pulse high to reset all front-ends and decoder state"/>
<!-- Port routing: selects which 16-bin group this instance handles.
Absolute bin = port_index*16 + local_slot (local_slot 0..15).
Samples with abs_bin outside [port_index*16 .. port_index*16+15] are dropped. -->
<property name='port_index' type='uchar' writable='true' default='0'
description="Which group of 16 channels (0..3 for a 64-channel channelizer)"/>
<!-- Shared carrier-correction loop tuning (one set; feeds all 16 front-ends) -->
<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 loop (holds current frequency estimate)"/>
<property name='cc_lpf_zero' type='bool' writable='true' default='false'
description="Zero Costas loop accumulator (forces de-rotation NCO to 0 Hz)"/>
<!-- Per-channel volatile status arrays (sequencelength=16, index = local slot 0..15) -->
<property name='frame_locked' type='bool' volatile='true' arraylength='16'
description="Frame-sync LOCKED state per channel (slot 0..15)"/>
<property name='cc_lock' type='bool' volatile='true' arraylength='16'
description="Carrier-correction lock status per channel"/>
<property name='burst_det' type='bool' volatile='true' arraylength='16'
description="Burst-detect output per channel"/>
<property name='energy_out' type='ulong' volatile='true' arraylength='16'
description="Burst-detect energy per channel"/>
<property name='omega_current' type='ushort' volatile='true' arraylength='16'
description="Timing-loop NCO step per channel"/>
<property name='path_metric' type='ushort' volatile='true' arraylength='16'
description="Viterbi path metric for most-recent decoded frame per channel"/>
<property name='dropped_frames' type='ulong' volatile='true' arraylength='16'
description="Frames dropped because channel BRAM was still full on frame_start (should stay 0)"/>
<!-- Ports -->
<port name="input" producer="false" protocol="complex_short_timed_sample-prot"/>
<port name="output" producer="true" protocol="uchar_timed_sample-prot"/>
</ComponentSpec>
Ports
Inputs:
input: Channelizer IQ stream (complex_short_timed_sampleprotocol); 16-sample positional messages, one IQ pair per bin per message (Q in bits 31:16, I in bits 15:0).Protocol:
complex_short_timed_sample-protOptional:
False
Outputs:
output: Decoded OPV frames (uchar_timed_sampleprotocol); per frame, a metadata message carriesabs_binand a 134-byte sample message carries the payload.Protocol:
uchar_timed_sample-protOptional:
False
Opcode Handling
On the input port, only sample opcodes are processed. The position
counter resets on SOM and increments on each subsequent word; SOM is the
complex_short_timed_sample_sample_op_e opcode with som='1'. All
non-sample opcodes (including metadata) are ignored and do not advance the
position counter.
On the output port, metadata opcodes carry the per-frame absolute bin
tag. sample opcodes carry the decoded payload bytes.
Properties
init: Pulse high to reset all front-ends and decoder state.Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
port_index: Which group of 16 channels (0..3 for a 64-channel channelizer).Type:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
0
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 loop (holds current frequency estimate).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lpf_zero: Zero Costas loop accumulator (forces de-rotation NCO to 0 Hz).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
frame_locked: Frame-sync LOCKED state per channel (slot 0..15).Type:
boolArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
cc_lock: Carrier-correction lock status per channel.Type:
boolArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
burst_det: Burst-detect output per channel.Type:
boolArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
energy_out: Burst-detect energy per channel.Type:
ulongArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
omega_current: Timing-loop NCO step per channel.Type:
ushortArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
path_metric: Viterbi path metric for most-recent decoded frame per channel.Type:
ushortArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
dropped_frames: Frames dropped because channel BRAM was still full on frame_start (should stay 0).Type:
ulongArray dimensions:
['16']
Access:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
Implementations
msk_demod_x16(HDL)HDL implementation of the 16-channel OPV MSK demodulator: 16 parallel
msk_demod_frontendinstances and one time-shared Viterbi decoder at 16:1.
Dependencies
The dependencies for this worker are:
ieee.std_logic_1164
ieee.numeric_std
msk_demod_frontend.vhd(local; bundles carrier correction, burst detect, timing recovery, soft quantizer, and frame sync into one entity)
carrier_correction_4p.vhd(shared source frommsk_demod.hdl)
msk_demod_channel.vhd(shared source frommsk_demod.hdl)
soft_quantizer.vhd(shared source frommsk_demod.hdl)
frame_sync.vhd(shared source frommsk_demod.hdl)
viterbi_decoder_k7_soft.vhd(shared source frommsk_demod.hdl)
opv_frame_decoder.vhd(shared source frommsk_demod.hdl)
burst_detectprimitive from thedsplibrary32 BRAM18 primitives (inferred from the flat
soft_bramarray withram_style="block")
Limitations
Limitations of msk_demod_x16 are:
Input is stalled during each Viterbi decode (
input_out.takede-asserted whilebuf_readyis non-zero). This is safe for the file-based test environment but means the upstream channelizer must tolerate backpressure; themsk_decode_x16variant removes this limitation with double-buffering.The Costas carrier-correction gains and shifts are shared across all 16 front-ends. Individual per-channel tuning is not supported.
The worker handles exactly 16 channels. Four instances are required to cover a 64-bin channelizer.
BRAM inference requires Vivado and the
ram_style="block"attribute; XSim simulates correctly but resource counts apply to synthesis targets only.
Testing
This component test suite uses the OpenCPI unit test framework. The unit test drives IQ samples through the complete front-end and back-end and verifies decoded OPV frame payloads against a software reference.
Tested platforms: None
Component testing not completed.