Skip to content

Signal Architecture

The Apollo Unified S-Band (USB) system multiplexes voice, telemetry, and ranging onto a single 2287.5 MHz carrier using nested modulation layers. Understanding these layers — and the reasons behind each design choice — is the key to understanding what gr-apollo does and why its blocks are structured the way they are.

All parameters in this section are from the NAA Telecommunication Systems Study Guide (Course A-624).

The spacecraft transmitter begins with a stable carrier at 2287.5 MHz. Multiple information streams are combined onto this carrier through a two-level modulation scheme: subcarriers are first individually modulated with their data, then the composite subcarrier signal phase-modulates the RF carrier.

graph TD
    A["PCM Telemetry<br/>51.2 kbps NRZ"] -->|BPSK| B["1.024 MHz<br/>Subcarrier"]
    C["Voice Audio<br/>300-3000 Hz"] -->|FM<br/>+/-29 kHz dev| D["1.25 MHz<br/>Subcarrier"]
    E["Ranging Code<br/>PRN sequence"] --> F["Ranging<br/>Subcarrier"]

    B --> G["Composite<br/>Baseband Signal"]
    D --> G
    F --> G

    G -->|PM<br/>0.133 rad peak| H["2287.5 MHz<br/>RF Carrier"]

    H --> I["Transmitted<br/>Downlink"]

    style A fill:#2d5016,stroke:#4a8c2a
    style C fill:#2d5016,stroke:#4a8c2a
    style E fill:#2d5016,stroke:#4a8c2a
    style H fill:#1a3a5c,stroke:#3a7abd
    style I fill:#1a3a5c,stroke:#3a7abd

This is a frequency-division multiplexing system. The PCM subcarrier sits at 1.024 MHz, voice at 1.25 MHz, and ranging at its own frequency. Because these subcarriers are well-separated in frequency, the receiver can extract each one independently with bandpass filters. The entire system uses a single RF carrier — hence “Unified” S-Band.

The PM peak deviation of 0.133 rad (7.6 degrees) is one of the most important numbers in the system. It seems absurdly small — barely a wiggle on the carrier phase. That is the point.

At small modulation indices, the relationship between the modulating signal and the recovered phase is very nearly linear. The small-angle approximation sin(theta) ~= theta holds to within 0.3% at 0.133 rad:

Angle (rad)sin(angle)Error
0.1330.13260.29%
0.50.47944.1%
1.00.841515.9%

This linearity means the PM demodulator output is a faithful reproduction of the composite subcarrier signal. There is no need for pre-distortion or nonlinear correction — the receiver just extracts the phase and gets the subcarriers back, ready for filtering and demodulation.

The tradeoff is signal power. Most of the transmitted power remains in the carrier rather than the sidebands. The spacecraft burns roughly 20 watts of RF power (via the traveling-wave tube amplifier), but only a fraction of a watt ends up in each subcarrier. The Deep Space Network’s 26-meter dishes made up the difference with raw antenna gain.

Every timing parameter in the Apollo PCM system derives from a single 512 kHz master clock in the Central Timing Equipment (CTE). This is not a coincidence — it is the entire design philosophy. When all frequencies share a common root, the receiver can exploit integer relationships for synchronization.

graph TD
    M["512 kHz<br/>Master Clock"] --> A["51.2 kbps bit clock<br/>divide by 10"]
    M --> B["1.6 kbps bit clock<br/>divide by 320"]
    A --> C["6,400 words/sec<br/>8 bits/word"]
    B --> D["200 words/sec<br/>8 bits/word"]
    C --> E["50 frames/sec<br/>128 words/frame"]
    D --> F["1 frame/sec<br/>200 words/frame"]
    E --> G["1 subframe/sec<br/>50 frames"]

    M --> H["1.024 MHz subcarrier<br/>multiply by 2"]

    style M fill:#5c3a1a,stroke:#bd7a3a
    style H fill:#1a3a5c,stroke:#3a7abd

At high rate: 512,000 / 10 = 51,200 bits/sec. Divide by 8 bits/word = 6,400 words/sec. Divide by 128 words/frame = 50 frames/sec. Multiply 50 frames by the frame period and you get exactly 1 second per subframe.

