MSK Demodulator (msk_demod)

Single-channel monolithic Opulent Voice MSK demodulator — the reference design from which the folded 16-channel chain was derived.

Function

msk_demod recovers Opulent Voice frames from a single complex baseband MSK stream. It performs the full receive pipeline in one worker: burst detection (energy gate), Gardner timing recovery, optional 4th-power carrier-offset correction (a Costas-style loop, compile-time enabled), differential soft decision, frame synchronisation on the sync word, and Viterbi decoding of the K=7 rate-1/2 code.

This worker is the reference / bring-up demodulator. For the 64-channel receiver, the equivalent functions are split into the folded chain — bd_x16, cr_x16, ted_x16, dsd_x16, fs_x16, msk_decode_x16 — which time-share one datapath across 16 channels. See the component library overview for the folded chain.

Volatile properties report demod status (burst_det, energy_out, omega_current, frame_locked, path_metric); the cc_lpf_* properties tune and report the 4th-power carrier-correction loop when it is synthesised in.

Interface

<!-- This is the spec file (OCS) for: msk_demod
     Add component spec attributes, like "protocol".
     Add property elements for spec properties.
     Add port elements for i/o ports -->
<ComponentSpec>
  <property name='init' type='bool' writable='true' default='false'/>
  <property name='burst_det' type='bool' volatile='true'/>
  <property name='energy_out' type='ulong' volatile='true'/>
  <property name='omega_current' type='ushort' volatile='true'/>
  <property name="frame_locked" type="bool" volatile="true"/>
  <property name="path_metric" type="ushort" volatile="true"/>

  <!-- 4th-power carrier offset correction (compile-time enabled via
       USE_CARRIER_CORRECTION constant in msk_demod.vhd; these properties
       tune the Costas loop at runtime and report status).  Active only when
       the corrector is synthesized in; with corrector disabled the loop
       inputs are tied off and the status outputs read constant. -->
  <property name='cc_lpf_p_gain' type='ulong' writable='true' default='1'
            description="4th-power Costas PI proportional gain"/>
  <property name='cc_lpf_i_gain' type='ulong' writable='true' default='1'
            description="4th-power Costas PI integral gain"/>
  <property name='cc_lpf_p_shift' type='uchar' writable='true' default='16'
            description="4th-power Costas PI proportional shift (R-shift after gain).
                         Default tuned on Charlie-to-Charlie OTA at ±8 kHz commanded
                         TX/RX offset; loop settles ~1 sec.  Larger = slower/quieter."/>
  <property name='cc_lpf_i_shift' type='uchar' writable='true' default='22'
            description="4th-power Costas PI integral shift (R-shift after gain).
                         Default tuned for Charlie-to-Charlie OTA tracking.  Rule of
                         thumb: i_shift = p_shift + 4 to 8.  i_shift>=26 essentially
                         freezes the integrator; original spec value of 30 was bad."/>
  <property name='cc_lpf_freeze' type='bool' writable='true' default='false'
            description="Freeze Costas loop (debug; output stays at current freq)"/>
  <property name='cc_lpf_zero' type='bool' writable='true' default='false'
            description="Zero Costas loop accumulator (forces de-rotation NCO to 0 Hz offset)"/>
  <property name='cc_lock' type='bool' volatile='true'
            description="Carrier-correction lock status (1 = locked)"/>
  <property name='cc_carrier_freq_word' type='long' volatile='true'
            description="Estimated carrier offset (signed NCO freq word; ÷ 2^32 × sample_rate = Hz)"/>
  <port name="input" producer="false" protocol="complex_short_timed_sample-prot"/>
  <port name="output" producer="true" protocol="uchar_timed_sample-prot"/>
  <!-- Debug: symbol-rate IQ output (timing-recovery output, before diff_detector).
       Feed to validate_soft.py or decode_iq.py for pipeline validation.
       Format: real(I)=bits[15:0], imag(Q)=bits[31:16] per protocol. -->
  <port name="sym_out" producer="true" protocol="complex_short_timed_sample-prot"
        optional="true"/>
  <!-- Debug: quantizer output (soft_q_d, the exact value fed to opv_frame_decoder).
       Sign-extended from 3-bit signed [-3..+3] to 8-bit signed [-3..+3], so
       byte values are 0xFD..0x03 (interpret as int8_t). One byte per symbol,
       gated by soft_q_valid_d (fires every symbol, including sync bits).
       Compare byte-for-byte against validate_soft.py's soft_q_all array. -->
  <port name="soft_q_out" producer="true" protocol="uchar_timed_sample-prot"
        optional="true"/>
</ComponentSpec>

Properties

  • init: Pulse high to reset demodulator state.

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • burst_det: Status — burst currently detected.

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • energy_out: Status — current burst-detector energy estimate.

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • omega_current: Status — current timing-recovery interpolation interval.

    • Type: ushort

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • frame_locked: Status — frame synchroniser is locked.

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • path_metric: Status — Viterbi survivor path metric.

    • Type: ushort

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • cc_lpf_p_gain: 4th-power Costas PI proportional gain. 4th-power Costas PI proportional gain.

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

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

    • Type: ulong

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 1

  • cc_lpf_p_shift: 4th-power Costas PI proportional shift (R-shift after gain). Default tuned on Charlie-to-Charlie OTA at ±8 kHz commanded TX/RX offset; loop settles ~1 sec. Larger = slower/quieter. Proportional right-shift after gain (default 16, OTA-tuned).

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 16

  • cc_lpf_i_shift: 4th-power Costas PI integral shift (R-shift after gain). Default tuned for Charlie-to-Charlie OTA tracking. Rule of thumb: i_shift = p_shift + 4 to 8. i_shift>=26 essentially freezes the integrator; original spec value of 30 was bad. Integral right-shift after gain (default 22; rule of thumb.

    • Type: uchar

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: 22

  • cc_lpf_freeze: Freeze Costas loop (debug; output stays at current freq).

    • 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 offset).

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: True

      • Initial: False

      • Volatile: False

    • Default value: false

  • cc_lock: Carrier-correction lock status (1 = locked).

    • Type: bool

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

  • cc_carrier_freq_word: Estimated carrier offset (signed NCO freq word; ÷ 2^32 × sample_rate = Hz).

    • Type: long

    • Access:

      • Parameter: False

      • Writable: False

      • Initial: False

      • Volatile: True

    • Default value: None

Ports

Inputs:

  • input: Complex baseband MSK IQ (complex_short_timed_sample).

    • Protocol: complex_short_timed_sample-prot

    • Optional: False

Outputs:

  • output: Decoded frame bytes.

    • Protocol: uchar_timed_sample-prot

    • Optional: False

  • sym_out

    • Protocol: complex_short_timed_sample-prot

    • Optional: True

  • soft_q_out

    • Protocol: uchar_timed_sample-prot

    • Optional: True

Implementations

  • msk_demod (HDL)

    Skeleton outline: Optional summary of the implementation of this worker. Anything before the next heading will be included as worker summary on component documentation page.

Dependencies

The dependencies to other elements in OpenCPI are:

  • ieee.std_logic_1164

  • ieee.numeric_std

Limitations

Limitations of msk_demod are:

  • Single channel. For many channels use the folded *_x16 chain rather than instantiating msk_demod per channel.

  • The 4th-power carrier corrector pulls in only about ±8 kHz; larger offsets must be removed upstream.

Testing

Tested platforms: None