[fix](trx-rds): tune RDS parameters for maximum sensitivity

- RRC_ALPHA 0.75→0.50: narrower noise BW, ~0.6 dB SNR gain
- COSTAS_KI 3.5e-7: maintain ζ≈0.68 (1e-6 caused loop instability)
- Soft confidence: use biphase_i.abs() instead of full vector magnitude
  so OSD confidence is aligned with bit-decision sign; suppresses
  false groups under noise with residual Costas phase error
- OSD(2) in locked mode: corrects ≤2-bit errors after block sync
- Search mode: hard decode only for Block A; OSD(1) in search yielded
  ~13% false Block A rate per bit, letting wrong clock candidates
  accumulate false groups as fast as the correct candidate
- Incumbent candidate tracking (best_candidate_idx): the winning
  candidate updates best_state at equal score; challengers need strictly
  higher score; best_score tracks incumbent even on no-state-change
  groups so challengers can't leapfrog on a single false group
- blocks_to_chips: add NRZI (NRZ-Mark) pre-encoding so the differential
  biphase decoder recovers actual data bits rather than XOR-of-pairs
- Add blocks_to_chips_round_trips_all_groups test: verifies all 16 blocks
  across 4 PS segments round-trip correctly without BPSK modulation

[fix](trx-backend-soapysdr): lower pilot lock threshold for weak-signal RDS

- PILOT_LOCK_THRESHOLD 0.25→0.20, add PILOT_LOCK_ONSET=0.30 constant
- Pilot reference engages at coherence ≥0.36 (was ≥0.45)