The 1.024 MHz PCM subcarrier is 512 kHz times 2. This means exactly 20 subcarrier cycles fit in one bit period (1,024,000 / 51,200 = 20). The BPSK demodulator can use this integer relationship for more stable symbol timing recovery.

gr-apollo’s default baseband sample rate is 5.12 MHz — exactly 10 times the master clock. This choice is deliberate:

RelationshipValueInteger?
Samples per master clock cycle5,120,000 / 512,000 = 10Yes
Samples per PCM subcarrier cycle5,120,000 / 1,024,000 = 5Yes
Samples per bit (high rate)5,120,000 / 51,200 = 100Yes
Samples per voice subcarrier cycle5,120,000 / 1,250,000 = 4.096No

Every PCM-related rate divides evenly into the sample rate. This eliminates fractional sample offsets in the bit timing recovery loop, making synchronization faster and more reliable. The voice subcarrier does not divide evenly, but voice is FM — it tolerates timing imprecision far better than digital data.

The receiver follows the signal structure in reverse. Each modulation layer gets its own block, and the blocks chain together in the same order the signal was built:

graph LR
    A["Complex<br/>Baseband<br/>5.12 MHz"] --> B["pm_demod"]
    B --> C["subcarrier_extract<br/>1.024 MHz"]
    C --> D["bpsk_demod"]
    D --> E["pcm_frame_sync"]
    E --> F["pcm_demux"]

    B --> G["voice_subcarrier_demod<br/>1.25 MHz"]

    F -->|"frames PDU"| H["AGC Data"]
    F -->|"telemetry PDU"| I["Telemetry Words"]
    F -->|"agc_data PDU"| J["downlink_decoder"]
    J -->|"downlink PDU"| K["AGC Bridge"]

    G --> L["Audio Out<br/>8 kHz"]

    style A fill:#1a3a5c,stroke:#3a7abd
    style B fill:#3a1a5c,stroke:#7a3abd
    style C fill:#3a1a5c,stroke:#7a3abd
    style D fill:#3a1a5c,stroke:#7a3abd
    style E fill:#5c3a1a,stroke:#bd7a3a
    style F fill:#5c3a1a,stroke:#bd7a3a
    style G fill:#2d5016,stroke:#4a8c2a

The first four blocks form a streaming signal processing chain, each transforming the signal’s representation:

  1. pm_demod — Carrier PLL locks to the residual carrier, then atan2(Im, Re) extracts instantaneous phase. Output is a float representing the composite modulating signal.
  2. subcarrier_extract — Frequency-translating FIR filter shifts the 1.024 MHz subcarrier to DC and applies a 150 kHz bandpass (matching the spec’s 949-1099 kHz range). Output is complex baseband.
  3. bpsk_demod — Costas loop resolves phase, Mueller & Muller TED recovers symbol timing, binary slicer makes hard decisions. Output is one byte per bit (0 or 1).
  4. pcm_frame_sync — Sliding-window correlator finds the 32-bit sync word. Transitions through SEARCH, VERIFY, and LOCKED states. Emits complete frames as PDU messages.

The usb_downlink_receiver hierarchical block wires the PCM chain together as a convenience — connect complex baseband in, get telemetry PDUs out. For finer control over PLL bandwidths or to tap intermediate signals, the individual blocks can be used directly.

The Costas loop in bpsk_demod has a fundamental 180-degree phase ambiguity. A 2nd-order Costas loop locks to one of two stable equilibria that are 180 degrees apart. If it locks to the wrong one, every recovered bit is inverted.

The system resolves this at the frame sync layer rather than at the Costas loop. The 32-bit sync word has a known pattern: [5-bit A][15-bit core][6-bit B][6-bit frame ID]. The A, B, and core fields are fixed (the core complements on odd frames, but this is deterministic). If the correlator finds the complement of the expected pattern everywhere, it knows the Costas loop locked to the wrong phase, and can invert the bit stream.

This is a well-known technique in BPSK systems. It is more reliable than trying to resolve the ambiguity in the Costas loop itself, because the frame sync pattern provides a definitive answer rather than a probabilistic one.

