[fix](trx-rds): stabilise Gardner TED to fix PI fluctuation and weak-signal regression

The Gardner TED (Tech 11) caused PI instability and worse weak-signal
pickup due to three issues:

1. Loop gains too aggressive: noise×noise error products at low SNR
   injected sub-chip jitter that degraded OSD soft confidence and PI
   LLR accumulation.  Reduced Kp from 4e-4→1.5e-4, Ki from 8e-8→2e-8
   (loop BW 0.11→0.053 Hz).

2. TED active during acquisition: before any group is decoded, the
   error signal is unreliable.  Now lock-gated (score >= 1) so the
   TED only engages after the first successful group decode, when
   timing is already close.  During acquisition, the 8-candidate
   architecture with fixed clocks provides adequate timing coverage.

3. Slow power estimate convergence: ted_power_est took ~420 ms to
   settle (0.999 alpha), causing the TED to over-steer during startup.
   Now uses 0.995 alpha (~84 ms convergence).

Additionally, when TED is gated off, the integrator decays toward zero
so stale corrections from a previous strong-signal period don't persist.

https://claude.ai/code/session_01KcVUcQQXrFyFA9NEjLhr9J
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-27 10:45:23 +00:00
committed by Stan Grams
parent e44e616ab8
commit 54ca86a93d
+37 -17
View File
@@ -29,17 +29,24 @@ const PI_ACC_THRESHOLD: u8 = 8;
/// matches typically cost 0.61.2. /// matches typically cost 0.61.2.
const OSD_MAX_FLIP_COST: f32 = 0.45; const OSD_MAX_FLIP_COST: f32 = 0.45;
/// Tech 11 — Gardner TED proportional gain (per chip, after power normalisation). /// Tech 11 — Gardner TED proportional gain (per chip, after power normalisation).
/// Together with GARDNER_KI these form a type-2 PLL with damping ratio /// Reduced from 4e-4 to 1.5e-4 to lower jitter at marginal SNR while still
/// ζ = Kp / (2·√Ki) ≈ 0.707 and natural frequency ωn = √Ki ≈ 2.83e-4 rad/chip /// tracking crystal offsets up to ~100 ppm. The narrower transient response
/// (loop BW ≈ 0.11 Hz at 2375 chips/s). /// is acceptable because the 8-candidate architecture covers the timing
const GARDNER_KP: f32 = 4e-4; /// acquisition range; TED only needs to track slow drift once locked.
const GARDNER_KP: f32 = 1.5e-4;
/// Tech 11 — Gardner TED integral gain (per chip, after power normalisation). /// Tech 11 — Gardner TED integral gain (per chip, after power normalisation).
/// Tracks crystal offsets (typically < 100 ppm) while the narrow loop BW /// Reduced from 8e-8 to 2e-8. Together with GARDNER_KP these give
/// keeps jitter low at ≤ 5 dB SNR. /// ζ ≈ 0.75 and ωn ≈ 1.4e-4 rad/chip (loop BW ≈ 0.053 Hz at 2375 chips/s).
const GARDNER_KI: f32 = 8e-8; const GARDNER_KI: f32 = 2e-8;
/// Tech 11 — maximum clock_inc change per chip (fraction of nominal). /// Tech 11 — maximum clock_inc change per chip (fraction of nominal).
/// ±1 % corresponds to ±23.75 Hz pull-in range at 2375 chips/s. /// ±1 % corresponds to ±23.75 Hz pull-in range at 2375 chips/s.
const GARDNER_MAX_FREQ_CORR_FRAC: f32 = 0.01; const GARDNER_MAX_FREQ_CORR_FRAC: f32 = 0.01;
/// Tech 11 — Gardner TED power estimate convergence time constant.
/// Faster than the previous 0.999/0.001 (now 0.995/0.005) so the power
/// estimate settles in ~200 chips (~84 ms) instead of ~1000 chips (~420 ms).
/// This reduces the startup transient where incorrect normalisation causes
/// the TED to over-steer.
const GARDNER_POWER_ALPHA: f32 = 0.995;
/// Tech 5 — Costas loop proportional gain for acquisition (per sample). /// Tech 5 — Costas loop proportional gain for acquisition (per sample).
const COSTAS_KP: f32 = 8e-4; const COSTAS_KP: f32 = 8e-4;
/// Tech 5 — Costas loop integral gain for acquisition (per sample). /// Tech 5 — Costas loop integral gain for acquisition (per sample).
@@ -326,7 +333,7 @@ struct Candidate {
prev_chip_i: f32, prev_chip_i: f32,
/// Tech 11: Gardner TED PI-loop integrator state. /// Tech 11: Gardner TED PI-loop integrator state.
ted_integrator: f32, ted_integrator: f32,
/// Tech 11: running estimate of chip I power, used for error normalisation. /// Tech 11: running estimate of chip I signal power, used for error normalisation.
ted_power_est: f32, ted_power_est: f32,
} }
@@ -373,8 +380,8 @@ impl Candidate {
prev_chip_i: 0.0, prev_chip_i: 0.0,
ted_integrator: 0.0, ted_integrator: 0.0,
// Start at 1.0 so the first normalised error is bounded (≤ signal // Start at 1.0 so the first normalised error is bounded (≤ signal
// amplitude). The estimate decays toward the true chip power over // amplitude). The estimate converges toward the true chip power
// the first few hundred chips via the 0.999/0.001 leaky average. // over the first ~200 chips via the GARDNER_POWER_ALPHA leaky average.
ted_power_est: 1.0, ted_power_est: 1.0,
} }
} }
@@ -400,14 +407,20 @@ impl Candidate {
self.mid_chip_pending = true; self.mid_chip_pending = true;
// Tech 11: Gardner TED — e[n] = x_mid[n] · (x[n] x[n1]). // Tech 11: Gardner TED — e[n] = x_mid[n] · (x[n] x[n1]).
// Normalise by a running power estimate so the loop bandwidth is //
// independent of the RDS subcarrier level within the composite signal. // Lock-gated: the TED only adjusts clock_inc after the candidate has
// ted_power_est starts at 1.0 so the first normalised error is bounded; // decoded at least one full group (score >= 1). Before that point
// it decays toward the true chip power over the first ~1000 chips. // (search mode, or freshly locked without a group), the error signal
self.ted_power_est = 0.999 * self.ted_power_est // is unreliable — noise×noise products dominate at low SNR — and the
+ 0.001 * (i * i + self.prev_chip_i * self.prev_chip_i) * 0.5; // resulting jitter degrades biphase soft values, OSD confidence, and
// PI LLR accumulation. Fixed-clock operation is more stable during
// acquisition because the 8-candidate architecture covers the timing
// search space via phase offsets.
let chip_power = (i * i + self.prev_chip_i * self.prev_chip_i) * 0.5;
self.ted_power_est = GARDNER_POWER_ALPHA * self.ted_power_est
+ (1.0 - GARDNER_POWER_ALPHA) * chip_power;
let max_corr = self.nominal_clock_inc * GARDNER_MAX_FREQ_CORR_FRAC; let max_corr = self.nominal_clock_inc * GARDNER_MAX_FREQ_CORR_FRAC;
if self.ted_power_est > 1e-10 { if self.ted_power_est > 1e-10 && self.score >= 1 {
let ted_err = self.mid_chip_i * (i - self.prev_chip_i) / self.ted_power_est; let ted_err = self.mid_chip_i * (i - self.prev_chip_i) / self.ted_power_est;
// Anti-windup: clamp the integrator so it cannot accumulate beyond // Anti-windup: clamp the integrator so it cannot accumulate beyond
// the correction ceiling even during prolonged large-error transients. // the correction ceiling even during prolonged large-error transients.
@@ -423,6 +436,13 @@ impl Candidate {
self.nominal_clock_inc * (1.0 - GARDNER_MAX_FREQ_CORR_FRAC), self.nominal_clock_inc * (1.0 - GARDNER_MAX_FREQ_CORR_FRAC),
self.nominal_clock_inc * (1.0 + GARDNER_MAX_FREQ_CORR_FRAC), self.nominal_clock_inc * (1.0 + GARDNER_MAX_FREQ_CORR_FRAC),
); );
} else {
// Below SNR gate: freeze the TED loop and decay the integrator
// toward zero so the clock drifts back toward nominal rate.
// This prevents the integrator from holding a stale correction
// from a previous strong-signal period that no longer applies.
self.ted_integrator *= 0.999;
self.clock_inc = self.nominal_clock_inc + self.ted_integrator;
} }
self.prev_chip_i = i; self.prev_chip_i = i;