WIP: end_to_end_clean_signal_decodes_ps still failing (13/15 pass).
Decoder skips segment 2 due to ISI from rectangular test chips through
RRC receive filter. chips_to_rds_signal needs RRC pulse shaping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-27 01:46:01 +01:00
parent 8b310c184b
commit 57e88b3590
4 changed files with 700 additions and 147 deletions
+99 -122
View File
@@ -1,137 +1,114 @@
# RDS Reception Improvements — WIP
# RDS Parameter Tuning — Work in Progress
Research into improving RDS demodulation robustness for two scenarios:
adjacent channel interference (ACI) and weak signal (low SNR).
## Goal
Maximum sensitivity (weak-signal decode) with zero false positive PI decodes.
## Signal Pipeline
## Changes Made
### `src/decoders/trx-rds/src/lib.rs`
#### Constants tuned
- `RRC_ALPHA = 0.50` (was 0.75) — narrower noise bandwidth, ~0.6 dB SNR gain
- `COSTAS_KI = 3.5e-7` — loop damping ζ≈0.68, well-damped (1e-6 caused instability)
- `PI_ACC_THRESHOLD = 2` — accumulate 2 Block A observations before committing PI
#### Soft confidence fix
In `Candidate::process_sample`, the soft confidence passed to `push_bit_soft` is now
`biphase_i.abs()` (was full vector magnitude). This aligns confidence with the bit
decision sign and prevents OSD(2) from false-decoding noise when the Costas loop
has residual phase error.
#### OSD(2) in locked mode (kept)
`decode_block_soft` performs OSD(2): hard decode → all 26 single-bit flips → all
325 two-bit flip pairs. Only active in locked mode; sequential B→C→D block-type
gating limits false positives.
#### Search mode: hard decode only
Removed OSD(1) from Block A acquisition (search mode). With OSD(1), ~13% of
random 26-bit words would falsely pass the Block A test per bit, allowing wrong
clock-phase candidates to accumulate false groups as fast as the correct candidate
accumulates real ones. Hard decode reduces the false Block A rate to ~0.5%.
#### Candidate selection: incumbent tracking
Added `best_candidate_idx: Option<usize>` to `RdsDecoder`. The incumbent (winning)
candidate can always update `best_state` at equal score (its `ps_seen`/`rt_seen`
arrays accumulate coherently). A challenger must achieve a strictly higher score to
take over. The incumbent's `best_score` is also updated when it returns `None`
(no state change) so challengers cannot leapfrog with a single false group.
#### Test fixes
- `blocks_to_chips`: added NRZI (NRZ-Mark) pre-encoding. The differential biphase
decoder computes `bit = input_bit XOR prev_input_bit`; without NRZI the recovered
bits were XOR-of-consecutive-bits, not the original data.
- `decode_block_soft_rejects_three_bit_error`: removed (OSD(2) legitimately finds
distance-2 codewords; `pure_noise_produces_zero_pi_decodes` is the real guard).
- New test: `blocks_to_chips_round_trips_all_groups` — verifies round-trip decode
of all 16 blocks across all 4 PS segments without BPSK modulation.
### `src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs`
- `PILOT_LOCK_THRESHOLD = 0.20` (was 0.25) — pilot reference enabled at lower coherence
- Added `PILOT_LOCK_ONSET = 0.30` constant (was hardcoded 0.4)
- `pilot_lock` ramp: `((pilot_coherence - PILOT_LOCK_ONSET) / 0.2).clamp(0.0, 1.0)`
— pilot reference engages at coherence ≥ 0.36 instead of ≥ 0.45
## Test Status
```
Antenna → IF filter → FM discriminator → 57 kHz BPF → Costas PLL →
Symbol timing → Manchester decode → Biphase decode →
Block sync → (26,16) FEC → Group assembly
cargo test -p trx-rds
```
## Prioritized Implementation Plan
13/15 passing:
- ✅ decode_block_recognizes_valid_offsets
- ✅ decode_block_soft_corrects_single_bit_error
- ✅ decode_block_soft_corrects_two_bit_error_osd2
- ✅ block_decode_rate_osd1_vs_osd2
- ✅ decode_block_soft_prefers_least_costly_flip
- ✅ full_group_with_two_bit_errors_in_each_locked_block
- ✅ pi_accumulation_corrects_weak_pi_after_threshold
- ✅ decoder_emits_ps_and_pty_from_group_0a
- ✅ rrc_tap_dc_gain
- ✅ pure_noise_produces_zero_pi_decodes
- ✅ end_to_end_with_pilot_reference_decodes_pi
- ✅ end_to_end_noisy_signal_snr_10db_decodes_pi
- ✅ costas_tracks_without_diverging_on_clean_signal
- ✅ blocks_to_chips_round_trips_all_groups ← new, proves chip encoding correct
- ❌ end_to_end_clean_signal_decodes_ps ← remaining failure
| # | Technique | Scenario | Complexity | Expected Gain |
|---|---|---|---|---|
| 1 | RRC matched filter | ACI | Low | Largest measured (32 vs 18/38 stations in empirical test) |
| 2 | 19 kHz pilot ×3 → 57 kHz carrier reference | Weak signal | Medium | 36 dB carrier phase noise reduction |
| 3 | Erasure declaration + erasure decoding | Weak signal | Low | 13 dB |
| 4 | 8th-order 57 kHz IIR bandpass filter | ACI | Low | Pre-filters ACI energy before Costas loop |
| 5 | Costas `tanh` soft phase detector | Weak signal | Trivial | ~1 dB |
| 6 | LLR accumulation across repeated groups | Weak signal | Low | √N SNR per repetition |
| 7 | Chase-II soft-decision block decoder | Both | Medium | 12 dB |
| 8 | OSD(2) block decoder | Both | Medium | 23 dB over hard-decision |
| 9 | IF CMA blind equalizer (pre-demodulation) | ACI | High | 710 dB ACI protection ratio improvement |
## Remaining Bug: `end_to_end_clean_signal_decodes_ps`
---
## Technique Notes
### 1. RRC Matched Filter
RDS uses known BPSK pulse shaping per IEC 62106. The receiver should apply a Root Raised
Cosine (RRC) matched filter rather than a generic FIR lowpass. The 2024 GNU Radio demodulator
comparison showed RRC-based decoders (Bloessl's gr-rds) decoded 32/38 real stations vs. 18/38
for plain FIR lowpass filter decoders — the single largest measured improvement.
- Roll-off factor: 1.0 per standard; experiment with 0.350.8 for sharper stopband
- Operate at 2375 Hz chip rate (Manchester doubles the symbol rate from 1187.5 baud)
### 2. 19 kHz Pilot ×3 Carrier Reference
Instead of running a Costas loop autonomously on the noisy 57 kHz subcarrier, multiply the
clean 19 kHz stereo pilot tone by 3 to produce a phase-coherent 57 kHz reference. The pilot
sits at much higher SNR than the RDS subcarrier. `redsea`'s own issue tracker identifies this
as the dominant weak-signal failure mode; patent CN113132039A addresses the same problem.
Without pilot lock, fall back to a Costas loop with a `tanh` soft phase detector (see #5).
### 3. Erasure Decoding
When `|LLR|` for a bit is below a confidence threshold, declare it an **erasure** (known-position
error) rather than a hard ±1 decision. The (26,16) RDS block code corrects:
- 5 random bit errors (hard decision)
- **10 erasures** — 2× improvement at no added decoder complexity
Requires propagating LLRs from the symbol detector to the syndrome decoder instead of hard
bits. The Group 0B PI cross-check (PI appears in both Block A and Block C) gives free
erasure resolution on the most critical field.
### 4. 8th-Order 57 kHz IIR Bandpass Filter
Hardware RDS chips (e.g. SAA6579) place an 8th-order bandpass filter at 57 kHz before the
carrier recovery stage. In software, an equivalent high-order IIR or long FIR centered at
57 kHz with ±4 kHz passband and steep roll-off attenuates adjacent-channel energy that bleeds
into the MPX spectrum after FM discrimination. Insert this stage immediately after FM
demodulation and before the Costas loop.
### 5. Costas `tanh` Soft Phase Detector
Replace the hard-slicer phase error `Re(z) * Im(z)` in the Costas loop with
`tanh(Re(z)/σ) * Im(z)`. This is the ML-derived phase error estimator and approaches the
Cramér-Rao bound at low SNR. Trivial code change, useful whenever the pilot reference (#2)
is unavailable.
### 6. LLR Accumulation Across Repeated Groups
RDS transmits the same data repeatedly (PI: every group every ~87.6 ms; PS name: ≥5 groups/sec).
Accumulate per-bit LLRs across N repetitions of the same field before decoding:
### Symptom
The decoder sees segments 0 (×8 candidates) and 1 (×1), then jumps to segment 3,
skipping segment 2. `ps_seen` never has all four flags set in the winning candidate,
so `program_service` is never assembled.
### Diagnosis (from temporary `eprintln!` in `process_group`)
```
LLR_acc[i] += LLR_n[i] // for known-repeated bit positions
[DBG] process_group pi=0x9801 seg=0 (×8 — all 8 clock candidates decode seg 0)
[DBG] process_group pi=0x9801 seg=1 (×1)
[DBG] process_group pi=0x9801 seg=3 (×1 — seg 2 skipped!)
[DBG] process_group pi=0x9BB2 seg=3 (false positive)
```
SNR improves as √N (3 dB per 4× accumulations). With ~11 PI observations per second, 1 second
of accumulation is feasible before display latency becomes noticeable.
Segment 2 is consistently skipped. The `blocks_to_chips_round_trips_all_groups`
test confirms the chip stream is correct for all 16 blocks. The issue is therefore
in the RRC filter / symbol clock / biphase chain between seg 1 and seg 2.
### 7. Chase-II Soft-Decision Block Decoder
### Key observation
- `blocks_to_chips_round_trips_all_groups` passes — chip encoding is correct
- The FIR block size is 256 samples, introducing a 255-sample startup delay where
the filter returns `(0.0, 0.0)` before the first batch is flushed
- The test signal uses rectangular chip pulses; the receiver RRC filter expects
RRC-shaped transmit pulses for zero ISI. Rectangular × RRC ≠ raised cosine.
The Chase-II algorithm generates `2^(2t)` hard-decision decoder trials by flipping the
`2t` least-reliable bit positions (identified by smallest `|LLR|`), then picks the trial with
minimum Euclidean metric. For the RDS (26,16) code with t=2: only 4 trials. The 26-bit block
size makes this extremely fast.
### Hypothesis
A rectangular chip pulse convolved with an RRC matched filter produces ISI. Over
time this may cause the effective chip sampling point to drift, eventually missing
the correct window for Block A of segment 2. `chips_to_rds_signal` should probably
pre-shape each chip with an RRC pulse to make it a proper matched-filter test.
Expected gain: ~12 dB over hard-decision decoding.
### 8. OSD(2) Block Decoder
Ordered Statistics Decoding at order 2 approaches ML performance for short linear block codes.
Procedure for the (26,16) code:
1. Sort all 26 bit positions by `|LLR|` descending (most to least reliable).
2. Gaussian-eliminate the 10×26 parity-check matrix to bring the most reliable positions into
systematic form.
3. Enumerate order-2 test patterns (all pairs from the least-reliable positions).
4. Select the minimum Euclidean-metric codeword.
The matrix is only 10×26 — Gaussian elimination is trivial. Expected gain: ~23 dB over
hard-decision decoding, ~0.51.5 dB over Chase-II.
Reference: Fossorier & Lin, "Soft decision decoding of linear block codes based on ordered
statistics," IEEE Trans. Inf. Theory, 1995.
### 9. IF CMA Blind Equalizer
FM signals are constant-envelope, so the Constant Modulus Algorithm can equalize the IF signal
without training data, driven by the cost `||s(t)|² - 1|`. A 2004 JSSC paper reports
**710 dB improvement in adjacent channel protection ratio** using a 6th-order blind equalizer
applied to the digitized IF signal before FM demodulation. This is the most powerful ACI
technique but requires operating at IF sample rates and handling the full pre-demodulation
signal. Implement last.
---
## Key References
- [site2241.net 2024 demodulator comparison](https://www.site2241.net/january2024.htm) — empirical RRC vs. FIR data
- [PySDR RDS end-to-end example](https://pysdr.org/content/rds.html) — complete Costas + M&M + block sync pipeline
- [gr-rds / Bloessl](https://github.com/alexmrqt/fm-rds) — best-performing open source implementation
- [redsea CHANGES.md](https://github.com/windytan/redsea/blob/master/CHANGES.md) — real-world weak signal bug history
- [Fossorier & Lin OSD](https://www.semanticscholar.org/paper/Soft-decision-decoding-of-linear-block-codes-based-Fossorier-Lin/2fde1414cd33dacfb96b7b0d5bbbe74b803704da) — foundational soft decoding for cyclic codes
- [IEEE: Digital RDS demodulation in FM subcarrier systems (2004)](https://ieeexplore.ieee.org/abstract/document/1412732)
- [ResearchGate: DSP-based digital IF AM/FM car radio receiver (JSSC 2004)](https://www.researchgate.net/publication/4050364_A_DSP-based_digital_if_AMFM_car-radio_receiver) — CMA equalizer, 710 dB ACI improvement
- [Information-Reduced Carrier Synchronization of BPSK/QPSK](https://www.researchgate.net/publication/254651395_Information-Reduced_Carrier_Synchronization_of_BPSK_and_QPSK_Using_Soft_Decision_Feedback)
- IEC 62106 (RDS standard), IEC 62634 (receiver measurements)
### Next steps
1. Fix `chips_to_rds_signal` to apply RRC pulse shaping per chip so that
RRC × RRC = raised cosine → zero ISI at the decoder's sampling instants.
2. Alternatively verify that the FIR startup zeros are not permanently skewing
the clock candidate phases.