MSK Decode x16 Back-End (msk_decode_x16)

Decodes 16 channels of tagged OPV soft decisions into frames using a single time-shared Viterbi decoder, with no DSP front-end.

Tested platforms include xsim.

Design

msk_decode_x16 is the shared back-end decoder of the folded 16-channel OPV demodulator chain. It receives tagged soft decisions produced by the upstream front-end fold workers (cr_x16 → ted_x16 → dsd_x16 → fs_x16), collects them into per-channel double-buffered BRAM soft stores, and time-multiplexes a single opv_frame_decoder (Viterbi, rate-1/2 k=7) across all 16 channels at 16:1.

The input stream uses complex_short_timed_sample metadata/sample framing. Per payload symbol, fs_x16 emits a metadata message (4 words; the local channel index 0..15 is carried in byte 2 of the value field), followed by a one-sample message whose bit 16 is frame_start and whose low 3 bits are the 3-bit signed quantized soft decision. Sync symbols have already been discarded by fs_x16; every forwarded symbol is a payload symbol.

The BRAM soft store consists of 32 banks arranged as a flat 1-D array (bank = ch*2 + buf_sel, flat_addr = bank*4096 + symbol_index). Each bank holds 2144 nibbles (4-bit cells: 3-bit signed soft + sign-extension bit). The power-of-2 depth (4096 entries, only 0..2143 used) is required for Vivado to infer BRAM18 primitives; a non-power-of-2 depth of 2144 triggers Synth 8-7186 and causes the array to fall back to flip-flops. The flat 1-D organisation with a registered bank index is likewise required — an array-of-arrays with a dynamic outer index is seen as a non-memory variable and also falls back to flip-flops. 32 banks of 4K x 4-bit each map to 32 BRAM18 primitives (one per bank), fitting easily within ZC706 or ZCU102 resources.

The double-buffer scheme decouples collection from decode: while the scheduler reads and re-drives one bank to the Viterbi, the next frame for that channel collects into the other bank. A sample beat that would write into a bank still awaiting decode is stalled via backpressure on input_out.take. Metadata beats are never stalled.

The scheduler scans the 32 bank-ready flags in priority order (lowest bank index first), picks the first ready bank, and re-drives all 2144 symbols to the shared opv_frame_decoder at one symbol per clock. It accounts for the 2-cycle BRAM read latency through the SCHED_RD0 / SCHED_RD1 / SCHED_LAST1 / SCHED_LAST2 pipeline drain states.

Each decoded frame produces one output frame sequence: a 16-byte metadata message (uchar_timed_sample metadata opcode, carrying abs_bin = port_index*16 + local_ch in value byte 0), followed by a 134-byte sample message containing the decoded payload. The output FSM waits until a complete frame is buffered in the 256-entry output FIFO before beginning the sample message, ensuring EOM is asserted exactly on byte 133.

Four instances of this worker (port_index 0..3) cover all 64 channels of a 64-bin channelizer. The frame_merge4 worker merges their four output streams into a single DMA transfer to the host.

Latency at 100 MHz: re-drive burst is 2146 clocks per channel; Viterbi decode is approximately 70,881 clocks (~0.71 ms); 16 channels x 0.71 ms = 11.4 ms against a 40 ms frame period (28.5% utilisation).

Interface

<?xml version='1.0' encoding='utf-8'?>
<!-- msk_decode_x16: 16-channel OPV back-end decoder.  This is msk_demod_x16 with
     the entire per-channel DSP front-end removed (carrier recovery, matched
     filter / timing, diff detector, soft quantizer, frame_sync are now the
     folded workers cr_x16 -> ted_x16 -> dsd_x16 -> fs_x16).  It keeps the SHARED
     back-end: per-channel double-buffered soft BRAM collection, the 16:1
     scheduler, the single opv_frame_decoder (Viterbi), and the output framing.

     INPUT (tagged payload soft, from fs_x16): per payload symbol, metadata(value
       = local channel 0..15) then a 1-sample message with bit 16 = frame_start
       and the low 3 bits = the signed quantized soft.  Sync symbols were already
       dropped by fs_x16; every forwarded symbol is a payload symbol.
     OUTPUT (decoded frames): uchar_timed_sample, per frame metadata(value =
       abs_bin = port_index*16 + local ch) then 134 payload bytes.

     Instantiate 4x (port_index 0..3), one per channelizer output port, for 64
     channels. -->
<ComponentSpec>

  <property name='init' type='bool' writable='true' default='false'
            description="Pulse high to reset all collect buffers and decoder state"/>

  <property name='port_index' type='uchar' writable='true' default='0'
            description="Which group of 16 channels (0..3); abs_bin = port_index*16 + local ch"/>

  <!-- Per-channel volatile status (index = local slot 0..15) -->
  <property name='path_metric'    type='ushort' volatile='true' arraylength='16'
            description="Viterbi path metric for the most-recent decoded frame per channel"/>
  <property name='dropped_frames' type='ulong'  volatile='true' arraylength='16'
            description="Frames dropped because the channel collect buffer was still full (should stay 0)"/>

  <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: Tagged soft-decision stream from fs_x16 (complex_short_timed_sample protocol); carries per-symbol metadata + 3-bit signed soft decisions for 16 channels.

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Outputs:

  • output: Decoded OPV frames (uchar_timed_sample protocol); each frame is preceded by a metadata message carrying abs_bin and followed by 134 payload bytes.

    • Protocol: uchar_timed_sample-prot

    • Optional: False

Opcode Handling

On the input port, metadata opcodes are parsed to extract the local channel index from the value field. sample opcodes carry the soft decision and frame_start flag.

On the output port, metadata opcodes carry the absolute bin tag; sample opcodes carry the decoded payload bytes.

All other input opcodes are not forwarded and do not affect decoder state.

Properties

  • init: Pulse high to reset all collect buffers 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); abs_bin = port_index*16 + local ch.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 0

  • path_metric: Viterbi path metric for the 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 the channel collect buffer was still full (should stay 0).

    • Type: ulong

      • Array dimensions: ['16']

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

Implementations

  • msk_decode_x16 (HDL)

    HDL implementation of the 16-channel OPV back-end decoder: per-channel double-buffered BRAM soft collection and a single time-shared Viterbi decoder.

Dependencies

The dependencies for this worker are:

  • ieee.std_logic_1164

  • ieee.numeric_std

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

  • opv_frame_decoder.vhd (shared source from msk_demod.hdl; wraps the Viterbi and output framing)

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

Limitations

Limitations of msk_decode_x16 are:

  • The worker handles exactly 16 channels. Four instances are required for 64 channels; a single instance cannot be reconfigured for a different channel count without modifying N_CH and rebuilding.

  • The init property resets all collect buffers and the shared Viterbi simultaneously; any frame currently being decoded is lost.

  • The tag FIFO depth is 4 entries. More than 4 frames in simultaneous in-flight decode (which is impossible at 16:1 scheduling) would cause tag corruption.

  • BRAM inference requires Vivado and the ram_style="block" attribute; XSim will simulate correctly but uses LUTRAM, so BRAM resource counts are only meaningful in synthesis targets.

Testing

This component test suite uses the OpenCPI unit test framework. The unit test drives the tagged soft-decision input format produced by fs_x16 and verifies that all decoded frames appear on the output with correct metadata and payload contents.

Tested platforms: None

Component testing not completed.