Complete signal processing pipeline from complex baseband to decoded PCM telemetry, verified against the 1965 NAA Study Guide (A-624): Core demod (Phase 1): - PM demodulator with carrier PLL recovery - 1.024 MHz subcarrier extractor (bandpass + downconvert) - BPSK demodulator with Costas loop + symbol sync - Convenience hier_block2 combining subcarrier + BPSK PCM frame processing (Phase 2): - 32-bit frame sync with Hamming distance correlator - SEARCH/VERIFY/LOCKED state machine, complement-on-odd handling - Frame demultiplexer (128-word, A/D voltage scaling) - AGC downlink decoder (15-bit word reassembly from DNTM1/DNTM2) Voice and analog (Phase 3): - 1.25 MHz FM voice subcarrier demod to 8 kHz audio - SCO demodulator for 9 analog sensor channels (14.5-165 kHz) Virtual AGC integration (Phase 4): - TCP bridge client with auto-reconnect and channel filtering - DSKY uplink encoder (VERB/NOUN/DATA command sequences) Top-level receiver (Phase 5): - usb_downlink_receiver hier_block2: one block, complex in, PDUs out - 14 GRC block YAML definitions for GNU Radio Companion - Example scripts for signal analysis and full-chain demo Infrastructure: - constants.py with all timing/frequency/frame parameters - protocol.py for sync word + AGC packet encode/decode - Synthetic USB signal generator for testing - 222 tests passing, ruff lint clean
60 lines
2.1 KiB
Python
60 lines
2.1 KiB
Python
"""
|
||
Apollo PM Demodulator — extracts phase modulation from complex baseband.
|
||
|
||
The spacecraft transmitter phase-modulates a 76.25 MHz carrier at 0.133 rad
|
||
peak deviation (7.6 degrees). After frequency multiplication (×30) to 2287.5 MHz
|
||
and downconversion to complex baseband at the receiver, this block recovers the
|
||
composite modulating signal containing all subcarriers.
|
||
|
||
At 0.133 rad, the small-angle approximation holds (sin(0.133) ≈ 0.1327,
|
||
<0.3% error), so the demodulated output is essentially linear with the
|
||
modulating signal.
|
||
|
||
Signal chain: complex baseband → carrier PLL → phase extraction → float output
|
||
|
||
Reference: IMPLEMENTATION_SPEC.md section 2.3
|
||
"""
|
||
|
||
from gnuradio import analog, blocks, gr
|
||
|
||
|
||
class pm_demod(gr.hier_block2):
|
||
"""Phase modulation demodulator with carrier recovery.
|
||
|
||
Inputs:
|
||
complex baseband (e.g., from SDR or usb_signal_gen)
|
||
|
||
Outputs:
|
||
float — demodulated composite signal containing all subcarriers
|
||
"""
|
||
|
||
def __init__(self, carrier_pll_bw: float = 0.02, sample_rate: float = 5_120_000):
|
||
gr.hier_block2.__init__(
|
||
self,
|
||
"apollo_pm_demod",
|
||
gr.io_signature(1, 1, gr.sizeof_gr_complex),
|
||
gr.io_signature(1, 1, gr.sizeof_float),
|
||
)
|
||
|
||
# Carrier tracking PLL — locks to the residual carrier in the PM signal.
|
||
# The PLL bandwidth needs to be narrow enough to track carrier drift
|
||
# but wide enough for acquisition. 0.02 rad/sample is a good default
|
||
# for the 5.12 MHz sample rate.
|
||
#
|
||
# PLL freq range: ±carrier_pll_bw * sample_rate / (2*pi) Hz
|
||
max_freq = carrier_pll_bw * 2.0
|
||
min_freq = -max_freq
|
||
self.pll = analog.pll_carriertracking_cc(carrier_pll_bw, max_freq, min_freq)
|
||
|
||
# Extract instantaneous phase: atan2(Im, Re)
|
||
self.phase = blocks.complex_to_arg(1)
|
||
|
||
# Connect: input → PLL → phase extraction → output
|
||
self.connect(self, self.pll, self.phase, self)
|
||
|
||
def get_carrier_pll_bw(self) -> float:
|
||
return self.pll.get_loop_bandwidth()
|
||
|
||
def set_carrier_pll_bw(self, bw: float):
|
||
self.pll.set_loop_bandwidth(bw)
|