The Apollo USB system maintains a coherent frequency relationship between uplink and downlink. The spacecraft’s receiver locks to the uplink at 2106.40625 MHz, and the transmitter generates its downlink carrier by multiplying the received frequency by 240/221:

2106.40625 MHz x 240/221 = 2287.5 MHz

This coherent turnaround allows the ground station to measure the two-way Doppler shift with extreme precision — the ratio is exact, so any frequency difference between transmitted uplink and received downlink is entirely due to spacecraft velocity. This is how NASA tracked the spacecraft’s range rate to centimeter-per-second precision using 1960s technology.

gr-apollo does not implement the coherent turnaround (it is a receiver, not a transponder), but the frequency plan explains why the numbers are what they are. The 2287.5 MHz downlink frequency is not arbitrary — it is locked to the uplink via a ratio that was carefully chosen to avoid ambiguities in the Doppler measurement.

gr-apollo’s TX blocks mirror the receive path. Where the receiver disassembles the signal layer by layer, the transmitter builds it up. Each TX block has a direct RX counterpart, and the internal architecture reflects this symmetry:

graph LR
    A["PCM Frame Source<br/>32-bit sync<br/>128 words/frame"] --> B["NRZ Encoder<br/>0/1 → +1/-1<br/>100 samp/bit"]
    B --> C["BPSK Mod<br/>× cos(1.024 MHz)"]
    C --> D["Σ"]

    E["Voice Mod<br/>FM → 1.25 MHz<br/>±29 kHz dev"] --> F["× 0.764"]
    F --> D

    D --> G["PM Mod<br/>exp(j·φ)"]
    G --> H["Complex<br/>Baseband<br/>5.12 MHz"]

    style A fill:#2d5016,stroke:#4a8c2a
    style B fill:#5c3a1a,stroke:#bd7a3a
    style C fill:#3a1a5c,stroke:#7a3abd
    style E fill:#2d5016,stroke:#4a8c2a
    style G fill:#1a3a5c,stroke:#3a7abd
    style H fill:#1a3a5c,stroke:#3a7abd

The PCM transmit path generates telemetry frames and modulates them onto the BPSK subcarrier:

  1. pcm_frame_source — Generates 128-word PCM frames with 32-bit sync words. Frame IDs cycle 1 through 50 (one subframe per second). Odd frames get a complemented sync core automatically. Dynamic payloads can be injected via the frame_data message port.
  2. nrz_encoder — Converts the bit stream (byte values 0/1) to a float NRZ waveform (+1.0/-1.0). Each bit is repeated for 100 samples at the default 5.12 MHz rate.
  3. bpsk_subcarrier_mod — Multiplies the NRZ data by a 1.024 MHz cosine carrier. This BPSK modulation flips the subcarrier phase 180 degrees on each bit transition — the inverse of the Costas loop recovery in bpsk_demod.

The usb_signal_source hierarchical block wires the entire TX chain together as a convenience — the transmit-side counterpart to usb_downlink_receiver. For scenarios that need finer control (like injecting external audio), the individual blocks can be assembled manually. The full_downlink_demo.py example shows this approach.

During pre-launch checkout and certain test configurations, the Apollo USB system switches from PM mode to wideband FM. In this mode, the PCM and voice subcarriers are replaced by 9 Subcarrier Oscillators (SCOs) that encode analog sensor voltages as FM tones:

graph TD
    A["Sensor 1\n0-5V DC"] -->|"SCO 1\n14.5 kHz"| G["Composite\nSCO Signal"]
    B["Sensor 5\n0-5V DC"] -->|"SCO 5\n52.5 kHz"| G
    C["Sensor 9\n0-5V DC"] -->|"SCO 9\n165 kHz"| G

    G -->|"FM\n500 kHz dev"| H["2287.5 MHz\nRF Carrier"]

    H --> I["Transmitted\nFM Downlink"]

    style A fill:#2d5016,stroke:#4a8c2a
    style B fill:#2d5016,stroke:#4a8c2a
    style C fill:#2d5016,stroke:#4a8c2a
    style H fill:#1a3a5c,stroke:#3a7abd
    style I fill:#1a3a5c,stroke:#3a7abd

The key differences from PM mode:

