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_detect from the dsp primitive 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 asserts frame_start on the first payload symbol of each detected frame and payload_valid for 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_sample protocol); 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-prot

    • Optional: False

Outputs:

  • output: Decoded OPV frames (uchar_timed_sample protocol); per frame, a metadata message carries abs_bin and a 134-byte sample message carries the payload.

    • Protocol: uchar_timed_sample-prot

    • Optional: 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: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • port_index: Which group of 16 channels (0..3 for a 64-channel channelizer).

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 0

  • cc_lpf_p_gain: 4th-power Costas PI proportional gain (shared across all 16 channels).

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

  • cc_lpf_i_gain: 4th-power Costas PI integral gain (shared).

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

  • cc_lpf_p_shift: 4th-power Costas PI proportional shift. Default tuned Charlie-to-Charlie OTA.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 16

  • cc_lpf_i_shift: 4th-power Costas PI integral shift. Rule of thumb: i_shift = p_shift + 4..8.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 22

  • cc_lpf_freeze: Freeze Costas loop (holds current frequency estimate).

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • cc_lpf_zero: Zero Costas loop accumulator (forces de-rotation NCO to 0 Hz).

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • frame_locked: Frame-sync LOCKED state per channel (slot 0..15).

    • Type: bool

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • cc_lock: Carrier-correction lock status per channel.

    • Type: bool

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • burst_det: Burst-detect output per channel.

    • Type: bool

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • energy_out: Burst-detect energy per channel.

    • Type: ulong

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • omega_current: Timing-loop NCO step per channel.

    • Type: ushort

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • path_metric: Viterbi path metric for most-recent decoded frame per channel.

    • Type: ushort

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • dropped_frames: Frames dropped because channel BRAM was still full on frame_start (should stay 0).

    • Type: ulong

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

Implementations

  • msk_demod_x16 (HDL)

    HDL implementation of the 16-channel OPV MSK demodulator: 16 parallel msk_demod_frontend instances 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 from msk_demod.hdl)

  • msk_demod_channel.vhd (shared source from msk_demod.hdl)

  • soft_quantizer.vhd (shared source from msk_demod.hdl)

  • frame_sync.vhd (shared source from msk_demod.hdl)

  • viterbi_decoder_k7_soft.vhd (shared source from msk_demod.hdl)

  • opv_frame_decoder.vhd (shared source from msk_demod.hdl)

  • burst_detect primitive from the dsp library

  • 32 BRAM18 primitives (inferred from the flat soft_bram array with ram_style="block")

Limitations

Limitations of msk_demod_x16 are:

  • Input is stalled during each Viterbi decode (input_out.take de-asserted while buf_ready is non-zero). This is safe for the file-based test environment but means the upstream channelizer must tolerate backpressure; the msk_decode_x16 variant 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.