# FPGA Software Replica for Offline Radar Data Processing and Replay: A Complete Guide

> Explore NawfalMotii79/PLFM_RADAR for a bit-accurate FPGA software replica of the AERIS-10 radar. Enable offline radar data processing and replay for algorithm development without hardware.

- Repository: [NawfalMotii79/PLFM_RADAR](https://github.com/NawfalMotii79/PLFM_RADAR)
- Tags: how-to-guide
- Published: 2026-08-20

---

**The AERIS-10 radar platform provides a bit-accurate Python replica (`SoftwareFPGA`) that mirrors the complete FPGA signal processing chain, enabling offline radar data replay and algorithm development without hardware.**

This guide explores the `SoftwareFPGA` module from the open-source AERIS-10 phased-array radar project. You'll learn how to use this FPGA software replica for offline radar data processing, from loading raw I/Q samples through generating detection-ready `RadarFrame` objects identical to hardware output.

## What Is the AERIS-10 Radar Platform?

AERIS-10 is a complete 10.5 GHz phased-array radar system combining physical hardware with a comprehensive software stack. The repository at `NawfalMotii79/PLFM_RADAR` contains everything from PCB schematics to FPGA firmware—and critically, a **software replica of the FPGA processing chain** that runs entirely in Python.

The platform supports two antenna configurations:

- **Nexus**: 8 × 16 patch array, approximately 3 km range
- **Extended**: 32 × 16 slotted waveguide array, approximately 20 km range

The hardware FPGA (Xilinx XC7A50T) executes a fixed pipeline: range FFT, range-bin decimation, MTI cancellation, Doppler FFT, DC notch filtering, and CFAR detection. The `SoftwareFPGA` class in [`9_Firmware/9_3_GUI/v7/software_fpga.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/software_fpga.py) replicates this pipeline exactly, bit-for-bit.

## Hardware Signal Processing Pipeline

Understanding the hardware chain clarifies what the software replica implements. The FPGA processes each chirp through these deterministic stages:

| Stage | Function | Hardware Location |
|-------|----------|-------------------|
| ADC Capture | 16-bit I/Q sampling | Main board ADC |
| Range FFT | 1024-point FFT per chirp | XC7A50T FPGA fabric |
| Range Decimation | 1024 → 64 bins | FPGA |
| MTI Canceller | Static clutter suppression | FPGA |
| Doppler FFT | 16-point FFT across chirps | FPGA |
| DC Notch | Zero DC bin removal | FPGA |
| Detection | Threshold or CFAR | FPGA |
| Frame Assembly | Pack into `RadarFrame` | FPGA → USB |

The `SoftwareFPGA` class mirrors this exact sequence using reference implementations from [`golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/golden_reference.py).

## The SoftwareFPGA API: Bit-Accurate Offline Processing

The `SoftwareFPGA` class provides a register-mirror architecture that matches the FPGA's control interface. This design ensures configuration parameters (CFAR enable, guard cells, training cells) produce identical results in software and hardware.

### Core API Structure

```python
from software_fpga import SoftwareFPGA, quantize_raw_iq

# Initialize the replica with default register values

fpga = SoftwareFPGA()

# Configure detection parameters

fpga.set_cfar_enable(True)      # Enable CFAR vs. simple threshold

fpga.set_cfar_guard(2)           # Guard cells around CUT

fpga.set_cfar_train(8)           # Training cells for noise estimation

# Process quantized I/Q data

frame = fpga.process_chirps(iq_i, iq_q, frame_number=0, timestamp=0.0)

```

### Register Mirror Implementation

The constructor populates default register values matching the RTL reset state:

```python

# From software_fpga.py - initialization mimics FPGA reset

self._registers = {
    'cfar_enable': 0,
    'cfar_guard_cells': 2,
    'cfar_training_cells': 8,
    'cfar_mode': 0,  # CA (Cell Averaging)

    'mti_enable': 1,
    'dc_notch_enable': 1,
    # ... additional register mirrors

}

```

This register-mirror pattern enables offline validation of FPGA configurations before deployment.

## Processing Stages in Detail

Each `SoftwareFPGA` processing stage calls a reference implementation from [`golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/golden_reference.py). These functions are the golden standard against which the VHDL/Verilog implementation is verified.

### 1. Range FFT (`run_range_fft`)

```python

# Called internally by process_chirps

from golden_reference import run_range_fft

# 1024-point complex FFT with FPGA-compatible twiddle factors

range_fft_result = run_range_fft(quantized_samples, twiddle_file='tw_range_1024.txt')

```

The function loads twiddle-factor files when available to ensure numerical matching with the hardware FFT implementation.

### 2. Range-Bin Decimation (`run_range_bin_decimator`)

Reduces 1024 range bins to 64 through configurable decimation filtering, matching the FPGA's resource-optimized filter structure.

### 3. MTI Canceller (`run_mti_canceller`)

```python
from golden_reference import run_mti_canceller

# Two-pulse canceller: y[n] = x[n] - x[n-1]

mti_output = run_mti_canceller(decimated_data)

```

Removes stationary clutter by subtracting consecutive chirps. Critical for detecting moving targets against static backgrounds.

### 4. Doppler FFT (`run_doppler_fft`)

16-point FFT across the chirp dimension (slow-time) to resolve velocity. The small FFT size reflects the 16-chirp coherent processing interval.

### 5. DC Notch (`run_dc_notch`)

Zeros the zero-Doppler bin to suppress leakage from imperfect MTI cancellation.

### 6. Detection

Two modes available:

- **Simple threshold** (`run_detection`): Fixed magnitude threshold
- **CFAR** (`run_cfar_ca`): Cell-Averaging CFAR with configurable guard and training cells

CFAR mode selection uses the `_CFAR_MODE_MAP` dictionary supporting CA, GO (Greatest Of), and SO (Smallest Of) variants.

## RadarFrame Data Structure

The `process_chirps` method returns a `RadarFrame` dataclass defined in [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py):

```python
from dataclasses import dataclass, field
import numpy as np

NUM_RANGE_BINS = 64
NUM_DOPPLER_BINS = 32

@dataclass
class RadarFrame:
    """One complete radar frame (64 range × 32 Doppler)."""
    timestamp: float = 0.0
    range_doppler_i: np.ndarray = field(
        default_factory=lambda: np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.int16)
    )
    range_doppler_q: np.ndarray = field(
        default_factory=lambda: np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.int16)
    )
    magnitude: np.ndarray = field(
        default_factory=lambda: np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
    )
    detections: np.ndarray = field(
        default_factory=lambda: np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
    )
    range_profile: np.ndarray = field(
        default_factory=lambda: np.zeros(NUM_RANGE_BINS, dtype=np.float64)
    )
    detection_count: int = 0
    frame_number: int = 0

