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

> Discover how to implement Automatic Gain Control (AGC) across FPGA, STM32, and GUI layers in the PLFM RADAR platform. Learn about real-time control, GPIO mirroring, and user interface integration for optimized performance.

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

---

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

```verilog
// 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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/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:

```cpp
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:

```cpp
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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/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:

```python
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:

```python

# 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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/9_Firmware/9_3_GUI/v7/agc_sim.py) provides a software reference that matches the Verilog implementation cycle-for-cycle:

```python
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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/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`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/agc_sim.py) module provides `process_agc_frame()` with identical behavior to `rx_gain_control.v`. Combined with [`test_cross_layer_contract.py`](https://github.com/NawfalMotii79/PLFM_RADAR/blob/main/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.