Wire a per-frame MCU→FPGA "beam pattern ready" handshake so the chirp scheduler can stall between 48-chirp frames until the MCU finishes writing the next ADAR1000 pattern. The legacy unused stm32_new_chirp input on PD8 is repurposed as stm32_beam_ready; chirp_scheduler.v gets a new S_BEAM_WAIT state entered after each frame_pulse and an 80 ms watchdog so a missed MCU toggle degrades to wall-clock cadence with a sticky telemetry bit rather than stalling the radar. Cold-reset defaults the handshake off (host_handshake_enable=0, new opcode 0x1A); the GUI opts in once the MCU PD8 wiring is verified on the bench. Both the FT601 and FT2232H status word 4 paths get the new beam_handshake_watchdog_fired sticky at bit [1] (reclaimed from the range_mode retirement in commit 1). RTL: - chirp_scheduler.v: 2-FF ASYNC_REG sync on beam_ready_async; 1-cycle edge detect (any transition, MCU side uses HAL_GPIO_TogglePin); new S_BEAM_WAIT state entered at frame_pulse when host_handshake_enable=1; 23-bit beam_watchdog counter with BEAM_WATCHDOG_MAX = 8_000_000 (~80 ms at 100 MHz, ~10 nominal frames); beam_handshake_watchdog_fired output sticky across mixers_enable cycles, cleared only by reset_n; mid-wait disable releases the FSM so dropping the opcode never strands the radar between frames. - radar_receiver_final.v: thread stm32_beam_ready_async + host_handshake_enable + beam_handshake_watchdog_fired through the scheduler instance. - radar_system_top.v: rename input port stm32_new_chirp → stm32_beam_ready; add host_handshake_enable register (cold-reset = 1'b0); opcode 0x1A dispatch (value[0]); add rx_beam_handshake_watchdog wire; pack into status_words[4][1] in both USB paths. - radar_system_top_50t.v: rename wrapper port + sub-instance wiring. - usb_data_interface.v + usb_data_interface_ft2232h.v: add status_beam_handshake_watchdog input + 2-FF level CDC (same convention as F-6.4 / F-1.2 stickies); refresh word-4 layout doc comment; pack beam_handshake_wd_sync_1 into status_words[4][1]. XDC: - xc7a50t_ftg256.xdc + xc7a200t_fbg484.xdc: rename stm32_new_chirp port references to stm32_beam_ready (same PD8 pin, F13 on 50T / L18 on 200T). MCU: - main.h: add FPGA_BEAM_READY_Pin = GPIO_PIN_8 + FPGA_BEAM_READY_GPIO_Port = GPIOD alongside the existing FPGA_FRAME_PULSE alias. - main.cpp:runRadarPulseSequence: insert HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_8) after each setCustomBeamPattern16(RX) — once after the per-azimuth broadside (vector_0), once after matrix1, once after matrix2 — between the SPI burst completion and waitForFramePulse. GUI: - radar_protocol.py: Opcode.HANDSHAKE_ENABLE = 0x1A; StatusResponse.beam_handshake_watchdog = 0 default; parse word 4 bit [1] in parse_status_packet; update word-4 layout comment. - test_GUI_V65_Tk.py: add beam_handshake_watchdog kwarg to _make_status_packet (sets bit [1] of word 4); refresh test_parse_status_word4_layout_co_spec to cover the new bit (used+9=32); add test_parse_status_beam_handshake_watchdog round-trip; test_handshake_enable_opcode pins 0x1A; defaults / chirps_mismatch / agc-coexist tests gain a watchdog==0 assertion; bump test_all_rtl_opcodes_present expected set to include 0x17/0x18/0x19/0x1A. TB: - new tb_chirp_scheduler_handshake.v (16 checks): legacy open-loop, edge exit (rising + falling), 200-cycle idle hold, watchdog auto-advance via force on dut.beam_watchdog, sticky-survives-mixers_disable, mid-wait disable release, reset_n clears sticky. - run_regression.sh: register the new TB in PHASE 1. - tb_radar_receiver_final.v: tie the 3 new receiver ports off (beam_ready_async=0, handshake_enable=0, watchdog unconnected). - tb_system_mechanics.v / tb_system_opcodes.v: explicit .stm32_beam_ready(1'b0) connection (the cold-reset host_handshake_enable=0 keeps the FSM out of S_BEAM_WAIT). - tb_usb_data_interface.v / tb_usb_protocol_v2.v / tb_e2e_dsp_to_host.v / tb_ft2232h_frame_drop.v: tie .status_beam_handshake_watchdog(1'b0). Ride-along ruff sweep (14 → 0 across the repo): - tb/cosim/compare_independent.py: RUF003 — '5×' → 'at least 5x'. - tb/cosim/gen_e2e_expected.py: noqa: E402 on the post-sys.path import; drop unused EXPECTED_RANGE_BIN + EXPECTED_DOPPLER_BIN_PER_SF imports; fold the detect-class slot if/else into a ternary (SIM108). - tb/cosim/gen_e2e_stimulus.py: drop int() wrapping round() at four call sites (RUF046 — round() already returns int in Python 3); rewrite the range-bin derivation comment block from code-like `# range_bin = ...` to prose (ERA001); strip stray f from placeholder-free error string (F541). - tb/cosim/tb_e2e_dsp_to_host_parse.py: open(path, 'r') → open(path) (UP015). - v7/dashboard.py: '3×' → '3x' (RUF003); drop quotes from 'StatusResponse | None' annotation (UP037, file already has `from __future__ import annotations`). CI summary (all suites green pre-commit): - ruff: All checks passed! - FPGA regression (iverilog): 43 / 0 / 0 (incl. new handshake TB 16/16). - MCU tests: 51 / 0 + 34 / 0 + 13 / 13 ADAR1000_AGC. - GUI Tk (test_GUI_V65_Tk): 120 / 0. - GUI v7 (test_v7): 152 / 0. Production rollout note: bitstream cold-resets with host_handshake_enable=0 so existing flashes keep their open-loop cadence until the GUI sends opcode 0x1A=1. Once enabled, the per-pattern dwell tracks both the chirp ladder (PD14 frame_pulse from commit-3 work) and the MCU pattern-write completion (PD8 toggle from this commit), eliminating drift from the SPI burst timing.
798 lines
37 KiB
Verilog
798 lines
37 KiB
Verilog
`include "radar_params.vh"
|
|
|
|
/**
|
|
* 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,
|
|
input wire ft601_reset_n, // FT601-domain synchronized reset
|
|
|
|
// Radar data inputs
|
|
input wire [31:0] range_profile,
|
|
input wire range_valid,
|
|
input wire [15:0] doppler_real,
|
|
input wire [15:0] doppler_imag,
|
|
input wire doppler_valid,
|
|
input wire cfar_detection,
|
|
input wire cfar_valid,
|
|
|
|
// 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 (active-HIGH per DS_FT600Q-FT601Q Table 3.2)
|
|
|
|
// Control signals
|
|
// 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)
|
|
output reg ft601_siwu_n, // Send immediate / Wakeup
|
|
|
|
// FIFO flags
|
|
input wire [1:0] ft601_srb, // Selected read buffer
|
|
input wire [1:0] ft601_swb, // Selected write buffer
|
|
|
|
// Clock
|
|
output wire ft601_clk_out, // Output clock to FT601 (forwarded via ODDR)
|
|
input wire ft601_clk_in, // Clock from FT601 (60/100MHz)
|
|
|
|
// ========== HOST COMMAND OUTPUTS (Gap 4: USB Read Path) ==========
|
|
// These outputs are registered in the ft601_clk domain and must be
|
|
// CDC-synchronized by the consumer (radar_system_top.v) before use
|
|
// in the clk_100m domain.
|
|
//
|
|
// Command word format: {opcode[7:0], addr[7:0], value[15:0]}
|
|
// 0x01 = Set radar mode (value[1:0] -> mode_controller mode)
|
|
// 0x02 = Single chirp trigger (value ignored, pulse cmd_valid)
|
|
// 0x03 = Set CFAR threshold (value[15:0] -> threshold)
|
|
// 0x04 = Set stream control (value[2:0] -> enable range/doppler/cfar)
|
|
// 0x10-0x15 = Chirp timing configuration (Gap 2)
|
|
// 0xFF = Status request (triggers status response packet)
|
|
output reg [31:0] cmd_data, // Last received command word
|
|
output reg cmd_valid, // Pulse: new command received (ft601_clk domain)
|
|
output reg [7:0] cmd_opcode, // Decoded opcode for convenience
|
|
output reg [7:0] cmd_addr, // Decoded register address
|
|
output reg [15:0] cmd_value, // Decoded value
|
|
|
|
// Gap 2: Stream control input (clk_100m domain, CDC'd internally)
|
|
// Bit 0 = range stream enable
|
|
// Bit 1 = doppler stream enable
|
|
// Bit 2 = cfar/detection stream enable
|
|
input wire [5:0] stream_control,
|
|
|
|
// Gap 2: Status readback inputs (clk_100m domain, CDC'd internally)
|
|
// When status_request pulses, the write FSM sends a status response
|
|
// packet containing the current register values.
|
|
input wire status_request, // 1-cycle pulse in clk_100m when 0xFF received
|
|
input wire [15:0] status_cfar_threshold, // Current CFAR threshold
|
|
input wire [5:0] status_stream_ctrl, // Current stream control (6-bit: [2:0]=stream en, [5:3]=format)
|
|
// status_radar_mode + status_range_mode retired in PR-AB.b expanded.
|
|
// Bits in status_words[0][23:22] and status_words[4][1:0] are reserved 0.
|
|
input wire [15:0] status_long_chirp, // Current long chirp cycles
|
|
input wire [15:0] status_long_listen, // Current long listen cycles
|
|
input wire [15:0] status_guard, // Current guard cycles
|
|
input wire [15:0] status_short_chirp, // Current short chirp cycles
|
|
input wire [15:0] status_short_listen, // Current short listen cycles
|
|
input wire [5:0] status_chirps_per_elev, // Current chirps per elevation
|
|
input wire status_chirps_mismatch, // TX-G: host requested chirps != Doppler FFT size
|
|
|
|
// Self-test status readback (opcode 0x31 / included in 0xFF status packet)
|
|
input wire [4:0] status_self_test_flags, // Per-test PASS(1)/FAIL(0) latched
|
|
input wire [7:0] status_self_test_detail, // Diagnostic detail byte latched
|
|
input wire status_self_test_busy, // Self-test FSM still running
|
|
|
|
// AGC status readback
|
|
input wire [3:0] status_agc_current_gain,
|
|
input wire [7:0] status_agc_peak_magnitude,
|
|
input wire [7:0] status_agc_saturation_count,
|
|
input wire status_agc_enable,
|
|
|
|
// AUDIT-S10: control-fault flags (clk domain). Exposed in status_words[5]
|
|
// [6:5] so host-side telemetry can graph each fault class independently
|
|
// of the gpio_dig7 split (which only the MCU observes). Both flags are
|
|
// sticky/slow-changing in the source domain (set on event, cleared by
|
|
// monitor reset), so 2-stage level CDC into ft601_clk_in is sufficient.
|
|
input wire status_range_decim_watchdog, // audit F-6.4
|
|
input wire status_ddc_cic_fir_overrun, // audit F-1.2
|
|
|
|
// PR-AB.b expanded commit 5: beam-ready handshake watchdog (clk domain).
|
|
// Sticky in chirp_scheduler; same 2-FF level CDC convention as the F-6.4
|
|
// and F-1.2 stickies above. Packed into status_words[4][1] downstream.
|
|
input wire status_beam_handshake_watchdog
|
|
);
|
|
|
|
// USB packet structure (same as before)
|
|
localparam HEADER = 8'hAA;
|
|
localparam FOOTER = 8'h55;
|
|
|
|
// FT601 configuration
|
|
localparam FT601_DATA_WIDTH = 32;
|
|
localparam FT601_BURST_SIZE = 512; // Max burst size in bytes
|
|
|
|
// ============================================================================
|
|
// WRITE FSM State definitions (Verilog-2001 compatible)
|
|
// ============================================================================
|
|
// 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 [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)
|
|
// ============================================================================
|
|
// FT601 245 synchronous FIFO read protocol:
|
|
// 1. Host has data available: RXF_N = 0 (active low)
|
|
// 2. FPGA asserts OE_N = 0 (bus turnaround: FT601 starts driving data bus)
|
|
// 3. Wait 1 cycle for bus turnaround settling
|
|
// 4. FPGA asserts RD_N = 0 (start reading: data valid on each posedge)
|
|
// 5. Sample ft601_data[31:0] while RD_N = 0 and RXF_N = 0
|
|
// 6. Deassert RD_N = 1, then OE_N = 1
|
|
//
|
|
// The read FSM only activates when the write FSM is IDLE and RXF indicates
|
|
// data is available. This prevents bus contention between TX and RX.
|
|
localparam [2:0] RD_IDLE = 3'd0, // Waiting for RXF
|
|
RD_OE_ASSERT = 3'd1, // Assert OE_N=0, wait turnaround
|
|
RD_READING = 3'd2, // Assert RD_N=0, sample data
|
|
RD_DEASSERT = 3'd3, // Deassert RD_N, then OE_N
|
|
RD_PROCESS = 3'd4; // Process received command word
|
|
|
|
reg [2:0] read_state;
|
|
reg [31:0] rx_data_captured; // Data word read from host
|
|
|
|
// ========== CDC INPUT SYNCHRONIZERS (clk domain -> ft601_clk_in domain) ==========
|
|
// The valid signals arrive from clk_100m but the state machine runs on ft601_clk_in.
|
|
// Even though both are 100 MHz, they are asynchronous clocks and need synchronization.
|
|
|
|
// 2-stage synchronizers for valid signals
|
|
(* ASYNC_REG = "TRUE" *) reg [1:0] range_valid_sync;
|
|
(* ASYNC_REG = "TRUE" *) reg [1:0] doppler_valid_sync;
|
|
(* ASYNC_REG = "TRUE" *) reg [1:0] cfar_valid_sync;
|
|
|
|
// Delayed versions of sync[1] for proper edge detection
|
|
reg range_valid_sync_d;
|
|
reg doppler_valid_sync_d;
|
|
reg cfar_valid_sync_d;
|
|
|
|
// Holding registers: data captured in SOURCE domain (clk_100m) when valid
|
|
// asserts, then read by ft601 domain after synchronized valid edge.
|
|
// This is safe because the data is stable for the entire time the valid
|
|
// pulse is being synchronized (2+ ft601_clk cycles).
|
|
reg [31:0] range_profile_hold;
|
|
reg [15:0] doppler_real_hold;
|
|
reg [15:0] doppler_imag_hold;
|
|
reg cfar_detection_hold;
|
|
|
|
// Gap 2: Status request toggle register (clk_100m domain).
|
|
// Declared here (before the always block) to satisfy iverilog forward-ref rules.
|
|
reg status_req_toggle_100m;
|
|
|
|
// Source-domain holding registers (clk domain)
|
|
always @(posedge clk or negedge reset_n) begin
|
|
if (!reset_n) begin
|
|
range_profile_hold <= 32'd0;
|
|
doppler_real_hold <= 16'd0;
|
|
doppler_imag_hold <= 16'd0;
|
|
cfar_detection_hold <= 1'b0;
|
|
status_req_toggle_100m <= 1'b0;
|
|
end else begin
|
|
if (range_valid)
|
|
range_profile_hold <= range_profile;
|
|
if (doppler_valid) begin
|
|
doppler_real_hold <= doppler_real;
|
|
doppler_imag_hold <= doppler_imag;
|
|
end
|
|
if (cfar_valid)
|
|
cfar_detection_hold <= cfar_detection;
|
|
// Gap 2: Toggle on status request pulse (CDC to ft601_clk)
|
|
if (status_request)
|
|
status_req_toggle_100m <= ~status_req_toggle_100m;
|
|
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;
|
|
reg [15:0] doppler_imag_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;
|
|
|
|
// 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).
|
|
//
|
|
// AUDIT-C16: pre-fix this was hardcoded `localparam [14:0] NUM_CELLS = 15'd16384`
|
|
// (50T sizing: 512 range x 32 doppler). On 200T builds with SUPPORT_LONG_RANGE
|
|
// defined, RP_MAX_OUTPUT_BINS=4096 -> a real frame is 131072 cells, so the
|
|
// fixed value made (a) the counter wrap 8x per actual frame and (b) the bit-7
|
|
// frame-start marker fire 8x at incorrect host-frame offsets, silently
|
|
// desyncing the GUI parser. The 15-bit width also could not hold 131072.
|
|
// Now derived from radar_params.vh; both value AND width scale with the build.
|
|
localparam integer NUM_CELLS = `RP_MAX_OUTPUT_BINS * `RP_NUM_DOPPLER_BINS;
|
|
reg [`RP_DOPPLER_MEM_ADDR_W-1: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
|
|
// 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 [5:0] stream_ctrl_sync_0;
|
|
(* ASYNC_REG = "TRUE" *) reg [5:0] stream_ctrl_sync_1;
|
|
wire stream_range_en = stream_ctrl_sync_1[0];
|
|
wire stream_doppler_en = stream_ctrl_sync_1[1];
|
|
wire stream_cfar_en = stream_ctrl_sync_1[2];
|
|
|
|
// Gap 2: Status request CDC (toggle CDC, same pattern as cmd_valid)
|
|
// status_request is a 1-cycle pulse in clk_100m. Toggle→sync→edge-detect.
|
|
// NOTE: status_req_toggle_100m declared above (before source-domain always block)
|
|
(* ASYNC_REG = "TRUE" *) reg [1:0] status_req_sync;
|
|
reg status_req_sync_prev;
|
|
|
|
// AUDIT-S10: 2-stage level CDC for control-fault flags (clk → ft601_clk_in).
|
|
// Sticky/slow-changing in source domain so 2-FF sync is sufficient.
|
|
(* ASYNC_REG = "TRUE" *) reg range_decim_watchdog_sync_0;
|
|
reg range_decim_watchdog_sync_1;
|
|
(* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0;
|
|
reg ddc_cic_fir_overrun_sync_1;
|
|
// PR-AB.b expanded commit 5: 2-FF level CDC for beam-handshake watchdog sticky.
|
|
(* ASYNC_REG = "TRUE" *) reg beam_handshake_wd_sync_0;
|
|
reg beam_handshake_wd_sync_1;
|
|
wire status_req_ft601 = status_req_sync[1] ^ status_req_sync_prev;
|
|
|
|
// Status snapshot: captured in ft601_clk domain when status request arrives.
|
|
// The clk_100m-domain status inputs are stable for many cycles after the
|
|
// command decode, so sampling them a few ft601_clk cycles later is safe.
|
|
reg [31:0] status_words [0:5]; // 6 status words (word 5 = self-test)
|
|
reg [2:0] status_word_idx;
|
|
|
|
wire range_valid_ft;
|
|
wire doppler_valid_ft;
|
|
wire cfar_valid_ft;
|
|
|
|
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;
|
|
range_valid_sync_d <= 1'b0;
|
|
doppler_valid_sync_d <= 1'b0;
|
|
cfar_valid_sync_d <= 1'b0;
|
|
range_profile_cap <= 32'd0;
|
|
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 <= 6'b000_001;
|
|
stream_ctrl_sync_1 <= 6'b000_001;
|
|
// Gap 2: status request CDC reset
|
|
status_req_sync <= 2'b00;
|
|
status_req_sync_prev <= 1'b0;
|
|
status_word_idx <= 3'd0;
|
|
// AUDIT-S10: control-fault flag CDC reset
|
|
range_decim_watchdog_sync_0 <= 1'b0;
|
|
range_decim_watchdog_sync_1 <= 1'b0;
|
|
ddc_cic_fir_overrun_sync_0 <= 1'b0;
|
|
ddc_cic_fir_overrun_sync_1 <= 1'b0;
|
|
beam_handshake_wd_sync_0 <= 1'b0;
|
|
beam_handshake_wd_sync_1 <= 1'b0;
|
|
end else begin
|
|
// Synchronize valid strobes (2-stage sync chain)
|
|
range_valid_sync <= {range_valid_sync[0], range_valid};
|
|
doppler_valid_sync <= {doppler_valid_sync[0], doppler_valid};
|
|
cfar_valid_sync <= {cfar_valid_sync[0], cfar_valid};
|
|
|
|
// Gap 2: stream control CDC (2-stage)
|
|
stream_ctrl_sync_0 <= stream_control;
|
|
stream_ctrl_sync_1 <= stream_ctrl_sync_0;
|
|
|
|
// Gap 2: status request CDC (toggle sync + edge detect)
|
|
status_req_sync <= {status_req_sync[0], status_req_toggle_100m};
|
|
status_req_sync_prev <= status_req_sync[1];
|
|
|
|
// AUDIT-S10: control-fault flag CDC (clk → ft601_clk_in, 2-stage)
|
|
range_decim_watchdog_sync_0 <= status_range_decim_watchdog;
|
|
range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0;
|
|
ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun;
|
|
ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0;
|
|
beam_handshake_wd_sync_0 <= status_beam_handshake_watchdog;
|
|
beam_handshake_wd_sync_1 <= beam_handshake_wd_sync_0;
|
|
|
|
// Gap 2: Capture status snapshot when request arrives in ft601 domain
|
|
if (status_req_ft601) begin
|
|
// Pack register values into 5x 32-bit status words
|
|
// Word 0: {0xFF[31:24], reserved[23:22]=0, stream[21:16], threshold[15:0]}
|
|
// (mode bits retired in PR-AB.b expanded — single-mode FSM)
|
|
status_words[0] <= {8'hFF, 2'd0, status_stream_ctrl,
|
|
status_cfar_threshold};
|
|
// Word 1: {long_chirp_cycles[15:0], long_listen_cycles[15:0]}
|
|
status_words[1] <= {status_long_chirp, status_long_listen};
|
|
// Word 2: {guard_cycles[15:0], short_chirp_cycles[15:0]}
|
|
status_words[2] <= {status_guard, status_short_chirp};
|
|
// Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0}
|
|
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
|
|
// Word 4 layout (post PR-AB.b expanded commit 5):
|
|
// [31:28] agc_current_gain
|
|
// [27:20] agc_peak_magnitude
|
|
// [19:12] agc_saturation_count
|
|
// [11] agc_enable
|
|
// [10] chirps_mismatch (TX-G)
|
|
// [9:2] reserved 0 (alpha_soft echo lives in ft2232h-only path)
|
|
// [1] beam_handshake_watchdog_fired (sticky, reset_n clear)
|
|
// [0] reserved 0 (range_mode bit retired PR-AB.b expanded)
|
|
status_words[4] <= {status_agc_current_gain,
|
|
status_agc_peak_magnitude,
|
|
status_agc_saturation_count,
|
|
status_agc_enable,
|
|
status_chirps_mismatch,
|
|
8'd0,
|
|
beam_handshake_wd_sync_1,
|
|
1'd0};
|
|
// Word 5: {reserved[6:0], self_test_busy[24], reserved[23:16],
|
|
// self_test_detail[15:8], reserved[7], cic_fir_overrun[6],
|
|
// range_decim_watchdog[5], self_test_flags[4:0]}
|
|
// AUDIT-S10: bits [6:5] expose control-fault classes that route
|
|
// to gpio_dig7 — gives host visibility regardless of MCU consumption.
|
|
status_words[5] <= {7'd0, status_self_test_busy,
|
|
8'd0, status_self_test_detail,
|
|
1'd0, // [7] reserved
|
|
ddc_cic_fir_overrun_sync_1, // [6] audit F-1.2
|
|
range_decim_watchdog_sync_1, // [5] audit F-6.4
|
|
status_self_test_flags}; // [4:0]
|
|
end
|
|
|
|
// Delayed version of sync[1] for edge detection
|
|
range_valid_sync_d <= range_valid_sync[1];
|
|
doppler_valid_sync_d <= doppler_valid_sync[1];
|
|
cfar_valid_sync_d <= cfar_valid_sync[1];
|
|
|
|
// Capture data on rising edge of FULLY SYNCHRONIZED valid (sync[1])
|
|
// Data in holding regs is stable by the time sync[1] rises (2+ cycles)
|
|
if (range_valid_sync[1] && !range_valid_sync_d)
|
|
range_profile_cap <= range_profile_hold;
|
|
if (doppler_valid_sync[1] && !doppler_valid_sync_d) begin
|
|
doppler_real_cap <= doppler_real_hold;
|
|
doppler_imag_cap <= doppler_imag_hold;
|
|
end
|
|
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
|
|
|
|
// Rising-edge detect on FULLY SYNCHRONIZED valid (sync[1], not sync[0])
|
|
// This provides full 2-stage metastability protection before use.
|
|
assign range_valid_ft = range_valid_sync[1] && !range_valid_sync_d;
|
|
assign doppler_valid_ft = doppler_valid_sync[1] && !doppler_valid_sync_d;
|
|
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_effective_reset_n) begin
|
|
if (!ft601_effective_reset_n) begin
|
|
current_state <= IDLE;
|
|
read_state <= RD_IDLE;
|
|
data_word_idx <= 2'd0;
|
|
ft601_data_out <= 0;
|
|
ft601_data_oe <= 0;
|
|
ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode
|
|
ft601_txe_n <= 1;
|
|
ft601_rxf_n <= 1;
|
|
ft601_wr_n <= 1;
|
|
ft601_rd_n <= 1;
|
|
ft601_oe_n <= 1;
|
|
ft601_siwu_n <= 1;
|
|
rx_data_captured <= 32'd0;
|
|
cmd_data <= 32'd0;
|
|
cmd_valid <= 1'b0;
|
|
cmd_opcode <= 8'd0;
|
|
cmd_addr <= 8'd0;
|
|
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 <= {`RP_DOPPLER_MEM_ADDR_W{1'b0}};
|
|
// 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
|
|
// Default: clear one-shot signals
|
|
cmd_valid <= 1'b0;
|
|
|
|
// Data-pending flag management: set on valid edge, cleared when
|
|
// consumed or skipped by write FSM. Must be in this always block
|
|
// (not the CDC sync block) to avoid Vivado multiple-driver DRC error.
|
|
if (doppler_valid_ft)
|
|
doppler_data_pending <= 1'b1;
|
|
if (cfar_valid_ft)
|
|
cfar_data_pending <= 1'b1;
|
|
|
|
// ================================================================
|
|
// READ FSM — host-to-FPGA command path (Gap 4)
|
|
//
|
|
// The read FSM takes priority over write when both could activate.
|
|
// It only starts when the write FSM is IDLE and ft601_rxf
|
|
// indicates data from host is available.
|
|
// ================================================================
|
|
case (read_state)
|
|
RD_IDLE: begin
|
|
// Only start reading if write FSM is idle and host has data.
|
|
// ft601_rxf active-low: 0 means data available from host.
|
|
if (current_state == IDLE && !ft601_rxf) begin
|
|
ft601_oe_n <= 1'b0; // Assert OE: tell FT601 to drive bus
|
|
ft601_data_oe <= 1'b0; // FPGA releases bus (FT601 drives)
|
|
read_state <= RD_OE_ASSERT;
|
|
end
|
|
end
|
|
|
|
RD_OE_ASSERT: begin
|
|
// 1-cycle turnaround: OE_N asserted, bus settling.
|
|
// FT601 spec requires 1 clock of OE_N before RD_N assertion.
|
|
if (!ft601_rxf) begin
|
|
ft601_rd_n <= 1'b0; // Assert RD: start reading
|
|
read_state <= RD_READING;
|
|
end else begin
|
|
// Host withdrew data — abort
|
|
ft601_oe_n <= 1'b1;
|
|
read_state <= RD_IDLE;
|
|
end
|
|
end
|
|
|
|
RD_READING: begin
|
|
// Data is valid on ft601_data. Sample it.
|
|
// For now we read a single 32-bit command word per transaction.
|
|
rx_data_captured <= ft601_data;
|
|
ft601_rd_n <= 1'b1; // Deassert RD
|
|
read_state <= RD_DEASSERT;
|
|
end
|
|
|
|
RD_DEASSERT: begin
|
|
// Deassert OE_N (1 cycle after RD_N deasserted)
|
|
ft601_oe_n <= 1'b1;
|
|
read_state <= RD_PROCESS;
|
|
end
|
|
|
|
RD_PROCESS: begin
|
|
// Decode the received command word and pulse cmd_valid.
|
|
// Format: {opcode[31:24], addr[23:16], value[15:0]}
|
|
cmd_data <= rx_data_captured;
|
|
cmd_opcode <= rx_data_captured[31:24];
|
|
cmd_addr <= rx_data_captured[23:16];
|
|
cmd_value <= rx_data_captured[15:0];
|
|
cmd_valid <= 1'b1;
|
|
read_state <= RD_IDLE;
|
|
end
|
|
|
|
default: read_state <= RD_IDLE;
|
|
endcase
|
|
|
|
// ================================================================
|
|
// WRITE FSM — FPGA-to-host data streaming (existing)
|
|
//
|
|
// Only operates when read FSM is idle (no bus contention).
|
|
// ================================================================
|
|
if (read_state == RD_IDLE) begin
|
|
case (current_state)
|
|
IDLE: begin
|
|
ft601_wr_n <= 1;
|
|
ft601_data_oe <= 0; // Release data bus
|
|
// Gap 2: Status readback takes priority
|
|
if (status_req_ft601 && ft601_rxf) begin
|
|
current_state <= SEND_STATUS;
|
|
status_word_idx <= 3'd0;
|
|
end
|
|
// 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
|
|
// 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 == {`RP_DOPPLER_MEM_ADDR_W{1'b0}}), 6'b0, cfar_detection_cap}
|
|
: {(sample_counter == {`RP_DOPPLER_MEM_ADDR_W{1'b0}}), 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_DATA_WORD: begin
|
|
if (!ft601_txe) begin
|
|
ft601_data_oe <= 1;
|
|
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
|
|
if (data_word_idx == 2'd2) begin
|
|
data_word_idx <= 2'd0;
|
|
current_state <= WAIT_ACK;
|
|
end else begin
|
|
data_word_idx <= data_word_idx + 2'd1;
|
|
end
|
|
end
|
|
end
|
|
|
|
// Gap 2: Status readback — send 6 x 32-bit status words
|
|
// Format: HEADER, status_words[0..5], FOOTER
|
|
SEND_STATUS: begin
|
|
if (!ft601_txe) begin
|
|
ft601_data_oe <= 1;
|
|
ft601_be <= 4'b1111;
|
|
case (status_word_idx)
|
|
3'd0: begin
|
|
// Send status header marker (0xBB = status response)
|
|
ft601_data_out <= {24'b0, 8'hBB};
|
|
ft601_be <= 4'b0001;
|
|
end
|
|
3'd1: ft601_data_out <= status_words[0];
|
|
3'd2: ft601_data_out <= status_words[1];
|
|
3'd3: ft601_data_out <= status_words[2];
|
|
3'd4: ft601_data_out <= status_words[3];
|
|
3'd5: ft601_data_out <= status_words[4];
|
|
3'd6: ft601_data_out <= status_words[5];
|
|
3'd7: begin
|
|
// Send status footer
|
|
ft601_data_out <= {24'b0, FOOTER};
|
|
ft601_be <= 4'b0001;
|
|
end
|
|
default: ;
|
|
endcase
|
|
ft601_wr_n <= 0;
|
|
if (status_word_idx == 3'd7) begin
|
|
status_word_idx <= 3'd0;
|
|
current_state <= WAIT_ACK;
|
|
end else begin
|
|
status_word_idx <= status_word_idx + 1;
|
|
end
|
|
end
|
|
end
|
|
|
|
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 - 1)
|
|
sample_counter <= {`RP_DOPPLER_MEM_ADDR_W{1'b0}};
|
|
else
|
|
sample_counter <= sample_counter + 1'b1;
|
|
current_state <= IDLE;
|
|
end
|
|
endcase
|
|
end
|
|
end
|
|
end
|
|
|
|
// ============================================================================
|
|
// FT601 clock output forwarding
|
|
// ============================================================================
|
|
// Forward ft601_clk_in back out via ODDR so that the forwarded clock at the
|
|
// pin has the same insertion delay as the data outputs (both go through the
|
|
// same BUFG). This makes the output delay analysis relative to the generated
|
|
// clock at the pin, where insertion delays cancel.
|
|
|
|
`ifndef SIMULATION
|
|
ODDR #(
|
|
.DDR_CLK_EDGE("OPPOSITE_EDGE"),
|
|
.INIT(1'b0),
|
|
.SRTYPE("SYNC")
|
|
) oddr_ft601_clk (
|
|
.Q(ft601_clk_out),
|
|
.C(ft601_clk_in),
|
|
.CE(1'b1),
|
|
.D1(1'b1),
|
|
.D2(1'b0),
|
|
.R(1'b0),
|
|
.S(1'b0)
|
|
);
|
|
`else
|
|
// Simulation: behavioral clock forwarding
|
|
reg ft601_clk_out_sim;
|
|
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;
|
|
end
|
|
// In simulation, just pass the clock through
|
|
assign ft601_clk_out = ft601_clk_in;
|
|
`endif
|
|
|
|
// ============================================================================
|
|
// TX-N9: payload-hold checker (simulation only)
|
|
//
|
|
// cmd_data / cmd_opcode / cmd_addr / cmd_value feed downstream CDC sync
|
|
// chains; safety property is that they only change on the cycle cmd_valid
|
|
// rises (RD_PROCESS), and are held stable on every other cycle. The FSM
|
|
// satisfies this implicitly today; this checker fires `[ASSERT FAIL]` on
|
|
// any payload change while cmd_valid is low so a future regression is
|
|
// caught in the simulation log. Synthesis-inert.
|
|
// ============================================================================
|
|
`ifdef SIMULATION
|
|
reg [31:0] cmd_data_prev_n9;
|
|
reg [7:0] cmd_opcode_prev_n9;
|
|
reg [7:0] cmd_addr_prev_n9;
|
|
reg [15:0] cmd_value_prev_n9;
|
|
reg cmd_valid_prev_n9;
|
|
|
|
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
|
|
if (!ft601_reset_n) begin
|
|
cmd_data_prev_n9 <= 32'd0;
|
|
cmd_opcode_prev_n9 <= 8'd0;
|
|
cmd_addr_prev_n9 <= 8'd0;
|
|
cmd_value_prev_n9 <= 16'd0;
|
|
cmd_valid_prev_n9 <= 1'b0;
|
|
end else begin
|
|
if (!cmd_valid && !cmd_valid_prev_n9) begin
|
|
if (cmd_data !== cmd_data_prev_n9)
|
|
$display("[ASSERT FAIL] TX-N9: cmd_data changed while cmd_valid=0 (%h -> %h)",
|
|
cmd_data_prev_n9, cmd_data);
|
|
if (cmd_opcode !== cmd_opcode_prev_n9)
|
|
$display("[ASSERT FAIL] TX-N9: cmd_opcode changed while cmd_valid=0 (%h -> %h)",
|
|
cmd_opcode_prev_n9, cmd_opcode);
|
|
if (cmd_addr !== cmd_addr_prev_n9)
|
|
$display("[ASSERT FAIL] TX-N9: cmd_addr changed while cmd_valid=0 (%h -> %h)",
|
|
cmd_addr_prev_n9, cmd_addr);
|
|
if (cmd_value !== cmd_value_prev_n9)
|
|
$display("[ASSERT FAIL] TX-N9: cmd_value changed while cmd_valid=0 (%h -> %h)",
|
|
cmd_value_prev_n9, cmd_value);
|
|
end
|
|
cmd_data_prev_n9 <= cmd_data;
|
|
cmd_opcode_prev_n9 <= cmd_opcode;
|
|
cmd_addr_prev_n9 <= cmd_addr;
|
|
cmd_value_prev_n9 <= cmd_value;
|
|
cmd_valid_prev_n9 <= cmd_valid;
|
|
end
|
|
end
|
|
`endif
|
|
|
|
endmodule |