```

This structure matches exactly what the FPGA transmits over USB, enabling seamless switching between hardware and software processing.

## Practical Code Examples

### Loading and Processing SDR-Captured Data

```python
import numpy as np
from software_fpga import SoftwareFPGA, quantize_raw_iq

# Load complex baseband from SDR or previous capture

# Shape: (num_chirps, samples_per_chirp)

raw_iq = np.fromfile('sdr_capture.bin', dtype=np.complex64).reshape(-1, 1024)

# Convert to FPGA 16-bit format (saturation arithmetic)

iq_i, iq_q = quantize_raw_iq(raw_iq)

# Initialize and configure software FPGA

fpga = SoftwareFPGA()
fpga.set_cfar_enable(True)
fpga.set_cfar_guard(2)
fpga.set_cfar_train(8)

# Process entire capture

frame = fpga.process_chirps(
    iq_i, iq_q,
    frame_number=1,
    timestamp=123.456
)

print(f"Detections: {frame.detection_count}")
print(f"Peak range bin: {frame.magnitude.argmax() // 32}")
print(f"Peak Doppler bin: {frame.magnitude.argmax() % 32}")

```

### Batch Processing Multiple Captures

```python
from pathlib import Path

fpga = SoftwareFPGA()
fpga.set_cfar_enable(True)

results = []
for capture_path in Path('captures/').glob('*.bin'):
    raw_iq = np.fromfile(capture_path, dtype=np.complex64).reshape(-1, 1024)
    iq_i, iq_q = quantize_raw_iq(raw_iq)
    
    frame = fpga.process_chirps(iq_i, iq_q)
    results.append({
        'file': capture_path.name,
        'detections': frame.detection_count,
        'max_magnitude': frame.magnitude.max()
    })

```

### Visualizing Range-Doppler Maps

```python
import matplotlib.pyplot as plt

def plot_range_doppler(frame: RadarFrame, title: str = "Range-Doppler Map"):
    """Visualize processed radar frame with proper axes."""
    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
    
    # Magnitude in dB

    mag_db = 20 * np.log10(frame.magnitude + 1e-6)
    im1 = axes[0].imshow(
        mag_db,
        aspect='auto',
        extent=[-16, 15, 0, 64],
        origin='lower',
        cmap='viridis'
    )
    axes[0].set_title('Magnitude (dB)')
    axes[0].set_xlabel('Doppler Bin')
    axes[0].set_ylabel('Range Bin')
    plt.colorbar(im1, ax=axes[0])
    
    # Detection mask

    im2 = axes[1].imshow(
        frame.detections,
        aspect='auto',
        extent=[-16, 15, 0, 64],
        origin='lower',
        cmap='Reds'
    )
    axes[1].set_title(f'Detections (count: {frame.detection_count})')
    axes[1].set_xlabel('Doppler Bin')
    axes[1].set_ylabel('Range Bin')
    
    plt.suptitle(title)
    plt.tight_layout()
    return fig

