26 Commits

Author SHA1 Message Date
Codex dce1dd5ea9 Fix GPS nav-solve phase seeding for cold PVT 2026-08-04 15:52:36 +03:00
Codex 28b55fdfb6 Fix GPS acquisition visibility and pull-in edge cases 2026-08-04 13:48:48 +03:00
onur 30179df8e6 feat(gps): add timer-based assisted reacquisition
Remove the fixed pseudorange start offset, preserve per-PRN navigation assistance across RF loss and power cycles, and seed coherent navigation measurement phase on reacquisition. Add full LNAV almanac visibility scheduling, power-safe QSPI journaling, RF-domain reset isolation, and PPS Gray CDC hardening with Linux, sanitizer, RTL, and end-to-end coverage.
2026-08-04 02:58:11 +03:00
onur 347e1c671d feat(pps): preserve loop dynamics across nav periods 2026-07-29 00:45:16 +03:00
onur a6bb080270 fix: select medany toolchain and available GDB 2026-07-29 00:16:57 +03:00
onur 9b0828dbaa feat: replay live PPS telemetry in fixed-point model 2026-07-29 00:16:43 +03:00
onur a697284bff fix(pps): align firmware TOW with current RTL 2026-07-28 21:53:33 +03:00
onur 988f9686e7 test(pps): analyze loop stability and telemetry 2026-07-28 21:41:41 +03:00
onur 73db2a8355 test(pps): add end-to-end timing validation 2026-07-28 21:13:10 +03:00
onur 156cfc8b77 fix(gps): adapt Doppler bandwidth from board validation 2026-07-26 03:15:52 +03:00
onur 23650be635 feat(gps): split Doppler paths for ESKF and PPS 2026-07-26 01:59:08 +03:00
onur 49b8609bb4 feat(gps): expose ESKF build profiles and 460800 UART 2026-07-25 19:39:36 +03:00
onur ecbb8d0b68 feat(pps): add adaptive disciplined timing loop 2026-07-25 17:56:41 +03:00
onur 0f6b3482c5 docs(pps): record live lock diagnosis 2026-07-25 13:32:24 +03:00
onur 971be036a0 feat(fusion): fuse Doppler velocity in ESKF 2026-07-25 13:11:33 +03:00
onur 2da5fb7153 docs(gps): document tracking quality and Doppler pipeline 2026-07-25 12:16:59 +03:00
onur 5b8cd8560f fix(gps): smooth Doppler measurements before velocity WLS 2026-07-25 12:09:02 +03:00
onur b2ca7db633 feat(gps): expose Doppler and signal quality measurements 2026-07-25 11:30:38 +03:00
onur 64860d1ced fix(gps): recover when tracking loss IRQ is missed 2026-07-24 20:33:45 +03:00
onur 86dfbeafac fix(gps): make acquisition and reacquisition deterministic 2026-07-24 20:13:33 +03:00
onur e57b19b4ee fix(gps): synchronize complete raw navigation epochs 2026-07-24 15:00:40 +03:00
onur fe0eb19dc5 fix(gps): harden navigation epoch recovery 2026-07-24 14:01:58 +03:00
onur ea055a36d2 fix(fusion): harden ESKF GPS failure handling 2026-07-24 12:33:26 +03:00
onur 93766b007f Clarify zero-based tracking PRN indices 2026-07-24 12:06:06 +03:00
onur bdce4a3046 Rate limit corrected UART telemetry 2026-07-23 23:55:33 +03:00
onur 9ff895600b Keep fused location streaming through GPS rejects 2026-07-23 23:29:08 +03:00
191 changed files with 30926 additions and 2644 deletions
+5
View File
@@ -7,3 +7,8 @@
# Generated build outputs.
/out/
/soc/out/
# Python runtime caches and privacy-sensitive raw board captures.
__pycache__/
*.py[cod]
/soc/software/pps_end_to_end_validation/hardware_logs/*.csv
+3
View File
@@ -40,6 +40,9 @@ help:
'' \
'Config:' \
' FPGA_BOARD=$(FPGA_BOARD) # vcu118 or genesys2' \
' UART_BAUD=460800 # FPGA firmware and host listener default' \
' GPS_ESKF_ENABLE=1 # gps_system: 1 ESKF, 0 GPS-only' \
' IMU_SENSOR=auto # gps_system: auto, tdk, st or stm' \
'' \
'Direct entry points:' \
' make -C soc help' \
+133
View File
@@ -86,6 +86,139 @@ make -C soc/software app APP=gps_system
make -C targets/fpga/vcu118 debug-load APP=gps_system
```
## GPS Firmware Profilleri: ESKF ve GPS-only
`gps_system` varsayilan olarak ESKF acik (`GPS_ESKF_ENABLE=1`) ve UART
`460800` baud olacak sekilde derlenir. Kok Makefile'in kart varsayilani VCU118
oldugu icin Genesys2 komutlarinda `FPGA_BOARD=genesys2` acikca yazilmalidir.
TDK ICM42688 + GPS + ESKF iceren birlesik Genesys2 MCS/BIN:
```sh
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-mcs
```
Yalniz GPS acquisition/tracking, WLS, Doppler velocity ve PPS calistiran;
IMU/ESKF baslatmayan firmware:
```sh
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=0 fpga-mcs
```
`GPS_ESKF_ENABLE` verilmezse deger `1`'dir. Iki komut ayni
`targets/fpga/genesys2/out/flash/genesys2_rocket.mcs` ve `.bin` yollarini
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:
```sh
make FPGA_BOARD=genesys2 APP=gps_system fpga-bitstream
make FPGA_BOARD=genesys2 APP=gps_system GPS_ESKF_ENABLE=1 \
IMU_SENSOR=tdk fpga-mcs
```
Firmware ve host UART dinleyicisinin varsayilani birlikte `460800`'dur:
```sh
make FPGA_BOARD=genesys2 fpga-uart
```
### Doppler, WLS ve ESKF veri yolu
```text
tracking loop, her 20 ms:
carrier Q7 + 20 prompt I/Q + wideband power + ABS_SAMPLE/code phase
-> sabit tracking snapshot
-> synchronizer + APB-domain atomik register + interrupt
-> C/N0 ve Doppler/range-rate donusumu
-> ham Doppler + tek-kanal FDE ile C/N0-weighted velocity WLS
-> ECEF velocity + 3x3 covariance + receiver clock drift
-> ECEF->ENU adapter
-> ayri velocity NIS gate
-> ayni capture epoch'unda 15-state ESKF update
-> daha yeni IMU history replay
```
RTL Doppler veya C/N0 uzerinde IIR yapmaz; ham ve 20 ms sabit snapshot'i
software'e verir. Software ayni PRN icin C/N0'yi `alpha=0.2` ile yumusatir;
Doppler'in hem ham de `alpha=0.05` IIR sonucu korunur. L1 range-rate
`-doppler_hz * c / 1575420000` ile hesaplanir. Ham yol tek-kanal
leave-one-out FDE'li velocity WLS'ten ESKF'ye, filtreli yol ayri velocity
WLS'ten yalniz PPS clock-bias drift predictor'una gider. PPS'in ana faz/zaman
olcumu position WLS `clock_bias_m` sonucudur; Doppler yalniz 200 ms epoch'lar
arasindaki turev prediction'ina ve kalite siniflandirmasina yardim eder. Her
iki velocity WLS state'i `[vx, vy, vz, receiver_clock_drift_mps]`'dir; uydu
LOS hizi ve broadcast satellite clock drift observation'dan ayrilir. C/N0,
range-rate agirligini belirler; post-fit residual gerekirse formal
covariance'i yalniz buyutur.
Klasik mutlak-pseudorange GNSS modelinde receiver zaman hatasi
`delta_t_r(t)` ise:
```text
code WLS 4. state: b_r = c * delta_t_r [metre]
Doppler WLS 4. state: bdot_r = c * d(delta_t_r)/dt [metre/saniye]
b_r(t + dt) ~= b_r(t) + bdot_r * dt
```
Burada `bdot_r`, receiver oscillator frequency error'inin range-rate
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 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.
ESKF; ENU position/velocity, quaternion attitude, accel bias ve gyro bias
iceren 15 hata durumlu filtredir. Her timestamp'li IMU ornegi prediction'a
girer. Gecikmis GPS position ve Doppler velocity olcumleri kendi capture
epoch'unda uygulanir, ardindan daha yeni IMU history replay edilir. Position
ve velocity'nin ayri NIS kapilari vardir; kotu Doppler ayni epoch'taki gecerli
position update'ini iptal etmez. Durağanlik supervisor'u uygun oldugunda ZUPT
ve zero-gyro update uygular; GPS kesintisinde filtre sahte konum olcumu
uretmeden dead-reckoning yapar ve covariance buyur.
C/N0, geometri, WLS clock sigma, integrity ve Doppler residual metrikleri PPS
software'inin `acquire/precision/robust/holdover` profilini de secer. PPS
donanimi bu metrikleri dogrudan yorumlamaz; software v2.2 mailbox ile PI
`Kp/Ki`, slew ve lock esiklerini atomik olarak gunceller. Ayrintilar:
- `hardware/external/gps/rtl/trck_src/README.md`
- `soc/software/gps_system/README.md`
- `soc/software/fusion/README.md`
- `hardware/external/gps/rtl/pps/README.md`
- `soc/software/pps_python/README.md`
Alt girisler:
```sh
+6
View File
@@ -46,3 +46,9 @@ external/gps/rtl/
pass this path as the default GPS_SYSTEM_DIR, so the Vivado flow does not
depend on a user-specific /home/... checkout.
```
GPS tracking olcum yolunun 20 ms Doppler/C/N0 snapshot, PCLK synchronizer,
APB register/interrupt ve software velocity WLS sozlesmesi
`external/gps/rtl/trck_src/README.md` icinde belgelenir. PPS PI periferigi,
runtime coefficient/lock register map'i ve fiziksel pin sinirlari
`external/gps/rtl/pps/README.md` icindedir.
+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
+270 -19
View File
@@ -40,6 +40,14 @@ module tracking_wrapper #(
import gps_top_pkg::*;
import trck_pkg::*;
localparam int unsigned MEAS_CARRIER_WL =
ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
localparam int unsigned MEAS_SUM_WL =
TRCK_CORR_WL + $clog2(MS_PER_BIT);
localparam int unsigned MEAS_WBP_WL =
(2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
localparam int unsigned MEAS_COUNT_WL = $clog2(MS_PER_BIT + 1);
// apb state machine
apb_state_e apb_bus_ps, apb_bus_ns;
// tracking lock state machine
@@ -59,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;
//
@@ -70,6 +80,22 @@ module tracking_wrapper #(
logic nav_bit, nav_bit_syned; // navigation bits.
logic [31 : 0] abs_sample, abs_sample_syned;
logic [REM_CODE_PH_I_WL-1 : 0] rem_code_ph, rem_code_ph_syned;
logic measurement_valid, measurement_valid_syned;
logic measurement_bit, measurement_bit_syned;
logic [31:0] measurement_abs_sample, measurement_abs_sample_syned;
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph;
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph_syned;
logic signed [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7;
logic [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7_syned;
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_i;
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_q;
logic [MEAS_SUM_WL-1:0] measurement_cn0_sum_i_syned;
logic [MEAS_SUM_WL-1:0] measurement_cn0_sum_q_syned;
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp;
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp_syned;
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count;
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count_syned;
logic [15:0] measurement_sequence, measurement_sequence_syned;
logic preamble_ok;
// ( internal flags )
@@ -77,6 +103,7 @@ module tracking_wrapper #(
logic trck_nav_bit_rcv_flag_p, trck_nav_bit_rcv_flag_n;
logic trck_timeout_flag_p, trck_timeout_flag_n;
logic trck_nav_solve_flag_p, trck_nav_solve_flag_n;
logic trck_measurement_flag_p, trck_measurement_flag_n;
logic [31 : 0] timeout_counter_p, timeout_counter_n; // tracking auto timeout value
@@ -92,17 +119,36 @@ 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.
logic measurement_valid_r;
logic measurement_capture_pending;
logic [1:0] measurement_settle_count;
logic measurement_capture;
logic measurement_interrupt_pulse;
logic measurement_data_valid_reg;
logic measurement_bit_reg;
logic measurement_lock_reg;
logic [31:0] measurement_abs_sample_reg;
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph_reg;
logic signed [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7_reg;
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_i_reg;
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_q_reg;
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp_reg;
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count_reg;
logic [15:0] measurement_sequence_reg;
logic[2 : 0] start_trck_ctl_p, start_trck_ctl_n;
logic[2 : 0] stop_trck_ctl_p, stop_trck_ctl_n;
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
@@ -152,11 +198,55 @@ module tracking_wrapper #(
.aresetn(PRESETN),
.Din(rem_code_ph),
.Dout(rem_code_ph_syned)
);
);
double_ff_syn#(1, 2) i_measurement_valid_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_valid), .Dout(measurement_valid_syned)
);
double_ff_syn#(1, 2) i_measurement_bit_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_bit), .Dout(measurement_bit_syned)
);
double_ff_syn#(32, 2) i_measurement_abs_sample_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_abs_sample), .Dout(measurement_abs_sample_syned)
);
double_ff_syn#(REM_CODE_PH_I_WL, 2) i_measurement_rem_code_ph_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_rem_code_ph), .Dout(measurement_rem_code_ph_syned)
);
double_ff_syn#(MEAS_CARRIER_WL, 2) i_measurement_carrier_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_carrier_freq_q7),
.Dout(measurement_carrier_freq_q7_syned)
);
double_ff_syn#(MEAS_SUM_WL, 2) i_measurement_sum_i_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_cn0_sum_i), .Dout(measurement_cn0_sum_i_syned)
);
double_ff_syn#(MEAS_SUM_WL, 2) i_measurement_sum_q_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_cn0_sum_q), .Dout(measurement_cn0_sum_q_syned)
);
double_ff_syn#(MEAS_WBP_WL, 2) i_measurement_wbp_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_cn0_wbp), .Dout(measurement_cn0_wbp_syned)
);
double_ff_syn#(MEAS_COUNT_WL, 2) i_measurement_count_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_cn0_sample_count),
.Dout(measurement_cn0_sample_count_syned)
);
double_ff_syn#(16, 2) i_measurement_sequence_syn(
.clk(PCLK), .aresetn(PRESETN),
.Din(measurement_sequence), .Dout(measurement_sequence_syned)
);
trck_top i_trck_top(
// common signals
.trk_rstn(PRESETN),
.afe_rstn(PRESETN),
.trk_rstn(trck_resetn),
.afe_rstn(afe_resetn),
.trk_clk(trck_clk),
.afe_clk(afe_clk),
.sample_align_i(sample_align), // from afe
@@ -173,15 +263,83 @@ module tracking_wrapper #(
// output to regs. (all values are valid on bit_vld except lock ?)
.bit_o(nav_bit),
.bit_vld_o(bit_valid),
.abs_sample_o(abs_sample), // 20 ms bit valid aligned
.rem_code_ph_o(rem_code_ph),
.lock_o(lock),
.abs_sample_o(abs_sample), // 20 ms bit valid aligned
.rem_code_ph_o(rem_code_ph),
.measurement_vld_o(measurement_valid),
.measurement_bit_o(measurement_bit),
.measurement_abs_sample_o(measurement_abs_sample),
.measurement_rem_code_ph_o(measurement_rem_code_ph),
.measurement_carrier_freq_q7_o(measurement_carrier_freq_q7),
.measurement_cn0_sum_i_o(measurement_cn0_sum_i),
.measurement_cn0_sum_q_o(measurement_cn0_sum_q),
.measurement_cn0_wbp_o(measurement_cn0_wbp),
.measurement_cn0_sample_count_o(measurement_cn0_sample_count),
.measurement_sequence_o(measurement_sequence),
.lock_o(lock),
.busy_o(busy),
//unconnected ( for debug purposes )
.fifo_of_o()
);
);
/*
* The source payload is stable for 20 ms. Its valid marker and every data
* bit pass through the same three-stage synchronizer; two extra PCLK cycles
* after the synchronized rising edge provide a conservative bus-settle
* margin before the software-visible registers and interrupt are updated.
*/
always_ff @(posedge PCLK or negedge PRESETN) begin : measurement_capture_seq
if(!PRESETN) begin
measurement_valid_r <= 1'b0;
measurement_capture_pending <= 1'b0;
measurement_settle_count <= '0;
measurement_capture <= 1'b0;
measurement_interrupt_pulse <= 1'b0;
measurement_data_valid_reg <= 1'b0;
measurement_bit_reg <= 1'b0;
measurement_lock_reg <= 1'b0;
measurement_abs_sample_reg <= '0;
measurement_rem_code_ph_reg <= '0;
measurement_carrier_freq_q7_reg <= '0;
measurement_cn0_sum_i_reg <= '0;
measurement_cn0_sum_q_reg <= '0;
measurement_cn0_wbp_reg <= '0;
measurement_cn0_sample_count_reg <= '0;
measurement_sequence_reg <= '0;
end else begin
measurement_valid_r <= measurement_valid_syned;
measurement_capture <= 1'b0;
measurement_interrupt_pulse <= measurement_capture;
if(measurement_valid_syned && !measurement_valid_r) begin
measurement_capture_pending <= 1'b1;
measurement_settle_count <= 2;
end else if(measurement_capture_pending) begin
if(measurement_settle_count != 0) begin
measurement_settle_count <= measurement_settle_count - 1'b1;
end else begin
measurement_capture_pending <= 1'b0;
measurement_capture <= 1'b1;
measurement_data_valid_reg <= 1'b1;
measurement_bit_reg <= measurement_bit_syned;
measurement_lock_reg <= lock_syned;
measurement_abs_sample_reg <= measurement_abs_sample_syned;
measurement_rem_code_ph_reg <= measurement_rem_code_ph_syned;
measurement_carrier_freq_q7_reg <=
$signed(measurement_carrier_freq_q7_syned);
measurement_cn0_sum_i_reg <=
$signed(measurement_cn0_sum_i_syned);
measurement_cn0_sum_q_reg <=
$signed(measurement_cn0_sum_q_syned);
measurement_cn0_wbp_reg <= measurement_cn0_wbp_syned;
measurement_cn0_sample_count_reg <=
measurement_cn0_sample_count_syned;
measurement_sequence_reg <= measurement_sequence_syned;
end
end
end
end
@@ -208,17 +366,20 @@ 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;
trck_timeout_flag_p <= '0;
trck_nav_solve_flag_p <= '0;
trck_nav_bit_rcv_flag_p <= '0;
trck_measurement_flag_p <= '0;
// control reg bit fields set by reset.
control_reg_p[TRCK_NAV_BITS_FLAG_ENABLE_POS] <= 1'b1;
control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] <= 1'b1;
control_reg_p[TRCK_STOP_ON_UNLOCK_FLAG_EN_POS] <= 1'b1;
control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS] <= 1'b1;
foreach(nav_bit_read_reg_p[i])
nav_bit_read_reg_p[i] <= '0;
@@ -238,12 +399,14 @@ 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;
trck_timeout_flag_p <= trck_timeout_flag_n;
trck_nav_bit_rcv_flag_p <= trck_nav_bit_rcv_flag_n;
trck_nav_solve_flag_p <= trck_nav_solve_flag_n;
trck_measurement_flag_p <= trck_measurement_flag_n;
foreach(nav_bit_read_reg_p[i])
nav_bit_read_reg_p[i] <= nav_bit_read_reg_n[i];
end
@@ -409,6 +572,10 @@ module tracking_wrapper #(
if((control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS]) & (nav_solve_timer_reached)) begin
gps_frame_ready = '1;
end
// intr due to a coherent 20 ms Doppler/CN0 measurement.
if(control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS] && measurement_interrupt_pulse) begin
gps_frame_ready = '1;
end
end
end
@@ -419,6 +586,7 @@ module tracking_wrapper #(
trck_timeout_flag_n = trck_timeout_flag_p; // (all internal clear signals are tied to generic int_clear signal)
trck_nav_bit_rcv_flag_n = trck_nav_bit_rcv_flag_p;
trck_nav_solve_flag_n = trck_nav_solve_flag_p;
trck_measurement_flag_n = trck_measurement_flag_p;
foreach(nav_bit_read_reg_n[i])
nav_bit_read_reg_n[i] = nav_bit_read_reg_p[i];
@@ -443,12 +611,21 @@ module tracking_wrapper #(
if(control_reg_p[TRCK_INTRPT_ENABLE_POS] && control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS]) intr_pending_n = '1;
end
if(measurement_capture) begin
if(control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS])
trck_measurement_flag_n = '1;
if(control_reg_p[TRCK_INTRPT_ENABLE_POS] &&
control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS])
intr_pending_n = '1;
end
// generic clear all interrupt pending flag and other flags.
if(intr_clear) begin // all internal flag clear
intr_pending_n = '0;
trck_timeout_flag_n = '0;
trck_nav_bit_rcv_flag_n = '0;
trck_nav_solve_flag_n = '0;
trck_measurement_flag_n = '0;
end
end : nav_bit_load_and_intr_flag_comb
@@ -459,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.
@@ -484,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
@@ -515,6 +697,10 @@ module tracking_wrapper #(
if(PWDATA[TRCK_NAV_SOLVE_FLAG_EN_POS]) control_reg_n[TRCK_NAV_SOLVE_FLAG_EN_POS] = 1'b1;
if(!PWDATA[TRCK_NAV_SOLVE_FLAG_EN_POS]) control_reg_n[TRCK_NAV_SOLVE_FLAG_EN_POS] = 1'b0;
if(PWDATA[TRCK_MEASUREMENT_INTR_EN_POS]) control_reg_n[TRCK_MEASUREMENT_INTR_EN_POS] = 1'b1;
if(!PWDATA[TRCK_MEASUREMENT_INTR_EN_POS]) control_reg_n[TRCK_MEASUREMENT_INTR_EN_POS] = 1'b0;
if(PWDATA[TRCK_MEASUREMENT_FLAG_EN_POS]) control_reg_n[TRCK_MEASUREMENT_FLAG_EN_POS] = 1'b1;
if(!PWDATA[TRCK_MEASUREMENT_FLAG_EN_POS]) control_reg_n[TRCK_MEASUREMENT_FLAG_EN_POS] = 1'b0;
end
(LOW_ADDRESS + PRN_NUMBER_REG_OFFSET):
begin
@@ -539,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:
@@ -560,20 +752,23 @@ module tracking_wrapper #(
(control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_INTR_EN_POS) |
(control_reg_p[TRCK_NAV_BITS_FLAG_ENABLE_POS] << trck_pkg::TRCK_NAV_BITS_FLAG_ENABLE_POS) |
(control_reg_p[TRCK_STOP_ON_UNLOCK_FLAG_EN_POS] << trck_pkg::TRCK_STOP_ON_UNLOCK_FLAG_EN_POS) |
(control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_FLAG_EN_POS)
(control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_FLAG_EN_POS) |
(control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS] << trck_pkg::TRCK_MEASUREMENT_INTR_EN_POS) |
(control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS] << trck_pkg::TRCK_MEASUREMENT_FLAG_EN_POS)
);
end
(LOW_ADDRESS + STATUS_REG_OFFSET):
begin
PRDATA = 32'(
(busy_syned << TRCK_BUSY_POS) |
(lock_syned << TRCK_LOCKED_POS) |
(0 << TRCK_FIFO_OV_POS) | // fifo overflow is not considered for now.
(intr_pending_p << TRCK_INTR_PEND_POS) |
(trck_timeout_flag_p << TRCK_UNLOCK_FLAG_POS) |
(trck_nav_bit_rcv_flag_p << TRCK_NAV_BITS_RCV_FLAG_POS) |
(trck_nav_solve_flag_p << TRCK_NAV_SOLVE_FLAG_POS)
);
(busy_syned << TRCK_BUSY_POS) |
(lock_syned << TRCK_LOCKED_POS) |
(0 << TRCK_FIFO_OV_POS) | // fifo overflow is not considered for now.
(intr_pending_p << TRCK_INTR_PEND_POS) |
(trck_timeout_flag_p << TRCK_UNLOCK_FLAG_POS) |
(trck_nav_bit_rcv_flag_p << TRCK_NAV_BITS_RCV_FLAG_POS) |
(trck_nav_solve_flag_p << TRCK_NAV_SOLVE_FLAG_POS) |
(trck_measurement_flag_p << TRCK_MEASUREMENT_FLAG_POS)
);
end
(LOW_ADDRESS + PRN_NUMBER_REG_OFFSET):
@@ -648,11 +843,67 @@ 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;
PRDATA = {{(32-REM_CODE_PH_I_WL){1'b0}}, rem_code_ph_reg_p};
end
(LOW_ADDRESS + MEASUREMENT_META_REG_OFFSET):
begin
PRDATA = {
8'h01,
11'b0,
measurement_cn0_sample_count_reg,
5'b0,
measurement_lock_reg,
measurement_bit_reg,
measurement_data_valid_reg
};
end
(LOW_ADDRESS + MEASUREMENT_SEQUENCE_REG_OFFSET):
begin
PRDATA = {16'b0, measurement_sequence_reg};
end
(LOW_ADDRESS + MEASUREMENT_CARRIER_REG_OFFSET):
begin
PRDATA = {{(32-MEAS_CARRIER_WL){
measurement_carrier_freq_q7_reg[MEAS_CARRIER_WL-1]}},
measurement_carrier_freq_q7_reg};
end
(LOW_ADDRESS + MEASUREMENT_ABS_SAMPLE_REG_OFFSET):
begin
PRDATA = measurement_abs_sample_reg;
end
(LOW_ADDRESS + MEASUREMENT_REM_CODE_REG_OFFSET):
begin
PRDATA = {{(32-REM_CODE_PH_I_WL){1'b0}},
measurement_rem_code_ph_reg};
end
(LOW_ADDRESS + MEASUREMENT_SUM_I_REG_OFFSET):
begin
PRDATA = {{(32-MEAS_SUM_WL){
measurement_cn0_sum_i_reg[MEAS_SUM_WL-1]}},
measurement_cn0_sum_i_reg};
end
(LOW_ADDRESS + MEASUREMENT_SUM_Q_REG_OFFSET):
begin
PRDATA = {{(32-MEAS_SUM_WL){
measurement_cn0_sum_q_reg[MEAS_SUM_WL-1]}},
measurement_cn0_sum_q_reg};
end
(LOW_ADDRESS + MEASUREMENT_WBP_LO_REG_OFFSET):
begin
PRDATA = measurement_cn0_wbp_reg[31:0];
end
(LOW_ADDRESS + MEASUREMENT_WBP_HI_REG_OFFSET):
begin
PRDATA = {{(64-MEAS_WBP_WL){1'b0}},
measurement_cn0_wbp_reg[MEAS_WBP_WL-1:32]};
end
endcase
end
end
+16 -3
View File
@@ -21,7 +21,17 @@ package trck_pkg;
NAV_DATA_9_REG_OFFSET = 60,
TRCK_TIMEOUT_REG_OFFSET = 64,
NAV_SOLVE_TIMER_REG_OFFSET = 68,
REM_CODE_PH_REG_OFFSET = 76
NAV_SOLVE_PHASE_REG_OFFSET = 72,
REM_CODE_PH_REG_OFFSET = 76,
MEASUREMENT_META_REG_OFFSET = 80,
MEASUREMENT_SEQUENCE_REG_OFFSET = 84,
MEASUREMENT_CARRIER_REG_OFFSET = 88,
MEASUREMENT_ABS_SAMPLE_REG_OFFSET = 92,
MEASUREMENT_REM_CODE_REG_OFFSET = 96,
MEASUREMENT_SUM_I_REG_OFFSET = 100,
MEASUREMENT_SUM_Q_REG_OFFSET = 104,
MEASUREMENT_WBP_LO_REG_OFFSET = 108,
MEASUREMENT_WBP_HI_REG_OFFSET = 112
}trck_wrapper_reg_offets_e;
@@ -39,7 +49,9 @@ package trck_pkg;
TRCK_STOP_ON_UNLOCK_FLAG_EN_POS = 9, // enable pos.
TRCK_NAV_SOLVE_ENABLE_POS = 10, // clear pos
TRCK_NAV_SOLVE_INTR_EN_POS = 11, // clear pos
TRCK_NAV_SOLVE_FLAG_EN_POS = 12 // clear pos
TRCK_NAV_SOLVE_FLAG_EN_POS = 12, // clear pos
TRCK_MEASUREMENT_INTR_EN_POS = 13,
TRCK_MEASUREMENT_FLAG_EN_POS = 14
}trck_control_reg_pos_e;
typedef enum
@@ -50,7 +62,8 @@ package trck_pkg;
TRCK_INTR_PEND_POS = 3,
TRCK_NAV_BITS_RCV_FLAG_POS = 4,
TRCK_UNLOCK_FLAG_POS = 5,
TRCK_NAV_SOLVE_FLAG_POS = 6
TRCK_NAV_SOLVE_FLAG_POS = 6,
TRCK_MEASUREMENT_FLAG_POS = 7
}trck_status_reg_pos_e;
localparam logic [7 : 0] PREAMBLE = 8'b10001011;
+11
View File
@@ -105,6 +105,17 @@ generate
.bit_o (trck_o[ch].bit_o),
.bit_vld_o (trck_o[ch].bit_vld),
.abs_sample_o (trck_o[ch].abs_sample),
.rem_code_ph_o (),
.measurement_vld_o (),
.measurement_bit_o (),
.measurement_abs_sample_o(),
.measurement_rem_code_ph_o(),
.measurement_carrier_freq_q7_o(),
.measurement_cn0_sum_i_o(),
.measurement_cn0_sum_q_o(),
.measurement_cn0_wbp_o(),
.measurement_cn0_sample_count_o(),
.measurement_sequence_o(),
.fifo_of_o (), //reserved
.lock_o (trck_o[ch].lock),
.busy_o (trck_o[ch].busy)
+1
View File
@@ -35,6 +35,7 @@ trck_src/cordic_atan_one_stage.sv
trck_src/cos2x.sv
trck_src/divider.sv
trck_src/lock_detector.sv
trck_src/tracking_measurement.sv
trck_src/trck_cntrl.sv
trck_src/trck_top.sv
trck_src/xil_fifo.sv
+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
+129 -15
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
@@ -24,6 +42,15 @@ Q2.30 LSB. Phase, integrator, and rate diagnostics
are signed Q32 counter-tick values. `MAX_SLEW_Q32` is the absolute rate limit
in Q32 ticks per 125 MHz clock; the reset value is approximately 20 ppm.
The PI controller is the hardware loop filter: `Kp` supplies proportional
phase correction and `Ki` is the retained frequency/integral state. There is
no additional phase IIR and no derivative term. An extra phase IIR was
modelled and rejected because it duplicated the small navigation pre-filter
and added acquisition/profile-transition delay. Software may change `Kp`,
`Ki`, maximum slew and lock policy while running; the core captures the whole
mailbox coherently at the next commit and preserves the integrator across a
normal profile transition.
Each rising PPS edge is held high for 100 ms (12,500,000 tracking-clock cycles
at 125 MHz), making the physical output easy to observe without changing the
disciplined edge timing. The width is derived from `CLK_FREQ_HZ` and the shared
@@ -38,15 +65,62 @@ The hardware does not add `POSTPONE_TOW`. Baremetal must commit an already
epoch-aligned GPS reference, so the verified 160 ms subframe readout correction
is owned exactly once by the navigation software.
Tracking C/N0 and Doppler are not inputs to the PPS RTL. The tracking wrapper
publishes those measurements through its separate 20 ms APB mailbox. Firmware
uses C/N0, geometry, WLS clock sigma, integrity and Doppler velocity residual
to select `acquire`, `precision`, `robust` or `holdover`, then writes the
corresponding PI coefficients, slew limit and lock thresholds through this
PPS mailbox. Doppler also predicts the WLS clock-bias datum between commits.
This keeps quality policy and the small navigation pre-filter in software
while the deterministic PI loop, fixed-point rate state and physical edge
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 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
of the position-WLS datum. Instead, it predicts that datum with:
```text
position_datum_drift =
-reference_geometric_range_rate
+ c * reference_satellite_clock_drift
```
The geometric term uses the Doppler-WLS receiver velocity plus the selected
reference satellite position and velocity. The PPS RTL sees only the
resulting timestamped reference and loop configuration.
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`, and that offset must be measured once on hardware.
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))
```
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 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
@@ -68,34 +142,74 @@ the SoC-level connection must use the exact PCLK event that updates
| `0x44` | `COMMIT` | WO | bit 0 commit, bit 1 coarse-align commit |
| `0x48..0x74` | diagnostics | RO | sequence, GPS time, phase, integrator, rate and pulse count |
| `0x78` | `DIAG_SNAPSHOT` | WO | bit 0 atomically snapshots all multiword diagnostics |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020100`) |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020200`) |
| `0x88` | `LOOP_CONFIG` | RW shadow | bits 15:8 acquire ticks, 23:16 release ticks, 31:24 consecutive acquire updates; bits 7:0 reserved |
Coarse alignment does not assert lock or drive the pin. Five consecutive PI
updates with at most eight tracking-clock ticks (64 ns at 125 MHz) phase error
acquire lock; an error above 32 ticks (256 ns) releases it. This hysteresis
allows realistic WLS epoch jitter without edge chatter. Holdover
retains an already acquired lock, while a newly committed bad reference drops
lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the physical PPS output
therefore distinguish an aligned timebase from a converged one.
Coarse alignment does not assert lock or drive the pin. The reset policy
requires five consecutive raw PI residuals within eight tracking-clock ticks
(64 ns at 125 MHz); a residual above 32 ticks (256 ns) releases lock. Runtime
profiles may widen the release threshold or require more good updates without
filtering the phase seen by the controller. Holdover retains the internal
frequency/lock state but deasserts `STATUS.VALID`; a newly committed bad
reference drops lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the
physical PPS output therefore distinguish a held/aligned timebase from a
currently qualified one.
Firmware uses a two-second no-reference watchdog to enter holdover but keeps
the Doppler-predicted bias and PI state for bounded same-datum gaps. On
recovery it fills all mailbox shadow words, releases holdover immediately
before the normal commit toggle, and therefore applies the first coherent
post-gap reference (v2.2 rejects normal PI commits that arrive while holdover
is still asserted). The physical PPS already runs from the held rate word and
does not phase-step; `STATUS.VALID` may precede the PI apply only by the
bounded synchronizer/commit-pipeline latency. A datum change or an observation
gap beyond the prediction horizon uses a coarse commit and remains invalid
until lock is reacquired.
Diagnostic words do not change until another `DIAG_SNAPSHOT` request completes;
software polls `STATUS.DIAG_BUSY` before reading the group. The disciplined
timebase continues in holdover and while the physical output is disabled;
`CONTROL.ENABLE` gates `pps_out` and validity, not timekeeping.
Run lint plus both unit-level Questa tests from this directory:
This also applies to the single-word `ACTIVE_SEQUENCE` and `PULSE_COUNT`
registers: they belong to the same frozen diagnostic group. A live debugger
must halt the CPU briefly, request a new snapshot, wait for status bit 10 to
clear, and only then read `0x48..0x74`. For example, with OpenOCD's GDB monitor:
```gdb
monitor halt
monitor mww 0x1002a078 1
monitor sleep 10
x/wx 0x1002a004
x/wx 0x1002a048
x/wx 0x1002a05c
x/wx 0x1002a060
x/wx 0x1002a074
monitor resume
```
Reading `ACTIVE_SEQUENCE` or `PULSE_COUNT` without this request can return the
previous snapshot, including reset zero, even while the core is accepting
commits and generating pulses.
Run lint plus all five Questa tests from this directory:
```
make -C tb check
```
`pps_wrapper_tb` uses the same ordered MMIO writes as `gps_system/src/gps_pps.c` and
injects CPU/APB latency. `pps_core_tb` independently stresses signed PI math,
injects CPU/APB latency; it separately verifies that a held normal commit is
not applied and that the firmware's shadow-first release sequence applies the
first recovery update. `pps_core_tb` independently stresses signed PI math,
slew/anti-windup saturation, stale/future epochs, phase saturation, sequence
wrap, holdover and output disable. `pps_convergence_tb` supplies an independent
GPS truth clock, a +15 ppm oscillator error followed by a +8 ppm temperature-like
step, deterministic measurement noise, varying 3--27 cycle software latency,
and 5 Hz navigation commits. It checks phase and rate convergence numerically.
`pps_adaptive_tb` changes `Kp`, `Ki`, maximum slew and lock policy while the
loop is live, checks integrator preservation, validates raw-residual lock
acquisition/release, and verifies that holdover suppresses validity.
`CLK_FREQ_HZ` is reduced only to shorten simulation; tick/Q-format behavior is
unchanged.
+23 -7
View File
@@ -16,6 +16,7 @@ module pps_core #(
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] kp_q46,
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] ki_q46,
input logic [pps_pkg::PPS_FRACTION_WIDTH-1:0] max_slew_q32,
input logic [31:0] loop_config,
output logic commit_ack_toggle,
output logic [pps_pkg::PPS_COUNTER_WIDTH-1:0] raw_counter,
output logic pps_out,
@@ -50,8 +51,7 @@ module pps_core #(
PPS_MILLISECONDS_PER_SECOND - 1) / PPS_MILLISECONDS_PER_SECOND);
localparam int unsigned PPS_PULSE_COUNTER_WIDTH =
(PPS_PULSE_WIDTH_TICKS <= 1) ? 1 : $clog2(PPS_PULSE_WIDTH_TICKS);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH =
(PPS_LOCK_ACQUIRE_UPDATES <= 1) ? 1 : $clog2(PPS_LOCK_ACQUIRE_UPDATES);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH = 8;
// A navigation commit is rare (normally 5 Hz), while the disciplined clock
// runs at 125 MHz. Keep the per-clock time accumulator short and process a
@@ -101,6 +101,9 @@ module pps_core #(
logic [PPS_SEQUENCE_WIDTH-1:0] epoch_sequence_work;
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_work, ki_q46_work;
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_work;
logic [7:0] lock_acquire_ticks_work;
logic [7:0] lock_release_ticks_work;
logic [7:0] lock_acquire_updates_work;
logic signed [PPS_INCREMENT_WIDTH-1:0] reference_increment_work;
// Registered arithmetic stages. No multiplier feeds another multiplier,
@@ -135,13 +138,15 @@ module pps_core #(
logic signed [PPS_PRODUCT_WIDTH-1:0] max_slew_wide;
logic signed [PPS_STATE_WIDTH-1:0] max_slew_signed;
logic [PPS_STATE_WIDTH-1:0] phase_abs_work;
logic [PPS_STATE_WIDTH-1:0] lock_acquire_phase_q32_work;
logic [PPS_STATE_WIDTH-1:0] lock_release_phase_q32_work;
assign raw_counter = raw_counter_p;
assign pps_out = pps_out_p;
assign aligned = aligned_p;
assign tracking = aligned_p && track_enable && !holdover;
assign locked = locked_p;
assign pps_valid = enable && aligned_p && locked_p;
assign pps_valid = enable && aligned_p && locked_p && !holdover;
assign stale_sequence = stale_sequence_p;
assign stale_age = stale_age_p;
assign rate_saturated = rate_saturated_p;
@@ -213,6 +218,10 @@ module pps_core #(
max_slew_signed = $signed({32'd0, max_slew_q32_work});
phase_abs_work = phase_work[PPS_STATE_WIDTH-1] ? $unsigned(-phase_work) :
$unsigned(phase_work);
lock_acquire_phase_q32_work =
{24'd0, lock_acquire_ticks_work, 32'd0};
lock_release_phase_q32_work =
{24'd0, lock_release_ticks_work, 32'd0};
end
always_ff @(posedge clk or negedge resetn) begin
@@ -250,6 +259,9 @@ module pps_core #(
kp_q46_work <= '0;
ki_q46_work <= '0;
max_slew_q32_work <= '0;
lock_acquire_ticks_work <= '0;
lock_release_ticks_work <= '0;
lock_acquire_updates_work <= '0;
reference_increment_work <= '0;
fraction_ticks_work <= '0;
commit_age_ticks_work <= '0;
@@ -310,6 +322,9 @@ module pps_core #(
kp_q46_work <= kp_q46;
ki_q46_work <= ki_q46;
max_slew_q32_work <= max_slew_q32;
lock_acquire_ticks_work <= loop_config[15:8];
lock_release_ticks_work <= loop_config[23:16];
lock_acquire_updates_work <= loop_config[31:24];
if (aligned_p)
reference_increment_work <= $signed({1'b0, NOMINAL_INCREMENT_Q32}) +
$signed(rate_word_q32_p);
@@ -445,15 +460,16 @@ module pps_core #(
// A wider release threshold keeps small measurement noise from
// chattering an already valid physical PPS output.
if (phase_overflow_work ||
(phase_abs_work > PPS_LOCK_RELEASE_PHASE_Q32)) begin
(phase_abs_work > lock_release_phase_q32_work)) begin
locked_p <= 1'b0;
lock_good_updates_p <= '0;
pps_out_p <= 1'b0;
pulse_ticks_remaining_p <= '0;
end else if (!locked_p) begin
if (phase_abs_work <= PPS_LOCK_ACQUIRE_PHASE_Q32) begin
if (lock_good_updates_p ==
PPS_LOCK_COUNTER_WIDTH'(PPS_LOCK_ACQUIRE_UPDATES - 1)) begin
if (phase_abs_work <= lock_acquire_phase_q32_work) begin
if ((lock_acquire_updates_work <= 8'd1) ||
(lock_good_updates_p >=
(lock_acquire_updates_work - 1'b1))) begin
locked_p <= 1'b1;
lock_good_updates_p <= '0;
end else begin
+9 -2
View File
@@ -28,6 +28,12 @@ package pps_pkg;
localparam logic [PPS_STATE_WIDTH-1:0] PPS_LOCK_RELEASE_PHASE_Q32 =
64'h0000_0020_0000_0000; // 32 tracking-clock ticks (256 ns at 125 MHz)
localparam logic [PPS_FRACTION_WIDTH-1:0] PPS_DEFAULT_MAX_SLEW_Q32 = 32'd85899;
localparam logic [31:0] PPS_DEFAULT_LOOP_CONFIG = {
8'(PPS_LOCK_ACQUIRE_UPDATES),
PPS_LOCK_RELEASE_PHASE_Q32[39:32],
PPS_LOCK_ACQUIRE_PHASE_Q32[39:32],
8'd0 // reserved
};
localparam logic [PPS_STATE_WIDTH-1:0] PPS_ONE_TICK_Q32 = 64'h0000_0001_0000_0000;
typedef enum logic [31:0] {
@@ -64,7 +70,8 @@ package pps_pkg;
PPS_DIAG_SNAPSHOT_OFFSET = 32'h0000_0078,
PPS_VERSION_REG_OFFSET = 32'h0000_007c,
PPS_KP_Q46_HI_OFFSET = 32'h0000_0080,
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084,
PPS_LOOP_CONFIG_OFFSET = 32'h0000_0088
} pps_wrapper_reg_offsets_e;
typedef enum int unsigned {
@@ -104,7 +111,7 @@ package pps_pkg;
PPS_APB_STATE_FINISH
} pps_apb_state_e;
localparam logic [31:0] PPS_VERSION = 32'h0002_0100;
localparam logic [31:0] PPS_VERSION = 32'h0002_0200;
function automatic logic [PPS_COUNTER_WIDTH-1:0] pps_gray_to_binary(
input logic [PPS_COUNTER_WIDTH-1:0] gray);
logic [PPS_COUNTER_WIDTH-1:0] binary;
+72 -23
View File
@@ -39,16 +39,25 @@ module pps_wrapper #(
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_p, kp_q46_n;
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46_p, ki_q46_n;
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_p, max_slew_q32_n;
logic [31:0] loop_config_p, loop_config_n;
logic commit_coarse_p, commit_coarse_n;
logic commit_req_toggle_p, commit_req_toggle_n;
logic clear_status_toggle_p, clear_status_toggle_n;
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;
@@ -65,7 +74,8 @@ module pps_wrapper #(
logic [PPS_SEQUENCE_WIDTH-1:0] active_sequence_pps, pulse_count_pps;
logic [PPS_TIME_WIDTH-1:0] active_seconds_pps;
logic [PPS_STATE_WIDTH-1:0] active_subsec_pps;
logic signed [PPS_STATE_WIDTH-1:0] phase_error_pps, integrator_pps, rate_word_pps;
logic signed [PPS_STATE_WIDTH-1:0] phase_error_pps;
logic signed [PPS_STATE_WIDTH-1:0] integrator_pps, rate_word_pps;
(* ASYNC_REG = "TRUE" *) logic [11:0] status_sync1_p, status_sync2_p;
logic [PPS_SEQUENCE_WIDTH-1:0] active_sequence_sync1_p, active_sequence_sync2_p;
@@ -83,24 +93,49 @@ module pps_wrapper #(
logic [PPS_SEQUENCE_WIDTH-1:0] diag_active_sequence_pps, diag_pulse_count_pps;
logic [PPS_TIME_WIDTH-1:0] diag_active_seconds_pps;
logic [PPS_STATE_WIDTH-1:0] diag_active_subsec_pps;
logic [PPS_STATE_WIDTH-1:0] diag_phase_error_pps, diag_integrator_pps, diag_rate_word_pps;
logic [PPS_STATE_WIDTH-1:0] diag_phase_error_pps;
logic [PPS_STATE_WIDTH-1:0] diag_integrator_pps, diag_rate_word_pps;
logic commit_busy;
logic diag_busy;
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;
@@ -147,6 +182,7 @@ module pps_wrapper #(
kp_q46_p <= '0;
ki_q46_p <= '0;
max_slew_q32_p <= PPS_DEFAULT_MAX_SLEW_Q32;
loop_config_p <= PPS_DEFAULT_LOOP_CONFIG;
commit_coarse_p <= 1'b0;
commit_req_toggle_p <= 1'b0;
clear_status_toggle_p <= 1'b0;
@@ -176,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;
@@ -190,6 +229,7 @@ module pps_wrapper #(
kp_q46_p <= kp_q46_n;
ki_q46_p <= ki_q46_n;
max_slew_q32_p <= max_slew_q32_n;
loop_config_p <= loop_config_n;
commit_coarse_p <= commit_coarse_n;
commit_req_toggle_p <= commit_req_toggle_n;
clear_status_toggle_p <= clear_status_toggle_n;
@@ -228,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
@@ -254,20 +304,13 @@ module pps_wrapper #(
kp_q46_n = kp_q46_p;
ki_q46_n = ki_q46_p;
max_slew_q32_n = max_slew_q32_p;
loop_config_n = loop_config_p;
commit_coarse_n = commit_coarse_p;
commit_req_toggle_n = commit_req_toggle_p;
clear_status_toggle_n = clear_status_toggle_p;
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;
@@ -318,6 +361,9 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET): begin
if (commit_busy) commit_overrun_n = 1'b1; else max_slew_q32_n = PWDATA;
end
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET): begin
if (commit_busy) commit_overrun_n = 1'b1; else loop_config_n = PWDATA;
end
(LOW_ADDRESS + PPS_COMMIT_REG_OFFSET): begin
if (PWDATA[PPS_COMMIT_VALID_POS]) begin
if (!commit_busy) begin
@@ -357,6 +403,7 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_KP_Q46_HI_OFFSET),
(LOW_ADDRESS + PPS_KI_Q46_HI_OFFSET),
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET),
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET),
(LOW_ADDRESS + PPS_COMMIT_REG_OFFSET),
(LOW_ADDRESS + PPS_DIAG_SNAPSHOT_OFFSET): begin end
default: PSLVERR = 1'b1;
@@ -378,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];
@@ -392,6 +439,7 @@ module pps_wrapper #(
(LOW_ADDRESS + PPS_KP_Q46_HI_OFFSET): PRDATA = {{16{kp_q46_p[47]}}, kp_q46_p[47:32]};
(LOW_ADDRESS + PPS_KI_Q46_HI_OFFSET): PRDATA = {{16{ki_q46_p[47]}}, ki_q46_p[47:32]};
(LOW_ADDRESS + PPS_MAX_SLEW_Q32_OFFSET): PRDATA = max_slew_q32_p;
(LOW_ADDRESS + PPS_LOOP_CONFIG_OFFSET): PRDATA = loop_config_p;
(LOW_ADDRESS + PPS_ACTIVE_SEQUENCE_OFFSET): PRDATA = active_sequence_sync2_p;
(LOW_ADDRESS + PPS_ACTIVE_SECONDS_LO_OFFSET): PRDATA = active_seconds_sync2_p[31:0];
(LOW_ADDRESS + PPS_ACTIVE_SECONDS_HI_OFFSET): PRDATA = active_seconds_sync2_p[63:32];
@@ -421,6 +469,7 @@ module pps_wrapper #(
.epoch_counter(epoch_counter_shadow_p), .gps_seconds(gps_seconds_shadow_p),
.gps_fraction_q32(gps_fraction_shadow_p), .epoch_sequence(epoch_sequence_shadow_p),
.kp_q46(kp_q46_p), .ki_q46(ki_q46_p), .max_slew_q32(max_slew_q32_p),
.loop_config(loop_config_p),
.commit_ack_toggle(commit_ack_pps), .raw_counter(raw_counter_pps),
.pps_out(pps_out), .aligned(aligned_pps),
.tracking(tracking_pps), .locked(locked_pps), .pps_valid(pps_valid),
+1
View File
@@ -14,6 +14,7 @@ test:
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_wrapper_tb $(QUESTA_VSIM) -c -l wrapper.log -wlf wrapper.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_core_tb $(QUESTA_VSIM) -c -l core.log -wlf core.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_convergence_tb $(QUESTA_VSIM) -c -l convergence.log -wlf convergence.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_adaptive_tb $(QUESTA_VSIM) -c -l adaptive.log -wlf adaptive.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_precision_tb $(QUESTA_VSIM) -c -l precision.log -wlf precision.wlf -do $(CURDIR)/run.do
clean:
+191
View File
@@ -0,0 +1,191 @@
`timescale 1ns/1ps
module pps_adaptive_tb;
import pps_pkg::*;
localparam int unsigned CLK_FREQ_HZ = 1000;
localparam logic [63:0] TICK_Q32 = 64'h0000_0001_0000_0000;
localparam logic [63:0] SECOND_Q32 = CLK_FREQ_HZ * TICK_Q32;
localparam logic [31:0] ACQUIRE_CONFIG = 32'h0540_0800;
localparam logic [31:0] PRECISION_CONFIG = 32'h0340_0800;
localparam logic signed [47:0] ACQUIRE_KP_Q46 = 48'sh0800_0000_0000;
localparam logic signed [47:0] ACQUIRE_KI_Q46 = 48'sh0400_0000_0000;
localparam logic signed [47:0] PRECISION_KP_Q46 = 48'sh0200_0000_0000;
localparam logic signed [47:0] PRECISION_KI_Q46 = 48'sh0100_0000_0000;
logic clk = 1'b0;
logic resetn = 1'b0;
logic enable = 1'b1;
logic track_enable = 1'b1;
logic holdover = 1'b0;
logic clear_status_toggle = 1'b0;
logic commit_toggle = 1'b0;
logic commit_coarse = 1'b0;
logic [63:0] epoch_counter = '0;
logic [63:0] gps_seconds = '0;
logic [31:0] gps_fraction_q32 = '0;
logic [31:0] epoch_sequence = '0;
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46 = ACQUIRE_KP_Q46;
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46 = ACQUIRE_KI_Q46;
logic [31:0] max_slew_q32 = 32'h7fff_ffff;
logic [31:0] loop_config = ACQUIRE_CONFIG;
logic commit_ack_toggle;
logic [63:0] raw_counter;
logic pps_out, aligned, tracking, locked, pps_valid;
logic stale_sequence, stale_age, rate_saturated, phase_saturated;
logic [31:0] active_sequence, pulse_count;
logic [63:0] active_seconds, active_subsec_q32;
logic signed [63:0] phase_error_q32;
logic signed [63:0] integrator_q32, rate_word_q32;
int checks = 0;
always #4 clk = ~clk;
pps_core #(.CLK_FREQ_HZ(CLK_FREQ_HZ)) dut (
.clk(clk), .resetn(resetn), .enable(enable), .track_enable(track_enable),
.holdover(holdover), .clear_status_toggle(clear_status_toggle),
.commit_toggle(commit_toggle), .commit_coarse(commit_coarse),
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(loop_config),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
.pps_valid(pps_valid), .stale_sequence(stale_sequence),
.stale_age(stale_age), .rate_saturated(rate_saturated),
.phase_saturated(phase_saturated), .active_sequence(active_sequence),
.active_seconds(active_seconds), .active_subsec_q32(active_subsec_q32),
.phase_error_q32(phase_error_q32), .integrator_q32(integrator_q32),
.rate_word_q32(rate_word_q32), .pulse_count(pulse_count)
);
task automatic check_adaptive(input logic condition, input string message);
begin
checks = checks + 1;
if (!condition)
$fatal(1, "ADAPTIVE CHECK %0d FAILED: %s", checks, message);
end
endtask
task automatic commit_anchor(
input logic [63:0] counter_value,
input logic [63:0] seconds_value,
input logic [31:0] fraction_value,
input logic [31:0] sequence_id,
input logic coarse);
logic expected_ack;
int timeout;
begin
@(negedge clk);
epoch_counter = counter_value;
gps_seconds = seconds_value;
gps_fraction_q32 = fraction_value;
epoch_sequence = sequence_id;
commit_coarse = coarse;
commit_toggle = ~commit_toggle;
expected_ack = commit_toggle;
timeout = 0;
while (commit_ack_toggle != expected_ack) begin
@(posedge clk);
timeout = timeout + 1;
if (timeout > 40) $fatal(1, "adaptive commit timeout");
end
@(negedge clk);
end
endtask
task automatic commit_current_offset(
input integer signed offset_ticks,
input logic [31:0] sequence_id);
logic [63:0] counter_value;
logic [63:0] seconds_value;
logic signed [64:0] subsec_value;
logic [31:0] fraction_value;
begin
@(negedge clk);
counter_value = raw_counter;
seconds_value = active_seconds;
subsec_value = $signed({1'b0, active_subsec_q32}) +
(offset_ticks * $signed({1'b0, TICK_Q32}));
if (subsec_value >= $signed({1'b0, SECOND_Q32})) begin
subsec_value = subsec_value - $signed({1'b0, SECOND_Q32});
seconds_value = seconds_value + 64'd1;
end else if (subsec_value < 0) begin
subsec_value = subsec_value + $signed({1'b0, SECOND_Q32});
seconds_value = seconds_value - 64'd1;
end
fraction_value = subsec_value / CLK_FREQ_HZ;
commit_anchor(
counter_value, seconds_value, fraction_value, sequence_id, 1'b0);
end
endtask
initial begin
logic [31:0] sequence_before_holdover;
logic signed [63:0] acquire_rate;
logic signed [63:0] acquire_integrator;
repeat (5) @(posedge clk);
resetn = 1'b1;
repeat (5) @(posedge clk);
commit_anchor(raw_counter, 64'd100, 32'd0, 32'd1, 1'b1);
check_adaptive(aligned && !locked && !pps_valid,
"coarse alignment starts invalid and unlocked");
// Lock detection uses the unfiltered PI phase residual. Five coherent
// updates inside the programmable eight-tick window are required.
for (int update = 0; update < 4; update = update + 1) begin
commit_current_offset(0, update + 2);
check_adaptive(!locked, "lock cannot assert before configured count");
end
commit_current_offset(0, 32'd6);
check_adaptive(locked && pps_valid,
"raw coherent residuals acquire lock");
// Exercise the acquisition gains, then replace Kp, Ki and lock policy
// while running. The core captures one coherent set per commit and keeps
// the integrator state across a profile transition.
commit_current_offset(1, 32'd7);
acquire_rate = rate_word_q32;
acquire_integrator = integrator_q32;
check_adaptive((acquire_rate > 0) && (acquire_integrator > 0),
"acquisition Kp and Ki both contribute");
kp_q46 = PRECISION_KP_Q46;
ki_q46 = PRECISION_KI_Q46;
loop_config = PRECISION_CONFIG;
commit_current_offset(1, 32'd8);
check_adaptive(locked && pps_valid,
"on-the-fly PI profile transition preserves lock");
check_adaptive(integrator_q32 > acquire_integrator,
"new Ki continues the preserved integrator");
check_adaptive(rate_word_q32 < acquire_rate,
"lower precision Kp/Ki reduce loop correction");
check_adaptive((dut.kp_q46_work == PRECISION_KP_Q46) &&
(dut.ki_q46_work == PRECISION_KI_Q46),
"commit atomically captures runtime Kp and Ki");
sequence_before_holdover = active_sequence;
holdover = 1'b1;
repeat (3) @(posedge clk);
check_adaptive(locked && !tracking && !pps_valid,
"holdover retains lock state but marks PPS invalid");
commit_current_offset(0, 32'd9);
check_adaptive(active_sequence == sequence_before_holdover,
"holdover rejects control updates");
holdover = 1'b0;
repeat (3) @(posedge clk);
check_adaptive(pps_valid && tracking,
"leaving holdover restores validity without a phase step");
commit_current_offset(200, 32'd10);
check_adaptive(!locked && !pps_valid,
"one raw residual beyond release threshold drops lock");
$display("PPS ADAPTIVE TB PASS: %0d checks phase=%0d rate=%0d",
checks, phase_error_q32, rate_word_q32);
$finish;
end
endmodule
@@ -75,6 +75,7 @@ module pps_convergence_tb;
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
.pps_valid(pps_valid),
+1
View File
@@ -40,6 +40,7 @@ module pps_core_tb;
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned),
.tracking(tracking), .locked(locked), .pps_valid(pps_valid),
+48 -11
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;
@@ -203,6 +212,9 @@ module pps_wrapper_tb;
read_ok(PPS_VERSION_REG_OFFSET, read_data);
check_true(read_data == PPS_VERSION, "version register");
read_ok(PPS_LOOP_CONFIG_OFFSET, read_data);
check_true(read_data == PPS_DEFAULT_LOOP_CONFIG,
"backward-compatible default loop configuration");
// Stop the destination clock to force a real mailbox-busy interval. A
// CPU write to any shadow field during that interval must be rejected;
@@ -235,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");
@@ -254,6 +265,10 @@ module pps_wrapper_tb;
write_ok(PPS_KI_Q46_LO_OFFSET, 32'h0000_0000);
write_ok(PPS_KI_Q46_HI_OFFSET, 32'h0000_0000);
write_ok(PPS_MAX_SLEW_Q32_OFFSET, 32'h1000_0000);
write_ok(PPS_LOOP_CONFIG_OFFSET, 32'h0540_0800);
read_ok(PPS_LOOP_CONFIG_OFFSET, read_data);
check_true(read_data == 32'h0540_0800,
"runtime loop configuration readback");
// GPS reference says the capture happened at second 100 + 0.2 s.
// Software and APB deliberately consume time before COMMIT. The core must
@@ -304,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;
@@ -339,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);
@@ -360,10 +373,34 @@ module pps_wrapper_tb;
pps_sw_snapshot_diagnostics();
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
check_true(active_sequence == 7, "holdover preserves the last accepted control epoch");
check_true(!pps_valid, "holdover deasserts validity while retaining the disciplined timebase");
apb_read(12'h088, read_data, read_error);
// Firmware writes every shadow field while held, then releases holdover
// immediately before the commit toggle. This makes the first post-gap
// normal update reach the PI instead of receiving a drop-only ACK.
@(negedge pps_clk);
lock_ref_counter = dut.i_pps_core.raw_counter;
lock_ref_seconds = dut.i_pps_core.active_seconds;
lock_ref_fraction = dut.i_pps_core.active_subsec_q32 / CLK_FREQ_HZ;
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, lock_ref_counter[31:0]);
write_ok(PPS_EPOCH_COUNTER_HI_OFFSET, lock_ref_counter[63:32]);
write_ok(PPS_GPS_SECONDS_LO_OFFSET, lock_ref_seconds[31:0]);
write_ok(PPS_GPS_SECONDS_HI_OFFSET, lock_ref_seconds[63:32]);
write_ok(PPS_GPS_FRACTION_Q32_OFFSET, lock_ref_fraction);
write_ok(PPS_EPOCH_SEQUENCE_OFFSET, 32'd9);
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_0003); // release after shadows
write_ok(PPS_COMMIT_REG_OFFSET, 32'h0000_0001);
wait_commit_idle();
pps_sw_snapshot_diagnostics();
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
check_true(active_sequence == 9,
"shadow-first holdover release applies first recovery update");
check_true(pps_valid,
"recovery update preserves lock and restores validity");
apb_read(12'h08c, read_data, read_error);
check_true(read_error, "unmapped APB register raises PSLVERR");
apb_write(12'h088, 32'h1, read_error);
apb_write(12'h08c, 32'h1, read_error);
check_true(read_error, "unmapped APB write raises PSLVERR");
write_ok(PPS_CAPTURE_SELECT_REG_OFFSET, 0);
+1
View File
@@ -9,6 +9,7 @@ vlog -sv \
[file join $tb_dir pps_wrapper_tb.sv] \
[file join $tb_dir pps_core_tb.sv] \
[file join $tb_dir pps_convergence_tb.sv] \
[file join $tb_dir pps_adaptive_tb.sv] \
[file join $tb_dir pps_precision_tb.sv]
vsim -c -voptargs=+acc work.$::env(PPS_TB_TOP)
run -all
+88 -1
View File
@@ -1,3 +1,4 @@
# GPS RTL
Take `Ankasys_Pulpissimo_SoC/pulpissimo/rtl/gps_system/hdl` folder from Ankasys_Pulpissimo_SoC svn repo, it has these subfolders:
@@ -16,6 +17,93 @@ trck_src:
acq_src:
acqusition top and instantiated sub ips hdl codes from Hilal & Ahmet's implementations.
## 20 ms Tracking Measurement Mailbox
Her tracking kanali, decode edilen her GPS L1 C/A navigation biti icin bir
atomik olcum uretir. Veri yolu:
```text
1 ms prompt I/Q
-> tracking_measurement (20 ornek akumulasyonu ve snapshot)
-> tracking clock domaininde 20 ms sabit payload
-> her alan icin iki FF senkronizer
-> iki tam PCLK settle cevrimi
-> tracking_wrapper APB register bankasi
-> measurement flag/interrupt
-> bare-metal software
```
`trck_src/tracking_measurement.sv` bir bit boyunca su degerleri toplar:
```text
sum_i = sum(I_prompt)
sum_q = sum(Q_prompt)
wbp = sum(I_prompt^2 + Q_prompt^2)
M = 20 adet 1 ms prompt ornegi
```
Prompt enerji karesi lock detector'daki mevcut multiplier sonucundan alinir;
ayni is icin ikinci bir multiplier/DSP yolu acilmaz. Bit epoch'unda total
carrier NCO frekansi Q7 formatinda, absolute sample, remaining-code phase,
navigation bit ve 16-bit sequence degeriyle birlikte snapshot edilir. Payload
bir sonraki bit olcumune kadar degismez.
Bu arayuz bir event FIFO'su degildir. Kaynak veri en az bir 20 ms bit boyunca
sabit kaldigi icin wrapper her multi-bit alani mevcut `double_ff_syn` yapisindan
gecirir, senkronize `valid` rising edge'inden sonra iki tam PCLK cevrimi bekler
ve butun bankayi tek seferde register eder. Measurement interrupt'i APB
snapshot'tan bir PCLK sonra uretilir; software interrupt'a girdiginde veri ve
status stabildir. Software yine de coherent read icin sequence degerini
payload'dan once ve sonra okuyup esitligini kontrol eder.
### APB registerlari
| Offset | Alan | Aciklama |
| ---: | --- | --- |
| `0x50` | `MEASUREMENT_META` | `[31:24]=version(1)`, `[12:8]=M`, bit 2 lock, bit 1 NAV bit, bit 0 valid |
| `0x54` | `MEASUREMENT_SEQUENCE` | 16-bit, her 20 ms olcumde artar |
| `0x58` | `MEASUREMENT_CARRIER_Q7` | Signed total carrier NCO frekansi, Hz icin `/128` |
| `0x5c` | `MEASUREMENT_ABS_SAMPLE` | Olcum epoch absolute sample |
| `0x60` | `MEASUREMENT_REM_CODE_PHASE` | 26-bit remaining-code residue |
| `0x64` | `MEASUREMENT_SUM_I` | Signed coherent prompt-I toplami |
| `0x68` | `MEASUREMENT_SUM_Q` | Signed coherent prompt-Q toplami |
| `0x6c` | `MEASUREMENT_WBP_LO` | Wideband power alt 32 bit |
| `0x70` | `MEASUREMENT_WBP_HI` | Wideband power ust bitleri |
`CONTROL_REG` bit 13 measurement interrupt enable, bit 14 measurement flag
enable'dir. `STATUS_REG` bit 7 measurement flag'dir. Genel interrupt clear
islemi bu flag'i de temizler.
### Neden Doppler IIR RTL'de degil?
IIR RTL'de de uygulanabilir; her kanal icin onceki Doppler state'i, signed
coefficient multiply, rounding/saturation ve reacquisition reset kurali
eklenmesi yeterlidir. Mevcut tasarimda bilerek software'de tutulur:
- APB bankasi ham ve tekrar analiz edilebilir carrier olcumunu korur.
- Filtre katsayisi hareket profiline gore bitstream almadan degistirilebilir.
- C/N0, satellite state ve WLS zaten software'de hesaplanir.
- Filtre veri/interrupt hizini azaltmaz; measurement yine 50 Hz'dir.
- Kart testinde kesintisiz kosuda measurement `missed`, `invalid` ve event
overflow sayaclari sifir kalmistir; CPU veya CDC baskisi RTL filtresi
gerektirmemistir.
RTL filtresi ancak bu ham register sozlesmesinin yaninda ikinci bir filtered
alan istenirse, olcum tamamen donanim icinde tuketilecekse veya software
latency/CPU butcesi kart testinde yetersiz kalirsa anlamli olur.
### RTL dogrulama
Questa testleri:
```bash
make -C hardware/external/gps/rtl/trck_src/tb test
```
Testler prompt enerjisini, 20-ornek akumulasyonunu, iki ard arda bit epoch'unu,
20 ms hold davranisini, double-FF + settle + atomik APB capture'i ve
interrupt/status-clear yolunu kontrol eder.
## SVN Revision
svn info
@@ -45,4 +133,3 @@ Last Changed Author: ankaaa
Last Changed Rev: 89
Last Changed Date: 2025-07-08 14:20:14 +0300 (Tue, 08 Jul 2025)
+136
View File
@@ -0,0 +1,136 @@
# GPS tracking measurement yolu
Bu dizindeki tracking loop yalniz code/carrier takibi yapmaz; her 20 ms GPS
L1 C/A navigation-bit epoch'unda software'in C/N0 ve Doppler kullanabilmesi
icin atomik bir measurement snapshot'i da uretir.
## 20 ms snapshot
`tracking_measurement.sv`, ilk NAV-bit kenarinda accumulator'lari arm eder ve
sonraki her bit epoch'unda su alanlari birlikte latch eder:
```text
nav bit
ABS_SAMPLE
remaining code phase
signed carrier frequency, Q7
20 x prompt I toplami
20 x prompt Q toplami
20 x (I_prompt^2 + Q_prompt^2) wideband power toplami
sample count
16-bit sequence
```
Prompt energy `lock_detector.sv` icinde pipeline'li oldugu icin bit kenarindan
sonra dort tracking clock beklenir; bu, son 1 ms enerji terimini eski epoch'a
dahil eder ve yeni epoch ile karistirmaz. Snapshot ve `measurement_valid`
sonraki bit epoch'una kadar, yaklasik 20 ms sabit kalir.
## APB aktarimi ve interrupt
`apb_wrappers/tracking_wrapper.sv`, valid ve payload'un her bitini
`double_ff_syn` yardimcisi ile PCLK domainine alir. Bu yardimcinin mevcut
implementasyonu `stage_1`, `stage_2` ve registered `Dout` olmak uzere uc
register kademesidir. Kaynak payload 20 ms sabit kaldigi icin FIFO veya toggle
handshake kullanilmaz. Wrapper synchronized valid kenarindan sonra iki ek PCLK
settle cycle bekler, tum alanlari APB-domain register bankina ayni anda kopyalar
ve measurement interrupt/flag uretir.
Software sequence'i okumadan once ve payload'dan sonra tekrar okuyarak atomik
snapshot dogrulamasi yapar. Sequence degisirse okuma tekrar edilir; sequence
atlarsa kayip measurement diagnostics sayaci artar.
Measurement register offsetleri:
| Offset | Alan |
|---:|---|
| `0x50` | version, valid, NAV bit, lock ve sample count metadata |
| `0x54` | 16-bit sequence |
| `0x58` | signed carrier frequency Q7 |
| `0x5c` | ABS_SAMPLE |
| `0x60` | remaining code phase |
| `0x64` | signed prompt sum I |
| `0x68` | signed prompt sum Q |
| `0x6c` | wideband power low |
| `0x70` | wideband power high |
## Software'e verilen anlam
Carrier register'i toplam NCO frekansidir. Software IF merkezini cikarir:
```text
doppler_hz = carrier_q7 / 128 - 4092000
range_rate_mps = -doppler_hz * c / 1575420000
```
Pozitif received-frequency Doppler azalan geometrik range anlamina geldigi
icin range-rate denkleminde eksi isareti vardir.
C/N0, 20 prompt icin narrowband/wideband power estimator'i ile hesaplanir:
```text
R = (sum_i^2 + sum_q^2) / WBP
C/N0_linear = 1000 * (R - 1) / (20 - R)
C/N0_dbhz = 10 * log10(C/N0_linear)
```
RTL IIR uygulamaz. Ham snapshot debug ve yeniden tune icin korunur. ISR state'i
ayni PRN'de C/N0 icin `alpha=0.2`, Doppler icin `alpha=0.05` software IIR
uygular; reacquisition veya PRN degisiminde eski kanal state'i kullanilmaz.
Filtrelenmis range-rate, satellite ECEF velocity ve satellite clock drift ile
C/N0-weighted 4-state velocity WLS'ye girer:
```text
state = [receiver_vx, receiver_vy, receiver_vz, receiver_clock_drift_mps]
observation = measured_range_rate
- satellite_LOS_rate
+ c * satellite_clock_drift
H = [-u_x, -u_y, -u_z, 1]
```
Buradaki dorduncu state, tum uydu Doppler observation'larina ortak giren
receiver clock/frequency drift'idir. Klasik mutlak-pseudorange modelinde
receiver zaman hatasi `delta_t_r(t)` icin:
```text
clock_bias_m = c * delta_t_r(t)
clock_drift_mps = c * d(delta_t_r(t))/dt
clock_bias_m(t+dt) ~= clock_bias_m(t) + clock_drift_mps * dt
```
`clock_drift_mps`, receiver local oscillator frequency error'inin range-rate
birimindeki ortak etkisidir. `0.1 ppm`, yaklasik `29.98 m/s`'ye karsilik
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.
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.
Iki WLS ayri observation ve normal equation kullanir; fakat Doppler WLS tam
bagimsiz degildir. Uydu LOS vektorlerini kurmak icin position WLS'den gelen
guncel receiver ECEF konumunu, ephemeristen gelen uydu ECEF konum/hiz ve clock
drift'ini kullanir. Satellite clock drift bilinen duzeltmedir; WLS'nin
dorduncu bilinmeyeni receiver clock drift'idir.
WLS en az dort gecerli uydu ister; ECEF velocity, 3x3 covariance, receiver
clock drift ve residual RMS uretir. Fusion adapter velocity/covariance'i ENU'ya
dondurur ve ayri velocity NIS kapisi ile 15-state ESKF'ye verir.
## Dogrulama
```sh
make -C hardware/external/gps/rtl/trck_src/tb test
make -C soc/software/gps_system/tests run
make -C soc/software/gps_system/tests sanitize
```
Questa testleri 20 prompt akumulasyonu, snapshot hold, synchronizer settle,
atomik APB copy ve interrupt'i; Linux C testleri Q7 Doppler/range-rate, C/N0
estimator, coherent read, satellite velocity/clock drift ve weighted velocity
WLS/covariance yolunu kontrol eder.
+4
View File
@@ -34,6 +34,8 @@ module lock_detector #(
input logic corr_vld_i,
input logic signed [IN_WL-1 : 0] i_p_i,
input logic signed [IN_WL-1 : 0] q_p_i,
output logic prompt_energy_vld_o,
output logic [(2 * IN_WL):0] prompt_energy_o,
output logic lock_o
);
localparam DIV_OUT_IL = DIV_OUT_WL - DIV_OUT_FL;
@@ -64,6 +66,8 @@ logic lock_decision_bad;
assign i_prompt_abs = i_p_i[IN_WL-1] ? (~i_p_i + {{(IN_WL-1){1'b0}}, 1'b1}) : i_p_i;
assign q_prompt_abs = q_p_i[IN_WL-1] ? (~q_p_i + {{(IN_WL-1){1'b0}}, 1'b1}) : q_p_i;
assign prompt_energy_sum = {1'b0, i_prompt_energy_r} + {1'b0, q_prompt_energy_r};
assign prompt_energy_vld_o = prompt_energy_vld;
assign prompt_energy_o = prompt_energy_sum;
cos2x #(
.IN_WL (IN_WL),
+26
View File
@@ -0,0 +1,26 @@
QUESTA_VLOG ?= vlog
QUESTA_VSIM ?= vsim
QUESTA_RUN_DIR ?= /tmp/gps_tracking_measurement_questa
RTL_DIR := $(abspath ../..)
.PHONY: test clean
test:
rm -rf $(QUESTA_RUN_DIR)
mkdir -p $(QUESTA_RUN_DIR)
cd $(QUESTA_RUN_DIR) && vlib work
cd $(RTL_DIR) && $(QUESTA_VLOG) -sv -work $(QUESTA_RUN_DIR)/work \
-f filelist.f \
$(CURDIR)/lock_detector_energy_tb.sv \
$(CURDIR)/tracking_measurement_tb.sv \
$(CURDIR)/tracking_wrapper_measurement_tb.sv
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c tracking_measurement_tb \
-do 'run -all; quit -f'
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c lock_detector_energy_tb \
-do 'run -all; quit -f'
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c tracking_wrapper_measurement_tb \
-do 'run -all; quit -f'
clean:
rm -rf $(QUESTA_RUN_DIR)
@@ -0,0 +1,54 @@
`timescale 1ns/1ps
import definitions::*;
module lock_detector_energy_tb;
logic clk = 1'b0;
logic rstn = 1'b0;
logic corr_valid = 1'b0;
logic signed [TRCK_CORR_WL-1:0] i_prompt = '0;
logic signed [TRCK_CORR_WL-1:0] q_prompt = '0;
logic energy_valid;
logic [(2*TRCK_CORR_WL):0] energy;
logic lock;
always #4 clk = ~clk;
lock_detector #(
.IN_WL(TRCK_CORR_WL),
.IN_FL(TRCK_CORR_FL),
.DIV_OUT_WL(LOCK_DET_DIV_O_WL),
.DIV_OUT_FL(LOCK_DET_DIV_O_FL),
.DIV_ITER(14)
) dut (
.rstn,
.clk,
.corr_vld_i(corr_valid),
.i_p_i(i_prompt),
.q_p_i(q_prompt),
.prompt_energy_vld_o(energy_valid),
.prompt_energy_o(energy),
.lock_o(lock)
);
initial begin
repeat(4) @(posedge clk);
rstn = 1'b1;
repeat(2) @(posedge clk);
i_prompt = -22'sd1234;
q_prompt = 22'sd567;
corr_valid = 1'b1;
@(posedge clk);
corr_valid = 1'b0;
wait(energy_valid);
#1;
if(energy !== ((1234 * 1234) + (567 * 567)))
$fatal(1, "prompt energy mismatch: got=%0d expected=%0d",
energy, ((1234 * 1234) + (567 * 567)));
$display("lock_detector_energy_tb PASS");
$finish;
end
endmodule
@@ -0,0 +1,185 @@
`timescale 1ns/1ps
import definitions::*;
module tracking_measurement_tb;
localparam int SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT);
localparam int WBP_WL = (2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
localparam int COUNT_WL = $clog2(MS_PER_BIT + 1);
localparam int CARRIER_WL = ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
logic clk = 1'b0;
logic rstn = 1'b0;
logic bit_valid = 1'b0;
logic nav_bit = 1'b0;
logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample = '0;
logic [REM_CODE_PH_I_WL-1:0] rem_code_phase = '0;
logic signed [CARRIER_WL-1:0] carrier_freq_q7 = '0;
logic corr_valid = 1'b0;
logic signed [TRCK_CORR_WL-1:0] i_prompt = '0;
logic signed [TRCK_CORR_WL-1:0] q_prompt = '0;
logic prompt_energy_valid = 1'b0;
logic [(2*TRCK_CORR_WL):0] prompt_energy = '0;
logic measurement_valid;
logic measurement_bit;
logic [ABS_SAMPLE_CNTR_WL-1:0] measurement_abs_sample;
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_phase;
logic signed [CARRIER_WL-1:0] measurement_carrier_freq_q7;
logic signed [SUM_WL-1:0] cn0_sum_i;
logic signed [SUM_WL-1:0] cn0_sum_q;
logic [WBP_WL-1:0] cn0_wbp;
logic [COUNT_WL-1:0] cn0_sample_count;
logic [15:0] sequence_value;
integer expected_sum_i;
integer expected_sum_q;
longint unsigned expected_wbp;
always #4 clk = ~clk;
tracking_measurement dut (
.clk,
.rstn,
.bit_valid_i(bit_valid),
.nav_bit_i(nav_bit),
.absolute_sample_i(absolute_sample),
.rem_code_phase_i(rem_code_phase),
.carrier_freq_q7_i(carrier_freq_q7),
.corr_valid_i(corr_valid),
.i_prompt_i(i_prompt),
.q_prompt_i(q_prompt),
.prompt_energy_valid_i(prompt_energy_valid),
.prompt_energy_i(prompt_energy),
.measurement_valid_o(measurement_valid),
.nav_bit_o(measurement_bit),
.absolute_sample_o(measurement_abs_sample),
.rem_code_phase_o(measurement_rem_code_phase),
.carrier_freq_q7_o(measurement_carrier_freq_q7),
.cn0_sum_i_o(cn0_sum_i),
.cn0_sum_q_o(cn0_sum_q),
.cn0_wbp_o(cn0_wbp),
.cn0_sample_count_o(cn0_sample_count),
.sequence_o(sequence_value)
);
task automatic clock_cycles(input int unsigned count);
repeat(count) @(posedge clk);
endtask
task automatic pulse_bit_epoch(input logic value);
nav_bit = value;
bit_valid = 1'b1;
clock_cycles(12);
bit_valid = 1'b0;
clock_cycles(3);
endtask
task automatic add_prompt(input integer i_value, input integer q_value);
longint unsigned energy;
energy = (i_value * i_value) + (q_value * q_value);
i_prompt = i_value;
q_prompt = q_value;
corr_valid = 1'b1;
@(posedge clk);
corr_valid = 1'b0;
@(posedge clk);
prompt_energy = energy;
prompt_energy_valid = 1'b1;
@(posedge clk);
prompt_energy_valid = 1'b0;
@(posedge clk);
expected_sum_i += i_value;
expected_sum_q += q_value;
expected_wbp += energy;
endtask
task automatic check_measurement(
input logic expected_bit,
input logic [31:0] expected_abs,
input logic [REM_CODE_PH_I_WL-1:0] expected_rem,
input logic signed [CARRIER_WL-1:0] expected_carrier,
input logic [15:0] expected_sequence);
if(!measurement_valid)
$fatal(1, "measurement valid did not remain high");
if(measurement_bit !== expected_bit)
$fatal(1, "NAV bit mismatch");
if(measurement_abs_sample !== expected_abs)
$fatal(1, "absolute sample mismatch");
if(measurement_rem_code_phase !== expected_rem)
$fatal(1, "remaining code phase mismatch");
if(measurement_carrier_freq_q7 !== expected_carrier)
$fatal(1, "carrier Q7 mismatch: got=%0d expected=%0d",
measurement_carrier_freq_q7, expected_carrier);
if($signed(cn0_sum_i) !== expected_sum_i)
$fatal(1, "sum I mismatch: got=%0d expected=%0d",
$signed(cn0_sum_i), expected_sum_i);
if($signed(cn0_sum_q) !== expected_sum_q)
$fatal(1, "sum Q mismatch: got=%0d expected=%0d",
$signed(cn0_sum_q), expected_sum_q);
if(cn0_wbp !== expected_wbp)
$fatal(1, "WBP mismatch: got=%0d expected=%0d", cn0_wbp, expected_wbp);
if(cn0_sample_count !== MS_PER_BIT)
$fatal(1, "sample count mismatch: got=%0d", cn0_sample_count);
if(sequence_value !== expected_sequence)
$fatal(1, "sequence mismatch: got=%0d expected=%0d",
sequence_value, expected_sequence);
endtask
initial begin
clock_cycles(4);
rstn = 1'b1;
clock_cycles(2);
/* First edge only establishes the 20 ms integration phase. */
pulse_bit_epoch(1'b0);
expected_sum_i = 0;
expected_sum_q = 0;
expected_wbp = 0;
for(integer i = 0; i < MS_PER_BIT; i++) begin
add_prompt(100 + i, -20 + i);
end
nav_bit = 1'b1;
absolute_sample = 32'h1234_5678;
rem_code_phase = REM_CODE_PH_I_WL'(321);
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ + 1250) * 128);
bit_valid = 1'b1;
clock_cycles(10);
check_measurement(
1'b1, 32'h1234_5678, REM_CODE_PH_I_WL'(321),
CARRIER_WL'((CENTER_FREQ + 1250) * 128), 16'd1);
/* Payload inputs may move; the registered measurement must not. */
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ - 3000) * 128);
absolute_sample = 32'hffff_0000;
clock_cycles(5);
check_measurement(
1'b1, 32'h1234_5678, REM_CODE_PH_I_WL'(321),
CARRIER_WL'((CENTER_FREQ + 1250) * 128), 16'd1);
bit_valid = 1'b0;
clock_cycles(3);
if(measurement_valid)
$fatal(1, "measurement valid failed to deassert");
expected_sum_i = 0;
expected_sum_q = 0;
expected_wbp = 0;
for(integer j = 0; j < MS_PER_BIT; j++) begin
add_prompt(-50, 25);
end
nav_bit = 1'b0;
absolute_sample = 32'h2345_6789;
rem_code_phase = REM_CODE_PH_I_WL'(777);
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ - 3000) * 128);
bit_valid = 1'b1;
clock_cycles(10);
check_measurement(
1'b0, 32'h2345_6789, REM_CODE_PH_I_WL'(777),
CARRIER_WL'((CENTER_FREQ - 3000) * 128), 16'd2);
$display("tracking_measurement_tb PASS");
$finish;
end
endmodule
@@ -0,0 +1,172 @@
`timescale 1ns/1ps
import definitions::*;
import trck_pkg::*;
module tracking_wrapper_measurement_tb;
localparam int CARRIER_WL = ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
localparam int SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT);
localparam int WBP_WL = (2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
localparam logic [29:0] EXPECTED_CARRIER =
CARRIER_WL'((CENTER_FREQ + 2500) * 128);
localparam logic [WBP_WL-1:0] EXPECTED_WBP =
WBP_WL'(50'h2_1234_89ab_cdef);
logic PCLK = 1'b0;
logic PRESETN = 1'b0;
logic [11:0] PADDR = '0;
logic PSEL = 1'b0;
logic PENABLE = 1'b0;
logic PWRITE = 1'b0;
logic [31:0] PWDATA = '0;
logic PREADY;
logic [31:0] PRDATA;
logic PSLVERR;
logic trck_resetn = 1'b0;
logic afe_resetn = 1'b0;
logic trck_clk = 1'b0;
logic afe_clk = 1'b0;
logic [3:0] afe_data = '0;
logic sample_align = 1'b0;
logic [31:0] sample_counter = '0;
logic gps_frame_ready;
logic pps_capture;
integer interrupt_count = 0;
always #5 PCLK = ~PCLK;
always #4 trck_clk = ~trck_clk;
always #8 afe_clk = ~afe_clk;
always @(posedge PCLK)
if(gps_frame_ready) interrupt_count++;
tracking_wrapper #(
.LOW_ADDRESS(0),
.HIGH_ADDRESS(4095),
.ADDR_BUS_WIDTH(12)
) dut (
.PCLK, .PRESETN, .PADDR, .PSEL, .PENABLE, .PWRITE, .PWDATA,
.PREADY, .PRDATA, .PSLVERR,
.trck_resetn, .afe_resetn, .trck_clk, .afe_clk, .afe_data,
.sample_align, .sample_counter, .gps_frame_ready, .pps_capture
);
task automatic pclk_cycles(input int unsigned count);
repeat(count) @(posedge PCLK);
endtask
task automatic apb_write(input logic [11:0] address,
input logic [31:0] data);
@(negedge PCLK);
PADDR = address;
PWDATA = data;
PWRITE = 1'b1;
PSEL = 1'b1;
PENABLE = 1'b0;
@(posedge PCLK);
#1;
@(negedge PCLK);
PENABLE = 1'b1;
@(posedge PCLK);
#1;
@(negedge PCLK);
PSEL = 1'b0;
PENABLE = 1'b0;
PWRITE = 1'b0;
pclk_cycles(2);
endtask
task automatic apb_read(input logic [11:0] address,
output logic [31:0] data);
@(negedge PCLK);
PADDR = address;
PWRITE = 1'b0;
PSEL = 1'b1;
PENABLE = 1'b0;
@(posedge PCLK);
#1;
data = PRDATA;
@(negedge PCLK);
PSEL = 1'b0;
pclk_cycles(2);
endtask
task automatic expect_read(input logic [11:0] address,
input logic [31:0] expected);
logic [31:0] value;
apb_read(address, value);
if(value !== expected)
$fatal(1, "APB read 0x%03x mismatch: got=0x%08x expected=0x%08x",
address, value, expected);
endtask
initial begin
force dut.measurement_valid = 1'b0;
force dut.measurement_bit = 1'b1;
force dut.measurement_abs_sample = 32'h89ab_cdef;
force dut.measurement_rem_code_ph = REM_CODE_PH_I_WL'(555);
force dut.measurement_carrier_freq_q7 = EXPECTED_CARRIER;
force dut.measurement_cn0_sum_i = SUM_WL'(-12345);
force dut.measurement_cn0_sum_q = SUM_WL'(6789);
force dut.measurement_cn0_wbp = EXPECTED_WBP;
force dut.measurement_cn0_sample_count = $clog2(MS_PER_BIT+1)'(20);
force dut.measurement_sequence = 16'h55aa;
force dut.lock = 1'b1;
force dut.busy = 1'b1;
force dut.bit_valid = 1'b0;
pclk_cycles(5);
PRESETN = 1'b1;
trck_resetn = 1'b1;
afe_resetn = 1'b1;
pclk_cycles(5);
apb_write(CONTROL_REG_OFFSET[11:0],
(1 << TRCK_INTRPT_ENABLE_POS) |
(1 << TRCK_MEASUREMENT_INTR_EN_POS) |
(1 << TRCK_MEASUREMENT_FLAG_EN_POS));
@(negedge trck_clk);
force dut.measurement_valid = 1'b1;
pclk_cycles(12);
if(interrupt_count !== 1)
$fatal(1, "expected one measurement interrupt, got %0d", interrupt_count);
expect_read(MEASUREMENT_META_REG_OFFSET[11:0],
{8'h01, 11'b0, 5'd20, 5'b0, 1'b1, 1'b1, 1'b1});
expect_read(MEASUREMENT_SEQUENCE_REG_OFFSET[11:0], 32'h0000_55aa);
expect_read(MEASUREMENT_CARRIER_REG_OFFSET[11:0],
{{2{EXPECTED_CARRIER[29]}}, EXPECTED_CARRIER});
expect_read(MEASUREMENT_ABS_SAMPLE_REG_OFFSET[11:0], 32'h89ab_cdef);
expect_read(MEASUREMENT_REM_CODE_REG_OFFSET[11:0], 32'd555);
expect_read(MEASUREMENT_SUM_I_REG_OFFSET[11:0], -32'sd12345);
expect_read(MEASUREMENT_SUM_Q_REG_OFFSET[11:0], 32'd6789);
expect_read(MEASUREMENT_WBP_LO_REG_OFFSET[11:0], EXPECTED_WBP[31:0]);
expect_read(MEASUREMENT_WBP_HI_REG_OFFSET[11:0],
{{(64-WBP_WL){1'b0}}, EXPECTED_WBP[WBP_WL-1:32]});
/* Source payload movement cannot tear the APB-domain snapshot. */
force dut.measurement_carrier_freq_q7 =
CARRIER_WL'((CENTER_FREQ - 4000) * 128);
force dut.measurement_cn0_sum_i = SUM_WL'(22222);
pclk_cycles(4);
expect_read(MEASUREMENT_CARRIER_REG_OFFSET[11:0],
{{2{EXPECTED_CARRIER[29]}}, EXPECTED_CARRIER});
expect_read(MEASUREMENT_SUM_I_REG_OFFSET[11:0], -32'sd12345);
expect_read(STATUS_REG_OFFSET[11:0],
(1 << TRCK_BUSY_POS) |
(1 << TRCK_LOCKED_POS) |
(1 << TRCK_INTR_PEND_POS) |
(1 << TRCK_MEASUREMENT_FLAG_POS));
apb_write(CONTROL_REG_OFFSET[11:0],
(1 << TRCK_INTRPT_ENABLE_POS) |
(1 << TRCK_INTRPT_CLEAR_POS) |
(1 << TRCK_MEASUREMENT_INTR_EN_POS) |
(1 << TRCK_MEASUREMENT_FLAG_EN_POS));
expect_read(STATUS_REG_OFFSET[11:0],
(1 << TRCK_BUSY_POS) | (1 << TRCK_LOCKED_POS));
$display("tracking_wrapper_measurement_tb PASS");
$finish;
end
endmodule
@@ -0,0 +1,142 @@
import definitions::*;
/*
* Builds one coherent tracking measurement for every decoded 20 ms NAV bit.
*
* The payload is registered in the tracking clock domain and remains unchanged
* until the next measurement. measurement_valid_o remains asserted for the
* rest of bit_valid_i so the APB clock can synchronize it without a CDC FIFO.
*/
module tracking_measurement #(
parameter int unsigned CORR_WL = TRCK_CORR_WL,
parameter int unsigned ENERGY_WL = (2 * TRCK_CORR_WL) + 1,
parameter int unsigned SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT),
parameter int unsigned WBP_WL = ENERGY_WL + $clog2(MS_PER_BIT),
parameter int unsigned COUNT_WL = $clog2(MS_PER_BIT + 1),
parameter int unsigned CARRIER_WL =
ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL,
parameter int unsigned SNAPSHOT_DELAY_CYCLES = 4
) (
input logic clk,
input logic rstn,
input logic bit_valid_i,
input logic nav_bit_i,
input logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample_i,
input logic [REM_CODE_PH_I_WL-1:0] rem_code_phase_i,
input logic signed [CARRIER_WL-1:0] carrier_freq_q7_i,
input logic corr_valid_i,
input logic signed [CORR_WL-1:0] i_prompt_i,
input logic signed [CORR_WL-1:0] q_prompt_i,
input logic prompt_energy_valid_i,
input logic [ENERGY_WL-1:0] prompt_energy_i,
output logic measurement_valid_o,
output logic nav_bit_o,
output logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample_o,
output logic [REM_CODE_PH_I_WL-1:0] rem_code_phase_o,
output logic signed [CARRIER_WL-1:0] carrier_freq_q7_o,
output logic signed [SUM_WL-1:0] cn0_sum_i_o,
output logic signed [SUM_WL-1:0] cn0_sum_q_o,
output logic [WBP_WL-1:0] cn0_wbp_o,
output logic [COUNT_WL-1:0] cn0_sample_count_o,
output logic [15:0] sequence_o
);
localparam int unsigned DELAY_WL =
(SNAPSHOT_DELAY_CYCLES < 2) ? 1 : $clog2(SNAPSHOT_DELAY_CYCLES + 1);
logic bit_valid_r;
logic measurement_armed;
logic snapshot_pending;
logic [DELAY_WL-1:0] snapshot_delay;
logic signed [SUM_WL-1:0] sum_i;
logic signed [SUM_WL-1:0] sum_q;
logic [WBP_WL-1:0] wbp;
logic [COUNT_WL-1:0] sample_count;
wire bit_epoch = bit_valid_i && !bit_valid_r;
always_ff @(posedge clk or negedge rstn) begin
if(!rstn) begin
bit_valid_r <= 1'b0;
measurement_armed <= 1'b0;
snapshot_pending <= 1'b0;
snapshot_delay <= '0;
sum_i <= '0;
sum_q <= '0;
wbp <= '0;
sample_count <= '0;
measurement_valid_o <= 1'b0;
nav_bit_o <= 1'b0;
absolute_sample_o <= '0;
rem_code_phase_o <= '0;
carrier_freq_q7_o <= '0;
cn0_sum_i_o <= '0;
cn0_sum_q_o <= '0;
cn0_wbp_o <= '0;
cn0_sample_count_o <= '0;
sequence_o <= '0;
end else begin
bit_valid_r <= bit_valid_i;
if(!bit_valid_i) begin
measurement_valid_o <= 1'b0;
end
if(measurement_armed && corr_valid_i) begin
sum_i <= sum_i + $signed(i_prompt_i);
sum_q <= sum_q + $signed(q_prompt_i);
sample_count <= sample_count + 1'b1;
end
if(measurement_armed && prompt_energy_valid_i) begin
wbp <= wbp + prompt_energy_i;
end
/*
* The first synchronized NAV-bit edge defines the integration phase.
* Samples gathered before that edge are intentionally discarded.
*/
if(bit_epoch) begin
if(!measurement_armed) begin
measurement_armed <= 1'b1;
sum_i <= '0;
sum_q <= '0;
wbp <= '0;
sample_count <= '0;
end else begin
snapshot_pending <= 1'b1;
snapshot_delay <= DELAY_WL'(SNAPSHOT_DELAY_CYCLES);
end
end
/*
* Prompt energy is pipelined in lock_detector. There is almost one
* millisecond before the next correlator result, so waiting a few
* tracking clocks includes the last energy term without mixing epochs.
*/
if(snapshot_pending) begin
if(snapshot_delay != 0) begin
snapshot_delay <= snapshot_delay - 1'b1;
end else begin
snapshot_pending <= 1'b0;
measurement_valid_o <= 1'b1;
nav_bit_o <= nav_bit_i;
absolute_sample_o <= absolute_sample_i;
rem_code_phase_o <= rem_code_phase_i;
carrier_freq_q7_o <= carrier_freq_q7_i;
cn0_sum_i_o <= sum_i;
cn0_sum_q_o <= sum_q;
cn0_wbp_o <= wbp;
cn0_sample_count_o <= sample_count;
sequence_o <= sequence_o + 1'b1;
sum_i <= '0;
sum_q <= '0;
wbp <= '0;
sample_count <= '0;
end
end
end
end
endmodule
+52 -1
View File
@@ -34,6 +34,27 @@ module trck_top
output logic bit_vld_o,
output logic [ABS_SAMPLE_CNTR_WL-1 : 0] abs_sample_o,
output logic [REM_CODE_PH_I_WL-1 : 0] rem_code_ph_o,
// Coherent 20 ms tracking measurement (held until the next NAV bit)
output logic measurement_vld_o,
output logic measurement_bit_o,
output logic [ABS_SAMPLE_CNTR_WL-1 : 0] measurement_abs_sample_o,
output logic [REM_CODE_PH_I_WL-1 : 0] measurement_rem_code_ph_o,
output logic signed [
ACQUIRED_FREQ_IL+TRCK_CARR_NCO_FREQ_FL-1 : 0
] measurement_carrier_freq_q7_o,
output logic signed [
TRCK_CORR_WL+$clog2(MS_PER_BIT)-1 : 0
] measurement_cn0_sum_i_o,
output logic signed [
TRCK_CORR_WL+$clog2(MS_PER_BIT)-1 : 0
] measurement_cn0_sum_q_o,
output logic [
(2*TRCK_CORR_WL)+1+$clog2(MS_PER_BIT)-1 : 0
] measurement_cn0_wbp_o,
output logic [$clog2(MS_PER_BIT+1)-1 : 0]
measurement_cn0_sample_count_o,
output logic [15:0] measurement_sequence_o,
// Status signals
output logic fifo_of_o,
@@ -77,6 +98,8 @@ logic ca_l, ca_p, ca_e;
logic corr_vld;
logic signed [TRCK_CORR_WL-TRCK_CORR_FL-1 : -TRCK_CORR_FL] i_e, i_p, i_l, q_e, q_p, q_l;
logic prompt_energy_vld;
logic [(2*TRCK_CORR_WL):0] prompt_energy;
logic signed [RF_DATA_WIDTH-1 : 0] rf_data;
@@ -223,7 +246,8 @@ Disc_PLL_i (
.y_i (q_p),
.vld_o (pll_error_vld),
.angle_o (pll_error)
.angle_o (pll_error),
.vec_len_o()
);
code_loop_discriminator #(
@@ -259,6 +283,8 @@ lock_detector_i (
.corr_vld_i (corr_vld),
.i_p_i (i_p),
.q_p_i (q_p),
.prompt_energy_vld_o(prompt_energy_vld),
.prompt_energy_o(prompt_energy),
.lock_o (lock_o)
);
@@ -276,6 +302,31 @@ bit_gen bit_gen_i (
.rem_code_ph_o (rem_code_ph_o)
);
tracking_measurement tracking_measurement_i (
.clk (trk_clk),
.rstn (rstn),
.bit_valid_i (bit_vld_o),
.nav_bit_i (bit_o),
.absolute_sample_i (abs_sample_o),
.rem_code_phase_i (rem_code_ph_o),
.carrier_freq_q7_i (carr_freq_sum_p),
.corr_valid_i (corr_vld),
.i_prompt_i (i_p),
.q_prompt_i (q_p),
.prompt_energy_valid_i (prompt_energy_vld),
.prompt_energy_i (prompt_energy),
.measurement_valid_o (measurement_vld_o),
.nav_bit_o (measurement_bit_o),
.absolute_sample_o (measurement_abs_sample_o),
.rem_code_phase_o (measurement_rem_code_ph_o),
.carrier_freq_q7_o (measurement_carrier_freq_q7_o),
.cn0_sum_i_o (measurement_cn0_sum_i_o),
.cn0_sum_q_o (measurement_cn0_sum_q_o),
.cn0_wbp_o (measurement_cn0_wbp_o),
.cn0_sample_count_o (measurement_cn0_sample_count_o),
.sequence_o (measurement_sequence_o)
);
//freq valid
always_ff @(posedge trk_clk or negedge rstn) begin
if (!rstn) begin
@@ -449,6 +449,7 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
val trackingClockNode = BundleBridgeSink[Clock]()
val afeClockNode = BundleBridgeSink[Clock]()
val rfDataNode = BundleBridgeSink[UInt]()
val clockReadyNode = BundleBridgeSink[Bool]()
val ppsOutNode = BundleBridgeSource(() => Bool())
lazy val module = new Impl
@@ -464,21 +465,38 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
// GDB ndmreset reaches this reset. Deassert it inside each GPS clock domain so
// APB, acquisition, tracking, and RF/AFE logic do not leave reset on an async edge.
val gpsDomainReset = reset.asBool || !clockReadyNode.bundle
// Keep the APB/fabric side alive when the external RF clock disappears so
// the CPU and its RAM ephemeris cache survive. Only the clock-dependent GPS
// DSP domains receive the lock-qualified local reset.
val apbReset = ResetCatchAndSync(clock, reset.asBool, 3, "gps_apb_reset")
val acqReset = ResetCatchAndSync(acqClockNode.bundle, reset.asBool, 3, "gps_acq_reset")
val trckReset = ResetCatchAndSync(trackingClockNode.bundle, reset.asBool, 3, "gps_trck_reset")
val afeReset = ResetCatchAndSync(afeClockNode.bundle, reset.asBool, 3, "gps_afe_reset")
val acqReset = ResetCatchAndSync(acqClockNode.bundle, gpsDomainReset, 3, "gps_acq_reset")
val trckReset = ResetCatchAndSync(trackingClockNode.bundle, gpsDomainReset, 3, "gps_trck_reset")
val afeReset = ResetCatchAndSync(afeClockNode.bundle, gpsDomainReset, 3, "gps_afe_reset")
// PPS uses the fabric-derived tracking clock but must keep counting during
// RF/MMCM loss. Only a real SoC reset may reset its monotonic timebase.
val ppsReset = ResetCatchAndSync(trackingClockNode.bundle, reset.asBool, 3, "gps_pps_reset")
val apbResetn = !apbReset
val acqResetn = !acqReset
val trckResetn = !trckReset
val afeResetn = !afeReset
val ppsResetn = !ppsReset
afe.io.afe_rstn := afeResetn
afe.io.trk_rstn := trckResetn
afe.io.afe_clk := afeClockNode.bundle
afe.io.trk_clk := trackingClockNode.bundle
afe.io.start_i := acq.io.afe_start
// A disappearing RF clock resets the AFE while the APB register bank is
// intentionally retained. Generate one local restart pulse when the RF
// domain returns; the normal software pulse remains valid at cold boot.
val afeAutoStart = withClockAndReset(afeClockNode.bundle, afeReset) {
val started = RegInit(false.B)
val pulse = !started
started := true.B
pulse
}
afe.io.start_i := acq.io.afe_start || afeAutoStart
afe.io.sample_counter_clear_i := acq.io.sample_counter_clear
acq.io.PCLK := clock
@@ -523,7 +541,7 @@ class APBGpsBlock(params: FpgaGPSParams, beatBytes: Int)(implicit p: Parameters)
pps.io.PWRITE := apb.pwrite
pps.io.PWDATA := apb.pwdata(31, 0)
pps.io.pps_clk := trackingClockNode.bundle
pps.io.pps_resetn := trckResetn
pps.io.pps_resetn := ppsResetn
pps.io.capture_i := VecInit(tracking.map(_.io.pps_capture)).asUInt
ppsOutNode.bundle := pps.io.pps_out
@@ -599,6 +617,7 @@ class VCU118RocketSystem(implicit p: Parameters) extends RocketSubsystem
val gpsTrackingClockSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Clock()))
val gpsRfClockSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Clock()))
val gpsRfDataSourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => UInt(4.W)))
val gpsClockReadySourceNode = p(VCU118GPSKey).map(_ => BundleBridgeSource(() => Bool()))
private val pmodIRQDevice = new SimpleDevice("pmod-sensor-irq", Seq("rocket,pmod-sensor-irq"))
val pmodIRQIntNode = IntSourceNode(Seq(IntSourcePortParameters(Seq(
IntSourceParameters(IntRange(0, 1), pmodIRQDevice.int)))))
@@ -680,6 +699,7 @@ class VCU118RocketSystem(implicit p: Parameters) extends RocketSubsystem
gpsTrackingClockSourceNode.foreach { source => dev.trackingClockNode := source }
gpsRfClockSourceNode.foreach { source => dev.afeClockNode := source }
gpsRfDataSourceNode.foreach { source => dev.rfDataNode := source }
gpsClockReadySourceNode.foreach { source => dev.clockReadyNode := source }
dev
}
val gpsPPSSinkNode = gps.map(_.ppsOutNode.makeSink())
@@ -714,6 +734,7 @@ class VCU118RocketSystemModuleImp[+L <: VCU118RocketSystem](_outer: L)
val gps_trck_clk = _outer.gpsTrackingClockSourceNode.map(_.makeIO("gps_trck_clk"))
val gps_rf_clk = _outer.gpsRfClockSourceNode.map(_.makeIO("gps_rf_clk"))
val gps_rf_data = _outer.gpsRfDataSourceNode.map(_.makeIO("gps_rf_data"))
val gps_clock_ready = _outer.gpsClockReadySourceNode.map(_.makeIO("gps_clock_ready"))
val gps_pps = _outer.gpsPPSSinkNode.map(_.makeIO("gps_pps"))
_outer.pmodIRQIntNode.out.foreach { case (out, _) =>
+4 -3
View File
@@ -18,9 +18,9 @@ JDK_SHA256="d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331"
MILL_URL="https://github.com/com-lihaoyi/mill/releases/download/$MILL_VERSION/$MILL_VERSION"
MILL_SHA256="ebe83649cd63acd33608f6d14452c074be2ed7d00cd2f69f88573d3f6909774f"
CIRCT_ARCHIVE="circt-full-shared-linux-x64.tar.gz"
CIRCT_ARCHIVE="firrtl-bin-linux-x64.tar.gz"
CIRCT_URL="https://github.com/llvm/circt/releases/download/firtool-$FIRTOOL_VERSION/$CIRCT_ARCHIVE"
CIRCT_SHA256="dde442dcd0ada92e6f7db26f4744667471f7be61d10f2876297efb435fc9a5ec"
CIRCT_SHA256="10bf9b135a65e5d93fb0555e967eac4258400ee629731ba68a69be6b7f9781ce"
Z3_ARCHIVE="z3-$Z3_VERSION-x64-glibc-2.39.zip"
Z3_URL="https://github.com/Z3Prover/z3/releases/download/z3-$Z3_VERSION/$Z3_ARCHIVE"
@@ -85,7 +85,8 @@ if [[ ! -x "$TOOLS_ROOT/mill/$MILL_VERSION/mill" ]]; then
"${SUDO[@]}" install -m 0755 "$work_dir/mill" "$TOOLS_ROOT/mill/$MILL_VERSION/mill"
fi
if [[ ! -x "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" ]]; then
if ! "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" --version \
>/dev/null 2>&1; then
download "$CIRCT_URL" "$work_dir/$CIRCT_ARCHIVE" "$CIRCT_SHA256"
mkdir -p "$work_dir/circt"
tar -xzf "$work_dir/$CIRCT_ARCHIVE" -C "$work_dir/circt"
+14 -4
View File
@@ -10,10 +10,15 @@ include $(REPO_DIR)/tools/project.mk
APP_FLASH_OFFSET ?= 0x06000000
APP ?= hello_uart
GPS_DEBUG ?= 1
GPS_DEBUG_LEVEL ?= 4
GPS_DEBUG_LEVEL ?= 1
GPS_ESKF_ENABLE ?= 1
IMU_SENSOR ?= auto
BSP_UART_CLOCK_HZ ?= 100000000
BSP_UART_BAUDRATE ?= 115200
BSP_UART_BAUDRATE ?= 460800
GPS_SYSTEM_UART_BAUDRATE ?= 460800
BSP_CLINT_TIMEBASE_HZ ?= 1000000
GPS_SYSTEM_CLINT_TIMEBASE_HZ ?= 500000
GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES ?= 5
# BSP_DTIM_* mirrors the FPGA DTIM_* knobs and is used by C code plus linker scripts.
BSP_DTIM_BASE ?= 0x80000000
BSP_DTIM_SIZE ?= 0x00100000
@@ -38,7 +43,12 @@ help:
@echo " APP_FLASH_OFFSET=$(APP_FLASH_OFFSET)"
@echo " BSP_UART_CLOCK_HZ=$(BSP_UART_CLOCK_HZ)"
@echo " BSP_UART_BAUDRATE=$(BSP_UART_BAUDRATE)"
@echo " GPS_SYSTEM_UART_BAUDRATE=$(GPS_SYSTEM_UART_BAUDRATE)"
@echo " GPS_ESKF_ENABLE=$(GPS_ESKF_ENABLE)"
@echo " IMU_SENSOR=$(IMU_SENSOR)"
@echo " BSP_CLINT_TIMEBASE_HZ=$(BSP_CLINT_TIMEBASE_HZ)"
@echo " GPS_SYSTEM_CLINT_TIMEBASE_HZ=$(GPS_SYSTEM_CLINT_TIMEBASE_HZ)"
@echo " GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES=$(GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES)"
@echo " BSP_DTIM_BASE=$(BSP_DTIM_BASE)"
@echo " BSP_DTIM_SIZE=$(BSP_DTIM_SIZE)"
@@ -57,11 +67,11 @@ steval-mki217v1-test:
@make -C steval_mki217v1_test TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/steval_mki217v1_test" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
gps-system:
@make -C gps_system TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/gps_system" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
@make -C gps_system TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/gps_system" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(GPS_SYSTEM_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(GPS_SYSTEM_CLINT_TIMEBASE_HZ)" IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES="$(GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
app:
@test -d "$(APP)"
@make -C "$(APP)" TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/$(APP)" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
@make -C "$(APP)" TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/$(APP)" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
flash-image: app
@mkdir -p "$(FLASH_DIR)"
+1 -1
View File
@@ -13,7 +13,7 @@ APP_FLASH_OFFSET ?= 0x06000000
MARCH ?= rv64imafdc_zicsr_zifencei
MABI ?= lp64d
BSP_UART_CLOCK_HZ ?= 100000000
BSP_UART_BAUDRATE ?= 115200
BSP_UART_BAUDRATE ?= 460800
BSP_DTIM_BASE ?= 0x80000000
BSP_DTIM_SIZE ?= 0x00100000
CONFIG_STAMP := $(BUILD_DIR)/build_config.txt
+1 -1
View File
@@ -7,7 +7,7 @@
#endif
#ifndef BSP_UART_BAUDRATE
#define BSP_UART_BAUDRATE 115200UL
#define BSP_UART_BAUDRATE 460800UL
#endif
void bsp_init(void)
+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
+11 -4
View File
@@ -13,7 +13,7 @@ LINKER_SCRIPT ?= $(COMMON_DIR)/linker_ram.ld
MARCH ?= rv64imafdc_zicsr_zifencei
MABI ?= lp64d
BSP_UART_CLOCK_HZ ?= 100000000
BSP_UART_BAUDRATE ?= 115200
BSP_UART_BAUDRATE ?= 460800
BSP_CLINT_TIMEBASE_HZ ?= 1000000
BSP_DTIM_BASE ?= 0x80000000
BSP_DTIM_SIZE ?= 0x00100000
@@ -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)
+160 -22
View File
@@ -6,6 +6,22 @@ Bu klasör Genesys2 kartından UART ile alınan GPS WLS konumlarını ve PMOD SP
ve jiroskop biası. Kartın durağan olduğu deneyde zero-velocity update (ZUPT)
uygulanır ve GPS innovation değerlerine aykırı-ölçüm kapısı konur.
Bare-metal ürün firmware'i ana dizinden iki profille üretilebilir:
```sh
# TDK + GPS + ESKF (varsayılan GPS_ESKF_ENABLE=1)
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-mcs
# GPS-only; IMU/ESKF kapalı, WLS/Doppler/PPS açık
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=0 fpga-mcs
```
Firmware ve UART kayıt aracının varsayılan hızı `460800` baud'dur. İki profil
aynı MCS/BIN çıktı yolunu kullandığı için ikinci build ilk dosyanın üzerine
yazar.
## Güncel durum ve devralma notu (2026-07-16)
Bu bölüm yeni bir geliştirme oturumunun nereden devam edeceğini tek yerde
@@ -66,11 +82,69 @@ Offline model:
UART GPS + IMU logları -> Python parser -> fixed/adaptive ESKF -> CSV/grafik/harita
Ürün yolu:
GPS WLS ---------> fusion_bus --\
-> fusion_engine -> 15-state ESKF -> fused LLH/ENU
GPS position + Doppler velocity --\
-> fusion_engine -> 15-state ESKF -> fused LLH/ENU
TDK veya ST IMU -> imu_sensor ----/
```
### Doppler ESKF'ye nasıl geliyor?
1. Tracking RTL her 20 ms NAV-bit epoch'unda carrier NCO Q7 değerini,
`ABS_SAMPLE`, remaining code phase ve 20 prompt'un I/Q/WBP toplamlarını
birlikte latch eder. Payload 20 ms sabit kalır; APB wrapper registered
synchronizer ve settle süresinden sonra atomik register bankı ile interrupt
üretir.
2. Firmware coherent mailbox okumasında sequence'i payload öncesi/sonrası
kontrol eder. Carrier değeri için
`doppler_hz = carrier_q7/128 - 4092000`, L1 pseudorange-rate için
`range_rate = -doppler_hz*c/1575420000` kullanılır.
3. Aynı PRN'in 50 Hz snapshot'larında C/N0 `alpha=0.2` ile yumuşatılır.
Doppler'in ham değeri ile `alpha=0.05` software-IIR değeri ayrı tutulur;
RTL hardware Doppler IIR içermez.
4. Ham Doppler, en az dört uyduyla C/N0-weighted velocity WLS
`[receiver_vx, receiver_vy, receiver_vz, receiver_clock_drift_mps]`
state'ini çözer. Her observation'dan satellite LOS velocity çıkarılır,
broadcast satellite clock drift eklenir. Inverse normal matrix ve post-fit
residual ECEF velocity covariance'ini üretir. En az altı kullanılabilir
uyduda `3.5 sigma` normalized-residual gate açılırsa bütün tek-kanal
leave-one-out adayları denenir ve en düşük weighted residual'lı aday ESKF
yolu için seçilir.
5. GPS adapter velocity ve 3x3 covariance'i ECEF'ten ENU'ya döndürür,
kart ölçümüne dayalı minimum `2/2/3 m/s` standard-deviation floor'larını
uygular ve position ile aynı capture timestamp'ini korur.
6. Fusion engine gecikmiş position ve velocity'yi aynı geçmiş epoch'unda
uygular. Velocity'nin ayrı 3-DOF `%99.9`, `NIS=16.266` kapısı vardır; bir
velocity reject aynı epoch'taki kabul edilmiş position update'ini bozmaz.
Sonra daha yeni IMU history current-time horizon'a kadar replay edilir.
Filtreli Doppler ikinci bir velocity WLS'e girer; bu sonuç fusion/ESKF'ye
yayınlanmaz. Yalnız PPS yazılım filtresinde position-WLS referans datumunun
clock-bias drift predictor'u ve PPS kalite metriği olarak kullanılır. PPS'in
ana ölçümü position WLS `clock_bias_m` değeridir. Doppler WLS'nin dördüncü
`receiver_clock_drift_mps` state'i PPS'e doğrudan verilmez; filtreli receiver
velocity ve referans uydu ephemeris hızıyla datum türevi ayrıca hesaplanır.
Klasik mutlak-pseudorange modelinde receiver clock bias
`c*delta_t_r`, Doppler clock drift ise bunun türevi
`c*d(delta_t_r)/dt`'dir. Doppler WLS carrier pseudorange-rate'ten
`[vx,vy,vz,receiver_clock_drift]` çözer. `0.1 ppm` receiver oscillator
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 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
broadcast ephemerise bağlıdır.
Bu zincirin RTL/register ayrıntısı
`hardware/external/gps/rtl/trck_src/README.md`, bare-metal WLS ve kart
doğrulaması `soc/software/gps_system/README.md` içindedir.
Python tarafındaki giriş noktaları:
- `serial_capture.py`: UART akışını zaman damgalı CSV olarak toplar;
@@ -87,12 +161,14 @@ backend'ini seçer, `imu_calibration` SI birimine ve ortak eksen takımına
dönüştürür, `fusion_engine` ise sensör markasını bilmeden motion detector ve
ESKF'yi çalıştırır. IMU ve GPS asenkrondur; ikisi de aynı monotonik CLINT/CPU
tick alanında ölçüm anında damgalanır. GPS WLS geç tamamlanırsa fixed-lag IMU
history replay ile update gerçek ölçüm epoch'unda uygulanır.
history replay ile position ve Doppler velocity update'leri atomik olarak
gerçek ölçüm epoch'unda uygulanır.
Doğrulama durumu:
- Linux/GCC sentetik hareket, stationary, ani ivmelenme/fren, dönüş, ZUPT,
GPS NIS rejection, gecikmiş GPS replay, covariance invariantları ve
position/velocity NIS rejection, gecikmiş aynı-epoch position+velocity
replay, ECEF->ENU velocity/covariance dönüşümü, covariance invariantları ve
ASan/UBSan testleri geçti. Ayrıntılı sonuçlar aşağıdadır.
- STEVAL-MKI217V1 üzerindeki LSM6DSOX'un SPI Mode-0 okuması, 60 saniyelik
stationary ham veri kaydı ve fiziksel INT1 yolu gerçek Genesys2 kartında
@@ -124,7 +200,10 @@ Sonraki ana adımlar:
reject, IRQ/sample-loss ve queue sayaçlarını kaydetmek.
2. Stationary -> ani hareket -> sabit hız -> fren -> tekrar stationary testini
yapmak; hareket sırasında ZUPT'nin kapalı kaldığını doğrulamak.
3. PPS için aşağıdaki lock teşhisini tamamlamak.
3. Yeni Doppler-aided firmware'i kartta çalıştırıp `pos_nis`, `vel_nis` ve
`velocity=accepted/rejected` sayaçlarıyla position-only koşuya karşı A/B
kayıt almak.
4. PPS için aşağıdaki lock teşhisini tamamlamak.
Prototip montaj notu: STEVAL jumper kablolarıyla serbest durduğu için başlangıç
sensor yönü sabit kabul edilmemelidir. Her açılışta en az iki saniye stationary
@@ -144,11 +223,12 @@ konum çözülmesi tek başına PPS üretildiğini kanıtlamaz.
İlk kabul edilen WLS/PPS commit'i yalnız coarse alignment yapar. Ardından faz
hatası en fazla sekiz adet 125 MHz tracking-clock tick'i, yani 64 ns olan beş
ardışık PI update gerekir. Lock kazanıldıktan sonra 256 ns üzerindeki yeni bir
faz hatası lock'u düşürür. WLS receiver-clock bias/pseudorange jitter'ı veya
epoch capture belirsizliği beş ardışık ölçümde 64 ns koşulunu bozarsa konum
gelmeye devam ederken PPS pini sürekli LOW kalabilir. Bu şu an en güçlü
olasılıktır; register ölçmeden eşik değiştirilmemelidir.
ardışık PI update gerekir. RTL reset release eşiği 256 ns'dir; v2.2 production
software profile'ları tek seyrek WLS sıçraması pulse kaybettirmesin diye
release eşiğini runtime `LOOP_CONFIG` ile 1.024 us yapar. WLS receiver-clock
bias/pseudorange jitter'ı veya epoch capture belirsizliği beş ardışık ölçümde
64 ns acquire koşulunu bozarsa konum gelmeye devam ederken PPS pini LOW
kalabilir.
Firmware ancak WLS başarılı, `solution_valid=true`, PPS referans satırı FDE ile
dışlanmamış ve referans kanalın capture sequence'i sıfır değilse PPS mailbox'a
@@ -158,19 +238,29 @@ ulaşmaz.
Genesys2 post-route checkpoint kontrolünde `gps_pps` portunun `JC10/AK28`,
LVCMOS33 olarak yerleştiği, sabit 0/1'e bağlanmadığı ve PPS core
`pps_out_p_reg/Q` tarafından sürüldüğü doğrulandı. Canlı ayrım için çalışan
kartta GDB ile şu register'lar okunmalıdır:
kartta CPU kısa süre durdurulmalı ve önce atomik diagnostic snapshot
istenmelidir. `ACTIVE_SEQUENCE`, faz/integrator/rate ve `PULSE_COUNT` aynı
donmuş snapshot grubundadır; `DIAG_SNAPSHOT` yazılmadan okunurlarsa core
çalışırken bile eski veya reset sıfırı görülebilir:
```gdb
monitor halt
x/wx 0x1002a07c
x/wx 0x1002a000
x/wx 0x1002a004
monitor mww 0x1002a078 1
monitor sleep 10
x/wx 0x1002a004
x/wx 0x1002a048
x/wx 0x1002a05c
x/wx 0x1002a060
x/wx 0x1002a074
monitor resume
```
| Register | Beklenen/yorum |
|---|---|
| `VERSION @ 0x1002a07c` | `0x00020100`; farklıysa ELF/bitstream eşleşmiyor |
| `VERSION @ 0x1002a07c` | Production v2.2 için `0x00020200`; farklıysa ELF/bitstream eşleşmesini kontrol edin |
| `CONTROL @ 0x1002a000` | Enable + track açıkken alt bitler genellikle `0x3` |
| `STATUS @ 0x1002a004` | `0x1`: commit/alignment yok; alt bitler `0x7`: aligned fakat lock değil; bit 11 set ve alt bitler `0xf`: locked/valid |
| `ACTIVE_SEQUENCE @ 0x1002a048` | `0`: firmware hiç geçerli PPS commit'i yapmamış; artıyorsa WLS referansları PPS'ye ulaşıyor |
@@ -182,6 +272,34 @@ lock acquisition/faz jitter'ıdır. `STATUS` lock gösteriyor ve `PULSE_COUNT`
artıyorsa JC10 sinyali JC11 GND referansıyla osiloskop veya logic analyzer'da
ölçülmelidir; pulse saniyede bir yaklaşık 100 ms HIGH'dır.
Erken 2026-07-25 v2.1 canlı Genesys2 ölçümünde `VERSION=0x00020100`,
`CONTROL=0x00000003` ve snapshot `ACTIVE_SEQUENCE=11583` görüldü.
`PULSE_COUNT=314`, PPS core'un resetten beri 314 `pps_out` pulse olayı
oluşturduğunu kanıtladı; post-route top-level yol da sabit LOW değildir.
Bu iki kanıt pinin elektriksel seviyesi için prob ölçümünün yerini tutmaz.
Ölçüm anında lock yoktu. Debugger'ın uzun halt etkisi temizlenip sticky bayraklar
sıfırlandıktan sonra 50 saniye boyunca status hatasız `0x00000007`
(enabled/aligned/tracking, unlocked) kaldı; pulse count artmadı. Üç ayrı
snapshot faz hatası yaklaşık `+1.95 us`, `+1.35 us` ve `-0.58 us` verdi.
Bunların tamamı beş ardışık update için gereken `+/-64 ns` acquire
penceresinden büyüktür.
Aynı koşudaki beş uydulu çözümlerde `GDOP` yaklaşık 17 ve `TDOP` yaklaşık
10.5'ti; ham GPS konum/irtifa da onlarca-yüzlerce metre oynuyordu. Bu zayıf
geometri ve WLS clock-bias gürültüsü, ölçülen mikrosaniye seviyesindeki PPS
faz oynaklığıyla uyumludur. Dolayısıyla mevcut kart bulgusu bir pin veya
APB/CDC kopukluğundan çok timing-observation kalite/lock-gating problemine
işaret eder. Eşik ölçümsüz büyütülmemeli; önce clock-bias/TDOP kalite kapısı,
outlier reddi veya faz gözlemi süzme stratejisi board A/B testiyle
değerlendirilmelidir.
Bu tarihsel v2.1 teşhisinden sonra v2.2 dinamik PI profilleri, Doppler
clock-drift prediction, bounded WLS clock-bias filtresi, 2 s validity
watchdog/30 s state koruma politikası ve fractional capture correction
eklenmiştir. Güncel sentetik ve kart sonuçları
`soc/software/gps_system/README.md` altındaki PPS bölümlerinde, register/RTL
davranışı `hardware/external/gps/rtl/pps/README.md` içinde tutulur.
## Çalıştırma
UART kaydı:
@@ -338,7 +456,8 @@ Bu nedenle sorumluluk iki katmana ayrilir:
olup olmadigini izler ve gerekli recovery/reset kararini burada verir.
Mevcut yalın recovery, son başarılı position fusion'dan sonra yedi saniyelik
timeout dolmuşsa ve en az üç rejected position fix'i kendi arasında zamansal
timeout dolmuşsa, platform harici rest bilgisi ve IMU/GNSS hareket vetolarıyla
duragansa ve en az üç rejected position fix'i kendi arasında zamansal
tutarlıysa yalnız position state ve position covariance'ini GNSS'e yeniden
ankrajlar. Position cross-covariance'lari gecersiz korelasyon tasimamasi icin
sifirlanir; velocity, attitude ve IMU bias state'leri korunur. Hard NIS gate
@@ -368,9 +487,11 @@ Karar yalniz rejected-candidate suresine degil son basarili position fusion
zamanina baglanmalidir. Uydu sayisi, finite covariance, DOP, WLS integrity ve
alternatif aiding kaynaklari birlikte degerlendirilmelidir. Tam dort uyduda
WLS residual redundancy/chi-square DOF bulunmadigi icin temporal consistency
daha onemlidir. Ileride Doppler velocity ayri innovation, fusion timeout ve
velocity-reset politikasi ile fuse edilmelidir. Yatay position ve height da
barometre gibi ek kaynaklar geldiginde birbirinden bagimsiz yonetilmelidir.
daha onemlidir. Doppler velocity artik ayri 3-DOF innovation/NIS kapisiyla
fuse edilir; bir velocity reject'i ayni epoch'taki position sonucunu bozmaz.
Henuz ayri bir velocity fusion-timeout/state-reset politikasi yoktur. Yatay
position ve height da barometre gibi ek kaynaklar geldiginde birbirinden
bagimsiz yonetilmelidir.
Baslangic GNSS'i icin mutlak bir dogruluk referansi yoksa tutarli fakat yanlis
bir GNSS kumesini otomatik olarak gercek konumdan ayirmak temel olarak mumkun
@@ -545,8 +666,10 @@ Hareket kararıyla ilgili iki koruma sentetik test sonucunda eklendi:
- Position-only GPS'in tek/iki gürültülü noktadan çıkardığı `still` kararı,
yüksek ESKF hızını ezip ZUPT başlatamıyor. GPS `moving` ek veto olarak
kullanılıyor; stationary hız kapısı 0.40 m/s, moving kapısı 0.50 m/s ile
histerezisli. İleride GPS Doppler velocity gelirse daha güçlü ve hızlı bir
duruş kanıtı olarak ayrıca eklenebilir.
histerezisli. Doppler velocity artık ESKF hız state'ini düzeltir, fakat kart
verisindeki birkaç m/s stationary saçılım nedeniyle doğrudan rest/ZUPT
kararı olarak kullanılmaz. İleride covariance/NIS'e bağlı çoklu-confirmation
stop evidence ayrı olarak eklenebilir.
Linux/GCC testlerini çalıştırma:
@@ -569,6 +692,13 @@ make profile
- sekiz ayrı deterministic noise/timing seed'i;
- 30 s boyunca 500 Hz stationary IMU ve gecikmiş 1 Hz GPS.
`gps_failure_scenario_test.c`, gerçek `gps_live_5min_20260713.csv` kaydındaki
3.297 Hz ölçüm hızı, 300 ms medyan periyot, 1.803 s maksimum boşluk ve yaklaşık
8.6 m yatay RMS saçılım ölçeğini kullanır. Normal akışın yanında hareket
sırasında 12 s tam kesinti, yüzde 35 rastgele kayıp ve 1.8 s boşluklar, gecikmiş
ölçümlerin toplu dönüşü ve kesinti sonrasında tutarlı fakat 80/-50/30 m hatalı
üç fix senaryoları regression testidir.
### Sentetik hareketli senaryo
Hareketli senaryo 200 Hz IMU ile 60 s sürer. Ground-truth profil şöyledir:
@@ -628,16 +758,21 @@ ham sample-to-sample noise'un jerk üzerinden sahte `MOVING` üretmediğini ve
- düz ve tilt edilmiş stationary gravity propagation;
- normal GPS NIS accept ve büyük outlier reject; reject sonrası state'in
bit seviyesine yakın değişmemesi;
- Doppler velocity NIS accept/reject; `2/2/3 m/s` konservatif covariance ile
5 Hz velocity aiding'in sentetik `0.05 m/s2` accel-bias hız sürüklenmesini
en az `4x` azaltması;
- ani hızlanma/fren, sabit hız sırasında stationary veto ve quiet dwell;
- stationary 500 Hz IMU noise altında false-motion olmaması;
- 100/200/500 Hz ve 3--17 ms düzensiz local-tick `dt` dizileri;
- 300 ms gecikmiş GPS replay sonucunun on-time filtre ile position, velocity,
- 300 ms gecikmiş aynı-epoch position+velocity replay sonucunun on-time filtre
ile position, velocity,
quaternion, bias, 15x15 covariance ve motion state'te `1e-11` toleransta
aynı olması;
- future GPS pending, checkpoint'ten eski GPS stale reject;
- GPS kesintisinde 2048-history rollover ve checkpoint'in zaman kaybetmeden
ilerlemesi;
- LLH--ECEF--ENU round-trip, covariance floor/PD kontrolü;
- LLH--ECEF--ENU round-trip, ECEF velocity/covariance rotasyonu ve covariance
floor/PD kontrolü;
- IMU/GPS SPSC queue FIFO sırası ve overflow sayaçları.
Uçtan uca acceptance limitleri test kodunun içinde sabittir: hareketli yatay
@@ -688,8 +823,11 @@ yatay RMSE'si 0.959 m, false-stationary sayısı sıfır oldu.
Bu sonuç GPS Doppler velocity'nin neden değerli olduğunu da gösterir. Position
farkından stationary/moving ayırımı düşük hızda ve kısa periodda zayıftır.
İleride GPS modülü güvenilir Doppler velocity ve covariance üretirse ayrı bir
velocity measurement update ve daha hızlı stop confirmation eklenmelidir.
Ürün yolu artık WLS'nin ECEF velocity covariance'ini ENU'ya döndürüp ayrı
velocity measurement update'inde kullanır. Default `2.0/2.0/3.0 m/s`
covariance floor ve 3-DOF `%99.9` NIS gate kartta ölçülen zayıf-geometri
saçılımına karşı konservatiftir. Daha hızlı stop confirmation, bu update'ten
ayrı ve çoklu-epoch istatistiksel kanıt gerektiren sonraki adımdır.
ASan ve UBSan hata vermedi. Sekiz adet 60 s hareketli koşu ile 30 s/500 Hz
stationary koşunun tamamı masaüstünde yaklaşık 0.70 s user CPU süresinde
+1 -1
View File
@@ -14,7 +14,7 @@ import serial
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--port", default="/dev/ttyUSB2")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--baud", type=int, default=460800)
parser.add_argument("--duration", type=float, default=60.0)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
+19 -6
View File
@@ -5,15 +5,16 @@ 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
.PHONY: all test gps-system-test scenario-logs sanitize profile benchmark clean
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
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)
@@ -39,7 +40,10 @@ gps_reacquisition_test: gps_reacquisition_test.c $(ESKF_SOURCES)
platform_scenario_matrix_test: platform_scenario_matrix_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
test: 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: gps_failure_scenario_test.c $(ESKF_SOURCES)
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
test: 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
./eskf_motion_test
./fusion_rest_detector_test
@@ -49,6 +53,7 @@ test: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test f
./async_rate_matrix_test
./gps_reacquisition_test
./platform_scenario_matrix_test
./gps_failure_scenario_test
gps-system-test: standalone_gps_integrity_test
./standalone_gps_integrity_test
@@ -68,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
@@ -122,6 +129,12 @@ sanitize:
-o platform_scenario_matrix_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./platform_scenario_matrix_test_san
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I../../gps_system/header gps_failure_scenario_test.c $(ESKF_SOURCES) \
-o gps_failure_scenario_test_san $(LDLIBS)
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./gps_failure_scenario_test_san
profile:
$(CC) -std=c11 -O2 -g -pg -Wall -Wextra -Wpedantic \
@@ -136,4 +149,4 @@ benchmark: standalone_gps_integrity_test
./standalone_gps_integrity_test --bench-double 100000
clean:
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_rest_detector_test fusion_rest_detector_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san async_tilt_recovery_test async_tilt_recovery_test_san async_rate_matrix_test async_rate_matrix_test_san gps_reacquisition_test gps_reacquisition_test_san platform_scenario_matrix_test platform_scenario_matrix_test_san gmon.out
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_rest_detector_test fusion_rest_detector_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san async_tilt_recovery_test async_tilt_recovery_test_san async_rate_matrix_test async_rate_matrix_test_san gps_reacquisition_test gps_reacquisition_test_san platform_scenario_matrix_test platform_scenario_matrix_test_san gps_failure_scenario_test gps_failure_scenario_test_san gmon.out
+189 -13
View File
@@ -119,6 +119,51 @@ static void test_zero_update_primitives(void)
}
}
static double quaternion_yaw(const double quaternion[4])
{
return atan2(
2.0 * (quaternion[0] * quaternion[3] +
quaternion[1] * quaternion[2]),
1.0 - 2.0 * (quaternion[2] * quaternion[2] +
quaternion[3] * quaternion[3]));
}
static void test_external_rest_cannot_erase_constant_turn(void)
{
fusion_engine_config_t config;
fusion_engine_t engine;
fusion_engine_default_config(&config, 1000000u);
CHECK(fusion_engine_init(&engine, &config),
"constant-turn engine initialization");
for(unsigned int sample = 0u ; sample <= 8u * 52u ; sample++)
{
uint64_t ticks = ((uint64_t)sample * 1000000u + 26u) / 52u;
double time_s = (double)ticks / 1000000.0;
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
imu.accel_mps2[2] = config.eskf.gravity_mps2;
imu.gyro_rps[2] =
(time_s >= 3.0) ? 20.0 * TEST_PI / 180.0 : 0.0;
imu.temperature_c = 25.0;
/* Models a stale/incorrect external landed flag. Inertial motion must
* still veto ZUPT and zero-gyro fusion. */
imu.vehicle_at_rest = true;
CHECK(fusion_engine_push_imu(&engine, &imu),
"constant-turn IMU accepted");
}
CHECK(engine.state == FUSION_ENGINE_RUNNING,
"constant-turn engine remains running");
double yaw_deg = quaternion_yaw(
engine.current.filter.body_to_nav_quaternion) * 180.0 / TEST_PI;
CHECK(fabs(yaw_deg - 100.0) < 1.0,
"stale external rest flag cannot erase a real constant turn");
CHECK(fabs(engine.current.filter.gyro_bias_rps[2]) <
0.5 * TEST_PI / 180.0,
"constant angular rate is not learned as gyro bias");
}
static void test_gravity_alignment(void)
{
eskf_t filter;
@@ -186,6 +231,81 @@ static void test_position_nis_gate(void)
"GPS accept/reject counters");
}
static void test_velocity_nis_gate(void)
{
eskf_t filter;
eskf_config_t config;
identity_filter(&filter, &config);
double covariance[9] = {
1.0, 0.1, 0.0,
0.1, 1.5, 0.0,
0.0, 0.0, 2.0
};
double normal_velocity[3] = {1.0, -0.5, 0.2};
double nis = NAN;
CHECK(eskf_velocity_update(&filter, &config, normal_velocity,
covariance, &nis) == ESKF_UPDATE_ACCEPTED,
"normal Doppler velocity accepted");
CHECK(isfinite(nis) && (nis < config.velocity_nis_gate),
"accepted Doppler velocity has finite in-gate NIS");
CHECK((filter.velocity_mps[0] > 0.0) &&
(filter.velocity_mps[1] < 0.0) &&
(filter.velocity_mps[2] > 0.0),
"Doppler update corrects all ESKF velocity axes");
double before[3];
memcpy(before, filter.velocity_mps, sizeof(before));
double outlier[3] = {100.0, -80.0, 50.0};
CHECK(eskf_velocity_update(&filter, &config, outlier,
covariance, &nis) == ESKF_UPDATE_REJECTED_NIS,
"large Doppler velocity outlier rejected by NIS");
CHECK(nearly_equal(before[0], filter.velocity_mps[0], 1.0e-12) &&
nearly_equal(before[1], filter.velocity_mps[1], 1.0e-12) &&
nearly_equal(before[2], filter.velocity_mps[2], 1.0e-12),
"velocity NIS rejection leaves nominal state unchanged");
CHECK(filter.velocity_update_accepted_count == 1u &&
filter.velocity_update_rejected_count == 1u,
"Doppler velocity accept/reject counters");
}
static double run_accel_bias_with_optional_doppler(bool fuse_doppler)
{
eskf_t filter;
eskf_config_t config;
identity_filter(&filter, &config);
const double velocity_covariance[9] = {
4.0, 0.0, 0.0,
0.0, 4.0, 0.0,
0.0, 0.0, 9.0
};
const double stationary_velocity[3] = {0.0, 0.0, 0.0};
for(unsigned int sample = 1u; sample <= 1000u; sample++)
{
/* Unmodelled 0.05 m/s2 x-axis accelerometer bias. */
double accel[3] = {0.05, 0.0, config.gravity_mps2};
double gyro[3] = {0.0, 0.0, 0.0};
CHECK(eskf_propagate(&filter, &config, accel, gyro, 0.01),
"accelerometer-bias profile propagation");
if(fuse_doppler && ((sample % 20u) == 0u))
{
CHECK(eskf_velocity_update(
&filter, &config, stationary_velocity,
velocity_covariance, NULL) == ESKF_UPDATE_ACCEPTED,
"5 Hz conservative Doppler velocity update");
}
}
return eskf_speed_mps(&filter);
}
static void test_doppler_velocity_limits_inertial_drift(void)
{
double unaided_speed = run_accel_bias_with_optional_doppler(false);
double doppler_aided_speed = run_accel_bias_with_optional_doppler(true);
CHECK(unaided_speed > 0.45,
"unmodelled accelerometer bias creates expected inertial speed drift");
CHECK(doppler_aided_speed < 0.25 * unaided_speed,
"conservative Doppler velocity materially limits inertial speed drift");
}
static double profile_acceleration(double time_s)
{
if((time_s >= 2.0) && (time_s < 4.0))
@@ -534,6 +654,21 @@ static fusion_position_measurement_t replay_position(uint64_t tick, double east_
return position;
}
static fusion_navigation_measurement_t replay_navigation(
uint64_t tick,
double east_m,
double east_velocity_mps)
{
fusion_navigation_measurement_t navigation = {0};
navigation.position = replay_position(tick, east_m);
navigation.velocity_enu_mps[0] = east_velocity_mps;
navigation.velocity_covariance_enu_mps2[0] = 0.25;
navigation.velocity_covariance_enu_mps2[4] = 0.25;
navigation.velocity_covariance_enu_mps2[8] = 1.0;
navigation.velocity_valid = true;
return navigation;
}
static void check_runtime_close(const fusion_runtime_t* first,
const fusion_runtime_t* second,
double tolerance)
@@ -576,28 +711,35 @@ static void test_delayed_position_replay(void)
static fusion_engine_t delayed;
initialize_engine_for_replay(&on_time);
initialize_engine_for_replay(&delayed);
fusion_position_measurement_t gps_at_1500 = replay_position(1500u, 0.125);
fusion_navigation_measurement_t gps_at_1500 =
replay_navigation(1500u, 0.125, 0.5);
for(uint64_t tick = 110u ; tick <= 3200u ; tick += 10u)
{
push_replay_imu(&on_time, tick);
push_replay_imu(&delayed, tick);
if(tick == 1500u)
{
eskf_update_result_t result = ESKF_UPDATE_INVALID;
CHECK(fusion_engine_push_position(&on_time, &gps_at_1500,
&result, NULL),
"on-time position update processed");
CHECK(result == ESKF_UPDATE_ACCEPTED,
"on-time position update accepted");
eskf_update_result_t position_result = ESKF_UPDATE_INVALID;
eskf_update_result_t velocity_result = ESKF_UPDATE_INVALID;
CHECK(fusion_engine_push_navigation(
&on_time, &gps_at_1500,
&position_result, NULL, &velocity_result, NULL),
"on-time position/velocity update processed");
CHECK((position_result == ESKF_UPDATE_ACCEPTED) &&
(velocity_result == ESKF_UPDATE_ACCEPTED),
"on-time position and Doppler velocity accepted");
}
if(tick == 1800u)
{
eskf_update_result_t result = ESKF_UPDATE_INVALID;
CHECK(fusion_engine_push_position(&delayed, &gps_at_1500,
&result, NULL),
"300 ms delayed position update replayed");
CHECK(result == ESKF_UPDATE_ACCEPTED,
"delayed position update accepted at measurement epoch");
eskf_update_result_t position_result = ESKF_UPDATE_INVALID;
eskf_update_result_t velocity_result = ESKF_UPDATE_INVALID;
CHECK(fusion_engine_push_navigation(
&delayed, &gps_at_1500,
&position_result, NULL, &velocity_result, NULL),
"300 ms delayed position/velocity update replayed");
CHECK((position_result == ESKF_UPDATE_ACCEPTED) &&
(velocity_result == ESKF_UPDATE_ACCEPTED),
"delayed position and velocity accepted at measurement epoch");
check_runtime_close(&on_time.current, &delayed.current, 1.0e-11);
}
}
@@ -606,6 +748,8 @@ static void test_delayed_position_replay(void)
"one delayed position caused one fixed-lag replay");
CHECK(delayed.diagnostics.replayed_imu_count > 0u,
"delayed position replayed stored IMU samples");
CHECK(delayed.diagnostics.velocity_accepted == 1u,
"delayed Doppler velocity is diagnosed as fused");
fusion_position_measurement_t future = replay_position(3500u, 1.0);
CHECK(fusion_engine_push_position(&delayed, &future, NULL, NULL),
@@ -682,6 +826,35 @@ static void test_gps_adapter(void)
invalid.covariance_enu_m2[0][0] = -1.0;
CHECK(!fusion_gps_adapter_convert(&adapter, &invalid, &local),
"invalid GPS covariance is rejected before ESKF");
fusion_gps_adapter_t velocity_adapter;
CHECK(fusion_gps_adapter_init(&velocity_adapter, &config),
"velocity GPS adapter initialization");
fusion_gps_measurement_t velocity_gps = adapter_gps(0.0, 0.0, 0.0);
velocity_gps.velocity_valid = true;
velocity_gps.velocity_ecef_mps[0] = 3.0;
velocity_gps.velocity_ecef_mps[1] = 1.0;
velocity_gps.velocity_ecef_mps[2] = -2.0;
velocity_gps.velocity_covariance_ecef_mps2[0][0] = 16.0;
velocity_gps.velocity_covariance_ecef_mps2[1][1] = 25.0;
velocity_gps.velocity_covariance_ecef_mps2[2][2] = 36.0;
fusion_navigation_measurement_t navigation;
CHECK(fusion_gps_adapter_convert_navigation(
&velocity_adapter, &velocity_gps, &navigation),
"GPS adapter converts position and Doppler velocity");
CHECK(navigation.velocity_valid,
"finite Doppler velocity covariance remains valid");
CHECK(nearly_equal(navigation.velocity_enu_mps[0], 1.0, 1.0e-12) &&
nearly_equal(navigation.velocity_enu_mps[1], -2.0, 1.0e-12) &&
nearly_equal(navigation.velocity_enu_mps[2], 3.0, 1.0e-12),
"ECEF Doppler velocity rotates to expected ENU axes");
CHECK(nearly_equal(
navigation.velocity_covariance_enu_mps2[0], 25.0, 1.0e-12) &&
nearly_equal(
navigation.velocity_covariance_enu_mps2[4], 36.0, 1.0e-12) &&
nearly_equal(
navigation.velocity_covariance_enu_mps2[8], 16.0, 1.0e-12),
"ECEF Doppler covariance rotates to ENU");
}
static void test_measurement_bus(void)
@@ -747,8 +920,11 @@ int main(void)
{
test_stationary_propagation();
test_zero_update_primitives();
test_external_rest_cannot_erase_constant_turn();
test_gravity_alignment();
test_position_nis_gate();
test_velocity_nis_gate();
test_doppler_velocity_limits_inertial_drift();
test_motion_transitions();
test_variable_rates();
test_irregular_imu_timestamps();
@@ -0,0 +1,436 @@
#include "fusion/fusion_engine.h"
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define TIMEBASE_HZ 1000000u
#define IMU_RATE_HZ 52u
#define GPS_PERIOD_TICKS 300000u
#define MAX_GPS_EVENTS 256u
#define PI 3.14159265358979323846
/*
* Timing/noise scale comes from logs/gps_live_5min_20260713.csv:
* 988 fixes in 299.697 s (3.297 Hz), 300 ms median interval, 1.803 s maximum
* interval, and about 8.6 m stationary horizontal RMS scatter. The 6 m
* per-axis covariance below reproduces that horizontal RMS scale.
*/
typedef enum {
SCENARIO_NORMAL = 0,
SCENARIO_MOVING_OUTAGE,
SCENARIO_INTERMITTENT,
SCENARIO_BURST_RETURN,
SCENARIO_BAD_RESTART
} scenario_kind_t;
typedef struct {
fusion_position_measurement_t measurement;
uint64_t delivery_ticks;
} gps_event_t;
typedef struct {
const char* name;
double horizontal_rmse_m;
double maximum_outage_error_m;
double error_at_outage_end_m;
double final_horizontal_error_m;
double maximum_gps_state_jump_m;
double speed_at_outage_start_mps;
double speed_at_outage_end_mps;
double horizontal_sigma_at_outage_end_m;
uint64_t gps_accepted;
uint64_t gps_rejected;
uint64_t gps_recoveries;
uint64_t stale_rejected;
uint64_t replay_count;
unsigned int generated;
unsigned int dropped;
bool finite;
} scenario_metrics_t;
static uint32_t random_state;
static double uniform_random(void)
{
random_state = 1664525u * random_state + 1013904223u;
return ((double)((random_state >> 8) & 0x00ffffffu) + 0.5) /
16777216.0;
}
static double gaussian_random(void)
{
double first = fmax(uniform_random(), 1.0e-12);
double second = uniform_random();
return sqrt(-2.0 * log(first)) * cos(2.0 * PI * second);
}
static void truth_at(double time_s, double position[3],
double velocity[3], double acceleration[3])
{
memset(position, 0, 3u * sizeof(double));
memset(velocity, 0, 3u * sizeof(double));
memset(acceleration, 0, 3u * sizeof(double));
if(time_s < 3.0)
{
return;
}
if(time_s < 5.0)
{
double elapsed = time_s - 3.0;
acceleration[0] = 1.5;
velocity[0] = 1.5 * elapsed;
position[0] = 0.75 * elapsed * elapsed;
return;
}
if(time_s < 50.0)
{
velocity[0] = 3.0;
position[0] = 3.0 + 3.0 * (time_s - 5.0);
return;
}
if(time_s < 52.0)
{
double elapsed = time_s - 50.0;
acceleration[0] = -1.5;
velocity[0] = 3.0 - 1.5 * elapsed;
position[0] = 138.0 + 3.0 * elapsed - 0.75 * elapsed * elapsed;
return;
}
position[0] = 141.0;
}
static fusion_imu_measurement_t make_imu(
uint64_t ticks, unsigned int sequence, double gravity_mps2)
{
double time_s = (double)ticks / (double)TIMEBASE_HZ;
double position[3];
double velocity[3];
double acceleration[3];
truth_at(time_s, position, velocity, acceleration);
(void)position;
(void)velocity;
fusion_imu_measurement_t imu;
memset(&imu, 0, sizeof(imu));
imu.timestamp_ticks = ticks;
imu.accel_mps2[0] = acceleration[0] + 0.030;
imu.accel_mps2[1] = -0.020;
imu.accel_mps2[2] = gravity_mps2 + 0.050;
imu.gyro_rps[0] = 0.0008;
imu.gyro_rps[1] = -0.0005;
imu.gyro_rps[2] = 0.0006;
if(time_s >= 3.0)
{
/* Small temperature-like residual drift after startup calibration. */
imu.accel_mps2[0] += 0.012 * (time_s - 3.0) / 62.0;
}
for(unsigned int axis = 0u ; axis < 3u ; axis++)
{
double phase = 0.37 * (double)sequence + 1.7 * (double)axis;
imu.accel_mps2[axis] += 0.025 * sin(phase);
imu.gyro_rps[axis] += 0.0007 * cos(1.31 * phase);
}
imu.temperature_c = 25.0 + 0.02 * time_s;
imu.sequence = sequence;
imu.vehicle_at_rest = (time_s < 2.9) || (time_s >= 52.5);
return imu;
}
static fusion_position_measurement_t make_gps(
uint64_t capture_ticks, bool bad_restart_fix)
{
double position[3];
double velocity[3];
double acceleration[3];
truth_at((double)capture_ticks / (double)TIMEBASE_HZ,
position, velocity, acceleration);
(void)velocity;
(void)acceleration;
fusion_position_measurement_t gps;
memset(&gps, 0, sizeof(gps));
gps.timestamp_ticks = capture_ticks;
gps.position_enu_m[0] = position[0] + 6.0 * gaussian_random();
gps.position_enu_m[1] = position[1] + 6.0 * gaussian_random();
gps.position_enu_m[2] = position[2] + 10.0 * gaussian_random();
if(bad_restart_fix)
{
gps.position_enu_m[0] += 80.0;
gps.position_enu_m[1] -= 50.0;
gps.position_enu_m[2] += 30.0;
}
gps.covariance_enu_m2[0] = 36.0;
gps.covariance_enu_m2[4] = 36.0;
gps.covariance_enu_m2[8] = 100.0;
gps.source_quality = 80.0;
gps.source_valid = true;
return gps;
}
static bool event_should_drop(scenario_kind_t kind, double capture_s)
{
if((kind == SCENARIO_MOVING_OUTAGE) &&
(capture_s >= 15.0) && (capture_s < 27.0))
{
return true;
}
if((kind == SCENARIO_BAD_RESTART) &&
(capture_s >= 15.0) && (capture_s < 23.1))
{
return true;
}
if(kind == SCENARIO_INTERMITTENT)
{
bool natural_long_gap =
((capture_s >= 12.0) && (capture_s < 13.8)) ||
((capture_s >= 31.0) && (capture_s < 32.8));
return natural_long_gap || (uniform_random() < 0.35);
}
return false;
}
static unsigned int build_events(
scenario_kind_t kind, gps_event_t events[MAX_GPS_EVENTS],
unsigned int* dropped_out)
{
unsigned int count = 0u;
unsigned int dropped = 0u;
uint64_t previous_delivery = 0u;
for(uint64_t capture = 3300000u ; capture <= 64000000u ;
capture += GPS_PERIOD_TICKS)
{
double capture_s = (double)capture / (double)TIMEBASE_HZ;
if(event_should_drop(kind, capture_s))
{
dropped++;
continue;
}
if(count >= MAX_GPS_EVENTS)
{
break;
}
bool bad_restart_fix = (kind == SCENARIO_BAD_RESTART) &&
(capture_s >= 23.1) && (capture_s < 24.0);
events[count].measurement = make_gps(capture, bad_restart_fix);
uint64_t delay = 120000u + (uint64_t)(250000.0 * uniform_random());
uint64_t delivery = capture + delay;
if((kind == SCENARIO_BURST_RETURN) &&
(capture_s >= 15.0) && (capture_s < 20.5))
{
delivery = 20700000u;
}
/* A receiver normally publishes epochs in capture order even when its
* processing latency jitters. Preserve that property here. */
if(delivery < previous_delivery)
{
delivery = previous_delivery;
}
events[count].delivery_ticks = delivery;
previous_delivery = delivery;
count++;
}
*dropped_out = dropped;
return count;
}
static double horizontal_error(const fusion_engine_t* engine, uint64_t ticks)
{
double truth_position[3];
double truth_velocity[3];
double truth_acceleration[3];
truth_at((double)ticks / (double)TIMEBASE_HZ,
truth_position, truth_velocity, truth_acceleration);
(void)truth_velocity;
(void)truth_acceleration;
return hypot(engine->current.filter.position_m[0] - truth_position[0],
engine->current.filter.position_m[1] - truth_position[1]);
}
static scenario_metrics_t run_scenario(
scenario_kind_t kind, const char* name, uint32_t seed)
{
static fusion_engine_t engine;
static gps_event_t events[MAX_GPS_EVENTS];
fusion_engine_config_t config;
scenario_metrics_t metrics;
memset(&metrics, 0, sizeof(metrics));
metrics.name = name;
random_state = seed;
unsigned int dropped = 0u;
unsigned int event_count = build_events(kind, events, &dropped);
metrics.generated = event_count;
metrics.dropped = dropped;
fusion_engine_default_config(&config, TIMEBASE_HZ);
if(!fusion_engine_init(&engine, &config))
{
return metrics;
}
fusion_position_measurement_t origin;
memset(&origin, 0, sizeof(origin));
origin.timestamp_ticks = 1u;
origin.covariance_enu_m2[0] = 36.0;
origin.covariance_enu_m2[4] = 36.0;
origin.covariance_enu_m2[8] = 100.0;
origin.source_quality = 100.0;
origin.source_valid = true;
if(!fusion_engine_push_position(&engine, &origin, NULL, NULL))
{
return metrics;
}
unsigned int event_head = 0u;
double squared_error_sum = 0.0;
unsigned int error_count = 0u;
unsigned int imu_count = 65u * IMU_RATE_HZ;
for(unsigned int sample = 0u ; sample <= imu_count ; sample++)
{
uint64_t ticks =
((uint64_t)sample * TIMEBASE_HZ + IMU_RATE_HZ / 2u) / IMU_RATE_HZ;
fusion_imu_measurement_t imu = make_imu(
ticks, sample, config.eskf.gravity_mps2);
if(!fusion_engine_push_imu(&engine, &imu))
{
return metrics;
}
while((event_head < event_count) &&
(events[event_head].delivery_ticks <= ticks))
{
double before[3];
memcpy(before, engine.current.filter.position_m, sizeof(before));
(void)fusion_engine_push_position(
&engine, &events[event_head].measurement, NULL, NULL);
double jump = hypot(
engine.current.filter.position_m[0] - before[0],
engine.current.filter.position_m[1] - before[1]);
if(jump > metrics.maximum_gps_state_jump_m)
{
metrics.maximum_gps_state_jump_m = jump;
}
event_head++;
}
if(engine.state != FUSION_ENGINE_RUNNING)
{
continue;
}
double time_s = (double)ticks / (double)TIMEBASE_HZ;
double error = horizontal_error(&engine, ticks);
if(time_s >= 3.0)
{
squared_error_sum += error * error;
error_count++;
}
if((time_s >= 15.0) && (time_s <= 27.0) &&
(error > metrics.maximum_outage_error_m))
{
metrics.maximum_outage_error_m = error;
}
if(fabs(time_s - 27.0) < 0.5 / (double)IMU_RATE_HZ)
{
metrics.error_at_outage_end_m = error;
metrics.speed_at_outage_end_mps =
hypot(engine.current.filter.velocity_mps[0],
engine.current.filter.velocity_mps[1]);
metrics.horizontal_sigma_at_outage_end_m = sqrt(
engine.current.filter.covariance[0][0] +
engine.current.filter.covariance[1][1]);
}
if(fabs(time_s - 15.0) < 0.5 / (double)IMU_RATE_HZ)
{
metrics.speed_at_outage_start_mps =
hypot(engine.current.filter.velocity_mps[0],
engine.current.filter.velocity_mps[1]);
}
}
metrics.horizontal_rmse_m =
(error_count != 0u) ? sqrt(squared_error_sum / (double)error_count) : NAN;
metrics.final_horizontal_error_m =
horizontal_error(&engine, 65u * TIMEBASE_HZ);
metrics.gps_accepted = engine.diagnostics.position_accepted;
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
metrics.gps_recoveries = engine.diagnostics.position_recovery_count;
metrics.stale_rejected = engine.diagnostics.position_stale_rejected;
metrics.replay_count = engine.diagnostics.replay_count;
metrics.finite = (engine.state == FUSION_ENGINE_RUNNING) &&
eskf_state_is_finite(&engine.current.filter) &&
isfinite(metrics.horizontal_rmse_m) &&
isfinite(metrics.final_horizontal_error_m);
return metrics;
}
static void print_metrics(const scenario_metrics_t* metrics)
{
printf("%-18s fixes=%3u drop=%2u accepted/rejected=%3llu/%2llu "
"recovery=%llu stale=%llu rmse=%5.2fm outage-max/end=%5.2f/%5.2fm "
"speed15/27=%4.2f/%4.2f sigma27=%5.1fm jump=%5.2fm final=%5.2fm finite=%u\n",
metrics->name, metrics->generated, metrics->dropped,
(unsigned long long)metrics->gps_accepted,
(unsigned long long)metrics->gps_rejected,
(unsigned long long)metrics->gps_recoveries,
(unsigned long long)metrics->stale_rejected,
metrics->horizontal_rmse_m,
metrics->maximum_outage_error_m,
metrics->error_at_outage_end_m,
metrics->speed_at_outage_start_mps,
metrics->speed_at_outage_end_mps,
metrics->horizontal_sigma_at_outage_end_m,
metrics->maximum_gps_state_jump_m,
metrics->final_horizontal_error_m,
metrics->finite ? 1u : 0u);
}
int main(void)
{
scenario_metrics_t normal = run_scenario(
SCENARIO_NORMAL, "normal", 0x10203040u);
scenario_metrics_t outage = run_scenario(
SCENARIO_MOVING_OUTAGE, "12s-moving-outage", 0x20304050u);
scenario_metrics_t intermittent = run_scenario(
SCENARIO_INTERMITTENT, "intermittent", 0x30405060u);
scenario_metrics_t burst = run_scenario(
SCENARIO_BURST_RETURN, "burst-return", 0x40506070u);
scenario_metrics_t restart = run_scenario(
SCENARIO_BAD_RESTART, "bad-restart", 0x50607080u);
puts("Real-log-scaled GNSS failure scenarios: 3.3 Hz, 6 m horizontal / 10 m vertical sigma");
print_metrics(&normal);
print_metrics(&outage);
print_metrics(&intermittent);
print_metrics(&burst);
print_metrics(&restart);
bool common_ok =
normal.finite && outage.finite && intermittent.finite && burst.finite &&
restart.finite &&
(normal.horizontal_rmse_m < 5.0) &&
/* Position-only dead reckoning is allowed to drift, but the error must
* remain statistically covered and recover once GNSS returns. */
(outage.maximum_outage_error_m <
2.0 * outage.horizontal_sigma_at_outage_end_m) &&
(outage.horizontal_sigma_at_outage_end_m > 10.0) &&
(outage.final_horizontal_error_m < 5.0) &&
(intermittent.final_horizontal_error_m < 6.0) &&
(burst.stale_rejected == 0u) &&
(burst.replay_count > 0u);
bool restart_safe =
(restart.gps_recoveries == 0u) &&
(restart.gps_rejected >= 3u) &&
(restart.maximum_gps_state_jump_m < 50.0) &&
(restart.final_horizontal_error_m < 6.0);
if(!common_ok || !restart_safe)
{
fprintf(stderr,
"FAIL: GNSS outage/reacquisition safety regression "
"(common=%u restart-safe=%u)\n",
common_ok ? 1u : 0u, restart_safe ? 1u : 0u);
return 1;
}
puts("PASS: realistic GNSS loss, intermittent delivery and reacquisition stayed bounded.");
return 0;
}
@@ -474,14 +474,14 @@ static void print_metrics(scenario_kind_t scenario, unsigned int gps_ms,
static int run_matrix(void)
{
static const unsigned int gps_periods_ms[] = {200u, 500u, 1000u};
static const unsigned int imu_periods_ms[] = {19u, 20u, 200u, 300u};
static const unsigned int imu_periods_ms[] = {19u, 20u, 100u, 200u, 300u};
bool passed = true;
printf("Four-satellite asynchronous platform matrix; 3 s outage and 2 s coherent GNSS fault\n");
for(unsigned int scenario = 0u; scenario < SCENARIO_COUNT; scenario++)
{
for(unsigned int gps_index = 0u; gps_index < 3u; gps_index++)
{
for(unsigned int imu_index = 0u; imu_index < 4u; imu_index++)
for(unsigned int imu_index = 0u; imu_index < 5u; imu_index++)
{
metrics_t metrics = run_case((scenario_kind_t)scenario,
gps_periods_ms[gps_index], imu_periods_ms[imu_index], NULL);
@@ -504,7 +504,7 @@ static int run_matrix(void)
fprintf(stderr, "FAIL: platform scenario matrix contract.\n");
return 1;
}
printf("PASS: 19/20 ms production rates stayed bounded; 200/300 ms coarse IMU "
printf("PASS: 19/20 ms production rates stayed bounded; 100/200/300 ms coarse IMU "
"rates remained finite and their dynamic accuracy loss is reported, not hidden.\n");
return 0;
}
@@ -512,6 +512,33 @@ static int run_matrix(void)
int main(int argc, char** argv)
{
if(argc == 1) return run_matrix();
if((argc == 6) && (strcmp(argv[1], "--benchmark") == 0))
{
scenario_kind_t scenario;
if(!parse_scenario(argv[2], &scenario))
{
fprintf(stderr, "unknown scenario: %s\n", argv[2]);
return 2;
}
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
unsigned int iterations = (unsigned int)strtoul(argv[5], NULL, 10);
if((iterations == 0u) ||
!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
!((imu_ms == 19u) || (imu_ms == 20u) || (imu_ms == 100u) ||
(imu_ms == 200u) || (imu_ms == 300u)))
{
fprintf(stderr, "supported GPS: 200/500/1000 ms; "
"IMU: 19/20/100/200/300 ms; iterations > 0\n");
return 2;
}
metrics_t metrics;
for(unsigned int iteration = 0u; iteration < iterations; iteration++)
metrics = run_case(scenario, gps_ms, imu_ms, NULL);
print_metrics(scenario, gps_ms, imu_ms, &metrics);
printf("completed %u benchmark iterations\n", iterations);
return metrics.finite ? 0 : 1;
}
if((argc == 6) && (strcmp(argv[1], "--log") == 0))
{
scenario_kind_t scenario;
@@ -523,10 +550,10 @@ int main(int argc, char** argv)
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
if(!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
!((imu_ms == 19u) || (imu_ms == 20u) ||
!((imu_ms == 19u) || (imu_ms == 20u) || (imu_ms == 100u) ||
(imu_ms == 200u) || (imu_ms == 300u)))
{
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/200/300 ms\n");
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/100/200/300 ms\n");
return 2;
}
FILE* log_file = fopen(argv[5], "w");
@@ -546,6 +573,9 @@ int main(int argc, char** argv)
return metrics.finite ? 0 : 1;
}
fprintf(stderr, "usage: %s [--log stationary|pedestrian|car|aircraft "
"200|500|1000 19|20|200|300 output.log]\n", argv[0]);
"200|500|1000 19|20|100|200|300 output.log]\n"
" %s [--benchmark stationary|pedestrian|car|aircraft "
"200|500|1000 19|20|100|200|300 iterations]\n",
argv[0], argv[0]);
return 2;
}
@@ -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,17 +1110,19 @@ 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_select(
bool selected = gps_nav_epoch_validate_complete(
candidates, candidate_count, EPOCH_SAMPLE_WINDOW,
EPOCH_TOW_TOLERANCE_SEC, &selection);
unsigned selected_count = selected ? selection.selected_count : 0u;
unsigned selected_count = selection.selected_count;
stage_metrics->selected_total += selected_count;
stage_metrics->epoch_exclusions += candidate_count - selected_count;
if (stage == STAGE_DELAYED_CHANNEL &&
(selected_count != 7u ||
(selected ||
selected_count != 7u ||
(selection.selected_mask & (1u << 7)) != 0u)) {
delayed_selection_failures++;
}
@@ -930,7 +1135,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
postcommit_join_count_failures++;
}
if (selected_count < STATE_DIM) {
if (!selected || selected_count < STATE_DIM) {
continue;
}
@@ -969,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,
@@ -983,14 +1198,14 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
CHECK(metrics[STAGE_THREE_DROPOUTS].fixes >= 58u,
"five remaining satellites must continue producing fixes");
CHECK(metrics[STAGE_MINIMUM_FOUR].fixes == 40u,
"four satellites must remain a publishable minimum fix");
"four satellites must remain a raw publishable minimum fix");
CHECK(metrics[STAGE_OUTAGE_THREE].fixes == 0u && below_minimum_fixes == 0u,
"three satellites must suppress the position solution");
CHECK(metrics[STAGE_REACQUIRE].fixes >= 58u,
"position must recover after satellite reacquisition");
CHECK(delayed_selection_failures == 0u &&
metrics[STAGE_DELAYED_CHANNEL].fixes >= 58u,
"delayed/newest outlier channel must be excluded without blocking fixes");
metrics[STAGE_DELAYED_CHANNEL].fixes == 0u,
"incoherent delayed channel must reject the complete raw epoch");
CHECK(precommit_join_count_failures == 0u,
"joining channel must remain excluded before matching SF1/SF2/SF3");
CHECK(postcommit_join_count_failures == 0u,
@@ -1009,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];
@@ -1039,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");
@@ -1285,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");
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.venv/
@@ -0,0 +1,241 @@
# GPS code measurement ve DLL kalite modeli
Bu dizin, mevcut firmware/RTL ölçüm yolunu değiştirmeden önce code-phase
hassasiyeti, DLL jitter telemetrisi, dinamik WLS sigma ve carrier/Doppler
smoothing seçeneklerini aynı sentetik veri üzerinde karşılaştırır.
Bu genel amaçlı bir RF/GNSS simülatörü değildir. Projeye özgü executable
tasarım modelidir.
Dizin iki ayrı kararı inceler:
1. Position WLS için C/N0'ın açıklamadığı DLL jitter/variance telemetrisi.
2. Velocity WLS ve ESKF öncesindeki sabit Doppler IIR'in gürültü-gecikme
etkisi.
## Modellenen mevcut mimari
| Proje davranışı | Model |
|---|---|
| Normalized early-minus-late DLL discriminator | `rtl_dll_model.rtl_discriminator()` |
| Signed Q2.13 discriminator quantization | `rtl_dll_model.py` |
| `LF_code.sv` C1=`21.25`, C2=`0.05712890625` ve Q20 çıkış | `CodeLoopFilterRtl` |
| 20 adet 1 ms DLL örneği/NAV bit | `synthesize_dll_window()` |
| Firmware C/N0 IIR, alpha=`0.2` | `ChannelQuality.update()` |
| URA/elevation/C/N0 position sigma | `firmware_sigma_m(CURRENT)` |
| ECEF x/y/z + common clock-bias WLS | `firmware_wls()` |
| 99% chi-square ve en az 7 uyduda leave-one-out FDE | `firmware_wls()` |
| Position solve periyodu 200 ms | `measurement_scenarios.py` |
| Doppler IIR alpha=`0.05`, 50 Hz | `doppler_eskf_model.py` |
| ECEF velocity + common clock-drift WLS | `velocity_wls()` |
| Velocity WLS periyodu 200 ms | `doppler_eskf_model.py` |
| TDK 100 Hz IMU process-noise profili | `VelocityBiasEskf` |
| Velocity 3-DOF `%99.9`, NIS=`16.266236` | `VelocityBiasEskf.update_velocity()` |
Kaynak eşlemeleri:
- `hardware/external/gps/rtl/trck_src/code_loop_discriminator.sv`
- `hardware/external/gps/rtl/trck_src/LF_code.sv`
- `hardware/external/gps/rtl/trck_src/NCO_code.sv`
- `hardware/external/gps/rtl/trck_src/tracking_measurement.sv`
- `hardware/external/gps/rtl/common/definitions.sv`
- `soc/software/gps_system/src/gps/gps_tracking_measurement.c`
- `soc/software/gps_system/src/gps/gps_interrupt_handlers.c`
- `soc/software/gps_system/src/gps/NavEquations.c`
`REM_CODE_PHASE`, DLL discriminator residual'ı olarak modellenmez. RTL'de bu
değer code NCO'nun epoch taşma remainder'ıdır. Yeni kalite metriğinin girdisi
RTL içindeki gerçek signed `dll_error` örnekleridir.
## Karşılaştırılan sigma politikaları
- `current_cn0`: mevcut URA + elevation + C/N0 modeli.
- `naive_add_dll`: DLL variance'ı bağımsızmış gibi ekler. Nominal durumda aynı
thermal jitter'ı iki kere saydığı için negatif kontrol olarak bulunur.
- `dll_excess`: yalnız ölçülen DLL variance'ın C/N0'dan beklenen variance'ı aşan
bölümünü code sigma'ya ekler.
- `dll_excess_guard`: excess variance'a ek olarak sürekli discriminator mean
stress'inde ağırlığı azaltır.
- `oracle_variance`: sentetik gerçek jitter çarpanını bilir. Ulaşılabilir üst
sınırı gösterir; firmware algoritması değildir.
Dinamik model:
```text
sigma_base^2 = sigma_URA^2 + sigma_elevation^2 + sigma_CN0^2
ratio = EWMA(measured_DLL_variance / expected_DLL_variance(C/N0))
sigma_dynamic^2 =
sigma_URA^2
+ sigma_elevation^2
+ sigma_CN0^2 * max(1, ratio)
```
Bu form C/N0 ile DLL thermal jitter'ını iki kere toplamaz. Yalnızca C/N0
modelinin açıklayamadığı kanal-bağımlı excess jitter'ı şişirir.
## Senaryolar
- Temiz açık gökyüzü
- Bütün kanallarda düşük C/N0
- C/N0'da görünmeyen tek-kanal DLL jitter
- Bütün kanallarda ortak, C/N0'da görünmeyen DLL jitter
- Gürültü burst ve recovery
- Re-lock sırasında DLL mean/jitter stress
- Düşük-variance stable multipath bias
- Bias + excess jitter
- Kötü geometri + gizli jitter
- İlk başta düşük, sonra iyileşen C/N0
Ayrı 1D model ham code, kör scalar IIR ve Doppler-predicted code smoothing'i
range-rate adımları ve residual carrier CFO altında karşılaştırır. Carrier
ölçümleri RTL cadence'i olan 50 Hz'de üretilir ve 200 ms WLS epoch'una kadar
entegre edilir. Ham, sabit `alpha=0.05` ve IMU hareket durumuna göre bant
genişliğini açan adaptif Doppler yolları birlikte denenir. Bu ayrım önemlidir:
PPS datum-drift predictor'u için yararlı olan yavaş Doppler filtresi, ESKF
velocity veya hızlı code-aiding prediction'ında dinamik lag üretebilir; tam
ham snapshot ise kartta ölçülen gürültü seviyesinde fazla varyans üretebilir.
## Doppler pre-IIR ve ESKF modeli
`doppler_eskf_model.py`, aynı truth, tracking gürültüsü ve IMU örnekleriyle şu
dört yolu karşılaştırır:
- Önceki: 50 Hz `alpha=0.05` IIR -> 5 Hz velocity WLS -> ESKF.
- Ham Doppler -> 5 Hz velocity WLS -> ESKF.
- C/N0 + düşük-geçiren navigation acceleration ile adaptif IIR -> WLS -> ESKF.
- Ham Doppler -> RAIM-benzeri leave-one-out robust WLS -> ESKF.
Mevcut `alpha=0.05` tek-kutuplu IIR için 50 Hz'de eşdeğer cutoff yaklaşık
`0.408 Hz`, zaman sabiti `0.390 s`, DC group delay ise yaklaşık `380 ms`'dir.
Bu gecikme constant velocity'de görünmez; acceleration sırasında velocity
measurement innovation'ına doğrudan girer.
ESKF modeli bu karar için gerekli `[velocity, accel-bias]` alt sistemidir.
Firmware'deki TDK process acceleration noise density `0.025
m/s2/sqrt(Hz)`, accelerometer bias random walk `0.0015 m/s2/sqrt(s)`, WLS'nin
gerçek 3x3 covariance'ı, fusion adapter'ın `2/2/3 m/s` ENU velocity sigma
tabanları ve velocity NIS kapısı kullanılır. Position, attitude, lever-arm ve
tam 15-state cross-covariance bu modelin kapsamında değildir.
Senaryo matrisi sabit uçuş, airliner/aggressive manevra, düşük C/N0, tek-kanal
Doppler burst, common receiver clock-drift step, 15 saniye GPS kesintisi ve
birleşik arıza koşullarını içerir.
Nominal 20-seed sentetik sonuçların özeti:
- ESKF ham WLS gürültüsünü nominalde yaklaşık `0.416 m/s` 3D RMS'ten `0.190
m/s` seviyesine indirir; temporal filtreleme zaten ESKF'de gerçekleşir.
- Sabit pre-IIR sakin uçuşta `0.182 m/s`, ham yol `0.190 m/s` verir. Airliner
manevrasında sabit IIR gecikmesi sonucu `0.416 m/s`, ham yol `0.190 m/s`;
aggressive manevrada sırasıyla `0.942 m/s` ve `0.190 m/s` oluşur. Adapter
sigma tabanları iki yolu da NIS içinde tutsa bile dinamik lag kalır.
- Düşük C/N0 manevrasında adaptif taslak en iyi gürültü/gecikme uzlaşmasını
verir; eşikleri firmware'e taşımadan önce kart loguyla kalibre etmek gerekir.
- ESKF yalnız aggregate 3D velocity gördüğünden tek uydu burst'ünün kaynağını
ayıramaz. Kanal outlier/FDE işlemi velocity WLS öncesinde yapılmalıdır.
- Common receiver clock-frequency hatası WLS'nin dördüncü clock-drift state'i
tarafından hızdan ayrılır.
Genesys2 sabit-kart ölçümünde tam ham yolun velocity-WLS RMS'i `9.17 m/s`,
p95'i `16.71 m/s` ve FDE oranı `86/120` oldu. Aynı epoch'larda sabit
`alpha=0.05` yol `0.92 m/s` RMS, `1.63 m/s` p95 ve `0/120` FDE verdi. Bu
nedenle nominal modelde iyi görünen doğrudan ham yol production'a alınmadı.
Kart gürültüsünü eşlemek için tracking noise `22x` ile kalibre edilen regresyon
modelindeki regresyon seed'leri için steady-7/airliner-3/aggressive-7 fused
RMS değerleri:
- sabit IIR: `0.451 / 0.484 / 1.036 m/s`,
- adaptif IIR: `0.451 / 0.336 / 0.538 m/s`,
- tam ham: `1.260 / 1.100 / 1.260 m/s`.
Production firmware ESKF için `alpha=0.05/0.20/0.40` hareket-adaptif Doppler
-> bandwidth-aware sigma -> kanal validity/FDE -> velocity WLS + covariance
-> ESKF yolunu kullanır. Ham alan tanısal kayıt ve offline analiz için korunur.
Ayrı sabit `alpha=0.05` Doppler velocity yalnız PPS position-WLS clock-bias
datumunun drift predictor'u ve PPS kalite metriği için kullanılır. Ayrıntılı
nominal tablo ve model sınırları `output/DOPPLER_ESKF_ANALYSIS.md` içindedir;
kart-kalibreli karar `test_doppler_eskf_model.py` ile regresyona alınmıştır.
## Kurulum ve çalıştırma
Repo içindeki `soc/software` dizininden:
```bash
python3 -m venv gps_measurement_python/.venv
gps_measurement_python/.venv/bin/pip install \
-r gps_measurement_python/requirements.txt
gps_measurement_python/.venv/bin/python -m unittest \
gps_measurement_python/test_measurement_model.py \
gps_measurement_python/test_doppler_eskf_model.py
gps_measurement_python/.venv/bin/python \
-m gps_measurement_python.analyze_measurement_scenarios \
--seeds 12 --jobs 4
gps_measurement_python/.venv/bin/python \
-m gps_measurement_python.analyze_doppler_eskf \
--seeds 20 --jobs 4
```
Sistem Python'unda `venv` paketi yoksa bağımlılıklar izole bir target dizinine
de kurulabilir:
```bash
python3 -m pip install --target /tmp/gps-measurement-deps \
-r gps_measurement_python/requirements.txt
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
-m unittest gps_measurement_python/test_measurement_model.py
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
-m gps_measurement_python.analyze_measurement_scenarios \
--seeds 12 --jobs 4
```
Çıktılar `gps_measurement_python/output/` altındadır:
- `scenario_summary.csv`
- `ANALYSIS.md`
- `01_position_3d_rms.png`
- `02_solution_availability.png`
- `03_noise_burst_transition.png`
- `04_code_smoothing_cfo.png`
- `doppler_eskf_summary.csv`
- `DOPPLER_ESKF_ANALYSIS.md`
- `05_doppler_eskf_velocity_rms.png`
- `06_airliner_maneuver_trace.png`
- `07_outlier_wls_nis.png`
- `08_low_cn0_wls_vs_eskf.png`
## Kalibrasyon sınırı
Modelde C/N0'dan beklenen normalized discriminator standard deviation'ı
45 dB-Hz için `0.020` kabul edilir ve firmware ile aynı 20 dB amplitude yasası
ile ölçeklenir. Bu sayı karttan alınmış bir kalibrasyon değildir.
Karar vermeden önce karttan her kanal için en az şu 50 Hz kayıt alınmalıdır:
```text
sequence, PRN, C/N0,
DLL mean, DLL variance, DLL sample count,
code NCO correction, carrier frequency,
ABS_SAMPLE, REM_CODE_PHASE, lock
```
Kayıtlarla `expected_DLL_variance(C/N0)` eğrisi çıkarılmalı; temperature,
elevation ve lock yaşına göre ayrıştırılmalıdır. Stable multipath, DLL yeni
denge noktasına oturduğunda düşük variance bırakabilir. Bu durum tek başına
DLL variance ile çözülemez; ek correlator tap'leri veya correlation-shape
metriği gerekir.
RTL için en düşük maliyetli keşif arayüzü, her 20 ms NAV-bit penceresinde
`dll_error` üzerinden aşağıdaki ham istatistikleri biriktirmektir:
```text
signed sum(error) : 15 + ceil(log2(20)) = 20 bit
unsigned sum(error^2) : 30 + ceil(log2(20)) = 35 bit
valid sample count : 5 bit
```
35-bit kare toplamı APB'de iki 32-bit register ile taşınabilir. Ortalama,
sample variance, C/N0 expectation ratio ve hysteresis yazılımda hesaplanır.
Bölme ve square-root RTL'ye konmaz. Bu tuple mevcut measurement sequence,
C/N0, carrier frequency, `ABS_SAMPLE` ve `REM_CODE_PHASE` ile aynı 20 ms
snapshot'ta tutulmalıdır.
@@ -0,0 +1 @@
"""Project-specific GPS code-measurement and WLS analysis model."""
@@ -0,0 +1,428 @@
"""Monte Carlo comparison of Doppler prefilter choices before velocity ESKF."""
from __future__ import annotations
import argparse
import csv
import math
from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from .doppler_eskf_model import (
DopplerEskfTrace,
DopplerPath,
PATH_ORDER,
VELOCITY_NIS_GATE,
flight_scenarios,
run_doppler_eskf_scenario,
)
PATH_LABEL = {
DopplerPath.FIXED_IIR: "Önceki sabit IIR",
DopplerPath.RAW: "Ham Doppler + ESKF",
DopplerPath.ADAPTIVE_IIR: "Adaptif IIR + ESKF",
DopplerPath.RAW_ROBUST: "Ham + robust WLS + ESKF",
}
@dataclass(frozen=True)
class TrialMetrics:
scenario: str
path: DopplerPath
fused_rms_mps: float
fused_p95_mps: float
quiet_rms_mps: float
dynamic_rms_mps: float
wls_rms_mps: float
wls_p95_mps: float
clock_drift_rms_mps: float
velocity_accept_percent: float
nis_reject_percent: float
fused_95_coverage_percent: float
excluded_epoch_percent: float
mean_covariance_scale: float
mean_alpha: float
@dataclass(frozen=True)
class Summary:
scenario: str
path: DopplerPath
seeds: int
fused_rms_mps: float
fused_rms_seed_std_mps: float
fused_p95_mps: float
quiet_rms_mps: float
dynamic_rms_mps: float
wls_rms_mps: float
wls_p95_mps: float
clock_drift_rms_mps: float
velocity_accept_percent: float
nis_reject_percent: float
fused_95_coverage_percent: float
excluded_epoch_percent: float
mean_covariance_scale: float
mean_alpha: float
def _rms(values: np.ndarray) -> float:
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
def _truth_at_wls(trace: DopplerEskfTrace) -> np.ndarray:
return np.column_stack(
[
np.interp(
trace.wls_time_s,
trace.time_s,
trace.truth_velocity_mps[:, axis],
)
for axis in range(3)
]
)
def trial_metrics(scenario_index: int, trace: DopplerEskfTrace) -> TrialMetrics:
scenario = flight_scenarios()[scenario_index]
burn_in = trace.time_s >= 5.0
fused_vector_error = trace.fused_velocity_mps - trace.truth_velocity_mps
fused_error = np.linalg.norm(fused_vector_error, axis=1)
dynamic = burn_in & (np.linalg.norm(trace.acceleration_mps2, axis=1) > 0.25)
quiet = burn_in & ~dynamic
fused_nees = []
for sample in np.flatnonzero(burn_in):
covariance = trace.fused_velocity_covariance_mps2[sample]
error = fused_vector_error[sample]
fused_nees.append(float(error @ np.linalg.inv(covariance) @ error))
fused_nees_array = np.asarray(fused_nees)
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
true_clock_drift = np.asarray(
[scenario.receiver_clock_drift_mps(float(time)) for time in trace.wls_time_s]
)
clock_error = trace.wls_clock_drift_mps - true_clock_drift
valid_nis = trace.wls_nis[np.isfinite(trace.wls_nis)]
return TrialMetrics(
scenario=trace.scenario,
path=trace.path,
fused_rms_mps=_rms(fused_error[burn_in]),
fused_p95_mps=float(np.percentile(fused_error[burn_in], 95.0)),
quiet_rms_mps=_rms(fused_error[quiet]),
dynamic_rms_mps=_rms(fused_error[dynamic]),
wls_rms_mps=_rms(wls_error),
wls_p95_mps=float(np.percentile(wls_error, 95.0)),
clock_drift_rms_mps=_rms(clock_error),
velocity_accept_percent=100.0 * float(np.mean(trace.wls_accepted)),
nis_reject_percent=100.0 * float(np.mean(valid_nis > VELOCITY_NIS_GATE)),
# 7.8147 is the 95-percent chi-square threshold for three states.
fused_95_coverage_percent=100.0 * float(np.mean(fused_nees_array <= 7.8147279)),
excluded_epoch_percent=100.0
* float(np.mean(trace.excluded_satellites >= 0)),
mean_covariance_scale=float(np.mean(trace.wls_covariance_scale)),
mean_alpha=float(np.mean(trace.adaptive_alpha)),
)
def _worker(task: tuple[int, int]) -> tuple[int, int, list[TrialMetrics]]:
scenario_index, seed = task
scenario = flight_scenarios()[scenario_index]
traces = run_doppler_eskf_scenario(scenario, seed)
return (
scenario_index,
seed,
[trial_metrics(scenario_index, traces[path]) for path in PATH_ORDER],
)
def summarize(items: list[TrialMetrics]) -> Summary:
if not items:
raise ValueError("cannot summarize an empty trial list")
def average(field_name: str) -> float:
values = np.asarray([getattr(item, field_name) for item in items], dtype=float)
finite = values[np.isfinite(values)]
return float(np.mean(finite)) if len(finite) else math.nan
fused_rms = np.asarray([item.fused_rms_mps for item in items])
return Summary(
scenario=items[0].scenario,
path=items[0].path,
seeds=len(items),
fused_rms_mps=average("fused_rms_mps"),
fused_rms_seed_std_mps=float(np.std(fused_rms)),
fused_p95_mps=average("fused_p95_mps"),
quiet_rms_mps=average("quiet_rms_mps"),
dynamic_rms_mps=average("dynamic_rms_mps"),
wls_rms_mps=average("wls_rms_mps"),
wls_p95_mps=average("wls_p95_mps"),
clock_drift_rms_mps=average("clock_drift_rms_mps"),
velocity_accept_percent=average("velocity_accept_percent"),
nis_reject_percent=average("nis_reject_percent"),
fused_95_coverage_percent=average("fused_95_coverage_percent"),
excluded_epoch_percent=average("excluded_epoch_percent"),
mean_covariance_scale=average("mean_covariance_scale"),
mean_alpha=average("mean_alpha"),
)
def write_csv(path: Path, summaries: list[Summary]) -> None:
with path.open("w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
writer.writeheader()
for item in summaries:
row = dict(item.__dict__)
row["path"] = item.path.value
writer.writerow(row)
def _summary_plot(path: Path, summaries: list[Summary]) -> None:
scenarios = flight_scenarios()
x = np.arange(len(scenarios))
width = 0.19
figure, axis = plt.subplots(figsize=(15, 6.3))
for path_index, doppler_path in enumerate(PATH_ORDER):
values = [
next(
item.fused_rms_mps
for item in summaries
if item.scenario == scenario.name and item.path == doppler_path
)
for scenario in scenarios
]
axis.bar(
x + (path_index - 1.5) * width,
values,
width,
label=PATH_LABEL[doppler_path],
)
axis.set_ylabel("ESKF 3D velocity RMS (m/s)")
axis.set_yscale("log")
axis.set_xticks(x)
axis.set_xticklabels([item.name for item in scenarios], rotation=25, ha="right")
axis.grid(axis="y", alpha=0.25)
axis.legend(ncol=2)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _maneuver_trace_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
reference = trace_set[DopplerPath.FIXED_IIR]
figure, axes = plt.subplots(3, 1, figsize=(13, 8.5), sharex=True)
for doppler_path in PATH_ORDER:
trace = trace_set[doppler_path]
error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
axes[0].plot(trace.time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
axes[1].plot(
reference.time_s,
np.linalg.norm(reference.acceleration_mps2, axis=1),
color="black",
linewidth=1.2,
label="True |acceleration|",
)
adaptive = trace_set[DopplerPath.ADAPTIVE_IIR]
axes[2].step(
adaptive.wls_time_s,
adaptive.adaptive_alpha,
where="post",
color="tab:green",
)
axes[0].set_ylabel("3D velocity error (m/s)")
axes[1].set_ylabel("Acceleration (m/s²)")
axes[2].set_ylabel("Adaptive alpha")
axes[2].set_xlabel("Time (s)")
axes[0].set_ylim(bottom=0.0)
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _outlier_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
for doppler_path in PATH_ORDER:
trace = trace_set[doppler_path]
error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
axes[0].plot(trace.wls_time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
axes[1].plot(trace.wls_time_s, trace.wls_nis, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[1].axhline(VELOCITY_NIS_GATE, color="black", linestyle="--", linewidth=1.0)
for start, stop in ((30.0, 31.0), (56.0, 57.0), (72.0, 72.6)):
for axis in axes:
axis.axvspan(start, stop, color="tab:red", alpha=0.10)
axes[0].set_ylabel("WLS velocity error (m/s)")
axes[1].set_ylabel("Velocity NIS")
axes[1].set_xlabel("Time (s)")
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _filtering_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
for doppler_path in (DopplerPath.FIXED_IIR, DopplerPath.RAW, DopplerPath.ADAPTIVE_IIR):
trace = trace_set[doppler_path]
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
fused_error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
axes[0].plot(trace.wls_time_s, wls_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[1].plot(trace.time_s, fused_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[0].set_ylabel("WLS 3D error (m/s)")
axes[1].set_ylabel("ESKF 3D error (m/s)")
axes[1].set_xlabel("Time (s)")
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _lookup(
summaries: list[Summary], scenario: str, doppler_path: DopplerPath
) -> Summary:
return next(
item
for item in summaries
if item.scenario == scenario and item.path == doppler_path
)
def write_report(path: Path, summaries: list[Summary], seed_count: int) -> None:
lines = [
"# Doppler WLS -> ESKF model sonuçları",
"",
f"Monte Carlo seed sayısı: `{seed_count}`; ilk 5 saniye RMS metriklerinden çıkarıldı.",
"50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.",
"",
"## Sonuç tablosu",
"",
"| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |",
"|---|---|---:|---:|---:|---:|---:|---:|",
]
for scenario in flight_scenarios():
for doppler_path in PATH_ORDER:
item = _lookup(summaries, scenario.name, doppler_path)
dynamic = "-" if not np.isfinite(item.dynamic_rms_mps) else f"{item.dynamic_rms_mps:.3f}"
lines.append(
f"| {scenario.name} | {PATH_LABEL[doppler_path]} | "
f"{item.fused_rms_mps:.3f} | {dynamic} | {item.wls_rms_mps:.3f} | "
f"{item.velocity_accept_percent:.1f} | {item.fused_95_coverage_percent:.1f} | "
f"{item.excluded_epoch_percent:.1f} |"
)
steady_fixed = _lookup(summaries, "steady_cruise", DopplerPath.FIXED_IIR)
steady_raw = _lookup(summaries, "steady_cruise", DopplerPath.RAW)
air_fixed = _lookup(summaries, "airliner_maneuver", DopplerPath.FIXED_IIR)
air_raw = _lookup(summaries, "airliner_maneuver", DopplerPath.RAW)
weak_fixed = _lookup(summaries, "low_cn0_airliner", DopplerPath.FIXED_IIR)
weak_raw = _lookup(summaries, "low_cn0_airliner", DopplerPath.RAW)
weak_adaptive = _lookup(summaries, "low_cn0_airliner", DopplerPath.ADAPTIVE_IIR)
outlier_raw = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW)
outlier_robust = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW_ROBUST)
clock_raw = _lookup(summaries, "clock_drift_steps", DopplerPath.RAW)
lines.extend(
[
"",
"## Bulgular",
"",
f"- Sabit hızda pre-IIR ESKF RMS'i `{steady_fixed.fused_rms_mps:.3f}` m/s, ham yol `{steady_raw.fused_rms_mps:.3f}` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `{steady_raw.wls_rms_mps:.3f}` m/s WLS RMS'ten bu seviyeye indiriyor.",
f"- Airliner manevrasında sabit IIR `{air_fixed.fused_rms_mps:.3f}` m/s ve `%{air_fixed.nis_reject_percent:.1f}` NIS ret oranı verirken ham yol `{air_raw.fused_rms_mps:.3f}` m/s ve `%{air_raw.nis_reject_percent:.1f}` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.",
f"- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `{weak_fixed.fused_rms_mps:.3f}`, `{weak_raw.fused_rms_mps:.3f}`, `{weak_adaptive.fused_rms_mps:.3f}` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.",
f"- Tek uydu burst'ünde raw WLS RMS `{outlier_raw.wls_rms_mps:.3f}` m/s, robust leave-one-out ile `{outlier_robust.wls_rms_mps:.3f}` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.",
f"- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `{clock_raw.clock_drift_rms_mps:.3f}` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.",
"",
"## Mimari kararı",
"",
"- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.",
"- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.",
"- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.",
"- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.",
"",
"## Model sınırları",
"",
"- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.",
"- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.",
"- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.",
"- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.",
"- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.",
"- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seeds", type=int, default=20)
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent / "output",
)
arguments = parser.parse_args()
arguments.output.mkdir(parents=True, exist_ok=True)
scenarios = flight_scenarios()
tasks = [
(scenario_index, seed)
for scenario_index in range(len(scenarios))
for seed in range(1, arguments.seeds + 1)
]
grouped: dict[tuple[int, DopplerPath], list[TrialMetrics]] = {
(scenario_index, doppler_path): []
for scenario_index in range(len(scenarios))
for doppler_path in PATH_ORDER
}
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
for scenario_index, seed, metrics in executor.map(_worker, tasks):
for item in metrics:
grouped[(scenario_index, item.path)].append(item)
print(f"completed {scenarios[scenario_index].name} seed={seed}", flush=True)
summaries = [
summarize(grouped[(scenario_index, doppler_path)])
for scenario_index in range(len(scenarios))
for doppler_path in PATH_ORDER
]
write_csv(arguments.output / "doppler_eskf_summary.csv", summaries)
write_report(
arguments.output / "DOPPLER_ESKF_ANALYSIS.md",
summaries,
arguments.seeds,
)
_summary_plot(arguments.output / "05_doppler_eskf_velocity_rms.png", summaries)
airliner = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "airliner_maneuver"), 1
)
_maneuver_trace_plot(arguments.output / "06_airliner_maneuver_trace.png", airliner)
outlier = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "single_satellite_outliers"), 1
)
_outlier_plot(arguments.output / "07_outlier_wls_nis.png", outlier)
weak = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "low_cn0_airliner"), 1
)
_filtering_plot(arguments.output / "08_low_cn0_wls_vs_eskf.png", weak)
print(f"wrote Doppler/ESKF analysis to {arguments.output}", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,425 @@
"""Run the complete DLL-quality/WLS and code-smoothing comparison."""
from __future__ import annotations
import argparse
import csv
import math
from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from .code_smoothing_model import run_smoothing_trial
from .firmware_wls_model import SigmaMethod
from .measurement_scenarios import EpochSample, run_scenario, scenario_matrix
METHOD_ORDER = (
SigmaMethod.CURRENT,
SigmaMethod.NAIVE_ADD_DLL,
SigmaMethod.DLL_EXCESS,
SigmaMethod.DLL_EXCESS_GUARD,
SigmaMethod.ORACLE,
)
METHOD_LABEL = {
SigmaMethod.CURRENT: "Mevcut C/N0",
SigmaMethod.NAIVE_ADD_DLL: "Naif DLL ekle",
SigmaMethod.DLL_EXCESS: "DLL excess",
SigmaMethod.DLL_EXCESS_GUARD: "DLL excess+mean",
SigmaMethod.ORACLE: "Oracle variance",
}
@dataclass(frozen=True)
class Summary:
scenario: str
method: SigmaMethod
epochs: int
horizontal_rms_m: float
position_3d_rms_m: float
position_3d_p95_m: float
valid_percent: float
valid_3d_rms_m: float
horizontal_2sigma_coverage_percent: float
false_valid_over_25m_percent: float
fde_percent: float
mean_measurement_sigma_m: float
def _worker(task: tuple[int, int]) -> tuple[int, int, list[EpochSample]]:
scenario_index, seed = task
scenario = scenario_matrix()[scenario_index]
return scenario_index, seed, run_scenario(scenario, seed, METHOD_ORDER)
def _rms(values: np.ndarray) -> float:
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
def summarize(scenario_name: str, method: SigmaMethod, samples: list[EpochSample]) -> Summary:
selected = [
item
for item in samples
if item.method == method and item.time_s >= 5.0
]
horizontal = np.asarray([item.horizontal_error_m for item in selected])
error_3d = np.asarray([item.position_error_3d_m for item in selected])
valid = np.asarray([item.valid for item in selected], dtype=bool)
horizontal_sigma = np.asarray([item.horizontal_sigma_m for item in selected])
fde = np.asarray([item.fde_applied for item in selected], dtype=bool)
mean_sigma = np.asarray([item.mean_measurement_sigma_m for item in selected])
false_valid = valid & (error_3d > 25.0)
return Summary(
scenario=scenario_name,
method=method,
epochs=len(selected),
horizontal_rms_m=_rms(horizontal),
position_3d_rms_m=_rms(error_3d),
position_3d_p95_m=float(np.percentile(error_3d, 95.0)),
valid_percent=100.0 * float(np.mean(valid)),
valid_3d_rms_m=_rms(error_3d[valid]),
horizontal_2sigma_coverage_percent=100.0
* float(np.mean(horizontal <= 2.0 * horizontal_sigma)),
false_valid_over_25m_percent=100.0 * float(np.mean(false_valid)),
fde_percent=100.0 * float(np.mean(fde)),
mean_measurement_sigma_m=float(np.mean(mean_sigma)),
)
def write_csv(path: Path, summaries: list[Summary]) -> None:
with path.open("w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
writer.writeheader()
for item in summaries:
row = dict(item.__dict__)
row["method"] = item.method.value
writer.writerow(row)
def _comparison_plot(
path: Path,
summaries: list[Summary],
field: str,
ylabel: str,
*,
logarithmic: bool = False,
) -> None:
scenarios = scenario_matrix()
x = np.arange(len(scenarios))
width = 0.16
figure, axis = plt.subplots(figsize=(15, 6.5))
for method_index, method in enumerate(METHOD_ORDER):
values = [
getattr(
next(
item
for item in summaries
if item.scenario == scenario.name and item.method == method
),
field,
)
for scenario in scenarios
]
axis.bar(
x + (method_index - 2) * width,
values,
width,
label=METHOD_LABEL[method],
)
axis.set_ylabel(ylabel)
if logarithmic:
axis.set_yscale("log")
axis.set_xticks(x)
axis.set_xticklabels([item.name for item in scenarios], rotation=28, ha="right")
axis.grid(axis="y", alpha=0.25)
axis.legend(ncol=3)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _transition_plot(path: Path, samples: list[EpochSample]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(12, 7), sharex=True)
for method in (
SigmaMethod.CURRENT,
SigmaMethod.DLL_EXCESS,
SigmaMethod.DLL_EXCESS_GUARD,
):
selected = [item for item in samples if item.method == method]
axes[0].plot(
[item.time_s for item in selected],
[item.position_error_3d_m for item in selected],
label=METHOD_LABEL[method],
linewidth=1.2,
)
reference = [item for item in samples if item.method == SigmaMethod.CURRENT]
axes[1].plot(
[item.time_s for item in reference],
[item.maximum_dll_ratio for item in reference],
color="tab:red",
)
axes[0].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
axes[1].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
axes[0].set_ylabel("3D konum hatası (m)")
axes[1].set_ylabel("Maks. DLL variance ratio")
axes[1].set_xlabel("Zaman (s)")
axes[0].legend()
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def smoothing_summary(seed_count: int) -> dict[str, dict[str, float]]:
result: dict[str, dict[str, float]] = {}
for label, cfo_mps in (
("cfo_compensated", 0.0),
("cfo_residual_0p5_mps", 0.5),
("cfo_residual_3_mps", 3.0),
):
trials = [
run_smoothing_trial(seed, doppler_cfo_mps=cfo_mps)
for seed in range(1, seed_count + 1)
]
result[label] = {
field: float(np.mean([getattr(item, field) for item in trials]))
for field in trials[0].__dataclass_fields__
}
return result
def _smoothing_plot(path: Path, smoothing: dict[str, dict[str, float]]) -> None:
labels = list(smoothing)
x = np.arange(len(labels))
width = 0.20
figure, axis = plt.subplots(figsize=(10, 5.5))
for offset, (field, title) in enumerate(
(
("raw_rmse_m", "Ham code"),
("scalar_iir_rmse_m", "Kör IIR"),
("doppler_smoothed_rmse_m", "Raw Doppler-predicted"),
(
"firmware_iir_doppler_smoothed_rmse_m",
"FW-IIR Doppler-predicted",
),
)
):
axis.bar(
x + (offset - 1.5) * width,
[smoothing[label][field] for label in labels],
width,
label=title,
)
axis.set_xticks(x)
axis.set_xticklabels(labels)
axis.set_ylabel("Range RMSE (m)")
axis.set_yscale("log")
axis.grid(axis="y", alpha=0.25)
axis.legend()
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def write_report(
path: Path,
summaries: list[Summary],
focused_rms: dict[str, dict[SigmaMethod, float]],
smoothing: dict[str, dict[str, float]],
seed_count: int,
) -> None:
current = {
item.scenario: item
for item in summaries
if item.method == SigmaMethod.CURRENT
}
lines = [
"# GPS code measurement model sonuçları",
"",
f"Monte Carlo seed sayısı: `{seed_count}`. İlk 5 saniye metriklere alınmadı.",
"",
"## Yöntem karşılaştırması",
"",
"| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |",
"|---|---|---:|---:|---:|---:|---:|",
]
for scenario in scenario_matrix():
baseline = current[scenario.name].position_3d_rms_m
for method in METHOD_ORDER:
item = next(
value
for value in summaries
if value.scenario == scenario.name and value.method == method
)
improvement = 100.0 * (baseline - item.position_3d_rms_m) / baseline
lines.append(
f"| {scenario.name} | {METHOD_LABEL[method]} | "
f"{item.position_3d_rms_m:.2f} | {improvement:+.1f}% | "
f"{item.position_3d_p95_m:.2f} | {item.valid_percent:.1f} | "
f"{item.false_valid_over_25m_percent:.2f} |"
)
lines.extend(
[
"",
"## Modelden çıkan karar özeti",
"",
"- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.",
"- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.",
"- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.",
"- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.",
"- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.",
"- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.",
"",
"## Bozulma penceresi odaklı 3D RMS",
"",
"| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |",
"|---|---:|---:|---:|---:|",
]
)
for label, values in focused_rms.items():
lines.append(
f"| {label} | {values[SigmaMethod.CURRENT]:.2f} | "
f"{values[SigmaMethod.DLL_EXCESS]:.2f} | "
f"{values[SigmaMethod.DLL_EXCESS_GUARD]:.2f} | "
f"{values[SigmaMethod.ORACLE]:.2f} |"
)
lines.extend(
[
"",
"## Code smoothing ve carrier CFO",
"",
"| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |",
"|---|---:|---:|---:|---:|---:|---:|---:|",
]
)
for label, values in smoothing.items():
lines.append(
f"| {label} | {values['raw_rmse_m']:.2f} | "
f"{values['scalar_iir_rmse_m']:.2f} | "
f"{values['doppler_smoothed_rmse_m']:.2f} | "
f"{values['firmware_iir_doppler_smoothed_rmse_m']:.2f} | "
f"{values['scalar_iir_peak_dynamic_error_m']:.2f} | "
f"{values['doppler_peak_dynamic_error_m']:.2f} | "
f"{values['firmware_iir_doppler_peak_dynamic_error_m']:.2f} |"
)
lines.extend(
[
"",
"## Yorum sınırları",
"",
"- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.",
"- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.",
"- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.",
"- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.",
"- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.",
"- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.",
"- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.",
"- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seeds", type=int, default=12)
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent / "output",
)
arguments = parser.parse_args()
arguments.output.mkdir(parents=True, exist_ok=True)
scenarios = scenario_matrix()
tasks = [
(scenario_index, seed)
for scenario_index in range(len(scenarios))
for seed in range(1, arguments.seeds + 1)
]
grouped: dict[int, list[EpochSample]] = {
index: [] for index in range(len(scenarios))
}
transition_samples: list[EpochSample] = []
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
for scenario_index, seed, samples in executor.map(_worker, tasks):
grouped[scenario_index].extend(samples)
if scenarios[scenario_index].name == "noise_burst_recovery" and seed == 1:
transition_samples = samples
print(
f"completed {scenarios[scenario_index].name} seed={seed}",
flush=True,
)
summaries = [
summarize(scenario.name, method, grouped[index])
for index, scenario in enumerate(scenarios)
for method in METHOD_ORDER
]
focused_windows = {
"noise_burst_recovery [22,45) s": ("noise_burst_recovery", 22.0, 45.0),
"dll_transient_stress [25,34) s": ("dll_transient_stress", 25.0, 34.0),
"cn0_step_recovery [5,30) s": ("cn0_step_recovery", 5.0, 30.0),
}
focused_rms: dict[str, dict[SigmaMethod, float]] = {}
for label, (scenario_name, start_s, stop_s) in focused_windows.items():
scenario_index = next(
index
for index, scenario in enumerate(scenarios)
if scenario.name == scenario_name
)
focused_rms[label] = {}
for method in METHOD_ORDER:
values = np.asarray(
[
item.position_error_3d_m
for item in grouped[scenario_index]
if item.method == method
and start_s <= item.time_s < stop_s
]
)
focused_rms[label][method] = _rms(values)
smoothing = smoothing_summary(max(arguments.seeds, 20))
write_csv(arguments.output / "scenario_summary.csv", summaries)
write_report(
arguments.output / "ANALYSIS.md",
summaries,
focused_rms,
smoothing,
arguments.seeds,
)
_comparison_plot(
arguments.output / "01_position_3d_rms.png",
summaries,
"position_3d_rms_m",
"3D RMS (m)",
logarithmic=True,
)
_comparison_plot(
arguments.output / "02_solution_availability.png",
summaries,
"valid_percent",
"Integrity-valid çözüm (%)",
)
_transition_plot(
arguments.output / "03_noise_burst_transition.png",
transition_samples,
)
_smoothing_plot(arguments.output / "04_code_smoothing_cfo.png", smoothing)
print(f"wrote analysis to {arguments.output}", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,132 @@
"""One-dimensional code smoothing study with carrier/Doppler aiding."""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class SmoothingResult:
raw_rmse_m: float
scalar_iir_rmse_m: float
doppler_smoothed_rmse_m: float
firmware_iir_doppler_smoothed_rmse_m: float
scalar_iir_peak_dynamic_error_m: float
doppler_peak_dynamic_error_m: float
firmware_iir_doppler_peak_dynamic_error_m: float
def run_smoothing_trial(
seed: int,
*,
duration_s: float = 80.0,
tracking_dt_s: float = 0.020,
wls_dt_s: float = 0.200,
code_sigma_m: float = 4.0,
doppler_sigma_mps: float = 0.25,
doppler_cfo_mps: float = 0.0,
alpha: float = 0.10,
firmware_doppler_alpha: float = 0.05,
) -> SmoothingResult:
"""Compare raw code, blind IIR and Doppler-propagated code smoothing.
The truth includes a range-rate step so a visually smooth but lagging
scalar IIR is penalized. ``doppler_cfo_mps`` represents residual receiver
carrier frequency offset after the nominal IF subtraction.
"""
if abs(round(wls_dt_s / tracking_dt_s) - wls_dt_s / tracking_dt_s) > 1.0e-12:
raise ValueError("WLS period must be an integer number of tracking periods")
rng = np.random.default_rng(seed)
tracking_time_s = np.arange(0.0, duration_s, tracking_dt_s)
true_rate_tracking = np.where(
tracking_time_s < 20.0,
-120.0,
np.where(tracking_time_s < 45.0, 230.0, -40.0),
)
true_range_tracking = np.empty_like(tracking_time_s)
true_range_tracking[0] = 21_000_000.0
true_range_tracking[1:] = (
true_range_tracking[0]
+ np.cumsum(true_rate_tracking[:-1] * tracking_dt_s)
)
measured_rate_tracking = (
true_rate_tracking
+ doppler_cfo_mps
+ rng.normal(0.0, doppler_sigma_mps, len(tracking_time_s))
)
filtered_rate_tracking = np.empty_like(measured_rate_tracking)
filtered_rate_tracking[0] = measured_rate_tracking[0]
for index in range(1, len(filtered_rate_tracking)):
filtered_rate_tracking[index] = (
firmware_doppler_alpha * measured_rate_tracking[index]
+ (1.0 - firmware_doppler_alpha) * filtered_rate_tracking[index - 1]
)
tracking_per_wls = int(round(wls_dt_s / tracking_dt_s))
wls_indices = np.arange(0, len(tracking_time_s), tracking_per_wls)
time_s = tracking_time_s[wls_indices]
true_range = true_range_tracking[wls_indices]
raw_code = true_range + rng.normal(0.0, code_sigma_m, len(time_s))
scalar = np.empty_like(raw_code)
doppler_smoothed = np.empty_like(raw_code)
firmware_iir_doppler_smoothed = np.empty_like(raw_code)
scalar[0] = raw_code[0]
doppler_smoothed[0] = raw_code[0]
firmware_iir_doppler_smoothed[0] = raw_code[0]
for index in range(1, len(time_s)):
scalar[index] = alpha * raw_code[index] + (1.0 - alpha) * scalar[index - 1]
start = wls_indices[index - 1]
stop = wls_indices[index]
raw_range_delta = (
float(np.sum(measured_rate_tracking[start:stop]))
* tracking_dt_s
)
filtered_range_delta = (
float(np.sum(filtered_rate_tracking[start:stop]))
* tracking_dt_s
)
prediction = doppler_smoothed[index - 1] + raw_range_delta
doppler_smoothed[index] = (
prediction + alpha * (raw_code[index] - prediction)
)
filtered_prediction = (
firmware_iir_doppler_smoothed[index - 1]
+ filtered_range_delta
)
firmware_iir_doppler_smoothed[index] = (
filtered_prediction
+ alpha * (raw_code[index] - filtered_prediction)
)
warm = time_s >= 5.0
dynamic = ((time_s >= 20.0) & (time_s < 25.0)) | (
(time_s >= 45.0) & (time_s < 50.0)
)
rmse = lambda error, mask: float(np.sqrt(np.mean(np.square(error[mask]))))
return SmoothingResult(
raw_rmse_m=rmse(raw_code - true_range, warm),
scalar_iir_rmse_m=rmse(scalar - true_range, warm),
doppler_smoothed_rmse_m=rmse(doppler_smoothed - true_range, warm),
firmware_iir_doppler_smoothed_rmse_m=rmse(
firmware_iir_doppler_smoothed - true_range,
warm,
),
scalar_iir_peak_dynamic_error_m=float(
np.max(np.abs(scalar[dynamic] - true_range[dynamic]))
),
doppler_peak_dynamic_error_m=float(
np.max(np.abs(doppler_smoothed[dynamic] - true_range[dynamic]))
),
firmware_iir_doppler_peak_dynamic_error_m=float(
np.max(
np.abs(
firmware_iir_doppler_smoothed[dynamic]
- true_range[dynamic]
)
)
),
)
@@ -0,0 +1,657 @@
"""Project-specific Doppler WLS to ESKF timing/noise model.
The model compares four paths using one shared 50 Hz tracking stream and one
shared 100 Hz IMU stream:
* the legacy fixed-alpha Doppler IIR before 5 Hz velocity WLS;
* raw Doppler into WLS, as a nominal-model comparison rather than production;
* a global IMU-driven adaptive-alpha IIR;
* raw Doppler with the per-satellite robust WLS gate used before the ESKF.
The ESKF is reduced to the velocity and accelerometer-bias error states that
matter to this experiment. Its process noise, 3-D measurement covariance and
99.9-percent velocity NIS gate mirror the project firmware. Attitude and
position are intentionally outside this model's scope.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable
import numpy as np
IMU_HZ = 100.0
TRACKING_HZ = 50.0
VELOCITY_WLS_HZ = 5.0
IMU_DT_S = 1.0 / IMU_HZ
TRACKING_STEPS = int(round(IMU_HZ / TRACKING_HZ))
WLS_STEPS = int(round(IMU_HZ / VELOCITY_WLS_HZ))
FIXED_DOPPLER_ALPHA = 0.05
CN0_ALPHA = 0.20
VELOCITY_NIS_GATE = 16.26623619623813
PROCESS_ACCEL_NOISE_DENSITY = 0.025
ACCEL_BIAS_RANDOM_WALK = 0.0015
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS = 2.0
MINIMUM_VERTICAL_VELOCITY_STD_MPS = 3.0
class DopplerPath(str, Enum):
FIXED_IIR = "fixed_iir_wls_eskf"
RAW = "raw_wls_eskf"
ADAPTIVE_IIR = "adaptive_iir_wls_eskf"
RAW_ROBUST = "raw_robust_wls_eskf"
PATH_ORDER = (
DopplerPath.FIXED_IIR,
DopplerPath.RAW,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW_ROBUST,
)
@dataclass(frozen=True)
class FlightScenario:
name: str
description: str
duration_s: float
initial_velocity_mps: np.ndarray
acceleration: Callable[[float], np.ndarray]
cn0_offset: Callable[[float], float]
noise_multiplier: Callable[[float, int], float]
outlier_mps: Callable[[float, int], float]
receiver_clock_drift_mps: Callable[[float], float]
gps_gaps: tuple[tuple[float, float], ...] = ()
@dataclass(frozen=True)
class VelocityWlsResult:
velocity_mps: np.ndarray
clock_drift_mps: float
covariance_mps2: np.ndarray
residual_mps: np.ndarray
residual_rms_mps: float
covariance_scale: float
excluded_satellite: int | None
@dataclass
class AdaptiveAlpha:
alpha: float = FIXED_DOPPLER_ALPHA
hold_samples: int = 0
filtered_acceleration_mps2: np.ndarray = field(
default_factory=lambda: np.zeros(3, dtype=float)
)
def update(
self,
measured_acceleration_mps2: np.ndarray,
median_cn0_dbhz: float,
) -> float:
# A raw 100 Hz accelerometer sample is too noisy for coefficient
# switching. This 50 Hz vector EWMA has an approximately 0.16 s time
# constant and, unlike smoothing |a|, does not retain the positive
# magnitude bias of zero-mean sensor noise.
motion_alpha = 0.12
self.filtered_acceleration_mps2 += motion_alpha * (
measured_acceleration_mps2 - self.filtered_acceleration_mps2
)
magnitude = float(np.linalg.norm(self.filtered_acceleration_mps2))
if median_cn0_dbhz >= 46.0:
quiet_alpha = 0.08
elif median_cn0_dbhz >= 37.0:
quiet_alpha = FIXED_DOPPLER_ALPHA
else:
quiet_alpha = 0.03
if magnitude >= 2.5:
self.alpha = 0.40
self.hold_samples = 25
elif magnitude >= 0.55:
self.alpha = 0.20
self.hold_samples = 25
elif self.hold_samples > 0:
self.hold_samples -= 1
else:
self.alpha = quiet_alpha
return self.alpha
@dataclass
class VelocityBiasEskf:
velocity_mps: np.ndarray
accel_bias_mps2: np.ndarray
covariance: np.ndarray
accepted: int = 0
rejected_nis: int = 0
last_nis: float = float("nan")
@classmethod
def initialize(
cls,
initial_velocity_mps: np.ndarray,
initial_velocity_error_mps: np.ndarray,
) -> "VelocityBiasEskf":
covariance = np.zeros((6, 6), dtype=float)
covariance[:3, :3] = np.eye(3)
covariance[3:, 3:] = np.eye(3) * 0.1**2
return cls(
velocity_mps=initial_velocity_mps + initial_velocity_error_mps,
accel_bias_mps2=np.zeros(3),
covariance=covariance,
)
def propagate(self, measured_acceleration_mps2: np.ndarray, dt_s: float) -> None:
self.velocity_mps += (
measured_acceleration_mps2 - self.accel_bias_mps2
) * dt_s
transition = np.eye(6)
transition[:3, 3:] = -np.eye(3) * dt_s
accel_variance = PROCESS_ACCEL_NOISE_DENSITY**2
bias_variance = ACCEL_BIAS_RANDOM_WALK**2
process = np.zeros((6, 6), dtype=float)
# Match eskf.c's first-order discrete injection: velocity and bias
# driving noise are added after F P F^T, without exact-discretization
# dt^2/dt^3 cross terms.
process[:3, :3] = np.eye(3) * accel_variance * dt_s
process[3:, 3:] = np.eye(3) * bias_variance * dt_s
self.covariance = (
transition @ self.covariance @ transition.T + process
)
self.covariance = 0.5 * (self.covariance + self.covariance.T)
def update_velocity(
self,
measurement_mps: np.ndarray,
covariance_mps2: np.ndarray,
) -> bool:
innovation = measurement_mps - self.velocity_mps
innovation_covariance = self.covariance[:3, :3] + covariance_mps2
inverse = np.linalg.inv(innovation_covariance)
nis = float(innovation @ inverse @ innovation)
self.last_nis = nis
if not np.isfinite(nis) or nis > VELOCITY_NIS_GATE:
self.rejected_nis += 1
return False
measurement_matrix = np.zeros((3, 6), dtype=float)
measurement_matrix[:, :3] = np.eye(3)
gain = self.covariance @ measurement_matrix.T @ inverse
correction = gain @ innovation
self.velocity_mps += correction[:3]
self.accel_bias_mps2 += correction[3:]
identity = np.eye(6)
residual_transform = identity - gain @ measurement_matrix
self.covariance = (
residual_transform
@ self.covariance
@ residual_transform.T
+ gain @ covariance_mps2 @ gain.T
)
self.covariance = 0.5 * (self.covariance + self.covariance.T)
self.accepted += 1
return True
@dataclass(frozen=True)
class DopplerEskfTrace:
scenario: str
path: DopplerPath
time_s: np.ndarray
truth_velocity_mps: np.ndarray
fused_velocity_mps: np.ndarray
fused_velocity_covariance_mps2: np.ndarray
acceleration_mps2: np.ndarray
wls_time_s: np.ndarray
wls_velocity_mps: np.ndarray
wls_clock_drift_mps: np.ndarray
wls_covariance_mps2: np.ndarray
wls_covariance_scale: np.ndarray
wls_nis: np.ndarray
wls_accepted: np.ndarray
adaptive_alpha: np.ndarray
excluded_satellites: np.ndarray
def _zero_acceleration(_time_s: float) -> np.ndarray:
return np.zeros(3)
def _airliner_acceleration(time_s: float) -> np.ndarray:
if 15.0 <= time_s < 25.0:
return np.asarray([2.0, 0.0, 0.0])
if 38.0 <= time_s < 52.0:
return np.asarray([0.0, 1.5, 0.25])
if 68.0 <= time_s < 76.0:
return np.asarray([-2.5, 0.0, -0.15])
return np.zeros(3)
def _aggressive_acceleration(time_s: float) -> np.ndarray:
if 10.0 <= time_s < 16.0:
return np.asarray([6.0, 0.0, 1.0])
if 26.0 <= time_s < 33.0:
return np.asarray([0.0, 7.0, 0.0])
if 46.0 <= time_s < 52.0:
return np.asarray([-5.0, -4.0, -1.0])
if 64.0 <= time_s < 76.0:
phase = 2.0 * np.pi * (time_s - 64.0) / 3.0
return np.asarray([3.0 * np.sin(phase), 4.0 * np.cos(phase), 0.5 * np.sin(phase)])
return np.zeros(3)
def _constant_zero(_time_s: float) -> float:
return 0.0
def _constant_one(_time_s: float, _satellite: int) -> float:
return 1.0
def _no_outlier(_time_s: float, _satellite: int) -> float:
return 0.0
def flight_scenarios() -> tuple[FlightScenario, ...]:
return (
FlightScenario(
"steady_cruise",
"Constant aircraft velocity with nominal open-sky Doppler noise.",
90.0,
np.asarray([180.0, 20.0, 0.0]),
_zero_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"airliner_maneuver",
"Moderate longitudinal, turn and climb-rate accelerations.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"aggressive_maneuver",
"High acceleration and oscillatory manoeuvre segments.",
90.0,
np.asarray([80.0, 0.0, 0.0]),
_aggressive_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"low_cn0_airliner",
"Airliner manoeuvre with a 10 dB signal-quality loss.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
lambda _time_s: -10.0,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"single_satellite_outliers",
"One channel has repeatable 12 m/s Doppler bursts.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
lambda time_s, satellite: (
12.0
if satellite == 2
and (
30.0 <= time_s < 31.0
or 56.0 <= time_s < 57.0
or 72.0 <= time_s < 72.6
)
else 0.0
),
_constant_zero,
),
FlightScenario(
"clock_drift_steps",
"Common receiver clock-frequency steps coexist with manoeuvres.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
lambda time_s: (
18.0 if 28.0 <= time_s < 55.0 else (-9.0 if time_s >= 55.0 else 0.0)
),
),
FlightScenario(
"gps_gap_recovery",
"Fifteen-second GNSS gap during an aircraft manoeuvre.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
gps_gaps=((42.0, 57.0),),
),
FlightScenario(
"mixed_flight_faults",
"Aggressive motion, temporary weak signal, channel bursts and clock step.",
90.0,
np.asarray([80.0, 0.0, 0.0]),
_aggressive_acceleration,
lambda time_s: -9.0 if 34.0 <= time_s < 60.0 else 0.0,
lambda time_s, satellite: (
2.5 if 34.0 <= time_s < 60.0 and satellite in (1, 6) else 1.0
),
lambda time_s, satellite: (
-10.0 if satellite == 4 and 54.0 <= time_s < 55.0 else 0.0
),
lambda time_s: 12.0 if time_s >= 48.0 else 0.0,
gps_gaps=((78.0, 83.0),),
),
)
def local_los_matrix() -> np.ndarray:
azimuth_deg = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
elevation_deg = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
azimuth = np.deg2rad(azimuth_deg)
elevation = np.deg2rad(elevation_deg)
return np.column_stack(
[
np.cos(elevation) * np.sin(azimuth),
np.cos(elevation) * np.cos(azimuth),
np.sin(elevation),
]
)
def velocity_sigma_mps(cn0_dbhz: np.ndarray) -> np.ndarray:
return np.clip(30.0 * np.power(10.0, -cn0_dbhz / 20.0), 0.05, 5.0)
def _solve_velocity_candidate(
design: np.ndarray,
observations_mps: np.ndarray,
sigma_mps: np.ndarray,
indices: np.ndarray,
) -> VelocityWlsResult:
selected_design = design[indices]
selected_observations = observations_mps[indices]
selected_sigma = sigma_mps[indices]
weights = 1.0 / np.square(selected_sigma)
normal = selected_design.T @ (weights[:, None] * selected_design)
inverse = np.linalg.inv(normal)
solution = inverse @ (selected_design.T @ (weights * selected_observations))
residual = selected_observations - selected_design @ solution
weighted_residual_square_sum = float(np.sum(weights * np.square(residual)))
degrees_of_freedom = len(indices) - 4
covariance_scale = (
max(1.0, weighted_residual_square_sum / degrees_of_freedom)
if degrees_of_freedom > 0
else 1.0
)
return VelocityWlsResult(
velocity_mps=solution[:3].copy(),
clock_drift_mps=float(solution[3]),
covariance_mps2=covariance_scale * inverse[:3, :3],
residual_mps=residual,
residual_rms_mps=float(np.sqrt(np.mean(np.square(residual)))),
covariance_scale=covariance_scale,
excluded_satellite=None,
)
def velocity_wls(
observations_mps: np.ndarray,
cn0_dbhz: np.ndarray,
*,
robust: bool = False,
) -> VelocityWlsResult:
los = local_los_matrix()
design = np.column_stack([-los, np.ones(len(los))])
sigma = velocity_sigma_mps(cn0_dbhz)
indices = np.arange(len(observations_mps), dtype=int)
full = _solve_velocity_candidate(design, observations_mps, sigma, indices)
if not robust or len(indices) < 6:
return full
normalized = np.abs(full.residual_mps) / sigma
if float(np.max(normalized)) <= 3.5:
return full
# The largest full-fit residual is not necessarily the bad satellite:
# WLS leverage can distribute one channel's error over the other rows.
# Compare every leave-one-out hypothesis and use the one with the lowest
# normalized post-fit residual energy. The firmware robust velocity WLS
# uses the same trigger and candidate selection.
candidates: list[tuple[float, int, VelocityWlsResult]] = []
for excluded in indices:
used = indices[indices != excluded]
candidate = _solve_velocity_candidate(design, observations_mps, sigma, used)
score = float(
np.sum(np.square(candidate.residual_mps / sigma[used]))
)
candidates.append((score, int(excluded), candidate))
_score, excluded, candidate = min(candidates, key=lambda item: item[0])
return VelocityWlsResult(
**{
**candidate.__dict__,
"excluded_satellite": excluded,
}
)
def fusion_velocity_covariance(covariance_mps2: np.ndarray) -> np.ndarray:
"""Apply fusion_gps_adapter.c's ENU velocity covariance sanitizer floors."""
output = np.asarray(covariance_mps2, dtype=float).copy()
variance_floor = np.square(
np.asarray(
[
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
MINIMUM_VERTICAL_VELOCITY_STD_MPS,
]
)
)
for axis in range(3):
output[axis, axis] = max(output[axis, axis], variance_floor[axis])
for row in range(3):
for column in range(row + 1, 3):
covariance = 0.5 * (output[row, column] + output[column, row])
limit = 0.95 * np.sqrt(output[row, row] * output[column, column])
covariance = float(np.clip(covariance, -limit, limit))
output[row, column] = covariance
output[column, row] = covariance
return output
def _in_gap(scenario: FlightScenario, time_s: float) -> bool:
return any(start <= time_s < stop for start, stop in scenario.gps_gaps)
def _truth_trajectory(scenario: FlightScenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
time_s = np.arange(0.0, scenario.duration_s, IMU_DT_S)
acceleration = np.asarray([scenario.acceleration(float(time)) for time in time_s])
velocity = np.empty((len(time_s), 3), dtype=float)
velocity[0] = scenario.initial_velocity_mps
velocity[1:] = velocity[0] + np.cumsum(acceleration[:-1] * IMU_DT_S, axis=0)
return time_s, acceleration, velocity
def run_doppler_eskf_scenario(
scenario: FlightScenario,
seed: int,
paths: tuple[DopplerPath, ...] = PATH_ORDER,
) -> dict[DopplerPath, DopplerEskfTrace]:
rng = np.random.default_rng(seed)
time_s, truth_acceleration, truth_velocity = _truth_trajectory(scenario)
satellite_count = len(local_los_matrix())
baseline_cn0 = np.asarray([47.0, 45.0, 43.0, 38.0, 46.0, 42.0, 40.0, 36.0])
filtered_cn0 = baseline_cn0.copy()
fixed_measurement: np.ndarray | None = None
adaptive_measurement: np.ndarray | None = None
latest_raw: np.ndarray | None = None
alpha_selector = AdaptiveAlpha()
latest_alpha = FIXED_DOPPLER_ALPHA
initial_error = rng.normal(0.0, 0.8, 3)
filters = {
path: VelocityBiasEskf.initialize(
scenario.initial_velocity_mps,
initial_error,
)
for path in paths
}
fused_history = {
path: np.empty((len(time_s), 3), dtype=float)
for path in paths
}
fused_covariance_history = {
path: np.empty((len(time_s), 3, 3), dtype=float)
for path in paths
}
wls_times = {path: [] for path in paths}
wls_velocity = {path: [] for path in paths}
wls_clock_drift = {path: [] for path in paths}
wls_covariance = {path: [] for path in paths}
wls_covariance_scale = {path: [] for path in paths}
wls_nis = {path: [] for path in paths}
wls_accepted = {path: [] for path in paths}
wls_alpha = {path: [] for path in paths}
excluded = {path: [] for path in paths}
true_accel_bias = rng.normal(0.0, 0.04, 3)
latest_acceleration_measurement = truth_acceleration[0] + true_accel_bias
los = local_los_matrix()
for sample in range(len(time_s)):
if sample > 0:
true_accel_bias += rng.normal(
0.0,
ACCEL_BIAS_RANDOM_WALK * np.sqrt(IMU_DT_S),
3,
)
imu_noise_std = PROCESS_ACCEL_NOISE_DENSITY / np.sqrt(IMU_DT_S)
latest_acceleration_measurement = (
truth_acceleration[sample - 1]
+ true_accel_bias
+ rng.normal(0.0, imu_noise_std, 3)
)
for path in paths:
filters[path].propagate(latest_acceleration_measurement, IMU_DT_S)
if sample % TRACKING_STEPS == 0:
current_time = float(time_s[sample])
true_cn0 = baseline_cn0 + scenario.cn0_offset(current_time)
raw_cn0 = np.clip(true_cn0 + rng.normal(0.0, 1.2, satellite_count), 10.0, 60.0)
filtered_cn0 = CN0_ALPHA * raw_cn0 + (1.0 - CN0_ALPHA) * filtered_cn0
nominal_sigma = velocity_sigma_mps(true_cn0)
actual_sigma = nominal_sigma * np.asarray(
[
scenario.noise_multiplier(current_time, satellite)
for satellite in range(satellite_count)
]
)
raw = (
-los @ truth_velocity[sample]
+ scenario.receiver_clock_drift_mps(current_time)
+ rng.normal(0.0, actual_sigma)
+ np.asarray(
[
scenario.outlier_mps(current_time, satellite)
for satellite in range(satellite_count)
]
)
)
if fixed_measurement is None:
fixed_measurement = raw.copy()
adaptive_measurement = raw.copy()
else:
fixed_measurement += FIXED_DOPPLER_ALPHA * (raw - fixed_measurement)
latest_alpha = alpha_selector.update(
latest_acceleration_measurement,
float(np.median(filtered_cn0)),
)
adaptive_measurement += latest_alpha * (raw - adaptive_measurement)
latest_raw = raw
if (
sample > 0
and sample % WLS_STEPS == 0
and latest_raw is not None
and not _in_gap(scenario, float(time_s[sample]))
):
for path in paths:
if path == DopplerPath.FIXED_IIR:
observations = fixed_measurement
elif path == DopplerPath.ADAPTIVE_IIR:
observations = adaptive_measurement
else:
observations = latest_raw
result = velocity_wls(
np.asarray(observations),
filtered_cn0,
robust=(path == DopplerPath.RAW_ROBUST),
)
accepted = filters[path].update_velocity(
result.velocity_mps,
fusion_velocity_covariance(result.covariance_mps2),
)
wls_times[path].append(float(time_s[sample]))
wls_velocity[path].append(result.velocity_mps)
wls_clock_drift[path].append(result.clock_drift_mps)
wls_covariance[path].append(result.covariance_mps2)
wls_covariance_scale[path].append(result.covariance_scale)
wls_nis[path].append(filters[path].last_nis)
wls_accepted[path].append(accepted)
wls_alpha[path].append(
latest_alpha
if path == DopplerPath.ADAPTIVE_IIR
else (FIXED_DOPPLER_ALPHA if path == DopplerPath.FIXED_IIR else 1.0)
)
excluded[path].append(
-1 if result.excluded_satellite is None else result.excluded_satellite
)
for path in paths:
fused_history[path][sample] = filters[path].velocity_mps
fused_covariance_history[path][sample] = filters[path].covariance[:3, :3]
return {
path: DopplerEskfTrace(
scenario=scenario.name,
path=path,
time_s=time_s,
truth_velocity_mps=truth_velocity,
fused_velocity_mps=fused_history[path],
fused_velocity_covariance_mps2=fused_covariance_history[path],
acceleration_mps2=truth_acceleration,
wls_time_s=np.asarray(wls_times[path]),
wls_velocity_mps=np.asarray(wls_velocity[path]),
wls_clock_drift_mps=np.asarray(wls_clock_drift[path]),
wls_covariance_mps2=np.asarray(wls_covariance[path]),
wls_covariance_scale=np.asarray(wls_covariance_scale[path]),
wls_nis=np.asarray(wls_nis[path]),
wls_accepted=np.asarray(wls_accepted[path], dtype=bool),
adaptive_alpha=np.asarray(wls_alpha[path]),
excluded_satellites=np.asarray(excluded[path], dtype=int),
)
for path in paths
}
@@ -0,0 +1,353 @@
"""Python mirror of the firmware's code-measurement WLS policy.
The model copies the receiver-specific parts of NavEquations.c:
* four states: ECEF x/y/z and common receiver clock bias in metres;
* URA + elevation + C/N0 variance model;
* normalized weights for the numerical solve and absolute weights for
covariance/integrity;
* the 99-percent chi-square table;
* single-satellite leave-one-out FDE only when at least seven measurements
are available.
Atmospheric and satellite-clock corrections are represented as already
corrected measurement errors. That keeps this study focused on tracking
quality rather than duplicating the existing PPS orbit/atmosphere model.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import numpy as np
URA_SIGMA_M = np.asarray(
[
2.4,
3.4,
4.85,
6.85,
9.65,
13.65,
24.0,
48.0,
96.0,
192.0,
384.0,
768.0,
1536.0,
3072.0,
6144.0,
],
dtype=float,
)
CHI_SQUARE_99 = np.asarray(
[0.0, 6.634896601, 9.210340372, 11.34486673, 13.27670414, 15.08627247],
dtype=float,
)
class SigmaMethod(str, Enum):
CURRENT = "current_cn0"
NAIVE_ADD_DLL = "naive_add_dll"
DLL_EXCESS = "dll_excess"
DLL_EXCESS_GUARD = "dll_excess_guard"
ORACLE = "oracle_variance"
@dataclass(frozen=True)
class SigmaInputs:
ura_index: np.ndarray
elevation_deg: np.ndarray
cn0_dbhz: np.ndarray
dll_variance_ratio: np.ndarray
dll_mean_z: np.ndarray
actual_jitter_multiplier: np.ndarray
@dataclass(frozen=True)
class WlsSolution:
position_ecef_m: np.ndarray
clock_bias_m: float
residual_m: np.ndarray
sigma_m: np.ndarray
covariance: np.ndarray
chi_square: float
chi_square_ratio: float
chi_square_pass: bool
valid: bool
used_indices: np.ndarray
excluded_index: int | None
def cn0_code_sigma_m(cn0_dbhz: np.ndarray | float) -> np.ndarray:
sigma = 3.0 * np.power(10.0, (45.0 - np.asarray(cn0_dbhz)) / 20.0)
return np.clip(sigma, 0.5, 30.0)
def elevation_sigma_m(elevation_deg: np.ndarray | float) -> np.ndarray:
bounded = np.clip(np.asarray(elevation_deg), 10.0, 90.0)
return 3.0 / np.sin(np.deg2rad(bounded))
def firmware_sigma_m(
inputs: SigmaInputs,
method: SigmaMethod,
) -> np.ndarray:
ura = URA_SIGMA_M[np.asarray(inputs.ura_index, dtype=int)]
elevation = elevation_sigma_m(inputs.elevation_deg)
code = cn0_code_sigma_m(inputs.cn0_dbhz)
ratio = np.clip(np.asarray(inputs.dll_variance_ratio), 0.05, 25.0)
if method == SigmaMethod.CURRENT:
code_variance_factor = np.ones_like(code)
elif method == SigmaMethod.NAIVE_ADD_DLL:
# Deliberately included as a negative control: at ratio=1 this counts
# the C/N0-predicted thermal jitter twice.
code_variance_factor = 1.0 + ratio
elif method == SigmaMethod.DLL_EXCESS:
# Only the measured excess over the C/N0 expectation is added.
code_variance_factor = np.maximum(1.0, ratio)
elif method == SigmaMethod.DLL_EXCESS_GUARD:
mean_factor = np.square(
np.maximum(1.0, np.asarray(inputs.dll_mean_z) / 3.0)
)
code_variance_factor = np.maximum.reduce(
[np.ones_like(code), ratio, mean_factor]
)
elif method == SigmaMethod.ORACLE:
code_variance_factor = np.square(
np.maximum(0.05, np.asarray(inputs.actual_jitter_multiplier))
)
else:
raise ValueError(f"unsupported sigma method: {method}")
return np.sqrt(
ura * ura
+ elevation * elevation
+ code * code * code_variance_factor
)
def ecef_to_lla(ecef_m: np.ndarray) -> tuple[float, float, float]:
x, y, z = np.asarray(ecef_m, dtype=float)
a = 6_378_137.0
inverse_flattening = 298.257223563
flattening = 1.0 / inverse_flattening
eccentricity_squared = flattening * (2.0 - flattening)
longitude = np.arctan2(y, x)
horizontal = np.hypot(x, y)
latitude = np.arctan2(
z,
horizontal * (1.0 - eccentricity_squared),
)
height = 0.0
for _ in range(12):
sine = np.sin(latitude)
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
height = horizontal / max(np.cos(latitude), 1.0e-15) - prime_vertical
next_latitude = np.arctan2(
z,
horizontal
* (
1.0
- eccentricity_squared
* prime_vertical
/ max(prime_vertical + height, 1.0)
),
)
if abs(next_latitude - latitude) < 1.0e-14:
latitude = next_latitude
break
latitude = next_latitude
return (
float(np.rad2deg(latitude)),
float(np.rad2deg(longitude)),
float(height),
)
def lla_to_ecef(latitude_deg: float, longitude_deg: float, height_m: float) -> np.ndarray:
a = 6_378_137.0
inverse_flattening = 298.257223563
flattening = 1.0 / inverse_flattening
eccentricity_squared = flattening * (2.0 - flattening)
latitude = np.deg2rad(latitude_deg)
longitude = np.deg2rad(longitude_deg)
sine = np.sin(latitude)
cosine = np.cos(latitude)
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
return np.asarray(
[
(prime_vertical + height_m) * cosine * np.cos(longitude),
(prime_vertical + height_m) * cosine * np.sin(longitude),
(
prime_vertical * (1.0 - eccentricity_squared)
+ height_m
)
* sine,
],
dtype=float,
)
def enu_axes(receiver_ecef_m: np.ndarray) -> np.ndarray:
latitude_deg, longitude_deg, _ = ecef_to_lla(receiver_ecef_m)
latitude = np.deg2rad(latitude_deg)
longitude = np.deg2rad(longitude_deg)
east = np.asarray([-np.sin(longitude), np.cos(longitude), 0.0])
north = np.asarray(
[
-np.sin(latitude) * np.cos(longitude),
-np.sin(latitude) * np.sin(longitude),
np.cos(latitude),
]
)
up = np.asarray(
[
np.cos(latitude) * np.cos(longitude),
np.cos(latitude) * np.sin(longitude),
np.sin(latitude),
]
)
return np.vstack([east, north, up])
def satellites_from_azel(
receiver_ecef_m: np.ndarray,
azimuth_deg: np.ndarray,
elevation_deg: np.ndarray,
range_m: float = 21_000_000.0,
) -> np.ndarray:
axes = enu_axes(receiver_ecef_m)
satellites: list[np.ndarray] = []
for azimuth, elevation in zip(azimuth_deg, elevation_deg):
azimuth_rad = np.deg2rad(azimuth)
elevation_rad = np.deg2rad(elevation)
los_enu = np.asarray(
[
np.cos(elevation_rad) * np.sin(azimuth_rad),
np.cos(elevation_rad) * np.cos(azimuth_rad),
np.sin(elevation_rad),
]
)
los_ecef = axes.T @ los_enu
satellites.append(receiver_ecef_m + range_m * los_ecef)
return np.asarray(satellites)
def position_error_enu_m(
estimated_ecef_m: np.ndarray,
true_ecef_m: np.ndarray,
) -> np.ndarray:
return enu_axes(true_ecef_m) @ (estimated_ecef_m - true_ecef_m)
def _candidate(
satellite_positions_m: np.ndarray,
observations_m: np.ndarray,
sigma_m: np.ndarray,
initial_state: np.ndarray,
used_indices: np.ndarray,
) -> WlsSolution:
state = np.asarray(initial_state, dtype=float).copy()
weights = 1.0 / np.square(sigma_m)
design = np.empty((len(observations_m), 4), dtype=float)
for _ in range(10):
difference = satellite_positions_m - state[:3]
geometric_range = np.linalg.norm(difference, axis=1)
design[:, :3] = -difference / geometric_range[:, None]
design[:, 3] = 1.0
residual = observations_m - geometric_range - state[3]
normal = design.T @ (weights[:, None] * design)
rhs = design.T @ (weights * residual)
update = np.linalg.solve(normal, rhs)
state += update
if np.linalg.norm(update) < 1.0e-4:
break
difference = satellite_positions_m - state[:3]
geometric_range = np.linalg.norm(difference, axis=1)
design[:, :3] = -difference / geometric_range[:, None]
design[:, 3] = 1.0
residual = observations_m - geometric_range - state[3]
normal = design.T @ (weights[:, None] * design)
covariance = np.linalg.inv(normal)
chi_square = float(np.sum(weights * np.square(residual)))
degrees_of_freedom = len(observations_m) - 4
threshold = (
float(CHI_SQUARE_99[degrees_of_freedom])
if 0 < degrees_of_freedom < len(CHI_SQUARE_99)
else 0.0
)
chi_square_pass = degrees_of_freedom > 0 and chi_square <= threshold
valid = (
(len(observations_m) == 4 and degrees_of_freedom == 0)
or chi_square_pass
)
ratio = chi_square / threshold if threshold > 0.0 else float("inf")
return WlsSolution(
position_ecef_m=state[:3].copy(),
clock_bias_m=float(state[3]),
residual_m=residual.copy(),
sigma_m=sigma_m.copy(),
covariance=covariance,
chi_square=chi_square,
chi_square_ratio=ratio,
chi_square_pass=chi_square_pass,
valid=valid,
used_indices=used_indices.copy(),
excluded_index=None,
)
def firmware_wls(
satellite_positions_m: np.ndarray,
observations_m: np.ndarray,
sigma_m: np.ndarray,
initial_state: np.ndarray,
*,
enable_fde: bool = True,
) -> WlsSolution:
count = len(observations_m)
if count < 4:
raise ValueError("position WLS needs at least four satellites")
full_indices = np.arange(count, dtype=int)
full = _candidate(
satellite_positions_m,
observations_m,
sigma_m,
initial_state,
full_indices,
)
if full.chi_square_pass or not enable_fde or count < 7:
return full
selected = full
passing_found = False
full_state = np.concatenate([full.position_ecef_m, [full.clock_bias_m]])
for excluded in range(count):
used = full_indices[full_indices != excluded]
candidate = _candidate(
satellite_positions_m[used],
observations_m[used],
sigma_m[used],
full_state,
used,
)
candidate = WlsSolution(
**{
**candidate.__dict__,
"excluded_index": excluded,
}
)
if candidate.chi_square_pass:
if not passing_found or candidate.chi_square_ratio < selected.chi_square_ratio:
selected = candidate
passing_found = True
elif not passing_found and candidate.chi_square_ratio < selected.chi_square_ratio:
selected = candidate
return selected
@@ -0,0 +1,368 @@
"""Synthetic code-tracking scenarios shared by the analysis and tests."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import numpy as np
from .firmware_wls_model import (
SigmaInputs,
SigmaMethod,
URA_SIGMA_M,
cn0_code_sigma_m,
elevation_sigma_m,
enu_axes,
firmware_sigma_m,
firmware_wls,
lla_to_ecef,
position_error_enu_m,
satellites_from_azel,
)
from .rtl_dll_model import expected_discriminator_std, synthesize_dll_window
TRACKING_PERIOD_S = 0.020
WLS_PERIOD_S = 0.200
TRACKING_UPDATES_PER_WLS = int(round(WLS_PERIOD_S / TRACKING_PERIOD_S))
FIRMWARE_CN0_ALPHA = 0.20
DLL_RATIO_ALPHA = 0.10
DLL_MEAN_ALPHA = 0.20
GOOD_AZIMUTH_DEG = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
GOOD_ELEVATION_DEG = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
POOR_AZIMUTH_DEG = np.asarray([25.0, 46.0, 68.0, 89.0, 112.0, 136.0, 159.0, 182.0])
POOR_ELEVATION_DEG = np.asarray([18.0, 22.0, 25.0, 29.0, 32.0, 27.0, 21.0, 16.0])
Profile = Callable[[float, int, float], float]
def _identity(_time_s: float, _satellite: int, baseline: float) -> float:
return baseline
def _one(_time_s: float, _satellite: int, _baseline: float) -> float:
return 1.0
def _zero(_time_s: float, _satellite: int, _baseline: float) -> float:
return 0.0
@dataclass(frozen=True)
class Scenario:
name: str
description: str
duration_s: float = 80.0
poor_geometry: bool = False
cn0_profile: Profile = _identity
jitter_profile: Profile = _one
discriminator_bias_profile: Profile = _zero
pseudorange_bias_profile: Profile = _zero
@dataclass
class ChannelQuality:
cn0_dbhz: float | None = None
dll_ratio: float = 1.0
dll_mean_z: float = 0.0
def update(
self,
raw_cn0_dbhz: float,
dll_samples: np.ndarray,
) -> None:
if self.cn0_dbhz is None:
self.cn0_dbhz = raw_cn0_dbhz
else:
self.cn0_dbhz = (
FIRMWARE_CN0_ALPHA * raw_cn0_dbhz
+ (1.0 - FIRMWARE_CN0_ALPHA) * self.cn0_dbhz
)
measured_variance = float(np.var(dll_samples, ddof=1))
expected_std = expected_discriminator_std(self.cn0_dbhz)
raw_ratio = measured_variance / max(expected_std * expected_std, 1.0e-15)
raw_ratio = float(np.clip(raw_ratio, 0.05, 25.0))
self.dll_ratio = (
DLL_RATIO_ALPHA * raw_ratio
+ (1.0 - DLL_RATIO_ALPHA) * self.dll_ratio
)
standard_error = np.sqrt(
max(measured_variance, expected_std * expected_std)
/ len(dll_samples)
)
raw_mean_z = abs(float(np.mean(dll_samples))) / max(standard_error, 1.0e-12)
raw_mean_z = float(np.clip(raw_mean_z, 0.0, 12.0))
self.dll_mean_z = (
DLL_MEAN_ALPHA * raw_mean_z
+ (1.0 - DLL_MEAN_ALPHA) * self.dll_mean_z
)
@dataclass(frozen=True)
class EpochSample:
method: SigmaMethod
time_s: float
enu_error_m: np.ndarray
horizontal_error_m: float
position_error_3d_m: float
horizontal_sigma_m: float
valid: bool
chi_square_ratio: float
fde_applied: bool
mean_measurement_sigma_m: float
maximum_dll_ratio: float
def scenario_matrix() -> tuple[Scenario, ...]:
return (
Scenario(
"clean_open_sky",
"C/N0 model and actual DLL jitter agree.",
),
Scenario(
"weak_signal",
"All channels lose 11 dB; current C/N0 weighting already sees it.",
cn0_profile=lambda _t, _s, baseline: baseline - 11.0,
),
Scenario(
"one_channel_hidden_jitter",
"One high-C/N0 channel has 4x DLL jitter not visible in C/N0.",
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 1 else 1.0,
),
Scenario(
"common_hidden_jitter",
"All channels have the same unmodelled 3x DLL jitter.",
jitter_profile=lambda _t, _satellite, _b: 3.0,
),
Scenario(
"noise_burst_recovery",
"Two channels have 5x jitter from 22 s to 45 s, then recover.",
jitter_profile=lambda time_s, satellite, _b: (
5.0
if 22.0 <= time_s < 45.0 and satellite in (1, 5)
else 1.0
),
),
Scenario(
"dll_transient_stress",
"A channel has discriminator mean without excess variance during transient stress.",
discriminator_bias_profile=lambda time_s, satellite, _b: (
0.055
if 25.0 <= time_s < 34.0 and satellite == 3
else 0.0
),
pseudorange_bias_profile=lambda time_s, satellite, _b: (
9.0
if 25.0 <= time_s < 34.0 and satellite == 3
else 0.0
),
),
Scenario(
"stable_multipath_bias",
"Low-jitter, high-C/N0 channel has a stable 14 m code bias.",
pseudorange_bias_profile=lambda _t, satellite, _b: (
14.0 if satellite == 6 else 0.0
),
),
Scenario(
"mixed_multipath",
"One channel has both a 10 m bias and 3x excess jitter.",
jitter_profile=lambda _t, satellite, _b: 3.0 if satellite == 2 else 1.0,
pseudorange_bias_profile=lambda _t, satellite, _b: (
10.0 if satellite == 2 else 0.0
),
),
Scenario(
"poor_geometry_hidden_jitter",
"Clustered geometry plus one channel with unmodelled 4x jitter.",
poor_geometry=True,
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 4 else 1.0,
),
Scenario(
"cn0_step_recovery",
"C/N0 is initially weak and improves; no hidden DLL noise exists.",
cn0_profile=lambda time_s, _satellite, baseline: (
baseline - 12.0 if time_s < 30.0 else baseline
),
),
)
def _geometry(scenario: Scenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
receiver = lla_to_ecef(39.9208, 32.8541, 930.0)
if scenario.poor_geometry:
azimuth = POOR_AZIMUTH_DEG
elevation = POOR_ELEVATION_DEG
else:
azimuth = GOOD_AZIMUTH_DEG
elevation = GOOD_ELEVATION_DEG
satellites = satellites_from_azel(receiver, azimuth, elevation)
return receiver, satellites, elevation.copy()
def run_scenario(
scenario: Scenario,
seed: int,
methods: tuple[SigmaMethod, ...] = tuple(SigmaMethod),
) -> list[EpochSample]:
rng = np.random.default_rng(seed)
receiver, satellites, elevations = _geometry(scenario)
satellite_count = len(satellites)
geometric_ranges = np.linalg.norm(satellites - receiver, axis=1)
baseline_cn0 = np.clip(34.0 + 0.16 * elevations, 30.0, 48.0)
ura_indices = np.zeros(satellite_count, dtype=np.uint8)
quality = [ChannelQuality() for _ in range(satellite_count)]
# These are deliberately slower/non-DLL terms. They prevent a synthetic
# white-noise-only experiment from overstating what DLL weighting can fix.
ura_error = rng.normal(0.0, 0.45 * URA_SIGMA_M[0], satellite_count)
atmospheric_state = rng.normal(
0.0,
0.35 * elevation_sigma_m(elevations),
satellite_count,
)
code_noise_state = np.zeros(satellite_count)
initial_state = np.concatenate(
[
receiver + np.asarray([450.0, -300.0, 220.0]),
[0.0],
]
)
method_state = {method: initial_state.copy() for method in methods}
samples: list[EpochSample] = []
clock_bias_m = 82.0
receiver_enu_axes = enu_axes(receiver)
tracking_steps = int(round(scenario.duration_s / TRACKING_PERIOD_S))
latest_actual_jitter = np.ones(satellite_count)
for tracking_step in range(tracking_steps):
time_s = tracking_step * TRACKING_PERIOD_S
for satellite in range(satellite_count):
true_cn0 = scenario.cn0_profile(
time_s,
satellite,
float(baseline_cn0[satellite]),
)
actual_jitter = scenario.jitter_profile(
time_s,
satellite,
1.0,
)
discriminator_bias = scenario.discriminator_bias_profile(
time_s,
satellite,
0.0,
)
latest_actual_jitter[satellite] = actual_jitter
dll_window = synthesize_dll_window(
rng,
cn0_dbhz=true_cn0,
jitter_multiplier=actual_jitter,
discriminator_bias=discriminator_bias,
)
# The hardware NWPR C/N0 estimate is noisy; firmware applies alpha=0.2.
raw_cn0 = float(np.clip(true_cn0 + rng.normal(0.0, 1.3), 10.0, 60.0))
quality[satellite].update(raw_cn0, dll_window.samples)
if (tracking_step + 1) % TRACKING_UPDATES_PER_WLS != 0:
continue
true_cn0 = np.asarray(
[
scenario.cn0_profile(
time_s,
satellite,
float(baseline_cn0[satellite]),
)
for satellite in range(satellite_count)
]
)
actual_code_sigma = cn0_code_sigma_m(true_cn0) * latest_actual_jitter
innovation = rng.normal(0.0, actual_code_sigma)
code_noise_state = (
0.25 * code_noise_state
+ np.sqrt(1.0 - 0.25**2) * innovation
)
atmospheric_innovation = rng.normal(
0.0,
0.35 * elevation_sigma_m(elevations),
)
atmospheric_state = (
0.97 * atmospheric_state
+ np.sqrt(1.0 - 0.97**2) * atmospheric_innovation
)
pseudorange_bias = np.asarray(
[
scenario.pseudorange_bias_profile(
time_s,
satellite,
0.0,
)
for satellite in range(satellite_count)
]
)
observations = (
geometric_ranges
+ clock_bias_m
+ ura_error
+ atmospheric_state
+ code_noise_state
+ pseudorange_bias
)
sigma_inputs = SigmaInputs(
ura_index=ura_indices,
elevation_deg=elevations,
cn0_dbhz=np.asarray([item.cn0_dbhz for item in quality], dtype=float),
dll_variance_ratio=np.asarray([item.dll_ratio for item in quality]),
dll_mean_z=np.asarray([item.dll_mean_z for item in quality]),
actual_jitter_multiplier=latest_actual_jitter.copy(),
)
for method in methods:
sigma = firmware_sigma_m(sigma_inputs, method)
solution = firmware_wls(
satellites,
observations,
sigma,
method_state[method],
)
# NavEquations.c updates its persistent WLS seed only after an
# integrity-valid solution.
if solution.valid:
method_state[method] = np.concatenate(
[solution.position_ecef_m, [solution.clock_bias_m]]
)
enu_error = position_error_enu_m(solution.position_ecef_m, receiver)
enu_covariance = (
# The top-left block is ECEF position covariance.
# R * Q * R' maps it to ENU.
receiver_enu_axes
@ solution.covariance[:3, :3]
@ receiver_enu_axes.T
)
horizontal_sigma = float(
np.sqrt(max(0.0, enu_covariance[0, 0] + enu_covariance[1, 1]))
)
samples.append(
EpochSample(
method=method,
time_s=time_s,
enu_error_m=enu_error,
horizontal_error_m=float(np.linalg.norm(enu_error[:2])),
position_error_3d_m=float(np.linalg.norm(enu_error)),
horizontal_sigma_m=horizontal_sigma,
valid=solution.valid,
chi_square_ratio=solution.chi_square_ratio,
fde_applied=solution.excluded_index is not None,
mean_measurement_sigma_m=float(np.mean(sigma)),
maximum_dll_ratio=float(
max(item.dll_ratio for item in quality)
),
)
)
return samples
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

@@ -0,0 +1,94 @@
# GPS code measurement model sonuçları
Monte Carlo seed sayısı: `12`. İlk 5 saniye metriklere alınmadı.
## Yöntem karşılaştırması
| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |
|---|---|---:|---:|---:|---:|---:|
| clean_open_sky | Mevcut C/N0 | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
| clean_open_sky | Naif DLL ekle | 10.02 | +0.3% | 17.25 | 100.0 | 0.31 |
| clean_open_sky | DLL excess | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
| clean_open_sky | DLL excess+mean | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
| clean_open_sky | Oracle variance | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
| weak_signal | Mevcut C/N0 | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
| weak_signal | Naif DLL ekle | 32.31 | +1.3% | 55.46 | 100.0 | 56.18 |
| weak_signal | DLL excess | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
| weak_signal | DLL excess+mean | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
| weak_signal | Oracle variance | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
| one_channel_hidden_jitter | Mevcut C/N0 | 12.27 | +0.0% | 19.94 | 100.0 | 1.07 |
| one_channel_hidden_jitter | Naif DLL ekle | 10.49 | +14.5% | 17.75 | 100.0 | 0.33 |
| one_channel_hidden_jitter | DLL excess | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
| one_channel_hidden_jitter | DLL excess+mean | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
| one_channel_hidden_jitter | Oracle variance | 10.46 | +14.7% | 17.74 | 100.0 | 0.33 |
| common_hidden_jitter | Mevcut C/N0 | 33.79 | +0.0% | 59.76 | 94.8 | 52.40 |
| common_hidden_jitter | Naif DLL ekle | 27.61 | +18.3% | 47.56 | 100.0 | 43.93 |
| common_hidden_jitter | DLL excess | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
| common_hidden_jitter | DLL excess+mean | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
| common_hidden_jitter | Oracle variance | 27.74 | +17.9% | 47.83 | 100.0 | 43.87 |
| noise_burst_recovery | Mevcut C/N0 | 12.96 | +0.0% | 22.13 | 98.9 | 2.58 |
| noise_burst_recovery | Naif DLL ekle | 10.52 | +18.8% | 18.03 | 100.0 | 0.53 |
| noise_burst_recovery | DLL excess | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
| noise_burst_recovery | DLL excess+mean | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
| noise_burst_recovery | Oracle variance | 10.47 | +19.2% | 17.90 | 100.0 | 0.49 |
| dll_transient_stress | Mevcut C/N0 | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
| dll_transient_stress | Naif DLL ekle | 10.12 | +0.3% | 17.33 | 100.0 | 0.38 |
| dll_transient_stress | DLL excess | 10.14 | +0.0% | 17.38 | 100.0 | 0.38 |
| dll_transient_stress | DLL excess+mean | 10.12 | +0.2% | 17.37 | 100.0 | 0.33 |
| dll_transient_stress | Oracle variance | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
| stable_multipath_bias | Mevcut C/N0 | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
| stable_multipath_bias | Naif DLL ekle | 12.39 | +1.3% | 20.87 | 100.0 | 1.47 |
| stable_multipath_bias | DLL excess | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
| stable_multipath_bias | DLL excess+mean | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
| stable_multipath_bias | Oracle variance | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
| mixed_multipath | Mevcut C/N0 | 14.43 | +0.0% | 25.38 | 100.0 | 5.64 |
| mixed_multipath | Naif DLL ekle | 11.26 | +22.0% | 19.60 | 100.0 | 0.73 |
| mixed_multipath | DLL excess | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
| mixed_multipath | DLL excess+mean | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
| mixed_multipath | Oracle variance | 11.06 | +23.4% | 19.20 | 100.0 | 0.73 |
| poor_geometry_hidden_jitter | Mevcut C/N0 | 293.82 | +0.0% | 574.96 | 100.0 | 94.31 |
| poor_geometry_hidden_jitter | Naif DLL ekle | 206.13 | +29.8% | 405.15 | 100.0 | 92.42 |
| poor_geometry_hidden_jitter | DLL excess | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
| poor_geometry_hidden_jitter | DLL excess+mean | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
| poor_geometry_hidden_jitter | Oracle variance | 196.20 | +33.2% | 386.58 | 100.0 | 91.27 |
| cn0_step_recovery | Mevcut C/N0 | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
| cn0_step_recovery | Naif DLL ekle | 22.27 | +1.4% | 47.59 | 100.0 | 21.42 |
| cn0_step_recovery | DLL excess | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
| cn0_step_recovery | DLL excess+mean | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
| cn0_step_recovery | Oracle variance | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
## Modelden çıkan karar özeti
- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.
- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.
- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.
- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.
- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.
- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.
## Bozulma penceresi odaklı 3D RMS
| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |
|---|---:|---:|---:|---:|
| noise_burst_recovery [22,45) s | 17.83 | 11.36 | 11.36 | 11.30 |
| dll_transient_stress [25,34) s | 10.83 | 10.82 | 10.65 | 10.83 |
| cn0_step_recovery [5,30) s | 36.48 | 36.48 | 36.48 | 36.48 |
## Code smoothing ve carrier CFO
| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |
|---|---:|---:|---:|---:|---:|---:|---:|
| cfo_compensated | 3.99 | 248.61 | 0.94 | 16.42 | 414.08 | 2.05 | 84.13 |
| cfo_residual_0p5_mps | 3.99 | 248.61 | 1.30 | 16.41 | 414.08 | 2.57 | 83.23 |
| cfo_residual_3_mps | 3.99 | 248.61 | 5.48 | 17.06 | 414.08 | 7.07 | 78.73 |
## Yorum sınırları
- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.
- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.
- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.
- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.
- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.
- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.
- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.
- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.
@@ -0,0 +1,65 @@
# Doppler WLS -> ESKF model sonuçları
Monte Carlo seed sayısı: `20`; ilk 5 saniye RMS metriklerinden çıkarıldı.
50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.
## Sonuç tablosu
| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |
|---|---|---:|---:|---:|---:|---:|---:|
| steady_cruise | Önceki sabit IIR | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Ham Doppler + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Adaptif IIR + ESKF | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Ham + robust WLS + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
| airliner_maneuver | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Önceki sabit IIR | 0.942 | 0.919 | 1.178 | 100.0 | 55.9 | 0.0 |
| aggressive_maneuver | Ham Doppler + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Adaptif IIR + ESKF | 0.200 | 0.197 | 0.171 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Önceki sabit IIR | 0.419 | 0.443 | 0.485 | 100.0 | 99.7 | 0.0 |
| low_cn0_airliner | Ham Doppler + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Adaptif IIR + ESKF | 0.206 | 0.203 | 0.338 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Ham + robust WLS + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Önceki sabit IIR | 0.495 | 0.478 | 1.146 | 100.0 | 97.7 | 0.0 |
| single_satellite_outliers | Ham Doppler + ESKF | 0.257 | 0.229 | 1.451 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Adaptif IIR + ESKF | 0.356 | 0.312 | 1.154 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Ham + robust WLS + ESKF | 0.191 | 0.179 | 0.418 | 100.0 | 100.0 | 2.9 |
| clock_drift_steps | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
| clock_drift_steps | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| clock_drift_steps | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
| clock_drift_steps | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Önceki sabit IIR | 0.397 | 0.413 | 0.431 | 100.0 | 99.9 | 0.0 |
| gps_gap_recovery | Ham Doppler + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Adaptif IIR + ESKF | 0.208 | 0.197 | 0.129 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Ham + robust WLS + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
| mixed_flight_faults | Önceki sabit IIR | 0.940 | 0.913 | 1.515 | 100.0 | 60.4 | 0.0 |
| mixed_flight_faults | Ham Doppler + ESKF | 0.224 | 0.209 | 1.498 | 100.0 | 100.0 | 0.0 |
| mixed_flight_faults | Adaptif IIR + ESKF | 0.295 | 0.275 | 0.838 | 100.0 | 99.9 | 0.0 |
| mixed_flight_faults | Ham + robust WLS + ESKF | 0.221 | 0.206 | 1.013 | 100.0 | 100.0 | 1.9 |
## Bulgular
- Sabit hızda pre-IIR ESKF RMS'i `0.182` m/s, ham yol `0.190` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `0.416` m/s WLS RMS'ten bu seviyeye indiriyor.
- Airliner manevrasında sabit IIR `0.416` m/s ve `%0.0` NIS ret oranı verirken ham yol `0.190` m/s ve `%0.0` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.
- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `0.419`, `0.251`, `0.206` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.
- Tek uydu burst'ünde raw WLS RMS `1.451` m/s, robust leave-one-out ile `0.418` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.
- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `0.269` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.
## Mimari kararı
- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.
- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.
- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.
- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.
## Model sınırları
- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.
- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.
- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.
- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.
- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.
- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.
@@ -0,0 +1,33 @@
scenario,path,seeds,fused_rms_mps,fused_rms_seed_std_mps,fused_p95_mps,quiet_rms_mps,dynamic_rms_mps,wls_rms_mps,wls_p95_mps,clock_drift_rms_mps,velocity_accept_percent,nis_reject_percent,fused_95_coverage_percent,excluded_epoch_percent,mean_covariance_scale,mean_alpha
steady_cruise,fixed_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
steady_cruise,raw_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
steady_cruise,adaptive_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
steady_cruise,raw_robust_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
airliner_maneuver,fixed_iir_wls_eskf,20,0.41605194656716626,0.04357555329949249,0.7083141902703941,0.4019739347287984,0.4380205569605863,0.440222331186762,0.9391237648850084,0.04445747832784397,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
airliner_maneuver,raw_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
airliner_maneuver,adaptive_iir_wls_eskf,20,0.19421091770965,0.04161706926344872,0.2913238026993562,0.19649429208827177,0.18911500227901482,0.1349904626304289,0.2634780317604405,0.06996150326971154,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
airliner_maneuver,raw_robust_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
aggressive_maneuver,fixed_iir_wls_eskf,20,0.9420862291590975,0.03697606175587584,1.7661046896298587,0.9551353937753919,0.918769792345089,1.1782538620062029,2.634559496211176,0.04445747832784312,100.0,0.0,55.891176470588235,0.0,1.0000030764799477,0.05000000000000001
aggressive_maneuver,raw_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
aggressive_maneuver,adaptive_iir_wls_eskf,20,0.20034438129208682,0.04215736847319882,0.3027038479248761,0.2017392380134829,0.19713256311771804,0.17059135831478262,0.3555978539018994,0.08938088837068828,100.0,0.0,100.0,0.0,1.0001185178364316,0.17603563474387524
aggressive_maneuver,raw_robust_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
low_cn0_airliner,fixed_iir_wls_eskf,20,0.4194988293998554,0.04110688289349549,0.7094271250760412,0.4045729059455276,0.4427791039662977,0.4845544319763686,0.9431345662991619,0.14058689054355467,100.0,0.0,99.74176470588235,0.0,1.0000281042403611,0.05000000000000001
low_cn0_airliner,raw_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
low_cn0_airliner,adaptive_iir_wls_eskf,20,0.20588009483954214,0.040099541607423995,0.3090368268997948,0.2069944088941925,0.20293127741446826,0.337603473203961,0.684354700050294,0.21325106769335284,100.0,0.0,100.0,0.0,1.0000902389090656,0.1040055679287305
low_cn0_airliner,raw_robust_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
single_satellite_outliers,fixed_iir_wls_eskf,20,0.49511238386612133,0.053875439119808514,0.8173709856169348,0.5050054814659805,0.4779542859711989,1.1463378050509494,0.9892652696081077,0.8027251360930692,100.0,0.0,97.6964705882353,0.0,6.927634385063575,0.05000000000000001
single_satellite_outliers,raw_wls_eskf,20,0.2570227443883875,0.05273042903908908,0.39439133440568985,0.27216019157834215,0.22927677845507954,1.4512624341492657,0.8297158565959334,1.0630956381619745,100.0,0.0,100.0,0.0,11.13597527632786,1.0
single_satellite_outliers,adaptive_iir_wls_eskf,20,0.3556263234893831,0.05851649747663683,0.595609347562141,0.3792437361871613,0.31186327209433895,1.1538474734782178,0.5683617007748845,0.8527337411854743,100.0,0.0,100.0,0.0,7.7254775572955,0.11647550111358576
single_satellite_outliers,raw_robust_wls_eskf,20,0.19054941715698687,0.03971061631217381,0.2889429468862187,0.19658211712738688,0.1789788721793858,0.4181969381744313,0.7254268588122093,0.27034697594947305,100.0,0.0,100.0,2.8953229398663693,1.2641999517115465,1.0
clock_drift_steps,fixed_iir_wls_eskf,20,0.4160519465671664,0.04357555329949111,0.7083141902703948,0.4019739347287993,0.4380205569605854,0.44022233118676146,0.9391237648850096,1.8177258394243663,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
clock_drift_steps,raw_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
clock_drift_steps,adaptive_iir_wls_eskf,20,0.19421091770965143,0.0416170692634497,0.29132380269935765,0.19649429208827301,0.18911500227901648,0.1349904626304289,0.26347803176043993,1.818528619384018,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
clock_drift_steps,raw_robust_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
gps_gap_recovery,fixed_iir_wls_eskf,20,0.3971781427894736,0.044796626065109466,0.7104470646835127,0.3873196668983798,0.41253405912166263,0.43094056066637465,0.9463346669492851,0.04458606585950929,100.0,0.0,99.90529411764706,0.0,1.0000036934211136,0.05
gps_gap_recovery,raw_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
gps_gap_recovery,adaptive_iir_wls_eskf,20,0.20776527476484566,0.044556420313135676,0.3257106582800945,0.21357805324970008,0.19661285828638408,0.12935782738837362,0.2613749696102362,0.06764578524374312,100.0,0.0,100.0,0.0,1.0000036934211136,0.10814839572192514
gps_gap_recovery,raw_robust_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
mixed_flight_faults,fixed_iir_wls_eskf,20,0.9404176641021843,0.04319034103529422,1.7661662430364344,0.9555417869207146,0.9132850762857121,1.5154618461064036,2.663221132275041,0.7591896512045151,100.0,0.0,60.37294117647059,0.0,1.3397749809447188,0.05000000000000001
mixed_flight_faults,raw_wls_eskf,20,0.22399771088975795,0.05069473595820174,0.3492819306443341,0.23151007332767243,0.2092729705094658,1.498455933396739,2.36689254151483,0.7603191788358898,100.0,0.0,100.0,0.0,2.1864248911994575,1.0
mixed_flight_faults,adaptive_iir_wls_eskf,20,0.2952659489493427,0.07200078881833771,0.4874378367724045,0.30541797568187434,0.2754557634719251,0.838415542523151,0.9361678311586857,0.47500979673114047,100.0,0.0,99.92470588235294,0.0,1.250042525522558,0.178938679245283
mixed_flight_faults,raw_robust_wls_eskf,20,0.2208132635451996,0.04372541489397352,0.3416439647301807,0.22864253849532457,0.20560052604298312,1.0129266912203836,2.2537697549366906,0.673490930924677,100.0,0.0,100.0,1.9103773584905661,1.5692458289027678,1.0
1 scenario path seeds fused_rms_mps fused_rms_seed_std_mps fused_p95_mps quiet_rms_mps dynamic_rms_mps wls_rms_mps wls_p95_mps clock_drift_rms_mps velocity_accept_percent nis_reject_percent fused_95_coverage_percent excluded_epoch_percent mean_covariance_scale mean_alpha
2 steady_cruise fixed_iir_wls_eskf 20 0.1821998934975134 0.040897665127266884 0.27466022199861373 0.1821998934975134 nan 0.0681533332956358 0.11645920599554568 0.04445747832784426 100.0 0.0 100.0 0.0 1.0000030764799477 0.05000000000000001
3 steady_cruise raw_wls_eskf 20 0.19046432375253602 0.040009781644375395 0.28906917942032884 0.19046432375253602 nan 0.4164164729808367 0.720889200812689 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343088 1.0
4 steady_cruise adaptive_iir_wls_eskf 20 0.1821998934975134 0.040897665127266884 0.27466022199861373 0.1821998934975134 nan 0.0681533332956358 0.11645920599554568 0.04445747832784426 100.0 0.0 100.0 0.0 1.0000030764799477 0.05000000000000001
5 steady_cruise raw_robust_wls_eskf 20 0.19046432375253602 0.040009781644375395 0.28906917942032884 0.19046432375253602 nan 0.4164164729808367 0.720889200812689 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343088 1.0
6 airliner_maneuver fixed_iir_wls_eskf 20 0.41605194656716626 0.04357555329949249 0.7083141902703941 0.4019739347287984 0.4380205569605863 0.440222331186762 0.9391237648850084 0.04445747832784397 100.0 0.0 99.71411764705883 0.0 1.0000030764799477 0.05000000000000001
7 airliner_maneuver raw_wls_eskf 20 0.19046432375253933 0.04000978164437598 0.28906917942032373 0.196483054968526 0.1789145728432288 0.4164164729808368 0.720889200812707 0.2689447776657465 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
8 airliner_maneuver adaptive_iir_wls_eskf 20 0.19421091770965 0.04161706926344872 0.2913238026993562 0.19649429208827177 0.18911500227901482 0.1349904626304289 0.2634780317604405 0.06996150326971154 100.0 0.0 100.0 0.0 1.0000030764799477 0.11647550111358576
9 airliner_maneuver raw_robust_wls_eskf 20 0.19046432375253933 0.04000978164437598 0.28906917942032373 0.196483054968526 0.1789145728432288 0.4164164729808368 0.720889200812707 0.2689447776657465 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
10 aggressive_maneuver fixed_iir_wls_eskf 20 0.9420862291590975 0.03697606175587584 1.7661046896298587 0.9551353937753919 0.918769792345089 1.1782538620062029 2.634559496211176 0.04445747832784312 100.0 0.0 55.891176470588235 0.0 1.0000030764799477 0.05000000000000001
11 aggressive_maneuver raw_wls_eskf 20 0.19046432375254996 0.04000978164436178 0.2890691794203225 0.1928303776618579 0.18561935253806933 0.41641647298083734 0.7208892008127159 0.2689447776657467 100.0 0.0 100.0 0.0 1.2633800541343083 1.0
12 aggressive_maneuver adaptive_iir_wls_eskf 20 0.20034438129208682 0.04215736847319882 0.3027038479248761 0.2017392380134829 0.19713256311771804 0.17059135831478262 0.3555978539018994 0.08938088837068828 100.0 0.0 100.0 0.0 1.0001185178364316 0.17603563474387524
13 aggressive_maneuver raw_robust_wls_eskf 20 0.19046432375254996 0.04000978164436178 0.2890691794203225 0.1928303776618579 0.18561935253806933 0.41641647298083734 0.7208892008127159 0.2689447776657467 100.0 0.0 100.0 0.0 1.2633800541343083 1.0
14 low_cn0_airliner fixed_iir_wls_eskf 20 0.4194988293998554 0.04110688289349549 0.7094271250760412 0.4045729059455276 0.4427791039662977 0.4845544319763686 0.9431345662991619 0.14058689054355467 100.0 0.0 99.74176470588235 0.0 1.0000281042403611 0.05000000000000001
15 low_cn0_airliner raw_wls_eskf 20 0.25103588282812916 0.04464089418791806 0.4041748199164089 0.2555504221567221 0.2416319579439005 1.3168245098334108 2.2796518151866563 0.8504780622313423 100.0 0.0 100.0 0.0 1.26385941740462 1.0
16 low_cn0_airliner adaptive_iir_wls_eskf 20 0.20588009483954214 0.040099541607423995 0.3090368268997948 0.2069944088941925 0.20293127741446826 0.337603473203961 0.684354700050294 0.21325106769335284 100.0 0.0 100.0 0.0 1.0000902389090656 0.1040055679287305
17 low_cn0_airliner raw_robust_wls_eskf 20 0.25103588282812916 0.04464089418791806 0.4041748199164089 0.2555504221567221 0.2416319579439005 1.3168245098334108 2.2796518151866563 0.8504780622313423 100.0 0.0 100.0 0.0 1.26385941740462 1.0
18 single_satellite_outliers fixed_iir_wls_eskf 20 0.49511238386612133 0.053875439119808514 0.8173709856169348 0.5050054814659805 0.4779542859711989 1.1463378050509494 0.9892652696081077 0.8027251360930692 100.0 0.0 97.6964705882353 0.0 6.927634385063575 0.05000000000000001
19 single_satellite_outliers raw_wls_eskf 20 0.2570227443883875 0.05273042903908908 0.39439133440568985 0.27216019157834215 0.22927677845507954 1.4512624341492657 0.8297158565959334 1.0630956381619745 100.0 0.0 100.0 0.0 11.13597527632786 1.0
20 single_satellite_outliers adaptive_iir_wls_eskf 20 0.3556263234893831 0.05851649747663683 0.595609347562141 0.3792437361871613 0.31186327209433895 1.1538474734782178 0.5683617007748845 0.8527337411854743 100.0 0.0 100.0 0.0 7.7254775572955 0.11647550111358576
21 single_satellite_outliers raw_robust_wls_eskf 20 0.19054941715698687 0.03971061631217381 0.2889429468862187 0.19658211712738688 0.1789788721793858 0.4181969381744313 0.7254268588122093 0.27034697594947305 100.0 0.0 100.0 2.8953229398663693 1.2641999517115465 1.0
22 clock_drift_steps fixed_iir_wls_eskf 20 0.4160519465671664 0.04357555329949111 0.7083141902703948 0.4019739347287993 0.4380205569605854 0.44022233118676146 0.9391237648850096 1.8177258394243663 100.0 0.0 99.71411764705883 0.0 1.0000030764799477 0.05000000000000001
23 clock_drift_steps raw_wls_eskf 20 0.19046432375253802 0.04000978164437644 0.28906917942032334 0.19648305496852497 0.17891457284322693 0.41641647298083695 0.7208892008126868 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
24 clock_drift_steps adaptive_iir_wls_eskf 20 0.19421091770965143 0.0416170692634497 0.29132380269935765 0.19649429208827301 0.18911500227901648 0.1349904626304289 0.26347803176043993 1.818528619384018 100.0 0.0 100.0 0.0 1.0000030764799477 0.11647550111358576
25 clock_drift_steps raw_robust_wls_eskf 20 0.19046432375253802 0.04000978164437644 0.28906917942032334 0.19648305496852497 0.17891457284322693 0.41641647298083695 0.7208892008126868 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
26 gps_gap_recovery fixed_iir_wls_eskf 20 0.3971781427894736 0.044796626065109466 0.7104470646835127 0.3873196668983798 0.41253405912166263 0.43094056066637465 0.9463346669492851 0.04458606585950929 100.0 0.0 99.90529411764706 0.0 1.0000036934211136 0.05
27 gps_gap_recovery raw_wls_eskf 20 0.2082177514862905 0.03682283889189333 0.32879462613500177 0.21668463381045563 0.1921901394048915 0.41707211010710354 0.7203717161799947 0.26913433965877315 100.0 0.0 100.0 0.0 1.2631217416869522 1.0
28 gps_gap_recovery adaptive_iir_wls_eskf 20 0.20776527476484566 0.044556420313135676 0.3257106582800945 0.21357805324970008 0.19661285828638408 0.12935782738837362 0.2613749696102362 0.06764578524374312 100.0 0.0 100.0 0.0 1.0000036934211136 0.10814839572192514
29 gps_gap_recovery raw_robust_wls_eskf 20 0.2082177514862905 0.03682283889189333 0.32879462613500177 0.21668463381045563 0.1921901394048915 0.41707211010710354 0.7203717161799947 0.26913433965877315 100.0 0.0 100.0 0.0 1.2631217416869522 1.0
30 mixed_flight_faults fixed_iir_wls_eskf 20 0.9404176641021843 0.04319034103529422 1.7661662430364344 0.9555417869207146 0.9132850762857121 1.5154618461064036 2.663221132275041 0.7591896512045151 100.0 0.0 60.37294117647059 0.0 1.3397749809447188 0.05000000000000001
31 mixed_flight_faults raw_wls_eskf 20 0.22399771088975795 0.05069473595820174 0.3492819306443341 0.23151007332767243 0.2092729705094658 1.498455933396739 2.36689254151483 0.7603191788358898 100.0 0.0 100.0 0.0 2.1864248911994575 1.0
32 mixed_flight_faults adaptive_iir_wls_eskf 20 0.2952659489493427 0.07200078881833771 0.4874378367724045 0.30541797568187434 0.2754557634719251 0.838415542523151 0.9361678311586857 0.47500979673114047 100.0 0.0 99.92470588235294 0.0 1.250042525522558 0.178938679245283
33 mixed_flight_faults raw_robust_wls_eskf 20 0.2208132635451996 0.04372541489397352 0.3416439647301807 0.22864253849532457 0.20560052604298312 1.0129266912203836 2.2537697549366906 0.673490930924677 100.0 0.0 100.0 1.9103773584905661 1.5692458289027678 1.0
@@ -0,0 +1,51 @@
scenario,method,epochs,horizontal_rms_m,position_3d_rms_m,position_3d_p95_m,valid_percent,valid_3d_rms_m,horizontal_2sigma_coverage_percent,false_valid_over_25m_percent,fde_percent,mean_measurement_sigma_m
clean_open_sky,current_cn0,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
clean_open_sky,naive_add_dll,4500,5.768593681325036,10.022431815549082,17.253420427433362,100.0,10.022431815549082,100.0,0.3111111111111111,0.0,10.452052333958017
clean_open_sky,dll_excess,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
clean_open_sky,dll_excess_guard,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
clean_open_sky,oracle_variance,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
weak_signal,current_cn0,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
weak_signal,naive_add_dll,4500,18.802930957724488,32.313115706990146,55.45511304556428,100.0,32.313115706990146,100.0,56.177777777777784,0.0,28.674069502747358
weak_signal,dll_excess,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
weak_signal,dll_excess_guard,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
weak_signal,oracle_variance,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
one_channel_hidden_jitter,current_cn0,4500,8.804153682379612,12.268224803643907,19.936371518380813,100.0,12.268224803643907,96.46666666666667,1.0666666666666667,9.666666666666666,8.840973580918838
one_channel_hidden_jitter,naive_add_dll,4500,6.529225405554112,10.487953223937716,17.74604536639535,100.0,10.487953223937716,99.97777777777777,0.33333333333333337,0.0,11.805515833602682
one_channel_hidden_jitter,dll_excess,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
one_channel_hidden_jitter,dll_excess_guard,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
one_channel_hidden_jitter,oracle_variance,4500,6.4491935598492525,10.463101509886709,17.736218582311878,100.0,10.463101509886709,99.97777777777777,0.33333333333333337,0.06666666666666667,10.315229331071063
common_hidden_jitter,current_cn0,4500,19.733300930595462,33.79270185129192,59.76307034516212,94.75555555555556,33.15386252421736,56.39999999999999,52.400000000000006,46.577777777777776,8.840973580918838
common_hidden_jitter,naive_add_dll,4500,16.033969673897577,27.61407554068951,47.55915451805768,100.0,27.61407554068951,99.46666666666667,43.93333333333334,0.26666666666666666,18.882004393424943
common_hidden_jitter,dll_excess,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
common_hidden_jitter,dll_excess_guard,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
common_hidden_jitter,oracle_variance,4500,16.106964558267112,27.740128015702773,47.83188793884836,100.0,27.740128015702773,98.88888888888889,43.86666666666667,0.4888888888888889,18.035214487051437
noise_burst_recovery,current_cn0,4500,9.325915933961204,12.96069950118485,22.132869786417263,98.93333333333332,12.552632065384056,94.71111111111111,2.577777777777778,12.11111111111111,8.840973580918838
noise_burst_recovery,naive_add_dll,4500,6.470839383968352,10.523606281690133,18.031622500344657,100.0,10.523606281690133,100.0,0.5333333333333333,0.06666666666666667,11.60221083527078
noise_burst_recovery,dll_excess,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
noise_burst_recovery,dll_excess_guard,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
noise_burst_recovery,oracle_variance,4500,6.367173301653832,10.473649717876942,17.90212603773505,100.0,10.473649717876942,99.97777777777777,0.4888888888888889,0.044444444444444446,10.187907003764948
dll_transient_stress,current_cn0,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
dll_transient_stress,naive_add_dll,4500,5.793995520088368,10.118871049708806,17.333372587049784,100.0,10.118871049708806,100.0,0.37777777777777777,0.0,10.45205253980181
dll_transient_stress,dll_excess,4500,5.8034283726190745,10.144627133073621,17.380953993070463,100.0,10.144627133073621,99.97777777777777,0.37777777777777777,0.0,8.908174353046816
dll_transient_stress,dll_excess_guard,4500,5.79072382051268,10.123295335548953,17.368844982312577,100.0,10.123295335548953,99.97777777777777,0.33333333333333337,0.0,8.947656011912292
dll_transient_stress,oracle_variance,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
stable_multipath_bias,current_cn0,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
stable_multipath_bias,naive_add_dll,4500,7.072680893174009,12.386663925234807,20.866871605887997,100.0,12.386663925234807,99.97777777777777,1.4666666666666666,0.0,10.452052333958017
stable_multipath_bias,dll_excess,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
stable_multipath_bias,dll_excess_guard,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
stable_multipath_bias,oracle_variance,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
mixed_multipath,current_cn0,4500,8.529840854093363,14.426936007413373,25.381172069686816,100.0,14.426936007413373,97.11111111111111,5.644444444444444,5.0,8.840973580918838
mixed_multipath,naive_add_dll,4500,6.586489015624914,11.258484405711224,19.599950438767102,100.0,11.258484405711224,100.0,0.7333333333333333,0.044444444444444446,11.636563744509052
mixed_multipath,dll_excess,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
mixed_multipath,dll_excess_guard,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
mixed_multipath,oracle_variance,4500,6.448333589783235,11.058021985103961,19.19754215965981,100.0,11.058021985103961,99.95555555555555,0.7333333333333333,0.08888888888888889,10.144307488606447
poor_geometry_hidden_jitter,current_cn0,4500,79.38795978543642,293.8222926191477,574.9617330272,100.0,293.8222926191477,79.0,94.31111111111112,1.1555555555555554,10.734703317378012
poor_geometry_hidden_jitter,naive_add_dll,4500,57.59864532145799,206.13456070122297,405.1527013193,100.0,206.13456070122297,99.66666666666667,92.42222222222222,0.022222222222222223,14.604104871190987
poor_geometry_hidden_jitter,dll_excess,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
poor_geometry_hidden_jitter,dll_excess_guard,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
poor_geometry_hidden_jitter,oracle_variance,4500,55.24662489772763,196.2046647890204,386.5771391089706,100.0,196.2046647890204,99.28888888888889,91.26666666666667,0.022222222222222223,12.721163297137666
cn0_step_recovery,current_cn0,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
cn0_step_recovery,naive_add_dll,4500,12.797089787478507,22.265941453345402,47.59129168788298,100.0,22.265941453345402,100.0,21.422222222222224,0.0,17.44366868773946
cn0_step_recovery,dll_excess,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
cn0_step_recovery,dll_excess_guard,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
cn0_step_recovery,oracle_variance,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
1 scenario method epochs horizontal_rms_m position_3d_rms_m position_3d_p95_m valid_percent valid_3d_rms_m horizontal_2sigma_coverage_percent false_valid_over_25m_percent fde_percent mean_measurement_sigma_m
2 clean_open_sky current_cn0 4500 5.778805688412197 10.054230284262685 17.35250490247279 100.0 10.054230284262685 99.97777777777777 0.3111111111111111 0.0 8.840973580918838
3 clean_open_sky naive_add_dll 4500 5.768593681325036 10.022431815549082 17.253420427433362 100.0 10.022431815549082 100.0 0.3111111111111111 0.0 10.452052333958017
4 clean_open_sky dll_excess 4500 5.7784464702133524 10.05299246387535 17.320782018324902 100.0 10.05299246387535 99.97777777777777 0.33333333333333337 0.0 8.908174172560404
5 clean_open_sky dll_excess_guard 4500 5.7784464702133524 10.05299246387535 17.320782018324902 100.0 10.05299246387535 99.97777777777777 0.33333333333333337 0.0 8.908174172560404
6 clean_open_sky oracle_variance 4500 5.778805688412197 10.054230284262685 17.35250490247279 100.0 10.054230284262685 99.97777777777777 0.3111111111111111 0.0 8.840973580918838
7 weak_signal current_cn0 4500 19.020289863331797 32.73415409569256 56.44388754013162 100.0 32.73415409569256 98.8 56.31111111111111 0.6222222222222222 20.84621216112656
8 weak_signal naive_add_dll 4500 18.802930957724488 32.313115706990146 55.45511304556428 100.0 32.313115706990146 100.0 56.177777777777784 0.0 28.674069502747358
9 weak_signal dll_excess 4500 18.986226727568837 32.6486650471064 56.115244341908046 100.0 32.6486650471064 98.91111111111111 56.222222222222214 0.5111111111111112 21.20058629982459
10 weak_signal dll_excess_guard 4500 18.986226727568837 32.6486650471064 56.115244341908046 100.0 32.6486650471064 98.91111111111111 56.222222222222214 0.5111111111111112 21.20058629982459
11 weak_signal oracle_variance 4500 19.020289863331797 32.73415409569256 56.44388754013162 100.0 32.73415409569256 98.8 56.31111111111111 0.6222222222222222 20.84621216112656
12 one_channel_hidden_jitter current_cn0 4500 8.804153682379612 12.268224803643907 19.936371518380813 100.0 12.268224803643907 96.46666666666667 1.0666666666666667 9.666666666666666 8.840973580918838
13 one_channel_hidden_jitter naive_add_dll 4500 6.529225405554112 10.487953223937716 17.74604536639535 100.0 10.487953223937716 99.97777777777777 0.33333333333333337 0.0 11.805515833602682
14 one_channel_hidden_jitter dll_excess 4500 6.455903980559815 10.46584556632944 17.675128387138177 100.0 10.46584556632944 99.97777777777777 0.33333333333333337 0.044444444444444446 10.358668146326426
15 one_channel_hidden_jitter dll_excess_guard 4500 6.455903980559815 10.46584556632944 17.675128387138177 100.0 10.46584556632944 99.97777777777777 0.33333333333333337 0.044444444444444446 10.358668146326426
16 one_channel_hidden_jitter oracle_variance 4500 6.4491935598492525 10.463101509886709 17.736218582311878 100.0 10.463101509886709 99.97777777777777 0.33333333333333337 0.06666666666666667 10.315229331071063
17 common_hidden_jitter current_cn0 4500 19.733300930595462 33.79270185129192 59.76307034516212 94.75555555555556 33.15386252421736 56.39999999999999 52.400000000000006 46.577777777777776 8.840973580918838
18 common_hidden_jitter naive_add_dll 4500 16.033969673897577 27.61407554068951 47.55915451805768 100.0 27.61407554068951 99.46666666666667 43.93333333333334 0.26666666666666666 18.882004393424943
19 common_hidden_jitter dll_excess 4500 16.085615037536463 27.688964211504615 47.828684727805175 100.0 27.688964211504615 98.97777777777777 43.888888888888886 0.4444444444444444 18.04555007574287
20 common_hidden_jitter dll_excess_guard 4500 16.085615037536463 27.688964211504615 47.828684727805175 100.0 27.688964211504615 98.97777777777777 43.888888888888886 0.4444444444444444 18.04555007574287
21 common_hidden_jitter oracle_variance 4500 16.106964558267112 27.740128015702773 47.83188793884836 100.0 27.740128015702773 98.88888888888889 43.86666666666667 0.4888888888888889 18.035214487051437
22 noise_burst_recovery current_cn0 4500 9.325915933961204 12.96069950118485 22.132869786417263 98.93333333333332 12.552632065384056 94.71111111111111 2.577777777777778 12.11111111111111 8.840973580918838
23 noise_burst_recovery naive_add_dll 4500 6.470839383968352 10.523606281690133 18.031622500344657 100.0 10.523606281690133 100.0 0.5333333333333333 0.06666666666666667 11.60221083527078
24 noise_burst_recovery dll_excess 4500 6.397124977878571 10.493909835494788 17.99630946143617 100.0 10.493909835494788 99.97777777777777 0.4888888888888889 0.13333333333333333 10.131003114059961
25 noise_burst_recovery dll_excess_guard 4500 6.397124977878571 10.493909835494788 17.99630946143617 100.0 10.493909835494788 99.97777777777777 0.4888888888888889 0.13333333333333333 10.131003114059961
26 noise_burst_recovery oracle_variance 4500 6.367173301653832 10.473649717876942 17.90212603773505 100.0 10.473649717876942 99.97777777777777 0.4888888888888889 0.044444444444444446 10.187907003764948
27 dll_transient_stress current_cn0 4500 5.803776392869777 10.145785618162538 17.425227799952054 100.0 10.145785618162538 99.97777777777777 0.35555555555555557 0.0 8.840973580918838
28 dll_transient_stress naive_add_dll 4500 5.793995520088368 10.118871049708806 17.333372587049784 100.0 10.118871049708806 100.0 0.37777777777777777 0.0 10.45205253980181
29 dll_transient_stress dll_excess 4500 5.8034283726190745 10.144627133073621 17.380953993070463 100.0 10.144627133073621 99.97777777777777 0.37777777777777777 0.0 8.908174353046816
30 dll_transient_stress dll_excess_guard 4500 5.79072382051268 10.123295335548953 17.368844982312577 100.0 10.123295335548953 99.97777777777777 0.33333333333333337 0.0 8.947656011912292
31 dll_transient_stress oracle_variance 4500 5.803776392869777 10.145785618162538 17.425227799952054 100.0 10.145785618162538 99.97777777777777 0.35555555555555557 0.0 8.840973580918838
32 stable_multipath_bias current_cn0 4500 7.12694286036133 12.545413964352555 20.99675149186582 100.0 12.545413964352555 99.8888888888889 1.6666666666666667 0.022222222222222223 8.840973580918838
33 stable_multipath_bias naive_add_dll 4500 7.072680893174009 12.386663925234807 20.866871605887997 100.0 12.386663925234807 99.97777777777777 1.4666666666666666 0.0 10.452052333958017
34 stable_multipath_bias dll_excess 4500 7.125500558670432 12.536689588412326 21.02122766562977 100.0 12.536689588412326 99.91111111111111 1.6444444444444446 0.022222222222222223 8.908174172560404
35 stable_multipath_bias dll_excess_guard 4500 7.125500558670432 12.536689588412326 21.02122766562977 100.0 12.536689588412326 99.91111111111111 1.6444444444444446 0.022222222222222223 8.908174172560404
36 stable_multipath_bias oracle_variance 4500 7.12694286036133 12.545413964352555 20.99675149186582 100.0 12.545413964352555 99.8888888888889 1.6666666666666667 0.022222222222222223 8.840973580918838
37 mixed_multipath current_cn0 4500 8.529840854093363 14.426936007413373 25.381172069686816 100.0 14.426936007413373 97.11111111111111 5.644444444444444 5.0 8.840973580918838
38 mixed_multipath naive_add_dll 4500 6.586489015624914 11.258484405711224 19.599950438767102 100.0 11.258484405711224 100.0 0.7333333333333333 0.044444444444444446 11.636563744509052
39 mixed_multipath dll_excess 4500 6.45479117311332 11.067801578489938 19.233225446062768 100.0 11.067801578489938 100.0 0.7333333333333333 0.08888888888888889 10.202310406784667
40 mixed_multipath dll_excess_guard 4500 6.45479117311332 11.067801578489938 19.233225446062768 100.0 11.067801578489938 100.0 0.7333333333333333 0.08888888888888889 10.202310406784667
41 mixed_multipath oracle_variance 4500 6.448333589783235 11.058021985103961 19.19754215965981 100.0 11.058021985103961 99.95555555555555 0.7333333333333333 0.08888888888888889 10.144307488606447
42 poor_geometry_hidden_jitter current_cn0 4500 79.38795978543642 293.8222926191477 574.9617330272 100.0 293.8222926191477 79.0 94.31111111111112 1.1555555555555554 10.734703317378012
43 poor_geometry_hidden_jitter naive_add_dll 4500 57.59864532145799 206.13456070122297 405.1527013193 100.0 206.13456070122297 99.66666666666667 92.42222222222222 0.022222222222222223 14.604104871190987
44 poor_geometry_hidden_jitter dll_excess 4500 55.3927819274298 196.86935630824075 390.63687676393124 100.0 196.86935630824075 99.24444444444444 91.4 0.022222222222222223 12.779670971493758
45 poor_geometry_hidden_jitter dll_excess_guard 4500 55.3927819274298 196.86935630824075 390.63687676393124 100.0 196.86935630824075 99.24444444444444 91.4 0.022222222222222223 12.779670971493758
46 poor_geometry_hidden_jitter oracle_variance 4500 55.24662489772763 196.2046647890204 386.5771391089706 100.0 196.2046647890204 99.28888888888889 91.26666666666667 0.022222222222222223 12.721163297137666
47 cn0_step_recovery current_cn0 4500 12.974359636882712 22.585010269106 47.95054105970103 100.0 22.585010269106 99.42222222222222 21.622222222222224 0.2222222222222222 13.475047639913695
48 cn0_step_recovery naive_add_dll 4500 12.797089787478507 22.265941453345402 47.59129168788298 100.0 22.265941453345402 100.0 21.422222222222224 0.0 17.44366868773946
49 cn0_step_recovery dll_excess 4500 12.96657717120691 22.586113735655264 47.91159461508304 100.0 22.586113735655264 99.46666666666667 21.488888888888887 0.2 13.653801199442478
50 cn0_step_recovery dll_excess_guard 4500 12.96657717120691 22.586113735655264 47.91159461508304 100.0 22.586113735655264 99.46666666666667 21.488888888888887 0.2 13.653801199442478
51 cn0_step_recovery oracle_variance 4500 12.974359636882712 22.585010269106 47.95054105970103 100.0 22.585010269106 99.42222222222222 21.622222222222224 0.2222222222222222 13.475047639913695
@@ -0,0 +1,2 @@
numpy>=1.24
matplotlib>=3.7
@@ -0,0 +1,200 @@
"""Small executable model of the RTL code-tracking observables.
This is deliberately scoped to the parts that matter to the navigation
measurement study:
* the normalized early-minus-late magnitude discriminator;
* its signed Q2.13 output;
* the fixed C1/C2 code-loop filter and Q20 NCO command;
* the 20 one-millisecond discriminator samples available per NAV bit.
It is not an RF waveform simulator. The C/N0-to-discriminator-noise mapping
is an explicit calibration assumption until real correlator/DLL telemetry is
recorded from the board.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
DLL_OUTPUT_FRACTION_BITS = 13
DLL_OUTPUT_WORD_BITS = 15
DLL_SPACING_CHIPS = 0.5
CODE_LOOP_C1 = 21.25
CODE_LOOP_C2 = 0.05712890625
CODE_LOOP_OUTPUT_FRACTION_BITS = 20
CODE_LOOP_OUTPUT_WORD_BITS = 28
DLL_SAMPLES_PER_NAV_BIT = 20
CODE_FREQUENCY_BASIS_HZ = 1.023e6
ABS_SAMPLE_COUNTER_MODULUS = 1 << 32
# Synthetic calibration point. It only fixes the scale of the Monte Carlo
# discriminator telemetry. Dynamic weighting uses a measured/expected ratio,
# so the absolute choice cancels in the correctly modelled case.
DLL_STD_AT_45_DBHZ = 0.020
def _signed_saturate(value: int, word_bits: int) -> int:
minimum = -(1 << (word_bits - 1))
maximum = (1 << (word_bits - 1)) - 1
return min(maximum, max(minimum, value))
def quantize_signed(value: float, word_bits: int, fraction_bits: int) -> int:
scaled = int(np.rint(value * (1 << fraction_bits)))
return _signed_saturate(scaled, word_bits)
def dequantize_signed(value: int, fraction_bits: int) -> float:
return float(value) / float(1 << fraction_bits)
def triangular_correlation(code_offset_chips: float) -> float:
"""Ideal one-chip triangular GPS C/A autocorrelation main lobe."""
return max(0.0, 1.0 - abs(code_offset_chips))
def ideal_early_late_magnitudes(
code_error_chips: float,
spacing_chips: float = DLL_SPACING_CHIPS,
) -> tuple[float, float]:
half_spacing = 0.5 * spacing_chips
early = triangular_correlation(code_error_chips + half_spacing)
late = triangular_correlation(code_error_chips - half_spacing)
return early, late
def normalized_early_minus_late(early_magnitude: float, late_magnitude: float) -> float:
denominator = early_magnitude + late_magnitude
if denominator <= 0.0:
return 0.0
return (early_magnitude - late_magnitude) / denominator
def rtl_discriminator(code_error_chips: float) -> tuple[int, float]:
"""Return the RTL-like Q2.13 discriminator code and dequantized value."""
early, late = ideal_early_late_magnitudes(code_error_chips)
value = normalized_early_minus_late(early, late)
code = quantize_signed(
value,
DLL_OUTPUT_WORD_BITS,
DLL_OUTPUT_FRACTION_BITS,
)
return code, dequantize_signed(code, DLL_OUTPUT_FRACTION_BITS)
def expected_discriminator_std(cn0_dbhz: float) -> float:
"""Expected normalized DLL jitter used by the synthetic calibration.
The 20 dB amplitude-domain law mirrors the firmware's code-sigma model.
The result is bounded because the normalized discriminator is finite.
"""
value = DLL_STD_AT_45_DBHZ * 10.0 ** ((45.0 - cn0_dbhz) / 20.0)
return float(np.clip(value, 0.002, 0.35))
def quantized_code_epoch_measurement(
true_abs_sample: float,
) -> tuple[int, int, float]:
"""Model ABS_SAMPLE + REM_CODE_PHASE and firmware reconstruction.
``true_abs_sample`` is the ideal code boundary in RF-sample units. The
hardware captures the following integer sample and reports the overshoot
with the remainder scale used by the C correction formula.
"""
raw_unwrapped = int(np.ceil(true_abs_sample))
overshoot_samples = raw_unwrapped - true_abs_sample
remainder = int(np.rint(overshoot_samples * CODE_FREQUENCY_BASIS_HZ))
raw_register = raw_unwrapped % ABS_SAMPLE_COUNTER_MODULUS
corrected = raw_unwrapped - remainder / CODE_FREQUENCY_BASIS_HZ
return raw_register, remainder, corrected
@dataclass(frozen=True)
class DllWindow:
samples: np.ndarray
mean: float
variance: float
expected_variance: float
variance_ratio: float
mean_z_score: float
def synthesize_dll_window(
rng: np.random.Generator,
*,
cn0_dbhz: float,
jitter_multiplier: float = 1.0,
discriminator_bias: float = 0.0,
sample_count: int = DLL_SAMPLES_PER_NAV_BIT,
) -> DllWindow:
"""Generate and Q2.13-quantize one NAV-bit DLL telemetry window."""
if sample_count < 2:
raise ValueError("DLL variance needs at least two samples")
expected_std = expected_discriminator_std(cn0_dbhz)
actual_std = expected_std * max(0.0, jitter_multiplier)
raw = discriminator_bias + rng.normal(0.0, actual_std, sample_count)
minimum = -(1 << (DLL_OUTPUT_WORD_BITS - 1))
maximum = (1 << (DLL_OUTPUT_WORD_BITS - 1)) - 1
quantized_codes = np.clip(
np.rint(raw * (1 << DLL_OUTPUT_FRACTION_BITS)),
minimum,
maximum,
).astype(np.int32)
samples = quantized_codes.astype(float) / float(1 << DLL_OUTPUT_FRACTION_BITS)
mean = float(np.mean(samples))
variance = float(np.var(samples, ddof=1))
expected_variance = expected_std * expected_std
ratio = variance / max(expected_variance, 1.0e-15)
standard_error = np.sqrt(max(variance, expected_variance) / sample_count)
mean_z = abs(mean) / max(float(standard_error), 1.0e-12)
return DllWindow(
samples=samples,
mean=mean,
variance=variance,
expected_variance=expected_variance,
variance_ratio=float(ratio),
mean_z_score=float(mean_z),
)
@dataclass
class CodeLoopFilterRtl:
"""Update-rate model of LF_code.sv, including coefficient quantization."""
integrator_q20: int = 0
c1_integer: int = int(CODE_LOOP_C1 * (1 << 2))
c2_integer: int = int(CODE_LOOP_C2 * (1 << 11))
def update_code(self, discriminator_q13: int) -> int:
# C1 product has Q15 and is zero-padded to Q20 in LF_code.sv.
proportional_q20 = (self.c1_integer * discriminator_q13) << 5
# C2 product has Q24 and LF_code.sv slices it back to Q20.
integral_increment_q20 = (self.c2_integer * discriminator_q13) >> 4
self.integrator_q20 = _signed_saturate(
self.integrator_q20 + integral_increment_q20,
CODE_LOOP_OUTPUT_WORD_BITS,
)
return _signed_saturate(
self.integrator_q20 + proportional_q20,
CODE_LOOP_OUTPUT_WORD_BITS,
)
def update(self, discriminator: float) -> float:
discriminator_q13 = quantize_signed(
discriminator,
DLL_OUTPUT_WORD_BITS,
DLL_OUTPUT_FRACTION_BITS,
)
command_q20 = self.update_code(discriminator_q13)
return dequantize_signed(command_q20, CODE_LOOP_OUTPUT_FRACTION_BITS)
@@ -0,0 +1,212 @@
"""Regression tests for the project-specific Doppler/WLS/ESKF model."""
from __future__ import annotations
from dataclasses import replace
import unittest
import numpy as np
from .doppler_eskf_model import (
AdaptiveAlpha,
DopplerPath,
FIXED_DOPPLER_ALPHA,
flight_scenarios,
local_los_matrix,
run_doppler_eskf_scenario,
velocity_wls,
)
def _scenario(name: str):
return next(item for item in flight_scenarios() if item.name == name)
def _fused_rms(trace) -> float:
selected = trace.time_s >= 5.0
error = trace.fused_velocity_mps[selected] - trace.truth_velocity_mps[selected]
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
def _wls_rms(trace) -> float:
truth = np.column_stack(
[
np.interp(
trace.wls_time_s,
trace.time_s,
trace.truth_velocity_mps[:, axis],
)
for axis in range(3)
]
)
error = trace.wls_velocity_mps - truth
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
def _board_raw_noise_scenario(name: str):
"""Match the 9.17 m/s raw-WLS RMS measured on Genesys2."""
scenario = _scenario(name)
nominal_noise = scenario.noise_multiplier
return replace(
scenario,
name=f"{scenario.name}_genesys2_noise",
noise_multiplier=lambda time_s, satellite: (
22.0 * nominal_noise(time_s, satellite)
),
)
class DopplerWlsTest(unittest.TestCase):
def test_four_state_wls_separates_velocity_and_clock_drift(self) -> None:
velocity = np.asarray([145.0, -32.0, 7.5])
clock_drift = 18.0
observation = -local_los_matrix() @ velocity + clock_drift
result = velocity_wls(observation, np.full(8, 45.0))
np.testing.assert_allclose(result.velocity_mps, velocity, atol=1.0e-10)
self.assertAlmostEqual(result.clock_drift_mps, clock_drift, places=10)
self.assertAlmostEqual(result.covariance_scale, 1.0)
def test_robust_wls_excludes_a_single_bad_channel(self) -> None:
velocity = np.asarray([120.0, 20.0, -2.0])
observation = -local_los_matrix() @ velocity + 4.0
observation[2] += 12.0
plain = velocity_wls(observation, np.full(8, 43.0))
robust = velocity_wls(observation, np.full(8, 43.0), robust=True)
self.assertEqual(robust.excluded_satellite, 2)
self.assertGreater(np.linalg.norm(plain.velocity_mps - velocity), 1.0)
np.testing.assert_allclose(robust.velocity_mps, velocity, atol=1.0e-9)
class AdaptiveAlphaTest(unittest.TestCase):
def test_motion_switches_alpha_and_quiet_state_recovers(self) -> None:
selector = AdaptiveAlpha()
for _ in range(80):
quiet = selector.update(np.zeros(3), 35.0)
self.assertAlmostEqual(quiet, 0.03)
dynamic_values = [
selector.update(np.asarray([4.0, 0.0, 0.0]), 35.0)
for _ in range(30)
]
self.assertGreaterEqual(max(dynamic_values), 0.40)
for _ in range(120):
recovered = selector.update(np.zeros(3), 40.0)
self.assertAlmostEqual(recovered, FIXED_DOPPLER_ALPHA)
class DopplerEskfIntegrationTest(unittest.TestCase):
def test_board_raw_noise_requires_adaptive_prefilter(self) -> None:
steady = run_doppler_eskf_scenario(
_board_raw_noise_scenario("steady_cruise"),
seed=7,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
airliner = run_doppler_eskf_scenario(
_board_raw_noise_scenario("airliner_maneuver"),
seed=3,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
aggressive = run_doppler_eskf_scenario(
_board_raw_noise_scenario("aggressive_maneuver"),
seed=7,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
steady_fixed = _fused_rms(steady[DopplerPath.FIXED_IIR])
steady_adaptive = _fused_rms(steady[DopplerPath.ADAPTIVE_IIR])
steady_raw = _fused_rms(steady[DopplerPath.RAW])
airliner_fixed = _fused_rms(airliner[DopplerPath.FIXED_IIR])
airliner_adaptive = _fused_rms(airliner[DopplerPath.ADAPTIVE_IIR])
airliner_raw = _fused_rms(airliner[DopplerPath.RAW])
aggressive_fixed = _fused_rms(aggressive[DopplerPath.FIXED_IIR])
aggressive_adaptive = _fused_rms(
aggressive[DopplerPath.ADAPTIVE_IIR]
)
aggressive_raw = _fused_rms(aggressive[DopplerPath.RAW])
# Direct 20 ms snapshots are too noisy at the level actually observed
# on the board. Adaptive bandwidth retains the quiet-state result and
# beats both direct raw and the 0.39 s fixed-IIR lag in manoeuvres.
self.assertLess(steady_adaptive, 1.05 * steady_fixed)
self.assertLess(steady_adaptive, 0.50 * steady_raw)
self.assertLess(airliner_adaptive, airliner_fixed)
self.assertLess(airliner_adaptive, 0.50 * airliner_raw)
self.assertLess(aggressive_adaptive, 0.65 * aggressive_fixed)
self.assertLess(aggressive_adaptive, 0.50 * aggressive_raw)
def test_eskf_filters_raw_doppler_without_pre_iir(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("steady_cruise"),
seed=7,
paths=(DopplerPath.RAW,),
)
raw = traces[DopplerPath.RAW]
self.assertLess(_fused_rms(raw), 0.45 * _wls_rms(raw))
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
def test_raw_path_avoids_fixed_iir_manoeuvre_lag(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("airliner_maneuver"),
seed=3,
paths=(DopplerPath.FIXED_IIR, DopplerPath.RAW),
)
fixed = traces[DopplerPath.FIXED_IIR]
raw = traces[DopplerPath.RAW]
self.assertLess(_fused_rms(raw), 0.35 * _fused_rms(fixed))
# The production 2/2/3 m/s adapter floors keep both paths inside the
# NIS gate; the fixed-IIR penalty now appears as lag error, not reject.
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
self.assertGreater(float(np.mean(fixed.wls_accepted)), 0.99)
def test_common_clock_step_does_not_leak_into_raw_velocity(self) -> None:
airliner = run_doppler_eskf_scenario(
_scenario("airliner_maneuver"),
seed=11,
paths=(DopplerPath.RAW,),
)[DopplerPath.RAW]
clock_step = run_doppler_eskf_scenario(
_scenario("clock_drift_steps"),
seed=11,
paths=(DopplerPath.RAW,),
)[DopplerPath.RAW]
np.testing.assert_allclose(
airliner.wls_velocity_mps,
clock_step.wls_velocity_mps,
atol=1.0e-10,
)
np.testing.assert_allclose(
airliner.fused_velocity_mps,
clock_step.fused_velocity_mps,
atol=1.0e-10,
)
def test_robust_wls_contains_channel_bursts_before_eskf(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("single_satellite_outliers"),
seed=5,
paths=(DopplerPath.RAW, DopplerPath.RAW_ROBUST),
)
raw = traces[DopplerPath.RAW]
robust = traces[DopplerPath.RAW_ROBUST]
self.assertLess(_wls_rms(robust), 0.65 * _wls_rms(raw))
self.assertGreater(float(np.mean(robust.excluded_satellites >= 0)), 0.02)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,182 @@
from __future__ import annotations
import unittest
import numpy as np
from gps_measurement_python.code_smoothing_model import run_smoothing_trial
from gps_measurement_python.firmware_wls_model import (
SigmaInputs,
SigmaMethod,
firmware_sigma_m,
)
from gps_measurement_python.measurement_scenarios import (
run_scenario,
scenario_matrix,
)
from gps_measurement_python.rtl_dll_model import (
CodeLoopFilterRtl,
quantized_code_epoch_measurement,
rtl_discriminator,
synthesize_dll_window,
)
class RtlDllModelTests(unittest.TestCase):
def test_discriminator_zero_and_q13_symmetry(self):
self.assertEqual(rtl_discriminator(0.0)[0], 0)
positive = rtl_discriminator(0.05)[1]
negative = rtl_discriminator(-0.05)[1]
self.assertAlmostEqual(positive, -negative, places=6)
self.assertLess(positive, 0.0)
def test_code_loop_integrator_accumulates_constant_error(self):
loop = CodeLoopFilterRtl()
first = loop.update(0.01)
twentieth = first
for _ in range(19):
twentieth = loop.update(0.01)
self.assertGreater(first, 0.0)
self.assertGreater(twentieth, first)
def test_dll_window_detects_excess_variance(self):
rng = np.random.default_rng(42)
nominal = synthesize_dll_window(
rng,
cn0_dbhz=45.0,
jitter_multiplier=1.0,
sample_count=4000,
)
noisy = synthesize_dll_window(
rng,
cn0_dbhz=45.0,
jitter_multiplier=3.0,
sample_count=4000,
)
self.assertAlmostEqual(nominal.variance_ratio, 1.0, delta=0.10)
self.assertAlmostEqual(noisy.variance_ratio, 9.0, delta=0.70)
def test_abs_sample_remainder_reconstructs_subsample_epoch(self):
for true_sample in (
12_345_678.314159,
12_345_678.0000001,
12_345_678.9999999,
12_345_678.0,
):
raw, remainder, corrected = quantized_code_epoch_measurement(true_sample)
self.assertEqual(raw, int(np.ceil(true_sample)))
self.assertLessEqual(
abs(corrected - true_sample),
0.5 / 1.023e6 + 1.0e-12,
)
self.assertGreaterEqual(remainder, 0)
class FirmwareSigmaTests(unittest.TestCase):
@staticmethod
def inputs(ratio: float = 1.0, mean_z: float = 0.0) -> SigmaInputs:
return SigmaInputs(
ura_index=np.asarray([0]),
elevation_deg=np.asarray([90.0]),
cn0_dbhz=np.asarray([45.0]),
dll_variance_ratio=np.asarray([ratio]),
dll_mean_z=np.asarray([mean_z]),
actual_jitter_multiplier=np.asarray([np.sqrt(ratio)]),
)
def test_current_matches_c_formula(self):
sigma = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
expected = np.sqrt(2.4**2 + 3.0**2 + 3.0**2)
self.assertAlmostEqual(sigma, expected, places=12)
def test_excess_method_does_not_double_count_nominal_dll_noise(self):
current = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
excess = firmware_sigma_m(self.inputs(), SigmaMethod.DLL_EXCESS)[0]
naive = firmware_sigma_m(self.inputs(), SigmaMethod.NAIVE_ADD_DLL)[0]
self.assertAlmostEqual(excess, current, places=12)
self.assertGreater(naive, current)
def test_excess_method_inflates_hidden_jitter(self):
current = firmware_sigma_m(self.inputs(9.0), SigmaMethod.CURRENT)[0]
excess = firmware_sigma_m(self.inputs(9.0), SigmaMethod.DLL_EXCESS)[0]
self.assertGreater(excess, 1.8 * current)
class ScenarioTests(unittest.TestCase):
def test_hidden_jitter_dynamic_sigma_improves_seeded_case(self):
scenario = next(
item
for item in scenario_matrix()
if item.name == "one_channel_hidden_jitter"
)
samples = run_scenario(
scenario,
17,
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
)
current = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
]
)
dynamic = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
]
)
self.assertLess(
np.sqrt(np.mean(dynamic * dynamic)),
0.90 * np.sqrt(np.mean(current * current)),
)
def test_stable_multipath_is_not_solved_by_variance_only(self):
scenario = next(
item
for item in scenario_matrix()
if item.name == "stable_multipath_bias"
)
samples = run_scenario(
scenario,
23,
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
)
current = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
]
)
dynamic = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
]
)
relative_difference = abs(
np.sqrt(np.mean(dynamic * dynamic))
/ np.sqrt(np.mean(current * current))
- 1.0
)
self.assertLess(relative_difference, 0.08)
def test_doppler_prediction_avoids_blind_iir_dynamic_lag(self):
result = run_smoothing_trial(31, doppler_cfo_mps=0.0)
self.assertLess(result.doppler_smoothed_rmse_m, result.raw_rmse_m)
self.assertLess(
result.doppler_peak_dynamic_error_m,
0.10 * result.scalar_iir_peak_dynamic_error_m,
)
self.assertGreater(
result.firmware_iir_doppler_peak_dynamic_error_m,
result.doppler_peak_dynamic_error_m,
)
if __name__ == "__main__":
unittest.main()
+29 -4
View File
@@ -5,12 +5,15 @@ TARGET := gps_system
# directory previously compiled every GPS_LOG call out and produced a silent
# UART image.
GPS_DEBUG ?= 1
GPS_DEBUG_LEVEL ?= 4
GPS_DEBUG_LEVEL ?= 1
GPS_BUILD_ID ?= $(shell git describe --always --dirty --abbrev=12 2>/dev/null || printf unknown)
# The current Genesys2 Rocket/fabric domain that clocks the UART is 50 MHz.
# Keep the requested 115200 baud physical rather than producing 57600 baud
# with the common 100 MHz fallback divider.
# The 50 Hz fused-location stream plus GPS diagnostics does not fit safely in
# the polling UART budget at 115200 baud.
BSP_UART_CLOCK_HZ ?= 50000000
BSP_UART_BAUDRATE ?= 460800
BSP_CLINT_TIMEBASE_HZ ?= 500000
# Select one sensor backend, or keep the default read-only WHO_AM_I probe.
# Accepted values: auto, tdk, st (stm is an alias for st).
@@ -42,6 +45,9 @@ endif
IMU_ACCEL_FS_G ?= 0
IMU_GYRO_FS_DPS ?= 0
GPS_ESKF_ENABLE ?= 1
# UART telemetry cadence only; ESKF prediction still runs for every IMU sample.
# At the default 50 Hz ODR, 5 samples produce one corrected record every 100 ms.
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES ?= 5
APP_SRCS := \
gps_system.c \
@@ -60,9 +66,20 @@ APP_SRCS := \
src/adapters/gps_fusion_publisher.c \
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 \
src/gps/gps_pps_time_filter.c \
src/gps/gps_tracking_measurement.c \
src/gps/linked_list.c \
src/gps/NavEquations.c \
src/gps/matrixLib.c
@@ -70,16 +87,24 @@ APP_SRCS := \
BSP_SRCS := init.c uart.c plic.c irq.c spi_flash.c pmod_spi.c
GPS_NAV_SOLVE_TIMER_TICKS ?= 10
GPS_PPS_STATUS_LOG_INTERVAL ?= 10
GPS_PPS_DOPPLER_PREDICTOR_ENABLE ?= 1
GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES ?= 0
include ../common/app.mk
CFLAGS += -Iheader
CFLAGS += $(IMU_BACKEND_CFLAG) \
-DGPS_BUILD_ID=\"$(GPS_BUILD_ID)\" \
-DIMU_ODR_HZ=$(IMU_ODR_HZ) \
-DIMU_ACCEL_FS_G=$(IMU_ACCEL_FS_G) \
-DIMU_GYRO_FS_DPS=$(IMU_GYRO_FS_DPS) \
-DGPS_ESKF_ENABLE=$(GPS_ESKF_ENABLE) \
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS)
-DIMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=$(IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES) \
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS) \
-DGPS_PPS_STATUS_LOG_INTERVAL=$(GPS_PPS_STATUS_LOG_INTERVAL) \
-DGPS_PPS_DOPPLER_PREDICTOR_ENABLE=$(GPS_PPS_DOPPLER_PREDICTOR_ENABLE) \
-DGPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES=$(GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES)
LDLIBS += -lm
ifeq ($(GPS_DEBUG),1)
File diff suppressed because it is too large Load Diff
+79 -20
View File
@@ -18,6 +18,33 @@
#error "GPS_ESKF_ENABLE must be 0 or 1"
#endif
#ifndef IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES
#define IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES 5u
#endif
#if (IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES < 1)
#error "IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES must be at least one"
#endif
#define IMU_SAMPLES_BEFORE_GPS_SERVICE 1u
#define IMU_SAMPLES_AFTER_GPS_SERVICE 3u
static void update_eskf_doppler_bandwidth(
const navigation_fusion_output_t* output)
{
if((output == NULL) || !output->local_valid)
{
return;
}
double alpha = 0.05;
if(output->motion_state == FUSION_MOTION_MOVING)
{
alpha = (output->linear_acceleration_norm_mps2 >= 2.5) ?
0.40 : 0.20;
}
gps_receiver_set_eskf_doppler_alpha(alpha);
}
static void initialize_sensor_fusion(gps_system_state_t* system)
{
system->fusion_enabled = (GPS_ESKF_ENABLE != 0);
@@ -43,18 +70,46 @@ static void initialize_sensor_fusion(gps_system_state_t* system)
true); /* Current prototype is the PX4-equivalent unarmed bench mode. */
}
static unsigned int feed_pending_imu_to_eskf(gps_system_state_t* system)
static unsigned int feed_pending_imu_to_eskf(
gps_system_state_t* system,
unsigned int max_samples)
{
if(!system->fusion_initialized)
if(!system->fusion_initialized || (max_samples == 0u))
{
return 0u;
}
unsigned int processed = 0u;
imu_sensor_service();
while(imu_sensor_take_measurement(&system->imu_sample))
while((processed < max_samples) &&
imu_sensor_take_measurement(&system->imu_sample))
{
(void)navigation_fusion_push_imu(&system->imu_sample);
bool accepted = navigation_fusion_push_imu(&system->imu_sample);
bool output_available = accepted &&
navigation_fusion_get_output(&system->fused_solution);
if(output_available)
{
update_eskf_doppler_bandwidth(&system->fused_solution);
}
if(output_available && system->fused_solution.global_valid &&
(system->fused_solution.timestamp_ticks !=
system->last_fused_location_tick))
{
system->fused_publish_sample_count++;
bool first_location =
(system->last_fused_location_tick == 0u);
bool publish_due =
(system->fused_publish_sample_count >=
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES);
if(first_location || publish_due)
{
gps_system_uart_publish_fused_location(
&system->fused_solution);
system->last_fused_location_tick =
system->fused_solution.timestamp_ticks;
system->fused_publish_sample_count = 0u;
}
}
processed++;
}
return processed;
@@ -76,23 +131,36 @@ int main(void)
gps_receiver_start_acquisition();
gps_system_uart_publish_startup(
system.fusion_enabled, system.fusion_initialized,
system.imu_initialized, system.imu_running);
system.imu_initialized, system.imu_running,
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES);
bool imu_backlog_may_remain = false;
for(;;)
{
/* GPS, IMU data-ready and the maintenance timer wake this loop. */
asm volatile("wfi");
/* Skip WFI only while draining a bounded IMU backlog. Every batch
* still services GPS before more prediction work, so a continuously
* non-empty IMU queue cannot starve navigation epochs. */
if(!imu_backlog_may_remain)
{
/* GPS, IMU data-ready and the maintenance timer wake this loop. */
asm volatile("wfi");
}
/* IMU prediction runs at the sensor's own timestamped rate. */
(void)feed_pending_imu_to_eskf(&system);
/* Limit pre-GPS work to one sample for low event latency. */
(void)feed_pending_imu_to_eskf(
&system, IMU_SAMPLES_BEFORE_GPS_SERVICE);
/* GPS remains an independent receiver: events first, then coherent WLS. */
gps_receiver_process_pending_events();
system.gps_epoch_ready = gps_receiver_solve_ready_epoch(
&system.gps_solution, &system.gps_measurement_ticks);
/* WLS may take long enough for more IMU samples to arrive. */
(void)feed_pending_imu_to_eskf(&system);
/* WLS may take long enough for more IMU samples to arrive. Drain a
* small batch, then return to GPS service before continuing. */
unsigned int imu_after_gps = feed_pending_imu_to_eskf(
&system, IMU_SAMPLES_AFTER_GPS_SERVICE);
imu_backlog_may_remain =
(imu_after_gps == IMU_SAMPLES_AFTER_GPS_SERVICE);
if(system.gps_epoch_ready && system.gps_solution.solution_valid)
{
@@ -114,15 +182,6 @@ int main(void)
/* Delayed GPS update is applied at its capture timestamp; newer
* IMU history is replayed to produce the current fused state. */
(void)navigation_fusion_push_gps(&system.gps_for_fusion);
if(navigation_fusion_get_output(&system.fused_solution) &&
system.fused_solution.global_valid)
{
gps_system_uart_publish_location("imu_corrected",
system.fused_solution.latitude_deg,
system.fused_solution.longitude_deg,
system.fused_solution.height_m,
&system.gps_solution);
}
}
}
@@ -14,11 +14,15 @@ void gps_system_uart_publish_location(
double height_m,
const gps_nav_solution_t* gps);
void gps_system_uart_publish_fused_location(
const navigation_fusion_output_t* output);
void gps_system_uart_publish_startup(
bool fusion_enabled,
bool fusion_initialized,
bool imu_initialized,
bool imu_running);
bool imu_running,
unsigned int fused_publish_every_samples);
void gps_system_uart_publish_periodic_fusion_status(
navigation_fusion_output_t* output,
@@ -27,6 +27,7 @@ typedef struct {
double gravity_mps2;
double max_propagation_step_s;
double position_nis_gate;
double velocity_nis_gate;
double covariance_diagonal_floor;
} eskf_config_t;
@@ -40,9 +41,12 @@ typedef struct {
double last_linear_accel_nav_mps2[ESKF_VECTOR_DIM];
double last_unbiased_gyro_rps[ESKF_VECTOR_DIM];
double last_position_nis;
double last_velocity_nis;
uint64_t propagation_count;
uint64_t position_update_accepted_count;
uint64_t position_update_rejected_count;
uint64_t velocity_update_accepted_count;
uint64_t velocity_update_rejected_count;
bool initialized;
} eskf_t;
@@ -84,6 +88,13 @@ eskf_update_result_t eskf_position_update(
const double covariance_m2[ESKF_VECTOR_DIM * ESKF_VECTOR_DIM],
double* nis_out);
eskf_update_result_t eskf_velocity_update(
eskf_t* filter,
const eskf_config_t* config,
const double velocity_mps[ESKF_VECTOR_DIM],
const double covariance_mps2[ESKF_VECTOR_DIM * ESKF_VECTOR_DIM],
double* nis_out);
/* Re-anchor position after a higher-level fusion timeout has established that
* repeated, valid position observations are mutually coherent. Velocity,
* attitude and IMU biases are preserved; position cross-covariances are
@@ -80,6 +80,9 @@ typedef struct {
uint64_t position_pending_overflow;
uint64_t position_accepted;
uint64_t position_recovery_count;
uint64_t velocity_received;
uint64_t velocity_nis_rejected;
uint64_t velocity_accepted;
uint64_t replay_count;
uint64_t replayed_imu_count;
uint64_t zero_velocity_update_count;
@@ -104,7 +107,7 @@ typedef struct {
fusion_runtime_t checkpoint;
fusion_calibration_accumulator_t calibration;
fusion_imu_measurement_t imu_history[FUSION_IMU_HISTORY_CAPACITY];
fusion_position_measurement_t pending_positions[FUSION_POSITION_PENDING_CAPACITY];
fusion_navigation_measurement_t pending_positions[FUSION_POSITION_PENDING_CAPACITY];
fusion_position_measurement_t initial_position;
fusion_position_measurement_t position_recovery_candidate;
fusion_engine_diagnostics_t diagnostics;
@@ -133,6 +136,13 @@ bool fusion_engine_push_position(
const fusion_position_measurement_t* measurement,
eskf_update_result_t* update_result_out,
double* nis_out);
bool fusion_engine_push_navigation(
fusion_engine_t* engine,
const fusion_navigation_measurement_t* measurement,
eskf_update_result_t* position_update_result_out,
double* position_nis_out,
eskf_update_result_t* velocity_update_result_out,
double* velocity_nis_out);
bool fusion_engine_get_runtime(const fusion_engine_t* engine,
fusion_runtime_t* runtime_out);
@@ -9,6 +9,9 @@ typedef struct {
double minimum_horizontal_std_m;
double minimum_vertical_std_m;
double maximum_position_std_m;
double minimum_horizontal_velocity_std_mps;
double minimum_vertical_velocity_std_mps;
double maximum_velocity_std_mps;
} fusion_gps_adapter_config_t;
typedef struct {
@@ -27,6 +30,10 @@ bool fusion_gps_adapter_init(fusion_gps_adapter_t* adapter,
bool fusion_gps_adapter_convert(fusion_gps_adapter_t* adapter,
const fusion_gps_measurement_t* gps,
fusion_position_measurement_t* position_out);
bool fusion_gps_adapter_convert_navigation(
fusion_gps_adapter_t* adapter,
const fusion_gps_measurement_t* gps,
fusion_navigation_measurement_t* navigation_out);
bool fusion_gps_adapter_local_to_geodetic(
const fusion_gps_adapter_t* adapter,
const double position_enu_m[3],
@@ -21,8 +21,13 @@ typedef struct {
double vacc_m;
double residual_rms_m;
double chi_square_ratio;
double velocity_ecef_mps[3];
double velocity_covariance_ecef_mps2[3][3];
double speed_mps;
double velocity_residual_rms_mps;
unsigned int used_satellites;
bool solution_valid;
bool velocity_valid;
} fusion_gps_measurement_t;
/* Timestamped IMU plus the independently supervised platform state consumed
@@ -45,4 +50,16 @@ typedef struct {
bool source_valid;
} fusion_position_measurement_t;
/*
* Position and Doppler velocity from one GNSS solve. Keeping them in one
* message makes the fixed-lag coordinator apply both observations at exactly
* the captured GPS epoch before replaying newer IMU samples.
*/
typedef struct {
fusion_position_measurement_t position;
double velocity_enu_mps[3];
double velocity_covariance_enu_mps2[9];
bool velocity_valid;
} fusion_navigation_measurement_t;
#endif
@@ -11,6 +11,7 @@
typedef struct {
uint64_t timestamp_ticks;
uint64_t last_accepted_gps_ticks;
double position_enu_m[3];
double velocity_enu_mps[3];
double latitude_deg;
@@ -18,11 +19,15 @@ typedef struct {
double height_m;
double position_std_m[3];
double speed_mps;
double linear_acceleration_norm_mps2;
double last_position_nis;
double last_velocity_nis;
fusion_motion_state_t motion_state;
uint64_t gps_accepted;
uint64_t gps_nis_rejected;
uint64_t gps_position_recoveries;
uint64_t gps_velocity_accepted;
uint64_t gps_velocity_nis_rejected;
uint64_t zero_update_count;
uint64_t replay_count;
uint64_t stale_gps_count;
@@ -53,8 +58,9 @@ void navigation_fusion_set_platform_landed(
* ESKF observation and is never rate-selected in this layer. */
bool navigation_fusion_push_imu(const imu_sample_t* measurement);
/* Observation input: convert a valid LLH fix to local ENU, apply it at the
* captured GPS epoch, then replay newer IMU history to current time. */
/* Observation input: convert a valid LLH fix and optional Doppler velocity to
* local ENU, apply both at the captured GPS epoch, then replay newer IMU
* history to current time. */
bool navigation_fusion_push_gps(const fusion_gps_measurement_t* measurement);
/* local_valid can exist without GPS. global_valid/valid require a GPS origin. */
@@ -3,11 +3,18 @@
#include "gps.h"
#include "gps/gps_ip_defines.h"
#include "gps/gps_nav_epoch.h"
#include "gps/matrixLib.h"
typedef struct gps_nav_solution {
double ecef_m[3];
double clock_bias_m;
double clock_bias_variance_m2;
double velocity_ecef_mps[3];
double velocity_covariance_ecef_mps2[3][3];
double clock_drift_mps;
double speed_mps;
double velocity_residual_rms_mps;
double latitude_deg;
double longitude_deg;
double height_m;
@@ -24,6 +31,13 @@
double postfit_residual_m[GPS_TRCK_IP_COUNT];
double elevation_deg[GPS_TRCK_IP_COUNT];
double measurement_sigma_m[GPS_TRCK_IP_COUNT];
double cn0_dbhz[GPS_TRCK_IP_COUNT];
/* Raw Doppler is published with the ESKF velocity solution. */
double doppler_hz[GPS_TRCK_IP_COUNT];
double pseudorange_rate_mps[GPS_TRCK_IP_COUNT];
/* Separate slow path used only by the PPS bias-drift predictor. */
double filtered_doppler_hz[GPS_TRCK_IP_COUNT];
double filtered_pseudorange_rate_mps[GPS_TRCK_IP_COUNT];
double full_postfit_residual_m[GPS_TRCK_IP_COUNT];
double full_elevation_deg[GPS_TRCK_IP_COUNT];
double full_measurement_sigma_m[GPS_TRCK_IP_COUNT];
@@ -43,6 +57,7 @@
unsigned int degrees_of_freedom;
uint32_t used_row_mask;
uint32_t excluded_row_mask;
uint32_t velocity_excluded_row_mask;
uint64_t candidate_cycles;
uint64_t full_solution_cycles;
uint64_t fde_cycles;
@@ -54,10 +69,12 @@
bool chi_square_pass;
bool fde_applied;
bool solution_valid;
bool velocity_valid;
bool velocity_fde_applied;
} gps_nav_solution_t;
void satpos(gps_ephemeris_t* eph, double* pos, double* vel, double* clk, double* clockCorr);
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, mat_type *satellitePositions, mat_type * velocities);
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, double* clockDrift, mat_type *satellitePositions, mat_type * velocities);
double check_t(long double tkl);
void subVect(double* y, const double* x1, const double* x2);
double normVect(const double* x);
@@ -65,8 +82,10 @@
bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber,
mat_type * az, mat_type * el,
const unsigned char* ura_indices,
const double* cn0_dbhz,
const ionoutc_t* ionoutc, double gps_tow,
gps_nav_solution_t* solution_out);
gps_nav_solution_t* solution_out);
void gps_wls_seed_position(const double receiver_ecef_m[3]);
void rotateSatelliteSTD(double const traveltime, const mat_type* X_sat, mat_type* X_sat_rot);
void topocent(const double* XPtr, const double* dxPtr, int size, t_phi_Lambda_h * phi_Lambda_h, t_topocentData* topcent);
void togeod(double a, double finv, double X, double Y, double Z, t_phi_Lambda_h * phi_Lambda_h, t_topocentData* topcent);
@@ -37,8 +37,21 @@
#define WN_INDEX 2
#define POSITION_VECTOR_COL 3
#define MAX_24_BIT_INT 0x00800000
// ADC sample frekansi
#define SAMPLES_PER_CODE 16368.0
/*
* RF ADC sample rate used for time and pseudorange conversion.
*
* The acquisition RTL buffers an integer 16368 samples per nominal C/A
* code period, but the Genesys2/VCU118 RF reference is 16.367981 MHz.
* Using 16368.000 samples/ms as a frequency creates a 1.161 ppm timing
* ramp which is reset by every six-second HOW anchor (about 6.97 us
* peak-to-peak). Keep the physical clock rate distinct from the integer
* acquisition-buffer length.
*/
#ifndef GPS_RF_SAMPLE_RATE_HZ
#define GPS_RF_SAMPLE_RATE_HZ 16367981.0
#endif
#define GPS_RF_SAMPLES_PER_MILLISECOND (GPS_RF_SAMPLE_RATE_HZ / 1000.0)
#define SAMPLES_PER_CODE GPS_RF_SAMPLES_PER_MILLISECOND
#define CODE_FREQ_BASIS 1.023E+6
@@ -0,0 +1,102 @@
#ifndef GPS_ACQ_MANAGER_H
#define GPS_ACQ_MANAGER_H
#include <stdbool.h>
#include <stdint.h>
#define GPS_ACQ_MANAGER_PRN_COUNT 32u
#define GPS_ACQ_MANAGER_INVALID_PRN 0xffu
typedef enum {
GPS_ACQ_PRN_DISABLED = 0,
GPS_ACQ_PRN_COLD,
GPS_ACQ_PRN_HOT,
GPS_ACQ_PRN_ACQUIRING,
GPS_ACQ_PRN_ACQUIRED,
GPS_ACQ_PRN_TRACKING,
GPS_ACQ_PRN_STALE
} gps_acq_prn_state_t;
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;
typedef struct {
gps_acq_prn_state_t state;
uint64_t last_attempt_tick;
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 {
gps_acq_manager_config_t config;
gps_acq_prn_entry_t prn[GPS_ACQ_MANAGER_PRN_COUNT];
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(
gps_acq_manager_t* manager,
const gps_acq_manager_config_t* config);
bool gps_acq_manager_enable_cold(
gps_acq_manager_t* manager,
unsigned int prn);
gps_acq_prn_state_t gps_acq_manager_state(
const gps_acq_manager_t* manager,
unsigned int prn);
const char* gps_acq_manager_state_name(gps_acq_prn_state_t state);
void gps_acq_manager_mark_acquired(
gps_acq_manager_t* manager,
unsigned int prn);
void gps_acq_manager_mark_tracking(
gps_acq_manager_t* manager,
unsigned int prn);
void gps_acq_manager_mark_stale(
gps_acq_manager_t* manager,
unsigned int prn);
void gps_acq_manager_mark_lost(
gps_acq_manager_t* manager,
unsigned int prn,
uint64_t now);
void gps_acq_manager_mark_failed(
gps_acq_manager_t* manager,
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,
unsigned int* prn_out,
bool* hot_out);
unsigned int gps_acq_manager_count_state(
const gps_acq_manager_t* manager,
gps_acq_prn_state_t state);
unsigned int gps_acq_manager_count_searchable(
const gps_acq_manager_t* manager);
#endif
@@ -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
@@ -0,0 +1,68 @@
#ifndef GPS_EPHEMERIS_CACHE_H
#define GPS_EPHEMERIS_CACHE_H
#include <stdbool.h>
#include <stdint.h>
#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;
typedef struct {
gps_ephemeris_cache_entry_t entry[GPS_PRN_COUNT];
uint64_t max_age_ticks;
} gps_ephemeris_cache_t;
void gps_ephemeris_cache_init(
gps_ephemeris_cache_t* cache,
uint64_t max_age_ticks);
bool gps_ephemeris_cache_store(
gps_ephemeris_cache_t* cache,
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);
#endif
@@ -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
@@ -2,20 +2,42 @@
#define GPS_INT_HAND
#include "gps/gps_ip_defines.h"
#include "gps/gps_tracking_measurement.h"
typedef struct {
uint32_t event_overflow_total;
uint32_t plic_drain_limit_total;
uint32_t plic_repeat_total;
uint32_t nav_epoch_coalesced_total;
uint32_t pps_sequence_discontinuity_total;
uint32_t nav_epoch_incomplete_total;
uint32_t nav_epoch_snapshot_overwrite_total;
uint32_t how_anchor_rejected_total;
uint32_t measurement_missed_total;
uint32_t measurement_invalid_total;
uint32_t satellite_connected_total;
uint32_t satellite_lost_total;
uint32_t ephemeris_cache_store_total;
uint32_t ephemeris_cache_restore_total;
uint32_t last_connected_prn;
uint32_t last_lost_prn;
uint64_t last_connected_tick;
uint64_t last_lost_tick;
} gps_runtime_diagnostics_t;
typedef struct {
uint32_t channel_mask;
uint32_t prn[GPS_TRCK_IP_COUNT];
gps_ephemeris_t ephemeris[GPS_TRCK_IP_COUNT];
gpstime_t tow[GPS_TRCK_IP_COUNT];
uint32_t raw_sample[GPS_TRCK_IP_COUNT];
double corrected_abs_sample[GPS_TRCK_IP_COUNT];
uint64_t pps_counter[GPS_TRCK_IP_COUNT];
uint32_t pps_capture_sequence[GPS_TRCK_IP_COUNT];
uint64_t local_timestamp_ticks[GPS_TRCK_IP_COUNT];
gps_tracking_measurement_t measurement[GPS_TRCK_IP_COUNT];
} gps_nav_epoch_snapshot_t;
typedef void (*gps_external_irq_handler_t)(void);
bool assign_tracking_from_result(const gps_acq_result_t* result);
@@ -26,10 +48,20 @@
gps_external_irq_handler_t handler);
void gps_irq_handler(void) GPS_IRQ_HANDLER_ATTR;
void gps_process_isr_events(void);
void gps_acq_configure_prns(
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_solve_period_ready(void);
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot);
void gps_get_runtime_diagnostics(gps_runtime_diagnostics_t* diagnostics);
void gps_tracking_set_eskf_doppler_alpha(double alpha);
double gps_tracking_get_eskf_doppler_alpha(void);
void trck_intr_handler(volatile gps_trck_t* const trck_ip, volatile gps_frame_t *gps_frame_parsed, volatile int unsigned gps_subframe[10], unsigned char trck_ip_no);
@@ -9,7 +9,11 @@
#include "gps/matrixLib.h"
#include "gps/gps_macros.h"
#ifdef __riscv
#define GPS_IRQ_HANDLER_ATTR __attribute__((interrupt("machine"), aligned(4)))
#else
#define GPS_IRQ_HANDLER_ATTR
#endif
@@ -18,7 +22,6 @@
// | || __/ | |_| | |___| _| | || |\ | |___ ___) |
// |___|_| |____/|_____|_| |___|_| \_|_____|____/
#define MIN_NEEDED_SATELLITE 4
#define GPS_TRCK_UNLOCK_TIMEOUT_TICKS 500000000u
#define GPS_PRN_COUNT 32u
#define GPS_PRN_INVALID 0xffu
@@ -43,13 +46,20 @@
#define GPS_ACQ_IP_BASE_ADDR(index) (BSP_GPS_ACQ_BASE + ((uintptr_t)(index) * BSP_GPS_ACQ_STRIDE))
#define GPS_ACQ_INTR_LINE(index) (BSP_GPS_ACQ_IRQ + ((unsigned int)(index) * BSP_GPS_ACQ_IRQ_STRIDE))
#define GPS_EVENT_QUEUE_SIZE 64u
#define GPS_SYSTICK_HZ 20u
/* Genesys2 CLINT is 500 kHz. Keep foreground maintenance at
* 10 Hz; the tracking IP's 20 ms NAV timer is independent. */
#define GPS_SYSTICK_HZ 10u
/* LNAV transmits only week modulo 1024. Products built for the
* current constellation era use the 2019 rollover epoch until a
* saved full week is available. */
#define GPS_LNAV_WEEK_ERA_BASE 2048
#define GPS_NAV_SOLVE_TIMER_TICK_MS 20u
#ifndef GPS_NAV_SOLVE_TIMER_TICKS
#define GPS_NAV_SOLVE_TIMER_TICKS 50u
#endif
#define GPS_NAV_BITS_PER_WEEK 30240000u
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
#define GPS_NAV_BITS_PER_SUBFRAME 300u
/* Keep integrity statistics active, but do not let leave-one-out FDE
* replace the full-WLS position until the real measurement-noise model
* is calibrated. The live stationary test produced a 22.7% exclusion
@@ -57,20 +67,56 @@
#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_STUCK_WARN_TICKS (GPS_SYSTICK_HZ * 2u)
/* 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
#if (GPS_NAV_SOLVE_TIMER_TICKS == 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
#elif ((GPS_NAV_BITS_PER_WEEK % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide 30240000 GPS navigation bits per week"
#elif ((GPS_NAV_BITS_PER_SUBFRAME % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide the 300-bit LNAV subframe"
#endif
_Static_assert((GPS_NAV_ENABLE_FDE == 0u) || (GPS_NAV_ENABLE_FDE == 1u),
"GPS_NAV_ENABLE_FDE must be zero or one");
_Static_assert(GPS_ACQ_MIN_SKY_MODEL_VISIBLE_COUNT <= GPS_PRN_COUNT,
"sky-model visible count threshold exceeds PRN count");
#if (ACQ_ENGINE_COUNT < 1UL)
#error "ACQ_ENGINE_COUNT must be at least 1"
@@ -123,6 +169,8 @@
#define GPS_TRCK_NAV_SOLVE_EN_POS 10
#define GPS_TRCK_NAV_SOLVE_INTR_EN_POS 11
#define GPS_TRCK_NAV_SOLVE_FLAG_EN_POS 12 // (Default 1)
#define GPS_TRCK_MEASUREMENT_INTR_EN_POS 13
#define GPS_TRCK_MEASUREMENT_FLAG_EN_POS 14
// acq ip status reg bits.
@@ -137,6 +185,18 @@
#define GPS_TRCK_NAV_BITS_RCV_POS 4
#define GPS_TRCK_STOP_UNLOCK_POS 5
#define GPS_TRCK_NAV_SOLVE_FLAG_POS 6
#define GPS_TRCK_MEASUREMENT_FLAG_POS 7
#define GPS_TRCK_MEAS_META_VALID_POS 0
#define GPS_TRCK_MEAS_META_NAV_BIT_POS 1
#define GPS_TRCK_MEAS_META_LOCK_POS 2
#define GPS_TRCK_MEAS_META_COUNT_SHIFT 8
#define GPS_TRCK_MEAS_META_COUNT_MASK 0x1fu
#define GPS_TRCK_MEAS_META_VERSION_SHIFT 24
#define GPS_TRCK_MEAS_META_VERSION 1u
#define GPS_TRCK_CARRIER_Q7_SCALE 128.0
#define GPS_TRCK_IF_CENTER_HZ 4092000.0
#define GPS_L1_CARRIER_HZ 1575420000.0
@@ -145,7 +205,10 @@
// gps defines from plutogpssim.
#define POSTPONE_TOW (double)(0.160)
/* Current tracking RTL validates the subframe on the ninth following
* navigation bit: 9 * 20 ms = 180 ms. satpos() and the PPS receive-time
* anchor must use this same delivery-epoch correction exactly once. */
#define POSTPONE_TOW (double)(0.180)
#define MAX_SAT (12) // 32 (TODO. OBSOLATED)
#define SECONDS_IN_WEEK 604800.0
@@ -155,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)
@@ -215,19 +276,19 @@
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
int32_t MEASUREMENT_CARRIER_Q7; // 'h58
uint32_t MEASUREMENT_ABS_SAMPLE; // 'h5c
uint32_t MEASUREMENT_REM_CODE_PHASE; // 'h60
int32_t MEASUREMENT_SUM_I; // 'h64
int32_t MEASUREMENT_SUM_Q; // 'h68
uint32_t MEASUREMENT_WBP_LO; // 'h6c
uint32_t MEASUREMENT_WBP_HI; // 'h70
}gps_trck_t;
typedef enum {
GPS_SAT_NOT_SEEN = 0,
GPS_SAT_ACQUIRING,
GPS_SAT_ACQUIRED,
GPS_SAT_TRACKING,
GPS_SAT_LOST,
GPS_SAT_STALE
} gps_satellite_state_t;
typedef enum {
GPS_ACQ_ENGINE_IDLE = 0,
GPS_ACQ_ENGINE_BUSY
@@ -254,6 +315,10 @@
_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");
#endif
@@ -436,8 +501,13 @@
void trck_enable_nav_bits_flag(volatile gps_trck_t* trck_ip); // enable nav bits internal flag generation.
void trck_enable_nav_bits_intr(volatile gps_trck_t* trck_ip); // enable nav bits intr generation.
void trck_disable_nav_bits_intr(volatile gps_trck_t* trck_ip); // disable nav bits intr generation.
void trck_enable_measurement_intr(volatile gps_trck_t* trck_ip);
void trck_disable_measurement_intr(volatile gps_trck_t* trck_ip);
void trck_enable_measurement_flag(volatile gps_trck_t* trck_ip);
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.
@@ -454,11 +524,16 @@
uint64_t gps_get_systick(void);
static inline uint64_t gps_read_cycle_counter(void)
{
#ifdef __riscv
uint64_t cycles;
asm volatile("rdcycle %0" : "=r"(cycles));
return cycles;
#else
return 0u;
#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);
@@ -5,8 +5,8 @@
/*
* Verbosity is cumulative:
* 1: navigation result (position and all DOP values) plus tracked
* satellite count/PRN summary
* 1: navigation result (position and all DOP values), tracked
* satellite summary and low-rate acquisition/reacquisition status
* 2: reserved for additional status detail
* 3: state transitions, warnings/errors and integrity summary
* 4: performance, NAV satellite and low-level diagnostic detail
@@ -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
@@ -4,6 +4,15 @@
#include <stdbool.h>
#include <stdint.h>
/*
* Four pseudoranges are the algebraic minimum for receiver x/y/z plus clock
* bias. They leave no residual degree of freedom, so a bad measurement can
* produce an arbitrary position while fitting perfectly. The raw receiver
* still exposes that minimum WLS fix and its covariance; the separate
* residual/integrity flags tell downstream fusion that no redundancy exists.
*/
#define GPS_NAV_MIN_WLS_SATELLITES 4u
/*
* The current receiver exposes nine tracking channels. Keep the epoch
* selector bounded so its exhaustive, allocation-free search has a fixed
@@ -11,6 +20,10 @@
*/
#define GPS_NAV_EPOCH_MAX_CANDIDATES 9u
_Static_assert(GPS_NAV_MIN_WLS_SATELLITES <=
GPS_NAV_EPOCH_MAX_CANDIDATES,
"GPS WLS minimum exceeds epoch capacity");
typedef struct {
int week;
double tow_sec;
@@ -24,7 +37,10 @@ typedef struct {
} gps_nav_epoch_selection_t;
typedef struct {
uint64_t epoch_id;
uint64_t first_capture_counter;
uint64_t minimum_capture_counter;
uint64_t maximum_capture_counter;
uint64_t deadline_local_ticks;
uint32_t expected_mask;
uint32_t received_mask;
bool active;
@@ -35,6 +51,7 @@ typedef enum {
GPS_NAV_EPOCH_EVENT_ACCEPTED,
GPS_NAV_EPOCH_EVENT_DUPLICATE,
GPS_NAV_EPOCH_EVENT_STALE,
GPS_NAV_EPOCH_EVENT_OUTLIER,
GPS_NAV_EPOCH_EVENT_RESTARTED,
GPS_NAV_EPOCH_EVENT_COMPLETE
} gps_nav_epoch_event_result_t;
@@ -46,19 +63,43 @@ bool gps_nav_epoch_select(
double tow_tolerance_sec,
gps_nav_epoch_selection_t* selection);
bool gps_nav_epoch_id(
int week,
double tow_sec,
unsigned int period_ms,
uint64_t* epoch_id);
/*
* Receiver publication is stricter than diagnostic clustering: every
* candidate in the frozen ready mask must belong to one coherent epoch.
* selection is still populated with the largest cluster on failure so the
* caller can report why the complete epoch was rejected.
*/
bool gps_nav_epoch_validate_complete(
const gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES],
unsigned int candidate_count,
uint32_t sample_window,
double tow_tolerance_sec,
gps_nav_epoch_selection_t* selection);
/*
* Validate a newest-only hardware mailbox after one or more capture events
* were coalesced. sequence_gap receives the number of real elapsed epochs;
* missing epochs are never synthesized.
*/
bool gps_nav_capture_sequence_consistent(
uint64_t previous_counter,
uint32_t previous_sequence,
uint64_t current_counter,
uint32_t current_sequence,
uint64_t capture_period_ticks,
uint32_t* sequence_gap);
void gps_nav_epoch_assembly_reset(gps_nav_epoch_assembly_t* assembly);
gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
gps_nav_epoch_assembly_t* assembly,
uint64_t epoch_id,
uint64_t capture_counter,
uint64_t local_timestamp_ticks,
unsigned int channel,
uint32_t ready_mask);
uint32_t ready_mask,
uint64_t capture_period_ticks,
uint64_t sync_window_ticks,
uint64_t holdoff_local_ticks);
bool gps_nav_epoch_assembly_remove_channel(
gps_nav_epoch_assembly_t* assembly,
@@ -67,4 +108,12 @@ bool gps_nav_epoch_assembly_remove_channel(
bool gps_nav_epoch_assembly_complete(
const gps_nav_epoch_assembly_t* assembly);
bool gps_nav_epoch_assembly_usable(
const gps_nav_epoch_assembly_t* assembly,
unsigned int minimum_channels);
bool gps_nav_epoch_assembly_expired(
const gps_nav_epoch_assembly_t* assembly,
uint64_t now_local_ticks);
#endif
@@ -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
+30 -1
View File
@@ -4,11 +4,40 @@
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_pps_quality.h"
#include "gps/gps_pps_time_filter.h"
#define GPS_PPS_CLOCK_HZ 125000000u
typedef struct {
uint32_t status;
uint32_t active_sequence;
uint32_t pulse_count;
int64_t phase_error_q32;
int64_t integrator_q32;
int64_t rate_word_q32;
} gps_pps_diagnostics_t;
void gps_pps_init(unsigned int nav_solve_tick);
void gps_pps_service(void);
bool gps_pps_is_locked(void);
bool gps_pps_set_loop_profile(gps_pps_profile_t profile);
void gps_pps_set_holdover(bool holdover);
void gps_pps_reset_time_filter(void);
bool gps_pps_read_diagnostics(gps_pps_diagnostics_t* diagnostics);
bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* sequence);
bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiver_tow_sec);
gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
uint64_t epoch_counter,
double measured_clock_bias_m,
bool clock_drift_valid,
double measured_clock_drift_mps,
double tdop,
double* filtered_clock_bias_m_out,
double* innovation_m_out);
bool gps_pps_commit_solution(
uint64_t epoch_counter,
int gps_week,
double receiver_tow_sec,
bool force_coarse);
#endif
@@ -0,0 +1,87 @@
#ifndef GPS_PPS_QUALITY_H
#define GPS_PPS_QUALITY_H
#include <stdbool.h>
#include <stdint.h>
#define GPS_PPS_BASELINE_UPDATE_PERIOD_SEC 0.200
typedef enum {
GPS_PPS_PROFILE_HOLDOVER = 0,
GPS_PPS_PROFILE_ROBUST,
GPS_PPS_PROFILE_PRECISION,
GPS_PPS_PROFILE_ACQUIRE
} gps_pps_profile_t;
typedef struct {
double tdop;
double gdop;
double clock_bias_sigma_m;
double mean_cn0_dbhz;
double minimum_cn0_dbhz;
double chi_square_ratio;
double velocity_residual_rms_mps;
unsigned int used_satellites;
bool solution_valid;
bool residual_test_valid;
bool velocity_valid;
bool fde_applied;
} gps_pps_quality_metrics_t;
typedef struct {
gps_pps_profile_t profile;
gps_pps_profile_t candidate;
unsigned int candidate_updates;
unsigned int upgrade_updates;
unsigned int degrade_updates;
} gps_pps_quality_selector_t;
typedef struct {
double natural_frequency_hz;
double damping;
double max_slew_ppm;
unsigned int lock_acquire_ticks;
unsigned int lock_release_ticks;
unsigned int lock_acquire_updates;
double software_bias_gain;
unsigned int software_settle_updates;
} gps_pps_loop_profile_t;
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector);
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec);
/*
* Select a bounded loop profile from timing geometry, code/carrier quality
* and integrity. Upgrades and degradations require sustained evidence; their
* update counts are scaled from the proven 200 ms timing when period-aware
* initialization is used. A single rejected WLS epoch is skipped without
* resetting the loop; sustained loss enters holdover, while the independent
* reference watchdog remains the final stale-data guard.
*/
gps_pps_profile_t gps_pps_quality_selector_update(
gps_pps_quality_selector_t* selector,
const gps_pps_quality_metrics_t* metrics,
bool* profile_changed_out);
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile);
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec);
/*
* Map the proven 200 ms discrete poles to another update period. The pole
* exponentiation preserves the current loop's continuous-time settling and
* damping while returning exactly the legacy coefficients at 200 ms.
*/
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out);
const char* gps_pps_profile_name(gps_pps_profile_t profile);
#endif
@@ -0,0 +1,85 @@
#ifndef GPS_PPS_TIME_FILTER_H
#define GPS_PPS_TIME_FILTER_H
#include <stdbool.h>
#include <stdint.h>
typedef enum {
GPS_PPS_TIME_FILTER_REJECTED = 0,
GPS_PPS_TIME_FILTER_COARSE,
GPS_PPS_TIME_FILTER_TRACKING
} gps_pps_time_filter_result_t;
typedef struct {
uint64_t epoch_counter;
uint64_t drift_anchor_counter;
double clock_bias_m;
double clock_drift_mps;
double previous_measurement_m;
double drift_anchor_measurement_m;
unsigned int accepted_updates;
bool initialized;
bool drift_initialized;
} gps_pps_time_filter_t;
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter);
/*
* Keep the current WLS datum satellite while it remains in the coherent
* snapshot. Fall back to the earliest capture only when that satellite is
* gone. Stable selection avoids artificial clock-bias jumps as satellites
* enter the solution.
*/
bool gps_pps_select_reference(
const unsigned int satellite_ids[],
const double capture_times[],
unsigned int count,
bool preferred_valid,
unsigned int preferred_id,
unsigned int* reference_index_out);
/*
* The position WLS observations are formed from 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(
const double receiver_ecef_m[3],
const double receiver_velocity_ecef_mps[3],
const double reference_satellite_ecef_m[3],
const double reference_satellite_velocity_ecef_mps[3],
double reference_satellite_clock_drift_sps,
double* clock_bias_drift_mps_out);
/*
* Smooth the pseudorange WLS receiver-clock bias without lagging its large
* linear oscillator drift. The Doppler WLS clock-drift state predicts the
* bias at the new capture epoch; only the residual bias noise is low-passed.
* If Doppler is temporarily unavailable, a bounded, slower finite-difference
* drift estimate keeps the filter usable in the robust profile. All three
* baseline per-update gains are converted from the actual elapsed time, so
* changing the NAV solve period does not change their real-time poles.
*
* COARSE is returned during initial settling, a reference-datum change, or
* after a gap too long for bounded Doppler prediction. The independent
* reference watchdog may suppress PPS validity during a shorter gap without
* destroying filter/PI state. The caller must keep PPS output invalid while
* COARSE is returned.
*/
gps_pps_time_filter_result_t gps_pps_time_filter_update(
gps_pps_time_filter_t* filter,
uint64_t epoch_counter,
double counter_frequency_hz,
double measured_clock_bias_m,
bool clock_drift_valid,
double measured_clock_drift_mps,
double tdop,
double bias_gain,
unsigned int settle_updates,
double* filtered_clock_bias_m_out,
double* innovation_m_out);
#endif
@@ -38,4 +38,9 @@ bool gps_receiver_solve_ready_epoch(
uint64_t gps_receiver_status_ticks(void);
uint64_t gps_receiver_status_ticks_per_second(void);
/* Select the low-latency Doppler EWMA used only by velocity WLS/ESKF.
* PPS keeps its independent fixed low-noise path. */
void gps_receiver_set_eskf_doppler_alpha(double alpha);
double gps_receiver_get_eskf_doppler_alpha(void);
#endif
@@ -16,6 +16,11 @@ int gps_time_resolve_how_week(bool previous_valid,
int fallback_week,
double how_tow_sec);
/* Extend a modulo-1024 LNAV week number around the previous monotonic week. */
int gps_time_extend_lnav_week(bool previous_valid,
int previous_week,
unsigned int lnav_week);
/* Convert a receiver-sample timestamp to GPS time from a HOW/sample anchor.
* raw counters are modulo 2^32. max_age_sec must keep the signed difference
* unambiguous (and also rejects a stale HOW anchor). */
@@ -31,4 +36,113 @@ bool gps_time_from_sample_anchor(int anchor_week,
double* event_tow_sec_out,
double* elapsed_sec_out);
/*
* Convert a periodic tracking event to GPS transmit time. The RF sample
* interval identifies and validates the event number; the result is then
* snapped to the known navigation-event grid. This removes the
* satellite-range-rate term present in receive-time sample intervals.
*/
bool gps_time_from_periodic_sample_anchor(
int anchor_week,
double anchor_tow_sec,
uint32_t anchor_raw_sample,
double anchor_corrected_sample,
uint32_t event_raw_sample,
double event_corrected_sample,
double samples_per_second,
double max_age_sec,
double event_period_sec,
double max_snap_error_sec,
int* event_week_out,
double* event_tow_sec_out,
double* elapsed_sec_out);
/*
* Refer a counter captured at the first RF sample edge after a code boundary
* back to that fractional code boundary. fractional_sample_delay is
* raw_abs_sample - corrected_abs_sample and must be in [0, 1].
*/
bool gps_time_correct_capture_counter(
uint64_t capture_counter,
double fractional_sample_delay,
double samples_per_second,
double counter_frequency_hz,
uint64_t* corrected_counter_out,
double* applied_correction_sec_out);
/*
* 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. */
bool gps_time_anchors_consistent(int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
double first_corrected_sample,
int second_week,
double second_tow_sec,
uint32_t second_raw_sample,
double second_corrected_sample,
double samples_per_second,
double max_gap_sec,
double tolerance_sec);
/* 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,90 @@
#ifndef GPS_TRACKING_MEASUREMENT_H
#define GPS_TRACKING_MEASUREMENT_H
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_ip_defines.h"
#define GPS_TRACK_PPS_DOPPLER_ALPHA 0.05
typedef struct {
uint32_t prn;
uint32_t sequence;
uint32_t absolute_sample;
uint32_t remaining_code_phase;
uint64_t local_timestamp_ticks;
int32_t carrier_freq_q7;
int32_t cn0_sum_i;
int32_t cn0_sum_q;
uint64_t cn0_wbp;
unsigned int cn0_sample_count;
/* Raw coherent 20 ms carrier snapshot retained for diagnostics. */
double doppler_hz;
double pseudorange_rate_mps;
/* Motion-adaptive low-latency path published to velocity WLS/ESKF. */
double eskf_doppler_hz;
double eskf_pseudorange_rate_mps;
/* Slow software IIR path retained only for PPS bias-drift prediction. */
double filtered_doppler_hz;
double filtered_pseudorange_rate_mps;
double cn0_dbhz;
bool nav_bit;
bool locked;
bool valid;
} gps_tracking_measurement_t;
/*
* Decode a coherent hardware mailbox. The sequence-before/after check protects
* callers that are not running directly in the measurement ISR.
*/
bool gps_tracking_measurement_read(
volatile const gps_trck_t* tracking_ip,
gps_tracking_measurement_t* measurement_out);
bool gps_tracking_cn0_dbhz(
int32_t sum_i,
int32_t sum_q,
uint64_t wideband_power,
unsigned int coherent_samples,
double* cn0_dbhz_out);
double gps_tracking_doppler_hz(int32_t carrier_freq_q7);
double gps_tracking_doppler_to_range_rate_mps(double doppler_hz);
/*
* Four-state WLS: receiver ECEF velocity (m/s) and receiver clock drift
* (m/s). Only valid/locked measurements with finite C/N0 are used.
*/
bool gps_tracking_velocity_wls(
const double receiver_ecef_m[3],
const double satellite_positions_m[][3],
const double satellite_velocities_mps[][3],
const double satellite_clock_drifts_sps[],
const gps_tracking_measurement_t measurements[],
unsigned int count,
double receiver_velocity_ecef_mps_out[3],
double receiver_velocity_covariance_ecef_mps2_out[3][3],
double* receiver_clock_drift_mps_out,
double* residual_rms_mps_out);
/*
* Raw-Doppler velocity solution with a one-channel leave-one-out FDE trial.
* FDE runs only for six or more usable channels and only when the full-fit
* maximum normalized residual exceeds 3.5 sigma. The returned bit mask uses
* indices from the input measurement array, not compacted WLS row indices.
*/
bool gps_tracking_velocity_wls_robust(
const double receiver_ecef_m[3],
const double satellite_positions_m[][3],
const double satellite_velocities_mps[][3],
const double satellite_clock_drifts_sps[],
const gps_tracking_measurement_t measurements[],
unsigned int count,
double receiver_velocity_ecef_mps_out[3],
double receiver_velocity_covariance_ecef_mps2_out[3][3],
double* receiver_clock_drift_mps_out,
double* residual_rms_mps_out,
uint32_t* excluded_measurement_mask_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

Some files were not shown because too many files have changed in this diff Show More