Automatic Gain Control (AGC) Implementation Across FPGA, STM32, and GUI Layers in PLFM RADAR

The PLFM RADAR platform implements a three-layer AGC system where the FPGA runs the real-time inner loop, the STM32 mirrors the enable state via GPIO, and the GUI provides user control through registers 0x28–0x2C with status feedback from bit 11 of status word 4.

The Automatic Gain Control (AGC) implementation across FPGA/STM32/GUI layers in the NawfalMotii79/PLFM_RADAR repository demonstrates a tightly-coupled, cross-domain design that ensures consistent gain behavior from hardware registers to user-facing controls. This architecture uses a shared register interface and a single GPIO line to maintain synchronization between the FPGA's real-time processing, the STM32's front-end management, and the Python-based graphical interface.

FPGA Inner-Loop: Real-Time Gain Correction

The FPGA layer handles the critical real-time processing that keeps received IQ magnitude at a programmable target. The core logic resides in 9_Firmware/9_2_FPGA/rx_gain_control.v, which implements a complete AGC transfer function with configurable attack, decay, and hold-off characteristics.

Register Interface (0x28–0x2C)

The FPGA exposes five control registers that define AGC behavior:

Register Address Purpose
AGC_ENABLE 0x28 Master enable flag (0 = off, 1 = on)
AGC_TARGET 0x29 Target magnitude level (default: 200)
AGC_ATTACK 0x2A Attack rate coefficient
AGC_DECAY 0x2B Decay rate coefficient
AGC_HOLDOFF 0x2C Hold-off period in frames

These registers are written by the host over the FT2232H USB-UART bridge. The testbench in 9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v validates this interface with sequences like:

// Write AGC_ENABLE = 1
send_command_ft2232h(8'h28, 8'h00, 8'h00, 8'h01);
// Write AGC_TARGET = 200 (0xC8)
send_command_ft2232h(8'h29, 8'h00, 8'h00, 8'hC8);
// Write AGC_ATTACK = 1
send_command_ft2232h(8'h2A, 8'h00, 8'h00, 8'h01);
// Write AGC_DECAY = 1
send_command_ft2232h(8'h2B, 8'h00, 8'h00, 8'h01);
// Write AGC_HOLDOFF = 4
send_command_ft2232h(8'h2C, 8'h00, 8'h00, 8'h04);

The FPGA exposes runtime state through status word 4, with bit 11 carrying the current AGC enabled flag. This bit serves as the single source of truth for downstream layers.

STM32 Outer-Loop: GPIO-Driven Enable Propagation

The STM32 layer in 9_Firmware/9_1_MCU/ADAR1000_AGC.cpp does not implement a separate AGC algorithm. Instead, it mirrors the FPGA's enable state through hardware GPIO synchronization.

ADAR1000_AGC Class Design

The ADAR1000_AGC class enforces a default-OFF safety policy at boot:

ADAR1000_AGC::ADAR1000_AGC()
    : enabled(false)     // AGC explicitly disabled at startup
{
}

On every radar frame, the MCU samples the DIG_6 GPIO line that the FPGA toggles to reflect its internal AGC enable state:

void update_agc_from_gpio()
{
    bool gpio_agc = read_gpio(DIG_6);   // Read FPGA AGC enable output
    agc_instance.setEnabled(gpio_agc);   // Propagate to ADAR1000 front-end
}

This design creates a pass-through chain: FPGA decision → DIG_6 signal → MCU update → ADAR1000 register write. The STM32 adds no latency to the control path while enabling front-end hardware coordination.

GUI Layer: User Control and Real-Time Monitoring

The Python-based GUI in 9_Firmware/9_3_GUI/v7/dashboard.py provides the human interface to the AGC system, with both control and visualization capabilities.

Control Panel Implementation

The "AGC (Auto Gain)" group box binds Qt sliders directly to the same 0x28–0x2C register set:

def _on_agc_enable(self):
    """Called when user clicks the AGC enable checkbox"""
    self._write_reg(0x28, 1)          # AGC_ENABLE = 1

    self._write_reg(0x29, 200)        # AGC_TARGET = 200 (default)

    self._write_reg(0x2A, 1)          # AGC_ATTACK = 1

    self._write_reg(0x2A, 1)          # AGC_DECAY = 1

    self._write_reg(0x2C, 4)          # AGC_HOLDOFF = 4

