# FPGA Register Map and Host Command Interface for Radar Control: AERIS-10 Implementation Guide

> Explore the AERIS-10 FPGA register map and host command interface for radar control. Configure chirp timing, CFAR, gain, and self-test via USB without reprogramming.

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

---

**The AERIS-10 radar exposes a 32-bit opcode-driven register map over USB, enabling runtime configuration of chirp timing, CFAR parameters, gain control, and self-test without FPGA reprogramming.**

This guide breaks down the **FPGA register map and host command interface** implemented in the AERIS-10 open-source radar system. The Xilinx XC7A50T-based design uses a compact binary protocol—deciphered from `radar_system_top.v`—that lets host software reconfigure transmitter sequences, receiver processing chains, and detection algorithms on the fly.

---

## Host Command Architecture

The top-level module `radar_system_top.v` sits at the center of all host-FPGA communication. It bridges USB data interfaces to internal control registers through a carefully designed clock-domain crossing (CDC) scheme.

### USB Interface Selection

The build-time parameter `USB_MODE` selects between two physical interfaces:

- **`USB_MODE = 0`** — FT601 USB 3.0 (higher bandwidth, bulk transfers)
- **`USB_MODE = 1`** — FT2232H USB 2.0 (wider compatibility, lower speed)

The generate block at lines 178-225 instantiates the appropriate interface module based on this parameter.

### Clock Domain Crossing

Host commands originate in the USB clock domain but must reach the 100 MHz system clock. The design uses a **toggle-based CDC** rather than a full FIFO:

```verilog
// Lines 66-94: Toggle-based CDC for cmd_valid pulse
always @(posedge usb_clk) begin
    usb_toggle <= usb_toggle ^ usb_cmd_valid;
end

always @(posedge sys_clk_100m) begin
    usb_toggle_sync1 <= usb_toggle;
    usb_toggle_sync2 <= usb_toggle_sync1;
    cmd_valid_100m   <= usb_toggle_sync2 ^ usb_toggle_sync1;  // 1-cycle pulse
end

```

This lightweight approach works because commands are infrequent and idempotent.

---

## 32-Bit Command Word Format

Every host transaction is exactly **4 bytes**, decoded as follows:

| Bit Field | Range | Purpose |
|-----------|-------|---------|
| **Opcode** | `[7:0]` | Command identifier (see table below) |
| **Address** | `[15:8]` | Reserved/sub-address (typically 0) |
| **Value** | `[31:16]` | 16-bit data payload |

The decode logic in lines 49-99 of `radar_system_top.v` routes each opcode to its dedicated `host_*` register.

---

## Complete Register Map

The following opcodes are implemented in the AERIS-10 firmware. Default values shown are power-on resets.

### Radar Mode and Control

| Opcode | Register | Function | Default |
|--------|----------|----------|---------|
| `0x01` | `host_radar_mode` | Operating mode: `2'b00`=idle, `2'b01`=auto, `2'b10`=manual | `2'b01` |
| `0x02` | `host_trigger_pulse` | One-shot trigger to start chirp sequence | — |
| `0x04` | `host_stream_control` | Output enable: `[0]`=range, `[1]`=doppler, `[2]`=CFAR | `3'b111` |

### Transmitter Timing (PLFM Chirp Parameters)

| Opcode | Register | Description | Default |
|--------|----------|-------------|---------|
| `0x10` | `host_long_chirp_cycles` | Long chirp duration in samples | `3000` |
| `0x11` | `host_long_listen_cycles` | Quiet period after long chirp | `13700` |
| `0x12` | `host_guard_cycles` | Guard interval between chirps | `17540` |
| `0x13` | `host_short_chirp_cycles` | Short chirp duration | `50` |
| `0x14` | `host_short_listen_cycles` | Quiet period after short chirp | `17450` |
| `0x15` | `host_chirps_per_elev` | Chirps per elevation angle | `32` |

**Critical constraint:** `host_chirps_per_elev` is hardware-clamped to 32 (the fixed Doppler FFT size). Lines 64-71 of `radar_system_top.v` enforce this:

```verilog
if (host_chirps_per_elev > 32) begin
    host_chirps_per_elev <= 32;
    chirps_mismatch_error <= 1'b1;  // Flag for host diagnostics
end

```

### Receiver Gain and Range Selection

| Opcode | Register | Function | Default |
|--------|----------|----------|---------|
| `0x16` | `host_gain_shift` | Digital gain: `[3]`=direction (0=amplify, 1=atten), `[2:0]`=shift amount | `0x0` |
| `0x20` | `host_range_mode` | Antenna selection: `2'b00`=auto, `2'b01`=short, `2'b10`=long | `2'b00` |

The gain register uses a 4-bit packed format interpreted as:

```verilog
// Lines 28-31: Gain decode
wire gain_dir      = host_gain_shift[3];       // 0 = shift left (amplify)
wire [2:0] gain_amt = host_gain_shift[2:0];    // 0-7 bit positions

```

