fix(fpga): TX range-mode awareness + clamp reserved host codes

C-1: plfm_chirp_controller was not range_mode-aware; it always ran
LONG_CHIRP for CHIRP_MAX/2 chirps even when the host selected 3 km
mode, where the ~4.5 km blind zone exceeds the 3 km max range and
pollutes the RX window. IDLE now branches straight to SHORT_CHIRP
when range_mode == RP_RANGE_MODE_3KM. host_range_mode is passed from
radar_system_top through radar_transmitter, CDC'd per-bit from
clk_100m to clk_120m_dac (coherency-safe: reserved codes are clamped
at the source so only bit[0] toggles).

S-3: opcode 0x20 now clamps reserved range-mode codes (2'b10, 2'b11)
to the 3 km default so a garbled host write cannot silently enable
long-range TX behaviour.

Regression: tb_chirp_controller adds a 3 km-mode group (5 checks)
verifying IDLE->SHORT_CHIRP skip path and DONE after CHIRP_MAX short
chirps; tb_system_e2e G14 labels updated for clamped reserved codes.
32/32 regression PASS (50/50 on chirp TB).
This commit is contained in:
Jason
2026-04-22 20:26:43 +05:45
parent 27c9c22ad2
commit 6af79f9c74
5 changed files with 228 additions and 105 deletions
+22 -4
View File
@@ -10,6 +10,10 @@ module plfm_chirp_controller_enhanced (
input wire new_elevation,
input wire new_azimuth,
input wire mixers_enable,
// Range mode (CDC-synchronized into clk_120m by the caller).
// 2'b00 = 3 km — short chirps only (skip LONG_CHIRP/LONG_LISTEN)
// 2'b01 = long-range — dual chirp (first half long, second half short)
input wire [1:0] range_mode,
output reg [7:0] chirp_data,
output reg chirp_valid,
output wire new_chirp_frame,
@@ -80,7 +84,11 @@ reg [7:0] long_chirp_rd_data;
assign chirp__toggling = new_chirp;
assign elevation__toggling = new_elevation;
assign azimuth__toggling = new_azimuth;
assign new_chirp_frame = (current_state == IDLE && next_state == LONG_CHIRP);
// new_chirp_frame fires on IDLE -> first active state (long or short
// depending on range_mode).
assign new_chirp_frame = (current_state == IDLE &&
(next_state == LONG_CHIRP ||
next_state == SHORT_CHIRP));
// Mixer TX/RX sequencing mutually exclusive based on chirp FSM state.
// TX mixer active during chirp transmission, RX mixer during listen.
@@ -179,10 +187,20 @@ end
always @(*) begin
case (current_state)
IDLE: begin
if (chirp__toggling && mixers_enable)
next_state = LONG_CHIRP;
else
// 3 km mode skips the long-chirp half entirely LONG_CHIRP's
// 4500 m blind zone exceeds the 3 km max range, so long chirps
// would just pollute the receive window. Go straight to
// SHORT_CHIRP and let the SHORT_LISTEN -> DONE guard
// (chirp_counter == CHIRP_MAX-1) terminate after CHIRP_MAX
// short chirps.
if (chirp__toggling && mixers_enable) begin
if (range_mode == `RP_RANGE_MODE_3KM)
next_state = SHORT_CHIRP;
else
next_state = LONG_CHIRP;
end else begin
next_state = IDLE;
end
end
LONG_CHIRP: begin
+27 -19
View File
@@ -182,12 +182,12 @@ wire [31:0] rx_doppler_output;
wire rx_doppler_valid;
wire [4:0] rx_doppler_bin;
wire [`RP_RANGE_BIN_BITS-1:0] rx_range_bin;
wire [31:0] rx_range_profile;
wire rx_range_valid;
wire [15:0] rx_range_profile_decimated;
wire rx_range_profile_decimated_valid;
wire [15:0] rx_doppler_real;
wire [15:0] rx_doppler_imag;
wire [31:0] rx_range_profile;
wire rx_range_valid;
wire [15:0] rx_range_profile_decimated;
wire rx_range_profile_decimated_valid;
wire [15:0] rx_doppler_real;
wire [15:0] rx_doppler_imag;
wire rx_doppler_data_valid;
reg rx_detect_flag; // Threshold detection result (was rx_cfar_detection)
reg rx_detect_valid; // Detection valid pulse (was rx_cfar_valid)
@@ -506,7 +506,10 @@ radar_transmitter tx_inst (
.stm32_cs_adar2_1v8(stm32_cs_adar2_1v8),
.stm32_cs_adar3_1v8(stm32_cs_adar3_1v8),
.stm32_cs_adar4_1v8(stm32_cs_adar4_1v8),
// Host range mode (clk_100m domain; CDC'd inside radar_transmitter)
.host_range_mode(host_range_mode),
// Beam Position Tracking
.current_elevation(tx_current_elevation),
.current_azimuth(tx_current_azimuth),
@@ -542,12 +545,12 @@ radar_receiver_final rx_inst (
.doppler_bin(rx_doppler_bin),
.range_bin(rx_range_bin),
// Range-profile outputs
.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),
.decimated_range_mag_out(rx_range_profile_decimated),
.decimated_range_valid_out(rx_range_profile_decimated_valid),
// Range-profile outputs
.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),
.decimated_range_mag_out(rx_range_profile_decimated),
.decimated_range_valid_out(rx_range_profile_decimated_valid),
.host_mode(host_radar_mode),
.host_trigger(host_trigger_pulse),
@@ -731,10 +734,10 @@ end
// DATA PACKING FOR USB
// ============================================================================
// USB range profile must match the advertised 512-bin frame payload, so source it
// from the decimated range stream that feeds Doppler rather than raw MF samples.
assign usb_range_profile = {16'd0, rx_range_profile_decimated};
assign usb_range_valid = rx_range_profile_decimated_valid;
// USB range profile must match the advertised 512-bin frame payload, so source it
// from the decimated range stream that feeds Doppler rather than raw MF samples.
assign usb_range_profile = {16'd0, rx_range_profile_decimated};
assign usb_range_valid = rx_range_profile_decimated_valid;
assign usb_doppler_real = rx_doppler_real;
assign usb_doppler_imag = rx_doppler_imag;
@@ -963,7 +966,7 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
host_radar_mode <= 2'b01; // Default: auto-scan
host_trigger_pulse <= 1'b0;
host_detect_threshold <= 16'd10000; // Default threshold
host_stream_control <= `RP_STREAM_CTRL_DEFAULT; // Default: all streams, mag-only mode
host_stream_control <= `RP_STREAM_CTRL_DEFAULT; // Default: all streams, mag-only mode
host_gain_shift <= 4'd0; // Default: pass-through (no gain change)
// Gap 2: chirp timing defaults (match radar_mode_controller parameters)
host_long_chirp_cycles <= 16'd3000;
@@ -1024,7 +1027,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
end
end
8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain
8'h20: host_range_mode <= usb_cmd_value[1:0]; // Range mode
// Range mode: clamp reserved codes (2'b10, 2'b11) to the safe
// 3 km default so a garbled host write cannot silently enable
// long-range TX behaviour.
8'h20: host_range_mode <= (usb_cmd_value[1:0] > 2'b01)
? `RP_RANGE_MODE_3KM
: usb_cmd_value[1:0];
// CFAR configuration opcodes
8'h21: host_cfar_guard <= usb_cmd_value[3:0];
8'h22: host_cfar_train <= usb_cmd_value[4:0];
+104 -78
View File
@@ -23,60 +23,65 @@ module radar_transmitter(
input wire clk_100m, // System clock
input wire clk_120m_dac, // 120MHz DAC clock
input wire reset_n, // Reset synchronized to clk_120m_dac
input wire reset_100m_n, // Reset synchronized to clk_100m (for edge detectors/CDC)
// DAC Interface
output wire [7:0] dac_data,
output wire dac_clk,
output wire dac_sleep,
output wire rx_mixer_en,
input wire reset_100m_n, // Reset synchronized to clk_100m (for edge detectors/CDC)
// DAC Interface
output wire [7:0] dac_data,
output wire dac_clk,
output wire dac_sleep,
output wire rx_mixer_en,
output wire tx_mixer_en,
// STM32 Control Interface
input wire stm32_new_chirp,
input wire stm32_new_elevation,
input wire stm32_new_azimuth,
// STM32 Control Interface
input wire stm32_new_chirp,
input wire stm32_new_elevation,
input wire stm32_new_azimuth,
input wire stm32_mixers_enable,
// Range mode from host (clk_100m domain, opcode 0x20). CDC'd to clk_120m_dac
// internally and fed to plfm_chirp_controller_enhanced so 3 km mode skips
// the long-chirp half of the waveform entirely.
input wire [1:0] host_range_mode,
output wire fpga_rf_switch,
// ADAR1000 Control Interface
output wire adar_tx_load_1,
output wire adar_rx_load_1,
output wire adar_tx_load_2,
output wire adar_rx_load_2,
output wire adar_tx_load_3,
output wire adar_rx_load_3,
output wire adar_tx_load_4,
output wire adar_rx_load_4,
output wire adar_tr_1,
output wire adar_tr_2,
output wire adar_tr_3,
output wire adar_tr_4,
// Level Shifter SPI Interface (STM32F7 to ADAR1000)
input wire stm32_sclk_3v3,
input wire stm32_mosi_3v3,
output wire stm32_miso_3v3,
input wire stm32_cs_adar1_3v3,
input wire stm32_cs_adar2_3v3,
input wire stm32_cs_adar3_3v3,
input wire stm32_cs_adar4_3v3,
output wire stm32_sclk_1v8,
output wire stm32_mosi_1v8,
input wire stm32_miso_1v8,
output wire stm32_cs_adar1_1v8,
output wire stm32_cs_adar2_1v8,
output wire stm32_cs_adar3_1v8,
// ADAR1000 Control Interface
output wire adar_tx_load_1,
output wire adar_rx_load_1,
output wire adar_tx_load_2,
output wire adar_rx_load_2,
output wire adar_tx_load_3,
output wire adar_rx_load_3,
output wire adar_tx_load_4,
output wire adar_rx_load_4,
output wire adar_tr_1,
output wire adar_tr_2,
output wire adar_tr_3,
output wire adar_tr_4,
// Level Shifter SPI Interface (STM32F7 to ADAR1000)
input wire stm32_sclk_3v3,
input wire stm32_mosi_3v3,
output wire stm32_miso_3v3,
input wire stm32_cs_adar1_3v3,
input wire stm32_cs_adar2_3v3,
input wire stm32_cs_adar3_3v3,
input wire stm32_cs_adar4_3v3,
output wire stm32_sclk_1v8,
output wire stm32_mosi_1v8,
input wire stm32_miso_1v8,
output wire stm32_cs_adar1_1v8,
output wire stm32_cs_adar2_1v8,
output wire stm32_cs_adar3_1v8,
output wire stm32_cs_adar4_1v8,
// Beam Position Tracking
output wire [5:0] current_elevation,
output wire [5:0] current_azimuth,
// Beam Position Tracking
output wire [5:0] current_elevation,
output wire [5:0] current_azimuth,
output wire [5:0] current_chirp,
output wire new_chirp_frame
output wire new_chirp_frame
);
@@ -143,6 +148,26 @@ always @(posedge clk_120m_dac or negedge reset_n) begin
end
assign new_chirp_pulse_120m = chirp_toggle_120m ^ chirp_toggle_120m_prev;
// Sync host_range_mode (clk_100m level) to clk_120m_dac domain.
// Only bit[0] toggles between the two valid codes (2'b00 / 2'b01) since
// reserved codes are clamped at the source, so per-bit 2FF synchronization
// has no coherency hazard.
wire [1:0] range_mode_120m;
cdc_single_bit #(.STAGES(2)) cdc_range_mode_bit0 (
.src_clk(clk_100m),
.dst_clk(clk_120m_dac),
.reset_n(reset_n),
.src_signal(host_range_mode[0]),
.dst_signal(range_mode_120m[0])
);
cdc_single_bit #(.STAGES(2)) cdc_range_mode_bit1 (
.src_clk(clk_100m),
.dst_clk(clk_120m_dac),
.reset_n(reset_n),
.src_signal(host_range_mode[1]),
.dst_signal(range_mode_120m[1])
);
// Sync stm32_mixers_enable (async GPIO level) to clk_120m_dac domain
cdc_single_bit #(.STAGES(3)) cdc_mixers_en_120m (
.src_clk(clk_100m), // Treat as pseudo-source (GPIO is async)
@@ -150,7 +175,7 @@ cdc_single_bit #(.STAGES(3)) cdc_mixers_en_120m (
.reset_n(reset_n),
.src_signal(stm32_mixers_enable),
.dst_signal(mixers_enable_120m)
);
);
// CDC synchronizers: async STM32 GPIO inputs -> clk_100m domain
// These prevent metastability in the edge detectors. Without these,
@@ -201,7 +226,7 @@ edge_detector_enhanced azimuth_edge (
.reset_n(reset_100m_n),
.signal_in(stm32_new_azimuth_sync),
.rising_falling_edge(new_azimuth_pulse)
);
);
// Enhanced PLFM Chirp Generation
plfm_chirp_controller_enhanced plfm_chirp_inst (
@@ -212,38 +237,39 @@ plfm_chirp_controller_enhanced plfm_chirp_inst (
.new_elevation(new_elevation_pulse),
.new_azimuth(new_azimuth_pulse),
.new_chirp_frame(new_chirp_frame),
.mixers_enable(mixers_enable_120m), // CDC-synchronized level in clk_120m domain
.chirp_data(chirp_data),
.chirp_valid(chirp_valid),
.chirp_done(chirp_sequence_done),
.rf_switch_ctrl(fpga_rf_switch),
.rx_mixer_en(rx_mixer_en),
.tx_mixer_en(tx_mixer_en),
.adar_tx_load_1(adar_tx_load_1),
.adar_rx_load_1(adar_rx_load_1),
.adar_tx_load_2(adar_tx_load_2),
.adar_rx_load_2(adar_rx_load_2),
.adar_tx_load_3(adar_tx_load_3),
.adar_rx_load_3(adar_rx_load_3),
.adar_tx_load_4(adar_tx_load_4),
.adar_rx_load_4(adar_rx_load_4),
.adar_tr_1(adar_tr_1),
.adar_tr_2(adar_tr_2),
.adar_tr_3(adar_tr_3),
.adar_tr_4(adar_tr_4),
.elevation_counter(current_elevation),
.azimuth_counter(current_azimuth),
.chirp_counter(current_chirp)
.mixers_enable(mixers_enable_120m), // CDC-synchronized level in clk_120m domain
.range_mode(range_mode_120m), // CDC-synchronized range mode in clk_120m domain
.chirp_data(chirp_data),
.chirp_valid(chirp_valid),
.chirp_done(chirp_sequence_done),
.rf_switch_ctrl(fpga_rf_switch),
.rx_mixer_en(rx_mixer_en),
.tx_mixer_en(tx_mixer_en),
.adar_tx_load_1(adar_tx_load_1),
.adar_rx_load_1(adar_rx_load_1),
.adar_tx_load_2(adar_tx_load_2),
.adar_rx_load_2(adar_rx_load_2),
.adar_tx_load_3(adar_tx_load_3),
.adar_rx_load_3(adar_rx_load_3),
.adar_tx_load_4(adar_tx_load_4),
.adar_rx_load_4(adar_rx_load_4),
.adar_tr_1(adar_tr_1),
.adar_tr_2(adar_tr_2),
.adar_tr_3(adar_tr_3),
.adar_tr_4(adar_tr_4),
.elevation_counter(current_elevation),
.azimuth_counter(current_azimuth),
.chirp_counter(current_chirp)
);
// Enhanced DAC Interface
dac_interface_enhanced dac_interface_inst (
.clk_120m(clk_120m_dac),
.reset_n(reset_n),
.chirp_data(chirp_data),
.chirp_valid(chirp_valid),
.dac_data(dac_data),
.dac_clk(dac_clk),
.dac_sleep(dac_sleep)
// Enhanced DAC Interface
dac_interface_enhanced dac_interface_inst (
.clk_120m(clk_120m_dac),
.reset_n(reset_n),
.chirp_data(chirp_data),
.chirp_valid(chirp_valid),
.dac_data(dac_data),
.dac_clk(dac_clk),
.dac_sleep(dac_sleep)
);
endmodule
+63 -1
View File
@@ -42,6 +42,7 @@ always #5 clk_100m = ~clk_100m;
// =========================================================================
reg new_chirp, new_elevation, new_azimuth;
reg mixers_enable;
reg [1:0] range_mode; // 2'b00 = 3 km (short-only), 2'b01 = long-range (dual)
wire [7:0] chirp_data;
wire chirp_valid;
@@ -78,6 +79,7 @@ plfm_chirp_controller_enhanced #(
.new_elevation(new_elevation),
.new_azimuth(new_azimuth),
.mixers_enable(mixers_enable),
.range_mode(range_mode),
.chirp_data(chirp_data),
.chirp_valid(chirp_valid),
.new_chirp_frame(new_chirp_frame),
@@ -184,7 +186,8 @@ initial begin
new_elevation = 0;
new_azimuth = 0;
mixers_enable = 0;
range_mode = 2'b01; // Default: long-range (dual) matches existing tests
$display("");
$display("============================================================");
$display(" CHIRP CONTROLLER TESTBENCH");
@@ -493,6 +496,65 @@ initial begin
check("ADAR load pins: adar_tx_load_1 is 0", adar_tx_load_1 == 1'b0);
check("ADAR load pins: adar_rx_load_1 is 0", adar_rx_load_1 == 1'b0);
// =====================================================================
// TEST GROUP 8: RANGE MODE — 3 KM SHORT-ONLY PATH (C-1)
//
// Bug: plfm_chirp_controller_enhanced was not range_mode-aware; the
// FSM always ran LONG_CHIRP/LONG_LISTEN for CHIRP_MAX/2 chirps even
// when the host had selected 3 km mode. LONG_CHIRP's ~4.5 km blind
// zone exceeds the 3 km max, so those long chirps pollute the RX
// window. Fix: IDLE branches to SHORT_CHIRP directly when
// range_mode == RP_RANGE_MODE_3KM.
//
// These checks verify the skip path: no LONG_CHIRP ever entered, FSM
// reaches DONE after CHIRP_MAX short chirps.
// =====================================================================
$display("--- Group 8: Range Mode 3 km (C-1) ---");
// Full reset into 3 km mode.
reset_n = 0;
mixers_enable = 0;
new_chirp = 0;
range_mode = 2'b00; // 3 km short-only
#100;
reset_n = 1;
@(posedge clk_120m);
mixers_enable = 1;
@(posedge clk_120m);
new_chirp = 1;
@(posedge clk_120m);
// T8.1: Skip LONG_CHIRP, enter SHORT_CHIRP directly from IDLE.
wait_for_state(3'b100, 10);
check("3 km mode: IDLE -> SHORT_CHIRP (skips LONG_CHIRP)",
dut.current_state == 3'b100);
// T8.2: FSM must never have entered any LONG_* state in this frame.
// (dut.current_state is sampled now; a transient visit would have
// been caught by the wait_for_state above landing in 3'b001/3'b010
// instead.)
check("3 km mode: FSM not in LONG_CHIRP",
dut.current_state != 3'b001);
check("3 km mode: FSM not in LONG_LISTEN",
dut.current_state != 3'b010);
// T8.3: After CHIRP_MAX short chirps, reach DONE. No GUARD_TIME in the
// 3 km path (GUARD bridges long->short; not needed here).
wait_for_state(3'b110,
(T2_SAMPLES + T2_RADAR_LISTENING) * CHIRP_MAX + 50);
check("3 km mode: reaches DONE after CHIRP_MAX short chirps",
dut.current_state == 3'b110);
// T8.4: chirp_counter cleared at DONE (same invariant as C-3).
new_chirp = 0;
@(posedge clk_120m);
check("3 km mode: chirp_counter reset to 0 after DONE",
chirp_counter == 6'd0);
// Restore default for any future tests.
range_mode = 2'b01;
// =====================================================================
// SUMMARY
// =====================================================================
+12 -3
View File
@@ -1166,10 +1166,19 @@ initial begin
check(dut.host_range_mode == 2'b01,
"G14.1: Opcode 0x20 -> host_range_mode = 2'b01 (long-range)");
// G14.2: Set range_mode to reserved value 0x02 (permissive: stored as-is)
// G14.2: Reserved value 0x02 must be clamped to 3 km (safe default)
// so a garbled host write cannot silently enable long-range TX.
bfm_send_cmd(8'h20, 8'h00, 16'h0002);
check(dut.host_range_mode == 2'b10,
"G14.2: Opcode 0x20 -> host_range_mode = 2'b10 (reserved)");
check(dut.host_range_mode == 2'b00,
"G14.2: Opcode 0x20 reserved=0x02 clamped to 2'b00 (3 km safe default)");
// G14.2b: Reserved value 0x03 also clamps to 3 km.
bfm_send_cmd(8'h20, 8'h00, 16'h0003);
check(dut.host_range_mode == 2'b00,
"G14.2b: Opcode 0x20 reserved=0x03 clamped to 2'b00 (3 km safe default)");
// Restore to a known-valid value before G14.3 asserts reset-to-3km.
bfm_send_cmd(8'h20, 8'h00, 16'h0001);
// G14.3: Restore range_mode to 3 km (0x00)
bfm_send_cmd(8'h20, 8'h00, 16'h0000);