16 Commits

142 changed files with 21033 additions and 1242 deletions
+6
View File
@@ -7,3 +7,9 @@
# Generated build outputs.
/out/
/soc/out/
/hardware/generated/
# Python runtime caches and privacy-sensitive raw board captures.
__pycache__/
*.py[cod]
/soc/software/pps_end_to_end_validation/hardware_logs/*.csv
+6 -2
View File
@@ -19,7 +19,7 @@ help:
' make doctor # check local JDK/Mill/CIRCT and RISC-V tools' \
' make soc-hw # emit shared SoC FIRRTL through FPGA config' \
' make fpga-rtl # emit selected FPGA SystemVerilog and wrapper' \
' make fpga-bitstream # run selected FPGA Vivado bitstream flow' \
' make fpga-bitstream # run selected board vendor bitstream flow' \
' make fpga-elf-flash-image' \
' # build APP ELF and board flash-format app image' \
' make fpga-mcs # generate combined bitstream + app MCS/BIN' \
@@ -39,13 +39,17 @@ help:
' make asic-sim # run ASIC-style Questa sanity simulation' \
'' \
'Config:' \
' FPGA_BOARD=$(FPGA_BOARD) # vcu118 or genesys2' \
' FPGA_BOARD=$(FPGA_BOARD) # vcu118, genesys2, or ti375c529' \
' 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' \
' make -C hardware/soc help' \
' make -C targets/fpga/vcu118 help' \
' make -C targets/fpga/genesys2 help' \
' make -C targets/fpga/ti375c529 help' \
' make -C targets/asic/rocket help'
doctor:
+243 -8
View File
@@ -1,6 +1,7 @@
# Rocket Project
Bu agac VCU118/Genesys2 FPGA ve ASIC-style Rocket akislari icin sade proje duzenidir.
Bu agac VCU118, Genesys2 ve Ti375C529 FPGA akislari ile ASIC-style Rocket
akisi icin sade proje duzenidir.
Upstream Rocket-Chip kaynaklari daginik kok klasorlerde tutulmaz; gerekli
upstream kaynaklar `hardware/upstream/rocket-chip/` altindadir.
@@ -22,6 +23,7 @@ hardware/
soc/software/ Bootloader, BSP, uygulamalar, flash image tool
targets/fpga/vcu118/ VCU118 build/programlama akisi
targets/fpga/genesys2/ Genesys2 build/programlama akisi
targets/fpga/ti375c529/ Ti375C529 Efinity build/programlama akisi
targets/asic/rocket/ ASIC wrapper/filelist/sim akisi
setup_tools.sh /tools/soc-tools host araci kurulum betigi
```
@@ -86,6 +88,121 @@ 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.
`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 onemli bir datum farki vardir: position WLS'e verilen capture
zamanlarindan ortak receiver zamani cikarilir ve `STARTOFFSET` eklenir. Bu
nedenle `solution.clock_bias_m`, saf `c*delta_t_r` degil, yaklasik
`c*STARTOFFSET - reference_range` olan referans-uyduya bagli goreli
datumdur. Doppler WLS'nin dorduncu bilinmeyeni yine uydulara ortak receiver
clock/frequency drift'tir; fakat iki fit arasinda
`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
@@ -94,6 +211,7 @@ make -C hardware/fpga/vcu118/rtl help
make -C hardware/sim/fpga/vcu118 help
make -C targets/fpga/vcu118 help
make -C targets/fpga/genesys2 help
make -C targets/fpga/ti375c529 help
make -C targets/asic/rocket help
```
@@ -104,6 +222,7 @@ FPGA karti kok Makefile'dan `FPGA_BOARD` ile secilir:
```sh
make FPGA_BOARD=vcu118 fpga-config
make FPGA_BOARD=genesys2 fpga-config
make FPGA_BOARD=ti375c529 fpga-config
```
Top-level `BOARD=...` alias'i da desteklenir. Orneklerde Genesys2 kullanilir:
@@ -117,7 +236,7 @@ Yanlislikla underscore kullanilmasi durumunda yaygin FPGA hedefleri icin
alias'lar da vardir (`fpga_program`, `fpga_debug_openocd` gibi).
`fpga-config` secilen kartin clock, flash, UART, DTIM ve RF ayarlarini
gosterir. RF kaynagi `RF_INPUT_MODE` ile secilir:
gosterir. Genesys2 ve VCU118'de RF kaynagi `RF_INPUT_MODE` ile secilir:
```text
RF_INPUT_MODE=pmod Varsayilan. Harici 4-bit RF data + RF clock PMOD'dan gelir.
@@ -125,10 +244,14 @@ RF_INPUT_MODE=generated Dahili RF clock uretilir, RF data sifira baglanir.
RF_INPUT_MODE=ethernet Kart bazli raw-Ethernet RF streamer path'i.
```
Ti375C529 Efinity wrapper'i su anda yalniz harici PMOD RF girisini kullanir;
bu kartta `RF_INPUT_MODE=pmod` birakilmalidir. `generated` ve `ethernet`
yollari Ti375C529 wrapper'ina henuz uygulanmamistir.
Top-level FPGA wrapper'lar elle yazilmaz; `targets/fpga/*/tools/gen_*_wrapper.py`
scriptleri tarafindan uretilir. Bu yuzden RF secimi RTL `ifdef` ile degil,
generator parametresi ile yapilir. `make rtl` ve Vivado `build.tcl` ayni
`RF_INPUT_MODE` degerini wrapper generator'a gecirir.
scriptleri tarafindan uretilir. Genesys2 ve VCU118'de RF secimi RTL `ifdef`
ile degil, generator parametresi ile yapilir. `make rtl` ve Vivado `build.tcl`
ayni `RF_INPUT_MODE` degerini wrapper generator'a gecirir.
Varsayilan `pmod` modunda Ethernet RF IP/RTL okunmaz ve top'ta Ethernet PHY
portlari olusmaz. Ethernet RF akisi gerektiginde:
@@ -160,11 +283,23 @@ uzerinden 1.8 V bank/LVCMOS18:
rf_data_pmod[1] AW15 PMOD0_2 J52.5 LVCMOS18 3.3 V
rf_data_pmod[2] AV15 PMOD0_3 J52.7 LVCMOS18 3.3 V
rf_data_pmod[3] AV16 PMOD0_4 J52.2 LVCMOS18 3.3 V
Ti375C529 PMOD0/J13, connector ve FPGA tarafi 3.3 V:
port FPGA pin PMOD pin FPGA resource FPGA I/O
rf_clk A12 J13.10 GPIOR_95_CLK12 3.3_V_LVCMOS
rf_data[0] G16 J13.2 GPIOR_89 3.3_V_LVCMOS
rf_data[1] F16 J13.3 GPIOR_90 3.3_V_LVCMOS
rf_data[2] F17 J13.4 GPIOR_91 3.3_V_LVCMOS
rf_data[3] G17 J13.7 GPIOR_92 3.3_V_LVCMOS
```
VCU118'de disaridan PMOD header'a 3.3 V seviye verilir. FPGA U1 package
pinlerine dogrudan 3.3 V uygulanmaz; o tarafta XDC `LVCMOS18` kullanir.
Ti375C529'de AX2771 `rf_clk` hatti mutlaka J13 pin 10'a baglanmalidir. Bu pin
global-clock girisidir. J13 pin 1 / G15 (`GPIOR_88_PLLIN1`) PLL girisi olsa da
dedicated fabric GCLK degildir ve bu akis RF saati icin onu kullanmaz.
PMOD SPI portu her RF modunda aktiftir ve ayri MMIO register block olarak
`0x10015000` adresindedir. Kart uzerindeki konfig flash SPI'si `0x10014000`
adresinde kalir. Yazilim driver'i single/quad calisma modunu `ctrl.bit2`
@@ -190,6 +325,15 @@ uzerinden 1.2 V bank/LVCMOS12:
pmod_spi_sck P30 PMOD1_3 J53.7 SCK LVCMOS12 3.3 V
pmod_spi_dq[2] P29 PMOD1_4 J53.2 LVCMOS12 3.3 V
pmod_spi_dq[3] L31 PMOD1_5 J53.4 LVCMOS12 3.3 V
Ti375C529 PMOD1/J14, connector ve FPGA tarafi 3.3 V:
port FPGA pin PMOD pin FPGA resource FPGA I/O
pmod_spi_csn B12 J14.1 GPIOR_96_CLK13 3.3_V_LVCMOS
pmod_spi_dq[0] C14 J14.2 MOSI GPIOR_97 3.3_V_LVCMOS
pmod_spi_dq[1] C13 J14.3 MISO GPIOR_98 3.3_V_LVCMOS
pmod_spi_sck C12 J14.4 SCK GPIOR_99 3.3_V_LVCMOS
pmod_spi_dq[2] D12 J14.7 GPIOR_100 3.3_V_LVCMOS
pmod_spi_dq[3] F12 J14.8 GPIOR_101_PLLIN1 3.3_V_LVCMOS
```
Sensorden gelen external interrupt hatti SPI'dan bagimsizdir. `pmod_irq`
@@ -204,8 +348,16 @@ Genesys2:
VCU118:
port FPGA pin PMOD pin FPGA I/O PMOD header level
pmod_irq M31 PMOD1_6 J53.6 LVCMOS12 3.3 V
Ti375C529:
port FPGA pin PMOD pin FPGA resource FPGA I/O
pmod_irq D13 J14.9 GPIOR_102 3.3_V_LVCMOS
gps_pps E13 J14.10 GPIOR_103 3.3_V_LVCMOS
```
Ti375C529'de `gps_pps` J14.10 uzerinden cikan 3.3 V PPS sinyalidir;
`pmod_irq` ise J14.9 uzerinden FPGA'ya girer.
Bu pinler mod bazli companion XDC dosyalarinda tutulur:
`hardware/fpga/genesys2/constraints/genesys2_pmod_rf.xdc`,
`hardware/fpga/vcu118/constraints/vcu118_pmod_rf.xdc`,
@@ -214,13 +366,45 @@ Bu pinler mod bazli companion XDC dosyalarinda tutulur:
`hardware/fpga/genesys2/constraints/genesys2_pmod_irq.xdc`,
`hardware/fpga/vcu118/constraints/vcu118_pmod_irq.xdc`,
`hardware/fpga/genesys2/constraints/genesys2_ethernet.xdc` ve
`hardware/fpga/vcu118/constraints/vcu118_ethernet.xdc`.
`hardware/fpga/vcu118/constraints/vcu118_ethernet.xdc`. Ti375C529 pinleri,
I/O standartlari, PLL'ler ve hard-JTAG baglantisi tek Efinity interface
scriptinde tutulur:
`hardware/fpga/ti375c529/constraints/ti375c529_rocket.isf`.
### Ti375C529 kart uzeri baglantilar
Ti375C529 development kit'te Rocket sistem saati kart uzerindeki 100 MHz
osilatorden, GPS isleme saatleri ise AX2771'in J13.10 uzerindeki
16.367981 MHz RF saatinden uretilir. SW3 active-low Rocket resetidir. FTDI
UART varsayilan olarak 460800 baud ve mevcut Linux enumeration'inda
`/dev/ttyUSB2` kullanir. Birden fazla kart veya farkli enumeration icin
`UART_PORT=...` ve `FTDI_URL=...` ile override edilebilir.
Lower 512-Mbit konfig flash'i kart icinde su pinlere baglidir; PMOD'a kablo
cekilmez:
```text
port FPGA pin FPGA resource
flash_csn A7 GPIOB_N_38 / SSL_N
flash_sck G6 GPIOB_P_39 / CCK
flash_dq[0] H4 GPIOB_N_40 / CDI0
flash_dq[1] H5 GPIOB_P_40 / CDI1
flash_dq[2] G5 GPIOB_N_39 / CDI2
flash_dq[3] B7 GPIOB_P_38 / CDI3
```
Efinity `JTAG_USER1`, mevcut Rocket DMI tunnel protokolunu kullanir. Host
OpenOCD build'inde `riscv set_bscan_tunnel_ir` destegi bulunmalidir; Efinity
ile gelen OpenOCD yerine uyumlu host OpenOCD kullanilir. Ayrintili Efinity,
clock, JTAG ve dogrulama notlari icin
`targets/fpga/ti375c529/README.md` dosyasina bakin.
## FPGA Flash ve MCS Akislari
Bu hedefler kok Makefile'dan secilen karta yonlenir. Genesys2 icin
`BOARD=genesys2`, VCU118 icin `BOARD=vcu118` kullanilir. `APP` verilmezse
varsayilan uygulama `hello_uart` olur.
`BOARD=genesys2`, VCU118 icin `BOARD=vcu118`, Ti375C529 icin
`BOARD=ti375c529` kullanilir. `APP` verilmezse varsayilan uygulama
`hello_uart` olur.
Sadece ELF'i derleyip bootloader'in okuyacagi flash-format app imajina cevirmek:
@@ -317,6 +501,57 @@ flash size: 32 MiB
interface: SPIx4
```
### Ti375C529 Efinity build ve flash
Efinity varsayilan olarak `/tools/efinix/efinity` altinda aranir. Farkli bir
kurulum `EFINITY_HOME=/path/to/efinity` ile secilebilir. Build, SRAM'e gecici
programlama ve kart tespiti kokten su sekilde yapilir:
```sh
make FPGA_BOARD=ti375c529 fpga-config
make FPGA_BOARD=ti375c529 fpga-bitstream
make -C targets/fpga/ti375c529 detect
make FPGA_BOARD=ti375c529 fpga-program
```
Ti375C529'de repository genelindeki `fpga-mcs` hedef adi korunur fakat cikti
Xilinx MCS/BIN degil, Efinity raw HEX'tir. Lower 512-Mbit flash yerlesimi:
```text
configuration offset: 0x00000000
app offset: 0x00800000
flash size: 64 MiB
interface: active-x1 configuration / user-mode quad SPI
```
Konfig ve uygulama imajlarini uretmek ve programlamak icin:
```sh
make FPGA_BOARD=ti375c529 APP=hello_uart fpga-mcs
make FPGA_BOARD=ti375c529 APP=hello_uart fpga-flash-app
make FPGA_BOARD=ti375c529 fpga-flash-bitstream
make FPGA_BOARD=ti375c529 APP=hello_uart fpga-flash-mcs
```
Ciktilar:
```text
targets/fpga/ti375c529/out/bitstream/ti375c529_rocket.bit
targets/fpga/ti375c529/out/bitstream/ti375c529_rocket.hex
targets/fpga/ti375c529/out/flash/ti375c529_rocket.hex
targets/fpga/ti375c529/out/flash/ti375c529_rocket.json
```
Flash programlama Efinity'nin Ti375'e uygun JTAG-to-SPI bridge'ini gecici
olarak yukler. Gercek persistent boot testi icin programlamadan sonra karta
power-cycle uygulanmalidir. GDB ile DTIM'e yukleme ve UART dinleme:
```sh
make FPGA_BOARD=ti375c529 fpga-debug-openocd
make FPGA_BOARD=ti375c529 APP=hello_uart fpga-debug-load
make FPGA_BOARD=ti375c529 fpga-uart
```
## GPS Reset Davranisi
OpenOCD/GDB `monitor reset` yolu Rocket debug module `ndmreset` sinyalini
+6
View File
@@ -46,3 +46,9 @@ external/gps/rtl/
pass this path as the default GPS_SYSTEM_DIR, so the Vivado flow does not
depend on a user-specific /home/... checkout.
```
GPS tracking olcum yolunun 20 ms Doppler/C/N0 snapshot, PCLK synchronizer,
APB register/interrupt ve software velocity WLS sozlesmesi
`external/gps/rtl/trck_src/README.md` icinde belgelenir. PPS PI periferigi,
runtime coefficient/lock register map'i ve fiziksel pin sinirlari
`external/gps/rtl/pps/README.md` icindedir.
+102 -11
View File
@@ -24,6 +24,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,6 +47,38 @@ 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-WLS bias
in this project is different: subtracting the common capture time and adding
`STARTOFFSET` makes it a reference-satellite-relative datum, approximately
`c*STARTOFFSET - reference_range`, rather than a pure receiver oscillator
offset.
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.
@@ -46,7 +87,17 @@ 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.
`ABS_SAMPLE/REM_CODE_PHASE`. `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 remaining PCLK/Gray pipeline delay is fixed and must be measured once on
hardware.
## APB register map
@@ -68,34 +119,74 @@ the SoC-level connection must use the exact PCLK event that updates
| `0x44` | `COMMIT` | WO | bit 0 commit, bit 1 coarse-align commit |
| `0x48..0x74` | diagnostics | RO | sequence, GPS time, phase, integrator, rate and pulse count |
| `0x78` | `DIAG_SNAPSHOT` | WO | bit 0 atomically snapshots all multiword diagnostics |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020100`) |
| `0x7c` | `VERSION` | RO | RTL interface version (`0x00020200`) |
| `0x88` | `LOOP_CONFIG` | RW shadow | bits 15:8 acquire ticks, 23:16 release ticks, 31:24 consecutive acquire updates; bits 7:0 reserved |
Coarse alignment does not assert lock or drive the pin. Five consecutive PI
updates with at most eight tracking-clock ticks (64 ns at 125 MHz) phase error
acquire lock; an error above 32 ticks (256 ns) releases it. This hysteresis
allows realistic WLS epoch jitter without edge chatter. Holdover
retains an already acquired lock, while a newly committed bad reference drops
lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the physical PPS output
therefore distinguish an aligned timebase from a converged one.
Coarse alignment does not assert lock or drive the pin. The reset policy
requires five consecutive raw PI residuals within eight tracking-clock ticks
(64 ns at 125 MHz); a residual above 32 ticks (256 ns) releases lock. Runtime
profiles may widen the release threshold or require more good updates without
filtering the phase seen by the controller. Holdover retains the internal
frequency/lock state but deasserts `STATUS.VALID`; a newly committed bad
reference drops lock immediately. `STATUS.LOCKED`, `STATUS.VALID`, and the
physical PPS output therefore distinguish a held/aligned timebase from a
currently qualified one.
Firmware uses a two-second no-reference watchdog to enter holdover but keeps
the Doppler-predicted bias and PI state for bounded same-datum gaps. On
recovery it fills all mailbox shadow words, releases holdover immediately
before the normal commit toggle, and therefore applies the first coherent
post-gap reference (v2.2 rejects normal PI commits that arrive while holdover
is still asserted). The physical PPS already runs from the held rate word and
does not phase-step; `STATUS.VALID` may precede the PI apply only by the
bounded synchronizer/commit-pipeline latency. A datum change or an observation
gap beyond the prediction horizon uses a coarse commit and remains invalid
until lock is reacquired.
Diagnostic words do not change until another `DIAG_SNAPSHOT` request completes;
software polls `STATUS.DIAG_BUSY` before reading the group. The disciplined
timebase continues in holdover and while the physical output is disabled;
`CONTROL.ENABLE` gates `pps_out` and validity, not timekeeping.
Run lint plus both unit-level Questa tests from this directory:
This also applies to the single-word `ACTIVE_SEQUENCE` and `PULSE_COUNT`
registers: they belong to the same frozen diagnostic group. A live debugger
must halt the CPU briefly, request a new snapshot, wait for status bit 10 to
clear, and only then read `0x48..0x74`. For example, with OpenOCD's GDB monitor:
```gdb
monitor halt
monitor mww 0x1002a078 1
monitor sleep 10
x/wx 0x1002a004
x/wx 0x1002a048
x/wx 0x1002a05c
x/wx 0x1002a060
x/wx 0x1002a074
monitor resume
```
Reading `ACTIVE_SEQUENCE` or `PULSE_COUNT` without this request can return the
previous snapshot, including reset zero, even while the core is accepting
commits and generating pulses.
Run lint plus all five Questa tests from this directory:
```
make -C tb check
```
`pps_wrapper_tb` uses the same ordered MMIO writes as `gps_system/src/gps_pps.c` and
injects CPU/APB latency. `pps_core_tb` independently stresses signed PI math,
injects CPU/APB latency; it separately verifies that a held normal commit is
not applied and that the firmware's shadow-first release sequence applies the
first recovery update. `pps_core_tb` independently stresses signed PI math,
slew/anti-windup saturation, stale/future epochs, phase saturation, sequence
wrap, holdover and output disable. `pps_convergence_tb` supplies an independent
GPS truth clock, a +15 ppm oscillator error followed by a +8 ppm temperature-like
step, deterministic measurement noise, varying 3--27 cycle software latency,
and 5 Hz navigation commits. It checks phase and rate convergence numerically.
`pps_adaptive_tb` changes `Kp`, `Ki`, maximum slew and lock policy while the
loop is live, checks integrator preservation, validates raw-residual lock
acquisition/release, and verifies that holdover suppresses validity.
`CLK_FREQ_HZ` is reduced only to shorten simulation; tick/Q-format behavior is
unchanged.
+23 -7
View File
@@ -16,6 +16,7 @@ module pps_core #(
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] kp_q46,
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] ki_q46,
input logic [pps_pkg::PPS_FRACTION_WIDTH-1:0] max_slew_q32,
input logic [31:0] loop_config,
output logic commit_ack_toggle,
output logic [pps_pkg::PPS_COUNTER_WIDTH-1:0] raw_counter,
output logic pps_out,
@@ -50,8 +51,7 @@ module pps_core #(
PPS_MILLISECONDS_PER_SECOND - 1) / PPS_MILLISECONDS_PER_SECOND);
localparam int unsigned PPS_PULSE_COUNTER_WIDTH =
(PPS_PULSE_WIDTH_TICKS <= 1) ? 1 : $clog2(PPS_PULSE_WIDTH_TICKS);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH =
(PPS_LOCK_ACQUIRE_UPDATES <= 1) ? 1 : $clog2(PPS_LOCK_ACQUIRE_UPDATES);
localparam int unsigned PPS_LOCK_COUNTER_WIDTH = 8;
// A navigation commit is rare (normally 5 Hz), while the disciplined clock
// runs at 125 MHz. Keep the per-clock time accumulator short and process a
@@ -101,6 +101,9 @@ module pps_core #(
logic [PPS_SEQUENCE_WIDTH-1:0] epoch_sequence_work;
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_work, ki_q46_work;
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_work;
logic [7:0] lock_acquire_ticks_work;
logic [7:0] lock_release_ticks_work;
logic [7:0] lock_acquire_updates_work;
logic signed [PPS_INCREMENT_WIDTH-1:0] reference_increment_work;
// Registered arithmetic stages. No multiplier feeds another multiplier,
@@ -135,13 +138,15 @@ module pps_core #(
logic signed [PPS_PRODUCT_WIDTH-1:0] max_slew_wide;
logic signed [PPS_STATE_WIDTH-1:0] max_slew_signed;
logic [PPS_STATE_WIDTH-1:0] phase_abs_work;
logic [PPS_STATE_WIDTH-1:0] lock_acquire_phase_q32_work;
logic [PPS_STATE_WIDTH-1:0] lock_release_phase_q32_work;
assign raw_counter = raw_counter_p;
assign pps_out = pps_out_p;
assign aligned = aligned_p;
assign tracking = aligned_p && track_enable && !holdover;
assign locked = locked_p;
assign pps_valid = enable && aligned_p && locked_p;
assign pps_valid = enable && aligned_p && locked_p && !holdover;
assign stale_sequence = stale_sequence_p;
assign stale_age = stale_age_p;
assign rate_saturated = rate_saturated_p;
@@ -213,6 +218,10 @@ module pps_core #(
max_slew_signed = $signed({32'd0, max_slew_q32_work});
phase_abs_work = phase_work[PPS_STATE_WIDTH-1] ? $unsigned(-phase_work) :
$unsigned(phase_work);
lock_acquire_phase_q32_work =
{24'd0, lock_acquire_ticks_work, 32'd0};
lock_release_phase_q32_work =
{24'd0, lock_release_ticks_work, 32'd0};
end
always_ff @(posedge clk or negedge resetn) begin
@@ -250,6 +259,9 @@ module pps_core #(
kp_q46_work <= '0;
ki_q46_work <= '0;
max_slew_q32_work <= '0;
lock_acquire_ticks_work <= '0;
lock_release_ticks_work <= '0;
lock_acquire_updates_work <= '0;
reference_increment_work <= '0;
fraction_ticks_work <= '0;
commit_age_ticks_work <= '0;
@@ -310,6 +322,9 @@ module pps_core #(
kp_q46_work <= kp_q46;
ki_q46_work <= ki_q46;
max_slew_q32_work <= max_slew_q32;
lock_acquire_ticks_work <= loop_config[15:8];
lock_release_ticks_work <= loop_config[23:16];
lock_acquire_updates_work <= loop_config[31:24];
if (aligned_p)
reference_increment_work <= $signed({1'b0, NOMINAL_INCREMENT_Q32}) +
$signed(rate_word_q32_p);
@@ -445,15 +460,16 @@ module pps_core #(
// A wider release threshold keeps small measurement noise from
// chattering an already valid physical PPS output.
if (phase_overflow_work ||
(phase_abs_work > PPS_LOCK_RELEASE_PHASE_Q32)) begin
(phase_abs_work > lock_release_phase_q32_work)) begin
locked_p <= 1'b0;
lock_good_updates_p <= '0;
pps_out_p <= 1'b0;
pulse_ticks_remaining_p <= '0;
end else if (!locked_p) begin
if (phase_abs_work <= PPS_LOCK_ACQUIRE_PHASE_Q32) begin
if (lock_good_updates_p ==
PPS_LOCK_COUNTER_WIDTH'(PPS_LOCK_ACQUIRE_UPDATES - 1)) begin
if (phase_abs_work <= lock_acquire_phase_q32_work) begin
if ((lock_acquire_updates_work <= 8'd1) ||
(lock_good_updates_p >=
(lock_acquire_updates_work - 1'b1))) begin
locked_p <= 1'b1;
lock_good_updates_p <= '0;
end else begin
+9 -2
View File
@@ -28,6 +28,12 @@ package pps_pkg;
localparam logic [PPS_STATE_WIDTH-1:0] PPS_LOCK_RELEASE_PHASE_Q32 =
64'h0000_0020_0000_0000; // 32 tracking-clock ticks (256 ns at 125 MHz)
localparam logic [PPS_FRACTION_WIDTH-1:0] PPS_DEFAULT_MAX_SLEW_Q32 = 32'd85899;
localparam logic [31:0] PPS_DEFAULT_LOOP_CONFIG = {
8'(PPS_LOCK_ACQUIRE_UPDATES),
PPS_LOCK_RELEASE_PHASE_Q32[39:32],
PPS_LOCK_ACQUIRE_PHASE_Q32[39:32],
8'd0 // reserved
};
localparam logic [PPS_STATE_WIDTH-1:0] PPS_ONE_TICK_Q32 = 64'h0000_0001_0000_0000;
typedef enum logic [31:0] {
@@ -64,7 +70,8 @@ package pps_pkg;
PPS_DIAG_SNAPSHOT_OFFSET = 32'h0000_0078,
PPS_VERSION_REG_OFFSET = 32'h0000_007c,
PPS_KP_Q46_HI_OFFSET = 32'h0000_0080,
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084,
PPS_LOOP_CONFIG_OFFSET = 32'h0000_0088
} pps_wrapper_reg_offsets_e;
typedef enum int unsigned {
@@ -104,7 +111,7 @@ package pps_pkg;
PPS_APB_STATE_FINISH
} pps_apb_state_e;
localparam logic [31:0] PPS_VERSION = 32'h0002_0100;
localparam logic [31:0] PPS_VERSION = 32'h0002_0200;
function automatic logic [PPS_COUNTER_WIDTH-1:0] pps_gray_to_binary(
input logic [PPS_COUNTER_WIDTH-1:0] gray);
logic [PPS_COUNTER_WIDTH-1:0] binary;
+14 -2
View File
@@ -39,6 +39,7 @@ 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;
@@ -65,7 +66,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,7 +85,8 @@ 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;
@@ -147,6 +150,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;
@@ -190,6 +194,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;
@@ -254,6 +259,7 @@ 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;
@@ -318,6 +324,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 +366,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;
@@ -392,6 +402,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 +432,7 @@ module pps_wrapper #(
.epoch_counter(epoch_counter_shadow_p), .gps_seconds(gps_seconds_shadow_p),
.gps_fraction_q32(gps_fraction_shadow_p), .epoch_sequence(epoch_sequence_shadow_p),
.kp_q46(kp_q46_p), .ki_q46(ki_q46_p), .max_slew_q32(max_slew_q32_p),
.loop_config(loop_config_p),
.commit_ack_toggle(commit_ack_pps), .raw_counter(raw_counter_pps),
.pps_out(pps_out), .aligned(aligned_pps),
.tracking(tracking_pps), .locked(locked_pps), .pps_valid(pps_valid),
+1
View File
@@ -14,6 +14,7 @@ test:
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_wrapper_tb $(QUESTA_VSIM) -c -l wrapper.log -wlf wrapper.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_core_tb $(QUESTA_VSIM) -c -l core.log -wlf core.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_convergence_tb $(QUESTA_VSIM) -c -l convergence.log -wlf convergence.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_adaptive_tb $(QUESTA_VSIM) -c -l adaptive.log -wlf adaptive.wlf -do $(CURDIR)/run.do
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_precision_tb $(QUESTA_VSIM) -c -l precision.log -wlf precision.wlf -do $(CURDIR)/run.do
clean:
+191
View File
@@ -0,0 +1,191 @@
`timescale 1ns/1ps
module pps_adaptive_tb;
import pps_pkg::*;
localparam int unsigned CLK_FREQ_HZ = 1000;
localparam logic [63:0] TICK_Q32 = 64'h0000_0001_0000_0000;
localparam logic [63:0] SECOND_Q32 = CLK_FREQ_HZ * TICK_Q32;
localparam logic [31:0] ACQUIRE_CONFIG = 32'h0540_0800;
localparam logic [31:0] PRECISION_CONFIG = 32'h0340_0800;
localparam logic signed [47:0] ACQUIRE_KP_Q46 = 48'sh0800_0000_0000;
localparam logic signed [47:0] ACQUIRE_KI_Q46 = 48'sh0400_0000_0000;
localparam logic signed [47:0] PRECISION_KP_Q46 = 48'sh0200_0000_0000;
localparam logic signed [47:0] PRECISION_KI_Q46 = 48'sh0100_0000_0000;
logic clk = 1'b0;
logic resetn = 1'b0;
logic enable = 1'b1;
logic track_enable = 1'b1;
logic holdover = 1'b0;
logic clear_status_toggle = 1'b0;
logic commit_toggle = 1'b0;
logic commit_coarse = 1'b0;
logic [63:0] epoch_counter = '0;
logic [63:0] gps_seconds = '0;
logic [31:0] gps_fraction_q32 = '0;
logic [31:0] epoch_sequence = '0;
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46 = ACQUIRE_KP_Q46;
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46 = ACQUIRE_KI_Q46;
logic [31:0] max_slew_q32 = 32'h7fff_ffff;
logic [31:0] loop_config = ACQUIRE_CONFIG;
logic commit_ack_toggle;
logic [63:0] raw_counter;
logic pps_out, aligned, tracking, locked, pps_valid;
logic stale_sequence, stale_age, rate_saturated, phase_saturated;
logic [31:0] active_sequence, pulse_count;
logic [63:0] active_seconds, active_subsec_q32;
logic signed [63:0] phase_error_q32;
logic signed [63:0] integrator_q32, rate_word_q32;
int checks = 0;
always #4 clk = ~clk;
pps_core #(.CLK_FREQ_HZ(CLK_FREQ_HZ)) dut (
.clk(clk), .resetn(resetn), .enable(enable), .track_enable(track_enable),
.holdover(holdover), .clear_status_toggle(clear_status_toggle),
.commit_toggle(commit_toggle), .commit_coarse(commit_coarse),
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(loop_config),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
.pps_valid(pps_valid), .stale_sequence(stale_sequence),
.stale_age(stale_age), .rate_saturated(rate_saturated),
.phase_saturated(phase_saturated), .active_sequence(active_sequence),
.active_seconds(active_seconds), .active_subsec_q32(active_subsec_q32),
.phase_error_q32(phase_error_q32), .integrator_q32(integrator_q32),
.rate_word_q32(rate_word_q32), .pulse_count(pulse_count)
);
task automatic check_adaptive(input logic condition, input string message);
begin
checks = checks + 1;
if (!condition)
$fatal(1, "ADAPTIVE CHECK %0d FAILED: %s", checks, message);
end
endtask
task automatic commit_anchor(
input logic [63:0] counter_value,
input logic [63:0] seconds_value,
input logic [31:0] fraction_value,
input logic [31:0] sequence_id,
input logic coarse);
logic expected_ack;
int timeout;
begin
@(negedge clk);
epoch_counter = counter_value;
gps_seconds = seconds_value;
gps_fraction_q32 = fraction_value;
epoch_sequence = sequence_id;
commit_coarse = coarse;
commit_toggle = ~commit_toggle;
expected_ack = commit_toggle;
timeout = 0;
while (commit_ack_toggle != expected_ack) begin
@(posedge clk);
timeout = timeout + 1;
if (timeout > 40) $fatal(1, "adaptive commit timeout");
end
@(negedge clk);
end
endtask
task automatic commit_current_offset(
input integer signed offset_ticks,
input logic [31:0] sequence_id);
logic [63:0] counter_value;
logic [63:0] seconds_value;
logic signed [64:0] subsec_value;
logic [31:0] fraction_value;
begin
@(negedge clk);
counter_value = raw_counter;
seconds_value = active_seconds;
subsec_value = $signed({1'b0, active_subsec_q32}) +
(offset_ticks * $signed({1'b0, TICK_Q32}));
if (subsec_value >= $signed({1'b0, SECOND_Q32})) begin
subsec_value = subsec_value - $signed({1'b0, SECOND_Q32});
seconds_value = seconds_value + 64'd1;
end else if (subsec_value < 0) begin
subsec_value = subsec_value + $signed({1'b0, SECOND_Q32});
seconds_value = seconds_value - 64'd1;
end
fraction_value = subsec_value / CLK_FREQ_HZ;
commit_anchor(
counter_value, seconds_value, fraction_value, sequence_id, 1'b0);
end
endtask
initial begin
logic [31:0] sequence_before_holdover;
logic signed [63:0] acquire_rate;
logic signed [63:0] acquire_integrator;
repeat (5) @(posedge clk);
resetn = 1'b1;
repeat (5) @(posedge clk);
commit_anchor(raw_counter, 64'd100, 32'd0, 32'd1, 1'b1);
check_adaptive(aligned && !locked && !pps_valid,
"coarse alignment starts invalid and unlocked");
// Lock detection uses the unfiltered PI phase residual. Five coherent
// updates inside the programmable eight-tick window are required.
for (int update = 0; update < 4; update = update + 1) begin
commit_current_offset(0, update + 2);
check_adaptive(!locked, "lock cannot assert before configured count");
end
commit_current_offset(0, 32'd6);
check_adaptive(locked && pps_valid,
"raw coherent residuals acquire lock");
// Exercise the acquisition gains, then replace Kp, Ki and lock policy
// while running. The core captures one coherent set per commit and keeps
// the integrator state across a profile transition.
commit_current_offset(1, 32'd7);
acquire_rate = rate_word_q32;
acquire_integrator = integrator_q32;
check_adaptive((acquire_rate > 0) && (acquire_integrator > 0),
"acquisition Kp and Ki both contribute");
kp_q46 = PRECISION_KP_Q46;
ki_q46 = PRECISION_KI_Q46;
loop_config = PRECISION_CONFIG;
commit_current_offset(1, 32'd8);
check_adaptive(locked && pps_valid,
"on-the-fly PI profile transition preserves lock");
check_adaptive(integrator_q32 > acquire_integrator,
"new Ki continues the preserved integrator");
check_adaptive(rate_word_q32 < acquire_rate,
"lower precision Kp/Ki reduce loop correction");
check_adaptive((dut.kp_q46_work == PRECISION_KP_Q46) &&
(dut.ki_q46_work == PRECISION_KI_Q46),
"commit atomically captures runtime Kp and Ki");
sequence_before_holdover = active_sequence;
holdover = 1'b1;
repeat (3) @(posedge clk);
check_adaptive(locked && !tracking && !pps_valid,
"holdover retains lock state but marks PPS invalid");
commit_current_offset(0, 32'd9);
check_adaptive(active_sequence == sequence_before_holdover,
"holdover rejects control updates");
holdover = 1'b0;
repeat (3) @(posedge clk);
check_adaptive(pps_valid && tracking,
"leaving holdover restores validity without a phase step");
commit_current_offset(200, 32'd10);
check_adaptive(!locked && !pps_valid,
"one raw residual beyond release threshold drops lock");
$display("PPS ADAPTIVE TB PASS: %0d checks phase=%0d rate=%0d",
checks, phase_error_q32, rate_word_q32);
$finish;
end
endmodule
@@ -75,6 +75,7 @@ module pps_convergence_tb;
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
.pps_valid(pps_valid),
+1
View File
@@ -40,6 +40,7 @@ module pps_core_tb;
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
.pps_out(pps_out), .aligned(aligned),
.tracking(tracking), .locked(locked), .pps_valid(pps_valid),
+33 -2
View File
@@ -203,6 +203,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;
@@ -254,6 +257,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
@@ -360,10 +367,34 @@ module pps_wrapper_tb;
pps_sw_snapshot_diagnostics();
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
check_true(active_sequence == 7, "holdover preserves the last accepted control epoch");
check_true(!pps_valid, "holdover deasserts validity while retaining the disciplined timebase");
apb_read(12'h088, read_data, read_error);
// Firmware writes every shadow field while held, then releases holdover
// immediately before the commit toggle. This makes the first post-gap
// normal update reach the PI instead of receiving a drop-only ACK.
@(negedge pps_clk);
lock_ref_counter = dut.i_pps_core.raw_counter;
lock_ref_seconds = dut.i_pps_core.active_seconds;
lock_ref_fraction = dut.i_pps_core.active_subsec_q32 / CLK_FREQ_HZ;
write_ok(PPS_EPOCH_COUNTER_LO_OFFSET, lock_ref_counter[31:0]);
write_ok(PPS_EPOCH_COUNTER_HI_OFFSET, lock_ref_counter[63:32]);
write_ok(PPS_GPS_SECONDS_LO_OFFSET, lock_ref_seconds[31:0]);
write_ok(PPS_GPS_SECONDS_HI_OFFSET, lock_ref_seconds[63:32]);
write_ok(PPS_GPS_FRACTION_Q32_OFFSET, lock_ref_fraction);
write_ok(PPS_EPOCH_SEQUENCE_OFFSET, 32'd9);
write_ok(PPS_CONTROL_REG_OFFSET, 32'h0000_0003); // release after shadows
write_ok(PPS_COMMIT_REG_OFFSET, 32'h0000_0001);
wait_commit_idle();
pps_sw_snapshot_diagnostics();
read_ok(PPS_ACTIVE_SEQUENCE_OFFSET, active_sequence);
check_true(active_sequence == 9,
"shadow-first holdover release applies first recovery update");
check_true(pps_valid,
"recovery update preserves lock and restores validity");
apb_read(12'h08c, read_data, read_error);
check_true(read_error, "unmapped APB register raises PSLVERR");
apb_write(12'h088, 32'h1, read_error);
apb_write(12'h08c, 32'h1, read_error);
check_true(read_error, "unmapped APB write raises PSLVERR");
write_ok(PPS_CAPTURE_SELECT_REG_OFFSET, 0);
+1
View File
@@ -9,6 +9,7 @@ vlog -sv \
[file join $tb_dir pps_wrapper_tb.sv] \
[file join $tb_dir pps_core_tb.sv] \
[file join $tb_dir pps_convergence_tb.sv] \
[file join $tb_dir pps_adaptive_tb.sv] \
[file join $tb_dir pps_precision_tb.sv]
vsim -c -voptargs=+acc work.$::env(PPS_TB_TOP)
run -all
+88 -1
View File
@@ -1,3 +1,4 @@
# GPS RTL
Take `Ankasys_Pulpissimo_SoC/pulpissimo/rtl/gps_system/hdl` folder from Ankasys_Pulpissimo_SoC svn repo, it has these subfolders:
@@ -16,6 +17,93 @@ trck_src:
acq_src:
acqusition top and instantiated sub ips hdl codes from Hilal & Ahmet's implementations.
## 20 ms Tracking Measurement Mailbox
Her tracking kanali, decode edilen her GPS L1 C/A navigation biti icin bir
atomik olcum uretir. Veri yolu:
```text
1 ms prompt I/Q
-> tracking_measurement (20 ornek akumulasyonu ve snapshot)
-> tracking clock domaininde 20 ms sabit payload
-> her alan icin iki FF senkronizer
-> iki tam PCLK settle cevrimi
-> tracking_wrapper APB register bankasi
-> measurement flag/interrupt
-> bare-metal software
```
`trck_src/tracking_measurement.sv` bir bit boyunca su degerleri toplar:
```text
sum_i = sum(I_prompt)
sum_q = sum(Q_prompt)
wbp = sum(I_prompt^2 + Q_prompt^2)
M = 20 adet 1 ms prompt ornegi
```
Prompt enerji karesi lock detector'daki mevcut multiplier sonucundan alinir;
ayni is icin ikinci bir multiplier/DSP yolu acilmaz. Bit epoch'unda total
carrier NCO frekansi Q7 formatinda, absolute sample, remaining-code phase,
navigation bit ve 16-bit sequence degeriyle birlikte snapshot edilir. Payload
bir sonraki bit olcumune kadar degismez.
Bu arayuz bir event FIFO'su degildir. Kaynak veri en az bir 20 ms bit boyunca
sabit kaldigi icin wrapper her multi-bit alani mevcut `double_ff_syn` yapisindan
gecirir, senkronize `valid` rising edge'inden sonra iki tam PCLK cevrimi bekler
ve butun bankayi tek seferde register eder. Measurement interrupt'i APB
snapshot'tan bir PCLK sonra uretilir; software interrupt'a girdiginde veri ve
status stabildir. Software yine de coherent read icin sequence degerini
payload'dan once ve sonra okuyup esitligini kontrol eder.
### APB registerlari
| Offset | Alan | Aciklama |
| ---: | --- | --- |
| `0x50` | `MEASUREMENT_META` | `[31:24]=version(1)`, `[12:8]=M`, bit 2 lock, bit 1 NAV bit, bit 0 valid |
| `0x54` | `MEASUREMENT_SEQUENCE` | 16-bit, her 20 ms olcumde artar |
| `0x58` | `MEASUREMENT_CARRIER_Q7` | Signed total carrier NCO frekansi, Hz icin `/128` |
| `0x5c` | `MEASUREMENT_ABS_SAMPLE` | Olcum epoch absolute sample |
| `0x60` | `MEASUREMENT_REM_CODE_PHASE` | 26-bit remaining-code residue |
| `0x64` | `MEASUREMENT_SUM_I` | Signed coherent prompt-I toplami |
| `0x68` | `MEASUREMENT_SUM_Q` | Signed coherent prompt-Q toplami |
| `0x6c` | `MEASUREMENT_WBP_LO` | Wideband power alt 32 bit |
| `0x70` | `MEASUREMENT_WBP_HI` | Wideband power ust bitleri |
`CONTROL_REG` bit 13 measurement interrupt enable, bit 14 measurement flag
enable'dir. `STATUS_REG` bit 7 measurement flag'dir. Genel interrupt clear
islemi bu flag'i de temizler.
### Neden Doppler IIR RTL'de degil?
IIR RTL'de de uygulanabilir; her kanal icin onceki Doppler state'i, signed
coefficient multiply, rounding/saturation ve reacquisition reset kurali
eklenmesi yeterlidir. Mevcut tasarimda bilerek software'de tutulur:
- APB bankasi ham ve tekrar analiz edilebilir carrier olcumunu korur.
- Filtre katsayisi hareket profiline gore bitstream almadan degistirilebilir.
- C/N0, satellite state ve WLS zaten software'de hesaplanir.
- Filtre veri/interrupt hizini azaltmaz; measurement yine 50 Hz'dir.
- Kart testinde kesintisiz kosuda measurement `missed`, `invalid` ve event
overflow sayaclari sifir kalmistir; CPU veya CDC baskisi RTL filtresi
gerektirmemistir.
RTL filtresi ancak bu ham register sozlesmesinin yaninda ikinci bir filtered
alan istenirse, olcum tamamen donanim icinde tuketilecekse veya software
latency/CPU butcesi kart testinde yetersiz kalirsa anlamli olur.
### RTL dogrulama
Questa testleri:
```bash
make -C hardware/external/gps/rtl/trck_src/tb test
```
Testler prompt enerjisini, 20-ornek akumulasyonunu, iki ard arda bit epoch'unu,
20 ms hold davranisini, double-FF + settle + atomik APB capture'i ve
interrupt/status-clear yolunu kontrol eder.
## SVN Revision
svn info
@@ -45,4 +133,3 @@ Last Changed Author: ankaaa
Last Changed Rev: 89
Last Changed Date: 2025-07-08 14:20:14 +0300 (Tue, 08 Jul 2025)
+134
View File
@@ -0,0 +1,134 @@
# 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.
Ortak capture zamani cikarildigi ve `STARTOFFSET` eklendigi icin
referans-uyduya bagli goreli bir datum cozer. 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.
@@ -0,0 +1,226 @@
# Efinity Interface Configuration for the Ti375C529 development kit.
# Pin assignments follow the kit user guide and the Efinity 2025.2 device DB.
# Board I/O bank voltages. Keep these aligned with the Efinix out-of-box
# Ti375C529 development-kit project.
design.set_device_property("2A", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2A", "2A_MODE_SEL", "2A")
design.set_device_property("2A", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2B", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2B", "2B_MODE_SEL", "2B")
design.set_device_property("2B", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2C", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2C", "2C_MODE_SEL", "2C")
design.set_device_property("2C", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2D", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2D", "2D_MODE_SEL", "2D")
design.set_device_property("2D", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2E", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2E", "2E_MODE_SEL", "2E")
design.set_device_property("2E", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4A_4B", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4A_4B", "4A_4B_MODE_SEL", "4A_4B")
design.set_device_property("4A_4B", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4C", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4C", "4C_MODE_SEL", "4C")
design.set_device_property("4C", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4D", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4D", "4D_MODE_SEL", "4D")
design.set_device_property("4D", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("BL2_BL3", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BL2_BL3", "BL2_MODE_SEL", "BL2")
design.set_mode_sel_name("BL2_BL3", "BL3_MODE_SEL", "BL3")
design.set_device_property("BL2_BL3", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("BR0", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BR0", "BR0_MODE_SEL", "BR0")
design.set_device_property("BR0", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("BR3_BR4", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BR3_BR4", "BR3_MODE_SEL", "BR3")
design.set_mode_sel_name("BR3_BR4", "BR4_MODE_SEL", "BR4")
design.set_device_property("BR3_BR4", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TL1_TL5", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TL1_TL5", "TL1_MODE_SEL", "TL1")
design.set_mode_sel_name("TL1_TL5", "TL5_MODE_SEL", "TL5")
design.set_device_property("TL1_TL5", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR0", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR0", "TR0_MODE_SEL", "TR0")
design.set_device_property("TR0", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR1", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR1", "TR1_MODE_SEL", "TR1")
design.set_device_property("TR1", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR2", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR2", "TR2_MODE_SEL", "TR2")
design.set_device_property("TR2", "VOLTAGE", "3.3", "IOBANK")
# External GPIO.
design.create_input_gpio("reset_n")
design.set_property("reset_n", "PULL_OPTION", "WEAK_PULLUP")
design.create_input_gpio("uart_rx")
design.create_output_gpio("uart_tx")
# Lower 512-Mbit configuration flash. After active configuration completes,
# these dedicated SPI pins become fabric GPIO and are driven by Rocket's MMIO
# SPI controller so the BootROM can read the application region.
design.create_output_gpio("flash_sck")
design.create_output_gpio("flash_csn")
design.create_inout_gpio("flash_dq", 3, 0)
# The AX2771 sample clock is routed to J13 pin 10 / GPIOR_95_CLK12 so it
# enters the fabric through a dedicated global-clock input instead of being
# promoted from normal GPIO routing by the placer.
design.create_input_clock_gpio("rf_clk")
design.create_input_gpio("rf_data", 3, 0)
design.create_output_gpio("pmod_spi_sck")
design.create_output_gpio("pmod_spi_csn")
design.create_inout_gpio("pmod_spi_dq", 3, 0)
design.create_input_gpio("pmod_irq")
design.create_output_gpio("gps_pps")
design.create_output_gpio("led", 4, 0)
# The two PMOD headers, FTDI UART, reset switch, 100 MHz oscillator, and
# LED9/LED10 use the development kit's 3.3 V banks. GPIOs otherwise default
# to 1.8 V LVCMOS, so state these assignments explicitly.
design.set_property("reset_n", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("uart_rx", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("uart_tx", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("flash_sck", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_csn", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[0]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[1]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[2]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[3]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_csn", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[0]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[1]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[2]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[3]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("rf_clk", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[1]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[2]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[3]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_sck", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_csn", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[1]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[2]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[3]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_irq", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("gps_pps", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("led[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("led[1]", "IO_STANDARD", "3.3_V_LVCMOS")
# Rocket/fabric clock: 100 MHz board oscillator -> 100 MHz GCLK.
design.create_block("fabric_pll", "PLL")
design.gen_pll_ref_clock("fabric_pll", pll_res="PLL_BL0", refclk_src="EXTERNAL", refclk_name="board_clk_100", ext_refclk_no="1")
design.set_property("board_clk_100", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("fabric_pll", "REFCLK_FREQ", "100", "PLL")
design.set_property("fabric_pll", "CLKOUT0_PIN", "fabric_clk", "PLL")
design.set_property("fabric_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("fabric_pll", "LOCKED_PIN", "fabric_pll_locked", "PLL")
design.auto_calc_pll_clock("fabric_pll", {"CLKOUT0_FREQ": "100.0", "CLKOUT0_PHASE": "0"})
# RF-coherent GPS processing clocks. The AX2771's 16.367981 MHz clock enters
# the fabric on J13 pin 10 / A12 (GPIOR_95_CLK12) and drives the same two-stage
# clock chain used by the Xilinx targets:
# RF -> ~55.55556 MHz -> 100 MHz acquisition / 125 MHz tracking.
design.create_block("gps_ref_pll", "PLL")
design.gen_pll_ref_clock("gps_ref_pll", pll_res="PLL_TR0", refclk_src="CORE")
design.set_property("gps_ref_pll", "CORE_CLK_PIN", "rf_clk", "PLL")
design.set_property("gps_ref_pll", "REFCLK_FREQ", "16.367981", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_EN", "1", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_PIN", "gps_ref_clk_gen", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_PIN", "gps_ref_feedback", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_ref_pll", "LOCKED_PIN", "gps_ref_pll_locked", "PLL")
design.set_property("gps_ref_pll", "FEEDBACK_MODE", "LOCAL", "PLL")
design.set_property("gps_ref_pll", "FEEDBACK_CLK", "CLK1", "PLL")
design.set_property("gps_ref_pll", "FRACTIONAL_MODE_EN", "1", "PLL")
design.set_property("gps_ref_pll", "FRACTIONAL_COEFFICIENT", "10715375", "PLL")
design.set_property("gps_ref_pll", "M", "4", "PLL")
design.set_property("gps_ref_pll", "N", "1", "PLL")
design.set_property("gps_ref_pll", "O", "1", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_DIV", "42", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_DIV", "35", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_PDIV", "5", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_SDIV", "30", "PLL")
design.calc_pll_clock("gps_ref_pll")
design.create_block("gps_domain_pll", "PLL")
design.gen_pll_ref_clock("gps_domain_pll", pll_res="PLL_TR1", refclk_src="CORE")
design.set_property("gps_domain_pll", "CORE_CLK_PIN", "gps_ref_clk_gen", "PLL")
design.set_property("gps_domain_pll", "REFCLK_FREQ", "55.55555593", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_EN", "1", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_EN", "1", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_PIN", "gps_domain_feedback", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_PIN", "gps_acq_clk_gen", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_PIN", "gps_trck_clk_gen", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "LOCKED_PIN", "gps_domain_pll_locked", "PLL")
design.set_property("gps_domain_pll", "FEEDBACK_MODE", "LOCAL", "PLL")
design.set_property("gps_domain_pll", "FEEDBACK_CLK", "CLK0", "PLL")
design.set_property("gps_domain_pll", "M", "2", "PLL")
design.set_property("gps_domain_pll", "N", "1", "PLL")
design.set_property("gps_domain_pll", "O", "2", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_DIV", "18", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_DIV", "20", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_DIV", "16", "PLL")
design.calc_pll_clock("gps_domain_pll")
# Hard JTAG USER1 exposes the TAP handshake at the FPGA core boundary.
design.create_block("rocket_jtag", "JTAG")
design.set_property("rocket_jtag", "NAME", "rocket_jtag", "JTAG")
design.set_property("rocket_jtag", "RESOURCE", "JTAG_USER1", "JTAG")
design.set_property("rocket_jtag", "CAPTURE", "jtag_CAPTURE", "JTAG")
design.set_property("rocket_jtag", "DRCK", "jtag_DRCK", "JTAG")
design.set_property("rocket_jtag", "RESET", "jtag_RESET", "JTAG")
design.set_property("rocket_jtag", "RUNTEST", "jtag_RUNTEST", "JTAG")
design.set_property("rocket_jtag", "SEL", "jtag_SEL", "JTAG")
design.set_property("rocket_jtag", "SHIFT", "jtag_SHIFT", "JTAG")
design.set_property("rocket_jtag", "TCK", "jtag_TCK", "JTAG")
design.set_property("rocket_jtag", "TDI", "jtag_TDI", "JTAG")
design.set_property("rocket_jtag", "TMS", "jtag_TMS", "JTAG")
design.set_property("rocket_jtag", "UPDATE", "jtag_UPDATE", "JTAG")
design.set_property("rocket_jtag", "TDO", "jtag_TDO", "JTAG")
design.assign_resource("rocket_jtag", "JTAG_USER1", "JTAG")
# Package assignments.
design.assign_pkg_pin("board_clk_100", "U4")
design.assign_pkg_pin("reset_n", "U19")
design.assign_pkg_pin("uart_rx", "E10")
design.assign_pkg_pin("uart_tx", "E9")
# Lower configuration flash U47: CDI0..3, CCK, and SSL_N. These assignments
# match Efinix's Ti375C529 JTAG-to-SPI loader reference design.
design.assign_pkg_pin("flash_dq[0]", "H4")
design.assign_pkg_pin("flash_dq[1]", "H5")
design.assign_pkg_pin("flash_dq[2]", "G5")
design.assign_pkg_pin("flash_dq[3]", "B7")
design.assign_pkg_pin("flash_sck", "G6")
design.assign_pkg_pin("flash_csn", "A7")
# PMOD0/J13 RF input. J13 pin 10 is the header's dedicated GCLK-capable pin;
# J13 pin 1 / G15 is intentionally unused.
design.assign_pkg_pin("rf_clk", "A12")
design.assign_pkg_pin("rf_data[0]", "G16")
design.assign_pkg_pin("rf_data[1]", "F16")
design.assign_pkg_pin("rf_data[2]", "F17")
design.assign_pkg_pin("rf_data[3]", "G17")
# PMOD1/J14 SPI + interrupt + PPS.
design.assign_pkg_pin("pmod_spi_csn", "B12")
design.assign_pkg_pin("pmod_spi_dq[0]", "C14")
design.assign_pkg_pin("pmod_spi_dq[1]", "C13")
design.assign_pkg_pin("pmod_spi_sck", "C12")
design.assign_pkg_pin("pmod_spi_dq[2]", "D12")
design.assign_pkg_pin("pmod_spi_dq[3]", "F12")
design.assign_pkg_pin("pmod_irq", "D13")
design.assign_pkg_pin("gps_pps", "E13")
# User LEDs LED9..LED13.
design.assign_pkg_pin("led[0]", "V4")
design.assign_pkg_pin("led[1]", "U6")
design.assign_pkg_pin("led[2]", "L18")
design.assign_pkg_pin("led[3]", "R20")
design.assign_pkg_pin("led[4]", "E19")
@@ -0,0 +1,8 @@
# AX2771 sample clock on PMOD J13. The 100/125 MHz GPS clocks generated by the
# two Efinity PLLs are frequency-related to this input; the Rocket fabric clock
# comes from the independent on-board 100 MHz oscillator.
create_clock -period 61.09495 -name rf_clk [get_ports {rf_clk}]
# The FTDI adapter is normally run at 1 MHz. Constraining the hard-TAP fabric
# interface to 10 MHz leaves margin if the debug adapter speed is increased.
create_clock -period 100.000 -name jtag_TCK [get_ports {jtag_TCK}]
@@ -0,0 +1,10 @@
# Efinity's generated .pt.sdc is read before this file and defines the fabric
# and PLL-output clocks. The Rocket oscillator, AX2771-derived GPS clocks, and
# hard-TAP JTAG clock are independent domains.
set_clock_groups -asynchronous \
-group [get_clocks {fabric_clk}] \
-group [get_clocks {rf_clk}] \
-group [get_clocks {gps_ref_clk_gen}] \
-group [get_clocks {gps_acq_clk_gen}] \
-group [get_clocks {gps_trck_clk_gen}] \
-group [get_clocks {jtag_TCK}]
+18
View File
@@ -0,0 +1,18 @@
.PHONY: help all shell generated clean
help:
@echo "Ti375C529 RTL targets:"
@echo " make shell # list hand-written FPGA shell RTL"
@echo " make generated # generate Rocket SystemVerilog and Efinix wrapper"
@echo " make clean # remove generated wrapper/verilog"
all: generated
shell:
@find . -type f -name '*.sv' -print | sort
generated:
@$(MAKE) -C ../../../../targets/fpga/ti375c529 rtl
clean:
rm -rf ../../../generated/ti375c529-soc-verilog ../../../../targets/fpga/ti375c529/out/build/generated
+4 -3
View File
@@ -18,9 +18,9 @@ JDK_SHA256="d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331"
MILL_URL="https://github.com/com-lihaoyi/mill/releases/download/$MILL_VERSION/$MILL_VERSION"
MILL_SHA256="ebe83649cd63acd33608f6d14452c074be2ed7d00cd2f69f88573d3f6909774f"
CIRCT_ARCHIVE="circt-full-shared-linux-x64.tar.gz"
CIRCT_ARCHIVE="firrtl-bin-linux-x64.tar.gz"
CIRCT_URL="https://github.com/llvm/circt/releases/download/firtool-$FIRTOOL_VERSION/$CIRCT_ARCHIVE"
CIRCT_SHA256="dde442dcd0ada92e6f7db26f4744667471f7be61d10f2876297efb435fc9a5ec"
CIRCT_SHA256="10bf9b135a65e5d93fb0555e967eac4258400ee629731ba68a69be6b7f9781ce"
Z3_ARCHIVE="z3-$Z3_VERSION-x64-glibc-2.39.zip"
Z3_URL="https://github.com/Z3Prover/z3/releases/download/z3-$Z3_VERSION/$Z3_ARCHIVE"
@@ -85,7 +85,8 @@ if [[ ! -x "$TOOLS_ROOT/mill/$MILL_VERSION/mill" ]]; then
"${SUDO[@]}" install -m 0755 "$work_dir/mill" "$TOOLS_ROOT/mill/$MILL_VERSION/mill"
fi
if [[ ! -x "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" ]]; then
if ! "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" --version \
>/dev/null 2>&1; then
download "$CIRCT_URL" "$work_dir/$CIRCT_ARCHIVE" "$CIRCT_SHA256"
mkdir -p "$work_dir/circt"
tar -xzf "$work_dir/$CIRCT_ARCHIVE" -C "$work_dir/circt"
+8 -4
View File
@@ -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)"
+1 -1
View File
@@ -13,7 +13,7 @@ APP_FLASH_OFFSET ?= 0x06000000
MARCH ?= rv64imafdc_zicsr_zifencei
MABI ?= lp64d
BSP_UART_CLOCK_HZ ?= 100000000
BSP_UART_BAUDRATE ?= 115200
BSP_UART_BAUDRATE ?= 460800
BSP_DTIM_BASE ?= 0x80000000
BSP_DTIM_SIZE ?= 0x00100000
CONFIG_STAMP := $(BUILD_DIR)/build_config.txt
+1 -1
View File
@@ -7,7 +7,7 @@
#endif
#ifndef BSP_UART_BAUDRATE
#define BSP_UART_BAUDRATE 115200UL
#define BSP_UART_BAUDRATE 460800UL
#endif
void bsp_init(void)
+1 -1
View File
@@ -13,7 +13,7 @@ LINKER_SCRIPT ?= $(COMMON_DIR)/linker_ram.ld
MARCH ?= rv64imafdc_zicsr_zifencei
MABI ?= lp64d
BSP_UART_CLOCK_HZ ?= 100000000
BSP_UART_BAUDRATE ?= 115200
BSP_UART_BAUDRATE ?= 460800
BSP_CLINT_TIMEBASE_HZ ?= 1000000
BSP_DTIM_BASE ?= 0x80000000
BSP_DTIM_SIZE ?= 0x00100000
+149 -21
View File
@@ -6,6 +6,22 @@ Bu klasör Genesys2 kartından UART ile alınan GPS WLS konumlarını ve PMOD SP
ve jiroskop biası. Kartın durağan olduğu deneyde zero-velocity update (ZUPT)
uygulanır ve GPS innovation değerlerine aykırı-ölçüm kapısı konur.
Bare-metal ürün firmware'i ana dizinden iki profille üretilebilir:
```sh
# TDK + GPS + ESKF (varsayılan GPS_ESKF_ENABLE=1)
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-mcs
# GPS-only; IMU/ESKF kapalı, WLS/Doppler/PPS açık
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=0 fpga-mcs
```
Firmware ve UART kayıt aracının varsayılan hızı `460800` baud'dur. İki profil
aynı MCS/BIN çıktı yolunu kullandığı için ikinci build ilk dosyanın üzerine
yazar.
## Güncel durum ve devralma notu (2026-07-16)
Bu bölüm yeni bir geliştirme oturumunun nereden devam edeceğini tek yerde
@@ -66,11 +82,67 @@ 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 capture'lar ortak receiver zamanına göre
farklandığı ve `STARTOFFSET` eklendiği için saf receiver clock bias'i değil,
referans-uyduya bağlı göreli bir datumdur. Bu yüzden position
`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 +159,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 +198,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 +221,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 +236,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 +270,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ı:
@@ -369,9 +485,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
@@ -546,8 +664,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:
@@ -636,16 +756,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
@@ -696,8 +821,11 @@ yatay RMSE'si 0.959 m, false-stationary sayısı sıfır oldu.
Bu sonuç GPS Doppler velocity'nin neden değerli olduğunu da gösterir. Position
farkından stationary/moving ayırımı düşük hızda ve kısa periodda zayıftır.
İleride GPS modülü güvenilir Doppler velocity ve covariance üretirse ayrı bir
velocity measurement update ve daha hızlı stop confirmation eklenmelidir.
Ürün yolu artık WLS'nin ECEF velocity covariance'ini ENU'ya döndürüp ayrı
velocity measurement update'inde kullanır. Default `2.0/2.0/3.0 m/s`
covariance floor ve 3-DOF `%99.9` NIS gate kartta ölçülen zayıf-geometri
saçılımına karşı konservatiftir. Daha hızlı stop confirmation, bu update'ten
ayrı ve çoklu-epoch istatistiksel kanıt gerektiren sonraki adımdır.
ASan ve UBSan hata vermedi. Sekiz adet 60 s hareketli koşu ile 30 s/500 Hz
stationary koşunun tamamı masaüstünde yaklaşık 0.70 s user CPU süresinde
+1 -1
View File
@@ -14,7 +14,7 @@ import serial
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--port", default="/dev/ttyUSB2")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--baud", type=int, default=460800)
parser.add_argument("--duration", type=float, default=60.0)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
+143 -13
View File
@@ -231,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))
@@ -579,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)
@@ -621,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);
}
}
@@ -651,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),
@@ -727,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)
@@ -795,6 +923,8 @@ int main(void)
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,3 @@
__pycache__/
*.py[cod]
.venv/
@@ -0,0 +1,241 @@
# GPS code measurement ve DLL kalite modeli
Bu dizin, mevcut firmware/RTL ölçüm yolunu değiştirmeden önce code-phase
hassasiyeti, DLL jitter telemetrisi, dinamik WLS sigma ve carrier/Doppler
smoothing seçeneklerini aynı sentetik veri üzerinde karşılaştırır.
Bu genel amaçlı bir RF/GNSS simülatörü değildir. Projeye özgü executable
tasarım modelidir.
Dizin iki ayrı kararı inceler:
1. Position WLS için C/N0'ın açıklamadığı DLL jitter/variance telemetrisi.
2. Velocity WLS ve ESKF öncesindeki sabit Doppler IIR'in gürültü-gecikme
etkisi.
## Modellenen mevcut mimari
| Proje davranışı | Model |
|---|---|
| Normalized early-minus-late DLL discriminator | `rtl_dll_model.rtl_discriminator()` |
| Signed Q2.13 discriminator quantization | `rtl_dll_model.py` |
| `LF_code.sv` C1=`21.25`, C2=`0.05712890625` ve Q20 çıkış | `CodeLoopFilterRtl` |
| 20 adet 1 ms DLL örneği/NAV bit | `synthesize_dll_window()` |
| Firmware C/N0 IIR, alpha=`0.2` | `ChannelQuality.update()` |
| URA/elevation/C/N0 position sigma | `firmware_sigma_m(CURRENT)` |
| ECEF x/y/z + common clock-bias WLS | `firmware_wls()` |
| 99% chi-square ve en az 7 uyduda leave-one-out FDE | `firmware_wls()` |
| Position solve periyodu 200 ms | `measurement_scenarios.py` |
| Doppler IIR alpha=`0.05`, 50 Hz | `doppler_eskf_model.py` |
| ECEF velocity + common clock-drift WLS | `velocity_wls()` |
| Velocity WLS periyodu 200 ms | `doppler_eskf_model.py` |
| TDK 100 Hz IMU process-noise profili | `VelocityBiasEskf` |
| Velocity 3-DOF `%99.9`, NIS=`16.266236` | `VelocityBiasEskf.update_velocity()` |
Kaynak eşlemeleri:
- `hardware/external/gps/rtl/trck_src/code_loop_discriminator.sv`
- `hardware/external/gps/rtl/trck_src/LF_code.sv`
- `hardware/external/gps/rtl/trck_src/NCO_code.sv`
- `hardware/external/gps/rtl/trck_src/tracking_measurement.sv`
- `hardware/external/gps/rtl/common/definitions.sv`
- `soc/software/gps_system/src/gps/gps_tracking_measurement.c`
- `soc/software/gps_system/src/gps/gps_interrupt_handlers.c`
- `soc/software/gps_system/src/gps/NavEquations.c`
`REM_CODE_PHASE`, DLL discriminator residual'ı olarak modellenmez. RTL'de bu
değer code NCO'nun epoch taşma remainder'ıdır. Yeni kalite metriğinin girdisi
RTL içindeki gerçek signed `dll_error` örnekleridir.
## Karşılaştırılan sigma politikaları
- `current_cn0`: mevcut URA + elevation + C/N0 modeli.
- `naive_add_dll`: DLL variance'ı bağımsızmış gibi ekler. Nominal durumda aynı
thermal jitter'ı iki kere saydığı için negatif kontrol olarak bulunur.
- `dll_excess`: yalnız ölçülen DLL variance'ın C/N0'dan beklenen variance'ı aşan
bölümünü code sigma'ya ekler.
- `dll_excess_guard`: excess variance'a ek olarak sürekli discriminator mean
stress'inde ağırlığı azaltır.
- `oracle_variance`: sentetik gerçek jitter çarpanını bilir. Ulaşılabilir üst
sınırı gösterir; firmware algoritması değildir.
Dinamik model:
```text
sigma_base^2 = sigma_URA^2 + sigma_elevation^2 + sigma_CN0^2
ratio = EWMA(measured_DLL_variance / expected_DLL_variance(C/N0))
sigma_dynamic^2 =
sigma_URA^2
+ sigma_elevation^2
+ sigma_CN0^2 * max(1, ratio)
```
Bu form C/N0 ile DLL thermal jitter'ını iki kere toplamaz. Yalnızca C/N0
modelinin açıklayamadığı kanal-bağımlı excess jitter'ı şişirir.
## Senaryolar
- Temiz açık gökyüzü
- Bütün kanallarda düşük C/N0
- C/N0'da görünmeyen tek-kanal DLL jitter
- Bütün kanallarda ortak, C/N0'da görünmeyen DLL jitter
- Gürültü burst ve recovery
- Re-lock sırasında DLL mean/jitter stress
- Düşük-variance stable multipath bias
- Bias + excess jitter
- Kötü geometri + gizli jitter
- İlk başta düşük, sonra iyileşen C/N0
Ayrı 1D model ham code, kör scalar IIR ve Doppler-predicted code smoothing'i
range-rate adımları ve residual carrier CFO altında karşılaştırır. Carrier
ölçümleri RTL cadence'i olan 50 Hz'de üretilir ve 200 ms WLS epoch'una kadar
entegre edilir. Ham, sabit `alpha=0.05` ve IMU hareket durumuna göre bant
genişliğini açan adaptif Doppler yolları birlikte denenir. Bu ayrım önemlidir:
PPS datum-drift predictor'u için yararlı olan yavaş Doppler filtresi, ESKF
velocity veya hızlı code-aiding prediction'ında dinamik lag üretebilir; tam
ham snapshot ise kartta ölçülen gürültü seviyesinde fazla varyans üretebilir.
## Doppler pre-IIR ve ESKF modeli
`doppler_eskf_model.py`, aynı truth, tracking gürültüsü ve IMU örnekleriyle şu
dört yolu karşılaştırır:
- Önceki: 50 Hz `alpha=0.05` IIR -> 5 Hz velocity WLS -> ESKF.
- Ham Doppler -> 5 Hz velocity WLS -> ESKF.
- C/N0 + düşük-geçiren navigation acceleration ile adaptif IIR -> WLS -> ESKF.
- Ham Doppler -> RAIM-benzeri leave-one-out robust WLS -> ESKF.
Mevcut `alpha=0.05` tek-kutuplu IIR için 50 Hz'de eşdeğer cutoff yaklaşık
`0.408 Hz`, zaman sabiti `0.390 s`, DC group delay ise yaklaşık `380 ms`'dir.
Bu gecikme constant velocity'de görünmez; acceleration sırasında velocity
measurement innovation'ına doğrudan girer.
ESKF modeli bu karar için gerekli `[velocity, accel-bias]` alt sistemidir.
Firmware'deki TDK process acceleration noise density `0.025
m/s2/sqrt(Hz)`, accelerometer bias random walk `0.0015 m/s2/sqrt(s)`, WLS'nin
gerçek 3x3 covariance'ı, fusion adapter'ın `2/2/3 m/s` ENU velocity sigma
tabanları ve velocity NIS kapısı kullanılır. Position, attitude, lever-arm ve
tam 15-state cross-covariance bu modelin kapsamında değildir.
Senaryo matrisi sabit uçuş, airliner/aggressive manevra, düşük C/N0, tek-kanal
Doppler burst, common receiver clock-drift step, 15 saniye GPS kesintisi ve
birleşik arıza koşullarını içerir.
Nominal 20-seed sentetik sonuçların özeti:
- ESKF ham WLS gürültüsünü nominalde yaklaşık `0.416 m/s` 3D RMS'ten `0.190
m/s` seviyesine indirir; temporal filtreleme zaten ESKF'de gerçekleşir.
- Sabit pre-IIR sakin uçuşta `0.182 m/s`, ham yol `0.190 m/s` verir. Airliner
manevrasında sabit IIR gecikmesi sonucu `0.416 m/s`, ham yol `0.190 m/s`;
aggressive manevrada sırasıyla `0.942 m/s` ve `0.190 m/s` oluşur. Adapter
sigma tabanları iki yolu da NIS içinde tutsa bile dinamik lag kalır.
- Düşük C/N0 manevrasında adaptif taslak en iyi gürültü/gecikme uzlaşmasını
verir; eşikleri firmware'e taşımadan önce kart loguyla kalibre etmek gerekir.
- ESKF yalnız aggregate 3D velocity gördüğünden tek uydu burst'ünün kaynağını
ayıramaz. Kanal outlier/FDE işlemi velocity WLS öncesinde yapılmalıdır.
- Common receiver clock-frequency hatası WLS'nin dördüncü clock-drift state'i
tarafından hızdan ayrılır.
Genesys2 sabit-kart ölçümünde tam ham yolun velocity-WLS RMS'i `9.17 m/s`,
p95'i `16.71 m/s` ve FDE oranı `86/120` oldu. Aynı epoch'larda sabit
`alpha=0.05` yol `0.92 m/s` RMS, `1.63 m/s` p95 ve `0/120` FDE verdi. Bu
nedenle nominal modelde iyi görünen doğrudan ham yol production'a alınmadı.
Kart gürültüsünü eşlemek için tracking noise `22x` ile kalibre edilen regresyon
modelindeki regresyon seed'leri için steady-7/airliner-3/aggressive-7 fused
RMS değerleri:
- sabit IIR: `0.451 / 0.484 / 1.036 m/s`,
- adaptif IIR: `0.451 / 0.336 / 0.538 m/s`,
- tam ham: `1.260 / 1.100 / 1.260 m/s`.
Production firmware ESKF için `alpha=0.05/0.20/0.40` hareket-adaptif Doppler
-> bandwidth-aware sigma -> kanal validity/FDE -> velocity WLS + covariance
-> ESKF yolunu kullanır. Ham alan tanısal kayıt ve offline analiz için korunur.
Ayrı sabit `alpha=0.05` Doppler velocity yalnız PPS position-WLS clock-bias
datumunun drift predictor'u ve PPS kalite metriği için kullanılır. Ayrıntılı
nominal tablo ve model sınırları `output/DOPPLER_ESKF_ANALYSIS.md` içindedir;
kart-kalibreli karar `test_doppler_eskf_model.py` ile regresyona alınmıştır.
## Kurulum ve çalıştırma
Repo içindeki `soc/software` dizininden:
```bash
python3 -m venv gps_measurement_python/.venv
gps_measurement_python/.venv/bin/pip install \
-r gps_measurement_python/requirements.txt
gps_measurement_python/.venv/bin/python -m unittest \
gps_measurement_python/test_measurement_model.py \
gps_measurement_python/test_doppler_eskf_model.py
gps_measurement_python/.venv/bin/python \
-m gps_measurement_python.analyze_measurement_scenarios \
--seeds 12 --jobs 4
gps_measurement_python/.venv/bin/python \
-m gps_measurement_python.analyze_doppler_eskf \
--seeds 20 --jobs 4
```
Sistem Python'unda `venv` paketi yoksa bağımlılıklar izole bir target dizinine
de kurulabilir:
```bash
python3 -m pip install --target /tmp/gps-measurement-deps \
-r gps_measurement_python/requirements.txt
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
-m unittest gps_measurement_python/test_measurement_model.py
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
-m gps_measurement_python.analyze_measurement_scenarios \
--seeds 12 --jobs 4
```
Çıktılar `gps_measurement_python/output/` altındadır:
- `scenario_summary.csv`
- `ANALYSIS.md`
- `01_position_3d_rms.png`
- `02_solution_availability.png`
- `03_noise_burst_transition.png`
- `04_code_smoothing_cfo.png`
- `doppler_eskf_summary.csv`
- `DOPPLER_ESKF_ANALYSIS.md`
- `05_doppler_eskf_velocity_rms.png`
- `06_airliner_maneuver_trace.png`
- `07_outlier_wls_nis.png`
- `08_low_cn0_wls_vs_eskf.png`
## Kalibrasyon sınırı
Modelde C/N0'dan beklenen normalized discriminator standard deviation'ı
45 dB-Hz için `0.020` kabul edilir ve firmware ile aynı 20 dB amplitude yasası
ile ölçeklenir. Bu sayı karttan alınmış bir kalibrasyon değildir.
Karar vermeden önce karttan her kanal için en az şu 50 Hz kayıt alınmalıdır:
```text
sequence, PRN, C/N0,
DLL mean, DLL variance, DLL sample count,
code NCO correction, carrier frequency,
ABS_SAMPLE, REM_CODE_PHASE, lock
```
Kayıtlarla `expected_DLL_variance(C/N0)` eğrisi çıkarılmalı; temperature,
elevation ve lock yaşına göre ayrıştırılmalıdır. Stable multipath, DLL yeni
denge noktasına oturduğunda düşük variance bırakabilir. Bu durum tek başına
DLL variance ile çözülemez; ek correlator tap'leri veya correlation-shape
metriği gerekir.
RTL için en düşük maliyetli keşif arayüzü, her 20 ms NAV-bit penceresinde
`dll_error` üzerinden aşağıdaki ham istatistikleri biriktirmektir:
```text
signed sum(error) : 15 + ceil(log2(20)) = 20 bit
unsigned sum(error^2) : 30 + ceil(log2(20)) = 35 bit
valid sample count : 5 bit
```
35-bit kare toplamı APB'de iki 32-bit register ile taşınabilir. Ortalama,
sample variance, C/N0 expectation ratio ve hysteresis yazılımda hesaplanır.
Bölme ve square-root RTL'ye konmaz. Bu tuple mevcut measurement sequence,
C/N0, carrier frequency, `ABS_SAMPLE` ve `REM_CODE_PHASE` ile aynı 20 ms
snapshot'ta tutulmalıdır.
@@ -0,0 +1 @@
"""Project-specific GPS code-measurement and WLS analysis model."""
@@ -0,0 +1,428 @@
"""Monte Carlo comparison of Doppler prefilter choices before velocity ESKF."""
from __future__ import annotations
import argparse
import csv
import math
from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from .doppler_eskf_model import (
DopplerEskfTrace,
DopplerPath,
PATH_ORDER,
VELOCITY_NIS_GATE,
flight_scenarios,
run_doppler_eskf_scenario,
)
PATH_LABEL = {
DopplerPath.FIXED_IIR: "Önceki sabit IIR",
DopplerPath.RAW: "Ham Doppler + ESKF",
DopplerPath.ADAPTIVE_IIR: "Adaptif IIR + ESKF",
DopplerPath.RAW_ROBUST: "Ham + robust WLS + ESKF",
}
@dataclass(frozen=True)
class TrialMetrics:
scenario: str
path: DopplerPath
fused_rms_mps: float
fused_p95_mps: float
quiet_rms_mps: float
dynamic_rms_mps: float
wls_rms_mps: float
wls_p95_mps: float
clock_drift_rms_mps: float
velocity_accept_percent: float
nis_reject_percent: float
fused_95_coverage_percent: float
excluded_epoch_percent: float
mean_covariance_scale: float
mean_alpha: float
@dataclass(frozen=True)
class Summary:
scenario: str
path: DopplerPath
seeds: int
fused_rms_mps: float
fused_rms_seed_std_mps: float
fused_p95_mps: float
quiet_rms_mps: float
dynamic_rms_mps: float
wls_rms_mps: float
wls_p95_mps: float
clock_drift_rms_mps: float
velocity_accept_percent: float
nis_reject_percent: float
fused_95_coverage_percent: float
excluded_epoch_percent: float
mean_covariance_scale: float
mean_alpha: float
def _rms(values: np.ndarray) -> float:
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
def _truth_at_wls(trace: DopplerEskfTrace) -> np.ndarray:
return np.column_stack(
[
np.interp(
trace.wls_time_s,
trace.time_s,
trace.truth_velocity_mps[:, axis],
)
for axis in range(3)
]
)
def trial_metrics(scenario_index: int, trace: DopplerEskfTrace) -> TrialMetrics:
scenario = flight_scenarios()[scenario_index]
burn_in = trace.time_s >= 5.0
fused_vector_error = trace.fused_velocity_mps - trace.truth_velocity_mps
fused_error = np.linalg.norm(fused_vector_error, axis=1)
dynamic = burn_in & (np.linalg.norm(trace.acceleration_mps2, axis=1) > 0.25)
quiet = burn_in & ~dynamic
fused_nees = []
for sample in np.flatnonzero(burn_in):
covariance = trace.fused_velocity_covariance_mps2[sample]
error = fused_vector_error[sample]
fused_nees.append(float(error @ np.linalg.inv(covariance) @ error))
fused_nees_array = np.asarray(fused_nees)
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
true_clock_drift = np.asarray(
[scenario.receiver_clock_drift_mps(float(time)) for time in trace.wls_time_s]
)
clock_error = trace.wls_clock_drift_mps - true_clock_drift
valid_nis = trace.wls_nis[np.isfinite(trace.wls_nis)]
return TrialMetrics(
scenario=trace.scenario,
path=trace.path,
fused_rms_mps=_rms(fused_error[burn_in]),
fused_p95_mps=float(np.percentile(fused_error[burn_in], 95.0)),
quiet_rms_mps=_rms(fused_error[quiet]),
dynamic_rms_mps=_rms(fused_error[dynamic]),
wls_rms_mps=_rms(wls_error),
wls_p95_mps=float(np.percentile(wls_error, 95.0)),
clock_drift_rms_mps=_rms(clock_error),
velocity_accept_percent=100.0 * float(np.mean(trace.wls_accepted)),
nis_reject_percent=100.0 * float(np.mean(valid_nis > VELOCITY_NIS_GATE)),
# 7.8147 is the 95-percent chi-square threshold for three states.
fused_95_coverage_percent=100.0 * float(np.mean(fused_nees_array <= 7.8147279)),
excluded_epoch_percent=100.0
* float(np.mean(trace.excluded_satellites >= 0)),
mean_covariance_scale=float(np.mean(trace.wls_covariance_scale)),
mean_alpha=float(np.mean(trace.adaptive_alpha)),
)
def _worker(task: tuple[int, int]) -> tuple[int, int, list[TrialMetrics]]:
scenario_index, seed = task
scenario = flight_scenarios()[scenario_index]
traces = run_doppler_eskf_scenario(scenario, seed)
return (
scenario_index,
seed,
[trial_metrics(scenario_index, traces[path]) for path in PATH_ORDER],
)
def summarize(items: list[TrialMetrics]) -> Summary:
if not items:
raise ValueError("cannot summarize an empty trial list")
def average(field_name: str) -> float:
values = np.asarray([getattr(item, field_name) for item in items], dtype=float)
finite = values[np.isfinite(values)]
return float(np.mean(finite)) if len(finite) else math.nan
fused_rms = np.asarray([item.fused_rms_mps for item in items])
return Summary(
scenario=items[0].scenario,
path=items[0].path,
seeds=len(items),
fused_rms_mps=average("fused_rms_mps"),
fused_rms_seed_std_mps=float(np.std(fused_rms)),
fused_p95_mps=average("fused_p95_mps"),
quiet_rms_mps=average("quiet_rms_mps"),
dynamic_rms_mps=average("dynamic_rms_mps"),
wls_rms_mps=average("wls_rms_mps"),
wls_p95_mps=average("wls_p95_mps"),
clock_drift_rms_mps=average("clock_drift_rms_mps"),
velocity_accept_percent=average("velocity_accept_percent"),
nis_reject_percent=average("nis_reject_percent"),
fused_95_coverage_percent=average("fused_95_coverage_percent"),
excluded_epoch_percent=average("excluded_epoch_percent"),
mean_covariance_scale=average("mean_covariance_scale"),
mean_alpha=average("mean_alpha"),
)
def write_csv(path: Path, summaries: list[Summary]) -> None:
with path.open("w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
writer.writeheader()
for item in summaries:
row = dict(item.__dict__)
row["path"] = item.path.value
writer.writerow(row)
def _summary_plot(path: Path, summaries: list[Summary]) -> None:
scenarios = flight_scenarios()
x = np.arange(len(scenarios))
width = 0.19
figure, axis = plt.subplots(figsize=(15, 6.3))
for path_index, doppler_path in enumerate(PATH_ORDER):
values = [
next(
item.fused_rms_mps
for item in summaries
if item.scenario == scenario.name and item.path == doppler_path
)
for scenario in scenarios
]
axis.bar(
x + (path_index - 1.5) * width,
values,
width,
label=PATH_LABEL[doppler_path],
)
axis.set_ylabel("ESKF 3D velocity RMS (m/s)")
axis.set_yscale("log")
axis.set_xticks(x)
axis.set_xticklabels([item.name for item in scenarios], rotation=25, ha="right")
axis.grid(axis="y", alpha=0.25)
axis.legend(ncol=2)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _maneuver_trace_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
reference = trace_set[DopplerPath.FIXED_IIR]
figure, axes = plt.subplots(3, 1, figsize=(13, 8.5), sharex=True)
for doppler_path in PATH_ORDER:
trace = trace_set[doppler_path]
error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
axes[0].plot(trace.time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
axes[1].plot(
reference.time_s,
np.linalg.norm(reference.acceleration_mps2, axis=1),
color="black",
linewidth=1.2,
label="True |acceleration|",
)
adaptive = trace_set[DopplerPath.ADAPTIVE_IIR]
axes[2].step(
adaptive.wls_time_s,
adaptive.adaptive_alpha,
where="post",
color="tab:green",
)
axes[0].set_ylabel("3D velocity error (m/s)")
axes[1].set_ylabel("Acceleration (m/s²)")
axes[2].set_ylabel("Adaptive alpha")
axes[2].set_xlabel("Time (s)")
axes[0].set_ylim(bottom=0.0)
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _outlier_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
for doppler_path in PATH_ORDER:
trace = trace_set[doppler_path]
error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
axes[0].plot(trace.wls_time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
axes[1].plot(trace.wls_time_s, trace.wls_nis, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[1].axhline(VELOCITY_NIS_GATE, color="black", linestyle="--", linewidth=1.0)
for start, stop in ((30.0, 31.0), (56.0, 57.0), (72.0, 72.6)):
for axis in axes:
axis.axvspan(start, stop, color="tab:red", alpha=0.10)
axes[0].set_ylabel("WLS velocity error (m/s)")
axes[1].set_ylabel("Velocity NIS")
axes[1].set_xlabel("Time (s)")
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _filtering_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
for doppler_path in (DopplerPath.FIXED_IIR, DopplerPath.RAW, DopplerPath.ADAPTIVE_IIR):
trace = trace_set[doppler_path]
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
fused_error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
axes[0].plot(trace.wls_time_s, wls_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[1].plot(trace.time_s, fused_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
axes[0].set_ylabel("WLS 3D error (m/s)")
axes[1].set_ylabel("ESKF 3D error (m/s)")
axes[1].set_xlabel("Time (s)")
axes[0].legend(ncol=2)
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _lookup(
summaries: list[Summary], scenario: str, doppler_path: DopplerPath
) -> Summary:
return next(
item
for item in summaries
if item.scenario == scenario and item.path == doppler_path
)
def write_report(path: Path, summaries: list[Summary], seed_count: int) -> None:
lines = [
"# Doppler WLS -> ESKF model sonuçları",
"",
f"Monte Carlo seed sayısı: `{seed_count}`; ilk 5 saniye RMS metriklerinden çıkarıldı.",
"50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.",
"",
"## Sonuç tablosu",
"",
"| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |",
"|---|---|---:|---:|---:|---:|---:|---:|",
]
for scenario in flight_scenarios():
for doppler_path in PATH_ORDER:
item = _lookup(summaries, scenario.name, doppler_path)
dynamic = "-" if not np.isfinite(item.dynamic_rms_mps) else f"{item.dynamic_rms_mps:.3f}"
lines.append(
f"| {scenario.name} | {PATH_LABEL[doppler_path]} | "
f"{item.fused_rms_mps:.3f} | {dynamic} | {item.wls_rms_mps:.3f} | "
f"{item.velocity_accept_percent:.1f} | {item.fused_95_coverage_percent:.1f} | "
f"{item.excluded_epoch_percent:.1f} |"
)
steady_fixed = _lookup(summaries, "steady_cruise", DopplerPath.FIXED_IIR)
steady_raw = _lookup(summaries, "steady_cruise", DopplerPath.RAW)
air_fixed = _lookup(summaries, "airliner_maneuver", DopplerPath.FIXED_IIR)
air_raw = _lookup(summaries, "airliner_maneuver", DopplerPath.RAW)
weak_fixed = _lookup(summaries, "low_cn0_airliner", DopplerPath.FIXED_IIR)
weak_raw = _lookup(summaries, "low_cn0_airliner", DopplerPath.RAW)
weak_adaptive = _lookup(summaries, "low_cn0_airliner", DopplerPath.ADAPTIVE_IIR)
outlier_raw = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW)
outlier_robust = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW_ROBUST)
clock_raw = _lookup(summaries, "clock_drift_steps", DopplerPath.RAW)
lines.extend(
[
"",
"## Bulgular",
"",
f"- Sabit hızda pre-IIR ESKF RMS'i `{steady_fixed.fused_rms_mps:.3f}` m/s, ham yol `{steady_raw.fused_rms_mps:.3f}` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `{steady_raw.wls_rms_mps:.3f}` m/s WLS RMS'ten bu seviyeye indiriyor.",
f"- Airliner manevrasında sabit IIR `{air_fixed.fused_rms_mps:.3f}` m/s ve `%{air_fixed.nis_reject_percent:.1f}` NIS ret oranı verirken ham yol `{air_raw.fused_rms_mps:.3f}` m/s ve `%{air_raw.nis_reject_percent:.1f}` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.",
f"- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `{weak_fixed.fused_rms_mps:.3f}`, `{weak_raw.fused_rms_mps:.3f}`, `{weak_adaptive.fused_rms_mps:.3f}` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.",
f"- Tek uydu burst'ünde raw WLS RMS `{outlier_raw.wls_rms_mps:.3f}` m/s, robust leave-one-out ile `{outlier_robust.wls_rms_mps:.3f}` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.",
f"- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `{clock_raw.clock_drift_rms_mps:.3f}` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.",
"",
"## Mimari kararı",
"",
"- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.",
"- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.",
"- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.",
"- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.",
"",
"## Model sınırları",
"",
"- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.",
"- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.",
"- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.",
"- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.",
"- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.",
"- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seeds", type=int, default=20)
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent / "output",
)
arguments = parser.parse_args()
arguments.output.mkdir(parents=True, exist_ok=True)
scenarios = flight_scenarios()
tasks = [
(scenario_index, seed)
for scenario_index in range(len(scenarios))
for seed in range(1, arguments.seeds + 1)
]
grouped: dict[tuple[int, DopplerPath], list[TrialMetrics]] = {
(scenario_index, doppler_path): []
for scenario_index in range(len(scenarios))
for doppler_path in PATH_ORDER
}
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
for scenario_index, seed, metrics in executor.map(_worker, tasks):
for item in metrics:
grouped[(scenario_index, item.path)].append(item)
print(f"completed {scenarios[scenario_index].name} seed={seed}", flush=True)
summaries = [
summarize(grouped[(scenario_index, doppler_path)])
for scenario_index in range(len(scenarios))
for doppler_path in PATH_ORDER
]
write_csv(arguments.output / "doppler_eskf_summary.csv", summaries)
write_report(
arguments.output / "DOPPLER_ESKF_ANALYSIS.md",
summaries,
arguments.seeds,
)
_summary_plot(arguments.output / "05_doppler_eskf_velocity_rms.png", summaries)
airliner = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "airliner_maneuver"), 1
)
_maneuver_trace_plot(arguments.output / "06_airliner_maneuver_trace.png", airliner)
outlier = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "single_satellite_outliers"), 1
)
_outlier_plot(arguments.output / "07_outlier_wls_nis.png", outlier)
weak = run_doppler_eskf_scenario(
next(item for item in scenarios if item.name == "low_cn0_airliner"), 1
)
_filtering_plot(arguments.output / "08_low_cn0_wls_vs_eskf.png", weak)
print(f"wrote Doppler/ESKF analysis to {arguments.output}", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,425 @@
"""Run the complete DLL-quality/WLS and code-smoothing comparison."""
from __future__ import annotations
import argparse
import csv
import math
from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from .code_smoothing_model import run_smoothing_trial
from .firmware_wls_model import SigmaMethod
from .measurement_scenarios import EpochSample, run_scenario, scenario_matrix
METHOD_ORDER = (
SigmaMethod.CURRENT,
SigmaMethod.NAIVE_ADD_DLL,
SigmaMethod.DLL_EXCESS,
SigmaMethod.DLL_EXCESS_GUARD,
SigmaMethod.ORACLE,
)
METHOD_LABEL = {
SigmaMethod.CURRENT: "Mevcut C/N0",
SigmaMethod.NAIVE_ADD_DLL: "Naif DLL ekle",
SigmaMethod.DLL_EXCESS: "DLL excess",
SigmaMethod.DLL_EXCESS_GUARD: "DLL excess+mean",
SigmaMethod.ORACLE: "Oracle variance",
}
@dataclass(frozen=True)
class Summary:
scenario: str
method: SigmaMethod
epochs: int
horizontal_rms_m: float
position_3d_rms_m: float
position_3d_p95_m: float
valid_percent: float
valid_3d_rms_m: float
horizontal_2sigma_coverage_percent: float
false_valid_over_25m_percent: float
fde_percent: float
mean_measurement_sigma_m: float
def _worker(task: tuple[int, int]) -> tuple[int, int, list[EpochSample]]:
scenario_index, seed = task
scenario = scenario_matrix()[scenario_index]
return scenario_index, seed, run_scenario(scenario, seed, METHOD_ORDER)
def _rms(values: np.ndarray) -> float:
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
def summarize(scenario_name: str, method: SigmaMethod, samples: list[EpochSample]) -> Summary:
selected = [
item
for item in samples
if item.method == method and item.time_s >= 5.0
]
horizontal = np.asarray([item.horizontal_error_m for item in selected])
error_3d = np.asarray([item.position_error_3d_m for item in selected])
valid = np.asarray([item.valid for item in selected], dtype=bool)
horizontal_sigma = np.asarray([item.horizontal_sigma_m for item in selected])
fde = np.asarray([item.fde_applied for item in selected], dtype=bool)
mean_sigma = np.asarray([item.mean_measurement_sigma_m for item in selected])
false_valid = valid & (error_3d > 25.0)
return Summary(
scenario=scenario_name,
method=method,
epochs=len(selected),
horizontal_rms_m=_rms(horizontal),
position_3d_rms_m=_rms(error_3d),
position_3d_p95_m=float(np.percentile(error_3d, 95.0)),
valid_percent=100.0 * float(np.mean(valid)),
valid_3d_rms_m=_rms(error_3d[valid]),
horizontal_2sigma_coverage_percent=100.0
* float(np.mean(horizontal <= 2.0 * horizontal_sigma)),
false_valid_over_25m_percent=100.0 * float(np.mean(false_valid)),
fde_percent=100.0 * float(np.mean(fde)),
mean_measurement_sigma_m=float(np.mean(mean_sigma)),
)
def write_csv(path: Path, summaries: list[Summary]) -> None:
with path.open("w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
writer.writeheader()
for item in summaries:
row = dict(item.__dict__)
row["method"] = item.method.value
writer.writerow(row)
def _comparison_plot(
path: Path,
summaries: list[Summary],
field: str,
ylabel: str,
*,
logarithmic: bool = False,
) -> None:
scenarios = scenario_matrix()
x = np.arange(len(scenarios))
width = 0.16
figure, axis = plt.subplots(figsize=(15, 6.5))
for method_index, method in enumerate(METHOD_ORDER):
values = [
getattr(
next(
item
for item in summaries
if item.scenario == scenario.name and item.method == method
),
field,
)
for scenario in scenarios
]
axis.bar(
x + (method_index - 2) * width,
values,
width,
label=METHOD_LABEL[method],
)
axis.set_ylabel(ylabel)
if logarithmic:
axis.set_yscale("log")
axis.set_xticks(x)
axis.set_xticklabels([item.name for item in scenarios], rotation=28, ha="right")
axis.grid(axis="y", alpha=0.25)
axis.legend(ncol=3)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def _transition_plot(path: Path, samples: list[EpochSample]) -> None:
figure, axes = plt.subplots(2, 1, figsize=(12, 7), sharex=True)
for method in (
SigmaMethod.CURRENT,
SigmaMethod.DLL_EXCESS,
SigmaMethod.DLL_EXCESS_GUARD,
):
selected = [item for item in samples if item.method == method]
axes[0].plot(
[item.time_s for item in selected],
[item.position_error_3d_m for item in selected],
label=METHOD_LABEL[method],
linewidth=1.2,
)
reference = [item for item in samples if item.method == SigmaMethod.CURRENT]
axes[1].plot(
[item.time_s for item in reference],
[item.maximum_dll_ratio for item in reference],
color="tab:red",
)
axes[0].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
axes[1].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
axes[0].set_ylabel("3D konum hatası (m)")
axes[1].set_ylabel("Maks. DLL variance ratio")
axes[1].set_xlabel("Zaman (s)")
axes[0].legend()
for axis in axes:
axis.grid(alpha=0.25)
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def smoothing_summary(seed_count: int) -> dict[str, dict[str, float]]:
result: dict[str, dict[str, float]] = {}
for label, cfo_mps in (
("cfo_compensated", 0.0),
("cfo_residual_0p5_mps", 0.5),
("cfo_residual_3_mps", 3.0),
):
trials = [
run_smoothing_trial(seed, doppler_cfo_mps=cfo_mps)
for seed in range(1, seed_count + 1)
]
result[label] = {
field: float(np.mean([getattr(item, field) for item in trials]))
for field in trials[0].__dataclass_fields__
}
return result
def _smoothing_plot(path: Path, smoothing: dict[str, dict[str, float]]) -> None:
labels = list(smoothing)
x = np.arange(len(labels))
width = 0.20
figure, axis = plt.subplots(figsize=(10, 5.5))
for offset, (field, title) in enumerate(
(
("raw_rmse_m", "Ham code"),
("scalar_iir_rmse_m", "Kör IIR"),
("doppler_smoothed_rmse_m", "Raw Doppler-predicted"),
(
"firmware_iir_doppler_smoothed_rmse_m",
"FW-IIR Doppler-predicted",
),
)
):
axis.bar(
x + (offset - 1.5) * width,
[smoothing[label][field] for label in labels],
width,
label=title,
)
axis.set_xticks(x)
axis.set_xticklabels(labels)
axis.set_ylabel("Range RMSE (m)")
axis.set_yscale("log")
axis.grid(axis="y", alpha=0.25)
axis.legend()
figure.tight_layout()
figure.savefig(path, dpi=150)
plt.close(figure)
def write_report(
path: Path,
summaries: list[Summary],
focused_rms: dict[str, dict[SigmaMethod, float]],
smoothing: dict[str, dict[str, float]],
seed_count: int,
) -> None:
current = {
item.scenario: item
for item in summaries
if item.method == SigmaMethod.CURRENT
}
lines = [
"# GPS code measurement model sonuçları",
"",
f"Monte Carlo seed sayısı: `{seed_count}`. İlk 5 saniye metriklere alınmadı.",
"",
"## Yöntem karşılaştırması",
"",
"| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |",
"|---|---|---:|---:|---:|---:|---:|",
]
for scenario in scenario_matrix():
baseline = current[scenario.name].position_3d_rms_m
for method in METHOD_ORDER:
item = next(
value
for value in summaries
if value.scenario == scenario.name and value.method == method
)
improvement = 100.0 * (baseline - item.position_3d_rms_m) / baseline
lines.append(
f"| {scenario.name} | {METHOD_LABEL[method]} | "
f"{item.position_3d_rms_m:.2f} | {improvement:+.1f}% | "
f"{item.position_3d_p95_m:.2f} | {item.valid_percent:.1f} | "
f"{item.false_valid_over_25m_percent:.2f} |"
)
lines.extend(
[
"",
"## Modelden çıkan karar özeti",
"",
"- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.",
"- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.",
"- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.",
"- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.",
"- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.",
"- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.",
"",
"## Bozulma penceresi odaklı 3D RMS",
"",
"| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |",
"|---|---:|---:|---:|---:|",
]
)
for label, values in focused_rms.items():
lines.append(
f"| {label} | {values[SigmaMethod.CURRENT]:.2f} | "
f"{values[SigmaMethod.DLL_EXCESS]:.2f} | "
f"{values[SigmaMethod.DLL_EXCESS_GUARD]:.2f} | "
f"{values[SigmaMethod.ORACLE]:.2f} |"
)
lines.extend(
[
"",
"## Code smoothing ve carrier CFO",
"",
"| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |",
"|---|---:|---:|---:|---:|---:|---:|---:|",
]
)
for label, values in smoothing.items():
lines.append(
f"| {label} | {values['raw_rmse_m']:.2f} | "
f"{values['scalar_iir_rmse_m']:.2f} | "
f"{values['doppler_smoothed_rmse_m']:.2f} | "
f"{values['firmware_iir_doppler_smoothed_rmse_m']:.2f} | "
f"{values['scalar_iir_peak_dynamic_error_m']:.2f} | "
f"{values['doppler_peak_dynamic_error_m']:.2f} | "
f"{values['firmware_iir_doppler_peak_dynamic_error_m']:.2f} |"
)
lines.extend(
[
"",
"## Yorum sınırları",
"",
"- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.",
"- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.",
"- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.",
"- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.",
"- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.",
"- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.",
"- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.",
"- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.",
"",
]
)
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seeds", type=int, default=12)
parser.add_argument("--jobs", type=int, default=4)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent / "output",
)
arguments = parser.parse_args()
arguments.output.mkdir(parents=True, exist_ok=True)
scenarios = scenario_matrix()
tasks = [
(scenario_index, seed)
for scenario_index in range(len(scenarios))
for seed in range(1, arguments.seeds + 1)
]
grouped: dict[int, list[EpochSample]] = {
index: [] for index in range(len(scenarios))
}
transition_samples: list[EpochSample] = []
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
for scenario_index, seed, samples in executor.map(_worker, tasks):
grouped[scenario_index].extend(samples)
if scenarios[scenario_index].name == "noise_burst_recovery" and seed == 1:
transition_samples = samples
print(
f"completed {scenarios[scenario_index].name} seed={seed}",
flush=True,
)
summaries = [
summarize(scenario.name, method, grouped[index])
for index, scenario in enumerate(scenarios)
for method in METHOD_ORDER
]
focused_windows = {
"noise_burst_recovery [22,45) s": ("noise_burst_recovery", 22.0, 45.0),
"dll_transient_stress [25,34) s": ("dll_transient_stress", 25.0, 34.0),
"cn0_step_recovery [5,30) s": ("cn0_step_recovery", 5.0, 30.0),
}
focused_rms: dict[str, dict[SigmaMethod, float]] = {}
for label, (scenario_name, start_s, stop_s) in focused_windows.items():
scenario_index = next(
index
for index, scenario in enumerate(scenarios)
if scenario.name == scenario_name
)
focused_rms[label] = {}
for method in METHOD_ORDER:
values = np.asarray(
[
item.position_error_3d_m
for item in grouped[scenario_index]
if item.method == method
and start_s <= item.time_s < stop_s
]
)
focused_rms[label][method] = _rms(values)
smoothing = smoothing_summary(max(arguments.seeds, 20))
write_csv(arguments.output / "scenario_summary.csv", summaries)
write_report(
arguments.output / "ANALYSIS.md",
summaries,
focused_rms,
smoothing,
arguments.seeds,
)
_comparison_plot(
arguments.output / "01_position_3d_rms.png",
summaries,
"position_3d_rms_m",
"3D RMS (m)",
logarithmic=True,
)
_comparison_plot(
arguments.output / "02_solution_availability.png",
summaries,
"valid_percent",
"Integrity-valid çözüm (%)",
)
_transition_plot(
arguments.output / "03_noise_burst_transition.png",
transition_samples,
)
_smoothing_plot(arguments.output / "04_code_smoothing_cfo.png", smoothing)
print(f"wrote analysis to {arguments.output}", flush=True)
if __name__ == "__main__":
main()
@@ -0,0 +1,132 @@
"""One-dimensional code smoothing study with carrier/Doppler aiding."""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
@dataclass(frozen=True)
class SmoothingResult:
raw_rmse_m: float
scalar_iir_rmse_m: float
doppler_smoothed_rmse_m: float
firmware_iir_doppler_smoothed_rmse_m: float
scalar_iir_peak_dynamic_error_m: float
doppler_peak_dynamic_error_m: float
firmware_iir_doppler_peak_dynamic_error_m: float
def run_smoothing_trial(
seed: int,
*,
duration_s: float = 80.0,
tracking_dt_s: float = 0.020,
wls_dt_s: float = 0.200,
code_sigma_m: float = 4.0,
doppler_sigma_mps: float = 0.25,
doppler_cfo_mps: float = 0.0,
alpha: float = 0.10,
firmware_doppler_alpha: float = 0.05,
) -> SmoothingResult:
"""Compare raw code, blind IIR and Doppler-propagated code smoothing.
The truth includes a range-rate step so a visually smooth but lagging
scalar IIR is penalized. ``doppler_cfo_mps`` represents residual receiver
carrier frequency offset after the nominal IF subtraction.
"""
if abs(round(wls_dt_s / tracking_dt_s) - wls_dt_s / tracking_dt_s) > 1.0e-12:
raise ValueError("WLS period must be an integer number of tracking periods")
rng = np.random.default_rng(seed)
tracking_time_s = np.arange(0.0, duration_s, tracking_dt_s)
true_rate_tracking = np.where(
tracking_time_s < 20.0,
-120.0,
np.where(tracking_time_s < 45.0, 230.0, -40.0),
)
true_range_tracking = np.empty_like(tracking_time_s)
true_range_tracking[0] = 21_000_000.0
true_range_tracking[1:] = (
true_range_tracking[0]
+ np.cumsum(true_rate_tracking[:-1] * tracking_dt_s)
)
measured_rate_tracking = (
true_rate_tracking
+ doppler_cfo_mps
+ rng.normal(0.0, doppler_sigma_mps, len(tracking_time_s))
)
filtered_rate_tracking = np.empty_like(measured_rate_tracking)
filtered_rate_tracking[0] = measured_rate_tracking[0]
for index in range(1, len(filtered_rate_tracking)):
filtered_rate_tracking[index] = (
firmware_doppler_alpha * measured_rate_tracking[index]
+ (1.0 - firmware_doppler_alpha) * filtered_rate_tracking[index - 1]
)
tracking_per_wls = int(round(wls_dt_s / tracking_dt_s))
wls_indices = np.arange(0, len(tracking_time_s), tracking_per_wls)
time_s = tracking_time_s[wls_indices]
true_range = true_range_tracking[wls_indices]
raw_code = true_range + rng.normal(0.0, code_sigma_m, len(time_s))
scalar = np.empty_like(raw_code)
doppler_smoothed = np.empty_like(raw_code)
firmware_iir_doppler_smoothed = np.empty_like(raw_code)
scalar[0] = raw_code[0]
doppler_smoothed[0] = raw_code[0]
firmware_iir_doppler_smoothed[0] = raw_code[0]
for index in range(1, len(time_s)):
scalar[index] = alpha * raw_code[index] + (1.0 - alpha) * scalar[index - 1]
start = wls_indices[index - 1]
stop = wls_indices[index]
raw_range_delta = (
float(np.sum(measured_rate_tracking[start:stop]))
* tracking_dt_s
)
filtered_range_delta = (
float(np.sum(filtered_rate_tracking[start:stop]))
* tracking_dt_s
)
prediction = doppler_smoothed[index - 1] + raw_range_delta
doppler_smoothed[index] = (
prediction + alpha * (raw_code[index] - prediction)
)
filtered_prediction = (
firmware_iir_doppler_smoothed[index - 1]
+ filtered_range_delta
)
firmware_iir_doppler_smoothed[index] = (
filtered_prediction
+ alpha * (raw_code[index] - filtered_prediction)
)
warm = time_s >= 5.0
dynamic = ((time_s >= 20.0) & (time_s < 25.0)) | (
(time_s >= 45.0) & (time_s < 50.0)
)
rmse = lambda error, mask: float(np.sqrt(np.mean(np.square(error[mask]))))
return SmoothingResult(
raw_rmse_m=rmse(raw_code - true_range, warm),
scalar_iir_rmse_m=rmse(scalar - true_range, warm),
doppler_smoothed_rmse_m=rmse(doppler_smoothed - true_range, warm),
firmware_iir_doppler_smoothed_rmse_m=rmse(
firmware_iir_doppler_smoothed - true_range,
warm,
),
scalar_iir_peak_dynamic_error_m=float(
np.max(np.abs(scalar[dynamic] - true_range[dynamic]))
),
doppler_peak_dynamic_error_m=float(
np.max(np.abs(doppler_smoothed[dynamic] - true_range[dynamic]))
),
firmware_iir_doppler_peak_dynamic_error_m=float(
np.max(
np.abs(
firmware_iir_doppler_smoothed[dynamic]
- true_range[dynamic]
)
)
),
)
@@ -0,0 +1,657 @@
"""Project-specific Doppler WLS to ESKF timing/noise model.
The model compares four paths using one shared 50 Hz tracking stream and one
shared 100 Hz IMU stream:
* the legacy fixed-alpha Doppler IIR before 5 Hz velocity WLS;
* raw Doppler into WLS, as a nominal-model comparison rather than production;
* a global IMU-driven adaptive-alpha IIR;
* raw Doppler with the per-satellite robust WLS gate used before the ESKF.
The ESKF is reduced to the velocity and accelerometer-bias error states that
matter to this experiment. Its process noise, 3-D measurement covariance and
99.9-percent velocity NIS gate mirror the project firmware. Attitude and
position are intentionally outside this model's scope.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable
import numpy as np
IMU_HZ = 100.0
TRACKING_HZ = 50.0
VELOCITY_WLS_HZ = 5.0
IMU_DT_S = 1.0 / IMU_HZ
TRACKING_STEPS = int(round(IMU_HZ / TRACKING_HZ))
WLS_STEPS = int(round(IMU_HZ / VELOCITY_WLS_HZ))
FIXED_DOPPLER_ALPHA = 0.05
CN0_ALPHA = 0.20
VELOCITY_NIS_GATE = 16.26623619623813
PROCESS_ACCEL_NOISE_DENSITY = 0.025
ACCEL_BIAS_RANDOM_WALK = 0.0015
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS = 2.0
MINIMUM_VERTICAL_VELOCITY_STD_MPS = 3.0
class DopplerPath(str, Enum):
FIXED_IIR = "fixed_iir_wls_eskf"
RAW = "raw_wls_eskf"
ADAPTIVE_IIR = "adaptive_iir_wls_eskf"
RAW_ROBUST = "raw_robust_wls_eskf"
PATH_ORDER = (
DopplerPath.FIXED_IIR,
DopplerPath.RAW,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW_ROBUST,
)
@dataclass(frozen=True)
class FlightScenario:
name: str
description: str
duration_s: float
initial_velocity_mps: np.ndarray
acceleration: Callable[[float], np.ndarray]
cn0_offset: Callable[[float], float]
noise_multiplier: Callable[[float, int], float]
outlier_mps: Callable[[float, int], float]
receiver_clock_drift_mps: Callable[[float], float]
gps_gaps: tuple[tuple[float, float], ...] = ()
@dataclass(frozen=True)
class VelocityWlsResult:
velocity_mps: np.ndarray
clock_drift_mps: float
covariance_mps2: np.ndarray
residual_mps: np.ndarray
residual_rms_mps: float
covariance_scale: float
excluded_satellite: int | None
@dataclass
class AdaptiveAlpha:
alpha: float = FIXED_DOPPLER_ALPHA
hold_samples: int = 0
filtered_acceleration_mps2: np.ndarray = field(
default_factory=lambda: np.zeros(3, dtype=float)
)
def update(
self,
measured_acceleration_mps2: np.ndarray,
median_cn0_dbhz: float,
) -> float:
# A raw 100 Hz accelerometer sample is too noisy for coefficient
# switching. This 50 Hz vector EWMA has an approximately 0.16 s time
# constant and, unlike smoothing |a|, does not retain the positive
# magnitude bias of zero-mean sensor noise.
motion_alpha = 0.12
self.filtered_acceleration_mps2 += motion_alpha * (
measured_acceleration_mps2 - self.filtered_acceleration_mps2
)
magnitude = float(np.linalg.norm(self.filtered_acceleration_mps2))
if median_cn0_dbhz >= 46.0:
quiet_alpha = 0.08
elif median_cn0_dbhz >= 37.0:
quiet_alpha = FIXED_DOPPLER_ALPHA
else:
quiet_alpha = 0.03
if magnitude >= 2.5:
self.alpha = 0.40
self.hold_samples = 25
elif magnitude >= 0.55:
self.alpha = 0.20
self.hold_samples = 25
elif self.hold_samples > 0:
self.hold_samples -= 1
else:
self.alpha = quiet_alpha
return self.alpha
@dataclass
class VelocityBiasEskf:
velocity_mps: np.ndarray
accel_bias_mps2: np.ndarray
covariance: np.ndarray
accepted: int = 0
rejected_nis: int = 0
last_nis: float = float("nan")
@classmethod
def initialize(
cls,
initial_velocity_mps: np.ndarray,
initial_velocity_error_mps: np.ndarray,
) -> "VelocityBiasEskf":
covariance = np.zeros((6, 6), dtype=float)
covariance[:3, :3] = np.eye(3)
covariance[3:, 3:] = np.eye(3) * 0.1**2
return cls(
velocity_mps=initial_velocity_mps + initial_velocity_error_mps,
accel_bias_mps2=np.zeros(3),
covariance=covariance,
)
def propagate(self, measured_acceleration_mps2: np.ndarray, dt_s: float) -> None:
self.velocity_mps += (
measured_acceleration_mps2 - self.accel_bias_mps2
) * dt_s
transition = np.eye(6)
transition[:3, 3:] = -np.eye(3) * dt_s
accel_variance = PROCESS_ACCEL_NOISE_DENSITY**2
bias_variance = ACCEL_BIAS_RANDOM_WALK**2
process = np.zeros((6, 6), dtype=float)
# Match eskf.c's first-order discrete injection: velocity and bias
# driving noise are added after F P F^T, without exact-discretization
# dt^2/dt^3 cross terms.
process[:3, :3] = np.eye(3) * accel_variance * dt_s
process[3:, 3:] = np.eye(3) * bias_variance * dt_s
self.covariance = (
transition @ self.covariance @ transition.T + process
)
self.covariance = 0.5 * (self.covariance + self.covariance.T)
def update_velocity(
self,
measurement_mps: np.ndarray,
covariance_mps2: np.ndarray,
) -> bool:
innovation = measurement_mps - self.velocity_mps
innovation_covariance = self.covariance[:3, :3] + covariance_mps2
inverse = np.linalg.inv(innovation_covariance)
nis = float(innovation @ inverse @ innovation)
self.last_nis = nis
if not np.isfinite(nis) or nis > VELOCITY_NIS_GATE:
self.rejected_nis += 1
return False
measurement_matrix = np.zeros((3, 6), dtype=float)
measurement_matrix[:, :3] = np.eye(3)
gain = self.covariance @ measurement_matrix.T @ inverse
correction = gain @ innovation
self.velocity_mps += correction[:3]
self.accel_bias_mps2 += correction[3:]
identity = np.eye(6)
residual_transform = identity - gain @ measurement_matrix
self.covariance = (
residual_transform
@ self.covariance
@ residual_transform.T
+ gain @ covariance_mps2 @ gain.T
)
self.covariance = 0.5 * (self.covariance + self.covariance.T)
self.accepted += 1
return True
@dataclass(frozen=True)
class DopplerEskfTrace:
scenario: str
path: DopplerPath
time_s: np.ndarray
truth_velocity_mps: np.ndarray
fused_velocity_mps: np.ndarray
fused_velocity_covariance_mps2: np.ndarray
acceleration_mps2: np.ndarray
wls_time_s: np.ndarray
wls_velocity_mps: np.ndarray
wls_clock_drift_mps: np.ndarray
wls_covariance_mps2: np.ndarray
wls_covariance_scale: np.ndarray
wls_nis: np.ndarray
wls_accepted: np.ndarray
adaptive_alpha: np.ndarray
excluded_satellites: np.ndarray
def _zero_acceleration(_time_s: float) -> np.ndarray:
return np.zeros(3)
def _airliner_acceleration(time_s: float) -> np.ndarray:
if 15.0 <= time_s < 25.0:
return np.asarray([2.0, 0.0, 0.0])
if 38.0 <= time_s < 52.0:
return np.asarray([0.0, 1.5, 0.25])
if 68.0 <= time_s < 76.0:
return np.asarray([-2.5, 0.0, -0.15])
return np.zeros(3)
def _aggressive_acceleration(time_s: float) -> np.ndarray:
if 10.0 <= time_s < 16.0:
return np.asarray([6.0, 0.0, 1.0])
if 26.0 <= time_s < 33.0:
return np.asarray([0.0, 7.0, 0.0])
if 46.0 <= time_s < 52.0:
return np.asarray([-5.0, -4.0, -1.0])
if 64.0 <= time_s < 76.0:
phase = 2.0 * np.pi * (time_s - 64.0) / 3.0
return np.asarray([3.0 * np.sin(phase), 4.0 * np.cos(phase), 0.5 * np.sin(phase)])
return np.zeros(3)
def _constant_zero(_time_s: float) -> float:
return 0.0
def _constant_one(_time_s: float, _satellite: int) -> float:
return 1.0
def _no_outlier(_time_s: float, _satellite: int) -> float:
return 0.0
def flight_scenarios() -> tuple[FlightScenario, ...]:
return (
FlightScenario(
"steady_cruise",
"Constant aircraft velocity with nominal open-sky Doppler noise.",
90.0,
np.asarray([180.0, 20.0, 0.0]),
_zero_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"airliner_maneuver",
"Moderate longitudinal, turn and climb-rate accelerations.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"aggressive_maneuver",
"High acceleration and oscillatory manoeuvre segments.",
90.0,
np.asarray([80.0, 0.0, 0.0]),
_aggressive_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"low_cn0_airliner",
"Airliner manoeuvre with a 10 dB signal-quality loss.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
lambda _time_s: -10.0,
_constant_one,
_no_outlier,
_constant_zero,
),
FlightScenario(
"single_satellite_outliers",
"One channel has repeatable 12 m/s Doppler bursts.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
lambda time_s, satellite: (
12.0
if satellite == 2
and (
30.0 <= time_s < 31.0
or 56.0 <= time_s < 57.0
or 72.0 <= time_s < 72.6
)
else 0.0
),
_constant_zero,
),
FlightScenario(
"clock_drift_steps",
"Common receiver clock-frequency steps coexist with manoeuvres.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
lambda time_s: (
18.0 if 28.0 <= time_s < 55.0 else (-9.0 if time_s >= 55.0 else 0.0)
),
),
FlightScenario(
"gps_gap_recovery",
"Fifteen-second GNSS gap during an aircraft manoeuvre.",
90.0,
np.asarray([120.0, 0.0, 0.0]),
_airliner_acceleration,
_constant_zero,
_constant_one,
_no_outlier,
_constant_zero,
gps_gaps=((42.0, 57.0),),
),
FlightScenario(
"mixed_flight_faults",
"Aggressive motion, temporary weak signal, channel bursts and clock step.",
90.0,
np.asarray([80.0, 0.0, 0.0]),
_aggressive_acceleration,
lambda time_s: -9.0 if 34.0 <= time_s < 60.0 else 0.0,
lambda time_s, satellite: (
2.5 if 34.0 <= time_s < 60.0 and satellite in (1, 6) else 1.0
),
lambda time_s, satellite: (
-10.0 if satellite == 4 and 54.0 <= time_s < 55.0 else 0.0
),
lambda time_s: 12.0 if time_s >= 48.0 else 0.0,
gps_gaps=((78.0, 83.0),),
),
)
def local_los_matrix() -> np.ndarray:
azimuth_deg = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
elevation_deg = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
azimuth = np.deg2rad(azimuth_deg)
elevation = np.deg2rad(elevation_deg)
return np.column_stack(
[
np.cos(elevation) * np.sin(azimuth),
np.cos(elevation) * np.cos(azimuth),
np.sin(elevation),
]
)
def velocity_sigma_mps(cn0_dbhz: np.ndarray) -> np.ndarray:
return np.clip(30.0 * np.power(10.0, -cn0_dbhz / 20.0), 0.05, 5.0)
def _solve_velocity_candidate(
design: np.ndarray,
observations_mps: np.ndarray,
sigma_mps: np.ndarray,
indices: np.ndarray,
) -> VelocityWlsResult:
selected_design = design[indices]
selected_observations = observations_mps[indices]
selected_sigma = sigma_mps[indices]
weights = 1.0 / np.square(selected_sigma)
normal = selected_design.T @ (weights[:, None] * selected_design)
inverse = np.linalg.inv(normal)
solution = inverse @ (selected_design.T @ (weights * selected_observations))
residual = selected_observations - selected_design @ solution
weighted_residual_square_sum = float(np.sum(weights * np.square(residual)))
degrees_of_freedom = len(indices) - 4
covariance_scale = (
max(1.0, weighted_residual_square_sum / degrees_of_freedom)
if degrees_of_freedom > 0
else 1.0
)
return VelocityWlsResult(
velocity_mps=solution[:3].copy(),
clock_drift_mps=float(solution[3]),
covariance_mps2=covariance_scale * inverse[:3, :3],
residual_mps=residual,
residual_rms_mps=float(np.sqrt(np.mean(np.square(residual)))),
covariance_scale=covariance_scale,
excluded_satellite=None,
)
def velocity_wls(
observations_mps: np.ndarray,
cn0_dbhz: np.ndarray,
*,
robust: bool = False,
) -> VelocityWlsResult:
los = local_los_matrix()
design = np.column_stack([-los, np.ones(len(los))])
sigma = velocity_sigma_mps(cn0_dbhz)
indices = np.arange(len(observations_mps), dtype=int)
full = _solve_velocity_candidate(design, observations_mps, sigma, indices)
if not robust or len(indices) < 6:
return full
normalized = np.abs(full.residual_mps) / sigma
if float(np.max(normalized)) <= 3.5:
return full
# The largest full-fit residual is not necessarily the bad satellite:
# WLS leverage can distribute one channel's error over the other rows.
# Compare every leave-one-out hypothesis and use the one with the lowest
# normalized post-fit residual energy. The firmware robust velocity WLS
# uses the same trigger and candidate selection.
candidates: list[tuple[float, int, VelocityWlsResult]] = []
for excluded in indices:
used = indices[indices != excluded]
candidate = _solve_velocity_candidate(design, observations_mps, sigma, used)
score = float(
np.sum(np.square(candidate.residual_mps / sigma[used]))
)
candidates.append((score, int(excluded), candidate))
_score, excluded, candidate = min(candidates, key=lambda item: item[0])
return VelocityWlsResult(
**{
**candidate.__dict__,
"excluded_satellite": excluded,
}
)
def fusion_velocity_covariance(covariance_mps2: np.ndarray) -> np.ndarray:
"""Apply fusion_gps_adapter.c's ENU velocity covariance sanitizer floors."""
output = np.asarray(covariance_mps2, dtype=float).copy()
variance_floor = np.square(
np.asarray(
[
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
MINIMUM_VERTICAL_VELOCITY_STD_MPS,
]
)
)
for axis in range(3):
output[axis, axis] = max(output[axis, axis], variance_floor[axis])
for row in range(3):
for column in range(row + 1, 3):
covariance = 0.5 * (output[row, column] + output[column, row])
limit = 0.95 * np.sqrt(output[row, row] * output[column, column])
covariance = float(np.clip(covariance, -limit, limit))
output[row, column] = covariance
output[column, row] = covariance
return output
def _in_gap(scenario: FlightScenario, time_s: float) -> bool:
return any(start <= time_s < stop for start, stop in scenario.gps_gaps)
def _truth_trajectory(scenario: FlightScenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
time_s = np.arange(0.0, scenario.duration_s, IMU_DT_S)
acceleration = np.asarray([scenario.acceleration(float(time)) for time in time_s])
velocity = np.empty((len(time_s), 3), dtype=float)
velocity[0] = scenario.initial_velocity_mps
velocity[1:] = velocity[0] + np.cumsum(acceleration[:-1] * IMU_DT_S, axis=0)
return time_s, acceleration, velocity
def run_doppler_eskf_scenario(
scenario: FlightScenario,
seed: int,
paths: tuple[DopplerPath, ...] = PATH_ORDER,
) -> dict[DopplerPath, DopplerEskfTrace]:
rng = np.random.default_rng(seed)
time_s, truth_acceleration, truth_velocity = _truth_trajectory(scenario)
satellite_count = len(local_los_matrix())
baseline_cn0 = np.asarray([47.0, 45.0, 43.0, 38.0, 46.0, 42.0, 40.0, 36.0])
filtered_cn0 = baseline_cn0.copy()
fixed_measurement: np.ndarray | None = None
adaptive_measurement: np.ndarray | None = None
latest_raw: np.ndarray | None = None
alpha_selector = AdaptiveAlpha()
latest_alpha = FIXED_DOPPLER_ALPHA
initial_error = rng.normal(0.0, 0.8, 3)
filters = {
path: VelocityBiasEskf.initialize(
scenario.initial_velocity_mps,
initial_error,
)
for path in paths
}
fused_history = {
path: np.empty((len(time_s), 3), dtype=float)
for path in paths
}
fused_covariance_history = {
path: np.empty((len(time_s), 3, 3), dtype=float)
for path in paths
}
wls_times = {path: [] for path in paths}
wls_velocity = {path: [] for path in paths}
wls_clock_drift = {path: [] for path in paths}
wls_covariance = {path: [] for path in paths}
wls_covariance_scale = {path: [] for path in paths}
wls_nis = {path: [] for path in paths}
wls_accepted = {path: [] for path in paths}
wls_alpha = {path: [] for path in paths}
excluded = {path: [] for path in paths}
true_accel_bias = rng.normal(0.0, 0.04, 3)
latest_acceleration_measurement = truth_acceleration[0] + true_accel_bias
los = local_los_matrix()
for sample in range(len(time_s)):
if sample > 0:
true_accel_bias += rng.normal(
0.0,
ACCEL_BIAS_RANDOM_WALK * np.sqrt(IMU_DT_S),
3,
)
imu_noise_std = PROCESS_ACCEL_NOISE_DENSITY / np.sqrt(IMU_DT_S)
latest_acceleration_measurement = (
truth_acceleration[sample - 1]
+ true_accel_bias
+ rng.normal(0.0, imu_noise_std, 3)
)
for path in paths:
filters[path].propagate(latest_acceleration_measurement, IMU_DT_S)
if sample % TRACKING_STEPS == 0:
current_time = float(time_s[sample])
true_cn0 = baseline_cn0 + scenario.cn0_offset(current_time)
raw_cn0 = np.clip(true_cn0 + rng.normal(0.0, 1.2, satellite_count), 10.0, 60.0)
filtered_cn0 = CN0_ALPHA * raw_cn0 + (1.0 - CN0_ALPHA) * filtered_cn0
nominal_sigma = velocity_sigma_mps(true_cn0)
actual_sigma = nominal_sigma * np.asarray(
[
scenario.noise_multiplier(current_time, satellite)
for satellite in range(satellite_count)
]
)
raw = (
-los @ truth_velocity[sample]
+ scenario.receiver_clock_drift_mps(current_time)
+ rng.normal(0.0, actual_sigma)
+ np.asarray(
[
scenario.outlier_mps(current_time, satellite)
for satellite in range(satellite_count)
]
)
)
if fixed_measurement is None:
fixed_measurement = raw.copy()
adaptive_measurement = raw.copy()
else:
fixed_measurement += FIXED_DOPPLER_ALPHA * (raw - fixed_measurement)
latest_alpha = alpha_selector.update(
latest_acceleration_measurement,
float(np.median(filtered_cn0)),
)
adaptive_measurement += latest_alpha * (raw - adaptive_measurement)
latest_raw = raw
if (
sample > 0
and sample % WLS_STEPS == 0
and latest_raw is not None
and not _in_gap(scenario, float(time_s[sample]))
):
for path in paths:
if path == DopplerPath.FIXED_IIR:
observations = fixed_measurement
elif path == DopplerPath.ADAPTIVE_IIR:
observations = adaptive_measurement
else:
observations = latest_raw
result = velocity_wls(
np.asarray(observations),
filtered_cn0,
robust=(path == DopplerPath.RAW_ROBUST),
)
accepted = filters[path].update_velocity(
result.velocity_mps,
fusion_velocity_covariance(result.covariance_mps2),
)
wls_times[path].append(float(time_s[sample]))
wls_velocity[path].append(result.velocity_mps)
wls_clock_drift[path].append(result.clock_drift_mps)
wls_covariance[path].append(result.covariance_mps2)
wls_covariance_scale[path].append(result.covariance_scale)
wls_nis[path].append(filters[path].last_nis)
wls_accepted[path].append(accepted)
wls_alpha[path].append(
latest_alpha
if path == DopplerPath.ADAPTIVE_IIR
else (FIXED_DOPPLER_ALPHA if path == DopplerPath.FIXED_IIR else 1.0)
)
excluded[path].append(
-1 if result.excluded_satellite is None else result.excluded_satellite
)
for path in paths:
fused_history[path][sample] = filters[path].velocity_mps
fused_covariance_history[path][sample] = filters[path].covariance[:3, :3]
return {
path: DopplerEskfTrace(
scenario=scenario.name,
path=path,
time_s=time_s,
truth_velocity_mps=truth_velocity,
fused_velocity_mps=fused_history[path],
fused_velocity_covariance_mps2=fused_covariance_history[path],
acceleration_mps2=truth_acceleration,
wls_time_s=np.asarray(wls_times[path]),
wls_velocity_mps=np.asarray(wls_velocity[path]),
wls_clock_drift_mps=np.asarray(wls_clock_drift[path]),
wls_covariance_mps2=np.asarray(wls_covariance[path]),
wls_covariance_scale=np.asarray(wls_covariance_scale[path]),
wls_nis=np.asarray(wls_nis[path]),
wls_accepted=np.asarray(wls_accepted[path], dtype=bool),
adaptive_alpha=np.asarray(wls_alpha[path]),
excluded_satellites=np.asarray(excluded[path], dtype=int),
)
for path in paths
}
@@ -0,0 +1,353 @@
"""Python mirror of the firmware's code-measurement WLS policy.
The model copies the receiver-specific parts of NavEquations.c:
* four states: ECEF x/y/z and common receiver clock bias in metres;
* URA + elevation + C/N0 variance model;
* normalized weights for the numerical solve and absolute weights for
covariance/integrity;
* the 99-percent chi-square table;
* single-satellite leave-one-out FDE only when at least seven measurements
are available.
Atmospheric and satellite-clock corrections are represented as already
corrected measurement errors. That keeps this study focused on tracking
quality rather than duplicating the existing PPS orbit/atmosphere model.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import numpy as np
URA_SIGMA_M = np.asarray(
[
2.4,
3.4,
4.85,
6.85,
9.65,
13.65,
24.0,
48.0,
96.0,
192.0,
384.0,
768.0,
1536.0,
3072.0,
6144.0,
],
dtype=float,
)
CHI_SQUARE_99 = np.asarray(
[0.0, 6.634896601, 9.210340372, 11.34486673, 13.27670414, 15.08627247],
dtype=float,
)
class SigmaMethod(str, Enum):
CURRENT = "current_cn0"
NAIVE_ADD_DLL = "naive_add_dll"
DLL_EXCESS = "dll_excess"
DLL_EXCESS_GUARD = "dll_excess_guard"
ORACLE = "oracle_variance"
@dataclass(frozen=True)
class SigmaInputs:
ura_index: np.ndarray
elevation_deg: np.ndarray
cn0_dbhz: np.ndarray
dll_variance_ratio: np.ndarray
dll_mean_z: np.ndarray
actual_jitter_multiplier: np.ndarray
@dataclass(frozen=True)
class WlsSolution:
position_ecef_m: np.ndarray
clock_bias_m: float
residual_m: np.ndarray
sigma_m: np.ndarray
covariance: np.ndarray
chi_square: float
chi_square_ratio: float
chi_square_pass: bool
valid: bool
used_indices: np.ndarray
excluded_index: int | None
def cn0_code_sigma_m(cn0_dbhz: np.ndarray | float) -> np.ndarray:
sigma = 3.0 * np.power(10.0, (45.0 - np.asarray(cn0_dbhz)) / 20.0)
return np.clip(sigma, 0.5, 30.0)
def elevation_sigma_m(elevation_deg: np.ndarray | float) -> np.ndarray:
bounded = np.clip(np.asarray(elevation_deg), 10.0, 90.0)
return 3.0 / np.sin(np.deg2rad(bounded))
def firmware_sigma_m(
inputs: SigmaInputs,
method: SigmaMethod,
) -> np.ndarray:
ura = URA_SIGMA_M[np.asarray(inputs.ura_index, dtype=int)]
elevation = elevation_sigma_m(inputs.elevation_deg)
code = cn0_code_sigma_m(inputs.cn0_dbhz)
ratio = np.clip(np.asarray(inputs.dll_variance_ratio), 0.05, 25.0)
if method == SigmaMethod.CURRENT:
code_variance_factor = np.ones_like(code)
elif method == SigmaMethod.NAIVE_ADD_DLL:
# Deliberately included as a negative control: at ratio=1 this counts
# the C/N0-predicted thermal jitter twice.
code_variance_factor = 1.0 + ratio
elif method == SigmaMethod.DLL_EXCESS:
# Only the measured excess over the C/N0 expectation is added.
code_variance_factor = np.maximum(1.0, ratio)
elif method == SigmaMethod.DLL_EXCESS_GUARD:
mean_factor = np.square(
np.maximum(1.0, np.asarray(inputs.dll_mean_z) / 3.0)
)
code_variance_factor = np.maximum.reduce(
[np.ones_like(code), ratio, mean_factor]
)
elif method == SigmaMethod.ORACLE:
code_variance_factor = np.square(
np.maximum(0.05, np.asarray(inputs.actual_jitter_multiplier))
)
else:
raise ValueError(f"unsupported sigma method: {method}")
return np.sqrt(
ura * ura
+ elevation * elevation
+ code * code * code_variance_factor
)
def ecef_to_lla(ecef_m: np.ndarray) -> tuple[float, float, float]:
x, y, z = np.asarray(ecef_m, dtype=float)
a = 6_378_137.0
inverse_flattening = 298.257223563
flattening = 1.0 / inverse_flattening
eccentricity_squared = flattening * (2.0 - flattening)
longitude = np.arctan2(y, x)
horizontal = np.hypot(x, y)
latitude = np.arctan2(
z,
horizontal * (1.0 - eccentricity_squared),
)
height = 0.0
for _ in range(12):
sine = np.sin(latitude)
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
height = horizontal / max(np.cos(latitude), 1.0e-15) - prime_vertical
next_latitude = np.arctan2(
z,
horizontal
* (
1.0
- eccentricity_squared
* prime_vertical
/ max(prime_vertical + height, 1.0)
),
)
if abs(next_latitude - latitude) < 1.0e-14:
latitude = next_latitude
break
latitude = next_latitude
return (
float(np.rad2deg(latitude)),
float(np.rad2deg(longitude)),
float(height),
)
def lla_to_ecef(latitude_deg: float, longitude_deg: float, height_m: float) -> np.ndarray:
a = 6_378_137.0
inverse_flattening = 298.257223563
flattening = 1.0 / inverse_flattening
eccentricity_squared = flattening * (2.0 - flattening)
latitude = np.deg2rad(latitude_deg)
longitude = np.deg2rad(longitude_deg)
sine = np.sin(latitude)
cosine = np.cos(latitude)
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
return np.asarray(
[
(prime_vertical + height_m) * cosine * np.cos(longitude),
(prime_vertical + height_m) * cosine * np.sin(longitude),
(
prime_vertical * (1.0 - eccentricity_squared)
+ height_m
)
* sine,
],
dtype=float,
)
def enu_axes(receiver_ecef_m: np.ndarray) -> np.ndarray:
latitude_deg, longitude_deg, _ = ecef_to_lla(receiver_ecef_m)
latitude = np.deg2rad(latitude_deg)
longitude = np.deg2rad(longitude_deg)
east = np.asarray([-np.sin(longitude), np.cos(longitude), 0.0])
north = np.asarray(
[
-np.sin(latitude) * np.cos(longitude),
-np.sin(latitude) * np.sin(longitude),
np.cos(latitude),
]
)
up = np.asarray(
[
np.cos(latitude) * np.cos(longitude),
np.cos(latitude) * np.sin(longitude),
np.sin(latitude),
]
)
return np.vstack([east, north, up])
def satellites_from_azel(
receiver_ecef_m: np.ndarray,
azimuth_deg: np.ndarray,
elevation_deg: np.ndarray,
range_m: float = 21_000_000.0,
) -> np.ndarray:
axes = enu_axes(receiver_ecef_m)
satellites: list[np.ndarray] = []
for azimuth, elevation in zip(azimuth_deg, elevation_deg):
azimuth_rad = np.deg2rad(azimuth)
elevation_rad = np.deg2rad(elevation)
los_enu = np.asarray(
[
np.cos(elevation_rad) * np.sin(azimuth_rad),
np.cos(elevation_rad) * np.cos(azimuth_rad),
np.sin(elevation_rad),
]
)
los_ecef = axes.T @ los_enu
satellites.append(receiver_ecef_m + range_m * los_ecef)
return np.asarray(satellites)
def position_error_enu_m(
estimated_ecef_m: np.ndarray,
true_ecef_m: np.ndarray,
) -> np.ndarray:
return enu_axes(true_ecef_m) @ (estimated_ecef_m - true_ecef_m)
def _candidate(
satellite_positions_m: np.ndarray,
observations_m: np.ndarray,
sigma_m: np.ndarray,
initial_state: np.ndarray,
used_indices: np.ndarray,
) -> WlsSolution:
state = np.asarray(initial_state, dtype=float).copy()
weights = 1.0 / np.square(sigma_m)
design = np.empty((len(observations_m), 4), dtype=float)
for _ in range(10):
difference = satellite_positions_m - state[:3]
geometric_range = np.linalg.norm(difference, axis=1)
design[:, :3] = -difference / geometric_range[:, None]
design[:, 3] = 1.0
residual = observations_m - geometric_range - state[3]
normal = design.T @ (weights[:, None] * design)
rhs = design.T @ (weights * residual)
update = np.linalg.solve(normal, rhs)
state += update
if np.linalg.norm(update) < 1.0e-4:
break
difference = satellite_positions_m - state[:3]
geometric_range = np.linalg.norm(difference, axis=1)
design[:, :3] = -difference / geometric_range[:, None]
design[:, 3] = 1.0
residual = observations_m - geometric_range - state[3]
normal = design.T @ (weights[:, None] * design)
covariance = np.linalg.inv(normal)
chi_square = float(np.sum(weights * np.square(residual)))
degrees_of_freedom = len(observations_m) - 4
threshold = (
float(CHI_SQUARE_99[degrees_of_freedom])
if 0 < degrees_of_freedom < len(CHI_SQUARE_99)
else 0.0
)
chi_square_pass = degrees_of_freedom > 0 and chi_square <= threshold
valid = (
(len(observations_m) == 4 and degrees_of_freedom == 0)
or chi_square_pass
)
ratio = chi_square / threshold if threshold > 0.0 else float("inf")
return WlsSolution(
position_ecef_m=state[:3].copy(),
clock_bias_m=float(state[3]),
residual_m=residual.copy(),
sigma_m=sigma_m.copy(),
covariance=covariance,
chi_square=chi_square,
chi_square_ratio=ratio,
chi_square_pass=chi_square_pass,
valid=valid,
used_indices=used_indices.copy(),
excluded_index=None,
)
def firmware_wls(
satellite_positions_m: np.ndarray,
observations_m: np.ndarray,
sigma_m: np.ndarray,
initial_state: np.ndarray,
*,
enable_fde: bool = True,
) -> WlsSolution:
count = len(observations_m)
if count < 4:
raise ValueError("position WLS needs at least four satellites")
full_indices = np.arange(count, dtype=int)
full = _candidate(
satellite_positions_m,
observations_m,
sigma_m,
initial_state,
full_indices,
)
if full.chi_square_pass or not enable_fde or count < 7:
return full
selected = full
passing_found = False
full_state = np.concatenate([full.position_ecef_m, [full.clock_bias_m]])
for excluded in range(count):
used = full_indices[full_indices != excluded]
candidate = _candidate(
satellite_positions_m[used],
observations_m[used],
sigma_m[used],
full_state,
used,
)
candidate = WlsSolution(
**{
**candidate.__dict__,
"excluded_index": excluded,
}
)
if candidate.chi_square_pass:
if not passing_found or candidate.chi_square_ratio < selected.chi_square_ratio:
selected = candidate
passing_found = True
elif not passing_found and candidate.chi_square_ratio < selected.chi_square_ratio:
selected = candidate
return selected
@@ -0,0 +1,368 @@
"""Synthetic code-tracking scenarios shared by the analysis and tests."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
import numpy as np
from .firmware_wls_model import (
SigmaInputs,
SigmaMethod,
URA_SIGMA_M,
cn0_code_sigma_m,
elevation_sigma_m,
enu_axes,
firmware_sigma_m,
firmware_wls,
lla_to_ecef,
position_error_enu_m,
satellites_from_azel,
)
from .rtl_dll_model import expected_discriminator_std, synthesize_dll_window
TRACKING_PERIOD_S = 0.020
WLS_PERIOD_S = 0.200
TRACKING_UPDATES_PER_WLS = int(round(WLS_PERIOD_S / TRACKING_PERIOD_S))
FIRMWARE_CN0_ALPHA = 0.20
DLL_RATIO_ALPHA = 0.10
DLL_MEAN_ALPHA = 0.20
GOOD_AZIMUTH_DEG = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
GOOD_ELEVATION_DEG = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
POOR_AZIMUTH_DEG = np.asarray([25.0, 46.0, 68.0, 89.0, 112.0, 136.0, 159.0, 182.0])
POOR_ELEVATION_DEG = np.asarray([18.0, 22.0, 25.0, 29.0, 32.0, 27.0, 21.0, 16.0])
Profile = Callable[[float, int, float], float]
def _identity(_time_s: float, _satellite: int, baseline: float) -> float:
return baseline
def _one(_time_s: float, _satellite: int, _baseline: float) -> float:
return 1.0
def _zero(_time_s: float, _satellite: int, _baseline: float) -> float:
return 0.0
@dataclass(frozen=True)
class Scenario:
name: str
description: str
duration_s: float = 80.0
poor_geometry: bool = False
cn0_profile: Profile = _identity
jitter_profile: Profile = _one
discriminator_bias_profile: Profile = _zero
pseudorange_bias_profile: Profile = _zero
@dataclass
class ChannelQuality:
cn0_dbhz: float | None = None
dll_ratio: float = 1.0
dll_mean_z: float = 0.0
def update(
self,
raw_cn0_dbhz: float,
dll_samples: np.ndarray,
) -> None:
if self.cn0_dbhz is None:
self.cn0_dbhz = raw_cn0_dbhz
else:
self.cn0_dbhz = (
FIRMWARE_CN0_ALPHA * raw_cn0_dbhz
+ (1.0 - FIRMWARE_CN0_ALPHA) * self.cn0_dbhz
)
measured_variance = float(np.var(dll_samples, ddof=1))
expected_std = expected_discriminator_std(self.cn0_dbhz)
raw_ratio = measured_variance / max(expected_std * expected_std, 1.0e-15)
raw_ratio = float(np.clip(raw_ratio, 0.05, 25.0))
self.dll_ratio = (
DLL_RATIO_ALPHA * raw_ratio
+ (1.0 - DLL_RATIO_ALPHA) * self.dll_ratio
)
standard_error = np.sqrt(
max(measured_variance, expected_std * expected_std)
/ len(dll_samples)
)
raw_mean_z = abs(float(np.mean(dll_samples))) / max(standard_error, 1.0e-12)
raw_mean_z = float(np.clip(raw_mean_z, 0.0, 12.0))
self.dll_mean_z = (
DLL_MEAN_ALPHA * raw_mean_z
+ (1.0 - DLL_MEAN_ALPHA) * self.dll_mean_z
)
@dataclass(frozen=True)
class EpochSample:
method: SigmaMethod
time_s: float
enu_error_m: np.ndarray
horizontal_error_m: float
position_error_3d_m: float
horizontal_sigma_m: float
valid: bool
chi_square_ratio: float
fde_applied: bool
mean_measurement_sigma_m: float
maximum_dll_ratio: float
def scenario_matrix() -> tuple[Scenario, ...]:
return (
Scenario(
"clean_open_sky",
"C/N0 model and actual DLL jitter agree.",
),
Scenario(
"weak_signal",
"All channels lose 11 dB; current C/N0 weighting already sees it.",
cn0_profile=lambda _t, _s, baseline: baseline - 11.0,
),
Scenario(
"one_channel_hidden_jitter",
"One high-C/N0 channel has 4x DLL jitter not visible in C/N0.",
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 1 else 1.0,
),
Scenario(
"common_hidden_jitter",
"All channels have the same unmodelled 3x DLL jitter.",
jitter_profile=lambda _t, _satellite, _b: 3.0,
),
Scenario(
"noise_burst_recovery",
"Two channels have 5x jitter from 22 s to 45 s, then recover.",
jitter_profile=lambda time_s, satellite, _b: (
5.0
if 22.0 <= time_s < 45.0 and satellite in (1, 5)
else 1.0
),
),
Scenario(
"dll_transient_stress",
"A channel has discriminator mean without excess variance during transient stress.",
discriminator_bias_profile=lambda time_s, satellite, _b: (
0.055
if 25.0 <= time_s < 34.0 and satellite == 3
else 0.0
),
pseudorange_bias_profile=lambda time_s, satellite, _b: (
9.0
if 25.0 <= time_s < 34.0 and satellite == 3
else 0.0
),
),
Scenario(
"stable_multipath_bias",
"Low-jitter, high-C/N0 channel has a stable 14 m code bias.",
pseudorange_bias_profile=lambda _t, satellite, _b: (
14.0 if satellite == 6 else 0.0
),
),
Scenario(
"mixed_multipath",
"One channel has both a 10 m bias and 3x excess jitter.",
jitter_profile=lambda _t, satellite, _b: 3.0 if satellite == 2 else 1.0,
pseudorange_bias_profile=lambda _t, satellite, _b: (
10.0 if satellite == 2 else 0.0
),
),
Scenario(
"poor_geometry_hidden_jitter",
"Clustered geometry plus one channel with unmodelled 4x jitter.",
poor_geometry=True,
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 4 else 1.0,
),
Scenario(
"cn0_step_recovery",
"C/N0 is initially weak and improves; no hidden DLL noise exists.",
cn0_profile=lambda time_s, _satellite, baseline: (
baseline - 12.0 if time_s < 30.0 else baseline
),
),
)
def _geometry(scenario: Scenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
receiver = lla_to_ecef(39.9208, 32.8541, 930.0)
if scenario.poor_geometry:
azimuth = POOR_AZIMUTH_DEG
elevation = POOR_ELEVATION_DEG
else:
azimuth = GOOD_AZIMUTH_DEG
elevation = GOOD_ELEVATION_DEG
satellites = satellites_from_azel(receiver, azimuth, elevation)
return receiver, satellites, elevation.copy()
def run_scenario(
scenario: Scenario,
seed: int,
methods: tuple[SigmaMethod, ...] = tuple(SigmaMethod),
) -> list[EpochSample]:
rng = np.random.default_rng(seed)
receiver, satellites, elevations = _geometry(scenario)
satellite_count = len(satellites)
geometric_ranges = np.linalg.norm(satellites - receiver, axis=1)
baseline_cn0 = np.clip(34.0 + 0.16 * elevations, 30.0, 48.0)
ura_indices = np.zeros(satellite_count, dtype=np.uint8)
quality = [ChannelQuality() for _ in range(satellite_count)]
# These are deliberately slower/non-DLL terms. They prevent a synthetic
# white-noise-only experiment from overstating what DLL weighting can fix.
ura_error = rng.normal(0.0, 0.45 * URA_SIGMA_M[0], satellite_count)
atmospheric_state = rng.normal(
0.0,
0.35 * elevation_sigma_m(elevations),
satellite_count,
)
code_noise_state = np.zeros(satellite_count)
initial_state = np.concatenate(
[
receiver + np.asarray([450.0, -300.0, 220.0]),
[0.0],
]
)
method_state = {method: initial_state.copy() for method in methods}
samples: list[EpochSample] = []
clock_bias_m = 82.0
receiver_enu_axes = enu_axes(receiver)
tracking_steps = int(round(scenario.duration_s / TRACKING_PERIOD_S))
latest_actual_jitter = np.ones(satellite_count)
for tracking_step in range(tracking_steps):
time_s = tracking_step * TRACKING_PERIOD_S
for satellite in range(satellite_count):
true_cn0 = scenario.cn0_profile(
time_s,
satellite,
float(baseline_cn0[satellite]),
)
actual_jitter = scenario.jitter_profile(
time_s,
satellite,
1.0,
)
discriminator_bias = scenario.discriminator_bias_profile(
time_s,
satellite,
0.0,
)
latest_actual_jitter[satellite] = actual_jitter
dll_window = synthesize_dll_window(
rng,
cn0_dbhz=true_cn0,
jitter_multiplier=actual_jitter,
discriminator_bias=discriminator_bias,
)
# The hardware NWPR C/N0 estimate is noisy; firmware applies alpha=0.2.
raw_cn0 = float(np.clip(true_cn0 + rng.normal(0.0, 1.3), 10.0, 60.0))
quality[satellite].update(raw_cn0, dll_window.samples)
if (tracking_step + 1) % TRACKING_UPDATES_PER_WLS != 0:
continue
true_cn0 = np.asarray(
[
scenario.cn0_profile(
time_s,
satellite,
float(baseline_cn0[satellite]),
)
for satellite in range(satellite_count)
]
)
actual_code_sigma = cn0_code_sigma_m(true_cn0) * latest_actual_jitter
innovation = rng.normal(0.0, actual_code_sigma)
code_noise_state = (
0.25 * code_noise_state
+ np.sqrt(1.0 - 0.25**2) * innovation
)
atmospheric_innovation = rng.normal(
0.0,
0.35 * elevation_sigma_m(elevations),
)
atmospheric_state = (
0.97 * atmospheric_state
+ np.sqrt(1.0 - 0.97**2) * atmospheric_innovation
)
pseudorange_bias = np.asarray(
[
scenario.pseudorange_bias_profile(
time_s,
satellite,
0.0,
)
for satellite in range(satellite_count)
]
)
observations = (
geometric_ranges
+ clock_bias_m
+ ura_error
+ atmospheric_state
+ code_noise_state
+ pseudorange_bias
)
sigma_inputs = SigmaInputs(
ura_index=ura_indices,
elevation_deg=elevations,
cn0_dbhz=np.asarray([item.cn0_dbhz for item in quality], dtype=float),
dll_variance_ratio=np.asarray([item.dll_ratio for item in quality]),
dll_mean_z=np.asarray([item.dll_mean_z for item in quality]),
actual_jitter_multiplier=latest_actual_jitter.copy(),
)
for method in methods:
sigma = firmware_sigma_m(sigma_inputs, method)
solution = firmware_wls(
satellites,
observations,
sigma,
method_state[method],
)
# NavEquations.c updates its persistent WLS seed only after an
# integrity-valid solution.
if solution.valid:
method_state[method] = np.concatenate(
[solution.position_ecef_m, [solution.clock_bias_m]]
)
enu_error = position_error_enu_m(solution.position_ecef_m, receiver)
enu_covariance = (
# The top-left block is ECEF position covariance.
# R * Q * R' maps it to ENU.
receiver_enu_axes
@ solution.covariance[:3, :3]
@ receiver_enu_axes.T
)
horizontal_sigma = float(
np.sqrt(max(0.0, enu_covariance[0, 0] + enu_covariance[1, 1]))
)
samples.append(
EpochSample(
method=method,
time_s=time_s,
enu_error_m=enu_error,
horizontal_error_m=float(np.linalg.norm(enu_error[:2])),
position_error_3d_m=float(np.linalg.norm(enu_error)),
horizontal_sigma_m=horizontal_sigma,
valid=solution.valid,
chi_square_ratio=solution.chi_square_ratio,
fde_applied=solution.excluded_index is not None,
mean_measurement_sigma_m=float(np.mean(sigma)),
maximum_dll_ratio=float(
max(item.dll_ratio for item in quality)
),
)
)
return samples
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

@@ -0,0 +1,94 @@
# GPS code measurement model sonuçları
Monte Carlo seed sayısı: `12`. İlk 5 saniye metriklere alınmadı.
## Yöntem karşılaştırması
| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |
|---|---|---:|---:|---:|---:|---:|
| clean_open_sky | Mevcut C/N0 | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
| clean_open_sky | Naif DLL ekle | 10.02 | +0.3% | 17.25 | 100.0 | 0.31 |
| clean_open_sky | DLL excess | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
| clean_open_sky | DLL excess+mean | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
| clean_open_sky | Oracle variance | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
| weak_signal | Mevcut C/N0 | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
| weak_signal | Naif DLL ekle | 32.31 | +1.3% | 55.46 | 100.0 | 56.18 |
| weak_signal | DLL excess | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
| weak_signal | DLL excess+mean | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
| weak_signal | Oracle variance | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
| one_channel_hidden_jitter | Mevcut C/N0 | 12.27 | +0.0% | 19.94 | 100.0 | 1.07 |
| one_channel_hidden_jitter | Naif DLL ekle | 10.49 | +14.5% | 17.75 | 100.0 | 0.33 |
| one_channel_hidden_jitter | DLL excess | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
| one_channel_hidden_jitter | DLL excess+mean | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
| one_channel_hidden_jitter | Oracle variance | 10.46 | +14.7% | 17.74 | 100.0 | 0.33 |
| common_hidden_jitter | Mevcut C/N0 | 33.79 | +0.0% | 59.76 | 94.8 | 52.40 |
| common_hidden_jitter | Naif DLL ekle | 27.61 | +18.3% | 47.56 | 100.0 | 43.93 |
| common_hidden_jitter | DLL excess | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
| common_hidden_jitter | DLL excess+mean | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
| common_hidden_jitter | Oracle variance | 27.74 | +17.9% | 47.83 | 100.0 | 43.87 |
| noise_burst_recovery | Mevcut C/N0 | 12.96 | +0.0% | 22.13 | 98.9 | 2.58 |
| noise_burst_recovery | Naif DLL ekle | 10.52 | +18.8% | 18.03 | 100.0 | 0.53 |
| noise_burst_recovery | DLL excess | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
| noise_burst_recovery | DLL excess+mean | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
| noise_burst_recovery | Oracle variance | 10.47 | +19.2% | 17.90 | 100.0 | 0.49 |
| dll_transient_stress | Mevcut C/N0 | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
| dll_transient_stress | Naif DLL ekle | 10.12 | +0.3% | 17.33 | 100.0 | 0.38 |
| dll_transient_stress | DLL excess | 10.14 | +0.0% | 17.38 | 100.0 | 0.38 |
| dll_transient_stress | DLL excess+mean | 10.12 | +0.2% | 17.37 | 100.0 | 0.33 |
| dll_transient_stress | Oracle variance | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
| stable_multipath_bias | Mevcut C/N0 | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
| stable_multipath_bias | Naif DLL ekle | 12.39 | +1.3% | 20.87 | 100.0 | 1.47 |
| stable_multipath_bias | DLL excess | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
| stable_multipath_bias | DLL excess+mean | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
| stable_multipath_bias | Oracle variance | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
| mixed_multipath | Mevcut C/N0 | 14.43 | +0.0% | 25.38 | 100.0 | 5.64 |
| mixed_multipath | Naif DLL ekle | 11.26 | +22.0% | 19.60 | 100.0 | 0.73 |
| mixed_multipath | DLL excess | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
| mixed_multipath | DLL excess+mean | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
| mixed_multipath | Oracle variance | 11.06 | +23.4% | 19.20 | 100.0 | 0.73 |
| poor_geometry_hidden_jitter | Mevcut C/N0 | 293.82 | +0.0% | 574.96 | 100.0 | 94.31 |
| poor_geometry_hidden_jitter | Naif DLL ekle | 206.13 | +29.8% | 405.15 | 100.0 | 92.42 |
| poor_geometry_hidden_jitter | DLL excess | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
| poor_geometry_hidden_jitter | DLL excess+mean | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
| poor_geometry_hidden_jitter | Oracle variance | 196.20 | +33.2% | 386.58 | 100.0 | 91.27 |
| cn0_step_recovery | Mevcut C/N0 | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
| cn0_step_recovery | Naif DLL ekle | 22.27 | +1.4% | 47.59 | 100.0 | 21.42 |
| cn0_step_recovery | DLL excess | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
| cn0_step_recovery | DLL excess+mean | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
| cn0_step_recovery | Oracle variance | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
## Modelden çıkan karar özeti
- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.
- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.
- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.
- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.
- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.
- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.
## Bozulma penceresi odaklı 3D RMS
| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |
|---|---:|---:|---:|---:|
| noise_burst_recovery [22,45) s | 17.83 | 11.36 | 11.36 | 11.30 |
| dll_transient_stress [25,34) s | 10.83 | 10.82 | 10.65 | 10.83 |
| cn0_step_recovery [5,30) s | 36.48 | 36.48 | 36.48 | 36.48 |
## Code smoothing ve carrier CFO
| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |
|---|---:|---:|---:|---:|---:|---:|---:|
| cfo_compensated | 3.99 | 248.61 | 0.94 | 16.42 | 414.08 | 2.05 | 84.13 |
| cfo_residual_0p5_mps | 3.99 | 248.61 | 1.30 | 16.41 | 414.08 | 2.57 | 83.23 |
| cfo_residual_3_mps | 3.99 | 248.61 | 5.48 | 17.06 | 414.08 | 7.07 | 78.73 |
## Yorum sınırları
- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.
- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.
- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.
- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.
- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.
- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.
- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.
- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.
@@ -0,0 +1,65 @@
# Doppler WLS -> ESKF model sonuçları
Monte Carlo seed sayısı: `20`; ilk 5 saniye RMS metriklerinden çıkarıldı.
50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.
## Sonuç tablosu
| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |
|---|---|---:|---:|---:|---:|---:|---:|
| steady_cruise | Önceki sabit IIR | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Ham Doppler + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Adaptif IIR + ESKF | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
| steady_cruise | Ham + robust WLS + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
| airliner_maneuver | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
| airliner_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Önceki sabit IIR | 0.942 | 0.919 | 1.178 | 100.0 | 55.9 | 0.0 |
| aggressive_maneuver | Ham Doppler + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Adaptif IIR + ESKF | 0.200 | 0.197 | 0.171 | 100.0 | 100.0 | 0.0 |
| aggressive_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Önceki sabit IIR | 0.419 | 0.443 | 0.485 | 100.0 | 99.7 | 0.0 |
| low_cn0_airliner | Ham Doppler + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Adaptif IIR + ESKF | 0.206 | 0.203 | 0.338 | 100.0 | 100.0 | 0.0 |
| low_cn0_airliner | Ham + robust WLS + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Önceki sabit IIR | 0.495 | 0.478 | 1.146 | 100.0 | 97.7 | 0.0 |
| single_satellite_outliers | Ham Doppler + ESKF | 0.257 | 0.229 | 1.451 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Adaptif IIR + ESKF | 0.356 | 0.312 | 1.154 | 100.0 | 100.0 | 0.0 |
| single_satellite_outliers | Ham + robust WLS + ESKF | 0.191 | 0.179 | 0.418 | 100.0 | 100.0 | 2.9 |
| clock_drift_steps | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
| clock_drift_steps | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| clock_drift_steps | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
| clock_drift_steps | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Önceki sabit IIR | 0.397 | 0.413 | 0.431 | 100.0 | 99.9 | 0.0 |
| gps_gap_recovery | Ham Doppler + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Adaptif IIR + ESKF | 0.208 | 0.197 | 0.129 | 100.0 | 100.0 | 0.0 |
| gps_gap_recovery | Ham + robust WLS + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
| mixed_flight_faults | Önceki sabit IIR | 0.940 | 0.913 | 1.515 | 100.0 | 60.4 | 0.0 |
| mixed_flight_faults | Ham Doppler + ESKF | 0.224 | 0.209 | 1.498 | 100.0 | 100.0 | 0.0 |
| mixed_flight_faults | Adaptif IIR + ESKF | 0.295 | 0.275 | 0.838 | 100.0 | 99.9 | 0.0 |
| mixed_flight_faults | Ham + robust WLS + ESKF | 0.221 | 0.206 | 1.013 | 100.0 | 100.0 | 1.9 |
## Bulgular
- Sabit hızda pre-IIR ESKF RMS'i `0.182` m/s, ham yol `0.190` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `0.416` m/s WLS RMS'ten bu seviyeye indiriyor.
- Airliner manevrasında sabit IIR `0.416` m/s ve `%0.0` NIS ret oranı verirken ham yol `0.190` m/s ve `%0.0` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.
- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `0.419`, `0.251`, `0.206` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.
- Tek uydu burst'ünde raw WLS RMS `1.451` m/s, robust leave-one-out ile `0.418` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.
- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `0.269` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.
## Mimari kararı
- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.
- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.
- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.
- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.
## Model sınırları
- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.
- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.
- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.
- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.
- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.
- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.
@@ -0,0 +1,33 @@
scenario,path,seeds,fused_rms_mps,fused_rms_seed_std_mps,fused_p95_mps,quiet_rms_mps,dynamic_rms_mps,wls_rms_mps,wls_p95_mps,clock_drift_rms_mps,velocity_accept_percent,nis_reject_percent,fused_95_coverage_percent,excluded_epoch_percent,mean_covariance_scale,mean_alpha
steady_cruise,fixed_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
steady_cruise,raw_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
steady_cruise,adaptive_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
steady_cruise,raw_robust_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
airliner_maneuver,fixed_iir_wls_eskf,20,0.41605194656716626,0.04357555329949249,0.7083141902703941,0.4019739347287984,0.4380205569605863,0.440222331186762,0.9391237648850084,0.04445747832784397,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
airliner_maneuver,raw_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
airliner_maneuver,adaptive_iir_wls_eskf,20,0.19421091770965,0.04161706926344872,0.2913238026993562,0.19649429208827177,0.18911500227901482,0.1349904626304289,0.2634780317604405,0.06996150326971154,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
airliner_maneuver,raw_robust_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
aggressive_maneuver,fixed_iir_wls_eskf,20,0.9420862291590975,0.03697606175587584,1.7661046896298587,0.9551353937753919,0.918769792345089,1.1782538620062029,2.634559496211176,0.04445747832784312,100.0,0.0,55.891176470588235,0.0,1.0000030764799477,0.05000000000000001
aggressive_maneuver,raw_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
aggressive_maneuver,adaptive_iir_wls_eskf,20,0.20034438129208682,0.04215736847319882,0.3027038479248761,0.2017392380134829,0.19713256311771804,0.17059135831478262,0.3555978539018994,0.08938088837068828,100.0,0.0,100.0,0.0,1.0001185178364316,0.17603563474387524
aggressive_maneuver,raw_robust_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
low_cn0_airliner,fixed_iir_wls_eskf,20,0.4194988293998554,0.04110688289349549,0.7094271250760412,0.4045729059455276,0.4427791039662977,0.4845544319763686,0.9431345662991619,0.14058689054355467,100.0,0.0,99.74176470588235,0.0,1.0000281042403611,0.05000000000000001
low_cn0_airliner,raw_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
low_cn0_airliner,adaptive_iir_wls_eskf,20,0.20588009483954214,0.040099541607423995,0.3090368268997948,0.2069944088941925,0.20293127741446826,0.337603473203961,0.684354700050294,0.21325106769335284,100.0,0.0,100.0,0.0,1.0000902389090656,0.1040055679287305
low_cn0_airliner,raw_robust_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
single_satellite_outliers,fixed_iir_wls_eskf,20,0.49511238386612133,0.053875439119808514,0.8173709856169348,0.5050054814659805,0.4779542859711989,1.1463378050509494,0.9892652696081077,0.8027251360930692,100.0,0.0,97.6964705882353,0.0,6.927634385063575,0.05000000000000001
single_satellite_outliers,raw_wls_eskf,20,0.2570227443883875,0.05273042903908908,0.39439133440568985,0.27216019157834215,0.22927677845507954,1.4512624341492657,0.8297158565959334,1.0630956381619745,100.0,0.0,100.0,0.0,11.13597527632786,1.0
single_satellite_outliers,adaptive_iir_wls_eskf,20,0.3556263234893831,0.05851649747663683,0.595609347562141,0.3792437361871613,0.31186327209433895,1.1538474734782178,0.5683617007748845,0.8527337411854743,100.0,0.0,100.0,0.0,7.7254775572955,0.11647550111358576
single_satellite_outliers,raw_robust_wls_eskf,20,0.19054941715698687,0.03971061631217381,0.2889429468862187,0.19658211712738688,0.1789788721793858,0.4181969381744313,0.7254268588122093,0.27034697594947305,100.0,0.0,100.0,2.8953229398663693,1.2641999517115465,1.0
clock_drift_steps,fixed_iir_wls_eskf,20,0.4160519465671664,0.04357555329949111,0.7083141902703948,0.4019739347287993,0.4380205569605854,0.44022233118676146,0.9391237648850096,1.8177258394243663,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
clock_drift_steps,raw_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
clock_drift_steps,adaptive_iir_wls_eskf,20,0.19421091770965143,0.0416170692634497,0.29132380269935765,0.19649429208827301,0.18911500227901648,0.1349904626304289,0.26347803176043993,1.818528619384018,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
clock_drift_steps,raw_robust_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
gps_gap_recovery,fixed_iir_wls_eskf,20,0.3971781427894736,0.044796626065109466,0.7104470646835127,0.3873196668983798,0.41253405912166263,0.43094056066637465,0.9463346669492851,0.04458606585950929,100.0,0.0,99.90529411764706,0.0,1.0000036934211136,0.05
gps_gap_recovery,raw_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
gps_gap_recovery,adaptive_iir_wls_eskf,20,0.20776527476484566,0.044556420313135676,0.3257106582800945,0.21357805324970008,0.19661285828638408,0.12935782738837362,0.2613749696102362,0.06764578524374312,100.0,0.0,100.0,0.0,1.0000036934211136,0.10814839572192514
gps_gap_recovery,raw_robust_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
mixed_flight_faults,fixed_iir_wls_eskf,20,0.9404176641021843,0.04319034103529422,1.7661662430364344,0.9555417869207146,0.9132850762857121,1.5154618461064036,2.663221132275041,0.7591896512045151,100.0,0.0,60.37294117647059,0.0,1.3397749809447188,0.05000000000000001
mixed_flight_faults,raw_wls_eskf,20,0.22399771088975795,0.05069473595820174,0.3492819306443341,0.23151007332767243,0.2092729705094658,1.498455933396739,2.36689254151483,0.7603191788358898,100.0,0.0,100.0,0.0,2.1864248911994575,1.0
mixed_flight_faults,adaptive_iir_wls_eskf,20,0.2952659489493427,0.07200078881833771,0.4874378367724045,0.30541797568187434,0.2754557634719251,0.838415542523151,0.9361678311586857,0.47500979673114047,100.0,0.0,99.92470588235294,0.0,1.250042525522558,0.178938679245283
mixed_flight_faults,raw_robust_wls_eskf,20,0.2208132635451996,0.04372541489397352,0.3416439647301807,0.22864253849532457,0.20560052604298312,1.0129266912203836,2.2537697549366906,0.673490930924677,100.0,0.0,100.0,1.9103773584905661,1.5692458289027678,1.0
1 scenario path seeds fused_rms_mps fused_rms_seed_std_mps fused_p95_mps quiet_rms_mps dynamic_rms_mps wls_rms_mps wls_p95_mps clock_drift_rms_mps velocity_accept_percent nis_reject_percent fused_95_coverage_percent excluded_epoch_percent mean_covariance_scale mean_alpha
2 steady_cruise fixed_iir_wls_eskf 20 0.1821998934975134 0.040897665127266884 0.27466022199861373 0.1821998934975134 nan 0.0681533332956358 0.11645920599554568 0.04445747832784426 100.0 0.0 100.0 0.0 1.0000030764799477 0.05000000000000001
3 steady_cruise raw_wls_eskf 20 0.19046432375253602 0.040009781644375395 0.28906917942032884 0.19046432375253602 nan 0.4164164729808367 0.720889200812689 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343088 1.0
4 steady_cruise adaptive_iir_wls_eskf 20 0.1821998934975134 0.040897665127266884 0.27466022199861373 0.1821998934975134 nan 0.0681533332956358 0.11645920599554568 0.04445747832784426 100.0 0.0 100.0 0.0 1.0000030764799477 0.05000000000000001
5 steady_cruise raw_robust_wls_eskf 20 0.19046432375253602 0.040009781644375395 0.28906917942032884 0.19046432375253602 nan 0.4164164729808367 0.720889200812689 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343088 1.0
6 airliner_maneuver fixed_iir_wls_eskf 20 0.41605194656716626 0.04357555329949249 0.7083141902703941 0.4019739347287984 0.4380205569605863 0.440222331186762 0.9391237648850084 0.04445747832784397 100.0 0.0 99.71411764705883 0.0 1.0000030764799477 0.05000000000000001
7 airliner_maneuver raw_wls_eskf 20 0.19046432375253933 0.04000978164437598 0.28906917942032373 0.196483054968526 0.1789145728432288 0.4164164729808368 0.720889200812707 0.2689447776657465 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
8 airliner_maneuver adaptive_iir_wls_eskf 20 0.19421091770965 0.04161706926344872 0.2913238026993562 0.19649429208827177 0.18911500227901482 0.1349904626304289 0.2634780317604405 0.06996150326971154 100.0 0.0 100.0 0.0 1.0000030764799477 0.11647550111358576
9 airliner_maneuver raw_robust_wls_eskf 20 0.19046432375253933 0.04000978164437598 0.28906917942032373 0.196483054968526 0.1789145728432288 0.4164164729808368 0.720889200812707 0.2689447776657465 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
10 aggressive_maneuver fixed_iir_wls_eskf 20 0.9420862291590975 0.03697606175587584 1.7661046896298587 0.9551353937753919 0.918769792345089 1.1782538620062029 2.634559496211176 0.04445747832784312 100.0 0.0 55.891176470588235 0.0 1.0000030764799477 0.05000000000000001
11 aggressive_maneuver raw_wls_eskf 20 0.19046432375254996 0.04000978164436178 0.2890691794203225 0.1928303776618579 0.18561935253806933 0.41641647298083734 0.7208892008127159 0.2689447776657467 100.0 0.0 100.0 0.0 1.2633800541343083 1.0
12 aggressive_maneuver adaptive_iir_wls_eskf 20 0.20034438129208682 0.04215736847319882 0.3027038479248761 0.2017392380134829 0.19713256311771804 0.17059135831478262 0.3555978539018994 0.08938088837068828 100.0 0.0 100.0 0.0 1.0001185178364316 0.17603563474387524
13 aggressive_maneuver raw_robust_wls_eskf 20 0.19046432375254996 0.04000978164436178 0.2890691794203225 0.1928303776618579 0.18561935253806933 0.41641647298083734 0.7208892008127159 0.2689447776657467 100.0 0.0 100.0 0.0 1.2633800541343083 1.0
14 low_cn0_airliner fixed_iir_wls_eskf 20 0.4194988293998554 0.04110688289349549 0.7094271250760412 0.4045729059455276 0.4427791039662977 0.4845544319763686 0.9431345662991619 0.14058689054355467 100.0 0.0 99.74176470588235 0.0 1.0000281042403611 0.05000000000000001
15 low_cn0_airliner raw_wls_eskf 20 0.25103588282812916 0.04464089418791806 0.4041748199164089 0.2555504221567221 0.2416319579439005 1.3168245098334108 2.2796518151866563 0.8504780622313423 100.0 0.0 100.0 0.0 1.26385941740462 1.0
16 low_cn0_airliner adaptive_iir_wls_eskf 20 0.20588009483954214 0.040099541607423995 0.3090368268997948 0.2069944088941925 0.20293127741446826 0.337603473203961 0.684354700050294 0.21325106769335284 100.0 0.0 100.0 0.0 1.0000902389090656 0.1040055679287305
17 low_cn0_airliner raw_robust_wls_eskf 20 0.25103588282812916 0.04464089418791806 0.4041748199164089 0.2555504221567221 0.2416319579439005 1.3168245098334108 2.2796518151866563 0.8504780622313423 100.0 0.0 100.0 0.0 1.26385941740462 1.0
18 single_satellite_outliers fixed_iir_wls_eskf 20 0.49511238386612133 0.053875439119808514 0.8173709856169348 0.5050054814659805 0.4779542859711989 1.1463378050509494 0.9892652696081077 0.8027251360930692 100.0 0.0 97.6964705882353 0.0 6.927634385063575 0.05000000000000001
19 single_satellite_outliers raw_wls_eskf 20 0.2570227443883875 0.05273042903908908 0.39439133440568985 0.27216019157834215 0.22927677845507954 1.4512624341492657 0.8297158565959334 1.0630956381619745 100.0 0.0 100.0 0.0 11.13597527632786 1.0
20 single_satellite_outliers adaptive_iir_wls_eskf 20 0.3556263234893831 0.05851649747663683 0.595609347562141 0.3792437361871613 0.31186327209433895 1.1538474734782178 0.5683617007748845 0.8527337411854743 100.0 0.0 100.0 0.0 7.7254775572955 0.11647550111358576
21 single_satellite_outliers raw_robust_wls_eskf 20 0.19054941715698687 0.03971061631217381 0.2889429468862187 0.19658211712738688 0.1789788721793858 0.4181969381744313 0.7254268588122093 0.27034697594947305 100.0 0.0 100.0 2.8953229398663693 1.2641999517115465 1.0
22 clock_drift_steps fixed_iir_wls_eskf 20 0.4160519465671664 0.04357555329949111 0.7083141902703948 0.4019739347287993 0.4380205569605854 0.44022233118676146 0.9391237648850096 1.8177258394243663 100.0 0.0 99.71411764705883 0.0 1.0000030764799477 0.05000000000000001
23 clock_drift_steps raw_wls_eskf 20 0.19046432375253802 0.04000978164437644 0.28906917942032334 0.19648305496852497 0.17891457284322693 0.41641647298083695 0.7208892008126868 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
24 clock_drift_steps adaptive_iir_wls_eskf 20 0.19421091770965143 0.0416170692634497 0.29132380269935765 0.19649429208827301 0.18911500227901648 0.1349904626304289 0.26347803176043993 1.818528619384018 100.0 0.0 100.0 0.0 1.0000030764799477 0.11647550111358576
25 clock_drift_steps raw_robust_wls_eskf 20 0.19046432375253802 0.04000978164437644 0.28906917942032334 0.19648305496852497 0.17891457284322693 0.41641647298083695 0.7208892008126868 0.268944777665747 100.0 0.0 100.0 0.0 1.2633800541343085 1.0
26 gps_gap_recovery fixed_iir_wls_eskf 20 0.3971781427894736 0.044796626065109466 0.7104470646835127 0.3873196668983798 0.41253405912166263 0.43094056066637465 0.9463346669492851 0.04458606585950929 100.0 0.0 99.90529411764706 0.0 1.0000036934211136 0.05
27 gps_gap_recovery raw_wls_eskf 20 0.2082177514862905 0.03682283889189333 0.32879462613500177 0.21668463381045563 0.1921901394048915 0.41707211010710354 0.7203717161799947 0.26913433965877315 100.0 0.0 100.0 0.0 1.2631217416869522 1.0
28 gps_gap_recovery adaptive_iir_wls_eskf 20 0.20776527476484566 0.044556420313135676 0.3257106582800945 0.21357805324970008 0.19661285828638408 0.12935782738837362 0.2613749696102362 0.06764578524374312 100.0 0.0 100.0 0.0 1.0000036934211136 0.10814839572192514
29 gps_gap_recovery raw_robust_wls_eskf 20 0.2082177514862905 0.03682283889189333 0.32879462613500177 0.21668463381045563 0.1921901394048915 0.41707211010710354 0.7203717161799947 0.26913433965877315 100.0 0.0 100.0 0.0 1.2631217416869522 1.0
30 mixed_flight_faults fixed_iir_wls_eskf 20 0.9404176641021843 0.04319034103529422 1.7661662430364344 0.9555417869207146 0.9132850762857121 1.5154618461064036 2.663221132275041 0.7591896512045151 100.0 0.0 60.37294117647059 0.0 1.3397749809447188 0.05000000000000001
31 mixed_flight_faults raw_wls_eskf 20 0.22399771088975795 0.05069473595820174 0.3492819306443341 0.23151007332767243 0.2092729705094658 1.498455933396739 2.36689254151483 0.7603191788358898 100.0 0.0 100.0 0.0 2.1864248911994575 1.0
32 mixed_flight_faults adaptive_iir_wls_eskf 20 0.2952659489493427 0.07200078881833771 0.4874378367724045 0.30541797568187434 0.2754557634719251 0.838415542523151 0.9361678311586857 0.47500979673114047 100.0 0.0 99.92470588235294 0.0 1.250042525522558 0.178938679245283
33 mixed_flight_faults raw_robust_wls_eskf 20 0.2208132635451996 0.04372541489397352 0.3416439647301807 0.22864253849532457 0.20560052604298312 1.0129266912203836 2.2537697549366906 0.673490930924677 100.0 0.0 100.0 1.9103773584905661 1.5692458289027678 1.0
@@ -0,0 +1,51 @@
scenario,method,epochs,horizontal_rms_m,position_3d_rms_m,position_3d_p95_m,valid_percent,valid_3d_rms_m,horizontal_2sigma_coverage_percent,false_valid_over_25m_percent,fde_percent,mean_measurement_sigma_m
clean_open_sky,current_cn0,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
clean_open_sky,naive_add_dll,4500,5.768593681325036,10.022431815549082,17.253420427433362,100.0,10.022431815549082,100.0,0.3111111111111111,0.0,10.452052333958017
clean_open_sky,dll_excess,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
clean_open_sky,dll_excess_guard,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
clean_open_sky,oracle_variance,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
weak_signal,current_cn0,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
weak_signal,naive_add_dll,4500,18.802930957724488,32.313115706990146,55.45511304556428,100.0,32.313115706990146,100.0,56.177777777777784,0.0,28.674069502747358
weak_signal,dll_excess,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
weak_signal,dll_excess_guard,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
weak_signal,oracle_variance,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
one_channel_hidden_jitter,current_cn0,4500,8.804153682379612,12.268224803643907,19.936371518380813,100.0,12.268224803643907,96.46666666666667,1.0666666666666667,9.666666666666666,8.840973580918838
one_channel_hidden_jitter,naive_add_dll,4500,6.529225405554112,10.487953223937716,17.74604536639535,100.0,10.487953223937716,99.97777777777777,0.33333333333333337,0.0,11.805515833602682
one_channel_hidden_jitter,dll_excess,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
one_channel_hidden_jitter,dll_excess_guard,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
one_channel_hidden_jitter,oracle_variance,4500,6.4491935598492525,10.463101509886709,17.736218582311878,100.0,10.463101509886709,99.97777777777777,0.33333333333333337,0.06666666666666667,10.315229331071063
common_hidden_jitter,current_cn0,4500,19.733300930595462,33.79270185129192,59.76307034516212,94.75555555555556,33.15386252421736,56.39999999999999,52.400000000000006,46.577777777777776,8.840973580918838
common_hidden_jitter,naive_add_dll,4500,16.033969673897577,27.61407554068951,47.55915451805768,100.0,27.61407554068951,99.46666666666667,43.93333333333334,0.26666666666666666,18.882004393424943
common_hidden_jitter,dll_excess,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
common_hidden_jitter,dll_excess_guard,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
common_hidden_jitter,oracle_variance,4500,16.106964558267112,27.740128015702773,47.83188793884836,100.0,27.740128015702773,98.88888888888889,43.86666666666667,0.4888888888888889,18.035214487051437
noise_burst_recovery,current_cn0,4500,9.325915933961204,12.96069950118485,22.132869786417263,98.93333333333332,12.552632065384056,94.71111111111111,2.577777777777778,12.11111111111111,8.840973580918838
noise_burst_recovery,naive_add_dll,4500,6.470839383968352,10.523606281690133,18.031622500344657,100.0,10.523606281690133,100.0,0.5333333333333333,0.06666666666666667,11.60221083527078
noise_burst_recovery,dll_excess,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
noise_burst_recovery,dll_excess_guard,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
noise_burst_recovery,oracle_variance,4500,6.367173301653832,10.473649717876942,17.90212603773505,100.0,10.473649717876942,99.97777777777777,0.4888888888888889,0.044444444444444446,10.187907003764948
dll_transient_stress,current_cn0,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
dll_transient_stress,naive_add_dll,4500,5.793995520088368,10.118871049708806,17.333372587049784,100.0,10.118871049708806,100.0,0.37777777777777777,0.0,10.45205253980181
dll_transient_stress,dll_excess,4500,5.8034283726190745,10.144627133073621,17.380953993070463,100.0,10.144627133073621,99.97777777777777,0.37777777777777777,0.0,8.908174353046816
dll_transient_stress,dll_excess_guard,4500,5.79072382051268,10.123295335548953,17.368844982312577,100.0,10.123295335548953,99.97777777777777,0.33333333333333337,0.0,8.947656011912292
dll_transient_stress,oracle_variance,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
stable_multipath_bias,current_cn0,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
stable_multipath_bias,naive_add_dll,4500,7.072680893174009,12.386663925234807,20.866871605887997,100.0,12.386663925234807,99.97777777777777,1.4666666666666666,0.0,10.452052333958017
stable_multipath_bias,dll_excess,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
stable_multipath_bias,dll_excess_guard,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
stable_multipath_bias,oracle_variance,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
mixed_multipath,current_cn0,4500,8.529840854093363,14.426936007413373,25.381172069686816,100.0,14.426936007413373,97.11111111111111,5.644444444444444,5.0,8.840973580918838
mixed_multipath,naive_add_dll,4500,6.586489015624914,11.258484405711224,19.599950438767102,100.0,11.258484405711224,100.0,0.7333333333333333,0.044444444444444446,11.636563744509052
mixed_multipath,dll_excess,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
mixed_multipath,dll_excess_guard,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
mixed_multipath,oracle_variance,4500,6.448333589783235,11.058021985103961,19.19754215965981,100.0,11.058021985103961,99.95555555555555,0.7333333333333333,0.08888888888888889,10.144307488606447
poor_geometry_hidden_jitter,current_cn0,4500,79.38795978543642,293.8222926191477,574.9617330272,100.0,293.8222926191477,79.0,94.31111111111112,1.1555555555555554,10.734703317378012
poor_geometry_hidden_jitter,naive_add_dll,4500,57.59864532145799,206.13456070122297,405.1527013193,100.0,206.13456070122297,99.66666666666667,92.42222222222222,0.022222222222222223,14.604104871190987
poor_geometry_hidden_jitter,dll_excess,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
poor_geometry_hidden_jitter,dll_excess_guard,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
poor_geometry_hidden_jitter,oracle_variance,4500,55.24662489772763,196.2046647890204,386.5771391089706,100.0,196.2046647890204,99.28888888888889,91.26666666666667,0.022222222222222223,12.721163297137666
cn0_step_recovery,current_cn0,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
cn0_step_recovery,naive_add_dll,4500,12.797089787478507,22.265941453345402,47.59129168788298,100.0,22.265941453345402,100.0,21.422222222222224,0.0,17.44366868773946
cn0_step_recovery,dll_excess,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
cn0_step_recovery,dll_excess_guard,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
cn0_step_recovery,oracle_variance,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
1 scenario method epochs horizontal_rms_m position_3d_rms_m position_3d_p95_m valid_percent valid_3d_rms_m horizontal_2sigma_coverage_percent false_valid_over_25m_percent fde_percent mean_measurement_sigma_m
2 clean_open_sky current_cn0 4500 5.778805688412197 10.054230284262685 17.35250490247279 100.0 10.054230284262685 99.97777777777777 0.3111111111111111 0.0 8.840973580918838
3 clean_open_sky naive_add_dll 4500 5.768593681325036 10.022431815549082 17.253420427433362 100.0 10.022431815549082 100.0 0.3111111111111111 0.0 10.452052333958017
4 clean_open_sky dll_excess 4500 5.7784464702133524 10.05299246387535 17.320782018324902 100.0 10.05299246387535 99.97777777777777 0.33333333333333337 0.0 8.908174172560404
5 clean_open_sky dll_excess_guard 4500 5.7784464702133524 10.05299246387535 17.320782018324902 100.0 10.05299246387535 99.97777777777777 0.33333333333333337 0.0 8.908174172560404
6 clean_open_sky oracle_variance 4500 5.778805688412197 10.054230284262685 17.35250490247279 100.0 10.054230284262685 99.97777777777777 0.3111111111111111 0.0 8.840973580918838
7 weak_signal current_cn0 4500 19.020289863331797 32.73415409569256 56.44388754013162 100.0 32.73415409569256 98.8 56.31111111111111 0.6222222222222222 20.84621216112656
8 weak_signal naive_add_dll 4500 18.802930957724488 32.313115706990146 55.45511304556428 100.0 32.313115706990146 100.0 56.177777777777784 0.0 28.674069502747358
9 weak_signal dll_excess 4500 18.986226727568837 32.6486650471064 56.115244341908046 100.0 32.6486650471064 98.91111111111111 56.222222222222214 0.5111111111111112 21.20058629982459
10 weak_signal dll_excess_guard 4500 18.986226727568837 32.6486650471064 56.115244341908046 100.0 32.6486650471064 98.91111111111111 56.222222222222214 0.5111111111111112 21.20058629982459
11 weak_signal oracle_variance 4500 19.020289863331797 32.73415409569256 56.44388754013162 100.0 32.73415409569256 98.8 56.31111111111111 0.6222222222222222 20.84621216112656
12 one_channel_hidden_jitter current_cn0 4500 8.804153682379612 12.268224803643907 19.936371518380813 100.0 12.268224803643907 96.46666666666667 1.0666666666666667 9.666666666666666 8.840973580918838
13 one_channel_hidden_jitter naive_add_dll 4500 6.529225405554112 10.487953223937716 17.74604536639535 100.0 10.487953223937716 99.97777777777777 0.33333333333333337 0.0 11.805515833602682
14 one_channel_hidden_jitter dll_excess 4500 6.455903980559815 10.46584556632944 17.675128387138177 100.0 10.46584556632944 99.97777777777777 0.33333333333333337 0.044444444444444446 10.358668146326426
15 one_channel_hidden_jitter dll_excess_guard 4500 6.455903980559815 10.46584556632944 17.675128387138177 100.0 10.46584556632944 99.97777777777777 0.33333333333333337 0.044444444444444446 10.358668146326426
16 one_channel_hidden_jitter oracle_variance 4500 6.4491935598492525 10.463101509886709 17.736218582311878 100.0 10.463101509886709 99.97777777777777 0.33333333333333337 0.06666666666666667 10.315229331071063
17 common_hidden_jitter current_cn0 4500 19.733300930595462 33.79270185129192 59.76307034516212 94.75555555555556 33.15386252421736 56.39999999999999 52.400000000000006 46.577777777777776 8.840973580918838
18 common_hidden_jitter naive_add_dll 4500 16.033969673897577 27.61407554068951 47.55915451805768 100.0 27.61407554068951 99.46666666666667 43.93333333333334 0.26666666666666666 18.882004393424943
19 common_hidden_jitter dll_excess 4500 16.085615037536463 27.688964211504615 47.828684727805175 100.0 27.688964211504615 98.97777777777777 43.888888888888886 0.4444444444444444 18.04555007574287
20 common_hidden_jitter dll_excess_guard 4500 16.085615037536463 27.688964211504615 47.828684727805175 100.0 27.688964211504615 98.97777777777777 43.888888888888886 0.4444444444444444 18.04555007574287
21 common_hidden_jitter oracle_variance 4500 16.106964558267112 27.740128015702773 47.83188793884836 100.0 27.740128015702773 98.88888888888889 43.86666666666667 0.4888888888888889 18.035214487051437
22 noise_burst_recovery current_cn0 4500 9.325915933961204 12.96069950118485 22.132869786417263 98.93333333333332 12.552632065384056 94.71111111111111 2.577777777777778 12.11111111111111 8.840973580918838
23 noise_burst_recovery naive_add_dll 4500 6.470839383968352 10.523606281690133 18.031622500344657 100.0 10.523606281690133 100.0 0.5333333333333333 0.06666666666666667 11.60221083527078
24 noise_burst_recovery dll_excess 4500 6.397124977878571 10.493909835494788 17.99630946143617 100.0 10.493909835494788 99.97777777777777 0.4888888888888889 0.13333333333333333 10.131003114059961
25 noise_burst_recovery dll_excess_guard 4500 6.397124977878571 10.493909835494788 17.99630946143617 100.0 10.493909835494788 99.97777777777777 0.4888888888888889 0.13333333333333333 10.131003114059961
26 noise_burst_recovery oracle_variance 4500 6.367173301653832 10.473649717876942 17.90212603773505 100.0 10.473649717876942 99.97777777777777 0.4888888888888889 0.044444444444444446 10.187907003764948
27 dll_transient_stress current_cn0 4500 5.803776392869777 10.145785618162538 17.425227799952054 100.0 10.145785618162538 99.97777777777777 0.35555555555555557 0.0 8.840973580918838
28 dll_transient_stress naive_add_dll 4500 5.793995520088368 10.118871049708806 17.333372587049784 100.0 10.118871049708806 100.0 0.37777777777777777 0.0 10.45205253980181
29 dll_transient_stress dll_excess 4500 5.8034283726190745 10.144627133073621 17.380953993070463 100.0 10.144627133073621 99.97777777777777 0.37777777777777777 0.0 8.908174353046816
30 dll_transient_stress dll_excess_guard 4500 5.79072382051268 10.123295335548953 17.368844982312577 100.0 10.123295335548953 99.97777777777777 0.33333333333333337 0.0 8.947656011912292
31 dll_transient_stress oracle_variance 4500 5.803776392869777 10.145785618162538 17.425227799952054 100.0 10.145785618162538 99.97777777777777 0.35555555555555557 0.0 8.840973580918838
32 stable_multipath_bias current_cn0 4500 7.12694286036133 12.545413964352555 20.99675149186582 100.0 12.545413964352555 99.8888888888889 1.6666666666666667 0.022222222222222223 8.840973580918838
33 stable_multipath_bias naive_add_dll 4500 7.072680893174009 12.386663925234807 20.866871605887997 100.0 12.386663925234807 99.97777777777777 1.4666666666666666 0.0 10.452052333958017
34 stable_multipath_bias dll_excess 4500 7.125500558670432 12.536689588412326 21.02122766562977 100.0 12.536689588412326 99.91111111111111 1.6444444444444446 0.022222222222222223 8.908174172560404
35 stable_multipath_bias dll_excess_guard 4500 7.125500558670432 12.536689588412326 21.02122766562977 100.0 12.536689588412326 99.91111111111111 1.6444444444444446 0.022222222222222223 8.908174172560404
36 stable_multipath_bias oracle_variance 4500 7.12694286036133 12.545413964352555 20.99675149186582 100.0 12.545413964352555 99.8888888888889 1.6666666666666667 0.022222222222222223 8.840973580918838
37 mixed_multipath current_cn0 4500 8.529840854093363 14.426936007413373 25.381172069686816 100.0 14.426936007413373 97.11111111111111 5.644444444444444 5.0 8.840973580918838
38 mixed_multipath naive_add_dll 4500 6.586489015624914 11.258484405711224 19.599950438767102 100.0 11.258484405711224 100.0 0.7333333333333333 0.044444444444444446 11.636563744509052
39 mixed_multipath dll_excess 4500 6.45479117311332 11.067801578489938 19.233225446062768 100.0 11.067801578489938 100.0 0.7333333333333333 0.08888888888888889 10.202310406784667
40 mixed_multipath dll_excess_guard 4500 6.45479117311332 11.067801578489938 19.233225446062768 100.0 11.067801578489938 100.0 0.7333333333333333 0.08888888888888889 10.202310406784667
41 mixed_multipath oracle_variance 4500 6.448333589783235 11.058021985103961 19.19754215965981 100.0 11.058021985103961 99.95555555555555 0.7333333333333333 0.08888888888888889 10.144307488606447
42 poor_geometry_hidden_jitter current_cn0 4500 79.38795978543642 293.8222926191477 574.9617330272 100.0 293.8222926191477 79.0 94.31111111111112 1.1555555555555554 10.734703317378012
43 poor_geometry_hidden_jitter naive_add_dll 4500 57.59864532145799 206.13456070122297 405.1527013193 100.0 206.13456070122297 99.66666666666667 92.42222222222222 0.022222222222222223 14.604104871190987
44 poor_geometry_hidden_jitter dll_excess 4500 55.3927819274298 196.86935630824075 390.63687676393124 100.0 196.86935630824075 99.24444444444444 91.4 0.022222222222222223 12.779670971493758
45 poor_geometry_hidden_jitter dll_excess_guard 4500 55.3927819274298 196.86935630824075 390.63687676393124 100.0 196.86935630824075 99.24444444444444 91.4 0.022222222222222223 12.779670971493758
46 poor_geometry_hidden_jitter oracle_variance 4500 55.24662489772763 196.2046647890204 386.5771391089706 100.0 196.2046647890204 99.28888888888889 91.26666666666667 0.022222222222222223 12.721163297137666
47 cn0_step_recovery current_cn0 4500 12.974359636882712 22.585010269106 47.95054105970103 100.0 22.585010269106 99.42222222222222 21.622222222222224 0.2222222222222222 13.475047639913695
48 cn0_step_recovery naive_add_dll 4500 12.797089787478507 22.265941453345402 47.59129168788298 100.0 22.265941453345402 100.0 21.422222222222224 0.0 17.44366868773946
49 cn0_step_recovery dll_excess 4500 12.96657717120691 22.586113735655264 47.91159461508304 100.0 22.586113735655264 99.46666666666667 21.488888888888887 0.2 13.653801199442478
50 cn0_step_recovery dll_excess_guard 4500 12.96657717120691 22.586113735655264 47.91159461508304 100.0 22.586113735655264 99.46666666666667 21.488888888888887 0.2 13.653801199442478
51 cn0_step_recovery oracle_variance 4500 12.974359636882712 22.585010269106 47.95054105970103 100.0 22.585010269106 99.42222222222222 21.622222222222224 0.2222222222222222 13.475047639913695
@@ -0,0 +1,2 @@
numpy>=1.24
matplotlib>=3.7
@@ -0,0 +1,200 @@
"""Small executable model of the RTL code-tracking observables.
This is deliberately scoped to the parts that matter to the navigation
measurement study:
* the normalized early-minus-late magnitude discriminator;
* its signed Q2.13 output;
* the fixed C1/C2 code-loop filter and Q20 NCO command;
* the 20 one-millisecond discriminator samples available per NAV bit.
It is not an RF waveform simulator. The C/N0-to-discriminator-noise mapping
is an explicit calibration assumption until real correlator/DLL telemetry is
recorded from the board.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
DLL_OUTPUT_FRACTION_BITS = 13
DLL_OUTPUT_WORD_BITS = 15
DLL_SPACING_CHIPS = 0.5
CODE_LOOP_C1 = 21.25
CODE_LOOP_C2 = 0.05712890625
CODE_LOOP_OUTPUT_FRACTION_BITS = 20
CODE_LOOP_OUTPUT_WORD_BITS = 28
DLL_SAMPLES_PER_NAV_BIT = 20
CODE_FREQUENCY_BASIS_HZ = 1.023e6
ABS_SAMPLE_COUNTER_MODULUS = 1 << 32
# Synthetic calibration point. It only fixes the scale of the Monte Carlo
# discriminator telemetry. Dynamic weighting uses a measured/expected ratio,
# so the absolute choice cancels in the correctly modelled case.
DLL_STD_AT_45_DBHZ = 0.020
def _signed_saturate(value: int, word_bits: int) -> int:
minimum = -(1 << (word_bits - 1))
maximum = (1 << (word_bits - 1)) - 1
return min(maximum, max(minimum, value))
def quantize_signed(value: float, word_bits: int, fraction_bits: int) -> int:
scaled = int(np.rint(value * (1 << fraction_bits)))
return _signed_saturate(scaled, word_bits)
def dequantize_signed(value: int, fraction_bits: int) -> float:
return float(value) / float(1 << fraction_bits)
def triangular_correlation(code_offset_chips: float) -> float:
"""Ideal one-chip triangular GPS C/A autocorrelation main lobe."""
return max(0.0, 1.0 - abs(code_offset_chips))
def ideal_early_late_magnitudes(
code_error_chips: float,
spacing_chips: float = DLL_SPACING_CHIPS,
) -> tuple[float, float]:
half_spacing = 0.5 * spacing_chips
early = triangular_correlation(code_error_chips + half_spacing)
late = triangular_correlation(code_error_chips - half_spacing)
return early, late
def normalized_early_minus_late(early_magnitude: float, late_magnitude: float) -> float:
denominator = early_magnitude + late_magnitude
if denominator <= 0.0:
return 0.0
return (early_magnitude - late_magnitude) / denominator
def rtl_discriminator(code_error_chips: float) -> tuple[int, float]:
"""Return the RTL-like Q2.13 discriminator code and dequantized value."""
early, late = ideal_early_late_magnitudes(code_error_chips)
value = normalized_early_minus_late(early, late)
code = quantize_signed(
value,
DLL_OUTPUT_WORD_BITS,
DLL_OUTPUT_FRACTION_BITS,
)
return code, dequantize_signed(code, DLL_OUTPUT_FRACTION_BITS)
def expected_discriminator_std(cn0_dbhz: float) -> float:
"""Expected normalized DLL jitter used by the synthetic calibration.
The 20 dB amplitude-domain law mirrors the firmware's code-sigma model.
The result is bounded because the normalized discriminator is finite.
"""
value = DLL_STD_AT_45_DBHZ * 10.0 ** ((45.0 - cn0_dbhz) / 20.0)
return float(np.clip(value, 0.002, 0.35))
def quantized_code_epoch_measurement(
true_abs_sample: float,
) -> tuple[int, int, float]:
"""Model ABS_SAMPLE + REM_CODE_PHASE and firmware reconstruction.
``true_abs_sample`` is the ideal code boundary in RF-sample units. The
hardware captures the following integer sample and reports the overshoot
with the remainder scale used by the C correction formula.
"""
raw_unwrapped = int(np.ceil(true_abs_sample))
overshoot_samples = raw_unwrapped - true_abs_sample
remainder = int(np.rint(overshoot_samples * CODE_FREQUENCY_BASIS_HZ))
raw_register = raw_unwrapped % ABS_SAMPLE_COUNTER_MODULUS
corrected = raw_unwrapped - remainder / CODE_FREQUENCY_BASIS_HZ
return raw_register, remainder, corrected
@dataclass(frozen=True)
class DllWindow:
samples: np.ndarray
mean: float
variance: float
expected_variance: float
variance_ratio: float
mean_z_score: float
def synthesize_dll_window(
rng: np.random.Generator,
*,
cn0_dbhz: float,
jitter_multiplier: float = 1.0,
discriminator_bias: float = 0.0,
sample_count: int = DLL_SAMPLES_PER_NAV_BIT,
) -> DllWindow:
"""Generate and Q2.13-quantize one NAV-bit DLL telemetry window."""
if sample_count < 2:
raise ValueError("DLL variance needs at least two samples")
expected_std = expected_discriminator_std(cn0_dbhz)
actual_std = expected_std * max(0.0, jitter_multiplier)
raw = discriminator_bias + rng.normal(0.0, actual_std, sample_count)
minimum = -(1 << (DLL_OUTPUT_WORD_BITS - 1))
maximum = (1 << (DLL_OUTPUT_WORD_BITS - 1)) - 1
quantized_codes = np.clip(
np.rint(raw * (1 << DLL_OUTPUT_FRACTION_BITS)),
minimum,
maximum,
).astype(np.int32)
samples = quantized_codes.astype(float) / float(1 << DLL_OUTPUT_FRACTION_BITS)
mean = float(np.mean(samples))
variance = float(np.var(samples, ddof=1))
expected_variance = expected_std * expected_std
ratio = variance / max(expected_variance, 1.0e-15)
standard_error = np.sqrt(max(variance, expected_variance) / sample_count)
mean_z = abs(mean) / max(float(standard_error), 1.0e-12)
return DllWindow(
samples=samples,
mean=mean,
variance=variance,
expected_variance=expected_variance,
variance_ratio=float(ratio),
mean_z_score=float(mean_z),
)
@dataclass
class CodeLoopFilterRtl:
"""Update-rate model of LF_code.sv, including coefficient quantization."""
integrator_q20: int = 0
c1_integer: int = int(CODE_LOOP_C1 * (1 << 2))
c2_integer: int = int(CODE_LOOP_C2 * (1 << 11))
def update_code(self, discriminator_q13: int) -> int:
# C1 product has Q15 and is zero-padded to Q20 in LF_code.sv.
proportional_q20 = (self.c1_integer * discriminator_q13) << 5
# C2 product has Q24 and LF_code.sv slices it back to Q20.
integral_increment_q20 = (self.c2_integer * discriminator_q13) >> 4
self.integrator_q20 = _signed_saturate(
self.integrator_q20 + integral_increment_q20,
CODE_LOOP_OUTPUT_WORD_BITS,
)
return _signed_saturate(
self.integrator_q20 + proportional_q20,
CODE_LOOP_OUTPUT_WORD_BITS,
)
def update(self, discriminator: float) -> float:
discriminator_q13 = quantize_signed(
discriminator,
DLL_OUTPUT_WORD_BITS,
DLL_OUTPUT_FRACTION_BITS,
)
command_q20 = self.update_code(discriminator_q13)
return dequantize_signed(command_q20, CODE_LOOP_OUTPUT_FRACTION_BITS)
@@ -0,0 +1,212 @@
"""Regression tests for the project-specific Doppler/WLS/ESKF model."""
from __future__ import annotations
from dataclasses import replace
import unittest
import numpy as np
from .doppler_eskf_model import (
AdaptiveAlpha,
DopplerPath,
FIXED_DOPPLER_ALPHA,
flight_scenarios,
local_los_matrix,
run_doppler_eskf_scenario,
velocity_wls,
)
def _scenario(name: str):
return next(item for item in flight_scenarios() if item.name == name)
def _fused_rms(trace) -> float:
selected = trace.time_s >= 5.0
error = trace.fused_velocity_mps[selected] - trace.truth_velocity_mps[selected]
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
def _wls_rms(trace) -> float:
truth = np.column_stack(
[
np.interp(
trace.wls_time_s,
trace.time_s,
trace.truth_velocity_mps[:, axis],
)
for axis in range(3)
]
)
error = trace.wls_velocity_mps - truth
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
def _board_raw_noise_scenario(name: str):
"""Match the 9.17 m/s raw-WLS RMS measured on Genesys2."""
scenario = _scenario(name)
nominal_noise = scenario.noise_multiplier
return replace(
scenario,
name=f"{scenario.name}_genesys2_noise",
noise_multiplier=lambda time_s, satellite: (
22.0 * nominal_noise(time_s, satellite)
),
)
class DopplerWlsTest(unittest.TestCase):
def test_four_state_wls_separates_velocity_and_clock_drift(self) -> None:
velocity = np.asarray([145.0, -32.0, 7.5])
clock_drift = 18.0
observation = -local_los_matrix() @ velocity + clock_drift
result = velocity_wls(observation, np.full(8, 45.0))
np.testing.assert_allclose(result.velocity_mps, velocity, atol=1.0e-10)
self.assertAlmostEqual(result.clock_drift_mps, clock_drift, places=10)
self.assertAlmostEqual(result.covariance_scale, 1.0)
def test_robust_wls_excludes_a_single_bad_channel(self) -> None:
velocity = np.asarray([120.0, 20.0, -2.0])
observation = -local_los_matrix() @ velocity + 4.0
observation[2] += 12.0
plain = velocity_wls(observation, np.full(8, 43.0))
robust = velocity_wls(observation, np.full(8, 43.0), robust=True)
self.assertEqual(robust.excluded_satellite, 2)
self.assertGreater(np.linalg.norm(plain.velocity_mps - velocity), 1.0)
np.testing.assert_allclose(robust.velocity_mps, velocity, atol=1.0e-9)
class AdaptiveAlphaTest(unittest.TestCase):
def test_motion_switches_alpha_and_quiet_state_recovers(self) -> None:
selector = AdaptiveAlpha()
for _ in range(80):
quiet = selector.update(np.zeros(3), 35.0)
self.assertAlmostEqual(quiet, 0.03)
dynamic_values = [
selector.update(np.asarray([4.0, 0.0, 0.0]), 35.0)
for _ in range(30)
]
self.assertGreaterEqual(max(dynamic_values), 0.40)
for _ in range(120):
recovered = selector.update(np.zeros(3), 40.0)
self.assertAlmostEqual(recovered, FIXED_DOPPLER_ALPHA)
class DopplerEskfIntegrationTest(unittest.TestCase):
def test_board_raw_noise_requires_adaptive_prefilter(self) -> None:
steady = run_doppler_eskf_scenario(
_board_raw_noise_scenario("steady_cruise"),
seed=7,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
airliner = run_doppler_eskf_scenario(
_board_raw_noise_scenario("airliner_maneuver"),
seed=3,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
aggressive = run_doppler_eskf_scenario(
_board_raw_noise_scenario("aggressive_maneuver"),
seed=7,
paths=(
DopplerPath.FIXED_IIR,
DopplerPath.ADAPTIVE_IIR,
DopplerPath.RAW,
),
)
steady_fixed = _fused_rms(steady[DopplerPath.FIXED_IIR])
steady_adaptive = _fused_rms(steady[DopplerPath.ADAPTIVE_IIR])
steady_raw = _fused_rms(steady[DopplerPath.RAW])
airliner_fixed = _fused_rms(airliner[DopplerPath.FIXED_IIR])
airliner_adaptive = _fused_rms(airliner[DopplerPath.ADAPTIVE_IIR])
airliner_raw = _fused_rms(airliner[DopplerPath.RAW])
aggressive_fixed = _fused_rms(aggressive[DopplerPath.FIXED_IIR])
aggressive_adaptive = _fused_rms(
aggressive[DopplerPath.ADAPTIVE_IIR]
)
aggressive_raw = _fused_rms(aggressive[DopplerPath.RAW])
# Direct 20 ms snapshots are too noisy at the level actually observed
# on the board. Adaptive bandwidth retains the quiet-state result and
# beats both direct raw and the 0.39 s fixed-IIR lag in manoeuvres.
self.assertLess(steady_adaptive, 1.05 * steady_fixed)
self.assertLess(steady_adaptive, 0.50 * steady_raw)
self.assertLess(airliner_adaptive, airliner_fixed)
self.assertLess(airliner_adaptive, 0.50 * airliner_raw)
self.assertLess(aggressive_adaptive, 0.65 * aggressive_fixed)
self.assertLess(aggressive_adaptive, 0.50 * aggressive_raw)
def test_eskf_filters_raw_doppler_without_pre_iir(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("steady_cruise"),
seed=7,
paths=(DopplerPath.RAW,),
)
raw = traces[DopplerPath.RAW]
self.assertLess(_fused_rms(raw), 0.45 * _wls_rms(raw))
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
def test_raw_path_avoids_fixed_iir_manoeuvre_lag(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("airliner_maneuver"),
seed=3,
paths=(DopplerPath.FIXED_IIR, DopplerPath.RAW),
)
fixed = traces[DopplerPath.FIXED_IIR]
raw = traces[DopplerPath.RAW]
self.assertLess(_fused_rms(raw), 0.35 * _fused_rms(fixed))
# The production 2/2/3 m/s adapter floors keep both paths inside the
# NIS gate; the fixed-IIR penalty now appears as lag error, not reject.
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
self.assertGreater(float(np.mean(fixed.wls_accepted)), 0.99)
def test_common_clock_step_does_not_leak_into_raw_velocity(self) -> None:
airliner = run_doppler_eskf_scenario(
_scenario("airliner_maneuver"),
seed=11,
paths=(DopplerPath.RAW,),
)[DopplerPath.RAW]
clock_step = run_doppler_eskf_scenario(
_scenario("clock_drift_steps"),
seed=11,
paths=(DopplerPath.RAW,),
)[DopplerPath.RAW]
np.testing.assert_allclose(
airliner.wls_velocity_mps,
clock_step.wls_velocity_mps,
atol=1.0e-10,
)
np.testing.assert_allclose(
airliner.fused_velocity_mps,
clock_step.fused_velocity_mps,
atol=1.0e-10,
)
def test_robust_wls_contains_channel_bursts_before_eskf(self) -> None:
traces = run_doppler_eskf_scenario(
_scenario("single_satellite_outliers"),
seed=5,
paths=(DopplerPath.RAW, DopplerPath.RAW_ROBUST),
)
raw = traces[DopplerPath.RAW]
robust = traces[DopplerPath.RAW_ROBUST]
self.assertLess(_wls_rms(robust), 0.65 * _wls_rms(raw))
self.assertGreater(float(np.mean(robust.excluded_satellites >= 0)), 0.02)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,182 @@
from __future__ import annotations
import unittest
import numpy as np
from gps_measurement_python.code_smoothing_model import run_smoothing_trial
from gps_measurement_python.firmware_wls_model import (
SigmaInputs,
SigmaMethod,
firmware_sigma_m,
)
from gps_measurement_python.measurement_scenarios import (
run_scenario,
scenario_matrix,
)
from gps_measurement_python.rtl_dll_model import (
CodeLoopFilterRtl,
quantized_code_epoch_measurement,
rtl_discriminator,
synthesize_dll_window,
)
class RtlDllModelTests(unittest.TestCase):
def test_discriminator_zero_and_q13_symmetry(self):
self.assertEqual(rtl_discriminator(0.0)[0], 0)
positive = rtl_discriminator(0.05)[1]
negative = rtl_discriminator(-0.05)[1]
self.assertAlmostEqual(positive, -negative, places=6)
self.assertLess(positive, 0.0)
def test_code_loop_integrator_accumulates_constant_error(self):
loop = CodeLoopFilterRtl()
first = loop.update(0.01)
twentieth = first
for _ in range(19):
twentieth = loop.update(0.01)
self.assertGreater(first, 0.0)
self.assertGreater(twentieth, first)
def test_dll_window_detects_excess_variance(self):
rng = np.random.default_rng(42)
nominal = synthesize_dll_window(
rng,
cn0_dbhz=45.0,
jitter_multiplier=1.0,
sample_count=4000,
)
noisy = synthesize_dll_window(
rng,
cn0_dbhz=45.0,
jitter_multiplier=3.0,
sample_count=4000,
)
self.assertAlmostEqual(nominal.variance_ratio, 1.0, delta=0.10)
self.assertAlmostEqual(noisy.variance_ratio, 9.0, delta=0.70)
def test_abs_sample_remainder_reconstructs_subsample_epoch(self):
for true_sample in (
12_345_678.314159,
12_345_678.0000001,
12_345_678.9999999,
12_345_678.0,
):
raw, remainder, corrected = quantized_code_epoch_measurement(true_sample)
self.assertEqual(raw, int(np.ceil(true_sample)))
self.assertLessEqual(
abs(corrected - true_sample),
0.5 / 1.023e6 + 1.0e-12,
)
self.assertGreaterEqual(remainder, 0)
class FirmwareSigmaTests(unittest.TestCase):
@staticmethod
def inputs(ratio: float = 1.0, mean_z: float = 0.0) -> SigmaInputs:
return SigmaInputs(
ura_index=np.asarray([0]),
elevation_deg=np.asarray([90.0]),
cn0_dbhz=np.asarray([45.0]),
dll_variance_ratio=np.asarray([ratio]),
dll_mean_z=np.asarray([mean_z]),
actual_jitter_multiplier=np.asarray([np.sqrt(ratio)]),
)
def test_current_matches_c_formula(self):
sigma = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
expected = np.sqrt(2.4**2 + 3.0**2 + 3.0**2)
self.assertAlmostEqual(sigma, expected, places=12)
def test_excess_method_does_not_double_count_nominal_dll_noise(self):
current = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
excess = firmware_sigma_m(self.inputs(), SigmaMethod.DLL_EXCESS)[0]
naive = firmware_sigma_m(self.inputs(), SigmaMethod.NAIVE_ADD_DLL)[0]
self.assertAlmostEqual(excess, current, places=12)
self.assertGreater(naive, current)
def test_excess_method_inflates_hidden_jitter(self):
current = firmware_sigma_m(self.inputs(9.0), SigmaMethod.CURRENT)[0]
excess = firmware_sigma_m(self.inputs(9.0), SigmaMethod.DLL_EXCESS)[0]
self.assertGreater(excess, 1.8 * current)
class ScenarioTests(unittest.TestCase):
def test_hidden_jitter_dynamic_sigma_improves_seeded_case(self):
scenario = next(
item
for item in scenario_matrix()
if item.name == "one_channel_hidden_jitter"
)
samples = run_scenario(
scenario,
17,
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
)
current = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
]
)
dynamic = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
]
)
self.assertLess(
np.sqrt(np.mean(dynamic * dynamic)),
0.90 * np.sqrt(np.mean(current * current)),
)
def test_stable_multipath_is_not_solved_by_variance_only(self):
scenario = next(
item
for item in scenario_matrix()
if item.name == "stable_multipath_bias"
)
samples = run_scenario(
scenario,
23,
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
)
current = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
]
)
dynamic = np.asarray(
[
item.position_error_3d_m
for item in samples
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
]
)
relative_difference = abs(
np.sqrt(np.mean(dynamic * dynamic))
/ np.sqrt(np.mean(current * current))
- 1.0
)
self.assertLess(relative_difference, 0.08)
def test_doppler_prediction_avoids_blind_iir_dynamic_lag(self):
result = run_smoothing_trial(31, doppler_cfo_mps=0.0)
self.assertLess(result.doppler_smoothed_rmse_m, result.raw_rmse_m)
self.assertLess(
result.doppler_peak_dynamic_error_m,
0.10 * result.scalar_iir_peak_dynamic_error_m,
)
self.assertGreater(
result.firmware_iir_doppler_peak_dynamic_error_m,
result.doppler_peak_dynamic_error_m,
)
if __name__ == "__main__":
unittest.main()
+10 -2
View File
@@ -5,7 +5,7 @@ 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.
@@ -71,6 +71,8 @@ APP_SRCS := \
src/gps/gps_time_anchor.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 \
@@ -79,6 +81,9 @@ 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
@@ -90,7 +95,10 @@ CFLAGS += $(IMU_BACKEND_CFLAG) \
-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)
+485 -37
View File
@@ -1,6 +1,8 @@
# GPS System Port Notes
Bu notlar GPS bare-metal yaziliminin VCU118 Rocket SoC portu ve interrupt debug sureci icin yazildi.
Bu notlar GPS bare-metal yaziliminin VCU118/Genesys2 Rocket SoC portunu,
tracking Doppler/C/N0 yolunu, ESKF entegrasyonunu ve interrupt debug surecini
belgeler.
## Uygulama akisi ve dosya sinirlari
@@ -17,11 +19,20 @@ gps_system.c
-> initialize GPS, IMU and optional ESKF
-> wfi
-> timestamped IMU queue -> navigation_fusion prediction
-> GPS events -> coherent WLS -> gps_nav_solution_t
-> valid GPS -> fusion_gps_measurement_t -> navigation_fusion update
-> 20 ms tracking mailbox -> C/N0 + Doppler/range-rate
-> GPS events -> coherent position/velocity WLS -> gps_nav_solution_t
-> valid GPS position + Doppler velocity -> fusion_gps_measurement_t
-> same-epoch navigation_fusion position/velocity update
-> navigation_fusion_output_t -> corrected lat/lon publish
```
Doppler WLS'nin ECEF velocity, covariance, speed ve residual sonucu
`fusion_gps_measurement_t` icine aktarilir. GPS adapter velocity ile
covariance'i ECEF'ten ENU'ya dondurur; position ve velocity ayni capture
epoch'unda ESKF'ye uygulanir, sonra daha yeni IMU history replay edilir.
Position ve velocity'nin ayri NIS kapilari/sayaclari vardir; bir velocity
outlier'i ayni epoch'taki gecerli position update'ini geri almaz.
Kaynak ve public header agaci ayni modul sinirlarini izler:
```text
@@ -57,6 +68,34 @@ ve raw kuyruga koyar. GPS ISR'leri acquisition/tracking olaylarini kaydeder.
interrupt'lari gelebilecegi icin IMU kuyrugu WLS'den hem once hem sonra drain
edilir; GPS update'i WLS bitis zamaninda degil capture epoch'unda uygulanir.
### ESKF entegrasyonunda yapilanlar
- TDK ICM42688 ve ST LSM6DSOX suruculeri sensor-markasindan bagimsiz
`imu_sensor`/`imu_sample_t` sinirinin arkasina alindi. Her IMU ornegi olcum
aninda monotonik CLINT timestamp'i alir; ISR icinde ESKF matematigi
calismaz.
- Filtre ENU position/velocity, quaternion attitude, accel bias ve gyro bias
iceren 15 hata durumlu ESKF'dir. Prediction her ham IMU orneginde midpoint
mechanization ile calisir; telemetry bolucusu prediction oranini dusurmez.
- GPS WLS position ve Doppler velocity ayni capture timestamp'iyle tek
`fusion_navigation_measurement_t` icinde tasinir. Gecikmis GPS olcumu
history icindeki kendi epoch'una uygulanir; sonraki IMU ornekleri current
time'a kadar tekrar oynatilir.
- Position ve velocity update'lerinin ayri 3-DOF NIS kapilari ve
accept/reject sayaclari vardir. Velocity bozuksa ayni epoch'taki gecerli
position update'i geri alinmaz.
- Doppler WLS ECEF velocity covariance'i ENU'ya dondurulur. Kart verisinde
zayif geometrinin formal covariance'i asiri iyimser yapmamasi icin
horizontal `2.0 m/s`, vertical `3.0 m/s` minimum standard deviation floor'u
uygulanir; daha buyuk formal belirsizlik korunur.
- Durağanlik supervisor'u IMU/GNSS hareket kanitlarini histerezisle
birlestirir ve yalniz guvenli durumda ZUPT/zero-gyro update uygular.
GPS kesintisinde sahte position olcumu uretilmez; dead-reckoning devam eder
ve covariance buyur.
- Kotu ilk fix sonraki tutarli fix'leri surekli NIS reject'e kilitlerse,
timeout ve coklu-fix sureklilik kosuluyla yalniz position state/covariance
yeniden ankrajlanir. Velocity, attitude ve IMU bias state'leri korunur.
## Generic IMU Mimarisi (TDK ve ST)
GPS/ESKF artik dogrudan bir sensor surucusune bagli degildir. Veri yolu:
@@ -120,6 +159,41 @@ Bu durumda GPS acquisition/tracking/WLS ve ham lat-lon yayini calismaya devam
eder; IMU stream'i ve `navigation_fusion` baslatilmaz. UART startup satirinda
`eskf_enabled` ve gercek init sonucu olan `eskf_ready` ayri alanlardir.
Ana proje dizininden Genesys2 MCS uretmek icin:
```sh
# TDK + ESKF; GPS_ESKF_ENABLE verilmezse de varsayilan 1'dir.
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-mcs
# GPS-only: tracking, C/N0, Doppler velocity WLS ve PPS calisir.
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=0 fpga-mcs
```
Her iki profil bootloader ve uygulamayi varsayilan `460800` baud ile derler.
Iki komut ayni MCS/BIN yoluna yazdigi icin iki imaj birlikte saklanacaksa
build'ler arasinda profil adiyla kopyalanmalidir. Bu secenekler software
derleme parametresidir; mevcut bitstream ile `fpga-mcs` yeterlidir.
PPS Doppler datum-drift predictor production'da varsayilan olarak aciktir.
Kontrollu A/B icin build-time secenekleri:
```sh
# Production: predictor sürekli acik, otomatik A/B yok.
make -C soc/software/gps_system \
GPS_PPS_DOPPLER_PREDICTOR_ENABLE=1 \
GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES=0
# Tani: her 200 navigation update'inda (~40 s) kapali/acik degistir.
make -C soc/software/gps_system \
GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES=200
```
`GPS_PPS_DOPPLER_PREDICTOR_ENABLE=0` yalniz kontrollu fallback testi icindir.
`GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES` sifirdan buyukse A/B secimi enable
degerini bilerek ezer; production image'da mutlaka `0` kalmalidir.
Otomatik WHO_AM_I algilama varsayilandir:
```sh
@@ -437,32 +511,30 @@ Bu kosuda `mcause=5` tekrar etmedi.
## Build ve Debug Komutlari
Build:
Ana dizinden kalici flash imaji:
```sh
make -C /home/ankaoc/Desktop/rocket/rocket-chip/soc/software/gps_system \
BUILD_DIR=/home/ankaoc/Desktop/rocket/rocket-chip/soc/out/software/gps_system \
clean all
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-mcs
```
OpenOCD:
```sh
make -C /home/ankaoc/Desktop/rocket/rocket-chip fpga-debug-openocd
make FPGA_BOARD=genesys2 fpga-debug-openocd
```
ELF'i RAM'e yukleyip calistirma:
```sh
/tools/toolchains/rvtc-medany/bin/riscv64-unknown-elf-gdb \
-q /home/ankaoc/Desktop/rocket/rocket-chip/soc/out/software/gps_system/gps_system.elf \
-ex "target extended-remote :3333" \
-ex "monitor reset halt" \
-ex "load" \
-ex "set \$pc = _start" \
-ex "monitor resume" \
-ex "detach" \
-ex "quit"
make FPGA_BOARD=genesys2 APP=gps_system \
GPS_ESKF_ENABLE=1 IMU_SENSOR=tdk fpga-debug-load
```
UART:
```sh
make FPGA_BOARD=genesys2 fpga-uart
```
Durum okumak icin claim register'ini okumadan:
@@ -698,36 +770,193 @@ corrected_abs_sample = (double)ABS_SAMPLE - ((double)REM_CODE_PHASE_REG / CODE_F
`REM_CODE_PHASE_REG` pozitif crossing residue olarak yakalandigi icin isaret eksi kullanilir: code boundary, raw `ABS_SAMPLE` sample kenarindan `REM_CODE_PHASE_REG / CODE_FREQ_BASIS` sample kadar once gerceklesmistir. Raw `ABS_SAMPLE` yine `gps_last_nav_solve_abs_sample` icinde saklanir ve wrap/coherence delta hesaplari icin kullanilir; corrected double sadece pseudorange/navigation hesabina girer.
- Least-squares `A` matrisi satirlari geometric range ile normalize edilir. Pseudorange ile normalize etmek DOP ve pozisyon hesaplamasini bozabilir.
- WLS weight artik yalniz elevation'a bagli degildir. Subframe 1'den parse edilen URA/SVA index nominal metre degerine cevrilir ve her satellite icin asagidaki model kullanilir:
- WLS weight artik yalniz elevation'a bagli degildir. Subframe 1'den parse edilen URA/SVA index nominal metre degerine cevrilir; tracking mailbox'tan
hesaplanan C/N0 da code-tracking kalite terimine donusturulur:
```text
variance = ura_sigma_m^2 + (3.0 m / sin(max(elevation, 10 deg)))^2
cn0_sigma_m = clamp(3.0 * 10^((45 - cn0_dbhz) / 20), 0.5, 30.0)
variance = ura_sigma_m^2
+ (3.0 m / sin(max(elevation, 10 deg)))^2
+ cn0_sigma_m^2
weight = 1 / variance
```
Ilk receiver konumu henuz yoksa ilk iteration URA-only weight kullanir. Azimut/elevation olustuktan sonra URA ve elevation birlikte kullanilir. Relative weight'ler her iteration en buyuk weight `1.0` olacak sekilde normalize edilir. URA index 15 accuracy prediction saglamadigi veya `svhlth` alani sifir olmadigi icin uygun olmayan satellite navigation-ready snapshot'a alinmaz.
- Formuldeki `3.0 m`, GPS yayini veya ICD tarafindan verilen bir deger degildir. Mevcut donanim canli signal-quality olcumu saglamadigi icin secilmis, konservatif ve tune edilmesi gereken zenith residual sigma varsayimidir. Elevation 90 derecede `3 m`, 30 derecede `6 m`, 10 derece ve altinda yaklasik `17.3 m` sigma uretir.
- Tracking donanimindan ileride C/N0 veya SNR geldiginde ham register degeri dogrudan `3.0` yerine yazilmamalidir. Once gercek pseudorange/code tracking hatasina karsilik gelen metre cinsinden `sigma_cn0` degerine kalibre edilmelidir. Hedef measurement modeli asagidaki yapida olmalidir:
Ilk receiver konumu henuz yoksa elevation terimi kullanilmaz; gecerli C/N0
varsa URA + C/N0 weight yine uygulanir. Azimut/elevation olustuktan sonra uc
terim birlikte kullanilir. Relative weight'ler her iteration en buyuk weight
`1.0` olacak sekilde normalize edilir. URA index 15 accuracy prediction
saglamadigi veya `svhlth` alani sifir olmadigi icin uygun olmayan satellite
navigation-ready snapshot'a alinmaz.
Formuldeki iki `3.0 m` katsayisi GPS yayini veya ICD tarafindan verilen
degerler degildir; receiver'a ozel, kart olcumleri arttikca kalibre edilmesi
gereken baslangic modelleridir. Elevation terimi zenith'te `3 m`, 30
derecede `6 m`, 10 derece ve altinda yaklasik `17.3 m` sigma uretir.
C/N0 terimi 45 dB-Hz'de `3 m` sigma uretir ve `0.5..30 m` araliginda
sinirlanir. Multipath/elevation ile C/N0 ayni belirsizligi temsil ediyorsa
ileride cift sayimi onlemek icin kart verisiyle birlikte tune edilmelidir.
### 20 ms C/N0, Doppler ve velocity WLS
Her tracking kanali navigation bit epoch'unda 20 adet 1 ms prompt orneginden
`sum_i`, `sum_q` ve wideband power (`WBP`) snapshot'i verir. Software
narrowband/wideband power estimator'ini kullanir:
```text
variance = ura_sigma_m^2 + sigma_cn0_m^2 + sigma_residual_atmosphere_m^2
weight = 1 / variance
R = (sum_i^2 + sum_q^2) / WBP
C/N0_linear = 1000 * (R - 1) / (20 - R)
C/N0_dbhz = 10 * log10(C/N0_linear)
```
Kalibre edilmis C/N0 modeli dusuk-elevation signal degradation ve multipath etkisini yeterince temsil ederse mevcut `3 m / sin(elevation)` terimi kaldirilabilir. Bunlari tam temsil etmiyorsa ayni terim daha kucuk bir residual-atmosphere tabani olarak korunur; iki model ayni belirsizligi iki kez saymamalidir. EKF eklendiginde ayni toplam variance satellite measurement covariance `R` icinde kullanilmalidir.
- Konum cozumu URA/elevation agirlikli WLS kullanmaya devam eder; raporlanan DOP degerleri ise bu normalize relative weight'lerden uretilmez. GDOP/PDOP/TDOP agirliksiz geometri matrisinden, HDOP/VDOP de ayni covariance ECEF'ten lokal ENU eksenlerine donduruldukten sonra hesaplanir. Boylece loglanan degerler klasik geometrik DOP anlamini korur.
Gecersiz oranlar reddedilir; sonuc `10..60 dB-Hz` araliginda sinirlanir.
Bir-bit estimator jitter'i ayni PRN'in ard arda olcumlerinde software
IIR'i ile `alpha=0.2` kullanilarak yumusatilir.
RTL snapshot, synchronizer, APB register ve measurement-interrupt ayrintilari
`hardware/external/gps/rtl/trck_src/README.md` icinde belgelenir. Ozetle
payload tracking domaininde yaklasik 20 ms sabit tutulur; valid ve tum alanlar
PCLK'e registered synchronizer ile alinir, iki ek settle cycle'dan sonra
atomik APB bankina kopyalanir. Software sequence-before/payload/sequence-after
okumasiyla coherence'i tekrar dogrular.
Carrier register'i signed Q7 toplam NCO frekansidir. Doppler ve range-rate
donusumu:
```text
doppler_hz = carrier_q7 / 128 - 4092000
range_rate_mps = -doppler_hz * c / 1575420000
```
Ham 20 ms carrier snapshot'inda kartta gorulen birkac hertz jitter, L1'de
metre/saniye seviyesinde range-rate gurultusune ve zayif geometride daha buyuk
receiver velocity hatasina donusur. Software ISR her PRN icin ham degeri
degistirmeden korur ve iki ayri state gunceller:
```text
PPS: doppler_slow = 0.05 * raw + 0.95 * previous
ESKF: doppler_fast = alpha * raw + (1-alpha) * previous
alpha = 0.05 stationary, 0.20 moving, 0.40 high acceleration
```
PPS yolu 50 Hz cadence'de yaklasik `0.39 s` zaman sabitine sahiptir. ESKF yolu
fusion motion state ve gravity-compensated linear acceleration ile bant
genisligini acar. Alpha buyudugunde EWMA noise kazanci kadar WLS range-rate
sigma da buyutulur; bu, FDE ve ESKF covariance'in filtre bant genisliginden
habersiz kalmasini engeller. Reacquisition/PRN degisiminde eski kanal state'i
kullanilmaz.
IIR donanimda da uygulanabilirdi; bunun icin kanal basina state,
coefficient-multiply, rounding/saturation ve reacquisition reset kurali
gerekir. Mevcut sistemde gerekli degildir: RTL'nin gorevi ham, atomik ve
20 ms sabit olcumu sunmaktir; C/N0/WLS zaten software'dedir, ham ve filtreli
yollar bitstream almadan ayrilabilir. Board testinde CPU/interrupt yolu
measurement kaybetmedigi icin
RTL filtresinin karsilayacagi bir bant genisligi veya latency problemi de
gorulmemistir. Ileride tamamen RTL icinde bir tuketici eklenirse ham register
yaninda ayri bir filtered register mantikli olabilir.
Velocity WLS her uydu icin satellite velocity ve clock drift'i hesaba katar:
```text
observation = measured_range_rate
- satellite_LOS_rate
+ c * satellite_clock_drift
H = [-u_x, -u_y, -u_z, 1]
state = [receiver_vx, receiver_vy, receiver_vz, receiver_clock_drift_mps]
sigma_rate = clamp(30 * 10^(-C/N0_dbhz / 20), 0.05, 5.0)
weight = 1 / sigma_rate^2
```
Her 200 ms navigation epoch'unda iki velocity tuketicisi vardir:
```text
adaptive range-rate -> robust velocity WLS -> fusion adapter -> ESKF
slow range-rate -> robust velocity WLS -> PPS bias-drift predictor
```
Tam ham 20 ms snapshot yolu kart A/B'sinde fazla gurultulu bulundugu icin
ESKF'ye yayinlanmaz. Adaptif yol sakin durumda PPS kadar dusuk gurultulu,
harekette ise sabit `alpha=0.05` yolundan daha dusuk gecikmelidir.
PPS'in ana olcumu position WLS `clock_bias_m` sonucudur. Filtreli velocity
yalniz bu referans-datum bias'inin 200 ms epoch'lar arasindaki turevini
ongormek ve PPS kalite profilini siniflandirmak icin kullanilir. Doppler
WLS'nin dorduncu `receiver_clock_drift_mps` state'i PPS'e dogrudan verilmez.
Position WLS ile Doppler WLS iki ayri fiziksel observation kullanir:
code-phase pseudorange konum/ofseti, carrier Doppler ise hiz/frekans hatasini
olcer. Klasik mutlak-pseudorange modelinde ayni receiver zaman hatasi
`delta_t_r(t)` icin:
```text
code WLS: [x, y, z, b_r], b_r = c * delta_t_r [m]
Doppler WLS: [vx, vy, vz, bdot_r], bdot_r = c * d(delta_t_r)/dt [m/s]
```
Doppler WLS'nin dorduncu bilinmeyeni receiver clock/frequency drift'idir.
Receiver oscillator `0.1 ppm` hizliysa `bdot_r` yaklasik `29.98 m/s` olur.
Bu etki butun uydu Doppler'larina ortak girer; cozulmezse bir kismi
`vx/vy/vz` receiver hareketi sanilir.
Bu projedeki position WLS'e verilen capture zamanlarindan ortak receiver
zamani cikarilir ve `STARTOFFSET` eklenir. Dolayisiyla
`solution.clock_bias_m`, saf `c*delta_t_r` yerine yaklasik
`c*STARTOFFSET - reference_range` olan referans-uyduya bagli goreli bir
datumdur. Doppler `solution.clock_drift_mps` ile bunun arasinda turev kisiti
kurulmaz.
Fit'ler matematiksel olarak ayridir ve `solution_valid`/`velocity_valid`
bagimsizdir. Bununla birlikte velocity WLS LOS birim vektorlerini hesaplamak
icin position WLS'nin guncel receiver ECEF konumunu kullanir; uydu ECEF
konumlari, hizlari ve satellite clock drift'leri broadcast ephemeristen gelir.
Position kabul edilip velocity reddedilebilir; velocity NIS reject ayni
epoch'taki position update'ini geri almaz.
En az dort uydu gerekir. Dort uyduda velocity residual serbestlik derecesi
yoktur; bes uydu ve dusuk PDOP'ta bile zayif geometri velocity gurultusunu
buyutebilir. Alti veya daha fazla kullanilabilir uyduda full-fit maksimum
normalized residual `3.5 sigma`'yi asarsa butun tek-kanal leave-one-out
adaylari cozulur; en dusuk weighted post-fit residual enerjili aday secilir.
Boylece ESKF'nin aggregate 3D velocity NIS kapisina gelmeden once bozuk kanal
ayrilabilir. Dort/bes uyduda redundancy yetersiz oldugu icin velocity FDE
uygulanmaz.
WLS, `inverse(H'WH)` matrisinden ECEF velocity covariance uretir. `N>4` ise
weighted post-fit reduced chi-square yalniz covariance'i buyutebilir; tek bir
sessiz epoch formal belirsizligi kucultemez. Sonuc `velocity_valid`, ECEF
velocity/covariance, `speed_mps`, `velocity_residual_rms_mps`,
`velocity_fde_applied` ve `velocity_excluded_row_mask` alanlarinda yayinlanir.
Fusion adapter covariance'i `R_ecef_to_enu * P * R^T` ile ENU'ya dondurur.
2026-07-25 sabit kart verisinde zayif geometriyle gorulen
`p95=6.020 m/s` speed hatasi nedeniyle default alt sinirlar yatay eksenlerde
`2.0 m/s`, dikey eksende `3.0 m/s`'dir. Bu konservatif tabanlar zayif geometri
ve kartta kalan Doppler hata kuyruklarini sinirlar; formal
covariance daha buyukse aynen korunur. Velocity innovation 3-DOF `%99.9`
`NIS=16.266` kapisindan gecer. Kabul/red sayilari ve son velocity NIS'i
`[FUSION]` UART status satirinda `velocity=accepted/rejected` ve `vel_nis`
olarak yayinlanir.
- Konum cozumu URA/elevation/CN0 agirlikli WLS kullanir; raporlanan DOP degerleri ise bu normalize relative weight'lerden uretilmez. GDOP/PDOP/TDOP agirliksiz geometri matrisinden, HDOP/VDOP de ayni covariance ECEF'ten lokal ENU eksenlerine donduruldukten sonra hesaplanir. Boylece loglanan degerler klasik geometrik DOP anlamini korur.
- Troposfer correction her satellite icin bagimsiz hesaplanir ve dort satellite ile de aktiftir. Ilk konumsuz iteration sabit baslangic correction'i kullanir; receiver konumu olustuktan sonra elevation ve metre cinsinden receiver height modele verilir. Model kilometre bekledigi icin height donusumu yapilir ve dusuk elevation'da gecerli olabilen 5 m uzeri delay degerleri artik keyfi olarak sifirlanmaz.
- Troposfer modelindeki mevcut `1013 hPa` basinc, `293 K` sicaklik ve `%50` bagil nem degerleri gecici fallback sabitleridir. Ileride gercek barometre/basinc, sicaklik ve nem sensorleri sisteme baglanacak; model her navigation cozumunde son gecerli sensor snapshot'indan beslenecektir. Sensor arayuzu modele basinci `hPa`, sicakligi `K` ve bagil nemi `%` olarak vermeli; birim donusumleri sensor surucusu ile navigation modeli arasinda tek bir yerde yapilmalidir. Sensor verisi henuz hazir degilse veya kullanilamiyorsa mevcut sabit atmosfer degerleriyle correction devam edecektir.
- Least-squares veya DOP inverse matrisi singular/invalid olursa firmware artik `print_mat(NULL)` veya NULL matrix uzerinden load fault uretmez. Bu epoch sessizce skip edilir; ayrinti sadece `DEBUG_TRACE` seviyesinde gorulur ve onceki gecerli position bozularak 20 km ziplamaya zorlanmaz.
### Navigation Veri Yolu Notlari
- Azimut ve elevasyon hesaplari aktif olarak elevation/URA weight ve Klobuchar correction icin kullanilir. `az` ve `el` heap matrisleri de doldurulur fakat mevcut solver bu matrisleri daha sonra okumaz. Satellite bazli metric arayuzu eklendiginde bu degerler matris yerine kalici metric kaydina tasinabilir.
- Azimut ve elevasyon hesaplari aktif olarak elevation/URA/CN0 weight ve Klobuchar correction icin kullanilir. `az` ve `el` heap matrisleri de doldurulur fakat mevcut solver bu matrisleri daha sonra okumaz. Satellite bazli metric arayuzu eklendiginde bu degerler matris yerine kalici metric kaydina tasinabilir.
- `rho0`/`range_t` eski range ve range-rate yolundan kalmistir. `computeRange()` icinde `rho` alanlarini dolduran eski blok kapali oldugu icin mevcut position-only akista `rho0` allocate edilir fakat sonuc uretmez.
- `velocities` matrisi gelecekte Doppler/receiver-velocity veya EKF icin anlamlidir. Mevcut `satpos()` velocity hesaplamadigi ve aktif `computeRange()` bu matrise yazmadigi icin bugunku position solve'da kullanilmaz.
- `computeRange()` icinde aktif parametreler `psTVLTime`, `sat_no`, `eph`, `clockCorr` ve `satellitePositions`'dir. `rho`, `ionoutc`, `xyz`, `tempPos` ve `velocities` eski genis API'den kalmistir. Ionosphere correction artik receiver azimut/elevasyonu hazir oldugunda WLS icinde uygulanir.
- `satpos()` analitik satellite ECEF velocity ve clock drift hesaplar;
`computeRange()` bunlari `velocities` ve `clockDrift` matrislerine yazar.
Position WLS bu alanlari kullanmaz; Doppler velocity WLS kullanir.
- `computeRange()` icinde aktif parametreler `psTVLTime`, `sat_no`, `eph`,
`clockCorr`, `clockDrift`, `satellitePositions` ve `velocities`'dir.
`rho`, `ionoutc`, `xyz` ve `tempPos` eski genis API'den kalmistir.
Ionosphere correction receiver azimut/elevasyonu hazir oldugunda WLS icinde
uygulanir.
- Ephemeris alanlarinin mevcut isimleri ile ICD anlamlari su sekildedir: `omgdot` gercekte `Omega0`, `omg0` gercekte argument of perigee `omega`, `omgkdot` gercekte `OmegaDot` degeridir. `satpos()` bunlari dogru matematiksel yerde kullanir; konu su an hesap hatasi degil, ileride yapilacak kontrollu isim duzeltmesidir.
- `POSTPONE_TOW=0.160` donanim/pipeline zaman kalibrasyonunun bir parcasidir; kaynagi dogrulanmadan kaldirilmamalidir.
- `POSTPONE_TOW=0.180` mevcut RTL'nin dokuzuncu takip eden navigation bitinde
yaptigi teslimi duzeltir. `satpos()` ve PPS receive-time anchor bu degeri
tam bir kez ve ayni firmware surumunde kullanir.
- Ilk nav-solve interrupt'i geldiginde o anda allocated, hardware busy ve nav-solve interrupt'i etkin olan kanallar epoch'un sabit bekleme maskesine alinir. Unlock interrupt'i gelen kanal artik tracked olmadigi icin bekleme maskesinden cikarilir; epoch ortasinda navigation-ready olan yeni kanal ise aktif maskeyi buyutmez ve sonraki epoch'tan itibaren beklenir. Kalan beklenen kanallarin tamami gelirse snapshot son interrupt'ta hemen yayinlanir. 30 ms holdoff dolarsa veya daha yeni fiziksel epoch baslarsa eksik eski epoch WLS calistirilmadan discard edilir; eski ve yeni olcumler birlestirilmez.
- Interrupt bekleme maskesi tamamlandiktan sonra frozen kanallarin tumu ortak TOW/`ABS_SAMPLE` kumesinde degilse epoch reddedilir. Selector'in buldugu daha kucuk alt kume yalniz diagnostic bilgidir ve WLS girdisi olamaz.
- Dort uydu 3D konum + receiver clock bias icin minimum ve yeterlidir. Bu durumda
@@ -760,9 +989,9 @@ GPS_LOG(level, category, ...)
| `GPS_DEBUG_LEVEL` | Eklenen cikti |
| ---: | --- |
| `1` | Temel telemetri: ham GPS/WLS `[LOCATION] source=gps`, gecerliyse ESKF `[LOCATION] source=imu_corrected`, DOP degerleri ve sifir tabanli donanim kanal numaralarini tasiyan `[TRK] tracked satellites=... prn_indices=[...]` (`0` = GPS PRN `G01`). Baslangicta fusion/IMU konfigurasyonu da bir kez yazilir. |
| `1` | Temel telemetri: ham GPS/WLS `[LOCATION] source=gps` satirinda DOP ile birlikte `velocity_valid`, `speed` ve `velocity_rms`; gecerliyse ESKF `[LOCATION] source=imu_corrected`; sifir tabanli donanim kanal numaralarini tasiyan `[TRK] tracked satellites=... prn_indices=[...]` (`0` = GPS PRN `G01`). Baslangicta fusion/IMU konfigurasyonu ve periyodik `[FUSION]` NIS/kabul-red durumu da gorulur. |
| `2` | Gelecekteki ek status ayrintilari icin ayrilmistir. Su an level 2'ye ozel mesaj yoktur; pratikte level 1 ile ayni ciktiyi verir. |
| `3` | Level 1'e ek olarak acquisition/tracking/navigation durum gecisleri, warning/error mesajlari, integrity bilgileri ve periyodik ayrintili `[FUSION] state=...` durumu. |
| `3` | Level 1'e ek olarak acquisition/tracking/navigation durum gecisleri, warning/error mesajlari ve integrity bilgileri. |
| `4` | Level 3'e ek olarak performans, scheduler, nav-bit/parser, uydu/olcum ve dusuk seviye TRACE tanilari. UART yuku en yuksek seviyedir. |
`Makefile` varsayilanlari `GPS_DEBUG=1` ve `GPS_DEBUG_LEVEL=4`'tur. Bu nedenle
@@ -814,6 +1043,23 @@ XLSX register dokumanlarina gore dogrulananlar:
- nav timer `0x44`
- reserved `0x48`
- remaining code phase `0x4c`
- measurement meta `0x50`
- measurement sequence `0x54`
- measurement carrier Q7 `0x58`
- measurement absolute sample `0x5c`
- measurement remaining code phase `0x60`
- measurement prompt sum-I `0x64`
- measurement prompt sum-Q `0x68`
- measurement WBP low/high `0x6c/0x70`
- TRCK measurement control/status bitleri:
- control bit 13: measurement interrupt enable
- control bit 14: measurement flag enable
- status bit 7: measurement flag
- Measurement meta `[31:24]=version(1)`, `[12:8]=sample count`, bit 2 lock,
bit 1 navigation bit ve bit 0 valid alanlarini tasir.
- Wrapper payload'i atomik register eder. Software yine de coherent read icin
sequence register'ini payload'dan once ve sonra okuyup iki degerin esit
oldugunu kontrol eder.
- Interrupt clear davranisi write-one toggle/clear seklinde ele alindi.
- TRCK XLSX icinde NAV_SOLVE flag aciklamasi ambiguous/copy-paste gibi gorunuyor; RTL davranisina gore nav timer solve flag olarak yorumlandi.
@@ -958,6 +1204,107 @@ Kalan warning'ler:
- linker tarafinda RWX LOAD segment warning'i,
- eski ASCII/comment formatting warning'i.
20 ms tracking measurement degisiklikleri icin host/Linux ve RTL regression
komutlari:
```sh
make -C soc/software/gps_system/tests run
make -C soc/software/gps_system/tests sanitize
make -C hardware/external/gps/rtl/trck_src/tb test
```
`run` ve `sanitize`, C/N0 estimator'i, Q7 ham/filtreli Doppler-range-rate
donusumu, coherent mailbox read'i, weighted velocity WLS/covariance,
tek-kanal robust FDE ve satellite velocity/clock drift hesaplarini kapsar.
Fusion regression'i ayrica
ECEF->ENU velocity/covariance rotasyonunu, velocity NIS kabul/redini ve ayni
epoch position+velocity delayed replay esdegerligini kapsar. RTL
Questa/vlog-vsim testi prompt akumulasyonu,
20 ms hold, synchronizer settle, atomik APB capture ve interrupt yolunu
kapsar.
### 2026-07-25 Genesys2 C/N0 ve Doppler Board Dogrulamasi
Kart ve GPS anteni sabitken canli tracking mailbox olcumlerinde:
- `M=20`, `valid=1` ve `lock=1` goruldu.
- C/N0 kanala gore yaklasik `38..46 dB-Hz` araligindaydi; lock durumundaki
canli L1 C/A sinyali icin makul ve PRN'ler arasinda ayirt ediciydi.
- Doppler kanala gore yaklasik `-3.9 kHz..+3.0 kHz` araligindaydi. Uydu
hareketi nedeniyle farkli PRN'lerde hem pozitif hem negatif deger
gorulmesi beklenen fiziksel davranistir.
- Ham kanal Doppler jitter'i yaklasik `6..13 Hz RMS`, yani L1'de
`1.2..2.5 m/s` range-rate jitter'iydi. `alpha=0.05` software IIR sonrasi
kanal standard deviation'i yaklasik `1.1..2.2 Hz` oldu; `3x..6x`
iyilesme olculdu.
- Ayni epoch'un offline karsilastirmasinda C/N0-weighted velocity WLS
`2.364 m/s`, equal-weight cozum `4.120 m/s` speed verdi. Bu tek epoch
kalite agirliginin dogru yonde fayda ettigini gosterir; genis veri seti
kalibrasyonunun yerine gecmez.
- IIR yuklu firmware'de 5 uydu ve `PDOP=13..17` ile 213 velocity sonucunun
mean/median/p95/max speed degerleri sirasiyla
`2.495/2.158/6.020/11.344 m/s` oldu. Sabit alici icin sifir degildir;
dusuk uydu sayisi, kotu geometri ve code/ephemeris hatalari halen baskindir.
- Satellite LOS rate cikarilip satellite clock drift eklendiginde kanallar
ortak yaklasik `129.9..130.8 m/s` receiver clock-drift teriminde
toplandi. Bu, Doppler isaretinin ve velocity observation modelinin
fonksiyonel olarak tutarli oldugunu dogruladi.
- Kesintisiz kosuda `event_overflow=0`, `plic_drain_limit=0`,
`plic_repeat=0`, `measurement_missed=0` ve `measurement_invalid=0`
kaldi. Debugger ile tekrar tekrar halt edilen onceki olcumlerde gorulen
missed sayilari donanim mailbox/CDC kaybi olarak yorumlanmamalidir.
C/N0 ve Doppler verisi fiziksel olarak makul, IIR velocity gurultusunu
azaltiyor ve C/N0 weighting ornek epoch'ta sonucu iyilestiriyor. Buna ragmen
position WLS'deki pseudorange/code-phase hata ve zayif geometriyi tek basina
duzeltmiyor. Doppler velocity artik ESKF state update'ine baglidir; formal
covariance, kart verisine dayali floor ve ayri NIS gate ile asiri guven
sinirlanir. Yukaridaki board istatistikleri ESKF entegrasyonundan onceki
velocity dogrulamasidir; entegrasyon sonrasi `vel_nis` ve
`velocity=accepted/rejected` saha sonucu bir sonraki bolumdedir.
### 2026-07-26 Genesys2 PPS ve Doppler/ESKF A/B
Kart sabitken once tam ham 20 ms snapshot -> robust WLS -> ESKF yolu olculdu.
Son 120 PPS penceresinde ham WLS speed RMS `9.17 m/s`, p95 `16.71 m/s` ve
raw-FDE `86/120` oldu. Yavas `alpha=0.05` PPS yolu ayni pencerede `0.92 m/s`
RMS, `1.63 m/s` p95 ve `0/120` FDE verdi. ESKF+ZUPT fused hizi dusuk tutsa da
bu sonuc tam ham yolun hareketli platform icin guvenli oldugunu dogrulamadi.
Bu nedenle ESKF icin yukarida tarif edilen ayri adaptif yol uygulandi. Ayni
kartta 240 epoch'luk yerlesmis pencerede `alpha=0.05`, speed RMS `0.48 m/s`,
p95 `0.83 m/s`, FDE `0/240` olculdu. Son 180 saniyede ESKF velocity kabul/red
farki `892/0`, velocity NIS median/p95/max `0.028/0.111/0.180`, fused speed
RMS `0.0052 m/s` oldu. IMU overflow sayaci bu pencerede `15` olarak sabit
kaldi; olcum telemetrisi yeni kayip uretmedi.
PPS predictor, yeniden boot etmeden 40 saniyelik kapali/acik segmentlerle A/B
yapildi (`GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES=200`, yalniz tanisal build).
Her segmentin ilk bes saniyesi atildiginda predictor acik segmentlerde software
clock-bias innovation RMS `12.52 m`, kapali segmentlerde `46.48 m` oldu.
Hardware phase p95 acikken `51.02 ns`, kapaliyken `58.21 ns` idi. Tek acik
segmentte gorulen bir `-216.48 ns` one-epoch phase transient'i lock kaybi
olusturmadi; bu nedenle physical-pin jitter sonucu olarak yorumlanmaz.
Production default predictor acik, A/B toggling kapali (`0`) kalir.
Son olarak `adaptive-pps-final-20260726` production image'i
`PREDICTOR_ENABLE=1`, `PREDICTOR_AB_UPDATES=0` ile RAM'den karta yuklendi.
Acquisition/coarse baslangici dislandiktan sonraki 120 lock status orneginde:
- predictor tum orneklerde `1`, ESKF alpha sabit kartta tum orneklerde `0.05`,
- adaptive velocity-WLS speed RMS/p95 `0.52/0.92 m/s`, FDE `0/120`,
- ESKF velocity kabul/red farki `448/0`, velocity NIS p95/max
`0.139/0.341`, fused speed RMS `0.0061 m/s`,
- PPS software clock innovation RMS `12.36 m`,
- internal hardware phase RMS ve p95-absolute `28.50/58.36 ns`, lock kaybi
`0/120`,
- IMU overflow sayaci `13..13`, tracking measurement missed sayaci `0..0`
kaldi.
Bu `hw_phase`, APB status'taki son commit residual'idir; fiziksel PPS pin
jitter'i degildir. Dolayisiyla `15-20 ns` pin hedefinin saglandigi bu logdan
iddia edilmez; scope veya time-interval counter ile ayri olculmelidir.
### 2026-07-08 ACQ Debug Notu
`make fpga_debug_load APP=gps_system GDB=/usr/bin/gdb-multiarch` sonrasi GDB ile dogrulanan durum:
@@ -996,21 +1343,122 @@ Bu timer/scheduler yavasligi degildir. Firmware start ediyor; tamamlanma icin RF
darbesi uretir. Bu olay `ABS_SAMPLE` ve `REM_CODE_PHASE` latch'iyle aynidir;
subframe-ready PPS timestamp'i olusturmaz.
- `leastSquaresSTD()` ayni basarili epoch'un `[x, y, z, b]` sonucunu verir.
Minimum corrected-ABS kanalinin receive-time ankraji su sekilde kurulur:
Ilk cozumde en erken corrected-ABS kanali zaman datum'u secilir; ayni SVID
coherent snapshot'ta kaldigi surece referans korunur. Receive-time ankraji:
```text
T_rx = TOW_ref + POSTPONE_TOW + STARTOFFSET/1000 - b/c
```
- `POSTPONE_TOW=0.160 s`, takip eden subframe'dan parity icin beklenen sekiz
bitin gecikmesidir ve yazilim tarafinda tam bir kez uygulanir. PPS RTL bu
degeri tekrar eklemez.
- Bu GPS zamani, ayni minimum-ABS kanalinin 125 MHz capture counter'i ile atomik
- `POSTPONE_TOW=0.180 s`, mevcut tracking RTL'nin parity kontrolunu dokuzuncu
takip eden bitte tamamlamasinin gecikmesidir ve yazilim tarafinda tam bir kez
uygulanir. PPS RTL bu degeri tekrar eklemez. Tracking RTL sekizinci bitte
validate edecek sekilde degistirilirse bu sabit ayni release'de 160 ms'ye
geri alinmalidir.
- Bu GPS zamani, ayni kalici referans kanalinin 125 MHz capture counter'i ile atomik
APB mailbox uzerinden eslenir. Ilk cozum coarse-align, sonraki cozumler Q2.46
dusuk bant genislikli PI rate discipline yapar. WLS/APB calisma suresi faz
hatasi olarak kullanilmaz.
- Tracking capture olayi integer RF sample kenarinda olustugu halde WLS,
`ABS_SAMPLE - REM_CODE_PHASE/1.023e6` alt-sample zamanini kullanir. Yazilim
mailbox `EPOCH_COUNTER` degerinden ayni fractional sample gecikmesini
`round(fraction * 125e6/16.367981e6)` ile cikarir. Boylece WLS zamani ile
hardware epoch'u ayni code boundary'ye referans olur; 125 MHz integer
mailbox yuvarlamasindan en fazla 4 ns kalir.
- HOW/sample sureklilik kontrolu kartin fiziksel `16.367981 MHz` RF frekansini
kullanir. Periyodik nav-solve TOW'u receiver propagation/range-rate
degisiminden uretilmez; sample farki yalniz olay numarasini dogrular ve TOW
bilinen 200 ms transmit-time grid'ine snap edilir.
- Donanimdaki kontrol filtresi PI loop'un kendisidir; ayri bir phase-IIR veya
D terimi yoktur. Yazilim `TDOP/GDOP`, formal clock-bias sigma, kullanilan
uydu sayisi, C/N0, chi-square/FDE ve Doppler velocity residual'ina gore
`acquire`, `precision`, `robust` ve `holdover` profillerini histerezisle
secer. Profil gecisinde `Kp`, `Ki`, `max-slew` ve v2.2 lock esikleri bir
sonraki mailbox commit'inde birlikte yakalanir; normal geciste integrator
korunur. Lock almak icin bes ardisik residual `64 ns` icinde olmalidir;
uretim release esigi tek seyrek WLS/capture sicrama pulse kaybettirmesin
diye `1.024 us` tutulur. Watchdog, quality holdover ve `20 ppm` slew clamp
buyuk arizalari ayri olarak sinirlar.
- Yazilim filtresi PI'nin yerine gecmez. Yalnizca 5 Hz WLS clock-bias
olcumunde filtreli-Doppler velocity'den turetilen referans-datum drift
prediction, bounded innovation ve aykiri olcum reddi yapar. Position WLS
`clock_bias_m` ana PPS olcumu olarak kalir; Doppler yalnız prediction'dir.
Ilk yerlesmede commit'ler coarse ve PPS invalid tutulur.
Periyodik background watchdog iki saniye yeni referans gelmezse cikis
validligini holdover ile kapatir; fakat ayni referans geri gelirse Doppler
prediction ve donanim PI state'i 30 saniyeye kadar korunur. Referans SVID
degisimi veya 30 saniyeyi asan bosluk coarse re-anchor yapar. PPS v2.2
holdover aktifken normal PI commit'ini uygulamadigi icin recovery'de butun
shadow word'ler once yazilir, holdover commit toggle'inin hemen oncesinde
birakilir ve ilk taze olcum PI'ya girer. Fiziksel PPS held rate ile
kesintisiz yurur; `STATUS.VALID` PI apply'dan yalniz bounded
synchronizer/commit-pipeline gecikmesi kadar once gorunebilir.
- PPS v2.2 `LOOP_CONFIG` register'i `0x88` adresindedir. Eski v2.1 bitstream
algilanirsa yazilim bu register'a dokunmaz; dinamik Q2.46 `Kp/Ki/max-slew`
yazimi mevcut register'larla calismaya devam eder.
- 125 MHz fiziksel kenar grid'i 8 ns'dir. Generated VCU118 ve Genesys2 top'lari
`gps_pps` output'unu expose eder. VCU118 icin PPS PMOD1/J53.8 (`R29`),
Genesys2 icin PMOD JC10 (`AK28`) pinine atanmistir. Board testinde
Gray/PCLK capture pipeline sabit offset'i ve
clock-tree/I/O gecikmesi olculup kalibre edilmelidir.
### PPS Sentetik Dogrulama (2026-07-25)
Python modeli ve Linux C testi ayni Q32/Q46 PI, kalite histerezisi, Doppler
prediction, holdover ve lock politikasini kullanir. Ayrica donanim phase-IIR
kullanilmaz. Test matrisi temiz/320 ns WLS noise, 650 ns zayif geometri,
baslangicta 1.1 us noise sonra temiz sinyal, aralikli 900 ns noise burstleri,
uydu kaybi ve geri gelmesi, 15 saniyelik state-korumali ve 30 saniye ustu
coarse WLS kesintisi, Doppler kaybi, multipath
basamagi, profil sicramalari, +18 ppm slew siniri ve +25 ppm sinir disi
osilatoru kapsar.
Monte Carlo sonucunda hedeflenen senaryolarin tamami lock/recovery sagladi.
Normal kart-noise senaryosunda 50/50 kosu lock oldu ve 125 MHz edge
quantization dahil bir saniyelik jitter p95 `13.62 ns` bulundu. Baslangicta
yuksek noise sonra temiz sinyal senaryosunda 30/30, noise-burst senaryosunda
30/30, iki uydu-kaybi araligi bulunan senaryoda 30/30 recovery goruldu.
`+18 ppm` 30/30 yerlesirken `+25 ppm`, `20 ppm` clamp disinda oldugu icin
0/10 lock uretti; yani saturation yanlis-valid olusturmadi. Bunlar sentetik
loop/jitter sonuclaridir; mutlak UTC/GPS faz dogrulugu icin karttaki sabit
capture/I/O offset'i ve gercek multipath halen osiloskop/time-interval olcumu
gerektirir.
### PPS Kart Dogrulamasi (2026-07-25, Genesys2)
- Kartta okunan RTL surumu `0x00020200`; dinamik `LOOP_CONFIG=0x08800800`
mailbox'i ve diagnostic snapshot calisti.
- Eski `16.368 MHz` zaman donusumu 6 saniyede yaklasik `6.97 us` hata
uretiyordu. Fiziksel `16.367981 MHz` donusumu ve 200 ms transmit-grid snap
sonrasinda 6 saniyelik mikro-saniye sawtooth kayboldu.
- Fractional capture duzeltmesi kartta epoch'a gore `0..64 ns` uygulandi
(son deger 8 ns grid'ine yuvarlanir). Duzeltmeden onceki uzun lock
penceresinde internal commit residual'i yaklasik `20.9 ns` median mutlak ve
`62.8 ns` p95 idi. Duzeltmeden sonraki precision penceresinde median mutlak
residual yaklasik `17 ns`, RMS `26 ns` ve p95 yaklasik `55 ns` oldu.
- Ilk kart yaziliminda iki saniyeyi asan normal coherent-WLS scheduling
bosluklari filtreyi coarse resetliyordu; bu PPS'in periyodik olarak
4--5 update invalid kalmasina yol acti. Esigi validlik watchdog'undan
ayirdiktan sonra `seq=10551 -> 10561` araliginda `pulse=2174 -> 2186`
ile yaklasik 12 saniyelik gercek kart boslugu goruldu. Buna ragmen
`mode=tracking`, `locked=1`, status `0x80f` ve PI integrator kesintisiz
kaldi; coarse commit veya faz basamagi olusmadi.
- Her WLS commit'inin loglandigi `pps-fullrate` tanisinda `seq=16090..17631`
arasinda 1514 commit izlendi. Ilk lock'tan sonra coarse veya unlock olmadi.
`seq>=16250` icin 1355 locked residual'in median mutlak degeri `17.29 ns`,
RMS'i `34.17 ns`, p95'i `52.76 ns`, p99'u `67.24 ns` oldu. Bu sayilar
fiziksel PPS pin jitter'i degil, 5 Hz WLS commit residual'idir.
- Ayni tanida quality'nin robust'a dustugu tek bir commit'te `750.93 ns`
residual goruldu ve bir sonraki 200 ms commit'te normale dondu. Robust
`1.024 us` release esigi gereksiz pulse kaybini onledi. Daha once precision
`512 ns` release ile gorulen tek kisa unlock nedeniyle uretim precision
esigi de `1.024 us` yapildi; acquisition kosulu bes ardisik `<=64 ns`
olarak kaldi. Son `pps-final` kart okumasinda `LOOP_CONFIG=0x05800800`,
`CONTROL=0x3`, valid/locked status `0x80f` goruldu ve kontrollu holdover
denemesinden sonra ilk normal commit ile control/valid geri geldi.
- Bu `hw_phase`, 5 Hz WLS reference commit residual'idir ve dogrudan fiziksel
1PPS kenar jitter'i degildir. Ayni pencerede proportional rate terimi
yaklasik `9.6 ppb RMS` idi; sentetik 125 MHz edge modeli kart-noise
senaryosunda bir saniyelik jitter p95 `13.62 ns` veriyor. `15--20 ns`
fiziksel hedef makuldur fakat pin uzerinde scope/TIC ile GPS-disiplinli
harici referansa karsi olculmeden saglandi diye raporlanmamalidir.
+23 -3
View File
@@ -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],
@@ -22,6 +22,7 @@ typedef struct {
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;
@@ -49,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. */
@@ -9,7 +9,9 @@
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;
@@ -30,8 +32,12 @@
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];
@@ -51,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;
@@ -63,6 +70,7 @@
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);
@@ -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
@@ -55,6 +55,8 @@
void gps_background_task(void);
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot);
void gps_get_runtime_diagnostics(gps_runtime_diagnostics_t* diagnostics);
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);
@@ -55,6 +55,7 @@
#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
@@ -83,6 +84,8 @@
#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");
@@ -174,7 +177,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
+30 -1
View File
@@ -4,11 +4,40 @@
#include <stdbool.h>
#include <stdint.h>
#include "gps/gps_pps_quality.h"
#include "gps/gps_pps_time_filter.h"
#define GPS_PPS_CLOCK_HZ 125000000u
typedef struct {
uint32_t status;
uint32_t active_sequence;
uint32_t pulse_count;
int64_t phase_error_q32;
int64_t integrator_q32;
int64_t rate_word_q32;
} gps_pps_diagnostics_t;
void gps_pps_init(unsigned int nav_solve_tick);
void gps_pps_service(void);
bool gps_pps_is_locked(void);
bool gps_pps_set_loop_profile(gps_pps_profile_t profile);
void gps_pps_set_holdover(bool holdover);
void gps_pps_reset_time_filter(void);
bool gps_pps_read_diagnostics(gps_pps_diagnostics_t* diagnostics);
bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* sequence);
bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiver_tow_sec);
gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
uint64_t epoch_counter,
double measured_clock_bias_m,
bool clock_drift_valid,
double measured_clock_drift_mps,
double tdop,
double* filtered_clock_bias_m_out,
double* innovation_m_out);
bool gps_pps_commit_solution(
uint64_t epoch_counter,
int gps_week,
double receiver_tow_sec,
bool force_coarse);
#endif
@@ -0,0 +1,87 @@
#ifndef GPS_PPS_QUALITY_H
#define GPS_PPS_QUALITY_H
#include <stdbool.h>
#include <stdint.h>
#define GPS_PPS_BASELINE_UPDATE_PERIOD_SEC 0.200
typedef enum {
GPS_PPS_PROFILE_HOLDOVER = 0,
GPS_PPS_PROFILE_ROBUST,
GPS_PPS_PROFILE_PRECISION,
GPS_PPS_PROFILE_ACQUIRE
} gps_pps_profile_t;
typedef struct {
double tdop;
double gdop;
double clock_bias_sigma_m;
double mean_cn0_dbhz;
double minimum_cn0_dbhz;
double chi_square_ratio;
double velocity_residual_rms_mps;
unsigned int used_satellites;
bool solution_valid;
bool residual_test_valid;
bool velocity_valid;
bool fde_applied;
} gps_pps_quality_metrics_t;
typedef struct {
gps_pps_profile_t profile;
gps_pps_profile_t candidate;
unsigned int candidate_updates;
unsigned int upgrade_updates;
unsigned int degrade_updates;
} gps_pps_quality_selector_t;
typedef struct {
double natural_frequency_hz;
double damping;
double max_slew_ppm;
unsigned int lock_acquire_ticks;
unsigned int lock_release_ticks;
unsigned int lock_acquire_updates;
double software_bias_gain;
unsigned int software_settle_updates;
} gps_pps_loop_profile_t;
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector);
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec);
/*
* Select a bounded loop profile from timing geometry, code/carrier quality
* and integrity. Upgrades and degradations require sustained evidence; their
* update counts are scaled from the proven 200 ms timing when period-aware
* initialization is used. A single rejected WLS epoch is skipped without
* resetting the loop; sustained loss enters holdover, while the independent
* reference watchdog remains the final stale-data guard.
*/
gps_pps_profile_t gps_pps_quality_selector_update(
gps_pps_quality_selector_t* selector,
const gps_pps_quality_metrics_t* metrics,
bool* profile_changed_out);
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile);
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec);
/*
* Map the proven 200 ms discrete poles to another update period. The pole
* exponentiation preserves the current loop's continuous-time settling and
* damping while returning exactly the legacy coefficients at 200 ms.
*/
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out);
const char* gps_pps_profile_name(gps_pps_profile_t profile);
#endif
@@ -0,0 +1,84 @@
#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 capture_i - min(capture).
* That subtraction removes the common receiver-clock offset and drift before
* WLS. The fitted clock-bias datum is STARTOFFSET*c - reference_range (plus
* the reference satellite clock correction), so its derivative is
* -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
@@ -36,6 +36,40 @@ 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);
/* 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. */
@@ -6,6 +6,8 @@
#include "gps/gps_ip_defines.h"
#define GPS_TRACK_PPS_DOPPLER_ALPHA 0.05
typedef struct {
uint32_t prn;
uint32_t sequence;
@@ -17,8 +19,15 @@ typedef struct {
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;
@@ -55,7 +64,27 @@ bool gps_tracking_velocity_wls(
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
@@ -27,6 +27,9 @@ bool gps_fusion_build_measurement(
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;
@@ -27,12 +27,13 @@ void gps_system_uart_publish_location(
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_LOCATION,
"source=%s lat=%.20lf lon=%.20lf height=%.3lf "
"gdop=%.10lf pdop=%.10lf hdop=%.10lf vdop=%.10lf tdop=%.10lf "
"tracked=%u used=%u velocity_valid=%u speed=%.3lf "
"tracked=%u used=%u velocity_valid=%u velocity_fde=%u speed=%.3lf "
"velocity_rms=%.3lf\r\n",
source, latitude_deg, longitude_deg, height_m,
gps->gdop, gps->pdop, gps->hdop, gps->vdop, gps->tdop,
gps->tracked_satellite_count, gps->used_satellite_count,
gps->velocity_valid ? 1u : 0u,
gps->velocity_fde_applied ? 1u : 0u,
gps->speed_mps, gps->velocity_residual_rms_mps);
}
@@ -114,17 +115,21 @@ void gps_system_uart_publish_periodic_fusion_status(
if(output->global_valid)
{
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_FUSION,
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_FUSION,
"state=%s lat=%.10lf lon=%.10lf height=%.3lf "
"speed=%.3lf nis=%.3lf gps=%lu/%lu recoveries=%lu "
"speed=%.3lf pos_nis=%.3lf vel_nis=%.3lf "
"gps=%lu/%lu velocity=%lu/%lu recoveries=%lu "
"zupt=%lu replay=%lu "
"sigma=%.2lf,%.2lf,%.2lf imu=%lu fused_imu=%lu "
"overflow=%lu missed=%lu\r\n",
fusion_motion_state_name(output->motion_state),
output->latitude_deg, output->longitude_deg, output->height_m,
output->speed_mps, output->last_position_nis,
output->last_velocity_nis,
(unsigned long)output->gps_accepted,
(unsigned long)output->gps_nis_rejected,
(unsigned long)output->gps_velocity_accepted,
(unsigned long)output->gps_velocity_nis_rejected,
(unsigned long)output->gps_position_recoveries,
(unsigned long)output->zero_update_count,
(unsigned long)output->replay_count,
@@ -137,7 +142,7 @@ void gps_system_uart_publish_periodic_fusion_status(
return;
}
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_FUSION,
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_FUSION,
"state=%s calibration_samples=%u gps=%lu/%lu recoveries=%lu "
"imu=%lu fused_imu=%lu overflow=%lu missed=%lu\r\n",
navigation_fusion_state_name(output->engine_state),
+50
View File
@@ -224,6 +224,7 @@ void eskf_default_config(eskf_config_t* config)
config->gravity_mps2 = 9.80665;
config->max_propagation_step_s = 0.02;
config->position_nis_gate = 16.26623619623813; /* chi-square 3 DOF, 99.9%. */
config->velocity_nis_gate = 16.26623619623813; /* chi-square 3 DOF, 99.9%. */
config->covariance_diagonal_floor = 1.0e-12;
}
@@ -319,6 +320,7 @@ bool eskf_initialize(eskf_t* filter,
}
covariance_symmetrize_and_floor(filter, config->covariance_diagonal_floor);
filter->last_position_nis = NAN;
filter->last_velocity_nis = NAN;
filter->initialized = true;
return true;
}
@@ -806,6 +808,54 @@ eskf_update_result_t eskf_position_update(
return ESKF_UPDATE_ACCEPTED;
}
eskf_update_result_t eskf_velocity_update(
eskf_t* filter,
const eskf_config_t* config,
const double velocity_mps[3],
const double covariance_mps2[9],
double* nis_out)
{
if(nis_out != NULL)
{
*nis_out = NAN;
}
if((filter == NULL) || !filter->initialized || (config == NULL) ||
!vector_is_finite(velocity_mps, 3u) ||
!vector_is_finite(covariance_mps2, 9u))
{
return ESKF_UPDATE_INVALID;
}
double residual[3];
for(unsigned int i = 0u; i < 3u; i++)
{
residual[i] = velocity_mps[i] - filter->velocity_mps[i];
}
double nis = NAN;
if(!eskf_selected_update(
filter, ESKF_VELOCITY_INDEX, residual, covariance_mps2,
true, config->velocity_nis_gate, &nis))
{
filter->last_velocity_nis = nis;
if(isfinite(nis) && (nis > config->velocity_nis_gate))
{
filter->velocity_update_rejected_count++;
if(nis_out != NULL)
{
*nis_out = nis;
}
return ESKF_UPDATE_REJECTED_NIS;
}
return ESKF_UPDATE_INVALID;
}
filter->last_velocity_nis = nis;
filter->velocity_update_accepted_count++;
if(nis_out != NULL)
{
*nis_out = nis;
}
return ESKF_UPDATE_ACCEPTED;
}
bool eskf_reset_position(
eskf_t* filter,
const double position_m[3],
@@ -176,6 +176,10 @@ bool fusion_engine_apply_imu_quality(
static bool config_valid(const fusion_engine_config_t* config)
{
return (config != NULL) && (config->timebase_hz != 0u) &&
isfinite(config->eskf.position_nis_gate) &&
(config->eskf.position_nis_gate > 0.0) &&
isfinite(config->eskf.velocity_nis_gate) &&
(config->eskf.velocity_nis_gate > 0.0) &&
isfinite(config->calibration_duration_s) &&
(config->calibration_duration_s > 0.0) &&
(config->calibration_min_samples >= 2u) &&
@@ -230,6 +234,18 @@ static bool position_valid(const fusion_position_measurement_t* measurement)
isfinite(measurement->source_quality);
}
static bool navigation_valid(
const fusion_navigation_measurement_t* measurement)
{
if((measurement == NULL) || !position_valid(&measurement->position))
{
return false;
}
return !measurement->velocity_valid ||
(finite_values(measurement->velocity_enu_mps, 3u) &&
finite_values(measurement->velocity_covariance_enu_mps2, 9u));
}
static void calibration_reset(fusion_engine_t* engine)
{
memset(&engine->calibration, 0, sizeof(engine->calibration));
@@ -865,13 +881,17 @@ static eskf_update_result_t runtime_apply_position(
return result;
}
static bool process_position_now(fusion_engine_t* engine,
const fusion_position_measurement_t* measurement,
eskf_update_result_t* result_out,
double* nis_out)
static bool process_navigation_now(
fusion_engine_t* engine,
const fusion_navigation_measurement_t* measurement,
eskf_update_result_t* position_result_out,
double* position_nis_out,
eskf_update_result_t* velocity_result_out,
double* velocity_nis_out)
{
const fusion_position_measurement_t* position = &measurement->position;
if(!engine->checkpoint_valid ||
(measurement->timestamp_ticks < engine->checkpoint.filter_timestamp_ticks))
(position->timestamp_ticks < engine->checkpoint.filter_timestamp_ticks))
{
engine->diagnostics.position_stale_rejected++;
return false;
@@ -882,7 +902,7 @@ static bool process_position_now(fusion_engine_t* engine,
for(unsigned int i = 0 ; i < engine->imu_history_count ; i++)
{
const fusion_imu_measurement_t* imu = history_at(engine, i);
if(imu->timestamp_ticks > measurement->timestamp_ticks)
if(imu->timestamp_ticks > position->timestamp_ticks)
{
next_imu = imu;
break;
@@ -894,7 +914,7 @@ static bool process_position_now(fusion_engine_t* engine,
}
replayed++;
}
if(revised.filter_timestamp_ticks < measurement->timestamp_ticks)
if(revised.filter_timestamp_ticks < position->timestamp_ticks)
{
if(!revised.have_held_imu)
{
@@ -902,12 +922,12 @@ static bool process_position_now(fusion_engine_t* engine,
return false;
}
fusion_imu_measurement_t endpoint = revised.held_imu;
endpoint.timestamp_ticks = measurement->timestamp_ticks;
endpoint.timestamp_ticks = position->timestamp_ticks;
if((next_imu != NULL) &&
(next_imu->timestamp_ticks > revised.held_imu.timestamp_ticks))
{
endpoint = interpolate_imu(&revised.held_imu, next_imu,
measurement->timestamp_ticks);
position->timestamp_ticks);
}
if(!runtime_propagate_to(engine, &revised, &endpoint))
{
@@ -915,31 +935,31 @@ static bool process_position_now(fusion_engine_t* engine,
return false;
}
}
double nis = NAN;
eskf_update_result_t result = runtime_apply_position(engine, &revised,
measurement, &nis);
if(result == ESKF_UPDATE_INVALID)
double position_nis = NAN;
eskf_update_result_t position_result = runtime_apply_position(
engine, &revised, position, &position_nis);
if(position_result == ESKF_UPDATE_INVALID)
{
return false;
}
if(result == ESKF_UPDATE_ACCEPTED)
if(position_result == ESKF_UPDATE_ACCEPTED)
{
position_recovery_clear(engine);
}
else if((result == ESKF_UPDATE_REJECTED_NIS) &&
position_recovery_ready(engine, &revised, measurement) &&
eskf_reset_position(&revised.filter, measurement->position_enu_m,
measurement->covariance_enu_m2))
else if((position_result == ESKF_UPDATE_REJECTED_NIS) &&
position_recovery_ready(engine, &revised, position) &&
eskf_reset_position(&revised.filter, position->position_enu_m,
position->covariance_enu_m2))
{
/* This is a deliberate position-state reset, not a Kalman update with
* a disabled gate. The independent IMU-derived attitude and bias state
* survive, while invalid position cross-covariances are discarded. */
runtime_record_recovered_position(&revised, measurement);
runtime_record_recovered_position(&revised, position);
engine->diagnostics.position_recovery_count++;
position_recovery_clear(engine);
result = ESKF_UPDATE_ACCEPTED;
position_result = ESKF_UPDATE_ACCEPTED;
}
if(result == ESKF_UPDATE_ACCEPTED)
if(position_result == ESKF_UPDATE_ACCEPTED)
{
engine->diagnostics.position_accepted++;
}
@@ -947,9 +967,34 @@ static bool process_position_now(fusion_engine_t* engine,
{
engine->diagnostics.position_nis_rejected++;
}
double velocity_nis = NAN;
eskf_update_result_t velocity_result = ESKF_UPDATE_INVALID;
if(measurement->velocity_valid)
{
velocity_result = eskf_velocity_update(
&revised.filter, &engine->config.eskf,
measurement->velocity_enu_mps,
measurement->velocity_covariance_enu_mps2,
&velocity_nis);
if(velocity_result == ESKF_UPDATE_ACCEPTED)
{
engine->diagnostics.velocity_accepted++;
}
else if(velocity_result == ESKF_UPDATE_REJECTED_NIS)
{
engine->diagnostics.velocity_nis_rejected++;
}
else
{
/* A malformed optional Doppler covariance must not discard a valid
* position update from the same epoch. */
velocity_result = ESKF_UPDATE_INVALID;
}
}
engine->checkpoint = revised;
engine->checkpoint_valid = true;
history_drop_through(engine, measurement->timestamp_ticks);
history_drop_through(engine, position->timestamp_ticks);
for(unsigned int i = 0 ; i < engine->imu_history_count ; i++)
{
if(!runtime_apply_imu(engine, &revised, history_at(engine, i), false))
@@ -962,19 +1007,27 @@ static bool process_position_now(fusion_engine_t* engine,
engine->current = revised;
engine->diagnostics.replay_count++;
engine->diagnostics.replayed_imu_count += replayed;
if(result_out != NULL)
if(position_result_out != NULL)
{
*result_out = result;
*position_result_out = position_result;
}
if(nis_out != NULL)
if(position_nis_out != NULL)
{
*nis_out = nis;
*position_nis_out = position_nis;
}
if(velocity_result_out != NULL)
{
*velocity_result_out = velocity_result;
}
if(velocity_nis_out != NULL)
{
*velocity_nis_out = velocity_nis;
}
return true;
}
static bool pending_insert(fusion_engine_t* engine,
const fusion_position_measurement_t* measurement)
const fusion_navigation_measurement_t* measurement)
{
if(engine->pending_position_count >= FUSION_POSITION_PENDING_CAPACITY)
{
@@ -983,8 +1036,8 @@ static bool pending_insert(fusion_engine_t* engine,
}
unsigned int index = engine->pending_position_count;
while((index != 0u) &&
(engine->pending_positions[index - 1u].timestamp_ticks >
measurement->timestamp_ticks))
(engine->pending_positions[index - 1u].position.timestamp_ticks >
measurement->position.timestamp_ticks))
{
engine->pending_positions[index] = engine->pending_positions[index - 1u];
index--;
@@ -997,16 +1050,18 @@ static bool pending_insert(fusion_engine_t* engine,
static void process_ready_pending_positions(fusion_engine_t* engine)
{
while((engine->pending_position_count != 0u) &&
(engine->pending_positions[0].timestamp_ticks <=
(engine->pending_positions[0].position.timestamp_ticks <=
engine->current.filter_timestamp_ticks))
{
fusion_position_measurement_t measurement = engine->pending_positions[0];
fusion_navigation_measurement_t measurement =
engine->pending_positions[0];
for(unsigned int i = 1u ; i < engine->pending_position_count ; i++)
{
engine->pending_positions[i - 1u] = engine->pending_positions[i];
}
engine->pending_position_count--;
(void)process_position_now(engine, &measurement, NULL, NULL);
(void)process_navigation_now(
engine, &measurement, NULL, NULL, NULL, NULL);
if(engine->state == FUSION_ENGINE_FAULT)
{
return;
@@ -1056,6 +1111,7 @@ bool fusion_engine_push_position(
eskf_update_result_t* update_result_out,
double* nis_out)
{
fusion_navigation_measurement_t navigation;
if(update_result_out != NULL)
{
*update_result_out = ESKF_UPDATE_INVALID;
@@ -1064,28 +1120,70 @@ bool fusion_engine_push_position(
{
*nis_out = NAN;
}
if(measurement == NULL)
{
return false;
}
memset(&navigation, 0, sizeof(navigation));
navigation.position = *measurement;
return fusion_engine_push_navigation(
engine, &navigation, update_result_out, nis_out, NULL, NULL);
}
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)
{
if(position_update_result_out != NULL)
{
*position_update_result_out = ESKF_UPDATE_INVALID;
}
if(position_nis_out != NULL)
{
*position_nis_out = NAN;
}
if(velocity_update_result_out != NULL)
{
*velocity_update_result_out = ESKF_UPDATE_INVALID;
}
if(velocity_nis_out != NULL)
{
*velocity_nis_out = NAN;
}
if((engine == NULL) || (engine->state == FUSION_ENGINE_FAULT) ||
!position_valid(measurement))
!navigation_valid(measurement))
{
return false;
}
engine->diagnostics.position_received++;
if(!measurement->source_valid)
if(measurement->velocity_valid)
{
engine->diagnostics.velocity_received++;
}
if(!measurement->position.source_valid)
{
engine->diagnostics.position_source_rejected++;
return false;
}
if(engine->state == FUSION_ENGINE_CALIBRATING)
{
engine->initial_position = *measurement;
engine->initial_position = measurement->position;
engine->have_initial_position = true;
return true;
}
if(measurement->timestamp_ticks > engine->current.filter_timestamp_ticks)
if(measurement->position.timestamp_ticks >
engine->current.filter_timestamp_ticks)
{
return pending_insert(engine, measurement);
}
return process_position_now(engine, measurement, update_result_out, nis_out);
return process_navigation_now(
engine, measurement,
position_update_result_out, position_nis_out,
velocity_update_result_out, velocity_nis_out);
}
bool fusion_engine_get_runtime(const fusion_engine_t* engine,
@@ -80,6 +80,14 @@ void fusion_gps_adapter_default_config(fusion_gps_adapter_config_t* config)
config->minimum_horizontal_std_m = 0.50;
config->minimum_vertical_std_m = 1.00;
config->maximum_position_std_m = 1000.0;
/*
* Board data shows several m/s stationary speed error with weak
* four/five-satellite geometry even after Doppler IIR smoothing. These
* floors keep repeated, correlated velocity fixes useful but conservative.
*/
config->minimum_horizontal_velocity_std_mps = 2.0;
config->minimum_vertical_velocity_std_mps = 3.0;
config->maximum_velocity_std_mps = 100.0;
}
bool fusion_gps_adapter_init(fusion_gps_adapter_t* adapter,
@@ -88,7 +96,11 @@ bool fusion_gps_adapter_init(fusion_gps_adapter_t* adapter,
if((adapter == NULL) || (config == NULL) ||
!(config->minimum_horizontal_std_m > 0.0) ||
!(config->minimum_vertical_std_m > 0.0) ||
!(config->maximum_position_std_m >= config->minimum_vertical_std_m))
!(config->maximum_position_std_m >= config->minimum_vertical_std_m) ||
!(config->minimum_horizontal_velocity_std_mps > 0.0) ||
!(config->minimum_vertical_velocity_std_mps > 0.0) ||
!(config->maximum_velocity_std_mps >=
config->minimum_vertical_velocity_std_mps))
{
return false;
}
@@ -97,19 +109,19 @@ bool fusion_gps_adapter_init(fusion_gps_adapter_t* adapter,
return true;
}
static bool sanitize_covariance(const fusion_gps_adapter_t* adapter,
const double input[3][3], double output[9])
static bool sanitize_covariance(
double input[3][3],
double horizontal_std_floor,
double vertical_std_floor,
double maximum_std,
double output[9])
{
double variance_floor[3] = {
adapter->config.minimum_horizontal_std_m *
adapter->config.minimum_horizontal_std_m,
adapter->config.minimum_horizontal_std_m *
adapter->config.minimum_horizontal_std_m,
adapter->config.minimum_vertical_std_m *
adapter->config.minimum_vertical_std_m
horizontal_std_floor * horizontal_std_floor,
horizontal_std_floor * horizontal_std_floor,
vertical_std_floor * vertical_std_floor
};
double variance_ceiling = adapter->config.maximum_position_std_m *
adapter->config.maximum_position_std_m;
double variance_ceiling = maximum_std * maximum_std;
for(unsigned int axis = 0 ; axis < 3u ; axis++)
{
double variance = input[axis][axis];
@@ -124,7 +136,8 @@ static bool sanitize_covariance(const fusion_gps_adapter_t* adapter,
{
for(unsigned int col = row + 1u ; col < 3u ; col++)
{
double covariance = 0.5 * (input[row][col] + input[col][row]);
double covariance = 0.5 *
(input[row][col] + input[col][row]);
if(!isfinite(covariance))
{
return false;
@@ -146,23 +159,32 @@ static bool sanitize_covariance(const fusion_gps_adapter_t* adapter,
(determinant > DBL_EPSILON) && isfinite(determinant);
}
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)
{
if((adapter == NULL) || (gps == NULL) || (position_out == NULL) ||
if((adapter == NULL) || (gps == NULL) || (navigation_out == NULL) ||
!llh_valid(gps->latitude_deg, gps->longitude_deg, gps->height_m))
{
return false;
}
memset(position_out, 0, sizeof(*position_out));
memset(navigation_out, 0, sizeof(*navigation_out));
fusion_position_measurement_t* position_out = &navigation_out->position;
position_out->timestamp_ticks = gps->timestamp_ticks;
position_out->source_valid = gps->solution_valid;
position_out->source_quality =
(isfinite(gps->chi_square_ratio) && (gps->chi_square_ratio >= 0.0))
? 100.0 / (1.0 + gps->chi_square_ratio) : 0.0;
if(!sanitize_covariance(adapter, gps->covariance_enu_m2,
position_out->covariance_enu_m2))
double position_covariance_enu_m2[3][3];
memcpy(position_covariance_enu_m2, gps->covariance_enu_m2,
sizeof(position_covariance_enu_m2));
if(!sanitize_covariance(
position_covariance_enu_m2,
adapter->config.minimum_horizontal_std_m,
adapter->config.minimum_vertical_std_m,
adapter->config.maximum_position_std_m,
position_out->covariance_enu_m2))
{
return false;
}
@@ -189,9 +211,73 @@ bool fusion_gps_adapter_convert(fusion_gps_adapter_t* adapter,
adapter->ecef_to_enu[row][col] * delta_ecef_m[col];
}
}
return isfinite(position_out->position_enu_m[0]) &&
isfinite(position_out->position_enu_m[1]) &&
isfinite(position_out->position_enu_m[2]);
if(!isfinite(position_out->position_enu_m[0]) ||
!isfinite(position_out->position_enu_m[1]) ||
!isfinite(position_out->position_enu_m[2]))
{
return false;
}
if(gps->solution_valid && gps->velocity_valid)
{
bool velocity_finite = true;
for(unsigned int row = 0u; row < 3u; row++)
{
for(unsigned int column = 0u; column < 3u; column++)
{
navigation_out->velocity_enu_mps[row] +=
adapter->ecef_to_enu[row][column] *
gps->velocity_ecef_mps[column];
}
velocity_finite =
velocity_finite &&
isfinite(navigation_out->velocity_enu_mps[row]);
}
double covariance_enu_mps2[3][3] = {{0.0}};
for(unsigned int row = 0u; row < 3u; row++)
{
for(unsigned int column = 0u; column < 3u; column++)
{
for(unsigned int first = 0u; first < 3u; first++)
{
for(unsigned int second = 0u; second < 3u; second++)
{
covariance_enu_mps2[row][column] +=
adapter->ecef_to_enu[row][first] *
gps->velocity_covariance_ecef_mps2[first][second] *
adapter->ecef_to_enu[column][second];
}
}
}
}
navigation_out->velocity_valid =
velocity_finite &&
sanitize_covariance(
covariance_enu_mps2,
adapter->config.minimum_horizontal_velocity_std_mps,
adapter->config.minimum_vertical_velocity_std_mps,
adapter->config.maximum_velocity_std_mps,
navigation_out->velocity_covariance_enu_mps2);
}
return true;
}
bool fusion_gps_adapter_convert(fusion_gps_adapter_t* adapter,
const fusion_gps_measurement_t* gps,
fusion_position_measurement_t* position_out)
{
if(position_out == NULL)
{
return false;
}
fusion_navigation_measurement_t navigation;
if(!fusion_gps_adapter_convert_navigation(adapter, gps, &navigation))
{
return false;
}
*position_out = navigation.position;
return true;
}
bool fusion_gps_adapter_local_to_geodetic(
@@ -90,12 +90,14 @@ bool navigation_fusion_push_gps(const fusion_gps_measurement_t* measurement)
{
return false;
}
fusion_position_measurement_t position;
if(!fusion_gps_adapter_convert(&gps_adapter, measurement, &position))
fusion_navigation_measurement_t navigation;
if(!fusion_gps_adapter_convert_navigation(
&gps_adapter, measurement, &navigation))
{
return false;
}
return fusion_engine_push_position(&fusion_engine, &position, NULL, NULL);
return fusion_engine_push_navigation(
&fusion_engine, &navigation, NULL, NULL, NULL, NULL);
}
bool navigation_fusion_get_output(navigation_fusion_output_t* output)
@@ -111,6 +113,10 @@ bool navigation_fusion_get_output(navigation_fusion_output_t* output)
output->gps_nis_rejected = fusion_engine.diagnostics.position_nis_rejected;
output->gps_position_recoveries =
fusion_engine.diagnostics.position_recovery_count;
output->gps_velocity_accepted =
fusion_engine.diagnostics.velocity_accepted;
output->gps_velocity_nis_rejected =
fusion_engine.diagnostics.velocity_nis_rejected;
output->zero_update_count =
fusion_engine.diagnostics.zero_velocity_update_count;
output->replay_count = fusion_engine.diagnostics.replay_count;
@@ -135,7 +141,10 @@ bool navigation_fusion_get_output(navigation_fusion_output_t* output)
sqrt(fmax(0.0, runtime->filter.covariance[axis][axis]));
}
output->speed_mps = eskf_speed_mps(&runtime->filter);
output->linear_acceleration_norm_mps2 =
runtime->motion.last_linear_accel_norm_mps2;
output->last_position_nis = runtime->filter.last_position_nis;
output->last_velocity_nis = runtime->filter.last_velocity_nis;
output->motion_state = runtime->motion.state;
output->vehicle_at_rest = runtime->vehicle_at_rest;
output->local_valid = true;
@@ -1208,6 +1208,28 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
covariance_between_axes(position_covariance, enu_axes[row], enu_axes[col]);
}
}
double clock_covariance_scale = 1.0;
if(degrees_of_freedom > 0u)
{
clock_covariance_scale =
fmax(1.0, weighted_residual_sum / (double)degrees_of_freedom);
}
solution_out->clock_bias_variance_m2 =
clock_covariance_scale * position_covariance->matrix[3][3];
if(!isfinite(solution_out->clock_bias_variance_m2) ||
(solution_out->clock_bias_variance_m2 < 0.0))
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
"clock-bias covariance rejected variance=%e scale=%e\r\n",
solution_out->clock_bias_variance_m2, clock_covariance_scale);
free_matrix(position_covariance);
free_matrix(A);
free_matrix(posVec);
free(absoluteWeights);
free(phi_lambda_h);
free(topcent);
return false;
}
free_matrix(position_covariance);
free_matrix(A);
@@ -9,6 +9,7 @@
#include <bsp/irq.h>
#include <bsp/mmio.h>
#include <bsp/plic.h>
#include <math.h>
#include <string.h>
typedef enum {
@@ -91,6 +92,12 @@ static gps_external_irq_handler_t gps_external_irq_handler = NULL;
(((uint64_t)GPS_PPS_CLOCK_HZ * GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS) / 1000u)
#define GPS_NAV_HOLDOFF_LOCAL_TICKS \
(((uint64_t)BSP_CLINT_TIMEBASE_HZ * GPS_NAV_EPOCH_HOLDOFF_MS) / 1000u)
#define GPS_TRACK_ESKF_DOPPLER_ALPHA_MIN 0.05
#define GPS_TRACK_ESKF_DOPPLER_ALPHA_MAX 0.40
#define GPS_NAV_TOW_SNAP_TOLERANCE_SEC 0.005
static volatile double gps_tracking_eskf_doppler_alpha =
GPS_TRACK_ESKF_DOPPLER_ALPHA_MIN;
_Static_assert(GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS <
(GPS_NAV_SOLVE_PERIOD_MS / 2u),
@@ -110,6 +117,22 @@ static unsigned int gps_mask_count(uint32_t mask)
return count;
}
void gps_tracking_set_eskf_doppler_alpha(double alpha)
{
if(!isfinite(alpha))
{
return;
}
gps_tracking_eskf_doppler_alpha = fmax(
GPS_TRACK_ESKF_DOPPLER_ALPHA_MIN,
fmin(GPS_TRACK_ESKF_DOPPLER_ALPHA_MAX, alpha));
}
double gps_tracking_get_eskf_doppler_alpha(void)
{
return gps_tracking_eskf_doppler_alpha;
}
static double gps_correct_abs_sample(uint32_t abs_sample, uint32_t rem_code_phase)
{
return (double)abs_sample - ((double)rem_code_phase / CODE_FREQ_BASIS);
@@ -147,6 +170,27 @@ static void gps_update_tracking_measurement_isr(
measurement.cn0_dbhz =
0.2 * measurement.cn0_dbhz +
0.8 * previous->cn0_dbhz;
/*
* The carrier loop's 20 ms snapshots contain several hertz of
* zero-mean jitter. At L1 that is metres/second of range-rate noise,
* which poor satellite geometry amplifies heavily in velocity WLS.
* A 0.05 pole at the 50 Hz navigation-bit cadence has an approximately
* 0.4 s time constant while retaining real receiver dynamics.
*/
measurement.filtered_doppler_hz =
GPS_TRACK_PPS_DOPPLER_ALPHA * measurement.doppler_hz +
(1.0 - GPS_TRACK_PPS_DOPPLER_ALPHA) *
previous->filtered_doppler_hz;
measurement.filtered_pseudorange_rate_mps =
gps_tracking_doppler_to_range_rate_mps(
measurement.filtered_doppler_hz);
double eskf_alpha = gps_tracking_eskf_doppler_alpha;
measurement.eskf_doppler_hz =
eskf_alpha * measurement.doppler_hz +
(1.0 - eskf_alpha) * previous->eskf_doppler_hz;
measurement.eskf_pseudorange_rate_mps =
gps_tracking_doppler_to_range_rate_mps(
measurement.eskf_doppler_hz);
}
measurement.local_timestamp_ticks = local_timestamp_ticks;
gps_latest_tracking_measurement[tracking_index] = measurement;
@@ -1146,7 +1190,7 @@ static bool gps_how_candidate_consistent(
first_raw_sample, first_corrected_sample,
second_week, second_tow_sec,
second_raw_sample, second_corrected_sample,
SAMPLES_PER_CODE * 1000.0,
GPS_RF_SAMPLE_RATE_HZ,
GPS_HOW_CONTINUITY_MAX_GAP_SEC,
GPS_HOW_CONTINUITY_TOLERANCE_SEC);
}
@@ -1373,17 +1417,20 @@ static void gps_process_tracking_nav_solve(const gps_isr_event_t* event)
int tow_week = 0;
double tow_sec = 0.0;
double tow_delta_sec = 0.0;
if(!gps_time_from_sample_anchor(node->gps_how_tow.week,
node->gps_how_tow.sec,
node->gps_how_abs_sample,
node->gps_how_corrected_abs_sample,
event->abs_sample_raw,
event->corrected_abs_sample,
SAMPLES_PER_CODE * 1000.0,
GPS_HOW_ANCHOR_MAX_AGE_SEC,
&tow_week,
&tow_sec,
&tow_delta_sec))
if(!gps_time_from_periodic_sample_anchor(
node->gps_how_tow.week,
node->gps_how_tow.sec,
node->gps_how_abs_sample,
node->gps_how_corrected_abs_sample,
event->abs_sample_raw,
event->corrected_abs_sample,
GPS_RF_SAMPLE_RATE_HZ,
GPS_HOW_ANCHOR_MAX_AGE_SEC,
(double)GPS_NAV_SOLVE_PERIOD_MS / 1000.0,
GPS_NAV_TOW_SNAP_TOLERANCE_SEC,
&tow_week,
&tow_sec,
&tow_delta_sec))
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"nav solve rejected by stale/invalid HOW anchor: trk=%u prn=%u anchor_tow=%.3f anchor_abs=%u event_abs=%u\r\n",
@@ -1897,6 +1944,7 @@ void gps_background_task(void)
gps_process_isr_events();
if(gps_take_timer_maintenance_pending())
{
gps_pps_service();
gps_reconcile_stopped_tracking_channels();
gps_mark_stale_measurements();
gps_check_acq_stuck_busy();
+389 -21
View File
@@ -10,6 +10,7 @@
#include "gps/gps_interrupt_handlers.h"
#include "gps/gps_nav_epoch.h"
#include "gps/gps_pps.h"
#include "gps/gps_time_anchor.h"
#include "gps/defines.h"
#include <bsp/irq.h>
@@ -61,6 +62,31 @@ volatile gps_ephem_ll_t* tracking_node = NULL;
static volatile bool gps_initial_position_valid = false;
static volatile bool gps_ionoutc_valid = false;
static ionoutc_t gps_last_ionoutc;
static gps_pps_quality_selector_t gps_pps_quality_selector;
static gps_pps_profile_t gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
static unsigned int gps_pps_status_log_decimator;
static bool gps_pps_reference_svid_valid;
static unsigned int gps_pps_reference_svid;
static bool gps_pps_filter_reference_svid_valid;
static unsigned int gps_pps_filter_reference_svid;
#if GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES > 0
static uint64_t gps_pps_predictor_ab_update_counter;
#endif
#if !defined(GPS_PPS_STATUS_LOG_INTERVAL) || (GPS_PPS_STATUS_LOG_INTERVAL < 1)
#error "GPS_PPS_STATUS_LOG_INTERVAL must be at least one"
#endif
#if !defined(GPS_PPS_DOPPLER_PREDICTOR_ENABLE) || \
((GPS_PPS_DOPPLER_PREDICTOR_ENABLE != 0) && \
(GPS_PPS_DOPPLER_PREDICTOR_ENABLE != 1))
#error "GPS_PPS_DOPPLER_PREDICTOR_ENABLE must be zero or one"
#endif
#if !defined(GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES) || \
(GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES < 0)
#error "GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES must be zero or positive"
#endif
volatile uint64_t gps_systick_ticks = 0;
@@ -658,6 +684,7 @@ static unsigned int gps_collect_completed_epoch_locked(
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT],
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT],
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT],
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT],
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT],
@@ -690,6 +717,9 @@ static unsigned int gps_collect_completed_epoch_locked(
snapshot_ips[count] = ip_no;
snapshot_pps_counters[count] =
epoch->pps_counter[ip_no];
snapshot_pps_sample_delays[count] =
(double)epoch->raw_sample[ip_no] -
epoch->corrected_abs_sample[ip_no];
snapshot_pps_sequences[count] =
epoch->pps_capture_sequence[ip_no];
snapshot_local_timestamps[count] =
@@ -707,6 +737,7 @@ static unsigned int gps_select_coherent_nav_snapshot(
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT],
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT],
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT],
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT],
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT],
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT],
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT],
@@ -754,6 +785,8 @@ static unsigned int gps_select_coherent_nav_snapshot(
snapshot_ips[destination] = snapshot_ips[source];
snapshot_pps_counters[destination] =
snapshot_pps_counters[source];
snapshot_pps_sample_delays[destination] =
snapshot_pps_sample_delays[source];
snapshot_pps_sequences[destination] =
snapshot_pps_sequences[source];
snapshot_local_timestamps[destination] =
@@ -799,12 +832,15 @@ static uint64_t gps_nav_median_local_timestamp(
static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
const uint64_t pps_counters[GPS_TRCK_IP_COUNT],
const double pps_sample_delays[GPS_TRCK_IP_COUNT],
const uint32_t pps_capture_sequences[GPS_TRCK_IP_COUNT],
const gps_tracking_measurement_t* measurements,
unsigned int satellite_count,
gps_nav_solution_t* solution_out)
{
if((ephemeris == NULL) || (measurements == NULL) ||
if((ephemeris == NULL) || (pps_counters == NULL) ||
(pps_sample_delays == NULL) ||
(pps_capture_sequences == NULL) || (measurements == NULL) ||
(solution_out == NULL) ||
(satellite_count < GPS_NAV_MIN_WLS_SATELLITES))
{
@@ -829,6 +865,14 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
ionoutc_t ionoutc_snapshot;
const ionoutc_t* ionoutc = gps_get_ionoutc(&ionoutc_snapshot) ? &ionoutc_snapshot : NULL;
bool ok = false;
bool pps_clock_bias_drift_valid = false;
double pps_clock_bias_drift_mps = 0.0;
bool pps_velocity_valid = false;
double eskf_doppler_alpha =
gps_tracking_get_eskf_doppler_alpha();
double pps_velocity_speed_mps = NAN;
double pps_velocity_residual_rms_mps = 0.0;
uint32_t pps_velocity_excluded_row_mask = 0u;
gps_nav_solution_t solution;
memset(&solution, 0, sizeof(solution));
memset(solution_out, 0, sizeof(*solution_out));
@@ -866,16 +910,30 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
satellitePositions, velocities);
}
double minimum = psTVLtime[0];
unsigned int minimum_index = 0u;
for(unsigned int i = 1u ; i < satellite_count ; i++)
unsigned int satellite_ids[GPS_TRCK_IP_COUNT];
for(unsigned int i = 0u; i < satellite_count; i++)
{
if(psTVLtime[i] < minimum)
{
minimum = psTVLtime[i];
minimum_index = i;
}
satellite_ids[i] = ephemeris[i].SVID;
}
unsigned int reference_index = 0u;
if(!gps_pps_select_reference(
satellite_ids, psTVLtime, satellite_count,
gps_pps_reference_svid_valid, gps_pps_reference_svid,
&reference_index))
{
goto cleanup;
}
double reference_capture_time = psTVLtime[reference_index];
uint64_t pps_reference_counter = 0u;
double pps_capture_correction_sec = 0.0;
bool pps_reference_counter_valid =
gps_time_correct_capture_counter(
pps_counters[reference_index],
pps_sample_delays[reference_index],
GPS_RF_SAMPLE_RATE_HZ,
(double)GPS_PPS_CLOCK_HZ,
&pps_reference_counter,
&pps_capture_correction_sec);
preciseTxTimes = create_matrix(1, satellite_count, "preciseTxTimes");
localPseudoRanges = create_matrix(1, satellite_count, "localPseudoRanges");
@@ -894,7 +952,8 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
for(unsigned int i = 0 ; i < satellite_count ; i++)
{
preciseTxTimes->matrix[0][i] = psTVLtime[i] - minimum + STARTOFFSET;
preciseTxTimes->matrix[0][i] =
psTVLtime[i] - reference_capture_time + STARTOFFSET;
localPseudoRanges->matrix[0][i] = preciseTxTimes->matrix[0][i] * (SPEED_OF_LIGHT / 1000.0);
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "psTVLtime=%.10lf preciseTx=%.10lf pseudo=%.10lf clockCorr=%.10lf corrected=%.10lf\r\n",
@@ -915,9 +974,20 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
solution.doppler_hz[row] = measurements[row].doppler_hz;
solution.pseudorange_rate_mps[row] =
measurements[row].pseudorange_rate_mps;
solution.filtered_doppler_hz[row] =
measurements[row].filtered_doppler_hz;
solution.filtered_pseudorange_rate_mps[row] =
measurements[row].filtered_pseudorange_rate_mps;
}
if(ok && solution.solution_valid)
{
double reference_noise_ratio =
eskf_doppler_alpha *
(2.0 - GPS_TRACK_PPS_DOPPLER_ALPHA) /
(GPS_TRACK_PPS_DOPPLER_ALPHA *
(2.0 - eskf_doppler_alpha));
double eskf_cn0_penalty_db =
10.0 * log10(fmax(1.0, reference_noise_ratio));
double satellite_positions_array[GPS_TRCK_IP_COUNT][3];
double satellite_velocities_array[GPS_TRCK_IP_COUNT][3];
gps_tracking_measurement_t
@@ -932,12 +1002,18 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
velocities->matrix[row][component];
}
velocity_measurements[row] = measurements[row];
velocity_measurements[row].doppler_hz =
measurements[row].eskf_doppler_hz;
velocity_measurements[row].pseudorange_rate_mps =
measurements[row].eskf_pseudorange_rate_mps;
velocity_measurements[row].cn0_dbhz -=
eskf_cn0_penalty_db;
if((solution.used_row_mask & (1u << row)) == 0u)
{
velocity_measurements[row].valid = false;
}
}
solution.velocity_valid = gps_tracking_velocity_wls(
solution.velocity_valid = gps_tracking_velocity_wls_robust(
solution.ecef_m,
satellite_positions_array,
satellite_velocities_array,
@@ -945,8 +1021,12 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
velocity_measurements,
satellite_count,
solution.velocity_ecef_mps,
solution.velocity_covariance_ecef_mps2,
&solution.clock_drift_mps,
&solution.velocity_residual_rms_mps);
&solution.velocity_residual_rms_mps,
&solution.velocity_excluded_row_mask);
solution.velocity_fde_applied =
solution.velocity_excluded_row_mask != 0u;
if(solution.velocity_valid)
{
solution.speed_mps =
@@ -957,6 +1037,63 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
solution.velocity_ecef_mps[2] *
solution.velocity_ecef_mps[2]);
}
/*
* PPS keeps the existing slow Doppler path as a low-noise derivative
* predictor for the position-WLS reference datum. This result is not
* published to fusion; fusion receives the adaptive low-latency
* solution above.
*/
for(unsigned int row = 0u; row < satellite_count; row++)
{
velocity_measurements[row].cn0_dbhz =
measurements[row].cn0_dbhz;
velocity_measurements[row].doppler_hz =
measurements[row].filtered_doppler_hz;
velocity_measurements[row].pseudorange_rate_mps =
measurements[row].filtered_pseudorange_rate_mps;
}
double pps_velocity_ecef_mps[3];
double pps_velocity_covariance_ecef_mps2[3][3];
double pps_receiver_clock_drift_mps = 0.0;
pps_velocity_valid = gps_tracking_velocity_wls_robust(
solution.ecef_m,
satellite_positions_array,
satellite_velocities_array,
clockDrift,
velocity_measurements,
satellite_count,
pps_velocity_ecef_mps,
pps_velocity_covariance_ecef_mps2,
&pps_receiver_clock_drift_mps,
&pps_velocity_residual_rms_mps,
&pps_velocity_excluded_row_mask);
if(pps_velocity_valid)
{
pps_velocity_speed_mps =
sqrt(pps_velocity_ecef_mps[0] * pps_velocity_ecef_mps[0] +
pps_velocity_ecef_mps[1] * pps_velocity_ecef_mps[1] +
pps_velocity_ecef_mps[2] * pps_velocity_ecef_mps[2]);
pps_clock_bias_drift_valid =
gps_pps_reference_bias_drift(
solution.ecef_m,
pps_velocity_ecef_mps,
satellite_positions_array[reference_index],
satellite_velocities_array[reference_index],
clockDrift[reference_index],
&pps_clock_bias_drift_mps);
}
if(solution.velocity_fde_applied ||
(pps_velocity_excluded_row_mask != 0u))
{
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
"velocity FDE adaptive_mask=0x%02lx pps_mask=0x%02lx "
"adaptive_rms=%.3lf pps_rms=%.3lf\r\n",
(unsigned long)solution.velocity_excluded_row_mask,
(unsigned long)pps_velocity_excluded_row_mask,
solution.velocity_residual_rms_mps,
pps_velocity_residual_rms_mps);
}
}
if(ok && solution.fde_applied)
{
@@ -970,27 +1107,244 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
}
}
bool pps_reference_used = ok &&
((solution.used_row_mask & (1u << minimum_index)) != 0u);
if(ok && solution.solution_valid && pps_reference_used &&
(pps_capture_sequences[minimum_index] != 0u))
((solution.used_row_mask & (1u << reference_index)) != 0u);
if(ok && solution.solution_valid && pps_reference_used)
{
gps_pps_reference_svid = ephemeris[reference_index].SVID;
gps_pps_reference_svid_valid = true;
}
else if(ok && !pps_reference_used)
{
/* Select a new datum on the next WLS solve if FDE rejected this one. */
gps_pps_reference_svid_valid = false;
}
gps_pps_quality_metrics_t pps_quality = {0};
pps_quality.solution_valid = ok && solution.solution_valid;
if(pps_quality.solution_valid)
{
double cn0_sum = 0.0;
double minimum_cn0 = INFINITY;
unsigned int cn0_count = 0u;
for(unsigned int row = 0u; row < satellite_count; row++)
{
if(((solution.used_row_mask & (1u << row)) != 0u) &&
isfinite(solution.cn0_dbhz[row]))
{
cn0_sum += solution.cn0_dbhz[row];
minimum_cn0 = fmin(minimum_cn0, solution.cn0_dbhz[row]);
cn0_count++;
}
}
pps_quality.tdop = solution.tdop;
pps_quality.gdop = solution.gdop;
pps_quality.clock_bias_sigma_m =
sqrt(solution.clock_bias_variance_m2);
pps_quality.mean_cn0_dbhz =
(cn0_count != 0u) ? (cn0_sum / (double)cn0_count) : NAN;
pps_quality.minimum_cn0_dbhz =
(cn0_count != 0u) ? minimum_cn0 : NAN;
pps_quality.chi_square_ratio = solution.chi_square_ratio;
pps_quality.velocity_residual_rms_mps =
pps_velocity_valid ? pps_velocity_residual_rms_mps : 0.0;
pps_quality.used_satellites = solution.used_satellite_count;
pps_quality.residual_test_valid = solution.residual_test_valid;
pps_quality.velocity_valid = pps_velocity_valid;
pps_quality.fde_applied =
solution.fde_applied ||
(pps_velocity_excluded_row_mask != 0u);
}
bool pps_profile_changed = false;
/*
* An invalid WLS epoch carries no usable quality evidence and is not fed
* into the profile selector. It is also never committed below. This
* lets isolated chi-square rejects pass without resetting a converged
* loop; continuous loss is independently caught by gps_pps_service()'s
* two-second reference watchdog.
*/
gps_pps_profile_t pps_profile = gps_pps_quality_selector.profile;
if(pps_quality.solution_valid)
{
pps_profile = gps_pps_quality_selector_update(
&gps_pps_quality_selector, &pps_quality,
&pps_profile_changed);
}
if(pps_profile == GPS_PPS_PROFILE_HOLDOVER)
{
gps_pps_set_holdover(true);
}
gps_pps_profile_t desired_loop_profile =
((pps_profile != GPS_PPS_PROFILE_HOLDOVER) &&
!gps_pps_is_locked()) ?
GPS_PPS_PROFILE_ACQUIRE : pps_profile;
if((desired_loop_profile != GPS_PPS_PROFILE_HOLDOVER) &&
(desired_loop_profile != gps_pps_applied_profile) &&
gps_pps_set_loop_profile(desired_loop_profile))
{
gps_pps_applied_profile = desired_loop_profile;
}
if(pps_profile_changed)
{
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
"PPS quality profile=%s tdop=%.2lf gdop=%.2lf "
"clock_sigma=%.2lf cn0_mean=%.1lf cn0_min=%.1lf "
"chi2_ratio=%.3lf velocity_rms=%.3lf\r\n",
gps_pps_profile_name(pps_profile),
pps_quality.tdop, pps_quality.gdop,
pps_quality.clock_bias_sigma_m,
pps_quality.mean_cn0_dbhz,
pps_quality.minimum_cn0_dbhz,
pps_quality.chi_square_ratio,
pps_quality.velocity_residual_rms_mps);
}
if(ok && solution.solution_valid && pps_reference_used &&
(pps_capture_sequences[reference_index] != 0u) &&
pps_reference_counter_valid &&
(pps_profile != GPS_PPS_PROFILE_HOLDOVER))
{
unsigned int reference_svid = ephemeris[reference_index].SVID;
if(!gps_pps_filter_reference_svid_valid ||
(gps_pps_filter_reference_svid != reference_svid))
{
gps_pps_reset_time_filter();
gps_pps_filter_reference_svid = reference_svid;
gps_pps_filter_reference_svid_valid = true;
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
"PPS WLS datum re-anchor ref_svid=%u\r\n",
reference_svid);
}
double filtered_clock_bias_m = 0.0;
double clock_innovation_m = 0.0;
bool pps_predictor_enabled =
(GPS_PPS_DOPPLER_PREDICTOR_ENABLE != 0) &&
pps_clock_bias_drift_valid;
#if GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES > 0
pps_predictor_enabled =
(((gps_pps_predictor_ab_update_counter /
GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES) & 1u) != 0u) &&
pps_clock_bias_drift_valid;
gps_pps_predictor_ab_update_counter++;
#endif
gps_pps_time_filter_result_t filter_result =
gps_pps_filter_clock_bias(
pps_reference_counter,
solution.clock_bias_m,
pps_predictor_enabled,
pps_clock_bias_drift_mps,
solution.tdop,
&filtered_clock_bias_m,
&clock_innovation_m);
if(filter_result == GPS_PPS_TIME_FILTER_REJECTED)
{
GPS_LOG(DEBUG_EVENTS, GPS_LOG_CAT_NAV,
"PPS clock observation rejected profile=%s bias=%.3lf "
"drift=%.3lf tdop=%.2lf\r\n",
gps_pps_profile_name(pps_profile),
solution.clock_bias_m,
pps_clock_bias_drift_valid ?
pps_clock_bias_drift_mps : NAN,
solution.tdop);
goto pps_anchor_done;
}
/* The synthetic STARTOFFSET datum is absorbed by WLS b. Removing b/c
* recovers the reference satellite's physical flight time; POSTPONE_TOW
* is applied exactly once here, just as it is in satpos(). */
double receiver_tow = ephemeris[minimum_index].tow.sec + POSTPONE_TOW +
(STARTOFFSET / 1000.0) - (solution.clock_bias_m / SPEED_OF_LIGHT);
if(!gps_pps_commit_solution(pps_counters[minimum_index],
ephemeris[minimum_index].tow.week,
receiver_tow))
double receiver_tow = ephemeris[reference_index].tow.sec + POSTPONE_TOW +
(STARTOFFSET / 1000.0) -
(filtered_clock_bias_m / SPEED_OF_LIGHT);
/*
* Keep holdover asserted throughout initial settling and re-anchor
* commits. A coarse commit is accepted while held and clears lock.
* gps_pps_commit_solution() releases holdover immediately before a
* normal mailbox request so the first coherent recovery update is
* applied by PPS v2.2 without stepping the held physical timebase.
*/
if(filter_result == GPS_PPS_TIME_FILTER_COARSE)
{
gps_pps_set_holdover(true);
}
if(!gps_pps_commit_solution(pps_reference_counter,
ephemeris[reference_index].tow.week,
receiver_tow,
filter_result == GPS_PPS_TIME_FILTER_COARSE))
{
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "PPS anchor skipped: mailbox busy or invalid time\r\n");
}
gps_pps_status_log_decimator++;
if(gps_pps_status_log_decimator >= GPS_PPS_STATUS_LOG_INTERVAL)
{
gps_pps_status_log_decimator = 0u;
gps_pps_diagnostics_t pps_diagnostics;
bool pps_diagnostics_valid =
gps_pps_read_diagnostics(&pps_diagnostics);
double hardware_phase_ns = pps_diagnostics_valid ?
((double)pps_diagnostics.phase_error_q32 /
4294967296.0) *
(1000000000.0 / (double)GPS_PPS_CLOCK_HZ) : NAN;
double rate_command_ppm = pps_diagnostics_valid ?
((double)pps_diagnostics.rate_word_q32 /
4294967296.0) * 1000000.0 : NAN;
double integrator_ppm = pps_diagnostics_valid ?
((double)pps_diagnostics.integrator_q32 /
4294967296.0) * 1000000.0 : NAN;
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"PPS profile=%s mode=%s locked=%u tdop=%.2lf gdop=%.2lf "
"clock_sigma=%.2lf cn0=%.1lf/%.1lf innovation=%.2lfm "
"raw_bias=%.3lfm filtered_bias=%.3lfm "
"clock_drift=%.3lfmps bias_drift=%.3lfmps "
"predictor=%u eskf_alpha=%.2lf "
"eskf_v=%.3lf/%.3lf/0x%02lx "
"pps_v=%.3lf/%.3lf/0x%02lx ref_svid=%u "
"capture_corr=%.2lfns "
"hw_phase=%.2lfns rate_cmd=%.3lfppm integ=%.3lfppm "
"pulse=%u seq=%u status=0x%03x\r\n",
gps_pps_profile_name(pps_profile),
(filter_result == GPS_PPS_TIME_FILTER_COARSE) ?
"coarse" : "tracking",
gps_pps_is_locked() ? 1u : 0u,
pps_quality.tdop, pps_quality.gdop,
pps_quality.clock_bias_sigma_m,
pps_quality.mean_cn0_dbhz,
pps_quality.minimum_cn0_dbhz,
clock_innovation_m,
solution.clock_bias_m,
filtered_clock_bias_m,
solution.clock_drift_mps,
pps_clock_bias_drift_mps,
pps_predictor_enabled ? 1u : 0u,
eskf_doppler_alpha,
solution.speed_mps,
solution.velocity_residual_rms_mps,
(unsigned long)solution.velocity_excluded_row_mask,
pps_velocity_speed_mps,
pps_velocity_residual_rms_mps,
(unsigned long)pps_velocity_excluded_row_mask,
ephemeris[reference_index].SVID,
pps_capture_correction_sec * 1.0e9,
hardware_phase_ns, rate_command_ppm, integrator_ppm,
pps_diagnostics_valid ?
pps_diagnostics.pulse_count : 0u,
pps_diagnostics_valid ?
pps_diagnostics.active_sequence : 0u,
pps_diagnostics_valid ?
pps_diagnostics.status : 0u);
}
}
else if(ok && solution.solution_valid && pps_reference_used &&
(pps_capture_sequences[reference_index] != 0u) &&
!pps_reference_counter_valid)
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"PPS anchor rejected: invalid fractional capture correction "
"ref_svid=%u sample_delay=%.9lf\r\n",
ephemeris[reference_index].SVID,
pps_sample_delays[reference_index]);
}
else if(ok && solution.solution_valid && !pps_reference_used)
{
GPS_LOG(DEBUG_WARNING, GPS_LOG_CAT_NAV,
"PPS anchor protected: FDE excluded pseudorange reference row=%u SVID=%u\r\n",
minimum_index, ephemeris[minimum_index].SVID);
reference_index, ephemeris[reference_index].SVID);
}
else if(ok && !solution.solution_valid)
{
@@ -998,6 +1352,7 @@ static bool nav_software_from_snapshot(const gps_ephemeris_t* ephemeris,
"PPS/last_solution protected from invalid best-effort NAVSOL quality=%.1lf ratio=%.3lf\r\n",
solution.quality_score, solution.chi_square_ratio);
}
pps_anchor_done:
cleanup:
if(preciseTxTimes != NULL) free_matrix(preciseTxTimes);
@@ -1139,6 +1494,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
gps_ephemeris_t snapshot[GPS_TRCK_IP_COUNT];
unsigned char snapshot_ips[GPS_TRCK_IP_COUNT];
uint64_t snapshot_pps_counters[GPS_TRCK_IP_COUNT];
double snapshot_pps_sample_delays[GPS_TRCK_IP_COUNT];
uint32_t snapshot_pps_sequences[GPS_TRCK_IP_COUNT];
uint64_t snapshot_local_timestamps[GPS_TRCK_IP_COUNT];
uint32_t snapshot_raw_samples[GPS_TRCK_IP_COUNT];
@@ -1185,6 +1541,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
candidate_count = gps_collect_completed_epoch_locked(
&completed_epoch,
snapshot, snapshot_ips, snapshot_pps_counters,
snapshot_pps_sample_delays,
snapshot_pps_sequences,
snapshot_local_timestamps, snapshot_raw_samples,
snapshot_measurements);
@@ -1210,6 +1567,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
{
satellite_count = gps_select_coherent_nav_snapshot(
snapshot, snapshot_ips, snapshot_pps_counters,
snapshot_pps_sample_delays,
snapshot_pps_sequences,
snapshot_local_timestamps, snapshot_raw_samples,
snapshot_measurements,
@@ -1266,6 +1624,7 @@ bool calculate_position(gps_nav_solution_t* solution_out,
memset(&nav_solution, 0, sizeof(nav_solution));
uint64_t nav_software_start_cycles = gps_read_cycle_counter();
bool nav_ok = nav_software_from_snapshot(snapshot, snapshot_pps_counters,
snapshot_pps_sample_delays,
snapshot_pps_sequences,
snapshot_measurements,
satellite_count,
@@ -1485,6 +1844,15 @@ void gps_startup_initialization(unsigned int nsp_tick)
GPS_NAV_ENABLE_FDE);
gps_init_acq_tables();
gps_pps_init(nsp_tick);
gps_pps_quality_selector_init_period(
&gps_pps_quality_selector,
(double)nsp_tick * (double)GPS_NAV_SOLVE_TIMER_TICK_MS / 1000.0);
gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
gps_pps_status_log_decimator = 0u;
gps_pps_reference_svid_valid = false;
gps_pps_reference_svid = 0u;
gps_pps_filter_reference_svid_valid = false;
gps_pps_filter_reference_svid = 0u;
gps_ionoutc_valid = false;
gps_last_ionoutc = (ionoutc_t){0};
gps_nav_epoch_reject_total = 0u;
+234 -15
View File
@@ -1,4 +1,5 @@
#include "gps/gps_pps.h"
#include "gps/gps_macros.h"
#include <bsp/soc.h>
#include <math.h>
@@ -39,6 +40,7 @@ typedef struct {
volatile uint32_t version; /* 0x7c */
volatile uint32_t kp_q46_hi; /* 0x80 */
volatile uint32_t ki_q46_hi; /* 0x84 */
volatile uint32_t loop_config; /* 0x88, PPS v2.2+ */
} gps_pps_regs_t;
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
@@ -46,23 +48,46 @@ _Static_assert(offsetof(gps_pps_regs_t, capture_counter_lo) == 0x14, "PPS captur
_Static_assert(offsetof(gps_pps_regs_t, epoch_counter_lo) == 0x20, "PPS epoch offset mismatch");
_Static_assert(offsetof(gps_pps_regs_t, commit) == 0x44, "PPS commit offset mismatch");
_Static_assert(offsetof(gps_pps_regs_t, kp_q46_hi) == 0x80, "PPS coefficient offset mismatch");
_Static_assert(offsetof(gps_pps_regs_t, loop_config) == 0x88, "PPS loop-config offset mismatch");
#endif
enum {
GPS_PPS_CONTROL_ENABLE = 1u << 0,
GPS_PPS_CONTROL_TRACK = 1u << 1,
GPS_PPS_CONTROL_HOLDOVER = 1u << 2,
GPS_PPS_CONTROL_CLEAR_STATUS = 1u << 3,
GPS_PPS_STATUS_ALIGNED = 1u << 1,
GPS_PPS_STATUS_COMMIT_BUSY = 1u << 4,
GPS_PPS_STATUS_DIAG_BUSY = 1u << 10,
GPS_PPS_STATUS_LOCKED = 1u << 11,
GPS_PPS_COMMIT_VALID = 1u << 0,
GPS_PPS_COMMIT_COARSE = 1u << 1
};
#define GPS_PPS_VERSION_ADAPTIVE_LOOP 0x00020200u
#define GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
#define GPS_PPS_REFERENCE_TIMEOUT_PERIODS 3u
static volatile gps_pps_regs_t* const gps_pps =
(volatile gps_pps_regs_t*)BSP_GPS_PPS_BASE;
static uint32_t gps_pps_solution_sequence;
static uint32_t gps_pps_version;
static unsigned int gps_pps_nav_solve_tick;
static gps_pps_profile_t gps_pps_active_loop_profile;
static bool gps_pps_force_coarse;
static bool gps_pps_reference_committed;
static bool gps_pps_holdover;
static uint64_t gps_pps_last_reference_counter;
static uint64_t gps_pps_reference_timeout_ticks;
static gps_pps_time_filter_t gps_pps_time_filter;
static uint64_t gps_pps_read_u64(
volatile uint32_t* lo, volatile uint32_t* hi)
{
uint32_t low_word = *lo;
uint32_t high_word = *hi;
return ((uint64_t)high_word << 32) | low_word;
}
static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int64_t value)
{
@@ -71,29 +96,110 @@ static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int6
*hi = (uint32_t)(encoded >> 32);
}
void gps_pps_init(unsigned int nav_solve_tick)
static double gps_pps_update_period_sec(void)
{
const double pi = 3.14159265358979323846;
const double natural_frequency_hz = 0.035;
const double damping = 0.9;
const double update_period_sec = (nav_solve_tick != 0u) ?
((double)nav_solve_tick * 0.020) : 0.200;
const double wn = 2.0 * pi * natural_frequency_hz;
const double q46 = 70368744177664.0;
int64_t kp_q46 = (int64_t)llround((2.0 * damping * wn / GPS_PPS_CLOCK_HZ) * q46);
int64_t ki_q46 = (int64_t)llround((wn * wn * update_period_sec / GPS_PPS_CLOCK_HZ) * q46);
return (gps_pps_nav_solve_tick != 0u) ?
((double)gps_pps_nav_solve_tick * 0.020) :
GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
static bool gps_pps_write_loop_profile(gps_pps_profile_t profile)
{
const double update_period_sec = gps_pps_update_period_sec();
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, update_period_sec);
int64_t kp_q46 = 0;
int64_t ki_q46 = 0;
uint32_t max_slew_q32 = 0u;
if(!gps_pps_loop_coefficients(
parameters, update_period_sec, (double)GPS_PPS_CLOCK_HZ,
&kp_q46, &ki_q46, &max_slew_q32))
{
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"PPS loop coefficient generation failed profile=%s period_ms=%.1lf\r\n",
gps_pps_profile_name(profile), update_period_sec * 1000.0);
return false;
}
gps_pps->control = 0u;
gps_pps_write_q46(&gps_pps->kp_q46_lo, &gps_pps->kp_q46_hi, kp_q46);
gps_pps_write_q46(&gps_pps->ki_q46_lo, &gps_pps->ki_q46_hi, ki_q46);
gps_pps->max_slew_q32 = 85899u; /* 20 ppm of one Q32 tick per local clock. */
gps_pps->max_slew_q32 = max_slew_q32;
if(gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP)
{
gps_pps->loop_config =
((parameters.lock_acquire_ticks & 0xffu) << 8) |
((parameters.lock_release_ticks & 0xffu) << 16) |
((parameters.lock_acquire_updates & 0xffu) << 24);
}
return true;
}
void gps_pps_init(unsigned int nav_solve_tick)
{
gps_pps_nav_solve_tick = nav_solve_tick;
uint64_t period_ticks =
((uint64_t)GPS_PPS_CLOCK_HZ * nav_solve_tick * 20u) / 1000u;
gps_pps_reference_timeout_ticks =
GPS_PPS_REFERENCE_TIMEOUT_PERIODS * period_ticks;
if(gps_pps_reference_timeout_ticks <
(uint64_t)GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS)
{
gps_pps_reference_timeout_ticks =
GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS;
}
gps_pps->control = 0u;
gps_pps_version = gps_pps->version;
(void)gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
gps_pps_active_loop_profile = GPS_PPS_PROFILE_ROBUST;
/* A GDB CPU reset does not reset the tracking-clock PPS state. Continue
* its mailbox sequence and force the first new solution to re-anchor the
* timebase instead of feeding a new epoch into the previous PI state. */
gps_pps_solution_sequence = gps_pps->active_sequence;
gps_pps_diagnostics_t initial_diagnostics;
if(gps_pps_read_diagnostics(&initial_diagnostics))
{
gps_pps_solution_sequence = initial_diagnostics.active_sequence;
}
else
{
gps_pps_solution_sequence = gps_pps->active_sequence;
}
gps_pps_force_coarse = true;
gps_pps_reference_committed = false;
gps_pps_holdover = true;
gps_pps_last_reference_counter = 0u;
gps_pps_time_filter_init(&gps_pps_time_filter);
gps_pps->control = GPS_PPS_CONTROL_ENABLE | GPS_PPS_CONTROL_TRACK |
GPS_PPS_CONTROL_HOLDOVER |
GPS_PPS_CONTROL_CLEAR_STATUS;
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"PPS init rtl=0x%08x adaptive=%u loop_config=0x%08x "
"clock_hz=%u update_ticks=%u reference_timeout_ms=%lu\r\n",
gps_pps_version,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ? 1u : 0u,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ?
gps_pps->loop_config : 0u,
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick,
(unsigned long)((gps_pps_reference_timeout_ticks * 1000u) /
GPS_PPS_CLOCK_HZ));
}
void gps_pps_service(void)
{
if(!gps_pps_reference_committed || gps_pps_holdover)
{
return;
}
/* Reading LO snapshots HI in hardware, so this is a coherent 64-bit
* free-running counter read over the 32-bit APB interface. */
uint32_t now_lo = gps_pps->raw_counter_lo;
uint32_t now_hi = gps_pps->raw_counter_hi;
uint64_t now = ((uint64_t)now_hi << 32) | now_lo;
if((now - gps_pps_last_reference_counter) >
gps_pps_reference_timeout_ticks)
{
gps_pps_set_holdover(true);
}
}
bool gps_pps_is_locked(void)
@@ -101,6 +207,73 @@ bool gps_pps_is_locked(void)
return (gps_pps->status & GPS_PPS_STATUS_LOCKED) != 0u;
}
bool gps_pps_read_diagnostics(gps_pps_diagnostics_t* diagnostics)
{
if((diagnostics == NULL) ||
((gps_pps->status & GPS_PPS_STATUS_DIAG_BUSY) != 0u))
{
return false;
}
gps_pps->diag_snapshot = 1u;
bool diag_started = false;
for(unsigned int attempt = 0u; attempt < 1000u; attempt++)
{
uint32_t status = gps_pps->status;
if((status & GPS_PPS_STATUS_DIAG_BUSY) != 0u)
{
diag_started = true;
}
/*
* The 125 MHz snapshot/ack can complete between two CPU MMIO
* reads. In that legitimate case DIAG_BUSY is never observed.
* Sixteen ordered APB reads are far longer than the request,
* snapshot, and two-flop return path; accepting a clear status
* after that delay still guarantees that the frozen group is new.
*/
else if(diag_started || (attempt >= 16u))
{
diagnostics->status = status;
diagnostics->active_sequence = gps_pps->active_sequence;
diagnostics->pulse_count = gps_pps->pulse_count;
diagnostics->phase_error_q32 = (int64_t)gps_pps_read_u64(
&gps_pps->phase_error_lo, &gps_pps->phase_error_hi);
diagnostics->integrator_q32 = (int64_t)gps_pps_read_u64(
&gps_pps->integrator_lo, &gps_pps->integrator_hi);
diagnostics->rate_word_q32 = (int64_t)gps_pps_read_u64(
&gps_pps->rate_word_lo, &gps_pps->rate_word_hi);
return true;
}
}
return false;
}
bool gps_pps_set_loop_profile(gps_pps_profile_t profile)
{
if((profile == GPS_PPS_PROFILE_HOLDOVER) ||
((gps_pps->status & GPS_PPS_STATUS_COMMIT_BUSY) != 0u))
{
return false;
}
if(!gps_pps_write_loop_profile(profile))
{
return false;
}
gps_pps_active_loop_profile = profile;
return true;
}
void gps_pps_set_holdover(bool holdover)
{
uint32_t control = GPS_PPS_CONTROL_ENABLE | GPS_PPS_CONTROL_TRACK;
if(holdover)
{
control |= GPS_PPS_CONTROL_HOLDOVER;
}
gps_pps->control = control;
gps_pps_holdover = holdover;
}
bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* sequence)
{
if((channel >= BSP_GPS_TRCK_COUNT) || (counter == NULL) || (sequence == NULL))
@@ -123,7 +296,38 @@ bool gps_pps_read_capture(unsigned int channel, uint64_t* counter, uint32_t* seq
return true;
}
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)
{
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(
gps_pps_active_loop_profile, gps_pps_update_period_sec());
return gps_pps_time_filter_update(
&gps_pps_time_filter, epoch_counter, (double)GPS_PPS_CLOCK_HZ,
measured_clock_bias_m, clock_drift_valid,
measured_clock_drift_mps, tdop,
parameters.software_bias_gain,
parameters.software_settle_updates,
filtered_clock_bias_m_out, innovation_m_out);
}
void gps_pps_reset_time_filter(void)
{
gps_pps_time_filter_init(&gps_pps_time_filter);
gps_pps_force_coarse = true;
}
bool gps_pps_commit_solution(
uint64_t epoch_counter,
int gps_week,
double receiver_tow_sec,
bool force_coarse)
{
if(!isfinite(receiver_tow_sec))
{
@@ -160,7 +364,7 @@ bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiv
uint64_t gps_seconds = ((uint64_t)(unsigned int)gps_week * 604800ULL) +
(uint64_t)whole_tow;
uint32_t next_sequence = gps_pps_solution_sequence + 1u;
bool coarse = gps_pps_force_coarse ||
bool coarse = force_coarse || gps_pps_force_coarse ||
((gps_pps->status & GPS_PPS_STATUS_ALIGNED) == 0u);
gps_pps->epoch_counter_lo = (uint32_t)epoch_counter;
@@ -169,9 +373,24 @@ bool gps_pps_commit_solution(uint64_t epoch_counter, int gps_week, double receiv
gps_pps->gps_seconds_hi = (uint32_t)(gps_seconds >> 32);
gps_pps->gps_fraction_q32 = (uint32_t)fraction_q32;
gps_pps->epoch_sequence = next_sequence;
/*
* PPS v2.2 deliberately rejects a normal PI update while HOLDOVER is
* asserted. Release it only after every shadow word is in place and
* immediately before toggling COMMIT, so the first coherent post-gap
* observation is applied instead of merely acknowledged. The physical
* PPS already runs from the held rate word, so this does not introduce a
* phase step; only the validity status precedes the PI apply by the
* bounded mailbox pipeline latency.
*/
if(!coarse && gps_pps_holdover)
{
gps_pps_set_holdover(false);
}
gps_pps->commit = GPS_PPS_COMMIT_VALID |
(coarse ? GPS_PPS_COMMIT_COARSE : 0u);
gps_pps_solution_sequence = next_sequence;
gps_pps_force_coarse = false;
gps_pps_reference_committed = true;
gps_pps_last_reference_counter = epoch_counter;
return true;
}
@@ -0,0 +1,346 @@
#include "gps/gps_pps_quality.h"
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <string.h>
#define GPS_PPS_PROFILE_UPGRADE_UPDATES 8u
#define GPS_PPS_PROFILE_DEGRADE_UPDATES 3u
#define GPS_PPS_MIN_HYSTERESIS_UPDATES 2u
#define GPS_PPS_MIN_LOCK_UPDATES 2u
#define GPS_PPS_MIN_SETTLE_UPDATES 3u
#define GPS_PPS_MAX_CONFIG_UPDATES 255u
static unsigned int gps_pps_period_updates(
unsigned int baseline_updates,
double update_period_sec,
unsigned int minimum_updates)
{
if(!isfinite(update_period_sec) || (update_period_sec <= 0.0))
{
update_period_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
double duration_sec =
(double)baseline_updates * GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
double required = ceil(duration_sec / update_period_sec - 1.0e-12);
if(required < (double)minimum_updates)
{
return minimum_updates;
}
if(required > (double)GPS_PPS_MAX_CONFIG_UPDATES)
{
return GPS_PPS_MAX_CONFIG_UPDATES;
}
return (unsigned int)required;
}
static bool gps_pps_metrics_finite(const gps_pps_quality_metrics_t* metrics)
{
return (metrics != NULL) &&
isfinite(metrics->tdop) &&
isfinite(metrics->gdop) &&
isfinite(metrics->clock_bias_sigma_m) &&
isfinite(metrics->mean_cn0_dbhz) &&
isfinite(metrics->minimum_cn0_dbhz) &&
isfinite(metrics->velocity_residual_rms_mps);
}
static gps_pps_profile_t gps_pps_classify_quality(
const gps_pps_quality_metrics_t* metrics)
{
if(!gps_pps_metrics_finite(metrics) ||
!metrics->solution_valid ||
(metrics->used_satellites < 4u) ||
(metrics->tdop < 0.0) ||
(metrics->gdop < 0.0) ||
(metrics->clock_bias_sigma_m < 0.0))
{
return GPS_PPS_PROFILE_HOLDOVER;
}
bool residual_ok =
metrics->residual_test_valid ?
(isfinite(metrics->chi_square_ratio) &&
(metrics->chi_square_ratio <= 1.0)) :
(metrics->used_satellites == 4u);
bool robust =
residual_ok &&
(metrics->tdop <= 8.0) &&
(metrics->gdop <= 12.0) &&
(metrics->clock_bias_sigma_m <= 100.0) &&
(metrics->mean_cn0_dbhz >= 22.0) &&
(metrics->minimum_cn0_dbhz >= 14.0) &&
(!metrics->velocity_valid ||
(metrics->velocity_residual_rms_mps <= 3.0));
if(!robust)
{
return GPS_PPS_PROFILE_HOLDOVER;
}
bool precision =
(metrics->used_satellites >= 6u) &&
metrics->residual_test_valid &&
!metrics->fde_applied &&
(metrics->tdop <= 3.0) &&
(metrics->gdop <= 6.0) &&
(metrics->clock_bias_sigma_m <= 35.0) &&
(metrics->mean_cn0_dbhz >= 30.0) &&
(metrics->minimum_cn0_dbhz >= 20.0) &&
(metrics->chi_square_ratio <= 0.75) &&
metrics->velocity_valid &&
(metrics->velocity_residual_rms_mps <= 1.0);
return precision ? GPS_PPS_PROFILE_PRECISION : GPS_PPS_PROFILE_ROBUST;
}
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector)
{
gps_pps_quality_selector_init_period(
selector, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec)
{
if(selector != NULL)
{
memset(selector, 0, sizeof(*selector));
selector->profile = GPS_PPS_PROFILE_HOLDOVER;
selector->candidate = GPS_PPS_PROFILE_HOLDOVER;
selector->upgrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_UPGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
selector->degrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_DEGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
}
}
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)
{
if(profile_changed_out != NULL)
{
*profile_changed_out = false;
}
if(selector == NULL)
{
return GPS_PPS_PROFILE_HOLDOVER;
}
gps_pps_profile_t target =
gps_pps_classify_quality(metrics);
if(target == selector->profile)
{
selector->candidate = target;
selector->candidate_updates = 0u;
return selector->profile;
}
if(target != selector->candidate)
{
selector->candidate = target;
selector->candidate_updates = 1u;
}
else if(selector->candidate_updates != UINT_MAX)
{
selector->candidate_updates++;
}
unsigned int required_updates =
(target > selector->profile) ?
selector->upgrade_updates :
selector->degrade_updates;
if(selector->candidate_updates >= required_updates)
{
selector->profile = target;
selector->candidate_updates = 0u;
if(profile_changed_out != NULL)
{
*profile_changed_out = true;
}
}
return selector->profile;
}
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
{
return gps_pps_loop_profile_for_period(
profile, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec)
{
gps_pps_loop_profile_t parameters;
switch(profile)
{
case GPS_PPS_PROFILE_ACQUIRE:
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.120,
.damping = 0.9,
.max_slew_ppm = 20.0,
.lock_acquire_ticks = 8u,
.lock_release_ticks = 128u,
.lock_acquire_updates = 5u,
.software_bias_gain = 1.0 / 16.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_PRECISION:
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.035,
.damping = 0.9,
.max_slew_ppm = 20.0,
.lock_acquire_ticks = 8u,
.lock_release_ticks = 128u,
.lock_acquire_updates = 5u,
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_ROBUST:
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.012,
.damping = 1.0,
.max_slew_ppm = 20.0,
.lock_acquire_ticks = 8u,
.lock_release_ticks = 128u,
.lock_acquire_updates = 8u,
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
case GPS_PPS_PROFILE_HOLDOVER:
default:
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.0,
.damping = 1.0,
.max_slew_ppm = 20.0,
.lock_acquire_ticks = 8u,
.lock_release_ticks = 128u,
.lock_acquire_updates = 8u,
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
}
parameters.lock_acquire_updates = gps_pps_period_updates(
parameters.lock_acquire_updates, update_period_sec,
GPS_PPS_MIN_LOCK_UPDATES);
parameters.software_settle_updates = gps_pps_period_updates(
parameters.software_settle_updates, update_period_sec,
GPS_PPS_MIN_SETTLE_UPDATES);
return parameters;
}
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out)
{
if((kp_q46_out == NULL) || (ki_q46_out == NULL) ||
(max_slew_q32_out == NULL) ||
!isfinite(update_period_sec) || (update_period_sec <= 0.0) ||
!isfinite(clock_frequency_hz) || (clock_frequency_hz <= 0.0) ||
!isfinite(parameters.natural_frequency_hz) ||
(parameters.natural_frequency_hz < 0.0) ||
!isfinite(parameters.damping) || (parameters.damping < 0.0) ||
!isfinite(parameters.max_slew_ppm) ||
(parameters.max_slew_ppm < 0.0))
{
return false;
}
const double pi = 3.14159265358979323846;
const double q46 = 70368744177664.0;
const double q32 = 4294967296.0;
const double baseline_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
/* Use the actual deployed Q2.46 baseline, not its ideal double value, so
* the mapped poles are those of the proven fixed-point implementation. */
const double baseline_kp =
(double)llround(
(2.0 * parameters.damping * wn / clock_frequency_hz) * q46) /
q46;
const double baseline_ki =
(double)llround(
(wn * wn * baseline_sec / clock_frequency_hz) * q46) /
q46;
const double baseline_ticks = clock_frequency_hz * baseline_sec;
const double baseline_sum =
2.0 - baseline_ticks * (baseline_kp + baseline_ki);
const double baseline_product =
1.0 - baseline_ticks * baseline_kp;
const double discriminant =
baseline_sum * baseline_sum - 4.0 * baseline_product;
const double period_ratio = update_period_sec / baseline_sec;
double target_sum = 0.0;
double target_product = 0.0;
if(discriminant >= 0.0)
{
double root = sqrt(discriminant);
double pole_a = 0.5 * (baseline_sum + root);
double pole_b = 0.5 * (baseline_sum - root);
if((pole_a < 0.0) || (pole_b < 0.0))
{
return false;
}
double target_a = pow(pole_a, period_ratio);
double target_b = pow(pole_b, period_ratio);
target_sum = target_a + target_b;
target_product = target_a * target_b;
}
else
{
if(baseline_product <= 0.0)
{
return false;
}
double radius = sqrt(baseline_product);
double cosine = baseline_sum / (2.0 * radius);
cosine = fmax(-1.0, fmin(1.0, cosine));
double angle = acos(cosine);
double target_radius = pow(radius, period_ratio);
target_sum =
2.0 * target_radius * cos(angle * period_ratio);
target_product = target_radius * target_radius;
}
double target_ticks = clock_frequency_hz * update_period_sec;
double kp = (1.0 - target_product) / target_ticks;
double ki = (1.0 - target_sum + target_product) / target_ticks;
if(!isfinite(kp) || !isfinite(ki) || (kp < 0.0) || (ki < 0.0))
{
return false;
}
*kp_q46_out = (int64_t)llround(kp * q46);
*ki_q46_out = (int64_t)llround(ki * q46);
*max_slew_q32_out = (uint32_t)llround(
parameters.max_slew_ppm * 1.0e-6 * q32);
return true;
}
const char* gps_pps_profile_name(gps_pps_profile_t profile)
{
switch(profile)
{
case GPS_PPS_PROFILE_ACQUIRE:
return "acquire";
case GPS_PPS_PROFILE_PRECISION:
return "precision";
case GPS_PPS_PROFILE_ROBUST:
return "robust";
case GPS_PPS_PROFILE_HOLDOVER:
default:
return "holdover";
}
}
@@ -0,0 +1,331 @@
#include "gps/gps_pps_time_filter.h"
#include <math.h>
#include <stddef.h>
#include <string.h>
/*
* Board data showed hundreds of nanoseconds of epoch-to-epoch pseudorange
* clock-bias noise even with seven satellites, while Doppler clock drift was
* stable. A 1/32 residual gain gives roughly a 6.4 s time constant at the
* nominal 5 Hz solve rate. The innovation clamp limits one accepted WLS epoch
* to less than 32 ns of reference movement.
*/
#define GPS_PPS_FILTER_DRIFT_GAIN (1.0 / 4.0)
#define GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN (1.0 / 16.0)
#define GPS_PPS_FILTER_MAX_DRIFT_MPS 10000.0
#define GPS_PPS_FILTER_MAX_TDOP 8.0
#define GPS_PPS_FILTER_MIN_INTERVAL_SEC 0.040
#define GPS_PPS_FILTER_MAX_PREDICTION_GAP_SEC 30.000
#define GPS_PPS_FILTER_INNOVATION_CLAMP_M 300.0
#define GPS_PPS_FILTER_HARD_OUTLIER_M 1500.0
#define GPS_PPS_SPEED_OF_LIGHT_MPS 299792458.0
#define GPS_PPS_FILTER_BASELINE_PERIOD_SEC 0.200
static double clamp_double(double value, double limit)
{
if(value > limit)
{
return limit;
}
if(value < -limit)
{
return -limit;
}
return value;
}
/* Preserve the baseline discrete pole when the navigation interval changes.
* Applying a baseline gain alpha N times is equivalent to one elapsed-time
* update with alpha_T = 1 - (1 - alpha)^N. This also handles an occasional
* missed solve without slowing the clock-bias or drift estimator. */
static double gps_pps_filter_elapsed_gain(
double baseline_gain, double elapsed_sec)
{
double periods = elapsed_sec / GPS_PPS_FILTER_BASELINE_PERIOD_SEC;
if(fabs(periods - 1.0) <= 1.0e-12)
{
return baseline_gain;
}
return -expm1(periods * log1p(-baseline_gain));
}
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter)
{
if(filter != NULL)
{
memset(filter, 0, sizeof(*filter));
}
}
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)
{
if((satellite_ids == NULL) || (capture_times == NULL) ||
(reference_index_out == NULL) || (count == 0u))
{
return false;
}
unsigned int earliest_index = 0u;
if(!isfinite(capture_times[0]))
{
return false;
}
for(unsigned int index = 0u; index < count; index++)
{
if(!isfinite(capture_times[index]))
{
return false;
}
if(preferred_valid && (satellite_ids[index] == preferred_id))
{
*reference_index_out = index;
return true;
}
if(capture_times[index] < capture_times[earliest_index])
{
earliest_index = index;
}
}
*reference_index_out = earliest_index;
return true;
}
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)
{
if((receiver_ecef_m == NULL) ||
(receiver_velocity_ecef_mps == NULL) ||
(reference_satellite_ecef_m == NULL) ||
(reference_satellite_velocity_ecef_mps == NULL) ||
(clock_bias_drift_mps_out == NULL) ||
!isfinite(reference_satellite_clock_drift_sps))
{
return false;
}
double delta_m[3];
double range_squared_m2 = 0.0;
for(unsigned int component = 0u; component < 3u; component++)
{
if(!isfinite(receiver_ecef_m[component]) ||
!isfinite(receiver_velocity_ecef_mps[component]) ||
!isfinite(reference_satellite_ecef_m[component]) ||
!isfinite(reference_satellite_velocity_ecef_mps[component]))
{
return false;
}
delta_m[component] =
reference_satellite_ecef_m[component] -
receiver_ecef_m[component];
range_squared_m2 += delta_m[component] * delta_m[component];
}
double range_m = sqrt(range_squared_m2);
if(!isfinite(range_m) || (range_m < 1.0))
{
return false;
}
double geometric_range_rate_mps = 0.0;
for(unsigned int component = 0u; component < 3u; component++)
{
double relative_velocity_mps =
reference_satellite_velocity_ecef_mps[component] -
receiver_velocity_ecef_mps[component];
geometric_range_rate_mps +=
(delta_m[component] / range_m) *
relative_velocity_mps;
}
/*
* capture_i - min(capture) removes the receiver clock before position
* WLS. The remaining bias datum is -range(reference), with the broadcast
* satellite-clock correction added to the observation.
*/
double bias_drift_mps =
-geometric_range_rate_mps +
GPS_PPS_SPEED_OF_LIGHT_MPS *
reference_satellite_clock_drift_sps;
if(!isfinite(bias_drift_mps))
{
return false;
}
*clock_bias_drift_mps_out = bias_drift_mps;
return true;
}
static gps_pps_time_filter_result_t gps_pps_time_filter_reanchor(
gps_pps_time_filter_t* filter,
uint64_t epoch_counter,
double measured_clock_bias_m,
bool clock_drift_valid,
double measured_clock_drift_mps,
double* filtered_clock_bias_m_out,
double* innovation_m_out)
{
filter->epoch_counter = epoch_counter;
filter->drift_anchor_counter = epoch_counter;
filter->clock_bias_m = measured_clock_bias_m;
filter->clock_drift_mps =
clock_drift_valid ? measured_clock_drift_mps : 0.0;
filter->previous_measurement_m = measured_clock_bias_m;
filter->drift_anchor_measurement_m = measured_clock_bias_m;
filter->accepted_updates = 1u;
filter->initialized = true;
filter->drift_initialized = clock_drift_valid;
*filtered_clock_bias_m_out = measured_clock_bias_m;
*innovation_m_out = 0.0;
return GPS_PPS_TIME_FILTER_COARSE;
}
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)
{
if((filter == NULL) || (filtered_clock_bias_m_out == NULL) ||
(innovation_m_out == NULL) ||
!isfinite(counter_frequency_hz) || (counter_frequency_hz <= 0.0) ||
!isfinite(measured_clock_bias_m) ||
!isfinite(tdop) || (tdop < 0.0) || (tdop > GPS_PPS_FILTER_MAX_TDOP) ||
!isfinite(bias_gain) || (bias_gain <= 0.0) || (bias_gain > 1.0) ||
(settle_updates == 0u) ||
(clock_drift_valid && !isfinite(measured_clock_drift_mps)))
{
return GPS_PPS_TIME_FILTER_REJECTED;
}
if(!filter->initialized)
{
return gps_pps_time_filter_reanchor(
filter, epoch_counter, measured_clock_bias_m,
clock_drift_valid, measured_clock_drift_mps,
filtered_clock_bias_m_out, innovation_m_out);
}
if(epoch_counter <= filter->epoch_counter)
{
return GPS_PPS_TIME_FILTER_REJECTED;
}
uint64_t elapsed_ticks = epoch_counter - filter->epoch_counter;
double elapsed_sec = (double)elapsed_ticks / counter_frequency_hz;
if(!isfinite(elapsed_sec) ||
(elapsed_sec < GPS_PPS_FILTER_MIN_INTERVAL_SEC))
{
return GPS_PPS_TIME_FILTER_REJECTED;
}
/*
* The independent two-second reference watchdog suppresses PPS validity
* during a missing-WLS interval. Do not discard the Doppler prediction
* or force a phase-stepping coarse commit for the shorter scheduling gaps
* observed on the board. Reference-satellite changes are reset by the
* caller; only a genuinely long same-datum gap is re-anchored here.
*/
if(elapsed_sec > GPS_PPS_FILTER_MAX_PREDICTION_GAP_SEC)
{
return gps_pps_time_filter_reanchor(
filter, epoch_counter, measured_clock_bias_m,
clock_drift_valid, measured_clock_drift_mps,
filtered_clock_bias_m_out, innovation_m_out);
}
double measurement_delta_m =
measured_clock_bias_m - filter->previous_measurement_m;
if(!clock_drift_valid &&
(fabs(measurement_delta_m) >
GPS_PPS_FILTER_MAX_DRIFT_MPS * elapsed_sec +
GPS_PPS_FILTER_HARD_OUTLIER_M))
{
return GPS_PPS_TIME_FILTER_REJECTED;
}
double anchor_elapsed_sec =
(double)(epoch_counter - filter->drift_anchor_counter) /
counter_frequency_hz;
double fallback_drift_mps = clamp_double(
(anchor_elapsed_sec > 0.0) ?
((measured_clock_bias_m -
filter->drift_anchor_measurement_m) /
anchor_elapsed_sec) :
(measurement_delta_m / elapsed_sec),
GPS_PPS_FILTER_MAX_DRIFT_MPS);
if(!filter->drift_initialized)
{
/* This happens only while PPS is still coarse. It prevents the
* hundreds-of-metres/second receiver-clock ramp from being mistaken
* for timing phase when Doppler velocity is temporarily absent. */
filter->clock_drift_mps = fallback_drift_mps;
filter->drift_initialized = true;
}
double predicted_clock_bias_m =
filter->clock_bias_m + filter->clock_drift_mps * elapsed_sec;
double innovation_m = measured_clock_bias_m - predicted_clock_bias_m;
if(!isfinite(predicted_clock_bias_m) || !isfinite(innovation_m) ||
(fabs(innovation_m) > GPS_PPS_FILTER_HARD_OUTLIER_M))
{
return GPS_PPS_TIME_FILTER_REJECTED;
}
double effective_bias_gain =
gps_pps_filter_elapsed_gain(bias_gain, elapsed_sec);
double effective_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_DRIFT_GAIN, elapsed_sec);
double effective_fallback_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN, elapsed_sec);
filter->clock_bias_m =
predicted_clock_bias_m +
effective_bias_gain *
clamp_double(innovation_m, GPS_PPS_FILTER_INNOVATION_CLAMP_M);
if(clock_drift_valid)
{
filter->clock_drift_mps +=
effective_drift_gain *
(measured_clock_drift_mps - filter->clock_drift_mps);
}
else
{
if(filter->accepted_updates < settle_updates)
{
/* While PPS output is deliberately invalid/coarse, use the slope
* from the re-anchor point so a single noisy first difference
* cannot leave a long clock-ramp transient. */
filter->clock_drift_mps = fallback_drift_mps;
}
else
{
filter->clock_drift_mps +=
effective_fallback_drift_gain *
(fallback_drift_mps - filter->clock_drift_mps);
}
}
filter->previous_measurement_m = measured_clock_bias_m;
filter->epoch_counter = epoch_counter;
if(filter->accepted_updates != UINT32_MAX)
{
filter->accepted_updates++;
}
*filtered_clock_bias_m_out = filter->clock_bias_m;
*innovation_m_out = innovation_m;
return (filter->accepted_updates < settle_updates) ?
GPS_PPS_TIME_FILTER_COARSE : GPS_PPS_TIME_FILTER_TRACKING;
}
@@ -42,3 +42,13 @@ uint64_t gps_receiver_status_ticks_per_second(void)
{
return GPS_SYSTICK_HZ;
}
void gps_receiver_set_eskf_doppler_alpha(double alpha)
{
gps_tracking_set_eskf_doppler_alpha(alpha);
}
double gps_receiver_get_eskf_doppler_alpha(void)
{
return gps_tracking_get_eskf_doppler_alpha();
}
@@ -109,6 +109,109 @@ bool gps_time_from_sample_anchor(int anchor_week,
return true;
}
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)
{
if((event_week_out == NULL) || (event_tow_sec_out == NULL) ||
!(event_period_sec > 0.0) || !(max_snap_error_sec >= 0.0) ||
!isfinite(event_period_sec) || !isfinite(max_snap_error_sec))
{
return false;
}
int sample_week = 0;
double sample_tow_sec = 0.0;
double sample_elapsed_sec = 0.0;
if(!gps_time_from_sample_anchor(
anchor_week, anchor_tow_sec,
anchor_raw_sample, anchor_corrected_sample,
event_raw_sample, event_corrected_sample,
samples_per_second, max_age_sec,
&sample_week, &sample_tow_sec, &sample_elapsed_sec))
{
return false;
}
(void)sample_week;
(void)sample_tow_sec;
double event_index = round(sample_elapsed_sec / event_period_sec);
double periodic_elapsed_sec = event_index * event_period_sec;
if(!isfinite(event_index) || !isfinite(periodic_elapsed_sec) ||
(event_index < 0.0) ||
(fabs(periodic_elapsed_sec - sample_elapsed_sec) >
max_snap_error_sec))
{
return false;
}
int event_week = anchor_week;
double event_tow_sec = gps_time_normalize_week(
anchor_tow_sec + periodic_elapsed_sec, &event_week);
*event_week_out = event_week;
*event_tow_sec_out = event_tow_sec;
if(elapsed_sec_out != NULL)
{
*elapsed_sec_out = periodic_elapsed_sec;
}
return true;
}
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)
{
if((corrected_counter_out == NULL) ||
!isfinite(fractional_sample_delay) ||
!isfinite(samples_per_second) ||
!isfinite(counter_frequency_hz) ||
(fractional_sample_delay < 0.0) ||
(fractional_sample_delay > 1.0 + 1.0e-6) ||
!(samples_per_second > 0.0) ||
!(counter_frequency_hz > 0.0))
{
return false;
}
double correction_ticks_exact =
fractional_sample_delay *
(counter_frequency_hz / samples_per_second);
if(!isfinite(correction_ticks_exact) ||
(correction_ticks_exact < 0.0) ||
(correction_ticks_exact > (double)INT64_MAX))
{
return false;
}
uint64_t correction_ticks = (uint64_t)llround(correction_ticks_exact);
if(correction_ticks > capture_counter)
{
return false;
}
*corrected_counter_out = capture_counter - correction_ticks;
if(applied_correction_sec_out != NULL)
{
*applied_correction_sec_out =
(double)correction_ticks / counter_frequency_hz;
}
return true;
}
bool gps_time_anchors_consistent(int first_week,
double first_tow_sec,
uint32_t first_raw_sample,
@@ -8,6 +8,8 @@
#define GPS_TRACK_MEASUREMENT_MIN_SATELLITES 4u
#define GPS_TRACK_MEASUREMENT_MAX_SATELLITES GPS_TRCK_IP_COUNT
#define GPS_TRACK_MEASUREMENT_EPSILON 1.0e-12
#define GPS_TRACK_VELOCITY_FDE_MIN_SATELLITES 6u
#define GPS_TRACK_VELOCITY_FDE_SIGMA_GATE 3.5
static double clamp_double(double value, double minimum, double maximum)
{
@@ -135,6 +137,14 @@ bool gps_tracking_measurement_read(
measurement_out->pseudorange_rate_mps =
gps_tracking_doppler_to_range_rate_mps(
measurement_out->doppler_hz);
measurement_out->filtered_doppler_hz =
measurement_out->doppler_hz;
measurement_out->filtered_pseudorange_rate_mps =
measurement_out->pseudorange_rate_mps;
measurement_out->eskf_doppler_hz =
measurement_out->doppler_hz;
measurement_out->eskf_pseudorange_rate_mps =
measurement_out->pseudorange_rate_mps;
unsigned int version =
meta >> GPS_TRCK_MEAS_META_VERSION_SHIFT;
@@ -149,7 +159,11 @@ bool gps_tracking_measurement_read(
(measurement_out->cn0_sample_count == 20u) &&
cn0_valid &&
isfinite(measurement_out->doppler_hz) &&
isfinite(measurement_out->pseudorange_rate_mps);
isfinite(measurement_out->pseudorange_rate_mps) &&
isfinite(measurement_out->eskf_doppler_hz) &&
isfinite(measurement_out->eskf_pseudorange_rate_mps) &&
isfinite(measurement_out->filtered_doppler_hz) &&
isfinite(measurement_out->filtered_pseudorange_rate_mps);
return measurement_out->valid;
}
@@ -232,7 +246,123 @@ static bool solve_four_by_four(
return true;
}
bool gps_tracking_velocity_wls(
static bool invert_four_by_four(double input[4][4], double inverse[4][4])
{
for(unsigned int column = 0u; column < 4u; column++)
{
double unit[4] = {0.0, 0.0, 0.0, 0.0};
double solution[4];
unit[column] = 1.0;
if(!solve_four_by_four(input, unit, solution))
{
return false;
}
for(unsigned int row = 0u; row < 4u; row++)
{
inverse[row][column] = solution[row];
}
}
for(unsigned int row = 0u; row < 4u; row++)
{
for(unsigned int column = row + 1u; column < 4u; column++)
{
double symmetric =
0.5 * (inverse[row][column] + inverse[column][row]);
inverse[row][column] = symmetric;
inverse[column][row] = symmetric;
}
}
return true;
}
typedef struct {
double solution[4];
double normal_inverse[4][4];
double residual_mps[GPS_TRACK_MEASUREMENT_MAX_SATELLITES];
double residual_square_sum;
double weighted_residual_square_sum;
unsigned int row_count;
} gps_velocity_wls_candidate_t;
static bool gps_velocity_solve_candidate(
const double design[][4],
const double observation[],
const double weights[],
unsigned int used,
unsigned int excluded_row,
gps_velocity_wls_candidate_t* candidate_out)
{
if((design == NULL) || (observation == NULL) || (weights == NULL) ||
(candidate_out == NULL) ||
(used < GPS_TRACK_MEASUREMENT_MIN_SATELLITES) ||
(used > GPS_TRACK_MEASUREMENT_MAX_SATELLITES))
{
return false;
}
double normal[4][4] = {{0.0}};
double rhs[4] = {0.0};
unsigned int selected = 0u;
for(unsigned int measurement_row = 0u;
measurement_row < used;
measurement_row++)
{
if(measurement_row == excluded_row)
{
continue;
}
double weight = weights[measurement_row];
for(unsigned int row = 0u; row < 4u; row++)
{
rhs[row] += weight * design[measurement_row][row] *
observation[measurement_row];
for(unsigned int column = 0u; column < 4u; column++)
{
normal[row][column] +=
weight * design[measurement_row][row] *
design[measurement_row][column];
}
}
selected++;
}
if(selected < GPS_TRACK_MEASUREMENT_MIN_SATELLITES)
{
return false;
}
memset(candidate_out, 0, sizeof(*candidate_out));
if(!solve_four_by_four(normal, rhs, candidate_out->solution) ||
!invert_four_by_four(normal, candidate_out->normal_inverse))
{
return false;
}
for(unsigned int measurement_row = 0u;
measurement_row < used;
measurement_row++)
{
candidate_out->residual_mps[measurement_row] = NAN;
if(measurement_row == excluded_row)
{
continue;
}
double predicted = 0.0;
for(unsigned int column = 0u; column < 4u; column++)
{
predicted += design[measurement_row][column] *
candidate_out->solution[column];
}
double residual = observation[measurement_row] - predicted;
candidate_out->residual_mps[measurement_row] = residual;
candidate_out->residual_square_sum += residual * residual;
candidate_out->weighted_residual_square_sum +=
weights[measurement_row] * residual * residual;
}
candidate_out->row_count = selected;
return isfinite(candidate_out->residual_square_sum) &&
isfinite(candidate_out->weighted_residual_square_sum);
}
static bool gps_tracking_velocity_wls_internal(
const double receiver_ecef_m[3],
const double satellite_positions_m[][3],
const double satellite_velocities_mps[][3],
@@ -240,14 +370,22 @@ bool gps_tracking_velocity_wls(
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)
double* residual_rms_mps_out,
bool robust,
uint32_t* excluded_measurement_mask_out)
{
if(excluded_measurement_mask_out != NULL)
{
*excluded_measurement_mask_out = 0u;
}
if((receiver_ecef_m == NULL) || (satellite_positions_m == NULL) ||
(satellite_velocities_mps == NULL) ||
(satellite_clock_drifts_sps == NULL) ||
(measurements == NULL) ||
(receiver_velocity_ecef_mps_out == NULL) ||
(receiver_velocity_covariance_ecef_mps2_out == NULL) ||
(receiver_clock_drift_mps_out == NULL) ||
(residual_rms_mps_out == NULL) ||
(count < GPS_TRACK_MEASUREMENT_MIN_SATELLITES) ||
@@ -256,10 +394,10 @@ bool gps_tracking_velocity_wls(
return false;
}
double normal[4][4] = {{0.0}};
double rhs[4] = {0.0};
double design[GPS_TRACK_MEASUREMENT_MAX_SATELLITES][4];
double observation[GPS_TRACK_MEASUREMENT_MAX_SATELLITES];
double weights[GPS_TRACK_MEASUREMENT_MAX_SATELLITES];
unsigned int measurement_index[GPS_TRACK_MEASUREMENT_MAX_SATELLITES];
unsigned int used = 0u;
for(unsigned int satellite = 0u; satellite < count; satellite++)
@@ -309,17 +447,8 @@ bool gps_tracking_velocity_wls(
30.0 * pow(10.0, -measurement->cn0_dbhz / 20.0);
sigma_rate_mps = clamp_double(sigma_rate_mps, 0.05, 5.0);
double weight = 1.0 / (sigma_rate_mps * sigma_rate_mps);
for(unsigned int row = 0u; row < 4u; row++)
{
rhs[row] +=
weight * design[used][row] * observation[used];
for(unsigned int column = 0u; column < 4u; column++)
{
normal[row][column] +=
weight * design[used][row] *
design[used][column];
}
}
weights[used] = weight;
measurement_index[used] = satellite;
used++;
}
@@ -328,28 +457,124 @@ bool gps_tracking_velocity_wls(
return false;
}
double solution[4];
if(!solve_four_by_four(normal, rhs, solution))
gps_velocity_wls_candidate_t full;
if(!gps_velocity_solve_candidate(
design, observation, weights, used,
GPS_TRACK_MEASUREMENT_MAX_SATELLITES, &full))
{
return false;
}
double residual_square_sum = 0.0;
for(unsigned int row = 0u; row < used; row++)
gps_velocity_wls_candidate_t selected = full;
unsigned int excluded_row = GPS_TRACK_MEASUREMENT_MAX_SATELLITES;
if(robust && (used >= GPS_TRACK_VELOCITY_FDE_MIN_SATELLITES))
{
double predicted = 0.0;
for(unsigned int column = 0u; column < 4u; column++)
double maximum_normalized_residual = 0.0;
for(unsigned int row = 0u; row < used; row++)
{
predicted += design[row][column] * solution[column];
double normalized =
fabs(full.residual_mps[row]) * sqrt(weights[row]);
maximum_normalized_residual =
fmax(maximum_normalized_residual, normalized);
}
double residual = observation[row] - predicted;
residual_square_sum += residual * residual;
if(maximum_normalized_residual > GPS_TRACK_VELOCITY_FDE_SIGMA_GATE)
{
double best_score = INFINITY;
for(unsigned int trial_row = 0u; trial_row < used; trial_row++)
{
gps_velocity_wls_candidate_t trial;
if(gps_velocity_solve_candidate(
design, observation, weights, used, trial_row, &trial) &&
(trial.weighted_residual_square_sum < best_score))
{
best_score = trial.weighted_residual_square_sum;
selected = trial;
excluded_row = trial_row;
}
}
}
}
if((excluded_row < used) && (excluded_measurement_mask_out != NULL))
{
*excluded_measurement_mask_out =
1u << measurement_index[excluded_row];
}
memcpy(receiver_velocity_ecef_mps_out, solution,
3u * sizeof(solution[0]));
*receiver_clock_drift_mps_out = solution[3];
/*
* The C/N0 model supplies absolute range-rate sigmas. Inflate the formal
* inverse-normal covariance when the post-fit residuals are larger than
* that model predicts; never shrink it from a fortuitously quiet epoch.
* Four-satellite epochs have zero redundancy, so retain scale 1 and let
* the poor geometry appear directly in the inverse normal matrix.
*/
double covariance_scale = 1.0;
if(selected.row_count > 4u)
{
covariance_scale = fmax(
1.0,
selected.weighted_residual_square_sum /
(double)(selected.row_count - 4u));
}
for(unsigned int row = 0u; row < 3u; row++)
{
for(unsigned int column = 0u; column < 3u; column++)
{
receiver_velocity_covariance_ecef_mps2_out[row][column] =
covariance_scale * selected.normal_inverse[row][column];
if(!isfinite(
receiver_velocity_covariance_ecef_mps2_out[row][column]))
{
return false;
}
}
}
memcpy(receiver_velocity_ecef_mps_out, selected.solution,
3u * sizeof(selected.solution[0]));
*receiver_clock_drift_mps_out = selected.solution[3];
*residual_rms_mps_out =
sqrt(residual_square_sum / (double)used);
sqrt(selected.residual_square_sum / (double)selected.row_count);
return isfinite(*residual_rms_mps_out);
}
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)
{
return gps_tracking_velocity_wls_internal(
receiver_ecef_m, satellite_positions_m,
satellite_velocities_mps, satellite_clock_drifts_sps,
measurements, count, receiver_velocity_ecef_mps_out,
receiver_velocity_covariance_ecef_mps2_out,
receiver_clock_drift_mps_out, residual_rms_mps_out,
false, NULL);
}
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)
{
return gps_tracking_velocity_wls_internal(
receiver_ecef_m, satellite_positions_m,
satellite_velocities_mps, satellite_clock_drifts_sps,
measurements, count, receiver_velocity_ecef_mps_out,
receiver_velocity_covariance_ecef_mps2_out,
receiver_clock_drift_mps_out, residual_rms_mps_out,
true, excluded_measurement_mask_out);
}
+52 -1
View File
@@ -8,6 +8,9 @@ TARGETS := test_imu_calibration test_gps_nav_epoch \
test_gps_ephemeris_cache test_gps_receiver_hardware_sim
TARGETS += test_gps_tracking_measurement
TARGETS += test_gps_satellite_velocity
TARGETS += test_gps_pps_time_filter
TARGETS += test_gps_pps_quality
TARGETS += test_gps_pps_adaptive_loop
.PHONY: all run sanitize clean
@@ -50,6 +53,19 @@ test_gps_satellite_velocity: test_gps_satellite_velocity.c \
-Wno-unused-variable -Wno-sign-compare \
-Ifake_bsp -I$(ROOT)/header $^ -lm -o $@
test_gps_pps_time_filter: test_gps_pps_time_filter.c \
$(ROOT)/src/gps/gps_pps_time_filter.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_gps_pps_quality: test_gps_pps_quality.c \
$(ROOT)/src/gps/gps_pps_quality.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
test_gps_pps_adaptive_loop: test_gps_pps_adaptive_loop.c \
$(ROOT)/src/gps/gps_pps_time_filter.c \
$(ROOT)/src/gps/gps_pps_quality.c
$(CC) $(CFLAGS) -I$(ROOT)/header $^ -lm -o $@
run: $(TARGETS)
./test_imu_calibration
./test_gps_nav_epoch
@@ -61,6 +77,9 @@ run: $(TARGETS)
./test_gps_receiver_hardware_sim
./test_gps_tracking_measurement
./test_gps_satellite_velocity
./test_gps_pps_time_filter
./test_gps_pps_quality
./test_gps_pps_adaptive_loop
sanitize:
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
@@ -77,6 +96,13 @@ sanitize:
-o test_gps_nav_epoch_coalescing_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_nav_epoch_coalescing_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_time_anchor.c \
$(ROOT)/src/gps/gps_time_anchor.c -lm \
-o test_gps_time_anchor_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_time_anchor_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_acq_manager.c \
@@ -122,11 +148,36 @@ sanitize:
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 \
./test_gps_satellite_velocity_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_pps_time_filter.c \
$(ROOT)/src/gps/gps_pps_time_filter.c -lm \
-o test_gps_pps_time_filter_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_time_filter_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_pps_quality.c \
$(ROOT)/src/gps/gps_pps_quality.c -lm \
-o test_gps_pps_quality_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_quality_san
$(CC) -O1 -g -std=c11 -Wall -Wextra -Werror \
-fsanitize=address,undefined -fno-omit-frame-pointer \
-I$(ROOT)/header test_gps_pps_adaptive_loop.c \
$(ROOT)/src/gps/gps_pps_time_filter.c \
$(ROOT)/src/gps/gps_pps_quality.c -lm \
-o test_gps_pps_adaptive_loop_san
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 ./test_gps_pps_adaptive_loop_san
clean:
rm -f $(TARGETS) test_gps_nav_epoch_coalescing_repro \
test_gps_nav_epoch_san test_gps_nav_epoch_coalescing_san \
test_gps_time_anchor_san \
test_gps_acq_manager_san test_gps_ephemeris_cache_san \
test_gps_receiver_hardware_sim_san \
test_gps_tracking_measurement_san \
test_gps_satellite_velocity_san
test_gps_satellite_velocity_san \
test_gps_pps_time_filter_san test_gps_pps_quality_san \
test_gps_pps_adaptive_loop_san
@@ -0,0 +1,539 @@
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gps/gps_pps_quality.h"
#include "gps/gps_pps_time_filter.h"
#define TRACKING_HZ 125000000.0
#define UPDATE_PERIOD_SEC 0.2
#define SPEED_OF_LIGHT_MPS 299792458.0
#define Q32_SCALE 4294967296.0
#define Q46_SCALE 70368744177664.0
typedef struct {
int64_t phase_q32;
int64_t integrator_q32;
int64_t rate_q32;
bool aligned;
bool locked;
unsigned int good_updates;
} synthetic_rtl_t;
typedef struct {
uint32_t state;
} synthetic_rng_t;
typedef enum {
SYNTHETIC_STEADY = 0,
SYNTHETIC_STARTUP_NOISY_THEN_CLEAN,
SYNTHETIC_NOISE_BURSTS,
SYNTHETIC_SATELLITE_LOSS,
SYNTHETIC_DOPPLER_LOSS,
SYNTHETIC_GEOMETRY_DEGRADE,
SYNTHETIC_REFERENCE_RANGE_RATE
} synthetic_pattern_t;
static double synthetic_uniform(synthetic_rng_t* rng)
{
rng->state = rng->state * 1664525u + 1013904223u;
return ((double)(rng->state >> 8) + 0.5) / 16777216.0;
}
static double synthetic_normal(synthetic_rng_t* rng)
{
double sum = 0.0;
for(unsigned int sample = 0u; sample < 12u; sample++)
{
sum += synthetic_uniform(rng);
}
return sum - 6.0;
}
static int64_t clamp_i128(__int128 value, int64_t limit)
{
if(value > limit)
{
return limit;
}
if(value < -(__int128)limit)
{
return -limit;
}
return (int64_t)value;
}
static int64_t seconds_to_phase_q32(double seconds)
{
return (int64_t)llround(seconds * TRACKING_HZ * Q32_SCALE);
}
static double phase_q32_to_seconds(int64_t phase_q32)
{
return (double)phase_q32 / (TRACKING_HZ * Q32_SCALE);
}
static void synthetic_rtl_advance(synthetic_rtl_t* rtl, double oscillator_ppm)
{
int64_t raw_ticks = (int64_t)llround(UPDATE_PERIOD_SEC * TRACKING_HZ);
int64_t oscillator_q32 = (int64_t)llround(
oscillator_ppm * 1.0e-6 * Q32_SCALE);
rtl->phase_q32 += raw_ticks * (oscillator_q32 + rtl->rate_q32);
}
static void profile_coefficients(
gps_pps_loop_profile_t profile,
int64_t* kp_q46,
int64_t* ki_q46,
int64_t* max_slew_q32)
{
uint32_t max_slew = 0u;
assert(gps_pps_loop_coefficients(
profile, UPDATE_PERIOD_SEC, TRACKING_HZ,
kp_q46, ki_q46, &max_slew));
*max_slew_q32 = max_slew;
}
static void synthetic_rtl_coarse(synthetic_rtl_t* rtl, int64_t reference_q32)
{
bool was_locked = rtl->locked;
memset(rtl, 0, sizeof(*rtl));
rtl->phase_q32 = reference_q32;
rtl->aligned = true;
assert(!was_locked || !rtl->locked);
}
static void synthetic_rtl_commit(
synthetic_rtl_t* rtl,
int64_t reference_q32,
gps_pps_loop_profile_t profile)
{
int64_t raw_phase_q32 = reference_q32 - rtl->phase_q32;
int64_t kp_q46;
int64_t ki_q46;
int64_t max_slew_q32;
profile_coefficients(profile, &kp_q46, &ki_q46, &max_slew_q32);
int64_t proportional_q32 =
(int64_t)(((__int128)raw_phase_q32 * kp_q46) >> 46);
__int128 integrator_candidate =
(__int128)rtl->integrator_q32 +
(((__int128)raw_phase_q32 * ki_q46) >> 46);
rtl->integrator_q32 =
clamp_i128(integrator_candidate, max_slew_q32);
rtl->rate_q32 = clamp_i128(
(__int128)rtl->integrator_q32 + proportional_q32,
max_slew_q32);
int64_t acquire_q32 =
(int64_t)profile.lock_acquire_ticks << 32;
int64_t release_q32 =
(int64_t)profile.lock_release_ticks << 32;
if(llabs(raw_phase_q32) > release_q32)
{
rtl->locked = false;
rtl->good_updates = 0u;
}
else if(!rtl->locked)
{
if(llabs(raw_phase_q32) <= acquire_q32)
{
rtl->good_updates++;
if(rtl->good_updates >= profile.lock_acquire_updates)
{
rtl->locked = true;
rtl->good_updates = 0u;
}
}
else
{
rtl->good_updates = 0u;
}
}
}
static gps_pps_quality_metrics_t precision_quality(void)
{
return (gps_pps_quality_metrics_t){
.tdop = 1.7,
.gdop = 3.2,
.clock_bias_sigma_m = 18.0,
.mean_cn0_dbhz = 38.0,
.minimum_cn0_dbhz = 27.0,
.chi_square_ratio = 0.35,
.velocity_residual_rms_mps = 0.25,
.used_satellites = 7u,
.solution_valid = true,
.residual_test_valid = true,
.velocity_valid = true,
.fde_applied = false
};
}
static gps_pps_quality_metrics_t robust_quality(bool velocity_valid)
{
gps_pps_quality_metrics_t quality = {
.tdop = 5.0,
.gdop = 8.0,
.clock_bias_sigma_m = 65.0,
.mean_cn0_dbhz = 27.0,
.minimum_cn0_dbhz = 18.0,
.chi_square_ratio = 0.85,
.velocity_residual_rms_mps = velocity_valid ? 1.8 : 0.0,
.used_satellites = 5u,
.solution_valid = true,
.residual_test_valid = true,
.velocity_valid = velocity_valid,
.fde_applied = false
};
return quality;
}
static void run_noisy_scenario(
unsigned int seed,
bool with_gap,
double oscillator_ppm,
double noise_sigma_ns,
bool clock_drift_valid,
bool use_robust_quality,
bool hard_outliers,
unsigned int update_count,
synthetic_pattern_t pattern,
double* first_lock_sec_out,
double* one_second_jitter_rms_sec_out,
unsigned int* coarse_count_out,
unsigned int* rejected_count_out,
bool* locked_after_recovery_out)
{
assert(update_count <= 1500u);
synthetic_rng_t rng = {.state = seed};
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init(&selector);
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
synthetic_rtl_t rtl = {0};
gps_pps_profile_t quality_profile = GPS_PPS_PROFILE_HOLDOVER;
double first_lock_sec = NAN;
unsigned int coarse_count = 0u;
unsigned int rejected_count = 0u;
double phase_history[1500] = {0.0};
bool lock_history[1500] = {false};
for(unsigned int update = 0u; update < update_count; update++)
{
double time_sec = UPDATE_PERIOD_SEC * (double)update;
synthetic_rtl_advance(&rtl, oscillator_ppm);
phase_history[update] = phase_q32_to_seconds(rtl.phase_q32);
lock_history[update] = rtl.locked;
if(with_gap && (time_sec >= 35.0) && (time_sec < 50.0))
{
/* The firmware reference watchdog deasserts PPS validity after
* two seconds without a successful commit. */
if(time_sec >= 37.0)
{
lock_history[update] = false;
}
continue;
}
bool sample_drift_valid = clock_drift_valid;
bool sample_robust_quality = use_robust_quality;
bool sample_solution_valid = true;
double sample_noise_sigma_ns = noise_sigma_ns;
double reference_range_rate_mps = 0.0;
switch(pattern)
{
case SYNTHETIC_STARTUP_NOISY_THEN_CLEAN:
sample_robust_quality = time_sec < 55.0;
sample_noise_sigma_ns =
(time_sec < 55.0) ? 1100.0 : 120.0;
break;
case SYNTHETIC_NOISE_BURSTS:
{
bool burst =
((time_sec >= 55.0) && (time_sec < 78.0)) ||
((time_sec >= 135.0) && (time_sec < 151.0)) ||
((time_sec >= 205.0) && (time_sec < 214.0));
sample_robust_quality = burst;
sample_noise_sigma_ns = burst ? 900.0 : 150.0;
break;
}
case SYNTHETIC_SATELLITE_LOSS:
sample_solution_valid =
!(((time_sec >= 55.0) && (time_sec < 78.0)) ||
((time_sec >= 145.0) && (time_sec < 151.0)));
break;
case SYNTHETIC_DOPPLER_LOSS:
sample_drift_valid =
!((time_sec >= 65.0) && (time_sec < 105.0));
break;
case SYNTHETIC_GEOMETRY_DEGRADE:
sample_robust_quality =
(time_sec >= 60.0) && (time_sec < 145.0);
sample_noise_sigma_ns =
sample_robust_quality ? 650.0 : 200.0;
break;
case SYNTHETIC_REFERENCE_RANGE_RATE:
reference_range_rate_mps = 130.0;
break;
case SYNTHETIC_STEADY:
default:
break;
}
bool changed = false;
gps_pps_quality_metrics_t quality = sample_robust_quality ?
robust_quality(sample_drift_valid) : precision_quality();
quality.velocity_valid = sample_drift_valid;
if(!sample_solution_valid)
{
quality.solution_valid = false;
quality.used_satellites = 2u;
quality.velocity_valid = false;
}
quality_profile = gps_pps_quality_selector_update(
&selector, &quality, &changed);
if(quality_profile == GPS_PPS_PROFILE_HOLDOVER)
{
lock_history[update] = false;
continue;
}
gps_pps_profile_t loop_profile =
rtl.locked ? quality_profile : GPS_PPS_PROFILE_ACQUIRE;
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile(loop_profile);
double true_bias_m =
1200.0 +
(-reference_range_rate_mps) * time_sec;
double noise_sec =
synthetic_normal(&rng) * sample_noise_sigma_ns * 1.0e-9;
if((update % 37u) == 0u)
{
noise_sec +=
(synthetic_uniform(&rng) < 0.5 ? -1.0 : 1.0) *
(hard_outliers ? 6.0e-6 : 900.0e-9);
}
double measured_bias_m =
true_bias_m - noise_sec * SPEED_OF_LIGHT_MPS;
double filtered_bias_m = 0.0;
double innovation_m = 0.0;
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
&filter,
(uint64_t)llround(time_sec * TRACKING_HZ),
TRACKING_HZ,
measured_bias_m,
sample_drift_valid,
-reference_range_rate_mps +
synthetic_normal(&rng) * 0.25,
quality.tdop,
parameters.software_bias_gain,
parameters.software_settle_updates,
&filtered_bias_m,
&innovation_m);
if(result == GPS_PPS_TIME_FILTER_REJECTED)
{
rejected_count++;
continue;
}
assert(isfinite(innovation_m));
int64_t reference_q32 = seconds_to_phase_q32(
-(filtered_bias_m - true_bias_m) / SPEED_OF_LIGHT_MPS);
if((result == GPS_PPS_TIME_FILTER_COARSE) || !rtl.aligned)
{
synthetic_rtl_coarse(&rtl, reference_q32);
coarse_count++;
}
else
{
synthetic_rtl_commit(&rtl, reference_q32, parameters);
}
if(rtl.locked && !isfinite(first_lock_sec))
{
first_lock_sec = time_sec;
}
phase_history[update] = phase_q32_to_seconds(rtl.phase_q32);
lock_history[update] = rtl.locked;
}
double jitter_square_sum = 0.0;
unsigned int jitter_count = 0u;
for(unsigned int update = (update_count / 2u) + 5u;
update < update_count;
update++)
{
if(lock_history[update] && lock_history[update - 5u])
{
double difference =
phase_history[update] - phase_history[update - 5u];
jitter_square_sum += difference * difference;
jitter_count++;
}
}
*first_lock_sec_out = first_lock_sec;
*one_second_jitter_rms_sec_out =
(jitter_count != 0u) ?
sqrt(jitter_square_sum / (double)jitter_count) : NAN;
*coarse_count_out = coarse_count;
*rejected_count_out = rejected_count;
*locked_after_recovery_out = lock_history[update_count - 1u];
}
static void test_noisy_fixed_point_loop(void)
{
for(unsigned int seed = 1u; seed <= 20u; seed++)
{
double first_lock_sec;
double jitter_rms_sec;
unsigned int coarse_count;
unsigned int rejected_count;
bool locked_after_recovery;
run_noisy_scenario(
seed, false, 1.65, 320.0, true, false, false, 600u,
SYNTHETIC_STEADY,
&first_lock_sec, &jitter_rms_sec, &coarse_count,
&rejected_count, &locked_after_recovery);
assert(isfinite(first_lock_sec));
assert(first_lock_sec < 15.0);
assert(jitter_rms_sec < 20.0e-9);
assert(coarse_count >= 10u);
assert(rejected_count == 0u);
assert(locked_after_recovery);
}
}
static void test_missing_wls_holds_and_resumes_without_reanchor(void)
{
double first_lock_sec;
double jitter_rms_sec;
unsigned int coarse_count;
unsigned int rejected_count;
bool locked_after_recovery;
run_noisy_scenario(
31u, true, 1.65, 320.0, true, false, false, 600u,
SYNTHETIC_STEADY,
&first_lock_sec, &jitter_rms_sec, &coarse_count,
&rejected_count, &locked_after_recovery);
assert(first_lock_sec < 15.0);
/* Initial settling emits repeated coarse commits; the bounded 15-second
* gap must not add another reset of the already converged PI state. */
assert(coarse_count == 11u);
assert(rejected_count == 0u);
assert(locked_after_recovery);
assert(jitter_rms_sec < 20.0e-9);
}
static void test_embedded_corner_case_matrix(void)
{
for(unsigned int scenario = 0u; scenario < 4u; scenario++)
{
double first_lock_sec;
double jitter_rms_sec;
unsigned int coarse_count;
unsigned int rejected_count;
bool locked_after_recovery;
bool no_doppler = scenario == 1u;
bool hard_outliers = scenario == 2u;
double oscillator_ppm = (scenario == 3u) ? 18.0 : 1.65;
double noise_sigma_ns = (scenario == 0u) ? 650.0 : 320.0;
run_noisy_scenario(
100u + scenario,
false,
oscillator_ppm,
noise_sigma_ns,
!no_doppler,
(scenario == 0u) || no_doppler,
hard_outliers,
1200u,
SYNTHETIC_STEADY,
&first_lock_sec,
&jitter_rms_sec,
&coarse_count,
&rejected_count,
&locked_after_recovery);
assert(isfinite(first_lock_sec));
assert(first_lock_sec < 35.0);
assert(isfinite(jitter_rms_sec));
assert(jitter_rms_sec < 20.0e-9);
assert(coarse_count >= 10u);
assert(hard_outliers ? (rejected_count > 0u) :
(rejected_count == 0u));
assert(locked_after_recovery);
}
double first_lock_sec;
double jitter_rms_sec;
unsigned int coarse_count;
unsigned int rejected_count;
bool locked_after_recovery;
run_noisy_scenario(
200u, false, 25.0, 50.0, true, false, false, 600u,
SYNTHETIC_STEADY,
&first_lock_sec, &jitter_rms_sec, &coarse_count,
&rejected_count, &locked_after_recovery);
assert(!isfinite(first_lock_sec));
assert(!isfinite(jitter_rms_sec));
assert(!locked_after_recovery);
}
static void test_time_varying_environment_matrix(void)
{
const synthetic_pattern_t patterns[] = {
SYNTHETIC_STARTUP_NOISY_THEN_CLEAN,
SYNTHETIC_NOISE_BURSTS,
SYNTHETIC_SATELLITE_LOSS,
SYNTHETIC_DOPPLER_LOSS,
SYNTHETIC_GEOMETRY_DEGRADE,
SYNTHETIC_REFERENCE_RANGE_RATE
};
for(unsigned int pattern_index = 0u;
pattern_index < (sizeof(patterns) / sizeof(patterns[0]));
pattern_index++)
{
for(unsigned int seed = 1u; seed <= 12u; seed++)
{
double first_lock_sec;
double jitter_rms_sec;
unsigned int coarse_count;
unsigned int rejected_count;
bool locked_after_recovery;
run_noisy_scenario(
300u + 31u * pattern_index + seed,
false,
(patterns[pattern_index] == SYNTHETIC_NOISE_BURSTS) ?
-2.5 : 1.65,
320.0,
true,
false,
false,
1500u,
patterns[pattern_index],
&first_lock_sec,
&jitter_rms_sec,
&coarse_count,
&rejected_count,
&locked_after_recovery);
assert(isfinite(first_lock_sec));
assert(first_lock_sec < 25.0);
assert(isfinite(jitter_rms_sec));
assert(jitter_rms_sec < 20.0e-9);
assert(coarse_count >= 10u);
assert(rejected_count == 0u);
assert(locked_after_recovery);
}
}
}
int main(void)
{
test_noisy_fixed_point_loop();
test_missing_wls_holds_and_resumes_without_reanchor();
test_embedded_corner_case_matrix();
test_time_varying_environment_matrix();
puts("gps_pps_adaptive_loop tests passed");
return 0;
}
@@ -0,0 +1,238 @@
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include "gps/gps_pps_quality.h"
static gps_pps_quality_metrics_t precision_metrics(void)
{
return (gps_pps_quality_metrics_t){
.tdop = 1.7,
.gdop = 3.2,
.clock_bias_sigma_m = 18.0,
.mean_cn0_dbhz = 38.0,
.minimum_cn0_dbhz = 29.0,
.chi_square_ratio = 0.35,
.velocity_residual_rms_mps = 0.25,
.used_satellites = 7u,
.solution_valid = true,
.residual_test_valid = true,
.velocity_valid = true,
.fde_applied = false
};
}
static void test_hysteretic_profile_transitions(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init(&selector);
gps_pps_quality_metrics_t metrics = precision_metrics();
bool changed = false;
for(unsigned int update = 0u; update < 7u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
assert(!changed);
}
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
assert(changed);
/* One DOP/CN0 excursion must not chatter the active loop profile. */
metrics.tdop = 4.5;
metrics.gdop = 7.0;
metrics.mean_cn0_dbhz = 27.0;
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
assert(!changed);
metrics = precision_metrics();
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
/* Sustained FDE operation degrades to the robust profile. */
metrics.fde_applied = true;
for(unsigned int update = 0u; update < 2u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
}
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
assert(changed);
/* One invalid WLS epoch is skipped; sustained invalidity holds over. */
metrics.solution_valid = false;
for(unsigned int update = 0u; update < 2u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
assert(!changed);
}
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
assert(changed);
}
static void test_alternating_quality_does_not_upgrade(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init(&selector);
gps_pps_quality_metrics_t good = precision_metrics();
gps_pps_quality_metrics_t poor = good;
poor.tdop = 9.0;
for(unsigned int update = 0u; update < 40u; update++)
{
const gps_pps_quality_metrics_t* metrics =
(update & 1u) ? &poor : &good;
bool changed = false;
assert(gps_pps_quality_selector_update(
&selector, metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
assert(!changed);
}
}
static void test_profile_parameters_are_ordered(void)
{
gps_pps_loop_profile_t robust =
gps_pps_loop_profile(GPS_PPS_PROFILE_ROBUST);
gps_pps_loop_profile_t precision =
gps_pps_loop_profile(GPS_PPS_PROFILE_PRECISION);
gps_pps_loop_profile_t acquire =
gps_pps_loop_profile(GPS_PPS_PROFILE_ACQUIRE);
assert(robust.natural_frequency_hz < precision.natural_frequency_hz);
assert(precision.natural_frequency_hz < acquire.natural_frequency_hz);
assert(robust.lock_acquire_updates > precision.lock_acquire_updates);
assert(robust.lock_release_ticks >= precision.lock_release_ticks);
}
static void test_period_scaled_hysteresis_and_settling(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init_period(&selector, 0.5);
assert(selector.upgrade_updates == 4u);
assert(selector.degrade_updates == 2u);
gps_pps_loop_profile_t precision =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_PRECISION, 0.5);
gps_pps_loop_profile_t robust =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_ROBUST, 0.5);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 5u);
assert(robust.lock_acquire_updates == 4u);
assert(robust.software_settle_updates == 7u);
gps_pps_quality_selector_init_period(&selector, 1.0);
assert(selector.upgrade_updates == 2u);
assert(selector.degrade_updates == 2u);
precision = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_PRECISION, 1.0);
robust = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_ROBUST, 1.0);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 3u);
assert(robust.lock_acquire_updates == 2u);
assert(robust.software_settle_updates == 4u);
}
static void assert_coefficients(
gps_pps_profile_t profile,
double period_sec,
int64_t expected_kp,
int64_t expected_ki)
{
int64_t kp = 0;
int64_t ki = 0;
uint32_t max_slew = 0u;
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, period_sec);
assert(gps_pps_loop_coefficients(
parameters, period_sec, 125000000.0,
&kp, &ki, &max_slew));
assert(kp == expected_kp);
assert(ki == expected_ki);
assert(max_slew == 85899u);
}
static void test_period_scaled_discrete_coefficients(void)
{
/* The production 200 ms coefficients remain bit-for-bit unchanged. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.2, 764018, 64006);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.2, 222838, 5445);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.2, 84891, 640);
/* Other cadences exponentiate those proven discrete poles in time. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.5, 615780, 127492);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.5, 209783, 12804);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.5, 82980, 1564);
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 1.0, 447388, 178038);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 1.0, 190239, 23149);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 1.0, 79923, 3011);
}
static void test_quality_boundaries_and_recovery(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init(&selector);
gps_pps_quality_metrics_t metrics = precision_metrics();
bool changed = false;
/* Exactly-on-threshold values remain usable in robust mode. */
metrics.used_satellites = 5u;
metrics.tdop = 8.0;
metrics.gdop = 12.0;
metrics.clock_bias_sigma_m = 100.0;
metrics.mean_cn0_dbhz = 22.0;
metrics.minimum_cn0_dbhz = 14.0;
metrics.chi_square_ratio = 1.0;
metrics.velocity_residual_rms_mps = 3.0;
for(unsigned int update = 0u; update < 8u; update++)
{
gps_pps_quality_selector_update(&selector, &metrics, &changed);
}
assert(selector.profile == GPS_PPS_PROFILE_ROBUST);
/* Non-finite metrics and loss below four satellites use fast hysteresis. */
metrics.tdop = NAN;
for(unsigned int update = 0u; update < 2u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_ROBUST);
}
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
assert(changed);
/* A single good epoch is not enough to leave holdover. */
metrics = precision_metrics();
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
metrics.used_satellites = 3u;
for(unsigned int update = 0u; update < 3u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
}
/* Recovery cannot expose PPS until eight consecutive good epochs. */
metrics = precision_metrics();
for(unsigned int update = 0u; update < 7u; update++)
{
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_HOLDOVER);
}
assert(gps_pps_quality_selector_update(
&selector, &metrics, &changed) == GPS_PPS_PROFILE_PRECISION);
}
int main(void)
{
test_hysteretic_profile_transitions();
test_alternating_quality_does_not_upgrade();
test_profile_parameters_are_ordered();
test_period_scaled_hysteresis_and_settling();
test_period_scaled_discrete_coefficients();
test_quality_boundaries_and_recovery();
puts("gps_pps_quality tests passed");
return 0;
}
@@ -0,0 +1,334 @@
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include "gps/gps_pps_time_filter.h"
#define COUNTER_HZ 125000000.0
static void test_elapsed_time_gain_preserves_filter_poles(void)
{
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double filtered = 0.0;
double innovation = 0.0;
assert(gps_pps_time_filter_update(
&filter, 0u, COUNTER_HZ, 0.0, true, 0.0, 1.0,
1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_COARSE);
assert(gps_pps_time_filter_update(
&filter, (uint64_t)COUNTER_HZ, COUNTER_HZ, 100.0, true, 100.0,
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_TRACKING);
/* One 1 s update has exactly the pole of five baseline 200 ms updates. */
double expected_bias_gain = 1.0 - pow(1.0 - 1.0 / 32.0, 5.0);
double expected_drift_gain = 1.0 - pow(1.0 - 1.0 / 4.0, 5.0);
assert(fabs(filtered - 100.0 * expected_bias_gain) < 1.0e-12);
assert(fabs(filter.clock_drift_mps - 100.0 * expected_drift_gain) <
1.0e-12);
}
static void test_drift_prediction_rejects_bias_noise(void)
{
static const double noise_m[] = {
0.0, 90.0, -120.0, 70.0, -45.0, 130.0, -80.0, 55.0,
-105.0, 35.0, 85.0, -65.0, 115.0, -95.0, 50.0, -30.0
};
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double squared_error_sum = 0.0;
unsigned int tracked = 0u;
for(unsigned int update = 0u; update < 160u; update++)
{
double time_sec = 0.2 * (double)update;
double true_bias_m = 1200.0 + 475.0 * time_sec;
double measured_bias_m =
true_bias_m + noise_m[update % (sizeof(noise_m) / sizeof(noise_m[0]))];
double filtered_bias_m = 0.0;
double innovation_m = 0.0;
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
&filter,
(uint64_t)llround(time_sec * COUNTER_HZ),
COUNTER_HZ,
measured_bias_m,
true,
475.0 + ((update & 1u) ? 0.3 : -0.3),
1.7,
1.0 / 32.0,
16u,
&filtered_bias_m,
&innovation_m);
assert(result != GPS_PPS_TIME_FILTER_REJECTED);
assert(isfinite(innovation_m));
if(result == GPS_PPS_TIME_FILTER_TRACKING)
{
double error_m = filtered_bias_m - true_bias_m;
squared_error_sum += error_m * error_m;
tracked++;
}
}
assert(tracked > 100u);
assert(sqrt(squared_error_sum / (double)tracked) < 12.0);
}
static void test_quality_gap_and_outlier_handling(void)
{
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double filtered = 0.0;
double innovation = 0.0;
assert(gps_pps_time_filter_update(
&filter, 0u, COUNTER_HZ, 100.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
&filtered, &innovation) == GPS_PPS_TIME_FILTER_COARSE);
assert(gps_pps_time_filter_update(
&filter, 25000000u, COUNTER_HZ, 102.0, true, 10.0, 12.0, 1.0 / 32.0, 1u,
&filtered, &innovation) == GPS_PPS_TIME_FILTER_REJECTED);
uint64_t saved_epoch = filter.epoch_counter;
double saved_bias = filter.clock_bias_m;
assert(gps_pps_time_filter_update(
&filter, 25000000u, COUNTER_HZ, 5000.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
&filtered, &innovation) == GPS_PPS_TIME_FILTER_REJECTED);
assert(filter.epoch_counter == saved_epoch);
assert(filter.clock_bias_m == saved_bias);
assert(gps_pps_time_filter_update(
&filter, 0u, COUNTER_HZ, 100.0, true, 10.0, 1.0,
1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_REJECTED);
assert(filter.epoch_counter == saved_epoch);
assert(filter.clock_bias_m == saved_bias);
assert(gps_pps_time_filter_update(
&filter, 375000000u, COUNTER_HZ, 130.0, true, 10.0, 1.0, 1.0 / 32.0, 1u,
&filtered, &innovation) == GPS_PPS_TIME_FILTER_TRACKING);
assert(fabs(filtered - 130.0) < 1.0e-12);
/* A gap beyond the bounded Doppler-prediction horizon must still reset
* the filter and require a safe coarse alignment. */
assert(gps_pps_time_filter_update(
&filter, 4250000000ULL, COUNTER_HZ, 440.0, true, 10.0,
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_COARSE);
assert(fabs(filtered - 440.0) < 1.0e-12);
}
static void test_bias_ramp_without_doppler_uses_bounded_fallback(void)
{
static const double noise_m[] = {
80.0, -110.0, 45.0, -25.0, 125.0, -70.0, 35.0, -95.0,
60.0, -40.0, 105.0, -85.0, 20.0, -15.0, 75.0, -55.0
};
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double squared_error_sum = 0.0;
unsigned int tracked = 0u;
for(unsigned int update = 0u; update < 500u; update++)
{
double time_sec = 0.2 * (double)update;
double true_bias_m = 1200.0 + 475.0 * time_sec;
double measured_bias_m =
true_bias_m +
noise_m[update % (sizeof(noise_m) / sizeof(noise_m[0]))];
double filtered_bias_m = 0.0;
double innovation_m = 0.0;
gps_pps_time_filter_result_t result = gps_pps_time_filter_update(
&filter,
(uint64_t)llround(time_sec * COUNTER_HZ),
COUNTER_HZ,
measured_bias_m,
false,
0.0,
5.0,
1.0 / 32.0,
16u,
&filtered_bias_m,
&innovation_m);
assert(result != GPS_PPS_TIME_FILTER_REJECTED);
if((result == GPS_PPS_TIME_FILTER_TRACKING) && (update >= 200u))
{
double error_m = filtered_bias_m - true_bias_m;
squared_error_sum += error_m * error_m;
tracked++;
}
}
assert(tracked > 250u);
assert(filter.drift_initialized);
assert(fabs(filter.clock_drift_mps) < 10000.0);
assert(sqrt(squared_error_sum / (double)tracked) < 45.0);
}
static void test_board_calibrated_doppler_predictor_beats_fallback(void)
{
/* Values measured on Genesys2 on 2026-07-26: clock-bias sigma was
* 15.87 m and reference-datum drift was about 291.75 m/s. Feed the
* exact same deterministic WLS noise to two filters so this is a true
* predictor on/off A/B, not two unrelated Monte-Carlo runs. */
static const double bias_noise_m[] = {
8.0, -18.0, 12.0, -7.0, 21.0, -14.0, 5.0, -25.0,
16.0, -3.0, 10.0, -20.0, 6.0, 24.0, -11.0, 2.0
};
static const double drift_noise_mps[] = {
1.8, -3.2, 4.1, -2.4, 0.7, 3.5, -4.3, 2.6,
-1.1, 4.6, -2.8, 1.5, -3.7, 2.2, -0.4, 3.0
};
gps_pps_time_filter_t aided;
gps_pps_time_filter_t fallback;
gps_pps_time_filter_init(&aided);
gps_pps_time_filter_init(&fallback);
double aided_squared_error_sum = 0.0;
double fallback_squared_error_sum = 0.0;
unsigned int tracked = 0u;
for(unsigned int update = 0u; update < 900u; update++)
{
double time_sec = 0.2 * (double)update;
double long_omega = 2.0 * 3.14159265358979323846 / 75.0;
double short_omega = 2.0 * 3.14159265358979323846 / 19.0;
double varying_bias_m =
5.0 * (1.0 - cos(long_omega * time_sec)) / long_omega +
2.0 * (1.0 - cos(short_omega * time_sec)) / short_omega;
double true_drift_mps =
291.75 + 5.0 * sin(long_omega * time_sec) +
2.0 * sin(short_omega * time_sec);
double true_bias_m =
159624.654 + 291.75 * time_sec + varying_bias_m;
double measured_bias_m = true_bias_m +
bias_noise_m[update %
(sizeof(bias_noise_m) / sizeof(bias_noise_m[0]))];
double aided_bias_m = 0.0;
double fallback_bias_m = 0.0;
double innovation_m = 0.0;
gps_pps_time_filter_result_t aided_result =
gps_pps_time_filter_update(
&aided,
(uint64_t)llround(time_sec * COUNTER_HZ),
COUNTER_HZ,
measured_bias_m,
true,
true_drift_mps + drift_noise_mps[update %
(sizeof(drift_noise_mps) /
sizeof(drift_noise_mps[0]))],
1.5,
1.0 / 32.0,
12u,
&aided_bias_m,
&innovation_m);
gps_pps_time_filter_result_t fallback_result =
gps_pps_time_filter_update(
&fallback,
(uint64_t)llround(time_sec * COUNTER_HZ),
COUNTER_HZ,
measured_bias_m,
false,
0.0,
1.5,
1.0 / 32.0,
12u,
&fallback_bias_m,
&innovation_m);
assert(aided_result != GPS_PPS_TIME_FILTER_REJECTED);
assert(fallback_result != GPS_PPS_TIME_FILTER_REJECTED);
if((update >= 300u) &&
(aided_result == GPS_PPS_TIME_FILTER_TRACKING) &&
(fallback_result == GPS_PPS_TIME_FILTER_TRACKING))
{
double aided_error_m = aided_bias_m - true_bias_m;
double fallback_error_m = fallback_bias_m - true_bias_m;
aided_squared_error_sum += aided_error_m * aided_error_m;
fallback_squared_error_sum +=
fallback_error_m * fallback_error_m;
tracked++;
}
}
assert(tracked > 550u);
double aided_rms_m = sqrt(aided_squared_error_sum / (double)tracked);
double fallback_rms_m =
sqrt(fallback_squared_error_sum / (double)tracked);
printf("board PPS bias RMS: Doppler=%.3f m fallback=%.3f m\n",
aided_rms_m, fallback_rms_m);
assert(aided_rms_m < 8.0);
assert(aided_rms_m < 0.35 * fallback_rms_m);
}
static void test_reference_geometry_defines_bias_drift(void)
{
const double receiver_position_m[3] = {0.0, 0.0, 0.0};
const double receiver_velocity_mps[3] = {10.0, -2.0, 1.0};
const double satellite_position_m[3] = {20200000.0, 0.0, 0.0};
const double satellite_velocity_mps[3] = {140.0, 30.0, -10.0};
double bias_drift_mps = 0.0;
/* LOS is +X, so WLS datum drift is -(140 - 10) = -130 m/s. */
assert(gps_pps_reference_bias_drift(
receiver_position_m,
receiver_velocity_mps,
satellite_position_m,
satellite_velocity_mps,
0.0,
&bias_drift_mps));
assert(fabs(bias_drift_mps + 130.0) < 1.0e-12);
/* Broadcast satellite-clock drift is part of the position observation. */
assert(gps_pps_reference_bias_drift(
receiver_position_m,
receiver_velocity_mps,
satellite_position_m,
satellite_velocity_mps,
1.0e-9,
&bias_drift_mps));
assert(fabs(
bias_drift_mps -
(-130.0 + 299792458.0 * 1.0e-9)) < 1.0e-12);
double invalid_satellite_position[3] = {0.0, 0.0, 0.0};
assert(!gps_pps_reference_bias_drift(
receiver_position_m,
receiver_velocity_mps,
invalid_satellite_position,
satellite_velocity_mps,
0.0,
&bias_drift_mps));
}
static void test_reference_selection_is_stable(void)
{
const unsigned int ids[] = {3u, 8u, 17u};
const double captures[] = {102.0, 100.0, 104.0};
unsigned int index = 99u;
assert(gps_pps_select_reference(
ids, captures, 3u, false, 0u, &index));
assert(index == 1u);
/* A newly earlier satellite must not move an existing WLS datum. */
assert(gps_pps_select_reference(
ids, captures, 3u, true, 3u, &index));
assert(index == 0u);
/* Loss of the preferred satellite selects the earliest remaining one. */
assert(gps_pps_select_reference(
ids, captures, 3u, true, 25u, &index));
assert(index == 1u);
double invalid_captures[] = {102.0, NAN, 104.0};
assert(!gps_pps_select_reference(
ids, invalid_captures, 3u, false, 0u, &index));
}
int main(void)
{
test_elapsed_time_gain_preserves_filter_poles();
test_drift_prediction_rejects_bias_noise();
test_quality_gap_and_outlier_handling();
test_bias_ramp_without_doppler_uses_bounded_fallback();
test_board_calibrated_doppler_predictor_beats_fallback();
test_reference_geometry_defines_bias_drift();
test_reference_selection_is_stable();
puts("gps_pps_time_filter tests passed");
return 0;
}
@@ -1,10 +1,11 @@
#include "gps/gps_time_anchor.h"
#include "gps/Navigation.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#define SAMPLE_RATE_HZ 16368000.0
#define SAMPLE_RATE_HZ GPS_RF_SAMPLE_RATE_HZ
static int failures;
@@ -99,6 +100,89 @@ static void invalid_inputs_are_rejected(void)
"out-of-range HOW was accepted");
}
static void physical_rf_rate_avoids_six_second_sawtooth(void)
{
const double elapsed_sec = 6.0;
const double samples = GPS_RF_SAMPLE_RATE_HZ * elapsed_sec;
const double legacy_elapsed_sec = samples / 16368000.0;
const double corrected_elapsed_sec = samples / GPS_RF_SAMPLE_RATE_HZ;
CHECK(fabs((legacy_elapsed_sec - elapsed_sec) * 1.0e9) > 6900.0,
"legacy 16.368 MHz conversion did not expose HOW sawtooth");
CHECK(fabs((corrected_elapsed_sec - elapsed_sec) * 1.0e9) < 0.001,
"physical RF rate left a six-second timing ramp");
}
static void periodic_event_time_removes_receive_range_rate(void)
{
const uint32_t anchor = 700000000u;
const double physical_elapsed_sec = 5.8 + 8.0e-6;
const uint32_t event =
anchor + (uint32_t)llround(physical_elapsed_sec * SAMPLE_RATE_HZ);
int week = 0;
double tow_sec = 0.0;
double elapsed_sec = 0.0;
CHECK(gps_time_from_periodic_sample_anchor(
380, 469362.0, anchor, (double)anchor,
event, (double)event, SAMPLE_RATE_HZ, 120.0,
0.2, 0.005, &week, &tow_sec, &elapsed_sec),
"periodic NAV event with range-rate delay was rejected");
CHECK(week == 380, "periodic NAV event changed GPS week");
CHECK(fabs(tow_sec - 469367.8) < 1.0e-9,
"periodic NAV event retained receive-time range-rate error");
CHECK(fabs(elapsed_sec - 5.8) < 1.0e-12,
"periodic NAV event did not report snapped elapsed time");
const uint32_t off_grid_event =
anchor + (uint32_t)llround(5.82 * SAMPLE_RATE_HZ);
CHECK(!gps_time_from_periodic_sample_anchor(
380, 469362.0, anchor, (double)anchor,
off_grid_event, (double)off_grid_event,
SAMPLE_RATE_HZ, 120.0, 0.2, 0.005,
&week, &tow_sec, &elapsed_sec),
"off-grid NAV event was accepted");
}
static void fractional_code_phase_corrects_capture_counter(void)
{
const uint64_t capture_counter = 1000000u;
uint64_t corrected_counter = 0u;
double correction_sec = 0.0;
CHECK(gps_time_correct_capture_counter(
capture_counter, 0.75, SAMPLE_RATE_HZ, 125000000.0,
&corrected_counter, &correction_sec),
"valid fractional code phase did not correct PPS capture");
/* 0.75 RF sample is 45.82 ns, rounded onto the 8 ns PPS counter grid. */
CHECK(corrected_counter == capture_counter - 6u,
"fractional code phase used the wrong capture-counter correction");
CHECK(fabs(correction_sec - 48.0e-9) < 1.0e-15,
"capture correction did not report the applied integer tick");
CHECK(gps_time_correct_capture_counter(
capture_counter, 0.0, SAMPLE_RATE_HZ, 125000000.0,
&corrected_counter, &correction_sec) &&
(corrected_counter == capture_counter),
"zero code residue changed the capture counter");
CHECK(!gps_time_correct_capture_counter(
capture_counter, -0.1, SAMPLE_RATE_HZ, 125000000.0,
&corrected_counter, NULL),
"negative fractional sample delay was accepted");
CHECK(!gps_time_correct_capture_counter(
capture_counter, 1.1, SAMPLE_RATE_HZ, 125000000.0,
&corrected_counter, NULL),
"capture correction larger than one RF sample was accepted");
CHECK(!gps_time_correct_capture_counter(
2u, 1.0, SAMPLE_RATE_HZ, 125000000.0,
&corrected_counter, NULL),
"capture-counter underflow was accepted");
CHECK(!gps_time_correct_capture_counter(
UINT64_MAX, 1.0, 1.0, 1.0e30,
&corrected_counter, NULL),
"capture correction beyond llround range was accepted");
}
int main(void)
{
normal_and_skipped_subframes_are_consistent();
@@ -106,6 +190,9 @@ int main(void)
temporary_week_can_upgrade_on_first_subframe_one();
captured_future_jump_is_rejected_and_recovers();
invalid_inputs_are_rejected();
physical_rf_rate_avoids_six_second_sawtooth();
periodic_event_time_removes_receive_range_rate();
fractional_code_phase_corrects_capture_counter();
if(failures == 0)
{
puts("gps_time_anchor tests passed");
@@ -78,6 +78,22 @@ static void test_cn0_and_mailbox_decode(void)
measurement.pseudorange_rate_mps,
expected_range_rate,
1.0e-9));
assert(close_to(
measurement.filtered_doppler_hz,
measurement.doppler_hz,
1.0e-12));
assert(close_to(
measurement.filtered_pseudorange_rate_mps,
measurement.pseudorange_rate_mps,
1.0e-12));
assert(close_to(
measurement.eskf_doppler_hz,
measurement.doppler_hz,
1.0e-12));
assert(close_to(
measurement.eskf_pseudorange_rate_mps,
measurement.pseudorange_rate_mps,
1.0e-12));
registers.MEASUREMENT_META &=
~(1u << GPS_TRCK_MEAS_META_LOCK_POS);
@@ -143,6 +159,7 @@ static void test_velocity_wls(void)
}
double solved_velocity[3];
double solved_velocity_covariance[3][3];
double solved_clock_drift;
double residual_rms;
assert(gps_tracking_velocity_wls(
@@ -153,6 +170,7 @@ static void test_velocity_wls(void)
measurements,
TEST_SATELLITES,
solved_velocity,
solved_velocity_covariance,
&solved_clock_drift,
&residual_rms));
@@ -168,6 +186,96 @@ static void test_velocity_wls(void)
expected_clock_drift_mps,
1.0e-9));
assert(residual_rms < 1.0e-9);
for(unsigned int component = 0u; component < 3u; component++)
{
assert(isfinite(
solved_velocity_covariance[component][component]));
assert(solved_velocity_covariance[component][component] > 0.0);
}
assert(close_to(
solved_velocity_covariance[0][1],
solved_velocity_covariance[1][0],
1.0e-12));
{
double clean_velocity[3];
double clean_covariance[3][3];
double clean_clock_drift;
double clean_residual_rms;
uint32_t clean_excluded_mask = UINT32_MAX;
assert(gps_tracking_velocity_wls_robust(
receiver_position,
satellite_positions,
satellite_velocities,
satellite_clock_drift,
measurements,
TEST_SATELLITES,
clean_velocity,
clean_covariance,
&clean_clock_drift,
&clean_residual_rms,
&clean_excluded_mask));
assert(clean_excluded_mask == 0u);
for(unsigned int component = 0u; component < 3u; component++)
{
assert(close_to(
clean_velocity[component],
solved_velocity[component],
1.0e-12));
}
}
/* One gross carrier burst must be isolated before aggregate ESKF fusion. */
measurements[2].pseudorange_rate_mps += 12.0;
double plain_velocity[3];
double plain_covariance[3][3];
double plain_clock_drift;
double plain_residual_rms;
assert(gps_tracking_velocity_wls(
receiver_position,
satellite_positions,
satellite_velocities,
satellite_clock_drift,
measurements,
TEST_SATELLITES,
plain_velocity,
plain_covariance,
&plain_clock_drift,
&plain_residual_rms));
double robust_velocity[3];
double robust_covariance[3][3];
double robust_clock_drift;
double robust_residual_rms;
uint32_t excluded_mask = 0u;
assert(gps_tracking_velocity_wls_robust(
receiver_position,
satellite_positions,
satellite_velocities,
satellite_clock_drift,
measurements,
TEST_SATELLITES,
robust_velocity,
robust_covariance,
&robust_clock_drift,
&robust_residual_rms,
&excluded_mask));
assert(excluded_mask == (1u << 2));
double plain_error_square = 0.0;
for(unsigned int component = 0u; component < 3u; component++)
{
double plain_error =
plain_velocity[component] - expected_velocity[component];
plain_error_square += plain_error * plain_error;
assert(close_to(
robust_velocity[component],
expected_velocity[component],
1.0e-8));
}
assert(sqrt(plain_error_square) > 1.0);
assert(close_to(
robust_clock_drift,
expected_clock_drift_mps,
1.0e-8));
assert(robust_residual_rms < 1.0e-8);
}
int main(void)
@@ -0,0 +1,260 @@
# GPS PPS end-to-end Linux validation
This package is a deterministic, causal validation of the project timing path
from synthetic GPS LNAV data to the physical PPS edge. It exists separately
from `pps_python`, whose models concentrate on WLS noise, oscillator behavior,
and adaptive-loop performance.
The validation does not use Linux wall-clock scheduling. The 1 kHz, 50 bit/s,
RF-sample, PCLK, and 125 MHz events advance from synthetic timestamps, making
the signed absolute-time result repeatable on a normal Linux host.
## What is modeled
The model follows the current firmware and RTL conventions:
1. It generates deterministic GPS LNAV subframes with D29*/D30* data inversion
and valid parity for all ten 30-bit words.
2. Each navigation bit is expanded to twenty 1 kHz prompt-sign samples.
3. The `bit_gen.sv` 40 ms `1 -> 0` transition acquisition, majority decision,
20 ms `bit_valid` cadence, and registered ABS_SAMPLE behavior are applied.
4. The tracking wrapper's chronological `2 + 300 + 8 = 310` bit window,
preamble search, parity check, `load_sigs`, and subframe interrupt ordering
are applied.
5. `nav_solve_timer` is initialized from HOW Z-count and counts later bit-valid
events. Navigation-solve interrupt, PPS counter capture, capture sequence,
and a 32-bit RF ABS_SAMPLE wrap are exercised.
6. Firmware reconstructs periodic TOW from the HOW/sample anchor and evaluates
`TOW + POSTPONE_TOW + STARTOFFSET - b/c`.
7. The timestamped 125 MHz PPS peripheral performs coarse/normal mapping,
ignores synthetic CPU/APB delay as phase, acquires lock after five valid
updates, and generates integer-second edges.
8. DUT edges are compared with an independent trusted PPS. Optional DUT and
trusted-output delays make board, cable, and vendor delay sign explicit.
This is a digital epoch-path validation, not an RF waveform simulator. The
existing `pps_python` tests continue to cover WLS, oscillator, PI, adaptive
quality, and holdover behavior.
## Run
From `soc/software`:
```bash
python3 -m pps_end_to_end_validation.pps_end_to_end
python3 -m unittest pps_end_to_end_validation/test_pps_end_to_end.py
```
The default command executes the diagnostic matrix:
```text
scenario bits correction reference error DUT - trusted PPS
current-rtl/180ms 9 180 ms -0.000032 ms +0.000032 ms
current-rtl/160ms 9 160 ms -20.000032 ms +20.000032 ms
current-bit/160ms 8 160 ms -0.000032 ms +0.000032 ms
```
Positive `DUT - trusted PPS` means the project output is late. The default
32 ns remainder is the explicitly configured four-tick PCLK capture pipeline;
it is not part of the LNAV-bit mismatch.
Run one scenario or obtain machine-readable output:
```bash
python3 -m pps_end_to_end_validation.pps_end_to_end \
--mode current-rtl --postpone-ms 160 --json
python3 -m pps_end_to_end_validation.pps_end_to_end \
--mode current-bit \
--dut-output-delay-ns 12 \
--trusted-output-delay-ns 35
```
## Closed-loop stability and dynamic tuning
The bit-level command above isolates the absolute epoch convention. The
companion stability analysis connects that result to the fixed-point
firmware/RTL PI model, replays the real GDOP and tracked-satellite history in
`fusion/logs/gps_live_5min_20260713.csv`, and synthesizes only the telemetry
that the repository did not capture:
```bash
python3 -m pps_end_to_end_validation.stability_analysis
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 500
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 1000
python3 -m unittest \
pps_end_to_end_validation/test_stability_analysis.py
```
Generated results are in `output/stability/`:
- `stability_report.md`: real-data inventory, discrete poles, absolute phase
wander, period jitter, dynamic parameter sweep, and recommendations;
- `parameter_sweep.csv`: machine-readable results for every scenario/tuning;
- `synthetic_telemetry.csv`: solve-cadence ABS_SAMPLE, REM_CODE_PHASE, corrected 125 MHz
capture, HDOP/VDOP/TDOP/GDOP, C/N0, pseudorange noise, clock sigma/drift,
oscillator and timing-error inputs.
The older fusion capture used by `stability_analysis.py` contains 989 GDOP
values and 990 tracked-satellite values (eight throughout), but no PPS
phase/rate, ABS_SAMPLE, C/N0, TDOP/HDOP, pseudorange residual, oscillator
temperature, or trusted-edge/TIC samples. The newer live-board workflow below
supplies many of those PPS status fields, while ABS_SAMPLE, REM_CODE_PHASE,
temperature, and trusted-edge timing remain unavailable. The stability CSV
labels replacements as synthetic; it must not be read as board telemetry.
The current acquire, precision, and robust fixed-point poles are inside the
unit circle at the 200, 500, and 1000 ms supported update rates, and the stress
analysis observes no unlock. It also exposes an important metrics issue:
cycle-to-cycle PPS period
jitter can remain below 20 ns while absolute phase moves slowly through tens
of nanoseconds. A physical 20 ns maximum therefore requires a common-clock
time-interval-counter capture and cable/output-delay calibration; a p95 period
jitter assertion or synthetic run cannot certify it.
The sweep includes the current coefficients, quieter/faster precision loops,
a robust-noise guard, a larger Doppler-drift gain, and a quality-scheduled
candidate. The generated report contains the current numerical comparison.
Production firmware maps the proven 200 ms discrete poles to the selected NAV
period and leaves the 200 ms Q2.46 constants bit-identical. The model and
firmware also scale quality/lock/settling timers in wall-clock time, convert
software bias/drift gains from actual elapsed time, and extend the stale-data
watchdog to `max(2 s, 3 * solve period)`. This preserves deterministic settling
and damping; it cannot preserve measurement-noise statistics when fewer WLS
samples are available. Production firmware uses the current-RTL-compatible
180 ms epoch correction independently of solve cadence.
A deterministic four-seed, 180 s smoke sweep with the current profiles gave:
| solve period | real-geometry absolute RMS | worst period step over current scenarios | unlocks |
|---:|---:|---:|---:|
| 200 ms | 11.17 ns | 16 ns | 0 |
| 500 ms | 15.96 ns | 16 ns | 0 |
| 1000 ms | 25.09 ns | 24 ns | 0 |
Thus 1000 ms is linearly stable but is not recommended when the stated
synthetic maximum-period-jitter target is 20 ns. Use 200 ms by default, or
500 ms when CPU load requires it. A 1000 ms solve needs hardware validation
and likely oscillator-temperature/frequency prediction; pole remapping alone
cannot recreate the missing measurements.
## Live board telemetry replay
`hardware_replay.py` accepts the host-timestamped UART CSV produced by the
fusion capture utility. It extracts the firmware fields that actually drive
or describe PPS: raw/filtered WLS clock bias, innovation, Doppler datum drift,
TDOP/GDOP/HDOP/VDOP, clock sigma, C/N0, tracked/used satellites, velocity/FDE,
reference SVID, fractional capture correction, RTL phase residual, rate word,
integrator, lock/profile, pulse count, and commit sequence.
Capture and replay from `soc/software`:
```bash
python3 fusion/serial_capture.py \
--port /dev/ttyUSB2 --baud 460800 --duration 300 \
--output /tmp/genesys2_pps.csv
python3 -m pps_end_to_end_validation.hardware_replay \
--input /tmp/genesys2_pps.csv \
--output-dir pps_end_to_end_validation/output/hardware_replay
python3 -m unittest \
pps_end_to_end_validation.test_hardware_replay
```
Raw UART files are ignored by Git because `[LOCATION]` records contain exact
coordinates. The replay output is normalized to PPS fields and deliberately
omits latitude/longitude.
For an already-running board, replay imports the first logged locked
filter/PI snapshot. Starting the model from reset would erase the observed
integrator and create a false acquisition transient. When status is decimated,
commit sequence numbers expand the trace back to the inferred solve cadence;
intermediate values are explicitly marked `interpolated=true`.
The replay infers cadence from UART host time divided by commit-sequence
advance, snapped to the 20 ms navigation-bit grid. Use
`--update-period-ms` only when host timestamps are unreliable.
The report separates three quantities that should not be conflated:
- direct `hw_phase` statistics from the board;
- fixed-point PI reproduction and a telemetry-conditioned model replay; and
- inferred active phase obtained by integrating `rate_cmd` under a constant
oscillator assumption.
Only the first two are directly constrained by firmware/RTL telemetry. The
third is useful for diagnosing slow wander but is not a trusted-generator
measurement. `hw_phase` itself is a 5 Hz commit residual, not the physical PPS
pin offset. A calibrated, common-clock TIC/scope capture is still required for
a maximum-20 ns output claim.
## 20 ms diagnosis
The current tracking wrapper shifts the current bit into `shift_reg_n`, while
its preamble/parity predicate reads the preceding `shift_reg_p`. On the eighth
following preamble bit, the validator therefore sees only the previous seven
bits. The register contains all eight after the edge, but the bit-valid edge
condition has ended. Validation occurs on the ninth bit, 20 ms later.
The implemented interface is consequently:
```text
current RTL delivery 9 * 20 ms = 180 ms
production POSTPONE_TOW 9 * 20 ms = 180 ms
signed epoch mismatch 0 ms
former firmware value 8 * 20 ms = 160 ms
former signed mismatch 20 ms
```
Production firmware now uses `POSTPONE_TOW=0.180` as the current-RTL-compatible
correction, consistently in both satellite-state and PPS calculations. It is
180 ms, not 190 ms. If RTL is later changed to validate a candidate window
containing the current eighth bit, firmware must return to 160 ms in the same
coordinated release.
The validation intentionally does not modify production RTL. Firmware changes
for period-aware control are exercised by the same coefficient and timing
tests; the 180 ms absolute epoch convention remains unchanged.
## Other regression coverage
The unit tests verify:
- parity-valid LNAV round-trip and rejection after one corrupted data bit;
- 1 kHz prompt-sign to 50 bit/s generation and the registered 1 ms sample lag;
- ninth-bit delivery in current RTL and eighth-bit delivery in the candidate
implementation;
- constant signed PPS error over successive physical edges;
- 180 ms firmware compatibility with current RTL;
- invariance to synthetic CPU/APB stalls when capture timestamps are used;
- DUT-versus-trusted output-delay sign; and
- rejection behavior explaining why firmware now compile-rejects
`NAV_SOLVE_TIMER=40`; and
- zero NAV-solve rejection and unchanged absolute epoch at N=10/25/50
(200/500/1000 ms).
The `N=40` case divides the GPS week but not a 300-bit subframe. When the model
refreshes the HOW/sample anchor after six seconds, the timer phase is half a
solve period away from that anchor and the current `N * 20 ms` snap rejects
otherwise valid solves. Firmware therefore requires N to divide both the GPS
week and the 300-bit subframe. Production-safe examples are N=10, 25, and 50.
If arbitrary timer periods are required later, reconstruct elapsed time on the
fundamental 20 ms navigation-bit grid instead of snapping through a refreshed
HOW anchor.
## Hardware confirmation
Before selecting a production correction:
1. Probe `bit_valid`, the final preamble bits, `preamble_ok`, parity result, and
`load_sigs` with ILA.
2. Count whether delivery is on following bit 8 or 9 in the flashed bitstream.
3. Measure DUT and trusted PPS with a common time-interval counter and record
the sign. The diagnosis predicts a DUT edge approximately 20 ms late.
4. Apply only nanosecond-scale output/cable calibration after the epoch
mismatch is removed.
@@ -0,0 +1 @@
"""End-to-end Linux validation for the GPS LNAV-to-PPS timing path."""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
host_monotonic_s,host_unix_s,time_s,profile,mode,locked,tdop,gdop,hdop,vdop,clock_sigma_m,cn0_mean_dbhz,cn0_min_dbhz,tracked_satellites,used_satellites,innovation_m,raw_bias_m,filtered_bias_m,clock_drift_mps,bias_drift_mps,predictor_enabled,eskf_alpha,eskf_speed_mps,eskf_velocity_rms_mps,eskf_fde_mask,pps_speed_mps,pps_velocity_rms_mps,pps_fde_mask,reference_svid,capture_correction_ns,hardware_phase_ns,rate_command_ppm,integrator_ppm,pulse_count,sequence,status
1.50420269,1785272055.903764,0.0,robust,tracking,True,1.25,2.86,1.8176601213,1.8176260703,12.54,42.7,37.7,6,5,4.52,693498.817,693494.442,110.788,-72.055,True,0.05,0.271,0.185,0,0.271,0.185,0,23,32.0,160.47,-1.536,-1.56,600,3040,2063
3.503689524,1785272057.9032507,2.0,robust,tracking,True,1.25,2.86,1.8176193953,1.8170996688,11.17,42.9,38.2,6,5,-19.2,693330.906,693349.504,111.201,-71.738,True,0.05,0.746,0.135,0,0.746,0.135,0,23,56.0,124.38,-1.541,-1.56,602,3050,2063
5.500479302,1785272059.9000404,4.0,robust,tracking,True,1.25,2.86,1.8175780522,1.8165687846,21.48,42.7,38.5,6,5,-7.0,693197.92,693204.698,111.206,-71.912,True,0.05,0.836,0.039,0,0.836,0.039,0,23,24.0,141.5,-1.539,-1.56,604,3060,2063
7.509988539,1785272061.9095497,6.0,robust,tracking,True,1.25,2.86,1.8175381286,1.816041859,11.14,43.0,38.3,6,5,13.6,693072.274,693059.102,111.221,-71.825,True,0.05,1.009,0.28,0,1.009,0.28,0,23,32.0,168.11,-1.535,-1.56,606,3070,2063
9.511653187,1785272063.9112144,8.0,robust,tracking,True,1.25,2.86,1.8174962624,1.8155120506,11.56,42.9,38.0,6,5,-31.15,692881.989,692912.165,110.864,-72.861,True,0.05,0.371,0.04,0,0.371,0.04,0,23,40.0,135.63,-1.539,-1.56,608,3080,2063
11.50378733,1785272065.9033487,10.0,robust,tracking,True,1.25,2.85,1.8174539102,1.8149874373,11.07,42.8,38.6,6,5,-9.78,692754.373,692763.848,110.014,-73.882,True,0.05,0.843,0.004,0,0.843,0.004,0,23,56.0,155.42,-1.536,-1.56,610,3090,2063
13.504178648,1785272067.9037397,12.0,robust,tracking,True,1.25,2.85,1.817413463,1.8144599125,11.18,42.9,38.1,6,5,6.48,692623.758,692617.477,110.329,-73.94,True,0.05,0.609,0.096,0,0.609,0.096,0,23,0.0,162.66,-1.535,-1.56,612,3100,2063
15.503554083,1785272069.9031153,14.0,robust,tracking,True,1.25,2.85,1.8173718635,1.8139319104,11.26,42.7,37.0,6,5,-1.31,692469.561,692470.834,111.54,-72.849,True,0.05,0.953,0.31,0,0.953,0.31,0,23,56.0,155.78,-1.536,-1.56,614,3110,2063
17.502924749,1785272071.9024858,16.0,robust,tracking,True,1.25,2.85,1.81733102,1.8134069522,15.4,42.4,36.5,6,5,-11.0,692313.987,692324.644,110.815,-73.581,True,0.05,0.736,0.168,0,0.736,0.168,0,23,32.0,133.05,-1.54,-1.56,616,3120,2063
19.506236288,1785272073.9057975,18.0,robust,tracking,True,1.24,2.85,1.8172902798,1.8128821788,11.11,43.1,38.1,6,5,-5.77,692171.911,692177.505,111.077,-73.779,True,0.05,0.4,0.027,0,0.4,0.027,0,23,24.0,131.86,-1.54,-1.56,618,3130,2063
21.504228726,1785272075.9037895,20.0,robust,tracking,True,1.24,2.85,1.8172490099,1.8123554135,11.15,43.0,38.0,6,5,8.57,692038.349,692030.045,110.612,-74.424,True,0.05,0.512,0.053,0,0.512,0.053,0,23,24.0,156.87,-1.536,-1.56,620,3140,2063
23.504153615,1785272077.903715,22.0,robust,tracking,True,1.24,2.85,1.8172104392,1.8118290291,11.3,42.6,37.7,6,5,-11.79,691868.479,691879.901,111.534,-73.806,True,0.05,0.721,0.036,0,0.721,0.036,0,23,56.0,142.52,-1.538,-1.56,622,3150,2063
25.510000609,1785272079.9095619,24.0,robust,tracking,True,1.24,2.85,1.8171689199,1.8113038519,11.08,43.1,38.1,6,5,-12.44,691719.345,691731.399,110.942,-74.863,True,0.05,0.359,0.101,0,0.359,0.101,0,23,48.0,145.5,-1.538,-1.56,624,3160,2063
27.50517151,1785272081.904733,26.0,robust,tracking,True,1.24,2.85,1.8171301373,1.8107796086,11.26,42.7,37.5,6,5,-18.16,691564.145,691581.742,111.5,-74.479,True,0.05,0.745,0.096,0,0.745,0.096,0,23,8.0,129.02,-1.54,-1.56,626,3170,2063
29.502641745,1785272083.9022028,28.0,robust,tracking,True,1.24,2.85,1.8170880804,1.8102547189,12.41,43.0,37.5,6,5,1.28,691433.115,691431.876,111.148,-74.903,True,0.05,0.332,0.115,0,0.332,0.115,0,23,0.0,127.84,-1.541,-1.56,628,3180,2063
31.500642933,1785272085.900204,30.0,robust,tracking,True,1.24,2.85,1.8170480561,1.8097326484,14.78,42.6,37.5,6,5,-1.39,691280.067,691281.411,111.681,-74.941,True,0.05,0.503,0.255,0,0.503,0.255,0,23,48.0,147.24,-1.538,-1.56,630,3190,2063
33.504307045,1785272087.903868,32.0,robust,tracking,True,1.24,2.85,1.8170082113,1.8092081529,22.56,42.9,38.4,6,5,-13.17,691116.408,691129.168,110.696,-75.765,True,0.05,0.206,0.027,0,0.206,0.027,0,23,16.0,132.81,-1.54,-1.56,632,3200,2063
35.509887989,1785272089.9094489,34.0,robust,tracking,True,1.24,2.85,1.8169688875,1.8086870549,11.18,42.9,38.0,6,5,-0.84,690974.741,690975.554,111.343,-75.376,True,0.05,0.636,0.055,0,0.636,0.055,0,23,56.0,156.39,-1.536,-1.56,634,3210,2063
37.506370892,1785272091.9059322,36.0,robust,tracking,True,1.24,2.85,1.8169289218,1.8081636846,10.95,43.4,39.5,6,5,-3.76,690819.152,690822.79,110.566,-76.732,True,0.05,0.72,0.058,0,0.72,0.058,0,23,32.0,156.77,-1.536,-1.56,636,3220,2063
39.504571912,1785272093.904133,38.0,robust,tracking,True,1.24,2.85,1.8168882102,1.8076411645,11.05,43.1,38.3,6,5,-8.88,690659.561,690668.166,111.385,-76.264,True,0.05,0.403,0.084,0,0.403,0.084,0,23,48.0,147.43,-1.538,-1.56,638,3230,2063
41.502657038,1785272095.9022183,40.0,robust,tracking,True,1.24,2.85,1.8168471874,1.8071184407,11.13,42.7,38.1,6,5,5.56,690520.399,690515.009,111.68,-75.754,True,0.05,0.957,0.102,0,0.957,0.102,0,23,32.0,147.4,-1.538,-1.56,640,3240,2063
43.50032369,1785272097.8998854,42.0,robust,tracking,True,1.24,2.85,1.816809289,1.8065980348,11.21,42.7,37.8,6,5,9.98,690370.621,690360.952,111.268,-76.081,True,0.05,0.807,0.041,0,0.807,0.041,0,23,0.0,136.35,-1.539,-1.56,642,3250,2063
45.712022504,1785272100.111584,44.0,robust,tracking,True,1.24,2.85,1.8167660234,1.8060226878,12.45,43.2,38.0,6,5,-7.85,690184.849,690192.456,111.097,-76.306,True,0.05,0.794,0.037,0,0.794,0.037,0,23,32.0,142.31,-1.538,-1.56,645,3260,2063
47.704670721,1785272102.1042318,46.0,robust,tracking,True,1.24,2.85,1.8167259917,1.8055025786,11.59,42.9,38.7,6,5,-14.42,690023.356,690037.324,110.386,-77.903,True,0.05,0.84,0.099,0,0.84,0.099,0,23,56.0,136.57,-1.539,-1.56,647,3270,2063
49.704359089,1785272104.1039205,48.0,robust,tracking,True,1.24,2.84,1.8166840843,1.804981981,11.04,43.4,38.2,6,5,-2.28,689880.716,689882.925,110.905,-78.019,True,0.05,0.509,0.107,0,0.509,0.107,0,23,16.0,148.6,-1.537,-1.56,649,3280,2063
51.709139916,1785272106.1087012,50.0,robust,tracking,True,1.24,2.84,1.8166456009,1.8044613748,10.99,43.3,38.1,6,5,-0.68,689726.579,689727.233,111.064,-77.798,True,0.05,0.094,0.103,0,0.094,0.103,0,23,48.0,144.29,-1.538,-1.56,651,3290,2063
53.703365655,1785272108.1029265,52.0,robust,tracking,True,1.24,2.84,1.8166083592,1.8039440331,12.68,42.8,37.5,6,5,-8.72,689564.342,689572.793,110.533,-78.338,True,0.05,0.504,0.187,0,0.504,0.187,0,23,8.0,148.54,-1.537,-1.56,653,3300,2063
55.700643307,1785272110.100204,54.0,robust,tracking,True,1.24,2.84,1.8165672875,1.8034221236,11.07,42.9,38.7,6,5,4.12,689421.398,689417.403,111.098,-77.598,True,0.05,0.645,0.08,0,0.645,0.08,0,23,40.0,140.91,-1.539,-1.56,655,3310,2063
57.904055806,1785272112.3036168,56.0,robust,tracking,True,1.24,2.84,1.8165247981,1.8028524558,16.67,42.9,38.2,6,5,-1.04,689243.202,689244.211,110.99,-78.254,True,0.05,0.342,0.02,0,0.342,0.02,0,23,48.0,144.27,-1.538,-1.56,657,3320,2063
60.10451733,1785272114.5040786,58.0,robust,tracking,True,1.24,2.84,1.8164827704,1.8022817921,11.19,42.9,38.0,6,5,-5.64,689066.66,689072.123,111.676,-78.014,True,0.05,0.73,0.15,0,0.73,0.15,0,23,48.0,126.86,-1.541,-1.56,659,3330,2063
62.108759952,1785272116.5083213,60.0,robust,tracking,True,1.24,2.84,1.8164435096,1.8017644864,16.38,43.1,39.2,6,5,-1.09,688913.112,688914.171,111.17,-78.316,True,0.05,0.696,0.007,0,0.696,0.007,0,23,24.0,153.74,-1.537,-1.56,661,3340,2063
64.300493137,1785272118.7000537,62.0,robust,tracking,True,1.24,2.84,1.8163984438,1.8011921234,11.32,42.4,37.4,6,5,8.12,688749.473,688741.606,110.452,-79.51,True,0.05,0.546,0.16,0,0.546,0.16,0,23,40.0,152.97,-1.537,-1.56,663,3350,2063
66.510932531,1785272120.9104934,64.0,robust,tracking,True,1.23,2.84,1.8163566675,1.8006252849,20.15,42.7,38.3,6,5,-2.84,688563.016,688565.769,110.92,-78.813,True,0.05,0.795,0.197,0,0.795,0.197,0,23,40.0,172.22,-1.535,-1.561,665,3360,2063
68.704372014,1785272123.103933,66.0,robust,tracking,True,1.23,2.84,1.8163145194,1.8000555234,15.52,42.8,38.5,6,5,-11.85,688380.019,688391.495,110.061,-80.47,True,0.05,0.796,0.074,0,0.796,0.074,0,23,48.0,155.17,-1.537,-1.56,668,3370,2063
70.703104898,1785272125.102666,68.0,robust,tracking,True,1.23,2.84,1.8162769265,1.7995395272,11.43,42.0,37.8,6,5,13.98,688244.124,688230.58,110.609,-80.264,True,0.05,0.381,0.125,0,0.381,0.125,0,23,16.0,179.45,-1.533,-1.56,670,3380,2063
72.702223062,1785272127.101784,70.0,robust,tracking,True,1.23,2.84,1.8162378878,1.7990227767,16.58,42.8,38.9,6,5,5.88,688078.671,688072.971,110.939,-80.36,True,0.05,0.666,0.053,0,0.666,0.053,0,23,24.0,166.76,-1.535,-1.56,672,3390,2063
74.700286036,1785272129.0998468,72.0,robust,tracking,True,1.23,2.84,1.816199267,1.7985063001,11.1,42.7,38.7,6,5,14.57,687926.671,687912.557,110.527,-80.516,True,0.05,0.312,0.03,0,0.312,0.03,0,23,32.0,162.81,-1.536,-1.56,674,3400,2063
76.71281947,1785272131.1123805,74.0,robust,tracking,True,1.23,2.84,1.8161600375,1.7979893506,11.11,42.7,38.6,6,5,-1.67,687750.089,687751.702,111.576,-79.6,True,0.05,1.026,0.083,0,1.026,0.083,0,23,16.0,142.71,-1.539,-1.56,676,3410,2063
78.704243128,1785272133.103804,76.0,robust,tracking,True,1.23,2.84,1.8161215432,1.7974747243,11.08,43.2,38.1,6,5,-4.48,687586.716,687591.059,110.819,-81.044,True,0.05,0.676,0.104,0,0.676,0.104,0,23,0.0,107.47,-1.544,-1.56,678,3420,2063
80.70401895,1785272135.10358,78.0,robust,tracking,True,1.23,2.84,1.8160831759,1.7969606981,20.64,42.3,37.5,6,5,-0.58,687429.447,687430.01,110.975,-81.135,True,0.05,0.351,0.106,0,0.351,0.106,0,23,56.0,161.62,-1.536,-1.56,680,3430,2063
82.710133903,1785272137.1096952,80.0,robust,tracking,True,1.23,2.84,1.8160471938,1.7964462492,21.18,43.0,39.1,6,5,11.46,687279.439,687268.334,110.976,-80.409,True,0.05,1.15,0.031,0,1.15,0.031,0,23,0.0,177.64,-1.534,-1.56,682,3440,2063
84.702669179,1785272139.1022303,82.0,robust,tracking,True,1.23,2.84,1.8160084174,1.7959315494,16.16,42.7,39.0,6,5,2.07,687108.261,687106.252,110.31,-81.912,True,0.05,0.401,0.04,0,0.401,0.04,0,23,48.0,145.45,-1.539,-1.56,684,3450,2063
86.700732144,1785272141.100293,84.0,robust,tracking,True,1.23,2.83,1.8159713938,1.7954167669,11.13,42.7,39.0,6,5,-12.66,686929.679,686941.939,110.952,-81.183,True,0.05,0.726,0.196,0,0.726,0.196,0,23,24.0,139.84,-1.539,-1.56,686,3460,2063
88.704090128,1785272143.103651,86.0,robust,tracking,True,1.23,2.83,1.8159335443,1.79490275,16.21,42.7,38.7,6,5,8.63,686784.906,686776.542,110.694,-82.024,True,0.05,0.209,0.052,0,0.209,0.052,0,23,48.0,176.01,-1.534,-1.56,688,3470,2063
90.704048278,1785272145.103609,88.0,robust,tracking,True,1.23,2.83,1.8158959585,1.7943905432,24.91,42.5,37.9,6,5,-0.15,686613.039,686613.185,110.563,-82.135,True,0.05,0.379,0.123,0,0.379,0.123,0,23,40.0,161.5,-1.536,-1.56,690,3480,2063
92.711508414,1785272147.1110694,90.0,robust,tracking,True,1.23,2.83,1.8158570425,1.793877413,24.43,42.7,38.0,6,5,15.4,686465.242,686450.326,111.554,-81.637,True,0.05,1.011,0.184,0,1.011,0.184,0,23,56.0,178.15,-1.534,-1.56,692,3490,2063
94.704749369,1785272149.1043103,92.0,robust,tracking,True,1.23,2.83,1.8158190794,1.7933647389,21.48,42.4,38.5,6,5,16.84,686303.4,686287.088,110.534,-82.726,True,0.05,0.195,0.029,0,0.195,0.029,0,23,48.0,171.95,-1.535,-1.56,694,3500,2063
96.703104991,1785272151.1026661,94.0,robust,tracking,True,1.23,2.83,1.815781917,1.7928524573,11.31,42.4,37.6,6,5,-1.05,686119.874,686120.892,109.749,-83.836,True,0.05,0.919,0.158,0,0.919,0.158,0,23,16.0,137.68,-1.54,-1.56,696,3510,2063
98.915123597,1785272153.3146844,96.0,robust,tracking,True,1.23,2.83,1.8157417458,1.792292301,11.49,42.2,37.0,6,5,-4.81,685933.786,685938.441,110.18,-83.485,True,0.05,0.687,0.188,0,0.687,0.188,0,23,40.0,130.24,-1.541,-1.56,698,3520,2063
101.104184426,1785272155.5037456,98.0,robust,tracking,True,1.23,2.83,1.8157012539,1.7917269932,14.98,42.4,37.7,6,5,-4.93,685751.271,685756.046,110.623,-83.514,True,0.05,0.285,0.185,0,0.285,0.185,0,23,40.0,144.59,-1.539,-1.56,700,3530,2063
103.108802454,1785272157.5083635,100.0,robust,tracking,True,1.23,2.83,1.8156626947,1.7912163488,25.23,42.4,38.0,6,5,8.11,685596.218,685588.366,110.401,-83.817,True,0.05,0.362,0.053,0,0.362,0.053,0,23,56.0,172.28,-1.535,-1.56,702,3540,2063
105.105464086,1785272159.505025,102.0,robust,tracking,True,1.23,2.83,1.8156280022,1.79070636,16.51,42.5,37.5,6,5,-15.09,685403.733,685418.356,110.792,-83.601,True,0.05,0.365,0.1,0,0.365,0.1,0,23,48.0,134.38,-1.54,-1.56,704,3550,2063
107.300312593,1785272161.6998734,104.0,robust,tracking,True,1.23,2.83,1.8155869207,1.790143438,20.41,42.6,38.3,6,5,4.06,685236.104,685232.167,111.771,-83.119,True,0.05,1.06,0.172,0,1.06,0.172,0,23,56.0,186.11,-1.532,-1.56,706,3560,2063
109.312626871,1785272163.7121878,106.0,robust,tracking,True,1.23,2.83,1.8155513007,1.7896363278,15.58,42.6,37.5,6,5,-5.9,685055.343,685061.062,110.302,-84.628,True,0.05,0.319,0.208,0,0.319,0.208,0,23,24.0,168.36,-1.535,-1.56,708,3570,2063
111.306453913,1785272165.7060149,108.0,robust,tracking,True,1.23,2.83,1.8155128487,1.789126556,21.63,42.6,38.0,6,5,16.48,684906.901,684890.938,110.502,-84.943,True,0.05,0.385,0.093,0,0.385,0.093,0,23,16.0,177.8,-1.534,-1.56,710,3580,2063
113.305323246,1785272167.7048843,110.0,robust,tracking,True,1.22,2.83,1.8154776515,1.7886176858,11.1,42.9,38.4,6,5,1.51,684721.855,684720.397,110.396,-84.747,True,0.05,0.197,0.218,0,0.197,0.218,0,23,32.0,163.81,-1.536,-1.56,712,3590,2063
115.30411121,1785272169.7036724,112.0,robust,tracking,True,1.22,2.83,1.8154394251,1.7881082833,22.64,43.1,38.0,6,5,-3.4,684547.463,684550.752,110.44,-85.241,True,0.05,0.429,0.029,0,0.429,0.029,0,23,8.0,152.36,-1.538,-1.56,714,3600,2063
117.303101565,1785272171.702663,114.0,robust,tracking,True,1.22,2.83,1.8154034756,1.7875997643,11.52,42.6,38.1,6,5,17.65,684398.444,684381.346,110.707,-84.967,True,0.05,0.405,0.059,0,0.405,0.059,0,23,24.0,192.86,-1.532,-1.56,716,3610,2063
119.306150448,1785272173.7057114,116.0,robust,tracking,True,1.22,2.83,1.8153676614,1.7870927007,12.04,42.5,37.5,6,5,-7.99,684203.354,684211.094,110.164,-85.459,True,0.05,0.631,0.155,0,0.631,0.155,0,23,32.0,144.3,-1.539,-1.56,718,3620,2063
121.300962576,1785272175.7005236,118.0,robust,tracking,True,1.22,2.83,1.8153303586,1.786584068,18.38,42.4,37.2,6,5,15.28,684055.675,684040.872,110.141,-86.063,True,0.05,0.481,0.133,0,0.481,0.133,0,23,48.0,200.68,-1.53,-1.56,720,3630,2063
123.303755452,1785272177.7033165,120.0,robust,tracking,True,1.22,2.82,1.815294335,1.7860767164,19.1,42.8,38.8,6,5,2.02,683871.81,683869.856,111.056,-85.022,True,0.05,1.151,0.085,0,1.151,0.085,0,23,64.0,169.36,-1.535,-1.56,722,3640,2063
125.304219262,1785272179.703781,122.0,robust,tracking,True,1.22,2.82,1.8152578155,1.785570688,11.11,42.7,38.6,6,5,-8.82,683688.289,683696.831,110.802,-85.693,True,0.05,0.637,0.051,0,0.637,0.051,0,23,8.0,158.16,-1.537,-1.56,724,3650,2063
127.304583545,1785272181.704145,124.0,robust,tracking,True,1.22,2.82,1.8152215994,1.7850628719,11.13,42.7,38.8,6,5,-1.48,683523.082,683524.519,110.235,-86.917,True,0.05,0.775,0.068,0,0.775,0.068,0,23,40.0,151.87,-1.538,-1.56,726,3660,2063
129.508905566,1785272183.9084663,126.0,robust,tracking,True,1.22,2.82,1.8151838123,1.7845085023,12.03,43.1,38.6,6,5,9.01,683342.021,683333.296,110.201,-87.019,True,0.05,0.331,0.094,0,0.331,0.094,0,23,0.0,157.45,-1.537,-1.56,728,3670,2063
131.500762179,1785272185.9003232,128.0,robust,tracking,True,1.22,2.82,1.8151487619,1.7840006032,12.83,42.6,38.6,6,5,3.63,683160.888,683157.371,110.357,-86.937,True,0.05,0.389,0.075,0,0.389,0.075,0,23,32.0,171.39,-1.535,-1.56,730,3680,2063
133.504344505,1785272187.9039054,130.0,robust,tracking,True,1.22,2.82,1.8151127862,1.7834955606,12.64,42.7,38.5,6,5,3.69,682986.797,682983.217,110.053,-87.63,True,0.05,0.791,0.01,0,0.791,0.01,0,23,0.0,175.65,-1.534,-1.56,732,3690,2063
135.509832366,1785272189.9094036,132.0,robust,tracking,True,1.22,2.82,1.8150762158,1.7829890753,11.13,42.7,38.4,6,5,5.1,682813.848,682808.909,110.531,-87.346,True,0.05,0.079,0.034,0,0.079,0.034,0,23,16.0,162.81,-1.536,-1.56,734,3700,2063
137.506720578,1785272191.9062817,134.0,robust,tracking,True,1.22,2.82,1.8150397064,1.7824837171,10.99,42.8,39.5,6,5,1.42,682635.318,682633.946,110.584,-87.635,True,0.05,0.038,0.097,0,0.038,0.097,0,23,32.0,155.09,-1.537,-1.56,736,3710,2063
139.504410571,1785272193.9039717,136.0,robust,tracking,True,1.22,2.82,1.8150041072,1.7819780719,11.15,42.7,38.1,6,5,-6.72,682453.853,682460.364,110.886,-87.452,True,0.05,0.412,0.407,0,0.412,0.407,0,23,48.0,139.96,-1.539,-1.56,738,3720,2063
141.503078598,1785272195.9026399,138.0,robust,tracking,True,1.22,2.82,1.8149699154,1.781475266,11.11,42.7,38.8,6,5,11.45,682296.086,682284.991,109.638,-89.46,True,0.05,1.419,0.005,0,1.419,0.005,0,23,40.0,167.17,-1.535,-1.56,740,3730,2063
143.500493916,1785272197.900055,140.0,robust,tracking,True,1.22,2.82,1.8149354252,1.7809734693,11.02,42.9,38.7,6,5,12.89,682120.284,682107.799,110.911,-87.736,True,0.05,0.88,0.067,0,0.88,0.067,0,23,56.0,162.67,-1.536,-1.56,742,3740,2063
145.512927625,1785272199.9124885,142.0,robust,tracking,True,1.22,2.82,1.8149005029,1.7804693538,10.96,42.9,39.4,6,5,-25.89,681906.117,681931.202,110.881,-87.866,True,0.05,0.775,0.078,0,0.775,0.078,0,23,8.0,107.83,-1.544,-1.56,744,3750,2063
147.506278297,1785272201.9058392,144.0,robust,tracking,True,1.22,2.82,1.8148661739,1.7799667491,10.98,43.0,39.0,6,5,-5.66,681746.195,681751.678,109.788,-89.613,True,0.05,1.009,0.035,0,1.009,0.035,0,23,56.0,152.27,-1.538,-1.56,746,3760,2063
149.505513865,1785272203.9050748,146.0,robust,tracking,True,1.22,2.82,1.8148312963,1.779461219,11.25,42.4,38.4,6,5,-7.95,681563.852,681571.554,110.854,-89.368,True,0.05,0.482,0.099,0,0.482,0.099,0,23,56.0,142.08,-1.539,-1.56,748,3770,2063
151.504103133,1785272205.903664,148.0,robust,tracking,True,1.22,2.82,1.8147976667,1.778959756,10.98,43.0,39.4,6,5,11.06,681403.38,681392.662,110.425,-89.358,True,0.05,0.199,0.058,0,0.199,0.058,0,23,16.0,161.29,-1.536,-1.56,750,3780,2063
153.502847324,1785272207.9024084,150.0,robust,tracking,True,1.22,2.82,1.8147608671,1.7784571849,15.23,42.6,38.9,6,5,9.66,681223.637,681214.276,110.279,-89.721,True,0.05,0.371,0.218,0,0.371,0.218,0,23,48.0,172.12,-1.535,-1.56,752,3790,2063
155.506297511,1785272209.9058583,152.0,robust,tracking,True,1.22,2.82,1.8147266277,1.7779565566,11.07,42.6,38.7,6,5,0.35,681034.417,681034.073,110.263,-89.772,True,0.05,0.371,0.13,0,0.371,0.13,0,23,56.0,180.04,-1.533,-1.56,754,3800,2063
157.500753602,1785272211.9003143,154.0,robust,tracking,True,1.22,2.82,1.814692541,1.777454151,11.13,42.4,38.4,6,5,8.94,680862.299,680853.639,110.737,-89.433,True,0.05,0.755,0.053,0,0.755,0.053,0,23,0.0,180.5,-1.533,-1.56,756,3810,2063
159.504450759,1785272213.904012,156.0,robust,tracking,True,1.22,2.82,1.8146585955,1.7769539906,11.22,42.3,38.2,6,5,14.47,680691.31,680677.294,110.442,-90.141,True,0.05,0.405,0.02,0,0.405,0.02,0,23,32.0,196.03,-1.531,-1.56,758,3820,2063
161.510423884,1785272215.9099848,158.0,robust,tracking,True,1.21,2.82,1.8146253251,1.7764523761,14.07,42.3,38.6,6,5,14.17,680511.698,680497.972,110.887,-89.977,True,0.05,0.551,0.182,0,0.551,0.182,0,23,32.0,184.51,-1.532,-1.56,760,3830,2063
163.504960681,1785272217.9045217,160.0,robust,tracking,True,1.21,2.81,1.8145907816,1.7759535814,10.99,42.6,39.7,6,5,9.57,680326.822,680317.554,110.339,-91.079,True,0.05,0.465,0.01,0,0.465,0.01,0,23,8.0,150.36,-1.538,-1.56,762,3840,2063
165.507220868,1785272219.9067822,162.0,robust,tracking,True,1.21,2.81,1.8145551226,1.7754518476,10.98,42.8,38.9,6,5,5.13,680143.185,680138.216,109.975,-91.641,True,0.05,0.834,0.034,0,0.834,0.034,0,23,0.0,144.58,-1.539,-1.56,764,3850,2063
167.503525545,1785272221.9030867,164.0,robust,tracking,True,1.21,2.81,1.8145228906,1.7749543943,14.36,42.8,38.7,6,5,4.72,679959.278,679954.706,110.179,-91.65,True,0.05,0.706,0.085,0,0.706,0.085,0,23,48.0,124.01,-1.542,-1.56,766,3860,2063
169.50061802,1785272223.900179,166.0,robust,tracking,True,1.21,2.81,1.8144898435,1.7744531221,11.09,42.8,37.7,6,5,13.57,679787.48,679774.331,110.775,-91.606,True,0.05,1.106,0.075,0,1.106,0.075,0,23,56.0,145.18,-1.538,-1.56,768,3870,2063
171.510388267,1785272225.9099493,168.0,robust,tracking,True,1.21,2.81,1.8144559568,1.7739550407,11.08,42.6,38.2,6,5,8.28,679599.18,679591.156,110.344,-91.505,True,0.05,0.285,0.062,0,0.285,0.062,0,23,56.0,137.18,-1.54,-1.56,770,3880,2063
173.500668495,1785272227.9002295,170.0,robust,tracking,True,1.21,2.81,1.8144213808,1.7734561125,11.07,42.5,38.0,6,5,-7.05,679401.0,679407.83,110.355,-91.896,True,0.05,0.239,0.023,0,0.239,0.023,0,23,48.0,151.22,-1.538,-1.56,772,3890,2063
175.509072834,1785272229.9086337,172.0,robust,tracking,True,1.21,2.81,1.8143875329,1.7729571416,14.88,42.6,38.0,6,5,-9.75,679214.759,679224.203,110.051,-92.203,True,0.05,0.388,0.024,0,0.388,0.024,0,23,16.0,127.04,-1.541,-1.56,774,3900,2063
177.511375431,1785272231.9109368,174.0,robust,tracking,True,1.21,2.81,1.8143541178,1.7724598606,11.01,42.7,39.1,6,5,-28.57,679010.25,679037.93,110.131,-92.548,True,0.05,0.679,0.066,0,0.679,0.066,0,23,16.0,144.09,-1.539,-1.56,776,3910,2063
179.5031023,1785272233.9026635,176.0,robust,tracking,True,1.21,2.81,1.8143216536,1.7719632746,11.04,42.6,38.1,6,5,-1.94,678850.304,678852.181,110.443,-92.232,True,0.05,0.439,0.164,0,0.439,0.164,0,23,24.0,156.61,-1.537,-1.56,778,3920,2063
181.500919559,1785272235.9004805,178.0,robust,tracking,True,1.21,2.81,1.8142885244,1.7714647145,11.67,42.7,39.6,6,5,14.37,678680.998,678667.079,110.223,-93.101,True,0.05,0.927,0.048,0,0.927,0.048,0,23,24.0,174.87,-1.534,-1.56,780,3930,2063
183.504154069,1785272237.903716,180.0,robust,tracking,True,1.21,2.81,1.8142515963,1.7709676955,11.12,42.4,38.5,6,5,3.52,678484.103,678480.688,110.013,-93.55,True,0.05,0.69,0.056,0,0.69,0.056,0,23,0.0,167.43,-1.535,-1.56,782,3940,2063
185.509701079,1785272239.9092622,182.0,robust,tracking,True,1.21,2.81,1.8142225906,1.7704719824,11.12,42.5,38.1,6,5,2.8,678298.275,678295.56,110.36,-92.989,True,0.05,0.188,0.156,0,0.188,0.156,0,23,0.0,175.14,-1.534,-1.56,784,3950,2063
187.511311613,1785272241.9108725,184.0,robust,tracking,True,1.21,2.81,1.8141898144,1.7699731912,11.11,42.3,38.5,6,5,-16.24,678094.632,678110.363,110.845,-93.036,True,0.05,0.46,0.009,0,0.46,0.009,0,23,32.0,150.91,-1.537,-1.56,786,3960,2063
189.504094586,1785272243.9036555,186.0,robust,tracking,True,1.21,2.81,1.8141560815,1.7694787336,11.13,42.5,37.9,6,5,-1.22,677924.284,677925.468,110.012,-93.838,True,0.05,0.542,0.031,0,0.542,0.031,0,23,32.0,167.75,-1.535,-1.56,788,3970,2063
191.503264307,1785272245.902825,188.0,robust,tracking,True,1.21,2.81,1.8141223587,1.7689845528,11.12,42.4,38.3,6,5,12.44,677751.751,677739.703,110.673,-93.502,True,0.05,0.691,0.123,0,0.691,0.123,0,23,32.0,181.14,-1.533,-1.56,790,3980,2063
193.500850983,1785272247.9004118,190.0,robust,tracking,True,1.21,2.81,1.8140919959,1.768488668,14.03,42.3,38.2,6,5,8.52,677559.555,677551.298,110.495,-93.954,True,0.05,0.376,0.004,0,0.376,0.004,0,23,56.0,155.05,-1.537,-1.56,792,3990,2063
195.507436225,1785272249.9069972,192.0,robust,tracking,True,1.21,2.81,1.8140572992,1.7679932608,10.97,42.9,38.9,6,5,-25.61,677336.289,677361.101,110.053,-94.87,True,0.05,0.565,0.132,0,0.565,0.132,0,23,0.0,143.1,-1.539,-1.56,794,4000,2063
197.710664279,1785272252.1102252,194.0,robust,tracking,True,1.21,2.81,1.8140224199,1.7674512561,11.06,42.7,38.3,6,5,-21.21,677131.244,677151.796,110.436,-94.668,True,0.05,0.467,0.099,0,0.467,0.099,0,23,16.0,127.66,-1.541,-1.56,797,4010,2063
199.904494084,1785272254.3040552,196.0,robust,tracking,True,1.21,2.81,1.8139878446,1.7669062479,11.21,42.4,37.9,6,5,2.5,676945.184,676942.759,110.719,-94.592,True,0.05,0.329,0.254,0,0.329,0.254,0,23,0.0,166.66,-1.535,-1.56,799,4020,2063
201.902748265,1785272256.3023093,198.0,robust,tracking,True,1.21,2.8,1.8139551469,1.7664122598,19.71,42.7,38.2,6,5,3.44,676756.483,676753.152,110.284,-95.508,True,0.05,0.834,0.075,0,0.834,0.075,0,23,48.0,159.1,-1.536,-1.56,801,4030,2063
203.900860006,1785272258.3004212,200.0,robust,tracking,True,1.21,2.8,1.8139243484,1.7659203155,21.27,42.6,37.9,6,5,16.7,676577.254,676561.076,111.005,-94.638,True,0.05,0.65,0.026,0,0.65,0.026,0,23,40.0,185.48,-1.532,-1.56,803,4040,2063
205.904311168,1785272260.303872,202.0,robust,tracking,True,1.21,2.8,1.8138926701,1.7654272361,11.23,42.3,37.5,6,5,-4.45,676364.571,676368.878,111.007,-94.853,True,0.05,0.681,0.154,0,0.681,0.154,0,23,32.0,146.48,-1.538,-1.56,805,4050,2063
207.911054786,1785272262.3106158,204.0,robust,tracking,True,1.21,2.8,1.8138607249,1.7649342967,11.29,42.2,37.1,6,5,-0.42,676178.038,676178.445,111.268,-95.121,True,0.05,0.669,0.187,0,0.669,0.187,0,23,56.0,148.37,-1.538,-1.56,807,4060,2063
209.904852911,1785272264.304414,206.0,robust,tracking,True,1.21,2.8,1.8138277745,1.7644418155,17.32,42.6,38.4,6,5,5.29,675993.222,675988.099,110.799,-95.578,True,0.05,0.772,0.012,0,0.772,0.012,0,23,48.0,167.53,-1.535,-1.56,809,4070,2063
211.904389957,1785272266.3039508,208.0,robust,tracking,True,1.2,2.8,1.8137949136,1.7639490637,14.88,42.5,38.1,6,5,4.76,675802.554,675797.942,109.97,-97.009,True,0.05,1.186,0.157,0,1.186,0.157,0,23,16.0,161.37,-1.536,-1.56,811,4080,2063
213.908887407,1785272268.3084483,210.0,robust,tracking,True,1.2,2.8,1.8137652527,1.7634585771,13.65,41.9,38.5,6,5,-19.97,675583.7,675603.041,110.734,-96.484,True,0.05,0.11,0.069,0,0.11,0.069,0,23,8.0,134.36,-1.54,-1.56,813,4090,2063
215.900878981,1785272270.3004403,212.0,robust,tracking,True,1.2,2.8,1.8137346862,1.7629700707,13.14,42.4,39.0,6,5,1.39,675411.049,675409.707,110.477,-96.41,True,0.05,0.489,0.149,0,0.489,0.149,0,23,48.0,164.12,-1.535,-1.56,815,4100,2063
217.908715503,1785272272.3082767,214.0,robust,tracking,True,1.2,2.8,1.8137007679,1.7624770049,11.19,42.1,38.1,6,5,-3.51,675214.305,675217.703,110.227,-97.513,True,0.05,0.667,0.022,0,0.667,0.022,0,23,56.0,141.12,-1.539,-1.56,817,4110,2063
219.904589627,1785272274.3041506,216.0,robust,tracking,True,1.2,2.8,1.8136705307,1.7619846326,17.3,42.2,38.8,6,5,-14.02,675007.687,675021.273,110.312,-97.218,True,0.05,0.452,0.069,0,0.452,0.069,0,23,24.0,158.28,-1.536,-1.56,819,4120,2063
221.9039367,1785272276.3034978,218.0,robust,tracking,True,1.2,2.8,1.8136409999,1.7614933749,11.12,42.1,39.2,6,5,-3.83,674821.953,674825.66,111.06,-96.371,True,0.05,1.326,0.028,0,1.326,0.028,0,23,48.0,170.5,-1.534,-1.56,821,4130,2063
223.91008895,1785272278.3096502,220.0,robust,tracking,True,1.2,2.8,1.8136105372,1.7610043295,11.73,42.3,38.7,6,5,0.13,674630.412,674630.288,111.079,-97.301,True,0.05,0.408,0.148,0,0.408,0.148,0,23,56.0,157.28,-1.536,-1.56,823,4140,2063
225.902916967,1785272280.302478,222.0,robust,tracking,True,1.2,2.8,1.8135794516,1.7605142092,21.26,41.9,39.1,6,5,-21.67,674413.179,674434.176,111.065,-97.127,True,0.05,0.752,0.068,0,0.752,0.068,0,23,48.0,124.73,-1.541,-1.56,825,4150,2063
227.901755726,1785272282.3013165,224.0,robust,tracking,True,1.2,2.8,1.8135490001,1.7600248185,11.02,42.5,39.3,6,5,13.8,674252.975,674239.611,110.715,-97.719,True,0.05,0.748,0.078,0,0.748,0.078,0,23,32.0,159.45,-1.536,-1.56,827,4160,2063
229.901619429,1785272284.3011806,226.0,robust,tracking,True,1.2,2.8,1.8135177823,1.7595391681,11.05,42.4,39.1,6,5,12.06,674053.956,674042.275,110.355,-98.939,True,0.05,0.78,0.043,0,0.78,0.043,0,23,56.0,170.95,-1.534,-1.56,829,4170,2063
231.90481137,1785272286.3043725,228.0,robust,tracking,True,1.2,2.8,1.8134877896,1.7590492494,11.09,42.4,38.1,6,5,12.72,673856.473,673844.146,110.721,-98.388,True,0.05,0.538,0.188,0,0.538,0.188,0,23,48.0,170.54,-1.534,-1.56,831,4180,2063
233.905326912,1785272288.3048878,230.0,robust,tracking,True,1.2,2.8,1.8134570418,1.7585604429,11.06,42.5,38.9,6,5,5.44,673653.112,673647.84,110.098,-98.963,True,0.05,0.301,0.311,0,0.301,0.311,0,23,8.0,171.59,-1.534,-1.56,833,4190,2063
235.904458614,1785272290.3040195,232.0,robust,tracking,True,1.2,2.8,1.8134261204,1.7580735849,11.06,42.5,38.5,6,5,-4.62,673445.637,673450.117,110.459,-98.844,True,0.05,0.829,0.028,0,0.829,0.028,0,23,0.0,153.73,-1.537,-1.56,835,4200,2063
237.904071483,1785272292.3036325,234.0,robust,tracking,True,1.2,2.8,1.8133959704,1.7575868747,12.8,42.0,37.7,6,5,-7.87,673243.852,673251.478,110.772,-98.845,True,0.05,0.559,0.037,0,0.559,0.037,0,23,0.0,146.96,-1.538,-1.56,837,4210,2063
239.908760739,1785272294.3083217,236.0,robust,tracking,True,1.2,2.8,1.8133666091,1.7570976374,11.1,42.3,38.6,6,5,4.99,673058.134,673053.301,110.565,-99.434,True,0.05,0.506,0.016,0,0.506,0.016,0,23,24.0,168.65,-1.535,-1.56,839,4220,2063
241.900563008,1785272296.300124,238.0,robust,tracking,True,1.2,2.79,1.8133344617,1.7566107514,11.14,42.3,38.4,6,5,-14.57,672839.788,672853.907,110.365,-99.522,True,0.05,0.253,0.141,0,0.253,0.141,0,23,0.0,147.65,-1.538,-1.56,841,4230,2063
243.904645899,1785272298.3042068,240.0,robust,tracking,True,1.2,2.79,1.8133057846,1.7561227378,11.1,42.2,38.8,6,5,3.18,672658.12,672655.039,109.903,-100.274,True,0.05,0.476,0.033,0,0.476,0.033,0,23,32.0,164.64,-1.535,-1.56,843,4240,2063
245.904621193,1785272300.3041825,242.0,robust,tracking,True,1.2,2.79,1.8132777969,1.7556370904,11.14,42.2,38.7,6,5,-0.81,672454.836,672455.618,110.573,-100.187,True,0.05,0.405,0.013,0,0.405,0.013,0,23,16.0,134.41,-1.54,-1.56,845,4250,2063
247.899436713,1785272302.2989979,244.0,robust,tracking,True,1.2,2.79,1.8132463791,1.7551512789,11.16,42.2,39.0,6,5,13.48,672269.138,672256.082,110.183,-100.179,True,0.05,0.565,0.113,0,0.565,0.113,0,23,40.0,169.04,-1.535,-1.56,847,4260,2063
249.911369626,1785272304.3109307,246.0,robust,tracking,True,1.2,2.79,1.813217727,1.754665179,11.11,42.5,39.2,6,5,-6.09,672049.638,672055.536,109.774,-100.683,True,0.05,0.486,0.021,0,0.486,0.021,0,23,0.0,135.93,-1.539,-1.56,849,4270,2063
251.903384606,1785272306.3029459,248.0,robust,tracking,True,1.2,2.79,1.8131881994,1.7541830169,11.18,42.2,39.0,6,5,10.96,671865.57,671854.952,109.778,-101.09,True,0.05,0.278,0.027,0,0.278,0.027,0,23,48.0,182.36,-1.532,-1.56,851,4280,2063
253.900781619,1785272308.3003423,250.0,robust,tracking,True,1.2,2.79,1.8131589097,1.7536951988,11.17,42.3,38.9,6,5,3.55,671659.189,671655.749,110.155,-100.561,True,0.05,0.584,0.008,0,0.584,0.008,0,23,16.0,141.85,-1.539,-1.56,853,4290,2063
255.910281973,1785272310.3098433,252.0,robust,tracking,True,1.2,2.79,1.8131293194,1.753212297,11.2,42.1,38.4,6,5,7.57,671462.581,671455.25,109.95,-101.447,True,0.05,0.189,0.096,0,0.189,0.096,0,23,16.0,141.76,-1.539,-1.56,855,4300,2063
257.904754461,1785272312.3043153,254.0,robust,tracking,True,1.2,2.79,1.8131029159,1.7527265372,11.15,42.4,38.7,6,5,-6.95,671245.456,671252.194,110.05,-101.621,True,0.05,0.518,0.084,0,0.518,0.084,0,23,56.0,134.53,-1.54,-1.56,857,4310,2063
259.904898749,1785272314.3044596,256.0,robust,tracking,True,1.2,2.79,1.8130720433,1.7522445406,11.3,41.8,38.4,6,5,-4.74,671044.584,671049.173,110.162,-101.624,True,0.05,0.281,0.225,0,0.281,0.225,0,23,0.0,142.99,-1.539,-1.56,859,4320,2063
261.904207465,1785272316.3037684,258.0,robust,tracking,True,1.19,2.79,1.8130440645,1.7517582107,11.31,42.0,38.4,6,5,-7.29,670837.352,670844.416,110.701,-101.27,True,0.05,0.844,0.152,0,0.844,0.152,0,23,56.0,167.54,-1.535,-1.56,861,4330,2063
263.903658409,1785272318.3032198,260.0,robust,tracking,True,1.19,2.79,1.8130134772,1.7512773631,11.34,42.0,38.5,6,5,-1.16,670637.855,670638.976,110.001,-102.901,True,0.05,0.697,0.02,0,0.697,0.02,0,23,48.0,166.85,-1.535,-1.56,863,4340,2063
265.906678701,1785272320.3062396,262.0,robust,tracking,True,1.19,2.79,1.8129886444,1.7507938348,14.74,42.5,38.9,6,5,10.12,670442.306,670432.505,110.568,-101.927,True,0.05,0.908,0.068,0,0.908,0.068,0,23,40.0,192.58,-1.531,-1.56,865,4350,2063
267.908740259,1785272322.308301,264.0,robust,tracking,True,1.19,2.79,1.8129614176,1.750311896,11.27,42.3,37.7,6,5,-11.12,670213.856,670224.632,110.96,-102.072,True,0.05,0.648,0.323,0,0.648,0.323,0,23,56.0,163.77,-1.535,-1.56,867,4360,2063
269.904666955,1785272324.304228,266.0,robust,tracking,True,1.19,2.79,1.8129315339,1.7498285268,11.35,41.9,37.9,6,5,0.23,670016.459,670016.233,110.584,-102.496,True,0.05,0.457,0.073,0,0.457,0.073,0,23,48.0,200.12,-1.53,-1.56,869,4370,2063
271.904417064,1785272326.303978,268.0,robust,tracking,True,1.19,2.79,1.8129033974,1.7493462127,11.29,42.0,38.2,6,5,-11.63,669798.53,669809.801,110.923,-102.202,True,0.05,0.955,0.178,0,0.955,0.178,0,23,16.0,154.9,-1.537,-1.56,871,4380,2063
273.904287144,1785272328.3038483,270.0,robust,tracking,True,1.19,2.79,1.8128759146,1.7488645046,11.24,42.2,38.4,6,5,0.32,669604.755,669604.44,110.23,-103.356,True,0.05,0.559,0.043,0,0.559,0.043,0,23,56.0,147.74,-1.538,-1.56,873,4390,2063
275.905752343,1785272330.3053133,272.0,robust,tracking,True,1.19,2.79,1.8128467845,1.7483871309,11.29,42.1,38.0,6,5,0.52,669397.133,669396.627,110.767,-103.125,True,0.05,0.839,0.033,0,0.839,0.033,0,23,48.0,175.42,-1.533,-1.56,875,4400,2063
277.901721957,1785272332.3012831,274.0,robust,tracking,True,1.19,2.79,1.8128179501,1.7479069779,11.23,42.2,38.8,6,5,-15.46,669172.771,669187.748,110.557,-103.518,True,0.05,0.505,0.152,0,0.505,0.152,0,23,8.0,141.55,-1.539,-1.56,877,4410,2063
279.901270286,1785272334.3008313,276.0,robust,tracking,True,1.19,2.79,1.8127918148,1.7474239577,11.26,42.2,38.9,6,5,5.21,668986.416,668981.367,110.705,-103.667,True,0.05,0.415,0.13,0,0.415,0.13,0,23,56.0,164.7,-1.535,-1.56,879,4420,2063
281.911805396,1785272336.3113663,278.0,robust,tracking,True,1.19,2.78,1.812764459,1.7469430861,11.14,42.3,38.6,6,5,9.2,668783.357,668774.443,110.765,-103.653,True,0.05,0.976,0.047,0,0.976,0.047,0,23,40.0,167.02,-1.535,-1.56,881,4430,2063
283.904627638,1785272338.3041887,280.0,robust,tracking,True,1.19,2.78,1.8127364977,1.7464643722,11.13,42.4,38.9,6,5,1.9,668567.589,668565.747,110.702,-104.554,True,0.05,0.496,0.084,0,0.496,0.084,0,23,56.0,150.73,-1.537,-1.56,883,4440,2063
285.904540282,1785272340.3041015,282.0,robust,tracking,True,1.19,2.78,1.8127111868,1.7459831701,11.26,42.2,38.2,6,5,-21.52,668336.38,668357.228,110.484,-104.5,True,0.05,0.4,0.023,0,0.4,0.023,0,23,16.0,127.92,-1.541,-1.56,885,4450,2063
287.903906607,1785272342.3034678,284.0,robust,tracking,True,1.19,2.78,1.8126838601,1.7455066962,11.09,42.3,39.2,6,5,-0.35,668145.403,668145.742,110.548,-104.863,True,0.05,0.13,0.213,0,0.13,0.213,0,23,0.0,156.5,-1.536,-1.56,887,4460,2063
289.903519609,1785272344.3030806,286.0,robust,tracking,True,1.19,2.78,1.8126545436,1.74502695,11.28,42.2,38.0,6,5,-17.14,667916.867,667933.474,110.923,-105.07,True,0.05,0.412,0.112,0,0.412,0.112,0,23,8.0,145.17,-1.538,-1.56,889,4470,2063
291.906994829,1785272346.3065557,288.0,robust,tracking,True,1.19,2.78,1.812627295,1.7445482935,12.84,42.5,38.6,6,5,-1.31,667721.551,667722.816,109.629,-106.134,True,0.05,0.721,0.164,0,0.721,0.164,0,23,40.0,158.89,-1.536,-1.56,891,4480,2063
293.904251457,1785272348.3038123,290.0,robust,tracking,True,1.19,2.78,1.8126009474,1.7440682755,11.16,42.3,38.2,6,5,14.39,667524.719,667510.777,110.381,-105.665,True,0.05,0.161,0.079,0,0.161,0.079,0,23,32.0,184.17,-1.532,-1.56,893,4490,2063
295.904732818,1785272350.3042939,292.0,robust,tracking,True,1.19,2.78,1.8125748376,1.7435915248,11.06,42.6,38.1,6,5,-9.72,667289.655,667299.068,110.248,-105.873,True,0.05,0.291,0.005,0,0.291,0.005,0,23,32.0,159.99,-1.536,-1.56,895,4500,2063
297.910797167,1785272352.3103583,294.0,robust,tracking,True,1.19,2.78,1.8125472744,1.7431147283,11.0,42.9,38.4,6,5,9.37,667097.136,667088.06,110.084,-106.181,True,0.05,0.237,0.074,0,0.237,0.074,0,23,16.0,168.11,-1.535,-1.56,897,4510,2063
299.905108443,1785272354.3046699,296.0,robust,tracking,True,1.19,2.78,1.812521259,1.7426375476,16.12,42.5,37.8,6,5,-12.01,666863.837,666875.472,110.096,-106.613,True,0.05,0.693,0.156,0,0.693,0.156,0,23,64.0,124.56,-1.541,-1.56,899,4520,2063
1 host_monotonic_s host_unix_s time_s profile mode locked tdop gdop hdop vdop clock_sigma_m cn0_mean_dbhz cn0_min_dbhz tracked_satellites used_satellites innovation_m raw_bias_m filtered_bias_m clock_drift_mps bias_drift_mps predictor_enabled eskf_alpha eskf_speed_mps eskf_velocity_rms_mps eskf_fde_mask pps_speed_mps pps_velocity_rms_mps pps_fde_mask reference_svid capture_correction_ns hardware_phase_ns rate_command_ppm integrator_ppm pulse_count sequence status
2 1.50420269 1785272055.903764 0.0 robust tracking True 1.25 2.86 1.8176601213 1.8176260703 12.54 42.7 37.7 6 5 4.52 693498.817 693494.442 110.788 -72.055 True 0.05 0.271 0.185 0 0.271 0.185 0 23 32.0 160.47 -1.536 -1.56 600 3040 2063
3 3.503689524 1785272057.9032507 2.0 robust tracking True 1.25 2.86 1.8176193953 1.8170996688 11.17 42.9 38.2 6 5 -19.2 693330.906 693349.504 111.201 -71.738 True 0.05 0.746 0.135 0 0.746 0.135 0 23 56.0 124.38 -1.541 -1.56 602 3050 2063
4 5.500479302 1785272059.9000404 4.0 robust tracking True 1.25 2.86 1.8175780522 1.8165687846 21.48 42.7 38.5 6 5 -7.0 693197.92 693204.698 111.206 -71.912 True 0.05 0.836 0.039 0 0.836 0.039 0 23 24.0 141.5 -1.539 -1.56 604 3060 2063
5 7.509988539 1785272061.9095497 6.0 robust tracking True 1.25 2.86 1.8175381286 1.816041859 11.14 43.0 38.3 6 5 13.6 693072.274 693059.102 111.221 -71.825 True 0.05 1.009 0.28 0 1.009 0.28 0 23 32.0 168.11 -1.535 -1.56 606 3070 2063
6 9.511653187 1785272063.9112144 8.0 robust tracking True 1.25 2.86 1.8174962624 1.8155120506 11.56 42.9 38.0 6 5 -31.15 692881.989 692912.165 110.864 -72.861 True 0.05 0.371 0.04 0 0.371 0.04 0 23 40.0 135.63 -1.539 -1.56 608 3080 2063
7 11.50378733 1785272065.9033487 10.0 robust tracking True 1.25 2.85 1.8174539102 1.8149874373 11.07 42.8 38.6 6 5 -9.78 692754.373 692763.848 110.014 -73.882 True 0.05 0.843 0.004 0 0.843 0.004 0 23 56.0 155.42 -1.536 -1.56 610 3090 2063
8 13.504178648 1785272067.9037397 12.0 robust tracking True 1.25 2.85 1.817413463 1.8144599125 11.18 42.9 38.1 6 5 6.48 692623.758 692617.477 110.329 -73.94 True 0.05 0.609 0.096 0 0.609 0.096 0 23 0.0 162.66 -1.535 -1.56 612 3100 2063
9 15.503554083 1785272069.9031153 14.0 robust tracking True 1.25 2.85 1.8173718635 1.8139319104 11.26 42.7 37.0 6 5 -1.31 692469.561 692470.834 111.54 -72.849 True 0.05 0.953 0.31 0 0.953 0.31 0 23 56.0 155.78 -1.536 -1.56 614 3110 2063
10 17.502924749 1785272071.9024858 16.0 robust tracking True 1.25 2.85 1.81733102 1.8134069522 15.4 42.4 36.5 6 5 -11.0 692313.987 692324.644 110.815 -73.581 True 0.05 0.736 0.168 0 0.736 0.168 0 23 32.0 133.05 -1.54 -1.56 616 3120 2063
11 19.506236288 1785272073.9057975 18.0 robust tracking True 1.24 2.85 1.8172902798 1.8128821788 11.11 43.1 38.1 6 5 -5.77 692171.911 692177.505 111.077 -73.779 True 0.05 0.4 0.027 0 0.4 0.027 0 23 24.0 131.86 -1.54 -1.56 618 3130 2063
12 21.504228726 1785272075.9037895 20.0 robust tracking True 1.24 2.85 1.8172490099 1.8123554135 11.15 43.0 38.0 6 5 8.57 692038.349 692030.045 110.612 -74.424 True 0.05 0.512 0.053 0 0.512 0.053 0 23 24.0 156.87 -1.536 -1.56 620 3140 2063
13 23.504153615 1785272077.903715 22.0 robust tracking True 1.24 2.85 1.8172104392 1.8118290291 11.3 42.6 37.7 6 5 -11.79 691868.479 691879.901 111.534 -73.806 True 0.05 0.721 0.036 0 0.721 0.036 0 23 56.0 142.52 -1.538 -1.56 622 3150 2063
14 25.510000609 1785272079.9095619 24.0 robust tracking True 1.24 2.85 1.8171689199 1.8113038519 11.08 43.1 38.1 6 5 -12.44 691719.345 691731.399 110.942 -74.863 True 0.05 0.359 0.101 0 0.359 0.101 0 23 48.0 145.5 -1.538 -1.56 624 3160 2063
15 27.50517151 1785272081.904733 26.0 robust tracking True 1.24 2.85 1.8171301373 1.8107796086 11.26 42.7 37.5 6 5 -18.16 691564.145 691581.742 111.5 -74.479 True 0.05 0.745 0.096 0 0.745 0.096 0 23 8.0 129.02 -1.54 -1.56 626 3170 2063
16 29.502641745 1785272083.9022028 28.0 robust tracking True 1.24 2.85 1.8170880804 1.8102547189 12.41 43.0 37.5 6 5 1.28 691433.115 691431.876 111.148 -74.903 True 0.05 0.332 0.115 0 0.332 0.115 0 23 0.0 127.84 -1.541 -1.56 628 3180 2063
17 31.500642933 1785272085.900204 30.0 robust tracking True 1.24 2.85 1.8170480561 1.8097326484 14.78 42.6 37.5 6 5 -1.39 691280.067 691281.411 111.681 -74.941 True 0.05 0.503 0.255 0 0.503 0.255 0 23 48.0 147.24 -1.538 -1.56 630 3190 2063
18 33.504307045 1785272087.903868 32.0 robust tracking True 1.24 2.85 1.8170082113 1.8092081529 22.56 42.9 38.4 6 5 -13.17 691116.408 691129.168 110.696 -75.765 True 0.05 0.206 0.027 0 0.206 0.027 0 23 16.0 132.81 -1.54 -1.56 632 3200 2063
19 35.509887989 1785272089.9094489 34.0 robust tracking True 1.24 2.85 1.8169688875 1.8086870549 11.18 42.9 38.0 6 5 -0.84 690974.741 690975.554 111.343 -75.376 True 0.05 0.636 0.055 0 0.636 0.055 0 23 56.0 156.39 -1.536 -1.56 634 3210 2063
20 37.506370892 1785272091.9059322 36.0 robust tracking True 1.24 2.85 1.8169289218 1.8081636846 10.95 43.4 39.5 6 5 -3.76 690819.152 690822.79 110.566 -76.732 True 0.05 0.72 0.058 0 0.72 0.058 0 23 32.0 156.77 -1.536 -1.56 636 3220 2063
21 39.504571912 1785272093.904133 38.0 robust tracking True 1.24 2.85 1.8168882102 1.8076411645 11.05 43.1 38.3 6 5 -8.88 690659.561 690668.166 111.385 -76.264 True 0.05 0.403 0.084 0 0.403 0.084 0 23 48.0 147.43 -1.538 -1.56 638 3230 2063
22 41.502657038 1785272095.9022183 40.0 robust tracking True 1.24 2.85 1.8168471874 1.8071184407 11.13 42.7 38.1 6 5 5.56 690520.399 690515.009 111.68 -75.754 True 0.05 0.957 0.102 0 0.957 0.102 0 23 32.0 147.4 -1.538 -1.56 640 3240 2063
23 43.50032369 1785272097.8998854 42.0 robust tracking True 1.24 2.85 1.816809289 1.8065980348 11.21 42.7 37.8 6 5 9.98 690370.621 690360.952 111.268 -76.081 True 0.05 0.807 0.041 0 0.807 0.041 0 23 0.0 136.35 -1.539 -1.56 642 3250 2063
24 45.712022504 1785272100.111584 44.0 robust tracking True 1.24 2.85 1.8167660234 1.8060226878 12.45 43.2 38.0 6 5 -7.85 690184.849 690192.456 111.097 -76.306 True 0.05 0.794 0.037 0 0.794 0.037 0 23 32.0 142.31 -1.538 -1.56 645 3260 2063
25 47.704670721 1785272102.1042318 46.0 robust tracking True 1.24 2.85 1.8167259917 1.8055025786 11.59 42.9 38.7 6 5 -14.42 690023.356 690037.324 110.386 -77.903 True 0.05 0.84 0.099 0 0.84 0.099 0 23 56.0 136.57 -1.539 -1.56 647 3270 2063
26 49.704359089 1785272104.1039205 48.0 robust tracking True 1.24 2.84 1.8166840843 1.804981981 11.04 43.4 38.2 6 5 -2.28 689880.716 689882.925 110.905 -78.019 True 0.05 0.509 0.107 0 0.509 0.107 0 23 16.0 148.6 -1.537 -1.56 649 3280 2063
27 51.709139916 1785272106.1087012 50.0 robust tracking True 1.24 2.84 1.8166456009 1.8044613748 10.99 43.3 38.1 6 5 -0.68 689726.579 689727.233 111.064 -77.798 True 0.05 0.094 0.103 0 0.094 0.103 0 23 48.0 144.29 -1.538 -1.56 651 3290 2063
28 53.703365655 1785272108.1029265 52.0 robust tracking True 1.24 2.84 1.8166083592 1.8039440331 12.68 42.8 37.5 6 5 -8.72 689564.342 689572.793 110.533 -78.338 True 0.05 0.504 0.187 0 0.504 0.187 0 23 8.0 148.54 -1.537 -1.56 653 3300 2063
29 55.700643307 1785272110.100204 54.0 robust tracking True 1.24 2.84 1.8165672875 1.8034221236 11.07 42.9 38.7 6 5 4.12 689421.398 689417.403 111.098 -77.598 True 0.05 0.645 0.08 0 0.645 0.08 0 23 40.0 140.91 -1.539 -1.56 655 3310 2063
30 57.904055806 1785272112.3036168 56.0 robust tracking True 1.24 2.84 1.8165247981 1.8028524558 16.67 42.9 38.2 6 5 -1.04 689243.202 689244.211 110.99 -78.254 True 0.05 0.342 0.02 0 0.342 0.02 0 23 48.0 144.27 -1.538 -1.56 657 3320 2063
31 60.10451733 1785272114.5040786 58.0 robust tracking True 1.24 2.84 1.8164827704 1.8022817921 11.19 42.9 38.0 6 5 -5.64 689066.66 689072.123 111.676 -78.014 True 0.05 0.73 0.15 0 0.73 0.15 0 23 48.0 126.86 -1.541 -1.56 659 3330 2063
32 62.108759952 1785272116.5083213 60.0 robust tracking True 1.24 2.84 1.8164435096 1.8017644864 16.38 43.1 39.2 6 5 -1.09 688913.112 688914.171 111.17 -78.316 True 0.05 0.696 0.007 0 0.696 0.007 0 23 24.0 153.74 -1.537 -1.56 661 3340 2063
33 64.300493137 1785272118.7000537 62.0 robust tracking True 1.24 2.84 1.8163984438 1.8011921234 11.32 42.4 37.4 6 5 8.12 688749.473 688741.606 110.452 -79.51 True 0.05 0.546 0.16 0 0.546 0.16 0 23 40.0 152.97 -1.537 -1.56 663 3350 2063
34 66.510932531 1785272120.9104934 64.0 robust tracking True 1.23 2.84 1.8163566675 1.8006252849 20.15 42.7 38.3 6 5 -2.84 688563.016 688565.769 110.92 -78.813 True 0.05 0.795 0.197 0 0.795 0.197 0 23 40.0 172.22 -1.535 -1.561 665 3360 2063
35 68.704372014 1785272123.103933 66.0 robust tracking True 1.23 2.84 1.8163145194 1.8000555234 15.52 42.8 38.5 6 5 -11.85 688380.019 688391.495 110.061 -80.47 True 0.05 0.796 0.074 0 0.796 0.074 0 23 48.0 155.17 -1.537 -1.56 668 3370 2063
36 70.703104898 1785272125.102666 68.0 robust tracking True 1.23 2.84 1.8162769265 1.7995395272 11.43 42.0 37.8 6 5 13.98 688244.124 688230.58 110.609 -80.264 True 0.05 0.381 0.125 0 0.381 0.125 0 23 16.0 179.45 -1.533 -1.56 670 3380 2063
37 72.702223062 1785272127.101784 70.0 robust tracking True 1.23 2.84 1.8162378878 1.7990227767 16.58 42.8 38.9 6 5 5.88 688078.671 688072.971 110.939 -80.36 True 0.05 0.666 0.053 0 0.666 0.053 0 23 24.0 166.76 -1.535 -1.56 672 3390 2063
38 74.700286036 1785272129.0998468 72.0 robust tracking True 1.23 2.84 1.816199267 1.7985063001 11.1 42.7 38.7 6 5 14.57 687926.671 687912.557 110.527 -80.516 True 0.05 0.312 0.03 0 0.312 0.03 0 23 32.0 162.81 -1.536 -1.56 674 3400 2063
39 76.71281947 1785272131.1123805 74.0 robust tracking True 1.23 2.84 1.8161600375 1.7979893506 11.11 42.7 38.6 6 5 -1.67 687750.089 687751.702 111.576 -79.6 True 0.05 1.026 0.083 0 1.026 0.083 0 23 16.0 142.71 -1.539 -1.56 676 3410 2063
40 78.704243128 1785272133.103804 76.0 robust tracking True 1.23 2.84 1.8161215432 1.7974747243 11.08 43.2 38.1 6 5 -4.48 687586.716 687591.059 110.819 -81.044 True 0.05 0.676 0.104 0 0.676 0.104 0 23 0.0 107.47 -1.544 -1.56 678 3420 2063
41 80.70401895 1785272135.10358 78.0 robust tracking True 1.23 2.84 1.8160831759 1.7969606981 20.64 42.3 37.5 6 5 -0.58 687429.447 687430.01 110.975 -81.135 True 0.05 0.351 0.106 0 0.351 0.106 0 23 56.0 161.62 -1.536 -1.56 680 3430 2063
42 82.710133903 1785272137.1096952 80.0 robust tracking True 1.23 2.84 1.8160471938 1.7964462492 21.18 43.0 39.1 6 5 11.46 687279.439 687268.334 110.976 -80.409 True 0.05 1.15 0.031 0 1.15 0.031 0 23 0.0 177.64 -1.534 -1.56 682 3440 2063
43 84.702669179 1785272139.1022303 82.0 robust tracking True 1.23 2.84 1.8160084174 1.7959315494 16.16 42.7 39.0 6 5 2.07 687108.261 687106.252 110.31 -81.912 True 0.05 0.401 0.04 0 0.401 0.04 0 23 48.0 145.45 -1.539 -1.56 684 3450 2063
44 86.700732144 1785272141.100293 84.0 robust tracking True 1.23 2.83 1.8159713938 1.7954167669 11.13 42.7 39.0 6 5 -12.66 686929.679 686941.939 110.952 -81.183 True 0.05 0.726 0.196 0 0.726 0.196 0 23 24.0 139.84 -1.539 -1.56 686 3460 2063
45 88.704090128 1785272143.103651 86.0 robust tracking True 1.23 2.83 1.8159335443 1.79490275 16.21 42.7 38.7 6 5 8.63 686784.906 686776.542 110.694 -82.024 True 0.05 0.209 0.052 0 0.209 0.052 0 23 48.0 176.01 -1.534 -1.56 688 3470 2063
46 90.704048278 1785272145.103609 88.0 robust tracking True 1.23 2.83 1.8158959585 1.7943905432 24.91 42.5 37.9 6 5 -0.15 686613.039 686613.185 110.563 -82.135 True 0.05 0.379 0.123 0 0.379 0.123 0 23 40.0 161.5 -1.536 -1.56 690 3480 2063
47 92.711508414 1785272147.1110694 90.0 robust tracking True 1.23 2.83 1.8158570425 1.793877413 24.43 42.7 38.0 6 5 15.4 686465.242 686450.326 111.554 -81.637 True 0.05 1.011 0.184 0 1.011 0.184 0 23 56.0 178.15 -1.534 -1.56 692 3490 2063
48 94.704749369 1785272149.1043103 92.0 robust tracking True 1.23 2.83 1.8158190794 1.7933647389 21.48 42.4 38.5 6 5 16.84 686303.4 686287.088 110.534 -82.726 True 0.05 0.195 0.029 0 0.195 0.029 0 23 48.0 171.95 -1.535 -1.56 694 3500 2063
49 96.703104991 1785272151.1026661 94.0 robust tracking True 1.23 2.83 1.815781917 1.7928524573 11.31 42.4 37.6 6 5 -1.05 686119.874 686120.892 109.749 -83.836 True 0.05 0.919 0.158 0 0.919 0.158 0 23 16.0 137.68 -1.54 -1.56 696 3510 2063
50 98.915123597 1785272153.3146844 96.0 robust tracking True 1.23 2.83 1.8157417458 1.792292301 11.49 42.2 37.0 6 5 -4.81 685933.786 685938.441 110.18 -83.485 True 0.05 0.687 0.188 0 0.687 0.188 0 23 40.0 130.24 -1.541 -1.56 698 3520 2063
51 101.104184426 1785272155.5037456 98.0 robust tracking True 1.23 2.83 1.8157012539 1.7917269932 14.98 42.4 37.7 6 5 -4.93 685751.271 685756.046 110.623 -83.514 True 0.05 0.285 0.185 0 0.285 0.185 0 23 40.0 144.59 -1.539 -1.56 700 3530 2063
52 103.108802454 1785272157.5083635 100.0 robust tracking True 1.23 2.83 1.8156626947 1.7912163488 25.23 42.4 38.0 6 5 8.11 685596.218 685588.366 110.401 -83.817 True 0.05 0.362 0.053 0 0.362 0.053 0 23 56.0 172.28 -1.535 -1.56 702 3540 2063
53 105.105464086 1785272159.505025 102.0 robust tracking True 1.23 2.83 1.8156280022 1.79070636 16.51 42.5 37.5 6 5 -15.09 685403.733 685418.356 110.792 -83.601 True 0.05 0.365 0.1 0 0.365 0.1 0 23 48.0 134.38 -1.54 -1.56 704 3550 2063
54 107.300312593 1785272161.6998734 104.0 robust tracking True 1.23 2.83 1.8155869207 1.790143438 20.41 42.6 38.3 6 5 4.06 685236.104 685232.167 111.771 -83.119 True 0.05 1.06 0.172 0 1.06 0.172 0 23 56.0 186.11 -1.532 -1.56 706 3560 2063
55 109.312626871 1785272163.7121878 106.0 robust tracking True 1.23 2.83 1.8155513007 1.7896363278 15.58 42.6 37.5 6 5 -5.9 685055.343 685061.062 110.302 -84.628 True 0.05 0.319 0.208 0 0.319 0.208 0 23 24.0 168.36 -1.535 -1.56 708 3570 2063
56 111.306453913 1785272165.7060149 108.0 robust tracking True 1.23 2.83 1.8155128487 1.789126556 21.63 42.6 38.0 6 5 16.48 684906.901 684890.938 110.502 -84.943 True 0.05 0.385 0.093 0 0.385 0.093 0 23 16.0 177.8 -1.534 -1.56 710 3580 2063
57 113.305323246 1785272167.7048843 110.0 robust tracking True 1.22 2.83 1.8154776515 1.7886176858 11.1 42.9 38.4 6 5 1.51 684721.855 684720.397 110.396 -84.747 True 0.05 0.197 0.218 0 0.197 0.218 0 23 32.0 163.81 -1.536 -1.56 712 3590 2063
58 115.30411121 1785272169.7036724 112.0 robust tracking True 1.22 2.83 1.8154394251 1.7881082833 22.64 43.1 38.0 6 5 -3.4 684547.463 684550.752 110.44 -85.241 True 0.05 0.429 0.029 0 0.429 0.029 0 23 8.0 152.36 -1.538 -1.56 714 3600 2063
59 117.303101565 1785272171.702663 114.0 robust tracking True 1.22 2.83 1.8154034756 1.7875997643 11.52 42.6 38.1 6 5 17.65 684398.444 684381.346 110.707 -84.967 True 0.05 0.405 0.059 0 0.405 0.059 0 23 24.0 192.86 -1.532 -1.56 716 3610 2063
60 119.306150448 1785272173.7057114 116.0 robust tracking True 1.22 2.83 1.8153676614 1.7870927007 12.04 42.5 37.5 6 5 -7.99 684203.354 684211.094 110.164 -85.459 True 0.05 0.631 0.155 0 0.631 0.155 0 23 32.0 144.3 -1.539 -1.56 718 3620 2063
61 121.300962576 1785272175.7005236 118.0 robust tracking True 1.22 2.83 1.8153303586 1.786584068 18.38 42.4 37.2 6 5 15.28 684055.675 684040.872 110.141 -86.063 True 0.05 0.481 0.133 0 0.481 0.133 0 23 48.0 200.68 -1.53 -1.56 720 3630 2063
62 123.303755452 1785272177.7033165 120.0 robust tracking True 1.22 2.82 1.815294335 1.7860767164 19.1 42.8 38.8 6 5 2.02 683871.81 683869.856 111.056 -85.022 True 0.05 1.151 0.085 0 1.151 0.085 0 23 64.0 169.36 -1.535 -1.56 722 3640 2063
63 125.304219262 1785272179.703781 122.0 robust tracking True 1.22 2.82 1.8152578155 1.785570688 11.11 42.7 38.6 6 5 -8.82 683688.289 683696.831 110.802 -85.693 True 0.05 0.637 0.051 0 0.637 0.051 0 23 8.0 158.16 -1.537 -1.56 724 3650 2063
64 127.304583545 1785272181.704145 124.0 robust tracking True 1.22 2.82 1.8152215994 1.7850628719 11.13 42.7 38.8 6 5 -1.48 683523.082 683524.519 110.235 -86.917 True 0.05 0.775 0.068 0 0.775 0.068 0 23 40.0 151.87 -1.538 -1.56 726 3660 2063
65 129.508905566 1785272183.9084663 126.0 robust tracking True 1.22 2.82 1.8151838123 1.7845085023 12.03 43.1 38.6 6 5 9.01 683342.021 683333.296 110.201 -87.019 True 0.05 0.331 0.094 0 0.331 0.094 0 23 0.0 157.45 -1.537 -1.56 728 3670 2063
66 131.500762179 1785272185.9003232 128.0 robust tracking True 1.22 2.82 1.8151487619 1.7840006032 12.83 42.6 38.6 6 5 3.63 683160.888 683157.371 110.357 -86.937 True 0.05 0.389 0.075 0 0.389 0.075 0 23 32.0 171.39 -1.535 -1.56 730 3680 2063
67 133.504344505 1785272187.9039054 130.0 robust tracking True 1.22 2.82 1.8151127862 1.7834955606 12.64 42.7 38.5 6 5 3.69 682986.797 682983.217 110.053 -87.63 True 0.05 0.791 0.01 0 0.791 0.01 0 23 0.0 175.65 -1.534 -1.56 732 3690 2063
68 135.509832366 1785272189.9094036 132.0 robust tracking True 1.22 2.82 1.8150762158 1.7829890753 11.13 42.7 38.4 6 5 5.1 682813.848 682808.909 110.531 -87.346 True 0.05 0.079 0.034 0 0.079 0.034 0 23 16.0 162.81 -1.536 -1.56 734 3700 2063
69 137.506720578 1785272191.9062817 134.0 robust tracking True 1.22 2.82 1.8150397064 1.7824837171 10.99 42.8 39.5 6 5 1.42 682635.318 682633.946 110.584 -87.635 True 0.05 0.038 0.097 0 0.038 0.097 0 23 32.0 155.09 -1.537 -1.56 736 3710 2063
70 139.504410571 1785272193.9039717 136.0 robust tracking True 1.22 2.82 1.8150041072 1.7819780719 11.15 42.7 38.1 6 5 -6.72 682453.853 682460.364 110.886 -87.452 True 0.05 0.412 0.407 0 0.412 0.407 0 23 48.0 139.96 -1.539 -1.56 738 3720 2063
71 141.503078598 1785272195.9026399 138.0 robust tracking True 1.22 2.82 1.8149699154 1.781475266 11.11 42.7 38.8 6 5 11.45 682296.086 682284.991 109.638 -89.46 True 0.05 1.419 0.005 0 1.419 0.005 0 23 40.0 167.17 -1.535 -1.56 740 3730 2063
72 143.500493916 1785272197.900055 140.0 robust tracking True 1.22 2.82 1.8149354252 1.7809734693 11.02 42.9 38.7 6 5 12.89 682120.284 682107.799 110.911 -87.736 True 0.05 0.88 0.067 0 0.88 0.067 0 23 56.0 162.67 -1.536 -1.56 742 3740 2063
73 145.512927625 1785272199.9124885 142.0 robust tracking True 1.22 2.82 1.8149005029 1.7804693538 10.96 42.9 39.4 6 5 -25.89 681906.117 681931.202 110.881 -87.866 True 0.05 0.775 0.078 0 0.775 0.078 0 23 8.0 107.83 -1.544 -1.56 744 3750 2063
74 147.506278297 1785272201.9058392 144.0 robust tracking True 1.22 2.82 1.8148661739 1.7799667491 10.98 43.0 39.0 6 5 -5.66 681746.195 681751.678 109.788 -89.613 True 0.05 1.009 0.035 0 1.009 0.035 0 23 56.0 152.27 -1.538 -1.56 746 3760 2063
75 149.505513865 1785272203.9050748 146.0 robust tracking True 1.22 2.82 1.8148312963 1.779461219 11.25 42.4 38.4 6 5 -7.95 681563.852 681571.554 110.854 -89.368 True 0.05 0.482 0.099 0 0.482 0.099 0 23 56.0 142.08 -1.539 -1.56 748 3770 2063
76 151.504103133 1785272205.903664 148.0 robust tracking True 1.22 2.82 1.8147976667 1.778959756 10.98 43.0 39.4 6 5 11.06 681403.38 681392.662 110.425 -89.358 True 0.05 0.199 0.058 0 0.199 0.058 0 23 16.0 161.29 -1.536 -1.56 750 3780 2063
77 153.502847324 1785272207.9024084 150.0 robust tracking True 1.22 2.82 1.8147608671 1.7784571849 15.23 42.6 38.9 6 5 9.66 681223.637 681214.276 110.279 -89.721 True 0.05 0.371 0.218 0 0.371 0.218 0 23 48.0 172.12 -1.535 -1.56 752 3790 2063
78 155.506297511 1785272209.9058583 152.0 robust tracking True 1.22 2.82 1.8147266277 1.7779565566 11.07 42.6 38.7 6 5 0.35 681034.417 681034.073 110.263 -89.772 True 0.05 0.371 0.13 0 0.371 0.13 0 23 56.0 180.04 -1.533 -1.56 754 3800 2063
79 157.500753602 1785272211.9003143 154.0 robust tracking True 1.22 2.82 1.814692541 1.777454151 11.13 42.4 38.4 6 5 8.94 680862.299 680853.639 110.737 -89.433 True 0.05 0.755 0.053 0 0.755 0.053 0 23 0.0 180.5 -1.533 -1.56 756 3810 2063
80 159.504450759 1785272213.904012 156.0 robust tracking True 1.22 2.82 1.8146585955 1.7769539906 11.22 42.3 38.2 6 5 14.47 680691.31 680677.294 110.442 -90.141 True 0.05 0.405 0.02 0 0.405 0.02 0 23 32.0 196.03 -1.531 -1.56 758 3820 2063
81 161.510423884 1785272215.9099848 158.0 robust tracking True 1.21 2.82 1.8146253251 1.7764523761 14.07 42.3 38.6 6 5 14.17 680511.698 680497.972 110.887 -89.977 True 0.05 0.551 0.182 0 0.551 0.182 0 23 32.0 184.51 -1.532 -1.56 760 3830 2063
82 163.504960681 1785272217.9045217 160.0 robust tracking True 1.21 2.81 1.8145907816 1.7759535814 10.99 42.6 39.7 6 5 9.57 680326.822 680317.554 110.339 -91.079 True 0.05 0.465 0.01 0 0.465 0.01 0 23 8.0 150.36 -1.538 -1.56 762 3840 2063
83 165.507220868 1785272219.9067822 162.0 robust tracking True 1.21 2.81 1.8145551226 1.7754518476 10.98 42.8 38.9 6 5 5.13 680143.185 680138.216 109.975 -91.641 True 0.05 0.834 0.034 0 0.834 0.034 0 23 0.0 144.58 -1.539 -1.56 764 3850 2063
84 167.503525545 1785272221.9030867 164.0 robust tracking True 1.21 2.81 1.8145228906 1.7749543943 14.36 42.8 38.7 6 5 4.72 679959.278 679954.706 110.179 -91.65 True 0.05 0.706 0.085 0 0.706 0.085 0 23 48.0 124.01 -1.542 -1.56 766 3860 2063
85 169.50061802 1785272223.900179 166.0 robust tracking True 1.21 2.81 1.8144898435 1.7744531221 11.09 42.8 37.7 6 5 13.57 679787.48 679774.331 110.775 -91.606 True 0.05 1.106 0.075 0 1.106 0.075 0 23 56.0 145.18 -1.538 -1.56 768 3870 2063
86 171.510388267 1785272225.9099493 168.0 robust tracking True 1.21 2.81 1.8144559568 1.7739550407 11.08 42.6 38.2 6 5 8.28 679599.18 679591.156 110.344 -91.505 True 0.05 0.285 0.062 0 0.285 0.062 0 23 56.0 137.18 -1.54 -1.56 770 3880 2063
87 173.500668495 1785272227.9002295 170.0 robust tracking True 1.21 2.81 1.8144213808 1.7734561125 11.07 42.5 38.0 6 5 -7.05 679401.0 679407.83 110.355 -91.896 True 0.05 0.239 0.023 0 0.239 0.023 0 23 48.0 151.22 -1.538 -1.56 772 3890 2063
88 175.509072834 1785272229.9086337 172.0 robust tracking True 1.21 2.81 1.8143875329 1.7729571416 14.88 42.6 38.0 6 5 -9.75 679214.759 679224.203 110.051 -92.203 True 0.05 0.388 0.024 0 0.388 0.024 0 23 16.0 127.04 -1.541 -1.56 774 3900 2063
89 177.511375431 1785272231.9109368 174.0 robust tracking True 1.21 2.81 1.8143541178 1.7724598606 11.01 42.7 39.1 6 5 -28.57 679010.25 679037.93 110.131 -92.548 True 0.05 0.679 0.066 0 0.679 0.066 0 23 16.0 144.09 -1.539 -1.56 776 3910 2063
90 179.5031023 1785272233.9026635 176.0 robust tracking True 1.21 2.81 1.8143216536 1.7719632746 11.04 42.6 38.1 6 5 -1.94 678850.304 678852.181 110.443 -92.232 True 0.05 0.439 0.164 0 0.439 0.164 0 23 24.0 156.61 -1.537 -1.56 778 3920 2063
91 181.500919559 1785272235.9004805 178.0 robust tracking True 1.21 2.81 1.8142885244 1.7714647145 11.67 42.7 39.6 6 5 14.37 678680.998 678667.079 110.223 -93.101 True 0.05 0.927 0.048 0 0.927 0.048 0 23 24.0 174.87 -1.534 -1.56 780 3930 2063
92 183.504154069 1785272237.903716 180.0 robust tracking True 1.21 2.81 1.8142515963 1.7709676955 11.12 42.4 38.5 6 5 3.52 678484.103 678480.688 110.013 -93.55 True 0.05 0.69 0.056 0 0.69 0.056 0 23 0.0 167.43 -1.535 -1.56 782 3940 2063
93 185.509701079 1785272239.9092622 182.0 robust tracking True 1.21 2.81 1.8142225906 1.7704719824 11.12 42.5 38.1 6 5 2.8 678298.275 678295.56 110.36 -92.989 True 0.05 0.188 0.156 0 0.188 0.156 0 23 0.0 175.14 -1.534 -1.56 784 3950 2063
94 187.511311613 1785272241.9108725 184.0 robust tracking True 1.21 2.81 1.8141898144 1.7699731912 11.11 42.3 38.5 6 5 -16.24 678094.632 678110.363 110.845 -93.036 True 0.05 0.46 0.009 0 0.46 0.009 0 23 32.0 150.91 -1.537 -1.56 786 3960 2063
95 189.504094586 1785272243.9036555 186.0 robust tracking True 1.21 2.81 1.8141560815 1.7694787336 11.13 42.5 37.9 6 5 -1.22 677924.284 677925.468 110.012 -93.838 True 0.05 0.542 0.031 0 0.542 0.031 0 23 32.0 167.75 -1.535 -1.56 788 3970 2063
96 191.503264307 1785272245.902825 188.0 robust tracking True 1.21 2.81 1.8141223587 1.7689845528 11.12 42.4 38.3 6 5 12.44 677751.751 677739.703 110.673 -93.502 True 0.05 0.691 0.123 0 0.691 0.123 0 23 32.0 181.14 -1.533 -1.56 790 3980 2063
97 193.500850983 1785272247.9004118 190.0 robust tracking True 1.21 2.81 1.8140919959 1.768488668 14.03 42.3 38.2 6 5 8.52 677559.555 677551.298 110.495 -93.954 True 0.05 0.376 0.004 0 0.376 0.004 0 23 56.0 155.05 -1.537 -1.56 792 3990 2063
98 195.507436225 1785272249.9069972 192.0 robust tracking True 1.21 2.81 1.8140572992 1.7679932608 10.97 42.9 38.9 6 5 -25.61 677336.289 677361.101 110.053 -94.87 True 0.05 0.565 0.132 0 0.565 0.132 0 23 0.0 143.1 -1.539 -1.56 794 4000 2063
99 197.710664279 1785272252.1102252 194.0 robust tracking True 1.21 2.81 1.8140224199 1.7674512561 11.06 42.7 38.3 6 5 -21.21 677131.244 677151.796 110.436 -94.668 True 0.05 0.467 0.099 0 0.467 0.099 0 23 16.0 127.66 -1.541 -1.56 797 4010 2063
100 199.904494084 1785272254.3040552 196.0 robust tracking True 1.21 2.81 1.8139878446 1.7669062479 11.21 42.4 37.9 6 5 2.5 676945.184 676942.759 110.719 -94.592 True 0.05 0.329 0.254 0 0.329 0.254 0 23 0.0 166.66 -1.535 -1.56 799 4020 2063
101 201.902748265 1785272256.3023093 198.0 robust tracking True 1.21 2.8 1.8139551469 1.7664122598 19.71 42.7 38.2 6 5 3.44 676756.483 676753.152 110.284 -95.508 True 0.05 0.834 0.075 0 0.834 0.075 0 23 48.0 159.1 -1.536 -1.56 801 4030 2063
102 203.900860006 1785272258.3004212 200.0 robust tracking True 1.21 2.8 1.8139243484 1.7659203155 21.27 42.6 37.9 6 5 16.7 676577.254 676561.076 111.005 -94.638 True 0.05 0.65 0.026 0 0.65 0.026 0 23 40.0 185.48 -1.532 -1.56 803 4040 2063
103 205.904311168 1785272260.303872 202.0 robust tracking True 1.21 2.8 1.8138926701 1.7654272361 11.23 42.3 37.5 6 5 -4.45 676364.571 676368.878 111.007 -94.853 True 0.05 0.681 0.154 0 0.681 0.154 0 23 32.0 146.48 -1.538 -1.56 805 4050 2063
104 207.911054786 1785272262.3106158 204.0 robust tracking True 1.21 2.8 1.8138607249 1.7649342967 11.29 42.2 37.1 6 5 -0.42 676178.038 676178.445 111.268 -95.121 True 0.05 0.669 0.187 0 0.669 0.187 0 23 56.0 148.37 -1.538 -1.56 807 4060 2063
105 209.904852911 1785272264.304414 206.0 robust tracking True 1.21 2.8 1.8138277745 1.7644418155 17.32 42.6 38.4 6 5 5.29 675993.222 675988.099 110.799 -95.578 True 0.05 0.772 0.012 0 0.772 0.012 0 23 48.0 167.53 -1.535 -1.56 809 4070 2063
106 211.904389957 1785272266.3039508 208.0 robust tracking True 1.2 2.8 1.8137949136 1.7639490637 14.88 42.5 38.1 6 5 4.76 675802.554 675797.942 109.97 -97.009 True 0.05 1.186 0.157 0 1.186 0.157 0 23 16.0 161.37 -1.536 -1.56 811 4080 2063
107 213.908887407 1785272268.3084483 210.0 robust tracking True 1.2 2.8 1.8137652527 1.7634585771 13.65 41.9 38.5 6 5 -19.97 675583.7 675603.041 110.734 -96.484 True 0.05 0.11 0.069 0 0.11 0.069 0 23 8.0 134.36 -1.54 -1.56 813 4090 2063
108 215.900878981 1785272270.3004403 212.0 robust tracking True 1.2 2.8 1.8137346862 1.7629700707 13.14 42.4 39.0 6 5 1.39 675411.049 675409.707 110.477 -96.41 True 0.05 0.489 0.149 0 0.489 0.149 0 23 48.0 164.12 -1.535 -1.56 815 4100 2063
109 217.908715503 1785272272.3082767 214.0 robust tracking True 1.2 2.8 1.8137007679 1.7624770049 11.19 42.1 38.1 6 5 -3.51 675214.305 675217.703 110.227 -97.513 True 0.05 0.667 0.022 0 0.667 0.022 0 23 56.0 141.12 -1.539 -1.56 817 4110 2063
110 219.904589627 1785272274.3041506 216.0 robust tracking True 1.2 2.8 1.8136705307 1.7619846326 17.3 42.2 38.8 6 5 -14.02 675007.687 675021.273 110.312 -97.218 True 0.05 0.452 0.069 0 0.452 0.069 0 23 24.0 158.28 -1.536 -1.56 819 4120 2063
111 221.9039367 1785272276.3034978 218.0 robust tracking True 1.2 2.8 1.8136409999 1.7614933749 11.12 42.1 39.2 6 5 -3.83 674821.953 674825.66 111.06 -96.371 True 0.05 1.326 0.028 0 1.326 0.028 0 23 48.0 170.5 -1.534 -1.56 821 4130 2063
112 223.91008895 1785272278.3096502 220.0 robust tracking True 1.2 2.8 1.8136105372 1.7610043295 11.73 42.3 38.7 6 5 0.13 674630.412 674630.288 111.079 -97.301 True 0.05 0.408 0.148 0 0.408 0.148 0 23 56.0 157.28 -1.536 -1.56 823 4140 2063
113 225.902916967 1785272280.302478 222.0 robust tracking True 1.2 2.8 1.8135794516 1.7605142092 21.26 41.9 39.1 6 5 -21.67 674413.179 674434.176 111.065 -97.127 True 0.05 0.752 0.068 0 0.752 0.068 0 23 48.0 124.73 -1.541 -1.56 825 4150 2063
114 227.901755726 1785272282.3013165 224.0 robust tracking True 1.2 2.8 1.8135490001 1.7600248185 11.02 42.5 39.3 6 5 13.8 674252.975 674239.611 110.715 -97.719 True 0.05 0.748 0.078 0 0.748 0.078 0 23 32.0 159.45 -1.536 -1.56 827 4160 2063
115 229.901619429 1785272284.3011806 226.0 robust tracking True 1.2 2.8 1.8135177823 1.7595391681 11.05 42.4 39.1 6 5 12.06 674053.956 674042.275 110.355 -98.939 True 0.05 0.78 0.043 0 0.78 0.043 0 23 56.0 170.95 -1.534 -1.56 829 4170 2063
116 231.90481137 1785272286.3043725 228.0 robust tracking True 1.2 2.8 1.8134877896 1.7590492494 11.09 42.4 38.1 6 5 12.72 673856.473 673844.146 110.721 -98.388 True 0.05 0.538 0.188 0 0.538 0.188 0 23 48.0 170.54 -1.534 -1.56 831 4180 2063
117 233.905326912 1785272288.3048878 230.0 robust tracking True 1.2 2.8 1.8134570418 1.7585604429 11.06 42.5 38.9 6 5 5.44 673653.112 673647.84 110.098 -98.963 True 0.05 0.301 0.311 0 0.301 0.311 0 23 8.0 171.59 -1.534 -1.56 833 4190 2063
118 235.904458614 1785272290.3040195 232.0 robust tracking True 1.2 2.8 1.8134261204 1.7580735849 11.06 42.5 38.5 6 5 -4.62 673445.637 673450.117 110.459 -98.844 True 0.05 0.829 0.028 0 0.829 0.028 0 23 0.0 153.73 -1.537 -1.56 835 4200 2063
119 237.904071483 1785272292.3036325 234.0 robust tracking True 1.2 2.8 1.8133959704 1.7575868747 12.8 42.0 37.7 6 5 -7.87 673243.852 673251.478 110.772 -98.845 True 0.05 0.559 0.037 0 0.559 0.037 0 23 0.0 146.96 -1.538 -1.56 837 4210 2063
120 239.908760739 1785272294.3083217 236.0 robust tracking True 1.2 2.8 1.8133666091 1.7570976374 11.1 42.3 38.6 6 5 4.99 673058.134 673053.301 110.565 -99.434 True 0.05 0.506 0.016 0 0.506 0.016 0 23 24.0 168.65 -1.535 -1.56 839 4220 2063
121 241.900563008 1785272296.300124 238.0 robust tracking True 1.2 2.79 1.8133344617 1.7566107514 11.14 42.3 38.4 6 5 -14.57 672839.788 672853.907 110.365 -99.522 True 0.05 0.253 0.141 0 0.253 0.141 0 23 0.0 147.65 -1.538 -1.56 841 4230 2063
122 243.904645899 1785272298.3042068 240.0 robust tracking True 1.2 2.79 1.8133057846 1.7561227378 11.1 42.2 38.8 6 5 3.18 672658.12 672655.039 109.903 -100.274 True 0.05 0.476 0.033 0 0.476 0.033 0 23 32.0 164.64 -1.535 -1.56 843 4240 2063
123 245.904621193 1785272300.3041825 242.0 robust tracking True 1.2 2.79 1.8132777969 1.7556370904 11.14 42.2 38.7 6 5 -0.81 672454.836 672455.618 110.573 -100.187 True 0.05 0.405 0.013 0 0.405 0.013 0 23 16.0 134.41 -1.54 -1.56 845 4250 2063
124 247.899436713 1785272302.2989979 244.0 robust tracking True 1.2 2.79 1.8132463791 1.7551512789 11.16 42.2 39.0 6 5 13.48 672269.138 672256.082 110.183 -100.179 True 0.05 0.565 0.113 0 0.565 0.113 0 23 40.0 169.04 -1.535 -1.56 847 4260 2063
125 249.911369626 1785272304.3109307 246.0 robust tracking True 1.2 2.79 1.813217727 1.754665179 11.11 42.5 39.2 6 5 -6.09 672049.638 672055.536 109.774 -100.683 True 0.05 0.486 0.021 0 0.486 0.021 0 23 0.0 135.93 -1.539 -1.56 849 4270 2063
126 251.903384606 1785272306.3029459 248.0 robust tracking True 1.2 2.79 1.8131881994 1.7541830169 11.18 42.2 39.0 6 5 10.96 671865.57 671854.952 109.778 -101.09 True 0.05 0.278 0.027 0 0.278 0.027 0 23 48.0 182.36 -1.532 -1.56 851 4280 2063
127 253.900781619 1785272308.3003423 250.0 robust tracking True 1.2 2.79 1.8131589097 1.7536951988 11.17 42.3 38.9 6 5 3.55 671659.189 671655.749 110.155 -100.561 True 0.05 0.584 0.008 0 0.584 0.008 0 23 16.0 141.85 -1.539 -1.56 853 4290 2063
128 255.910281973 1785272310.3098433 252.0 robust tracking True 1.2 2.79 1.8131293194 1.753212297 11.2 42.1 38.4 6 5 7.57 671462.581 671455.25 109.95 -101.447 True 0.05 0.189 0.096 0 0.189 0.096 0 23 16.0 141.76 -1.539 -1.56 855 4300 2063
129 257.904754461 1785272312.3043153 254.0 robust tracking True 1.2 2.79 1.8131029159 1.7527265372 11.15 42.4 38.7 6 5 -6.95 671245.456 671252.194 110.05 -101.621 True 0.05 0.518 0.084 0 0.518 0.084 0 23 56.0 134.53 -1.54 -1.56 857 4310 2063
130 259.904898749 1785272314.3044596 256.0 robust tracking True 1.2 2.79 1.8130720433 1.7522445406 11.3 41.8 38.4 6 5 -4.74 671044.584 671049.173 110.162 -101.624 True 0.05 0.281 0.225 0 0.281 0.225 0 23 0.0 142.99 -1.539 -1.56 859 4320 2063
131 261.904207465 1785272316.3037684 258.0 robust tracking True 1.19 2.79 1.8130440645 1.7517582107 11.31 42.0 38.4 6 5 -7.29 670837.352 670844.416 110.701 -101.27 True 0.05 0.844 0.152 0 0.844 0.152 0 23 56.0 167.54 -1.535 -1.56 861 4330 2063
132 263.903658409 1785272318.3032198 260.0 robust tracking True 1.19 2.79 1.8130134772 1.7512773631 11.34 42.0 38.5 6 5 -1.16 670637.855 670638.976 110.001 -102.901 True 0.05 0.697 0.02 0 0.697 0.02 0 23 48.0 166.85 -1.535 -1.56 863 4340 2063
133 265.906678701 1785272320.3062396 262.0 robust tracking True 1.19 2.79 1.8129886444 1.7507938348 14.74 42.5 38.9 6 5 10.12 670442.306 670432.505 110.568 -101.927 True 0.05 0.908 0.068 0 0.908 0.068 0 23 40.0 192.58 -1.531 -1.56 865 4350 2063
134 267.908740259 1785272322.308301 264.0 robust tracking True 1.19 2.79 1.8129614176 1.750311896 11.27 42.3 37.7 6 5 -11.12 670213.856 670224.632 110.96 -102.072 True 0.05 0.648 0.323 0 0.648 0.323 0 23 56.0 163.77 -1.535 -1.56 867 4360 2063
135 269.904666955 1785272324.304228 266.0 robust tracking True 1.19 2.79 1.8129315339 1.7498285268 11.35 41.9 37.9 6 5 0.23 670016.459 670016.233 110.584 -102.496 True 0.05 0.457 0.073 0 0.457 0.073 0 23 48.0 200.12 -1.53 -1.56 869 4370 2063
136 271.904417064 1785272326.303978 268.0 robust tracking True 1.19 2.79 1.8129033974 1.7493462127 11.29 42.0 38.2 6 5 -11.63 669798.53 669809.801 110.923 -102.202 True 0.05 0.955 0.178 0 0.955 0.178 0 23 16.0 154.9 -1.537 -1.56 871 4380 2063
137 273.904287144 1785272328.3038483 270.0 robust tracking True 1.19 2.79 1.8128759146 1.7488645046 11.24 42.2 38.4 6 5 0.32 669604.755 669604.44 110.23 -103.356 True 0.05 0.559 0.043 0 0.559 0.043 0 23 56.0 147.74 -1.538 -1.56 873 4390 2063
138 275.905752343 1785272330.3053133 272.0 robust tracking True 1.19 2.79 1.8128467845 1.7483871309 11.29 42.1 38.0 6 5 0.52 669397.133 669396.627 110.767 -103.125 True 0.05 0.839 0.033 0 0.839 0.033 0 23 48.0 175.42 -1.533 -1.56 875 4400 2063
139 277.901721957 1785272332.3012831 274.0 robust tracking True 1.19 2.79 1.8128179501 1.7479069779 11.23 42.2 38.8 6 5 -15.46 669172.771 669187.748 110.557 -103.518 True 0.05 0.505 0.152 0 0.505 0.152 0 23 8.0 141.55 -1.539 -1.56 877 4410 2063
140 279.901270286 1785272334.3008313 276.0 robust tracking True 1.19 2.79 1.8127918148 1.7474239577 11.26 42.2 38.9 6 5 5.21 668986.416 668981.367 110.705 -103.667 True 0.05 0.415 0.13 0 0.415 0.13 0 23 56.0 164.7 -1.535 -1.56 879 4420 2063
141 281.911805396 1785272336.3113663 278.0 robust tracking True 1.19 2.78 1.812764459 1.7469430861 11.14 42.3 38.6 6 5 9.2 668783.357 668774.443 110.765 -103.653 True 0.05 0.976 0.047 0 0.976 0.047 0 23 40.0 167.02 -1.535 -1.56 881 4430 2063
142 283.904627638 1785272338.3041887 280.0 robust tracking True 1.19 2.78 1.8127364977 1.7464643722 11.13 42.4 38.9 6 5 1.9 668567.589 668565.747 110.702 -104.554 True 0.05 0.496 0.084 0 0.496 0.084 0 23 56.0 150.73 -1.537 -1.56 883 4440 2063
143 285.904540282 1785272340.3041015 282.0 robust tracking True 1.19 2.78 1.8127111868 1.7459831701 11.26 42.2 38.2 6 5 -21.52 668336.38 668357.228 110.484 -104.5 True 0.05 0.4 0.023 0 0.4 0.023 0 23 16.0 127.92 -1.541 -1.56 885 4450 2063
144 287.903906607 1785272342.3034678 284.0 robust tracking True 1.19 2.78 1.8126838601 1.7455066962 11.09 42.3 39.2 6 5 -0.35 668145.403 668145.742 110.548 -104.863 True 0.05 0.13 0.213 0 0.13 0.213 0 23 0.0 156.5 -1.536 -1.56 887 4460 2063
145 289.903519609 1785272344.3030806 286.0 robust tracking True 1.19 2.78 1.8126545436 1.74502695 11.28 42.2 38.0 6 5 -17.14 667916.867 667933.474 110.923 -105.07 True 0.05 0.412 0.112 0 0.412 0.112 0 23 8.0 145.17 -1.538 -1.56 889 4470 2063
146 291.906994829 1785272346.3065557 288.0 robust tracking True 1.19 2.78 1.812627295 1.7445482935 12.84 42.5 38.6 6 5 -1.31 667721.551 667722.816 109.629 -106.134 True 0.05 0.721 0.164 0 0.721 0.164 0 23 40.0 158.89 -1.536 -1.56 891 4480 2063
147 293.904251457 1785272348.3038123 290.0 robust tracking True 1.19 2.78 1.8126009474 1.7440682755 11.16 42.3 38.2 6 5 14.39 667524.719 667510.777 110.381 -105.665 True 0.05 0.161 0.079 0 0.161 0.079 0 23 32.0 184.17 -1.532 -1.56 893 4490 2063
148 295.904732818 1785272350.3042939 292.0 robust tracking True 1.19 2.78 1.8125748376 1.7435915248 11.06 42.6 38.1 6 5 -9.72 667289.655 667299.068 110.248 -105.873 True 0.05 0.291 0.005 0 0.291 0.005 0 23 32.0 159.99 -1.536 -1.56 895 4500 2063
149 297.910797167 1785272352.3103583 294.0 robust tracking True 1.19 2.78 1.8125472744 1.7431147283 11.0 42.9 38.4 6 5 9.37 667097.136 667088.06 110.084 -106.181 True 0.05 0.237 0.074 0 0.237 0.074 0 23 16.0 168.11 -1.535 -1.56 897 4510 2063
150 299.905108443 1785272354.3046699 296.0 robust tracking True 1.19 2.78 1.812521259 1.7426375476 16.12 42.5 37.8 6 5 -12.01 666863.837 666875.472 110.096 -106.613 True 0.05 0.693 0.156 0 0.693 0.156 0 23 64.0 124.56 -1.541 -1.56 899 4520 2063
@@ -0,0 +1,52 @@
# Genesys2 live PPS telemetry replay
Source: `pps_end_to_end_validation/hardware_logs/genesys2_pps_20260728.csv`
## Direct firmware/RTL observations
- Samples/duration: 149 / 296.0 s.
- Status interval: 2.000 s; median sequence step 10 (2.0 s of 200 ms solves).
- Profiles: robust; locked fraction 1.000; transitions 0; reference SVIDs [23].
- TDOP/GDOP medians: 1.220 / 2.820; clock sigma median 11.24 m.
- C/N0 mean/min medians: 42.6 / 38.4 dB-Hz; tracked/used medians 6/5.
- WLS innovation RMS/p95-absolute: 10.47 / 19.66 m.
- Raw/filtered bias slopes: -90.020 / -90.025 m/s; PPS velocity residual median 0.079 m/s.
- Internal hardware phase mean: 155.37 ns; median-centered RMS/p95/max/p-p: 17.92 / 31.26 / 47.95 / 93.21 ns.
- Ten-second averaged phase wander p-p: 46.16 ns.
- Sampled phase-step RMS/p95/max: 22.84 / 44.13 / 56.38 ns. These are at the status interval, not consecutive physical 1 PPS edges.
- Rate command median/span: -1.5370 / 0.0140 ppm; integrator median -1.5600 ppm.
- Fixed-point PI rate reproduction RMS/p95/max: 0.430 / 0.852 / 0.921 ppb. This is the difference between logged `rate_cmd` and `integrator + Kp*hw_phase`; the remaining error is dominated by three-decimal UART rounding.
- Phase correlation with rate command / WLS innovation: 0.988 / 0.656.
- A constant oscillator inferred from zero net frequency is 1.536779 ppm. Integrating the logged command under that assumption gives centered phase RMS/p-p 23.65 / 92.48 ns.
`hw_phase` is the RTL PI residual captured over APB. It is not a trusted-generator time-interval measurement of the output pin, so these values cannot certify physical PPS jitter or absolute offset.
## Telemetry-conditioned end-to-end loop replay
The replay imports the first locked phase/rate/integrator and software filter snapshot instead of incorrectly resetting the board to an acquisition state. It uses raw/filtered clock bias, Doppler datum drift, TDOP/GDOP, clock sigma, C/N0, satellite count, FDE, reference SVID and the production fixed-point coefficients. The reference trajectory is reconstructed from filtered bias, the logged PI residual and the integrated rate command.
- Expanded updates: 1481; interpolation factor 9.94.
- Snapshot starts locked at 0.00 s; post-warmup locked fraction 1.000; unlocks 0.
- Constant-oscillator convention: 1.536779 ppm.
- Modeled physical-grid edge centered RMS/p95/p-p: 41.32 / 80.00 / 168.00 ns.
- Modeled one-second period RMS/p95/max: 5.51 / 8.00 / 16.00 ns.
- Modeled PI residual centered RMS/p95/p-p: 21.47 / 46.79 / 135.63 ns.
- Logged-epoch PI residual reproduction RMS/p95 error: 18.07 / 44.30 ns.
- Logged-epoch filtered-bias reproduction RMS/p95: 9.87 / 22.13 m.
## Tuning sensitivity on the same trace
| tuning | edge RMS (ns) | edge p-p (ns) | period max (ns) | residual reproduction RMS (ns) | unlocks |
|---|---:|---:|---:|---:|---:|
| current | 41.32 | 168.00 | 16.00 | 18.07 | 0 |
| robust_bias_1_64 | 57.06 | 216.00 | 16.00 | 24.53 | 0 |
| drift_gain_1_2 | 42.23 | 168.00 | 16.00 | 18.03 | 0 |
These counterfactuals reuse a reference trajectory reconstructed from the current filter, and nine of every ten solve epochs are interpolated. They are useful as a rejection test, not as evidence to deploy new gains. On this capture neither `robust_bias_1_64` nor `drift_gain_1_2` improves the current loop's absolute wander.
## Limits and next measurement
- Missing status epochs are linearly interpolated when sequence step is greater than one. This preserves low-frequency wander but understates unlogged 200 ms noise; it is not an exact firmware replay.
- The inferred oscillator and active phase assume one constant crystal frequency chosen to make net phase slope zero over this capture. Temperature telemetry was unavailable, so slow crystal motion is not separable from the reconstructed reference.
- Level-1 telemetry does not expose ABS_SAMPLE, REM_CODE_PHASE, full capture counter, chi-square ratio, pseudorange residual vector, board temperature, or a trusted PPS edge timestamp. The bit/subframe part of the end-to-end model remains synthetic for those fields.
- Set `GPS_PPS_STATUS_LOG_INTERVAL=1` for exact 200 ms input cadence. For a max-20 ns pin claim, capture DUT and trusted PPS with a common-clock TIC/scope after cable/output-delay calibration.
@@ -0,0 +1,25 @@
scenario,tuning,runs,lock_p95_s,absolute_rms_median_ns,absolute_max_p95_ns,absolute_peak_to_peak_median_ns,period_rms_median_ns,period_p95_median_ns,period_max_worst_ns,wander_10s_peak_to_peak_median_ns,profile_transitions_total,unlocks_total
real_geometry_board_noise,current,24,10.770000000000001,10.881176406988356,32.0,48.00000000000001,3.867084378182096,8.0,8.000000000000002,36.8,0,0
real_geometry_board_noise,robust_noise_guard,24,10.770000000000001,10.881176406988356,32.0,48.00000000000001,3.867084378182096,8.0,8.000000000000002,36.8,0,0
real_geometry_board_noise,quiet_precision,24,10.770000000000001,6.311779001549132,16.000000000000004,24.0,2.7197398634103065,7.999999999999999,8.000000000000005,20.0,0,0
real_geometry_board_noise,fast_precision,24,10.770000000000001,11.482151620012546,40.0,56.0,4.4110037618184545,8.0,16.0,40.0,0,0
real_geometry_board_noise,drift_gain_1_2,24,10.770000000000001,9.830551108590587,32.0,40.0,3.761748764716249,8.0,8.000000000000002,33.6,0,0
real_geometry_board_noise,quality_scheduled_drift,24,10.770000000000001,9.830551108590587,32.0,40.0,3.761748764716249,8.0,8.000000000000002,33.6,0,0
scope_like_multipath,current,24,10.770000000000001,22.943340809908015,62.79999999999998,88.0,4.727734084565672,8.000000000000002,16.0,80.80000000000001,0,0
scope_like_multipath,robust_noise_guard,24,10.770000000000001,22.943340809908015,62.79999999999998,88.0,4.727734084565672,8.000000000000002,16.0,80.80000000000001,0,0
scope_like_multipath,quiet_precision,24,10.770000000000001,17.147532219212493,40.0,63.99999999999999,3.7617487647162484,8.000000000000004,8.000000000000005,58.4,0,0
scope_like_multipath,fast_precision,24,10.770000000000001,22.40714058089752,56.00000000000001,92.0,5.040539175116031,8.000000000000002,16.0,78.0,0,0
scope_like_multipath,drift_gain_1_2,24,10.8,21.354092092763203,62.79999999999998,88.0,4.465388951979694,8.0,16.0,74.80000000000001,0,0
scope_like_multipath,quality_scheduled_drift,24,10.8,21.354092092763203,62.79999999999998,88.0,4.465388951979694,8.0,16.0,74.80000000000001,0,0
quality_transitions,current,24,10.770000000000001,18.351896116449183,96.0,80.0,4.4110037618184545,8.0,23.99999999999999,73.20000000000002,72,0
quality_transitions,robust_noise_guard,24,10.770000000000001,17.481868755279223,80.0,80.0,4.374396022586886,8.0,16.00000000000001,67.6,72,0
quality_transitions,quiet_precision,24,10.770000000000001,10.467771592924619,56.0,47.99999999999999,3.0800577533580347,7.999999999999999,16.0,42.0,72,0
quality_transitions,fast_precision,24,10.770000000000001,19.145424296464835,88.00000000000001,88.0,4.861884148700504,8.0,24.000000000000004,73.6,72,0
quality_transitions,drift_gain_1_2,24,10.770000000000001,16.291766736261422,83.39999999999999,76.0,4.089452052592408,8.0,16.000000000000004,63.599999999999994,72,0
quality_transitions,quality_scheduled_drift,24,10.770000000000001,16.766578539927025,83.39999999999999,80.0,4.224793965697071,8.0,16.00000000000001,67.6,72,0
thermal_0p05ppm,current,24,10.770000000000001,39.2873108676108,102.79999999999998,136.0,5.727479384554999,8.000000000000005,24.0,124.0,0,0
thermal_0p05ppm,robust_noise_guard,24,10.770000000000001,39.2873108676108,102.79999999999998,136.0,5.727479384554999,8.000000000000005,24.0,124.0,0,0
thermal_0p05ppm,quiet_precision,24,10.770000000000001,210.25812707207118,416.0,648.0,13.000191801484194,24.000000000000004,32.0,632.4,0,0
thermal_0p05ppm,fast_precision,24,10.770000000000001,21.850277755888733,63.39999999999999,96.0,5.82476895848467,8.000000000000002,16.000000000000004,77.60000000000001,0,0
thermal_0p05ppm,drift_gain_1_2,24,10.770000000000001,38.40828815100511,96.0,136.0,5.656836393570529,8.000000000000005,16.000000000000004,122.0,0,0
thermal_0p05ppm,quality_scheduled_drift,24,10.770000000000001,38.40828815100511,96.0,136.0,5.656836393570529,8.000000000000005,16.000000000000004,122.0,0,0
1 scenario tuning runs lock_p95_s absolute_rms_median_ns absolute_max_p95_ns absolute_peak_to_peak_median_ns period_rms_median_ns period_p95_median_ns period_max_worst_ns wander_10s_peak_to_peak_median_ns profile_transitions_total unlocks_total
2 real_geometry_board_noise current 24 10.770000000000001 10.881176406988356 32.0 48.00000000000001 3.867084378182096 8.0 8.000000000000002 36.8 0 0
3 real_geometry_board_noise robust_noise_guard 24 10.770000000000001 10.881176406988356 32.0 48.00000000000001 3.867084378182096 8.0 8.000000000000002 36.8 0 0
4 real_geometry_board_noise quiet_precision 24 10.770000000000001 6.311779001549132 16.000000000000004 24.0 2.7197398634103065 7.999999999999999 8.000000000000005 20.0 0 0
5 real_geometry_board_noise fast_precision 24 10.770000000000001 11.482151620012546 40.0 56.0 4.4110037618184545 8.0 16.0 40.0 0 0
6 real_geometry_board_noise drift_gain_1_2 24 10.770000000000001 9.830551108590587 32.0 40.0 3.761748764716249 8.0 8.000000000000002 33.6 0 0
7 real_geometry_board_noise quality_scheduled_drift 24 10.770000000000001 9.830551108590587 32.0 40.0 3.761748764716249 8.0 8.000000000000002 33.6 0 0
8 scope_like_multipath current 24 10.770000000000001 22.943340809908015 62.79999999999998 88.0 4.727734084565672 8.000000000000002 16.0 80.80000000000001 0 0
9 scope_like_multipath robust_noise_guard 24 10.770000000000001 22.943340809908015 62.79999999999998 88.0 4.727734084565672 8.000000000000002 16.0 80.80000000000001 0 0
10 scope_like_multipath quiet_precision 24 10.770000000000001 17.147532219212493 40.0 63.99999999999999 3.7617487647162484 8.000000000000004 8.000000000000005 58.4 0 0
11 scope_like_multipath fast_precision 24 10.770000000000001 22.40714058089752 56.00000000000001 92.0 5.040539175116031 8.000000000000002 16.0 78.0 0 0
12 scope_like_multipath drift_gain_1_2 24 10.8 21.354092092763203 62.79999999999998 88.0 4.465388951979694 8.0 16.0 74.80000000000001 0 0
13 scope_like_multipath quality_scheduled_drift 24 10.8 21.354092092763203 62.79999999999998 88.0 4.465388951979694 8.0 16.0 74.80000000000001 0 0
14 quality_transitions current 24 10.770000000000001 18.351896116449183 96.0 80.0 4.4110037618184545 8.0 23.99999999999999 73.20000000000002 72 0
15 quality_transitions robust_noise_guard 24 10.770000000000001 17.481868755279223 80.0 80.0 4.374396022586886 8.0 16.00000000000001 67.6 72 0
16 quality_transitions quiet_precision 24 10.770000000000001 10.467771592924619 56.0 47.99999999999999 3.0800577533580347 7.999999999999999 16.0 42.0 72 0
17 quality_transitions fast_precision 24 10.770000000000001 19.145424296464835 88.00000000000001 88.0 4.861884148700504 8.0 24.000000000000004 73.6 72 0
18 quality_transitions drift_gain_1_2 24 10.770000000000001 16.291766736261422 83.39999999999999 76.0 4.089452052592408 8.0 16.000000000000004 63.599999999999994 72 0
19 quality_transitions quality_scheduled_drift 24 10.770000000000001 16.766578539927025 83.39999999999999 80.0 4.224793965697071 8.0 16.00000000000001 67.6 72 0
20 thermal_0p05ppm current 24 10.770000000000001 39.2873108676108 102.79999999999998 136.0 5.727479384554999 8.000000000000005 24.0 124.0 0 0
21 thermal_0p05ppm robust_noise_guard 24 10.770000000000001 39.2873108676108 102.79999999999998 136.0 5.727479384554999 8.000000000000005 24.0 124.0 0 0
22 thermal_0p05ppm quiet_precision 24 10.770000000000001 210.25812707207118 416.0 648.0 13.000191801484194 24.000000000000004 32.0 632.4 0 0
23 thermal_0p05ppm fast_precision 24 10.770000000000001 21.850277755888733 63.39999999999999 96.0 5.82476895848467 8.000000000000002 16.000000000000004 77.60000000000001 0 0
24 thermal_0p05ppm drift_gain_1_2 24 10.770000000000001 38.40828815100511 96.0 136.0 5.656836393570529 8.000000000000005 16.000000000000004 122.0 0 0
25 thermal_0p05ppm quality_scheduled_drift 24 10.770000000000001 38.40828815100511 96.0 136.0 5.656836393570529 8.000000000000005 16.000000000000004 122.0 0 0
@@ -0,0 +1,70 @@
# PPS stability analysis: current RTL + 180 ms firmware model
This report is generated by `stability_analysis.py`. The absolute LNAV epoch path and the stochastic control loop are evaluated separately so a 20 ms semantic error cannot be hidden by loop noise.
## Absolute epoch result
- Current RTL delivered the validated subframe after 9 following bits.
- With 180 ms postponement, reference error was -0.000032 ms.
- DUT minus trusted PPS was 32.01 ns. That remainder is the model's configured four-PCLK capture pipeline, not a measured board delay.
- Production firmware and this analysis both define `POSTPONE_TOW` as 180 ms for the current RTL delivery semantics.
## Hardware data used by this synthetic sweep
- Real GPS capture: 989 GDOP samples and 990 tracked-satellite samples over about five minutes.
- GDOP min/median/p95/max: 1.86 / 1.93 / 3.15 / 3.15.
- Tracked satellites: 8..8 (median 8).
- Horizontal scatter RMS/p95/max: 8.58 / 15.15 / 23.77 m. No surveyed truth was recorded.
- The older fusion capture used by this sweep has no ABS_SAMPLE, REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/integrator, temperature, or trusted-edge TIC samples. Those columns in the companion CSV are explicitly synthetic. Newer PPS status captures are analyzed separately by `hardware_replay.py` and are not silently mixed into this historical baseline.
Synthetic timing uses the board-calibration values already encoded in the PPS tests: TDOP 1.50, GDOP 2.92, clock sigma 15.87 m (52.94 ns), C/N0 40.3/34.8 dB-Hz, drift noise 3.47 m/s, seven-satellite reference point, -291.75 m/s datum range rate, and 1.592 ppm local oscillator. Real GDOP and satellite count replace the geometry fields when available.
## Current-loop results
`period max` is the worst |edge[n]-edge[n-1]| across all seeds. `absolute p-p` and `10 s wander p-p` expose the slow left/right motion that period RMS hides.
| scenario | abs RMS median (ns) | abs p-p median (ns) | 10 s wander p-p (ns) | period RMS (ns) | period max (ns) | unlocks |
|---|---:|---:|---:|---:|---:|---:|
| real_geometry_board_noise | 10.88 | 48.00 | 36.80 | 3.87 | 8.00 | 0 |
| scope_like_multipath | 22.94 | 88.00 | 80.80 | 4.73 | 16.00 | 0 |
| quality_transitions | 18.35 | 80.00 | 73.20 | 4.41 | 24.00 | 0 |
| thermal_0p05ppm | 39.29 | 136.00 | 124.00 | 5.73 | 24.00 | 0 |
The loop does not numerically oscillate or lose lock in these runs. The scope-like symptom is low-frequency phase wander: it can be tens of nanoseconds while one-second period jitter remains below 20 ns. Therefore a `p95 period jitter < 20 ns` assertion is not sufficient for an absolute ±20 ns requirement.
## Linear stability
The fixed-point coefficients give the following discrete poles at the 200 ms navigation update rate:
| profile | natural frequency | damping | poles | max magnitude |
|---|---:|---:|---|---:|
| acquire | 0.120 Hz | 0.90 | 0.852913+0.033242j, 0.852913-0.033242j | 0.853561 |
| precision | 0.035 Hz | 0.90 | 0.959449+0.017031j, 0.959449-0.017031j | 0.959600 |
| robust | 0.012 Hz | 1.00 | 0.986668+0.000000j, 0.982945+0.000000j | 0.986668 |
All magnitudes are below one. Profile changes preserve the PI integrator, and the RTL snapshots Kp/Ki/max-slew/lock limits together with the next commit, so the current acquire/precision/robust switching is structurally sound. Stable individual modes do not prove arbitrary-switching stability, but the asymmetric 8-update upgrade / 3-update downgrade hysteresis and the stress runs show no coefficient chatter or unlock.
## Dynamic parameter sweep
| tuning | worst period max (ns) | median abs RMS over scenarios (ns) | transitions | unlocks |
|---|---:|---:|---:|---:|
| current | 24.00 | 20.65 | 72 | 0 |
| robust_noise_guard | 24.00 | 20.21 | 72 | 0 |
| quiet_precision | 32.00 | 13.81 | 72 | 0 |
| fast_precision | 24.00 | 20.50 | 72 | 0 |
| drift_gain_1_2 | 16.00 | 18.82 | 72 | 0 |
| quality_scheduled_drift | 16.00 | 19.06 | 72 | 0 |
The safest small candidate is `robust_noise_guard`: retain the current 0.035 Hz precision loop and 0.012 Hz robust loop, but reduce only the robust-profile WLS residual gain from 1/32 to 1/64. In the synthetic quality-transition case its worst period step changed from 24.00 to 16.00 ns. This is a candidate for hardware A/B testing, not enough evidence for a production constant change.
The best synthetic all-scenario period result is `quality_scheduled_drift`: use drift gain 1/2 only with a precision-quality Doppler solution, fall back to the current 1/4 gain in robust quality, and combine that with the 1/64 robust bias gain. Its worst period step was 16.00 ns with no unlocks. It is the most meaningful dynamic A/B candidate, but the repository has no real drift-covariance or TIC samples with which to validate the threshold.
Narrowing precision to 0.012 Hz improves stationary noise but lags even the explicit 0.05 ppm/120 s synthetic oscillator variation. Widening it improves oscillator tracking but passes more WLS/multipath motion. A geometry-only bandwidth scheduler cannot resolve that tradeoff.
## Recommended architecture update
1. Keep current acquire/precision/robust PI profiles until real TIC and oscillator-temperature data are available. Do not tune from scope appearance alone.
2. Log the existing firmware status line at every solve during validation: ABS_SAMPLE, REM_CODE_PHASE, capture sequence/counter, TDOP/HDOP/GDOP, C/N0, pseudorange residual RMS, raw/filtered bias, innovation, bias drift, hardware phase, rate, integrator, profile, lock, FPGA/oscillator temperature, DUT edge and trusted edge.
3. A/B test the simulated scheduled filter first: precision drift gain 1/2, robust drift gain 1/4, precision bias gain 1/32, and robust bias gain 1/64. Longer term, use estimated phase/drift covariance to interpolate those software gains within bounds. Keep RTL PI bandwidth changes hysteretic and preserve its integrator as today.
4. If temperature/frequency acceleration is the main source, add oscillator temperature feed-forward or a phase/frequency/frequency-drift estimator. Switching PI bandwidth alone either passes WLS wander or lags the oscillator.
5. Define acceptance separately: calibrated mean offset, absolute centered error/p-p wander, one-second period error, holdover, and re-lock. A physical max-20 ns claim requires a common-clock TIC run after cable/output-delay calibration; this synthetic test cannot certify it.

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