6 Commits

Author SHA1 Message Date
Codex dce1dd5ea9 Fix GPS nav-solve phase seeding for cold PVT 2026-08-04 15:52:36 +03:00
Codex 28b55fdfb6 Fix GPS acquisition visibility and pull-in edge cases 2026-08-04 13:48:48 +03:00
onur 30179df8e6 feat(gps): add timer-based assisted reacquisition
Remove the fixed pseudorange start offset, preserve per-PRN navigation assistance across RF loss and power cycles, and seed coherent navigation measurement phase on reacquisition. Add full LNAV almanac visibility scheduling, power-safe QSPI journaling, RF-domain reset isolation, and PPS Gray CDC hardening with Linux, sanitizer, RTL, and end-to-end coverage.
2026-08-04 02:58:11 +03:00
onur 347e1c671d feat(pps): preserve loop dynamics across nav periods 2026-07-29 00:45:16 +03:00
onur a6bb080270 fix: select medany toolchain and available GDB 2026-07-29 00:16:57 +03:00
onur 9b0828dbaa feat: replay live PPS telemetry in fixed-point model 2026-07-29 00:16:43 +03:00
96 changed files with 10549 additions and 375 deletions
+5
View File
@@ -7,3 +7,8 @@
# Generated build outputs.
/out/
/soc/out/
# Python runtime caches and privacy-sensitive raw board captures.
__pycache__/
*.py[cod]
/soc/software/pps_end_to_end_validation/hardware_logs/*.csv
+24 -6
View File
@@ -112,6 +112,12 @@ make FPGA_BOARD=genesys2 APP=gps_system \
kullanir; ikinci build birincinin ciktisinin uzerine yazar. Iki profili
saklamak gerekiyorsa her build'den sonra dosyayi profil adiyla kopyalayin.
Cold scan, sky-guided search, independent unknown-orbit discovery, hot
reacquisition, ephemeris cache ve mevcut almanak siniri dahil acquisition
algoritmasinin tam aciklamasi
[`soc/software/gps_system/README.md`](soc/software/gps_system/README.md#acquisition-engine-algoritmasi)
altindadir.
`fpga-mcs` mevcut `.bit` dosyasini kullanip bootloader ve uygulamayi yeniden
derler. Yalniz software profili veya baud degisikligi icin sentez gerekmez.
Tracking/PPS RTL veya Chisel degistiyse once:
@@ -171,12 +177,24 @@ birimindeki ortak etkisidir. Ornegin `0.1 ppm` yaklasik `29.98 m/s` eder;
Doppler WLS bu ortak terimi uydu yonune gore degisen gercek `vx/vy/vz`
hareketinden ayirir.
Bu projede onemli bir datum farki vardir: position WLS'e verilen capture
zamanlarindan ortak receiver zamani cikarilir ve `STARTOFFSET` eklenir. Bu
nedenle `solution.clock_bias_m`, saf `c*delta_t_r` degil, yaklasik
`c*STARTOFFSET - reference_range` olan referans-uyduya bagli goreli
datumdur. Doppler WLS'nin dorduncu bilinmeyeni yine uydulara ortak receiver
clock/frequency drift'tir; fakat iki fit arasinda
Bu projede position WLS gozlemleri, ortak RF sample counter ve ayni anda
yakalanan fractional code-NCO residue ile decode edilmis satellite transmit
time'in sabit bir referans kanala gore farkindan uretilir:
```text
S_i = unwrap32(ABS_SAMPLE_i) - REM_CODE_PHASE_i / 1.023e6
P_i/c = (S_i - S_ref)/16.367981e6 - (T_tx_i - T_tx_ref)
```
16.367981 MHz integer counter tek basina 61.095 ns/18.31 m adimlidir;
fractional NCO residue bu sample quantization'ini giderir. 64-bit 125 MHz
counter position range'i yerine epoch assembly, GPS-time/PPS ankraji ve
holdover icin kullanilir. Bu nedenle startup'a bagli sabit bir flight-time
offset'i yoktur.
`solution.clock_bias_m`, saf `c*delta_t_r` degil, yaklasik
`-reference_range + c*reference_satellite_clock_correction` olan
referans-uyduya bagli goreli datumdur. Doppler WLS'nin dorduncu bilinmeyeni
yine uydulara ortak receiver clock/frequency drift'tir; fakat iki fit arasinda
`clock_bias_m(t+dt)=clock_bias_m(t)+clock_drift_mps*dt` kisiti kurulmaz.
Fit'ler ayri measurement/normal-equation kullanir. Doppler WLS, LOS
vektorleri icin position WLS'nin guncel receiver konumuna baglidir.
+2 -2
View File
@@ -213,7 +213,7 @@ always_ff @(posedge acq_clk or negedge acq_rstn)
S_PULLIN : begin
pullin_flag <= 1;
if (pullin_flag == 0) begin
freq_bin_cnt <= max_freq_bin_idx - 1;
freq_bin_cnt <= (max_freq_bin_idx == 0) ? 0 : (max_freq_bin_idx - 1);
max_corr <= 0;
//STATE <= S_START;
STATE <= S_WR_DATA;
@@ -314,7 +314,7 @@ always_ff @(posedge afe_clk or negedge afe_rstn) begin
wr_mem_end <= 0;
if (sample_align_i && wr_mem_start_r2) begin
wr_mem_en <= 1;
end else if (rf_wr_addr == 2*SAMPLES_PER_CODE) begin // 2 code lenght
end else if (rf_wr_addr == 2*SAMPLES_PER_CODE - 1) begin // 2 code length
wr_mem_en <= 0;
wr_mem_end <= 1;
end
+23 -3
View File
@@ -67,6 +67,8 @@ module tracking_wrapper #(
// nav solve timer I/O
logic nav_solve_timer_reached;
logic nav_solve_timer_load_abs_sample_signal;
logic nav_solve_phase_seed_valid;
logic [31:0] nav_solve_phase_seed;
//
@@ -117,6 +119,7 @@ module tracking_wrapper #(
logic [31 : 0] control_reg_p, control_reg_n;
logic [31 : 0] timeout_reg_p, timeout_reg_n;
logic [31 : 0] nav_solve_timer_reg_p, nav_solve_timer_reg_n;
logic [31 : 0] nav_solve_phase_reg_p, nav_solve_phase_reg_n;
logic [30 : 0] nav_bit_read_reg_p[10], nav_bit_read_reg_n[10];
// Atomic APB-domain copy of the 20 ms measurement.
@@ -143,9 +146,9 @@ module tracking_wrapper #(
assign to_start_trck_pin = (start_trck_ctl_p != 0) ? 1 : 0;
assign to_stop_trck_pin = (stop_trck_ctl_p != 0) ? 1 : 0;
// ABS_SAMPLE and REM_CODE_PHASE are latched on this same PCLK pulse. PPS
// timestamps only navigation-solve epochs; subframe-ready is not a position
// epoch and therefore must not create a PPS anchor.
// This pulse and the measurement payload originate on the same 125 MHz
// tracking epoch. PPS latches it natively; the payload uses the bundled-data
// PCLK crossing below. Subframe-ready is not a position/PPS epoch.
assign pps_capture = nav_solve_timer_reached;
// comb. logic register cdc
@@ -363,6 +366,7 @@ module tracking_wrapper #(
intr_pending_p <= '0;
timeout_reg_p <= trck_pkg::TRCK_TIMEOUT_RESET_VAL;
nav_solve_timer_reg_p <= 'ha;
nav_solve_phase_reg_p <= '0;
timeout_counter_p <= '0;
start_trck_ctl_p <= '0;
stop_trck_ctl_p <= '0;
@@ -395,6 +399,7 @@ module tracking_wrapper #(
intr_pending_p <= intr_pending_n;
timeout_reg_p <= timeout_reg_n;
nav_solve_timer_reg_p <= nav_solve_timer_reg_n;
nav_solve_phase_reg_p <= nav_solve_phase_reg_n;
timeout_counter_p <= timeout_counter_n;
start_trck_ctl_p <= start_trck_ctl_n;
stop_trck_ctl_p <= stop_trck_ctl_n;
@@ -631,6 +636,8 @@ module tracking_wrapper #(
.resetn(PRESETN), // active low reset.
.enable(control_reg_p[TRCK_NAV_SOLVE_ENABLE_POS]), // timer enable bit.
.counter_max_val(nav_solve_timer_reg_p), // timer generates interrupt when internal timer reaches this value.
.phase_seed_valid(nav_solve_phase_seed_valid),
.phase_seed(nav_solve_phase_seed),
.bit_valid(bit_valid_syned), // timer counts bit valid signals.
.subframe_ready(load_sigs), // signal that asserts 1 clock cycle after a subframe bit reached ( in essence, 300 bit + 8 preamble bit !!!)
.subframe_count(how_z_count), // HOW Z-count of this globally identified subframe boundary.
@@ -656,6 +663,9 @@ module tracking_wrapper #(
control_reg_n = control_reg_p;
timeout_reg_n = timeout_reg_p;
nav_solve_timer_reg_n = nav_solve_timer_reg_p;
nav_solve_phase_reg_n = nav_solve_phase_reg_p;
nav_solve_phase_seed_valid = 1'b0;
nav_solve_phase_seed = nav_solve_phase_reg_p;
if(apb_bus_ps == APB_STATE_SEL) begin
if(PWRITE) begin // write to register
@@ -715,6 +725,12 @@ module tracking_wrapper #(
begin
nav_solve_timer_reg_n = PWDATA;
end
(LOW_ADDRESS + NAV_SOLVE_PHASE_REG_OFFSET):
begin
nav_solve_phase_reg_n = PWDATA;
nav_solve_phase_seed = PWDATA;
nav_solve_phase_seed_valid = 1'b1;
end
default:
@@ -827,6 +843,10 @@ module tracking_wrapper #(
begin
PRDATA = nav_solve_timer_reg_p;
end
(LOW_ADDRESS + NAV_SOLVE_PHASE_REG_OFFSET):
begin
PRDATA = nav_solve_phase_reg_p;
end
(LOW_ADDRESS + REM_CODE_PH_REG_OFFSET):
begin
//PRDATA = rem_code_ph_reg_p;
+1
View File
@@ -21,6 +21,7 @@ package trck_pkg;
NAV_DATA_9_REG_OFFSET = 60,
TRCK_TIMEOUT_REG_OFFSET = 64,
NAV_SOLVE_TIMER_REG_OFFSET = 68,
NAV_SOLVE_PHASE_REG_OFFSET = 72,
REM_CODE_PH_REG_OFFSET = 76,
MEASUREMENT_META_REG_OFFSET = 80,
MEASUREMENT_SEQUENCE_REG_OFFSET = 84,
+16 -1
View File
@@ -4,6 +4,8 @@ module nav_solve_timer(
input logic resetn,
input logic enable,
input logic [31 : 0] counter_max_val,
input logic phase_seed_valid,
input logic [31 : 0] phase_seed,
input logic bit_valid,
input logic subframe_ready,
input logic [16 : 0] subframe_count,
@@ -84,6 +86,18 @@ module nav_solve_timer(
phase_bit_n = 5'd25;
nav_solve_state_n = nav_solve_timer_pkg::ALIGN;
end
/* A cached, week-qualified HOW/sample anchor lets software identify the
* global navigation-bit phase from the first coherent 20 ms tracking
* measurement. This explicit seed starts COUNT without waiting as long
* as six seconds for another complete LNAV subframe. */
if(phase_seed_valid && enable && trck_busy &&
(counter_max_val != 0) && (phase_seed < counter_max_val)) begin
internal_counter_n = phase_seed;
phase_dividend_n = '0;
phase_remainder_n = '0;
phase_bit_n = '0;
nav_solve_state_n = nav_solve_timer_pkg::COUNT;
end
if(nav_solve_state_p == nav_solve_timer_pkg::ALIGN) begin
if(!enable || !trck_busy || (counter_max_val == 0)) begin
nav_solve_state_n = nav_solve_timer_pkg::IDLE;
@@ -114,7 +128,8 @@ module nav_solve_timer(
internal_counter_n = (internal_counter_p == (counter_max_val - 1)) ? 0 : (internal_counter_p + 1);
end
end
if(nav_solve_state_p == nav_solve_timer_pkg::IDLE) begin
if((nav_solve_state_p == nav_solve_timer_pkg::IDLE) &&
!phase_seed_valid) begin
internal_counter_n = '0;
end
@@ -5,6 +5,10 @@ module nav_solve_timer_tb;
logic resetn = 1'b0;
logic enable = 1'b1;
logic [31:0] counter_max_val;
logic phase_seed_valid_a = 1'b0;
logic phase_seed_valid_b = 1'b0;
logic [31:0] phase_seed_a = '0;
logic [31:0] phase_seed_b = '0;
logic bit_valid = 1'b0;
logic subframe_ready_a = 1'b0;
logic subframe_ready_b = 1'b0;
@@ -22,7 +26,9 @@ module nav_solve_timer_tb;
always #5 clk = ~clk;
nav_solve_timer timer_a (
.clk, .resetn, .enable, .counter_max_val, .bit_valid,
.clk, .resetn, .enable, .counter_max_val,
.phase_seed_valid(phase_seed_valid_a), .phase_seed(phase_seed_a),
.bit_valid,
.subframe_ready(subframe_ready_a),
.subframe_count(subframe_count_a),
.trck_busy, .nav_solve_timer_reached(reached_a),
@@ -30,7 +36,9 @@ module nav_solve_timer_tb;
);
nav_solve_timer timer_b (
.clk, .resetn, .enable, .counter_max_val, .bit_valid,
.clk, .resetn, .enable, .counter_max_val,
.phase_seed_valid(phase_seed_valid_b), .phase_seed(phase_seed_b),
.bit_valid,
.subframe_ready(subframe_ready_b),
.subframe_count(subframe_count_b),
.trck_busy, .nav_solve_timer_reached(reached_b),
@@ -96,11 +104,36 @@ module nav_solve_timer_tb;
repeat(period * 3) pulse_nav_bit();
endtask
task automatic run_cached_seed_scenario(input int unsigned period,
input int unsigned start_bit);
counter_max_val = period;
global_bit = start_bit;
b_active = 1'b1;
resetn = 1'b0;
clock_cycles(3);
resetn = 1'b1;
clock_cycles(2);
/* Software observed start_bit's already-consumed measurement and writes
* its global bit phase. No subframe_ready pulse is provided. */
phase_seed_a = start_bit % period;
phase_seed_b = start_bit % period;
phase_seed_valid_a = 1'b1;
phase_seed_valid_b = 1'b1;
@(posedge clk);
@(negedge clk);
phase_seed_valid_a = 1'b0;
phase_seed_valid_b = 1'b0;
repeat(period * 3) pulse_nav_bit();
endtask
initial begin
run_late_join_scenario(40);
run_late_join_scenario(37);
run_late_join_scenario(512);
run_late_join_scenario(1);
run_cached_seed_scenario(10, 30007);
run_cached_seed_scenario(37, 30111);
$display("nav_solve_timer_tb PASS");
$finish;
end
+36 -13
View File
@@ -17,6 +17,24 @@ commit aligns the separate disciplined accumulator. Later commits update a
signed fixed-point PI rate word; holdover freezes the last rate word. The raw
counter is never slewed or reset by a navigation solution.
At cold power-on the raw counter resets to zero and immediately increments on
every 125 MHz clock, even with no antenna signal. It is therefore a valid
elapsed-time/capture source, but it has no GPS week/TOW meaning by itself.
`STATUS.VALID`, lock, and the physical PPS pin remain suppressed until fresh
LNAV time/ephemeris supports WLS, a coarse mapping is committed, and the lock
qualification completes.
Position differential pseudorange uses the tracking-domain observation
`ABS_SAMPLE - REM_CODE_PHASE/1.023e6`, not per-channel PPS capture-counter
differences. The shared RF sample counter supplies the integer datum and the
code-NCO residue removes its 61 ns integer-sample quantum. The 125 MHz raw
counter remains the monotonic source for epoch assembly, GPS-time/PPS mailbox
anchoring, discipline, and holdover. Thus PPS capture quantization and its CDC
pipeline are not injected into XYZ WLS. RF sample-rate scale error still does
not behave as a common WLS bias, but its effect is limited to the
millisecond-scale inter-channel receive-time span rather than the duration of
a tunnel outage.
`GPS_FRACTION_Q32` is a Q0.32 fraction of one GPS second. `KP` and `KI` are
signed 48-bit Q2.46 discrete coefficients. Q2.46 is required because the
125 MHz clock makes the low-bandwidth integral coefficient smaller than one
@@ -59,11 +77,13 @@ generation remain in hardware.
In a conventional absolute-pseudorange model, receiver clock bias and
receiver clock drift are the level and slope of the same physical clock:
`b_r = c*delta_t_r` and `bdot_r = c*d(delta_t_r)/dt`. The position-WLS bias
in this project is different: subtracting the common capture time and adding
`STARTOFFSET` makes it a reference-satellite-relative datum, approximately
`c*STARTOFFSET - reference_range`, rather than a pure receiver oscillator
offset.
`b_r = c*delta_t_r` and `bdot_r = c*d(delta_t_r)/dt`. The position observations
in this project instead subtract satellite transmit-time differences from
wrap-safe corrected RF-sample differences. The fractional code residue removes
the integer RF-sample quantum. No startup or nominal flight-time constant is
added. The position-WLS bias is therefore approximately
`-reference_range` plus the reference satellite clock correction, rather than
a pure receiver oscillator offset.
The Doppler WLS still solves a fourth, common receiver clock/frequency-drift
state. The PPS pre-filter does not use that state directly as the derivative
@@ -83,12 +103,14 @@ The APB register offsets live in `pps_pkg.sv`. Multiword input fields are
shadow registers and must be followed by one `COMMIT` write. A new mailbox
must not be written while `STATUS.COMMIT_BUSY` is set.
The `capture_i` vector is synchronous to `PCLK`. Each pulse stores the
Gray-synchronized 125 MHz counter and increments that channel's capture
sequence. The two-stage crossing adds a small fixed timestamp pipeline offset;
the SoC-level connection must use the exact PCLK event that updates
`ABS_SAMPLE/REM_CODE_PHASE`. `CAPTURE_COUNTER` initially refers to the integer
RF-sample event. Before writing `EPOCH_COUNTER`, navigation software subtracts
The `capture_i` vector is synchronous to `pps_clk`, which is the same 125 MHz
clock used by every tracking channel. Each one-cycle pulse latches the native
raw counter and increments that channel's source-domain sequence, so it cannot
be missed or phase-quantized by the slower APB clock. The stable bundled
counter crosses to PCLK first and its visible sequence is delayed one extra
PCLK cycle; firmware's sequence/data/sequence read rejects any capture that
changes during an APB transaction. `CAPTURE_COUNTER` initially refers to the
integer RF-sample event. Before writing `EPOCH_COUNTER`, navigation software subtracts
```
round((REM_CODE_PHASE / 1.023e6) * (125e6 / RF_HZ))
@@ -96,8 +118,9 @@ round((REM_CODE_PHASE / 1.023e6) * (125e6 / RF_HZ))
counter ticks, so the counter and corrected WLS observation refer to the same
fractional code boundary. This removes an otherwise variable 0--61 ns term.
The remaining PCLK/Gray pipeline delay is fixed and must be measured once on
hardware.
The PPS timebase reset is independent of RF/MMCM readiness, so its raw counter,
disciplined accumulator and holdover rate survive a tunnel or antenna-clock
loss while acquisition/tracking DSP domains reset and restart.
## APB register map
+58 -21
View File
@@ -46,10 +46,18 @@ module pps_wrapper #(
logic commit_overrun_p, commit_overrun_n;
logic diag_req_toggle_p, diag_req_toggle_n;
logic [PPS_COUNTER_WIDTH-1:0] capture_counter_p [NUM_CAPTURE_CHANNELS];
logic [PPS_COUNTER_WIDTH-1:0] capture_counter_n [NUM_CAPTURE_CHANNELS];
logic [PPS_SEQUENCE_WIDTH-1:0] capture_sequence_p [NUM_CAPTURE_CHANNELS];
logic [PPS_SEQUENCE_WIDTH-1:0] capture_sequence_n [NUM_CAPTURE_CHANNELS];
logic [PPS_COUNTER_WIDTH-1:0] capture_counter_pps [NUM_CAPTURE_CHANNELS];
logic [PPS_SEQUENCE_WIDTH-1:0] capture_sequence_pps [NUM_CAPTURE_CHANNELS];
(* ASYNC_REG = "TRUE" *) logic [PPS_COUNTER_WIDTH-1:0]
capture_counter_sync1_p [NUM_CAPTURE_CHANNELS];
(* ASYNC_REG = "TRUE" *) logic [PPS_COUNTER_WIDTH-1:0]
capture_counter_sync2_p [NUM_CAPTURE_CHANNELS];
(* ASYNC_REG = "TRUE" *) logic [PPS_SEQUENCE_WIDTH-1:0]
capture_sequence_sync1_p [NUM_CAPTURE_CHANNELS];
(* ASYNC_REG = "TRUE" *) logic [PPS_SEQUENCE_WIDTH-1:0]
capture_sequence_sync2_p [NUM_CAPTURE_CHANNELS];
logic [PPS_SEQUENCE_WIDTH-1:0]
capture_sequence_visible_p [NUM_CAPTURE_CHANNELS];
logic [PPS_COUNTER_WIDTH-1:0] raw_snapshot_p, raw_snapshot_n;
logic [PPS_COUNTER_WIDTH-1:0] raw_counter_pps;
@@ -93,17 +101,41 @@ module pps_wrapper #(
logic apb_write_access;
logic apb_read_access;
logic [31:0] paddr_extended;
integer channel_pps;
integer channel_seq;
integer channel_comb;
assign raw_counter_pclk = pps_gray_to_binary(raw_gray_sync2_p);
assign raw_counter_gray_pps = raw_counter_pps ^ (raw_counter_pps >> 1);
assign commit_busy = commit_req_toggle_p ^ commit_ack_sync2_p;
assign diag_busy = diag_req_toggle_p ^ diag_ack_sync2_p;
assign apb_write_access = (apb_bus_ps == PPS_APB_STATE_ACCESS) && PWRITE;
assign apb_read_access = (apb_bus_ps == PPS_APB_STATE_ACCESS) && !PWRITE;
assign paddr_extended = {{(32-ADDR_BUS_WIDTH){1'b0}}, PADDR};
/* Capture events are native to this same 125 MHz tracking/PPS domain. Latch
* the counter here so an 8 ns event cannot be missed or quantized by PCLK.
* Counter/sequence remain stable for an entire navigation-solve period; the
* delayed visible sequence below is the bundled-data validity marker. */
always_ff @(posedge pps_clk or negedge pps_resetn) begin
if (!pps_resetn) begin
raw_counter_gray_pps <= '0;
for (channel_pps = 0; channel_pps < NUM_CAPTURE_CHANNELS;
channel_pps = channel_pps + 1) begin
capture_counter_pps[channel_pps] <= '0;
capture_sequence_pps[channel_pps] <= '0;
end
end else begin
raw_counter_gray_pps <= raw_counter_pps ^ (raw_counter_pps >> 1);
for (channel_pps = 0; channel_pps < NUM_CAPTURE_CHANNELS;
channel_pps = channel_pps + 1) begin
if (capture_i[channel_pps]) begin
capture_counter_pps[channel_pps] <= raw_counter_pps;
capture_sequence_pps[channel_pps] <=
capture_sequence_pps[channel_pps] + 1'b1;
end
end
end
end
always_ff @(posedge pps_clk or negedge pps_resetn) begin
if (!pps_resetn) begin
control_sync1_p <= '0;
@@ -180,8 +212,11 @@ module pps_wrapper #(
rate_word_sync1_p <= '0;
rate_word_sync2_p <= '0;
for (channel_seq = 0; channel_seq < NUM_CAPTURE_CHANNELS; channel_seq = channel_seq + 1) begin
capture_counter_p[channel_seq] <= '0;
capture_sequence_p[channel_seq] <= '0;
capture_counter_sync1_p[channel_seq] <= '0;
capture_counter_sync2_p[channel_seq] <= '0;
capture_sequence_sync1_p[channel_seq] <= '0;
capture_sequence_sync2_p[channel_seq] <= '0;
capture_sequence_visible_p[channel_seq] <= '0;
end
end else begin
apb_bus_ps <= apb_bus_ns;
@@ -233,8 +268,18 @@ module pps_wrapper #(
rate_word_sync1_p <= diag_rate_word_pps;
rate_word_sync2_p <= rate_word_sync1_p;
for (channel_seq = 0; channel_seq < NUM_CAPTURE_CHANNELS; channel_seq = channel_seq + 1) begin
capture_counter_p[channel_seq] <= capture_counter_n[channel_seq];
capture_sequence_p[channel_seq] <= capture_sequence_n[channel_seq];
capture_counter_sync1_p[channel_seq] <=
capture_counter_pps[channel_seq];
capture_counter_sync2_p[channel_seq] <=
capture_counter_sync1_p[channel_seq];
capture_sequence_sync1_p[channel_seq] <=
capture_sequence_pps[channel_seq];
capture_sequence_sync2_p[channel_seq] <=
capture_sequence_sync1_p[channel_seq];
/* Publish sequence one PCLK later than the payload. Firmware reads
* sequence/data/sequence and therefore never accepts a mixed capture. */
capture_sequence_visible_p[channel_seq] <=
capture_sequence_sync2_p[channel_seq];
end
end
end
@@ -266,14 +311,6 @@ module pps_wrapper #(
commit_overrun_n = commit_overrun_p;
diag_req_toggle_n = diag_req_toggle_p;
raw_snapshot_n = raw_snapshot_p;
for (channel_comb = 0; channel_comb < NUM_CAPTURE_CHANNELS; channel_comb = channel_comb + 1) begin
capture_counter_n[channel_comb] = capture_counter_p[channel_comb];
capture_sequence_n[channel_comb] = capture_sequence_p[channel_comb];
if (capture_i[channel_comb]) begin
capture_counter_n[channel_comb] = raw_counter_pclk;
capture_sequence_n[channel_comb] = capture_sequence_p[channel_comb] + 32'd1;
end
end
if (apb_read_access && (paddr_extended == (LOW_ADDRESS + PPS_RAW_COUNTER_LO_REG_OFFSET)))
raw_snapshot_n = raw_counter_pclk;
@@ -388,9 +425,9 @@ module pps_wrapper #(
PRDATA = '0;
PRDATA[CAPTURE_SEL_WIDTH-1:0] = capture_select_p;
end
(LOW_ADDRESS + PPS_CAPTURE_COUNTER_LO_OFFSET): PRDATA = capture_counter_p[capture_select_p][31:0];
(LOW_ADDRESS + PPS_CAPTURE_COUNTER_HI_OFFSET): PRDATA = capture_counter_p[capture_select_p][63:32];
(LOW_ADDRESS + PPS_CAPTURE_SEQUENCE_OFFSET): PRDATA = capture_sequence_p[capture_select_p];
(LOW_ADDRESS + PPS_CAPTURE_COUNTER_LO_OFFSET): PRDATA = capture_counter_sync2_p[capture_select_p][31:0];
(LOW_ADDRESS + PPS_CAPTURE_COUNTER_HI_OFFSET): PRDATA = capture_counter_sync2_p[capture_select_p][63:32];
(LOW_ADDRESS + PPS_CAPTURE_SEQUENCE_OFFSET): PRDATA = capture_sequence_visible_p[capture_select_p];
(LOW_ADDRESS + PPS_EPOCH_COUNTER_LO_OFFSET): PRDATA = epoch_counter_shadow_p[31:0];
(LOW_ADDRESS + PPS_EPOCH_COUNTER_HI_OFFSET): PRDATA = epoch_counter_shadow_p[63:32];
(LOW_ADDRESS + PPS_GPS_SECONDS_LO_OFFSET): PRDATA = gps_seconds_shadow_p[31:0];
+15 -9
View File
@@ -114,6 +114,15 @@ module pps_wrapper_tb;
end
endtask
task automatic pulse_native_capture(input int unsigned channel_index);
begin
/* Real tracking capture is exactly one shared 125 MHz PPS-clock pulse,
* potentially narrower than a PCLK period. */
@(negedge pps_clk); capture_i[channel_index] = 1'b1;
@(negedge pps_clk); capture_i[channel_index] = 1'b0;
end
endtask
task automatic wait_commit_idle;
logic [31:0] status;
int timeout;
@@ -238,9 +247,8 @@ module pps_wrapper_tb;
check_true((dut.i_pps_core.raw_counter - raw_snapshot_value) < 100,
"raw-counter low-read snapshot keeps the high word coherent");
@(negedge PCLK); capture_i[1] = 1'b1;
@(negedge PCLK); capture_i[1] = 1'b0;
repeat (3) @(posedge PCLK);
pulse_native_capture(1);
repeat (5) @(posedge PCLK);
read_capture(1, capture_counter, capture_sequence_id);
check_true(capture_sequence_id == 1, "channel capture sequence increments");
check_true(capture_counter != 0, "channel capture stores the free-running counter");
@@ -311,9 +319,8 @@ module pps_wrapper_tb;
// Build the next navigation reference from a new captured hardware epoch.
repeat (40) @(posedge pps_clk);
@(negedge PCLK); capture_i[2] = 1'b1;
@(negedge PCLK); capture_i[2] = 1'b0;
repeat (3) @(posedge PCLK);
pulse_native_capture(2);
repeat (5) @(posedge PCLK);
read_capture(2, second_capture, second_capture_sequence);
check_true(second_capture_sequence == 1, "independent capture bank per tracking channel");
phase_ticks = (second_capture - pps_edge_counter) + 2;
@@ -346,9 +353,8 @@ module pps_wrapper_tb;
"new diagnostic snapshot advances the observed time");
// Replaying sequence 7 must acknowledge the mailbox but not update PI.
@(negedge PCLK); capture_i[2] = 1'b1;
@(negedge PCLK); capture_i[2] = 1'b0;
repeat (3) @(posedge PCLK);
pulse_native_capture(2);
repeat (5) @(posedge PCLK);
read_capture(2, second_capture, second_capture_sequence);
check_true(second_capture_sequence == 2, "repeated channel capture advances its sequence");
pps_sw_commit(second_capture, 102, ticks_to_fraction_q32(phase_ticks), 7, 1'b0);
+4 -2
View File
@@ -105,8 +105,10 @@ gelir. Bu ortak state olmadan oscillator hatasi receiver hareketinin bir
parcasi sanilirdi.
Bu projedeki position WLS bias'i ise mutlak receiver clock bias'i degildir.
Ortak capture zamani cikarildigi ve `STARTOFFSET` eklendigi icin
referans-uyduya bagli goreli bir datum cozer. Bu nedenle Doppler WLS
Wrap-safe corrected RF-sample farkindan satellite transmit-time farki
cikarildigi icin, sabit startup offset'i olmadan referans-uyduya bagli goreli
bir datum cozer. Fractional code residue integer RF-sample quantization'ini
giderir. Bu nedenle Doppler WLS
`clock_drift_mps` state'i, position WLS `clock_bias_m` degerinin turevi olarak
dogrudan baglanmaz.
@@ -449,6 +449,7 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
val trackingClockNode = BundleBridgeSink[Clock]()
val afeClockNode = BundleBridgeSink[Clock]()
val rfDataNode = BundleBridgeSink[UInt]()
val clockReadyNode = BundleBridgeSink[Bool]()
val ppsOutNode = BundleBridgeSource(() => Bool())
lazy val module = new Impl
@@ -464,21 +465,38 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
// GDB ndmreset reaches this reset. Deassert it inside each GPS clock domain so
// APB, acquisition, tracking, and RF/AFE logic do not leave reset on an async edge.
val gpsDomainReset = reset.asBool || !clockReadyNode.bundle
// Keep the APB/fabric side alive when the external RF clock disappears so
// the CPU and its RAM ephemeris cache survive. Only the clock-dependent GPS
// DSP domains receive the lock-qualified local reset.
val apbReset = ResetCatchAndSync(clock, reset.asBool, 3, "gps_apb_reset")
val acqReset = ResetCatchAndSync(acqClockNode.bundle, reset.asBool, 3, "gps_acq_reset")
val trckReset = ResetCatchAndSync(trackingClockNode.bundle, reset.asBool, 3, "gps_trck_reset")
val afeReset = ResetCatchAndSync(afeClockNode.bundle, reset.asBool, 3, "gps_afe_reset")
val acqReset = ResetCatchAndSync(acqClockNode.bundle, gpsDomainReset, 3, "gps_acq_reset")
val trckReset = ResetCatchAndSync(trackingClockNode.bundle, gpsDomainReset, 3, "gps_trck_reset")
val afeReset = ResetCatchAndSync(afeClockNode.bundle, gpsDomainReset, 3, "gps_afe_reset")
// PPS uses the fabric-derived tracking clock but must keep counting during
// RF/MMCM loss. Only a real SoC reset may reset its monotonic timebase.
val ppsReset = ResetCatchAndSync(trackingClockNode.bundle, reset.asBool, 3, "gps_pps_reset")
val apbResetn = !apbReset
val acqResetn = !acqReset
val trckResetn = !trckReset
val afeResetn = !afeReset
val ppsResetn = !ppsReset
afe.io.afe_rstn := afeResetn
afe.io.trk_rstn := trckResetn
afe.io.afe_clk := afeClockNode.bundle
afe.io.trk_clk := trackingClockNode.bundle
afe.io.start_i := acq.io.afe_start
// A disappearing RF clock resets the AFE while the APB register bank is
// intentionally retained. Generate one local restart pulse when the RF
// domain returns; the normal software pulse remains valid at cold boot.
val afeAutoStart = withClockAndReset(afeClockNode.bundle, afeReset) {
val started = RegInit(false.B)
val pulse = !started
started := true.B
pulse
}
afe.io.start_i := acq.io.afe_start || afeAutoStart
afe.io.sample_counter_clear_i := acq.io.sample_counter_clear
acq.io.PCLK := clock
@@ -523,7 +541,7 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
pps.io.PWRITE := apb.pwrite
pps.io.PWDATA := apb.pwdata(31, 0)
pps.io.pps_clk := trackingClockNode.bundle
pps.io.pps_resetn := trckResetn
pps.io.pps_resetn := ppsResetn
pps.io.capture_i := VecInit(tracking.map(_.io.pps_capture)).asUInt
ppsOutNode.bundle := pps.io.pps_out
@@ -599,6 +617,7 @@ class VCU118RocketSystem(implicit p: Parameters) extends RocketSubsystem
val gpsTrackingClockSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Clock()))
val gpsRfClockSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Clock()))
val gpsRfDataSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => UInt(4.W)))
val gpsClockReadySourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Bool()))
private val pmodIRQDevice = new SimpleDevice("pmod-sensor-irq", Seq("rocket,pmod-sensor-irq"))
val pmodIRQIntNode = IntSourceNode(Seq(IntSourcePortParameters(Seq(
IntSourceParameters(IntRange(0, 1), pmodIRQDevice.int)))))
@@ -680,6 +699,7 @@ class VCU118RocketSystem(implicit p: Parameters) extends RocketSubsystem
gpsTrackingClockSourceNode.foreach { source => dev.trackingClockNode := source }
gpsRfClockSourceNode.foreach { source => dev.afeClockNode := source }
gpsRfDataSourceNode.foreach { source => dev.rfDataNode := source }
gpsClockReadySourceNode.foreach { source => dev.clockReadyNode := source }
dev
}
val gpsPPSSinkNode = gps.map(_.ppsOutNode.makeSink())
@@ -714,6 +734,7 @@ class VCU118RocketSystemModuleImp[+L <: VCU118RocketSystem](_outer: L)
val gps_trck_clk = _outer.gpsTrackingClockSourceNode.map(_.makeIO("gps_trck_clk"))
val gps_rf_clk = _outer.gpsRfClockSourceNode.map(_.makeIO("gps_rf_clk"))
val gps_rf_data = _outer.gpsRfDataSourceNode.map(_.makeIO("gps_rf_data"))
val gps_clock_ready = _outer.gpsClockReadySourceNode.map(_.makeIO("gps_clock_ready"))
val gps_pps = _outer.gpsPPSSinkNode.map(_.makeIO("gps_pps"))
_outer.pmodIRQIntNode.out.foreach { case (out, _) =>
+4 -3
View File
@@ -18,9 +18,9 @@ JDK_SHA256="d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331"
MILL_URL="https://github.com/com-lihaoyi/mill/releases/download/$MILL_VERSION/$MILL_VERSION"
MILL_SHA256="ebe83649cd63acd33608f6d14452c074be2ed7d00cd2f69f88573d3f6909774f"
CIRCT_ARCHIVE="circt-full-shared-linux-x64.tar.gz"
CIRCT_ARCHIVE="firrtl-bin-linux-x64.tar.gz"
CIRCT_URL="https://github.com/llvm/circt/releases/download/firtool-$FIRTOOL_VERSION/$CIRCT_ARCHIVE"
CIRCT_SHA256="dde442dcd0ada92e6f7db26f4744667471f7be61d10f2876297efb435fc9a5ec"
CIRCT_SHA256="10bf9b135a65e5d93fb0555e967eac4258400ee629731ba68a69be6b7f9781ce"
Z3_ARCHIVE="z3-$Z3_VERSION-x64-glibc-2.39.zip"
Z3_URL="https://github.com/Z3Prover/z3/releases/download/z3-$Z3_VERSION/$Z3_ARCHIVE"
@@ -85,7 +85,8 @@ if [[ ! -x "$TOOLS_ROOT/mill/$MILL_VERSION/mill" ]]; then
"${SUDO[@]}" install -m 0755 "$work_dir/mill" "$TOOLS_ROOT/mill/$MILL_VERSION/mill"
fi
if [[ ! -x "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" ]]; then
if ! "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" --version \
>/dev/null 2>&1; then
download "$CIRCT_URL" "$work_dir/$CIRCT_ARCHIVE" "$CIRCT_SHA256"
mkdir -p "$work_dir/circt"
tar -xzf "$work_dir/$CIRCT_ARCHIVE" -C "$work_dir/circt"
+76
View File
@@ -3,6 +3,8 @@
#include "bsp/mmio.h"
#include "bsp/soc.h"
#include <stddef.h>
#define SPI_CTRL (BSP_SPI_BASE + 0x00u)
#define SPI_TX (BSP_SPI_BASE + 0x04u)
#define SPI_RX (BSP_SPI_BASE + 0x08u)
@@ -11,6 +13,15 @@
#define SPI_CTRL_BUSY 0x1u
#define SPI_CTRL_CSN 0x2u
#define FLASH_CMD_WRITE_ENABLE 0x06u
#define FLASH_CMD_READ_STATUS 0x05u
#define FLASH_CMD_PAGE_PROGRAM_4B 0x12u
#define FLASH_CMD_SECTOR_ERASE_4B 0xdcu
#define FLASH_STATUS_WIP 0x01u
#define FLASH_STATUS_WEL 0x02u
#define FLASH_PAGE_SIZE 256u
#define FLASH_READY_POLL_LIMIT 5000000u
static void delay_cycles(uint32_t cycles)
{
for (volatile uint32_t i = 0; i < cycles; ++i) {
@@ -47,6 +58,32 @@ static void flash_cmd(uint8_t cmd)
spi_select(0);
}
static uint8_t flash_read_status(void)
{
uint8_t status;
spi_select(1);
(void)spi_xfer8(FLASH_CMD_READ_STATUS);
status = spi_xfer8(0u);
spi_select(0);
return status;
}
static bool flash_write_enable(void)
{
flash_cmd(FLASH_CMD_WRITE_ENABLE);
return (flash_read_status() & FLASH_STATUS_WEL) != 0u;
}
static bool flash_wait_operation(void)
{
for(uint32_t poll = 0u; poll < FLASH_READY_POLL_LIMIT; ++poll) {
if((flash_read_status() & FLASH_STATUS_WIP) == 0u) {
return true;
}
}
return false;
}
void bsp_spi_flash_init(void)
{
bsp_write32(SPI_CTRL, SPI_CTRL_CSN);
@@ -88,3 +125,42 @@ void bsp_spi_flash_read(uint32_t flash_addr, void *dst, uint32_t len)
}
spi_select(0);
}
bool bsp_spi_flash_program_page(
uint32_t flash_addr, const void *src, uint32_t len)
{
if((src == NULL) || (len == 0u) || (len > FLASH_PAGE_SIZE) ||
((flash_addr & (FLASH_PAGE_SIZE - 1u)) + len > FLASH_PAGE_SIZE) ||
!flash_write_enable()) {
return false;
}
const uint8_t *data = (const uint8_t *)src;
spi_select(1);
(void)spi_xfer8(FLASH_CMD_PAGE_PROGRAM_4B);
(void)spi_xfer8((uint8_t)(flash_addr >> 24));
(void)spi_xfer8((uint8_t)(flash_addr >> 16));
(void)spi_xfer8((uint8_t)(flash_addr >> 8));
(void)spi_xfer8((uint8_t)flash_addr);
for(uint32_t i = 0u; i < len; ++i) {
(void)spi_xfer8(data[i]);
}
spi_select(0);
return flash_wait_operation();
}
bool bsp_spi_flash_erase_sector(uint32_t flash_addr)
{
if(!flash_write_enable()) {
return false;
}
spi_select(1);
(void)spi_xfer8(FLASH_CMD_SECTOR_ERASE_4B);
(void)spi_xfer8((uint8_t)(flash_addr >> 24));
(void)spi_xfer8((uint8_t)(flash_addr >> 16));
(void)spi_xfer8((uint8_t)(flash_addr >> 8));
(void)spi_xfer8((uint8_t)flash_addr);
spi_select(0);
return flash_wait_operation();
}
+4
View File
@@ -1,11 +1,15 @@
#ifndef BSP_SPI_FLASH_H
#define BSP_SPI_FLASH_H
#include <stdbool.h>
#include <stdint.h>
void bsp_spi_flash_init(void);
void bsp_spi_flash_reset(void);
uint32_t bsp_spi_flash_read_id(void);
void bsp_spi_flash_read(uint32_t flash_addr, void *dst, uint32_t len);
bool bsp_spi_flash_program_page(
uint32_t flash_addr, const void *src, uint32_t len);
bool bsp_spi_flash_erase_sector(uint32_t flash_addr);
#endif
+10 -3
View File
@@ -30,6 +30,8 @@ APP_OBJS := $(addprefix $(BUILD_DIR)/,$(APP_SRCS:=.o))
COMMON_OBJS := $(BUILD_DIR)/common_start.S.o $(BUILD_DIR)/common_syscalls.c.o
BSP_OBJS := $(addprefix $(BUILD_DIR)/bsp_,$(BSP_SRCS:=.o))
OBJS := $(COMMON_OBJS) $(BSP_OBJS) $(APP_OBJS)
DEPS := $(OBJS:.o=.d)
DEPFLAGS := -MMD -MP
.PHONY: all clean FORCE
@@ -64,15 +66,15 @@ $(BUILD_DIR)/common_start.S.o: $(COMMON_DIR)/start.S $(CONFIG_STAMP) | $(BUILD_D
$(BUILD_DIR)/common_syscalls.c.o: $(COMMON_DIR)/syscalls.c $(CONFIG_STAMP) | $(BUILD_DIR)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
$(BUILD_DIR)/bsp_%.c.o: $(BSP_DIR)/drivers/%.c $(CONFIG_STAMP) | $(BUILD_DIR)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
$(BUILD_DIR)/%.c.o: %.c $(CONFIG_STAMP) | $(BUILD_DIR)
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
$(BUILD_DIR)/%.S.o: %.S $(CONFIG_STAMP) | $(BUILD_DIR)
mkdir -p $(dir $@)
@@ -87,5 +89,10 @@ $(BUILD_DIR)/$(TARGET).img: $(BUILD_DIR)/$(TARGET).elf
$(BUILD_DIR)/$(TARGET).dump: $(BUILD_DIR)/$(TARGET).elf
$(OBJDUMP) -d -S $< > $@
# Header-only firmware changes must invalidate their objects. Without these
# generated dependencies, `debug-load` could silently reload an older ELF even
# though a configuration header had changed.
-include $(DEPS)
clean:
rm -rf $(BUILD_DIR)
+5 -3
View File
@@ -131,9 +131,11 @@ Klasik mutlak-pseudorange modelinde receiver clock bias
error'i yaklaşık `29.98 m/s` ortak Doppler drift'idir; dördüncü state
çözülmezse bunun bir bölümü platform hızı sanılır.
Bu projedeki position WLS bias'i ise capture'lar ortak receiver zamanına göre
farklandığı ve `STARTOFFSET` eklendiği için saf receiver clock bias'i değil,
referans-uyduya bağlı göreli bir datumdur. Bu yüzden position
Bu projedeki position WLS bias'i ise wrap-safe corrected RF-sample farkindan
satellite transmit-time farki cikarilarak kuruldugu icin saf receiver clock
bias'i değil, yaklaşık `-reference_range` olan referans-uyduya bağlı göreli
bir datumdur. Fractional code residue 61 ns integer RF-sample quantization'ini
giderir; sabit startup offset'i kullanilmaz. Bu yüzden position
`clock_bias_m` ile Doppler `clock_drift_mps` arasında doğrudan türev kısıtı
kurulmaz. Doppler WLS ayrı bir normal equation kullanır fakat LOS vektörleri
için güncel position WLS receiver konumuna, uydu konum/hız/clock drift'i için
+6 -3
View File
@@ -5,6 +5,7 @@ LDLIBS ?= -lm
GPS_TIME_SOURCE := ../../gps_system/src/gps/gps_time_anchor.c
GPS_EPOCH_SOURCE := ../../gps_system/src/gps/gps_nav_epoch.c
GPS_EPHEMERIS_CACHE_SOURCE := ../../gps_system/src/gps/gps_ephemeris_cache.c
ESKF_SOURCES := ../../gps_system/src/fusion/eskf.c ../../gps_system/src/fusion/fusion_motion.c ../../gps_system/src/fusion/fusion_rest_detector.c ../../gps_system/src/fusion/fusion_engine.c ../../gps_system/src/fusion/fusion_gps_adapter.c ../../gps_system/src/fusion/fusion_bus.c
IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
@@ -12,8 +13,8 @@ IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
all: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test gps_failure_scenario_test
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) $(GPS_EPHEMERIS_CACHE_SOURCE)
$(CC) $(CFLAGS) -Wno-comment -I../../gps_system/tests/fake_bsp $^ -o $@ $(LDLIBS)
eskf_motion_test: eskf_motion_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
@@ -72,8 +73,10 @@ scenario-logs: platform_scenario_matrix_test
sanitize:
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment \
-I../../gps_system/header standalone_gps_integrity_test.c \
$(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) \
-I../../gps_system/tests/fake_bsp \
$(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE) $(GPS_EPHEMERIS_CACHE_SOURCE) \
-o standalone_gps_integrity_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./standalone_gps_integrity_test_san
@@ -10,12 +10,15 @@
#include <time.h>
#include "gps/gps_nav_epoch.h"
#include "gps/gps_ephemeris_cache.h"
#include "gps/gps_time_anchor.h"
#define MAX_SATELLITES 9
#define STATE_DIM 4
#define PI 3.14159265358979323846
#define TEST_PI 3.14159265358979323846
#define RECEIVER_SAMPLES_PER_SECOND 16368000.0
#define RECEIVER_COUNTER_PPM 0.20
#define SPEED_OF_LIGHT_MPS 299792458.0
#define EPOCH_SAMPLE_WINDOW 409200u
#define EPOCH_TOW_TOLERANCE_SEC 0.001
@@ -229,8 +232,8 @@ static void create_measurements(measurement_t measurements[MAX_SATELLITES],
enu_axes(latitude_rad, longitude_rad, axes);
for (unsigned index = 0; index < count; index++) {
double azimuth = azimuth_deg[index] * PI / 180.0;
double elevation = elevation_deg[index] * PI / 180.0;
double azimuth = azimuth_deg[index] * TEST_PI / 180.0;
double elevation = elevation_deg[index] * TEST_PI / 180.0;
double los_enu[3] = {
cos(elevation) * sin(azimuth),
cos(elevation) * cos(azimuth),
@@ -572,6 +575,7 @@ typedef struct {
bool have_sf[3];
unsigned char iod[3];
bool ephemeris_valid;
unsigned ephemeris_decode_count;
} linux_nav_channel_t;
typedef struct {
@@ -582,6 +586,9 @@ typedef struct {
unsigned fixes;
unsigned fde_fixes;
unsigned epoch_exclusions;
unsigned sample_observations;
unsigned ephemeris_cache_restores;
unsigned ephemeris_decodes;
double east_sum_m;
double north_sum_m;
double up_sum_m;
@@ -627,7 +634,7 @@ static double synthetic_rng_gaussian(synthetic_rng_t* rng)
{
double u1 = synthetic_rng_uniform(rng);
double u2 = synthetic_rng_uniform(rng);
return sqrt(-2.0 * log(u1)) * cos(2.0 * PI * u2);
return sqrt(-2.0 * log(u1)) * cos(2.0 * TEST_PI * u2);
}
static void linux_nav_receive_ephemeris_subframe(linux_nav_channel_t* channel,
@@ -640,13 +647,161 @@ static void linux_nav_receive_ephemeris_subframe(linux_nav_channel_t* channel,
unsigned slot = subframe_id - 1u;
channel->have_sf[slot] = true;
channel->iod[slot] = iod;
if (channel->have_sf[0] && channel->have_sf[1] && channel->have_sf[2] &&
if (!channel->ephemeris_valid &&
channel->have_sf[0] && channel->have_sf[1] && channel->have_sf[2] &&
channel->iod[0] == channel->iod[1] &&
channel->iod[1] == channel->iod[2]) {
channel->ephemeris_valid = true;
channel->ephemeris_decode_count++;
}
}
static bool linux_nav_cache_ephemeris(gps_ephemeris_cache_t* cache,
unsigned satellite,
unsigned char iod,
uint64_t tick)
{
gps_ephemeris_t ephemeris;
memset(&ephemeris, 0, sizeof(ephemeris));
ephemeris.SVID = (unsigned char)satellite;
ephemeris.iode = iod;
ephemeris.ephemeris_valid = true;
ephemeris.toe.week = 2300;
ephemeris.toe.sec = 604000.0;
return gps_ephemeris_cache_store(
cache, satellite, &ephemeris, tick);
}
static bool linux_nav_restore_ephemeris(gps_ephemeris_cache_t* cache,
linux_nav_channel_t* channel,
unsigned satellite,
uint64_t tick)
{
gps_ephemeris_t ephemeris;
if (!gps_ephemeris_cache_restore(
cache, satellite, tick, &ephemeris)) {
return false;
}
channel->ephemeris_valid = ephemeris.ephemeris_valid;
memset(channel->have_sf, 0, sizeof(channel->have_sf));
memset(channel->iod, 0, sizeof(channel->iod));
return channel->ephemeris_valid;
}
static bool synthetic_apply_receiver_sample_datum(
measurement_t measurements[MAX_SATELLITES],
unsigned count,
unsigned epoch,
int week,
double tow_sec,
double absolute_clock_bias_m,
double receiver_counter_ppm)
{
if (count == 0u) {
return false;
}
const double actual_sample_hz =
RECEIVER_SAMPLES_PER_SECOND *
(1.0 + receiver_counter_ppm * 1.0e-6);
/* Production unwraps every channel's shared 32-bit RF sample latch around
* the epoch reference. Keep an arbitrary moving origin here to prove that
* neither boot time nor a counter rollover is a range datum. */
const double epoch_sample = 1000000000.375 +
fmod((double)epoch * 0.2 * actual_sample_hz, 4294967296.0);
double capture_sample[MAX_SATELLITES];
for (unsigned satellite = 0u; satellite < count; satellite++) {
double propagation_sec =
(measurements[satellite].pseudorange_m - absolute_clock_bias_m) /
SPEED_OF_LIGHT_MPS;
if (!isfinite(propagation_sec) || propagation_sec < 0.0) {
return false;
}
capture_sample[satellite] = epoch_sample +
propagation_sec * actual_sample_hz;
}
for (unsigned satellite = 0u; satellite < count; satellite++) {
double relative_observation_sec = 0.0;
if (!gps_time_sample_differential_observation(
week, tow_sec, capture_sample[0],
week, tow_sec, capture_sample[satellite],
RECEIVER_SAMPLES_PER_SECOND,
&relative_observation_sec)) {
return false;
}
measurements[satellite].pseudorange_m =
relative_observation_sec * SPEED_OF_LIGHT_MPS;
}
return true;
}
static void verify_common_start_offset_cannot_change_position(
const measurement_t absolute_measurements[MAX_SATELLITES],
unsigned count,
const double receiver_truth[3],
double latitude_rad,
double longitude_rad)
{
measurement_t differential[MAX_SATELLITES];
measurement_t shifted[MAX_SATELLITES];
const double old_start_offset_m =
0.068802 * SPEED_OF_LIGHT_MPS;
const double reference_observation_m =
absolute_measurements[0].pseudorange_m;
for (unsigned row = 0u; row < count; row++) {
differential[row] = absolute_measurements[row];
shifted[row] = absolute_measurements[row];
differential[row].pseudorange_m -= reference_observation_m;
shifted[row].pseudorange_m =
differential[row].pseudorange_m + old_start_offset_m;
}
double differential_seed[STATE_DIM] = {
receiver_truth[0] + 120.0,
receiver_truth[1] - 90.0,
receiver_truth[2] + 60.0,
-20200000.0
};
double shifted_seed[STATE_DIM] = {
receiver_truth[0] + 120.0,
receiver_truth[1] - 90.0,
receiver_truth[2] + 60.0,
400000.0
};
solution_t differential_solution;
solution_t differential_full;
solution_t shifted_solution;
solution_t shifted_full;
bool differential_ok = solve_with_fde(
differential, count, differential_seed,
latitude_rad, longitude_rad,
&differential_solution, &differential_full);
bool shifted_ok = solve_with_fde(
shifted, count, shifted_seed,
latitude_rad, longitude_rad,
&shifted_solution, &shifted_full);
CHECK(differential_ok && shifted_ok &&
differential_solution.solution_valid &&
shifted_solution.solution_valid,
"common-datum WLS invariance solutions must be valid");
if (!differential_ok || !shifted_ok) {
return;
}
double position_delta[3] = {
shifted_solution.state[0] - differential_solution.state[0],
shifted_solution.state[1] - differential_solution.state[1],
shifted_solution.state[2] - differential_solution.state[2]
};
CHECK(vector_norm3(position_delta) < 1.0e-4,
"68.802 ms common offset changed WLS XYZ");
CHECK(fabs((shifted_solution.state[3] -
differential_solution.state[3]) -
old_start_offset_m) < 1.0e-4,
"68.802 ms common offset was not confined to WLS clock bias");
}
static unsigned synthetic_stage_for_epoch(unsigned epoch)
{
static const unsigned ends[SYNTHETIC_STAGE_COUNT] =
@@ -773,6 +928,7 @@ static void synthetic_print_metrics(synthetic_stage_metrics_t metrics)
}
printf("%-21s epochs=%3u fixes=%3u avg=%4.1f->%4.1f "
"sample=%4u cache=%2u decode=%2u "
"epoch_excl=%3u fde=%3u hRMSE=%6.2f hSTD=%6.2f "
"hP95=%6.2f hMAX=%6.2f vRMSE=%6.2f vSTD=%6.2f\n",
metrics.name, metrics.epochs, metrics.fixes,
@@ -780,6 +936,9 @@ static void synthetic_print_metrics(synthetic_stage_metrics_t metrics)
? (double)metrics.candidate_total / (double)metrics.epochs : 0.0,
metrics.epochs != 0u
? (double)metrics.selected_total / (double)metrics.epochs : 0.0,
metrics.sample_observations,
metrics.ephemeris_cache_restores,
metrics.ephemeris_decodes,
metrics.epoch_exclusions, metrics.fde_fixes,
horizontal_rmse, horizontal_spread, p95, metrics.horizontal_max_m,
vertical_rmse, vertical_spread);
@@ -809,9 +968,17 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
linux_nav_channel_t channels[MAX_SATELLITES];
memset(channels, 0, sizeof(channels));
gps_ephemeris_cache_t ephemeris_cache;
gps_ephemeris_cache_init(&ephemeris_cache, 1000u);
for (unsigned satellite = 0u; satellite < 8u; satellite++) {
channels[satellite].ephemeris_valid = true;
channels[satellite].ephemeris_decode_count = 1u;
CHECK(linux_nav_cache_ephemeris(
&ephemeris_cache, satellite,
(unsigned char)(17u + satellite), 0u),
"initial per-PRN ephemeris cache store failed");
}
metrics[STAGE_BASELINE].ephemeris_decodes = 8u;
linux_nav_channel_t iod_regression = {0};
linux_nav_receive_ephemeris_subframe(&iod_regression, 1u, 17u);
@@ -830,7 +997,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
receiver_truth[0] + 120.0,
receiver_truth[1] - 90.0,
receiver_truth[2] + 60.0,
clock_bias_m - 200.0
-20200000.0
};
unsigned delayed_selection_failures = 0u;
@@ -838,14 +1005,40 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
unsigned precommit_join_count_failures = 0u;
unsigned postcommit_join_count_failures = 0u;
unsigned fde_wrong_exclusion = 0u;
unsigned sample_datum_failures = 0u;
unsigned ephemeris_cache_restore_total = 0u;
bool saw_week_rollover = false;
bool saw_sample_rollover = false;
uint32_t previous_raw_sample = 0u;
uint32_t previous_active_mask = 0x0ffu;
for (unsigned epoch = 0u; epoch < 540u; epoch++) {
unsigned stage = synthetic_stage_for_epoch(epoch);
synthetic_stage_metrics_t* stage_metrics = &metrics[stage];
stage_metrics->epochs++;
uint32_t active_mask = synthetic_active_mask(stage);
/* A tracking slot is volatile; the per-PRN cache is not. Model a
* building/bridge obstruction by clearing the channel copy on loss and
* restoring it from RAM exactly once when acquisition returns. */
for (unsigned satellite = 0u; satellite < MAX_SATELLITES; satellite++) {
bool was_active = (previous_active_mask & (1u << satellite)) != 0u;
bool is_active = (active_mask & (1u << satellite)) != 0u;
if (was_active && !is_active) {
channels[satellite].ephemeris_valid = false;
memset(channels[satellite].have_sf, 0,
sizeof(channels[satellite].have_sf));
memset(channels[satellite].iod, 0,
sizeof(channels[satellite].iod));
} else if (!was_active && is_active &&
linux_nav_restore_ephemeris(
&ephemeris_cache, &channels[satellite],
satellite, epoch)) {
stage_metrics->ephemeris_cache_restores++;
ephemeris_cache_restore_total++;
}
}
previous_active_mask = active_mask;
if (stage == STAGE_EPHEMERIS_JOIN && epoch == 420u) {
linux_nav_receive_ephemeris_subframe(&channels[8], 1u, 29u);
@@ -853,6 +1046,12 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
}
if (stage == STAGE_EPHEMERIS_JOIN && epoch == 450u) {
linux_nav_receive_ephemeris_subframe(&channels[8], 3u, 29u);
if (channels[8].ephemeris_valid) {
CHECK(linux_nav_cache_ephemeris(
&ephemeris_cache, 8u, 29u, epoch),
"newly decoded PRN ephemeris was not cached");
stage_metrics->ephemeris_decodes++;
}
}
const double zero_noise[MAX_SATELLITES] = {0.0};
@@ -875,6 +1074,11 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
if (week > 2300) {
saw_week_rollover = true;
}
if (!synthetic_apply_receiver_sample_datum(
master, MAX_SATELLITES, epoch, week, tow_sec,
clock_bias_m, RECEIVER_COUNTER_PPM)) {
sample_datum_failures++;
}
uint32_t raw_sample =
(UINT32_MAX - 100000u) + epoch * 3273600u;
if (epoch != 0u && raw_sample < previous_raw_sample) {
@@ -885,7 +1089,6 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES];
unsigned candidate_to_satellite[GPS_NAV_EPOCH_MAX_CANDIDATES];
unsigned candidate_count = 0u;
uint32_t active_mask = synthetic_active_mask(stage);
for (unsigned satellite = 0u; satellite < MAX_SATELLITES; satellite++) {
if ((active_mask & (1u << satellite)) == 0u ||
!channels[satellite].ephemeris_valid) {
@@ -907,6 +1110,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
candidate_count++;
}
stage_metrics->candidate_total += candidate_count;
stage_metrics->sample_observations += candidate_count;
gps_nav_epoch_selection_t selection;
bool selected = gps_nav_epoch_validate_complete(
@@ -970,13 +1174,23 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
}
puts("\nGeneral synthetic GPS channel/epoch scenario");
puts("stage epochs/fixes candidates->selected and position scatter");
puts("stage epochs/fixes sample/cache/decode telemetry and position scatter");
for (unsigned stage = 0u; stage < SYNTHETIC_STAGE_COUNT; stage++) {
synthetic_print_metrics(metrics[stage]);
}
CHECK(saw_week_rollover, "general scenario must cross a GPS week boundary");
CHECK(saw_sample_rollover, "general scenario must cross uint32 ABS_SAMPLE rollover");
CHECK(sample_datum_failures == 0u,
"free-running RF sample counter failed to form an end-to-end observation");
CHECK(ephemeris_cache_restore_total == 5u,
"every returning cached PRN must restore exactly once");
CHECK(metrics[STAGE_REACQUIRE].ephemeris_cache_restores == 4u &&
metrics[STAGE_REACQUIRE].ephemeris_decodes == 0u,
"obstruction recovery must restore RAM ephemeris without SF1/SF2/SF3 decode");
CHECK(channels[8].ephemeris_decode_count == 1u &&
metrics[STAGE_EPHEMERIS_JOIN].ephemeris_decodes == 1u,
"an uncached PRN must join only after one matching ephemeris decode");
CHECK(metrics[STAGE_BASELINE].fixes >= 98u,
"baseline must produce stable fixes across time/sample rollover");
CHECK(metrics[STAGE_ONE_DROPOUT].fixes >= 58u,
@@ -1010,11 +1224,198 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
"minimum four-satellite geometry must stay within the synthetic bound");
}
typedef struct {
const char* name;
unsigned outage_sec;
uint32_t dropped_mask;
uint64_t cache_max_age_sec;
unsigned expected_cache_restores;
unsigned expected_reparses;
double receiver_counter_ppm;
} counter_reacquisition_case_t;
static unsigned synthetic_mask_count(uint32_t mask)
{
unsigned count = 0u;
while (mask != 0u) {
count += mask & 1u;
mask >>= 1u;
}
return count;
}
static bool synthetic_solve_counter_epoch(
unsigned epoch,
uint32_t active_mask,
const double receiver_truth[3],
double latitude_rad,
double longitude_rad,
double receiver_counter_ppm,
double* position_error_out)
{
const double no_noise[MAX_SATELLITES] = {0.0};
measurement_t all_measurements[MAX_SATELLITES];
create_measurements(all_measurements, 7u, receiver_truth,
latitude_rad, longitude_rad, 74000.0, no_noise);
int week = 0;
double tow_sec = 0.0;
synthetic_epoch_time(epoch, &week, &tow_sec);
if (!synthetic_apply_receiver_sample_datum(
all_measurements, 7u, epoch, week, tow_sec, 74000.0,
receiver_counter_ppm)) {
return false;
}
measurement_t selected[MAX_SATELLITES];
unsigned count = 0u;
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
if ((active_mask & (1u << satellite)) != 0u) {
selected[count++] = all_measurements[satellite];
}
}
if (count < STATE_DIM) {
return false;
}
double seed[STATE_DIM] = {
receiver_truth[0] + 120.0,
receiver_truth[1] - 90.0,
receiver_truth[2] + 60.0,
-20200000.0
};
solution_t solution;
solution_t full_solution;
if (!solve_with_fde(selected, count, seed,
latitude_rad, longitude_rad,
&solution, &full_solution) ||
!solution.solution_valid) {
return false;
}
if (position_error_out != NULL) {
*position_error_out = position_error_m(&solution, receiver_truth);
}
return true;
}
static void run_counter_reacquisition_matrix(const double receiver_truth[3],
double latitude_rad,
double longitude_rad)
{
static const counter_reacquisition_case_t cases[] = {
{"tunnel-all-30s", 30u, 0x07fu, 7200u, 7u, 0u, 0.2},
{"drop-6of7-15s", 15u, 0x07eu, 7200u, 6u, 0u, 0.2},
{"drop-6of7-2s", 2u, 0x07eu, 7200u, 6u, 0u, 0.2},
{"drop-1of7-15s", 15u, 0x040u, 7200u, 1u, 0u, 0.2},
{"stale-all-7201s", 7201u, 0x07fu, 7200u, 0u, 7u, 0.2},
{"tunnel-30s-15ppm", 30u, 0x07fu, 7200u, 7u, 0u, 15.0}
};
const uint32_t full_mask = 0x07fu;
puts("\nRF free-counter + PPS epoch + per-PRN RAM reacquisition matrix");
puts("scenario outage drop remain ppm outage_fix cache reparse nav_wait recovery_error");
for (unsigned case_index = 0u;
case_index < sizeof(cases) / sizeof(cases[0]);
case_index++) {
const counter_reacquisition_case_t* scenario = &cases[case_index];
gps_ephemeris_cache_t cache;
gps_ephemeris_cache_init(&cache, scenario->cache_max_age_sec);
linux_nav_channel_t channels[7];
memset(channels, 0, sizeof(channels));
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
channels[satellite].ephemeris_valid = true;
channels[satellite].ephemeris_decode_count = 1u;
CHECK(linux_nav_cache_ephemeris(
&cache, satellite,
(unsigned char)(40u + satellite), 0u),
"reacquisition matrix cache initialization failed");
}
uint32_t outage_active_mask =
full_mask & ~scenario->dropped_mask;
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
if ((scenario->dropped_mask & (1u << satellite)) != 0u) {
channels[satellite].ephemeris_valid = false;
}
}
unsigned remain = synthetic_mask_count(outage_active_mask);
double outage_error_m = NAN;
bool outage_fix = synthetic_solve_counter_epoch(
(scenario->outage_sec * 5u) / 2u,
outage_active_mask, receiver_truth,
latitude_rad, longitude_rad,
scenario->receiver_counter_ppm, &outage_error_m);
CHECK(outage_fix == (remain >= STATE_DIM),
"outage fix availability did not match the four-satellite minimum");
if (outage_fix) {
CHECK(outage_error_m < 25.0,
"partial-outage RF-counter solution exceeded the recovery bound");
}
unsigned cache_restores = 0u;
unsigned reparses = 0u;
for (unsigned satellite = 0u; satellite < 7u; satellite++) {
if ((scenario->dropped_mask & (1u << satellite)) == 0u) {
continue;
}
if (linux_nav_restore_ephemeris(
&cache, &channels[satellite], satellite,
scenario->outage_sec)) {
cache_restores++;
continue;
}
/* Expired/missing RAM data follows the real matching-IOD commit
* rule; a partial or mismatched set is never made usable. */
linux_nav_receive_ephemeris_subframe(
&channels[satellite], 1u, (unsigned char)(70u + satellite));
linux_nav_receive_ephemeris_subframe(
&channels[satellite], 2u, (unsigned char)(70u + satellite));
linux_nav_receive_ephemeris_subframe(
&channels[satellite], 3u, (unsigned char)(70u + satellite));
if (channels[satellite].ephemeris_valid) {
reparses++;
CHECK(linux_nav_cache_ephemeris(
&cache, satellite,
(unsigned char)(70u + satellite),
scenario->outage_sec + 18u),
"fresh post-expiry ephemeris was not returned to RAM");
}
}
CHECK(cache_restores == scenario->expected_cache_restores,
"reacquisition matrix restored the wrong number of cached PRNs");
CHECK(reparses == scenario->expected_reparses,
"reacquisition matrix reparsed the wrong number of PRNs");
unsigned nav_wait_sec = (reparses == 0u) ? 6u : 18u;
unsigned recovery_epoch =
(scenario->outage_sec + nav_wait_sec) * 5u;
double recovery_error_m = NAN;
bool recovery_fix = synthetic_solve_counter_epoch(
recovery_epoch, full_mask, receiver_truth,
latitude_rad, longitude_rad,
scenario->receiver_counter_ppm, &recovery_error_m);
CHECK(recovery_fix && recovery_error_m < 25.0,
"post-reacquisition RF-counter position did not recover correctly");
printf("%-20s %6u %4u %6u %5.1f %10s %5u %7u %8u %14.3f m\n",
scenario->name,
scenario->outage_sec,
synthetic_mask_count(scenario->dropped_mask),
remain,
scenario->receiver_counter_ppm,
outage_fix ? "valid" : "suppressed",
cache_restores,
reparses,
nav_wait_sec,
recovery_error_m);
}
}
int main(int argc, char** argv)
{
const unsigned satellite_count = 7;
const double latitude_rad = 39.925 * PI / 180.0;
const double longitude_rad = 32.837 * PI / 180.0;
const double latitude_rad = 39.925 * TEST_PI / 180.0;
const double longitude_rad = 32.837 * TEST_PI / 180.0;
const double receiver_height_m = 890.0;
const double clock_bias_m = 74000.0;
double receiver_truth[3];
@@ -1040,6 +1441,10 @@ int main(int argc, char** argv)
seed, latitude_rad, longitude_rad);
}
verify_common_start_offset_cannot_change_position(
clean, satellite_count, receiver_truth,
latitude_rad, longitude_rad);
puts("Synthetic standalone GPS integrity/FDE comparison");
puts("quality = 100/(1 + chi_square/threshold); 50 is the 99% reject boundary\n");
@@ -1286,6 +1691,8 @@ int main(int argc, char** argv)
run_general_synthetic_scenario(receiver_truth, latitude_rad,
longitude_rad, clock_bias_m);
run_counter_reacquisition_matrix(receiver_truth,
latitude_rad, longitude_rad);
if (failures == 0) {
puts("\nPASS: all synthetic integrity checks passed");
+6
View File
@@ -67,8 +67,14 @@ APP_SRCS := \
src/gps/gps_ip.c \
src/gps/gps_interrupt_handlers.c \
src/gps/gps_acq_manager.c \
src/gps/gps_almanac.c \
src/gps/gps_assistance_data.c \
src/gps/gps_assistance_store.c \
src/gps/gps_flash_journal.c \
src/gps/gps_ephemeris_cache.c \
src/gps/gps_visibility.c \
src/gps/gps_time_anchor.c \
src/gps/gps_measurement_freshness.c \
src/gps/gps_nav_epoch.c \
src/gps/gps_pps.c \
src/gps/gps_pps_quality.c \
+321 -36
View File
@@ -634,18 +634,100 @@ calculate_position();
- `ACQUIRING`, `ACQUIRED`, `TRACKING` durumlarindaki PRN'leri skip eder,
- kayip veya stale PRN'leri tekrar listeye alabilir,
- acquisition retry icin tick tabanli delay uygular.
- Gercek tracking loss sonrasi ilk hot reacquisition denemesi hemen yapilir.
Deneme sinyal henuz geri gelmedigi icin basarisizsa ayni PRN icin sabit 30 s
beklenmez; `GPS_ACQ_HOT_RETRY_DELAY_TICKS=2 s` sonra tekrar uygun olur.
Round-robin cursor ve `GPS_ACQ_MAX_HOT_STREAK` bir absent PRN'in engine'i
tek basina kullanmasini ve cold discovery'yi sonsuza kadar ac birakmasini
engeller. Bu delay ephemeris TTL'i degildir; fresh ephemeris ve HOW/sample
anchor butun hot denemeler boyunca RAM'de korunur.
### Acquisition Engine Algoritmasi
Bu firmware'de "hot" ve "cold" ayirimi acquisition RTL correlatorunun Doppler
veya code-phase arama penceresini degistirmez. Ayrim, hangi PRN'in tek ACQ
engine'ine once verilecegini ve tracking basladiginda cache'deki navigation
state'inin nasil geri yuklenecegini belirleyen software scheduler politikasidir.
Scheduler her 100 ms background gecisinde once donanim `BUSY` ve software
channel ownership bilgisini uzlastirir; yalniz bos tracking kanali varsa yeni
arama baslatir. Genesys2 tasariminda bir ACQ engine ve dokuz tracking kanali
vardir.
PRN state gecisleri su sekildedir:
```text
power-on: DISABLED -> COLD -> ACQUIRING
acquisition success: ACQUIRING -> ACQUIRED -> TRACKING
acquisition failure: ACQUIRING -> COLD
measurement deadline: TRACKING -> STALE -> TRACKING (lock devam ediyorsa)
real unlock/stop: TRACKING|STALE -> HOT -> ACQUIRING
expired hot window: HOT -> COLD
```
Arama iki farkli constellation-knowledge fazinda calisir:
1. Cold start veya incomplete/stale almanak: visibility filtresi tamamen
kapatilir. Gecerli position ve birkac full ephemeris olsa bile 32 hardware
PRN'i eski round-robin sirayla aranmaya devam eder. Partial almanak bir
uyduyu "gorunmez" ilan etmek icin kullanilmaz.
2. Complete/fresh almanak: her PRN icin fresh full ephemeris varsa precise
orbit kullanilir; yoksa reduced-precision almanak orbiti kullanilir. 5
derece elevation mask'inin ustundeki adaylar elevation'a gore siralanir.
Almanakta unavailable (`health=111111`) veya mask altinda olan PRN'ler normal
cold queue'dan cikarilir.
Bu iki faz icindeki arama onceligi su kurallari kullanir:
1. Yakinda gercekten track edilmis `HOT` PRN'ler sky modelinden bagimsiz olarak
once aranir. Onceki lock, bu uydu icin gozlemsel gorunurluk kanitidir. Ilk
deneme hemen, sonraki basarisiz denemeler ayni PRN icin 2 s araliklidir.
2. Incomplete almanak fazinda tum `COLD` PRN'ler round-robin aranir. Complete
fazda yalniz predicted-visible `COLD` PRN'ler elevation onceligiyle aranir.
3. Complete modelde mask altindaki PRN'ler position/time veya orbit modelinin
hatali/stale olmasina karsi yalniz 60 s'de bir round-robin safety probe alir.
4. Ardisik en fazla dort hot denemeden sonra bir eligible cold/discovery
denemesine yer verilir. Boylece coklu tunnel/drop durumunda reacquisition
yuksek oncelikli kalirken yeni uydu kesfi starvation'a ugramaz.
LNAV almanak collector, subframe 5 SV ID 1..24 ile subframe 4 SV ID 25..32
orbit sayfalarini, iki page-25 health grubunu ve subframe 5 page 25 `toa/WNa`
referansini RAM'de biriktirir. Broadcast SV ID, zero-based acquisition RTL
index'ine `SV_ID - 1` olarak cevrilir. Bir almanak seti su kosullarda complete
kabul edilir:
- `toa/WNa`, SF5 PRN 1..24 health ve SF4 PRN 25..32 health kayitlari vardir,
- her available PRN icin ayni `toa` degerine ait orbit sayfasi vardir,
- orbit sayfasi olmayan PRN page-25 health'te acikca `111111` unavailable'dir,
- RAM reception age ve GPS week/`toa` age limitleri asilmamistir.
Tek bir satellite'in tum almanak dongusunu yaymasi 25 adet 30 saniyelik frame,
yani yaklasik 12.5 dakika surer. Birden fazla tracking kanali ayni sayfalari
redundant olarak hizlandirmadan tekrarlar. Complete olmadan sky-guided mod
acilmaz; complete set daha sonra stale olur veya yeni `toa` seti partial kalirsa
firmware otomatik olarak 32-PRN round-robin'e geri doner. Tracking kanallari
dolunca background arama durur; nav-bit decode ve almanak toplama devam eder.
Onceki five-satellite plateau'nun sebebi safety probe'un yalniz normal visible
queue tamamen bosken calismasiydi. Bir predicted-visible absent PRN her retry
delay sonunda yeniden eligible oldugu icin queue hic bosalmiyor ve unknown PRN
probe'u 15 dakika sonra bile acilmiyordu. Yeni politika partial almanak boyunca
visibility suppression'i hic acmadigi icin bu starvation kosulu yoktur. Linux
hardware simulation testinde bes tracked uydu, bir partial modelin mask-altinda
isaretledigi PRN'leri yine normal round-robin ile arayarak 25 simulation tick
icinde dokuz tracked uyduya cikar.
### Satellite State Model
Her PRN icin state tutulur:
```c
GPS_SAT_NOT_SEEN
GPS_SAT_ACQUIRING
GPS_SAT_ACQUIRED
GPS_SAT_TRACKING
GPS_SAT_LOST
GPS_SAT_STALE
GPS_ACQ_PRN_DISABLED
GPS_ACQ_PRN_COLD
GPS_ACQ_PRN_HOT
GPS_ACQ_PRN_ACQUIRING
GPS_ACQ_PRN_ACQUIRED
GPS_ACQ_PRN_TRACKING
GPS_ACQ_PRN_STALE
```
Bu state modeli acquisition scheduler, tracking handoff ve stale measurement temizligi icin kullanilir.
@@ -680,10 +762,10 @@ GPS_EVENT_QUEUE_SIZE
- `gps_systick_ticks` global tick counter olarak kullanilir.
- Systick acquisition scheduler'i periyodik olarak uyandirir.
- Timer interrupt sadece tick arttirir, scheduler pending flag set eder ve bir sonraki compare degerini programlar.
- Mevcut ayar `GPS_SYSTICK_HZ=20u`'dir. CLINT timebase board build'inden gelir:
- Mevcut ayar `GPS_SYSTICK_HZ=10u`'dir. CLINT timebase board build'inden gelir:
Genesys2'de 50 MHz SoC clock `/100` bolucu nedeniyle `500000 Hz`, VCU118'de
100 MHz SoC clock nedeniyle `1000000 Hz` kullanilir. Her iki kartta machine
timer interval'i gercek zamanda 50 ms'dir.
timer interval'i gercek zamanda 100 ms'dir.
- Systick sadece bos acquisition engine'e yeni is atamak ve background maintenance yapmak icin kullanilir.
- Acquisition completion/stop systick timeout ile yapilmaz; ACQ engine normalde sadece ACQ-end interrupt ile tamamlanir.
- `GPS_ACQ_STUCK_WARN_TICKS` diagnostic-only stuck-busy warning icindir. ACQ'yi stop/restart etmez; sadece software-owned ACQ engine beklenenden uzun busy kalirsa `rf_clk`, `sample_align_i` veya `acq_end` yolunu kontrol etmek icin log basar.
@@ -693,34 +775,143 @@ GPS_EVENT_QUEUE_SIZE
### Tracking Freshness ve Stale Data Koruma
- Tracking linked-list node'una freshness alanlari eklendi:
- Tracking linked-list node'una phase-aware freshness alani eklendi:
```c
gps_last_measurement_tick
gps_measurement_valid
gps_measurement_freshness_t gps_measurement_freshness
gps_nav_solve_sequence
gps_last_nav_solve_pps_counter
```
- Navigation input'u hazir kabul edilmeden once su kontroller yapilir:
- measurement valid mi,
- lock valid mi,
- measurement tick'i stale timeout icinde mi.
- Stale timeout:
- Freshness state machine event cadence'ini acikca ayirir:
- `GPS_MEASUREMENT_PHASE_LNAV`: cold-acquisition subframe'leri 6 saniyede bir
geldiginden periyodik measurement deadline uygulanmaz. Kanal liveness'i
tracking `BUSY/LOCK`, stop/unlock interrupt'i ve stopped-channel
reconciliation ile belirlenir.
- `GPS_MEASUREMENT_PHASE_NAV_SOLVE_ARMED`: firmware nav-solve interrupt'ini
actigi anda 3 saniyelik deadline baslar.
- `GPS_MEASUREMENT_PHASE_NAV_SOLVE_ACTIVE`: gelen her nav-solve olcumu
deadline'i yeniler. Bu asamada gelen LNAV olayi deadline'i yenileyemez.
- Periyodik stale timeout:
```c
GPS_TRK_MEASUREMENT_STALE_TICKS
```
- Navigation input'u kullanilmadan once phase-valid, lock ve—yalniz nav-solve
armed/active phase'lerinde—measurement deadline kontrol edilir.
- Tracking channel lock kaybederse:
- measurement invalid/stale yapilir,
- PRN `GPS_SAT_LOST` durumuna alinir,
- tracking slot bosaltilir,
- PRN tekrar acquisition listesine eklenebilir.
- Position calculation artik stale veya unlock olmus measurement kullanmaz.
- Her PRN icin commit edilmis ephemeris `gps_ephemeris_cache_t.entry[32]`
dizisinde CPU RAM/BSS icinde tutulur. Tracking slot'u silinse bile cache
silinmez; ayni PRN reacquire edildiginde ephemeris yeni kanala kopyalanir.
- Cache yalniz matching IODC/IODE ile commit edilmis, `ephemeris_valid=true`
setleri kabul eder. Ayni broadcast issue tekrar alinirsa iki saatlik TTL
yenilenmez; yeni IOD/toe issue'su TTL'yi yeniden baslatir. Expired entry
restore sirasinda invalid edilir ve kanal yeniden SF1/SF2/SF3 toplar.
- Gecerli cache ile reacquisition tam ephemeris turunu veya siradaki 6 saniyelik
subframe/HOW sinirini beklemez. Cache'deki week-qualified HOW/sample anchor'i
ilk coherent 20 ms hardware measurement'ina tasinir ve `NAV_SOLVE_PHASE`
yazilarak timer dogrudan COUNT durumuna alinir. Boylece tracking lock
sonrasi ek gecikme ilk coherent measurement ve en fazla bir nav-solve
periyodudur (varsayilan `N=10` icin yaklasik 20..220 ms). RAM power-cycle
boyunca kalici degildir. Power-cycle sonrasi asagidaki flash-assistance yolu
en az bir parity-valid live subframe ile zamani yeniden dogrular; RF phase
flash'tan restore edilmez.
- Subframe 1'den gelen 6-bit `svhlth` alani `000000` degilse kanal tracking/nav-bit guncellemesine devam eder fakat navigation snapshot'a alinmaz. Boylece broadcast unhealthy olarak isaretlenen SV konum hesabinda kullanilmaz.
- Navigation hesaplamasi icin interrupt disable suresi kisaltildi; sadece snapshot/promote bolumleri korunuyor.
### Power-cycle Flash Assistance ve Yazma Politikasi
Genesys2 32 MiB QSPI flash'in son 512 KiB'i GPS assistance icin ayrilmistir.
Bitstream `0x00000000`, uygulama `0x01800000` adresinden baslar; build scriptleri
artik bu iki image'in assistance rezervasyonuna tasmasini hata olarak reddeder.
```text
0x00000000 ... bitstream
0x01800000 ... bootloader application image
0x01f80000 ... journal bank 0 (ilk 64 KiB kullanilir)
0x01fc0000 ... journal bank 1 (ilk 64 KiB kullanilir)
0x02000000 flash sonu
```
Iki logical bank ayri 256 KiB erase boundary'sindedir. Bu nedenle kartin
S25FL256S'i 64 KiB veya 256 KiB uniform-sector modunda olsa da `0xDC` erase
bitstream/application bolgesine dokunmaz. Her record version, sequence, payload
boyu, header CRC, payload CRC ve en son yazilan commit word tasir. Yeni bank
ancak eski committed record dururken erase edilir; power loss header, herhangi
bir payload page'i veya commit yazisi sirasinda olursa reboot eski CRC-valid
record'a geri doner. Corrupt en yeni bank da diger banktaki onceki sequence'e
fallback eder.
Flash snapshot su navigation assistance verilerini saklar:
- her PRN icin matching SF1/SF2/SF3'ten atomik commit edilmis full ephemeris,
IODC/IODE, toe/toc, URA ve broadcast SV health,
- SF5 PRN 1..24 ve SF4 PRN 25..32 almanak orbitleri, `toa/WNa` referansi ve
iki 6-bit health grubu,
- SF4 page 18'in tam Klobuchar alpha/beta modeli, GPS-UTC `A0/A1`, `tot/WNt`,
current/future leap second (`dtLS/dtLSF`) ve `WNlsf/DN` alanlari,
- son integrity-valid WLS ECEF/lat/lon/height konumu ve full GPS week/TOW.
Su state bilerek flash'a yazilmaz: raw/corrected RF sample anchor'i,
`NAV_SOLVE_PHASE`, PPS loop/filter state'i, tracking NCO/loop state'i ve WLS
receiver clock bias'i. Bunlar power-cycle sonrasi yeni fabric/RF/oscillator
phase'ine ait olmadigi icin restore edilmeleri pseudorange scatter uretirdi.
Konum WLS seed'inde yalniz ECEF kullanilir; clock bias dorduncu unknown olarak
sifirdan yeniden cozulur.
Yazma periodic degil, content-change event driven'dir:
1. Ephemeris RAM'de her nav interrupt'inda izlenmeye devam eder. Yalniz coherent
setin IODC/IODE/toe/toc issue'su degisirse flash dirty olur. Ayni issue'nun
tekrari reception confirmation'dir; iki saatlik TTL'yi veya flash deadline'i
yenilemez.
2. Almanak page'i her reception'da RAM age'ini yeniler; fakat `WNa/toa`, orbit
veya health icerigi degismediyse flash dirty olmaz. Yeni epoch'in degisen
sayfalari geldikce quiet deadline ileri gider, boylece her 6 saniyelik page
icin degil ilgili set toplandiktan sonra tek snapshot yazilir.
3. Iono/UTC/leap snapshot'i page-18 icerigi gercekten degistiginde yazilir.
4. Ilk valid konum veya persisted konumdan en az 1 km hareket yeni assistance
event'idir. Sabit anten icin zaman tabanli periyodik position write yoktur.
5. Ilgili degisikliklerin tek record'da toplanmasi icin son degisiklikten sonra
60 s quiet-time uygulanir; surekli degisimde 15 dakika maximum delay vardir.
Payload CRC son committed snapshot ile ayniysa program/erase yapilmaz.
Power-on'da flash yalniz bir kez okunur ve data `provisional` kalir. Ilk
parity-valid LNAV subframe'in HOW'u live TOW'u verir; SF1 varsa 10-bit week
saved full week etrafinda cozulur. Hic saved record yoksa current 2019 rollover
era base'i (`2048`) kullanilir. Sonra:
- ephemeris her PRN icin live time ile `toe` arasinda en fazla iki saatse RAM'e
import edilir; reboot bu entry'ye yeni iki saat vermez, gercek kalan TTL
korunur,
- ilgili satellite'in ilk live SF1/SF2/SF3'u cache IOD'si ile uyusmuyorsa
yalniz o PRN hemen invalid edilir; ilk page SF4/SF5 ise time-age validation
ile kullanilir ve sonraki issue-bearing page yine mismatch kontrolu yapar,
- almanak `WNa/toa` age'i yedi gunden eskiyse, iono/UTC snapshot'i yedi gunden
eskiyse reddedilir; son konum coarse seed olarak en fazla 30 gun tutulur,
- fresh complete almanak + restored konum WLS/coarse seed olarak kullanilir, fakat
sky scheduler'i PRN suppression moduna sokmaz; bunun icin live position gerekir.
Partial/stale almanakta davranis yine tum 32 PRN round-robin'dir.
Combined full-device MCS programlama islemi programmer erase politikasi
nedeniyle runtime journal'i silebilir; bu yeni firmware'in normal QSPI runtime
yazma adreslerinin bitstream/app ile cakistigi anlamina gelmez. Kalici
assistance korunacaksa yalniz app-region programlama hedefi kullanilmalidir.
Linux testleri fake NOR flash'ta 1->0 page semantics, iki-bank rotation,
deduplication, her page/commit asamasinda torn write, CRC corruption fallback,
power-cycle restore, per-PRN IOD mismatch, mixed stale/fresh satellite ve uzun
idle surede sifir write / identical almanakta dedup / changed almanakta tek
debounced commit durumlarini kapsar. `test_gps_receiver_hardware_sim` ayrica 30 s tum-satellite
tunnel, 2 s kisa fade, 6/7 satellite icin 15 s blockage ve stale cache decode
yollarini calistirir.
### Nav Bits + Nav Solve Akisi
- Tracking kanali ilk basladiginda nav-bits mode kullanilir:
@@ -732,10 +923,73 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
- Firmware nav bit interrupt'lari ile subframe verisini surekli toplar.
- Bir tracking kanali ancak IODC/IODE degerleri eslesen subframe 1, 2 ve 3 seti atomik olarak commit edildikten sonra navigation-ready olur. Subframe 4 ve 5 ilk fix'i bloke etmez; page 18 iyonosfer/UTC ve almanak verisi arka planda toplanmaya devam eder.
- En az 4 satellite icin tutarli ephemeris commit edilince nav-bit snapshot'i yalniz ephemeris uygunlugunu ve kullanilacak kanallari belirler; bu snapshot'tan pozisyon hesaplanmaz.
- Tutarlı ephemerisi hazir olan tracking kanallarinda nav-solve akisi etkinlestirilir:
- Tutarlı ephemerisi hazir olan ve HOW/sample anchor'indan hardware phase seed'i
tamamlanmis tracking kanallarinda nav-solve akisi etkinlestirilir:
- nav bits interrupt/flag acik kalir,
- `NAV_SOLVE_INTR_EN=1`,
- `NAV_SOLVE_FLAG_EN=1`.
#### `NAV_SOLVE_PHASE` Neden Gereklidir?
Fresh ephemeris yalniz satellite orbit ve clock bilgisinin tekrar decode
edilmesini gereksiz yapar; tek basina per-channel nav-solve timer'ini yeniden
baslatip diger kanallarla hizalayamaz. Tracking unlock oldugunda `trck_busy=0`
RTL timer'ini `IDLE` durumuna ve sayacini sifira dondurur. Bu durumda software'in
yalniz `NAV_SOLVE_INTR_EN` bitini acmasi yeterli degildir: timer COUNT durumunda
olmadigi icin interrupt uretilmez ve eski yol yeni bir `subframe_ready` darbesini
0..6 saniye bekler.
Her reacquired kanali bagimsiz olarak counter zero'dan baslatmak da dogru
degildir. Kanallar farkli 20 ms navigation-bit kenarlarinda lock olur; bu nedenle
lokal sayaclari ayni degerden baslatmak wrap/interrupt darbelerini farkli fiziksel
epoch'lara koyar. Coalescent snapshot ayni receiver zamanina ait olcumler ister;
farkli epoch'lari yalniz software interrupt zamanina bakarak birlestirmek
pseudorange ve position scatter hatasi yaratir.
HOW-anchor phase seed yolu bu nedenle hem cold ephemeris decode sonrasinda hem
cached reacquisition sonrasinda su sirayi izler:
1. Cold yolda parity-valid live HOW TOW, corrected/raw RF sample ve CPU systick
anchor'i commit edilir. Cached yolda ayni bilgiler yeni tracking kanalina
restore edilir.
2. Background software kopyasi yerine channel'in en yeni atomik 20 ms hardware
measurement mailbox'i okunur; PRN, valid ve lock alanlari dogrulanir.
3. Anchor, bu measurement'in GPS zamanina tasinir ve global bit numarasi ile
seed hesaplanir:
```text
global_bit = round(measurement_tow_seconds / 0.020)
phase = global_bit mod GPS_NAV_SOLVE_TIMER_TICKS
```
4. Nav-solve flag/interrupt kapaliyken phase `NAV_SOLVE_PHASE` register'ina
yazilir. RTL bu yazma darbesiyle `IDLE` durumundan dogrudan `COUNT` durumuna
gecer. Yazma sirasinda mailbox sequence ilerlemisse interrupt acilmaz; yeni
mailbox ile sonraki background turunda yeniden seed edilir.
5. Stable seed sonrasinda nav-solve flag ve interrupt acilir ve tracking node
`gps_nav_phase_seeded=true` olur. Her channel'in lokal counter'i artik ayni
global GPS bit numarasinin modulo-N fazini tasidigi icin sonraki wrap
darbeleri ayni coalescent epoch'ta olusur.
Bu register ephemeris cache'in yerine gecmez ve TOW uretmez; cache'deki zaman
anchor'ini RTL'nin periodic measurement fazina uygular. Epoch assembler yine
25 ms RF-sample penceresini, TOW cluster'ini, 64-bit PPS capture counter'ini ve
frozen expected channel maskesini kontrol eder. Dolayisiyla phase seed tek
basina hatali kanallari zorla ayni solution'a sokmaz.
Cold start'ta fresh flash assistance da yoksa RAM cache ve valid HOW/sample
anchor olmadigi icin phase tahmin edilmez. Orijinal parity-valid LNAV/HOW yolu
kullanilir, SF1/SF2/SF3 ephemerisi decode edilir ve ayni live HOW/sample
anchor'i ile `NAV_SOLVE_PHASE` seed edilir. Fresh flash ephemerisi varsa ilk
live subframe zaman/issue kontrolunden sonra orbit restore edilir, fakat
power-cycle oncesi HOW/sample anchor yine kullanilmaz. RF sample counter'in
resetlenmesi veya RF clock'un durmasi halinde CPU systick ile RF elapsed-time
kontrolu uyusmaz; ephemeris cache korunur fakat unsafe phase seed reddedilir ve
en fazla bir yeni HOW beklenerek guvenli hizalama yapilir.
`GPS_NAV_SOLVE_TIMER_TICKS`, hem 300-bit subframe'i hem 30240000-bit GPS
haftasini tam bolecek sekilde compile-time'da kontrol edildigi icin week
rollover da ortak fazi bozmaz.
- 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 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.
@@ -745,9 +999,9 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
- 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.
- Klobuchar gecikmesi WLS'nin receiver ECEF, azimut ve elevasyon hesabindan uretilir. Ilk unseeded iteration iyonosfersizdir; yaklasik receiver konumu olustuktan sonraki iteration'larda gecikme gozlenen pseudorange'dan cikarilir.
- Bundan sonra periyodik pozisyon hesaplamasi nav-solve interrupt'lari ile tetiklenir. RTL `subframe_ready` sonrasinda COUNT durumuna girer ve `NAV_SOLVE_TIMER=N` icin N adet navigation `bit_valid` darbesi sayar. Her navigation biti 20 ms oldugu icin periyot `N * 20 ms`'dir; firmware `GPS_NAV_SOLVE_TIMER_TICKS` build ayarini `gps_startup_initialization()` fonksiyonuna verir.
- Bu siralama onemlidir: RTL `nav_solve_timer` sayaci `NAV_SOLVE_EN` ve ilk gecerli `subframe_ready` ile baslar. `NAV_SOLVE_EN` nav-bit asamasinda acik tutulur; tutarli subframe 1..3 ephemerisi commit edilene kadar yalniz nav-solve interrupt/flag kapali kalir. Timer COUNT durumuna girdikten sonra sonraki subframe darbeleri sayaci sifirlamaz veya yeniden fazlamaz.
- 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.
- Bundan sonra periyodik pozisyon hesaplamasi nav-solve interrupt'lari ile tetiklenir. Current internal-timer RTL'de cold ve cached yollarin ikisi de `NAV_SOLVE_PHASE` write pulse'u sonrasinda ortak global-bit fazina hizalanir; yalniz `NAV_SOLVE_INTR_EN` acmak timer'lari hizalamaz. `NAV_SOLVE_TIMER=N` icin N adet navigation `bit_valid` darbesi sayilir. Her navigation biti 20 ms oldugu icin periyot `N * 20 ms`'dir; firmware `GPS_NAV_SOLVE_TIMER_TICKS` build ayarini `gps_startup_initialization()` fonksiyonuna verir.
- Bu siralama onemlidir: `NAV_SOLVE_EN` nav-bit asamasinda acik tutulabilir, fakat tutarli subframe 1..3 ephemerisi ve hardware phase seed tamamlanmadan kanal navigation-ready sayilmaz. Stable seed sonrasinda nav-solve interrupt/flag acilir. Timer COUNT durumuna girdikten sonra sonraki subframe darbeleri sayaci sifirlamaz veya yeniden fazlamaz.
- Tracking kanallari farkli zamanlarda lock olabilir ve ilk gecerli subframe'lerini 6 saniyenin farkli katlarinda gorebilir. Bu nedenle hepsini counter zero'dan baslatmak farkli fiziksel epoch'larda interrupt uretir. Firmware her kanali live/cached HOW anchor'i ile en yeni 20 ms measurement mailbox'ina tasir ve `round(TOW / 0.020) mod NAV_SOLVE_TIMER` fazini yazar. Firmware hem subframe/HOW hem week rollover boyunca ortak fazi korumak icin `300 mod N == 0` ve `30240000 mod N == 0` kosullarini compile-time'da zorunlu tutar. Ornegin `N=10`, `N=25`, `N=50`, `N=75`, `N=100`, `N=150` ve `N=300` gecerlidir; kosullari 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.
- 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.
@@ -759,8 +1013,8 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
- 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.
- Tracking nav-solve interrupt'lari ayni GPS epoch'unda birbirine cok yakin gelebilir. External interrupt handler bu yuzden PLIC'te bekleyen farkli GPS source ID'lerini tek ISR girisinde bounded olarak drain eder. Ayni source ID tekrar claim edilirse handler durur; bu durum level interrupt clear'in henuz donanima yansimadigini gosterir ve `PLIC repeated source while draining` WARN sayaci ile raporlanir.
- Normal nav-bit interrupt'larinda mevcut tracking node tekrar insert edilmeye calisilmaz. Bu nedenle `requested node already exists` UART spam'i normal akista gorulmemelidir; per-subframe `nav bits` logu `TRACE` seviyesine alinmistir.
- Nav solve mode'da `NAV_SOLVE_TIMER`, subframe fazindan sonra kac adet 20 ms navigation biti sayilacagini; dolayisiyla ABS_SAMPLE latch ve interrupt epoch'unu belirler. Her islenen event'te TOW `NAV_SOLVE_TIMER * 20 ms` artirilir ve GPS hafta sinirinda normalize edilir. Raw `ABS_SAMPLE` TOW artisini belirlemez; yalniz ayni nav-solve epoch'undan gelen kanallari secmek ve pseudorange olcumunu olusturmak icin kullanilir. Pseudorange hesabina giden `gps_ephems.abs_sample` receiver sample'in corrected double halidir.
- `hardware/external/gps/rtl/gps_timer/tb/nav_solve_timer_tb.sv`, RTL'nin ayni hafta icindeki genel fazlama davranisini bir kanalin digerinden tam bir subframe gec basladigi `N=40`, `N=37`, `N=512` ve `N=1` durumlariyla kontrol eder. Firmware build kurali bunlardan GPS haftasindaki `30240000` navigation biti tam bolmeyen degerleri sistem konfigurasyonu olarak reddeder.
- Nav solve mode'da `NAV_SOLVE_TIMER`, subframe fazindan sonra kac adet 20 ms navigation biti sayilacagini; dolayisiyla ABS_SAMPLE latch ve interrupt epoch'unu belirler. Her islenen event'te TOW `NAV_SOLVE_TIMER * 20 ms` artirilir ve GPS hafta sinirinda normalize edilir. Raw `ABS_SAMPLE` TOW artisini belirlemez; epoch coherence ve fractional code-boundary correction icin kullanilir. Position pseudorange'in receiver-time terimi ortak RF sample counter ile ayni anda yakalanan `REM_CODE_PHASE_REG`'den gelir. PPS wrapper'in 64-bit 125 MHz capture counter'i ise epoch assembly, GPS-time mailbox, PPS discipline ve holdover icin kullanilir; position WLS range datum'una girmez.
- `hardware/external/gps/rtl/gps_timer/tb/nav_solve_timer_tb.sv`, hem cold/HOW alignment'i hem cached `NAV_SOLVE_PHASE` seed yolunu kontrol eder. Cold testleri bir kanalin digerinden tam bir subframe gec basladigi `N=40`, `N=37`, `N=512` ve `N=1` durumlarini; cached testleri farkli reacquisition zamanlarinda verilen global-bit phase'lerin ayni wrap epoch'una geldigini kapsar. Firmware build kurali 300-bit subframe'i veya GPS haftasindaki `30240000` navigation biti tam bolmeyen degerleri sistem konfigurasyonu olarak reddeder.
- Tracking wrapper'daki `REM_CODE_PHASE_REG` donanimda IEEE-754 double degildir. RTL'de code NCO phase accumulator 46 bittir; software'e expose edilen kisim `rem_code_ph[45:20]` yani 26-bit integer residue'dur ve APB `PRDATA[25:0]` alanina zero-extend edilerek verilir. Bu yuzden firmware bu register'i `uint32_t` okur; `uint64_t` veya `double*` olarak MMIO okumak iki 32-bit APB read uretir ve register map ile uyumlu degildir.
- Navigation icin kullanilan corrected receiver sample formulu:
@@ -897,12 +1151,28 @@ Receiver oscillator `0.1 ppm` hizliysa `bdot_r` yaklasik `29.98 m/s` olur.
Bu etki butun uydu Doppler'larina ortak girer; cozulmezse bir kismi
`vx/vy/vz` receiver hareketi sanilir.
Bu projedeki position WLS'e verilen capture zamanlarindan ortak receiver
zamani cikarilir ve `STARTOFFSET` eklenir. Dolayisiyla
Bu projedeki position WLS gozlemi, tum kanallarca paylasilan RF sample counter
ile ayni code boundary'de yakalanan code-NCO residue'u birlestirir:
```text
S_i = unwrap32(ABS_SAMPLE_i) - REM_CODE_PHASE_i / 1.023e6
P_i/c = (S_i - S_ref)/16.367981e6 - (T_tx_i - T_tx_ref)
```
`ABS_SAMPLE` tek basina kullanilsaydi 16.367981 MHz'in 61.095 ns periyodu
18.31 m range quantum'u olustururdu. `REM_CODE_PHASE` code-NCO crossing
residue'u ayni latch'te sagladigi icin integer sample altina inilir; gercek
dogruluk register quantization'undan cok DLL thermal noise, multipath ve uydu
geometrisiyle sinirlidir. Free-running sample counter'in reset degeri ve ortak
phase bu kanal farkinda yok olur; sabit startup/flight-time offset'i
kullanilmaz. RF sample-frequency scale hatasi ortak bias degildir ve WLS'nin
dorduncu state'i bunu tamamen silemez; fakat etkisi outage suresiyle degil,
ayni epoch'taki kanallar arasi millisecond-scale receive-time farkiyla buyur.
Dolayisiyla
`solution.clock_bias_m`, saf `c*delta_t_r` yerine yaklasik
`c*STARTOFFSET - reference_range` olan referans-uyduya bagli goreli bir
datumdur. Doppler `solution.clock_drift_mps` ile bunun arasinda turev kisiti
kurulmaz.
`-reference_range + c*reference_satellite_clock_correction` olan
referans-uyduya bagli bir datumdur. Doppler `solution.clock_drift_mps` ile
bunun arasinda dogrudan turev kisiti kurulmaz.
Fit'ler matematiksel olarak ayridir ve `solution_valid`/`velocity_valid`
bagimsizdir. Bununla birlikte velocity WLS LOS birim vektorlerini hesaplamak
@@ -957,7 +1227,15 @@ olarak yayinlanir.
- `POSTPONE_TOW=0.180` mevcut RTL'nin dokuzuncu takip eden navigation bitinde
yaptigi teslimi duzeltir. `satpos()` ve PPS receive-time anchor bu degeri
tam bir kez ve ayni firmware surumunde kullanir.
- 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.
- Ilk nav-solve interrupt'i geldiginde o anda allocated, hardware busy, lock'lu,
ephemeris-valid, measurement-fresh ve phase-seeded 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
@@ -1041,7 +1319,8 @@ XLSX register dokumanlarina gore dogrulananlar:
- nav data `0x18..0x3c`
- timeout `0x40`
- nav timer `0x44`
- reserved `0x48`
- nav solve phase seed `0x48` (`NAV_SOLVE_PHASE`; write pulse timer'i
global 20 ms bit fazinda COUNT durumuna alir)
- remaining code phase `0x4c`
- measurement meta `0x50`
- measurement sequence `0x54`
@@ -1341,13 +1620,19 @@ Bu timer/scheduler yavasligi degildir. Firmware start ediyor; tamamlanma icin RF
- PPS APB periferigi `0x1002a000` adresindedir ve PLIC interrupt kullanmaz.
- Her tracking wrapper, yalniz `nav_solve_timer_reached` olayinda PPS capture
darbesi uretir. Bu olay `ABS_SAMPLE` ve `REM_CODE_PHASE` latch'iyle aynidir;
subframe-ready PPS timestamp'i olusturmaz.
subframe-ready PPS timestamp'i olusturmaz. Darbe ve PPS raw counter ayni
125 MHz domain'de latch edilir; stable counter APB'ye tasindiktan bir PCLK
sonra sequence publish edilir. Boylece 8 ns darbe 50 MHz PCLK tarafinda
kacmaz ve firmware sequence/data/sequence kontrolu mixed payload kabul etmez.
- PPS reset'i RF/MMCM-ready reset'inden ayridir. RF kablosu veya sample clock
kayboldugunda acquisition/tracking DSP resetlenebilir, fakat 125 MHz raw PPS
counter, disciplined accumulator ve holdover rate SoC resetine kadar yurur.
- `leastSquaresSTD()` ayni basarili epoch'un `[x, y, z, b]` sonucunu verir.
Ilk cozumde en erken corrected-ABS kanali zaman datum'u secilir; ayni SVID
coherent snapshot'ta kaldigi surece referans korunur. Receive-time ankraji:
```text
T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
T_rx = TOW_ref + POSTPONE_TOW - b/c
```
- `POSTPONE_TOW=0.180 s`, mevcut tracking RTL'nin parity kontrolunu dokuzuncu
@@ -1362,9 +1647,10 @@ T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
- Tracking capture olayi integer RF sample kenarinda olustugu halde WLS,
`ABS_SAMPLE - REM_CODE_PHASE/1.023e6` alt-sample zamanini kullanir. Yazilim
mailbox `EPOCH_COUNTER` degerinden ayni fractional sample gecikmesini
`round(fraction * 125e6/16.367981e6)` ile cikarir. Boylece WLS zamani ile
hardware epoch'u ayni code boundary'ye referans olur; 125 MHz integer
mailbox yuvarlamasindan en fazla 4 ns kalir.
`round(fraction * 125e6/16.367981e6)` ile cikarir. Bu fractional duzeltme
yalniz PPS epoch/mailbox ankrajini ayni code boundary'ye getirir; position
WLS zaten dogrudan fractional RF observation'i kullandigi icin 125 MHz
capture quantization'i XYZ hesabina girmez.
- HOW/sample sureklilik kontrolu kartin fiziksel `16.367981 MHz` RF frekansini
kullanir. Periyodik nav-solve TOW'u receiver propagation/range-rate
degisiminden uretilmez; sample farki yalniz olay numarasini dogrular ve TOW
@@ -1399,8 +1685,7 @@ T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
- 125 MHz fiziksel kenar grid'i 8 ns'dir. Generated VCU118 ve Genesys2 top'lari
`gps_pps` output'unu expose eder. VCU118 icin PPS PMOD1/J53.8 (`R29`),
Genesys2 icin PMOD JC10 (`AK28`) pinine atanmistir. Board testinde
Gray/PCLK capture pipeline sabit offset'i ve
clock-tree/I/O gecikmesi olculup kalibre edilmelidir.
clock-tree/I/O/output-buffer gecikmesi olculup kalibre edilmelidir.
### PPS Sentetik Dogrulama (2026-07-25)
@@ -84,7 +84,8 @@
const unsigned char* ura_indices,
const double* cn0_dbhz,
const ionoutc_t* ionoutc, double gps_tow,
gps_nav_solution_t* solution_out);
gps_nav_solution_t* solution_out);
void gps_wls_seed_position(const double receiver_ecef_m[3]);
void rotateSatelliteSTD(double const traveltime, const mat_type* X_sat, mat_type* X_sat_rot);
void topocent(const double* XPtr, const double* dxPtr, int size, t_phi_Lambda_h * phi_Lambda_h, t_topocentData* topcent);
void togeod(double a, double finv, double X, double Y, double Z, t_phi_Lambda_h * phi_Lambda_h, t_topocentData* topcent);
@@ -21,6 +21,8 @@ typedef struct {
uint64_t retry_delay_ticks;
uint64_t hot_retry_delay_ticks;
uint64_t hot_window_ticks;
uint64_t unknown_probe_interval_ticks;
uint64_t visibility_probe_interval_ticks;
unsigned int max_hot_streak;
} gps_acq_manager_config_t;
@@ -30,6 +32,9 @@ typedef struct {
uint64_t hot_until_tick;
bool has_attempted;
bool attempt_was_hot;
bool visibility_known;
bool predicted_visible;
int visibility_priority;
} gps_acq_prn_entry_t;
typedef struct {
@@ -38,6 +43,9 @@ typedef struct {
unsigned int cold_cursor;
unsigned int hot_cursor;
unsigned int hot_streak;
uint64_t next_unknown_probe_tick;
uint64_t next_visibility_probe_tick;
bool visibility_active;
} gps_acq_manager_t;
void gps_acq_manager_init(
@@ -69,6 +77,17 @@ void gps_acq_manager_mark_failed(
unsigned int prn,
uint64_t now);
void gps_acq_manager_set_visibility_mode(
gps_acq_manager_t* manager,
bool active,
uint64_t now);
bool gps_acq_manager_set_visibility(
gps_acq_manager_t* manager,
unsigned int prn,
bool known,
bool predicted_visible,
int priority);
bool gps_acq_manager_start_next(
gps_acq_manager_t* manager,
uint64_t now,
@@ -0,0 +1,95 @@
#ifndef GPS_ALMANAC_H
#define GPS_ALMANAC_H
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_ip_defines.h"
#define GPS_ALMANAC_PRN_COUNT 32u
#define GPS_ALMANAC_UNAVAILABLE_HEALTH 0x3fu
typedef struct {
double eccentricity;
double toa_sec;
double delta_i_rad;
double omega_dot_rad_per_sec;
double sqrt_a;
double omega0_rad;
double argument_of_perigee_rad;
double mean_anomaly_rad;
double af0_sec;
double af1_sec_per_sec;
uint64_t received_tick;
unsigned char health8;
bool valid;
} gps_almanac_entry_t;
typedef struct {
gps_almanac_entry_t entry[GPS_ALMANAC_PRN_COUNT];
unsigned char health6[GPS_ALMANAC_PRN_COUNT];
uint64_t entry_mask;
uint64_t health6_mask;
uint64_t reference_tick;
uint64_t sf4_health_tick;
uint64_t max_cache_age_ticks;
double reference_toa_sec;
unsigned int reference_week_mod256;
bool reference_valid;
bool sf4_health_valid;
} gps_almanac_cache_t;
typedef struct {
unsigned int matching_orbit_count;
unsigned int unavailable_count;
unsigned int missing_count;
bool reference_valid;
bool sf4_health_valid;
bool sf5_health_valid;
bool time_valid;
bool complete;
} gps_almanac_progress_t;
void gps_almanac_cache_init(
gps_almanac_cache_t* cache,
uint64_t max_cache_age_ticks);
/* Ingests a parity-qualified LNAV subframe already normalized by the tracking
* RTL. Returns true only when an almanac orbit or page-25 reference/health
* record changed. Identical repeats refresh RAM reception age but return
* false so they do not trigger a flash write. */
bool gps_almanac_cache_ingest(
gps_almanac_cache_t* cache,
unsigned char subframe_id,
const unsigned int words[10],
uint64_t now);
bool gps_almanac_cache_progress(
const gps_almanac_cache_t* cache,
uint64_t now,
gpstime_t gps_time,
gps_almanac_progress_t* progress);
bool gps_almanac_cache_complete(
const gps_almanac_cache_t* cache,
uint64_t now,
gpstime_t gps_time);
bool gps_almanac_cache_prn_unavailable(
const gps_almanac_cache_t* cache,
unsigned int prn,
uint64_t now,
gpstime_t gps_time);
/* Converts the reduced-precision orbit into the existing satpos() input
* representation. The result is for acquisition visibility only, never for
* pseudorange/WLS navigation. */
bool gps_almanac_cache_to_ephemeris(
const gps_almanac_cache_t* cache,
unsigned int prn,
uint64_t now,
gpstime_t gps_time,
gps_ephemeris_t* ephemeris,
bool* healthy);
#endif
@@ -0,0 +1,74 @@
#ifndef GPS_ASSISTANCE_DATA_H
#define GPS_ASSISTANCE_DATA_H
#include <stdbool.h>
#include <stdint.h>
#include "gps/NavEquations.h"
#include "gps/gps_almanac.h"
#include "gps/gps_ephemeris_cache.h"
#define GPS_ASSISTANCE_PAYLOAD_MAGIC UINT32_C(0x47504153)
#define GPS_ASSISTANCE_PAYLOAD_VERSION 1u
#define GPS_ASSISTANCE_FLAG_IONOUTC UINT32_C(0x00000001)
#define GPS_ASSISTANCE_FLAG_POSITION UINT32_C(0x00000002)
#define GPS_ASSISTANCE_POSITION_MAX_AGE_SEC (30.0 * 86400.0)
typedef struct {
double ecef_m[3];
double latitude_deg;
double longitude_deg;
double height_m;
gpstime_t gps_time;
bool valid;
} gps_assistance_position_t;
typedef struct {
uint32_t magic;
uint16_t version;
uint16_t payload_size;
uint32_t flags;
uint32_t ephemeris_mask;
gpstime_t saved_time;
gps_ephemeris_t ephemeris[GPS_PRN_COUNT];
gps_almanac_cache_t almanac;
ionoutc_t ionoutc;
gps_assistance_position_t position;
} gps_assistance_payload_t;
typedef struct {
uint32_t ephemeris_mask;
bool almanac_restored;
bool ionoutc_restored;
bool position_restored;
ionoutc_t ionoutc;
gps_assistance_position_t position;
} gps_assistance_restore_result_t;
bool gps_assistance_time_valid(gpstime_t gps_time);
double gps_assistance_time_delta_sec(gpstime_t newer, gpstime_t older);
void gps_assistance_payload_build(
gps_assistance_payload_t* payload,
const gps_ephemeris_cache_t* ephemeris_cache,
const gps_almanac_cache_t* almanac_cache,
const ionoutc_t* ionoutc,
const gps_assistance_position_t* position,
gpstime_t saved_time);
bool gps_assistance_payload_valid(const gps_assistance_payload_t* payload);
bool gps_assistance_ephemeris_issue_matches(
const gps_ephemeris_t* ephemeris,
unsigned char subframe_id,
const unsigned int words[10]);
bool gps_assistance_restore(
const gps_assistance_payload_t* payload,
gpstime_t current_time,
uint64_t now,
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
gps_assistance_restore_result_t* result);
#endif
@@ -0,0 +1,48 @@
#ifndef GPS_ASSISTANCE_STORE_H
#define GPS_ASSISTANCE_STORE_H
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_assistance_data.h"
#define GPS_ASSISTANCE_FLASH_REGION0 UINT32_C(0x01f80000)
#define GPS_ASSISTANCE_FLASH_REGION1 UINT32_C(0x01fc0000)
#define GPS_ASSISTANCE_FLASH_REGION_SIZE UINT32_C(0x00010000)
void gps_assistance_store_init(
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
uint64_t now);
/* Supplies a parity-qualified live subframe. Returns true when a restored
* ephemeris has passed this PRN's first live time/issue check and may hydrate
* its tracking channel. */
bool gps_assistance_store_observe_subframe(
unsigned int prn,
unsigned char subframe_id,
const unsigned int words[10],
unsigned int how_tow_sec,
uint64_t now,
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
gps_ephemeris_t* restored_ephemeris_out,
gps_assistance_position_t* restored_position_out,
gpstime_t* current_time_out);
void gps_assistance_store_note_ephemeris_change(
unsigned int prn,
uint64_t now);
void gps_assistance_store_note_almanac_change(uint64_t now);
void gps_assistance_store_note_ionoutc_change(uint64_t now);
void gps_assistance_store_note_solution(
const gps_nav_solution_t* solution,
gpstime_t gps_time,
uint64_t now);
void gps_assistance_store_service(
gps_ephemeris_cache_t* ephemeris_cache,
const gps_almanac_cache_t* almanac_cache,
uint64_t now);
#endif
@@ -6,9 +6,20 @@
#include "gps/gps_ip_defines.h"
typedef struct {
gpstime_t tow;
uint32_t raw_sample;
double corrected_sample;
uint64_t committed_tick;
bool week_valid;
bool valid;
} gps_cached_time_anchor_t;
typedef struct {
gps_ephemeris_t ephemeris;
uint64_t committed_tick;
uint64_t age_bias_ticks;
gps_cached_time_anchor_t time_anchor;
bool valid;
} gps_ephemeris_cache_entry_t;
@@ -25,11 +36,31 @@ bool gps_ephemeris_cache_store(
unsigned int prn,
const gps_ephemeris_t* ephemeris,
uint64_t now);
bool gps_ephemeris_cache_same_issue(
const gps_ephemeris_t* first,
const gps_ephemeris_t* second);
bool gps_ephemeris_cache_import(
gps_ephemeris_cache_t* cache,
unsigned int prn,
const gps_ephemeris_t* ephemeris,
uint64_t now,
uint64_t age_ticks);
bool gps_ephemeris_cache_restore(
gps_ephemeris_cache_t* cache,
unsigned int prn,
uint64_t now,
gps_ephemeris_t* ephemeris_out);
bool gps_ephemeris_cache_store_time_anchor(
gps_ephemeris_cache_t* cache,
unsigned int prn,
const gps_cached_time_anchor_t* anchor,
uint64_t now);
bool gps_ephemeris_cache_restore_time_anchor(
gps_ephemeris_cache_t* cache,
unsigned int prn,
uint64_t now,
uint64_t max_age_ticks,
gps_cached_time_anchor_t* anchor_out);
void gps_ephemeris_cache_invalidate(
gps_ephemeris_cache_t* cache,
unsigned int prn);
@@ -0,0 +1,68 @@
#ifndef GPS_FLASH_JOURNAL_H
#define GPS_FLASH_JOURNAL_H
#include <stdbool.h>
#include <stdint.h>
#define GPS_FLASH_JOURNAL_PAGE_SIZE 256u
#define GPS_FLASH_JOURNAL_REGION_COUNT 2u
typedef bool (*gps_flash_journal_read_fn)(
void* context, uint32_t address, void* destination, uint32_t length);
typedef bool (*gps_flash_journal_program_fn)(
void* context, uint32_t address, const void* source, uint32_t length);
typedef bool (*gps_flash_journal_erase_fn)(
void* context, uint32_t region_address);
typedef struct {
void* context;
gps_flash_journal_read_fn read;
gps_flash_journal_program_fn program;
gps_flash_journal_erase_fn erase_region;
} gps_flash_journal_io_t;
typedef struct {
gps_flash_journal_io_t io;
uint32_t region_address[GPS_FLASH_JOURNAL_REGION_COUNT];
uint32_t region_size;
uint32_t maximum_payload_size;
uint32_t next_offset[GPS_FLASH_JOURNAL_REGION_COUNT];
uint64_t latest_sequence;
uint32_t latest_payload_crc;
uint32_t latest_payload_size;
unsigned int active_region;
bool latest_valid;
bool initialized;
} gps_flash_journal_t;
typedef enum {
GPS_FLASH_JOURNAL_WRITE_FAILED = 0,
GPS_FLASH_JOURNAL_WRITE_COMMITTED,
GPS_FLASH_JOURNAL_WRITE_UNCHANGED
} gps_flash_journal_write_result_t;
uint32_t gps_flash_journal_crc32(
const void* data,
uint32_t length);
bool gps_flash_journal_init(
gps_flash_journal_t* journal,
const gps_flash_journal_io_t* io,
uint32_t first_region_address,
uint32_t second_region_address,
uint32_t region_size,
uint32_t maximum_payload_size);
bool gps_flash_journal_load_latest(
gps_flash_journal_t* journal,
void* payload,
uint32_t payload_capacity,
uint32_t* payload_size_out,
uint64_t* sequence_out);
gps_flash_journal_write_result_t gps_flash_journal_write(
gps_flash_journal_t* journal,
const void* payload,
uint32_t payload_size);
#endif
@@ -52,6 +52,11 @@
const unsigned char* prns,
unsigned int count);
void gps_acq_scheduler_run(void);
void gps_acq_update_visibility_hint(
const double receiver_ecef_m[3],
double receiver_latitude_deg,
double receiver_longitude_deg,
gpstime_t gps_time);
void gps_background_task(void);
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot);
void gps_get_runtime_diagnostics(gps_runtime_diagnostics_t* diagnostics);
@@ -49,12 +49,17 @@
/* Genesys2 CLINT is 500 kHz. Keep foreground maintenance at
* 10 Hz; the tracking IP's 20 ms NAV timer is independent. */
#define GPS_SYSTICK_HZ 10u
/* LNAV transmits only week modulo 1024. Products built for the
* current constellation era use the 2019 rollover epoch until a
* saved full week is available. */
#define GPS_LNAV_WEEK_ERA_BASE 2048
#define GPS_NAV_SOLVE_TIMER_TICK_MS 20u
#ifndef GPS_NAV_SOLVE_TIMER_TICKS
#define GPS_NAV_SOLVE_TIMER_TICKS 50u
#endif
#define GPS_NAV_BITS_PER_WEEK 30240000u
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
#define GPS_NAV_BITS_PER_SUBFRAME 300u
/* Keep integrity statistics active, but do not let leave-one-out FDE
* replace the full-WLS position until the real measurement-noise model
* is calibrated. The live stationary test produced a 22.7% exclusion
@@ -62,20 +67,42 @@
#define GPS_NAV_ENABLE_FDE 0u
#define GPS_PROFILE_CPU_HZ 50000000ULL
#define GPS_ACQ_RETRY_DELAY_TICKS (GPS_SYSTICK_HZ / 2u)
#define GPS_ACQ_HOT_RETRY_DELAY_TICKS (GPS_SYSTICK_HZ * 30u)
/* A failed reacquisition scan must not impose a 30 s blind period.
* Keep a short per-PRN delay so one absent SV cannot monopolize the
* engine, while a restored signal is revisited within two seconds. */
#define GPS_ACQ_HOT_RETRY_DELAY_TICKS (GPS_SYSTICK_HZ * 2u)
#define GPS_ACQ_REACQ_PRIORITY_TICKS \
(GPS_SYSTICK_HZ * 2u * 60u * 60u)
#define GPS_ACQ_MAX_HOT_STREAK 4u
#define GPS_ACQ_STUCK_WARN_TICKS (GPS_SYSTICK_HZ * 8u)
#define GPS_ACQ_VISIBILITY_UPDATE_TICKS (GPS_SYSTICK_HZ * 10u)
/* Satellites without an orbit model cannot be classified by location.
* Probe one unknown PRN at a controlled rate until tracking capacity is
* filled; do not let one failing predicted-visible PRN block discovery. */
#define GPS_ACQ_UNKNOWN_PROBE_TICKS (GPS_SYSTICK_HZ * 2u)
#define GPS_ACQ_VISIBILITY_PROBE_TICKS (GPS_SYSTICK_HZ * 5u)
#define GPS_ACQ_MIN_ELEVATION_DEG 5.0
#define GPS_ACQ_MIN_SKY_MODEL_VISIBLE_COUNT 6u
#define GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS \
(GPS_SYSTICK_HZ * 2u * 60u * 60u)
/* Almanac is a coarse acquisition aid. Reject RAM records that have
* not been observed for a week; GPS-time/toa age is checked separately. */
#define GPS_ALMANAC_CACHE_MAX_AGE_TICKS \
(GPS_SYSTICK_HZ * 7u * 24u * 60u * 60u)
#define GPS_ACQ_STATUS_LOG_PERIOD_TICKS (GPS_SYSTICK_HZ * 5u)
#define GPS_NAV_STATUS_LOG_PERIOD_TICKS GPS_SYSTICK_HZ
#define GPS_TRK_MEASUREMENT_STALE_TICKS (GPS_SYSTICK_HZ * 3u)
#define GPS_TRK_MEASUREMENT_LIVENESS_TICKS (GPS_SYSTICK_HZ * 2u)
#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
/* A signed 32-bit RF-sample delta is unambiguous for 131.2 s.
* Keep margin below that limit while covering a 30 s tunnel plus
* acquisition time. */
#define GPS_HOW_ANCHOR_MAX_AGE_SEC 120.0
#define GPS_HOW_REACQ_ANCHOR_MAX_AGE_TICKS \
(GPS_SYSTICK_HZ * 110u)
#define GPS_HOW_REACQ_LOCAL_TIMER_TOLERANCE_SEC 0.30
#define GPS_HOW_CONTINUITY_MAX_GAP_SEC 120.0
#define GPS_HOW_CONTINUITY_TOLERANCE_SEC 0.050
@@ -83,9 +110,13 @@
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
#elif ((GPS_NAV_BITS_PER_WEEK % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide 30240000 GPS navigation bits per week"
#elif ((GPS_NAV_BITS_PER_SUBFRAME % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide the 300-bit LNAV subframe"
#endif
_Static_assert((GPS_NAV_ENABLE_FDE == 0u) || (GPS_NAV_ENABLE_FDE == 1u),
"GPS_NAV_ENABLE_FDE must be zero or one");
_Static_assert(GPS_ACQ_MIN_SKY_MODEL_VISIBLE_COUNT <= GPS_PRN_COUNT,
"sky-model visible count threshold exceeds PRN count");
#if (ACQ_ENGINE_COUNT < 1UL)
#error "ACQ_ENGINE_COUNT must be at least 1"
@@ -187,8 +218,6 @@
#define SECONDS_IN_MINUTE 60.0
//#define SAMPLE_RATE 16368000
#define STARTOFFSET 68.802
/*! \brief Maximum number of iterations for least square search */
#define NUMBER_OF_ITERATIONS (100)
@@ -247,7 +276,7 @@
uint32_t NAV_BIT_READ_REG9; // 'h3c
uint32_t TIMEOUT_REG; // 'h40
uint32_t NAV_SOLVE_TIMER; // 'h44
uint32_t RESERVED_48; // 'h48
uint32_t NAV_SOLVE_PHASE; // 'h48, write seeds timer phase
uint32_t REM_CODE_PHASE_REG; // 'h4c
uint32_t MEASUREMENT_META; // 'h50
uint32_t MEASUREMENT_SEQUENCE; // 'h54
@@ -286,6 +315,7 @@
_Static_assert(offsetof(gps_trck_t, PRN_NUMBER) == 0x04, "gps_trck_t PRN offset mismatch");
_Static_assert(offsetof(gps_trck_t, STATUS_REG) == 0x10, "gps_trck_t STATUS offset mismatch");
_Static_assert(offsetof(gps_trck_t, DOPPLER_REG) == 0x14, "gps_trck_t DOPPLER offset mismatch");
_Static_assert(offsetof(gps_trck_t, NAV_SOLVE_PHASE) == 0x48, "gps_trck_t NAV phase offset mismatch");
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_META) == 0x50, "gps_trck_t measurement meta offset mismatch");
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_CARRIER_Q7) == 0x58, "gps_trck_t measurement carrier offset mismatch");
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_WBP_HI) == 0x70, "gps_trck_t measurement WBP offset mismatch");
@@ -477,6 +507,7 @@
void trck_disable_measurement_flag(volatile gps_trck_t* trck_ip);
void trck_set_nav_solve_timer(unsigned int tick, volatile gps_trck_t* trck_ip); // in case of gps, 1 tick is 20 ms.
void trck_seed_nav_solve_phase(unsigned int phase, volatile gps_trck_t* trck_ip);
void trck_clear_intr_flag(volatile gps_trck_t* trck_ip); // clear tracking generic interrupt flag.
@@ -502,6 +533,7 @@
#endif
}
bool gps_get_ionoutc(ionoutc_t* ionoutc);
void gps_set_ionoutc(const ionoutc_t* ionoutc);
void gps_background_task(void);
void gps_process_isr_events(void);
void gps_acq_scheduler_run(void);
@@ -0,0 +1,51 @@
#ifndef GPS_MEASUREMENT_FRESHNESS_H_INCLUDED
#define GPS_MEASUREMENT_FRESHNESS_H_INCLUDED
#include <stdbool.h>
#include <stdint.h>
/*
* Tracking has two fundamentally different event cadences. During cold
* acquisition, parity-checked LNAV subframes arrive every six seconds. Once
* ephemeris is ready, the receiver arms the short-period NAV-solve stream.
* Keeping that phase explicit prevents a periodic-measurement deadline from
* being applied to LNAV collection.
*/
typedef enum {
GPS_MEASUREMENT_PHASE_LNAV = 0,
GPS_MEASUREMENT_PHASE_NAV_SOLVE_ARMED,
GPS_MEASUREMENT_PHASE_NAV_SOLVE_ACTIVE
} gps_measurement_phase_t;
typedef struct {
uint64_t last_event_tick;
bool valid;
gps_measurement_phase_t phase;
} gps_measurement_freshness_t;
void gps_measurement_freshness_init(
volatile gps_measurement_freshness_t* freshness);
void gps_measurement_freshness_note_lnav(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick);
void gps_measurement_freshness_arm_nav_solve(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick);
void gps_measurement_freshness_note_nav_solve(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick);
void gps_measurement_freshness_invalidate(
volatile gps_measurement_freshness_t* freshness);
bool gps_measurement_freshness_expired(
const volatile gps_measurement_freshness_t* freshness,
uint64_t now,
uint64_t periodic_deadline_ticks);
bool gps_measurement_freshness_usable(
const volatile gps_measurement_freshness_t* freshness,
uint64_t now,
uint64_t periodic_deadline_ticks);
#endif
@@ -0,0 +1,22 @@
#ifndef GPS_NAV_PHASE_H
#define GPS_NAV_PHASE_H
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_ip_defines.h"
static inline bool gps_nav_phase_control_armed(uint32_t control)
{
return ((control & (1u << GPS_TRCK_NAV_SOLVE_INTR_EN_POS)) != 0u) &&
((control & (1u << GPS_TRCK_NAV_SOLVE_FLAG_EN_POS)) != 0u);
}
static inline bool gps_nav_phase_preserve_seed_after_how(
bool previously_seeded,
uint32_t control)
{
return previously_seeded && gps_nav_phase_control_armed(control);
}
#endif
@@ -2,6 +2,9 @@
#define GPS_PPS_QUALITY_H
#include <stdbool.h>
#include <stdint.h>
#define GPS_PPS_BASELINE_UPDATE_PERIOD_SEC 0.200
typedef enum {
GPS_PPS_PROFILE_HOLDOVER = 0,
@@ -29,6 +32,8 @@ typedef struct {
gps_pps_profile_t profile;
gps_pps_profile_t candidate;
unsigned int candidate_updates;
unsigned int upgrade_updates;
unsigned int degrade_updates;
} gps_pps_quality_selector_t;
typedef struct {
@@ -43,13 +48,17 @@ typedef struct {
} gps_pps_loop_profile_t;
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector);
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec);
/*
* Select a bounded loop profile from timing geometry, code/carrier quality
* and integrity. Upgrades require sustained evidence and degradations require
* three consecutive observations. A single rejected WLS epoch is skipped
* without resetting the loop; sustained loss enters holdover, while the
* independent reference watchdog remains the final stale-data guard.
* and integrity. Upgrades and degradations require sustained evidence; their
* update counts are scaled from the proven 200 ms timing when period-aware
* initialization is used. A single rejected WLS epoch is skipped without
* resetting the loop; sustained loss enters holdover, while the independent
* reference watchdog remains the final stale-data guard.
*/
gps_pps_profile_t gps_pps_quality_selector_update(
gps_pps_quality_selector_t* selector,
@@ -57,6 +66,22 @@ gps_pps_profile_t gps_pps_quality_selector_update(
bool* profile_changed_out);
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile);
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec);
/*
* Map the proven 200 ms discrete poles to another update period. The pole
* exponentiation preserves the current loop's continuous-time settling and
* damping while returning exactly the legacy coefficients at 200 ms.
*/
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out);
const char* gps_pps_profile_name(gps_pps_profile_t profile);
#endif
@@ -39,10 +39,11 @@ bool gps_pps_select_reference(
unsigned int* reference_index_out);
/*
* The position WLS observations are formed from capture_i - min(capture).
* That subtraction removes the common receiver-clock offset and drift before
* WLS. The fitted clock-bias datum is STARTOFFSET*c - reference_range (plus
* the reference satellite clock correction), so its derivative is
* The position WLS observations are formed from corrected RF-sample and
* satellite-transmit-time differences relative to one stable reference.
* That subtraction removes the arbitrary RF-counter origin before WLS. The
* fitted clock-bias datum is -reference_range (plus the reference satellite
* clock correction), so its derivative is
* -reference_range_rate + c*reference_satellite_clock_drift.
*/
bool gps_pps_reference_bias_drift(
@@ -58,7 +59,9 @@ bool gps_pps_reference_bias_drift(
* linear oscillator drift. The Doppler WLS clock-drift state predicts the
* bias at the new capture epoch; only the residual bias noise is low-passed.
* If Doppler is temporarily unavailable, a bounded, slower finite-difference
* drift estimate keeps the filter usable in the robust profile.
* drift estimate keeps the filter usable in the robust profile. All three
* baseline per-update gains are converted from the actual elapsed time, so
* changing the NAV solve period does not change their real-time poles.
*
* COARSE is returned during initial settling, a reference-datum change, or
* after a gap too long for bounded Doppler prediction. The independent
@@ -70,6 +70,55 @@ bool gps_time_correct_capture_counter(
uint64_t* corrected_counter_out,
double* applied_correction_sec_out);
/*
* Form one clock-datum-free code observation from the free-running receiver
* counter and the decoded satellite transmit time. The returned value is
*
* (receiver_i - receiver_reference) -
* (transmit_i - transmit_reference)
*
* in seconds. Multiplying by c gives a pseudorange with an arbitrary common
* offset; the fourth WLS state estimates that offset. No receiver startup
* delay or calibrated signal-flight-time constant is required.
*/
bool gps_time_differential_observation(
int reference_week,
double reference_tow_sec,
uint64_t reference_counter,
int event_week,
double event_tow_sec,
uint64_t event_counter,
double counter_frequency_hz,
double* observation_sec_out);
/*
* Form the same clock-datum-free observation from the receiver's shared RF
* sample counter. The caller supplies samples already unwrapped around the
* selected epoch reference. This counter is latched with code phase in the
* tracking clock domain and therefore preserves the precision that would be
* lost by independently crossing each event into the PPS/APB clock domain.
*/
bool gps_time_sample_differential_observation(
int reference_week,
double reference_tow_sec,
double reference_sample,
int event_week,
double event_tow_sec,
double event_sample,
double samples_per_second,
double* observation_sec_out);
/* Recover receiver GPS TOW from the WLS bias of the differential observation
* above. The reference row's receiver and transmit differences are zero, so
* receiver time is corrected_transmit_tow - bias/c. The caller may pass a
* value just outside the GPS week; gps_pps_commit_solution() normalizes it and
* adjusts the week number atomically. */
bool gps_time_receiver_tow_from_differential_bias(
double corrected_transmit_tow_sec,
double wls_clock_bias_m,
double propagation_speed_mps,
double* receiver_tow_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. */
@@ -85,4 +134,15 @@ bool gps_time_anchors_consistent(int first_week,
double max_gap_sec,
double tolerance_sec);
/* Cross-check an RF-counter interval against an independent free-running CPU
* timer. This detects an RF/MMCM/sample-counter reset while retaining cached
* ephemeris across the outage. */
bool gps_time_elapsed_consistent_with_local_timer(
double rf_elapsed_sec,
uint64_t anchor_local_tick,
uint64_t event_local_tick,
double local_ticks_per_second,
double tolerance_sec,
double* local_elapsed_sec_out);
#endif
@@ -0,0 +1,16 @@
#ifndef GPS_VISIBILITY_H
#define GPS_VISIBILITY_H
#include <stdbool.h>
/* Compute geometric elevation from ECEF positions and the receiver's
* geodetic latitude/longitude. This intentionally has no acquisition state
* so the sky-mask geometry is host-testable. */
bool gps_visibility_elevation_deg(
const double receiver_ecef_m[3],
double receiver_latitude_deg,
double receiver_longitude_deg,
const double satellite_ecef_m[3],
double* elevation_deg_out);
#endif
@@ -2,6 +2,7 @@
#define LINKED_LIST_H_INCLUDED
#include "gps/gps_ip_defines.h"
#include "gps/gps_measurement_freshness.h"
typedef struct gps_ephem_ll {
volatile gps_ephemeris_t* data_p;
@@ -14,6 +15,7 @@ typedef struct gps_ephem_ll {
volatile uint64_t gps_how_tick;
volatile bool gps_how_anchor_valid;
volatile bool gps_how_week_valid;
volatile bool gps_nav_phase_seeded;
volatile gpstime_t gps_pending_how_tow;
volatile uint32_t gps_pending_how_abs_sample;
volatile double gps_pending_how_corrected_abs_sample;
@@ -25,8 +27,7 @@ typedef struct gps_ephem_ll {
volatile uint64_t gps_last_nav_solve_local_ticks;
volatile uint64_t gps_last_nav_solve_pps_counter;
volatile uint32_t gps_last_pps_capture_sequence;
volatile uint64_t gps_last_measurement_tick;
volatile bool gps_measurement_valid;
gps_measurement_freshness_t gps_measurement_freshness;
} gps_ephem_ll_t;
extern volatile gps_ephem_ll_t* tracking_node;
@@ -681,6 +681,24 @@ static bool least_squares_build_postfit(const mat_type* satpos,
static bool gps_wls_last_solution_valid = false;
static double gps_wls_last_solution[4] = {0.0, 0.0, 0.0, 0.0};
void gps_wls_seed_position(const double receiver_ecef_m[3])
{
if((receiver_ecef_m == NULL) ||
!isfinite(receiver_ecef_m[0]) ||
!isfinite(receiver_ecef_m[1]) ||
!isfinite(receiver_ecef_m[2]))
{
return;
}
gps_wls_last_solution[0] = receiver_ecef_m[0];
gps_wls_last_solution[1] = receiver_ecef_m[1];
gps_wls_last_solution[2] = receiver_ecef_m[2];
/* Receiver clock phase has no meaning after a CPU/fabric restart. It stays
* the fourth WLS unknown and deliberately starts at zero. */
gps_wls_last_solution[3] = 0.0;
gps_wls_last_solution_valid = true;
}
// Function leastSquaresSTD
// Definition : initialize all matc cpp class variables.
// Arguments :
@@ -30,13 +30,50 @@ static bool gps_acq_manager_hot_active(
(now <= entry->hot_until_tick);
}
typedef enum {
GPS_ACQ_VISIBILITY_ANY = 0,
GPS_ACQ_VISIBILITY_PREDICTED,
GPS_ACQ_VISIBILITY_UNKNOWN,
GPS_ACQ_VISIBILITY_BELOW_MASK
} gps_acq_visibility_filter_t;
static bool gps_acq_manager_visibility_matches(
const gps_acq_manager_t* manager,
const gps_acq_prn_entry_t* entry,
gps_acq_visibility_filter_t filter)
{
if(!manager->visibility_active ||
(filter == GPS_ACQ_VISIBILITY_ANY))
{
return true;
}
switch(filter)
{
case GPS_ACQ_VISIBILITY_PREDICTED:
return entry->visibility_known &&
entry->predicted_visible;
case GPS_ACQ_VISIBILITY_UNKNOWN:
return !entry->visibility_known;
case GPS_ACQ_VISIBILITY_BELOW_MASK:
return entry->visibility_known &&
!entry->predicted_visible;
case GPS_ACQ_VISIBILITY_ANY:
default:
return true;
}
}
static bool gps_acq_manager_find(
const gps_acq_manager_t* manager,
uint64_t now,
bool hot,
unsigned int cursor,
gps_acq_visibility_filter_t visibility_filter,
unsigned int* prn_out)
{
bool found = false;
int best_priority = 0;
for(unsigned int offset = 0u;
offset < GPS_ACQ_MANAGER_PRN_COUNT;
offset++)
@@ -49,15 +86,67 @@ static bool gps_acq_manager_find(
: ((entry->state == GPS_ACQ_PRN_COLD) ||
((entry->state == GPS_ACQ_PRN_HOT) &&
(now > entry->hot_until_tick)));
if(candidate &&
bool visibility_allowed =
gps_acq_manager_visibility_matches(
manager, entry, visibility_filter);
if(candidate && visibility_allowed &&
gps_acq_manager_retry_due(
manager, entry, now, hot))
{
*prn_out = prn;
return true;
int priority = (manager->visibility_active &&
entry->visibility_known)
? entry->visibility_priority : 0;
if(!found || (priority > best_priority))
{
*prn_out = prn;
best_priority = priority;
found = true;
}
}
}
return false;
return found;
}
void gps_acq_manager_set_visibility_mode(
gps_acq_manager_t* manager,
bool active,
uint64_t now)
{
if(manager == NULL)
{
return;
}
bool was_active = manager->visibility_active;
manager->visibility_active = active;
if(active && !was_active)
{
uint64_t unknown_interval =
manager->config.unknown_probe_interval_ticks;
uint64_t visibility_interval =
manager->config.visibility_probe_interval_ticks;
manager->next_unknown_probe_tick = now +
((unknown_interval != 0u) ? unknown_interval : 1u);
manager->next_visibility_probe_tick = now +
((visibility_interval != 0u) ? visibility_interval : 1u);
}
}
bool gps_acq_manager_set_visibility(
gps_acq_manager_t* manager,
unsigned int prn,
bool known,
bool predicted_visible,
int priority)
{
if((manager == NULL) || !gps_acq_manager_prn_valid(prn))
{
return false;
}
gps_acq_prn_entry_t* entry = &manager->prn[prn];
entry->visibility_known = known;
entry->predicted_visible = known && predicted_visible;
entry->visibility_priority = priority;
return true;
}
void gps_acq_manager_init(
@@ -219,10 +308,47 @@ bool gps_acq_manager_start_next(
unsigned int hot_prn = GPS_ACQ_MANAGER_INVALID_PRN;
unsigned int cold_prn = GPS_ACQ_MANAGER_INVALID_PRN;
/* A recently tracked PRN is empirical visibility evidence. Never let an
* incomplete or slightly stale sky model suppress its hot reacquisition. */
bool have_hot = gps_acq_manager_find(
manager, now, true, manager->hot_cursor, &hot_prn);
manager, now, true, manager->hot_cursor,
GPS_ACQ_VISIBILITY_ANY, &hot_prn);
bool have_cold = gps_acq_manager_find(
manager, now, false, manager->cold_cursor, &cold_prn);
manager, now, false, manager->cold_cursor,
GPS_ACQ_VISIBILITY_PREDICTED, &cold_prn);
bool unknown_probe = false;
bool visibility_probe = false;
if(manager->visibility_active &&
(now >= manager->next_unknown_probe_tick))
{
/* Unknown-orbit discovery has its own deadline. It must not depend on
* the predicted-visible queue becoming empty: one absent visible PRN
* would otherwise prevent discovery forever. */
unsigned int unknown_prn = GPS_ACQ_MANAGER_INVALID_PRN;
if(gps_acq_manager_find(
manager, now, false, manager->cold_cursor,
GPS_ACQ_VISIBILITY_UNKNOWN, &unknown_prn))
{
cold_prn = unknown_prn;
have_cold = true;
unknown_probe = true;
}
}
if(manager->visibility_active && !unknown_probe &&
(now >= manager->next_visibility_probe_tick))
{
/* Known below-mask SVs remain suppressed except for this much slower
* stale-position/ephemeris safety check. */
unsigned int below_mask_prn = GPS_ACQ_MANAGER_INVALID_PRN;
if(gps_acq_manager_find(
manager, now, false, manager->cold_cursor,
GPS_ACQ_VISIBILITY_BELOW_MASK, &below_mask_prn))
{
cold_prn = below_mask_prn;
have_cold = true;
visibility_probe = true;
}
}
bool choose_hot = have_hot &&
(!have_cold ||
(manager->hot_streak < manager->config.max_hot_streak));
@@ -252,6 +378,20 @@ bool gps_acq_manager_start_next(
entry->last_attempt_tick = now;
entry->has_attempted = true;
entry->state = GPS_ACQ_PRN_ACQUIRING;
if(!choose_hot && unknown_probe)
{
uint64_t interval =
manager->config.unknown_probe_interval_ticks;
manager->next_unknown_probe_tick = now +
((interval != 0u) ? interval : 1u);
}
else if(!choose_hot && visibility_probe)
{
uint64_t interval =
manager->config.visibility_probe_interval_ticks;
manager->next_visibility_probe_tick = now +
((interval != 0u) ? interval : 1u);
}
*prn_out = prn;
if(hot_out != NULL)
{
@@ -0,0 +1,387 @@
#include "gps/gps_almanac.h"
#include <math.h>
#include <stddef.h>
#include <string.h>
#define GPS_ALMANAC_DATA_ID_LNAV 1u
#define GPS_ALMANAC_SF5_REFERENCE_SV_ID 51u
#define GPS_ALMANAC_SF4_HEALTH_SV_ID 63u
#define GPS_ALMANAC_WEEK_SECONDS 604800.0
#define GPS_ALMANAC_MAX_REFERENCE_DISTANCE_SEC (7.0 * 86400.0)
#define GPS_ALMANAC_PI 3.1415926535897932384626433832795
static int32_t gps_almanac_sign_extend(uint32_t value, unsigned int bits)
{
uint32_t sign = 1u << (bits - 1u);
return (int32_t)((value ^ sign) - sign);
}
static bool gps_almanac_tick_fresh(
const gps_almanac_cache_t* cache,
uint64_t now,
uint64_t tick)
{
return (now >= tick) &&
((cache->max_cache_age_ticks == 0u) ||
((now - tick) <= cache->max_cache_age_ticks));
}
static int gps_almanac_resolve_week(
unsigned int week_mod256,
int current_week)
{
int candidate = (current_week / 256) * 256 +
(int)(week_mod256 & 0xffu);
if((candidate - current_week) > 128)
{
candidate -= 256;
}
else if((current_week - candidate) > 128)
{
candidate += 256;
}
return candidate;
}
static double gps_almanac_reference_delta_sec(
const gps_almanac_cache_t* cache,
gpstime_t gps_time)
{
int reference_week = gps_almanac_resolve_week(
cache->reference_week_mod256, gps_time.week);
return ((double)(gps_time.week - reference_week) *
GPS_ALMANAC_WEEK_SECONDS) +
gps_time.sec - cache->reference_toa_sec;
}
static bool gps_almanac_reference_fresh(
const gps_almanac_cache_t* cache,
uint64_t now,
gpstime_t gps_time)
{
if(!cache->reference_valid ||
!gps_almanac_tick_fresh(cache, now, cache->reference_tick) ||
(gps_time.week < 0) || !isfinite(gps_time.sec) ||
(gps_time.sec < 0.0) ||
(gps_time.sec >= GPS_ALMANAC_WEEK_SECONDS))
{
return false;
}
return fabs(gps_almanac_reference_delta_sec(cache, gps_time)) <=
GPS_ALMANAC_MAX_REFERENCE_DISTANCE_SEC;
}
static bool gps_almanac_decode_orbit(
const unsigned int words[10],
gps_almanac_entry_t* entry)
{
uint32_t af0_raw = (((words[9] >> 22) & 0xffu) << 3) |
((words[9] >> 8) & 0x07u);
uint32_t af1_raw = (words[9] >> 11) & 0x7ffu;
entry->eccentricity =
(double)((words[2] >> 6) & 0xffffu) * 0x1p-21;
entry->toa_sec =
(double)((words[3] >> 22) & 0xffu) * 4096.0;
entry->delta_i_rad =
(double)gps_almanac_sign_extend(
(words[3] >> 6) & 0xffffu, 16u) *
0x1p-19 * GPS_ALMANAC_PI;
entry->omega_dot_rad_per_sec =
(double)gps_almanac_sign_extend(
(words[4] >> 14) & 0xffffu, 16u) *
0x1p-38 * GPS_ALMANAC_PI;
entry->health8 = (unsigned char)((words[4] >> 6) & 0xffu);
entry->sqrt_a =
(double)((words[5] >> 6) & 0xffffffu) * 0x1p-11;
entry->omega0_rad =
(double)gps_almanac_sign_extend(
(words[6] >> 6) & 0xffffffu, 24u) *
0x1p-23 * GPS_ALMANAC_PI;
entry->argument_of_perigee_rad =
(double)gps_almanac_sign_extend(
(words[7] >> 6) & 0xffffffu, 24u) *
0x1p-23 * GPS_ALMANAC_PI;
entry->mean_anomaly_rad =
(double)gps_almanac_sign_extend(
(words[8] >> 6) & 0xffffffu, 24u) *
0x1p-23 * GPS_ALMANAC_PI;
entry->af0_sec =
(double)gps_almanac_sign_extend(af0_raw, 11u) * 0x1p-20;
entry->af1_sec_per_sec =
(double)gps_almanac_sign_extend(af1_raw, 11u) * 0x1p-38;
return (entry->eccentricity >= 0.0) &&
(entry->eccentricity <= 0.03) &&
(entry->toa_sec >= 0.0) &&
(entry->toa_sec < GPS_ALMANAC_WEEK_SECONDS) &&
(entry->sqrt_a >= 2530.0) &&
(entry->sqrt_a < 8192.0);
}
static void gps_almanac_decode_health_group(
uint32_t data24,
unsigned int first_prn,
unsigned int count,
gps_almanac_cache_t* cache)
{
for(unsigned int i = 0u ; i < count ; i++)
{
unsigned int shift = 18u - (6u * i);
unsigned int prn = first_prn + i;
cache->health6[prn] =
(unsigned char)((data24 >> shift) & 0x3fu);
cache->health6_mask |= UINT64_C(1) << prn;
}
}
static bool gps_almanac_ingest_sf5_reference(
gps_almanac_cache_t* cache,
const unsigned int words[10],
uint64_t now)
{
unsigned int toa_raw = (words[2] >> 14) & 0xffu;
unsigned int week_mod256 = (words[2] >> 6) & 0xffu;
unsigned char previous_health[24];
memcpy(previous_health, cache->health6, sizeof(previous_health));
uint64_t previous_mask = cache->health6_mask;
double previous_toa = cache->reference_toa_sec;
unsigned int previous_week = cache->reference_week_mod256;
bool previous_valid = cache->reference_valid;
for(unsigned int word = 3u ; word <= 8u ; word++)
{
gps_almanac_decode_health_group(
(words[word] >> 6) & 0xffffffu,
(word - 3u) * 4u, 4u, cache);
}
cache->reference_toa_sec = (double)(toa_raw << 12);
cache->reference_week_mod256 = week_mod256;
cache->reference_tick = now;
cache->reference_valid = true;
return !previous_valid ||
(previous_toa != cache->reference_toa_sec) ||
(previous_week != cache->reference_week_mod256) ||
((previous_mask & UINT64_C(0x00ffffff)) !=
(cache->health6_mask & UINT64_C(0x00ffffff))) ||
(memcmp(previous_health, cache->health6,
sizeof(previous_health)) != 0);
}
static bool gps_almanac_ingest_sf4_health(
gps_almanac_cache_t* cache,
const unsigned int words[10],
uint64_t now)
{
unsigned char previous_health[8];
memcpy(previous_health, &cache->health6[24], sizeof(previous_health));
uint64_t previous_mask = cache->health6_mask;
bool previous_valid = cache->sf4_health_valid;
cache->health6[24] = (unsigned char)((words[7] >> 6) & 0x3fu);
cache->health6_mask |= UINT64_C(1) << 24;
gps_almanac_decode_health_group(
(words[8] >> 6) & 0xffffffu, 25u, 4u, cache);
/* Word 10 carries PRNs 30..32 in its 18 MSBs. Align them with the same
* 24-bit health-group helper by padding six zero LSBs. */
gps_almanac_decode_health_group(
((words[9] >> 12) & 0x3ffffu) << 6, 29u, 3u, cache);
cache->sf4_health_tick = now;
cache->sf4_health_valid = true;
return !previous_valid ||
((previous_mask & UINT64_C(0xff000000)) !=
(cache->health6_mask & UINT64_C(0xff000000))) ||
(memcmp(previous_health, &cache->health6[24],
sizeof(previous_health)) != 0);
}
void gps_almanac_cache_init(
gps_almanac_cache_t* cache,
uint64_t max_cache_age_ticks)
{
if(cache == NULL)
{
return;
}
memset(cache, 0, sizeof(*cache));
cache->max_cache_age_ticks = max_cache_age_ticks;
}
bool gps_almanac_cache_ingest(
gps_almanac_cache_t* cache,
unsigned char subframe_id,
const unsigned int words[10],
uint64_t now)
{
if((cache == NULL) || (words == NULL) ||
((subframe_id != 4u) && (subframe_id != 5u)))
{
return false;
}
unsigned int data_id = (words[2] >> 28) & 0x03u;
unsigned int sv_id = (words[2] >> 22) & 0x3fu;
if(data_id != GPS_ALMANAC_DATA_ID_LNAV)
{
return false;
}
if((subframe_id == 5u) &&
(sv_id == GPS_ALMANAC_SF5_REFERENCE_SV_ID))
{
return gps_almanac_ingest_sf5_reference(cache, words, now);
}
if((subframe_id == 4u) &&
(sv_id == GPS_ALMANAC_SF4_HEALTH_SV_ID))
{
return gps_almanac_ingest_sf4_health(cache, words, now);
}
if((sv_id < 1u) || (sv_id > GPS_ALMANAC_PRN_COUNT))
{
return false;
}
unsigned int prn = sv_id - 1u;
gps_almanac_entry_t decoded;
memset(&decoded, 0, sizeof(decoded));
if(!gps_almanac_decode_orbit(words, &decoded))
{
return false;
}
decoded.received_tick = now;
decoded.valid = true;
gps_almanac_entry_t previous = cache->entry[prn];
previous.received_tick = 0u;
gps_almanac_entry_t comparable = decoded;
comparable.received_tick = 0u;
bool changed =
(cache->entry_mask & (UINT64_C(1) << prn)) == 0u ||
(memcmp(&previous, &comparable, sizeof(previous)) != 0);
cache->entry[prn] = decoded;
cache->entry_mask |= UINT64_C(1) << prn;
return changed;
}
bool gps_almanac_cache_progress(
const gps_almanac_cache_t* cache,
uint64_t now,
gpstime_t gps_time,
gps_almanac_progress_t* progress)
{
if((cache == NULL) || (progress == NULL))
{
return false;
}
memset(progress, 0, sizeof(*progress));
progress->reference_valid = cache->reference_valid &&
gps_almanac_tick_fresh(cache, now, cache->reference_tick);
progress->sf4_health_valid = cache->sf4_health_valid &&
gps_almanac_tick_fresh(cache, now, cache->sf4_health_tick);
progress->sf5_health_valid =
(cache->health6_mask & UINT64_C(0x00ffffff)) ==
UINT64_C(0x00ffffff);
progress->time_valid = gps_almanac_reference_fresh(
cache, now, gps_time);
for(unsigned int prn = 0u ; prn < GPS_ALMANAC_PRN_COUNT ; prn++)
{
bool health_known =
(cache->health6_mask & (UINT64_C(1) << prn)) != 0u;
bool unavailable = health_known &&
(cache->health6[prn] == GPS_ALMANAC_UNAVAILABLE_HEALTH);
bool orbit_matches = cache->entry[prn].valid &&
gps_almanac_tick_fresh(
cache, now, cache->entry[prn].received_tick) &&
cache->reference_valid &&
(cache->entry[prn].toa_sec == cache->reference_toa_sec);
if(unavailable)
{
progress->unavailable_count++;
}
else if(orbit_matches)
{
progress->matching_orbit_count++;
}
else
{
progress->missing_count++;
}
}
progress->complete = progress->reference_valid &&
progress->sf4_health_valid &&
progress->sf5_health_valid &&
progress->time_valid &&
(progress->missing_count == 0u);
return true;
}
bool gps_almanac_cache_complete(
const gps_almanac_cache_t* cache,
uint64_t now,
gpstime_t gps_time)
{
gps_almanac_progress_t progress;
return gps_almanac_cache_progress(
cache, now, gps_time, &progress) && progress.complete;
}
bool gps_almanac_cache_prn_unavailable(
const gps_almanac_cache_t* cache,
unsigned int prn,
uint64_t now,
gpstime_t gps_time)
{
return (cache != NULL) && (prn < GPS_ALMANAC_PRN_COUNT) &&
gps_almanac_cache_complete(cache, now, gps_time) &&
(cache->health6[prn] == GPS_ALMANAC_UNAVAILABLE_HEALTH);
}
bool gps_almanac_cache_to_ephemeris(
const gps_almanac_cache_t* cache,
unsigned int prn,
uint64_t now,
gpstime_t gps_time,
gps_ephemeris_t* ephemeris,
bool* healthy)
{
if((cache == NULL) || (ephemeris == NULL) ||
(prn >= GPS_ALMANAC_PRN_COUNT) ||
!gps_almanac_cache_complete(cache, now, gps_time))
{
return false;
}
const gps_almanac_entry_t* entry = &cache->entry[prn];
if(!entry->valid ||
!gps_almanac_tick_fresh(cache, now, entry->received_tick) ||
(entry->toa_sec != cache->reference_toa_sec) ||
(cache->health6[prn] == GPS_ALMANAC_UNAVAILABLE_HEALTH))
{
return false;
}
bool health_ok = (entry->health8 == 0u) &&
(cache->health6[prn] == 0u);
memset(ephemeris, 0, sizeof(*ephemeris));
ephemeris->WN = (unsigned int)gps_almanac_resolve_week(
cache->reference_week_mod256, gps_time.week);
ephemeris->tow = gps_time;
ephemeris->toe.week = (int)ephemeris->WN;
ephemeris->toe.sec = entry->toa_sec;
ephemeris->toc = ephemeris->toe;
ephemeris->ecc = entry->eccentricity;
ephemeris->sqrta = entry->sqrt_a;
ephemeris->A = entry->sqrt_a * entry->sqrt_a;
ephemeris->inc0 = (0.30 * GPS_ALMANAC_PI) + entry->delta_i_rad;
ephemeris->omgdot = entry->omega0_rad;
ephemeris->omg0 = entry->argument_of_perigee_rad;
ephemeris->omgkdot = entry->omega_dot_rad_per_sec;
ephemeris->m0 = entry->mean_anomaly_rad;
ephemeris->af0 = entry->af0_sec;
ephemeris->af1 = entry->af1_sec_per_sec;
ephemeris->svhlth = health_ok ? 0u : 1u;
ephemeris->update = 0u;
ephemeris->ephemeris_valid = true;
ephemeris->SVID = (unsigned char)prn;
if(healthy != NULL)
{
*healthy = health_ok;
}
return true;
}
@@ -0,0 +1,288 @@
#include "gps/gps_assistance_data.h"
#include <math.h>
#include <stddef.h>
#include <string.h>
#include "gps/gps_time_anchor.h"
#define GPS_ASSISTANCE_WEEK_SECONDS 604800.0
#define GPS_ASSISTANCE_ALMANAC_MAX_AGE_SEC (7.0 * 86400.0)
bool gps_assistance_time_valid(gpstime_t gps_time)
{
return (gps_time.week >= 0) && isfinite(gps_time.sec) &&
(gps_time.sec >= 0.0) &&
(gps_time.sec < GPS_ASSISTANCE_WEEK_SECONDS);
}
double gps_assistance_time_delta_sec(gpstime_t newer, gpstime_t older)
{
if(!gps_assistance_time_valid(newer) ||
!gps_assistance_time_valid(older))
{
return NAN;
}
return ((double)(newer.week - older.week) *
GPS_ASSISTANCE_WEEK_SECONDS) + newer.sec - older.sec;
}
static bool gps_assistance_ephemeris_structural_valid(
const gps_ephemeris_t* ephemeris,
unsigned int prn)
{
return ephemeris->ephemeris_valid &&
(ephemeris->SVID == prn) &&
(ephemeris->sqrta > 5000.0) &&
(ephemeris->sqrta < 5300.0) &&
(ephemeris->A > 2.5e7) &&
(ephemeris->A < 3.0e7) &&
(ephemeris->ecc >= 0.0) &&
(ephemeris->ecc < 0.1) &&
isfinite(ephemeris->toe.sec) &&
(ephemeris->toe.sec >= 0.0) &&
(ephemeris->toe.sec < GPS_ASSISTANCE_WEEK_SECONDS);
}
static bool gps_assistance_ephemeris_age(
const gps_ephemeris_t* ephemeris,
gpstime_t current_time,
double* age_sec_out)
{
int toe_week = gps_time_extend_lnav_week(
true, current_time.week, ephemeris->WN);
gpstime_t toe = {.week = toe_week, .sec = ephemeris->toe.sec};
double age_sec = gps_assistance_time_delta_sec(current_time, toe);
/* A toe modestly ahead of reception is normal around an issue boundary;
* account for it symmetrically without accepting another GPS week. */
age_sec = fabs(age_sec);
if(!isfinite(age_sec) ||
(age_sec > ((double)GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS /
(double)GPS_SYSTICK_HZ)))
{
return false;
}
*age_sec_out = age_sec;
return true;
}
void gps_assistance_payload_build(
gps_assistance_payload_t* payload,
const gps_ephemeris_cache_t* ephemeris_cache,
const gps_almanac_cache_t* almanac_cache,
const ionoutc_t* ionoutc,
const gps_assistance_position_t* position,
gpstime_t saved_time)
{
if(payload == NULL)
{
return;
}
memset(payload, 0, sizeof(*payload));
payload->magic = GPS_ASSISTANCE_PAYLOAD_MAGIC;
payload->version = GPS_ASSISTANCE_PAYLOAD_VERSION;
payload->payload_size = (uint16_t)sizeof(*payload);
payload->saved_time = saved_time;
if(ephemeris_cache != NULL)
{
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
const gps_ephemeris_cache_entry_t* entry =
&ephemeris_cache->entry[prn];
if(entry->valid && gps_assistance_ephemeris_structural_valid(
&entry->ephemeris, prn))
{
payload->ephemeris[prn] = entry->ephemeris;
payload->ephemeris[prn].abs_sample = 0.0;
payload->ephemeris_mask |= UINT32_C(1) << prn;
}
}
}
if(almanac_cache != NULL)
{
payload->almanac = *almanac_cache;
payload->almanac.max_cache_age_ticks =
GPS_ALMANAC_CACHE_MAX_AGE_TICKS;
payload->almanac.reference_tick = 0u;
payload->almanac.sf4_health_tick = 0u;
for(unsigned int prn = 0u ; prn < GPS_ALMANAC_PRN_COUNT ; prn++)
{
payload->almanac.entry[prn].received_tick = 0u;
}
}
if((ionoutc != NULL) && ionoutc->enable && ionoutc->vflg)
{
payload->ionoutc = *ionoutc;
payload->flags |= GPS_ASSISTANCE_FLAG_IONOUTC;
}
if((position != NULL) && position->valid &&
gps_assistance_time_valid(position->gps_time) &&
isfinite(position->ecef_m[0]) &&
isfinite(position->ecef_m[1]) &&
isfinite(position->ecef_m[2]) &&
isfinite(position->latitude_deg) &&
isfinite(position->longitude_deg) &&
isfinite(position->height_m))
{
payload->position = *position;
payload->flags |= GPS_ASSISTANCE_FLAG_POSITION;
}
}
bool gps_assistance_payload_valid(const gps_assistance_payload_t* payload)
{
if((payload == NULL) ||
(payload->magic != GPS_ASSISTANCE_PAYLOAD_MAGIC) ||
(payload->version != GPS_ASSISTANCE_PAYLOAD_VERSION) ||
(payload->payload_size != sizeof(*payload)) ||
!gps_assistance_time_valid(payload->saved_time))
{
return false;
}
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
if(((payload->ephemeris_mask & (UINT32_C(1) << prn)) != 0u) &&
!gps_assistance_ephemeris_structural_valid(
&payload->ephemeris[prn], prn))
{
return false;
}
}
return true;
}
bool gps_assistance_ephemeris_issue_matches(
const gps_ephemeris_t* ephemeris,
unsigned char subframe_id,
const unsigned int words[10])
{
if((ephemeris == NULL) || (words == NULL))
{
return false;
}
if(subframe_id == 1u)
{
unsigned int iodc = ((((words[2] >> 6) & 0x03u) << 8) |
((words[7] >> 22) & 0xffu));
return ephemeris->iodc == iodc;
}
if(subframe_id == 2u)
{
return ephemeris->iode ==
(unsigned char)((words[2] >> 22) & 0xffu);
}
if(subframe_id == 3u)
{
return ephemeris->iode ==
(unsigned char)((words[9] >> 22) & 0xffu);
}
return (subframe_id == 4u) || (subframe_id == 5u);
}
static int gps_assistance_resolve_almanac_week(
unsigned int week_mod256,
int current_week)
{
int candidate = (current_week & ~0xff) | (int)(week_mod256 & 0xffu);
if((candidate - current_week) > 128)
{
candidate -= 256;
}
else if((current_week - candidate) > 128)
{
candidate += 256;
}
return candidate;
}
bool gps_assistance_restore(
const gps_assistance_payload_t* payload,
gpstime_t current_time,
uint64_t now,
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
gps_assistance_restore_result_t* result)
{
if(!gps_assistance_payload_valid(payload) ||
!gps_assistance_time_valid(current_time) ||
(ephemeris_cache == NULL) || (almanac_cache == NULL) ||
(result == NULL))
{
return false;
}
memset(result, 0, sizeof(*result));
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
if((payload->ephemeris_mask & (UINT32_C(1) << prn)) == 0u)
{
continue;
}
double age_sec = 0.0;
if(gps_assistance_ephemeris_age(
&payload->ephemeris[prn], current_time, &age_sec) &&
gps_ephemeris_cache_import(
ephemeris_cache, prn, &payload->ephemeris[prn], now,
(uint64_t)llround(age_sec * (double)GPS_SYSTICK_HZ)))
{
result->ephemeris_mask |= UINT32_C(1) << prn;
}
}
if(payload->almanac.reference_valid)
{
int reference_week = gps_assistance_resolve_almanac_week(
payload->almanac.reference_week_mod256,
current_time.week);
gpstime_t reference_time = {
.week = reference_week,
.sec = payload->almanac.reference_toa_sec
};
double almanac_age_sec = fabs(gps_assistance_time_delta_sec(
current_time, reference_time));
if(isfinite(almanac_age_sec) &&
(almanac_age_sec <= GPS_ASSISTANCE_ALMANAC_MAX_AGE_SEC))
{
*almanac_cache = payload->almanac;
almanac_cache->max_cache_age_ticks =
GPS_ALMANAC_CACHE_MAX_AGE_TICKS;
uint64_t age_ticks = (uint64_t)llround(
almanac_age_sec * (double)GPS_SYSTICK_HZ);
uint64_t base_tick = age_ticks <= now ? now - age_ticks : 0u;
almanac_cache->reference_tick = base_tick;
almanac_cache->sf4_health_tick = base_tick;
for(unsigned int prn = 0u ;
prn < GPS_ALMANAC_PRN_COUNT ; prn++)
{
almanac_cache->entry[prn].received_tick = base_tick;
}
/* Uptime can be shorter than GPS data age. The almanac's separate
* GPS-time freshness check preserves the true remaining lifetime. */
result->almanac_restored = true;
}
}
double snapshot_age_sec = gps_assistance_time_delta_sec(
current_time, payload->saved_time);
if(isfinite(snapshot_age_sec) && (snapshot_age_sec >= -60.0) &&
(snapshot_age_sec <= GPS_ASSISTANCE_ALMANAC_MAX_AGE_SEC) &&
((payload->flags & GPS_ASSISTANCE_FLAG_IONOUTC) != 0u) &&
payload->ionoutc.enable && payload->ionoutc.vflg)
{
result->ionoutc = payload->ionoutc;
result->ionoutc_restored = true;
}
if(((payload->flags & GPS_ASSISTANCE_FLAG_POSITION) != 0u) &&
payload->position.valid)
{
double position_age_sec = gps_assistance_time_delta_sec(
current_time, payload->position.gps_time);
if(isfinite(position_age_sec) && (position_age_sec >= -60.0) &&
(position_age_sec <= GPS_ASSISTANCE_POSITION_MAX_AGE_SEC))
{
result->position = payload->position;
result->position_restored = true;
}
}
return true;
}
@@ -0,0 +1,429 @@
#include "gps/gps_assistance_store.h"
#include <bsp/spi_flash.h>
#include <math.h>
#include <stddef.h>
#include <string.h>
#include "gps/gps_flash_journal.h"
#include "gps/gps_macros.h"
#include "gps/gps_time_anchor.h"
#define GPS_ASSISTANCE_WRITE_QUIET_TICKS (GPS_SYSTICK_HZ * 60u)
#define GPS_ASSISTANCE_WRITE_MAX_DELAY_TICKS (GPS_SYSTICK_HZ * 15u * 60u)
#define GPS_ASSISTANCE_POSITION_MOVE_WRITE_M 1000.0
typedef struct {
gps_flash_journal_t journal;
gps_assistance_payload_t loaded_payload;
gps_assistance_position_t latest_position;
gps_assistance_position_t persisted_position;
gpstime_t live_time;
uint32_t restored_ephemeris_mask;
uint32_t approved_ephemeris_mask;
uint64_t dirty_first_tick;
uint64_t dirty_last_tick;
bool flash_ready;
bool provisional_valid;
bool live_time_valid;
bool dirty;
} gps_assistance_store_state_t;
static gps_assistance_store_state_t gps_assistance_state;
static gps_assistance_payload_t gps_assistance_write_payload;
_Static_assert(sizeof(gps_assistance_payload_t) <= UINT16_MAX,
"assistance payload size field is too small");
_Static_assert(sizeof(gps_assistance_payload_t) <
(GPS_ASSISTANCE_FLASH_REGION_SIZE - GPS_FLASH_JOURNAL_PAGE_SIZE),
"assistance payload does not fit a flash journal region");
static bool gps_assistance_flash_read(
void* context, uint32_t address, void* destination, uint32_t length)
{
(void)context;
bsp_spi_flash_read(address, destination, length);
return true;
}
static bool gps_assistance_flash_program(
void* context, uint32_t address, const void* source, uint32_t length)
{
(void)context;
return bsp_spi_flash_program_page(address, source, length);
}
static bool gps_assistance_flash_erase(
void* context, uint32_t region_address)
{
(void)context;
/* 0xDC erases the containing uniform sector. The two logical 64 KiB
* journals start on separate 256 KiB boundaries, so this is safe for both
* the FL-S 64 KiB and 256 KiB sector configurations. */
return bsp_spi_flash_erase_sector(region_address);
}
static void gps_assistance_mark_dirty(uint64_t now)
{
if(!gps_assistance_state.dirty)
{
gps_assistance_state.dirty_first_tick = now;
}
gps_assistance_state.dirty_last_tick = now;
gps_assistance_state.dirty = true;
}
void gps_assistance_store_init(
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
uint64_t now)
{
(void)ephemeris_cache;
(void)almanac_cache;
(void)now;
memset(&gps_assistance_state, 0, sizeof(gps_assistance_state));
bsp_spi_flash_init();
uint32_t flash_id = bsp_spi_flash_read_id();
(void)flash_id;
const gps_flash_journal_io_t io = {
.context = NULL,
.read = gps_assistance_flash_read,
.program = gps_assistance_flash_program,
.erase_region = gps_assistance_flash_erase
};
gps_assistance_state.flash_ready = gps_flash_journal_init(
&gps_assistance_state.journal, &io,
GPS_ASSISTANCE_FLASH_REGION0,
GPS_ASSISTANCE_FLASH_REGION1,
GPS_ASSISTANCE_FLASH_REGION_SIZE,
(uint32_t)sizeof(gps_assistance_payload_t));
uint32_t payload_size = 0u;
uint64_t sequence = 0u;
if(gps_assistance_state.flash_ready &&
gps_flash_journal_load_latest(
&gps_assistance_state.journal,
&gps_assistance_state.loaded_payload,
sizeof(gps_assistance_state.loaded_payload),
&payload_size, &sequence) &&
(payload_size == sizeof(gps_assistance_state.loaded_payload)) &&
gps_assistance_payload_valid(
&gps_assistance_state.loaded_payload))
{
gps_assistance_state.provisional_valid = true;
if((gps_assistance_state.loaded_payload.flags &
GPS_ASSISTANCE_FLAG_POSITION) != 0u)
{
gps_assistance_state.persisted_position =
gps_assistance_state.loaded_payload.position;
}
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"flash assistance loaded provisional: sequence=%lu eph_mask=0x%08lx almanac_ref=%u ionoutc=%u position=%u id=0x%06lx\r\n",
(unsigned long)sequence,
(unsigned long)gps_assistance_state.loaded_payload.ephemeris_mask,
gps_assistance_state.loaded_payload.almanac.reference_valid ? 1u : 0u,
(gps_assistance_state.loaded_payload.flags & GPS_ASSISTANCE_FLAG_IONOUTC) != 0u,
(gps_assistance_state.loaded_payload.flags & GPS_ASSISTANCE_FLAG_POSITION) != 0u,
(unsigned long)flash_id);
}
else
{
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"flash assistance empty/invalid: ready=%u id=0x%06lx policy=live_lnav\r\n",
gps_assistance_state.flash_ready ? 1u : 0u,
(unsigned long)flash_id);
}
}
static bool gps_assistance_resolve_live_time(
unsigned char subframe_id,
const unsigned int words[10],
unsigned int how_tow_sec,
gpstime_t* live_time_out)
{
if((words == NULL) || (live_time_out == NULL) ||
(how_tow_sec >= (unsigned int)GPS_TIME_WEEK_SECONDS))
{
return false;
}
bool have_reference = gps_assistance_state.live_time_valid ||
gps_assistance_state.provisional_valid;
gpstime_t reference = gps_assistance_state.live_time_valid ?
gps_assistance_state.live_time :
gps_assistance_state.loaded_payload.saved_time;
int week;
if(subframe_id == 1u)
{
unsigned int week_mod1024 = (words[2] >> 20) & 0x3ffu;
week = have_reference ?
gps_time_extend_lnav_week(
true, reference.week, week_mod1024) :
(GPS_LNAV_WEEK_ERA_BASE + (int)week_mod1024);
}
else if(have_reference)
{
week = gps_time_resolve_how_week(
true, reference.week, reference.sec,
reference.week, (double)how_tow_sec);
}
else
{
return false;
}
live_time_out->week = week;
live_time_out->sec = (double)how_tow_sec;
return gps_assistance_time_valid(*live_time_out);
}
bool gps_assistance_store_observe_subframe(
unsigned int prn,
unsigned char subframe_id,
const unsigned int words[10],
unsigned int how_tow_sec,
uint64_t now,
gps_ephemeris_cache_t* ephemeris_cache,
gps_almanac_cache_t* almanac_cache,
gps_ephemeris_t* restored_ephemeris_out,
gps_assistance_position_t* restored_position_out,
gpstime_t* current_time_out)
{
if((prn >= GPS_PRN_COUNT) || (subframe_id < 1u) ||
(subframe_id > 5u) || (words == NULL) ||
(ephemeris_cache == NULL) || (almanac_cache == NULL))
{
return false;
}
if(restored_position_out != NULL)
{
memset(restored_position_out, 0,
sizeof(*restored_position_out));
}
if(current_time_out != NULL)
{
memset(current_time_out, 0, sizeof(*current_time_out));
}
gpstime_t live_time;
if(gps_assistance_resolve_live_time(
subframe_id, words, how_tow_sec, &live_time))
{
gps_assistance_state.live_time = live_time;
gps_assistance_state.live_time_valid = true;
}
else if(!gps_assistance_state.live_time_valid)
{
return false;
}
if(current_time_out != NULL)
{
*current_time_out = gps_assistance_state.live_time;
}
if(gps_assistance_state.provisional_valid)
{
gps_assistance_restore_result_t restored;
if(gps_assistance_restore(
&gps_assistance_state.loaded_payload,
gps_assistance_state.live_time, now,
ephemeris_cache, almanac_cache, &restored))
{
gps_assistance_state.restored_ephemeris_mask =
restored.ephemeris_mask;
if(restored.ionoutc_restored)
{
gps_set_ionoutc(&restored.ionoutc);
}
if(restored.position_restored)
{
gps_assistance_state.latest_position = restored.position;
gps_wls_seed_position(restored.position.ecef_m);
if(restored_position_out != NULL)
{
*restored_position_out = restored.position;
}
}
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"flash assistance validated: week=%d tow=%.0f eph_mask=0x%08lx almanac=%u ionoutc=%u position=%u\r\n",
gps_assistance_state.live_time.week,
gps_assistance_state.live_time.sec,
(unsigned long)restored.ephemeris_mask,
restored.almanac_restored ? 1u : 0u,
restored.ionoutc_restored ? 1u : 0u,
restored.position_restored ? 1u : 0u);
}
gps_assistance_state.provisional_valid = false;
}
uint32_t bit = UINT32_C(1) << prn;
if((gps_assistance_state.restored_ephemeris_mask & bit) != 0u)
{
gps_ephemeris_t restored_ephemeris;
if(!gps_ephemeris_cache_restore(
ephemeris_cache, prn, now, &restored_ephemeris))
{
gps_assistance_state.restored_ephemeris_mask &= ~bit;
gps_assistance_state.approved_ephemeris_mask &= ~bit;
}
else if(!gps_assistance_ephemeris_issue_matches(
&restored_ephemeris, subframe_id, words))
{
gps_ephemeris_cache_invalidate(ephemeris_cache, prn);
gps_assistance_state.restored_ephemeris_mask &= ~bit;
gps_assistance_state.approved_ephemeris_mask &= ~bit;
gps_assistance_mark_dirty(now);
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"restored ephemeris rejected by live issue: prn=%u sf=%u cached_iodc=%u cached_iode=%u\r\n",
prn, subframe_id, restored_ephemeris.iodc,
restored_ephemeris.iode);
}
else
{
gps_assistance_state.approved_ephemeris_mask |= bit;
if(restored_ephemeris_out != NULL)
{
*restored_ephemeris_out = restored_ephemeris;
}
return true;
}
}
return false;
}
void gps_assistance_store_note_ephemeris_change(
unsigned int prn,
uint64_t now)
{
if(prn >= GPS_PRN_COUNT)
{
return;
}
uint32_t bit = UINT32_C(1) << prn;
gps_assistance_state.restored_ephemeris_mask &= ~bit;
gps_assistance_state.approved_ephemeris_mask &= ~bit;
gps_assistance_mark_dirty(now);
}
void gps_assistance_store_note_almanac_change(uint64_t now)
{
gps_assistance_mark_dirty(now);
}
void gps_assistance_store_note_ionoutc_change(uint64_t now)
{
gps_assistance_mark_dirty(now);
}
static double gps_assistance_position_distance(
const gps_assistance_position_t* first,
const gps_assistance_position_t* second)
{
double dx = first->ecef_m[0] - second->ecef_m[0];
double dy = first->ecef_m[1] - second->ecef_m[1];
double dz = first->ecef_m[2] - second->ecef_m[2];
return sqrt(dx * dx + dy * dy + dz * dz);
}
void gps_assistance_store_note_solution(
const gps_nav_solution_t* solution,
gpstime_t gps_time,
uint64_t now)
{
if((solution == NULL) || !solution->solution_valid ||
!gps_assistance_time_valid(gps_time))
{
return;
}
gps_assistance_position_t position = {
.ecef_m = {
solution->ecef_m[0], solution->ecef_m[1],
solution->ecef_m[2]
},
.latitude_deg = solution->latitude_deg,
.longitude_deg = solution->longitude_deg,
.height_m = solution->height_m,
.gps_time = gps_time,
.valid = true
};
if(!isfinite(position.ecef_m[0]) ||
!isfinite(position.ecef_m[1]) ||
!isfinite(position.ecef_m[2]))
{
return;
}
gps_assistance_state.latest_position = position;
if(!gps_assistance_state.persisted_position.valid ||
(gps_assistance_position_distance(
&position, &gps_assistance_state.persisted_position) >=
GPS_ASSISTANCE_POSITION_MOVE_WRITE_M))
{
gps_assistance_mark_dirty(now);
}
}
void gps_assistance_store_service(
gps_ephemeris_cache_t* ephemeris_cache,
const gps_almanac_cache_t* almanac_cache,
uint64_t now)
{
if(!gps_assistance_state.flash_ready ||
!gps_assistance_state.dirty ||
!gps_assistance_state.live_time_valid ||
(ephemeris_cache == NULL) || (almanac_cache == NULL))
{
return;
}
bool quiet = (now - gps_assistance_state.dirty_last_tick) >=
GPS_ASSISTANCE_WRITE_QUIET_TICKS;
bool maximum_delay = (now - gps_assistance_state.dirty_first_tick) >=
GPS_ASSISTANCE_WRITE_MAX_DELAY_TICKS;
if(!quiet && !maximum_delay)
{
return;
}
/* Expire entries before serialization. This does not refresh any issue. */
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
gps_ephemeris_t ignored;
(void)gps_ephemeris_cache_restore(
ephemeris_cache, prn, now, &ignored);
}
ionoutc_t ionoutc;
const ionoutc_t* ionoutc_ptr = gps_get_ionoutc(&ionoutc) ?
&ionoutc : NULL;
const gps_assistance_position_t* position_ptr =
gps_assistance_state.latest_position.valid ?
&gps_assistance_state.latest_position : NULL;
gps_assistance_payload_build(
&gps_assistance_write_payload,
ephemeris_cache, almanac_cache,
ionoutc_ptr, position_ptr,
gps_assistance_state.live_time);
gps_flash_journal_write_result_t write_result =
gps_flash_journal_write(
&gps_assistance_state.journal,
&gps_assistance_write_payload,
sizeof(gps_assistance_write_payload));
if(write_result != GPS_FLASH_JOURNAL_WRITE_FAILED)
{
gps_assistance_state.dirty = false;
if(position_ptr != NULL)
{
gps_assistance_state.persisted_position = *position_ptr;
}
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"flash assistance %s: sequence=%lu eph_mask=0x%08lx almanac_ref=%u ionoutc=%u position=%u\r\n",
write_result == GPS_FLASH_JOURNAL_WRITE_COMMITTED ?
"committed" : "unchanged",
(unsigned long)gps_assistance_state.journal.latest_sequence,
(unsigned long)gps_assistance_write_payload.ephemeris_mask,
gps_assistance_write_payload.almanac.reference_valid ? 1u : 0u,
(gps_assistance_write_payload.flags & GPS_ASSISTANCE_FLAG_IONOUTC) != 0u,
(gps_assistance_write_payload.flags & GPS_ASSISTANCE_FLAG_POSITION) != 0u);
}
else
{
gps_assistance_state.dirty_last_tick = now;
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"flash assistance write failed; previous committed record retained\r\n");
}
}
@@ -1,8 +1,21 @@
#include "gps/gps_ephemeris_cache.h"
#include <math.h>
#include <stddef.h>
#include <string.h>
bool gps_ephemeris_cache_same_issue(
const gps_ephemeris_t* first,
const gps_ephemeris_t* second)
{
return (first->SVID == second->SVID) &&
(first->iode == second->iode) &&
(first->iodc == second->iodc) &&
(first->WN == second->WN) &&
(first->toe.sec == second->toe.sec) &&
(first->toc.sec == second->toc.sec);
}
void gps_ephemeris_cache_init(
gps_ephemeris_cache_t* cache,
uint64_t max_age_ticks)
@@ -28,9 +41,43 @@ bool gps_ephemeris_cache_store(
{
return false;
}
cache->entry[prn].ephemeris = *ephemeris;
cache->entry[prn].committed_tick = now;
cache->entry[prn].valid = true;
gps_ephemeris_cache_entry_t* entry = &cache->entry[prn];
bool same_issue = entry->valid &&
gps_ephemeris_cache_same_issue(&entry->ephemeris, ephemeris);
entry->ephemeris = *ephemeris;
/* Repeated reception of the same IOD is useful confirmation but must not
* make one broadcast issue immortal in RAM. Only a genuinely new issue
* starts a fresh cache-validity interval. */
if(!same_issue)
{
entry->committed_tick = now;
entry->age_bias_ticks = 0u;
}
entry->valid = true;
return true;
}
bool gps_ephemeris_cache_import(
gps_ephemeris_cache_t* cache,
unsigned int prn,
const gps_ephemeris_t* ephemeris,
uint64_t now,
uint64_t age_ticks)
{
if((cache == NULL) || (ephemeris == NULL) ||
(prn >= GPS_PRN_COUNT) || !ephemeris->ephemeris_valid ||
(ephemeris->SVID != prn) || (age_ticks > cache->max_age_ticks))
{
return false;
}
gps_ephemeris_cache_entry_t* entry = &cache->entry[prn];
entry->ephemeris = *ephemeris;
/* RF/sample phase is reset at power-on and is never imported. */
entry->ephemeris.abs_sample = 0.0;
entry->committed_tick = now;
entry->age_bias_ticks = age_ticks;
memset(&entry->time_anchor, 0, sizeof(entry->time_anchor));
entry->valid = true;
return true;
}
@@ -46,8 +93,11 @@ bool gps_ephemeris_cache_restore(
return false;
}
gps_ephemeris_cache_entry_t* entry = &cache->entry[prn];
if(!entry->valid ||
((now - entry->committed_tick) > cache->max_age_ticks))
if(!entry->valid || (now < entry->committed_tick) ||
((now - entry->committed_tick) >
(cache->max_age_ticks -
(entry->age_bias_ticks <= cache->max_age_ticks ?
entry->age_bias_ticks : cache->max_age_ticks))))
{
entry->valid = false;
return false;
@@ -56,6 +106,55 @@ bool gps_ephemeris_cache_restore(
return true;
}
bool gps_ephemeris_cache_store_time_anchor(
gps_ephemeris_cache_t* cache,
unsigned int prn,
const gps_cached_time_anchor_t* anchor,
uint64_t now)
{
if((cache == NULL) || (anchor == NULL) ||
(prn >= GPS_PRN_COUNT) || !cache->entry[prn].valid ||
!anchor->valid || !anchor->week_valid ||
(anchor->tow.week < 0) || !isfinite(anchor->tow.sec) ||
(anchor->tow.sec < 0.0) ||
(anchor->tow.sec >= 604800.0) ||
!isfinite(anchor->corrected_sample))
{
return false;
}
gps_cached_time_anchor_t stored = *anchor;
stored.committed_tick = now;
cache->entry[prn].time_anchor = stored;
return true;
}
bool gps_ephemeris_cache_restore_time_anchor(
gps_ephemeris_cache_t* cache,
unsigned int prn,
uint64_t now,
uint64_t max_age_ticks,
gps_cached_time_anchor_t* anchor_out)
{
if((cache == NULL) || (anchor_out == NULL) ||
(prn >= GPS_PRN_COUNT) || (max_age_ticks == 0u))
{
return false;
}
gps_ephemeris_cache_entry_t* entry = &cache->entry[prn];
if(!entry->valid || !entry->time_anchor.valid ||
!entry->time_anchor.week_valid ||
((now - entry->time_anchor.committed_tick) > max_age_ticks))
{
entry->time_anchor.valid = false;
return false;
}
*anchor_out = entry->time_anchor;
return true;
}
void gps_ephemeris_cache_invalidate(
gps_ephemeris_cache_t* cache,
unsigned int prn)
@@ -0,0 +1,412 @@
#include "gps/gps_flash_journal.h"
#include <stddef.h>
#include <string.h>
#define GPS_FLASH_JOURNAL_MAGIC UINT32_C(0x4750534a)
#define GPS_FLASH_JOURNAL_FORMAT_VERSION 1u
#define GPS_FLASH_JOURNAL_COMMIT UINT32_C(0x434f4d54)
#define GPS_FLASH_JOURNAL_ERASED UINT32_MAX
typedef struct {
uint32_t magic;
uint16_t format_version;
uint16_t header_size;
uint64_t sequence;
uint32_t payload_size;
uint32_t payload_crc;
uint32_t header_crc;
uint32_t commit;
} gps_flash_journal_record_header_t;
typedef struct {
bool valid;
uint64_t sequence;
uint32_t payload_crc;
uint32_t payload_size;
uint32_t record_offset;
uint32_t next_offset;
} gps_flash_journal_region_scan_t;
_Static_assert(sizeof(gps_flash_journal_record_header_t) == 32u,
"flash journal header must remain 32 bytes");
static uint32_t gps_flash_journal_align_page(uint32_t value)
{
return (value + GPS_FLASH_JOURNAL_PAGE_SIZE - 1u) &
~(GPS_FLASH_JOURNAL_PAGE_SIZE - 1u);
}
uint32_t gps_flash_journal_crc32(const void* data, uint32_t length)
{
const unsigned char* bytes = (const unsigned char*)data;
uint32_t crc = UINT32_MAX;
for(uint32_t i = 0u ; i < length ; i++)
{
crc ^= bytes[i];
for(unsigned int bit = 0u ; bit < 8u ; bit++)
{
uint32_t mask = (uint32_t)-(int32_t)(crc & 1u);
crc = (crc >> 1) ^ (UINT32_C(0xedb88320) & mask);
}
}
return ~crc;
}
static uint32_t gps_flash_journal_header_crc(
const gps_flash_journal_record_header_t* header)
{
/* header_crc and commit are deliberately excluded. Commit is programmed
* last, after the payload is durable. */
return gps_flash_journal_crc32(
header, (uint32_t)offsetof(
gps_flash_journal_record_header_t, header_crc));
}
static bool gps_flash_journal_crc_flash(
const gps_flash_journal_t* journal,
uint32_t address,
uint32_t length,
uint32_t* crc_out)
{
unsigned char buffer[GPS_FLASH_JOURNAL_PAGE_SIZE];
uint32_t crc = UINT32_MAX;
while(length != 0u)
{
uint32_t chunk = length > sizeof(buffer) ?
(uint32_t)sizeof(buffer) : length;
if(!journal->io.read(
journal->io.context, address, buffer, chunk))
{
return false;
}
for(uint32_t i = 0u ; i < chunk ; i++)
{
crc ^= buffer[i];
for(unsigned int bit = 0u ; bit < 8u ; bit++)
{
uint32_t mask = (uint32_t)-(int32_t)(crc & 1u);
crc = (crc >> 1) ^
(UINT32_C(0xedb88320) & mask);
}
}
address += chunk;
length -= chunk;
}
*crc_out = ~crc;
return true;
}
static bool gps_flash_journal_header_valid(
const gps_flash_journal_t* journal,
const gps_flash_journal_record_header_t* header)
{
return (header->magic == GPS_FLASH_JOURNAL_MAGIC) &&
(header->format_version == GPS_FLASH_JOURNAL_FORMAT_VERSION) &&
(header->header_size == sizeof(*header)) &&
(header->payload_size != 0u) &&
(header->payload_size <= journal->maximum_payload_size) &&
(header->header_crc == gps_flash_journal_header_crc(header));
}
static bool gps_flash_journal_sequence_newer(uint64_t first, uint64_t second)
{
return (int64_t)(first - second) > 0;
}
static bool gps_flash_journal_scan_region(
const gps_flash_journal_t* journal,
unsigned int region,
gps_flash_journal_region_scan_t* scan)
{
memset(scan, 0, sizeof(*scan));
uint32_t offset = 0u;
while((offset + GPS_FLASH_JOURNAL_PAGE_SIZE) <= journal->region_size)
{
gps_flash_journal_record_header_t header;
if(!journal->io.read(
journal->io.context,
journal->region_address[region] + offset,
&header, sizeof(header)))
{
return false;
}
if(header.magic == GPS_FLASH_JOURNAL_ERASED)
{
break;
}
if(!gps_flash_journal_header_valid(journal, &header))
{
/* A power failure can leave only the final sequential slot torn.
* Its intended length is unknowable, so retire this bank. */
offset = journal->region_size;
break;
}
uint32_t record_size = gps_flash_journal_align_page(
GPS_FLASH_JOURNAL_PAGE_SIZE + header.payload_size);
if((record_size > journal->region_size) ||
(offset > (journal->region_size - record_size)))
{
offset = journal->region_size;
break;
}
if(header.commit != GPS_FLASH_JOURNAL_COMMIT)
{
offset += record_size;
break;
}
uint32_t payload_crc = 0u;
if(!gps_flash_journal_crc_flash(
journal,
journal->region_address[region] + offset +
GPS_FLASH_JOURNAL_PAGE_SIZE,
header.payload_size, &payload_crc))
{
return false;
}
if(payload_crc != header.payload_crc)
{
offset += record_size;
break;
}
if(!scan->valid || gps_flash_journal_sequence_newer(
header.sequence, scan->sequence))
{
scan->valid = true;
scan->sequence = header.sequence;
scan->payload_crc = header.payload_crc;
scan->payload_size = header.payload_size;
scan->record_offset = offset;
}
offset += record_size;
}
scan->next_offset = offset;
return true;
}
bool gps_flash_journal_init(
gps_flash_journal_t* journal,
const gps_flash_journal_io_t* io,
uint32_t first_region_address,
uint32_t second_region_address,
uint32_t region_size,
uint32_t maximum_payload_size)
{
if((journal == NULL) || (io == NULL) || (io->read == NULL) ||
(io->program == NULL) || (io->erase_region == NULL) ||
(region_size < (2u * GPS_FLASH_JOURNAL_PAGE_SIZE)) ||
((region_size % GPS_FLASH_JOURNAL_PAGE_SIZE) != 0u) ||
(maximum_payload_size == 0u) ||
(gps_flash_journal_align_page(
GPS_FLASH_JOURNAL_PAGE_SIZE + maximum_payload_size) > region_size))
{
return false;
}
memset(journal, 0, sizeof(*journal));
journal->io = *io;
journal->region_address[0] = first_region_address;
journal->region_address[1] = second_region_address;
journal->region_size = region_size;
journal->maximum_payload_size = maximum_payload_size;
gps_flash_journal_region_scan_t scan[GPS_FLASH_JOURNAL_REGION_COUNT];
for(unsigned int region = 0u ;
region < GPS_FLASH_JOURNAL_REGION_COUNT ; region++)
{
if(!gps_flash_journal_scan_region(journal, region, &scan[region]))
{
return false;
}
journal->next_offset[region] = scan[region].next_offset;
}
if(scan[0].valid || scan[1].valid)
{
unsigned int latest = !scan[0].valid ? 1u :
(!scan[1].valid ? 0u :
(gps_flash_journal_sequence_newer(
scan[1].sequence, scan[0].sequence) ? 1u : 0u));
journal->active_region = latest;
journal->latest_sequence = scan[latest].sequence;
journal->latest_payload_crc = scan[latest].payload_crc;
journal->latest_payload_size = scan[latest].payload_size;
journal->latest_valid = true;
}
journal->initialized = true;
return true;
}
bool gps_flash_journal_load_latest(
gps_flash_journal_t* journal,
void* payload,
uint32_t payload_capacity,
uint32_t* payload_size_out,
uint64_t* sequence_out)
{
if((journal == NULL) || !journal->initialized ||
!journal->latest_valid || (payload == NULL) ||
(payload_capacity < journal->latest_payload_size))
{
return false;
}
gps_flash_journal_region_scan_t scan;
if(!gps_flash_journal_scan_region(
journal, journal->active_region, &scan) || !scan.valid ||
(scan.sequence != journal->latest_sequence) ||
(scan.payload_size != journal->latest_payload_size))
{
return false;
}
uint32_t address = journal->region_address[journal->active_region] +
scan.record_offset + GPS_FLASH_JOURNAL_PAGE_SIZE;
if(!journal->io.read(
journal->io.context, address, payload, scan.payload_size))
{
return false;
}
if(gps_flash_journal_crc32(payload, scan.payload_size) !=
scan.payload_crc)
{
return false;
}
if(payload_size_out != NULL)
{
*payload_size_out = scan.payload_size;
}
if(sequence_out != NULL)
{
*sequence_out = scan.sequence;
}
return true;
}
static bool gps_flash_journal_program_pages(
const gps_flash_journal_t* journal,
uint32_t address,
const unsigned char* data,
uint32_t length)
{
while(length != 0u)
{
uint32_t page_remaining = GPS_FLASH_JOURNAL_PAGE_SIZE -
(address & (GPS_FLASH_JOURNAL_PAGE_SIZE - 1u));
uint32_t chunk = length < page_remaining ? length : page_remaining;
if(!journal->io.program(
journal->io.context, address, data, chunk))
{
return false;
}
address += chunk;
data += chunk;
length -= chunk;
}
return true;
}
gps_flash_journal_write_result_t gps_flash_journal_write(
gps_flash_journal_t* journal,
const void* payload,
uint32_t payload_size)
{
if((journal == NULL) || !journal->initialized || (payload == NULL) ||
(payload_size == 0u) ||
(payload_size > journal->maximum_payload_size))
{
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
uint32_t payload_crc = gps_flash_journal_crc32(payload, payload_size);
if(journal->latest_valid &&
(journal->latest_payload_size == payload_size) &&
(journal->latest_payload_crc == payload_crc))
{
return GPS_FLASH_JOURNAL_WRITE_UNCHANGED;
}
uint32_t record_size = gps_flash_journal_align_page(
GPS_FLASH_JOURNAL_PAGE_SIZE + payload_size);
unsigned int region = journal->active_region;
if(!journal->latest_valid)
{
region = 0u;
if(!journal->io.erase_region(
journal->io.context, journal->region_address[region]))
{
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
journal->next_offset[region] = 0u;
}
else if((journal->next_offset[region] > journal->region_size) ||
(record_size >
(journal->region_size - journal->next_offset[region])))
{
region ^= 1u;
if(!journal->io.erase_region(
journal->io.context, journal->region_address[region]))
{
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
journal->next_offset[region] = 0u;
}
uint32_t offset = journal->next_offset[region];
uint64_t sequence = journal->latest_valid ?
journal->latest_sequence + 1u : 1u;
gps_flash_journal_record_header_t header = {
.magic = GPS_FLASH_JOURNAL_MAGIC,
.format_version = GPS_FLASH_JOURNAL_FORMAT_VERSION,
.header_size = sizeof(header),
.payload_size = payload_size,
.sequence = sequence,
.payload_crc = payload_crc,
.header_crc = 0u,
.commit = GPS_FLASH_JOURNAL_ERASED
};
header.header_crc = gps_flash_journal_header_crc(&header);
uint32_t base = journal->region_address[region] + offset;
if(!gps_flash_journal_program_pages(
journal, base, (const unsigned char*)&header, sizeof(header)) ||
!gps_flash_journal_program_pages(
journal, base + GPS_FLASH_JOURNAL_PAGE_SIZE,
(const unsigned char*)payload, payload_size))
{
/* Leave the prior bank/record intact. The torn slot is never reused. */
journal->next_offset[region] = offset + record_size;
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
uint32_t commit = GPS_FLASH_JOURNAL_COMMIT;
if(!journal->io.program(
journal->io.context,
base + (uint32_t)offsetof(
gps_flash_journal_record_header_t, commit),
&commit, sizeof(commit)))
{
journal->next_offset[region] = offset + record_size;
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
/* Read back the complete record, including CRC, before publishing it as
* latest in RAM. */
gps_flash_journal_region_scan_t verify;
if(!gps_flash_journal_scan_region(journal, region, &verify) ||
!verify.valid || (verify.sequence != sequence) ||
(verify.payload_crc != payload_crc))
{
journal->next_offset[region] = offset + record_size;
return GPS_FLASH_JOURNAL_WRITE_FAILED;
}
journal->active_region = region;
journal->latest_sequence = sequence;
journal->latest_payload_crc = payload_crc;
journal->latest_payload_size = payload_size;
journal->latest_valid = true;
journal->next_offset[region] = verify.next_offset;
return GPS_FLASH_JOURNAL_WRITE_COMMITTED;
}
@@ -1,11 +1,16 @@
#include "gps/linked_list.h"
#include "gps/gps_acq_manager.h"
#include "gps/gps_almanac.h"
#include "gps/gps_assistance_store.h"
#include "gps/gps_ephemeris_cache.h"
#include "gps/gps_interrupt_handlers.h"
#include "gps/Navigation.h"
#include "gps/NavEquations.h"
#include "gps/gps_nav_epoch.h"
#include "gps/gps_nav_phase.h"
#include "gps/gps_pps.h"
#include "gps/gps_time_anchor.h"
#include "gps/gps_visibility.h"
#include <bsp/irq.h>
#include <bsp/mmio.h>
#include <bsp/plic.h>
@@ -61,10 +66,13 @@ static gps_acq_result_t acq_shadow_result[GPS_ACQ_IP_COUNT];
static gps_tracking_shadow_t trk_shadow[GPS_TRCK_IP_COUNT];
static gps_tracking_measurement_t
gps_latest_tracking_measurement[GPS_TRCK_IP_COUNT];
static volatile uint64_t
gps_latest_tracking_measurement_tick[GPS_TRCK_IP_COUNT];
static volatile unsigned char satellite_tracking_ip[GPS_PRN_COUNT];
static gps_acq_result_t satellite_acq_result[GPS_PRN_COUNT];
static gps_acq_manager_t gps_acq_manager;
static gps_almanac_cache_t gps_almanac_cache;
static gps_ephemeris_cache_t gps_ephemeris_cache;
static gps_isr_event_t gps_event_queue[GPS_EVENT_QUEUE_SIZE];
@@ -81,6 +89,7 @@ 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 uint64_t gps_last_acq_status_log_tick = 0u;
static uint64_t gps_last_visibility_update_tick = 0u;
static unsigned int gps_external_irq_id = 0u;
static gps_external_irq_handler_t gps_external_irq_handler = NULL;
@@ -194,6 +203,8 @@ static void gps_update_tracking_measurement_isr(
}
measurement.local_timestamp_ticks = local_timestamp_ticks;
gps_latest_tracking_measurement[tracking_index] = measurement;
gps_latest_tracking_measurement_tick[tracking_index] =
gps_get_systick();
}
static bool gps_copy_latest_tracking_measurement(
@@ -223,15 +234,35 @@ static bool gps_copy_latest_tracking_measurement(
static uint32_t gps_nav_ready_channel_mask(void)
{
uint32_t ready_mask = 0u;
uint64_t now = gps_get_systick();
for(unsigned int i = 0u ; i < GPS_TRCK_IP_COUNT ; i++)
{
uint32_t status = trck_ips[i]->STATUS_REG;
uint32_t control = trck_ips[i]->CONTROL_REG;
volatile gps_ephem_ll_t* node =
get_element_by_ip_number(&tracking_node, i);
bool busy = (status & (1u << GPS_TRCK_BUSY_POS)) != 0u;
bool locked = (status & (1u << GPS_TRCK_LOCK_POS)) != 0u;
bool nav_irq_enabled =
(control & (1u << GPS_TRCK_NAV_SOLVE_INTR_EN_POS)) != 0u;
if(tracking_allocated[i] && busy && locked && nav_irq_enabled)
bool usable_ephemeris =
(node != NULL) &&
(node->data_p != NULL) &&
node->data_p->ephemeris_valid &&
(node->data_p->SVID == trck_ips[i]->PRN_NUMBER) &&
(node->data_p->update == 0u) &&
(node->data_p->svhlth == 0u) &&
(node->data_p->sva < 15u) &&
(node->data_p->A > 0.0) &&
(node->data_p->ecc >= 0.0) &&
(node->data_p->ecc < 1.0);
bool fresh_measurement =
(node != NULL) &&
gps_measurement_freshness_usable(
&node->gps_measurement_freshness,
now, GPS_TRK_MEASUREMENT_STALE_TICKS);
if(tracking_allocated[i] && busy && locked &&
gps_nav_phase_control_armed(control) &&
(node != NULL) && node->gps_nav_phase_seeded &&
usable_ephemeris && fresh_measurement)
{
ready_mask |= 1u << i;
}
@@ -427,6 +458,7 @@ static void gps_clear_tracking_slot(unsigned char trck_ip_no)
memset((void*)&gps_subframes[trck_ip_no][0], 0, sizeof(gps_subframes[trck_ip_no]));
memset(&gps_latest_tracking_measurement[trck_ip_no], 0,
sizeof(gps_latest_tracking_measurement[trck_ip_no]));
gps_latest_tracking_measurement_tick[trck_ip_no] = 0u;
}
static void gps_set_tracking_nav_bits_mode(unsigned char trck_ip_no)
@@ -452,6 +484,10 @@ static void gps_runtime_state_init(void)
.hot_retry_delay_ticks =
GPS_ACQ_HOT_RETRY_DELAY_TICKS,
.hot_window_ticks = GPS_ACQ_REACQ_PRIORITY_TICKS,
.unknown_probe_interval_ticks =
GPS_ACQ_UNKNOWN_PROBE_TICKS,
.visibility_probe_interval_ticks =
GPS_ACQ_VISIBILITY_PROBE_TICKS,
.max_hot_streak = GPS_ACQ_MAX_HOT_STREAK
};
@@ -470,12 +506,19 @@ static void gps_runtime_state_init(void)
gps_runtime_diagnostics.last_connected_prn = GPS_PRN_INVALID;
gps_runtime_diagnostics.last_lost_prn = GPS_PRN_INVALID;
gps_last_acq_status_log_tick = 0u;
gps_last_visibility_update_tick = 0u;
gps_external_irq_id = 0u;
gps_external_irq_handler = NULL;
gps_acq_manager_init(&gps_acq_manager, &acq_config);
gps_almanac_cache_init(
&gps_almanac_cache,
GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_ephemeris_cache_init(
&gps_ephemeris_cache,
GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_assistance_store_init(
&gps_ephemeris_cache, &gps_almanac_cache,
gps_get_systick());
for(unsigned int i = 0 ; i < GPS_ACQ_IP_COUNT ; i++)
{
@@ -524,6 +567,163 @@ void gps_acq_configure_prns(
gps_acq_schedule_pending = true;
}
void gps_acq_update_visibility_hint(
const double receiver_ecef_m[3],
double receiver_latitude_deg,
double receiver_longitude_deg,
gpstime_t gps_time)
{
uint64_t now = gps_get_systick();
if((receiver_ecef_m == NULL) ||
((gps_last_visibility_update_tick != 0u) &&
((now - gps_last_visibility_update_tick) <
GPS_ACQ_VISIBILITY_UPDATE_TICKS)) ||
!isfinite(receiver_latitude_deg) ||
!isfinite(receiver_longitude_deg) ||
(receiver_latitude_deg < -90.0) ||
(receiver_latitude_deg > 90.0) ||
(gps_time.week < 0) || !isfinite(gps_time.sec) ||
(gps_time.sec < 0.0) ||
(gps_time.sec >= GPS_TIME_WEEK_SECONDS))
{
return;
}
for(unsigned int component = 0u ; component < 3u ; component++)
{
if(!isfinite(receiver_ecef_m[component]))
{
return;
}
}
gps_almanac_progress_t almanac_progress;
bool almanac_complete = gps_almanac_cache_progress(
&gps_almanac_cache, now, gps_time,
&almanac_progress) && almanac_progress.complete;
if(!almanac_complete)
{
/* A partial almanac cannot safely classify the unseen constellation.
* Restore the original all-PRN round robin until SF4/SF5 are complete. */
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
(void)gps_acq_manager_set_visibility(
&gps_acq_manager, prn, false, false, 0);
}
gps_acq_manager_set_visibility_mode(
&gps_acq_manager, false, now);
gps_last_visibility_update_tick = now;
gps_acq_schedule_pending = true;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_SCHED,
"acquisition sky model: active=0 almanac_complete=0 orbit=%u unavailable=%u missing=%u ref=%u sf4_health=%u sf5_health=%u time=%u policy=all_prn_round_robin\r\n",
almanac_progress.matching_orbit_count,
almanac_progress.unavailable_count,
almanac_progress.missing_count,
almanac_progress.reference_valid ? 1u : 0u,
almanac_progress.sf4_health_valid ? 1u : 0u,
almanac_progress.sf5_health_valid ? 1u : 0u,
almanac_progress.time_valid ? 1u : 0u);
return;
}
unsigned int known_count = 0u;
unsigned int ephemeris_orbit_count = 0u;
unsigned int almanac_orbit_count = 0u;
unsigned int usable_orbit_count = 0u;
unsigned int visible_count = 0u;
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
gps_ephemeris_t ephemeris;
bool have_ephemeris = gps_ephemeris_cache_restore(
&gps_ephemeris_cache, prn, now, &ephemeris);
bool almanac_healthy = false;
bool have_almanac = false;
if(!have_ephemeris)
{
have_almanac = gps_almanac_cache_to_ephemeris(
&gps_almanac_cache, prn, now, gps_time,
&ephemeris, &almanac_healthy);
}
if(!have_ephemeris && !have_almanac)
{
bool unavailable = gps_almanac_cache_prn_unavailable(
&gps_almanac_cache, prn, now, gps_time);
(void)gps_acq_manager_set_visibility(
&gps_acq_manager, prn, unavailable, false, -900);
known_count += unavailable ? 1u : 0u;
continue;
}
known_count++;
ephemeris_orbit_count += have_ephemeris ? 1u : 0u;
almanac_orbit_count += have_almanac ? 1u : 0u;
bool orbit_usable = ephemeris.ephemeris_valid &&
(ephemeris.SVID == prn) &&
(have_ephemeris ? (ephemeris.svhlth == 0u) :
almanac_healthy) &&
(ephemeris.update == 0u) &&
(ephemeris.A > 0.0) &&
(ephemeris.ecc >= 0.0) &&
(ephemeris.ecc < 1.0);
double elevation_deg = -90.0;
if(orbit_usable)
{
ephemeris.tow = gps_time;
double satellite_position_m[3] = {0.0, 0.0, 0.0};
double satellite_velocity_mps[3] = {0.0, 0.0, 0.0};
double satellite_clock[2] = {0.0, 0.0};
double satellite_clock_correction = 0.0;
satpos(&ephemeris, satellite_position_m,
satellite_velocity_mps, satellite_clock,
&satellite_clock_correction);
orbit_usable = gps_visibility_elevation_deg(
receiver_ecef_m, receiver_latitude_deg,
receiver_longitude_deg, satellite_position_m,
&elevation_deg);
}
usable_orbit_count += orbit_usable ? 1u : 0u;
bool predicted_visible = orbit_usable &&
(elevation_deg >= GPS_ACQ_MIN_ELEVATION_DEG);
visible_count += predicted_visible ? 1u : 0u;
int priority = isfinite(elevation_deg)
? (int)llround(elevation_deg * 10.0) : -900;
(void)gps_acq_manager_set_visibility(
&gps_acq_manager, prn, true,
predicted_visible, priority);
}
if(visible_count < GPS_ACQ_MIN_SKY_MODEL_VISIBLE_COUNT)
{
for(unsigned int prn = 0u ; prn < GPS_PRN_COUNT ; prn++)
{
(void)gps_acq_manager_set_visibility(
&gps_acq_manager, prn, false, false, 0);
}
gps_acq_manager_set_visibility_mode(
&gps_acq_manager, false, now);
gps_last_visibility_update_tick = now;
gps_acq_schedule_pending = true;
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_SCHED,
"acquisition sky model rejected: predicted_visible=%u min=%u known=%u ephemeris=%u almanac=%u policy=all_prn_round_robin\r\n",
visible_count, GPS_ACQ_MIN_SKY_MODEL_VISIBLE_COUNT,
known_count, ephemeris_orbit_count,
almanac_orbit_count);
return;
}
gps_acq_manager_set_visibility_mode(
&gps_acq_manager, true, now);
gps_last_visibility_update_tick = now;
gps_acq_schedule_pending = true;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_SCHED,
"acquisition sky model: active=1 almanac_complete=1 known=%u ephemeris=%u almanac=%u unavailable=%u usable_orbit=%u predicted_visible=%u mask_deg=%.1f safety_probe_s=%u\r\n",
known_count, ephemeris_orbit_count, almanac_orbit_count,
almanac_progress.unavailable_count, usable_orbit_count,
visible_count,
GPS_ACQ_MIN_ELEVATION_DEG,
GPS_ACQ_VISIBILITY_PROBE_TICKS / GPS_SYSTICK_HZ);
}
bool gps_interrupt_attach_external_source(
unsigned int irq_id,
gps_external_irq_handler_t handler)
@@ -718,6 +918,55 @@ static bool gps_tracking_prn_active(unsigned int prn_no)
return false;
}
static bool gps_tracking_prn_measurement_alive(
unsigned int prn_no,
unsigned char* trck_ip_no_out)
{
if(!gps_prn_valid(prn_no))
{
return false;
}
uint64_t now = gps_get_systick();
for(unsigned char i = 0 ; i < GPS_TRCK_IP_COUNT ; i++)
{
volatile gps_trck_t* trk = trck_ips[i];
uint32_t status = trk->STATUS_REG;
bool busy = (status & (1u << GPS_TRCK_BUSY_POS)) != 0u;
bool locked = (status & (1u << GPS_TRCK_LOCK_POS)) != 0u;
bool stop_unlock =
(status & (1u << GPS_TRCK_STOP_UNLOCK_POS)) != 0u;
bool allocated =
tracking_allocated[i] || trck_ip_exists(&tracking_node, i);
if(!(allocated || busy) || (trk->PRN_NUMBER != prn_no) ||
!busy || !locked || stop_unlock)
{
continue;
}
uint64_t irq_state = bsp_irq_save();
uint64_t last_tick = gps_latest_tracking_measurement_tick[i];
bsp_irq_restore(irq_state);
if((last_tick == 0u) || (now < last_tick) ||
((now - last_tick) > GPS_TRK_MEASUREMENT_LIVENESS_TICKS))
{
continue;
}
gps_tracking_measurement_t measurement;
if(gps_copy_latest_tracking_measurement(i, prn_no, &measurement) &&
measurement.locked)
{
if(trck_ip_no_out != NULL)
{
*trck_ip_no_out = i;
}
return true;
}
}
return false;
}
static void gps_mark_satellite_tracking(unsigned int prn_no, unsigned char trck_ip_no)
{
if(gps_prn_valid(prn_no))
@@ -798,10 +1047,21 @@ bool assign_tracking_from_result(const gps_acq_result_t* result)
tracking_allocated[i] = true;
tracking_not_busy_polls[i] = 0u;
gps_clear_tracking_slot(i);
gps_frames_parsed[i].gps_ephems.SVID =
(unsigned char)prn_no;
gps_ephemeris_t cached_ephemeris;
if(gps_ephemeris_cache_restore(
gps_cached_time_anchor_t cached_anchor;
memset(&cached_anchor, 0, sizeof(cached_anchor));
bool ephemeris_restored = gps_ephemeris_cache_restore(
&gps_ephemeris_cache, prn_no,
gps_get_systick(), &cached_ephemeris))
gps_get_systick(), &cached_ephemeris);
bool anchor_restored = ephemeris_restored &&
gps_ephemeris_cache_restore_time_anchor(
&gps_ephemeris_cache, prn_no,
gps_get_systick(),
GPS_HOW_REACQ_ANCHOR_MAX_AGE_TICKS,
&cached_anchor);
if(ephemeris_restored)
{
gps_frames_parsed[i].gps_ephems =
cached_ephemeris;
@@ -811,6 +1071,32 @@ bool assign_tracking_from_result(const gps_acq_result_t* result)
"restored ephemeris cache: prn=%u trk=%u iode=%u\r\n",
prn_no, i, cached_ephemeris.iode);
}
if(!insert_element(&tracking_node,
&gps_frames_parsed[i].gps_ephems, i))
{
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_TRK,
"tracking node allocation failed: prn=%u trk=%u\r\n",
prn_no, i);
}
volatile gps_ephem_ll_t* node =
get_element_by_ip_number(&tracking_node, i);
if((node != NULL) && anchor_restored)
{
node->gps_how_tow = cached_anchor.tow;
node->gps_how_abs_sample = cached_anchor.raw_sample;
node->gps_how_corrected_abs_sample =
cached_anchor.corrected_sample;
node->gps_how_tick = cached_anchor.committed_tick;
node->gps_how_anchor_valid = true;
node->gps_how_week_valid = true;
node->gps_nav_phase_seeded = false;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"restored HOW/sample anchor: prn=%u trk=%u week=%d tow=%.3f age_ticks=%lu\r\n",
prn_no, i, cached_anchor.tow.week,
cached_anchor.tow.sec,
(unsigned long)(gps_get_systick() -
cached_anchor.committed_tick));
}
gps_set_tracking_nav_bits_mode(i);
trck_ips[i]->PRN_NUMBER = result->prn;
trck_ips[i]->CODE_PHASE = result->code_phase;
@@ -1106,24 +1392,46 @@ static void gps_poll_tracking_pending_events(void)
}
}
static void gps_mark_node_fresh(volatile gps_ephem_ll_t* node, unsigned int status, uint64_t tick)
static void gps_mark_node_fresh(volatile gps_ephem_ll_t* node,
unsigned int status,
uint64_t tick,
bool periodic_nav_solve)
{
if(node == NULL)
{
return;
}
node->gps_last_measurement_tick = tick;
node->gps_measurement_valid = ((status & (1u << GPS_TRCK_LOCK_POS)) != 0u) &&
((status & (1u << GPS_TRCK_STOP_UNLOCK_POS)) == 0u);
bool tracking_locked =
((status & (1u << GPS_TRCK_LOCK_POS)) != 0u) &&
((status & (1u << GPS_TRCK_STOP_UNLOCK_POS)) == 0u);
if(periodic_nav_solve)
{
gps_measurement_freshness_note_nav_solve(
&node->gps_measurement_freshness,
tracking_locked, tick);
}
else
{
gps_measurement_freshness_note_lnav(
&node->gps_measurement_freshness,
tracking_locked, tick);
}
if((node->data_p != NULL) && gps_prn_valid(node->data_p->SVID))
{
unsigned int prn = node->data_p->SVID;
if(node->gps_measurement_valid)
/* LNAV proves hardware lock during cold collection. After NAV solve is
* armed, only its short-period events may recover a measurement-stale
* channel; a later six-second LNAV event must not create another state
* oscillation. */
if(tracking_locked &&
(periodic_nav_solve ||
(node->gps_measurement_freshness.phase ==
GPS_MEASUREMENT_PHASE_LNAV)))
{
gps_mark_satellite_tracking(prn, node->tracking_ip_no);
}
else
else if(!tracking_locked)
{
gps_acq_manager_mark_stale(&gps_acq_manager, prn);
satellite_tracking_ip[prn] = node->tracking_ip_no;
@@ -1157,7 +1465,8 @@ static void gps_process_tracking_drop(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)
{
node->gps_measurement_valid = false;
gps_measurement_freshness_invalidate(
&node->gps_measurement_freshness);
}
tracking_allocated[trck_ip_no] = false;
@@ -1201,6 +1510,10 @@ static void gps_commit_how_anchor(volatile gps_ephem_ll_t* node,
const gps_isr_event_t* event,
bool week_valid)
{
bool preserve_phase_seed =
gps_nav_phase_preserve_seed_after_how(
node->gps_nav_phase_seeded,
event->control);
node->gps_how_tow.week = week;
node->gps_how_tow.sec = tow_sec;
node->gps_how_abs_sample = event->abs_sample_raw;
@@ -1208,7 +1521,26 @@ static void gps_commit_how_anchor(volatile gps_ephem_ll_t* node,
node->gps_how_tick = event->tick;
node->gps_how_anchor_valid = true;
node->gps_how_week_valid = week_valid;
/* A HOW/sample anchor is only the information needed to seed the hardware
* periodic timer. Cold channels reach this point before NAV_SOLVE_PHASE
* has been written, so they must stay unseeded until the background pass
* aligns the timer from the newest 20 ms mailbox. */
node->gps_nav_phase_seeded = preserve_phase_seed;
node->gps_pending_how_valid = false;
if((node->data_p != NULL) && gps_prn_valid(node->data_p->SVID))
{
gps_cached_time_anchor_t cached_anchor = {
.tow = {.week = week, .sec = tow_sec},
.raw_sample = event->abs_sample_raw,
.corrected_sample = event->corrected_abs_sample,
.committed_tick = event->tick,
.week_valid = week_valid,
.valid = true
};
(void)gps_ephemeris_cache_store_time_anchor(
&gps_ephemeris_cache, node->data_p->SVID,
&cached_anchor, event->tick);
}
}
static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
@@ -1243,34 +1575,94 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
gps_frame_parsed->gps_ephems.abs_sample = event->corrected_abs_sample;
gps_frame_parsed->gps_ephems.SVID = (unsigned char)event->prn;
ionoutc_t ionoutc_before;
bool ionoutc_before_valid = gps_get_ionoutc(&ionoutc_before);
unsigned int how_tow_sec = 0u;
unsigned char subframe_id = parse_subframe((unsigned int*)gps_subframe,
(gps_frame_t*)gps_frame_parsed,
&how_tow_sec);
if((subframe_id == 4u) || (subframe_id == 5u))
{
bool almanac_changed = gps_almanac_cache_ingest(
&gps_almanac_cache, subframe_id,
(const unsigned int*)gps_subframe, event->tick);
if(almanac_changed)
{
gps_assistance_store_note_almanac_change(event->tick);
}
}
ionoutc_t ionoutc_after;
bool ionoutc_after_valid = gps_get_ionoutc(&ionoutc_after);
if(ionoutc_after_valid &&
(!ionoutc_before_valid ||
(memcmp(&ionoutc_before, &ionoutc_after,
sizeof(ionoutc_after)) != 0)))
{
gps_assistance_store_note_ionoutc_change(event->tick);
}
gps_ephemeris_t restored_ephemeris = {0};
gps_assistance_position_t restored_position = {0};
gpstime_t assistance_time = {0};
bool restore_approved = gps_assistance_store_observe_subframe(
event->prn, subframe_id,
(const unsigned int*)gps_subframe, how_tow_sec,
event->tick, &gps_ephemeris_cache, &gps_almanac_cache,
&restored_ephemeris, &restored_position, &assistance_time);
if(restore_approved &&
!gps_frame_parsed->gps_ephems.ephemeris_valid)
{
gps_frame_parsed->gps_ephems = restored_ephemeris;
gps_frame_parsed->gps_ephems.SVID = (unsigned char)event->prn;
gps_frame_parsed->gps_ephems.tow = assistance_time;
gps_frame_parsed->gps_ephems.abs_sample =
event->corrected_abs_sample;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"tracking hydrated from validated flash ephemeris: prn=%u trk=%u iode=%u sf=%u\r\n",
event->prn, trck_ip_no,
restored_ephemeris.iode, subframe_id);
}
if((subframe_id >= 1u) && (subframe_id <= 3u) &&
(gps_frame_parsed->pending_ephemeris_mask == 0u) &&
gps_ephemeris_cache_store(
&gps_ephemeris_cache, event->prn,
(const gps_ephemeris_t*)&gps_frame_parsed->gps_ephems,
event->tick))
gps_frame_parsed->gps_ephems.ephemeris_valid)
{
gps_runtime_diagnostics.ephemeris_cache_store_total++;
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"stored ephemeris cache: prn=%u trk=%u iode=%u\r\n",
event->prn, trck_ip_no,
gps_frame_parsed->gps_ephems.iode);
bool new_issue =
!gps_ephemeris_cache.entry[event->prn].valid ||
!gps_ephemeris_cache_same_issue(
&gps_ephemeris_cache.entry[event->prn].ephemeris,
(const gps_ephemeris_t*)
&gps_frame_parsed->gps_ephems);
if(gps_ephemeris_cache_store(
&gps_ephemeris_cache, event->prn,
(const gps_ephemeris_t*)&gps_frame_parsed->gps_ephems,
event->tick))
{
gps_runtime_diagnostics.ephemeris_cache_store_total++;
if(new_issue)
{
gps_assistance_store_note_ephemeris_change(
event->prn, event->tick);
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"stored new ephemeris issue: prn=%u trk=%u iodc=%u iode=%u\r\n",
event->prn, trck_ip_no,
gps_frame_parsed->gps_ephems.iodc,
gps_frame_parsed->gps_ephems.iode);
}
}
}
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;
int fallback_week = gps_assistance_time_valid(assistance_time) ?
assistance_time.week : gps_frame_parsed->gps_ephems.tow.week;
if(subframe_id == 1u)
{
fallback_week = gps_time_extend_lnav_week(
node->gps_how_anchor_valid,
node->gps_how_tow.week,
(gps_subframe[2] >> 20) & 0x3ffu);
fallback_week = node->gps_how_anchor_valid ?
gps_time_extend_lnav_week(
true, node->gps_how_tow.week,
(gps_subframe[2] >> 20) & 0x3ffu) :
fallback_week;
}
/* Subframe 1 supplies the modulo-1024 week. Other subframes resolve
* HOW rollover from the previous monotonic anchor. */
@@ -1345,7 +1737,7 @@ static void gps_process_tracking_nav_bits(const gps_isr_event_t* event)
trck_ip_no, subframe_id, how_tow_sec);
}
gps_mark_node_fresh(node, event->status, event->tick);
gps_mark_node_fresh(node, event->status, event->tick, false);
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_TRK, "nav bits: trk=%u prn=%u mask=0x%x status=0x%x\r\n",
trck_ip_no, event->prn, node->gps_subframe_mask, event->status);
@@ -1449,7 +1841,7 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
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;
gps_mark_node_fresh(node, event->status, event->tick);
gps_mark_node_fresh(node, event->status, event->tick, true);
if(!event->pps_capture_valid)
{
@@ -1695,11 +2087,24 @@ static void gps_reconcile_tracked_prns(void)
{
for(unsigned int prn = 0 ; prn < GPS_PRN_COUNT ; prn++)
{
gps_acq_prn_state_t state =
gps_acq_manager_state(&gps_acq_manager, prn);
unsigned char recovered_trk_ip = GPS_PRN_INVALID;
bool stale_alive =
(state == GPS_ACQ_PRN_STALE) &&
gps_tracking_prn_measurement_alive(
prn, &recovered_trk_ip);
if(gps_tracking_prn_active(prn) &&
(gps_acq_manager_state(&gps_acq_manager, prn) !=
GPS_ACQ_PRN_STALE))
((state != GPS_ACQ_PRN_STALE) || stale_alive))
{
gps_acq_manager_mark_tracking(&gps_acq_manager, prn);
if(state == GPS_ACQ_PRN_STALE)
{
gps_mark_satellite_tracking(prn, recovered_trk_ip);
}
else
{
gps_acq_manager_mark_tracking(&gps_acq_manager, prn);
}
}
}
}
@@ -1729,14 +2134,42 @@ static bool gps_acq_engine_available(unsigned int engine_id, volatile gps_acq_t*
return true;
}
static unsigned int gps_free_tracking_channel_count(void)
{
unsigned int free_count = 0u;
for(unsigned char i = 0u ; i < GPS_TRCK_IP_COUNT ; i++)
{
bool busy = (trck_ips[i]->STATUS_REG &
(1u << GPS_TRCK_BUSY_POS)) != 0u;
if(!busy && !tracking_allocated[i] &&
!trck_ip_exists(&tracking_node, i))
{
free_count++;
}
}
return free_count;
}
void gps_acq_scheduler_run(void)
{
uint64_t now = gps_get_systick();
gps_reconcile_tracked_prns();
gps_promote_acquired_satellites();
unsigned int free_tracking_channels =
gps_free_tracking_channel_count();
unsigned int waiting_for_tracking =
gps_acq_manager_count_state(
&gps_acq_manager, GPS_ACQ_PRN_ACQUIRED);
unsigned int searches_with_capacity =
(free_tracking_channels > waiting_for_tracking)
? (free_tracking_channels - waiting_for_tracking) : 0u;
for(unsigned int i = 0 ; i < GPS_ACQ_IP_COUNT ; i++)
{
if(searches_with_capacity == 0u)
{
break;
}
volatile gps_acq_t* acq_ip = gps_get_acq_ip(i);
if(acq_ip == NULL)
{
@@ -1760,6 +2193,7 @@ void gps_acq_scheduler_run(void)
acq_engine_start_tick[i] = now;
acq_engine_stuck_warned[i] = false;
acq_start(acq_ip, (unsigned char)prn);
searches_with_capacity--;
GPS_LOG(DEBUG_DEBUG, GPS_LOG_CAT_SCHED,
"satellite search started: engine=%u prn=%u priority=%s\r\n",
i, prn, hot ? "reacquire" : "cold");
@@ -1853,18 +2287,25 @@ static void gps_mark_stale_measurements(void)
volatile gps_ephem_ll_t* iterator = (volatile gps_ephem_ll_t*)tracking_node;
while(iterator != NULL)
{
if(iterator->gps_measurement_valid &&
((now - iterator->gps_last_measurement_tick) > GPS_TRK_MEASUREMENT_STALE_TICKS))
if(gps_measurement_freshness_expired(
&iterator->gps_measurement_freshness,
now, GPS_TRK_MEASUREMENT_STALE_TICKS))
{
iterator->gps_measurement_valid = false;
uint64_t age = now -
iterator->gps_measurement_freshness.last_event_tick;
gps_measurement_freshness_invalidate(
&iterator->gps_measurement_freshness);
if(iterator->data_p != NULL && gps_prn_valid(iterator->data_p->SVID))
{
gps_acq_manager_mark_stale(
&gps_acq_manager,
iterator->data_p->SVID);
}
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_TRK, "stale measurement: trk=%u age_ticks=%lu\r\n",
iterator->tracking_ip_no, (unsigned long)(now - iterator->gps_last_measurement_tick));
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_TRK,
"stale periodic measurement: trk=%u phase=%u age_ticks=%lu\r\n",
iterator->tracking_ip_no,
(unsigned int)iterator->gps_measurement_freshness.phase,
(unsigned long)age);
}
iterator = iterator->next;
}
@@ -1896,6 +2337,139 @@ static unsigned int gps_prn_for_tracking_channel(
return GPS_PRN_INVALID;
}
static void gps_seed_nav_timers_from_how_anchor(void)
{
const double bit_period_sec =
(double)GPS_NAV_SOLVE_TIMER_TICK_MS / 1000.0;
for(volatile gps_ephem_ll_t* node = tracking_node;
node != NULL; node = node->next)
{
unsigned char trck_ip_no = node->tracking_ip_no;
if(node->gps_nav_phase_seeded ||
!node->gps_how_anchor_valid ||
!node->gps_how_week_valid ||
(node->data_p == NULL) ||
!node->data_p->ephemeris_valid ||
(trck_ip_no >= GPS_TRCK_IP_COUNT))
{
continue;
}
uint32_t status = trck_ips[trck_ip_no]->STATUS_REG;
uint32_t required =
(1u << GPS_TRCK_BUSY_POS) |
(1u << GPS_TRCK_LOCK_POS);
if(((status & required) != required) ||
((status & (1u << GPS_TRCK_STOP_UNLOCK_POS)) != 0u))
{
continue;
}
/* Read the hardware mailbox here instead of using the background
* software copy. Under verbose UART logging that copy can be one or
* more 20 ms bits old, which would seed a different timer phase. */
gps_tracking_measurement_t measurement;
if(!gps_tracking_measurement_read(
trck_ips[trck_ip_no], &measurement) ||
!measurement.valid || !measurement.locked ||
(measurement.prn != node->data_p->SVID))
{
continue;
}
double corrected_sample = gps_correct_abs_sample(
measurement.absolute_sample,
measurement.remaining_code_phase);
int measurement_week = 0;
double measurement_tow_sec = 0.0;
double anchor_elapsed_sec = 0.0;
if(!gps_time_from_periodic_sample_anchor(
node->gps_how_tow.week,
node->gps_how_tow.sec,
node->gps_how_abs_sample,
node->gps_how_corrected_abs_sample,
measurement.absolute_sample,
corrected_sample,
GPS_RF_SAMPLE_RATE_HZ,
GPS_HOW_ANCHOR_MAX_AGE_SEC,
bit_period_sec,
GPS_NAV_TOW_SNAP_TOLERANCE_SEC,
&measurement_week,
&measurement_tow_sec,
&anchor_elapsed_sec))
{
continue;
}
/* The CLINT/systick is independent of the RF MMCM. Agreement with the
* RF-sample elapsed time proves that the shared 32-bit sample counter did
* not reset while the cable/clock was absent. A mismatch retains the
* ephemeris but deliberately waits for one new HOW anchor. */
double local_elapsed_sec = 0.0;
if(!gps_time_elapsed_consistent_with_local_timer(
anchor_elapsed_sec, node->gps_how_tick,
gps_get_systick(), (double)GPS_SYSTICK_HZ,
GPS_HOW_REACQ_LOCAL_TIMER_TOLERANCE_SEC,
&local_elapsed_sec))
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"NAV timer not seeded after RF counter discontinuity: trk=%u prn=%u rf_elapsed=%.3f local_elapsed=%.3f\r\n",
trck_ip_no, node->data_p->SVID,
anchor_elapsed_sec, local_elapsed_sec);
node->gps_how_anchor_valid = false;
continue;
}
double global_bit = round(measurement_tow_sec /
bit_period_sec);
double phase_double = fmod(global_bit,
(double)GPS_NAV_SOLVE_TIMER_TICKS);
if(!isfinite(phase_double))
{
continue;
}
if(phase_double < 0.0)
{
phase_double += GPS_NAV_SOLVE_TIMER_TICKS;
}
unsigned int phase = (unsigned int)phase_double;
if(phase >= GPS_NAV_SOLVE_TIMER_TICKS)
{
continue;
}
/* The measurement's bit edge has already occurred. Loading that bit's
* phase makes the next hardware pulse advance to the next global bit;
* phase zero therefore remains common across all channels. */
trck_seed_nav_solve_phase(phase, trck_ips[trck_ip_no]);
/* If a newer coherent bit reached the hardware mailbox during the APB
* transaction, leave interrupts disabled and repeat from that newer
* mailbox on the next background pass. The timer may be temporarily
* seeded, but no mismatched event can enter an epoch. */
uint16_t hardware_sequence = (uint16_t)
trck_ips[trck_ip_no]->MEASUREMENT_SEQUENCE;
if(hardware_sequence != (uint16_t)measurement.sequence)
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
"cached NAV seed retried after mailbox advance: trk=%u prn=%u old_seq=%u new_seq=%u\r\n",
trck_ip_no, node->data_p->SVID,
(unsigned int)(uint16_t)measurement.sequence,
(unsigned int)hardware_sequence);
continue;
}
node->gps_nav_phase_seeded = true;
gps_measurement_freshness_arm_nav_solve(
&node->gps_measurement_freshness, true,
gps_get_systick());
trck_enable_nav_solve_flag(trck_ips[trck_ip_no]);
trck_enable_nav_solve_intr(trck_ips[trck_ip_no]);
GPS_LOG(DEBUG_INFO, GPS_LOG_CAT_NAV,
"phase-seeded NAV timer: trk=%u prn=%u phase=%u week=%d tow=%.3f abs=%u\r\n",
trck_ip_no, node->data_p->SVID, phase,
measurement_week, measurement_tow_sec,
measurement.absolute_sample);
}
}
static void gps_reconcile_stopped_tracking_channels(void)
{
for(unsigned char i = 0u ; i < GPS_TRCK_IP_COUNT ; i++)
@@ -1942,6 +2516,7 @@ void gps_background_task(void)
gps_poll_acq_pending_completions();
gps_poll_tracking_pending_events();
gps_process_isr_events();
gps_seed_nav_timers_from_how_anchor();
if(gps_take_timer_maintenance_pending())
{
gps_pps_service();
@@ -1954,4 +2529,7 @@ void gps_background_task(void)
{
gps_acq_scheduler_run();
}
gps_assistance_store_service(
&gps_ephemeris_cache, &gps_almanac_cache,
gps_get_systick());
}
+79 -18
View File
@@ -8,7 +8,9 @@
#include "gps/gps_ip_defines.h"
#include "gps/linked_list.h"
#include "gps/gps_interrupt_handlers.h"
#include "gps/gps_assistance_store.h"
#include "gps/gps_nav_epoch.h"
#include "gps/gps_nav_phase.h"
#include "gps/gps_pps.h"
#include "gps/gps_time_anchor.h"
#include "gps/defines.h"
@@ -138,6 +140,16 @@ bool gps_get_ionoutc(ionoutc_t* ionoutc)
return true;
}
void gps_set_ionoutc(const ionoutc_t* ionoutc)
{
if((ionoutc == NULL) || !ionoutc->enable || !ionoutc->vflg)
{
return;
}
gps_last_ionoutc = *ionoutc;
gps_ionoutc_valid = true;
}
static double gps_unwrap_corrected_abs_sample(uint32_t raw_sample,
double corrected_sample,
uint32_t reference_sample)
@@ -565,17 +577,16 @@ static bool gps_node_ready_for_position(volatile gps_ephem_ll_t* node)
}
if(!gps_tracking_ip_alive(node->tracking_ip_no) ||
!gps_node_has_committed_ephemeris(node))
!gps_node_has_committed_ephemeris(node) ||
!node->gps_nav_phase_seeded)
{
return false;
}
if(!node->gps_measurement_valid)
{
return false;
}
uint64_t now = gps_get_systick();
if((now - node->gps_last_measurement_tick) > GPS_TRK_MEASUREMENT_STALE_TICKS)
if(!gps_measurement_freshness_usable(
&node->gps_measurement_freshness,
now, GPS_TRK_MEASUREMENT_STALE_TICKS))
{
return false;
}
@@ -587,7 +598,7 @@ static bool gps_node_ready_for_position(volatile gps_ephem_ll_t* node)
static bool gps_channel_nav_solve_enabled(unsigned char trck_ip_no)
{
return (trck_ips[trck_ip_no]->CONTROL_REG & (1u << GPS_TRCK_NAV_SOLVE_INTR_EN_POS)) != 0u;
return gps_nav_phase_control_armed(trck_ips[trck_ip_no]->CONTROL_REG);
}
static void gps_set_channel_nav_solve_mode(unsigned char trck_ip_no)
@@ -598,6 +609,15 @@ static void gps_set_channel_nav_solve_mode(unsigned char trck_ip_no)
trck_enable_nav_bits_flag(trck_ips[trck_ip_no]);
trck_enable_nav_solve(trck_ips[trck_ip_no]);
trck_enable_nav_solve_flag(trck_ips[trck_ip_no]);
volatile gps_ephem_ll_t* node =
get_element_by_ip_number(&tracking_node, trck_ip_no);
if(node != NULL)
{
gps_measurement_freshness_arm_nav_solve(
&node->gps_measurement_freshness,
gps_tracking_ip_alive(trck_ip_no),
gps_get_systick());
}
/* Enable the interrupt last so hardware cannot raise a nav-solve IRQ
* before its source flag is observable by the ISR. */
trck_enable_nav_solve_intr(trck_ips[trck_ip_no]);
@@ -923,7 +943,6 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
{
goto cleanup;
}
double reference_capture_time = psTVLtime[reference_index];
uint64_t pps_reference_counter = 0u;
double pps_capture_correction_sec = 0.0;
bool pps_reference_counter_valid =
@@ -952,11 +971,28 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
for(unsigned int i = 0 ; i < satellite_count ; i++)
{
double differential_observation_sec = 0.0;
if(!gps_time_sample_differential_observation(
ephemeris[reference_index].tow.week,
ephemeris[reference_index].tow.sec,
ephemeris[reference_index].abs_sample,
ephemeris[i].tow.week,
ephemeris[i].tow.sec,
ephemeris[i].abs_sample,
GPS_RF_SAMPLE_RATE_HZ,
&differential_observation_sec))
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"receiver sample pseudorange rejected: ref=%u row=%u svid=%u\r\n",
reference_index, i, ephemeris[i].SVID);
goto cleanup;
}
preciseTxTimes->matrix[0][i] =
psTVLtime[i] - reference_capture_time + STARTOFFSET;
localPseudoRanges->matrix[0][i] = preciseTxTimes->matrix[0][i] * (SPEED_OF_LIGHT / 1000.0);
differential_observation_sec;
localPseudoRanges->matrix[0][i] =
differential_observation_sec * SPEED_OF_LIGHT;
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "psTVLtime=%.10lf preciseTx=%.10lf pseudo=%.10lf clockCorr=%.10lf corrected=%.10lf\r\n",
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "sample_time_ms=%.10lf rf_delta=%.10lf pseudo=%.10lf clockCorr=%.10lf corrected=%.10lf\r\n",
psTVLtime[i],
preciseTxTimes->matrix[0][i],
localPseudoRanges->matrix[0][i],
@@ -1246,12 +1282,19 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
solution.tdop);
goto pps_anchor_done;
}
/* The synthetic STARTOFFSET datum is absorbed by WLS b. Removing b/c
* recovers the reference satellite's physical flight time; POSTPONE_TOW
* is applied exactly once here, just as it is in satpos(). */
double receiver_tow = ephemeris[reference_index].tow.sec + POSTPONE_TOW +
(STARTOFFSET / 1000.0) -
(filtered_clock_bias_m / SPEED_OF_LIGHT);
/* The reference receiver-sample/transmit-time differences are both zero, so its
* WLS bias is satellite_clock_correction*c - reference_range. Removing
* b/c recovers the physical flight time; POSTPONE_TOW is applied exactly
* once here, just as it is in satpos(). */
double receiver_tow = 0.0;
if(!gps_time_receiver_tow_from_differential_bias(
ephemeris[reference_index].tow.sec + POSTPONE_TOW,
filtered_clock_bias_m, SPEED_OF_LIGHT, &receiver_tow))
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"PPS anchor rejected: invalid differential receiver TOW\r\n");
goto pps_anchor_done;
}
/*
* Keep holdover asserted throughout initial settling and re-anchor
* commits. A coarse commit is accepted while held and clears lock.
@@ -1633,6 +1676,17 @@ bool calculate_position(gps_nav_solution_t* solution_out,
if(nav_ok)
{
nav_solution.tracked_satellite_count = tracked_count;
if(nav_solution.solution_valid && (satellite_count != 0u))
{
gps_assistance_store_note_solution(
&nav_solution, snapshot[0].tow,
gps_get_systick());
gps_acq_update_visibility_hint(
nav_solution.ecef_m,
nav_solution.latitude_deg,
nav_solution.longitude_deg,
snapshot[0].tow);
}
uint64_t measurement_timestamp = gps_nav_median_local_timestamp(
snapshot_local_timestamps, satellite_count);
if(measurement_timestamp != 0u)
@@ -1827,6 +1881,11 @@ void trck_set_nav_solve_timer(unsigned int tick, volatile gps_trck_t* trck_ip)
trck_ip->NAV_SOLVE_TIMER = tick;
}
void trck_seed_nav_solve_phase(unsigned int phase, volatile gps_trck_t* trck_ip)
{
trck_ip->NAV_SOLVE_PHASE = phase;
}
void trck_clear_intr_flag(volatile gps_trck_t* trck_ip)
{
trck_ip->CONTROL_REG |= (1 << GPS_TRCK_INTR_CLR_POS);
@@ -1844,7 +1903,9 @@ void gps_startup_initialization(unsigned int nsp_tick)
GPS_NAV_ENABLE_FDE);
gps_init_acq_tables();
gps_pps_init(nsp_tick);
gps_pps_quality_selector_init(&gps_pps_quality_selector);
gps_pps_quality_selector_init_period(
&gps_pps_quality_selector,
(double)nsp_tick * (double)GPS_NAV_SOLVE_TIMER_TICK_MS / 1000.0);
gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
gps_pps_status_log_decimator = 0u;
gps_pps_reference_svid_valid = false;
@@ -0,0 +1,100 @@
#include "gps/gps_measurement_freshness.h"
#include <stddef.h>
void gps_measurement_freshness_init(
volatile gps_measurement_freshness_t* freshness)
{
if(freshness == NULL)
{
return;
}
freshness->last_event_tick = 0u;
freshness->valid = false;
freshness->phase = GPS_MEASUREMENT_PHASE_LNAV;
}
void gps_measurement_freshness_note_lnav(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick)
{
if(freshness == NULL)
{
return;
}
if(!tracking_locked)
{
freshness->valid = false;
return;
}
/* LNAV remains useful proof of lock, but it must not refresh an armed or
* active periodic-measurement deadline. */
if(freshness->phase == GPS_MEASUREMENT_PHASE_LNAV)
{
freshness->last_event_tick = tick;
freshness->valid = true;
}
}
void gps_measurement_freshness_arm_nav_solve(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick)
{
if(freshness == NULL)
{
return;
}
freshness->phase = GPS_MEASUREMENT_PHASE_NAV_SOLVE_ARMED;
freshness->last_event_tick = tick;
freshness->valid = tracking_locked;
}
void gps_measurement_freshness_note_nav_solve(
volatile gps_measurement_freshness_t* freshness,
bool tracking_locked,
uint64_t tick)
{
if(freshness == NULL)
{
return;
}
freshness->phase = GPS_MEASUREMENT_PHASE_NAV_SOLVE_ACTIVE;
freshness->last_event_tick = tick;
freshness->valid = tracking_locked;
}
void gps_measurement_freshness_invalidate(
volatile gps_measurement_freshness_t* freshness)
{
if(freshness != NULL)
{
freshness->valid = false;
}
}
bool gps_measurement_freshness_expired(
const volatile gps_measurement_freshness_t* freshness,
uint64_t now,
uint64_t periodic_deadline_ticks)
{
if((freshness == NULL) || !freshness->valid ||
(freshness->phase == GPS_MEASUREMENT_PHASE_LNAV))
{
return false;
}
return (now - freshness->last_event_tick) > periodic_deadline_ticks;
}
bool gps_measurement_freshness_usable(
const volatile gps_measurement_freshness_t* freshness,
uint64_t now,
uint64_t periodic_deadline_ticks)
{
return (freshness != NULL) && freshness->valid &&
!gps_measurement_freshness_expired(
freshness, now, periodic_deadline_ticks);
}
+49 -19
View File
@@ -65,7 +65,8 @@ enum {
};
#define GPS_PPS_VERSION_ADAPTIVE_LOOP 0x00020200u
#define GPS_PPS_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
#define GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
#define GPS_PPS_REFERENCE_TIMEOUT_PERIODS 3u
static volatile gps_pps_regs_t* const gps_pps =
(volatile gps_pps_regs_t*)BSP_GPS_PPS_BASE;
@@ -77,6 +78,7 @@ static bool gps_pps_force_coarse;
static bool gps_pps_reference_committed;
static bool gps_pps_holdover;
static uint64_t gps_pps_last_reference_counter;
static uint64_t gps_pps_reference_timeout_ticks;
static gps_pps_time_filter_t gps_pps_time_filter;
static uint64_t gps_pps_read_u64(
@@ -94,19 +96,30 @@ static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int6
*hi = (uint32_t)(encoded >> 32);
}
static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
static double gps_pps_update_period_sec(void)
{
const double pi = 3.14159265358979323846;
gps_pps_loop_profile_t parameters = gps_pps_loop_profile(profile);
const double update_period_sec = (gps_pps_nav_solve_tick != 0u) ?
((double)gps_pps_nav_solve_tick * 0.020) : 0.200;
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
const double q46 = 70368744177664.0;
int64_t kp_q46 = (int64_t)llround(
(2.0 * parameters.damping * wn / GPS_PPS_CLOCK_HZ) * q46);
int64_t ki_q46 = (int64_t)llround((wn * wn * update_period_sec / GPS_PPS_CLOCK_HZ) * q46);
uint32_t max_slew_q32 = (uint32_t)llround(
parameters.max_slew_ppm * 1.0e-6 * 4294967296.0);
return (gps_pps_nav_solve_tick != 0u) ?
((double)gps_pps_nav_solve_tick * 0.020) :
GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
static bool gps_pps_write_loop_profile(gps_pps_profile_t profile)
{
const double update_period_sec = gps_pps_update_period_sec();
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, update_period_sec);
int64_t kp_q46 = 0;
int64_t ki_q46 = 0;
uint32_t max_slew_q32 = 0u;
if(!gps_pps_loop_coefficients(
parameters, update_period_sec, (double)GPS_PPS_CLOCK_HZ,
&kp_q46, &ki_q46, &max_slew_q32))
{
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"PPS loop coefficient generation failed profile=%s period_ms=%.1lf\r\n",
gps_pps_profile_name(profile), update_period_sec * 1000.0);
return false;
}
gps_pps_write_q46(&gps_pps->kp_q46_lo, &gps_pps->kp_q46_hi, kp_q46);
gps_pps_write_q46(&gps_pps->ki_q46_lo, &gps_pps->ki_q46_hi, ki_q46);
@@ -118,14 +131,25 @@ static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
((parameters.lock_release_ticks & 0xffu) << 16) |
((parameters.lock_acquire_updates & 0xffu) << 24);
}
return true;
}
void gps_pps_init(unsigned int nav_solve_tick)
{
gps_pps_nav_solve_tick = nav_solve_tick;
uint64_t period_ticks =
((uint64_t)GPS_PPS_CLOCK_HZ * nav_solve_tick * 20u) / 1000u;
gps_pps_reference_timeout_ticks =
GPS_PPS_REFERENCE_TIMEOUT_PERIODS * period_ticks;
if(gps_pps_reference_timeout_ticks <
(uint64_t)GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS)
{
gps_pps_reference_timeout_ticks =
GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS;
}
gps_pps->control = 0u;
gps_pps_version = gps_pps->version;
gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
(void)gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
gps_pps_active_loop_profile = GPS_PPS_PROFILE_ROBUST;
/* A GDB CPU reset does not reset the tracking-clock PPS state. Continue
* its mailbox sequence and force the first new solution to re-anchor the
@@ -149,12 +173,14 @@ void gps_pps_init(unsigned int nav_solve_tick)
GPS_PPS_CONTROL_CLEAR_STATUS;
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"PPS init rtl=0x%08x adaptive=%u loop_config=0x%08x "
"clock_hz=%u update_ticks=%u\r\n",
"clock_hz=%u update_ticks=%u reference_timeout_ms=%lu\r\n",
gps_pps_version,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ? 1u : 0u,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ?
gps_pps->loop_config : 0u,
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick);
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick,
(unsigned long)((gps_pps_reference_timeout_ticks * 1000u) /
GPS_PPS_CLOCK_HZ));
}
void gps_pps_service(void)
@@ -170,7 +196,7 @@ void gps_pps_service(void)
uint32_t now_hi = gps_pps->raw_counter_hi;
uint64_t now = ((uint64_t)now_hi << 32) | now_lo;
if((now - gps_pps_last_reference_counter) >
(uint64_t)GPS_PPS_REFERENCE_TIMEOUT_TICKS)
gps_pps_reference_timeout_ticks)
{
gps_pps_set_holdover(true);
}
@@ -229,7 +255,10 @@ bool gps_pps_set_loop_profile(gps_pps_profile_t profile)
{
return false;
}
gps_pps_write_loop_profile(profile);
if(!gps_pps_write_loop_profile(profile))
{
return false;
}
gps_pps_active_loop_profile = profile;
return true;
}
@@ -277,7 +306,8 @@ gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
double* innovation_m_out)
{
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile(gps_pps_active_loop_profile);
gps_pps_loop_profile_for_period(
gps_pps_active_loop_profile, gps_pps_update_period_sec());
return gps_pps_time_filter_update(
&gps_pps_time_filter, epoch_counter, (double)GPS_PPS_CLOCK_HZ,
measured_clock_bias_m, clock_drift_valid,
@@ -7,6 +7,33 @@
#define GPS_PPS_PROFILE_UPGRADE_UPDATES 8u
#define GPS_PPS_PROFILE_DEGRADE_UPDATES 3u
#define GPS_PPS_MIN_HYSTERESIS_UPDATES 2u
#define GPS_PPS_MIN_LOCK_UPDATES 2u
#define GPS_PPS_MIN_SETTLE_UPDATES 3u
#define GPS_PPS_MAX_CONFIG_UPDATES 255u
static unsigned int gps_pps_period_updates(
unsigned int baseline_updates,
double update_period_sec,
unsigned int minimum_updates)
{
if(!isfinite(update_period_sec) || (update_period_sec <= 0.0))
{
update_period_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
double duration_sec =
(double)baseline_updates * GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
double required = ceil(duration_sec / update_period_sec - 1.0e-12);
if(required < (double)minimum_updates)
{
return minimum_updates;
}
if(required > (double)GPS_PPS_MAX_CONFIG_UPDATES)
{
return GPS_PPS_MAX_CONFIG_UPDATES;
}
return (unsigned int)required;
}
static bool gps_pps_metrics_finite(const gps_pps_quality_metrics_t* metrics)
{
@@ -67,12 +94,26 @@ static gps_pps_profile_t gps_pps_classify_quality(
}
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector)
{
gps_pps_quality_selector_init_period(
selector, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec)
{
if(selector != NULL)
{
memset(selector, 0, sizeof(*selector));
selector->profile = GPS_PPS_PROFILE_HOLDOVER;
selector->candidate = GPS_PPS_PROFILE_HOLDOVER;
selector->upgrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_UPGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
selector->degrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_DEGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
}
}
@@ -111,8 +152,8 @@ gps_pps_profile_t gps_pps_quality_selector_update(
unsigned int required_updates =
(target > selector->profile) ?
GPS_PPS_PROFILE_UPGRADE_UPDATES :
GPS_PPS_PROFILE_DEGRADE_UPDATES;
selector->upgrade_updates :
selector->degrade_updates;
if(selector->candidate_updates >= required_updates)
{
selector->profile = target;
@@ -127,10 +168,19 @@ gps_pps_profile_t gps_pps_quality_selector_update(
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
{
return gps_pps_loop_profile_for_period(
profile, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec)
{
gps_pps_loop_profile_t parameters;
switch(profile)
{
case GPS_PPS_PROFILE_ACQUIRE:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.120,
.damping = 0.9,
.max_slew_ppm = 20.0,
@@ -140,8 +190,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 16.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_PRECISION:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.035,
.damping = 0.9,
.max_slew_ppm = 20.0,
@@ -151,8 +202,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_ROBUST:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.012,
.damping = 1.0,
.max_slew_ppm = 20.0,
@@ -162,9 +214,10 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
case GPS_PPS_PROFILE_HOLDOVER:
default:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.0,
.damping = 1.0,
.max_slew_ppm = 20.0,
@@ -174,7 +227,106 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
}
parameters.lock_acquire_updates = gps_pps_period_updates(
parameters.lock_acquire_updates, update_period_sec,
GPS_PPS_MIN_LOCK_UPDATES);
parameters.software_settle_updates = gps_pps_period_updates(
parameters.software_settle_updates, update_period_sec,
GPS_PPS_MIN_SETTLE_UPDATES);
return parameters;
}
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out)
{
if((kp_q46_out == NULL) || (ki_q46_out == NULL) ||
(max_slew_q32_out == NULL) ||
!isfinite(update_period_sec) || (update_period_sec <= 0.0) ||
!isfinite(clock_frequency_hz) || (clock_frequency_hz <= 0.0) ||
!isfinite(parameters.natural_frequency_hz) ||
(parameters.natural_frequency_hz < 0.0) ||
!isfinite(parameters.damping) || (parameters.damping < 0.0) ||
!isfinite(parameters.max_slew_ppm) ||
(parameters.max_slew_ppm < 0.0))
{
return false;
}
const double pi = 3.14159265358979323846;
const double q46 = 70368744177664.0;
const double q32 = 4294967296.0;
const double baseline_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
/* Use the actual deployed Q2.46 baseline, not its ideal double value, so
* the mapped poles are those of the proven fixed-point implementation. */
const double baseline_kp =
(double)llround(
(2.0 * parameters.damping * wn / clock_frequency_hz) * q46) /
q46;
const double baseline_ki =
(double)llround(
(wn * wn * baseline_sec / clock_frequency_hz) * q46) /
q46;
const double baseline_ticks = clock_frequency_hz * baseline_sec;
const double baseline_sum =
2.0 - baseline_ticks * (baseline_kp + baseline_ki);
const double baseline_product =
1.0 - baseline_ticks * baseline_kp;
const double discriminant =
baseline_sum * baseline_sum - 4.0 * baseline_product;
const double period_ratio = update_period_sec / baseline_sec;
double target_sum = 0.0;
double target_product = 0.0;
if(discriminant >= 0.0)
{
double root = sqrt(discriminant);
double pole_a = 0.5 * (baseline_sum + root);
double pole_b = 0.5 * (baseline_sum - root);
if((pole_a < 0.0) || (pole_b < 0.0))
{
return false;
}
double target_a = pow(pole_a, period_ratio);
double target_b = pow(pole_b, period_ratio);
target_sum = target_a + target_b;
target_product = target_a * target_b;
}
else
{
if(baseline_product <= 0.0)
{
return false;
}
double radius = sqrt(baseline_product);
double cosine = baseline_sum / (2.0 * radius);
cosine = fmax(-1.0, fmin(1.0, cosine));
double angle = acos(cosine);
double target_radius = pow(radius, period_ratio);
target_sum =
2.0 * target_radius * cos(angle * period_ratio);
target_product = target_radius * target_radius;
}
double target_ticks = clock_frequency_hz * update_period_sec;
double kp = (1.0 - target_product) / target_ticks;
double ki = (1.0 - target_sum + target_product) / target_ticks;
if(!isfinite(kp) || !isfinite(ki) || (kp < 0.0) || (ki < 0.0))
{
return false;
}
*kp_q46_out = (int64_t)llround(kp * q46);
*ki_q46_out = (int64_t)llround(ki * q46);
*max_slew_q32_out = (uint32_t)llround(
parameters.max_slew_ppm * 1.0e-6 * q32);
return true;
}
const char* gps_pps_profile_name(gps_pps_profile_t profile)
@@ -20,6 +20,7 @@
#define GPS_PPS_FILTER_INNOVATION_CLAMP_M 300.0
#define GPS_PPS_FILTER_HARD_OUTLIER_M 1500.0
#define GPS_PPS_SPEED_OF_LIGHT_MPS 299792458.0
#define GPS_PPS_FILTER_BASELINE_PERIOD_SEC 0.200
static double clamp_double(double value, double limit)
{
@@ -34,6 +35,21 @@ static double clamp_double(double value, double limit)
return value;
}
/* Preserve the baseline discrete pole when the navigation interval changes.
* Applying a baseline gain alpha N times is equivalent to one elapsed-time
* update with alpha_T = 1 - (1 - alpha)^N. This also handles an occasional
* missed solve without slowing the clock-bias or drift estimator. */
static double gps_pps_filter_elapsed_gain(
double baseline_gain, double elapsed_sec)
{
double periods = elapsed_sec / GPS_PPS_FILTER_BASELINE_PERIOD_SEC;
if(fabs(periods - 1.0) <= 1.0e-12)
{
return baseline_gain;
}
return -expm1(periods * log1p(-baseline_gain));
}
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter)
{
if(filter != NULL)
@@ -269,14 +285,20 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
return GPS_PPS_TIME_FILTER_REJECTED;
}
double effective_bias_gain =
gps_pps_filter_elapsed_gain(bias_gain, elapsed_sec);
double effective_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_DRIFT_GAIN, elapsed_sec);
double effective_fallback_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN, elapsed_sec);
filter->clock_bias_m =
predicted_clock_bias_m +
bias_gain *
effective_bias_gain *
clamp_double(innovation_m, GPS_PPS_FILTER_INNOVATION_CLAMP_M);
if(clock_drift_valid)
{
filter->clock_drift_mps +=
GPS_PPS_FILTER_DRIFT_GAIN *
effective_drift_gain *
(measured_clock_drift_mps - filter->clock_drift_mps);
}
else
@@ -291,7 +313,7 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
else
{
filter->clock_drift_mps +=
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN *
effective_fallback_drift_gain *
(fallback_drift_mps - filter->clock_drift_mps);
}
}
@@ -168,6 +168,33 @@ bool gps_time_from_periodic_sample_anchor(
return true;
}
bool gps_time_elapsed_consistent_with_local_timer(
double rf_elapsed_sec,
uint64_t anchor_local_tick,
uint64_t event_local_tick,
double local_ticks_per_second,
double tolerance_sec,
double* local_elapsed_sec_out)
{
if(!isfinite(rf_elapsed_sec) || (rf_elapsed_sec < 0.0) ||
!isfinite(local_ticks_per_second) ||
!(local_ticks_per_second > 0.0) ||
!isfinite(tolerance_sec) || !(tolerance_sec >= 0.0) ||
(event_local_tick < anchor_local_tick))
{
return false;
}
double local_elapsed_sec =
(double)(event_local_tick - anchor_local_tick) /
local_ticks_per_second;
if(local_elapsed_sec_out != NULL)
{
*local_elapsed_sec_out = local_elapsed_sec;
}
return isfinite(local_elapsed_sec) &&
(fabs(local_elapsed_sec - rf_elapsed_sec) <= tolerance_sec);
}
bool gps_time_correct_capture_counter(
uint64_t capture_counter,
double fractional_sample_delay,
@@ -212,6 +239,117 @@ bool gps_time_correct_capture_counter(
return true;
}
bool gps_time_differential_observation(
int reference_week,
double reference_tow_sec,
uint64_t reference_counter,
int event_week,
double event_tow_sec,
uint64_t event_counter,
double counter_frequency_hz,
double* observation_sec_out)
{
if((observation_sec_out == NULL) ||
(reference_week < 0) || (event_week < 0) ||
!isfinite(reference_tow_sec) || !isfinite(event_tow_sec) ||
(reference_tow_sec < 0.0) ||
(reference_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
(event_tow_sec < 0.0) ||
(event_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
!isfinite(counter_frequency_hz) || !(counter_frequency_hz > 0.0))
{
return false;
}
/* Subtract the counters as integers before conversion. This retains every
* hardware tick even after the 64-bit timer has run beyond double's exact
* integer range. */
double receiver_delta_ticks = (event_counter >= reference_counter)
? (double)(event_counter - reference_counter)
: -(double)(reference_counter - event_counter);
double receiver_delta_sec =
receiver_delta_ticks / counter_frequency_hz;
double transmit_delta_sec =
((double)event_week - (double)reference_week) *
GPS_TIME_WEEK_SECONDS +
event_tow_sec - reference_tow_sec;
double observation_sec = receiver_delta_sec - transmit_delta_sec;
if(!isfinite(receiver_delta_sec) || !isfinite(transmit_delta_sec) ||
!isfinite(observation_sec))
{
return false;
}
*observation_sec_out = observation_sec;
return true;
}
bool gps_time_sample_differential_observation(
int reference_week,
double reference_tow_sec,
double reference_sample,
int event_week,
double event_tow_sec,
double event_sample,
double samples_per_second,
double* observation_sec_out)
{
if((observation_sec_out == NULL) ||
(reference_week < 0) || (event_week < 0) ||
!isfinite(reference_tow_sec) || !isfinite(event_tow_sec) ||
(reference_tow_sec < 0.0) ||
(reference_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
(event_tow_sec < 0.0) ||
(event_tow_sec >= GPS_TIME_WEEK_SECONDS) ||
!isfinite(reference_sample) || !isfinite(event_sample) ||
!isfinite(samples_per_second) || !(samples_per_second > 0.0))
{
return false;
}
double receiver_delta_sec =
(event_sample - reference_sample) / samples_per_second;
double transmit_delta_sec =
((double)event_week - (double)reference_week) *
GPS_TIME_WEEK_SECONDS +
event_tow_sec - reference_tow_sec;
double observation_sec = receiver_delta_sec - transmit_delta_sec;
if(!isfinite(receiver_delta_sec) || !isfinite(transmit_delta_sec) ||
!isfinite(observation_sec))
{
return false;
}
*observation_sec_out = observation_sec;
return true;
}
bool gps_time_receiver_tow_from_differential_bias(
double corrected_transmit_tow_sec,
double wls_clock_bias_m,
double propagation_speed_mps,
double* receiver_tow_sec_out)
{
if((receiver_tow_sec_out == NULL) ||
!isfinite(corrected_transmit_tow_sec) ||
!isfinite(wls_clock_bias_m) ||
!isfinite(propagation_speed_mps) ||
!(propagation_speed_mps > 0.0))
{
return false;
}
double receiver_tow_sec = corrected_transmit_tow_sec -
wls_clock_bias_m / propagation_speed_mps;
if(!isfinite(receiver_tow_sec))
{
return false;
}
*receiver_tow_sec_out = receiver_tow_sec;
return true;
}
bool gps_time_anchors_consistent(int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
@@ -0,0 +1,59 @@
#include "gps/gps_visibility.h"
#include <math.h>
#include <stddef.h>
#define GPS_VISIBILITY_PI 3.14159265358979323846
bool gps_visibility_elevation_deg(
const double receiver_ecef_m[3],
double receiver_latitude_deg,
double receiver_longitude_deg,
const double satellite_ecef_m[3],
double* elevation_deg_out)
{
if((receiver_ecef_m == NULL) || (satellite_ecef_m == NULL) ||
(elevation_deg_out == NULL) ||
!isfinite(receiver_latitude_deg) ||
!isfinite(receiver_longitude_deg) ||
(receiver_latitude_deg < -90.0) ||
(receiver_latitude_deg > 90.0))
{
return false;
}
double latitude_rad =
receiver_latitude_deg * GPS_VISIBILITY_PI / 180.0;
double longitude_rad =
receiver_longitude_deg * GPS_VISIBILITY_PI / 180.0;
double receiver_up[3] = {
cos(latitude_rad) * cos(longitude_rad),
cos(latitude_rad) * sin(longitude_rad),
sin(latitude_rad)
};
double range_squared = 0.0;
double up_projection_m = 0.0;
for(unsigned int component = 0u ; component < 3u ; component++)
{
if(!isfinite(receiver_ecef_m[component]) ||
!isfinite(satellite_ecef_m[component]))
{
return false;
}
double line_of_sight_m =
satellite_ecef_m[component] -
receiver_ecef_m[component];
range_squared += line_of_sight_m * line_of_sight_m;
up_projection_m += line_of_sight_m * receiver_up[component];
}
if(!isfinite(range_squared) || !(range_squared > 0.0))
{
return false;
}
double sine_elevation = up_projection_m / sqrt(range_squared);
sine_elevation = fmax(-1.0, fmin(1.0, sine_elevation));
*elevation_deg_out =
asin(sine_elevation) * 180.0 / GPS_VISIBILITY_PI;
return isfinite(*elevation_deg_out);
}
@@ -67,6 +67,7 @@ bool insert_element(volatile gps_ephem_ll_t** node,
new_node->gps_how_tick = 0u;
new_node->gps_how_anchor_valid = false;
new_node->gps_how_week_valid = false;
new_node->gps_nav_phase_seeded = 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;
@@ -80,8 +81,8 @@ bool insert_element(volatile gps_ephem_ll_t** node,
new_node->gps_last_nav_solve_local_ticks = 0u;
new_node->gps_last_nav_solve_pps_counter = 0u;
new_node->gps_last_pps_capture_sequence = 0u;
new_node->gps_last_measurement_tick = 0u;
new_node->gps_measurement_valid = false;
gps_measurement_freshness_init(
&new_node->gps_measurement_freshness);
if(*node == NULL)
{
+130 -4
View File
@@ -1,18 +1,26 @@
CC ?= gcc
CFLAGS ?= -O2 -std=c11 -Wall -Wextra -Werror
PYTHON ?= python3
ROOT := ..
TARGETS := test_imu_calibration test_gps_nav_epoch \
test_gps_nav_epoch_coalescing test_gps_time_anchor \
test_plic_warm_reset test_gps_acq_manager \
test_gps_ephemeris_cache test_gps_receiver_hardware_sim
TARGETS += test_gps_measurement_freshness
TARGETS += test_gps_nav_phase
TARGETS += test_gps_tracking_measurement
TARGETS += test_gps_satellite_velocity
TARGETS += test_gps_pps_time_filter
TARGETS += test_gps_pps_quality
TARGETS += test_gps_pps_adaptive_loop
TARGETS += test_gps_visibility
TARGETS += test_gps_almanac
TARGETS += test_gps_flash_journal
TARGETS += test_gps_assistance_data
TARGETS += test_gps_assistance_store
.PHONY: all run sanitize clean
.PHONY: all run python-models sanitize clean
all: $(TARGETS)
@@ -28,19 +36,58 @@ test_gps_nav_epoch_coalescing: test_gps_nav_epoch_coalescing.c $(ROOT)/src/gps/g
test_gps_time_anchor: test_gps_time_anchor.c $(ROOT)/src/gps/gps_time_anchor.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_gps_measurement_freshness: test_gps_measurement_freshness.c \
$(ROOT)/src/gps/gps_measurement_freshness.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -o $@
test_gps_nav_phase: test_gps_nav_phase.c
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -o $@
test_plic_warm_reset: test_plic_warm_reset.c $(ROOT)/../bsp/drivers/plic.c
$(CC) $(CFLAGS) -Ifake_bsp -I$(ROOT)/../bsp/include $^ -o $@
test_gps_acq_manager: test_gps_acq_manager.c $(ROOT)/src/gps/gps_acq_manager.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -o $@
test_gps_visibility: test_gps_visibility.c $(ROOT)/src/gps/gps_visibility.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_gps_ephemeris_cache: test_gps_ephemeris_cache.c $(ROOT)/src/gps/gps_ephemeris_cache.c
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -o $@
test_gps_almanac: test_gps_almanac.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/NavEquations.c \
$(ROOT)/src/gps/matrixLib.c
$(CC) $(CFLAGS) -Wno-comment -Wno-unused-parameter \
-Wno-unused-variable -Wno-sign-compare \
-Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
test_gps_flash_journal: test_gps_flash_journal.c \
$(ROOT)/src/gps/gps_flash_journal.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -o $@
test_gps_assistance_data: test_gps_assistance_data.c \
$(ROOT)/src/gps/gps_assistance_data.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_time_anchor.c
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
test_gps_assistance_store: test_gps_assistance_store.c \
$(ROOT)/src/gps/gps_assistance_store.c \
$(ROOT)/src/gps/gps_assistance_data.c \
$(ROOT)/src/gps/gps_flash_journal.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_time_anchor.c
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
test_gps_receiver_hardware_sim: test_gps_receiver_hardware_sim.c \
$(ROOT)/src/gps/gps_acq_manager.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -o $@
$(CC) $(CFLAGS) -Wno-comment -Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
test_gps_tracking_measurement: test_gps_tracking_measurement.c \
$(ROOT)/src/gps/gps_tracking_measurement.c
@@ -71,15 +118,26 @@ run: $(TARGETS)
./test_gps_nav_epoch
./test_gps_nav_epoch_coalescing
./test_gps_time_anchor
./test_gps_measurement_freshness
./test_gps_nav_phase
./test_plic_warm_reset
./test_gps_acq_manager
./test_gps_ephemeris_cache
./test_gps_almanac
./test_gps_flash_journal
./test_gps_assistance_data
./test_gps_assistance_store
./test_gps_receiver_hardware_sim
./test_gps_tracking_measurement
./test_gps_satellite_velocity
./test_gps_pps_time_filter
./test_gps_pps_quality
./test_gps_pps_adaptive_loop
./test_gps_visibility
$(PYTHON) test_acq_search_model.py
python-models:
$(PYTHON) test_acq_search_model.py
sanitize:
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
@@ -103,6 +161,19 @@ sanitize:
-o test_gps_time_anchor_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_time_anchor_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_measurement_freshness.c \
$(ROOT)/src/gps/gps_measurement_freshness.c \
-o test_gps_measurement_freshness_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_measurement_freshness_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment -Ifake_bsp -I$(ROOT)/header \
test_gps_nav_phase.c -o test_gps_nav_phase_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_nav_phase_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_acq_manager.c \
@@ -118,13 +189,56 @@ sanitize:
-o test_gps_ephemeris_cache_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_ephemeris_cache_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment -Wno-unused-parameter -Wno-unused-variable \
-Wno-sign-compare -Ifake_bsp -I$(ROOT)/header \
test_gps_almanac.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/NavEquations.c \
$(ROOT)/src/gps/matrixLib.c -lm \
-o test_gps_almanac_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_almanac_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_flash_journal.c \
$(ROOT)/src/gps/gps_flash_journal.c \
-o test_gps_flash_journal_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_flash_journal_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment -Ifake_bsp -I$(ROOT)/header \
test_gps_assistance_data.c \
$(ROOT)/src/gps/gps_assistance_data.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_time_anchor.c -lm \
-o test_gps_assistance_data_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_assistance_data_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment -Ifake_bsp -I$(ROOT)/header \
test_gps_assistance_store.c \
$(ROOT)/src/gps/gps_assistance_store.c \
$(ROOT)/src/gps/gps_assistance_data.c \
$(ROOT)/src/gps/gps_flash_journal.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_time_anchor.c -lm \
-o test_gps_assistance_store_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_assistance_store_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-Wno-comment -Ifake_bsp -I$(ROOT)/header \
test_gps_receiver_hardware_sim.c \
$(ROOT)/src/gps/gps_acq_manager.c \
$(ROOT)/src/gps/gps_almanac.c \
$(ROOT)/src/gps/gps_ephemeris_cache.c \
-o test_gps_receiver_hardware_sim_san
-lm -o test_gps_receiver_hardware_sim_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 \
./test_gps_receiver_hardware_sim_san
@@ -170,14 +284,26 @@ sanitize:
-o test_gps_pps_adaptive_loop_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_adaptive_loop_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_visibility.c \
$(ROOT)/src/gps/gps_visibility.c -lm \
-o test_gps_visibility_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_visibility_san
clean:
rm -f $(TARGETS) test_gps_nav_epoch_coalescing_repro \
test_gps_nav_epoch_san test_gps_nav_epoch_coalescing_san \
test_gps_time_anchor_san \
test_gps_measurement_freshness_san test_gps_nav_phase_san \
test_gps_acq_manager_san test_gps_ephemeris_cache_san \
test_gps_almanac_san \
test_gps_flash_journal_san \
test_gps_assistance_data_san \
test_gps_assistance_store_san \
test_gps_receiver_hardware_sim_san \
test_gps_tracking_measurement_san \
test_gps_satellite_velocity_san \
test_gps_pps_time_filter_san test_gps_pps_quality_san \
test_gps_pps_adaptive_loop_san
test_gps_pps_adaptive_loop_san test_gps_visibility_san
@@ -0,0 +1,14 @@
#ifndef TEST_BSP_SPI_FLASH_H
#define TEST_BSP_SPI_FLASH_H
#include <stdbool.h>
#include <stdint.h>
void bsp_spi_flash_init(void);
uint32_t bsp_spi_flash_read_id(void);
void bsp_spi_flash_read(uint32_t address, void* destination, uint32_t length);
bool bsp_spi_flash_program_page(
uint32_t address, const void* source, uint32_t length);
bool bsp_spi_flash_erase_sector(uint32_t address);
#endif
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Model-level checks for acquisition search edge cases."""
from pathlib import Path
import re
TEST_DIR = Path(__file__).resolve().parent
REPO_ROOT = TEST_DIR.parents[3]
DEFINITIONS = REPO_ROOT / "hardware/external/gps/rtl/common/definitions.sv"
ACQ_TOP = REPO_ROOT / "hardware/external/gps/rtl/acq_src/acq_top.sv"
def read_parameters(path):
text = path.read_text(encoding="utf-8")
params = {}
parameter_re = (
r"parameter\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([^;]+);"
)
for match in re.finditer(parameter_re, text):
value = match.group(2).strip()
if re.fullmatch(r"\d+", value):
params[match.group(1)] = int(value)
elif re.fullmatch(r"\d+\.\d+", value):
params[match.group(1)] = float(value)
return params
def check(condition, message):
if not condition:
raise AssertionError(message)
def main():
params = read_parameters(DEFINITIONS)
acq_top = ACQ_TOP.read_text(encoding="utf-8")
samples_per_code = params["SAMPLES_PER_CODE"]
num_freq_bin = params["NUM_FREQ_BIN"]
freq_bin_wl = params["FREQ_BIN_WL"]
dopp_step = params["DOPP_STEP"]
pullin_freq_step = params["PULLIN_FREQ_STEP"]
num_pullin = params["NUM_PULLIN"]
acq_base_freq = params["ACQ_BASE_FREQ"]
check(samples_per_code == 16368, "unexpected sample count model")
check(num_freq_bin == 41, "unexpected coarse Doppler bin count")
check(num_pullin == 11, "unexpected pull-in bin count")
check(dopp_step == 500, "unexpected coarse Doppler step")
check(pullin_freq_step == 100, "unexpected pull-in Doppler step")
check(acq_base_freq == 4082000, "unexpected acquisition base frequency")
check(
"freq_bin_cnt <= (max_freq_bin_idx == 0) ? 0 : "
"(max_freq_bin_idx - 1);" in acq_top,
"RTL must saturate pull-in search at coarse bin zero",
)
check(
"rf_wr_addr == 2*SAMPLES_PER_CODE - 1" in acq_top,
"RTL must capture exactly two 1 ms code windows",
)
max_encoded_bin = (1 << freq_bin_wl) - 1
legacy_bin_zero_start = (0 - 1) & max_encoded_bin
check(
legacy_bin_zero_start >= num_freq_bin,
"legacy bin-zero decrement no longer demonstrates an out-of-range wrap",
)
covered_frequencies = set()
for coarse_bin in range(num_freq_bin):
pullin_start = 0 if coarse_bin == 0 else coarse_bin - 1
check(0 <= pullin_start < num_freq_bin, "pull-in start out of coarse range")
coarse_frequency = acq_base_freq + dopp_step * coarse_bin
for pullin_bin in range(num_pullin):
pullin_frequency = (
acq_base_freq +
dopp_step * pullin_start +
pullin_freq_step * pullin_bin
)
covered_frequencies.add(pullin_frequency)
if coarse_bin == 0:
check(
pullin_frequency >= acq_base_freq,
"coarse bin zero pull-in searched below the table",
)
else:
check(
coarse_frequency - dopp_step <= pullin_frequency <=
coarse_frequency + dopp_step,
"pull-in search does not cover the expected +/-500 Hz window",
)
lowest_expected = acq_base_freq
highest_expected = (
acq_base_freq +
dopp_step * (num_freq_bin - 2) +
pullin_freq_step * (num_pullin - 1)
)
check(min(covered_frequencies) == lowest_expected, "unexpected low search edge")
check(max(covered_frequencies) == highest_expected, "unexpected high search edge")
check(len(covered_frequencies) >= num_freq_bin * 5, "pull-in coverage collapsed")
last_sample_written = (2 * samples_per_code) - 1
check(last_sample_written == 32735, "unexpected two-code capture fencepost")
print(
"ACQ search model PASS: "
f"coarse_bins={num_freq_bin} pullin_bins={num_pullin} "
f"freq_range={lowest_expected}..{highest_expected}Hz "
f"last_sample={last_sample_written}"
)
if __name__ == "__main__":
main()
@@ -15,6 +15,8 @@ static gps_acq_manager_t make_manager(
.retry_delay_ticks = retry_delay,
.hot_retry_delay_ticks = hot_retry_delay,
.hot_window_ticks = hot_window,
.unknown_probe_interval_ticks = 2u,
.visibility_probe_interval_ticks = 60u,
.max_hot_streak = max_hot_streak
};
gps_acq_manager_init(&manager, &config);
@@ -132,6 +134,55 @@ static void test_failed_hot_search_is_backed_off(void)
assert(start_next(&manager, 35u, true) == 20u);
}
static void test_visibility_priority_discovery_and_safety_probe(void)
{
gps_acq_manager_t manager =
make_manager(1u, 1u, 100u, 4u);
for(unsigned int prn = 1u ; prn <= 4u ; prn++)
{
assert(gps_acq_manager_enable_cold(&manager, prn));
}
assert(gps_acq_manager_set_visibility(
&manager, 1u, true, true, 400));
assert(gps_acq_manager_set_visibility(
&manager, 2u, true, false, -100));
assert(gps_acq_manager_set_visibility(
&manager, 3u, true, true, 200));
assert(gps_acq_manager_set_visibility(
&manager, 4u, false, false, 0));
gps_acq_manager_set_visibility_mode(&manager, true, 10u);
/* Highest predicted elevation wins, independent of numeric PRN. */
assert(start_next(&manager, 10u, false) == 1u);
/* Keep the highest predicted-visible satellite absent and retryable. This
* used to starve every unknown PRN forever. */
gps_acq_manager_mark_failed(&manager, 1u, 10u);
assert(start_next(&manager, 11u, false) == 1u);
gps_acq_manager_mark_failed(&manager, 1u, 11u);
/* The independent discovery deadline overrides that failing visible
* candidate and advances through the unknown-orbit queue. */
assert(start_next(&manager, 12u, false) == 4u);
gps_acq_manager_mark_tracking(&manager, 4u);
assert(start_next(&manager, 12u, false) == 1u);
gps_acq_manager_mark_tracking(&manager, 1u);
assert(start_next(&manager, 12u, false) == 3u);
gps_acq_manager_mark_tracking(&manager, 3u);
unsigned int ignored;
assert(!gps_acq_manager_start_next(
&manager, 69u, &ignored, NULL));
/* Known below-mask PRNs are not scanned continuously. Exactly one is
* admitted when the slower stale-model safety probe becomes due. */
assert(start_next(&manager, 70u, false) == 2u);
gps_acq_manager_mark_failed(&manager, 2u, 70u);
assert(!gps_acq_manager_start_next(
&manager, 71u, &ignored, NULL));
/* Removing the position/time model restores full cold-search behavior. */
gps_acq_manager_set_visibility_mode(&manager, false, 72u);
assert(start_next(&manager, 72u, false) == 2u);
}
int main(void)
{
test_cold_round_robin_and_retry_from_tick_zero();
@@ -139,6 +190,7 @@ int main(void)
test_expired_hot_becomes_cold_and_tracking_is_excluded();
test_invalid_inputs_are_safe();
test_failed_hot_search_is_backed_off();
test_visibility_priority_discovery_and_safety_probe();
puts("gps acquisition manager tests passed");
return 0;
}
@@ -0,0 +1,234 @@
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "gps/NavEquations.h"
#include "gps/gps_almanac.h"
#define TEST_PI 3.1415926535897932384626433832795
static uint32_t unsigned_field(int32_t value, unsigned int bits)
{
uint32_t mask = (UINT32_C(1) << bits) - 1u;
return (uint32_t)value & mask;
}
static void make_orbit_page(
unsigned int words[10],
unsigned int sv_id,
unsigned int toa_raw,
unsigned char health8)
{
const uint32_t eccentricity_raw = 0x1234u;
const int32_t delta_i_raw = -1234;
const int32_t omega_dot_raw = -321;
const uint32_t sqrt_a_raw = 10555000u;
const int32_t omega0_raw = -345678;
const int32_t argument_raw = 456789;
const int32_t mean_anomaly_raw = -567890;
const int32_t af0_raw_signed = -100;
const int32_t af1_raw_signed = 50;
uint32_t af0_raw = unsigned_field(af0_raw_signed, 11u);
uint32_t af1_raw = unsigned_field(af1_raw_signed, 11u);
memset(words, 0, sizeof(unsigned int) * 10u);
words[2] = (1u << 28) | ((sv_id & 0x3fu) << 22) |
(eccentricity_raw << 6);
words[3] = ((toa_raw & 0xffu) << 22) |
(unsigned_field(delta_i_raw, 16u) << 6);
words[4] = (unsigned_field(omega_dot_raw, 16u) << 14) |
((uint32_t)health8 << 6);
words[5] = sqrt_a_raw << 6;
words[6] = unsigned_field(omega0_raw, 24u) << 6;
words[7] = unsigned_field(argument_raw, 24u) << 6;
words[8] = unsigned_field(mean_anomaly_raw, 24u) << 6;
words[9] = ((af0_raw >> 3) << 22) |
(af1_raw << 11) |
((af0_raw & 0x07u) << 8);
}
static void make_sf5_reference(
unsigned int words[10],
unsigned int toa_raw,
unsigned int week_mod256,
const unsigned char health6[GPS_ALMANAC_PRN_COUNT])
{
memset(words, 0, sizeof(unsigned int) * 10u);
words[2] = (1u << 28) | (51u << 22) |
((toa_raw & 0xffu) << 14) |
((week_mod256 & 0xffu) << 6);
for(unsigned int prn = 0u ; prn < 24u ; prn++)
{
unsigned int word = 3u + (prn / 4u);
unsigned int shift = 24u - (6u * (prn % 4u));
words[word] |= (uint32_t)(health6[prn] & 0x3fu) << shift;
}
}
static void make_sf4_health(
unsigned int words[10],
const unsigned char health6[GPS_ALMANAC_PRN_COUNT])
{
memset(words, 0, sizeof(unsigned int) * 10u);
words[2] = (1u << 28) | (63u << 22);
words[7] = (uint32_t)(health6[24] & 0x3fu) << 6;
for(unsigned int prn = 25u ; prn <= 28u ; prn++)
{
unsigned int shift = 24u - (6u * (prn - 25u));
words[8] |= (uint32_t)(health6[prn] & 0x3fu) << shift;
}
for(unsigned int prn = 29u ; prn <= 31u ; prn++)
{
unsigned int shift = 24u - (6u * (prn - 29u));
words[9] |= (uint32_t)(health6[prn] & 0x3fu) << shift;
}
}
static void ingest_constellation(
gps_almanac_cache_t* cache,
unsigned int toa_raw,
unsigned int week_mod256,
int skipped_prn,
int unavailable_prn,
uint64_t tick)
{
unsigned int words[10];
unsigned char health6[GPS_ALMANAC_PRN_COUNT] = {0};
if(unavailable_prn >= 0)
{
health6[unavailable_prn] = GPS_ALMANAC_UNAVAILABLE_HEALTH;
}
for(unsigned int prn = 0u ; prn < GPS_ALMANAC_PRN_COUNT ; prn++)
{
if((int)prn == skipped_prn)
{
continue;
}
make_orbit_page(words, prn + 1u, toa_raw, 0u);
assert(gps_almanac_cache_ingest(
cache, (prn < 24u) ? 5u : 4u, words, tick + prn));
}
make_sf4_health(words, health6);
assert(gps_almanac_cache_ingest(cache, 4u, words, tick + 40u));
make_sf5_reference(words, toa_raw, week_mod256, health6);
assert(gps_almanac_cache_ingest(cache, 5u, words, tick + 41u));
}
static void test_page_bit_layout_and_zero_based_mapping(void)
{
gps_almanac_cache_t cache;
gps_almanac_cache_init(&cache, 10000u);
unsigned int words[10];
make_orbit_page(words, 1u, 40u, 0x12u);
assert(gps_almanac_cache_ingest(&cache, 5u, words, 100u));
const gps_almanac_entry_t* entry = &cache.entry[0];
assert(entry->valid);
assert(entry->health8 == 0x12u);
assert(fabs(entry->eccentricity - (0x1234u * 0x1p-21)) < 1e-15);
assert(entry->toa_sec == 40.0 * 4096.0);
assert(fabs(entry->delta_i_rad -
(-1234.0 * 0x1p-19 * TEST_PI)) < 1e-15);
assert(fabs(entry->omega_dot_rad_per_sec -
(-321.0 * 0x1p-38 * TEST_PI)) < 1e-20);
assert(fabs(entry->sqrt_a - (10555000.0 * 0x1p-11)) < 1e-12);
assert(fabs(entry->af0_sec - (-100.0 * 0x1p-20)) < 1e-15);
assert(fabs(entry->af1_sec_per_sec - (50.0 * 0x1p-38)) < 1e-20);
assert(!cache.entry[1].valid);
/* Identical repeats refresh RAM age without reporting a content change to
* the flash persistence policy. */
assert(!gps_almanac_cache_ingest(&cache, 5u, words, 200u));
assert(cache.entry[0].received_tick == 200u);
}
static void test_complete_requires_reference_health_and_every_available_orbit(void)
{
gps_almanac_cache_t cache;
gps_almanac_cache_init(&cache, 10000u);
const gpstime_t gps_time = {.week = 2300, .sec = 164000.0};
unsigned int week_mod256 = (unsigned int)gps_time.week & 0xffu;
ingest_constellation(
&cache, 40u, week_mod256, 31, -1, 100u);
gps_almanac_progress_t progress;
assert(gps_almanac_cache_progress(
&cache, 200u, gps_time, &progress));
assert(!progress.complete);
assert(progress.matching_orbit_count == 31u);
assert(progress.missing_count == 1u);
unsigned int words[10];
make_orbit_page(words, 32u, 40u, 0u);
assert(gps_almanac_cache_ingest(&cache, 4u, words, 201u));
assert(gps_almanac_cache_complete(&cache, 201u, gps_time));
}
static void test_explicit_unavailable_satellite_allows_completion(void)
{
gps_almanac_cache_t cache;
gps_almanac_cache_init(&cache, 10000u);
const gpstime_t gps_time = {.week = 2300, .sec = 164000.0};
ingest_constellation(
&cache, 40u, (unsigned int)gps_time.week & 0xffu,
31, 31, 100u);
assert(gps_almanac_cache_complete(&cache, 200u, gps_time));
assert(gps_almanac_cache_prn_unavailable(
&cache, 31u, 200u, gps_time));
gps_ephemeris_t ephemeris;
assert(!gps_almanac_cache_to_ephemeris(
&cache, 31u, 200u, gps_time, &ephemeris, NULL));
}
static void test_conversion_week_health_and_staleness(void)
{
gps_almanac_cache_t cache;
gps_almanac_cache_init(&cache, 1000u);
const gpstime_t gps_time = {.week = 2300, .sec = 164000.0};
ingest_constellation(
&cache, 40u, (unsigned int)gps_time.week & 0xffu,
-1, -1, 100u);
gps_ephemeris_t ephemeris;
bool healthy = false;
assert(gps_almanac_cache_to_ephemeris(
&cache, 0u, 200u, gps_time, &ephemeris, &healthy));
assert(healthy);
assert(ephemeris.SVID == 0u);
assert(ephemeris.WN == 2300u);
assert(ephemeris.toe.sec == 40.0 * 4096.0);
assert(fabs(ephemeris.inc0 -
((0.30 * TEST_PI) + cache.entry[0].delta_i_rad)) < 1e-15);
assert(ephemeris.A == ephemeris.sqrta * ephemeris.sqrta);
double position_m[3] = {0.0, 0.0, 0.0};
double velocity_mps[3] = {0.0, 0.0, 0.0};
double clock[2] = {0.0, 0.0};
double clock_correction = 0.0;
satpos(&ephemeris, position_m, velocity_mps, clock,
&clock_correction);
double radius_m = sqrt(
(position_m[0] * position_m[0]) +
(position_m[1] * position_m[1]) +
(position_m[2] * position_m[2]));
assert(isfinite(radius_m));
assert(radius_m > 2.0e7);
assert(radius_m < 3.0e7);
assert(!gps_almanac_cache_complete(&cache, 1200u, gps_time));
gpstime_t far_time = gps_time;
far_time.sec += 8.0 * 86400.0;
far_time.week++;
far_time.sec -= 604800.0;
assert(!gps_almanac_cache_complete(&cache, 200u, far_time));
}
int main(void)
{
test_page_bit_layout_and_zero_based_mapping();
test_complete_requires_reference_health_and_every_available_orbit();
test_explicit_unavailable_satellite_allows_completion();
test_conversion_week_health_and_staleness();
puts("gps LNAV SF4/SF5 almanac tests passed");
return 0;
}
@@ -0,0 +1,175 @@
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "gps/gps_assistance_data.h"
static gps_ephemeris_t make_ephemeris(
unsigned int prn,
int current_week,
double toe_sec,
unsigned int issue)
{
gps_ephemeris_t ephemeris;
memset(&ephemeris, 0, sizeof(ephemeris));
ephemeris.WN = (unsigned int)current_week & 0x3ffu;
ephemeris.toe.sec = toe_sec;
ephemeris.toc.sec = toe_sec;
ephemeris.sqrta = 5153.795;
ephemeris.A = ephemeris.sqrta * ephemeris.sqrta;
ephemeris.ecc = 0.01;
ephemeris.iodc = issue;
ephemeris.iode = (unsigned char)issue;
ephemeris.SVID = (unsigned char)prn;
ephemeris.ephemeris_valid = true;
ephemeris.abs_sample = 123456789.25;
return ephemeris;
}
static void test_fresh_mixed_ephemeris_restore_and_true_remaining_age(void)
{
const gpstime_t saved_time = {.week = 2300, .sec = 100000.0};
gps_ephemeris_cache_t source;
gps_ephemeris_cache_init(&source, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
for(unsigned int prn = 0u ; prn < 7u ; prn++)
{
double age_sec = (prn == 6u) ? 3.0 * 3600.0 :
(double)(prn + 1u) * 600.0;
gps_ephemeris_t ephemeris = make_ephemeris(
prn, saved_time.week, saved_time.sec - age_sec,
40u + prn);
assert(gps_ephemeris_cache_store(
&source, prn, &ephemeris, 100u));
}
gps_almanac_cache_t source_almanac;
gps_almanac_cache_init(
&source_almanac, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_payload_t payload;
gps_assistance_payload_build(
&payload, &source, &source_almanac, NULL, NULL, saved_time);
assert(payload.ephemeris[0].abs_sample == 0.0);
gps_ephemeris_cache_t restored_cache;
gps_ephemeris_cache_init(
&restored_cache, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_t restored_almanac;
gps_almanac_cache_init(
&restored_almanac, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_restore_result_t result;
const uint64_t boot_now = 10u;
assert(gps_assistance_restore(
&payload, saved_time, boot_now,
&restored_cache, &restored_almanac, &result));
assert(result.ephemeris_mask == UINT32_C(0x3f));
assert(!restored_cache.entry[0].time_anchor.valid);
gps_ephemeris_t output;
assert(gps_ephemeris_cache_restore(
&restored_cache, 0u, boot_now, &output));
/* PRN0 was already 10 minutes old. It has 110 minutes, not a newly
* granted two hours, after reboot. */
uint64_t remaining_ticks = GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS -
600u * GPS_SYSTICK_HZ;
assert(gps_ephemeris_cache_restore(
&restored_cache, 0u, boot_now + remaining_ticks, &output));
assert(!gps_ephemeris_cache_restore(
&restored_cache, 0u, boot_now + remaining_ticks + 1u, &output));
}
static void test_issue_check_is_per_satellite_and_per_subframe(void)
{
gps_ephemeris_t ephemeris = make_ephemeris(3u, 2300, 90000.0, 0x155u);
unsigned int words[10] = {0};
words[2] = ((ephemeris.iodc >> 8) & 0x03u) << 6;
words[7] = (ephemeris.iodc & 0xffu) << 22;
assert(gps_assistance_ephemeris_issue_matches(
&ephemeris, 1u, words));
words[7] ^= UINT32_C(1) << 22;
assert(!gps_assistance_ephemeris_issue_matches(
&ephemeris, 1u, words));
memset(words, 0, sizeof(words));
words[2] = (uint32_t)ephemeris.iode << 22;
assert(gps_assistance_ephemeris_issue_matches(
&ephemeris, 2u, words));
words[2] += UINT32_C(1) << 22;
assert(!gps_assistance_ephemeris_issue_matches(
&ephemeris, 2u, words));
assert(gps_assistance_ephemeris_issue_matches(
&ephemeris, 4u, words));
}
static void test_iono_position_and_almanac_independent_staleness(void)
{
const gpstime_t saved_time = {.week = 2400, .sec = 200000.0};
gps_ephemeris_cache_t source;
gps_ephemeris_cache_init(&source, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_t almanac;
gps_almanac_cache_init(&almanac, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
almanac.reference_valid = true;
almanac.reference_week_mod256 = (unsigned int)saved_time.week & 0xffu;
almanac.reference_toa_sec = saved_time.sec - 86400.0;
almanac.reference_tick = 50u;
ionoutc_t ionoutc = {.enable = true, .vflg = true, .dtls = 18};
gps_assistance_position_t position = {
.ecef_m = {4200000.0, 2800000.0, 3900000.0},
.latitude_deg = 41.0,
.longitude_deg = 29.0,
.height_m = 100.0,
.gps_time = saved_time,
.valid = true
};
gps_assistance_payload_t payload;
gps_assistance_payload_build(
&payload, &source, &almanac, &ionoutc, &position, saved_time);
gps_ephemeris_cache_t output_eph;
gps_almanac_cache_t output_almanac;
gps_ephemeris_cache_init(
&output_eph, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&output_almanac, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_restore_result_t result;
gpstime_t current = saved_time;
current.sec += 2.0 * 86400.0;
assert(gps_assistance_restore(
&payload, current, 20u,
&output_eph, &output_almanac, &result));
assert(result.almanac_restored);
assert(result.ionoutc_restored);
assert(result.position_restored);
assert(result.ionoutc.dtls == 18);
/* Eight days rejects almanac and iono, while a coarse position remains a
* safe acquisition/WLS seed under the separate 30-day policy. */
current = saved_time;
current.week += 1;
current.sec += 86400.0;
if(current.sec >= 604800.0)
{
current.sec -= 604800.0;
current.week++;
}
gps_ephemeris_cache_init(
&output_eph, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&output_almanac, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
assert(gps_assistance_restore(
&payload, current, 20u,
&output_eph, &output_almanac, &result));
assert(!result.almanac_restored);
assert(!result.ionoutc_restored);
assert(result.position_restored);
}
int main(void)
{
test_fresh_mixed_ephemeris_restore_and_true_remaining_age();
test_issue_check_is_per_satellite_and_per_subframe();
test_iono_position_and_almanac_independent_staleness();
puts("gps assistance freshness/issue tests passed");
return 0;
}
@@ -0,0 +1,300 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "gps/gps_assistance_store.h"
#define FLASH_BASE GPS_ASSISTANCE_FLASH_REGION0
#define FLASH_BYTES UINT32_C(0x00080000)
static unsigned char flash_memory[FLASH_BYTES];
static unsigned int flash_program_count;
static unsigned int flash_erase_count;
static ionoutc_t live_ionoutc;
static bool live_ionoutc_valid;
static double seeded_ecef[3];
static bool position_seeded;
void bsp_spi_flash_init(void)
{
}
uint32_t bsp_spi_flash_read_id(void)
{
return UINT32_C(0x010219);
}
void bsp_spi_flash_read(uint32_t address, void* destination, uint32_t length)
{
assert(address >= FLASH_BASE);
uint32_t offset = address - FLASH_BASE;
assert(offset <= FLASH_BYTES);
assert(length <= FLASH_BYTES - offset);
memcpy(destination, &flash_memory[offset], length);
}
bool bsp_spi_flash_program_page(
uint32_t address, const void* source, uint32_t length)
{
assert(address >= FLASH_BASE);
uint32_t offset = address - FLASH_BASE;
assert(length > 0u && length <= 256u);
assert((address / 256u) == ((address + length - 1u) / 256u));
assert(offset <= FLASH_BYTES);
assert(length <= FLASH_BYTES - offset);
const unsigned char* bytes = source;
for(uint32_t i = 0u ; i < length ; i++)
{
flash_memory[offset + i] &= bytes[i];
}
flash_program_count++;
return true;
}
bool bsp_spi_flash_erase_sector(uint32_t address)
{
assert((address == GPS_ASSISTANCE_FLASH_REGION0) ||
(address == GPS_ASSISTANCE_FLASH_REGION1));
uint32_t offset = address - FLASH_BASE;
memset(&flash_memory[offset], 0xff, GPS_ASSISTANCE_FLASH_REGION_SIZE);
flash_erase_count++;
return true;
}
bool gps_get_ionoutc(ionoutc_t* ionoutc)
{
if((ionoutc == NULL) || !live_ionoutc_valid)
{
return false;
}
*ionoutc = live_ionoutc;
return true;
}
void gps_set_ionoutc(const ionoutc_t* ionoutc)
{
assert(ionoutc != NULL);
live_ionoutc = *ionoutc;
live_ionoutc_valid = true;
}
void gps_wls_seed_position(const double receiver_ecef_m[3])
{
memcpy(seeded_ecef, receiver_ecef_m, sizeof(seeded_ecef));
position_seeded = true;
}
static gps_ephemeris_t make_ephemeris(unsigned int prn, double toe_sec)
{
gps_ephemeris_t ephemeris;
memset(&ephemeris, 0, sizeof(ephemeris));
ephemeris.WN = 2300u & 0x3ffu;
ephemeris.toe.sec = toe_sec;
ephemeris.toc.sec = toe_sec;
ephemeris.sqrta = 5153.7;
ephemeris.A = ephemeris.sqrta * ephemeris.sqrta;
ephemeris.ecc = 0.01;
ephemeris.iodc = 55u;
ephemeris.iode = 55u;
ephemeris.SVID = (unsigned char)prn;
ephemeris.ephemeris_valid = true;
ephemeris.abs_sample = 999999.5;
return ephemeris;
}
static void make_sf1_words(unsigned int words[10], unsigned int full_week)
{
memset(words, 0, 10u * sizeof(words[0]));
words[2] = (full_week & 0x3ffu) << 20;
}
static void test_power_cycle_restore_then_live_issue_rejection(void)
{
memset(flash_memory, 0xff, sizeof(flash_memory));
flash_program_count = 0u;
flash_erase_count = 0u;
live_ionoutc = (ionoutc_t){
.enable = true,
.vflg = true,
.alpha0 = 1.0e-8,
.dtls = 18
};
live_ionoutc_valid = true;
position_seeded = false;
gps_ephemeris_cache_t ephemeris_cache;
gps_almanac_cache_t almanac_cache;
gps_ephemeris_cache_init(
&ephemeris_cache, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&almanac_cache, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_store_init(&ephemeris_cache, &almanac_cache, 0u);
unsigned int words[10];
make_sf1_words(words, 2300u);
gps_ephemeris_t ignored_ephemeris;
gps_assistance_position_t ignored_position;
gpstime_t current_time;
assert(!gps_assistance_store_observe_subframe(
0u, 1u, words, 100000u, 10u,
&ephemeris_cache, &almanac_cache,
&ignored_ephemeris, &ignored_position, &current_time));
assert(current_time.week == 2300);
gps_ephemeris_t ephemeris = make_ephemeris(0u, 99000.0);
assert(gps_ephemeris_cache_store(
&ephemeris_cache, 0u, &ephemeris, 10u));
gps_assistance_store_note_ephemeris_change(0u, 10u);
gps_nav_solution_t solution;
memset(&solution, 0, sizeof(solution));
solution.ecef_m[0] = 4200000.0;
solution.ecef_m[1] = 2800000.0;
solution.ecef_m[2] = 3900000.0;
solution.latitude_deg = 41.0;
solution.longitude_deg = 29.0;
solution.height_m = 100.0;
solution.solution_valid = true;
gps_assistance_store_note_solution(
&solution, current_time, 10u);
gps_assistance_store_note_ionoutc_change(10u);
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
10u + 60u * GPS_SYSTICK_HZ);
assert(flash_program_count > 0u);
assert(flash_erase_count == 1u);
/* Simulated power cycle clears RAM/cache and all invalid phase state. */
gps_ephemeris_cache_init(
&ephemeris_cache, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&almanac_cache, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
live_ionoutc_valid = false;
position_seeded = false;
gps_assistance_store_init(&ephemeris_cache, &almanac_cache, 0u);
memset(words, 0, sizeof(words));
gps_ephemeris_t restored_ephemeris;
gps_assistance_position_t restored_position;
assert(gps_assistance_store_observe_subframe(
0u, 4u, words, 100006u, 60u,
&ephemeris_cache, &almanac_cache,
&restored_ephemeris, &restored_position, &current_time));
assert(restored_ephemeris.iode == 55u);
assert(restored_ephemeris.abs_sample == 0.0);
assert(!ephemeris_cache.entry[0].time_anchor.valid);
assert(restored_position.valid);
assert(position_seeded);
assert(seeded_ecef[0] == solution.ecef_m[0]);
assert(live_ionoutc_valid && live_ionoutc.dtls == 18);
/* The first issue-bearing subframe is checked per satellite. */
memset(words, 0, sizeof(words));
words[2] = 56u << 22;
assert(!gps_assistance_store_observe_subframe(
0u, 2u, words, 100012u, 120u,
&ephemeris_cache, &almanac_cache,
&restored_ephemeris, &restored_position, &current_time));
assert(!ephemeris_cache.entry[0].valid);
}
static void test_three_hour_power_off_rejects_ephemeris(void)
{
gps_ephemeris_cache_t ephemeris_cache;
gps_almanac_cache_t almanac_cache;
gps_ephemeris_cache_init(
&ephemeris_cache, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&almanac_cache, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_store_init(&ephemeris_cache, &almanac_cache, 0u);
unsigned int words[10] = {0};
gps_ephemeris_t restored_ephemeris;
gps_assistance_position_t restored_position;
gpstime_t current_time;
assert(!gps_assistance_store_observe_subframe(
0u, 4u, words, 100000u + 3u * 3600u, 60u,
&ephemeris_cache, &almanac_cache,
&restored_ephemeris, &restored_position, &current_time));
assert(!ephemeris_cache.entry[0].valid);
}
static void test_almanac_flash_writes_are_event_driven(void)
{
memset(flash_memory, 0xff, sizeof(flash_memory));
flash_program_count = 0u;
flash_erase_count = 0u;
live_ionoutc_valid = false;
gps_ephemeris_cache_t ephemeris_cache;
gps_almanac_cache_t almanac_cache;
gps_ephemeris_cache_init(
&ephemeris_cache, GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS);
gps_almanac_cache_init(
&almanac_cache, GPS_ALMANAC_CACHE_MAX_AGE_TICKS);
gps_assistance_store_init(&ephemeris_cache, &almanac_cache, 0u);
unsigned int words[10];
make_sf1_words(words, 2300u);
gps_ephemeris_t ignored_ephemeris;
gps_assistance_position_t ignored_position;
gpstime_t current_time;
assert(!gps_assistance_store_observe_subframe(
0u, 1u, words, 100000u, 10u,
&ephemeris_cache, &almanac_cache,
&ignored_ephemeris, &ignored_position, &current_time));
/* Elapsed time by itself must never program or erase the flash. */
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
10u + 24u * 60u * 60u * GPS_SYSTICK_HZ);
assert(flash_program_count == 0u);
assert(flash_erase_count == 0u);
/* A real content event is debounced, then committed exactly once. */
almanac_cache.reference_valid = true;
almanac_cache.reference_week_mod256 =
(unsigned int)current_time.week & 0xffu;
almanac_cache.reference_toa_sec = 98304.0;
almanac_cache.reference_tick = 20u;
gps_assistance_store_note_almanac_change(20u);
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
20u + 60u * GPS_SYSTICK_HZ - 1u);
assert(flash_program_count == 0u);
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
20u + 60u * GPS_SYSTICK_HZ);
assert(flash_program_count > 0u);
assert(flash_erase_count == 1u);
unsigned int committed_program_count = flash_program_count;
unsigned int committed_erase_count = flash_erase_count;
/* The ingest layer normally suppresses this duplicate event. The journal
* CRC deduplication is a second guard against an identical flash write. */
uint64_t duplicate_tick = 100u * GPS_SYSTICK_HZ;
gps_assistance_store_note_almanac_change(duplicate_tick);
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
duplicate_tick + 60u * GPS_SYSTICK_HZ);
assert(flash_program_count == committed_program_count);
assert(flash_erase_count == committed_erase_count);
/* A new toa/WNa/orbit/health content set changes the serialized payload
* and is therefore persisted after the same quiet interval. */
almanac_cache.reference_toa_sec += 4096.0;
uint64_t changed_tick = 200u * GPS_SYSTICK_HZ;
gps_assistance_store_note_almanac_change(changed_tick);
gps_assistance_store_service(
&ephemeris_cache, &almanac_cache,
changed_tick + 60u * GPS_SYSTICK_HZ);
assert(flash_program_count > committed_program_count);
}
int main(void)
{
test_power_cycle_restore_then_live_issue_rejection();
test_three_hour_power_off_rejects_ephemeris();
test_almanac_flash_writes_are_event_driven();
puts("gps assistance power-cycle integration tests passed");
return 0;
}
@@ -72,10 +72,69 @@ static void test_validation_independence_and_invalidate(void)
&cache, 19u, 40u, &output));
}
static void test_repeated_issue_does_not_hide_staleness(void)
{
gps_ephemeris_cache_t cache;
gps_ephemeris_cache_init(&cache, 100u);
gps_ephemeris_t issue17 = make_ephemeris(6u, 17u);
gps_ephemeris_t output;
assert(gps_ephemeris_cache_store(
&cache, 6u, &issue17, 1000u));
assert(gps_ephemeris_cache_store(
&cache, 6u, &issue17, 1090u));
assert(!gps_ephemeris_cache_restore(
&cache, 6u, 1101u, &output));
/* A changed IOD is a fresh broadcast issue and receives a new TTL. */
issue17.iode = 18u;
issue17.iodc = 18u;
assert(gps_ephemeris_cache_store(
&cache, 6u, &issue17, 1200u));
assert(gps_ephemeris_cache_restore(
&cache, 6u, 1300u, &output));
assert(output.iode == 18u);
}
static void test_time_anchor_is_independent_and_short_lived(void)
{
gps_ephemeris_cache_t cache;
gps_ephemeris_cache_init(&cache, 1000u);
gps_ephemeris_t ephemeris = make_ephemeris(11u, 9u);
gps_cached_time_anchor_t anchor = {
.tow = {.week = 2430, .sec = 345678.0},
.raw_sample = 0xffff0000u,
.corrected_sample = 4294901759.75,
.week_valid = true,
.valid = true
};
gps_cached_time_anchor_t restored;
/* An anchor cannot outlive or exist without its ephemeris slot. */
assert(!gps_ephemeris_cache_store_time_anchor(
&cache, 11u, &anchor, 50u));
assert(gps_ephemeris_cache_store(
&cache, 11u, &ephemeris, 50u));
assert(gps_ephemeris_cache_store_time_anchor(
&cache, 11u, &anchor, 60u));
assert(gps_ephemeris_cache_restore_time_anchor(
&cache, 11u, 170u, 110u, &restored));
assert(restored.tow.week == 2430);
assert(restored.raw_sample == 0xffff0000u);
assert(!gps_ephemeris_cache_restore_time_anchor(
&cache, 11u, 171u, 110u, &restored));
/* The two-hour-class ephemeris remains usable after the short counter
* anchor expires; firmware then waits for one fresh HOW, not SF1/2/3. */
assert(gps_ephemeris_cache_restore(
&cache, 11u, 171u, &ephemeris));
}
int main(void)
{
test_store_restore_and_expiry();
test_validation_independence_and_invalidate();
test_repeated_issue_does_not_hide_staleness();
test_time_anchor_is_independent_and_short_lived();
puts("gps ephemeris cache tests passed");
return 0;
}
@@ -0,0 +1,259 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "gps/gps_flash_journal.h"
#define FAKE_FLASH_SIZE 8192u
#define REGION_SIZE 2048u
#define REGION0 0u
#define REGION1 4096u
typedef struct {
unsigned char bytes[FAKE_FLASH_SIZE];
unsigned int program_calls;
unsigned int erase_calls;
unsigned int fail_program_call;
unsigned int fail_program_after_bytes;
bool fail_erase;
} fake_flash_t;
static void fake_flash_reset(fake_flash_t* flash)
{
memset(flash, 0, sizeof(*flash));
memset(flash->bytes, 0xff, sizeof(flash->bytes));
}
static bool fake_read(
void* context, uint32_t address, void* destination, uint32_t length)
{
fake_flash_t* flash = context;
if((address > FAKE_FLASH_SIZE) ||
(length > (FAKE_FLASH_SIZE - address)))
{
return false;
}
memcpy(destination, &flash->bytes[address], length);
return true;
}
static bool fake_program(
void* context, uint32_t address, const void* source, uint32_t length)
{
fake_flash_t* flash = context;
flash->program_calls++;
if((length == 0u) || (length > GPS_FLASH_JOURNAL_PAGE_SIZE) ||
((address / GPS_FLASH_JOURNAL_PAGE_SIZE) !=
((address + length - 1u) / GPS_FLASH_JOURNAL_PAGE_SIZE)) ||
(address > FAKE_FLASH_SIZE) ||
(length > (FAKE_FLASH_SIZE - address)))
{
return false;
}
const unsigned char* data = source;
uint32_t programmed = length;
bool fail = flash->fail_program_call != 0u &&
flash->program_calls == flash->fail_program_call;
if(fail && (flash->fail_program_after_bytes < programmed))
{
programmed = flash->fail_program_after_bytes;
}
for(uint32_t i = 0u ; i < programmed ; i++)
{
/* NOR page programming can only clear bits. */
flash->bytes[address + i] &= data[i];
}
return !fail;
}
static bool fake_erase(void* context, uint32_t region_address)
{
fake_flash_t* flash = context;
flash->erase_calls++;
if(flash->fail_erase ||
((region_address != REGION0) && (region_address != REGION1)))
{
return false;
}
memset(&flash->bytes[region_address], 0xff, REGION_SIZE);
return true;
}
static gps_flash_journal_io_t fake_io(fake_flash_t* flash)
{
const gps_flash_journal_io_t io = {
.context = flash,
.read = fake_read,
.program = fake_program,
.erase_region = fake_erase
};
return io;
}
static void init_journal(
gps_flash_journal_t* journal,
fake_flash_t* flash,
uint32_t maximum_payload)
{
gps_flash_journal_io_t io = fake_io(flash);
assert(gps_flash_journal_init(
journal, &io, REGION0, REGION1, REGION_SIZE,
maximum_payload));
}
static void test_crc_vector(void)
{
static const char vector[] = "123456789";
assert(gps_flash_journal_crc32(vector, 9u) == UINT32_C(0xcbf43926));
}
static void test_empty_write_load_and_deduplicate(void)
{
fake_flash_t flash;
fake_flash_reset(&flash);
gps_flash_journal_t journal;
init_journal(&journal, &flash, 800u);
unsigned char output[800];
assert(!gps_flash_journal_load_latest(
&journal, output, sizeof(output), NULL, NULL));
unsigned char payload[700];
for(unsigned int i = 0u ; i < sizeof(payload) ; i++)
{
payload[i] = (unsigned char)(i * 17u);
}
assert(gps_flash_journal_write(
&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
unsigned int programs_after_commit = flash.program_calls;
assert(gps_flash_journal_write(
&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_UNCHANGED);
assert(flash.program_calls == programs_after_commit);
uint32_t size = 0u;
uint64_t sequence = 0u;
memset(output, 0, sizeof(output));
assert(gps_flash_journal_load_latest(
&journal, output, sizeof(output), &size, &sequence));
assert(size == sizeof(payload));
assert(sequence == 1u);
assert(memcmp(output, payload, sizeof(payload)) == 0);
/* A CPU reset reconstructs journal state only from flash. */
gps_flash_journal_t rebooted;
init_journal(&rebooted, &flash, 800u);
memset(output, 0, sizeof(output));
assert(gps_flash_journal_load_latest(
&rebooted, output, sizeof(output), &size, &sequence));
assert(sequence == 1u);
assert(memcmp(output, payload, sizeof(payload)) == 0);
}
static void test_rotation_keeps_latest_and_old_bank_until_commit(void)
{
fake_flash_t flash;
fake_flash_reset(&flash);
gps_flash_journal_t journal;
init_journal(&journal, &flash, 700u);
unsigned char payload[700];
for(unsigned int version = 1u ; version <= 4u ; version++)
{
memset(payload, (int)version, sizeof(payload));
assert(gps_flash_journal_write(
&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
}
assert(journal.latest_sequence == 4u);
assert(flash.erase_calls >= 2u);
gps_flash_journal_t rebooted;
init_journal(&rebooted, &flash, 700u);
unsigned char output[700];
uint64_t sequence = 0u;
assert(gps_flash_journal_load_latest(
&rebooted, output, sizeof(output), NULL, &sequence));
assert(sequence == 4u);
assert(output[0] == 4u && output[699] == 4u);
}
static void test_every_torn_program_keeps_previous_record(void)
{
/* Header, three payload pages, and final commit are separate calls. */
for(unsigned int failed_call_in_write = 1u ;
failed_call_in_write <= 5u ; failed_call_in_write++)
{
fake_flash_t flash;
fake_flash_reset(&flash);
gps_flash_journal_t journal;
init_journal(&journal, &flash, 700u);
unsigned char old_payload[700];
unsigned char new_payload[700];
memset(old_payload, 0x31, sizeof(old_payload));
memset(new_payload, 0x72, sizeof(new_payload));
assert(gps_flash_journal_write(
&journal, old_payload, sizeof(old_payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
flash.fail_program_call =
flash.program_calls + failed_call_in_write;
flash.fail_program_after_bytes = 1u;
assert(gps_flash_journal_write(
&journal, new_payload, sizeof(new_payload)) ==
GPS_FLASH_JOURNAL_WRITE_FAILED);
flash.fail_program_call = 0u;
gps_flash_journal_t rebooted;
init_journal(&rebooted, &flash, 700u);
unsigned char output[700];
uint64_t sequence = 0u;
assert(gps_flash_journal_load_latest(
&rebooted, output, sizeof(output), NULL, &sequence));
assert(sequence == 1u);
assert(memcmp(output, old_payload, sizeof(output)) == 0);
}
}
static void test_corrupt_latest_falls_back_to_other_bank(void)
{
fake_flash_t flash;
fake_flash_reset(&flash);
gps_flash_journal_t journal;
init_journal(&journal, &flash, 700u);
unsigned char payload[700];
memset(payload, 0x11, sizeof(payload));
assert(gps_flash_journal_write(&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
memset(payload, 0x22, sizeof(payload));
assert(gps_flash_journal_write(&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
memset(payload, 0x33, sizeof(payload));
assert(gps_flash_journal_write(&journal, payload, sizeof(payload)) ==
GPS_FLASH_JOURNAL_WRITE_COMMITTED);
/* Three 700-byte records force sequence 3 into the second bank. */
assert(journal.active_region == 1u);
flash.bytes[REGION1 + GPS_FLASH_JOURNAL_PAGE_SIZE + 100u] ^= 0x01u;
gps_flash_journal_t rebooted;
init_journal(&rebooted, &flash, 700u);
unsigned char output[700];
uint64_t sequence = 0u;
assert(gps_flash_journal_load_latest(
&rebooted, output, sizeof(output), NULL, &sequence));
assert(sequence == 2u);
assert(output[0] == 0x22u);
}
int main(void)
{
test_crc_vector();
test_empty_write_load_and_deduplicate();
test_rotation_keeps_latest_and_old_bank_until_commit();
test_every_torn_program_keeps_previous_record();
test_corrupt_latest_falls_back_to_other_bank();
puts("gps flash journal fault-injection tests passed");
return 0;
}
@@ -0,0 +1,82 @@
#include "gps/gps_measurement_freshness.h"
#include <assert.h>
#include <stdio.h>
#define SYSTICK_HZ 10u
#define PERIODIC_DEADLINE_TICKS (3u * SYSTICK_HZ)
#define LNAV_SUBFRAME_TICKS (6u * SYSTICK_HZ)
static void cold_lnav_cadence_never_uses_periodic_deadline(void)
{
gps_measurement_freshness_t freshness;
gps_measurement_freshness_init(&freshness);
/* Three cold-acquisition subframes span the full ephemeris collection
* interval. Every gap is longer than the periodic deadline. */
for(uint64_t subframe = 0u; subframe < 3u; subframe++)
{
uint64_t event_tick = subframe * LNAV_SUBFRAME_TICKS;
gps_measurement_freshness_note_lnav(
&freshness, true, event_tick);
assert(gps_measurement_freshness_usable(
&freshness,
event_tick + LNAV_SUBFRAME_TICKS - 1u,
PERIODIC_DEADLINE_TICKS));
assert(!gps_measurement_freshness_expired(
&freshness,
event_tick + LNAV_SUBFRAME_TICKS - 1u,
PERIODIC_DEADLINE_TICKS));
}
assert(freshness.phase == GPS_MEASUREMENT_PHASE_LNAV);
}
static void armed_and_active_measurements_use_periodic_deadline(void)
{
gps_measurement_freshness_t freshness;
gps_measurement_freshness_init(&freshness);
gps_measurement_freshness_note_lnav(&freshness, true, 120u);
gps_measurement_freshness_arm_nav_solve(&freshness, true, 121u);
assert(freshness.phase == GPS_MEASUREMENT_PHASE_NAV_SOLVE_ARMED);
assert(gps_measurement_freshness_usable(
&freshness, 151u, PERIODIC_DEADLINE_TICKS));
assert(gps_measurement_freshness_expired(
&freshness, 152u, PERIODIC_DEADLINE_TICKS));
/* A six-second LNAV event is still decoded in NAV-solve mode, but cannot
* hide the missing short-period measurement stream. */
gps_measurement_freshness_note_lnav(&freshness, true, 160u);
assert(gps_measurement_freshness_expired(
&freshness, 160u, PERIODIC_DEADLINE_TICKS));
gps_measurement_freshness_note_nav_solve(&freshness, true, 161u);
assert(freshness.phase == GPS_MEASUREMENT_PHASE_NAV_SOLVE_ACTIVE);
assert(gps_measurement_freshness_usable(
&freshness, 180u, PERIODIC_DEADLINE_TICKS));
assert(gps_measurement_freshness_expired(
&freshness, 192u, PERIODIC_DEADLINE_TICKS));
}
static void unlock_invalidates_every_phase(void)
{
gps_measurement_freshness_t freshness;
gps_measurement_freshness_init(&freshness);
gps_measurement_freshness_note_lnav(&freshness, true, 10u);
gps_measurement_freshness_note_lnav(&freshness, false, 11u);
assert(!gps_measurement_freshness_usable(
&freshness, 11u, PERIODIC_DEADLINE_TICKS));
gps_measurement_freshness_arm_nav_solve(&freshness, true, 20u);
gps_measurement_freshness_note_nav_solve(&freshness, false, 21u);
assert(!gps_measurement_freshness_usable(
&freshness, 21u, PERIODIC_DEADLINE_TICKS));
}
int main(void)
{
cold_lnav_cadence_never_uses_periodic_deadline();
armed_and_active_measurements_use_periodic_deadline();
unlock_invalidates_every_phase();
puts("gps measurement freshness tests passed");
return 0;
}
@@ -0,0 +1,45 @@
#include <assert.h>
#include <stdint.h>
#include "gps/gps_nav_phase.h"
static uint32_t tracking_control_without_nav_solve_irq(void)
{
return (1u << GPS_TRCK_INTR_EN_POS) |
(1u << GPS_TRCK_NAV_BITS_INTR_EN_POS) |
(1u << GPS_TRCK_NAV_BITS_FLAG_EN_POS) |
(1u << GPS_TRCK_STOP_UNLOCK_EN_POS) |
(1u << GPS_TRCK_UNLOCK_INTR_EN_POS) |
(1u << GPS_TRCK_UNLOCK_FLAG_EN_POS) |
(1u << GPS_TRCK_NAV_SOLVE_EN_POS) |
(1u << GPS_TRCK_MEASUREMENT_INTR_EN_POS) |
(1u << GPS_TRCK_MEASUREMENT_FLAG_EN_POS);
}
int main(void)
{
uint32_t cold_control = tracking_control_without_nav_solve_irq();
assert(!gps_nav_phase_control_armed(cold_control));
assert(!gps_nav_phase_preserve_seed_after_how(true, cold_control));
assert(!gps_nav_phase_preserve_seed_after_how(false, cold_control));
uint32_t flag_only =
cold_control | (1u << GPS_TRCK_NAV_SOLVE_FLAG_EN_POS);
assert(!gps_nav_phase_control_armed(flag_only));
assert(!gps_nav_phase_preserve_seed_after_how(true, flag_only));
uint32_t irq_only =
cold_control | (1u << GPS_TRCK_NAV_SOLVE_INTR_EN_POS);
assert(!gps_nav_phase_control_armed(irq_only));
assert(!gps_nav_phase_preserve_seed_after_how(true, irq_only));
uint32_t armed_control =
cold_control |
(1u << GPS_TRCK_NAV_SOLVE_INTR_EN_POS) |
(1u << GPS_TRCK_NAV_SOLVE_FLAG_EN_POS);
assert(gps_nav_phase_control_armed(armed_control));
assert(gps_nav_phase_preserve_seed_after_how(true, armed_control));
assert(!gps_nav_phase_preserve_seed_after_how(false, armed_control));
return 0;
}
@@ -91,14 +91,11 @@ static void profile_coefficients(
int64_t* ki_q46,
int64_t* max_slew_q32)
{
const double pi = 3.14159265358979323846;
double wn = 2.0 * pi * profile.natural_frequency_hz;
*kp_q46 = (int64_t)llround(
(2.0 * profile.damping * wn / TRACKING_HZ) * Q46_SCALE);
*ki_q46 = (int64_t)llround(
(wn * wn * UPDATE_PERIOD_SEC / TRACKING_HZ) * Q46_SCALE);
*max_slew_q32 = (int64_t)llround(
profile.max_slew_ppm * 1.0e-6 * Q32_SCALE);
uint32_t max_slew = 0u;
assert(gps_pps_loop_coefficients(
profile, UPDATE_PERIOD_SEC, TRACKING_HZ,
kp_q46, ki_q46, &max_slew));
*max_slew_q32 = max_slew;
}
static void synthetic_rtl_coarse(synthetic_rtl_t* rtl, int64_t reference_q32)
@@ -107,6 +107,69 @@ static void test_profile_parameters_are_ordered(void)
assert(robust.lock_release_ticks >= precision.lock_release_ticks);
}
static void test_period_scaled_hysteresis_and_settling(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init_period(&selector, 0.5);
assert(selector.upgrade_updates == 4u);
assert(selector.degrade_updates == 2u);
gps_pps_loop_profile_t precision =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_PRECISION, 0.5);
gps_pps_loop_profile_t robust =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_ROBUST, 0.5);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 5u);
assert(robust.lock_acquire_updates == 4u);
assert(robust.software_settle_updates == 7u);
gps_pps_quality_selector_init_period(&selector, 1.0);
assert(selector.upgrade_updates == 2u);
assert(selector.degrade_updates == 2u);
precision = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_PRECISION, 1.0);
robust = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_ROBUST, 1.0);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 3u);
assert(robust.lock_acquire_updates == 2u);
assert(robust.software_settle_updates == 4u);
}
static void assert_coefficients(
gps_pps_profile_t profile,
double period_sec,
int64_t expected_kp,
int64_t expected_ki)
{
int64_t kp = 0;
int64_t ki = 0;
uint32_t max_slew = 0u;
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, period_sec);
assert(gps_pps_loop_coefficients(
parameters, period_sec, 125000000.0,
&kp, &ki, &max_slew));
assert(kp == expected_kp);
assert(ki == expected_ki);
assert(max_slew == 85899u);
}
static void test_period_scaled_discrete_coefficients(void)
{
/* The production 200 ms coefficients remain bit-for-bit unchanged. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.2, 764018, 64006);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.2, 222838, 5445);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.2, 84891, 640);
/* Other cadences exponentiate those proven discrete poles in time. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.5, 615780, 127492);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.5, 209783, 12804);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.5, 82980, 1564);
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 1.0, 447388, 178038);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 1.0, 190239, 23149);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 1.0, 79923, 3011);
}
static void test_quality_boundaries_and_recovery(void)
{
gps_pps_quality_selector_t selector;
@@ -167,6 +230,8 @@ int main(void)
test_hysteretic_profile_transitions();
test_alternating_quality_does_not_upgrade();
test_profile_parameters_are_ordered();
test_period_scaled_hysteresis_and_settling();
test_period_scaled_discrete_coefficients();
test_quality_boundaries_and_recovery();
puts("gps_pps_quality tests passed");
return 0;
@@ -6,6 +6,30 @@
#define COUNTER_HZ 125000000.0
static void test_elapsed_time_gain_preserves_filter_poles(void)
{
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double filtered = 0.0;
double innovation = 0.0;
assert(gps_pps_time_filter_update(
&filter, 0u, COUNTER_HZ, 0.0, true, 0.0, 1.0,
1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_COARSE);
assert(gps_pps_time_filter_update(
&filter, (uint64_t)COUNTER_HZ, COUNTER_HZ, 100.0, true, 100.0,
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_TRACKING);
/* One 1 s update has exactly the pole of five baseline 200 ms updates. */
double expected_bias_gain = 1.0 - pow(1.0 - 1.0 / 32.0, 5.0);
double expected_drift_gain = 1.0 - pow(1.0 - 1.0 / 4.0, 5.0);
assert(fabs(filtered - 100.0 * expected_bias_gain) < 1.0e-12);
assert(fabs(filter.clock_drift_mps - 100.0 * expected_drift_gain) <
1.0e-12);
}
static void test_drift_prediction_rejects_bias_noise(void)
{
static const double noise_m[] = {
@@ -298,6 +322,7 @@ static void test_reference_selection_is_stable(void)
int main(void)
{
test_elapsed_time_gain_preserves_filter_poles();
test_drift_prediction_rejects_bias_noise();
test_quality_gap_and_outlier_handling();
test_bias_ramp_without_doppler_uses_bounded_fallback();
@@ -4,19 +4,31 @@
#include <string.h>
#include "gps/gps_acq_manager.h"
#include "gps/gps_almanac.h"
#include "gps/gps_ephemeris_cache.h"
#define SIM_SEARCH_TICKS 5u
#define SIM_UNLOCK_TICKS 10u
#define SIM_NAV_SUBFRAME_TICKS 6u
#define SIM_NAV_PHASE_SEED_TICKS 1u
#define SIM_EPHEMERIS_DECODE_TICKS 18u
#define SIM_ANCHOR_MAX_AGE_TICKS 110u
/* PVT-ready means an orbit/HOW anchor has also seeded the periodic timer. */
#define SIM_COLD_NAV_READY_TICKS \
(SIM_EPHEMERIS_DECODE_TICKS + SIM_NAV_PHASE_SEED_TICKS)
#define SIM_REPARSED_NAV_READY_TICKS \
(SIM_NAV_SUBFRAME_TICKS + SIM_NAV_PHASE_SEED_TICKS)
typedef struct {
gps_acq_manager_t manager;
gps_almanac_cache_t almanac;
gps_ephemeris_cache_t cache;
bool visible[GPS_ACQ_MANAGER_PRN_COUNT];
bool tracked[GPS_ACQ_MANAGER_PRN_COUNT];
bool hardware_busy[GPS_ACQ_MANAGER_PRN_COUNT];
bool measurement_alive[GPS_ACQ_MANAGER_PRN_COUNT];
uint64_t invisible_since[GPS_ACQ_MANAGER_PRN_COUNT];
uint64_t nav_ready_tick[GPS_ACQ_MANAGER_PRN_COUNT];
unsigned int not_busy_polls[GPS_ACQ_MANAGER_PRN_COUNT];
bool engine_busy;
bool drop_completion_irq;
@@ -27,6 +39,8 @@ typedef struct {
unsigned int polled_completions;
unsigned int polled_losses;
unsigned int cache_restores;
unsigned int ephemeris_decodes;
unsigned int search_attempts[GPS_ACQ_MANAGER_PRN_COUNT];
} receiver_sim_t;
static unsigned int tracked_count(const receiver_sim_t* sim)
@@ -41,16 +55,35 @@ static unsigned int tracked_count(const receiver_sim_t* sim)
return count;
}
static void sim_init(receiver_sim_t* sim)
static unsigned int nav_ready_count(const receiver_sim_t* sim)
{
unsigned int count = 0u;
for(unsigned int prn = 0u;
prn < GPS_ACQ_MANAGER_PRN_COUNT;
prn++)
{
count += sim->tracked[prn] &&
(sim->nav_ready_tick[prn] != 0u) &&
(sim->tick >= sim->nav_ready_tick[prn]);
}
return count;
}
static void sim_init_with_visibility_probe(
receiver_sim_t* sim,
uint64_t visibility_probe_ticks)
{
memset(sim, 0, sizeof(*sim));
const gps_acq_manager_config_t config = {
.retry_delay_ticks = 1u,
.hot_retry_delay_ticks = 30u,
.hot_window_ticks = 7200u,
.unknown_probe_interval_ticks = 2u,
.visibility_probe_interval_ticks = visibility_probe_ticks,
.max_hot_streak = 4u
};
gps_acq_manager_init(&sim->manager, &config);
gps_almanac_cache_init(&sim->almanac, 7200u);
gps_ephemeris_cache_init(&sim->cache, 7200u);
for(unsigned int prn = 0u;
prn < GPS_ACQ_MANAGER_PRN_COUNT;
@@ -62,6 +95,11 @@ static void sim_init(receiver_sim_t* sim)
sim->engine_prn = GPS_ACQ_MANAGER_INVALID_PRN;
}
static void sim_init(receiver_sim_t* sim)
{
sim_init_with_visibility_probe(sim, 60u);
}
static void cache_ephemeris(receiver_sim_t* sim, unsigned int prn)
{
gps_ephemeris_t ephemeris;
@@ -71,6 +109,16 @@ static void cache_ephemeris(receiver_sim_t* sim, unsigned int prn)
ephemeris.ephemeris_valid = true;
assert(gps_ephemeris_cache_store(
&sim->cache, prn, &ephemeris, sim->tick));
gps_cached_time_anchor_t anchor = {
.tow = {.week = 2430, .sec = 100000.0 + (double)sim->tick},
.raw_sample = (uint32_t)(sim->tick * 16367981u),
.corrected_sample = (double)(uint32_t)(sim->tick * 16367981u),
.week_valid = true,
.valid = true
};
assert(gps_ephemeris_cache_store_time_anchor(
&sim->cache, prn, &anchor, sim->tick));
sim->ephemeris_decodes++;
}
static void sim_start_engine(receiver_sim_t* sim)
@@ -86,6 +134,7 @@ static void sim_start_engine(receiver_sim_t* sim)
sim->engine_busy = true;
sim->engine_prn = prn;
sim->engine_start = sim->tick;
sim->search_attempts[prn]++;
}
}
@@ -105,6 +154,7 @@ static void sim_complete_engine(receiver_sim_t* sim)
gps_acq_manager_mark_tracking(&sim->manager, prn);
sim->tracked[prn] = true;
sim->hardware_busy[prn] = true;
sim->measurement_alive[prn] = true;
sim->not_busy_polls[prn] = 0u;
sim->invisible_since[prn] = 0u;
gps_ephemeris_t restored;
@@ -112,10 +162,19 @@ static void sim_complete_engine(receiver_sim_t* sim)
&sim->cache, prn, sim->tick, &restored))
{
sim->cache_restores++;
gps_cached_time_anchor_t anchor;
bool direct_seed = gps_ephemeris_cache_restore_time_anchor(
&sim->cache, prn, sim->tick,
SIM_ANCHOR_MAX_AGE_TICKS, &anchor);
sim->nav_ready_tick[prn] = sim->tick +
(direct_seed ? SIM_NAV_PHASE_SEED_TICKS :
SIM_REPARSED_NAV_READY_TICKS);
}
else
{
cache_ephemeris(sim, prn);
sim->nav_ready_tick[prn] =
sim->tick + SIM_COLD_NAV_READY_TICKS;
}
}
else
@@ -128,6 +187,8 @@ static void sim_complete_engine(receiver_sim_t* sim)
static void sim_mark_tracking_lost(receiver_sim_t* sim, unsigned int prn)
{
sim->tracked[prn] = false;
sim->measurement_alive[prn] = false;
sim->nav_ready_tick[prn] = 0u;
sim->not_busy_polls[prn] = 0u;
gps_acq_manager_mark_lost(
&sim->manager, prn, sim->tick);
@@ -155,6 +216,7 @@ static void sim_update_tracking(receiver_sim_t* sim)
SIM_UNLOCK_TICKS)
{
sim->hardware_busy[prn] = false;
sim->measurement_alive[prn] = false;
if(!sim->drop_loss_irq)
{
sim_mark_tracking_lost(sim, prn);
@@ -177,6 +239,29 @@ static void sim_update_tracking(receiver_sim_t* sim)
}
}
static unsigned int manager_count_state(
const receiver_sim_t* sim,
gps_acq_prn_state_t state)
{
return gps_acq_manager_count_state(&sim->manager, state);
}
static void sim_reconcile_stale_locked_measurements(receiver_sim_t* sim)
{
for(unsigned int prn = 0u;
prn < GPS_ACQ_MANAGER_PRN_COUNT;
prn++)
{
if((gps_acq_manager_state(&sim->manager, prn) ==
GPS_ACQ_PRN_STALE) &&
sim->hardware_busy[prn] &&
sim->measurement_alive[prn])
{
gps_acq_manager_mark_tracking(&sim->manager, prn);
}
}
}
static void sim_step(receiver_sim_t* sim)
{
sim->tick++;
@@ -249,9 +334,14 @@ static void test_power_on_without_signal_then_visibility(void)
assert(sim.tracked[29]);
assert((sim.tick - signal_return_tick) <=
(GPS_ACQ_MANAGER_PRN_COUNT * SIM_SEARCH_TICKS));
assert(sim.cache_restores == 0u);
assert(sim.ephemeris_decodes == 1u);
assert(nav_ready_count(&sim) == 0u);
sim_run(&sim, SIM_COLD_NAV_READY_TICKS);
assert(nav_ready_count(&sim) == 1u);
}
static void test_long_outage_hot_reacquisition_and_cached_nav(void)
static void test_counter_ambiguous_outage_uses_cached_ephemeris_and_new_how(void)
{
receiver_sim_t sim;
sim_init(&sim);
@@ -264,7 +354,14 @@ static void test_long_outage_hot_reacquisition_and_cached_nav(void)
{
sim_step(&sim);
}
unsigned int cold_fix_tick = (unsigned int)sim.tick;
uint64_t cold_tracking_tick = sim.tick;
while(nav_ready_count(&sim) < 4u)
{
sim_step(&sim);
}
uint64_t cold_nav_wait = sim.tick - cold_tracking_tick;
assert(cold_nav_wait == SIM_COLD_NAV_READY_TICKS);
assert(sim.ephemeris_decodes == 4u);
memset(sim.visible, 0, sizeof(sim.visible));
sim_run(&sim, SIM_UNLOCK_TICKS + 120u);
@@ -283,10 +380,137 @@ static void test_long_outage_hot_reacquisition_and_cached_nav(void)
assert(tracked_count(&sim) == 4u);
assert((sim.tick - return_tick) <= 45u);
assert(sim.cache_restores >= 4u);
/* Cached ephemeris leaves only the next valid LNAV subframe/HOW anchor
* before the channel can contribute to a coherent WLS epoch. */
assert((sim.tick + SIM_NAV_SUBFRAME_TICKS - return_tick) <= 51u);
assert(cold_fix_tick <= 21u);
uint64_t reacquired_tracking_tick = sim.tick;
/* The RF counter is deliberately ambiguous after this >110 tick gap.
* Ephemeris remains cached, but timing safely waits for one new HOW. */
while(nav_ready_count(&sim) < 4u)
{
sim_step(&sim);
}
uint64_t cached_nav_wait = sim.tick - reacquired_tracking_tick;
assert(cached_nav_wait == SIM_REPARSED_NAV_READY_TICKS);
assert(sim.ephemeris_decodes == 4u);
assert(cold_nav_wait > cached_nav_wait);
printf("receiver reacquisition telemetry: cold_nav_wait=%lu cached_nav_wait=%lu saved=%lu cache_restores=%u reparses=%u\n",
(unsigned long)cold_nav_wait,
(unsigned long)cached_nav_wait,
(unsigned long)(cold_nav_wait - cached_nav_wait),
sim.cache_restores,
sim.ephemeris_decodes - 4u);
}
static void test_30_second_tunnel_phase_seeds_cached_nav(void)
{
receiver_sim_t sim;
sim_init(&sim);
for(unsigned int prn = 0u ; prn < 4u ; prn++)
{
sim.visible[prn] = true;
}
sim_start_engine(&sim);
while(nav_ready_count(&sim) < 4u)
{
sim_step(&sim);
}
unsigned int decoded = sim.ephemeris_decodes;
memset(sim.visible, 0, sizeof(sim.visible));
sim_run(&sim, SIM_UNLOCK_TICKS + 30u);
assert(tracked_count(&sim) == 0u);
for(unsigned int prn = 0u ; prn < 4u ; prn++)
{
sim.visible[prn] = true;
}
while(tracked_count(&sim) < 4u)
{
sim_step(&sim);
}
uint64_t all_tracked_tick = sim.tick;
while(nav_ready_count(&sim) < 4u)
{
sim_step(&sim);
}
assert((sim.tick - all_tracked_tick) == SIM_NAV_PHASE_SEED_TICKS);
assert(sim.ephemeris_decodes == decoded);
assert(sim.cache_restores >= 4u);
}
static void test_partial_loss_short_and_15_second_gaps(void)
{
receiver_sim_t sim;
sim_init(&sim);
for(unsigned int prn = 0u ; prn < 7u ; prn++)
{
sim.visible[prn] = true;
}
sim_start_engine(&sim);
while(nav_ready_count(&sim) < 7u)
{
sim_step(&sim);
}
/* Two seconds is below the hardware unlock qualification: no reacquisition
* and no navigation outage are introduced. */
for(unsigned int prn = 0u ; prn < 6u ; prn++)
{
sim.visible[prn] = false;
}
sim_run(&sim, 2u);
for(unsigned int prn = 0u ; prn < 6u ; prn++)
{
sim.visible[prn] = true;
}
sim_run(&sim, 2u);
assert(tracked_count(&sim) == 7u);
assert(nav_ready_count(&sim) == 7u);
/* A 15 second blockage drops six channels. The continuously tracked PRN
* remains valid and every returned channel uses the cached phase path. */
for(unsigned int prn = 0u ; prn < 6u ; prn++)
{
sim.visible[prn] = false;
}
sim_run(&sim, SIM_UNLOCK_TICKS + 15u);
assert(tracked_count(&sim) == 1u);
for(unsigned int prn = 0u ; prn < 6u ; prn++)
{
sim.visible[prn] = true;
}
while(tracked_count(&sim) < 7u)
{
sim_step(&sim);
}
uint64_t all_tracked_tick = sim.tick;
while(nav_ready_count(&sim) < 7u)
{
sim_step(&sim);
}
assert((sim.tick - all_tracked_tick) == SIM_NAV_PHASE_SEED_TICKS);
assert(sim.ephemeris_decodes == 7u);
}
static void test_stale_cache_forces_ephemeris_decode(void)
{
receiver_sim_t sim;
sim_init(&sim);
cache_ephemeris(&sim, 3u);
assert(sim.ephemeris_decodes == 1u);
sim.tick = sim.cache.max_age_ticks + 1u;
gps_ephemeris_t stale;
assert(!gps_ephemeris_cache_restore(
&sim.cache, 3u, sim.tick, &stale));
/* A cold completion after expiry decodes and stores a new set instead of
* silently using the stale RAM entry. */
sim.visible[3] = true;
sim.engine_busy = true;
sim.engine_prn = 3u;
sim_complete_engine(&sim);
assert(sim.tracked[3]);
assert(sim.cache_restores == 0u);
assert(sim.ephemeris_decodes == 2u);
assert(sim.nav_ready_tick[3] ==
sim.tick + SIM_COLD_NAV_READY_TICKS);
}
static void test_hot_failures_do_not_starve_cold_search(void)
@@ -296,6 +520,8 @@ static void test_hot_failures_do_not_starve_cold_search(void)
.retry_delay_ticks = 1u,
.hot_retry_delay_ticks = 1u,
.hot_window_ticks = 100u,
.unknown_probe_interval_ticks = 2u,
.visibility_probe_interval_ticks = 60u,
.max_hot_streak = 4u
};
gps_acq_manager_init(&manager, &config);
@@ -322,6 +548,136 @@ static void test_hot_failures_do_not_starve_cold_search(void)
assert(cold_seen);
}
static void test_incomplete_almanac_keeps_full_round_robin(void)
{
receiver_sim_t sim;
sim_init(&sim);
for(unsigned int prn = 0u ; prn < 5u ; prn++)
{
sim.visible[prn] = true;
}
sim_start_engine(&sim);
while((tracked_count(&sim) < 5u) && (sim.tick < 60u))
{
sim_step(&sim);
}
assert(tracked_count(&sim) == 5u);
/* Position plus five ephemerides is still not a constellation almanac.
* Even deliberately restrictive visibility hints must remain inactive. */
for(unsigned int prn = 0u ; prn < 5u ; prn++)
{
assert(gps_acq_manager_set_visibility(
&sim.manager, prn, true, true, 500 - (int)prn));
}
assert(gps_acq_manager_set_visibility(
&sim.manager, 5u, true, true, 600));
assert(gps_acq_manager_set_visibility(
&sim.manager, 10u, true, false, -100));
for(unsigned int prn = 6u ; prn <= 9u ; prn++)
{
sim.visible[prn] = true;
}
const gpstime_t gps_time = {.week = 2300, .sec = 164000.0};
assert(!gps_almanac_cache_complete(
&sim.almanac, sim.tick, gps_time));
gps_acq_manager_set_visibility_mode(
&sim.manager, false, sim.tick);
uint64_t model_start_tick = sim.tick;
while((tracked_count(&sim) < 9u) &&
((sim.tick - model_start_tick) < 900u))
{
sim_step(&sim);
}
assert(tracked_count(&sim) == 9u);
assert((sim.tick - model_start_tick) < 60u);
for(unsigned int prn = 6u ; prn <= 9u ; prn++)
{
assert(sim.search_attempts[prn] != 0u);
}
/* PRN 10 was hinted below-mask, but the partial almanac is not authorized
* to suppress it: the old round robin advances to it after PRN 9. */
assert(sim.search_attempts[10u] != 0u);
printf("incomplete-almanac round-robin telemetry: initial=5 final=%u ticks=%lu absent_prn_attempts=%u\n",
tracked_count(&sim),
(unsigned long)(sim.tick - model_start_tick),
sim.search_attempts[5u]);
}
static void test_wrong_sky_model_safety_probe_finds_real_satellites(void)
{
receiver_sim_t sim;
sim_init_with_visibility_probe(&sim, 5u);
for(unsigned int prn = 0u;
prn < GPS_ACQ_MANAGER_PRN_COUNT;
prn++)
{
assert(gps_acq_manager_set_visibility(
&sim.manager, prn, true, false, -900));
}
for(unsigned int prn = 0u ; prn < 4u ; prn++)
{
assert(gps_acq_manager_set_visibility(
&sim.manager, prn, true, true, 500 - (int)prn));
}
for(unsigned int prn = 10u ; prn < 14u ; prn++)
{
sim.visible[prn] = true;
}
gps_acq_manager_set_visibility_mode(&sim.manager, true, sim.tick);
sim_start_engine(&sim);
uint64_t start_tick = sim.tick;
while((tracked_count(&sim) < 4u) &&
((sim.tick - start_tick) < 120u))
{
sim_step(&sim);
}
assert(tracked_count(&sim) == 4u);
for(unsigned int prn = 10u ; prn < 14u ; prn++)
{
assert(sim.tracked[prn]);
assert(sim.search_attempts[prn] != 0u);
}
printf("wrong-sky-model recovery telemetry: ticks=%lu attempts=[%u,%u,%u,%u]\n",
(unsigned long)(sim.tick - start_tick),
sim.search_attempts[10u], sim.search_attempts[11u],
sim.search_attempts[12u], sim.search_attempts[13u]);
}
static void test_stale_locked_measurements_recover_tracking_state(void)
{
receiver_sim_t sim;
sim_init(&sim);
for(unsigned int prn = 0u ; prn < GPS_TRCK_IP_COUNT ; prn++)
{
sim.visible[prn] = true;
}
sim_start_engine(&sim);
while(tracked_count(&sim) < GPS_TRCK_IP_COUNT)
{
sim_step(&sim);
}
gps_acq_manager_mark_stale(&sim.manager, 7u);
gps_acq_manager_mark_stale(&sim.manager, 8u);
assert(manager_count_state(&sim, GPS_ACQ_PRN_TRACKING) ==
GPS_TRCK_IP_COUNT - 2u);
assert(manager_count_state(&sim, GPS_ACQ_PRN_STALE) == 2u);
sim_reconcile_stale_locked_measurements(&sim);
assert(manager_count_state(&sim, GPS_ACQ_PRN_TRACKING) ==
GPS_TRCK_IP_COUNT);
assert(manager_count_state(&sim, GPS_ACQ_PRN_STALE) == 0u);
gps_acq_manager_mark_stale(&sim.manager, 8u);
sim.measurement_alive[8u] = false;
sim_reconcile_stale_locked_measurements(&sim);
assert(gps_acq_manager_state(&sim.manager, 8u) ==
GPS_ACQ_PRN_STALE);
}
static void test_missed_completion_interrupt_is_polled(void)
{
receiver_sim_t sim;
@@ -371,10 +727,16 @@ int main(void)
{
test_cold_boot_and_short_signal_gap();
test_power_on_without_signal_then_visibility();
test_long_outage_hot_reacquisition_and_cached_nav();
test_counter_ambiguous_outage_uses_cached_ephemeris_and_new_how();
test_30_second_tunnel_phase_seeds_cached_nav();
test_partial_loss_short_and_15_second_gaps();
test_hot_failures_do_not_starve_cold_search();
test_incomplete_almanac_keeps_full_round_robin();
test_wrong_sky_model_safety_probe_finds_real_satellites();
test_stale_locked_measurements_recover_tracking_state();
test_missed_completion_interrupt_is_polled();
test_missed_tracking_loss_interrupt_is_reconciled();
test_stale_cache_forces_ephemeris_decode();
puts("gps receiver hardware-simulation scenarios passed");
return 0;
}
@@ -77,6 +77,92 @@ static void test_broadcast_ephemeris_velocity(void)
assert(sqrt(speed_squared) < 5000.0);
}
static void test_cold_wls_accepts_differential_timer_datum(void)
{
const unsigned int satellite_count = 6u;
/* Keep this convergence test above the atmospheric-model ceiling; the
* timer datum and cold WLS state are the variables under test here. */
const double receiver[3] = {6398137.0, 0.0, 0.0};
const double range_m = 21000000.0;
const double directions[6][3] = {
{ 1.0, 0.0, 0.0},
{ 0.2, 0.9, 0.3872983346207417},
{ 0.3, -0.4, 0.8660254037844386},
{-0.2, 0.5, 0.8426149773176358},
{ 0.4, -0.8, -0.4472135954999579},
{-0.5, -0.5, 0.7071067811865475}
};
mat_type* satellite_positions =
create_matrix((int)satellite_count, 3, "coldDifferentialSatPos");
mat_type* observations =
create_matrix(1, (int)satellite_count, "coldDifferentialObs");
mat_type* az = create_matrix(1, (int)satellite_count, "coldDifferentialAz");
mat_type* el = create_matrix(1, (int)satellite_count, "coldDifferentialEl");
assert(satellite_positions != NULL);
assert(observations != NULL);
assert(az != NULL);
assert(el != NULL);
initialize_all(0.0, satellite_positions);
initialize_all(0.0, observations);
initialize_all(0.0, az);
initialize_all(0.0, el);
double modeled_range_m[6];
for(unsigned int satellite = 0u;
satellite < satellite_count;
satellite++)
{
for(unsigned int component = 0u; component < 3u; component++)
{
satellite_positions->matrix[satellite][component] =
receiver[component] +
directions[satellite][component] * range_m;
}
double earth_angle = OMEGA_EARTH * range_m / SPEED_OF_LIGHT;
double rotated[3] = {
cos(earth_angle) * satellite_positions->matrix[satellite][0] +
sin(earth_angle) * satellite_positions->matrix[satellite][1],
-sin(earth_angle) * satellite_positions->matrix[satellite][0] +
cos(earth_angle) * satellite_positions->matrix[satellite][1],
satellite_positions->matrix[satellite][2]
};
modeled_range_m[satellite] = sqrt(
(rotated[0] - receiver[0]) * (rotated[0] - receiver[0]) +
(rotated[1] - receiver[1]) * (rotated[1] - receiver[1]) +
(rotated[2] - receiver[2]) * (rotated[2] - receiver[2]));
}
for(unsigned int satellite = 0u;
satellite < satellite_count;
satellite++)
{
observations->matrix[0][satellite] =
modeled_range_m[satellite] - modeled_range_m[0];
}
unsigned char ura[6] = {0u, 0u, 0u, 0u, 0u, 0u};
double cn0_dbhz[6] = {45.0, 45.0, 45.0, 45.0, 45.0, 45.0};
gps_nav_solution_t solution;
assert(leastSquaresSTD(
satellite_positions, observations, (int)satellite_count,
az, el, ura, cn0_dbhz, NULL, 345700.0, &solution));
double error_m = sqrt(
(solution.ecef_m[0] - receiver[0]) *
(solution.ecef_m[0] - receiver[0]) +
(solution.ecef_m[1] - receiver[1]) *
(solution.ecef_m[1] - receiver[1]) +
(solution.ecef_m[2] - receiver[2]) *
(solution.ecef_m[2] - receiver[2]));
assert(solution.solution_valid);
assert(error_m < 10.0);
assert(solution.clock_bias_m < -20000000.0);
assert(solution.clock_bias_m > -22000000.0);
free_matrix(satellite_positions);
free_matrix(observations);
free_matrix(az);
free_matrix(el);
}
static void test_cn0_changes_position_weight(void)
{
const unsigned int satellite_count = 6u;
@@ -143,6 +229,7 @@ static void test_cn0_changes_position_weight(void)
int main(void)
{
test_broadcast_ephemeris_velocity();
test_cold_wls_accepts_differential_timer_datum();
test_cn0_changes_position_weight();
puts("test_gps_satellite_velocity PASS");
return 0;
@@ -183,6 +183,134 @@ static void fractional_code_phase_corrects_capture_counter(void)
"capture correction beyond llround range was accepted");
}
static void free_counter_forms_differential_pseudorange(void)
{
const double counter_hz = 125000000.0;
const uint64_t reference_counter = UINT64_C(1) << 60;
const uint64_t event_counter =
reference_counter + UINT64_C(25625000); /* 205 ms */
double observation_sec = 0.0;
/* The transmit epoch crosses the GPS week by 200 ms while the receiver
* timer advances by 205 ms. The observation is therefore exactly 5 ms,
* independent of the timer's arbitrary, very large origin. */
CHECK(gps_time_differential_observation(
2300, 604799.9, reference_counter,
2301, 0.1, event_counter,
counter_hz, &observation_sec),
"week-crossing timer observation was rejected");
CHECK(fabs(observation_sec - 0.005) < 1.0e-10,
"receiver/transmit timer difference produced the wrong pseudorange");
const uint64_t origin_shift = UINT64_C(1) << 61;
double shifted_observation_sec = 0.0;
CHECK(gps_time_differential_observation(
2300, 604799.9, reference_counter + origin_shift,
2301, 0.1, event_counter + origin_shift,
counter_hz, &shifted_observation_sec) &&
fabs(shifted_observation_sec - observation_sec) < 1.0e-15,
"free-counter origin changed the differential pseudorange");
CHECK(gps_time_differential_observation(
2300, 100.0, 1000000u,
2300, 99.998, 750000u,
counter_hz, &observation_sec) &&
fabs(observation_sec) < 1.0e-14,
"negative receiver/transmit deltas did not cancel");
CHECK(!gps_time_differential_observation(
2300, 604800.0, 0u,
2300, 0.0, 0u,
counter_hz, &observation_sec),
"out-of-range transmit time was accepted");
CHECK(!gps_time_differential_observation(
2300, 0.0, 0u,
2300, 0.0, 0u,
0.0, &observation_sec),
"zero receiver-counter frequency was accepted");
}
static void rf_sample_counter_forms_precise_differential_pseudorange(void)
{
const double reference_sample = 1000000000.375;
const double receiver_delta_sec = 0.205;
const double event_sample =
reference_sample + receiver_delta_sec * SAMPLE_RATE_HZ;
double observation_sec = 0.0;
CHECK(gps_time_sample_differential_observation(
2300, 604799.9, reference_sample,
2301, 0.1, event_sample,
SAMPLE_RATE_HZ, &observation_sec),
"week-crossing RF-sample observation was rejected");
CHECK(fabs(observation_sec - 0.005) < 1.0e-10,
"RF-sample/transmit difference produced the wrong pseudorange");
/* A shared counter origin, including an unwrapped 32-bit rollover, is a
* common datum and cannot alter position. */
const double origin_shift = 4294967296.0;
double shifted_observation_sec = 0.0;
CHECK(gps_time_sample_differential_observation(
2300, 604799.9, reference_sample + origin_shift,
2301, 0.1, event_sample + origin_shift,
SAMPLE_RATE_HZ, &shifted_observation_sec) &&
fabs(shifted_observation_sec - observation_sec) < 1.0e-12,
"RF sample-counter origin changed differential pseudorange");
CHECK(!gps_time_sample_differential_observation(
2300, 0.0, reference_sample,
2300, 0.0, event_sample,
0.0, &observation_sec),
"zero RF sample rate was accepted");
CHECK(!gps_time_sample_differential_observation(
2300, 0.0, reference_sample,
2300, 0.0, NAN,
SAMPLE_RATE_HZ, &observation_sec),
"non-finite RF sample was accepted");
}
static void differential_bias_recovers_receiver_tow(void)
{
const double speed_of_light_mps = 299792458.0;
const double flight_time_sec = 0.068;
double receiver_tow_sec = 0.0;
CHECK(gps_time_receiver_tow_from_differential_bias(
100000.18, -flight_time_sec * speed_of_light_mps,
speed_of_light_mps, &receiver_tow_sec) &&
fabs(receiver_tow_sec - 100000.248) < 1.0e-10,
"negative differential WLS bias did not recover flight time");
/* Positive WLS bias moves receiver time earlier. This explicitly guards
* the subtraction sign used by the production PPS anchor. */
CHECK(gps_time_receiver_tow_from_differential_bias(
100000.18, flight_time_sec * speed_of_light_mps,
speed_of_light_mps, &receiver_tow_sec) &&
fabs(receiver_tow_sec - 100000.112) < 1.0e-10,
"differential WLS bias was added instead of subtracted");
CHECK(!gps_time_receiver_tow_from_differential_bias(
100000.18, 0.0, 0.0, &receiver_tow_sec),
"zero propagation speed was accepted");
CHECK(!gps_time_receiver_tow_from_differential_bias(
100000.18, 0.0, speed_of_light_mps, NULL),
"null receiver-TOW output was accepted");
}
static void local_timer_detects_rf_counter_reset(void)
{
double local_elapsed_sec = 0.0;
CHECK(gps_time_elapsed_consistent_with_local_timer(
30.08, 1000u, 1301u, 10.0, 0.30,
&local_elapsed_sec),
"continuous RF counter disagreed with quantized CPU timer");
CHECK(fabs(local_elapsed_sec - 30.1) < 1.0e-12,
"CPU timer interval was computed incorrectly");
CHECK(!gps_time_elapsed_consistent_with_local_timer(
87.4, 1000u, 1301u, 10.0, 0.30, NULL),
"RF counter reset/random modulo interval passed CPU timer cross-check");
CHECK(!gps_time_elapsed_consistent_with_local_timer(
1.0, 1301u, 1000u, 10.0, 0.30, NULL),
"backward CPU timer interval was accepted");
}
int main(void)
{
normal_and_skipped_subframes_are_consistent();
@@ -193,6 +321,10 @@ int main(void)
physical_rf_rate_avoids_six_second_sawtooth();
periodic_event_time_removes_receive_range_rate();
fractional_code_phase_corrects_capture_counter();
free_counter_forms_differential_pseudorange();
rf_sample_counter_forms_precise_differential_pseudorange();
differential_bias_recovers_receiver_tow();
local_timer_detects_rf_counter_reset();
if(failures == 0)
{
puts("gps_time_anchor tests passed");
@@ -0,0 +1,40 @@
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include "gps/gps_visibility.h"
static void expect_near(double actual, double expected, double tolerance)
{
assert(fabs(actual - expected) <= tolerance);
}
int main(void)
{
const double earth_radius_m = 6378137.0;
const double receiver[3] = {earth_radius_m, 0.0, 0.0};
double elevation = 0.0;
const double zenith_satellite[3] = {26560000.0, 0.0, 0.0};
assert(gps_visibility_elevation_deg(
receiver, 0.0, 0.0, zenith_satellite, &elevation));
expect_near(elevation, 90.0, 1.0e-9);
const double opposite_hemisphere_satellite[3] =
{-26560000.0, 0.0, 0.0};
assert(gps_visibility_elevation_deg(
receiver, 0.0, 0.0,
opposite_hemisphere_satellite, &elevation));
expect_near(elevation, -90.0, 1.0e-9);
const double horizon_satellite[3] =
{earth_radius_m, 26560000.0, 0.0};
assert(gps_visibility_elevation_deg(
receiver, 0.0, 0.0, horizon_satellite, &elevation));
expect_near(elevation, 0.0, 1.0e-9);
assert(!gps_visibility_elevation_deg(
receiver, 100.0, 0.0, zenith_satellite, &elevation));
puts("gps visibility geometry tests passed");
return 0;
}
+103 -18
View File
@@ -25,7 +25,8 @@ The model follows the current firmware and RTL conventions:
events. Navigation-solve interrupt, PPS counter capture, capture sequence,
and a 32-bit RF ABS_SAMPLE wrap are exercised.
6. Firmware reconstructs periodic TOW from the HOW/sample anchor and evaluates
`TOW + POSTPONE_TOW + STARTOFFSET - b/c`.
`TOW + POSTPONE_TOW - b/c`; the timer-difference WLS datum contains no
startup constant.
7. The timestamped 125 MHz PPS peripheral performs coarse/normal mapping,
ignores synthetic CPU/APB delay as phase, acquires lock after five valid
updates, and generates integer-second edges.
@@ -80,6 +81,10 @@ that the repository did not capture:
```bash
python3 -m pps_end_to_end_validation.stability_analysis
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 500
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 1000
python3 -m unittest \
pps_end_to_end_validation/test_stability_analysis.py
```
@@ -89,19 +94,22 @@ Generated results are in `output/stability/`:
- `stability_report.md`: real-data inventory, discrete poles, absolute phase
wander, period jitter, dynamic parameter sweep, and recommendations;
- `parameter_sweep.csv`: machine-readable results for every scenario/tuning;
- `synthetic_telemetry.csv`: 5 Hz ABS_SAMPLE, REM_CODE_PHASE, corrected 125 MHz
- `synthetic_telemetry.csv`: solve-cadence ABS_SAMPLE, REM_CODE_PHASE, corrected 125 MHz
capture, HDOP/VDOP/TDOP/GDOP, C/N0, pseudorange noise, clock sigma/drift,
oscillator and timing-error inputs.
The real capture contains 989 GDOP values and 990 tracked-satellite values
(eight throughout), but no PPS phase/rate, ABS_SAMPLE, C/N0, TDOP/HDOP,
pseudorange residual, oscillator temperature, or trusted-edge/TIC samples.
The generated CSV labels its replacements as synthetic; it must not be read as
board telemetry.
The older fusion capture used by `stability_analysis.py` contains 989 GDOP
values and 990 tracked-satellite values (eight throughout), but no PPS
phase/rate, ABS_SAMPLE, C/N0, TDOP/HDOP, pseudorange residual, oscillator
temperature, or trusted-edge/TIC samples. The newer live-board workflow below
supplies many of those PPS status fields, while ABS_SAMPLE, REM_CODE_PHASE,
temperature, and trusted-edge timing remain unavailable. The stability CSV
labels replacements as synthetic; it must not be read as board telemetry.
The current acquire, precision, and robust fixed-point poles are inside the
unit circle at the 200 ms update rate, and the stress analysis observes no
unlock. It also exposes an important metrics issue: cycle-to-cycle PPS period
unit circle at the 200, 500, and 1000 ms supported update rates, and the stress
analysis observes no unlock. It also exposes an important metrics issue:
cycle-to-cycle PPS period
jitter can remain below 20 ns while absolute phase moves slowly through tens
of nanoseconds. A physical 20 ns maximum therefore requires a common-clock
time-interval-counter capture and cable/output-delay calibration; a p95 period
@@ -110,8 +118,79 @@ jitter assertion or synthetic run cannot certify it.
The sweep includes the current coefficients, quieter/faster precision loops,
a robust-noise guard, a larger Doppler-drift gain, and a quality-scheduled
candidate. The generated report contains the current numerical comparison.
The stability analysis does not alter loop constants. Production firmware now
uses the current-RTL-compatible 180 ms epoch correction described below.
Production firmware maps the proven 200 ms discrete poles to the selected NAV
period and leaves the 200 ms Q2.46 constants bit-identical. The model and
firmware also scale quality/lock/settling timers in wall-clock time, convert
software bias/drift gains from actual elapsed time, and extend the stale-data
watchdog to `max(2 s, 3 * solve period)`. This preserves deterministic settling
and damping; it cannot preserve measurement-noise statistics when fewer WLS
samples are available. Production firmware uses the current-RTL-compatible
180 ms epoch correction independently of solve cadence.
A deterministic four-seed, 180 s smoke sweep with the current profiles gave:
| solve period | real-geometry absolute RMS | worst period step over current scenarios | unlocks |
|---:|---:|---:|---:|
| 200 ms | 11.17 ns | 16 ns | 0 |
| 500 ms | 15.96 ns | 16 ns | 0 |
| 1000 ms | 25.09 ns | 24 ns | 0 |
Thus 1000 ms is linearly stable but is not recommended when the stated
synthetic maximum-period-jitter target is 20 ns. Use 200 ms by default, or
500 ms when CPU load requires it. A 1000 ms solve needs hardware validation
and likely oscillator-temperature/frequency prediction; pole remapping alone
cannot recreate the missing measurements.
## Live board telemetry replay
`hardware_replay.py` accepts the host-timestamped UART CSV produced by the
fusion capture utility. It extracts the firmware fields that actually drive
or describe PPS: raw/filtered WLS clock bias, innovation, Doppler datum drift,
TDOP/GDOP/HDOP/VDOP, clock sigma, C/N0, tracked/used satellites, velocity/FDE,
reference SVID, fractional capture correction, RTL phase residual, rate word,
integrator, lock/profile, pulse count, and commit sequence.
Capture and replay from `soc/software`:
```bash
python3 fusion/serial_capture.py \
--port /dev/ttyUSB2 --baud 460800 --duration 300 \
--output /tmp/genesys2_pps.csv
python3 -m pps_end_to_end_validation.hardware_replay \
--input /tmp/genesys2_pps.csv \
--output-dir pps_end_to_end_validation/output/hardware_replay
python3 -m unittest \
pps_end_to_end_validation.test_hardware_replay
```
Raw UART files are ignored by Git because `[LOCATION]` records contain exact
coordinates. The replay output is normalized to PPS fields and deliberately
omits latitude/longitude.
For an already-running board, replay imports the first logged locked
filter/PI snapshot. Starting the model from reset would erase the observed
integrator and create a false acquisition transient. When status is decimated,
commit sequence numbers expand the trace back to the inferred solve cadence;
intermediate values are explicitly marked `interpolated=true`.
The replay infers cadence from UART host time divided by commit-sequence
advance, snapped to the 20 ms navigation-bit grid. Use
`--update-period-ms` only when host timestamps are unreliable.
The report separates three quantities that should not be conflated:
- direct `hw_phase` statistics from the board;
- fixed-point PI reproduction and a telemetry-conditioned model replay; and
- inferred active phase obtained by integrating `rate_cmd` under a constant
oscillator assumption.
Only the first two are directly constrained by firmware/RTL telemetry. The
third is useful for diagnosing slow wander but is not a trusted-generator
measurement. `hw_phase` itself is a 5 Hz commit residual, not the physical PPS
pin offset. A calibrated, common-clock TIC/scope capture is still required for
a maximum-20 ns output claim.
## 20 ms diagnosis
@@ -138,7 +217,9 @@ correction, consistently in both satellite-state and PPS calculations. It is
containing the current eighth bit, firmware must return to 160 ms in the same
coordinated release.
The validation intentionally does not modify production RTL or firmware.
The validation intentionally does not modify production RTL. Firmware changes
for period-aware control are exercised by the same coefficient and timing
tests; the 180 ms absolute epoch convention remains unchanged.
## Other regression coverage
@@ -152,16 +233,20 @@ The unit tests verify:
- 180 ms firmware compatibility with current RTL;
- invariance to synthetic CPU/APB stalls when capture timestamps are used;
- DUT-versus-trusted output-delay sign; and
- rejection behavior for the firmware-permitted `NAV_SOLVE_TIMER=40` case.
- rejection behavior explaining why firmware now compile-rejects
`NAV_SOLVE_TIMER=40`; and
- zero NAV-solve rejection and unchanged absolute epoch at N=10/25/50
(200/500/1000 ms).
The `N=40` case divides the GPS week but not a 300-bit subframe. When firmware
The `N=40` case divides the GPS week but not a 300-bit subframe. When the model
refreshes the HOW/sample anchor after six seconds, the timer phase is half a
solve period away from that anchor and the current `N * 20 ms` snap rejects
otherwise valid solves. Production `N=10` divides 300 and is unaffected.
otherwise valid solves. Firmware therefore requires N to divide both the GPS
week and the 300-bit subframe. Production-safe examples are N=10, 25, and 50.
If general timer periods are required, reconstruct elapsed time on the
fundamental 20 ms navigation-bit grid. A narrower compatibility restriction is
to accept only periods for which `300 % N == 0`.
If arbitrary timer periods are required later, reconstruct elapsed time on the
fundamental 20 ms navigation-bit grid instead of snapping through a refreshed
HOW anchor.
## Hardware confirmation
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
host_monotonic_s,host_unix_s,time_s,profile,mode,locked,tdop,gdop,hdop,vdop,clock_sigma_m,cn0_mean_dbhz,cn0_min_dbhz,tracked_satellites,used_satellites,innovation_m,raw_bias_m,filtered_bias_m,clock_drift_mps,bias_drift_mps,predictor_enabled,eskf_alpha,eskf_speed_mps,eskf_velocity_rms_mps,eskf_fde_mask,pps_speed_mps,pps_velocity_rms_mps,pps_fde_mask,reference_svid,capture_correction_ns,hardware_phase_ns,rate_command_ppm,integrator_ppm,pulse_count,sequence,status
1.50420269,1785272055.903764,0.0,robust,tracking,True,1.25,2.86,1.8176601213,1.8176260703,12.54,42.7,37.7,6,5,4.52,693498.817,693494.442,110.788,-72.055,True,0.05,0.271,0.185,0,0.271,0.185,0,23,32.0,160.47,-1.536,-1.56,600,3040,2063
3.503689524,1785272057.9032507,2.0,robust,tracking,True,1.25,2.86,1.8176193953,1.8170996688,11.17,42.9,38.2,6,5,-19.2,693330.906,693349.504,111.201,-71.738,True,0.05,0.746,0.135,0,0.746,0.135,0,23,56.0,124.38,-1.541,-1.56,602,3050,2063
5.500479302,1785272059.9000404,4.0,robust,tracking,True,1.25,2.86,1.8175780522,1.8165687846,21.48,42.7,38.5,6,5,-7.0,693197.92,693204.698,111.206,-71.912,True,0.05,0.836,0.039,0,0.836,0.039,0,23,24.0,141.5,-1.539,-1.56,604,3060,2063
7.509988539,1785272061.9095497,6.0,robust,tracking,True,1.25,2.86,1.8175381286,1.816041859,11.14,43.0,38.3,6,5,13.6,693072.274,693059.102,111.221,-71.825,True,0.05,1.009,0.28,0,1.009,0.28,0,23,32.0,168.11,-1.535,-1.56,606,3070,2063
9.511653187,1785272063.9112144,8.0,robust,tracking,True,1.25,2.86,1.8174962624,1.8155120506,11.56,42.9,38.0,6,5,-31.15,692881.989,692912.165,110.864,-72.861,True,0.05,0.371,0.04,0,0.371,0.04,0,23,40.0,135.63,-1.539,-1.56,608,3080,2063
11.50378733,1785272065.9033487,10.0,robust,tracking,True,1.25,2.85,1.8174539102,1.8149874373,11.07,42.8,38.6,6,5,-9.78,692754.373,692763.848,110.014,-73.882,True,0.05,0.843,0.004,0,0.843,0.004,0,23,56.0,155.42,-1.536,-1.56,610,3090,2063
13.504178648,1785272067.9037397,12.0,robust,tracking,True,1.25,2.85,1.817413463,1.8144599125,11.18,42.9,38.1,6,5,6.48,692623.758,692617.477,110.329,-73.94,True,0.05,0.609,0.096,0,0.609,0.096,0,23,0.0,162.66,-1.535,-1.56,612,3100,2063
15.503554083,1785272069.9031153,14.0,robust,tracking,True,1.25,2.85,1.8173718635,1.8139319104,11.26,42.7,37.0,6,5,-1.31,692469.561,692470.834,111.54,-72.849,True,0.05,0.953,0.31,0,0.953,0.31,0,23,56.0,155.78,-1.536,-1.56,614,3110,2063
17.502924749,1785272071.9024858,16.0,robust,tracking,True,1.25,2.85,1.81733102,1.8134069522,15.4,42.4,36.5,6,5,-11.0,692313.987,692324.644,110.815,-73.581,True,0.05,0.736,0.168,0,0.736,0.168,0,23,32.0,133.05,-1.54,-1.56,616,3120,2063
19.506236288,1785272073.9057975,18.0,robust,tracking,True,1.24,2.85,1.8172902798,1.8128821788,11.11,43.1,38.1,6,5,-5.77,692171.911,692177.505,111.077,-73.779,True,0.05,0.4,0.027,0,0.4,0.027,0,23,24.0,131.86,-1.54,-1.56,618,3130,2063
21.504228726,1785272075.9037895,20.0,robust,tracking,True,1.24,2.85,1.8172490099,1.8123554135,11.15,43.0,38.0,6,5,8.57,692038.349,692030.045,110.612,-74.424,True,0.05,0.512,0.053,0,0.512,0.053,0,23,24.0,156.87,-1.536,-1.56,620,3140,2063
23.504153615,1785272077.903715,22.0,robust,tracking,True,1.24,2.85,1.8172104392,1.8118290291,11.3,42.6,37.7,6,5,-11.79,691868.479,691879.901,111.534,-73.806,True,0.05,0.721,0.036,0,0.721,0.036,0,23,56.0,142.52,-1.538,-1.56,622,3150,2063
25.510000609,1785272079.9095619,24.0,robust,tracking,True,1.24,2.85,1.8171689199,1.8113038519,11.08,43.1,38.1,6,5,-12.44,691719.345,691731.399,110.942,-74.863,True,0.05,0.359,0.101,0,0.359,0.101,0,23,48.0,145.5,-1.538,-1.56,624,3160,2063
27.50517151,1785272081.904733,26.0,robust,tracking,True,1.24,2.85,1.8171301373,1.8107796086,11.26,42.7,37.5,6,5,-18.16,691564.145,691581.742,111.5,-74.479,True,0.05,0.745,0.096,0,0.745,0.096,0,23,8.0,129.02,-1.54,-1.56,626,3170,2063
29.502641745,1785272083.9022028,28.0,robust,tracking,True,1.24,2.85,1.8170880804,1.8102547189,12.41,43.0,37.5,6,5,1.28,691433.115,691431.876,111.148,-74.903,True,0.05,0.332,0.115,0,0.332,0.115,0,23,0.0,127.84,-1.541,-1.56,628,3180,2063
31.500642933,1785272085.900204,30.0,robust,tracking,True,1.24,2.85,1.8170480561,1.8097326484,14.78,42.6,37.5,6,5,-1.39,691280.067,691281.411,111.681,-74.941,True,0.05,0.503,0.255,0,0.503,0.255,0,23,48.0,147.24,-1.538,-1.56,630,3190,2063
33.504307045,1785272087.903868,32.0,robust,tracking,True,1.24,2.85,1.8170082113,1.8092081529,22.56,42.9,38.4,6,5,-13.17,691116.408,691129.168,110.696,-75.765,True,0.05,0.206,0.027,0,0.206,0.027,0,23,16.0,132.81,-1.54,-1.56,632,3200,2063
35.509887989,1785272089.9094489,34.0,robust,tracking,True,1.24,2.85,1.8169688875,1.8086870549,11.18,42.9,38.0,6,5,-0.84,690974.741,690975.554,111.343,-75.376,True,0.05,0.636,0.055,0,0.636,0.055,0,23,56.0,156.39,-1.536,-1.56,634,3210,2063
37.506370892,1785272091.9059322,36.0,robust,tracking,True,1.24,2.85,1.8169289218,1.8081636846,10.95,43.4,39.5,6,5,-3.76,690819.152,690822.79,110.566,-76.732,True,0.05,0.72,0.058,0,0.72,0.058,0,23,32.0,156.77,-1.536,-1.56,636,3220,2063
39.504571912,1785272093.904133,38.0,robust,tracking,True,1.24,2.85,1.8168882102,1.8076411645,11.05,43.1,38.3,6,5,-8.88,690659.561,690668.166,111.385,-76.264,True,0.05,0.403,0.084,0,0.403,0.084,0,23,48.0,147.43,-1.538,-1.56,638,3230,2063
41.502657038,1785272095.9022183,40.0,robust,tracking,True,1.24,2.85,1.8168471874,1.8071184407,11.13,42.7,38.1,6,5,5.56,690520.399,690515.009,111.68,-75.754,True,0.05,0.957,0.102,0,0.957,0.102,0,23,32.0,147.4,-1.538,-1.56,640,3240,2063
43.50032369,1785272097.8998854,42.0,robust,tracking,True,1.24,2.85,1.816809289,1.8065980348,11.21,42.7,37.8,6,5,9.98,690370.621,690360.952,111.268,-76.081,True,0.05,0.807,0.041,0,0.807,0.041,0,23,0.0,136.35,-1.539,-1.56,642,3250,2063
45.712022504,1785272100.111584,44.0,robust,tracking,True,1.24,2.85,1.8167660234,1.8060226878,12.45,43.2,38.0,6,5,-7.85,690184.849,690192.456,111.097,-76.306,True,0.05,0.794,0.037,0,0.794,0.037,0,23,32.0,142.31,-1.538,-1.56,645,3260,2063
47.704670721,1785272102.1042318,46.0,robust,tracking,True,1.24,2.85,1.8167259917,1.8055025786,11.59,42.9,38.7,6,5,-14.42,690023.356,690037.324,110.386,-77.903,True,0.05,0.84,0.099,0,0.84,0.099,0,23,56.0,136.57,-1.539,-1.56,647,3270,2063
49.704359089,1785272104.1039205,48.0,robust,tracking,True,1.24,2.84,1.8166840843,1.804981981,11.04,43.4,38.2,6,5,-2.28,689880.716,689882.925,110.905,-78.019,True,0.05,0.509,0.107,0,0.509,0.107,0,23,16.0,148.6,-1.537,-1.56,649,3280,2063
51.709139916,1785272106.1087012,50.0,robust,tracking,True,1.24,2.84,1.8166456009,1.8044613748,10.99,43.3,38.1,6,5,-0.68,689726.579,689727.233,111.064,-77.798,True,0.05,0.094,0.103,0,0.094,0.103,0,23,48.0,144.29,-1.538,-1.56,651,3290,2063
53.703365655,1785272108.1029265,52.0,robust,tracking,True,1.24,2.84,1.8166083592,1.8039440331,12.68,42.8,37.5,6,5,-8.72,689564.342,689572.793,110.533,-78.338,True,0.05,0.504,0.187,0,0.504,0.187,0,23,8.0,148.54,-1.537,-1.56,653,3300,2063
55.700643307,1785272110.100204,54.0,robust,tracking,True,1.24,2.84,1.8165672875,1.8034221236,11.07,42.9,38.7,6,5,4.12,689421.398,689417.403,111.098,-77.598,True,0.05,0.645,0.08,0,0.645,0.08,0,23,40.0,140.91,-1.539,-1.56,655,3310,2063
57.904055806,1785272112.3036168,56.0,robust,tracking,True,1.24,2.84,1.8165247981,1.8028524558,16.67,42.9,38.2,6,5,-1.04,689243.202,689244.211,110.99,-78.254,True,0.05,0.342,0.02,0,0.342,0.02,0,23,48.0,144.27,-1.538,-1.56,657,3320,2063
60.10451733,1785272114.5040786,58.0,robust,tracking,True,1.24,2.84,1.8164827704,1.8022817921,11.19,42.9,38.0,6,5,-5.64,689066.66,689072.123,111.676,-78.014,True,0.05,0.73,0.15,0,0.73,0.15,0,23,48.0,126.86,-1.541,-1.56,659,3330,2063
62.108759952,1785272116.5083213,60.0,robust,tracking,True,1.24,2.84,1.8164435096,1.8017644864,16.38,43.1,39.2,6,5,-1.09,688913.112,688914.171,111.17,-78.316,True,0.05,0.696,0.007,0,0.696,0.007,0,23,24.0,153.74,-1.537,-1.56,661,3340,2063
64.300493137,1785272118.7000537,62.0,robust,tracking,True,1.24,2.84,1.8163984438,1.8011921234,11.32,42.4,37.4,6,5,8.12,688749.473,688741.606,110.452,-79.51,True,0.05,0.546,0.16,0,0.546,0.16,0,23,40.0,152.97,-1.537,-1.56,663,3350,2063
66.510932531,1785272120.9104934,64.0,robust,tracking,True,1.23,2.84,1.8163566675,1.8006252849,20.15,42.7,38.3,6,5,-2.84,688563.016,688565.769,110.92,-78.813,True,0.05,0.795,0.197,0,0.795,0.197,0,23,40.0,172.22,-1.535,-1.561,665,3360,2063
68.704372014,1785272123.103933,66.0,robust,tracking,True,1.23,2.84,1.8163145194,1.8000555234,15.52,42.8,38.5,6,5,-11.85,688380.019,688391.495,110.061,-80.47,True,0.05,0.796,0.074,0,0.796,0.074,0,23,48.0,155.17,-1.537,-1.56,668,3370,2063
70.703104898,1785272125.102666,68.0,robust,tracking,True,1.23,2.84,1.8162769265,1.7995395272,11.43,42.0,37.8,6,5,13.98,688244.124,688230.58,110.609,-80.264,True,0.05,0.381,0.125,0,0.381,0.125,0,23,16.0,179.45,-1.533,-1.56,670,3380,2063
72.702223062,1785272127.101784,70.0,robust,tracking,True,1.23,2.84,1.8162378878,1.7990227767,16.58,42.8,38.9,6,5,5.88,688078.671,688072.971,110.939,-80.36,True,0.05,0.666,0.053,0,0.666,0.053,0,23,24.0,166.76,-1.535,-1.56,672,3390,2063
74.700286036,1785272129.0998468,72.0,robust,tracking,True,1.23,2.84,1.816199267,1.7985063001,11.1,42.7,38.7,6,5,14.57,687926.671,687912.557,110.527,-80.516,True,0.05,0.312,0.03,0,0.312,0.03,0,23,32.0,162.81,-1.536,-1.56,674,3400,2063
76.71281947,1785272131.1123805,74.0,robust,tracking,True,1.23,2.84,1.8161600375,1.7979893506,11.11,42.7,38.6,6,5,-1.67,687750.089,687751.702,111.576,-79.6,True,0.05,1.026,0.083,0,1.026,0.083,0,23,16.0,142.71,-1.539,-1.56,676,3410,2063
78.704243128,1785272133.103804,76.0,robust,tracking,True,1.23,2.84,1.8161215432,1.7974747243,11.08,43.2,38.1,6,5,-4.48,687586.716,687591.059,110.819,-81.044,True,0.05,0.676,0.104,0,0.676,0.104,0,23,0.0,107.47,-1.544,-1.56,678,3420,2063
80.70401895,1785272135.10358,78.0,robust,tracking,True,1.23,2.84,1.8160831759,1.7969606981,20.64,42.3,37.5,6,5,-0.58,687429.447,687430.01,110.975,-81.135,True,0.05,0.351,0.106,0,0.351,0.106,0,23,56.0,161.62,-1.536,-1.56,680,3430,2063
82.710133903,1785272137.1096952,80.0,robust,tracking,True,1.23,2.84,1.8160471938,1.7964462492,21.18,43.0,39.1,6,5,11.46,687279.439,687268.334,110.976,-80.409,True,0.05,1.15,0.031,0,1.15,0.031,0,23,0.0,177.64,-1.534,-1.56,682,3440,2063
84.702669179,1785272139.1022303,82.0,robust,tracking,True,1.23,2.84,1.8160084174,1.7959315494,16.16,42.7,39.0,6,5,2.07,687108.261,687106.252,110.31,-81.912,True,0.05,0.401,0.04,0,0.401,0.04,0,23,48.0,145.45,-1.539,-1.56,684,3450,2063
86.700732144,1785272141.100293,84.0,robust,tracking,True,1.23,2.83,1.8159713938,1.7954167669,11.13,42.7,39.0,6,5,-12.66,686929.679,686941.939,110.952,-81.183,True,0.05,0.726,0.196,0,0.726,0.196,0,23,24.0,139.84,-1.539,-1.56,686,3460,2063
88.704090128,1785272143.103651,86.0,robust,tracking,True,1.23,2.83,1.8159335443,1.79490275,16.21,42.7,38.7,6,5,8.63,686784.906,686776.542,110.694,-82.024,True,0.05,0.209,0.052,0,0.209,0.052,0,23,48.0,176.01,-1.534,-1.56,688,3470,2063
90.704048278,1785272145.103609,88.0,robust,tracking,True,1.23,2.83,1.8158959585,1.7943905432,24.91,42.5,37.9,6,5,-0.15,686613.039,686613.185,110.563,-82.135,True,0.05,0.379,0.123,0,0.379,0.123,0,23,40.0,161.5,-1.536,-1.56,690,3480,2063
92.711508414,1785272147.1110694,90.0,robust,tracking,True,1.23,2.83,1.8158570425,1.793877413,24.43,42.7,38.0,6,5,15.4,686465.242,686450.326,111.554,-81.637,True,0.05,1.011,0.184,0,1.011,0.184,0,23,56.0,178.15,-1.534,-1.56,692,3490,2063
94.704749369,1785272149.1043103,92.0,robust,tracking,True,1.23,2.83,1.8158190794,1.7933647389,21.48,42.4,38.5,6,5,16.84,686303.4,686287.088,110.534,-82.726,True,0.05,0.195,0.029,0,0.195,0.029,0,23,48.0,171.95,-1.535,-1.56,694,3500,2063
96.703104991,1785272151.1026661,94.0,robust,tracking,True,1.23,2.83,1.815781917,1.7928524573,11.31,42.4,37.6,6,5,-1.05,686119.874,686120.892,109.749,-83.836,True,0.05,0.919,0.158,0,0.919,0.158,0,23,16.0,137.68,-1.54,-1.56,696,3510,2063
98.915123597,1785272153.3146844,96.0,robust,tracking,True,1.23,2.83,1.8157417458,1.792292301,11.49,42.2,37.0,6,5,-4.81,685933.786,685938.441,110.18,-83.485,True,0.05,0.687,0.188,0,0.687,0.188,0,23,40.0,130.24,-1.541,-1.56,698,3520,2063
101.104184426,1785272155.5037456,98.0,robust,tracking,True,1.23,2.83,1.8157012539,1.7917269932,14.98,42.4,37.7,6,5,-4.93,685751.271,685756.046,110.623,-83.514,True,0.05,0.285,0.185,0,0.285,0.185,0,23,40.0,144.59,-1.539,-1.56,700,3530,2063
103.108802454,1785272157.5083635,100.0,robust,tracking,True,1.23,2.83,1.8156626947,1.7912163488,25.23,42.4,38.0,6,5,8.11,685596.218,685588.366,110.401,-83.817,True,0.05,0.362,0.053,0,0.362,0.053,0,23,56.0,172.28,-1.535,-1.56,702,3540,2063
105.105464086,1785272159.505025,102.0,robust,tracking,True,1.23,2.83,1.8156280022,1.79070636,16.51,42.5,37.5,6,5,-15.09,685403.733,685418.356,110.792,-83.601,True,0.05,0.365,0.1,0,0.365,0.1,0,23,48.0,134.38,-1.54,-1.56,704,3550,2063
107.300312593,1785272161.6998734,104.0,robust,tracking,True,1.23,2.83,1.8155869207,1.790143438,20.41,42.6,38.3,6,5,4.06,685236.104,685232.167,111.771,-83.119,True,0.05,1.06,0.172,0,1.06,0.172,0,23,56.0,186.11,-1.532,-1.56,706,3560,2063
109.312626871,1785272163.7121878,106.0,robust,tracking,True,1.23,2.83,1.8155513007,1.7896363278,15.58,42.6,37.5,6,5,-5.9,685055.343,685061.062,110.302,-84.628,True,0.05,0.319,0.208,0,0.319,0.208,0,23,24.0,168.36,-1.535,-1.56,708,3570,2063
111.306453913,1785272165.7060149,108.0,robust,tracking,True,1.23,2.83,1.8155128487,1.789126556,21.63,42.6,38.0,6,5,16.48,684906.901,684890.938,110.502,-84.943,True,0.05,0.385,0.093,0,0.385,0.093,0,23,16.0,177.8,-1.534,-1.56,710,3580,2063
113.305323246,1785272167.7048843,110.0,robust,tracking,True,1.22,2.83,1.8154776515,1.7886176858,11.1,42.9,38.4,6,5,1.51,684721.855,684720.397,110.396,-84.747,True,0.05,0.197,0.218,0,0.197,0.218,0,23,32.0,163.81,-1.536,-1.56,712,3590,2063
115.30411121,1785272169.7036724,112.0,robust,tracking,True,1.22,2.83,1.8154394251,1.7881082833,22.64,43.1,38.0,6,5,-3.4,684547.463,684550.752,110.44,-85.241,True,0.05,0.429,0.029,0,0.429,0.029,0,23,8.0,152.36,-1.538,-1.56,714,3600,2063
117.303101565,1785272171.702663,114.0,robust,tracking,True,1.22,2.83,1.8154034756,1.7875997643,11.52,42.6,38.1,6,5,17.65,684398.444,684381.346,110.707,-84.967,True,0.05,0.405,0.059,0,0.405,0.059,0,23,24.0,192.86,-1.532,-1.56,716,3610,2063
119.306150448,1785272173.7057114,116.0,robust,tracking,True,1.22,2.83,1.8153676614,1.7870927007,12.04,42.5,37.5,6,5,-7.99,684203.354,684211.094,110.164,-85.459,True,0.05,0.631,0.155,0,0.631,0.155,0,23,32.0,144.3,-1.539,-1.56,718,3620,2063
121.300962576,1785272175.7005236,118.0,robust,tracking,True,1.22,2.83,1.8153303586,1.786584068,18.38,42.4,37.2,6,5,15.28,684055.675,684040.872,110.141,-86.063,True,0.05,0.481,0.133,0,0.481,0.133,0,23,48.0,200.68,-1.53,-1.56,720,3630,2063
123.303755452,1785272177.7033165,120.0,robust,tracking,True,1.22,2.82,1.815294335,1.7860767164,19.1,42.8,38.8,6,5,2.02,683871.81,683869.856,111.056,-85.022,True,0.05,1.151,0.085,0,1.151,0.085,0,23,64.0,169.36,-1.535,-1.56,722,3640,2063
125.304219262,1785272179.703781,122.0,robust,tracking,True,1.22,2.82,1.8152578155,1.785570688,11.11,42.7,38.6,6,5,-8.82,683688.289,683696.831,110.802,-85.693,True,0.05,0.637,0.051,0,0.637,0.051,0,23,8.0,158.16,-1.537,-1.56,724,3650,2063
127.304583545,1785272181.704145,124.0,robust,tracking,True,1.22,2.82,1.8152215994,1.7850628719,11.13,42.7,38.8,6,5,-1.48,683523.082,683524.519,110.235,-86.917,True,0.05,0.775,0.068,0,0.775,0.068,0,23,40.0,151.87,-1.538,-1.56,726,3660,2063
129.508905566,1785272183.9084663,126.0,robust,tracking,True,1.22,2.82,1.8151838123,1.7845085023,12.03,43.1,38.6,6,5,9.01,683342.021,683333.296,110.201,-87.019,True,0.05,0.331,0.094,0,0.331,0.094,0,23,0.0,157.45,-1.537,-1.56,728,3670,2063
131.500762179,1785272185.9003232,128.0,robust,tracking,True,1.22,2.82,1.8151487619,1.7840006032,12.83,42.6,38.6,6,5,3.63,683160.888,683157.371,110.357,-86.937,True,0.05,0.389,0.075,0,0.389,0.075,0,23,32.0,171.39,-1.535,-1.56,730,3680,2063
133.504344505,1785272187.9039054,130.0,robust,tracking,True,1.22,2.82,1.8151127862,1.7834955606,12.64,42.7,38.5,6,5,3.69,682986.797,682983.217,110.053,-87.63,True,0.05,0.791,0.01,0,0.791,0.01,0,23,0.0,175.65,-1.534,-1.56,732,3690,2063
135.509832366,1785272189.9094036,132.0,robust,tracking,True,1.22,2.82,1.8150762158,1.7829890753,11.13,42.7,38.4,6,5,5.1,682813.848,682808.909,110.531,-87.346,True,0.05,0.079,0.034,0,0.079,0.034,0,23,16.0,162.81,-1.536,-1.56,734,3700,2063
137.506720578,1785272191.9062817,134.0,robust,tracking,True,1.22,2.82,1.8150397064,1.7824837171,10.99,42.8,39.5,6,5,1.42,682635.318,682633.946,110.584,-87.635,True,0.05,0.038,0.097,0,0.038,0.097,0,23,32.0,155.09,-1.537,-1.56,736,3710,2063
139.504410571,1785272193.9039717,136.0,robust,tracking,True,1.22,2.82,1.8150041072,1.7819780719,11.15,42.7,38.1,6,5,-6.72,682453.853,682460.364,110.886,-87.452,True,0.05,0.412,0.407,0,0.412,0.407,0,23,48.0,139.96,-1.539,-1.56,738,3720,2063
141.503078598,1785272195.9026399,138.0,robust,tracking,True,1.22,2.82,1.8149699154,1.781475266,11.11,42.7,38.8,6,5,11.45,682296.086,682284.991,109.638,-89.46,True,0.05,1.419,0.005,0,1.419,0.005,0,23,40.0,167.17,-1.535,-1.56,740,3730,2063
143.500493916,1785272197.900055,140.0,robust,tracking,True,1.22,2.82,1.8149354252,1.7809734693,11.02,42.9,38.7,6,5,12.89,682120.284,682107.799,110.911,-87.736,True,0.05,0.88,0.067,0,0.88,0.067,0,23,56.0,162.67,-1.536,-1.56,742,3740,2063
145.512927625,1785272199.9124885,142.0,robust,tracking,True,1.22,2.82,1.8149005029,1.7804693538,10.96,42.9,39.4,6,5,-25.89,681906.117,681931.202,110.881,-87.866,True,0.05,0.775,0.078,0,0.775,0.078,0,23,8.0,107.83,-1.544,-1.56,744,3750,2063
147.506278297,1785272201.9058392,144.0,robust,tracking,True,1.22,2.82,1.8148661739,1.7799667491,10.98,43.0,39.0,6,5,-5.66,681746.195,681751.678,109.788,-89.613,True,0.05,1.009,0.035,0,1.009,0.035,0,23,56.0,152.27,-1.538,-1.56,746,3760,2063
149.505513865,1785272203.9050748,146.0,robust,tracking,True,1.22,2.82,1.8148312963,1.779461219,11.25,42.4,38.4,6,5,-7.95,681563.852,681571.554,110.854,-89.368,True,0.05,0.482,0.099,0,0.482,0.099,0,23,56.0,142.08,-1.539,-1.56,748,3770,2063
151.504103133,1785272205.903664,148.0,robust,tracking,True,1.22,2.82,1.8147976667,1.778959756,10.98,43.0,39.4,6,5,11.06,681403.38,681392.662,110.425,-89.358,True,0.05,0.199,0.058,0,0.199,0.058,0,23,16.0,161.29,-1.536,-1.56,750,3780,2063
153.502847324,1785272207.9024084,150.0,robust,tracking,True,1.22,2.82,1.8147608671,1.7784571849,15.23,42.6,38.9,6,5,9.66,681223.637,681214.276,110.279,-89.721,True,0.05,0.371,0.218,0,0.371,0.218,0,23,48.0,172.12,-1.535,-1.56,752,3790,2063
155.506297511,1785272209.9058583,152.0,robust,tracking,True,1.22,2.82,1.8147266277,1.7779565566,11.07,42.6,38.7,6,5,0.35,681034.417,681034.073,110.263,-89.772,True,0.05,0.371,0.13,0,0.371,0.13,0,23,56.0,180.04,-1.533,-1.56,754,3800,2063
157.500753602,1785272211.9003143,154.0,robust,tracking,True,1.22,2.82,1.814692541,1.777454151,11.13,42.4,38.4,6,5,8.94,680862.299,680853.639,110.737,-89.433,True,0.05,0.755,0.053,0,0.755,0.053,0,23,0.0,180.5,-1.533,-1.56,756,3810,2063
159.504450759,1785272213.904012,156.0,robust,tracking,True,1.22,2.82,1.8146585955,1.7769539906,11.22,42.3,38.2,6,5,14.47,680691.31,680677.294,110.442,-90.141,True,0.05,0.405,0.02,0,0.405,0.02,0,23,32.0,196.03,-1.531,-1.56,758,3820,2063
161.510423884,1785272215.9099848,158.0,robust,tracking,True,1.21,2.82,1.8146253251,1.7764523761,14.07,42.3,38.6,6,5,14.17,680511.698,680497.972,110.887,-89.977,True,0.05,0.551,0.182,0,0.551,0.182,0,23,32.0,184.51,-1.532,-1.56,760,3830,2063
163.504960681,1785272217.9045217,160.0,robust,tracking,True,1.21,2.81,1.8145907816,1.7759535814,10.99,42.6,39.7,6,5,9.57,680326.822,680317.554,110.339,-91.079,True,0.05,0.465,0.01,0,0.465,0.01,0,23,8.0,150.36,-1.538,-1.56,762,3840,2063
165.507220868,1785272219.9067822,162.0,robust,tracking,True,1.21,2.81,1.8145551226,1.7754518476,10.98,42.8,38.9,6,5,5.13,680143.185,680138.216,109.975,-91.641,True,0.05,0.834,0.034,0,0.834,0.034,0,23,0.0,144.58,-1.539,-1.56,764,3850,2063
167.503525545,1785272221.9030867,164.0,robust,tracking,True,1.21,2.81,1.8145228906,1.7749543943,14.36,42.8,38.7,6,5,4.72,679959.278,679954.706,110.179,-91.65,True,0.05,0.706,0.085,0,0.706,0.085,0,23,48.0,124.01,-1.542,-1.56,766,3860,2063
169.50061802,1785272223.900179,166.0,robust,tracking,True,1.21,2.81,1.8144898435,1.7744531221,11.09,42.8,37.7,6,5,13.57,679787.48,679774.331,110.775,-91.606,True,0.05,1.106,0.075,0,1.106,0.075,0,23,56.0,145.18,-1.538,-1.56,768,3870,2063
171.510388267,1785272225.9099493,168.0,robust,tracking,True,1.21,2.81,1.8144559568,1.7739550407,11.08,42.6,38.2,6,5,8.28,679599.18,679591.156,110.344,-91.505,True,0.05,0.285,0.062,0,0.285,0.062,0,23,56.0,137.18,-1.54,-1.56,770,3880,2063
173.500668495,1785272227.9002295,170.0,robust,tracking,True,1.21,2.81,1.8144213808,1.7734561125,11.07,42.5,38.0,6,5,-7.05,679401.0,679407.83,110.355,-91.896,True,0.05,0.239,0.023,0,0.239,0.023,0,23,48.0,151.22,-1.538,-1.56,772,3890,2063
175.509072834,1785272229.9086337,172.0,robust,tracking,True,1.21,2.81,1.8143875329,1.7729571416,14.88,42.6,38.0,6,5,-9.75,679214.759,679224.203,110.051,-92.203,True,0.05,0.388,0.024,0,0.388,0.024,0,23,16.0,127.04,-1.541,-1.56,774,3900,2063
177.511375431,1785272231.9109368,174.0,robust,tracking,True,1.21,2.81,1.8143541178,1.7724598606,11.01,42.7,39.1,6,5,-28.57,679010.25,679037.93,110.131,-92.548,True,0.05,0.679,0.066,0,0.679,0.066,0,23,16.0,144.09,-1.539,-1.56,776,3910,2063
179.5031023,1785272233.9026635,176.0,robust,tracking,True,1.21,2.81,1.8143216536,1.7719632746,11.04,42.6,38.1,6,5,-1.94,678850.304,678852.181,110.443,-92.232,True,0.05,0.439,0.164,0,0.439,0.164,0,23,24.0,156.61,-1.537,-1.56,778,3920,2063
181.500919559,1785272235.9004805,178.0,robust,tracking,True,1.21,2.81,1.8142885244,1.7714647145,11.67,42.7,39.6,6,5,14.37,678680.998,678667.079,110.223,-93.101,True,0.05,0.927,0.048,0,0.927,0.048,0,23,24.0,174.87,-1.534,-1.56,780,3930,2063
183.504154069,1785272237.903716,180.0,robust,tracking,True,1.21,2.81,1.8142515963,1.7709676955,11.12,42.4,38.5,6,5,3.52,678484.103,678480.688,110.013,-93.55,True,0.05,0.69,0.056,0,0.69,0.056,0,23,0.0,167.43,-1.535,-1.56,782,3940,2063
185.509701079,1785272239.9092622,182.0,robust,tracking,True,1.21,2.81,1.8142225906,1.7704719824,11.12,42.5,38.1,6,5,2.8,678298.275,678295.56,110.36,-92.989,True,0.05,0.188,0.156,0,0.188,0.156,0,23,0.0,175.14,-1.534,-1.56,784,3950,2063
187.511311613,1785272241.9108725,184.0,robust,tracking,True,1.21,2.81,1.8141898144,1.7699731912,11.11,42.3,38.5,6,5,-16.24,678094.632,678110.363,110.845,-93.036,True,0.05,0.46,0.009,0,0.46,0.009,0,23,32.0,150.91,-1.537,-1.56,786,3960,2063
189.504094586,1785272243.9036555,186.0,robust,tracking,True,1.21,2.81,1.8141560815,1.7694787336,11.13,42.5,37.9,6,5,-1.22,677924.284,677925.468,110.012,-93.838,True,0.05,0.542,0.031,0,0.542,0.031,0,23,32.0,167.75,-1.535,-1.56,788,3970,2063
191.503264307,1785272245.902825,188.0,robust,tracking,True,1.21,2.81,1.8141223587,1.7689845528,11.12,42.4,38.3,6,5,12.44,677751.751,677739.703,110.673,-93.502,True,0.05,0.691,0.123,0,0.691,0.123,0,23,32.0,181.14,-1.533,-1.56,790,3980,2063
193.500850983,1785272247.9004118,190.0,robust,tracking,True,1.21,2.81,1.8140919959,1.768488668,14.03,42.3,38.2,6,5,8.52,677559.555,677551.298,110.495,-93.954,True,0.05,0.376,0.004,0,0.376,0.004,0,23,56.0,155.05,-1.537,-1.56,792,3990,2063
195.507436225,1785272249.9069972,192.0,robust,tracking,True,1.21,2.81,1.8140572992,1.7679932608,10.97,42.9,38.9,6,5,-25.61,677336.289,677361.101,110.053,-94.87,True,0.05,0.565,0.132,0,0.565,0.132,0,23,0.0,143.1,-1.539,-1.56,794,4000,2063
197.710664279,1785272252.1102252,194.0,robust,tracking,True,1.21,2.81,1.8140224199,1.7674512561,11.06,42.7,38.3,6,5,-21.21,677131.244,677151.796,110.436,-94.668,True,0.05,0.467,0.099,0,0.467,0.099,0,23,16.0,127.66,-1.541,-1.56,797,4010,2063
199.904494084,1785272254.3040552,196.0,robust,tracking,True,1.21,2.81,1.8139878446,1.7669062479,11.21,42.4,37.9,6,5,2.5,676945.184,676942.759,110.719,-94.592,True,0.05,0.329,0.254,0,0.329,0.254,0,23,0.0,166.66,-1.535,-1.56,799,4020,2063
201.902748265,1785272256.3023093,198.0,robust,tracking,True,1.21,2.8,1.8139551469,1.7664122598,19.71,42.7,38.2,6,5,3.44,676756.483,676753.152,110.284,-95.508,True,0.05,0.834,0.075,0,0.834,0.075,0,23,48.0,159.1,-1.536,-1.56,801,4030,2063
203.900860006,1785272258.3004212,200.0,robust,tracking,True,1.21,2.8,1.8139243484,1.7659203155,21.27,42.6,37.9,6,5,16.7,676577.254,676561.076,111.005,-94.638,True,0.05,0.65,0.026,0,0.65,0.026,0,23,40.0,185.48,-1.532,-1.56,803,4040,2063
205.904311168,1785272260.303872,202.0,robust,tracking,True,1.21,2.8,1.8138926701,1.7654272361,11.23,42.3,37.5,6,5,-4.45,676364.571,676368.878,111.007,-94.853,True,0.05,0.681,0.154,0,0.681,0.154,0,23,32.0,146.48,-1.538,-1.56,805,4050,2063
207.911054786,1785272262.3106158,204.0,robust,tracking,True,1.21,2.8,1.8138607249,1.7649342967,11.29,42.2,37.1,6,5,-0.42,676178.038,676178.445,111.268,-95.121,True,0.05,0.669,0.187,0,0.669,0.187,0,23,56.0,148.37,-1.538,-1.56,807,4060,2063
209.904852911,1785272264.304414,206.0,robust,tracking,True,1.21,2.8,1.8138277745,1.7644418155,17.32,42.6,38.4,6,5,5.29,675993.222,675988.099,110.799,-95.578,True,0.05,0.772,0.012,0,0.772,0.012,0,23,48.0,167.53,-1.535,-1.56,809,4070,2063
211.904389957,1785272266.3039508,208.0,robust,tracking,True,1.2,2.8,1.8137949136,1.7639490637,14.88,42.5,38.1,6,5,4.76,675802.554,675797.942,109.97,-97.009,True,0.05,1.186,0.157,0,1.186,0.157,0,23,16.0,161.37,-1.536,-1.56,811,4080,2063
213.908887407,1785272268.3084483,210.0,robust,tracking,True,1.2,2.8,1.8137652527,1.7634585771,13.65,41.9,38.5,6,5,-19.97,675583.7,675603.041,110.734,-96.484,True,0.05,0.11,0.069,0,0.11,0.069,0,23,8.0,134.36,-1.54,-1.56,813,4090,2063
215.900878981,1785272270.3004403,212.0,robust,tracking,True,1.2,2.8,1.8137346862,1.7629700707,13.14,42.4,39.0,6,5,1.39,675411.049,675409.707,110.477,-96.41,True,0.05,0.489,0.149,0,0.489,0.149,0,23,48.0,164.12,-1.535,-1.56,815,4100,2063
217.908715503,1785272272.3082767,214.0,robust,tracking,True,1.2,2.8,1.8137007679,1.7624770049,11.19,42.1,38.1,6,5,-3.51,675214.305,675217.703,110.227,-97.513,True,0.05,0.667,0.022,0,0.667,0.022,0,23,56.0,141.12,-1.539,-1.56,817,4110,2063
219.904589627,1785272274.3041506,216.0,robust,tracking,True,1.2,2.8,1.8136705307,1.7619846326,17.3,42.2,38.8,6,5,-14.02,675007.687,675021.273,110.312,-97.218,True,0.05,0.452,0.069,0,0.452,0.069,0,23,24.0,158.28,-1.536,-1.56,819,4120,2063
221.9039367,1785272276.3034978,218.0,robust,tracking,True,1.2,2.8,1.8136409999,1.7614933749,11.12,42.1,39.2,6,5,-3.83,674821.953,674825.66,111.06,-96.371,True,0.05,1.326,0.028,0,1.326,0.028,0,23,48.0,170.5,-1.534,-1.56,821,4130,2063
223.91008895,1785272278.3096502,220.0,robust,tracking,True,1.2,2.8,1.8136105372,1.7610043295,11.73,42.3,38.7,6,5,0.13,674630.412,674630.288,111.079,-97.301,True,0.05,0.408,0.148,0,0.408,0.148,0,23,56.0,157.28,-1.536,-1.56,823,4140,2063
225.902916967,1785272280.302478,222.0,robust,tracking,True,1.2,2.8,1.8135794516,1.7605142092,21.26,41.9,39.1,6,5,-21.67,674413.179,674434.176,111.065,-97.127,True,0.05,0.752,0.068,0,0.752,0.068,0,23,48.0,124.73,-1.541,-1.56,825,4150,2063
227.901755726,1785272282.3013165,224.0,robust,tracking,True,1.2,2.8,1.8135490001,1.7600248185,11.02,42.5,39.3,6,5,13.8,674252.975,674239.611,110.715,-97.719,True,0.05,0.748,0.078,0,0.748,0.078,0,23,32.0,159.45,-1.536,-1.56,827,4160,2063
229.901619429,1785272284.3011806,226.0,robust,tracking,True,1.2,2.8,1.8135177823,1.7595391681,11.05,42.4,39.1,6,5,12.06,674053.956,674042.275,110.355,-98.939,True,0.05,0.78,0.043,0,0.78,0.043,0,23,56.0,170.95,-1.534,-1.56,829,4170,2063
231.90481137,1785272286.3043725,228.0,robust,tracking,True,1.2,2.8,1.8134877896,1.7590492494,11.09,42.4,38.1,6,5,12.72,673856.473,673844.146,110.721,-98.388,True,0.05,0.538,0.188,0,0.538,0.188,0,23,48.0,170.54,-1.534,-1.56,831,4180,2063
233.905326912,1785272288.3048878,230.0,robust,tracking,True,1.2,2.8,1.8134570418,1.7585604429,11.06,42.5,38.9,6,5,5.44,673653.112,673647.84,110.098,-98.963,True,0.05,0.301,0.311,0,0.301,0.311,0,23,8.0,171.59,-1.534,-1.56,833,4190,2063
235.904458614,1785272290.3040195,232.0,robust,tracking,True,1.2,2.8,1.8134261204,1.7580735849,11.06,42.5,38.5,6,5,-4.62,673445.637,673450.117,110.459,-98.844,True,0.05,0.829,0.028,0,0.829,0.028,0,23,0.0,153.73,-1.537,-1.56,835,4200,2063
237.904071483,1785272292.3036325,234.0,robust,tracking,True,1.2,2.8,1.8133959704,1.7575868747,12.8,42.0,37.7,6,5,-7.87,673243.852,673251.478,110.772,-98.845,True,0.05,0.559,0.037,0,0.559,0.037,0,23,0.0,146.96,-1.538,-1.56,837,4210,2063
239.908760739,1785272294.3083217,236.0,robust,tracking,True,1.2,2.8,1.8133666091,1.7570976374,11.1,42.3,38.6,6,5,4.99,673058.134,673053.301,110.565,-99.434,True,0.05,0.506,0.016,0,0.506,0.016,0,23,24.0,168.65,-1.535,-1.56,839,4220,2063
241.900563008,1785272296.300124,238.0,robust,tracking,True,1.2,2.79,1.8133344617,1.7566107514,11.14,42.3,38.4,6,5,-14.57,672839.788,672853.907,110.365,-99.522,True,0.05,0.253,0.141,0,0.253,0.141,0,23,0.0,147.65,-1.538,-1.56,841,4230,2063
243.904645899,1785272298.3042068,240.0,robust,tracking,True,1.2,2.79,1.8133057846,1.7561227378,11.1,42.2,38.8,6,5,3.18,672658.12,672655.039,109.903,-100.274,True,0.05,0.476,0.033,0,0.476,0.033,0,23,32.0,164.64,-1.535,-1.56,843,4240,2063
245.904621193,1785272300.3041825,242.0,robust,tracking,True,1.2,2.79,1.8132777969,1.7556370904,11.14,42.2,38.7,6,5,-0.81,672454.836,672455.618,110.573,-100.187,True,0.05,0.405,0.013,0,0.405,0.013,0,23,16.0,134.41,-1.54,-1.56,845,4250,2063
247.899436713,1785272302.2989979,244.0,robust,tracking,True,1.2,2.79,1.8132463791,1.7551512789,11.16,42.2,39.0,6,5,13.48,672269.138,672256.082,110.183,-100.179,True,0.05,0.565,0.113,0,0.565,0.113,0,23,40.0,169.04,-1.535,-1.56,847,4260,2063
249.911369626,1785272304.3109307,246.0,robust,tracking,True,1.2,2.79,1.813217727,1.754665179,11.11,42.5,39.2,6,5,-6.09,672049.638,672055.536,109.774,-100.683,True,0.05,0.486,0.021,0,0.486,0.021,0,23,0.0,135.93,-1.539,-1.56,849,4270,2063
251.903384606,1785272306.3029459,248.0,robust,tracking,True,1.2,2.79,1.8131881994,1.7541830169,11.18,42.2,39.0,6,5,10.96,671865.57,671854.952,109.778,-101.09,True,0.05,0.278,0.027,0,0.278,0.027,0,23,48.0,182.36,-1.532,-1.56,851,4280,2063
253.900781619,1785272308.3003423,250.0,robust,tracking,True,1.2,2.79,1.8131589097,1.7536951988,11.17,42.3,38.9,6,5,3.55,671659.189,671655.749,110.155,-100.561,True,0.05,0.584,0.008,0,0.584,0.008,0,23,16.0,141.85,-1.539,-1.56,853,4290,2063
255.910281973,1785272310.3098433,252.0,robust,tracking,True,1.2,2.79,1.8131293194,1.753212297,11.2,42.1,38.4,6,5,7.57,671462.581,671455.25,109.95,-101.447,True,0.05,0.189,0.096,0,0.189,0.096,0,23,16.0,141.76,-1.539,-1.56,855,4300,2063
257.904754461,1785272312.3043153,254.0,robust,tracking,True,1.2,2.79,1.8131029159,1.7527265372,11.15,42.4,38.7,6,5,-6.95,671245.456,671252.194,110.05,-101.621,True,0.05,0.518,0.084,0,0.518,0.084,0,23,56.0,134.53,-1.54,-1.56,857,4310,2063
259.904898749,1785272314.3044596,256.0,robust,tracking,True,1.2,2.79,1.8130720433,1.7522445406,11.3,41.8,38.4,6,5,-4.74,671044.584,671049.173,110.162,-101.624,True,0.05,0.281,0.225,0,0.281,0.225,0,23,0.0,142.99,-1.539,-1.56,859,4320,2063
261.904207465,1785272316.3037684,258.0,robust,tracking,True,1.19,2.79,1.8130440645,1.7517582107,11.31,42.0,38.4,6,5,-7.29,670837.352,670844.416,110.701,-101.27,True,0.05,0.844,0.152,0,0.844,0.152,0,23,56.0,167.54,-1.535,-1.56,861,4330,2063
263.903658409,1785272318.3032198,260.0,robust,tracking,True,1.19,2.79,1.8130134772,1.7512773631,11.34,42.0,38.5,6,5,-1.16,670637.855,670638.976,110.001,-102.901,True,0.05,0.697,0.02,0,0.697,0.02,0,23,48.0,166.85,-1.535,-1.56,863,4340,2063
265.906678701,1785272320.3062396,262.0,robust,tracking,True,1.19,2.79,1.8129886444,1.7507938348,14.74,42.5,38.9,6,5,10.12,670442.306,670432.505,110.568,-101.927,True,0.05,0.908,0.068,0,0.908,0.068,0,23,40.0,192.58,-1.531,-1.56,865,4350,2063
267.908740259,1785272322.308301,264.0,robust,tracking,True,1.19,2.79,1.8129614176,1.750311896,11.27,42.3,37.7,6,5,-11.12,670213.856,670224.632,110.96,-102.072,True,0.05,0.648,0.323,0,0.648,0.323,0,23,56.0,163.77,-1.535,-1.56,867,4360,2063
269.904666955,1785272324.304228,266.0,robust,tracking,True,1.19,2.79,1.8129315339,1.7498285268,11.35,41.9,37.9,6,5,0.23,670016.459,670016.233,110.584,-102.496,True,0.05,0.457,0.073,0,0.457,0.073,0,23,48.0,200.12,-1.53,-1.56,869,4370,2063
271.904417064,1785272326.303978,268.0,robust,tracking,True,1.19,2.79,1.8129033974,1.7493462127,11.29,42.0,38.2,6,5,-11.63,669798.53,669809.801,110.923,-102.202,True,0.05,0.955,0.178,0,0.955,0.178,0,23,16.0,154.9,-1.537,-1.56,871,4380,2063
273.904287144,1785272328.3038483,270.0,robust,tracking,True,1.19,2.79,1.8128759146,1.7488645046,11.24,42.2,38.4,6,5,0.32,669604.755,669604.44,110.23,-103.356,True,0.05,0.559,0.043,0,0.559,0.043,0,23,56.0,147.74,-1.538,-1.56,873,4390,2063
275.905752343,1785272330.3053133,272.0,robust,tracking,True,1.19,2.79,1.8128467845,1.7483871309,11.29,42.1,38.0,6,5,0.52,669397.133,669396.627,110.767,-103.125,True,0.05,0.839,0.033,0,0.839,0.033,0,23,48.0,175.42,-1.533,-1.56,875,4400,2063
277.901721957,1785272332.3012831,274.0,robust,tracking,True,1.19,2.79,1.8128179501,1.7479069779,11.23,42.2,38.8,6,5,-15.46,669172.771,669187.748,110.557,-103.518,True,0.05,0.505,0.152,0,0.505,0.152,0,23,8.0,141.55,-1.539,-1.56,877,4410,2063
279.901270286,1785272334.3008313,276.0,robust,tracking,True,1.19,2.79,1.8127918148,1.7474239577,11.26,42.2,38.9,6,5,5.21,668986.416,668981.367,110.705,-103.667,True,0.05,0.415,0.13,0,0.415,0.13,0,23,56.0,164.7,-1.535,-1.56,879,4420,2063
281.911805396,1785272336.3113663,278.0,robust,tracking,True,1.19,2.78,1.812764459,1.7469430861,11.14,42.3,38.6,6,5,9.2,668783.357,668774.443,110.765,-103.653,True,0.05,0.976,0.047,0,0.976,0.047,0,23,40.0,167.02,-1.535,-1.56,881,4430,2063
283.904627638,1785272338.3041887,280.0,robust,tracking,True,1.19,2.78,1.8127364977,1.7464643722,11.13,42.4,38.9,6,5,1.9,668567.589,668565.747,110.702,-104.554,True,0.05,0.496,0.084,0,0.496,0.084,0,23,56.0,150.73,-1.537,-1.56,883,4440,2063
285.904540282,1785272340.3041015,282.0,robust,tracking,True,1.19,2.78,1.8127111868,1.7459831701,11.26,42.2,38.2,6,5,-21.52,668336.38,668357.228,110.484,-104.5,True,0.05,0.4,0.023,0,0.4,0.023,0,23,16.0,127.92,-1.541,-1.56,885,4450,2063
287.903906607,1785272342.3034678,284.0,robust,tracking,True,1.19,2.78,1.8126838601,1.7455066962,11.09,42.3,39.2,6,5,-0.35,668145.403,668145.742,110.548,-104.863,True,0.05,0.13,0.213,0,0.13,0.213,0,23,0.0,156.5,-1.536,-1.56,887,4460,2063
289.903519609,1785272344.3030806,286.0,robust,tracking,True,1.19,2.78,1.8126545436,1.74502695,11.28,42.2,38.0,6,5,-17.14,667916.867,667933.474,110.923,-105.07,True,0.05,0.412,0.112,0,0.412,0.112,0,23,8.0,145.17,-1.538,-1.56,889,4470,2063
291.906994829,1785272346.3065557,288.0,robust,tracking,True,1.19,2.78,1.812627295,1.7445482935,12.84,42.5,38.6,6,5,-1.31,667721.551,667722.816,109.629,-106.134,True,0.05,0.721,0.164,0,0.721,0.164,0,23,40.0,158.89,-1.536,-1.56,891,4480,2063
293.904251457,1785272348.3038123,290.0,robust,tracking,True,1.19,2.78,1.8126009474,1.7440682755,11.16,42.3,38.2,6,5,14.39,667524.719,667510.777,110.381,-105.665,True,0.05,0.161,0.079,0,0.161,0.079,0,23,32.0,184.17,-1.532,-1.56,893,4490,2063
295.904732818,1785272350.3042939,292.0,robust,tracking,True,1.19,2.78,1.8125748376,1.7435915248,11.06,42.6,38.1,6,5,-9.72,667289.655,667299.068,110.248,-105.873,True,0.05,0.291,0.005,0,0.291,0.005,0,23,32.0,159.99,-1.536,-1.56,895,4500,2063
297.910797167,1785272352.3103583,294.0,robust,tracking,True,1.19,2.78,1.8125472744,1.7431147283,11.0,42.9,38.4,6,5,9.37,667097.136,667088.06,110.084,-106.181,True,0.05,0.237,0.074,0,0.237,0.074,0,23,16.0,168.11,-1.535,-1.56,897,4510,2063
299.905108443,1785272354.3046699,296.0,robust,tracking,True,1.19,2.78,1.812521259,1.7426375476,16.12,42.5,37.8,6,5,-12.01,666863.837,666875.472,110.096,-106.613,True,0.05,0.693,0.156,0,0.693,0.156,0,23,64.0,124.56,-1.541,-1.56,899,4520,2063
1 host_monotonic_s host_unix_s time_s profile mode locked tdop gdop hdop vdop clock_sigma_m cn0_mean_dbhz cn0_min_dbhz tracked_satellites used_satellites innovation_m raw_bias_m filtered_bias_m clock_drift_mps bias_drift_mps predictor_enabled eskf_alpha eskf_speed_mps eskf_velocity_rms_mps eskf_fde_mask pps_speed_mps pps_velocity_rms_mps pps_fde_mask reference_svid capture_correction_ns hardware_phase_ns rate_command_ppm integrator_ppm pulse_count sequence status
2 1.50420269 1785272055.903764 0.0 robust tracking True 1.25 2.86 1.8176601213 1.8176260703 12.54 42.7 37.7 6 5 4.52 693498.817 693494.442 110.788 -72.055 True 0.05 0.271 0.185 0 0.271 0.185 0 23 32.0 160.47 -1.536 -1.56 600 3040 2063
3 3.503689524 1785272057.9032507 2.0 robust tracking True 1.25 2.86 1.8176193953 1.8170996688 11.17 42.9 38.2 6 5 -19.2 693330.906 693349.504 111.201 -71.738 True 0.05 0.746 0.135 0 0.746 0.135 0 23 56.0 124.38 -1.541 -1.56 602 3050 2063
4 5.500479302 1785272059.9000404 4.0 robust tracking True 1.25 2.86 1.8175780522 1.8165687846 21.48 42.7 38.5 6 5 -7.0 693197.92 693204.698 111.206 -71.912 True 0.05 0.836 0.039 0 0.836 0.039 0 23 24.0 141.5 -1.539 -1.56 604 3060 2063
5 7.509988539 1785272061.9095497 6.0 robust tracking True 1.25 2.86 1.8175381286 1.816041859 11.14 43.0 38.3 6 5 13.6 693072.274 693059.102 111.221 -71.825 True 0.05 1.009 0.28 0 1.009 0.28 0 23 32.0 168.11 -1.535 -1.56 606 3070 2063
6 9.511653187 1785272063.9112144 8.0 robust tracking True 1.25 2.86 1.8174962624 1.8155120506 11.56 42.9 38.0 6 5 -31.15 692881.989 692912.165 110.864 -72.861 True 0.05 0.371 0.04 0 0.371 0.04 0 23 40.0 135.63 -1.539 -1.56 608 3080 2063
7 11.50378733 1785272065.9033487 10.0 robust tracking True 1.25 2.85 1.8174539102 1.8149874373 11.07 42.8 38.6 6 5 -9.78 692754.373 692763.848 110.014 -73.882 True 0.05 0.843 0.004 0 0.843 0.004 0 23 56.0 155.42 -1.536 -1.56 610 3090 2063
8 13.504178648 1785272067.9037397 12.0 robust tracking True 1.25 2.85 1.817413463 1.8144599125 11.18 42.9 38.1 6 5 6.48 692623.758 692617.477 110.329 -73.94 True 0.05 0.609 0.096 0 0.609 0.096 0 23 0.0 162.66 -1.535 -1.56 612 3100 2063
9 15.503554083 1785272069.9031153 14.0 robust tracking True 1.25 2.85 1.8173718635 1.8139319104 11.26 42.7 37.0 6 5 -1.31 692469.561 692470.834 111.54 -72.849 True 0.05 0.953 0.31 0 0.953 0.31 0 23 56.0 155.78 -1.536 -1.56 614 3110 2063
10 17.502924749 1785272071.9024858 16.0 robust tracking True 1.25 2.85 1.81733102 1.8134069522 15.4 42.4 36.5 6 5 -11.0 692313.987 692324.644 110.815 -73.581 True 0.05 0.736 0.168 0 0.736 0.168 0 23 32.0 133.05 -1.54 -1.56 616 3120 2063
11 19.506236288 1785272073.9057975 18.0 robust tracking True 1.24 2.85 1.8172902798 1.8128821788 11.11 43.1 38.1 6 5 -5.77 692171.911 692177.505 111.077 -73.779 True 0.05 0.4 0.027 0 0.4 0.027 0 23 24.0 131.86 -1.54 -1.56 618 3130 2063
12 21.504228726 1785272075.9037895 20.0 robust tracking True 1.24 2.85 1.8172490099 1.8123554135 11.15 43.0 38.0 6 5 8.57 692038.349 692030.045 110.612 -74.424 True 0.05 0.512 0.053 0 0.512 0.053 0 23 24.0 156.87 -1.536 -1.56 620 3140 2063
13 23.504153615 1785272077.903715 22.0 robust tracking True 1.24 2.85 1.8172104392 1.8118290291 11.3 42.6 37.7 6 5 -11.79 691868.479 691879.901 111.534 -73.806 True 0.05 0.721 0.036 0 0.721 0.036 0 23 56.0 142.52 -1.538 -1.56 622 3150 2063
14 25.510000609 1785272079.9095619 24.0 robust tracking True 1.24 2.85 1.8171689199 1.8113038519 11.08 43.1 38.1 6 5 -12.44 691719.345 691731.399 110.942 -74.863 True 0.05 0.359 0.101 0 0.359 0.101 0 23 48.0 145.5 -1.538 -1.56 624 3160 2063
15 27.50517151 1785272081.904733 26.0 robust tracking True 1.24 2.85 1.8171301373 1.8107796086 11.26 42.7 37.5 6 5 -18.16 691564.145 691581.742 111.5 -74.479 True 0.05 0.745 0.096 0 0.745 0.096 0 23 8.0 129.02 -1.54 -1.56 626 3170 2063
16 29.502641745 1785272083.9022028 28.0 robust tracking True 1.24 2.85 1.8170880804 1.8102547189 12.41 43.0 37.5 6 5 1.28 691433.115 691431.876 111.148 -74.903 True 0.05 0.332 0.115 0 0.332 0.115 0 23 0.0 127.84 -1.541 -1.56 628 3180 2063
17 31.500642933 1785272085.900204 30.0 robust tracking True 1.24 2.85 1.8170480561 1.8097326484 14.78 42.6 37.5 6 5 -1.39 691280.067 691281.411 111.681 -74.941 True 0.05 0.503 0.255 0 0.503 0.255 0 23 48.0 147.24 -1.538 -1.56 630 3190 2063
18 33.504307045 1785272087.903868 32.0 robust tracking True 1.24 2.85 1.8170082113 1.8092081529 22.56 42.9 38.4 6 5 -13.17 691116.408 691129.168 110.696 -75.765 True 0.05 0.206 0.027 0 0.206 0.027 0 23 16.0 132.81 -1.54 -1.56 632 3200 2063
19 35.509887989 1785272089.9094489 34.0 robust tracking True 1.24 2.85 1.8169688875 1.8086870549 11.18 42.9 38.0 6 5 -0.84 690974.741 690975.554 111.343 -75.376 True 0.05 0.636 0.055 0 0.636 0.055 0 23 56.0 156.39 -1.536 -1.56 634 3210 2063
20 37.506370892 1785272091.9059322 36.0 robust tracking True 1.24 2.85 1.8169289218 1.8081636846 10.95 43.4 39.5 6 5 -3.76 690819.152 690822.79 110.566 -76.732 True 0.05 0.72 0.058 0 0.72 0.058 0 23 32.0 156.77 -1.536 -1.56 636 3220 2063
21 39.504571912 1785272093.904133 38.0 robust tracking True 1.24 2.85 1.8168882102 1.8076411645 11.05 43.1 38.3 6 5 -8.88 690659.561 690668.166 111.385 -76.264 True 0.05 0.403 0.084 0 0.403 0.084 0 23 48.0 147.43 -1.538 -1.56 638 3230 2063
22 41.502657038 1785272095.9022183 40.0 robust tracking True 1.24 2.85 1.8168471874 1.8071184407 11.13 42.7 38.1 6 5 5.56 690520.399 690515.009 111.68 -75.754 True 0.05 0.957 0.102 0 0.957 0.102 0 23 32.0 147.4 -1.538 -1.56 640 3240 2063
23 43.50032369 1785272097.8998854 42.0 robust tracking True 1.24 2.85 1.816809289 1.8065980348 11.21 42.7 37.8 6 5 9.98 690370.621 690360.952 111.268 -76.081 True 0.05 0.807 0.041 0 0.807 0.041 0 23 0.0 136.35 -1.539 -1.56 642 3250 2063
24 45.712022504 1785272100.111584 44.0 robust tracking True 1.24 2.85 1.8167660234 1.8060226878 12.45 43.2 38.0 6 5 -7.85 690184.849 690192.456 111.097 -76.306 True 0.05 0.794 0.037 0 0.794 0.037 0 23 32.0 142.31 -1.538 -1.56 645 3260 2063
25 47.704670721 1785272102.1042318 46.0 robust tracking True 1.24 2.85 1.8167259917 1.8055025786 11.59 42.9 38.7 6 5 -14.42 690023.356 690037.324 110.386 -77.903 True 0.05 0.84 0.099 0 0.84 0.099 0 23 56.0 136.57 -1.539 -1.56 647 3270 2063
26 49.704359089 1785272104.1039205 48.0 robust tracking True 1.24 2.84 1.8166840843 1.804981981 11.04 43.4 38.2 6 5 -2.28 689880.716 689882.925 110.905 -78.019 True 0.05 0.509 0.107 0 0.509 0.107 0 23 16.0 148.6 -1.537 -1.56 649 3280 2063
27 51.709139916 1785272106.1087012 50.0 robust tracking True 1.24 2.84 1.8166456009 1.8044613748 10.99 43.3 38.1 6 5 -0.68 689726.579 689727.233 111.064 -77.798 True 0.05 0.094 0.103 0 0.094 0.103 0 23 48.0 144.29 -1.538 -1.56 651 3290 2063
28 53.703365655 1785272108.1029265 52.0 robust tracking True 1.24 2.84 1.8166083592 1.8039440331 12.68 42.8 37.5 6 5 -8.72 689564.342 689572.793 110.533 -78.338 True 0.05 0.504 0.187 0 0.504 0.187 0 23 8.0 148.54 -1.537 -1.56 653 3300 2063
29 55.700643307 1785272110.100204 54.0 robust tracking True 1.24 2.84 1.8165672875 1.8034221236 11.07 42.9 38.7 6 5 4.12 689421.398 689417.403 111.098 -77.598 True 0.05 0.645 0.08 0 0.645 0.08 0 23 40.0 140.91 -1.539 -1.56 655 3310 2063
30 57.904055806 1785272112.3036168 56.0 robust tracking True 1.24 2.84 1.8165247981 1.8028524558 16.67 42.9 38.2 6 5 -1.04 689243.202 689244.211 110.99 -78.254 True 0.05 0.342 0.02 0 0.342 0.02 0 23 48.0 144.27 -1.538 -1.56 657 3320 2063
31 60.10451733 1785272114.5040786 58.0 robust tracking True 1.24 2.84 1.8164827704 1.8022817921 11.19 42.9 38.0 6 5 -5.64 689066.66 689072.123 111.676 -78.014 True 0.05 0.73 0.15 0 0.73 0.15 0 23 48.0 126.86 -1.541 -1.56 659 3330 2063
32 62.108759952 1785272116.5083213 60.0 robust tracking True 1.24 2.84 1.8164435096 1.8017644864 16.38 43.1 39.2 6 5 -1.09 688913.112 688914.171 111.17 -78.316 True 0.05 0.696 0.007 0 0.696 0.007 0 23 24.0 153.74 -1.537 -1.56 661 3340 2063
33 64.300493137 1785272118.7000537 62.0 robust tracking True 1.24 2.84 1.8163984438 1.8011921234 11.32 42.4 37.4 6 5 8.12 688749.473 688741.606 110.452 -79.51 True 0.05 0.546 0.16 0 0.546 0.16 0 23 40.0 152.97 -1.537 -1.56 663 3350 2063
34 66.510932531 1785272120.9104934 64.0 robust tracking True 1.23 2.84 1.8163566675 1.8006252849 20.15 42.7 38.3 6 5 -2.84 688563.016 688565.769 110.92 -78.813 True 0.05 0.795 0.197 0 0.795 0.197 0 23 40.0 172.22 -1.535 -1.561 665 3360 2063
35 68.704372014 1785272123.103933 66.0 robust tracking True 1.23 2.84 1.8163145194 1.8000555234 15.52 42.8 38.5 6 5 -11.85 688380.019 688391.495 110.061 -80.47 True 0.05 0.796 0.074 0 0.796 0.074 0 23 48.0 155.17 -1.537 -1.56 668 3370 2063
36 70.703104898 1785272125.102666 68.0 robust tracking True 1.23 2.84 1.8162769265 1.7995395272 11.43 42.0 37.8 6 5 13.98 688244.124 688230.58 110.609 -80.264 True 0.05 0.381 0.125 0 0.381 0.125 0 23 16.0 179.45 -1.533 -1.56 670 3380 2063
37 72.702223062 1785272127.101784 70.0 robust tracking True 1.23 2.84 1.8162378878 1.7990227767 16.58 42.8 38.9 6 5 5.88 688078.671 688072.971 110.939 -80.36 True 0.05 0.666 0.053 0 0.666 0.053 0 23 24.0 166.76 -1.535 -1.56 672 3390 2063
38 74.700286036 1785272129.0998468 72.0 robust tracking True 1.23 2.84 1.816199267 1.7985063001 11.1 42.7 38.7 6 5 14.57 687926.671 687912.557 110.527 -80.516 True 0.05 0.312 0.03 0 0.312 0.03 0 23 32.0 162.81 -1.536 -1.56 674 3400 2063
39 76.71281947 1785272131.1123805 74.0 robust tracking True 1.23 2.84 1.8161600375 1.7979893506 11.11 42.7 38.6 6 5 -1.67 687750.089 687751.702 111.576 -79.6 True 0.05 1.026 0.083 0 1.026 0.083 0 23 16.0 142.71 -1.539 -1.56 676 3410 2063
40 78.704243128 1785272133.103804 76.0 robust tracking True 1.23 2.84 1.8161215432 1.7974747243 11.08 43.2 38.1 6 5 -4.48 687586.716 687591.059 110.819 -81.044 True 0.05 0.676 0.104 0 0.676 0.104 0 23 0.0 107.47 -1.544 -1.56 678 3420 2063
41 80.70401895 1785272135.10358 78.0 robust tracking True 1.23 2.84 1.8160831759 1.7969606981 20.64 42.3 37.5 6 5 -0.58 687429.447 687430.01 110.975 -81.135 True 0.05 0.351 0.106 0 0.351 0.106 0 23 56.0 161.62 -1.536 -1.56 680 3430 2063
42 82.710133903 1785272137.1096952 80.0 robust tracking True 1.23 2.84 1.8160471938 1.7964462492 21.18 43.0 39.1 6 5 11.46 687279.439 687268.334 110.976 -80.409 True 0.05 1.15 0.031 0 1.15 0.031 0 23 0.0 177.64 -1.534 -1.56 682 3440 2063
43 84.702669179 1785272139.1022303 82.0 robust tracking True 1.23 2.84 1.8160084174 1.7959315494 16.16 42.7 39.0 6 5 2.07 687108.261 687106.252 110.31 -81.912 True 0.05 0.401 0.04 0 0.401 0.04 0 23 48.0 145.45 -1.539 -1.56 684 3450 2063
44 86.700732144 1785272141.100293 84.0 robust tracking True 1.23 2.83 1.8159713938 1.7954167669 11.13 42.7 39.0 6 5 -12.66 686929.679 686941.939 110.952 -81.183 True 0.05 0.726 0.196 0 0.726 0.196 0 23 24.0 139.84 -1.539 -1.56 686 3460 2063
45 88.704090128 1785272143.103651 86.0 robust tracking True 1.23 2.83 1.8159335443 1.79490275 16.21 42.7 38.7 6 5 8.63 686784.906 686776.542 110.694 -82.024 True 0.05 0.209 0.052 0 0.209 0.052 0 23 48.0 176.01 -1.534 -1.56 688 3470 2063
46 90.704048278 1785272145.103609 88.0 robust tracking True 1.23 2.83 1.8158959585 1.7943905432 24.91 42.5 37.9 6 5 -0.15 686613.039 686613.185 110.563 -82.135 True 0.05 0.379 0.123 0 0.379 0.123 0 23 40.0 161.5 -1.536 -1.56 690 3480 2063
47 92.711508414 1785272147.1110694 90.0 robust tracking True 1.23 2.83 1.8158570425 1.793877413 24.43 42.7 38.0 6 5 15.4 686465.242 686450.326 111.554 -81.637 True 0.05 1.011 0.184 0 1.011 0.184 0 23 56.0 178.15 -1.534 -1.56 692 3490 2063
48 94.704749369 1785272149.1043103 92.0 robust tracking True 1.23 2.83 1.8158190794 1.7933647389 21.48 42.4 38.5 6 5 16.84 686303.4 686287.088 110.534 -82.726 True 0.05 0.195 0.029 0 0.195 0.029 0 23 48.0 171.95 -1.535 -1.56 694 3500 2063
49 96.703104991 1785272151.1026661 94.0 robust tracking True 1.23 2.83 1.815781917 1.7928524573 11.31 42.4 37.6 6 5 -1.05 686119.874 686120.892 109.749 -83.836 True 0.05 0.919 0.158 0 0.919 0.158 0 23 16.0 137.68 -1.54 -1.56 696 3510 2063
50 98.915123597 1785272153.3146844 96.0 robust tracking True 1.23 2.83 1.8157417458 1.792292301 11.49 42.2 37.0 6 5 -4.81 685933.786 685938.441 110.18 -83.485 True 0.05 0.687 0.188 0 0.687 0.188 0 23 40.0 130.24 -1.541 -1.56 698 3520 2063
51 101.104184426 1785272155.5037456 98.0 robust tracking True 1.23 2.83 1.8157012539 1.7917269932 14.98 42.4 37.7 6 5 -4.93 685751.271 685756.046 110.623 -83.514 True 0.05 0.285 0.185 0 0.285 0.185 0 23 40.0 144.59 -1.539 -1.56 700 3530 2063
52 103.108802454 1785272157.5083635 100.0 robust tracking True 1.23 2.83 1.8156626947 1.7912163488 25.23 42.4 38.0 6 5 8.11 685596.218 685588.366 110.401 -83.817 True 0.05 0.362 0.053 0 0.362 0.053 0 23 56.0 172.28 -1.535 -1.56 702 3540 2063
53 105.105464086 1785272159.505025 102.0 robust tracking True 1.23 2.83 1.8156280022 1.79070636 16.51 42.5 37.5 6 5 -15.09 685403.733 685418.356 110.792 -83.601 True 0.05 0.365 0.1 0 0.365 0.1 0 23 48.0 134.38 -1.54 -1.56 704 3550 2063
54 107.300312593 1785272161.6998734 104.0 robust tracking True 1.23 2.83 1.8155869207 1.790143438 20.41 42.6 38.3 6 5 4.06 685236.104 685232.167 111.771 -83.119 True 0.05 1.06 0.172 0 1.06 0.172 0 23 56.0 186.11 -1.532 -1.56 706 3560 2063
55 109.312626871 1785272163.7121878 106.0 robust tracking True 1.23 2.83 1.8155513007 1.7896363278 15.58 42.6 37.5 6 5 -5.9 685055.343 685061.062 110.302 -84.628 True 0.05 0.319 0.208 0 0.319 0.208 0 23 24.0 168.36 -1.535 -1.56 708 3570 2063
56 111.306453913 1785272165.7060149 108.0 robust tracking True 1.23 2.83 1.8155128487 1.789126556 21.63 42.6 38.0 6 5 16.48 684906.901 684890.938 110.502 -84.943 True 0.05 0.385 0.093 0 0.385 0.093 0 23 16.0 177.8 -1.534 -1.56 710 3580 2063
57 113.305323246 1785272167.7048843 110.0 robust tracking True 1.22 2.83 1.8154776515 1.7886176858 11.1 42.9 38.4 6 5 1.51 684721.855 684720.397 110.396 -84.747 True 0.05 0.197 0.218 0 0.197 0.218 0 23 32.0 163.81 -1.536 -1.56 712 3590 2063
58 115.30411121 1785272169.7036724 112.0 robust tracking True 1.22 2.83 1.8154394251 1.7881082833 22.64 43.1 38.0 6 5 -3.4 684547.463 684550.752 110.44 -85.241 True 0.05 0.429 0.029 0 0.429 0.029 0 23 8.0 152.36 -1.538 -1.56 714 3600 2063
59 117.303101565 1785272171.702663 114.0 robust tracking True 1.22 2.83 1.8154034756 1.7875997643 11.52 42.6 38.1 6 5 17.65 684398.444 684381.346 110.707 -84.967 True 0.05 0.405 0.059 0 0.405 0.059 0 23 24.0 192.86 -1.532 -1.56 716 3610 2063
60 119.306150448 1785272173.7057114 116.0 robust tracking True 1.22 2.83 1.8153676614 1.7870927007 12.04 42.5 37.5 6 5 -7.99 684203.354 684211.094 110.164 -85.459 True 0.05 0.631 0.155 0 0.631 0.155 0 23 32.0 144.3 -1.539 -1.56 718 3620 2063
61 121.300962576 1785272175.7005236 118.0 robust tracking True 1.22 2.83 1.8153303586 1.786584068 18.38 42.4 37.2 6 5 15.28 684055.675 684040.872 110.141 -86.063 True 0.05 0.481 0.133 0 0.481 0.133 0 23 48.0 200.68 -1.53 -1.56 720 3630 2063
62 123.303755452 1785272177.7033165 120.0 robust tracking True 1.22 2.82 1.815294335 1.7860767164 19.1 42.8 38.8 6 5 2.02 683871.81 683869.856 111.056 -85.022 True 0.05 1.151 0.085 0 1.151 0.085 0 23 64.0 169.36 -1.535 -1.56 722 3640 2063
63 125.304219262 1785272179.703781 122.0 robust tracking True 1.22 2.82 1.8152578155 1.785570688 11.11 42.7 38.6 6 5 -8.82 683688.289 683696.831 110.802 -85.693 True 0.05 0.637 0.051 0 0.637 0.051 0 23 8.0 158.16 -1.537 -1.56 724 3650 2063
64 127.304583545 1785272181.704145 124.0 robust tracking True 1.22 2.82 1.8152215994 1.7850628719 11.13 42.7 38.8 6 5 -1.48 683523.082 683524.519 110.235 -86.917 True 0.05 0.775 0.068 0 0.775 0.068 0 23 40.0 151.87 -1.538 -1.56 726 3660 2063
65 129.508905566 1785272183.9084663 126.0 robust tracking True 1.22 2.82 1.8151838123 1.7845085023 12.03 43.1 38.6 6 5 9.01 683342.021 683333.296 110.201 -87.019 True 0.05 0.331 0.094 0 0.331 0.094 0 23 0.0 157.45 -1.537 -1.56 728 3670 2063
66 131.500762179 1785272185.9003232 128.0 robust tracking True 1.22 2.82 1.8151487619 1.7840006032 12.83 42.6 38.6 6 5 3.63 683160.888 683157.371 110.357 -86.937 True 0.05 0.389 0.075 0 0.389 0.075 0 23 32.0 171.39 -1.535 -1.56 730 3680 2063
67 133.504344505 1785272187.9039054 130.0 robust tracking True 1.22 2.82 1.8151127862 1.7834955606 12.64 42.7 38.5 6 5 3.69 682986.797 682983.217 110.053 -87.63 True 0.05 0.791 0.01 0 0.791 0.01 0 23 0.0 175.65 -1.534 -1.56 732 3690 2063
68 135.509832366 1785272189.9094036 132.0 robust tracking True 1.22 2.82 1.8150762158 1.7829890753 11.13 42.7 38.4 6 5 5.1 682813.848 682808.909 110.531 -87.346 True 0.05 0.079 0.034 0 0.079 0.034 0 23 16.0 162.81 -1.536 -1.56 734 3700 2063
69 137.506720578 1785272191.9062817 134.0 robust tracking True 1.22 2.82 1.8150397064 1.7824837171 10.99 42.8 39.5 6 5 1.42 682635.318 682633.946 110.584 -87.635 True 0.05 0.038 0.097 0 0.038 0.097 0 23 32.0 155.09 -1.537 -1.56 736 3710 2063
70 139.504410571 1785272193.9039717 136.0 robust tracking True 1.22 2.82 1.8150041072 1.7819780719 11.15 42.7 38.1 6 5 -6.72 682453.853 682460.364 110.886 -87.452 True 0.05 0.412 0.407 0 0.412 0.407 0 23 48.0 139.96 -1.539 -1.56 738 3720 2063
71 141.503078598 1785272195.9026399 138.0 robust tracking True 1.22 2.82 1.8149699154 1.781475266 11.11 42.7 38.8 6 5 11.45 682296.086 682284.991 109.638 -89.46 True 0.05 1.419 0.005 0 1.419 0.005 0 23 40.0 167.17 -1.535 -1.56 740 3730 2063
72 143.500493916 1785272197.900055 140.0 robust tracking True 1.22 2.82 1.8149354252 1.7809734693 11.02 42.9 38.7 6 5 12.89 682120.284 682107.799 110.911 -87.736 True 0.05 0.88 0.067 0 0.88 0.067 0 23 56.0 162.67 -1.536 -1.56 742 3740 2063
73 145.512927625 1785272199.9124885 142.0 robust tracking True 1.22 2.82 1.8149005029 1.7804693538 10.96 42.9 39.4 6 5 -25.89 681906.117 681931.202 110.881 -87.866 True 0.05 0.775 0.078 0 0.775 0.078 0 23 8.0 107.83 -1.544 -1.56 744 3750 2063
74 147.506278297 1785272201.9058392 144.0 robust tracking True 1.22 2.82 1.8148661739 1.7799667491 10.98 43.0 39.0 6 5 -5.66 681746.195 681751.678 109.788 -89.613 True 0.05 1.009 0.035 0 1.009 0.035 0 23 56.0 152.27 -1.538 -1.56 746 3760 2063
75 149.505513865 1785272203.9050748 146.0 robust tracking True 1.22 2.82 1.8148312963 1.779461219 11.25 42.4 38.4 6 5 -7.95 681563.852 681571.554 110.854 -89.368 True 0.05 0.482 0.099 0 0.482 0.099 0 23 56.0 142.08 -1.539 -1.56 748 3770 2063
76 151.504103133 1785272205.903664 148.0 robust tracking True 1.22 2.82 1.8147976667 1.778959756 10.98 43.0 39.4 6 5 11.06 681403.38 681392.662 110.425 -89.358 True 0.05 0.199 0.058 0 0.199 0.058 0 23 16.0 161.29 -1.536 -1.56 750 3780 2063
77 153.502847324 1785272207.9024084 150.0 robust tracking True 1.22 2.82 1.8147608671 1.7784571849 15.23 42.6 38.9 6 5 9.66 681223.637 681214.276 110.279 -89.721 True 0.05 0.371 0.218 0 0.371 0.218 0 23 48.0 172.12 -1.535 -1.56 752 3790 2063
78 155.506297511 1785272209.9058583 152.0 robust tracking True 1.22 2.82 1.8147266277 1.7779565566 11.07 42.6 38.7 6 5 0.35 681034.417 681034.073 110.263 -89.772 True 0.05 0.371 0.13 0 0.371 0.13 0 23 56.0 180.04 -1.533 -1.56 754 3800 2063
79 157.500753602 1785272211.9003143 154.0 robust tracking True 1.22 2.82 1.814692541 1.777454151 11.13 42.4 38.4 6 5 8.94 680862.299 680853.639 110.737 -89.433 True 0.05 0.755 0.053 0 0.755 0.053 0 23 0.0 180.5 -1.533 -1.56 756 3810 2063
80 159.504450759 1785272213.904012 156.0 robust tracking True 1.22 2.82 1.8146585955 1.7769539906 11.22 42.3 38.2 6 5 14.47 680691.31 680677.294 110.442 -90.141 True 0.05 0.405 0.02 0 0.405 0.02 0 23 32.0 196.03 -1.531 -1.56 758 3820 2063
81 161.510423884 1785272215.9099848 158.0 robust tracking True 1.21 2.82 1.8146253251 1.7764523761 14.07 42.3 38.6 6 5 14.17 680511.698 680497.972 110.887 -89.977 True 0.05 0.551 0.182 0 0.551 0.182 0 23 32.0 184.51 -1.532 -1.56 760 3830 2063
82 163.504960681 1785272217.9045217 160.0 robust tracking True 1.21 2.81 1.8145907816 1.7759535814 10.99 42.6 39.7 6 5 9.57 680326.822 680317.554 110.339 -91.079 True 0.05 0.465 0.01 0 0.465 0.01 0 23 8.0 150.36 -1.538 -1.56 762 3840 2063
83 165.507220868 1785272219.9067822 162.0 robust tracking True 1.21 2.81 1.8145551226 1.7754518476 10.98 42.8 38.9 6 5 5.13 680143.185 680138.216 109.975 -91.641 True 0.05 0.834 0.034 0 0.834 0.034 0 23 0.0 144.58 -1.539 -1.56 764 3850 2063
84 167.503525545 1785272221.9030867 164.0 robust tracking True 1.21 2.81 1.8145228906 1.7749543943 14.36 42.8 38.7 6 5 4.72 679959.278 679954.706 110.179 -91.65 True 0.05 0.706 0.085 0 0.706 0.085 0 23 48.0 124.01 -1.542 -1.56 766 3860 2063
85 169.50061802 1785272223.900179 166.0 robust tracking True 1.21 2.81 1.8144898435 1.7744531221 11.09 42.8 37.7 6 5 13.57 679787.48 679774.331 110.775 -91.606 True 0.05 1.106 0.075 0 1.106 0.075 0 23 56.0 145.18 -1.538 -1.56 768 3870 2063
86 171.510388267 1785272225.9099493 168.0 robust tracking True 1.21 2.81 1.8144559568 1.7739550407 11.08 42.6 38.2 6 5 8.28 679599.18 679591.156 110.344 -91.505 True 0.05 0.285 0.062 0 0.285 0.062 0 23 56.0 137.18 -1.54 -1.56 770 3880 2063
87 173.500668495 1785272227.9002295 170.0 robust tracking True 1.21 2.81 1.8144213808 1.7734561125 11.07 42.5 38.0 6 5 -7.05 679401.0 679407.83 110.355 -91.896 True 0.05 0.239 0.023 0 0.239 0.023 0 23 48.0 151.22 -1.538 -1.56 772 3890 2063
88 175.509072834 1785272229.9086337 172.0 robust tracking True 1.21 2.81 1.8143875329 1.7729571416 14.88 42.6 38.0 6 5 -9.75 679214.759 679224.203 110.051 -92.203 True 0.05 0.388 0.024 0 0.388 0.024 0 23 16.0 127.04 -1.541 -1.56 774 3900 2063
89 177.511375431 1785272231.9109368 174.0 robust tracking True 1.21 2.81 1.8143541178 1.7724598606 11.01 42.7 39.1 6 5 -28.57 679010.25 679037.93 110.131 -92.548 True 0.05 0.679 0.066 0 0.679 0.066 0 23 16.0 144.09 -1.539 -1.56 776 3910 2063
90 179.5031023 1785272233.9026635 176.0 robust tracking True 1.21 2.81 1.8143216536 1.7719632746 11.04 42.6 38.1 6 5 -1.94 678850.304 678852.181 110.443 -92.232 True 0.05 0.439 0.164 0 0.439 0.164 0 23 24.0 156.61 -1.537 -1.56 778 3920 2063
91 181.500919559 1785272235.9004805 178.0 robust tracking True 1.21 2.81 1.8142885244 1.7714647145 11.67 42.7 39.6 6 5 14.37 678680.998 678667.079 110.223 -93.101 True 0.05 0.927 0.048 0 0.927 0.048 0 23 24.0 174.87 -1.534 -1.56 780 3930 2063
92 183.504154069 1785272237.903716 180.0 robust tracking True 1.21 2.81 1.8142515963 1.7709676955 11.12 42.4 38.5 6 5 3.52 678484.103 678480.688 110.013 -93.55 True 0.05 0.69 0.056 0 0.69 0.056 0 23 0.0 167.43 -1.535 -1.56 782 3940 2063
93 185.509701079 1785272239.9092622 182.0 robust tracking True 1.21 2.81 1.8142225906 1.7704719824 11.12 42.5 38.1 6 5 2.8 678298.275 678295.56 110.36 -92.989 True 0.05 0.188 0.156 0 0.188 0.156 0 23 0.0 175.14 -1.534 -1.56 784 3950 2063
94 187.511311613 1785272241.9108725 184.0 robust tracking True 1.21 2.81 1.8141898144 1.7699731912 11.11 42.3 38.5 6 5 -16.24 678094.632 678110.363 110.845 -93.036 True 0.05 0.46 0.009 0 0.46 0.009 0 23 32.0 150.91 -1.537 -1.56 786 3960 2063
95 189.504094586 1785272243.9036555 186.0 robust tracking True 1.21 2.81 1.8141560815 1.7694787336 11.13 42.5 37.9 6 5 -1.22 677924.284 677925.468 110.012 -93.838 True 0.05 0.542 0.031 0 0.542 0.031 0 23 32.0 167.75 -1.535 -1.56 788 3970 2063
96 191.503264307 1785272245.902825 188.0 robust tracking True 1.21 2.81 1.8141223587 1.7689845528 11.12 42.4 38.3 6 5 12.44 677751.751 677739.703 110.673 -93.502 True 0.05 0.691 0.123 0 0.691 0.123 0 23 32.0 181.14 -1.533 -1.56 790 3980 2063
97 193.500850983 1785272247.9004118 190.0 robust tracking True 1.21 2.81 1.8140919959 1.768488668 14.03 42.3 38.2 6 5 8.52 677559.555 677551.298 110.495 -93.954 True 0.05 0.376 0.004 0 0.376 0.004 0 23 56.0 155.05 -1.537 -1.56 792 3990 2063
98 195.507436225 1785272249.9069972 192.0 robust tracking True 1.21 2.81 1.8140572992 1.7679932608 10.97 42.9 38.9 6 5 -25.61 677336.289 677361.101 110.053 -94.87 True 0.05 0.565 0.132 0 0.565 0.132 0 23 0.0 143.1 -1.539 -1.56 794 4000 2063
99 197.710664279 1785272252.1102252 194.0 robust tracking True 1.21 2.81 1.8140224199 1.7674512561 11.06 42.7 38.3 6 5 -21.21 677131.244 677151.796 110.436 -94.668 True 0.05 0.467 0.099 0 0.467 0.099 0 23 16.0 127.66 -1.541 -1.56 797 4010 2063
100 199.904494084 1785272254.3040552 196.0 robust tracking True 1.21 2.81 1.8139878446 1.7669062479 11.21 42.4 37.9 6 5 2.5 676945.184 676942.759 110.719 -94.592 True 0.05 0.329 0.254 0 0.329 0.254 0 23 0.0 166.66 -1.535 -1.56 799 4020 2063
101 201.902748265 1785272256.3023093 198.0 robust tracking True 1.21 2.8 1.8139551469 1.7664122598 19.71 42.7 38.2 6 5 3.44 676756.483 676753.152 110.284 -95.508 True 0.05 0.834 0.075 0 0.834 0.075 0 23 48.0 159.1 -1.536 -1.56 801 4030 2063
102 203.900860006 1785272258.3004212 200.0 robust tracking True 1.21 2.8 1.8139243484 1.7659203155 21.27 42.6 37.9 6 5 16.7 676577.254 676561.076 111.005 -94.638 True 0.05 0.65 0.026 0 0.65 0.026 0 23 40.0 185.48 -1.532 -1.56 803 4040 2063
103 205.904311168 1785272260.303872 202.0 robust tracking True 1.21 2.8 1.8138926701 1.7654272361 11.23 42.3 37.5 6 5 -4.45 676364.571 676368.878 111.007 -94.853 True 0.05 0.681 0.154 0 0.681 0.154 0 23 32.0 146.48 -1.538 -1.56 805 4050 2063
104 207.911054786 1785272262.3106158 204.0 robust tracking True 1.21 2.8 1.8138607249 1.7649342967 11.29 42.2 37.1 6 5 -0.42 676178.038 676178.445 111.268 -95.121 True 0.05 0.669 0.187 0 0.669 0.187 0 23 56.0 148.37 -1.538 -1.56 807 4060 2063
105 209.904852911 1785272264.304414 206.0 robust tracking True 1.21 2.8 1.8138277745 1.7644418155 17.32 42.6 38.4 6 5 5.29 675993.222 675988.099 110.799 -95.578 True 0.05 0.772 0.012 0 0.772 0.012 0 23 48.0 167.53 -1.535 -1.56 809 4070 2063
106 211.904389957 1785272266.3039508 208.0 robust tracking True 1.2 2.8 1.8137949136 1.7639490637 14.88 42.5 38.1 6 5 4.76 675802.554 675797.942 109.97 -97.009 True 0.05 1.186 0.157 0 1.186 0.157 0 23 16.0 161.37 -1.536 -1.56 811 4080 2063
107 213.908887407 1785272268.3084483 210.0 robust tracking True 1.2 2.8 1.8137652527 1.7634585771 13.65 41.9 38.5 6 5 -19.97 675583.7 675603.041 110.734 -96.484 True 0.05 0.11 0.069 0 0.11 0.069 0 23 8.0 134.36 -1.54 -1.56 813 4090 2063
108 215.900878981 1785272270.3004403 212.0 robust tracking True 1.2 2.8 1.8137346862 1.7629700707 13.14 42.4 39.0 6 5 1.39 675411.049 675409.707 110.477 -96.41 True 0.05 0.489 0.149 0 0.489 0.149 0 23 48.0 164.12 -1.535 -1.56 815 4100 2063
109 217.908715503 1785272272.3082767 214.0 robust tracking True 1.2 2.8 1.8137007679 1.7624770049 11.19 42.1 38.1 6 5 -3.51 675214.305 675217.703 110.227 -97.513 True 0.05 0.667 0.022 0 0.667 0.022 0 23 56.0 141.12 -1.539 -1.56 817 4110 2063
110 219.904589627 1785272274.3041506 216.0 robust tracking True 1.2 2.8 1.8136705307 1.7619846326 17.3 42.2 38.8 6 5 -14.02 675007.687 675021.273 110.312 -97.218 True 0.05 0.452 0.069 0 0.452 0.069 0 23 24.0 158.28 -1.536 -1.56 819 4120 2063
111 221.9039367 1785272276.3034978 218.0 robust tracking True 1.2 2.8 1.8136409999 1.7614933749 11.12 42.1 39.2 6 5 -3.83 674821.953 674825.66 111.06 -96.371 True 0.05 1.326 0.028 0 1.326 0.028 0 23 48.0 170.5 -1.534 -1.56 821 4130 2063
112 223.91008895 1785272278.3096502 220.0 robust tracking True 1.2 2.8 1.8136105372 1.7610043295 11.73 42.3 38.7 6 5 0.13 674630.412 674630.288 111.079 -97.301 True 0.05 0.408 0.148 0 0.408 0.148 0 23 56.0 157.28 -1.536 -1.56 823 4140 2063
113 225.902916967 1785272280.302478 222.0 robust tracking True 1.2 2.8 1.8135794516 1.7605142092 21.26 41.9 39.1 6 5 -21.67 674413.179 674434.176 111.065 -97.127 True 0.05 0.752 0.068 0 0.752 0.068 0 23 48.0 124.73 -1.541 -1.56 825 4150 2063
114 227.901755726 1785272282.3013165 224.0 robust tracking True 1.2 2.8 1.8135490001 1.7600248185 11.02 42.5 39.3 6 5 13.8 674252.975 674239.611 110.715 -97.719 True 0.05 0.748 0.078 0 0.748 0.078 0 23 32.0 159.45 -1.536 -1.56 827 4160 2063
115 229.901619429 1785272284.3011806 226.0 robust tracking True 1.2 2.8 1.8135177823 1.7595391681 11.05 42.4 39.1 6 5 12.06 674053.956 674042.275 110.355 -98.939 True 0.05 0.78 0.043 0 0.78 0.043 0 23 56.0 170.95 -1.534 -1.56 829 4170 2063
116 231.90481137 1785272286.3043725 228.0 robust tracking True 1.2 2.8 1.8134877896 1.7590492494 11.09 42.4 38.1 6 5 12.72 673856.473 673844.146 110.721 -98.388 True 0.05 0.538 0.188 0 0.538 0.188 0 23 48.0 170.54 -1.534 -1.56 831 4180 2063
117 233.905326912 1785272288.3048878 230.0 robust tracking True 1.2 2.8 1.8134570418 1.7585604429 11.06 42.5 38.9 6 5 5.44 673653.112 673647.84 110.098 -98.963 True 0.05 0.301 0.311 0 0.301 0.311 0 23 8.0 171.59 -1.534 -1.56 833 4190 2063
118 235.904458614 1785272290.3040195 232.0 robust tracking True 1.2 2.8 1.8134261204 1.7580735849 11.06 42.5 38.5 6 5 -4.62 673445.637 673450.117 110.459 -98.844 True 0.05 0.829 0.028 0 0.829 0.028 0 23 0.0 153.73 -1.537 -1.56 835 4200 2063
119 237.904071483 1785272292.3036325 234.0 robust tracking True 1.2 2.8 1.8133959704 1.7575868747 12.8 42.0 37.7 6 5 -7.87 673243.852 673251.478 110.772 -98.845 True 0.05 0.559 0.037 0 0.559 0.037 0 23 0.0 146.96 -1.538 -1.56 837 4210 2063
120 239.908760739 1785272294.3083217 236.0 robust tracking True 1.2 2.8 1.8133666091 1.7570976374 11.1 42.3 38.6 6 5 4.99 673058.134 673053.301 110.565 -99.434 True 0.05 0.506 0.016 0 0.506 0.016 0 23 24.0 168.65 -1.535 -1.56 839 4220 2063
121 241.900563008 1785272296.300124 238.0 robust tracking True 1.2 2.79 1.8133344617 1.7566107514 11.14 42.3 38.4 6 5 -14.57 672839.788 672853.907 110.365 -99.522 True 0.05 0.253 0.141 0 0.253 0.141 0 23 0.0 147.65 -1.538 -1.56 841 4230 2063
122 243.904645899 1785272298.3042068 240.0 robust tracking True 1.2 2.79 1.8133057846 1.7561227378 11.1 42.2 38.8 6 5 3.18 672658.12 672655.039 109.903 -100.274 True 0.05 0.476 0.033 0 0.476 0.033 0 23 32.0 164.64 -1.535 -1.56 843 4240 2063
123 245.904621193 1785272300.3041825 242.0 robust tracking True 1.2 2.79 1.8132777969 1.7556370904 11.14 42.2 38.7 6 5 -0.81 672454.836 672455.618 110.573 -100.187 True 0.05 0.405 0.013 0 0.405 0.013 0 23 16.0 134.41 -1.54 -1.56 845 4250 2063
124 247.899436713 1785272302.2989979 244.0 robust tracking True 1.2 2.79 1.8132463791 1.7551512789 11.16 42.2 39.0 6 5 13.48 672269.138 672256.082 110.183 -100.179 True 0.05 0.565 0.113 0 0.565 0.113 0 23 40.0 169.04 -1.535 -1.56 847 4260 2063
125 249.911369626 1785272304.3109307 246.0 robust tracking True 1.2 2.79 1.813217727 1.754665179 11.11 42.5 39.2 6 5 -6.09 672049.638 672055.536 109.774 -100.683 True 0.05 0.486 0.021 0 0.486 0.021 0 23 0.0 135.93 -1.539 -1.56 849 4270 2063
126 251.903384606 1785272306.3029459 248.0 robust tracking True 1.2 2.79 1.8131881994 1.7541830169 11.18 42.2 39.0 6 5 10.96 671865.57 671854.952 109.778 -101.09 True 0.05 0.278 0.027 0 0.278 0.027 0 23 48.0 182.36 -1.532 -1.56 851 4280 2063
127 253.900781619 1785272308.3003423 250.0 robust tracking True 1.2 2.79 1.8131589097 1.7536951988 11.17 42.3 38.9 6 5 3.55 671659.189 671655.749 110.155 -100.561 True 0.05 0.584 0.008 0 0.584 0.008 0 23 16.0 141.85 -1.539 -1.56 853 4290 2063
128 255.910281973 1785272310.3098433 252.0 robust tracking True 1.2 2.79 1.8131293194 1.753212297 11.2 42.1 38.4 6 5 7.57 671462.581 671455.25 109.95 -101.447 True 0.05 0.189 0.096 0 0.189 0.096 0 23 16.0 141.76 -1.539 -1.56 855 4300 2063
129 257.904754461 1785272312.3043153 254.0 robust tracking True 1.2 2.79 1.8131029159 1.7527265372 11.15 42.4 38.7 6 5 -6.95 671245.456 671252.194 110.05 -101.621 True 0.05 0.518 0.084 0 0.518 0.084 0 23 56.0 134.53 -1.54 -1.56 857 4310 2063
130 259.904898749 1785272314.3044596 256.0 robust tracking True 1.2 2.79 1.8130720433 1.7522445406 11.3 41.8 38.4 6 5 -4.74 671044.584 671049.173 110.162 -101.624 True 0.05 0.281 0.225 0 0.281 0.225 0 23 0.0 142.99 -1.539 -1.56 859 4320 2063
131 261.904207465 1785272316.3037684 258.0 robust tracking True 1.19 2.79 1.8130440645 1.7517582107 11.31 42.0 38.4 6 5 -7.29 670837.352 670844.416 110.701 -101.27 True 0.05 0.844 0.152 0 0.844 0.152 0 23 56.0 167.54 -1.535 -1.56 861 4330 2063
132 263.903658409 1785272318.3032198 260.0 robust tracking True 1.19 2.79 1.8130134772 1.7512773631 11.34 42.0 38.5 6 5 -1.16 670637.855 670638.976 110.001 -102.901 True 0.05 0.697 0.02 0 0.697 0.02 0 23 48.0 166.85 -1.535 -1.56 863 4340 2063
133 265.906678701 1785272320.3062396 262.0 robust tracking True 1.19 2.79 1.8129886444 1.7507938348 14.74 42.5 38.9 6 5 10.12 670442.306 670432.505 110.568 -101.927 True 0.05 0.908 0.068 0 0.908 0.068 0 23 40.0 192.58 -1.531 -1.56 865 4350 2063
134 267.908740259 1785272322.308301 264.0 robust tracking True 1.19 2.79 1.8129614176 1.750311896 11.27 42.3 37.7 6 5 -11.12 670213.856 670224.632 110.96 -102.072 True 0.05 0.648 0.323 0 0.648 0.323 0 23 56.0 163.77 -1.535 -1.56 867 4360 2063
135 269.904666955 1785272324.304228 266.0 robust tracking True 1.19 2.79 1.8129315339 1.7498285268 11.35 41.9 37.9 6 5 0.23 670016.459 670016.233 110.584 -102.496 True 0.05 0.457 0.073 0 0.457 0.073 0 23 48.0 200.12 -1.53 -1.56 869 4370 2063
136 271.904417064 1785272326.303978 268.0 robust tracking True 1.19 2.79 1.8129033974 1.7493462127 11.29 42.0 38.2 6 5 -11.63 669798.53 669809.801 110.923 -102.202 True 0.05 0.955 0.178 0 0.955 0.178 0 23 16.0 154.9 -1.537 -1.56 871 4380 2063
137 273.904287144 1785272328.3038483 270.0 robust tracking True 1.19 2.79 1.8128759146 1.7488645046 11.24 42.2 38.4 6 5 0.32 669604.755 669604.44 110.23 -103.356 True 0.05 0.559 0.043 0 0.559 0.043 0 23 56.0 147.74 -1.538 -1.56 873 4390 2063
138 275.905752343 1785272330.3053133 272.0 robust tracking True 1.19 2.79 1.8128467845 1.7483871309 11.29 42.1 38.0 6 5 0.52 669397.133 669396.627 110.767 -103.125 True 0.05 0.839 0.033 0 0.839 0.033 0 23 48.0 175.42 -1.533 -1.56 875 4400 2063
139 277.901721957 1785272332.3012831 274.0 robust tracking True 1.19 2.79 1.8128179501 1.7479069779 11.23 42.2 38.8 6 5 -15.46 669172.771 669187.748 110.557 -103.518 True 0.05 0.505 0.152 0 0.505 0.152 0 23 8.0 141.55 -1.539 -1.56 877 4410 2063
140 279.901270286 1785272334.3008313 276.0 robust tracking True 1.19 2.79 1.8127918148 1.7474239577 11.26 42.2 38.9 6 5 5.21 668986.416 668981.367 110.705 -103.667 True 0.05 0.415 0.13 0 0.415 0.13 0 23 56.0 164.7 -1.535 -1.56 879 4420 2063
141 281.911805396 1785272336.3113663 278.0 robust tracking True 1.19 2.78 1.812764459 1.7469430861 11.14 42.3 38.6 6 5 9.2 668783.357 668774.443 110.765 -103.653 True 0.05 0.976 0.047 0 0.976 0.047 0 23 40.0 167.02 -1.535 -1.56 881 4430 2063
142 283.904627638 1785272338.3041887 280.0 robust tracking True 1.19 2.78 1.8127364977 1.7464643722 11.13 42.4 38.9 6 5 1.9 668567.589 668565.747 110.702 -104.554 True 0.05 0.496 0.084 0 0.496 0.084 0 23 56.0 150.73 -1.537 -1.56 883 4440 2063
143 285.904540282 1785272340.3041015 282.0 robust tracking True 1.19 2.78 1.8127111868 1.7459831701 11.26 42.2 38.2 6 5 -21.52 668336.38 668357.228 110.484 -104.5 True 0.05 0.4 0.023 0 0.4 0.023 0 23 16.0 127.92 -1.541 -1.56 885 4450 2063
144 287.903906607 1785272342.3034678 284.0 robust tracking True 1.19 2.78 1.8126838601 1.7455066962 11.09 42.3 39.2 6 5 -0.35 668145.403 668145.742 110.548 -104.863 True 0.05 0.13 0.213 0 0.13 0.213 0 23 0.0 156.5 -1.536 -1.56 887 4460 2063
145 289.903519609 1785272344.3030806 286.0 robust tracking True 1.19 2.78 1.8126545436 1.74502695 11.28 42.2 38.0 6 5 -17.14 667916.867 667933.474 110.923 -105.07 True 0.05 0.412 0.112 0 0.412 0.112 0 23 8.0 145.17 -1.538 -1.56 889 4470 2063
146 291.906994829 1785272346.3065557 288.0 robust tracking True 1.19 2.78 1.812627295 1.7445482935 12.84 42.5 38.6 6 5 -1.31 667721.551 667722.816 109.629 -106.134 True 0.05 0.721 0.164 0 0.721 0.164 0 23 40.0 158.89 -1.536 -1.56 891 4480 2063
147 293.904251457 1785272348.3038123 290.0 robust tracking True 1.19 2.78 1.8126009474 1.7440682755 11.16 42.3 38.2 6 5 14.39 667524.719 667510.777 110.381 -105.665 True 0.05 0.161 0.079 0 0.161 0.079 0 23 32.0 184.17 -1.532 -1.56 893 4490 2063
148 295.904732818 1785272350.3042939 292.0 robust tracking True 1.19 2.78 1.8125748376 1.7435915248 11.06 42.6 38.1 6 5 -9.72 667289.655 667299.068 110.248 -105.873 True 0.05 0.291 0.005 0 0.291 0.005 0 23 32.0 159.99 -1.536 -1.56 895 4500 2063
149 297.910797167 1785272352.3103583 294.0 robust tracking True 1.19 2.78 1.8125472744 1.7431147283 11.0 42.9 38.4 6 5 9.37 667097.136 667088.06 110.084 -106.181 True 0.05 0.237 0.074 0 0.237 0.074 0 23 16.0 168.11 -1.535 -1.56 897 4510 2063
150 299.905108443 1785272354.3046699 296.0 robust tracking True 1.19 2.78 1.812521259 1.7426375476 16.12 42.5 37.8 6 5 -12.01 666863.837 666875.472 110.096 -106.613 True 0.05 0.693 0.156 0 0.693 0.156 0 23 64.0 124.56 -1.541 -1.56 899 4520 2063
@@ -0,0 +1,52 @@
# Genesys2 live PPS telemetry replay
Source: `pps_end_to_end_validation/hardware_logs/genesys2_pps_20260728.csv`
## Direct firmware/RTL observations
- Samples/duration: 149 / 296.0 s.
- Status interval: 2.000 s; median sequence step 10 (2.0 s of 200 ms solves).
- Profiles: robust; locked fraction 1.000; transitions 0; reference SVIDs [23].
- TDOP/GDOP medians: 1.220 / 2.820; clock sigma median 11.24 m.
- C/N0 mean/min medians: 42.6 / 38.4 dB-Hz; tracked/used medians 6/5.
- WLS innovation RMS/p95-absolute: 10.47 / 19.66 m.
- Raw/filtered bias slopes: -90.020 / -90.025 m/s; PPS velocity residual median 0.079 m/s.
- Internal hardware phase mean: 155.37 ns; median-centered RMS/p95/max/p-p: 17.92 / 31.26 / 47.95 / 93.21 ns.
- Ten-second averaged phase wander p-p: 46.16 ns.
- Sampled phase-step RMS/p95/max: 22.84 / 44.13 / 56.38 ns. These are at the status interval, not consecutive physical 1 PPS edges.
- Rate command median/span: -1.5370 / 0.0140 ppm; integrator median -1.5600 ppm.
- Fixed-point PI rate reproduction RMS/p95/max: 0.430 / 0.852 / 0.921 ppb. This is the difference between logged `rate_cmd` and `integrator + Kp*hw_phase`; the remaining error is dominated by three-decimal UART rounding.
- Phase correlation with rate command / WLS innovation: 0.988 / 0.656.
- A constant oscillator inferred from zero net frequency is 1.536779 ppm. Integrating the logged command under that assumption gives centered phase RMS/p-p 23.65 / 92.48 ns.
`hw_phase` is the RTL PI residual captured over APB. It is not a trusted-generator time-interval measurement of the output pin, so these values cannot certify physical PPS jitter or absolute offset.
## Telemetry-conditioned end-to-end loop replay
The replay imports the first locked phase/rate/integrator and software filter snapshot instead of incorrectly resetting the board to an acquisition state. It uses raw/filtered clock bias, Doppler datum drift, TDOP/GDOP, clock sigma, C/N0, satellite count, FDE, reference SVID and the production fixed-point coefficients. The reference trajectory is reconstructed from filtered bias, the logged PI residual and the integrated rate command.
- Expanded updates: 1481; interpolation factor 9.94.
- Snapshot starts locked at 0.00 s; post-warmup locked fraction 1.000; unlocks 0.
- Constant-oscillator convention: 1.536779 ppm.
- Modeled physical-grid edge centered RMS/p95/p-p: 41.32 / 80.00 / 168.00 ns.
- Modeled one-second period RMS/p95/max: 5.51 / 8.00 / 16.00 ns.
- Modeled PI residual centered RMS/p95/p-p: 21.47 / 46.79 / 135.63 ns.
- Logged-epoch PI residual reproduction RMS/p95 error: 18.07 / 44.30 ns.
- Logged-epoch filtered-bias reproduction RMS/p95: 9.87 / 22.13 m.
## Tuning sensitivity on the same trace
| tuning | edge RMS (ns) | edge p-p (ns) | period max (ns) | residual reproduction RMS (ns) | unlocks |
|---|---:|---:|---:|---:|---:|
| current | 41.32 | 168.00 | 16.00 | 18.07 | 0 |
| robust_bias_1_64 | 57.06 | 216.00 | 16.00 | 24.53 | 0 |
| drift_gain_1_2 | 42.23 | 168.00 | 16.00 | 18.03 | 0 |
These counterfactuals reuse a reference trajectory reconstructed from the current filter, and nine of every ten solve epochs are interpolated. They are useful as a rejection test, not as evidence to deploy new gains. On this capture neither `robust_bias_1_64` nor `drift_gain_1_2` improves the current loop's absolute wander.
## Limits and next measurement
- Missing status epochs are linearly interpolated when sequence step is greater than one. This preserves low-frequency wander but understates unlogged 200 ms noise; it is not an exact firmware replay.
- The inferred oscillator and active phase assume one constant crystal frequency chosen to make net phase slope zero over this capture. Temperature telemetry was unavailable, so slow crystal motion is not separable from the reconstructed reference.
- Level-1 telemetry does not expose ABS_SAMPLE, REM_CODE_PHASE, full capture counter, chi-square ratio, pseudorange residual vector, board temperature, or a trusted PPS edge timestamp. The bit/subframe part of the end-to-end model remains synthetic for those fields.
- Set `GPS_PPS_STATUS_LOG_INTERVAL=1` for exact 200 ms input cadence. For a max-20 ns pin claim, capture DUT and trusted PPS with a common-clock TIC/scope after cable/output-delay calibration.
@@ -9,13 +9,13 @@ This report is generated by `stability_analysis.py`. The absolute LNAV epoch pat
- DUT minus trusted PPS was 32.01 ns. That remainder is the model's configured four-PCLK capture pipeline, not a measured board delay.
- Production firmware and this analysis both define `POSTPONE_TOW` as 180 ms for the current RTL delivery semantics.
## Repository hardware data
## Hardware data used by this synthetic sweep
- Real GPS capture: 989 GDOP samples and 990 tracked-satellite samples over about five minutes.
- GDOP min/median/p95/max: 1.86 / 1.93 / 3.15 / 3.15.
- Tracked satellites: 8..8 (median 8).
- Horizontal scatter RMS/p95/max: 8.58 / 15.15 / 23.77 m. No surveyed truth was recorded.
- No real ABS_SAMPLE, REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/integrator, temperature, or trusted-edge TIC samples exist in the repository. Those columns in the companion CSV are explicitly synthetic.
- The older fusion capture used by this sweep has no ABS_SAMPLE, REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/integrator, temperature, or trusted-edge TIC samples. Those columns in the companion CSV are explicitly synthetic. Newer PPS status captures are analyzed separately by `hardware_replay.py` and are not silently mixed into this historical baseline.
Synthetic timing uses the board-calibration values already encoded in the PPS tests: TDOP 1.50, GDOP 2.92, clock sigma 15.87 m (52.94 ns), C/N0 40.3/34.8 dB-Hz, drift noise 3.47 m/s, seven-satellite reference point, -291.75 m/s datum range rate, and 1.592 ppm local oscillator. Real GDOP and satellite count replace the geometry fields when available.
@@ -9,7 +9,7 @@ models the project boundary which can create a constant absolute-time error:
* current registered-window and proposed current-bit-aware validation;
* subframe-ready and periodic navigation-solve interrupts;
* RF ABS_SAMPLE anchoring and the timestamped 125 MHz PPS capture;
* the firmware HOW + POSTPONE_TOW + STARTOFFSET - b/c mapping; and
* the firmware HOW + POSTPONE_TOW - b/c mapping; and
* coarse/normal PPS commits followed by comparison with an independent PPS.
Long stretches between 50 bit/s events are evaluated analytically. No result
@@ -40,7 +40,6 @@ TRACKING_INTEGRATION_PERIOD_S = 0.001
TRACKING_INTEGRATIONS_PER_BIT = 20
RF_HZ = 16_367_981.0
PPS_HZ = 125_000_000.0
START_OFFSET_S = 68.802e-3
NAV_TOW_SNAP_TOLERANCE_S = 0.005
Q32 = 1 << 32
TICK_INTEGER_TOLERANCE = 0.01
@@ -447,8 +446,8 @@ class FirmwareTimingModel:
self.event_period_s = nav_solve_period_bits * LNAV_BIT_PERIOD_S
self.postpone_tow_s = postpone_tow_s
self.propagation_s = propagation_s
# In this project's WLS datum STARTOFFSET - b/c recovers flight time.
self.synthetic_clock_bias_m = (START_OFFSET_S - propagation_s) * C
# In the raw-counter-difference WLS datum, -b/c recovers flight time.
self.synthetic_clock_bias_m = -propagation_s * C
self.anchor: SubframeInterrupt | None = None
self.rejected_nav_solves = 0
@@ -479,7 +478,6 @@ class FirmwareTimingModel:
gps_reference = (
transmit_tow
+ self.postpone_tow_s
+ START_OFFSET_S
- self.synthetic_clock_bias_m / C
)
whole = math.floor(gps_reference)
@@ -38,6 +38,7 @@ RF_HZ = 16_367_981.0
CODE_FREQ_HZ = 1_023_000.0
SPEED_OF_LIGHT_MPS = 299_792_458.0
UPDATE_PERIOD_S = 0.2
NAV_BIT_PERIOD_S = 0.020
BOARD_CLOCK_SIGMA_M = 15.87
BOARD_TDOP = 1.50
BOARD_GDOP = 2.92
@@ -297,14 +298,15 @@ def synthesize_epochs(
*,
duration_s: float,
seed: int,
update_period_s: float = UPDATE_PERIOD_S,
) -> list[SyntheticEpoch]:
"""Combine real GDOP/satellite count with documented board noise."""
rng = random.Random(seed)
result: list[SyntheticEpoch] = []
count = round(duration_s / UPDATE_PERIOD_S)
count = round(duration_s / update_period_s)
rf_initial = (1 << 32) - round(10.25 * RF_HZ) - 1000
for update in range(count):
time_s = update * UPDATE_PERIOD_S
time_s = update * update_period_s
gdop = telemetry.gdop_at(time_s)
satellites = max(4, min(8, telemetry.tracked_at(time_s)))
tdop = gdop * (BOARD_TDOP / BOARD_GDOP)
@@ -432,8 +434,14 @@ def synthesize_epochs(
return result
def _run_loop(epochs: list[SyntheticEpoch], tuning: Tuning):
def _run_loop(
epochs: list[SyntheticEpoch],
tuning: Tuning,
*,
update_period_s: float = UPDATE_PERIOD_S,
):
model = FixedPointAdaptivePps(
update_period_s=update_period_s,
oscillator_ppm=BOARD_OSCILLATOR_PPM,
profiles=tuning.profiles,
acquire_profile=tuning.acquire,
@@ -451,7 +459,7 @@ def _run_loop(epochs: list[SyntheticEpoch], tuning: Tuning):
else tuning.drift_gain
)
model.update(epoch.observation)
finish_s = epochs[-1].time_s + UPDATE_PERIOD_S if epochs else 0.0
finish_s = epochs[-1].time_s + update_period_s if epochs else 0.0
return model.finish(finish_s)
@@ -515,12 +523,15 @@ def aggregate(
)
def discrete_poles(profile: LoopProfile) -> tuple[complex, complex]:
def discrete_poles(
profile: LoopProfile,
update_period_s: float = UPDATE_PERIOD_S,
) -> tuple[complex, complex]:
"""Poles of the exact sample/commit ordering, before slew saturation."""
kp_q46, ki_q46, _ = profile.fixed_coefficients(UPDATE_PERIOD_S)
kp_q46, ki_q46, _ = profile.fixed_coefficients(update_period_s)
kp = kp_q46 / (1 << 46)
ki = ki_q46 / (1 << 46)
ticks = UPDATE_PERIOD_S * TRACKING_HZ
ticks = update_period_s * TRACKING_HZ
a = 1.0 - ticks * (kp + ki)
b = ticks
c = -ki
@@ -573,6 +584,7 @@ def write_report(
telemetry: RealTelemetry,
results: list[Aggregate],
epoch_result,
update_period_s: float = UPDATE_PERIOD_S,
) -> None:
gdop = telemetry.gdop
tracked = telemetry.tracked
@@ -600,7 +612,7 @@ def write_report(
"- Production firmware and this analysis both define `POSTPONE_TOW` "
"as 180 ms for the current RTL delivery semantics.",
"",
"## Repository hardware data",
"## Hardware data used by this synthetic sweep",
"",
f"- Real GPS capture: {len(gdop)} GDOP samples and {len(tracked)} "
"tracked-satellite samples over about five minutes.",
@@ -613,10 +625,12 @@ def write_report(
f"{_fmt(telemetry.horizontal_rms_m)} / "
f"{_fmt(telemetry.horizontal_p95_m)} / "
f"{_fmt(telemetry.horizontal_max_m)} m. No surveyed truth was recorded.",
"- No real ABS_SAMPLE, REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange "
"residual, PPS phase/rate/integrator, temperature, or trusted-edge TIC "
"samples exist in the repository. Those columns in the companion CSV "
"are explicitly synthetic.",
"- The older fusion capture used by this sweep has no ABS_SAMPLE, "
"REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/"
"integrator, temperature, or trusted-edge TIC samples. Those columns "
"in the companion CSV are explicitly synthetic. Newer PPS status "
"captures are analyzed separately by `hardware_replay.py` and are not "
"silently mixed into this historical baseline.",
"",
"Synthetic timing uses the board-calibration values already encoded "
"in the PPS tests: TDOP 1.50, GDOP 2.92, clock sigma 15.87 m "
@@ -655,7 +669,7 @@ def write_report(
"## Linear stability",
"",
"The fixed-point coefficients give the following discrete poles "
"at the 200 ms navigation update rate:",
f"at the {update_period_s * 1000.0:.0f} ms navigation update rate:",
"",
"| profile | natural frequency | damping | poles | max magnitude |",
"|---|---:|---:|---|---:|",
@@ -669,7 +683,7 @@ def write_report(
)
]
for name, profile in profile_rows:
poles = discrete_poles(profile)
poles = discrete_poles(profile, update_period_s)
pole_text = ", ".join(
f"{pole.real:.6f}{pole.imag:+.6f}j" for pole in poles
)
@@ -685,9 +699,11 @@ def write_report(
"integrator, and the RTL snapshots Kp/Ki/max-slew/lock limits "
"together with the next commit, so the current acquire/precision/"
"robust switching is structurally sound. Stable individual modes "
"do not prove arbitrary-switching stability, but the asymmetric "
"do not prove arbitrary-switching stability, but the baseline "
"8-update upgrade / 3-update downgrade hysteresis and the stress "
"runs show no coefficient chatter or unlock.",
"runs show no coefficient chatter or unlock. Hysteresis and "
"settling counts are scaled to preserve their wall-clock duration "
"with at least two/three independent updates.",
"",
"## Dynamic parameter sweep",
"",
@@ -782,6 +798,12 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--duration", type=float, default=300.0)
parser.add_argument("--seeds", type=int, default=24)
parser.add_argument(
"--update-period-ms",
type=float,
default=UPDATE_PERIOD_S * 1000.0,
help="NAV solve period (production-safe examples: 200, 500, 1000)",
)
parser.add_argument(
"--output-dir",
type=Path,
@@ -795,6 +817,19 @@ def main() -> int:
args = parse_args()
if args.duration < 120.0 or args.seeds < 1:
raise SystemExit("duration must be >= 120 s and seeds must be positive")
update_period_s = args.update_period_ms / 1000.0
nav_ticks = round(update_period_s / NAV_BIT_PERIOD_S)
if (
not math.isfinite(update_period_s)
or update_period_s <= 0.0
or abs(update_period_s - nav_ticks * NAV_BIT_PERIOD_S) > 1.0e-12
or nav_ticks <= 0
or 300 % nav_ticks != 0
):
raise SystemExit(
"update period must be N*20 ms with positive N dividing "
"the 300-bit subframe"
)
software_dir = Path(__file__).resolve().parents[1]
telemetry = load_real_telemetry(software_dir)
if not telemetry.gdop or not telemetry.tracked:
@@ -816,6 +851,7 @@ def main() -> int:
scenario,
duration_s=args.duration,
seed=seed,
update_period_s=update_period_s,
)
for seed in range(1, args.seeds + 1)
]
@@ -824,7 +860,12 @@ def main() -> int:
for tuning in tuning_matrix():
runs = [
measure_trace(
_run_loop(epochs, tuning), after_s=analysis_start
_run_loop(
epochs,
tuning,
update_period_s=update_period_s,
),
after_s=analysis_start,
)
for epochs in seed_epochs
]
@@ -856,6 +897,7 @@ def main() -> int:
telemetry,
results,
epoch_result,
update_period_s,
)
print(f"wrote {args.output_dir}")
return 0
@@ -0,0 +1,147 @@
from __future__ import annotations
import csv
import math
from pathlib import Path
import tempfile
import unittest
from pps_python.adaptive_pps import (
PROFILES,
Profile,
Q32,
TRACKING_HZ,
)
from pps_end_to_end_validation.hardware_replay import (
UPDATE_PERIOD_S,
expand_samples,
load_firmware_telemetry,
observed_metrics,
replay_firmware_telemetry,
replay_tuning_sensitivity,
)
def _status_line(
index: int,
*,
update_period_s: float = UPDATE_PERIOD_S,
log_interval_updates: int = 10,
) -> str:
phase_ns = 140.0 + 15.0 * math.sin(index * 0.7)
integrator_ppm = -1.560
kp_q46, _, _ = PROFILES[Profile.ROBUST].fixed_coefficients(
update_period_s
)
phase_q32 = round(phase_ns * 1.0e-9 * TRACKING_HZ * Q32)
proportional_q32 = (phase_q32 * kp_q46) >> 46
rate_ppm = (
round(integrator_ppm * 1.0e-6 * Q32) + proportional_q32
) / Q32 * 1.0e6
raw_bias_m = 700_000.0 - 80.0 * (2.0 * index) + 4.0 * math.sin(index)
filtered_bias_m = 700_000.0 - 80.0 * (2.0 * index)
return (
"[NAV] PPS profile=robust mode=tracking locked=1 "
"tdop=1.22 gdop=2.82 clock_sigma=11.24 cn0=42.6/38.4 "
f"innovation={raw_bias_m - filtered_bias_m:.2f}m "
f"raw_bias={raw_bias_m:.3f}m filtered_bias={filtered_bias_m:.3f}m "
"clock_drift=110.000mps bias_drift=-80.000mps predictor=1 "
"eskf_alpha=0.05 eskf_v=0.100/0.050/0x00 "
"pps_v=0.100/0.080/0x00 ref_svid=23 capture_corr=32.00ns "
f"hw_phase={phase_ns:.2f}ns rate_cmd={rate_ppm:.3f}ppm "
f"integ={integrator_ppm:.3f}ppm pulse={100 + 2 * index} "
f"seq={1000 + log_interval_updates * index} status=0x80f"
)
def _write_capture(
path: Path,
count: int = 16,
*,
update_period_s: float = UPDATE_PERIOD_S,
log_interval_updates: int = 10,
) -> None:
location = (
"[LOCATION] source=gps lat=0 lon=0 height=0 "
"gdop=2.82 pdop=2.54 hdop=1.81 vdop=1.78 tdop=1.22 "
"tracked=6 used=5 velocity_valid=1 velocity_fde=0 "
"speed=0.1 velocity_rms=0.08"
)
with path.open("w", encoding="utf-8", newline="") as output:
writer = csv.writer(output, lineterminator="\n")
writer.writerow(("host_monotonic_s", "host_unix_s", "uart_line"))
for index in range(count):
host_s = update_period_s * log_interval_updates * index
writer.writerow((host_s - 0.01, 1000.0 + host_s, location))
writer.writerow((
host_s,
1000.01 + host_s,
_status_line(
index,
update_period_s=update_period_s,
log_interval_updates=log_interval_updates,
),
))
class HardwareReplayTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.capture = Path(self.temporary.name) / "capture.csv"
_write_capture(self.capture)
def tearDown(self) -> None:
self.temporary.cleanup()
def test_uart_parser_and_sequence_expansion(self):
samples = load_firmware_telemetry(self.capture)
self.assertEqual(len(samples), 16)
self.assertEqual(samples[-1].time_s, 30.0)
self.assertEqual(samples[0].tracked_satellites, 6)
self.assertEqual(samples[0].used_satellites, 5)
expanded = expand_samples(samples)
self.assertEqual(len(expanded), 151)
self.assertEqual(sum(interpolated for _, interpolated in expanded), 135)
def test_fixed_point_pi_and_checkpoint_replay(self):
samples = load_firmware_telemetry(self.capture)
observed = observed_metrics(samples)
self.assertEqual(observed.locked_fraction, 1.0)
self.assertLess(observed.pi_rate_reproduction_rms_ppb, 0.6)
points, replay = replay_firmware_telemetry(
samples, warmup_s=5.0
)
self.assertEqual(len(points), 151)
self.assertEqual(replay.unlocks, 0)
self.assertEqual(replay.locked_fraction_after_warmup, 1.0)
self.assertTrue(math.isfinite(replay.modeled_edge_centered_rms_ns))
self.assertGreater(replay.modeled_edge_centered_rms_ns, 0.0)
def test_one_second_cadence_is_inferred_from_uart_sequence(self):
_write_capture(
self.capture,
update_period_s=1.0,
log_interval_updates=2,
)
samples = load_firmware_telemetry(self.capture)
self.assertEqual(samples[-1].time_s, 30.0)
self.assertEqual(len(expand_samples(samples)), 31)
observed = observed_metrics(samples)
self.assertLess(observed.pi_rate_reproduction_rms_ppb, 0.6)
_, replay = replay_firmware_telemetry(samples, warmup_s=5.0)
self.assertEqual(replay.expanded_updates, 31)
self.assertEqual(replay.unlocks, 0)
def test_tuning_sensitivity_keeps_candidates_separate(self):
samples = load_firmware_telemetry(self.capture)
results = replay_tuning_sensitivity(samples, warmup_s=5.0)
self.assertEqual(
[result.tuning for result in results],
["current", "robust_bias_1_64", "drift_gain_1_2"],
)
self.assertTrue(all(result.unlocks == 0 for result in results))
if __name__ == "__main__":
unittest.main()
@@ -81,11 +81,10 @@ class PpsEndToEndTests(unittest.TestCase):
self.assertGreater(result.nav_solve_interrupts, 20)
self.assertEqual(result.firmware_rejected_nav_solves, 0)
def test_permitted_40_bit_period_rejects_after_how_anchor_moves(self):
# Firmware's compile-time rule accepts 40 because it divides the GPS
# week. It does not divide a 300-bit subframe, so refreshing the HOW
# sample anchor changes timer phase by half a solve period. The
# current N*20 ms snap then rejects otherwise valid capture events.
def test_non_subframe_divisor_explains_40_bit_build_restriction(self):
# N=40 divides the GPS week but not a 300-bit subframe. Refreshing the
# HOW sample anchor changes timer phase by half a solve period, so the
# model rejects solves. Firmware now prevents this configuration.
self.assertEqual(30_240_000 % 40, 0)
self.assertNotEqual(300 % 40, 0)
result = run_end_to_end(
@@ -93,6 +92,23 @@ class PpsEndToEndTests(unittest.TestCase):
)
self.assertGreater(result.firmware_rejected_nav_solves, 0)
def test_supported_200_500_1000ms_periods_keep_absolute_epoch(self):
for period_bits in (10, 25, 50):
result = run_end_to_end(
EndToEndConfig(
nav_solve_period_bits=period_bits,
subframes=4,
)
)
self.assertEqual(result.firmware_rejected_nav_solves, 0)
self.assertAlmostEqual(
result.expected_semantic_offset_ms, 0.0, places=9
)
self.assertLess(
abs(result.pps.edges[0].dut_minus_trusted_s),
100.0e-9,
)
def test_current_rtl_and_160ms_correction_make_pps_20ms_late(self):
result = run_end_to_end(EndToEndConfig(postpone_tow_s=0.160))
offset_s = result.pps.edges[0].dut_minus_trusted_s
@@ -40,8 +40,23 @@ class StabilityAnalysisTests(unittest.TestCase):
PROFILES[Profile.PRECISION],
PROFILES[Profile.ROBUST],
):
poles = discrete_poles(profile)
self.assertTrue(all(abs(pole) < 1.0 for pole in poles))
baseline_poles = discrete_poles(profile)
self.assertTrue(all(abs(pole) < 1.0 for pole in baseline_poles))
for period_s in (0.5, 1.0):
mapped_poles = discrete_poles(profile, period_s)
expected_poles = tuple(
pole ** (period_s / 0.2) for pole in baseline_poles
)
self.assertTrue(all(abs(pole) < 1.0 for pole in mapped_poles))
self.assertLess(
max(
abs(actual - expected)
for actual, expected in zip(
mapped_poles, expected_poles
)
),
1.0e-4,
)
def test_synthetic_telemetry_exercises_wrap_and_physical_fields(self):
epochs = synthesize_epochs(
@@ -79,6 +94,25 @@ class StabilityAnalysisTests(unittest.TestCase):
metrics.period_max_ns,
)
def test_current_loop_remains_stable_at_supported_slower_cadences(self):
for period_s in (0.5, 1.0):
epochs = synthesize_epochs(
self.telemetry,
scenario_matrix()[0],
duration_s=180.0,
seed=4,
update_period_s=period_s,
)
trace = _run_loop(
epochs,
tuning_matrix()[0],
update_period_s=period_s,
)
metrics = measure_trace(trace, after_s=60.0)
self.assertLess(metrics.first_lock_s, 15.0)
self.assertEqual(metrics.unlocks, 0)
self.assertLessEqual(metrics.period_max_ns, 16.0 + 1.0e-9)
if __name__ == "__main__":
unittest.main()
+71 -10
View File
@@ -9,7 +9,8 @@ davranışlarını taklit eder:
- RF `ABS_SAMPLE` sayacı: `16.367981 MHz`, 32 bit ve wrap eden sayaç.
- Tracking/PPS sayacı: `125 MHz` free-running sayaç.
- Navigation solve periyodu: `NAV_SOLVE_TIMER=10`, yani `200 ms`.
- Varsayılan navigation solve periyodu: `NAV_SOLVE_TIMER=10`, yani `200 ms`;
period-aware loop doğrulaması ayrıca 500 ve 1000 ms'yi kapsar.
- Ortak navigation bit transmit epoch'u ve uyduların farklı propagation
delay'lerle art arda tracking olayı üretmesi.
- Firmware'deki corrected sample denklemi:
@@ -29,11 +30,13 @@ davranışlarını taklit eder:
Model ve C ayni 8 ns integer mailbox yuvarlamasini kullanir.
- `gps_unwrap_corrected_abs_sample()` ile aynı signed modulo-32 unwrap.
- `gps_ip.c` ile aynı pseudorange datum'u:
- `gps_ip.c` ile aynı free-counter pseudorange datum'u:
```text
precise_ms_i = abs_i / 16367.981 - reference + 68.802
P_i = precise_ms_i * c / 1000 + satellite_clock_i * c
S_i = unwrap32(ABS_SAMPLE_i) - REM_CODE_PHASE_i / 1.023e6
P_i/c = (S_i - S_ref) / 16.367981e6
- (transmit_i - transmit_ref)
+ satellite_clock_i
```
- `NavEquations.c` ile aynı dört state:
@@ -131,13 +134,13 @@ Mevcut observation denkleminde WLS bias doğrudan PPS fazı değildir. İlk
ilişki:
```text
b/c = STARTOFFSET - signal_flight_time_ref
b/c = -signal_flight_time_ref + satellite_clock_ref
```
Bu nedenle doğru GPS referansı:
```text
GPS_TIME_AT_CAPTURE = transmit_epoch_TOW + STARTOFFSET - b/c
GPS_TIME_AT_CAPTURE = transmit_epoch_TOW - b/c
```
ile kurulur ve referans kanala ait `tracking_tick_125` ile eşlenir. Referans
@@ -175,7 +178,7 @@ teslim edilen subframe için:
```text
transmit_epoch_TOW = HOW_TOW + POSTPONE_TOW
GPS_TIME_AT_CAPTURE = transmit_epoch_TOW + STARTOFFSET - b/c
GPS_TIME_AT_CAPTURE = transmit_epoch_TOW - b/c
```
`POSTPONE_TOW` signal flight time veya receiver clock bias değildir; hardware
@@ -204,8 +207,8 @@ bu üç etkiyi ayrı gösterir.
ilk hardware delivery epoch'u için 180 ms sabit readout düzeltmesi ayrıca
eklenir.
3. İlk coherent WLS çözümünde minimum corrected-ABS kanalı datum olarak
seçilir; aynı SVID mevcut kaldığı sürece onun uçuş süresi
`STARTOFFSET - b/c` ile elde edilir.
seçilir; aynı SVID mevcut kaldığı sürece onun uçuş süresi `-b/c` ile elde
edilir (referans satellite clock correction'i dahil).
4. Bu receive GPS zamanı aynı kanalın yakalanmış 125 MHz timestamp'ıyla
eşlenerek PPS accumulator coarse-align edilir.
5. Sonraki çözümler aynı eşleşmeden residual phase error üretip PI'yi besler.
@@ -239,7 +242,7 @@ PI, raw 125 MHz sayacı değiştirmez. Ayrı GPS time accumulator'ın rate word'
ayarlar:
```text
integrator += Ki * phase_error * update_period
integrator += Ki_discrete * phase_error
rate_command = integrator + Kp * phase_error
```
@@ -267,6 +270,64 @@ olması gerekir. Üretim profilleri tek seyrek WLS sıçramasında fiziksel puls
kaçırmamak için lock'u ancak `128 tick = 1.024 us` üstünde bırakır; watchdog,
quality holdover ve slew clamp daha büyük arızaları ayrıca sınırlar.
### NAV solve periyodu değiştiğinde loop dinamiği
`Ki` zaten ayrık-zaman katsayısıdır; aynı sayıyı 200, 500 ve 1000 ms'de
kullanmak aynı loop değildir. Basitçe `Ki`'yi periyotla çarpmak da uzun
periyotlarda mevcut RTL'nin gerçek kutuplarını yeterince doğru korumaz.
Firmware ve Python model şu uyumlu yöntemi kullanır:
1. Kartta doğrulanmış 200 ms Q2.46 katsayılarından RTL'nin iki ayrık kutbu
çıkarılır.
2. Yeni `T` periyodu için her kutup `T / 0.2` kuvvetine yükseltilir.
3. Hedef kutuplardan, RTL'nin önce-integral-sonra-proportional commit sırasına
uygun yeni `Kp` ve `Ki` geri hesaplanır.
Bu yöntem 200 ms'de eski katsayıları bit düzeyinde aynen üretir; 500 ve
1000 ms'de aynı gerçek-zaman damping ve settling davranışını korur:
| NAV periyodu | profil | Kp Q2.46 | Ki Q2.46 |
|---:|---|---:|---:|
| 200 ms | acquire / precision / robust | 764018 / 222838 / 84891 | 64006 / 5445 / 640 |
| 500 ms | acquire / precision / robust | 615780 / 209783 / 82980 | 127492 / 12804 / 1564 |
| 1000 ms | acquire / precision / robust | 447388 / 190239 / 79923 | 178038 / 23149 / 3011 |
Toplam döngü yalnız RTL PI'den oluşmadığı için yazılım clock-bias, Doppler
drift ve fallback drift kazançları da gerçek geçen süreyle
`alpha_T = 1 - (1 - alpha_200ms)^(T/0.2)` biçiminde uygulanır. Quality/profile
histerezisi, lock ve filter-settle sayaçları saniye cinsinden ölçeklenir;
bağımsız örnek güvenliği için sırasıyla en az 2/2/3 update korunur. Reference
watchdog `max(2 s, 3*T)` olur.
Desteklenen build değeri `GPS_NAV_SOLVE_TIMER_TICKS=N` için N, 300 bitlik
LNAV subframe'i bölmelidir. Doğrulanan örnekler N=10, 25 ve 50'dir. N=40,
HOW anchor yenilendiğinde timer fazını kaydırdığı için compile-time reddedilir.
`POSTPONE_TOW=180 ms` NAV solve periyodundan bağımsızdır ve değişmez.
Örnek firmware build'leri:
```bash
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=10 all # 200 ms, varsayılan
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=25 all # 500 ms
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=50 all # 1000 ms
```
Linux stability sweep'i aynı periyotları doğrudan kabul eder:
```bash
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 500
```
Kutup eşlemesi deterministik loop dinamiğini korur; daha seyrek WLS ölçümü
aynı gürültü performansını garanti etmez. Örnek sayısı azalınca aliasing,
outlier yakalama gecikmesi ve mutlak faz wander'ı değişebilir. Bu nedenle her
kart/periyot kombinasyonu trusted PPS ile ayrıca ölçülmelidir.
## Adaptif PPS senaryo matrisi
Tam fixed-point/holdover matrisi:
+211 -16
View File
@@ -8,7 +8,7 @@ rounding, lock acquisition/release and holdover/re-acquisition.
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from enum import IntEnum
import math
from typing import Iterable, Mapping
@@ -16,7 +16,9 @@ from typing import Iterable, Mapping
TRACKING_HZ = 125_000_000
SPEED_OF_LIGHT_MPS = 299_792_458.0
REFERENCE_WATCHDOG_SEC = 2.0
BASELINE_UPDATE_PERIOD_S = 0.2
MIN_REFERENCE_WATCHDOG_SEC = 2.0
REFERENCE_WATCHDOG_PERIODS = 3.0
MAX_PREDICTION_GAP_SEC = 30.0
Q32 = 1 << 32
Q46 = 1 << 46
@@ -24,6 +26,27 @@ MAX_SIGNED_64 = (1 << 63) - 1
MIN_SIGNED_64 = -(1 << 63)
def _period_updates(
baseline_updates: int,
update_period_s: float,
minimum_updates: int,
) -> int:
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
duration_s = baseline_updates * BASELINE_UPDATE_PERIOD_S
return min(
255,
max(minimum_updates, math.ceil(duration_s / update_period_s - 1e-12)),
)
def _elapsed_gain(baseline_gain: float, elapsed_s: float) -> float:
periods = elapsed_s / BASELINE_UPDATE_PERIOD_S
if abs(periods - 1.0) <= 1e-12:
return baseline_gain
return -math.expm1(periods * math.log1p(-baseline_gain))
class Profile(IntEnum):
HOLDOVER = 0
ROBUST = 1
@@ -58,14 +81,74 @@ class LoopProfile:
software_settle_updates: int
def fixed_coefficients(self, update_period_s: float) -> tuple[int, int, int]:
"""Map the proven 200 ms discrete poles to ``update_period_s``.
The 200 ms result is exactly the legacy production coefficient set.
At another NAV cadence each baseline pole is raised to T/0.2 before
recovering Kp/Ki for the RTL's integrate-then-proportional ordering.
"""
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
omega = 2.0 * math.pi * self.natural_frequency_hz
kp_q46 = round((2.0 * self.damping * omega / TRACKING_HZ) * Q46)
ki_q46 = round(
(omega * omega * update_period_s / TRACKING_HZ) * Q46
baseline_kp = math.floor(
(2.0 * self.damping * omega / TRACKING_HZ) * Q46 + 0.5
) / Q46
baseline_ki = math.floor(
(
omega * omega * BASELINE_UPDATE_PERIOD_S / TRACKING_HZ
) * Q46 + 0.5
) / Q46
baseline_ticks = TRACKING_HZ * BASELINE_UPDATE_PERIOD_S
baseline_sum = 2.0 - baseline_ticks * (
baseline_kp + baseline_ki
)
baseline_product = 1.0 - baseline_ticks * baseline_kp
discriminant = baseline_sum * baseline_sum - 4.0 * baseline_product
period_ratio = update_period_s / BASELINE_UPDATE_PERIOD_S
if discriminant >= 0.0:
root = math.sqrt(discriminant)
pole_a = 0.5 * (baseline_sum + root)
pole_b = 0.5 * (baseline_sum - root)
if pole_a < 0.0 or pole_b < 0.0:
raise ValueError("baseline loop has an unmappable negative pole")
target_a = pole_a**period_ratio
target_b = pole_b**period_ratio
target_sum = target_a + target_b
target_product = target_a * target_b
else:
if baseline_product <= 0.0:
raise ValueError("baseline loop has invalid complex poles")
radius = math.sqrt(baseline_product)
cosine = max(-1.0, min(1.0, baseline_sum / (2.0 * radius)))
angle = math.acos(cosine)
target_radius = radius**period_ratio
target_sum = 2.0 * target_radius * math.cos(
angle * period_ratio
)
target_product = target_radius * target_radius
target_ticks = TRACKING_HZ * update_period_s
kp = (1.0 - target_product) / target_ticks
ki = (1.0 - target_sum + target_product) / target_ticks
if not math.isfinite(kp) or not math.isfinite(ki) or kp < 0 or ki < 0:
raise ValueError("mapped loop coefficients are invalid")
kp_q46 = math.floor(kp * Q46 + 0.5)
ki_q46 = math.floor(ki * Q46 + 0.5)
max_slew_q32 = math.floor(
self.max_slew_ppm * 1.0e-6 * Q32 + 0.5
)
max_slew_q32 = round(self.max_slew_ppm * 1.0e-6 * Q32)
return kp_q46, ki_q46, max_slew_q32
def for_update_period(self, update_period_s: float) -> LoopProfile:
return replace(
self,
lock_acquire_updates=_period_updates(
self.lock_acquire_updates, update_period_s, 2
),
software_settle_updates=_period_updates(
self.software_settle_updates, update_period_s, 3
),
)
PROFILES = {
Profile.HOLDOVER: LoopProfile(
@@ -149,10 +232,12 @@ def classify_quality(metrics: QualityMetrics) -> Profile:
class QualitySelector:
"""Three-profile selector with asymmetric transition hysteresis."""
def __init__(self) -> None:
def __init__(self, update_period_s: float = BASELINE_UPDATE_PERIOD_S) -> None:
self.profile = Profile.HOLDOVER
self.candidate = Profile.HOLDOVER
self.candidate_updates = 0
self.upgrade_updates = _period_updates(8, update_period_s, 2)
self.degrade_updates = _period_updates(3, update_period_s, 2)
def update(self, metrics: QualityMetrics) -> tuple[Profile, bool]:
target = classify_quality(metrics)
@@ -165,7 +250,11 @@ class QualitySelector:
self.candidate_updates = 1
else:
self.candidate_updates += 1
required = 8 if target > self.profile else 3
required = (
self.upgrade_updates
if target > self.profile
else self.degrade_updates
)
if self.candidate_updates >= required:
self.profile = target
self.candidate_updates = 0
@@ -339,19 +428,28 @@ class FixedPointAdaptivePps:
software_drift_gain: float = 1.0 / 4.0,
software_fallback_drift_gain: float = 1.0 / 16.0,
) -> None:
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
self.update_period_s = update_period_s
self.oscillator_fraction = oscillator_ppm * 1.0e-6
self.adaptive = adaptive
self.software_filter = software_filter
self.receiver_clock_bias_m = receiver_clock_bias_m
self.receiver_clock_drift_mps = receiver_clock_drift_mps
self.profiles = dict(PROFILES if profiles is None else profiles)
baseline_profiles = dict(PROFILES if profiles is None else profiles)
self.profiles = {
profile: parameters.for_update_period(update_period_s)
for profile, parameters in baseline_profiles.items()
}
missing_profiles = set(Profile) - self.profiles.keys()
if missing_profiles:
raise ValueError(f"missing loop profiles: {sorted(missing_profiles)}")
self.acquire_profile = (
baseline_acquire_profile = (
ACQUIRE_PROFILE if acquire_profile is None else acquire_profile
)
self.acquire_profile = baseline_acquire_profile.for_update_period(
update_period_s
)
if not 0.0 < software_drift_gain <= 1.0:
raise ValueError("software_drift_gain must be in (0, 1]")
if not 0.0 < software_fallback_drift_gain <= 1.0:
@@ -360,7 +458,11 @@ class FixedPointAdaptivePps:
)
self.software_drift_gain = software_drift_gain
self.software_fallback_drift_gain = software_fallback_drift_gain
self.selector = QualitySelector()
self.selector = QualitySelector(update_period_s)
self.reference_watchdog_sec = max(
MIN_REFERENCE_WATCHDOG_SEC,
REFERENCE_WATCHDOG_PERIODS * update_period_s,
)
self.profile = Profile.HOLDOVER if adaptive else Profile.PRECISION
self.parameters = self.profiles[self.profile]
self.kp_q46, self.ki_q46, self.max_slew_q32 = (
@@ -404,6 +506,88 @@ class FixedPointAdaptivePps:
def _phase_q32_to_seconds(value: int) -> float:
return value / (TRACKING_HZ * Q32)
def seed_tracking_state(
self,
*,
time_s: float,
profile: Profile,
phase_error_s: float,
rate_command_ppm: float,
integrator_ppm: float,
software_error_s: float,
software_bias_m: float,
software_clock_drift_mps: float,
software_previous_measurement_m: float,
reference_id: int,
) -> None:
"""Start a replay from a captured, already-locked board snapshot.
Normal simulations deliberately start unaligned and exercise coarse
acquisition. Hardware logs, however, are commonly captured after the
board has been tracking for minutes. Replaying such a log from reset
introduces an artificial acquisition transient and clears the PI
integrator that was present on the board. This method imports only
state that the production diagnostic registers and firmware status
expose; subsequent updates still use the normal bit-accurate path.
The method must be called before any observation has been recorded.
It is a model facility and has no firmware/RTL side effect.
"""
finite_values = (
time_s,
phase_error_s,
rate_command_ppm,
integrator_ppm,
software_error_s,
software_bias_m,
software_clock_drift_mps,
software_previous_measurement_m,
)
if self.trace.time_s:
raise ValueError("tracking state must be seeded before replay")
if not all(math.isfinite(value) for value in finite_values):
raise ValueError("tracking state values must be finite")
if time_s < 0.0:
raise ValueError("tracking state time must be non-negative")
if profile == Profile.HOLDOVER:
raise ValueError("a locked snapshot cannot use holdover profile")
self.current_time_s = time_s
self.raw_tick_remainder = 0.0
self.phase_q32 = self._seconds_to_phase_q32(phase_error_s)
self.profile = profile
self.selector.profile = profile
self.selector.candidate = profile
self.selector.candidate_updates = 0
self.aligned = True
self.locked = True
self.lock_good_updates = 0
self.reference_watchdog_holdover = False
self._select_loop_parameters()
self.integrator_q32 = self._clamp(
round(integrator_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.rate_word_q32 = self._clamp(
round(rate_command_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.software_error_s = software_error_s
self.software_bias_m = software_bias_m
self.software_clock_drift_mps = software_clock_drift_mps
self.software_previous_measurement_m = (
software_previous_measurement_m
)
self.software_drift_anchor_time_s = time_s
self.software_drift_anchor_measurement_m = (
software_previous_measurement_m
)
self.software_drift_initialized = True
self.software_updates = max(
1, self.parameters.software_settle_updates
)
self.software_reference_id = reference_id
self.last_observation_time_s = time_s
def _apply_profile(self, profile: Profile) -> None:
self.profile = profile
self._select_loop_parameters()
@@ -570,12 +754,21 @@ class FixedPointAdaptivePps:
return False, False
innovation_m = max(-300.0, min(300.0, innovation_m))
effective_bias_gain = _elapsed_gain(
self.parameters.software_bias_gain, interval_s
)
effective_drift_gain = _elapsed_gain(
self.software_drift_gain, interval_s
)
effective_fallback_drift_gain = _elapsed_gain(
self.software_fallback_drift_gain, interval_s
)
self.software_bias_m = (
predicted_bias_m
+ self.parameters.software_bias_gain * innovation_m
+ effective_bias_gain * innovation_m
)
if observation.clock_drift_valid:
self.software_clock_drift_mps += self.software_drift_gain * (
self.software_clock_drift_mps += effective_drift_gain * (
measured_drift_mps - self.software_clock_drift_mps
)
else:
@@ -583,7 +776,7 @@ class FixedPointAdaptivePps:
self.software_clock_drift_mps = fallback_drift_mps
else:
self.software_clock_drift_mps += (
self.software_fallback_drift_gain *
effective_fallback_drift_gain *
(fallback_drift_mps - self.software_clock_drift_mps)
)
self.software_previous_measurement_m = measured_bias_m
@@ -649,7 +842,7 @@ class FixedPointAdaptivePps:
if (
self.last_observation_time_s is None
or observation.time_s - self.last_observation_time_s
> REFERENCE_WATCHDOG_SEC
> self.reference_watchdog_sec
):
self.reference_watchdog_holdover = True
self._record(observation, 0, 0)
@@ -665,7 +858,7 @@ class FixedPointAdaptivePps:
if (
self.last_observation_time_s is None
or observation.time_s - self.last_observation_time_s
> REFERENCE_WATCHDOG_SEC
> self.reference_watchdog_sec
):
self.reference_watchdog_holdover = True
self._record(observation, 0, 0)
@@ -735,6 +928,7 @@ def run_observations(
observations: Iterable[Observation],
*,
finish_time_s: float,
update_period_s: float = BASELINE_UPDATE_PERIOD_S,
oscillator_ppm: float = 1.65,
adaptive: bool = True,
software_filter: bool = True,
@@ -746,6 +940,7 @@ def run_observations(
software_fallback_drift_gain: float = 1.0 / 16.0,
) -> AdaptiveTrace:
model = FixedPointAdaptivePps(
update_period_s=update_period_s,
oscillator_ppm=oscillator_ppm,
adaptive=adaptive,
software_filter=software_filter,
+18 -18
View File
@@ -300,10 +300,10 @@ def page_executive(report: Report) -> None:
0.07,
y,
"Bu projede konum WLS'i doğru çalışırken elde edilen dördüncü state, doğrudan "
"125 MHz receiver clock fazı değildir. Pseudorange'lar her epoch'ta minimum "
"ABS_SAMPLE ile normalize edildiği için RF sayacının mutlak fazı observation'dan "
"çıkar. WLS bias; 68.802 ms sentetik datum ile minimum kanalın gözlenen uçuş "
"süresi arasındaki ortak farkı taşır.",
"125 MHz receiver clock fazı değildir. Pseudorange'lar her epoch'ta corrected RF "
"sample ve satellite transmit-time farklarıyla kurulduğu için sayacın mutlak fazı "
"observation'dan çıkar. WLS bias, referans kanalın negatif uçuş süresini ve "
"satellite clock correction'ini taşıyan göreli datumdur.",
width=100,
fontsize=10.2,
)
@@ -556,7 +556,7 @@ def page_project_map(report: Report) -> None:
(0.77, 0.54, 0.17, 0.10, "Tracking ISR", "event queue +\nshadow counter", LIGHT, NAVY),
(0.54, 0.54, 0.18, 0.10, "Linked-list state", "TOW owner\nsequence + sample", LIGHT, NAVY),
(0.28, 0.54, 0.20, 0.10, "Coherent snapshot", "25 ms window\nmodulo unwrap", PALE_GREEN, GREEN),
(0.06, 0.54, 0.17, 0.10, "Pseudorange", "minimum +\n68.802 ms", PALE_GREEN, GREEN),
(0.06, 0.54, 0.17, 0.10, "Pseudorange", "ΔS_RF −\nΔT transmit", PALE_GREEN, GREEN),
(0.06, 0.34, 0.17, 0.10, "Satellite model", "TOW + 180 ms\nephemeris/clock", PALE_BLUE, BLUE),
(0.32, 0.34, 0.23, 0.10, "Weighted LS", "[X Y Z b]\niono + tropo + URA/el", PALE_GREEN, GREEN),
(0.64, 0.34, 0.30, 0.10, "Bugünkü çıkış", "position log + static last_solution[4]\nclock bias dışarı taşınmıyor", "#FBEAEA", RED),
@@ -651,14 +651,14 @@ def page_pseudorange(report: Report) -> None:
y = 0.84
ax.text(0.08, y, "1 Fractional RF sample düzeltmesi", fontsize=11, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"A_i^{corr}=A_i^{raw}-\frac{REM_i}{1.023\times10^6}")
ax.text(0.08, y, "2 Wrap-safe coherent unwrap ve minimum seçimi", fontsize=11, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"A_{min}=\min_i\left(A_i^{corr}\right)")
ax.text(0.08, y, "3 C kodundaki sentetik propagation datum'u", fontsize=11, color=NAVY, weight="bold")
ax.text(0.08, y, "2 Corrected RF sample ve transmit-time farkı", fontsize=11, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"\Delta t_i=\frac{S_i-S_{ref}}{16.367981\,MHz}-(T_{tx,i}-T_{tx,ref})")
ax.text(0.08, y, "3 Offset-free göreli pseudorange datum'u", fontsize=11, color=NAVY, weight="bold")
y = report.equation(
ax,
y - 0.025,
r"P'_i=c\left[\frac{A_i^{corr}-A_{min}}{16.368\,MHz}+S\right]+c\,\delta t_{SV,i}",
"S = 68.802 ms; gps_ip.c içinde ms formunda hesaplanır",
r"P'_i=c\left[\Delta t_i+\delta t_{SV,i}\right]",
"Free-running counter başlangıcı ve ortak uzun-dönem drift farkta yok olur",
face=PALE_ORANGE,
)
report.box(
@@ -667,8 +667,8 @@ def page_pseudorange(report: Report) -> None:
0.17,
0.40,
0.14,
"Minimum neden var?",
"Raw ABS_SAMPLE'ın receiver açılışına bağlı büyük ortak sayısını çıkarır. Uydular arası arrival-time farkları korunur; konum geometrisi bu farklarda bulunur.",
"Referans neden var?",
"RF counter'ın receiver açılışına bağlı ortak sayısını wrap-safe çıkarır. Uydular arası arrival/transmit-time farkları korunur; konum geometrisi bu farklarda bulunur.",
face=PALE_BLUE,
)
report.box(
@@ -677,8 +677,8 @@ def page_pseudorange(report: Report) -> None:
0.17,
0.40,
0.14,
"68.802 ms neden var?",
"Göreli range'leri fiziksel 20–26 bin km büyüklüğüne taşır ve WLS common bias'ı sıfıra yakın tutar. Yeni bilgi eklemez; datum seçer.",
"Sabit offset neden yok?",
"WLS dördüncü state göreli datumun büyük ortak bias'ını zaten çözer. Fiziksel konum için kalibre edilmiş nominal uçuş süresi gerekmez.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
@@ -1136,7 +1136,7 @@ def page_sim_method(report: Report) -> None:
("Navigation", "Ortak 20 ms transmit-bit epoch; NAV_SOLVE_TIMER=10 → 200 ms"),
("Satellite", "Sentetik gerçekçi ECEF/az-el geometrisi, satellite clock, path delay"),
("Corrections", "Earth rotation, Klobuchar, aktif C troposfer modeli, URA/elevation noise"),
("Firmware", "Modulo unwrap, minimum + 68.802 ms, clock correction, cached four-state WLS"),
("Firmware", "Raw-counter/transmit-time difference, clock correction, cached four-state WLS"),
("Latency", "Satellite arrival span, WLS compute, scheduler stall, APB jitter"),
("PPS", "125 MHz raw oscillator, coarse mapping, timestamped/untagged PI, 8 ns edge grid"),
]
@@ -1264,7 +1264,7 @@ def page_verification(report: Report) -> None:
fig, ax = report.new_page("Doğrulama matrisi ve beklenen sonuç")
tests = [
("ABS common +5 ms", "XYZ aynı; normalized b aynı", "Observation common phase iptalini kanıtlar"),
("STARTOFFSET +1 ms", "XYZ aynı; b +c×1 ms", "Datum dönüşüm işaretini kanıtlar"),
("RF counter common shift", "XYZ ve göreli b aynı", "RF counter origin iptalini kanıtlar"),
("32-bit ABS wrap", "Pseudorange süreklidir", "Signed modulo unwrap"),
("Reference SVID değişimi", "Raw b step; mapped GPS time continuous", "Min datum ownership"),
("APB +50 ms delay", "Timestamped PPS fazı değişmez", "Epoch association"),
@@ -1340,7 +1340,7 @@ def page_conclusion(report: Report) -> None:
("1", "Raw clocks", "16.368 MHz measurement + 125 MHz monotonic counter"),
("2", "Paired capture", "ABS / REM / C125 / sequence aynı event"),
("3", "Coherent WLS", "minimum datum + all-satellite [XYZ b]"),
("4", "GPS receive time", "T_TX(epoch-aligned) + 68.802 ms − b/c"),
("4", "GPS receive time", "T_TX(epoch-aligned) − b/c"),
("5", "Atomic mapping", "(C125,k, TGPS,k, seq) shadow + commit"),
("6", "Disciplined PPS", "immutable raw counter + low-bandwidth PI accumulator"),
]
@@ -1414,7 +1414,7 @@ def generate(output_pdf: Path) -> None:
report,
"İlk HOW neden PPS receive-now değildir?",
required_plots[4],
"Sentetik acquisition/subframe Monte Carlo: mevcut hardware delivery, dokuzuncu takip eden bit nedeniyle HOW sınırından 180 ms ve signal flight time kadar sonradır. HOW +180 ms +68.802 ms kaba yaklaşım bırakır; WLS flight-time + paired timestamp doğru handover'dır.",
"Sentetik acquisition/subframe Monte Carlo: mevcut hardware delivery, dokuzuncu takip eden bit nedeniyle HOW sınırından 180 ms ve signal flight time kadar sonradır. HOW +180 ms tek başına uçuş süresi kadar hata bırakır; offset-free WLS flight-time + paired timestamp doğru handover'dır.",
)
plot_page(
report,
+10 -8
View File
@@ -5,7 +5,8 @@ mirrors the conventions used by gps_system/src/gps_ip.c and NavEquations.c:
* corrected_abs = raw_abs - rem_code_phase / CODE_FREQ_BASIS
* modulo-32 ABS_SAMPLE values are unwrapped around the newest channel
* obs_i = c * ((abs_i - min(abs)) / SAMPLES_PER_CODE + STARTOFFSET) / 1000
* obs_i/c = (corrected_abs_i - corrected_abs_ref) / RF_HZ
- (transmit_i - transmit_ref)
* satellite clock correction is added to the observation
* [x, y, z, common_bias_m] are solved together with URA/elevation weights
@@ -27,8 +28,6 @@ RF_HZ = 16_367_981.0
TRACKING_HZ = 125_000_000.0
SAMPLES_PER_CODE = 16_367.981 # physical RF samples/ms used by the C code
CODE_FREQ_BASIS = 1.023e6
START_OFFSET_MS = 68.802
START_OFFSET_S = START_OFFSET_MS / 1000.0
NAV_SOLVE_PERIOD_S = 10 * 0.020
POSTPONE_TOW_S = 0.180
OMEGA_EARTH = 7.2921151467e-5
@@ -680,10 +679,14 @@ class ReceiverEpochSimulator:
corrected_abs = np.asarray(
[measurement.corrected_abs_unwrapped for measurement in measurements]
)
ps_tvl_ms = corrected_abs / SAMPLES_PER_CODE
reference_index = int(np.argmin(ps_tvl_ms))
precise_tx_ms = ps_tvl_ms - ps_tvl_ms[reference_index] + START_OFFSET_MS
observations_m = precise_tx_ms * (C / 1000.0)
reference_index = int(np.argmin(corrected_abs))
# All channels in this synthetic epoch share one transmit time. Mirror
# firmware's wrap-safe corrected RF-sample difference. The fractional
# code residue removes the 61 ns integer-sample quantization; the
# separately captured 125 MHz counter is used only for PPS epoch mapping.
observations_m = (
corrected_abs - corrected_abs[reference_index]
) * (C / RF_HZ)
observations_m += C * np.asarray(
[measurement.satellite_clock_s for measurement in measurements]
)
@@ -711,7 +714,6 @@ class ReceiverEpochSimulator:
gps_reference_estimate = (
self.gps_tow_start_s
+ transmit_true_s
+ START_OFFSET_S
- wls.clock_bias_m / C
)
gps_reference_true = self.gps_tow_start_s + reference.arrival_true_s
+6 -8
View File
@@ -22,7 +22,6 @@ try:
DEFAULT_IONO,
POSTPONE_TOW_S,
RF_HZ,
START_OFFSET_S,
TRACKING_HZ,
ReceiverEpochSimulator,
build_clock_trajectory,
@@ -41,7 +40,6 @@ except ImportError: # allow: python3 pps_python/simulate_pps.py
DEFAULT_IONO,
POSTPONE_TOW_S,
RF_HZ,
START_OFFSET_S,
TRACKING_HZ,
ReceiverEpochSimulator,
build_clock_trajectory,
@@ -321,7 +319,7 @@ def simulate_startup_tow(
"first_flight_s": first_flight,
# Parsed HOW labels transmit subframe time, not receive-now.
"how_as_receive_error_s": -(POSTPONE_TOW_S + first_flight),
"how_plus_pipeline_startoffset_error_s": START_OFFSET_S - first_flight,
"how_plus_pipeline_without_wls_error_s": -first_flight,
"wls_aligned_error_s": wls_error,
# Old receive reference written as if it were valid at APB-now.
"apb_now_error_s": wls_error - sampled_latency,
@@ -349,13 +347,13 @@ def plot_startup_tow(output_dir: Path, startup: dict[str, np.ndarray]) -> None:
axes[0, 1].grid(True, alpha=0.3)
axes[1, 0].hist(
startup["how_plus_pipeline_startoffset_error_s"] * 1.0e3,
startup["how_plus_pipeline_without_wls_error_s"] * 1.0e3,
bins=35,
color="tab:orange",
alpha=0.8,
)
axes[1, 0].set_xlabel("Phase error [ms]")
axes[1, 0].set_title("Provisional: HOW + 180 ms + 68.802 ms")
axes[1, 0].set_title("Still wrong: HOW + 180 ms without WLS flight time")
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].hist(
@@ -398,7 +396,7 @@ def build_summary(
f"derived GPS reference RMS error: {finite_rms(timing_error)*1e9:.3f} ns",
f"capture-to-commit delay mean/p99: {np.mean(latency)*1e3:.3f} / {np.percentile(latency, 99)*1e3:.3f} ms",
f"first HOW used as receive-now, median phase error: {np.median(startup['how_as_receive_error_s'])*1e3:.3f} ms",
f"first HOW + 180 ms + 68.802 ms, median phase error: {np.median(startup['how_plus_pipeline_startoffset_error_s'])*1e3:.3f} ms",
f"first HOW + 180 ms without WLS, median phase error: {np.median(startup['how_plus_pipeline_without_wls_error_s'])*1e3:.3f} ms",
]
for start, stop, label in [
(0.0, 20.0, "4 satellites"),
@@ -426,8 +424,8 @@ def build_summary(
[
"",
"Interpretation:",
"- bias/c is millisecond-scale because it contains the 68.802 ms synthetic datum and the minimum satellite path.",
"- epoch-aligned TOW (first delivery: HOW + 180 ms) + STARTOFFSET - bias/c reconstructs GPS time at the paired timestamp.",
"- bias/c is millisecond-scale because the timer-difference datum contains the negative reference-satellite path.",
"- epoch-aligned TOW (first delivery: HOW + 180 ms) - bias/c reconstructs GPS time at the paired timestamp.",
"- treating that old reference as valid at APB arrival converts software latency into PPS phase error.",
"- timestamping removes that phase misassociation; control application delay remains only as loop delay.",
"- baremetal applies the fixed 180 ms readout correction once; the PPS peripheral must not add it again.",
@@ -6,6 +6,7 @@ import unittest
from pps_python.adaptive_pps import (
ACQUIRE_PROFILE,
FixedPointAdaptivePps,
Observation,
Profile,
PROFILES,
@@ -59,6 +60,61 @@ def noisy_observations(
class AdaptivePpsTests(unittest.TestCase):
def test_period_aware_coefficients_match_firmware(self):
expected = {
0.2: {
"acquire": (764018, 64006),
Profile.PRECISION: (222838, 5445),
Profile.ROBUST: (84891, 640),
},
0.5: {
"acquire": (615780, 127492),
Profile.PRECISION: (209783, 12804),
Profile.ROBUST: (82980, 1564),
},
1.0: {
"acquire": (447388, 178038),
Profile.PRECISION: (190239, 23149),
Profile.ROBUST: (79923, 3011),
},
}
for period_s, coefficients in expected.items():
self.assertEqual(
ACQUIRE_PROFILE.fixed_coefficients(period_s)[:2],
coefficients["acquire"],
)
for profile in (Profile.PRECISION, Profile.ROBUST):
self.assertEqual(
PROFILES[profile].fixed_coefficients(period_s)[:2],
coefficients[profile],
)
def test_wall_clock_hysteresis_and_settling_scale_with_period(self):
model_500ms = FixedPointAdaptivePps(update_period_s=0.5)
self.assertEqual(model_500ms.selector.upgrade_updates, 4)
self.assertEqual(model_500ms.selector.degrade_updates, 2)
self.assertEqual(
model_500ms.profiles[Profile.PRECISION].software_settle_updates,
5,
)
self.assertEqual(
model_500ms.profiles[Profile.ROBUST].lock_acquire_updates,
4,
)
model_1s = FixedPointAdaptivePps(update_period_s=1.0)
self.assertEqual(model_1s.selector.upgrade_updates, 2)
self.assertEqual(model_1s.selector.degrade_updates, 2)
self.assertEqual(
model_1s.profiles[Profile.PRECISION].software_settle_updates,
3,
)
self.assertEqual(
model_1s.profiles[Profile.ROBUST].lock_acquire_updates,
2,
)
self.assertEqual(model_1s.reference_watchdog_sec, 3.0)
def test_board_calibrated_doppler_predictor_reduces_pps_error(self):
"""Genesys2 WLS statistics: predictor-on must beat fallback-only."""
quality_metrics = QualityMetrics(
+4 -4
View File
@@ -29,17 +29,17 @@ done
export Z3_LIB_DIR="${Z3_LIB_DIR:-$_gps_z3_dir}"
_gps_riscv_gcc=""
if [[ -n "${TOOLCHAIN:-}" ]]; then
_gps_candidates=("$TOOLCHAIN")
elif [[ -n "${RISCV_TOOLCHAIN:-}" ]]; then
if [[ -n "${RISCV_TOOLCHAIN:-}" ]]; then
_gps_candidates=("$RISCV_TOOLCHAIN" "$RISCV_TOOLCHAIN/bin")
elif [[ -n "${TOOLCHAIN:-}" ]]; then
_gps_candidates=("$TOOLCHAIN")
else
_gps_candidates=(
"$GPS_ROOT/tools/local/riscv/bin"
/tools/toolchains-medany/bin
/tools/toolchains/rvtc-medany/bin
/tools/toolchain/bin
/tools/toolchains/bin
/tools/toolchains/rvtc-medany/bin
)
fi
+41 -2
View File
@@ -60,6 +60,10 @@ ETH_RF_SENDER ?= $(OUT_DIR)/tools/send_gps_rf
BITSTREAM_OFFSET ?= 0x00000000
APP_FLASH_OFFSET ?= 0x01800000
# Runtime GPS assistance journal. This tail reservation contains two logical
# 64 KiB record areas on separate 256 KiB erase-sector boundaries.
ASSISTANCE_FLASH_OFFSET ?= 0x01F80000
ASSISTANCE_FLASH_SIZE ?= 0x00080000
FLASH_SIZE_MB ?= 32
FLASH_INTERFACE ?= SPIx4
CFGMEM_PART ?= s25fl256sxxxxxx0-spi-x1_x2_x4
@@ -75,6 +79,7 @@ IMU_SENSOR ?= auto
FLASH_MCS ?= $(OUT_DIR)/flash/genesys2_rocket.mcs
FLASH_BIN ?= $(OUT_DIR)/flash/genesys2_rocket.bin
BIT_FILE ?= $(OUT_DIR)/bitstream/genesys2_rocket.bit
BITSTREAM_PROVENANCE ?= $(BIT_FILE).provenance
DEBUG_DIR := $(FPGA_DIR)/flow/debug
OPENOCD_CFG ?= $(DEBUG_DIR)/openocd_genesys2_bscan.cfg
GDB_LOAD_SCRIPT ?= $(DEBUG_DIR)/gdb_load_and_run.gdb
@@ -98,7 +103,7 @@ FIRRTL_DIR ?= $(HW_GENERATED_DIR)/genesys2-soc
VERILOG_DIR ?= $(HW_GENERATED_DIR)/genesys2-soc-verilog
SOFTWARE_TARGETS := software
BUILD_TARGETS := chisel rtl soc bitstream fpga-config
BUILD_TARGETS := chisel rtl soc bitstream validate-bitstream-bootloader fpga-config
FLASH_IMAGE_TARGETS := flash-image flash-regions
PROGRAM_TARGETS := \
program-fpga \
@@ -267,13 +272,41 @@ bitstream: rtl
-log "$(LOG_DIR)/build_bitstream.log" \
-journal "$(LOG_DIR)/build_bitstream.jou" \
-source "$(FPGA_SCRIPT_DIR)/build.tcl"
test -f "$(BIT_FILE)"
test -f "$(BOOTLOADER_BUILD_DIR)/bootloader.img"
mkdir -p "$$(dirname "$(BITSTREAM_PROVENANCE)")"
tmp="$(BITSTREAM_PROVENANCE).tmp.$$$$"
printf 'bitstream_sha256=%s\nbootloader_sha256=%s\n' \
"$$(sha256sum "$(BIT_FILE)" | awk '{print $$1}')" \
"$$(sha256sum "$(BOOTLOADER_BUILD_DIR)/bootloader.img" | awk '{print $$1}')" \
> "$$tmp"
mv "$$tmp" "$(BITSTREAM_PROVENANCE)"
flash-image: software
validate-bitstream-bootloader:
@if [[ ! -f "$(BIT_FILE)" ]]; then
echo "Missing bitstream: $(BIT_FILE)" >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
if [[ ! -f "$(BITSTREAM_PROVENANCE)" ]]; then
echo "Refusing to combine an unverified/stale FPGA bitstream with the application." >&2
echo "Missing provenance: $(BITSTREAM_PROVENANCE)" >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
expected_bit="$$(sed -n 's/^bitstream_sha256=//p' "$(BITSTREAM_PROVENANCE)")"
expected_boot="$$(sed -n 's/^bootloader_sha256=//p' "$(BITSTREAM_PROVENANCE)")"
actual_bit="$$(sha256sum "$(BIT_FILE)" | awk '{print $$1}')"
actual_boot="$$(sha256sum "$(BOOTLOADER_BUILD_DIR)/bootloader.img" | awk '{print $$1}')"
if [[ -z "$$expected_bit" || -z "$$expected_boot" || \
"$$expected_bit" != "$$actual_bit" || "$$expected_boot" != "$$actual_boot" ]]; then
echo "Refusing to combine a bitstream with a different embedded bootloader/application configuration." >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
flash-image: software
@$(MAKE_BIN) -C "$(FPGA_DIR)" validate-bitstream-bootloader
mkdir -p "$(LOG_DIR)"
cd "$(LOG_DIR)"
BIT_FILE="$(BIT_FILE)" \
@@ -284,6 +317,8 @@ flash-image: software
FLASH_INTERFACE="$(FLASH_INTERFACE)" \
BITSTREAM_OFFSET="$(BITSTREAM_OFFSET)" \
APP_FLASH_OFFSET="$(APP_FLASH_OFFSET)" \
ASSISTANCE_FLASH_OFFSET="$(ASSISTANCE_FLASH_OFFSET)" \
ASSISTANCE_FLASH_SIZE="$(ASSISTANCE_FLASH_SIZE)" \
"$(VIVADO)" -mode batch \
-log "$(LOG_DIR)/write_flash.log" \
-journal "$(LOG_DIR)/write_flash.jou" \
@@ -345,6 +380,8 @@ flash-regions:
FLASH_INTERFACE="$(FLASH_INTERFACE)" \
BITSTREAM_OFFSET="$(BITSTREAM_OFFSET)" \
APP_FLASH_OFFSET="$(APP_FLASH_OFFSET)" \
ASSISTANCE_FLASH_OFFSET="$(ASSISTANCE_FLASH_OFFSET)" \
ASSISTANCE_FLASH_SIZE="$(ASSISTANCE_FLASH_SIZE)" \
FLASH_REGION_NAMES="$$joined" \
"$(VIVADO)" -mode batch \
-log "$(LOG_DIR)/write_flash_regions.log" \
@@ -479,6 +516,8 @@ program-flash-regions:
FLASH_INTERFACE="$(FLASH_INTERFACE)" \
BITSTREAM_OFFSET="$(BITSTREAM_OFFSET)" \
APP_FLASH_OFFSET="$(APP_FLASH_OFFSET)" \
ASSISTANCE_FLASH_OFFSET="$(ASSISTANCE_FLASH_OFFSET)" \
ASSISTANCE_FLASH_SIZE="$(ASSISTANCE_FLASH_SIZE)" \
FLASH_REGION_NAMES="$$region_names" \
"$(VIVADO)" -mode batch \
-log "$(LOG_DIR)/write_flash_regions.log" \
@@ -20,6 +20,8 @@ set flash_size_mb [getenv_default FLASH_SIZE_MB 32]
set flash_interface [getenv_default FLASH_INTERFACE SPIx4]
set bitstream_offset [getenv_default BITSTREAM_OFFSET 0x00000000]
set app_flash_offset [getenv_default APP_FLASH_OFFSET 0x01800000]
set assistance_flash_offset [getenv_default ASSISTANCE_FLASH_OFFSET 0x01f80000]
set assistance_flash_size [getenv_default ASSISTANCE_FLASH_SIZE 0x00080000]
file mkdir [file dirname $mcs_file]
file mkdir [file dirname $bin_file]
@@ -34,12 +36,24 @@ if {![file exists $app_image]} {
set flash_size_bytes [expr {$flash_size_mb * 1024 * 1024}]
set bitstream_end [region_end $bitstream_offset $bit_file]
set app_end [region_end $app_flash_offset $app_image]
set assistance_end [expr {$assistance_flash_offset + $assistance_flash_size}]
if {$bitstream_end > $flash_size_bytes} {
error [format "Bitstream region exceeds flash: end=0x%08x flash_size=0x%08x" $bitstream_end $flash_size_bytes]
}
if {$app_end > $flash_size_bytes} {
error [format "App region exceeds flash: end=0x%08x flash_size=0x%08x" $app_end $flash_size_bytes]
}
if {$assistance_end > $flash_size_bytes} {
error [format "GPS assistance reservation exceeds flash: end=0x%08x flash_size=0x%08x" $assistance_end $flash_size_bytes]
}
if {$bitstream_offset < $assistance_end && $assistance_flash_offset < $bitstream_end} {
error [format "Bitstream overlaps GPS assistance reservation: bit=[0x%08x,0x%08x) assistance=[0x%08x,0x%08x)" \
$bitstream_offset $bitstream_end $assistance_flash_offset $assistance_end]
}
if {$app_flash_offset < $assistance_end && $assistance_flash_offset < $app_end} {
error [format "App overlaps GPS assistance reservation: app=[0x%08x,0x%08x) assistance=[0x%08x,0x%08x)" \
$app_flash_offset $app_end $assistance_flash_offset $assistance_end]
}
if {$bitstream_offset < $app_end && $app_flash_offset < $bitstream_end} {
error [format "Bitstream/app flash regions overlap: bit=[0x%08x,0x%08x) app=[0x%08x,0x%08x)" \
$bitstream_offset $bitstream_end $app_flash_offset $app_end]
@@ -52,6 +66,7 @@ puts " size/interface: ${flash_size_mb}MiB $flash_interface"
puts [format " flash size: 0x%08x bytes" $flash_size_bytes]
puts [format " bitstream end: 0x%08x" $bitstream_end]
puts [format " app end: 0x%08x" $app_end]
puts [format " assistance reserved: 0x%08x-0x%08x" $assistance_flash_offset $assistance_end]
write_cfgmem -force -format MCS -size $flash_size_mb -interface $flash_interface \
-loadbit "up $bitstream_offset $bit_file" \
@@ -20,8 +20,14 @@ set flash_size_mb [getenv_default FLASH_SIZE_MB 32]
set flash_interface [getenv_default FLASH_INTERFACE SPIx4]
set bitstream_offset [getenv_default BITSTREAM_OFFSET 0x00000000]
set app_flash_offset [getenv_default APP_FLASH_OFFSET 0x01800000]
set assistance_flash_offset [getenv_default ASSISTANCE_FLASH_OFFSET 0x01f80000]
set assistance_flash_size [getenv_default ASSISTANCE_FLASH_SIZE 0x00080000]
set region_names [split [getenv_default FLASH_REGION_NAMES "bitstream;app"] ";"]
set flash_size_bytes [expr {$flash_size_mb * 1024 * 1024}]
set assistance_end [expr {$assistance_flash_offset + $assistance_flash_size}]
if {$assistance_end > $flash_size_bytes} {
error [format "GPS assistance reservation exceeds flash: end=0x%08x flash_size=0x%08x" $assistance_end $flash_size_bytes]
}
foreach region $region_names {
if {$region eq ""} {
@@ -38,6 +44,10 @@ foreach region $region_names {
error [format "Bitstream region crosses APP_FLASH_OFFSET: bit_end=0x%08x app_offset=0x%08x" \
$bitstream_end $app_flash_offset]
}
if {$bitstream_offset < $assistance_end && $assistance_flash_offset < $bitstream_end} {
error [format "Bitstream overlaps GPS assistance reservation: bit=[0x%08x,0x%08x) assistance=[0x%08x,0x%08x)" \
$bitstream_offset $bitstream_end $assistance_flash_offset $assistance_end]
}
file mkdir [file dirname $bitstream_mcs]
puts "Generating bitstream region MCS:"
puts " bitstream: $bit_file @ $bitstream_offset"
@@ -55,6 +65,10 @@ foreach region $region_names {
if {$app_end > $flash_size_bytes} {
error [format "App region exceeds flash: end=0x%08x flash_size=0x%08x" $app_end $flash_size_bytes]
}
if {$app_flash_offset < $assistance_end && $assistance_flash_offset < $app_end} {
error [format "App overlaps GPS assistance reservation: app=[0x%08x,0x%08x) assistance=[0x%08x,0x%08x)" \
$app_flash_offset $app_end $assistance_flash_offset $assistance_end]
}
file mkdir [file dirname $app_mcs]
puts "Generating app region MCS:"
puts " app image: $app_image @ $app_flash_offset"
@@ -179,6 +179,8 @@ def main():
value = "gps_rf_clk_selected"
elif name == "gps_rf_data":
value = "gps_rf_data_selected"
elif name == "gps_clock_ready":
value = "gps_clock_locked & gps_rf_ready"
elif name == "gps_afe_clk":
value = "gps_rf_clk_selected"
elif name == "debug_systemjtag_jtag_TCK":
@@ -775,7 +777,9 @@ module genesys2_rocket_top (
.O(gps_trck_clk_gen)
);
wire pll_ready_async = fabric_pll_locked & gps_clock_locked & gps_rf_ready & cpu_resetn;
// The Rocket/fabric clock and RAM cache must survive loss of the external RF
// clock. gps_clock_ready independently resets only the GPS DSP domains.
wire pll_ready_async = fabric_pll_locked & cpu_resetn;
(* async_reg = "true" *) reg [2:0] pll_ready_sync = 3'b000;
reg [15:0] reset_shift = 16'hFFFF;
always @(posedge fabric_clk) begin
@@ -177,6 +177,8 @@ def main():
value = "gps_rf_clk_selected"
elif name == "gps_rf_data":
value = "gps_rf_data_selected"
elif name == "gps_clock_ready":
value = "gps_clock_locked & gps_rf_ready"
elif name == "gps_afe_clk":
value = "gps_rf_clk_selected"
elif name == "debug_systemjtag_jtag_TCK":
@@ -784,7 +786,9 @@ module vcu118_rocket_top (
.O(gps_trck_clk_gen)
);
wire pll_ready_async = fabric_pll_locked & gps_clock_locked & gps_rf_ready & ~cpu_reset;
// The Rocket/fabric clock and RAM cache must survive loss of the external RF
// clock. gps_clock_ready independently resets only the GPS DSP domains.
wire pll_ready_async = fabric_pll_locked & ~cpu_reset;
(* async_reg = "true" *) reg [2:0] pll_ready_sync = 3'b000;
reg [15:0] reset_shift = 16'hFFFF;
always @(posedge fabric_clk) begin
+21
View File
@@ -30,6 +30,27 @@ if [[ -n "$TOOLCHAIN" ]]; then
fi
check riscv-gcc "$riscv_gcc" --version
if [[ -n "$riscv_gcc" && -x "$riscv_gcc" ]]; then
medany_dir="$(mktemp -d "${TMPDIR:-/tmp}/gps-medany-check.XXXXXXXX")"
printf '%s\n' \
'#include <stdlib.h>' \
'void _start(void) { void *p = malloc(16); free(p); for (;;) {} }' \
> "$medany_dir/check.c"
if "$riscv_gcc" \
-march=rv64imafdc_zicsr_zifencei -mabi=lp64d -mcmodel=medany \
-Os -ffreestanding -fno-builtin -static -nostartfiles \
-Wl,-Ttext=0x80000000 -Wl,--unresolved-symbols=ignore-all \
"$medany_dir/check.c" -Wl,--start-group -lc -lgcc -Wl,--end-group \
-o "$medany_dir/check.elf" >/dev/null 2>&1; then
printf 'OK %-10s %s\n' riscv-libc 'medany @ 0x80000000'
else
printf 'INCOMPAT %-10s %s\n' riscv-libc \
'libc is not medany; select RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany'
status=1
fi
rm -rf "$medany_dir"
fi
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
printf 'WARNING Kilitli JDK/CIRCT paketleri Linux-x86_64 icindir.\n'
fi
+15 -4
View File
@@ -21,23 +21,32 @@ VIVADO ?= $(lastword $(sort $(wildcard /tools/Xilinx/*/Vivado/bin/vivado) $(shel
# TOOLCHAIN keeps compatibility with the existing software Makefiles.
RISCV_TOOLCHAIN ?=
_RISCV_SEARCH_DIRS := \
$(if $(strip $(RISCV_TOOLCHAIN)),$(RISCV_TOOLCHAIN) $(RISCV_TOOLCHAIN)/bin) \
$(TOOLCHAIN) \
$(RISCV_TOOLCHAIN) \
$(RISCV_TOOLCHAIN)/bin \
$(PROJECT_TOOLS_DIR)/riscv/bin \
/tools/toolchains-medany/bin \
/tools/toolchains/rvtc-medany/bin \
/tools/toolchain/bin \
/tools/toolchains/bin \
/tools/toolchains/rvtc-medany/bin
/tools/toolchains/bin
_RISCV_GCC := $(firstword \
$(foreach d,$(_RISCV_SEARCH_DIRS),$(wildcard $(d)/riscv64-unknown-elf-gcc)) \
$(foreach d,$(_RISCV_SEARCH_DIRS),$(wildcard $(d)/riscv64-none-elf-gcc)) \
$(shell command -v riscv64-unknown-elf-gcc 2>/dev/null) \
$(shell command -v riscv64-none-elf-gcc 2>/dev/null))
# An explicit RISCV_TOOLCHAIN is authoritative, including over an old
# TOOLCHAIN/RISCV_PREFIX inherited from the shell. Without override, Make's
# environment-variable semantics leave CROSS pointing at the stale toolchain
# even though _RISCV_GCC was found in RISCV_TOOLCHAIN.
ifneq ($(strip $(RISCV_TOOLCHAIN)),)
override TOOLCHAIN := $(patsubst %/,%,$(dir $(_RISCV_GCC)))
override RISCV_PREFIX := $(if $(_RISCV_GCC),$(patsubst %-gcc,%-,$(notdir $(_RISCV_GCC))),riscv64-unknown-elf-)
override CROSS := $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
else
TOOLCHAIN ?= $(patsubst %/,%,$(dir $(_RISCV_GCC)))
RISCV_PREFIX ?= $(if $(_RISCV_GCC),$(patsubst %-gcc,%-,$(notdir $(_RISCV_GCC))),riscv64-unknown-elf-)
CROSS ?= $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
endif
# Some compiler-only toolchains (notably rvtc-medany) do not ship GDB. Keep
# using the selected compiler toolchain, but fall back to the full local
@@ -45,6 +54,8 @@ CROSS ?= $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
_RISCV_GDB := $(firstword \
$(wildcard $(TOOLCHAIN)/$(RISCV_PREFIX)gdb) \
$(wildcard /tools/toolchains/tc/toolchain/bin/$(RISCV_PREFIX)gdb) \
$(wildcard /tools/toolchain/bin/$(RISCV_PREFIX)gdb) \
$(lastword $(sort $(wildcard /tools/Xilinx/*/gnu/riscv/lin/bin/$(RISCV_PREFIX)gdb))) \
$(shell command -v $(RISCV_PREFIX)gdb 2>/dev/null))
GDB ?= $(if $(_RISCV_GDB),$(_RISCV_GDB),$(CROSS)gdb)