feat(pps): preserve loop dynamics across nav periods
This commit is contained in:
@@ -55,6 +55,7 @@
|
||||
#endif
|
||||
#define GPS_NAV_BITS_PER_WEEK 30240000u
|
||||
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
|
||||
#define GPS_NAV_BITS_PER_SUBFRAME 300u
|
||||
/* Keep integrity statistics active, but do not let leave-one-out FDE
|
||||
* replace the full-WLS position until the real measurement-noise model
|
||||
* is calibrated. The live stationary test produced a 22.7% exclusion
|
||||
@@ -83,6 +84,8 @@
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
|
||||
#elif ((GPS_NAV_BITS_PER_WEEK % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide 30240000 GPS navigation bits per week"
|
||||
#elif ((GPS_NAV_BITS_PER_SUBFRAME % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide the 300-bit LNAV subframe"
|
||||
#endif
|
||||
_Static_assert((GPS_NAV_ENABLE_FDE == 0u) || (GPS_NAV_ENABLE_FDE == 1u),
|
||||
"GPS_NAV_ENABLE_FDE must be zero or one");
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#define GPS_PPS_QUALITY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define GPS_PPS_BASELINE_UPDATE_PERIOD_SEC 0.200
|
||||
|
||||
typedef enum {
|
||||
GPS_PPS_PROFILE_HOLDOVER = 0,
|
||||
@@ -29,6 +32,8 @@ typedef struct {
|
||||
gps_pps_profile_t profile;
|
||||
gps_pps_profile_t candidate;
|
||||
unsigned int candidate_updates;
|
||||
unsigned int upgrade_updates;
|
||||
unsigned int degrade_updates;
|
||||
} gps_pps_quality_selector_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -43,13 +48,17 @@ typedef struct {
|
||||
} gps_pps_loop_profile_t;
|
||||
|
||||
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector);
|
||||
void gps_pps_quality_selector_init_period(
|
||||
gps_pps_quality_selector_t* selector,
|
||||
double update_period_sec);
|
||||
|
||||
/*
|
||||
* Select a bounded loop profile from timing geometry, code/carrier quality
|
||||
* and integrity. Upgrades require sustained evidence and degradations require
|
||||
* three consecutive observations. A single rejected WLS epoch is skipped
|
||||
* without resetting the loop; sustained loss enters holdover, while the
|
||||
* independent reference watchdog remains the final stale-data guard.
|
||||
* and integrity. Upgrades and degradations require sustained evidence; their
|
||||
* update counts are scaled from the proven 200 ms timing when period-aware
|
||||
* initialization is used. A single rejected WLS epoch is skipped without
|
||||
* resetting the loop; sustained loss enters holdover, while the independent
|
||||
* reference watchdog remains the final stale-data guard.
|
||||
*/
|
||||
gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
gps_pps_quality_selector_t* selector,
|
||||
@@ -57,6 +66,22 @@ gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
bool* profile_changed_out);
|
||||
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile);
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
|
||||
gps_pps_profile_t profile,
|
||||
double update_period_sec);
|
||||
|
||||
/*
|
||||
* Map the proven 200 ms discrete poles to another update period. The pole
|
||||
* exponentiation preserves the current loop's continuous-time settling and
|
||||
* damping while returning exactly the legacy coefficients at 200 ms.
|
||||
*/
|
||||
bool gps_pps_loop_coefficients(
|
||||
gps_pps_loop_profile_t parameters,
|
||||
double update_period_sec,
|
||||
double clock_frequency_hz,
|
||||
int64_t* kp_q46_out,
|
||||
int64_t* ki_q46_out,
|
||||
uint32_t* max_slew_q32_out);
|
||||
const char* gps_pps_profile_name(gps_pps_profile_t profile);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -58,7 +58,9 @@ bool gps_pps_reference_bias_drift(
|
||||
* linear oscillator drift. The Doppler WLS clock-drift state predicts the
|
||||
* bias at the new capture epoch; only the residual bias noise is low-passed.
|
||||
* If Doppler is temporarily unavailable, a bounded, slower finite-difference
|
||||
* drift estimate keeps the filter usable in the robust profile.
|
||||
* drift estimate keeps the filter usable in the robust profile. All three
|
||||
* baseline per-update gains are converted from the actual elapsed time, so
|
||||
* changing the NAV solve period does not change their real-time poles.
|
||||
*
|
||||
* COARSE is returned during initial settling, a reference-datum change, or
|
||||
* after a gap too long for bounded Doppler prediction. The independent
|
||||
|
||||
@@ -1844,7 +1844,9 @@ void gps_startup_initialization(unsigned int nsp_tick)
|
||||
GPS_NAV_ENABLE_FDE);
|
||||
gps_init_acq_tables();
|
||||
gps_pps_init(nsp_tick);
|
||||
gps_pps_quality_selector_init(&gps_pps_quality_selector);
|
||||
gps_pps_quality_selector_init_period(
|
||||
&gps_pps_quality_selector,
|
||||
(double)nsp_tick * (double)GPS_NAV_SOLVE_TIMER_TICK_MS / 1000.0);
|
||||
gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
gps_pps_status_log_decimator = 0u;
|
||||
gps_pps_reference_svid_valid = false;
|
||||
|
||||
@@ -65,7 +65,8 @@ enum {
|
||||
};
|
||||
|
||||
#define GPS_PPS_VERSION_ADAPTIVE_LOOP 0x00020200u
|
||||
#define GPS_PPS_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
|
||||
#define GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
|
||||
#define GPS_PPS_REFERENCE_TIMEOUT_PERIODS 3u
|
||||
|
||||
static volatile gps_pps_regs_t* const gps_pps =
|
||||
(volatile gps_pps_regs_t*)BSP_GPS_PPS_BASE;
|
||||
@@ -77,6 +78,7 @@ static bool gps_pps_force_coarse;
|
||||
static bool gps_pps_reference_committed;
|
||||
static bool gps_pps_holdover;
|
||||
static uint64_t gps_pps_last_reference_counter;
|
||||
static uint64_t gps_pps_reference_timeout_ticks;
|
||||
static gps_pps_time_filter_t gps_pps_time_filter;
|
||||
|
||||
static uint64_t gps_pps_read_u64(
|
||||
@@ -94,19 +96,30 @@ static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int6
|
||||
*hi = (uint32_t)(encoded >> 32);
|
||||
}
|
||||
|
||||
static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
|
||||
static double gps_pps_update_period_sec(void)
|
||||
{
|
||||
const double pi = 3.14159265358979323846;
|
||||
gps_pps_loop_profile_t parameters = gps_pps_loop_profile(profile);
|
||||
const double update_period_sec = (gps_pps_nav_solve_tick != 0u) ?
|
||||
((double)gps_pps_nav_solve_tick * 0.020) : 0.200;
|
||||
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
|
||||
const double q46 = 70368744177664.0;
|
||||
int64_t kp_q46 = (int64_t)llround(
|
||||
(2.0 * parameters.damping * wn / GPS_PPS_CLOCK_HZ) * q46);
|
||||
int64_t ki_q46 = (int64_t)llround((wn * wn * update_period_sec / GPS_PPS_CLOCK_HZ) * q46);
|
||||
uint32_t max_slew_q32 = (uint32_t)llround(
|
||||
parameters.max_slew_ppm * 1.0e-6 * 4294967296.0);
|
||||
return (gps_pps_nav_solve_tick != 0u) ?
|
||||
((double)gps_pps_nav_solve_tick * 0.020) :
|
||||
GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
|
||||
}
|
||||
|
||||
static bool gps_pps_write_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
const double update_period_sec = gps_pps_update_period_sec();
|
||||
gps_pps_loop_profile_t parameters =
|
||||
gps_pps_loop_profile_for_period(profile, update_period_sec);
|
||||
int64_t kp_q46 = 0;
|
||||
int64_t ki_q46 = 0;
|
||||
uint32_t max_slew_q32 = 0u;
|
||||
if(!gps_pps_loop_coefficients(
|
||||
parameters, update_period_sec, (double)GPS_PPS_CLOCK_HZ,
|
||||
&kp_q46, &ki_q46, &max_slew_q32))
|
||||
{
|
||||
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
|
||||
"PPS loop coefficient generation failed profile=%s period_ms=%.1lf\r\n",
|
||||
gps_pps_profile_name(profile), update_period_sec * 1000.0);
|
||||
return false;
|
||||
}
|
||||
|
||||
gps_pps_write_q46(&gps_pps->kp_q46_lo, &gps_pps->kp_q46_hi, kp_q46);
|
||||
gps_pps_write_q46(&gps_pps->ki_q46_lo, &gps_pps->ki_q46_hi, ki_q46);
|
||||
@@ -118,14 +131,25 @@ static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
|
||||
((parameters.lock_release_ticks & 0xffu) << 16) |
|
||||
((parameters.lock_acquire_updates & 0xffu) << 24);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void gps_pps_init(unsigned int nav_solve_tick)
|
||||
{
|
||||
gps_pps_nav_solve_tick = nav_solve_tick;
|
||||
uint64_t period_ticks =
|
||||
((uint64_t)GPS_PPS_CLOCK_HZ * nav_solve_tick * 20u) / 1000u;
|
||||
gps_pps_reference_timeout_ticks =
|
||||
GPS_PPS_REFERENCE_TIMEOUT_PERIODS * period_ticks;
|
||||
if(gps_pps_reference_timeout_ticks <
|
||||
(uint64_t)GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS)
|
||||
{
|
||||
gps_pps_reference_timeout_ticks =
|
||||
GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS;
|
||||
}
|
||||
gps_pps->control = 0u;
|
||||
gps_pps_version = gps_pps->version;
|
||||
gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
|
||||
(void)gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
|
||||
gps_pps_active_loop_profile = GPS_PPS_PROFILE_ROBUST;
|
||||
/* A GDB CPU reset does not reset the tracking-clock PPS state. Continue
|
||||
* its mailbox sequence and force the first new solution to re-anchor the
|
||||
@@ -149,12 +173,14 @@ void gps_pps_init(unsigned int nav_solve_tick)
|
||||
GPS_PPS_CONTROL_CLEAR_STATUS;
|
||||
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
|
||||
"PPS init rtl=0x%08x adaptive=%u loop_config=0x%08x "
|
||||
"clock_hz=%u update_ticks=%u\r\n",
|
||||
"clock_hz=%u update_ticks=%u reference_timeout_ms=%lu\r\n",
|
||||
gps_pps_version,
|
||||
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ? 1u : 0u,
|
||||
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ?
|
||||
gps_pps->loop_config : 0u,
|
||||
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick);
|
||||
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick,
|
||||
(unsigned long)((gps_pps_reference_timeout_ticks * 1000u) /
|
||||
GPS_PPS_CLOCK_HZ));
|
||||
}
|
||||
|
||||
void gps_pps_service(void)
|
||||
@@ -170,7 +196,7 @@ void gps_pps_service(void)
|
||||
uint32_t now_hi = gps_pps->raw_counter_hi;
|
||||
uint64_t now = ((uint64_t)now_hi << 32) | now_lo;
|
||||
if((now - gps_pps_last_reference_counter) >
|
||||
(uint64_t)GPS_PPS_REFERENCE_TIMEOUT_TICKS)
|
||||
gps_pps_reference_timeout_ticks)
|
||||
{
|
||||
gps_pps_set_holdover(true);
|
||||
}
|
||||
@@ -229,7 +255,10 @@ bool gps_pps_set_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
gps_pps_write_loop_profile(profile);
|
||||
if(!gps_pps_write_loop_profile(profile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
gps_pps_active_loop_profile = profile;
|
||||
return true;
|
||||
}
|
||||
@@ -277,7 +306,8 @@ gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
|
||||
double* innovation_m_out)
|
||||
{
|
||||
gps_pps_loop_profile_t parameters =
|
||||
gps_pps_loop_profile(gps_pps_active_loop_profile);
|
||||
gps_pps_loop_profile_for_period(
|
||||
gps_pps_active_loop_profile, gps_pps_update_period_sec());
|
||||
return gps_pps_time_filter_update(
|
||||
&gps_pps_time_filter, epoch_counter, (double)GPS_PPS_CLOCK_HZ,
|
||||
measured_clock_bias_m, clock_drift_valid,
|
||||
|
||||
@@ -7,6 +7,33 @@
|
||||
|
||||
#define GPS_PPS_PROFILE_UPGRADE_UPDATES 8u
|
||||
#define GPS_PPS_PROFILE_DEGRADE_UPDATES 3u
|
||||
#define GPS_PPS_MIN_HYSTERESIS_UPDATES 2u
|
||||
#define GPS_PPS_MIN_LOCK_UPDATES 2u
|
||||
#define GPS_PPS_MIN_SETTLE_UPDATES 3u
|
||||
#define GPS_PPS_MAX_CONFIG_UPDATES 255u
|
||||
|
||||
static unsigned int gps_pps_period_updates(
|
||||
unsigned int baseline_updates,
|
||||
double update_period_sec,
|
||||
unsigned int minimum_updates)
|
||||
{
|
||||
if(!isfinite(update_period_sec) || (update_period_sec <= 0.0))
|
||||
{
|
||||
update_period_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
|
||||
}
|
||||
double duration_sec =
|
||||
(double)baseline_updates * GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
|
||||
double required = ceil(duration_sec / update_period_sec - 1.0e-12);
|
||||
if(required < (double)minimum_updates)
|
||||
{
|
||||
return minimum_updates;
|
||||
}
|
||||
if(required > (double)GPS_PPS_MAX_CONFIG_UPDATES)
|
||||
{
|
||||
return GPS_PPS_MAX_CONFIG_UPDATES;
|
||||
}
|
||||
return (unsigned int)required;
|
||||
}
|
||||
|
||||
static bool gps_pps_metrics_finite(const gps_pps_quality_metrics_t* metrics)
|
||||
{
|
||||
@@ -67,12 +94,26 @@ static gps_pps_profile_t gps_pps_classify_quality(
|
||||
}
|
||||
|
||||
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector)
|
||||
{
|
||||
gps_pps_quality_selector_init_period(
|
||||
selector, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
|
||||
}
|
||||
|
||||
void gps_pps_quality_selector_init_period(
|
||||
gps_pps_quality_selector_t* selector,
|
||||
double update_period_sec)
|
||||
{
|
||||
if(selector != NULL)
|
||||
{
|
||||
memset(selector, 0, sizeof(*selector));
|
||||
selector->profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
selector->candidate = GPS_PPS_PROFILE_HOLDOVER;
|
||||
selector->upgrade_updates = gps_pps_period_updates(
|
||||
GPS_PPS_PROFILE_UPGRADE_UPDATES, update_period_sec,
|
||||
GPS_PPS_MIN_HYSTERESIS_UPDATES);
|
||||
selector->degrade_updates = gps_pps_period_updates(
|
||||
GPS_PPS_PROFILE_DEGRADE_UPDATES, update_period_sec,
|
||||
GPS_PPS_MIN_HYSTERESIS_UPDATES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +152,8 @@ gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
|
||||
unsigned int required_updates =
|
||||
(target > selector->profile) ?
|
||||
GPS_PPS_PROFILE_UPGRADE_UPDATES :
|
||||
GPS_PPS_PROFILE_DEGRADE_UPDATES;
|
||||
selector->upgrade_updates :
|
||||
selector->degrade_updates;
|
||||
if(selector->candidate_updates >= required_updates)
|
||||
{
|
||||
selector->profile = target;
|
||||
@@ -127,10 +168,19 @@ gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
return gps_pps_loop_profile_for_period(
|
||||
profile, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
|
||||
}
|
||||
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
|
||||
gps_pps_profile_t profile,
|
||||
double update_period_sec)
|
||||
{
|
||||
gps_pps_loop_profile_t parameters;
|
||||
switch(profile)
|
||||
{
|
||||
case GPS_PPS_PROFILE_ACQUIRE:
|
||||
return (gps_pps_loop_profile_t){
|
||||
parameters = (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.120,
|
||||
.damping = 0.9,
|
||||
.max_slew_ppm = 20.0,
|
||||
@@ -140,8 +190,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
.software_bias_gain = 1.0 / 16.0,
|
||||
.software_settle_updates = 12u
|
||||
};
|
||||
break;
|
||||
case GPS_PPS_PROFILE_PRECISION:
|
||||
return (gps_pps_loop_profile_t){
|
||||
parameters = (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.035,
|
||||
.damping = 0.9,
|
||||
.max_slew_ppm = 20.0,
|
||||
@@ -151,8 +202,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 12u
|
||||
};
|
||||
break;
|
||||
case GPS_PPS_PROFILE_ROBUST:
|
||||
return (gps_pps_loop_profile_t){
|
||||
parameters = (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.012,
|
||||
.damping = 1.0,
|
||||
.max_slew_ppm = 20.0,
|
||||
@@ -162,9 +214,10 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 16u
|
||||
};
|
||||
break;
|
||||
case GPS_PPS_PROFILE_HOLDOVER:
|
||||
default:
|
||||
return (gps_pps_loop_profile_t){
|
||||
parameters = (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.0,
|
||||
.damping = 1.0,
|
||||
.max_slew_ppm = 20.0,
|
||||
@@ -174,7 +227,106 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 16u
|
||||
};
|
||||
break;
|
||||
}
|
||||
parameters.lock_acquire_updates = gps_pps_period_updates(
|
||||
parameters.lock_acquire_updates, update_period_sec,
|
||||
GPS_PPS_MIN_LOCK_UPDATES);
|
||||
parameters.software_settle_updates = gps_pps_period_updates(
|
||||
parameters.software_settle_updates, update_period_sec,
|
||||
GPS_PPS_MIN_SETTLE_UPDATES);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
bool gps_pps_loop_coefficients(
|
||||
gps_pps_loop_profile_t parameters,
|
||||
double update_period_sec,
|
||||
double clock_frequency_hz,
|
||||
int64_t* kp_q46_out,
|
||||
int64_t* ki_q46_out,
|
||||
uint32_t* max_slew_q32_out)
|
||||
{
|
||||
if((kp_q46_out == NULL) || (ki_q46_out == NULL) ||
|
||||
(max_slew_q32_out == NULL) ||
|
||||
!isfinite(update_period_sec) || (update_period_sec <= 0.0) ||
|
||||
!isfinite(clock_frequency_hz) || (clock_frequency_hz <= 0.0) ||
|
||||
!isfinite(parameters.natural_frequency_hz) ||
|
||||
(parameters.natural_frequency_hz < 0.0) ||
|
||||
!isfinite(parameters.damping) || (parameters.damping < 0.0) ||
|
||||
!isfinite(parameters.max_slew_ppm) ||
|
||||
(parameters.max_slew_ppm < 0.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double pi = 3.14159265358979323846;
|
||||
const double q46 = 70368744177664.0;
|
||||
const double q32 = 4294967296.0;
|
||||
const double baseline_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
|
||||
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
|
||||
/* Use the actual deployed Q2.46 baseline, not its ideal double value, so
|
||||
* the mapped poles are those of the proven fixed-point implementation. */
|
||||
const double baseline_kp =
|
||||
(double)llround(
|
||||
(2.0 * parameters.damping * wn / clock_frequency_hz) * q46) /
|
||||
q46;
|
||||
const double baseline_ki =
|
||||
(double)llround(
|
||||
(wn * wn * baseline_sec / clock_frequency_hz) * q46) /
|
||||
q46;
|
||||
const double baseline_ticks = clock_frequency_hz * baseline_sec;
|
||||
const double baseline_sum =
|
||||
2.0 - baseline_ticks * (baseline_kp + baseline_ki);
|
||||
const double baseline_product =
|
||||
1.0 - baseline_ticks * baseline_kp;
|
||||
const double discriminant =
|
||||
baseline_sum * baseline_sum - 4.0 * baseline_product;
|
||||
const double period_ratio = update_period_sec / baseline_sec;
|
||||
double target_sum = 0.0;
|
||||
double target_product = 0.0;
|
||||
|
||||
if(discriminant >= 0.0)
|
||||
{
|
||||
double root = sqrt(discriminant);
|
||||
double pole_a = 0.5 * (baseline_sum + root);
|
||||
double pole_b = 0.5 * (baseline_sum - root);
|
||||
if((pole_a < 0.0) || (pole_b < 0.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
double target_a = pow(pole_a, period_ratio);
|
||||
double target_b = pow(pole_b, period_ratio);
|
||||
target_sum = target_a + target_b;
|
||||
target_product = target_a * target_b;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(baseline_product <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
double radius = sqrt(baseline_product);
|
||||
double cosine = baseline_sum / (2.0 * radius);
|
||||
cosine = fmax(-1.0, fmin(1.0, cosine));
|
||||
double angle = acos(cosine);
|
||||
double target_radius = pow(radius, period_ratio);
|
||||
target_sum =
|
||||
2.0 * target_radius * cos(angle * period_ratio);
|
||||
target_product = target_radius * target_radius;
|
||||
}
|
||||
|
||||
double target_ticks = clock_frequency_hz * update_period_sec;
|
||||
double kp = (1.0 - target_product) / target_ticks;
|
||||
double ki = (1.0 - target_sum + target_product) / target_ticks;
|
||||
if(!isfinite(kp) || !isfinite(ki) || (kp < 0.0) || (ki < 0.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*kp_q46_out = (int64_t)llround(kp * q46);
|
||||
*ki_q46_out = (int64_t)llround(ki * q46);
|
||||
*max_slew_q32_out = (uint32_t)llround(
|
||||
parameters.max_slew_ppm * 1.0e-6 * q32);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* gps_pps_profile_name(gps_pps_profile_t profile)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define GPS_PPS_FILTER_INNOVATION_CLAMP_M 300.0
|
||||
#define GPS_PPS_FILTER_HARD_OUTLIER_M 1500.0
|
||||
#define GPS_PPS_SPEED_OF_LIGHT_MPS 299792458.0
|
||||
#define GPS_PPS_FILTER_BASELINE_PERIOD_SEC 0.200
|
||||
|
||||
static double clamp_double(double value, double limit)
|
||||
{
|
||||
@@ -34,6 +35,21 @@ static double clamp_double(double value, double limit)
|
||||
return value;
|
||||
}
|
||||
|
||||
/* Preserve the baseline discrete pole when the navigation interval changes.
|
||||
* Applying a baseline gain alpha N times is equivalent to one elapsed-time
|
||||
* update with alpha_T = 1 - (1 - alpha)^N. This also handles an occasional
|
||||
* missed solve without slowing the clock-bias or drift estimator. */
|
||||
static double gps_pps_filter_elapsed_gain(
|
||||
double baseline_gain, double elapsed_sec)
|
||||
{
|
||||
double periods = elapsed_sec / GPS_PPS_FILTER_BASELINE_PERIOD_SEC;
|
||||
if(fabs(periods - 1.0) <= 1.0e-12)
|
||||
{
|
||||
return baseline_gain;
|
||||
}
|
||||
return -expm1(periods * log1p(-baseline_gain));
|
||||
}
|
||||
|
||||
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter)
|
||||
{
|
||||
if(filter != NULL)
|
||||
@@ -269,14 +285,20 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
|
||||
double effective_bias_gain =
|
||||
gps_pps_filter_elapsed_gain(bias_gain, elapsed_sec);
|
||||
double effective_drift_gain = gps_pps_filter_elapsed_gain(
|
||||
GPS_PPS_FILTER_DRIFT_GAIN, elapsed_sec);
|
||||
double effective_fallback_drift_gain = gps_pps_filter_elapsed_gain(
|
||||
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN, elapsed_sec);
|
||||
filter->clock_bias_m =
|
||||
predicted_clock_bias_m +
|
||||
bias_gain *
|
||||
effective_bias_gain *
|
||||
clamp_double(innovation_m, GPS_PPS_FILTER_INNOVATION_CLAMP_M);
|
||||
if(clock_drift_valid)
|
||||
{
|
||||
filter->clock_drift_mps +=
|
||||
GPS_PPS_FILTER_DRIFT_GAIN *
|
||||
effective_drift_gain *
|
||||
(measured_clock_drift_mps - filter->clock_drift_mps);
|
||||
}
|
||||
else
|
||||
@@ -291,7 +313,7 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
|
||||
else
|
||||
{
|
||||
filter->clock_drift_mps +=
|
||||
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN *
|
||||
effective_fallback_drift_gain *
|
||||
(fallback_drift_mps - filter->clock_drift_mps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,14 +91,11 @@ static void profile_coefficients(
|
||||
int64_t* ki_q46,
|
||||
int64_t* max_slew_q32)
|
||||
{
|
||||
const double pi = 3.14159265358979323846;
|
||||
double wn = 2.0 * pi * profile.natural_frequency_hz;
|
||||
*kp_q46 = (int64_t)llround(
|
||||
(2.0 * profile.damping * wn / TRACKING_HZ) * Q46_SCALE);
|
||||
*ki_q46 = (int64_t)llround(
|
||||
(wn * wn * UPDATE_PERIOD_SEC / TRACKING_HZ) * Q46_SCALE);
|
||||
*max_slew_q32 = (int64_t)llround(
|
||||
profile.max_slew_ppm * 1.0e-6 * Q32_SCALE);
|
||||
uint32_t max_slew = 0u;
|
||||
assert(gps_pps_loop_coefficients(
|
||||
profile, UPDATE_PERIOD_SEC, TRACKING_HZ,
|
||||
kp_q46, ki_q46, &max_slew));
|
||||
*max_slew_q32 = max_slew;
|
||||
}
|
||||
|
||||
static void synthetic_rtl_coarse(synthetic_rtl_t* rtl, int64_t reference_q32)
|
||||
|
||||
@@ -107,6 +107,69 @@ static void test_profile_parameters_are_ordered(void)
|
||||
assert(robust.lock_release_ticks >= precision.lock_release_ticks);
|
||||
}
|
||||
|
||||
static void test_period_scaled_hysteresis_and_settling(void)
|
||||
{
|
||||
gps_pps_quality_selector_t selector;
|
||||
gps_pps_quality_selector_init_period(&selector, 0.5);
|
||||
assert(selector.upgrade_updates == 4u);
|
||||
assert(selector.degrade_updates == 2u);
|
||||
gps_pps_loop_profile_t precision =
|
||||
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_PRECISION, 0.5);
|
||||
gps_pps_loop_profile_t robust =
|
||||
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_ROBUST, 0.5);
|
||||
assert(precision.lock_acquire_updates == 2u);
|
||||
assert(precision.software_settle_updates == 5u);
|
||||
assert(robust.lock_acquire_updates == 4u);
|
||||
assert(robust.software_settle_updates == 7u);
|
||||
|
||||
gps_pps_quality_selector_init_period(&selector, 1.0);
|
||||
assert(selector.upgrade_updates == 2u);
|
||||
assert(selector.degrade_updates == 2u);
|
||||
precision = gps_pps_loop_profile_for_period(
|
||||
GPS_PPS_PROFILE_PRECISION, 1.0);
|
||||
robust = gps_pps_loop_profile_for_period(
|
||||
GPS_PPS_PROFILE_ROBUST, 1.0);
|
||||
assert(precision.lock_acquire_updates == 2u);
|
||||
assert(precision.software_settle_updates == 3u);
|
||||
assert(robust.lock_acquire_updates == 2u);
|
||||
assert(robust.software_settle_updates == 4u);
|
||||
}
|
||||
|
||||
static void assert_coefficients(
|
||||
gps_pps_profile_t profile,
|
||||
double period_sec,
|
||||
int64_t expected_kp,
|
||||
int64_t expected_ki)
|
||||
{
|
||||
int64_t kp = 0;
|
||||
int64_t ki = 0;
|
||||
uint32_t max_slew = 0u;
|
||||
gps_pps_loop_profile_t parameters =
|
||||
gps_pps_loop_profile_for_period(profile, period_sec);
|
||||
assert(gps_pps_loop_coefficients(
|
||||
parameters, period_sec, 125000000.0,
|
||||
&kp, &ki, &max_slew));
|
||||
assert(kp == expected_kp);
|
||||
assert(ki == expected_ki);
|
||||
assert(max_slew == 85899u);
|
||||
}
|
||||
|
||||
static void test_period_scaled_discrete_coefficients(void)
|
||||
{
|
||||
/* The production 200 ms coefficients remain bit-for-bit unchanged. */
|
||||
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.2, 764018, 64006);
|
||||
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.2, 222838, 5445);
|
||||
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.2, 84891, 640);
|
||||
|
||||
/* Other cadences exponentiate those proven discrete poles in time. */
|
||||
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.5, 615780, 127492);
|
||||
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.5, 209783, 12804);
|
||||
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.5, 82980, 1564);
|
||||
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 1.0, 447388, 178038);
|
||||
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 1.0, 190239, 23149);
|
||||
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 1.0, 79923, 3011);
|
||||
}
|
||||
|
||||
static void test_quality_boundaries_and_recovery(void)
|
||||
{
|
||||
gps_pps_quality_selector_t selector;
|
||||
@@ -167,6 +230,8 @@ int main(void)
|
||||
test_hysteretic_profile_transitions();
|
||||
test_alternating_quality_does_not_upgrade();
|
||||
test_profile_parameters_are_ordered();
|
||||
test_period_scaled_hysteresis_and_settling();
|
||||
test_period_scaled_discrete_coefficients();
|
||||
test_quality_boundaries_and_recovery();
|
||||
puts("gps_pps_quality tests passed");
|
||||
return 0;
|
||||
|
||||
@@ -6,6 +6,30 @@
|
||||
|
||||
#define COUNTER_HZ 125000000.0
|
||||
|
||||
static void test_elapsed_time_gain_preserves_filter_poles(void)
|
||||
{
|
||||
gps_pps_time_filter_t filter;
|
||||
gps_pps_time_filter_init(&filter);
|
||||
double filtered = 0.0;
|
||||
double innovation = 0.0;
|
||||
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 0u, COUNTER_HZ, 0.0, true, 0.0, 1.0,
|
||||
1.0 / 32.0, 1u, &filtered, &innovation) ==
|
||||
GPS_PPS_TIME_FILTER_COARSE);
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, (uint64_t)COUNTER_HZ, COUNTER_HZ, 100.0, true, 100.0,
|
||||
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
|
||||
GPS_PPS_TIME_FILTER_TRACKING);
|
||||
|
||||
/* One 1 s update has exactly the pole of five baseline 200 ms updates. */
|
||||
double expected_bias_gain = 1.0 - pow(1.0 - 1.0 / 32.0, 5.0);
|
||||
double expected_drift_gain = 1.0 - pow(1.0 - 1.0 / 4.0, 5.0);
|
||||
assert(fabs(filtered - 100.0 * expected_bias_gain) < 1.0e-12);
|
||||
assert(fabs(filter.clock_drift_mps - 100.0 * expected_drift_gain) <
|
||||
1.0e-12);
|
||||
}
|
||||
|
||||
static void test_drift_prediction_rejects_bias_noise(void)
|
||||
{
|
||||
static const double noise_m[] = {
|
||||
@@ -298,6 +322,7 @@ static void test_reference_selection_is_stable(void)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_elapsed_time_gain_preserves_filter_poles();
|
||||
test_drift_prediction_rejects_bias_noise();
|
||||
test_quality_gap_and_outlier_handling();
|
||||
test_bias_ramp_without_doppler_uses_bounded_fallback();
|
||||
|
||||
Reference in New Issue
Block a user