Merge pull request #42 from joyshmitz/fix/dual-subframe-consistency

fix(rtl,gui,cosim,formal): adapt surrounding files for dual 16-pt FFT
This commit is contained in:
NawfalMotii79
2026-04-06 21:21:11 +01:00
committed by GitHub
8 changed files with 218 additions and 215 deletions
@@ -4,24 +4,24 @@ cover
[options] [options]
bmc: mode bmc bmc: mode bmc
bmc: depth 150 bmc: depth 512
cover: mode cover cover: mode cover
cover: depth 150 cover: depth 1024
[engines] [engines]
smtbmc z3 smtbmc z3
[script] [script]
read_verilog -formal doppler_processor.v read_verilog -formal doppler_processor.v
read_verilog -formal xfft_32.v read_verilog -formal xfft_16.v
read_verilog -formal fft_engine.v read_verilog -formal fft_engine.v
read_verilog -formal fv_doppler_processor.v read_verilog -formal fv_doppler_processor.v
prep -top fv_doppler_processor prep -top fv_doppler_processor
[files] [files]
../doppler_processor.v ../doppler_processor.v
../xfft_32.v ../xfft_16.v
../fft_engine.v ../fft_engine.v
../fft_twiddle_32.mem ../fft_twiddle_16.mem
../fft_twiddle_1024.mem ../fft_twiddle_1024.mem
fv_doppler_processor.v fv_doppler_processor.v
@@ -8,7 +8,8 @@
// Single-clock design: clk is an input wire, async2sync handles async reset. // Single-clock design: clk is an input wire, async2sync handles async reset.
// Each formal step = one clock edge. // Each formal step = one clock edge.
// //
// Parameters reduced: RANGE_BINS=4, CHIRPS_PER_FRAME=4, CHIRPS_PER_SUBFRAME=2, DOPPLER_FFT_SIZE=2. // Parameters: RANGE_BINS reduced for tractability, but the FFT/sub-frame size
// remains 16 so the wrapper matches the real xfft_16 interface.
// Includes full xfft_16 and fft_engine sub-modules. // Includes full xfft_16 and fft_engine sub-modules.
// //
// Focus: memory address bounds (highest-value finding) and state encoding. // Focus: memory address bounds (highest-value finding) and state encoding.
@@ -17,12 +18,12 @@ module fv_doppler_processor (
input wire clk input wire clk
); );
// Reduced parameters for tractable BMC // Only RANGE_BINS is reduced; the FFT wrapper still expects 16 samples.
localparam RANGE_BINS = 4; localparam RANGE_BINS = 2;
localparam CHIRPS_PER_FRAME = 4; localparam CHIRPS_PER_FRAME = 32;
localparam CHIRPS_PER_SUBFRAME = 2; // Dual sub-frame: 2 chirps per sub-frame localparam CHIRPS_PER_SUBFRAME = 16;
localparam DOPPLER_FFT_SIZE = 2; // FFT size matches sub-frame size localparam DOPPLER_FFT_SIZE = 16;
localparam MEM_DEPTH = RANGE_BINS * CHIRPS_PER_FRAME; // 16 localparam MEM_DEPTH = RANGE_BINS * CHIRPS_PER_FRAME;
// State encoding (mirrors DUT localparams) // State encoding (mirrors DUT localparams)
localparam S_IDLE = 3'b000; localparam S_IDLE = 3'b000;
@@ -130,9 +131,11 @@ module fv_doppler_processor (
assume(!data_valid); assume(!data_valid);
end end
// new_chirp_frame must be a clean pulse (not during active processing) // new_chirp_frame may assert during accumulation at the 16-chirp boundary.
// Only suppress it during FFT-processing states.
always @(posedge clk) begin always @(posedge clk) begin
if (reset_n && state != S_IDLE) if (reset_n && (state == S_PRE_READ || state == S_LOAD_FFT ||
state == S_FFT_WAIT || state == S_OUTPUT))
assume(!new_chirp_frame); assume(!new_chirp_frame);
end end
@@ -201,11 +204,17 @@ module fv_doppler_processor (
end end
// ================================================================ // ================================================================
// COVER 1: Complete processing of all range bins // COVER 1: Complete processing of all range bins after a full frame was
// actually accumulated.
// ================================================================ // ================================================================
reg f_seen_full;
initial f_seen_full = 1'b0;
always @(posedge clk)
if (frame_buffer_full) f_seen_full <= 1'b1;
always @(posedge clk) begin always @(posedge clk) begin
if (reset_n) if (reset_n)
cover(frame_complete && f_past_valid); cover(frame_complete && f_seen_full);
end end
// ================================================================ // ================================================================
@@ -6,7 +6,7 @@ cover
bmc: mode bmc bmc: mode bmc
bmc: depth 200 bmc: depth 200
cover: mode cover cover: mode cover
cover: depth 200 cover: depth 600
[engines] [engines]
smtbmc z3 smtbmc z3
@@ -66,13 +66,15 @@ module fv_radar_mode_controller (
(* anyseq *) wire stm32_new_azimuth; (* anyseq *) wire stm32_new_azimuth;
(* anyseq *) wire trigger; (* anyseq *) wire trigger;
// Gap 2: Formal cfg_* inputs — solver-driven for exhaustive coverage // Runtime config inputs are pinned to the reduced localparams so this
(* anyseq *) wire [15:0] cfg_long_chirp_cycles; // wrapper proves one tractable configuration. It does not sweep the full
(* anyseq *) wire [15:0] cfg_long_listen_cycles; // runtime-configurable cfg_* space.
(* anyseq *) wire [15:0] cfg_guard_cycles; wire [15:0] cfg_long_chirp_cycles = LONG_CHIRP_CYCLES;
(* anyseq *) wire [15:0] cfg_short_chirp_cycles; wire [15:0] cfg_long_listen_cycles = LONG_LISTEN_CYCLES;
(* anyseq *) wire [15:0] cfg_short_listen_cycles; wire [15:0] cfg_guard_cycles = GUARD_CYCLES;
(* anyseq *) wire [5:0] cfg_chirps_per_elev; wire [15:0] cfg_short_chirp_cycles = SHORT_CHIRP_CYCLES;
wire [15:0] cfg_short_listen_cycles = SHORT_LISTEN_CYCLES;
wire [5:0] cfg_chirps_per_elev = CHIRPS_PER_ELEVATION;
// ================================================================ // ================================================================
// DUT outputs // DUT outputs
@@ -109,7 +111,8 @@ module fv_radar_mode_controller (
.stm32_new_elevation(stm32_new_elevation), .stm32_new_elevation(stm32_new_elevation),
.stm32_new_azimuth (stm32_new_azimuth), .stm32_new_azimuth (stm32_new_azimuth),
.trigger (trigger), .trigger (trigger),
// Gap 2: Runtime-configurable timing inputs // Runtime-configurable timing ports, bound here to the reduced wrapper
// constants for tractable proof depth.
.cfg_long_chirp_cycles (cfg_long_chirp_cycles), .cfg_long_chirp_cycles (cfg_long_chirp_cycles),
.cfg_long_listen_cycles (cfg_long_listen_cycles), .cfg_long_listen_cycles (cfg_long_listen_cycles),
.cfg_guard_cycles (cfg_guard_cycles), .cfg_guard_cycles (cfg_guard_cycles),
@@ -181,7 +184,7 @@ module fv_radar_mode_controller (
// ================================================================ // ================================================================
always @(posedge clk) begin always @(posedge clk) begin
if (reset_n && f_past_valid) begin if (reset_n && f_past_valid) begin
if ($past(mode) == 2'b10 && if ($past(mode) == 2'b10 && mode == 2'b10 &&
$past(scan_state) == S_LONG_LISTEN && $past(scan_state) == S_LONG_LISTEN &&
$past(timer) == LONG_LISTEN_CYCLES - 1) $past(timer) == LONG_LISTEN_CYCLES - 1)
assert(scan_state == S_IDLE); assert(scan_state == S_IDLE);
@@ -202,11 +205,11 @@ module fv_radar_mode_controller (
end end
// ================================================================ // ================================================================
// COVER 1: Full scan completes (scan_complete pulses) // COVER 1: Full auto-scan completes
// ================================================================ // ================================================================
always @(posedge clk) begin always @(posedge clk) begin
if (reset_n) if (reset_n)
cover(scan_complete); cover(scan_complete && mode == 2'b01);
end end
// ================================================================ // ================================================================
+18 -12
View File
@@ -217,10 +217,11 @@ reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32)
reg host_status_request; // Opcode 0xFF (self-clearing pulse) reg host_status_request; // Opcode 0xFF (self-clearing pulse)
// Fix 4: Doppler/chirps mismatch protection // Fix 4: Doppler/chirps mismatch protection
// DOPPLER_FFT_SIZE is compile-time (32). If host sets chirps_per_elev to a // DOPPLER_FRAME_CHIRPS is the fixed chirp count expected by the staggered-PRI
// Doppler path (16 long + 16 short). If host sets chirps_per_elev to a
// different value, Doppler accumulation is corrupted. Clamp at command decode // different value, Doppler accumulation is corrupted. Clamp at command decode
// and flag the mismatch so the host knows. // and flag the mismatch so the host knows.
localparam DOPPLER_FFT_SIZE = 32; // Must match doppler_processor parameter localparam DOPPLER_FRAME_CHIRPS = 32; // Total chirps per Doppler frame
reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size reg chirps_mismatch_error; // Set if host tried to set chirps != FFT size
// Fix 7: Range-mode register (opcode 0x20) // Fix 7: Range-mode register (opcode 0x20)
@@ -532,16 +533,21 @@ assign rx_doppler_data_valid = rx_doppler_valid;
// ============================================================================ // ============================================================================
// DC NOTCH FILTER (post-Doppler-FFT, pre-CFAR) // DC NOTCH FILTER (post-Doppler-FFT, pre-CFAR)
// ============================================================================ // ============================================================================
// Zeros out Doppler bins within ±host_dc_notch_width of DC (bin 0). // Zeros out Doppler bins within ±host_dc_notch_width of DC for BOTH
// In a 32-point FFT, DC is bin 0; negative Doppler wraps to bins 31,30,... // sub-frames in the dual 16-pt FFT architecture.
// notch_width=1 → zero bins {0}. notch_width=2 → zero bins {0,1,31}. etc. // doppler_bin[4:0] = {sub_frame, bin[3:0]}:
// Sub-frame 0: bins 0-15, DC = bin 0, wrap = bin 15
// Sub-frame 1: bins 16-31, DC = bin 16, wrap = bin 31
// notch_width=1 → zero bins {0,16}. notch_width=2 → zero bins
// {0,1,15,16,17,31}. etc.
// When host_dc_notch_width=0: pass-through (no zeroing). // When host_dc_notch_width=0: pass-through (no zeroing).
wire dc_notch_active; wire dc_notch_active;
wire [4:0] dop_bin_unsigned = rx_doppler_bin; wire [4:0] dop_bin_unsigned = rx_doppler_bin;
wire [3:0] bin_within_sf = dop_bin_unsigned[3:0];
assign dc_notch_active = (host_dc_notch_width != 3'd0) && assign dc_notch_active = (host_dc_notch_width != 3'd0) &&
(dop_bin_unsigned < {2'b0, host_dc_notch_width} || (bin_within_sf < {1'b0, host_dc_notch_width} ||
dop_bin_unsigned > (5'd31 - {2'b0, host_dc_notch_width} + 5'd1)); bin_within_sf > (4'd15 - {1'b0, host_dc_notch_width} + 4'd1));
// Notched Doppler data: zero I/Q when in notch zone, pass through otherwise // Notched Doppler data: zero I/Q when in notch zone, pass through otherwise
wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output; wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output;
@@ -814,18 +820,18 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
8'h13: host_short_chirp_cycles <= usb_cmd_value; 8'h13: host_short_chirp_cycles <= usb_cmd_value;
8'h14: host_short_listen_cycles <= usb_cmd_value; 8'h14: host_short_listen_cycles <= usb_cmd_value;
8'h15: begin 8'h15: begin
// Fix 4: Clamp chirps_per_elev to DOPPLER_FFT_SIZE. // Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size.
// If host requests a different value, clamp and set error flag. // If host requests a different value, clamp and set error flag.
if (usb_cmd_value[5:0] > DOPPLER_FFT_SIZE[5:0]) begin if (usb_cmd_value[5:0] > DOPPLER_FRAME_CHIRPS[5:0]) begin
host_chirps_per_elev <= DOPPLER_FFT_SIZE[5:0]; host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0];
chirps_mismatch_error <= 1'b1; chirps_mismatch_error <= 1'b1;
end else if (usb_cmd_value[5:0] == 6'd0) begin end else if (usb_cmd_value[5:0] == 6'd0) begin
host_chirps_per_elev <= DOPPLER_FFT_SIZE[5:0]; host_chirps_per_elev <= DOPPLER_FRAME_CHIRPS[5:0];
chirps_mismatch_error <= 1'b1; chirps_mismatch_error <= 1'b1;
end else begin end else begin
host_chirps_per_elev <= usb_cmd_value[5:0]; host_chirps_per_elev <= usb_cmd_value[5:0];
// Clear error only if value matches FFT size exactly // Clear error only if value matches FFT size exactly
chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FFT_SIZE[5:0]); chirps_mismatch_error <= (usb_cmd_value[5:0] != DOPPLER_FRAME_CHIRPS[5:0]);
end end
end end
8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain 8'h16: host_gain_shift <= usb_cmd_value[3:0]; // Fix 3: digital gain
@@ -76,23 +76,20 @@ FFT_DATA_W = 16
FFT_INTERNAL_W = 32 FFT_INTERNAL_W = 32
FFT_TWIDDLE_W = 16 FFT_TWIDDLE_W = 16
# Doppler # Doppler — dual 16-pt FFT architecture
DOPPLER_FFT_SIZE = 32 DOPPLER_FFT_SIZE = 16 # per sub-frame
DOPPLER_TOTAL_BINS = 32 # total output (2 sub-frames x 16)
DOPPLER_RANGE_BINS = 64 DOPPLER_RANGE_BINS = 64
DOPPLER_CHIRPS = 32 DOPPLER_CHIRPS = 32
CHIRPS_PER_SUBFRAME = 16
DOPPLER_WINDOW_TYPE = 0 # Hamming DOPPLER_WINDOW_TYPE = 0 # Hamming
# Hamming window coefficients from doppler_processor.v (Q15) # 16-point Hamming window coefficients from doppler_processor.v (Q15)
HAMMING_Q15 = [ HAMMING_Q15 = [
0x0800, 0x0862, 0x09CB, 0x0C3B, 0x0A3D, 0x0E5C, 0x1B6D, 0x3088,
0x0FB2, 0x142F, 0x19B2, 0x2039, 0x4B33, 0x6573, 0x7642, 0x7F62,
0x27C4, 0x3050, 0x39DB, 0x4462, 0x7F62, 0x7642, 0x6573, 0x4B33,
0x4FE3, 0x5C5A, 0x69C4, 0x781D, 0x3088, 0x1B6D, 0x0E5C, 0x0A3D,
0x7FFF, # Peak
0x781D, 0x69C4, 0x5C5A, 0x4FE3,
0x4462, 0x39DB, 0x3050, 0x27C4,
0x2039, 0x19B2, 0x142F, 0x0FB2,
0x0C3B, 0x09CB, 0x0862,
] ]
# ADI dataset parameters # ADI dataset parameters
@@ -652,108 +649,109 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
# =========================================================================== # ===========================================================================
# Stage 3: Doppler FFT (32-point with Hamming window, bit-accurate) # Stage 3: Doppler FFT (dual 16-point with Hamming window, bit-accurate)
# =========================================================================== # ===========================================================================
def run_doppler_fft(range_data_i, range_data_q, twiddle_file_32=None): def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None):
""" """
Bit-accurate Doppler processor matching doppler_processor.v. Bit-accurate Doppler processor matching doppler_processor.v (dual 16-pt FFT).
Input: range_data_i/q shape (DOPPLER_CHIRPS, FFT_SIZE) — 16-bit signed Input: range_data_i/q shape (DOPPLER_CHIRPS, FFT_SIZE) — 16-bit signed
Only first DOPPLER_RANGE_BINS columns are processed. Only first DOPPLER_RANGE_BINS columns are processed.
Output: doppler_map_i/q shape (DOPPLER_RANGE_BINS, DOPPLER_FFT_SIZE) — 16-bit signed Output: doppler_map_i/q shape (DOPPLER_RANGE_BINS, DOPPLER_TOTAL_BINS) — 16-bit signed
Pipeline per range bin: Architecture per range bin:
1. Read 32 chirps for this range bin Sub-frame 0 (long PRI): chirps 0..15 → 16-pt Hamming → 16-pt FFT → bins 0-15
2. Apply Hamming window (Q15 multiply + round >>> 15) Sub-frame 1 (short PRI): chirps 16..31 → 16-pt Hamming → 16-pt FFT → bins 16-31
3. 32-point FFT
""" """
n_chirps = DOPPLER_CHIRPS n_chirps = DOPPLER_CHIRPS
n_range = DOPPLER_RANGE_BINS n_range = DOPPLER_RANGE_BINS
n_fft = DOPPLER_FFT_SIZE n_fft = DOPPLER_FFT_SIZE
n_total = DOPPLER_TOTAL_BINS
n_sf = CHIRPS_PER_SUBFRAME
print(f"[DOPPLER] Processing {n_range} range bins x {n_chirps} chirps → {n_fft}-point FFT") print(f"[DOPPLER] Processing {n_range} range bins x {n_chirps} chirps → dual {n_fft}-point FFT")
# Build Hamming window as signed 16-bit # Build 16-point Hamming window as signed 16-bit
hamming = np.array([int(v) for v in HAMMING_Q15], dtype=np.int64) hamming = np.array([int(v) for v in HAMMING_Q15], dtype=np.int64)
assert len(hamming) == n_fft, f"Hamming length {len(hamming)} != {n_fft}" assert len(hamming) == n_fft, f"Hamming length {len(hamming)} != {n_fft}"
# Build 32-point twiddle factors # Build 16-point twiddle factors
if twiddle_file_32 and os.path.exists(twiddle_file_32): if twiddle_file_16 and os.path.exists(twiddle_file_16):
cos_rom_32 = load_twiddle_rom(twiddle_file_32) cos_rom_16 = load_twiddle_rom(twiddle_file_16)
else: else:
cos_rom_32 = np.round(32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)).astype(np.int64) cos_rom_16 = np.round(32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)).astype(np.int64)
doppler_map_i = np.zeros((n_range, n_fft), dtype=np.int64) LOG2N_16 = 4
doppler_map_q = np.zeros((n_range, n_fft), dtype=np.int64) doppler_map_i = np.zeros((n_range, n_total), dtype=np.int64)
doppler_map_q = np.zeros((n_range, n_total), dtype=np.int64)
for rbin in range(n_range): for rbin in range(n_range):
# Extract chirp stack for this range bin
chirp_i = np.zeros(n_chirps, dtype=np.int64) chirp_i = np.zeros(n_chirps, dtype=np.int64)
chirp_q = np.zeros(n_chirps, dtype=np.int64) chirp_q = np.zeros(n_chirps, dtype=np.int64)
for c in range(n_chirps): for c in range(n_chirps):
chirp_i[c] = int(range_data_i[c, rbin]) chirp_i[c] = int(range_data_i[c, rbin])
chirp_q[c] = int(range_data_q[c, rbin]) chirp_q[c] = int(range_data_q[c, rbin])
# Apply Hamming window (Q15 multiply with rounding) # Process each sub-frame independently
windowed_i = np.zeros(n_fft, dtype=np.int64) for sf in range(2):
windowed_q = np.zeros(n_fft, dtype=np.int64) chirp_start = sf * n_sf
for k in range(n_fft): bin_offset = sf * n_fft
# 16-bit x 16-bit = 32-bit, then round and shift >>> 15
mult_i = chirp_i[k] * hamming[k]
mult_q = chirp_q[k] * hamming[k]
windowed_i[k] = saturate((mult_i + (1 << 14)) >> 15, 16)
windowed_q[k] = saturate((mult_q + (1 << 14)) >> 15, 16)
# 32-point FFT (same algorithm as range FFT, different N) windowed_i = np.zeros(n_fft, dtype=np.int64)
LOG2N_32 = 5 windowed_q = np.zeros(n_fft, dtype=np.int64)
mem_re = np.zeros(n_fft, dtype=np.int64) for k in range(n_fft):
mem_im = np.zeros(n_fft, dtype=np.int64) ci = chirp_i[chirp_start + k]
cq = chirp_q[chirp_start + k]
mult_i = ci * hamming[k]
mult_q = cq * hamming[k]
windowed_i[k] = saturate((mult_i + (1 << 14)) >> 15, 16)
windowed_q[k] = saturate((mult_q + (1 << 14)) >> 15, 16)
# Bit-reversed loading, sign-extend to 32-bit mem_re = np.zeros(n_fft, dtype=np.int64)
for n in range(n_fft): mem_im = np.zeros(n_fft, dtype=np.int64)
br = 0
for b in range(LOG2N_32):
if n & (1 << b):
br |= (1 << (LOG2N_32 - 1 - b))
mem_re[br] = windowed_i[n]
mem_im[br] = windowed_q[n]
# Butterfly stages for n in range(n_fft):
half = 1 br = 0
for stg in range(LOG2N_32): for b in range(LOG2N_16):
for bfly in range(n_fft // 2): if n & (1 << b):
idx = bfly & (half - 1) br |= (1 << (LOG2N_16 - 1 - b))
grp = bfly - idx mem_re[br] = windowed_i[n]
addr_even = (grp << 1) | idx mem_im[br] = windowed_q[n]
addr_odd = addr_even + half
tw_idx = (idx << (LOG2N_32 - 1 - stg)) % (n_fft // 2) half = 1
tw_cos, tw_sin = fft_twiddle_lookup(tw_idx, n_fft, cos_rom_32) for stg in range(LOG2N_16):
for bfly in range(n_fft // 2):
idx = bfly & (half - 1)
grp = bfly - idx
addr_even = (grp << 1) | idx
addr_odd = addr_even + half
a_re = mem_re[addr_even] tw_idx = (idx << (LOG2N_16 - 1 - stg)) % (n_fft // 2)
a_im = mem_im[addr_even] tw_cos, tw_sin = fft_twiddle_lookup(tw_idx, n_fft, cos_rom_16)
b_re = mem_re[addr_odd]
b_im = mem_im[addr_odd]
prod_re = b_re * tw_cos + b_im * tw_sin a_re = mem_re[addr_even]
prod_im = b_im * tw_cos - b_re * tw_sin a_im = mem_im[addr_even]
b_re = mem_re[addr_odd]
b_im = mem_im[addr_odd]
prod_re_shifted = prod_re >> 15 prod_re = b_re * tw_cos + b_im * tw_sin
prod_im_shifted = prod_im >> 15 prod_im = b_im * tw_cos - b_re * tw_sin
mem_re[addr_even] = a_re + prod_re_shifted prod_re_shifted = prod_re >> 15
mem_im[addr_even] = a_im + prod_im_shifted prod_im_shifted = prod_im >> 15
mem_re[addr_odd] = a_re - prod_re_shifted
mem_im[addr_odd] = a_im - prod_im_shifted
half <<= 1 mem_re[addr_even] = a_re + prod_re_shifted
mem_im[addr_even] = a_im + prod_im_shifted
mem_re[addr_odd] = a_re - prod_re_shifted
mem_im[addr_odd] = a_im - prod_im_shifted
# Saturate 32-bit → 16-bit half <<= 1
for n in range(n_fft):
doppler_map_i[rbin, n] = saturate(mem_re[n], 16)
doppler_map_q[rbin, n] = saturate(mem_im[n], 16)
print(f" Doppler map: shape ({n_range}, {n_fft}), " for n in range(n_fft):
doppler_map_i[rbin, bin_offset + n] = saturate(mem_re[n], 16)
doppler_map_q[rbin, bin_offset + n] = saturate(mem_im[n], 16)
print(f" Doppler map: shape ({n_range}, {n_total}), "
f"I range [{doppler_map_i.min()}, {doppler_map_i.max()}]") f"I range [{doppler_map_i.min()}, {doppler_map_i.max()}]")
return doppler_map_i, doppler_map_q return doppler_map_i, doppler_map_q
@@ -821,23 +819,24 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
Input: doppler_i/q — shape (NUM_RANGE_BINS, NUM_DOPPLER_BINS), 16-bit signed Input: doppler_i/q — shape (NUM_RANGE_BINS, NUM_DOPPLER_BINS), 16-bit signed
Output: notched_i/q — shape (NUM_RANGE_BINS, NUM_DOPPLER_BINS), 16-bit signed Output: notched_i/q — shape (NUM_RANGE_BINS, NUM_DOPPLER_BINS), 16-bit signed
Zeros Doppler bins within ±width of DC (bin 0). Zeros Doppler bins within ±width of DC for BOTH sub-frames.
In a 32-point FFT, DC is bin 0; negative Doppler wraps to bins 31,30,... doppler_bin[4:0] = {sub_frame, bin[3:0]}:
Sub-frame 0: bins 0-15, DC = bin 0, wrap = bin 15
Sub-frame 1: bins 16-31, DC = bin 16, wrap = bin 31
width=0: pass-through width=0: pass-through
width=1: zero bins {0} width=1: zero bins {0, 16}
width=2: zero bins {0, 1, 31} width=2: zero bins {0, 1, 15, 16, 17, 31} etc.
width=3: zero bins {0, 1, 2, 30, 31} etc.
RTL logic (from radar_system_top.v lines 517-524): RTL logic (from radar_system_top.v):
bin_within_sf = dop_bin[3:0]
dc_notch_active = (width != 0) && dc_notch_active = (width != 0) &&
(dop_bin < width || dop_bin > (31 - width + 1)) (bin_within_sf < width || bin_within_sf > (15 - width + 1))
notched_data = dc_notch_active ? 0 : doppler_data
""" """
n_range, n_doppler = doppler_i.shape n_range, n_doppler = doppler_i.shape
notched_i = doppler_i.copy() notched_i = doppler_i.copy()
notched_q = doppler_q.copy() notched_q = doppler_q.copy()
print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins") print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)")
if width == 0: if width == 0:
print(f" Pass-through (width=0)") print(f" Pass-through (width=0)")
@@ -845,9 +844,8 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
zeroed_count = 0 zeroed_count = 0
for dbin in range(n_doppler): for dbin in range(n_doppler):
# Replicate RTL comparison (unsigned 5-bit): bin_within_sf = dbin & 0xF
# dop_bin < width OR dop_bin > (31 - width + 1) active = (bin_within_sf < width) or (bin_within_sf > (15 - width + 1))
active = (dbin < width) or (dbin > (31 - width + 1))
if active: if active:
notched_i[:, dbin] = 0 notched_i[:, dbin] = 0
notched_q[:, dbin] = 0 notched_q[:, dbin] = 0
@@ -1049,11 +1047,15 @@ def run_float_reference(iq_i, iq_q):
n_range = min(DOPPLER_RANGE_BINS, n_samples) n_range = min(DOPPLER_RANGE_BINS, n_samples)
hamming_float = np.array(HAMMING_Q15, dtype=np.float64) / 32768.0 hamming_float = np.array(HAMMING_Q15, dtype=np.float64) / 32768.0
doppler_map = np.zeros((n_range, DOPPLER_FFT_SIZE), dtype=np.complex128) doppler_map = np.zeros((n_range, DOPPLER_TOTAL_BINS), dtype=np.complex128)
for rbin in range(n_range): for rbin in range(n_range):
chirp_stack = range_fft[:DOPPLER_CHIRPS, rbin] chirp_stack = range_fft[:DOPPLER_CHIRPS, rbin]
windowed = chirp_stack * hamming_float for sf in range(2):
doppler_map[rbin, :] = np.fft.fft(windowed) sf_start = sf * CHIRPS_PER_SUBFRAME
sf_end = sf_start + CHIRPS_PER_SUBFRAME
bin_offset = sf * DOPPLER_FFT_SIZE
windowed = chirp_stack[sf_start:sf_end] * hamming_float
doppler_map[rbin, bin_offset:bin_offset + DOPPLER_FFT_SIZE] = np.fft.fft(windowed)
return range_fft, doppler_map return range_fft, doppler_map
@@ -1235,10 +1237,10 @@ def main():
# Run Doppler FFT (bit-accurate) — "direct" path (first 64 bins) # Run Doppler FFT (bit-accurate) — "direct" path (first 64 bins)
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
print(f"\n{'=' * 72}") print(f"\n{'=' * 72}")
print("Stage 3: Doppler FFT (32-point with Hamming window)") print("Stage 3: Doppler FFT (dual 16-point with Hamming window)")
print(" [direct path: first 64 range bins, no decimation]") print(" [direct path: first 64 range bins, no decimation]")
twiddle_32 = os.path.join(fpga_dir, "fft_twiddle_32.mem") twiddle_16 = os.path.join(fpga_dir, "fft_twiddle_16.mem")
doppler_i, doppler_q = run_doppler_fft(all_range_i, all_range_q, twiddle_file_32=twiddle_32) doppler_i, doppler_q = run_doppler_fft(all_range_i, all_range_q, twiddle_file_16=twiddle_16)
write_hex_files(output_dir, doppler_i, doppler_q, "doppler_map") write_hex_files(output_dir, doppler_i, doppler_q, "doppler_map")
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@@ -1276,7 +1278,7 @@ def main():
print(f"\n{'=' * 72}") print(f"\n{'=' * 72}")
print("Stage 3b: Doppler FFT on decimated data (full-chain path)") print("Stage 3b: Doppler FFT on decimated data (full-chain path)")
fc_doppler_i, fc_doppler_q = run_doppler_fft( fc_doppler_i, fc_doppler_q = run_doppler_fft(
decim_i, decim_q, twiddle_file_32=twiddle_32 decim_i, decim_q, twiddle_file_16=twiddle_16
) )
write_hex_files(output_dir, fc_doppler_i, fc_doppler_q, "fullchain_doppler_ref") write_hex_files(output_dir, fc_doppler_i, fc_doppler_q, "fullchain_doppler_ref")
@@ -1284,12 +1286,12 @@ def main():
fc_doppler_packed_file = os.path.join(output_dir, "fullchain_doppler_ref_packed.hex") fc_doppler_packed_file = os.path.join(output_dir, "fullchain_doppler_ref_packed.hex")
with open(fc_doppler_packed_file, 'w') as f: with open(fc_doppler_packed_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
i_val = int(fc_doppler_i[rbin, dbin]) & 0xFFFF i_val = int(fc_doppler_i[rbin, dbin]) & 0xFFFF
q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF
packed = (q_val << 16) | i_val packed = (q_val << 16) | i_val
f.write(f"{packed:08X}\n") f.write(f"{packed:08X}\n")
print(f" Wrote {fc_doppler_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} packed IQ words)") print(f" Wrote {fc_doppler_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
# Save numpy arrays for the full-chain path # Save numpy arrays for the full-chain path
np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i) np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i)
@@ -1313,7 +1315,7 @@ def main():
print(f"\n{'=' * 72}") print(f"\n{'=' * 72}")
print("Stage 3b+c: Doppler FFT on MTI-filtered decimated data") print("Stage 3b+c: Doppler FFT on MTI-filtered decimated data")
mti_doppler_i, mti_doppler_q = run_doppler_fft( mti_doppler_i, mti_doppler_q = run_doppler_fft(
mti_i, mti_q, twiddle_file_32=twiddle_32 mti_i, mti_q, twiddle_file_16=twiddle_16
) )
write_hex_files(output_dir, mti_doppler_i, mti_doppler_q, "fullchain_mti_doppler_ref") write_hex_files(output_dir, mti_doppler_i, mti_doppler_q, "fullchain_mti_doppler_ref")
np.save(os.path.join(output_dir, "fullchain_mti_doppler_i.npy"), mti_doppler_i) np.save(os.path.join(output_dir, "fullchain_mti_doppler_i.npy"), mti_doppler_i)
@@ -1330,12 +1332,12 @@ def main():
fc_notched_packed_file = os.path.join(output_dir, "fullchain_notched_ref_packed.hex") fc_notched_packed_file = os.path.join(output_dir, "fullchain_notched_ref_packed.hex")
with open(fc_notched_packed_file, 'w') as f: with open(fc_notched_packed_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
i_val = int(notched_i[rbin, dbin]) & 0xFFFF i_val = int(notched_i[rbin, dbin]) & 0xFFFF
q_val = int(notched_q[rbin, dbin]) & 0xFFFF q_val = int(notched_q[rbin, dbin]) & 0xFFFF
packed = (q_val << 16) | i_val packed = (q_val << 16) | i_val
f.write(f"{packed:08X}\n") f.write(f"{packed:08X}\n")
print(f" Wrote {fc_notched_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} packed IQ words)") print(f" Wrote {fc_notched_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
# CFAR on DC-notched data # CFAR on DC-notched data
CFAR_GUARD = 2 CFAR_GUARD = 2
@@ -1355,28 +1357,28 @@ def main():
cfar_mag_file = os.path.join(output_dir, "fullchain_cfar_mag.hex") cfar_mag_file = os.path.join(output_dir, "fullchain_cfar_mag.hex")
with open(cfar_mag_file, 'w') as f: with open(cfar_mag_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
m = int(cfar_mag[rbin, dbin]) & 0x1FFFF m = int(cfar_mag[rbin, dbin]) & 0x1FFFF
f.write(f"{m:05X}\n") f.write(f"{m:05X}\n")
print(f" Wrote {cfar_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} mag values)") print(f" Wrote {cfar_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} mag values)")
# 2. Threshold map (17-bit unsigned) # 2. Threshold map (17-bit unsigned)
cfar_thr_file = os.path.join(output_dir, "fullchain_cfar_thr.hex") cfar_thr_file = os.path.join(output_dir, "fullchain_cfar_thr.hex")
with open(cfar_thr_file, 'w') as f: with open(cfar_thr_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
t = int(cfar_thr[rbin, dbin]) & 0x1FFFF t = int(cfar_thr[rbin, dbin]) & 0x1FFFF
f.write(f"{t:05X}\n") f.write(f"{t:05X}\n")
print(f" Wrote {cfar_thr_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} threshold values)") print(f" Wrote {cfar_thr_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} threshold values)")
# 3. Detection flags (1-bit per cell) # 3. Detection flags (1-bit per cell)
cfar_det_file = os.path.join(output_dir, "fullchain_cfar_det.hex") cfar_det_file = os.path.join(output_dir, "fullchain_cfar_det.hex")
with open(cfar_det_file, 'w') as f: with open(cfar_det_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
d = 1 if cfar_flags[rbin, dbin] else 0 d = 1 if cfar_flags[rbin, dbin] else 0
f.write(f"{d:01X}\n") f.write(f"{d:01X}\n")
print(f" Wrote {cfar_det_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} detection flags)") print(f" Wrote {cfar_det_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} detection flags)")
# 4. Detection list (text) # 4. Detection list (text)
cfar_detections = np.argwhere(cfar_flags) cfar_detections = np.argwhere(cfar_flags)
@@ -1416,10 +1418,10 @@ def main():
fc_det_mag_file = os.path.join(output_dir, "fullchain_detection_mag.hex") fc_det_mag_file = os.path.join(output_dir, "fullchain_detection_mag.hex")
with open(fc_det_mag_file, 'w') as f: with open(fc_det_mag_file, 'w') as f:
for rbin in range(DOPPLER_RANGE_BINS): for rbin in range(DOPPLER_RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE): for dbin in range(DOPPLER_TOTAL_BINS):
m = int(fc_mag[rbin, dbin]) & 0x1FFFF # 17-bit unsigned m = int(fc_mag[rbin, dbin]) & 0x1FFFF # 17-bit unsigned
f.write(f"{m:05X}\n") f.write(f"{m:05X}\n")
print(f" Wrote {fc_det_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_FFT_SIZE} magnitude values)") print(f" Wrote {fc_det_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} magnitude values)")
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Run detection on direct-path Doppler map (for backward compatibility) # Run detection on direct-path Doppler map (for backward compatibility)
+11 -28
View File
@@ -78,14 +78,8 @@ class RadarDashboard:
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
# Radar parameters for physical axis labels (ADI CN0566 defaults) # Radar parameters used for range-axis scaling.
# Config: [sample_rate=4e6, IF=1e5, RF=9.9e9, chirps=256, BW=500e6,
# ramp_time=300e-6, ...]
SAMPLE_RATE = 4e6 # Hz — ADC sample rate (baseband)
BANDWIDTH = 500e6 # Hz — chirp bandwidth BANDWIDTH = 500e6 # Hz — chirp bandwidth
RAMP_TIME = 300e-6 # s — chirp ramp time
CENTER_FREQ = 10.5e9 # Hz — X-band center frequency
NUM_CHIRPS_FRAME = 32 # chirps per Doppler frame
C = 3e8 # m/s — speed of light C = 3e8 # m/s — speed of light
def __init__(self, root: tk.Tk, connection: FT601Connection, def __init__(self, root: tk.Tk, connection: FT601Connection,
@@ -188,15 +182,8 @@ class RadarDashboard:
range_per_bin = range_res * 16 range_per_bin = range_res * 16
max_range = range_per_bin * NUM_RANGE_BINS max_range = range_per_bin * NUM_RANGE_BINS
# Velocity resolution: dv = lambda / (2 * N_chirps * T_chirp) doppler_bin_lo = 0
wavelength = self.C / self.CENTER_FREQ doppler_bin_hi = NUM_DOPPLER_BINS
# Max unambiguous velocity = lambda / (4 * T_chirp)
max_vel = wavelength / (4.0 * self.RAMP_TIME)
vel_per_bin = 2.0 * max_vel / NUM_DOPPLER_BINS
# Doppler axis: bin 0 = 0 Hz (DC), wraps at Nyquist
# For display: center DC, so shift axis to [-max_vel, +max_vel)
vel_lo = -max_vel
vel_hi = max_vel
# Matplotlib figure with 3 subplots # Matplotlib figure with 3 subplots
self.fig = Figure(figsize=(14, 7), facecolor=BG) self.fig = Figure(figsize=(14, 7), facecolor=BG)
@@ -209,20 +196,17 @@ class RadarDashboard:
self._rd_img = self.ax_rd.imshow( self._rd_img = self.ax_rd.imshow(
np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS)), np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS)),
aspect="auto", cmap="inferno", origin="lower", aspect="auto", cmap="inferno", origin="lower",
extent=[vel_lo, vel_hi, 0, max_range], extent=[doppler_bin_lo, doppler_bin_hi, 0, max_range],
vmin=0, vmax=1000, vmin=0, vmax=1000,
) )
self.ax_rd.set_title("Range-Doppler Map", color=FG, fontsize=12) self.ax_rd.set_title("Range-Doppler Map", color=FG, fontsize=12)
self.ax_rd.set_xlabel("Velocity (m/s)", color=FG) self.ax_rd.set_xlabel("Doppler Bin (0-15: long PRI, 16-31: short PRI)", color=FG)
self.ax_rd.set_ylabel("Range (m)", color=FG) self.ax_rd.set_ylabel("Range (m)", color=FG)
self.ax_rd.tick_params(colors=FG) self.ax_rd.tick_params(colors=FG)
# Save axis limits for coordinate conversions # Save axis limits for coordinate conversions
self._vel_lo = vel_lo
self._vel_hi = vel_hi
self._max_range = max_range self._max_range = max_range
self._range_per_bin = range_per_bin self._range_per_bin = range_per_bin
self._vel_per_bin = vel_per_bin
# CFAR detection overlay (scatter) # CFAR detection overlay (scatter)
self._det_scatter = self.ax_rd.scatter([], [], s=30, c=GREEN, self._det_scatter = self.ax_rd.scatter([], [], s=30, c=GREEN,
@@ -504,10 +488,9 @@ class RadarDashboard:
self.lbl_detections.config(text=f"Det: {frame.detection_count}") self.lbl_detections.config(text=f"Det: {frame.detection_count}")
self.lbl_frame.config(text=f"Frame: {frame.frame_number}") self.lbl_frame.config(text=f"Frame: {frame.frame_number}")
# Update range-Doppler heatmap # Update range-Doppler heatmap in raw dual-subframe bin order
# FFT-shift Doppler axis so DC (bin 0) is in the center mag = frame.magnitude
mag = np.fft.fftshift(frame.magnitude, axes=1) det_shifted = frame.detections
det_shifted = np.fft.fftshift(frame.detections, axes=1)
# Stable colorscale via EMA smoothing of vmax # Stable colorscale via EMA smoothing of vmax
frame_vmax = float(np.max(mag)) if np.max(mag) > 0 else 1.0 frame_vmax = float(np.max(mag)) if np.max(mag) > 0 else 1.0
@@ -518,13 +501,13 @@ class RadarDashboard:
self._rd_img.set_data(mag) self._rd_img.set_data(mag)
self._rd_img.set_clim(vmin=0, vmax=stable_vmax) self._rd_img.set_clim(vmin=0, vmax=stable_vmax)
# Update CFAR overlay — convert bin indices to physical coordinates # Update CFAR overlay in raw Doppler-bin coordinates
det_coords = np.argwhere(det_shifted > 0) det_coords = np.argwhere(det_shifted > 0)
if len(det_coords) > 0: if len(det_coords) > 0:
# det_coords[:, 0] = range bin, det_coords[:, 1] = Doppler bin # det_coords[:, 0] = range bin, det_coords[:, 1] = Doppler bin
range_m = (det_coords[:, 0] + 0.5) * self._range_per_bin range_m = (det_coords[:, 0] + 0.5) * self._range_per_bin
vel_ms = self._vel_lo + (det_coords[:, 1] + 0.5) * self._vel_per_bin doppler_bins = det_coords[:, 1] + 0.5
offsets = np.column_stack([vel_ms, range_m]) offsets = np.column_stack([doppler_bins, range_m])
self._det_scatter.set_offsets(offsets) self._det_scatter.set_offsets(offsets)
else: else:
self._det_scatter.set_offsets(np.empty((0, 2))) self._det_scatter.set_offsets(np.empty((0, 2)))