Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dce1dd5ea9 | |||
| 28b55fdfb6 | |||
| 30179df8e6 | |||
| 347e1c671d | |||
| a6bb080270 | |||
| 9b0828dbaa | |||
| a697284bff | |||
| 988f9686e7 | |||
| 73db2a8355 | |||
| 156cfc8b77 | |||
| 23650be635 | |||
| 49b8609bb4 | |||
| ecbb8d0b68 | |||
| 0f6b3482c5 | |||
| 971be036a0 | |||
| 2da5fb7153 | |||
| 5b8cd8560f | |||
| b2ca7db633 | |||
| 64860d1ced | |||
| 86dfbeafac | |||
| e57b19b4ee | |||
| fe0eb19dc5 | |||
| ea055a36d2 | |||
| 93766b007f |
@@ -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
|
||||
|
||||
@@ -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' \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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, _) =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,9 +10,11 @@ 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
|
||||
@@ -42,6 +44,8 @@ help:
|
||||
@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)"
|
||||
@@ -63,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="$(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
|
||||
@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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef BSP_UART_BAUDRATE
|
||||
#define BSP_UART_BAUDRATE 115200UL
|
||||
#define BSP_UART_BAUDRATE 460800UL
|
||||
#endif
|
||||
|
||||
void bsp_init(void)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
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
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
@@ -5,7 +5,8 @@ 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.
|
||||
# The 50 Hz fused-location stream plus GPS diagnostics does not fit safely in
|
||||
@@ -65,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
|
||||
@@ -75,17 +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) \
|
||||
-DIMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=$(IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES) \
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS)
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS) \
|
||||
-DGPS_PPS_STATUS_LOG_INTERVAL=$(GPS_PPS_STATUS_LOG_INTERVAL) \
|
||||
-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)
|
||||
|
||||
@@ -29,6 +29,22 @@
|
||||
#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);
|
||||
@@ -69,9 +85,13 @@ static unsigned int feed_pending_imu_to_eskf(
|
||||
imu_sensor_take_measurement(&system->imu_sample))
|
||||
{
|
||||
bool accepted = navigation_fusion_push_imu(&system->imu_sample);
|
||||
if(accepted &&
|
||||
navigation_fusion_get_output(&system->fused_solution) &&
|
||||
system->fused_solution.global_valid &&
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,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;
|
||||
@@ -54,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
|
||||
@@ -46,12 +49,17 @@
|
||||
/* Genesys2 CLINT is 500 kHz. Keep foreground maintenance at
|
||||
* 10 Hz; the tracking IP's 20 ms NAV timer is independent. */
|
||||
#define GPS_SYSTICK_HZ 10u
|
||||
/* LNAV transmits only week modulo 1024. Products built for the
|
||||
* current constellation era use the 2019 rollover epoch until a
|
||||
* saved full week is available. */
|
||||
#define GPS_LNAV_WEEK_ERA_BASE 2048
|
||||
#define GPS_NAV_SOLVE_TIMER_TICK_MS 20u
|
||||
#ifndef GPS_NAV_SOLVE_TIMER_TICKS
|
||||
#define GPS_NAV_SOLVE_TIMER_TICKS 50u
|
||||
#endif
|
||||
#define GPS_NAV_BITS_PER_WEEK 30240000u
|
||||
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
|
||||
#define GPS_NAV_BITS_PER_SUBFRAME 300u
|
||||
/* Keep integrity statistics active, but do not let leave-one-out FDE
|
||||
* replace the full-WLS position until the real measurement-noise model
|
||||
* is calibrated. The live stationary test produced a 22.7% exclusion
|
||||
@@ -59,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"
|
||||
@@ -125,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.
|
||||
@@ -139,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
|
||||
|
||||
|
||||
|
||||
@@ -147,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
|
||||
@@ -157,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)
|
||||
|
||||
@@ -217,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
|
||||
@@ -256,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
|
||||
|
||||
|
||||
@@ -438,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.
|
||||
|
||||
@@ -456,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
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@
|
||||
#define LINKED_LIST_H_INCLUDED
|
||||
|
||||
#include "gps/gps_ip_defines.h"
|
||||
#include "gps/gps_measurement_freshness.h"
|
||||
|
||||
typedef struct gps_ephem_ll {
|
||||
volatile gps_ephemeris_t* data_p;
|
||||
@@ -13,25 +14,23 @@ typedef struct gps_ephem_ll {
|
||||
volatile double gps_how_corrected_abs_sample;
|
||||
volatile uint64_t gps_how_tick;
|
||||
volatile bool gps_how_anchor_valid;
|
||||
volatile bool gps_how_week_valid;
|
||||
volatile bool gps_nav_phase_seeded;
|
||||
volatile gpstime_t gps_pending_how_tow;
|
||||
volatile uint32_t gps_pending_how_abs_sample;
|
||||
volatile double gps_pending_how_corrected_abs_sample;
|
||||
volatile bool gps_pending_how_valid;
|
||||
volatile uint32_t gps_nav_solve_sequence;
|
||||
volatile uint32_t gps_nav_solve_consumed_sequence;
|
||||
volatile gpstime_t gps_last_nav_solve_tow;
|
||||
volatile uint32_t gps_last_nav_solve_abs_sample;
|
||||
volatile double gps_last_nav_solve_corrected_abs_sample;
|
||||
volatile uint64_t gps_last_nav_solve_local_ticks;
|
||||
volatile uint64_t gps_last_nav_solve_pps_counter;
|
||||
volatile uint32_t gps_last_pps_capture_sequence;
|
||||
volatile uint64_t gps_last_measurement_tick;
|
||||
volatile bool gps_measurement_valid;
|
||||
gps_measurement_freshness_t gps_measurement_freshness;
|
||||
} gps_ephem_ll_t;
|
||||
|
||||
typedef struct gps_acq_ll {
|
||||
unsigned int prn_no;
|
||||
struct gps_acq_ll* next;
|
||||
} gps_acq_ll_t;
|
||||
|
||||
extern volatile gps_ephem_ll_t* tracking_node;
|
||||
extern volatile gps_acq_ll_t* acquisition_node;
|
||||
|
||||
bool insert_element(volatile gps_ephem_ll_t** node,
|
||||
volatile gps_ephemeris_t* data_p,
|
||||
@@ -41,8 +40,4 @@ bool trck_ip_exists(volatile gps_ephem_ll_t** node, unsigned char trck_ip_no);
|
||||
volatile gps_ephem_ll_t* get_element_by_ip_number(volatile gps_ephem_ll_t** node,
|
||||
unsigned char trck_ip_no);
|
||||
|
||||
bool insert_acq_element(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
bool delete_acq_element_by_prn_number(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
bool acq_prn_exists(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -24,7 +24,17 @@ bool gps_fusion_build_measurement(
|
||||
measurement_out->vacc_m = solution->vacc_m;
|
||||
measurement_out->residual_rms_m = solution->residual_rms_m;
|
||||
measurement_out->chi_square_ratio = solution->chi_square_ratio;
|
||||
memcpy(measurement_out->velocity_ecef_mps,
|
||||
solution->velocity_ecef_mps,
|
||||
sizeof(measurement_out->velocity_ecef_mps));
|
||||
memcpy(measurement_out->velocity_covariance_ecef_mps2,
|
||||
solution->velocity_covariance_ecef_mps2,
|
||||
sizeof(measurement_out->velocity_covariance_ecef_mps2));
|
||||
measurement_out->speed_mps = solution->speed_mps;
|
||||
measurement_out->velocity_residual_rms_mps =
|
||||
solution->velocity_residual_rms_mps;
|
||||
measurement_out->used_satellites = solution->used_satellite_count;
|
||||
measurement_out->solution_valid = solution->solution_valid;
|
||||
measurement_out->velocity_valid = solution->velocity_valid;
|
||||
return true;
|
||||
}
|
||||
|
||||