[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:
@@ -29,17 +29,24 @@ const PI_ACC_THRESHOLD: u8 = 8;
|
|||||||
/// matches typically cost 0.6–1.2.
|
/// matches typically cost 0.6–1.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[n−1]).
|
// Tech 11: Gardner TED — e[n] = x_mid[n] · (x[n] − x[n−1]).
|
||||||
// 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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user