Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc5796e2fc | |||
| 50454f8c23 | |||
| 347e1c671d | |||
| a6bb080270 | |||
| 9b0828dbaa | |||
| a697284bff | |||
| 988f9686e7 | |||
| 73db2a8355 | |||
| 156cfc8b77 | |||
| 23650be635 | |||
| 49b8609bb4 | |||
| ecbb8d0b68 | |||
| 0f6b3482c5 | |||
| 971be036a0 | |||
| 2da5fb7153 | |||
| 5b8cd8560f | |||
| b2ca7db633 | |||
| 64860d1ced | |||
| 86dfbeafac | |||
| e57b19b4ee | |||
| fe0eb19dc5 | |||
| ea055a36d2 | |||
| 93766b007f | |||
| bdce4a3046 | |||
| 9ff895600b |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -40,6 +40,14 @@ module tracking_wrapper #(
|
||||
import gps_top_pkg::*;
|
||||
import trck_pkg::*;
|
||||
|
||||
localparam int unsigned MEAS_CARRIER_WL =
|
||||
ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
|
||||
localparam int unsigned MEAS_SUM_WL =
|
||||
TRCK_CORR_WL + $clog2(MS_PER_BIT);
|
||||
localparam int unsigned MEAS_WBP_WL =
|
||||
(2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
|
||||
localparam int unsigned MEAS_COUNT_WL = $clog2(MS_PER_BIT + 1);
|
||||
|
||||
// apb state machine
|
||||
apb_state_e apb_bus_ps, apb_bus_ns;
|
||||
// tracking lock state machine
|
||||
@@ -70,6 +78,22 @@ module tracking_wrapper #(
|
||||
logic nav_bit, nav_bit_syned; // navigation bits.
|
||||
logic [31 : 0] abs_sample, abs_sample_syned;
|
||||
logic [REM_CODE_PH_I_WL-1 : 0] rem_code_ph, rem_code_ph_syned;
|
||||
logic measurement_valid, measurement_valid_syned;
|
||||
logic measurement_bit, measurement_bit_syned;
|
||||
logic [31:0] measurement_abs_sample, measurement_abs_sample_syned;
|
||||
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph;
|
||||
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph_syned;
|
||||
logic signed [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7;
|
||||
logic [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7_syned;
|
||||
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_i;
|
||||
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_q;
|
||||
logic [MEAS_SUM_WL-1:0] measurement_cn0_sum_i_syned;
|
||||
logic [MEAS_SUM_WL-1:0] measurement_cn0_sum_q_syned;
|
||||
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp;
|
||||
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp_syned;
|
||||
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count;
|
||||
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count_syned;
|
||||
logic [15:0] measurement_sequence, measurement_sequence_syned;
|
||||
logic preamble_ok;
|
||||
|
||||
// ( internal flags )
|
||||
@@ -77,6 +101,7 @@ module tracking_wrapper #(
|
||||
logic trck_nav_bit_rcv_flag_p, trck_nav_bit_rcv_flag_n;
|
||||
logic trck_timeout_flag_p, trck_timeout_flag_n;
|
||||
logic trck_nav_solve_flag_p, trck_nav_solve_flag_n;
|
||||
logic trck_measurement_flag_p, trck_measurement_flag_n;
|
||||
|
||||
logic [31 : 0] timeout_counter_p, timeout_counter_n; // tracking auto timeout value
|
||||
|
||||
@@ -94,6 +119,24 @@ module tracking_wrapper #(
|
||||
logic [31 : 0] nav_solve_timer_reg_p, nav_solve_timer_reg_n;
|
||||
logic [30 : 0] nav_bit_read_reg_p[10], nav_bit_read_reg_n[10];
|
||||
|
||||
// Atomic APB-domain copy of the 20 ms measurement.
|
||||
logic measurement_valid_r;
|
||||
logic measurement_capture_pending;
|
||||
logic [1:0] measurement_settle_count;
|
||||
logic measurement_capture;
|
||||
logic measurement_interrupt_pulse;
|
||||
logic measurement_data_valid_reg;
|
||||
logic measurement_bit_reg;
|
||||
logic measurement_lock_reg;
|
||||
logic [31:0] measurement_abs_sample_reg;
|
||||
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_ph_reg;
|
||||
logic signed [MEAS_CARRIER_WL-1:0] measurement_carrier_freq_q7_reg;
|
||||
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_i_reg;
|
||||
logic signed [MEAS_SUM_WL-1:0] measurement_cn0_sum_q_reg;
|
||||
logic [MEAS_WBP_WL-1:0] measurement_cn0_wbp_reg;
|
||||
logic [MEAS_COUNT_WL-1:0] measurement_cn0_sample_count_reg;
|
||||
logic [15:0] measurement_sequence_reg;
|
||||
|
||||
|
||||
logic[2 : 0] start_trck_ctl_p, start_trck_ctl_n;
|
||||
logic[2 : 0] stop_trck_ctl_p, stop_trck_ctl_n;
|
||||
@@ -152,11 +195,55 @@ module tracking_wrapper #(
|
||||
.aresetn(PRESETN),
|
||||
.Din(rem_code_ph),
|
||||
.Dout(rem_code_ph_syned)
|
||||
);
|
||||
);
|
||||
|
||||
double_ff_syn#(1, 2) i_measurement_valid_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_valid), .Dout(measurement_valid_syned)
|
||||
);
|
||||
double_ff_syn#(1, 2) i_measurement_bit_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_bit), .Dout(measurement_bit_syned)
|
||||
);
|
||||
double_ff_syn#(32, 2) i_measurement_abs_sample_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_abs_sample), .Dout(measurement_abs_sample_syned)
|
||||
);
|
||||
double_ff_syn#(REM_CODE_PH_I_WL, 2) i_measurement_rem_code_ph_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_rem_code_ph), .Dout(measurement_rem_code_ph_syned)
|
||||
);
|
||||
double_ff_syn#(MEAS_CARRIER_WL, 2) i_measurement_carrier_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_carrier_freq_q7),
|
||||
.Dout(measurement_carrier_freq_q7_syned)
|
||||
);
|
||||
double_ff_syn#(MEAS_SUM_WL, 2) i_measurement_sum_i_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_cn0_sum_i), .Dout(measurement_cn0_sum_i_syned)
|
||||
);
|
||||
double_ff_syn#(MEAS_SUM_WL, 2) i_measurement_sum_q_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_cn0_sum_q), .Dout(measurement_cn0_sum_q_syned)
|
||||
);
|
||||
double_ff_syn#(MEAS_WBP_WL, 2) i_measurement_wbp_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_cn0_wbp), .Dout(measurement_cn0_wbp_syned)
|
||||
);
|
||||
double_ff_syn#(MEAS_COUNT_WL, 2) i_measurement_count_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_cn0_sample_count),
|
||||
.Dout(measurement_cn0_sample_count_syned)
|
||||
);
|
||||
double_ff_syn#(16, 2) i_measurement_sequence_syn(
|
||||
.clk(PCLK), .aresetn(PRESETN),
|
||||
.Din(measurement_sequence), .Dout(measurement_sequence_syned)
|
||||
);
|
||||
|
||||
trck_top i_trck_top(
|
||||
// common signals
|
||||
.trk_rstn(PRESETN),
|
||||
.afe_rstn(PRESETN),
|
||||
.trk_rstn(trck_resetn),
|
||||
.afe_rstn(afe_resetn),
|
||||
.trk_clk(trck_clk),
|
||||
.afe_clk(afe_clk),
|
||||
.sample_align_i(sample_align), // from afe
|
||||
@@ -173,15 +260,83 @@ module tracking_wrapper #(
|
||||
// output to regs. (all values are valid on bit_vld except lock ?)
|
||||
.bit_o(nav_bit),
|
||||
.bit_vld_o(bit_valid),
|
||||
.abs_sample_o(abs_sample), // 20 ms bit valid aligned
|
||||
.rem_code_ph_o(rem_code_ph),
|
||||
.lock_o(lock),
|
||||
.abs_sample_o(abs_sample), // 20 ms bit valid aligned
|
||||
.rem_code_ph_o(rem_code_ph),
|
||||
.measurement_vld_o(measurement_valid),
|
||||
.measurement_bit_o(measurement_bit),
|
||||
.measurement_abs_sample_o(measurement_abs_sample),
|
||||
.measurement_rem_code_ph_o(measurement_rem_code_ph),
|
||||
.measurement_carrier_freq_q7_o(measurement_carrier_freq_q7),
|
||||
.measurement_cn0_sum_i_o(measurement_cn0_sum_i),
|
||||
.measurement_cn0_sum_q_o(measurement_cn0_sum_q),
|
||||
.measurement_cn0_wbp_o(measurement_cn0_wbp),
|
||||
.measurement_cn0_sample_count_o(measurement_cn0_sample_count),
|
||||
.measurement_sequence_o(measurement_sequence),
|
||||
.lock_o(lock),
|
||||
.busy_o(busy),
|
||||
|
||||
//unconnected ( for debug purposes )
|
||||
.fifo_of_o()
|
||||
|
||||
);
|
||||
);
|
||||
|
||||
/*
|
||||
* The source payload is stable for 20 ms. Its valid marker and every data
|
||||
* bit pass through the same three-stage synchronizer; two extra PCLK cycles
|
||||
* after the synchronized rising edge provide a conservative bus-settle
|
||||
* margin before the software-visible registers and interrupt are updated.
|
||||
*/
|
||||
always_ff @(posedge PCLK or negedge PRESETN) begin : measurement_capture_seq
|
||||
if(!PRESETN) begin
|
||||
measurement_valid_r <= 1'b0;
|
||||
measurement_capture_pending <= 1'b0;
|
||||
measurement_settle_count <= '0;
|
||||
measurement_capture <= 1'b0;
|
||||
measurement_interrupt_pulse <= 1'b0;
|
||||
measurement_data_valid_reg <= 1'b0;
|
||||
measurement_bit_reg <= 1'b0;
|
||||
measurement_lock_reg <= 1'b0;
|
||||
measurement_abs_sample_reg <= '0;
|
||||
measurement_rem_code_ph_reg <= '0;
|
||||
measurement_carrier_freq_q7_reg <= '0;
|
||||
measurement_cn0_sum_i_reg <= '0;
|
||||
measurement_cn0_sum_q_reg <= '0;
|
||||
measurement_cn0_wbp_reg <= '0;
|
||||
measurement_cn0_sample_count_reg <= '0;
|
||||
measurement_sequence_reg <= '0;
|
||||
end else begin
|
||||
measurement_valid_r <= measurement_valid_syned;
|
||||
measurement_capture <= 1'b0;
|
||||
measurement_interrupt_pulse <= measurement_capture;
|
||||
|
||||
if(measurement_valid_syned && !measurement_valid_r) begin
|
||||
measurement_capture_pending <= 1'b1;
|
||||
measurement_settle_count <= 2;
|
||||
end else if(measurement_capture_pending) begin
|
||||
if(measurement_settle_count != 0) begin
|
||||
measurement_settle_count <= measurement_settle_count - 1'b1;
|
||||
end else begin
|
||||
measurement_capture_pending <= 1'b0;
|
||||
measurement_capture <= 1'b1;
|
||||
measurement_data_valid_reg <= 1'b1;
|
||||
measurement_bit_reg <= measurement_bit_syned;
|
||||
measurement_lock_reg <= lock_syned;
|
||||
measurement_abs_sample_reg <= measurement_abs_sample_syned;
|
||||
measurement_rem_code_ph_reg <= measurement_rem_code_ph_syned;
|
||||
measurement_carrier_freq_q7_reg <=
|
||||
$signed(measurement_carrier_freq_q7_syned);
|
||||
measurement_cn0_sum_i_reg <=
|
||||
$signed(measurement_cn0_sum_i_syned);
|
||||
measurement_cn0_sum_q_reg <=
|
||||
$signed(measurement_cn0_sum_q_syned);
|
||||
measurement_cn0_wbp_reg <= measurement_cn0_wbp_syned;
|
||||
measurement_cn0_sample_count_reg <=
|
||||
measurement_cn0_sample_count_syned;
|
||||
measurement_sequence_reg <= measurement_sequence_syned;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -214,11 +369,13 @@ module tracking_wrapper #(
|
||||
trck_timeout_flag_p <= '0;
|
||||
trck_nav_solve_flag_p <= '0;
|
||||
trck_nav_bit_rcv_flag_p <= '0;
|
||||
trck_measurement_flag_p <= '0;
|
||||
|
||||
// control reg bit fields set by reset.
|
||||
control_reg_p[TRCK_NAV_BITS_FLAG_ENABLE_POS] <= 1'b1;
|
||||
control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] <= 1'b1;
|
||||
control_reg_p[TRCK_STOP_ON_UNLOCK_FLAG_EN_POS] <= 1'b1;
|
||||
control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS] <= 1'b1;
|
||||
|
||||
foreach(nav_bit_read_reg_p[i])
|
||||
nav_bit_read_reg_p[i] <= '0;
|
||||
@@ -244,6 +401,7 @@ module tracking_wrapper #(
|
||||
trck_timeout_flag_p <= trck_timeout_flag_n;
|
||||
trck_nav_bit_rcv_flag_p <= trck_nav_bit_rcv_flag_n;
|
||||
trck_nav_solve_flag_p <= trck_nav_solve_flag_n;
|
||||
trck_measurement_flag_p <= trck_measurement_flag_n;
|
||||
foreach(nav_bit_read_reg_p[i])
|
||||
nav_bit_read_reg_p[i] <= nav_bit_read_reg_n[i];
|
||||
end
|
||||
@@ -409,6 +567,10 @@ module tracking_wrapper #(
|
||||
if((control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS]) & (nav_solve_timer_reached)) begin
|
||||
gps_frame_ready = '1;
|
||||
end
|
||||
// intr due to a coherent 20 ms Doppler/CN0 measurement.
|
||||
if(control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS] && measurement_interrupt_pulse) begin
|
||||
gps_frame_ready = '1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -419,6 +581,7 @@ module tracking_wrapper #(
|
||||
trck_timeout_flag_n = trck_timeout_flag_p; // (all internal clear signals are tied to generic int_clear signal)
|
||||
trck_nav_bit_rcv_flag_n = trck_nav_bit_rcv_flag_p;
|
||||
trck_nav_solve_flag_n = trck_nav_solve_flag_p;
|
||||
trck_measurement_flag_n = trck_measurement_flag_p;
|
||||
foreach(nav_bit_read_reg_n[i])
|
||||
nav_bit_read_reg_n[i] = nav_bit_read_reg_p[i];
|
||||
|
||||
@@ -443,12 +606,21 @@ module tracking_wrapper #(
|
||||
if(control_reg_p[TRCK_INTRPT_ENABLE_POS] && control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS]) intr_pending_n = '1;
|
||||
end
|
||||
|
||||
if(measurement_capture) begin
|
||||
if(control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS])
|
||||
trck_measurement_flag_n = '1;
|
||||
if(control_reg_p[TRCK_INTRPT_ENABLE_POS] &&
|
||||
control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS])
|
||||
intr_pending_n = '1;
|
||||
end
|
||||
|
||||
// generic clear all interrupt pending flag and other flags.
|
||||
if(intr_clear) begin // all internal flag clear
|
||||
intr_pending_n = '0;
|
||||
trck_timeout_flag_n = '0;
|
||||
trck_nav_bit_rcv_flag_n = '0;
|
||||
trck_nav_solve_flag_n = '0;
|
||||
trck_measurement_flag_n = '0;
|
||||
end
|
||||
end : nav_bit_load_and_intr_flag_comb
|
||||
|
||||
@@ -515,6 +687,10 @@ module tracking_wrapper #(
|
||||
|
||||
if(PWDATA[TRCK_NAV_SOLVE_FLAG_EN_POS]) control_reg_n[TRCK_NAV_SOLVE_FLAG_EN_POS] = 1'b1;
|
||||
if(!PWDATA[TRCK_NAV_SOLVE_FLAG_EN_POS]) control_reg_n[TRCK_NAV_SOLVE_FLAG_EN_POS] = 1'b0;
|
||||
if(PWDATA[TRCK_MEASUREMENT_INTR_EN_POS]) control_reg_n[TRCK_MEASUREMENT_INTR_EN_POS] = 1'b1;
|
||||
if(!PWDATA[TRCK_MEASUREMENT_INTR_EN_POS]) control_reg_n[TRCK_MEASUREMENT_INTR_EN_POS] = 1'b0;
|
||||
if(PWDATA[TRCK_MEASUREMENT_FLAG_EN_POS]) control_reg_n[TRCK_MEASUREMENT_FLAG_EN_POS] = 1'b1;
|
||||
if(!PWDATA[TRCK_MEASUREMENT_FLAG_EN_POS]) control_reg_n[TRCK_MEASUREMENT_FLAG_EN_POS] = 1'b0;
|
||||
end
|
||||
(LOW_ADDRESS + PRN_NUMBER_REG_OFFSET):
|
||||
begin
|
||||
@@ -560,20 +736,23 @@ module tracking_wrapper #(
|
||||
(control_reg_p[TRCK_NAV_SOLVE_INTR_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_INTR_EN_POS) |
|
||||
(control_reg_p[TRCK_NAV_BITS_FLAG_ENABLE_POS] << trck_pkg::TRCK_NAV_BITS_FLAG_ENABLE_POS) |
|
||||
(control_reg_p[TRCK_STOP_ON_UNLOCK_FLAG_EN_POS] << trck_pkg::TRCK_STOP_ON_UNLOCK_FLAG_EN_POS) |
|
||||
(control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_FLAG_EN_POS)
|
||||
(control_reg_p[TRCK_NAV_SOLVE_FLAG_EN_POS] << trck_pkg::TRCK_NAV_SOLVE_FLAG_EN_POS) |
|
||||
(control_reg_p[TRCK_MEASUREMENT_INTR_EN_POS] << trck_pkg::TRCK_MEASUREMENT_INTR_EN_POS) |
|
||||
(control_reg_p[TRCK_MEASUREMENT_FLAG_EN_POS] << trck_pkg::TRCK_MEASUREMENT_FLAG_EN_POS)
|
||||
);
|
||||
end
|
||||
(LOW_ADDRESS + STATUS_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = 32'(
|
||||
(busy_syned << TRCK_BUSY_POS) |
|
||||
(lock_syned << TRCK_LOCKED_POS) |
|
||||
(0 << TRCK_FIFO_OV_POS) | // fifo overflow is not considered for now.
|
||||
(intr_pending_p << TRCK_INTR_PEND_POS) |
|
||||
(trck_timeout_flag_p << TRCK_UNLOCK_FLAG_POS) |
|
||||
(trck_nav_bit_rcv_flag_p << TRCK_NAV_BITS_RCV_FLAG_POS) |
|
||||
(trck_nav_solve_flag_p << TRCK_NAV_SOLVE_FLAG_POS)
|
||||
);
|
||||
(busy_syned << TRCK_BUSY_POS) |
|
||||
(lock_syned << TRCK_LOCKED_POS) |
|
||||
(0 << TRCK_FIFO_OV_POS) | // fifo overflow is not considered for now.
|
||||
(intr_pending_p << TRCK_INTR_PEND_POS) |
|
||||
(trck_timeout_flag_p << TRCK_UNLOCK_FLAG_POS) |
|
||||
(trck_nav_bit_rcv_flag_p << TRCK_NAV_BITS_RCV_FLAG_POS) |
|
||||
(trck_nav_solve_flag_p << TRCK_NAV_SOLVE_FLAG_POS) |
|
||||
(trck_measurement_flag_p << TRCK_MEASUREMENT_FLAG_POS)
|
||||
);
|
||||
|
||||
end
|
||||
(LOW_ADDRESS + PRN_NUMBER_REG_OFFSET):
|
||||
@@ -653,6 +832,58 @@ module tracking_wrapper #(
|
||||
//PRDATA = rem_code_ph_reg_p;
|
||||
PRDATA = {{(32-REM_CODE_PH_I_WL){1'b0}}, rem_code_ph_reg_p};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_META_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {
|
||||
8'h01,
|
||||
11'b0,
|
||||
measurement_cn0_sample_count_reg,
|
||||
5'b0,
|
||||
measurement_lock_reg,
|
||||
measurement_bit_reg,
|
||||
measurement_data_valid_reg
|
||||
};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_SEQUENCE_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {16'b0, measurement_sequence_reg};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_CARRIER_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {{(32-MEAS_CARRIER_WL){
|
||||
measurement_carrier_freq_q7_reg[MEAS_CARRIER_WL-1]}},
|
||||
measurement_carrier_freq_q7_reg};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_ABS_SAMPLE_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = measurement_abs_sample_reg;
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_REM_CODE_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {{(32-REM_CODE_PH_I_WL){1'b0}},
|
||||
measurement_rem_code_ph_reg};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_SUM_I_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {{(32-MEAS_SUM_WL){
|
||||
measurement_cn0_sum_i_reg[MEAS_SUM_WL-1]}},
|
||||
measurement_cn0_sum_i_reg};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_SUM_Q_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {{(32-MEAS_SUM_WL){
|
||||
measurement_cn0_sum_q_reg[MEAS_SUM_WL-1]}},
|
||||
measurement_cn0_sum_q_reg};
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_WBP_LO_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = measurement_cn0_wbp_reg[31:0];
|
||||
end
|
||||
(LOW_ADDRESS + MEASUREMENT_WBP_HI_REG_OFFSET):
|
||||
begin
|
||||
PRDATA = {{(64-MEAS_WBP_WL){1'b0}},
|
||||
measurement_cn0_wbp_reg[MEAS_WBP_WL-1:32]};
|
||||
end
|
||||
endcase
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,16 @@ package trck_pkg;
|
||||
NAV_DATA_9_REG_OFFSET = 60,
|
||||
TRCK_TIMEOUT_REG_OFFSET = 64,
|
||||
NAV_SOLVE_TIMER_REG_OFFSET = 68,
|
||||
REM_CODE_PH_REG_OFFSET = 76
|
||||
REM_CODE_PH_REG_OFFSET = 76,
|
||||
MEASUREMENT_META_REG_OFFSET = 80,
|
||||
MEASUREMENT_SEQUENCE_REG_OFFSET = 84,
|
||||
MEASUREMENT_CARRIER_REG_OFFSET = 88,
|
||||
MEASUREMENT_ABS_SAMPLE_REG_OFFSET = 92,
|
||||
MEASUREMENT_REM_CODE_REG_OFFSET = 96,
|
||||
MEASUREMENT_SUM_I_REG_OFFSET = 100,
|
||||
MEASUREMENT_SUM_Q_REG_OFFSET = 104,
|
||||
MEASUREMENT_WBP_LO_REG_OFFSET = 108,
|
||||
MEASUREMENT_WBP_HI_REG_OFFSET = 112
|
||||
}trck_wrapper_reg_offets_e;
|
||||
|
||||
|
||||
@@ -39,7 +48,9 @@ package trck_pkg;
|
||||
TRCK_STOP_ON_UNLOCK_FLAG_EN_POS = 9, // enable pos.
|
||||
TRCK_NAV_SOLVE_ENABLE_POS = 10, // clear pos
|
||||
TRCK_NAV_SOLVE_INTR_EN_POS = 11, // clear pos
|
||||
TRCK_NAV_SOLVE_FLAG_EN_POS = 12 // clear pos
|
||||
TRCK_NAV_SOLVE_FLAG_EN_POS = 12, // clear pos
|
||||
TRCK_MEASUREMENT_INTR_EN_POS = 13,
|
||||
TRCK_MEASUREMENT_FLAG_EN_POS = 14
|
||||
}trck_control_reg_pos_e;
|
||||
|
||||
typedef enum
|
||||
@@ -50,7 +61,8 @@ package trck_pkg;
|
||||
TRCK_INTR_PEND_POS = 3,
|
||||
TRCK_NAV_BITS_RCV_FLAG_POS = 4,
|
||||
TRCK_UNLOCK_FLAG_POS = 5,
|
||||
TRCK_NAV_SOLVE_FLAG_POS = 6
|
||||
TRCK_NAV_SOLVE_FLAG_POS = 6,
|
||||
TRCK_MEASUREMENT_FLAG_POS = 7
|
||||
}trck_status_reg_pos_e;
|
||||
|
||||
localparam logic [7 : 0] PREAMBLE = 8'b10001011;
|
||||
|
||||
@@ -105,6 +105,17 @@ generate
|
||||
.bit_o (trck_o[ch].bit_o),
|
||||
.bit_vld_o (trck_o[ch].bit_vld),
|
||||
.abs_sample_o (trck_o[ch].abs_sample),
|
||||
.rem_code_ph_o (),
|
||||
.measurement_vld_o (),
|
||||
.measurement_bit_o (),
|
||||
.measurement_abs_sample_o(),
|
||||
.measurement_rem_code_ph_o(),
|
||||
.measurement_carrier_freq_q7_o(),
|
||||
.measurement_cn0_sum_i_o(),
|
||||
.measurement_cn0_sum_q_o(),
|
||||
.measurement_cn0_wbp_o(),
|
||||
.measurement_cn0_sample_count_o(),
|
||||
.measurement_sequence_o(),
|
||||
.fifo_of_o (), //reserved
|
||||
.lock_o (trck_o[ch].lock),
|
||||
.busy_o (trck_o[ch].busy)
|
||||
|
||||
@@ -35,6 +35,7 @@ trck_src/cordic_atan_one_stage.sv
|
||||
trck_src/cos2x.sv
|
||||
trck_src/divider.sv
|
||||
trck_src/lock_detector.sv
|
||||
trck_src/tracking_measurement.sv
|
||||
trck_src/trck_cntrl.sv
|
||||
trck_src/trck_top.sv
|
||||
trck_src/xil_fifo.sv
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ module pps_core #(
|
||||
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] kp_q46,
|
||||
input logic signed [pps_pkg::PPS_COEFF_WIDTH-1:0] ki_q46,
|
||||
input logic [pps_pkg::PPS_FRACTION_WIDTH-1:0] max_slew_q32,
|
||||
input logic [31:0] loop_config,
|
||||
output logic commit_ack_toggle,
|
||||
output logic [pps_pkg::PPS_COUNTER_WIDTH-1:0] raw_counter,
|
||||
output logic pps_out,
|
||||
@@ -50,8 +51,7 @@ module pps_core #(
|
||||
PPS_MILLISECONDS_PER_SECOND - 1) / PPS_MILLISECONDS_PER_SECOND);
|
||||
localparam int unsigned PPS_PULSE_COUNTER_WIDTH =
|
||||
(PPS_PULSE_WIDTH_TICKS <= 1) ? 1 : $clog2(PPS_PULSE_WIDTH_TICKS);
|
||||
localparam int unsigned PPS_LOCK_COUNTER_WIDTH =
|
||||
(PPS_LOCK_ACQUIRE_UPDATES <= 1) ? 1 : $clog2(PPS_LOCK_ACQUIRE_UPDATES);
|
||||
localparam int unsigned PPS_LOCK_COUNTER_WIDTH = 8;
|
||||
|
||||
// A navigation commit is rare (normally 5 Hz), while the disciplined clock
|
||||
// runs at 125 MHz. Keep the per-clock time accumulator short and process a
|
||||
@@ -101,6 +101,9 @@ module pps_core #(
|
||||
logic [PPS_SEQUENCE_WIDTH-1:0] epoch_sequence_work;
|
||||
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46_work, ki_q46_work;
|
||||
logic [PPS_FRACTION_WIDTH-1:0] max_slew_q32_work;
|
||||
logic [7:0] lock_acquire_ticks_work;
|
||||
logic [7:0] lock_release_ticks_work;
|
||||
logic [7:0] lock_acquire_updates_work;
|
||||
logic signed [PPS_INCREMENT_WIDTH-1:0] reference_increment_work;
|
||||
|
||||
// Registered arithmetic stages. No multiplier feeds another multiplier,
|
||||
@@ -135,13 +138,15 @@ module pps_core #(
|
||||
logic signed [PPS_PRODUCT_WIDTH-1:0] max_slew_wide;
|
||||
logic signed [PPS_STATE_WIDTH-1:0] max_slew_signed;
|
||||
logic [PPS_STATE_WIDTH-1:0] phase_abs_work;
|
||||
logic [PPS_STATE_WIDTH-1:0] lock_acquire_phase_q32_work;
|
||||
logic [PPS_STATE_WIDTH-1:0] lock_release_phase_q32_work;
|
||||
|
||||
assign raw_counter = raw_counter_p;
|
||||
assign pps_out = pps_out_p;
|
||||
assign aligned = aligned_p;
|
||||
assign tracking = aligned_p && track_enable && !holdover;
|
||||
assign locked = locked_p;
|
||||
assign pps_valid = enable && aligned_p && locked_p;
|
||||
assign pps_valid = enable && aligned_p && locked_p && !holdover;
|
||||
assign stale_sequence = stale_sequence_p;
|
||||
assign stale_age = stale_age_p;
|
||||
assign rate_saturated = rate_saturated_p;
|
||||
@@ -213,6 +218,10 @@ module pps_core #(
|
||||
max_slew_signed = $signed({32'd0, max_slew_q32_work});
|
||||
phase_abs_work = phase_work[PPS_STATE_WIDTH-1] ? $unsigned(-phase_work) :
|
||||
$unsigned(phase_work);
|
||||
lock_acquire_phase_q32_work =
|
||||
{24'd0, lock_acquire_ticks_work, 32'd0};
|
||||
lock_release_phase_q32_work =
|
||||
{24'd0, lock_release_ticks_work, 32'd0};
|
||||
end
|
||||
|
||||
always_ff @(posedge clk or negedge resetn) begin
|
||||
@@ -250,6 +259,9 @@ module pps_core #(
|
||||
kp_q46_work <= '0;
|
||||
ki_q46_work <= '0;
|
||||
max_slew_q32_work <= '0;
|
||||
lock_acquire_ticks_work <= '0;
|
||||
lock_release_ticks_work <= '0;
|
||||
lock_acquire_updates_work <= '0;
|
||||
reference_increment_work <= '0;
|
||||
fraction_ticks_work <= '0;
|
||||
commit_age_ticks_work <= '0;
|
||||
@@ -310,6 +322,9 @@ module pps_core #(
|
||||
kp_q46_work <= kp_q46;
|
||||
ki_q46_work <= ki_q46;
|
||||
max_slew_q32_work <= max_slew_q32;
|
||||
lock_acquire_ticks_work <= loop_config[15:8];
|
||||
lock_release_ticks_work <= loop_config[23:16];
|
||||
lock_acquire_updates_work <= loop_config[31:24];
|
||||
if (aligned_p)
|
||||
reference_increment_work <= $signed({1'b0, NOMINAL_INCREMENT_Q32}) +
|
||||
$signed(rate_word_q32_p);
|
||||
@@ -445,15 +460,16 @@ module pps_core #(
|
||||
// A wider release threshold keeps small measurement noise from
|
||||
// chattering an already valid physical PPS output.
|
||||
if (phase_overflow_work ||
|
||||
(phase_abs_work > PPS_LOCK_RELEASE_PHASE_Q32)) begin
|
||||
(phase_abs_work > lock_release_phase_q32_work)) begin
|
||||
locked_p <= 1'b0;
|
||||
lock_good_updates_p <= '0;
|
||||
pps_out_p <= 1'b0;
|
||||
pulse_ticks_remaining_p <= '0;
|
||||
end else if (!locked_p) begin
|
||||
if (phase_abs_work <= PPS_LOCK_ACQUIRE_PHASE_Q32) begin
|
||||
if (lock_good_updates_p ==
|
||||
PPS_LOCK_COUNTER_WIDTH'(PPS_LOCK_ACQUIRE_UPDATES - 1)) begin
|
||||
if (phase_abs_work <= lock_acquire_phase_q32_work) begin
|
||||
if ((lock_acquire_updates_work <= 8'd1) ||
|
||||
(lock_good_updates_p >=
|
||||
(lock_acquire_updates_work - 1'b1))) begin
|
||||
locked_p <= 1'b1;
|
||||
lock_good_updates_p <= '0;
|
||||
end else begin
|
||||
|
||||
@@ -28,6 +28,12 @@ package pps_pkg;
|
||||
localparam logic [PPS_STATE_WIDTH-1:0] PPS_LOCK_RELEASE_PHASE_Q32 =
|
||||
64'h0000_0020_0000_0000; // 32 tracking-clock ticks (256 ns at 125 MHz)
|
||||
localparam logic [PPS_FRACTION_WIDTH-1:0] PPS_DEFAULT_MAX_SLEW_Q32 = 32'd85899;
|
||||
localparam logic [31:0] PPS_DEFAULT_LOOP_CONFIG = {
|
||||
8'(PPS_LOCK_ACQUIRE_UPDATES),
|
||||
PPS_LOCK_RELEASE_PHASE_Q32[39:32],
|
||||
PPS_LOCK_ACQUIRE_PHASE_Q32[39:32],
|
||||
8'd0 // reserved
|
||||
};
|
||||
localparam logic [PPS_STATE_WIDTH-1:0] PPS_ONE_TICK_Q32 = 64'h0000_0001_0000_0000;
|
||||
|
||||
typedef enum logic [31:0] {
|
||||
@@ -64,7 +70,8 @@ package pps_pkg;
|
||||
PPS_DIAG_SNAPSHOT_OFFSET = 32'h0000_0078,
|
||||
PPS_VERSION_REG_OFFSET = 32'h0000_007c,
|
||||
PPS_KP_Q46_HI_OFFSET = 32'h0000_0080,
|
||||
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084
|
||||
PPS_KI_Q46_HI_OFFSET = 32'h0000_0084,
|
||||
PPS_LOOP_CONFIG_OFFSET = 32'h0000_0088
|
||||
} pps_wrapper_reg_offsets_e;
|
||||
|
||||
typedef enum int unsigned {
|
||||
@@ -104,7 +111,7 @@ package pps_pkg;
|
||||
PPS_APB_STATE_FINISH
|
||||
} pps_apb_state_e;
|
||||
|
||||
localparam logic [31:0] PPS_VERSION = 32'h0002_0100;
|
||||
localparam logic [31:0] PPS_VERSION = 32'h0002_0200;
|
||||
function automatic logic [PPS_COUNTER_WIDTH-1:0] pps_gray_to_binary(
|
||||
input logic [PPS_COUNTER_WIDTH-1:0] gray);
|
||||
logic [PPS_COUNTER_WIDTH-1:0] binary;
|
||||
|
||||
@@ -39,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),
|
||||
|
||||
@@ -14,6 +14,7 @@ test:
|
||||
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_wrapper_tb $(QUESTA_VSIM) -c -l wrapper.log -wlf wrapper.wlf -do $(CURDIR)/run.do
|
||||
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_core_tb $(QUESTA_VSIM) -c -l core.log -wlf core.wlf -do $(CURDIR)/run.do
|
||||
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_convergence_tb $(QUESTA_VSIM) -c -l convergence.log -wlf convergence.wlf -do $(CURDIR)/run.do
|
||||
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_adaptive_tb $(QUESTA_VSIM) -c -l adaptive.log -wlf adaptive.wlf -do $(CURDIR)/run.do
|
||||
cd $(QUESTA_RUN_DIR) && PPS_TB_DIR=$(CURDIR) PPS_TB_TOP=pps_precision_tb $(QUESTA_VSIM) -c -l precision.log -wlf precision.wlf -do $(CURDIR)/run.do
|
||||
|
||||
clean:
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module pps_adaptive_tb;
|
||||
import pps_pkg::*;
|
||||
|
||||
localparam int unsigned CLK_FREQ_HZ = 1000;
|
||||
localparam logic [63:0] TICK_Q32 = 64'h0000_0001_0000_0000;
|
||||
localparam logic [63:0] SECOND_Q32 = CLK_FREQ_HZ * TICK_Q32;
|
||||
localparam logic [31:0] ACQUIRE_CONFIG = 32'h0540_0800;
|
||||
localparam logic [31:0] PRECISION_CONFIG = 32'h0340_0800;
|
||||
localparam logic signed [47:0] ACQUIRE_KP_Q46 = 48'sh0800_0000_0000;
|
||||
localparam logic signed [47:0] ACQUIRE_KI_Q46 = 48'sh0400_0000_0000;
|
||||
localparam logic signed [47:0] PRECISION_KP_Q46 = 48'sh0200_0000_0000;
|
||||
localparam logic signed [47:0] PRECISION_KI_Q46 = 48'sh0100_0000_0000;
|
||||
|
||||
logic clk = 1'b0;
|
||||
logic resetn = 1'b0;
|
||||
logic enable = 1'b1;
|
||||
logic track_enable = 1'b1;
|
||||
logic holdover = 1'b0;
|
||||
logic clear_status_toggle = 1'b0;
|
||||
logic commit_toggle = 1'b0;
|
||||
logic commit_coarse = 1'b0;
|
||||
logic [63:0] epoch_counter = '0;
|
||||
logic [63:0] gps_seconds = '0;
|
||||
logic [31:0] gps_fraction_q32 = '0;
|
||||
logic [31:0] epoch_sequence = '0;
|
||||
logic signed [PPS_COEFF_WIDTH-1:0] kp_q46 = ACQUIRE_KP_Q46;
|
||||
logic signed [PPS_COEFF_WIDTH-1:0] ki_q46 = ACQUIRE_KI_Q46;
|
||||
logic [31:0] max_slew_q32 = 32'h7fff_ffff;
|
||||
logic [31:0] loop_config = ACQUIRE_CONFIG;
|
||||
logic commit_ack_toggle;
|
||||
logic [63:0] raw_counter;
|
||||
logic pps_out, aligned, tracking, locked, pps_valid;
|
||||
logic stale_sequence, stale_age, rate_saturated, phase_saturated;
|
||||
logic [31:0] active_sequence, pulse_count;
|
||||
logic [63:0] active_seconds, active_subsec_q32;
|
||||
logic signed [63:0] phase_error_q32;
|
||||
logic signed [63:0] integrator_q32, rate_word_q32;
|
||||
int checks = 0;
|
||||
|
||||
always #4 clk = ~clk;
|
||||
|
||||
pps_core #(.CLK_FREQ_HZ(CLK_FREQ_HZ)) dut (
|
||||
.clk(clk), .resetn(resetn), .enable(enable), .track_enable(track_enable),
|
||||
.holdover(holdover), .clear_status_toggle(clear_status_toggle),
|
||||
.commit_toggle(commit_toggle), .commit_coarse(commit_coarse),
|
||||
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
|
||||
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
|
||||
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
|
||||
.loop_config(loop_config),
|
||||
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
|
||||
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
|
||||
.pps_valid(pps_valid), .stale_sequence(stale_sequence),
|
||||
.stale_age(stale_age), .rate_saturated(rate_saturated),
|
||||
.phase_saturated(phase_saturated), .active_sequence(active_sequence),
|
||||
.active_seconds(active_seconds), .active_subsec_q32(active_subsec_q32),
|
||||
.phase_error_q32(phase_error_q32), .integrator_q32(integrator_q32),
|
||||
.rate_word_q32(rate_word_q32), .pulse_count(pulse_count)
|
||||
);
|
||||
|
||||
task automatic check_adaptive(input logic condition, input string message);
|
||||
begin
|
||||
checks = checks + 1;
|
||||
if (!condition)
|
||||
$fatal(1, "ADAPTIVE CHECK %0d FAILED: %s", checks, message);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic commit_anchor(
|
||||
input logic [63:0] counter_value,
|
||||
input logic [63:0] seconds_value,
|
||||
input logic [31:0] fraction_value,
|
||||
input logic [31:0] sequence_id,
|
||||
input logic coarse);
|
||||
logic expected_ack;
|
||||
int timeout;
|
||||
begin
|
||||
@(negedge clk);
|
||||
epoch_counter = counter_value;
|
||||
gps_seconds = seconds_value;
|
||||
gps_fraction_q32 = fraction_value;
|
||||
epoch_sequence = sequence_id;
|
||||
commit_coarse = coarse;
|
||||
commit_toggle = ~commit_toggle;
|
||||
expected_ack = commit_toggle;
|
||||
timeout = 0;
|
||||
while (commit_ack_toggle != expected_ack) begin
|
||||
@(posedge clk);
|
||||
timeout = timeout + 1;
|
||||
if (timeout > 40) $fatal(1, "adaptive commit timeout");
|
||||
end
|
||||
@(negedge clk);
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic commit_current_offset(
|
||||
input integer signed offset_ticks,
|
||||
input logic [31:0] sequence_id);
|
||||
logic [63:0] counter_value;
|
||||
logic [63:0] seconds_value;
|
||||
logic signed [64:0] subsec_value;
|
||||
logic [31:0] fraction_value;
|
||||
begin
|
||||
@(negedge clk);
|
||||
counter_value = raw_counter;
|
||||
seconds_value = active_seconds;
|
||||
subsec_value = $signed({1'b0, active_subsec_q32}) +
|
||||
(offset_ticks * $signed({1'b0, TICK_Q32}));
|
||||
if (subsec_value >= $signed({1'b0, SECOND_Q32})) begin
|
||||
subsec_value = subsec_value - $signed({1'b0, SECOND_Q32});
|
||||
seconds_value = seconds_value + 64'd1;
|
||||
end else if (subsec_value < 0) begin
|
||||
subsec_value = subsec_value + $signed({1'b0, SECOND_Q32});
|
||||
seconds_value = seconds_value - 64'd1;
|
||||
end
|
||||
fraction_value = subsec_value / CLK_FREQ_HZ;
|
||||
commit_anchor(
|
||||
counter_value, seconds_value, fraction_value, sequence_id, 1'b0);
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
logic [31:0] sequence_before_holdover;
|
||||
logic signed [63:0] acquire_rate;
|
||||
logic signed [63:0] acquire_integrator;
|
||||
|
||||
repeat (5) @(posedge clk);
|
||||
resetn = 1'b1;
|
||||
repeat (5) @(posedge clk);
|
||||
|
||||
commit_anchor(raw_counter, 64'd100, 32'd0, 32'd1, 1'b1);
|
||||
check_adaptive(aligned && !locked && !pps_valid,
|
||||
"coarse alignment starts invalid and unlocked");
|
||||
|
||||
// Lock detection uses the unfiltered PI phase residual. Five coherent
|
||||
// updates inside the programmable eight-tick window are required.
|
||||
for (int update = 0; update < 4; update = update + 1) begin
|
||||
commit_current_offset(0, update + 2);
|
||||
check_adaptive(!locked, "lock cannot assert before configured count");
|
||||
end
|
||||
commit_current_offset(0, 32'd6);
|
||||
check_adaptive(locked && pps_valid,
|
||||
"raw coherent residuals acquire lock");
|
||||
|
||||
// Exercise the acquisition gains, then replace Kp, Ki and lock policy
|
||||
// while running. The core captures one coherent set per commit and keeps
|
||||
// the integrator state across a profile transition.
|
||||
commit_current_offset(1, 32'd7);
|
||||
acquire_rate = rate_word_q32;
|
||||
acquire_integrator = integrator_q32;
|
||||
check_adaptive((acquire_rate > 0) && (acquire_integrator > 0),
|
||||
"acquisition Kp and Ki both contribute");
|
||||
|
||||
kp_q46 = PRECISION_KP_Q46;
|
||||
ki_q46 = PRECISION_KI_Q46;
|
||||
loop_config = PRECISION_CONFIG;
|
||||
commit_current_offset(1, 32'd8);
|
||||
check_adaptive(locked && pps_valid,
|
||||
"on-the-fly PI profile transition preserves lock");
|
||||
check_adaptive(integrator_q32 > acquire_integrator,
|
||||
"new Ki continues the preserved integrator");
|
||||
check_adaptive(rate_word_q32 < acquire_rate,
|
||||
"lower precision Kp/Ki reduce loop correction");
|
||||
check_adaptive((dut.kp_q46_work == PRECISION_KP_Q46) &&
|
||||
(dut.ki_q46_work == PRECISION_KI_Q46),
|
||||
"commit atomically captures runtime Kp and Ki");
|
||||
|
||||
sequence_before_holdover = active_sequence;
|
||||
holdover = 1'b1;
|
||||
repeat (3) @(posedge clk);
|
||||
check_adaptive(locked && !tracking && !pps_valid,
|
||||
"holdover retains lock state but marks PPS invalid");
|
||||
commit_current_offset(0, 32'd9);
|
||||
check_adaptive(active_sequence == sequence_before_holdover,
|
||||
"holdover rejects control updates");
|
||||
holdover = 1'b0;
|
||||
repeat (3) @(posedge clk);
|
||||
check_adaptive(pps_valid && tracking,
|
||||
"leaving holdover restores validity without a phase step");
|
||||
|
||||
commit_current_offset(200, 32'd10);
|
||||
check_adaptive(!locked && !pps_valid,
|
||||
"one raw residual beyond release threshold drops lock");
|
||||
|
||||
$display("PPS ADAPTIVE TB PASS: %0d checks phase=%0d rate=%0d",
|
||||
checks, phase_error_q32, rate_word_q32);
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -75,6 +75,7 @@ module pps_convergence_tb;
|
||||
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
|
||||
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
|
||||
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
|
||||
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
|
||||
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
|
||||
.pps_out(pps_out), .aligned(aligned), .tracking(tracking), .locked(locked),
|
||||
.pps_valid(pps_valid),
|
||||
|
||||
@@ -40,6 +40,7 @@ module pps_core_tb;
|
||||
.epoch_counter(epoch_counter), .gps_seconds(gps_seconds),
|
||||
.gps_fraction_q32(gps_fraction_q32), .epoch_sequence(epoch_sequence),
|
||||
.kp_q46(kp_q46), .ki_q46(ki_q46), .max_slew_q32(max_slew_q32),
|
||||
.loop_config(PPS_DEFAULT_LOOP_CONFIG),
|
||||
.commit_ack_toggle(commit_ack_toggle), .raw_counter(raw_counter),
|
||||
.pps_out(pps_out), .aligned(aligned),
|
||||
.tracking(tracking), .locked(locked), .pps_valid(pps_valid),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ vlog -sv \
|
||||
[file join $tb_dir pps_wrapper_tb.sv] \
|
||||
[file join $tb_dir pps_core_tb.sv] \
|
||||
[file join $tb_dir pps_convergence_tb.sv] \
|
||||
[file join $tb_dir pps_adaptive_tb.sv] \
|
||||
[file join $tb_dir pps_precision_tb.sv]
|
||||
vsim -c -voptargs=+acc work.$::env(PPS_TB_TOP)
|
||||
run -all
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# GPS RTL
|
||||
|
||||
Take `Ankasys_Pulpissimo_SoC/pulpissimo/rtl/gps_system/hdl` folder from Ankasys_Pulpissimo_SoC svn repo, it has these subfolders:
|
||||
|
||||
@@ -16,6 +17,93 @@ trck_src:
|
||||
acq_src:
|
||||
acqusition top and instantiated sub ips hdl codes from Hilal & Ahmet's implementations.
|
||||
|
||||
## 20 ms Tracking Measurement Mailbox
|
||||
|
||||
Her tracking kanali, decode edilen her GPS L1 C/A navigation biti icin bir
|
||||
atomik olcum uretir. Veri yolu:
|
||||
|
||||
```text
|
||||
1 ms prompt I/Q
|
||||
-> tracking_measurement (20 ornek akumulasyonu ve snapshot)
|
||||
-> tracking clock domaininde 20 ms sabit payload
|
||||
-> her alan icin iki FF senkronizer
|
||||
-> iki tam PCLK settle cevrimi
|
||||
-> tracking_wrapper APB register bankasi
|
||||
-> measurement flag/interrupt
|
||||
-> bare-metal software
|
||||
```
|
||||
|
||||
`trck_src/tracking_measurement.sv` bir bit boyunca su degerleri toplar:
|
||||
|
||||
```text
|
||||
sum_i = sum(I_prompt)
|
||||
sum_q = sum(Q_prompt)
|
||||
wbp = sum(I_prompt^2 + Q_prompt^2)
|
||||
M = 20 adet 1 ms prompt ornegi
|
||||
```
|
||||
|
||||
Prompt enerji karesi lock detector'daki mevcut multiplier sonucundan alinir;
|
||||
ayni is icin ikinci bir multiplier/DSP yolu acilmaz. Bit epoch'unda total
|
||||
carrier NCO frekansi Q7 formatinda, absolute sample, remaining-code phase,
|
||||
navigation bit ve 16-bit sequence degeriyle birlikte snapshot edilir. Payload
|
||||
bir sonraki bit olcumune kadar degismez.
|
||||
|
||||
Bu arayuz bir event FIFO'su degildir. Kaynak veri en az bir 20 ms bit boyunca
|
||||
sabit kaldigi icin wrapper her multi-bit alani mevcut `double_ff_syn` yapisindan
|
||||
gecirir, senkronize `valid` rising edge'inden sonra iki tam PCLK cevrimi bekler
|
||||
ve butun bankayi tek seferde register eder. Measurement interrupt'i APB
|
||||
snapshot'tan bir PCLK sonra uretilir; software interrupt'a girdiginde veri ve
|
||||
status stabildir. Software yine de coherent read icin sequence degerini
|
||||
payload'dan once ve sonra okuyup esitligini kontrol eder.
|
||||
|
||||
### APB registerlari
|
||||
|
||||
| Offset | Alan | Aciklama |
|
||||
| ---: | --- | --- |
|
||||
| `0x50` | `MEASUREMENT_META` | `[31:24]=version(1)`, `[12:8]=M`, bit 2 lock, bit 1 NAV bit, bit 0 valid |
|
||||
| `0x54` | `MEASUREMENT_SEQUENCE` | 16-bit, her 20 ms olcumde artar |
|
||||
| `0x58` | `MEASUREMENT_CARRIER_Q7` | Signed total carrier NCO frekansi, Hz icin `/128` |
|
||||
| `0x5c` | `MEASUREMENT_ABS_SAMPLE` | Olcum epoch absolute sample |
|
||||
| `0x60` | `MEASUREMENT_REM_CODE_PHASE` | 26-bit remaining-code residue |
|
||||
| `0x64` | `MEASUREMENT_SUM_I` | Signed coherent prompt-I toplami |
|
||||
| `0x68` | `MEASUREMENT_SUM_Q` | Signed coherent prompt-Q toplami |
|
||||
| `0x6c` | `MEASUREMENT_WBP_LO` | Wideband power alt 32 bit |
|
||||
| `0x70` | `MEASUREMENT_WBP_HI` | Wideband power ust bitleri |
|
||||
|
||||
`CONTROL_REG` bit 13 measurement interrupt enable, bit 14 measurement flag
|
||||
enable'dir. `STATUS_REG` bit 7 measurement flag'dir. Genel interrupt clear
|
||||
islemi bu flag'i de temizler.
|
||||
|
||||
### Neden Doppler IIR RTL'de degil?
|
||||
|
||||
IIR RTL'de de uygulanabilir; her kanal icin onceki Doppler state'i, signed
|
||||
coefficient multiply, rounding/saturation ve reacquisition reset kurali
|
||||
eklenmesi yeterlidir. Mevcut tasarimda bilerek software'de tutulur:
|
||||
|
||||
- APB bankasi ham ve tekrar analiz edilebilir carrier olcumunu korur.
|
||||
- Filtre katsayisi hareket profiline gore bitstream almadan degistirilebilir.
|
||||
- C/N0, satellite state ve WLS zaten software'de hesaplanir.
|
||||
- Filtre veri/interrupt hizini azaltmaz; measurement yine 50 Hz'dir.
|
||||
- Kart testinde kesintisiz kosuda measurement `missed`, `invalid` ve event
|
||||
overflow sayaclari sifir kalmistir; CPU veya CDC baskisi RTL filtresi
|
||||
gerektirmemistir.
|
||||
|
||||
RTL filtresi ancak bu ham register sozlesmesinin yaninda ikinci bir filtered
|
||||
alan istenirse, olcum tamamen donanim icinde tuketilecekse veya software
|
||||
latency/CPU butcesi kart testinde yetersiz kalirsa anlamli olur.
|
||||
|
||||
### RTL dogrulama
|
||||
|
||||
Questa testleri:
|
||||
|
||||
```bash
|
||||
make -C hardware/external/gps/rtl/trck_src/tb test
|
||||
```
|
||||
|
||||
Testler prompt enerjisini, 20-ornek akumulasyonunu, iki ard arda bit epoch'unu,
|
||||
20 ms hold davranisini, double-FF + settle + atomik APB capture'i ve
|
||||
interrupt/status-clear yolunu kontrol eder.
|
||||
|
||||
|
||||
## SVN Revision
|
||||
svn info
|
||||
@@ -45,4 +133,3 @@ Last Changed Author: ankaaa
|
||||
Last Changed Rev: 89
|
||||
Last Changed Date: 2025-07-08 14:20:14 +0300 (Tue, 08 Jul 2025)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,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.
|
||||
@@ -34,6 +34,8 @@ module lock_detector #(
|
||||
input logic corr_vld_i,
|
||||
input logic signed [IN_WL-1 : 0] i_p_i,
|
||||
input logic signed [IN_WL-1 : 0] q_p_i,
|
||||
output logic prompt_energy_vld_o,
|
||||
output logic [(2 * IN_WL):0] prompt_energy_o,
|
||||
output logic lock_o
|
||||
);
|
||||
localparam DIV_OUT_IL = DIV_OUT_WL - DIV_OUT_FL;
|
||||
@@ -64,6 +66,8 @@ logic lock_decision_bad;
|
||||
assign i_prompt_abs = i_p_i[IN_WL-1] ? (~i_p_i + {{(IN_WL-1){1'b0}}, 1'b1}) : i_p_i;
|
||||
assign q_prompt_abs = q_p_i[IN_WL-1] ? (~q_p_i + {{(IN_WL-1){1'b0}}, 1'b1}) : q_p_i;
|
||||
assign prompt_energy_sum = {1'b0, i_prompt_energy_r} + {1'b0, q_prompt_energy_r};
|
||||
assign prompt_energy_vld_o = prompt_energy_vld;
|
||||
assign prompt_energy_o = prompt_energy_sum;
|
||||
|
||||
cos2x #(
|
||||
.IN_WL (IN_WL),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
QUESTA_VLOG ?= vlog
|
||||
QUESTA_VSIM ?= vsim
|
||||
QUESTA_RUN_DIR ?= /tmp/gps_tracking_measurement_questa
|
||||
|
||||
RTL_DIR := $(abspath ../..)
|
||||
|
||||
.PHONY: test clean
|
||||
|
||||
test:
|
||||
rm -rf $(QUESTA_RUN_DIR)
|
||||
mkdir -p $(QUESTA_RUN_DIR)
|
||||
cd $(QUESTA_RUN_DIR) && vlib work
|
||||
cd $(RTL_DIR) && $(QUESTA_VLOG) -sv -work $(QUESTA_RUN_DIR)/work \
|
||||
-f filelist.f \
|
||||
$(CURDIR)/lock_detector_energy_tb.sv \
|
||||
$(CURDIR)/tracking_measurement_tb.sv \
|
||||
$(CURDIR)/tracking_wrapper_measurement_tb.sv
|
||||
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c tracking_measurement_tb \
|
||||
-do 'run -all; quit -f'
|
||||
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c lock_detector_energy_tb \
|
||||
-do 'run -all; quit -f'
|
||||
cd $(QUESTA_RUN_DIR) && $(QUESTA_VSIM) -c tracking_wrapper_measurement_tb \
|
||||
-do 'run -all; quit -f'
|
||||
|
||||
clean:
|
||||
rm -rf $(QUESTA_RUN_DIR)
|
||||
@@ -0,0 +1,54 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
import definitions::*;
|
||||
|
||||
module lock_detector_energy_tb;
|
||||
logic clk = 1'b0;
|
||||
logic rstn = 1'b0;
|
||||
logic corr_valid = 1'b0;
|
||||
logic signed [TRCK_CORR_WL-1:0] i_prompt = '0;
|
||||
logic signed [TRCK_CORR_WL-1:0] q_prompt = '0;
|
||||
logic energy_valid;
|
||||
logic [(2*TRCK_CORR_WL):0] energy;
|
||||
logic lock;
|
||||
|
||||
always #4 clk = ~clk;
|
||||
|
||||
lock_detector #(
|
||||
.IN_WL(TRCK_CORR_WL),
|
||||
.IN_FL(TRCK_CORR_FL),
|
||||
.DIV_OUT_WL(LOCK_DET_DIV_O_WL),
|
||||
.DIV_OUT_FL(LOCK_DET_DIV_O_FL),
|
||||
.DIV_ITER(14)
|
||||
) dut (
|
||||
.rstn,
|
||||
.clk,
|
||||
.corr_vld_i(corr_valid),
|
||||
.i_p_i(i_prompt),
|
||||
.q_p_i(q_prompt),
|
||||
.prompt_energy_vld_o(energy_valid),
|
||||
.prompt_energy_o(energy),
|
||||
.lock_o(lock)
|
||||
);
|
||||
|
||||
initial begin
|
||||
repeat(4) @(posedge clk);
|
||||
rstn = 1'b1;
|
||||
repeat(2) @(posedge clk);
|
||||
|
||||
i_prompt = -22'sd1234;
|
||||
q_prompt = 22'sd567;
|
||||
corr_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
corr_valid = 1'b0;
|
||||
|
||||
wait(energy_valid);
|
||||
#1;
|
||||
if(energy !== ((1234 * 1234) + (567 * 567)))
|
||||
$fatal(1, "prompt energy mismatch: got=%0d expected=%0d",
|
||||
energy, ((1234 * 1234) + (567 * 567)));
|
||||
|
||||
$display("lock_detector_energy_tb PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,185 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
import definitions::*;
|
||||
|
||||
module tracking_measurement_tb;
|
||||
localparam int SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT);
|
||||
localparam int WBP_WL = (2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
|
||||
localparam int COUNT_WL = $clog2(MS_PER_BIT + 1);
|
||||
localparam int CARRIER_WL = ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
|
||||
|
||||
logic clk = 1'b0;
|
||||
logic rstn = 1'b0;
|
||||
logic bit_valid = 1'b0;
|
||||
logic nav_bit = 1'b0;
|
||||
logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample = '0;
|
||||
logic [REM_CODE_PH_I_WL-1:0] rem_code_phase = '0;
|
||||
logic signed [CARRIER_WL-1:0] carrier_freq_q7 = '0;
|
||||
logic corr_valid = 1'b0;
|
||||
logic signed [TRCK_CORR_WL-1:0] i_prompt = '0;
|
||||
logic signed [TRCK_CORR_WL-1:0] q_prompt = '0;
|
||||
logic prompt_energy_valid = 1'b0;
|
||||
logic [(2*TRCK_CORR_WL):0] prompt_energy = '0;
|
||||
|
||||
logic measurement_valid;
|
||||
logic measurement_bit;
|
||||
logic [ABS_SAMPLE_CNTR_WL-1:0] measurement_abs_sample;
|
||||
logic [REM_CODE_PH_I_WL-1:0] measurement_rem_code_phase;
|
||||
logic signed [CARRIER_WL-1:0] measurement_carrier_freq_q7;
|
||||
logic signed [SUM_WL-1:0] cn0_sum_i;
|
||||
logic signed [SUM_WL-1:0] cn0_sum_q;
|
||||
logic [WBP_WL-1:0] cn0_wbp;
|
||||
logic [COUNT_WL-1:0] cn0_sample_count;
|
||||
logic [15:0] sequence_value;
|
||||
|
||||
integer expected_sum_i;
|
||||
integer expected_sum_q;
|
||||
longint unsigned expected_wbp;
|
||||
|
||||
always #4 clk = ~clk;
|
||||
|
||||
tracking_measurement dut (
|
||||
.clk,
|
||||
.rstn,
|
||||
.bit_valid_i(bit_valid),
|
||||
.nav_bit_i(nav_bit),
|
||||
.absolute_sample_i(absolute_sample),
|
||||
.rem_code_phase_i(rem_code_phase),
|
||||
.carrier_freq_q7_i(carrier_freq_q7),
|
||||
.corr_valid_i(corr_valid),
|
||||
.i_prompt_i(i_prompt),
|
||||
.q_prompt_i(q_prompt),
|
||||
.prompt_energy_valid_i(prompt_energy_valid),
|
||||
.prompt_energy_i(prompt_energy),
|
||||
.measurement_valid_o(measurement_valid),
|
||||
.nav_bit_o(measurement_bit),
|
||||
.absolute_sample_o(measurement_abs_sample),
|
||||
.rem_code_phase_o(measurement_rem_code_phase),
|
||||
.carrier_freq_q7_o(measurement_carrier_freq_q7),
|
||||
.cn0_sum_i_o(cn0_sum_i),
|
||||
.cn0_sum_q_o(cn0_sum_q),
|
||||
.cn0_wbp_o(cn0_wbp),
|
||||
.cn0_sample_count_o(cn0_sample_count),
|
||||
.sequence_o(sequence_value)
|
||||
);
|
||||
|
||||
task automatic clock_cycles(input int unsigned count);
|
||||
repeat(count) @(posedge clk);
|
||||
endtask
|
||||
|
||||
task automatic pulse_bit_epoch(input logic value);
|
||||
nav_bit = value;
|
||||
bit_valid = 1'b1;
|
||||
clock_cycles(12);
|
||||
bit_valid = 1'b0;
|
||||
clock_cycles(3);
|
||||
endtask
|
||||
|
||||
task automatic add_prompt(input integer i_value, input integer q_value);
|
||||
longint unsigned energy;
|
||||
energy = (i_value * i_value) + (q_value * q_value);
|
||||
i_prompt = i_value;
|
||||
q_prompt = q_value;
|
||||
corr_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
corr_valid = 1'b0;
|
||||
@(posedge clk);
|
||||
prompt_energy = energy;
|
||||
prompt_energy_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
prompt_energy_valid = 1'b0;
|
||||
@(posedge clk);
|
||||
expected_sum_i += i_value;
|
||||
expected_sum_q += q_value;
|
||||
expected_wbp += energy;
|
||||
endtask
|
||||
|
||||
task automatic check_measurement(
|
||||
input logic expected_bit,
|
||||
input logic [31:0] expected_abs,
|
||||
input logic [REM_CODE_PH_I_WL-1:0] expected_rem,
|
||||
input logic signed [CARRIER_WL-1:0] expected_carrier,
|
||||
input logic [15:0] expected_sequence);
|
||||
if(!measurement_valid)
|
||||
$fatal(1, "measurement valid did not remain high");
|
||||
if(measurement_bit !== expected_bit)
|
||||
$fatal(1, "NAV bit mismatch");
|
||||
if(measurement_abs_sample !== expected_abs)
|
||||
$fatal(1, "absolute sample mismatch");
|
||||
if(measurement_rem_code_phase !== expected_rem)
|
||||
$fatal(1, "remaining code phase mismatch");
|
||||
if(measurement_carrier_freq_q7 !== expected_carrier)
|
||||
$fatal(1, "carrier Q7 mismatch: got=%0d expected=%0d",
|
||||
measurement_carrier_freq_q7, expected_carrier);
|
||||
if($signed(cn0_sum_i) !== expected_sum_i)
|
||||
$fatal(1, "sum I mismatch: got=%0d expected=%0d",
|
||||
$signed(cn0_sum_i), expected_sum_i);
|
||||
if($signed(cn0_sum_q) !== expected_sum_q)
|
||||
$fatal(1, "sum Q mismatch: got=%0d expected=%0d",
|
||||
$signed(cn0_sum_q), expected_sum_q);
|
||||
if(cn0_wbp !== expected_wbp)
|
||||
$fatal(1, "WBP mismatch: got=%0d expected=%0d", cn0_wbp, expected_wbp);
|
||||
if(cn0_sample_count !== MS_PER_BIT)
|
||||
$fatal(1, "sample count mismatch: got=%0d", cn0_sample_count);
|
||||
if(sequence_value !== expected_sequence)
|
||||
$fatal(1, "sequence mismatch: got=%0d expected=%0d",
|
||||
sequence_value, expected_sequence);
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
clock_cycles(4);
|
||||
rstn = 1'b1;
|
||||
clock_cycles(2);
|
||||
|
||||
/* First edge only establishes the 20 ms integration phase. */
|
||||
pulse_bit_epoch(1'b0);
|
||||
|
||||
expected_sum_i = 0;
|
||||
expected_sum_q = 0;
|
||||
expected_wbp = 0;
|
||||
for(integer i = 0; i < MS_PER_BIT; i++) begin
|
||||
add_prompt(100 + i, -20 + i);
|
||||
end
|
||||
|
||||
nav_bit = 1'b1;
|
||||
absolute_sample = 32'h1234_5678;
|
||||
rem_code_phase = REM_CODE_PH_I_WL'(321);
|
||||
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ + 1250) * 128);
|
||||
bit_valid = 1'b1;
|
||||
clock_cycles(10);
|
||||
check_measurement(
|
||||
1'b1, 32'h1234_5678, REM_CODE_PH_I_WL'(321),
|
||||
CARRIER_WL'((CENTER_FREQ + 1250) * 128), 16'd1);
|
||||
|
||||
/* Payload inputs may move; the registered measurement must not. */
|
||||
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ - 3000) * 128);
|
||||
absolute_sample = 32'hffff_0000;
|
||||
clock_cycles(5);
|
||||
check_measurement(
|
||||
1'b1, 32'h1234_5678, REM_CODE_PH_I_WL'(321),
|
||||
CARRIER_WL'((CENTER_FREQ + 1250) * 128), 16'd1);
|
||||
bit_valid = 1'b0;
|
||||
clock_cycles(3);
|
||||
if(measurement_valid)
|
||||
$fatal(1, "measurement valid failed to deassert");
|
||||
|
||||
expected_sum_i = 0;
|
||||
expected_sum_q = 0;
|
||||
expected_wbp = 0;
|
||||
for(integer j = 0; j < MS_PER_BIT; j++) begin
|
||||
add_prompt(-50, 25);
|
||||
end
|
||||
nav_bit = 1'b0;
|
||||
absolute_sample = 32'h2345_6789;
|
||||
rem_code_phase = REM_CODE_PH_I_WL'(777);
|
||||
carrier_freq_q7 = CARRIER_WL'((CENTER_FREQ - 3000) * 128);
|
||||
bit_valid = 1'b1;
|
||||
clock_cycles(10);
|
||||
check_measurement(
|
||||
1'b0, 32'h2345_6789, REM_CODE_PH_I_WL'(777),
|
||||
CARRIER_WL'((CENTER_FREQ - 3000) * 128), 16'd2);
|
||||
|
||||
$display("tracking_measurement_tb PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,172 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
import definitions::*;
|
||||
import trck_pkg::*;
|
||||
|
||||
module tracking_wrapper_measurement_tb;
|
||||
localparam int CARRIER_WL = ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL;
|
||||
localparam int SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT);
|
||||
localparam int WBP_WL = (2 * TRCK_CORR_WL) + 1 + $clog2(MS_PER_BIT);
|
||||
localparam logic [29:0] EXPECTED_CARRIER =
|
||||
CARRIER_WL'((CENTER_FREQ + 2500) * 128);
|
||||
localparam logic [WBP_WL-1:0] EXPECTED_WBP =
|
||||
WBP_WL'(50'h2_1234_89ab_cdef);
|
||||
|
||||
logic PCLK = 1'b0;
|
||||
logic PRESETN = 1'b0;
|
||||
logic [11:0] PADDR = '0;
|
||||
logic PSEL = 1'b0;
|
||||
logic PENABLE = 1'b0;
|
||||
logic PWRITE = 1'b0;
|
||||
logic [31:0] PWDATA = '0;
|
||||
logic PREADY;
|
||||
logic [31:0] PRDATA;
|
||||
logic PSLVERR;
|
||||
logic trck_resetn = 1'b0;
|
||||
logic afe_resetn = 1'b0;
|
||||
logic trck_clk = 1'b0;
|
||||
logic afe_clk = 1'b0;
|
||||
logic [3:0] afe_data = '0;
|
||||
logic sample_align = 1'b0;
|
||||
logic [31:0] sample_counter = '0;
|
||||
logic gps_frame_ready;
|
||||
logic pps_capture;
|
||||
integer interrupt_count = 0;
|
||||
|
||||
always #5 PCLK = ~PCLK;
|
||||
always #4 trck_clk = ~trck_clk;
|
||||
always #8 afe_clk = ~afe_clk;
|
||||
always @(posedge PCLK)
|
||||
if(gps_frame_ready) interrupt_count++;
|
||||
|
||||
tracking_wrapper #(
|
||||
.LOW_ADDRESS(0),
|
||||
.HIGH_ADDRESS(4095),
|
||||
.ADDR_BUS_WIDTH(12)
|
||||
) dut (
|
||||
.PCLK, .PRESETN, .PADDR, .PSEL, .PENABLE, .PWRITE, .PWDATA,
|
||||
.PREADY, .PRDATA, .PSLVERR,
|
||||
.trck_resetn, .afe_resetn, .trck_clk, .afe_clk, .afe_data,
|
||||
.sample_align, .sample_counter, .gps_frame_ready, .pps_capture
|
||||
);
|
||||
|
||||
task automatic pclk_cycles(input int unsigned count);
|
||||
repeat(count) @(posedge PCLK);
|
||||
endtask
|
||||
|
||||
task automatic apb_write(input logic [11:0] address,
|
||||
input logic [31:0] data);
|
||||
@(negedge PCLK);
|
||||
PADDR = address;
|
||||
PWDATA = data;
|
||||
PWRITE = 1'b1;
|
||||
PSEL = 1'b1;
|
||||
PENABLE = 1'b0;
|
||||
@(posedge PCLK);
|
||||
#1;
|
||||
@(negedge PCLK);
|
||||
PENABLE = 1'b1;
|
||||
@(posedge PCLK);
|
||||
#1;
|
||||
@(negedge PCLK);
|
||||
PSEL = 1'b0;
|
||||
PENABLE = 1'b0;
|
||||
PWRITE = 1'b0;
|
||||
pclk_cycles(2);
|
||||
endtask
|
||||
|
||||
task automatic apb_read(input logic [11:0] address,
|
||||
output logic [31:0] data);
|
||||
@(negedge PCLK);
|
||||
PADDR = address;
|
||||
PWRITE = 1'b0;
|
||||
PSEL = 1'b1;
|
||||
PENABLE = 1'b0;
|
||||
@(posedge PCLK);
|
||||
#1;
|
||||
data = PRDATA;
|
||||
@(negedge PCLK);
|
||||
PSEL = 1'b0;
|
||||
pclk_cycles(2);
|
||||
endtask
|
||||
|
||||
task automatic expect_read(input logic [11:0] address,
|
||||
input logic [31:0] expected);
|
||||
logic [31:0] value;
|
||||
apb_read(address, value);
|
||||
if(value !== expected)
|
||||
$fatal(1, "APB read 0x%03x mismatch: got=0x%08x expected=0x%08x",
|
||||
address, value, expected);
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
force dut.measurement_valid = 1'b0;
|
||||
force dut.measurement_bit = 1'b1;
|
||||
force dut.measurement_abs_sample = 32'h89ab_cdef;
|
||||
force dut.measurement_rem_code_ph = REM_CODE_PH_I_WL'(555);
|
||||
force dut.measurement_carrier_freq_q7 = EXPECTED_CARRIER;
|
||||
force dut.measurement_cn0_sum_i = SUM_WL'(-12345);
|
||||
force dut.measurement_cn0_sum_q = SUM_WL'(6789);
|
||||
force dut.measurement_cn0_wbp = EXPECTED_WBP;
|
||||
force dut.measurement_cn0_sample_count = $clog2(MS_PER_BIT+1)'(20);
|
||||
force dut.measurement_sequence = 16'h55aa;
|
||||
force dut.lock = 1'b1;
|
||||
force dut.busy = 1'b1;
|
||||
force dut.bit_valid = 1'b0;
|
||||
|
||||
pclk_cycles(5);
|
||||
PRESETN = 1'b1;
|
||||
trck_resetn = 1'b1;
|
||||
afe_resetn = 1'b1;
|
||||
pclk_cycles(5);
|
||||
|
||||
apb_write(CONTROL_REG_OFFSET[11:0],
|
||||
(1 << TRCK_INTRPT_ENABLE_POS) |
|
||||
(1 << TRCK_MEASUREMENT_INTR_EN_POS) |
|
||||
(1 << TRCK_MEASUREMENT_FLAG_EN_POS));
|
||||
|
||||
@(negedge trck_clk);
|
||||
force dut.measurement_valid = 1'b1;
|
||||
pclk_cycles(12);
|
||||
if(interrupt_count !== 1)
|
||||
$fatal(1, "expected one measurement interrupt, got %0d", interrupt_count);
|
||||
|
||||
expect_read(MEASUREMENT_META_REG_OFFSET[11:0],
|
||||
{8'h01, 11'b0, 5'd20, 5'b0, 1'b1, 1'b1, 1'b1});
|
||||
expect_read(MEASUREMENT_SEQUENCE_REG_OFFSET[11:0], 32'h0000_55aa);
|
||||
expect_read(MEASUREMENT_CARRIER_REG_OFFSET[11:0],
|
||||
{{2{EXPECTED_CARRIER[29]}}, EXPECTED_CARRIER});
|
||||
expect_read(MEASUREMENT_ABS_SAMPLE_REG_OFFSET[11:0], 32'h89ab_cdef);
|
||||
expect_read(MEASUREMENT_REM_CODE_REG_OFFSET[11:0], 32'd555);
|
||||
expect_read(MEASUREMENT_SUM_I_REG_OFFSET[11:0], -32'sd12345);
|
||||
expect_read(MEASUREMENT_SUM_Q_REG_OFFSET[11:0], 32'd6789);
|
||||
expect_read(MEASUREMENT_WBP_LO_REG_OFFSET[11:0], EXPECTED_WBP[31:0]);
|
||||
expect_read(MEASUREMENT_WBP_HI_REG_OFFSET[11:0],
|
||||
{{(64-WBP_WL){1'b0}}, EXPECTED_WBP[WBP_WL-1:32]});
|
||||
|
||||
/* Source payload movement cannot tear the APB-domain snapshot. */
|
||||
force dut.measurement_carrier_freq_q7 =
|
||||
CARRIER_WL'((CENTER_FREQ - 4000) * 128);
|
||||
force dut.measurement_cn0_sum_i = SUM_WL'(22222);
|
||||
pclk_cycles(4);
|
||||
expect_read(MEASUREMENT_CARRIER_REG_OFFSET[11:0],
|
||||
{{2{EXPECTED_CARRIER[29]}}, EXPECTED_CARRIER});
|
||||
expect_read(MEASUREMENT_SUM_I_REG_OFFSET[11:0], -32'sd12345);
|
||||
|
||||
expect_read(STATUS_REG_OFFSET[11:0],
|
||||
(1 << TRCK_BUSY_POS) |
|
||||
(1 << TRCK_LOCKED_POS) |
|
||||
(1 << TRCK_INTR_PEND_POS) |
|
||||
(1 << TRCK_MEASUREMENT_FLAG_POS));
|
||||
apb_write(CONTROL_REG_OFFSET[11:0],
|
||||
(1 << TRCK_INTRPT_ENABLE_POS) |
|
||||
(1 << TRCK_INTRPT_CLEAR_POS) |
|
||||
(1 << TRCK_MEASUREMENT_INTR_EN_POS) |
|
||||
(1 << TRCK_MEASUREMENT_FLAG_EN_POS));
|
||||
expect_read(STATUS_REG_OFFSET[11:0],
|
||||
(1 << TRCK_BUSY_POS) | (1 << TRCK_LOCKED_POS));
|
||||
|
||||
$display("tracking_wrapper_measurement_tb PASS");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,142 @@
|
||||
import definitions::*;
|
||||
|
||||
/*
|
||||
* Builds one coherent tracking measurement for every decoded 20 ms NAV bit.
|
||||
*
|
||||
* The payload is registered in the tracking clock domain and remains unchanged
|
||||
* until the next measurement. measurement_valid_o remains asserted for the
|
||||
* rest of bit_valid_i so the APB clock can synchronize it without a CDC FIFO.
|
||||
*/
|
||||
module tracking_measurement #(
|
||||
parameter int unsigned CORR_WL = TRCK_CORR_WL,
|
||||
parameter int unsigned ENERGY_WL = (2 * TRCK_CORR_WL) + 1,
|
||||
parameter int unsigned SUM_WL = TRCK_CORR_WL + $clog2(MS_PER_BIT),
|
||||
parameter int unsigned WBP_WL = ENERGY_WL + $clog2(MS_PER_BIT),
|
||||
parameter int unsigned COUNT_WL = $clog2(MS_PER_BIT + 1),
|
||||
parameter int unsigned CARRIER_WL =
|
||||
ACQUIRED_FREQ_IL + TRCK_CARR_NCO_FREQ_FL,
|
||||
parameter int unsigned SNAPSHOT_DELAY_CYCLES = 4
|
||||
) (
|
||||
input logic clk,
|
||||
input logic rstn,
|
||||
input logic bit_valid_i,
|
||||
input logic nav_bit_i,
|
||||
input logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample_i,
|
||||
input logic [REM_CODE_PH_I_WL-1:0] rem_code_phase_i,
|
||||
input logic signed [CARRIER_WL-1:0] carrier_freq_q7_i,
|
||||
input logic corr_valid_i,
|
||||
input logic signed [CORR_WL-1:0] i_prompt_i,
|
||||
input logic signed [CORR_WL-1:0] q_prompt_i,
|
||||
input logic prompt_energy_valid_i,
|
||||
input logic [ENERGY_WL-1:0] prompt_energy_i,
|
||||
|
||||
output logic measurement_valid_o,
|
||||
output logic nav_bit_o,
|
||||
output logic [ABS_SAMPLE_CNTR_WL-1:0] absolute_sample_o,
|
||||
output logic [REM_CODE_PH_I_WL-1:0] rem_code_phase_o,
|
||||
output logic signed [CARRIER_WL-1:0] carrier_freq_q7_o,
|
||||
output logic signed [SUM_WL-1:0] cn0_sum_i_o,
|
||||
output logic signed [SUM_WL-1:0] cn0_sum_q_o,
|
||||
output logic [WBP_WL-1:0] cn0_wbp_o,
|
||||
output logic [COUNT_WL-1:0] cn0_sample_count_o,
|
||||
output logic [15:0] sequence_o
|
||||
);
|
||||
|
||||
localparam int unsigned DELAY_WL =
|
||||
(SNAPSHOT_DELAY_CYCLES < 2) ? 1 : $clog2(SNAPSHOT_DELAY_CYCLES + 1);
|
||||
|
||||
logic bit_valid_r;
|
||||
logic measurement_armed;
|
||||
logic snapshot_pending;
|
||||
logic [DELAY_WL-1:0] snapshot_delay;
|
||||
logic signed [SUM_WL-1:0] sum_i;
|
||||
logic signed [SUM_WL-1:0] sum_q;
|
||||
logic [WBP_WL-1:0] wbp;
|
||||
logic [COUNT_WL-1:0] sample_count;
|
||||
|
||||
wire bit_epoch = bit_valid_i && !bit_valid_r;
|
||||
|
||||
always_ff @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
bit_valid_r <= 1'b0;
|
||||
measurement_armed <= 1'b0;
|
||||
snapshot_pending <= 1'b0;
|
||||
snapshot_delay <= '0;
|
||||
sum_i <= '0;
|
||||
sum_q <= '0;
|
||||
wbp <= '0;
|
||||
sample_count <= '0;
|
||||
measurement_valid_o <= 1'b0;
|
||||
nav_bit_o <= 1'b0;
|
||||
absolute_sample_o <= '0;
|
||||
rem_code_phase_o <= '0;
|
||||
carrier_freq_q7_o <= '0;
|
||||
cn0_sum_i_o <= '0;
|
||||
cn0_sum_q_o <= '0;
|
||||
cn0_wbp_o <= '0;
|
||||
cn0_sample_count_o <= '0;
|
||||
sequence_o <= '0;
|
||||
end else begin
|
||||
bit_valid_r <= bit_valid_i;
|
||||
|
||||
if(!bit_valid_i) begin
|
||||
measurement_valid_o <= 1'b0;
|
||||
end
|
||||
|
||||
if(measurement_armed && corr_valid_i) begin
|
||||
sum_i <= sum_i + $signed(i_prompt_i);
|
||||
sum_q <= sum_q + $signed(q_prompt_i);
|
||||
sample_count <= sample_count + 1'b1;
|
||||
end
|
||||
if(measurement_armed && prompt_energy_valid_i) begin
|
||||
wbp <= wbp + prompt_energy_i;
|
||||
end
|
||||
|
||||
/*
|
||||
* The first synchronized NAV-bit edge defines the integration phase.
|
||||
* Samples gathered before that edge are intentionally discarded.
|
||||
*/
|
||||
if(bit_epoch) begin
|
||||
if(!measurement_armed) begin
|
||||
measurement_armed <= 1'b1;
|
||||
sum_i <= '0;
|
||||
sum_q <= '0;
|
||||
wbp <= '0;
|
||||
sample_count <= '0;
|
||||
end else begin
|
||||
snapshot_pending <= 1'b1;
|
||||
snapshot_delay <= DELAY_WL'(SNAPSHOT_DELAY_CYCLES);
|
||||
end
|
||||
end
|
||||
|
||||
/*
|
||||
* Prompt energy is pipelined in lock_detector. There is almost one
|
||||
* millisecond before the next correlator result, so waiting a few
|
||||
* tracking clocks includes the last energy term without mixing epochs.
|
||||
*/
|
||||
if(snapshot_pending) begin
|
||||
if(snapshot_delay != 0) begin
|
||||
snapshot_delay <= snapshot_delay - 1'b1;
|
||||
end else begin
|
||||
snapshot_pending <= 1'b0;
|
||||
measurement_valid_o <= 1'b1;
|
||||
nav_bit_o <= nav_bit_i;
|
||||
absolute_sample_o <= absolute_sample_i;
|
||||
rem_code_phase_o <= rem_code_phase_i;
|
||||
carrier_freq_q7_o <= carrier_freq_q7_i;
|
||||
cn0_sum_i_o <= sum_i;
|
||||
cn0_sum_q_o <= sum_q;
|
||||
cn0_wbp_o <= wbp;
|
||||
cn0_sample_count_o <= sample_count;
|
||||
sequence_o <= sequence_o + 1'b1;
|
||||
|
||||
sum_i <= '0;
|
||||
sum_q <= '0;
|
||||
wbp <= '0;
|
||||
sample_count <= '0;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -34,6 +34,27 @@ module trck_top
|
||||
output logic bit_vld_o,
|
||||
output logic [ABS_SAMPLE_CNTR_WL-1 : 0] abs_sample_o,
|
||||
output logic [REM_CODE_PH_I_WL-1 : 0] rem_code_ph_o,
|
||||
|
||||
// Coherent 20 ms tracking measurement (held until the next NAV bit)
|
||||
output logic measurement_vld_o,
|
||||
output logic measurement_bit_o,
|
||||
output logic [ABS_SAMPLE_CNTR_WL-1 : 0] measurement_abs_sample_o,
|
||||
output logic [REM_CODE_PH_I_WL-1 : 0] measurement_rem_code_ph_o,
|
||||
output logic signed [
|
||||
ACQUIRED_FREQ_IL+TRCK_CARR_NCO_FREQ_FL-1 : 0
|
||||
] measurement_carrier_freq_q7_o,
|
||||
output logic signed [
|
||||
TRCK_CORR_WL+$clog2(MS_PER_BIT)-1 : 0
|
||||
] measurement_cn0_sum_i_o,
|
||||
output logic signed [
|
||||
TRCK_CORR_WL+$clog2(MS_PER_BIT)-1 : 0
|
||||
] measurement_cn0_sum_q_o,
|
||||
output logic [
|
||||
(2*TRCK_CORR_WL)+1+$clog2(MS_PER_BIT)-1 : 0
|
||||
] measurement_cn0_wbp_o,
|
||||
output logic [$clog2(MS_PER_BIT+1)-1 : 0]
|
||||
measurement_cn0_sample_count_o,
|
||||
output logic [15:0] measurement_sequence_o,
|
||||
|
||||
// Status signals
|
||||
output logic fifo_of_o,
|
||||
@@ -77,6 +98,8 @@ logic ca_l, ca_p, ca_e;
|
||||
|
||||
logic corr_vld;
|
||||
logic signed [TRCK_CORR_WL-TRCK_CORR_FL-1 : -TRCK_CORR_FL] i_e, i_p, i_l, q_e, q_p, q_l;
|
||||
logic prompt_energy_vld;
|
||||
logic [(2*TRCK_CORR_WL):0] prompt_energy;
|
||||
|
||||
logic signed [RF_DATA_WIDTH-1 : 0] rf_data;
|
||||
|
||||
@@ -223,7 +246,8 @@ Disc_PLL_i (
|
||||
.y_i (q_p),
|
||||
|
||||
.vld_o (pll_error_vld),
|
||||
.angle_o (pll_error)
|
||||
.angle_o (pll_error),
|
||||
.vec_len_o()
|
||||
);
|
||||
|
||||
code_loop_discriminator #(
|
||||
@@ -259,6 +283,8 @@ lock_detector_i (
|
||||
.corr_vld_i (corr_vld),
|
||||
.i_p_i (i_p),
|
||||
.q_p_i (q_p),
|
||||
.prompt_energy_vld_o(prompt_energy_vld),
|
||||
.prompt_energy_o(prompt_energy),
|
||||
.lock_o (lock_o)
|
||||
);
|
||||
|
||||
@@ -276,6 +302,31 @@ bit_gen bit_gen_i (
|
||||
.rem_code_ph_o (rem_code_ph_o)
|
||||
);
|
||||
|
||||
tracking_measurement tracking_measurement_i (
|
||||
.clk (trk_clk),
|
||||
.rstn (rstn),
|
||||
.bit_valid_i (bit_vld_o),
|
||||
.nav_bit_i (bit_o),
|
||||
.absolute_sample_i (abs_sample_o),
|
||||
.rem_code_phase_i (rem_code_ph_o),
|
||||
.carrier_freq_q7_i (carr_freq_sum_p),
|
||||
.corr_valid_i (corr_vld),
|
||||
.i_prompt_i (i_p),
|
||||
.q_prompt_i (q_p),
|
||||
.prompt_energy_valid_i (prompt_energy_vld),
|
||||
.prompt_energy_i (prompt_energy),
|
||||
.measurement_valid_o (measurement_vld_o),
|
||||
.nav_bit_o (measurement_bit_o),
|
||||
.absolute_sample_o (measurement_abs_sample_o),
|
||||
.rem_code_phase_o (measurement_rem_code_ph_o),
|
||||
.carrier_freq_q7_o (measurement_carrier_freq_q7_o),
|
||||
.cn0_sum_i_o (measurement_cn0_sum_i_o),
|
||||
.cn0_sum_q_o (measurement_cn0_sum_q_o),
|
||||
.cn0_wbp_o (measurement_cn0_wbp_o),
|
||||
.cn0_sample_count_o (measurement_cn0_sample_count_o),
|
||||
.sequence_o (measurement_sequence_o)
|
||||
);
|
||||
|
||||
//freq valid
|
||||
always_ff @(posedge trk_clk or negedge rstn) begin
|
||||
if (!rstn) begin
|
||||
|
||||
@@ -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}]
|
||||
@@ -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
|
||||
@@ -18,9 +18,9 @@ JDK_SHA256="d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331"
|
||||
MILL_URL="https://github.com/com-lihaoyi/mill/releases/download/$MILL_VERSION/$MILL_VERSION"
|
||||
MILL_SHA256="ebe83649cd63acd33608f6d14452c074be2ed7d00cd2f69f88573d3f6909774f"
|
||||
|
||||
CIRCT_ARCHIVE="circt-full-shared-linux-x64.tar.gz"
|
||||
CIRCT_ARCHIVE="firrtl-bin-linux-x64.tar.gz"
|
||||
CIRCT_URL="https://github.com/llvm/circt/releases/download/firtool-$FIRTOOL_VERSION/$CIRCT_ARCHIVE"
|
||||
CIRCT_SHA256="dde442dcd0ada92e6f7db26f4744667471f7be61d10f2876297efb435fc9a5ec"
|
||||
CIRCT_SHA256="10bf9b135a65e5d93fb0555e967eac4258400ee629731ba68a69be6b7f9781ce"
|
||||
|
||||
Z3_ARCHIVE="z3-$Z3_VERSION-x64-glibc-2.39.zip"
|
||||
Z3_URL="https://github.com/Z3Prover/z3/releases/download/z3-$Z3_VERSION/$Z3_ARCHIVE"
|
||||
@@ -85,7 +85,8 @@ if [[ ! -x "$TOOLS_ROOT/mill/$MILL_VERSION/mill" ]]; then
|
||||
"${SUDO[@]}" install -m 0755 "$work_dir/mill" "$TOOLS_ROOT/mill/$MILL_VERSION/mill"
|
||||
fi
|
||||
|
||||
if [[ ! -x "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" ]]; then
|
||||
if ! "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" --version \
|
||||
>/dev/null 2>&1; then
|
||||
download "$CIRCT_URL" "$work_dir/$CIRCT_ARCHIVE" "$CIRCT_SHA256"
|
||||
mkdir -p "$work_dir/circt"
|
||||
tar -xzf "$work_dir/$CIRCT_ARCHIVE" -C "$work_dir/circt"
|
||||
|
||||
@@ -10,10 +10,15 @@ include $(REPO_DIR)/tools/project.mk
|
||||
APP_FLASH_OFFSET ?= 0x06000000
|
||||
APP ?= hello_uart
|
||||
GPS_DEBUG ?= 1
|
||||
GPS_DEBUG_LEVEL ?= 4
|
||||
GPS_DEBUG_LEVEL ?= 1
|
||||
GPS_ESKF_ENABLE ?= 1
|
||||
IMU_SENSOR ?= auto
|
||||
BSP_UART_CLOCK_HZ ?= 100000000
|
||||
BSP_UART_BAUDRATE ?= 115200
|
||||
BSP_UART_BAUDRATE ?= 460800
|
||||
GPS_SYSTEM_UART_BAUDRATE ?= 460800
|
||||
BSP_CLINT_TIMEBASE_HZ ?= 1000000
|
||||
GPS_SYSTEM_CLINT_TIMEBASE_HZ ?= 500000
|
||||
GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES ?= 5
|
||||
# BSP_DTIM_* mirrors the FPGA DTIM_* knobs and is used by C code plus linker scripts.
|
||||
BSP_DTIM_BASE ?= 0x80000000
|
||||
BSP_DTIM_SIZE ?= 0x00100000
|
||||
@@ -38,7 +43,12 @@ help:
|
||||
@echo " APP_FLASH_OFFSET=$(APP_FLASH_OFFSET)"
|
||||
@echo " BSP_UART_CLOCK_HZ=$(BSP_UART_CLOCK_HZ)"
|
||||
@echo " BSP_UART_BAUDRATE=$(BSP_UART_BAUDRATE)"
|
||||
@echo " GPS_SYSTEM_UART_BAUDRATE=$(GPS_SYSTEM_UART_BAUDRATE)"
|
||||
@echo " GPS_ESKF_ENABLE=$(GPS_ESKF_ENABLE)"
|
||||
@echo " IMU_SENSOR=$(IMU_SENSOR)"
|
||||
@echo " BSP_CLINT_TIMEBASE_HZ=$(BSP_CLINT_TIMEBASE_HZ)"
|
||||
@echo " GPS_SYSTEM_CLINT_TIMEBASE_HZ=$(GPS_SYSTEM_CLINT_TIMEBASE_HZ)"
|
||||
@echo " GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES=$(GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES)"
|
||||
@echo " BSP_DTIM_BASE=$(BSP_DTIM_BASE)"
|
||||
@echo " BSP_DTIM_SIZE=$(BSP_DTIM_SIZE)"
|
||||
|
||||
@@ -57,11 +67,11 @@ steval-mki217v1-test:
|
||||
@make -C steval_mki217v1_test TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/steval_mki217v1_test" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
|
||||
|
||||
gps-system:
|
||||
@make -C gps_system TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/gps_system" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
|
||||
@make -C gps_system TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/gps_system" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(GPS_SYSTEM_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(GPS_SYSTEM_CLINT_TIMEBASE_HZ)" IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES="$(GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
|
||||
|
||||
app:
|
||||
@test -d "$(APP)"
|
||||
@make -C "$(APP)" TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/$(APP)" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
|
||||
@make -C "$(APP)" TOOLCHAIN="$(TOOLCHAIN)" BUILD_DIR="$(OUT_DIR)/$(APP)" GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" BSP_DTIM_BASE="$(BSP_DTIM_BASE)" BSP_DTIM_SIZE="$(BSP_DTIM_SIZE)" all
|
||||
|
||||
flash-image: app
|
||||
@mkdir -p "$(FLASH_DIR)"
|
||||
|
||||
@@ -13,7 +13,7 @@ APP_FLASH_OFFSET ?= 0x06000000
|
||||
MARCH ?= rv64imafdc_zicsr_zifencei
|
||||
MABI ?= lp64d
|
||||
BSP_UART_CLOCK_HZ ?= 100000000
|
||||
BSP_UART_BAUDRATE ?= 115200
|
||||
BSP_UART_BAUDRATE ?= 460800
|
||||
BSP_DTIM_BASE ?= 0x80000000
|
||||
BSP_DTIM_SIZE ?= 0x00100000
|
||||
CONFIG_STAMP := $(BUILD_DIR)/build_config.txt
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef BSP_UART_BAUDRATE
|
||||
#define BSP_UART_BAUDRATE 115200UL
|
||||
#define BSP_UART_BAUDRATE 460800UL
|
||||
#endif
|
||||
|
||||
void bsp_init(void)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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ı:
|
||||
@@ -338,7 +454,8 @@ Bu nedenle sorumluluk iki katmana ayrilir:
|
||||
olup olmadigini izler ve gerekli recovery/reset kararini burada verir.
|
||||
|
||||
Mevcut yalın recovery, son başarılı position fusion'dan sonra yedi saniyelik
|
||||
timeout dolmuşsa ve en az üç rejected position fix'i kendi arasında zamansal
|
||||
timeout dolmuşsa, platform harici rest bilgisi ve IMU/GNSS hareket vetolarıyla
|
||||
duragansa ve en az üç rejected position fix'i kendi arasında zamansal
|
||||
tutarlıysa yalnız position state ve position covariance'ini GNSS'e yeniden
|
||||
ankrajlar. Position cross-covariance'lari gecersiz korelasyon tasimamasi icin
|
||||
sifirlanir; velocity, attitude ve IMU bias state'leri korunur. Hard NIS gate
|
||||
@@ -368,9 +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
|
||||
@@ -545,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:
|
||||
|
||||
@@ -569,6 +690,13 @@ make profile
|
||||
- sekiz ayrı deterministic noise/timing seed'i;
|
||||
- 30 s boyunca 500 Hz stationary IMU ve gecikmiş 1 Hz GPS.
|
||||
|
||||
`gps_failure_scenario_test.c`, gerçek `gps_live_5min_20260713.csv` kaydındaki
|
||||
3.297 Hz ölçüm hızı, 300 ms medyan periyot, 1.803 s maksimum boşluk ve yaklaşık
|
||||
8.6 m yatay RMS saçılım ölçeğini kullanır. Normal akışın yanında hareket
|
||||
sırasında 12 s tam kesinti, yüzde 35 rastgele kayıp ve 1.8 s boşluklar, gecikmiş
|
||||
ölçümlerin toplu dönüşü ve kesinti sonrasında tutarlı fakat 80/-50/30 m hatalı
|
||||
üç fix senaryoları regression testidir.
|
||||
|
||||
### Sentetik hareketli senaryo
|
||||
|
||||
Hareketli senaryo 200 Hz IMU ile 60 s sürer. Ground-truth profil şöyledir:
|
||||
@@ -628,16 +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
|
||||
@@ -688,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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -10,7 +10,7 @@ IMU_PROFILE_SOURCE := ../../gps_system/src/adapters/imu_fusion_profile.c
|
||||
|
||||
.PHONY: all test gps-system-test scenario-logs sanitize profile benchmark clean
|
||||
|
||||
all: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test
|
||||
all: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test gps_failure_scenario_test
|
||||
|
||||
standalone_gps_integrity_test: standalone_gps_integrity_test.c $(GPS_TIME_SOURCE) $(GPS_EPOCH_SOURCE)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||
@@ -39,7 +39,10 @@ gps_reacquisition_test: gps_reacquisition_test.c $(ESKF_SOURCES)
|
||||
platform_scenario_matrix_test: platform_scenario_matrix_test.c $(ESKF_SOURCES)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||
|
||||
test: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test
|
||||
gps_failure_scenario_test: gps_failure_scenario_test.c $(ESKF_SOURCES)
|
||||
$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
|
||||
|
||||
test: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test fusion_synthetic_test imu_quality_synthetic_test async_tilt_recovery_test async_rate_matrix_test gps_reacquisition_test platform_scenario_matrix_test gps_failure_scenario_test
|
||||
./standalone_gps_integrity_test
|
||||
./eskf_motion_test
|
||||
./fusion_rest_detector_test
|
||||
@@ -49,6 +52,7 @@ test: standalone_gps_integrity_test eskf_motion_test fusion_rest_detector_test f
|
||||
./async_rate_matrix_test
|
||||
./gps_reacquisition_test
|
||||
./platform_scenario_matrix_test
|
||||
./gps_failure_scenario_test
|
||||
|
||||
gps-system-test: standalone_gps_integrity_test
|
||||
./standalone_gps_integrity_test
|
||||
@@ -122,6 +126,12 @@ sanitize:
|
||||
-o platform_scenario_matrix_test_san $(LDLIBS)
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./platform_scenario_matrix_test_san
|
||||
$(CC) -std=c11 -O1 -g -Wall -Wextra -Wpedantic -Werror \
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-I../../gps_system/header gps_failure_scenario_test.c $(ESKF_SOURCES) \
|
||||
-o gps_failure_scenario_test_san $(LDLIBS)
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
|
||||
UBSAN_OPTIONS=halt_on_error=1 ./gps_failure_scenario_test_san
|
||||
|
||||
profile:
|
||||
$(CC) -std=c11 -O2 -g -pg -Wall -Wextra -Wpedantic \
|
||||
@@ -136,4 +146,4 @@ benchmark: standalone_gps_integrity_test
|
||||
./standalone_gps_integrity_test --bench-double 100000
|
||||
|
||||
clean:
|
||||
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_rest_detector_test fusion_rest_detector_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san async_tilt_recovery_test async_tilt_recovery_test_san async_rate_matrix_test async_rate_matrix_test_san gps_reacquisition_test gps_reacquisition_test_san platform_scenario_matrix_test platform_scenario_matrix_test_san gmon.out
|
||||
rm -f standalone_gps_integrity_test standalone_gps_integrity_test_san standalone_gps_integrity_test_gprof eskf_motion_test eskf_motion_test_san fusion_rest_detector_test fusion_rest_detector_test_san fusion_synthetic_test fusion_synthetic_test_san fusion_synthetic_test_gprof fusion_synthetic_trace imu_quality_synthetic_test imu_quality_synthetic_test_san async_tilt_recovery_test async_tilt_recovery_test_san async_rate_matrix_test async_rate_matrix_test_san gps_reacquisition_test gps_reacquisition_test_san platform_scenario_matrix_test platform_scenario_matrix_test_san gps_failure_scenario_test gps_failure_scenario_test_san gmon.out
|
||||
|
||||
@@ -119,6 +119,51 @@ static void test_zero_update_primitives(void)
|
||||
}
|
||||
}
|
||||
|
||||
static double quaternion_yaw(const double quaternion[4])
|
||||
{
|
||||
return atan2(
|
||||
2.0 * (quaternion[0] * quaternion[3] +
|
||||
quaternion[1] * quaternion[2]),
|
||||
1.0 - 2.0 * (quaternion[2] * quaternion[2] +
|
||||
quaternion[3] * quaternion[3]));
|
||||
}
|
||||
|
||||
static void test_external_rest_cannot_erase_constant_turn(void)
|
||||
{
|
||||
fusion_engine_config_t config;
|
||||
fusion_engine_t engine;
|
||||
fusion_engine_default_config(&config, 1000000u);
|
||||
CHECK(fusion_engine_init(&engine, &config),
|
||||
"constant-turn engine initialization");
|
||||
|
||||
for(unsigned int sample = 0u ; sample <= 8u * 52u ; sample++)
|
||||
{
|
||||
uint64_t ticks = ((uint64_t)sample * 1000000u + 26u) / 52u;
|
||||
double time_s = (double)ticks / 1000000.0;
|
||||
fusion_imu_measurement_t imu;
|
||||
memset(&imu, 0, sizeof(imu));
|
||||
imu.timestamp_ticks = ticks;
|
||||
imu.accel_mps2[2] = config.eskf.gravity_mps2;
|
||||
imu.gyro_rps[2] =
|
||||
(time_s >= 3.0) ? 20.0 * TEST_PI / 180.0 : 0.0;
|
||||
imu.temperature_c = 25.0;
|
||||
/* Models a stale/incorrect external landed flag. Inertial motion must
|
||||
* still veto ZUPT and zero-gyro fusion. */
|
||||
imu.vehicle_at_rest = true;
|
||||
CHECK(fusion_engine_push_imu(&engine, &imu),
|
||||
"constant-turn IMU accepted");
|
||||
}
|
||||
CHECK(engine.state == FUSION_ENGINE_RUNNING,
|
||||
"constant-turn engine remains running");
|
||||
double yaw_deg = quaternion_yaw(
|
||||
engine.current.filter.body_to_nav_quaternion) * 180.0 / TEST_PI;
|
||||
CHECK(fabs(yaw_deg - 100.0) < 1.0,
|
||||
"stale external rest flag cannot erase a real constant turn");
|
||||
CHECK(fabs(engine.current.filter.gyro_bias_rps[2]) <
|
||||
0.5 * TEST_PI / 180.0,
|
||||
"constant angular rate is not learned as gyro bias");
|
||||
}
|
||||
|
||||
static void test_gravity_alignment(void)
|
||||
{
|
||||
eskf_t filter;
|
||||
@@ -186,6 +231,81 @@ static void test_position_nis_gate(void)
|
||||
"GPS accept/reject counters");
|
||||
}
|
||||
|
||||
static void test_velocity_nis_gate(void)
|
||||
{
|
||||
eskf_t filter;
|
||||
eskf_config_t config;
|
||||
identity_filter(&filter, &config);
|
||||
double covariance[9] = {
|
||||
1.0, 0.1, 0.0,
|
||||
0.1, 1.5, 0.0,
|
||||
0.0, 0.0, 2.0
|
||||
};
|
||||
double normal_velocity[3] = {1.0, -0.5, 0.2};
|
||||
double nis = NAN;
|
||||
CHECK(eskf_velocity_update(&filter, &config, normal_velocity,
|
||||
covariance, &nis) == ESKF_UPDATE_ACCEPTED,
|
||||
"normal Doppler velocity accepted");
|
||||
CHECK(isfinite(nis) && (nis < config.velocity_nis_gate),
|
||||
"accepted Doppler velocity has finite in-gate NIS");
|
||||
CHECK((filter.velocity_mps[0] > 0.0) &&
|
||||
(filter.velocity_mps[1] < 0.0) &&
|
||||
(filter.velocity_mps[2] > 0.0),
|
||||
"Doppler update corrects all ESKF velocity axes");
|
||||
double before[3];
|
||||
memcpy(before, filter.velocity_mps, sizeof(before));
|
||||
double outlier[3] = {100.0, -80.0, 50.0};
|
||||
CHECK(eskf_velocity_update(&filter, &config, outlier,
|
||||
covariance, &nis) == ESKF_UPDATE_REJECTED_NIS,
|
||||
"large Doppler velocity outlier rejected by NIS");
|
||||
CHECK(nearly_equal(before[0], filter.velocity_mps[0], 1.0e-12) &&
|
||||
nearly_equal(before[1], filter.velocity_mps[1], 1.0e-12) &&
|
||||
nearly_equal(before[2], filter.velocity_mps[2], 1.0e-12),
|
||||
"velocity NIS rejection leaves nominal state unchanged");
|
||||
CHECK(filter.velocity_update_accepted_count == 1u &&
|
||||
filter.velocity_update_rejected_count == 1u,
|
||||
"Doppler velocity accept/reject counters");
|
||||
}
|
||||
|
||||
static double run_accel_bias_with_optional_doppler(bool fuse_doppler)
|
||||
{
|
||||
eskf_t filter;
|
||||
eskf_config_t config;
|
||||
identity_filter(&filter, &config);
|
||||
const double velocity_covariance[9] = {
|
||||
4.0, 0.0, 0.0,
|
||||
0.0, 4.0, 0.0,
|
||||
0.0, 0.0, 9.0
|
||||
};
|
||||
const double stationary_velocity[3] = {0.0, 0.0, 0.0};
|
||||
for(unsigned int sample = 1u; sample <= 1000u; sample++)
|
||||
{
|
||||
/* Unmodelled 0.05 m/s2 x-axis accelerometer bias. */
|
||||
double accel[3] = {0.05, 0.0, config.gravity_mps2};
|
||||
double gyro[3] = {0.0, 0.0, 0.0};
|
||||
CHECK(eskf_propagate(&filter, &config, accel, gyro, 0.01),
|
||||
"accelerometer-bias profile propagation");
|
||||
if(fuse_doppler && ((sample % 20u) == 0u))
|
||||
{
|
||||
CHECK(eskf_velocity_update(
|
||||
&filter, &config, stationary_velocity,
|
||||
velocity_covariance, NULL) == ESKF_UPDATE_ACCEPTED,
|
||||
"5 Hz conservative Doppler velocity update");
|
||||
}
|
||||
}
|
||||
return eskf_speed_mps(&filter);
|
||||
}
|
||||
|
||||
static void test_doppler_velocity_limits_inertial_drift(void)
|
||||
{
|
||||
double unaided_speed = run_accel_bias_with_optional_doppler(false);
|
||||
double doppler_aided_speed = run_accel_bias_with_optional_doppler(true);
|
||||
CHECK(unaided_speed > 0.45,
|
||||
"unmodelled accelerometer bias creates expected inertial speed drift");
|
||||
CHECK(doppler_aided_speed < 0.25 * unaided_speed,
|
||||
"conservative Doppler velocity materially limits inertial speed drift");
|
||||
}
|
||||
|
||||
static double profile_acceleration(double time_s)
|
||||
{
|
||||
if((time_s >= 2.0) && (time_s < 4.0))
|
||||
@@ -534,6 +654,21 @@ static fusion_position_measurement_t replay_position(uint64_t tick, double east_
|
||||
return position;
|
||||
}
|
||||
|
||||
static fusion_navigation_measurement_t replay_navigation(
|
||||
uint64_t tick,
|
||||
double east_m,
|
||||
double east_velocity_mps)
|
||||
{
|
||||
fusion_navigation_measurement_t navigation = {0};
|
||||
navigation.position = replay_position(tick, east_m);
|
||||
navigation.velocity_enu_mps[0] = east_velocity_mps;
|
||||
navigation.velocity_covariance_enu_mps2[0] = 0.25;
|
||||
navigation.velocity_covariance_enu_mps2[4] = 0.25;
|
||||
navigation.velocity_covariance_enu_mps2[8] = 1.0;
|
||||
navigation.velocity_valid = true;
|
||||
return navigation;
|
||||
}
|
||||
|
||||
static void check_runtime_close(const fusion_runtime_t* first,
|
||||
const fusion_runtime_t* second,
|
||||
double tolerance)
|
||||
@@ -576,28 +711,35 @@ static void test_delayed_position_replay(void)
|
||||
static fusion_engine_t delayed;
|
||||
initialize_engine_for_replay(&on_time);
|
||||
initialize_engine_for_replay(&delayed);
|
||||
fusion_position_measurement_t gps_at_1500 = replay_position(1500u, 0.125);
|
||||
fusion_navigation_measurement_t gps_at_1500 =
|
||||
replay_navigation(1500u, 0.125, 0.5);
|
||||
for(uint64_t tick = 110u ; tick <= 3200u ; tick += 10u)
|
||||
{
|
||||
push_replay_imu(&on_time, tick);
|
||||
push_replay_imu(&delayed, tick);
|
||||
if(tick == 1500u)
|
||||
{
|
||||
eskf_update_result_t result = ESKF_UPDATE_INVALID;
|
||||
CHECK(fusion_engine_push_position(&on_time, &gps_at_1500,
|
||||
&result, NULL),
|
||||
"on-time position update processed");
|
||||
CHECK(result == ESKF_UPDATE_ACCEPTED,
|
||||
"on-time position update accepted");
|
||||
eskf_update_result_t position_result = ESKF_UPDATE_INVALID;
|
||||
eskf_update_result_t velocity_result = ESKF_UPDATE_INVALID;
|
||||
CHECK(fusion_engine_push_navigation(
|
||||
&on_time, &gps_at_1500,
|
||||
&position_result, NULL, &velocity_result, NULL),
|
||||
"on-time position/velocity update processed");
|
||||
CHECK((position_result == ESKF_UPDATE_ACCEPTED) &&
|
||||
(velocity_result == ESKF_UPDATE_ACCEPTED),
|
||||
"on-time position and Doppler velocity accepted");
|
||||
}
|
||||
if(tick == 1800u)
|
||||
{
|
||||
eskf_update_result_t result = ESKF_UPDATE_INVALID;
|
||||
CHECK(fusion_engine_push_position(&delayed, &gps_at_1500,
|
||||
&result, NULL),
|
||||
"300 ms delayed position update replayed");
|
||||
CHECK(result == ESKF_UPDATE_ACCEPTED,
|
||||
"delayed position update accepted at measurement epoch");
|
||||
eskf_update_result_t position_result = ESKF_UPDATE_INVALID;
|
||||
eskf_update_result_t velocity_result = ESKF_UPDATE_INVALID;
|
||||
CHECK(fusion_engine_push_navigation(
|
||||
&delayed, &gps_at_1500,
|
||||
&position_result, NULL, &velocity_result, NULL),
|
||||
"300 ms delayed position/velocity update replayed");
|
||||
CHECK((position_result == ESKF_UPDATE_ACCEPTED) &&
|
||||
(velocity_result == ESKF_UPDATE_ACCEPTED),
|
||||
"delayed position and velocity accepted at measurement epoch");
|
||||
check_runtime_close(&on_time.current, &delayed.current, 1.0e-11);
|
||||
}
|
||||
}
|
||||
@@ -606,6 +748,8 @@ static void test_delayed_position_replay(void)
|
||||
"one delayed position caused one fixed-lag replay");
|
||||
CHECK(delayed.diagnostics.replayed_imu_count > 0u,
|
||||
"delayed position replayed stored IMU samples");
|
||||
CHECK(delayed.diagnostics.velocity_accepted == 1u,
|
||||
"delayed Doppler velocity is diagnosed as fused");
|
||||
|
||||
fusion_position_measurement_t future = replay_position(3500u, 1.0);
|
||||
CHECK(fusion_engine_push_position(&delayed, &future, NULL, NULL),
|
||||
@@ -682,6 +826,35 @@ static void test_gps_adapter(void)
|
||||
invalid.covariance_enu_m2[0][0] = -1.0;
|
||||
CHECK(!fusion_gps_adapter_convert(&adapter, &invalid, &local),
|
||||
"invalid GPS covariance is rejected before ESKF");
|
||||
|
||||
fusion_gps_adapter_t velocity_adapter;
|
||||
CHECK(fusion_gps_adapter_init(&velocity_adapter, &config),
|
||||
"velocity GPS adapter initialization");
|
||||
fusion_gps_measurement_t velocity_gps = adapter_gps(0.0, 0.0, 0.0);
|
||||
velocity_gps.velocity_valid = true;
|
||||
velocity_gps.velocity_ecef_mps[0] = 3.0;
|
||||
velocity_gps.velocity_ecef_mps[1] = 1.0;
|
||||
velocity_gps.velocity_ecef_mps[2] = -2.0;
|
||||
velocity_gps.velocity_covariance_ecef_mps2[0][0] = 16.0;
|
||||
velocity_gps.velocity_covariance_ecef_mps2[1][1] = 25.0;
|
||||
velocity_gps.velocity_covariance_ecef_mps2[2][2] = 36.0;
|
||||
fusion_navigation_measurement_t navigation;
|
||||
CHECK(fusion_gps_adapter_convert_navigation(
|
||||
&velocity_adapter, &velocity_gps, &navigation),
|
||||
"GPS adapter converts position and Doppler velocity");
|
||||
CHECK(navigation.velocity_valid,
|
||||
"finite Doppler velocity covariance remains valid");
|
||||
CHECK(nearly_equal(navigation.velocity_enu_mps[0], 1.0, 1.0e-12) &&
|
||||
nearly_equal(navigation.velocity_enu_mps[1], -2.0, 1.0e-12) &&
|
||||
nearly_equal(navigation.velocity_enu_mps[2], 3.0, 1.0e-12),
|
||||
"ECEF Doppler velocity rotates to expected ENU axes");
|
||||
CHECK(nearly_equal(
|
||||
navigation.velocity_covariance_enu_mps2[0], 25.0, 1.0e-12) &&
|
||||
nearly_equal(
|
||||
navigation.velocity_covariance_enu_mps2[4], 36.0, 1.0e-12) &&
|
||||
nearly_equal(
|
||||
navigation.velocity_covariance_enu_mps2[8], 16.0, 1.0e-12),
|
||||
"ECEF Doppler covariance rotates to ENU");
|
||||
}
|
||||
|
||||
static void test_measurement_bus(void)
|
||||
@@ -747,8 +920,11 @@ int main(void)
|
||||
{
|
||||
test_stationary_propagation();
|
||||
test_zero_update_primitives();
|
||||
test_external_rest_cannot_erase_constant_turn();
|
||||
test_gravity_alignment();
|
||||
test_position_nis_gate();
|
||||
test_velocity_nis_gate();
|
||||
test_doppler_velocity_limits_inertial_drift();
|
||||
test_motion_transitions();
|
||||
test_variable_rates();
|
||||
test_irregular_imu_timestamps();
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
#include "fusion/fusion_engine.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TIMEBASE_HZ 1000000u
|
||||
#define IMU_RATE_HZ 52u
|
||||
#define GPS_PERIOD_TICKS 300000u
|
||||
#define MAX_GPS_EVENTS 256u
|
||||
#define PI 3.14159265358979323846
|
||||
|
||||
/*
|
||||
* Timing/noise scale comes from logs/gps_live_5min_20260713.csv:
|
||||
* 988 fixes in 299.697 s (3.297 Hz), 300 ms median interval, 1.803 s maximum
|
||||
* interval, and about 8.6 m stationary horizontal RMS scatter. The 6 m
|
||||
* per-axis covariance below reproduces that horizontal RMS scale.
|
||||
*/
|
||||
typedef enum {
|
||||
SCENARIO_NORMAL = 0,
|
||||
SCENARIO_MOVING_OUTAGE,
|
||||
SCENARIO_INTERMITTENT,
|
||||
SCENARIO_BURST_RETURN,
|
||||
SCENARIO_BAD_RESTART
|
||||
} scenario_kind_t;
|
||||
|
||||
typedef struct {
|
||||
fusion_position_measurement_t measurement;
|
||||
uint64_t delivery_ticks;
|
||||
} gps_event_t;
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
double horizontal_rmse_m;
|
||||
double maximum_outage_error_m;
|
||||
double error_at_outage_end_m;
|
||||
double final_horizontal_error_m;
|
||||
double maximum_gps_state_jump_m;
|
||||
double speed_at_outage_start_mps;
|
||||
double speed_at_outage_end_mps;
|
||||
double horizontal_sigma_at_outage_end_m;
|
||||
uint64_t gps_accepted;
|
||||
uint64_t gps_rejected;
|
||||
uint64_t gps_recoveries;
|
||||
uint64_t stale_rejected;
|
||||
uint64_t replay_count;
|
||||
unsigned int generated;
|
||||
unsigned int dropped;
|
||||
bool finite;
|
||||
} scenario_metrics_t;
|
||||
|
||||
static uint32_t random_state;
|
||||
|
||||
static double uniform_random(void)
|
||||
{
|
||||
random_state = 1664525u * random_state + 1013904223u;
|
||||
return ((double)((random_state >> 8) & 0x00ffffffu) + 0.5) /
|
||||
16777216.0;
|
||||
}
|
||||
|
||||
static double gaussian_random(void)
|
||||
{
|
||||
double first = fmax(uniform_random(), 1.0e-12);
|
||||
double second = uniform_random();
|
||||
return sqrt(-2.0 * log(first)) * cos(2.0 * PI * second);
|
||||
}
|
||||
|
||||
static void truth_at(double time_s, double position[3],
|
||||
double velocity[3], double acceleration[3])
|
||||
{
|
||||
memset(position, 0, 3u * sizeof(double));
|
||||
memset(velocity, 0, 3u * sizeof(double));
|
||||
memset(acceleration, 0, 3u * sizeof(double));
|
||||
if(time_s < 3.0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(time_s < 5.0)
|
||||
{
|
||||
double elapsed = time_s - 3.0;
|
||||
acceleration[0] = 1.5;
|
||||
velocity[0] = 1.5 * elapsed;
|
||||
position[0] = 0.75 * elapsed * elapsed;
|
||||
return;
|
||||
}
|
||||
if(time_s < 50.0)
|
||||
{
|
||||
velocity[0] = 3.0;
|
||||
position[0] = 3.0 + 3.0 * (time_s - 5.0);
|
||||
return;
|
||||
}
|
||||
if(time_s < 52.0)
|
||||
{
|
||||
double elapsed = time_s - 50.0;
|
||||
acceleration[0] = -1.5;
|
||||
velocity[0] = 3.0 - 1.5 * elapsed;
|
||||
position[0] = 138.0 + 3.0 * elapsed - 0.75 * elapsed * elapsed;
|
||||
return;
|
||||
}
|
||||
position[0] = 141.0;
|
||||
}
|
||||
|
||||
static fusion_imu_measurement_t make_imu(
|
||||
uint64_t ticks, unsigned int sequence, double gravity_mps2)
|
||||
{
|
||||
double time_s = (double)ticks / (double)TIMEBASE_HZ;
|
||||
double position[3];
|
||||
double velocity[3];
|
||||
double acceleration[3];
|
||||
truth_at(time_s, position, velocity, acceleration);
|
||||
(void)position;
|
||||
(void)velocity;
|
||||
|
||||
fusion_imu_measurement_t imu;
|
||||
memset(&imu, 0, sizeof(imu));
|
||||
imu.timestamp_ticks = ticks;
|
||||
imu.accel_mps2[0] = acceleration[0] + 0.030;
|
||||
imu.accel_mps2[1] = -0.020;
|
||||
imu.accel_mps2[2] = gravity_mps2 + 0.050;
|
||||
imu.gyro_rps[0] = 0.0008;
|
||||
imu.gyro_rps[1] = -0.0005;
|
||||
imu.gyro_rps[2] = 0.0006;
|
||||
if(time_s >= 3.0)
|
||||
{
|
||||
/* Small temperature-like residual drift after startup calibration. */
|
||||
imu.accel_mps2[0] += 0.012 * (time_s - 3.0) / 62.0;
|
||||
}
|
||||
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
||||
{
|
||||
double phase = 0.37 * (double)sequence + 1.7 * (double)axis;
|
||||
imu.accel_mps2[axis] += 0.025 * sin(phase);
|
||||
imu.gyro_rps[axis] += 0.0007 * cos(1.31 * phase);
|
||||
}
|
||||
imu.temperature_c = 25.0 + 0.02 * time_s;
|
||||
imu.sequence = sequence;
|
||||
imu.vehicle_at_rest = (time_s < 2.9) || (time_s >= 52.5);
|
||||
return imu;
|
||||
}
|
||||
|
||||
static fusion_position_measurement_t make_gps(
|
||||
uint64_t capture_ticks, bool bad_restart_fix)
|
||||
{
|
||||
double position[3];
|
||||
double velocity[3];
|
||||
double acceleration[3];
|
||||
truth_at((double)capture_ticks / (double)TIMEBASE_HZ,
|
||||
position, velocity, acceleration);
|
||||
(void)velocity;
|
||||
(void)acceleration;
|
||||
|
||||
fusion_position_measurement_t gps;
|
||||
memset(&gps, 0, sizeof(gps));
|
||||
gps.timestamp_ticks = capture_ticks;
|
||||
gps.position_enu_m[0] = position[0] + 6.0 * gaussian_random();
|
||||
gps.position_enu_m[1] = position[1] + 6.0 * gaussian_random();
|
||||
gps.position_enu_m[2] = position[2] + 10.0 * gaussian_random();
|
||||
if(bad_restart_fix)
|
||||
{
|
||||
gps.position_enu_m[0] += 80.0;
|
||||
gps.position_enu_m[1] -= 50.0;
|
||||
gps.position_enu_m[2] += 30.0;
|
||||
}
|
||||
gps.covariance_enu_m2[0] = 36.0;
|
||||
gps.covariance_enu_m2[4] = 36.0;
|
||||
gps.covariance_enu_m2[8] = 100.0;
|
||||
gps.source_quality = 80.0;
|
||||
gps.source_valid = true;
|
||||
return gps;
|
||||
}
|
||||
|
||||
static bool event_should_drop(scenario_kind_t kind, double capture_s)
|
||||
{
|
||||
if((kind == SCENARIO_MOVING_OUTAGE) &&
|
||||
(capture_s >= 15.0) && (capture_s < 27.0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if((kind == SCENARIO_BAD_RESTART) &&
|
||||
(capture_s >= 15.0) && (capture_s < 23.1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(kind == SCENARIO_INTERMITTENT)
|
||||
{
|
||||
bool natural_long_gap =
|
||||
((capture_s >= 12.0) && (capture_s < 13.8)) ||
|
||||
((capture_s >= 31.0) && (capture_s < 32.8));
|
||||
return natural_long_gap || (uniform_random() < 0.35);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static unsigned int build_events(
|
||||
scenario_kind_t kind, gps_event_t events[MAX_GPS_EVENTS],
|
||||
unsigned int* dropped_out)
|
||||
{
|
||||
unsigned int count = 0u;
|
||||
unsigned int dropped = 0u;
|
||||
uint64_t previous_delivery = 0u;
|
||||
for(uint64_t capture = 3300000u ; capture <= 64000000u ;
|
||||
capture += GPS_PERIOD_TICKS)
|
||||
{
|
||||
double capture_s = (double)capture / (double)TIMEBASE_HZ;
|
||||
if(event_should_drop(kind, capture_s))
|
||||
{
|
||||
dropped++;
|
||||
continue;
|
||||
}
|
||||
if(count >= MAX_GPS_EVENTS)
|
||||
{
|
||||
break;
|
||||
}
|
||||
bool bad_restart_fix = (kind == SCENARIO_BAD_RESTART) &&
|
||||
(capture_s >= 23.1) && (capture_s < 24.0);
|
||||
events[count].measurement = make_gps(capture, bad_restart_fix);
|
||||
uint64_t delay = 120000u + (uint64_t)(250000.0 * uniform_random());
|
||||
uint64_t delivery = capture + delay;
|
||||
if((kind == SCENARIO_BURST_RETURN) &&
|
||||
(capture_s >= 15.0) && (capture_s < 20.5))
|
||||
{
|
||||
delivery = 20700000u;
|
||||
}
|
||||
/* A receiver normally publishes epochs in capture order even when its
|
||||
* processing latency jitters. Preserve that property here. */
|
||||
if(delivery < previous_delivery)
|
||||
{
|
||||
delivery = previous_delivery;
|
||||
}
|
||||
events[count].delivery_ticks = delivery;
|
||||
previous_delivery = delivery;
|
||||
count++;
|
||||
}
|
||||
*dropped_out = dropped;
|
||||
return count;
|
||||
}
|
||||
|
||||
static double horizontal_error(const fusion_engine_t* engine, uint64_t ticks)
|
||||
{
|
||||
double truth_position[3];
|
||||
double truth_velocity[3];
|
||||
double truth_acceleration[3];
|
||||
truth_at((double)ticks / (double)TIMEBASE_HZ,
|
||||
truth_position, truth_velocity, truth_acceleration);
|
||||
(void)truth_velocity;
|
||||
(void)truth_acceleration;
|
||||
return hypot(engine->current.filter.position_m[0] - truth_position[0],
|
||||
engine->current.filter.position_m[1] - truth_position[1]);
|
||||
}
|
||||
|
||||
static scenario_metrics_t run_scenario(
|
||||
scenario_kind_t kind, const char* name, uint32_t seed)
|
||||
{
|
||||
static fusion_engine_t engine;
|
||||
static gps_event_t events[MAX_GPS_EVENTS];
|
||||
fusion_engine_config_t config;
|
||||
scenario_metrics_t metrics;
|
||||
memset(&metrics, 0, sizeof(metrics));
|
||||
metrics.name = name;
|
||||
random_state = seed;
|
||||
unsigned int dropped = 0u;
|
||||
unsigned int event_count = build_events(kind, events, &dropped);
|
||||
metrics.generated = event_count;
|
||||
metrics.dropped = dropped;
|
||||
|
||||
fusion_engine_default_config(&config, TIMEBASE_HZ);
|
||||
if(!fusion_engine_init(&engine, &config))
|
||||
{
|
||||
return metrics;
|
||||
}
|
||||
fusion_position_measurement_t origin;
|
||||
memset(&origin, 0, sizeof(origin));
|
||||
origin.timestamp_ticks = 1u;
|
||||
origin.covariance_enu_m2[0] = 36.0;
|
||||
origin.covariance_enu_m2[4] = 36.0;
|
||||
origin.covariance_enu_m2[8] = 100.0;
|
||||
origin.source_quality = 100.0;
|
||||
origin.source_valid = true;
|
||||
if(!fusion_engine_push_position(&engine, &origin, NULL, NULL))
|
||||
{
|
||||
return metrics;
|
||||
}
|
||||
|
||||
unsigned int event_head = 0u;
|
||||
double squared_error_sum = 0.0;
|
||||
unsigned int error_count = 0u;
|
||||
unsigned int imu_count = 65u * IMU_RATE_HZ;
|
||||
for(unsigned int sample = 0u ; sample <= imu_count ; sample++)
|
||||
{
|
||||
uint64_t ticks =
|
||||
((uint64_t)sample * TIMEBASE_HZ + IMU_RATE_HZ / 2u) / IMU_RATE_HZ;
|
||||
fusion_imu_measurement_t imu = make_imu(
|
||||
ticks, sample, config.eskf.gravity_mps2);
|
||||
if(!fusion_engine_push_imu(&engine, &imu))
|
||||
{
|
||||
return metrics;
|
||||
}
|
||||
|
||||
while((event_head < event_count) &&
|
||||
(events[event_head].delivery_ticks <= ticks))
|
||||
{
|
||||
double before[3];
|
||||
memcpy(before, engine.current.filter.position_m, sizeof(before));
|
||||
(void)fusion_engine_push_position(
|
||||
&engine, &events[event_head].measurement, NULL, NULL);
|
||||
double jump = hypot(
|
||||
engine.current.filter.position_m[0] - before[0],
|
||||
engine.current.filter.position_m[1] - before[1]);
|
||||
if(jump > metrics.maximum_gps_state_jump_m)
|
||||
{
|
||||
metrics.maximum_gps_state_jump_m = jump;
|
||||
}
|
||||
event_head++;
|
||||
}
|
||||
|
||||
if(engine.state != FUSION_ENGINE_RUNNING)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double time_s = (double)ticks / (double)TIMEBASE_HZ;
|
||||
double error = horizontal_error(&engine, ticks);
|
||||
if(time_s >= 3.0)
|
||||
{
|
||||
squared_error_sum += error * error;
|
||||
error_count++;
|
||||
}
|
||||
if((time_s >= 15.0) && (time_s <= 27.0) &&
|
||||
(error > metrics.maximum_outage_error_m))
|
||||
{
|
||||
metrics.maximum_outage_error_m = error;
|
||||
}
|
||||
if(fabs(time_s - 27.0) < 0.5 / (double)IMU_RATE_HZ)
|
||||
{
|
||||
metrics.error_at_outage_end_m = error;
|
||||
metrics.speed_at_outage_end_mps =
|
||||
hypot(engine.current.filter.velocity_mps[0],
|
||||
engine.current.filter.velocity_mps[1]);
|
||||
metrics.horizontal_sigma_at_outage_end_m = sqrt(
|
||||
engine.current.filter.covariance[0][0] +
|
||||
engine.current.filter.covariance[1][1]);
|
||||
}
|
||||
if(fabs(time_s - 15.0) < 0.5 / (double)IMU_RATE_HZ)
|
||||
{
|
||||
metrics.speed_at_outage_start_mps =
|
||||
hypot(engine.current.filter.velocity_mps[0],
|
||||
engine.current.filter.velocity_mps[1]);
|
||||
}
|
||||
}
|
||||
|
||||
metrics.horizontal_rmse_m =
|
||||
(error_count != 0u) ? sqrt(squared_error_sum / (double)error_count) : NAN;
|
||||
metrics.final_horizontal_error_m =
|
||||
horizontal_error(&engine, 65u * TIMEBASE_HZ);
|
||||
metrics.gps_accepted = engine.diagnostics.position_accepted;
|
||||
metrics.gps_rejected = engine.diagnostics.position_nis_rejected;
|
||||
metrics.gps_recoveries = engine.diagnostics.position_recovery_count;
|
||||
metrics.stale_rejected = engine.diagnostics.position_stale_rejected;
|
||||
metrics.replay_count = engine.diagnostics.replay_count;
|
||||
metrics.finite = (engine.state == FUSION_ENGINE_RUNNING) &&
|
||||
eskf_state_is_finite(&engine.current.filter) &&
|
||||
isfinite(metrics.horizontal_rmse_m) &&
|
||||
isfinite(metrics.final_horizontal_error_m);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
static void print_metrics(const scenario_metrics_t* metrics)
|
||||
{
|
||||
printf("%-18s fixes=%3u drop=%2u accepted/rejected=%3llu/%2llu "
|
||||
"recovery=%llu stale=%llu rmse=%5.2fm outage-max/end=%5.2f/%5.2fm "
|
||||
"speed15/27=%4.2f/%4.2f sigma27=%5.1fm jump=%5.2fm final=%5.2fm finite=%u\n",
|
||||
metrics->name, metrics->generated, metrics->dropped,
|
||||
(unsigned long long)metrics->gps_accepted,
|
||||
(unsigned long long)metrics->gps_rejected,
|
||||
(unsigned long long)metrics->gps_recoveries,
|
||||
(unsigned long long)metrics->stale_rejected,
|
||||
metrics->horizontal_rmse_m,
|
||||
metrics->maximum_outage_error_m,
|
||||
metrics->error_at_outage_end_m,
|
||||
metrics->speed_at_outage_start_mps,
|
||||
metrics->speed_at_outage_end_mps,
|
||||
metrics->horizontal_sigma_at_outage_end_m,
|
||||
metrics->maximum_gps_state_jump_m,
|
||||
metrics->final_horizontal_error_m,
|
||||
metrics->finite ? 1u : 0u);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
scenario_metrics_t normal = run_scenario(
|
||||
SCENARIO_NORMAL, "normal", 0x10203040u);
|
||||
scenario_metrics_t outage = run_scenario(
|
||||
SCENARIO_MOVING_OUTAGE, "12s-moving-outage", 0x20304050u);
|
||||
scenario_metrics_t intermittent = run_scenario(
|
||||
SCENARIO_INTERMITTENT, "intermittent", 0x30405060u);
|
||||
scenario_metrics_t burst = run_scenario(
|
||||
SCENARIO_BURST_RETURN, "burst-return", 0x40506070u);
|
||||
scenario_metrics_t restart = run_scenario(
|
||||
SCENARIO_BAD_RESTART, "bad-restart", 0x50607080u);
|
||||
|
||||
puts("Real-log-scaled GNSS failure scenarios: 3.3 Hz, 6 m horizontal / 10 m vertical sigma");
|
||||
print_metrics(&normal);
|
||||
print_metrics(&outage);
|
||||
print_metrics(&intermittent);
|
||||
print_metrics(&burst);
|
||||
print_metrics(&restart);
|
||||
|
||||
bool common_ok =
|
||||
normal.finite && outage.finite && intermittent.finite && burst.finite &&
|
||||
restart.finite &&
|
||||
(normal.horizontal_rmse_m < 5.0) &&
|
||||
/* Position-only dead reckoning is allowed to drift, but the error must
|
||||
* remain statistically covered and recover once GNSS returns. */
|
||||
(outage.maximum_outage_error_m <
|
||||
2.0 * outage.horizontal_sigma_at_outage_end_m) &&
|
||||
(outage.horizontal_sigma_at_outage_end_m > 10.0) &&
|
||||
(outage.final_horizontal_error_m < 5.0) &&
|
||||
(intermittent.final_horizontal_error_m < 6.0) &&
|
||||
(burst.stale_rejected == 0u) &&
|
||||
(burst.replay_count > 0u);
|
||||
bool restart_safe =
|
||||
(restart.gps_recoveries == 0u) &&
|
||||
(restart.gps_rejected >= 3u) &&
|
||||
(restart.maximum_gps_state_jump_m < 50.0) &&
|
||||
(restart.final_horizontal_error_m < 6.0);
|
||||
if(!common_ok || !restart_safe)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"FAIL: GNSS outage/reacquisition safety regression "
|
||||
"(common=%u restart-safe=%u)\n",
|
||||
common_ok ? 1u : 0u, restart_safe ? 1u : 0u);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS: realistic GNSS loss, intermittent delivery and reacquisition stayed bounded.");
|
||||
return 0;
|
||||
}
|
||||
@@ -474,14 +474,14 @@ static void print_metrics(scenario_kind_t scenario, unsigned int gps_ms,
|
||||
static int run_matrix(void)
|
||||
{
|
||||
static const unsigned int gps_periods_ms[] = {200u, 500u, 1000u};
|
||||
static const unsigned int imu_periods_ms[] = {19u, 20u, 200u, 300u};
|
||||
static const unsigned int imu_periods_ms[] = {19u, 20u, 100u, 200u, 300u};
|
||||
bool passed = true;
|
||||
printf("Four-satellite asynchronous platform matrix; 3 s outage and 2 s coherent GNSS fault\n");
|
||||
for(unsigned int scenario = 0u; scenario < SCENARIO_COUNT; scenario++)
|
||||
{
|
||||
for(unsigned int gps_index = 0u; gps_index < 3u; gps_index++)
|
||||
{
|
||||
for(unsigned int imu_index = 0u; imu_index < 4u; imu_index++)
|
||||
for(unsigned int imu_index = 0u; imu_index < 5u; imu_index++)
|
||||
{
|
||||
metrics_t metrics = run_case((scenario_kind_t)scenario,
|
||||
gps_periods_ms[gps_index], imu_periods_ms[imu_index], NULL);
|
||||
@@ -504,7 +504,7 @@ static int run_matrix(void)
|
||||
fprintf(stderr, "FAIL: platform scenario matrix contract.\n");
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: 19/20 ms production rates stayed bounded; 200/300 ms coarse IMU "
|
||||
printf("PASS: 19/20 ms production rates stayed bounded; 100/200/300 ms coarse IMU "
|
||||
"rates remained finite and their dynamic accuracy loss is reported, not hidden.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -512,6 +512,33 @@ static int run_matrix(void)
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if(argc == 1) return run_matrix();
|
||||
if((argc == 6) && (strcmp(argv[1], "--benchmark") == 0))
|
||||
{
|
||||
scenario_kind_t scenario;
|
||||
if(!parse_scenario(argv[2], &scenario))
|
||||
{
|
||||
fprintf(stderr, "unknown scenario: %s\n", argv[2]);
|
||||
return 2;
|
||||
}
|
||||
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
|
||||
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
|
||||
unsigned int iterations = (unsigned int)strtoul(argv[5], NULL, 10);
|
||||
if((iterations == 0u) ||
|
||||
!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
|
||||
!((imu_ms == 19u) || (imu_ms == 20u) || (imu_ms == 100u) ||
|
||||
(imu_ms == 200u) || (imu_ms == 300u)))
|
||||
{
|
||||
fprintf(stderr, "supported GPS: 200/500/1000 ms; "
|
||||
"IMU: 19/20/100/200/300 ms; iterations > 0\n");
|
||||
return 2;
|
||||
}
|
||||
metrics_t metrics;
|
||||
for(unsigned int iteration = 0u; iteration < iterations; iteration++)
|
||||
metrics = run_case(scenario, gps_ms, imu_ms, NULL);
|
||||
print_metrics(scenario, gps_ms, imu_ms, &metrics);
|
||||
printf("completed %u benchmark iterations\n", iterations);
|
||||
return metrics.finite ? 0 : 1;
|
||||
}
|
||||
if((argc == 6) && (strcmp(argv[1], "--log") == 0))
|
||||
{
|
||||
scenario_kind_t scenario;
|
||||
@@ -523,10 +550,10 @@ int main(int argc, char** argv)
|
||||
unsigned int gps_ms = (unsigned int)strtoul(argv[3], NULL, 10);
|
||||
unsigned int imu_ms = (unsigned int)strtoul(argv[4], NULL, 10);
|
||||
if(!((gps_ms == 200u) || (gps_ms == 500u) || (gps_ms == 1000u)) ||
|
||||
!((imu_ms == 19u) || (imu_ms == 20u) ||
|
||||
!((imu_ms == 19u) || (imu_ms == 20u) || (imu_ms == 100u) ||
|
||||
(imu_ms == 200u) || (imu_ms == 300u)))
|
||||
{
|
||||
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/200/300 ms\n");
|
||||
fprintf(stderr, "supported GPS: 200/500/1000 ms; IMU: 19/20/100/200/300 ms\n");
|
||||
return 2;
|
||||
}
|
||||
FILE* log_file = fopen(argv[5], "w");
|
||||
@@ -546,6 +573,9 @@ int main(int argc, char** argv)
|
||||
return metrics.finite ? 0 : 1;
|
||||
}
|
||||
fprintf(stderr, "usage: %s [--log stationary|pedestrian|car|aircraft "
|
||||
"200|500|1000 19|20|200|300 output.log]\n", argv[0]);
|
||||
"200|500|1000 19|20|100|200|300 output.log]\n"
|
||||
" %s [--benchmark stationary|pedestrian|car|aircraft "
|
||||
"200|500|1000 19|20|100|200|300 iterations]\n",
|
||||
argv[0], argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -909,15 +909,16 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
stage_metrics->candidate_total += candidate_count;
|
||||
|
||||
gps_nav_epoch_selection_t selection;
|
||||
bool selected = gps_nav_epoch_select(
|
||||
bool selected = gps_nav_epoch_validate_complete(
|
||||
candidates, candidate_count, EPOCH_SAMPLE_WINDOW,
|
||||
EPOCH_TOW_TOLERANCE_SEC, &selection);
|
||||
unsigned selected_count = selected ? selection.selected_count : 0u;
|
||||
unsigned selected_count = selection.selected_count;
|
||||
stage_metrics->selected_total += selected_count;
|
||||
stage_metrics->epoch_exclusions += candidate_count - selected_count;
|
||||
|
||||
if (stage == STAGE_DELAYED_CHANNEL &&
|
||||
(selected_count != 7u ||
|
||||
(selected ||
|
||||
selected_count != 7u ||
|
||||
(selection.selected_mask & (1u << 7)) != 0u)) {
|
||||
delayed_selection_failures++;
|
||||
}
|
||||
@@ -930,7 +931,7 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
postcommit_join_count_failures++;
|
||||
}
|
||||
|
||||
if (selected_count < STATE_DIM) {
|
||||
if (!selected || selected_count < STATE_DIM) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -983,14 +984,14 @@ static void run_general_synthetic_scenario(const double receiver_truth[3],
|
||||
CHECK(metrics[STAGE_THREE_DROPOUTS].fixes >= 58u,
|
||||
"five remaining satellites must continue producing fixes");
|
||||
CHECK(metrics[STAGE_MINIMUM_FOUR].fixes == 40u,
|
||||
"four satellites must remain a publishable minimum fix");
|
||||
"four satellites must remain a raw publishable minimum fix");
|
||||
CHECK(metrics[STAGE_OUTAGE_THREE].fixes == 0u && below_minimum_fixes == 0u,
|
||||
"three satellites must suppress the position solution");
|
||||
CHECK(metrics[STAGE_REACQUIRE].fixes >= 58u,
|
||||
"position must recover after satellite reacquisition");
|
||||
CHECK(delayed_selection_failures == 0u &&
|
||||
metrics[STAGE_DELAYED_CHANNEL].fixes >= 58u,
|
||||
"delayed/newest outlier channel must be excluded without blocking fixes");
|
||||
metrics[STAGE_DELAYED_CHANNEL].fixes == 0u,
|
||||
"incoherent delayed channel must reject the complete raw epoch");
|
||||
CHECK(precommit_join_count_failures == 0u,
|
||||
"joining channel must remain excluded before matching SF1/SF2/SF3");
|
||||
CHECK(postcommit_join_count_failures == 0u,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
@@ -0,0 +1,241 @@
|
||||
# GPS code measurement ve DLL kalite modeli
|
||||
|
||||
Bu dizin, mevcut firmware/RTL ölçüm yolunu değiştirmeden önce code-phase
|
||||
hassasiyeti, DLL jitter telemetrisi, dinamik WLS sigma ve carrier/Doppler
|
||||
smoothing seçeneklerini aynı sentetik veri üzerinde karşılaştırır.
|
||||
|
||||
Bu genel amaçlı bir RF/GNSS simülatörü değildir. Projeye özgü executable
|
||||
tasarım modelidir.
|
||||
|
||||
Dizin iki ayrı kararı inceler:
|
||||
|
||||
1. Position WLS için C/N0'ın açıklamadığı DLL jitter/variance telemetrisi.
|
||||
2. Velocity WLS ve ESKF öncesindeki sabit Doppler IIR'in gürültü-gecikme
|
||||
etkisi.
|
||||
|
||||
## Modellenen mevcut mimari
|
||||
|
||||
| Proje davranışı | Model |
|
||||
|---|---|
|
||||
| Normalized early-minus-late DLL discriminator | `rtl_dll_model.rtl_discriminator()` |
|
||||
| Signed Q2.13 discriminator quantization | `rtl_dll_model.py` |
|
||||
| `LF_code.sv` C1=`21.25`, C2=`0.05712890625` ve Q20 çıkış | `CodeLoopFilterRtl` |
|
||||
| 20 adet 1 ms DLL örneği/NAV bit | `synthesize_dll_window()` |
|
||||
| Firmware C/N0 IIR, alpha=`0.2` | `ChannelQuality.update()` |
|
||||
| URA/elevation/C/N0 position sigma | `firmware_sigma_m(CURRENT)` |
|
||||
| ECEF x/y/z + common clock-bias WLS | `firmware_wls()` |
|
||||
| 99% chi-square ve en az 7 uyduda leave-one-out FDE | `firmware_wls()` |
|
||||
| Position solve periyodu 200 ms | `measurement_scenarios.py` |
|
||||
| Doppler IIR alpha=`0.05`, 50 Hz | `doppler_eskf_model.py` |
|
||||
| ECEF velocity + common clock-drift WLS | `velocity_wls()` |
|
||||
| Velocity WLS periyodu 200 ms | `doppler_eskf_model.py` |
|
||||
| TDK 100 Hz IMU process-noise profili | `VelocityBiasEskf` |
|
||||
| Velocity 3-DOF `%99.9`, NIS=`16.266236` | `VelocityBiasEskf.update_velocity()` |
|
||||
|
||||
Kaynak eşlemeleri:
|
||||
|
||||
- `hardware/external/gps/rtl/trck_src/code_loop_discriminator.sv`
|
||||
- `hardware/external/gps/rtl/trck_src/LF_code.sv`
|
||||
- `hardware/external/gps/rtl/trck_src/NCO_code.sv`
|
||||
- `hardware/external/gps/rtl/trck_src/tracking_measurement.sv`
|
||||
- `hardware/external/gps/rtl/common/definitions.sv`
|
||||
- `soc/software/gps_system/src/gps/gps_tracking_measurement.c`
|
||||
- `soc/software/gps_system/src/gps/gps_interrupt_handlers.c`
|
||||
- `soc/software/gps_system/src/gps/NavEquations.c`
|
||||
|
||||
`REM_CODE_PHASE`, DLL discriminator residual'ı olarak modellenmez. RTL'de bu
|
||||
değer code NCO'nun epoch taşma remainder'ıdır. Yeni kalite metriğinin girdisi
|
||||
RTL içindeki gerçek signed `dll_error` örnekleridir.
|
||||
|
||||
## Karşılaştırılan sigma politikaları
|
||||
|
||||
- `current_cn0`: mevcut URA + elevation + C/N0 modeli.
|
||||
- `naive_add_dll`: DLL variance'ı bağımsızmış gibi ekler. Nominal durumda aynı
|
||||
thermal jitter'ı iki kere saydığı için negatif kontrol olarak bulunur.
|
||||
- `dll_excess`: yalnız ölçülen DLL variance'ın C/N0'dan beklenen variance'ı aşan
|
||||
bölümünü code sigma'ya ekler.
|
||||
- `dll_excess_guard`: excess variance'a ek olarak sürekli discriminator mean
|
||||
stress'inde ağırlığı azaltır.
|
||||
- `oracle_variance`: sentetik gerçek jitter çarpanını bilir. Ulaşılabilir üst
|
||||
sınırı gösterir; firmware algoritması değildir.
|
||||
|
||||
Dinamik model:
|
||||
|
||||
```text
|
||||
sigma_base^2 = sigma_URA^2 + sigma_elevation^2 + sigma_CN0^2
|
||||
ratio = EWMA(measured_DLL_variance / expected_DLL_variance(C/N0))
|
||||
|
||||
sigma_dynamic^2 =
|
||||
sigma_URA^2
|
||||
+ sigma_elevation^2
|
||||
+ sigma_CN0^2 * max(1, ratio)
|
||||
```
|
||||
|
||||
Bu form C/N0 ile DLL thermal jitter'ını iki kere toplamaz. Yalnızca C/N0
|
||||
modelinin açıklayamadığı kanal-bağımlı excess jitter'ı şişirir.
|
||||
|
||||
## Senaryolar
|
||||
|
||||
- Temiz açık gökyüzü
|
||||
- Bütün kanallarda düşük C/N0
|
||||
- C/N0'da görünmeyen tek-kanal DLL jitter
|
||||
- Bütün kanallarda ortak, C/N0'da görünmeyen DLL jitter
|
||||
- Gürültü burst ve recovery
|
||||
- Re-lock sırasında DLL mean/jitter stress
|
||||
- Düşük-variance stable multipath bias
|
||||
- Bias + excess jitter
|
||||
- Kötü geometri + gizli jitter
|
||||
- İlk başta düşük, sonra iyileşen C/N0
|
||||
|
||||
Ayrı 1D model ham code, kör scalar IIR ve Doppler-predicted code smoothing'i
|
||||
range-rate adımları ve residual carrier CFO altında karşılaştırır. Carrier
|
||||
ölçümleri RTL cadence'i olan 50 Hz'de üretilir ve 200 ms WLS epoch'una kadar
|
||||
entegre edilir. Ham, sabit `alpha=0.05` ve IMU hareket durumuna göre bant
|
||||
genişliğini açan adaptif Doppler yolları birlikte denenir. Bu ayrım önemlidir:
|
||||
PPS datum-drift predictor'u için yararlı olan yavaş Doppler filtresi, ESKF
|
||||
velocity veya hızlı code-aiding prediction'ında dinamik lag üretebilir; tam
|
||||
ham snapshot ise kartta ölçülen gürültü seviyesinde fazla varyans üretebilir.
|
||||
|
||||
## Doppler pre-IIR ve ESKF modeli
|
||||
|
||||
`doppler_eskf_model.py`, aynı truth, tracking gürültüsü ve IMU örnekleriyle şu
|
||||
dört yolu karşılaştırır:
|
||||
|
||||
- Önceki: 50 Hz `alpha=0.05` IIR -> 5 Hz velocity WLS -> ESKF.
|
||||
- Ham Doppler -> 5 Hz velocity WLS -> ESKF.
|
||||
- C/N0 + düşük-geçiren navigation acceleration ile adaptif IIR -> WLS -> ESKF.
|
||||
- Ham Doppler -> RAIM-benzeri leave-one-out robust WLS -> ESKF.
|
||||
|
||||
Mevcut `alpha=0.05` tek-kutuplu IIR için 50 Hz'de eşdeğer cutoff yaklaşık
|
||||
`0.408 Hz`, zaman sabiti `0.390 s`, DC group delay ise yaklaşık `380 ms`'dir.
|
||||
Bu gecikme constant velocity'de görünmez; acceleration sırasında velocity
|
||||
measurement innovation'ına doğrudan girer.
|
||||
|
||||
ESKF modeli bu karar için gerekli `[velocity, accel-bias]` alt sistemidir.
|
||||
Firmware'deki TDK process acceleration noise density `0.025
|
||||
m/s2/sqrt(Hz)`, accelerometer bias random walk `0.0015 m/s2/sqrt(s)`, WLS'nin
|
||||
gerçek 3x3 covariance'ı, fusion adapter'ın `2/2/3 m/s` ENU velocity sigma
|
||||
tabanları ve velocity NIS kapısı kullanılır. Position, attitude, lever-arm ve
|
||||
tam 15-state cross-covariance bu modelin kapsamında değildir.
|
||||
|
||||
Senaryo matrisi sabit uçuş, airliner/aggressive manevra, düşük C/N0, tek-kanal
|
||||
Doppler burst, common receiver clock-drift step, 15 saniye GPS kesintisi ve
|
||||
birleşik arıza koşullarını içerir.
|
||||
|
||||
Nominal 20-seed sentetik sonuçların özeti:
|
||||
|
||||
- ESKF ham WLS gürültüsünü nominalde yaklaşık `0.416 m/s` 3D RMS'ten `0.190
|
||||
m/s` seviyesine indirir; temporal filtreleme zaten ESKF'de gerçekleşir.
|
||||
- Sabit pre-IIR sakin uçuşta `0.182 m/s`, ham yol `0.190 m/s` verir. Airliner
|
||||
manevrasında sabit IIR gecikmesi sonucu `0.416 m/s`, ham yol `0.190 m/s`;
|
||||
aggressive manevrada sırasıyla `0.942 m/s` ve `0.190 m/s` oluşur. Adapter
|
||||
sigma tabanları iki yolu da NIS içinde tutsa bile dinamik lag kalır.
|
||||
- Düşük C/N0 manevrasında adaptif taslak en iyi gürültü/gecikme uzlaşmasını
|
||||
verir; eşikleri firmware'e taşımadan önce kart loguyla kalibre etmek gerekir.
|
||||
- ESKF yalnız aggregate 3D velocity gördüğünden tek uydu burst'ünün kaynağını
|
||||
ayıramaz. Kanal outlier/FDE işlemi velocity WLS öncesinde yapılmalıdır.
|
||||
- Common receiver clock-frequency hatası WLS'nin dördüncü clock-drift state'i
|
||||
tarafından hızdan ayrılır.
|
||||
|
||||
Genesys2 sabit-kart ölçümünde tam ham yolun velocity-WLS RMS'i `9.17 m/s`,
|
||||
p95'i `16.71 m/s` ve FDE oranı `86/120` oldu. Aynı epoch'larda sabit
|
||||
`alpha=0.05` yol `0.92 m/s` RMS, `1.63 m/s` p95 ve `0/120` FDE verdi. Bu
|
||||
nedenle nominal modelde iyi görünen doğrudan ham yol production'a alınmadı.
|
||||
Kart gürültüsünü eşlemek için tracking noise `22x` ile kalibre edilen regresyon
|
||||
modelindeki regresyon seed'leri için steady-7/airliner-3/aggressive-7 fused
|
||||
RMS değerleri:
|
||||
|
||||
- sabit IIR: `0.451 / 0.484 / 1.036 m/s`,
|
||||
- adaptif IIR: `0.451 / 0.336 / 0.538 m/s`,
|
||||
- tam ham: `1.260 / 1.100 / 1.260 m/s`.
|
||||
|
||||
Production firmware ESKF için `alpha=0.05/0.20/0.40` hareket-adaptif Doppler
|
||||
-> bandwidth-aware sigma -> kanal validity/FDE -> velocity WLS + covariance
|
||||
-> ESKF yolunu kullanır. Ham alan tanısal kayıt ve offline analiz için korunur.
|
||||
Ayrı sabit `alpha=0.05` Doppler velocity yalnız PPS position-WLS clock-bias
|
||||
datumunun drift predictor'u ve PPS kalite metriği için kullanılır. Ayrıntılı
|
||||
nominal tablo ve model sınırları `output/DOPPLER_ESKF_ANALYSIS.md` içindedir;
|
||||
kart-kalibreli karar `test_doppler_eskf_model.py` ile regresyona alınmıştır.
|
||||
|
||||
## Kurulum ve çalıştırma
|
||||
|
||||
Repo içindeki `soc/software` dizininden:
|
||||
|
||||
```bash
|
||||
python3 -m venv gps_measurement_python/.venv
|
||||
gps_measurement_python/.venv/bin/pip install \
|
||||
-r gps_measurement_python/requirements.txt
|
||||
gps_measurement_python/.venv/bin/python -m unittest \
|
||||
gps_measurement_python/test_measurement_model.py \
|
||||
gps_measurement_python/test_doppler_eskf_model.py
|
||||
gps_measurement_python/.venv/bin/python \
|
||||
-m gps_measurement_python.analyze_measurement_scenarios \
|
||||
--seeds 12 --jobs 4
|
||||
gps_measurement_python/.venv/bin/python \
|
||||
-m gps_measurement_python.analyze_doppler_eskf \
|
||||
--seeds 20 --jobs 4
|
||||
```
|
||||
|
||||
Sistem Python'unda `venv` paketi yoksa bağımlılıklar izole bir target dizinine
|
||||
de kurulabilir:
|
||||
|
||||
```bash
|
||||
python3 -m pip install --target /tmp/gps-measurement-deps \
|
||||
-r gps_measurement_python/requirements.txt
|
||||
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
|
||||
-m unittest gps_measurement_python/test_measurement_model.py
|
||||
PYTHONPATH=/tmp/gps-measurement-deps:. python3 \
|
||||
-m gps_measurement_python.analyze_measurement_scenarios \
|
||||
--seeds 12 --jobs 4
|
||||
```
|
||||
|
||||
Çıktılar `gps_measurement_python/output/` altındadır:
|
||||
|
||||
- `scenario_summary.csv`
|
||||
- `ANALYSIS.md`
|
||||
- `01_position_3d_rms.png`
|
||||
- `02_solution_availability.png`
|
||||
- `03_noise_burst_transition.png`
|
||||
- `04_code_smoothing_cfo.png`
|
||||
- `doppler_eskf_summary.csv`
|
||||
- `DOPPLER_ESKF_ANALYSIS.md`
|
||||
- `05_doppler_eskf_velocity_rms.png`
|
||||
- `06_airliner_maneuver_trace.png`
|
||||
- `07_outlier_wls_nis.png`
|
||||
- `08_low_cn0_wls_vs_eskf.png`
|
||||
|
||||
## Kalibrasyon sınırı
|
||||
|
||||
Modelde C/N0'dan beklenen normalized discriminator standard deviation'ı
|
||||
45 dB-Hz için `0.020` kabul edilir ve firmware ile aynı 20 dB amplitude yasası
|
||||
ile ölçeklenir. Bu sayı karttan alınmış bir kalibrasyon değildir.
|
||||
|
||||
Karar vermeden önce karttan her kanal için en az şu 50 Hz kayıt alınmalıdır:
|
||||
|
||||
```text
|
||||
sequence, PRN, C/N0,
|
||||
DLL mean, DLL variance, DLL sample count,
|
||||
code NCO correction, carrier frequency,
|
||||
ABS_SAMPLE, REM_CODE_PHASE, lock
|
||||
```
|
||||
|
||||
Kayıtlarla `expected_DLL_variance(C/N0)` eğrisi çıkarılmalı; temperature,
|
||||
elevation ve lock yaşına göre ayrıştırılmalıdır. Stable multipath, DLL yeni
|
||||
denge noktasına oturduğunda düşük variance bırakabilir. Bu durum tek başına
|
||||
DLL variance ile çözülemez; ek correlator tap'leri veya correlation-shape
|
||||
metriği gerekir.
|
||||
|
||||
RTL için en düşük maliyetli keşif arayüzü, her 20 ms NAV-bit penceresinde
|
||||
`dll_error` üzerinden aşağıdaki ham istatistikleri biriktirmektir:
|
||||
|
||||
```text
|
||||
signed sum(error) : 15 + ceil(log2(20)) = 20 bit
|
||||
unsigned sum(error^2) : 30 + ceil(log2(20)) = 35 bit
|
||||
valid sample count : 5 bit
|
||||
```
|
||||
|
||||
35-bit kare toplamı APB'de iki 32-bit register ile taşınabilir. Ortalama,
|
||||
sample variance, C/N0 expectation ratio ve hysteresis yazılımda hesaplanır.
|
||||
Bölme ve square-root RTL'ye konmaz. Bu tuple mevcut measurement sequence,
|
||||
C/N0, carrier frequency, `ABS_SAMPLE` ve `REM_CODE_PHASE` ile aynı 20 ms
|
||||
snapshot'ta tutulmalıdır.
|
||||
@@ -0,0 +1 @@
|
||||
"""Project-specific GPS code-measurement and WLS analysis model."""
|
||||
@@ -0,0 +1,428 @@
|
||||
"""Monte Carlo comparison of Doppler prefilter choices before velocity ESKF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from .doppler_eskf_model import (
|
||||
DopplerEskfTrace,
|
||||
DopplerPath,
|
||||
PATH_ORDER,
|
||||
VELOCITY_NIS_GATE,
|
||||
flight_scenarios,
|
||||
run_doppler_eskf_scenario,
|
||||
)
|
||||
|
||||
|
||||
PATH_LABEL = {
|
||||
DopplerPath.FIXED_IIR: "Önceki sabit IIR",
|
||||
DopplerPath.RAW: "Ham Doppler + ESKF",
|
||||
DopplerPath.ADAPTIVE_IIR: "Adaptif IIR + ESKF",
|
||||
DopplerPath.RAW_ROBUST: "Ham + robust WLS + ESKF",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrialMetrics:
|
||||
scenario: str
|
||||
path: DopplerPath
|
||||
fused_rms_mps: float
|
||||
fused_p95_mps: float
|
||||
quiet_rms_mps: float
|
||||
dynamic_rms_mps: float
|
||||
wls_rms_mps: float
|
||||
wls_p95_mps: float
|
||||
clock_drift_rms_mps: float
|
||||
velocity_accept_percent: float
|
||||
nis_reject_percent: float
|
||||
fused_95_coverage_percent: float
|
||||
excluded_epoch_percent: float
|
||||
mean_covariance_scale: float
|
||||
mean_alpha: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Summary:
|
||||
scenario: str
|
||||
path: DopplerPath
|
||||
seeds: int
|
||||
fused_rms_mps: float
|
||||
fused_rms_seed_std_mps: float
|
||||
fused_p95_mps: float
|
||||
quiet_rms_mps: float
|
||||
dynamic_rms_mps: float
|
||||
wls_rms_mps: float
|
||||
wls_p95_mps: float
|
||||
clock_drift_rms_mps: float
|
||||
velocity_accept_percent: float
|
||||
nis_reject_percent: float
|
||||
fused_95_coverage_percent: float
|
||||
excluded_epoch_percent: float
|
||||
mean_covariance_scale: float
|
||||
mean_alpha: float
|
||||
|
||||
|
||||
def _rms(values: np.ndarray) -> float:
|
||||
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
|
||||
|
||||
|
||||
def _truth_at_wls(trace: DopplerEskfTrace) -> np.ndarray:
|
||||
return np.column_stack(
|
||||
[
|
||||
np.interp(
|
||||
trace.wls_time_s,
|
||||
trace.time_s,
|
||||
trace.truth_velocity_mps[:, axis],
|
||||
)
|
||||
for axis in range(3)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def trial_metrics(scenario_index: int, trace: DopplerEskfTrace) -> TrialMetrics:
|
||||
scenario = flight_scenarios()[scenario_index]
|
||||
burn_in = trace.time_s >= 5.0
|
||||
fused_vector_error = trace.fused_velocity_mps - trace.truth_velocity_mps
|
||||
fused_error = np.linalg.norm(fused_vector_error, axis=1)
|
||||
dynamic = burn_in & (np.linalg.norm(trace.acceleration_mps2, axis=1) > 0.25)
|
||||
quiet = burn_in & ~dynamic
|
||||
|
||||
fused_nees = []
|
||||
for sample in np.flatnonzero(burn_in):
|
||||
covariance = trace.fused_velocity_covariance_mps2[sample]
|
||||
error = fused_vector_error[sample]
|
||||
fused_nees.append(float(error @ np.linalg.inv(covariance) @ error))
|
||||
fused_nees_array = np.asarray(fused_nees)
|
||||
|
||||
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
|
||||
true_clock_drift = np.asarray(
|
||||
[scenario.receiver_clock_drift_mps(float(time)) for time in trace.wls_time_s]
|
||||
)
|
||||
clock_error = trace.wls_clock_drift_mps - true_clock_drift
|
||||
valid_nis = trace.wls_nis[np.isfinite(trace.wls_nis)]
|
||||
|
||||
return TrialMetrics(
|
||||
scenario=trace.scenario,
|
||||
path=trace.path,
|
||||
fused_rms_mps=_rms(fused_error[burn_in]),
|
||||
fused_p95_mps=float(np.percentile(fused_error[burn_in], 95.0)),
|
||||
quiet_rms_mps=_rms(fused_error[quiet]),
|
||||
dynamic_rms_mps=_rms(fused_error[dynamic]),
|
||||
wls_rms_mps=_rms(wls_error),
|
||||
wls_p95_mps=float(np.percentile(wls_error, 95.0)),
|
||||
clock_drift_rms_mps=_rms(clock_error),
|
||||
velocity_accept_percent=100.0 * float(np.mean(trace.wls_accepted)),
|
||||
nis_reject_percent=100.0 * float(np.mean(valid_nis > VELOCITY_NIS_GATE)),
|
||||
# 7.8147 is the 95-percent chi-square threshold for three states.
|
||||
fused_95_coverage_percent=100.0 * float(np.mean(fused_nees_array <= 7.8147279)),
|
||||
excluded_epoch_percent=100.0
|
||||
* float(np.mean(trace.excluded_satellites >= 0)),
|
||||
mean_covariance_scale=float(np.mean(trace.wls_covariance_scale)),
|
||||
mean_alpha=float(np.mean(trace.adaptive_alpha)),
|
||||
)
|
||||
|
||||
|
||||
def _worker(task: tuple[int, int]) -> tuple[int, int, list[TrialMetrics]]:
|
||||
scenario_index, seed = task
|
||||
scenario = flight_scenarios()[scenario_index]
|
||||
traces = run_doppler_eskf_scenario(scenario, seed)
|
||||
return (
|
||||
scenario_index,
|
||||
seed,
|
||||
[trial_metrics(scenario_index, traces[path]) for path in PATH_ORDER],
|
||||
)
|
||||
|
||||
|
||||
def summarize(items: list[TrialMetrics]) -> Summary:
|
||||
if not items:
|
||||
raise ValueError("cannot summarize an empty trial list")
|
||||
|
||||
def average(field_name: str) -> float:
|
||||
values = np.asarray([getattr(item, field_name) for item in items], dtype=float)
|
||||
finite = values[np.isfinite(values)]
|
||||
return float(np.mean(finite)) if len(finite) else math.nan
|
||||
|
||||
fused_rms = np.asarray([item.fused_rms_mps for item in items])
|
||||
return Summary(
|
||||
scenario=items[0].scenario,
|
||||
path=items[0].path,
|
||||
seeds=len(items),
|
||||
fused_rms_mps=average("fused_rms_mps"),
|
||||
fused_rms_seed_std_mps=float(np.std(fused_rms)),
|
||||
fused_p95_mps=average("fused_p95_mps"),
|
||||
quiet_rms_mps=average("quiet_rms_mps"),
|
||||
dynamic_rms_mps=average("dynamic_rms_mps"),
|
||||
wls_rms_mps=average("wls_rms_mps"),
|
||||
wls_p95_mps=average("wls_p95_mps"),
|
||||
clock_drift_rms_mps=average("clock_drift_rms_mps"),
|
||||
velocity_accept_percent=average("velocity_accept_percent"),
|
||||
nis_reject_percent=average("nis_reject_percent"),
|
||||
fused_95_coverage_percent=average("fused_95_coverage_percent"),
|
||||
excluded_epoch_percent=average("excluded_epoch_percent"),
|
||||
mean_covariance_scale=average("mean_covariance_scale"),
|
||||
mean_alpha=average("mean_alpha"),
|
||||
)
|
||||
|
||||
|
||||
def write_csv(path: Path, summaries: list[Summary]) -> None:
|
||||
with path.open("w", newline="", encoding="utf-8") as output:
|
||||
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
|
||||
writer.writeheader()
|
||||
for item in summaries:
|
||||
row = dict(item.__dict__)
|
||||
row["path"] = item.path.value
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def _summary_plot(path: Path, summaries: list[Summary]) -> None:
|
||||
scenarios = flight_scenarios()
|
||||
x = np.arange(len(scenarios))
|
||||
width = 0.19
|
||||
figure, axis = plt.subplots(figsize=(15, 6.3))
|
||||
for path_index, doppler_path in enumerate(PATH_ORDER):
|
||||
values = [
|
||||
next(
|
||||
item.fused_rms_mps
|
||||
for item in summaries
|
||||
if item.scenario == scenario.name and item.path == doppler_path
|
||||
)
|
||||
for scenario in scenarios
|
||||
]
|
||||
axis.bar(
|
||||
x + (path_index - 1.5) * width,
|
||||
values,
|
||||
width,
|
||||
label=PATH_LABEL[doppler_path],
|
||||
)
|
||||
axis.set_ylabel("ESKF 3D velocity RMS (m/s)")
|
||||
axis.set_yscale("log")
|
||||
axis.set_xticks(x)
|
||||
axis.set_xticklabels([item.name for item in scenarios], rotation=25, ha="right")
|
||||
axis.grid(axis="y", alpha=0.25)
|
||||
axis.legend(ncol=2)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def _maneuver_trace_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
|
||||
reference = trace_set[DopplerPath.FIXED_IIR]
|
||||
figure, axes = plt.subplots(3, 1, figsize=(13, 8.5), sharex=True)
|
||||
for doppler_path in PATH_ORDER:
|
||||
trace = trace_set[doppler_path]
|
||||
error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
|
||||
axes[0].plot(trace.time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
|
||||
axes[1].plot(
|
||||
reference.time_s,
|
||||
np.linalg.norm(reference.acceleration_mps2, axis=1),
|
||||
color="black",
|
||||
linewidth=1.2,
|
||||
label="True |acceleration|",
|
||||
)
|
||||
adaptive = trace_set[DopplerPath.ADAPTIVE_IIR]
|
||||
axes[2].step(
|
||||
adaptive.wls_time_s,
|
||||
adaptive.adaptive_alpha,
|
||||
where="post",
|
||||
color="tab:green",
|
||||
)
|
||||
axes[0].set_ylabel("3D velocity error (m/s)")
|
||||
axes[1].set_ylabel("Acceleration (m/s²)")
|
||||
axes[2].set_ylabel("Adaptive alpha")
|
||||
axes[2].set_xlabel("Time (s)")
|
||||
axes[0].set_ylim(bottom=0.0)
|
||||
axes[0].legend(ncol=2)
|
||||
for axis in axes:
|
||||
axis.grid(alpha=0.25)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def _outlier_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
|
||||
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
|
||||
for doppler_path in PATH_ORDER:
|
||||
trace = trace_set[doppler_path]
|
||||
error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
|
||||
axes[0].plot(trace.wls_time_s, error, label=PATH_LABEL[doppler_path], linewidth=1.0)
|
||||
axes[1].plot(trace.wls_time_s, trace.wls_nis, label=PATH_LABEL[doppler_path], linewidth=0.9)
|
||||
axes[1].axhline(VELOCITY_NIS_GATE, color="black", linestyle="--", linewidth=1.0)
|
||||
for start, stop in ((30.0, 31.0), (56.0, 57.0), (72.0, 72.6)):
|
||||
for axis in axes:
|
||||
axis.axvspan(start, stop, color="tab:red", alpha=0.10)
|
||||
axes[0].set_ylabel("WLS velocity error (m/s)")
|
||||
axes[1].set_ylabel("Velocity NIS")
|
||||
axes[1].set_xlabel("Time (s)")
|
||||
axes[0].legend(ncol=2)
|
||||
for axis in axes:
|
||||
axis.grid(alpha=0.25)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def _filtering_plot(path: Path, trace_set: dict[DopplerPath, DopplerEskfTrace]) -> None:
|
||||
figure, axes = plt.subplots(2, 1, figsize=(13, 7), sharex=True)
|
||||
for doppler_path in (DopplerPath.FIXED_IIR, DopplerPath.RAW, DopplerPath.ADAPTIVE_IIR):
|
||||
trace = trace_set[doppler_path]
|
||||
wls_error = np.linalg.norm(trace.wls_velocity_mps - _truth_at_wls(trace), axis=1)
|
||||
fused_error = np.linalg.norm(trace.fused_velocity_mps - trace.truth_velocity_mps, axis=1)
|
||||
axes[0].plot(trace.wls_time_s, wls_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
|
||||
axes[1].plot(trace.time_s, fused_error, label=PATH_LABEL[doppler_path], linewidth=0.9)
|
||||
axes[0].set_ylabel("WLS 3D error (m/s)")
|
||||
axes[1].set_ylabel("ESKF 3D error (m/s)")
|
||||
axes[1].set_xlabel("Time (s)")
|
||||
axes[0].legend(ncol=2)
|
||||
for axis in axes:
|
||||
axis.grid(alpha=0.25)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def _lookup(
|
||||
summaries: list[Summary], scenario: str, doppler_path: DopplerPath
|
||||
) -> Summary:
|
||||
return next(
|
||||
item
|
||||
for item in summaries
|
||||
if item.scenario == scenario and item.path == doppler_path
|
||||
)
|
||||
|
||||
|
||||
def write_report(path: Path, summaries: list[Summary], seed_count: int) -> None:
|
||||
lines = [
|
||||
"# Doppler WLS -> ESKF model sonuçları",
|
||||
"",
|
||||
f"Monte Carlo seed sayısı: `{seed_count}`; ilk 5 saniye RMS metriklerinden çıkarıldı.",
|
||||
"50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.",
|
||||
"",
|
||||
"## Sonuç tablosu",
|
||||
"",
|
||||
"| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |",
|
||||
"|---|---|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for scenario in flight_scenarios():
|
||||
for doppler_path in PATH_ORDER:
|
||||
item = _lookup(summaries, scenario.name, doppler_path)
|
||||
dynamic = "-" if not np.isfinite(item.dynamic_rms_mps) else f"{item.dynamic_rms_mps:.3f}"
|
||||
lines.append(
|
||||
f"| {scenario.name} | {PATH_LABEL[doppler_path]} | "
|
||||
f"{item.fused_rms_mps:.3f} | {dynamic} | {item.wls_rms_mps:.3f} | "
|
||||
f"{item.velocity_accept_percent:.1f} | {item.fused_95_coverage_percent:.1f} | "
|
||||
f"{item.excluded_epoch_percent:.1f} |"
|
||||
)
|
||||
|
||||
steady_fixed = _lookup(summaries, "steady_cruise", DopplerPath.FIXED_IIR)
|
||||
steady_raw = _lookup(summaries, "steady_cruise", DopplerPath.RAW)
|
||||
air_fixed = _lookup(summaries, "airliner_maneuver", DopplerPath.FIXED_IIR)
|
||||
air_raw = _lookup(summaries, "airliner_maneuver", DopplerPath.RAW)
|
||||
weak_fixed = _lookup(summaries, "low_cn0_airliner", DopplerPath.FIXED_IIR)
|
||||
weak_raw = _lookup(summaries, "low_cn0_airliner", DopplerPath.RAW)
|
||||
weak_adaptive = _lookup(summaries, "low_cn0_airliner", DopplerPath.ADAPTIVE_IIR)
|
||||
outlier_raw = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW)
|
||||
outlier_robust = _lookup(summaries, "single_satellite_outliers", DopplerPath.RAW_ROBUST)
|
||||
clock_raw = _lookup(summaries, "clock_drift_steps", DopplerPath.RAW)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Bulgular",
|
||||
"",
|
||||
f"- Sabit hızda pre-IIR ESKF RMS'i `{steady_fixed.fused_rms_mps:.3f}` m/s, ham yol `{steady_raw.fused_rms_mps:.3f}` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `{steady_raw.wls_rms_mps:.3f}` m/s WLS RMS'ten bu seviyeye indiriyor.",
|
||||
f"- Airliner manevrasında sabit IIR `{air_fixed.fused_rms_mps:.3f}` m/s ve `%{air_fixed.nis_reject_percent:.1f}` NIS ret oranı verirken ham yol `{air_raw.fused_rms_mps:.3f}` m/s ve `%{air_raw.nis_reject_percent:.1f}` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.",
|
||||
f"- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `{weak_fixed.fused_rms_mps:.3f}`, `{weak_raw.fused_rms_mps:.3f}`, `{weak_adaptive.fused_rms_mps:.3f}` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.",
|
||||
f"- Tek uydu burst'ünde raw WLS RMS `{outlier_raw.wls_rms_mps:.3f}` m/s, robust leave-one-out ile `{outlier_robust.wls_rms_mps:.3f}` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.",
|
||||
f"- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `{clock_raw.clock_drift_rms_mps:.3f}` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.",
|
||||
"",
|
||||
"## Mimari kararı",
|
||||
"",
|
||||
"- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.",
|
||||
"- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.",
|
||||
"- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.",
|
||||
"- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.",
|
||||
"",
|
||||
"## Model sınırları",
|
||||
"",
|
||||
"- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.",
|
||||
"- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.",
|
||||
"- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.",
|
||||
"- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.",
|
||||
"- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.",
|
||||
"- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--seeds", type=int, default=20)
|
||||
parser.add_argument("--jobs", type=int, default=4)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent / "output",
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
arguments.output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
scenarios = flight_scenarios()
|
||||
tasks = [
|
||||
(scenario_index, seed)
|
||||
for scenario_index in range(len(scenarios))
|
||||
for seed in range(1, arguments.seeds + 1)
|
||||
]
|
||||
grouped: dict[tuple[int, DopplerPath], list[TrialMetrics]] = {
|
||||
(scenario_index, doppler_path): []
|
||||
for scenario_index in range(len(scenarios))
|
||||
for doppler_path in PATH_ORDER
|
||||
}
|
||||
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
|
||||
for scenario_index, seed, metrics in executor.map(_worker, tasks):
|
||||
for item in metrics:
|
||||
grouped[(scenario_index, item.path)].append(item)
|
||||
print(f"completed {scenarios[scenario_index].name} seed={seed}", flush=True)
|
||||
|
||||
summaries = [
|
||||
summarize(grouped[(scenario_index, doppler_path)])
|
||||
for scenario_index in range(len(scenarios))
|
||||
for doppler_path in PATH_ORDER
|
||||
]
|
||||
write_csv(arguments.output / "doppler_eskf_summary.csv", summaries)
|
||||
write_report(
|
||||
arguments.output / "DOPPLER_ESKF_ANALYSIS.md",
|
||||
summaries,
|
||||
arguments.seeds,
|
||||
)
|
||||
_summary_plot(arguments.output / "05_doppler_eskf_velocity_rms.png", summaries)
|
||||
|
||||
airliner = run_doppler_eskf_scenario(
|
||||
next(item for item in scenarios if item.name == "airliner_maneuver"), 1
|
||||
)
|
||||
_maneuver_trace_plot(arguments.output / "06_airliner_maneuver_trace.png", airliner)
|
||||
outlier = run_doppler_eskf_scenario(
|
||||
next(item for item in scenarios if item.name == "single_satellite_outliers"), 1
|
||||
)
|
||||
_outlier_plot(arguments.output / "07_outlier_wls_nis.png", outlier)
|
||||
weak = run_doppler_eskf_scenario(
|
||||
next(item for item in scenarios if item.name == "low_cn0_airliner"), 1
|
||||
)
|
||||
_filtering_plot(arguments.output / "08_low_cn0_wls_vs_eskf.png", weak)
|
||||
print(f"wrote Doppler/ESKF analysis to {arguments.output}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,425 @@
|
||||
"""Run the complete DLL-quality/WLS and code-smoothing comparison."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from .code_smoothing_model import run_smoothing_trial
|
||||
from .firmware_wls_model import SigmaMethod
|
||||
from .measurement_scenarios import EpochSample, run_scenario, scenario_matrix
|
||||
|
||||
|
||||
METHOD_ORDER = (
|
||||
SigmaMethod.CURRENT,
|
||||
SigmaMethod.NAIVE_ADD_DLL,
|
||||
SigmaMethod.DLL_EXCESS,
|
||||
SigmaMethod.DLL_EXCESS_GUARD,
|
||||
SigmaMethod.ORACLE,
|
||||
)
|
||||
METHOD_LABEL = {
|
||||
SigmaMethod.CURRENT: "Mevcut C/N0",
|
||||
SigmaMethod.NAIVE_ADD_DLL: "Naif DLL ekle",
|
||||
SigmaMethod.DLL_EXCESS: "DLL excess",
|
||||
SigmaMethod.DLL_EXCESS_GUARD: "DLL excess+mean",
|
||||
SigmaMethod.ORACLE: "Oracle variance",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Summary:
|
||||
scenario: str
|
||||
method: SigmaMethod
|
||||
epochs: int
|
||||
horizontal_rms_m: float
|
||||
position_3d_rms_m: float
|
||||
position_3d_p95_m: float
|
||||
valid_percent: float
|
||||
valid_3d_rms_m: float
|
||||
horizontal_2sigma_coverage_percent: float
|
||||
false_valid_over_25m_percent: float
|
||||
fde_percent: float
|
||||
mean_measurement_sigma_m: float
|
||||
|
||||
|
||||
def _worker(task: tuple[int, int]) -> tuple[int, int, list[EpochSample]]:
|
||||
scenario_index, seed = task
|
||||
scenario = scenario_matrix()[scenario_index]
|
||||
return scenario_index, seed, run_scenario(scenario, seed, METHOD_ORDER)
|
||||
|
||||
|
||||
def _rms(values: np.ndarray) -> float:
|
||||
return float(np.sqrt(np.mean(np.square(values)))) if len(values) else math.nan
|
||||
|
||||
|
||||
def summarize(scenario_name: str, method: SigmaMethod, samples: list[EpochSample]) -> Summary:
|
||||
selected = [
|
||||
item
|
||||
for item in samples
|
||||
if item.method == method and item.time_s >= 5.0
|
||||
]
|
||||
horizontal = np.asarray([item.horizontal_error_m for item in selected])
|
||||
error_3d = np.asarray([item.position_error_3d_m for item in selected])
|
||||
valid = np.asarray([item.valid for item in selected], dtype=bool)
|
||||
horizontal_sigma = np.asarray([item.horizontal_sigma_m for item in selected])
|
||||
fde = np.asarray([item.fde_applied for item in selected], dtype=bool)
|
||||
mean_sigma = np.asarray([item.mean_measurement_sigma_m for item in selected])
|
||||
false_valid = valid & (error_3d > 25.0)
|
||||
return Summary(
|
||||
scenario=scenario_name,
|
||||
method=method,
|
||||
epochs=len(selected),
|
||||
horizontal_rms_m=_rms(horizontal),
|
||||
position_3d_rms_m=_rms(error_3d),
|
||||
position_3d_p95_m=float(np.percentile(error_3d, 95.0)),
|
||||
valid_percent=100.0 * float(np.mean(valid)),
|
||||
valid_3d_rms_m=_rms(error_3d[valid]),
|
||||
horizontal_2sigma_coverage_percent=100.0
|
||||
* float(np.mean(horizontal <= 2.0 * horizontal_sigma)),
|
||||
false_valid_over_25m_percent=100.0 * float(np.mean(false_valid)),
|
||||
fde_percent=100.0 * float(np.mean(fde)),
|
||||
mean_measurement_sigma_m=float(np.mean(mean_sigma)),
|
||||
)
|
||||
|
||||
|
||||
def write_csv(path: Path, summaries: list[Summary]) -> None:
|
||||
with path.open("w", newline="", encoding="utf-8") as output:
|
||||
writer = csv.DictWriter(output, fieldnames=list(Summary.__dataclass_fields__))
|
||||
writer.writeheader()
|
||||
for item in summaries:
|
||||
row = dict(item.__dict__)
|
||||
row["method"] = item.method.value
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def _comparison_plot(
|
||||
path: Path,
|
||||
summaries: list[Summary],
|
||||
field: str,
|
||||
ylabel: str,
|
||||
*,
|
||||
logarithmic: bool = False,
|
||||
) -> None:
|
||||
scenarios = scenario_matrix()
|
||||
x = np.arange(len(scenarios))
|
||||
width = 0.16
|
||||
figure, axis = plt.subplots(figsize=(15, 6.5))
|
||||
for method_index, method in enumerate(METHOD_ORDER):
|
||||
values = [
|
||||
getattr(
|
||||
next(
|
||||
item
|
||||
for item in summaries
|
||||
if item.scenario == scenario.name and item.method == method
|
||||
),
|
||||
field,
|
||||
)
|
||||
for scenario in scenarios
|
||||
]
|
||||
axis.bar(
|
||||
x + (method_index - 2) * width,
|
||||
values,
|
||||
width,
|
||||
label=METHOD_LABEL[method],
|
||||
)
|
||||
axis.set_ylabel(ylabel)
|
||||
if logarithmic:
|
||||
axis.set_yscale("log")
|
||||
axis.set_xticks(x)
|
||||
axis.set_xticklabels([item.name for item in scenarios], rotation=28, ha="right")
|
||||
axis.grid(axis="y", alpha=0.25)
|
||||
axis.legend(ncol=3)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def _transition_plot(path: Path, samples: list[EpochSample]) -> None:
|
||||
figure, axes = plt.subplots(2, 1, figsize=(12, 7), sharex=True)
|
||||
for method in (
|
||||
SigmaMethod.CURRENT,
|
||||
SigmaMethod.DLL_EXCESS,
|
||||
SigmaMethod.DLL_EXCESS_GUARD,
|
||||
):
|
||||
selected = [item for item in samples if item.method == method]
|
||||
axes[0].plot(
|
||||
[item.time_s for item in selected],
|
||||
[item.position_error_3d_m for item in selected],
|
||||
label=METHOD_LABEL[method],
|
||||
linewidth=1.2,
|
||||
)
|
||||
reference = [item for item in samples if item.method == SigmaMethod.CURRENT]
|
||||
axes[1].plot(
|
||||
[item.time_s for item in reference],
|
||||
[item.maximum_dll_ratio for item in reference],
|
||||
color="tab:red",
|
||||
)
|
||||
axes[0].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
|
||||
axes[1].axvspan(22.0, 45.0, color="tab:orange", alpha=0.15)
|
||||
axes[0].set_ylabel("3D konum hatası (m)")
|
||||
axes[1].set_ylabel("Maks. DLL variance ratio")
|
||||
axes[1].set_xlabel("Zaman (s)")
|
||||
axes[0].legend()
|
||||
for axis in axes:
|
||||
axis.grid(alpha=0.25)
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def smoothing_summary(seed_count: int) -> dict[str, dict[str, float]]:
|
||||
result: dict[str, dict[str, float]] = {}
|
||||
for label, cfo_mps in (
|
||||
("cfo_compensated", 0.0),
|
||||
("cfo_residual_0p5_mps", 0.5),
|
||||
("cfo_residual_3_mps", 3.0),
|
||||
):
|
||||
trials = [
|
||||
run_smoothing_trial(seed, doppler_cfo_mps=cfo_mps)
|
||||
for seed in range(1, seed_count + 1)
|
||||
]
|
||||
result[label] = {
|
||||
field: float(np.mean([getattr(item, field) for item in trials]))
|
||||
for field in trials[0].__dataclass_fields__
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _smoothing_plot(path: Path, smoothing: dict[str, dict[str, float]]) -> None:
|
||||
labels = list(smoothing)
|
||||
x = np.arange(len(labels))
|
||||
width = 0.20
|
||||
figure, axis = plt.subplots(figsize=(10, 5.5))
|
||||
for offset, (field, title) in enumerate(
|
||||
(
|
||||
("raw_rmse_m", "Ham code"),
|
||||
("scalar_iir_rmse_m", "Kör IIR"),
|
||||
("doppler_smoothed_rmse_m", "Raw Doppler-predicted"),
|
||||
(
|
||||
"firmware_iir_doppler_smoothed_rmse_m",
|
||||
"FW-IIR Doppler-predicted",
|
||||
),
|
||||
)
|
||||
):
|
||||
axis.bar(
|
||||
x + (offset - 1.5) * width,
|
||||
[smoothing[label][field] for label in labels],
|
||||
width,
|
||||
label=title,
|
||||
)
|
||||
axis.set_xticks(x)
|
||||
axis.set_xticklabels(labels)
|
||||
axis.set_ylabel("Range RMSE (m)")
|
||||
axis.set_yscale("log")
|
||||
axis.grid(axis="y", alpha=0.25)
|
||||
axis.legend()
|
||||
figure.tight_layout()
|
||||
figure.savefig(path, dpi=150)
|
||||
plt.close(figure)
|
||||
|
||||
|
||||
def write_report(
|
||||
path: Path,
|
||||
summaries: list[Summary],
|
||||
focused_rms: dict[str, dict[SigmaMethod, float]],
|
||||
smoothing: dict[str, dict[str, float]],
|
||||
seed_count: int,
|
||||
) -> None:
|
||||
current = {
|
||||
item.scenario: item
|
||||
for item in summaries
|
||||
if item.method == SigmaMethod.CURRENT
|
||||
}
|
||||
lines = [
|
||||
"# GPS code measurement model sonuçları",
|
||||
"",
|
||||
f"Monte Carlo seed sayısı: `{seed_count}`. İlk 5 saniye metriklere alınmadı.",
|
||||
"",
|
||||
"## Yöntem karşılaştırması",
|
||||
"",
|
||||
"| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |",
|
||||
"|---|---|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for scenario in scenario_matrix():
|
||||
baseline = current[scenario.name].position_3d_rms_m
|
||||
for method in METHOD_ORDER:
|
||||
item = next(
|
||||
value
|
||||
for value in summaries
|
||||
if value.scenario == scenario.name and value.method == method
|
||||
)
|
||||
improvement = 100.0 * (baseline - item.position_3d_rms_m) / baseline
|
||||
lines.append(
|
||||
f"| {scenario.name} | {METHOD_LABEL[method]} | "
|
||||
f"{item.position_3d_rms_m:.2f} | {improvement:+.1f}% | "
|
||||
f"{item.position_3d_p95_m:.2f} | {item.valid_percent:.1f} | "
|
||||
f"{item.false_valid_over_25m_percent:.2f} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Modelden çıkan karar özeti",
|
||||
"",
|
||||
"- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.",
|
||||
"- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.",
|
||||
"- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.",
|
||||
"- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.",
|
||||
"- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.",
|
||||
"- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.",
|
||||
"",
|
||||
"## Bozulma penceresi odaklı 3D RMS",
|
||||
"",
|
||||
"| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |",
|
||||
"|---|---:|---:|---:|---:|",
|
||||
]
|
||||
)
|
||||
for label, values in focused_rms.items():
|
||||
lines.append(
|
||||
f"| {label} | {values[SigmaMethod.CURRENT]:.2f} | "
|
||||
f"{values[SigmaMethod.DLL_EXCESS]:.2f} | "
|
||||
f"{values[SigmaMethod.DLL_EXCESS_GUARD]:.2f} | "
|
||||
f"{values[SigmaMethod.ORACLE]:.2f} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Code smoothing ve carrier CFO",
|
||||
"",
|
||||
"| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |",
|
||||
"|---|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
)
|
||||
for label, values in smoothing.items():
|
||||
lines.append(
|
||||
f"| {label} | {values['raw_rmse_m']:.2f} | "
|
||||
f"{values['scalar_iir_rmse_m']:.2f} | "
|
||||
f"{values['doppler_smoothed_rmse_m']:.2f} | "
|
||||
f"{values['firmware_iir_doppler_smoothed_rmse_m']:.2f} | "
|
||||
f"{values['scalar_iir_peak_dynamic_error_m']:.2f} | "
|
||||
f"{values['doppler_peak_dynamic_error_m']:.2f} | "
|
||||
f"{values['firmware_iir_doppler_peak_dynamic_error_m']:.2f} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Yorum sınırları",
|
||||
"",
|
||||
"- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.",
|
||||
"- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.",
|
||||
"- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.",
|
||||
"- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.",
|
||||
"- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.",
|
||||
"- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.",
|
||||
"- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.",
|
||||
"- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--seeds", type=int, default=12)
|
||||
parser.add_argument("--jobs", type=int, default=4)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent / "output",
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
arguments.output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
scenarios = scenario_matrix()
|
||||
tasks = [
|
||||
(scenario_index, seed)
|
||||
for scenario_index in range(len(scenarios))
|
||||
for seed in range(1, arguments.seeds + 1)
|
||||
]
|
||||
grouped: dict[int, list[EpochSample]] = {
|
||||
index: [] for index in range(len(scenarios))
|
||||
}
|
||||
transition_samples: list[EpochSample] = []
|
||||
with ProcessPoolExecutor(max_workers=max(1, arguments.jobs)) as executor:
|
||||
for scenario_index, seed, samples in executor.map(_worker, tasks):
|
||||
grouped[scenario_index].extend(samples)
|
||||
if scenarios[scenario_index].name == "noise_burst_recovery" and seed == 1:
|
||||
transition_samples = samples
|
||||
print(
|
||||
f"completed {scenarios[scenario_index].name} seed={seed}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
summaries = [
|
||||
summarize(scenario.name, method, grouped[index])
|
||||
for index, scenario in enumerate(scenarios)
|
||||
for method in METHOD_ORDER
|
||||
]
|
||||
focused_windows = {
|
||||
"noise_burst_recovery [22,45) s": ("noise_burst_recovery", 22.0, 45.0),
|
||||
"dll_transient_stress [25,34) s": ("dll_transient_stress", 25.0, 34.0),
|
||||
"cn0_step_recovery [5,30) s": ("cn0_step_recovery", 5.0, 30.0),
|
||||
}
|
||||
focused_rms: dict[str, dict[SigmaMethod, float]] = {}
|
||||
for label, (scenario_name, start_s, stop_s) in focused_windows.items():
|
||||
scenario_index = next(
|
||||
index
|
||||
for index, scenario in enumerate(scenarios)
|
||||
if scenario.name == scenario_name
|
||||
)
|
||||
focused_rms[label] = {}
|
||||
for method in METHOD_ORDER:
|
||||
values = np.asarray(
|
||||
[
|
||||
item.position_error_3d_m
|
||||
for item in grouped[scenario_index]
|
||||
if item.method == method
|
||||
and start_s <= item.time_s < stop_s
|
||||
]
|
||||
)
|
||||
focused_rms[label][method] = _rms(values)
|
||||
smoothing = smoothing_summary(max(arguments.seeds, 20))
|
||||
|
||||
write_csv(arguments.output / "scenario_summary.csv", summaries)
|
||||
write_report(
|
||||
arguments.output / "ANALYSIS.md",
|
||||
summaries,
|
||||
focused_rms,
|
||||
smoothing,
|
||||
arguments.seeds,
|
||||
)
|
||||
_comparison_plot(
|
||||
arguments.output / "01_position_3d_rms.png",
|
||||
summaries,
|
||||
"position_3d_rms_m",
|
||||
"3D RMS (m)",
|
||||
logarithmic=True,
|
||||
)
|
||||
_comparison_plot(
|
||||
arguments.output / "02_solution_availability.png",
|
||||
summaries,
|
||||
"valid_percent",
|
||||
"Integrity-valid çözüm (%)",
|
||||
)
|
||||
_transition_plot(
|
||||
arguments.output / "03_noise_burst_transition.png",
|
||||
transition_samples,
|
||||
)
|
||||
_smoothing_plot(arguments.output / "04_code_smoothing_cfo.png", smoothing)
|
||||
print(f"wrote analysis to {arguments.output}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
"""One-dimensional code smoothing study with carrier/Doppler aiding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SmoothingResult:
|
||||
raw_rmse_m: float
|
||||
scalar_iir_rmse_m: float
|
||||
doppler_smoothed_rmse_m: float
|
||||
firmware_iir_doppler_smoothed_rmse_m: float
|
||||
scalar_iir_peak_dynamic_error_m: float
|
||||
doppler_peak_dynamic_error_m: float
|
||||
firmware_iir_doppler_peak_dynamic_error_m: float
|
||||
|
||||
|
||||
def run_smoothing_trial(
|
||||
seed: int,
|
||||
*,
|
||||
duration_s: float = 80.0,
|
||||
tracking_dt_s: float = 0.020,
|
||||
wls_dt_s: float = 0.200,
|
||||
code_sigma_m: float = 4.0,
|
||||
doppler_sigma_mps: float = 0.25,
|
||||
doppler_cfo_mps: float = 0.0,
|
||||
alpha: float = 0.10,
|
||||
firmware_doppler_alpha: float = 0.05,
|
||||
) -> SmoothingResult:
|
||||
"""Compare raw code, blind IIR and Doppler-propagated code smoothing.
|
||||
|
||||
The truth includes a range-rate step so a visually smooth but lagging
|
||||
scalar IIR is penalized. ``doppler_cfo_mps`` represents residual receiver
|
||||
carrier frequency offset after the nominal IF subtraction.
|
||||
"""
|
||||
|
||||
if abs(round(wls_dt_s / tracking_dt_s) - wls_dt_s / tracking_dt_s) > 1.0e-12:
|
||||
raise ValueError("WLS period must be an integer number of tracking periods")
|
||||
rng = np.random.default_rng(seed)
|
||||
tracking_time_s = np.arange(0.0, duration_s, tracking_dt_s)
|
||||
true_rate_tracking = np.where(
|
||||
tracking_time_s < 20.0,
|
||||
-120.0,
|
||||
np.where(tracking_time_s < 45.0, 230.0, -40.0),
|
||||
)
|
||||
true_range_tracking = np.empty_like(tracking_time_s)
|
||||
true_range_tracking[0] = 21_000_000.0
|
||||
true_range_tracking[1:] = (
|
||||
true_range_tracking[0]
|
||||
+ np.cumsum(true_rate_tracking[:-1] * tracking_dt_s)
|
||||
)
|
||||
measured_rate_tracking = (
|
||||
true_rate_tracking
|
||||
+ doppler_cfo_mps
|
||||
+ rng.normal(0.0, doppler_sigma_mps, len(tracking_time_s))
|
||||
)
|
||||
filtered_rate_tracking = np.empty_like(measured_rate_tracking)
|
||||
filtered_rate_tracking[0] = measured_rate_tracking[0]
|
||||
for index in range(1, len(filtered_rate_tracking)):
|
||||
filtered_rate_tracking[index] = (
|
||||
firmware_doppler_alpha * measured_rate_tracking[index]
|
||||
+ (1.0 - firmware_doppler_alpha) * filtered_rate_tracking[index - 1]
|
||||
)
|
||||
|
||||
tracking_per_wls = int(round(wls_dt_s / tracking_dt_s))
|
||||
wls_indices = np.arange(0, len(tracking_time_s), tracking_per_wls)
|
||||
time_s = tracking_time_s[wls_indices]
|
||||
true_range = true_range_tracking[wls_indices]
|
||||
raw_code = true_range + rng.normal(0.0, code_sigma_m, len(time_s))
|
||||
|
||||
scalar = np.empty_like(raw_code)
|
||||
doppler_smoothed = np.empty_like(raw_code)
|
||||
firmware_iir_doppler_smoothed = np.empty_like(raw_code)
|
||||
scalar[0] = raw_code[0]
|
||||
doppler_smoothed[0] = raw_code[0]
|
||||
firmware_iir_doppler_smoothed[0] = raw_code[0]
|
||||
for index in range(1, len(time_s)):
|
||||
scalar[index] = alpha * raw_code[index] + (1.0 - alpha) * scalar[index - 1]
|
||||
start = wls_indices[index - 1]
|
||||
stop = wls_indices[index]
|
||||
raw_range_delta = (
|
||||
float(np.sum(measured_rate_tracking[start:stop]))
|
||||
* tracking_dt_s
|
||||
)
|
||||
filtered_range_delta = (
|
||||
float(np.sum(filtered_rate_tracking[start:stop]))
|
||||
* tracking_dt_s
|
||||
)
|
||||
prediction = doppler_smoothed[index - 1] + raw_range_delta
|
||||
doppler_smoothed[index] = (
|
||||
prediction + alpha * (raw_code[index] - prediction)
|
||||
)
|
||||
filtered_prediction = (
|
||||
firmware_iir_doppler_smoothed[index - 1]
|
||||
+ filtered_range_delta
|
||||
)
|
||||
firmware_iir_doppler_smoothed[index] = (
|
||||
filtered_prediction
|
||||
+ alpha * (raw_code[index] - filtered_prediction)
|
||||
)
|
||||
|
||||
warm = time_s >= 5.0
|
||||
dynamic = ((time_s >= 20.0) & (time_s < 25.0)) | (
|
||||
(time_s >= 45.0) & (time_s < 50.0)
|
||||
)
|
||||
rmse = lambda error, mask: float(np.sqrt(np.mean(np.square(error[mask]))))
|
||||
return SmoothingResult(
|
||||
raw_rmse_m=rmse(raw_code - true_range, warm),
|
||||
scalar_iir_rmse_m=rmse(scalar - true_range, warm),
|
||||
doppler_smoothed_rmse_m=rmse(doppler_smoothed - true_range, warm),
|
||||
firmware_iir_doppler_smoothed_rmse_m=rmse(
|
||||
firmware_iir_doppler_smoothed - true_range,
|
||||
warm,
|
||||
),
|
||||
scalar_iir_peak_dynamic_error_m=float(
|
||||
np.max(np.abs(scalar[dynamic] - true_range[dynamic]))
|
||||
),
|
||||
doppler_peak_dynamic_error_m=float(
|
||||
np.max(np.abs(doppler_smoothed[dynamic] - true_range[dynamic]))
|
||||
),
|
||||
firmware_iir_doppler_peak_dynamic_error_m=float(
|
||||
np.max(
|
||||
np.abs(
|
||||
firmware_iir_doppler_smoothed[dynamic]
|
||||
- true_range[dynamic]
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,657 @@
|
||||
"""Project-specific Doppler WLS to ESKF timing/noise model.
|
||||
|
||||
The model compares four paths using one shared 50 Hz tracking stream and one
|
||||
shared 100 Hz IMU stream:
|
||||
|
||||
* the legacy fixed-alpha Doppler IIR before 5 Hz velocity WLS;
|
||||
* raw Doppler into WLS, as a nominal-model comparison rather than production;
|
||||
* a global IMU-driven adaptive-alpha IIR;
|
||||
* raw Doppler with the per-satellite robust WLS gate used before the ESKF.
|
||||
|
||||
The ESKF is reduced to the velocity and accelerometer-bias error states that
|
||||
matter to this experiment. Its process noise, 3-D measurement covariance and
|
||||
99.9-percent velocity NIS gate mirror the project firmware. Attitude and
|
||||
position are intentionally outside this model's scope.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
IMU_HZ = 100.0
|
||||
TRACKING_HZ = 50.0
|
||||
VELOCITY_WLS_HZ = 5.0
|
||||
IMU_DT_S = 1.0 / IMU_HZ
|
||||
TRACKING_STEPS = int(round(IMU_HZ / TRACKING_HZ))
|
||||
WLS_STEPS = int(round(IMU_HZ / VELOCITY_WLS_HZ))
|
||||
FIXED_DOPPLER_ALPHA = 0.05
|
||||
CN0_ALPHA = 0.20
|
||||
VELOCITY_NIS_GATE = 16.26623619623813
|
||||
PROCESS_ACCEL_NOISE_DENSITY = 0.025
|
||||
ACCEL_BIAS_RANDOM_WALK = 0.0015
|
||||
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS = 2.0
|
||||
MINIMUM_VERTICAL_VELOCITY_STD_MPS = 3.0
|
||||
|
||||
|
||||
class DopplerPath(str, Enum):
|
||||
FIXED_IIR = "fixed_iir_wls_eskf"
|
||||
RAW = "raw_wls_eskf"
|
||||
ADAPTIVE_IIR = "adaptive_iir_wls_eskf"
|
||||
RAW_ROBUST = "raw_robust_wls_eskf"
|
||||
|
||||
|
||||
PATH_ORDER = (
|
||||
DopplerPath.FIXED_IIR,
|
||||
DopplerPath.RAW,
|
||||
DopplerPath.ADAPTIVE_IIR,
|
||||
DopplerPath.RAW_ROBUST,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FlightScenario:
|
||||
name: str
|
||||
description: str
|
||||
duration_s: float
|
||||
initial_velocity_mps: np.ndarray
|
||||
acceleration: Callable[[float], np.ndarray]
|
||||
cn0_offset: Callable[[float], float]
|
||||
noise_multiplier: Callable[[float, int], float]
|
||||
outlier_mps: Callable[[float, int], float]
|
||||
receiver_clock_drift_mps: Callable[[float], float]
|
||||
gps_gaps: tuple[tuple[float, float], ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VelocityWlsResult:
|
||||
velocity_mps: np.ndarray
|
||||
clock_drift_mps: float
|
||||
covariance_mps2: np.ndarray
|
||||
residual_mps: np.ndarray
|
||||
residual_rms_mps: float
|
||||
covariance_scale: float
|
||||
excluded_satellite: int | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdaptiveAlpha:
|
||||
alpha: float = FIXED_DOPPLER_ALPHA
|
||||
hold_samples: int = 0
|
||||
filtered_acceleration_mps2: np.ndarray = field(
|
||||
default_factory=lambda: np.zeros(3, dtype=float)
|
||||
)
|
||||
|
||||
def update(
|
||||
self,
|
||||
measured_acceleration_mps2: np.ndarray,
|
||||
median_cn0_dbhz: float,
|
||||
) -> float:
|
||||
# A raw 100 Hz accelerometer sample is too noisy for coefficient
|
||||
# switching. This 50 Hz vector EWMA has an approximately 0.16 s time
|
||||
# constant and, unlike smoothing |a|, does not retain the positive
|
||||
# magnitude bias of zero-mean sensor noise.
|
||||
motion_alpha = 0.12
|
||||
self.filtered_acceleration_mps2 += motion_alpha * (
|
||||
measured_acceleration_mps2 - self.filtered_acceleration_mps2
|
||||
)
|
||||
magnitude = float(np.linalg.norm(self.filtered_acceleration_mps2))
|
||||
|
||||
if median_cn0_dbhz >= 46.0:
|
||||
quiet_alpha = 0.08
|
||||
elif median_cn0_dbhz >= 37.0:
|
||||
quiet_alpha = FIXED_DOPPLER_ALPHA
|
||||
else:
|
||||
quiet_alpha = 0.03
|
||||
|
||||
if magnitude >= 2.5:
|
||||
self.alpha = 0.40
|
||||
self.hold_samples = 25
|
||||
elif magnitude >= 0.55:
|
||||
self.alpha = 0.20
|
||||
self.hold_samples = 25
|
||||
elif self.hold_samples > 0:
|
||||
self.hold_samples -= 1
|
||||
else:
|
||||
self.alpha = quiet_alpha
|
||||
return self.alpha
|
||||
|
||||
|
||||
@dataclass
|
||||
class VelocityBiasEskf:
|
||||
velocity_mps: np.ndarray
|
||||
accel_bias_mps2: np.ndarray
|
||||
covariance: np.ndarray
|
||||
accepted: int = 0
|
||||
rejected_nis: int = 0
|
||||
last_nis: float = float("nan")
|
||||
|
||||
@classmethod
|
||||
def initialize(
|
||||
cls,
|
||||
initial_velocity_mps: np.ndarray,
|
||||
initial_velocity_error_mps: np.ndarray,
|
||||
) -> "VelocityBiasEskf":
|
||||
covariance = np.zeros((6, 6), dtype=float)
|
||||
covariance[:3, :3] = np.eye(3)
|
||||
covariance[3:, 3:] = np.eye(3) * 0.1**2
|
||||
return cls(
|
||||
velocity_mps=initial_velocity_mps + initial_velocity_error_mps,
|
||||
accel_bias_mps2=np.zeros(3),
|
||||
covariance=covariance,
|
||||
)
|
||||
|
||||
def propagate(self, measured_acceleration_mps2: np.ndarray, dt_s: float) -> None:
|
||||
self.velocity_mps += (
|
||||
measured_acceleration_mps2 - self.accel_bias_mps2
|
||||
) * dt_s
|
||||
|
||||
transition = np.eye(6)
|
||||
transition[:3, 3:] = -np.eye(3) * dt_s
|
||||
accel_variance = PROCESS_ACCEL_NOISE_DENSITY**2
|
||||
bias_variance = ACCEL_BIAS_RANDOM_WALK**2
|
||||
process = np.zeros((6, 6), dtype=float)
|
||||
# Match eskf.c's first-order discrete injection: velocity and bias
|
||||
# driving noise are added after F P F^T, without exact-discretization
|
||||
# dt^2/dt^3 cross terms.
|
||||
process[:3, :3] = np.eye(3) * accel_variance * dt_s
|
||||
process[3:, 3:] = np.eye(3) * bias_variance * dt_s
|
||||
self.covariance = (
|
||||
transition @ self.covariance @ transition.T + process
|
||||
)
|
||||
self.covariance = 0.5 * (self.covariance + self.covariance.T)
|
||||
|
||||
def update_velocity(
|
||||
self,
|
||||
measurement_mps: np.ndarray,
|
||||
covariance_mps2: np.ndarray,
|
||||
) -> bool:
|
||||
innovation = measurement_mps - self.velocity_mps
|
||||
innovation_covariance = self.covariance[:3, :3] + covariance_mps2
|
||||
inverse = np.linalg.inv(innovation_covariance)
|
||||
nis = float(innovation @ inverse @ innovation)
|
||||
self.last_nis = nis
|
||||
if not np.isfinite(nis) or nis > VELOCITY_NIS_GATE:
|
||||
self.rejected_nis += 1
|
||||
return False
|
||||
|
||||
measurement_matrix = np.zeros((3, 6), dtype=float)
|
||||
measurement_matrix[:, :3] = np.eye(3)
|
||||
gain = self.covariance @ measurement_matrix.T @ inverse
|
||||
correction = gain @ innovation
|
||||
self.velocity_mps += correction[:3]
|
||||
self.accel_bias_mps2 += correction[3:]
|
||||
identity = np.eye(6)
|
||||
residual_transform = identity - gain @ measurement_matrix
|
||||
self.covariance = (
|
||||
residual_transform
|
||||
@ self.covariance
|
||||
@ residual_transform.T
|
||||
+ gain @ covariance_mps2 @ gain.T
|
||||
)
|
||||
self.covariance = 0.5 * (self.covariance + self.covariance.T)
|
||||
self.accepted += 1
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DopplerEskfTrace:
|
||||
scenario: str
|
||||
path: DopplerPath
|
||||
time_s: np.ndarray
|
||||
truth_velocity_mps: np.ndarray
|
||||
fused_velocity_mps: np.ndarray
|
||||
fused_velocity_covariance_mps2: np.ndarray
|
||||
acceleration_mps2: np.ndarray
|
||||
wls_time_s: np.ndarray
|
||||
wls_velocity_mps: np.ndarray
|
||||
wls_clock_drift_mps: np.ndarray
|
||||
wls_covariance_mps2: np.ndarray
|
||||
wls_covariance_scale: np.ndarray
|
||||
wls_nis: np.ndarray
|
||||
wls_accepted: np.ndarray
|
||||
adaptive_alpha: np.ndarray
|
||||
excluded_satellites: np.ndarray
|
||||
|
||||
|
||||
def _zero_acceleration(_time_s: float) -> np.ndarray:
|
||||
return np.zeros(3)
|
||||
|
||||
|
||||
def _airliner_acceleration(time_s: float) -> np.ndarray:
|
||||
if 15.0 <= time_s < 25.0:
|
||||
return np.asarray([2.0, 0.0, 0.0])
|
||||
if 38.0 <= time_s < 52.0:
|
||||
return np.asarray([0.0, 1.5, 0.25])
|
||||
if 68.0 <= time_s < 76.0:
|
||||
return np.asarray([-2.5, 0.0, -0.15])
|
||||
return np.zeros(3)
|
||||
|
||||
|
||||
def _aggressive_acceleration(time_s: float) -> np.ndarray:
|
||||
if 10.0 <= time_s < 16.0:
|
||||
return np.asarray([6.0, 0.0, 1.0])
|
||||
if 26.0 <= time_s < 33.0:
|
||||
return np.asarray([0.0, 7.0, 0.0])
|
||||
if 46.0 <= time_s < 52.0:
|
||||
return np.asarray([-5.0, -4.0, -1.0])
|
||||
if 64.0 <= time_s < 76.0:
|
||||
phase = 2.0 * np.pi * (time_s - 64.0) / 3.0
|
||||
return np.asarray([3.0 * np.sin(phase), 4.0 * np.cos(phase), 0.5 * np.sin(phase)])
|
||||
return np.zeros(3)
|
||||
|
||||
|
||||
def _constant_zero(_time_s: float) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _constant_one(_time_s: float, _satellite: int) -> float:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _no_outlier(_time_s: float, _satellite: int) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def flight_scenarios() -> tuple[FlightScenario, ...]:
|
||||
return (
|
||||
FlightScenario(
|
||||
"steady_cruise",
|
||||
"Constant aircraft velocity with nominal open-sky Doppler noise.",
|
||||
90.0,
|
||||
np.asarray([180.0, 20.0, 0.0]),
|
||||
_zero_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
_constant_zero,
|
||||
),
|
||||
FlightScenario(
|
||||
"airliner_maneuver",
|
||||
"Moderate longitudinal, turn and climb-rate accelerations.",
|
||||
90.0,
|
||||
np.asarray([120.0, 0.0, 0.0]),
|
||||
_airliner_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
_constant_zero,
|
||||
),
|
||||
FlightScenario(
|
||||
"aggressive_maneuver",
|
||||
"High acceleration and oscillatory manoeuvre segments.",
|
||||
90.0,
|
||||
np.asarray([80.0, 0.0, 0.0]),
|
||||
_aggressive_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
_constant_zero,
|
||||
),
|
||||
FlightScenario(
|
||||
"low_cn0_airliner",
|
||||
"Airliner manoeuvre with a 10 dB signal-quality loss.",
|
||||
90.0,
|
||||
np.asarray([120.0, 0.0, 0.0]),
|
||||
_airliner_acceleration,
|
||||
lambda _time_s: -10.0,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
_constant_zero,
|
||||
),
|
||||
FlightScenario(
|
||||
"single_satellite_outliers",
|
||||
"One channel has repeatable 12 m/s Doppler bursts.",
|
||||
90.0,
|
||||
np.asarray([120.0, 0.0, 0.0]),
|
||||
_airliner_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
lambda time_s, satellite: (
|
||||
12.0
|
||||
if satellite == 2
|
||||
and (
|
||||
30.0 <= time_s < 31.0
|
||||
or 56.0 <= time_s < 57.0
|
||||
or 72.0 <= time_s < 72.6
|
||||
)
|
||||
else 0.0
|
||||
),
|
||||
_constant_zero,
|
||||
),
|
||||
FlightScenario(
|
||||
"clock_drift_steps",
|
||||
"Common receiver clock-frequency steps coexist with manoeuvres.",
|
||||
90.0,
|
||||
np.asarray([120.0, 0.0, 0.0]),
|
||||
_airliner_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
lambda time_s: (
|
||||
18.0 if 28.0 <= time_s < 55.0 else (-9.0 if time_s >= 55.0 else 0.0)
|
||||
),
|
||||
),
|
||||
FlightScenario(
|
||||
"gps_gap_recovery",
|
||||
"Fifteen-second GNSS gap during an aircraft manoeuvre.",
|
||||
90.0,
|
||||
np.asarray([120.0, 0.0, 0.0]),
|
||||
_airliner_acceleration,
|
||||
_constant_zero,
|
||||
_constant_one,
|
||||
_no_outlier,
|
||||
_constant_zero,
|
||||
gps_gaps=((42.0, 57.0),),
|
||||
),
|
||||
FlightScenario(
|
||||
"mixed_flight_faults",
|
||||
"Aggressive motion, temporary weak signal, channel bursts and clock step.",
|
||||
90.0,
|
||||
np.asarray([80.0, 0.0, 0.0]),
|
||||
_aggressive_acceleration,
|
||||
lambda time_s: -9.0 if 34.0 <= time_s < 60.0 else 0.0,
|
||||
lambda time_s, satellite: (
|
||||
2.5 if 34.0 <= time_s < 60.0 and satellite in (1, 6) else 1.0
|
||||
),
|
||||
lambda time_s, satellite: (
|
||||
-10.0 if satellite == 4 and 54.0 <= time_s < 55.0 else 0.0
|
||||
),
|
||||
lambda time_s: 12.0 if time_s >= 48.0 else 0.0,
|
||||
gps_gaps=((78.0, 83.0),),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def local_los_matrix() -> np.ndarray:
|
||||
azimuth_deg = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
|
||||
elevation_deg = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
|
||||
azimuth = np.deg2rad(azimuth_deg)
|
||||
elevation = np.deg2rad(elevation_deg)
|
||||
return np.column_stack(
|
||||
[
|
||||
np.cos(elevation) * np.sin(azimuth),
|
||||
np.cos(elevation) * np.cos(azimuth),
|
||||
np.sin(elevation),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def velocity_sigma_mps(cn0_dbhz: np.ndarray) -> np.ndarray:
|
||||
return np.clip(30.0 * np.power(10.0, -cn0_dbhz / 20.0), 0.05, 5.0)
|
||||
|
||||
|
||||
def _solve_velocity_candidate(
|
||||
design: np.ndarray,
|
||||
observations_mps: np.ndarray,
|
||||
sigma_mps: np.ndarray,
|
||||
indices: np.ndarray,
|
||||
) -> VelocityWlsResult:
|
||||
selected_design = design[indices]
|
||||
selected_observations = observations_mps[indices]
|
||||
selected_sigma = sigma_mps[indices]
|
||||
weights = 1.0 / np.square(selected_sigma)
|
||||
normal = selected_design.T @ (weights[:, None] * selected_design)
|
||||
inverse = np.linalg.inv(normal)
|
||||
solution = inverse @ (selected_design.T @ (weights * selected_observations))
|
||||
residual = selected_observations - selected_design @ solution
|
||||
weighted_residual_square_sum = float(np.sum(weights * np.square(residual)))
|
||||
degrees_of_freedom = len(indices) - 4
|
||||
covariance_scale = (
|
||||
max(1.0, weighted_residual_square_sum / degrees_of_freedom)
|
||||
if degrees_of_freedom > 0
|
||||
else 1.0
|
||||
)
|
||||
return VelocityWlsResult(
|
||||
velocity_mps=solution[:3].copy(),
|
||||
clock_drift_mps=float(solution[3]),
|
||||
covariance_mps2=covariance_scale * inverse[:3, :3],
|
||||
residual_mps=residual,
|
||||
residual_rms_mps=float(np.sqrt(np.mean(np.square(residual)))),
|
||||
covariance_scale=covariance_scale,
|
||||
excluded_satellite=None,
|
||||
)
|
||||
|
||||
|
||||
def velocity_wls(
|
||||
observations_mps: np.ndarray,
|
||||
cn0_dbhz: np.ndarray,
|
||||
*,
|
||||
robust: bool = False,
|
||||
) -> VelocityWlsResult:
|
||||
los = local_los_matrix()
|
||||
design = np.column_stack([-los, np.ones(len(los))])
|
||||
sigma = velocity_sigma_mps(cn0_dbhz)
|
||||
indices = np.arange(len(observations_mps), dtype=int)
|
||||
full = _solve_velocity_candidate(design, observations_mps, sigma, indices)
|
||||
if not robust or len(indices) < 6:
|
||||
return full
|
||||
|
||||
normalized = np.abs(full.residual_mps) / sigma
|
||||
if float(np.max(normalized)) <= 3.5:
|
||||
return full
|
||||
|
||||
# The largest full-fit residual is not necessarily the bad satellite:
|
||||
# WLS leverage can distribute one channel's error over the other rows.
|
||||
# Compare every leave-one-out hypothesis and use the one with the lowest
|
||||
# normalized post-fit residual energy. The firmware robust velocity WLS
|
||||
# uses the same trigger and candidate selection.
|
||||
candidates: list[tuple[float, int, VelocityWlsResult]] = []
|
||||
for excluded in indices:
|
||||
used = indices[indices != excluded]
|
||||
candidate = _solve_velocity_candidate(design, observations_mps, sigma, used)
|
||||
score = float(
|
||||
np.sum(np.square(candidate.residual_mps / sigma[used]))
|
||||
)
|
||||
candidates.append((score, int(excluded), candidate))
|
||||
_score, excluded, candidate = min(candidates, key=lambda item: item[0])
|
||||
return VelocityWlsResult(
|
||||
**{
|
||||
**candidate.__dict__,
|
||||
"excluded_satellite": excluded,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def fusion_velocity_covariance(covariance_mps2: np.ndarray) -> np.ndarray:
|
||||
"""Apply fusion_gps_adapter.c's ENU velocity covariance sanitizer floors."""
|
||||
|
||||
output = np.asarray(covariance_mps2, dtype=float).copy()
|
||||
variance_floor = np.square(
|
||||
np.asarray(
|
||||
[
|
||||
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
|
||||
MINIMUM_HORIZONTAL_VELOCITY_STD_MPS,
|
||||
MINIMUM_VERTICAL_VELOCITY_STD_MPS,
|
||||
]
|
||||
)
|
||||
)
|
||||
for axis in range(3):
|
||||
output[axis, axis] = max(output[axis, axis], variance_floor[axis])
|
||||
for row in range(3):
|
||||
for column in range(row + 1, 3):
|
||||
covariance = 0.5 * (output[row, column] + output[column, row])
|
||||
limit = 0.95 * np.sqrt(output[row, row] * output[column, column])
|
||||
covariance = float(np.clip(covariance, -limit, limit))
|
||||
output[row, column] = covariance
|
||||
output[column, row] = covariance
|
||||
return output
|
||||
|
||||
|
||||
def _in_gap(scenario: FlightScenario, time_s: float) -> bool:
|
||||
return any(start <= time_s < stop for start, stop in scenario.gps_gaps)
|
||||
|
||||
|
||||
def _truth_trajectory(scenario: FlightScenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
time_s = np.arange(0.0, scenario.duration_s, IMU_DT_S)
|
||||
acceleration = np.asarray([scenario.acceleration(float(time)) for time in time_s])
|
||||
velocity = np.empty((len(time_s), 3), dtype=float)
|
||||
velocity[0] = scenario.initial_velocity_mps
|
||||
velocity[1:] = velocity[0] + np.cumsum(acceleration[:-1] * IMU_DT_S, axis=0)
|
||||
return time_s, acceleration, velocity
|
||||
|
||||
|
||||
def run_doppler_eskf_scenario(
|
||||
scenario: FlightScenario,
|
||||
seed: int,
|
||||
paths: tuple[DopplerPath, ...] = PATH_ORDER,
|
||||
) -> dict[DopplerPath, DopplerEskfTrace]:
|
||||
rng = np.random.default_rng(seed)
|
||||
time_s, truth_acceleration, truth_velocity = _truth_trajectory(scenario)
|
||||
satellite_count = len(local_los_matrix())
|
||||
baseline_cn0 = np.asarray([47.0, 45.0, 43.0, 38.0, 46.0, 42.0, 40.0, 36.0])
|
||||
filtered_cn0 = baseline_cn0.copy()
|
||||
fixed_measurement: np.ndarray | None = None
|
||||
adaptive_measurement: np.ndarray | None = None
|
||||
latest_raw: np.ndarray | None = None
|
||||
alpha_selector = AdaptiveAlpha()
|
||||
latest_alpha = FIXED_DOPPLER_ALPHA
|
||||
|
||||
initial_error = rng.normal(0.0, 0.8, 3)
|
||||
filters = {
|
||||
path: VelocityBiasEskf.initialize(
|
||||
scenario.initial_velocity_mps,
|
||||
initial_error,
|
||||
)
|
||||
for path in paths
|
||||
}
|
||||
fused_history = {
|
||||
path: np.empty((len(time_s), 3), dtype=float)
|
||||
for path in paths
|
||||
}
|
||||
fused_covariance_history = {
|
||||
path: np.empty((len(time_s), 3, 3), dtype=float)
|
||||
for path in paths
|
||||
}
|
||||
wls_times = {path: [] for path in paths}
|
||||
wls_velocity = {path: [] for path in paths}
|
||||
wls_clock_drift = {path: [] for path in paths}
|
||||
wls_covariance = {path: [] for path in paths}
|
||||
wls_covariance_scale = {path: [] for path in paths}
|
||||
wls_nis = {path: [] for path in paths}
|
||||
wls_accepted = {path: [] for path in paths}
|
||||
wls_alpha = {path: [] for path in paths}
|
||||
excluded = {path: [] for path in paths}
|
||||
|
||||
true_accel_bias = rng.normal(0.0, 0.04, 3)
|
||||
latest_acceleration_measurement = truth_acceleration[0] + true_accel_bias
|
||||
los = local_los_matrix()
|
||||
|
||||
for sample in range(len(time_s)):
|
||||
if sample > 0:
|
||||
true_accel_bias += rng.normal(
|
||||
0.0,
|
||||
ACCEL_BIAS_RANDOM_WALK * np.sqrt(IMU_DT_S),
|
||||
3,
|
||||
)
|
||||
imu_noise_std = PROCESS_ACCEL_NOISE_DENSITY / np.sqrt(IMU_DT_S)
|
||||
latest_acceleration_measurement = (
|
||||
truth_acceleration[sample - 1]
|
||||
+ true_accel_bias
|
||||
+ rng.normal(0.0, imu_noise_std, 3)
|
||||
)
|
||||
for path in paths:
|
||||
filters[path].propagate(latest_acceleration_measurement, IMU_DT_S)
|
||||
|
||||
if sample % TRACKING_STEPS == 0:
|
||||
current_time = float(time_s[sample])
|
||||
true_cn0 = baseline_cn0 + scenario.cn0_offset(current_time)
|
||||
raw_cn0 = np.clip(true_cn0 + rng.normal(0.0, 1.2, satellite_count), 10.0, 60.0)
|
||||
filtered_cn0 = CN0_ALPHA * raw_cn0 + (1.0 - CN0_ALPHA) * filtered_cn0
|
||||
nominal_sigma = velocity_sigma_mps(true_cn0)
|
||||
actual_sigma = nominal_sigma * np.asarray(
|
||||
[
|
||||
scenario.noise_multiplier(current_time, satellite)
|
||||
for satellite in range(satellite_count)
|
||||
]
|
||||
)
|
||||
raw = (
|
||||
-los @ truth_velocity[sample]
|
||||
+ scenario.receiver_clock_drift_mps(current_time)
|
||||
+ rng.normal(0.0, actual_sigma)
|
||||
+ np.asarray(
|
||||
[
|
||||
scenario.outlier_mps(current_time, satellite)
|
||||
for satellite in range(satellite_count)
|
||||
]
|
||||
)
|
||||
)
|
||||
if fixed_measurement is None:
|
||||
fixed_measurement = raw.copy()
|
||||
adaptive_measurement = raw.copy()
|
||||
else:
|
||||
fixed_measurement += FIXED_DOPPLER_ALPHA * (raw - fixed_measurement)
|
||||
latest_alpha = alpha_selector.update(
|
||||
latest_acceleration_measurement,
|
||||
float(np.median(filtered_cn0)),
|
||||
)
|
||||
adaptive_measurement += latest_alpha * (raw - adaptive_measurement)
|
||||
latest_raw = raw
|
||||
|
||||
if (
|
||||
sample > 0
|
||||
and sample % WLS_STEPS == 0
|
||||
and latest_raw is not None
|
||||
and not _in_gap(scenario, float(time_s[sample]))
|
||||
):
|
||||
for path in paths:
|
||||
if path == DopplerPath.FIXED_IIR:
|
||||
observations = fixed_measurement
|
||||
elif path == DopplerPath.ADAPTIVE_IIR:
|
||||
observations = adaptive_measurement
|
||||
else:
|
||||
observations = latest_raw
|
||||
result = velocity_wls(
|
||||
np.asarray(observations),
|
||||
filtered_cn0,
|
||||
robust=(path == DopplerPath.RAW_ROBUST),
|
||||
)
|
||||
accepted = filters[path].update_velocity(
|
||||
result.velocity_mps,
|
||||
fusion_velocity_covariance(result.covariance_mps2),
|
||||
)
|
||||
wls_times[path].append(float(time_s[sample]))
|
||||
wls_velocity[path].append(result.velocity_mps)
|
||||
wls_clock_drift[path].append(result.clock_drift_mps)
|
||||
wls_covariance[path].append(result.covariance_mps2)
|
||||
wls_covariance_scale[path].append(result.covariance_scale)
|
||||
wls_nis[path].append(filters[path].last_nis)
|
||||
wls_accepted[path].append(accepted)
|
||||
wls_alpha[path].append(
|
||||
latest_alpha
|
||||
if path == DopplerPath.ADAPTIVE_IIR
|
||||
else (FIXED_DOPPLER_ALPHA if path == DopplerPath.FIXED_IIR else 1.0)
|
||||
)
|
||||
excluded[path].append(
|
||||
-1 if result.excluded_satellite is None else result.excluded_satellite
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
fused_history[path][sample] = filters[path].velocity_mps
|
||||
fused_covariance_history[path][sample] = filters[path].covariance[:3, :3]
|
||||
|
||||
return {
|
||||
path: DopplerEskfTrace(
|
||||
scenario=scenario.name,
|
||||
path=path,
|
||||
time_s=time_s,
|
||||
truth_velocity_mps=truth_velocity,
|
||||
fused_velocity_mps=fused_history[path],
|
||||
fused_velocity_covariance_mps2=fused_covariance_history[path],
|
||||
acceleration_mps2=truth_acceleration,
|
||||
wls_time_s=np.asarray(wls_times[path]),
|
||||
wls_velocity_mps=np.asarray(wls_velocity[path]),
|
||||
wls_clock_drift_mps=np.asarray(wls_clock_drift[path]),
|
||||
wls_covariance_mps2=np.asarray(wls_covariance[path]),
|
||||
wls_covariance_scale=np.asarray(wls_covariance_scale[path]),
|
||||
wls_nis=np.asarray(wls_nis[path]),
|
||||
wls_accepted=np.asarray(wls_accepted[path], dtype=bool),
|
||||
adaptive_alpha=np.asarray(wls_alpha[path]),
|
||||
excluded_satellites=np.asarray(excluded[path], dtype=int),
|
||||
)
|
||||
for path in paths
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
"""Python mirror of the firmware's code-measurement WLS policy.
|
||||
|
||||
The model copies the receiver-specific parts of NavEquations.c:
|
||||
|
||||
* four states: ECEF x/y/z and common receiver clock bias in metres;
|
||||
* URA + elevation + C/N0 variance model;
|
||||
* normalized weights for the numerical solve and absolute weights for
|
||||
covariance/integrity;
|
||||
* the 99-percent chi-square table;
|
||||
* single-satellite leave-one-out FDE only when at least seven measurements
|
||||
are available.
|
||||
|
||||
Atmospheric and satellite-clock corrections are represented as already
|
||||
corrected measurement errors. That keeps this study focused on tracking
|
||||
quality rather than duplicating the existing PPS orbit/atmosphere model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
URA_SIGMA_M = np.asarray(
|
||||
[
|
||||
2.4,
|
||||
3.4,
|
||||
4.85,
|
||||
6.85,
|
||||
9.65,
|
||||
13.65,
|
||||
24.0,
|
||||
48.0,
|
||||
96.0,
|
||||
192.0,
|
||||
384.0,
|
||||
768.0,
|
||||
1536.0,
|
||||
3072.0,
|
||||
6144.0,
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
CHI_SQUARE_99 = np.asarray(
|
||||
[0.0, 6.634896601, 9.210340372, 11.34486673, 13.27670414, 15.08627247],
|
||||
dtype=float,
|
||||
)
|
||||
|
||||
|
||||
class SigmaMethod(str, Enum):
|
||||
CURRENT = "current_cn0"
|
||||
NAIVE_ADD_DLL = "naive_add_dll"
|
||||
DLL_EXCESS = "dll_excess"
|
||||
DLL_EXCESS_GUARD = "dll_excess_guard"
|
||||
ORACLE = "oracle_variance"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SigmaInputs:
|
||||
ura_index: np.ndarray
|
||||
elevation_deg: np.ndarray
|
||||
cn0_dbhz: np.ndarray
|
||||
dll_variance_ratio: np.ndarray
|
||||
dll_mean_z: np.ndarray
|
||||
actual_jitter_multiplier: np.ndarray
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WlsSolution:
|
||||
position_ecef_m: np.ndarray
|
||||
clock_bias_m: float
|
||||
residual_m: np.ndarray
|
||||
sigma_m: np.ndarray
|
||||
covariance: np.ndarray
|
||||
chi_square: float
|
||||
chi_square_ratio: float
|
||||
chi_square_pass: bool
|
||||
valid: bool
|
||||
used_indices: np.ndarray
|
||||
excluded_index: int | None
|
||||
|
||||
|
||||
def cn0_code_sigma_m(cn0_dbhz: np.ndarray | float) -> np.ndarray:
|
||||
sigma = 3.0 * np.power(10.0, (45.0 - np.asarray(cn0_dbhz)) / 20.0)
|
||||
return np.clip(sigma, 0.5, 30.0)
|
||||
|
||||
|
||||
def elevation_sigma_m(elevation_deg: np.ndarray | float) -> np.ndarray:
|
||||
bounded = np.clip(np.asarray(elevation_deg), 10.0, 90.0)
|
||||
return 3.0 / np.sin(np.deg2rad(bounded))
|
||||
|
||||
|
||||
def firmware_sigma_m(
|
||||
inputs: SigmaInputs,
|
||||
method: SigmaMethod,
|
||||
) -> np.ndarray:
|
||||
ura = URA_SIGMA_M[np.asarray(inputs.ura_index, dtype=int)]
|
||||
elevation = elevation_sigma_m(inputs.elevation_deg)
|
||||
code = cn0_code_sigma_m(inputs.cn0_dbhz)
|
||||
ratio = np.clip(np.asarray(inputs.dll_variance_ratio), 0.05, 25.0)
|
||||
|
||||
if method == SigmaMethod.CURRENT:
|
||||
code_variance_factor = np.ones_like(code)
|
||||
elif method == SigmaMethod.NAIVE_ADD_DLL:
|
||||
# Deliberately included as a negative control: at ratio=1 this counts
|
||||
# the C/N0-predicted thermal jitter twice.
|
||||
code_variance_factor = 1.0 + ratio
|
||||
elif method == SigmaMethod.DLL_EXCESS:
|
||||
# Only the measured excess over the C/N0 expectation is added.
|
||||
code_variance_factor = np.maximum(1.0, ratio)
|
||||
elif method == SigmaMethod.DLL_EXCESS_GUARD:
|
||||
mean_factor = np.square(
|
||||
np.maximum(1.0, np.asarray(inputs.dll_mean_z) / 3.0)
|
||||
)
|
||||
code_variance_factor = np.maximum.reduce(
|
||||
[np.ones_like(code), ratio, mean_factor]
|
||||
)
|
||||
elif method == SigmaMethod.ORACLE:
|
||||
code_variance_factor = np.square(
|
||||
np.maximum(0.05, np.asarray(inputs.actual_jitter_multiplier))
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"unsupported sigma method: {method}")
|
||||
|
||||
return np.sqrt(
|
||||
ura * ura
|
||||
+ elevation * elevation
|
||||
+ code * code * code_variance_factor
|
||||
)
|
||||
|
||||
|
||||
def ecef_to_lla(ecef_m: np.ndarray) -> tuple[float, float, float]:
|
||||
x, y, z = np.asarray(ecef_m, dtype=float)
|
||||
a = 6_378_137.0
|
||||
inverse_flattening = 298.257223563
|
||||
flattening = 1.0 / inverse_flattening
|
||||
eccentricity_squared = flattening * (2.0 - flattening)
|
||||
longitude = np.arctan2(y, x)
|
||||
horizontal = np.hypot(x, y)
|
||||
latitude = np.arctan2(
|
||||
z,
|
||||
horizontal * (1.0 - eccentricity_squared),
|
||||
)
|
||||
height = 0.0
|
||||
for _ in range(12):
|
||||
sine = np.sin(latitude)
|
||||
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
|
||||
height = horizontal / max(np.cos(latitude), 1.0e-15) - prime_vertical
|
||||
next_latitude = np.arctan2(
|
||||
z,
|
||||
horizontal
|
||||
* (
|
||||
1.0
|
||||
- eccentricity_squared
|
||||
* prime_vertical
|
||||
/ max(prime_vertical + height, 1.0)
|
||||
),
|
||||
)
|
||||
if abs(next_latitude - latitude) < 1.0e-14:
|
||||
latitude = next_latitude
|
||||
break
|
||||
latitude = next_latitude
|
||||
return (
|
||||
float(np.rad2deg(latitude)),
|
||||
float(np.rad2deg(longitude)),
|
||||
float(height),
|
||||
)
|
||||
|
||||
|
||||
def lla_to_ecef(latitude_deg: float, longitude_deg: float, height_m: float) -> np.ndarray:
|
||||
a = 6_378_137.0
|
||||
inverse_flattening = 298.257223563
|
||||
flattening = 1.0 / inverse_flattening
|
||||
eccentricity_squared = flattening * (2.0 - flattening)
|
||||
latitude = np.deg2rad(latitude_deg)
|
||||
longitude = np.deg2rad(longitude_deg)
|
||||
sine = np.sin(latitude)
|
||||
cosine = np.cos(latitude)
|
||||
prime_vertical = a / np.sqrt(1.0 - eccentricity_squared * sine * sine)
|
||||
return np.asarray(
|
||||
[
|
||||
(prime_vertical + height_m) * cosine * np.cos(longitude),
|
||||
(prime_vertical + height_m) * cosine * np.sin(longitude),
|
||||
(
|
||||
prime_vertical * (1.0 - eccentricity_squared)
|
||||
+ height_m
|
||||
)
|
||||
* sine,
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
|
||||
|
||||
def enu_axes(receiver_ecef_m: np.ndarray) -> np.ndarray:
|
||||
latitude_deg, longitude_deg, _ = ecef_to_lla(receiver_ecef_m)
|
||||
latitude = np.deg2rad(latitude_deg)
|
||||
longitude = np.deg2rad(longitude_deg)
|
||||
east = np.asarray([-np.sin(longitude), np.cos(longitude), 0.0])
|
||||
north = np.asarray(
|
||||
[
|
||||
-np.sin(latitude) * np.cos(longitude),
|
||||
-np.sin(latitude) * np.sin(longitude),
|
||||
np.cos(latitude),
|
||||
]
|
||||
)
|
||||
up = np.asarray(
|
||||
[
|
||||
np.cos(latitude) * np.cos(longitude),
|
||||
np.cos(latitude) * np.sin(longitude),
|
||||
np.sin(latitude),
|
||||
]
|
||||
)
|
||||
return np.vstack([east, north, up])
|
||||
|
||||
|
||||
def satellites_from_azel(
|
||||
receiver_ecef_m: np.ndarray,
|
||||
azimuth_deg: np.ndarray,
|
||||
elevation_deg: np.ndarray,
|
||||
range_m: float = 21_000_000.0,
|
||||
) -> np.ndarray:
|
||||
axes = enu_axes(receiver_ecef_m)
|
||||
satellites: list[np.ndarray] = []
|
||||
for azimuth, elevation in zip(azimuth_deg, elevation_deg):
|
||||
azimuth_rad = np.deg2rad(azimuth)
|
||||
elevation_rad = np.deg2rad(elevation)
|
||||
los_enu = np.asarray(
|
||||
[
|
||||
np.cos(elevation_rad) * np.sin(azimuth_rad),
|
||||
np.cos(elevation_rad) * np.cos(azimuth_rad),
|
||||
np.sin(elevation_rad),
|
||||
]
|
||||
)
|
||||
los_ecef = axes.T @ los_enu
|
||||
satellites.append(receiver_ecef_m + range_m * los_ecef)
|
||||
return np.asarray(satellites)
|
||||
|
||||
|
||||
def position_error_enu_m(
|
||||
estimated_ecef_m: np.ndarray,
|
||||
true_ecef_m: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
return enu_axes(true_ecef_m) @ (estimated_ecef_m - true_ecef_m)
|
||||
|
||||
|
||||
def _candidate(
|
||||
satellite_positions_m: np.ndarray,
|
||||
observations_m: np.ndarray,
|
||||
sigma_m: np.ndarray,
|
||||
initial_state: np.ndarray,
|
||||
used_indices: np.ndarray,
|
||||
) -> WlsSolution:
|
||||
state = np.asarray(initial_state, dtype=float).copy()
|
||||
weights = 1.0 / np.square(sigma_m)
|
||||
design = np.empty((len(observations_m), 4), dtype=float)
|
||||
|
||||
for _ in range(10):
|
||||
difference = satellite_positions_m - state[:3]
|
||||
geometric_range = np.linalg.norm(difference, axis=1)
|
||||
design[:, :3] = -difference / geometric_range[:, None]
|
||||
design[:, 3] = 1.0
|
||||
residual = observations_m - geometric_range - state[3]
|
||||
normal = design.T @ (weights[:, None] * design)
|
||||
rhs = design.T @ (weights * residual)
|
||||
update = np.linalg.solve(normal, rhs)
|
||||
state += update
|
||||
if np.linalg.norm(update) < 1.0e-4:
|
||||
break
|
||||
|
||||
difference = satellite_positions_m - state[:3]
|
||||
geometric_range = np.linalg.norm(difference, axis=1)
|
||||
design[:, :3] = -difference / geometric_range[:, None]
|
||||
design[:, 3] = 1.0
|
||||
residual = observations_m - geometric_range - state[3]
|
||||
normal = design.T @ (weights[:, None] * design)
|
||||
covariance = np.linalg.inv(normal)
|
||||
chi_square = float(np.sum(weights * np.square(residual)))
|
||||
degrees_of_freedom = len(observations_m) - 4
|
||||
threshold = (
|
||||
float(CHI_SQUARE_99[degrees_of_freedom])
|
||||
if 0 < degrees_of_freedom < len(CHI_SQUARE_99)
|
||||
else 0.0
|
||||
)
|
||||
chi_square_pass = degrees_of_freedom > 0 and chi_square <= threshold
|
||||
valid = (
|
||||
(len(observations_m) == 4 and degrees_of_freedom == 0)
|
||||
or chi_square_pass
|
||||
)
|
||||
ratio = chi_square / threshold if threshold > 0.0 else float("inf")
|
||||
return WlsSolution(
|
||||
position_ecef_m=state[:3].copy(),
|
||||
clock_bias_m=float(state[3]),
|
||||
residual_m=residual.copy(),
|
||||
sigma_m=sigma_m.copy(),
|
||||
covariance=covariance,
|
||||
chi_square=chi_square,
|
||||
chi_square_ratio=ratio,
|
||||
chi_square_pass=chi_square_pass,
|
||||
valid=valid,
|
||||
used_indices=used_indices.copy(),
|
||||
excluded_index=None,
|
||||
)
|
||||
|
||||
|
||||
def firmware_wls(
|
||||
satellite_positions_m: np.ndarray,
|
||||
observations_m: np.ndarray,
|
||||
sigma_m: np.ndarray,
|
||||
initial_state: np.ndarray,
|
||||
*,
|
||||
enable_fde: bool = True,
|
||||
) -> WlsSolution:
|
||||
count = len(observations_m)
|
||||
if count < 4:
|
||||
raise ValueError("position WLS needs at least four satellites")
|
||||
full_indices = np.arange(count, dtype=int)
|
||||
full = _candidate(
|
||||
satellite_positions_m,
|
||||
observations_m,
|
||||
sigma_m,
|
||||
initial_state,
|
||||
full_indices,
|
||||
)
|
||||
if full.chi_square_pass or not enable_fde or count < 7:
|
||||
return full
|
||||
|
||||
selected = full
|
||||
passing_found = False
|
||||
full_state = np.concatenate([full.position_ecef_m, [full.clock_bias_m]])
|
||||
for excluded in range(count):
|
||||
used = full_indices[full_indices != excluded]
|
||||
candidate = _candidate(
|
||||
satellite_positions_m[used],
|
||||
observations_m[used],
|
||||
sigma_m[used],
|
||||
full_state,
|
||||
used,
|
||||
)
|
||||
candidate = WlsSolution(
|
||||
**{
|
||||
**candidate.__dict__,
|
||||
"excluded_index": excluded,
|
||||
}
|
||||
)
|
||||
if candidate.chi_square_pass:
|
||||
if not passing_found or candidate.chi_square_ratio < selected.chi_square_ratio:
|
||||
selected = candidate
|
||||
passing_found = True
|
||||
elif not passing_found and candidate.chi_square_ratio < selected.chi_square_ratio:
|
||||
selected = candidate
|
||||
return selected
|
||||
@@ -0,0 +1,368 @@
|
||||
"""Synthetic code-tracking scenarios shared by the analysis and tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .firmware_wls_model import (
|
||||
SigmaInputs,
|
||||
SigmaMethod,
|
||||
URA_SIGMA_M,
|
||||
cn0_code_sigma_m,
|
||||
elevation_sigma_m,
|
||||
enu_axes,
|
||||
firmware_sigma_m,
|
||||
firmware_wls,
|
||||
lla_to_ecef,
|
||||
position_error_enu_m,
|
||||
satellites_from_azel,
|
||||
)
|
||||
from .rtl_dll_model import expected_discriminator_std, synthesize_dll_window
|
||||
|
||||
|
||||
TRACKING_PERIOD_S = 0.020
|
||||
WLS_PERIOD_S = 0.200
|
||||
TRACKING_UPDATES_PER_WLS = int(round(WLS_PERIOD_S / TRACKING_PERIOD_S))
|
||||
FIRMWARE_CN0_ALPHA = 0.20
|
||||
DLL_RATIO_ALPHA = 0.10
|
||||
DLL_MEAN_ALPHA = 0.20
|
||||
|
||||
GOOD_AZIMUTH_DEG = np.asarray([8.0, 55.0, 104.0, 151.0, 205.0, 253.0, 301.0, 342.0])
|
||||
GOOD_ELEVATION_DEG = np.asarray([73.0, 48.0, 31.0, 18.0, 64.0, 39.0, 24.0, 14.0])
|
||||
POOR_AZIMUTH_DEG = np.asarray([25.0, 46.0, 68.0, 89.0, 112.0, 136.0, 159.0, 182.0])
|
||||
POOR_ELEVATION_DEG = np.asarray([18.0, 22.0, 25.0, 29.0, 32.0, 27.0, 21.0, 16.0])
|
||||
|
||||
|
||||
Profile = Callable[[float, int, float], float]
|
||||
|
||||
|
||||
def _identity(_time_s: float, _satellite: int, baseline: float) -> float:
|
||||
return baseline
|
||||
|
||||
|
||||
def _one(_time_s: float, _satellite: int, _baseline: float) -> float:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _zero(_time_s: float, _satellite: int, _baseline: float) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Scenario:
|
||||
name: str
|
||||
description: str
|
||||
duration_s: float = 80.0
|
||||
poor_geometry: bool = False
|
||||
cn0_profile: Profile = _identity
|
||||
jitter_profile: Profile = _one
|
||||
discriminator_bias_profile: Profile = _zero
|
||||
pseudorange_bias_profile: Profile = _zero
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelQuality:
|
||||
cn0_dbhz: float | None = None
|
||||
dll_ratio: float = 1.0
|
||||
dll_mean_z: float = 0.0
|
||||
|
||||
def update(
|
||||
self,
|
||||
raw_cn0_dbhz: float,
|
||||
dll_samples: np.ndarray,
|
||||
) -> None:
|
||||
if self.cn0_dbhz is None:
|
||||
self.cn0_dbhz = raw_cn0_dbhz
|
||||
else:
|
||||
self.cn0_dbhz = (
|
||||
FIRMWARE_CN0_ALPHA * raw_cn0_dbhz
|
||||
+ (1.0 - FIRMWARE_CN0_ALPHA) * self.cn0_dbhz
|
||||
)
|
||||
|
||||
measured_variance = float(np.var(dll_samples, ddof=1))
|
||||
expected_std = expected_discriminator_std(self.cn0_dbhz)
|
||||
raw_ratio = measured_variance / max(expected_std * expected_std, 1.0e-15)
|
||||
raw_ratio = float(np.clip(raw_ratio, 0.05, 25.0))
|
||||
self.dll_ratio = (
|
||||
DLL_RATIO_ALPHA * raw_ratio
|
||||
+ (1.0 - DLL_RATIO_ALPHA) * self.dll_ratio
|
||||
)
|
||||
|
||||
standard_error = np.sqrt(
|
||||
max(measured_variance, expected_std * expected_std)
|
||||
/ len(dll_samples)
|
||||
)
|
||||
raw_mean_z = abs(float(np.mean(dll_samples))) / max(standard_error, 1.0e-12)
|
||||
raw_mean_z = float(np.clip(raw_mean_z, 0.0, 12.0))
|
||||
self.dll_mean_z = (
|
||||
DLL_MEAN_ALPHA * raw_mean_z
|
||||
+ (1.0 - DLL_MEAN_ALPHA) * self.dll_mean_z
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EpochSample:
|
||||
method: SigmaMethod
|
||||
time_s: float
|
||||
enu_error_m: np.ndarray
|
||||
horizontal_error_m: float
|
||||
position_error_3d_m: float
|
||||
horizontal_sigma_m: float
|
||||
valid: bool
|
||||
chi_square_ratio: float
|
||||
fde_applied: bool
|
||||
mean_measurement_sigma_m: float
|
||||
maximum_dll_ratio: float
|
||||
|
||||
|
||||
def scenario_matrix() -> tuple[Scenario, ...]:
|
||||
return (
|
||||
Scenario(
|
||||
"clean_open_sky",
|
||||
"C/N0 model and actual DLL jitter agree.",
|
||||
),
|
||||
Scenario(
|
||||
"weak_signal",
|
||||
"All channels lose 11 dB; current C/N0 weighting already sees it.",
|
||||
cn0_profile=lambda _t, _s, baseline: baseline - 11.0,
|
||||
),
|
||||
Scenario(
|
||||
"one_channel_hidden_jitter",
|
||||
"One high-C/N0 channel has 4x DLL jitter not visible in C/N0.",
|
||||
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 1 else 1.0,
|
||||
),
|
||||
Scenario(
|
||||
"common_hidden_jitter",
|
||||
"All channels have the same unmodelled 3x DLL jitter.",
|
||||
jitter_profile=lambda _t, _satellite, _b: 3.0,
|
||||
),
|
||||
Scenario(
|
||||
"noise_burst_recovery",
|
||||
"Two channels have 5x jitter from 22 s to 45 s, then recover.",
|
||||
jitter_profile=lambda time_s, satellite, _b: (
|
||||
5.0
|
||||
if 22.0 <= time_s < 45.0 and satellite in (1, 5)
|
||||
else 1.0
|
||||
),
|
||||
),
|
||||
Scenario(
|
||||
"dll_transient_stress",
|
||||
"A channel has discriminator mean without excess variance during transient stress.",
|
||||
discriminator_bias_profile=lambda time_s, satellite, _b: (
|
||||
0.055
|
||||
if 25.0 <= time_s < 34.0 and satellite == 3
|
||||
else 0.0
|
||||
),
|
||||
pseudorange_bias_profile=lambda time_s, satellite, _b: (
|
||||
9.0
|
||||
if 25.0 <= time_s < 34.0 and satellite == 3
|
||||
else 0.0
|
||||
),
|
||||
),
|
||||
Scenario(
|
||||
"stable_multipath_bias",
|
||||
"Low-jitter, high-C/N0 channel has a stable 14 m code bias.",
|
||||
pseudorange_bias_profile=lambda _t, satellite, _b: (
|
||||
14.0 if satellite == 6 else 0.0
|
||||
),
|
||||
),
|
||||
Scenario(
|
||||
"mixed_multipath",
|
||||
"One channel has both a 10 m bias and 3x excess jitter.",
|
||||
jitter_profile=lambda _t, satellite, _b: 3.0 if satellite == 2 else 1.0,
|
||||
pseudorange_bias_profile=lambda _t, satellite, _b: (
|
||||
10.0 if satellite == 2 else 0.0
|
||||
),
|
||||
),
|
||||
Scenario(
|
||||
"poor_geometry_hidden_jitter",
|
||||
"Clustered geometry plus one channel with unmodelled 4x jitter.",
|
||||
poor_geometry=True,
|
||||
jitter_profile=lambda _t, satellite, _b: 4.0 if satellite == 4 else 1.0,
|
||||
),
|
||||
Scenario(
|
||||
"cn0_step_recovery",
|
||||
"C/N0 is initially weak and improves; no hidden DLL noise exists.",
|
||||
cn0_profile=lambda time_s, _satellite, baseline: (
|
||||
baseline - 12.0 if time_s < 30.0 else baseline
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _geometry(scenario: Scenario) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
receiver = lla_to_ecef(39.9208, 32.8541, 930.0)
|
||||
if scenario.poor_geometry:
|
||||
azimuth = POOR_AZIMUTH_DEG
|
||||
elevation = POOR_ELEVATION_DEG
|
||||
else:
|
||||
azimuth = GOOD_AZIMUTH_DEG
|
||||
elevation = GOOD_ELEVATION_DEG
|
||||
satellites = satellites_from_azel(receiver, azimuth, elevation)
|
||||
return receiver, satellites, elevation.copy()
|
||||
|
||||
|
||||
def run_scenario(
|
||||
scenario: Scenario,
|
||||
seed: int,
|
||||
methods: tuple[SigmaMethod, ...] = tuple(SigmaMethod),
|
||||
) -> list[EpochSample]:
|
||||
rng = np.random.default_rng(seed)
|
||||
receiver, satellites, elevations = _geometry(scenario)
|
||||
satellite_count = len(satellites)
|
||||
geometric_ranges = np.linalg.norm(satellites - receiver, axis=1)
|
||||
baseline_cn0 = np.clip(34.0 + 0.16 * elevations, 30.0, 48.0)
|
||||
ura_indices = np.zeros(satellite_count, dtype=np.uint8)
|
||||
quality = [ChannelQuality() for _ in range(satellite_count)]
|
||||
|
||||
# These are deliberately slower/non-DLL terms. They prevent a synthetic
|
||||
# white-noise-only experiment from overstating what DLL weighting can fix.
|
||||
ura_error = rng.normal(0.0, 0.45 * URA_SIGMA_M[0], satellite_count)
|
||||
atmospheric_state = rng.normal(
|
||||
0.0,
|
||||
0.35 * elevation_sigma_m(elevations),
|
||||
satellite_count,
|
||||
)
|
||||
code_noise_state = np.zeros(satellite_count)
|
||||
initial_state = np.concatenate(
|
||||
[
|
||||
receiver + np.asarray([450.0, -300.0, 220.0]),
|
||||
[0.0],
|
||||
]
|
||||
)
|
||||
method_state = {method: initial_state.copy() for method in methods}
|
||||
samples: list[EpochSample] = []
|
||||
clock_bias_m = 82.0
|
||||
receiver_enu_axes = enu_axes(receiver)
|
||||
|
||||
tracking_steps = int(round(scenario.duration_s / TRACKING_PERIOD_S))
|
||||
latest_actual_jitter = np.ones(satellite_count)
|
||||
for tracking_step in range(tracking_steps):
|
||||
time_s = tracking_step * TRACKING_PERIOD_S
|
||||
for satellite in range(satellite_count):
|
||||
true_cn0 = scenario.cn0_profile(
|
||||
time_s,
|
||||
satellite,
|
||||
float(baseline_cn0[satellite]),
|
||||
)
|
||||
actual_jitter = scenario.jitter_profile(
|
||||
time_s,
|
||||
satellite,
|
||||
1.0,
|
||||
)
|
||||
discriminator_bias = scenario.discriminator_bias_profile(
|
||||
time_s,
|
||||
satellite,
|
||||
0.0,
|
||||
)
|
||||
latest_actual_jitter[satellite] = actual_jitter
|
||||
dll_window = synthesize_dll_window(
|
||||
rng,
|
||||
cn0_dbhz=true_cn0,
|
||||
jitter_multiplier=actual_jitter,
|
||||
discriminator_bias=discriminator_bias,
|
||||
)
|
||||
# The hardware NWPR C/N0 estimate is noisy; firmware applies alpha=0.2.
|
||||
raw_cn0 = float(np.clip(true_cn0 + rng.normal(0.0, 1.3), 10.0, 60.0))
|
||||
quality[satellite].update(raw_cn0, dll_window.samples)
|
||||
|
||||
if (tracking_step + 1) % TRACKING_UPDATES_PER_WLS != 0:
|
||||
continue
|
||||
|
||||
true_cn0 = np.asarray(
|
||||
[
|
||||
scenario.cn0_profile(
|
||||
time_s,
|
||||
satellite,
|
||||
float(baseline_cn0[satellite]),
|
||||
)
|
||||
for satellite in range(satellite_count)
|
||||
]
|
||||
)
|
||||
actual_code_sigma = cn0_code_sigma_m(true_cn0) * latest_actual_jitter
|
||||
innovation = rng.normal(0.0, actual_code_sigma)
|
||||
code_noise_state = (
|
||||
0.25 * code_noise_state
|
||||
+ np.sqrt(1.0 - 0.25**2) * innovation
|
||||
)
|
||||
atmospheric_innovation = rng.normal(
|
||||
0.0,
|
||||
0.35 * elevation_sigma_m(elevations),
|
||||
)
|
||||
atmospheric_state = (
|
||||
0.97 * atmospheric_state
|
||||
+ np.sqrt(1.0 - 0.97**2) * atmospheric_innovation
|
||||
)
|
||||
pseudorange_bias = np.asarray(
|
||||
[
|
||||
scenario.pseudorange_bias_profile(
|
||||
time_s,
|
||||
satellite,
|
||||
0.0,
|
||||
)
|
||||
for satellite in range(satellite_count)
|
||||
]
|
||||
)
|
||||
observations = (
|
||||
geometric_ranges
|
||||
+ clock_bias_m
|
||||
+ ura_error
|
||||
+ atmospheric_state
|
||||
+ code_noise_state
|
||||
+ pseudorange_bias
|
||||
)
|
||||
sigma_inputs = SigmaInputs(
|
||||
ura_index=ura_indices,
|
||||
elevation_deg=elevations,
|
||||
cn0_dbhz=np.asarray([item.cn0_dbhz for item in quality], dtype=float),
|
||||
dll_variance_ratio=np.asarray([item.dll_ratio for item in quality]),
|
||||
dll_mean_z=np.asarray([item.dll_mean_z for item in quality]),
|
||||
actual_jitter_multiplier=latest_actual_jitter.copy(),
|
||||
)
|
||||
|
||||
for method in methods:
|
||||
sigma = firmware_sigma_m(sigma_inputs, method)
|
||||
solution = firmware_wls(
|
||||
satellites,
|
||||
observations,
|
||||
sigma,
|
||||
method_state[method],
|
||||
)
|
||||
# NavEquations.c updates its persistent WLS seed only after an
|
||||
# integrity-valid solution.
|
||||
if solution.valid:
|
||||
method_state[method] = np.concatenate(
|
||||
[solution.position_ecef_m, [solution.clock_bias_m]]
|
||||
)
|
||||
enu_error = position_error_enu_m(solution.position_ecef_m, receiver)
|
||||
enu_covariance = (
|
||||
# The top-left block is ECEF position covariance.
|
||||
# R * Q * R' maps it to ENU.
|
||||
receiver_enu_axes
|
||||
@ solution.covariance[:3, :3]
|
||||
@ receiver_enu_axes.T
|
||||
)
|
||||
horizontal_sigma = float(
|
||||
np.sqrt(max(0.0, enu_covariance[0, 0] + enu_covariance[1, 1]))
|
||||
)
|
||||
samples.append(
|
||||
EpochSample(
|
||||
method=method,
|
||||
time_s=time_s,
|
||||
enu_error_m=enu_error,
|
||||
horizontal_error_m=float(np.linalg.norm(enu_error[:2])),
|
||||
position_error_3d_m=float(np.linalg.norm(enu_error)),
|
||||
horizontal_sigma_m=horizontal_sigma,
|
||||
valid=solution.valid,
|
||||
chi_square_ratio=solution.chi_square_ratio,
|
||||
fde_applied=solution.excluded_index is not None,
|
||||
mean_measurement_sigma_m=float(np.mean(sigma)),
|
||||
maximum_dll_ratio=float(
|
||||
max(item.dll_ratio for item in quality)
|
||||
),
|
||||
)
|
||||
)
|
||||
return samples
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 352 KiB |
@@ -0,0 +1,94 @@
|
||||
# GPS code measurement model sonuçları
|
||||
|
||||
Monte Carlo seed sayısı: `12`. İlk 5 saniye metriklere alınmadı.
|
||||
|
||||
## Yöntem karşılaştırması
|
||||
|
||||
| Senaryo | Yöntem | 3D RMS (m) | Mevcuda göre | P95 (m) | Valid (%) | >25 m false-valid (%) |
|
||||
|---|---|---:|---:|---:|---:|---:|
|
||||
| clean_open_sky | Mevcut C/N0 | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
|
||||
| clean_open_sky | Naif DLL ekle | 10.02 | +0.3% | 17.25 | 100.0 | 0.31 |
|
||||
| clean_open_sky | DLL excess | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
|
||||
| clean_open_sky | DLL excess+mean | 10.05 | +0.0% | 17.32 | 100.0 | 0.33 |
|
||||
| clean_open_sky | Oracle variance | 10.05 | +0.0% | 17.35 | 100.0 | 0.31 |
|
||||
| weak_signal | Mevcut C/N0 | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
|
||||
| weak_signal | Naif DLL ekle | 32.31 | +1.3% | 55.46 | 100.0 | 56.18 |
|
||||
| weak_signal | DLL excess | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
|
||||
| weak_signal | DLL excess+mean | 32.65 | +0.3% | 56.12 | 100.0 | 56.22 |
|
||||
| weak_signal | Oracle variance | 32.73 | +0.0% | 56.44 | 100.0 | 56.31 |
|
||||
| one_channel_hidden_jitter | Mevcut C/N0 | 12.27 | +0.0% | 19.94 | 100.0 | 1.07 |
|
||||
| one_channel_hidden_jitter | Naif DLL ekle | 10.49 | +14.5% | 17.75 | 100.0 | 0.33 |
|
||||
| one_channel_hidden_jitter | DLL excess | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
|
||||
| one_channel_hidden_jitter | DLL excess+mean | 10.47 | +14.7% | 17.68 | 100.0 | 0.33 |
|
||||
| one_channel_hidden_jitter | Oracle variance | 10.46 | +14.7% | 17.74 | 100.0 | 0.33 |
|
||||
| common_hidden_jitter | Mevcut C/N0 | 33.79 | +0.0% | 59.76 | 94.8 | 52.40 |
|
||||
| common_hidden_jitter | Naif DLL ekle | 27.61 | +18.3% | 47.56 | 100.0 | 43.93 |
|
||||
| common_hidden_jitter | DLL excess | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
|
||||
| common_hidden_jitter | DLL excess+mean | 27.69 | +18.1% | 47.83 | 100.0 | 43.89 |
|
||||
| common_hidden_jitter | Oracle variance | 27.74 | +17.9% | 47.83 | 100.0 | 43.87 |
|
||||
| noise_burst_recovery | Mevcut C/N0 | 12.96 | +0.0% | 22.13 | 98.9 | 2.58 |
|
||||
| noise_burst_recovery | Naif DLL ekle | 10.52 | +18.8% | 18.03 | 100.0 | 0.53 |
|
||||
| noise_burst_recovery | DLL excess | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
|
||||
| noise_burst_recovery | DLL excess+mean | 10.49 | +19.0% | 18.00 | 100.0 | 0.49 |
|
||||
| noise_burst_recovery | Oracle variance | 10.47 | +19.2% | 17.90 | 100.0 | 0.49 |
|
||||
| dll_transient_stress | Mevcut C/N0 | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
|
||||
| dll_transient_stress | Naif DLL ekle | 10.12 | +0.3% | 17.33 | 100.0 | 0.38 |
|
||||
| dll_transient_stress | DLL excess | 10.14 | +0.0% | 17.38 | 100.0 | 0.38 |
|
||||
| dll_transient_stress | DLL excess+mean | 10.12 | +0.2% | 17.37 | 100.0 | 0.33 |
|
||||
| dll_transient_stress | Oracle variance | 10.15 | +0.0% | 17.43 | 100.0 | 0.36 |
|
||||
| stable_multipath_bias | Mevcut C/N0 | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
|
||||
| stable_multipath_bias | Naif DLL ekle | 12.39 | +1.3% | 20.87 | 100.0 | 1.47 |
|
||||
| stable_multipath_bias | DLL excess | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
|
||||
| stable_multipath_bias | DLL excess+mean | 12.54 | +0.1% | 21.02 | 100.0 | 1.64 |
|
||||
| stable_multipath_bias | Oracle variance | 12.55 | +0.0% | 21.00 | 100.0 | 1.67 |
|
||||
| mixed_multipath | Mevcut C/N0 | 14.43 | +0.0% | 25.38 | 100.0 | 5.64 |
|
||||
| mixed_multipath | Naif DLL ekle | 11.26 | +22.0% | 19.60 | 100.0 | 0.73 |
|
||||
| mixed_multipath | DLL excess | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
|
||||
| mixed_multipath | DLL excess+mean | 11.07 | +23.3% | 19.23 | 100.0 | 0.73 |
|
||||
| mixed_multipath | Oracle variance | 11.06 | +23.4% | 19.20 | 100.0 | 0.73 |
|
||||
| poor_geometry_hidden_jitter | Mevcut C/N0 | 293.82 | +0.0% | 574.96 | 100.0 | 94.31 |
|
||||
| poor_geometry_hidden_jitter | Naif DLL ekle | 206.13 | +29.8% | 405.15 | 100.0 | 92.42 |
|
||||
| poor_geometry_hidden_jitter | DLL excess | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
|
||||
| poor_geometry_hidden_jitter | DLL excess+mean | 196.87 | +33.0% | 390.64 | 100.0 | 91.40 |
|
||||
| poor_geometry_hidden_jitter | Oracle variance | 196.20 | +33.2% | 386.58 | 100.0 | 91.27 |
|
||||
| cn0_step_recovery | Mevcut C/N0 | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
|
||||
| cn0_step_recovery | Naif DLL ekle | 22.27 | +1.4% | 47.59 | 100.0 | 21.42 |
|
||||
| cn0_step_recovery | DLL excess | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
|
||||
| cn0_step_recovery | DLL excess+mean | 22.59 | -0.0% | 47.91 | 100.0 | 21.49 |
|
||||
| cn0_step_recovery | Oracle variance | 22.59 | +0.0% | 47.95 | 100.0 | 21.62 |
|
||||
|
||||
## Modelden çıkan karar özeti
|
||||
|
||||
- C/N0 ile uyumlu nominal DLL jitter'da `dll_excess`, mevcut çözümü değiştirmemelidir.
|
||||
- Bütün measurement sigmaları aynı sabitle çarpılırsa WLS konum noktası değişmez; yalnız covariance/integrity değişir. Fakat ortak DLL jitter çarpanı, C/N0/elevation bileşimi kanaldan kanala farklı olduğu için toplam sigmaları eşit oranda büyütmez.
|
||||
- Kanal-bağımlı ve C/N0'da görünmeyen excess jitter varsa dinamik sigma o kanalı down-weight ederek konumu iyileştirebilir.
|
||||
- Stable multipath bias düşük DLL variance bırakabildiği için variance-only yöntem çözüm değildir.
|
||||
- 50 Hz'de DLL ratio alpha=0.10 yaklaşık 0.19 s zaman sabiti ve 0.57 s yüzde-95 geçiş verir.
|
||||
- Aşağıdaki yüzdeler seçilen sentetik hata büyüklüklerine koşulludur; kart için kazanç tahmini değildir.
|
||||
|
||||
## Bozulma penceresi odaklı 3D RMS
|
||||
|
||||
| Senaryo/pencere | Mevcut (m) | DLL excess (m) | DLL excess+mean (m) | Oracle variance (m) |
|
||||
|---|---:|---:|---:|---:|
|
||||
| noise_burst_recovery [22,45) s | 17.83 | 11.36 | 11.36 | 11.30 |
|
||||
| dll_transient_stress [25,34) s | 10.83 | 10.82 | 10.65 | 10.83 |
|
||||
| cn0_step_recovery [5,30) s | 36.48 | 36.48 | 36.48 | 36.48 |
|
||||
|
||||
## Code smoothing ve carrier CFO
|
||||
|
||||
| Carrier-rate koşulu | Ham RMSE (m) | Kör IIR RMSE (m) | Raw Doppler RMSE (m) | FW-IIR Doppler RMSE (m) | Kör IIR peak (m) | Raw Doppler peak (m) | FW-IIR Doppler peak (m) |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| cfo_compensated | 3.99 | 248.61 | 0.94 | 16.42 | 414.08 | 2.05 | 84.13 |
|
||||
| cfo_residual_0p5_mps | 3.99 | 248.61 | 1.30 | 16.41 | 414.08 | 2.57 | 83.23 |
|
||||
| cfo_residual_3_mps | 3.99 | 248.61 | 5.48 | 17.06 | 414.08 | 7.07 | 78.73 |
|
||||
|
||||
## Yorum sınırları
|
||||
|
||||
- `current_cn0`, firmware'deki URA/elevation/C/N0 sigma modelidir.
|
||||
- `naive_add_dll`, aynı thermal jitter'ı iki kere saymanın negatif kontrolüdür.
|
||||
- `dll_excess`, yalnız ölçülen DLL variance'ın C/N0 beklentisini aşan kısmını code variance'a ekler.
|
||||
- `dll_excess_guard`, variance'a ek olarak sürekli discriminator mean stress'ini kullanır.
|
||||
- `oracle_variance`, sentetik gerçek jitter çarpanını bilir; yalnız variance kaynakları için erişilebilir üst sınırdır, bias'ı bilmez.
|
||||
- Stable multipath DLL'nin yeni denge noktasında düşük residual variance bırakabilir. Bu nedenle variance-only yönteminin onu çözmesi beklenmez.
|
||||
- Discriminator gürültü ölçeği kart correlator kayıtları gelene kadar kalibrasyon varsayımıdır; oran tabanlı yöntem mutlak ölçeğe göre daha az hassastır.
|
||||
- Sonuçlar bitstream veya firmware değişikliği değildir; karar öncesi executable tasarım keşfidir.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Doppler WLS -> ESKF model sonuçları
|
||||
|
||||
Monte Carlo seed sayısı: `20`; ilk 5 saniye RMS metriklerinden çıkarıldı.
|
||||
50 Hz tracking, önceki `alpha=0.05` Doppler IIR, 5 Hz dört-state velocity WLS, fusion adapter'ın `2/2/3 m/s` ENU sigma tabanları, TDK profiliyle 100 Hz IMU propagation ve 3-DOF `%99.9` NIS kapısı modellenmiştir.
|
||||
|
||||
## Sonuç tablosu
|
||||
|
||||
| Senaryo | Yol | ESKF RMS (m/s) | Dynamic RMS | WLS RMS | Accept (%) | 95% cov. (%) | Exclude (%) |
|
||||
|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| steady_cruise | Önceki sabit IIR | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
|
||||
| steady_cruise | Ham Doppler + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| steady_cruise | Adaptif IIR + ESKF | 0.182 | - | 0.068 | 100.0 | 100.0 | 0.0 |
|
||||
| steady_cruise | Ham + robust WLS + ESKF | 0.190 | - | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| airliner_maneuver | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
|
||||
| airliner_maneuver | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| airliner_maneuver | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
|
||||
| airliner_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| aggressive_maneuver | Önceki sabit IIR | 0.942 | 0.919 | 1.178 | 100.0 | 55.9 | 0.0 |
|
||||
| aggressive_maneuver | Ham Doppler + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| aggressive_maneuver | Adaptif IIR + ESKF | 0.200 | 0.197 | 0.171 | 100.0 | 100.0 | 0.0 |
|
||||
| aggressive_maneuver | Ham + robust WLS + ESKF | 0.190 | 0.186 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| low_cn0_airliner | Önceki sabit IIR | 0.419 | 0.443 | 0.485 | 100.0 | 99.7 | 0.0 |
|
||||
| low_cn0_airliner | Ham Doppler + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
|
||||
| low_cn0_airliner | Adaptif IIR + ESKF | 0.206 | 0.203 | 0.338 | 100.0 | 100.0 | 0.0 |
|
||||
| low_cn0_airliner | Ham + robust WLS + ESKF | 0.251 | 0.242 | 1.317 | 100.0 | 100.0 | 0.0 |
|
||||
| single_satellite_outliers | Önceki sabit IIR | 0.495 | 0.478 | 1.146 | 100.0 | 97.7 | 0.0 |
|
||||
| single_satellite_outliers | Ham Doppler + ESKF | 0.257 | 0.229 | 1.451 | 100.0 | 100.0 | 0.0 |
|
||||
| single_satellite_outliers | Adaptif IIR + ESKF | 0.356 | 0.312 | 1.154 | 100.0 | 100.0 | 0.0 |
|
||||
| single_satellite_outliers | Ham + robust WLS + ESKF | 0.191 | 0.179 | 0.418 | 100.0 | 100.0 | 2.9 |
|
||||
| clock_drift_steps | Önceki sabit IIR | 0.416 | 0.438 | 0.440 | 100.0 | 99.7 | 0.0 |
|
||||
| clock_drift_steps | Ham Doppler + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| clock_drift_steps | Adaptif IIR + ESKF | 0.194 | 0.189 | 0.135 | 100.0 | 100.0 | 0.0 |
|
||||
| clock_drift_steps | Ham + robust WLS + ESKF | 0.190 | 0.179 | 0.416 | 100.0 | 100.0 | 0.0 |
|
||||
| gps_gap_recovery | Önceki sabit IIR | 0.397 | 0.413 | 0.431 | 100.0 | 99.9 | 0.0 |
|
||||
| gps_gap_recovery | Ham Doppler + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
|
||||
| gps_gap_recovery | Adaptif IIR + ESKF | 0.208 | 0.197 | 0.129 | 100.0 | 100.0 | 0.0 |
|
||||
| gps_gap_recovery | Ham + robust WLS + ESKF | 0.208 | 0.192 | 0.417 | 100.0 | 100.0 | 0.0 |
|
||||
| mixed_flight_faults | Önceki sabit IIR | 0.940 | 0.913 | 1.515 | 100.0 | 60.4 | 0.0 |
|
||||
| mixed_flight_faults | Ham Doppler + ESKF | 0.224 | 0.209 | 1.498 | 100.0 | 100.0 | 0.0 |
|
||||
| mixed_flight_faults | Adaptif IIR + ESKF | 0.295 | 0.275 | 0.838 | 100.0 | 99.9 | 0.0 |
|
||||
| mixed_flight_faults | Ham + robust WLS + ESKF | 0.221 | 0.206 | 1.013 | 100.0 | 100.0 | 1.9 |
|
||||
|
||||
## Bulgular
|
||||
|
||||
- Sabit hızda pre-IIR ESKF RMS'i `0.182` m/s, ham yol `0.190` m/s. Yani pre-IIR sakin durumda daha düzgün çıktı veriyor; ESKF ham gürültüyü zaten `0.416` m/s WLS RMS'ten bu seviyeye indiriyor.
|
||||
- Airliner manevrasında sabit IIR `0.416` m/s ve `%0.0` NIS ret oranı verirken ham yol `0.190` m/s ve `%0.0` ret verdi. Konservatif adapter tabanları rejection'ı sınırlasa bile covariance'ın temsil etmediği IIR gecikmesi fused velocity'de dinamik hata bırakır.
|
||||
- Düşük C/N0 manevrasında sabit/ham/adaptif RMS sırasıyla `0.419`, `0.251`, `0.206` m/s oldu. Adaptif yol bu sentetik koşulda gürültü/gecikme uzlaşmasını sağladı; gerçek eşikler kart verisi ister.
|
||||
- Tek uydu burst'ünde raw WLS RMS `1.451` m/s, robust leave-one-out ile `0.418` m/s. ESKF son RMS farkı daha küçük; çünkü mevcut residual covariance inflation aggregate hatayı zaten zayıflatıyor. Robust kapı yine de hatalı kanalı bütün epoch'u reddetmeden ayırır.
|
||||
- Common clock-drift step raw velocity RMS'ini bozmadı; clock-drift state RMS'i `0.269` m/s. Dördüncü WLS bilinmeyeni ortak frekans hatasını üç hız bileşeninden ayırıyor.
|
||||
|
||||
## Mimari kararı
|
||||
|
||||
- Nominal sentetik model ham Doppler'in sabit pre-IIR gecikmesini önlediğini gösterir; fakat Genesys2 kart-kalibreli noise regresyonunda tam ham yol fazla gürültülüdür. Production ana yolu: hareket-adaptif Doppler -> bandwidth-aware sigma -> kanal validity/outlier kontrolü -> 5 Hz robust velocity WLS + gerçek covariance -> adapter tabanları -> ESKF.
|
||||
- ESKF yalnız 3D aggregate velocity gördüğü için tek uydu hatasını sonradan teşhis edemez. Per-satellite robust gate/FDE WLS öncesinde kalmalıdır.
|
||||
- Ayrı sabit `alpha=0.05` yol PPS drift predictor ve standalone sakin-hız kalite metriği için tutulur; ham snapshot tanısal/offline analiz için korunur.
|
||||
- Production adaptif IIR sakin/hareketli/yüksek-ivme için `alpha=0.05/0.20/0.40` seçer. Alpha büyüdükçe EWMA noise gain'i WLS sigma'sına yansıtılır; böylece FDE ve ESKF covariance bant genişliğini bilir.
|
||||
|
||||
## Model sınırları
|
||||
|
||||
- Sonuçlar sentetik hata büyüklüklerine koşulludur; kartta yüzde kazanç iddiası değildir.
|
||||
- Uydu geometrisi sabit sekiz-kanal local LOS modelidir. Satellite velocity/clock düzeltmelerinin doğru yapıldığı varsayılmıştır.
|
||||
- ESKF, bu soruya etki eden `[velocity, accel-bias]` alt sistemine indirgenmiştir; attitude, lever-arm ve tam position cross-covariance modellenmemiştir.
|
||||
- Tam firmware'deki position update'in velocity/accel-bias cross-covariance üzerinden sağlayabileceği kısmi recovery bu azaltılmış modelde yoktur. Bu nedenle uzun NIS-ret zincirinin mutlak büyüklüğü değil, pre-IIR'in gecikme ve ret mekanizması karar girdisidir.
|
||||
- Carrier loop gürültüsü C/N0 sigma yasasıyla üretilmiştir. Gerçek correlation, oscillator phase noise ve cycle slip dağılımları kart loguyla kalibre edilmelidir.
|
||||
- Robust WLS'nin `3.5 sigma`, en az altı uydu ve bütün tek-kanal leave-one-out seçimi C implementasyonuyla aynıdır. RTL değişmez; raw/filtered ayrımı firmware'dedir.
|
||||
@@ -0,0 +1,33 @@
|
||||
scenario,path,seeds,fused_rms_mps,fused_rms_seed_std_mps,fused_p95_mps,quiet_rms_mps,dynamic_rms_mps,wls_rms_mps,wls_p95_mps,clock_drift_rms_mps,velocity_accept_percent,nis_reject_percent,fused_95_coverage_percent,excluded_epoch_percent,mean_covariance_scale,mean_alpha
|
||||
steady_cruise,fixed_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
|
||||
steady_cruise,raw_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
|
||||
steady_cruise,adaptive_iir_wls_eskf,20,0.1821998934975134,0.040897665127266884,0.27466022199861373,0.1821998934975134,nan,0.0681533332956358,0.11645920599554568,0.04445747832784426,100.0,0.0,100.0,0.0,1.0000030764799477,0.05000000000000001
|
||||
steady_cruise,raw_robust_wls_eskf,20,0.19046432375253602,0.040009781644375395,0.28906917942032884,0.19046432375253602,nan,0.4164164729808367,0.720889200812689,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343088,1.0
|
||||
airliner_maneuver,fixed_iir_wls_eskf,20,0.41605194656716626,0.04357555329949249,0.7083141902703941,0.4019739347287984,0.4380205569605863,0.440222331186762,0.9391237648850084,0.04445747832784397,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
|
||||
airliner_maneuver,raw_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
|
||||
airliner_maneuver,adaptive_iir_wls_eskf,20,0.19421091770965,0.04161706926344872,0.2913238026993562,0.19649429208827177,0.18911500227901482,0.1349904626304289,0.2634780317604405,0.06996150326971154,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
|
||||
airliner_maneuver,raw_robust_wls_eskf,20,0.19046432375253933,0.04000978164437598,0.28906917942032373,0.196483054968526,0.1789145728432288,0.4164164729808368,0.720889200812707,0.2689447776657465,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
|
||||
aggressive_maneuver,fixed_iir_wls_eskf,20,0.9420862291590975,0.03697606175587584,1.7661046896298587,0.9551353937753919,0.918769792345089,1.1782538620062029,2.634559496211176,0.04445747832784312,100.0,0.0,55.891176470588235,0.0,1.0000030764799477,0.05000000000000001
|
||||
aggressive_maneuver,raw_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
|
||||
aggressive_maneuver,adaptive_iir_wls_eskf,20,0.20034438129208682,0.04215736847319882,0.3027038479248761,0.2017392380134829,0.19713256311771804,0.17059135831478262,0.3555978539018994,0.08938088837068828,100.0,0.0,100.0,0.0,1.0001185178364316,0.17603563474387524
|
||||
aggressive_maneuver,raw_robust_wls_eskf,20,0.19046432375254996,0.04000978164436178,0.2890691794203225,0.1928303776618579,0.18561935253806933,0.41641647298083734,0.7208892008127159,0.2689447776657467,100.0,0.0,100.0,0.0,1.2633800541343083,1.0
|
||||
low_cn0_airliner,fixed_iir_wls_eskf,20,0.4194988293998554,0.04110688289349549,0.7094271250760412,0.4045729059455276,0.4427791039662977,0.4845544319763686,0.9431345662991619,0.14058689054355467,100.0,0.0,99.74176470588235,0.0,1.0000281042403611,0.05000000000000001
|
||||
low_cn0_airliner,raw_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
|
||||
low_cn0_airliner,adaptive_iir_wls_eskf,20,0.20588009483954214,0.040099541607423995,0.3090368268997948,0.2069944088941925,0.20293127741446826,0.337603473203961,0.684354700050294,0.21325106769335284,100.0,0.0,100.0,0.0,1.0000902389090656,0.1040055679287305
|
||||
low_cn0_airliner,raw_robust_wls_eskf,20,0.25103588282812916,0.04464089418791806,0.4041748199164089,0.2555504221567221,0.2416319579439005,1.3168245098334108,2.2796518151866563,0.8504780622313423,100.0,0.0,100.0,0.0,1.26385941740462,1.0
|
||||
single_satellite_outliers,fixed_iir_wls_eskf,20,0.49511238386612133,0.053875439119808514,0.8173709856169348,0.5050054814659805,0.4779542859711989,1.1463378050509494,0.9892652696081077,0.8027251360930692,100.0,0.0,97.6964705882353,0.0,6.927634385063575,0.05000000000000001
|
||||
single_satellite_outliers,raw_wls_eskf,20,0.2570227443883875,0.05273042903908908,0.39439133440568985,0.27216019157834215,0.22927677845507954,1.4512624341492657,0.8297158565959334,1.0630956381619745,100.0,0.0,100.0,0.0,11.13597527632786,1.0
|
||||
single_satellite_outliers,adaptive_iir_wls_eskf,20,0.3556263234893831,0.05851649747663683,0.595609347562141,0.3792437361871613,0.31186327209433895,1.1538474734782178,0.5683617007748845,0.8527337411854743,100.0,0.0,100.0,0.0,7.7254775572955,0.11647550111358576
|
||||
single_satellite_outliers,raw_robust_wls_eskf,20,0.19054941715698687,0.03971061631217381,0.2889429468862187,0.19658211712738688,0.1789788721793858,0.4181969381744313,0.7254268588122093,0.27034697594947305,100.0,0.0,100.0,2.8953229398663693,1.2641999517115465,1.0
|
||||
clock_drift_steps,fixed_iir_wls_eskf,20,0.4160519465671664,0.04357555329949111,0.7083141902703948,0.4019739347287993,0.4380205569605854,0.44022233118676146,0.9391237648850096,1.8177258394243663,100.0,0.0,99.71411764705883,0.0,1.0000030764799477,0.05000000000000001
|
||||
clock_drift_steps,raw_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
|
||||
clock_drift_steps,adaptive_iir_wls_eskf,20,0.19421091770965143,0.0416170692634497,0.29132380269935765,0.19649429208827301,0.18911500227901648,0.1349904626304289,0.26347803176043993,1.818528619384018,100.0,0.0,100.0,0.0,1.0000030764799477,0.11647550111358576
|
||||
clock_drift_steps,raw_robust_wls_eskf,20,0.19046432375253802,0.04000978164437644,0.28906917942032334,0.19648305496852497,0.17891457284322693,0.41641647298083695,0.7208892008126868,0.268944777665747,100.0,0.0,100.0,0.0,1.2633800541343085,1.0
|
||||
gps_gap_recovery,fixed_iir_wls_eskf,20,0.3971781427894736,0.044796626065109466,0.7104470646835127,0.3873196668983798,0.41253405912166263,0.43094056066637465,0.9463346669492851,0.04458606585950929,100.0,0.0,99.90529411764706,0.0,1.0000036934211136,0.05
|
||||
gps_gap_recovery,raw_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
|
||||
gps_gap_recovery,adaptive_iir_wls_eskf,20,0.20776527476484566,0.044556420313135676,0.3257106582800945,0.21357805324970008,0.19661285828638408,0.12935782738837362,0.2613749696102362,0.06764578524374312,100.0,0.0,100.0,0.0,1.0000036934211136,0.10814839572192514
|
||||
gps_gap_recovery,raw_robust_wls_eskf,20,0.2082177514862905,0.03682283889189333,0.32879462613500177,0.21668463381045563,0.1921901394048915,0.41707211010710354,0.7203717161799947,0.26913433965877315,100.0,0.0,100.0,0.0,1.2631217416869522,1.0
|
||||
mixed_flight_faults,fixed_iir_wls_eskf,20,0.9404176641021843,0.04319034103529422,1.7661662430364344,0.9555417869207146,0.9132850762857121,1.5154618461064036,2.663221132275041,0.7591896512045151,100.0,0.0,60.37294117647059,0.0,1.3397749809447188,0.05000000000000001
|
||||
mixed_flight_faults,raw_wls_eskf,20,0.22399771088975795,0.05069473595820174,0.3492819306443341,0.23151007332767243,0.2092729705094658,1.498455933396739,2.36689254151483,0.7603191788358898,100.0,0.0,100.0,0.0,2.1864248911994575,1.0
|
||||
mixed_flight_faults,adaptive_iir_wls_eskf,20,0.2952659489493427,0.07200078881833771,0.4874378367724045,0.30541797568187434,0.2754557634719251,0.838415542523151,0.9361678311586857,0.47500979673114047,100.0,0.0,99.92470588235294,0.0,1.250042525522558,0.178938679245283
|
||||
mixed_flight_faults,raw_robust_wls_eskf,20,0.2208132635451996,0.04372541489397352,0.3416439647301807,0.22864253849532457,0.20560052604298312,1.0129266912203836,2.2537697549366906,0.673490930924677,100.0,0.0,100.0,1.9103773584905661,1.5692458289027678,1.0
|
||||
|
@@ -0,0 +1,51 @@
|
||||
scenario,method,epochs,horizontal_rms_m,position_3d_rms_m,position_3d_p95_m,valid_percent,valid_3d_rms_m,horizontal_2sigma_coverage_percent,false_valid_over_25m_percent,fde_percent,mean_measurement_sigma_m
|
||||
clean_open_sky,current_cn0,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
|
||||
clean_open_sky,naive_add_dll,4500,5.768593681325036,10.022431815549082,17.253420427433362,100.0,10.022431815549082,100.0,0.3111111111111111,0.0,10.452052333958017
|
||||
clean_open_sky,dll_excess,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
|
||||
clean_open_sky,dll_excess_guard,4500,5.7784464702133524,10.05299246387535,17.320782018324902,100.0,10.05299246387535,99.97777777777777,0.33333333333333337,0.0,8.908174172560404
|
||||
clean_open_sky,oracle_variance,4500,5.778805688412197,10.054230284262685,17.35250490247279,100.0,10.054230284262685,99.97777777777777,0.3111111111111111,0.0,8.840973580918838
|
||||
weak_signal,current_cn0,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
|
||||
weak_signal,naive_add_dll,4500,18.802930957724488,32.313115706990146,55.45511304556428,100.0,32.313115706990146,100.0,56.177777777777784,0.0,28.674069502747358
|
||||
weak_signal,dll_excess,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
|
||||
weak_signal,dll_excess_guard,4500,18.986226727568837,32.6486650471064,56.115244341908046,100.0,32.6486650471064,98.91111111111111,56.222222222222214,0.5111111111111112,21.20058629982459
|
||||
weak_signal,oracle_variance,4500,19.020289863331797,32.73415409569256,56.44388754013162,100.0,32.73415409569256,98.8,56.31111111111111,0.6222222222222222,20.84621216112656
|
||||
one_channel_hidden_jitter,current_cn0,4500,8.804153682379612,12.268224803643907,19.936371518380813,100.0,12.268224803643907,96.46666666666667,1.0666666666666667,9.666666666666666,8.840973580918838
|
||||
one_channel_hidden_jitter,naive_add_dll,4500,6.529225405554112,10.487953223937716,17.74604536639535,100.0,10.487953223937716,99.97777777777777,0.33333333333333337,0.0,11.805515833602682
|
||||
one_channel_hidden_jitter,dll_excess,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
|
||||
one_channel_hidden_jitter,dll_excess_guard,4500,6.455903980559815,10.46584556632944,17.675128387138177,100.0,10.46584556632944,99.97777777777777,0.33333333333333337,0.044444444444444446,10.358668146326426
|
||||
one_channel_hidden_jitter,oracle_variance,4500,6.4491935598492525,10.463101509886709,17.736218582311878,100.0,10.463101509886709,99.97777777777777,0.33333333333333337,0.06666666666666667,10.315229331071063
|
||||
common_hidden_jitter,current_cn0,4500,19.733300930595462,33.79270185129192,59.76307034516212,94.75555555555556,33.15386252421736,56.39999999999999,52.400000000000006,46.577777777777776,8.840973580918838
|
||||
common_hidden_jitter,naive_add_dll,4500,16.033969673897577,27.61407554068951,47.55915451805768,100.0,27.61407554068951,99.46666666666667,43.93333333333334,0.26666666666666666,18.882004393424943
|
||||
common_hidden_jitter,dll_excess,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
|
||||
common_hidden_jitter,dll_excess_guard,4500,16.085615037536463,27.688964211504615,47.828684727805175,100.0,27.688964211504615,98.97777777777777,43.888888888888886,0.4444444444444444,18.04555007574287
|
||||
common_hidden_jitter,oracle_variance,4500,16.106964558267112,27.740128015702773,47.83188793884836,100.0,27.740128015702773,98.88888888888889,43.86666666666667,0.4888888888888889,18.035214487051437
|
||||
noise_burst_recovery,current_cn0,4500,9.325915933961204,12.96069950118485,22.132869786417263,98.93333333333332,12.552632065384056,94.71111111111111,2.577777777777778,12.11111111111111,8.840973580918838
|
||||
noise_burst_recovery,naive_add_dll,4500,6.470839383968352,10.523606281690133,18.031622500344657,100.0,10.523606281690133,100.0,0.5333333333333333,0.06666666666666667,11.60221083527078
|
||||
noise_burst_recovery,dll_excess,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
|
||||
noise_burst_recovery,dll_excess_guard,4500,6.397124977878571,10.493909835494788,17.99630946143617,100.0,10.493909835494788,99.97777777777777,0.4888888888888889,0.13333333333333333,10.131003114059961
|
||||
noise_burst_recovery,oracle_variance,4500,6.367173301653832,10.473649717876942,17.90212603773505,100.0,10.473649717876942,99.97777777777777,0.4888888888888889,0.044444444444444446,10.187907003764948
|
||||
dll_transient_stress,current_cn0,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
|
||||
dll_transient_stress,naive_add_dll,4500,5.793995520088368,10.118871049708806,17.333372587049784,100.0,10.118871049708806,100.0,0.37777777777777777,0.0,10.45205253980181
|
||||
dll_transient_stress,dll_excess,4500,5.8034283726190745,10.144627133073621,17.380953993070463,100.0,10.144627133073621,99.97777777777777,0.37777777777777777,0.0,8.908174353046816
|
||||
dll_transient_stress,dll_excess_guard,4500,5.79072382051268,10.123295335548953,17.368844982312577,100.0,10.123295335548953,99.97777777777777,0.33333333333333337,0.0,8.947656011912292
|
||||
dll_transient_stress,oracle_variance,4500,5.803776392869777,10.145785618162538,17.425227799952054,100.0,10.145785618162538,99.97777777777777,0.35555555555555557,0.0,8.840973580918838
|
||||
stable_multipath_bias,current_cn0,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
|
||||
stable_multipath_bias,naive_add_dll,4500,7.072680893174009,12.386663925234807,20.866871605887997,100.0,12.386663925234807,99.97777777777777,1.4666666666666666,0.0,10.452052333958017
|
||||
stable_multipath_bias,dll_excess,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
|
||||
stable_multipath_bias,dll_excess_guard,4500,7.125500558670432,12.536689588412326,21.02122766562977,100.0,12.536689588412326,99.91111111111111,1.6444444444444446,0.022222222222222223,8.908174172560404
|
||||
stable_multipath_bias,oracle_variance,4500,7.12694286036133,12.545413964352555,20.99675149186582,100.0,12.545413964352555,99.8888888888889,1.6666666666666667,0.022222222222222223,8.840973580918838
|
||||
mixed_multipath,current_cn0,4500,8.529840854093363,14.426936007413373,25.381172069686816,100.0,14.426936007413373,97.11111111111111,5.644444444444444,5.0,8.840973580918838
|
||||
mixed_multipath,naive_add_dll,4500,6.586489015624914,11.258484405711224,19.599950438767102,100.0,11.258484405711224,100.0,0.7333333333333333,0.044444444444444446,11.636563744509052
|
||||
mixed_multipath,dll_excess,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
|
||||
mixed_multipath,dll_excess_guard,4500,6.45479117311332,11.067801578489938,19.233225446062768,100.0,11.067801578489938,100.0,0.7333333333333333,0.08888888888888889,10.202310406784667
|
||||
mixed_multipath,oracle_variance,4500,6.448333589783235,11.058021985103961,19.19754215965981,100.0,11.058021985103961,99.95555555555555,0.7333333333333333,0.08888888888888889,10.144307488606447
|
||||
poor_geometry_hidden_jitter,current_cn0,4500,79.38795978543642,293.8222926191477,574.9617330272,100.0,293.8222926191477,79.0,94.31111111111112,1.1555555555555554,10.734703317378012
|
||||
poor_geometry_hidden_jitter,naive_add_dll,4500,57.59864532145799,206.13456070122297,405.1527013193,100.0,206.13456070122297,99.66666666666667,92.42222222222222,0.022222222222222223,14.604104871190987
|
||||
poor_geometry_hidden_jitter,dll_excess,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
|
||||
poor_geometry_hidden_jitter,dll_excess_guard,4500,55.3927819274298,196.86935630824075,390.63687676393124,100.0,196.86935630824075,99.24444444444444,91.4,0.022222222222222223,12.779670971493758
|
||||
poor_geometry_hidden_jitter,oracle_variance,4500,55.24662489772763,196.2046647890204,386.5771391089706,100.0,196.2046647890204,99.28888888888889,91.26666666666667,0.022222222222222223,12.721163297137666
|
||||
cn0_step_recovery,current_cn0,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
|
||||
cn0_step_recovery,naive_add_dll,4500,12.797089787478507,22.265941453345402,47.59129168788298,100.0,22.265941453345402,100.0,21.422222222222224,0.0,17.44366868773946
|
||||
cn0_step_recovery,dll_excess,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
|
||||
cn0_step_recovery,dll_excess_guard,4500,12.96657717120691,22.586113735655264,47.91159461508304,100.0,22.586113735655264,99.46666666666667,21.488888888888887,0.2,13.653801199442478
|
||||
cn0_step_recovery,oracle_variance,4500,12.974359636882712,22.585010269106,47.95054105970103,100.0,22.585010269106,99.42222222222222,21.622222222222224,0.2222222222222222,13.475047639913695
|
||||
|
@@ -0,0 +1,2 @@
|
||||
numpy>=1.24
|
||||
matplotlib>=3.7
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Small executable model of the RTL code-tracking observables.
|
||||
|
||||
This is deliberately scoped to the parts that matter to the navigation
|
||||
measurement study:
|
||||
|
||||
* the normalized early-minus-late magnitude discriminator;
|
||||
* its signed Q2.13 output;
|
||||
* the fixed C1/C2 code-loop filter and Q20 NCO command;
|
||||
* the 20 one-millisecond discriminator samples available per NAV bit.
|
||||
|
||||
It is not an RF waveform simulator. The C/N0-to-discriminator-noise mapping
|
||||
is an explicit calibration assumption until real correlator/DLL telemetry is
|
||||
recorded from the board.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
DLL_OUTPUT_FRACTION_BITS = 13
|
||||
DLL_OUTPUT_WORD_BITS = 15
|
||||
DLL_SPACING_CHIPS = 0.5
|
||||
CODE_LOOP_C1 = 21.25
|
||||
CODE_LOOP_C2 = 0.05712890625
|
||||
CODE_LOOP_OUTPUT_FRACTION_BITS = 20
|
||||
CODE_LOOP_OUTPUT_WORD_BITS = 28
|
||||
DLL_SAMPLES_PER_NAV_BIT = 20
|
||||
CODE_FREQUENCY_BASIS_HZ = 1.023e6
|
||||
ABS_SAMPLE_COUNTER_MODULUS = 1 << 32
|
||||
|
||||
# Synthetic calibration point. It only fixes the scale of the Monte Carlo
|
||||
# discriminator telemetry. Dynamic weighting uses a measured/expected ratio,
|
||||
# so the absolute choice cancels in the correctly modelled case.
|
||||
DLL_STD_AT_45_DBHZ = 0.020
|
||||
|
||||
|
||||
def _signed_saturate(value: int, word_bits: int) -> int:
|
||||
minimum = -(1 << (word_bits - 1))
|
||||
maximum = (1 << (word_bits - 1)) - 1
|
||||
return min(maximum, max(minimum, value))
|
||||
|
||||
|
||||
def quantize_signed(value: float, word_bits: int, fraction_bits: int) -> int:
|
||||
scaled = int(np.rint(value * (1 << fraction_bits)))
|
||||
return _signed_saturate(scaled, word_bits)
|
||||
|
||||
|
||||
def dequantize_signed(value: int, fraction_bits: int) -> float:
|
||||
return float(value) / float(1 << fraction_bits)
|
||||
|
||||
|
||||
def triangular_correlation(code_offset_chips: float) -> float:
|
||||
"""Ideal one-chip triangular GPS C/A autocorrelation main lobe."""
|
||||
|
||||
return max(0.0, 1.0 - abs(code_offset_chips))
|
||||
|
||||
|
||||
def ideal_early_late_magnitudes(
|
||||
code_error_chips: float,
|
||||
spacing_chips: float = DLL_SPACING_CHIPS,
|
||||
) -> tuple[float, float]:
|
||||
half_spacing = 0.5 * spacing_chips
|
||||
early = triangular_correlation(code_error_chips + half_spacing)
|
||||
late = triangular_correlation(code_error_chips - half_spacing)
|
||||
return early, late
|
||||
|
||||
|
||||
def normalized_early_minus_late(early_magnitude: float, late_magnitude: float) -> float:
|
||||
denominator = early_magnitude + late_magnitude
|
||||
if denominator <= 0.0:
|
||||
return 0.0
|
||||
return (early_magnitude - late_magnitude) / denominator
|
||||
|
||||
|
||||
def rtl_discriminator(code_error_chips: float) -> tuple[int, float]:
|
||||
"""Return the RTL-like Q2.13 discriminator code and dequantized value."""
|
||||
|
||||
early, late = ideal_early_late_magnitudes(code_error_chips)
|
||||
value = normalized_early_minus_late(early, late)
|
||||
code = quantize_signed(
|
||||
value,
|
||||
DLL_OUTPUT_WORD_BITS,
|
||||
DLL_OUTPUT_FRACTION_BITS,
|
||||
)
|
||||
return code, dequantize_signed(code, DLL_OUTPUT_FRACTION_BITS)
|
||||
|
||||
|
||||
def expected_discriminator_std(cn0_dbhz: float) -> float:
|
||||
"""Expected normalized DLL jitter used by the synthetic calibration.
|
||||
|
||||
The 20 dB amplitude-domain law mirrors the firmware's code-sigma model.
|
||||
The result is bounded because the normalized discriminator is finite.
|
||||
"""
|
||||
|
||||
value = DLL_STD_AT_45_DBHZ * 10.0 ** ((45.0 - cn0_dbhz) / 20.0)
|
||||
return float(np.clip(value, 0.002, 0.35))
|
||||
|
||||
|
||||
def quantized_code_epoch_measurement(
|
||||
true_abs_sample: float,
|
||||
) -> tuple[int, int, float]:
|
||||
"""Model ABS_SAMPLE + REM_CODE_PHASE and firmware reconstruction.
|
||||
|
||||
``true_abs_sample`` is the ideal code boundary in RF-sample units. The
|
||||
hardware captures the following integer sample and reports the overshoot
|
||||
with the remainder scale used by the C correction formula.
|
||||
"""
|
||||
|
||||
raw_unwrapped = int(np.ceil(true_abs_sample))
|
||||
overshoot_samples = raw_unwrapped - true_abs_sample
|
||||
remainder = int(np.rint(overshoot_samples * CODE_FREQUENCY_BASIS_HZ))
|
||||
raw_register = raw_unwrapped % ABS_SAMPLE_COUNTER_MODULUS
|
||||
corrected = raw_unwrapped - remainder / CODE_FREQUENCY_BASIS_HZ
|
||||
return raw_register, remainder, corrected
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DllWindow:
|
||||
samples: np.ndarray
|
||||
mean: float
|
||||
variance: float
|
||||
expected_variance: float
|
||||
variance_ratio: float
|
||||
mean_z_score: float
|
||||
|
||||
|
||||
def synthesize_dll_window(
|
||||
rng: np.random.Generator,
|
||||
*,
|
||||
cn0_dbhz: float,
|
||||
jitter_multiplier: float = 1.0,
|
||||
discriminator_bias: float = 0.0,
|
||||
sample_count: int = DLL_SAMPLES_PER_NAV_BIT,
|
||||
) -> DllWindow:
|
||||
"""Generate and Q2.13-quantize one NAV-bit DLL telemetry window."""
|
||||
|
||||
if sample_count < 2:
|
||||
raise ValueError("DLL variance needs at least two samples")
|
||||
expected_std = expected_discriminator_std(cn0_dbhz)
|
||||
actual_std = expected_std * max(0.0, jitter_multiplier)
|
||||
raw = discriminator_bias + rng.normal(0.0, actual_std, sample_count)
|
||||
minimum = -(1 << (DLL_OUTPUT_WORD_BITS - 1))
|
||||
maximum = (1 << (DLL_OUTPUT_WORD_BITS - 1)) - 1
|
||||
quantized_codes = np.clip(
|
||||
np.rint(raw * (1 << DLL_OUTPUT_FRACTION_BITS)),
|
||||
minimum,
|
||||
maximum,
|
||||
).astype(np.int32)
|
||||
samples = quantized_codes.astype(float) / float(1 << DLL_OUTPUT_FRACTION_BITS)
|
||||
mean = float(np.mean(samples))
|
||||
variance = float(np.var(samples, ddof=1))
|
||||
expected_variance = expected_std * expected_std
|
||||
ratio = variance / max(expected_variance, 1.0e-15)
|
||||
standard_error = np.sqrt(max(variance, expected_variance) / sample_count)
|
||||
mean_z = abs(mean) / max(float(standard_error), 1.0e-12)
|
||||
return DllWindow(
|
||||
samples=samples,
|
||||
mean=mean,
|
||||
variance=variance,
|
||||
expected_variance=expected_variance,
|
||||
variance_ratio=float(ratio),
|
||||
mean_z_score=float(mean_z),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodeLoopFilterRtl:
|
||||
"""Update-rate model of LF_code.sv, including coefficient quantization."""
|
||||
|
||||
integrator_q20: int = 0
|
||||
|
||||
c1_integer: int = int(CODE_LOOP_C1 * (1 << 2))
|
||||
c2_integer: int = int(CODE_LOOP_C2 * (1 << 11))
|
||||
|
||||
def update_code(self, discriminator_q13: int) -> int:
|
||||
# C1 product has Q15 and is zero-padded to Q20 in LF_code.sv.
|
||||
proportional_q20 = (self.c1_integer * discriminator_q13) << 5
|
||||
|
||||
# C2 product has Q24 and LF_code.sv slices it back to Q20.
|
||||
integral_increment_q20 = (self.c2_integer * discriminator_q13) >> 4
|
||||
self.integrator_q20 = _signed_saturate(
|
||||
self.integrator_q20 + integral_increment_q20,
|
||||
CODE_LOOP_OUTPUT_WORD_BITS,
|
||||
)
|
||||
return _signed_saturate(
|
||||
self.integrator_q20 + proportional_q20,
|
||||
CODE_LOOP_OUTPUT_WORD_BITS,
|
||||
)
|
||||
|
||||
def update(self, discriminator: float) -> float:
|
||||
discriminator_q13 = quantize_signed(
|
||||
discriminator,
|
||||
DLL_OUTPUT_WORD_BITS,
|
||||
DLL_OUTPUT_FRACTION_BITS,
|
||||
)
|
||||
command_q20 = self.update_code(discriminator_q13)
|
||||
return dequantize_signed(command_q20, CODE_LOOP_OUTPUT_FRACTION_BITS)
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Regression tests for the project-specific Doppler/WLS/ESKF model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .doppler_eskf_model import (
|
||||
AdaptiveAlpha,
|
||||
DopplerPath,
|
||||
FIXED_DOPPLER_ALPHA,
|
||||
flight_scenarios,
|
||||
local_los_matrix,
|
||||
run_doppler_eskf_scenario,
|
||||
velocity_wls,
|
||||
)
|
||||
|
||||
|
||||
def _scenario(name: str):
|
||||
return next(item for item in flight_scenarios() if item.name == name)
|
||||
|
||||
|
||||
def _fused_rms(trace) -> float:
|
||||
selected = trace.time_s >= 5.0
|
||||
error = trace.fused_velocity_mps[selected] - trace.truth_velocity_mps[selected]
|
||||
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
|
||||
|
||||
|
||||
def _wls_rms(trace) -> float:
|
||||
truth = np.column_stack(
|
||||
[
|
||||
np.interp(
|
||||
trace.wls_time_s,
|
||||
trace.time_s,
|
||||
trace.truth_velocity_mps[:, axis],
|
||||
)
|
||||
for axis in range(3)
|
||||
]
|
||||
)
|
||||
error = trace.wls_velocity_mps - truth
|
||||
return float(np.sqrt(np.mean(np.sum(np.square(error), axis=1))))
|
||||
|
||||
|
||||
def _board_raw_noise_scenario(name: str):
|
||||
"""Match the 9.17 m/s raw-WLS RMS measured on Genesys2."""
|
||||
scenario = _scenario(name)
|
||||
nominal_noise = scenario.noise_multiplier
|
||||
return replace(
|
||||
scenario,
|
||||
name=f"{scenario.name}_genesys2_noise",
|
||||
noise_multiplier=lambda time_s, satellite: (
|
||||
22.0 * nominal_noise(time_s, satellite)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DopplerWlsTest(unittest.TestCase):
|
||||
def test_four_state_wls_separates_velocity_and_clock_drift(self) -> None:
|
||||
velocity = np.asarray([145.0, -32.0, 7.5])
|
||||
clock_drift = 18.0
|
||||
observation = -local_los_matrix() @ velocity + clock_drift
|
||||
result = velocity_wls(observation, np.full(8, 45.0))
|
||||
|
||||
np.testing.assert_allclose(result.velocity_mps, velocity, atol=1.0e-10)
|
||||
self.assertAlmostEqual(result.clock_drift_mps, clock_drift, places=10)
|
||||
self.assertAlmostEqual(result.covariance_scale, 1.0)
|
||||
|
||||
def test_robust_wls_excludes_a_single_bad_channel(self) -> None:
|
||||
velocity = np.asarray([120.0, 20.0, -2.0])
|
||||
observation = -local_los_matrix() @ velocity + 4.0
|
||||
observation[2] += 12.0
|
||||
|
||||
plain = velocity_wls(observation, np.full(8, 43.0))
|
||||
robust = velocity_wls(observation, np.full(8, 43.0), robust=True)
|
||||
|
||||
self.assertEqual(robust.excluded_satellite, 2)
|
||||
self.assertGreater(np.linalg.norm(plain.velocity_mps - velocity), 1.0)
|
||||
np.testing.assert_allclose(robust.velocity_mps, velocity, atol=1.0e-9)
|
||||
|
||||
|
||||
class AdaptiveAlphaTest(unittest.TestCase):
|
||||
def test_motion_switches_alpha_and_quiet_state_recovers(self) -> None:
|
||||
selector = AdaptiveAlpha()
|
||||
for _ in range(80):
|
||||
quiet = selector.update(np.zeros(3), 35.0)
|
||||
self.assertAlmostEqual(quiet, 0.03)
|
||||
|
||||
dynamic_values = [
|
||||
selector.update(np.asarray([4.0, 0.0, 0.0]), 35.0)
|
||||
for _ in range(30)
|
||||
]
|
||||
self.assertGreaterEqual(max(dynamic_values), 0.40)
|
||||
|
||||
for _ in range(120):
|
||||
recovered = selector.update(np.zeros(3), 40.0)
|
||||
self.assertAlmostEqual(recovered, FIXED_DOPPLER_ALPHA)
|
||||
|
||||
|
||||
class DopplerEskfIntegrationTest(unittest.TestCase):
|
||||
def test_board_raw_noise_requires_adaptive_prefilter(self) -> None:
|
||||
steady = run_doppler_eskf_scenario(
|
||||
_board_raw_noise_scenario("steady_cruise"),
|
||||
seed=7,
|
||||
paths=(
|
||||
DopplerPath.FIXED_IIR,
|
||||
DopplerPath.ADAPTIVE_IIR,
|
||||
DopplerPath.RAW,
|
||||
),
|
||||
)
|
||||
airliner = run_doppler_eskf_scenario(
|
||||
_board_raw_noise_scenario("airliner_maneuver"),
|
||||
seed=3,
|
||||
paths=(
|
||||
DopplerPath.FIXED_IIR,
|
||||
DopplerPath.ADAPTIVE_IIR,
|
||||
DopplerPath.RAW,
|
||||
),
|
||||
)
|
||||
aggressive = run_doppler_eskf_scenario(
|
||||
_board_raw_noise_scenario("aggressive_maneuver"),
|
||||
seed=7,
|
||||
paths=(
|
||||
DopplerPath.FIXED_IIR,
|
||||
DopplerPath.ADAPTIVE_IIR,
|
||||
DopplerPath.RAW,
|
||||
),
|
||||
)
|
||||
steady_fixed = _fused_rms(steady[DopplerPath.FIXED_IIR])
|
||||
steady_adaptive = _fused_rms(steady[DopplerPath.ADAPTIVE_IIR])
|
||||
steady_raw = _fused_rms(steady[DopplerPath.RAW])
|
||||
airliner_fixed = _fused_rms(airliner[DopplerPath.FIXED_IIR])
|
||||
airliner_adaptive = _fused_rms(airliner[DopplerPath.ADAPTIVE_IIR])
|
||||
airliner_raw = _fused_rms(airliner[DopplerPath.RAW])
|
||||
aggressive_fixed = _fused_rms(aggressive[DopplerPath.FIXED_IIR])
|
||||
aggressive_adaptive = _fused_rms(
|
||||
aggressive[DopplerPath.ADAPTIVE_IIR]
|
||||
)
|
||||
aggressive_raw = _fused_rms(aggressive[DopplerPath.RAW])
|
||||
|
||||
# Direct 20 ms snapshots are too noisy at the level actually observed
|
||||
# on the board. Adaptive bandwidth retains the quiet-state result and
|
||||
# beats both direct raw and the 0.39 s fixed-IIR lag in manoeuvres.
|
||||
self.assertLess(steady_adaptive, 1.05 * steady_fixed)
|
||||
self.assertLess(steady_adaptive, 0.50 * steady_raw)
|
||||
self.assertLess(airliner_adaptive, airliner_fixed)
|
||||
self.assertLess(airliner_adaptive, 0.50 * airliner_raw)
|
||||
self.assertLess(aggressive_adaptive, 0.65 * aggressive_fixed)
|
||||
self.assertLess(aggressive_adaptive, 0.50 * aggressive_raw)
|
||||
|
||||
def test_eskf_filters_raw_doppler_without_pre_iir(self) -> None:
|
||||
traces = run_doppler_eskf_scenario(
|
||||
_scenario("steady_cruise"),
|
||||
seed=7,
|
||||
paths=(DopplerPath.RAW,),
|
||||
)
|
||||
raw = traces[DopplerPath.RAW]
|
||||
self.assertLess(_fused_rms(raw), 0.45 * _wls_rms(raw))
|
||||
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
|
||||
|
||||
def test_raw_path_avoids_fixed_iir_manoeuvre_lag(self) -> None:
|
||||
traces = run_doppler_eskf_scenario(
|
||||
_scenario("airliner_maneuver"),
|
||||
seed=3,
|
||||
paths=(DopplerPath.FIXED_IIR, DopplerPath.RAW),
|
||||
)
|
||||
fixed = traces[DopplerPath.FIXED_IIR]
|
||||
raw = traces[DopplerPath.RAW]
|
||||
self.assertLess(_fused_rms(raw), 0.35 * _fused_rms(fixed))
|
||||
# The production 2/2/3 m/s adapter floors keep both paths inside the
|
||||
# NIS gate; the fixed-IIR penalty now appears as lag error, not reject.
|
||||
self.assertGreater(float(np.mean(raw.wls_accepted)), 0.99)
|
||||
self.assertGreater(float(np.mean(fixed.wls_accepted)), 0.99)
|
||||
|
||||
def test_common_clock_step_does_not_leak_into_raw_velocity(self) -> None:
|
||||
airliner = run_doppler_eskf_scenario(
|
||||
_scenario("airliner_maneuver"),
|
||||
seed=11,
|
||||
paths=(DopplerPath.RAW,),
|
||||
)[DopplerPath.RAW]
|
||||
clock_step = run_doppler_eskf_scenario(
|
||||
_scenario("clock_drift_steps"),
|
||||
seed=11,
|
||||
paths=(DopplerPath.RAW,),
|
||||
)[DopplerPath.RAW]
|
||||
|
||||
np.testing.assert_allclose(
|
||||
airliner.wls_velocity_mps,
|
||||
clock_step.wls_velocity_mps,
|
||||
atol=1.0e-10,
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
airliner.fused_velocity_mps,
|
||||
clock_step.fused_velocity_mps,
|
||||
atol=1.0e-10,
|
||||
)
|
||||
|
||||
def test_robust_wls_contains_channel_bursts_before_eskf(self) -> None:
|
||||
traces = run_doppler_eskf_scenario(
|
||||
_scenario("single_satellite_outliers"),
|
||||
seed=5,
|
||||
paths=(DopplerPath.RAW, DopplerPath.RAW_ROBUST),
|
||||
)
|
||||
raw = traces[DopplerPath.RAW]
|
||||
robust = traces[DopplerPath.RAW_ROBUST]
|
||||
self.assertLess(_wls_rms(robust), 0.65 * _wls_rms(raw))
|
||||
self.assertGreater(float(np.mean(robust.excluded_satellites >= 0)), 0.02)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from gps_measurement_python.code_smoothing_model import run_smoothing_trial
|
||||
from gps_measurement_python.firmware_wls_model import (
|
||||
SigmaInputs,
|
||||
SigmaMethod,
|
||||
firmware_sigma_m,
|
||||
)
|
||||
from gps_measurement_python.measurement_scenarios import (
|
||||
run_scenario,
|
||||
scenario_matrix,
|
||||
)
|
||||
from gps_measurement_python.rtl_dll_model import (
|
||||
CodeLoopFilterRtl,
|
||||
quantized_code_epoch_measurement,
|
||||
rtl_discriminator,
|
||||
synthesize_dll_window,
|
||||
)
|
||||
|
||||
|
||||
class RtlDllModelTests(unittest.TestCase):
|
||||
def test_discriminator_zero_and_q13_symmetry(self):
|
||||
self.assertEqual(rtl_discriminator(0.0)[0], 0)
|
||||
positive = rtl_discriminator(0.05)[1]
|
||||
negative = rtl_discriminator(-0.05)[1]
|
||||
self.assertAlmostEqual(positive, -negative, places=6)
|
||||
self.assertLess(positive, 0.0)
|
||||
|
||||
def test_code_loop_integrator_accumulates_constant_error(self):
|
||||
loop = CodeLoopFilterRtl()
|
||||
first = loop.update(0.01)
|
||||
twentieth = first
|
||||
for _ in range(19):
|
||||
twentieth = loop.update(0.01)
|
||||
self.assertGreater(first, 0.0)
|
||||
self.assertGreater(twentieth, first)
|
||||
|
||||
def test_dll_window_detects_excess_variance(self):
|
||||
rng = np.random.default_rng(42)
|
||||
nominal = synthesize_dll_window(
|
||||
rng,
|
||||
cn0_dbhz=45.0,
|
||||
jitter_multiplier=1.0,
|
||||
sample_count=4000,
|
||||
)
|
||||
noisy = synthesize_dll_window(
|
||||
rng,
|
||||
cn0_dbhz=45.0,
|
||||
jitter_multiplier=3.0,
|
||||
sample_count=4000,
|
||||
)
|
||||
self.assertAlmostEqual(nominal.variance_ratio, 1.0, delta=0.10)
|
||||
self.assertAlmostEqual(noisy.variance_ratio, 9.0, delta=0.70)
|
||||
|
||||
def test_abs_sample_remainder_reconstructs_subsample_epoch(self):
|
||||
for true_sample in (
|
||||
12_345_678.314159,
|
||||
12_345_678.0000001,
|
||||
12_345_678.9999999,
|
||||
12_345_678.0,
|
||||
):
|
||||
raw, remainder, corrected = quantized_code_epoch_measurement(true_sample)
|
||||
self.assertEqual(raw, int(np.ceil(true_sample)))
|
||||
self.assertLessEqual(
|
||||
abs(corrected - true_sample),
|
||||
0.5 / 1.023e6 + 1.0e-12,
|
||||
)
|
||||
self.assertGreaterEqual(remainder, 0)
|
||||
|
||||
|
||||
class FirmwareSigmaTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def inputs(ratio: float = 1.0, mean_z: float = 0.0) -> SigmaInputs:
|
||||
return SigmaInputs(
|
||||
ura_index=np.asarray([0]),
|
||||
elevation_deg=np.asarray([90.0]),
|
||||
cn0_dbhz=np.asarray([45.0]),
|
||||
dll_variance_ratio=np.asarray([ratio]),
|
||||
dll_mean_z=np.asarray([mean_z]),
|
||||
actual_jitter_multiplier=np.asarray([np.sqrt(ratio)]),
|
||||
)
|
||||
|
||||
def test_current_matches_c_formula(self):
|
||||
sigma = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
|
||||
expected = np.sqrt(2.4**2 + 3.0**2 + 3.0**2)
|
||||
self.assertAlmostEqual(sigma, expected, places=12)
|
||||
|
||||
def test_excess_method_does_not_double_count_nominal_dll_noise(self):
|
||||
current = firmware_sigma_m(self.inputs(), SigmaMethod.CURRENT)[0]
|
||||
excess = firmware_sigma_m(self.inputs(), SigmaMethod.DLL_EXCESS)[0]
|
||||
naive = firmware_sigma_m(self.inputs(), SigmaMethod.NAIVE_ADD_DLL)[0]
|
||||
self.assertAlmostEqual(excess, current, places=12)
|
||||
self.assertGreater(naive, current)
|
||||
|
||||
def test_excess_method_inflates_hidden_jitter(self):
|
||||
current = firmware_sigma_m(self.inputs(9.0), SigmaMethod.CURRENT)[0]
|
||||
excess = firmware_sigma_m(self.inputs(9.0), SigmaMethod.DLL_EXCESS)[0]
|
||||
self.assertGreater(excess, 1.8 * current)
|
||||
|
||||
|
||||
class ScenarioTests(unittest.TestCase):
|
||||
def test_hidden_jitter_dynamic_sigma_improves_seeded_case(self):
|
||||
scenario = next(
|
||||
item
|
||||
for item in scenario_matrix()
|
||||
if item.name == "one_channel_hidden_jitter"
|
||||
)
|
||||
samples = run_scenario(
|
||||
scenario,
|
||||
17,
|
||||
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
|
||||
)
|
||||
current = np.asarray(
|
||||
[
|
||||
item.position_error_3d_m
|
||||
for item in samples
|
||||
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
|
||||
]
|
||||
)
|
||||
dynamic = np.asarray(
|
||||
[
|
||||
item.position_error_3d_m
|
||||
for item in samples
|
||||
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
|
||||
]
|
||||
)
|
||||
self.assertLess(
|
||||
np.sqrt(np.mean(dynamic * dynamic)),
|
||||
0.90 * np.sqrt(np.mean(current * current)),
|
||||
)
|
||||
|
||||
def test_stable_multipath_is_not_solved_by_variance_only(self):
|
||||
scenario = next(
|
||||
item
|
||||
for item in scenario_matrix()
|
||||
if item.name == "stable_multipath_bias"
|
||||
)
|
||||
samples = run_scenario(
|
||||
scenario,
|
||||
23,
|
||||
(SigmaMethod.CURRENT, SigmaMethod.DLL_EXCESS),
|
||||
)
|
||||
current = np.asarray(
|
||||
[
|
||||
item.position_error_3d_m
|
||||
for item in samples
|
||||
if item.method == SigmaMethod.CURRENT and item.time_s >= 5.0
|
||||
]
|
||||
)
|
||||
dynamic = np.asarray(
|
||||
[
|
||||
item.position_error_3d_m
|
||||
for item in samples
|
||||
if item.method == SigmaMethod.DLL_EXCESS and item.time_s >= 5.0
|
||||
]
|
||||
)
|
||||
relative_difference = abs(
|
||||
np.sqrt(np.mean(dynamic * dynamic))
|
||||
/ np.sqrt(np.mean(current * current))
|
||||
- 1.0
|
||||
)
|
||||
self.assertLess(relative_difference, 0.08)
|
||||
|
||||
def test_doppler_prediction_avoids_blind_iir_dynamic_lag(self):
|
||||
result = run_smoothing_trial(31, doppler_cfo_mps=0.0)
|
||||
self.assertLess(result.doppler_smoothed_rmse_m, result.raw_rmse_m)
|
||||
self.assertLess(
|
||||
result.doppler_peak_dynamic_error_m,
|
||||
0.10 * result.scalar_iir_peak_dynamic_error_m,
|
||||
)
|
||||
self.assertGreater(
|
||||
result.firmware_iir_doppler_peak_dynamic_error_m,
|
||||
result.doppler_peak_dynamic_error_m,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,12 +5,15 @@ TARGET := gps_system
|
||||
# directory previously compiled every GPS_LOG call out and produced a silent
|
||||
# UART image.
|
||||
GPS_DEBUG ?= 1
|
||||
GPS_DEBUG_LEVEL ?= 4
|
||||
GPS_DEBUG_LEVEL ?= 1
|
||||
GPS_BUILD_ID ?= $(shell git describe --always --dirty --abbrev=12 2>/dev/null || printf unknown)
|
||||
|
||||
# The current Genesys2 Rocket/fabric domain that clocks the UART is 50 MHz.
|
||||
# Keep the requested 115200 baud physical rather than producing 57600 baud
|
||||
# with the common 100 MHz fallback divider.
|
||||
# The 50 Hz fused-location stream plus GPS diagnostics does not fit safely in
|
||||
# the polling UART budget at 115200 baud.
|
||||
BSP_UART_CLOCK_HZ ?= 50000000
|
||||
BSP_UART_BAUDRATE ?= 460800
|
||||
BSP_CLINT_TIMEBASE_HZ ?= 500000
|
||||
|
||||
# Select one sensor backend, or keep the default read-only WHO_AM_I probe.
|
||||
# Accepted values: auto, tdk, st (stm is an alias for st).
|
||||
@@ -42,6 +45,9 @@ endif
|
||||
IMU_ACCEL_FS_G ?= 0
|
||||
IMU_GYRO_FS_DPS ?= 0
|
||||
GPS_ESKF_ENABLE ?= 1
|
||||
# UART telemetry cadence only; ESKF prediction still runs for every IMU sample.
|
||||
# At the default 50 Hz ODR, 5 samples produce one corrected record every 100 ms.
|
||||
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES ?= 5
|
||||
|
||||
APP_SRCS := \
|
||||
gps_system.c \
|
||||
@@ -60,9 +66,14 @@ APP_SRCS := \
|
||||
src/adapters/gps_fusion_publisher.c \
|
||||
src/gps/gps_ip.c \
|
||||
src/gps/gps_interrupt_handlers.c \
|
||||
src/gps/gps_acq_manager.c \
|
||||
src/gps/gps_ephemeris_cache.c \
|
||||
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 \
|
||||
src/gps/matrixLib.c
|
||||
@@ -70,16 +81,24 @@ APP_SRCS := \
|
||||
BSP_SRCS := init.c uart.c plic.c irq.c spi_flash.c pmod_spi.c
|
||||
|
||||
GPS_NAV_SOLVE_TIMER_TICKS ?= 10
|
||||
GPS_PPS_STATUS_LOG_INTERVAL ?= 10
|
||||
GPS_PPS_DOPPLER_PREDICTOR_ENABLE ?= 1
|
||||
GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES ?= 0
|
||||
|
||||
include ../common/app.mk
|
||||
|
||||
CFLAGS += -Iheader
|
||||
CFLAGS += $(IMU_BACKEND_CFLAG) \
|
||||
-DGPS_BUILD_ID=\"$(GPS_BUILD_ID)\" \
|
||||
-DIMU_ODR_HZ=$(IMU_ODR_HZ) \
|
||||
-DIMU_ACCEL_FS_G=$(IMU_ACCEL_FS_G) \
|
||||
-DIMU_GYRO_FS_DPS=$(IMU_GYRO_FS_DPS) \
|
||||
-DGPS_ESKF_ENABLE=$(GPS_ESKF_ENABLE) \
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS)
|
||||
-DIMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=$(IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES) \
|
||||
-DGPS_NAV_SOLVE_TIMER_TICKS=$(GPS_NAV_SOLVE_TIMER_TICKS) \
|
||||
-DGPS_PPS_STATUS_LOG_INTERVAL=$(GPS_PPS_STATUS_LOG_INTERVAL) \
|
||||
-DGPS_PPS_DOPPLER_PREDICTOR_ENABLE=$(GPS_PPS_DOPPLER_PREDICTOR_ENABLE) \
|
||||
-DGPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES=$(GPS_PPS_DOPPLER_PREDICTOR_AB_UPDATES)
|
||||
LDLIBS += -lm
|
||||
|
||||
ifeq ($(GPS_DEBUG),1)
|
||||
|
||||
@@ -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
|
||||
@@ -165,7 +239,8 @@ uzakta kalirsa, yalniz hard NIS rejection kullanmak filtreyi yanlis baslangica
|
||||
kilitler. Fusion engine bu nedenle ardisik reddedilen position fix'lerinin
|
||||
zamansal surekliligini izler. Son başarılı fusion'dan sonra yedi saniye timeout
|
||||
dolmuşsa ve en az uc fix birbirinin devamı
|
||||
ise yalniz ESKF konumu ve konum kovaryansi son olcume yeniden ankrajlanir;
|
||||
ise ve platform IMU/GNSS hareket vetolariyla duragan kabul ediliyorsa yalniz
|
||||
ESKF konumu ve konum kovaryansi son olcume yeniden ankrajlanir;
|
||||
attitude, hiz ve IMU bias state'leri korunur, gecersiz hale gelen position
|
||||
cross-covariance'lari sifirlanir. Tek aykiri fix bu yolu tetiklemez. Bu davranis
|
||||
100/200/1000 ms GPS oranlarinda `gps_reacquisition_test.c` ile dogrulanir ve
|
||||
@@ -198,9 +273,9 @@ bagimsiz yuksek-ODR zamanlama gerekirse sensor FIFO timestamp'i kullanilmalidir.
|
||||
|
||||
### Level-1 konum UART protokolu
|
||||
|
||||
Her gecerli GPS navigation epoch'unda level 1'de iki konum satiri art arda
|
||||
yayinlanir. Ilk satir WLS GPS konumu, ikinci satir o anda mevcut IMU+ESKF
|
||||
duzeltilmis konumdur:
|
||||
Her gecerli GPS navigation epoch'unda ham WLS GPS konumu yayinlanir. ESKF
|
||||
duzeltilmis konumu ise GPS epoch'undan bagimsiz olarak IMU ornek sayaci
|
||||
uzerinden yayinlanir:
|
||||
|
||||
```text
|
||||
[LOCATION] source=gps lat=... lon=... height=... gdop=... pdop=... hdop=... vdop=... tdop=... tracked=... used=...
|
||||
@@ -208,10 +283,28 @@ duzeltilmis konumdur:
|
||||
```
|
||||
|
||||
`tracked`, donanimda calisan tracking kanal sayisini; `used`, ilgili WLS
|
||||
cozumunde kullanilan uydu sayisini belirtir. DOP alanlari ayni GPS epoch'unun
|
||||
geometrisidir ve iki satirda ayni tasinir. ESKF henuz kalibrasyon/origin
|
||||
asamasindaysa yalniz GPS satiri yayinlanir; ESKF valid olduktan sonra her GPS
|
||||
konumu geldiginde iki satir da yayinlanir. Eski `[NAV] position` ve periyodik
|
||||
cozumunde kullanilan uydu sayisini belirtir. ESKF henuz kalibrasyon/origin
|
||||
asamasindaysa yalniz GPS satiri yayinlanir. Origin kurulduktan sonra GPS
|
||||
reddedilse veya gecici olarak kesilse de corrected konum IMU ile ilerlemeye
|
||||
devam eder.
|
||||
|
||||
Varsayilan corrected UART periyodu bes IMU ornegidir. 50 Hz sensor hedefinde
|
||||
bu yaklasik 100 ms/10 Hz demektir. Bu bolucu yalniz UART logunu seyreltir;
|
||||
sensor okuma ve ESKF prediction her IMU orneginde calismaya devam eder:
|
||||
|
||||
```sh
|
||||
# Varsayilan: 20 ms x 5 ~= 100 ms
|
||||
make -C soc/software/gps_system IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=5
|
||||
|
||||
# 20 ms x 2 ~= 40 ms
|
||||
make -C soc/software/gps_system IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES=2
|
||||
|
||||
# Parent software hedefi uzerinden ayni ayar
|
||||
make -C soc/software gps-system GPS_SYSTEM_IMU_PUBLISH_EVERY_N_SAMPLES=5
|
||||
```
|
||||
|
||||
Deger en az `1` olmalidir. Secilen bolucu startup satirinda
|
||||
`publish_every=` alaniyla gorulur. Eski `[NAV] position` ve periyodik
|
||||
`[FUSION] lat/lon` ayrintilari daha yuksek debug seviyesindedir; level 1
|
||||
parser'inin ayni konumu birden fazla kez gormesi engellenir.
|
||||
|
||||
@@ -418,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:
|
||||
@@ -608,7 +699,7 @@ GPS_EVENT_QUEUE_SIZE
|
||||
gps_last_measurement_tick
|
||||
gps_measurement_valid
|
||||
gps_nav_solve_sequence
|
||||
gps_nav_solve_consumed_sequence
|
||||
gps_last_nav_solve_pps_counter
|
||||
```
|
||||
|
||||
- Navigation input'u hazir kabul edilmeden once su kontroller yapilir:
|
||||
@@ -646,10 +737,10 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
|
||||
- `NAV_SOLVE_INTR_EN=1`,
|
||||
- `NAV_SOLVE_FLAG_EN=1`.
|
||||
- Ilk pozisyon dahil butun pozisyon hesaplari, kanallardan senkron nav-solve tick ve `ABS_SAMPLE` geldikten sonra olusturulan coherent nav-solve snapshot'i ile yapilir.
|
||||
- Nav-bit ve nav-solve handler'lari ephemeris deposunu ortak kullansa da corrected `ABS_SAMPLE` nav-solve icin node icinde ayri saklanir. Pozisyon snapshot'i bu ayri alani kullanir; araya giren nav-bit interrupt'i pozisyon olcum epoch'unu degistiremez.
|
||||
- Nav-bit ve nav-solve handler'lari canli ephemeris deposunu ortak kullansa da her kabul edilen nav-solve event'i corrected `ABS_SAMPLE`, TOW ve o anda commit edilmis ephemerisin tam kopyasini aktif epoch snapshot'ina birlikte yazar. WLS daha sonra canli node listesinden ephemeris okumaz; araya giren nav-bit interrupt'i eski olcumle yeni ephemerisi karistiramaz.
|
||||
- Coherent snapshot olusturulurken 32-bit raw `ABS_SAMPLE` degerleri en yeni sample etrafinda modulo fark ile unwrap edilir. Sayac wrap sinirinin iki tarafindaki uydular ayni pseudorange hesabinda kullanilabilir.
|
||||
- Nav bits akisi ilk tutarli subframe 1..3 seti commit edildikten sonra kapatilmaz. Boylece yeni subframe 1..3 verisi ephemerisi, subframe 4 page 18 verisi iyonosfer/UTC parametrelerini guncel tutar.
|
||||
- Nav-solve interrupt'lari acilmadan once TOW, nav-bit HOW alanindan kurulur. Nav-solve acildiktan sonra normal nav-bit HOW gecmisi tutulmaya devam eder fakat canli TOW'a yazmaz. Her nav-solve interrupt'i tam olarak `NAV_SOLVE_TIMER * 20 ms` ilerlemeyi temsil eder ve TOW bu sabit miktarda artirilir.
|
||||
- Nav-solve interrupt'lari acilmadan once TOW, parity kontrolunden gecen nav-bit HOW alanindan kurulur. Her yeni HOW'un GPS zaman farki receiver sample-counter farkiyla karsilastirilir; tek bir sureksiz HOW saglikli anchor'i degistiremez. Iki ardışık aday kendi aralarinda tutarliysa kanal reset gerektirmeden yeni zaman cizgisine gecerek toparlanir. Subframe 1 gelmeden onceki gecici hafta, ilk gecerli WN ile atomik olarak guncellenir.
|
||||
- Subframe 1..3 ham kelimeleri once staging alaninda biriktirilir. IODC'nin alt 8 biti ile iki IODE alani eslesen tam set olusmadan canli ephemeris degistirilmez; yayin guncellemesi sirasinda eski/yeni ephemeris alanlari karismaz.
|
||||
- Subframe 1 `toc` alani parity bitleri disarida birakilarak 16 bit okunur ve `2^4` ile olceklenir.
|
||||
- Subframe 4 page 18 henuz alinmadiysa iyonosfer duzeltmesi uygulanmaz. Page 18 hazir oldugunda her navigation solve tek bir `ionoutc` kopyasi kullanir.
|
||||
@@ -659,9 +750,10 @@ GPS_TRK_MEASUREMENT_STALE_TICKS
|
||||
- Tracking kanallari farkli zamanlarda lock olabilir ve ilk gecerli subframe'lerini 6 saniyenin farkli katlarinda gorebilir. Timer bu nedenle ilk subframe'in HOW alanindaki 17-bit Z-count degerini kullanarak `Z_COUNT * 300 mod NAV_SOLVE_TIMER` baslangic fazini hesaplar. Hesap ALIGN durumunda 26 PCLK cevriminde ard arda modulo ile yapilir; 20 ms'lik ilk navigation bitinden cok once tamamlanir. RTL ayni GPS haftasi icinde keyfi pozitif `N` degerlerini hizalayabilir; firmware ise hafta rollover'i oncesinden aktif kalan kanallarla rollover sonrasinda katilan kanallarin da ayni fazda kalmasi icin `30240000 mod N == 0` kosulunu compile time'da zorunlu tutar. Ornegin `N=40`, `N=100`, `N=125`, `N=150`, `N=200` ve `N=300` gecerlidir; kosulu saglamayan build acik bir `#error` ile durur.
|
||||
- Initial position sonrasinda subframe'lerini tamamlayan yeni tracking kanallarinda da nav bits acik tutularak nav-solve otomatik etkinlestirilir.
|
||||
- Coherence kontrolu sadece software systick ile yapilmaz; periodic snapshot icin tracking node'da saklanan nav-solve interrupt sonrasi raw `ABS_SAMPLE` kullanilir. `GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS=25` oldugu icin 4 satellite measurement'i 25 ms'den fazla ayriksa navigation solve calistirilmaz. Bu pencere interrupt skew'den cok uydu path delay farkini kapsamak icindir: 20 ms yaklasik 6000 km, 25 ms yaklasik 7500 km pseudorange farkina denk gelir. 25 ms, dusuk-elevation uydu geometrisi ve tracking jitter icin pay birakir ama 50 ms kadar gevsek degildir.
|
||||
- Her nav periyodunda yeni sequence'i bulunan adaylar sabit boyutlu, heap kullanmayan epoch selector'a verilir. Selector TOW ve modulo-32-bit `ABS_SAMPLE` kosullarini ikili olarak saglayan en buyuk coherent alt kumeyi secer; tek bir gecikmis veya bozuk kanal diger en az 4 uyduyu bloke etmez. Esit buyuklukte ayrik kumelerde daha yeni sample epoch'u tercih edilir.
|
||||
- En az 4 uydu secildiginde kume disindaki snapshot sequence'leri emekliye ayrilir. Tuketim snapshot aninda kaydedilen sequence degerine kadar yapildigi icin arada gelen daha yeni interrupt korunur. Tamamlanan donanim epoch'u WLS sonucu basarili veya basarisiz olsa da yalniz bir kez denenir; ayni olcumlerin ilgisiz bir foreground wake-up ile tekrar cozulmesi engellenir.
|
||||
- Navigation snapshot ayni SVID/PRN'i ikinci kez kabul etmez. Ayni uydu iki tracking IP uzerinden ready gorunurse ikinci kopya sessizce snapshot disinda birakilir; ayrinti sadece `DEBUG_TRACE` seviyesinde gorulur.
|
||||
- Interrupt'lar epoch'a GPS TOW ile degil, butun kanallarda ortak olan 64-bit PPS capture counter ile atanir. Ilk olcum sabit expected maskeyi ve 30 ms bounded holdoff'u baslatir. 25 ms pencereye giren kanallar ayni fiziksel epoch'a katilir; yarim nav periyodundan uzaktaki eski/yeni capture'lar stale veya yeni epoch olarak siniflanir. Boylece bozuk bir gelecek HOW degeri epoch assembler'i kilitleyemez.
|
||||
- Yalniz frozen expected maskenin tamami geldiginde snapshot sabit boyutlu iki buffer arasinda immutable olarak foreground'a devredilir. 30 ms holdoff dolan eksik epoch ve daha yeni counter geldigi anda yarim kalan eski epoch butunuyle discard edilir. Hardware mailbox birden fazla pulse'u kacirdiysa sequence/counter farki tutarli olan en yeni gercek olcum kabul edilir; elde olmayan ara olcumler sentetik olarak uretilmez. Sequence ile 64-bit capture counter uyusmazsa event yalniz rebase icin kullanilir ve epoch'a girmez.
|
||||
- Immutable snapshot icindeki butun adaylar heap kullanmayan selector ile ikili TOW ve modulo-32-bit `ABS_SAMPLE` coherence kontrolunden gecirilir. En buyuk coherent kume frozen aday sayisindan bir eksik bile olsa tum epoch reddedilir; WLS icin alt kumeye dusulmez. Tamamlanan snapshot WLS sonucu basarili veya basarisiz olsa da yalniz bir kez denenir.
|
||||
- Navigation snapshot ayni SVID/PRN'i ikinci kez kabul etmez. Ayni uydu iki tracking IP uzerinden frozen ready maskeye girerse aday sayisi maskeyle uyusmayacagi icin tum epoch reddedilir; ayni uydu iki kez WLS'e giremez.
|
||||
- UART debug satirindaki `coherent_nav=` degeri, o anda ayni nav-solve penceresinde kullanilabilir kanal sayisini gosterir. 600 ms pozisyon gelmiyorsa `busy channels`, `ready channels` ve `coherent_nav` birlikte kontrol edilmelidir.
|
||||
- Tracking ISR event queue dolarsa, her tracking IP icin son event shadow buffer'a alinir. Shadow overwrite sayaci WARN olarak loglanir; bu log gorulurse CPU nav hesaplamasi veya UART debug cikisi tracking interrupt isleme hizini geciktiriyor olabilir.
|
||||
- Bir tracking kanali shadow buffer'a dustukten sonra shadow main context tarafindan tuketilene kadar ayni kanalin yeni event'leri tekrar ana queue'ya alinmaz; en yeni shadow snapshot'i korunur. Boylece daha yeni queue event'inin daha eski shadow event'inden once islenmesi ve raw sample zamaninin geriye gitmesi engellenir.
|
||||
@@ -678,38 +770,195 @@ 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.
|
||||
- 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. WLS ancak bu kanallarin ayni GPS epoch'una ait interrupt'lari tamamlandiginda tetiklenir. Unlock interrupt'i gelen kanal bekleme maskesinden cikarilir; epoch ortasinda navigation-ready olan yeni kanal ise aktif maskeyi buyutmez ve sonraki epoch'tan itibaren beklenir. Timeout yoktur. Eksik kalan bir epoch'tan daha yeni bir interrupt gelirse eski maske temizlenir ve yeni epoch ilk kanaldan baslatilarak butun ready kanallar yeniden beklenir; eski ve yeni olcumler birlestirilmez.
|
||||
- Interrupt bekleme maskesi tamamlandiktan sonra en az dort uyduluk en buyuk ortak TOW/`ABS_SAMPLE` kumesi WLS girdisi olarak kullanilir; kume disi kanallar `epoch_excluded` olarak raporlanir ve sonraki donanim epoch'unda yeniden katilabilir.
|
||||
- `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
|
||||
`dof=N-4=0` oldugu icin WLS konumu publish edilir fakat residual chi-square
|
||||
integrity bayragi false kalir. Bes veya daha fazla uyduda publish icin mevcut
|
||||
@@ -740,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 `[TRK] tracked satellites=... prns=[...]`. 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
|
||||
@@ -794,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.
|
||||
|
||||
@@ -938,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:
|
||||
@@ -976,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.
|
||||
|
||||
@@ -18,6 +18,33 @@
|
||||
#error "GPS_ESKF_ENABLE must be 0 or 1"
|
||||
#endif
|
||||
|
||||
#ifndef IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES
|
||||
#define IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES 5u
|
||||
#endif
|
||||
|
||||
#if (IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES < 1)
|
||||
#error "IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES must be at least one"
|
||||
#endif
|
||||
|
||||
#define IMU_SAMPLES_BEFORE_GPS_SERVICE 1u
|
||||
#define IMU_SAMPLES_AFTER_GPS_SERVICE 3u
|
||||
|
||||
static void update_eskf_doppler_bandwidth(
|
||||
const navigation_fusion_output_t* output)
|
||||
{
|
||||
if((output == NULL) || !output->local_valid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
double alpha = 0.05;
|
||||
if(output->motion_state == FUSION_MOTION_MOVING)
|
||||
{
|
||||
alpha = (output->linear_acceleration_norm_mps2 >= 2.5) ?
|
||||
0.40 : 0.20;
|
||||
}
|
||||
gps_receiver_set_eskf_doppler_alpha(alpha);
|
||||
}
|
||||
|
||||
static void initialize_sensor_fusion(gps_system_state_t* system)
|
||||
{
|
||||
system->fusion_enabled = (GPS_ESKF_ENABLE != 0);
|
||||
@@ -43,18 +70,46 @@ static void initialize_sensor_fusion(gps_system_state_t* system)
|
||||
true); /* Current prototype is the PX4-equivalent unarmed bench mode. */
|
||||
}
|
||||
|
||||
static unsigned int feed_pending_imu_to_eskf(gps_system_state_t* system)
|
||||
static unsigned int feed_pending_imu_to_eskf(
|
||||
gps_system_state_t* system,
|
||||
unsigned int max_samples)
|
||||
{
|
||||
if(!system->fusion_initialized)
|
||||
if(!system->fusion_initialized || (max_samples == 0u))
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
|
||||
unsigned int processed = 0u;
|
||||
imu_sensor_service();
|
||||
while(imu_sensor_take_measurement(&system->imu_sample))
|
||||
while((processed < max_samples) &&
|
||||
imu_sensor_take_measurement(&system->imu_sample))
|
||||
{
|
||||
(void)navigation_fusion_push_imu(&system->imu_sample);
|
||||
bool accepted = navigation_fusion_push_imu(&system->imu_sample);
|
||||
bool output_available = accepted &&
|
||||
navigation_fusion_get_output(&system->fused_solution);
|
||||
if(output_available)
|
||||
{
|
||||
update_eskf_doppler_bandwidth(&system->fused_solution);
|
||||
}
|
||||
if(output_available && system->fused_solution.global_valid &&
|
||||
(system->fused_solution.timestamp_ticks !=
|
||||
system->last_fused_location_tick))
|
||||
{
|
||||
system->fused_publish_sample_count++;
|
||||
bool first_location =
|
||||
(system->last_fused_location_tick == 0u);
|
||||
bool publish_due =
|
||||
(system->fused_publish_sample_count >=
|
||||
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES);
|
||||
if(first_location || publish_due)
|
||||
{
|
||||
gps_system_uart_publish_fused_location(
|
||||
&system->fused_solution);
|
||||
system->last_fused_location_tick =
|
||||
system->fused_solution.timestamp_ticks;
|
||||
system->fused_publish_sample_count = 0u;
|
||||
}
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
return processed;
|
||||
@@ -76,23 +131,36 @@ int main(void)
|
||||
gps_receiver_start_acquisition();
|
||||
gps_system_uart_publish_startup(
|
||||
system.fusion_enabled, system.fusion_initialized,
|
||||
system.imu_initialized, system.imu_running);
|
||||
system.imu_initialized, system.imu_running,
|
||||
IMU_CORRECTED_PUBLISH_EVERY_N_SAMPLES);
|
||||
|
||||
bool imu_backlog_may_remain = false;
|
||||
for(;;)
|
||||
{
|
||||
/* GPS, IMU data-ready and the maintenance timer wake this loop. */
|
||||
asm volatile("wfi");
|
||||
/* Skip WFI only while draining a bounded IMU backlog. Every batch
|
||||
* still services GPS before more prediction work, so a continuously
|
||||
* non-empty IMU queue cannot starve navigation epochs. */
|
||||
if(!imu_backlog_may_remain)
|
||||
{
|
||||
/* GPS, IMU data-ready and the maintenance timer wake this loop. */
|
||||
asm volatile("wfi");
|
||||
}
|
||||
|
||||
/* IMU prediction runs at the sensor's own timestamped rate. */
|
||||
(void)feed_pending_imu_to_eskf(&system);
|
||||
/* Limit pre-GPS work to one sample for low event latency. */
|
||||
(void)feed_pending_imu_to_eskf(
|
||||
&system, IMU_SAMPLES_BEFORE_GPS_SERVICE);
|
||||
|
||||
/* GPS remains an independent receiver: events first, then coherent WLS. */
|
||||
gps_receiver_process_pending_events();
|
||||
system.gps_epoch_ready = gps_receiver_solve_ready_epoch(
|
||||
&system.gps_solution, &system.gps_measurement_ticks);
|
||||
|
||||
/* WLS may take long enough for more IMU samples to arrive. */
|
||||
(void)feed_pending_imu_to_eskf(&system);
|
||||
/* WLS may take long enough for more IMU samples to arrive. Drain a
|
||||
* small batch, then return to GPS service before continuing. */
|
||||
unsigned int imu_after_gps = feed_pending_imu_to_eskf(
|
||||
&system, IMU_SAMPLES_AFTER_GPS_SERVICE);
|
||||
imu_backlog_may_remain =
|
||||
(imu_after_gps == IMU_SAMPLES_AFTER_GPS_SERVICE);
|
||||
|
||||
if(system.gps_epoch_ready && system.gps_solution.solution_valid)
|
||||
{
|
||||
@@ -114,15 +182,6 @@ int main(void)
|
||||
/* Delayed GPS update is applied at its capture timestamp; newer
|
||||
* IMU history is replayed to produce the current fused state. */
|
||||
(void)navigation_fusion_push_gps(&system.gps_for_fusion);
|
||||
if(navigation_fusion_get_output(&system.fused_solution) &&
|
||||
system.fused_solution.global_valid)
|
||||
{
|
||||
gps_system_uart_publish_location("imu_corrected",
|
||||
system.fused_solution.latitude_deg,
|
||||
system.fused_solution.longitude_deg,
|
||||
system.fused_solution.height_m,
|
||||
&system.gps_solution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,15 @@ void gps_system_uart_publish_location(
|
||||
double height_m,
|
||||
const gps_nav_solution_t* gps);
|
||||
|
||||
void gps_system_uart_publish_fused_location(
|
||||
const navigation_fusion_output_t* output);
|
||||
|
||||
void gps_system_uart_publish_startup(
|
||||
bool fusion_enabled,
|
||||
bool fusion_initialized,
|
||||
bool imu_initialized,
|
||||
bool imu_running);
|
||||
bool imu_running,
|
||||
unsigned int fused_publish_every_samples);
|
||||
|
||||
void gps_system_uart_publish_periodic_fusion_status(
|
||||
navigation_fusion_output_t* output,
|
||||
|
||||
@@ -27,6 +27,7 @@ typedef struct {
|
||||
double gravity_mps2;
|
||||
double max_propagation_step_s;
|
||||
double position_nis_gate;
|
||||
double velocity_nis_gate;
|
||||
double covariance_diagonal_floor;
|
||||
} eskf_config_t;
|
||||
|
||||
@@ -40,9 +41,12 @@ typedef struct {
|
||||
double last_linear_accel_nav_mps2[ESKF_VECTOR_DIM];
|
||||
double last_unbiased_gyro_rps[ESKF_VECTOR_DIM];
|
||||
double last_position_nis;
|
||||
double last_velocity_nis;
|
||||
uint64_t propagation_count;
|
||||
uint64_t position_update_accepted_count;
|
||||
uint64_t position_update_rejected_count;
|
||||
uint64_t velocity_update_accepted_count;
|
||||
uint64_t velocity_update_rejected_count;
|
||||
bool initialized;
|
||||
} eskf_t;
|
||||
|
||||
@@ -84,6 +88,13 @@ eskf_update_result_t eskf_position_update(
|
||||
const double covariance_m2[ESKF_VECTOR_DIM * ESKF_VECTOR_DIM],
|
||||
double* nis_out);
|
||||
|
||||
eskf_update_result_t eskf_velocity_update(
|
||||
eskf_t* filter,
|
||||
const eskf_config_t* config,
|
||||
const double velocity_mps[ESKF_VECTOR_DIM],
|
||||
const double covariance_mps2[ESKF_VECTOR_DIM * ESKF_VECTOR_DIM],
|
||||
double* nis_out);
|
||||
|
||||
/* Re-anchor position after a higher-level fusion timeout has established that
|
||||
* repeated, valid position observations are mutually coherent. Velocity,
|
||||
* attitude and IMU biases are preserved; position cross-covariances are
|
||||
|
||||
@@ -80,6 +80,9 @@ typedef struct {
|
||||
uint64_t position_pending_overflow;
|
||||
uint64_t position_accepted;
|
||||
uint64_t position_recovery_count;
|
||||
uint64_t velocity_received;
|
||||
uint64_t velocity_nis_rejected;
|
||||
uint64_t velocity_accepted;
|
||||
uint64_t replay_count;
|
||||
uint64_t replayed_imu_count;
|
||||
uint64_t zero_velocity_update_count;
|
||||
@@ -104,7 +107,7 @@ typedef struct {
|
||||
fusion_runtime_t checkpoint;
|
||||
fusion_calibration_accumulator_t calibration;
|
||||
fusion_imu_measurement_t imu_history[FUSION_IMU_HISTORY_CAPACITY];
|
||||
fusion_position_measurement_t pending_positions[FUSION_POSITION_PENDING_CAPACITY];
|
||||
fusion_navigation_measurement_t pending_positions[FUSION_POSITION_PENDING_CAPACITY];
|
||||
fusion_position_measurement_t initial_position;
|
||||
fusion_position_measurement_t position_recovery_candidate;
|
||||
fusion_engine_diagnostics_t diagnostics;
|
||||
@@ -133,6 +136,13 @@ bool fusion_engine_push_position(
|
||||
const fusion_position_measurement_t* measurement,
|
||||
eskf_update_result_t* update_result_out,
|
||||
double* nis_out);
|
||||
bool fusion_engine_push_navigation(
|
||||
fusion_engine_t* engine,
|
||||
const fusion_navigation_measurement_t* measurement,
|
||||
eskf_update_result_t* position_update_result_out,
|
||||
double* position_nis_out,
|
||||
eskf_update_result_t* velocity_update_result_out,
|
||||
double* velocity_nis_out);
|
||||
|
||||
bool fusion_engine_get_runtime(const fusion_engine_t* engine,
|
||||
fusion_runtime_t* runtime_out);
|
||||
|
||||
@@ -9,6 +9,9 @@ typedef struct {
|
||||
double minimum_horizontal_std_m;
|
||||
double minimum_vertical_std_m;
|
||||
double maximum_position_std_m;
|
||||
double minimum_horizontal_velocity_std_mps;
|
||||
double minimum_vertical_velocity_std_mps;
|
||||
double maximum_velocity_std_mps;
|
||||
} fusion_gps_adapter_config_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -27,6 +30,10 @@ bool fusion_gps_adapter_init(fusion_gps_adapter_t* adapter,
|
||||
bool fusion_gps_adapter_convert(fusion_gps_adapter_t* adapter,
|
||||
const fusion_gps_measurement_t* gps,
|
||||
fusion_position_measurement_t* position_out);
|
||||
bool fusion_gps_adapter_convert_navigation(
|
||||
fusion_gps_adapter_t* adapter,
|
||||
const fusion_gps_measurement_t* gps,
|
||||
fusion_navigation_measurement_t* navigation_out);
|
||||
bool fusion_gps_adapter_local_to_geodetic(
|
||||
const fusion_gps_adapter_t* adapter,
|
||||
const double position_enu_m[3],
|
||||
|
||||
@@ -21,8 +21,13 @@ typedef struct {
|
||||
double vacc_m;
|
||||
double residual_rms_m;
|
||||
double chi_square_ratio;
|
||||
double velocity_ecef_mps[3];
|
||||
double velocity_covariance_ecef_mps2[3][3];
|
||||
double speed_mps;
|
||||
double velocity_residual_rms_mps;
|
||||
unsigned int used_satellites;
|
||||
bool solution_valid;
|
||||
bool velocity_valid;
|
||||
} fusion_gps_measurement_t;
|
||||
|
||||
/* Timestamped IMU plus the independently supervised platform state consumed
|
||||
@@ -45,4 +50,16 @@ typedef struct {
|
||||
bool source_valid;
|
||||
} fusion_position_measurement_t;
|
||||
|
||||
/*
|
||||
* Position and Doppler velocity from one GNSS solve. Keeping them in one
|
||||
* message makes the fixed-lag coordinator apply both observations at exactly
|
||||
* the captured GPS epoch before replaying newer IMU samples.
|
||||
*/
|
||||
typedef struct {
|
||||
fusion_position_measurement_t position;
|
||||
double velocity_enu_mps[3];
|
||||
double velocity_covariance_enu_mps2[9];
|
||||
bool velocity_valid;
|
||||
} fusion_navigation_measurement_t;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
typedef struct {
|
||||
uint64_t timestamp_ticks;
|
||||
uint64_t last_accepted_gps_ticks;
|
||||
double position_enu_m[3];
|
||||
double velocity_enu_mps[3];
|
||||
double latitude_deg;
|
||||
@@ -18,11 +19,15 @@ typedef struct {
|
||||
double height_m;
|
||||
double position_std_m[3];
|
||||
double speed_mps;
|
||||
double linear_acceleration_norm_mps2;
|
||||
double last_position_nis;
|
||||
double last_velocity_nis;
|
||||
fusion_motion_state_t motion_state;
|
||||
uint64_t gps_accepted;
|
||||
uint64_t gps_nis_rejected;
|
||||
uint64_t gps_position_recoveries;
|
||||
uint64_t gps_velocity_accepted;
|
||||
uint64_t gps_velocity_nis_rejected;
|
||||
uint64_t zero_update_count;
|
||||
uint64_t replay_count;
|
||||
uint64_t stale_gps_count;
|
||||
@@ -53,8 +58,9 @@ void navigation_fusion_set_platform_landed(
|
||||
* ESKF observation and is never rate-selected in this layer. */
|
||||
bool navigation_fusion_push_imu(const imu_sample_t* measurement);
|
||||
|
||||
/* Observation input: convert a valid LLH fix to local ENU, apply it at the
|
||||
* captured GPS epoch, then replay newer IMU history to current time. */
|
||||
/* Observation input: convert a valid LLH fix and optional Doppler velocity to
|
||||
* local ENU, apply both at the captured GPS epoch, then replay newer IMU
|
||||
* history to current time. */
|
||||
bool navigation_fusion_push_gps(const fusion_gps_measurement_t* measurement);
|
||||
|
||||
/* local_valid can exist without GPS. global_valid/valid require a GPS origin. */
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
#include "gps.h"
|
||||
#include "gps/gps_ip_defines.h"
|
||||
#include "gps/gps_nav_epoch.h"
|
||||
#include "gps/matrixLib.h"
|
||||
|
||||
typedef struct gps_nav_solution {
|
||||
double ecef_m[3];
|
||||
double clock_bias_m;
|
||||
double clock_bias_variance_m2;
|
||||
double velocity_ecef_mps[3];
|
||||
double velocity_covariance_ecef_mps2[3][3];
|
||||
double clock_drift_mps;
|
||||
double speed_mps;
|
||||
double velocity_residual_rms_mps;
|
||||
double latitude_deg;
|
||||
double longitude_deg;
|
||||
double height_m;
|
||||
@@ -24,6 +31,13 @@
|
||||
double postfit_residual_m[GPS_TRCK_IP_COUNT];
|
||||
double elevation_deg[GPS_TRCK_IP_COUNT];
|
||||
double measurement_sigma_m[GPS_TRCK_IP_COUNT];
|
||||
double cn0_dbhz[GPS_TRCK_IP_COUNT];
|
||||
/* Raw Doppler is published with the ESKF velocity solution. */
|
||||
double doppler_hz[GPS_TRCK_IP_COUNT];
|
||||
double pseudorange_rate_mps[GPS_TRCK_IP_COUNT];
|
||||
/* Separate slow path used only by the PPS bias-drift predictor. */
|
||||
double filtered_doppler_hz[GPS_TRCK_IP_COUNT];
|
||||
double filtered_pseudorange_rate_mps[GPS_TRCK_IP_COUNT];
|
||||
double full_postfit_residual_m[GPS_TRCK_IP_COUNT];
|
||||
double full_elevation_deg[GPS_TRCK_IP_COUNT];
|
||||
double full_measurement_sigma_m[GPS_TRCK_IP_COUNT];
|
||||
@@ -43,6 +57,7 @@
|
||||
unsigned int degrees_of_freedom;
|
||||
uint32_t used_row_mask;
|
||||
uint32_t excluded_row_mask;
|
||||
uint32_t velocity_excluded_row_mask;
|
||||
uint64_t candidate_cycles;
|
||||
uint64_t full_solution_cycles;
|
||||
uint64_t fde_cycles;
|
||||
@@ -54,10 +69,12 @@
|
||||
bool chi_square_pass;
|
||||
bool fde_applied;
|
||||
bool solution_valid;
|
||||
bool velocity_valid;
|
||||
bool velocity_fde_applied;
|
||||
} gps_nav_solution_t;
|
||||
|
||||
void satpos(gps_ephemeris_t* eph, double* pos, double* vel, double* clk, double* clockCorr);
|
||||
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, mat_type *satellitePositions, mat_type * velocities);
|
||||
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, double* clockDrift, mat_type *satellitePositions, mat_type * velocities);
|
||||
double check_t(long double tkl);
|
||||
void subVect(double* y, const double* x1, const double* x2);
|
||||
double normVect(const double* x);
|
||||
@@ -65,6 +82,7 @@
|
||||
bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber,
|
||||
mat_type * az, mat_type * el,
|
||||
const unsigned char* ura_indices,
|
||||
const double* cn0_dbhz,
|
||||
const ionoutc_t* ionoutc, double gps_tow,
|
||||
gps_nav_solution_t* solution_out);
|
||||
void rotateSatelliteSTD(double const traveltime, const mat_type* X_sat, mat_type* X_sat_rot);
|
||||
|
||||
@@ -37,8 +37,21 @@
|
||||
#define WN_INDEX 2
|
||||
#define POSITION_VECTOR_COL 3
|
||||
#define MAX_24_BIT_INT 0x00800000
|
||||
// ADC sample frekansi
|
||||
#define SAMPLES_PER_CODE 16368.0
|
||||
/*
|
||||
* RF ADC sample rate used for time and pseudorange conversion.
|
||||
*
|
||||
* The acquisition RTL buffers an integer 16368 samples per nominal C/A
|
||||
* code period, but the Genesys2/VCU118 RF reference is 16.367981 MHz.
|
||||
* Using 16368.000 samples/ms as a frequency creates a 1.161 ppm timing
|
||||
* ramp which is reset by every six-second HOW anchor (about 6.97 us
|
||||
* peak-to-peak). Keep the physical clock rate distinct from the integer
|
||||
* acquisition-buffer length.
|
||||
*/
|
||||
#ifndef GPS_RF_SAMPLE_RATE_HZ
|
||||
#define GPS_RF_SAMPLE_RATE_HZ 16367981.0
|
||||
#endif
|
||||
#define GPS_RF_SAMPLES_PER_MILLISECOND (GPS_RF_SAMPLE_RATE_HZ / 1000.0)
|
||||
#define SAMPLES_PER_CODE GPS_RF_SAMPLES_PER_MILLISECOND
|
||||
|
||||
#define CODE_FREQ_BASIS 1.023E+6
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef GPS_ACQ_MANAGER_H
|
||||
#define GPS_ACQ_MANAGER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define GPS_ACQ_MANAGER_PRN_COUNT 32u
|
||||
#define GPS_ACQ_MANAGER_INVALID_PRN 0xffu
|
||||
|
||||
typedef enum {
|
||||
GPS_ACQ_PRN_DISABLED = 0,
|
||||
GPS_ACQ_PRN_COLD,
|
||||
GPS_ACQ_PRN_HOT,
|
||||
GPS_ACQ_PRN_ACQUIRING,
|
||||
GPS_ACQ_PRN_ACQUIRED,
|
||||
GPS_ACQ_PRN_TRACKING,
|
||||
GPS_ACQ_PRN_STALE
|
||||
} gps_acq_prn_state_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t retry_delay_ticks;
|
||||
uint64_t hot_retry_delay_ticks;
|
||||
uint64_t hot_window_ticks;
|
||||
unsigned int max_hot_streak;
|
||||
} gps_acq_manager_config_t;
|
||||
|
||||
typedef struct {
|
||||
gps_acq_prn_state_t state;
|
||||
uint64_t last_attempt_tick;
|
||||
uint64_t hot_until_tick;
|
||||
bool has_attempted;
|
||||
bool attempt_was_hot;
|
||||
} gps_acq_prn_entry_t;
|
||||
|
||||
typedef struct {
|
||||
gps_acq_manager_config_t config;
|
||||
gps_acq_prn_entry_t prn[GPS_ACQ_MANAGER_PRN_COUNT];
|
||||
unsigned int cold_cursor;
|
||||
unsigned int hot_cursor;
|
||||
unsigned int hot_streak;
|
||||
} gps_acq_manager_t;
|
||||
|
||||
void gps_acq_manager_init(
|
||||
gps_acq_manager_t* manager,
|
||||
const gps_acq_manager_config_t* config);
|
||||
bool gps_acq_manager_enable_cold(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn);
|
||||
gps_acq_prn_state_t gps_acq_manager_state(
|
||||
const gps_acq_manager_t* manager,
|
||||
unsigned int prn);
|
||||
const char* gps_acq_manager_state_name(gps_acq_prn_state_t state);
|
||||
|
||||
void gps_acq_manager_mark_acquired(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn);
|
||||
void gps_acq_manager_mark_tracking(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn);
|
||||
void gps_acq_manager_mark_stale(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn);
|
||||
void gps_acq_manager_mark_lost(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn,
|
||||
uint64_t now);
|
||||
void gps_acq_manager_mark_failed(
|
||||
gps_acq_manager_t* manager,
|
||||
unsigned int prn,
|
||||
uint64_t now);
|
||||
|
||||
bool gps_acq_manager_start_next(
|
||||
gps_acq_manager_t* manager,
|
||||
uint64_t now,
|
||||
unsigned int* prn_out,
|
||||
bool* hot_out);
|
||||
unsigned int gps_acq_manager_count_state(
|
||||
const gps_acq_manager_t* manager,
|
||||
gps_acq_prn_state_t state);
|
||||
unsigned int gps_acq_manager_count_searchable(
|
||||
const gps_acq_manager_t* manager);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef GPS_EPHEMERIS_CACHE_H
|
||||
#define GPS_EPHEMERIS_CACHE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "gps/gps_ip_defines.h"
|
||||
|
||||
typedef struct {
|
||||
gps_ephemeris_t ephemeris;
|
||||
uint64_t committed_tick;
|
||||
bool valid;
|
||||
} gps_ephemeris_cache_entry_t;
|
||||
|
||||
typedef struct {
|
||||
gps_ephemeris_cache_entry_t entry[GPS_PRN_COUNT];
|
||||
uint64_t max_age_ticks;
|
||||
} gps_ephemeris_cache_t;
|
||||
|
||||
void gps_ephemeris_cache_init(
|
||||
gps_ephemeris_cache_t* cache,
|
||||
uint64_t max_age_ticks);
|
||||
bool gps_ephemeris_cache_store(
|
||||
gps_ephemeris_cache_t* cache,
|
||||
unsigned int prn,
|
||||
const gps_ephemeris_t* ephemeris,
|
||||
uint64_t now);
|
||||
bool gps_ephemeris_cache_restore(
|
||||
gps_ephemeris_cache_t* cache,
|
||||
unsigned int prn,
|
||||
uint64_t now,
|
||||
gps_ephemeris_t* ephemeris_out);
|
||||
void gps_ephemeris_cache_invalidate(
|
||||
gps_ephemeris_cache_t* cache,
|
||||
unsigned int prn);
|
||||
|
||||
#endif
|
||||
@@ -2,20 +2,42 @@
|
||||
#define GPS_INT_HAND
|
||||
|
||||
#include "gps/gps_ip_defines.h"
|
||||
#include "gps/gps_tracking_measurement.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t event_overflow_total;
|
||||
uint32_t plic_drain_limit_total;
|
||||
uint32_t plic_repeat_total;
|
||||
uint32_t nav_epoch_coalesced_total;
|
||||
uint32_t pps_sequence_discontinuity_total;
|
||||
uint32_t nav_epoch_incomplete_total;
|
||||
uint32_t nav_epoch_snapshot_overwrite_total;
|
||||
uint32_t how_anchor_rejected_total;
|
||||
uint32_t measurement_missed_total;
|
||||
uint32_t measurement_invalid_total;
|
||||
uint32_t satellite_connected_total;
|
||||
uint32_t satellite_lost_total;
|
||||
uint32_t ephemeris_cache_store_total;
|
||||
uint32_t ephemeris_cache_restore_total;
|
||||
uint32_t last_connected_prn;
|
||||
uint32_t last_lost_prn;
|
||||
uint64_t last_connected_tick;
|
||||
uint64_t last_lost_tick;
|
||||
} gps_runtime_diagnostics_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t channel_mask;
|
||||
uint32_t prn[GPS_TRCK_IP_COUNT];
|
||||
gps_ephemeris_t ephemeris[GPS_TRCK_IP_COUNT];
|
||||
gpstime_t tow[GPS_TRCK_IP_COUNT];
|
||||
uint32_t raw_sample[GPS_TRCK_IP_COUNT];
|
||||
double corrected_abs_sample[GPS_TRCK_IP_COUNT];
|
||||
uint64_t pps_counter[GPS_TRCK_IP_COUNT];
|
||||
uint32_t pps_capture_sequence[GPS_TRCK_IP_COUNT];
|
||||
uint64_t local_timestamp_ticks[GPS_TRCK_IP_COUNT];
|
||||
gps_tracking_measurement_t measurement[GPS_TRCK_IP_COUNT];
|
||||
} gps_nav_epoch_snapshot_t;
|
||||
|
||||
typedef void (*gps_external_irq_handler_t)(void);
|
||||
|
||||
bool assign_tracking_from_result(const gps_acq_result_t* result);
|
||||
@@ -26,10 +48,15 @@
|
||||
gps_external_irq_handler_t handler);
|
||||
void gps_irq_handler(void) GPS_IRQ_HANDLER_ATTR;
|
||||
void gps_process_isr_events(void);
|
||||
void gps_acq_configure_prns(
|
||||
const unsigned char* prns,
|
||||
unsigned int count);
|
||||
void gps_acq_scheduler_run(void);
|
||||
void gps_background_task(void);
|
||||
bool gps_take_nav_solve_period_ready(void);
|
||||
bool gps_take_nav_epoch_snapshot(gps_nav_epoch_snapshot_t* snapshot);
|
||||
void gps_get_runtime_diagnostics(gps_runtime_diagnostics_t* diagnostics);
|
||||
void gps_tracking_set_eskf_doppler_alpha(double alpha);
|
||||
double gps_tracking_get_eskf_doppler_alpha(void);
|
||||
void trck_intr_handler(volatile gps_trck_t* const trck_ip, volatile gps_frame_t *gps_frame_parsed, volatile int unsigned gps_subframe[10], unsigned char trck_ip_no);
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
#include "gps/matrixLib.h"
|
||||
#include "gps/gps_macros.h"
|
||||
|
||||
#ifdef __riscv
|
||||
#define GPS_IRQ_HANDLER_ATTR __attribute__((interrupt("machine"), aligned(4)))
|
||||
#else
|
||||
#define GPS_IRQ_HANDLER_ATTR
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +22,6 @@
|
||||
// | || __/ | |_| | |___| _| | || |\ | |___ ___) |
|
||||
// |___|_| |____/|_____|_| |___|_| \_|_____|____/
|
||||
|
||||
#define MIN_NEEDED_SATELLITE 4
|
||||
#define GPS_TRCK_UNLOCK_TIMEOUT_TICKS 500000000u
|
||||
#define GPS_PRN_COUNT 32u
|
||||
#define GPS_PRN_INVALID 0xffu
|
||||
@@ -43,13 +46,16 @@
|
||||
#define GPS_ACQ_IP_BASE_ADDR(index) (BSP_GPS_ACQ_BASE + ((uintptr_t)(index) * BSP_GPS_ACQ_STRIDE))
|
||||
#define GPS_ACQ_INTR_LINE(index) (BSP_GPS_ACQ_IRQ + ((unsigned int)(index) * BSP_GPS_ACQ_IRQ_STRIDE))
|
||||
#define GPS_EVENT_QUEUE_SIZE 64u
|
||||
#define GPS_SYSTICK_HZ 20u
|
||||
/* Genesys2 CLINT is 500 kHz. Keep foreground maintenance at
|
||||
* 10 Hz; the tracking IP's 20 ms NAV timer is independent. */
|
||||
#define GPS_SYSTICK_HZ 10u
|
||||
#define GPS_NAV_SOLVE_TIMER_TICK_MS 20u
|
||||
#ifndef GPS_NAV_SOLVE_TIMER_TICKS
|
||||
#define GPS_NAV_SOLVE_TIMER_TICKS 50u
|
||||
#endif
|
||||
#define GPS_NAV_BITS_PER_WEEK 30240000u
|
||||
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
|
||||
#define GPS_NAV_BITS_PER_SUBFRAME 300u
|
||||
/* Keep integrity statistics active, but do not let leave-one-out FDE
|
||||
* replace the full-WLS position until the real measurement-noise model
|
||||
* is calibrated. The live stationary test produced a 22.7% exclusion
|
||||
@@ -57,17 +63,29 @@
|
||||
#define GPS_NAV_ENABLE_FDE 0u
|
||||
#define GPS_PROFILE_CPU_HZ 50000000ULL
|
||||
#define GPS_ACQ_RETRY_DELAY_TICKS (GPS_SYSTICK_HZ / 2u)
|
||||
#define GPS_ACQ_STUCK_WARN_TICKS (GPS_SYSTICK_HZ * 2u)
|
||||
#define GPS_ACQ_HOT_RETRY_DELAY_TICKS (GPS_SYSTICK_HZ * 30u)
|
||||
#define GPS_ACQ_REACQ_PRIORITY_TICKS \
|
||||
(GPS_SYSTICK_HZ * 2u * 60u * 60u)
|
||||
#define GPS_ACQ_MAX_HOT_STREAK 4u
|
||||
#define GPS_ACQ_STUCK_WARN_TICKS (GPS_SYSTICK_HZ * 8u)
|
||||
#define GPS_EPHEMERIS_CACHE_MAX_AGE_TICKS \
|
||||
(GPS_SYSTICK_HZ * 2u * 60u * 60u)
|
||||
#define GPS_ACQ_STATUS_LOG_PERIOD_TICKS (GPS_SYSTICK_HZ * 5u)
|
||||
#define GPS_NAV_STATUS_LOG_PERIOD_TICKS GPS_SYSTICK_HZ
|
||||
#define GPS_TRK_MEASUREMENT_STALE_TICKS (GPS_SYSTICK_HZ * 3u)
|
||||
#define GPS_NAV_ABS_SAMPLE_SYNC_WINDOW_MS 25u
|
||||
#define GPS_NAV_EPOCH_HOLDOFF_MS 30u
|
||||
#define GPS_NAV_TOW_CLUSTER_TOLERANCE_SEC 0.001
|
||||
#define GPS_HOW_ANCHOR_MAX_AGE_SEC 12.0
|
||||
#define GPS_HOW_CONTINUITY_MAX_GAP_SEC 120.0
|
||||
#define GPS_HOW_CONTINUITY_TOLERANCE_SEC 0.050
|
||||
|
||||
#if (GPS_NAV_SOLVE_TIMER_TICKS == 0u)
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
|
||||
#elif ((GPS_NAV_BITS_PER_WEEK % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide 30240000 GPS navigation bits per week"
|
||||
#elif ((GPS_NAV_BITS_PER_SUBFRAME % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
|
||||
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide the 300-bit LNAV subframe"
|
||||
#endif
|
||||
_Static_assert((GPS_NAV_ENABLE_FDE == 0u) || (GPS_NAV_ENABLE_FDE == 1u),
|
||||
"GPS_NAV_ENABLE_FDE must be zero or one");
|
||||
@@ -123,6 +141,8 @@
|
||||
#define GPS_TRCK_NAV_SOLVE_EN_POS 10
|
||||
#define GPS_TRCK_NAV_SOLVE_INTR_EN_POS 11
|
||||
#define GPS_TRCK_NAV_SOLVE_FLAG_EN_POS 12 // (Default 1)
|
||||
#define GPS_TRCK_MEASUREMENT_INTR_EN_POS 13
|
||||
#define GPS_TRCK_MEASUREMENT_FLAG_EN_POS 14
|
||||
|
||||
|
||||
// acq ip status reg bits.
|
||||
@@ -137,6 +157,18 @@
|
||||
#define GPS_TRCK_NAV_BITS_RCV_POS 4
|
||||
#define GPS_TRCK_STOP_UNLOCK_POS 5
|
||||
#define GPS_TRCK_NAV_SOLVE_FLAG_POS 6
|
||||
#define GPS_TRCK_MEASUREMENT_FLAG_POS 7
|
||||
|
||||
#define GPS_TRCK_MEAS_META_VALID_POS 0
|
||||
#define GPS_TRCK_MEAS_META_NAV_BIT_POS 1
|
||||
#define GPS_TRCK_MEAS_META_LOCK_POS 2
|
||||
#define GPS_TRCK_MEAS_META_COUNT_SHIFT 8
|
||||
#define GPS_TRCK_MEAS_META_COUNT_MASK 0x1fu
|
||||
#define GPS_TRCK_MEAS_META_VERSION_SHIFT 24
|
||||
#define GPS_TRCK_MEAS_META_VERSION 1u
|
||||
#define GPS_TRCK_CARRIER_Q7_SCALE 128.0
|
||||
#define GPS_TRCK_IF_CENTER_HZ 4092000.0
|
||||
#define GPS_L1_CARRIER_HZ 1575420000.0
|
||||
|
||||
|
||||
|
||||
@@ -145,7 +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
|
||||
@@ -217,17 +252,17 @@
|
||||
uint32_t NAV_SOLVE_TIMER; // 'h44
|
||||
uint32_t RESERVED_48; // 'h48
|
||||
uint32_t REM_CODE_PHASE_REG; // 'h4c
|
||||
uint32_t MEASUREMENT_META; // 'h50
|
||||
uint32_t MEASUREMENT_SEQUENCE; // 'h54
|
||||
int32_t MEASUREMENT_CARRIER_Q7; // 'h58
|
||||
uint32_t MEASUREMENT_ABS_SAMPLE; // 'h5c
|
||||
uint32_t MEASUREMENT_REM_CODE_PHASE; // 'h60
|
||||
int32_t MEASUREMENT_SUM_I; // 'h64
|
||||
int32_t MEASUREMENT_SUM_Q; // 'h68
|
||||
uint32_t MEASUREMENT_WBP_LO; // 'h6c
|
||||
uint32_t MEASUREMENT_WBP_HI; // 'h70
|
||||
}gps_trck_t;
|
||||
|
||||
typedef enum {
|
||||
GPS_SAT_NOT_SEEN = 0,
|
||||
GPS_SAT_ACQUIRING,
|
||||
GPS_SAT_ACQUIRED,
|
||||
GPS_SAT_TRACKING,
|
||||
GPS_SAT_LOST,
|
||||
GPS_SAT_STALE
|
||||
} gps_satellite_state_t;
|
||||
|
||||
typedef enum {
|
||||
GPS_ACQ_ENGINE_IDLE = 0,
|
||||
GPS_ACQ_ENGINE_BUSY
|
||||
@@ -254,6 +289,9 @@
|
||||
_Static_assert(offsetof(gps_trck_t, PRN_NUMBER) == 0x04, "gps_trck_t PRN offset mismatch");
|
||||
_Static_assert(offsetof(gps_trck_t, STATUS_REG) == 0x10, "gps_trck_t STATUS offset mismatch");
|
||||
_Static_assert(offsetof(gps_trck_t, DOPPLER_REG) == 0x14, "gps_trck_t DOPPLER offset mismatch");
|
||||
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_META) == 0x50, "gps_trck_t measurement meta offset mismatch");
|
||||
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_CARRIER_Q7) == 0x58, "gps_trck_t measurement carrier offset mismatch");
|
||||
_Static_assert(offsetof(gps_trck_t, MEASUREMENT_WBP_HI) == 0x70, "gps_trck_t measurement WBP offset mismatch");
|
||||
#endif
|
||||
|
||||
|
||||
@@ -436,6 +474,10 @@
|
||||
void trck_enable_nav_bits_flag(volatile gps_trck_t* trck_ip); // enable nav bits internal flag generation.
|
||||
void trck_enable_nav_bits_intr(volatile gps_trck_t* trck_ip); // enable nav bits intr generation.
|
||||
void trck_disable_nav_bits_intr(volatile gps_trck_t* trck_ip); // disable nav bits intr generation.
|
||||
void trck_enable_measurement_intr(volatile gps_trck_t* trck_ip);
|
||||
void trck_disable_measurement_intr(volatile gps_trck_t* trck_ip);
|
||||
void trck_enable_measurement_flag(volatile gps_trck_t* trck_ip);
|
||||
void trck_disable_measurement_flag(volatile gps_trck_t* trck_ip);
|
||||
|
||||
void trck_set_nav_solve_timer(unsigned int tick, volatile gps_trck_t* trck_ip); // in case of gps, 1 tick is 20 ms.
|
||||
|
||||
@@ -454,9 +496,13 @@
|
||||
uint64_t gps_get_systick(void);
|
||||
static inline uint64_t gps_read_cycle_counter(void)
|
||||
{
|
||||
#ifdef __riscv
|
||||
uint64_t cycles;
|
||||
asm volatile("rdcycle %0" : "=r"(cycles));
|
||||
return cycles;
|
||||
#else
|
||||
return 0u;
|
||||
#endif
|
||||
}
|
||||
bool gps_get_ionoutc(ionoutc_t* ionoutc);
|
||||
void gps_background_task(void);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
/*
|
||||
* Verbosity is cumulative:
|
||||
* 1: navigation result (position and all DOP values) plus tracked
|
||||
* satellite count/PRN summary
|
||||
* 1: navigation result (position and all DOP values), tracked
|
||||
* satellite summary and low-rate acquisition/reacquisition status
|
||||
* 2: reserved for additional status detail
|
||||
* 3: state transitions, warnings/errors and integrity summary
|
||||
* 4: performance, NAV satellite and low-level diagnostic detail
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Four pseudoranges are the algebraic minimum for receiver x/y/z plus clock
|
||||
* bias. They leave no residual degree of freedom, so a bad measurement can
|
||||
* produce an arbitrary position while fitting perfectly. The raw receiver
|
||||
* still exposes that minimum WLS fix and its covariance; the separate
|
||||
* residual/integrity flags tell downstream fusion that no redundancy exists.
|
||||
*/
|
||||
#define GPS_NAV_MIN_WLS_SATELLITES 4u
|
||||
|
||||
/*
|
||||
* The current receiver exposes nine tracking channels. Keep the epoch
|
||||
* selector bounded so its exhaustive, allocation-free search has a fixed
|
||||
@@ -11,6 +20,10 @@
|
||||
*/
|
||||
#define GPS_NAV_EPOCH_MAX_CANDIDATES 9u
|
||||
|
||||
_Static_assert(GPS_NAV_MIN_WLS_SATELLITES <=
|
||||
GPS_NAV_EPOCH_MAX_CANDIDATES,
|
||||
"GPS WLS minimum exceeds epoch capacity");
|
||||
|
||||
typedef struct {
|
||||
int week;
|
||||
double tow_sec;
|
||||
@@ -24,7 +37,10 @@ typedef struct {
|
||||
} gps_nav_epoch_selection_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t epoch_id;
|
||||
uint64_t first_capture_counter;
|
||||
uint64_t minimum_capture_counter;
|
||||
uint64_t maximum_capture_counter;
|
||||
uint64_t deadline_local_ticks;
|
||||
uint32_t expected_mask;
|
||||
uint32_t received_mask;
|
||||
bool active;
|
||||
@@ -35,6 +51,7 @@ typedef enum {
|
||||
GPS_NAV_EPOCH_EVENT_ACCEPTED,
|
||||
GPS_NAV_EPOCH_EVENT_DUPLICATE,
|
||||
GPS_NAV_EPOCH_EVENT_STALE,
|
||||
GPS_NAV_EPOCH_EVENT_OUTLIER,
|
||||
GPS_NAV_EPOCH_EVENT_RESTARTED,
|
||||
GPS_NAV_EPOCH_EVENT_COMPLETE
|
||||
} gps_nav_epoch_event_result_t;
|
||||
@@ -46,19 +63,43 @@ bool gps_nav_epoch_select(
|
||||
double tow_tolerance_sec,
|
||||
gps_nav_epoch_selection_t* selection);
|
||||
|
||||
bool gps_nav_epoch_id(
|
||||
int week,
|
||||
double tow_sec,
|
||||
unsigned int period_ms,
|
||||
uint64_t* epoch_id);
|
||||
/*
|
||||
* Receiver publication is stricter than diagnostic clustering: every
|
||||
* candidate in the frozen ready mask must belong to one coherent epoch.
|
||||
* selection is still populated with the largest cluster on failure so the
|
||||
* caller can report why the complete epoch was rejected.
|
||||
*/
|
||||
bool gps_nav_epoch_validate_complete(
|
||||
const gps_nav_epoch_key_t candidates[GPS_NAV_EPOCH_MAX_CANDIDATES],
|
||||
unsigned int candidate_count,
|
||||
uint32_t sample_window,
|
||||
double tow_tolerance_sec,
|
||||
gps_nav_epoch_selection_t* selection);
|
||||
|
||||
/*
|
||||
* Validate a newest-only hardware mailbox after one or more capture events
|
||||
* were coalesced. sequence_gap receives the number of real elapsed epochs;
|
||||
* missing epochs are never synthesized.
|
||||
*/
|
||||
bool gps_nav_capture_sequence_consistent(
|
||||
uint64_t previous_counter,
|
||||
uint32_t previous_sequence,
|
||||
uint64_t current_counter,
|
||||
uint32_t current_sequence,
|
||||
uint64_t capture_period_ticks,
|
||||
uint32_t* sequence_gap);
|
||||
|
||||
void gps_nav_epoch_assembly_reset(gps_nav_epoch_assembly_t* assembly);
|
||||
|
||||
gps_nav_epoch_event_result_t gps_nav_epoch_assembly_push(
|
||||
gps_nav_epoch_assembly_t* assembly,
|
||||
uint64_t epoch_id,
|
||||
uint64_t capture_counter,
|
||||
uint64_t local_timestamp_ticks,
|
||||
unsigned int channel,
|
||||
uint32_t ready_mask);
|
||||
uint32_t ready_mask,
|
||||
uint64_t capture_period_ticks,
|
||||
uint64_t sync_window_ticks,
|
||||
uint64_t holdoff_local_ticks);
|
||||
|
||||
bool gps_nav_epoch_assembly_remove_channel(
|
||||
gps_nav_epoch_assembly_t* assembly,
|
||||
@@ -67,4 +108,12 @@ bool gps_nav_epoch_assembly_remove_channel(
|
||||
bool gps_nav_epoch_assembly_complete(
|
||||
const gps_nav_epoch_assembly_t* assembly);
|
||||
|
||||
bool gps_nav_epoch_assembly_usable(
|
||||
const gps_nav_epoch_assembly_t* assembly,
|
||||
unsigned int minimum_channels);
|
||||
|
||||
bool gps_nav_epoch_assembly_expired(
|
||||
const gps_nav_epoch_assembly_t* assembly,
|
||||
uint64_t now_local_ticks);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,11 @@ int gps_time_resolve_how_week(bool previous_valid,
|
||||
int fallback_week,
|
||||
double how_tow_sec);
|
||||
|
||||
/* Extend a modulo-1024 LNAV week number around the previous monotonic week. */
|
||||
int gps_time_extend_lnav_week(bool previous_valid,
|
||||
int previous_week,
|
||||
unsigned int lnav_week);
|
||||
|
||||
/* Convert a receiver-sample timestamp to GPS time from a HOW/sample anchor.
|
||||
* raw counters are modulo 2^32. max_age_sec must keep the signed difference
|
||||
* unambiguous (and also rejects a stale HOW anchor). */
|
||||
@@ -31,4 +36,53 @@ 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. */
|
||||
bool gps_time_anchors_consistent(int first_week,
|
||||
double first_tow_sec,
|
||||
uint32_t first_raw_sample,
|
||||
double first_corrected_sample,
|
||||
int second_week,
|
||||
double second_tow_sec,
|
||||
uint32_t second_raw_sample,
|
||||
double second_corrected_sample,
|
||||
double samples_per_second,
|
||||
double max_gap_sec,
|
||||
double tolerance_sec);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#ifndef GPS_TRACKING_MEASUREMENT_H
|
||||
#define GPS_TRACKING_MEASUREMENT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "gps/gps_ip_defines.h"
|
||||
|
||||
#define GPS_TRACK_PPS_DOPPLER_ALPHA 0.05
|
||||
|
||||
typedef struct {
|
||||
uint32_t prn;
|
||||
uint32_t sequence;
|
||||
uint32_t absolute_sample;
|
||||
uint32_t remaining_code_phase;
|
||||
uint64_t local_timestamp_ticks;
|
||||
int32_t carrier_freq_q7;
|
||||
int32_t cn0_sum_i;
|
||||
int32_t cn0_sum_q;
|
||||
uint64_t cn0_wbp;
|
||||
unsigned int cn0_sample_count;
|
||||
/* Raw coherent 20 ms carrier snapshot retained for diagnostics. */
|
||||
double doppler_hz;
|
||||
double pseudorange_rate_mps;
|
||||
/* Motion-adaptive low-latency path published to velocity WLS/ESKF. */
|
||||
double eskf_doppler_hz;
|
||||
double eskf_pseudorange_rate_mps;
|
||||
/* Slow software IIR path retained only for PPS bias-drift prediction. */
|
||||
double filtered_doppler_hz;
|
||||
double filtered_pseudorange_rate_mps;
|
||||
double cn0_dbhz;
|
||||
bool nav_bit;
|
||||
bool locked;
|
||||
bool valid;
|
||||
} gps_tracking_measurement_t;
|
||||
|
||||
/*
|
||||
* Decode a coherent hardware mailbox. The sequence-before/after check protects
|
||||
* callers that are not running directly in the measurement ISR.
|
||||
*/
|
||||
bool gps_tracking_measurement_read(
|
||||
volatile const gps_trck_t* tracking_ip,
|
||||
gps_tracking_measurement_t* measurement_out);
|
||||
|
||||
bool gps_tracking_cn0_dbhz(
|
||||
int32_t sum_i,
|
||||
int32_t sum_q,
|
||||
uint64_t wideband_power,
|
||||
unsigned int coherent_samples,
|
||||
double* cn0_dbhz_out);
|
||||
|
||||
double gps_tracking_doppler_hz(int32_t carrier_freq_q7);
|
||||
double gps_tracking_doppler_to_range_rate_mps(double doppler_hz);
|
||||
|
||||
/*
|
||||
* Four-state WLS: receiver ECEF velocity (m/s) and receiver clock drift
|
||||
* (m/s). Only valid/locked measurements with finite C/N0 are used.
|
||||
*/
|
||||
bool gps_tracking_velocity_wls(
|
||||
const double receiver_ecef_m[3],
|
||||
const double satellite_positions_m[][3],
|
||||
const double satellite_velocities_mps[][3],
|
||||
const double satellite_clock_drifts_sps[],
|
||||
const gps_tracking_measurement_t measurements[],
|
||||
unsigned int count,
|
||||
double receiver_velocity_ecef_mps_out[3],
|
||||
double receiver_velocity_covariance_ecef_mps2_out[3][3],
|
||||
double* receiver_clock_drift_mps_out,
|
||||
double* residual_rms_mps_out);
|
||||
|
||||
/*
|
||||
* Raw-Doppler velocity solution with a one-channel leave-one-out FDE trial.
|
||||
* FDE runs only for six or more usable channels and only when the full-fit
|
||||
* maximum normalized residual exceeds 3.5 sigma. The returned bit mask uses
|
||||
* indices from the input measurement array, not compacted WLS row indices.
|
||||
*/
|
||||
bool gps_tracking_velocity_wls_robust(
|
||||
const double receiver_ecef_m[3],
|
||||
const double satellite_positions_m[][3],
|
||||
const double satellite_velocities_mps[][3],
|
||||
const double satellite_clock_drifts_sps[],
|
||||
const gps_tracking_measurement_t measurements[],
|
||||
unsigned int count,
|
||||
double receiver_velocity_ecef_mps_out[3],
|
||||
double receiver_velocity_covariance_ecef_mps2_out[3][3],
|
||||
double* receiver_clock_drift_mps_out,
|
||||
double* residual_rms_mps_out,
|
||||
uint32_t* excluded_measurement_mask_out);
|
||||
|
||||
#endif
|
||||
@@ -13,8 +13,12 @@ typedef struct gps_ephem_ll {
|
||||
volatile double gps_how_corrected_abs_sample;
|
||||
volatile uint64_t gps_how_tick;
|
||||
volatile bool gps_how_anchor_valid;
|
||||
volatile bool gps_how_week_valid;
|
||||
volatile gpstime_t gps_pending_how_tow;
|
||||
volatile uint32_t gps_pending_how_abs_sample;
|
||||
volatile double gps_pending_how_corrected_abs_sample;
|
||||
volatile bool gps_pending_how_valid;
|
||||
volatile uint32_t gps_nav_solve_sequence;
|
||||
volatile uint32_t gps_nav_solve_consumed_sequence;
|
||||
volatile gpstime_t gps_last_nav_solve_tow;
|
||||
volatile uint32_t gps_last_nav_solve_abs_sample;
|
||||
volatile double gps_last_nav_solve_corrected_abs_sample;
|
||||
@@ -25,13 +29,7 @@ typedef struct gps_ephem_ll {
|
||||
volatile bool gps_measurement_valid;
|
||||
} gps_ephem_ll_t;
|
||||
|
||||
typedef struct gps_acq_ll {
|
||||
unsigned int prn_no;
|
||||
struct gps_acq_ll* next;
|
||||
} gps_acq_ll_t;
|
||||
|
||||
extern volatile gps_ephem_ll_t* tracking_node;
|
||||
extern volatile gps_acq_ll_t* acquisition_node;
|
||||
|
||||
bool insert_element(volatile gps_ephem_ll_t** node,
|
||||
volatile gps_ephemeris_t* data_p,
|
||||
@@ -41,8 +39,4 @@ bool trck_ip_exists(volatile gps_ephem_ll_t** node, unsigned char trck_ip_no);
|
||||
volatile gps_ephem_ll_t* get_element_by_ip_number(volatile gps_ephem_ll_t** node,
|
||||
unsigned char trck_ip_no);
|
||||
|
||||
bool insert_acq_element(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
bool delete_acq_element_by_prn_number(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
bool acq_prn_exists(volatile gps_acq_ll_t** node, unsigned int prn_no);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -17,7 +17,9 @@ typedef struct {
|
||||
imu_sample_t imu_sample;
|
||||
navigation_fusion_output_t fused_solution;
|
||||
uint64_t gps_measurement_ticks;
|
||||
uint64_t last_fused_location_tick;
|
||||
uint64_t last_status_tick;
|
||||
uint32_t fused_publish_sample_count;
|
||||
bool gps_epoch_ready;
|
||||
bool fusion_enabled;
|
||||
bool imu_initialized;
|
||||
|
||||
@@ -24,7 +24,17 @@ bool gps_fusion_build_measurement(
|
||||
measurement_out->vacc_m = solution->vacc_m;
|
||||
measurement_out->residual_rms_m = solution->residual_rms_m;
|
||||
measurement_out->chi_square_ratio = solution->chi_square_ratio;
|
||||
memcpy(measurement_out->velocity_ecef_mps,
|
||||
solution->velocity_ecef_mps,
|
||||
sizeof(measurement_out->velocity_ecef_mps));
|
||||
memcpy(measurement_out->velocity_covariance_ecef_mps2,
|
||||
solution->velocity_covariance_ecef_mps2,
|
||||
sizeof(measurement_out->velocity_covariance_ecef_mps2));
|
||||
measurement_out->speed_mps = solution->speed_mps;
|
||||
measurement_out->velocity_residual_rms_mps =
|
||||
solution->velocity_residual_rms_mps;
|
||||
measurement_out->used_satellites = solution->used_satellite_count;
|
||||
measurement_out->solution_valid = solution->solution_valid;
|
||||
measurement_out->velocity_valid = solution->velocity_valid;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#include "gps/gps_receiver.h"
|
||||
#include "imu/imu_sensor.h"
|
||||
|
||||
#define GPS_AIDED_MAX_AGE_MS 1000u
|
||||
|
||||
#ifndef GPS_BUILD_ID
|
||||
#define GPS_BUILD_ID "unknown"
|
||||
#endif
|
||||
|
||||
void gps_system_uart_publish_location(
|
||||
const char* source,
|
||||
double latitude_deg,
|
||||
@@ -21,23 +27,57 @@ 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\r\n",
|
||||
"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->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);
|
||||
}
|
||||
|
||||
void gps_system_uart_publish_fused_location(
|
||||
const navigation_fusion_output_t* output)
|
||||
{
|
||||
if((output == NULL) || !output->global_valid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t age_ticks = output->timestamp_ticks >= output->last_accepted_gps_ticks
|
||||
? output->timestamp_ticks - output->last_accepted_gps_ticks : 0u;
|
||||
uint64_t age_ms_wide =
|
||||
age_ticks / (BSP_CLINT_TIMEBASE_HZ / 1000u);
|
||||
uint32_t age_ms = age_ms_wide > UINT32_MAX
|
||||
? UINT32_MAX : (uint32_t)age_ms_wide;
|
||||
const char* mode =
|
||||
(output->gps_accepted > 0u) && (age_ms <= GPS_AIDED_MAX_AGE_MS)
|
||||
? "gps_aided" : "dead_reckoning";
|
||||
|
||||
/* The application rate-limits this compact record by IMU sample count,
|
||||
* leaving UART headroom without reducing the ESKF prediction rate. */
|
||||
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_LOCATION,
|
||||
"source=imu_corrected mode=%s lat=%.9lf lon=%.9lf "
|
||||
"height=%.2lf gps_age_ms=%lu\r\n",
|
||||
mode, output->latitude_deg, output->longitude_deg,
|
||||
output->height_m, (unsigned long)age_ms);
|
||||
}
|
||||
|
||||
void gps_system_uart_publish_startup(
|
||||
bool fusion_enabled,
|
||||
bool fusion_initialized,
|
||||
bool imu_initialized,
|
||||
bool imu_running)
|
||||
bool imu_running,
|
||||
unsigned int fused_publish_every_samples)
|
||||
{
|
||||
imu_sensor_diagnostics_t imu;
|
||||
imu_sensor_get_diagnostics(&imu);
|
||||
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_FUSION,
|
||||
"init eskf_enabled=%u eskf_ready=%u imu=%u sensor=%s who=0x%02x "
|
||||
"running=%u target_hz=%lu actual_hz=%lu nav_ms=%u timebase_hz=%lu\r\n",
|
||||
"init build=%s eskf_enabled=%u eskf_ready=%u imu=%u sensor=%s who=0x%02x "
|
||||
"running=%u target_hz=%lu actual_hz=%lu publish_every=%u "
|
||||
"nav_ms=%u timebase_hz=%lu\r\n",
|
||||
GPS_BUILD_ID,
|
||||
fusion_enabled ? 1u : 0u,
|
||||
fusion_initialized ? 1u : 0u,
|
||||
imu_initialized ? 1u : 0u,
|
||||
@@ -46,6 +86,7 @@ void gps_system_uart_publish_startup(
|
||||
imu_running ? 1u : 0u,
|
||||
(unsigned long)imu.requested_odr_hz,
|
||||
(unsigned long)imu.configured_odr_hz,
|
||||
fused_publish_every_samples,
|
||||
GPS_NAV_SOLVE_PERIOD_MS,
|
||||
(unsigned long)BSP_CLINT_TIMEBASE_HZ);
|
||||
}
|
||||
@@ -74,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,
|
||||
@@ -97,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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -390,11 +392,16 @@ static bool eskf_propagate_step(eskf_t* filter,
|
||||
return false;
|
||||
}
|
||||
|
||||
double system[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
double transition[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int i = 0 ; i < 3u ; i++)
|
||||
{
|
||||
system[ESKF_POSITION_INDEX + i][ESKF_VELOCITY_INDEX + i] = 1.0;
|
||||
system[ESKF_ATTITUDE_INDEX + i][ESKF_GYRO_BIAS_INDEX + i] = -1.0;
|
||||
transition[i][i] = 1.0;
|
||||
transition[ESKF_VELOCITY_INDEX + i][ESKF_VELOCITY_INDEX + i] = 1.0;
|
||||
transition[ESKF_ATTITUDE_INDEX + i][ESKF_ATTITUDE_INDEX + i] = 1.0;
|
||||
transition[ESKF_ACCEL_BIAS_INDEX + i][ESKF_ACCEL_BIAS_INDEX + i] = 1.0;
|
||||
transition[ESKF_GYRO_BIAS_INDEX + i][ESKF_GYRO_BIAS_INDEX + i] = 1.0;
|
||||
transition[ESKF_POSITION_INDEX + i][ESKF_VELOCITY_INDEX + i] = dt_s;
|
||||
transition[ESKF_ATTITUDE_INDEX + i][ESKF_GYRO_BIAS_INDEX + i] = -dt_s;
|
||||
}
|
||||
double force_skew[3][3];
|
||||
double rate_skew[3][3];
|
||||
@@ -409,27 +416,16 @@ static bool eskf_propagate_step(eskf_t* filter,
|
||||
{
|
||||
rotated_skew += rotation[row][k] * force_skew[k][col];
|
||||
}
|
||||
system[ESKF_VELOCITY_INDEX + row][ESKF_ATTITUDE_INDEX + col] =
|
||||
-rotated_skew;
|
||||
system[ESKF_VELOCITY_INDEX + row][ESKF_ACCEL_BIAS_INDEX + col] =
|
||||
-rotation[row][col];
|
||||
system[ESKF_ATTITUDE_INDEX + row][ESKF_ATTITUDE_INDEX + col] =
|
||||
-rate_skew[row][col];
|
||||
transition[ESKF_VELOCITY_INDEX + row][ESKF_ATTITUDE_INDEX + col] +=
|
||||
-rotated_skew * dt_s;
|
||||
transition[ESKF_VELOCITY_INDEX + row][ESKF_ACCEL_BIAS_INDEX + col] =
|
||||
-rotation[row][col] * dt_s;
|
||||
transition[ESKF_ATTITUDE_INDEX + row][ESKF_ATTITUDE_INDEX + col] +=
|
||||
-rate_skew[row][col] * dt_s;
|
||||
}
|
||||
}
|
||||
|
||||
double transition[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
transition[row][col] = system[row][col] * dt_s;
|
||||
}
|
||||
transition[row][row] += 1.0;
|
||||
}
|
||||
|
||||
double intermediate[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
double propagated[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
@@ -448,21 +444,18 @@ static bool eskf_propagate_step(eskf_t* filter,
|
||||
}
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
double value = intermediate[row][k];
|
||||
if(value == 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
double value = 0.0;
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
{
|
||||
double coefficient = transition[col][k];
|
||||
if(coefficient != 0.0)
|
||||
{
|
||||
propagated[row][col] += value * coefficient;
|
||||
value += intermediate[row][k] * coefficient;
|
||||
}
|
||||
}
|
||||
filter->covariance[row][col] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,17 +470,20 @@ static bool eskf_propagate_step(eskf_t* filter,
|
||||
square(config->accel_noise_density[axis]) *
|
||||
rotation[col][axis];
|
||||
}
|
||||
propagated[ESKF_VELOCITY_INDEX + row][ESKF_VELOCITY_INDEX + col] +=
|
||||
filter->covariance[ESKF_VELOCITY_INDEX + row]
|
||||
[ESKF_VELOCITY_INDEX + col] +=
|
||||
accel_noise * dt_s;
|
||||
}
|
||||
propagated[ESKF_ATTITUDE_INDEX + row][ESKF_ATTITUDE_INDEX + row] +=
|
||||
filter->covariance[ESKF_ATTITUDE_INDEX + row]
|
||||
[ESKF_ATTITUDE_INDEX + row] +=
|
||||
square(config->gyro_noise_density[row]) * dt_s;
|
||||
propagated[ESKF_ACCEL_BIAS_INDEX + row][ESKF_ACCEL_BIAS_INDEX + row] +=
|
||||
filter->covariance[ESKF_ACCEL_BIAS_INDEX + row]
|
||||
[ESKF_ACCEL_BIAS_INDEX + row] +=
|
||||
square(config->accel_bias_random_walk[row]) * dt_s;
|
||||
propagated[ESKF_GYRO_BIAS_INDEX + row][ESKF_GYRO_BIAS_INDEX + row] +=
|
||||
filter->covariance[ESKF_GYRO_BIAS_INDEX + row]
|
||||
[ESKF_GYRO_BIAS_INDEX + row] +=
|
||||
square(config->gyro_bias_random_walk[row]) * dt_s;
|
||||
}
|
||||
memcpy(filter->covariance, propagated, sizeof(propagated));
|
||||
covariance_symmetrize_and_floor(filter, config->covariance_diagonal_floor);
|
||||
filter->propagation_count++;
|
||||
return eskf_state_is_finite(filter);
|
||||
@@ -591,10 +587,10 @@ static bool eskf_inject_error(eskf_t* filter, const double error[ESKF_ERROR_STAT
|
||||
}
|
||||
|
||||
/* Right attitude-error reset after q <- q (x) Exp(delta_theta). */
|
||||
double reset[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int i = 0 ; i < ESKF_ERROR_STATE_DIM ; i++)
|
||||
double attitude_reset[3][3] = {{0.0}};
|
||||
for(unsigned int i = 0 ; i < 3u ; i++)
|
||||
{
|
||||
reset[i][i] = 1.0;
|
||||
attitude_reset[i][i] = 1.0;
|
||||
}
|
||||
double attitude_skew[3][3];
|
||||
skew_matrix(attitude_error, attitude_skew);
|
||||
@@ -602,47 +598,48 @@ static bool eskf_inject_error(eskf_t* filter, const double error[ESKF_ERROR_STAT
|
||||
{
|
||||
for(unsigned int col = 0 ; col < 3u ; col++)
|
||||
{
|
||||
reset[ESKF_ATTITUDE_INDEX + row][ESKF_ATTITUDE_INDEX + col] -=
|
||||
0.5 * attitude_skew[row][col];
|
||||
attitude_reset[row][col] -= 0.5 * attitude_skew[row][col];
|
||||
}
|
||||
}
|
||||
double intermediate[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
double reset_covariance[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
|
||||
/* G differs from identity only in the 3x3 attitude block. Apply G*P
|
||||
* using three temporary rows, then apply G^T using three scalar columns.
|
||||
* This keeps the Joseph/reset path below the bare-metal stack budget. */
|
||||
double attitude_rows[3][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0u ; row < 3u ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
for(unsigned int col = 0u ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
double coefficient = reset[row][k];
|
||||
if(coefficient == 0.0)
|
||||
for(unsigned int k = 0u ; k < 3u ; k++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
intermediate[row][col] += coefficient * filter->covariance[k][col];
|
||||
attitude_rows[row][col] += attitude_reset[row][k] *
|
||||
filter->covariance[ESKF_ATTITUDE_INDEX + k][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
for(unsigned int row = 0u ; row < 3u ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
memcpy(filter->covariance[ESKF_ATTITUDE_INDEX + row],
|
||||
attitude_rows[row], sizeof(attitude_rows[row]));
|
||||
}
|
||||
for(unsigned int row = 0u ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
double attitude_columns[3] = {0.0, 0.0, 0.0};
|
||||
for(unsigned int col = 0u ; col < 3u ; col++)
|
||||
{
|
||||
double value = intermediate[row][k];
|
||||
if(value == 0.0)
|
||||
for(unsigned int k = 0u ; k < 3u ; k++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
double coefficient = reset[col][k];
|
||||
if(coefficient != 0.0)
|
||||
{
|
||||
reset_covariance[row][col] += value * coefficient;
|
||||
}
|
||||
attitude_columns[col] +=
|
||||
filter->covariance[row][ESKF_ATTITUDE_INDEX + k] *
|
||||
attitude_reset[col][k];
|
||||
}
|
||||
}
|
||||
for(unsigned int col = 0u ; col < 3u ; col++)
|
||||
{
|
||||
filter->covariance[row][ESKF_ATTITUDE_INDEX + col] =
|
||||
attitude_columns[col];
|
||||
}
|
||||
}
|
||||
memcpy(filter->covariance, reset_covariance, sizeof(reset_covariance));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -719,76 +716,44 @@ static bool eskf_selected_update(eskf_t* filter,
|
||||
}
|
||||
}
|
||||
|
||||
double joseph_left[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
joseph_left[row][row] = 1.0;
|
||||
for(unsigned int col = 0 ; col < 3u ; col++)
|
||||
{
|
||||
joseph_left[row][selected_state_index + col] -= gain[row][col];
|
||||
}
|
||||
}
|
||||
/* Joseph form with A = I-KH. H selects three direct state components, so
|
||||
* A*P can be formed without materializing a second 15x15 matrix. */
|
||||
double intermediate[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
double updated_covariance[ESKF_ERROR_STATE_DIM][ESKF_ERROR_STATE_DIM] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
double coefficient = joseph_left[row][k];
|
||||
if(coefficient == 0.0)
|
||||
double value = filter->covariance[row][col];
|
||||
for(unsigned int selected = 0u ; selected < 3u ; selected++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
intermediate[row][col] +=
|
||||
coefficient * filter->covariance[k][col];
|
||||
}
|
||||
}
|
||||
}
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int k = 0 ; k < ESKF_ERROR_STATE_DIM ; k++)
|
||||
{
|
||||
double value = intermediate[row][k];
|
||||
if(value == 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
double coefficient = joseph_left[col][k];
|
||||
if(coefficient != 0.0)
|
||||
{
|
||||
updated_covariance[row][col] += value * coefficient;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
double gain_times_covariance[ESKF_ERROR_STATE_DIM][3] = {{0.0}};
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int second = 0 ; second < 3u ; second++)
|
||||
{
|
||||
for(unsigned int first = 0 ; first < 3u ; first++)
|
||||
{
|
||||
gain_times_covariance[row][second] += gain[row][first] *
|
||||
measurement_covariance[first * 3u + second];
|
||||
value -= gain[row][selected] *
|
||||
filter->covariance[selected_state_index + selected][col];
|
||||
}
|
||||
intermediate[row][col] = value;
|
||||
}
|
||||
}
|
||||
for(unsigned int row = 0 ; row < ESKF_ERROR_STATE_DIM ; row++)
|
||||
{
|
||||
for(unsigned int col = 0 ; col < ESKF_ERROR_STATE_DIM ; col++)
|
||||
{
|
||||
for(unsigned int second = 0 ; second < 3u ; second++)
|
||||
double value = intermediate[row][col];
|
||||
for(unsigned int selected = 0u ; selected < 3u ; selected++)
|
||||
{
|
||||
updated_covariance[row][col] +=
|
||||
gain_times_covariance[row][second] * gain[col][second];
|
||||
value -= intermediate[row][selected_state_index + selected] *
|
||||
gain[col][selected];
|
||||
}
|
||||
for(unsigned int first = 0u ; first < 3u ; first++)
|
||||
{
|
||||
for(unsigned int second = 0u ; second < 3u ; second++)
|
||||
{
|
||||
value += gain[row][first] *
|
||||
measurement_covariance[first * 3u + second] *
|
||||
gain[col][second];
|
||||
}
|
||||
}
|
||||
filter->covariance[row][col] = value;
|
||||
}
|
||||
}
|
||||
memcpy(filter->covariance, updated_covariance, sizeof(updated_covariance));
|
||||
if(!eskf_inject_error(filter, error))
|
||||
{
|
||||
return false;
|
||||
@@ -843,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],
|
||||
|
||||
@@ -136,8 +136,8 @@ void fusion_engine_default_config(fusion_engine_config_t* config,
|
||||
config->gnss_moving_confirmations = 2u;
|
||||
config->gnss_still_confirmations = 2u;
|
||||
/* A hard innovation gate protects against isolated GNSS outliers, but it
|
||||
* must not permanently lock the filter to a bad startup position. After a
|
||||
* full second of mutually continuous rejected fixes, re-anchor position
|
||||
* must not permanently lock a stationary filter to a bad startup position.
|
||||
* After a timeout of mutually continuous rejected fixes, re-anchor position
|
||||
* while preserving inertial velocity, attitude and bias states. */
|
||||
config->position_recovery_timeout_s = 7.0;
|
||||
config->position_recovery_max_speed_mps = 100.0;
|
||||
@@ -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));
|
||||
@@ -301,6 +317,31 @@ static bool runtime_apply_imu(fusion_engine_t* engine,
|
||||
const fusion_imu_measurement_t* measurement,
|
||||
bool count_zero_updates);
|
||||
|
||||
static bool runtime_allows_zero_velocity_update(
|
||||
const fusion_engine_t* engine,
|
||||
const fusion_runtime_t* runtime)
|
||||
{
|
||||
(void)engine;
|
||||
return runtime->vehicle_at_rest &&
|
||||
(runtime->gnss_evidence != FUSION_GNSS_MOTION_MOVING);
|
||||
}
|
||||
|
||||
static bool runtime_allows_zero_gyro_update(
|
||||
const fusion_engine_t* engine,
|
||||
const fusion_runtime_t* runtime)
|
||||
{
|
||||
return runtime_allows_zero_velocity_update(engine, runtime) &&
|
||||
(runtime->motion.last_gyro_norm_rps <=
|
||||
engine->config.motion.stationary_gyro_limit_rps);
|
||||
}
|
||||
|
||||
static bool runtime_allows_position_recovery(
|
||||
const fusion_engine_t* engine,
|
||||
const fusion_runtime_t* runtime)
|
||||
{
|
||||
return runtime_allows_zero_gyro_update(engine, runtime);
|
||||
}
|
||||
|
||||
static void history_push(fusion_engine_t* engine,
|
||||
const fusion_imu_measurement_t* measurement)
|
||||
{
|
||||
@@ -415,9 +456,12 @@ static bool runtime_apply_imu(fusion_engine_t* engine,
|
||||
runtime->gnss_still_count = 0u;
|
||||
}
|
||||
runtime->vehicle_at_rest = measurement->vehicle_at_rest;
|
||||
bool at_rest_continuing = measurement->vehicle_at_rest &&
|
||||
start_measurement.vehicle_at_rest;
|
||||
if(measurement->vehicle_at_rest)
|
||||
bool zero_velocity_allowed =
|
||||
runtime_allows_zero_velocity_update(engine, runtime);
|
||||
bool zero_gyro_allowed =
|
||||
runtime_allows_zero_gyro_update(engine, runtime);
|
||||
bool at_rest_continuing = start_measurement.vehicle_at_rest;
|
||||
if(zero_gyro_allowed && at_rest_continuing)
|
||||
{
|
||||
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
||||
{
|
||||
@@ -435,36 +479,51 @@ static bool runtime_apply_imu(fusion_engine_t* engine,
|
||||
}
|
||||
uint64_t zero_period_ticks =
|
||||
seconds_to_ticks(engine, engine->config.zero_update_period_s);
|
||||
if(at_rest_continuing &&
|
||||
if(at_rest_continuing && (zero_velocity_allowed || zero_gyro_allowed) &&
|
||||
((measurement->timestamp_ticks - runtime->last_zero_update_ticks) >=
|
||||
zero_period_ticks))
|
||||
{
|
||||
double mean_gyro_rps[3];
|
||||
if(!(runtime->zero_gyro_duration_s > 0.0))
|
||||
bool update_applied = false;
|
||||
if(zero_velocity_allowed)
|
||||
{
|
||||
return false;
|
||||
if(!eskf_zero_velocity_update(
|
||||
&runtime->filter, engine->config.zero_velocity_std_mps))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
update_applied = true;
|
||||
if(count_zero_updates)
|
||||
{
|
||||
engine->diagnostics.zero_velocity_update_count++;
|
||||
}
|
||||
}
|
||||
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
||||
if(zero_gyro_allowed && (runtime->zero_gyro_duration_s > 0.0))
|
||||
{
|
||||
mean_gyro_rps[axis] = runtime->zero_gyro_integral_rad[axis] /
|
||||
runtime->zero_gyro_duration_s;
|
||||
double mean_gyro_rps[3];
|
||||
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
||||
{
|
||||
mean_gyro_rps[axis] = runtime->zero_gyro_integral_rad[axis] /
|
||||
runtime->zero_gyro_duration_s;
|
||||
}
|
||||
if(!eskf_zero_angular_rate_update(
|
||||
&runtime->filter, mean_gyro_rps,
|
||||
engine->config.zero_gyro_std_rps))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
update_applied = true;
|
||||
if(count_zero_updates)
|
||||
{
|
||||
engine->diagnostics.zero_gyro_update_count++;
|
||||
}
|
||||
}
|
||||
if(!eskf_zero_velocity_update(
|
||||
&runtime->filter, engine->config.zero_velocity_std_mps) ||
|
||||
!eskf_zero_angular_rate_update(
|
||||
&runtime->filter, mean_gyro_rps, engine->config.zero_gyro_std_rps))
|
||||
if(update_applied)
|
||||
{
|
||||
return false;
|
||||
runtime->last_zero_update_ticks = measurement->timestamp_ticks;
|
||||
}
|
||||
memset(runtime->zero_gyro_integral_rad, 0,
|
||||
sizeof(runtime->zero_gyro_integral_rad));
|
||||
runtime->zero_gyro_duration_s = 0.0;
|
||||
runtime->last_zero_update_ticks = measurement->timestamp_ticks;
|
||||
if(count_zero_updates)
|
||||
{
|
||||
engine->diagnostics.zero_velocity_update_count++;
|
||||
engine->diagnostics.zero_gyro_update_count++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -706,6 +765,15 @@ static bool position_recovery_ready(
|
||||
const fusion_runtime_t* runtime,
|
||||
const fusion_position_measurement_t* measurement)
|
||||
{
|
||||
/* Position-only GNSS cannot distinguish a coherent receiver restart error
|
||||
* or spoofed track from a true position discontinuity while moving. Keep
|
||||
* automatic re-anchoring as a stationary startup/recovery mechanism; a
|
||||
* mobile reset needs independent velocity/yaw aiding or an external command. */
|
||||
if(!runtime_allows_position_recovery(engine, runtime))
|
||||
{
|
||||
position_recovery_clear(engine);
|
||||
return false;
|
||||
}
|
||||
if(!engine->position_recovery_active)
|
||||
{
|
||||
position_recovery_begin(engine, measurement);
|
||||
@@ -813,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;
|
||||
@@ -830,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;
|
||||
@@ -842,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)
|
||||
{
|
||||
@@ -850,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))
|
||||
{
|
||||
@@ -863,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++;
|
||||
}
|
||||
@@ -895,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))
|
||||
@@ -910,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)
|
||||
{
|
||||
@@ -931,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--;
|
||||
@@ -945,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;
|
||||
@@ -1004,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;
|
||||
@@ -1012,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;
|
||||
@@ -123,6 +129,8 @@ bool navigation_fusion_get_output(navigation_fusion_output_t* output)
|
||||
}
|
||||
const fusion_runtime_t* runtime = &fusion_engine.current;
|
||||
output->timestamp_ticks = runtime->filter_timestamp_ticks;
|
||||
output->last_accepted_gps_ticks =
|
||||
runtime->last_accepted_position_ticks;
|
||||
memcpy(output->position_enu_m, runtime->filter.position_m,
|
||||
sizeof(output->position_enu_m));
|
||||
memcpy(output->velocity_enu_mps, runtime->filter.velocity_mps,
|
||||
@@ -133,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;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
Returns:
|
||||
rho - [out] computed range ( pseudo range)
|
||||
*/
|
||||
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, mat_type *satellitePositions, mat_type * velocities){
|
||||
void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_ephemeris_t* eph, ionoutc_t* ionoutc, double* xyz, t_temp_pos* tempPos, double* clockCorr, double* clockDrift, mat_type *satellitePositions, mat_type * velocities){
|
||||
|
||||
double pos[3], vel[3], clk[2];
|
||||
double los[3];
|
||||
@@ -28,7 +28,7 @@ void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_eph
|
||||
//gpstime_t * gpstime = (gpstime_t*) malloc(sizeof(gpstime_t));
|
||||
//gpstime->sec = eph->toe.sec;
|
||||
//gpstime->week = eph->toe.week;
|
||||
|
||||
|
||||
//CPP'ye GOREydi, KAPATILDI// double abs_sample[SVN] = { 66564551., 66525117.,66589459.,66666672.,66633917.,66700000. };
|
||||
//CPP'ye GOREydi, KAPATILDI// int codePhase = 2772; // TODO should be given by tracker. Şu anda matlab'dan alindi..
|
||||
|
||||
@@ -38,6 +38,10 @@ void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_eph
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
satellitePositions->matrix[sat_no][i] = pos[i];
|
||||
velocities->matrix[sat_no][i] = vel[i];
|
||||
}
|
||||
if(clockDrift != NULL) {
|
||||
*clockDrift = clk[1];
|
||||
}
|
||||
/*for (i = 0; i < 3; i++) tempPos->sv_po_at_pseudo_obs[i] = pos[i];
|
||||
//memset(los, '0', sizeof(double));
|
||||
@@ -168,20 +172,6 @@ void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_eph
|
||||
double ek;
|
||||
double ekdot;
|
||||
double cek, sek;
|
||||
double pk;
|
||||
double c2pk, s2pk;
|
||||
double uk;
|
||||
double ukdot;
|
||||
double cuk, suk;
|
||||
double ok;
|
||||
double sok, cok;
|
||||
double ik;
|
||||
double ikdot;
|
||||
double sik, cik;
|
||||
double rk;
|
||||
double rkdot;
|
||||
double xpk, ypk;
|
||||
double xpkdot, ypkdot;
|
||||
int cnt = 0;
|
||||
|
||||
|
||||
@@ -210,8 +200,12 @@ void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_eph
|
||||
|
||||
ekdot = mkdot/( 1 - eph->ecc*cek );
|
||||
double tak = atan2(sqrt(1.0 - eph->ecc * eph->ecc) * sek, cek - eph->ecc);
|
||||
double takdot = sek * ekdot * (1.0 + eph->ecc * cos(tak)) / (sin(tak) * (1.0 - eph->ecc* cek));
|
||||
relativistic = -4.442807633E-10 * eph->ecc * eph->sqrta * sek;
|
||||
double takdot =
|
||||
sqrt(1.0 - eph->ecc * eph->ecc) * ekdot /
|
||||
(1.0 - eph->ecc * cek);
|
||||
const double relativistic_coefficient = -4.442807633E-10;
|
||||
relativistic =
|
||||
relativistic_coefficient * eph->ecc * eph->sqrta * sek;
|
||||
|
||||
double phi = tak + eph->omg0;
|
||||
phi = remainder(phi, 2 * PI);
|
||||
@@ -219,18 +213,59 @@ void computeRange(double* psTVLTime, unsigned char sat_no, range_t* rho, gps_eph
|
||||
double u = phi + eph->cus * sin(2.0 * phi) + eph->cuc * cos(2.0 * phi);
|
||||
double r = eph->A * (1.0 - eph->ecc * cos(ek)) + eph->crc * cos(2 * phi) + eph->crs * sin(2 * phi);
|
||||
double i = eph->inc0 + eph->idot * tk + eph->cic * cos(2 * phi) + eph->cis * sin(2 * phi);
|
||||
double udot = takdot +
|
||||
2.0 * (eph->cus * cos(2.0 * phi) -
|
||||
eph->cuc * sin(2.0 * phi)) * takdot;
|
||||
double rdot = eph->A * eph->ecc * sin(ek) * ekdot +
|
||||
2.0 * (eph->crs * cos(2.0 * phi) -
|
||||
eph->crc * sin(2.0 * phi)) * takdot;
|
||||
double idot = eph->idot +
|
||||
2.0 * (eph->cis * cos(2.0 * phi) -
|
||||
eph->cic * sin(2.0 * phi)) * takdot;
|
||||
|
||||
double Omega = eph->omgdot + (eph->omgkdot - OMEGA_EARTH) * tk - OMEGA_EARTH * eph->toe.sec;
|
||||
Omega = remainder(Omega + 2 * PI, 2 * PI);
|
||||
double Omega_dot = eph->omgkdot - OMEGA_EARTH;
|
||||
|
||||
pos[0] = cos(u) * r * cos(Omega) - sin(u) * r * cos(i) * sin(Omega);
|
||||
pos[1] = cos(u) * r * sin(Omega) + sin(u) * r * cos(i) * cos(Omega);
|
||||
pos[2] = sin(u) * r * sin(i);
|
||||
double x_orbit = r * cos(u);
|
||||
double y_orbit = r * sin(u);
|
||||
double x_orbit_dot = rdot * cos(u) - y_orbit * udot;
|
||||
double y_orbit_dot = rdot * sin(u) + x_orbit * udot;
|
||||
|
||||
pos[0] = x_orbit * cos(Omega) - y_orbit * cos(i) * sin(Omega);
|
||||
pos[1] = x_orbit * sin(Omega) + y_orbit * cos(i) * cos(Omega);
|
||||
pos[2] = y_orbit * sin(i);
|
||||
vel[0] =
|
||||
x_orbit_dot * cos(Omega) -
|
||||
x_orbit * sin(Omega) * Omega_dot -
|
||||
y_orbit_dot * cos(i) * sin(Omega) +
|
||||
y_orbit * sin(i) * idot * sin(Omega) -
|
||||
y_orbit * cos(i) * cos(Omega) * Omega_dot;
|
||||
vel[1] =
|
||||
x_orbit_dot * sin(Omega) +
|
||||
x_orbit * cos(Omega) * Omega_dot +
|
||||
y_orbit_dot * cos(i) * cos(Omega) -
|
||||
y_orbit * sin(i) * idot * cos(Omega) -
|
||||
y_orbit * cos(i) * sin(Omega) * Omega_dot;
|
||||
vel[2] = y_orbit_dot * sin(i) + y_orbit * cos(i) * idot;
|
||||
|
||||
// Satellite clock correction
|
||||
// relativistic correction
|
||||
//*clockCorr = eph->af0 + tk * (eph->af1 + tk * eph->af2) + relativistic - eph->tgd; //7.23706041149e-05
|
||||
*clockCorr = (eph->af2 * clock_tk + eph->af1) * clock_tk + eph->af0 - eph->tgd + relativistic;
|
||||
double clock_correction =
|
||||
(eph->af2 * clock_tk + eph->af1) * clock_tk +
|
||||
eph->af0 - eph->tgd + relativistic;
|
||||
double clock_drift =
|
||||
(2.0 * eph->af2 * clock_tk) + eph->af1 +
|
||||
relativistic_coefficient * eph->ecc * eph->sqrta *
|
||||
cek * ekdot;
|
||||
if(clockCorr != NULL) {
|
||||
*clockCorr = clock_correction;
|
||||
}
|
||||
if(clk != NULL) {
|
||||
clk[0] = clock_correction;
|
||||
clk[1] = clock_drift;
|
||||
}
|
||||
/*if (clk[0] != SECONDS_IN_HALF_WEEK) {
|
||||
*clockCorr = clk[0];//eph->af0 + tk * (eph->af1 + tk * eph->af2) + relativistic - eph->tgd;
|
||||
}
|
||||
@@ -268,7 +303,9 @@ static bool least_squares_ura_sigma_m(unsigned char ura_index, double* sigma_m)
|
||||
|
||||
static double least_squares_measurement_weight(unsigned char ura_index,
|
||||
double elevation_deg,
|
||||
bool elevation_valid)
|
||||
bool elevation_valid,
|
||||
double cn0_dbhz,
|
||||
bool cn0_valid)
|
||||
{
|
||||
const double dtr = 0.01745329251994329576923690768489;
|
||||
const double min_elevation_deg = 10.0;
|
||||
@@ -296,6 +333,25 @@ static double least_squares_measurement_weight(unsigned char ura_index,
|
||||
double elevation_sigma_m = elevation_sigma_zenith_m / sin_el;
|
||||
variance_m2 += elevation_sigma_m * elevation_sigma_m;
|
||||
}
|
||||
if(cn0_valid && isfinite(cn0_dbhz))
|
||||
{
|
||||
/*
|
||||
* Receiver-specific code-tracking model. At 45 dB-Hz the additional
|
||||
* sigma is 3 m; every 20 dB change scales amplitude-domain error by
|
||||
* ten. Bounds prevent one channel dominating before calibration.
|
||||
*/
|
||||
double cn0_sigma_m =
|
||||
3.0 * pow(10.0, (45.0 - cn0_dbhz) / 20.0);
|
||||
if(cn0_sigma_m < 0.5)
|
||||
{
|
||||
cn0_sigma_m = 0.5;
|
||||
}
|
||||
if(cn0_sigma_m > 30.0)
|
||||
{
|
||||
cn0_sigma_m = 30.0;
|
||||
}
|
||||
variance_m2 += cn0_sigma_m * cn0_sigma_m;
|
||||
}
|
||||
return 1.0 / variance_m2;
|
||||
}
|
||||
|
||||
@@ -527,6 +583,7 @@ static bool least_squares_build_postfit(const mat_type* satpos,
|
||||
const mat_type* obs,
|
||||
int satellite_count,
|
||||
const unsigned char* ura_indices,
|
||||
const double* cn0_dbhz,
|
||||
const ionoutc_t* ionoutc,
|
||||
double gps_tow,
|
||||
const double state[4],
|
||||
@@ -583,7 +640,10 @@ static bool least_squares_build_postfit(const mat_type* satpos,
|
||||
az->matrix[0][i] = topocentric.azim;
|
||||
el->matrix[0][i] = topocentric.elv;
|
||||
|
||||
double weight = least_squares_measurement_weight(ura_indices[i], topocentric.elv, true);
|
||||
double weight = least_squares_measurement_weight(
|
||||
ura_indices[i], topocentric.elv, true,
|
||||
(cn0_dbhz != NULL) ? cn0_dbhz[i] : NAN,
|
||||
(cn0_dbhz != NULL) && isfinite(cn0_dbhz[i]));
|
||||
if(!isfinite(weight) || (weight <= 0.0))
|
||||
{
|
||||
return false;
|
||||
@@ -629,6 +689,7 @@ static double gps_wls_last_solution[4] = {0.0, 0.0, 0.0, 0.0};
|
||||
static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber,
|
||||
mat_type * az, mat_type * el,
|
||||
const unsigned char* ura_indices,
|
||||
const double* cn0_dbhz,
|
||||
const ionoutc_t* ionoutc, double gps_tow,
|
||||
const double seed[4], bool seed_valid,
|
||||
gps_nav_solution_t* solution_out)
|
||||
@@ -636,7 +697,7 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
if((satpos == NULL) || (obs == NULL) || (az == NULL) || (el == NULL) ||
|
||||
(ura_indices == NULL) || (solution_out == NULL) ||
|
||||
(satpos->matrix == NULL) || (obs->matrix == NULL) ||
|
||||
(ActiveSatellitesNumber < MIN_NEEDED_SATELLITE) ||
|
||||
(ActiveSatellitesNumber < (int)GPS_NAV_MIN_WLS_SATELLITES) ||
|
||||
((unsigned int)ActiveSatellitesNumber > GPS_TRCK_IP_COUNT))
|
||||
{
|
||||
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV, "least squares skipped invalid inputs, sats=%d\r\n",
|
||||
@@ -748,7 +809,10 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
initialize_all(0.0, A);
|
||||
for(int i = 0 ; i < ActiveSatellitesNumber ; i++)
|
||||
{
|
||||
absoluteWeights[i] = least_squares_measurement_weight(ura_indices[i], 0.0, false);
|
||||
absoluteWeights[i] = least_squares_measurement_weight(
|
||||
ura_indices[i], 0.0, false,
|
||||
(cn0_dbhz != NULL) ? cn0_dbhz[i] : NAN,
|
||||
(cn0_dbhz != NULL) && isfinite(cn0_dbhz[i]));
|
||||
}
|
||||
|
||||
bool seeded_from_last_solution = seed_valid && (seed != NULL);
|
||||
@@ -843,7 +907,10 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
//TODO: topocent e giren matrix lerin elemanları doğru yükleniyor mu diye bakılacak. !!!!!!!!!!!!!!!!!1
|
||||
az->matrix[0][i] = topcent->azim;
|
||||
el->matrix[0][i] = topcent->elv;
|
||||
absoluteWeights[i] = least_squares_measurement_weight(ura_indices[i], topcent->elv, true);
|
||||
absoluteWeights[i] = least_squares_measurement_weight(
|
||||
ura_indices[i], topcent->elv, true,
|
||||
(cn0_dbhz != NULL) ? cn0_dbhz[i] : NAN,
|
||||
(cn0_dbhz != NULL) && isfinite(cn0_dbhz[i]));
|
||||
iono = klobuchar_delay_meters(ionoutc, gps_tow,
|
||||
phi_lambda_h->phi, phi_lambda_h->lambda,
|
||||
topcent->azim, topcent->elv);
|
||||
@@ -989,7 +1056,8 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
|
||||
double residuals_m[GPS_TRCK_IP_COUNT];
|
||||
if(!least_squares_build_postfit(satpos, obs, ActiveSatellitesNumber,
|
||||
ura_indices, ionoutc, gps_tow,
|
||||
ura_indices, cn0_dbhz,
|
||||
ionoutc, gps_tow,
|
||||
posVec->matrix[0], A, az, el,
|
||||
residuals_m, absoluteWeights))
|
||||
{
|
||||
@@ -1140,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);
|
||||
|
||||
@@ -1201,11 +1291,14 @@ static bool least_squares_candidate(mat_type* satpos, mat_type* obs, int ActiveS
|
||||
solution_out->fde_applied = false;
|
||||
/* Four observations exactly determine x/y/z plus receiver clock bias. With
|
||||
* zero residual DOF no chi-square integrity statement is possible, but the
|
||||
* finite WLS solution and geometry covariance are still a valid minimum fix. */
|
||||
* raw finite WLS solution and covariance remain available to downstream
|
||||
* fusion, which performs its own temporal innovation test. */
|
||||
solution_out->solution_valid = solution_out->candidate_valid &&
|
||||
(((solution_out->used_satellite_count == 4u) &&
|
||||
(((solution_out->used_satellite_count ==
|
||||
GPS_NAV_MIN_WLS_SATELLITES) &&
|
||||
(solution_out->degrees_of_freedom == 0u)) ||
|
||||
(solution_out->residual_test_valid && solution_out->chi_square_pass));
|
||||
(solution_out->residual_test_valid &&
|
||||
solution_out->chi_square_pass));
|
||||
solution_out->iteration_count = iteration_count;
|
||||
solution_out->candidate_cycles = gps_read_cycle_counter() - candidate_start_cycles;
|
||||
|
||||
@@ -1231,6 +1324,7 @@ least_squares_fail:
|
||||
bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber,
|
||||
mat_type * az, mat_type * el,
|
||||
const unsigned char* ura_indices,
|
||||
const double* cn0_dbhz,
|
||||
const ionoutc_t* ionoutc, double gps_tow,
|
||||
gps_nav_solution_t* solution_out)
|
||||
{
|
||||
@@ -1243,7 +1337,8 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
|
||||
gps_nav_solution_t full_solution;
|
||||
if(!least_squares_candidate(satpos, obs, ActiveSatellitesNumber,
|
||||
az, el, ura_indices, ionoutc, gps_tow,
|
||||
az, el, ura_indices, cn0_dbhz,
|
||||
ionoutc, gps_tow,
|
||||
gps_wls_last_solution,
|
||||
gps_wls_last_solution_valid,
|
||||
&full_solution))
|
||||
@@ -1275,6 +1370,7 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
mat_type* subset_az = create_matrix(1, subset_count, "fdeAz");
|
||||
mat_type* subset_el = create_matrix(1, subset_count, "fdeEl");
|
||||
unsigned char subset_ura[GPS_TRCK_IP_COUNT];
|
||||
double subset_cn0[GPS_TRCK_IP_COUNT];
|
||||
if((subset_satpos == NULL) || (subset_obs == NULL) ||
|
||||
(subset_az == NULL) || (subset_el == NULL))
|
||||
{
|
||||
@@ -1304,6 +1400,9 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
}
|
||||
subset_obs->matrix[0][subset_row] = obs->matrix[0][original_row];
|
||||
subset_ura[subset_row] = ura_indices[original_row];
|
||||
subset_cn0[subset_row] =
|
||||
(cn0_dbhz != NULL) ?
|
||||
cn0_dbhz[original_row] : NAN;
|
||||
subset_row++;
|
||||
}
|
||||
|
||||
@@ -1314,6 +1413,7 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
gps_nav_solution_t candidate;
|
||||
bool candidate_ok = least_squares_candidate(subset_satpos, subset_obs, subset_count,
|
||||
subset_az, subset_el, subset_ura,
|
||||
subset_cn0,
|
||||
ionoutc, gps_tow,
|
||||
full_seed, true, &candidate);
|
||||
free_matrix(subset_satpos);
|
||||
@@ -1392,7 +1492,8 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
? (gps_read_cycle_counter() - fde_start_cycles) : 0u;
|
||||
selected_solution.total_wls_cycles = gps_read_cycle_counter() - total_wls_start_cycles;
|
||||
selected_solution.solution_valid = selected_solution.candidate_valid &&
|
||||
(((selected_solution.used_satellite_count == 4u) &&
|
||||
(((selected_solution.used_satellite_count ==
|
||||
GPS_NAV_MIN_WLS_SATELLITES) &&
|
||||
(selected_solution.degrees_of_freedom == 0u)) ||
|
||||
(selected_solution.residual_test_valid &&
|
||||
selected_solution.chi_square_pass));
|
||||
@@ -1406,8 +1507,7 @@ bool leastSquaresSTD(mat_type* satpos, mat_type* obs, int ActiveSatellitesNumber
|
||||
}
|
||||
*solution_out = selected_solution;
|
||||
|
||||
/* Print only publishable fixes. A four-satellite minimum fix is publishable,
|
||||
* although its separate integrity flag remains false because residual DOF=0. */
|
||||
/* Print every finite raw fix. Integrity remains a separate downstream input. */
|
||||
if(solution_out->solution_valid)
|
||||
{
|
||||
GPS_LOG(DEBUG_TRACE, GPS_LOG_CAT_NAV,
|
||||
|
||||