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:
@@ -2117,8 +2117,9 @@ int main(void)
|
|||||||
runRadarPulseSequence();
|
runRadarPulseSequence();
|
||||||
|
|
||||||
/* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13),
|
/* [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,
|
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
||||||
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
||||||
outerAgc.update(sat);
|
outerAgc.update(sat);
|
||||||
|
|||||||
@@ -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)
|
// Previous AGC enable state (for detecting 0→1 transition)
|
||||||
reg agc_enable_prev;
|
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
|
// EFFECTIVE GAIN SELECTION
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -198,6 +203,15 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
// Track AGC enable transitions
|
// Track AGC enable transitions
|
||||||
agc_enable_prev <= agc_enable;
|
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) ----
|
// ---- Data pipeline (1-cycle latency) ----
|
||||||
valid_out <= valid_in;
|
valid_out <= valid_in;
|
||||||
if (valid_in) begin
|
if (valid_in) begin
|
||||||
@@ -215,9 +229,13 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
|
|
||||||
// ---- Frame boundary: AGC update + metric snapshot ----
|
// ---- Frame boundary: AGC update + metric snapshot ----
|
||||||
if (frame_boundary) begin
|
if (frame_boundary) begin
|
||||||
// Snapshot per-frame metrics to output registers
|
// Snapshot per-frame metrics INCLUDING current sample if valid_in
|
||||||
saturation_count <= frame_sat_count;
|
saturation_count <= wire_frame_sat_incr
|
||||||
peak_magnitude <= frame_peak[14:7]; // Upper 8 bits of 15-bit peak
|
? (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
|
// Reset per-frame accumulators for next frame
|
||||||
frame_sat_count <= 8'd0;
|
frame_sat_count <= 8'd0;
|
||||||
@@ -225,15 +243,17 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
|
|
||||||
if (agc_enable) begin
|
if (agc_enable) begin
|
||||||
// AGC auto-adjustment at frame boundary
|
// 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)
|
// 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}));
|
$signed({1'b0, agc_attack}));
|
||||||
holdoff_counter <= agc_holdoff; // Reset holdoff
|
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
|
// Signal too weak: increase gain after holdoff expires
|
||||||
if (holdoff_counter == 4'd0) begin
|
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}));
|
$signed({1'b0, agc_decay}));
|
||||||
end else begin
|
end else begin
|
||||||
holdoff_counter <= holdoff_counter - 4'd1;
|
holdoff_counter <= holdoff_counter - 4'd1;
|
||||||
|
|||||||
@@ -545,6 +545,315 @@ initial begin
|
|||||||
check(current_gain == 4'b0001,
|
check(current_gain == 4'b0001,
|
||||||
"T16.3: Gain increased after holdoff expired (gain 0->1)");
|
"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
|
// SUMMARY
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -734,10 +734,10 @@ class RadarDashboard:
|
|||||||
mode_str = "AUTO" if status.agc_enable else "MANUAL"
|
mode_str = "AUTO" if status.agc_enable else "MANUAL"
|
||||||
mode_color = GREEN if status.agc_enable else FG
|
mode_color = GREEN if status.agc_enable else FG
|
||||||
self._agc_badge.config(text=f"AGC: {mode_str}", foreground=mode_color)
|
self._agc_badge.config(text=f"AGC: {mode_str}", foreground=mode_color)
|
||||||
self._agc_current_gain_lbl.config(
|
self._agc_gain_value.config(
|
||||||
text=f"Current Gain: {status.agc_current_gain}")
|
text=f"Gain: {status.agc_current_gain}")
|
||||||
self._agc_current_peak_lbl.config(
|
self._agc_peak_value.config(
|
||||||
text=f"Peak Mag: {status.agc_peak_magnitude}")
|
text=f"Peak: {status.agc_peak_magnitude}")
|
||||||
|
|
||||||
total_sat = sum(self._agc_sat_history)
|
total_sat = sum(self._agc_sat_history)
|
||||||
if total_sat > 10:
|
if total_sat > 10:
|
||||||
@@ -746,8 +746,8 @@ class RadarDashboard:
|
|||||||
sat_color = YELLOW
|
sat_color = YELLOW
|
||||||
else:
|
else:
|
||||||
sat_color = GREEN
|
sat_color = GREEN
|
||||||
self._agc_sat_total_lbl.config(
|
self._agc_sat_badge.config(
|
||||||
text=f"Total Saturations: {total_sat}", foreground=sat_color)
|
text=f"Saturation: {total_sat}", foreground=sat_color)
|
||||||
|
|
||||||
# ---- Throttle matplotlib redraws ---------------------------------
|
# ---- Throttle matplotlib redraws ---------------------------------
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|||||||
@@ -462,6 +462,11 @@ _HARDWARE_ONLY_OPCODES = {
|
|||||||
0x15, # CHIRPS_PER_ELEV
|
0x15, # CHIRPS_PER_ELEV
|
||||||
0x16, # GAIN_SHIFT
|
0x16, # GAIN_SHIFT
|
||||||
0x20, # RANGE_MODE
|
0x20, # RANGE_MODE
|
||||||
|
0x28, # AGC_ENABLE
|
||||||
|
0x29, # AGC_TARGET
|
||||||
|
0x2A, # AGC_ATTACK
|
||||||
|
0x2B, # AGC_DECAY
|
||||||
|
0x2C, # AGC_HOLDOFF
|
||||||
0x30, # SELF_TEST_TRIGGER
|
0x30, # SELF_TEST_TRIGGER
|
||||||
0x31, # SELF_TEST_STATUS
|
0x31, # SELF_TEST_STATUS
|
||||||
0xFF, # STATUS_REQUEST
|
0xFF, # STATUS_REQUEST
|
||||||
@@ -469,6 +474,7 @@ _HARDWARE_ONLY_OPCODES = {
|
|||||||
|
|
||||||
# Replay-adjustable opcodes (re-run signal processing)
|
# Replay-adjustable opcodes (re-run signal processing)
|
||||||
_REPLAY_ADJUSTABLE_OPCODES = {
|
_REPLAY_ADJUSTABLE_OPCODES = {
|
||||||
|
0x03, # DETECT_THRESHOLD
|
||||||
0x21, # CFAR_GUARD
|
0x21, # CFAR_GUARD
|
||||||
0x22, # CFAR_TRAIN
|
0x22, # CFAR_TRAIN
|
||||||
0x23, # CFAR_ALPHA
|
0x23, # CFAR_ALPHA
|
||||||
@@ -612,6 +618,7 @@ class ReplayConnection:
|
|||||||
self._cfar_alpha: int = 0x30
|
self._cfar_alpha: int = 0x30
|
||||||
self._cfar_mode: int = 0 # 0=CA, 1=GO, 2=SO
|
self._cfar_mode: int = 0 # 0=CA, 1=GO, 2=SO
|
||||||
self._cfar_enable: bool = True
|
self._cfar_enable: bool = True
|
||||||
|
self._detect_threshold: int = 10000 # RTL default (host_detect_threshold)
|
||||||
# Raw source arrays (loaded once, reprocessed on param change)
|
# Raw source arrays (loaded once, reprocessed on param change)
|
||||||
self._dop_mti_i: np.ndarray | None = None
|
self._dop_mti_i: np.ndarray | None = None
|
||||||
self._dop_mti_q: 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"(MTI={'ON' if self._mti_enable else 'OFF'}, "
|
||||||
f"{self._frame_len} bytes/frame)")
|
f"{self._frame_len} bytes/frame)")
|
||||||
return True
|
return True
|
||||||
except (OSError, ValueError, struct.error) as e:
|
except (OSError, ValueError, IndexError, struct.error) as e:
|
||||||
log.error(f"Replay open failed: {e}")
|
log.error(f"Replay open failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -676,7 +683,11 @@ class ReplayConnection:
|
|||||||
if opcode in _REPLAY_ADJUSTABLE_OPCODES:
|
if opcode in _REPLAY_ADJUSTABLE_OPCODES:
|
||||||
changed = False
|
changed = False
|
||||||
with self._lock:
|
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:
|
if self._cfar_guard != value:
|
||||||
self._cfar_guard = value
|
self._cfar_guard = value
|
||||||
changed = True
|
changed = True
|
||||||
@@ -768,7 +779,10 @@ class ReplayConnection:
|
|||||||
mode=self._cfar_mode,
|
mode=self._cfar_mode,
|
||||||
)
|
)
|
||||||
else:
|
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())
|
det_count = int(det.sum())
|
||||||
log.info(f"Replay: rebuilt {NUM_CELLS} packets ("
|
log.info(f"Replay: rebuilt {NUM_CELLS} packets ("
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QTableWidget, QTableWidgetItem, QHeaderView,
|
QTableWidget, QTableWidgetItem, QHeaderView,
|
||||||
QPlainTextEdit, QStatusBar, QMessageBox,
|
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.backends.backend_qtagg import FigureCanvasQTAgg
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
@@ -173,8 +173,10 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._gui_timer.timeout.connect(self._refresh_gui)
|
self._gui_timer.timeout.connect(self._refresh_gui)
|
||||||
self._gui_timer.start(100)
|
self._gui_timer.start(100)
|
||||||
|
|
||||||
# Log handler for diagnostics
|
# Log handler for diagnostics (thread-safe via Qt signal)
|
||||||
self._log_handler = _QtLogHandler(self._log_append)
|
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)
|
self._log_handler.setLevel(logging.INFO)
|
||||||
logging.getLogger().addHandler(self._log_handler)
|
logging.getLogger().addHandler(self._log_handler)
|
||||||
|
|
||||||
@@ -403,7 +405,7 @@ class RadarDashboard(QMainWindow):
|
|||||||
self._targets_table_main = QTableWidget()
|
self._targets_table_main = QTableWidget()
|
||||||
self._targets_table_main.setColumnCount(5)
|
self._targets_table_main.setColumnCount(5)
|
||||||
self._targets_table_main.setHorizontalHeaderLabels([
|
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.setAlternatingRowColors(True)
|
||||||
self._targets_table_main.setSelectionBehavior(
|
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__()
|
super().__init__()
|
||||||
self._callback = callback
|
self._bridge = bridge
|
||||||
self.setFormatter(logging.Formatter(
|
self.setFormatter(logging.Formatter(
|
||||||
"%(asctime)s %(levelname)-8s %(message)s",
|
"%(asctime)s %(levelname)-8s %(message)s",
|
||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
@@ -1736,6 +1745,6 @@ class _QtLogHandler(logging.Handler):
|
|||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
try:
|
try:
|
||||||
msg = self.format(record)
|
msg = self.format(record)
|
||||||
self._callback(msg)
|
self._bridge.log_message.emit(msg)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -504,6 +504,37 @@ module tb_cross_layer_ft2232h;
|
|||||||
check(cmd_opcode === 8'h27 && cmd_value === 16'h0003,
|
check(cmd_opcode === 8'h27 && cmd_value === 16'h0003,
|
||||||
"Cmd 0x27: DC_NOTCH_WIDTH=3");
|
"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
|
// Self-test / status
|
||||||
send_command_ft2232h(8'h30, 8'h00, 8'h00, 8'h01); // SELF_TEST_TRIGGER
|
send_command_ft2232h(8'h30, 8'h00, 8'h00, 8'h01); // SELF_TEST_TRIGGER
|
||||||
$fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n",
|
$fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user