Building a Custom GPS L1 Tracking Engine on FPGA: An Engineering Journey

🔗 Source Code & Full Documentation: GitHub Repository

When I decided to build a custom GPS L1 Baseband Tracking Engine from scratch, I knew it wasn’t going to be easy. I spent several hours a day sitting in my chair, staring at waveforms, debugging code, and questioning my life choices. You can buy a commercial GPS chip for a few dollars, but those chips are black boxes. I wanted access to the raw internal tracking loops, I/Q samples, and correlator outputs. I wanted to build a system capable of ultra-tight integration and resilient against interference. This is the story of how I built it.

The High-Level Architecture

I originally started by prototyping the DSP datapath using Python (NumPy), building a float64 baseline. My goal was to eventually run this on a Xilinx Zynq-7000 FPGA (PYNQ-Z2) to take advantage of the massive parallelism of hardware.

The core architecture I developed features:
Carrier NCO: A precision 32-bit Phase Accumulator coupled with a custom Sine/Cosine ROM for local carrier wipeoff.
Code NCO: A 33-bit Phase Accumulator handling high-frequency Doppler shifts.
Gold Code Generator: Real-time PRN sequence generation for GPS satellites.
Correlators: Cycle-aligned Early, Prompt, and Late (E/P/L) integrators for In-phase (I) and Quadrature (Q) arms.
AXI Integration: AXI-Stream for high-bandwidth RF data and AXI-Lite for configuration registers communicating with the ARM PS.

The Struggle: HLS vs. Native VHDL

My first attempt at moving to hardware was using Xilinx Vitis HLS. I spent an entire week fighting with HLS pragmas (#pragma HLS PIPELINE, #pragma HLS UNROLL). No matter what I did, the latency was completely non-deterministic. Sometimes the loop took 12 clock cycles, sometimes 15. In GPS tracking, phase and timing are everything. If my NCO phase jumped because of pipeline stalls, the PLL completely lost lock.

I realized I couldn’t rely on the “HLS Illusion.” I needed true, cycle-accurate control over the DSP datapath. So, I bit the bullet and rewrote the Carrier NCO, Code NCO, and Correlators in pure, structural Native VHDL. It was a massive step backward in time, but when I simulated the native VHDL and saw exactly a 1-cycle latency between the NCO and the multiplier, I knew it was the right call. The determinism of native RTL design was absolutely necessary here.

The NCO Truncation Bug (The “Jumpy” Phase Issue)

Even after switching to VHDL, I ran into a wall. I fed the hardware some test data I generated in Python, and the output from my Correlator was complete garbage.

I stared at Vivado simulation waveforms for two days. The phase accumulator was incrementing correctly, but the actual Sine/Cosine output from my Lookup Table (ROM) looked jittery. I eventually realized that in my Python code, I was using 64-bit floating-point math. But in the FPGA, I was truncating my 32-bit phase accumulator down to the top 10 bits to address my ROM (1024 entries).

I wasn’t rounding; I was just slicing the bits off. At certain phase boundaries, this introduced a severe quantization error, causing the NCO to output a frequency that essentially vibrated, throwing off the Costas Loop. I fixed this by remodeling the exact bit-width quantization in MATLAB/Python and expanding the ROM to reduce the quantization noise.

Hardware Acceleration vs. CPU execution

Why go through all this trouble? Because an FPGA obliterates a CPU for this specific task. To track just one satellite, a CPU has to perform thousands of multiplications sequentially. To get a 3D position fix, I needed to track at least 4 satellites. A CPU struggles to keep up with the 4 MSPS data rate.

By pushing the high-speed math (Carrier Wipe-off, Code Correlation) to the FPGA’s Programmable Logic (PL), I achieved massive parallelism. The FPGA runs multiple tracking “channels” independently, taking a fraction of the power and time, while leaving the ARM processor (PS) free to handle the lightweight high-level loop filters (PLL/DLL).

Hardware-in-the-Loop (HIL) Verification

Hardware is only as good as the math it computes. I developed a rigorous Hardware-in-the-Loop verification pipeline:
1. Raw GPS RF I/Q baseband data is loaded into the FPGA’s DDR memory and streamed to the PL via DMA.
2. The Native VHDL pipeline processes 1ms epochs in real-time.
3. The ARM PS reads the integrators via AXI-Lite and updates the NCOs.
4. The tracking history is compared against my theoretical pure-Python float64 reference model.

The result? The hardware successfully locks onto all available satellites and matches the float64 tracking model to within the acceptable DSP noise floor margin (<20Hz drift).

This project was an absolute battle, but building a mathematically proven, custom DSP engine from the ground up proved to me the absolute necessity of mastering the lowest levels of hardware design.

(If you are looking for an engineer who can bridge the gap between complex theoretical math and deterministically perfect FPGA hardware, this is the kind of dedication I bring to the table. Let’s talk.)