fix: 9 bugs from code review — RTL sign-ext & snapshot, thread safety, protocol fixes

- rx_gain_control.v: sign-extension fix ({agc_gain[3],agc_gain} not {1'b0,agc_gain})
  + inclusive frame_boundary snapshot via combinational helpers (Bug #7)
- v7/dashboard.py: Qt thread-safe logging via pyqtSignal bridge (Bug #1)
  + table headers corrected to 'Range (m)' / 'Velocity (m/s)' (Bug #2)
- main.cpp: guard outerAgc.applyGain() with if(outerAgc.enabled) (Bug #3)
- radar_protocol.py: replay L1 threshold detection when CFAR disabled (Bug #4)
  + IndexError guard in replay open (Bug #5) + AGC opcodes in _HARDWARE_ONLY_OPCODES
- radar_dashboard.py: AGC monitor attribute name fixes (3 labels)
- tb_rx_gain_control.v: Tests 17-19 (sign-ext, simultaneous valid+boundary, enable toggle)
- tb_cross_layer_ft2232h.v: AGC opcode vectors 0x28-0x2C in Exercise A (Bug #6)

Vivado 50T build verified: WNS=+0.002ns, WHS=+0.028ns — all timing constraints met.
All tests pass: MCU 21/21, GUI 120/120, cross-layer 29/29, FPGA 25/25 (68 checks).
This commit is contained in:
Jason
2026-04-13 23:35:10 +05:45
parent 88ce0819a8
commit b4d1869582
7 changed files with 412 additions and 28 deletions
+6 -6
View File
@@ -734,10 +734,10 @@ class RadarDashboard:
mode_str = "AUTO" if status.agc_enable else "MANUAL"
mode_color = GREEN if status.agc_enable else FG
self._agc_badge.config(text=f"AGC: {mode_str}", foreground=mode_color)
self._agc_current_gain_lbl.config(
text=f"Current Gain: {status.agc_current_gain}")
self._agc_current_peak_lbl.config(
text=f"Peak Mag: {status.agc_peak_magnitude}")
self._agc_gain_value.config(
text=f"Gain: {status.agc_current_gain}")
self._agc_peak_value.config(
text=f"Peak: {status.agc_peak_magnitude}")
total_sat = sum(self._agc_sat_history)
if total_sat > 10:
@@ -746,8 +746,8 @@ class RadarDashboard:
sat_color = YELLOW
else:
sat_color = GREEN
self._agc_sat_total_lbl.config(
text=f"Total Saturations: {total_sat}", foreground=sat_color)
self._agc_sat_badge.config(
text=f"Saturation: {total_sat}", foreground=sat_color)
# ---- Throttle matplotlib redraws ---------------------------------
now = time.monotonic()
+17 -3
View File
@@ -462,6 +462,11 @@ _HARDWARE_ONLY_OPCODES = {
0x15, # CHIRPS_PER_ELEV
0x16, # GAIN_SHIFT
0x20, # RANGE_MODE
0x28, # AGC_ENABLE
0x29, # AGC_TARGET
0x2A, # AGC_ATTACK
0x2B, # AGC_DECAY
0x2C, # AGC_HOLDOFF
0x30, # SELF_TEST_TRIGGER
0x31, # SELF_TEST_STATUS
0xFF, # STATUS_REQUEST
@@ -469,6 +474,7 @@ _HARDWARE_ONLY_OPCODES = {
# Replay-adjustable opcodes (re-run signal processing)
_REPLAY_ADJUSTABLE_OPCODES = {
0x03, # DETECT_THRESHOLD
0x21, # CFAR_GUARD
0x22, # CFAR_TRAIN
0x23, # CFAR_ALPHA
@@ -612,6 +618,7 @@ class ReplayConnection:
self._cfar_alpha: int = 0x30
self._cfar_mode: int = 0 # 0=CA, 1=GO, 2=SO
self._cfar_enable: bool = True
self._detect_threshold: int = 10000 # RTL default (host_detect_threshold)
# Raw source arrays (loaded once, reprocessed on param change)
self._dop_mti_i: np.ndarray | None = None
self._dop_mti_q: np.ndarray | None = None
@@ -633,7 +640,7 @@ class ReplayConnection:
f"(MTI={'ON' if self._mti_enable else 'OFF'}, "
f"{self._frame_len} bytes/frame)")
return True
except (OSError, ValueError, struct.error) as e:
except (OSError, ValueError, IndexError, struct.error) as e:
log.error(f"Replay open failed: {e}")
return False
@@ -676,7 +683,11 @@ class ReplayConnection:
if opcode in _REPLAY_ADJUSTABLE_OPCODES:
changed = False
with self._lock:
if opcode == 0x21: # CFAR_GUARD
if opcode == 0x03: # DETECT_THRESHOLD
if self._detect_threshold != value:
self._detect_threshold = value
changed = True
elif opcode == 0x21: # CFAR_GUARD
if self._cfar_guard != value:
self._cfar_guard = value
changed = True
@@ -768,7 +779,10 @@ class ReplayConnection:
mode=self._cfar_mode,
)
else:
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=bool)
# Simple threshold fallback matching RTL cfar_ca.v:
# detect = (|I| + |Q|) > detect_threshold (L1 norm)
mag = np.abs(dop_i) + np.abs(dop_q)
det = mag > self._detect_threshold
det_count = int(det.sum())
log.info(f"Replay: rebuilt {NUM_CELLS} packets ("
+19 -10
View File
@@ -36,7 +36,7 @@ from PyQt6.QtWidgets import (
QTableWidget, QTableWidgetItem, QHeaderView,
QPlainTextEdit, QStatusBar, QMessageBox,
)
from PyQt6.QtCore import Qt, QTimer, pyqtSlot
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
from matplotlib.figure import Figure
@@ -173,8 +173,10 @@ class RadarDashboard(QMainWindow):
self._gui_timer.timeout.connect(self._refresh_gui)
self._gui_timer.start(100)
# Log handler for diagnostics
self._log_handler = _QtLogHandler(self._log_append)
# Log handler for diagnostics (thread-safe via Qt signal)
self._log_bridge = _LogSignalBridge(self)
self._log_bridge.log_message.connect(self._log_append)
self._log_handler = _QtLogHandler(self._log_bridge)
self._log_handler.setLevel(logging.INFO)
logging.getLogger().addHandler(self._log_handler)
@@ -403,7 +405,7 @@ class RadarDashboard(QMainWindow):
self._targets_table_main = QTableWidget()
self._targets_table_main.setColumnCount(5)
self._targets_table_main.setHorizontalHeaderLabels([
"Range Bin", "Doppler Bin", "Magnitude", "SNR (dB)", "Track ID",
"Range (m)", "Velocity (m/s)", "Magnitude", "SNR (dB)", "Track ID",
])
self._targets_table_main.setAlternatingRowColors(True)
self._targets_table_main.setSelectionBehavior(
@@ -1719,15 +1721,22 @@ class RadarDashboard(QMainWindow):
# =============================================================================
# Qt-compatible log handler (routes Python logging -> QTextEdit)
# Qt-compatible log handler (routes Python logging -> QTextEdit via signal)
# =============================================================================
class _QtLogHandler(logging.Handler):
"""Sends log records to a callback (called on the thread that emitted)."""
def __init__(self, callback):
class _LogSignalBridge(QObject):
"""Thread-safe bridge: emits a Qt signal so the slot runs on the GUI thread."""
log_message = pyqtSignal(str)
class _QtLogHandler(logging.Handler):
"""Sends log records to a QObject signal (safe from any thread)."""
def __init__(self, bridge: _LogSignalBridge):
super().__init__()
self._callback = callback
self._bridge = bridge
self.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-8s %(message)s",
datefmt="%H:%M:%S",
@@ -1736,6 +1745,6 @@ class _QtLogHandler(logging.Handler):
def emit(self, record):
try:
msg = self.format(record)
self._callback(msg)
self._bridge.log_message.emit(msg)
except RuntimeError:
pass