From e5d1b3cfc31e2661a86337117fa6df10c4af1408 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:16:26 +0200 Subject: [PATCH] Gap 4 USB Read Path: wire host-to-FPGA command path with toggle CDC, add read path tests - usb_data_interface.v: Add FT601 read FSM (RD_IDLE/OE_ASSERT/READING/ DEASSERT/PROCESS) + cmd_data/valid/opcode/addr/value output ports - radar_system_top.v: Connect cmd_* ports from usb_inst, add toggle CDC for cmd_valid (ft601_clk -> clk_100m), add command decode registers (mode/trigger/cfar_threshold/stream_control), wire host_mode and host_trigger to rx_inst - radar_receiver_final.v: Add host_mode[1:0] and host_trigger input ports, replace hardcoded mode/trigger in radar_mode_controller instance - tb_usb_data_interface.v: Connect cmd_* ports, add host data bus driver, add Test Groups 12 (single command), 13 (multiple commands), 14 (read/write interleave). USB TB now has 55 checks. Regression: 18/18 PASS --- 9_Firmware/9_2_FPGA/radar_receiver_final.v | 11 +- 9_Firmware/9_2_FPGA/radar_system_top.v | 96 +++++- .../9_2_FPGA/tb/tb_usb_data_interface.v | 170 +++++++++- 9_Firmware/9_2_FPGA/usb_data_interface.v | 292 ++++++++++++------ 4 files changed, 477 insertions(+), 92 deletions(-) diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index 5aaa71c..c0c988b 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -22,7 +22,12 @@ module radar_receiver_final ( // Matched filter range profile output (for USB) output wire signed [15:0] range_profile_i_out, output wire signed [15:0] range_profile_q_out, - output wire range_profile_valid_out + output wire range_profile_valid_out, + + // Host command inputs (Gap 4: USB Read Path) + // CDC-synchronized in radar_system_top.v before reaching here + input wire [1:0] host_mode, // Radar mode: 00=STM32, 01=auto-scan, 10=single-chirp + input wire host_trigger // Single-chirp trigger pulse (1 clk cycle) ); // ========== INTERNAL SIGNALS ========== @@ -81,11 +86,11 @@ wire [5:0] rmc_azimuth_count; radar_mode_controller rmc ( .clk(clk), .reset_n(reset_n), - .mode(2'b01), // Auto-scan mode + .mode(host_mode), // Controlled by host via USB (default: 2'b01 auto-scan) .stm32_new_chirp(1'b0), // Unused in auto mode .stm32_new_elevation(1'b0), // Unused in auto mode .stm32_new_azimuth(1'b0), // Unused in auto mode - .trigger(1'b0), // Unused in auto mode + .trigger(host_trigger), // Single-chirp trigger from host via USB .use_long_chirp(use_long_chirp), .mc_new_chirp(mc_new_chirp), .mc_new_elevation(mc_new_elevation), diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index e004145..7fb13a0 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -176,6 +176,21 @@ wire usb_cfar_valid; // System status reg [3:0] status_reg; +// USB host command outputs (Gap 4: USB Read Path) +// These are in the ft601_clk domain; CDC'd to clk_100m below +wire [31:0] usb_cmd_data; +wire usb_cmd_valid; // 1-cycle pulse in ft601_clk domain +wire [7:0] usb_cmd_opcode; +wire [7:0] usb_cmd_addr; +wire [15:0] usb_cmd_value; + +// USB command decode registers (clk_100m domain, driven by CDC block below) +// Declared here (before rx_inst) so Icarus Verilog can resolve forward refs. +reg [1:0] host_radar_mode; +reg host_trigger_pulse; +reg [15:0] host_cfar_threshold; +reg [2:0] host_stream_control; + // ============================================================================ // CLOCK BUFFERING // ============================================================================ @@ -407,7 +422,11 @@ radar_receiver_final rx_inst ( // Matched filter range profile (for USB) .range_profile_i_out(rx_range_profile[15:0]), .range_profile_q_out(rx_range_profile[31:16]), - .range_profile_valid_out(rx_range_valid) + .range_profile_valid_out(rx_range_valid), + + // Host command inputs (Gap 4: USB Read Path, CDC-synchronized) + .host_mode(host_radar_mode), + .host_trigger(host_trigger_pulse) ); // ============================================================================ @@ -496,9 +515,82 @@ usb_data_interface usb_inst ( .ft601_srb(ft601_srb), .ft601_swb(ft601_swb), .ft601_clk_out(ft601_clk_out), - .ft601_clk_in(ft601_clk_buf) + .ft601_clk_in(ft601_clk_buf), + + // Host command outputs (Gap 4: USB Read Path) + .cmd_data(usb_cmd_data), + .cmd_valid(usb_cmd_valid), + .cmd_opcode(usb_cmd_opcode), + .cmd_addr(usb_cmd_addr), + .cmd_value(usb_cmd_value) ); +// ============================================================================ +// USB COMMAND CDC: ft601_clk → clk_100m (Gap 4: USB Read Path) +// ============================================================================ +// cmd_valid is a 1-cycle pulse in ft601_clk. Use toggle CDC (same pattern +// as chirp_frame_toggle_120m above) to safely transfer it to clk_100m. +// cmd_data/opcode/addr/value are held stable after cmd_valid pulses, so +// we simply sample them in clk_100m when the CDC'd pulse arrives. + +// Step 1: Toggle on cmd_valid pulse (ft601_clk domain) +reg cmd_valid_toggle_ft601; +always @(posedge ft601_clk_buf or negedge sys_reset_ft601_n) begin + if (!sys_reset_ft601_n) + cmd_valid_toggle_ft601 <= 1'b0; + else if (usb_cmd_valid) + cmd_valid_toggle_ft601 <= ~cmd_valid_toggle_ft601; +end + +// Step 2: Synchronize toggle to clk_100m domain (3-stage) +wire cmd_valid_toggle_100m; +cdc_single_bit #( + .STAGES(3) +) cdc_cmd_valid ( + .src_clk(ft601_clk_buf), + .dst_clk(clk_100m_buf), + .reset_n(sys_reset_n), + .src_signal(cmd_valid_toggle_ft601), + .dst_signal(cmd_valid_toggle_100m) +); + +// Step 3: Edge-detect toggle to recover pulse in clk_100m domain +reg cmd_valid_toggle_100m_prev; +always @(posedge clk_100m_buf or negedge sys_reset_n) begin + if (!sys_reset_n) + cmd_valid_toggle_100m_prev <= 1'b0; + else + cmd_valid_toggle_100m_prev <= cmd_valid_toggle_100m; +end +wire cmd_valid_100m = cmd_valid_toggle_100m ^ cmd_valid_toggle_100m_prev; + +// Step 4: Command decode registers in clk_100m domain +// Sample cmd_data fields when CDC'd valid pulse arrives. Data is stable +// because the read FSM holds cmd_opcode/addr/value until the next command. +// NOTE: reg declarations for host_radar_mode, host_trigger_pulse, +// host_cfar_threshold, host_stream_control are in INTERNAL SIGNALS section +// above (before rx_inst) to avoid Icarus Verilog forward-reference errors. + +always @(posedge clk_100m_buf or negedge sys_reset_n) begin + if (!sys_reset_n) begin + host_radar_mode <= 2'b01; // Default: auto-scan + host_trigger_pulse <= 1'b0; + host_cfar_threshold <= 16'd10000; // Default threshold + host_stream_control <= 3'b111; // Default: all streams enabled + end else begin + host_trigger_pulse <= 1'b0; // Self-clearing pulse + if (cmd_valid_100m) begin + case (usb_cmd_opcode) + 8'h01: host_radar_mode <= usb_cmd_value[1:0]; + 8'h02: host_trigger_pulse <= 1'b1; + 8'h03: host_cfar_threshold <= usb_cmd_value; + 8'h04: host_stream_control <= usb_cmd_value[2:0]; + default: ; // 0xFF status request handled elsewhere + endcase + end + end +end + // ============================================================================ // OUTPUT ASSIGNMENTS // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v index 11e3d53..f8738c1 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v @@ -47,6 +47,18 @@ module tb_usb_data_interface; // Pulldown: when nobody drives, data reads as 0 (not X) pulldown pd[31:0] (ft601_data); + // Host-to-FPGA data bus driver (for read path testing) + reg [31:0] host_data_drive; + reg host_data_drive_en; + assign ft601_data = host_data_drive_en ? host_data_drive : 32'hzzzz_zzzz; + + // DUT command outputs (Gap 4: USB Read Path) + wire [31:0] cmd_data; + wire cmd_valid; + wire [7:0] cmd_opcode; + wire [7:0] cmd_addr; + wire [15:0] cmd_value; + // ── Clock generators (asynchronous) ──────────────────────── always #(CLK_PERIOD / 2) clk = ~clk; always #(FT_CLK_PERIOD / 2) ft601_clk_in = ~ft601_clk_in; @@ -76,7 +88,14 @@ module tb_usb_data_interface; .ft601_srb (ft601_srb), .ft601_swb (ft601_swb), .ft601_clk_out (ft601_clk_out), - .ft601_clk_in (ft601_clk_in) + .ft601_clk_in (ft601_clk_in), + + // Host command outputs (Gap 4: USB Read Path) + .cmd_data (cmd_data), + .cmd_valid (cmd_valid), + .cmd_opcode (cmd_opcode), + .cmd_addr (cmd_addr), + .cmd_value (cmd_value) ); // ── Test bookkeeping ─────────────────────────────────────── @@ -118,6 +137,8 @@ module tb_usb_data_interface; ft601_rxf = 1; ft601_srb = 2'b00; ft601_swb = 2'b00; + host_data_drive = 32'h0; + host_data_drive_en = 0; repeat (6) @(posedge ft601_clk_in); reset_n = 1; repeat (2) @(posedge ft601_clk_in); @@ -182,6 +203,52 @@ module tb_usb_data_interface; end endtask + // ── Helper: wait for read FSM to reach a specific state ─── + task wait_for_read_state; + input [2:0] target; + input integer max_cyc; + integer cnt; + begin + cnt = 0; + while (uut.read_state !== target && cnt < max_cyc) begin + @(posedge ft601_clk_in); + cnt = cnt + 1; + end + end + endtask + + // ── Helper: send a single host command word via the read path ── + // Simulates the FT601 host presenting a 32-bit command word. + // Protocol: Assert RXF=0 (data available), wait for OE_N=0, + // drive data bus, wait for RD_N=0, then release. + task send_host_command; + input [31:0] cmd_word; + begin + // Signal host has data + ft601_rxf = 0; + // Wait for FPGA to assert OE_N (bus turnaround) + wait_for_read_state(3'd1, 20); // RD_OE_ASSERT = 3'd1 + @(posedge ft601_clk_in); #1; + // Drive data bus (FT601 drives in real hardware) + host_data_drive = cmd_word; + host_data_drive_en = 1; + // Wait for FPGA to assert RD_N=0 (RD_READING state) + wait_for_read_state(3'd2, 20); // RD_READING = 3'd2 + @(posedge ft601_clk_in); #1; + // Data has been sampled. FPGA deasserts RD then OE. + // Wait for RD_PROCESS or back to RD_IDLE + wait_for_read_state(3'd4, 20); // RD_PROCESS = 3'd4 + @(posedge ft601_clk_in); #1; + // Release bus and deassert RXF + host_data_drive_en = 0; + host_data_drive = 32'h0; + ft601_rxf = 1; + // Wait for read FSM to return to idle + wait_for_read_state(3'd0, 20); // RD_IDLE = 3'd0 + @(posedge ft601_clk_in); #1; + end + endtask + // Drive a complete packet through the FSM by sequentially providing // range, doppler (4x), and cfar valid pulses. task drive_full_packet; @@ -212,6 +279,8 @@ module tb_usb_data_interface; pass_count = 0; fail_count = 0; test_num = 0; + host_data_drive = 32'h0; + host_data_drive_en = 0; csv_file = $fopen("tb_usb_data_interface.csv", "w"); $fwrite(csv_file, "test_num,pass_fail,label\n"); @@ -553,6 +622,105 @@ module tb_usb_data_interface; check(uut.range_profile_cap === 32'hCCCC_DDDD, "Packet 2 range data captured correctly"); + // ════════════════════════════════════════════════════════ + // TEST GROUP 12: Read Path - Single Command (Gap 4) + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 12: Read Path - Single Command ---"); + apply_reset; + // Write FSM is IDLE, so read FSM can activate + + // Send "Set radar mode" command: opcode=0x01, addr=0x00, value=0x0002 + send_host_command({8'h01, 8'h00, 16'h0002}); + + check(cmd_opcode === 8'h01, + "Read path: cmd_opcode=0x01 (set mode)"); + check(cmd_addr === 8'h00, + "Read path: cmd_addr=0x00"); + check(cmd_value === 16'h0002, + "Read path: cmd_value=0x0002 (single-chirp mode)"); + check(cmd_data === {8'h01, 8'h00, 16'h0002}, + "Read path: cmd_data matches full command word"); + + // Verify read FSM returned to idle + check(uut.read_state === 3'd0, + "Read FSM returned to RD_IDLE after command"); + + // ════════════════════════════════════════════════════════ + // TEST GROUP 13: Read Path - Multiple Commands (Gap 4) + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 13: Read Path - Multiple Commands ---"); + apply_reset; + + // Command 1: Set radar mode to auto-scan (0x01) + send_host_command({8'h01, 8'h00, 16'h0001}); + check(cmd_opcode === 8'h01, + "Multi-cmd 1: opcode=0x01 (set mode)"); + check(cmd_value === 16'h0001, + "Multi-cmd 1: value=0x0001 (auto-scan)"); + + // Command 2: Single chirp trigger (0x02) + send_host_command({8'h02, 8'h00, 16'h0000}); + check(cmd_opcode === 8'h02, + "Multi-cmd 2: opcode=0x02 (trigger)"); + + // Command 3: Set CFAR threshold (0x03) + send_host_command({8'h03, 8'h00, 16'h1234}); + check(cmd_opcode === 8'h03, + "Multi-cmd 3: opcode=0x03 (CFAR threshold)"); + check(cmd_value === 16'h1234, + "Multi-cmd 3: value=0x1234"); + + // Command 4: Set stream control (0x04) + send_host_command({8'h04, 8'h00, 16'h0005}); + check(cmd_opcode === 8'h04, + "Multi-cmd 4: opcode=0x04 (stream control)"); + check(cmd_value === 16'h0005, + "Multi-cmd 4: value=0x0005 (range+cfar)"); + + // ════════════════════════════════════════════════════════ + // TEST GROUP 14: Read/Write Interleave (Gap 4) + // Verifies no bus contention: read FSM only operates when + // write FSM is IDLE. + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 14: Read/Write Interleave ---"); + apply_reset; + ft601_txe = 0; + + // Start a write packet + assert_range_valid(32'hFACE_FEED); + wait_for_state(S_SEND_HEADER, 50); + @(posedge ft601_clk_in); #1; + + // While write FSM is active, assert RXF=0 (host has data) + // Read FSM should NOT activate (read_state stays RD_IDLE) + ft601_rxf = 0; + repeat (5) @(posedge ft601_clk_in); #1; + + check(uut.read_state === 3'd0, + "Read FSM stays in RD_IDLE while write FSM active"); + + // 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; + + check(uut.current_state === S_IDLE, + "Write packet completed, FSM in IDLE"); + + // Now send a read command — should work fine after write completes + send_host_command({8'h01, 8'h00, 16'h0002}); + check(cmd_opcode === 8'h01, + "Read after write: cmd_opcode=0x01"); + check(cmd_value === 16'h0002, + "Read after write: cmd_value=0x0002"); + // ════════════════════════════════════════════════════════ // Summary // ════════════════════════════════════════════════════════ diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 19ffeb7..67081d6 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -33,7 +33,24 @@ module usb_data_interface ( // Clock output wire ft601_clk_out, // Output clock to FT601 (forwarded via ODDR) - input wire ft601_clk_in // Clock from FT601 (60/100MHz) + 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) + // 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 ); // USB packet structure (same as before) @@ -44,7 +61,9 @@ localparam FOOTER = 8'h55; localparam FT601_DATA_WIDTH = 32; localparam FT601_BURST_SIZE = 512; // Max burst size in bytes -// State definitions (Verilog-2001 compatible) +// ============================================================================ +// WRITE FSM State definitions (Verilog-2001 compatible) +// ============================================================================ localparam [2:0] IDLE = 3'd0, SEND_HEADER = 3'd1, SEND_RANGE_DATA = 3'd2, @@ -59,6 +78,28 @@ reg [31:0] data_buffer; reg [31:0] ft601_data_out; reg ft601_data_oe; // Output enable for bidirectional data bus +// ============================================================================ +// 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. @@ -159,6 +200,7 @@ 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 current_state <= IDLE; + read_state <= RD_IDLE; byte_counter <= 0; ft601_data_out <= 0; ft601_data_oe <= 0; @@ -169,101 +211,179 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin 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; // 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 - case (current_state) - IDLE: begin - ft601_wr_n <= 1; - ft601_data_oe <= 0; // Release data bus - if (range_valid_ft || doppler_valid_ft || cfar_valid_ft) begin - current_state <= SEND_HEADER; - byte_counter <= 0; + // Default: clear one-shot signals + cmd_valid <= 1'b0; + + // ================================================================ + // 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 - - 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 - current_state <= SEND_RANGE_DATA; + + 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 - - SEND_RANGE_DATA: 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}; - endcase - - ft601_wr_n <= 0; - - if (byte_counter == 3) begin - byte_counter <= 0; - current_state <= SEND_DOPPLER_DATA; - end else begin - byte_counter <= byte_counter + 1; - 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 - - SEND_DOPPLER_DATA: begin - if (!ft601_txe && doppler_valid_ft) 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; - current_state <= SEND_DETECTION_DATA; - end else begin - byte_counter <= byte_counter + 1; - end - end + + RD_DEASSERT: begin + // Deassert OE_N (1 cycle after RD_N deasserted) + ft601_oe_n <= 1'b1; + read_state <= RD_PROCESS; end - - SEND_DETECTION_DATA: begin - if (!ft601_txe && cfar_valid_ft) begin - ft601_data_oe <= 1; - ft601_be <= 4'b0001; - ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap}; - ft601_wr_n <= 0; - 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 - - WAIT_ACK: begin - ft601_wr_n <= 1; - ft601_data_oe <= 0; // Release data bus - current_state <= IDLE; + + 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 + if (range_valid_ft || doppler_valid_ft || cfar_valid_ft) 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; + 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 + current_state <= SEND_RANGE_DATA; + end + end + + SEND_RANGE_DATA: 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}; + endcase + + ft601_wr_n <= 0; + + if (byte_counter == 3) begin + byte_counter <= 0; + current_state <= SEND_DOPPLER_DATA; + end else begin + byte_counter <= byte_counter + 1; + end + end + end + + SEND_DOPPLER_DATA: begin + if (!ft601_txe && doppler_valid_ft) 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; + current_state <= SEND_DETECTION_DATA; + end else begin + byte_counter <= byte_counter + 1; + end + end + end + + SEND_DETECTION_DATA: begin + if (!ft601_txe && cfar_valid_ft) begin + ft601_data_oe <= 1; + ft601_be <= 4'b0001; + ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap}; + ft601_wr_n <= 0; + 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 + + WAIT_ACK: begin + ft601_wr_n <= 1; + ft601_data_oe <= 0; // Release data bus + current_state <= IDLE; + end + endcase + end end end