feat(pps): add adaptive disciplined timing loop
This commit is contained in:
@@ -5,7 +5,7 @@ TARGET := gps_system
|
||||
# directory previously compiled every GPS_LOG call out and produced a silent
|
||||
# UART image.
|
||||
GPS_DEBUG ?= 1
|
||||
GPS_DEBUG_LEVEL ?= 4
|
||||
GPS_DEBUG_LEVEL ?= 1
|
||||
GPS_BUILD_ID ?= $(shell git describe --always --dirty --abbrev=12 2>/dev/null || printf unknown)
|
||||
|
||||
# The current Genesys2 Rocket/fabric domain that clocks the UART is 50 MHz.
|
||||
@@ -71,6 +71,8 @@ APP_SRCS := \
|
||||
src/gps/gps_time_anchor.c \
|
||||
src/gps/gps_nav_epoch.c \
|
||||
src/gps/gps_pps.c \
|
||||
src/gps/gps_pps_quality.c \
|
||||
src/gps/gps_pps_time_filter.c \
|
||||
src/gps/gps_tracking_measurement.c \
|
||||
src/gps/linked_list.c \
|
||||
src/gps/NavEquations.c \
|
||||
@@ -79,6 +81,7 @@ APP_SRCS := \
|
||||
BSP_SRCS := init.c uart.c plic.c irq.c spi_flash.c pmod_spi.c
|
||||
|
||||
GPS_NAV_SOLVE_TIMER_TICKS ?= 10
|
||||
GPS_PPS_STATUS_LOG_INTERVAL ?= 10
|
||||
|
||||
include ../common/app.mk
|
||||
|
||||
@@ -90,7 +93,8 @@ CFLAGS += $(IMU_BACKEND_CFLAG) \
|
||||
-DIMU_GYRO_FS_DPS=$(IMU_GYRO_FS_DPS) \
|
||||
-DGPS_ESKF_ENABLE=$(GPS_ESKF_ENABLE) \
|
||||
-DIMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=$(IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES) \
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS)
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS) \
|
||||
-DGPS_PPS_STATUS_LOG_INTERVAL=$(GPS_PPS_STATUS_LOG_INTERVAL)
|
||||
LDLIBS += -lm
|
||||
|
||||
ifeq ($(GPS_DEBUG),1)
|
||||
|
||||
@@ -1173,7 +1173,8 @@ Bu timer/scheduler yavasligi degildir. Firmware start ediyor; tamamlanma icin RF
|
||||
darbesi uretir. Bu olay `ABS_SAMPLE` ve `REM_CODE_PHASE` latch'iyle aynidir;
|
||||
subframe-ready PPS timestamp'i olusturmaz.
|
||||
- `leastSquaresSTD()` ayni basarili epoch'un `[x, y, z, b]` sonucunu verir.
|
||||
Minimum corrected-ABS kanalinin receive-time ankraji su sekilde kurulur:
|
||||
Ilk cozumde en erken corrected-ABS kanali zaman datum'u secilir; ayni SVID
|
||||
coherent snapshot'ta kaldigi surece referans korunur. Receive-time ankraji:
|
||||
|
||||
```text
|
||||
T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
|
||||
@@ -1182,12 +1183,108 @@ T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
|
||||
- `POSTPONE_TOW=0.160 s`, takip eden subframe'dan parity icin beklenen sekiz
|
||||
bitin gecikmesidir ve yazilim tarafinda tam bir kez uygulanir. PPS RTL bu
|
||||
degeri tekrar eklemez.
|
||||
- Bu GPS zamani, ayni minimum-ABS kanalinin 125 MHz capture counter'i ile atomik
|
||||
- Bu GPS zamani, ayni kalici referans kanalinin 125 MHz capture counter'i ile atomik
|
||||
APB mailbox uzerinden eslenir. Ilk cozum coarse-align, sonraki cozumler Q2.46
|
||||
dusuk bant genislikli PI rate discipline yapar. WLS/APB calisma suresi faz
|
||||
hatasi olarak kullanilmaz.
|
||||
- Tracking capture olayi integer RF sample kenarinda olustugu halde WLS,
|
||||
`ABS_SAMPLE - REM_CODE_PHASE/1.023e6` alt-sample zamanini kullanir. Yazilim
|
||||
mailbox `EPOCH_COUNTER` degerinden ayni fractional sample gecikmesini
|
||||
`round(fraction * 125e6/16.367981e6)` ile cikarir. Boylece WLS zamani ile
|
||||
hardware epoch'u ayni code boundary'ye referans olur; 125 MHz integer
|
||||
mailbox yuvarlamasindan en fazla 4 ns kalir.
|
||||
- HOW/sample sureklilik kontrolu kartin fiziksel `16.367981 MHz` RF frekansini
|
||||
kullanir. Periyodik nav-solve TOW'u receiver propagation/range-rate
|
||||
degisiminden uretilmez; sample farki yalniz olay numarasini dogrular ve TOW
|
||||
bilinen 200 ms transmit-time grid'ine snap edilir.
|
||||
- Donanimdaki kontrol filtresi PI loop'un kendisidir; ayri bir phase-IIR veya
|
||||
D terimi yoktur. Yazilim `TDOP/GDOP`, formal clock-bias sigma, kullanilan
|
||||
uydu sayisi, C/N0, chi-square/FDE ve Doppler velocity residual'ina gore
|
||||
`acquire`, `precision`, `robust` ve `holdover` profillerini histerezisle
|
||||
secer. Profil gecisinde `Kp`, `Ki`, `max-slew` ve v2.2 lock esikleri bir
|
||||
sonraki mailbox commit'inde birlikte yakalanir; normal geciste integrator
|
||||
korunur. Lock almak icin bes ardisik residual `64 ns` icinde olmalidir;
|
||||
uretim release esigi tek seyrek WLS/capture sicrama pulse kaybettirmesin
|
||||
diye `1.024 us` tutulur. Watchdog, quality holdover ve `20 ppm` slew clamp
|
||||
buyuk arizalari ayri olarak sinirlar.
|
||||
- Yazilim filtresi PI'nin yerine gecmez. Yalnizca 5 Hz WLS clock-bias
|
||||
olcumunde Doppler clock-drift prediction, bounded innovation ve aykiri
|
||||
olcum reddi yapar. Ilk yerlesmede commit'ler coarse ve PPS invalid tutulur.
|
||||
Periyodik background watchdog iki saniye yeni referans gelmezse cikis
|
||||
validligini holdover ile kapatir; fakat ayni referans geri gelirse Doppler
|
||||
prediction ve donanim PI state'i 30 saniyeye kadar korunur. Referans SVID
|
||||
degisimi veya 30 saniyeyi asan bosluk coarse re-anchor yapar. PPS v2.2
|
||||
holdover aktifken normal PI commit'ini uygulamadigi icin recovery'de butun
|
||||
shadow word'ler once yazilir, holdover commit toggle'inin hemen oncesinde
|
||||
birakilir ve ilk taze olcum PI'ya girer. Fiziksel PPS held rate ile
|
||||
kesintisiz yurur; `STATUS.VALID` PI apply'dan yalniz bounded
|
||||
synchronizer/commit-pipeline gecikmesi kadar once gorunebilir.
|
||||
- PPS v2.2 `LOOP_CONFIG` register'i `0x88` adresindedir. Eski v2.1 bitstream
|
||||
algilanirsa yazilim bu register'a dokunmaz; dinamik Q2.46 `Kp/Ki/max-slew`
|
||||
yazimi mevcut register'larla calismaya devam eder.
|
||||
- 125 MHz fiziksel kenar grid'i 8 ns'dir. Generated VCU118 ve Genesys2 top'lari
|
||||
`gps_pps` output'unu expose eder. VCU118 icin PPS PMOD1/J53.8 (`R29`),
|
||||
Genesys2 icin PMOD JC10 (`AK28`) pinine atanmistir. Board testinde
|
||||
Gray/PCLK capture pipeline sabit offset'i ve
|
||||
clock-tree/I/O gecikmesi olculup kalibre edilmelidir.
|
||||
|
||||
### PPS Sentetik Dogrulama (2026-07-25)
|
||||
|
||||
Python modeli ve Linux C testi ayni Q32/Q46 PI, kalite histerezisi, Doppler
|
||||
prediction, holdover ve lock politikasini kullanir. Ayrica donanim phase-IIR
|
||||
kullanilmaz. Test matrisi temiz/320 ns WLS noise, 650 ns zayif geometri,
|
||||
baslangicta 1.1 us noise sonra temiz sinyal, aralikli 900 ns noise burstleri,
|
||||
uydu kaybi ve geri gelmesi, 15 saniyelik state-korumali ve 30 saniye ustu
|
||||
coarse WLS kesintisi, Doppler kaybi, multipath
|
||||
basamagi, profil sicramalari, +18 ppm slew siniri ve +25 ppm sinir disi
|
||||
osilatoru kapsar.
|
||||
|
||||
Monte Carlo sonucunda hedeflenen senaryolarin tamami lock/recovery sagladi.
|
||||
Normal kart-noise senaryosunda 50/50 kosu lock oldu ve 125 MHz edge
|
||||
quantization dahil bir saniyelik jitter p95 `13.62 ns` bulundu. Baslangicta
|
||||
yuksek noise sonra temiz sinyal senaryosunda 30/30, noise-burst senaryosunda
|
||||
30/30, iki uydu-kaybi araligi bulunan senaryoda 30/30 recovery goruldu.
|
||||
`+18 ppm` 30/30 yerlesirken `+25 ppm`, `20 ppm` clamp disinda oldugu icin
|
||||
0/10 lock uretti; yani saturation yanlis-valid olusturmadi. Bunlar sentetik
|
||||
loop/jitter sonuclaridir; mutlak UTC/GPS faz dogrulugu icin karttaki sabit
|
||||
capture/I/O offset'i ve gercek multipath halen osiloskop/time-interval olcumu
|
||||
gerektirir.
|
||||
|
||||
### PPS Kart Dogrulamasi (2026-07-25, Genesys2)
|
||||
|
||||
- Kartta okunan RTL surumu `0x00020200`; dinamik `LOOP_CONFIG=0x08800800`
|
||||
mailbox'i ve diagnostic snapshot calisti.
|
||||
- Eski `16.368 MHz` zaman donusumu 6 saniyede yaklasik `6.97 us` hata
|
||||
uretiyordu. Fiziksel `16.367981 MHz` donusumu ve 200 ms transmit-grid snap
|
||||
sonrasinda 6 saniyelik mikro-saniye sawtooth kayboldu.
|
||||
- Fractional capture duzeltmesi kartta epoch'a gore `0..64 ns` uygulandi
|
||||
(son deger 8 ns grid'ine yuvarlanir). Duzeltmeden onceki uzun lock
|
||||
penceresinde internal commit residual'i yaklasik `20.9 ns` median mutlak ve
|
||||
`62.8 ns` p95 idi. Duzeltmeden sonraki precision penceresinde median mutlak
|
||||
residual yaklasik `17 ns`, RMS `26 ns` ve p95 yaklasik `55 ns` oldu.
|
||||
- Ilk kart yaziliminda iki saniyeyi asan normal coherent-WLS scheduling
|
||||
bosluklari filtreyi coarse resetliyordu; bu PPS'in periyodik olarak
|
||||
4--5 update invalid kalmasina yol acti. Esigi validlik watchdog'undan
|
||||
ayirdiktan sonra `seq=10551 -> 10561` araliginda `pulse=2174 -> 2186`
|
||||
ile yaklasik 12 saniyelik gercek kart boslugu goruldu. Buna ragmen
|
||||
`mode=tracking`, `locked=1`, status `0x80f` ve PI integrator kesintisiz
|
||||
kaldi; coarse commit veya faz basamagi olusmadi.
|
||||
- Her WLS commit'inin loglandigi `pps-fullrate` tanisinda `seq=16090..17631`
|
||||
arasinda 1514 commit izlendi. Ilk lock'tan sonra coarse veya unlock olmadi.
|
||||
`seq>=16250` icin 1355 locked residual'in median mutlak degeri `17.29 ns`,
|
||||
RMS'i `34.17 ns`, p95'i `52.76 ns`, p99'u `67.24 ns` oldu. Bu sayilar
|
||||
fiziksel PPS pin jitter'i degil, 5 Hz WLS commit residual'idir.
|
||||
- Ayni tanida quality'nin robust'a dustugu tek bir commit'te `750.93 ns`
|
||||
residual goruldu ve bir sonraki 200 ms commit'te normale dondu. Robust
|
||||
`1.024 us` release esigi gereksiz pulse kaybini onledi. Daha once precision
|
||||
`512 ns` release ile gorulen tek kisa unlock nedeniyle uretim precision
|
||||
esigi de `1.024 us` yapildi; acquisition kosulu bes ardisik `<=64 ns`
|
||||
olarak kaldi. Son `pps-final` kart okumasinda `LOOP_CONFIG=0x05800800`,
|
||||
`CONTROL=0x3`, valid/locked status `0x80f` goruldu ve kontrollu holdover
|
||||
denemesinden sonra ilk normal commit ile control/valid geri geldi.
|
||||
- Bu `hw_phase`, 5 Hz WLS reference commit residual'idir ve dogrudan fiziksel
|
||||
1PPS kenar jitter'i degildir. Ayni pencerede proportional rate terimi
|
||||
yaklasik `9.6 ppb RMS` idi; sentetik 125 MHz edge modeli kart-noise
|
||||
senaryosunda bir saniyelik jitter p95 `13.62 ns` veriyor. `15--20 ns`
|
||||
fiziksel hedef makuldur fakat pin uzerinde scope/TIC ile GPS-disiplinli
|
||||
harici referansa karsi olculmeden saglandi diye raporlanmamalidir.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
typedef struct gps_nav_solution {
|
||||
double ecef_m[3];
|
||||
double clock_bias_m;
|
||||
double clock_bias_variance_m2;
|
||||
double velocity_ecef_mps[3];
|
||||
double velocity_covariance_ecef_mps2[3][3];
|
||||
double clock_drift_mps;
|
||||
|
||||
@@ -37,8 +37,21 @@
|
||||
#define WN_INDEX 2
|
||||
#define POSITION_VECTOR_COL 3
|
||||
#define MAX_24_BIT_INT 0x00800000
|
||||
// ADC sample frekansi
|
||||
#define SAMPLES_PER_CODE 16368.0
|
||||
/*
|
||||
* RF ADC sample rate used for time and pseudorange conversion.
|
||||
*
|
||||
* The acquisition RTL buffers an integer 16368 samples per nominal C/A
|
||||
* code period, but the Genesys2/VCU118 RF reference is 16.367981 MHz.
|
||||
* Using 16368.000 samples/ms as a frequency creates a 1.161 ppm timing
|
||||
* ramp which is reset by every six-second HOW anchor (about 6.97 us
|
||||
* peak-to-peak). Keep the physical clock rate distinct from the integer
|
||||
* acquisition-buffer length.
|
||||
*/
|
||||
#ifndef GPS_RF_SAMPLE_RATE_HZ
|
||||
#define GPS_RF_SAMPLE_RATE_HZ 16367981.0
|
||||
#endif
|
||||
#define GPS_RF_SAMPLES_PER_MILLISECOND (GPS_RF_SAMPLE_RATE_HZ / 1000.0)
|
||||
#define SAMPLES_PER_CODE GPS_RF_SAMPLES_PER_MILLISECOND
|
||||
|
||||
#define CODE_FREQ_BASIS 1.023E+6
|
||||
|
||||
|
||||
@@ -4,11 +4,40 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "gps/gps_pps_quality.h"
|
||||
#include "gps/gps_pps_time_filter.h"
|
||||
|
||||
#define GPS_PPS_CLOCK_HZ 125000000u
|
||||
|
||||
typedef struct {
|
||||
uint32_t status;
|
||||
uint32_t active_sequence;
|
||||
uint32_t pulse_count;
|
||||
int64_t phase_error_q32;
|
||||
int64_t integrator_q32;
|
||||
int64_t rate_word_q32;
|
||||
} gps_pps_diagnostics_t;
|
||||
|
||||
void gps_pps_init(unsigned int nav_solve_tick);
|
||||
void gps_pps_service(void);
|
||||
bool gps_pps_is_locked(void);
|
||||
bool gps_pps_set_loop_profile(gps_pps_profile_t profile);
|
||||
void gps_pps_set_holdover(bool holdover);
|
||||
void gps_pps_reset_time_filter(void);
|
||||
bool gps_pps_read_diagnostics(gps_pps_diagnostics_t* diagnostics);
|
||||
bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* sequence);
|
||||
bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiver_tow_sec);
|
||||
gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
|
||||
uint64_t epoch_counter,
|
||||
double measured_clock_bias_m,
|
||||
bool clock_drift_valid,
|
||||
double measured_clock_drift_mps,
|
||||
double tdop,
|
||||
double* filtered_clock_bias_m_out,
|
||||
double* innovation_m_out);
|
||||
bool gps_pps_commit_solution(
|
||||
uint64_t epoch_counter,
|
||||
int gps_week,
|
||||
double receiver_tow_sec,
|
||||
bool force_coarse);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef GPS_PPS_QUALITY_H
|
||||
#define GPS_PPS_QUALITY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
GPS_PPS_PROFILE_HOLDOVER = 0,
|
||||
GPS_PPS_PROFILE_ROBUST,
|
||||
GPS_PPS_PROFILE_PRECISION,
|
||||
GPS_PPS_PROFILE_ACQUIRE
|
||||
} gps_pps_profile_t;
|
||||
|
||||
typedef struct {
|
||||
double tdop;
|
||||
double gdop;
|
||||
double clock_bias_sigma_m;
|
||||
double mean_cn0_dbhz;
|
||||
double minimum_cn0_dbhz;
|
||||
double chi_square_ratio;
|
||||
double velocity_residual_rms_mps;
|
||||
unsigned int used_satellites;
|
||||
bool solution_valid;
|
||||
bool residual_test_valid;
|
||||
bool velocity_valid;
|
||||
bool fde_applied;
|
||||
} gps_pps_quality_metrics_t;
|
||||
|
||||
typedef struct {
|
||||
gps_pps_profile_t profile;
|
||||
gps_pps_profile_t candidate;
|
||||
unsigned int candidate_updates;
|
||||
} gps_pps_quality_selector_t;
|
||||
|
||||
typedef struct {
|
||||
double natural_frequency_hz;
|
||||
double damping;
|
||||
double max_slew_ppm;
|
||||
unsigned int lock_acquire_ticks;
|
||||
unsigned int lock_release_ticks;
|
||||
unsigned int lock_acquire_updates;
|
||||
double software_bias_gain;
|
||||
unsigned int software_settle_updates;
|
||||
} gps_pps_loop_profile_t;
|
||||
|
||||
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
gps_pps_quality_selector_t* selector,
|
||||
const gps_pps_quality_metrics_t* metrics,
|
||||
bool* profile_changed_out);
|
||||
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile);
|
||||
const char* gps_pps_profile_name(gps_pps_profile_t profile);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
#ifndef GPS_PPS_TIME_FILTER_H
|
||||
#define GPS_PPS_TIME_FILTER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
GPS_PPS_TIME_FILTER_REJECTED = 0,
|
||||
GPS_PPS_TIME_FILTER_COARSE,
|
||||
GPS_PPS_TIME_FILTER_TRACKING
|
||||
} gps_pps_time_filter_result_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t epoch_counter;
|
||||
uint64_t drift_anchor_counter;
|
||||
double clock_bias_m;
|
||||
double clock_drift_mps;
|
||||
double previous_measurement_m;
|
||||
double drift_anchor_measurement_m;
|
||||
unsigned int accepted_updates;
|
||||
bool initialized;
|
||||
bool drift_initialized;
|
||||
} gps_pps_time_filter_t;
|
||||
|
||||
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter);
|
||||
|
||||
/*
|
||||
* Keep the current WLS datum satellite while it remains in the coherent
|
||||
* snapshot. Fall back to the earliest capture only when that satellite is
|
||||
* gone. Stable selection avoids artificial clock-bias jumps as satellites
|
||||
* enter the solution.
|
||||
*/
|
||||
bool gps_pps_select_reference(
|
||||
const unsigned int satellite_ids[],
|
||||
const double capture_times[],
|
||||
unsigned int count,
|
||||
bool preferred_valid,
|
||||
unsigned int preferred_id,
|
||||
unsigned int* reference_index_out);
|
||||
|
||||
/*
|
||||
* The position WLS observations are formed from capture_i - min(capture).
|
||||
* That subtraction removes the common receiver-clock offset and drift before
|
||||
* WLS. The fitted clock-bias datum is STARTOFFSET*c - reference_range (plus
|
||||
* the reference satellite clock correction), so its derivative is
|
||||
* -reference_range_rate + c*reference_satellite_clock_drift.
|
||||
*/
|
||||
bool gps_pps_reference_bias_drift(
|
||||
const double receiver_ecef_m[3],
|
||||
const double receiver_velocity_ecef_mps[3],
|
||||
const double reference_satellite_ecef_m[3],
|
||||
const double reference_satellite_velocity_ecef_mps[3],
|
||||
double reference_satellite_clock_drift_sps,
|
||||
double* clock_bias_drift_mps_out);
|
||||
|
||||
/*
|
||||
* Smooth the pseudorange WLS receiver-clock bias without lagging its large
|
||||
* 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.
|
||||
*
|
||||
* COARSE is returned during initial settling, a reference-datum change, or
|
||||
* after a gap too long for bounded Doppler prediction. The independent
|
||||
* reference watchdog may suppress PPS validity during a shorter gap without
|
||||
* destroying filter/PI state. The caller must keep PPS output invalid while
|
||||
* COARSE is returned.
|
||||
*/
|
||||
gps_pps_time_filter_result_t gps_pps_time_filter_update(
|
||||
gps_pps_time_filter_t* filter,
|
||||
uint64_t epoch_counter,
|
||||
double counter_frequency_hz,
|
||||
double measured_clock_bias_m,
|
||||
bool clock_drift_valid,
|
||||
double measured_clock_drift_mps,
|
||||
double tdop,
|
||||
double bias_gain,
|
||||
unsigned int settle_updates,
|
||||
double* filtered_clock_bias_m_out,
|
||||
double* innovation_m_out);
|
||||
|
||||
#endif
|
||||
@@ -36,6 +36,40 @@ bool gps_time_from_sample_anchor(int anchor_week,
|
||||
double* event_tow_sec_out,
|
||||
double* elapsed_sec_out);
|
||||
|
||||
/*
|
||||
* Convert a periodic tracking event to GPS transmit time. The RF sample
|
||||
* interval identifies and validates the event number; the result is then
|
||||
* snapped to the known navigation-event grid. This removes the
|
||||
* satellite-range-rate term present in receive-time sample intervals.
|
||||
*/
|
||||
bool gps_time_from_periodic_sample_anchor(
|
||||
int anchor_week,
|
||||
double anchor_tow_sec,
|
||||
uint32_t anchor_raw_sample,
|
||||
double anchor_corrected_sample,
|
||||
uint32_t event_raw_sample,
|
||||
double event_corrected_sample,
|
||||
double samples_per_second,
|
||||
double max_age_sec,
|
||||
double event_period_sec,
|
||||
double max_snap_error_sec,
|
||||
int* event_week_out,
|
||||
double* event_tow_sec_out,
|
||||
double* elapsed_sec_out);
|
||||
|
||||
/*
|
||||
* Refer a counter captured at the first RF sample edge after a code boundary
|
||||
* back to that fractional code boundary. fractional_sample_delay is
|
||||
* raw_abs_sample - corrected_abs_sample and must be in [0, 1].
|
||||
*/
|
||||
bool gps_time_correct_capture_counter(
|
||||
uint64_t capture_counter,
|
||||
double fractional_sample_delay,
|
||||
double samples_per_second,
|
||||
double counter_frequency_hz,
|
||||
uint64_t* corrected_counter_out,
|
||||
double* applied_correction_sec_out);
|
||||
|
||||
/* Validate that two decoded HOW/sample anchors describe the same monotonic
|
||||
* receiver timeline. This keeps a single transient HOW from replacing a
|
||||
* healthy anchor while still permitting normal week rollover. */
|
||||
|
||||
@@ -1208,6 +1208,28 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
covariance_between_axes(position_covariance, enu_axes[row], enu_axes[col]);
|
||||
}
|
||||
}
|
||||
double clock_covariance_scale = 1.0;
|
||||
if(degrees_of_freedom > 0u)
|
||||
{
|
||||
clock_covariance_scale =
|
||||
fmax(1.0, weighted_residual_sum / (double)degrees_of_freedom);
|
||||
}
|
||||
solution_out->clock_bias_variance_m2 =
|
||||
clock_covariance_scale * position_covariance->matrix[3][3];
|
||||
if(!isfinite(solution_out->clock_bias_variance_m2) ||
|
||||
(solution_out->clock_bias_variance_m2 < 0.0))
|
||||
{
|
||||
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
|
||||
"clock-bias covariance rejected variance=%e scale=%e\r\n",
|
||||
solution_out->clock_bias_variance_m2, clock_covariance_scale);
|
||||
free_matrix(position_covariance);
|
||||
free_matrix(A);
|
||||
free_matrix(posVec);
|
||||
free(absoluteWeights);
|
||||
free(phi_lambda_h);
|
||||
free(topcent);
|
||||
return false;
|
||||
}
|
||||
free_matrix(position_covariance);
|
||||
free_matrix(A);
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ static gps_external_irq_handler_t gps_external_irq_handler = NULL;
|
||||
#define GPS_NAV_HOLDOFF_LOCAL_TICKS \
|
||||
(((uint64_t)BSP_CLINT_TIMEBASE_HZ * GPS_NAV_EPOCH_HOLDOFF_MS) / 1000u)
|
||||
#define GPS_TRACK_DOPPLER_SMOOTH_ALPHA 0.05
|
||||
#define GPS_NAV_TOW_SNAP_TOLERANCE_SEC 0.005
|
||||
|
||||
_Static_assert(GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS <
|
||||
(GPS_NAV_SOLVE_PERIOD_MS / 2u),
|
||||
@@ -1161,7 +1162,7 @@ static bool gps_how_candidate_consistent(
|
||||
first_raw_sample, first_corrected_sample,
|
||||
second_week, second_tow_sec,
|
||||
second_raw_sample, second_corrected_sample,
|
||||
SAMPLES_PER_CODE * 1000.0,
|
||||
GPS_RF_SAMPLE_RATE_HZ,
|
||||
GPS_HOW_CONTINUITY_MAX_GAP_SEC,
|
||||
GPS_HOW_CONTINUITY_TOLERANCE_SEC);
|
||||
}
|
||||
@@ -1388,17 +1389,20 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
|
||||
int tow_week = 0;
|
||||
double tow_sec = 0.0;
|
||||
double tow_delta_sec = 0.0;
|
||||
if(!gps_time_from_sample_anchor(node->gps_how_tow.week,
|
||||
node->gps_how_tow.sec,
|
||||
node->gps_how_abs_sample,
|
||||
node->gps_how_corrected_abs_sample,
|
||||
event->abs_sample_raw,
|
||||
event->corrected_abs_sample,
|
||||
SAMPLES_PER_CODE * 1000.0,
|
||||
GPS_HOW_ANCHOR_MAX_AGE_SEC,
|
||||
&tow_week,
|
||||
&tow_sec,
|
||||
&tow_delta_sec))
|
||||
if(!gps_time_from_periodic_sample_anchor(
|
||||
node->gps_how_tow.week,
|
||||
node->gps_how_tow.sec,
|
||||
node->gps_how_abs_sample,
|
||||
node->gps_how_corrected_abs_sample,
|
||||
event->abs_sample_raw,
|
||||
event->corrected_abs_sample,
|
||||
GPS_RF_SAMPLE_RATE_HZ,
|
||||
GPS_HOW_ANCHOR_MAX_AGE_SEC,
|
||||
(double)GPS_NAV_SOLVE_PERIOD_MS / 1000.0,
|
||||
GPS_NAV_TOW_SNAP_TOLERANCE_SEC,
|
||||
&tow_week,
|
||||
&tow_sec,
|
||||
&tow_delta_sec))
|
||||
{
|
||||
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
|
||||
"nav solve rejected by stale/invalid HOW anchor: trk=%u prn=%u anchor_tow=%.3f anchor_abs=%u event_abs=%u\r\n",
|
||||
@@ -1912,6 +1916,7 @@ void gps_background_task(void)
|
||||
gps_process_isr_events();
|
||||
if(gps_take_timer_maintenance_pending())
|
||||
{
|
||||
gps_pps_service();
|
||||
gps_reconcile_stopped_tracking_channels();
|
||||
gps_mark_stale_measurements();
|
||||
gps_check_acq_stuck_busy();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "gps/gps_interrupt_handlers.h"
|
||||
#include "gps/gps_nav_epoch.h"
|
||||
#include "gps/gps_pps.h"
|
||||
#include "gps/gps_time_anchor.h"
|
||||
#include "gps/defines.h"
|
||||
#include <bsp/irq.h>
|
||||
|
||||
@@ -61,6 +62,17 @@ volatile gps_ephem_ll_t* tracking_node = NULL;
|
||||
static volatile bool gps_initial_position_valid = false;
|
||||
static volatile bool gps_ionoutc_valid = false;
|
||||
static ionoutc_t gps_last_ionoutc;
|
||||
static gps_pps_quality_selector_t gps_pps_quality_selector;
|
||||
static gps_pps_profile_t gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
static unsigned int gps_pps_status_log_decimator;
|
||||
static bool gps_pps_reference_svid_valid;
|
||||
static unsigned int gps_pps_reference_svid;
|
||||
static bool gps_pps_filter_reference_svid_valid;
|
||||
static unsigned int gps_pps_filter_reference_svid;
|
||||
|
||||
#if !defined(GPS_PPS_STATUS_LOG_INTERVAL) || (GPS_PPS_STATUS_LOG_INTERVAL < 1)
|
||||
#error "GPS_PPS_STATUS_LOG_INTERVAL must be at least one"
|
||||
#endif
|
||||
|
||||
volatile uint64_t gps_systick_ticks = 0;
|
||||
|
||||
@@ -658,6 +670,7 @@ static unsigned int gps_collect_completed_epoch_locked(
|
||||
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
|
||||
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
|
||||
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT],
|
||||
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT],
|
||||
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT],
|
||||
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT],
|
||||
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT],
|
||||
@@ -690,6 +703,9 @@ static unsigned int gps_collect_completed_epoch_locked(
|
||||
snapshot_ips[count] = ip_no;
|
||||
snapshot_pps_counters[count] =
|
||||
epoch->pps_counter[ip_no];
|
||||
snapshot_pps_sample_delays[count] =
|
||||
(double)epoch->raw_sample[ip_no] -
|
||||
epoch->corrected_abs_sample[ip_no];
|
||||
snapshot_pps_sequences[count] =
|
||||
epoch->pps_capture_sequence[ip_no];
|
||||
snapshot_local_timestamps[count] =
|
||||
@@ -707,6 +723,7 @@ static unsigned int gps_select_coherent_nav_snapshot(
|
||||
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
|
||||
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
|
||||
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT],
|
||||
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT],
|
||||
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT],
|
||||
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT],
|
||||
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT],
|
||||
@@ -754,6 +771,8 @@ static unsigned int gps_select_coherent_nav_snapshot(
|
||||
snapshot_ips[destination] = snapshot_ips[source];
|
||||
snapshot_pps_counters[destination] =
|
||||
snapshot_pps_counters[source];
|
||||
snapshot_pps_sample_delays[destination] =
|
||||
snapshot_pps_sample_delays[source];
|
||||
snapshot_pps_sequences[destination] =
|
||||
snapshot_pps_sequences[source];
|
||||
snapshot_local_timestamps[destination] =
|
||||
@@ -799,12 +818,15 @@ static uint64_t gps_nav_median_local_timestamp(
|
||||
|
||||
static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
const uint64_t pps_counters[GPS_TRCK_IP_COUNT],
|
||||
const double pps_sample_delays[GPS_TRCK_IP_COUNT],
|
||||
const uint32_t pps_capture_sequences[GPS_TRCK_IP_COUNT],
|
||||
const gps_tracking_measurement_t* measurements,
|
||||
unsigned int satellite_count,
|
||||
gps_nav_solution_t* solution_out)
|
||||
{
|
||||
if((ephemeris == NULL) || (measurements == NULL) ||
|
||||
if((ephemeris == NULL) || (pps_counters == NULL) ||
|
||||
(pps_sample_delays == NULL) ||
|
||||
(pps_capture_sequences == NULL) || (measurements == NULL) ||
|
||||
(solution_out == NULL) ||
|
||||
(satellite_count < GPS_NAV_MIN_WLS_SATELLITES))
|
||||
{
|
||||
@@ -829,6 +851,8 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
ionoutc_t ionoutc_snapshot;
|
||||
const ionoutc_t* ionoutc = gps_get_ionoutc(&ionoutc_snapshot) ? &ionoutc_snapshot : NULL;
|
||||
bool ok = false;
|
||||
bool pps_clock_bias_drift_valid = false;
|
||||
double pps_clock_bias_drift_mps = 0.0;
|
||||
gps_nav_solution_t solution;
|
||||
memset(&solution, 0, sizeof(solution));
|
||||
memset(solution_out, 0, sizeof(*solution_out));
|
||||
@@ -866,16 +890,30 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
satellitePositions, velocities);
|
||||
}
|
||||
|
||||
double minimum = psTVLtime[0];
|
||||
unsigned int minimum_index = 0u;
|
||||
for(unsigned int i = 1u ; i < satellite_count ; i++)
|
||||
unsigned int satellite_ids[GPS_TRCK_IP_COUNT];
|
||||
for(unsigned int i = 0u; i < satellite_count; i++)
|
||||
{
|
||||
if(psTVLtime[i] < minimum)
|
||||
{
|
||||
minimum = psTVLtime[i];
|
||||
minimum_index = i;
|
||||
}
|
||||
satellite_ids[i] = ephemeris[i].SVID;
|
||||
}
|
||||
unsigned int reference_index = 0u;
|
||||
if(!gps_pps_select_reference(
|
||||
satellite_ids, psTVLtime, satellite_count,
|
||||
gps_pps_reference_svid_valid, gps_pps_reference_svid,
|
||||
&reference_index))
|
||||
{
|
||||
goto cleanup;
|
||||
}
|
||||
double reference_capture_time = psTVLtime[reference_index];
|
||||
uint64_t pps_reference_counter = 0u;
|
||||
double pps_capture_correction_sec = 0.0;
|
||||
bool pps_reference_counter_valid =
|
||||
gps_time_correct_capture_counter(
|
||||
pps_counters[reference_index],
|
||||
pps_sample_delays[reference_index],
|
||||
GPS_RF_SAMPLE_RATE_HZ,
|
||||
(double)GPS_PPS_CLOCK_HZ,
|
||||
&pps_reference_counter,
|
||||
&pps_capture_correction_sec);
|
||||
|
||||
preciseTxTimes = create_matrix(1, satellite_count, "preciseTxTimes");
|
||||
localPseudoRanges = create_matrix(1, satellite_count, "localPseudoRanges");
|
||||
@@ -894,7 +932,8 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
|
||||
for(unsigned int i = 0 ; i < satellite_count ; i++)
|
||||
{
|
||||
preciseTxTimes->matrix[0][i] = psTVLtime[i] - minimum + STARTOFFSET;
|
||||
preciseTxTimes->matrix[0][i] =
|
||||
psTVLtime[i] - reference_capture_time + STARTOFFSET;
|
||||
localPseudoRanges->matrix[0][i] = preciseTxTimes->matrix[0][i] * (SPEED_OF_LIGHT / 1000.0);
|
||||
|
||||
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "psTVLtime=%.10lf preciseTx=%.10lf pseudo=%.10lf clockCorr=%.10lf corrected=%.10lf\r\n",
|
||||
@@ -957,6 +996,14 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
solution.velocity_ecef_mps[1] +
|
||||
solution.velocity_ecef_mps[2] *
|
||||
solution.velocity_ecef_mps[2]);
|
||||
pps_clock_bias_drift_valid =
|
||||
gps_pps_reference_bias_drift(
|
||||
solution.ecef_m,
|
||||
solution.velocity_ecef_mps,
|
||||
satellite_positions_array[reference_index],
|
||||
satellite_velocities_array[reference_index],
|
||||
clockDrift[reference_index],
|
||||
&pps_clock_bias_drift_mps);
|
||||
}
|
||||
}
|
||||
if(ok && solution.fde_applied)
|
||||
@@ -971,27 +1018,221 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
}
|
||||
}
|
||||
bool pps_reference_used = ok &&
|
||||
((solution.used_row_mask & (1u << minimum_index)) != 0u);
|
||||
if(ok && solution.solution_valid && pps_reference_used &&
|
||||
(pps_capture_sequences[minimum_index] != 0u))
|
||||
((solution.used_row_mask & (1u << reference_index)) != 0u);
|
||||
if(ok && solution.solution_valid && pps_reference_used)
|
||||
{
|
||||
gps_pps_reference_svid = ephemeris[reference_index].SVID;
|
||||
gps_pps_reference_svid_valid = true;
|
||||
}
|
||||
else if(ok && !pps_reference_used)
|
||||
{
|
||||
/* Select a new datum on the next WLS solve if FDE rejected this one. */
|
||||
gps_pps_reference_svid_valid = false;
|
||||
}
|
||||
gps_pps_quality_metrics_t pps_quality = {0};
|
||||
pps_quality.solution_valid = ok && solution.solution_valid;
|
||||
if(pps_quality.solution_valid)
|
||||
{
|
||||
double cn0_sum = 0.0;
|
||||
double minimum_cn0 = INFINITY;
|
||||
unsigned int cn0_count = 0u;
|
||||
for(unsigned int row = 0u; row < satellite_count; row++)
|
||||
{
|
||||
if(((solution.used_row_mask & (1u << row)) != 0u) &&
|
||||
isfinite(solution.cn0_dbhz[row]))
|
||||
{
|
||||
cn0_sum += solution.cn0_dbhz[row];
|
||||
minimum_cn0 = fmin(minimum_cn0, solution.cn0_dbhz[row]);
|
||||
cn0_count++;
|
||||
}
|
||||
}
|
||||
pps_quality.tdop = solution.tdop;
|
||||
pps_quality.gdop = solution.gdop;
|
||||
pps_quality.clock_bias_sigma_m =
|
||||
sqrt(solution.clock_bias_variance_m2);
|
||||
pps_quality.mean_cn0_dbhz =
|
||||
(cn0_count != 0u) ? (cn0_sum / (double)cn0_count) : NAN;
|
||||
pps_quality.minimum_cn0_dbhz =
|
||||
(cn0_count != 0u) ? minimum_cn0 : NAN;
|
||||
pps_quality.chi_square_ratio = solution.chi_square_ratio;
|
||||
pps_quality.velocity_residual_rms_mps =
|
||||
solution.velocity_valid ?
|
||||
solution.velocity_residual_rms_mps : 0.0;
|
||||
pps_quality.used_satellites = solution.used_satellite_count;
|
||||
pps_quality.residual_test_valid = solution.residual_test_valid;
|
||||
pps_quality.velocity_valid = solution.velocity_valid;
|
||||
pps_quality.fde_applied = solution.fde_applied;
|
||||
}
|
||||
bool pps_profile_changed = false;
|
||||
/*
|
||||
* An invalid WLS epoch carries no usable quality evidence and is not fed
|
||||
* into the profile selector. It is also never committed below. This
|
||||
* lets isolated chi-square rejects pass without resetting a converged
|
||||
* loop; continuous loss is independently caught by gps_pps_service()'s
|
||||
* two-second reference watchdog.
|
||||
*/
|
||||
gps_pps_profile_t pps_profile = gps_pps_quality_selector.profile;
|
||||
if(pps_quality.solution_valid)
|
||||
{
|
||||
pps_profile = gps_pps_quality_selector_update(
|
||||
&gps_pps_quality_selector, &pps_quality,
|
||||
&pps_profile_changed);
|
||||
}
|
||||
if(pps_profile == GPS_PPS_PROFILE_HOLDOVER)
|
||||
{
|
||||
gps_pps_set_holdover(true);
|
||||
}
|
||||
gps_pps_profile_t desired_loop_profile =
|
||||
((pps_profile != GPS_PPS_PROFILE_HOLDOVER) &&
|
||||
!gps_pps_is_locked()) ?
|
||||
GPS_PPS_PROFILE_ACQUIRE : pps_profile;
|
||||
if((desired_loop_profile != GPS_PPS_PROFILE_HOLDOVER) &&
|
||||
(desired_loop_profile != gps_pps_applied_profile) &&
|
||||
gps_pps_set_loop_profile(desired_loop_profile))
|
||||
{
|
||||
gps_pps_applied_profile = desired_loop_profile;
|
||||
}
|
||||
if(pps_profile_changed)
|
||||
{
|
||||
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
|
||||
"PPS quality profile=%s tdop=%.2lf gdop=%.2lf "
|
||||
"clock_sigma=%.2lf cn0_mean=%.1lf cn0_min=%.1lf "
|
||||
"chi2_ratio=%.3lf velocity_rms=%.3lf\r\n",
|
||||
gps_pps_profile_name(pps_profile),
|
||||
pps_quality.tdop, pps_quality.gdop,
|
||||
pps_quality.clock_bias_sigma_m,
|
||||
pps_quality.mean_cn0_dbhz,
|
||||
pps_quality.minimum_cn0_dbhz,
|
||||
pps_quality.chi_square_ratio,
|
||||
pps_quality.velocity_residual_rms_mps);
|
||||
}
|
||||
|
||||
if(ok && solution.solution_valid && pps_reference_used &&
|
||||
(pps_capture_sequences[reference_index] != 0u) &&
|
||||
pps_reference_counter_valid &&
|
||||
(pps_profile != GPS_PPS_PROFILE_HOLDOVER))
|
||||
{
|
||||
unsigned int reference_svid = ephemeris[reference_index].SVID;
|
||||
if(!gps_pps_filter_reference_svid_valid ||
|
||||
(gps_pps_filter_reference_svid != reference_svid))
|
||||
{
|
||||
gps_pps_reset_time_filter();
|
||||
gps_pps_filter_reference_svid = reference_svid;
|
||||
gps_pps_filter_reference_svid_valid = true;
|
||||
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
|
||||
"PPS WLS datum re-anchor ref_svid=%u\r\n",
|
||||
reference_svid);
|
||||
}
|
||||
double filtered_clock_bias_m = 0.0;
|
||||
double clock_innovation_m = 0.0;
|
||||
gps_pps_time_filter_result_t filter_result =
|
||||
gps_pps_filter_clock_bias(
|
||||
pps_reference_counter,
|
||||
solution.clock_bias_m,
|
||||
pps_clock_bias_drift_valid,
|
||||
pps_clock_bias_drift_mps,
|
||||
solution.tdop,
|
||||
&filtered_clock_bias_m,
|
||||
&clock_innovation_m);
|
||||
if(filter_result == GPS_PPS_TIME_FILTER_REJECTED)
|
||||
{
|
||||
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
|
||||
"PPS clock observation rejected profile=%s bias=%.3lf "
|
||||
"drift=%.3lf tdop=%.2lf\r\n",
|
||||
gps_pps_profile_name(pps_profile),
|
||||
solution.clock_bias_m,
|
||||
solution.clock_drift_mps,
|
||||
solution.tdop);
|
||||
goto pps_anchor_done;
|
||||
}
|
||||
/* The synthetic STARTOFFSET datum is absorbed by WLS b. Removing b/c
|
||||
* recovers the reference satellite's physical flight time; POSTPONE_TOW
|
||||
* is applied exactly once here, just as it is in satpos(). */
|
||||
double receiver_tow = ephemeris[minimum_index].tow.sec + POSTPONE_TOW +
|
||||
(STARTOFFSET / 1000.0) - (solution.clock_bias_m / SPEED_OF_LIGHT);
|
||||
if(!gps_pps_commit_solution(pps_counters[minimum_index],
|
||||
ephemeris[minimum_index].tow.week,
|
||||
receiver_tow))
|
||||
double receiver_tow = ephemeris[reference_index].tow.sec + POSTPONE_TOW +
|
||||
(STARTOFFSET / 1000.0) -
|
||||
(filtered_clock_bias_m / SPEED_OF_LIGHT);
|
||||
/*
|
||||
* Keep holdover asserted throughout initial settling and re-anchor
|
||||
* commits. A coarse commit is accepted while held and clears lock.
|
||||
* gps_pps_commit_solution() releases holdover immediately before a
|
||||
* normal mailbox request so the first coherent recovery update is
|
||||
* applied by PPS v2.2 without stepping the held physical timebase.
|
||||
*/
|
||||
if(filter_result == GPS_PPS_TIME_FILTER_COARSE)
|
||||
{
|
||||
gps_pps_set_holdover(true);
|
||||
}
|
||||
if(!gps_pps_commit_solution(pps_reference_counter,
|
||||
ephemeris[reference_index].tow.week,
|
||||
receiver_tow,
|
||||
filter_result == GPS_PPS_TIME_FILTER_COARSE))
|
||||
{
|
||||
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "PPS anchor skipped: mailbox busy or invalid time\r\n");
|
||||
}
|
||||
gps_pps_status_log_decimator++;
|
||||
if(gps_pps_status_log_decimator >= GPS_PPS_STATUS_LOG_INTERVAL)
|
||||
{
|
||||
gps_pps_status_log_decimator = 0u;
|
||||
gps_pps_diagnostics_t pps_diagnostics;
|
||||
bool pps_diagnostics_valid =
|
||||
gps_pps_read_diagnostics(&pps_diagnostics);
|
||||
double hardware_phase_ns = pps_diagnostics_valid ?
|
||||
((double)pps_diagnostics.phase_error_q32 /
|
||||
4294967296.0) *
|
||||
(1000000000.0 / (double)GPS_PPS_CLOCK_HZ) : NAN;
|
||||
double rate_command_ppm = pps_diagnostics_valid ?
|
||||
((double)pps_diagnostics.rate_word_q32 /
|
||||
4294967296.0) * 1000000.0 : NAN;
|
||||
double integrator_ppm = pps_diagnostics_valid ?
|
||||
((double)pps_diagnostics.integrator_q32 /
|
||||
4294967296.0) * 1000000.0 : NAN;
|
||||
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
|
||||
"PPS profile=%s mode=%s locked=%u tdop=%.2lf gdop=%.2lf "
|
||||
"clock_sigma=%.2lf cn0=%.1lf/%.1lf innovation=%.2lfm "
|
||||
"raw_bias=%.3lfm filtered_bias=%.3lfm "
|
||||
"clock_drift=%.3lfmps bias_drift=%.3lfmps ref_svid=%u "
|
||||
"capture_corr=%.2lfns "
|
||||
"hw_phase=%.2lfns rate_cmd=%.3lfppm integ=%.3lfppm "
|
||||
"pulse=%u seq=%u status=0x%03x\r\n",
|
||||
gps_pps_profile_name(pps_profile),
|
||||
(filter_result == GPS_PPS_TIME_FILTER_COARSE) ?
|
||||
"coarse" : "tracking",
|
||||
gps_pps_is_locked() ? 1u : 0u,
|
||||
pps_quality.tdop, pps_quality.gdop,
|
||||
pps_quality.clock_bias_sigma_m,
|
||||
pps_quality.mean_cn0_dbhz,
|
||||
pps_quality.minimum_cn0_dbhz,
|
||||
clock_innovation_m,
|
||||
solution.clock_bias_m,
|
||||
filtered_clock_bias_m,
|
||||
solution.clock_drift_mps,
|
||||
pps_clock_bias_drift_mps,
|
||||
ephemeris[reference_index].SVID,
|
||||
pps_capture_correction_sec * 1.0e9,
|
||||
hardware_phase_ns, rate_command_ppm, integrator_ppm,
|
||||
pps_diagnostics_valid ?
|
||||
pps_diagnostics.pulse_count : 0u,
|
||||
pps_diagnostics_valid ?
|
||||
pps_diagnostics.active_sequence : 0u,
|
||||
pps_diagnostics_valid ?
|
||||
pps_diagnostics.status : 0u);
|
||||
}
|
||||
}
|
||||
else if(ok && solution.solution_valid && pps_reference_used &&
|
||||
(pps_capture_sequences[reference_index] != 0u) &&
|
||||
!pps_reference_counter_valid)
|
||||
{
|
||||
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
|
||||
"PPS anchor rejected: invalid fractional capture correction "
|
||||
"ref_svid=%u sample_delay=%.9lf\r\n",
|
||||
ephemeris[reference_index].SVID,
|
||||
pps_sample_delays[reference_index]);
|
||||
}
|
||||
else if(ok && solution.solution_valid && !pps_reference_used)
|
||||
{
|
||||
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
|
||||
"PPS anchor protected: FDE excluded pseudorange reference row=%u SVID=%u\r\n",
|
||||
minimum_index, ephemeris[minimum_index].SVID);
|
||||
reference_index, ephemeris[reference_index].SVID);
|
||||
}
|
||||
else if(ok && !solution.solution_valid)
|
||||
{
|
||||
@@ -999,6 +1240,7 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
|
||||
"PPS/last_solution protected from invalid best-effort NAVSOL quality=%.1lf ratio=%.3lf\r\n",
|
||||
solution.quality_score, solution.chi_square_ratio);
|
||||
}
|
||||
pps_anchor_done:
|
||||
|
||||
cleanup:
|
||||
if(preciseTxTimes != NULL) free_matrix(preciseTxTimes);
|
||||
@@ -1140,6 +1382,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
|
||||
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT];
|
||||
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT];
|
||||
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT];
|
||||
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT];
|
||||
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT];
|
||||
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT];
|
||||
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT];
|
||||
@@ -1186,6 +1429,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
|
||||
candidate_count = gps_collect_completed_epoch_locked(
|
||||
&completed_epoch,
|
||||
snapshot, snapshot_ips, snapshot_pps_counters,
|
||||
snapshot_pps_sample_delays,
|
||||
snapshot_pps_sequences,
|
||||
snapshot_local_timestamps, snapshot_raw_samples,
|
||||
snapshot_measurements);
|
||||
@@ -1211,6 +1455,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
|
||||
{
|
||||
satellite_count = gps_select_coherent_nav_snapshot(
|
||||
snapshot, snapshot_ips, snapshot_pps_counters,
|
||||
snapshot_pps_sample_delays,
|
||||
snapshot_pps_sequences,
|
||||
snapshot_local_timestamps, snapshot_raw_samples,
|
||||
snapshot_measurements,
|
||||
@@ -1267,6 +1512,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
|
||||
memset(&nav_solution, 0, sizeof(nav_solution));
|
||||
uint64_t nav_software_start_cycles = gps_read_cycle_counter();
|
||||
bool nav_ok = nav_software_from_snapshot(snapshot, snapshot_pps_counters,
|
||||
snapshot_pps_sample_delays,
|
||||
snapshot_pps_sequences,
|
||||
snapshot_measurements,
|
||||
satellite_count,
|
||||
@@ -1486,6 +1732,13 @@ 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_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
gps_pps_status_log_decimator = 0u;
|
||||
gps_pps_reference_svid_valid = false;
|
||||
gps_pps_reference_svid = 0u;
|
||||
gps_pps_filter_reference_svid_valid = false;
|
||||
gps_pps_filter_reference_svid = 0u;
|
||||
gps_ionoutc_valid = false;
|
||||
gps_last_ionoutc = (ionoutc_t){0};
|
||||
gps_nav_epoch_reject_total = 0u;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "gps/gps_pps.h"
|
||||
#include "gps/gps_macros.h"
|
||||
|
||||
#include <bsp/soc.h>
|
||||
#include <math.h>
|
||||
@@ -39,6 +40,7 @@ typedef struct {
|
||||
volatile uint32_t version; /* 0x7c */
|
||||
volatile uint32_t kp_q46_hi; /* 0x80 */
|
||||
volatile uint32_t ki_q46_hi; /* 0x84 */
|
||||
volatile uint32_t loop_config; /* 0x88, PPS v2.2+ */
|
||||
} gps_pps_regs_t;
|
||||
|
||||
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
|
||||
@@ -46,23 +48,44 @@ _Static_assert(offsetof(gps_pps_regs_t, capture_counter_lo) == 0x14, "PPS captur
|
||||
_Static_assert(offsetof(gps_pps_regs_t, epoch_counter_lo) == 0x20, "PPS epoch offset mismatch");
|
||||
_Static_assert(offsetof(gps_pps_regs_t, commit) == 0x44, "PPS commit offset mismatch");
|
||||
_Static_assert(offsetof(gps_pps_regs_t, kp_q46_hi) == 0x80, "PPS coefficient offset mismatch");
|
||||
_Static_assert(offsetof(gps_pps_regs_t, loop_config) == 0x88, "PPS loop-config offset mismatch");
|
||||
#endif
|
||||
|
||||
enum {
|
||||
GPS_PPS_CONTROL_ENABLE = 1u << 0,
|
||||
GPS_PPS_CONTROL_TRACK = 1u << 1,
|
||||
GPS_PPS_CONTROL_HOLDOVER = 1u << 2,
|
||||
GPS_PPS_CONTROL_CLEAR_STATUS = 1u << 3,
|
||||
GPS_PPS_STATUS_ALIGNED = 1u << 1,
|
||||
GPS_PPS_STATUS_COMMIT_BUSY = 1u << 4,
|
||||
GPS_PPS_STATUS_DIAG_BUSY = 1u << 10,
|
||||
GPS_PPS_STATUS_LOCKED = 1u << 11,
|
||||
GPS_PPS_COMMIT_VALID = 1u << 0,
|
||||
GPS_PPS_COMMIT_COARSE = 1u << 1
|
||||
};
|
||||
|
||||
#define GPS_PPS_VERSION_ADAPTIVE_LOOP 0x00020200u
|
||||
#define GPS_PPS_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
|
||||
|
||||
static volatile gps_pps_regs_t* const gps_pps =
|
||||
(volatile gps_pps_regs_t*)BSP_GPS_PPS_BASE;
|
||||
static uint32_t gps_pps_solution_sequence;
|
||||
static uint32_t gps_pps_version;
|
||||
static unsigned int gps_pps_nav_solve_tick;
|
||||
static gps_pps_profile_t gps_pps_active_loop_profile;
|
||||
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 gps_pps_time_filter_t gps_pps_time_filter;
|
||||
|
||||
static uint64_t gps_pps_read_u64(
|
||||
volatile uint32_t* lo, volatile uint32_t* hi)
|
||||
{
|
||||
uint32_t low_word = *lo;
|
||||
uint32_t high_word = *hi;
|
||||
return ((uint64_t)high_word << 32) | low_word;
|
||||
}
|
||||
|
||||
static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int64_t value)
|
||||
{
|
||||
@@ -71,29 +94,86 @@ static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int6
|
||||
*hi = (uint32_t)(encoded >> 32);
|
||||
}
|
||||
|
||||
void gps_pps_init(unsigned int nav_solve_tick)
|
||||
static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
const double pi = 3.14159265358979323846;
|
||||
const double natural_frequency_hz = 0.035;
|
||||
const double damping = 0.9;
|
||||
const double update_period_sec = (nav_solve_tick != 0u) ?
|
||||
((double)nav_solve_tick * 0.020) : 0.200;
|
||||
const double wn = 2.0 * pi * natural_frequency_hz;
|
||||
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 * damping * wn / GPS_PPS_CLOCK_HZ) * q46);
|
||||
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);
|
||||
|
||||
gps_pps->control = 0u;
|
||||
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);
|
||||
gps_pps->max_slew_q32 = 85899u; /* 20 ppm of one Q32 tick per local clock. */
|
||||
gps_pps->max_slew_q32 = max_slew_q32;
|
||||
if(gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP)
|
||||
{
|
||||
gps_pps->loop_config =
|
||||
((parameters.lock_acquire_ticks & 0xffu) << 8) |
|
||||
((parameters.lock_release_ticks & 0xffu) << 16) |
|
||||
((parameters.lock_acquire_updates & 0xffu) << 24);
|
||||
}
|
||||
}
|
||||
|
||||
void gps_pps_init(unsigned int nav_solve_tick)
|
||||
{
|
||||
gps_pps_nav_solve_tick = nav_solve_tick;
|
||||
gps_pps->control = 0u;
|
||||
gps_pps_version = gps_pps->version;
|
||||
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
|
||||
* timebase instead of feeding a new epoch into the previous PI state. */
|
||||
gps_pps_solution_sequence = gps_pps->active_sequence;
|
||||
gps_pps_diagnostics_t initial_diagnostics;
|
||||
if(gps_pps_read_diagnostics(&initial_diagnostics))
|
||||
{
|
||||
gps_pps_solution_sequence = initial_diagnostics.active_sequence;
|
||||
}
|
||||
else
|
||||
{
|
||||
gps_pps_solution_sequence = gps_pps->active_sequence;
|
||||
}
|
||||
gps_pps_force_coarse = true;
|
||||
gps_pps_reference_committed = false;
|
||||
gps_pps_holdover = true;
|
||||
gps_pps_last_reference_counter = 0u;
|
||||
gps_pps_time_filter_init(&gps_pps_time_filter);
|
||||
gps_pps->control = GPS_PPS_CONTROL_ENABLE | GPS_PPS_CONTROL_TRACK |
|
||||
GPS_PPS_CONTROL_HOLDOVER |
|
||||
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",
|
||||
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);
|
||||
}
|
||||
|
||||
void gps_pps_service(void)
|
||||
{
|
||||
if(!gps_pps_reference_committed || gps_pps_holdover)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reading LO snapshots HI in hardware, so this is a coherent 64-bit
|
||||
* free-running counter read over the 32-bit APB interface. */
|
||||
uint32_t now_lo = gps_pps->raw_counter_lo;
|
||||
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_set_holdover(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool gps_pps_is_locked(void)
|
||||
@@ -101,6 +181,70 @@ bool gps_pps_is_locked(void)
|
||||
return (gps_pps->status & GPS_PPS_STATUS_LOCKED) != 0u;
|
||||
}
|
||||
|
||||
bool gps_pps_read_diagnostics(gps_pps_diagnostics_t* diagnostics)
|
||||
{
|
||||
if((diagnostics == NULL) ||
|
||||
((gps_pps->status & GPS_PPS_STATUS_DIAG_BUSY) != 0u))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
gps_pps->diag_snapshot = 1u;
|
||||
bool diag_started = false;
|
||||
for(unsigned int attempt = 0u; attempt < 1000u; attempt++)
|
||||
{
|
||||
uint32_t status = gps_pps->status;
|
||||
if((status & GPS_PPS_STATUS_DIAG_BUSY) != 0u)
|
||||
{
|
||||
diag_started = true;
|
||||
}
|
||||
/*
|
||||
* The 125 MHz snapshot/ack can complete between two CPU MMIO
|
||||
* reads. In that legitimate case DIAG_BUSY is never observed.
|
||||
* Sixteen ordered APB reads are far longer than the request,
|
||||
* snapshot, and two-flop return path; accepting a clear status
|
||||
* after that delay still guarantees that the frozen group is new.
|
||||
*/
|
||||
else if(diag_started || (attempt >= 16u))
|
||||
{
|
||||
diagnostics->status = status;
|
||||
diagnostics->active_sequence = gps_pps->active_sequence;
|
||||
diagnostics->pulse_count = gps_pps->pulse_count;
|
||||
diagnostics->phase_error_q32 = (int64_t)gps_pps_read_u64(
|
||||
&gps_pps->phase_error_lo, &gps_pps->phase_error_hi);
|
||||
diagnostics->integrator_q32 = (int64_t)gps_pps_read_u64(
|
||||
&gps_pps->integrator_lo, &gps_pps->integrator_hi);
|
||||
diagnostics->rate_word_q32 = (int64_t)gps_pps_read_u64(
|
||||
&gps_pps->rate_word_lo, &gps_pps->rate_word_hi);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool gps_pps_set_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
if((profile == GPS_PPS_PROFILE_HOLDOVER) ||
|
||||
((gps_pps->status & GPS_PPS_STATUS_COMMIT_BUSY) != 0u))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
gps_pps_write_loop_profile(profile);
|
||||
gps_pps_active_loop_profile = profile;
|
||||
return true;
|
||||
}
|
||||
|
||||
void gps_pps_set_holdover(bool holdover)
|
||||
{
|
||||
uint32_t control = GPS_PPS_CONTROL_ENABLE | GPS_PPS_CONTROL_TRACK;
|
||||
if(holdover)
|
||||
{
|
||||
control |= GPS_PPS_CONTROL_HOLDOVER;
|
||||
}
|
||||
gps_pps->control = control;
|
||||
gps_pps_holdover = holdover;
|
||||
}
|
||||
|
||||
bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* sequence)
|
||||
{
|
||||
if((channel >= BSP_GPS_TRCK_COUNT) || (counter == NULL) || (sequence == NULL))
|
||||
@@ -123,7 +267,37 @@ bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* seq
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiver_tow_sec)
|
||||
gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
|
||||
uint64_t epoch_counter,
|
||||
double measured_clock_bias_m,
|
||||
bool clock_drift_valid,
|
||||
double measured_clock_drift_mps,
|
||||
double tdop,
|
||||
double* filtered_clock_bias_m_out,
|
||||
double* innovation_m_out)
|
||||
{
|
||||
gps_pps_loop_profile_t parameters =
|
||||
gps_pps_loop_profile(gps_pps_active_loop_profile);
|
||||
return gps_pps_time_filter_update(
|
||||
&gps_pps_time_filter, epoch_counter, (double)GPS_PPS_CLOCK_HZ,
|
||||
measured_clock_bias_m, clock_drift_valid,
|
||||
measured_clock_drift_mps, tdop,
|
||||
parameters.software_bias_gain,
|
||||
parameters.software_settle_updates,
|
||||
filtered_clock_bias_m_out, innovation_m_out);
|
||||
}
|
||||
|
||||
void gps_pps_reset_time_filter(void)
|
||||
{
|
||||
gps_pps_time_filter_init(&gps_pps_time_filter);
|
||||
gps_pps_force_coarse = true;
|
||||
}
|
||||
|
||||
bool gps_pps_commit_solution(
|
||||
uint64_t epoch_counter,
|
||||
int gps_week,
|
||||
double receiver_tow_sec,
|
||||
bool force_coarse)
|
||||
{
|
||||
if(!isfinite(receiver_tow_sec))
|
||||
{
|
||||
@@ -160,7 +334,7 @@ bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiv
|
||||
uint64_t gps_seconds = ((uint64_t)(unsigned int)gps_week * 604800ULL) +
|
||||
(uint64_t)whole_tow;
|
||||
uint32_t next_sequence = gps_pps_solution_sequence + 1u;
|
||||
bool coarse = gps_pps_force_coarse ||
|
||||
bool coarse = force_coarse || gps_pps_force_coarse ||
|
||||
((gps_pps->status & GPS_PPS_STATUS_ALIGNED) == 0u);
|
||||
|
||||
gps_pps->epoch_counter_lo = (uint32_t)epoch_counter;
|
||||
@@ -169,9 +343,24 @@ bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiv
|
||||
gps_pps->gps_seconds_hi = (uint32_t)(gps_seconds >> 32);
|
||||
gps_pps->gps_fraction_q32 = (uint32_t)fraction_q32;
|
||||
gps_pps->epoch_sequence = next_sequence;
|
||||
/*
|
||||
* PPS v2.2 deliberately rejects a normal PI update while HOLDOVER is
|
||||
* asserted. Release it only after every shadow word is in place and
|
||||
* immediately before toggling COMMIT, so the first coherent post-gap
|
||||
* observation is applied instead of merely acknowledged. The physical
|
||||
* PPS already runs from the held rate word, so this does not introduce a
|
||||
* phase step; only the validity status precedes the PI apply by the
|
||||
* bounded mailbox pipeline latency.
|
||||
*/
|
||||
if(!coarse && gps_pps_holdover)
|
||||
{
|
||||
gps_pps_set_holdover(false);
|
||||
}
|
||||
gps_pps->commit = GPS_PPS_COMMIT_VALID |
|
||||
(coarse ? GPS_PPS_COMMIT_COARSE : 0u);
|
||||
gps_pps_solution_sequence = next_sequence;
|
||||
gps_pps_force_coarse = false;
|
||||
gps_pps_reference_committed = true;
|
||||
gps_pps_last_reference_counter = epoch_counter;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "gps/gps_pps_quality.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#define GPS_PPS_PROFILE_UPGRADE_UPDATES 8u
|
||||
#define GPS_PPS_PROFILE_DEGRADE_UPDATES 3u
|
||||
|
||||
static bool gps_pps_metrics_finite(const gps_pps_quality_metrics_t* metrics)
|
||||
{
|
||||
return (metrics != NULL) &&
|
||||
isfinite(metrics->tdop) &&
|
||||
isfinite(metrics->gdop) &&
|
||||
isfinite(metrics->clock_bias_sigma_m) &&
|
||||
isfinite(metrics->mean_cn0_dbhz) &&
|
||||
isfinite(metrics->minimum_cn0_dbhz) &&
|
||||
isfinite(metrics->velocity_residual_rms_mps);
|
||||
}
|
||||
|
||||
static gps_pps_profile_t gps_pps_classify_quality(
|
||||
const gps_pps_quality_metrics_t* metrics)
|
||||
{
|
||||
if(!gps_pps_metrics_finite(metrics) ||
|
||||
!metrics->solution_valid ||
|
||||
(metrics->used_satellites < 4u) ||
|
||||
(metrics->tdop < 0.0) ||
|
||||
(metrics->gdop < 0.0) ||
|
||||
(metrics->clock_bias_sigma_m < 0.0))
|
||||
{
|
||||
return GPS_PPS_PROFILE_HOLDOVER;
|
||||
}
|
||||
|
||||
bool residual_ok =
|
||||
metrics->residual_test_valid ?
|
||||
(isfinite(metrics->chi_square_ratio) &&
|
||||
(metrics->chi_square_ratio <= 1.0)) :
|
||||
(metrics->used_satellites == 4u);
|
||||
bool robust =
|
||||
residual_ok &&
|
||||
(metrics->tdop <= 8.0) &&
|
||||
(metrics->gdop <= 12.0) &&
|
||||
(metrics->clock_bias_sigma_m <= 100.0) &&
|
||||
(metrics->mean_cn0_dbhz >= 22.0) &&
|
||||
(metrics->minimum_cn0_dbhz >= 14.0) &&
|
||||
(!metrics->velocity_valid ||
|
||||
(metrics->velocity_residual_rms_mps <= 3.0));
|
||||
if(!robust)
|
||||
{
|
||||
return GPS_PPS_PROFILE_HOLDOVER;
|
||||
}
|
||||
|
||||
bool precision =
|
||||
(metrics->used_satellites >= 6u) &&
|
||||
metrics->residual_test_valid &&
|
||||
!metrics->fde_applied &&
|
||||
(metrics->tdop <= 3.0) &&
|
||||
(metrics->gdop <= 6.0) &&
|
||||
(metrics->clock_bias_sigma_m <= 35.0) &&
|
||||
(metrics->mean_cn0_dbhz >= 30.0) &&
|
||||
(metrics->minimum_cn0_dbhz >= 20.0) &&
|
||||
(metrics->chi_square_ratio <= 0.75) &&
|
||||
metrics->velocity_valid &&
|
||||
(metrics->velocity_residual_rms_mps <= 1.0);
|
||||
return precision ? GPS_PPS_PROFILE_PRECISION : GPS_PPS_PROFILE_ROBUST;
|
||||
}
|
||||
|
||||
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector)
|
||||
{
|
||||
if(selector != NULL)
|
||||
{
|
||||
memset(selector, 0, sizeof(*selector));
|
||||
selector->profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
selector->candidate = GPS_PPS_PROFILE_HOLDOVER;
|
||||
}
|
||||
}
|
||||
|
||||
gps_pps_profile_t gps_pps_quality_selector_update(
|
||||
gps_pps_quality_selector_t* selector,
|
||||
const gps_pps_quality_metrics_t* metrics,
|
||||
bool* profile_changed_out)
|
||||
{
|
||||
if(profile_changed_out != NULL)
|
||||
{
|
||||
*profile_changed_out = false;
|
||||
}
|
||||
if(selector == NULL)
|
||||
{
|
||||
return GPS_PPS_PROFILE_HOLDOVER;
|
||||
}
|
||||
|
||||
gps_pps_profile_t target =
|
||||
gps_pps_classify_quality(metrics);
|
||||
|
||||
if(target == selector->profile)
|
||||
{
|
||||
selector->candidate = target;
|
||||
selector->candidate_updates = 0u;
|
||||
return selector->profile;
|
||||
}
|
||||
if(target != selector->candidate)
|
||||
{
|
||||
selector->candidate = target;
|
||||
selector->candidate_updates = 1u;
|
||||
}
|
||||
else if(selector->candidate_updates != UINT_MAX)
|
||||
{
|
||||
selector->candidate_updates++;
|
||||
}
|
||||
|
||||
unsigned int required_updates =
|
||||
(target > selector->profile) ?
|
||||
GPS_PPS_PROFILE_UPGRADE_UPDATES :
|
||||
GPS_PPS_PROFILE_DEGRADE_UPDATES;
|
||||
if(selector->candidate_updates >= required_updates)
|
||||
{
|
||||
selector->profile = target;
|
||||
selector->candidate_updates = 0u;
|
||||
if(profile_changed_out != NULL)
|
||||
{
|
||||
*profile_changed_out = true;
|
||||
}
|
||||
}
|
||||
return selector->profile;
|
||||
}
|
||||
|
||||
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
|
||||
{
|
||||
switch(profile)
|
||||
{
|
||||
case GPS_PPS_PROFILE_ACQUIRE:
|
||||
return (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.120,
|
||||
.damping = 0.9,
|
||||
.max_slew_ppm = 20.0,
|
||||
.lock_acquire_ticks = 8u,
|
||||
.lock_release_ticks = 128u,
|
||||
.lock_acquire_updates = 5u,
|
||||
.software_bias_gain = 1.0 / 16.0,
|
||||
.software_settle_updates = 12u
|
||||
};
|
||||
case GPS_PPS_PROFILE_PRECISION:
|
||||
return (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.035,
|
||||
.damping = 0.9,
|
||||
.max_slew_ppm = 20.0,
|
||||
.lock_acquire_ticks = 8u,
|
||||
.lock_release_ticks = 128u,
|
||||
.lock_acquire_updates = 5u,
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 12u
|
||||
};
|
||||
case GPS_PPS_PROFILE_ROBUST:
|
||||
return (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.012,
|
||||
.damping = 1.0,
|
||||
.max_slew_ppm = 20.0,
|
||||
.lock_acquire_ticks = 8u,
|
||||
.lock_release_ticks = 128u,
|
||||
.lock_acquire_updates = 8u,
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 16u
|
||||
};
|
||||
case GPS_PPS_PROFILE_HOLDOVER:
|
||||
default:
|
||||
return (gps_pps_loop_profile_t){
|
||||
.natural_frequency_hz = 0.0,
|
||||
.damping = 1.0,
|
||||
.max_slew_ppm = 20.0,
|
||||
.lock_acquire_ticks = 8u,
|
||||
.lock_release_ticks = 128u,
|
||||
.lock_acquire_updates = 8u,
|
||||
.software_bias_gain = 1.0 / 32.0,
|
||||
.software_settle_updates = 16u
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const char* gps_pps_profile_name(gps_pps_profile_t profile)
|
||||
{
|
||||
switch(profile)
|
||||
{
|
||||
case GPS_PPS_PROFILE_ACQUIRE:
|
||||
return "acquire";
|
||||
case GPS_PPS_PROFILE_PRECISION:
|
||||
return "precision";
|
||||
case GPS_PPS_PROFILE_ROBUST:
|
||||
return "robust";
|
||||
case GPS_PPS_PROFILE_HOLDOVER:
|
||||
default:
|
||||
return "holdover";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
#include "gps/gps_pps_time_filter.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* Board data showed hundreds of nanoseconds of epoch-to-epoch pseudorange
|
||||
* clock-bias noise even with seven satellites, while Doppler clock drift was
|
||||
* stable. A 1/32 residual gain gives roughly a 6.4 s time constant at the
|
||||
* nominal 5 Hz solve rate. The innovation clamp limits one accepted WLS epoch
|
||||
* to less than 32 ns of reference movement.
|
||||
*/
|
||||
#define GPS_PPS_FILTER_DRIFT_GAIN (1.0 / 4.0)
|
||||
#define GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN (1.0 / 16.0)
|
||||
#define GPS_PPS_FILTER_MAX_DRIFT_MPS 10000.0
|
||||
#define GPS_PPS_FILTER_MAX_TDOP 8.0
|
||||
#define GPS_PPS_FILTER_MIN_INTERVAL_SEC 0.040
|
||||
#define GPS_PPS_FILTER_MAX_PREDICTION_GAP_SEC 30.000
|
||||
#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
|
||||
|
||||
static double clamp_double(double value, double limit)
|
||||
{
|
||||
if(value > limit)
|
||||
{
|
||||
return limit;
|
||||
}
|
||||
if(value < -limit)
|
||||
{
|
||||
return -limit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter)
|
||||
{
|
||||
if(filter != NULL)
|
||||
{
|
||||
memset(filter, 0, sizeof(*filter));
|
||||
}
|
||||
}
|
||||
|
||||
bool gps_pps_select_reference(
|
||||
const unsigned int satellite_ids[],
|
||||
const double capture_times[],
|
||||
unsigned int count,
|
||||
bool preferred_valid,
|
||||
unsigned int preferred_id,
|
||||
unsigned int* reference_index_out)
|
||||
{
|
||||
if((satellite_ids == NULL) || (capture_times == NULL) ||
|
||||
(reference_index_out == NULL) || (count == 0u))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int earliest_index = 0u;
|
||||
if(!isfinite(capture_times[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for(unsigned int index = 0u; index < count; index++)
|
||||
{
|
||||
if(!isfinite(capture_times[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(preferred_valid && (satellite_ids[index] == preferred_id))
|
||||
{
|
||||
*reference_index_out = index;
|
||||
return true;
|
||||
}
|
||||
if(capture_times[index] < capture_times[earliest_index])
|
||||
{
|
||||
earliest_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
*reference_index_out = earliest_index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gps_pps_reference_bias_drift(
|
||||
const double receiver_ecef_m[3],
|
||||
const double receiver_velocity_ecef_mps[3],
|
||||
const double reference_satellite_ecef_m[3],
|
||||
const double reference_satellite_velocity_ecef_mps[3],
|
||||
double reference_satellite_clock_drift_sps,
|
||||
double* clock_bias_drift_mps_out)
|
||||
{
|
||||
if((receiver_ecef_m == NULL) ||
|
||||
(receiver_velocity_ecef_mps == NULL) ||
|
||||
(reference_satellite_ecef_m == NULL) ||
|
||||
(reference_satellite_velocity_ecef_mps == NULL) ||
|
||||
(clock_bias_drift_mps_out == NULL) ||
|
||||
!isfinite(reference_satellite_clock_drift_sps))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double delta_m[3];
|
||||
double range_squared_m2 = 0.0;
|
||||
for(unsigned int component = 0u; component < 3u; component++)
|
||||
{
|
||||
if(!isfinite(receiver_ecef_m[component]) ||
|
||||
!isfinite(receiver_velocity_ecef_mps[component]) ||
|
||||
!isfinite(reference_satellite_ecef_m[component]) ||
|
||||
!isfinite(reference_satellite_velocity_ecef_mps[component]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
delta_m[component] =
|
||||
reference_satellite_ecef_m[component] -
|
||||
receiver_ecef_m[component];
|
||||
range_squared_m2 += delta_m[component] * delta_m[component];
|
||||
}
|
||||
double range_m = sqrt(range_squared_m2);
|
||||
if(!isfinite(range_m) || (range_m < 1.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double geometric_range_rate_mps = 0.0;
|
||||
for(unsigned int component = 0u; component < 3u; component++)
|
||||
{
|
||||
double relative_velocity_mps =
|
||||
reference_satellite_velocity_ecef_mps[component] -
|
||||
receiver_velocity_ecef_mps[component];
|
||||
geometric_range_rate_mps +=
|
||||
(delta_m[component] / range_m) *
|
||||
relative_velocity_mps;
|
||||
}
|
||||
/*
|
||||
* capture_i - min(capture) removes the receiver clock before position
|
||||
* WLS. The remaining bias datum is -range(reference), with the broadcast
|
||||
* satellite-clock correction added to the observation.
|
||||
*/
|
||||
double bias_drift_mps =
|
||||
-geometric_range_rate_mps +
|
||||
GPS_PPS_SPEED_OF_LIGHT_MPS *
|
||||
reference_satellite_clock_drift_sps;
|
||||
if(!isfinite(bias_drift_mps))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*clock_bias_drift_mps_out = bias_drift_mps;
|
||||
return true;
|
||||
}
|
||||
|
||||
static gps_pps_time_filter_result_t gps_pps_time_filter_reanchor(
|
||||
gps_pps_time_filter_t* filter,
|
||||
uint64_t epoch_counter,
|
||||
double measured_clock_bias_m,
|
||||
bool clock_drift_valid,
|
||||
double measured_clock_drift_mps,
|
||||
double* filtered_clock_bias_m_out,
|
||||
double* innovation_m_out)
|
||||
{
|
||||
filter->epoch_counter = epoch_counter;
|
||||
filter->drift_anchor_counter = epoch_counter;
|
||||
filter->clock_bias_m = measured_clock_bias_m;
|
||||
filter->clock_drift_mps =
|
||||
clock_drift_valid ? measured_clock_drift_mps : 0.0;
|
||||
filter->previous_measurement_m = measured_clock_bias_m;
|
||||
filter->drift_anchor_measurement_m = measured_clock_bias_m;
|
||||
filter->accepted_updates = 1u;
|
||||
filter->initialized = true;
|
||||
filter->drift_initialized = clock_drift_valid;
|
||||
*filtered_clock_bias_m_out = measured_clock_bias_m;
|
||||
*innovation_m_out = 0.0;
|
||||
return GPS_PPS_TIME_FILTER_COARSE;
|
||||
}
|
||||
|
||||
gps_pps_time_filter_result_t gps_pps_time_filter_update(
|
||||
gps_pps_time_filter_t* filter,
|
||||
uint64_t epoch_counter,
|
||||
double counter_frequency_hz,
|
||||
double measured_clock_bias_m,
|
||||
bool clock_drift_valid,
|
||||
double measured_clock_drift_mps,
|
||||
double tdop,
|
||||
double bias_gain,
|
||||
unsigned int settle_updates,
|
||||
double* filtered_clock_bias_m_out,
|
||||
double* innovation_m_out)
|
||||
{
|
||||
if((filter == NULL) || (filtered_clock_bias_m_out == NULL) ||
|
||||
(innovation_m_out == NULL) ||
|
||||
!isfinite(counter_frequency_hz) || (counter_frequency_hz <= 0.0) ||
|
||||
!isfinite(measured_clock_bias_m) ||
|
||||
!isfinite(tdop) || (tdop < 0.0) || (tdop > GPS_PPS_FILTER_MAX_TDOP) ||
|
||||
!isfinite(bias_gain) || (bias_gain <= 0.0) || (bias_gain > 1.0) ||
|
||||
(settle_updates == 0u) ||
|
||||
(clock_drift_valid && !isfinite(measured_clock_drift_mps)))
|
||||
{
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
|
||||
if(!filter->initialized)
|
||||
{
|
||||
return gps_pps_time_filter_reanchor(
|
||||
filter, epoch_counter, measured_clock_bias_m,
|
||||
clock_drift_valid, measured_clock_drift_mps,
|
||||
filtered_clock_bias_m_out, innovation_m_out);
|
||||
}
|
||||
|
||||
if(epoch_counter <= filter->epoch_counter)
|
||||
{
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
uint64_t elapsed_ticks = epoch_counter - filter->epoch_counter;
|
||||
double elapsed_sec = (double)elapsed_ticks / counter_frequency_hz;
|
||||
if(!isfinite(elapsed_sec) ||
|
||||
(elapsed_sec < GPS_PPS_FILTER_MIN_INTERVAL_SEC))
|
||||
{
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
/*
|
||||
* The independent two-second reference watchdog suppresses PPS validity
|
||||
* during a missing-WLS interval. Do not discard the Doppler prediction
|
||||
* or force a phase-stepping coarse commit for the shorter scheduling gaps
|
||||
* observed on the board. Reference-satellite changes are reset by the
|
||||
* caller; only a genuinely long same-datum gap is re-anchored here.
|
||||
*/
|
||||
if(elapsed_sec > GPS_PPS_FILTER_MAX_PREDICTION_GAP_SEC)
|
||||
{
|
||||
return gps_pps_time_filter_reanchor(
|
||||
filter, epoch_counter, measured_clock_bias_m,
|
||||
clock_drift_valid, measured_clock_drift_mps,
|
||||
filtered_clock_bias_m_out, innovation_m_out);
|
||||
}
|
||||
|
||||
double measurement_delta_m =
|
||||
measured_clock_bias_m - filter->previous_measurement_m;
|
||||
if(!clock_drift_valid &&
|
||||
(fabs(measurement_delta_m) >
|
||||
GPS_PPS_FILTER_MAX_DRIFT_MPS * elapsed_sec +
|
||||
GPS_PPS_FILTER_HARD_OUTLIER_M))
|
||||
{
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
double anchor_elapsed_sec =
|
||||
(double)(epoch_counter - filter->drift_anchor_counter) /
|
||||
counter_frequency_hz;
|
||||
double fallback_drift_mps = clamp_double(
|
||||
(anchor_elapsed_sec > 0.0) ?
|
||||
((measured_clock_bias_m -
|
||||
filter->drift_anchor_measurement_m) /
|
||||
anchor_elapsed_sec) :
|
||||
(measurement_delta_m / elapsed_sec),
|
||||
GPS_PPS_FILTER_MAX_DRIFT_MPS);
|
||||
if(!filter->drift_initialized)
|
||||
{
|
||||
/* This happens only while PPS is still coarse. It prevents the
|
||||
* hundreds-of-metres/second receiver-clock ramp from being mistaken
|
||||
* for timing phase when Doppler velocity is temporarily absent. */
|
||||
filter->clock_drift_mps = fallback_drift_mps;
|
||||
filter->drift_initialized = true;
|
||||
}
|
||||
|
||||
double predicted_clock_bias_m =
|
||||
filter->clock_bias_m + filter->clock_drift_mps * elapsed_sec;
|
||||
double innovation_m = measured_clock_bias_m - predicted_clock_bias_m;
|
||||
if(!isfinite(predicted_clock_bias_m) || !isfinite(innovation_m) ||
|
||||
(fabs(innovation_m) > GPS_PPS_FILTER_HARD_OUTLIER_M))
|
||||
{
|
||||
return GPS_PPS_TIME_FILTER_REJECTED;
|
||||
}
|
||||
|
||||
filter->clock_bias_m =
|
||||
predicted_clock_bias_m +
|
||||
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 *
|
||||
(measured_clock_drift_mps - filter->clock_drift_mps);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(filter->accepted_updates < settle_updates)
|
||||
{
|
||||
/* While PPS output is deliberately invalid/coarse, use the slope
|
||||
* from the re-anchor point so a single noisy first difference
|
||||
* cannot leave a long clock-ramp transient. */
|
||||
filter->clock_drift_mps = fallback_drift_mps;
|
||||
}
|
||||
else
|
||||
{
|
||||
filter->clock_drift_mps +=
|
||||
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN *
|
||||
(fallback_drift_mps - filter->clock_drift_mps);
|
||||
}
|
||||
}
|
||||
filter->previous_measurement_m = measured_clock_bias_m;
|
||||
filter->epoch_counter = epoch_counter;
|
||||
if(filter->accepted_updates != UINT32_MAX)
|
||||
{
|
||||
filter->accepted_updates++;
|
||||
}
|
||||
|
||||
*filtered_clock_bias_m_out = filter->clock_bias_m;
|
||||
*innovation_m_out = innovation_m;
|
||||
return (filter->accepted_updates < settle_updates) ?
|
||||
GPS_PPS_TIME_FILTER_COARSE : GPS_PPS_TIME_FILTER_TRACKING;
|
||||
}
|
||||
@@ -109,6 +109,109 @@ bool gps_time_from_sample_anchor(int anchor_week,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gps_time_from_periodic_sample_anchor(
|
||||
int anchor_week,
|
||||
double anchor_tow_sec,
|
||||
uint32_t anchor_raw_sample,
|
||||
double anchor_corrected_sample,
|
||||
uint32_t event_raw_sample,
|
||||
double event_corrected_sample,
|
||||
double samples_per_second,
|
||||
double max_age_sec,
|
||||
double event_period_sec,
|
||||
double max_snap_error_sec,
|
||||
int* event_week_out,
|
||||
double* event_tow_sec_out,
|
||||
double* elapsed_sec_out)
|
||||
{
|
||||
if((event_week_out == NULL) || (event_tow_sec_out == NULL) ||
|
||||
!(event_period_sec > 0.0) || !(max_snap_error_sec >= 0.0) ||
|
||||
!isfinite(event_period_sec) || !isfinite(max_snap_error_sec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int sample_week = 0;
|
||||
double sample_tow_sec = 0.0;
|
||||
double sample_elapsed_sec = 0.0;
|
||||
if(!gps_time_from_sample_anchor(
|
||||
anchor_week, anchor_tow_sec,
|
||||
anchor_raw_sample, anchor_corrected_sample,
|
||||
event_raw_sample, event_corrected_sample,
|
||||
samples_per_second, max_age_sec,
|
||||
&sample_week, &sample_tow_sec, &sample_elapsed_sec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
(void)sample_week;
|
||||
(void)sample_tow_sec;
|
||||
|
||||
double event_index = round(sample_elapsed_sec / event_period_sec);
|
||||
double periodic_elapsed_sec = event_index * event_period_sec;
|
||||
if(!isfinite(event_index) || !isfinite(periodic_elapsed_sec) ||
|
||||
(event_index < 0.0) ||
|
||||
(fabs(periodic_elapsed_sec - sample_elapsed_sec) >
|
||||
max_snap_error_sec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int event_week = anchor_week;
|
||||
double event_tow_sec = gps_time_normalize_week(
|
||||
anchor_tow_sec + periodic_elapsed_sec, &event_week);
|
||||
*event_week_out = event_week;
|
||||
*event_tow_sec_out = event_tow_sec;
|
||||
if(elapsed_sec_out != NULL)
|
||||
{
|
||||
*elapsed_sec_out = periodic_elapsed_sec;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gps_time_correct_capture_counter(
|
||||
uint64_t capture_counter,
|
||||
double fractional_sample_delay,
|
||||
double samples_per_second,
|
||||
double counter_frequency_hz,
|
||||
uint64_t* corrected_counter_out,
|
||||
double* applied_correction_sec_out)
|
||||
{
|
||||
if((corrected_counter_out == NULL) ||
|
||||
!isfinite(fractional_sample_delay) ||
|
||||
!isfinite(samples_per_second) ||
|
||||
!isfinite(counter_frequency_hz) ||
|
||||
(fractional_sample_delay < 0.0) ||
|
||||
(fractional_sample_delay > 1.0 + 1.0e-6) ||
|
||||
!(samples_per_second > 0.0) ||
|
||||
!(counter_frequency_hz > 0.0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double correction_ticks_exact =
|
||||
fractional_sample_delay *
|
||||
(counter_frequency_hz / samples_per_second);
|
||||
if(!isfinite(correction_ticks_exact) ||
|
||||
(correction_ticks_exact < 0.0) ||
|
||||
(correction_ticks_exact > (double)INT64_MAX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t correction_ticks = (uint64_t)llround(correction_ticks_exact);
|
||||
if(correction_ticks > capture_counter)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*corrected_counter_out = capture_counter - correction_ticks;
|
||||
if(applied_correction_sec_out != NULL)
|
||||
{
|
||||
*applied_correction_sec_out =
|
||||
(double)correction_ticks / counter_frequency_hz;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gps_time_anchors_consistent(int first_week,
|
||||
double first_tow_sec,
|
||||
uint32_t first_raw_sample,
|
||||
|
||||
@@ -8,6 +8,9 @@ TARGETS := test_imu_calibration test_gps_nav_epoch \
|
||||
test_gps_ephemeris_cache test_gps_receiver_hardware_sim
|
||||
TARGETS += test_gps_tracking_measurement
|
||||
TARGETS += test_gps_satellite_velocity
|
||||
TARGETS += test_gps_pps_time_filter
|
||||
TARGETS += test_gps_pps_quality
|
||||
TARGETS += test_gps_pps_adaptive_loop
|
||||
|
||||
.PHONY: all run sanitize clean
|
||||
|
||||
@@ -50,6 +53,19 @@ test_gps_satellite_velocity: test_gps_satellite_velocity.c \
|
||||
-Wno-unused-variable -Wno-sign-compare \
|
||||
-Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
|
||||
|
||||
test_gps_pps_time_filter: test_gps_pps_time_filter.c \
|
||||
$(ROOT)/src/gps/gps_pps_time_filter.c
|
||||
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
|
||||
|
||||
test_gps_pps_quality: test_gps_pps_quality.c \
|
||||
$(ROOT)/src/gps/gps_pps_quality.c
|
||||
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
|
||||
|
||||
test_gps_pps_adaptive_loop: test_gps_pps_adaptive_loop.c \
|
||||
$(ROOT)/src/gps/gps_pps_time_filter.c \
|
||||
$(ROOT)/src/gps/gps_pps_quality.c
|
||||
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
|
||||
|
||||
run: $(TARGETS)
|
||||
./test_imu_calibration
|
||||
./test_gps_nav_epoch
|
||||
@@ -61,6 +77,9 @@ run: $(TARGETS)
|
||||
./test_gps_receiver_hardware_sim
|
||||
./test_gps_tracking_measurement
|
||||
./test_gps_satellite_velocity
|
||||
./test_gps_pps_time_filter
|
||||
./test_gps_pps_quality
|
||||
./test_gps_pps_adaptive_loop
|
||||
|
||||
sanitize:
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
@@ -77,6 +96,13 @@ sanitize:
|
||||
-o test_gps_nav_epoch_coalescing_san
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_nav_epoch_coalescing_san
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I$(ROOT)/header test_gps_time_anchor.c \
|
||||
$(ROOT)/src/gps/gps_time_anchor.c -lm \
|
||||
-o test_gps_time_anchor_san
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_time_anchor_san
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I$(ROOT)/header test_gps_acq_manager.c \
|
||||
@@ -122,11 +148,36 @@ sanitize:
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 \
|
||||
./test_gps_satellite_velocity_san
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I$(ROOT)/header test_gps_pps_time_filter.c \
|
||||
$(ROOT)/src/gps/gps_pps_time_filter.c -lm \
|
||||
-o test_gps_pps_time_filter_san
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_time_filter_san
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I$(ROOT)/header test_gps_pps_quality.c \
|
||||
$(ROOT)/src/gps/gps_pps_quality.c -lm \
|
||||
-o test_gps_pps_quality_san
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_quality_san
|
||||
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I$(ROOT)/header test_gps_pps_adaptive_loop.c \
|
||||
$(ROOT)/src/gps/gps_pps_time_filter.c \
|
||||
$(ROOT)/src/gps/gps_pps_quality.c -lm \
|
||||
-o test_gps_pps_adaptive_loop_san
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_adaptive_loop_san
|
||||
|
||||
clean:
|
||||
rm -f $(TARGETS) test_gps_nav_epoch_coalescing_repro \
|
||||
test_gps_nav_epoch_san test_gps_nav_epoch_coalescing_san \
|
||||
test_gps_time_anchor_san \
|
||||
test_gps_acq_manager_san test_gps_ephemeris_cache_san \
|
||||
test_gps_receiver_hardware_sim_san \
|
||||
test_gps_tracking_measurement_san \
|
||||
test_gps_satellite_velocity_san
|
||||
test_gps_satellite_velocity_san \
|
||||
test_gps_pps_time_filter_san test_gps_pps_quality_san \
|
||||
test_gps_pps_adaptive_loop_san
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gps/gps_pps_quality.h"
|
||||
#include "gps/gps_pps_time_filter.h"
|
||||
|
||||
#define TRACKING_HZ 125000000.0
|
||||
#define UPDATE_PERIOD_SEC 0.2
|
||||
#define SPEED_OF_LIGHT_MPS 299792458.0
|
||||
#define Q32_SCALE 4294967296.0
|
||||
#define Q46_SCALE 70368744177664.0
|
||||
|
||||
typedef struct {
|
||||
int64_t phase_q32;
|
||||
int64_t integrator_q32;
|
||||
int64_t rate_q32;
|
||||
bool aligned;
|
||||
bool locked;
|
||||
unsigned int good_updates;
|
||||
} synthetic_rtl_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t state;
|
||||
} synthetic_rng_t;
|
||||
|
||||
typedef enum {
|
||||
SYNTHETIC_STEADY = 0,
|
||||
SYNTHETIC_STARTUP_NOISY_THEN_CLEAN,
|
||||
SYNTHETIC_NOISE_BURSTS,
|
||||
SYNTHETIC_SATELLITE_LOSS,
|
||||
SYNTHETIC_DOPPLER_LOSS,
|
||||
SYNTHETIC_GEOMETRY_DEGRADE,
|
||||
SYNTHETIC_REFERENCE_RANGE_RATE
|
||||
} synthetic_pattern_t;
|
||||
|
||||
static double synthetic_uniform(synthetic_rng_t* rng)
|
||||
{
|
||||
rng->state = rng->state * 1664525u + 1013904223u;
|
||||
return ((double)(rng->state >> 8) + 0.5) / 16777216.0;
|
||||
}
|
||||
|
||||
static double synthetic_normal(synthetic_rng_t* rng)
|
||||
{
|
||||
double sum = 0.0;
|
||||
for(unsigned int sample = 0u; sample < 12u; sample++)
|
||||
{
|
||||
sum += synthetic_uniform(rng);
|
||||
}
|
||||
return sum - 6.0;
|
||||
}
|
||||
|
||||
static int64_t clamp_i128(__int128 value, int64_t limit)
|
||||
{
|
||||
if(value > limit)
|
||||
{
|
||||
return limit;
|
||||
}
|
||||
if(value < -(__int128)limit)
|
||||
{
|
||||
return -limit;
|
||||
}
|
||||
return (int64_t)value;
|
||||
}
|
||||
|
||||
static int64_t seconds_to_phase_q32(double seconds)
|
||||
{
|
||||
return (int64_t)llround(seconds * TRACKING_HZ * Q32_SCALE);
|
||||
}
|
||||
|
||||
static double phase_q32_to_seconds(int64_t phase_q32)
|
||||
{
|
||||
return (double)phase_q32 / (TRACKING_HZ * Q32_SCALE);
|
||||
}
|
||||
|
||||
static void synthetic_rtl_advance(synthetic_rtl_t* rtl, double oscillator_ppm)
|
||||
{
|
||||
int64_t raw_ticks = (int64_t)llround(UPDATE_PERIOD_SEC * TRACKING_HZ);
|
||||
int64_t oscillator_q32 = (int64_t)llround(
|
||||
oscillator_ppm * 1.0e-6 * Q32_SCALE);
|
||||
rtl->phase_q32 += raw_ticks * (oscillator_q32 + rtl->rate_q32);
|
||||
}
|
||||
|
||||
static void profile_coefficients(
|
||||
gps_pps_loop_profile_t profile,
|
||||
int64_t* kp_q46,
|
||||
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);
|
||||
}
|
||||
|
||||
static void synthetic_rtl_coarse(synthetic_rtl_t* rtl, int64_t reference_q32)
|
||||
{
|
||||
bool was_locked = rtl->locked;
|
||||
memset(rtl, 0, sizeof(*rtl));
|
||||
rtl->phase_q32 = reference_q32;
|
||||
rtl->aligned = true;
|
||||
assert(!was_locked || !rtl->locked);
|
||||
}
|
||||
|
||||
static void synthetic_rtl_commit(
|
||||
synthetic_rtl_t* rtl,
|
||||
int64_t reference_q32,
|
||||
gps_pps_loop_profile_t profile)
|
||||
{
|
||||
int64_t raw_phase_q32 = reference_q32 - rtl->phase_q32;
|
||||
|
||||
int64_t kp_q46;
|
||||
int64_t ki_q46;
|
||||
int64_t max_slew_q32;
|
||||
profile_coefficients(profile, &kp_q46, &ki_q46, &max_slew_q32);
|
||||
int64_t proportional_q32 =
|
||||
(int64_t)(((__int128)raw_phase_q32 * kp_q46) >> 46);
|
||||
__int128 integrator_candidate =
|
||||
(__int128)rtl->integrator_q32 +
|
||||
(((__int128)raw_phase_q32 * ki_q46) >> 46);
|
||||
rtl->integrator_q32 =
|
||||
clamp_i128(integrator_candidate, max_slew_q32);
|
||||
rtl->rate_q32 = clamp_i128(
|
||||
(__int128)rtl->integrator_q32 + proportional_q32,
|
||||
max_slew_q32);
|
||||
|
||||
int64_t acquire_q32 =
|
||||
(int64_t)profile.lock_acquire_ticks << 32;
|
||||
int64_t release_q32 =
|
||||
(int64_t)profile.lock_release_ticks << 32;
|
||||
if(llabs(raw_phase_q32) > release_q32)
|
||||
{
|
||||
rtl->locked = false;
|
||||
rtl->good_updates = 0u;
|
||||
}
|
||||
else if(!rtl->locked)
|
||||
{
|
||||
if(llabs(raw_phase_q32) <= acquire_q32)
|
||||
{
|
||||
rtl->good_updates++;
|
||||
if(rtl->good_updates >= profile.lock_acquire_updates)
|
||||
{
|
||||
rtl->locked = true;
|
||||
rtl->good_updates = 0u;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rtl->good_updates = 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gps_pps_quality_metrics_t precision_quality(void)
|
||||
{
|
||||
return (gps_pps_quality_metrics_t){
|
||||
.tdop = 1.7,
|
||||
.gdop = 3.2,
|
||||
.clock_bias_sigma_m = 18.0,
|
||||
.mean_cn0_dbhz = 38.0,
|
||||
.minimum_cn0_dbhz = 27.0,
|
||||
.chi_square_ratio = 0.35,
|
||||
.velocity_residual_rms_mps = 0.25,
|
||||
.used_satellites = 7u,
|
||||
.solution_valid = true,
|
||||
.residual_test_valid = true,
|
||||
.velocity_valid = true,
|
||||
.fde_applied = false
|
||||
};
|
||||
}
|
||||
|
||||
static gps_pps_quality_metrics_t robust_quality(bool velocity_valid)
|
||||
{
|
||||
gps_pps_quality_metrics_t quality = {
|
||||
.tdop = 5.0,
|
||||
.gdop = 8.0,
|
||||
.clock_bias_sigma_m = 65.0,
|
||||
.mean_cn0_dbhz = 27.0,
|
||||
.minimum_cn0_dbhz = 18.0,
|
||||
.chi_square_ratio = 0.85,
|
||||
.velocity_residual_rms_mps = velocity_valid ? 1.8 : 0.0,
|
||||
.used_satellites = 5u,
|
||||
.solution_valid = true,
|
||||
.residual_test_valid = true,
|
||||
.velocity_valid = velocity_valid,
|
||||
.fde_applied = false
|
||||
};
|
||||
return quality;
|
||||
}
|
||||
|
||||
static void run_noisy_scenario(
|
||||
unsigned int seed,
|
||||
bool with_gap,
|
||||
double oscillator_ppm,
|
||||
double noise_sigma_ns,
|
||||
bool clock_drift_valid,
|
||||
bool use_robust_quality,
|
||||
bool hard_outliers,
|
||||
unsigned int update_count,
|
||||
synthetic_pattern_t pattern,
|
||||
double* first_lock_sec_out,
|
||||
double* one_second_jitter_rms_sec_out,
|
||||
unsigned int* coarse_count_out,
|
||||
unsigned int* rejected_count_out,
|
||||
bool* locked_after_recovery_out)
|
||||
{
|
||||
assert(update_count <= 1500u);
|
||||
synthetic_rng_t rng = {.state = seed};
|
||||
gps_pps_quality_selector_t selector;
|
||||
gps_pps_quality_selector_init(&selector);
|
||||
gps_pps_time_filter_t filter;
|
||||
gps_pps_time_filter_init(&filter);
|
||||
synthetic_rtl_t rtl = {0};
|
||||
gps_pps_profile_t quality_profile = GPS_PPS_PROFILE_HOLDOVER;
|
||||
double first_lock_sec = NAN;
|
||||
unsigned int coarse_count = 0u;
|
||||
unsigned int rejected_count = 0u;
|
||||
double phase_history[1500] = {0.0};
|
||||
bool lock_history[1500] = {false};
|
||||
|
||||
for(unsigned int update = 0u; update < update_count; update++)
|
||||
{
|
||||
double time_sec = UPDATE_PERIOD_SEC * (double)update;
|
||||
synthetic_rtl_advance(&rtl, oscillator_ppm);
|
||||
phase_history[update] = phase_q32_to_seconds(rtl.phase_q32);
|
||||
lock_history[update] = rtl.locked;
|
||||
if(with_gap && (time_sec >= 35.0) && (time_sec < 50.0))
|
||||
{
|
||||
/* The firmware reference watchdog deasserts PPS validity after
|
||||
* two seconds without a successful commit. */
|
||||
if(time_sec >= 37.0)
|
||||
{
|
||||
lock_history[update] = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
bool sample_drift_valid = clock_drift_valid;
|
||||
bool sample_robust_quality = use_robust_quality;
|
||||
bool sample_solution_valid = true;
|
||||
double sample_noise_sigma_ns = noise_sigma_ns;
|
||||
double reference_range_rate_mps = 0.0;
|
||||
switch(pattern)
|
||||
{
|
||||
case SYNTHETIC_STARTUP_NOISY_THEN_CLEAN:
|
||||
sample_robust_quality = time_sec < 55.0;
|
||||
sample_noise_sigma_ns =
|
||||
(time_sec < 55.0) ? 1100.0 : 120.0;
|
||||
break;
|
||||
case SYNTHETIC_NOISE_BURSTS:
|
||||
{
|
||||
bool burst =
|
||||
((time_sec >= 55.0) && (time_sec < 78.0)) ||
|
||||
((time_sec >= 135.0) && (time_sec < 151.0)) ||
|
||||
((time_sec >= 205.0) && (time_sec < 214.0));
|
||||
sample_robust_quality = burst;
|
||||
sample_noise_sigma_ns = burst ? 900.0 : 150.0;
|
||||
break;
|
||||
}
|
||||
case SYNTHETIC_SATELLITE_LOSS:
|
||||
sample_solution_valid =
|
||||
!(((time_sec >= 55.0) && (time_sec < 78.0)) ||
|
||||
((time_sec >= 145.0) && (time_sec < 151.0)));
|
||||
break;
|
||||
case SYNTHETIC_DOPPLER_LOSS:
|
||||
sample_drift_valid =
|
||||
!((time_sec >= 65.0) && (time_sec < 105.0));
|
||||
break;
|
||||
case SYNTHETIC_GEOMETRY_DEGRADE:
|
||||
sample_robust_quality =
|
||||
(time_sec >= 60.0) && (time_sec < 145.0);
|
||||
sample_noise_sigma_ns =
|
||||
sample_robust_quality ? 650.0 : 200.0;
|
||||
break;
|
||||
case SYNTHETIC_REFERENCE_RANGE_RATE:
|
||||
reference_range_rate_mps = 130.0;
|
||||
break;
|
||||
case SYNTHETIC_STEADY:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
gps_pps_quality_metrics_t quality = sample_robust_quality ?
|
||||
robust_quality(sample_drift_valid) : precision_quality();
|
||||
quality.velocity_valid = sample_drift_valid;
|
||||
if(!sample_solution_valid)
|
||||
{
|
||||
quality.solution_valid = false;
|
||||
quality.used_satellites = 2u;
|
||||
quality.velocity_valid = false;
|
||||
}
|
||||
quality_profile = gps_pps_quality_selector_update(
|
||||
&selector, &quality, &changed);
|
||||
if(quality_profile == GPS_PPS_PROFILE_HOLDOVER)
|
||||
{
|
||||
lock_history[update] = false;
|
||||
continue;
|
||||
}
|
||||
gps_pps_profile_t loop_profile =
|
||||
rtl.locked ? quality_profile : GPS_PPS_PROFILE_ACQUIRE;
|
||||
gps_pps_loop_profile_t parameters =
|
||||
gps_pps_loop_profile(loop_profile);
|
||||
|
||||
double true_bias_m =
|
||||
1200.0 +
|
||||
(-reference_range_rate_mps) * time_sec;
|
||||
double noise_sec =
|
||||
synthetic_normal(&rng) * sample_noise_sigma_ns * 1.0e-9;
|
||||
if((update % 37u) == 0u)
|
||||
{
|
||||
noise_sec +=
|
||||
(synthetic_uniform(&rng) < 0.5 ? -1.0 : 1.0) *
|
||||
(hard_outliers ? 6.0e-6 : 900.0e-9);
|
||||
}
|
||||
double measured_bias_m =
|
||||
true_bias_m - noise_sec * SPEED_OF_LIGHT_MPS;
|
||||
double filtered_bias_m = 0.0;
|
||||
double innovation_m = 0.0;
|
||||
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
|
||||
&filter,
|
||||
(uint64_t)llround(time_sec * TRACKING_HZ),
|
||||
TRACKING_HZ,
|
||||
measured_bias_m,
|
||||
sample_drift_valid,
|
||||
-reference_range_rate_mps +
|
||||
synthetic_normal(&rng) * 0.25,
|
||||
quality.tdop,
|
||||
parameters.software_bias_gain,
|
||||
parameters.software_settle_updates,
|
||||
&filtered_bias_m,
|
||||
&innovation_m);
|
||||
if(result == GPS_PPS_TIME_FILTER_REJECTED)
|
||||
{
|
||||
rejected_count++;
|
||||
continue;
|
||||
}
|
||||
assert(isfinite(innovation_m));
|
||||
int64_t reference_q32 = seconds_to_phase_q32(
|
||||
-(filtered_bias_m - true_bias_m) / SPEED_OF_LIGHT_MPS);
|
||||
if((result == GPS_PPS_TIME_FILTER_COARSE) || !rtl.aligned)
|
||||
{
|
||||
synthetic_rtl_coarse(&rtl, reference_q32);
|
||||
coarse_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
synthetic_rtl_commit(&rtl, reference_q32, parameters);
|
||||
}
|
||||
if(rtl.locked && !isfinite(first_lock_sec))
|
||||
{
|
||||
first_lock_sec = time_sec;
|
||||
}
|
||||
phase_history[update] = phase_q32_to_seconds(rtl.phase_q32);
|
||||
lock_history[update] = rtl.locked;
|
||||
}
|
||||
|
||||
double jitter_square_sum = 0.0;
|
||||
unsigned int jitter_count = 0u;
|
||||
for(unsigned int update = (update_count / 2u) + 5u;
|
||||
update < update_count;
|
||||
update++)
|
||||
{
|
||||
if(lock_history[update] && lock_history[update - 5u])
|
||||
{
|
||||
double difference =
|
||||
phase_history[update] - phase_history[update - 5u];
|
||||
jitter_square_sum += difference * difference;
|
||||
jitter_count++;
|
||||
}
|
||||
}
|
||||
*first_lock_sec_out = first_lock_sec;
|
||||
*one_second_jitter_rms_sec_out =
|
||||
(jitter_count != 0u) ?
|
||||
sqrt(jitter_square_sum / (double)jitter_count) : NAN;
|
||||
*coarse_count_out = coarse_count;
|
||||
*rejected_count_out = rejected_count;
|
||||
*locked_after_recovery_out = lock_history[update_count - 1u];
|
||||
}
|
||||
|
||||
static void test_noisy_fixed_point_loop(void)
|
||||
{
|
||||
for(unsigned int seed = 1u; seed <= 20u; seed++)
|
||||
{
|
||||
double first_lock_sec;
|
||||
double jitter_rms_sec;
|
||||
unsigned int coarse_count;
|
||||
unsigned int rejected_count;
|
||||
bool locked_after_recovery;
|
||||
run_noisy_scenario(
|
||||
seed, false, 1.65, 320.0, true, false, false, 600u,
|
||||
SYNTHETIC_STEADY,
|
||||
&first_lock_sec, &jitter_rms_sec, &coarse_count,
|
||||
&rejected_count, &locked_after_recovery);
|
||||
assert(isfinite(first_lock_sec));
|
||||
assert(first_lock_sec < 15.0);
|
||||
assert(jitter_rms_sec < 20.0e-9);
|
||||
assert(coarse_count >= 10u);
|
||||
assert(rejected_count == 0u);
|
||||
assert(locked_after_recovery);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_missing_wls_holds_and_resumes_without_reanchor(void)
|
||||
{
|
||||
double first_lock_sec;
|
||||
double jitter_rms_sec;
|
||||
unsigned int coarse_count;
|
||||
unsigned int rejected_count;
|
||||
bool locked_after_recovery;
|
||||
run_noisy_scenario(
|
||||
31u, true, 1.65, 320.0, true, false, false, 600u,
|
||||
SYNTHETIC_STEADY,
|
||||
&first_lock_sec, &jitter_rms_sec, &coarse_count,
|
||||
&rejected_count, &locked_after_recovery);
|
||||
assert(first_lock_sec < 15.0);
|
||||
/* Initial settling emits repeated coarse commits; the bounded 15-second
|
||||
* gap must not add another reset of the already converged PI state. */
|
||||
assert(coarse_count == 11u);
|
||||
assert(rejected_count == 0u);
|
||||
assert(locked_after_recovery);
|
||||
assert(jitter_rms_sec < 20.0e-9);
|
||||
}
|
||||
|
||||
static void test_embedded_corner_case_matrix(void)
|
||||
{
|
||||
for(unsigned int scenario = 0u; scenario < 4u; scenario++)
|
||||
{
|
||||
double first_lock_sec;
|
||||
double jitter_rms_sec;
|
||||
unsigned int coarse_count;
|
||||
unsigned int rejected_count;
|
||||
bool locked_after_recovery;
|
||||
bool no_doppler = scenario == 1u;
|
||||
bool hard_outliers = scenario == 2u;
|
||||
double oscillator_ppm = (scenario == 3u) ? 18.0 : 1.65;
|
||||
double noise_sigma_ns = (scenario == 0u) ? 650.0 : 320.0;
|
||||
run_noisy_scenario(
|
||||
100u + scenario,
|
||||
false,
|
||||
oscillator_ppm,
|
||||
noise_sigma_ns,
|
||||
!no_doppler,
|
||||
(scenario == 0u) || no_doppler,
|
||||
hard_outliers,
|
||||
1200u,
|
||||
SYNTHETIC_STEADY,
|
||||
&first_lock_sec,
|
||||
&jitter_rms_sec,
|
||||
&coarse_count,
|
||||
&rejected_count,
|
||||
&locked_after_recovery);
|
||||
assert(isfinite(first_lock_sec));
|
||||
assert(first_lock_sec < 35.0);
|
||||
assert(isfinite(jitter_rms_sec));
|
||||
assert(jitter_rms_sec < 20.0e-9);
|
||||
assert(coarse_count >= 10u);
|
||||
assert(hard_outliers ? (rejected_count > 0u) :
|
||||
(rejected_count == 0u));
|
||||
assert(locked_after_recovery);
|
||||
}
|
||||
|
||||
double first_lock_sec;
|
||||
double jitter_rms_sec;
|
||||
unsigned int coarse_count;
|
||||
unsigned int rejected_count;
|
||||
bool locked_after_recovery;
|
||||
run_noisy_scenario(
|
||||
200u, false, 25.0, 50.0, true, false, false, 600u,
|
||||
SYNTHETIC_STEADY,
|
||||
&first_lock_sec, &jitter_rms_sec, &coarse_count,
|
||||
&rejected_count, &locked_after_recovery);
|
||||
assert(!isfinite(first_lock_sec));
|
||||
assert(!isfinite(jitter_rms_sec));
|
||||
assert(!locked_after_recovery);
|
||||
}
|
||||
|
||||
static void test_time_varying_environment_matrix(void)
|
||||
{
|
||||
const synthetic_pattern_t patterns[] = {
|
||||
SYNTHETIC_STARTUP_NOISY_THEN_CLEAN,
|
||||
SYNTHETIC_NOISE_BURSTS,
|
||||
SYNTHETIC_SATELLITE_LOSS,
|
||||
SYNTHETIC_DOPPLER_LOSS,
|
||||
SYNTHETIC_GEOMETRY_DEGRADE,
|
||||
SYNTHETIC_REFERENCE_RANGE_RATE
|
||||
};
|
||||
for(unsigned int pattern_index = 0u;
|
||||
pattern_index < (sizeof(patterns) / sizeof(patterns[0]));
|
||||
pattern_index++)
|
||||
{
|
||||
for(unsigned int seed = 1u; seed <= 12u; seed++)
|
||||
{
|
||||
double first_lock_sec;
|
||||
double jitter_rms_sec;
|
||||
unsigned int coarse_count;
|
||||
unsigned int rejected_count;
|
||||
bool locked_after_recovery;
|
||||
run_noisy_scenario(
|
||||
300u + 31u * pattern_index + seed,
|
||||
false,
|
||||
(patterns[pattern_index] == SYNTHETIC_NOISE_BURSTS) ?
|
||||
-2.5 : 1.65,
|
||||
320.0,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
1500u,
|
||||
patterns[pattern_index],
|
||||
&first_lock_sec,
|
||||
&jitter_rms_sec,
|
||||
&coarse_count,
|
||||
&rejected_count,
|
||||
&locked_after_recovery);
|
||||
assert(isfinite(first_lock_sec));
|
||||
assert(first_lock_sec < 25.0);
|
||||
assert(isfinite(jitter_rms_sec));
|
||||
assert(jitter_rms_sec < 20.0e-9);
|
||||
assert(coarse_count >= 10u);
|
||||
assert(rejected_count == 0u);
|
||||
assert(locked_after_recovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_noisy_fixed_point_loop();
|
||||
test_missing_wls_holds_and_resumes_without_reanchor();
|
||||
test_embedded_corner_case_matrix();
|
||||
test_time_varying_environment_matrix();
|
||||
puts("gps_pps_adaptive_loop tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gps/gps_pps_quality.h"
|
||||
|
||||
static gps_pps_quality_metrics_t precision_metrics(void)
|
||||
{
|
||||
return (gps_pps_quality_metrics_t){
|
||||
.tdop = 1.7,
|
||||
.gdop = 3.2,
|
||||
.clock_bias_sigma_m = 18.0,
|
||||
.mean_cn0_dbhz = 38.0,
|
||||
.minimum_cn0_dbhz = 29.0,
|
||||
.chi_square_ratio = 0.35,
|
||||
.velocity_residual_rms_mps = 0.25,
|
||||
.used_satellites = 7u,
|
||||
.solution_valid = true,
|
||||
.residual_test_valid = true,
|
||||
.velocity_valid = true,
|
||||
.fde_applied = false
|
||||
};
|
||||
}
|
||||
|
||||
static void test_hysteretic_profile_transitions(void)
|
||||
{
|
||||
gps_pps_quality_selector_t selector;
|
||||
gps_pps_quality_selector_init(&selector);
|
||||
gps_pps_quality_metrics_t metrics = precision_metrics();
|
||||
bool changed = false;
|
||||
|
||||
for(unsigned int update = 0u; update < 7u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
assert(!changed);
|
||||
}
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
|
||||
assert(changed);
|
||||
|
||||
/* One DOP/CN0 excursion must not chatter the active loop profile. */
|
||||
metrics.tdop = 4.5;
|
||||
metrics.gdop = 7.0;
|
||||
metrics.mean_cn0_dbhz = 27.0;
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
|
||||
assert(!changed);
|
||||
metrics = precision_metrics();
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
|
||||
|
||||
/* Sustained FDE operation degrades to the robust profile. */
|
||||
metrics.fde_applied = true;
|
||||
for(unsigned int update = 0u; update < 2u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
|
||||
}
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
|
||||
assert(changed);
|
||||
|
||||
/* One invalid WLS epoch is skipped; sustained invalidity holds over. */
|
||||
metrics.solution_valid = false;
|
||||
for(unsigned int update = 0u; update < 2u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
|
||||
assert(!changed);
|
||||
}
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
assert(changed);
|
||||
}
|
||||
|
||||
static void test_alternating_quality_does_not_upgrade(void)
|
||||
{
|
||||
gps_pps_quality_selector_t selector;
|
||||
gps_pps_quality_selector_init(&selector);
|
||||
gps_pps_quality_metrics_t good = precision_metrics();
|
||||
gps_pps_quality_metrics_t poor = good;
|
||||
poor.tdop = 9.0;
|
||||
|
||||
for(unsigned int update = 0u; update < 40u; update++)
|
||||
{
|
||||
const gps_pps_quality_metrics_t* metrics =
|
||||
(update & 1u) ? &poor : &good;
|
||||
bool changed = false;
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
assert(!changed);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_profile_parameters_are_ordered(void)
|
||||
{
|
||||
gps_pps_loop_profile_t robust =
|
||||
gps_pps_loop_profile(GPS_PPS_PROFILE_ROBUST);
|
||||
gps_pps_loop_profile_t precision =
|
||||
gps_pps_loop_profile(GPS_PPS_PROFILE_PRECISION);
|
||||
gps_pps_loop_profile_t acquire =
|
||||
gps_pps_loop_profile(GPS_PPS_PROFILE_ACQUIRE);
|
||||
assert(robust.natural_frequency_hz < precision.natural_frequency_hz);
|
||||
assert(precision.natural_frequency_hz < acquire.natural_frequency_hz);
|
||||
assert(robust.lock_acquire_updates > precision.lock_acquire_updates);
|
||||
assert(robust.lock_release_ticks >= precision.lock_release_ticks);
|
||||
}
|
||||
|
||||
static void test_quality_boundaries_and_recovery(void)
|
||||
{
|
||||
gps_pps_quality_selector_t selector;
|
||||
gps_pps_quality_selector_init(&selector);
|
||||
gps_pps_quality_metrics_t metrics = precision_metrics();
|
||||
bool changed = false;
|
||||
|
||||
/* Exactly-on-threshold values remain usable in robust mode. */
|
||||
metrics.used_satellites = 5u;
|
||||
metrics.tdop = 8.0;
|
||||
metrics.gdop = 12.0;
|
||||
metrics.clock_bias_sigma_m = 100.0;
|
||||
metrics.mean_cn0_dbhz = 22.0;
|
||||
metrics.minimum_cn0_dbhz = 14.0;
|
||||
metrics.chi_square_ratio = 1.0;
|
||||
metrics.velocity_residual_rms_mps = 3.0;
|
||||
for(unsigned int update = 0u; update < 8u; update++)
|
||||
{
|
||||
gps_pps_quality_selector_update(&selector, &metrics, &changed);
|
||||
}
|
||||
assert(selector.profile == GPS_PPS_PROFILE_ROBUST);
|
||||
|
||||
/* Non-finite metrics and loss below four satellites use fast hysteresis. */
|
||||
metrics.tdop = NAN;
|
||||
for(unsigned int update = 0u; update < 2u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
|
||||
}
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
assert(changed);
|
||||
|
||||
/* A single good epoch is not enough to leave holdover. */
|
||||
metrics = precision_metrics();
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
metrics.used_satellites = 3u;
|
||||
for(unsigned int update = 0u; update < 3u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
}
|
||||
|
||||
/* Recovery cannot expose PPS until eight consecutive good epochs. */
|
||||
metrics = precision_metrics();
|
||||
for(unsigned int update = 0u; update < 7u; update++)
|
||||
{
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
|
||||
}
|
||||
assert(gps_pps_quality_selector_update(
|
||||
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_hysteretic_profile_transitions();
|
||||
test_alternating_quality_does_not_upgrade();
|
||||
test_profile_parameters_are_ordered();
|
||||
test_quality_boundaries_and_recovery();
|
||||
puts("gps_pps_quality tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gps/gps_pps_time_filter.h"
|
||||
|
||||
#define COUNTER_HZ 125000000.0
|
||||
|
||||
static void test_drift_prediction_rejects_bias_noise(void)
|
||||
{
|
||||
static const double noise_m[] = {
|
||||
0.0, 90.0, -120.0, 70.0, -45.0, 130.0, -80.0, 55.0,
|
||||
-105.0, 35.0, 85.0, -65.0, 115.0, -95.0, 50.0, -30.0
|
||||
};
|
||||
gps_pps_time_filter_t filter;
|
||||
gps_pps_time_filter_init(&filter);
|
||||
|
||||
double squared_error_sum = 0.0;
|
||||
unsigned int tracked = 0u;
|
||||
for(unsigned int update = 0u; update < 160u; update++)
|
||||
{
|
||||
double time_sec = 0.2 * (double)update;
|
||||
double true_bias_m = 1200.0 + 475.0 * time_sec;
|
||||
double measured_bias_m =
|
||||
true_bias_m + noise_m[update % (sizeof(noise_m) / sizeof(noise_m[0]))];
|
||||
double filtered_bias_m = 0.0;
|
||||
double innovation_m = 0.0;
|
||||
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
|
||||
&filter,
|
||||
(uint64_t)llround(time_sec * COUNTER_HZ),
|
||||
COUNTER_HZ,
|
||||
measured_bias_m,
|
||||
true,
|
||||
475.0 + ((update & 1u) ? 0.3 : -0.3),
|
||||
1.7,
|
||||
1.0 / 32.0,
|
||||
16u,
|
||||
&filtered_bias_m,
|
||||
&innovation_m);
|
||||
assert(result != GPS_PPS_TIME_FILTER_REJECTED);
|
||||
assert(isfinite(innovation_m));
|
||||
if(result == GPS_PPS_TIME_FILTER_TRACKING)
|
||||
{
|
||||
double error_m = filtered_bias_m - true_bias_m;
|
||||
squared_error_sum += error_m * error_m;
|
||||
tracked++;
|
||||
}
|
||||
}
|
||||
assert(tracked > 100u);
|
||||
assert(sqrt(squared_error_sum / (double)tracked) < 12.0);
|
||||
}
|
||||
|
||||
static void test_quality_gap_and_outlier_handling(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, 100.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
|
||||
&filtered, &innovation) == GPS_PPS_TIME_FILTER_COARSE);
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 25000000u, COUNTER_HZ, 102.0, true, 10.0, 12.0, 1.0 / 32.0, 1u,
|
||||
&filtered, &innovation) == GPS_PPS_TIME_FILTER_REJECTED);
|
||||
|
||||
uint64_t saved_epoch = filter.epoch_counter;
|
||||
double saved_bias = filter.clock_bias_m;
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 25000000u, COUNTER_HZ, 5000.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
|
||||
&filtered, &innovation) == GPS_PPS_TIME_FILTER_REJECTED);
|
||||
assert(filter.epoch_counter == saved_epoch);
|
||||
assert(filter.clock_bias_m == saved_bias);
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 0u, COUNTER_HZ, 100.0, true, 10.0, 1.0,
|
||||
1.0 / 32.0, 1u, &filtered, &innovation) ==
|
||||
GPS_PPS_TIME_FILTER_REJECTED);
|
||||
assert(filter.epoch_counter == saved_epoch);
|
||||
assert(filter.clock_bias_m == saved_bias);
|
||||
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 375000000u, COUNTER_HZ, 130.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
|
||||
&filtered, &innovation) == GPS_PPS_TIME_FILTER_TRACKING);
|
||||
assert(fabs(filtered - 130.0) < 1.0e-12);
|
||||
|
||||
/* A gap beyond the bounded Doppler-prediction horizon must still reset
|
||||
* the filter and require a safe coarse alignment. */
|
||||
assert(gps_pps_time_filter_update(
|
||||
&filter, 4250000000ULL, COUNTER_HZ, 440.0, true, 10.0,
|
||||
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
|
||||
GPS_PPS_TIME_FILTER_COARSE);
|
||||
assert(fabs(filtered - 440.0) < 1.0e-12);
|
||||
}
|
||||
|
||||
static void test_bias_ramp_without_doppler_uses_bounded_fallback(void)
|
||||
{
|
||||
static const double noise_m[] = {
|
||||
80.0, -110.0, 45.0, -25.0, 125.0, -70.0, 35.0, -95.0,
|
||||
60.0, -40.0, 105.0, -85.0, 20.0, -15.0, 75.0, -55.0
|
||||
};
|
||||
gps_pps_time_filter_t filter;
|
||||
gps_pps_time_filter_init(&filter);
|
||||
|
||||
double squared_error_sum = 0.0;
|
||||
unsigned int tracked = 0u;
|
||||
for(unsigned int update = 0u; update < 500u; update++)
|
||||
{
|
||||
double time_sec = 0.2 * (double)update;
|
||||
double true_bias_m = 1200.0 + 475.0 * time_sec;
|
||||
double measured_bias_m =
|
||||
true_bias_m +
|
||||
noise_m[update % (sizeof(noise_m) / sizeof(noise_m[0]))];
|
||||
double filtered_bias_m = 0.0;
|
||||
double innovation_m = 0.0;
|
||||
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
|
||||
&filter,
|
||||
(uint64_t)llround(time_sec * COUNTER_HZ),
|
||||
COUNTER_HZ,
|
||||
measured_bias_m,
|
||||
false,
|
||||
0.0,
|
||||
5.0,
|
||||
1.0 / 32.0,
|
||||
16u,
|
||||
&filtered_bias_m,
|
||||
&innovation_m);
|
||||
assert(result != GPS_PPS_TIME_FILTER_REJECTED);
|
||||
if((result == GPS_PPS_TIME_FILTER_TRACKING) && (update >= 200u))
|
||||
{
|
||||
double error_m = filtered_bias_m - true_bias_m;
|
||||
squared_error_sum += error_m * error_m;
|
||||
tracked++;
|
||||
}
|
||||
}
|
||||
assert(tracked > 250u);
|
||||
assert(filter.drift_initialized);
|
||||
assert(fabs(filter.clock_drift_mps) < 10000.0);
|
||||
assert(sqrt(squared_error_sum / (double)tracked) < 45.0);
|
||||
}
|
||||
|
||||
static void test_reference_geometry_defines_bias_drift(void)
|
||||
{
|
||||
const double receiver_position_m[3] = {0.0, 0.0, 0.0};
|
||||
const double receiver_velocity_mps[3] = {10.0, -2.0, 1.0};
|
||||
const double satellite_position_m[3] = {20200000.0, 0.0, 0.0};
|
||||
const double satellite_velocity_mps[3] = {140.0, 30.0, -10.0};
|
||||
double bias_drift_mps = 0.0;
|
||||
|
||||
/* LOS is +X, so WLS datum drift is -(140 - 10) = -130 m/s. */
|
||||
assert(gps_pps_reference_bias_drift(
|
||||
receiver_position_m,
|
||||
receiver_velocity_mps,
|
||||
satellite_position_m,
|
||||
satellite_velocity_mps,
|
||||
0.0,
|
||||
&bias_drift_mps));
|
||||
assert(fabs(bias_drift_mps + 130.0) < 1.0e-12);
|
||||
|
||||
/* Broadcast satellite-clock drift is part of the position observation. */
|
||||
assert(gps_pps_reference_bias_drift(
|
||||
receiver_position_m,
|
||||
receiver_velocity_mps,
|
||||
satellite_position_m,
|
||||
satellite_velocity_mps,
|
||||
1.0e-9,
|
||||
&bias_drift_mps));
|
||||
assert(fabs(
|
||||
bias_drift_mps -
|
||||
(-130.0 + 299792458.0 * 1.0e-9)) < 1.0e-12);
|
||||
|
||||
double invalid_satellite_position[3] = {0.0, 0.0, 0.0};
|
||||
assert(!gps_pps_reference_bias_drift(
|
||||
receiver_position_m,
|
||||
receiver_velocity_mps,
|
||||
invalid_satellite_position,
|
||||
satellite_velocity_mps,
|
||||
0.0,
|
||||
&bias_drift_mps));
|
||||
}
|
||||
|
||||
static void test_reference_selection_is_stable(void)
|
||||
{
|
||||
const unsigned int ids[] = {3u, 8u, 17u};
|
||||
const double captures[] = {102.0, 100.0, 104.0};
|
||||
unsigned int index = 99u;
|
||||
|
||||
assert(gps_pps_select_reference(
|
||||
ids, captures, 3u, false, 0u, &index));
|
||||
assert(index == 1u);
|
||||
|
||||
/* A newly earlier satellite must not move an existing WLS datum. */
|
||||
assert(gps_pps_select_reference(
|
||||
ids, captures, 3u, true, 3u, &index));
|
||||
assert(index == 0u);
|
||||
|
||||
/* Loss of the preferred satellite selects the earliest remaining one. */
|
||||
assert(gps_pps_select_reference(
|
||||
ids, captures, 3u, true, 25u, &index));
|
||||
assert(index == 1u);
|
||||
|
||||
double invalid_captures[] = {102.0, NAN, 104.0};
|
||||
assert(!gps_pps_select_reference(
|
||||
ids, invalid_captures, 3u, false, 0u, &index));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_drift_prediction_rejects_bias_noise();
|
||||
test_quality_gap_and_outlier_handling();
|
||||
test_bias_ramp_without_doppler_uses_bounded_fallback();
|
||||
test_reference_geometry_defines_bias_drift();
|
||||
test_reference_selection_is_stable();
|
||||
puts("gps_pps_time_filter tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
#include "gps/gps_time_anchor.h"
|
||||
#include "gps/Navigation.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define SAMPLE_RATE_HZ 16368000.0
|
||||
#define SAMPLE_RATE_HZ GPS_RF_SAMPLE_RATE_HZ
|
||||
|
||||
static int failures;
|
||||
|
||||
@@ -99,6 +100,89 @@ static void invalid_inputs_are_rejected(void)
|
||||
"out-of-range HOW was accepted");
|
||||
}
|
||||
|
||||
static void physical_rf_rate_avoids_six_second_sawtooth(void)
|
||||
{
|
||||
const double elapsed_sec = 6.0;
|
||||
const double samples = GPS_RF_SAMPLE_RATE_HZ * elapsed_sec;
|
||||
const double legacy_elapsed_sec = samples / 16368000.0;
|
||||
const double corrected_elapsed_sec = samples / GPS_RF_SAMPLE_RATE_HZ;
|
||||
|
||||
CHECK(fabs((legacy_elapsed_sec - elapsed_sec) * 1.0e9) > 6900.0,
|
||||
"legacy 16.368 MHz conversion did not expose HOW sawtooth");
|
||||
CHECK(fabs((corrected_elapsed_sec - elapsed_sec) * 1.0e9) < 0.001,
|
||||
"physical RF rate left a six-second timing ramp");
|
||||
}
|
||||
|
||||
static void periodic_event_time_removes_receive_range_rate(void)
|
||||
{
|
||||
const uint32_t anchor = 700000000u;
|
||||
const double physical_elapsed_sec = 5.8 + 8.0e-6;
|
||||
const uint32_t event =
|
||||
anchor + (uint32_t)llround(physical_elapsed_sec * SAMPLE_RATE_HZ);
|
||||
int week = 0;
|
||||
double tow_sec = 0.0;
|
||||
double elapsed_sec = 0.0;
|
||||
|
||||
CHECK(gps_time_from_periodic_sample_anchor(
|
||||
380, 469362.0, anchor, (double)anchor,
|
||||
event, (double)event, SAMPLE_RATE_HZ, 120.0,
|
||||
0.2, 0.005, &week, &tow_sec, &elapsed_sec),
|
||||
"periodic NAV event with range-rate delay was rejected");
|
||||
CHECK(week == 380, "periodic NAV event changed GPS week");
|
||||
CHECK(fabs(tow_sec - 469367.8) < 1.0e-9,
|
||||
"periodic NAV event retained receive-time range-rate error");
|
||||
CHECK(fabs(elapsed_sec - 5.8) < 1.0e-12,
|
||||
"periodic NAV event did not report snapped elapsed time");
|
||||
|
||||
const uint32_t off_grid_event =
|
||||
anchor + (uint32_t)llround(5.82 * SAMPLE_RATE_HZ);
|
||||
CHECK(!gps_time_from_periodic_sample_anchor(
|
||||
380, 469362.0, anchor, (double)anchor,
|
||||
off_grid_event, (double)off_grid_event,
|
||||
SAMPLE_RATE_HZ, 120.0, 0.2, 0.005,
|
||||
&week, &tow_sec, &elapsed_sec),
|
||||
"off-grid NAV event was accepted");
|
||||
}
|
||||
|
||||
static void fractional_code_phase_corrects_capture_counter(void)
|
||||
{
|
||||
const uint64_t capture_counter = 1000000u;
|
||||
uint64_t corrected_counter = 0u;
|
||||
double correction_sec = 0.0;
|
||||
|
||||
CHECK(gps_time_correct_capture_counter(
|
||||
capture_counter, 0.75, SAMPLE_RATE_HZ, 125000000.0,
|
||||
&corrected_counter, &correction_sec),
|
||||
"valid fractional code phase did not correct PPS capture");
|
||||
/* 0.75 RF sample is 45.82 ns, rounded onto the 8 ns PPS counter grid. */
|
||||
CHECK(corrected_counter == capture_counter - 6u,
|
||||
"fractional code phase used the wrong capture-counter correction");
|
||||
CHECK(fabs(correction_sec - 48.0e-9) < 1.0e-15,
|
||||
"capture correction did not report the applied integer tick");
|
||||
|
||||
CHECK(gps_time_correct_capture_counter(
|
||||
capture_counter, 0.0, SAMPLE_RATE_HZ, 125000000.0,
|
||||
&corrected_counter, &correction_sec) &&
|
||||
(corrected_counter == capture_counter),
|
||||
"zero code residue changed the capture counter");
|
||||
CHECK(!gps_time_correct_capture_counter(
|
||||
capture_counter, -0.1, SAMPLE_RATE_HZ, 125000000.0,
|
||||
&corrected_counter, NULL),
|
||||
"negative fractional sample delay was accepted");
|
||||
CHECK(!gps_time_correct_capture_counter(
|
||||
capture_counter, 1.1, SAMPLE_RATE_HZ, 125000000.0,
|
||||
&corrected_counter, NULL),
|
||||
"capture correction larger than one RF sample was accepted");
|
||||
CHECK(!gps_time_correct_capture_counter(
|
||||
2u, 1.0, SAMPLE_RATE_HZ, 125000000.0,
|
||||
&corrected_counter, NULL),
|
||||
"capture-counter underflow was accepted");
|
||||
CHECK(!gps_time_correct_capture_counter(
|
||||
UINT64_MAX, 1.0, 1.0, 1.0e30,
|
||||
&corrected_counter, NULL),
|
||||
"capture correction beyond llround range was accepted");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
normal_and_skipped_subframes_are_consistent();
|
||||
@@ -106,6 +190,9 @@ int main(void)
|
||||
temporary_week_can_upgrade_on_first_subframe_one();
|
||||
captured_future_jump_is_rejected_and_recovers();
|
||||
invalid_inputs_are_rejected();
|
||||
physical_rf_rate_avoids_six_second_sawtooth();
|
||||
periodic_event_time_removes_receive_range_rate();
|
||||
fractional_code_phase_corrects_capture_counter();
|
||||
if(failures == 0)
|
||||
{
|
||||
puts("gps_time_anchor tests passed");
|
||||
|
||||
Reference in New Issue
Block a user