### CFAR Detection Parameters

| Opcode | Register | Description | Default |
|--------|----------|-------------|---------|
| `0x21` | `host_cfar_guard` | Guard cells (exclude from noise estimate) | `4'd2` |
| `0x22` | `host_cfar_train` | Training cells (noise reference window) | `5'd8` |
| `0x23` | `host_cfar_alpha` | Threshold multiplier in Q4.4 fixed-point | `0x30` |
| `0x24` | `host_cfar_mode` | Algorithm: `2'b00`=CA, `2'b01`=GO, `2'b10`=SO | `2'b00` |
| `0x25` | `host_cfar_enable` | `1`=CFAR enabled, `0`=simple magnitude threshold | `0` |
| `0x26` | `host_mti_enable` | Motion target indicator filter enable | `0` |
| `0x27` | `host_dc_notch_width` | DC bin suppression width (bins) | `0` |

### AGC Loop Control

| Opcode | Register | Description | Default |
|--------|----------|-------------|---------|
| `0x28` | `host_agc_enable` | Automatic gain control enable | `0` |
| `0x29` | `host_agc_target` | Target power level | `200` |
| `0x2A` | `host_agc_attack` | Attack rate (gain reduction speed) | `1` |
| `0x2B` | `host_agc_decay` | Decay rate (gain recovery speed) | `1` |
| `0x2C` | `host_agc_holdoff` | Hold-off samples after saturation | `4` |

### Diagnostic and Self-Test

| Opcode | Register | Function |
|--------|----------|----------|
| `0x30` | `host_self_test_trigger` | One-shot initiation of built-in self-test |
| `0x31` | `host_status_request` | Read back status/self-test results |
| `0xFF` | `host_status_request` | Alias for status request |

---

## Host-Side Python Implementation

The repository includes a Python reference implementation using `pyftdi`. Below is a minimal, reusable driver core:

```python
#!/usr/bin/env python3
"""
AERIS-10 Host Command Interface
Minimal reference implementation for the FPGA register map.
"""

import struct
import time
from pyftdi.ftdi import Ftdi


class AERIS10Host:
    """USB command interface to AERIS-10 radar FPGA."""
    
    # Opcode definitions (match firmware register map)

    OPCODES = {
        'radar_mode': 0x01,
        'trigger_pulse': 0x02,
        'detect_threshold': 0x03,
        'stream_control': 0x04,
        'long_chirp_cycles': 0x10,
        'long_listen_cycles': 0x11,
        'guard_cycles': 0x12,
        'short_chirp_cycles': 0x13,
        'short_listen_cycles': 0x14,
        'chirps_per_elev': 0x15,
        'gain_shift': 0x16,
        'range_mode': 0x20,
        'cfar_guard': 0x21,
        'cfar_train': 0x22,
        'cfar_alpha': 0x23,
        'cfar_mode': 0x24,
        'cfar_enable': 0x25,
        'mti_enable': 0x26,
        'dc_notch_width': 0x27,
        'agc_enable': 0x28,
        'agc_target': 0x29,
        'agc_attack': 0x2A,
        'agc_decay': 0x2B,
        'agc_holdoff': 0x2C,
        'self_test_trigger': 0x30,
        'status_request': 0x31,
    }
    
    # Range mode values

    RANGE_AUTO = 0b00
    RANGE_SHORT = 0b01   # ~3 km

    RANGE_LONG = 0b10    # ~10 km

    
    # CFAR mode values

    CFAR_CA = 0b00   # Cell Averaging

    CFAR_GO = 0b01   # Greatest Of

    CFAR_SO = 0b10   # Smallest Of

    
    def __init__(self, ftdi_url: str = 'ftdi://ftdi:2232h/1'):
        self.ftdi = Ftdi()
        self.ftdi.open_bitbang_from_url(ftdi_url)
        self.ftdi.set_baudrate(115200)
        
    def send_command(self, opcode: int, value: int = 0, addr: int = 0) -> None:
        """Encode and transmit 32-bit command word."""
        # Pack: [31:16]=value, [15:8]=addr, [7:0]=opcode

        packed = struct.pack('<I', (value << 16) | (addr << 8) | opcode)
        self.ftdi.write_data(packed)
        time.sleep(0.001)  # FPGA capture window

        
    def configure_range_mode(self, mode: int) -> None:
        """Select antenna/range configuration."""
        self.send_command(self.OPCODES['range_mode'], mode)
        
    def configure_cfar(self, 
                       enabled: bool = True,
                       mode: int = 0,
                       guard_cells: int = 2,
                       train_cells: int = 8,
                       alpha: int = 0x30) -> None:
        """Configure CFAR detection parameters."""
        self.send_command(self.OPCODES['cfar_enable'], int(enabled))
        self.send_command(self.OPCODES['cfar_mode'], mode)
        self.send_command(self.OPCODES['cfar_guard'], guard_cells)
        self.send_command(self.OPCODES['cfar_train'], train_cells)
        self.send_command(self.OPCODES['cfar_alpha'], alpha)
        
    def arm_trigger(self) -> None:
        """Fire one-shot radar trigger."""
        self.send_command(self.OPCODES['trigger_pulse'])
        
    def run_self_test(self) -> None:
        """Initiate built-in self-test sequence."""
        self.send_command(self.OPCODES['self_test_trigger'])
        
    def close(self) -> None:
        self.ftdi.close()


# Example usage

if __name__ == '__main__':
    radar = AERIS10Host('ftdi://ftdi:2232h/1')
    
    # Configure for short-range operation with CA-CFAR

    radar.configure_range_mode(AERIS10Host.RANGE_SHORT)
    radar.configure_cfar(
        enabled=True,
        mode=AERIS10Host.CFAR_CA,
        guard_cells=4,
        train_cells=16,
        alpha=0x40
    )
    
    # Start acquisition

    radar.arm_trigger()
    
    radar.close()

```

