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

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:

// 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:

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:

// 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:

#!/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 — 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →