feat(fpga): make FT2232H default USB interface, rewrite FT601 write FSM, add clock-loss watchdog

- Set USB_MODE default to 1 (FT2232H) in radar_system_top.v; 200T build
  overrides to USB_MODE=0 via build_200t.tcl generic property
- Rewrite FT601 write FSM: 4-state architecture with 3-word packed data,
  pending-flag gating, and frame sync counter
- Add FT2232H read FSM rd_cmd_complete flag, stream field zeroing, and
  range_data_ready 1-cycle pipeline delay in both USB modules
- Implement clock-loss watchdog: ft_heartbeat toggle + 16-bit timeout
  counter drives ft_clk_lost, feeding ft_effective_reset_n via 2-stage
  ASYNC_REG synchronizer chain
- Fix sample_counter reset literal width (11'd0 -> 12'd0)
- Add FT2232H I/O timing constraints to 50T XDC; fix dac_clk comments
- Document vestigial ft601_txe_n/rxf_n ports (needed for 200T XDC)
- Tie off AGC ports on TE0713 dev wrapper
- Rewrite tb_usb_data_interface.v for new 4-state FSM (89 checks)
- Add USB_MODE=1 regression runs; remove dead CHECK 5/6 loop
- Update diag_log.h USB interface comment
This commit is contained in:
Jason
2026-04-16 16:18:52 +05:45
parent 8609e455a0
commit f393e96d69
12 changed files with 623 additions and 407 deletions
+8 -7
View File
@@ -32,8 +32,8 @@ the `USB_MODE` parameter in `radar_system_top.v`:
| USB_MODE | Interface | Bus Width | Speed | Board Target |
|----------|-----------|-----------|-------|--------------|
| 0 (default) | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board |
| 1 | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board |
| 0 | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board |
| 1 (default) | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board |
### How USB_MODE Works
@@ -72,7 +72,8 @@ The parameter is set via a **wrapper module** that overrides the default:
```
- **200T dev board**: `radar_system_top` is used directly as the top module.
`USB_MODE` defaults to `0` (FT601). No wrapper needed.
`USB_MODE` defaults to `1` (FT2232H) since production is the primary target.
Override with `.USB_MODE(0)` for FT601 builds.
### RTL Files by USB Interface
@@ -158,7 +159,7 @@ The build scripts automatically select the correct top module and constraints:
You do NOT need to set `USB_MODE` manually. The top module selection handles it:
- `radar_system_top_50t` forces `USB_MODE=1` internally
- `radar_system_top` defaults to `USB_MODE=0`
- `radar_system_top` defaults to `USB_MODE=1` (FT2232H, production default)
## How to Select Constraints in Vivado
@@ -190,9 +191,9 @@ read_xdc constraints/te0713_te0701_minimal.xdc
| Target | Top module | USB_MODE | USB Interface | Notes |
|--------|------------|----------|---------------|-------|
| 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 |
| 200T Dev (FBG484) | `radar_system_top` | 0 (default) | FT601 (32-bit) | No wrapper needed |
| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (default) | FT601 (32-bit) | Minimal bring-up wrapper |
| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (default) | FT601 (32-bit) | Alternate SoM wrapper |
| 200T Dev (FBG484) | `radar_system_top` | 0 (override) | FT601 (32-bit) | Build script overrides default USB_MODE=1 |
| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (override) | FT601 (32-bit) | Minimal bring-up wrapper |
| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (override) | FT601 (32-bit) | Alternate SoM wrapper |
## Trenz Split Status
@@ -70,9 +70,10 @@ set_input_jitter [get_clocks clk_100m] 0.1
# NOTE: The physical DAC (U3, AD9708) receives its clock directly from the
# AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA
# uses this clock input for internal DAC data timing only. The RTL port
# `dac_clk` is an output that assigns clk_120m directly — it has no
# separate physical pin on this board and should be removed from the
# RTL or left unconnected.
# `dac_clk` is an RTL output that assigns clk_120m directly. It has no
# physical pin on the 50T board and is left unconnected here. The port
# CANNOT be removed from the RTL because the 200T board uses it with
# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc).
# FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC).
# Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC).
set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}]
@@ -332,6 +333,44 @@ set_property DRIVE 8 [get_ports {ft_data[*]}]
# ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz)
# --------------------------------------------------------------------------
# FT2232H Source-Synchronous Timing Constraints
# --------------------------------------------------------------------------
# FT2232H 245 Synchronous FIFO mode timing (60 MHz, period = 16.667 ns):
#
# FPGA Read Path (FT2232H drives data, FPGA samples):
# - Data valid before CLKOUT rising edge: t_vr(max) = 7.0 ns
# - Data hold after CLKOUT rising edge: t_hr(min) = 0.0 ns
# - Input delay max = period - t_vr = 16.667 - 7.0 = 9.667 ns
# - Input delay min = t_hr = 0.0 ns
#
# FPGA Write Path (FPGA drives data, FT2232H samples):
# - Data setup before next CLKOUT rising: t_su = 5.0 ns
# - Data hold after CLKOUT rising: t_hd = 0.0 ns
# - Output delay max = period - t_su = 16.667 - 5.0 = 11.667 ns
# - Output delay min = t_hd = 0.0 ns
# --------------------------------------------------------------------------
# Input delays: FT2232H → FPGA (data bus and status signals)
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_rxf_n}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rxf_n}]
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_txe_n}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_txe_n}]
# Output delays: FPGA → FT2232H (control strobes and data bus when writing)
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_data[*]}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_rd_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rd_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_wr_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_wr_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_oe_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_oe_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_siwu}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_siwu}]
# ============================================================================
# STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS
# ============================================================================
@@ -418,10 +457,10 @@ set_property BITSTREAM.CONFIG.UNUSEDPIN Pullup [current_design]
# 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7).
# Dedicated pins — no XDC constraints needed.
#
# 5. dac_clk port: The RTL top module declares `dac_clk` as an output, but
# the physical board wires the DAC clock (AD9708 CLOCK pin) directly from
# the AD9523, not from the FPGA. This port should be removed from the RTL
# or left unconnected. It currently just assigns clk_120m_dac passthrough.
# 5. dac_clk port: Not connected on the 50T board (DAC clocked directly from
# AD9523). The RTL port exists for 200T board compatibility, where the FPGA
# forwards the DAC clock via ODDR to pin H17 with generated clock and
# timing constraints (see xc7a200t_fbg484.xdc). Do NOT remove from RTL.
#
# ============================================================================
# END OF CONSTRAINTS
+1 -1
View File
@@ -142,7 +142,7 @@ module radar_system_top (
parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp
parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing
parameter USB_ENABLE = 1'b1; // Enable USB data transfer
parameter USB_MODE = 0; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T)
parameter USB_MODE = 1; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T production default)
// ============================================================================
// INTERNAL SIGNALS
@@ -138,7 +138,12 @@ usb_data_interface usb_inst (
.status_range_mode(2'b01),
.status_self_test_flags(5'b11111),
.status_self_test_detail(8'hA5),
.status_self_test_busy(1'b0)
.status_self_test_busy(1'b0),
// AGC status: tie off with benign defaults (no AGC on dev board)
.status_agc_current_gain(4'd0),
.status_agc_peak_magnitude(8'd0),
.status_agc_saturation_count(8'd0),
.status_agc_enable(1'b0)
);
endmodule
+36 -23
View File
@@ -70,6 +70,7 @@ PROD_RTL=(
xfft_16.v
fft_engine.v
usb_data_interface.v
usb_data_interface_ft2232h.v
edge_detector.v
radar_mode_controller.v
rx_gain_control.v
@@ -219,26 +220,9 @@ run_lint_static() {
fi
done
# --- Single-line regex checks across all production RTL ---
for f in "$@"; do
[[ -f "$f" ]] || continue
case "$f" in tb/*) continue ;; esac
local linenum=0
while IFS= read -r line; do
linenum=$((linenum + 1))
# CHECK 5: $readmemh / $readmemb in synthesizable code
# (Only valid in simulation blocks — flag if outside `ifdef SIMULATION)
# This is hard to check line-by-line without tracking ifdefs.
# Skip for v1.
# CHECK 6: Unused `include files (informational only)
# Skip for v1.
: # placeholder — prevents empty loop body
done < "$f"
done
# CHECK 5 ($readmemh in synth code) and CHECK 6 (unused includes)
# require multi-line ifdef tracking / cross-file analysis. Not feasible
# with line-by-line regex. Omitted — use Vivado lint instead.
if [[ "$err_count" -gt 0 ]]; then
echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)"
@@ -452,7 +436,7 @@ if [[ "$QUICK" -eq 0 ]]; then
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_16.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v \
usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
@@ -466,11 +450,40 @@ if [[ "$QUICK" -eq 0 ]]; then
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_16.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v \
usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
# USB_MODE=1 (FT2232H production) variants of system tests
run_test "System Top USB_MODE=1 (FT2232H)" \
tb/tb_system_ft2232h_reg.vvp \
-DUSB_MODE_1 \
tb/radar_system_tb.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_16.v fft_engine.v \
usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
run_test "System E2E USB_MODE=1 (FT2232H)" \
tb/tb_system_e2e_ft2232h_reg.vvp \
-DUSB_MODE_1 \
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_16.v fft_engine.v \
usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
else
echo " (skipped receiver golden + system top + E2E — use without --quick)"
SKIP=$((SKIP + 4))
SKIP=$((SKIP + 6))
fi
echo ""
@@ -108,6 +108,9 @@ add_files -fileset constrs_1 -norecurse [file join $project_root "constraints" "
set_property top $top_module [current_fileset]
set_property verilog_define {FFT_XPM_BRAM} [current_fileset]
# Override USB_MODE to 0 (FT601) for 200T premium board.
# The RTL default is USB_MODE=1 (FT2232H, production 50T).
set_property generic {USB_MODE=0} [current_fileset]
# ==============================================================================
# 2. Synthesis
+11 -1
View File
@@ -430,7 +430,13 @@ end
// DUT INSTANTIATION
// ============================================================================
radar_system_top dut (
radar_system_top #(
`ifdef USB_MODE_1
.USB_MODE(1) // FT2232H interface (production 50T board)
`else
.USB_MODE(0) // FT601 interface (200T dev board)
`endif
) dut (
// System Clocks
.clk_100m(clk_100m),
.clk_120m_dac(clk_120m_dac),
@@ -619,7 +625,11 @@ initial begin
// Optional: dump specific signals for debugging
$dumpvars(1, dut.tx_inst);
$dumpvars(1, dut.rx_inst);
`ifdef USB_MODE_1
$dumpvars(1, dut.gen_ft2232h.usb_inst);
`else
$dumpvars(1, dut.gen_ft601.usb_inst);
`endif
end
endmodule
+14 -8
View File
@@ -382,7 +382,13 @@ end
// ============================================================================
// DUT INSTANTIATION
// ============================================================================
radar_system_top dut (
radar_system_top #(
`ifdef USB_MODE_1
.USB_MODE(1) // FT2232H interface (production 50T board)
`else
.USB_MODE(0) // FT601 interface (200T dev board)
`endif
) dut (
.clk_100m(clk_100m),
.clk_120m_dac(clk_120m_dac),
.ft601_clk_in(ft601_clk_in),
@@ -554,10 +560,10 @@ initial begin
do_reset;
// CRITICAL: Configure stream control to range-only BEFORE any chirps
// fire. The USB write FSM blocks on doppler_valid_ft if doppler stream
// is enabled but no Doppler data arrives (needs 32 chirps/frame).
// Without this, the write FSM deadlocks and the read FSM can never
// activate (it requires write FSM == IDLE).
// fire. The USB write FSM gates on pending flags: if doppler stream is
// enabled but no Doppler data arrives (needs 32 chirps/frame), the FSM
// stays in IDLE waiting for doppler_data_pending. With the write FSM
// not in IDLE, the read FSM cannot activate (bus arbitration rule).
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only
// Wait for stream_control CDC to propagate (2-stage sync in ft601_clk)
// Must be long enough that stream_ctrl_sync_1 is updated before any
@@ -778,7 +784,7 @@ initial begin
// Restore defaults for subsequent tests
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (prevents write FSM deadlock)
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (TB lacks 32-chirp doppler data)
bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles
$display("");
@@ -913,7 +919,7 @@ initial begin
// Need to re-send configuration since reset clears all registers
stm32_mixers_enable = 1;
ft601_txe = 0;
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (prevent deadlock)
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (TB lacks doppler data)
#500; // Wait for stream_control CDC
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan
bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing
@@ -947,7 +953,7 @@ initial begin
check(dut.host_stream_control == 3'b000,
"G10.2: All streams disabled (stream_control = 3'b000)");
// G10.3: Re-enable range only (keep range-only to prevent write FSM deadlock)
// G10.3: Re-enable range only (TB uses range-only no doppler processing)
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001
check(dut.host_stream_control == 3'b001,
"G10.3: Range stream re-enabled (stream_control = 3'b001)");
+180 -210
View File
@@ -6,15 +6,11 @@ module tb_usb_data_interface;
localparam CLK_PERIOD = 10.0; // 100 MHz main clock
localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous)
// State definitions (mirror the DUT)
localparam [2:0] S_IDLE = 3'd0,
S_SEND_HEADER = 3'd1,
S_SEND_RANGE = 3'd2,
S_SEND_DOPPLER = 3'd3,
S_SEND_DETECT = 3'd4,
S_SEND_FOOTER = 3'd5,
S_WAIT_ACK = 3'd6,
S_SEND_STATUS = 3'd7; // Gap 2: status readback
// State definitions (mirror the DUT 4-state packed-word FSM)
localparam [3:0] S_IDLE = 4'd0,
S_SEND_DATA_WORD = 4'd1,
S_SEND_STATUS = 4'd2,
S_WAIT_ACK = 4'd3;
// Signals
reg clk;
@@ -219,9 +215,9 @@ module tb_usb_data_interface;
end
endtask
// Helper: wait for DUT to reach a specific state
// Helper: wait for DUT to reach a specific write FSM state
task wait_for_state;
input [2:0] target;
input [3:0] target;
input integer max_cyc;
integer cnt;
begin
@@ -280,7 +276,7 @@ module tb_usb_data_interface;
// 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.
// Call BEFORE assert_range_valid in tests that need doppler/cfar data.
task preload_pending_data;
begin
@(posedge ft601_clk_in);
@@ -354,24 +350,26 @@ module tb_usb_data_interface;
end
endtask
// Drive a complete packet through the FSM by sequentially providing
// range, doppler (4x), and cfar valid pulses.
// Drive a complete data packet through the new 3-word packed FSM.
// Pre-loads pending flags, triggers range_valid, and waits for IDLE.
// With the new FSM, all data is pre-packed in IDLE then sent as 3 words.
task drive_full_packet;
input [31:0] rng;
input [15:0] dr;
input [15:0] di;
input det;
begin
// Pre-load pending flags so FSM enters doppler/cfar states
// Set doppler/cfar captured values via CDC inputs
@(posedge clk);
doppler_real = dr;
doppler_imag = di;
cfar_detection = det;
@(posedge clk);
// Pre-load pending flags so FSM includes doppler/cfar in packet
preload_pending_data;
// Trigger the packet
assert_range_valid(rng);
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(det);
// Wait for complete packet cycle: IDLE SEND_DATA_WORD(×3) WAIT_ACK IDLE
wait_for_state(S_IDLE, 100);
end
endtask
@@ -414,101 +412,138 @@ module tb_usb_data_interface;
"ft601_siwu_n=1 after reset");
//
// TEST GROUP 2: Range data packet
// TEST GROUP 2: Data packet word packing
//
// Use backpressure to freeze the FSM at specific states
// so we can reliably sample outputs.
// New FSM packs 11-byte data into 3 × 32-bit words:
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]}
// Word 1: {range[7:0], dop_re_hi, dop_re_lo, dop_im_hi}
// Word 2: {dop_im_lo, detection, FOOTER, 0x00} BE=1110
//
// The DUT uses range_data_ready (1-cycle delayed range_valid_ft)
// to trigger packing. Doppler/CFAR _cap registers must be
// pre-loaded via hierarchical access because no valid pulse is
// given in this test (we only want to verify packing, not CDC).
//
$display("\n--- Test Group 2: Range Data Packet ---");
$display("\n--- Test Group 2: Data Packet Word Packing ---");
apply_reset;
ft601_txe = 1; // Stall so we can inspect packed words
// Stall at SEND_HEADER so we can verify first range word later
ft601_txe = 1;
// Set known doppler/cfar values on clk-domain inputs
@(posedge clk);
doppler_real = 16'hABCD;
doppler_imag = 16'hEF01;
cfar_detection = 1'b1;
@(posedge clk);
// Pre-load pending flags AND captured-data registers directly.
// No doppler/cfar valid pulses are given, so the CDC capture path
// never fires we must set the _cap registers via hierarchical
// access for the word-packing checks to be meaningful.
preload_pending_data;
@(posedge ft601_clk_in);
uut.doppler_real_cap = 16'hABCD;
uut.doppler_imag_cap = 16'hEF01;
uut.cfar_detection_cap = 1'b1;
@(posedge ft601_clk_in);
assert_range_valid(32'hDEAD_BEEF);
wait_for_state(S_SEND_HEADER, 50);
repeat (2) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_HEADER,
"Stalled in SEND_HEADER (backpressure)");
// Release: FSM drives header then moves to SEND_RANGE_DATA
// FSM should be in SEND_DATA_WORD, stalled on ft601_txe=1
wait_for_state(S_SEND_DATA_WORD, 50);
repeat (2) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_DATA_WORD,
"Stalled in SEND_DATA_WORD (backpressure)");
// Verify pre-packed words
// range_profile = 0xDEAD_BEEF range[31:24]=0xDE, [23:16]=0xAD, [15:8]=0xBE, [7:0]=0xEF
// Word 0: {0xAA, 0xDE, 0xAD, 0xBE}
check(uut.data_pkt_word0 === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
"Word 0: {HEADER=AA, range[31:8]}");
// Word 1: {0xEF, 0xAB, 0xCD, 0xEF}
check(uut.data_pkt_word1 === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
"Word 1: {range[7:0], dop_re, dop_im_hi}");
// Word 2: {0x01, detection_byte, 0x55, 0x00}
// detection_byte bit 7 = frame_start (sample_counter==0 1), bit 0 = cfar=1
// so detection_byte = 8'b1000_0001 = 8'h81
check(uut.data_pkt_word2 === {8'h01, 8'h81, 8'h55, 8'h00},
"Word 2: {dop_im_lo, det=81, FOOTER=55, pad=00}");
check(uut.data_pkt_be2 === 4'b1110,
"Word 2 BE=1110 (3 valid bytes + 1 pad)");
// Release backpressure and verify word 0 appears on bus.
// On the first posedge with !ft601_txe the FSM drives word 0 and
// advances data_word_idx 01 via NBA. After #1 the NBA has
// resolved, so we see idx=1 and ft601_data_out=word0.
ft601_txe = 0;
@(posedge ft601_clk_in); #1;
// Now the FSM registered the header output and will transition
// At the NEXT posedge the state becomes SEND_RANGE_DATA
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_RANGE,
"Entered SEND_RANGE_DATA after header");
// The first range word should be on the data bus (byte_counter=0 just
// drove range_profile_cap, byte_counter incremented to 1)
check(uut.ft601_data_out === 32'hDEAD_BEEF || uut.byte_counter <= 8'd1,
"Range data word 0 driven (range_profile_cap)");
check(uut.ft601_data_out === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
"Word 0 driven on data bus after backpressure release");
check(ft601_wr_n === 1'b0,
"Write strobe active during range data");
"Write strobe active during SEND_DATA_WORD");
check(ft601_be === 4'b1111,
"Byte enable=1111 for range data");
"Byte enable=1111 for word 0");
check(uut.ft601_data_oe === 1'b1,
"Data bus output enabled during SEND_DATA_WORD");
// Wait for all 4 range words to complete
wait_for_state(S_SEND_DOPPLER, 50);
#1;
check(uut.current_state === S_SEND_DOPPLER,
"Advanced to SEND_DOPPLER_DATA after 4 range words");
// Next posedge: FSM drives word 1, advances idx 12.
// After NBA: idx=2, ft601_data_out=word1.
@(posedge ft601_clk_in); #1;
check(uut.data_word_idx === 2'd2,
"data_word_idx advanced past word 1 (now 2)");
check(uut.ft601_data_out === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
"Word 1 driven on data bus");
check(ft601_be === 4'b1111,
"Byte enable=1111 for word 1");
// Next posedge: FSM drives word 2, idx resets 20,
// and current_state transitions to WAIT_ACK.
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_WAIT_ACK,
"Transitioned to WAIT_ACK after 3 data words");
check(uut.ft601_data_out === {8'h01, 8'h81, 8'h55, 8'h00},
"Word 2 driven on data bus");
check(ft601_be === 4'b1110,
"Byte enable=1110 for word 2 (last byte is pad)");
// Then back to IDLE
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_IDLE,
"Returned to IDLE after WAIT_ACK");
//
// TEST GROUP 3: Header verification (stall to observe)
// TEST GROUP 3: Header and footer verification
//
$display("\n--- Test Group 3: Header Verification ---");
$display("\n--- Test Group 3: Header and Footer Verification ---");
apply_reset;
ft601_txe = 1; // Stall at SEND_HEADER
ft601_txe = 1; // Stall to inspect
@(posedge clk);
range_profile = 32'hCAFE_BABE;
range_valid = 1;
repeat (4) @(posedge ft601_clk_in);
doppler_real = 16'h0000;
doppler_imag = 16'h0000;
cfar_detection = 1'b0;
@(posedge clk);
range_valid = 0;
repeat (3) @(posedge ft601_clk_in);
preload_pending_data;
assert_range_valid(32'hCAFE_BABE);
wait_for_state(S_SEND_HEADER, 50);
wait_for_state(S_SEND_DATA_WORD, 50);
repeat (2) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_HEADER,
"Stalled in SEND_HEADER with backpressure");
// Release backpressure - header will be latched at next posedge
ft601_txe = 0;
@(posedge ft601_clk_in); #1;
check(uut.ft601_data_out[7:0] === 8'hAA,
"Header byte 0xAA on data bus");
check(ft601_be === 4'b0001,
"Byte enable=0001 for header (lower byte only)");
check(ft601_wr_n === 1'b0,
"Write strobe active during header");
check(uut.ft601_data_oe === 1'b1,
"Data bus output enabled during header");
// Header is in byte 3 (MSB) of word 0
check(uut.data_pkt_word0[31:24] === 8'hAA,
"Header byte 0xAA in word 0 MSB");
// Footer is in byte 1 (bits [15:8]) of word 2
check(uut.data_pkt_word2[15:8] === 8'h55,
"Footer byte 0x55 in word 2");
//
// TEST GROUP 4: Doppler data verification
// TEST GROUP 4: Doppler data capture verification
//
$display("\n--- Test Group 4: Doppler Data Verification ---");
$display("\n--- Test Group 4: Doppler Data Capture ---");
apply_reset;
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);
wait_for_state(S_SEND_DOPPLER, 100);
#1;
check(uut.current_state === S_SEND_DOPPLER,
"Reached SEND_DOPPLER_DATA");
// Provide doppler data via valid pulse (updates captured values)
@(posedge clk);
doppler_real = 16'hAAAA;
@@ -524,110 +559,70 @@ module tb_usb_data_interface;
check(uut.doppler_imag_cap === 16'h5555,
"doppler_imag captured correctly");
// The FSM has doppler_data_pending set and sends 4 bytes, then
// transitions past SEND_DETECT (cfar_data_pending=0) to IDLE.
// Drive a packet with pending doppler + cfar (both needed for gating
// since all streams are enabled after reset/apply_reset).
preload_pending_data;
assert_range_valid(32'h0000_0001);
wait_for_state(S_IDLE, 100);
#1;
check(uut.current_state === S_IDLE,
"Doppler done, packet completed");
"Packet completed with doppler data");
check(uut.doppler_data_pending === 1'b0,
"doppler_data_pending cleared after packet");
//
// TEST GROUP 5: CFAR detection data
//
$display("\n--- Test Group 5: CFAR Detection Data ---");
// Start a new packet with both doppler and cfar pending to verify
// 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");
// Verify the full packet completed with cfar data consumed
"cfar_data_pending cleared after packet");
check(uut.current_state === S_IDLE &&
uut.doppler_data_pending === 1'b0 &&
uut.cfar_data_pending === 1'b0,
"CFAR detection sent, FSM advanced past SEND_DETECTION_DATA");
"CFAR detection sent, all pending flags cleared");
//
// TEST GROUP 6: Footer check
//
// Strategy: drive packet with ft601_txe=0 all the way through.
// The SEND_FOOTER state is only active for 1 cycle, but we can
// poll the state machine at each ft601_clk_in edge to observe
// it. We use a monitor-style approach: run the packet and
// capture what ft601_data_out contains when we see SEND_FOOTER.
// TEST GROUP 6: Footer retained after packet
//
$display("\n--- Test Group 6: Footer Check ---");
$display("\n--- Test Group 6: Footer Retention ---");
apply_reset;
ft601_txe = 0;
// Drive packet through range data
@(posedge clk);
cfar_detection = 1'b1;
@(posedge clk);
preload_pending_data;
assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_DOPPLER, 100);
// Feed doppler data (need 4 pulses)
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
wait_for_state(S_SEND_DETECT, 100);
// Feed cfar data, but keep ft601_txe=0 so it flows through
pulse_cfar_once(1'b1);
// Now the FSM should pass through SEND_FOOTER quickly.
// Use wait_for_state to reach SEND_FOOTER, or it may already
// be at WAIT_ACK/IDLE. Let's catch WAIT_ACK or IDLE.
// The footer values are latched into registers, so we can
// verify them even after the state transitions.
// Key verification: the FOOTER constant (0x55) must have been
// driven. We check this by looking at the constant definition.
// Since we can't easily freeze the FSM at SEND_FOOTER without
// also stalling SEND_DETECTION_DATA (both check ft601_txe),
// we verify the footer indirectly:
// 1. The packet completed (reached IDLE/WAIT_ACK)
// 2. ft601_data_out last held 0x55 during SEND_FOOTER
wait_for_state(S_IDLE, 100);
#1;
// If we reached IDLE, the full sequence ran including footer
check(uut.current_state === S_IDLE,
"Full packet incl. footer completed, back in IDLE");
// The registered ft601_data_out should still hold 0x55 from
// SEND_FOOTER (WAIT_ACK and IDLE don't overwrite ft601_data_out).
// Actually, looking at the DUT: WAIT_ACK only sets wr_n=1 and
// data_oe=0, it doesn't change ft601_data_out. So it retains 0x55.
check(uut.ft601_data_out[7:0] === 8'h55,
"ft601_data_out retains footer 0x55 after packet");
// The last word driven was word 2 which contains footer 0x55.
// WAIT_ACK and IDLE don't overwrite ft601_data_out, so it retains
// the last driven value.
check(uut.ft601_data_out[15:8] === 8'h55,
"ft601_data_out retains footer 0x55 in word 2 position");
// Verify WAIT_ACK behavior by doing another packet and catching it
// Verify WAIT_ACK IDLE transition
apply_reset;
ft601_txe = 0;
preload_pending_data;
assert_range_valid(32'h1234_5678);
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b0);
// WAIT_ACK lasts exactly 1 ft601_clk_in cycle then goes IDLE.
// Poll for IDLE (which means WAIT_ACK already happened).
wait_for_state(S_IDLE, 100);
#1;
check(uut.current_state === S_IDLE,
"Returned to IDLE after WAIT_ACK");
check(ft601_wr_n === 1'b1,
"ft601_wr_n deasserted in IDLE (was deasserted in WAIT_ACK)");
"ft601_wr_n deasserted in IDLE");
check(uut.ft601_data_oe === 1'b0,
"Data bus released in IDLE (was released in WAIT_ACK)");
"Data bus released in IDLE");
//
// TEST GROUP 7: Full packet sequence (end-to-end)
@@ -646,23 +641,24 @@ module tb_usb_data_interface;
//
$display("\n--- Test Group 8: FIFO Backpressure ---");
apply_reset;
ft601_txe = 1;
ft601_txe = 1; // FIFO full stall
preload_pending_data;
assert_range_valid(32'hBBBB_CCCC);
wait_for_state(S_SEND_HEADER, 50);
wait_for_state(S_SEND_DATA_WORD, 50);
repeat (10) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_HEADER,
"Stalled in SEND_HEADER when ft601_txe=1 (FIFO full)");
check(uut.current_state === S_SEND_DATA_WORD,
"Stalled in SEND_DATA_WORD when ft601_txe=1 (FIFO full)");
check(ft601_wr_n === 1'b1,
"ft601_wr_n not asserted during backpressure stall");
ft601_txe = 0;
repeat (2) @(posedge ft601_clk_in); #1;
repeat (6) @(posedge ft601_clk_in); #1;
check(uut.current_state !== S_SEND_HEADER,
"Resumed from SEND_HEADER after backpressure released");
check(uut.current_state === S_IDLE || uut.current_state === S_WAIT_ACK,
"Resumed and completed after backpressure released");
//
// TEST GROUP 9: Clock divider
@@ -705,13 +701,6 @@ module tb_usb_data_interface;
ft601_txe = 0;
preload_pending_data;
assert_range_valid(32'h1111_2222);
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b0);
wait_for_state(S_WAIT_ACK, 50);
#1;
@@ -805,7 +794,7 @@ module tb_usb_data_interface;
// Start a write packet
preload_pending_data;
assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_HEADER, 50);
wait_for_state(S_SEND_DATA_WORD, 50);
@(posedge ft601_clk_in); #1;
// While write FSM is active, assert RXF=0 (host has data)
@@ -818,13 +807,6 @@ module tb_usb_data_interface;
// Deassert RXF, complete the write packet
ft601_rxf = 1;
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b1);
wait_for_state(S_IDLE, 100);
@(posedge ft601_clk_in); #1;
@@ -841,32 +823,42 @@ module tb_usb_data_interface;
//
// TEST GROUP 15: Stream Control Gating (Gap 2)
// Verify that disabling individual streams causes the write
// FSM to skip those data phases.
// FSM to zero those fields in the packed words.
//
$display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---");
// 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only)
apply_reset;
ft601_txe = 0;
ft601_txe = 1; // Stall to inspect packed words
stream_control = 3'b101; // range + cfar, no doppler
// Wait for CDC propagation (2-stage sync)
repeat (6) @(posedge ft601_clk_in);
// 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);
// FSM: IDLE -> SEND_HEADER -> SEND_RANGE (doppler disabled) -> SEND_DETECT -> FOOTER
// The FSM races through SEND_DETECT in 1 cycle (cfar_data_pending is consumed).
// Verify the packet completed correctly (doppler was skipped).
wait_for_state(S_IDLE, 200);
#1;
// 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)");
@(posedge clk);
doppler_real = 16'hAAAA;
doppler_imag = 16'hBBBB;
cfar_detection = 1'b1;
@(posedge clk);
preload_cfar_pending;
assert_range_valid(32'hAA11_BB22);
wait_for_state(S_SEND_DATA_WORD, 200);
repeat (2) @(posedge ft601_clk_in); #1;
// With doppler disabled, doppler fields in words 1 and 2 should be zero
// Word 1: {range[7:0], 0x00, 0x00, 0x00} (doppler zeroed)
check(uut.data_pkt_word1[23:0] === 24'h000000,
"Stream gate: doppler bytes zeroed in word 1 when disabled");
// Word 2 byte 3 (dop_im_lo) should also be zero
check(uut.data_pkt_word2[31:24] === 8'h00,
"Stream gate: dop_im_lo zeroed in word 2 when disabled");
// Let it complete
ft601_txe = 0;
wait_for_state(S_IDLE, 100);
#1;
check(uut.current_state === S_IDLE,
"Stream gate: packet completed without doppler");
@@ -951,28 +943,6 @@ module tb_usb_data_interface;
"Status readback: returned to IDLE after 8-word response");
// Verify the status snapshot was captured correctly.
// status_words[0] = {0xFF, 3'b000, mode[1:0], 5'b0, stream_ctrl[2:0], cfar_threshold[15:0]}
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
// = 0xFF_09_05_ABCD... let's compute:
// Byte 3: 0xFF = 8'hFF
// Byte 2: {3'b000, 2'b01} = 5'b00001 + 3 high bits of next field...
// Actually the packing is: {8'hFF, 3'b000, status_radar_mode[1:0], 5'b00000, status_stream_ctrl[2:0], status_cfar_threshold[15:0]}
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
// = 8'hFF, 5'b00001, 8'b00000101, 16'hABCD
// = FF_09_05_ABCD? Let me compute carefully:
// Bits [31:24] = 8'hFF = 0xFF
// Bits [23:21] = 3'b000
// Bits [20:19] = 2'b01 (mode)
// Bits [18:14] = 5'b00000
// Bits [13:11] = 3'b101 (stream_ctrl)
// Bits [10:0] = ... wait, cfar_threshold is 16 bits [15:0]
// Total bits = 8+3+2+5+3+16 = 37 bits won't fit in 32!
// Re-reading the RTL: the packing at line 241 is:
// {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold}
// = 8 + 3 + 2 + 5 + 3 + 16 = 37 bits
// This would be truncated to 32 bits. Let me re-read the actual RTL to check.
// For now, just verify status_words[1] (word index 1 in the packet = idx 2 in FSM)
// status_words[1] = {status_long_chirp, status_long_listen} = {16'd3000, 16'd13700}
check(uut.status_words[1] === {16'd3000, 16'd13700},
"Status readback: word 1 = {long_chirp, long_listen}");
check(uut.status_words[2] === {16'd17540, 16'd50},
+186 -129
View File
@@ -1,3 +1,17 @@
/**
* usb_data_interface.v
*
* FT601 USB 3.0 SuperSpeed FIFO Interface (32-bit bus, 100 MHz ft601_clk).
* Used on the 200T premium dev board. Production 50T board uses
* usb_data_interface_ft2232h.v (FT2232H, 8-bit, 60 MHz) instead.
*
* USB disconnect recovery:
* A clock-activity watchdog in the clk domain detects when ft601_clk_in
* stops (USB cable unplugged). After ~0.65 ms of silence (65536 system
* clocks) it asserts ft601_clk_lost, which is OR'd into the FT-domain
* reset so FSMs and FIFOs return to a clean state. When ft601_clk_in
* resumes, a 2-stage reset synchronizer deasserts the reset cleanly.
*/
module usb_data_interface (
input wire clk, // Main clock (100MHz recommended)
input wire reset_n,
@@ -15,13 +29,18 @@ module usb_data_interface (
// FT601 Interface (Slave FIFO mode)
// Data bus
inout wire [31:0] ft601_data, // 32-bit bidirectional data bus
output reg [3:0] ft601_be, // Byte enable (4 lanes for 32-bit mode)
output reg [3:0] ft601_be, // Byte enable (active-HIGH per FT601 datasheet / AN_378)
// Control signals
output reg ft601_txe_n, // Transmit enable (active low)
output reg ft601_rxf_n, // Receive enable (active low)
input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write)
input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read)
// VESTIGIAL OUTPUTS — kept for 200T board port compatibility.
// On the 200T, these are constrained to physical pins G21 (TXE) and
// G22 (RXF) in xc7a200t_fbg484.xdc. Removing them from the RTL would
// break the 200T build. They are reset to 1 and never driven; the
// actual FT601 flow-control inputs are ft601_txe and ft601_rxf below.
output reg ft601_txe_n, // VESTIGIAL: unused output, always 1
output reg ft601_rxf_n, // VESTIGIAL: unused output, always 1
input wire ft601_txe, // TXE: Transmit FIFO Not Full (active-low: 0 = space available)
input wire ft601_rxf, // RXF: Receive FIFO Not Empty (active-low: 0 = data available)
output reg ft601_wr_n, // Write strobe (active low)
output reg ft601_rd_n, // Read strobe (active low)
output reg ft601_oe_n, // Output enable (active low)
@@ -97,21 +116,26 @@ localparam FT601_BURST_SIZE = 512; // Max burst size in bytes
// ============================================================================
// WRITE FSM State definitions (Verilog-2001 compatible)
// ============================================================================
localparam [2:0] IDLE = 3'd0,
SEND_HEADER = 3'd1,
SEND_RANGE_DATA = 3'd2,
SEND_DOPPLER_DATA = 3'd3,
SEND_DETECTION_DATA = 3'd4,
SEND_FOOTER = 3'd5,
WAIT_ACK = 3'd6,
SEND_STATUS = 3'd7; // Gap 2: status readback
// Rewritten: data packet is now 3 x 32-bit writes (11 payload bytes + 1 pad).
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} BE=1111
// Word 1: {range[7:0], doppler_real[15:8], doppler_real[7:0], doppler_imag[15:8]} BE=1111
// Word 2: {doppler_imag[7:0], detection, FOOTER, 8'h00} BE=1110
localparam [3:0] IDLE = 4'd0,
SEND_DATA_WORD = 4'd1,
SEND_STATUS = 4'd2,
WAIT_ACK = 4'd3;
reg [2:0] current_state;
reg [7:0] byte_counter;
reg [31:0] data_buffer;
reg [3:0] current_state;
reg [1:0] data_word_idx; // 0..2 for 3-word data packet
reg [31:0] ft601_data_out;
reg ft601_data_oe; // Output enable for bidirectional data bus
// Pre-packed data words (registered snapshot of CDC'd data)
reg [31:0] data_pkt_word0;
reg [31:0] data_pkt_word1;
reg [31:0] data_pkt_word2;
reg [3:0] data_pkt_be2; // BE for last word (BE=1110 since byte 3 is pad)
// ============================================================================
// READ FSM State definitions (Gap 4: USB Read Path)
// ============================================================================
@@ -184,6 +208,67 @@ always @(posedge clk or negedge reset_n) begin
end
end
// ============================================================================
// CLOCK-ACTIVITY WATCHDOG (clk domain)
// ============================================================================
// Detects when ft601_clk_in stops (USB cable unplugged). A toggle register
// in the ft601_clk domain flips every edge. The clk domain synchronizes it
// and checks for transitions. If no transition is seen for 2^16 = 65536
// clk cycles (~0.65 ms at 100 MHz), ft601_clk_lost asserts.
// Toggle register: flips every ft601_clk edge (ft601_clk domain)
reg ft601_heartbeat;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n)
ft601_heartbeat <= 1'b0;
else
ft601_heartbeat <= ~ft601_heartbeat;
end
// Synchronize heartbeat into clk domain (2-stage)
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_hb_sync;
reg ft601_hb_prev;
reg [15:0] ft601_clk_timeout;
reg ft601_clk_lost;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ft601_hb_sync <= 2'b00;
ft601_hb_prev <= 1'b0;
ft601_clk_timeout <= 16'd0;
ft601_clk_lost <= 1'b0;
end else begin
ft601_hb_sync <= {ft601_hb_sync[0], ft601_heartbeat};
ft601_hb_prev <= ft601_hb_sync[1];
if (ft601_hb_sync[1] != ft601_hb_prev) begin
// ft601_clk is alive reset counter, clear lost flag
ft601_clk_timeout <= 16'd0;
ft601_clk_lost <= 1'b0;
end else if (!ft601_clk_lost) begin
if (ft601_clk_timeout == 16'hFFFF)
ft601_clk_lost <= 1'b1;
else
ft601_clk_timeout <= ft601_clk_timeout + 16'd1;
end
end
end
// Effective FT601-domain reset: asserted by global reset OR clock loss.
// Deassertion synchronized to ft601_clk via 2-stage sync to avoid
// metastability on the recovery edge.
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_reset_sync;
wire ft601_reset_raw_n = ft601_reset_n & ~ft601_clk_lost;
always @(posedge ft601_clk_in or negedge ft601_reset_raw_n) begin
if (!ft601_reset_raw_n)
ft601_reset_sync <= 2'b00;
else
ft601_reset_sync <= {ft601_reset_sync[0], 1'b1};
end
wire ft601_effective_reset_n = ft601_reset_sync[1];
// FT601-domain captured data (sampled from holding regs on sync'd edge)
reg [31:0] range_profile_cap;
reg [15:0] doppler_real_cap;
@@ -197,6 +282,18 @@ reg cfar_detection_cap;
reg doppler_data_pending;
reg cfar_data_pending;
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
// edge that range_profile_cap is captured (non-blocking). If the FSM
// reads range_profile_cap on that same edge it sees the STALE value.
// Delaying the trigger by one cycle guarantees the capture register has
// settled before the FSM packs the data words.
reg range_data_ready;
// Frame sync: sample counter (ft601_clk domain, wraps at NUM_CELLS)
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
reg [11:0] sample_counter;
// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in)
// stream_control changes infrequently (only on host USB command), so
// per-bit 2-stage synchronizers are sufficient. No Gray coding needed
@@ -228,8 +325,8 @@ wire range_valid_ft;
wire doppler_valid_ft;
wire cfar_valid_ft;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n) begin
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
if (!ft601_effective_reset_n) begin
range_valid_sync <= 2'b00;
doppler_valid_sync <= 2'b00;
cfar_valid_sync <= 2'b00;
@@ -240,6 +337,7 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
doppler_real_cap <= 16'd0;
doppler_imag_cap <= 16'd0;
cfar_detection_cap <= 1'b0;
range_data_ready <= 1'b0;
// Fix #5: Default to range-only on reset (prevents write FSM deadlock)
stream_ctrl_sync_0 <= 3'b001;
stream_ctrl_sync_1 <= 3'b001;
@@ -302,6 +400,10 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin
cfar_detection_cap <= cfar_detection_hold;
end
// 1-cycle delayed trigger: ensures range_profile_cap has settled
// before the FSM reads it for word packing.
range_data_ready <= range_valid_ft;
end
end
@@ -314,11 +416,11 @@ assign cfar_valid_ft = cfar_valid_sync[1] && !cfar_valid_sync_d;
// FT601 data bus direction control
assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n) begin
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
if (!ft601_effective_reset_n) begin
current_state <= IDLE;
read_state <= RD_IDLE;
byte_counter <= 0;
data_word_idx <= 2'd0;
ft601_data_out <= 0;
ft601_data_oe <= 0;
ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode
@@ -336,6 +438,11 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
cmd_value <= 16'd0;
doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0;
data_pkt_word0 <= 32'd0;
data_pkt_word1 <= 32'd0;
data_pkt_word2 <= 32'd0;
data_pkt_be2 <= 4'b1110;
sample_counter <= 12'd0;
// NOTE: ft601_clk_out is driven by the clk-domain always block below.
// Do NOT assign it here (ft601_clk_in domain) causes multi-driven net.
end else begin
@@ -424,124 +531,66 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
current_state <= SEND_STATUS;
status_word_idx <= 3'd0;
end
// Trigger write FSM on range_valid edge (primary data source).
// Doppler/cfar data_pending flags are checked inside
// SEND_DOPPLER_DATA and SEND_DETECTION_DATA to skip or send.
// 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
// Trigger on range_data_ready (1 cycle after range_valid_ft)
// so that range_profile_cap has settled from the CDC block.
// Gate on pending flags: only send when all enabled
// streams have fresh data (avoids stale doppler/CFAR)
else if (range_data_ready && stream_range_en
&& (!stream_doppler_en || doppler_data_pending)
&& (!stream_cfar_en || cfar_data_pending)) begin
// Don't start write if a read is about to begin
if (ft601_rxf) begin // rxf=1 means no host data pending
current_state <= SEND_HEADER;
byte_counter <= 0;
// Pack 11-byte data packet into 3 x 32-bit words
// Doppler fields zeroed when stream disabled
// CFAR field zeroed when stream disabled
data_pkt_word0 <= {HEADER,
range_profile_cap[31:24],
range_profile_cap[23:16],
range_profile_cap[15:8]};
data_pkt_word1 <= {range_profile_cap[7:0],
stream_doppler_en ? doppler_real_cap[15:8] : 8'd0,
stream_doppler_en ? doppler_real_cap[7:0] : 8'd0,
stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0};
data_pkt_word2 <= {stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0,
stream_cfar_en
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
: {(sample_counter == 12'd0), 7'd0},
FOOTER,
8'h00}; // pad byte
data_pkt_be2 <= 4'b1110; // 3 valid bytes + 1 pad
data_word_idx <= 2'd0;
current_state <= SEND_DATA_WORD;
end
end
end
SEND_HEADER: begin
if (!ft601_txe) begin // FT601 TX FIFO not empty
ft601_data_oe <= 1;
ft601_data_out <= {24'b0, HEADER};
ft601_be <= 4'b0001; // Only lower byte valid
ft601_wr_n <= 0; // Assert write strobe
// Gap 2: skip to first enabled stream
if (stream_range_en)
current_state <= SEND_RANGE_DATA;
else if (stream_doppler_en)
current_state <= SEND_DOPPLER_DATA;
else if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER; // No streams — send footer only
end
end
SEND_RANGE_DATA: begin
SEND_DATA_WORD: begin
if (!ft601_txe) begin
ft601_data_oe <= 1;
ft601_be <= 4'b1111; // All bytes valid for 32-bit word
case (byte_counter)
0: ft601_data_out <= range_profile_cap;
1: ft601_data_out <= {range_profile_cap[23:0], 8'h00};
2: ft601_data_out <= {range_profile_cap[15:0], 16'h0000};
3: ft601_data_out <= {range_profile_cap[7:0], 24'h000000};
ft601_wr_n <= 0;
case (data_word_idx)
2'd0: begin
ft601_data_out <= data_pkt_word0;
ft601_be <= 4'b1111;
end
2'd1: begin
ft601_data_out <= data_pkt_word1;
ft601_be <= 4'b1111;
end
2'd2: begin
ft601_data_out <= data_pkt_word2;
ft601_be <= data_pkt_be2;
end
default: ;
endcase
ft601_wr_n <= 0;
if (byte_counter == 3) begin
byte_counter <= 0;
// Gap 2: skip disabled streams
if (stream_doppler_en)
current_state <= SEND_DOPPLER_DATA;
else if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
if (data_word_idx == 2'd2) begin
data_word_idx <= 2'd0;
current_state <= WAIT_ACK;
end else begin
byte_counter <= byte_counter + 1;
data_word_idx <= data_word_idx + 2'd1;
end
end
end
SEND_DOPPLER_DATA: begin
if (!ft601_txe && doppler_data_pending) begin
ft601_data_oe <= 1;
ft601_be <= 4'b1111;
case (byte_counter)
0: ft601_data_out <= {doppler_real_cap, doppler_imag_cap};
1: ft601_data_out <= {doppler_imag_cap, doppler_real_cap[15:8], 8'h00};
2: ft601_data_out <= {doppler_real_cap[7:0], doppler_imag_cap[15:8], 16'h0000};
3: ft601_data_out <= {doppler_imag_cap[7:0], 24'h000000};
endcase
ft601_wr_n <= 0;
if (byte_counter == 3) begin
byte_counter <= 0;
doppler_data_pending <= 1'b0;
if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
end else begin
byte_counter <= byte_counter + 1;
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
SEND_DETECTION_DATA: begin
if (!ft601_txe && cfar_data_pending) begin
ft601_data_oe <= 1;
ft601_be <= 4'b0001;
ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap};
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;
end
end
SEND_FOOTER: begin
if (!ft601_txe) begin
ft601_data_oe <= 1;
ft601_be <= 4'b0001;
ft601_data_out <= {24'b0, FOOTER};
ft601_wr_n <= 0;
current_state <= WAIT_ACK;
end
end
// Gap 2: Status readback send 6 x 32-bit status words
// Format: HEADER, status_words[0..5], FOOTER
@@ -581,6 +630,14 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
WAIT_ACK: begin
ft601_wr_n <= 1;
ft601_data_oe <= 0; // Release data bus
// Clear pending flags data consumed
doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0;
// Advance frame sync counter
if (sample_counter == NUM_CELLS - 12'd1)
sample_counter <= 12'd0;
else
sample_counter <= sample_counter + 12'd1;
current_state <= IDLE;
end
endcase
@@ -613,8 +670,8 @@ ODDR #(
`else
// Simulation: behavioral clock forwarding
reg ft601_clk_out_sim;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n)
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
if (!ft601_effective_reset_n)
ft601_clk_out_sim <= 1'b0;
else
ft601_clk_out_sim <= 1'b1;
+131 -19
View File
@@ -36,6 +36,13 @@
* Clock domains:
* clk = 100 MHz system clock (radar data domain)
* ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain)
*
* USB disconnect recovery:
* A clock-activity watchdog in the clk domain detects when ft_clk stops
* (USB cable unplugged). After ~0.65 ms of silence (65536 system clocks)
* it asserts ft_clk_lost, which is OR'd into the FT-domain reset so
* FSMs and FIFOs return to a clean state. When ft_clk resumes, a 2-stage
* reset synchronizer deasserts the reset cleanly in the ft_clk domain.
*/
module usb_data_interface_ft2232h (
@@ -59,7 +66,9 @@ module usb_data_interface_ft2232h (
output reg ft_rd_n, // Read strobe (active low)
output reg ft_wr_n, // Write strobe (active low)
output reg ft_oe_n, // Output enable (active low) bus direction
output reg ft_siwu, // Send Immediate / WakeUp
output reg ft_siwu, // Send Immediate / WakeUp UNUSED: held low.
// SIWU could flush the TX FIFO for lower latency
// but is not needed at current data rates. Deferred.
// Clock from FT2232H (directly used no ODDR forwarding needed)
input wire ft_clk, // 60 MHz from FT2232H CLKOUT
@@ -134,6 +143,7 @@ localparam [2:0] RD_IDLE = 3'd0,
reg [2:0] rd_state;
reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word
reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command
reg rd_cmd_complete; // Set when all 4 bytes received (distinguishes from abort)
// ============================================================================
// DATA BUS DIRECTION CONTROL
@@ -192,6 +202,70 @@ always @(posedge clk or negedge reset_n) begin
end
end
// ============================================================================
// CLOCK-ACTIVITY WATCHDOG (clk domain)
// ============================================================================
// Detects when ft_clk stops (USB cable unplugged). A toggle register in the
// ft_clk domain flips every ft_clk edge. The clk domain synchronizes it and
// checks for transitions. If no transition is seen for 2^16 = 65536 clk
// cycles (~0.65 ms at 100 MHz), ft_clk_lost asserts.
//
// ft_clk_lost feeds into the effective reset for the ft_clk domain so that
// FSMs and capture registers return to a clean state automatically.
// Toggle register: flips every ft_clk edge (ft_clk domain)
reg ft_heartbeat;
always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_reset_n)
ft_heartbeat <= 1'b0;
else
ft_heartbeat <= ~ft_heartbeat;
end
// Synchronize heartbeat into clk domain (2-stage)
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_hb_sync;
reg ft_hb_prev;
reg [15:0] ft_clk_timeout;
reg ft_clk_lost;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ft_hb_sync <= 2'b00;
ft_hb_prev <= 1'b0;
ft_clk_timeout <= 16'd0;
ft_clk_lost <= 1'b0;
end else begin
ft_hb_sync <= {ft_hb_sync[0], ft_heartbeat};
ft_hb_prev <= ft_hb_sync[1];
if (ft_hb_sync[1] != ft_hb_prev) begin
// ft_clk is alive reset counter, clear lost flag
ft_clk_timeout <= 16'd0;
ft_clk_lost <= 1'b0;
end else if (!ft_clk_lost) begin
if (ft_clk_timeout == 16'hFFFF)
ft_clk_lost <= 1'b1;
else
ft_clk_timeout <= ft_clk_timeout + 16'd1;
end
end
end
// Effective FT-domain reset: asserted by global reset OR clock loss.
// Deassertion synchronized to ft_clk via 2-stage sync to avoid
// metastability on the recovery edge.
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_reset_sync;
wire ft_reset_raw_n = ft_reset_n & ~ft_clk_lost;
always @(posedge ft_clk or negedge ft_reset_raw_n) begin
if (!ft_reset_raw_n)
ft_reset_sync <= 2'b00;
else
ft_reset_sync <= {ft_reset_sync[0], 1'b1};
end
wire ft_effective_reset_n = ft_reset_sync[1];
// --- 3-stage synchronizers (ft_clk domain) ---
// 3 stages for better MTBF at 60 MHz
@@ -228,12 +302,25 @@ reg cfar_detection_cap;
reg doppler_data_pending;
reg cfar_data_pending;
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
// edge that range_profile_cap is captured (non-blocking). If the FSM
// reads range_profile_cap on that same edge it sees the STALE value.
// Delaying the trigger by one cycle guarantees the capture register has
// settled before the byte mux reads it.
reg range_data_ready;
// Frame sync: sample counter (ft_clk domain, wraps at NUM_CELLS)
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
// This allows the Python host to resynchronize without a protocol change.
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
reg [11:0] sample_counter;
// Status snapshot (ft_clk domain)
reg [31:0] status_words [0:5];
integer si; // status_words loop index
always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_reset_n) begin
always @(posedge ft_clk or negedge ft_effective_reset_n) begin
if (!ft_effective_reset_n) begin
range_toggle_sync <= 3'b000;
doppler_toggle_sync <= 3'b000;
cfar_toggle_sync <= 3'b000;
@@ -246,6 +333,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
doppler_real_cap <= 16'd0;
doppler_imag_cap <= 16'd0;
cfar_detection_cap <= 1'b0;
range_data_ready <= 1'b0;
// Default to range-only on reset (prevents write FSM deadlock)
stream_ctrl_sync_0 <= 3'b001;
stream_ctrl_sync_1 <= 3'b001;
@@ -279,6 +367,10 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
if (cfar_valid_ft)
cfar_detection_cap <= cfar_detection_hold;
// 1-cycle delayed trigger: ensures range_profile_cap has settled
// before the FSM reads it via the byte mux.
range_data_ready <= range_valid_ft;
// Status snapshot on request
if (status_req_ft) begin
// Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
@@ -315,11 +407,16 @@ always @(*) begin
5'd2: data_pkt_byte = range_profile_cap[23:16];
5'd3: data_pkt_byte = range_profile_cap[15:8];
5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB
5'd5: data_pkt_byte = doppler_real_cap[15:8]; // doppler_real MSB
5'd6: data_pkt_byte = doppler_real_cap[7:0]; // doppler_real LSB
5'd7: data_pkt_byte = doppler_imag_cap[15:8]; // doppler_imag MSB
5'd8: data_pkt_byte = doppler_imag_cap[7:0]; // doppler_imag LSB
5'd9: data_pkt_byte = {7'b0, cfar_detection_cap}; // detection
// Doppler fields: zero when stream_doppler_en is off
5'd5: data_pkt_byte = stream_doppler_en ? doppler_real_cap[15:8] : 8'd0;
5'd6: data_pkt_byte = stream_doppler_en ? doppler_real_cap[7:0] : 8'd0;
5'd7: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0;
5'd8: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0;
// Detection field: zero when stream_cfar_en is off
// Bit 7 = frame_start flag (sample_counter == 0), bit 0 = cfar_detection
5'd9: data_pkt_byte = stream_cfar_en
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
: {(sample_counter == 12'd0), 7'd0};
5'd10: data_pkt_byte = FOOTER;
default: data_pkt_byte = 8'h00;
endcase
@@ -376,12 +473,13 @@ end
// Write FSM and Read FSM share the bus. Write FSM operates when Read FSM
// is idle. Read FSM takes priority when host has data available.
always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_reset_n) begin
always @(posedge ft_clk or negedge ft_effective_reset_n) begin
if (!ft_effective_reset_n) begin
wr_state <= WR_IDLE;
wr_byte_idx <= 5'd0;
rd_state <= RD_IDLE;
rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b0;
rd_shift_reg <= 32'd0;
ft_data_out <= 8'd0;
ft_data_oe <= 1'b0;
@@ -396,6 +494,7 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
cmd_value <= 16'd0;
doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0;
sample_counter <= 12'd0;
end else begin
// Default: clear one-shot signals
cmd_valid <= 1'b0;
@@ -437,17 +536,19 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
rd_shift_reg <= {rd_shift_reg[23:0], ft_data};
if (rd_byte_cnt == 2'd3) begin
// All 4 bytes received
ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0;
rd_state <= RD_DEASSERT;
ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b1;
rd_state <= RD_DEASSERT;
end else begin
rd_byte_cnt <= rd_byte_cnt + 2'd1;
// Keep reading if more data available
if (ft_rxf_n) begin
// Host ran out of data mid-command abort
ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0;
rd_state <= RD_DEASSERT;
ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b0;
rd_state <= RD_DEASSERT;
end
end
end
@@ -456,7 +557,8 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
// Deassert OE (1 cycle after RD deasserted)
ft_oe_n <= 1'b1;
// Only process if we received a full 4-byte command
if (rd_byte_cnt == 2'd0) begin
if (rd_cmd_complete) begin
rd_cmd_complete <= 1'b0;
rd_state <= RD_PROCESS;
end else begin
// Incomplete command discard
@@ -491,8 +593,13 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
wr_state <= WR_STATUS_SEND;
wr_byte_idx <= 5'd0;
end
// Trigger on range_valid edge (primary data trigger)
else if (range_valid_ft && stream_range_en) begin
// Trigger on range_data_ready (1 cycle after range_valid_ft)
// so that range_profile_cap has settled from the CDC block.
// Gate on pending flags: only send when all enabled
// streams have fresh data (avoids stale doppler/CFAR)
else if (range_data_ready && stream_range_en
&& (!stream_doppler_en || doppler_data_pending)
&& (!stream_cfar_en || cfar_data_pending)) begin
if (ft_rxf_n) begin // No host read pending
wr_state <= WR_DATA_SEND;
wr_byte_idx <= 5'd0;
@@ -538,6 +645,11 @@ always @(posedge ft_clk or negedge ft_reset_n) begin
// Clear pending flags data consumed
doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0;
// Advance frame sync counter
if (sample_counter == NUM_CELLS - 12'd1)
sample_counter <= 12'd0;
else
sample_counter <= sample_counter + 12'd1;
wr_state <= WR_IDLE;
end