fix(rtl,gui,cosim,formal): adapt surrounding files for dual 16-pt FFT (follow-up to PR #33)
- radar_system_top.v: DC notch now masks to dop_bin[3:0] per sub-frame so both sub-frames get their DC zeroed correctly; rename DOPPLER_FFT_SIZE → DOPPLER_FRAME_CHIRPS to avoid confusion with the per-FFT size (now 16) - radar_dashboard.py: remove fftshift (crosses sub-frame boundary), display raw Doppler bins, remove dead velocity constants - golden_reference.py: model dual 16-pt FFT with per-sub-frame Hamming window, update DC notch and CFAR to match RTL - fv_doppler_processor.sby: reference xfft_16.v / fft_twiddle_16.mem, raise BMC depth to 512 and cover to 1024 - fv_radar_mode_controller.sby: raise cover depth to 600 - fv_radar_mode_controller.v: pin cfg_* to reduced constants (documented as single-config proof), fix Property 5 mode guard, strengthen Cover 1 - STALE_NOTICE.md: document that real-data hex files are stale and need regeneration with external dataset Closes #39
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
Reference in New Issue
Block a user