3 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
73 changed files with 6617 additions and 261 deletions
+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, _) =>
+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)
@@ -85,6 +85,7 @@
const double* cn0_dbhz,
const ionoutc_t* ionoutc, double gps_tow,
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,6 +49,10 @@
/* 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
@@ -63,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
@@ -89,6 +115,8 @@
#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"
@@ -190,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)
@@ -250,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
@@ -289,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");
@@ -480,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.
@@ -505,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
@@ -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(
@@ -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,16 +86,68 @@ 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))
{
int priority = (manager->visibility_active &&
entry->visibility_known)
? entry->visibility_priority : 0;
if(!found || (priority > best_priority))
{
*prn_out = prn;
return true;
best_priority = priority;
found = true;
}
}
}
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(
gps_acq_manager_t* manager,
@@ -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) &&
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_frame_parsed->gps_ephems.ephemeris_valid)
{
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 ephemeris cache: prn=%u trk=%u iode=%u\r\n",
"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,14 +2087,27 @@ 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))
{
if(state == GPS_ACQ_PRN_STALE)
{
gps_mark_satellite_tracking(prn, recovered_trk_ip);
}
else
{
gps_acq_manager_mark_tracking(&gps_acq_manager, prn);
}
}
}
}
static bool gps_acq_engine_available(unsigned int engine_id, volatile gps_acq_t* acq_ip)
{
@@ -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());
}
+76 -17
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);
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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.
@@ -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)
+10 -8
View File
@@ -30,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:
@@ -132,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
@@ -176,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
@@ -205,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.
+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.",
+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