8 Commits

62 changed files with 10852 additions and 1030 deletions
+6
View File
@@ -7,3 +7,9 @@
# Generated build outputs.
/out/
/soc/out/
/hardware/generated/
# Python runtime caches and privacy-sensitive raw board captures.
__pycache__/
*.py[cod]
/soc/software/pps_end_to_end_validation/hardware_logs/*.csv
+3 -2
View File
@@ -19,7 +19,7 @@ help:
' make doctor # check local JDK/Mill/CIRCT and RISC-V tools' \
' make soc-hw # emit shared SoC FIRRTL through FPGA config' \
' make fpga-rtl # emit selected FPGA SystemVerilog and wrapper' \
' make fpga-bitstream # run selected FPGA Vivado bitstream flow' \
' make fpga-bitstream # run selected board vendor bitstream flow' \
' make fpga-elf-flash-image' \
' # build APP ELF and board flash-format app image' \
' make fpga-mcs # generate combined bitstream + app MCS/BIN' \
@@ -39,7 +39,7 @@ 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' \
@@ -49,6 +49,7 @@ 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:
+128 -8
View File
@@ -1,6 +1,7 @@
# Rocket Project
Bu agac VCU118/Genesys2 FPGA ve ASIC-style Rocket akislari icin sade proje duzenidir.
Bu agac VCU118, Genesys2 ve Ti375C529 FPGA akislari ile ASIC-style Rocket
akisi icin sade proje duzenidir.
Upstream Rocket-Chip kaynaklari daginik kok klasorlerde tutulmaz; gerekli
upstream kaynaklar `hardware/upstream/rocket-chip/` altindadir.
@@ -22,6 +23,7 @@ hardware/
soc/software/ Bootloader, BSP, uygulamalar, flash image tool
targets/fpga/vcu118/ VCU118 build/programlama akisi
targets/fpga/genesys2/ Genesys2 build/programlama akisi
targets/fpga/ti375c529/ Ti375C529 Efinity build/programlama akisi
targets/asic/rocket/ ASIC wrapper/filelist/sim akisi
setup_tools.sh /tools/soc-tools host araci kurulum betigi
```
@@ -209,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
```
@@ -219,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:
@@ -232,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.
@@ -240,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:
@@ -275,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`
@@ -305,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`
@@ -319,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`,
@@ -329,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:
@@ -432,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
@@ -0,0 +1,226 @@
# Efinity Interface Configuration for the Ti375C529 development kit.
# Pin assignments follow the kit user guide and the Efinity 2025.2 device DB.
# Board I/O bank voltages. Keep these aligned with the Efinix out-of-box
# Ti375C529 development-kit project.
design.set_device_property("2A", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2A", "2A_MODE_SEL", "2A")
design.set_device_property("2A", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2B", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2B", "2B_MODE_SEL", "2B")
design.set_device_property("2B", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2C", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2C", "2C_MODE_SEL", "2C")
design.set_device_property("2C", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2D", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2D", "2D_MODE_SEL", "2D")
design.set_device_property("2D", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("2E", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("2E", "2E_MODE_SEL", "2E")
design.set_device_property("2E", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4A_4B", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4A_4B", "4A_4B_MODE_SEL", "4A_4B")
design.set_device_property("4A_4B", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4C", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4C", "4C_MODE_SEL", "4C")
design.set_device_property("4C", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("4D", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("4D", "4D_MODE_SEL", "4D")
design.set_device_property("4D", "VOLTAGE", "1.8", "IOBANK")
design.set_device_property("BL2_BL3", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BL2_BL3", "BL2_MODE_SEL", "BL2")
design.set_mode_sel_name("BL2_BL3", "BL3_MODE_SEL", "BL3")
design.set_device_property("BL2_BL3", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("BR0", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BR0", "BR0_MODE_SEL", "BR0")
design.set_device_property("BR0", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("BR3_BR4", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("BR3_BR4", "BR3_MODE_SEL", "BR3")
design.set_mode_sel_name("BR3_BR4", "BR4_MODE_SEL", "BR4")
design.set_device_property("BR3_BR4", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TL1_TL5", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TL1_TL5", "TL1_MODE_SEL", "TL1")
design.set_mode_sel_name("TL1_TL5", "TL5_MODE_SEL", "TL5")
design.set_device_property("TL1_TL5", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR0", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR0", "TR0_MODE_SEL", "TR0")
design.set_device_property("TR0", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR1", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR1", "TR1_MODE_SEL", "TR1")
design.set_device_property("TR1", "VOLTAGE", "3.3", "IOBANK")
design.set_device_property("TR2", "DYNAMIC_VOLTAGE", "0", "IOBANK")
design.set_mode_sel_name("TR2", "TR2_MODE_SEL", "TR2")
design.set_device_property("TR2", "VOLTAGE", "3.3", "IOBANK")
# External GPIO.
design.create_input_gpio("reset_n")
design.set_property("reset_n", "PULL_OPTION", "WEAK_PULLUP")
design.create_input_gpio("uart_rx")
design.create_output_gpio("uart_tx")
# Lower 512-Mbit configuration flash. After active configuration completes,
# these dedicated SPI pins become fabric GPIO and are driven by Rocket's MMIO
# SPI controller so the BootROM can read the application region.
design.create_output_gpio("flash_sck")
design.create_output_gpio("flash_csn")
design.create_inout_gpio("flash_dq", 3, 0)
# The AX2771 sample clock is routed to J13 pin 10 / GPIOR_95_CLK12 so it
# enters the fabric through a dedicated global-clock input instead of being
# promoted from normal GPIO routing by the placer.
design.create_input_clock_gpio("rf_clk")
design.create_input_gpio("rf_data", 3, 0)
design.create_output_gpio("pmod_spi_sck")
design.create_output_gpio("pmod_spi_csn")
design.create_inout_gpio("pmod_spi_dq", 3, 0)
design.create_input_gpio("pmod_irq")
design.create_output_gpio("gps_pps")
design.create_output_gpio("led", 4, 0)
# The two PMOD headers, FTDI UART, reset switch, 100 MHz oscillator, and
# LED9/LED10 use the development kit's 3.3 V banks. GPIOs otherwise default
# to 1.8 V LVCMOS, so state these assignments explicitly.
design.set_property("reset_n", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("uart_rx", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("uart_tx", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("flash_sck", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_csn", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[0]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[1]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[2]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_dq[3]", "IO_STANDARD", "1.8_V_LVCMOS")
design.set_property("flash_csn", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[0]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[1]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[2]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("flash_dq[3]", "PULL_OPTION", "WEAK_PULLUP")
design.set_property("rf_clk", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[1]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[2]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("rf_data[3]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_sck", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_csn", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[1]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[2]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_spi_dq[3]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("pmod_irq", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("gps_pps", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("led[0]", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("led[1]", "IO_STANDARD", "3.3_V_LVCMOS")
# Rocket/fabric clock: 100 MHz board oscillator -> 100 MHz GCLK.
design.create_block("fabric_pll", "PLL")
design.gen_pll_ref_clock("fabric_pll", pll_res="PLL_BL0", refclk_src="EXTERNAL", refclk_name="board_clk_100", ext_refclk_no="1")
design.set_property("board_clk_100", "IO_STANDARD", "3.3_V_LVCMOS")
design.set_property("fabric_pll", "REFCLK_FREQ", "100", "PLL")
design.set_property("fabric_pll", "CLKOUT0_PIN", "fabric_clk", "PLL")
design.set_property("fabric_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("fabric_pll", "LOCKED_PIN", "fabric_pll_locked", "PLL")
design.auto_calc_pll_clock("fabric_pll", {"CLKOUT0_FREQ": "100.0", "CLKOUT0_PHASE": "0"})
# RF-coherent GPS processing clocks. The AX2771's 16.367981 MHz clock enters
# the fabric on J13 pin 10 / A12 (GPIOR_95_CLK12) and drives the same two-stage
# clock chain used by the Xilinx targets:
# RF -> ~55.55556 MHz -> 100 MHz acquisition / 125 MHz tracking.
design.create_block("gps_ref_pll", "PLL")
design.gen_pll_ref_clock("gps_ref_pll", pll_res="PLL_TR0", refclk_src="CORE")
design.set_property("gps_ref_pll", "CORE_CLK_PIN", "rf_clk", "PLL")
design.set_property("gps_ref_pll", "REFCLK_FREQ", "16.367981", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_EN", "1", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_PIN", "gps_ref_clk_gen", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_PIN", "gps_ref_feedback", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_ref_pll", "LOCKED_PIN", "gps_ref_pll_locked", "PLL")
design.set_property("gps_ref_pll", "FEEDBACK_MODE", "LOCAL", "PLL")
design.set_property("gps_ref_pll", "FEEDBACK_CLK", "CLK1", "PLL")
design.set_property("gps_ref_pll", "FRACTIONAL_MODE_EN", "1", "PLL")
design.set_property("gps_ref_pll", "FRACTIONAL_COEFFICIENT", "10715375", "PLL")
design.set_property("gps_ref_pll", "M", "4", "PLL")
design.set_property("gps_ref_pll", "N", "1", "PLL")
design.set_property("gps_ref_pll", "O", "1", "PLL")
design.set_property("gps_ref_pll", "CLKOUT0_DIV", "42", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_DIV", "35", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_PDIV", "5", "PLL")
design.set_property("gps_ref_pll", "CLKOUT1_SDIV", "30", "PLL")
design.calc_pll_clock("gps_ref_pll")
design.create_block("gps_domain_pll", "PLL")
design.gen_pll_ref_clock("gps_domain_pll", pll_res="PLL_TR1", refclk_src="CORE")
design.set_property("gps_domain_pll", "CORE_CLK_PIN", "gps_ref_clk_gen", "PLL")
design.set_property("gps_domain_pll", "REFCLK_FREQ", "55.55555593", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_EN", "1", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_EN", "1", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_PIN", "gps_domain_feedback", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_PIN", "gps_acq_clk_gen", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_PIN", "gps_trck_clk_gen", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_CONN_TYPE", "gclk", "PLL")
design.set_property("gps_domain_pll", "LOCKED_PIN", "gps_domain_pll_locked", "PLL")
design.set_property("gps_domain_pll", "FEEDBACK_MODE", "LOCAL", "PLL")
design.set_property("gps_domain_pll", "FEEDBACK_CLK", "CLK0", "PLL")
design.set_property("gps_domain_pll", "M", "2", "PLL")
design.set_property("gps_domain_pll", "N", "1", "PLL")
design.set_property("gps_domain_pll", "O", "2", "PLL")
design.set_property("gps_domain_pll", "CLKOUT0_DIV", "18", "PLL")
design.set_property("gps_domain_pll", "CLKOUT3_DIV", "20", "PLL")
design.set_property("gps_domain_pll", "CLKOUT4_DIV", "16", "PLL")
design.calc_pll_clock("gps_domain_pll")
# Hard JTAG USER1 exposes the TAP handshake at the FPGA core boundary.
design.create_block("rocket_jtag", "JTAG")
design.set_property("rocket_jtag", "NAME", "rocket_jtag", "JTAG")
design.set_property("rocket_jtag", "RESOURCE", "JTAG_USER1", "JTAG")
design.set_property("rocket_jtag", "CAPTURE", "jtag_CAPTURE", "JTAG")
design.set_property("rocket_jtag", "DRCK", "jtag_DRCK", "JTAG")
design.set_property("rocket_jtag", "RESET", "jtag_RESET", "JTAG")
design.set_property("rocket_jtag", "RUNTEST", "jtag_RUNTEST", "JTAG")
design.set_property("rocket_jtag", "SEL", "jtag_SEL", "JTAG")
design.set_property("rocket_jtag", "SHIFT", "jtag_SHIFT", "JTAG")
design.set_property("rocket_jtag", "TCK", "jtag_TCK", "JTAG")
design.set_property("rocket_jtag", "TDI", "jtag_TDI", "JTAG")
design.set_property("rocket_jtag", "TMS", "jtag_TMS", "JTAG")
design.set_property("rocket_jtag", "UPDATE", "jtag_UPDATE", "JTAG")
design.set_property("rocket_jtag", "TDO", "jtag_TDO", "JTAG")
design.assign_resource("rocket_jtag", "JTAG_USER1", "JTAG")
# Package assignments.
design.assign_pkg_pin("board_clk_100", "U4")
design.assign_pkg_pin("reset_n", "U19")
design.assign_pkg_pin("uart_rx", "E10")
design.assign_pkg_pin("uart_tx", "E9")
# Lower configuration flash U47: CDI0..3, CCK, and SSL_N. These assignments
# match Efinix's Ti375C529 JTAG-to-SPI loader reference design.
design.assign_pkg_pin("flash_dq[0]", "H4")
design.assign_pkg_pin("flash_dq[1]", "H5")
design.assign_pkg_pin("flash_dq[2]", "G5")
design.assign_pkg_pin("flash_dq[3]", "B7")
design.assign_pkg_pin("flash_sck", "G6")
design.assign_pkg_pin("flash_csn", "A7")
# PMOD0/J13 RF input. J13 pin 10 is the header's dedicated GCLK-capable pin;
# J13 pin 1 / G15 is intentionally unused.
design.assign_pkg_pin("rf_clk", "A12")
design.assign_pkg_pin("rf_data[0]", "G16")
design.assign_pkg_pin("rf_data[1]", "F16")
design.assign_pkg_pin("rf_data[2]", "F17")
design.assign_pkg_pin("rf_data[3]", "G17")
# PMOD1/J14 SPI + interrupt + PPS.
design.assign_pkg_pin("pmod_spi_csn", "B12")
design.assign_pkg_pin("pmod_spi_dq[0]", "C14")
design.assign_pkg_pin("pmod_spi_dq[1]", "C13")
design.assign_pkg_pin("pmod_spi_sck", "C12")
design.assign_pkg_pin("pmod_spi_dq[2]", "D12")
design.assign_pkg_pin("pmod_spi_dq[3]", "F12")
design.assign_pkg_pin("pmod_irq", "D13")
design.assign_pkg_pin("gps_pps", "E13")
# User LEDs LED9..LED13.
design.assign_pkg_pin("led[0]", "V4")
design.assign_pkg_pin("led[1]", "U6")
design.assign_pkg_pin("led[2]", "L18")
design.assign_pkg_pin("led[3]", "R20")
design.assign_pkg_pin("led[4]", "E19")
@@ -0,0 +1,8 @@
# AX2771 sample clock on PMOD J13. The 100/125 MHz GPS clocks generated by the
# two Efinity PLLs are frequency-related to this input; the Rocket fabric clock
# comes from the independent on-board 100 MHz oscillator.
create_clock -period 61.09495 -name rf_clk [get_ports {rf_clk}]
# The FTDI adapter is normally run at 1 MHz. Constraining the hard-TAP fabric
# interface to 10 MHz leaves margin if the debug adapter speed is increased.
create_clock -period 100.000 -name jtag_TCK [get_ports {jtag_TCK}]
@@ -0,0 +1,10 @@
# Efinity's generated .pt.sdc is read before this file and defines the fabric
# and PLL-output clocks. The Rocket oscillator, AX2771-derived GPS clocks, and
# hard-TAP JTAG clock are independent domains.
set_clock_groups -asynchronous \
-group [get_clocks {fabric_clk}] \
-group [get_clocks {rf_clk}] \
-group [get_clocks {gps_ref_clk_gen}] \
-group [get_clocks {gps_acq_clk_gen}] \
-group [get_clocks {gps_trck_clk_gen}] \
-group [get_clocks {jtag_TCK}]
+18
View File
@@ -0,0 +1,18 @@
.PHONY: help all shell generated clean
help:
@echo "Ti375C529 RTL targets:"
@echo " make shell # list hand-written FPGA shell RTL"
@echo " make generated # generate Rocket SystemVerilog and Efinix wrapper"
@echo " make clean # remove generated wrapper/verilog"
all: generated
shell:
@find . -type f -name '*.sv' -print | sort
generated:
@$(MAKE) -C ../../../../targets/fpga/ti375c529 rtl
clean:
rm -rf ../../../generated/ti375c529-soc-verilog ../../../../targets/fpga/ti375c529/out/build/generated
+4 -3
View File
@@ -18,9 +18,9 @@ JDK_SHA256="d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331"
MILL_URL="https://github.com/com-lihaoyi/mill/releases/download/$MILL_VERSION/$MILL_VERSION"
MILL_SHA256="ebe83649cd63acd33608f6d14452c074be2ed7d00cd2f69f88573d3f6909774f"
CIRCT_ARCHIVE="circt-full-shared-linux-x64.tar.gz"
CIRCT_ARCHIVE="firrtl-bin-linux-x64.tar.gz"
CIRCT_URL="https://github.com/llvm/circt/releases/download/firtool-$FIRTOOL_VERSION/$CIRCT_ARCHIVE"
CIRCT_SHA256="dde442dcd0ada92e6f7db26f4744667471f7be61d10f2876297efb435fc9a5ec"
CIRCT_SHA256="10bf9b135a65e5d93fb0555e967eac4258400ee629731ba68a69be6b7f9781ce"
Z3_ARCHIVE="z3-$Z3_VERSION-x64-glibc-2.39.zip"
Z3_URL="https://github.com/Z3Prover/z3/releases/download/z3-$Z3_VERSION/$Z3_ARCHIVE"
@@ -85,7 +85,8 @@ if [[ ! -x "$TOOLS_ROOT/mill/$MILL_VERSION/mill" ]]; then
"${SUDO[@]}" install -m 0755 "$work_dir/mill" "$TOOLS_ROOT/mill/$MILL_VERSION/mill"
fi
if [[ ! -x "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" ]]; then
if ! "$TOOLS_ROOT/circt/firtool-$FIRTOOL_VERSION/bin/firtool" --version \
>/dev/null 2>&1; then
download "$CIRCT_URL" "$work_dir/$CIRCT_ARCHIVE" "$CIRCT_SHA256"
mkdir -p "$work_dir/circt"
tar -xzf "$work_dir/$CIRCT_ARCHIVE" -C "$work_dir/circt"
+8 -4
View File
@@ -954,7 +954,9 @@ olarak yayinlanir.
Ionosphere correction receiver azimut/elevasyonu hazir oldugunda WLS icinde
uygulanir.
- Ephemeris alanlarinin mevcut isimleri ile ICD anlamlari su sekildedir: `omgdot` gercekte `Omega0`, `omg0` gercekte argument of perigee `omega`, `omgkdot` gercekte `OmegaDot` degeridir. `satpos()` bunlari dogru matematiksel yerde kullanir; konu su an hesap hatasi degil, ileride yapilacak kontrollu isim duzeltmesidir.
- `POSTPONE_TOW=0.160` donanim/pipeline zaman kalibrasyonunun bir parcasidir; kaynagi dogrulanmadan kaldirilmamalidir.
- `POSTPONE_TOW=0.180` mevcut RTL'nin dokuzuncu takip eden navigation bitinde
yaptigi teslimi duzeltir. `satpos()` ve PPS receive-time anchor bu degeri
tam bir kez ve ayni firmware surumunde kullanir.
- Ilk nav-solve interrupt'i geldiginde o anda allocated, hardware busy ve nav-solve interrupt'i etkin olan kanallar epoch'un sabit bekleme maskesine alinir. Unlock interrupt'i gelen kanal artik tracked olmadigi icin bekleme maskesinden cikarilir; epoch ortasinda navigation-ready olan yeni kanal ise aktif maskeyi buyutmez ve sonraki epoch'tan itibaren beklenir. Kalan beklenen kanallarin tamami gelirse snapshot son interrupt'ta hemen yayinlanir. 30 ms holdoff dolarsa veya daha yeni fiziksel epoch baslarsa eksik eski epoch WLS calistirilmadan discard edilir; eski ve yeni olcumler birlestirilmez.
- Interrupt bekleme maskesi tamamlandiktan sonra frozen kanallarin tumu ortak TOW/`ABS_SAMPLE` kumesinde degilse epoch reddedilir. Selector'in buldugu daha kucuk alt kume yalniz diagnostic bilgidir ve WLS girdisi olamaz.
- Dort uydu 3D konum + receiver clock bias icin minimum ve yeterlidir. Bu durumda
@@ -1348,9 +1350,11 @@ Bu timer/scheduler yavasligi degildir. Firmware start ediyor; tamamlanma icin RF
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.
- `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
@@ -55,6 +55,7 @@
#endif
#define GPS_NAV_BITS_PER_WEEK 30240000u
#define GPS_NAV_SOLVE_PERIOD_MS (GPS_NAV_SOLVE_TIMER_TICKS * GPS_NAV_SOLVE_TIMER_TICK_MS)
#define GPS_NAV_BITS_PER_SUBFRAME 300u
/* Keep integrity statistics active, but do not let leave-one-out FDE
* replace the full-WLS position until the real measurement-noise model
* is calibrated. The live stationary test produced a 22.7% exclusion
@@ -83,6 +84,8 @@
#error "GPS_NAV_SOLVE_TIMER_TICKS must be non-zero"
#elif ((GPS_NAV_BITS_PER_WEEK % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide 30240000 GPS navigation bits per week"
#elif ((GPS_NAV_BITS_PER_SUBFRAME % GPS_NAV_SOLVE_TIMER_TICKS) != 0u)
#error "GPS_NAV_SOLVE_TIMER_TICKS must divide the 300-bit LNAV subframe"
#endif
_Static_assert((GPS_NAV_ENABLE_FDE == 0u) || (GPS_NAV_ENABLE_FDE == 1u),
"GPS_NAV_ENABLE_FDE must be zero or one");
@@ -174,7 +177,10 @@
// gps defines from plutogpssim.
#define POSTPONE_TOW (double)(0.160)
/* Current tracking RTL validates the subframe on the ninth following
* navigation bit: 9 * 20 ms = 180 ms. satpos() and the PPS receive-time
* anchor must use this same delivery-epoch correction exactly once. */
#define POSTPONE_TOW (double)(0.180)
#define MAX_SAT (12) // 32 (TODO. OBSOLATED)
#define SECONDS_IN_WEEK 604800.0
@@ -2,6 +2,9 @@
#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,
@@ -29,6 +32,8 @@ 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 {
@@ -43,13 +48,17 @@ typedef struct {
} 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 require sustained evidence and degradations require
* three consecutive observations. 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.
* 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,
@@ -57,6 +66,22 @@ gps_pps_profile_t gps_pps_quality_selector_update(
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
@@ -58,7 +58,9 @@ bool gps_pps_reference_bias_drift(
* 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.
* 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
+3 -1
View File
@@ -1844,7 +1844,9 @@ void gps_startup_initialization(unsigned int nsp_tick)
GPS_NAV_ENABLE_FDE);
gps_init_acq_tables();
gps_pps_init(nsp_tick);
gps_pps_quality_selector_init(&gps_pps_quality_selector);
gps_pps_quality_selector_init_period(
&gps_pps_quality_selector,
(double)nsp_tick * (double)GPS_NAV_SOLVE_TIMER_TICK_MS / 1000.0);
gps_pps_applied_profile = GPS_PPS_PROFILE_HOLDOVER;
gps_pps_status_log_decimator = 0u;
gps_pps_reference_svid_valid = false;
+49 -19
View File
@@ -65,7 +65,8 @@ enum {
};
#define GPS_PPS_VERSION_ADAPTIVE_LOOP 0x00020200u
#define GPS_PPS_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
#define GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS (2u * GPS_PPS_CLOCK_HZ)
#define GPS_PPS_REFERENCE_TIMEOUT_PERIODS 3u
static volatile gps_pps_regs_t* const gps_pps =
(volatile gps_pps_regs_t*)BSP_GPS_PPS_BASE;
@@ -77,6 +78,7 @@ static bool gps_pps_force_coarse;
static bool gps_pps_reference_committed;
static bool gps_pps_holdover;
static uint64_t gps_pps_last_reference_counter;
static uint64_t gps_pps_reference_timeout_ticks;
static gps_pps_time_filter_t gps_pps_time_filter;
static uint64_t gps_pps_read_u64(
@@ -94,19 +96,30 @@ static void gps_pps_write_q46(volatile uint32_t* lo, volatile uint32_t* hi, int6
*hi = (uint32_t)(encoded >> 32);
}
static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
static double gps_pps_update_period_sec(void)
{
const double pi = 3.14159265358979323846;
gps_pps_loop_profile_t parameters = gps_pps_loop_profile(profile);
const double update_period_sec = (gps_pps_nav_solve_tick != 0u) ?
((double)gps_pps_nav_solve_tick * 0.020) : 0.200;
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
const double q46 = 70368744177664.0;
int64_t kp_q46 = (int64_t)llround(
(2.0 * parameters.damping * wn / GPS_PPS_CLOCK_HZ) * q46);
int64_t ki_q46 = (int64_t)llround((wn * wn * update_period_sec / GPS_PPS_CLOCK_HZ) * q46);
uint32_t max_slew_q32 = (uint32_t)llround(
parameters.max_slew_ppm * 1.0e-6 * 4294967296.0);
return (gps_pps_nav_solve_tick != 0u) ?
((double)gps_pps_nav_solve_tick * 0.020) :
GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
static bool gps_pps_write_loop_profile(gps_pps_profile_t profile)
{
const double update_period_sec = gps_pps_update_period_sec();
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, update_period_sec);
int64_t kp_q46 = 0;
int64_t ki_q46 = 0;
uint32_t max_slew_q32 = 0u;
if(!gps_pps_loop_coefficients(
parameters, update_period_sec, (double)GPS_PPS_CLOCK_HZ,
&kp_q46, &ki_q46, &max_slew_q32))
{
GPS_LOG(DEBUG_ERROR, GPS_LOG_CAT_NAV,
"PPS loop coefficient generation failed profile=%s period_ms=%.1lf\r\n",
gps_pps_profile_name(profile), update_period_sec * 1000.0);
return false;
}
gps_pps_write_q46(&gps_pps->kp_q46_lo, &gps_pps->kp_q46_hi, kp_q46);
gps_pps_write_q46(&gps_pps->ki_q46_lo, &gps_pps->ki_q46_hi, ki_q46);
@@ -118,14 +131,25 @@ static void gps_pps_write_loop_profile(gps_pps_profile_t profile)
((parameters.lock_release_ticks & 0xffu) << 16) |
((parameters.lock_acquire_updates & 0xffu) << 24);
}
return true;
}
void gps_pps_init(unsigned int nav_solve_tick)
{
gps_pps_nav_solve_tick = nav_solve_tick;
uint64_t period_ticks =
((uint64_t)GPS_PPS_CLOCK_HZ * nav_solve_tick * 20u) / 1000u;
gps_pps_reference_timeout_ticks =
GPS_PPS_REFERENCE_TIMEOUT_PERIODS * period_ticks;
if(gps_pps_reference_timeout_ticks <
(uint64_t)GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS)
{
gps_pps_reference_timeout_ticks =
GPS_PPS_MIN_REFERENCE_TIMEOUT_TICKS;
}
gps_pps->control = 0u;
gps_pps_version = gps_pps->version;
gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
(void)gps_pps_write_loop_profile(GPS_PPS_PROFILE_ROBUST);
gps_pps_active_loop_profile = GPS_PPS_PROFILE_ROBUST;
/* A GDB CPU reset does not reset the tracking-clock PPS state. Continue
* its mailbox sequence and force the first new solution to re-anchor the
@@ -149,12 +173,14 @@ void gps_pps_init(unsigned int nav_solve_tick)
GPS_PPS_CONTROL_CLEAR_STATUS;
GPS_LOG(DEBUG_STATUS, GPS_LOG_CAT_NAV,
"PPS init rtl=0x%08x adaptive=%u loop_config=0x%08x "
"clock_hz=%u update_ticks=%u\r\n",
"clock_hz=%u update_ticks=%u reference_timeout_ms=%lu\r\n",
gps_pps_version,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ? 1u : 0u,
gps_pps_version >= GPS_PPS_VERSION_ADAPTIVE_LOOP ?
gps_pps->loop_config : 0u,
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick);
GPS_PPS_CLOCK_HZ, gps_pps_nav_solve_tick,
(unsigned long)((gps_pps_reference_timeout_ticks * 1000u) /
GPS_PPS_CLOCK_HZ));
}
void gps_pps_service(void)
@@ -170,7 +196,7 @@ void gps_pps_service(void)
uint32_t now_hi = gps_pps->raw_counter_hi;
uint64_t now = ((uint64_t)now_hi << 32) | now_lo;
if((now - gps_pps_last_reference_counter) >
(uint64_t)GPS_PPS_REFERENCE_TIMEOUT_TICKS)
gps_pps_reference_timeout_ticks)
{
gps_pps_set_holdover(true);
}
@@ -229,7 +255,10 @@ bool gps_pps_set_loop_profile(gps_pps_profile_t profile)
{
return false;
}
gps_pps_write_loop_profile(profile);
if(!gps_pps_write_loop_profile(profile))
{
return false;
}
gps_pps_active_loop_profile = profile;
return true;
}
@@ -277,7 +306,8 @@ gps_pps_time_filter_result_t gps_pps_filter_clock_bias(
double* innovation_m_out)
{
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile(gps_pps_active_loop_profile);
gps_pps_loop_profile_for_period(
gps_pps_active_loop_profile, gps_pps_update_period_sec());
return gps_pps_time_filter_update(
&gps_pps_time_filter, epoch_counter, (double)GPS_PPS_CLOCK_HZ,
measured_clock_bias_m, clock_drift_valid,
@@ -7,6 +7,33 @@
#define GPS_PPS_PROFILE_UPGRADE_UPDATES 8u
#define GPS_PPS_PROFILE_DEGRADE_UPDATES 3u
#define GPS_PPS_MIN_HYSTERESIS_UPDATES 2u
#define GPS_PPS_MIN_LOCK_UPDATES 2u
#define GPS_PPS_MIN_SETTLE_UPDATES 3u
#define GPS_PPS_MAX_CONFIG_UPDATES 255u
static unsigned int gps_pps_period_updates(
unsigned int baseline_updates,
double update_period_sec,
unsigned int minimum_updates)
{
if(!isfinite(update_period_sec) || (update_period_sec <= 0.0))
{
update_period_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
}
double duration_sec =
(double)baseline_updates * GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
double required = ceil(duration_sec / update_period_sec - 1.0e-12);
if(required < (double)minimum_updates)
{
return minimum_updates;
}
if(required > (double)GPS_PPS_MAX_CONFIG_UPDATES)
{
return GPS_PPS_MAX_CONFIG_UPDATES;
}
return (unsigned int)required;
}
static bool gps_pps_metrics_finite(const gps_pps_quality_metrics_t* metrics)
{
@@ -67,12 +94,26 @@ static gps_pps_profile_t gps_pps_classify_quality(
}
void gps_pps_quality_selector_init(gps_pps_quality_selector_t* selector)
{
gps_pps_quality_selector_init_period(
selector, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
void gps_pps_quality_selector_init_period(
gps_pps_quality_selector_t* selector,
double update_period_sec)
{
if(selector != NULL)
{
memset(selector, 0, sizeof(*selector));
selector->profile = GPS_PPS_PROFILE_HOLDOVER;
selector->candidate = GPS_PPS_PROFILE_HOLDOVER;
selector->upgrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_UPGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
selector->degrade_updates = gps_pps_period_updates(
GPS_PPS_PROFILE_DEGRADE_UPDATES, update_period_sec,
GPS_PPS_MIN_HYSTERESIS_UPDATES);
}
}
@@ -111,8 +152,8 @@ gps_pps_profile_t gps_pps_quality_selector_update(
unsigned int required_updates =
(target > selector->profile) ?
GPS_PPS_PROFILE_UPGRADE_UPDATES :
GPS_PPS_PROFILE_DEGRADE_UPDATES;
selector->upgrade_updates :
selector->degrade_updates;
if(selector->candidate_updates >= required_updates)
{
selector->profile = target;
@@ -127,10 +168,19 @@ gps_pps_profile_t gps_pps_quality_selector_update(
gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
{
return gps_pps_loop_profile_for_period(
profile, GPS_PPS_BASELINE_UPDATE_PERIOD_SEC);
}
gps_pps_loop_profile_t gps_pps_loop_profile_for_period(
gps_pps_profile_t profile,
double update_period_sec)
{
gps_pps_loop_profile_t parameters;
switch(profile)
{
case GPS_PPS_PROFILE_ACQUIRE:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.120,
.damping = 0.9,
.max_slew_ppm = 20.0,
@@ -140,8 +190,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 16.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_PRECISION:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.035,
.damping = 0.9,
.max_slew_ppm = 20.0,
@@ -151,8 +202,9 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 12u
};
break;
case GPS_PPS_PROFILE_ROBUST:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.012,
.damping = 1.0,
.max_slew_ppm = 20.0,
@@ -162,9 +214,10 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
case GPS_PPS_PROFILE_HOLDOVER:
default:
return (gps_pps_loop_profile_t){
parameters = (gps_pps_loop_profile_t){
.natural_frequency_hz = 0.0,
.damping = 1.0,
.max_slew_ppm = 20.0,
@@ -174,7 +227,106 @@ gps_pps_loop_profile_t gps_pps_loop_profile(gps_pps_profile_t profile)
.software_bias_gain = 1.0 / 32.0,
.software_settle_updates = 16u
};
break;
}
parameters.lock_acquire_updates = gps_pps_period_updates(
parameters.lock_acquire_updates, update_period_sec,
GPS_PPS_MIN_LOCK_UPDATES);
parameters.software_settle_updates = gps_pps_period_updates(
parameters.software_settle_updates, update_period_sec,
GPS_PPS_MIN_SETTLE_UPDATES);
return parameters;
}
bool gps_pps_loop_coefficients(
gps_pps_loop_profile_t parameters,
double update_period_sec,
double clock_frequency_hz,
int64_t* kp_q46_out,
int64_t* ki_q46_out,
uint32_t* max_slew_q32_out)
{
if((kp_q46_out == NULL) || (ki_q46_out == NULL) ||
(max_slew_q32_out == NULL) ||
!isfinite(update_period_sec) || (update_period_sec <= 0.0) ||
!isfinite(clock_frequency_hz) || (clock_frequency_hz <= 0.0) ||
!isfinite(parameters.natural_frequency_hz) ||
(parameters.natural_frequency_hz < 0.0) ||
!isfinite(parameters.damping) || (parameters.damping < 0.0) ||
!isfinite(parameters.max_slew_ppm) ||
(parameters.max_slew_ppm < 0.0))
{
return false;
}
const double pi = 3.14159265358979323846;
const double q46 = 70368744177664.0;
const double q32 = 4294967296.0;
const double baseline_sec = GPS_PPS_BASELINE_UPDATE_PERIOD_SEC;
const double wn = 2.0 * pi * parameters.natural_frequency_hz;
/* Use the actual deployed Q2.46 baseline, not its ideal double value, so
* the mapped poles are those of the proven fixed-point implementation. */
const double baseline_kp =
(double)llround(
(2.0 * parameters.damping * wn / clock_frequency_hz) * q46) /
q46;
const double baseline_ki =
(double)llround(
(wn * wn * baseline_sec / clock_frequency_hz) * q46) /
q46;
const double baseline_ticks = clock_frequency_hz * baseline_sec;
const double baseline_sum =
2.0 - baseline_ticks * (baseline_kp + baseline_ki);
const double baseline_product =
1.0 - baseline_ticks * baseline_kp;
const double discriminant =
baseline_sum * baseline_sum - 4.0 * baseline_product;
const double period_ratio = update_period_sec / baseline_sec;
double target_sum = 0.0;
double target_product = 0.0;
if(discriminant >= 0.0)
{
double root = sqrt(discriminant);
double pole_a = 0.5 * (baseline_sum + root);
double pole_b = 0.5 * (baseline_sum - root);
if((pole_a < 0.0) || (pole_b < 0.0))
{
return false;
}
double target_a = pow(pole_a, period_ratio);
double target_b = pow(pole_b, period_ratio);
target_sum = target_a + target_b;
target_product = target_a * target_b;
}
else
{
if(baseline_product <= 0.0)
{
return false;
}
double radius = sqrt(baseline_product);
double cosine = baseline_sum / (2.0 * radius);
cosine = fmax(-1.0, fmin(1.0, cosine));
double angle = acos(cosine);
double target_radius = pow(radius, period_ratio);
target_sum =
2.0 * target_radius * cos(angle * period_ratio);
target_product = target_radius * target_radius;
}
double target_ticks = clock_frequency_hz * update_period_sec;
double kp = (1.0 - target_product) / target_ticks;
double ki = (1.0 - target_sum + target_product) / target_ticks;
if(!isfinite(kp) || !isfinite(ki) || (kp < 0.0) || (ki < 0.0))
{
return false;
}
*kp_q46_out = (int64_t)llround(kp * q46);
*ki_q46_out = (int64_t)llround(ki * q46);
*max_slew_q32_out = (uint32_t)llround(
parameters.max_slew_ppm * 1.0e-6 * q32);
return true;
}
const char* gps_pps_profile_name(gps_pps_profile_t profile)
@@ -20,6 +20,7 @@
#define GPS_PPS_FILTER_INNOVATION_CLAMP_M 300.0
#define GPS_PPS_FILTER_HARD_OUTLIER_M 1500.0
#define GPS_PPS_SPEED_OF_LIGHT_MPS 299792458.0
#define GPS_PPS_FILTER_BASELINE_PERIOD_SEC 0.200
static double clamp_double(double value, double limit)
{
@@ -34,6 +35,21 @@ static double clamp_double(double value, double limit)
return value;
}
/* Preserve the baseline discrete pole when the navigation interval changes.
* Applying a baseline gain alpha N times is equivalent to one elapsed-time
* update with alpha_T = 1 - (1 - alpha)^N. This also handles an occasional
* missed solve without slowing the clock-bias or drift estimator. */
static double gps_pps_filter_elapsed_gain(
double baseline_gain, double elapsed_sec)
{
double periods = elapsed_sec / GPS_PPS_FILTER_BASELINE_PERIOD_SEC;
if(fabs(periods - 1.0) <= 1.0e-12)
{
return baseline_gain;
}
return -expm1(periods * log1p(-baseline_gain));
}
void gps_pps_time_filter_init(gps_pps_time_filter_t* filter)
{
if(filter != NULL)
@@ -269,14 +285,20 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
return GPS_PPS_TIME_FILTER_REJECTED;
}
double effective_bias_gain =
gps_pps_filter_elapsed_gain(bias_gain, elapsed_sec);
double effective_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_DRIFT_GAIN, elapsed_sec);
double effective_fallback_drift_gain = gps_pps_filter_elapsed_gain(
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN, elapsed_sec);
filter->clock_bias_m =
predicted_clock_bias_m +
bias_gain *
effective_bias_gain *
clamp_double(innovation_m, GPS_PPS_FILTER_INNOVATION_CLAMP_M);
if(clock_drift_valid)
{
filter->clock_drift_mps +=
GPS_PPS_FILTER_DRIFT_GAIN *
effective_drift_gain *
(measured_clock_drift_mps - filter->clock_drift_mps);
}
else
@@ -291,7 +313,7 @@ gps_pps_time_filter_result_t gps_pps_time_filter_update(
else
{
filter->clock_drift_mps +=
GPS_PPS_FILTER_FALLBACK_DRIFT_GAIN *
effective_fallback_drift_gain *
(fallback_drift_mps - filter->clock_drift_mps);
}
}
@@ -91,14 +91,11 @@ static void profile_coefficients(
int64_t* ki_q46,
int64_t* max_slew_q32)
{
const double pi = 3.14159265358979323846;
double wn = 2.0 * pi * profile.natural_frequency_hz;
*kp_q46 = (int64_t)llround(
(2.0 * profile.damping * wn / TRACKING_HZ) * Q46_SCALE);
*ki_q46 = (int64_t)llround(
(wn * wn * UPDATE_PERIOD_SEC / TRACKING_HZ) * Q46_SCALE);
*max_slew_q32 = (int64_t)llround(
profile.max_slew_ppm * 1.0e-6 * Q32_SCALE);
uint32_t max_slew = 0u;
assert(gps_pps_loop_coefficients(
profile, UPDATE_PERIOD_SEC, TRACKING_HZ,
kp_q46, ki_q46, &max_slew));
*max_slew_q32 = max_slew;
}
static void synthetic_rtl_coarse(synthetic_rtl_t* rtl, int64_t reference_q32)
@@ -107,6 +107,69 @@ static void test_profile_parameters_are_ordered(void)
assert(robust.lock_release_ticks >= precision.lock_release_ticks);
}
static void test_period_scaled_hysteresis_and_settling(void)
{
gps_pps_quality_selector_t selector;
gps_pps_quality_selector_init_period(&selector, 0.5);
assert(selector.upgrade_updates == 4u);
assert(selector.degrade_updates == 2u);
gps_pps_loop_profile_t precision =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_PRECISION, 0.5);
gps_pps_loop_profile_t robust =
gps_pps_loop_profile_for_period(GPS_PPS_PROFILE_ROBUST, 0.5);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 5u);
assert(robust.lock_acquire_updates == 4u);
assert(robust.software_settle_updates == 7u);
gps_pps_quality_selector_init_period(&selector, 1.0);
assert(selector.upgrade_updates == 2u);
assert(selector.degrade_updates == 2u);
precision = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_PRECISION, 1.0);
robust = gps_pps_loop_profile_for_period(
GPS_PPS_PROFILE_ROBUST, 1.0);
assert(precision.lock_acquire_updates == 2u);
assert(precision.software_settle_updates == 3u);
assert(robust.lock_acquire_updates == 2u);
assert(robust.software_settle_updates == 4u);
}
static void assert_coefficients(
gps_pps_profile_t profile,
double period_sec,
int64_t expected_kp,
int64_t expected_ki)
{
int64_t kp = 0;
int64_t ki = 0;
uint32_t max_slew = 0u;
gps_pps_loop_profile_t parameters =
gps_pps_loop_profile_for_period(profile, period_sec);
assert(gps_pps_loop_coefficients(
parameters, period_sec, 125000000.0,
&kp, &ki, &max_slew));
assert(kp == expected_kp);
assert(ki == expected_ki);
assert(max_slew == 85899u);
}
static void test_period_scaled_discrete_coefficients(void)
{
/* The production 200 ms coefficients remain bit-for-bit unchanged. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.2, 764018, 64006);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.2, 222838, 5445);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.2, 84891, 640);
/* Other cadences exponentiate those proven discrete poles in time. */
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 0.5, 615780, 127492);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 0.5, 209783, 12804);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 0.5, 82980, 1564);
assert_coefficients(GPS_PPS_PROFILE_ACQUIRE, 1.0, 447388, 178038);
assert_coefficients(GPS_PPS_PROFILE_PRECISION, 1.0, 190239, 23149);
assert_coefficients(GPS_PPS_PROFILE_ROBUST, 1.0, 79923, 3011);
}
static void test_quality_boundaries_and_recovery(void)
{
gps_pps_quality_selector_t selector;
@@ -167,6 +230,8 @@ int main(void)
test_hysteretic_profile_transitions();
test_alternating_quality_does_not_upgrade();
test_profile_parameters_are_ordered();
test_period_scaled_hysteresis_and_settling();
test_period_scaled_discrete_coefficients();
test_quality_boundaries_and_recovery();
puts("gps_pps_quality tests passed");
return 0;
@@ -6,6 +6,30 @@
#define COUNTER_HZ 125000000.0
static void test_elapsed_time_gain_preserves_filter_poles(void)
{
gps_pps_time_filter_t filter;
gps_pps_time_filter_init(&filter);
double filtered = 0.0;
double innovation = 0.0;
assert(gps_pps_time_filter_update(
&filter, 0u, COUNTER_HZ, 0.0, true, 0.0, 1.0,
1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_COARSE);
assert(gps_pps_time_filter_update(
&filter, (uint64_t)COUNTER_HZ, COUNTER_HZ, 100.0, true, 100.0,
1.0, 1.0 / 32.0, 1u, &filtered, &innovation) ==
GPS_PPS_TIME_FILTER_TRACKING);
/* One 1 s update has exactly the pole of five baseline 200 ms updates. */
double expected_bias_gain = 1.0 - pow(1.0 - 1.0 / 32.0, 5.0);
double expected_drift_gain = 1.0 - pow(1.0 - 1.0 / 4.0, 5.0);
assert(fabs(filtered - 100.0 * expected_bias_gain) < 1.0e-12);
assert(fabs(filter.clock_drift_mps - 100.0 * expected_drift_gain) <
1.0e-12);
}
static void test_drift_prediction_rejects_bias_noise(void)
{
static const double noise_m[] = {
@@ -298,6 +322,7 @@ static void test_reference_selection_is_stable(void)
int main(void)
{
test_elapsed_time_gain_preserves_filter_poles();
test_drift_prediction_rejects_bias_noise();
test_quality_gap_and_outlier_handling();
test_bias_ramp_without_doppler_uses_bounded_fallback();
@@ -0,0 +1,260 @@
# GPS PPS end-to-end Linux validation
This package is a deterministic, causal validation of the project timing path
from synthetic GPS LNAV data to the physical PPS edge. It exists separately
from `pps_python`, whose models concentrate on WLS noise, oscillator behavior,
and adaptive-loop performance.
The validation does not use Linux wall-clock scheduling. The 1 kHz, 50 bit/s,
RF-sample, PCLK, and 125 MHz events advance from synthetic timestamps, making
the signed absolute-time result repeatable on a normal Linux host.
## What is modeled
The model follows the current firmware and RTL conventions:
1. It generates deterministic GPS LNAV subframes with D29*/D30* data inversion
and valid parity for all ten 30-bit words.
2. Each navigation bit is expanded to twenty 1 kHz prompt-sign samples.
3. The `bit_gen.sv` 40 ms `1 -> 0` transition acquisition, majority decision,
20 ms `bit_valid` cadence, and registered ABS_SAMPLE behavior are applied.
4. The tracking wrapper's chronological `2 + 300 + 8 = 310` bit window,
preamble search, parity check, `load_sigs`, and subframe interrupt ordering
are applied.
5. `nav_solve_timer` is initialized from HOW Z-count and counts later bit-valid
events. Navigation-solve interrupt, PPS counter capture, capture sequence,
and a 32-bit RF ABS_SAMPLE wrap are exercised.
6. Firmware reconstructs periodic TOW from the HOW/sample anchor and evaluates
`TOW + POSTPONE_TOW + STARTOFFSET - b/c`.
7. The timestamped 125 MHz PPS peripheral performs coarse/normal mapping,
ignores synthetic CPU/APB delay as phase, acquires lock after five valid
updates, and generates integer-second edges.
8. DUT edges are compared with an independent trusted PPS. Optional DUT and
trusted-output delays make board, cable, and vendor delay sign explicit.
This is a digital epoch-path validation, not an RF waveform simulator. The
existing `pps_python` tests continue to cover WLS, oscillator, PI, adaptive
quality, and holdover behavior.
## Run
From `soc/software`:
```bash
python3 -m pps_end_to_end_validation.pps_end_to_end
python3 -m unittest pps_end_to_end_validation/test_pps_end_to_end.py
```
The default command executes the diagnostic matrix:
```text
scenario bits correction reference error DUT - trusted PPS
current-rtl/180ms 9 180 ms -0.000032 ms +0.000032 ms
current-rtl/160ms 9 160 ms -20.000032 ms +20.000032 ms
current-bit/160ms 8 160 ms -0.000032 ms +0.000032 ms
```
Positive `DUT - trusted PPS` means the project output is late. The default
32 ns remainder is the explicitly configured four-tick PCLK capture pipeline;
it is not part of the LNAV-bit mismatch.
Run one scenario or obtain machine-readable output:
```bash
python3 -m pps_end_to_end_validation.pps_end_to_end \
--mode current-rtl --postpone-ms 160 --json
python3 -m pps_end_to_end_validation.pps_end_to_end \
--mode current-bit \
--dut-output-delay-ns 12 \
--trusted-output-delay-ns 35
```
## Closed-loop stability and dynamic tuning
The bit-level command above isolates the absolute epoch convention. The
companion stability analysis connects that result to the fixed-point
firmware/RTL PI model, replays the real GDOP and tracked-satellite history in
`fusion/logs/gps_live_5min_20260713.csv`, and synthesizes only the telemetry
that the repository did not capture:
```bash
python3 -m pps_end_to_end_validation.stability_analysis
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 500
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 1000
python3 -m unittest \
pps_end_to_end_validation/test_stability_analysis.py
```
Generated results are in `output/stability/`:
- `stability_report.md`: real-data inventory, discrete poles, absolute phase
wander, period jitter, dynamic parameter sweep, and recommendations;
- `parameter_sweep.csv`: machine-readable results for every scenario/tuning;
- `synthetic_telemetry.csv`: solve-cadence ABS_SAMPLE, REM_CODE_PHASE, corrected 125 MHz
capture, HDOP/VDOP/TDOP/GDOP, C/N0, pseudorange noise, clock sigma/drift,
oscillator and timing-error inputs.
The older fusion capture used by `stability_analysis.py` contains 989 GDOP
values and 990 tracked-satellite values (eight throughout), but no PPS
phase/rate, ABS_SAMPLE, C/N0, TDOP/HDOP, pseudorange residual, oscillator
temperature, or trusted-edge/TIC samples. The newer live-board workflow below
supplies many of those PPS status fields, while ABS_SAMPLE, REM_CODE_PHASE,
temperature, and trusted-edge timing remain unavailable. The stability CSV
labels replacements as synthetic; it must not be read as board telemetry.
The current acquire, precision, and robust fixed-point poles are inside the
unit circle at the 200, 500, and 1000 ms supported update rates, and the stress
analysis observes no unlock. It also exposes an important metrics issue:
cycle-to-cycle PPS period
jitter can remain below 20 ns while absolute phase moves slowly through tens
of nanoseconds. A physical 20 ns maximum therefore requires a common-clock
time-interval-counter capture and cable/output-delay calibration; a p95 period
jitter assertion or synthetic run cannot certify it.
The sweep includes the current coefficients, quieter/faster precision loops,
a robust-noise guard, a larger Doppler-drift gain, and a quality-scheduled
candidate. The generated report contains the current numerical comparison.
Production firmware maps the proven 200 ms discrete poles to the selected NAV
period and leaves the 200 ms Q2.46 constants bit-identical. The model and
firmware also scale quality/lock/settling timers in wall-clock time, convert
software bias/drift gains from actual elapsed time, and extend the stale-data
watchdog to `max(2 s, 3 * solve period)`. This preserves deterministic settling
and damping; it cannot preserve measurement-noise statistics when fewer WLS
samples are available. Production firmware uses the current-RTL-compatible
180 ms epoch correction independently of solve cadence.
A deterministic four-seed, 180 s smoke sweep with the current profiles gave:
| solve period | real-geometry absolute RMS | worst period step over current scenarios | unlocks |
|---:|---:|---:|---:|
| 200 ms | 11.17 ns | 16 ns | 0 |
| 500 ms | 15.96 ns | 16 ns | 0 |
| 1000 ms | 25.09 ns | 24 ns | 0 |
Thus 1000 ms is linearly stable but is not recommended when the stated
synthetic maximum-period-jitter target is 20 ns. Use 200 ms by default, or
500 ms when CPU load requires it. A 1000 ms solve needs hardware validation
and likely oscillator-temperature/frequency prediction; pole remapping alone
cannot recreate the missing measurements.
## Live board telemetry replay
`hardware_replay.py` accepts the host-timestamped UART CSV produced by the
fusion capture utility. It extracts the firmware fields that actually drive
or describe PPS: raw/filtered WLS clock bias, innovation, Doppler datum drift,
TDOP/GDOP/HDOP/VDOP, clock sigma, C/N0, tracked/used satellites, velocity/FDE,
reference SVID, fractional capture correction, RTL phase residual, rate word,
integrator, lock/profile, pulse count, and commit sequence.
Capture and replay from `soc/software`:
```bash
python3 fusion/serial_capture.py \
--port /dev/ttyUSB2 --baud 460800 --duration 300 \
--output /tmp/genesys2_pps.csv
python3 -m pps_end_to_end_validation.hardware_replay \
--input /tmp/genesys2_pps.csv \
--output-dir pps_end_to_end_validation/output/hardware_replay
python3 -m unittest \
pps_end_to_end_validation.test_hardware_replay
```
Raw UART files are ignored by Git because `[LOCATION]` records contain exact
coordinates. The replay output is normalized to PPS fields and deliberately
omits latitude/longitude.
For an already-running board, replay imports the first logged locked
filter/PI snapshot. Starting the model from reset would erase the observed
integrator and create a false acquisition transient. When status is decimated,
commit sequence numbers expand the trace back to the inferred solve cadence;
intermediate values are explicitly marked `interpolated=true`.
The replay infers cadence from UART host time divided by commit-sequence
advance, snapped to the 20 ms navigation-bit grid. Use
`--update-period-ms` only when host timestamps are unreliable.
The report separates three quantities that should not be conflated:
- direct `hw_phase` statistics from the board;
- fixed-point PI reproduction and a telemetry-conditioned model replay; and
- inferred active phase obtained by integrating `rate_cmd` under a constant
oscillator assumption.
Only the first two are directly constrained by firmware/RTL telemetry. The
third is useful for diagnosing slow wander but is not a trusted-generator
measurement. `hw_phase` itself is a 5 Hz commit residual, not the physical PPS
pin offset. A calibrated, common-clock TIC/scope capture is still required for
a maximum-20 ns output claim.
## 20 ms diagnosis
The current tracking wrapper shifts the current bit into `shift_reg_n`, while
its preamble/parity predicate reads the preceding `shift_reg_p`. On the eighth
following preamble bit, the validator therefore sees only the previous seven
bits. The register contains all eight after the edge, but the bit-valid edge
condition has ended. Validation occurs on the ninth bit, 20 ms later.
The implemented interface is consequently:
```text
current RTL delivery 9 * 20 ms = 180 ms
production POSTPONE_TOW 9 * 20 ms = 180 ms
signed epoch mismatch 0 ms
former firmware value 8 * 20 ms = 160 ms
former signed mismatch 20 ms
```
Production firmware now uses `POSTPONE_TOW=0.180` as the current-RTL-compatible
correction, consistently in both satellite-state and PPS calculations. It is
180 ms, not 190 ms. If RTL is later changed to validate a candidate window
containing the current eighth bit, firmware must return to 160 ms in the same
coordinated release.
The validation intentionally does not modify production RTL. Firmware changes
for period-aware control are exercised by the same coefficient and timing
tests; the 180 ms absolute epoch convention remains unchanged.
## Other regression coverage
The unit tests verify:
- parity-valid LNAV round-trip and rejection after one corrupted data bit;
- 1 kHz prompt-sign to 50 bit/s generation and the registered 1 ms sample lag;
- ninth-bit delivery in current RTL and eighth-bit delivery in the candidate
implementation;
- constant signed PPS error over successive physical edges;
- 180 ms firmware compatibility with current RTL;
- invariance to synthetic CPU/APB stalls when capture timestamps are used;
- DUT-versus-trusted output-delay sign; and
- rejection behavior explaining why firmware now compile-rejects
`NAV_SOLVE_TIMER=40`; and
- zero NAV-solve rejection and unchanged absolute epoch at N=10/25/50
(200/500/1000 ms).
The `N=40` case divides the GPS week but not a 300-bit subframe. When the model
refreshes the HOW/sample anchor after six seconds, the timer phase is half a
solve period away from that anchor and the current `N * 20 ms` snap rejects
otherwise valid solves. Firmware therefore requires N to divide both the GPS
week and the 300-bit subframe. Production-safe examples are N=10, 25, and 50.
If arbitrary timer periods are required later, reconstruct elapsed time on the
fundamental 20 ms navigation-bit grid instead of snapping through a refreshed
HOW anchor.
## Hardware confirmation
Before selecting a production correction:
1. Probe `bit_valid`, the final preamble bits, `preamble_ok`, parity result, and
`load_sigs` with ILA.
2. Count whether delivery is on following bit 8 or 9 in the flashed bitstream.
3. Measure DUT and trusted PPS with a common time-interval counter and record
the sign. The diagnosis predicts a DUT edge approximately 20 ms late.
4. Apply only nanosecond-scale output/cable calibration after the epoch
mismatch is removed.
@@ -0,0 +1 @@
"""End-to-end Linux validation for the GPS LNAV-to-PPS timing path."""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
host_monotonic_s,host_unix_s,time_s,profile,mode,locked,tdop,gdop,hdop,vdop,clock_sigma_m,cn0_mean_dbhz,cn0_min_dbhz,tracked_satellites,used_satellites,innovation_m,raw_bias_m,filtered_bias_m,clock_drift_mps,bias_drift_mps,predictor_enabled,eskf_alpha,eskf_speed_mps,eskf_velocity_rms_mps,eskf_fde_mask,pps_speed_mps,pps_velocity_rms_mps,pps_fde_mask,reference_svid,capture_correction_ns,hardware_phase_ns,rate_command_ppm,integrator_ppm,pulse_count,sequence,status
1.50420269,1785272055.903764,0.0,robust,tracking,True,1.25,2.86,1.8176601213,1.8176260703,12.54,42.7,37.7,6,5,4.52,693498.817,693494.442,110.788,-72.055,True,0.05,0.271,0.185,0,0.271,0.185,0,23,32.0,160.47,-1.536,-1.56,600,3040,2063
3.503689524,1785272057.9032507,2.0,robust,tracking,True,1.25,2.86,1.8176193953,1.8170996688,11.17,42.9,38.2,6,5,-19.2,693330.906,693349.504,111.201,-71.738,True,0.05,0.746,0.135,0,0.746,0.135,0,23,56.0,124.38,-1.541,-1.56,602,3050,2063
5.500479302,1785272059.9000404,4.0,robust,tracking,True,1.25,2.86,1.8175780522,1.8165687846,21.48,42.7,38.5,6,5,-7.0,693197.92,693204.698,111.206,-71.912,True,0.05,0.836,0.039,0,0.836,0.039,0,23,24.0,141.5,-1.539,-1.56,604,3060,2063
7.509988539,1785272061.9095497,6.0,robust,tracking,True,1.25,2.86,1.8175381286,1.816041859,11.14,43.0,38.3,6,5,13.6,693072.274,693059.102,111.221,-71.825,True,0.05,1.009,0.28,0,1.009,0.28,0,23,32.0,168.11,-1.535,-1.56,606,3070,2063
9.511653187,1785272063.9112144,8.0,robust,tracking,True,1.25,2.86,1.8174962624,1.8155120506,11.56,42.9,38.0,6,5,-31.15,692881.989,692912.165,110.864,-72.861,True,0.05,0.371,0.04,0,0.371,0.04,0,23,40.0,135.63,-1.539,-1.56,608,3080,2063
11.50378733,1785272065.9033487,10.0,robust,tracking,True,1.25,2.85,1.8174539102,1.8149874373,11.07,42.8,38.6,6,5,-9.78,692754.373,692763.848,110.014,-73.882,True,0.05,0.843,0.004,0,0.843,0.004,0,23,56.0,155.42,-1.536,-1.56,610,3090,2063
13.504178648,1785272067.9037397,12.0,robust,tracking,True,1.25,2.85,1.817413463,1.8144599125,11.18,42.9,38.1,6,5,6.48,692623.758,692617.477,110.329,-73.94,True,0.05,0.609,0.096,0,0.609,0.096,0,23,0.0,162.66,-1.535,-1.56,612,3100,2063
15.503554083,1785272069.9031153,14.0,robust,tracking,True,1.25,2.85,1.8173718635,1.8139319104,11.26,42.7,37.0,6,5,-1.31,692469.561,692470.834,111.54,-72.849,True,0.05,0.953,0.31,0,0.953,0.31,0,23,56.0,155.78,-1.536,-1.56,614,3110,2063
17.502924749,1785272071.9024858,16.0,robust,tracking,True,1.25,2.85,1.81733102,1.8134069522,15.4,42.4,36.5,6,5,-11.0,692313.987,692324.644,110.815,-73.581,True,0.05,0.736,0.168,0,0.736,0.168,0,23,32.0,133.05,-1.54,-1.56,616,3120,2063
19.506236288,1785272073.9057975,18.0,robust,tracking,True,1.24,2.85,1.8172902798,1.8128821788,11.11,43.1,38.1,6,5,-5.77,692171.911,692177.505,111.077,-73.779,True,0.05,0.4,0.027,0,0.4,0.027,0,23,24.0,131.86,-1.54,-1.56,618,3130,2063
21.504228726,1785272075.9037895,20.0,robust,tracking,True,1.24,2.85,1.8172490099,1.8123554135,11.15,43.0,38.0,6,5,8.57,692038.349,692030.045,110.612,-74.424,True,0.05,0.512,0.053,0,0.512,0.053,0,23,24.0,156.87,-1.536,-1.56,620,3140,2063
23.504153615,1785272077.903715,22.0,robust,tracking,True,1.24,2.85,1.8172104392,1.8118290291,11.3,42.6,37.7,6,5,-11.79,691868.479,691879.901,111.534,-73.806,True,0.05,0.721,0.036,0,0.721,0.036,0,23,56.0,142.52,-1.538,-1.56,622,3150,2063
25.510000609,1785272079.9095619,24.0,robust,tracking,True,1.24,2.85,1.8171689199,1.8113038519,11.08,43.1,38.1,6,5,-12.44,691719.345,691731.399,110.942,-74.863,True,0.05,0.359,0.101,0,0.359,0.101,0,23,48.0,145.5,-1.538,-1.56,624,3160,2063
27.50517151,1785272081.904733,26.0,robust,tracking,True,1.24,2.85,1.8171301373,1.8107796086,11.26,42.7,37.5,6,5,-18.16,691564.145,691581.742,111.5,-74.479,True,0.05,0.745,0.096,0,0.745,0.096,0,23,8.0,129.02,-1.54,-1.56,626,3170,2063
29.502641745,1785272083.9022028,28.0,robust,tracking,True,1.24,2.85,1.8170880804,1.8102547189,12.41,43.0,37.5,6,5,1.28,691433.115,691431.876,111.148,-74.903,True,0.05,0.332,0.115,0,0.332,0.115,0,23,0.0,127.84,-1.541,-1.56,628,3180,2063
31.500642933,1785272085.900204,30.0,robust,tracking,True,1.24,2.85,1.8170480561,1.8097326484,14.78,42.6,37.5,6,5,-1.39,691280.067,691281.411,111.681,-74.941,True,0.05,0.503,0.255,0,0.503,0.255,0,23,48.0,147.24,-1.538,-1.56,630,3190,2063
33.504307045,1785272087.903868,32.0,robust,tracking,True,1.24,2.85,1.8170082113,1.8092081529,22.56,42.9,38.4,6,5,-13.17,691116.408,691129.168,110.696,-75.765,True,0.05,0.206,0.027,0,0.206,0.027,0,23,16.0,132.81,-1.54,-1.56,632,3200,2063
35.509887989,1785272089.9094489,34.0,robust,tracking,True,1.24,2.85,1.8169688875,1.8086870549,11.18,42.9,38.0,6,5,-0.84,690974.741,690975.554,111.343,-75.376,True,0.05,0.636,0.055,0,0.636,0.055,0,23,56.0,156.39,-1.536,-1.56,634,3210,2063
37.506370892,1785272091.9059322,36.0,robust,tracking,True,1.24,2.85,1.8169289218,1.8081636846,10.95,43.4,39.5,6,5,-3.76,690819.152,690822.79,110.566,-76.732,True,0.05,0.72,0.058,0,0.72,0.058,0,23,32.0,156.77,-1.536,-1.56,636,3220,2063
39.504571912,1785272093.904133,38.0,robust,tracking,True,1.24,2.85,1.8168882102,1.8076411645,11.05,43.1,38.3,6,5,-8.88,690659.561,690668.166,111.385,-76.264,True,0.05,0.403,0.084,0,0.403,0.084,0,23,48.0,147.43,-1.538,-1.56,638,3230,2063
41.502657038,1785272095.9022183,40.0,robust,tracking,True,1.24,2.85,1.8168471874,1.8071184407,11.13,42.7,38.1,6,5,5.56,690520.399,690515.009,111.68,-75.754,True,0.05,0.957,0.102,0,0.957,0.102,0,23,32.0,147.4,-1.538,-1.56,640,3240,2063
43.50032369,1785272097.8998854,42.0,robust,tracking,True,1.24,2.85,1.816809289,1.8065980348,11.21,42.7,37.8,6,5,9.98,690370.621,690360.952,111.268,-76.081,True,0.05,0.807,0.041,0,0.807,0.041,0,23,0.0,136.35,-1.539,-1.56,642,3250,2063
45.712022504,1785272100.111584,44.0,robust,tracking,True,1.24,2.85,1.8167660234,1.8060226878,12.45,43.2,38.0,6,5,-7.85,690184.849,690192.456,111.097,-76.306,True,0.05,0.794,0.037,0,0.794,0.037,0,23,32.0,142.31,-1.538,-1.56,645,3260,2063
47.704670721,1785272102.1042318,46.0,robust,tracking,True,1.24,2.85,1.8167259917,1.8055025786,11.59,42.9,38.7,6,5,-14.42,690023.356,690037.324,110.386,-77.903,True,0.05,0.84,0.099,0,0.84,0.099,0,23,56.0,136.57,-1.539,-1.56,647,3270,2063
49.704359089,1785272104.1039205,48.0,robust,tracking,True,1.24,2.84,1.8166840843,1.804981981,11.04,43.4,38.2,6,5,-2.28,689880.716,689882.925,110.905,-78.019,True,0.05,0.509,0.107,0,0.509,0.107,0,23,16.0,148.6,-1.537,-1.56,649,3280,2063
51.709139916,1785272106.1087012,50.0,robust,tracking,True,1.24,2.84,1.8166456009,1.8044613748,10.99,43.3,38.1,6,5,-0.68,689726.579,689727.233,111.064,-77.798,True,0.05,0.094,0.103,0,0.094,0.103,0,23,48.0,144.29,-1.538,-1.56,651,3290,2063
53.703365655,1785272108.1029265,52.0,robust,tracking,True,1.24,2.84,1.8166083592,1.8039440331,12.68,42.8,37.5,6,5,-8.72,689564.342,689572.793,110.533,-78.338,True,0.05,0.504,0.187,0,0.504,0.187,0,23,8.0,148.54,-1.537,-1.56,653,3300,2063
55.700643307,1785272110.100204,54.0,robust,tracking,True,1.24,2.84,1.8165672875,1.8034221236,11.07,42.9,38.7,6,5,4.12,689421.398,689417.403,111.098,-77.598,True,0.05,0.645,0.08,0,0.645,0.08,0,23,40.0,140.91,-1.539,-1.56,655,3310,2063
57.904055806,1785272112.3036168,56.0,robust,tracking,True,1.24,2.84,1.8165247981,1.8028524558,16.67,42.9,38.2,6,5,-1.04,689243.202,689244.211,110.99,-78.254,True,0.05,0.342,0.02,0,0.342,0.02,0,23,48.0,144.27,-1.538,-1.56,657,3320,2063
60.10451733,1785272114.5040786,58.0,robust,tracking,True,1.24,2.84,1.8164827704,1.8022817921,11.19,42.9,38.0,6,5,-5.64,689066.66,689072.123,111.676,-78.014,True,0.05,0.73,0.15,0,0.73,0.15,0,23,48.0,126.86,-1.541,-1.56,659,3330,2063
62.108759952,1785272116.5083213,60.0,robust,tracking,True,1.24,2.84,1.8164435096,1.8017644864,16.38,43.1,39.2,6,5,-1.09,688913.112,688914.171,111.17,-78.316,True,0.05,0.696,0.007,0,0.696,0.007,0,23,24.0,153.74,-1.537,-1.56,661,3340,2063
64.300493137,1785272118.7000537,62.0,robust,tracking,True,1.24,2.84,1.8163984438,1.8011921234,11.32,42.4,37.4,6,5,8.12,688749.473,688741.606,110.452,-79.51,True,0.05,0.546,0.16,0,0.546,0.16,0,23,40.0,152.97,-1.537,-1.56,663,3350,2063
66.510932531,1785272120.9104934,64.0,robust,tracking,True,1.23,2.84,1.8163566675,1.8006252849,20.15,42.7,38.3,6,5,-2.84,688563.016,688565.769,110.92,-78.813,True,0.05,0.795,0.197,0,0.795,0.197,0,23,40.0,172.22,-1.535,-1.561,665,3360,2063
68.704372014,1785272123.103933,66.0,robust,tracking,True,1.23,2.84,1.8163145194,1.8000555234,15.52,42.8,38.5,6,5,-11.85,688380.019,688391.495,110.061,-80.47,True,0.05,0.796,0.074,0,0.796,0.074,0,23,48.0,155.17,-1.537,-1.56,668,3370,2063
70.703104898,1785272125.102666,68.0,robust,tracking,True,1.23,2.84,1.8162769265,1.7995395272,11.43,42.0,37.8,6,5,13.98,688244.124,688230.58,110.609,-80.264,True,0.05,0.381,0.125,0,0.381,0.125,0,23,16.0,179.45,-1.533,-1.56,670,3380,2063
72.702223062,1785272127.101784,70.0,robust,tracking,True,1.23,2.84,1.8162378878,1.7990227767,16.58,42.8,38.9,6,5,5.88,688078.671,688072.971,110.939,-80.36,True,0.05,0.666,0.053,0,0.666,0.053,0,23,24.0,166.76,-1.535,-1.56,672,3390,2063
74.700286036,1785272129.0998468,72.0,robust,tracking,True,1.23,2.84,1.816199267,1.7985063001,11.1,42.7,38.7,6,5,14.57,687926.671,687912.557,110.527,-80.516,True,0.05,0.312,0.03,0,0.312,0.03,0,23,32.0,162.81,-1.536,-1.56,674,3400,2063
76.71281947,1785272131.1123805,74.0,robust,tracking,True,1.23,2.84,1.8161600375,1.7979893506,11.11,42.7,38.6,6,5,-1.67,687750.089,687751.702,111.576,-79.6,True,0.05,1.026,0.083,0,1.026,0.083,0,23,16.0,142.71,-1.539,-1.56,676,3410,2063
78.704243128,1785272133.103804,76.0,robust,tracking,True,1.23,2.84,1.8161215432,1.7974747243,11.08,43.2,38.1,6,5,-4.48,687586.716,687591.059,110.819,-81.044,True,0.05,0.676,0.104,0,0.676,0.104,0,23,0.0,107.47,-1.544,-1.56,678,3420,2063
80.70401895,1785272135.10358,78.0,robust,tracking,True,1.23,2.84,1.8160831759,1.7969606981,20.64,42.3,37.5,6,5,-0.58,687429.447,687430.01,110.975,-81.135,True,0.05,0.351,0.106,0,0.351,0.106,0,23,56.0,161.62,-1.536,-1.56,680,3430,2063
82.710133903,1785272137.1096952,80.0,robust,tracking,True,1.23,2.84,1.8160471938,1.7964462492,21.18,43.0,39.1,6,5,11.46,687279.439,687268.334,110.976,-80.409,True,0.05,1.15,0.031,0,1.15,0.031,0,23,0.0,177.64,-1.534,-1.56,682,3440,2063
84.702669179,1785272139.1022303,82.0,robust,tracking,True,1.23,2.84,1.8160084174,1.7959315494,16.16,42.7,39.0,6,5,2.07,687108.261,687106.252,110.31,-81.912,True,0.05,0.401,0.04,0,0.401,0.04,0,23,48.0,145.45,-1.539,-1.56,684,3450,2063
86.700732144,1785272141.100293,84.0,robust,tracking,True,1.23,2.83,1.8159713938,1.7954167669,11.13,42.7,39.0,6,5,-12.66,686929.679,686941.939,110.952,-81.183,True,0.05,0.726,0.196,0,0.726,0.196,0,23,24.0,139.84,-1.539,-1.56,686,3460,2063
88.704090128,1785272143.103651,86.0,robust,tracking,True,1.23,2.83,1.8159335443,1.79490275,16.21,42.7,38.7,6,5,8.63,686784.906,686776.542,110.694,-82.024,True,0.05,0.209,0.052,0,0.209,0.052,0,23,48.0,176.01,-1.534,-1.56,688,3470,2063
90.704048278,1785272145.103609,88.0,robust,tracking,True,1.23,2.83,1.8158959585,1.7943905432,24.91,42.5,37.9,6,5,-0.15,686613.039,686613.185,110.563,-82.135,True,0.05,0.379,0.123,0,0.379,0.123,0,23,40.0,161.5,-1.536,-1.56,690,3480,2063
92.711508414,1785272147.1110694,90.0,robust,tracking,True,1.23,2.83,1.8158570425,1.793877413,24.43,42.7,38.0,6,5,15.4,686465.242,686450.326,111.554,-81.637,True,0.05,1.011,0.184,0,1.011,0.184,0,23,56.0,178.15,-1.534,-1.56,692,3490,2063
94.704749369,1785272149.1043103,92.0,robust,tracking,True,1.23,2.83,1.8158190794,1.7933647389,21.48,42.4,38.5,6,5,16.84,686303.4,686287.088,110.534,-82.726,True,0.05,0.195,0.029,0,0.195,0.029,0,23,48.0,171.95,-1.535,-1.56,694,3500,2063
96.703104991,1785272151.1026661,94.0,robust,tracking,True,1.23,2.83,1.815781917,1.7928524573,11.31,42.4,37.6,6,5,-1.05,686119.874,686120.892,109.749,-83.836,True,0.05,0.919,0.158,0,0.919,0.158,0,23,16.0,137.68,-1.54,-1.56,696,3510,2063
98.915123597,1785272153.3146844,96.0,robust,tracking,True,1.23,2.83,1.8157417458,1.792292301,11.49,42.2,37.0,6,5,-4.81,685933.786,685938.441,110.18,-83.485,True,0.05,0.687,0.188,0,0.687,0.188,0,23,40.0,130.24,-1.541,-1.56,698,3520,2063
101.104184426,1785272155.5037456,98.0,robust,tracking,True,1.23,2.83,1.8157012539,1.7917269932,14.98,42.4,37.7,6,5,-4.93,685751.271,685756.046,110.623,-83.514,True,0.05,0.285,0.185,0,0.285,0.185,0,23,40.0,144.59,-1.539,-1.56,700,3530,2063
103.108802454,1785272157.5083635,100.0,robust,tracking,True,1.23,2.83,1.8156626947,1.7912163488,25.23,42.4,38.0,6,5,8.11,685596.218,685588.366,110.401,-83.817,True,0.05,0.362,0.053,0,0.362,0.053,0,23,56.0,172.28,-1.535,-1.56,702,3540,2063
105.105464086,1785272159.505025,102.0,robust,tracking,True,1.23,2.83,1.8156280022,1.79070636,16.51,42.5,37.5,6,5,-15.09,685403.733,685418.356,110.792,-83.601,True,0.05,0.365,0.1,0,0.365,0.1,0,23,48.0,134.38,-1.54,-1.56,704,3550,2063
107.300312593,1785272161.6998734,104.0,robust,tracking,True,1.23,2.83,1.8155869207,1.790143438,20.41,42.6,38.3,6,5,4.06,685236.104,685232.167,111.771,-83.119,True,0.05,1.06,0.172,0,1.06,0.172,0,23,56.0,186.11,-1.532,-1.56,706,3560,2063
109.312626871,1785272163.7121878,106.0,robust,tracking,True,1.23,2.83,1.8155513007,1.7896363278,15.58,42.6,37.5,6,5,-5.9,685055.343,685061.062,110.302,-84.628,True,0.05,0.319,0.208,0,0.319,0.208,0,23,24.0,168.36,-1.535,-1.56,708,3570,2063
111.306453913,1785272165.7060149,108.0,robust,tracking,True,1.23,2.83,1.8155128487,1.789126556,21.63,42.6,38.0,6,5,16.48,684906.901,684890.938,110.502,-84.943,True,0.05,0.385,0.093,0,0.385,0.093,0,23,16.0,177.8,-1.534,-1.56,710,3580,2063
113.305323246,1785272167.7048843,110.0,robust,tracking,True,1.22,2.83,1.8154776515,1.7886176858,11.1,42.9,38.4,6,5,1.51,684721.855,684720.397,110.396,-84.747,True,0.05,0.197,0.218,0,0.197,0.218,0,23,32.0,163.81,-1.536,-1.56,712,3590,2063
115.30411121,1785272169.7036724,112.0,robust,tracking,True,1.22,2.83,1.8154394251,1.7881082833,22.64,43.1,38.0,6,5,-3.4,684547.463,684550.752,110.44,-85.241,True,0.05,0.429,0.029,0,0.429,0.029,0,23,8.0,152.36,-1.538,-1.56,714,3600,2063
117.303101565,1785272171.702663,114.0,robust,tracking,True,1.22,2.83,1.8154034756,1.7875997643,11.52,42.6,38.1,6,5,17.65,684398.444,684381.346,110.707,-84.967,True,0.05,0.405,0.059,0,0.405,0.059,0,23,24.0,192.86,-1.532,-1.56,716,3610,2063
119.306150448,1785272173.7057114,116.0,robust,tracking,True,1.22,2.83,1.8153676614,1.7870927007,12.04,42.5,37.5,6,5,-7.99,684203.354,684211.094,110.164,-85.459,True,0.05,0.631,0.155,0,0.631,0.155,0,23,32.0,144.3,-1.539,-1.56,718,3620,2063
121.300962576,1785272175.7005236,118.0,robust,tracking,True,1.22,2.83,1.8153303586,1.786584068,18.38,42.4,37.2,6,5,15.28,684055.675,684040.872,110.141,-86.063,True,0.05,0.481,0.133,0,0.481,0.133,0,23,48.0,200.68,-1.53,-1.56,720,3630,2063
123.303755452,1785272177.7033165,120.0,robust,tracking,True,1.22,2.82,1.815294335,1.7860767164,19.1,42.8,38.8,6,5,2.02,683871.81,683869.856,111.056,-85.022,True,0.05,1.151,0.085,0,1.151,0.085,0,23,64.0,169.36,-1.535,-1.56,722,3640,2063
125.304219262,1785272179.703781,122.0,robust,tracking,True,1.22,2.82,1.8152578155,1.785570688,11.11,42.7,38.6,6,5,-8.82,683688.289,683696.831,110.802,-85.693,True,0.05,0.637,0.051,0,0.637,0.051,0,23,8.0,158.16,-1.537,-1.56,724,3650,2063
127.304583545,1785272181.704145,124.0,robust,tracking,True,1.22,2.82,1.8152215994,1.7850628719,11.13,42.7,38.8,6,5,-1.48,683523.082,683524.519,110.235,-86.917,True,0.05,0.775,0.068,0,0.775,0.068,0,23,40.0,151.87,-1.538,-1.56,726,3660,2063
129.508905566,1785272183.9084663,126.0,robust,tracking,True,1.22,2.82,1.8151838123,1.7845085023,12.03,43.1,38.6,6,5,9.01,683342.021,683333.296,110.201,-87.019,True,0.05,0.331,0.094,0,0.331,0.094,0,23,0.0,157.45,-1.537,-1.56,728,3670,2063
131.500762179,1785272185.9003232,128.0,robust,tracking,True,1.22,2.82,1.8151487619,1.7840006032,12.83,42.6,38.6,6,5,3.63,683160.888,683157.371,110.357,-86.937,True,0.05,0.389,0.075,0,0.389,0.075,0,23,32.0,171.39,-1.535,-1.56,730,3680,2063
133.504344505,1785272187.9039054,130.0,robust,tracking,True,1.22,2.82,1.8151127862,1.7834955606,12.64,42.7,38.5,6,5,3.69,682986.797,682983.217,110.053,-87.63,True,0.05,0.791,0.01,0,0.791,0.01,0,23,0.0,175.65,-1.534,-1.56,732,3690,2063
135.509832366,1785272189.9094036,132.0,robust,tracking,True,1.22,2.82,1.8150762158,1.7829890753,11.13,42.7,38.4,6,5,5.1,682813.848,682808.909,110.531,-87.346,True,0.05,0.079,0.034,0,0.079,0.034,0,23,16.0,162.81,-1.536,-1.56,734,3700,2063
137.506720578,1785272191.9062817,134.0,robust,tracking,True,1.22,2.82,1.8150397064,1.7824837171,10.99,42.8,39.5,6,5,1.42,682635.318,682633.946,110.584,-87.635,True,0.05,0.038,0.097,0,0.038,0.097,0,23,32.0,155.09,-1.537,-1.56,736,3710,2063
139.504410571,1785272193.9039717,136.0,robust,tracking,True,1.22,2.82,1.8150041072,1.7819780719,11.15,42.7,38.1,6,5,-6.72,682453.853,682460.364,110.886,-87.452,True,0.05,0.412,0.407,0,0.412,0.407,0,23,48.0,139.96,-1.539,-1.56,738,3720,2063
141.503078598,1785272195.9026399,138.0,robust,tracking,True,1.22,2.82,1.8149699154,1.781475266,11.11,42.7,38.8,6,5,11.45,682296.086,682284.991,109.638,-89.46,True,0.05,1.419,0.005,0,1.419,0.005,0,23,40.0,167.17,-1.535,-1.56,740,3730,2063
143.500493916,1785272197.900055,140.0,robust,tracking,True,1.22,2.82,1.8149354252,1.7809734693,11.02,42.9,38.7,6,5,12.89,682120.284,682107.799,110.911,-87.736,True,0.05,0.88,0.067,0,0.88,0.067,0,23,56.0,162.67,-1.536,-1.56,742,3740,2063
145.512927625,1785272199.9124885,142.0,robust,tracking,True,1.22,2.82,1.8149005029,1.7804693538,10.96,42.9,39.4,6,5,-25.89,681906.117,681931.202,110.881,-87.866,True,0.05,0.775,0.078,0,0.775,0.078,0,23,8.0,107.83,-1.544,-1.56,744,3750,2063
147.506278297,1785272201.9058392,144.0,robust,tracking,True,1.22,2.82,1.8148661739,1.7799667491,10.98,43.0,39.0,6,5,-5.66,681746.195,681751.678,109.788,-89.613,True,0.05,1.009,0.035,0,1.009,0.035,0,23,56.0,152.27,-1.538,-1.56,746,3760,2063
149.505513865,1785272203.9050748,146.0,robust,tracking,True,1.22,2.82,1.8148312963,1.779461219,11.25,42.4,38.4,6,5,-7.95,681563.852,681571.554,110.854,-89.368,True,0.05,0.482,0.099,0,0.482,0.099,0,23,56.0,142.08,-1.539,-1.56,748,3770,2063
151.504103133,1785272205.903664,148.0,robust,tracking,True,1.22,2.82,1.8147976667,1.778959756,10.98,43.0,39.4,6,5,11.06,681403.38,681392.662,110.425,-89.358,True,0.05,0.199,0.058,0,0.199,0.058,0,23,16.0,161.29,-1.536,-1.56,750,3780,2063
153.502847324,1785272207.9024084,150.0,robust,tracking,True,1.22,2.82,1.8147608671,1.7784571849,15.23,42.6,38.9,6,5,9.66,681223.637,681214.276,110.279,-89.721,True,0.05,0.371,0.218,0,0.371,0.218,0,23,48.0,172.12,-1.535,-1.56,752,3790,2063
155.506297511,1785272209.9058583,152.0,robust,tracking,True,1.22,2.82,1.8147266277,1.7779565566,11.07,42.6,38.7,6,5,0.35,681034.417,681034.073,110.263,-89.772,True,0.05,0.371,0.13,0,0.371,0.13,0,23,56.0,180.04,-1.533,-1.56,754,3800,2063
157.500753602,1785272211.9003143,154.0,robust,tracking,True,1.22,2.82,1.814692541,1.777454151,11.13,42.4,38.4,6,5,8.94,680862.299,680853.639,110.737,-89.433,True,0.05,0.755,0.053,0,0.755,0.053,0,23,0.0,180.5,-1.533,-1.56,756,3810,2063
159.504450759,1785272213.904012,156.0,robust,tracking,True,1.22,2.82,1.8146585955,1.7769539906,11.22,42.3,38.2,6,5,14.47,680691.31,680677.294,110.442,-90.141,True,0.05,0.405,0.02,0,0.405,0.02,0,23,32.0,196.03,-1.531,-1.56,758,3820,2063
161.510423884,1785272215.9099848,158.0,robust,tracking,True,1.21,2.82,1.8146253251,1.7764523761,14.07,42.3,38.6,6,5,14.17,680511.698,680497.972,110.887,-89.977,True,0.05,0.551,0.182,0,0.551,0.182,0,23,32.0,184.51,-1.532,-1.56,760,3830,2063
163.504960681,1785272217.9045217,160.0,robust,tracking,True,1.21,2.81,1.8145907816,1.7759535814,10.99,42.6,39.7,6,5,9.57,680326.822,680317.554,110.339,-91.079,True,0.05,0.465,0.01,0,0.465,0.01,0,23,8.0,150.36,-1.538,-1.56,762,3840,2063
165.507220868,1785272219.9067822,162.0,robust,tracking,True,1.21,2.81,1.8145551226,1.7754518476,10.98,42.8,38.9,6,5,5.13,680143.185,680138.216,109.975,-91.641,True,0.05,0.834,0.034,0,0.834,0.034,0,23,0.0,144.58,-1.539,-1.56,764,3850,2063
167.503525545,1785272221.9030867,164.0,robust,tracking,True,1.21,2.81,1.8145228906,1.7749543943,14.36,42.8,38.7,6,5,4.72,679959.278,679954.706,110.179,-91.65,True,0.05,0.706,0.085,0,0.706,0.085,0,23,48.0,124.01,-1.542,-1.56,766,3860,2063
169.50061802,1785272223.900179,166.0,robust,tracking,True,1.21,2.81,1.8144898435,1.7744531221,11.09,42.8,37.7,6,5,13.57,679787.48,679774.331,110.775,-91.606,True,0.05,1.106,0.075,0,1.106,0.075,0,23,56.0,145.18,-1.538,-1.56,768,3870,2063
171.510388267,1785272225.9099493,168.0,robust,tracking,True,1.21,2.81,1.8144559568,1.7739550407,11.08,42.6,38.2,6,5,8.28,679599.18,679591.156,110.344,-91.505,True,0.05,0.285,0.062,0,0.285,0.062,0,23,56.0,137.18,-1.54,-1.56,770,3880,2063
173.500668495,1785272227.9002295,170.0,robust,tracking,True,1.21,2.81,1.8144213808,1.7734561125,11.07,42.5,38.0,6,5,-7.05,679401.0,679407.83,110.355,-91.896,True,0.05,0.239,0.023,0,0.239,0.023,0,23,48.0,151.22,-1.538,-1.56,772,3890,2063
175.509072834,1785272229.9086337,172.0,robust,tracking,True,1.21,2.81,1.8143875329,1.7729571416,14.88,42.6,38.0,6,5,-9.75,679214.759,679224.203,110.051,-92.203,True,0.05,0.388,0.024,0,0.388,0.024,0,23,16.0,127.04,-1.541,-1.56,774,3900,2063
177.511375431,1785272231.9109368,174.0,robust,tracking,True,1.21,2.81,1.8143541178,1.7724598606,11.01,42.7,39.1,6,5,-28.57,679010.25,679037.93,110.131,-92.548,True,0.05,0.679,0.066,0,0.679,0.066,0,23,16.0,144.09,-1.539,-1.56,776,3910,2063
179.5031023,1785272233.9026635,176.0,robust,tracking,True,1.21,2.81,1.8143216536,1.7719632746,11.04,42.6,38.1,6,5,-1.94,678850.304,678852.181,110.443,-92.232,True,0.05,0.439,0.164,0,0.439,0.164,0,23,24.0,156.61,-1.537,-1.56,778,3920,2063
181.500919559,1785272235.9004805,178.0,robust,tracking,True,1.21,2.81,1.8142885244,1.7714647145,11.67,42.7,39.6,6,5,14.37,678680.998,678667.079,110.223,-93.101,True,0.05,0.927,0.048,0,0.927,0.048,0,23,24.0,174.87,-1.534,-1.56,780,3930,2063
183.504154069,1785272237.903716,180.0,robust,tracking,True,1.21,2.81,1.8142515963,1.7709676955,11.12,42.4,38.5,6,5,3.52,678484.103,678480.688,110.013,-93.55,True,0.05,0.69,0.056,0,0.69,0.056,0,23,0.0,167.43,-1.535,-1.56,782,3940,2063
185.509701079,1785272239.9092622,182.0,robust,tracking,True,1.21,2.81,1.8142225906,1.7704719824,11.12,42.5,38.1,6,5,2.8,678298.275,678295.56,110.36,-92.989,True,0.05,0.188,0.156,0,0.188,0.156,0,23,0.0,175.14,-1.534,-1.56,784,3950,2063
187.511311613,1785272241.9108725,184.0,robust,tracking,True,1.21,2.81,1.8141898144,1.7699731912,11.11,42.3,38.5,6,5,-16.24,678094.632,678110.363,110.845,-93.036,True,0.05,0.46,0.009,0,0.46,0.009,0,23,32.0,150.91,-1.537,-1.56,786,3960,2063
189.504094586,1785272243.9036555,186.0,robust,tracking,True,1.21,2.81,1.8141560815,1.7694787336,11.13,42.5,37.9,6,5,-1.22,677924.284,677925.468,110.012,-93.838,True,0.05,0.542,0.031,0,0.542,0.031,0,23,32.0,167.75,-1.535,-1.56,788,3970,2063
191.503264307,1785272245.902825,188.0,robust,tracking,True,1.21,2.81,1.8141223587,1.7689845528,11.12,42.4,38.3,6,5,12.44,677751.751,677739.703,110.673,-93.502,True,0.05,0.691,0.123,0,0.691,0.123,0,23,32.0,181.14,-1.533,-1.56,790,3980,2063
193.500850983,1785272247.9004118,190.0,robust,tracking,True,1.21,2.81,1.8140919959,1.768488668,14.03,42.3,38.2,6,5,8.52,677559.555,677551.298,110.495,-93.954,True,0.05,0.376,0.004,0,0.376,0.004,0,23,56.0,155.05,-1.537,-1.56,792,3990,2063
195.507436225,1785272249.9069972,192.0,robust,tracking,True,1.21,2.81,1.8140572992,1.7679932608,10.97,42.9,38.9,6,5,-25.61,677336.289,677361.101,110.053,-94.87,True,0.05,0.565,0.132,0,0.565,0.132,0,23,0.0,143.1,-1.539,-1.56,794,4000,2063
197.710664279,1785272252.1102252,194.0,robust,tracking,True,1.21,2.81,1.8140224199,1.7674512561,11.06,42.7,38.3,6,5,-21.21,677131.244,677151.796,110.436,-94.668,True,0.05,0.467,0.099,0,0.467,0.099,0,23,16.0,127.66,-1.541,-1.56,797,4010,2063
199.904494084,1785272254.3040552,196.0,robust,tracking,True,1.21,2.81,1.8139878446,1.7669062479,11.21,42.4,37.9,6,5,2.5,676945.184,676942.759,110.719,-94.592,True,0.05,0.329,0.254,0,0.329,0.254,0,23,0.0,166.66,-1.535,-1.56,799,4020,2063
201.902748265,1785272256.3023093,198.0,robust,tracking,True,1.21,2.8,1.8139551469,1.7664122598,19.71,42.7,38.2,6,5,3.44,676756.483,676753.152,110.284,-95.508,True,0.05,0.834,0.075,0,0.834,0.075,0,23,48.0,159.1,-1.536,-1.56,801,4030,2063
203.900860006,1785272258.3004212,200.0,robust,tracking,True,1.21,2.8,1.8139243484,1.7659203155,21.27,42.6,37.9,6,5,16.7,676577.254,676561.076,111.005,-94.638,True,0.05,0.65,0.026,0,0.65,0.026,0,23,40.0,185.48,-1.532,-1.56,803,4040,2063
205.904311168,1785272260.303872,202.0,robust,tracking,True,1.21,2.8,1.8138926701,1.7654272361,11.23,42.3,37.5,6,5,-4.45,676364.571,676368.878,111.007,-94.853,True,0.05,0.681,0.154,0,0.681,0.154,0,23,32.0,146.48,-1.538,-1.56,805,4050,2063
207.911054786,1785272262.3106158,204.0,robust,tracking,True,1.21,2.8,1.8138607249,1.7649342967,11.29,42.2,37.1,6,5,-0.42,676178.038,676178.445,111.268,-95.121,True,0.05,0.669,0.187,0,0.669,0.187,0,23,56.0,148.37,-1.538,-1.56,807,4060,2063
209.904852911,1785272264.304414,206.0,robust,tracking,True,1.21,2.8,1.8138277745,1.7644418155,17.32,42.6,38.4,6,5,5.29,675993.222,675988.099,110.799,-95.578,True,0.05,0.772,0.012,0,0.772,0.012,0,23,48.0,167.53,-1.535,-1.56,809,4070,2063
211.904389957,1785272266.3039508,208.0,robust,tracking,True,1.2,2.8,1.8137949136,1.7639490637,14.88,42.5,38.1,6,5,4.76,675802.554,675797.942,109.97,-97.009,True,0.05,1.186,0.157,0,1.186,0.157,0,23,16.0,161.37,-1.536,-1.56,811,4080,2063
213.908887407,1785272268.3084483,210.0,robust,tracking,True,1.2,2.8,1.8137652527,1.7634585771,13.65,41.9,38.5,6,5,-19.97,675583.7,675603.041,110.734,-96.484,True,0.05,0.11,0.069,0,0.11,0.069,0,23,8.0,134.36,-1.54,-1.56,813,4090,2063
215.900878981,1785272270.3004403,212.0,robust,tracking,True,1.2,2.8,1.8137346862,1.7629700707,13.14,42.4,39.0,6,5,1.39,675411.049,675409.707,110.477,-96.41,True,0.05,0.489,0.149,0,0.489,0.149,0,23,48.0,164.12,-1.535,-1.56,815,4100,2063
217.908715503,1785272272.3082767,214.0,robust,tracking,True,1.2,2.8,1.8137007679,1.7624770049,11.19,42.1,38.1,6,5,-3.51,675214.305,675217.703,110.227,-97.513,True,0.05,0.667,0.022,0,0.667,0.022,0,23,56.0,141.12,-1.539,-1.56,817,4110,2063
219.904589627,1785272274.3041506,216.0,robust,tracking,True,1.2,2.8,1.8136705307,1.7619846326,17.3,42.2,38.8,6,5,-14.02,675007.687,675021.273,110.312,-97.218,True,0.05,0.452,0.069,0,0.452,0.069,0,23,24.0,158.28,-1.536,-1.56,819,4120,2063
221.9039367,1785272276.3034978,218.0,robust,tracking,True,1.2,2.8,1.8136409999,1.7614933749,11.12,42.1,39.2,6,5,-3.83,674821.953,674825.66,111.06,-96.371,True,0.05,1.326,0.028,0,1.326,0.028,0,23,48.0,170.5,-1.534,-1.56,821,4130,2063
223.91008895,1785272278.3096502,220.0,robust,tracking,True,1.2,2.8,1.8136105372,1.7610043295,11.73,42.3,38.7,6,5,0.13,674630.412,674630.288,111.079,-97.301,True,0.05,0.408,0.148,0,0.408,0.148,0,23,56.0,157.28,-1.536,-1.56,823,4140,2063
225.902916967,1785272280.302478,222.0,robust,tracking,True,1.2,2.8,1.8135794516,1.7605142092,21.26,41.9,39.1,6,5,-21.67,674413.179,674434.176,111.065,-97.127,True,0.05,0.752,0.068,0,0.752,0.068,0,23,48.0,124.73,-1.541,-1.56,825,4150,2063
227.901755726,1785272282.3013165,224.0,robust,tracking,True,1.2,2.8,1.8135490001,1.7600248185,11.02,42.5,39.3,6,5,13.8,674252.975,674239.611,110.715,-97.719,True,0.05,0.748,0.078,0,0.748,0.078,0,23,32.0,159.45,-1.536,-1.56,827,4160,2063
229.901619429,1785272284.3011806,226.0,robust,tracking,True,1.2,2.8,1.8135177823,1.7595391681,11.05,42.4,39.1,6,5,12.06,674053.956,674042.275,110.355,-98.939,True,0.05,0.78,0.043,0,0.78,0.043,0,23,56.0,170.95,-1.534,-1.56,829,4170,2063
231.90481137,1785272286.3043725,228.0,robust,tracking,True,1.2,2.8,1.8134877896,1.7590492494,11.09,42.4,38.1,6,5,12.72,673856.473,673844.146,110.721,-98.388,True,0.05,0.538,0.188,0,0.538,0.188,0,23,48.0,170.54,-1.534,-1.56,831,4180,2063
233.905326912,1785272288.3048878,230.0,robust,tracking,True,1.2,2.8,1.8134570418,1.7585604429,11.06,42.5,38.9,6,5,5.44,673653.112,673647.84,110.098,-98.963,True,0.05,0.301,0.311,0,0.301,0.311,0,23,8.0,171.59,-1.534,-1.56,833,4190,2063
235.904458614,1785272290.3040195,232.0,robust,tracking,True,1.2,2.8,1.8134261204,1.7580735849,11.06,42.5,38.5,6,5,-4.62,673445.637,673450.117,110.459,-98.844,True,0.05,0.829,0.028,0,0.829,0.028,0,23,0.0,153.73,-1.537,-1.56,835,4200,2063
237.904071483,1785272292.3036325,234.0,robust,tracking,True,1.2,2.8,1.8133959704,1.7575868747,12.8,42.0,37.7,6,5,-7.87,673243.852,673251.478,110.772,-98.845,True,0.05,0.559,0.037,0,0.559,0.037,0,23,0.0,146.96,-1.538,-1.56,837,4210,2063
239.908760739,1785272294.3083217,236.0,robust,tracking,True,1.2,2.8,1.8133666091,1.7570976374,11.1,42.3,38.6,6,5,4.99,673058.134,673053.301,110.565,-99.434,True,0.05,0.506,0.016,0,0.506,0.016,0,23,24.0,168.65,-1.535,-1.56,839,4220,2063
241.900563008,1785272296.300124,238.0,robust,tracking,True,1.2,2.79,1.8133344617,1.7566107514,11.14,42.3,38.4,6,5,-14.57,672839.788,672853.907,110.365,-99.522,True,0.05,0.253,0.141,0,0.253,0.141,0,23,0.0,147.65,-1.538,-1.56,841,4230,2063
243.904645899,1785272298.3042068,240.0,robust,tracking,True,1.2,2.79,1.8133057846,1.7561227378,11.1,42.2,38.8,6,5,3.18,672658.12,672655.039,109.903,-100.274,True,0.05,0.476,0.033,0,0.476,0.033,0,23,32.0,164.64,-1.535,-1.56,843,4240,2063
245.904621193,1785272300.3041825,242.0,robust,tracking,True,1.2,2.79,1.8132777969,1.7556370904,11.14,42.2,38.7,6,5,-0.81,672454.836,672455.618,110.573,-100.187,True,0.05,0.405,0.013,0,0.405,0.013,0,23,16.0,134.41,-1.54,-1.56,845,4250,2063
247.899436713,1785272302.2989979,244.0,robust,tracking,True,1.2,2.79,1.8132463791,1.7551512789,11.16,42.2,39.0,6,5,13.48,672269.138,672256.082,110.183,-100.179,True,0.05,0.565,0.113,0,0.565,0.113,0,23,40.0,169.04,-1.535,-1.56,847,4260,2063
249.911369626,1785272304.3109307,246.0,robust,tracking,True,1.2,2.79,1.813217727,1.754665179,11.11,42.5,39.2,6,5,-6.09,672049.638,672055.536,109.774,-100.683,True,0.05,0.486,0.021,0,0.486,0.021,0,23,0.0,135.93,-1.539,-1.56,849,4270,2063
251.903384606,1785272306.3029459,248.0,robust,tracking,True,1.2,2.79,1.8131881994,1.7541830169,11.18,42.2,39.0,6,5,10.96,671865.57,671854.952,109.778,-101.09,True,0.05,0.278,0.027,0,0.278,0.027,0,23,48.0,182.36,-1.532,-1.56,851,4280,2063
253.900781619,1785272308.3003423,250.0,robust,tracking,True,1.2,2.79,1.8131589097,1.7536951988,11.17,42.3,38.9,6,5,3.55,671659.189,671655.749,110.155,-100.561,True,0.05,0.584,0.008,0,0.584,0.008,0,23,16.0,141.85,-1.539,-1.56,853,4290,2063
255.910281973,1785272310.3098433,252.0,robust,tracking,True,1.2,2.79,1.8131293194,1.753212297,11.2,42.1,38.4,6,5,7.57,671462.581,671455.25,109.95,-101.447,True,0.05,0.189,0.096,0,0.189,0.096,0,23,16.0,141.76,-1.539,-1.56,855,4300,2063
257.904754461,1785272312.3043153,254.0,robust,tracking,True,1.2,2.79,1.8131029159,1.7527265372,11.15,42.4,38.7,6,5,-6.95,671245.456,671252.194,110.05,-101.621,True,0.05,0.518,0.084,0,0.518,0.084,0,23,56.0,134.53,-1.54,-1.56,857,4310,2063
259.904898749,1785272314.3044596,256.0,robust,tracking,True,1.2,2.79,1.8130720433,1.7522445406,11.3,41.8,38.4,6,5,-4.74,671044.584,671049.173,110.162,-101.624,True,0.05,0.281,0.225,0,0.281,0.225,0,23,0.0,142.99,-1.539,-1.56,859,4320,2063
261.904207465,1785272316.3037684,258.0,robust,tracking,True,1.19,2.79,1.8130440645,1.7517582107,11.31,42.0,38.4,6,5,-7.29,670837.352,670844.416,110.701,-101.27,True,0.05,0.844,0.152,0,0.844,0.152,0,23,56.0,167.54,-1.535,-1.56,861,4330,2063
263.903658409,1785272318.3032198,260.0,robust,tracking,True,1.19,2.79,1.8130134772,1.7512773631,11.34,42.0,38.5,6,5,-1.16,670637.855,670638.976,110.001,-102.901,True,0.05,0.697,0.02,0,0.697,0.02,0,23,48.0,166.85,-1.535,-1.56,863,4340,2063
265.906678701,1785272320.3062396,262.0,robust,tracking,True,1.19,2.79,1.8129886444,1.7507938348,14.74,42.5,38.9,6,5,10.12,670442.306,670432.505,110.568,-101.927,True,0.05,0.908,0.068,0,0.908,0.068,0,23,40.0,192.58,-1.531,-1.56,865,4350,2063
267.908740259,1785272322.308301,264.0,robust,tracking,True,1.19,2.79,1.8129614176,1.750311896,11.27,42.3,37.7,6,5,-11.12,670213.856,670224.632,110.96,-102.072,True,0.05,0.648,0.323,0,0.648,0.323,0,23,56.0,163.77,-1.535,-1.56,867,4360,2063
269.904666955,1785272324.304228,266.0,robust,tracking,True,1.19,2.79,1.8129315339,1.7498285268,11.35,41.9,37.9,6,5,0.23,670016.459,670016.233,110.584,-102.496,True,0.05,0.457,0.073,0,0.457,0.073,0,23,48.0,200.12,-1.53,-1.56,869,4370,2063
271.904417064,1785272326.303978,268.0,robust,tracking,True,1.19,2.79,1.8129033974,1.7493462127,11.29,42.0,38.2,6,5,-11.63,669798.53,669809.801,110.923,-102.202,True,0.05,0.955,0.178,0,0.955,0.178,0,23,16.0,154.9,-1.537,-1.56,871,4380,2063
273.904287144,1785272328.3038483,270.0,robust,tracking,True,1.19,2.79,1.8128759146,1.7488645046,11.24,42.2,38.4,6,5,0.32,669604.755,669604.44,110.23,-103.356,True,0.05,0.559,0.043,0,0.559,0.043,0,23,56.0,147.74,-1.538,-1.56,873,4390,2063
275.905752343,1785272330.3053133,272.0,robust,tracking,True,1.19,2.79,1.8128467845,1.7483871309,11.29,42.1,38.0,6,5,0.52,669397.133,669396.627,110.767,-103.125,True,0.05,0.839,0.033,0,0.839,0.033,0,23,48.0,175.42,-1.533,-1.56,875,4400,2063
277.901721957,1785272332.3012831,274.0,robust,tracking,True,1.19,2.79,1.8128179501,1.7479069779,11.23,42.2,38.8,6,5,-15.46,669172.771,669187.748,110.557,-103.518,True,0.05,0.505,0.152,0,0.505,0.152,0,23,8.0,141.55,-1.539,-1.56,877,4410,2063
279.901270286,1785272334.3008313,276.0,robust,tracking,True,1.19,2.79,1.8127918148,1.7474239577,11.26,42.2,38.9,6,5,5.21,668986.416,668981.367,110.705,-103.667,True,0.05,0.415,0.13,0,0.415,0.13,0,23,56.0,164.7,-1.535,-1.56,879,4420,2063
281.911805396,1785272336.3113663,278.0,robust,tracking,True,1.19,2.78,1.812764459,1.7469430861,11.14,42.3,38.6,6,5,9.2,668783.357,668774.443,110.765,-103.653,True,0.05,0.976,0.047,0,0.976,0.047,0,23,40.0,167.02,-1.535,-1.56,881,4430,2063
283.904627638,1785272338.3041887,280.0,robust,tracking,True,1.19,2.78,1.8127364977,1.7464643722,11.13,42.4,38.9,6,5,1.9,668567.589,668565.747,110.702,-104.554,True,0.05,0.496,0.084,0,0.496,0.084,0,23,56.0,150.73,-1.537,-1.56,883,4440,2063
285.904540282,1785272340.3041015,282.0,robust,tracking,True,1.19,2.78,1.8127111868,1.7459831701,11.26,42.2,38.2,6,5,-21.52,668336.38,668357.228,110.484,-104.5,True,0.05,0.4,0.023,0,0.4,0.023,0,23,16.0,127.92,-1.541,-1.56,885,4450,2063
287.903906607,1785272342.3034678,284.0,robust,tracking,True,1.19,2.78,1.8126838601,1.7455066962,11.09,42.3,39.2,6,5,-0.35,668145.403,668145.742,110.548,-104.863,True,0.05,0.13,0.213,0,0.13,0.213,0,23,0.0,156.5,-1.536,-1.56,887,4460,2063
289.903519609,1785272344.3030806,286.0,robust,tracking,True,1.19,2.78,1.8126545436,1.74502695,11.28,42.2,38.0,6,5,-17.14,667916.867,667933.474,110.923,-105.07,True,0.05,0.412,0.112,0,0.412,0.112,0,23,8.0,145.17,-1.538,-1.56,889,4470,2063
291.906994829,1785272346.3065557,288.0,robust,tracking,True,1.19,2.78,1.812627295,1.7445482935,12.84,42.5,38.6,6,5,-1.31,667721.551,667722.816,109.629,-106.134,True,0.05,0.721,0.164,0,0.721,0.164,0,23,40.0,158.89,-1.536,-1.56,891,4480,2063
293.904251457,1785272348.3038123,290.0,robust,tracking,True,1.19,2.78,1.8126009474,1.7440682755,11.16,42.3,38.2,6,5,14.39,667524.719,667510.777,110.381,-105.665,True,0.05,0.161,0.079,0,0.161,0.079,0,23,32.0,184.17,-1.532,-1.56,893,4490,2063
295.904732818,1785272350.3042939,292.0,robust,tracking,True,1.19,2.78,1.8125748376,1.7435915248,11.06,42.6,38.1,6,5,-9.72,667289.655,667299.068,110.248,-105.873,True,0.05,0.291,0.005,0,0.291,0.005,0,23,32.0,159.99,-1.536,-1.56,895,4500,2063
297.910797167,1785272352.3103583,294.0,robust,tracking,True,1.19,2.78,1.8125472744,1.7431147283,11.0,42.9,38.4,6,5,9.37,667097.136,667088.06,110.084,-106.181,True,0.05,0.237,0.074,0,0.237,0.074,0,23,16.0,168.11,-1.535,-1.56,897,4510,2063
299.905108443,1785272354.3046699,296.0,robust,tracking,True,1.19,2.78,1.812521259,1.7426375476,16.12,42.5,37.8,6,5,-12.01,666863.837,666875.472,110.096,-106.613,True,0.05,0.693,0.156,0,0.693,0.156,0,23,64.0,124.56,-1.541,-1.56,899,4520,2063
1 host_monotonic_s host_unix_s time_s profile mode locked tdop gdop hdop vdop clock_sigma_m cn0_mean_dbhz cn0_min_dbhz tracked_satellites used_satellites innovation_m raw_bias_m filtered_bias_m clock_drift_mps bias_drift_mps predictor_enabled eskf_alpha eskf_speed_mps eskf_velocity_rms_mps eskf_fde_mask pps_speed_mps pps_velocity_rms_mps pps_fde_mask reference_svid capture_correction_ns hardware_phase_ns rate_command_ppm integrator_ppm pulse_count sequence status
2 1.50420269 1785272055.903764 0.0 robust tracking True 1.25 2.86 1.8176601213 1.8176260703 12.54 42.7 37.7 6 5 4.52 693498.817 693494.442 110.788 -72.055 True 0.05 0.271 0.185 0 0.271 0.185 0 23 32.0 160.47 -1.536 -1.56 600 3040 2063
3 3.503689524 1785272057.9032507 2.0 robust tracking True 1.25 2.86 1.8176193953 1.8170996688 11.17 42.9 38.2 6 5 -19.2 693330.906 693349.504 111.201 -71.738 True 0.05 0.746 0.135 0 0.746 0.135 0 23 56.0 124.38 -1.541 -1.56 602 3050 2063
4 5.500479302 1785272059.9000404 4.0 robust tracking True 1.25 2.86 1.8175780522 1.8165687846 21.48 42.7 38.5 6 5 -7.0 693197.92 693204.698 111.206 -71.912 True 0.05 0.836 0.039 0 0.836 0.039 0 23 24.0 141.5 -1.539 -1.56 604 3060 2063
5 7.509988539 1785272061.9095497 6.0 robust tracking True 1.25 2.86 1.8175381286 1.816041859 11.14 43.0 38.3 6 5 13.6 693072.274 693059.102 111.221 -71.825 True 0.05 1.009 0.28 0 1.009 0.28 0 23 32.0 168.11 -1.535 -1.56 606 3070 2063
6 9.511653187 1785272063.9112144 8.0 robust tracking True 1.25 2.86 1.8174962624 1.8155120506 11.56 42.9 38.0 6 5 -31.15 692881.989 692912.165 110.864 -72.861 True 0.05 0.371 0.04 0 0.371 0.04 0 23 40.0 135.63 -1.539 -1.56 608 3080 2063
7 11.50378733 1785272065.9033487 10.0 robust tracking True 1.25 2.85 1.8174539102 1.8149874373 11.07 42.8 38.6 6 5 -9.78 692754.373 692763.848 110.014 -73.882 True 0.05 0.843 0.004 0 0.843 0.004 0 23 56.0 155.42 -1.536 -1.56 610 3090 2063
8 13.504178648 1785272067.9037397 12.0 robust tracking True 1.25 2.85 1.817413463 1.8144599125 11.18 42.9 38.1 6 5 6.48 692623.758 692617.477 110.329 -73.94 True 0.05 0.609 0.096 0 0.609 0.096 0 23 0.0 162.66 -1.535 -1.56 612 3100 2063
9 15.503554083 1785272069.9031153 14.0 robust tracking True 1.25 2.85 1.8173718635 1.8139319104 11.26 42.7 37.0 6 5 -1.31 692469.561 692470.834 111.54 -72.849 True 0.05 0.953 0.31 0 0.953 0.31 0 23 56.0 155.78 -1.536 -1.56 614 3110 2063
10 17.502924749 1785272071.9024858 16.0 robust tracking True 1.25 2.85 1.81733102 1.8134069522 15.4 42.4 36.5 6 5 -11.0 692313.987 692324.644 110.815 -73.581 True 0.05 0.736 0.168 0 0.736 0.168 0 23 32.0 133.05 -1.54 -1.56 616 3120 2063
11 19.506236288 1785272073.9057975 18.0 robust tracking True 1.24 2.85 1.8172902798 1.8128821788 11.11 43.1 38.1 6 5 -5.77 692171.911 692177.505 111.077 -73.779 True 0.05 0.4 0.027 0 0.4 0.027 0 23 24.0 131.86 -1.54 -1.56 618 3130 2063
12 21.504228726 1785272075.9037895 20.0 robust tracking True 1.24 2.85 1.8172490099 1.8123554135 11.15 43.0 38.0 6 5 8.57 692038.349 692030.045 110.612 -74.424 True 0.05 0.512 0.053 0 0.512 0.053 0 23 24.0 156.87 -1.536 -1.56 620 3140 2063
13 23.504153615 1785272077.903715 22.0 robust tracking True 1.24 2.85 1.8172104392 1.8118290291 11.3 42.6 37.7 6 5 -11.79 691868.479 691879.901 111.534 -73.806 True 0.05 0.721 0.036 0 0.721 0.036 0 23 56.0 142.52 -1.538 -1.56 622 3150 2063
14 25.510000609 1785272079.9095619 24.0 robust tracking True 1.24 2.85 1.8171689199 1.8113038519 11.08 43.1 38.1 6 5 -12.44 691719.345 691731.399 110.942 -74.863 True 0.05 0.359 0.101 0 0.359 0.101 0 23 48.0 145.5 -1.538 -1.56 624 3160 2063
15 27.50517151 1785272081.904733 26.0 robust tracking True 1.24 2.85 1.8171301373 1.8107796086 11.26 42.7 37.5 6 5 -18.16 691564.145 691581.742 111.5 -74.479 True 0.05 0.745 0.096 0 0.745 0.096 0 23 8.0 129.02 -1.54 -1.56 626 3170 2063
16 29.502641745 1785272083.9022028 28.0 robust tracking True 1.24 2.85 1.8170880804 1.8102547189 12.41 43.0 37.5 6 5 1.28 691433.115 691431.876 111.148 -74.903 True 0.05 0.332 0.115 0 0.332 0.115 0 23 0.0 127.84 -1.541 -1.56 628 3180 2063
17 31.500642933 1785272085.900204 30.0 robust tracking True 1.24 2.85 1.8170480561 1.8097326484 14.78 42.6 37.5 6 5 -1.39 691280.067 691281.411 111.681 -74.941 True 0.05 0.503 0.255 0 0.503 0.255 0 23 48.0 147.24 -1.538 -1.56 630 3190 2063
18 33.504307045 1785272087.903868 32.0 robust tracking True 1.24 2.85 1.8170082113 1.8092081529 22.56 42.9 38.4 6 5 -13.17 691116.408 691129.168 110.696 -75.765 True 0.05 0.206 0.027 0 0.206 0.027 0 23 16.0 132.81 -1.54 -1.56 632 3200 2063
19 35.509887989 1785272089.9094489 34.0 robust tracking True 1.24 2.85 1.8169688875 1.8086870549 11.18 42.9 38.0 6 5 -0.84 690974.741 690975.554 111.343 -75.376 True 0.05 0.636 0.055 0 0.636 0.055 0 23 56.0 156.39 -1.536 -1.56 634 3210 2063
20 37.506370892 1785272091.9059322 36.0 robust tracking True 1.24 2.85 1.8169289218 1.8081636846 10.95 43.4 39.5 6 5 -3.76 690819.152 690822.79 110.566 -76.732 True 0.05 0.72 0.058 0 0.72 0.058 0 23 32.0 156.77 -1.536 -1.56 636 3220 2063
21 39.504571912 1785272093.904133 38.0 robust tracking True 1.24 2.85 1.8168882102 1.8076411645 11.05 43.1 38.3 6 5 -8.88 690659.561 690668.166 111.385 -76.264 True 0.05 0.403 0.084 0 0.403 0.084 0 23 48.0 147.43 -1.538 -1.56 638 3230 2063
22 41.502657038 1785272095.9022183 40.0 robust tracking True 1.24 2.85 1.8168471874 1.8071184407 11.13 42.7 38.1 6 5 5.56 690520.399 690515.009 111.68 -75.754 True 0.05 0.957 0.102 0 0.957 0.102 0 23 32.0 147.4 -1.538 -1.56 640 3240 2063
23 43.50032369 1785272097.8998854 42.0 robust tracking True 1.24 2.85 1.816809289 1.8065980348 11.21 42.7 37.8 6 5 9.98 690370.621 690360.952 111.268 -76.081 True 0.05 0.807 0.041 0 0.807 0.041 0 23 0.0 136.35 -1.539 -1.56 642 3250 2063
24 45.712022504 1785272100.111584 44.0 robust tracking True 1.24 2.85 1.8167660234 1.8060226878 12.45 43.2 38.0 6 5 -7.85 690184.849 690192.456 111.097 -76.306 True 0.05 0.794 0.037 0 0.794 0.037 0 23 32.0 142.31 -1.538 -1.56 645 3260 2063
25 47.704670721 1785272102.1042318 46.0 robust tracking True 1.24 2.85 1.8167259917 1.8055025786 11.59 42.9 38.7 6 5 -14.42 690023.356 690037.324 110.386 -77.903 True 0.05 0.84 0.099 0 0.84 0.099 0 23 56.0 136.57 -1.539 -1.56 647 3270 2063
26 49.704359089 1785272104.1039205 48.0 robust tracking True 1.24 2.84 1.8166840843 1.804981981 11.04 43.4 38.2 6 5 -2.28 689880.716 689882.925 110.905 -78.019 True 0.05 0.509 0.107 0 0.509 0.107 0 23 16.0 148.6 -1.537 -1.56 649 3280 2063
27 51.709139916 1785272106.1087012 50.0 robust tracking True 1.24 2.84 1.8166456009 1.8044613748 10.99 43.3 38.1 6 5 -0.68 689726.579 689727.233 111.064 -77.798 True 0.05 0.094 0.103 0 0.094 0.103 0 23 48.0 144.29 -1.538 -1.56 651 3290 2063
28 53.703365655 1785272108.1029265 52.0 robust tracking True 1.24 2.84 1.8166083592 1.8039440331 12.68 42.8 37.5 6 5 -8.72 689564.342 689572.793 110.533 -78.338 True 0.05 0.504 0.187 0 0.504 0.187 0 23 8.0 148.54 -1.537 -1.56 653 3300 2063
29 55.700643307 1785272110.100204 54.0 robust tracking True 1.24 2.84 1.8165672875 1.8034221236 11.07 42.9 38.7 6 5 4.12 689421.398 689417.403 111.098 -77.598 True 0.05 0.645 0.08 0 0.645 0.08 0 23 40.0 140.91 -1.539 -1.56 655 3310 2063
30 57.904055806 1785272112.3036168 56.0 robust tracking True 1.24 2.84 1.8165247981 1.8028524558 16.67 42.9 38.2 6 5 -1.04 689243.202 689244.211 110.99 -78.254 True 0.05 0.342 0.02 0 0.342 0.02 0 23 48.0 144.27 -1.538 -1.56 657 3320 2063
31 60.10451733 1785272114.5040786 58.0 robust tracking True 1.24 2.84 1.8164827704 1.8022817921 11.19 42.9 38.0 6 5 -5.64 689066.66 689072.123 111.676 -78.014 True 0.05 0.73 0.15 0 0.73 0.15 0 23 48.0 126.86 -1.541 -1.56 659 3330 2063
32 62.108759952 1785272116.5083213 60.0 robust tracking True 1.24 2.84 1.8164435096 1.8017644864 16.38 43.1 39.2 6 5 -1.09 688913.112 688914.171 111.17 -78.316 True 0.05 0.696 0.007 0 0.696 0.007 0 23 24.0 153.74 -1.537 -1.56 661 3340 2063
33 64.300493137 1785272118.7000537 62.0 robust tracking True 1.24 2.84 1.8163984438 1.8011921234 11.32 42.4 37.4 6 5 8.12 688749.473 688741.606 110.452 -79.51 True 0.05 0.546 0.16 0 0.546 0.16 0 23 40.0 152.97 -1.537 -1.56 663 3350 2063
34 66.510932531 1785272120.9104934 64.0 robust tracking True 1.23 2.84 1.8163566675 1.8006252849 20.15 42.7 38.3 6 5 -2.84 688563.016 688565.769 110.92 -78.813 True 0.05 0.795 0.197 0 0.795 0.197 0 23 40.0 172.22 -1.535 -1.561 665 3360 2063
35 68.704372014 1785272123.103933 66.0 robust tracking True 1.23 2.84 1.8163145194 1.8000555234 15.52 42.8 38.5 6 5 -11.85 688380.019 688391.495 110.061 -80.47 True 0.05 0.796 0.074 0 0.796 0.074 0 23 48.0 155.17 -1.537 -1.56 668 3370 2063
36 70.703104898 1785272125.102666 68.0 robust tracking True 1.23 2.84 1.8162769265 1.7995395272 11.43 42.0 37.8 6 5 13.98 688244.124 688230.58 110.609 -80.264 True 0.05 0.381 0.125 0 0.381 0.125 0 23 16.0 179.45 -1.533 -1.56 670 3380 2063
37 72.702223062 1785272127.101784 70.0 robust tracking True 1.23 2.84 1.8162378878 1.7990227767 16.58 42.8 38.9 6 5 5.88 688078.671 688072.971 110.939 -80.36 True 0.05 0.666 0.053 0 0.666 0.053 0 23 24.0 166.76 -1.535 -1.56 672 3390 2063
38 74.700286036 1785272129.0998468 72.0 robust tracking True 1.23 2.84 1.816199267 1.7985063001 11.1 42.7 38.7 6 5 14.57 687926.671 687912.557 110.527 -80.516 True 0.05 0.312 0.03 0 0.312 0.03 0 23 32.0 162.81 -1.536 -1.56 674 3400 2063
39 76.71281947 1785272131.1123805 74.0 robust tracking True 1.23 2.84 1.8161600375 1.7979893506 11.11 42.7 38.6 6 5 -1.67 687750.089 687751.702 111.576 -79.6 True 0.05 1.026 0.083 0 1.026 0.083 0 23 16.0 142.71 -1.539 -1.56 676 3410 2063
40 78.704243128 1785272133.103804 76.0 robust tracking True 1.23 2.84 1.8161215432 1.7974747243 11.08 43.2 38.1 6 5 -4.48 687586.716 687591.059 110.819 -81.044 True 0.05 0.676 0.104 0 0.676 0.104 0 23 0.0 107.47 -1.544 -1.56 678 3420 2063
41 80.70401895 1785272135.10358 78.0 robust tracking True 1.23 2.84 1.8160831759 1.7969606981 20.64 42.3 37.5 6 5 -0.58 687429.447 687430.01 110.975 -81.135 True 0.05 0.351 0.106 0 0.351 0.106 0 23 56.0 161.62 -1.536 -1.56 680 3430 2063
42 82.710133903 1785272137.1096952 80.0 robust tracking True 1.23 2.84 1.8160471938 1.7964462492 21.18 43.0 39.1 6 5 11.46 687279.439 687268.334 110.976 -80.409 True 0.05 1.15 0.031 0 1.15 0.031 0 23 0.0 177.64 -1.534 -1.56 682 3440 2063
43 84.702669179 1785272139.1022303 82.0 robust tracking True 1.23 2.84 1.8160084174 1.7959315494 16.16 42.7 39.0 6 5 2.07 687108.261 687106.252 110.31 -81.912 True 0.05 0.401 0.04 0 0.401 0.04 0 23 48.0 145.45 -1.539 -1.56 684 3450 2063
44 86.700732144 1785272141.100293 84.0 robust tracking True 1.23 2.83 1.8159713938 1.7954167669 11.13 42.7 39.0 6 5 -12.66 686929.679 686941.939 110.952 -81.183 True 0.05 0.726 0.196 0 0.726 0.196 0 23 24.0 139.84 -1.539 -1.56 686 3460 2063
45 88.704090128 1785272143.103651 86.0 robust tracking True 1.23 2.83 1.8159335443 1.79490275 16.21 42.7 38.7 6 5 8.63 686784.906 686776.542 110.694 -82.024 True 0.05 0.209 0.052 0 0.209 0.052 0 23 48.0 176.01 -1.534 -1.56 688 3470 2063
46 90.704048278 1785272145.103609 88.0 robust tracking True 1.23 2.83 1.8158959585 1.7943905432 24.91 42.5 37.9 6 5 -0.15 686613.039 686613.185 110.563 -82.135 True 0.05 0.379 0.123 0 0.379 0.123 0 23 40.0 161.5 -1.536 -1.56 690 3480 2063
47 92.711508414 1785272147.1110694 90.0 robust tracking True 1.23 2.83 1.8158570425 1.793877413 24.43 42.7 38.0 6 5 15.4 686465.242 686450.326 111.554 -81.637 True 0.05 1.011 0.184 0 1.011 0.184 0 23 56.0 178.15 -1.534 -1.56 692 3490 2063
48 94.704749369 1785272149.1043103 92.0 robust tracking True 1.23 2.83 1.8158190794 1.7933647389 21.48 42.4 38.5 6 5 16.84 686303.4 686287.088 110.534 -82.726 True 0.05 0.195 0.029 0 0.195 0.029 0 23 48.0 171.95 -1.535 -1.56 694 3500 2063
49 96.703104991 1785272151.1026661 94.0 robust tracking True 1.23 2.83 1.815781917 1.7928524573 11.31 42.4 37.6 6 5 -1.05 686119.874 686120.892 109.749 -83.836 True 0.05 0.919 0.158 0 0.919 0.158 0 23 16.0 137.68 -1.54 -1.56 696 3510 2063
50 98.915123597 1785272153.3146844 96.0 robust tracking True 1.23 2.83 1.8157417458 1.792292301 11.49 42.2 37.0 6 5 -4.81 685933.786 685938.441 110.18 -83.485 True 0.05 0.687 0.188 0 0.687 0.188 0 23 40.0 130.24 -1.541 -1.56 698 3520 2063
51 101.104184426 1785272155.5037456 98.0 robust tracking True 1.23 2.83 1.8157012539 1.7917269932 14.98 42.4 37.7 6 5 -4.93 685751.271 685756.046 110.623 -83.514 True 0.05 0.285 0.185 0 0.285 0.185 0 23 40.0 144.59 -1.539 -1.56 700 3530 2063
52 103.108802454 1785272157.5083635 100.0 robust tracking True 1.23 2.83 1.8156626947 1.7912163488 25.23 42.4 38.0 6 5 8.11 685596.218 685588.366 110.401 -83.817 True 0.05 0.362 0.053 0 0.362 0.053 0 23 56.0 172.28 -1.535 -1.56 702 3540 2063
53 105.105464086 1785272159.505025 102.0 robust tracking True 1.23 2.83 1.8156280022 1.79070636 16.51 42.5 37.5 6 5 -15.09 685403.733 685418.356 110.792 -83.601 True 0.05 0.365 0.1 0 0.365 0.1 0 23 48.0 134.38 -1.54 -1.56 704 3550 2063
54 107.300312593 1785272161.6998734 104.0 robust tracking True 1.23 2.83 1.8155869207 1.790143438 20.41 42.6 38.3 6 5 4.06 685236.104 685232.167 111.771 -83.119 True 0.05 1.06 0.172 0 1.06 0.172 0 23 56.0 186.11 -1.532 -1.56 706 3560 2063
55 109.312626871 1785272163.7121878 106.0 robust tracking True 1.23 2.83 1.8155513007 1.7896363278 15.58 42.6 37.5 6 5 -5.9 685055.343 685061.062 110.302 -84.628 True 0.05 0.319 0.208 0 0.319 0.208 0 23 24.0 168.36 -1.535 -1.56 708 3570 2063
56 111.306453913 1785272165.7060149 108.0 robust tracking True 1.23 2.83 1.8155128487 1.789126556 21.63 42.6 38.0 6 5 16.48 684906.901 684890.938 110.502 -84.943 True 0.05 0.385 0.093 0 0.385 0.093 0 23 16.0 177.8 -1.534 -1.56 710 3580 2063
57 113.305323246 1785272167.7048843 110.0 robust tracking True 1.22 2.83 1.8154776515 1.7886176858 11.1 42.9 38.4 6 5 1.51 684721.855 684720.397 110.396 -84.747 True 0.05 0.197 0.218 0 0.197 0.218 0 23 32.0 163.81 -1.536 -1.56 712 3590 2063
58 115.30411121 1785272169.7036724 112.0 robust tracking True 1.22 2.83 1.8154394251 1.7881082833 22.64 43.1 38.0 6 5 -3.4 684547.463 684550.752 110.44 -85.241 True 0.05 0.429 0.029 0 0.429 0.029 0 23 8.0 152.36 -1.538 -1.56 714 3600 2063
59 117.303101565 1785272171.702663 114.0 robust tracking True 1.22 2.83 1.8154034756 1.7875997643 11.52 42.6 38.1 6 5 17.65 684398.444 684381.346 110.707 -84.967 True 0.05 0.405 0.059 0 0.405 0.059 0 23 24.0 192.86 -1.532 -1.56 716 3610 2063
60 119.306150448 1785272173.7057114 116.0 robust tracking True 1.22 2.83 1.8153676614 1.7870927007 12.04 42.5 37.5 6 5 -7.99 684203.354 684211.094 110.164 -85.459 True 0.05 0.631 0.155 0 0.631 0.155 0 23 32.0 144.3 -1.539 -1.56 718 3620 2063
61 121.300962576 1785272175.7005236 118.0 robust tracking True 1.22 2.83 1.8153303586 1.786584068 18.38 42.4 37.2 6 5 15.28 684055.675 684040.872 110.141 -86.063 True 0.05 0.481 0.133 0 0.481 0.133 0 23 48.0 200.68 -1.53 -1.56 720 3630 2063
62 123.303755452 1785272177.7033165 120.0 robust tracking True 1.22 2.82 1.815294335 1.7860767164 19.1 42.8 38.8 6 5 2.02 683871.81 683869.856 111.056 -85.022 True 0.05 1.151 0.085 0 1.151 0.085 0 23 64.0 169.36 -1.535 -1.56 722 3640 2063
63 125.304219262 1785272179.703781 122.0 robust tracking True 1.22 2.82 1.8152578155 1.785570688 11.11 42.7 38.6 6 5 -8.82 683688.289 683696.831 110.802 -85.693 True 0.05 0.637 0.051 0 0.637 0.051 0 23 8.0 158.16 -1.537 -1.56 724 3650 2063
64 127.304583545 1785272181.704145 124.0 robust tracking True 1.22 2.82 1.8152215994 1.7850628719 11.13 42.7 38.8 6 5 -1.48 683523.082 683524.519 110.235 -86.917 True 0.05 0.775 0.068 0 0.775 0.068 0 23 40.0 151.87 -1.538 -1.56 726 3660 2063
65 129.508905566 1785272183.9084663 126.0 robust tracking True 1.22 2.82 1.8151838123 1.7845085023 12.03 43.1 38.6 6 5 9.01 683342.021 683333.296 110.201 -87.019 True 0.05 0.331 0.094 0 0.331 0.094 0 23 0.0 157.45 -1.537 -1.56 728 3670 2063
66 131.500762179 1785272185.9003232 128.0 robust tracking True 1.22 2.82 1.8151487619 1.7840006032 12.83 42.6 38.6 6 5 3.63 683160.888 683157.371 110.357 -86.937 True 0.05 0.389 0.075 0 0.389 0.075 0 23 32.0 171.39 -1.535 -1.56 730 3680 2063
67 133.504344505 1785272187.9039054 130.0 robust tracking True 1.22 2.82 1.8151127862 1.7834955606 12.64 42.7 38.5 6 5 3.69 682986.797 682983.217 110.053 -87.63 True 0.05 0.791 0.01 0 0.791 0.01 0 23 0.0 175.65 -1.534 -1.56 732 3690 2063
68 135.509832366 1785272189.9094036 132.0 robust tracking True 1.22 2.82 1.8150762158 1.7829890753 11.13 42.7 38.4 6 5 5.1 682813.848 682808.909 110.531 -87.346 True 0.05 0.079 0.034 0 0.079 0.034 0 23 16.0 162.81 -1.536 -1.56 734 3700 2063
69 137.506720578 1785272191.9062817 134.0 robust tracking True 1.22 2.82 1.8150397064 1.7824837171 10.99 42.8 39.5 6 5 1.42 682635.318 682633.946 110.584 -87.635 True 0.05 0.038 0.097 0 0.038 0.097 0 23 32.0 155.09 -1.537 -1.56 736 3710 2063
70 139.504410571 1785272193.9039717 136.0 robust tracking True 1.22 2.82 1.8150041072 1.7819780719 11.15 42.7 38.1 6 5 -6.72 682453.853 682460.364 110.886 -87.452 True 0.05 0.412 0.407 0 0.412 0.407 0 23 48.0 139.96 -1.539 -1.56 738 3720 2063
71 141.503078598 1785272195.9026399 138.0 robust tracking True 1.22 2.82 1.8149699154 1.781475266 11.11 42.7 38.8 6 5 11.45 682296.086 682284.991 109.638 -89.46 True 0.05 1.419 0.005 0 1.419 0.005 0 23 40.0 167.17 -1.535 -1.56 740 3730 2063
72 143.500493916 1785272197.900055 140.0 robust tracking True 1.22 2.82 1.8149354252 1.7809734693 11.02 42.9 38.7 6 5 12.89 682120.284 682107.799 110.911 -87.736 True 0.05 0.88 0.067 0 0.88 0.067 0 23 56.0 162.67 -1.536 -1.56 742 3740 2063
73 145.512927625 1785272199.9124885 142.0 robust tracking True 1.22 2.82 1.8149005029 1.7804693538 10.96 42.9 39.4 6 5 -25.89 681906.117 681931.202 110.881 -87.866 True 0.05 0.775 0.078 0 0.775 0.078 0 23 8.0 107.83 -1.544 -1.56 744 3750 2063
74 147.506278297 1785272201.9058392 144.0 robust tracking True 1.22 2.82 1.8148661739 1.7799667491 10.98 43.0 39.0 6 5 -5.66 681746.195 681751.678 109.788 -89.613 True 0.05 1.009 0.035 0 1.009 0.035 0 23 56.0 152.27 -1.538 -1.56 746 3760 2063
75 149.505513865 1785272203.9050748 146.0 robust tracking True 1.22 2.82 1.8148312963 1.779461219 11.25 42.4 38.4 6 5 -7.95 681563.852 681571.554 110.854 -89.368 True 0.05 0.482 0.099 0 0.482 0.099 0 23 56.0 142.08 -1.539 -1.56 748 3770 2063
76 151.504103133 1785272205.903664 148.0 robust tracking True 1.22 2.82 1.8147976667 1.778959756 10.98 43.0 39.4 6 5 11.06 681403.38 681392.662 110.425 -89.358 True 0.05 0.199 0.058 0 0.199 0.058 0 23 16.0 161.29 -1.536 -1.56 750 3780 2063
77 153.502847324 1785272207.9024084 150.0 robust tracking True 1.22 2.82 1.8147608671 1.7784571849 15.23 42.6 38.9 6 5 9.66 681223.637 681214.276 110.279 -89.721 True 0.05 0.371 0.218 0 0.371 0.218 0 23 48.0 172.12 -1.535 -1.56 752 3790 2063
78 155.506297511 1785272209.9058583 152.0 robust tracking True 1.22 2.82 1.8147266277 1.7779565566 11.07 42.6 38.7 6 5 0.35 681034.417 681034.073 110.263 -89.772 True 0.05 0.371 0.13 0 0.371 0.13 0 23 56.0 180.04 -1.533 -1.56 754 3800 2063
79 157.500753602 1785272211.9003143 154.0 robust tracking True 1.22 2.82 1.814692541 1.777454151 11.13 42.4 38.4 6 5 8.94 680862.299 680853.639 110.737 -89.433 True 0.05 0.755 0.053 0 0.755 0.053 0 23 0.0 180.5 -1.533 -1.56 756 3810 2063
80 159.504450759 1785272213.904012 156.0 robust tracking True 1.22 2.82 1.8146585955 1.7769539906 11.22 42.3 38.2 6 5 14.47 680691.31 680677.294 110.442 -90.141 True 0.05 0.405 0.02 0 0.405 0.02 0 23 32.0 196.03 -1.531 -1.56 758 3820 2063
81 161.510423884 1785272215.9099848 158.0 robust tracking True 1.21 2.82 1.8146253251 1.7764523761 14.07 42.3 38.6 6 5 14.17 680511.698 680497.972 110.887 -89.977 True 0.05 0.551 0.182 0 0.551 0.182 0 23 32.0 184.51 -1.532 -1.56 760 3830 2063
82 163.504960681 1785272217.9045217 160.0 robust tracking True 1.21 2.81 1.8145907816 1.7759535814 10.99 42.6 39.7 6 5 9.57 680326.822 680317.554 110.339 -91.079 True 0.05 0.465 0.01 0 0.465 0.01 0 23 8.0 150.36 -1.538 -1.56 762 3840 2063
83 165.507220868 1785272219.9067822 162.0 robust tracking True 1.21 2.81 1.8145551226 1.7754518476 10.98 42.8 38.9 6 5 5.13 680143.185 680138.216 109.975 -91.641 True 0.05 0.834 0.034 0 0.834 0.034 0 23 0.0 144.58 -1.539 -1.56 764 3850 2063
84 167.503525545 1785272221.9030867 164.0 robust tracking True 1.21 2.81 1.8145228906 1.7749543943 14.36 42.8 38.7 6 5 4.72 679959.278 679954.706 110.179 -91.65 True 0.05 0.706 0.085 0 0.706 0.085 0 23 48.0 124.01 -1.542 -1.56 766 3860 2063
85 169.50061802 1785272223.900179 166.0 robust tracking True 1.21 2.81 1.8144898435 1.7744531221 11.09 42.8 37.7 6 5 13.57 679787.48 679774.331 110.775 -91.606 True 0.05 1.106 0.075 0 1.106 0.075 0 23 56.0 145.18 -1.538 -1.56 768 3870 2063
86 171.510388267 1785272225.9099493 168.0 robust tracking True 1.21 2.81 1.8144559568 1.7739550407 11.08 42.6 38.2 6 5 8.28 679599.18 679591.156 110.344 -91.505 True 0.05 0.285 0.062 0 0.285 0.062 0 23 56.0 137.18 -1.54 -1.56 770 3880 2063
87 173.500668495 1785272227.9002295 170.0 robust tracking True 1.21 2.81 1.8144213808 1.7734561125 11.07 42.5 38.0 6 5 -7.05 679401.0 679407.83 110.355 -91.896 True 0.05 0.239 0.023 0 0.239 0.023 0 23 48.0 151.22 -1.538 -1.56 772 3890 2063
88 175.509072834 1785272229.9086337 172.0 robust tracking True 1.21 2.81 1.8143875329 1.7729571416 14.88 42.6 38.0 6 5 -9.75 679214.759 679224.203 110.051 -92.203 True 0.05 0.388 0.024 0 0.388 0.024 0 23 16.0 127.04 -1.541 -1.56 774 3900 2063
89 177.511375431 1785272231.9109368 174.0 robust tracking True 1.21 2.81 1.8143541178 1.7724598606 11.01 42.7 39.1 6 5 -28.57 679010.25 679037.93 110.131 -92.548 True 0.05 0.679 0.066 0 0.679 0.066 0 23 16.0 144.09 -1.539 -1.56 776 3910 2063
90 179.5031023 1785272233.9026635 176.0 robust tracking True 1.21 2.81 1.8143216536 1.7719632746 11.04 42.6 38.1 6 5 -1.94 678850.304 678852.181 110.443 -92.232 True 0.05 0.439 0.164 0 0.439 0.164 0 23 24.0 156.61 -1.537 -1.56 778 3920 2063
91 181.500919559 1785272235.9004805 178.0 robust tracking True 1.21 2.81 1.8142885244 1.7714647145 11.67 42.7 39.6 6 5 14.37 678680.998 678667.079 110.223 -93.101 True 0.05 0.927 0.048 0 0.927 0.048 0 23 24.0 174.87 -1.534 -1.56 780 3930 2063
92 183.504154069 1785272237.903716 180.0 robust tracking True 1.21 2.81 1.8142515963 1.7709676955 11.12 42.4 38.5 6 5 3.52 678484.103 678480.688 110.013 -93.55 True 0.05 0.69 0.056 0 0.69 0.056 0 23 0.0 167.43 -1.535 -1.56 782 3940 2063
93 185.509701079 1785272239.9092622 182.0 robust tracking True 1.21 2.81 1.8142225906 1.7704719824 11.12 42.5 38.1 6 5 2.8 678298.275 678295.56 110.36 -92.989 True 0.05 0.188 0.156 0 0.188 0.156 0 23 0.0 175.14 -1.534 -1.56 784 3950 2063
94 187.511311613 1785272241.9108725 184.0 robust tracking True 1.21 2.81 1.8141898144 1.7699731912 11.11 42.3 38.5 6 5 -16.24 678094.632 678110.363 110.845 -93.036 True 0.05 0.46 0.009 0 0.46 0.009 0 23 32.0 150.91 -1.537 -1.56 786 3960 2063
95 189.504094586 1785272243.9036555 186.0 robust tracking True 1.21 2.81 1.8141560815 1.7694787336 11.13 42.5 37.9 6 5 -1.22 677924.284 677925.468 110.012 -93.838 True 0.05 0.542 0.031 0 0.542 0.031 0 23 32.0 167.75 -1.535 -1.56 788 3970 2063
96 191.503264307 1785272245.902825 188.0 robust tracking True 1.21 2.81 1.8141223587 1.7689845528 11.12 42.4 38.3 6 5 12.44 677751.751 677739.703 110.673 -93.502 True 0.05 0.691 0.123 0 0.691 0.123 0 23 32.0 181.14 -1.533 -1.56 790 3980 2063
97 193.500850983 1785272247.9004118 190.0 robust tracking True 1.21 2.81 1.8140919959 1.768488668 14.03 42.3 38.2 6 5 8.52 677559.555 677551.298 110.495 -93.954 True 0.05 0.376 0.004 0 0.376 0.004 0 23 56.0 155.05 -1.537 -1.56 792 3990 2063
98 195.507436225 1785272249.9069972 192.0 robust tracking True 1.21 2.81 1.8140572992 1.7679932608 10.97 42.9 38.9 6 5 -25.61 677336.289 677361.101 110.053 -94.87 True 0.05 0.565 0.132 0 0.565 0.132 0 23 0.0 143.1 -1.539 -1.56 794 4000 2063
99 197.710664279 1785272252.1102252 194.0 robust tracking True 1.21 2.81 1.8140224199 1.7674512561 11.06 42.7 38.3 6 5 -21.21 677131.244 677151.796 110.436 -94.668 True 0.05 0.467 0.099 0 0.467 0.099 0 23 16.0 127.66 -1.541 -1.56 797 4010 2063
100 199.904494084 1785272254.3040552 196.0 robust tracking True 1.21 2.81 1.8139878446 1.7669062479 11.21 42.4 37.9 6 5 2.5 676945.184 676942.759 110.719 -94.592 True 0.05 0.329 0.254 0 0.329 0.254 0 23 0.0 166.66 -1.535 -1.56 799 4020 2063
101 201.902748265 1785272256.3023093 198.0 robust tracking True 1.21 2.8 1.8139551469 1.7664122598 19.71 42.7 38.2 6 5 3.44 676756.483 676753.152 110.284 -95.508 True 0.05 0.834 0.075 0 0.834 0.075 0 23 48.0 159.1 -1.536 -1.56 801 4030 2063
102 203.900860006 1785272258.3004212 200.0 robust tracking True 1.21 2.8 1.8139243484 1.7659203155 21.27 42.6 37.9 6 5 16.7 676577.254 676561.076 111.005 -94.638 True 0.05 0.65 0.026 0 0.65 0.026 0 23 40.0 185.48 -1.532 -1.56 803 4040 2063
103 205.904311168 1785272260.303872 202.0 robust tracking True 1.21 2.8 1.8138926701 1.7654272361 11.23 42.3 37.5 6 5 -4.45 676364.571 676368.878 111.007 -94.853 True 0.05 0.681 0.154 0 0.681 0.154 0 23 32.0 146.48 -1.538 -1.56 805 4050 2063
104 207.911054786 1785272262.3106158 204.0 robust tracking True 1.21 2.8 1.8138607249 1.7649342967 11.29 42.2 37.1 6 5 -0.42 676178.038 676178.445 111.268 -95.121 True 0.05 0.669 0.187 0 0.669 0.187 0 23 56.0 148.37 -1.538 -1.56 807 4060 2063
105 209.904852911 1785272264.304414 206.0 robust tracking True 1.21 2.8 1.8138277745 1.7644418155 17.32 42.6 38.4 6 5 5.29 675993.222 675988.099 110.799 -95.578 True 0.05 0.772 0.012 0 0.772 0.012 0 23 48.0 167.53 -1.535 -1.56 809 4070 2063
106 211.904389957 1785272266.3039508 208.0 robust tracking True 1.2 2.8 1.8137949136 1.7639490637 14.88 42.5 38.1 6 5 4.76 675802.554 675797.942 109.97 -97.009 True 0.05 1.186 0.157 0 1.186 0.157 0 23 16.0 161.37 -1.536 -1.56 811 4080 2063
107 213.908887407 1785272268.3084483 210.0 robust tracking True 1.2 2.8 1.8137652527 1.7634585771 13.65 41.9 38.5 6 5 -19.97 675583.7 675603.041 110.734 -96.484 True 0.05 0.11 0.069 0 0.11 0.069 0 23 8.0 134.36 -1.54 -1.56 813 4090 2063
108 215.900878981 1785272270.3004403 212.0 robust tracking True 1.2 2.8 1.8137346862 1.7629700707 13.14 42.4 39.0 6 5 1.39 675411.049 675409.707 110.477 -96.41 True 0.05 0.489 0.149 0 0.489 0.149 0 23 48.0 164.12 -1.535 -1.56 815 4100 2063
109 217.908715503 1785272272.3082767 214.0 robust tracking True 1.2 2.8 1.8137007679 1.7624770049 11.19 42.1 38.1 6 5 -3.51 675214.305 675217.703 110.227 -97.513 True 0.05 0.667 0.022 0 0.667 0.022 0 23 56.0 141.12 -1.539 -1.56 817 4110 2063
110 219.904589627 1785272274.3041506 216.0 robust tracking True 1.2 2.8 1.8136705307 1.7619846326 17.3 42.2 38.8 6 5 -14.02 675007.687 675021.273 110.312 -97.218 True 0.05 0.452 0.069 0 0.452 0.069 0 23 24.0 158.28 -1.536 -1.56 819 4120 2063
111 221.9039367 1785272276.3034978 218.0 robust tracking True 1.2 2.8 1.8136409999 1.7614933749 11.12 42.1 39.2 6 5 -3.83 674821.953 674825.66 111.06 -96.371 True 0.05 1.326 0.028 0 1.326 0.028 0 23 48.0 170.5 -1.534 -1.56 821 4130 2063
112 223.91008895 1785272278.3096502 220.0 robust tracking True 1.2 2.8 1.8136105372 1.7610043295 11.73 42.3 38.7 6 5 0.13 674630.412 674630.288 111.079 -97.301 True 0.05 0.408 0.148 0 0.408 0.148 0 23 56.0 157.28 -1.536 -1.56 823 4140 2063
113 225.902916967 1785272280.302478 222.0 robust tracking True 1.2 2.8 1.8135794516 1.7605142092 21.26 41.9 39.1 6 5 -21.67 674413.179 674434.176 111.065 -97.127 True 0.05 0.752 0.068 0 0.752 0.068 0 23 48.0 124.73 -1.541 -1.56 825 4150 2063
114 227.901755726 1785272282.3013165 224.0 robust tracking True 1.2 2.8 1.8135490001 1.7600248185 11.02 42.5 39.3 6 5 13.8 674252.975 674239.611 110.715 -97.719 True 0.05 0.748 0.078 0 0.748 0.078 0 23 32.0 159.45 -1.536 -1.56 827 4160 2063
115 229.901619429 1785272284.3011806 226.0 robust tracking True 1.2 2.8 1.8135177823 1.7595391681 11.05 42.4 39.1 6 5 12.06 674053.956 674042.275 110.355 -98.939 True 0.05 0.78 0.043 0 0.78 0.043 0 23 56.0 170.95 -1.534 -1.56 829 4170 2063
116 231.90481137 1785272286.3043725 228.0 robust tracking True 1.2 2.8 1.8134877896 1.7590492494 11.09 42.4 38.1 6 5 12.72 673856.473 673844.146 110.721 -98.388 True 0.05 0.538 0.188 0 0.538 0.188 0 23 48.0 170.54 -1.534 -1.56 831 4180 2063
117 233.905326912 1785272288.3048878 230.0 robust tracking True 1.2 2.8 1.8134570418 1.7585604429 11.06 42.5 38.9 6 5 5.44 673653.112 673647.84 110.098 -98.963 True 0.05 0.301 0.311 0 0.301 0.311 0 23 8.0 171.59 -1.534 -1.56 833 4190 2063
118 235.904458614 1785272290.3040195 232.0 robust tracking True 1.2 2.8 1.8134261204 1.7580735849 11.06 42.5 38.5 6 5 -4.62 673445.637 673450.117 110.459 -98.844 True 0.05 0.829 0.028 0 0.829 0.028 0 23 0.0 153.73 -1.537 -1.56 835 4200 2063
119 237.904071483 1785272292.3036325 234.0 robust tracking True 1.2 2.8 1.8133959704 1.7575868747 12.8 42.0 37.7 6 5 -7.87 673243.852 673251.478 110.772 -98.845 True 0.05 0.559 0.037 0 0.559 0.037 0 23 0.0 146.96 -1.538 -1.56 837 4210 2063
120 239.908760739 1785272294.3083217 236.0 robust tracking True 1.2 2.8 1.8133666091 1.7570976374 11.1 42.3 38.6 6 5 4.99 673058.134 673053.301 110.565 -99.434 True 0.05 0.506 0.016 0 0.506 0.016 0 23 24.0 168.65 -1.535 -1.56 839 4220 2063
121 241.900563008 1785272296.300124 238.0 robust tracking True 1.2 2.79 1.8133344617 1.7566107514 11.14 42.3 38.4 6 5 -14.57 672839.788 672853.907 110.365 -99.522 True 0.05 0.253 0.141 0 0.253 0.141 0 23 0.0 147.65 -1.538 -1.56 841 4230 2063
122 243.904645899 1785272298.3042068 240.0 robust tracking True 1.2 2.79 1.8133057846 1.7561227378 11.1 42.2 38.8 6 5 3.18 672658.12 672655.039 109.903 -100.274 True 0.05 0.476 0.033 0 0.476 0.033 0 23 32.0 164.64 -1.535 -1.56 843 4240 2063
123 245.904621193 1785272300.3041825 242.0 robust tracking True 1.2 2.79 1.8132777969 1.7556370904 11.14 42.2 38.7 6 5 -0.81 672454.836 672455.618 110.573 -100.187 True 0.05 0.405 0.013 0 0.405 0.013 0 23 16.0 134.41 -1.54 -1.56 845 4250 2063
124 247.899436713 1785272302.2989979 244.0 robust tracking True 1.2 2.79 1.8132463791 1.7551512789 11.16 42.2 39.0 6 5 13.48 672269.138 672256.082 110.183 -100.179 True 0.05 0.565 0.113 0 0.565 0.113 0 23 40.0 169.04 -1.535 -1.56 847 4260 2063
125 249.911369626 1785272304.3109307 246.0 robust tracking True 1.2 2.79 1.813217727 1.754665179 11.11 42.5 39.2 6 5 -6.09 672049.638 672055.536 109.774 -100.683 True 0.05 0.486 0.021 0 0.486 0.021 0 23 0.0 135.93 -1.539 -1.56 849 4270 2063
126 251.903384606 1785272306.3029459 248.0 robust tracking True 1.2 2.79 1.8131881994 1.7541830169 11.18 42.2 39.0 6 5 10.96 671865.57 671854.952 109.778 -101.09 True 0.05 0.278 0.027 0 0.278 0.027 0 23 48.0 182.36 -1.532 -1.56 851 4280 2063
127 253.900781619 1785272308.3003423 250.0 robust tracking True 1.2 2.79 1.8131589097 1.7536951988 11.17 42.3 38.9 6 5 3.55 671659.189 671655.749 110.155 -100.561 True 0.05 0.584 0.008 0 0.584 0.008 0 23 16.0 141.85 -1.539 -1.56 853 4290 2063
128 255.910281973 1785272310.3098433 252.0 robust tracking True 1.2 2.79 1.8131293194 1.753212297 11.2 42.1 38.4 6 5 7.57 671462.581 671455.25 109.95 -101.447 True 0.05 0.189 0.096 0 0.189 0.096 0 23 16.0 141.76 -1.539 -1.56 855 4300 2063
129 257.904754461 1785272312.3043153 254.0 robust tracking True 1.2 2.79 1.8131029159 1.7527265372 11.15 42.4 38.7 6 5 -6.95 671245.456 671252.194 110.05 -101.621 True 0.05 0.518 0.084 0 0.518 0.084 0 23 56.0 134.53 -1.54 -1.56 857 4310 2063
130 259.904898749 1785272314.3044596 256.0 robust tracking True 1.2 2.79 1.8130720433 1.7522445406 11.3 41.8 38.4 6 5 -4.74 671044.584 671049.173 110.162 -101.624 True 0.05 0.281 0.225 0 0.281 0.225 0 23 0.0 142.99 -1.539 -1.56 859 4320 2063
131 261.904207465 1785272316.3037684 258.0 robust tracking True 1.19 2.79 1.8130440645 1.7517582107 11.31 42.0 38.4 6 5 -7.29 670837.352 670844.416 110.701 -101.27 True 0.05 0.844 0.152 0 0.844 0.152 0 23 56.0 167.54 -1.535 -1.56 861 4330 2063
132 263.903658409 1785272318.3032198 260.0 robust tracking True 1.19 2.79 1.8130134772 1.7512773631 11.34 42.0 38.5 6 5 -1.16 670637.855 670638.976 110.001 -102.901 True 0.05 0.697 0.02 0 0.697 0.02 0 23 48.0 166.85 -1.535 -1.56 863 4340 2063
133 265.906678701 1785272320.3062396 262.0 robust tracking True 1.19 2.79 1.8129886444 1.7507938348 14.74 42.5 38.9 6 5 10.12 670442.306 670432.505 110.568 -101.927 True 0.05 0.908 0.068 0 0.908 0.068 0 23 40.0 192.58 -1.531 -1.56 865 4350 2063
134 267.908740259 1785272322.308301 264.0 robust tracking True 1.19 2.79 1.8129614176 1.750311896 11.27 42.3 37.7 6 5 -11.12 670213.856 670224.632 110.96 -102.072 True 0.05 0.648 0.323 0 0.648 0.323 0 23 56.0 163.77 -1.535 -1.56 867 4360 2063
135 269.904666955 1785272324.304228 266.0 robust tracking True 1.19 2.79 1.8129315339 1.7498285268 11.35 41.9 37.9 6 5 0.23 670016.459 670016.233 110.584 -102.496 True 0.05 0.457 0.073 0 0.457 0.073 0 23 48.0 200.12 -1.53 -1.56 869 4370 2063
136 271.904417064 1785272326.303978 268.0 robust tracking True 1.19 2.79 1.8129033974 1.7493462127 11.29 42.0 38.2 6 5 -11.63 669798.53 669809.801 110.923 -102.202 True 0.05 0.955 0.178 0 0.955 0.178 0 23 16.0 154.9 -1.537 -1.56 871 4380 2063
137 273.904287144 1785272328.3038483 270.0 robust tracking True 1.19 2.79 1.8128759146 1.7488645046 11.24 42.2 38.4 6 5 0.32 669604.755 669604.44 110.23 -103.356 True 0.05 0.559 0.043 0 0.559 0.043 0 23 56.0 147.74 -1.538 -1.56 873 4390 2063
138 275.905752343 1785272330.3053133 272.0 robust tracking True 1.19 2.79 1.8128467845 1.7483871309 11.29 42.1 38.0 6 5 0.52 669397.133 669396.627 110.767 -103.125 True 0.05 0.839 0.033 0 0.839 0.033 0 23 48.0 175.42 -1.533 -1.56 875 4400 2063
139 277.901721957 1785272332.3012831 274.0 robust tracking True 1.19 2.79 1.8128179501 1.7479069779 11.23 42.2 38.8 6 5 -15.46 669172.771 669187.748 110.557 -103.518 True 0.05 0.505 0.152 0 0.505 0.152 0 23 8.0 141.55 -1.539 -1.56 877 4410 2063
140 279.901270286 1785272334.3008313 276.0 robust tracking True 1.19 2.79 1.8127918148 1.7474239577 11.26 42.2 38.9 6 5 5.21 668986.416 668981.367 110.705 -103.667 True 0.05 0.415 0.13 0 0.415 0.13 0 23 56.0 164.7 -1.535 -1.56 879 4420 2063
141 281.911805396 1785272336.3113663 278.0 robust tracking True 1.19 2.78 1.812764459 1.7469430861 11.14 42.3 38.6 6 5 9.2 668783.357 668774.443 110.765 -103.653 True 0.05 0.976 0.047 0 0.976 0.047 0 23 40.0 167.02 -1.535 -1.56 881 4430 2063
142 283.904627638 1785272338.3041887 280.0 robust tracking True 1.19 2.78 1.8127364977 1.7464643722 11.13 42.4 38.9 6 5 1.9 668567.589 668565.747 110.702 -104.554 True 0.05 0.496 0.084 0 0.496 0.084 0 23 56.0 150.73 -1.537 -1.56 883 4440 2063
143 285.904540282 1785272340.3041015 282.0 robust tracking True 1.19 2.78 1.8127111868 1.7459831701 11.26 42.2 38.2 6 5 -21.52 668336.38 668357.228 110.484 -104.5 True 0.05 0.4 0.023 0 0.4 0.023 0 23 16.0 127.92 -1.541 -1.56 885 4450 2063
144 287.903906607 1785272342.3034678 284.0 robust tracking True 1.19 2.78 1.8126838601 1.7455066962 11.09 42.3 39.2 6 5 -0.35 668145.403 668145.742 110.548 -104.863 True 0.05 0.13 0.213 0 0.13 0.213 0 23 0.0 156.5 -1.536 -1.56 887 4460 2063
145 289.903519609 1785272344.3030806 286.0 robust tracking True 1.19 2.78 1.8126545436 1.74502695 11.28 42.2 38.0 6 5 -17.14 667916.867 667933.474 110.923 -105.07 True 0.05 0.412 0.112 0 0.412 0.112 0 23 8.0 145.17 -1.538 -1.56 889 4470 2063
146 291.906994829 1785272346.3065557 288.0 robust tracking True 1.19 2.78 1.812627295 1.7445482935 12.84 42.5 38.6 6 5 -1.31 667721.551 667722.816 109.629 -106.134 True 0.05 0.721 0.164 0 0.721 0.164 0 23 40.0 158.89 -1.536 -1.56 891 4480 2063
147 293.904251457 1785272348.3038123 290.0 robust tracking True 1.19 2.78 1.8126009474 1.7440682755 11.16 42.3 38.2 6 5 14.39 667524.719 667510.777 110.381 -105.665 True 0.05 0.161 0.079 0 0.161 0.079 0 23 32.0 184.17 -1.532 -1.56 893 4490 2063
148 295.904732818 1785272350.3042939 292.0 robust tracking True 1.19 2.78 1.8125748376 1.7435915248 11.06 42.6 38.1 6 5 -9.72 667289.655 667299.068 110.248 -105.873 True 0.05 0.291 0.005 0 0.291 0.005 0 23 32.0 159.99 -1.536 -1.56 895 4500 2063
149 297.910797167 1785272352.3103583 294.0 robust tracking True 1.19 2.78 1.8125472744 1.7431147283 11.0 42.9 38.4 6 5 9.37 667097.136 667088.06 110.084 -106.181 True 0.05 0.237 0.074 0 0.237 0.074 0 23 16.0 168.11 -1.535 -1.56 897 4510 2063
150 299.905108443 1785272354.3046699 296.0 robust tracking True 1.19 2.78 1.812521259 1.7426375476 16.12 42.5 37.8 6 5 -12.01 666863.837 666875.472 110.096 -106.613 True 0.05 0.693 0.156 0 0.693 0.156 0 23 64.0 124.56 -1.541 -1.56 899 4520 2063
@@ -0,0 +1,52 @@
# Genesys2 live PPS telemetry replay
Source: `pps_end_to_end_validation/hardware_logs/genesys2_pps_20260728.csv`
## Direct firmware/RTL observations
- Samples/duration: 149 / 296.0 s.
- Status interval: 2.000 s; median sequence step 10 (2.0 s of 200 ms solves).
- Profiles: robust; locked fraction 1.000; transitions 0; reference SVIDs [23].
- TDOP/GDOP medians: 1.220 / 2.820; clock sigma median 11.24 m.
- C/N0 mean/min medians: 42.6 / 38.4 dB-Hz; tracked/used medians 6/5.
- WLS innovation RMS/p95-absolute: 10.47 / 19.66 m.
- Raw/filtered bias slopes: -90.020 / -90.025 m/s; PPS velocity residual median 0.079 m/s.
- Internal hardware phase mean: 155.37 ns; median-centered RMS/p95/max/p-p: 17.92 / 31.26 / 47.95 / 93.21 ns.
- Ten-second averaged phase wander p-p: 46.16 ns.
- Sampled phase-step RMS/p95/max: 22.84 / 44.13 / 56.38 ns. These are at the status interval, not consecutive physical 1 PPS edges.
- Rate command median/span: -1.5370 / 0.0140 ppm; integrator median -1.5600 ppm.
- Fixed-point PI rate reproduction RMS/p95/max: 0.430 / 0.852 / 0.921 ppb. This is the difference between logged `rate_cmd` and `integrator + Kp*hw_phase`; the remaining error is dominated by three-decimal UART rounding.
- Phase correlation with rate command / WLS innovation: 0.988 / 0.656.
- A constant oscillator inferred from zero net frequency is 1.536779 ppm. Integrating the logged command under that assumption gives centered phase RMS/p-p 23.65 / 92.48 ns.
`hw_phase` is the RTL PI residual captured over APB. It is not a trusted-generator time-interval measurement of the output pin, so these values cannot certify physical PPS jitter or absolute offset.
## Telemetry-conditioned end-to-end loop replay
The replay imports the first locked phase/rate/integrator and software filter snapshot instead of incorrectly resetting the board to an acquisition state. It uses raw/filtered clock bias, Doppler datum drift, TDOP/GDOP, clock sigma, C/N0, satellite count, FDE, reference SVID and the production fixed-point coefficients. The reference trajectory is reconstructed from filtered bias, the logged PI residual and the integrated rate command.
- Expanded updates: 1481; interpolation factor 9.94.
- Snapshot starts locked at 0.00 s; post-warmup locked fraction 1.000; unlocks 0.
- Constant-oscillator convention: 1.536779 ppm.
- Modeled physical-grid edge centered RMS/p95/p-p: 41.32 / 80.00 / 168.00 ns.
- Modeled one-second period RMS/p95/max: 5.51 / 8.00 / 16.00 ns.
- Modeled PI residual centered RMS/p95/p-p: 21.47 / 46.79 / 135.63 ns.
- Logged-epoch PI residual reproduction RMS/p95 error: 18.07 / 44.30 ns.
- Logged-epoch filtered-bias reproduction RMS/p95: 9.87 / 22.13 m.
## Tuning sensitivity on the same trace
| tuning | edge RMS (ns) | edge p-p (ns) | period max (ns) | residual reproduction RMS (ns) | unlocks |
|---|---:|---:|---:|---:|---:|
| current | 41.32 | 168.00 | 16.00 | 18.07 | 0 |
| robust_bias_1_64 | 57.06 | 216.00 | 16.00 | 24.53 | 0 |
| drift_gain_1_2 | 42.23 | 168.00 | 16.00 | 18.03 | 0 |
These counterfactuals reuse a reference trajectory reconstructed from the current filter, and nine of every ten solve epochs are interpolated. They are useful as a rejection test, not as evidence to deploy new gains. On this capture neither `robust_bias_1_64` nor `drift_gain_1_2` improves the current loop's absolute wander.
## Limits and next measurement
- Missing status epochs are linearly interpolated when sequence step is greater than one. This preserves low-frequency wander but understates unlogged 200 ms noise; it is not an exact firmware replay.
- The inferred oscillator and active phase assume one constant crystal frequency chosen to make net phase slope zero over this capture. Temperature telemetry was unavailable, so slow crystal motion is not separable from the reconstructed reference.
- Level-1 telemetry does not expose ABS_SAMPLE, REM_CODE_PHASE, full capture counter, chi-square ratio, pseudorange residual vector, board temperature, or a trusted PPS edge timestamp. The bit/subframe part of the end-to-end model remains synthetic for those fields.
- Set `GPS_PPS_STATUS_LOG_INTERVAL=1` for exact 200 ms input cadence. For a max-20 ns pin claim, capture DUT and trusted PPS with a common-clock TIC/scope after cable/output-delay calibration.
@@ -0,0 +1,25 @@
scenario,tuning,runs,lock_p95_s,absolute_rms_median_ns,absolute_max_p95_ns,absolute_peak_to_peak_median_ns,period_rms_median_ns,period_p95_median_ns,period_max_worst_ns,wander_10s_peak_to_peak_median_ns,profile_transitions_total,unlocks_total
real_geometry_board_noise,current,24,10.770000000000001,10.881176406988356,32.0,48.00000000000001,3.867084378182096,8.0,8.000000000000002,36.8,0,0
real_geometry_board_noise,robust_noise_guard,24,10.770000000000001,10.881176406988356,32.0,48.00000000000001,3.867084378182096,8.0,8.000000000000002,36.8,0,0
real_geometry_board_noise,quiet_precision,24,10.770000000000001,6.311779001549132,16.000000000000004,24.0,2.7197398634103065,7.999999999999999,8.000000000000005,20.0,0,0
real_geometry_board_noise,fast_precision,24,10.770000000000001,11.482151620012546,40.0,56.0,4.4110037618184545,8.0,16.0,40.0,0,0
real_geometry_board_noise,drift_gain_1_2,24,10.770000000000001,9.830551108590587,32.0,40.0,3.761748764716249,8.0,8.000000000000002,33.6,0,0
real_geometry_board_noise,quality_scheduled_drift,24,10.770000000000001,9.830551108590587,32.0,40.0,3.761748764716249,8.0,8.000000000000002,33.6,0,0
scope_like_multipath,current,24,10.770000000000001,22.943340809908015,62.79999999999998,88.0,4.727734084565672,8.000000000000002,16.0,80.80000000000001,0,0
scope_like_multipath,robust_noise_guard,24,10.770000000000001,22.943340809908015,62.79999999999998,88.0,4.727734084565672,8.000000000000002,16.0,80.80000000000001,0,0
scope_like_multipath,quiet_precision,24,10.770000000000001,17.147532219212493,40.0,63.99999999999999,3.7617487647162484,8.000000000000004,8.000000000000005,58.4,0,0
scope_like_multipath,fast_precision,24,10.770000000000001,22.40714058089752,56.00000000000001,92.0,5.040539175116031,8.000000000000002,16.0,78.0,0,0
scope_like_multipath,drift_gain_1_2,24,10.8,21.354092092763203,62.79999999999998,88.0,4.465388951979694,8.0,16.0,74.80000000000001,0,0
scope_like_multipath,quality_scheduled_drift,24,10.8,21.354092092763203,62.79999999999998,88.0,4.465388951979694,8.0,16.0,74.80000000000001,0,0
quality_transitions,current,24,10.770000000000001,18.351896116449183,96.0,80.0,4.4110037618184545,8.0,23.99999999999999,73.20000000000002,72,0
quality_transitions,robust_noise_guard,24,10.770000000000001,17.481868755279223,80.0,80.0,4.374396022586886,8.0,16.00000000000001,67.6,72,0
quality_transitions,quiet_precision,24,10.770000000000001,10.467771592924619,56.0,47.99999999999999,3.0800577533580347,7.999999999999999,16.0,42.0,72,0
quality_transitions,fast_precision,24,10.770000000000001,19.145424296464835,88.00000000000001,88.0,4.861884148700504,8.0,24.000000000000004,73.6,72,0
quality_transitions,drift_gain_1_2,24,10.770000000000001,16.291766736261422,83.39999999999999,76.0,4.089452052592408,8.0,16.000000000000004,63.599999999999994,72,0
quality_transitions,quality_scheduled_drift,24,10.770000000000001,16.766578539927025,83.39999999999999,80.0,4.224793965697071,8.0,16.00000000000001,67.6,72,0
thermal_0p05ppm,current,24,10.770000000000001,39.2873108676108,102.79999999999998,136.0,5.727479384554999,8.000000000000005,24.0,124.0,0,0
thermal_0p05ppm,robust_noise_guard,24,10.770000000000001,39.2873108676108,102.79999999999998,136.0,5.727479384554999,8.000000000000005,24.0,124.0,0,0
thermal_0p05ppm,quiet_precision,24,10.770000000000001,210.25812707207118,416.0,648.0,13.000191801484194,24.000000000000004,32.0,632.4,0,0
thermal_0p05ppm,fast_precision,24,10.770000000000001,21.850277755888733,63.39999999999999,96.0,5.82476895848467,8.000000000000002,16.000000000000004,77.60000000000001,0,0
thermal_0p05ppm,drift_gain_1_2,24,10.770000000000001,38.40828815100511,96.0,136.0,5.656836393570529,8.000000000000005,16.000000000000004,122.0,0,0
thermal_0p05ppm,quality_scheduled_drift,24,10.770000000000001,38.40828815100511,96.0,136.0,5.656836393570529,8.000000000000005,16.000000000000004,122.0,0,0
1 scenario tuning runs lock_p95_s absolute_rms_median_ns absolute_max_p95_ns absolute_peak_to_peak_median_ns period_rms_median_ns period_p95_median_ns period_max_worst_ns wander_10s_peak_to_peak_median_ns profile_transitions_total unlocks_total
2 real_geometry_board_noise current 24 10.770000000000001 10.881176406988356 32.0 48.00000000000001 3.867084378182096 8.0 8.000000000000002 36.8 0 0
3 real_geometry_board_noise robust_noise_guard 24 10.770000000000001 10.881176406988356 32.0 48.00000000000001 3.867084378182096 8.0 8.000000000000002 36.8 0 0
4 real_geometry_board_noise quiet_precision 24 10.770000000000001 6.311779001549132 16.000000000000004 24.0 2.7197398634103065 7.999999999999999 8.000000000000005 20.0 0 0
5 real_geometry_board_noise fast_precision 24 10.770000000000001 11.482151620012546 40.0 56.0 4.4110037618184545 8.0 16.0 40.0 0 0
6 real_geometry_board_noise drift_gain_1_2 24 10.770000000000001 9.830551108590587 32.0 40.0 3.761748764716249 8.0 8.000000000000002 33.6 0 0
7 real_geometry_board_noise quality_scheduled_drift 24 10.770000000000001 9.830551108590587 32.0 40.0 3.761748764716249 8.0 8.000000000000002 33.6 0 0
8 scope_like_multipath current 24 10.770000000000001 22.943340809908015 62.79999999999998 88.0 4.727734084565672 8.000000000000002 16.0 80.80000000000001 0 0
9 scope_like_multipath robust_noise_guard 24 10.770000000000001 22.943340809908015 62.79999999999998 88.0 4.727734084565672 8.000000000000002 16.0 80.80000000000001 0 0
10 scope_like_multipath quiet_precision 24 10.770000000000001 17.147532219212493 40.0 63.99999999999999 3.7617487647162484 8.000000000000004 8.000000000000005 58.4 0 0
11 scope_like_multipath fast_precision 24 10.770000000000001 22.40714058089752 56.00000000000001 92.0 5.040539175116031 8.000000000000002 16.0 78.0 0 0
12 scope_like_multipath drift_gain_1_2 24 10.8 21.354092092763203 62.79999999999998 88.0 4.465388951979694 8.0 16.0 74.80000000000001 0 0
13 scope_like_multipath quality_scheduled_drift 24 10.8 21.354092092763203 62.79999999999998 88.0 4.465388951979694 8.0 16.0 74.80000000000001 0 0
14 quality_transitions current 24 10.770000000000001 18.351896116449183 96.0 80.0 4.4110037618184545 8.0 23.99999999999999 73.20000000000002 72 0
15 quality_transitions robust_noise_guard 24 10.770000000000001 17.481868755279223 80.0 80.0 4.374396022586886 8.0 16.00000000000001 67.6 72 0
16 quality_transitions quiet_precision 24 10.770000000000001 10.467771592924619 56.0 47.99999999999999 3.0800577533580347 7.999999999999999 16.0 42.0 72 0
17 quality_transitions fast_precision 24 10.770000000000001 19.145424296464835 88.00000000000001 88.0 4.861884148700504 8.0 24.000000000000004 73.6 72 0
18 quality_transitions drift_gain_1_2 24 10.770000000000001 16.291766736261422 83.39999999999999 76.0 4.089452052592408 8.0 16.000000000000004 63.599999999999994 72 0
19 quality_transitions quality_scheduled_drift 24 10.770000000000001 16.766578539927025 83.39999999999999 80.0 4.224793965697071 8.0 16.00000000000001 67.6 72 0
20 thermal_0p05ppm current 24 10.770000000000001 39.2873108676108 102.79999999999998 136.0 5.727479384554999 8.000000000000005 24.0 124.0 0 0
21 thermal_0p05ppm robust_noise_guard 24 10.770000000000001 39.2873108676108 102.79999999999998 136.0 5.727479384554999 8.000000000000005 24.0 124.0 0 0
22 thermal_0p05ppm quiet_precision 24 10.770000000000001 210.25812707207118 416.0 648.0 13.000191801484194 24.000000000000004 32.0 632.4 0 0
23 thermal_0p05ppm fast_precision 24 10.770000000000001 21.850277755888733 63.39999999999999 96.0 5.82476895848467 8.000000000000002 16.000000000000004 77.60000000000001 0 0
24 thermal_0p05ppm drift_gain_1_2 24 10.770000000000001 38.40828815100511 96.0 136.0 5.656836393570529 8.000000000000005 16.000000000000004 122.0 0 0
25 thermal_0p05ppm quality_scheduled_drift 24 10.770000000000001 38.40828815100511 96.0 136.0 5.656836393570529 8.000000000000005 16.000000000000004 122.0 0 0
@@ -0,0 +1,70 @@
# PPS stability analysis: current RTL + 180 ms firmware model
This report is generated by `stability_analysis.py`. The absolute LNAV epoch path and the stochastic control loop are evaluated separately so a 20 ms semantic error cannot be hidden by loop noise.
## Absolute epoch result
- Current RTL delivered the validated subframe after 9 following bits.
- With 180 ms postponement, reference error was -0.000032 ms.
- DUT minus trusted PPS was 32.01 ns. That remainder is the model's configured four-PCLK capture pipeline, not a measured board delay.
- Production firmware and this analysis both define `POSTPONE_TOW` as 180 ms for the current RTL delivery semantics.
## Hardware data used by this synthetic sweep
- Real GPS capture: 989 GDOP samples and 990 tracked-satellite samples over about five minutes.
- GDOP min/median/p95/max: 1.86 / 1.93 / 3.15 / 3.15.
- Tracked satellites: 8..8 (median 8).
- Horizontal scatter RMS/p95/max: 8.58 / 15.15 / 23.77 m. No surveyed truth was recorded.
- The older fusion capture used by this sweep has no ABS_SAMPLE, REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/integrator, temperature, or trusted-edge TIC samples. Those columns in the companion CSV are explicitly synthetic. Newer PPS status captures are analyzed separately by `hardware_replay.py` and are not silently mixed into this historical baseline.
Synthetic timing uses the board-calibration values already encoded in the PPS tests: TDOP 1.50, GDOP 2.92, clock sigma 15.87 m (52.94 ns), C/N0 40.3/34.8 dB-Hz, drift noise 3.47 m/s, seven-satellite reference point, -291.75 m/s datum range rate, and 1.592 ppm local oscillator. Real GDOP and satellite count replace the geometry fields when available.
## Current-loop results
`period max` is the worst |edge[n]-edge[n-1]| across all seeds. `absolute p-p` and `10 s wander p-p` expose the slow left/right motion that period RMS hides.
| scenario | abs RMS median (ns) | abs p-p median (ns) | 10 s wander p-p (ns) | period RMS (ns) | period max (ns) | unlocks |
|---|---:|---:|---:|---:|---:|---:|
| real_geometry_board_noise | 10.88 | 48.00 | 36.80 | 3.87 | 8.00 | 0 |
| scope_like_multipath | 22.94 | 88.00 | 80.80 | 4.73 | 16.00 | 0 |
| quality_transitions | 18.35 | 80.00 | 73.20 | 4.41 | 24.00 | 0 |
| thermal_0p05ppm | 39.29 | 136.00 | 124.00 | 5.73 | 24.00 | 0 |
The loop does not numerically oscillate or lose lock in these runs. The scope-like symptom is low-frequency phase wander: it can be tens of nanoseconds while one-second period jitter remains below 20 ns. Therefore a `p95 period jitter < 20 ns` assertion is not sufficient for an absolute ±20 ns requirement.
## Linear stability
The fixed-point coefficients give the following discrete poles at the 200 ms navigation update rate:
| profile | natural frequency | damping | poles | max magnitude |
|---|---:|---:|---|---:|
| acquire | 0.120 Hz | 0.90 | 0.852913+0.033242j, 0.852913-0.033242j | 0.853561 |
| precision | 0.035 Hz | 0.90 | 0.959449+0.017031j, 0.959449-0.017031j | 0.959600 |
| robust | 0.012 Hz | 1.00 | 0.986668+0.000000j, 0.982945+0.000000j | 0.986668 |
All magnitudes are below one. Profile changes preserve the PI integrator, and the RTL snapshots Kp/Ki/max-slew/lock limits together with the next commit, so the current acquire/precision/robust switching is structurally sound. Stable individual modes do not prove arbitrary-switching stability, but the asymmetric 8-update upgrade / 3-update downgrade hysteresis and the stress runs show no coefficient chatter or unlock.
## Dynamic parameter sweep
| tuning | worst period max (ns) | median abs RMS over scenarios (ns) | transitions | unlocks |
|---|---:|---:|---:|---:|
| current | 24.00 | 20.65 | 72 | 0 |
| robust_noise_guard | 24.00 | 20.21 | 72 | 0 |
| quiet_precision | 32.00 | 13.81 | 72 | 0 |
| fast_precision | 24.00 | 20.50 | 72 | 0 |
| drift_gain_1_2 | 16.00 | 18.82 | 72 | 0 |
| quality_scheduled_drift | 16.00 | 19.06 | 72 | 0 |
The safest small candidate is `robust_noise_guard`: retain the current 0.035 Hz precision loop and 0.012 Hz robust loop, but reduce only the robust-profile WLS residual gain from 1/32 to 1/64. In the synthetic quality-transition case its worst period step changed from 24.00 to 16.00 ns. This is a candidate for hardware A/B testing, not enough evidence for a production constant change.
The best synthetic all-scenario period result is `quality_scheduled_drift`: use drift gain 1/2 only with a precision-quality Doppler solution, fall back to the current 1/4 gain in robust quality, and combine that with the 1/64 robust bias gain. Its worst period step was 16.00 ns with no unlocks. It is the most meaningful dynamic A/B candidate, but the repository has no real drift-covariance or TIC samples with which to validate the threshold.
Narrowing precision to 0.012 Hz improves stationary noise but lags even the explicit 0.05 ppm/120 s synthetic oscillator variation. Widening it improves oscillator tracking but passes more WLS/multipath motion. A geometry-only bandwidth scheduler cannot resolve that tradeoff.
## Recommended architecture update
1. Keep current acquire/precision/robust PI profiles until real TIC and oscillator-temperature data are available. Do not tune from scope appearance alone.
2. Log the existing firmware status line at every solve during validation: ABS_SAMPLE, REM_CODE_PHASE, capture sequence/counter, TDOP/HDOP/GDOP, C/N0, pseudorange residual RMS, raw/filtered bias, innovation, bias drift, hardware phase, rate, integrator, profile, lock, FPGA/oscillator temperature, DUT edge and trusted edge.
3. A/B test the simulated scheduled filter first: precision drift gain 1/2, robust drift gain 1/4, precision bias gain 1/32, and robust bias gain 1/64. Longer term, use estimated phase/drift covariance to interpolate those software gains within bounds. Keep RTL PI bandwidth changes hysteretic and preserve its integrator as today.
4. If temperature/frequency acceleration is the main source, add oscillator temperature feed-forward or a phase/frequency/frequency-drift estimator. Switching PI bandwidth alone either passes WLS wander or lags the oscillator.
5. Define acceptance separately: calibrated mean offset, absolute centered error/p-p wander, one-second period error, holdover, and re-lock. A physical max-20 ns claim requires a common-clock TIC run after cable/output-delay calibration; this synthetic test cannot certify it.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,874 @@
"""Causal Linux model of the GPS LNAV-to-PPS timing path.
This module is intentionally narrower than a general GNSS simulator. It
models the project boundary which can create a constant absolute-time error:
* parity-correct 30-bit GPS LNAV words, including D30* data inversion;
* the 1 kHz prompt-sign transition detector and 20 ms bit-valid generator;
* the tracking wrapper's 310-bit subframe/preamble search window;
* current registered-window and proposed current-bit-aware validation;
* subframe-ready and periodic navigation-solve interrupts;
* RF ABS_SAMPLE anchoring and the timestamped 125 MHz PPS capture;
* the firmware HOW + POSTPONE_TOW + STARTOFFSET - b/c mapping; and
* coarse/normal PPS commits followed by comparison with an independent PPS.
Long stretches between 50 bit/s events are evaluated analytically. No result
depends on the Linux scheduler or wall clock.
"""
from __future__ import annotations
import argparse
import json
import math
from collections import deque
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Iterable, Sequence
C = 299_792_458.0
PREAMBLE = 0x8B
LNAV_WORD_BITS = 30
LNAV_DATA_BITS = 24
LNAV_WORDS_PER_SUBFRAME = 10
LNAV_SUBFRAME_BITS = LNAV_WORD_BITS * LNAV_WORDS_PER_SUBFRAME
LNAV_BIT_PERIOD_S = 0.020
LNAV_BITS_PER_SUBFRAME = 300
LNAV_WINDOW_BITS = 2 + LNAV_SUBFRAME_BITS + 8
TRACKING_INTEGRATION_PERIOD_S = 0.001
TRACKING_INTEGRATIONS_PER_BIT = 20
RF_HZ = 16_367_981.0
PPS_HZ = 125_000_000.0
START_OFFSET_S = 68.802e-3
NAV_TOW_SNAP_TOLERANCE_S = 0.005
Q32 = 1 << 32
TICK_INTEGER_TOLERANCE = 0.01
class DecoderMode(str, Enum):
"""Subframe search ordering relative to the current bit-valid event."""
CURRENT_RTL = "current-rtl"
CURRENT_BIT = "current-bit"
def _bits(value: int, width: int) -> list[int]:
return [(value >> shift) & 1 for shift in range(width - 1, -1, -1)]
def _integer(bits: Iterable[int]) -> int:
value = 0
for bit in bits:
value = (value << 1) | int(bit)
return value
def _xor(values: Iterable[int]) -> int:
result = 0
for value in values:
result ^= int(value)
return result
def lnav_parity(data_bits: Sequence[int], previous_d29: int, previous_d30: int) -> tuple[int, ...]:
"""Return transmitted D25*..D30* using the equations in tracking_wrapper.
``data_bits`` are normalized D1..D24. The equations below are a direct
index-form transcription of ``parity_calc`` in tracking_wrapper.sv.
"""
if len(data_bits) != LNAV_DATA_BITS:
raise ValueError("an LNAV word must contain 24 data bits")
d = [0] + [int(bit) for bit in data_bits] # ICD bit numbers are one based.
return (
_xor((previous_d29, d[1], d[2], d[3], d[5], d[6], d[10], d[11],
d[12], d[13], d[14], d[17], d[18], d[20], d[23])),
_xor((previous_d30, d[2], d[3], d[4], d[6], d[7], d[11], d[12],
d[13], d[14], d[15], d[18], d[19], d[21], d[24])),
_xor((previous_d29, d[1], d[3], d[4], d[5], d[7], d[8], d[12],
d[13], d[14], d[15], d[16], d[19], d[20], d[22])),
_xor((previous_d30, d[2], d[4], d[5], d[6], d[8], d[9], d[13],
d[14], d[15], d[16], d[17], d[20], d[21], d[23])),
_xor((previous_d30, d[1], d[3], d[5], d[6], d[7], d[9], d[10],
d[14], d[15], d[16], d[17], d[18], d[21], d[22], d[24])),
_xor((previous_d29, d[3], d[5], d[6], d[8], d[9], d[10], d[11],
d[13], d[15], d[19], d[22], d[23], d[24])),
)
def encode_lnav_word(
data: int, previous_d29: int, previous_d30: int
) -> tuple[tuple[int, ...], int, int, int]:
"""Encode one normalized 24-bit word and return transmitted bits/state."""
if data < 0 or data >= (1 << LNAV_DATA_BITS):
raise ValueError("LNAV data does not fit in 24 bits")
normalized = _bits(data, LNAV_DATA_BITS)
parity = lnav_parity(normalized, previous_d29, previous_d30)
transmitted_data = tuple(bit ^ previous_d30 for bit in normalized)
transmitted = transmitted_data + parity
normalized_word = (data << 6) | _integer(parity)
return transmitted, parity[4], parity[5], normalized_word
@dataclass(frozen=True)
class EncodedSubframe:
z_count: int
subframe_id: int
transmitted_bits: tuple[int, ...]
normalized_words: tuple[int, ...]
ending_d29: int
ending_d30: int
def encode_lnav_subframe(
z_count: int,
subframe_id: int,
previous_d29: int,
previous_d30: int,
*,
week: int = 2200,
) -> EncodedSubframe:
"""Generate a deterministic, parity-valid LNAV subframe.
The TLM preamble, HOW Z-count/subframe ID, and the modulo-1024 week field
used by this firmware are placed in their implemented bit positions. The
remaining data is deterministic filler so all ten parity checks are live.
"""
if not 0 <= z_count < (1 << 17):
raise ValueError("HOW Z-count must fit in 17 bits")
if not 1 <= subframe_id <= 5:
raise ValueError("subframe ID must be 1..5")
data_words = [0] * LNAV_WORDS_PER_SUBFRAME
data_words[0] = PREAMBLE << 16
data_words[1] = (z_count << 7) | (subframe_id << 2)
if subframe_id == 1:
data_words[2] = (week & 0x3FF) << 14
for index in range(2 if subframe_id != 1 else 3, LNAV_WORDS_PER_SUBFRAME):
# Avoid changing the semantic fields above while exercising parity
# with a stable, non-zero bit pattern.
data_words[index] = (
(0x5A_0000 ^ (z_count * 0x45D9F3B) ^ (subframe_id << 12) ^ index)
& 0xFF_FFFF
)
emitted: list[int] = []
normalized_words: list[int] = []
d29, d30 = int(previous_d29), int(previous_d30)
for data in data_words:
word, d29, d30, normalized_word = encode_lnav_word(data, d29, d30)
emitted.extend(word)
normalized_words.append(normalized_word)
return EncodedSubframe(
z_count=z_count,
subframe_id=subframe_id,
transmitted_bits=tuple(emitted),
normalized_words=tuple(normalized_words),
ending_d29=d29,
ending_d30=d30,
)
@dataclass(frozen=True)
class DecodedSubframe:
z_count: int
subframe_id: int
normalized_words: tuple[int, ...]
def validate_rtl_window(window: Sequence[int]) -> DecodedSubframe | None:
"""Mirror the RTL preamble and ten-word parity predicate.
The chronological window is ``previous D29*, D30*`` followed by the
300-bit candidate subframe and eight bits of the following preamble.
"""
if len(window) != LNAV_WINDOW_BITS:
return None
preamble = tuple(_bits(PREAMBLE, 8))
inverse_preamble = tuple(bit ^ 1 for bit in preamble)
first = tuple(window[2:10])
following = tuple(window[302:310])
if first not in (preamble, inverse_preamble):
return None
if following not in (preamble, inverse_preamble):
return None
d29, d30 = int(window[0]), int(window[1])
normalized_words: list[int] = []
for word_index in range(LNAV_WORDS_PER_SUBFRAME):
start = 2 + word_index * LNAV_WORD_BITS
transmitted = tuple(int(bit) for bit in window[start : start + LNAV_WORD_BITS])
normalized_data = tuple(bit ^ d30 for bit in transmitted[:LNAV_DATA_BITS])
expected_parity = lnav_parity(normalized_data, d29, d30)
if tuple(transmitted[LNAV_DATA_BITS:]) != expected_parity:
return None
normalized_words.append((_integer(normalized_data) << 6) | _integer(expected_parity))
d29, d30 = transmitted[28], transmitted[29]
how_word = normalized_words[1]
return DecodedSubframe(
z_count=(how_word >> 13) & 0x1FFFF,
subframe_id=(how_word >> 8) & 0x7,
normalized_words=tuple(normalized_words),
)
@dataclass(frozen=True)
class SampleStamp:
raw: int
corrected: float
@dataclass(frozen=True)
class BitEvent:
stream_index: int
value: int
transmit_time_s: float
receive_time_s: float
sample_time_s: float
sample: SampleStamp
@dataclass(frozen=True)
class PromptSample:
value: int
receive_time_s: float
sample: SampleStamp
class BitGeneratorModel:
"""Mirror bit_gen.sv at its 1 kHz valid-input clock enables."""
def __init__(self, *, propagation_s: float):
self.propagation_s = propagation_s
self.prompt_history: deque[int] = deque([0] * 40, maxlen=40)
self.record_bits = False
self.loop_counter = 0
self.prompt_ones = 0
self.absolute_sample: SampleStamp | None = None
self.absolute_sample_time_s = 0.0
self.output_index = 0
def feed(self, prompt: PromptSample) -> BitEvent | None:
bit_transition = tuple(self.prompt_history) == (
(1,) * TRACKING_INTEGRATIONS_PER_BIT
+ (0,) * TRACKING_INTEGRATIONS_PER_BIT
)
loop_enabled = self.record_bits
loop_full = self.loop_counter == TRACKING_INTEGRATIONS_PER_BIT - 1
bit_value = int(self.prompt_ones > TRACKING_INTEGRATIONS_PER_BIT / 2 - 1)
sampled_before_edge = self.absolute_sample
sample_time_before_edge = self.absolute_sample_time_s
# All assignments in bit_gen are nonblocking: the output on a full
# loop sees the ABS_SAMPLE registered by the preceding 1 ms input.
self.prompt_history.append(prompt.value)
self.absolute_sample = prompt.sample
self.absolute_sample_time_s = prompt.receive_time_s
if bit_transition:
self.record_bits = True
if loop_enabled:
if loop_full:
self.loop_counter = 0
self.prompt_ones = 0
else:
self.loop_counter += 1
self.prompt_ones += prompt.value
if not loop_full or sampled_before_edge is None:
return None
event = BitEvent(
stream_index=self.output_index,
value=bit_value,
transmit_time_s=prompt.receive_time_s - self.propagation_s,
receive_time_s=prompt.receive_time_s,
sample_time_s=sample_time_before_edge,
sample=sampled_before_edge,
)
self.output_index += 1
return event
@dataclass(frozen=True)
class SubframeInterrupt:
z_count: int
subframe_id: int
words: tuple[int, ...]
bit_index_after_how: int
sample_time_s: float
irq_time_s: float
sample: SampleStamp
class TrackingDecoderModel:
"""Event-level model of tracking_wrapper's shift/search/IRQ ordering."""
def __init__(self, mode: DecoderMode, pps_hz: float = PPS_HZ):
self.mode = DecoderMode(mode)
self.pps_hz = pps_hz
self.history: deque[int] = deque(maxlen=LNAV_WINDOW_BITS)
def feed(self, event: BitEvent) -> SubframeInterrupt | None:
registered = tuple(self.history)
if self.mode == DecoderMode.CURRENT_RTL:
decoded = validate_rtl_window(registered)
self.history.append(event.value)
else:
candidate = (registered + (event.value,))[-LNAV_WINDOW_BITS:]
decoded = validate_rtl_window(candidate)
self.history.append(event.value)
if decoded is None:
return None
how_boundary = decoded.z_count * 6.0
bit_index = int(round((event.transmit_time_s - how_boundary) / LNAV_BIT_PERIOD_S))
# load_sigs is edge-detected before gps_frame_ready. This two-PCLK
# delay affects interrupt latency but not the latched ABS_SAMPLE.
irq_time = event.receive_time_s + 2.0 / self.pps_hz
return SubframeInterrupt(
z_count=decoded.z_count,
subframe_id=decoded.subframe_id,
words=decoded.normalized_words,
bit_index_after_how=bit_index,
sample_time_s=event.sample_time_s,
irq_time_s=irq_time,
sample=event.sample,
)
@dataclass(frozen=True)
class CounterClock:
nominal_hz: float
epoch_time_s: float
oscillator_ppm: float = 0.0
@property
def physical_hz(self) -> float:
return self.nominal_hz * (1.0 + self.oscillator_ppm * 1.0e-6)
def first_tick_at_or_after(self, time_s: float) -> int:
exact = (time_s - self.epoch_time_s) * self.physical_hz
nearest = round(exact)
# Absolute GPS TOW is intentionally retained in the model. Subtracting
# two ~345 ks binary64 values can leave a few thousandths of one 8 ns
# tick even when the synthetic event was constructed exactly on that
# tick. Snap only this numerical residue; genuinely asynchronous
# events still use the following hardware clock edge.
if abs(exact - nearest) <= TICK_INTEGER_TOLERANCE:
exact = float(nearest)
return max(0, int(math.ceil(exact)))
def time_for_tick(self, tick: int) -> float:
return self.epoch_time_s + tick / self.physical_hz
@dataclass(frozen=True)
class NavSolveInterrupt:
sample_time_s: float
irq_time_s: float
bit_index_after_how: int
sample: SampleStamp
pps_counter: int
pps_capture_time_s: float
pps_capture_sequence: int
class NavSolveTimerModel:
"""Mirror the current timer: seed from Z*300, then count later bit pulses."""
def __init__(
self,
period_bits: int,
subframe: SubframeInterrupt,
clock: CounterClock,
*,
capture_pipeline_ticks: int,
):
if period_bits <= 0:
raise ValueError("navigation solve period must be positive")
self.period_bits = period_bits
self.counter = (subframe.z_count * LNAV_BITS_PER_SUBFRAME) % period_bits
self.start_sample_time_s = subframe.sample_time_s
self.how_boundary_s = subframe.z_count * 6.0
self.clock = clock
self.capture_pipeline_ticks = capture_pipeline_ticks
self.sequence = 0
def feed(self, event: BitEvent) -> NavSolveInterrupt | None:
if event.sample_time_s <= self.start_sample_time_s:
return None
reached = self.counter == self.period_bits - 1
self.counter = 0 if reached else self.counter + 1
if not reached:
return None
bit_tick = self.clock.first_tick_at_or_after(event.receive_time_s)
capture_tick = bit_tick + self.capture_pipeline_ticks
self.sequence += 1
return NavSolveInterrupt(
sample_time_s=event.sample_time_s,
irq_time_s=self.clock.time_for_tick(capture_tick + 1),
bit_index_after_how=int(
round((event.transmit_time_s - self.how_boundary_s) / LNAV_BIT_PERIOD_S)
),
sample=event.sample,
pps_counter=capture_tick,
pps_capture_time_s=self.clock.time_for_tick(capture_tick),
pps_capture_sequence=self.sequence,
)
@dataclass(frozen=True)
class FirmwareReference:
nav_bit_index_after_how: int
periodic_elapsed_s: float
transmit_tow_s: float
gps_time_at_capture_s: float
gps_time_at_capture_q32_s: float
true_capture_time_s: float
signed_reference_error_s: float
epoch_counter: int
class FirmwareTimingModel:
"""Mirror HOW sample anchoring and the current PPS receiver-time equation."""
def __init__(
self,
*,
nav_solve_period_bits: int,
postpone_tow_s: float,
propagation_s: float,
):
self.event_period_s = nav_solve_period_bits * LNAV_BIT_PERIOD_S
self.postpone_tow_s = postpone_tow_s
self.propagation_s = propagation_s
# In this project's WLS datum STARTOFFSET - b/c recovers flight time.
self.synthetic_clock_bias_m = (START_OFFSET_S - propagation_s) * C
self.anchor: SubframeInterrupt | None = None
self.rejected_nav_solves = 0
def on_subframe(self, event: SubframeInterrupt) -> None:
self.anchor = event
@staticmethod
def _signed_u32_delta(current: int, previous: int) -> int:
delta = (current - previous) & 0xFFFF_FFFF
return delta - (1 << 32) if delta & 0x8000_0000 else delta
def on_nav_solve(self, event: NavSolveInterrupt) -> FirmwareReference | None:
if self.anchor is None:
raise RuntimeError("navigation solve arrived before a valid HOW anchor")
raw_delta = self._signed_u32_delta(event.sample.raw, self.anchor.sample.raw)
anchor_fraction = self.anchor.sample.corrected - self.anchor.sample.raw
event_fraction = event.sample.corrected - event.sample.raw
elapsed_s = (raw_delta + event_fraction - anchor_fraction) / RF_HZ
event_index = int(math.floor(elapsed_s / self.event_period_s + 0.5))
periodic_elapsed_s = event_index * self.event_period_s
if abs(periodic_elapsed_s - elapsed_s) > NAV_TOW_SNAP_TOLERANCE_S:
# gps_time_from_periodic_sample_anchor() returns false and the
# interrupt handler drops this otherwise coherent NAV solve.
self.rejected_nav_solves += 1
return None
transmit_tow = self.anchor.z_count * 6.0 + periodic_elapsed_s
gps_reference = (
transmit_tow
+ self.postpone_tow_s
+ START_OFFSET_S
- self.synthetic_clock_bias_m / C
)
whole = math.floor(gps_reference)
fraction_q32 = int(math.floor((gps_reference - whole) * Q32 + 0.5))
if fraction_q32 == Q32:
whole += 1
fraction_q32 = 0
gps_reference_q32 = whole + fraction_q32 / Q32
return FirmwareReference(
nav_bit_index_after_how=event.bit_index_after_how,
periodic_elapsed_s=periodic_elapsed_s,
transmit_tow_s=transmit_tow,
gps_time_at_capture_s=gps_reference,
gps_time_at_capture_q32_s=gps_reference_q32,
true_capture_time_s=event.pps_capture_time_s,
signed_reference_error_s=gps_reference_q32 - event.pps_capture_time_s,
epoch_counter=event.pps_counter,
)
@dataclass(frozen=True)
class PpsEdge:
gps_second: int
dut_edge_time_s: float
trusted_edge_time_s: float
dut_minus_trusted_s: float
@dataclass(frozen=True)
class PpsCoreResult:
normal_commits_to_lock: int
lock_time_s: float
commit_age_s: float
edges: tuple[PpsEdge, ...]
def simulate_pps_core(
references: Sequence[FirmwareReference],
clock: CounterClock,
*,
compute_delay_s: float,
core_pipeline_ticks: int,
lock_updates: int,
dut_output_delay_s: float,
trusted_output_delay_s: float,
) -> PpsCoreResult:
"""Model timestamped coarse/normal commits and nominal locked PPS edges."""
if len(references) < lock_updates + 1:
raise RuntimeError("not enough navigation references to acquire PPS lock")
coarse = references[0]
mapping_counter = coarse.epoch_counter
mapping_time = coarse.gps_time_at_capture_q32_s
good_updates = 0
lock_reference: FirmwareReference | None = None
for reference in references[1:]:
mapped_at_capture = mapping_time + (
reference.epoch_counter - mapping_counter
) / clock.nominal_hz
phase_error = reference.gps_time_at_capture_q32_s - mapped_at_capture
if abs(phase_error) <= 8.0 / clock.nominal_hz:
good_updates += 1
else:
good_updates = 0
if good_updates >= lock_updates:
lock_reference = reference
break
if lock_reference is None:
raise RuntimeError("PPS loop did not acquire lock")
commit_time = lock_reference.true_capture_time_s + compute_delay_s
commit_tick = clock.first_tick_at_or_after(commit_time) + core_pipeline_ticks
lock_time = clock.time_for_tick(commit_tick)
commit_age_s = lock_time - lock_reference.true_capture_time_s
lock_counter = commit_tick
mapped_at_lock = mapping_time + (lock_counter - mapping_counter) / clock.nominal_hz
edges: list[PpsEdge] = []
target_second = int(math.floor(mapped_at_lock)) + 1
for _ in range(3):
ticks_exact = (target_second - mapping_time) * clock.nominal_hz
nearest_tick = round(ticks_exact)
if abs(ticks_exact - nearest_tick) <= TICK_INTEGER_TOLERANCE:
ticks_exact = float(nearest_tick)
ticks_from_mapping = int(math.ceil(ticks_exact))
edge_tick = mapping_counter + ticks_from_mapping
dut_edge = clock.time_for_tick(edge_tick) + dut_output_delay_s
trusted_edge = target_second + trusted_output_delay_s
edges.append(
PpsEdge(
gps_second=target_second,
dut_edge_time_s=dut_edge,
trusted_edge_time_s=trusted_edge,
dut_minus_trusted_s=dut_edge - trusted_edge,
)
)
target_second += 1
return PpsCoreResult(
normal_commits_to_lock=good_updates,
lock_time_s=lock_time,
commit_age_s=commit_age_s,
edges=tuple(edges),
)
@dataclass(frozen=True)
class EndToEndConfig:
decoder_mode: DecoderMode = DecoderMode.CURRENT_RTL
postpone_tow_s: float = 0.180
nav_solve_period_bits: int = 10
propagation_s: float = 0.070
pps_capture_pipeline_ticks: int = 4
firmware_compute_delay_s: float = 0.010
core_pipeline_ticks: int = 12
dut_output_delay_s: float = 0.0
trusted_output_delay_s: float = 0.0
first_z_count: int = 57_600
subframes: int = 3
# Put the first HOW capture just before a modulo-32 wrap so the first
# periodic solve also exercises the firmware's signed unwrap rule.
rf_initial_sample: int = (
(1 << 32) - int(10.25 * RF_HZ) - 1_000
)
@dataclass(frozen=True)
class EndToEndResult:
decoder_mode: str
postpone_tow_ms: float
bit_valid_events: int
bit_generator_sample_lag_ms: float
subframe_interrupts: int
nav_solve_interrupts: int
firmware_rejected_nav_solves: int
first_subframe_readout_bits: int
first_nav_solve_bit: int
first_reference_error_ms: float
expected_semantic_offset_ms: float
pps: PpsCoreResult
def _sample_at(time_s: float, epoch_s: float, initial_sample: int) -> SampleStamp:
exact = initial_sample + (time_s - epoch_s) * RF_HZ
raw_unwrapped = int(math.ceil(exact - 1.0e-10))
fractional_delay = raw_unwrapped - exact
raw = raw_unwrapped & 0xFFFF_FFFF
# This is the firmware representation: corrected_abs is near the wrapped
# integer ABS_SAMPLE, not a separately unwrapped floating-point counter.
return SampleStamp(raw=raw, corrected=raw - fractional_delay)
def _build_stream(config: EndToEndConfig) -> tuple[tuple[int, ...], float]:
d29, d30 = 0, 0
stream: list[int] = [d29, d30]
for offset in range(config.subframes):
subframe = encode_lnav_subframe(
config.first_z_count + offset,
offset % 5 + 1,
d29,
d30,
)
stream.extend(subframe.transmitted_bits)
d29, d30 = subframe.ending_d29, subframe.ending_d30
return tuple(stream), config.first_z_count * 6.0
def _generate_bit_events(
stream: Sequence[int],
*,
first_how_boundary_s: float,
propagation_s: float,
counter_epoch_s: float,
rf_initial_sample: int,
) -> tuple[BitEvent, ...]:
"""Expand LNAV bits to the 1 kHz prompt-sign input consumed by bit_gen."""
first_bit_event_tx = first_how_boundary_s - (
LNAV_SUBFRAME_BITS + 1
) * LNAV_BIT_PERIOD_S
first_prompt_receive = (
first_bit_event_tx
+ propagation_s
- LNAV_BIT_PERIOD_S
)
prompt_values = (
[1] * TRACKING_INTEGRATIONS_PER_BIT
+ [0] * TRACKING_INTEGRATIONS_PER_BIT
)
for value in stream:
prompt_values.extend([int(value)] * TRACKING_INTEGRATIONS_PER_BIT)
# The first integration of the following bit makes loop_counter_full true
# and emits the final requested bit, just as in bit_gen.sv.
prompt_values.append(0)
training_duration_s = 2 * LNAV_BIT_PERIOD_S
prompt_start_s = first_prompt_receive - training_duration_s
generator = BitGeneratorModel(propagation_s=propagation_s)
events: list[BitEvent] = []
for prompt_index, value in enumerate(prompt_values):
receive_time = (
prompt_start_s
+ prompt_index * TRACKING_INTEGRATION_PERIOD_S
)
output = generator.feed(
PromptSample(
value=value,
receive_time_s=receive_time,
sample=_sample_at(
receive_time,
counter_epoch_s,
rf_initial_sample,
),
)
)
if output is not None:
events.append(output)
if tuple(event.value for event in events) != tuple(stream):
raise RuntimeError("bit_gen model did not reproduce the requested LNAV stream")
return tuple(events)
def run_end_to_end(config: EndToEndConfig) -> EndToEndResult:
if config.subframes < 2:
raise ValueError("at least two subframes are required for preamble validation")
stream, first_how_boundary = _build_stream(config)
counter_epoch = first_how_boundary - 10.0
# Oscillator/PI behavior is already covered by pps_loop/adaptive_pps. This
# absolute-epoch regression uses an exact clock so a control-loop transient
# cannot hide or imitate an integer navigation-bit error.
clock = CounterClock(PPS_HZ, counter_epoch)
bit_events = _generate_bit_events(
stream,
first_how_boundary_s=first_how_boundary,
propagation_s=config.propagation_s,
counter_epoch_s=counter_epoch,
rf_initial_sample=config.rf_initial_sample,
)
decoder = TrackingDecoderModel(config.decoder_mode)
firmware = FirmwareTimingModel(
nav_solve_period_bits=config.nav_solve_period_bits,
postpone_tow_s=config.postpone_tow_s,
propagation_s=config.propagation_s,
)
timer: NavSolveTimerModel | None = None
subframe_events: list[SubframeInterrupt] = []
nav_events: list[NavSolveInterrupt] = []
references: list[FirmwareReference] = []
for bit_event in bit_events:
# An already-running timer sees the bit even when a later subframe is
# validated on the same event. Firmware handles NAV_BITS before
# NAV_SOLVE when both status flags share one interrupt.
nav_event = timer.feed(bit_event) if timer is not None else None
subframe_event = decoder.feed(bit_event)
if subframe_event is not None:
subframe_events.append(subframe_event)
firmware.on_subframe(subframe_event)
if timer is None:
timer = NavSolveTimerModel(
config.nav_solve_period_bits,
subframe_event,
clock,
capture_pipeline_ticks=config.pps_capture_pipeline_ticks,
)
if nav_event is not None:
nav_events.append(nav_event)
reference = firmware.on_nav_solve(nav_event)
if reference is not None:
references.append(reference)
if not subframe_events or not nav_events or not references:
raise RuntimeError("synthetic stream did not reach the PPS timing path")
pps = simulate_pps_core(
references,
clock,
compute_delay_s=config.firmware_compute_delay_s,
core_pipeline_ticks=config.core_pipeline_ticks,
lock_updates=5,
dut_output_delay_s=config.dut_output_delay_s,
trusted_output_delay_s=config.trusted_output_delay_s,
)
return EndToEndResult(
decoder_mode=config.decoder_mode.value,
postpone_tow_ms=config.postpone_tow_s * 1.0e3,
bit_valid_events=len(bit_events),
bit_generator_sample_lag_ms=(
bit_events[0].receive_time_s - bit_events[0].sample_time_s
)
* 1.0e3,
subframe_interrupts=len(subframe_events),
nav_solve_interrupts=len(nav_events),
firmware_rejected_nav_solves=firmware.rejected_nav_solves,
first_subframe_readout_bits=subframe_events[0].bit_index_after_how,
first_nav_solve_bit=nav_events[0].bit_index_after_how,
first_reference_error_ms=references[0].signed_reference_error_s * 1.0e3,
expected_semantic_offset_ms=(
subframe_events[0].bit_index_after_how * LNAV_BIT_PERIOD_S
- config.postpone_tow_s
)
* 1.0e3,
pps=pps,
)
def default_comparison() -> tuple[EndToEndResult, ...]:
return (
run_end_to_end(EndToEndConfig()),
run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_RTL,
postpone_tow_s=0.160,
)
),
run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_BIT,
postpone_tow_s=0.160,
)
),
)
def _result_dict(result: EndToEndResult) -> dict[str, object]:
value = asdict(result)
value["pps"]["edges"] = [asdict(edge) for edge in result.pps.edges]
return value
def _print_table(results: Sequence[EndToEndResult]) -> None:
print(
"scenario bits correction reference error "
"DUT - trusted PPS"
)
for result in results:
offset_ms = result.pps.edges[0].dut_minus_trusted_s * 1.0e3
scenario = f"{result.decoder_mode}/{result.postpone_tow_ms:.0f}ms"
print(
f"{scenario:<25} {result.first_subframe_readout_bits:>4d} "
f"{result.postpone_tow_ms:>7.0f} ms "
f"{result.first_reference_error_ms:>+12.6f} ms "
f"{offset_ms:>+13.6f} ms"
)
print("\nPositive DUT - trusted means that the project PPS edge is late.")
print(
"The residual tens of nanoseconds are the configured PCLK capture "
"pipeline, not an LNAV bit."
)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--mode",
choices=("matrix", DecoderMode.CURRENT_RTL.value, DecoderMode.CURRENT_BIT.value),
default="matrix",
help="run the three-way diagnosis matrix or one decoder mode",
)
parser.add_argument("--postpone-ms", type=float, default=180.0)
parser.add_argument("--propagation-ms", type=float, default=70.0)
parser.add_argument("--capture-pipeline-ticks", type=int, default=4)
parser.add_argument("--compute-delay-ms", type=float, default=10.0)
parser.add_argument("--dut-output-delay-ns", type=float, default=0.0)
parser.add_argument("--trusted-output-delay-ns", type=float, default=0.0)
parser.add_argument("--json", action="store_true")
args = parser.parse_args(argv)
if args.mode == "matrix":
results = default_comparison()
else:
results = (
run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode(args.mode),
postpone_tow_s=args.postpone_ms * 1.0e-3,
propagation_s=args.propagation_ms * 1.0e-3,
pps_capture_pipeline_ticks=args.capture_pipeline_ticks,
firmware_compute_delay_s=args.compute_delay_ms * 1.0e-3,
dut_output_delay_s=args.dut_output_delay_ns * 1.0e-9,
trusted_output_delay_s=args.trusted_output_delay_ns * 1.0e-9,
)
),
)
if args.json:
print(json.dumps([_result_dict(result) for result in results], indent=2))
else:
_print_table(results)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,907 @@
#!/usr/bin/env python3
"""End-to-end PPS epoch, telemetry, and closed-loop stability analysis.
The bit-level model proves the current-RTL/180 ms epoch convention. This
module then drives the fixed-point firmware/RTL loop model with the telemetry
that is present in the repository and deterministic synthetic replacements for
the timing fields that were not captured on the board.
"""
from __future__ import annotations
import argparse
import csv
from dataclasses import dataclass, replace
import math
from pathlib import Path
import random
import re
import statistics
from typing import Iterable
from pps_python.adaptive_pps import (
ACQUIRE_PROFILE,
PROFILES,
FixedPointAdaptivePps,
LoopProfile,
Observation,
Profile,
QualityMetrics,
TRACKING_HZ,
classify_quality,
)
from .pps_end_to_end import DecoderMode, EndToEndConfig, run_end_to_end
RF_HZ = 16_367_981.0
CODE_FREQ_HZ = 1_023_000.0
SPEED_OF_LIGHT_MPS = 299_792_458.0
UPDATE_PERIOD_S = 0.2
NAV_BIT_PERIOD_S = 0.020
BOARD_CLOCK_SIGMA_M = 15.87
BOARD_TDOP = 1.50
BOARD_GDOP = 2.92
BOARD_CN0_MEAN_DBHZ = 40.3
BOARD_CN0_MIN_DBHZ = 34.8
BOARD_DRIFT_SIGMA_MPS = 3.47
BOARD_OSCILLATOR_PPM = 1.592
BOARD_REFERENCE_RANGE_RATE_MPS = -291.75
@dataclass(frozen=True)
class RealTelemetry:
gdop_time_s: tuple[float, ...]
gdop: tuple[float, ...]
tracked_time_s: tuple[float, ...]
tracked: tuple[int, ...]
horizontal_rms_m: float
horizontal_p95_m: float
horizontal_max_m: float
def gdop_at(self, time_s: float) -> float:
return _linear_sample(self.gdop_time_s, self.gdop, time_s)
def tracked_at(self, time_s: float) -> int:
if not self.tracked:
return 8
index = _previous_index(self.tracked_time_s, time_s)
return self.tracked[index]
@dataclass(frozen=True)
class Scenario:
name: str
multipath: bool = False
geometry_transitions: bool = False
thermal_amplitude_ppm: float = 0.0
thermal_period_s: float = 120.0
@dataclass(frozen=True)
class Tuning:
name: str
profiles: dict[Profile, LoopProfile]
acquire: LoopProfile
drift_gain: float
robust_drift_gain: float | None = None
@dataclass(frozen=True)
class SyntheticEpoch:
scenario: str
time_s: float
abs_sample: int
rem_code_phase: int
corrected_abs_sample: float
capture_counter_125: int
capture_correction_ns: float
used_satellites: int
gdop: float
hdop: float
vdop: float
tdop: float
cn0_mean_dbhz: float
cn0_min_dbhz: float
pseudorange_noise_rms_m: float
clock_bias_sigma_m: float
clock_drift_error_mps: float
timing_error_ns: float
oscillator_ppm: float
quality_profile: str
observation: Observation
@dataclass(frozen=True)
class RunMetrics:
first_lock_s: float
absolute_rms_ns: float
absolute_p95_ns: float
absolute_max_ns: float
absolute_peak_to_peak_ns: float
period_rms_ns: float
period_p95_ns: float
period_max_ns: float
wander_10s_peak_to_peak_ns: float
profile_transitions: int
unlocks: int
@dataclass(frozen=True)
class Aggregate:
scenario: str
tuning: str
runs: int
lock_p95_s: float
absolute_rms_median_ns: float
absolute_max_p95_ns: float
absolute_peak_to_peak_median_ns: float
period_rms_median_ns: float
period_p95_median_ns: float
period_max_worst_ns: float
wander_10s_peak_to_peak_median_ns: float
profile_transitions_total: int
unlocks_total: int
def _percentile(values: Iterable[float], fraction: float) -> float:
ordered = sorted(values)
if not ordered:
return math.nan
position = (len(ordered) - 1) * fraction
lower = math.floor(position)
upper = math.ceil(position)
if lower == upper:
return ordered[lower]
weight = position - lower
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
def _previous_index(times: tuple[float, ...], time_s: float) -> int:
if not times:
return 0
low = 0
high = len(times)
while low < high:
middle = (low + high) // 2
if times[middle] <= time_s:
low = middle + 1
else:
high = middle
return max(0, low - 1)
def _linear_sample(
times: tuple[float, ...], values: tuple[float, ...], time_s: float
) -> float:
if not values:
return 1.93
if time_s <= times[0]:
return values[0]
if time_s >= times[-1]:
return values[-1]
lower = _previous_index(times, time_s)
upper = min(lower + 1, len(times) - 1)
span = times[upper] - times[lower]
if span <= 0.0:
return values[lower]
fraction = (time_s - times[lower]) / span
return values[lower] + fraction * (values[upper] - values[lower])
def load_real_telemetry(software_dir: Path) -> RealTelemetry:
"""Load the only real GPS capture and its horizontal replay output."""
log_path = software_dir / "fusion/logs/gps_live_5min_20260713.csv"
comparison_path = (
software_dir
/ "fusion/output/model_compare_5min_20260713/model_comparison.csv"
)
gdop_time: list[float] = []
gdop: list[float] = []
tracked_time: list[float] = []
tracked: list[int] = []
if log_path.exists():
with log_path.open(
"r", encoding="utf-8", errors="replace", newline=""
) as source:
for row in csv.reader(source):
if len(row) < 3:
continue
try:
time_s = float(row[0])
except ValueError:
continue
match = re.search(r"GDOP=([0-9.]+)", row[2])
if match:
gdop_time.append(time_s)
gdop.append(float(match.group(1)))
match = re.search(r"tracked satellites=(\d+)", row[2])
if match:
tracked_time.append(time_s)
tracked.append(int(match.group(1)))
radii: list[float] = []
if comparison_path.exists():
with comparison_path.open("r", encoding="utf-8", newline="") as source:
for row in csv.DictReader(source):
try:
radii.append(
math.hypot(float(row["gps_e_m"]), float(row["gps_n_m"]))
)
except (KeyError, TypeError, ValueError):
continue
horizontal_rms = (
math.sqrt(statistics.mean(value * value for value in radii))
if radii
else math.nan
)
return RealTelemetry(
tuple(gdop_time),
tuple(gdop),
tuple(tracked_time),
tuple(tracked),
horizontal_rms,
_percentile(radii, 0.95),
max(radii, default=math.nan),
)
def scenario_matrix() -> tuple[Scenario, ...]:
return (
Scenario("real_geometry_board_noise"),
Scenario("scope_like_multipath", multipath=True),
Scenario("quality_transitions", geometry_transitions=True),
Scenario("thermal_0p05ppm", thermal_amplitude_ppm=0.05),
)
def tuning_matrix() -> tuple[Tuning, ...]:
current = dict(PROFILES)
guarded = dict(PROFILES)
guarded[Profile.ROBUST] = replace(
guarded[Profile.ROBUST], software_bias_gain=1.0 / 64.0
)
quiet = dict(guarded)
quiet[Profile.PRECISION] = replace(
quiet[Profile.PRECISION],
natural_frequency_hz=0.012,
damping=1.0,
software_bias_gain=1.0 / 16.0,
)
fast = dict(guarded)
fast[Profile.PRECISION] = replace(
fast[Profile.PRECISION], natural_frequency_hz=0.050, damping=1.0
)
return (
Tuning("current", current, ACQUIRE_PROFILE, 1.0 / 4.0),
Tuning("robust_noise_guard", guarded, ACQUIRE_PROFILE, 1.0 / 4.0),
Tuning("quiet_precision", quiet, ACQUIRE_PROFILE, 1.0 / 4.0),
Tuning("fast_precision", fast, ACQUIRE_PROFILE, 1.0 / 4.0),
Tuning("drift_gain_1_2", guarded, ACQUIRE_PROFILE, 1.0 / 2.0),
Tuning(
"quality_scheduled_drift",
guarded,
ACQUIRE_PROFILE,
1.0 / 2.0,
1.0 / 4.0,
),
)
def _weak_geometry(time_s: float) -> bool:
return (80.0 <= time_s < 110.0) or (180.0 <= time_s < 205.0)
def synthesize_epochs(
telemetry: RealTelemetry,
scenario: Scenario,
*,
duration_s: float,
seed: int,
update_period_s: float = UPDATE_PERIOD_S,
) -> list[SyntheticEpoch]:
"""Combine real GDOP/satellite count with documented board noise."""
rng = random.Random(seed)
result: list[SyntheticEpoch] = []
count = round(duration_s / update_period_s)
rf_initial = (1 << 32) - round(10.25 * RF_HZ) - 1000
for update in range(count):
time_s = update * update_period_s
gdop = telemetry.gdop_at(time_s)
satellites = max(4, min(8, telemetry.tracked_at(time_s)))
tdop = gdop * (BOARD_TDOP / BOARD_GDOP)
pdop = math.sqrt(max(0.0, gdop * gdop - tdop * tdop))
vdop = 0.65 * pdop
hdop = math.sqrt(max(0.0, pdop * pdop - vdop * vdop))
cn0_mean = (
BOARD_CN0_MEAN_DBHZ
+ 1.2 * math.sin(2.0 * math.pi * time_s / 93.0)
+ rng.gauss(0.0, 0.35)
)
cn0_min = cn0_mean - (
BOARD_CN0_MEAN_DBHZ - BOARD_CN0_MIN_DBHZ
)
chi_square = 0.35
velocity_residual = 0.23
drift_sigma = BOARD_DRIFT_SIGMA_MPS
if scenario.geometry_transitions and _weak_geometry(time_s):
gdop = 8.0
tdop = 5.0
hdop = 4.3
vdop = math.sqrt(max(0.0, gdop * gdop - tdop * tdop - hdop * hdop))
satellites = 5
cn0_mean = 27.0
cn0_min = 18.0
chi_square = 0.85
velocity_residual = 1.8
clock_sigma = 65.0
drift_sigma = 5.0
uere_sigma = clock_sigma / tdop
else:
uere_sigma = (
(BOARD_CLOCK_SIGMA_M / BOARD_TDOP)
* 10.0 ** ((BOARD_CN0_MEAN_DBHZ - cn0_mean) / 20.0)
)
clock_sigma = tdop * uere_sigma
pseudorange_errors = [
rng.gauss(0.0, uere_sigma) for _ in range(satellites)
]
pseudorange_rms = math.sqrt(
statistics.mean(value * value for value in pseudorange_errors)
)
long_omega = 2.0 * math.pi / 75.0
short_omega = 2.0 * math.pi / 19.0
true_datum_drift = (
5.0 * math.sin(long_omega * time_s)
+ 2.0 * math.sin(short_omega * time_s)
)
true_datum_offset = (
5.0 * (1.0 - math.cos(long_omega * time_s)) / long_omega
+ 2.0 * (1.0 - math.cos(short_omega * time_s)) / short_omega
)
timing_error_s = rng.gauss(0.0, clock_sigma / SPEED_OF_LIGHT_MPS)
if scenario.multipath:
timing_error_s += (
25.0e-9 * math.sin(2.0 * math.pi * time_s / 67.0)
+ 15.0e-9
* math.sin(2.0 * math.pi * time_s / 29.0 + 0.8)
)
drift_error = true_datum_drift + rng.gauss(0.0, drift_sigma)
oscillator_ppm = BOARD_OSCILLATOR_PPM + (
scenario.thermal_amplitude_ppm
* math.sin(2.0 * math.pi * time_s / scenario.thermal_period_s)
)
quality = QualityMetrics(
tdop,
gdop,
clock_sigma,
cn0_mean,
cn0_min,
chi_square,
velocity_residual,
satellites,
)
observation = Observation(
time_s,
timing_error_s,
drift_error,
quality,
True,
BOARD_REFERENCE_RANGE_RATE_MPS,
1,
true_datum_offset,
)
continuous_abs = rf_initial + time_s * RF_HZ
raw_unwrapped = math.ceil(continuous_abs - 1.0e-12)
fractional_sample = raw_unwrapped - continuous_abs
rem_code_phase = round(fractional_sample * CODE_FREQ_HZ)
abs_sample = raw_unwrapped & 0xFFFF_FFFF
corrected_abs = abs_sample - rem_code_phase / CODE_FREQ_HZ
raw_capture_counter = math.floor(
time_s * TRACKING_HZ * (1.0 + oscillator_ppm * 1.0e-6)
)
capture_correction_ticks = round(
(rem_code_phase / CODE_FREQ_HZ) * (TRACKING_HZ / RF_HZ)
)
capture_counter = raw_capture_counter - capture_correction_ticks
result.append(
SyntheticEpoch(
scenario.name,
time_s,
abs_sample,
rem_code_phase,
corrected_abs,
capture_counter,
capture_correction_ticks / TRACKING_HZ * 1.0e9,
satellites,
gdop,
hdop,
vdop,
tdop,
cn0_mean,
cn0_min,
pseudorange_rms,
clock_sigma,
drift_error,
timing_error_s * 1.0e9,
oscillator_ppm,
classify_quality(quality).name.lower(),
observation,
)
)
return result
def _run_loop(
epochs: list[SyntheticEpoch],
tuning: Tuning,
*,
update_period_s: float = UPDATE_PERIOD_S,
):
model = FixedPointAdaptivePps(
update_period_s=update_period_s,
oscillator_ppm=BOARD_OSCILLATOR_PPM,
profiles=tuning.profiles,
acquire_profile=tuning.acquire,
software_drift_gain=tuning.drift_gain,
)
for epoch in epochs:
# Update at a solve boundary; the selected value applies to the
# preceding synthetic interval, whose endpoint is this observation.
model.oscillator_fraction = epoch.oscillator_ppm * 1.0e-6
if tuning.robust_drift_gain is not None:
target = classify_quality(epoch.observation.quality)
model.software_drift_gain = (
tuning.robust_drift_gain
if target != Profile.PRECISION
else tuning.drift_gain
)
model.update(epoch.observation)
finish_s = epochs[-1].time_s + update_period_s if epochs else 0.0
return model.finish(finish_s)
def measure_trace(trace, *, after_s: float) -> RunMetrics:
edge_errors = [
error for _, error in trace.integer_second_edge_errors(after_s=after_s)
]
if len(edge_errors) < 12:
raise RuntimeError("not enough locked integer-second PPS edges")
center = statistics.median(edge_errors)
centered = [value - center for value in edge_errors]
periods = [
edge_errors[index] - edge_errors[index - 1]
for index in range(1, len(edge_errors))
]
moving_10s = [
statistics.mean(centered[max(0, index - 9) : index + 1])
for index in range(len(centered))
]
profiles = [
profile
for time_s, profile in zip(trace.time_s, trace.profile)
if time_s >= after_s
]
profile_transitions = sum(
profiles[index] != profiles[index - 1]
for index in range(1, len(profiles))
)
return RunMetrics(
trace.first_lock_time() if trace.first_lock_time() is not None else math.inf,
math.sqrt(statistics.mean(value * value for value in centered)) * 1.0e9,
_percentile((abs(value) for value in centered), 0.95) * 1.0e9,
max(abs(value) for value in centered) * 1.0e9,
(max(centered) - min(centered)) * 1.0e9,
math.sqrt(statistics.mean(value * value for value in periods)) * 1.0e9,
_percentile((abs(value) for value in periods), 0.95) * 1.0e9,
max(abs(value) for value in periods) * 1.0e9,
(max(moving_10s) - min(moving_10s)) * 1.0e9,
profile_transitions,
trace.unlock_events,
)
def aggregate(
scenario: Scenario, tuning: Tuning, runs: list[RunMetrics]
) -> Aggregate:
return Aggregate(
scenario.name,
tuning.name,
len(runs),
_percentile((run.first_lock_s for run in runs), 0.95),
statistics.median(run.absolute_rms_ns for run in runs),
_percentile((run.absolute_max_ns for run in runs), 0.95),
statistics.median(run.absolute_peak_to_peak_ns for run in runs),
statistics.median(run.period_rms_ns for run in runs),
statistics.median(run.period_p95_ns for run in runs),
max(run.period_max_ns for run in runs),
statistics.median(run.wander_10s_peak_to_peak_ns for run in runs),
sum(run.profile_transitions for run in runs),
sum(run.unlocks for run in runs),
)
def discrete_poles(
profile: LoopProfile,
update_period_s: float = UPDATE_PERIOD_S,
) -> tuple[complex, complex]:
"""Poles of the exact sample/commit ordering, before slew saturation."""
kp_q46, ki_q46, _ = profile.fixed_coefficients(update_period_s)
kp = kp_q46 / (1 << 46)
ki = ki_q46 / (1 << 46)
ticks = update_period_s * TRACKING_HZ
a = 1.0 - ticks * (kp + ki)
b = ticks
c = -ki
d = 1.0
trace = a + d
determinant = a * d - b * c
discriminant = complex(trace * trace - 4.0 * determinant, 0.0) ** 0.5
return (trace + discriminant) / 2.0, (trace - discriminant) / 2.0
def write_telemetry(path: Path, epochs: list[SyntheticEpoch]) -> None:
fields = [
"scenario", "time_s", "abs_sample", "rem_code_phase",
"corrected_abs_sample", "capture_counter_125", "used_satellites",
"capture_correction_ns",
"gdop", "hdop", "vdop", "tdop", "cn0_mean_dbhz",
"cn0_min_dbhz", "pseudorange_noise_rms_m", "clock_bias_sigma_m",
"clock_drift_error_mps", "timing_error_ns", "oscillator_ppm",
"quality_profile",
]
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as output:
writer = csv.DictWriter(
output, fieldnames=fields, lineterminator="\n"
)
writer.writeheader()
for epoch in epochs:
writer.writerow({field: getattr(epoch, field) for field in fields})
def write_sweep(path: Path, results: list[Aggregate]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as output:
writer = csv.DictWriter(
output,
fieldnames=list(Aggregate.__annotations__),
lineterminator="\n",
)
writer.writeheader()
for item in results:
writer.writerow(item.__dict__)
def _fmt(value: float) -> str:
return "n/a" if not math.isfinite(value) else f"{value:.2f}"
def write_report(
path: Path,
telemetry: RealTelemetry,
results: list[Aggregate],
epoch_result,
update_period_s: float = UPDATE_PERIOD_S,
) -> None:
gdop = telemetry.gdop
tracked = telemetry.tracked
current = [item for item in results if item.tuning == "current"]
guarded = [
item for item in results if item.tuning == "robust_noise_guard"
]
lines = [
"# PPS stability analysis: current RTL + 180 ms firmware model",
"",
"This report is generated by `stability_analysis.py`. The absolute "
"LNAV epoch path and the stochastic control loop are evaluated "
"separately so a 20 ms semantic error cannot be hidden by loop noise.",
"",
"## Absolute epoch result",
"",
f"- Current RTL delivered the validated subframe after "
f"{epoch_result.first_subframe_readout_bits} following bits.",
f"- With 180 ms postponement, reference error was "
f"{epoch_result.first_reference_error_ms:.6f} ms.",
f"- DUT minus trusted PPS was "
f"{epoch_result.pps.edges[0].dut_minus_trusted_s * 1.0e9:.2f} ns. "
"That remainder is the model's configured four-PCLK capture pipeline, "
"not a measured board delay.",
"- Production firmware and this analysis both define `POSTPONE_TOW` "
"as 180 ms for the current RTL delivery semantics.",
"",
"## Hardware data used by this synthetic sweep",
"",
f"- Real GPS capture: {len(gdop)} GDOP samples and {len(tracked)} "
"tracked-satellite samples over about five minutes.",
f"- GDOP min/median/p95/max: {_fmt(min(gdop))} / "
f"{_fmt(statistics.median(gdop))} / {_fmt(_percentile(gdop, 0.95))} / "
f"{_fmt(max(gdop))}.",
f"- Tracked satellites: {min(tracked)}..{max(tracked)} "
f"(median {statistics.median(tracked):.0f}).",
f"- Horizontal scatter RMS/p95/max: "
f"{_fmt(telemetry.horizontal_rms_m)} / "
f"{_fmt(telemetry.horizontal_p95_m)} / "
f"{_fmt(telemetry.horizontal_max_m)} m. No surveyed truth was recorded.",
"- The older fusion capture used by this sweep has no ABS_SAMPLE, "
"REM_CODE_PHASE, TDOP/HDOP, C/N0, pseudorange residual, PPS phase/rate/"
"integrator, temperature, or trusted-edge TIC samples. Those columns "
"in the companion CSV are explicitly synthetic. Newer PPS status "
"captures are analyzed separately by `hardware_replay.py` and are not "
"silently mixed into this historical baseline.",
"",
"Synthetic timing uses the board-calibration values already encoded "
"in the PPS tests: TDOP 1.50, GDOP 2.92, clock sigma 15.87 m "
"(52.94 ns), C/N0 40.3/34.8 dB-Hz, drift noise 3.47 m/s, seven-satellite "
"reference point, -291.75 m/s datum range rate, and 1.592 ppm local "
"oscillator. Real GDOP and satellite count replace the geometry fields "
"when available.",
"",
"## Current-loop results",
"",
"`period max` is the worst |edge[n]-edge[n-1]| across all seeds. "
"`absolute p-p` and `10 s wander p-p` expose the slow left/right "
"motion that period RMS hides.",
"",
"| scenario | abs RMS median (ns) | abs p-p median (ns) | "
"10 s wander p-p (ns) | period RMS (ns) | period max (ns) | unlocks |",
"|---|---:|---:|---:|---:|---:|---:|",
]
for item in current:
lines.append(
f"| {item.scenario} | {item.absolute_rms_median_ns:.2f} | "
f"{item.absolute_peak_to_peak_median_ns:.2f} | "
f"{item.wander_10s_peak_to_peak_median_ns:.2f} | "
f"{item.period_rms_median_ns:.2f} | "
f"{item.period_max_worst_ns:.2f} | {item.unlocks_total} |"
)
lines.extend(
[
"",
"The loop does not numerically oscillate or lose lock in these "
"runs. The scope-like symptom is low-frequency phase wander: it "
"can be tens of nanoseconds while one-second period jitter remains "
"below 20 ns. Therefore a `p95 period jitter < 20 ns` assertion is "
"not sufficient for an absolute ±20 ns requirement.",
"",
"## Linear stability",
"",
"The fixed-point coefficients give the following discrete poles "
f"at the {update_period_s * 1000.0:.0f} ms navigation update rate:",
"",
"| profile | natural frequency | damping | poles | max magnitude |",
"|---|---:|---:|---|---:|",
]
)
profile_rows = [("acquire", ACQUIRE_PROFILE)] + [
(name, PROFILES[profile])
for name, profile in (
("precision", Profile.PRECISION),
("robust", Profile.ROBUST),
)
]
for name, profile in profile_rows:
poles = discrete_poles(profile, update_period_s)
pole_text = ", ".join(
f"{pole.real:.6f}{pole.imag:+.6f}j" for pole in poles
)
lines.append(
f"| {name} | {profile.natural_frequency_hz:.3f} Hz | "
f"{profile.damping:.2f} | {pole_text} | "
f"{max(abs(pole) for pole in poles):.6f} |"
)
lines.extend(
[
"",
"All magnitudes are below one. Profile changes preserve the PI "
"integrator, and the RTL snapshots Kp/Ki/max-slew/lock limits "
"together with the next commit, so the current acquire/precision/"
"robust switching is structurally sound. Stable individual modes "
"do not prove arbitrary-switching stability, but the baseline "
"8-update upgrade / 3-update downgrade hysteresis and the stress "
"runs show no coefficient chatter or unlock. Hysteresis and "
"settling counts are scaled to preserve their wall-clock duration "
"with at least two/three independent updates.",
"",
"## Dynamic parameter sweep",
"",
"| tuning | worst period max (ns) | median abs RMS over scenarios "
"(ns) | transitions | unlocks |",
"|---|---:|---:|---:|---:|",
]
)
for tuning in tuning_matrix():
subset = [item for item in results if item.tuning == tuning.name]
median_scenario_rms = statistics.median(
item.absolute_rms_median_ns for item in subset
)
lines.append(
f"| {tuning.name} | "
f"{max(item.period_max_worst_ns for item in subset):.2f} | "
f"{median_scenario_rms:.2f} | "
f"{sum(item.profile_transitions_total for item in subset)} | "
f"{sum(item.unlocks_total for item in subset)} |"
)
current_transition = next(
item for item in current if item.scenario == "quality_transitions"
)
guarded_transition = next(
item for item in guarded if item.scenario == "quality_transitions"
)
scheduled = [
item for item in results
if item.tuning == "quality_scheduled_drift"
]
lines.extend(
[
"",
"The safest small candidate is `robust_noise_guard`: retain the "
"current 0.035 Hz precision loop and 0.012 Hz robust loop, but "
"reduce only the robust-profile WLS residual gain from 1/32 to "
"1/64. In the synthetic quality-transition case its worst period "
f"step changed from {current_transition.period_max_worst_ns:.2f} "
f"to {guarded_transition.period_max_worst_ns:.2f} ns. This is a "
"candidate for hardware A/B testing, not enough evidence for a "
"production constant change.",
"",
"The best synthetic all-scenario period result is "
"`quality_scheduled_drift`: use drift gain 1/2 only with a "
"precision-quality Doppler solution, fall back to the current 1/4 "
"gain in robust quality, and combine that with the 1/64 robust "
"bias gain. Its worst period step was "
f"{max(item.period_max_worst_ns for item in scheduled):.2f} ns "
"with no unlocks. It is the most meaningful dynamic A/B candidate, "
"but the repository has no real drift-covariance or TIC samples "
"with which to validate the threshold.",
"",
"Narrowing precision to 0.012 Hz improves stationary noise but "
"lags even the explicit 0.05 ppm/120 s synthetic oscillator "
"variation. Widening it improves oscillator tracking but passes "
"more WLS/multipath motion. A geometry-only bandwidth scheduler "
"cannot resolve that tradeoff.",
"",
"## Recommended architecture update",
"",
"1. Keep current acquire/precision/robust PI profiles until real "
"TIC and oscillator-temperature data are available. Do not tune "
"from scope appearance alone.",
"2. Log the existing firmware status line at every solve during "
"validation: ABS_SAMPLE, REM_CODE_PHASE, capture sequence/counter, "
"TDOP/HDOP/GDOP, C/N0, pseudorange residual RMS, raw/filtered bias, "
"innovation, bias drift, hardware phase, rate, integrator, profile, "
"lock, FPGA/oscillator temperature, DUT edge and trusted edge.",
"3. A/B test the simulated scheduled filter first: precision "
"drift gain 1/2, robust drift gain 1/4, precision bias gain 1/32, "
"and robust bias gain 1/64. Longer term, use estimated phase/drift "
"covariance to interpolate those software gains within bounds. "
"Keep RTL PI bandwidth changes hysteretic and preserve its "
"integrator as today.",
"4. If temperature/frequency acceleration is the main source, add "
"oscillator temperature feed-forward or a phase/frequency/frequency-"
"drift estimator. Switching PI bandwidth alone either passes WLS "
"wander or lags the oscillator.",
"5. Define acceptance separately: calibrated mean offset, absolute "
"centered error/p-p wander, one-second period error, holdover, and "
"re-lock. A physical max-20 ns claim requires a common-clock TIC "
"run after cable/output-delay calibration; this synthetic test "
"cannot certify it.",
"",
]
)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines), encoding="utf-8")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--duration", type=float, default=300.0)
parser.add_argument("--seeds", type=int, default=24)
parser.add_argument(
"--update-period-ms",
type=float,
default=UPDATE_PERIOD_S * 1000.0,
help="NAV solve period (production-safe examples: 200, 500, 1000)",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path(__file__).resolve().parent / "output/stability",
)
parser.add_argument("--no-write", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.duration < 120.0 or args.seeds < 1:
raise SystemExit("duration must be >= 120 s and seeds must be positive")
update_period_s = args.update_period_ms / 1000.0
nav_ticks = round(update_period_s / NAV_BIT_PERIOD_S)
if (
not math.isfinite(update_period_s)
or update_period_s <= 0.0
or abs(update_period_s - nav_ticks * NAV_BIT_PERIOD_S) > 1.0e-12
or nav_ticks <= 0
or 300 % nav_ticks != 0
):
raise SystemExit(
"update period must be N*20 ms with positive N dividing "
"the 300-bit subframe"
)
software_dir = Path(__file__).resolve().parents[1]
telemetry = load_real_telemetry(software_dir)
if not telemetry.gdop or not telemetry.tracked:
raise SystemExit("real GPS GDOP/satellite capture was not found")
epoch_result = run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_RTL,
postpone_tow_s=0.180,
)
)
analysis_start = max(60.0, args.duration / 3.0)
results: list[Aggregate] = []
telemetry_epochs: list[SyntheticEpoch] = []
for scenario in scenario_matrix():
seed_epochs = [
synthesize_epochs(
telemetry,
scenario,
duration_s=args.duration,
seed=seed,
update_period_s=update_period_s,
)
for seed in range(1, args.seeds + 1)
]
if scenario.name == "scope_like_multipath":
telemetry_epochs = seed_epochs[0]
for tuning in tuning_matrix():
runs = [
measure_trace(
_run_loop(
epochs,
tuning,
update_period_s=update_period_s,
),
after_s=analysis_start,
)
for epochs in seed_epochs
]
results.append(aggregate(scenario, tuning, runs))
print(
"scenario tuning abs_rms "
"abs_pp wander10 period_rms period_max unlock"
)
for item in results:
print(
f"{item.scenario:27s} {item.tuning:22s} "
f"{item.absolute_rms_median_ns:7.2f} "
f"{item.absolute_peak_to_peak_median_ns:7.2f} "
f"{item.wander_10s_peak_to_peak_median_ns:8.2f} "
f"{item.period_rms_median_ns:10.2f} "
f"{item.period_max_worst_ns:10.2f} "
f"{item.unlocks_total:6d}"
)
print(
"current RTL + 180 ms: DUT-trusted = "
f"{epoch_result.pps.edges[0].dut_minus_trusted_s * 1.0e9:.2f} ns"
)
if not args.no_write:
write_telemetry(args.output_dir / "synthetic_telemetry.csv", telemetry_epochs)
write_sweep(args.output_dir / "parameter_sweep.csv", results)
write_report(
args.output_dir / "stability_report.md",
telemetry,
results,
epoch_result,
update_period_s,
)
print(f"wrote {args.output_dir}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,147 @@
from __future__ import annotations
import csv
import math
from pathlib import Path
import tempfile
import unittest
from pps_python.adaptive_pps import (
PROFILES,
Profile,
Q32,
TRACKING_HZ,
)
from pps_end_to_end_validation.hardware_replay import (
UPDATE_PERIOD_S,
expand_samples,
load_firmware_telemetry,
observed_metrics,
replay_firmware_telemetry,
replay_tuning_sensitivity,
)
def _status_line(
index: int,
*,
update_period_s: float = UPDATE_PERIOD_S,
log_interval_updates: int = 10,
) -> str:
phase_ns = 140.0 + 15.0 * math.sin(index * 0.7)
integrator_ppm = -1.560
kp_q46, _, _ = PROFILES[Profile.ROBUST].fixed_coefficients(
update_period_s
)
phase_q32 = round(phase_ns * 1.0e-9 * TRACKING_HZ * Q32)
proportional_q32 = (phase_q32 * kp_q46) >> 46
rate_ppm = (
round(integrator_ppm * 1.0e-6 * Q32) + proportional_q32
) / Q32 * 1.0e6
raw_bias_m = 700_000.0 - 80.0 * (2.0 * index) + 4.0 * math.sin(index)
filtered_bias_m = 700_000.0 - 80.0 * (2.0 * index)
return (
"[NAV] PPS profile=robust mode=tracking locked=1 "
"tdop=1.22 gdop=2.82 clock_sigma=11.24 cn0=42.6/38.4 "
f"innovation={raw_bias_m - filtered_bias_m:.2f}m "
f"raw_bias={raw_bias_m:.3f}m filtered_bias={filtered_bias_m:.3f}m "
"clock_drift=110.000mps bias_drift=-80.000mps predictor=1 "
"eskf_alpha=0.05 eskf_v=0.100/0.050/0x00 "
"pps_v=0.100/0.080/0x00 ref_svid=23 capture_corr=32.00ns "
f"hw_phase={phase_ns:.2f}ns rate_cmd={rate_ppm:.3f}ppm "
f"integ={integrator_ppm:.3f}ppm pulse={100 + 2 * index} "
f"seq={1000 + log_interval_updates * index} status=0x80f"
)
def _write_capture(
path: Path,
count: int = 16,
*,
update_period_s: float = UPDATE_PERIOD_S,
log_interval_updates: int = 10,
) -> None:
location = (
"[LOCATION] source=gps lat=0 lon=0 height=0 "
"gdop=2.82 pdop=2.54 hdop=1.81 vdop=1.78 tdop=1.22 "
"tracked=6 used=5 velocity_valid=1 velocity_fde=0 "
"speed=0.1 velocity_rms=0.08"
)
with path.open("w", encoding="utf-8", newline="") as output:
writer = csv.writer(output, lineterminator="\n")
writer.writerow(("host_monotonic_s", "host_unix_s", "uart_line"))
for index in range(count):
host_s = update_period_s * log_interval_updates * index
writer.writerow((host_s - 0.01, 1000.0 + host_s, location))
writer.writerow((
host_s,
1000.01 + host_s,
_status_line(
index,
update_period_s=update_period_s,
log_interval_updates=log_interval_updates,
),
))
class HardwareReplayTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.capture = Path(self.temporary.name) / "capture.csv"
_write_capture(self.capture)
def tearDown(self) -> None:
self.temporary.cleanup()
def test_uart_parser_and_sequence_expansion(self):
samples = load_firmware_telemetry(self.capture)
self.assertEqual(len(samples), 16)
self.assertEqual(samples[-1].time_s, 30.0)
self.assertEqual(samples[0].tracked_satellites, 6)
self.assertEqual(samples[0].used_satellites, 5)
expanded = expand_samples(samples)
self.assertEqual(len(expanded), 151)
self.assertEqual(sum(interpolated for _, interpolated in expanded), 135)
def test_fixed_point_pi_and_checkpoint_replay(self):
samples = load_firmware_telemetry(self.capture)
observed = observed_metrics(samples)
self.assertEqual(observed.locked_fraction, 1.0)
self.assertLess(observed.pi_rate_reproduction_rms_ppb, 0.6)
points, replay = replay_firmware_telemetry(
samples, warmup_s=5.0
)
self.assertEqual(len(points), 151)
self.assertEqual(replay.unlocks, 0)
self.assertEqual(replay.locked_fraction_after_warmup, 1.0)
self.assertTrue(math.isfinite(replay.modeled_edge_centered_rms_ns))
self.assertGreater(replay.modeled_edge_centered_rms_ns, 0.0)
def test_one_second_cadence_is_inferred_from_uart_sequence(self):
_write_capture(
self.capture,
update_period_s=1.0,
log_interval_updates=2,
)
samples = load_firmware_telemetry(self.capture)
self.assertEqual(samples[-1].time_s, 30.0)
self.assertEqual(len(expand_samples(samples)), 31)
observed = observed_metrics(samples)
self.assertLess(observed.pi_rate_reproduction_rms_ppb, 0.6)
_, replay = replay_firmware_telemetry(samples, warmup_s=5.0)
self.assertEqual(replay.expanded_updates, 31)
self.assertEqual(replay.unlocks, 0)
def test_tuning_sensitivity_keeps_candidates_separate(self):
samples = load_firmware_telemetry(self.capture)
results = replay_tuning_sensitivity(samples, warmup_s=5.0)
self.assertEqual(
[result.tuning for result in results],
["current", "robust_bias_1_64", "drift_gain_1_2"],
)
self.assertTrue(all(result.unlocks == 0 for result in results))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,176 @@
from __future__ import annotations
from pathlib import Path
import re
import unittest
from pps_end_to_end_validation.pps_end_to_end import (
DecoderMode,
EndToEndConfig,
LNAV_WINDOW_BITS,
encode_lnav_subframe,
run_end_to_end,
validate_rtl_window,
)
class LnavDecoderTests(unittest.TestCase):
def make_window(self):
previous_d29 = 0
previous_d30 = 0
current = encode_lnav_subframe(
57_600, 1, previous_d29, previous_d30
)
following = encode_lnav_subframe(
57_601,
2,
current.ending_d29,
current.ending_d30,
)
window = (
previous_d29,
previous_d30,
*current.transmitted_bits,
*following.transmitted_bits[:8],
)
self.assertEqual(len(window), LNAV_WINDOW_BITS)
return current, window
def test_parity_valid_subframe_round_trips_through_rtl_predicate(self):
current, window = self.make_window()
decoded = validate_rtl_window(window)
self.assertIsNotNone(decoded)
self.assertEqual(decoded.z_count, current.z_count)
self.assertEqual(decoded.subframe_id, current.subframe_id)
self.assertEqual(decoded.normalized_words, current.normalized_words)
def test_single_data_bit_corruption_prevents_subframe_interrupt(self):
_, window = self.make_window()
corrupted = list(window)
corrupted[2 + 73] ^= 1
self.assertIsNone(validate_rtl_window(corrupted))
class PpsEndToEndTests(unittest.TestCase):
def test_model_default_matches_production_firmware_postpone_tow(self):
header = (
Path(__file__).resolve().parents[1]
/ "gps_system/header/gps/gps_ip_defines.h"
).read_text(encoding="utf-8")
match = re.search(
r"#define\s+POSTPONE_TOW\s+\(double\)\(([0-9.]+)\)",
header,
)
self.assertIsNotNone(match)
self.assertAlmostEqual(float(match.group(1)), 0.180)
self.assertAlmostEqual(
EndToEndConfig().postpone_tow_s,
float(match.group(1)),
)
def test_bit_generator_recreates_50bps_stream_and_registered_sample_lag(self):
result = run_end_to_end(EndToEndConfig())
self.assertEqual(result.bit_valid_events, 2 + 3 * 300)
self.assertAlmostEqual(result.bit_generator_sample_lag_ms, 1.0, places=6)
def test_current_rtl_validates_on_ninth_following_bit(self):
result = run_end_to_end(EndToEndConfig())
self.assertEqual(result.first_subframe_readout_bits, 9)
self.assertEqual(result.first_nav_solve_bit, 19)
self.assertGreaterEqual(result.subframe_interrupts, 2)
self.assertGreater(result.nav_solve_interrupts, 20)
self.assertEqual(result.firmware_rejected_nav_solves, 0)
def test_non_subframe_divisor_explains_40_bit_build_restriction(self):
# N=40 divides the GPS week but not a 300-bit subframe. Refreshing the
# HOW sample anchor changes timer phase by half a solve period, so the
# model rejects solves. Firmware now prevents this configuration.
self.assertEqual(30_240_000 % 40, 0)
self.assertNotEqual(300 % 40, 0)
result = run_end_to_end(
EndToEndConfig(nav_solve_period_bits=40, subframes=4)
)
self.assertGreater(result.firmware_rejected_nav_solves, 0)
def test_supported_200_500_1000ms_periods_keep_absolute_epoch(self):
for period_bits in (10, 25, 50):
result = run_end_to_end(
EndToEndConfig(
nav_solve_period_bits=period_bits,
subframes=4,
)
)
self.assertEqual(result.firmware_rejected_nav_solves, 0)
self.assertAlmostEqual(
result.expected_semantic_offset_ms, 0.0, places=9
)
self.assertLess(
abs(result.pps.edges[0].dut_minus_trusted_s),
100.0e-9,
)
def test_current_rtl_and_160ms_correction_make_pps_20ms_late(self):
result = run_end_to_end(EndToEndConfig(postpone_tow_s=0.160))
offset_s = result.pps.edges[0].dut_minus_trusted_s
self.assertAlmostEqual(result.expected_semantic_offset_ms, 20.0, places=9)
self.assertGreater(offset_s, 0.019999)
self.assertLess(offset_s, 0.020001)
for edge in result.pps.edges:
self.assertAlmostEqual(edge.dut_minus_trusted_s, offset_s, places=9)
def test_current_bit_validation_and_160ms_remove_lnav_bit_error(self):
result = run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_BIT,
postpone_tow_s=0.160,
)
)
self.assertEqual(result.first_subframe_readout_bits, 8)
self.assertEqual(result.first_nav_solve_bit, 18)
self.assertAlmostEqual(result.expected_semantic_offset_ms, 0.0, places=9)
self.assertLess(abs(result.pps.edges[0].dut_minus_trusted_s), 100.0e-9)
def test_180ms_production_firmware_matches_current_rtl(self):
result = run_end_to_end(EndToEndConfig())
self.assertEqual(result.first_subframe_readout_bits, 9)
self.assertAlmostEqual(result.expected_semantic_offset_ms, 0.0, places=9)
self.assertLess(abs(result.pps.edges[0].dut_minus_trusted_s), 100.0e-9)
def test_timestamped_commit_rejects_cpu_delay_as_phase(self):
fast = run_end_to_end(
EndToEndConfig(firmware_compute_delay_s=0.001)
)
stalled = run_end_to_end(
EndToEndConfig(firmware_compute_delay_s=0.080)
)
self.assertAlmostEqual(
fast.pps.edges[0].dut_minus_trusted_s,
stalled.pps.edges[0].dut_minus_trusted_s,
places=9,
)
self.assertGreater(stalled.pps.commit_age_s, fast.pps.commit_age_s)
def test_vendor_and_dut_fixed_delays_have_expected_sign(self):
result = run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_BIT,
postpone_tow_s=0.160,
dut_output_delay_s=12.0e-9,
trusted_output_delay_s=35.0e-9,
)
)
baseline = run_end_to_end(
EndToEndConfig(
decoder_mode=DecoderMode.CURRENT_BIT,
postpone_tow_s=0.160,
)
)
delta = (
result.pps.edges[0].dut_minus_trusted_s
- baseline.pps.edges[0].dut_minus_trusted_s
)
self.assertAlmostEqual(delta, -23.0e-9, places=10)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,118 @@
from __future__ import annotations
import math
from pathlib import Path
import unittest
from pps_python.adaptive_pps import ACQUIRE_PROFILE, PROFILES, Profile
from pps_end_to_end_validation.stability_analysis import (
discrete_poles,
load_real_telemetry,
measure_trace,
scenario_matrix,
synthesize_epochs,
tuning_matrix,
_run_loop,
)
SOFTWARE_DIR = Path(__file__).resolve().parents[1]
class StabilityAnalysisTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.telemetry = load_real_telemetry(SOFTWARE_DIR)
def test_repository_capture_is_loaded(self):
self.assertEqual(len(self.telemetry.gdop), 989)
self.assertEqual(len(self.telemetry.tracked), 990)
self.assertEqual(set(self.telemetry.tracked), {8})
self.assertAlmostEqual(min(self.telemetry.gdop), 1.8593929921)
self.assertAlmostEqual(max(self.telemetry.gdop), 3.1537716348)
self.assertGreater(self.telemetry.horizontal_rms_m, 8.0)
self.assertLess(self.telemetry.horizontal_rms_m, 9.0)
def test_fixed_point_profiles_have_inside_unit_circle_poles(self):
for profile in (
ACQUIRE_PROFILE,
PROFILES[Profile.PRECISION],
PROFILES[Profile.ROBUST],
):
baseline_poles = discrete_poles(profile)
self.assertTrue(all(abs(pole) < 1.0 for pole in baseline_poles))
for period_s in (0.5, 1.0):
mapped_poles = discrete_poles(profile, period_s)
expected_poles = tuple(
pole ** (period_s / 0.2) for pole in baseline_poles
)
self.assertTrue(all(abs(pole) < 1.0 for pole in mapped_poles))
self.assertLess(
max(
abs(actual - expected)
for actual, expected in zip(
mapped_poles, expected_poles
)
),
1.0e-4,
)
def test_synthetic_telemetry_exercises_wrap_and_physical_fields(self):
epochs = synthesize_epochs(
self.telemetry,
scenario_matrix()[0],
duration_s=120.0,
seed=1,
)
self.assertEqual(len(epochs), 600)
self.assertTrue(any(epochs[i].abs_sample < epochs[i - 1].abs_sample
for i in range(1, len(epochs))))
self.assertTrue(all(0 <= epoch.rem_code_phase <= 1_023_000
for epoch in epochs))
self.assertTrue(all(epoch.used_satellites == 8 for epoch in epochs))
self.assertTrue(all(math.isfinite(epoch.corrected_abs_sample)
for epoch in epochs))
self.assertTrue(all(epoch.capture_correction_ns >= 0.0
for epoch in epochs))
def test_current_loop_locks_without_unlock_in_board_scenario(self):
epochs = synthesize_epochs(
self.telemetry,
scenario_matrix()[0],
duration_s=120.0,
seed=4,
)
trace = _run_loop(epochs, tuning_matrix()[0])
metrics = measure_trace(trace, after_s=60.0)
self.assertLess(metrics.first_lock_s, 15.0)
self.assertEqual(metrics.unlocks, 0)
self.assertLessEqual(metrics.period_max_ns, 16.0 + 1.0e-9)
# Slow absolute motion is deliberately measured separately.
self.assertGreater(
metrics.absolute_peak_to_peak_ns,
metrics.period_max_ns,
)
def test_current_loop_remains_stable_at_supported_slower_cadences(self):
for period_s in (0.5, 1.0):
epochs = synthesize_epochs(
self.telemetry,
scenario_matrix()[0],
duration_s=180.0,
seed=4,
update_period_s=period_s,
)
trace = _run_loop(
epochs,
tuning_matrix()[0],
update_period_s=period_s,
)
metrics = measure_trace(trace, after_s=60.0)
self.assertLess(metrics.first_lock_s, 15.0)
self.assertEqual(metrics.unlocks, 0)
self.assertLessEqual(metrics.period_max_ns, 16.0 + 1.0e-9)
if __name__ == "__main__":
unittest.main()
+99 -11
View File
@@ -9,7 +9,8 @@ davranışlarını taklit eder:
- RF `ABS_SAMPLE` sayacı: `16.367981 MHz`, 32 bit ve wrap eden sayaç.
- Tracking/PPS sayacı: `125 MHz` free-running sayaç.
- Navigation solve periyodu: `NAV_SOLVE_TIMER=10`, yani `200 ms`.
- Varsayılan navigation solve periyodu: `NAV_SOLVE_TIMER=10`, yani `200 ms`;
period-aware loop doğrulaması ayrıca 500 ve 1000 ms'yi kapsar.
- Ortak navigation bit transmit epoch'u ve uyduların farklı propagation
delay'lerle art arda tracking olayı üretmesi.
- Firmware'deki corrected sample denklemi:
@@ -59,6 +60,28 @@ python3 pps_python/simulate_adaptive_pps.py
python3 -m unittest pps_python/test_pps_adaptive.py
```
## Bit-level Linux PPS uçtan uca testi
Bit üretiminden trusted PPS karşılaştırmasına kadar deterministik mutlak zaman
regresyonu ayrı `pps_end_to_end_validation` paketine taşınmıştır. Ayrıntılı
kapsam, 20 ms teşhisi ve donanım doğrulama adımları için
[`pps_end_to_end_validation/README.md`](../pps_end_to_end_validation/README.md)
dosyasına bakın.
Software dizininden çalıştırma:
```bash
python3 -m pps_end_to_end_validation.pps_end_to_end
python3 -m unittest pps_end_to_end_validation/test_pps_end_to_end.py
```
```text
scenario bits correction reference error DUT - trusted PPS
current-rtl/180ms 9 180 ms -0.000032 ms +0.000032 ms
current-rtl/160ms 9 160 ms -20.000032 ms +20.000032 ms
current-bit/160ms 8 160 ms -0.000032 ms +0.000032 ms
```
Kısa veya farklı PI koşusu:
```bash
@@ -145,10 +168,11 @@ bias_drift =
uydunun transmit subframe sınırı etiketidir. Full-subframe interrupt'ının
receiver'a ulaştığı an değildir.
Mevcut hardware, subframe doğrulama/parity akışında takip eden subframe'den
8 bit daha bekler. Navigation data 50 bit/s olduğu için bu sabit readout
gecikmesi `8 × 20 ms = 160 ms` olur ve yazılımda `POSTPONE_TOW=0.160` ile
temsil edilir. Bu yüzden ilk teslim edilen subframe için:
Mevcut hardware, subframe doğrulama/parity akışında eski shift register'i
okuduğu için takip eden subframe'in dokuzuncu bitinde teslim yapar. Navigation
data 50 bit/s olduğu için sabit readout gecikmesi `9 × 20 ms = 180 ms` olur ve
production yazılımda `POSTPONE_TOW=0.180` ile temsil edilir. Bu yüzden ilk
teslim edilen subframe için:
```text
transmit_epoch_TOW = HOW_TOW + POSTPONE_TOW
@@ -165,6 +189,10 @@ epoch-aligned transmit time'ı baremetal navigation katmanında tam bir kez
oluşturmalıdır. PPS APB periferiki kendisine verilen GPS reference time'a
`POSTPONE_TOW` değerini tekrar eklememelidir.
Tracking RTL ileride current-bit candidate window ile sekizinci bitte validate
edecek şekilde düzeltilirse aynı release'de firmware değeri 160 ms'ye geri
alınmalıdır; RTL ve firmware düzeltmeleri birbirinden bağımsız uygulanamaz.
İlk HOW'un hangi kanaldan geleceği sadece uydunun yakınlığına bağlı değildir.
Acquisition tamamlanma anı, bit lock ve bir sonraki tam subframe sınırı çoğu
zaman propagation delay farkından daha baskındır. Monte Carlo startup modeli
@@ -173,8 +201,9 @@ bu üç etkiyi ayrı gösterir.
Önerilen ownership:
1. Raw 125 MHz counter resetten itibaren free-running çalışır.
2. İlk geçerli HOW week/6-second mutlak zaman etiketini sağlar; ilk hardware
delivery epoch'u için 160 ms sabit readout düzeltmesi ayrıca eklenir.
2. İlk geçerli HOW week/6-second mutlak zaman etiketini sağlar; mevcut RTL'nin
ilk hardware delivery epoch'u için 180 ms sabit readout düzeltmesi ayrıca
eklenir.
3. İlk coherent WLS çözümünde minimum corrected-ABS kanalı datum olarak
seçilir; aynı SVID mevcut kaldığı sürece onun uçuş süresi
`STARTOFFSET - b/c` ile elde edilir.
@@ -211,7 +240,7 @@ PI, raw 125 MHz sayacı değiştirmez. Ayrı GPS time accumulator'ın rate word'
ayarlar:
```text
integrator += Ki * phase_error * update_period
integrator += Ki_discrete * phase_error
rate_command = integrator + Kp * phase_error
```
@@ -239,6 +268,64 @@ olması gerekir. Üretim profilleri tek seyrek WLS sıçramasında fiziksel puls
kaçırmamak için lock'u ancak `128 tick = 1.024 us` üstünde bırakır; watchdog,
quality holdover ve slew clamp daha büyük arızaları ayrıca sınırlar.
### NAV solve periyodu değiştiğinde loop dinamiği
`Ki` zaten ayrık-zaman katsayısıdır; aynı sayıyı 200, 500 ve 1000 ms'de
kullanmak aynı loop değildir. Basitçe `Ki`'yi periyotla çarpmak da uzun
periyotlarda mevcut RTL'nin gerçek kutuplarını yeterince doğru korumaz.
Firmware ve Python model şu uyumlu yöntemi kullanır:
1. Kartta doğrulanmış 200 ms Q2.46 katsayılarından RTL'nin iki ayrık kutbu
çıkarılır.
2. Yeni `T` periyodu için her kutup `T / 0.2` kuvvetine yükseltilir.
3. Hedef kutuplardan, RTL'nin önce-integral-sonra-proportional commit sırasına
uygun yeni `Kp` ve `Ki` geri hesaplanır.
Bu yöntem 200 ms'de eski katsayıları bit düzeyinde aynen üretir; 500 ve
1000 ms'de aynı gerçek-zaman damping ve settling davranışını korur:
| NAV periyodu | profil | Kp Q2.46 | Ki Q2.46 |
|---:|---|---:|---:|
| 200 ms | acquire / precision / robust | 764018 / 222838 / 84891 | 64006 / 5445 / 640 |
| 500 ms | acquire / precision / robust | 615780 / 209783 / 82980 | 127492 / 12804 / 1564 |
| 1000 ms | acquire / precision / robust | 447388 / 190239 / 79923 | 178038 / 23149 / 3011 |
Toplam döngü yalnız RTL PI'den oluşmadığı için yazılım clock-bias, Doppler
drift ve fallback drift kazançları da gerçek geçen süreyle
`alpha_T = 1 - (1 - alpha_200ms)^(T/0.2)` biçiminde uygulanır. Quality/profile
histerezisi, lock ve filter-settle sayaçları saniye cinsinden ölçeklenir;
bağımsız örnek güvenliği için sırasıyla en az 2/2/3 update korunur. Reference
watchdog `max(2 s, 3*T)` olur.
Desteklenen build değeri `GPS_NAV_SOLVE_TIMER_TICKS=N` için N, 300 bitlik
LNAV subframe'i bölmelidir. Doğrulanan örnekler N=10, 25 ve 50'dir. N=40,
HOW anchor yenilendiğinde timer fazını kaydırdığı için compile-time reddedilir.
`POSTPONE_TOW=180 ms` NAV solve periyodundan bağımsızdır ve değişmez.
Örnek firmware build'leri:
```bash
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=10 all # 200 ms, varsayılan
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=25 all # 500 ms
make -C gps_system RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany \
GPS_NAV_SOLVE_TIMER_TICKS=50 all # 1000 ms
```
Linux stability sweep'i aynı periyotları doğrudan kabul eder:
```bash
python3 -m pps_end_to_end_validation.stability_analysis \
--update-period-ms 500
```
Kutup eşlemesi deterministik loop dinamiğini korur; daha seyrek WLS ölçümü
aynı gürültü performansını garanti etmez. Örnek sayısı azalınca aliasing,
outlier yakalama gecikmesi ve mutlak faz wander'ı değişebilir. Bu nedenle her
kart/periyot kombinasyonu trusted PPS ile ayrıca ölçülmelidir.
## Adaptif PPS senaryo matrisi
Tam fixed-point/holdover matrisi:
@@ -303,9 +390,10 @@ yapar.
çözümde minimum corrected ABS kanalı seçiliyor ve aynı SVID coherent
snapshot'ta kaldığı sürece datum sabit tutuluyor. O kanalın TOW'u ve aynı
nav-solve olayındaki C125 capture'ı birlikte taşınıyor.
5. `POSTPONE_TOW=0.160`, hardware'ın mevcut subframe'i doğrulamak için takip
eden subframe'den 8 bit daha beklemesinden oluşan sabit readout gecikmesidir.
Uydu konum/saat epoch'u buna göre 160 ms ileri alınır. Bu değer signal
5. `POSTPONE_TOW=0.180`, hardware'ın mevcut subframe'i eski shift register
üzerinden doğrulayıp dokuzuncu takip eden bitte teslim etmesinden oluşan
sabit readout gecikmesidir. Uydu konum/saat epoch'u buna göre 180 ms ileri
alınır. Bu değer signal
flight time veya WLS receiver clock bias ile birleştirilmemelidir. Mevcut
`satpos()` zaten kullanım anında bir kez ekler; PPS peripheral tekrar eklemez.
6. 125 MHz timestamp `ABS_SAMPLE` ve `REM_CODE_PHASE` ile aynı hardware
+268 -30
View File
@@ -8,15 +8,17 @@ rounding, lock acquisition/release and holdover/re-acquisition.
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, replace
from enum import IntEnum
import math
from typing import Iterable
from typing import Iterable, Mapping
TRACKING_HZ = 125_000_000
SPEED_OF_LIGHT_MPS = 299_792_458.0
REFERENCE_WATCHDOG_SEC = 2.0
BASELINE_UPDATE_PERIOD_S = 0.2
MIN_REFERENCE_WATCHDOG_SEC = 2.0
REFERENCE_WATCHDOG_PERIODS = 3.0
MAX_PREDICTION_GAP_SEC = 30.0
Q32 = 1 << 32
Q46 = 1 << 46
@@ -24,6 +26,27 @@ MAX_SIGNED_64 = (1 << 63) - 1
MIN_SIGNED_64 = -(1 << 63)
def _period_updates(
baseline_updates: int,
update_period_s: float,
minimum_updates: int,
) -> int:
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
duration_s = baseline_updates * BASELINE_UPDATE_PERIOD_S
return min(
255,
max(minimum_updates, math.ceil(duration_s / update_period_s - 1e-12)),
)
def _elapsed_gain(baseline_gain: float, elapsed_s: float) -> float:
periods = elapsed_s / BASELINE_UPDATE_PERIOD_S
if abs(periods - 1.0) <= 1e-12:
return baseline_gain
return -math.expm1(periods * math.log1p(-baseline_gain))
class Profile(IntEnum):
HOLDOVER = 0
ROBUST = 1
@@ -58,14 +81,74 @@ class LoopProfile:
software_settle_updates: int
def fixed_coefficients(self, update_period_s: float) -> tuple[int, int, int]:
"""Map the proven 200 ms discrete poles to ``update_period_s``.
The 200 ms result is exactly the legacy production coefficient set.
At another NAV cadence each baseline pole is raised to T/0.2 before
recovering Kp/Ki for the RTL's integrate-then-proportional ordering.
"""
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
omega = 2.0 * math.pi * self.natural_frequency_hz
kp_q46 = round((2.0 * self.damping * omega / TRACKING_HZ) * Q46)
ki_q46 = round(
(omega * omega * update_period_s / TRACKING_HZ) * Q46
baseline_kp = math.floor(
(2.0 * self.damping * omega / TRACKING_HZ) * Q46 + 0.5
) / Q46
baseline_ki = math.floor(
(
omega * omega * BASELINE_UPDATE_PERIOD_S / TRACKING_HZ
) * Q46 + 0.5
) / Q46
baseline_ticks = TRACKING_HZ * BASELINE_UPDATE_PERIOD_S
baseline_sum = 2.0 - baseline_ticks * (
baseline_kp + baseline_ki
)
baseline_product = 1.0 - baseline_ticks * baseline_kp
discriminant = baseline_sum * baseline_sum - 4.0 * baseline_product
period_ratio = update_period_s / BASELINE_UPDATE_PERIOD_S
if discriminant >= 0.0:
root = math.sqrt(discriminant)
pole_a = 0.5 * (baseline_sum + root)
pole_b = 0.5 * (baseline_sum - root)
if pole_a < 0.0 or pole_b < 0.0:
raise ValueError("baseline loop has an unmappable negative pole")
target_a = pole_a**period_ratio
target_b = pole_b**period_ratio
target_sum = target_a + target_b
target_product = target_a * target_b
else:
if baseline_product <= 0.0:
raise ValueError("baseline loop has invalid complex poles")
radius = math.sqrt(baseline_product)
cosine = max(-1.0, min(1.0, baseline_sum / (2.0 * radius)))
angle = math.acos(cosine)
target_radius = radius**period_ratio
target_sum = 2.0 * target_radius * math.cos(
angle * period_ratio
)
target_product = target_radius * target_radius
target_ticks = TRACKING_HZ * update_period_s
kp = (1.0 - target_product) / target_ticks
ki = (1.0 - target_sum + target_product) / target_ticks
if not math.isfinite(kp) or not math.isfinite(ki) or kp < 0 or ki < 0:
raise ValueError("mapped loop coefficients are invalid")
kp_q46 = math.floor(kp * Q46 + 0.5)
ki_q46 = math.floor(ki * Q46 + 0.5)
max_slew_q32 = math.floor(
self.max_slew_ppm * 1.0e-6 * Q32 + 0.5
)
max_slew_q32 = round(self.max_slew_ppm * 1.0e-6 * Q32)
return kp_q46, ki_q46, max_slew_q32
def for_update_period(self, update_period_s: float) -> LoopProfile:
return replace(
self,
lock_acquire_updates=_period_updates(
self.lock_acquire_updates, update_period_s, 2
),
software_settle_updates=_period_updates(
self.software_settle_updates, update_period_s, 3
),
)
PROFILES = {
Profile.HOLDOVER: LoopProfile(
@@ -149,10 +232,12 @@ def classify_quality(metrics: QualityMetrics) -> Profile:
class QualitySelector:
"""Three-profile selector with asymmetric transition hysteresis."""
def __init__(self) -> None:
def __init__(self, update_period_s: float = BASELINE_UPDATE_PERIOD_S) -> None:
self.profile = Profile.HOLDOVER
self.candidate = Profile.HOLDOVER
self.candidate_updates = 0
self.upgrade_updates = _period_updates(8, update_period_s, 2)
self.degrade_updates = _period_updates(3, update_period_s, 2)
def update(self, metrics: QualityMetrics) -> tuple[Profile, bool]:
target = classify_quality(metrics)
@@ -165,7 +250,11 @@ class QualitySelector:
self.candidate_updates = 1
else:
self.candidate_updates += 1
required = 8 if target > self.profile else 3
required = (
self.upgrade_updates
if target > self.profile
else self.degrade_updates
)
if self.candidate_updates >= required:
self.profile = target
self.candidate_updates = 0
@@ -242,17 +331,7 @@ class AdaptiveTrace:
def one_second_edge_jitter_rms(self, *, after_s: float = 0.0) -> float:
"""Return 1 s PPS-edge jitter including the 125 MHz output grid."""
selected = {
round(time_s, 9): (
round(-phase_s * TRACKING_HZ) / TRACKING_HZ
)
for time_s, phase_s, locked in zip(
self.time_s, self.phase_error_s, self.locked
)
if locked
and time_s >= after_s
and abs(time_s - round(time_s)) < 1.0e-9
}
selected = dict(self.integer_second_edge_errors(after_s=after_s))
differences = [
edge_error - selected[round(time_s - 1.0, 9)]
for time_s, edge_error in selected.items()
@@ -264,6 +343,31 @@ class AdaptiveTrace:
sum(value * value for value in differences) / len(differences)
)
def integer_second_edge_errors(
self,
*,
after_s: float = 0.0,
locked_only: bool = True,
) -> list[tuple[float, float]]:
"""Return physical PPS edge error samples on the 125 MHz grid.
Period jitter alone hides low-frequency motion. Exposing the signed
edge error lets stability analyses report absolute RMS, peak-to-peak
wander and moving-average wander alongside cycle-to-cycle jitter.
"""
return [
(
round(time_s, 9),
round(-phase_s * TRACKING_HZ) / TRACKING_HZ,
)
for time_s, phase_s, locked in zip(
self.time_s, self.phase_error_s, self.locked
)
if (locked or not locked_only)
and time_s >= after_s
and abs(time_s - round(time_s)) < 1.0e-9
]
def phase_rms(self, *, after_s: float = 0.0) -> float:
values = [
phase_s
@@ -319,16 +423,48 @@ class FixedPointAdaptivePps:
software_filter: bool = True,
receiver_clock_bias_m: float = 1200.0,
receiver_clock_drift_mps: float = 475.0,
profiles: Mapping[Profile, LoopProfile] | None = None,
acquire_profile: LoopProfile | None = None,
software_drift_gain: float = 1.0 / 4.0,
software_fallback_drift_gain: float = 1.0 / 16.0,
) -> None:
if not math.isfinite(update_period_s) or update_period_s <= 0.0:
raise ValueError("update_period_s must be finite and positive")
self.update_period_s = update_period_s
self.oscillator_fraction = oscillator_ppm * 1.0e-6
self.adaptive = adaptive
self.software_filter = software_filter
self.receiver_clock_bias_m = receiver_clock_bias_m
self.receiver_clock_drift_mps = receiver_clock_drift_mps
self.selector = QualitySelector()
baseline_profiles = dict(PROFILES if profiles is None else profiles)
self.profiles = {
profile: parameters.for_update_period(update_period_s)
for profile, parameters in baseline_profiles.items()
}
missing_profiles = set(Profile) - self.profiles.keys()
if missing_profiles:
raise ValueError(f"missing loop profiles: {sorted(missing_profiles)}")
baseline_acquire_profile = (
ACQUIRE_PROFILE if acquire_profile is None else acquire_profile
)
self.acquire_profile = baseline_acquire_profile.for_update_period(
update_period_s
)
if not 0.0 < software_drift_gain <= 1.0:
raise ValueError("software_drift_gain must be in (0, 1]")
if not 0.0 < software_fallback_drift_gain <= 1.0:
raise ValueError(
"software_fallback_drift_gain must be in (0, 1]"
)
self.software_drift_gain = software_drift_gain
self.software_fallback_drift_gain = software_fallback_drift_gain
self.selector = QualitySelector(update_period_s)
self.reference_watchdog_sec = max(
MIN_REFERENCE_WATCHDOG_SEC,
REFERENCE_WATCHDOG_PERIODS * update_period_s,
)
self.profile = Profile.HOLDOVER if adaptive else Profile.PRECISION
self.parameters = PROFILES[self.profile]
self.parameters = self.profiles[self.profile]
self.kp_q46, self.ki_q46, self.max_slew_q32 = (
self.parameters.fixed_coefficients(update_period_s)
)
@@ -370,16 +506,98 @@ class FixedPointAdaptivePps:
def _phase_q32_to_seconds(value: int) -> float:
return value / (TRACKING_HZ * Q32)
def seed_tracking_state(
self,
*,
time_s: float,
profile: Profile,
phase_error_s: float,
rate_command_ppm: float,
integrator_ppm: float,
software_error_s: float,
software_bias_m: float,
software_clock_drift_mps: float,
software_previous_measurement_m: float,
reference_id: int,
) -> None:
"""Start a replay from a captured, already-locked board snapshot.
Normal simulations deliberately start unaligned and exercise coarse
acquisition. Hardware logs, however, are commonly captured after the
board has been tracking for minutes. Replaying such a log from reset
introduces an artificial acquisition transient and clears the PI
integrator that was present on the board. This method imports only
state that the production diagnostic registers and firmware status
expose; subsequent updates still use the normal bit-accurate path.
The method must be called before any observation has been recorded.
It is a model facility and has no firmware/RTL side effect.
"""
finite_values = (
time_s,
phase_error_s,
rate_command_ppm,
integrator_ppm,
software_error_s,
software_bias_m,
software_clock_drift_mps,
software_previous_measurement_m,
)
if self.trace.time_s:
raise ValueError("tracking state must be seeded before replay")
if not all(math.isfinite(value) for value in finite_values):
raise ValueError("tracking state values must be finite")
if time_s < 0.0:
raise ValueError("tracking state time must be non-negative")
if profile == Profile.HOLDOVER:
raise ValueError("a locked snapshot cannot use holdover profile")
self.current_time_s = time_s
self.raw_tick_remainder = 0.0
self.phase_q32 = self._seconds_to_phase_q32(phase_error_s)
self.profile = profile
self.selector.profile = profile
self.selector.candidate = profile
self.selector.candidate_updates = 0
self.aligned = True
self.locked = True
self.lock_good_updates = 0
self.reference_watchdog_holdover = False
self._select_loop_parameters()
self.integrator_q32 = self._clamp(
round(integrator_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.rate_word_q32 = self._clamp(
round(rate_command_ppm * 1.0e-6 * Q32), self.max_slew_q32
)
self.software_error_s = software_error_s
self.software_bias_m = software_bias_m
self.software_clock_drift_mps = software_clock_drift_mps
self.software_previous_measurement_m = (
software_previous_measurement_m
)
self.software_drift_anchor_time_s = time_s
self.software_drift_anchor_measurement_m = (
software_previous_measurement_m
)
self.software_drift_initialized = True
self.software_updates = max(
1, self.parameters.software_settle_updates
)
self.software_reference_id = reference_id
self.last_observation_time_s = time_s
def _apply_profile(self, profile: Profile) -> None:
self.profile = profile
self._select_loop_parameters()
def _select_loop_parameters(self) -> None:
parameters = (
ACQUIRE_PROFILE
self.acquire_profile
if self.adaptive and
self.profile != Profile.HOLDOVER and not self.locked
else PROFILES[self.profile]
else self.profiles[self.profile]
)
if parameters == self.parameters:
return
@@ -536,20 +754,30 @@ class FixedPointAdaptivePps:
return False, False
innovation_m = max(-300.0, min(300.0, innovation_m))
effective_bias_gain = _elapsed_gain(
self.parameters.software_bias_gain, interval_s
)
effective_drift_gain = _elapsed_gain(
self.software_drift_gain, interval_s
)
effective_fallback_drift_gain = _elapsed_gain(
self.software_fallback_drift_gain, interval_s
)
self.software_bias_m = (
predicted_bias_m
+ self.parameters.software_bias_gain * innovation_m
+ effective_bias_gain * innovation_m
)
if observation.clock_drift_valid:
self.software_clock_drift_mps += 0.25 * (
self.software_clock_drift_mps += effective_drift_gain * (
measured_drift_mps - self.software_clock_drift_mps
)
else:
if self.software_updates < self.parameters.software_settle_updates:
self.software_clock_drift_mps = fallback_drift_mps
else:
self.software_clock_drift_mps += (1.0 / 16.0) * (
fallback_drift_mps - self.software_clock_drift_mps
self.software_clock_drift_mps += (
effective_fallback_drift_gain *
(fallback_drift_mps - self.software_clock_drift_mps)
)
self.software_previous_measurement_m = measured_bias_m
self.software_error_s = -(
@@ -614,7 +842,7 @@ class FixedPointAdaptivePps:
if (
self.last_observation_time_s is None
or observation.time_s - self.last_observation_time_s
> REFERENCE_WATCHDOG_SEC
> self.reference_watchdog_sec
):
self.reference_watchdog_holdover = True
self._record(observation, 0, 0)
@@ -630,7 +858,7 @@ class FixedPointAdaptivePps:
if (
self.last_observation_time_s is None
or observation.time_s - self.last_observation_time_s
> REFERENCE_WATCHDOG_SEC
> self.reference_watchdog_sec
):
self.reference_watchdog_holdover = True
self._record(observation, 0, 0)
@@ -700,18 +928,28 @@ def run_observations(
observations: Iterable[Observation],
*,
finish_time_s: float,
update_period_s: float = BASELINE_UPDATE_PERIOD_S,
oscillator_ppm: float = 1.65,
adaptive: bool = True,
software_filter: bool = True,
receiver_clock_bias_m: float = 1200.0,
receiver_clock_drift_mps: float = 475.0,
profiles: Mapping[Profile, LoopProfile] | None = None,
acquire_profile: LoopProfile | None = None,
software_drift_gain: float = 1.0 / 4.0,
software_fallback_drift_gain: float = 1.0 / 16.0,
) -> AdaptiveTrace:
model = FixedPointAdaptivePps(
update_period_s=update_period_s,
oscillator_ppm=oscillator_ppm,
adaptive=adaptive,
software_filter=software_filter,
receiver_clock_bias_m=receiver_clock_bias_m,
receiver_clock_drift_mps=receiver_clock_drift_mps,
profiles=profiles,
acquire_profile=acquire_profile,
software_drift_gain=software_drift_gain,
software_fallback_drift_gain=software_fallback_drift_gain,
)
for observation in observations:
model.update(observation)
+8 -8
View File
@@ -338,7 +338,7 @@ def page_executive(report: Report) -> None:
ax,
0.55,
r"T_{GPS,k}=T_{TX,k}+S-\frac{b_k}{c}",
"Minimum corrected-ABS kanalının receive GPS zamanı; ilk delivery için TTX,k = HOW + 0.160 s",
"Minimum corrected-ABS kanalının receive GPS zamanı; ilk delivery için TTX,k = HOW + 0.180 s",
)
report.equation(
ax,
@@ -455,7 +455,7 @@ def page_lnav_timeline(report: Report) -> None:
ax.text(x + 0.039, 0.58, label, color="white" if index < 2 else INK, ha="center", va="center", fontsize=7.5, weight="bold" if index < 2 else "normal")
report.arrow(ax, (0.205, 0.545), (0.86, 0.49), color=ORANGE, text="HOW Z-count: sonraki 6 s subframe sınırının transmit-time etiketi", rad=-0.12)
ax.add_patch(FancyBboxPatch((0.18, 0.445), 0.64, 0.045, boxstyle="round,pad=0.006", facecolor=PALE_ORANGE, edgecolor=ORANGE))
ax.text(0.50, 0.467, "Hardware takip eden subframe'den 8 bit bekler: 8 × 20 ms = POSTPONE_TOW = 160 ms", fontsize=7.6, color=ORANGE, ha="center", va="center", weight="bold")
ax.text(0.50, 0.467, "Mevcut hardware 9. takip eden bitte teslim eder: 9 × 20 ms = POSTPONE_TOW = 180 ms", fontsize=7.6, color=ORANGE, ha="center", va="center", weight="bold")
report.box(
ax,
@@ -518,7 +518,7 @@ def page_tx_rx(report: Report) -> None:
color=ORANGE,
)
report.equation(ax, 0.42, r"T_{RX,i}=T_{HOW}+\frac{\rho_i+I_i+T_i}{c}-\delta t_{SV,i}", "Aynı transmit sınırı her kanalda farklı receive timestamp üretir")
ax.text(0.50, 0.325, r"Hardware delivery epoch: $T_{event,i}=T_{HOW}+0.160\,s+\tau_i$", fontsize=9.2, color=ORANGE, ha="center", weight="bold")
ax.text(0.50, 0.325, r"Hardware delivery epoch: $T_{event,i}=T_{HOW}+0.180\,s+\tau_i$", fontsize=9.2, color=ORANGE, ha="center", weight="bold")
report.box(
ax,
0.07,
@@ -557,7 +557,7 @@ def page_project_map(report: Report) -> None:
(0.54, 0.54, 0.18, 0.10, "Linked-list state", "TOW owner\nsequence + sample", LIGHT, NAVY),
(0.28, 0.54, 0.20, 0.10, "Coherent snapshot", "25 ms window\nmodulo unwrap", PALE_GREEN, GREEN),
(0.06, 0.54, 0.17, 0.10, "Pseudorange", "minimum +\n68.802 ms", PALE_GREEN, GREEN),
(0.06, 0.34, 0.17, 0.10, "Satellite model", "TOW + 160 ms\nephemeris/clock", PALE_BLUE, BLUE),
(0.06, 0.34, 0.17, 0.10, "Satellite model", "TOW + 180 ms\nephemeris/clock", PALE_BLUE, BLUE),
(0.32, 0.34, 0.23, 0.10, "Weighted LS", "[X Y Z b]\niono + tropo + URA/el", PALE_GREEN, GREEN),
(0.64, 0.34, 0.30, 0.10, "Bugünkü çıkış", "position log + static last_solution[4]\nclock bias dışarı taşınmıyor", "#FBEAEA", RED),
]
@@ -796,7 +796,7 @@ def page_counter_mapping(report: Report) -> None:
fig, ax = report.new_page("GPS time ile 125 MHz counter mapping'i")
ax.text(0.08, 0.84, "İlk coherent WLS epoch'u k", fontsize=11, color=NAVY, weight="bold")
boxes = [
(0.07, 0.68, 0.25, 0.11, "Transmit etiketi", "İlk delivery: HOW + 0.160 s\nsonra nav-solve epoch", PALE_BLUE, BLUE),
(0.07, 0.68, 0.25, 0.11, "Transmit etiketi", "İlk delivery: HOW + 0.180 s\nsonra nav-solve epoch", PALE_BLUE, BLUE),
(0.375, 0.68, 0.25, 0.11, "WLS flight time", "τmin = S − b/c\nall-satellite solution", PALE_GREEN, GREEN),
(0.68, 0.68, 0.25, 0.11, "Receive reference", "T_GPS,k = T_TX,k + τmin", PALE_ORANGE, ORANGE),
(0.375, 0.47, 0.25, 0.11, "Hardware timestamp", "C125,k\naynı ABS/REM event", PALE_BLUE, BLUE),
@@ -1304,7 +1304,7 @@ def page_open_points(report: Report) -> None:
fig, ax = report.new_page("Projede ölçülerek kapatılacak açık noktalar")
points = [
("HOW boundary semantiği", "parse_subframe Z-count × 6 yapıyor. Full-subframe interrupt, bit_gen ABS_SAMPLE ve gerçek transmit next-subframe sınırı arasındaki sabit pipeline cycle sayısı scope/ILA ile ölçülmeli."),
("POSTPONE_TOW = 0.160", "Doğrulanmış hardware readout gecikmesidir: takip eden subframe'den 8 bit beklenir; 50 bit/s hızda 160 ms. Mevcut satpos() kullanım anında bir kez ekler; PPS peripheral tekrar eklemez."),
("POSTPONE_TOW = 0.180", "Mevcut RTL readout gecikmesidir: doğrulama dokuzuncu takip eden bitte yapılır; 50 bit/s hızda 180 ms. Mevcut satpos() kullanım anında bir kez ekler; PPS peripheral tekrar eklemez."),
("RF ↔ 125 MHz bağı", "Aynı PLL/reference mı, bağımsız mı? Ortak drift ve independent jitter ölçülmeli; cross-timestamp uncertainty buna göre bütçelenmeli."),
("Cold-start WLS timing", "Dört uyduda vertical/clock korelasyonu yüksek. Coarse alignment yapılabilir; PPS quality düşük işaretlenmeli ve PI bandwidth agresif seçilmemeli."),
("Atmosfer/multipath", "Iono/tropo modele girse de residual b ve position'a sızar. Timing-only mode veya sabit/smoothed position ileride clock çözümünü iyileştirebilir."),
@@ -1361,7 +1361,7 @@ def page_conclusion(report: Report) -> None:
0.80,
0.09,
"En önemli mimari karar",
"İlk uydu PPS'i başlatmaz. Baremetal HOW'a 160 ms readout düzeltmesini bir kez uygular; ilk coherent WLS bu etiketi free-running 125 MHz counter'a bağlar. PPS peripheral 160 ms'yi tekrar eklemez.",
"İlk uydu PPS'i başlatmaz. Baremetal HOW'a 180 ms readout düzeltmesini bir kez uygular; ilk coherent WLS bu etiketi free-running 125 MHz counter'a bağlar. PPS peripheral 180 ms'yi tekrar eklemez.",
face=NAVY,
edge=NAVY,
title_color="#8ED6E8",
@@ -1414,7 +1414,7 @@ def generate(output_pdf: Path) -> None:
report,
"İlk HOW neden PPS receive-now değildir?",
required_plots[4],
"Sentetik acquisition/subframe Monte Carlo: hardware delivery, takip eden subframe'in 8 biti nedeniyle HOW sınırından 160 ms ve signal flight time kadar sonradır. HOW +160 ms +68.802 ms kaba yaklaşım bırakır; WLS flight-time + paired timestamp doğru handover'dır.",
"Sentetik acquisition/subframe Monte Carlo: mevcut hardware delivery, dokuzuncu takip eden bit nedeniyle HOW sınırından 180 ms ve signal flight time kadar sonradır. HOW +180 ms +68.802 ms kaba yaklaşım bırakır; WLS flight-time + paired timestamp doğru handover'dır.",
)
plot_page(
report,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -4,18 +4,18 @@ epochs: 900
raw WLS bias/c range: -5.888015 .. 0.673753 ms
derived GPS reference RMS error: 60.522 ns
capture-to-commit delay mean/p99: 21.600 / 31.529 ms
first HOW used as receive-now, median phase error: -229.876 ms
first HOW + 160 ms + 68.802 ms, median phase error: -1.074 ms
first HOW used as receive-now, median phase error: -249.876 ms
first HOW + 180 ms + 68.802 ms, median phase error: -1.074 ms
4 satellites: position RMS=103.877 m, timing RMS=144.920 ns
5 satellites: position RMS=20.194 m, timing RMS=45.603 ns
7 satellites / drop-reacquisition: position RMS=14.626 m, timing RMS=36.888 ns
timestamped PI: tail phase RMS=274.599 ns, |phase| p95=409.200 ns, PPS RMS=815.734 ns, rejected stale commits=0
timestamped, PI disabled: tail phase RMS=233591.232 ns, |phase| p95=302909.699 ns, PPS RMS=181359.923 ns, rejected stale commits=0
timestamped PI: tail phase RMS=274.256 ns, |phase| p95=410.422 ns, PPS RMS=815.642 ns, rejected stale commits=0
timestamped, PI disabled: tail phase RMS=233589.285 ns, |phase| p95=302907.720 ns, PPS RMS=181358.257 ns, rejected stale commits=0
untagged APB-now PI: tail phase RMS=18651473.186 ns, |phase| p95=19146158.593 ns, PPS RMS=18060541.154 ns, rejected stale commits=0
Interpretation:
- bias/c is millisecond-scale because it contains the 68.802 ms synthetic datum and the minimum satellite path.
- epoch-aligned TOW (first delivery: HOW + 160 ms) + STARTOFFSET - bias/c reconstructs GPS time at the paired timestamp.
- epoch-aligned TOW (first delivery: HOW + 180 ms) + STARTOFFSET - bias/c reconstructs GPS time at the paired timestamp.
- treating that old reference as valid at APB arrival converts software latency into PPS phase error.
- timestamping removes that phase misassociation; control application delay remains only as loop delay.
- baremetal applies the fixed 160 ms readout correction once; the PPS peripheral must not add it again.
- baremetal applies the fixed 180 ms readout correction once; the PPS peripheral must not add it again.
+1 -1
View File
@@ -30,7 +30,7 @@ CODE_FREQ_BASIS = 1.023e6
START_OFFSET_MS = 68.802
START_OFFSET_S = START_OFFSET_MS / 1000.0
NAV_SOLVE_PERIOD_S = 10 * 0.020
POSTPONE_TOW_S = 0.160
POSTPONE_TOW_S = 0.180
OMEGA_EARTH = 7.2921151467e-5
WEEK_S = 604_800.0
+7 -6
View File
@@ -298,8 +298,9 @@ def simulate_startup_tow(
acquisition_ready = rng.uniform(0.0, 18.0, size=(trial_count, len(satellites)))
complete_subframe_tx = (np.floor(acquisition_ready / 6.0) + 2.0) * 6.0
# The current hardware validates/delivers the completed subframe only
# after receiving eight bits from the following subframe. At 50 bit/s
# this is a deterministic 160 ms readout delay (POSTPONE_TOW), distinct
# on the ninth following bit because validation reads the old shift
# register. At 50 bit/s
# this is a deterministic 180 ms readout delay (POSTPONE_TOW), distinct
# from satellite signal flight time.
how_interrupt_rx = (
complete_subframe_tx + POSTPONE_TOW_S + flight_times[None, :]
@@ -354,7 +355,7 @@ def plot_startup_tow(output_dir: Path, startup: dict[str, np.ndarray]) -> None:
alpha=0.8,
)
axes[1, 0].set_xlabel("Phase error [ms]")
axes[1, 0].set_title("Provisional: HOW + 160 ms + 68.802 ms")
axes[1, 0].set_title("Provisional: HOW + 180 ms + 68.802 ms")
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].hist(
@@ -397,7 +398,7 @@ def build_summary(
f"derived GPS reference RMS error: {finite_rms(timing_error)*1e9:.3f} ns",
f"capture-to-commit delay mean/p99: {np.mean(latency)*1e3:.3f} / {np.percentile(latency, 99)*1e3:.3f} ms",
f"first HOW used as receive-now, median phase error: {np.median(startup['how_as_receive_error_s'])*1e3:.3f} ms",
f"first HOW + 160 ms + 68.802 ms, median phase error: {np.median(startup['how_plus_pipeline_startoffset_error_s'])*1e3:.3f} ms",
f"first HOW + 180 ms + 68.802 ms, median phase error: {np.median(startup['how_plus_pipeline_startoffset_error_s'])*1e3:.3f} ms",
]
for start, stop, label in [
(0.0, 20.0, "4 satellites"),
@@ -426,10 +427,10 @@ def build_summary(
"",
"Interpretation:",
"- bias/c is millisecond-scale because it contains the 68.802 ms synthetic datum and the minimum satellite path.",
"- epoch-aligned TOW (first delivery: HOW + 160 ms) + STARTOFFSET - bias/c reconstructs GPS time at the paired timestamp.",
"- epoch-aligned TOW (first delivery: HOW + 180 ms) + STARTOFFSET - bias/c reconstructs GPS time at the paired timestamp.",
"- treating that old reference as valid at APB arrival converts software latency into PPS phase error.",
"- timestamping removes that phase misassociation; control application delay remains only as loop delay.",
"- baremetal applies the fixed 160 ms readout correction once; the PPS peripheral must not add it again.",
"- baremetal applies the fixed 180 ms readout correction once; the PPS peripheral must not add it again.",
]
)
return "\n".join(lines) + "\n"
@@ -6,6 +6,7 @@ import unittest
from pps_python.adaptive_pps import (
ACQUIRE_PROFILE,
FixedPointAdaptivePps,
Observation,
Profile,
PROFILES,
@@ -59,6 +60,61 @@ def noisy_observations(
class AdaptivePpsTests(unittest.TestCase):
def test_period_aware_coefficients_match_firmware(self):
expected = {
0.2: {
"acquire": (764018, 64006),
Profile.PRECISION: (222838, 5445),
Profile.ROBUST: (84891, 640),
},
0.5: {
"acquire": (615780, 127492),
Profile.PRECISION: (209783, 12804),
Profile.ROBUST: (82980, 1564),
},
1.0: {
"acquire": (447388, 178038),
Profile.PRECISION: (190239, 23149),
Profile.ROBUST: (79923, 3011),
},
}
for period_s, coefficients in expected.items():
self.assertEqual(
ACQUIRE_PROFILE.fixed_coefficients(period_s)[:2],
coefficients["acquire"],
)
for profile in (Profile.PRECISION, Profile.ROBUST):
self.assertEqual(
PROFILES[profile].fixed_coefficients(period_s)[:2],
coefficients[profile],
)
def test_wall_clock_hysteresis_and_settling_scale_with_period(self):
model_500ms = FixedPointAdaptivePps(update_period_s=0.5)
self.assertEqual(model_500ms.selector.upgrade_updates, 4)
self.assertEqual(model_500ms.selector.degrade_updates, 2)
self.assertEqual(
model_500ms.profiles[Profile.PRECISION].software_settle_updates,
5,
)
self.assertEqual(
model_500ms.profiles[Profile.ROBUST].lock_acquire_updates,
4,
)
model_1s = FixedPointAdaptivePps(update_period_s=1.0)
self.assertEqual(model_1s.selector.upgrade_updates, 2)
self.assertEqual(model_1s.selector.degrade_updates, 2)
self.assertEqual(
model_1s.profiles[Profile.PRECISION].software_settle_updates,
3,
)
self.assertEqual(
model_1s.profiles[Profile.ROBUST].lock_acquire_updates,
2,
)
self.assertEqual(model_1s.reference_watchdog_sec, 3.0)
def test_board_calibrated_doppler_predictor_reduces_pps_error(self):
"""Genesys2 WLS statistics: predictor-on must beat fallback-only."""
quality_metrics = QualityMetrics(
+4 -4
View File
@@ -29,17 +29,17 @@ done
export Z3_LIB_DIR="${Z3_LIB_DIR:-$_gps_z3_dir}"
_gps_riscv_gcc=""
if [[ -n "${TOOLCHAIN:-}" ]]; then
_gps_candidates=("$TOOLCHAIN")
elif [[ -n "${RISCV_TOOLCHAIN:-}" ]]; then
if [[ -n "${RISCV_TOOLCHAIN:-}" ]]; then
_gps_candidates=("$RISCV_TOOLCHAIN" "$RISCV_TOOLCHAIN/bin")
elif [[ -n "${TOOLCHAIN:-}" ]]; then
_gps_candidates=("$TOOLCHAIN")
else
_gps_candidates=(
"$GPS_ROOT/tools/local/riscv/bin"
/tools/toolchains-medany/bin
/tools/toolchains/rvtc-medany/bin
/tools/toolchain/bin
/tools/toolchains/bin
/tools/toolchains/rvtc-medany/bin
)
fi
+12
View File
@@ -0,0 +1,12 @@
/out/
/flow/efinity/ti375c529_rocket.isf
/flow/efinity/ti375c529_rocket.peri.xml
/flow/efinity/ti375c529_rocket.pt.sdc
/flow/efinity/ti375c529_rocket.pt.rpt
/flow/efinity/ti375c529_rocket.pinout.*
/flow/efinity/ti375c529_rocket.interface.csv
/flow/efinity/ti375c529_rocket_template.v
/flow/efinity/outflow/
/flow/efinity/work_pnr/
/flow/efinity/work_syn/
/flow/efinity/RUNPNR_*
+408
View File
@@ -0,0 +1,408 @@
SHELL := /bin/bash
.SHELLFLAGS := -euo pipefail -c
.ONESHELL:
FPGA_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
REPO_DIR := $(abspath $(FPGA_DIR)/../../..)
SOC_SW_DIR := $(REPO_DIR)/soc/software
SOC_OUT_DIR := $(REPO_DIR)/soc/out
HW_DIR := $(REPO_DIR)/hardware
HW_GENERATED_DIR := $(HW_DIR)/generated
GPS_SYSTEM_DIR ?= $(HW_DIR)/external/gps/rtl
FLOW_DIR := $(FPGA_DIR)/flow/efinity
DEBUG_DIR := $(FPGA_DIR)/flow/debug
TOOL_SCRIPT_DIR := $(FPGA_DIR)/tools
OUT_DIR := $(FPGA_DIR)/out
LOG_DIR := $(OUT_DIR)/logs
include $(REPO_DIR)/tools/project.mk
BOARD ?= ti375c529
DEVICE ?= Ti375C529
TIMING_MODEL ?= C4
SOC_CLK_HZ ?= 100000000
GPS_ACQ_CLK_HZ ?= 100000000
GPS_TRCK_CLK_HZ ?= 125000000
GPS_RF_CLK_HZ ?= 16367981
RF_INPUT_MODE ?= pmod
UART_PORT ?= /dev/ttyUSB2
UART_BAUD ?= 460800
OPENOCD ?= $(shell command -v openocd)
OPENOCD_ADAPTER_SPEED ?= 1000
GDB_HOST ?= localhost
GDB_PORT ?= 3333
GDB ?= $(firstword $(wildcard /tools/toolchain/bin/riscv64-unknown-elf-gdb) $(CROSS)gdb)
# Leave empty for Efinity's board-profile auto-detection. Boards without an
# FTDI serial number require a bus/device-qualified URL, which changes after
# re-enumeration; users with multiple boards can override this explicitly.
FTDI_URL ?=
BSP_UART_CLOCK_HZ ?= $(SOC_CLK_HZ)
BSP_UART_BAUDRATE ?= $(UART_BAUD)
BSP_CLINT_TIMEBASE_HZ ?= 1000000
DTIM_BASE ?= 0x80000000
DTIM_SIZE ?= 0x00100000
# The compressed Ti375 configuration currently occupies about 5.2 MB.
# Keep the application on a 64-KiB boundary with ample growth margin.
APP_FLASH_OFFSET ?= 0x00800000
FLASH_SIZE ?= 0x04000000
GPS_DEBUG ?= 1
GPS_DEBUG_LEVEL ?= 4
GPS_ESKF_ENABLE ?= 1
IMU_SENSOR ?= auto
app ?=
ifneq ($(strip $(app)),)
APP ?= $(app)
else
APP ?= hello_uart
endif
FIRRTL_DIR ?= $(HW_GENERATED_DIR)/ti375c529-soc
VERILOG_DIR ?= $(HW_GENERATED_DIR)/ti375c529-soc-verilog
WRAPPER := $(OUT_DIR)/build/generated/ti375c529_rocket_top.sv
GPS_EFINITY_DIR := $(OUT_DIR)/build/generated/gps_efinity
SOURCE_LIST := $(OUT_DIR)/build/efinity_sources.f
PROJECT_XML := $(FLOW_DIR)/ti375c529_rocket.xml
INTERFACE_SOURCE := $(HW_DIR)/fpga/ti375c529/constraints/ti375c529_rocket.isf
INTERFACE_SCRIPT := $(FLOW_DIR)/ti375c529_rocket.isf
BIT_FILE ?= $(OUT_DIR)/bitstream/ti375c529_rocket.bit
CONFIG_HEX ?= $(OUT_DIR)/bitstream/ti375c529_rocket.hex
APP_ELF ?= $(SOC_OUT_DIR)/software/$(APP)/$(APP).elf
APP_FLASH_IMAGE ?= $(SOC_OUT_DIR)/flash/$(APP).flash.bin
FLASH_DIR := $(OUT_DIR)/flash
FLASH_REGION_DIR := $(FLASH_DIR)/regions
APP_HEX ?= $(FLASH_REGION_DIR)/app.hex
FLASH_HEX ?= $(FLASH_DIR)/ti375c529_rocket.hex
FLASH_MANIFEST ?= $(FLASH_DIR)/ti375c529_rocket.json
FLASH_LOADER ?= $(EFINITY_HOME)/pgm/fli/titanium/u006A0A79.bit
FLASH_VERIFY ?= onchipx2
OPENOCD_CFG ?= $(DEBUG_DIR)/openocd_ti375c529_bscan.cfg
GDB_LOAD_SCRIPT ?= $(DEBUG_DIR)/gdb_load_and_run.gdb
GDB_ATTACH_SCRIPT ?= $(DEBUG_DIR)/gdb_attach.gdb
.PHONY: help all fpga-config detect software chisel rtl soc interface map bitstream \
program-fpga debug-openocd debug-load debug-gdb uart-listen \
flash-image flash-regions program-flash program-flash-elf \
program-flash-bitstream program-flash-regions program-flash-all \
program-flash-combined program-flash-loader program-app program-bitstream \
clean clean-sw clean-fpga FORCE
help:
@printf '%s\n' \
'Ti375C529 Efinity targets:' \
' make detect Scan USB and read the Ti375 JTAG IDCODE' \
' make software Build the Rocket boot ROM and selected RV64 app' \
' make chisel Emit Rocket FIRRTL with the existing FPGA config' \
' make rtl Emit split SystemVerilog, Efinix shell, and file list' \
' make interface Generate Efinity pin/PLL/JTAG interface data' \
' make map Run Efinity synthesis only' \
' make bitstream Run the complete Efinity flow' \
' make program-fpga Load BIT_FILE into Ti375 SRAM over JTAG' \
' make flash-image Generate combined Lower-flash Efinity HEX' \
' make program-flash Program only the application flash region' \
' make program-flash-bitstream' \
' Program only the configuration region' \
' make program-flash-combined' \
' Program configuration + application HEX' \
' make debug-openocd Start Rocket DMI/GDB server through JTAG_USER1' \
' make debug-load Load APP ELF into DTIM through GDB' \
' make uart-listen Listen to FTDI UART interface 2' \
' make fpga-config Print resolved tools, clocks, files, and USB config'
all: bitstream
fpga-config:
@printf '%s\n' \
'Ti375C529 FPGA config:' \
' EFINITY_HOME=$(EFINITY_HOME)' \
' EFX_RUN=$(EFX_RUN)' \
' EFX_PGM=$(EFX_PGM)' \
' OPENOCD=$(OPENOCD)' \
' GDB=$(GDB)' \
' DEVICE=$(DEVICE)' \
' TIMING_MODEL=$(TIMING_MODEL)' \
' SOC_CLK_HZ=$(SOC_CLK_HZ)' \
' GPS_ACQ_CLK_HZ=$(GPS_ACQ_CLK_HZ)' \
' GPS_TRCK_CLK_HZ=$(GPS_TRCK_CLK_HZ)' \
' GPS_RF_CLK_HZ=$(GPS_RF_CLK_HZ)' \
' RF_INPUT_MODE=$(RF_INPUT_MODE)' \
' BIT_FILE=$(BIT_FILE)' \
' CONFIG_HEX=$(CONFIG_HEX)' \
' FLASH_HEX=$(FLASH_HEX)' \
' APP_FLASH_OFFSET=$(APP_FLASH_OFFSET)' \
' FLASH_LOADER=$(FLASH_LOADER)' \
' FTDI_URL=$(FTDI_URL)' \
' UART_PORT=$(UART_PORT)' \
' UART_BAUD=$(UART_BAUD)'
detect:
@set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
"$(EFX_PGM)" --scan_usb
software:
@$(MAKE) -C "$(SOC_SW_DIR)" \
TOOLCHAIN="$(TOOLCHAIN)" APP="$(APP)" \
APP_FLASH_OFFSET="$(APP_FLASH_OFFSET)" \
BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" \
BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" \
GPS_SYSTEM_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" \
BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" \
BSP_DTIM_BASE="$(DTIM_BASE)" BSP_DTIM_SIZE="$(DTIM_SIZE)" \
GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" \
GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" \
bootloader flash-image
$(APP_ELF): FORCE
@$(MAKE) -C "$(SOC_SW_DIR)" \
TOOLCHAIN="$(TOOLCHAIN)" APP="$(APP)" \
BSP_UART_CLOCK_HZ="$(BSP_UART_CLOCK_HZ)" \
BSP_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" \
GPS_SYSTEM_UART_BAUDRATE="$(BSP_UART_BAUDRATE)" \
BSP_CLINT_TIMEBASE_HZ="$(BSP_CLINT_TIMEBASE_HZ)" \
BSP_DTIM_BASE="$(DTIM_BASE)" BSP_DTIM_SIZE="$(DTIM_SIZE)" \
GPS_DEBUG="$(GPS_DEBUG)" GPS_DEBUG_LEVEL="$(GPS_DEBUG_LEVEL)" \
GPS_ESKF_ENABLE="$(GPS_ESKF_ENABLE)" IMU_SENSOR="$(IMU_SENSOR)" app
chisel: software
@cd "$(REPO_DIR)"
"$(MILL)" -i 'rocketchip[6.7.0].assembly'
rm -rf "$(FIRRTL_DIR)"
mkdir -p "$(FIRRTL_DIR)"
"$(JAVA_HOME)/bin/java" \
-DROCKET_DTIM_BASE="$(DTIM_BASE)" \
-DROCKET_DTIM_SIZE="$(DTIM_SIZE)" \
-jar out/rocketchip/6.7.0/assembly.dest/out.jar \
--dir "$(FIRRTL_DIR)" \
--top freechips.rocketchip.fpga.VCU118RocketSystem \
--config freechips.rocketchip.fpga.VCU118RocketConfig
rtl: chisel
@cd "$(REPO_DIR)"
rm -rf "$(VERILOG_DIR)"
mkdir -p "$(VERILOG_DIR)" "$(dir $(WRAPPER))"
$(FIRTOOL_ENV) "$(FIRTOOL)" \
"$(FIRRTL_DIR)/VCU118RocketSystem.fir" \
--split-verilog -o "$(VERILOG_DIR)" \
--preserve-aggregate=none \
--disable-annotation-unknown \
--disable-all-randomization \
--strip-debug-info
find "$(VERILOG_DIR)" -maxdepth 1 -type f \( -name '*.sv' -o -name '*.v' \) \
-printf './%f\n' | sort > "$(VERILOG_DIR)/filelist.f"
perl -0pi -e 's/^([ \t]*)if \([^\n]*\)\n[ \t]*assert__[^\n]*:\s*assert\([^\n]*\);\n//mg; s/^[ \t]*assert__[^\n]*:\s*assert\([^\n]*\);\n//mg' "$(VERILOG_DIR)"/*.sv
python3 "$(TOOL_SCRIPT_DIR)/prepare_efinity_rtl.py" \
--verilog-dir "$(VERILOG_DIR)" \
--gps-source-dir "$(GPS_SYSTEM_DIR)" \
--gps-output-dir "$(GPS_EFINITY_DIR)"
python3 "$(TOOL_SCRIPT_DIR)/gen_ti375c529_wrapper.py" \
--rocket-top "$(VERILOG_DIR)/VCU118RocketSystem.sv" \
--out "$(WRAPPER)"
: > "$(SOURCE_LIST)"
while IFS= read -r rel; do
rel="$${rel#./}"
printf '%s\n' "$(VERILOG_DIR)/$$rel" >> "$(SOURCE_LIST)"
done < "$(VERILOG_DIR)/filelist.f"
printf '%s\n' "$(HW_DIR)/upstream/rocket-chip/src/main/resources/vsrc/plusarg_reader.v" >> "$(SOURCE_LIST)"
while IFS= read -r rel; do
[[ -z "$$rel" || "$$rel" == \#* ]] && continue
case "$$rel" in
trck_src/xil_fifo.sv|trck_src/pulp_fifo/top.sv) continue ;;
acq_src/acq_nco_carr.sv)
printf '%s\n' "$(GPS_EFINITY_DIR)/acq_nco_carr.sv" >> "$(SOURCE_LIST)"
continue ;;
acq_src/twiddle_ram.sv)
printf '%s\n' "$(GPS_EFINITY_DIR)/twiddle_ram.sv" >> "$(SOURCE_LIST)"
continue ;;
trck_src/NCO_carr.sv)
printf '%s\n' "$(GPS_EFINITY_DIR)/NCO_carr.sv" >> "$(SOURCE_LIST)"
continue ;;
esac
printf '%s\n' "$(GPS_SYSTEM_DIR)/$$rel" >> "$(SOURCE_LIST)"
done < "$(GPS_SYSTEM_DIR)/filelist.f"
# Efinix JTAG_USER1 has the same two-bit TDO lead-in as the Kintex-7
# BSCANE2 path, so use the Genesys2-aligned Rocket DMI tunnel.
printf '%s\n' "$(HW_DIR)/fpga/genesys2/rtl/rocket_bscan_dmi_tunnel.sv" >> "$(SOURCE_LIST)"
$(MAKE) "$(INTERFACE_SCRIPT)"
soc: rtl
$(INTERFACE_SCRIPT): $(INTERFACE_SOURCE)
cp "$<" "$@"
interface: $(INTERFACE_SCRIPT)
@mkdir -p "$(LOG_DIR)"
cd "$(FLOW_DIR)"
# Efinity prefers a matching generated .peri.xml over the source .isf.
# Remove it so pin/PLL changes in the checked-in interface script are applied.
rm -f "$(FLOW_DIR)/ti375c529_rocket.peri.xml"
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
python3 "$(EFX_RUN)" "$(PROJECT_XML)" --flow interface --un_flow \
--pt_opts "peri_scripts=$(INTERFACE_SCRIPT)" 2>&1 | tee "$(LOG_DIR)/interface.log"
map: rtl interface
@mkdir -p "$(LOG_DIR)"
cd "$(FLOW_DIR)"
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
python3 "$(EFX_RUN)" "$(PROJECT_XML)" --flow map 2>&1 | tee "$(LOG_DIR)/map.log"
bitstream: rtl interface
@mkdir -p "$(LOG_DIR)" "$(dir $(BIT_FILE))"
cd "$(FLOW_DIR)"
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
: > "$(LOG_DIR)/full.log"
python3 "$(EFX_RUN)" "$(PROJECT_XML)" --flow map 2>&1 | tee -a "$(LOG_DIR)/full.log"
python3 "$(EFX_RUN)" "$(PROJECT_XML)" --flow pnr --work_dir work_pnr 2>&1 | tee -a "$(LOG_DIR)/full.log"
python3 "$(EFX_RUN)" "$(PROJECT_XML)" --flow pgm --work_dir work_pnr 2>&1 | tee -a "$(LOG_DIR)/full.log"
generated_bit="$$(find "$(FLOW_DIR)/outflow" "$(OUT_DIR)" -type f -name 'ti375c529_rocket.bit' 2>/dev/null | head -n 1)"
if [[ -z "$$generated_bit" ]]; then
echo "Efinity completed without a discoverable ti375c529_rocket.bit" >&2
exit 1
fi
if [[ "$$generated_bit" != "$(BIT_FILE)" ]]; then
cp "$$generated_bit" "$(BIT_FILE)"
fi
generated_hex="$$(find "$(FLOW_DIR)/outflow" "$(OUT_DIR)" -type f -name 'ti375c529_rocket.hex' ! -path '$(FLASH_DIR)/*' 2>/dev/null | head -n 1)"
if [[ -z "$$generated_hex" ]]; then
echo "Efinity completed without a discoverable ti375c529_rocket.hex" >&2
exit 1
fi
if [[ "$$generated_hex" != "$(CONFIG_HEX)" ]]; then
cp "$$generated_hex" "$(CONFIG_HEX)"
fi
printf 'Bitstream: %s\nConfiguration HEX: %s\n' "$(BIT_FILE)" "$(CONFIG_HEX)"
program-fpga:
@if [[ ! -f "$(BIT_FILE)" ]]; then
echo "Missing bitstream: $(BIT_FILE)" >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
ftdi_args=()
if [[ -n "$(FTDI_URL)" ]]; then
ftdi_args+=(--url "$(FTDI_URL)")
fi
"$(EFX_PGM)" --mode jtag "$${ftdi_args[@]}" "$(BIT_FILE)"
debug-openocd:
@"$(OPENOCD)" \
-c "set ADAPTER_SPEED $(OPENOCD_ADAPTER_SPEED)" \
-c "gdb_port $(GDB_PORT)" \
-f "$(OPENOCD_CFG)"
debug-load: $(APP_ELF)
@"$(GDB)" -q "$(APP_ELF)" \
-ex "set confirm off" \
-ex "set pagination off" \
-ex "target extended-remote $(GDB_HOST):$(GDB_PORT)" \
-x "$(GDB_LOAD_SCRIPT)"
debug-gdb: $(APP_ELF)
@"$(GDB)" -q "$(APP_ELF)" \
-ex "set confirm off" \
-ex "set pagination off" \
-ex "target extended-remote $(GDB_HOST):$(GDB_PORT)" \
-x "$(GDB_ATTACH_SCRIPT)"
uart-listen:
@stty -F "$(UART_PORT)" "$(UART_BAUD)" cs8 -cstopb -parenb -ixon -ixoff raw
@cat "$(UART_PORT)"
flash-image: flash-regions
flash-regions: software
@if [[ ! -f "$(CONFIG_HEX)" ]]; then
echo "Missing Efinity configuration HEX: $(CONFIG_HEX)" >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
mkdir -p "$(FLASH_DIR)" "$(FLASH_REGION_DIR)"
python3 "$(TOOL_SCRIPT_DIR)/make_flash_hex.py" \
--config-hex "$(CONFIG_HEX)" \
--app-bin "$(APP_FLASH_IMAGE)" \
--app-offset "$(APP_FLASH_OFFSET)" \
--flash-size "$(FLASH_SIZE)" \
--combined-hex "$(FLASH_HEX)" \
--app-hex "$(APP_HEX)" \
--manifest "$(FLASH_MANIFEST)"
program-flash-loader:
@if [[ ! -f "$(FLASH_LOADER)" ]]; then
echo "Missing Ti375 JTAG-to-SPI loader: $(FLASH_LOADER)" >&2
exit 1
fi
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
ftdi_args=()
if [[ -n "$(FTDI_URL)" ]]; then
ftdi_args+=(--url "$(FTDI_URL)")
fi
"$(EFX_PGM)" --mode jtag "$${ftdi_args[@]}" "$(FLASH_LOADER)"
program-flash program-flash-elf program-app: flash-regions
@$(MAKE) -C "$(FPGA_DIR)" program-flash-loader
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
ftdi_args=()
if [[ -n "$(FTDI_URL)" ]]; then
ftdi_args+=(--url "$(FTDI_URL)")
fi
"$(EFX_PGM)" --mode jtag_bridge --jtag_bridge_mode all \
--verify_method "$(FLASH_VERIFY)" --address "$(APP_FLASH_OFFSET)" \
"$${ftdi_args[@]}" "$(APP_HEX)"
program-flash-bitstream program-bitstream:
@if [[ ! -f "$(CONFIG_HEX)" ]]; then
echo "Missing Efinity configuration HEX: $(CONFIG_HEX)" >&2
echo "Run: make -C $(FPGA_DIR) bitstream" >&2
exit 1
fi
$(MAKE) -C "$(FPGA_DIR)" program-flash-loader
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
ftdi_args=()
if [[ -n "$(FTDI_URL)" ]]; then
ftdi_args+=(--url "$(FTDI_URL)")
fi
"$(EFX_PGM)" --mode jtag_bridge --jtag_bridge_mode all \
--verify_method "$(FLASH_VERIFY)" --address 0 \
"$${ftdi_args[@]}" "$(CONFIG_HEX)"
program-flash-regions program-flash-all program-flash-combined: flash-image
@$(MAKE) -C "$(FPGA_DIR)" program-flash-loader
set +u
source "$(EFINITY_SETUP)" >/dev/null 2>&1
set -u
ftdi_args=()
if [[ -n "$(FTDI_URL)" ]]; then
ftdi_args+=(--url "$(FTDI_URL)")
fi
"$(EFX_PGM)" --mode jtag_bridge --jtag_bridge_mode all \
--verify_method "$(FLASH_VERIFY)" --address 0 \
"$${ftdi_args[@]}" "$(FLASH_HEX)"
clean-sw:
@$(MAKE) -C "$(SOC_SW_DIR)" clean
clean-fpga:
rm -rf "$(OUT_DIR)" "$(FIRRTL_DIR)" "$(VERILOG_DIR)"
rm -f "$(INTERFACE_SCRIPT)" \
"$(FLOW_DIR)/ti375c529_rocket.peri.xml" \
"$(FLOW_DIR)/ti375c529_rocket.pt.sdc" \
"$(FLOW_DIR)/ti375c529_rocket.pt.rpt"
clean: clean-fpga
FORCE:
+160
View File
@@ -0,0 +1,160 @@
# Ti375C529 Rocket FPGA flow
This target keeps the project's RV64 Rocket/GPS subsystem in FPGA fabric and
replaces only the Xilinx board shell and build flow with Efinix equivalents.
It does not use the Ti375 hardened RV32 subsystem.
Detailed porting, synthesis, clocking, JTAG, and verification notes are in
[`flow/efinity/README.md`](flow/efinity/README.md).
## Build and program
```bash
make FPGA_BOARD=ti375c529 fpga-config
make FPGA_BOARD=ti375c529 fpga-rtl
make FPGA_BOARD=ti375c529 fpga-bitstream
make FPGA_BOARD=ti375c529 fpga-program
```
The Makefile auto-discovers Efinity under `/tools/efinix/efinity`. Override
`EFINITY_HOME` when another installation is required.
Programming auto-selects the connected Ti375 development kit. If more than
one compatible FTDI device is attached, run `make FPGA_BOARD=ti375c529
fpga-detect` to list the bus-qualified URLs and pass the selected one as
`FTDI_URL=...`.
Volatile JTAG programming and GDB loading into Rocket DTIM remain available.
Persistent boot uses the board's Lower 512-Mbit SPI flash with this map:
| Region | Flash offset | Contents |
|---|---:|---|
| FPGA configuration | `0x00000000` | Efinity active-mode configuration HEX |
| Rocket application | `0x00800000` | Boot header and ELF-derived payload |
The application starts at 8 MiB because the compressed Ti375 configuration is
currently about 5.19 MB; the previous 4 MiB trial offset would overlap it.
Generate or program the Efinity raw HEX images with:
```bash
make FPGA_BOARD=ti375c529 fpga-mcs APP=hello_uart
make FPGA_BOARD=ti375c529 fpga-flash-app APP=hello_uart
make FPGA_BOARD=ti375c529 fpga-flash-bitstream
make FPGA_BOARD=ti375c529 fpga-flash-mcs APP=hello_uart
```
`fpga-mcs` is the repository-wide compatibility target name. For this Efinix
board it produces `.hex`, not Xilinx `.mcs`. `fpga-flash-app` changes only the
application region; `fpga-flash-mcs` erases/programs configuration plus the
application through Efinity's pre-built JTAG-to-SPI bridge and verifies the
readback. Cold boot from this layout was verified by a complete board power
cycle: Ti375 configured from flash and Rocket loaded and completed
`hello_uart` from the application region without a JTAG/GDB load.
## Debug and load
Use the host OpenOCD build, not the OpenOCD shipped with Efinity. The build
must include `riscv set_bscan_tunnel_ir`; Ti375 uses a 5-bit outer TAP and
selects `JTAG_USER1` with instruction `0x08`.
OpenOCD 0.12 can be given that upstream-compatible command with
`flow/debug/openocd-0.12-ti375-bscan.patch`.
In one terminal:
```bash
make FPGA_BOARD=ti375c529 fpga-debug-openocd
```
In a second terminal:
```bash
make FPGA_BOARD=ti375c529 fpga-debug-load APP=hello_uart
make FPGA_BOARD=ti375c529 fpga-uart
```
The target deliberately selects `/tools/toolchain/bin/riscv64-unknown-elf-gdb`
when it exists. The medany compiler toolchain is still used to build software,
but it is not expected to contain GDB.
Efinity 2025.2 leaves Rocket's block-local packed store-data array undriven
(`VDB-1002`). `tools/prepare_efinity_rtl.py` rewrites that generated RTL
expression as an equivalent explicit `case`. Without the rewrite, DMI itself
works, but GPR/CSR values stored into Debug Module data registers read as zero;
OpenOCD consequently reports a misleading `misa=0`.
The patch is deliberately post-generation: Chisel and `firtool` first emit the
Ti375 SystemVerilog tree, then `prepare_efinity_rtl.py` modifies only the
generated Ti375 `Rocket.sv` before Efinity synthesis. It does not edit Rocket's
native/upstream Chisel sources.
Two equivalent four-entry packed temporaries in the generated pbus/cbus
`TLAtomicAutomata` modules are rewritten for the same reason, preserving the
four atomic-operation mappings while removing their `VDB-1002` risk.
The same script generates Ti375-only copies of the acquisition carrier NCO,
tracking carrier NCO, and FFT twiddle ROM under
`out/build/generated/gps_efinity/`. This avoids Efinity 2025.2
whole-array/`initial`/`$sin` elaboration failures without editing the shared GPS
RTL. Only the Ti375 source list selects these copies; Genesys2 and VCU118 still
use the originals. Detailed source paths, warning interpretation, and Questa
RTL/post-map equivalence results are in the Efinity flow notes.
The affected generated source is
`hardware/generated/ti375c529-soc-verilog/Rocket.sv`; Efinity records the
original diagnostic in
`targets/fpga/ti375c529/flow/efinity/outflow/ti375c529_rocket.warn.log`.
The checked-in repair is
`targets/fpga/ti375c529/tools/prepare_efinity_rtl.py` and runs after every
`firtool` invocation, before Efinity synthesis.
## Board connections
PMOD0/J13 carries the GPS RF sampler:
| Signal | PMOD pin | FPGA resource | Package pin |
|---|---:|---|---|
| `rf_clk` | 10 | GPIOR_95_CLK12 | A12 |
| `rf_data[0]` | 2 | GPIOR_89 | G16 |
| `rf_data[1]` | 3 | GPIOR_90 | F16 |
| `rf_data[2]` | 4 | GPIOR_91 | F17 |
| `rf_data[3]` | 7 | GPIOR_92 | G17 |
`rf_clk` must be wired to J13 pin 10, not J13 pin 1. Pin 10 is the PMOD0
header's dedicated 3.3 V global-clock input. J13 pin 1 (`G15`,
`GPIOR_88_PLLIN1`) can feed a PLL but is not a dedicated fabric GCLK input;
using it as the raw AFE clock makes Efinity promote normal GPIO routing into a
GBUF and issue an extra-clock-insertion-delay warning.
As on Genesys2 and VCU118, the AX2771 clock is both the raw AFE sample clock
and the source of the GPS processing-clock chain. Two Titanium PLLs generate
an intermediate 55.5556 MHz clock, then 100 MHz acquisition and 125 MHz
tracking clocks. The Rocket fabric uses the independent on-board 100 MHz
oscillator.
PMOD1/J14 carries the sensor SPI, interrupt, and PPS:
| Signal | PMOD pin | FPGA resource | Package pin |
|---|---:|---|---|
| `pmod_spi_csn` | 1 | GPIOR_96_CLK13 | B12 |
| `pmod_spi_dq[0]` | 2 | GPIOR_97 | C14 |
| `pmod_spi_dq[1]` | 3 | GPIOR_98 | C13 |
| `pmod_spi_sck` | 4 | GPIOR_99 | C12 |
| `pmod_spi_dq[2]` | 7 | GPIOR_100 | D12 |
| `pmod_spi_dq[3]` | 8 | GPIOR_101_PLLIN1 | F12 |
| `pmod_irq` | 9 | GPIOR_102 | D13 |
| `gps_pps` | 10 | GPIOR_103 | E13 |
The FTDI UART is `/dev/ttyUSB2` on the current Linux enumeration. SW3 is the
active-low Rocket reset. JTAG uses Efinix `JTAG_USER1` and the existing Rocket
DMI tunnel protocol.
The Lower flash user-mode connection is internal to the development kit:
| Signal | FPGA resource | Package pin |
|---|---|---|
| `flash_csn` | GPIOB_N_38 / SSL_N | A7 |
| `flash_sck` | GPIOB_P_39 / CCK | G6 |
| `flash_dq[0]` | GPIOB_N_40 / CDI0 | H4 |
| `flash_dq[1]` | GPIOB_P_40 / CDI1 | H5 |
| `flash_dq[2]` | GPIOB_N_39 / CDI2 | G5 |
| `flash_dq[3]` | GPIOB_P_38 / CDI3 | B7 |
+12
View File
@@ -0,0 +1,12 @@
Board name: Efinix Titanium Ti375 C529 Development Kit
Official link: https://www.efinixinc.com/products-devkits-titaniumti375c529.html
FPGA device: Ti375C529
Efinity family: Titanium
Efinity timing model: C4
Initial soft-Rocket flow clocks:
SoC/fabric: 100 MHz from the on-board 100 MHz oscillator
GPS RF: external AX2771 PMOD0/J13 clock, nominally 16.367981 MHz
GPS acquisition: 100 MHz, PLL-derived from the GPS RF clock
GPS tracking: 125 MHz, PLL-derived from the GPS RF clock
@@ -0,0 +1 @@
monitor halt
@@ -0,0 +1,13 @@
monitor halt
monitor reset halt
set $mie = 0
set $mstatus = 0x6000
set {unsigned long long}0x02004000 = 0xffffffffffffffff
load
set $mie = 0
set $mstatus = 0x6000
set $sp = (unsigned long)&_stack_top
set $pc = _start
monitor resume
disconnect
quit
@@ -0,0 +1,62 @@
--- a/src/target/riscv/riscv.c
+++ b/src/target/riscv/riscv.c
@@ -127,6 +127,7 @@
static bscan_tunnel_type_t bscan_tunnel_type;
int bscan_tunnel_ir_width; /* if zero, then tunneling is not present/active */
+static int bscan_tunnel_ir_id; /* outer TAP IR used to access the tunnel */
static const uint8_t bscan_zero[4] = {0};
static const uint8_t bscan_one[4] = {1};
@@ -451,8 +452,12 @@
select_idcode.num_bits = target->tap->ir_length;
if (bscan_tunnel_ir_width != 0) {
- assert(target->tap->ir_length >= 6);
- uint32_t ir_user4_raw = 0x23 << (target->tap->ir_length - 6);
+ uint32_t ir_user4_raw = bscan_tunnel_ir_id;
+ /* Preserve the legacy Xilinx USER4 default when no IR was supplied. */
+ if (ir_user4_raw == 0) {
+ assert(target->tap->ir_length >= 6);
+ ir_user4_raw = 0x23 << (target->tap->ir_length - 6);
+ }
h_u32_to_le(ir_user4, ir_user4_raw);
select_user4.num_bits = target->tap->ir_length;
bscan_tunneled_ir_width[0] = bscan_tunnel_ir_width;
@@ -2859,6 +2864,21 @@
return ERROR_OK;
}
+COMMAND_HANDLER(riscv_set_bscan_tunnel_ir)
+{
+ int ir_id = 0;
+
+ if (CMD_ARGC > 1)
+ return ERROR_COMMAND_SYNTAX_ERROR;
+
+ if (CMD_ARGC == 1)
+ COMMAND_PARSE_NUMBER(int, CMD_ARGV[0], ir_id);
+
+ LOG_INFO("Bscan tunnel IR 0x%x selected", ir_id);
+ bscan_tunnel_ir_id = ir_id;
+ return ERROR_OK;
+}
+
COMMAND_HANDLER(riscv_set_enable_virt2phys)
{
if (CMD_ARGC != 1) {
@@ -3072,6 +3092,14 @@
"1: DATA_REGISTER}"
},
{
+ .name = "set_bscan_tunnel_ir",
+ .handler = riscv_set_bscan_tunnel_ir,
+ .mode = COMMAND_ANY,
+ .usage = "[value]",
+ .help = "Specify the outer JTAG TAP IR used to access the BSCAN tunnel. "
+ "The default remains the Xilinx FPGA USER4 instruction."
+ },
+ {
.name = "set_enable_virt2phys",
.handler = riscv_set_enable_virt2phys,
.mode = COMMAND_ANY,
@@ -0,0 +1,32 @@
# Rocket debug through the Ti375 hard TAP, JTAG_USER1, and the project's DMI tunnel.
adapter driver ftdi
ftdi vid_pid 0x0403 0x6011
ftdi channel 1
ftdi layout_init 0x08 0x0b
if {![info exists ADAPTER_SPEED]} {
set ADAPTER_SPEED 1000
}
adapter speed $ADAPTER_SPEED
jtag newtap ti375 fpga -expected-id 0x006A0A79 -irlen 5 -ircapture 0x5 -irmask 0xF
target create rocket.cpu riscv -chain-position ti375.fpga -coreid 0
# The outer Ti375 USER1 instruction is 0x08. The inner tunnel implements the
# same 5-bit Rocket DTM IR and framing used by the Xilinx targets.
riscv set_ir dtmcs 0x10
riscv set_ir dmi 0x11
riscv use_bscan_tunnel 5 0
# Ti375 requires an OpenOCD build with the upstream configurable outer BSCAN
# instruction support. Keep the catch only so older builds produce a readable
# diagnostic before their legacy Xilinx-specific BSCAN path rejects this TAP.
if {[catch {riscv set_bscan_tunnel_ir 8}]} {
echo "OpenOCD lacks 'riscv set_bscan_tunnel_ir'; Ti375 requires that command"
}
riscv set_reset_timeout_sec 10
riscv set_command_timeout_sec 10
init
halt
@@ -0,0 +1,327 @@
# Ti375C529 Efinity flow notes
This directory contains the Efinity project for the Ti375C529 development kit.
The port keeps the same RV64 Rocket/GPS subsystem used by the Genesys2 and
VCU118 targets. Only the FPGA shell, clocks, pin constraints, programming flow,
and JTAG transport are vendor-specific. The hardened RV32 subsystem in the
Ti375 device is not used.
## What was added
- Added `ti375c529` to the root FPGA board selection and build targets.
- Added Efinity 2025.2 discovery through `EFINITY_HOME`; the current installation
is `/tools/efinix/efinity/2025.2`.
- Added a generated Ti375 wrapper around `VCU118RocketSystem` so the existing
Rocket, DTIM, UART, GPS acquisition, tracking, PPS, and SPI logic is reused.
- Added the Efinity XML project, interface script, SDC constraints, PLLs, pin
assignments, and JTAG programming target.
- Added FTDI UART operation at 460800 baud. On the current host it enumerates as
`/dev/ttyUSB2`.
- Added host OpenOCD/GDB debug through the Ti375 hard TAP and `JTAG_USER1`.
- Added Efinity-specific generated-RTL rewrites for Rocket's store-data path and
the GPS acquisition/tracking lookup tables that Efinity 2025.2 mis-elaborates.
## Clock structure
The Rocket fabric and GPS processing clocks have separate sources:
- The on-board 100 MHz oscillator drives the fabric PLL and the 100 MHz Rocket
system clock.
- AX2771 `rf_clk` enters through the dedicated global-clock input at J13 pin 10
/ package pin A12 (`GPIOR_95_CLK12`). It is the raw AFE sample clock and feeds
the GPS PLL chain. J13 pin 1 / G15 is not used for the RF clock because it is
a PLL input, not a dedicated fabric GCLK input.
- The GPS PLL chain generates 100 MHz acquisition and 125 MHz tracking clocks.
- Acquisition and tracking reset release is gated by the relevant PLL lock
signals.
The four AX2771 data bits use J13 pins 2, 3, 4, and 7, corresponding to package
pins G16, F16, F17, and G17. The AX2771 clock must be jumpered to J13 pin 10;
do not connect it to the former J13 pin 1 location.
## Efinity synthesis adaptation
This adaptation does **not** modify Rocket's native/upstream Chisel sources or
the shared GPS SystemVerilog. The normal Chisel and `firtool` stages run first
and emit SystemVerilog into `hardware/generated/ti375c529-soc-verilog/`.
Immediately afterward,
`targets/fpga/ti375c529/tools/prepare_efinity_rtl.py` patches the generated
Ti375 `Rocket.sv` and writes Efinity-only GPS replacements under
`targets/fpga/ti375c529/out/build/generated/gps_efinity/`. The Ti375 source-list
recipe substitutes those generated copies for exactly these three shared GPS
files:
- `acq_src/acq_nco_carr.sv`
- `trck_src/NCO_carr.sv`
- `acq_src/twiddle_ram.sv`
All other GPS RTL remains shared. Efinity consumes the replacements; Genesys2
and VCU118 continue to consume the original GPS files. Deleting either
generated directory and running `make FPGA_BOARD=ti375c529 fpga-rtl` repeats
the same deterministic generation and substitution.
### Rocket store-data path
The CIRCT-generated `Rocket.sv` contains this block-local packed temporary for
store-data byte-lane replication:
```systemverilog
automatic logic [3:0][63:0] _GEN_24 = ...;
mem_reg_rs2 <= _GEN_24[ex_reg_mem_size];
```
Vivado synthesizes this construct correctly. Efinity 2025.2 reports `_GEN_24`
as `VDB-1002: net does not have a driver`, leaving Rocket store data at zero.
This initially looked like a Debug Module failure because abstract GPR/CSR
reads execute a hart instruction that stores the result into Debug Module data
registers. DMI was working, but OpenOCD received zeros and reported the
misleading result `misa=0`.
`../../tools/prepare_efinity_rtl.py` replaces only that generated expression
with an equivalent explicit `case` for byte, half-word, word, and double-word
stores. The script runs automatically after `firtool`; generated RTL is not
edited by hand. After the rewrite, the Rocket `VDB-1002` warning disappears
from the current synthesis run. `ti375c529_rocket.warn.log` is reused by
Efinity and can contain entries from older runs, so the generated source and
the final warning block should be checked together.
The same Efinity limitation also affected the four-entry packed atomic-operation
LUT emitted into generated `TLAtomicAutomata_pbus.sv` and
`TLAtomicAutomata_cbus.sv`. Those `_GEN_4`/`_GEN_13` temporaries select the
TileLink atomic ALU operation; they are unrelated to GPS acquisition but could
break software atomics. The post-generation script rewrites both to an
equivalent four-way `case`. Questa confirmed all four index mappings, and their
`VDB-1002` diagnostics disappear from the current single-run `map.out`.
The exact paths are:
- Generated failing/fixed source:
`hardware/generated/ti375c529-soc-verilog/Rocket.sv`
- Generated TileLink atomic sources:
`hardware/generated/ti375c529-soc-verilog/TLAtomicAutomata_pbus.sv` and
`hardware/generated/ti375c529-soc-verilog/TLAtomicAutomata_cbus.sv`
- Efinity synthesis warning log:
`targets/fpga/ti375c529/flow/efinity/outflow/ti375c529_rocket.warn.log`
- Deterministic repair:
`targets/fpga/ti375c529/tools/prepare_efinity_rtl.py`
- Invocation point:
`targets/fpga/ti375c529/Makefile`, in the `rtl` recipe immediately after
`firtool`
### GPS lookup-table paths
The original acquisition carrier NCO is
`hardware/external/gps/rtl/acq_src/acq_nco_carr.sv`. It initializes the
frequency and pull-in arrays with whole-package-array assignments and builds
its sine table in an `initial` loop using `$sin`. Efinity 2025.2 reports the
loop variable as never assigned and the three arrays as undriven. Stand-alone
post-map comparison confirmed that this is functional, not cosmetic: the
unpatched Efinity netlist produced incorrect sine/cosine samples in all 18,040
tested production-parameter cycles.
The generated Ti375 copy replaces the frequency, pull-in, sine, and cosine
reads with constant functions generated from `definitions.sv`. Questa compared
41 frequency bins, 11 pull-in bins, and 40 cycles per combination: all 18,040
RTL samples and all 18,040 Efinity post-map samples matched the original RTL.
The current full-project synthesis no longer reports the acquisition arrays or
its `$sin` loop as undriven.
The separate tracking carrier NCO is
`hardware/external/gps/rtl/trck_src/NCO_carr.sv`. Its
`sinwav_lut = TRCK_SINWAV_LUT` package-array initializer triggers the same
Efinity `VDB-1002` behavior. The Ti375 copy uses an exact 128-entry constant
function generated from `TRCK_SINWAV_LUT`. Questa matched every table entry at
RTL and then matched 5,000 Efinity post-map cycles while visiting all 128
addresses.
The acquisition FFT twiddle source is
`hardware/external/gps/rtl/acq_src/twiddle_ram.sv`. Its whole-array
`COS_ROM_ARRAY` and `SIN_ROM_ARRAY` assignments are converted only for Ti375 to
two generated, exactly 15-bit-wide `$readmemb` files. Efinity infers 30
`EFX_RAM10` blocks with embedded INIT data. Efinity still emits its generic
`sin_rom/cos_rom does not have a driver` elaboration warning for this recognized
ROM form, but this one is demonstrably benign: Questa matched all 8,192 RTL
addresses and all 8,192 addresses in the Efinity post-map RAM netlist. Check
the current `ti375c529_rocket.map.out` for a single run; the `.warn.log` and
`.info.log` files retain records from older builds and can otherwise make fixed
source paths look current.
## JTAG and OpenOCD
Ti375 and the Xilinx boards expose the same inner Rocket DTM protocol, but the
outer FPGA TAP is different:
| Item | Xilinx flow | Ti375 flow |
|---|---|---|
| Outer TAP IR width | Xilinx-specific | 5 bits |
| User instruction | legacy USER4 handling | `JTAG_USER1 = 0x08` |
| Inner DTM IR width | 5 bits | 5 bits |
| DTMCS / DMI IR | `0x10` / `0x11` | `0x10` / `0x11` |
The host OpenOCD must therefore implement:
```text
riscv set_bscan_tunnel_ir 8
```
A fixed OpenOCD pathname is **not** required. The target Makefile evaluates
`command -v openocd`, so the first compatible binary in `PATH` is used. On the
current machine this resolves to:
```text
/home/onur/.local/bin/openocd
```
That location is only the installation choice for the patched host OpenOCD; it
is not part of the hardware flow. Another compatible build can be selected
without changing files:
```bash
make FPGA_BOARD=ti375c529 fpga-debug-openocd OPENOCD=/path/to/openocd
```
The original `/usr/local/bin/openocd` 0.12 build assumes the Xilinx USER4
instruction and asserts that the outer TAP IR is at least six bits. It cannot
drive the Ti375 5-bit TAP unchanged. `../debug/openocd-0.12-ti375-bscan.patch`
adds the configurable outer instruction to OpenOCD 0.12. The OpenOCD shipped
with Efinity is not required by this flow.
The selected paths can be checked with:
```bash
make FPGA_BOARD=ti375c529 fpga-config
command -v openocd
strings "$(command -v openocd)" | grep set_bscan_tunnel_ir
```
The line `Disabling abstract command reads/writes from CSRs` is informational,
not a failure. OpenOCD falls back to Rocket's Program Buffer for CSR access.
## GDB selection
Software is still compiled with the medany toolchain. That toolchain does not
contain GDB, so the debug target deliberately selects:
```text
/tools/toolchain/bin/riscv64-unknown-elf-gdb
```
This can also be overridden with `GDB=/path/to/riscv64-unknown-elf-gdb`.
## Build, program, and debug
From the repository root:
```bash
make FPGA_BOARD=ti375c529 fpga-config
make FPGA_BOARD=ti375c529 fpga-rtl
make FPGA_BOARD=ti375c529 fpga-bitstream
make FPGA_BOARD=ti375c529 fpga-program
```
Start the host OpenOCD server in one terminal:
```bash
make FPGA_BOARD=ti375c529 fpga-debug-openocd
```
Load and run an application, then monitor UART, from another terminal:
```bash
make FPGA_BOARD=ti375c529 fpga-debug-load APP=hello_uart
make FPGA_BOARD=ti375c529 fpga-uart
```
## Persistent Lower-flash boot
The kit has Lower and Upper 512-Mbit SPI NOR devices. This port intentionally
uses one MCS-like combined layout in the Lower ISSI IS25WP512 because Efinity's
command-line `jtag_bridge` mode directly addresses that flash:
| Offset | Image |
|---:|---|
| `0x00000000` | Compressed, active-x1 FPGA configuration |
| `0x00800000` | Rocket boot header followed by the application payload |
The application offset is 64-KiB-sector aligned and leaves roughly 3.2 MB
between the current 5,190,596-byte configuration and the firmware region.
`spi_low_power_mode=off` is embedded in the configuration so the Lower flash
stays available when Rocket enters user mode.
Rocket's existing MMIO SPI controller is connected to the configuration pins
after configuration completes:
```text
spi_sck/spi_csn/spi_dq_* -> A7/G6/H4/H5/G5/B7 -> Lower flash U47
```
Generate the configuration, per-application, and combined raw Efinity HEX
files:
```bash
make FPGA_BOARD=ti375c529 fpga-bitstream
make FPGA_BOARD=ti375c529 fpga-mcs APP=hello_uart
```
Outputs are:
- `out/bitstream/ti375c529_rocket.hex`: configuration region
- `out/flash/regions/app.hex`: application region
- `out/flash/ti375c529_rocket.hex`: combined image
- `out/flash/ti375c529_rocket.json`: offsets, sizes, and SHA-256 manifest
Program only the application, only the configuration, or the combined image:
```bash
make FPGA_BOARD=ti375c529 fpga-flash-app APP=hello_uart
make FPGA_BOARD=ti375c529 fpga-flash-bitstream
make FPGA_BOARD=ti375c529 fpga-flash-mcs APP=hello_uart
```
The Makefile first loads Efinity's device-matched bridge
`/tools/efinix/efinity/2025.2/pgm/fli/titanium/u006A0A79.bit`, then uses
`jtag_bridge` programming with on-chip verification. A fixed Efinity install
path is not required; `FLASH_LOADER` is derived from `EFINITY_HOME` and can be
overridden.
After bridge programming, the FPGA still contains the bridge design. Power
cycle the kit for a true active-flash boot, or use `fpga-program` to load the
normal design immediately and test the BootROM/application flash path.
## On-board verification
The completed flow was checked on the connected Ti375C529 kit with AX2771
attached:
- Efinity interface, map, PNR, and programming-image stages passed.
- Final patched timing passed with WNS `+0.101 ns` and WHS `+0.021 ns`.
- The Ti375 JTAG IDCODE was read as `0x006a0a79`.
- OpenOCD found one RV64 hart and read `misa=0x800000000080112d`.
- A raw Program Buffer GPR/store test returned `0x5a` through DMI.
- `/tools/toolchain/bin/riscv64-unknown-elf-gdb` loaded 40,880 bytes into DTIM.
- `hello_uart` printed the expected Rocket/DTIM/UART messages at 460800 baud.
- The persistent image passed with setup slack `+0.094 ns` and hold slack
`+0.021 ns`.
- The Lower flash was detected as ISSI IS25WP512, JEDEC `0x9d701a`.
- The combined 8,429,552-byte HEX completed erase, write, readback, and verify.
- BootROM read the payload at `0x00800040`, verified CRC `0xed3bbdf5`, copied
40,880 bytes into DTIM, and ran `hello_uart` without GDB.
- A full board power cycle then configured Ti375 from Lower flash. Host OpenOCD
found the RV64 hart through `JTAG_USER1`; its PC was at
`0x8000017e <_exit>` after `hello_uart` returned, and GDB `compare-sections`
matched the complete ELF `.text` range in DTIM. This confirms cold boot of
both hardware and software without a volatile JTAG/GDB load.
## Relevant files
- `ti375c529_rocket.xml`: Efinity project definition.
- `ti375c529_rocket.isf`: generated/copy of the checked-in interface script.
- `../../../../../hardware/fpga/ti375c529/constraints/`: source interface and SDC
constraints.
- `../../tools/gen_ti375c529_wrapper.py`: Efinix board-shell generator.
- `../../tools/prepare_efinity_rtl.py`: deterministic Rocket and GPS
Efinity-only RTL generation/rewrite.
- `../../tools/make_flash_hex.py`: validated Efinity raw-HEX image builder.
- `../debug/openocd_ti375c529_bscan.cfg`: Ti375 hard-TAP/OpenOCD configuration.
- `../debug/openocd-0.12-ti375-bscan.patch`: host OpenOCD 0.12 compatibility
patch.
@@ -0,0 +1,71 @@
<efx:project xmlns:efx="http://www.efinixinc.com/enf_proj" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="ti375c529_rocket" description="RV64 Rocket and GPS on the Ti375C529 development kit" sw_version="2025.2.288.2.10" xsi:schemaLocation="http://www.efinixinc.com/enf_proj enf_proj.xsd" last_run_state="pass" last_run_flow="bitstream" last_change="1785370142" config_result_in_sync="sync" design_ood="sync" place_ood="sync" route_ood="sync">
<efx:device_info>
<efx:family name="Titanium" />
<efx:device name="Ti375C529" />
<efx:timing_model name="C4" />
</efx:device_info>
<efx:design_info def_veri_version="verilog_2k" def_vhdl_version="vhdl_2008" unified_flow="true">
<efx:top_module name="ti375c529_rocket_top" />
<efx:design_file name="../../out/build/generated/ti375c529_rocket_top.sv" version="default" library="default" />
<efx:design_file name="../../out/build/efinity_sources.f" version="na" library="default" />
<efx:top_vhdl_arch name="" />
</efx:design_info>
<efx:constraint_info>
<efx:sdc_file name="../../../../../hardware/fpga/ti375c529/constraints/ti375c529_rocket.sdc" />
<efx:sdc_file name="outflow/ti375c529_rocket.pt.sdc" />
<efx:sdc_file name="../../../../../hardware/fpga/ti375c529/constraints/ti375c529_rocket_clock_groups.sdc" />
<efx:inter_file name="" />
</efx:constraint_info>
<efx:isf_info>
<efx:isf_file name="ti375c529_rocket.isf" />
</efx:isf_info>
<efx:sim_info />
<efx:misc_info />
<efx:ooc_info />
<efx:ip_info />
<efx:synthesis tool_name="efx_map">
<efx:param name="work_dir" value="../../out/build/work_syn" value_type="e_string" />
<efx:param name="write_efx_verilog" value="on" value_type="e_bool" />
<efx:param name="blackbox-error" value="1" value_type="e_option" />
<efx:param name="hdl-compile-unit" value="1" value_type="e_option" />
<efx:param name="infer-clk-enable" value="3" value_type="e_option" />
<efx:param name="infer-sync-set-reset" value="1" value_type="e_option" />
<efx:param name="mode" value="speed" value_type="e_option" />
<efx:param name="retiming" value="1" value_type="e_option" />
<efx:param name="seq_opt" value="1" value_type="e_option" />
<efx:param name="use-logic-for-small-mem" value="64" value_type="e_integer" />
<efx:param name="use-logic-for-small-rom" value="64" value_type="e_integer" />
<efx:param name="max_threads" value="-1" value_type="e_integer" />
<efx:param name="include" value="../../../../../hardware/external/gps/rtl/trck_src/pulp_fifo" value_type="e_string" />
<efx:param name="include" value="../../out/build/generated/gps_efinity" value_type="e_string" />
</efx:synthesis>
<efx:place_and_route tool_name="efx_pnr">
<efx:param name="work_dir" value="work_pnr" value_type="e_string" />
<efx:param name="seed" value="2" value_type="e_integer" />
<efx:param name="placer_effort_level" value="2" value_type="e_option" />
<efx:param name="max_threads" value="-1" value_type="e_integer" />
<efx:param name="beneficial_skew" value="on" value_type="e_option" />
<efx:param name="print_critical_path" value="10" value_type="e_integer" />
</efx:place_and_route>
<efx:bitstream_generation tool_name="efx_pgm">
<efx:param name="mode" value="active" value_type="e_option" />
<efx:param name="width" value="1" value_type="e_option" />
<efx:param name="enable_roms" value="smart" value_type="e_option" />
<efx:param name="bitstream_compression" value="on" value_type="e_bool" />
<efx:param name="spi_low_power_mode" value="off" value_type="e_bool" />
<efx:param name="release_tri_then_reset" value="on" value_type="e_bool" />
<efx:param name="generate_bit" value="on" value_type="e_bool" />
<efx:param name="generate_hex" value="on" value_type="e_bool" />
<efx:param name="jtag_usercode" value="0x52564B54" value_type="e_string" />
</efx:bitstream_generation>
<efx:debugger>
<efx:param name="work_dir" value="../../out/build/work_dbg" value_type="e_string" />
<efx:param name="auto_instantiation" value="off" value_type="e_bool" />
<efx:param name="profile" value="NONE" value_type="e_string" />
</efx:debugger>
<efx:security>
<efx:param name="enable_bitstream_encrypt" value="off" value_type="e_bool" />
<efx:param name="enable_bitstream_auth" value="off" value_type="e_bool" />
<efx:param name="randomize_iv_value" value="on" value_type="e_bool" />
</efx:security>
</efx:project>
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""Generate the Efinix Ti375 board shell around the shared Rocket subsystem."""
import argparse
import re
from pathlib import Path
def width_bits(width):
if not width:
return 1
match = re.fullmatch(r"\[(\d+):(\d+)\]", width.strip())
if not match:
raise ValueError(f"unsupported width: {width}")
return abs(int(match.group(1)) - int(match.group(2))) + 1
def parse_ports(verilog_path):
text = Path(verilog_path).read_text()
module_name = Path(verilog_path).stem
start = text.index(f"module {module_name}(")
end = text.index(");", start)
header = re.sub(r"//.*", "", text[start:end])
pieces = re.split(r"\b(input|output)\b", header)[1:]
ports = []
for direction, body in zip(pieces[0::2], pieces[1::2]):
body = body.replace("\n", " ")
body = re.split(r"\b(input|output)\b", body)[0]
default_width = ""
width_match = re.match(r"\s*(\[[^\]]+\])\s*(.*)", body)
if width_match:
default_width = width_match.group(1)
rest = width_match.group(2)
else:
rest = body
for item in rest.split(","):
item = item.strip()
if not item:
continue
item_match = re.match(r"(\[[^\]]+\])\s*(\w+)$", item)
if item_match:
width, name = item_match.groups()
else:
width = default_width
name = item.split()[-1]
if re.fullmatch(r"\w+", name):
ports.append((direction, width, name))
return ports
def const_for(width):
bits = width_bits(width)
return "1'b0" if bits == 1 else f"{bits}'d0"
def is_clock(name):
return name.endswith("_clock") or name == "debug_clock"
def is_reset(name):
return name.endswith("_reset") or name == "debug_reset"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--rocket-top", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args()
ports = parse_ports(args.rocket_top)
port_by_name = {name: (direction, width) for direction, width, name in ports}
required = {
"uart_rxd", "uart_txd",
"spi_sck", "spi_csn", "spi_dq_i", "spi_dq_o", "spi_dq_oe",
"pmod_spi_sck", "pmod_spi_csn",
"pmod_spi_dq_i", "pmod_spi_dq_o", "pmod_spi_dq_oe",
"pmod_irq", "gps_acq_clk", "gps_trck_clk",
"gps_rf_clk", "gps_rf_data", "gps_pps",
}
missing = sorted(required - port_by_name.keys())
if missing:
raise RuntimeError(f"Rocket top is missing required Ti375 ports: {', '.join(missing)}")
passthrough = {"pmod_spi_sck", "pmod_spi_csn", "pmod_irq", "gps_pps"}
def decl(width, name):
return f" wire {width + ' ' if width else ''}{name};"
assigns = []
for direction, width, name in ports:
if direction != "input" or name in passthrough:
continue
if name.startswith("debug_clockeddmi_dmi_req"):
continue
if name in {
"debug_clockeddmi_dmi_resp_ready",
"debug_clockeddmi_dmiClock",
"debug_clockeddmi_dmiReset",
}:
continue
if name == "resetctrl_hartIsInReset_0":
value = "soc_reset"
elif name == "debug_reset":
value = "fabric_reset"
elif is_reset(name):
value = "soc_reset"
elif name == "gps_acq_clk":
value = "gps_acq_clk_gen"
elif name == "gps_trck_clk":
value = "gps_trck_clk_gen"
elif name in {"gps_rf_clk", "gps_afe_clk"}:
value = "rf_clk"
elif name == "gps_rf_data":
value = "rf_data"
elif is_clock(name):
value = "fabric_clk"
elif name == "debug_dmactiveAck":
value = "debug_dmactive"
elif name == "uart_rxd":
value = "uart_rx"
elif name == "spi_dq_i":
value = "flash_dq_IN"
elif name == "pmod_spi_dq_i":
value = "pmod_spi_dq_IN"
elif name == "interrupts":
value = "2'b00"
else:
value = const_for(width)
assigns.append(f" assign {name} = {value};")
port_map = ",\n".join(f" .{name}({name})" for _, _, name in ports)
internal_decls = "\n".join(
decl(width, name) for _, width, name in ports if name not in passthrough
)
content = f"""`timescale 1ns/1ps
module ti375c529_rocket_top (
(* syn_peri_port = 0 *) input wire fabric_clk,
(* syn_peri_port = 0 *) input wire fabric_pll_locked,
(* syn_peri_port = 0 *) input wire board_clk_100,
(* syn_peri_port = 0 *) input wire gps_ref_clk_gen,
(* syn_peri_port = 0 *) input wire gps_acq_clk_gen,
(* syn_peri_port = 0 *) input wire gps_trck_clk_gen,
(* syn_peri_port = 0 *) input wire gps_domain_feedback,
(* syn_peri_port = 0 *) input wire gps_ref_pll_locked,
(* syn_peri_port = 0 *) input wire gps_domain_pll_locked,
(* syn_peri_port = 0 *) input wire reset_n,
(* syn_peri_port = 0 *) input wire uart_rx,
(* syn_peri_port = 0 *) output wire uart_tx,
(* syn_peri_port = 0 *) output wire flash_sck,
(* syn_peri_port = 0 *) output wire flash_csn,
(* syn_peri_port = 0 *) input wire [3:0] flash_dq_IN,
(* syn_peri_port = 0 *) output wire [3:0] flash_dq_OUT,
(* syn_peri_port = 0 *) output wire [3:0] flash_dq_OE,
(* syn_peri_port = 0 *) input wire rf_clk,
(* syn_peri_port = 0 *) input wire [3:0] rf_data,
(* syn_peri_port = 0 *) output wire pmod_spi_sck,
(* syn_peri_port = 0 *) output wire pmod_spi_csn,
(* syn_peri_port = 0 *) input wire [3:0] pmod_spi_dq_IN,
(* syn_peri_port = 0 *) output wire [3:0] pmod_spi_dq_OUT,
(* syn_peri_port = 0 *) output wire [3:0] pmod_spi_dq_OE,
(* syn_peri_port = 0 *) input wire pmod_irq,
(* syn_peri_port = 0 *) output wire gps_pps,
(* syn_peri_port = 0 *) input wire jtag_CAPTURE,
(* syn_peri_port = 0 *) input wire jtag_DRCK,
(* syn_peri_port = 0 *) input wire jtag_RESET,
(* syn_peri_port = 0 *) input wire jtag_RUNTEST,
(* syn_peri_port = 0 *) input wire jtag_SEL,
(* syn_peri_port = 0 *) input wire jtag_SHIFT,
(* syn_peri_port = 0 *) input wire jtag_TCK,
(* syn_peri_port = 0 *) input wire jtag_TDI,
(* syn_peri_port = 0 *) input wire jtag_TMS,
(* syn_peri_port = 0 *) input wire jtag_UPDATE,
(* syn_peri_port = 0 *) output wire jtag_TDO,
(* syn_peri_port = 0 *) output wire [4:0] led
);
wire gps_clock_locked = gps_ref_pll_locked & gps_domain_pll_locked;
wire pll_ready_async = fabric_pll_locked & gps_clock_locked & reset_n;
(* async_reg = "true" *) reg [2:0] pll_ready_sync = 3'b000;
reg [15:0] reset_shift = 16'hffff;
always @(posedge fabric_clk) begin
pll_ready_sync <= {{pll_ready_sync[1:0], pll_ready_async}};
if (!pll_ready_sync[2])
reset_shift <= 16'hffff;
else
reset_shift <= {{reset_shift[14:0], 1'b0}};
end
wire fabric_reset = reset_shift[15];
reg [31:0] heartbeat = 32'd0;
always @(posedge fabric_clk) begin
if (fabric_reset)
heartbeat <= 32'd0;
else
heartbeat <= heartbeat + 32'd1;
end
{internal_decls}
wire soc_reset = fabric_reset | debug_ndreset;
rocket_bscan_dmi_tunnel u_rocket_dmi_tunnel (
.tck(jtag_TCK),
.reset(fabric_reset | jtag_RESET),
.bscan_capture(jtag_CAPTURE),
.bscan_sel(jtag_SEL),
.bscan_shift(jtag_SHIFT),
.bscan_tdi(jtag_TDI),
.bscan_update(jtag_UPDATE),
.bscan_tdo(jtag_TDO),
.dmi_clock(debug_clockeddmi_dmiClock),
.dmi_reset(debug_clockeddmi_dmiReset),
.dmi_req_ready(debug_clockeddmi_dmi_req_ready),
.dmi_req_valid(debug_clockeddmi_dmi_req_valid),
.dmi_req_addr(debug_clockeddmi_dmi_req_bits_addr),
.dmi_req_data(debug_clockeddmi_dmi_req_bits_data),
.dmi_req_op(debug_clockeddmi_dmi_req_bits_op),
.dmi_resp_ready(debug_clockeddmi_dmi_resp_ready),
.dmi_resp_valid(debug_clockeddmi_dmi_resp_valid),
.dmi_resp_data(debug_clockeddmi_dmi_resp_bits_data),
.dmi_resp_resp(debug_clockeddmi_dmi_resp_bits_resp)
);
assign pmod_spi_dq_OUT = pmod_spi_dq_o;
assign pmod_spi_dq_OE = pmod_spi_dq_oe;
assign flash_sck = spi_sck;
assign flash_csn = spi_csn;
assign flash_dq_OUT = spi_dq_o;
assign flash_dq_OE = spi_dq_oe;
assign uart_tx = uart_txd;
// The kit LEDs are active-low. Show heartbeat, PLL locks, reset release,
// and debug-module activity from LED13 down to LED9.
assign led = ~{{heartbeat[25], gps_clock_locked, fabric_pll_locked,
~fabric_reset, debug_dmactive}};
{chr(10).join(assigns)}
(* syn_preserve = 1 *)
{Path(args.rocket_top).stem} u_rocket (
{port_map}
);
// These hard-TAP outputs are intentionally unused by the DMI framing logic.
wire _unused_hard_signals = jtag_DRCK ^ jtag_RUNTEST ^ jtag_TMS ^
board_clk_100 ^ gps_ref_clk_gen ^
gps_domain_feedback;
endmodule
"""
output = Path(args.out)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(content)
if __name__ == "__main__":
main()
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Build Efinity raw-HEX images for Ti375C529 persistent Rocket boot."""
import argparse
import hashlib
import json
import re
from pathlib import Path
HEX_BYTE = re.compile(r"[0-9a-fA-F]{2}")
def parse_int(value):
return int(value, 0)
def read_raw_hex(path):
data = bytearray()
with path.open() as source:
for line_number, line in enumerate(source, 1):
value = line.strip()
if not HEX_BYTE.fullmatch(value):
raise SystemExit(
f"{path}:{line_number}: expected one hexadecimal byte per line"
)
data.append(int(value, 16))
return bytes(data)
def write_raw_hex(path, chunks):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as output:
for chunk in chunks:
for value in chunk:
output.write(f"{value:02X}\n")
def sha256(data):
return hashlib.sha256(data).hexdigest()
def main():
parser = argparse.ArgumentParser(
description="Combine an Efinity configuration HEX and Rocket app image"
)
parser.add_argument("--config-hex", type=Path, required=True)
parser.add_argument("--app-bin", type=Path, required=True)
parser.add_argument("--app-offset", type=parse_int, required=True)
parser.add_argument("--flash-size", type=parse_int, default=0x04000000)
parser.add_argument("--combined-hex", type=Path, required=True)
parser.add_argument("--app-hex", type=Path, required=True)
parser.add_argument("--manifest", type=Path, required=True)
args = parser.parse_args()
config = read_raw_hex(args.config_hex)
app = args.app_bin.read_bytes()
app_end = args.app_offset + len(app)
if args.app_offset & 0xFFFF:
raise SystemExit("application offset must be aligned to a 64-KiB sector")
if len(config) > args.app_offset:
raise SystemExit(
f"configuration image (0x{len(config):x} bytes) overlaps application "
f"at 0x{args.app_offset:x}"
)
if app_end > args.flash_size:
raise SystemExit(
f"application ends at 0x{app_end:x}, beyond flash size 0x{args.flash_size:x}"
)
padding_size = args.app_offset - len(config)
write_raw_hex(args.app_hex, (app,))
write_raw_hex(args.combined_hex, (config, bytes([0xFF]) * padding_size, app))
manifest = {
"format": "Efinity raw HEX (one byte per line)",
"flash": "Ti375C529 lower SPI NOR (U47)",
"flash_size": f"0x{args.flash_size:08x}",
"configuration": {
"source": str(args.config_hex),
"offset": "0x00000000",
"size": len(config),
"sha256": sha256(config),
},
"application": {
"source": str(args.app_bin),
"offset": f"0x{args.app_offset:08x}",
"size": len(app),
"sha256": sha256(app),
},
"combined": {
"image": str(args.combined_hex),
"programmed_length": app_end,
},
}
args.manifest.parent.mkdir(parents=True, exist_ok=True)
args.manifest.write_text(json.dumps(manifest, indent=2) + "\n")
print(f"configuration: 0x00000000..0x{len(config) - 1:08x}")
print(f"application: 0x{args.app_offset:08x}..0x{app_end - 1:08x}")
print(f"combined HEX: {args.combined_hex}")
if __name__ == "__main__":
main()
@@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""Apply small, deterministic Efinity synthesis adaptations to generated RTL."""
from __future__ import annotations
import argparse
import math
import re
from pathlib import Path
ROCKET_STORE_DATA = """ else begin
automatic logic [3:0][63:0] _GEN_24 =
{{ex_rs_1}, {{2{ex_rs_1[31:0]}}}, {{4{ex_rs_1[15:0]}}}, {{8{ex_rs_1[7:0]}}}};
mem_reg_rs2 <= _GEN_24[ex_reg_mem_size];
end"""
EFINITY_STORE_DATA = """ else begin
// Efinity 2025.2 leaves the block-local packed automatic array above
// undriven (VDB-1002). Spell out the byte-lane replication instead.
case (ex_reg_mem_size)
2'h0: mem_reg_rs2 <= {8{ex_rs_1[7:0]}};
2'h1: mem_reg_rs2 <= {4{ex_rs_1[15:0]}};
2'h2: mem_reg_rs2 <= {2{ex_rs_1[31:0]}};
default: mem_reg_rs2 <= ex_rs_1;
endcase
end"""
ATOMIC_LUT_DECLARATION = re.compile(
r"^(?P<indent>\s*)automatic logic \[3:0\]\[3:0\] (?P<name>_GEN_[0-9]+) "
r"= '\{4'hC, 4'h8, 4'hE, 4'h6\};$",
re.MULTILINE,
)
NCO_ROM_START = '(*rom_style = "distributed" *) logic [PH_ACC_WL-1 : 0] freq_word_arr'
NCO_ROM_END = "logic [PH_ACC_WL-1 : 0] sin_phase, cos_phase;"
def parse_integer_parameter(text: str, name: str) -> int:
match = re.search(rf"\bparameter\s+{re.escape(name)}\s*=\s*([0-9]+)\s*;", text)
if match is None:
raise RuntimeError(f"could not find integer parameter {name}")
return int(match.group(1))
def parse_sv_integer(token: str) -> tuple[int, int | None]:
token = token.strip().replace("_", "")
match = re.fullmatch(r"(-)?([0-9]+)'([dDhHbB])([0-9a-fA-F]+)", token)
if match is not None:
sign, width_text, radix_text, digits = match.groups()
radix = {"d": 10, "h": 16, "b": 2}[radix_text.lower()]
value = int(digits, radix)
if sign:
value = -value
return value, int(width_text)
if re.fullmatch(r"-?[0-9]+", token):
return int(token), None
raise RuntimeError(f"unsupported SystemVerilog integer literal: {token!r}")
def parse_array_parameter(text: str, name: str) -> list[tuple[int, int | None]]:
match = re.search(
rf"\bparameter\s+\[[^\]]+\]\s+{re.escape(name)}\s*\[[^\]]+\]\s*=\s*\{{(.*?)\}}\s*;",
text,
re.DOTALL,
)
if match is None:
raise RuntimeError(f"could not find array parameter {name}")
return [parse_sv_integer(token) for token in match.group(1).split(",") if token.strip()]
def sv_case_function(
name: str,
input_width: str,
output_width: str,
values: list[int],
literal_width: int,
) -> str:
lines = [
f"function automatic logic [{output_width}-1 : 0] {name};",
f" input logic [{input_width}-1 : 0] index;",
" begin",
" case (index)",
]
for index, value in enumerate(values):
literal = f"-{literal_width}'sd{-value}" if value < 0 else f"{literal_width}'d{value}"
lines.append(f" {index}: {name} = {literal};")
lines.extend(
[
f" default: {name} = '0;",
" endcase",
" end",
"endfunction",
]
)
return "\n".join(lines)
def round_sv_real(value: float) -> int:
"""Match real-to-integral assignment rounding used by SystemVerilog."""
return math.floor(value + 0.5) if value >= 0 else math.ceil(value - 0.5)
def build_nco_rom_functions(definitions: str) -> str:
phase_width = parse_integer_parameter(definitions, "ACQ_NCO_PH_ACC_WL")
sine_integer_bits = parse_integer_parameter(definitions, "ACQ_SIN_IL")
sine_fraction_bits = parse_integer_parameter(definitions, "ACQ_SIN_FL")
lut_address_width = parse_integer_parameter(definitions, "ACQ_SIN_LUT_DEPTH_WL")
freq_values = [value for value, _ in parse_array_parameter(definitions, "FREQ_WORD_ARRAY")]
pullin_values = [value for value, _ in parse_array_parameter(definitions, "PULLIN_FREQ_WORD_ARRAY")]
sine_width = sine_integer_bits + sine_fraction_bits
sine_values = [
round_sv_real(
math.sin(2.0 * math.pi * index / (1 << lut_address_width))
* (1 << sine_fraction_bits)
)
for index in range(1 << lut_address_width)
]
return "\n\n".join(
[
"// Efinity 2025.2 mis-elaborates the original initial/$sin ROMs.\n"
"// These constant functions are generated from definitions.sv for Ti375 only.",
sv_case_function(
"efinity_freq_word", "FREQ_BIN_WL", "PH_ACC_WL", freq_values, phase_width
),
sv_case_function(
"efinity_pullin_word", "NUM_PULLIN_WL", "PH_ACC_WL", pullin_values, phase_width
),
sv_case_function(
"efinity_sine", "LUT_DEPTH_WL", "ACQ_SIN_IL + ACQ_SIN_FL", sine_values, sine_width
),
]
)
def rewrite_acquisition_nco(source: Path, destination: Path, definitions: str) -> None:
text = source.read_text(encoding="utf-8")
start = text.find(NCO_ROM_START)
end = text.find(NCO_ROM_END, start)
if start < 0 or end < 0:
raise RuntimeError(f"could not locate acquisition NCO ROM block in {source}")
text = text[:start] + build_nco_rom_functions(definitions) + "\n\n" + text[end:]
old_sum = (
"freq_word_arr[freq_bin_idx_i] + "
"pullin_freq_word_arr[pullin_freq_bin_idx_i]"
)
new_sum = (
"efinity_freq_word(freq_bin_idx_i) + "
"efinity_pullin_word(pullin_freq_bin_idx_i)"
)
if text.count(old_sum) != 2:
raise RuntimeError(f"expected two NCO phase increments in {source}")
text = text.replace(old_sum, new_sum)
if text.count("sinwav_lut[truncated_sin_phase]") != 1:
raise RuntimeError(f"expected one sine ROM read in {source}")
if text.count("sinwav_lut[truncated_cos_phase]") != 1:
raise RuntimeError(f"expected one cosine ROM read in {source}")
text = text.replace("sinwav_lut[truncated_sin_phase]", "efinity_sine(truncated_sin_phase)")
text = text.replace("sinwav_lut[truncated_cos_phase]", "efinity_sine(truncated_cos_phase)")
destination.write_text(text, encoding="utf-8")
def rewrite_tracking_nco(source: Path, destination: Path, definitions: str) -> None:
text = source.read_text(encoding="utf-8")
declaration = (
'(*rom_style = "distributed" *) logic [SIN_IL-1 : -SIN_FL] '
"sinwav_lut [0 : 2**LUT_DEPTH_WL-1];"
)
values = [value for value, _ in parse_array_parameter(definitions, "TRCK_SINWAV_LUT")]
expected_entries = parse_integer_parameter(definitions, "NUM_TRCK_SINWAV_LUT")
if len(values) != expected_entries:
raise RuntimeError(
f"expected {expected_entries} tracking sine entries, got {len(values)}"
)
function = (
"// Efinity 2025.2 leaves the original package-array initialized ROM undriven.\n"
"// This Ti375-only constant function preserves the exact definitions.sv values.\n"
+ sv_case_function(
"efinity_tracking_sine",
"LUT_DEPTH_WL",
"SIN_IL + SIN_FL",
values,
parse_integer_parameter(definitions, "TRCK_CARR_NCO_SIN_WL"),
)
)
if text.count(declaration) != 1:
raise RuntimeError(f"expected one tracking sine ROM declaration in {source}")
text = text.replace(declaration, function)
initializer = """initial begin
sinwav_lut = TRCK_SINWAV_LUT;
end
"""
if text.count(initializer) != 1:
raise RuntimeError(f"expected one tracking sine ROM initializer in {source}")
text = text.replace(initializer, "")
if text.count("sinwav_lut[accum_truncated_sin]") != 1:
raise RuntimeError(f"expected one tracking sine ROM read in {source}")
if text.count("sinwav_lut[accum_truncated_cos]") != 1:
raise RuntimeError(f"expected one tracking cosine ROM read in {source}")
text = text.replace(
"sinwav_lut[accum_truncated_sin]", "efinity_tracking_sine(accum_truncated_sin)"
)
text = text.replace(
"sinwav_lut[accum_truncated_cos]", "efinity_tracking_sine(accum_truncated_cos)"
)
destination.write_text(text, encoding="utf-8")
def write_mem_file(path: Path, entries: list[tuple[int, int | None]], width: int) -> None:
mask = (1 << width) - 1
path.write_text(
# Binary keeps each row exactly TWIDDLE_WIDTH bits. A hexadecimal row
# would round 15 bits up to four digits and Efinity warns about a
# spurious 16-to-15-bit truncation even though the value is correct.
"".join(f"{value & mask:0{width}b}\n" for value, _ in entries),
encoding="ascii",
)
def rewrite_twiddle_rom(source: Path, destination: Path) -> None:
text = source.read_text(encoding="utf-8")
old = """initial begin
cos_rom = COS_ROM_ARRAY;
sin_rom = SIN_ROM_ARRAY;
end"""
new = """initial begin
// Generated Ti375 files are resolved through Efinity's memory-init include path.
$readmemb("ti375_twiddle_cos_15.mem", cos_rom);
$readmemb("ti375_twiddle_sin_15.mem", sin_rom);
end"""
if text.count(old) != 1:
raise RuntimeError(f"expected one twiddle ROM initializer in {source}")
destination.write_text(text.replace(old, new), encoding="utf-8")
def prepare_gps_rtl(source_dir: Path, output_dir: Path) -> None:
definitions_path = source_dir / "common" / "definitions.sv"
definitions = definitions_path.read_text(encoding="utf-8")
output_dir.mkdir(parents=True, exist_ok=True)
rewrite_acquisition_nco(
source_dir / "acq_src" / "acq_nco_carr.sv",
output_dir / "acq_nco_carr.sv",
definitions,
)
rewrite_tracking_nco(
source_dir / "trck_src" / "NCO_carr.sv",
output_dir / "NCO_carr.sv",
definitions,
)
rewrite_twiddle_rom(
source_dir / "acq_src" / "twiddle_ram.sv",
output_dir / "twiddle_ram.sv",
)
cos_entries = parse_array_parameter(definitions, "COS_ROM_ARRAY")
sin_entries = parse_array_parameter(definitions, "SIN_ROM_ARRAY")
expected_entries = parse_integer_parameter(definitions, "NUM_TWIDDLE_ROM_IDX")
if len(cos_entries) != expected_entries or len(sin_entries) != expected_entries:
raise RuntimeError(
f"expected {expected_entries} twiddle entries, got "
f"cos={len(cos_entries)} sin={len(sin_entries)}"
)
write_mem_file(output_dir / "ti375_twiddle_cos_15.mem", cos_entries, 15)
write_mem_file(output_dir / "ti375_twiddle_sin_15.mem", sin_entries, 15)
def rewrite_rocket_store_data(path: Path) -> None:
text = path.read_text(encoding="utf-8")
if EFINITY_STORE_DATA in text:
return
if text.count(ROCKET_STORE_DATA) != 1:
raise RuntimeError(f"expected one Rocket store-data array in {path}")
path.write_text(text.replace(ROCKET_STORE_DATA, EFINITY_STORE_DATA), encoding="utf-8")
def rewrite_tilelink_atomic_lut(path: Path) -> None:
text = path.read_text(encoding="utf-8")
if "// Avoid Efinity VDB-1002 on the block-local packed LUT." in text:
return
match = ATOMIC_LUT_DECLARATION.search(text)
if match is None:
raise RuntimeError(f"expected one TileLink atomic packed LUT in {path}")
if ATOMIC_LUT_DECLARATION.search(text, match.end()) is not None:
raise RuntimeError(f"expected only one TileLink atomic packed LUT in {path}")
indent = match.group("indent")
temporary = match.group("name")
old_read = f"{indent}cam_a_0_lut <= {temporary}[auto_in_a_bits_param[1:0]];"
new_read = f"""{indent}// Avoid Efinity VDB-1002 on the block-local packed LUT.
{indent}case (auto_in_a_bits_param[1:0])
{indent} 2'h0: cam_a_0_lut <= 4'h6;
{indent} 2'h1: cam_a_0_lut <= 4'hE;
{indent} 2'h2: cam_a_0_lut <= 4'h8;
{indent} default: cam_a_0_lut <= 4'hC;
{indent}endcase"""
if text.count(old_read) != 1:
raise RuntimeError(f"expected one TileLink atomic packed LUT read in {path}")
text = text[: match.start()] + text[match.end() + 1 :]
text = text.replace(old_read, new_read)
path.write_text(text, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--verilog-dir", type=Path, required=True)
parser.add_argument("--gps-source-dir", type=Path, required=True)
parser.add_argument("--gps-output-dir", type=Path, required=True)
args = parser.parse_args()
rewrite_rocket_store_data(args.verilog_dir / "Rocket.sv")
rewrite_tilelink_atomic_lut(args.verilog_dir / "TLAtomicAutomata_pbus.sv")
rewrite_tilelink_atomic_lut(args.verilog_dir / "TLAtomicAutomata_cbus.sv")
prepare_gps_rtl(args.gps_source_dir, args.gps_output_dir)
if __name__ == "__main__":
main()
+21
View File
@@ -30,6 +30,27 @@ if [[ -n "$TOOLCHAIN" ]]; then
fi
check riscv-gcc "$riscv_gcc" --version
if [[ -n "$riscv_gcc" && -x "$riscv_gcc" ]]; then
medany_dir="$(mktemp -d "${TMPDIR:-/tmp}/gps-medany-check.XXXXXXXX")"
printf '%s\n' \
'#include <stdlib.h>' \
'void _start(void) { void *p = malloc(16); free(p); for (;;) {} }' \
> "$medany_dir/check.c"
if "$riscv_gcc" \
-march=rv64imafdc_zicsr_zifencei -mabi=lp64d -mcmodel=medany \
-Os -ffreestanding -fno-builtin -static -nostartfiles \
-Wl,-Ttext=0x80000000 -Wl,--unresolved-symbols=ignore-all \
"$medany_dir/check.c" -Wl,--start-group -lc -lgcc -Wl,--end-group \
-o "$medany_dir/check.elf" >/dev/null 2>&1; then
printf 'OK %-10s %s\n' riscv-libc 'medany @ 0x80000000'
else
printf 'INCOMPAT %-10s %s\n' riscv-libc \
'libc is not medany; select RISCV_TOOLCHAIN=/tools/toolchains/rvtc-medany'
status=1
fi
rm -rf "$medany_dir"
fi
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
printf 'WARNING Kilitli JDK/CIRCT paketleri Linux-x86_64 icindir.\n'
fi
+19 -4
View File
@@ -16,28 +16,41 @@ FIRTOOL ?= $(firstword $(wildcard $(SOC_TOOLS_ROOT)/circt-current/bin/firtool) $
Z3_LIB_DIR ?= $(firstword $(wildcard $(SOC_TOOLS_ROOT)/z3-current/bin) $(wildcard $(PROJECT_TOOLS_DIR)/z3/*/bin))
CIRCT_LIB_DIR ?= $(patsubst %/bin/firtool,%/lib,$(FIRTOOL))
VIVADO ?= $(lastword $(sort $(wildcard /tools/Xilinx/*/Vivado/bin/vivado) $(shell command -v vivado 2>/dev/null)))
EFINITY_HOME ?= $(lastword $(sort $(wildcard /tools/efinix/efinity/*)))
EFINITY_SETUP ?= $(EFINITY_HOME)/bin/setup.sh
EFX_RUN ?= $(EFINITY_HOME)/scripts/efx_run.py
EFX_PGM ?= $(EFINITY_HOME)/pgm/bin/ftdi_pgm.sh
# RISCV_TOOLCHAIN may point either to the toolchain root or its bin directory.
# TOOLCHAIN keeps compatibility with the existing software Makefiles.
RISCV_TOOLCHAIN ?=
_RISCV_SEARCH_DIRS := \
$(if $(strip $(RISCV_TOOLCHAIN)),$(RISCV_TOOLCHAIN) $(RISCV_TOOLCHAIN)/bin) \
$(TOOLCHAIN) \
$(RISCV_TOOLCHAIN) \
$(RISCV_TOOLCHAIN)/bin \
$(PROJECT_TOOLS_DIR)/riscv/bin \
/tools/toolchains-medany/bin \
/tools/toolchains/rvtc-medany/bin \
/tools/toolchain/bin \
/tools/toolchains/bin \
/tools/toolchains/rvtc-medany/bin
/tools/toolchains/bin
_RISCV_GCC := $(firstword \
$(foreach d,$(_RISCV_SEARCH_DIRS),$(wildcard $(d)/riscv64-unknown-elf-gcc)) \
$(foreach d,$(_RISCV_SEARCH_DIRS),$(wildcard $(d)/riscv64-none-elf-gcc)) \
$(shell command -v riscv64-unknown-elf-gcc 2>/dev/null) \
$(shell command -v riscv64-none-elf-gcc 2>/dev/null))
# An explicit RISCV_TOOLCHAIN is authoritative, including over an old
# TOOLCHAIN/RISCV_PREFIX inherited from the shell. Without override, Make's
# environment-variable semantics leave CROSS pointing at the stale toolchain
# even though _RISCV_GCC was found in RISCV_TOOLCHAIN.
ifneq ($(strip $(RISCV_TOOLCHAIN)),)
override TOOLCHAIN := $(patsubst %/,%,$(dir $(_RISCV_GCC)))
override RISCV_PREFIX := $(if $(_RISCV_GCC),$(patsubst %-gcc,%-,$(notdir $(_RISCV_GCC))),riscv64-unknown-elf-)
override CROSS := $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
else
TOOLCHAIN ?= $(patsubst %/,%,$(dir $(_RISCV_GCC)))
RISCV_PREFIX ?= $(if $(_RISCV_GCC),$(patsubst %-gcc,%-,$(notdir $(_RISCV_GCC))),riscv64-unknown-elf-)
CROSS ?= $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
endif
# Some compiler-only toolchains (notably rvtc-medany) do not ship GDB. Keep
# using the selected compiler toolchain, but fall back to the full local
@@ -45,6 +58,8 @@ CROSS ?= $(if $(strip $(TOOLCHAIN)),$(TOOLCHAIN)/)$(RISCV_PREFIX)
_RISCV_GDB := $(firstword \
$(wildcard $(TOOLCHAIN)/$(RISCV_PREFIX)gdb) \
$(wildcard /tools/toolchains/tc/toolchain/bin/$(RISCV_PREFIX)gdb) \
$(wildcard /tools/toolchain/bin/$(RISCV_PREFIX)gdb) \
$(lastword $(sort $(wildcard /tools/Xilinx/*/gnu/riscv/lin/bin/$(RISCV_PREFIX)gdb))) \
$(shell command -v $(RISCV_PREFIX)gdb 2>/dev/null))
GDB ?= $(if $(_RISCV_GDB),$(_RISCV_GDB),$(CROSS)gdb)