---

## Command Flow and Latency

Understanding the end-to-end path helps estimate response times:

1. **Host software** packs 32-bit command → Python `pyftdi` → USB bulk OUT
2. **USB interface module** (`usb_data_interface.v` or `usb_data_interface_ft2232h.v`) parses packet and asserts `usb_cmd_valid`
3. **CDC toggle** (lines 66-94) synchronizes to 100 MHz domain as `cmd_valid_100m`
4. **Decode logic** (lines 49-99) updates `host_*` register combinationally
5. **Downstream modules** sample new values on next clock edge

**Typical latency:** <50 µs from host write to register update, dominated by USB transaction time.

---

## Key Source Files

| File | Lines of Interest | Purpose |
|------|-------------------|---------|
| `9_Firmware/9_2_FPGA/radar_system_top.v` | 13-19 (clocks), 35-98 (TX), 102-166 (RX), 178-225 (USB select), 49-99 (command decode) | Top-level integration and register map |
| `9_Firmware/9_2_FPGA/usb_data_interface.v` | 52-56 (cmd output) | FT601 USB 3.0 packet handler |
| `9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v` | — | FT2232H USB 2.0 variant |
| `9_Firmware/9_2_FPGA/radar_transmitter.v` | — | Chirp generation, consumes `host_*` timing regs |
| `9_Firmware/9_2_FPGA/radar_receiver_final.v` | — | Signal processing, consumes CFAR/AGC/MTI regs |
| `9_Firmware/9_2_FPGA/cfar_ca.v` | — | CFAR implementation, consumes `host_cfar_*` |
| [`8_Utils/Python/GUI_V7_Tk.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/8_Utils/Python/GUI_V7_Tk.py) | — | Full-featured reference GUI |

---

## Summary

- **Command format:** Fixed 32-bit words with 8-bit opcode, 16-bit value, 8-bit address
- **Key constraint:** `chirps_per_elev` clamped to 32; mismatch sets error flag
- **Self-clearing registers:** `host_trigger_pulse`, `host_self_test_trigger`, `host_status_request` reset automatically
- **Gain encoding:** 4-bit packed format with direction bit and 3-bit magnitude
- **CFAR flexibility:** Three algorithms (CA/GO/SO), programmable guard/train cells, Q4.4 threshold multiplier
- **No FPGA rebuild required:** All parameters runtime-configurable over USB

---

## Frequently Asked Questions

### How do I determine which USB interface my AERIS-10 hardware uses?

Check the physical connector and FTDI chip marking. FT601 (USB 3.0) provides higher throughput for raw ADC streaming, while FT2232H (USB 2.0) is more common on older or cost-optimized boards. The `USB_MODE` parameter in `radar_system_top.v` must match your hardware—synthesis will fail to route if mismatched. Use `pyftdi`'s device discovery (`Ftdi.show_devices()`) to identify your specific URL.

### What happens if I request more than 32 chirps per elevation?

The FPGA hardware-clamps `host_chirps_per_elev` to 32 and sets the `chirps_mismatch_error` flag. This protects the fixed-size Doppler FFT (32-point) from buffer overruns. Your host software can detect this condition by issuing a status request (`opcode 0x31` or `0xFF`) and checking the error bits in the response.

### Can I change CFAR parameters while the radar is running?

Yes—all `host_*` registers update combinationally and take effect on the next 100 MHz clock edge. However, mid-frame changes to `host_cfar_guard` or `host_cfar_train` may produce transient detection artifacts at range-bin boundaries. For clean parameter swaps, pause streaming via `host_stream_control` (opcode `0x04`), modify parameters, then resume.

### How do I interpret the `host_cfar_alpha` Q4.4 fixed-point value?

Q4.4 means 4 integer bits and 4 fractional bits, so the real value is `alpha / 16`. The default `0x30` equals `48/16 = 3.0`, a common 3-sigma threshold multiplier. Valid range is `0x00` (0.0) to `0xFF` (15.9375). For higher Pd (detection probability) in clutter, reduce alpha; for lower Pfa (false alarm rate), increase it.