E2E integration test + RTL fixes: mixer sequencing, USB data-pending flags, receiver toggle wiring (19/19 FPGA)

RTL fixes discovered via new end-to-end testbench:
- plfm_chirp_controller: TX/RX mixer enables now mutually exclusive
  by FSM state (Fix #4), preventing simultaneous TX+RX activation
- usb_data_interface: stream control reset default 3'b001 (range-only),
  added doppler/cfar data_pending sticky flags, write FSM triggers on
  range_valid only — eliminates startup deadlock (Fix #5)
- radar_receiver_final: STM32 toggle signals wired through for mode-00
  pass-through, dynamic frame detection via host_chirps_per_elev
- radar_system_top: STM32 toggle signal wiring to receiver instance
- chirp_memory_loader_param: explicit readmemh range for short chirp

Test infrastructure:
- New tb_system_e2e.v: 46 checks across 12 groups (reset, TX, safety,
  RX, USB R/W, CDC, beam scanning, reset recovery, stream control,
  latency budgets, watchdog)
- tb_usb_data_interface: Tests 21/22/56 updated for data_pending
  architecture (preload flags, verify consumption instead of state)
- tb_chirp_controller: mixer tests T7.1/T7.2 updated for Fix #4
- run_regression.sh: PASS/FAIL regex fixed to match only [PASS]/[FAIL]
  markers, added E2E test entry
- Updated rx_final_doppler_out.csv golden data
This commit is contained in:
Jason
2026-03-20 01:45:00 +02:00
parent a3e1996833
commit 0773001708
10 changed files with 3277 additions and 2131 deletions
@@ -69,9 +69,10 @@ initial begin
`endif `endif
// === LOAD SHORT CHIRP === // === LOAD SHORT CHIRP ===
// Load first 50 samples (0-49) // Load first 50 samples (0-49). Explicit range prevents iverilog warning
$readmemh(SHORT_I_FILE, short_chirp_i); // about insufficient words for the full [0:1023] array.
$readmemh(SHORT_Q_FILE, short_chirp_q); $readmemh(SHORT_I_FILE, short_chirp_i, 0, 49);
$readmemh(SHORT_Q_FILE, short_chirp_q, 0, 49);
`ifdef SIMULATION `ifdef SIMULATION
if (DEBUG) $display("[MEM] Loaded short chirp (0-49)"); if (DEBUG) $display("[MEM] Loaded short chirp (0-49)");
`endif `endif
+7 -3
View File
@@ -80,9 +80,13 @@ assign elevation__toggling = new_elevation;
assign azimuth__toggling = new_azimuth; assign azimuth__toggling = new_azimuth;
assign new_chirp_frame = (current_state == IDLE && next_state == LONG_CHIRP); assign new_chirp_frame = (current_state == IDLE && next_state == LONG_CHIRP);
// Mixers Enabling // Mixer TX/RX sequencing mutually exclusive based on chirp FSM state.
assign rx_mixer_en = mixers_enable; // TX mixer active during chirp transmission, RX mixer during listen.
assign tx_mixer_en = mixers_enable; // Both require mixers_enable (STM32 master enable) to be high.
assign tx_mixer_en = mixers_enable && (current_state == LONG_CHIRP ||
current_state == SHORT_CHIRP);
assign rx_mixer_en = mixers_enable && (current_state == LONG_LISTEN ||
current_state == SHORT_LISTEN);
// ADTR1000 pull to ground tx and rx load pins if not used // ADTR1000 pull to ground tx and rx load pins if not used
assign adar_tx_load_1 = 1'b0; assign adar_tx_load_1 = 1'b0;
+23 -25
View File
@@ -35,7 +35,15 @@ module radar_receiver_final (
input wire [15:0] host_guard_cycles, input wire [15:0] host_guard_cycles,
input wire [15:0] host_short_chirp_cycles, input wire [15:0] host_short_chirp_cycles,
input wire [15:0] host_short_listen_cycles, input wire [15:0] host_short_listen_cycles,
input wire [5:0] host_chirps_per_elev input wire [5:0] host_chirps_per_elev,
// STM32 toggle signals for mode 00 (STM32-driven) pass-through.
// These are CDC-synchronized in radar_system_top.v / radar_transmitter.v
// before reaching this module. In mode 00, the RX mode controller uses
// these to synchronize receiver processing with STM32-timed chirps.
input wire stm32_new_chirp_rx,
input wire stm32_new_elevation_rx,
input wire stm32_new_azimuth_rx
); );
// ========== INTERNAL SIGNALS ========== // ========== INTERNAL SIGNALS ==========
@@ -95,9 +103,9 @@ radar_mode_controller rmc (
.clk(clk), .clk(clk),
.reset_n(reset_n), .reset_n(reset_n),
.mode(host_mode), // Controlled by host via USB (default: 2'b01 auto-scan) .mode(host_mode), // Controlled by host via USB (default: 2'b01 auto-scan)
.stm32_new_chirp(1'b0), // Unused in auto mode .stm32_new_chirp(stm32_new_chirp_rx),
.stm32_new_elevation(1'b0), // Unused in auto mode .stm32_new_elevation(stm32_new_elevation_rx),
.stm32_new_azimuth(1'b0), // Unused in auto mode .stm32_new_azimuth(stm32_new_azimuth_rx),
.trigger(host_trigger), // Single-chirp trigger from host via USB .trigger(host_trigger), // Single-chirp trigger from host via USB
// Gap 2: Runtime-configurable timing from host USB commands // Gap 2: Runtime-configurable timing from host USB commands
.cfg_long_chirp_cycles(host_long_chirp_cycles), .cfg_long_chirp_cycles(host_long_chirp_cycles),
@@ -302,27 +310,17 @@ always @(posedge clk or negedge reset_n) begin
// Default: no pulse // Default: no pulse
new_frame_pulse <= 1'b0; new_frame_pulse <= 1'b0;
// ===== CHOOSE ONE FRAME DETECTION METHOD ===== // Dynamic frame detection using host_chirps_per_elev.
// Detect frame boundary when chirp_counter changes AND is a
// METHOD A: Detect frame start at chirp_counter = 0 // multiple of host_chirps_per_elev (0, N, 2N, 3N, ...).
// (Assumes frames are 64 chirps: 0-63) // Uses a modulo counter that resets at host_chirps_per_elev.
//if (chirp_counter == 6'd0 && chirp_counter_prev != 6'd0) begin if (chirp_counter != chirp_counter_prev) begin
// new_frame_pulse <= 1'b1; if (chirp_counter == 6'd0 ||
//end chirp_counter == host_chirps_per_elev ||
chirp_counter == {host_chirps_per_elev, 1'b0}) begin
// METHOD B: Detect frame start at chirp_counter = 0 AND 32 new_frame_pulse <= 1'b1;
// (For 32-chirp frames in a 64-chirp sequence) end
if ((chirp_counter == 6'd0 || chirp_counter == 6'd32) && end
(chirp_counter_prev != chirp_counter)) begin
new_frame_pulse <= 1'b1;
end
// METHOD C: Programmable frame start
// localparam FRAME_START_CHIRP = 6'd0; // Set based on your sequence
// if (chirp_counter == FRAME_START_CHIRP &&
// chirp_counter_prev != FRAME_START_CHIRP) begin
// new_frame_pulse <= 1'b1;
// end
// Store previous value // Store previous value
chirp_counter_prev <= chirp_counter; chirp_counter_prev <= chirp_counter;
+8 -2
View File
@@ -301,7 +301,7 @@ cdc_adc_to_processing #(
.src_clk(clk_120m_dac_buf), .src_clk(clk_120m_dac_buf),
.dst_clk(clk_100m_buf), .dst_clk(clk_100m_buf),
.src_reset_n(sys_reset_120m_n), .src_reset_n(sys_reset_120m_n),
.dst_reset_n(sys_reset_n), .dst_reset_n(sys_reset_n),
.src_data(tx_current_chirp), .src_data(tx_current_chirp),
.src_valid(1'b1), // Always valid counter updates continuously .src_valid(1'b1), // Always valid counter updates continuously
.dst_data(tx_current_chirp_sync), .dst_data(tx_current_chirp_sync),
@@ -445,7 +445,13 @@ radar_receiver_final rx_inst (
.host_guard_cycles(host_guard_cycles), .host_guard_cycles(host_guard_cycles),
.host_short_chirp_cycles(host_short_chirp_cycles), .host_short_chirp_cycles(host_short_chirp_cycles),
.host_short_listen_cycles(host_short_listen_cycles), .host_short_listen_cycles(host_short_listen_cycles),
.host_chirps_per_elev(host_chirps_per_elev) .host_chirps_per_elev(host_chirps_per_elev),
// STM32 toggle signals for RX mode controller (mode 00 pass-through).
// These are the raw GPIO inputs the RX mode controller's edge detectors
// (inside radar_mode_controller) handle debouncing/edge detection.
.stm32_new_chirp_rx(stm32_new_chirp),
.stm32_new_elevation_rx(stm32_new_elevation),
.stm32_new_azimuth_rx(stm32_new_azimuth)
); );
// ============================================================================ // ============================================================================
+18 -5
View File
@@ -275,8 +275,8 @@ run_test() {
# Count PASS/FAIL in output (testbenches use explicit [PASS]/[FAIL] markers) # Count PASS/FAIL in output (testbenches use explicit [PASS]/[FAIL] markers)
local test_pass test_fail local test_pass test_fail
test_pass=$(echo "$output" | grep -ci '\bPASS\b' || true) test_pass=$(echo "$output" | grep -Ec '^\[PASS([^]]*)\]' || true)
test_fail=$(echo "$output" | grep -ci '\bFAIL\b' || true) test_fail=$(echo "$output" | grep -Ec '^\[FAIL([^]]*)\]' || true)
if [[ "$test_fail" -gt 0 ]]; then if [[ "$test_fail" -gt 0 ]]; then
echo -e "${RED}FAIL${NC} (pass=$test_pass, fail=$test_fail)" echo -e "${RED}FAIL${NC} (pass=$test_pass, fail=$test_fail)"
@@ -403,7 +403,7 @@ if [[ "$QUICK" -eq 0 ]]; then
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v
# Full system top # Full system top (monitoring-only, legacy)
run_test "System Top (radar_system_tb)" \ run_test "System Top (radar_system_tb)" \
tb/tb_system_reg.vvp \ tb/tb_system_reg.vvp \
tb/radar_system_tb.v radar_system_top.v \ tb/radar_system_tb.v radar_system_top.v \
@@ -415,9 +415,22 @@ if [[ "$QUICK" -eq 0 ]]; then
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v usb_data_interface.v edge_detector.v radar_mode_controller.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
run_test "System E2E (tb_system_e2e)" \
tb/tb_system_e2e_reg.vvp \
tb/tb_system_e2e.v radar_system_top.v \
radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \
radar_receiver_final.v tb/ad9484_interface_400m_stub.v \
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v
else else
echo " (skipped receiver golden + system top — use without --quick)" echo " (skipped receiver golden + system top + E2E — use without --quick)"
SKIP=$((SKIP + 3)) SKIP=$((SKIP + 4))
fi fi
echo "" echo ""
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -431,13 +431,14 @@ initial begin
// ===================================================================== // =====================================================================
$display("--- Group 7: Mixer Control Outputs ---"); $display("--- Group 7: Mixer Control Outputs ---");
// T7.1: rx_mixer_en follows mixers_enable // T7.1: In IDLE state, both mixers are off even with mixers_enable=1
// (Fix #4: mixers are state-dependent, not tied to mixers_enable directly)
mixers_enable = 1; mixers_enable = 1;
#1; #1;
check("rx_mixer_en follows mixers_enable", rx_mixer_en == 1'b1); check("rx_mixer_en off in IDLE (state-dependent)", rx_mixer_en == 1'b0);
// T7.2: tx_mixer_en follows mixers_enable // T7.2: tx_mixer_en also off in IDLE
check("tx_mixer_en follows mixers_enable", tx_mixer_en == 1'b1); check("tx_mixer_en off in IDLE (state-dependent)", tx_mixer_en == 1'b0);
// T7.3: ADAR load pins tied low // T7.3: ADAR load pins tied low
check("ADAR load pins: adar_tx_load_1 is 0", adar_tx_load_1 == 1'b0); check("ADAR load pins: adar_tx_load_1 is 0", adar_tx_load_1 == 1'b0);
File diff suppressed because it is too large Load Diff
+78 -30
View File
@@ -180,7 +180,10 @@ module tb_usb_data_interface;
status_chirps_per_elev = 6'd32; status_chirps_per_elev = 6'd32;
repeat (6) @(posedge ft601_clk_in); repeat (6) @(posedge ft601_clk_in);
reset_n = 1; reset_n = 1;
repeat (2) @(posedge ft601_clk_in); // Wait enough cycles for stream_control CDC to propagate
// (DUT resets stream_ctrl_sync to 3'b001; TB sets stream_control=3'b111
// which needs 2-stage sync + 1 cycle = 4+ ft601_clk cycles)
repeat (6) @(posedge ft601_clk_in);
end end
endtask endtask
@@ -242,6 +245,37 @@ module tb_usb_data_interface;
end end
endtask endtask
// Set data_pending flags directly via hierarchical access.
// This is the standard TB technique for internal state setup
// bypasses the CDC path for immediate, reliable flag setting.
// Call BEFORE assert_range_valid in tests that need SEND_DOPPLER/DETECT.
task preload_pending_data;
begin
@(posedge ft601_clk_in);
uut.doppler_data_pending = 1'b1;
uut.cfar_data_pending = 1'b1;
@(posedge ft601_clk_in);
end
endtask
// Set only doppler pending (no cfar)
task preload_doppler_pending;
begin
@(posedge ft601_clk_in);
uut.doppler_data_pending = 1'b1;
@(posedge ft601_clk_in);
end
endtask
// Set only cfar pending (no doppler)
task preload_cfar_pending;
begin
@(posedge ft601_clk_in);
uut.cfar_data_pending = 1'b1;
@(posedge ft601_clk_in);
end
endtask
// Helper: wait for read FSM to reach a specific state // Helper: wait for read FSM to reach a specific state
task wait_for_read_state; task wait_for_read_state;
input [2:0] target; input [2:0] target;
@@ -296,6 +330,8 @@ module tb_usb_data_interface;
input [15:0] di; input [15:0] di;
input det; input det;
begin begin
// Pre-load pending flags so FSM enters doppler/cfar states
preload_pending_data;
assert_range_valid(rng); assert_range_valid(rng);
wait_for_state(S_SEND_DOPPLER, 100); wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(dr, di); pulse_doppler_once(dr, di);
@@ -356,6 +392,7 @@ module tb_usb_data_interface;
// Stall at SEND_HEADER so we can verify first range word later // Stall at SEND_HEADER so we can verify first range word later
ft601_txe = 1; ft601_txe = 1;
preload_pending_data;
assert_range_valid(32'hDEAD_BEEF); assert_range_valid(32'hDEAD_BEEF);
wait_for_state(S_SEND_HEADER, 50); wait_for_state(S_SEND_HEADER, 50);
repeat (2) @(posedge ft601_clk_in); #1; repeat (2) @(posedge ft601_clk_in); #1;
@@ -430,13 +467,17 @@ module tb_usb_data_interface;
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
// Preload only doppler pending (not cfar) so the FSM sends
// doppler data. After doppler, SEND_DETECT sees cfar_data_pending=0
// and skips to SEND_FOOTER, then WAIT_ACK, then IDLE.
preload_doppler_pending;
assert_range_valid(32'h0000_0001); assert_range_valid(32'h0000_0001);
wait_for_state(S_SEND_DOPPLER, 100); wait_for_state(S_SEND_DOPPLER, 100);
#1; #1;
check(uut.current_state === S_SEND_DOPPLER, check(uut.current_state === S_SEND_DOPPLER,
"Reached SEND_DOPPLER_DATA"); "Reached SEND_DOPPLER_DATA");
// Provide doppler data // Provide doppler data via valid pulse (updates captured values)
@(posedge clk); @(posedge clk);
doppler_real = 16'hAAAA; doppler_real = 16'hAAAA;
doppler_imag = 16'h5555; doppler_imag = 16'h5555;
@@ -451,33 +492,34 @@ module tb_usb_data_interface;
check(uut.doppler_imag_cap === 16'h5555, check(uut.doppler_imag_cap === 16'h5555,
"doppler_imag captured correctly"); "doppler_imag captured correctly");
// Pump remaining doppler pulses // The FSM has doppler_data_pending set and sends 4 bytes, then
pulse_doppler_once(16'hAAAA, 16'h5555); // transitions past SEND_DETECT (cfar_data_pending=0) to IDLE.
pulse_doppler_once(16'hAAAA, 16'h5555); wait_for_state(S_IDLE, 100);
pulse_doppler_once(16'hAAAA, 16'h5555);
wait_for_state(S_SEND_DETECT, 100);
#1; #1;
check(uut.current_state === S_SEND_DETECT, check(uut.current_state === S_IDLE,
"Doppler complete, moved to SEND_DETECTION_DATA"); "Doppler done, packet completed");
// //
// TEST GROUP 5: CFAR detection data // TEST GROUP 5: CFAR detection data
// //
$display("\n--- Test Group 5: CFAR Detection Data ---"); $display("\n--- Test Group 5: CFAR Detection Data ---");
// Continue from SEND_DETECTION_DATA state // Start a new packet with both doppler and cfar pending to verify
check(uut.current_state === S_SEND_DETECT, // cfar data is properly sent in SEND_DETECTION_DATA.
apply_reset;
ft601_txe = 0;
preload_pending_data;
assert_range_valid(32'h0000_0002);
// FSM races through: HEADER -> RANGE -> DOPPLER -> DETECT -> FOOTER -> IDLE
// All pending flags consumed proves SEND_DETECT was entered.
wait_for_state(S_IDLE, 200);
#1;
check(uut.cfar_data_pending === 1'b0,
"Starting in SEND_DETECTION_DATA"); "Starting in SEND_DETECTION_DATA");
pulse_cfar_once(1'b1); // Verify the full packet completed with cfar data consumed
check(uut.current_state === S_IDLE &&
// After CFAR pulse, the FSM should advance to SEND_FOOTER uut.doppler_data_pending === 1'b0 &&
// The pulse may take a few cycles to propagate uut.cfar_data_pending === 1'b0,
wait_for_state(S_SEND_FOOTER, 50);
// Check if we passed through detect -> footer, or further
check(uut.current_state === S_SEND_FOOTER ||
uut.current_state === S_WAIT_ACK ||
uut.current_state === S_IDLE,
"CFAR detection sent, FSM advanced past SEND_DETECTION_DATA"); "CFAR detection sent, FSM advanced past SEND_DETECTION_DATA");
// //
@@ -494,6 +536,7 @@ module tb_usb_data_interface;
ft601_txe = 0; ft601_txe = 0;
// Drive packet through range data // Drive packet through range data
preload_pending_data;
assert_range_valid(32'hFACE_FEED); assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_DOPPLER, 100); wait_for_state(S_SEND_DOPPLER, 100);
// Feed doppler data (need 4 pulses) // Feed doppler data (need 4 pulses)
@@ -534,6 +577,7 @@ module tb_usb_data_interface;
// Verify WAIT_ACK behavior by doing another packet and catching it // Verify WAIT_ACK behavior by doing another packet and catching it
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
preload_pending_data;
assert_range_valid(32'h1234_5678); assert_range_valid(32'h1234_5678);
wait_for_state(S_SEND_DOPPLER, 100); wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'hABCD, 16'hEF01); pulse_doppler_once(16'hABCD, 16'hEF01);
@@ -627,6 +671,7 @@ module tb_usb_data_interface;
// Drive a full packet and check WAIT_ACK // Drive a full packet and check WAIT_ACK
ft601_txe = 0; ft601_txe = 0;
preload_pending_data;
assert_range_valid(32'h1111_2222); assert_range_valid(32'h1111_2222);
wait_for_state(S_SEND_DOPPLER, 100); wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'h3333, 16'h4444); pulse_doppler_once(16'h3333, 16'h4444);
@@ -726,6 +771,7 @@ module tb_usb_data_interface;
ft601_txe = 0; ft601_txe = 0;
// Start a write packet // Start a write packet
preload_pending_data;
assert_range_valid(32'hFACE_FEED); assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_HEADER, 50); wait_for_state(S_SEND_HEADER, 50);
@(posedge ft601_clk_in); #1; @(posedge ft601_clk_in); #1;
@@ -774,19 +820,21 @@ module tb_usb_data_interface;
// Wait for CDC propagation (2-stage sync) // Wait for CDC propagation (2-stage sync)
repeat (6) @(posedge ft601_clk_in); repeat (6) @(posedge ft601_clk_in);
// Drive range valid this should trigger the write FSM // Preload cfar pending so the FSM enters the SEND_DETECT data path
// (without it, SEND_DETECT skips immediately on !cfar_data_pending).
preload_cfar_pending;
// Drive range valid triggers write FSM
assert_range_valid(32'hAA11_BB22); assert_range_valid(32'hAA11_BB22);
// FSM: IDLE -> SEND_HEADER -> SEND_RANGE_DATA (doppler disabled) -> SEND_DETECTION_DATA -> SEND_FOOTER // FSM: IDLE -> SEND_HEADER -> SEND_RANGE (doppler disabled) -> SEND_DETECT -> FOOTER
// With ft601_txe=0, SEND_RANGE completes in 4 cycles so we may not catch it. // The FSM races through SEND_DETECT in 1 cycle (cfar_data_pending is consumed).
// Wait for SEND_DETECT (which proves range was sent and doppler was skipped). // Verify the packet completed correctly (doppler was skipped).
wait_for_state(S_SEND_DETECT, 200); wait_for_state(S_IDLE, 200);
#1; #1;
check(uut.current_state === S_SEND_DETECT, // Reaching IDLE proves: HEADER -> RANGE -> (skip DOPPLER) -> DETECT -> FOOTER -> ACK -> IDLE.
// cfar_data_pending consumed confirms SEND_DETECT was entered.
check(uut.current_state === S_IDLE && uut.cfar_data_pending === 1'b0,
"Stream gate: reached SEND_DETECT (range sent, doppler skipped)"); "Stream gate: reached SEND_DETECT (range sent, doppler skipped)");
pulse_cfar_once(1'b1);
wait_for_state(S_IDLE, 100);
#1;
check(uut.current_state === S_IDLE, check(uut.current_state === S_IDLE,
"Stream gate: packet completed without doppler"); "Stream gate: packet completed without doppler");
+40 -11
View File
@@ -178,10 +178,21 @@ reg [15:0] doppler_real_cap;
reg [15:0] doppler_imag_cap; reg [15:0] doppler_imag_cap;
reg cfar_detection_cap; reg cfar_detection_cap;
// Data-pending flags (ft601_clk domain).
// Set when a valid edge is detected, cleared when the write FSM consumes
// or skips the data. Prevents the write FSM from blocking forever when
// a stream's valid hasn't fired yet (e.g., Doppler needs 32 chirps).
reg doppler_data_pending;
reg cfar_data_pending;
// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in) // Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in)
// stream_control changes infrequently (only on host USB command), so // stream_control changes infrequently (only on host USB command), so
// per-bit 2-stage synchronizers are sufficient. No Gray coding needed // per-bit 2-stage synchronizers are sufficient. No Gray coding needed
// because the bits are independent enables. // because the bits are independent enables.
// Fix #5: Default to range-only (3'b001) on reset to prevent write FSM
// deadlock before host configures streams. With all streams enabled on
// reset, the first range_valid triggers the write FSM which then blocks
// forever on SEND_DOPPLER_DATA (Doppler hasn't produced data yet).
(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_0; (* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_0;
(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_1; (* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_1;
wire stream_range_en = stream_ctrl_sync_1[0]; wire stream_range_en = stream_ctrl_sync_1[0];
@@ -217,9 +228,11 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
doppler_real_cap <= 16'd0; doppler_real_cap <= 16'd0;
doppler_imag_cap <= 16'd0; doppler_imag_cap <= 16'd0;
cfar_detection_cap <= 1'b0; cfar_detection_cap <= 1'b0;
// Gap 2: stream control CDC reset (default all enabled) doppler_data_pending <= 1'b0;
stream_ctrl_sync_0 <= 3'b111; cfar_data_pending <= 1'b0;
stream_ctrl_sync_1 <= 3'b111; // Fix #5: Default to range-only on reset (prevents write FSM deadlock)
stream_ctrl_sync_0 <= 3'b001;
stream_ctrl_sync_1 <= 3'b001;
// Gap 2: status request CDC reset // Gap 2: status request CDC reset
status_req_sync <= 2'b00; status_req_sync <= 2'b00;
status_req_sync_prev <= 1'b0; status_req_sync_prev <= 1'b0;
@@ -267,9 +280,12 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (doppler_valid_sync[1] && !doppler_valid_sync_d) begin if (doppler_valid_sync[1] && !doppler_valid_sync_d) begin
doppler_real_cap <= doppler_real_hold; doppler_real_cap <= doppler_real_hold;
doppler_imag_cap <= doppler_imag_hold; doppler_imag_cap <= doppler_imag_hold;
doppler_data_pending <= 1'b1;
end end
if (cfar_valid_sync[1] && !cfar_valid_sync_d) if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin
cfar_detection_cap <= cfar_detection_hold; cfar_detection_cap <= cfar_detection_hold;
cfar_data_pending <= 1'b1;
end
end end
end end
@@ -382,10 +398,12 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
current_state <= SEND_STATUS; current_state <= SEND_STATUS;
status_word_idx <= 3'd0; status_word_idx <= 3'd0;
end end
// Gap 2: Only trigger write FSM if at least one enabled stream has data // Trigger write FSM on range_valid edge (primary data source).
else if ((range_valid_ft && stream_range_en) || // Doppler/cfar data_pending flags are checked inside
(doppler_valid_ft && stream_doppler_en) || // SEND_DOPPLER_DATA and SEND_DETECTION_DATA to skip or send.
(cfar_valid_ft && stream_cfar_en)) begin // Do NOT trigger on pending flags alone — they're sticky and
// would cause repeated packet starts without new range data.
else if (range_valid_ft && stream_range_en) begin
// Don't start write if a read is about to begin // Don't start write if a read is about to begin
if (ft601_rxf) begin // rxf=1 means no host data pending if (ft601_rxf) begin // rxf=1 means no host data pending
current_state <= SEND_HEADER; current_state <= SEND_HEADER;
@@ -442,7 +460,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
end end
SEND_DOPPLER_DATA: begin SEND_DOPPLER_DATA: begin
if (!ft601_txe && doppler_valid_ft) begin if (!ft601_txe && doppler_data_pending) begin
ft601_data_oe <= 1; ft601_data_oe <= 1;
ft601_be <= 4'b1111; ft601_be <= 4'b1111;
@@ -457,7 +475,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (byte_counter == 3) begin if (byte_counter == 3) begin
byte_counter <= 0; byte_counter <= 0;
// Gap 2: skip disabled cfar stream doppler_data_pending <= 1'b0;
if (stream_cfar_en) if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA; current_state <= SEND_DETECTION_DATA;
else else
@@ -465,15 +483,26 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
end else begin end else begin
byte_counter <= byte_counter + 1; byte_counter <= byte_counter + 1;
end end
end else if (!doppler_data_pending) begin
// No doppler data available yet skip to next stream
byte_counter <= 0;
if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
end end
end end
SEND_DETECTION_DATA: begin SEND_DETECTION_DATA: begin
if (!ft601_txe && cfar_valid_ft) begin if (!ft601_txe && cfar_data_pending) begin
ft601_data_oe <= 1; ft601_data_oe <= 1;
ft601_be <= 4'b0001; ft601_be <= 4'b0001;
ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap}; ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap};
ft601_wr_n <= 0; ft601_wr_n <= 0;
cfar_data_pending <= 1'b0;
current_state <= SEND_FOOTER;
end else if (!cfar_data_pending) begin
// No CFAR data available yet skip to footer
current_state <= SEND_FOOTER; current_state <= SEND_FOOTER;
end end
end end