Status Display and Monitoring

The GUI reads back the hardware state through status word 4, bit 11 to ensure the displayed status matches actual FPGA operation:


# Status parsing from FPGA status word

agc_enabled = (status_word_4 >> 11) & 0x1
self.agc_status_label.setText(f"AGC: {'ON' if agc_enabled else 'OFF'}")

The "AGC Monitor" tab implements a 0.5-second refresh cycle using ring buffers to visualize:

  • Instantaneous gain value
  • Peak magnitude history
  • Saturation event timeline

This closed-loop verification ensures that user commands propagate through the entire chain and that actual hardware state feeds back to the display.

Cross-Layer Verification: Simulation and Test

The repository includes bit-accurate simulation and automated contract testing to guarantee consistency across all three layers.

Python Simulation Model

9_Firmware/9_3_GUI/v7/agc_sim.py provides a software reference that matches the Verilog implementation cycle-for-cycle:

from agc_sim import AGCConfig, AGCState, process_agc_frame

# Configure identical to FPGA defaults

cfg = AGCConfig(target=200, attack=1, decay=1, holdoff=4)
state = AGCState(gain=0, holdoff_counter=0)

# Process one IQ frame through the AGC model

result = process_agc_frame(iq_frame, cfg, state)
print(f"Gain: {result.gain}, Saturated: {result.saturation}")

The process_agc_frame() function implements the same non-linear transfer function as rx_gain_control.v, enabling pre-hardware validation of gain curves.

Automated Contract Tests

9_Firmware/tests/cross_layer/test_cross_layer_contract.py enforces behavioral contracts across layers:

  • Register default agreement: FPGA reset values match GUI initialization
  • Bit 11 parsing correctness: GUI extraction of status word 4 matches hardware specification
  • Enable state propagation: FPGA, GPIO, and GUI agree on AGC active/inactive states

These tests run against both the simulation model and recorded hardware traces from tb_cross_layer_ft2232h.v.

Summary

The PLFM RADAR AGC implementation demonstrates robust cross-layer engineering with these key characteristics:

  • Single register namespace: Addresses 0x28–0x2C are authoritative across FPGA, host driver, and GUI code
  • GPIO-synchronized enable: The DIG_6 line ensures FPGA and STM32 agree on AGC active state without software polling
  • Bit 11 state feedback: Status word 4 provides hardware-verified truth that prevents GUI misreporting
  • Simulation-verified behavior: agc_sim.py matches rx_gain_control.v for pre-silicon validation
  • Default-safe operation: All layers initialize with AGC disabled, requiring explicit user enable

Frequently Asked Questions

What registers control the AGC in PLFM RADAR?

Registers 0x28 through 0x2C control the AGC: 0x28 for enable, 0x29 for target magnitude, 0x2A for attack rate, 0x2B for decay rate, and 0x2C for hold-off period. These registers are accessed via the FT2232H USB-UART bridge from both the GUI and test fixtures.

How does the STM32 know when the FPGA has AGC enabled?

The FPGA drives the DIG_6 GPIO line high when AGC is enabled and low when disabled. The STM32 samples this line on every frame in update_agc_from_gpio() and propagates the state to the ADAR1000 front-end chip. This hardware signal eliminates synchronization latency versus register polling.

Where can I find the AGC status in the FPGA output?

The AGC enabled flag appears in status word 4, bit 11. The GUI reads this word during its 0.5-second refresh cycle and updates the "AGC: ON/OFF" label accordingly. This bit is the authoritative hardware state—if the FPGA disables AGC due to saturation or error, the GUI reflects this immediately.

Is there a way to test AGC behavior without hardware?

Yes. The agc_sim.py module provides process_agc_frame() with identical behavior to rx_gain_control.v. Combined with test_cross_layer_contract.py, you can validate AGC curves, verify GUI parsing logic, and confirm register defaults entirely in simulation before deploying to the FPGA.

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 →