diff --git a/.gitignore b/.gitignore
index d855b3f..ebfc574 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
# Generated build outputs.
/out/
/soc/out/
+/hardware/generated/
# Python runtime caches and privacy-sensitive raw board captures.
__pycache__/
diff --git a/Makefile b/Makefile
index c8c3d0d..c0182e6 100644
--- a/Makefile
+++ b/Makefile
@@ -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:
diff --git a/hardware/fpga/ti375c529/constraints/ti375c529_rocket.isf b/hardware/fpga/ti375c529/constraints/ti375c529_rocket.isf
new file mode 100644
index 0000000..096f150
--- /dev/null
+++ b/hardware/fpga/ti375c529/constraints/ti375c529_rocket.isf
@@ -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")
diff --git a/hardware/fpga/ti375c529/constraints/ti375c529_rocket.sdc b/hardware/fpga/ti375c529/constraints/ti375c529_rocket.sdc
new file mode 100644
index 0000000..a5f092f
--- /dev/null
+++ b/hardware/fpga/ti375c529/constraints/ti375c529_rocket.sdc
@@ -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}]
diff --git a/hardware/fpga/ti375c529/constraints/ti375c529_rocket_clock_groups.sdc b/hardware/fpga/ti375c529/constraints/ti375c529_rocket_clock_groups.sdc
new file mode 100644
index 0000000..c0a1e08
--- /dev/null
+++ b/hardware/fpga/ti375c529/constraints/ti375c529_rocket_clock_groups.sdc
@@ -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}]
diff --git a/hardware/fpga/ti375c529/rtl/Makefile b/hardware/fpga/ti375c529/rtl/Makefile
new file mode 100644
index 0000000..9d9fb78
--- /dev/null
+++ b/hardware/fpga/ti375c529/rtl/Makefile
@@ -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
diff --git a/targets/fpga/ti375c529/.gitignore b/targets/fpga/ti375c529/.gitignore
new file mode 100644
index 0000000..21d7d8e
--- /dev/null
+++ b/targets/fpga/ti375c529/.gitignore
@@ -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_*
diff --git a/targets/fpga/ti375c529/Makefile b/targets/fpga/ti375c529/Makefile
new file mode 100644
index 0000000..1ba5bd8
--- /dev/null
+++ b/targets/fpga/ti375c529/Makefile
@@ -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:
diff --git a/targets/fpga/ti375c529/README.md b/targets/fpga/ti375c529/README.md
new file mode 100644
index 0000000..dd2b042
--- /dev/null
+++ b/targets/fpga/ti375c529/README.md
@@ -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 |
diff --git a/targets/fpga/ti375c529/board.me b/targets/fpga/ti375c529/board.me
new file mode 100644
index 0000000..24ca87c
--- /dev/null
+++ b/targets/fpga/ti375c529/board.me
@@ -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
diff --git a/targets/fpga/ti375c529/flow/debug/gdb_attach.gdb b/targets/fpga/ti375c529/flow/debug/gdb_attach.gdb
new file mode 100644
index 0000000..f027923
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/debug/gdb_attach.gdb
@@ -0,0 +1 @@
+monitor halt
diff --git a/targets/fpga/ti375c529/flow/debug/gdb_load_and_run.gdb b/targets/fpga/ti375c529/flow/debug/gdb_load_and_run.gdb
new file mode 100644
index 0000000..db56f1c
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/debug/gdb_load_and_run.gdb
@@ -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
diff --git a/targets/fpga/ti375c529/flow/debug/openocd-0.12-ti375-bscan.patch b/targets/fpga/ti375c529/flow/debug/openocd-0.12-ti375-bscan.patch
new file mode 100644
index 0000000..a5ccf3d
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/debug/openocd-0.12-ti375-bscan.patch
@@ -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,
diff --git a/targets/fpga/ti375c529/flow/debug/openocd_ti375c529_bscan.cfg b/targets/fpga/ti375c529/flow/debug/openocd_ti375c529_bscan.cfg
new file mode 100644
index 0000000..3c183b0
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/debug/openocd_ti375c529_bscan.cfg
@@ -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
diff --git a/targets/fpga/ti375c529/flow/efinity/README.md b/targets/fpga/ti375c529/flow/efinity/README.md
new file mode 100644
index 0000000..50b1f0f
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/efinity/README.md
@@ -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.
diff --git a/targets/fpga/ti375c529/flow/efinity/ti375c529_rocket.xml b/targets/fpga/ti375c529/flow/efinity/ti375c529_rocket.xml
new file mode 100644
index 0000000..4d1aad7
--- /dev/null
+++ b/targets/fpga/ti375c529/flow/efinity/ti375c529_rocket.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/targets/fpga/ti375c529/tools/gen_ti375c529_wrapper.py b/targets/fpga/ti375c529/tools/gen_ti375c529_wrapper.py
new file mode 100644
index 0000000..3fdaa69
--- /dev/null
+++ b/targets/fpga/ti375c529/tools/gen_ti375c529_wrapper.py
@@ -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()
diff --git a/targets/fpga/ti375c529/tools/make_flash_hex.py b/targets/fpga/ti375c529/tools/make_flash_hex.py
new file mode 100644
index 0000000..e2888ec
--- /dev/null
+++ b/targets/fpga/ti375c529/tools/make_flash_hex.py
@@ -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()
diff --git a/targets/fpga/ti375c529/tools/prepare_efinity_rtl.py b/targets/fpga/ti375c529/tools/prepare_efinity_rtl.py
new file mode 100644
index 0000000..d97684c
--- /dev/null
+++ b/targets/fpga/ti375c529/tools/prepare_efinity_rtl.py
@@ -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\s*)automatic logic \[3:0\]\[3:0\] (?P_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()
diff --git a/tools/project.mk b/tools/project.mk
index d80222a..27cb321 100644
--- a/tools/project.mk
+++ b/tools/project.mk
@@ -16,6 +16,10 @@ 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.