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:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
burst_det: Status — burst currently detected.Type:
boolAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
energy_out: Status — current burst-detector energy estimate.Type:
ulongAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
omega_current: Status — current timing-recovery interpolation interval.Type:
ushortAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
frame_locked: Status — frame synchroniser is locked.Type:
boolAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
path_metric: Status — Viterbi survivor path metric.Type:
ushortAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
cc_lpf_p_gain: 4th-power Costas PI proportional gain. 4th-power Costas PI proportional gain.Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
1
cc_lpf_i_gain: 4th-power Costas PI integral gain. 4th-power Costas PI integral gain.Type:
ulongAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
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:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
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:
ucharAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
22
cc_lpf_freeze: Freeze Costas loop (debug; output stays at current freq).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lpf_zero: Zero Costas loop accumulator (forces de-rotation NCO to 0 Hz offset).Type:
boolAccess:
Parameter:
FalseWritable:
TrueInitial:
FalseVolatile:
False
Default value:
false
cc_lock: Carrier-correction lock status (1 = locked).Type:
boolAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
cc_carrier_freq_word: Estimated carrier offset (signed NCO freq word; ÷ 2^32 × sample_rate = Hz).Type:
longAccess:
Parameter:
FalseWritable:
FalseInitial:
FalseVolatile:
True
Default value:
None
Ports
Inputs:
input: Complex baseband MSK IQ (complex_short_timed_sample).Protocol:
complex_short_timed_sample-protOptional:
False
Outputs:
output: Decoded frame bytes.Protocol:
uchar_timed_sample-protOptional:
False
sym_outProtocol:
complex_short_timed_sample-protOptional:
True
soft_q_outProtocol:
uchar_timed_sample-protOptional:
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
*_x16chain rather than instantiatingmsk_demodper channel.The 4th-power carrier corrector pulls in only about ±8 kHz; larger offsets must be removed upstream.
Testing
Tested platforms: None