# Usage

fig = plot_range_doppler(frame, "AERIS-10 Offline Processing")
plt.show()

```

## Integration with GUI Replay Mode

The PyQt-based GUI ([`GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/GUI_V7_PyQt.py)) uses `SoftwareFPGA` when hardware is unavailable. The worker thread ([`workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/workers.py)) instantiates the replica transparently:

```python

# Simplified from workers.py

class ProcessingWorker(QObject):
    update_plot = pyqtSignal(RadarFrame)
    
    def __init__(self, use_hardware: bool = False):
        super().__init__()
        self.use_hardware = use_hardware
        self.fpga = None
    
    def initialize(self):
        if self.use_hardware:
            # Initialize USB connection to FPGA

            pass
        else:
            # Use software replica for offline replay

            self.fpga = SoftwareFPGA()
            self.fpga.set_cfar_enable(self.config.get('cfar_enable', True))
    
    def process_file(self, filepath: str):
        raw_iq = np.fromfile(filepath, dtype=np.complex64).reshape(-1, 1024)
        iq_i, iq_q = quantize_raw_iq(raw_iq)
        
        frame = self.fpga.process_chirps(iq_i, iq_q)
        self.update_plot.emit(frame)

```

This architecture ensures identical processing whether connected to hardware or replaying saved data.

## Key Source Files and Locations

| File | Path | Purpose |
|------|------|---------|
| [`software_fpga.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/software_fpga.py) | [`9_Firmware/9_3_GUI/v7/software_fpga.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/software_fpga.py) | Main `SoftwareFPGA` class implementation |
| [`golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/golden_reference.py) | [`9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py) | Reference signal processing functions |
| [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py) | [`9_Firmware/9_3_GUI/radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/radar_protocol.py) | `RadarFrame` and `StatusResponse` dataclasses |
| [`GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/GUI_V7_PyQt.py) | [`9_Firmware/9_3_GUI/v7/GUI_V7_PyQt.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/GUI_V7_PyQt.py) | PyQt GUI with replay mode |
| [`workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/workers.py) | [`9_Firmware/9_3_GUI/v7/workers.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/workers.py) | Background processing threads |

## Performance Considerations

The `SoftwareFPGA` replica prioritizes bit-accuracy over speed. For large datasets:

- **NumPy operations** provide reasonable performance for development
- **Batch processing** enables overnight analysis of capture archives
- **FFTW twiddle factors** (when available) accelerate FFT computation

For production deployments, the hardware FPGA implementation maintains real-time performance at the cost of flexibility.

## Summary

- **`SoftwareFPGA`** provides a **bit-accurate Python replica** of the AERIS-10 FPGA processing chain
- **Register-mirror architecture** ensures configuration compatibility with hardware
- **Reference implementations** in [`golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/golden_reference.py) validate the VHDL/Verilog design
- **`RadarFrame`** dataclass enables seamless data exchange between software and hardware paths
- **GUI integration** supports transparent switching between live hardware and offline replay

## Frequently Asked Questions

### How does `SoftwareFPGA` guarantee bit-accurate results?

`SoftwareFPGA` uses fixed-point arithmetic, saturation quantization, and identical twiddle factors to the hardware FFT implementation. Each processing stage calls a reference function from [`golden_reference.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/golden_reference.py) that is co-simulated against the RTL during FPGA verification. The constructor initializes register values matching the FPGA's reset state.

### Can I use `SoftwareFPGA` with my own radar hardware?

Yes, provided you adapt the input quantization (`quantize_raw_iq`) to match your ADC bit-width and sample format. The processing chain parameters (FFT sizes, decimation ratios, detection thresholds) are configurable through the register interface. You may need to modify `NUM_RANGE_BINS` and `NUM_DOPPLER_BINS` in [`radar_protocol.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/radar_protocol.py) for different geometries.

### What is the performance difference between `SoftwareFPGA` and hardware?

The hardware XC7A50T FPGA processes chirps in real-time at the radar PRF. The Python replica processes data post-capture, typically at 10-100× slower than real-time depending on CPU. For algorithm development, the software replica enables rapid iteration; for deployment, the bitstream synthesis ensures identical behavior.

### How do I validate that my software processing matches hardware output?

Capture raw I/Q data from the hardware's debug interface, process through `SoftwareFPGA`, and compare against the FPGA's USB output. The repository includes co-simulation infrastructure in `9_Firmware/9_2_FPGA/tb/cosim/` for automated verification. Bit-exact matching confirms algorithm correctness before RTL modification.