Tune Demodulator Parameters
The gr-apollo demodulation chain has several parameters that control how aggressively each stage tracks the signal. The defaults work well for clean synthetic signals, but real-world recordings and noisy captures often need adjustment.
This guide walks through each tunable parameter, explains what it controls, and gives practical guidance on when and how to change it.
Parameter Overview
Section titled “Parameter Overview”| Parameter | Default | Block | What It Controls |
|---|---|---|---|
carrier_pll_bw | 0.02 | pm_demod | Carrier tracking loop bandwidth (rad/sample) |
bpsk_loop_bw | 0.045 | bpsk_demod | Costas loop + symbol sync bandwidth |
subcarrier_bw | 150,000 Hz | subcarrier_extract | Bandpass filter width around 1.024 MHz |
max_bit_errors | 3 | pcm_frame_sync | Hamming distance threshold for sync word match |
bit_rate | 51,200 | pcm_frame_sync | PCM bit rate (high or low) |
All of these are constructor arguments to usb_downlink_receiver, or can be set on individual blocks when building a custom flowgraph.
Carrier PLL Bandwidth
Section titled “Carrier PLL Bandwidth”The carrier_pll_bw parameter controls how quickly the PLL in pm_demod locks onto the residual carrier and tracks frequency drift. It is specified in radians per sample at the 5.12 MHz baseband rate.
| Value | Behavior | Use When |
|---|---|---|
| 0.005 | Very narrow — slow acquisition, best noise rejection | Weak signal, steady carrier |
| 0.02 | Default — balanced tracking and noise | General-purpose decoding |
| 0.05 | Wide — fast acquisition, admits more noise | Strong signal, rapid frequency changes |
| 0.10 | Very wide — immediate lock, noisy output | Initial acquisition sweep, testing |
Adjusting the Carrier PLL
Section titled “Adjusting the Carrier PLL”from apollo.usb_downlink_receiver import usb_downlink_receiver
receiver = usb_downlink_receiver( carrier_pll_bw=0.01, # Narrow for weak signals)from apollo.pm_demod import pm_demod
pm = pm_demod(carrier_pll_bw=0.01, sample_rate=5_120_000)
# Can also adjust at runtime:pm.set_carrier_pll_bw(0.03)BPSK Loop Bandwidth
Section titled “BPSK Loop Bandwidth”The bpsk_loop_bw parameter sets the bandwidth for both the Costas carrier-recovery loop and the Mueller & Muller symbol timing recovery inside bpsk_demod. It affects how well the demodulator tracks phase variations in the 1.024 MHz BPSK subcarrier.
| Value | Behavior | Use When |
|---|---|---|
| 0.01 | Very tight tracking | Clean signal, minimal phase noise |
| 0.045 | Default | General-purpose, synthetic signals |
| 0.08 | Loose tracking | Noisy signal with phase jitter |
| 0.12 | Very loose | Severely degraded signal, testing only |
The symbol sync loop bandwidth is automatically derived as loop_bw * 0.5:
# Inside bpsk_demod.__init__:self.sym_sync = digital.symbol_sync_cc( digital.TED_MUELLER_AND_MULLER, self._sps, # samples per symbol loop_bw * 0.5, # timing loop BW = half the Costas BW 1.0, # damping factor 1.0, # TED gain 1.5, # max deviation (samples) 1, # output samples per symbol bpsk_constellation,)Subcarrier Bandwidth
Section titled “Subcarrier Bandwidth”The subcarrier_bw parameter sets the passband width of the frequency-translating FIR filter in subcarrier_extract. The default 150 kHz matches the Apollo PCM bandpass filter specification (949—1099 kHz).
| Value | Behavior | Trade-off |
|---|---|---|
| 80,000 Hz | Very narrow | Rejects adjacent subcarriers but may clip sideband energy |
| 150,000 Hz | Default (spec) | Matches the original hardware BPF |
| 200,000 Hz | Wide | More tolerant of frequency offset, but admits more noise |
| 250,000 Hz | Very wide | Use only if the subcarrier frequency is uncertain |
from apollo.subcarrier_extract import subcarrier_extract
sc = subcarrier_extract( center_freq=1_024_000, bandwidth=200_000, # Wider than default sample_rate=5_120_000,)Frame Sync Bit Error Threshold
Section titled “Frame Sync Bit Error Threshold”The max_bit_errors parameter controls how many bits in the 26-bit static portion of the sync word (the 5-bit A field + 15-bit core + 6-bit B field) can differ from the reference and still count as a match. The 6-bit frame ID field is not included in the correlation.
| Value | Behavior | Trade-off |
|---|---|---|
| 0 | Exact match only | No false locks, but very slow acquisition on noisy signals |
| 3 | Default | Tolerates 3 bit errors in the 26-bit pattern |
| 5 | Permissive | Faster acquisition, but higher chance of false sync |
| 8 | Very permissive | Testing only — will produce many false frames |
from apollo.pcm_frame_sync import FrameSyncEngine
engine = FrameSyncEngine( bit_rate=51200, max_bit_errors=2, # Stricter than default)Frame Sync State Machine
Section titled “Frame Sync State Machine”The sync engine uses a three-state machine that provides additional protection against false locks:
stateDiagram-v2
[*] --> SEARCH
SEARCH --> VERIFY : Sync match found (Hamming distance <= max_bit_errors)
VERIFY --> LOCKED : verify_count consecutive hits (default 2)
VERIFY --> SEARCH : miss_limit consecutive misses (default 3)
LOCKED --> LOCKED : Continuous sync matches
LOCKED --> SEARCH : miss_limit consecutive misses
Even with a permissive max_bit_errors, the VERIFY stage requires multiple consecutive sync hits at the expected frame spacing before declaring lock.
High Rate vs. Low Rate
Section titled “High Rate vs. Low Rate”Apollo PCM telemetry operates at two bit rates, both derived from the 512 kHz master clock:
| Parameter | High Rate | Low Rate |
|---|---|---|
| Bit rate | 51,200 bps | 1,600 bps |
| Clock divider | 512 kHz / 10 | 512 kHz / 320 |
| Words per frame | 128 | 200 |
| Frames per second | 50 | 1 |
| Frame period | ~20 ms | 1 s |
from apollo.usb_downlink_receiver import usb_downlink_receiver
receiver = usb_downlink_receiver( bit_rate=51200,)from apollo.usb_downlink_receiver import usb_downlink_receiver
receiver = usb_downlink_receiver( bit_rate=1600,)Troubleshooting by Symptom
Section titled “Troubleshooting by Symptom”Use this table to identify which parameter to adjust based on what you observe:
| Symptom | Likely Cause | Parameter to Adjust |
|---|---|---|
| No frames decoded at all | Carrier PLL not locking | Increase carrier_pll_bw to 0.05+ |
| Frames appear then disappear | BPSK loop tracking noise | Decrease bpsk_loop_bw to 0.02 |
| Low sync confidence values | Bit errors in sync word | Increase max_bit_errors to 4-5 |
| Noisy decoded voltage values | Too much noise in passband | Decrease subcarrier_bw to 100,000 |
| PLL acquires then drifts | Carrier PLL too narrow for drift | Increase carrier_pll_bw slightly |
| Rapid false frame emissions | Sync threshold too permissive | Decrease max_bit_errors to 1-2 |
| ”Frame too short” errors | Wrong bit rate selected | Switch between 51200 and 1600 |
Recommended Starting Points
Section titled “Recommended Starting Points”For a synthetic test signal at 30 dB SNR, the defaults work as-is:
receiver = usb_downlink_receiver( carrier_pll_bw=0.02, bpsk_loop_bw=0.045, subcarrier_bw=150_000, max_bit_errors=3, bit_rate=51200,)For a weak or noisy real-world recording, start with these and iterate:
receiver = usb_downlink_receiver( carrier_pll_bw=0.05, # Wider for acquisition bpsk_loop_bw=0.03, # Tighter to reject noise subcarrier_bw=120_000, # Narrower to reduce noise floor max_bit_errors=4, # More tolerant of bit errors bit_rate=51200,)