PropertyPM ModeFM Mode
Carrier modulationPhase (0.133 rad peak)Frequency (wideband)
Data formatDigital (PCM frames)Analog (voltage-to-frequency)
Subcarriers1.024 MHz BPSK + 1.25 MHz FM9 SCO tones (14.5-165 kHz)
DemodulationPhase extractionFrequency extraction

The FM mode blocks mirror the PM mode three-layer architecture:

graph LR
    subgraph "Layer 1: Carrier"
        A["fm_mod"] --> B["fm_demod"]
    end
    subgraph "Layer 2: Subcarrier"
        C["sco_mod\n(per channel)"] --> D["sco_demod\n(per channel)"]
    end
    subgraph "Layer 3: Convenience"
        E["fm_signal_source"] --> F["fm_downlink_receiver"]
    end

    style A fill:#1a3a5c,stroke:#3a7abd,color:#fff
    style B fill:#1a3a5c,stroke:#3a7abd,color:#fff
    style C fill:#5c3a1a,stroke:#bd7a3a,color:#fff
    style D fill:#5c3a1a,stroke:#bd7a3a,color:#fff
    style E fill:#2d5016,stroke:#4a8c2a,color:#fff
    style F fill:#2d5016,stroke:#4a8c2a,color:#fff

The fm_signal_source and fm_downlink_receiver convenience blocks wire the full chain together, just as usb_signal_source and usb_downlink_receiver do for PM mode. The FM receiver uses streaming float outputs (one per SCO channel) rather than PDU messages, since SCO telemetry is continuous analog data.

The downlink carries telemetry from spacecraft to ground. The uplink does the reverse — delivering commands from Mission Control to the spacecraft on 2106.40625 MHz.

The modulation scheme is the same family as the downlink (PM carrier with FM subcarriers), but the parameters are dramatically different. Where the downlink tiptoes along at 0.133 rad peak deviation, the uplink runs at 1.0 rad. This asymmetry is not a design inconsistency — it reflects a fundamental physical reality. The ground station has megawatt-class transmitters and 26-meter dish antennas. The spacecraft has a 20-watt traveling-wave tube and a dish measured in inches. The ground can afford the power overhead of higher modulation index; the spacecraft cannot waste a single fraction of a watt.

The uplink carries two information streams:

  • Command data — DSKY commands encoded as 15-bit AGC words, transmitted at 2 kbps NRZ on a 70 kHz FM subcarrier with +/-4 kHz deviation
  • Voice (optional) — Crew-directed audio on a 30 kHz FM subcarrier with +/-7.5 kHz deviation
graph LR
    subgraph "TX — Ground Station"
        A["DSKY Commands<br/>15-bit AGC words"] --> B["Word Serializer<br/>15 bits → NRZ<br/>2 kbps"]
        B --> C["NRZ Encoder<br/>0/1 → +1/-1"]
        C --> D["FM Mod<br/>70 kHz<br/>±4 kHz dev"]
        D --> E["Σ"]
        V1["Voice Audio<br/>300-3000 Hz"] -->|"FM<br/>30 kHz<br/>±7.5 kHz dev"| E
        E --> F["PM Mod<br/>1.0 rad peak"]
        F --> G["2106.4 MHz<br/>RF Carrier"]
    end

    G -->|"240/221<br/>turnaround"| H

    subgraph "RX — Spacecraft"
        H["RF Input<br/>2106.4 MHz"] --> I["PM Demod<br/>Carrier PLL"]
        I --> J["Extract<br/>70 kHz"]
        J --> K["FM Demod"]
        K --> L["Slicer<br/>2 kbps"]
        L --> M["Word<br/>Deserializer"]
        M --> N["AGC<br/>Commands"]
    end

    style A fill:#2d5016,stroke:#4a8c2a
    style V1 fill:#2d5016,stroke:#4a8c2a
    style G fill:#1a3a5c,stroke:#3a7abd
    style H fill:#1a3a5c,stroke:#3a7abd
    style F fill:#3a1a5c,stroke:#7a3abd
    style I fill:#3a1a5c,stroke:#7a3abd
    style N fill:#5c3a1a,stroke:#bd7a3a

