Compare commits

...

2 Commits

Author SHA1 Message Date
onur e57b19b4ee fix(gps): synchronize complete raw navigation epochs 2026-07-24 15:00:40 +03:00
onur fe0eb19dc5 fix(gps): harden navigation epoch recovery 2026-07-24 14:01:58 +03:00
18 changed files with 1280 additions and 818 deletions
@@ -909,15 +909,16 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
stage_metrics->candidate_total += candidate_count;
gps_nav_epoch_selection_t selection;
bool selected = gps_nav_epoch_select(
bool selected = gps_nav_epoch_validate_complete(
candidates, candidate_count, EPOCH_SAMPLE_WINDOW,
EPOCH_TOW_TOLERANCE_SEC, &selection);
unsigned selected_count = selected ? selection.selected_count : 0u;
unsigned selected_count = selection.selected_count;
stage_metrics->selected_total += selected_count;
stage_metrics->epoch_exclusions += candidate_count - selected_count;
if (stage == STAGE_DELAYED_CHANNEL &&
(selected_count != 7u ||
(selected ||
selected_count != 7u ||
(selection.selected_mask & (1u << 7)) != 0u)) {
delayed_selection_failures++;
}
@@ -930,7 +931,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
postcommit_join_count_failures++;
}
if (selected_count < STATE_DIM) {
if (!selected || selected_count < STATE_DIM) {
continue;
}
@@ -983,14 +984,14 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
CHECK(metrics[STAGE_THREE_DROPOUTS].fixes >= 58u,
"five remaining satellites must continue producing fixes");
CHECK(metrics[STAGE_MINIMUM_FOUR].fixes == 40u,
"four satellites must remain a publishable minimum fix");
"four satellites must remain a raw publishable minimum fix");
CHECK(metrics[STAGE_OUTAGE_THREE].fixes == 0u && below_minimum_fixes == 0u,
"three satellites must suppress the position solution");
CHECK(metrics[STAGE_REACQUIRE].fixes >= 58u,
"position must recover after satellite reacquisition");
CHECK(delayed_selection_failures == 0u &&
metrics[STAGE_DELAYED_CHANNEL].fixes >= 58u,
"delayed/newest outlier channel must be excluded without blocking fixes");
metrics[STAGE_DELAYED_CHANNEL].fixes == 0u,
"incoherent delayed channel must reject the complete raw epoch");
CHECK(precommit_join_count_failures == 0u,
"joining channel must remain excluded before matching SF1/SF2/SF3");
CHECK(postcommit_join_count_failures == 0u,
+9 -8
View File
@@ -627,7 +627,7 @@ GPS_EVENT_QUEUE_SIZE
gps_last_measurement_tick
gps_measurement_valid
gps_nav_solve_sequence
gps_nav_solve_consumed_sequence
gps_last_nav_solve_pps_counter
```
- Navigation input'u hazir kabul edilmeden once su kontroller yapilir:
@@ -665,10 +665,10 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
- `NAV_SOLVE_INTR_EN=1`,
- `NAV_SOLVE_FLAG_EN=1`.
- Ilk pozisyon dahil butun pozisyon hesaplari, kanallardan senkron nav-solve tick ve `ABS_SAMPLE` geldikten sonra olusturulan coherent nav-solve snapshot'i ile yapilir.
- Nav-bit ve nav-solve handler'lari ephemeris deposunu ortak kullansa da corrected `ABS_SAMPLE` nav-solve icin node icinde ayri saklanir. Pozisyon snapshot'i bu ayri alani kullanir; araya giren nav-bit interrupt'i pozisyon olcum epoch'unu degistiremez.
- Nav-bit ve nav-solve handler'lari canli ephemeris deposunu ortak kullansa da her kabul edilen nav-solve event'i corrected `ABS_SAMPLE`, TOW ve o anda commit edilmis ephemerisin tam kopyasini aktif epoch snapshot'ina birlikte yazar. WLS daha sonra canli node listesinden ephemeris okumaz; araya giren nav-bit interrupt'i eski olcumle yeni ephemerisi karistiramaz.
- Coherent snapshot olusturulurken 32-bit raw `ABS_SAMPLE` degerleri en yeni sample etrafinda modulo fark ile unwrap edilir. Sayac wrap sinirinin iki tarafindaki uydular ayni pseudorange hesabinda kullanilabilir.
- Nav bits akisi ilk tutarli subframe 1..3 seti commit edildikten sonra kapatilmaz. Boylece yeni subframe 1..3 verisi ephemerisi, subframe 4 page 18 verisi iyonosfer/UTC parametrelerini guncel tutar.
- Nav-solve interrupt'lari acilmadan once TOW, nav-bit HOW alanindan kurulur. Nav-solve acildiktan sonra normal nav-bit HOW gecmisi tutulmaya devam eder fakat canli TOW'a yazmaz. Her nav-solve interrupt'i tam olarak `NAV_SOLVE_TIMER * 20 ms` ilerlemeyi temsil eder ve TOW bu sabit miktarda artirilir.
- Nav-solve interrupt'lari acilmadan once TOW, parity kontrolunden gecen nav-bit HOW alanindan kurulur. Her yeni HOW'un GPS zaman farki receiver sample-counter farkiyla karsilastirilir; tek bir sureksiz HOW saglikli anchor'i degistiremez. Iki ardışık aday kendi aralarinda tutarliysa kanal reset gerektirmeden yeni zaman cizgisine gecerek toparlanir. Subframe 1 gelmeden onceki gecici hafta, ilk gecerli WN ile atomik olarak guncellenir.
- Subframe 1..3 ham kelimeleri once staging alaninda biriktirilir. IODC'nin alt 8 biti ile iki IODE alani eslesen tam set olusmadan canli ephemeris degistirilmez; yayin guncellemesi sirasinda eski/yeni ephemeris alanlari karismaz.
- Subframe 1 `toc` alani parity bitleri disarida birakilarak 16 bit okunur ve `2^4` ile olceklenir.
- Subframe 4 page 18 henuz alinmadiysa iyonosfer duzeltmesi uygulanmaz. Page 18 hazir oldugunda her navigation solve tek bir `ionoutc` kopyasi kullanir.
@@ -678,9 +678,10 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
- Tracking kanallari farkli zamanlarda lock olabilir ve ilk gecerli subframe'lerini 6 saniyenin farkli katlarinda gorebilir. Timer bu nedenle ilk subframe'in HOW alanindaki 17-bit Z-count degerini kullanarak `Z_COUNT * 300 mod NAV_SOLVE_TIMER` baslangic fazini hesaplar. Hesap ALIGN durumunda 26 PCLK cevriminde ard arda modulo ile yapilir; 20 ms'lik ilk navigation bitinden cok once tamamlanir. RTL ayni GPS haftasi icinde keyfi pozitif `N` degerlerini hizalayabilir; firmware ise hafta rollover'i oncesinden aktif kalan kanallarla rollover sonrasinda katilan kanallarin da ayni fazda kalmasi icin `30240000 mod N == 0` kosulunu compile time'da zorunlu tutar. Ornegin `N=40`, `N=100`, `N=125`, `N=150`, `N=200` ve `N=300` gecerlidir; kosulu saglamayan build acik bir `#error` ile durur.
- Initial position sonrasinda subframe'lerini tamamlayan yeni tracking kanallarinda da nav bits acik tutularak nav-solve otomatik etkinlestirilir.
- Coherence kontrolu sadece software systick ile yapilmaz; periodic snapshot icin tracking node'da saklanan nav-solve interrupt sonrasi raw `ABS_SAMPLE` kullanilir. `GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS=25` oldugu icin 4 satellite measurement'i 25 ms'den fazla ayriksa navigation solve calistirilmaz. Bu pencere interrupt skew'den cok uydu path delay farkini kapsamak icindir: 20 ms yaklasik 6000 km, 25 ms yaklasik 7500 km pseudorange farkina denk gelir. 25 ms, dusuk-elevation uydu geometrisi ve tracking jitter icin pay birakir ama 50 ms kadar gevsek degildir.
- Her nav periyodunda yeni sequence'i bulunan adaylar sabit boyutlu, heap kullanmayan epoch selector'a verilir. Selector TOW ve modulo-32-bit `ABS_SAMPLE` kosullarini ikili olarak saglayan en buyuk coherent alt kumeyi secer; tek bir gecikmis veya bozuk kanal diger en az 4 uyduyu bloke etmez. Esit buyuklukte ayrik kumelerde daha yeni sample epoch'u tercih edilir.
- En az 4 uydu secildiginde kume disindaki snapshot sequence'leri emekliye ayrilir. Tuketim snapshot aninda kaydedilen sequence degerine kadar yapildigi icin arada gelen daha yeni interrupt korunur. Tamamlanan donanim epoch'u WLS sonucu basarili veya basarisiz olsa da yalniz bir kez denenir; ayni olcumlerin ilgisiz bir foreground wake-up ile tekrar cozulmesi engellenir.
- Navigation snapshot ayni SVID/PRN'i ikinci kez kabul etmez. Ayni uydu iki tracking IP uzerinden ready gorunurse ikinci kopya sessizce snapshot disinda birakilir; ayrinti sadece `DEBUG_TRACE` seviyesinde gorulur.
- Interrupt'lar epoch'a GPS TOW ile degil, butun kanallarda ortak olan 64-bit PPS capture counter ile atanir. Ilk olcum sabit expected maskeyi ve 30 ms bounded holdoff'u baslatir. 25 ms pencereye giren kanallar ayni fiziksel epoch'a katilir; yarim nav periyodundan uzaktaki eski/yeni capture'lar stale veya yeni epoch olarak siniflanir. Boylece bozuk bir gelecek HOW degeri epoch assembler'i kilitleyemez.
- Yalniz frozen expected maskenin tamami geldiginde snapshot sabit boyutlu iki buffer arasinda immutable olarak foreground'a devredilir. 30 ms holdoff dolan eksik epoch ve daha yeni counter geldigi anda yarim kalan eski epoch butunuyle discard edilir. Hardware mailbox birden fazla pulse'u kacirdiysa sequence/counter farki tutarli olan en yeni gercek olcum kabul edilir; elde olmayan ara olcumler sentetik olarak uretilmez. Sequence ile 64-bit capture counter uyusmazsa event yalniz rebase icin kullanilir ve epoch'a girmez.
- Immutable snapshot icindeki butun adaylar heap kullanmayan selector ile ikili TOW ve modulo-32-bit `ABS_SAMPLE` coherence kontrolunden gecirilir. En buyuk coherent kume frozen aday sayisindan bir eksik bile olsa tum epoch reddedilir; WLS icin alt kumeye dusulmez. Tamamlanan snapshot WLS sonucu basarili veya basarisiz olsa da yalniz bir kez denenir.
- Navigation snapshot ayni SVID/PRN'i ikinci kez kabul etmez. Ayni uydu iki tracking IP uzerinden frozen ready maskeye girerse aday sayisi maskeyle uyusmayacagi icin tum epoch reddedilir; ayni uydu iki kez WLS'e giremez.
- UART debug satirindaki `coherent_nav=` degeri, o anda ayni nav-solve penceresinde kullanilabilir kanal sayisini gosterir. 600 ms pozisyon gelmiyorsa `busy channels`, `ready channels` ve `coherent_nav` birlikte kontrol edilmelidir.
- Tracking ISR event queue dolarsa, her tracking IP icin son event shadow buffer'a alinir. Shadow overwrite sayaci WARN olarak loglanir; bu log gorulurse CPU nav hesaplamasi veya UART debug cikisi tracking interrupt isleme hizini geciktiriyor olabilir.
- Bir tracking kanali shadow buffer'a dustukten sonra shadow main context tarafindan tuketilene kadar ayni kanalin yeni event'leri tekrar ana queue'ya alinmaz; en yeni shadow snapshot'i korunur. Boylece daha yeni queue event'inin daha eski shadow event'inden once islenmesi ve raw sample zamaninin geriye gitmesi engellenir.
@@ -727,8 +728,8 @@ weight = 1 / variance
- `computeRange()` icinde aktif parametreler `psTVLTime`, `sat_no`, `eph`, `clockCorr` ve `satellitePositions`'dir. `rho`, `ionoutc`, `xyz`, `tempPos` ve `velocities` eski genis API'den kalmistir. Ionosphere correction artik receiver azimut/elevasyonu hazir oldugunda WLS icinde uygulanir.
- Ephemeris alanlarinin mevcut isimleri ile ICD anlamlari su sekildedir: `omgdot` gercekte `Omega0`, `omg0` gercekte argument of perigee `omega`, `omgkdot` gercekte `OmegaDot` degeridir. `satpos()` bunlari dogru matematiksel yerde kullanir; konu su an hesap hatasi degil, ileride yapilacak kontrollu isim duzeltmesidir.
- `POSTPONE_TOW=0.160` donanim/pipeline zaman kalibrasyonunun bir parcasidir; kaynagi dogrulanmadan kaldirilmamalidir.
- Ilk nav-solve interrupt'i geldiginde o anda allocated, hardware busy ve nav-solve interrupt'i etkin olan kanallar epoch'un sabit bekleme maskesine alinir. WLS ancak bu kanallarin ayni GPS epoch'una ait interrupt'lari tamamlandiginda tetiklenir. Unlock interrupt'i gelen kanal bekleme maskesinden cikarilir; epoch ortasinda navigation-ready olan yeni kanal ise aktif maskeyi buyutmez ve sonraki epoch'tan itibaren beklenir. Timeout yoktur. Eksik kalan bir epoch'tan daha yeni bir interrupt gelirse eski maske temizlenir ve yeni epoch ilk kanaldan baslatilarak butun ready kanallar yeniden beklenir; eski ve yeni olcumler birlestirilmez.
- Interrupt bekleme maskesi tamamlandiktan sonra en az dort uyduluk en buyuk ortak TOW/`ABS_SAMPLE` kumesi WLS girdisi olarak kullanilir; kume disi kanallar `epoch_excluded` olarak raporlanir ve sonraki donanim epoch'unda yeniden katilabilir.
- Ilk nav-solve interrupt'i geldiginde o anda allocated, hardware busy ve nav-solve interrupt'i etkin olan kanallar epoch'un sabit bekleme maskesine alinir. Unlock interrupt'i gelen kanal artik tracked olmadigi icin bekleme maskesinden cikarilir; epoch ortasinda navigation-ready olan yeni kanal ise aktif maskeyi buyutmez ve sonraki epoch'tan itibaren beklenir. Kalan beklenen kanallarin tamami gelirse snapshot son interrupt'ta hemen yayinlanir. 30 ms holdoff dolarsa veya daha yeni fiziksel epoch baslarsa eksik eski epoch WLS calistirilmadan discard edilir; eski ve yeni olcumler birlestirilmez.
- Interrupt bekleme maskesi tamamlandiktan sonra frozen kanallarin tumu ortak TOW/`ABS_SAMPLE` kumesinde degilse epoch reddedilir. Selector'in buldugu daha kucuk alt kume yalniz diagnostic bilgidir ve WLS girdisi olamaz.
- Dort uydu 3D konum + receiver clock bias icin minimum ve yeterlidir. Bu durumda
`dof=N-4=0` oldugu icin WLS konumu publish edilir fakat residual chi-square
integrity bayragi false kalir. Bes veya daha fazla uyduda publish icin mevcut
@@ -3,6 +3,7 @@
#include "gps.h"
#include "gps/gps_ip_defines.h"
#include "gps/gps_nav_epoch.h"
#include "gps/matrixLib.h"
typedef struct gps_nav_solution {
@@ -8,6 +8,10 @@
uint32_t plic_drain_limit_total;
uint32_t plic_repeat_total;
uint32_t nav_epoch_coalesced_total;
uint32_t pps_sequence_discontinuity_total;
uint32_t nav_epoch_incomplete_total;
uint32_t nav_epoch_snapshot_overwrite_total;
uint32_t how_anchor_rejected_total;
uint32_t satellite_connected_total;
uint32_t satellite_lost_total;
uint32_t last_connected_prn;
@@ -16,6 +20,18 @@
uint64_t last_lost_tick;
} gps_runtime_diagnostics_t;
typedef struct {
uint32_t channel_mask;
uint32_t prn[GPS_TRCK_IP_COUNT];
gps_ephemeris_t ephemeris[GPS_TRCK_IP_COUNT];
gpstime_t tow[GPS_TRCK_IP_COUNT];
uint32_t raw_sample[GPS_TRCK_IP_COUNT];
double corrected_abs_sample[GPS_TRCK_IP_COUNT];
uint64_t pps_counter[GPS_TRCK_IP_COUNT];
uint32_t pps_capture_sequence[GPS_TRCK_IP_COUNT];
uint64_t local_timestamp_ticks[GPS_TRCK_IP_COUNT];
} gps_nav_epoch_snapshot_t;
typedef void (*gps_external_irq_handler_t)(void);
bool assign_tracking_from_result(const gps_acq_result_t* result);
@@ -28,7 +44,7 @@
void gps_process_isr_events(void);
void gps_acq_scheduler_run(void);
void gps_background_task(void);
bool gps_take_nav_solve_period_ready(void);
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot);
void gps_get_runtime_diagnostics(gps_runtime_diagnostics_t* diagnostics);
void trck_intr_handler(volatile gps_trck_t* const trck_ip, volatile gps_frame_t *gps_frame_parsed, volatile int unsigned gps_subframe[10], unsigned char trck_ip_no);
@@ -18,7 +18,6 @@
// | || __/ | |_| | |___| _| | || |\ | |___ ___) |
// |___|_| |____/|_____|_| |___|_| \_|_____|____/
#define MIN_NEEDED_SATELLITE 4
#define GPS_TRCK_UNLOCK_TIMEOUT_TICKS 500000000u
#define GPS_PRN_COUNT 32u
#define GPS_PRN_INVALID 0xffu
@@ -63,8 +62,11 @@
#define GPS_NAV_STATUS_LOG_PERIOD_TICKS GPS_SYSTICK_HZ
#define GPS_TRK_MEASUREMENT_STALE_TICKS (GPS_SYSTICK_HZ * 3u)
#define GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS 25u
#define GPS_NAV_EPOCH_HOLDOFF_MS 30u
#define GPS_NAV_TOW_CLUSTER_TOLERANCE_SEC 0.001
#define GPS_HOW_ANCHOR_MAX_AGE_SEC 12.0
#define GPS_HOW_CONTINUITY_MAX_GAP_SEC 120.0
#define GPS_HOW_CONTINUITY_TOLERANCE_SEC 0.050
#if (GPS_NAV_SOLVE_TIMER_TICKS == 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
@@ -4,6 +4,15 @@
#include <stdbool.h>
#include <stdint.h>
/*
* Four pseudoranges are the algebraic minimum for receiver x/y/z plus clock
* bias. They leave no residual degree of freedom, so a bad measurement can
* produce an arbitrary position while fitting perfectly. The raw receiver
* still exposes that minimum WLS fix and its covariance; the separate
* residual/integrity flags tell downstream fusion that no redundancy exists.
*/
#define GPS_NAV_MIN_WLS_SATELLITES 4u
/*
* The current receiver exposes nine tracking channels. Keep the epoch
* selector bounded so its exhaustive, allocation-free search has a fixed
@@ -11,6 +20,10 @@
*/
#define GPS_NAV_EPOCH_MAX_CANDIDATES 9u
_Static_assert(GPS_NAV_MIN_WLS_SATELLITES <=
GPS_NAV_EPOCH_MAX_CANDIDATES,
"GPS WLS minimum exceeds epoch capacity");
typedef struct {
int week;
double tow_sec;
@@ -24,7 +37,10 @@ typedef struct {
} gps_nav_epoch_selection_t;
typedef struct {
uint64_t epoch_id;
uint64_t first_capture_counter;
uint64_t minimum_capture_counter;
uint64_t maximum_capture_counter;
uint64_t deadline_local_ticks;
uint32_t expected_mask;
uint32_t received_mask;
bool active;
@@ -35,6 +51,7 @@ typedef enum {
GPS_NAV_EPOCH_EVENT_ACCEPTED,
GPS_NAV_EPOCH_EVENT_DUPLICATE,
GPS_NAV_EPOCH_EVENT_STALE,
GPS_NAV_EPOCH_EVENT_OUTLIER,
GPS_NAV_EPOCH_EVENT_RESTARTED,
GPS_NAV_EPOCH_EVENT_COMPLETE
} gps_nav_epoch_event_result_t;
@@ -46,19 +63,43 @@ bool gps_nav_epoch_select(
double tow_tolerance_sec,
gps_nav_epoch_selection_t* selection);
bool gps_nav_epoch_id(
int week,
double tow_sec,
unsigned int period_ms,
uint64_t* epoch_id);
/*
* Receiver publication is stricter than diagnostic clustering: every
* candidate in the frozen ready mask must belong to one coherent epoch.
* selection is still populated with the largest cluster on failure so the
* caller can report why the complete epoch was rejected.
*/
bool gps_nav_epoch_validate_complete(
const gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES],
unsigned int candidate_count,
uint32_t sample_window,
double tow_tolerance_sec,
gps_nav_epoch_selection_t* selection);
/*
* Validate a newest-only hardware mailbox after one or more capture events
* were coalesced. sequence_gap receives the number of real elapsed epochs;
* missing epochs are never synthesized.
*/
bool gps_nav_capture_sequence_consistent(
uint64_t previous_counter,
uint32_t previous_sequence,
uint64_t current_counter,
uint32_t current_sequence,
uint64_t capture_period_ticks,
uint32_t* sequence_gap);
void gps_nav_epoch_assembly_reset(gps_nav_epoch_assembly_t* assembly);
gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
gps_nav_epoch_assembly_t* assembly,
uint64_t epoch_id,
uint64_t capture_counter,
uint64_t local_timestamp_ticks,
unsigned int channel,
uint32_t ready_mask);
uint32_t ready_mask,
uint64_t capture_period_ticks,
uint64_t sync_window_ticks,
uint64_t holdoff_local_ticks);
bool gps_nav_epoch_assembly_remove_channel(
gps_nav_epoch_assembly_t* assembly,
@@ -67,4 +108,12 @@ bool gps_nav_epoch_assembly_remove_channel(
bool gps_nav_epoch_assembly_complete(
const gps_nav_epoch_assembly_t* assembly);
bool gps_nav_epoch_assembly_usable(
const gps_nav_epoch_assembly_t* assembly,
unsigned int minimum_channels);
bool gps_nav_epoch_assembly_expired(
const gps_nav_epoch_assembly_t* assembly,
uint64_t now_local_ticks);
#endif
@@ -16,6 +16,11 @@ int gps_time_resolve_how_week(bool previous_valid,
int fallback_week,
double how_tow_sec);
/* Extend a modulo-1024 LNAV week number around the previous monotonic week. */
int gps_time_extend_lnav_week(bool previous_valid,
int previous_week,
unsigned int lnav_week);
/* Convert a receiver-sample timestamp to GPS time from a HOW/sample anchor.
* raw counters are modulo 2^32. max_age_sec must keep the signed difference
* unambiguous (and also rejects a stale HOW anchor). */
@@ -31,4 +36,19 @@ bool gps_time_from_sample_anchor(int anchor_week,
double* event_tow_sec_out,
double* elapsed_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. */
bool gps_time_anchors_consistent(int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
double first_corrected_sample,
int second_week,
double second_tow_sec,
uint32_t second_raw_sample,
double second_corrected_sample,
double samples_per_second,
double max_gap_sec,
double tolerance_sec);
#endif
@@ -13,8 +13,12 @@ typedef struct gps_ephem_ll {
volatile double gps_how_corrected_abs_sample;
volatile uint64_t gps_how_tick;
volatile bool gps_how_anchor_valid;
volatile bool gps_how_week_valid;
volatile gpstime_t gps_pending_how_tow;
volatile uint32_t gps_pending_how_abs_sample;
volatile double gps_pending_how_corrected_abs_sample;
volatile bool gps_pending_how_valid;
volatile uint32_t gps_nav_solve_sequence;
volatile uint32_t gps_nav_solve_consumed_sequence;
volatile gpstime_t gps_last_nav_solve_tow;
volatile uint32_t gps_last_nav_solve_abs_sample;
volatile double gps_last_nav_solve_corrected_abs_sample;
+10 -7
View File
@@ -636,7 +636,7 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
if((satpos == NULL) || (obs == NULL) || (az == NULL) || (el == NULL) ||
(ura_indices == NULL) || (solution_out == NULL) ||
(satpos->matrix == NULL) || (obs->matrix == NULL) ||
(ActiveSatellitesNumber < MIN_NEEDED_SATELLITE) ||
(ActiveSatellitesNumber < (int)GPS_NAV_MIN_WLS_SATELLITES) ||
((unsigned int)ActiveSatellitesNumber > GPS_TRCK_IP_COUNT))
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "least squares skipped invalid inputs, sats=%d\r\n",
@@ -1201,11 +1201,14 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
solution_out->fde_applied = false;
/* Four observations exactly determine x/y/z plus receiver clock bias. With
* zero residual DOF no chi-square integrity statement is possible, but the
* finite WLS solution and geometry covariance are still a valid minimum fix. */
* raw finite WLS solution and covariance remain available to downstream
* fusion, which performs its own temporal innovation test. */
solution_out->solution_valid = solution_out->candidate_valid &&
(((solution_out->used_satellite_count == 4u) &&
(((solution_out->used_satellite_count ==
GPS_NAV_MIN_WLS_SATELLITES) &&
(solution_out->degrees_of_freedom == 0u)) ||
(solution_out->residual_test_valid && solution_out->chi_square_pass));
(solution_out->residual_test_valid &&
solution_out->chi_square_pass));
solution_out->iteration_count = iteration_count;
solution_out->candidate_cycles = gps_read_cycle_counter() - candidate_start_cycles;
@@ -1392,7 +1395,8 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
? (gps_read_cycle_counter() - fde_start_cycles) : 0u;
selected_solution.total_wls_cycles = gps_read_cycle_counter() - total_wls_start_cycles;
selected_solution.solution_valid = selected_solution.candidate_valid &&
(((selected_solution.used_satellite_count == 4u) &&
(((selected_solution.used_satellite_count ==
GPS_NAV_MIN_WLS_SATELLITES) &&
(selected_solution.degrees_of_freedom == 0u)) ||
(selected_solution.residual_test_valid &&
selected_solution.chi_square_pass));
@@ -1406,8 +1410,7 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
}
*solution_out = selected_solution;
/* Print only publishable fixes. A four-satellite minimum fix is publishable,
* although its separate integrity flag remains false because residual DOF=0. */
/* Print every finite raw fix. Integrity remains a separate downstream input. */
if(solution_out->solution_valid)
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
@@ -25,7 +25,6 @@ typedef struct {
uint32_t rem_code_phase;
double corrected_abs_sample;
uint32_t nav_solve_timer;
uint32_t nav_solve_event_count;
uint64_t local_timestamp_ticks;
uint64_t pps_counter;
uint32_t pps_capture_sequence;
@@ -40,7 +39,6 @@ typedef struct {
bool nav_solve_pending;
bool misc_pending;
unsigned int nav_bits_overwrite_count;
uint32_t nav_solve_event_count;
gps_isr_event_t drop_event;
gps_isr_event_t nav_bits_event;
gps_isr_event_t nav_solve_event;
@@ -71,14 +69,40 @@ static volatile unsigned int gps_plic_drain_limit_count = 0;
static volatile unsigned int gps_plic_repeat_count = 0;
static volatile bool gps_acq_schedule_pending = true;
static volatile bool gps_timer_maintenance_pending = true;
static volatile bool gps_nav_solve_period_pending = false;
static gps_nav_epoch_assembly_t gps_nav_epoch_assembly;
static gps_nav_epoch_snapshot_t gps_nav_active_snapshot;
static gps_nav_epoch_snapshot_t gps_nav_completed_snapshot;
static bool gps_nav_completed_snapshot_pending;
static volatile gps_runtime_diagnostics_t gps_runtime_diagnostics;
static unsigned int gps_acq_round_robin_prn = 0;
static unsigned int gps_external_irq_id = 0u;
static gps_external_irq_handler_t gps_external_irq_handler = NULL;
#define GPS_PLIC_DRAIN_LIMIT (GPS_TRCK_IP_COUNT + GPS_ACQ_IP_COUNT + 4u)
#define GPS_NAV_CAPTURE_PERIOD_TICKS \
(((uint64_t)GPS_PPS_CLOCK_HZ * GPS_NAV_SOLVE_PERIOD_MS) / 1000u)
#define GPS_NAV_CAPTURE_WINDOW_TICKS \
(((uint64_t)GPS_PPS_CLOCK_HZ * GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS) / 1000u)
#define GPS_NAV_HOLDOFF_LOCAL_TICKS \
(((uint64_t)BSP_CLINT_TIMEBASE_HZ * GPS_NAV_EPOCH_HOLDOFF_MS) / 1000u)
_Static_assert(GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS <
(GPS_NAV_SOLVE_PERIOD_MS / 2u),
"NAV capture window must be below half the solve period");
_Static_assert(GPS_NAV_EPOCH_HOLDOFF_MS >=
GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS,
"NAV holdoff must cover the capture window");
static unsigned int gps_mask_count(uint32_t mask)
{
unsigned int count = 0u;
while(mask != 0u)
{
count += mask & 1u;
mask >>= 1u;
}
return count;
}
static const char* gps_satellite_state_name(gps_satellite_state_t state)
{
@@ -232,37 +256,60 @@ static bool gps_take_timer_maintenance_pending(void)
return pending;
}
bool gps_take_nav_solve_period_ready(void)
static void gps_nav_discard_active_snapshot(bool reset_assembly)
{
bool tracking_shadow_pending = false;
bool acquisition_shadow_pending = false;
memset(&gps_nav_active_snapshot, 0, sizeof(gps_nav_active_snapshot));
if(reset_assembly)
{
gps_nav_epoch_assembly_reset(&gps_nav_epoch_assembly);
}
}
static void gps_nav_publish_active_snapshot(uint32_t channel_mask,
bool reset_assembly)
{
channel_mask &= gps_nav_active_snapshot.channel_mask;
if(gps_mask_count(channel_mask) < GPS_NAV_MIN_WLS_SATELLITES)
{
gps_runtime_diagnostics.nav_epoch_incomplete_total++;
gps_nav_discard_active_snapshot(reset_assembly);
return;
}
if(gps_nav_completed_snapshot_pending)
{
gps_runtime_diagnostics.nav_epoch_snapshot_overwrite_total++;
}
gps_nav_active_snapshot.channel_mask = channel_mask;
gps_nav_completed_snapshot = gps_nav_active_snapshot;
gps_nav_completed_snapshot_pending = true;
gps_nav_discard_active_snapshot(reset_assembly);
}
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot)
{
if(snapshot == NULL)
{
return false;
}
uint64_t irq_state = bsp_irq_save();
for(unsigned int i = 0 ; i < GPS_TRCK_IP_COUNT ; i++)
uint64_t now_local_ticks = bsp_read64(BSP_CLINT_MTIME);
if(gps_nav_epoch_assembly_expired(&gps_nav_epoch_assembly,
now_local_ticks))
{
if(trk_shadow[i].drop_pending || trk_shadow[i].nav_bits_pending ||
trk_shadow[i].nav_solve_pending || trk_shadow[i].misc_pending)
{
tracking_shadow_pending = true;
break;
}
gps_runtime_diagnostics.nav_epoch_incomplete_total++;
/* An epoch is an all-or-nothing hardware measurement set. Publishing
* a timed-out subset changes its satellite geometry according to IRQ
* timing and was the source of large raw-position jumps. */
gps_nav_discard_active_snapshot(true);
}
for(unsigned int i = 0 ; i < GPS_ACQ_IP_COUNT ; i++)
{
if(acq_shadow_pending[i])
{
acquisition_shadow_pending = true;
break;
}
}
bool pipeline_idle = (gps_event_head == gps_event_tail) &&
!tracking_shadow_pending && !acquisition_shadow_pending;
bool ready = gps_nav_solve_period_pending && pipeline_idle;
bool ready = gps_nav_completed_snapshot_pending;
if(ready)
{
/* The complete epoch is handed to the foreground exactly once. Events
* arriving while WLS runs start the next assembly. */
gps_nav_solve_period_pending = false;
gps_nav_epoch_assembly_reset(&gps_nav_epoch_assembly);
*snapshot = gps_nav_completed_snapshot;
gps_nav_completed_snapshot_pending = false;
}
bsp_irq_restore(irq_state);
return ready;
@@ -313,8 +360,10 @@ static void gps_runtime_state_init(void)
gps_plic_repeat_count = 0;
gps_acq_schedule_pending = true;
gps_timer_maintenance_pending = true;
gps_nav_solve_period_pending = false;
gps_nav_epoch_assembly_reset(&gps_nav_epoch_assembly);
memset(&gps_nav_active_snapshot, 0, sizeof(gps_nav_active_snapshot));
memset(&gps_nav_completed_snapshot, 0, sizeof(gps_nav_completed_snapshot));
gps_nav_completed_snapshot_pending = false;
memset((void*)&gps_runtime_diagnostics, 0, sizeof(gps_runtime_diagnostics));
gps_runtime_diagnostics.last_connected_prn = GPS_PRN_INVALID;
gps_runtime_diagnostics.last_lost_prn = GPS_PRN_INVALID;
@@ -708,9 +757,8 @@ void trck_intr_handler(volatile gps_trck_t* const trck_ip,
event.rem_code_phase = trck_ip->REM_CODE_PHASE_REG;
event.corrected_abs_sample = gps_correct_abs_sample(event.abs_sample_raw, event.rem_code_phase);
event.nav_solve_timer = trck_ip->NAV_SOLVE_TIMER;
event.nav_solve_event_count = (status_reg & (1u << GPS_TRCK_NAV_SOLVE_FLAG_POS)) ? 1u : 0u;
event.local_timestamp_ticks = local_timestamp_ticks;
if(event.nav_solve_event_count != 0u)
if((status_reg & (1u << GPS_TRCK_NAV_SOLVE_FLAG_POS)) != 0u)
{
event.pps_capture_valid = gps_pps_read_capture(trck_ip_no,
&event.pps_counter,
@@ -741,6 +789,9 @@ void trck_intr_handler(volatile gps_trck_t* const trck_ip,
if(status_reg & (1u << GPS_TRCK_STOP_UNLOCK_POS))
{
shadow->drop_event = event;
shadow->drop_event.status &=
~((1u << GPS_TRCK_NAV_BITS_RCV_POS) |
(1u << GPS_TRCK_NAV_SOLVE_FLAG_POS));
shadow->drop_pending = true;
}
if(status_reg & (1u << GPS_TRCK_NAV_BITS_RCV_POS))
@@ -750,12 +801,17 @@ void trck_intr_handler(volatile gps_trck_t* const trck_ip,
shadow->nav_bits_overwrite_count++;
}
shadow->nav_bits_event = event;
shadow->nav_bits_event.status &=
~((1u << GPS_TRCK_STOP_UNLOCK_POS) |
(1u << GPS_TRCK_NAV_SOLVE_FLAG_POS));
shadow->nav_bits_pending = true;
}
if(status_reg & (1u << GPS_TRCK_NAV_SOLVE_FLAG_POS))
{
shadow->nav_solve_event = event;
shadow->nav_solve_event_count++;
shadow->nav_solve_event.status &=
~((1u << GPS_TRCK_STOP_UNLOCK_POS) |
(1u << GPS_TRCK_NAV_BITS_RCV_POS));
shadow->nav_solve_pending = true;
}
if((status_reg & ((1u << GPS_TRCK_STOP_UNLOCK_POS) |
@@ -763,6 +819,10 @@ void trck_intr_handler(volatile gps_trck_t* const trck_ip,
(1u << GPS_TRCK_NAV_SOLVE_FLAG_POS))) == 0u)
{
shadow->misc_event = event;
shadow->misc_event.status &=
~((1u << GPS_TRCK_STOP_UNLOCK_POS) |
(1u << GPS_TRCK_NAV_BITS_RCV_POS) |
(1u << GPS_TRCK_NAV_SOLVE_FLAG_POS));
shadow->misc_pending = true;
}
}
@@ -912,10 +972,17 @@ static void gps_process_tracking_drop(const gps_isr_event_t* event)
unsigned char trck_ip_no = event->index;
unsigned int dropped_prn = event->prn;
if(gps_nav_epoch_assembly_remove_channel(
&gps_nav_epoch_assembly, trck_ip_no))
bool epoch_complete = gps_nav_epoch_assembly_remove_channel(
&gps_nav_epoch_assembly, trck_ip_no);
gps_nav_active_snapshot.channel_mask &= ~(1u << trck_ip_no);
if(epoch_complete)
{
gps_nav_solve_period_pending = true;
gps_nav_publish_active_snapshot(
gps_nav_epoch_assembly.received_mask, true);
}
else if(!gps_nav_epoch_assembly.active)
{
gps_nav_discard_active_snapshot(false);
}
if(gps_prn_valid(dropped_prn))
@@ -944,6 +1011,42 @@ static void gps_process_tracking_drop(const gps_isr_event_t* event)
trck_ip_no, dropped_prn, event->status, event->control);
}
static bool gps_how_candidate_consistent(
int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
double first_corrected_sample,
int second_week,
double second_tow_sec,
uint32_t second_raw_sample,
double second_corrected_sample)
{
return gps_time_anchors_consistent(
first_week, first_tow_sec,
first_raw_sample, first_corrected_sample,
second_week, second_tow_sec,
second_raw_sample, second_corrected_sample,
SAMPLES_PER_CODE * 1000.0,
GPS_HOW_CONTINUITY_MAX_GAP_SEC,
GPS_HOW_CONTINUITY_TOLERANCE_SEC);
}
static void gps_commit_how_anchor(volatile gps_ephem_ll_t* node,
int week,
double tow_sec,
const gps_isr_event_t* event,
bool week_valid)
{
node->gps_how_tow.week = week;
node->gps_how_tow.sec = tow_sec;
node->gps_how_abs_sample = event->abs_sample_raw;
node->gps_how_corrected_abs_sample = event->corrected_abs_sample;
node->gps_how_tick = event->tick;
node->gps_how_anchor_valid = true;
node->gps_how_week_valid = week_valid;
node->gps_pending_how_valid = false;
}
static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
{
unsigned char trck_ip_no = event->index;
@@ -980,39 +1083,89 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
unsigned char subframe_id = parse_subframe((unsigned int*)gps_subframe,
(gps_frame_t*)gps_frame_parsed,
&how_tow_sec);
if((subframe_id >= 1u) && (subframe_id <= 5u))
if((subframe_id >= 1u) && (subframe_id <= 5u) &&
(how_tow_sec < (unsigned int)GPS_TIME_WEEK_SECONDS))
{
node->gps_subframe_mask |= (1u << subframe_id);
int fallback_week = gps_frame_parsed->gps_ephems.tow.week;
if(subframe_id == 1u)
{
/* WN is ten bits in LNAV. Keeping it in the same modulo domain as
* the ephemeris is sufficient for satellite-time differences. */
fallback_week = (int)((gps_subframe[2] >> 20) & 0x3ffu);
fallback_week = gps_time_extend_lnav_week(
node->gps_how_anchor_valid,
node->gps_how_tow.week,
(gps_subframe[2] >> 20) & 0x3ffu);
}
/* A subframe-1 WN is authoritative in its native 10-bit domain. On
* other subframes, resolve the HOW seconds rollover from the previous
* anchor. This also upgrades a pre-SF1 temporary week immediately. */
/* Subframe 1 supplies the modulo-1024 week. Other subframes resolve
* HOW rollover from the previous monotonic anchor. */
int anchor_week = (subframe_id == 1u) ? fallback_week :
gps_time_resolve_how_week(node->gps_how_anchor_valid,
node->gps_how_tow.week,
node->gps_how_tow.sec,
fallback_week,
(double)how_tow_sec);
node->gps_how_tow.week = anchor_week;
node->gps_how_tow.sec = (double)how_tow_sec;
node->gps_how_abs_sample = event->abs_sample_raw;
node->gps_how_corrected_abs_sample = event->corrected_abs_sample;
node->gps_how_tick = event->tick;
node->gps_how_anchor_valid = true;
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
"HOW anchor: trk=%u prn=%u week=%d tow=%.3f abs=%u corrected=%.6f\r\n",
trck_ip_no, event->prn, anchor_week, (double)how_tow_sec,
event->abs_sample_raw, event->corrected_abs_sample);
/* Before the first SF1, fallback_week is only a placeholder. Validate
* TOW/sample continuity in that temporary domain, then atomically
* upgrade to the parity-checked LNAV week carried by SF1. */
int continuity_week =
((subframe_id == 1u) && !node->gps_how_week_valid &&
node->gps_how_anchor_valid) ?
node->gps_how_tow.week : anchor_week;
bool accept_anchor = !node->gps_how_anchor_valid ||
gps_how_candidate_consistent(
node->gps_how_tow.week, node->gps_how_tow.sec,
node->gps_how_abs_sample,
node->gps_how_corrected_abs_sample,
continuity_week, (double)how_tow_sec,
event->abs_sample_raw, event->corrected_abs_sample);
bool recovered_anchor = false;
if(!accept_anchor && node->gps_pending_how_valid)
{
accept_anchor = gps_how_candidate_consistent(
node->gps_pending_how_tow.week,
node->gps_pending_how_tow.sec,
node->gps_pending_how_abs_sample,
node->gps_pending_how_corrected_abs_sample,
anchor_week, (double)how_tow_sec,
event->abs_sample_raw, event->corrected_abs_sample);
recovered_anchor = accept_anchor;
}
if(accept_anchor)
{
gps_commit_how_anchor(node, anchor_week,
(double)how_tow_sec, event,
node->gps_how_week_valid ||
(subframe_id == 1u));
GPS_LOG(recovered_anchor ? DEBUG_WARNING : DEBUG_TRACE,
GPS_LOG_CAT_NAV,
"%s HOW anchor: trk=%u prn=%u week=%d tow=%.3f abs=%u corrected=%.6f\r\n",
recovered_anchor ? "recovered" : "accepted",
trck_ip_no, event->prn, anchor_week,
(double)how_tow_sec, event->abs_sample_raw,
event->corrected_abs_sample);
}
else
{
node->gps_pending_how_tow.week = anchor_week;
node->gps_pending_how_tow.sec = (double)how_tow_sec;
node->gps_pending_how_abs_sample = event->abs_sample_raw;
node->gps_pending_how_corrected_abs_sample =
event->corrected_abs_sample;
node->gps_pending_how_valid = true;
gps_runtime_diagnostics.how_anchor_rejected_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"HOW discontinuity rejected: trk=%u prn=%u current=%d/%.3f candidate=%d/%.3f rejects=%u\r\n",
trck_ip_no, event->prn,
node->gps_how_tow.week, node->gps_how_tow.sec,
anchor_week, (double)how_tow_sec,
gps_runtime_diagnostics.how_anchor_rejected_total);
}
}
else
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_TRK, "invalid subframe id: trk=%u subframe=%u\r\n", trck_ip_no, subframe_id);
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_TRK,
"invalid subframe/HOW: trk=%u subframe=%u tow=%u\r\n",
trck_ip_no, subframe_id, how_tow_sec);
}
gps_mark_node_fresh(node, event->status, event->tick);
@@ -1032,32 +1185,53 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
volatile gps_ephem_ll_t* node = get_element_by_ip_number(&tracking_node, trck_ip_no);
if(node != NULL)
{
unsigned int nav_solve_event_count = event->nav_solve_event_count;
unsigned int captured_epoch_count = 1u;
if(event->pps_capture_valid)
{
uint32_t previous_capture_sequence = node->gps_last_pps_capture_sequence;
if(previous_capture_sequence != 0u)
if(node->gps_last_nav_solve_pps_counter != 0u)
{
nav_solve_event_count = event->pps_capture_sequence - previous_capture_sequence;
if(nav_solve_event_count == 0u)
uint32_t sequence_delta = 0u;
bool sequence_consistent =
gps_nav_capture_sequence_consistent(
node->gps_last_nav_solve_pps_counter,
previous_capture_sequence,
event->pps_counter,
event->pps_capture_sequence,
GPS_NAV_CAPTURE_PERIOD_TICKS,
&sequence_delta);
if(sequence_consistent)
{
captured_epoch_count = sequence_delta;
if(sequence_delta > 1u)
{
gps_runtime_diagnostics.nav_epoch_coalesced_total +=
sequence_delta - 1u;
}
}
else
{
gps_runtime_diagnostics.pps_sequence_discontinuity_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"PPS capture sequence discontinuity: trk=%u prn=%u previous=%u current=%u total=%u\r\n",
trck_ip_no, event->prn,
previous_capture_sequence,
event->pps_capture_sequence,
gps_runtime_diagnostics.pps_sequence_discontinuity_total);
/* Rebase the newest-only transport metadata, but never let
* an inconsistent counter/sequence pair enter an epoch. */
node->gps_last_nav_solve_pps_counter =
event->pps_counter;
node->gps_last_pps_capture_sequence =
event->pps_capture_sequence;
return;
}
if(nav_solve_event_count > event->nav_solve_event_count)
{
gps_runtime_diagnostics.nav_epoch_coalesced_total +=
nav_solve_event_count - event->nav_solve_event_count;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"recovered %u coalesced nav-solve epochs: trk=%u prn=%u\r\n",
nav_solve_event_count, trck_ip_no, event->prn);
}
}
}
else if(node->gps_last_pps_capture_sequence != 0u)
{
/* Keep the sequence baseline moving on the rare APB capture race. A
* later valid capture still recovers any epochs coalesced meanwhile. */
node->gps_last_pps_capture_sequence += nav_solve_event_count;
/* Capture sequencing is transport metadata. Rebase it even if the
* decoded HOW below is unavailable or rejected. */
node->gps_last_nav_solve_pps_counter = event->pps_counter;
node->gps_last_pps_capture_sequence =
event->pps_capture_sequence;
}
if(!node->gps_how_anchor_valid)
@@ -1091,56 +1265,92 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
gps_frame_parsed->gps_ephems.tow.week = tow_week;
gps_frame_parsed->gps_ephems.tow.sec = tow_sec;
gps_frame_parsed->gps_ephems.abs_sample = event->corrected_abs_sample;
node->gps_nav_solve_sequence += nav_solve_event_count;
/* A one-entry hardware mailbox carries only the newest measurement.
* Missed capture pulses are diagnostic counts, not synthetic samples. */
node->gps_nav_solve_sequence++;
node->gps_last_nav_solve_tow.week = tow_week;
node->gps_last_nav_solve_tow.sec = tow_sec;
node->gps_last_nav_solve_abs_sample = event->abs_sample_raw;
node->gps_last_nav_solve_corrected_abs_sample = event->corrected_abs_sample;
node->gps_last_nav_solve_local_ticks = event->local_timestamp_ticks;
if(event->pps_capture_valid)
{
node->gps_last_nav_solve_pps_counter = event->pps_counter;
node->gps_last_pps_capture_sequence = event->pps_capture_sequence;
}
gps_mark_node_fresh(node, event->status, event->tick);
uint64_t epoch_id;
if(!gps_nav_epoch_id(tow_week, tow_sec,
GPS_NAV_SOLVE_PERIOD_MS, &epoch_id))
if(!event->pps_capture_valid)
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"nav solve has invalid epoch id: trk=%u week=%d tow=%.6f\r\n",
trck_ip_no, tow_week, tow_sec);
"nav solve omitted after incoherent PPS capture read: trk=%u prn=%u\r\n",
trck_ip_no, event->prn);
return;
}
uint64_t previous_epoch = gps_nav_epoch_assembly.epoch_id;
uint32_t previous_expected = gps_nav_epoch_assembly.expected_mask;
uint32_t previous_received = gps_nav_epoch_assembly.received_mask;
gps_nav_epoch_event_result_t assembly_result =
gps_nav_epoch_assembly_push(
&gps_nav_epoch_assembly, epoch_id, trck_ip_no,
gps_nav_ready_channel_mask());
&gps_nav_epoch_assembly,
event->pps_counter,
event->local_timestamp_ticks,
trck_ip_no,
gps_nav_ready_channel_mask(),
GPS_NAV_CAPTURE_PERIOD_TICKS,
GPS_NAV_CAPTURE_WINDOW_TICKS,
GPS_NAV_HOLDOFF_LOCAL_TICKS);
if(assembly_result == GPS_NAV_EPOCH_EVENT_RESTARTED)
{
gps_runtime_diagnostics.nav_epoch_incomplete_total++;
/* The first interrupt from a newer period proves the old expected
* set will never complete. Drop it rather than solving a partial
* satellite set. */
gps_nav_discard_active_snapshot(false);
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"NAV capture epoch advanced: old_expected=0x%x old_received=0x%x new_counter=%lu first_trk=%u\r\n",
previous_expected, previous_received,
(unsigned long)event->pps_counter, trck_ip_no);
}
if((assembly_result == GPS_NAV_EPOCH_EVENT_ACCEPTED) ||
(assembly_result == GPS_NAV_EPOCH_EVENT_RESTARTED) ||
(assembly_result == GPS_NAV_EPOCH_EVENT_COMPLETE))
{
uint32_t channel_mask = 1u << trck_ip_no;
gps_nav_active_snapshot.channel_mask |= channel_mask;
gps_nav_active_snapshot.prn[trck_ip_no] = event->prn;
gps_nav_active_snapshot.ephemeris[trck_ip_no] =
gps_frame_parsed->gps_ephems;
gps_nav_active_snapshot.tow[trck_ip_no].week = tow_week;
gps_nav_active_snapshot.tow[trck_ip_no].sec = tow_sec;
gps_nav_active_snapshot.raw_sample[trck_ip_no] =
event->abs_sample_raw;
gps_nav_active_snapshot.corrected_abs_sample[trck_ip_no] =
event->corrected_abs_sample;
gps_nav_active_snapshot.pps_counter[trck_ip_no] =
event->pps_counter;
gps_nav_active_snapshot.pps_capture_sequence[trck_ip_no] =
event->pps_capture_sequence;
gps_nav_active_snapshot.local_timestamp_ticks[trck_ip_no] =
event->local_timestamp_ticks;
}
if(assembly_result == GPS_NAV_EPOCH_EVENT_COMPLETE)
{
gps_nav_solve_period_pending = true;
gps_nav_publish_active_snapshot(
gps_nav_epoch_assembly.received_mask, true);
}
else if(assembly_result == GPS_NAV_EPOCH_EVENT_STALE)
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"stale nav epoch interrupt ignored: trk=%u epoch=%lu active=%lu\r\n",
trck_ip_no, (unsigned long)epoch_id,
(unsigned long)gps_nav_epoch_assembly.epoch_id);
"stale NAV capture ignored: trk=%u counter=%lu active_min=%lu\r\n",
trck_ip_no, (unsigned long)event->pps_counter,
(unsigned long)gps_nav_epoch_assembly.minimum_capture_counter);
}
else if(assembly_result == GPS_NAV_EPOCH_EVENT_RESTARTED)
else if(assembly_result == GPS_NAV_EPOCH_EVENT_OUTLIER)
{
/* The previous snapshot is no longer the active assembly. Do not
* let an already-pending foreground hand-off consume mixed nodes. */
gps_nav_solve_period_pending = false;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"NAV epoch restarted by newer interrupt: old=%lu expected=0x%x received=0x%x new=%lu first_trk=%u\r\n",
(unsigned long)previous_epoch,
previous_expected, previous_received,
(unsigned long)epoch_id, trck_ip_no);
"NAV capture outlier ignored: trk=%u counter=%lu active=[%lu,%lu]\r\n",
trck_ip_no, (unsigned long)event->pps_counter,
(unsigned long)gps_nav_epoch_assembly.minimum_capture_counter,
(unsigned long)gps_nav_epoch_assembly.maximum_capture_counter);
}
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_TRK,
"nav solve: trk=%u abs_raw=%u rem=%u abs=%.6f timer=%u count=%u dt=%.6f tow=%f seq=%u\r\n",
@@ -1149,7 +1359,7 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
event->rem_code_phase,
gps_frame_parsed->gps_ephems.abs_sample,
event->nav_solve_timer,
nav_solve_event_count,
captured_epoch_count,
tow_delta_sec,
gps_frame_parsed->gps_ephems.tow.sec,
node->gps_nav_solve_sequence);
@@ -1243,7 +1453,6 @@ static void gps_process_shadow_tracking_events(void)
}
if(shadow.nav_solve_pending)
{
shadow.nav_solve_event.nav_solve_event_count = shadow.nav_solve_event_count;
gps_process_tracking_event(&shadow.nav_solve_event);
}
if(shadow.misc_pending)
+135 -165
View File
@@ -18,6 +18,17 @@
_Static_assert(GPS_TRCK_IP_COUNT <= GPS_NAV_EPOCH_MAX_CANDIDATES,
"GPS nav epoch selector capacity is smaller than the tracking channel count");
static unsigned int gps_nav_mask_count(uint32_t mask)
{
unsigned int count = 0u;
while(mask != 0u)
{
count += mask & 1u;
mask >>= 1u;
}
return count;
}
volatile gps_acq_t* const acq0_ip = (volatile gps_acq_t*)ACQ0_IP_BASE_ADDR;
volatile gps_acq_t* acq_ips[GPS_ACQ_IP_COUNT];
unsigned int acq_ips_intr_lines[GPS_ACQ_IP_COUNT];
@@ -127,7 +138,6 @@ static bool gps_snapshot_has_svid(const gps_ephemeris_t snapshot[GPS_TRCK_IP_COU
return false;
}
static uint32_t gps_nav_epoch_excluded_total;
static uint32_t gps_nav_epoch_reject_total;
static int32_t gps_sign_extend(uint32_t value, unsigned int bits)
@@ -507,6 +517,21 @@ static bool gps_node_has_committed_ephemeris(volatile gps_ephem_ll_t* node)
node->data_p->ephemeris_valid;
}
static bool gps_ephemeris_ready_for_position(
const gps_ephemeris_t* eph,
unsigned int expected_svid)
{
return (eph != NULL) &&
eph->ephemeris_valid &&
(eph->SVID == expected_svid) &&
(eph->update == 0u) &&
(eph->svhlth == 0u) &&
(eph->sva < 15u) &&
(eph->A > 0.0) &&
(eph->ecc >= 0.0) &&
(eph->ecc < 1.0);
}
static bool gps_node_ready_for_position(volatile gps_ephem_ll_t* node)
{
if((node == NULL) || (node->data_p == NULL) || (node->tracking_ip_no >= GPS_TRCK_IP_COUNT))
@@ -530,14 +555,9 @@ static bool gps_node_ready_for_position(volatile gps_ephem_ll_t* node)
return false;
}
gps_ephemeris_t* eph = (gps_ephemeris_t*)node->data_p;
return (eph->SVID == (unsigned char)trck_ips[node->tracking_ip_no]->PRN_NUMBER) &&
(eph->update == 0u) &&
(eph->svhlth == 0u) &&
(eph->sva < 15u) &&
(eph->A > 0.0) &&
(eph->ecc >= 0.0) &&
(eph->ecc < 1.0);
return gps_ephemeris_ready_for_position(
(const gps_ephemeris_t*)node->data_p,
trck_ips[node->tracking_ip_no]->PRN_NUMBER);
}
static bool gps_channel_nav_solve_enabled(unsigned char trck_ip_no)
@@ -570,7 +590,6 @@ static void gps_enable_nav_solve_for_snapshot_locked(const unsigned char snapsho
if(!gps_channel_nav_solve_enabled(ip_no))
{
gps_set_channel_nav_solve_mode(ip_no);
node->gps_nav_solve_consumed_sequence = node->gps_nav_solve_sequence;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_TRK,
"tracking channel %u enabled nav solve; nav bits remain enabled\r\n",
ip_no);
@@ -599,9 +618,7 @@ static unsigned int gps_collect_tracked_channels_locked(
static unsigned int gps_collect_nav_snapshot_locked(gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT],
bool require_nav_solve_mode,
bool require_nav_sequence)
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT])
{
unsigned int count = 0;
@@ -611,12 +628,7 @@ static unsigned int gps_collect_nav_snapshot_locked(gps_ephemeris_t snapshot[GPS
{
unsigned char ip_no = iterator->tracking_ip_no;
gps_ephemeris_t* eph = (gps_ephemeris_t*)iterator->data_p;
if(gps_node_ready_for_position(iterator) &&
(!require_nav_solve_mode || gps_channel_nav_solve_enabled(ip_no)) &&
(!require_nav_sequence ||
((eph != NULL) &&
(iterator->gps_nav_solve_sequence !=
iterator->gps_nav_solve_consumed_sequence))))
if(gps_node_ready_for_position(iterator) && (eph != NULL))
{
if(gps_snapshot_has_svid(snapshot, count, eph->SVID))
{
@@ -626,19 +638,10 @@ static unsigned int gps_collect_nav_snapshot_locked(gps_ephemeris_t snapshot[GPS
continue;
}
snapshot[count] = *eph;
if(require_nav_sequence)
if(snapshot_raw_samples != NULL)
{
/* Nav bits may update the shared ephemeris between nav-solve
* epochs. Position measurements must use the timing latched by
* the nav-solve event, not the shared nav-bit fields. */
snapshot[count].tow = iterator->gps_last_nav_solve_tow;
snapshot[count].abs_sample =
iterator->gps_last_nav_solve_corrected_abs_sample;
if(snapshot_raw_samples != NULL)
{
snapshot_raw_samples[count] =
iterator->gps_last_nav_solve_abs_sample;
}
snapshot_raw_samples[count] =
iterator->gps_last_nav_solve_abs_sample;
}
snapshot_ips[count] = ip_no;
count++;
@@ -651,21 +654,65 @@ static unsigned int gps_collect_nav_snapshot_locked(gps_ephemeris_t snapshot[GPS
return count;
}
static unsigned int gps_collect_completed_epoch_locked(
const gps_nav_epoch_snapshot_t* epoch,
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],
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])
{
if(epoch == NULL)
{
return 0u;
}
unsigned int count = 0u;
for(unsigned char ip_no = 0u ; ip_no < GPS_TRCK_IP_COUNT ; ip_no++)
{
if((epoch->channel_mask & (1u << ip_no)) == 0u)
{
continue;
}
const gps_ephemeris_t* eph = &epoch->ephemeris[ip_no];
if(!gps_ephemeris_ready_for_position(
eph, epoch->prn[ip_no]) ||
gps_snapshot_has_svid(snapshot, count, eph->SVID))
{
continue;
}
snapshot[count] = *eph;
snapshot[count].tow = epoch->tow[ip_no];
snapshot[count].abs_sample =
epoch->corrected_abs_sample[ip_no];
snapshot_ips[count] = ip_no;
snapshot_pps_counters[count] =
epoch->pps_counter[ip_no];
snapshot_pps_sequences[count] =
epoch->pps_capture_sequence[ip_no];
snapshot_local_timestamps[count] =
epoch->local_timestamp_ticks[ip_no];
snapshot_raw_samples[count] =
epoch->raw_sample[ip_no];
count++;
}
return count;
}
static unsigned int gps_select_coherent_nav_snapshot(
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
uint32_t snapshot_sequences[GPS_TRCK_IP_COUNT],
uint64_t snapshot_pps_counters[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],
unsigned int candidate_count,
uint32_t* selected_mask_out)
unsigned int candidate_count)
{
if((candidate_count == 0u) ||
(candidate_count > GPS_NAV_EPOCH_MAX_CANDIDATES))
{
*selected_mask_out = 0u;
return 0u;
}
@@ -678,12 +725,12 @@ static unsigned int gps_select_coherent_nav_snapshot(
}
gps_nav_epoch_selection_t selection;
if(!gps_nav_epoch_select(candidates, candidate_count,
GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_SAMPLES,
GPS_NAV_TOW_CLUSTER_TOLERANCE_SEC,
&selection))
if(!gps_nav_epoch_validate_complete(
candidates, candidate_count,
GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_SAMPLES,
GPS_NAV_TOW_CLUSTER_TOLERANCE_SEC,
&selection))
{
*selected_mask_out = 0u;
return 0u;
}
@@ -702,7 +749,6 @@ static unsigned int gps_select_coherent_nav_snapshot(
{
snapshot[destination] = snapshot[source];
snapshot_ips[destination] = snapshot_ips[source];
snapshot_sequences[destination] = snapshot_sequences[source];
snapshot_pps_counters[destination] =
snapshot_pps_counters[source];
snapshot_pps_sequences[destination] =
@@ -720,28 +766,9 @@ static unsigned int gps_select_coherent_nav_snapshot(
destination++;
}
*selected_mask_out = selection.selected_mask;
return destination;
}
static void gps_capture_nav_metadata_locked(const unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
uint32_t snapshot_sequences[GPS_TRCK_IP_COUNT],
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT],
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT],
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT],
unsigned int satellite_count)
{
for(unsigned int i = 0 ; i < satellite_count ; i++)
{
volatile gps_ephem_ll_t* node = get_element_by_ip_number(&tracking_node, snapshot_ips[i]);
snapshot_sequences[i] = (node != NULL) ? node->gps_nav_solve_sequence : 0u;
snapshot_pps_counters[i] = (node != NULL) ? node->gps_last_nav_solve_pps_counter : 0u;
snapshot_pps_sequences[i] = (node != NULL) ? node->gps_last_pps_capture_sequence : 0u;
snapshot_local_timestamps[i] =
(node != NULL) ? node->gps_last_nav_solve_local_ticks : 0u;
}
}
static uint64_t gps_nav_median_local_timestamp(
const uint64_t timestamps[GPS_TRCK_IP_COUNT], unsigned int count)
{
@@ -765,48 +792,6 @@ static uint64_t gps_nav_median_local_timestamp(
return (valid_count != 0u) ? sorted[valid_count / 2u] : 0u;
}
static void gps_consume_nav_sequences_locked(const unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
const uint32_t snapshot_sequences[GPS_TRCK_IP_COUNT],
unsigned int satellite_count)
{
for(unsigned int i = 0 ; i < satellite_count ; i++)
{
volatile gps_ephem_ll_t* node = get_element_by_ip_number(&tracking_node, snapshot_ips[i]);
if(node != NULL)
{
/* Consume only the epoch copied into this solution. A newer sequence,
* if present, remains different from consumed and is solved next. */
node->gps_nav_solve_consumed_sequence = snapshot_sequences[i];
}
}
}
static void gps_consume_rejected_nav_sequences_locked(
const unsigned char candidate_ips[GPS_TRCK_IP_COUNT],
const uint32_t candidate_sequences[GPS_TRCK_IP_COUNT],
unsigned int candidate_count,
uint32_t selected_mask)
{
for(unsigned int i = 0u ; i < candidate_count ; i++)
{
if((selected_mask & (1u << i)) != 0u)
{
continue;
}
volatile gps_ephem_ll_t* node =
get_element_by_ip_number(&tracking_node, candidate_ips[i]);
if(node != NULL)
{
/*
* Consume exactly the rejected snapshot. If an interrupt has
* already produced a newer sequence, it remains different from
* this consumed value and is preserved for the next epoch.
*/
node->gps_nav_solve_consumed_sequence = candidate_sequences[i];
}
}
}
static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
const uint64_t pps_counters[GPS_TRCK_IP_COUNT],
const uint32_t pps_capture_sequences[GPS_TRCK_IP_COUNT],
@@ -814,7 +799,7 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
gps_nav_solution_t* solution_out)
{
if((ephemeris == NULL) || (solution_out == NULL) ||
(satellite_count < MIN_NEEDED_SATELLITE))
(satellite_count < GPS_NAV_MIN_WLS_SATELLITES))
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "nav solve skipped, usable satellites=%u\r\n", satellite_count);
return false;
@@ -1088,14 +1073,13 @@ bool calculate_position(gps_nav_solution_t* solution_out,
bool result_ready = false;
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT];
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT];
uint32_t snapshot_sequences[GPS_TRCK_IP_COUNT];
uint64_t snapshot_pps_counters[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];
gps_nav_epoch_snapshot_t completed_epoch;
unsigned int satellite_count = 0;
unsigned int candidate_count = 0;
uint32_t selected_candidate_mask = 0u;
bool run_nav = false;
bool initial_run = false;
unsigned int ready_count;
@@ -1110,17 +1094,18 @@ bool calculate_position(gps_nav_solution_t* solution_out,
#ifdef EKF_ENABLE
(void)g_ekf;
#endif
bool nav_period_ready = gps_take_nav_solve_period_ready();
bool nav_period_ready =
gps_take_nav_epoch_snapshot(&completed_epoch);
uint64_t nav_period_start_cycles = nav_period_ready ? gps_read_cycle_counter() : 0u;
uint64_t irq_state = bsp_irq_save();
ready_count = gps_collect_nav_snapshot_locked(
snapshot, snapshot_ips, NULL, false, false);
snapshot, snapshot_ips, NULL);
tracked_count = gps_collect_tracked_channels_locked(
GPS_DEBUG_ENABLED(DEBUG_CHANNELS) ? tracked_prns : NULL);
if(ready_count < MIN_NEEDED_SATELLITE)
if(ready_count < GPS_NAV_MIN_WLS_SATELLITES)
{
gps_initial_position_valid = false;
}
@@ -1131,13 +1116,23 @@ bool calculate_position(gps_nav_solution_t* solution_out,
gps_enable_nav_solve_for_snapshot_locked(snapshot_ips, ready_count);
if(nav_period_ready)
{
candidate_count = gps_collect_nav_snapshot_locked(
snapshot, snapshot_ips, snapshot_raw_samples, true, true);
gps_capture_nav_metadata_locked(snapshot_ips, snapshot_sequences,
snapshot_pps_counters,
snapshot_pps_sequences,
snapshot_local_timestamps,
candidate_count);
candidate_count = gps_collect_completed_epoch_locked(
&completed_epoch,
snapshot, snapshot_ips, snapshot_pps_counters,
snapshot_pps_sequences,
snapshot_local_timestamps, snapshot_raw_samples);
unsigned int completed_channel_count =
gps_nav_mask_count(completed_epoch.channel_mask);
if(candidate_count != completed_channel_count)
{
gps_nav_epoch_reject_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"immutable NAV snapshot rejected: captured=%u valid=%u mask=0x%x rejects=%u\r\n",
completed_channel_count, candidate_count,
completed_epoch.channel_mask,
gps_nav_epoch_reject_total);
candidate_count = 0u;
}
initial_run = !gps_initial_position_valid;
}
}
@@ -1146,52 +1141,22 @@ bool calculate_position(gps_nav_solution_t* solution_out,
if(nav_period_ready && (candidate_count != 0u))
{
/*
* Preserve the pre-compaction identity of rejected candidates so their
* exact sequence can be retired without touching a newer interrupt.
*/
unsigned char candidate_ips[GPS_TRCK_IP_COUNT];
uint32_t candidate_sequences[GPS_TRCK_IP_COUNT];
memcpy(candidate_ips, snapshot_ips,
candidate_count * sizeof(candidate_ips[0]));
memcpy(candidate_sequences, snapshot_sequences,
candidate_count * sizeof(candidate_sequences[0]));
satellite_count = gps_select_coherent_nav_snapshot(
snapshot, snapshot_ips, snapshot_sequences,
snapshot_pps_counters, snapshot_pps_sequences,
snapshot, snapshot_ips, snapshot_pps_counters,
snapshot_pps_sequences,
snapshot_local_timestamps, snapshot_raw_samples,
candidate_count, &selected_candidate_mask);
candidate_count);
coherent_nav_count = satellite_count;
run_nav = satellite_count >= MIN_NEEDED_SATELLITE;
run_nav = satellite_count >= GPS_NAV_MIN_WLS_SATELLITES;
if(run_nav && (satellite_count < candidate_count))
if(!run_nav)
{
unsigned int excluded_count = candidate_count - satellite_count;
gps_nav_epoch_excluded_total += excluded_count;
uint64_t consume_irq_state = bsp_irq_save();
gps_consume_rejected_nav_sequences_locked(
candidate_ips, candidate_sequences, candidate_count,
selected_candidate_mask);
bsp_irq_restore(consume_irq_state);
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"NAV coherent subset selected: candidates=%u selected=%u excluded=%u excluded_total=%u\r\n",
candidate_count, satellite_count, excluded_count,
gps_nav_epoch_excluded_total);
}
else if(!run_nav)
{
uint64_t consume_irq_state = bsp_irq_save();
gps_consume_rejected_nav_sequences_locked(
candidate_ips, candidate_sequences, candidate_count, 0u);
bsp_irq_restore(consume_irq_state);
if(candidate_count >= MIN_NEEDED_SATELLITE)
if(candidate_count >= GPS_NAV_MIN_WLS_SATELLITES)
{
gps_nav_epoch_reject_total++;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"NAV epoch has no coherent four-satellite subset: candidates=%u best=%u rejects=%u\r\n",
candidate_count, satellite_count,
gps_nav_epoch_reject_total);
"complete NAV epoch rejected as incoherent: candidates=%u rejects=%u\r\n",
candidate_count, gps_nav_epoch_reject_total);
}
}
}
@@ -1202,8 +1167,15 @@ bool calculate_position(gps_nav_solution_t* solution_out,
(coherent_nav_count != last_nav_status_coherent_count) ||
((now - last_nav_status_log_tick) >= GPS_NAV_STATUS_LOG_PERIOD_TICKS))
{
GPS_LOG(DEBUG_DEBUG, GPS_LOG_CAT_NAV, "tracked channels=%u ready channels=%u coherent_nav=%u\r\n",
tracked_count, ready_count, coherent_nav_count);
gps_runtime_diagnostics_t status_diagnostics;
gps_get_runtime_diagnostics(&status_diagnostics);
GPS_LOG(DEBUG_DEBUG, GPS_LOG_CAT_NAV,
"tracked channels=%u ready channels=%u coherent_nav=%u incomplete=%u snapshot_overwrite=%u pps_discontinuity=%u how_reject=%u\r\n",
tracked_count, ready_count, coherent_nav_count,
status_diagnostics.nav_epoch_incomplete_total,
status_diagnostics.nav_epoch_snapshot_overwrite_total,
status_diagnostics.pps_sequence_discontinuity_total,
status_diagnostics.how_anchor_rejected_total);
last_nav_status_tracked_count = tracked_count;
last_nav_status_ready_count = ready_count;
last_nav_status_coherent_count = coherent_nav_count;
@@ -1241,13 +1213,8 @@ bool calculate_position(gps_nav_solution_t* solution_out,
result_ready = true;
}
}
/* A completed hardware epoch is attempted once. Retrying the same
* measurements after a WLS failure would couple solution cadence to an
* unrelated foreground wake-up. */
uint64_t consume_irq_state = bsp_irq_save();
gps_consume_nav_sequences_locked(
snapshot_ips, snapshot_sequences, satellite_count);
bsp_irq_restore(consume_irq_state);
/* The completed snapshot is handed out exactly once. Retrying after a
* WLS failure would couple solution cadence to a foreground wake-up. */
uint64_t foreground_cycles = gps_read_cycle_counter() - nav_period_start_cycles;
uint64_t period_budget_cycles = (GPS_PROFILE_CPU_HZ * GPS_NAV_SOLVE_PERIOD_MS) / 1000u;
bool period_overrun = foreground_cycles >= period_budget_cycles;
@@ -1260,7 +1227,8 @@ bool calculate_position(gps_nav_solution_t* solution_out,
"fde=%u valid=%u budget_cycles=%lu overrun=%u math_corr=%.6lf "
"math_clock_corr=%.6lf "
"isr_overflow=%u plic_repeat=%u drain_limit=%u coalesced=%u lost=%u "
"epoch_excluded=%u epoch_rejected=%u\r\n",
"pps_discontinuity=%u epoch_incomplete=%u snapshot_overwrite=%u how_reject=%u "
"epoch_rejected=%u\r\n",
GPS_NAV_SOLVE_PERIOD_MS,
(unsigned long)GPS_PROFILE_CPU_HZ,
(unsigned long)foreground_cycles,
@@ -1283,7 +1251,10 @@ bool calculate_position(gps_nav_solution_t* solution_out,
runtime_diagnostics.plic_drain_limit_total,
runtime_diagnostics.nav_epoch_coalesced_total,
runtime_diagnostics.satellite_lost_total,
gps_nav_epoch_excluded_total,
runtime_diagnostics.pps_sequence_discontinuity_total,
runtime_diagnostics.nav_epoch_incomplete_total,
runtime_diagnostics.nav_epoch_snapshot_overwrite_total,
runtime_diagnostics.how_anchor_rejected_total,
gps_nav_epoch_reject_total);
(void)foreground_cycles;
(void)nav_software_cycles;
@@ -1426,7 +1397,6 @@ void gps_startup_initialization(unsigned int nsp_tick)
gps_pps_init(nsp_tick);
gps_ionoutc_valid = false;
gps_last_ionoutc = (ionoutc_t){0};
gps_nav_epoch_excluded_total = 0u;
gps_nav_epoch_reject_total = 0u;
for(unsigned int i = 0 ; i < GPS_ACQ_IP_COUNT ; i++)
{
+119 -30
View File
@@ -5,7 +5,6 @@
#define GPS_WEEK_SECONDS 604800.0
#define GPS_HALF_WEEK_SECONDS 302400.0
#define GPS_WEEK_MILLISECONDS 604800000ULL
static double gps_nav_epoch_tow_delta(
const gps_nav_epoch_key_t* first,
@@ -180,30 +179,59 @@ bool gps_nav_epoch_select(
return selection->selected_count != 0u;
}
bool gps_nav_epoch_id(
int week,
double tow_sec,
unsigned int period_ms,
uint64_t* epoch_id)
bool gps_nav_epoch_validate_complete(
const gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES],
unsigned int candidate_count,
uint32_t sample_window,
double tow_tolerance_sec,
gps_nav_epoch_selection_t* selection)
{
if((epoch_id == NULL) || (week < 0) || (period_ms == 0u) ||
((GPS_WEEK_MILLISECONDS % period_ms) != 0u) ||
!isfinite(tow_sec) || (tow_sec < 0.0) ||
(tow_sec >= GPS_WEEK_SECONDS))
if(!gps_nav_epoch_select(candidates, candidate_count, sample_window,
tow_tolerance_sec, selection))
{
return false;
}
uint32_t complete_mask = (1u << candidate_count) - 1u;
return (selection->selected_count == candidate_count) &&
(selection->selected_mask == complete_mask);
}
bool gps_nav_capture_sequence_consistent(
uint64_t previous_counter,
uint32_t previous_sequence,
uint64_t current_counter,
uint32_t current_sequence,
uint64_t capture_period_ticks,
uint32_t* sequence_gap)
{
if(sequence_gap != NULL)
{
*sequence_gap = 0u;
}
if((previous_counter == 0u) || (current_counter <= previous_counter) ||
(capture_period_ticks == 0u))
{
return false;
}
uint64_t epochs_per_week = GPS_WEEK_MILLISECONDS / period_ms;
double epoch_in_week = (tow_sec * 1000.0) / (double)period_ms;
uint64_t rounded_epoch = (uint64_t)llround(epoch_in_week);
if(rounded_epoch == epochs_per_week)
uint32_t sequence_delta = current_sequence - previous_sequence;
if(sequence_delta == 0u)
{
week++;
rounded_epoch = 0u;
return false;
}
uint64_t counter_delta = current_counter - previous_counter;
uint64_t expected_delta =
(counter_delta + (capture_period_ticks / 2u)) /
capture_period_ticks;
if((expected_delta == 0u) || (expected_delta > UINT32_MAX) ||
(sequence_delta != (uint32_t)expected_delta))
{
return false;
}
if(sequence_gap != NULL)
{
*sequence_gap = sequence_delta;
}
*epoch_id = ((uint64_t)(unsigned int)week * epochs_per_week) +
rounded_epoch;
return true;
}
@@ -213,7 +241,10 @@ void gps_nav_epoch_assembly_reset(gps_nav_epoch_assembly_t* assembly)
{
return;
}
assembly->epoch_id = 0u;
assembly->first_capture_counter = 0u;
assembly->minimum_capture_counter = 0u;
assembly->maximum_capture_counter = 0u;
assembly->deadline_local_ticks = 0u;
assembly->expected_mask = 0u;
assembly->received_mask = 0u;
assembly->active = false;
@@ -230,12 +261,21 @@ bool gps_nav_epoch_assembly_complete(
gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
gps_nav_epoch_assembly_t* assembly,
uint64_t epoch_id,
uint64_t capture_counter,
uint64_t local_timestamp_ticks,
unsigned int channel,
uint32_t ready_mask)
uint32_t ready_mask,
uint64_t capture_period_ticks,
uint64_t sync_window_ticks,
uint64_t holdoff_local_ticks)
{
if((assembly == NULL) ||
(channel >= GPS_NAV_EPOCH_MAX_CANDIDATES))
(channel >= GPS_NAV_EPOCH_MAX_CANDIDATES) ||
(capture_counter == 0u) ||
(capture_period_ticks == 0u) ||
(sync_window_ticks == 0u) ||
(sync_window_ticks >= (capture_period_ticks / 2u)) ||
(holdoff_local_ticks == 0u))
{
return GPS_NAV_EPOCH_EVENT_INVALID;
}
@@ -252,20 +292,40 @@ gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
bool restarted = false;
if(assembly->active)
{
if(epoch_id < assembly->epoch_id)
uint64_t minimum = assembly->minimum_capture_counter;
uint64_t maximum = assembly->maximum_capture_counter;
uint64_t next_minimum =
(capture_counter < minimum) ? capture_counter : minimum;
uint64_t next_maximum =
(capture_counter > maximum) ? capture_counter : maximum;
if((next_maximum - next_minimum) > sync_window_ticks)
{
return GPS_NAV_EPOCH_EVENT_STALE;
}
if(epoch_id > assembly->epoch_id)
{
gps_nav_epoch_assembly_reset(assembly);
restarted = true;
uint64_t epoch_separation = capture_period_ticks / 2u;
if((capture_counter > maximum) &&
((capture_counter - maximum) >= epoch_separation))
{
gps_nav_epoch_assembly_reset(assembly);
restarted = true;
}
else if((capture_counter < minimum) &&
((minimum - capture_counter) >= epoch_separation))
{
return GPS_NAV_EPOCH_EVENT_STALE;
}
else
{
return GPS_NAV_EPOCH_EVENT_OUTLIER;
}
}
}
if(!assembly->active)
{
assembly->epoch_id = epoch_id;
assembly->first_capture_counter = capture_counter;
assembly->minimum_capture_counter = capture_counter;
assembly->maximum_capture_counter = capture_counter;
assembly->deadline_local_ticks =
local_timestamp_ticks + holdoff_local_ticks;
assembly->expected_mask = ready_mask;
assembly->received_mask = 0u;
assembly->active = true;
@@ -280,6 +340,14 @@ gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
return GPS_NAV_EPOCH_EVENT_DUPLICATE;
}
if(capture_counter < assembly->minimum_capture_counter)
{
assembly->minimum_capture_counter = capture_counter;
}
if(capture_counter > assembly->maximum_capture_counter)
{
assembly->maximum_capture_counter = capture_counter;
}
assembly->received_mask |= channel_mask;
if(gps_nav_epoch_assembly_complete(assembly))
{
@@ -310,3 +378,24 @@ bool gps_nav_epoch_assembly_remove_channel(
}
return gps_nav_epoch_assembly_complete(assembly);
}
bool gps_nav_epoch_assembly_usable(
const gps_nav_epoch_assembly_t* assembly,
unsigned int minimum_channels)
{
if((assembly == NULL) || !assembly->active ||
(minimum_channels == 0u))
{
return false;
}
return gps_nav_epoch_mask_count(assembly->received_mask) >=
minimum_channels;
}
bool gps_nav_epoch_assembly_expired(
const gps_nav_epoch_assembly_t* assembly,
uint64_t now_local_ticks)
{
return (assembly != NULL) && assembly->active &&
(now_local_ticks >= assembly->deadline_local_ticks);
}
@@ -42,6 +42,28 @@ int gps_time_resolve_how_week(bool previous_valid,
return week;
}
int gps_time_extend_lnav_week(bool previous_valid,
int previous_week,
unsigned int lnav_week)
{
int week = (int)(lnav_week & 0x3ffu);
if(!previous_valid || (previous_week < 0))
{
return week;
}
int candidate = (previous_week & ~0x3ff) | week;
if((candidate - previous_week) > 512)
{
candidate -= 1024;
}
else if((previous_week - candidate) > 512)
{
candidate += 1024;
}
return candidate;
}
bool gps_time_from_sample_anchor(int anchor_week,
double anchor_tow_sec,
uint32_t anchor_raw_sample,
@@ -86,3 +108,50 @@ bool gps_time_from_sample_anchor(int anchor_week,
}
return true;
}
bool gps_time_anchors_consistent(int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
double first_corrected_sample,
int second_week,
double second_tow_sec,
uint32_t second_raw_sample,
double second_corrected_sample,
double samples_per_second,
double max_gap_sec,
double tolerance_sec)
{
if((first_week < 0) || (second_week < 0) ||
!isfinite(first_tow_sec) || !isfinite(second_tow_sec) ||
!isfinite(first_corrected_sample) ||
!isfinite(second_corrected_sample) ||
(first_tow_sec < 0.0) ||
(first_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
(second_tow_sec < 0.0) ||
(second_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
!(samples_per_second > 0.0) || !(max_gap_sec > 0.0) ||
!(tolerance_sec >= 0.0))
{
return false;
}
int32_t raw_delta = (int32_t)(second_raw_sample - first_raw_sample);
double first_fraction =
first_corrected_sample - (double)first_raw_sample;
double second_fraction =
second_corrected_sample - (double)second_raw_sample;
double sample_elapsed =
((double)raw_delta + second_fraction - first_fraction) /
samples_per_second;
if(!isfinite(sample_elapsed) || (sample_elapsed < -0.001) ||
(sample_elapsed > max_gap_sec))
{
return false;
}
double gps_elapsed =
((double)(second_week - first_week) * GPS_TIME_WEEK_SECONDS) +
second_tow_sec - first_tow_sec;
return isfinite(gps_elapsed) &&
(fabs(gps_elapsed - sample_elapsed) <= tolerance_sec);
}
@@ -66,8 +66,13 @@ bool insert_element(volatile gps_ephem_ll_t** node,
new_node->gps_how_corrected_abs_sample = 0.0;
new_node->gps_how_tick = 0u;
new_node->gps_how_anchor_valid = false;
new_node->gps_how_week_valid = false;
new_node->gps_pending_how_tow.week = 0;
new_node->gps_pending_how_tow.sec = 0.0;
new_node->gps_pending_how_abs_sample = 0u;
new_node->gps_pending_how_corrected_abs_sample = 0.0;
new_node->gps_pending_how_valid = false;
new_node->gps_nav_solve_sequence = 0u;
new_node->gps_nav_solve_consumed_sequence = 0u;
new_node->gps_last_nav_solve_tow.week = 0;
new_node->gps_last_nav_solve_tow.sec = 0.0;
new_node->gps_last_nav_solve_abs_sample = 0u;
+25 -3
View File
@@ -3,9 +3,10 @@ CFLAGS ?= -O2 -std=c11 -Wall -Wextra -Werror
ROOT := ..
TARGETS := test_imu_calibration test_gps_nav_epoch \
test_gps_nav_epoch_coalescing test_plic_warm_reset
test_gps_nav_epoch_coalescing test_gps_time_anchor \
test_plic_warm_reset
.PHONY: all run clean
.PHONY: all run sanitize clean
all: $(TARGETS)
@@ -18,6 +19,9 @@ test_gps_nav_epoch: test_gps_nav_epoch.c $(ROOT)/src/gps/gps_nav_epoch.c
test_gps_nav_epoch_coalescing: test_gps_nav_epoch_coalescing.c $(ROOT)/src/gps/gps_nav_epoch.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_gps_time_anchor: test_gps_time_anchor.c $(ROOT)/src/gps/gps_time_anchor.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_plic_warm_reset: test_plic_warm_reset.c $(ROOT)/../bsp/drivers/plic.c
$(CC) $(CFLAGS) -Ifake_bsp -I$(ROOT)/../bsp/include $^ -o $@
@@ -25,7 +29,25 @@ run: $(TARGETS)
./test_imu_calibration
./test_gps_nav_epoch
./test_gps_nav_epoch_coalescing
./test_gps_time_anchor
./test_plic_warm_reset
sanitize:
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_nav_epoch.c \
$(ROOT)/src/gps/gps_nav_epoch.c -lm \
-o test_gps_nav_epoch_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_nav_epoch_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_nav_epoch_coalescing.c \
$(ROOT)/src/gps/gps_nav_epoch.c -lm \
-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
clean:
rm -f $(TARGETS) test_gps_nav_epoch_coalescing_repro
rm -f $(TARGETS) test_gps_nav_epoch_coalescing_repro \
test_gps_nav_epoch_san test_gps_nav_epoch_coalescing_san
+273 -349
View File
@@ -5,8 +5,11 @@
#include <stdio.h>
#include <string.h>
#define SAMPLE_WINDOW 409200u
#define TOW_TOLERANCE 0.001
#define SAMPLE_WINDOW 409200u
#define TOW_TOLERANCE 0.001
#define CAPTURE_PERIOD_TICKS 25000000ULL
#define CAPTURE_WINDOW_TICKS 3125000ULL
#define HOLDOFF_LOCAL_TICKS 15000ULL
static int failures;
@@ -29,167 +32,28 @@ static gps_nav_epoch_selection_t select_epoch(
return selection;
}
static unsigned int mask_count(uint32_t mask)
static bool validate_complete_epoch(
const gps_nav_epoch_key_t* candidates,
unsigned int count,
gps_nav_epoch_selection_t* selection)
{
unsigned int count = 0u;
while(mask != 0u)
{
count += mask & 1u;
mask >>= 1u;
}
return count;
memset(selection, 0, sizeof(*selection));
return gps_nav_epoch_validate_complete(
candidates, count, SAMPLE_WINDOW,
TOW_TOLERANCE, selection);
}
static void epoch_id_rounding_and_week_wrap(void)
static gps_nav_epoch_event_result_t push_capture(
gps_nav_epoch_assembly_t* assembly,
uint64_t capture_counter,
uint64_t local_ticks,
unsigned int channel,
uint32_t ready_mask)
{
uint64_t before_wrap;
uint64_t after_wrap;
uint64_t same_epoch_a;
uint64_t same_epoch_b;
CHECK(gps_nav_epoch_id(2300, 604799.9999, 200u, &before_wrap),
"epoch id rejected a valid pre-wrap TOW");
CHECK(gps_nav_epoch_id(2301, 0.0001, 200u, &after_wrap),
"epoch id rejected a valid post-wrap TOW");
CHECK(before_wrap == after_wrap,
"week rollover split one rounded navigation epoch");
CHECK(gps_nav_epoch_id(2301, 123.3997, 200u, &same_epoch_a) &&
gps_nav_epoch_id(2301, 123.4003, 200u, &same_epoch_b) &&
(same_epoch_a == same_epoch_b),
"sub-millisecond TOW skew split one navigation epoch");
CHECK(!gps_nav_epoch_id(2301, NAN, 200u, &same_epoch_a),
"non-finite TOW produced an epoch id");
CHECK(!gps_nav_epoch_id(2301, 1.0, 333u, &same_epoch_a),
"non-integral epochs per GPS week were accepted");
}
static void assembly_waits_for_every_ready_channel(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint32_t ready_mask = 0xafu;
const unsigned int order[] = {7u, 0u, 5u, 3u, 2u, 1u};
for(unsigned int i = 0u ; i < sizeof(order) / sizeof(order[0]) ; i++)
{
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 1000u, order[i], ready_mask);
bool last = i + 1u == sizeof(order) / sizeof(order[0]);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) == last,
"assembly completed before every ready channel arrived");
}
CHECK(assembly.expected_mask == ready_mask,
"assembly changed its frozen ready mask");
CHECK(assembly.received_mask == ready_mask,
"assembly lost a ready-channel interrupt");
}
static void busy_unlocked_channel_does_not_block_epoch(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
/* Model the board failure: channels 0..8 are allocated and busy, but
* channel 4 has lost lock and therefore cannot produce this NAV epoch. */
const uint32_t allocated_busy_mask = 0x1ffu;
const uint32_t locked_mask = allocated_busy_mask & ~(1u << 4);
const uint32_t ready_mask = allocated_busy_mask & locked_mask;
for(unsigned int channel = 0u ; channel < 9u ; channel++)
{
if((ready_mask & (1u << channel)) == 0u)
{
continue;
}
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 1500u, channel, ready_mask);
bool last = channel == 8u;
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) == last,
"busy unlocked channel blocked a complete locked-channel epoch");
}
CHECK(assembly.expected_mask == 0x1efu,
"busy unlocked channel entered the ready mask");
CHECK(assembly.received_mask == assembly.expected_mask,
"locked channels did not complete the epoch");
}
static void assembly_handles_duplicates_and_epoch_order(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
CHECK(gps_nav_epoch_assembly_push(&assembly, 2000u, 0u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"first channel did not start assembly");
CHECK(gps_nav_epoch_assembly_push(&assembly, 2000u, 0u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_DUPLICATE,
"duplicate channel interrupt was not idempotent");
CHECK(gps_nav_epoch_assembly_push(&assembly, 1999u, 1u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_STALE,
"stale epoch interrupt was not rejected");
CHECK(gps_nav_epoch_assembly_push(&assembly, 2001u, 1u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_RESTARTED,
"newer epoch did not replace the incomplete epoch");
CHECK((assembly.epoch_id == 2001u) &&
(assembly.expected_mask == 0x0fu) &&
(assembly.received_mask == 0x02u),
"newer epoch restart retained measurements from the old epoch");
}
static void assembly_disconnect_and_reconnect(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
CHECK(gps_nav_epoch_assembly_push(&assembly, 3000u, 0u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"disconnect scenario did not start");
CHECK(gps_nav_epoch_assembly_push(&assembly, 3000u, 1u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"second interrupt unexpectedly completed");
CHECK(!gps_nav_epoch_assembly_remove_channel(&assembly, 4u),
"pending-channel disconnect completed too early");
CHECK(gps_nav_epoch_assembly_push(&assembly, 3000u, 2u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"post-disconnect interrupt was rejected");
CHECK(gps_nav_epoch_assembly_push(&assembly, 3000u, 3u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_COMPLETE,
"unlock did not release the removed channel from the epoch");
gps_nav_epoch_assembly_reset(&assembly);
CHECK(gps_nav_epoch_assembly_push(&assembly, 3001u, 0u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"reconnect epoch did not start");
for(unsigned int channel = 1u ; channel < 5u ; channel++)
{
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 3001u, channel, 0x1fu);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) ==
(channel == 4u),
"reconnected channel was not required by the next epoch");
}
}
static void channel_join_does_not_expand_active_epoch(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
CHECK(gps_nav_epoch_assembly_push(&assembly, 4000u, 0u, 0x0fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"join scenario did not start");
CHECK(gps_nav_epoch_assembly_push(&assembly, 4000u, 4u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_INVALID,
"joining channel changed the active epoch");
CHECK(assembly.expected_mask == 0x0fu,
"active expected mask expanded after a channel joined");
for(unsigned int channel = 1u ; channel < 4u ; channel++)
{
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 4000u, channel, 0x1fu);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) ==
(channel == 3u),
"original ready set did not complete after a join");
}
return gps_nav_epoch_assembly_push(
assembly, capture_counter, local_ticks, channel, ready_mask,
CAPTURE_PERIOD_TICKS, CAPTURE_WINDOW_TICKS,
HOLDOFF_LOCAL_TICKS);
}
static uint32_t deterministic_random(uint32_t* state)
@@ -202,122 +66,217 @@ static uint32_t deterministic_random(uint32_t* state)
return value;
}
static void randomized_interrupt_and_unlock_model(void)
static void local_capture_assembly_completes(void)
{
uint32_t random_state = 0x6a09e667u;
for(unsigned int trial = 0u ; trial < 10000u ; trial++)
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint32_t ready_mask = 0x7fu;
const unsigned int order[] = {4u, 0u, 6u, 2u, 5u, 1u, 3u};
const uint64_t base = 810102900000ULL;
for(unsigned int i = 0u ; i < sizeof(order) / sizeof(order[0]) ; i++)
{
unsigned int channel_count =
4u + (deterministic_random(&random_state) % 6u);
uint32_t ready_mask = (1u << channel_count) - 1u;
unsigned int order[GPS_NAV_EPOCH_MAX_CANDIDATES];
for(unsigned int i = 0u ; i < channel_count ; i++)
{
order[i] = i;
}
for(unsigned int i = channel_count ; i > 1u ; i--)
{
unsigned int swap =
deterministic_random(&random_state) % i;
unsigned int temporary = order[i - 1u];
order[i - 1u] = order[swap];
order[swap] = temporary;
}
gps_nav_epoch_event_result_t result =
push_capture(&assembly, base + (uint64_t)i * 250000u,
100000u + i, order[i], ready_mask);
bool last = i + 1u == sizeof(order) / sizeof(order[0]);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) == last,
"coherent local capture group completed at the wrong event");
}
CHECK(assembly.expected_mask == ready_mask,
"assembly changed its frozen ready mask");
CHECK(assembly.received_mask == ready_mask,
"assembly lost a ready-channel capture");
CHECK((assembly.maximum_capture_counter -
assembly.minimum_capture_counter) <= CAPTURE_WINDOW_TICKS,
"coherent group exceeded its capture span");
}
bool disconnect =
(deterministic_random(&random_state) & 3u) == 0u;
unsigned int dropped_channel = disconnect ?
(deterministic_random(&random_state) % channel_count) :
GPS_NAV_EPOCH_MAX_CANDIDATES;
uint32_t final_mask = ready_mask;
if(disconnect)
{
final_mask &= ~(1u << dropped_channel);
}
static void coalesced_capture_sequence_validation(void)
{
uint32_t gap = 0u;
const uint64_t period = CAPTURE_PERIOD_TICKS;
const uint64_t base = 5000000000ULL;
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
bool complete = false;
for(unsigned int i = 0u ; i < channel_count ; i++)
{
unsigned int channel = order[i];
if(channel == dropped_channel)
{
complete = gps_nav_epoch_assembly_remove_channel(
&assembly, channel);
continue;
}
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 5000u + trial, channel, ready_mask);
if((deterministic_random(&random_state) & 7u) == 0u)
{
CHECK(gps_nav_epoch_assembly_push(
&assembly, 5000u + trial, channel,
ready_mask) ==
GPS_NAV_EPOCH_EVENT_DUPLICATE,
"random duplicate interrupt was not idempotent");
}
complete = result == GPS_NAV_EPOCH_EVENT_COMPLETE;
}
if(disconnect &&
((assembly.expected_mask & (1u << dropped_channel)) != 0u))
{
complete = gps_nav_epoch_assembly_remove_channel(
&assembly, dropped_channel);
}
CHECK(mask_count(final_mask) < 4u ||
(complete && gps_nav_epoch_assembly_complete(&assembly)),
"random interrupt/unlock sequence did not complete");
CHECK(assembly.expected_mask == final_mask,
"random unlock changed the wrong expected channel");
CHECK(assembly.received_mask == final_mask,
"random model lost or invented an interrupt");
CHECK(gps_nav_capture_sequence_consistent(
base, 10u, base + period, 11u, period, &gap) &&
(gap == 1u),
"single capture sequence advance was rejected");
CHECK(gps_nav_capture_sequence_consistent(
base, 10u, base + 5u * period, 15u, period, &gap) &&
(gap == 5u),
"coalesced newest capture did not preserve its real epoch gap");
CHECK(gps_nav_capture_sequence_consistent(
base, UINT32_MAX - 1u, base + 3u * period, 1u,
period, &gap) &&
(gap == 3u),
"capture sequence wrap was not handled modulo uint32");
CHECK(gps_nav_capture_sequence_consistent(
base, UINT32_MAX, base + period, 0u, period, &gap) &&
(gap == 1u),
"capture sequence value zero at wrap was treated as uninitialized");
CHECK(gps_nav_capture_sequence_consistent(
base, 0u, base + period, 1u, period, &gap) &&
(gap == 1u),
"capture sequence did not continue after wrapping through zero");
CHECK(gps_nav_capture_sequence_consistent(
base, 10u, base + period + period / 3u,
11u, period, &gap),
"bounded hardware-counter phase jitter was rejected");
CHECK(!gps_nav_capture_sequence_consistent(
base, 10u, base + period, 10u, period, &gap),
"duplicate capture sequence entered a new epoch");
CHECK(!gps_nav_capture_sequence_consistent(
base, 10u, base - 1u, 11u, period, &gap),
"backward hardware counter was accepted");
CHECK(!gps_nav_capture_sequence_consistent(
base, 10u, base + 2u * period, 11u, period, &gap),
"counter/sequence disagreement was accepted");
CHECK(!gps_nav_capture_sequence_consistent(
base, 10u, base + period, 11u, 0u, &gap),
"zero capture period was accepted");
}
static void missing_channel_expires_as_usable_subset(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint32_t ready_mask = 0x3fu;
const uint64_t base = 900000000ULL;
for(unsigned int channel = 0u ; channel < 5u ; channel++)
{
CHECK(push_capture(&assembly,
base + (uint64_t)channel * 100000u,
200000u + channel,
channel, ready_mask) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"partial epoch rejected a coherent channel");
}
CHECK(!gps_nav_epoch_assembly_complete(&assembly),
"missing ready channel completed before holdoff");
CHECK(gps_nav_epoch_assembly_usable(
&assembly, GPS_NAV_MIN_WLS_SATELLITES),
"five-channel partial epoch was not usable");
CHECK(!gps_nav_epoch_assembly_expired(
&assembly, assembly.deadline_local_ticks - 1u),
"epoch expired before its deadline");
CHECK(gps_nav_epoch_assembly_expired(
&assembly, assembly.deadline_local_ticks),
"epoch did not expire at its deadline");
}
static void incomplete_epoch_never_becomes_complete(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint32_t ready_mask = 0x1fu;
const uint64_t base = 950000000ULL;
for(unsigned int channel = 0u ; channel < 4u ; channel++)
{
CHECK(push_capture(&assembly,
base + (uint64_t)channel * 100000u,
250000u + channel,
channel, ready_mask) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"four-channel partial epoch rejected a coherent channel");
}
CHECK(gps_nav_epoch_assembly_usable(
&assembly, GPS_NAV_MIN_WLS_SATELLITES),
"four coherent channels must remain algebraically solvable");
CHECK(!gps_nav_epoch_assembly_complete(&assembly),
"timed-out subset incorrectly completed the expected epoch");
CHECK(gps_nav_epoch_assembly_expired(
&assembly, assembly.deadline_local_ticks),
"incomplete epoch did not reach its discard deadline");
}
static void local_outlier_cannot_poison_epoch(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint32_t ready_mask = 0x1fu;
const uint64_t base = 1000000000ULL;
CHECK(push_capture(&assembly, base, 300000u, 0u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"outlier scenario did not start");
CHECK(push_capture(&assembly, base + 6000000u, 300001u,
1u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_OUTLIER,
"ambiguous local timestamp entered the active group");
CHECK((assembly.received_mask == 0x01u) &&
(assembly.minimum_capture_counter == base) &&
(assembly.maximum_capture_counter == base),
"local outlier mutated the active group");
CHECK(push_capture(&assembly, base - CAPTURE_PERIOD_TICKS,
300002u, 2u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_STALE,
"delayed previous epoch was not rejected");
CHECK(push_capture(&assembly, base + CAPTURE_PERIOD_TICKS,
300003u, 1u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_RESTARTED,
"new local epoch did not replace the incomplete group");
CHECK((assembly.first_capture_counter ==
base + CAPTURE_PERIOD_TICKS) &&
(assembly.received_mask == 0x02u),
"new epoch retained state from the old group");
}
static void duplicate_join_and_disconnect_are_bounded(void)
{
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
const uint64_t base = 1100000000ULL;
CHECK(push_capture(&assembly, base, 400000u, 0u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"membership scenario did not start");
CHECK(push_capture(&assembly, base, 400001u, 0u, 0x1fu) ==
GPS_NAV_EPOCH_EVENT_DUPLICATE,
"duplicate capture was not idempotent");
CHECK(push_capture(&assembly, base + 100000u, 400002u,
5u, 0x3fu) ==
GPS_NAV_EPOCH_EVENT_INVALID,
"joining channel expanded the active epoch");
CHECK(!gps_nav_epoch_assembly_remove_channel(&assembly, 4u),
"disconnect completed a one-channel group");
for(unsigned int channel = 1u ; channel < 4u ; channel++)
{
gps_nav_epoch_event_result_t result =
push_capture(&assembly,
base + (uint64_t)channel * 100000u,
400010u + channel, channel, 0x0fu);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) ==
(channel == 3u),
"remaining frozen membership completed incorrectly");
}
}
static void multi_epoch_hardware_interrupt_model(void)
static void randomized_capture_liveness(void)
{
gps_nav_epoch_assembly_t assembly;
uint32_t random_state = 0xbb67ae85u;
uint32_t ready_mask = 0x7fu;
unsigned int completed_epochs = 0u;
unsigned int dropped_epochs = 0u;
unsigned int joined_epochs = 0u;
for(unsigned int epoch = 0u ; epoch < 2000u ; epoch++)
uint32_t random_state = 0x6a09e667u;
uint64_t base = 2000000000ULL;
for(unsigned int trial = 0u ; trial < 100000u ; trial++)
{
bool drop_before_interrupt = (epoch == 250u);
bool drop_after_interrupt = (epoch == 900u);
bool join_during_epoch = (epoch == 500u) || (epoch == 1200u);
unsigned int changing_channel =
(epoch == 1200u) ? 6u : 5u;
if(epoch == 400u)
{
ready_mask |= 1u << 2;
}
if(epoch == 1050u)
{
ready_mask |= 1u << 4;
}
if(join_during_epoch)
{
ready_mask &= ~(1u << changing_channel);
}
uint32_t epoch_ready_mask = ready_mask;
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
unsigned int ready_count =
4u + (deterministic_random(&random_state) % 6u);
uint32_t ready_mask = (1u << ready_count) - 1u;
unsigned int delivered_count =
4u + (deterministic_random(&random_state) %
(ready_count - 3u));
unsigned int order[GPS_NAV_EPOCH_MAX_CANDIDATES];
unsigned int order_count = 0u;
for(unsigned int channel = 0u ;
channel < GPS_NAV_EPOCH_MAX_CANDIDATES ; channel++)
for(unsigned int i = 0u ; i < ready_count ; i++)
{
if((epoch_ready_mask & (1u << channel)) != 0u)
{
order[order_count++] = channel;
}
order[i] = i;
}
for(unsigned int i = order_count ; i > 1u ; i--)
for(unsigned int i = ready_count ; i > 1u ; i--)
{
unsigned int swap =
deterministic_random(&random_state) % i;
@@ -326,76 +285,32 @@ static void multi_epoch_hardware_interrupt_model(void)
order[swap] = temporary;
}
gps_nav_epoch_assembly_reset(&assembly);
bool completed = false;
for(unsigned int event_index = 0u ;
event_index < order_count ; event_index++)
for(unsigned int i = 0u ; i < delivered_count ; i++)
{
unsigned int channel = order[event_index];
if(drop_before_interrupt && (channel == 2u))
{
ready_mask &= ~(1u << channel);
(void)gps_nav_epoch_assembly_remove_channel(
&assembly, channel);
dropped_epochs++;
continue;
}
uint64_t offset =
deterministic_random(&random_state) %
(CAPTURE_WINDOW_TICKS + 1u);
gps_nav_epoch_event_result_t result =
gps_nav_epoch_assembly_push(
&assembly, 100000u + epoch, channel,
epoch_ready_mask);
if(result == GPS_NAV_EPOCH_EVENT_COMPLETE)
{
completed = true;
}
if(drop_after_interrupt && (channel == 4u))
{
ready_mask &= ~(1u << channel);
if(gps_nav_epoch_assembly_remove_channel(
&assembly, channel))
{
completed = true;
}
dropped_epochs++;
}
if(join_during_epoch && (event_index == 0u))
{
ready_mask |= 1u << changing_channel;
CHECK(gps_nav_epoch_assembly_push(
&assembly, 100000u + epoch,
changing_channel, ready_mask) ==
GPS_NAV_EPOCH_EVENT_INVALID,
"mid-epoch connection expanded the frozen mask");
joined_epochs++;
}
if((event_index == 0u) && (epoch != 0u))
{
CHECK(gps_nav_epoch_assembly_push(
&assembly, 99999u + epoch,
channel, epoch_ready_mask) ==
GPS_NAV_EPOCH_EVENT_STALE,
"delayed previous-epoch interrupt was accepted");
}
push_capture(&assembly, base + offset,
500000u + i, order[i], ready_mask);
CHECK((result == GPS_NAV_EPOCH_EVENT_ACCEPTED) ||
(result == GPS_NAV_EPOCH_EVENT_COMPLETE),
"random coherent capture was rejected");
}
CHECK(completed &&
gps_nav_epoch_assembly_complete(&assembly),
"hardware stream did not complete a live epoch");
CHECK(assembly.received_mask == assembly.expected_mask,
"hardware stream completed with a missing interrupt");
completed_epochs++;
CHECK(gps_nav_epoch_assembly_usable(
&assembly, GPS_NAV_MIN_WLS_SATELLITES),
"random stream lost algebraic four-channel liveness");
CHECK(gps_nav_epoch_assembly_complete(&assembly) ==
(delivered_count == ready_count),
"random partial epoch claimed all expected channels");
if(delivered_count != ready_count)
{
CHECK(gps_nav_epoch_assembly_expired(
&assembly, assembly.deadline_local_ticks),
"partial random epoch did not expire");
}
base += CAPTURE_PERIOD_TICKS;
}
CHECK(completed_epochs == 2000u,
"hardware stream produced more or fewer than one completion per epoch");
CHECK(dropped_epochs == 2u,
"hardware stream did not exercise both unlock positions");
CHECK(joined_epochs == 2u,
"hardware stream did not exercise both reconnect positions");
}
static void all_coherent_across_sample_wrap(void)
@@ -411,24 +326,28 @@ static void all_coherent_across_sample_wrap(void)
"sample/week wrap split a coherent epoch");
CHECK(selection.selected_mask == 0x0fu,
"sample/week wrap selected the wrong rows");
CHECK(validate_complete_epoch(candidates, 4u, &selection),
"fully coherent week/sample-wrap epoch was rejected");
}
static void tow_outlier_does_not_block_good_cluster(void)
{
gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES] = {
{2300, 100.0000, 1000000u},
{2300, 100.0001, 1010000u},
{2300, 99.9999, 1020000u},
{2300, 100.0002, 1030000u},
{2300, 100.0000, 1040000u},
{2300, 100.0001, 1050000u},
{2300, 100.2000, 1060000u}
{380, 469367.4000, 2982422271u},
{380, 469367.4001, 2982428475u},
{381, 28831.6000, 2982490274u},
{380, 469367.3999, 2982550441u},
{380, 469367.4002, 2982661405u}
};
gps_nav_epoch_selection_t selection = select_epoch(candidates, 7u);
CHECK(selection.selected_count == 6u,
"single TOW outlier blocked the six-satellite cluster");
CHECK(selection.selected_mask == 0x3fu,
"TOW outlier was not excluded");
gps_nav_epoch_selection_t selection = select_epoch(candidates, 5u);
CHECK(selection.selected_count == 4u,
"live 45.6-hour TOW outlier blocked the good cluster");
CHECK(selection.selected_mask == 0x1bu,
"live TOW outlier was not excluded");
CHECK(!validate_complete_epoch(candidates, 5u, &selection),
"TOW outlier incorrectly allowed a partial receiver epoch");
CHECK(selection.selected_count == 4u,
"strict rejection lost diagnostic largest-cluster metadata");
}
static void newest_sample_outlier_does_not_define_the_cluster(void)
@@ -447,6 +366,8 @@ static void newest_sample_outlier_does_not_define_the_cluster(void)
"newest raw-sample outlier displaced the largest cluster");
CHECK(selection.selected_mask == 0x3fu,
"newest raw-sample outlier was not excluded");
CHECK(!validate_complete_epoch(candidates, 7u, &selection),
"sample outlier incorrectly allowed a partial receiver epoch");
}
static void equal_clusters_prefer_newer_epoch(void)
@@ -466,6 +387,8 @@ static void equal_clusters_prefer_newer_epoch(void)
"equal epoch clusters returned the wrong size");
CHECK(selection.selected_mask == 0xf0u,
"equal epoch clusters did not prefer the newer epoch");
CHECK(!validate_complete_epoch(candidates, 8u, &selection),
"two complete-looking epoch clusters were mixed");
}
static void nonfinite_candidate_is_excluded(void)
@@ -482,18 +405,19 @@ static void nonfinite_candidate_is_excluded(void)
"invalid TOW candidate blocked valid measurements");
CHECK(selection.selected_mask == 0x0fu,
"invalid TOW candidate was selected");
CHECK(!validate_complete_epoch(candidates, 5u, &selection),
"nonfinite TOW incorrectly allowed a partial receiver epoch");
}
int main(void)
{
epoch_id_rounding_and_week_wrap();
assembly_waits_for_every_ready_channel();
busy_unlocked_channel_does_not_block_epoch();
assembly_handles_duplicates_and_epoch_order();
assembly_disconnect_and_reconnect();
channel_join_does_not_expand_active_epoch();
randomized_interrupt_and_unlock_model();
multi_epoch_hardware_interrupt_model();
local_capture_assembly_completes();
coalesced_capture_sequence_validation();
missing_channel_expires_as_usable_subset();
incomplete_epoch_never_becomes_complete();
local_outlier_cannot_poison_epoch();
duplicate_join_and_disconnect_are_bounded();
randomized_capture_liveness();
all_coherent_across_sample_wrap();
tow_outlier_does_not_block_good_cluster();
newest_sample_outlier_does_not_define_the_cluster();
@@ -4,15 +4,9 @@
#include <stdint.h>
#include <stdio.h>
#define CHANNEL_COUNT 6u
#define READY_MASK 0x3fu
#define MISSING_CH 4u
typedef struct {
bool pending;
uint32_t capture_sequence;
uint64_t mailbox_epoch;
} synthetic_tracking_irq_t;
#define PERIOD_TICKS 25000000ULL
#define WINDOW_TICKS 3125000ULL
#define HOLDOFF_TICKS 15000ULL
static int failures;
@@ -23,152 +17,121 @@ static int failures;
} \
} while(0)
/*
* The tracking interrupt source is a sticky pending bit, while the associated
* measurement registers are a one-entry mailbox. A second pulse therefore
* advances the PPS capture sequence and overwrites the mailbox even when the
* first pulse has not yet been serviced.
*/
static void hardware_nav_pulse(
synthetic_tracking_irq_t channels[CHANNEL_COUNT],
uint64_t epoch)
{
for(unsigned int channel = 0u ; channel < CHANNEL_COUNT ; channel++)
{
channels[channel].capture_sequence++;
channels[channel].mailbox_epoch = epoch;
channels[channel].pending = true;
}
}
static gps_nav_epoch_event_result_t service_channel(
synthetic_tracking_irq_t channels[CHANNEL_COUNT],
uint32_t serviced_sequence[CHANNEL_COUNT],
static gps_nav_epoch_event_result_t push(
gps_nav_epoch_assembly_t* assembly,
uint64_t counter,
uint64_t local_ticks,
unsigned int channel,
unsigned int* coalesced_pulses)
uint32_t ready_mask)
{
if((channel >= CHANNEL_COUNT) || !channels[channel].pending)
{
return GPS_NAV_EPOCH_EVENT_INVALID;
}
uint32_t sequence_delta =
channels[channel].capture_sequence - serviced_sequence[channel];
if((serviced_sequence[channel] != 0u) && (sequence_delta > 1u))
{
*coalesced_pulses += sequence_delta - 1u;
}
serviced_sequence[channel] = channels[channel].capture_sequence;
channels[channel].pending = false;
return gps_nav_epoch_assembly_push(
assembly, channels[channel].mailbox_epoch, channel, READY_MASK);
assembly, counter, local_ticks, channel, ready_mask,
PERIOD_TICKS, WINDOW_TICKS, HOLDOFF_TICKS);
}
static void service_complete_epoch(
synthetic_tracking_irq_t channels[CHANNEL_COUNT],
uint32_t serviced_sequence[CHANNEL_COUNT],
gps_nav_epoch_assembly_t* assembly,
unsigned int* coalesced_pulses)
static void live_fault_recovery(void)
{
for(unsigned int channel = 0u ; channel < CHANNEL_COUNT ; channel++)
/* Exact topology from the captured board state: channel 6 is locked but
* 11.6 seconds stale; channel 3 carries a transient GPS time 45.6 hours
* ahead. Local capture time still places five measurements in one epoch. */
const uint32_t ready_mask =
(1u << 0) | (1u << 2) | (1u << 3) |
(1u << 4) | (1u << 6) | (1u << 7);
const unsigned int channels[] = {3u, 0u, 2u, 4u, 7u};
const uint64_t captures[] = {
810103886055ULL,
810102907239ULL,
810102954617ULL,
810104733473ULL,
810103426569ULL
};
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
for(unsigned int i = 0u ; i < 5u ; i++)
{
CHECK(push(&assembly, captures[i], 100000u + i,
channels[i], ready_mask) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"live coherent local capture was rejected");
}
CHECK(assembly.received_mask ==
((1u << 0) | (1u << 2) | (1u << 3) |
(1u << 4) | (1u << 7)),
"live capture mask differs from the expected five channels");
CHECK(gps_nav_epoch_assembly_usable(
&assembly, GPS_NAV_MIN_WLS_SATELLITES),
"live five-channel group lost its diagnostic measurements");
CHECK(!gps_nav_epoch_assembly_complete(&assembly),
"missing ready channel produced a publishable raw epoch");
CHECK(gps_nav_epoch_assembly_expired(
&assembly, assembly.deadline_local_ticks),
"missing channel did not release the live group at holdoff");
/* A physically later local group advances normally regardless of whatever
* week/TOW any channel decoded. */
uint32_t old_mask = assembly.received_mask;
CHECK(push(&assembly, captures[0] + PERIOD_TICKS,
200000u, 3u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_RESTARTED,
"next physical epoch did not advance after the live fault");
CHECK(old_mask != 0u && assembly.received_mask == (1u << 3),
"next epoch mixed measurements from the previous group");
}
static void overwritten_mailbox_remains_bounded(void)
{
const uint32_t ready_mask = 0x3fu;
gps_nav_epoch_assembly_t assembly;
gps_nav_epoch_assembly_reset(&assembly);
uint64_t first_epoch = 900000000000ULL;
/* Channel 4's first mailbox value is overwritten. The other five remain
* bounded diagnostics, but must never become a publishable raw epoch. */
for(unsigned int channel = 0u ; channel < 6u ; channel++)
{
if(channel == 4u)
{
continue;
}
CHECK(push(&assembly, first_epoch + channel * 100000u,
300000u + channel, channel, ready_mask) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"partial mailbox epoch rejected");
}
CHECK(gps_nav_epoch_assembly_usable(
&assembly, GPS_NAV_MIN_WLS_SATELLITES),
"mailbox overwrite lost the received diagnostic subset");
CHECK(!gps_nav_epoch_assembly_complete(&assembly),
"overwritten mailbox produced a partial raw epoch");
uint32_t previous_mask = assembly.received_mask;
CHECK(push(&assembly, first_epoch + PERIOD_TICKS,
400000u, 0u, ready_mask) ==
GPS_NAV_EPOCH_EVENT_RESTARTED,
"new mailbox epoch did not advance");
CHECK(previous_mask == 0x2fu,
"partial mailbox diagnostic mask changed before discard");
for(unsigned int channel = 1u ; channel < 6u ; channel++)
{
gps_nav_epoch_event_result_t result =
service_channel(channels, serviced_sequence, assembly,
channel, coalesced_pulses);
push(&assembly,
first_epoch + PERIOD_TICKS + channel * 100000u,
400000u + channel, channel, ready_mask);
CHECK((result == GPS_NAV_EPOCH_EVENT_COMPLETE) ==
(channel == CHANNEL_COUNT - 1u),
"healthy epoch did not complete on its final channel");
(channel == 5u),
"healthy mailbox epoch completed incorrectly");
}
}
int main(void)
{
synthetic_tracking_irq_t channels[CHANNEL_COUNT] = {0};
uint32_t serviced_sequence[CHANNEL_COUNT] = {0};
gps_nav_epoch_assembly_t assembly;
unsigned int coalesced_pulses = 0u;
unsigned int completed_epochs = 0u;
const uint64_t baseline_epoch = 1151141640u;
const uint64_t blocked_epoch = 1151141641u;
/* Establish the same non-zero PPS sequence baseline as the running board. */
gps_nav_epoch_assembly_reset(&assembly);
hardware_nav_pulse(channels, baseline_epoch);
service_complete_epoch(channels, serviced_sequence, &assembly,
&coalesced_pulses);
completed_epochs++;
gps_nav_epoch_assembly_reset(&assembly);
/* All six pulse, but the CPU services every source except tracking 4. */
hardware_nav_pulse(channels, blocked_epoch);
for(unsigned int channel = 0u ; channel < CHANNEL_COUNT ; channel++)
{
if(channel == MISSING_CH)
{
continue;
}
CHECK(service_channel(channels, serviced_sequence, &assembly,
channel, &coalesced_pulses) ==
GPS_NAV_EPOCH_EVENT_ACCEPTED,
"partial blocked epoch was not accepted");
}
CHECK(assembly.expected_mask == 0x3fu,
"blocked epoch expected mask differs from the board");
CHECK(assembly.received_mask == 0x2fu,
"blocked epoch received mask differs from the board");
/*
* Before channel 4 is serviced, the next hardware pulse overwrites its
* one-entry mailbox. Its sequence delta is two, but only the newer epoch
* can be delivered to the assembler.
*/
hardware_nav_pulse(channels, blocked_epoch + 1u);
for(unsigned int channel = 0u ; channel < CHANNEL_COUNT ; channel++)
{
gps_nav_epoch_event_result_t result =
service_channel(channels, serviced_sequence, &assembly,
channel, &coalesced_pulses);
gps_nav_epoch_event_result_t expected =
(channel == 0u) ? GPS_NAV_EPOCH_EVENT_RESTARTED :
(channel == CHANNEL_COUNT - 1u) ?
GPS_NAV_EPOCH_EVENT_COMPLETE :
GPS_NAV_EPOCH_EVENT_ACCEPTED;
CHECK(result == expected,
"newer hardware epoch did not restart and complete cleanly");
}
CHECK(coalesced_pulses == 1u,
"PPS sequence did not expose the overwritten channel-4 pulse");
CHECK((assembly.epoch_id == blocked_epoch + 1u) &&
(assembly.expected_mask == READY_MASK) &&
(assembly.received_mask == READY_MASK) &&
gps_nav_epoch_assembly_complete(&assembly),
"assembler did not recover on the first complete newer epoch");
completed_epochs++;
/* Every later healthy epoch must continue to complete normally. */
for(uint64_t offset = 2u ; offset < 102u ; offset++)
{
gps_nav_epoch_assembly_reset(&assembly);
hardware_nav_pulse(channels, blocked_epoch + offset);
service_complete_epoch(channels, serviced_sequence, &assembly,
&coalesced_pulses);
completed_epochs++;
}
CHECK(completed_epochs == 102u,
"recovery stream lost or duplicated a completed epoch");
live_fault_recovery();
overwritten_mailbox_remains_bounded();
if(failures == 0)
{
printf("RECOVERED: active=%llu expected=0x%x received=0x%x "
"coalesced=%u completed_epochs=%u\n",
(unsigned long long)assembly.epoch_id,
assembly.expected_mask,
assembly.received_mask,
coalesced_pulses,
completed_epochs);
puts("gps_nav_epoch coalescing/fault tests passed");
}
return failures == 0 ? 0 : 1;
}
@@ -0,0 +1,114 @@
#include "gps/gps_time_anchor.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#define SAMPLE_RATE_HZ 16368000.0
static int failures;
#define CHECK(condition, message) do { \
if(!(condition)) { \
fprintf(stderr, "FAIL: %s\n", message); \
failures++; \
} \
} while(0)
static void normal_and_skipped_subframes_are_consistent(void)
{
uint32_t first = 1000000000u;
uint32_t six_seconds = (uint32_t)(SAMPLE_RATE_HZ * 6.0);
CHECK(gps_time_anchors_consistent(
380, 469362.0, first, (double)first,
380, 469368.0, first + six_seconds,
(double)(first + six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"normal six-second HOW progression was rejected");
CHECK(gps_time_anchors_consistent(
380, 469362.0, first, (double)first,
380, 469380.0, first + 3u * six_seconds,
(double)(first + 3u * six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"skipped subframes broke HOW continuity");
}
static void week_rollover_is_consistent(void)
{
uint32_t first = 2000000000u;
uint32_t six_seconds = (uint32_t)(SAMPLE_RATE_HZ * 6.0);
CHECK(gps_time_anchors_consistent(
380, 604794.0, first, (double)first,
381, 0.0, first + six_seconds,
(double)(first + six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"valid GPS week rollover was rejected");
CHECK(gps_time_extend_lnav_week(true, 1023, 0u) == 1024,
"LNAV week rollover was not extended monotonically");
CHECK(gps_time_extend_lnav_week(true, 1024, 1023u) == 1023,
"previous modulo week was not extended backward");
CHECK(gps_time_extend_lnav_week(false, 0, 380u) == 380,
"initial LNAV week was altered");
}
static void temporary_week_can_upgrade_on_first_subframe_one(void)
{
uint32_t first = 2500000000u;
uint32_t six_seconds = (uint32_t)(SAMPLE_RATE_HZ * 6.0);
CHECK(gps_time_anchors_consistent(
0, 470940.0, first, (double)first,
0, 470946.0, first + six_seconds,
(double)(first + six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"temporary-week TOW continuity was rejected");
CHECK(gps_time_extend_lnav_week(true, 0, 380u) == 380,
"first SF1 could not upgrade the temporary week");
}
static void captured_future_jump_is_rejected_and_recovers(void)
{
uint32_t first = 3000000000u;
uint32_t six_seconds = (uint32_t)(SAMPLE_RATE_HZ * 6.0);
CHECK(!gps_time_anchors_consistent(
380, 469362.0, first, (double)first,
381, 28830.0, first + six_seconds,
(double)(first + six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"captured 45.6-hour HOW jump was accepted");
/* Two consecutive candidates on the replacement timeline agree, allowing
* the channel-level pending-anchor logic to recover without a reset. */
CHECK(gps_time_anchors_consistent(
380, 469368.0, first + six_seconds,
(double)(first + six_seconds),
380, 469374.0, first + 2u * six_seconds,
(double)(first + 2u * six_seconds),
SAMPLE_RATE_HZ, 120.0, 0.050),
"two good HOW candidates could not establish recovery");
}
static void invalid_inputs_are_rejected(void)
{
CHECK(!gps_time_anchors_consistent(
380, NAN, 1u, 1.0, 380, 6.0, 2u, 2.0,
SAMPLE_RATE_HZ, 120.0, 0.050),
"non-finite HOW was accepted");
CHECK(!gps_time_anchors_consistent(
380, 0.0, 1u, 1.0, 380, 604800.0, 2u, 2.0,
SAMPLE_RATE_HZ, 120.0, 0.050),
"out-of-range HOW was accepted");
}
int main(void)
{
normal_and_skipped_subframes_are_consistent();
week_rollover_is_consistent();
temporary_week_can_upgrade_on_first_subframe_one();
captured_future_jump_is_rejected_and_recovers();
invalid_inputs_are_rejected();
if(failures == 0)
{
puts("gps_time_anchor tests passed");
}
return failures == 0 ? 0 : 1;
}