diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp index 324de26..310ee78 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp @@ -2117,8 +2117,9 @@ int main(void) runRadarPulseSequence(); /* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13), - * adjust ADAR1000 VGA common gain once per radar frame (~258 ms). */ - { + * adjust ADAR1000 VGA common gain once per radar frame (~258 ms). + * Only run when AGC is enabled — otherwise leave VGA gains untouched. */ + if (outerAgc.enabled) { bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port, FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET; outerAgc.update(sat); diff --git a/9_Firmware/9_2_FPGA/rx_gain_control.v b/9_Firmware/9_2_FPGA/rx_gain_control.v index 2876625..f43ac0b 100644 --- a/9_Firmware/9_2_FPGA/rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/rx_gain_control.v @@ -86,6 +86,11 @@ reg [14:0] frame_peak; // Peak |sample| this frame (15-bit unsigned) // Previous AGC enable state (for detecting 0→1 transition) reg agc_enable_prev; +// Combinational helpers for inclusive frame-boundary snapshot +// (used when valid_in and frame_boundary coincide) +reg wire_frame_sat_incr; +reg wire_frame_peak_update; + // ========================================================================= // EFFECTIVE GAIN SELECTION // ========================================================================= @@ -198,6 +203,15 @@ always @(posedge clk or negedge reset_n) begin // Track AGC enable transitions agc_enable_prev <= agc_enable; + // Compute inclusive metrics: if valid_in fires this cycle, + // include current sample in the snapshot taken at frame_boundary. + // This avoids losing the last sample when valid_in and + // frame_boundary coincide (NBA last-write-wins would otherwise + // snapshot stale values then reset, dropping the sample entirely). + wire_frame_sat_incr = (valid_in && (overflow_i || overflow_q) + && (frame_sat_count != 8'hFF)); + wire_frame_peak_update = (valid_in && (max_iq > frame_peak)); + // ---- Data pipeline (1-cycle latency) ---- valid_out <= valid_in; if (valid_in) begin @@ -215,9 +229,13 @@ always @(posedge clk or negedge reset_n) begin // ---- Frame boundary: AGC update + metric snapshot ---- if (frame_boundary) begin - // Snapshot per-frame metrics to output registers - saturation_count <= frame_sat_count; - peak_magnitude <= frame_peak[14:7]; // Upper 8 bits of 15-bit peak + // Snapshot per-frame metrics INCLUDING current sample if valid_in + saturation_count <= wire_frame_sat_incr + ? (frame_sat_count + 8'd1) + : frame_sat_count; + peak_magnitude <= wire_frame_peak_update + ? max_iq[14:7] + : frame_peak[14:7]; // Reset per-frame accumulators for next frame frame_sat_count <= 8'd0; @@ -225,15 +243,17 @@ always @(posedge clk or negedge reset_n) begin if (agc_enable) begin // AGC auto-adjustment at frame boundary - if (frame_sat_count > 8'd0) begin + // Use inclusive counts/peaks (accounting for simultaneous valid_in) + if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin // Clipping detected: reduce gain immediately (attack) - agc_gain <= clamp_gain($signed({1'b0, agc_gain}) - + agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) - $signed({1'b0, agc_attack})); holdoff_counter <= agc_holdoff; // Reset holdoff - end else if (frame_peak[14:7] < agc_target) begin + end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7]) + < agc_target) begin // Signal too weak: increase gain after holdoff expires if (holdoff_counter == 4'd0) begin - agc_gain <= clamp_gain($signed({1'b0, agc_gain}) + + agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) + $signed({1'b0, agc_decay})); end else begin holdoff_counter <= holdoff_counter - 4'd1; diff --git a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v index 32aaf82..75904fc 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v @@ -545,6 +545,315 @@ initial begin check(current_gain == 4'b0001, "T16.3: Gain increased after holdoff expired (gain 0->1)"); + // --------------------------------------------------------------- + // TEST 17: Repeated attacks drive gain negative, clamp at -7, + // then decay recovers + // --------------------------------------------------------------- + $display(""); + $display("--- Test 17: Repeated attack → negative clamp → decay recovery ---"); + + // ----- 17a: Walk gain from +7 down through zero via repeated attack ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_111; // amplify x128, internal gain = +7 + agc_enable = 0; + agc_attack = 4'd2; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0_111, + "T17a.1: AGC initialized at gain +7 (0x7)"); + + // Frame 1: saturating at gain +7 → gain 7-2=5 + send_sample(16'sd1000, 16'sd1000); // 1000<<7 = 128000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_101, + "T17a.2: After attack: gain +5 (0x5)"); + + // Frame 2: still saturating at gain +5 → gain 5-2=3 + send_sample(16'sd1000, 16'sd1000); // 1000<<5 = 32000 → no overflow + send_sample(16'sd2000, 16'sd2000); // 2000<<5 = 64000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_011, + "T17a.3: After attack: gain +3 (0x3)"); + + // Frame 3: saturating at gain +3 → gain 3-2=1 + send_sample(16'sd5000, 16'sd5000); // 5000<<3 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_001, + "T17a.4: After attack: gain +1 (0x1)"); + + // Frame 4: saturating at gain +1 → gain 1-2=-1 → encoding 0x9 + send_sample(16'sd20000, 16'sd20000); // 20000<<1 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_001, + "T17a.5: Attack crossed zero: gain -1 (0x9)"); + + // Frame 5: at gain -1 (right shift 1), 20000>>>1=10000, NO overflow. + // peak = 20000 → [14:7]=156 > target(100) → HOLD, gain stays -1 + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_001, + "T17a.6: No overflow at -1, peak>target → HOLD, gain stays -1"); + + // ----- 17b: Max attack step clamps at -7 ----- + $display(""); + $display("--- Test 17b: Max attack clamps at -7 ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_011; // amplify x8, internal gain = +3 + agc_attack = 4'd15; // max attack step + agc_enable = 0; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0_011, + "T17b.1: Initialized at gain +3"); + + // One saturating frame: gain = clamp(3 - 15) = clamp(-12) = -7 → 0xF + send_sample(16'sd5000, 16'sd5000); // 5000<<3 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17b.2: Gain clamped at -7 (0xF) after max attack"); + + // Another frame at gain -7: 5000>>>7 = 39, peak = 5000→[14:7]=39 < target(100) + // → decay path, but holdoff counter was reset to 2 by the attack above + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17b.3: Gain still -7 (holdoff active, 2→1)"); + + // ----- 17c: Decay recovery from -7 after holdoff ----- + $display(""); + $display("--- Test 17c: Decay recovery from deep negative ---"); + + // Holdoff was 2. After attack (frame above), holdoff=2. + // Frame after 17b.3: holdoff decrements to 0 + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17c.1: Gain still -7 (holdoff 1→0)"); + + // Now holdoff=0, next weak frame should trigger decay: -7 + 1 = -6 → 0xE + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_110, + "T17c.2: Decay from -7 to -6 (0xE) after holdoff expired"); + + // One more decay: -6 + 1 = -5 → 0xD + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_101, + "T17c.3: Decay from -6 to -5 (0xD)"); + + // Verify output is actually attenuated: at gain -5 (right shift 5), + // 5000 >>> 5 = 156 + send_sample(16'sd5000, 16'sd0); + check(data_i_out == 16'sd156, + "T17c.4: Output correctly attenuated: 5000>>>5 = 156"); + + // ================================================================= + // Test 18: valid_in + frame_boundary on the SAME cycle + // Verify the coincident sample is included in the frame snapshot + // (Bug #7 fix — previously lost due to NBA last-write-wins) + // ================================================================= + $display(""); + $display("--- Test 18: valid_in + frame_boundary simultaneous ---"); + + // ----- 18a: Coincident saturating sample included in sat count ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_011; // amplify x8 (shift left 3) + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send one normal sample first (establishes a non-zero frame) + send_sample(16'sd100, 16'sd100); // small, no overflow at gain +3 + + // Now: assert valid_in AND frame_boundary on the SAME posedge. + // The sample is large enough to overflow at gain +3: 5000<<3 = 40000 > 32767 + @(negedge clk); + data_i_in = 16'sd5000; + data_q_in = 16'sd5000; + valid_in = 1'b1; + frame_boundary = 1'b1; + @(posedge clk); #1; // DUT samples both signals + @(negedge clk); + valid_in = 1'b0; + frame_boundary = 1'b0; + @(posedge clk); #1; // let NBA settle + @(posedge clk); #1; + + // Saturation count should be 1 (the coincident sample overflowed) + check(saturation_count == 8'd1, + "T18a.1: Coincident saturating sample counted in snapshot (sat_count=1)"); + + // Peak should reflect pre-gain max(|5000|,|5000|) = 5000 → [14:7] = 39 + // (or at least >= the first sample's peak of 100→[14:7]=0) + check(peak_magnitude == 8'd39, + "T18a.2: Coincident sample peak included in snapshot (peak=39)"); + + // AGC should have attacked (sat > 0): gain +3 → +3-1 = +2 + check(current_gain == 4'b0_010, + "T18a.3: AGC attacked on coincident saturation (gain +3 → +2)"); + + // ----- 18b: Coincident non-saturating peak updates snapshot ----- + $display(""); + $display("--- Test 18b: Coincident peak-only sample ---"); + + reset_n = 0; + agc_enable = 0; // deassert so transition fires with NEW gain_shift + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_000; // no amplification (shift 0) + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd0; + agc_target = 8'd200; // high target so signal is "weak" + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send a small sample + send_sample(16'sd50, 16'sd50); + + // Coincident frame_boundary + valid_in with a LARGER sample (not saturating) + @(negedge clk); + data_i_in = 16'sd10000; + data_q_in = 16'sd10000; + valid_in = 1'b1; + frame_boundary = 1'b1; + @(posedge clk); #1; + @(negedge clk); + valid_in = 1'b0; + frame_boundary = 1'b0; + @(posedge clk); #1; + @(posedge clk); #1; + + // Peak should be max(|10000|,|10000|) = 10000 → [14:7] = 78 + check(peak_magnitude == 8'd78, + "T18b.1: Coincident larger peak included (peak=78)"); + // No saturation at gain 0 + check(saturation_count == 8'd0, + "T18b.2: No saturation (gain=0, no overflow)"); + + // ================================================================= + // Test 19: AGC enable toggle mid-frame + // Verify gain initializes from gain_shift and holdoff resets + // ================================================================= + $display(""); + $display("--- Test 19: AGC enable toggle mid-frame ---"); + + // ----- 19a: Enable AGC mid-frame, verify gain init ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_101; // amplify x32 (shift left 5), internal = +5 + agc_attack = 4'd2; + agc_decay = 4'd1; + agc_holdoff = 4'd3; + agc_target = 8'd100; + agc_enable = 0; // start disabled + @(posedge clk); #1; + + // With AGC off, current_gain should follow gain_shift directly + check(current_gain == 4'b0_101, + "T19a.1: AGC disabled → current_gain = gain_shift (0x5)"); + + // Send a few samples (building up frame metrics) + send_sample(16'sd1000, 16'sd1000); + send_sample(16'sd2000, 16'sd2000); + + // Toggle AGC enable ON mid-frame + @(negedge clk); + agc_enable = 1; + @(posedge clk); #1; + @(posedge clk); #1; // let enable transition register + + // Gain should initialize from gain_shift encoding (0b0_101 → +5) + check(current_gain == 4'b0_101, + "T19a.2: AGC enabled mid-frame → gain initialized from gain_shift (+5)"); + + // Send a saturating sample, then boundary + send_sample(16'sd5000, 16'sd5000); // 5000<<5 overflows + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + + // AGC should attack: gain +5 → +5-2 = +3 + check(current_gain == 4'b0_011, + "T19a.3: After boundary, AGC attacked (gain +5 → +3)"); + + // ----- 19b: Disable AGC mid-frame, verify passthrough ----- + $display(""); + $display("--- Test 19b: Disable AGC mid-frame ---"); + + // Change gain_shift to a new value + @(negedge clk); + gain_shift = 4'b1_010; // attenuate by 2 (right shift 2) + agc_enable = 0; + @(posedge clk); #1; + @(posedge clk); #1; + + // With AGC off, current_gain should follow gain_shift + check(current_gain == 4'b1_010, + "T19b.1: AGC disabled → current_gain = gain_shift (0xA, atten 2)"); + + // Send sample: 1000 >> 2 = 250 + send_sample(16'sd1000, 16'sd0); + check(data_i_out == 16'sd250, + "T19b.2: Output uses host gain_shift when AGC off: 1000>>2=250"); + + // ----- 19c: Re-enable, verify gain re-initializes ----- + @(negedge clk); + gain_shift = 4'b0_010; // amplify by 4 (shift left 2), internal = +2 + agc_enable = 1; + @(posedge clk); #1; + @(posedge clk); #1; + + check(current_gain == 4'b0_010, + "T19c.1: AGC re-enabled → gain re-initialized from gain_shift (+2)"); + // --------------------------------------------------------------- // SUMMARY // --------------------------------------------------------------- diff --git a/9_Firmware/9_3_GUI/radar_dashboard.py b/9_Firmware/9_3_GUI/radar_dashboard.py index b4b35e8..7575e63 100644 --- a/9_Firmware/9_3_GUI/radar_dashboard.py +++ b/9_Firmware/9_3_GUI/radar_dashboard.py @@ -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() diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 0bf0a7a..77cd091 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -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 (" diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 6d79776..ce09438 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -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 diff --git a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v index bfd2c8e..94d0a82 100644 --- a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v +++ b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v @@ -504,6 +504,37 @@ module tb_cross_layer_ft2232h; check(cmd_opcode === 8'h27 && cmd_value === 16'h0003, "Cmd 0x27: DC_NOTCH_WIDTH=3"); + // AGC registers (0x28-0x2C) + send_command_ft2232h(8'h28, 8'h00, 8'h00, 8'h01); // AGC_ENABLE=1 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h28, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h28 && cmd_value === 16'h0001, + "Cmd 0x28: AGC_ENABLE=1"); + + send_command_ft2232h(8'h29, 8'h00, 8'h00, 8'hC8); // AGC_TARGET=200 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h29, 8'h00, 16'h00C8, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h29 && cmd_value === 16'h00C8, + "Cmd 0x29: AGC_TARGET=200"); + + send_command_ft2232h(8'h2A, 8'h00, 8'h00, 8'h02); // AGC_ATTACK=2 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2A, 8'h00, 16'h0002, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2A && cmd_value === 16'h0002, + "Cmd 0x2A: AGC_ATTACK=2"); + + send_command_ft2232h(8'h2B, 8'h00, 8'h00, 8'h03); // AGC_DECAY=3 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2B, 8'h00, 16'h0003, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2B && cmd_value === 16'h0003, + "Cmd 0x2B: AGC_DECAY=3"); + + send_command_ft2232h(8'h2C, 8'h00, 8'h00, 8'h06); // AGC_HOLDOFF=6 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2C, 8'h00, 16'h0006, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2C && cmd_value === 16'h0006, + "Cmd 0x2C: AGC_HOLDOFF=6"); + // Self-test / status send_command_ft2232h(8'h30, 8'h00, 8'h00, 8'h01); // SELF_TEST_TRIGGER $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n",