The uplink transmit chain assembles commands into the modulated carrier:

  1. uplink_encoder — Accepts DSKY command PDUs and formats them as 15-bit AGC uplink words. The word format matches the Apollo AGC’s INLINK channel (channel 45) protocol.
  2. uplink_word_serializer — Serializes 15-bit words into a continuous NRZ bit stream at 2 kbps. Each bit is held for the full bit period.
  3. nrz_encoder — Converts the 0/1 byte stream to +1.0/-1.0 floats, identical to the downlink NRZ encoder.
  4. FM modulator — Frequency-modulates the NRZ data onto a 70 kHz subcarrier with +/-4 kHz deviation.
  5. PM modulator — Phase-modulates the composite subcarrier signal onto the carrier at 1.0 rad peak deviation.

The usb_uplink_source hierarchical block wires this chain together.

Voice and telemetry tell the ground station what the spacecraft is doing. Ranging tells them where it is. The Apollo USB system measures Earth-to-spacecraft distance by timing how long a known signal takes to make the round trip.

The ranging signal is a composite pseudo-random noise (PRN) code, transmitted by the ground station and transponded (retransmitted) by the spacecraft. The ground station correlates the received code against its own reference copy, and the time offset between them is the two-way light-time delay.

graph LR
    A["PRN Generator<br/>~994 kchip/s"] --> B["NRZ Encode<br/>0/1 → +1/-1"]
    B --> C["PM Mod onto<br/>Uplink Carrier"]
    C --> D["Spacecraft<br/>Transponder"]
    D --> E["PM Mod onto<br/>Downlink Carrier"]
    E --> F["Ground RX<br/>PRN Correlator"]
    F --> G["Range<br/>Measurement"]

    A --> H["Reference Copy<br/>(delayed)"]
    H --> F

    style A fill:#5c3a1a,stroke:#bd7a3a
    style D fill:#1a3a5c,stroke:#3a7abd
    style F fill:#3a1a5c,stroke:#7a3abd
    style G fill:#2d5016,stroke:#4a8c2a

The PRN code is not a single sequence — it is five component codes combined with Boolean logic:

CodeLength (chips)Type
CL2Clock (alternating 0,1)
X11Fixed pattern
A31Maximal-length LFSR
B63Maximal-length LFSR
C127Maximal-length LFSR

The combination logic produces one output chip per clock:

output = (NOT(X) AND majority(A, B, C)) XOR CL

where majority(A, B, C) outputs 1 when two or more inputs are 1. The combined code length is the product of the component lengths: 2 x 11 x 31 x 63 x 127 = 5,456,682 chips. At the chip rate of approximately 994 kchip/s, one full code period takes about 5.49 seconds.

The maximal-length LFSR sequences (A, B, C) give the composite code excellent autocorrelation properties — a sharp peak when aligned, low sidelobes everywhere else. This is what makes unambiguous range measurement possible even at the signal-to-noise ratios encountered at lunar distance.

Correlating against the full 5.4-million-chip code in one pass would be computationally ruinous, even by modern standards. The Apollo system uses a sequential strategy that exploits the composite structure:

  1. Correlate CL first (length 2) — resolves range to within half the CL period
  2. Correlate X (length 11) — refines within the CL ambiguity window
  3. Correlate A (length 31) — further refinement
  4. Correlate B (length 63) — further refinement
  5. Correlate C (length 127) — final resolution

Each stage resolves the ambiguity left by the previous one. The total number of trial positions searched is 2 + 11 + 31 + 63 + 127 = 234, rather than 5,456,682. This makes acquisition practical in seconds rather than hours.

The two-way range resolution is determined by the chip rate:

resolution = c / (2 x chip_rate) = 299,792,458 / (2 x 994,000) ~ 150.8 meters

This is the minimum distinguishable range difference. The actual measurement precision is much better than this, because the correlator interpolates between chips. NASA routinely achieved ranging precision on the order of 15 meters to the Moon — about one-tenth of a chip.

  • ranging_source — Generates the composite PRN chip stream by clocking the five component code generators and combining their outputs. Configurable chip rate (default ~994 kchip/s). Output is one byte per chip (0 or 1).
  • ranging_mod — NRZ-encodes the chip stream (+1/-1) and scales it for injection into the PM modulator’s composite baseband signal.