feat: hybrid AGC (FPGA phases 1-3 + GUI phase 6) with timing fix

FPGA:
- rx_gain_control.v rewritten: per-frame peak/saturation tracking,
  auto-shift AGC with attack/decay/holdoff, signed gain -7 to +7
- New registers 0x28-0x2C (agc_enable/target/attack/decay/holdoff)
- status_words[4] carries AGC metrics (gain, peak, sat_count, enable)
- DIG_5 GPIO outputs saturation flag for STM32 outer loop
- Both USB interfaces (FT601 + FT2232H) updated with AGC status ports

Timing fix (WNS +0.001ns -> +0.045ns, 45x improvement):
- CIC max_fanout 4->16 on valid pipeline registers
- +200ps setup uncertainty on 400MHz domain
- ExtraNetDelay_high placement + AggressiveExplore routing

GUI:
- AGC opcodes + status parsing in radar_protocol.py
- AGC control groups in both tkinter and V7 PyQt dashboards
- 11 new AGC tests (103/103 GUI tests pass)

Cross-layer:
- AGC opcodes/defaults/status assertions added (29/29 pass)
- contract_parser.py: fixed comment stripping in concat parser

All tests green: 25 FPGA + 103 GUI + 29 cross-layer = 157 pass
This commit is contained in:
Jason
2026-04-13 19:24:11 +05:45
parent 23b2beee53
commit ffba27a10a
19 changed files with 863 additions and 69 deletions
+54 -1
View File
@@ -379,6 +379,44 @@ class RadarDashboard:
command=lambda: self._send_cmd(0x25, 0)).pack(
side="left", expand=True, fill="x", padx=(2, 0))
# ── AGC (Automatic Gain Control) ──────────────────────────────
grp_agc = ttk.LabelFrame(right, text="AGC (Auto Gain)", padding=10)
grp_agc.pack(fill="x", pady=(0, 8))
agc_params = [
("AGC Enable", 0x28, "0", 1, "0=manual, 1=auto"),
("AGC Target", 0x29, "200", 8, "0-255, peak target"),
("AGC Attack", 0x2A, "1", 4, "0-15, atten step"),
("AGC Decay", 0x2B, "1", 4, "0-15, gain-up step"),
("AGC Holdoff", 0x2C, "4", 4, "0-15, frames"),
]
for label, opcode, default, bits, hint in agc_params:
self._add_param_row(grp_agc, label, opcode, default, bits, hint)
# AGC quick toggle
agc_row = ttk.Frame(grp_agc)
agc_row.pack(fill="x", pady=2)
ttk.Button(agc_row, text="Enable AGC",
command=lambda: self._send_cmd(0x28, 1)).pack(
side="left", expand=True, fill="x", padx=(0, 2))
ttk.Button(agc_row, text="Disable AGC",
command=lambda: self._send_cmd(0x28, 0)).pack(
side="left", expand=True, fill="x", padx=(2, 0))
# AGC status readback labels
agc_st = ttk.LabelFrame(grp_agc, text="AGC Status", padding=6)
agc_st.pack(fill="x", pady=(4, 0))
self._agc_labels = {}
for name, default_text in [
("enable", "AGC: --"),
("gain", "Gain: --"),
("peak", "Peak: --"),
("sat", "Sat Count: --"),
]:
lbl = ttk.Label(agc_st, text=default_text, font=("Menlo", 9))
lbl.pack(anchor="w")
self._agc_labels[name] = lbl
# ── Custom Command (advanced / debug) ─────────────────────────
grp_cust = ttk.LabelFrame(right, text="Custom Command", padding=10)
grp_cust.pack(fill="x", pady=(0, 8))
@@ -521,7 +559,7 @@ class RadarDashboard:
self.root.after(0, self._update_self_test_labels, status)
def _update_self_test_labels(self, status: StatusResponse):
"""Update the self-test result labels from a StatusResponse."""
"""Update the self-test result labels and AGC status from a StatusResponse."""
if not hasattr(self, '_st_labels'):
return
flags = status.self_test_flags
@@ -556,6 +594,21 @@ class RadarDashboard:
self._st_labels[key].config(
text=f"{name}: {result_str}", foreground=color)
# AGC status readback
if hasattr(self, '_agc_labels'):
agc_str = "AUTO" if status.agc_enable else "MANUAL"
agc_color = GREEN if status.agc_enable else FG
self._agc_labels["enable"].config(
text=f"AGC: {agc_str}", foreground=agc_color)
self._agc_labels["gain"].config(
text=f"Gain: {status.agc_current_gain}")
self._agc_labels["peak"].config(
text=f"Peak: {status.agc_peak_magnitude}")
sat_color = RED if status.agc_saturation_count > 0 else FG
self._agc_labels["sat"].config(
text=f"Sat Count: {status.agc_saturation_count}",
foreground=sat_color)
# --------------------------------------------------------- Display loop
def _schedule_update(self):
self._update_display()