Implement the transmit/generate side as streaming GNU Radio blocks, complementing the existing receive chain. Each block maps to a physical instrument on CuriousMarc's Keysight bench: pcm_frame_source - PCM bit stream generator (sync_block + FrameSourceEngine) nrz_encoder - bits to NRZ waveform (+1/-1) with upsampling bpsk_subcarrier_mod - NRZ x cos(1.024 MHz) BPSK modulator fm_voice_subcarrier_mod - 1.25 MHz FM test tone source pm_mod - phase modulator: exp(j * deviation * input) usb_signal_source - convenience wrapper wiring all blocks together Includes GRC YAML definitions for all blocks under [Apollo USB] category, 49 new tests (271 total, all passing), and a loopback test that validates the full TX->RX round trip including frame recovery with 30 dB AWGN.
60 lines
1.9 KiB
Python
60 lines
1.9 KiB
Python
"""
|
|
Apollo BPSK Subcarrier Modulator -- NRZ data onto 1.024 MHz subcarrier.
|
|
|
|
The transmit-side counterpart to bpsk_subcarrier_demod. Takes an NRZ baseband
|
|
waveform (+1/-1) and modulates it onto a 1.024 MHz cosine subcarrier via
|
|
simple multiplication: output(t) = nrz(t) * cos(2*pi*f_sc*t).
|
|
|
|
This is Bi-Phase Shift Keying (BPSK): the cosine phase flips 180 degrees
|
|
when the NRZ data changes sign.
|
|
|
|
On the real spacecraft, a 33522B AWG (or equivalent) generates this
|
|
BPSK-modulated subcarrier before summing with the voice subcarrier.
|
|
|
|
Reference: IMPLEMENTATION_SPEC.md section 4.2
|
|
"""
|
|
|
|
from gnuradio import analog, blocks, gr
|
|
|
|
from apollo.constants import PCM_SUBCARRIER_HZ, SAMPLE_RATE_BASEBAND
|
|
|
|
|
|
class bpsk_subcarrier_mod(gr.hier_block2):
|
|
"""BPSK modulator: NRZ float input -> BPSK subcarrier float output."""
|
|
|
|
def __init__(
|
|
self,
|
|
subcarrier_freq: float = PCM_SUBCARRIER_HZ,
|
|
sample_rate: float = SAMPLE_RATE_BASEBAND,
|
|
):
|
|
gr.hier_block2.__init__(
|
|
self,
|
|
"apollo_bpsk_subcarrier_mod",
|
|
gr.io_signature(1, 1, gr.sizeof_float),
|
|
gr.io_signature(1, 1, gr.sizeof_float),
|
|
)
|
|
|
|
self._subcarrier_freq = subcarrier_freq
|
|
self._sample_rate = sample_rate
|
|
|
|
# 1.024 MHz cosine subcarrier (continuous phase, maintained by sig_source)
|
|
self.carrier = analog.sig_source_f(
|
|
sample_rate, analog.GR_COS_WAVE, subcarrier_freq, 1.0, 0,
|
|
)
|
|
|
|
# Multiply NRZ data by subcarrier
|
|
self.mixer = blocks.multiply_ff(1)
|
|
|
|
# Connect: input (NRZ) -> mixer port 0, carrier -> mixer port 1 -> output
|
|
self.connect(self, (self.mixer, 0))
|
|
self.connect(self.carrier, (self.mixer, 1))
|
|
self.connect(self.mixer, self)
|
|
|
|
@property
|
|
def subcarrier_freq(self) -> float:
|
|
return self._subcarrier_freq
|
|
|
|
@property
|
|
def sample_rate(self) -> float:
|
|
return self._sample_rate
|