Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cb7688814 | |||
| 86b493a780 | |||
| c023337949 | |||
| d259e5c106 |
@@ -0,0 +1,21 @@
|
||||
# Enforce LF line endings for all text files going forward.
|
||||
# Existing CRLF files are left as-is to avoid polluting git blame.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Binary files — ensure git doesn't mangle these
|
||||
*.npy binary
|
||||
*.h5 binary
|
||||
*.hdf5 binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.pdf binary
|
||||
*.zip binary
|
||||
*.bin binary
|
||||
*.mem binary
|
||||
*.hex binary
|
||||
*.vvp binary
|
||||
*.s2p binary
|
||||
*.s3p binary
|
||||
*.step binary
|
||||
*.FCStd binary
|
||||
*.FCBak binary
|
||||
@@ -32,6 +32,12 @@
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rtl_doppler_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/compare_doppler_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rtl_multiseg_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/rtl_mf_*.csv
|
||||
9_Firmware/9_2_FPGA/tb/cosim/compare_mf_*.csv
|
||||
|
||||
# Golden reference outputs (regenerated by testbenches)
|
||||
9_Firmware/9_2_FPGA/tb/golden/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import numpy as np
|
||||
|
||||
# Define parameters
|
||||
fs = 120e6 # Sampling frequency
|
||||
# NOTE: This is a standalone LUT generation utility. The production chirp LUT
|
||||
# is generated by 9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py with
|
||||
# CHIRP_BW=20e6 (target: 30e6 Phase 1) and DAC_CLK=120e6.
|
||||
fs = 120e6 # Sampling frequency (DAC clock from AD9523 OUT10)
|
||||
Ts = 1 / fs # Sampling time
|
||||
Tb = 1e-6 # Burst time
|
||||
Tau = 30e-6 # Pulse repetition time
|
||||
|
||||
@@ -6,16 +6,16 @@ RadarSettings::RadarSettings() {
|
||||
}
|
||||
|
||||
void RadarSettings::resetToDefaults() {
|
||||
system_frequency = 10.0e9; // 10 GHz
|
||||
chirp_duration_1 = 30.0e-6; // 30 �s
|
||||
chirp_duration_2 = 0.5e-6; // 0.5 �s
|
||||
system_frequency = 10.5e9; // 10.5 GHz (PLFM TX LO, ADF4382 config)
|
||||
chirp_duration_1 = 30.0e-6; // 30 µs
|
||||
chirp_duration_2 = 0.5e-6; // 0.5 µs
|
||||
chirps_per_position = 32;
|
||||
freq_min = 10.0e6; // 10 MHz
|
||||
freq_max = 30.0e6; // 30 MHz
|
||||
prf1 = 1000.0; // 1 kHz
|
||||
prf2 = 2000.0; // 2 kHz
|
||||
max_distance = 50000.0; // 50 km
|
||||
map_size = 50000.0; // 50 km
|
||||
max_distance = 1536.0; // 1536 m (64 bins × 24 m, 3 km mode)
|
||||
map_size = 1536.0; // 1536 m
|
||||
|
||||
settings_valid = true;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ bool RadarSettings::validateSettings() {
|
||||
if (prf1 < 100 || prf1 > 10000) return false;
|
||||
if (prf2 < 100 || prf2 > 10000) return false;
|
||||
if (max_distance < 100 || max_distance > 100000) return false;
|
||||
if (map_size < 1000 || map_size > 200000) return false;
|
||||
if (map_size < 100 || map_size > 200000) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -18,3 +18,9 @@ test_bug12_pa_cal_loop_inverted
|
||||
test_bug13_dac2_adc_buffer_mismatch
|
||||
test_bug14_diag_section_args
|
||||
test_bug15_htim3_dangling_extern
|
||||
test_agc_outer_loop
|
||||
test_gap3_emergency_state_ordering
|
||||
test_gap3_emergency_stop_rails
|
||||
test_gap3_idq_periodic_reread
|
||||
test_gap3_iwdg_config
|
||||
test_gap3_temperature_max
|
||||
|
||||
@@ -18,10 +18,9 @@ module matched_filter_multi_segment (
|
||||
input wire mc_new_elevation, // Toggle for new elevation (32)
|
||||
input wire mc_new_azimuth, // Toggle for new azimuth (50)
|
||||
|
||||
input wire [15:0] long_chirp_real,
|
||||
input wire [15:0] long_chirp_imag,
|
||||
input wire [15:0] short_chirp_real,
|
||||
input wire [15:0] short_chirp_imag,
|
||||
// Reference chirp (upstream memory loader selects long/short via use_long_chirp)
|
||||
input wire [15:0] ref_chirp_real,
|
||||
input wire [15:0] ref_chirp_imag,
|
||||
|
||||
// Memory system interface
|
||||
output reg [1:0] segment_request,
|
||||
@@ -244,6 +243,7 @@ always @(posedge clk or negedge reset_n) begin
|
||||
if (!use_long_chirp) begin
|
||||
if (chirp_samples_collected >= SHORT_CHIRP_SAMPLES - 1) begin
|
||||
state <= ST_ZERO_PAD;
|
||||
chirp_complete <= 1; // Bug A fix: mark chirp done so ST_OUTPUT exits to IDLE
|
||||
`ifdef SIMULATION
|
||||
$display("[MULTI_SEG_FIXED] Short chirp: collected %d samples, starting zero-pad",
|
||||
chirp_samples_collected + 1);
|
||||
@@ -500,11 +500,9 @@ matched_filter_processing_chain m_f_p_c(
|
||||
// Chirp Selection
|
||||
.chirp_counter(chirp_counter),
|
||||
|
||||
// Reference Chirp Memory Interfaces
|
||||
.long_chirp_real(long_chirp_real),
|
||||
.long_chirp_imag(long_chirp_imag),
|
||||
.short_chirp_real(short_chirp_real),
|
||||
.short_chirp_imag(short_chirp_imag),
|
||||
// Reference Chirp Memory Interface (single pair — upstream selects long/short)
|
||||
.ref_chirp_real(ref_chirp_real),
|
||||
.ref_chirp_imag(ref_chirp_imag),
|
||||
|
||||
// Output
|
||||
.range_profile_i(fft_pc_i),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* .clk, .reset_n
|
||||
* .adc_data_i, .adc_data_q, .adc_valid <- from input buffer
|
||||
* .chirp_counter <- 6-bit frame counter
|
||||
* .long_chirp_real/imag, .short_chirp_real/imag <- reference (time-domain)
|
||||
* .ref_chirp_real/imag <- reference (time-domain)
|
||||
* .range_profile_i, .range_profile_q, .range_profile_valid -> output
|
||||
* .chain_state -> 4-bit status
|
||||
*
|
||||
@@ -48,10 +48,10 @@ module matched_filter_processing_chain (
|
||||
input wire [5:0] chirp_counter,
|
||||
|
||||
// Reference chirp (time-domain, latency-aligned by upstream buffer)
|
||||
input wire [15:0] long_chirp_real,
|
||||
input wire [15:0] long_chirp_imag,
|
||||
input wire [15:0] short_chirp_real,
|
||||
input wire [15:0] short_chirp_imag,
|
||||
// Upstream chirp_memory_loader_param selects long/short reference
|
||||
// via use_long_chirp — this single pair carries whichever is active.
|
||||
input wire [15:0] ref_chirp_real,
|
||||
input wire [15:0] ref_chirp_imag,
|
||||
|
||||
// Output: range profile (pulse-compressed)
|
||||
output wire signed [15:0] range_profile_i,
|
||||
@@ -189,8 +189,8 @@ always @(posedge clk or negedge reset_n) begin
|
||||
// Store first sample (signal + reference)
|
||||
fwd_buf_i[0] <= $signed(adc_data_i);
|
||||
fwd_buf_q[0] <= $signed(adc_data_q);
|
||||
ref_buf_i[0] <= $signed(long_chirp_real);
|
||||
ref_buf_q[0] <= $signed(long_chirp_imag);
|
||||
ref_buf_i[0] <= $signed(ref_chirp_real);
|
||||
ref_buf_q[0] <= $signed(ref_chirp_imag);
|
||||
fwd_in_count <= 1;
|
||||
state <= ST_FWD_FFT;
|
||||
end
|
||||
@@ -205,8 +205,8 @@ always @(posedge clk or negedge reset_n) begin
|
||||
if (adc_valid && fwd_in_count < FFT_SIZE) begin
|
||||
fwd_buf_i[fwd_in_count] <= $signed(adc_data_i);
|
||||
fwd_buf_q[fwd_in_count] <= $signed(adc_data_q);
|
||||
ref_buf_i[fwd_in_count] <= $signed(long_chirp_real);
|
||||
ref_buf_q[fwd_in_count] <= $signed(long_chirp_imag);
|
||||
ref_buf_i[fwd_in_count] <= $signed(ref_chirp_real);
|
||||
ref_buf_q[fwd_in_count] <= $signed(ref_chirp_imag);
|
||||
fwd_in_count <= fwd_in_count + 1;
|
||||
end
|
||||
|
||||
@@ -775,16 +775,16 @@ always @(posedge clk) begin : ref_bram_port
|
||||
if (adc_valid) begin
|
||||
we = 1'b1;
|
||||
addr = 0;
|
||||
wdata_i = $signed(long_chirp_real);
|
||||
wdata_q = $signed(long_chirp_imag);
|
||||
wdata_i = $signed(ref_chirp_real);
|
||||
wdata_q = $signed(ref_chirp_imag);
|
||||
end
|
||||
end
|
||||
ST_COLLECT: begin
|
||||
if (adc_valid && collect_count < FFT_SIZE) begin
|
||||
we = 1'b1;
|
||||
addr = collect_count[ADDR_BITS-1:0];
|
||||
wdata_i = $signed(long_chirp_real);
|
||||
wdata_q = $signed(long_chirp_imag);
|
||||
wdata_i = $signed(ref_chirp_real);
|
||||
wdata_q = $signed(ref_chirp_imag);
|
||||
end
|
||||
end
|
||||
ST_REF_FFT: begin
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// ============================================================================
|
||||
// radar_params.vh — Single Source of Truth for AERIS-10 FPGA Parameters
|
||||
// ============================================================================
|
||||
//
|
||||
// ALL modules in the FPGA processing chain MUST `include this file instead of
|
||||
// hardcoding range bins, segment counts, chirp samples, or timing values.
|
||||
//
|
||||
// This file uses `define macros (not localparam) so it can be included at any
|
||||
// scope. Each consuming module should include this file inside its body and
|
||||
// optionally alias macros to localparams for readability.
|
||||
//
|
||||
// BOARD VARIANTS:
|
||||
// SUPPORT_LONG_RANGE = 0 (50T, USB_MODE=1) — 3 km mode only, 64 range bins
|
||||
// SUPPORT_LONG_RANGE = 1 (200T, USB_MODE=0) — 3 km + 20 km modes, up to 1024 bins
|
||||
//
|
||||
// RANGE MODES (runtime, via host_range_mode register, opcode 0x20):
|
||||
// 2'b00 = 3 km (default on both boards)
|
||||
// 2'b01 = 20 km (200T only; clamped to 3 km on 50T)
|
||||
// 2'b10 = Reserved
|
||||
// 2'b11 = Reserved
|
||||
//
|
||||
// USAGE:
|
||||
// `include "radar_params.vh"
|
||||
// Then reference `RP_FFT_SIZE, `RP_MAX_OUTPUT_BINS, etc.
|
||||
//
|
||||
// PHYSICAL CONSTANTS (derived from hardware):
|
||||
// ADC clock: 400 MSPS
|
||||
// CIC decimation: 4x
|
||||
// Processing rate: 100 MSPS (post-DDC)
|
||||
// Range per sample: c / (2 * 100e6) = 1.5 m
|
||||
// Decimation factor: 16 (1024 FFT bins -> 64 output bins per segment)
|
||||
// Range per dec. bin: 1.5 m * 16 = 24.0 m
|
||||
// Carrier frequency: 10.5 GHz
|
||||
//
|
||||
// CHIRP BANDWIDTH (Phase 1 target — currently 20 MHz, planned 30 MHz):
|
||||
// Range resolution: c / (2 * BW)
|
||||
// 20 MHz -> 7.5 m
|
||||
// 30 MHz -> 5.0 m
|
||||
// NOTE: Range resolution is independent of range-per-bin. Resolution
|
||||
// determines the minimum separation between two targets; range-per-bin
|
||||
// determines the spatial sampling grid.
|
||||
// ============================================================================
|
||||
|
||||
`ifndef RADAR_PARAMS_VH
|
||||
`define RADAR_PARAMS_VH
|
||||
|
||||
// ============================================================================
|
||||
// BOARD VARIANT — set at synthesis time, NOT runtime
|
||||
// ============================================================================
|
||||
// Default to 50T (conservative). Override in top-level or synthesis script:
|
||||
// +define+SUPPORT_LONG_RANGE
|
||||
// or via Vivado: set_property verilog_define {SUPPORT_LONG_RANGE} [current_fileset]
|
||||
|
||||
// Note: SUPPORT_LONG_RANGE is a flag define (ifdef/ifndef), not a value.
|
||||
// `ifndef SUPPORT_LONG_RANGE means 50T (no long range).
|
||||
// `ifdef SUPPORT_LONG_RANGE means 200T (long range supported).
|
||||
|
||||
// ============================================================================
|
||||
// FFT AND PROCESSING CONSTANTS (fixed, both modes)
|
||||
// ============================================================================
|
||||
|
||||
`define RP_FFT_SIZE 1024 // Range FFT points per segment
|
||||
`define RP_OVERLAP_SAMPLES 128 // Overlap between adjacent segments
|
||||
`define RP_SEGMENT_ADVANCE 896 // FFT_SIZE - OVERLAP = 1024 - 128
|
||||
`define RP_DECIMATION_FACTOR 16 // Range bin decimation (1024 -> 64)
|
||||
`define RP_BINS_PER_SEGMENT 64 // FFT_SIZE / DECIMATION_FACTOR
|
||||
`define RP_DOPPLER_FFT_SIZE 16 // Per sub-frame Doppler FFT
|
||||
`define RP_CHIRPS_PER_FRAME 32 // Total chirps (16 long + 16 short)
|
||||
`define RP_CHIRPS_PER_SUBFRAME 16 // Chirps per Doppler sub-frame
|
||||
`define RP_NUM_DOPPLER_BINS 32 // 2 sub-frames * 16 = 32
|
||||
`define RP_DATA_WIDTH 16 // ADC/processing data width
|
||||
|
||||
// ============================================================================
|
||||
// 3 KM MODE PARAMETERS (both 50T and 200T)
|
||||
// ============================================================================
|
||||
|
||||
`define RP_LONG_CHIRP_SAMPLES_3KM 3000 // 30 us at 100 MSPS
|
||||
`define RP_LONG_SEGMENTS_3KM 4 // ceil((3000-1024)/896) + 1 = 4
|
||||
`define RP_OUTPUT_RANGE_BINS_3KM 64 // Downstream pipeline expects 64 range bins (NOTE: will become 128 after 2048-pt FFT upgrade)
|
||||
`define RP_SHORT_CHIRP_SAMPLES 50 // 0.5 us at 100 MSPS (same both modes)
|
||||
`define RP_SHORT_SEGMENTS 1 // Single segment for short chirp
|
||||
|
||||
// Derived 3 km limits
|
||||
`define RP_MAX_RANGE_3KM 1536 // 64 bins * 24 m = 1536 m
|
||||
|
||||
// ============================================================================
|
||||
// 20 KM MODE PARAMETERS (200T only)
|
||||
// ============================================================================
|
||||
|
||||
`define RP_LONG_CHIRP_SAMPLES_20KM 13700 // 137 us at 100 MSPS (= listen window)
|
||||
`define RP_LONG_SEGMENTS_20KM 16 // ceil((13700-1024)/896) + 1 = 16
|
||||
`define RP_OUTPUT_RANGE_BINS_20KM 1024 // 16 segments * 64 dec. bins each
|
||||
|
||||
// Derived 20 km limits
|
||||
`define RP_MAX_RANGE_20KM 24576 // 1024 bins * 24 m = 24576 m
|
||||
|
||||
// ============================================================================
|
||||
// MAX VALUES (for sizing buffers — compile-time, based on board variant)
|
||||
// ============================================================================
|
||||
|
||||
`ifdef SUPPORT_LONG_RANGE
|
||||
`define RP_MAX_SEGMENTS 16
|
||||
`define RP_MAX_OUTPUT_BINS 1024
|
||||
`define RP_MAX_CHIRP_SAMPLES 13700
|
||||
`else
|
||||
`define RP_MAX_SEGMENTS 4
|
||||
`define RP_MAX_OUTPUT_BINS 64
|
||||
`define RP_MAX_CHIRP_SAMPLES 3000
|
||||
`endif
|
||||
|
||||
// ============================================================================
|
||||
// BIT WIDTHS (derived from MAX values)
|
||||
// ============================================================================
|
||||
|
||||
// Segment index: ceil(log2(MAX_SEGMENTS))
|
||||
// 50T: log2(4) = 2 bits
|
||||
// 200T: log2(16) = 4 bits
|
||||
`ifdef SUPPORT_LONG_RANGE
|
||||
`define RP_SEGMENT_IDX_WIDTH 4
|
||||
`define RP_RANGE_BIN_WIDTH 10
|
||||
`define RP_CHIRP_MEM_ADDR_W 14 // log2(16*1024) = 14
|
||||
`define RP_DOPPLER_MEM_ADDR_W 15 // log2(1024*32) = 15
|
||||
`define RP_CFAR_MAG_ADDR_W 15 // log2(1024*32) = 15
|
||||
`else
|
||||
`define RP_SEGMENT_IDX_WIDTH 2
|
||||
`define RP_RANGE_BIN_WIDTH 6
|
||||
`define RP_CHIRP_MEM_ADDR_W 12 // log2(4*1024) = 12
|
||||
`define RP_DOPPLER_MEM_ADDR_W 11 // log2(64*32) = 11
|
||||
`define RP_CFAR_MAG_ADDR_W 11 // log2(64*32) = 11
|
||||
`endif
|
||||
|
||||
// Derived depths (for memory declarations)
|
||||
// Usage: reg [15:0] mem [0:`RP_CHIRP_MEM_DEPTH-1];
|
||||
`define RP_CHIRP_MEM_DEPTH (`RP_MAX_SEGMENTS * `RP_FFT_SIZE)
|
||||
`define RP_DOPPLER_MEM_DEPTH (`RP_MAX_OUTPUT_BINS * `RP_CHIRPS_PER_FRAME)
|
||||
`define RP_CFAR_MAG_DEPTH (`RP_MAX_OUTPUT_BINS * `RP_NUM_DOPPLER_BINS)
|
||||
|
||||
// ============================================================================
|
||||
// CHIRP TIMING DEFAULTS (100 MHz clock cycles)
|
||||
// ============================================================================
|
||||
// Reset defaults for host-configurable timing registers.
|
||||
// Match radar_mode_controller.v parameters and main.cpp STM32 defaults.
|
||||
|
||||
`define RP_DEF_LONG_CHIRP_CYCLES 3000 // 30 us
|
||||
`define RP_DEF_LONG_LISTEN_CYCLES 13700 // 137 us
|
||||
`define RP_DEF_GUARD_CYCLES 17540 // 175.4 us
|
||||
`define RP_DEF_SHORT_CHIRP_CYCLES 50 // 0.5 us
|
||||
`define RP_DEF_SHORT_LISTEN_CYCLES 17450 // 174.5 us
|
||||
`define RP_DEF_CHIRPS_PER_ELEV 32
|
||||
|
||||
// ============================================================================
|
||||
// BLIND ZONE CONSTANTS (informational, for comments and GUI)
|
||||
// ============================================================================
|
||||
// Long chirp blind zone: c * 30 us / 2 = 4500 m
|
||||
// Short chirp blind zone: c * 0.5 us / 2 = 75 m
|
||||
|
||||
`define RP_LONG_BLIND_ZONE_M 4500
|
||||
`define RP_SHORT_BLIND_ZONE_M 75
|
||||
|
||||
// ============================================================================
|
||||
// PHYSICAL CONSTANTS (integer-scaled for Verilog — use in comments/assertions)
|
||||
// ============================================================================
|
||||
// Range per ADC sample: 1.5 m (stored as 15 in units of 0.1 m)
|
||||
// Range per decimated bin: 24.0 m (stored as 240 in units of 0.1 m)
|
||||
// Processing rate: 100 MSPS
|
||||
|
||||
`define RP_RANGE_PER_SAMPLE_DM 15 // 1.5 m in decimeters
|
||||
`define RP_RANGE_PER_BIN_DM 240 // 24.0 m in decimeters
|
||||
`define RP_PROCESSING_RATE_MHZ 100
|
||||
|
||||
// ============================================================================
|
||||
// AGC DEFAULTS
|
||||
// ============================================================================
|
||||
`define RP_DEF_AGC_TARGET 200
|
||||
`define RP_DEF_AGC_ATTACK 1
|
||||
`define RP_DEF_AGC_DECAY 1
|
||||
`define RP_DEF_AGC_HOLDOFF 4
|
||||
|
||||
// ============================================================================
|
||||
// CFAR DEFAULTS
|
||||
// ============================================================================
|
||||
`define RP_DEF_CFAR_GUARD 2
|
||||
`define RP_DEF_CFAR_TRAIN 8
|
||||
`define RP_DEF_CFAR_ALPHA 8'h30 // 3.0 in Q4.4
|
||||
`define RP_DEF_CFAR_MODE 2'b00 // CA-CFAR
|
||||
|
||||
// ============================================================================
|
||||
// DETECTION DEFAULTS
|
||||
// ============================================================================
|
||||
`define RP_DEF_DETECT_THRESHOLD 10000
|
||||
|
||||
// ============================================================================
|
||||
// RANGE MODE ENCODING
|
||||
// ============================================================================
|
||||
`define RP_RANGE_MODE_3KM 2'b00
|
||||
`define RP_RANGE_MODE_20KM 2'b01
|
||||
`define RP_RANGE_MODE_RSVD2 2'b10
|
||||
`define RP_RANGE_MODE_RSVD3 2'b11
|
||||
|
||||
`endif // RADAR_PARAMS_VH
|
||||
@@ -102,9 +102,9 @@ wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter
|
||||
wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude
|
||||
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
|
||||
|
||||
// Reference signals for the processing chain
|
||||
wire [15:0] long_chirp_real, long_chirp_imag;
|
||||
wire [15:0] short_chirp_real, short_chirp_imag;
|
||||
// Reference signal for the processing chain (carries long OR short ref
|
||||
// depending on use_long_chirp — selected by chirp_memory_loader_param)
|
||||
wire [15:0] ref_chirp_real, ref_chirp_imag;
|
||||
|
||||
// ========== DOPPLER PROCESSING SIGNALS ==========
|
||||
wire [31:0] range_data_32bit;
|
||||
@@ -292,7 +292,8 @@ end
|
||||
// sample_addr_wire removed — was unused implicit wire (synthesis warning)
|
||||
|
||||
// 4. CRITICAL: Reference Chirp Latency Buffer
|
||||
// This aligns reference data with FFT output (2159 cycle delay)
|
||||
// This aligns reference data with FFT output (3187 cycle delay)
|
||||
// TODO: verify empirically during hardware bring-up with correlation test
|
||||
wire [15:0] delayed_ref_i, delayed_ref_q;
|
||||
wire mem_ready_delayed;
|
||||
|
||||
@@ -308,11 +309,10 @@ latency_buffer #(
|
||||
.valid_out(mem_ready_delayed)
|
||||
);
|
||||
|
||||
// Assign delayed reference signals
|
||||
assign long_chirp_real = delayed_ref_i;
|
||||
assign long_chirp_imag = delayed_ref_q;
|
||||
assign short_chirp_real = delayed_ref_i;
|
||||
assign short_chirp_imag = delayed_ref_q;
|
||||
// Assign delayed reference signals (single pair — chirp_memory_loader_param
|
||||
// selects long/short reference upstream via use_long_chirp)
|
||||
assign ref_chirp_real = delayed_ref_i;
|
||||
assign ref_chirp_imag = delayed_ref_q;
|
||||
|
||||
// 5. Dual Chirp Matched Filter
|
||||
|
||||
@@ -336,10 +336,8 @@ matched_filter_multi_segment mf_dual (
|
||||
.mc_new_chirp(mc_new_chirp),
|
||||
.mc_new_elevation(mc_new_elevation),
|
||||
.mc_new_azimuth(mc_new_azimuth),
|
||||
.long_chirp_real(delayed_ref_i), // From latency buffer
|
||||
.long_chirp_imag(delayed_ref_q),
|
||||
.short_chirp_real(delayed_ref_i), // Same for short chirp
|
||||
.short_chirp_imag(delayed_ref_q),
|
||||
.ref_chirp_real(delayed_ref_i), // From latency buffer (long or short ref)
|
||||
.ref_chirp_imag(delayed_ref_q),
|
||||
.segment_request(segment_request),
|
||||
.mem_request(mem_request),
|
||||
.sample_addr_out(sample_addr_from_chain),
|
||||
|
||||
@@ -253,6 +253,68 @@ run_lint_static() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: compile, run, and compare a matched-filter co-sim scenario
|
||||
# run_mf_cosim <scenario_name> <define_flag>
|
||||
# ---------------------------------------------------------------------------
|
||||
run_mf_cosim() {
|
||||
local name="$1"
|
||||
local define="$2"
|
||||
local vvp="tb/tb_mf_cosim_${name}.vvp"
|
||||
local scenario_lower="$name"
|
||||
|
||||
printf " %-45s " "MF Co-Sim ($name)"
|
||||
|
||||
# Compile — build command as string to handle optional define
|
||||
local cmd="iverilog -g2001 -DSIMULATION"
|
||||
if [[ -n "$define" ]]; then
|
||||
cmd="$cmd $define"
|
||||
fi
|
||||
cmd="$cmd -o $vvp tb/tb_mf_cosim.v matched_filter_processing_chain.v fft_engine.v chirp_memory_loader_param.v"
|
||||
|
||||
if ! eval "$cmd" 2>/tmp/iverilog_err_$$; then
|
||||
echo -e "${RED}COMPILE FAIL${NC}"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): compile error ($(head -1 /tmp/iverilog_err_$$))"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# Run TB
|
||||
local output
|
||||
output=$(timeout 120 vvp "$vvp" 2>&1) || true
|
||||
rm -f "$vvp"
|
||||
|
||||
# Check TB internal pass/fail
|
||||
local tb_fail
|
||||
tb_fail=$(echo "$output" | grep -Ec '^\[FAIL' || true)
|
||||
if [[ "$tb_fail" -gt 0 ]]; then
|
||||
echo -e "${RED}FAIL${NC} (TB internal failure)"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): TB internal failure"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
# Run Python compare
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
local compare_out
|
||||
local compare_rc=0
|
||||
compare_out=$(python3 tb/cosim/compare_mf.py "$scenario_lower" 2>&1) || compare_rc=$?
|
||||
if [[ "$compare_rc" -ne 0 ]]; then
|
||||
echo -e "${RED}FAIL${NC} (compare_mf.py mismatch)"
|
||||
ERRORS="$ERRORS\n MF Co-Sim ($name): Python compare failed"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}SKIP${NC} (RTL passed, python3 not found — compare skipped)"
|
||||
SKIP=$((SKIP + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}PASS${NC} (RTL + Python compare)"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: compile and run a single testbench
|
||||
# run_test <name> <vvp_path> <iverilog_args...>
|
||||
@@ -416,30 +478,14 @@ run_test "Full-Chain Real-Data (decim→Doppler, exact match)" \
|
||||
doppler_processor.v xfft_16.v fft_engine.v
|
||||
|
||||
if [[ "$QUICK" -eq 0 ]]; then
|
||||
# Golden generate
|
||||
run_test "Receiver (golden generate)" \
|
||||
tb/tb_rx_golden_reg.vvp \
|
||||
-DGOLDEN_GENERATE \
|
||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
||||
chirp_memory_loader_param.v latency_buffer.v \
|
||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
||||
rx_gain_control.v mti_canceller.v
|
||||
|
||||
# Golden compare
|
||||
run_test "Receiver (golden compare)" \
|
||||
tb/tb_rx_compare_reg.vvp \
|
||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
||||
chirp_memory_loader_param.v latency_buffer.v \
|
||||
matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
||||
rx_gain_control.v mti_canceller.v
|
||||
# NOTE: The "Receiver golden generate/compare" pair was REMOVED because
|
||||
# it was self-blessing: both passes ran the same RTL with the same
|
||||
# deterministic stimulus, so the test always passed regardless of bugs.
|
||||
# Real co-sim coverage is provided by:
|
||||
# - tb_doppler_realdata.v (committed Python golden hex, exact match)
|
||||
# - tb_fullchain_realdata.v (committed Python golden hex, exact match)
|
||||
# A proper full-pipeline co-sim (DDC→MF→Decim→Doppler vs Python) is
|
||||
# planned as a replacement (Phase C of CI test plan).
|
||||
|
||||
# Full system top (monitoring-only, legacy)
|
||||
run_test "System Top (radar_system_tb)" \
|
||||
@@ -469,12 +515,28 @@ if [[ "$QUICK" -eq 0 ]]; then
|
||||
usb_data_interface.v edge_detector.v radar_mode_controller.v \
|
||||
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
|
||||
else
|
||||
echo " (skipped receiver golden + system top + E2E — use without --quick)"
|
||||
SKIP=$((SKIP + 4))
|
||||
echo " (skipped system top + E2E — use without --quick)"
|
||||
SKIP=$((SKIP + 2))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 2b: MATCHED FILTER CO-SIMULATION (RTL vs Python golden reference)
|
||||
# Runs tb_mf_cosim.v for 4 scenarios, then compare_mf.py validates output
|
||||
# against committed Python golden CSV files. In SIMULATION mode, thresholds
|
||||
# are generous (behavioral vs fixed-point twiddles differ) — validates
|
||||
# state machine mechanics, output count, and energy sanity.
|
||||
# ===========================================================================
|
||||
echo "--- PHASE 2b: Matched Filter Co-Sim ---"
|
||||
|
||||
run_mf_cosim "chirp" ""
|
||||
run_mf_cosim "dc" "-DSCENARIO_DC"
|
||||
run_mf_cosim "impulse" "-DSCENARIO_IMPULSE"
|
||||
run_mf_cosim "tone5" "-DSCENARIO_TONE5"
|
||||
|
||||
echo ""
|
||||
|
||||
# ===========================================================================
|
||||
# PHASE 3: UNIT TESTS — Signal Processing
|
||||
# ===========================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
"""
|
||||
golden_reference.py — AERIS-10 FPGA bit-accurate golden reference model
|
||||
|
||||
Uses ADI CN0566 Phaser radar data (10.525 GHz X-band FMCW) to validate
|
||||
the FPGA signal processing pipeline stage by stage:
|
||||
Uses ADI CN0566 Phaser radar data (10.525 GHz, used as test stimulus only) to
|
||||
validate the FPGA signal processing pipeline stage by stage:
|
||||
|
||||
ADC → DDC (NCO+mixer+CIC+FIR) → Range FFT → Doppler FFT → Detection
|
||||
|
||||
@@ -90,7 +90,8 @@ HAMMING_Q15 = [
|
||||
0x3088, 0x1B6D, 0x0E5C, 0x0A3D,
|
||||
]
|
||||
|
||||
# ADI dataset parameters
|
||||
# ADI dataset parameters — used ONLY for loading/requantizing ADI Phaser test data.
|
||||
# These are NOT PLFM hardware parameters. See AERIS-10 constants below.
|
||||
ADI_SAMPLE_RATE = 4e6 # 4 MSPS
|
||||
ADI_IF_FREQ = 100e3 # 100 kHz IF
|
||||
ADI_RF_FREQ = 9.9e9 # 9.9 GHz
|
||||
@@ -99,9 +100,17 @@ ADI_RAMP_TIME = 300e-6 # 300 us
|
||||
ADI_NUM_CHIRPS = 256
|
||||
ADI_SAMPLES_PER_CHIRP = 1079
|
||||
|
||||
# AERIS-10 parameters
|
||||
AERIS_FS = 400e6 # 400 MHz ADC clock
|
||||
AERIS_IF = 120e6 # 120 MHz IF
|
||||
# AERIS-10 hardware parameters (from ADF4382/AD9523/main.cpp configuration)
|
||||
AERIS_FS = 400e6 # 400 MHz ADC clock (AD9523 OUT4)
|
||||
AERIS_IF = 120e6 # 120 MHz IF (TX 10.5 GHz - RX 10.38 GHz)
|
||||
AERIS_FS_PROCESSING = 100e6 # Post-DDC rate (400 MSPS / 4x CIC)
|
||||
AERIS_CARRIER_HZ = 10.5e9 # TX LO (ADF4382, verified)
|
||||
AERIS_RX_LO_HZ = 10.38e9 # RX LO (ADF4382)
|
||||
AERIS_CHIRP_BW = 20e6 # Chirp bandwidth (target: 30 MHz Phase 1)
|
||||
AERIS_LONG_CHIRP_S = 30e-6 # Long chirp duration
|
||||
AERIS_PRI_S = 167e-6 # Pulse repetition interval
|
||||
AERIS_DECIMATION = 16 # Range bin decimation (1024 → 64)
|
||||
AERIS_RANGE_PER_BIN = 24.0 # Meters per decimated bin
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -421,13 +421,13 @@ def test_latency_buffer():
|
||||
#
|
||||
# For synthesis: the latency_buffer feeds ref data to the chain via
|
||||
# chirp_memory_loader_param → latency_buffer → chain.
|
||||
# But wait — looking at radar_receiver_final.v:
|
||||
# Looking at radar_receiver_final.v:
|
||||
# - mem_request drives valid_in on the latency buffer
|
||||
# - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles
|
||||
# - The delayed output feeds long_chirp_real/imag → chain
|
||||
# - The delayed output feeds ref_chirp_real/imag → chain
|
||||
#
|
||||
# The purpose: the chain in the SYNTHESIS branch reads reference data
|
||||
# via the long_chirp_real/imag ports DURING ST_FWD_FFT (while collecting
|
||||
# via the ref_chirp_real/imag ports DURING ST_FWD_FFT (while collecting
|
||||
# input samples). The reference data needs to arrive LATENCY cycles
|
||||
# after the first mem_request, where LATENCY accounts for:
|
||||
# - The fft_engine pipeline latency from input to output
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,10 +18,8 @@ module tb_matched_filter_processing_chain;
|
||||
reg [15:0] adc_data_q;
|
||||
reg adc_valid;
|
||||
reg [5:0] chirp_counter;
|
||||
reg [15:0] long_chirp_real;
|
||||
reg [15:0] long_chirp_imag;
|
||||
reg [15:0] short_chirp_real;
|
||||
reg [15:0] short_chirp_imag;
|
||||
reg [15:0] ref_chirp_real;
|
||||
reg [15:0] ref_chirp_imag;
|
||||
wire signed [15:0] range_profile_i;
|
||||
wire signed [15:0] range_profile_q;
|
||||
wire range_profile_valid;
|
||||
@@ -83,10 +81,8 @@ module tb_matched_filter_processing_chain;
|
||||
.adc_data_q (adc_data_q),
|
||||
.adc_valid (adc_valid),
|
||||
.chirp_counter (chirp_counter),
|
||||
.long_chirp_real (long_chirp_real),
|
||||
.long_chirp_imag (long_chirp_imag),
|
||||
.short_chirp_real (short_chirp_real),
|
||||
.short_chirp_imag (short_chirp_imag),
|
||||
.ref_chirp_real (ref_chirp_real),
|
||||
.ref_chirp_imag (ref_chirp_imag),
|
||||
.range_profile_i (range_profile_i),
|
||||
.range_profile_q (range_profile_q),
|
||||
.range_profile_valid (range_profile_valid),
|
||||
@@ -133,10 +129,8 @@ module tb_matched_filter_processing_chain;
|
||||
adc_data_i = 16'd0;
|
||||
adc_data_q = 16'd0;
|
||||
chirp_counter = 6'd0;
|
||||
long_chirp_real = 16'd0;
|
||||
long_chirp_imag = 16'd0;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'd0;
|
||||
ref_chirp_imag = 16'd0;
|
||||
cap_enable = 0;
|
||||
cap_count = 0;
|
||||
cap_max_abs = 0;
|
||||
@@ -168,10 +162,8 @@ module tb_matched_filter_processing_chain;
|
||||
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
|
||||
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
||||
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
||||
long_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||
long_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||
ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -187,10 +179,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||
adc_data_i = 16'sh1000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -233,10 +223,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||
adc_data_i = gold_sig_i[k];
|
||||
adc_data_q = gold_sig_q[k];
|
||||
long_chirp_real = gold_ref_i[k];
|
||||
long_chirp_imag = gold_ref_q[k];
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = gold_ref_i[k];
|
||||
ref_chirp_imag = gold_ref_q[k];
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -374,10 +362,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'd0;
|
||||
adc_data_q = 16'd0;
|
||||
long_chirp_real = 16'd0;
|
||||
long_chirp_imag = 16'd0;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'd0;
|
||||
ref_chirp_imag = 16'd0;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -449,10 +435,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
|
||||
adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0));
|
||||
long_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
|
||||
long_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
|
||||
ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -568,10 +552,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'sh7FFF;
|
||||
adc_data_q = 16'sh7FFF;
|
||||
long_chirp_real = 16'sh7FFF;
|
||||
long_chirp_imag = 16'sh7FFF;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh7FFF;
|
||||
ref_chirp_imag = 16'sh7FFF;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -589,10 +571,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'sh8000;
|
||||
adc_data_q = 16'sh8000;
|
||||
long_chirp_real = 16'sh8000;
|
||||
long_chirp_imag = 16'sh8000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh8000;
|
||||
ref_chirp_imag = 16'sh8000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -611,16 +591,14 @@ module tb_matched_filter_processing_chain;
|
||||
if (i % 2 == 0) begin
|
||||
adc_data_i = 16'sh7FFF;
|
||||
adc_data_q = 16'sh7FFF;
|
||||
long_chirp_real = 16'sh7FFF;
|
||||
long_chirp_imag = 16'sh7FFF;
|
||||
ref_chirp_real = 16'sh7FFF;
|
||||
ref_chirp_imag = 16'sh7FFF;
|
||||
end else begin
|
||||
adc_data_i = 16'sh8000;
|
||||
adc_data_q = 16'sh8000;
|
||||
long_chirp_real = 16'sh8000;
|
||||
long_chirp_imag = 16'sh8000;
|
||||
ref_chirp_real = 16'sh8000;
|
||||
ref_chirp_imag = 16'sh8000;
|
||||
end
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -641,10 +619,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < 512; i = i + 1) begin
|
||||
adc_data_i = 16'sh1000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -683,10 +659,8 @@ module tb_matched_filter_processing_chain;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'sh1000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
|
||||
|
||||
@@ -28,10 +28,8 @@ module tb_mf_chain_synth;
|
||||
reg [15:0] adc_data_q;
|
||||
reg adc_valid;
|
||||
reg [5:0] chirp_counter;
|
||||
reg [15:0] long_chirp_real;
|
||||
reg [15:0] long_chirp_imag;
|
||||
reg [15:0] short_chirp_real;
|
||||
reg [15:0] short_chirp_imag;
|
||||
reg [15:0] ref_chirp_real;
|
||||
reg [15:0] ref_chirp_imag;
|
||||
wire signed [15:0] range_profile_i;
|
||||
wire signed [15:0] range_profile_q;
|
||||
wire range_profile_valid;
|
||||
@@ -78,10 +76,8 @@ module tb_mf_chain_synth;
|
||||
.adc_data_q (adc_data_q),
|
||||
.adc_valid (adc_valid),
|
||||
.chirp_counter (chirp_counter),
|
||||
.long_chirp_real (long_chirp_real),
|
||||
.long_chirp_imag (long_chirp_imag),
|
||||
.short_chirp_real (short_chirp_real),
|
||||
.short_chirp_imag (short_chirp_imag),
|
||||
.ref_chirp_real (ref_chirp_real),
|
||||
.ref_chirp_imag (ref_chirp_imag),
|
||||
.range_profile_i (range_profile_i),
|
||||
.range_profile_q (range_profile_q),
|
||||
.range_profile_valid (range_profile_valid),
|
||||
@@ -130,10 +126,8 @@ module tb_mf_chain_synth;
|
||||
adc_data_i = 16'd0;
|
||||
adc_data_q = 16'd0;
|
||||
chirp_counter = 6'd0;
|
||||
long_chirp_real = 16'd0;
|
||||
long_chirp_imag = 16'd0;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'd0;
|
||||
ref_chirp_imag = 16'd0;
|
||||
cap_enable = 0;
|
||||
cap_count = 0;
|
||||
cap_max_abs = 0;
|
||||
@@ -177,10 +171,8 @@ module tb_mf_chain_synth;
|
||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||
adc_data_i = 16'sh1000; // +4096
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -199,10 +191,8 @@ module tb_mf_chain_synth;
|
||||
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
|
||||
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
||||
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
||||
long_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||
long_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||
ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -219,16 +209,14 @@ module tb_mf_chain_synth;
|
||||
if (k == 0) begin
|
||||
adc_data_i = 16'sh4000; // 0.5 in Q15
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh4000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
ref_chirp_real = 16'sh4000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
end else begin
|
||||
adc_data_i = 16'sh0000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh0000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
ref_chirp_real = 16'sh0000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
end
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk);
|
||||
#1;
|
||||
@@ -309,10 +297,8 @@ module tb_mf_chain_synth;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'd0;
|
||||
adc_data_q = 16'd0;
|
||||
long_chirp_real = 16'd0;
|
||||
long_chirp_imag = 16'd0;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'd0;
|
||||
ref_chirp_imag = 16'd0;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -379,10 +365,8 @@ module tb_mf_chain_synth;
|
||||
for (i = 0; i < 512; i = i + 1) begin
|
||||
adc_data_i = 16'sh1000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -439,10 +423,8 @@ module tb_mf_chain_synth;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
|
||||
adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0));
|
||||
long_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
|
||||
long_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
|
||||
ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -469,10 +451,8 @@ module tb_mf_chain_synth;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'sh7FFF;
|
||||
adc_data_q = 16'sh7FFF;
|
||||
long_chirp_real = 16'sh7FFF;
|
||||
long_chirp_imag = 16'sh7FFF;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh7FFF;
|
||||
ref_chirp_imag = 16'sh7FFF;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
end
|
||||
@@ -495,10 +475,8 @@ module tb_mf_chain_synth;
|
||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||
adc_data_i = 16'sh1000;
|
||||
adc_data_q = 16'sh0000;
|
||||
long_chirp_real = 16'sh1000;
|
||||
long_chirp_imag = 16'sh0000;
|
||||
short_chirp_real = 16'd0;
|
||||
short_chirp_imag = 16'd0;
|
||||
ref_chirp_real = 16'sh1000;
|
||||
ref_chirp_imag = 16'sh0000;
|
||||
adc_valid = 1'b1;
|
||||
@(posedge clk); #1;
|
||||
|
||||
|
||||
@@ -88,10 +88,8 @@ reg [15:0] adc_data_i;
|
||||
reg [15:0] adc_data_q;
|
||||
reg adc_valid;
|
||||
reg [5:0] chirp_counter;
|
||||
reg [15:0] long_chirp_real;
|
||||
reg [15:0] long_chirp_imag;
|
||||
reg [15:0] short_chirp_real;
|
||||
reg [15:0] short_chirp_imag;
|
||||
reg [15:0] ref_chirp_real;
|
||||
reg [15:0] ref_chirp_imag;
|
||||
|
||||
wire signed [15:0] range_profile_i;
|
||||
wire signed [15:0] range_profile_q;
|
||||
@@ -108,10 +106,8 @@ matched_filter_processing_chain dut (
|
||||
.adc_data_q(adc_data_q),
|
||||
.adc_valid(adc_valid),
|
||||
.chirp_counter(chirp_counter),
|
||||
.long_chirp_real(long_chirp_real),
|
||||
.long_chirp_imag(long_chirp_imag),
|
||||
.short_chirp_real(short_chirp_real),
|
||||
.short_chirp_imag(short_chirp_imag),
|
||||
.ref_chirp_real(ref_chirp_real),
|
||||
.ref_chirp_imag(ref_chirp_imag),
|
||||
.range_profile_i(range_profile_i),
|
||||
.range_profile_q(range_profile_q),
|
||||
.range_profile_valid(range_profile_valid),
|
||||
@@ -157,10 +153,8 @@ task apply_reset;
|
||||
adc_data_q <= 16'd0;
|
||||
adc_valid <= 1'b0;
|
||||
chirp_counter <= 6'd0;
|
||||
long_chirp_real <= 16'd0;
|
||||
long_chirp_imag <= 16'd0;
|
||||
short_chirp_real <= 16'd0;
|
||||
short_chirp_imag <= 16'd0;
|
||||
ref_chirp_real <= 16'd0;
|
||||
ref_chirp_imag <= 16'd0;
|
||||
repeat(4) @(posedge clk);
|
||||
reset_n <= 1'b1;
|
||||
@(posedge clk);
|
||||
@@ -201,18 +195,16 @@ initial begin
|
||||
@(posedge clk);
|
||||
adc_data_i <= sig_mem_i[i];
|
||||
adc_data_q <= sig_mem_q[i];
|
||||
long_chirp_real <= ref_mem_i[i];
|
||||
long_chirp_imag <= ref_mem_q[i];
|
||||
short_chirp_real <= 16'd0;
|
||||
short_chirp_imag <= 16'd0;
|
||||
ref_chirp_real <= ref_mem_i[i];
|
||||
ref_chirp_imag <= ref_mem_q[i];
|
||||
adc_valid <= 1'b1;
|
||||
end
|
||||
@(posedge clk);
|
||||
adc_valid <= 1'b0;
|
||||
adc_data_i <= 16'd0;
|
||||
adc_data_q <= 16'd0;
|
||||
long_chirp_real <= 16'd0;
|
||||
long_chirp_imag <= 16'd0;
|
||||
ref_chirp_real <= 16'd0;
|
||||
ref_chirp_imag <= 16'd0;
|
||||
|
||||
$display("All samples fed. Waiting for processing...");
|
||||
|
||||
|
||||
@@ -56,10 +56,8 @@ reg [5:0] chirp_counter;
|
||||
reg mc_new_chirp;
|
||||
reg mc_new_elevation;
|
||||
reg mc_new_azimuth;
|
||||
reg [15:0] long_chirp_real;
|
||||
reg [15:0] long_chirp_imag;
|
||||
reg [15:0] short_chirp_real;
|
||||
reg [15:0] short_chirp_imag;
|
||||
reg [15:0] ref_chirp_real;
|
||||
reg [15:0] ref_chirp_imag;
|
||||
reg mem_ready;
|
||||
|
||||
wire signed [15:0] pc_i_w;
|
||||
@@ -84,10 +82,8 @@ matched_filter_multi_segment dut (
|
||||
.mc_new_chirp(mc_new_chirp),
|
||||
.mc_new_elevation(mc_new_elevation),
|
||||
.mc_new_azimuth(mc_new_azimuth),
|
||||
.long_chirp_real(long_chirp_real),
|
||||
.long_chirp_imag(long_chirp_imag),
|
||||
.short_chirp_real(short_chirp_real),
|
||||
.short_chirp_imag(short_chirp_imag),
|
||||
.ref_chirp_real(ref_chirp_real),
|
||||
.ref_chirp_imag(ref_chirp_imag),
|
||||
.segment_request(segment_request),
|
||||
.sample_addr_out(sample_addr_out),
|
||||
.mem_request(mem_request),
|
||||
@@ -123,11 +119,11 @@ end
|
||||
always @(posedge clk) begin
|
||||
if (mem_request) begin
|
||||
if (use_long_chirp) begin
|
||||
long_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}];
|
||||
long_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}];
|
||||
ref_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}];
|
||||
ref_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}];
|
||||
end else begin
|
||||
short_chirp_real <= ref_mem_i[sample_addr_out];
|
||||
short_chirp_imag <= ref_mem_q[sample_addr_out];
|
||||
ref_chirp_real <= ref_mem_i[sample_addr_out];
|
||||
ref_chirp_imag <= ref_mem_q[sample_addr_out];
|
||||
end
|
||||
mem_ready <= 1'b1;
|
||||
end else begin
|
||||
@@ -176,10 +172,8 @@ task apply_reset;
|
||||
mc_new_chirp <= 1'b0;
|
||||
mc_new_elevation <= 1'b0;
|
||||
mc_new_azimuth <= 1'b0;
|
||||
long_chirp_real <= 16'd0;
|
||||
long_chirp_imag <= 16'd0;
|
||||
short_chirp_real <= 16'd0;
|
||||
short_chirp_imag <= 16'd0;
|
||||
ref_chirp_real <= 16'd0;
|
||||
ref_chirp_imag <= 16'd0;
|
||||
mem_ready <= 1'b0;
|
||||
repeat(10) @(posedge clk);
|
||||
reset_n <= 1'b1;
|
||||
|
||||
@@ -7,43 +7,21 @@
|
||||
// -> matched_filter_multi_segment -> range_bin_decimator
|
||||
// -> doppler_processor_optimized -> doppler_output
|
||||
//
|
||||
// ============================================================================
|
||||
// TWO MODES (compile-time define):
|
||||
//
|
||||
// 1. GOLDEN_GENERATE mode (-DGOLDEN_GENERATE):
|
||||
// Dumps all Doppler output samples to golden reference files.
|
||||
// Run once on known-good RTL:
|
||||
// iverilog -g2001 -DSIMULATION -DGOLDEN_GENERATE -o tb_golden_gen.vvp \
|
||||
// <src files> tb/tb_radar_receiver_final.v
|
||||
// mkdir -p tb/golden
|
||||
// vvp tb_golden_gen.vvp
|
||||
//
|
||||
// 2. Default mode (no GOLDEN_GENERATE):
|
||||
// Loads golden files, compares each Doppler output against reference,
|
||||
// and runs physics-based bounds checks.
|
||||
// iverilog -g2001 -DSIMULATION -o tb_radar_receiver_final.vvp \
|
||||
// <src files> tb/tb_radar_receiver_final.v
|
||||
// vvp tb_radar_receiver_final.vvp
|
||||
//
|
||||
// PREREQUISITES:
|
||||
// - The directory tb/golden/ must exist before running either mode.
|
||||
// Create it with: mkdir -p tb/golden
|
||||
//
|
||||
// TAP POINTS:
|
||||
// Tap 1 (DDC output) - bounds checking only (CDC jitter -> non-deterministic)
|
||||
// Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i
|
||||
// Tap 2 (Doppler output) - golden compared (deterministic after MF buffering)
|
||||
// Tap 2 (Doppler output) - structural + bounds checks (deterministic after MF)
|
||||
// Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0],
|
||||
// range_bin_out[5:0]
|
||||
//
|
||||
// Golden file: tb/golden/golden_doppler.mem
|
||||
// 2048 entries of 32-bit hex, indexed by range_bin*32 + doppler_bin
|
||||
//
|
||||
// Strategy:
|
||||
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
|
||||
// - Overrides radar_mode_controller timing params for fast simulation
|
||||
// - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband)
|
||||
// - Verifies structural correctness + golden comparison + bounds checks
|
||||
// - Verifies structural correctness (S1-S10) + physics bounds checks (B1-B5)
|
||||
// - Bit-accurate golden comparison is done by the MF co-sim tests
|
||||
// (tb_mf_cosim.v + compare_mf.py) and full-chain co-sim tests
|
||||
// (tb_doppler_realdata.v, tb_fullchain_realdata.v), not here.
|
||||
//
|
||||
// Convention: check task, VCD dump, CSV output, pass/fail summary
|
||||
// ============================================================================
|
||||
@@ -194,46 +172,6 @@ task check;
|
||||
end
|
||||
endtask
|
||||
|
||||
// ============================================================================
|
||||
// GOLDEN MEMORY DECLARATIONS AND LOAD/STORE LOGIC
|
||||
// ============================================================================
|
||||
localparam GOLDEN_ENTRIES = 2048; // 64 range bins * 32 Doppler bins
|
||||
localparam GOLDEN_TOLERANCE = 2; // +/- 2 LSB tolerance for comparison
|
||||
|
||||
reg [31:0] golden_doppler [0:2047];
|
||||
|
||||
// -- Golden comparison tracking --
|
||||
integer golden_match_count;
|
||||
integer golden_mismatch_count;
|
||||
integer golden_max_err_i;
|
||||
integer golden_max_err_q;
|
||||
integer golden_compare_count;
|
||||
|
||||
`ifdef GOLDEN_GENERATE
|
||||
// In generate mode, we just initialize the array to X/0
|
||||
// and fill it as outputs arrive
|
||||
integer gi;
|
||||
initial begin
|
||||
for (gi = 0; gi < GOLDEN_ENTRIES; gi = gi + 1)
|
||||
golden_doppler[gi] = 32'd0;
|
||||
golden_match_count = 0;
|
||||
golden_mismatch_count = 0;
|
||||
golden_max_err_i = 0;
|
||||
golden_max_err_q = 0;
|
||||
golden_compare_count = 0;
|
||||
end
|
||||
`else
|
||||
// In comparison mode, load the golden reference
|
||||
initial begin
|
||||
$readmemh("tb/golden/golden_doppler.mem", golden_doppler);
|
||||
golden_match_count = 0;
|
||||
golden_mismatch_count = 0;
|
||||
golden_max_err_i = 0;
|
||||
golden_max_err_q = 0;
|
||||
golden_compare_count = 0;
|
||||
end
|
||||
`endif
|
||||
|
||||
// ============================================================================
|
||||
// DDC ENERGY ACCUMULATOR (Bounds Check B1)
|
||||
// ============================================================================
|
||||
@@ -257,7 +195,7 @@ always @(posedge clk_100m) begin
|
||||
end
|
||||
|
||||
// ============================================================================
|
||||
// DOPPLER OUTPUT CAPTURE, GOLDEN COMPARISON, AND DUPLICATE DETECTION
|
||||
// DOPPLER OUTPUT CAPTURE AND DUPLICATE DETECTION
|
||||
// ============================================================================
|
||||
integer doppler_output_count;
|
||||
integer doppler_frame_count;
|
||||
@@ -311,13 +249,6 @@ end
|
||||
// Monitor doppler outputs -- only after reset released
|
||||
always @(posedge clk_100m) begin
|
||||
if (reset_n && doppler_valid) begin : doppler_capture_block
|
||||
// ---- Signed intermediates for golden comparison ----
|
||||
reg signed [16:0] actual_i, actual_q;
|
||||
reg signed [16:0] expected_i, expected_q;
|
||||
reg signed [16:0] err_i_signed, err_q_signed;
|
||||
integer abs_err_i, abs_err_q;
|
||||
integer gidx;
|
||||
reg [31:0] expected_val;
|
||||
// ---- Magnitude intermediates for B2 ----
|
||||
reg signed [16:0] mag_i_signed, mag_q_signed;
|
||||
integer mag_i, mag_q, mag_sum;
|
||||
@@ -350,9 +281,6 @@ always @(posedge clk_100m) begin
|
||||
if ((doppler_output_count % 256) == 0)
|
||||
$display("[INFO] %0d doppler outputs so far (t=%0t)", doppler_output_count, $time);
|
||||
|
||||
// ---- Golden index computation ----
|
||||
gidx = range_bin_out * 32 + doppler_bin;
|
||||
|
||||
// ---- Duplicate detection (B5) ----
|
||||
if (range_bin_out < 64 && doppler_bin < 32) begin
|
||||
if (index_seen[range_bin_out][doppler_bin]) begin
|
||||
@@ -376,44 +304,6 @@ always @(posedge clk_100m) begin
|
||||
if (mag_sum > peak_dbin_mag[range_bin_out])
|
||||
peak_dbin_mag[range_bin_out] = mag_sum;
|
||||
end
|
||||
|
||||
`ifdef GOLDEN_GENERATE
|
||||
// ---- GOLDEN GENERATE: store output ----
|
||||
if (gidx < GOLDEN_ENTRIES)
|
||||
golden_doppler[gidx] = doppler_output;
|
||||
`else
|
||||
// ---- GOLDEN COMPARE: check against reference ----
|
||||
if (gidx < GOLDEN_ENTRIES) begin
|
||||
expected_val = golden_doppler[gidx];
|
||||
|
||||
actual_i = $signed(doppler_output[15:0]);
|
||||
actual_q = $signed(doppler_output[31:16]);
|
||||
expected_i = $signed(expected_val[15:0]);
|
||||
expected_q = $signed(expected_val[31:16]);
|
||||
|
||||
err_i_signed = actual_i - expected_i;
|
||||
err_q_signed = actual_q - expected_q;
|
||||
|
||||
abs_err_i = (err_i_signed < 0) ? -err_i_signed : err_i_signed;
|
||||
abs_err_q = (err_q_signed < 0) ? -err_q_signed : err_q_signed;
|
||||
|
||||
golden_compare_count = golden_compare_count + 1;
|
||||
|
||||
if (abs_err_i > golden_max_err_i) golden_max_err_i = abs_err_i;
|
||||
if (abs_err_q > golden_max_err_q) golden_max_err_q = abs_err_q;
|
||||
|
||||
if (abs_err_i <= GOLDEN_TOLERANCE && abs_err_q <= GOLDEN_TOLERANCE) begin
|
||||
golden_match_count = golden_match_count + 1;
|
||||
end else begin
|
||||
golden_mismatch_count = golden_mismatch_count + 1;
|
||||
if (golden_mismatch_count <= 20)
|
||||
$display("[MISMATCH] idx=%0d rbin=%0d dbin=%0d actual=%08h expected=%08h err_i=%0d err_q=%0d",
|
||||
gidx, range_bin_out, doppler_bin,
|
||||
doppler_output, expected_val,
|
||||
abs_err_i, abs_err_q);
|
||||
end
|
||||
end
|
||||
`endif
|
||||
end
|
||||
|
||||
// Track frame completions via doppler_proc -- only after reset
|
||||
@@ -556,13 +446,6 @@ initial begin
|
||||
end
|
||||
end
|
||||
|
||||
// ---- DUMP GOLDEN FILE (generate mode only) ----
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$writememh("tb/golden/golden_doppler.mem", golden_doppler);
|
||||
$display("[GOLDEN_GENERATE] Wrote tb/golden/golden_doppler.mem (%0d entries captured)",
|
||||
doppler_output_count);
|
||||
`endif
|
||||
|
||||
// ================================================================
|
||||
// RUN CHECKS
|
||||
// ================================================================
|
||||
@@ -649,33 +532,7 @@ initial begin
|
||||
"S10: DDC produced substantial output (>100 valid samples)");
|
||||
|
||||
// ================================================================
|
||||
// GOLDEN COMPARISON REPORT
|
||||
// ================================================================
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$display("");
|
||||
$display("Golden comparison: SKIPPED (GOLDEN_GENERATE mode)");
|
||||
$display(" Wrote golden reference with %0d Doppler samples", doppler_output_count);
|
||||
`else
|
||||
$display("");
|
||||
$display("------------------------------------------------------------");
|
||||
$display("GOLDEN COMPARISON (tolerance=%0d LSB)", GOLDEN_TOLERANCE);
|
||||
$display("------------------------------------------------------------");
|
||||
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
|
||||
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
|
||||
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
|
||||
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
|
||||
|
||||
// CHECK G1: All golden comparisons match
|
||||
if (golden_compare_count > 0) begin
|
||||
check(golden_mismatch_count == 0,
|
||||
"G1: All Doppler outputs match golden reference within tolerance");
|
||||
end else begin
|
||||
check(0, "G1: All Doppler outputs match golden reference (NO COMPARISONS)");
|
||||
end
|
||||
`endif
|
||||
|
||||
// ================================================================
|
||||
// BOUNDS CHECKS (active in both modes)
|
||||
// BOUNDS CHECKS
|
||||
// ================================================================
|
||||
$display("");
|
||||
$display("------------------------------------------------------------");
|
||||
@@ -748,16 +605,8 @@ initial begin
|
||||
// ================================================================
|
||||
$display("");
|
||||
$display("============================================================");
|
||||
$display("INTEGRATION TEST -- GOLDEN COMPARISON + BOUNDS");
|
||||
$display("INTEGRATION TEST -- STRUCTURAL + BOUNDS");
|
||||
$display("============================================================");
|
||||
`ifdef GOLDEN_GENERATE
|
||||
$display("Mode: GOLDEN_GENERATE (reference dump, comparison skipped)");
|
||||
`else
|
||||
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
|
||||
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
|
||||
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
|
||||
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
|
||||
`endif
|
||||
$display("Bounds checks:");
|
||||
$display(" B1: DDC RMS energy in range [%0d, %0d]",
|
||||
(ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY);
|
||||
|
||||
@@ -108,7 +108,7 @@ class GPSData:
|
||||
@dataclass
|
||||
class RadarSettings:
|
||||
"""Radar system configuration"""
|
||||
system_frequency: float = 10e9 # Hz
|
||||
system_frequency: float = 10.5e9 # Hz (PLFM TX LO)
|
||||
chirp_duration_1: float = 30e-6 # Long chirp duration (s)
|
||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration (s)
|
||||
chirps_per_position: int = 32
|
||||
@@ -116,8 +116,8 @@ class RadarSettings:
|
||||
freq_max: float = 30e6 # Hz
|
||||
prf1: float = 1000 # PRF 1 (Hz)
|
||||
prf2: float = 2000 # PRF 2 (Hz)
|
||||
max_distance: float = 50000 # Max detection range (m)
|
||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
||||
max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m
|
||||
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||
|
||||
|
||||
class TileServer(Enum):
|
||||
@@ -198,7 +198,7 @@ class RadarMapWidget(QWidget):
|
||||
pitch=0.0
|
||||
)
|
||||
self._targets: list[RadarTarget] = []
|
||||
self._coverage_radius = 50000 # meters
|
||||
self._coverage_radius = 1536 # meters (64 bins x 24 m, 3 km mode)
|
||||
self._tile_server = TileServer.OPENSTREETMAP
|
||||
self._show_coverage = True
|
||||
self._show_trails = False
|
||||
@@ -1088,7 +1088,7 @@ class TargetSimulator(QObject):
|
||||
new_range = target.range - target.velocity * 0.5 # 0.5 second update
|
||||
|
||||
# Check if target is still in range
|
||||
if new_range < 500 or new_range > 50000:
|
||||
if new_range < 50 or new_range > 1536:
|
||||
# Remove this target and add a new one
|
||||
continue
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class RadarTarget:
|
||||
|
||||
@dataclass
|
||||
class RadarSettings:
|
||||
system_frequency: float = 10e9
|
||||
system_frequency: float = 10.5e9
|
||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||
chirps_per_position: int = 32
|
||||
@@ -89,8 +89,8 @@ class RadarSettings:
|
||||
freq_max: float = 30e6
|
||||
prf1: float = 1000
|
||||
prf2: float = 2000
|
||||
max_distance: float = 50000
|
||||
map_size: float = 50000 # Map size in meters
|
||||
max_distance: float = 1536
|
||||
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1196,8 +1196,8 @@ class RadarGUI:
|
||||
("Frequency Max (Hz):", "freq_max", 30e6),
|
||||
("PRF1 (Hz):", "prf1", 1000),
|
||||
("PRF2 (Hz):", "prf2", 2000),
|
||||
("Max Distance (m):", "max_distance", 50000),
|
||||
("Map Size (m):", "map_size", 50000),
|
||||
("Max Distance (m):", "max_distance", 1536),
|
||||
("Map Size (m):", "map_size", 1536),
|
||||
("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"),
|
||||
]
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class RadarTarget:
|
||||
|
||||
@dataclass
|
||||
class RadarSettings:
|
||||
system_frequency: float = 10e9
|
||||
system_frequency: float = 10.5e9
|
||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||
chirps_per_position: int = 32
|
||||
@@ -85,8 +85,8 @@ class RadarSettings:
|
||||
freq_max: float = 30e6
|
||||
prf1: float = 1000
|
||||
prf2: float = 2000
|
||||
max_distance: float = 50000
|
||||
map_size: float = 50000 # Map size in meters
|
||||
max_distance: float = 1536
|
||||
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -1254,8 +1254,8 @@ class RadarGUI:
|
||||
("Frequency Max (Hz):", "freq_max", 30e6),
|
||||
("PRF1 (Hz):", "prf1", 1000),
|
||||
("PRF2 (Hz):", "prf2", 2000),
|
||||
("Max Distance (m):", "max_distance", 50000),
|
||||
("Map Size (m):", "map_size", 50000),
|
||||
("Max Distance (m):", "max_distance", 1536),
|
||||
("Map Size (m):", "map_size", 1536),
|
||||
]
|
||||
|
||||
self.settings_vars = {}
|
||||
|
||||
@@ -64,7 +64,7 @@ class RadarTarget:
|
||||
|
||||
@dataclass
|
||||
class RadarSettings:
|
||||
system_frequency: float = 10e9
|
||||
system_frequency: float = 10.5e9
|
||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||
chirps_per_position: int = 32
|
||||
@@ -72,8 +72,8 @@ class RadarSettings:
|
||||
freq_max: float = 30e6
|
||||
prf1: float = 1000
|
||||
prf2: float = 2000
|
||||
max_distance: float = 50000
|
||||
map_size: float = 50000 # Map size in meters
|
||||
max_distance: float = 1536
|
||||
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||
|
||||
@dataclass
|
||||
class GPSData:
|
||||
@@ -1653,8 +1653,8 @@ class RadarGUI:
|
||||
('Frequency Max (Hz):', 'freq_max', 30e6),
|
||||
('PRF1 (Hz):', 'prf1', 1000),
|
||||
('PRF2 (Hz):', 'prf2', 2000),
|
||||
('Max Distance (m):', 'max_distance', 50000),
|
||||
('Map Size (m):', 'map_size', 50000),
|
||||
('Max Distance (m):', 'max_distance', 1536),
|
||||
('Map Size (m):', 'map_size', 1536),
|
||||
('Google Maps API Key:', 'google_maps_api_key', 'YOUR_GOOGLE_MAPS_API_KEY')
|
||||
]
|
||||
|
||||
|
||||
@@ -98,9 +98,10 @@ class DemoTarget:
|
||||
|
||||
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
||||
|
||||
# Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max
|
||||
_RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m
|
||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 m
|
||||
# Physical range grid: matched-filter receiver, 100 MSPS post-DDC, 16:1 decimation
|
||||
# range_per_bin = c / (2 * 100e6) * 16 = 24.0 m
|
||||
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # 24.0 m
|
||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # 1536 m
|
||||
|
||||
def __init__(self, tid: int):
|
||||
self.id = tid
|
||||
@@ -187,10 +188,10 @@ class DemoSimulator:
|
||||
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
||||
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
||||
|
||||
# Range/Doppler scaling (approximate)
|
||||
range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin
|
||||
# Range/Doppler scaling -- matched-filter receiver, 100 MSPS, 16:1 decimation
|
||||
range_per_bin = (3e8 / (2 * 100e6)) * 16 # 24.0 m/bin
|
||||
max_range = range_per_bin * NUM_RANGE_BINS
|
||||
vel_per_bin = 1.484 # m/s per Doppler bin (from WaveformConfig)
|
||||
vel_per_bin = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
|
||||
|
||||
for t in targets:
|
||||
if t.range_m > max_range or t.range_m < 0:
|
||||
@@ -385,7 +386,9 @@ class RadarDashboard:
|
||||
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
||||
|
||||
# Radar parameters used for range-axis scaling.
|
||||
BANDWIDTH = 500e6 # Hz — chirp bandwidth
|
||||
# Matched-filter receiver: range_per_bin = c / (2 * fs_processing) * decimation
|
||||
# = 3e8 / (2 * 100e6) * 16 = 24.0 m/bin
|
||||
BANDWIDTH = 20e6 # Hz — chirp bandwidth (for display/info only)
|
||||
C = 3e8 # m/s — speed of light
|
||||
|
||||
def __init__(self, root: tk.Tk, connection: FT2232HConnection,
|
||||
@@ -514,10 +517,9 @@ class RadarDashboard:
|
||||
self._build_log_tab(tab_log)
|
||||
|
||||
def _build_display_tab(self, parent):
|
||||
# Compute physical axis limits
|
||||
range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin
|
||||
# After decimation 1024→64, each range bin = 16 FFT bins
|
||||
range_per_bin = range_res * 16
|
||||
# Compute physical axis limits -- matched-filter receiver
|
||||
# Range per bin: c / (2 * fs_processing) * decimation_factor = 24.0 m
|
||||
range_per_bin = self.C / (2.0 * 100e6) * 16 # 24.0 m
|
||||
max_range = range_per_bin * NUM_RANGE_BINS
|
||||
|
||||
doppler_bin_lo = 0
|
||||
|
||||
@@ -45,7 +45,7 @@ class RadarSettings:
|
||||
range_bins: int = 1024
|
||||
doppler_bins: int = 32
|
||||
prf: float = 1000
|
||||
max_range: float = 5000
|
||||
max_range: float = 1536
|
||||
max_velocity: float = 100
|
||||
cfar_threshold: float = 13.0
|
||||
|
||||
@@ -577,7 +577,7 @@ class RadarDemoGUI:
|
||||
('Range Bins:', 'range_bins', 1024, 256, 2048),
|
||||
('Doppler Bins:', 'doppler_bins', 32, 8, 128),
|
||||
('PRF (Hz):', 'prf', 1000, 100, 10000),
|
||||
('Max Range (m):', 'max_range', 5000, 100, 50000),
|
||||
('Max Range (m):', 'max_range', 1536, 100, 25000),
|
||||
('Max Velocity (m/s):', 'max_vel', 100, 10, 500),
|
||||
('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0)
|
||||
]
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
# ruff: noqa: T201
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
One-off AGC saturation analysis for ADI CN0566 raw IQ captures.
|
||||
|
||||
Bit-accurate simulation of rx_gain_control.v AGC inner loop applied
|
||||
to real captured IQ data. Three scenarios per dataset:
|
||||
|
||||
Row 1 — AGC OFF: Fixed gain_shift=0 (pass-through). Shows raw clipping.
|
||||
Row 2 — AGC ON: Auto-adjusts from gain_shift=0. Clipping clears.
|
||||
Row 3 — AGC delayed: OFF for first half, ON at midpoint.
|
||||
Shows the transition: clipping → AGC activates → clears.
|
||||
|
||||
Key RTL details modelled exactly:
|
||||
- gain_shift[3]=direction (0=amplify/left, 1=attenuate/right), [2:0]=amount
|
||||
- Internal agc_gain is signed -7..+7
|
||||
- Peak is measured PRE-gain (raw input |sample|, upper 8 of 15 bits)
|
||||
- Saturation is measured POST-gain (overflow from shift)
|
||||
- Attack: gain -= agc_attack when any sample clips (immediate)
|
||||
- Decay: gain += agc_decay when peak < target AND holdoff expired
|
||||
- Hold: when peak >= target AND no saturation, hold gain, reset holdoff
|
||||
|
||||
Usage:
|
||||
python adi_agc_analysis.py
|
||||
python adi_agc_analysis.py --data /path/to/file.npy --label "my capture"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from v7.agc_sim import (
|
||||
encoding_to_signed,
|
||||
apply_gain_shift,
|
||||
quantize_iq,
|
||||
AGCConfig,
|
||||
AGCState,
|
||||
process_agc_frame,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FPGA AGC parameters (rx_gain_control.v reset defaults)
|
||||
# ---------------------------------------------------------------------------
|
||||
AGC_TARGET = 200 # host_agc_target (8-bit, default 200)
|
||||
ADC_RAIL = 4095 # 12-bit ADC max absolute value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-frame AGC simulation using v7.agc_sim (bit-accurate to RTL)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def simulate_agc(frames: np.ndarray, agc_enabled: bool = True,
|
||||
enable_at_frame: int = 0,
|
||||
initial_gain_enc: int = 0x00) -> dict:
|
||||
"""Simulate FPGA inner-loop AGC across all frames.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frames : (N, chirps, samples) complex — raw ADC captures (12-bit range)
|
||||
agc_enabled : if False, gain stays fixed
|
||||
enable_at_frame : frame index where AGC activates
|
||||
initial_gain_enc : gain_shift[3:0] encoding when AGC enables (default 0x00 = pass-through)
|
||||
"""
|
||||
n_frames = frames.shape[0]
|
||||
|
||||
# Output arrays
|
||||
out_gain_enc = np.zeros(n_frames, dtype=int)
|
||||
out_gain_signed = np.zeros(n_frames, dtype=int)
|
||||
out_peak_mag = np.zeros(n_frames, dtype=int)
|
||||
out_sat_count = np.zeros(n_frames, dtype=int)
|
||||
out_sat_rate = np.zeros(n_frames, dtype=float)
|
||||
out_rms_post = np.zeros(n_frames, dtype=float)
|
||||
|
||||
# AGC state — managed by process_agc_frame()
|
||||
state = AGCState(
|
||||
gain=encoding_to_signed(initial_gain_enc),
|
||||
holdoff_counter=0,
|
||||
was_enabled=False,
|
||||
)
|
||||
|
||||
for i in range(n_frames):
|
||||
frame_i, frame_q = quantize_iq(frames[i])
|
||||
|
||||
agc_active = agc_enabled and (i >= enable_at_frame)
|
||||
|
||||
# Build per-frame config (enable toggles at enable_at_frame)
|
||||
config = AGCConfig(enabled=agc_active)
|
||||
|
||||
result = process_agc_frame(frame_i, frame_q, config, state)
|
||||
|
||||
# RMS of shifted signal
|
||||
rms = float(np.sqrt(np.mean(
|
||||
result.shifted_i.astype(np.float64)**2
|
||||
+ result.shifted_q.astype(np.float64)**2)))
|
||||
|
||||
total_samples = frame_i.size + frame_q.size
|
||||
sat_rate = result.overflow_raw / total_samples if total_samples > 0 else 0.0
|
||||
|
||||
# Record outputs
|
||||
out_gain_enc[i] = result.gain_enc
|
||||
out_gain_signed[i] = result.gain_signed
|
||||
out_peak_mag[i] = result.peak_mag_8bit
|
||||
out_sat_count[i] = result.saturation_count
|
||||
out_sat_rate[i] = sat_rate
|
||||
out_rms_post[i] = rms
|
||||
|
||||
return {
|
||||
"gain_enc": out_gain_enc,
|
||||
"gain_signed": out_gain_signed,
|
||||
"peak_mag": out_peak_mag,
|
||||
"sat_count": out_sat_count,
|
||||
"sat_rate": out_sat_rate,
|
||||
"rms_post": out_rms_post,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Range-Doppler processing for heatmap display
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_frame_rd(frame: np.ndarray, gain_enc: int,
|
||||
n_range: int = 64,
|
||||
n_doppler: int = 32) -> np.ndarray:
|
||||
"""Range-Doppler magnitude for one frame with gain applied."""
|
||||
frame_i, frame_q = quantize_iq(frame)
|
||||
si, sq, _ = apply_gain_shift(frame_i, frame_q, gain_enc)
|
||||
|
||||
iq = si.astype(np.float64) + 1j * sq.astype(np.float64)
|
||||
n_chirps, _ = iq.shape
|
||||
|
||||
range_fft = np.fft.fft(iq, axis=1)[:, :n_range]
|
||||
doppler_fft = np.fft.fftshift(np.fft.fft(range_fft, axis=0), axes=0)
|
||||
center = n_chirps // 2
|
||||
half_d = n_doppler // 2
|
||||
doppler_fft = doppler_fft[center - half_d:center + half_d, :]
|
||||
|
||||
rd_mag = np.abs(doppler_fft.real) + np.abs(doppler_fft.imag)
|
||||
return rd_mag.T # (n_range, n_doppler)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plotting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def plot_scenario(axes, data: np.ndarray, agc: dict, title: str,
|
||||
enable_frame: int = 0):
|
||||
"""Plot one AGC scenario across 5 axes."""
|
||||
n = data.shape[0]
|
||||
xs = np.arange(n)
|
||||
|
||||
# Range-Doppler heatmap
|
||||
if enable_frame > 0 and enable_frame < n:
|
||||
f_before = max(0, enable_frame - 1)
|
||||
f_after = min(n - 1, n - 2)
|
||||
rd_before = process_frame_rd(data[f_before], int(agc["gain_enc"][f_before]))
|
||||
rd_after = process_frame_rd(data[f_after], int(agc["gain_enc"][f_after]))
|
||||
combined = np.hstack([rd_before, rd_after])
|
||||
im = axes[0].imshow(
|
||||
20 * np.log10(combined + 1), aspect="auto", origin="lower",
|
||||
cmap="inferno", interpolation="nearest")
|
||||
axes[0].axvline(x=rd_before.shape[1] - 0.5, color="cyan",
|
||||
linewidth=2, linestyle="--")
|
||||
axes[0].set_title(f"{title}\nL: f{f_before} (pre) | R: f{f_after} (post)")
|
||||
else:
|
||||
worst = int(np.argmax(agc["sat_count"]))
|
||||
best = int(np.argmin(agc["sat_count"]))
|
||||
f_show = worst if agc["sat_count"][worst] > 0 else best
|
||||
rd = process_frame_rd(data[f_show], int(agc["gain_enc"][f_show]))
|
||||
im = axes[0].imshow(
|
||||
20 * np.log10(rd + 1), aspect="auto", origin="lower",
|
||||
cmap="inferno", interpolation="nearest")
|
||||
axes[0].set_title(f"{title}\nFrame {f_show}")
|
||||
|
||||
axes[0].set_xlabel("Doppler bin")
|
||||
axes[0].set_ylabel("Range bin")
|
||||
plt.colorbar(im, ax=axes[0], label="dB", shrink=0.8)
|
||||
|
||||
# Signed gain history (the real AGC state)
|
||||
axes[1].plot(xs, agc["gain_signed"], color="#00ff88", linewidth=1.5)
|
||||
axes[1].axhline(y=0, color="gray", linestyle=":", alpha=0.5,
|
||||
label="Pass-through")
|
||||
if enable_frame > 0:
|
||||
axes[1].axvline(x=enable_frame, color="yellow", linewidth=2,
|
||||
linestyle="--", label="AGC ON")
|
||||
axes[1].set_ylim(-8, 8)
|
||||
axes[1].set_ylabel("Gain (signed)")
|
||||
axes[1].set_title("AGC Internal Gain (-7=max atten, +7=max amp)")
|
||||
axes[1].legend(fontsize=7, loc="upper right")
|
||||
axes[1].grid(True, alpha=0.3)
|
||||
|
||||
# Peak magnitude (PRE-gain, 8-bit)
|
||||
axes[2].plot(xs, agc["peak_mag"], color="#ffaa00", linewidth=1.0)
|
||||
axes[2].axhline(y=AGC_TARGET, color="cyan", linestyle="--",
|
||||
alpha=0.7, label=f"Target ({AGC_TARGET})")
|
||||
axes[2].axhspan(240, 255, color="red", alpha=0.15, label="Clip zone")
|
||||
if enable_frame > 0:
|
||||
axes[2].axvline(x=enable_frame, color="yellow", linewidth=2,
|
||||
linestyle="--", alpha=0.8)
|
||||
axes[2].set_ylim(0, 260)
|
||||
axes[2].set_ylabel("Peak (8-bit)")
|
||||
axes[2].set_title("Peak Magnitude (pre-gain, raw input)")
|
||||
axes[2].legend(fontsize=7, loc="upper right")
|
||||
axes[2].grid(True, alpha=0.3)
|
||||
|
||||
# Saturation count (POST-gain overflow)
|
||||
axes[3].fill_between(xs, agc["sat_count"], color="red", alpha=0.4)
|
||||
axes[3].plot(xs, agc["sat_count"], color="red", linewidth=0.8)
|
||||
if enable_frame > 0:
|
||||
axes[3].axvline(x=enable_frame, color="yellow", linewidth=2,
|
||||
linestyle="--", alpha=0.8)
|
||||
axes[3].set_ylabel("Overflow Count")
|
||||
total = int(agc["sat_count"].sum())
|
||||
axes[3].set_title(f"Post-Gain Overflow (total={total})")
|
||||
axes[3].grid(True, alpha=0.3)
|
||||
|
||||
# RMS signal level (post-gain)
|
||||
axes[4].plot(xs, agc["rms_post"], color="#44aaff", linewidth=1.0)
|
||||
if enable_frame > 0:
|
||||
axes[4].axvline(x=enable_frame, color="yellow", linewidth=2,
|
||||
linestyle="--", alpha=0.8)
|
||||
axes[4].set_ylabel("RMS")
|
||||
axes[4].set_xlabel("Frame")
|
||||
axes[4].set_title("Post-Gain RMS Level")
|
||||
axes[4].grid(True, alpha=0.3)
|
||||
|
||||
|
||||
def analyze_dataset(data: np.ndarray, label: str):
|
||||
"""Run 3-scenario analysis for one dataset."""
|
||||
n_frames = data.shape[0]
|
||||
mid = n_frames // 2
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {label} — shape {data.shape}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Raw ADC stats
|
||||
raw_sat = np.sum((np.abs(data.real) >= ADC_RAIL) |
|
||||
(np.abs(data.imag) >= ADC_RAIL))
|
||||
print(f" Raw ADC saturation: {raw_sat} samples "
|
||||
f"({100*raw_sat/(2*data.size):.2f}%)")
|
||||
|
||||
# Scenario 1: AGC OFF — pass-through (gain_shift=0x00)
|
||||
print(" [1/3] AGC OFF (gain=0, pass-through) ...")
|
||||
agc_off = simulate_agc(data, agc_enabled=False, initial_gain_enc=0x00)
|
||||
print(f" Post-gain overflow: {agc_off['sat_count'].sum()} "
|
||||
f"(should be 0 — no amplification)")
|
||||
|
||||
# Scenario 2: AGC ON from frame 0
|
||||
print(" [2/3] AGC ON (from start) ...")
|
||||
agc_on = simulate_agc(data, agc_enabled=True, enable_at_frame=0,
|
||||
initial_gain_enc=0x00)
|
||||
print(f" Final gain: {agc_on['gain_signed'][-1]} "
|
||||
f"(enc=0x{agc_on['gain_enc'][-1]:X})")
|
||||
print(f" Post-gain overflow: {agc_on['sat_count'].sum()}")
|
||||
|
||||
# Scenario 3: AGC delayed
|
||||
print(f" [3/3] AGC delayed (ON at frame {mid}) ...")
|
||||
agc_delayed = simulate_agc(data, agc_enabled=True,
|
||||
enable_at_frame=mid,
|
||||
initial_gain_enc=0x00)
|
||||
pre_sat = int(agc_delayed["sat_count"][:mid].sum())
|
||||
post_sat = int(agc_delayed["sat_count"][mid:].sum())
|
||||
print(f" Pre-AGC overflow: {pre_sat} "
|
||||
f"Post-AGC overflow: {post_sat}")
|
||||
|
||||
# Plot
|
||||
fig, axes = plt.subplots(3, 5, figsize=(28, 14))
|
||||
fig.suptitle(f"AERIS-10 AGC Analysis — {label}\n"
|
||||
f"({n_frames} frames, {data.shape[1]} chirps, "
|
||||
f"{data.shape[2]} samples/chirp, "
|
||||
f"raw ADC sat={100*raw_sat/(2*data.size):.2f}%)",
|
||||
fontsize=13, fontweight="bold", y=0.99)
|
||||
|
||||
plot_scenario(axes[0], data, agc_off, "AGC OFF (pass-through)")
|
||||
plot_scenario(axes[1], data, agc_on, "AGC ON (from start)")
|
||||
plot_scenario(axes[2], data, agc_delayed,
|
||||
f"AGC delayed (ON at frame {mid})", enable_frame=mid)
|
||||
|
||||
for ax, lbl in zip(axes[:, 0],
|
||||
["AGC OFF", "AGC ON", "AGC DELAYED"],
|
||||
strict=True):
|
||||
ax.annotate(lbl, xy=(-0.35, 0.5), xycoords="axes fraction",
|
||||
fontsize=13, fontweight="bold", color="white",
|
||||
ha="center", va="center", rotation=90)
|
||||
|
||||
plt.tight_layout(rect=[0.03, 0, 1, 0.95])
|
||||
return fig
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AGC analysis for ADI raw IQ captures "
|
||||
"(bit-accurate rx_gain_control.v simulation)")
|
||||
parser.add_argument("--amp", type=str,
|
||||
default=str(Path.home() / "Downloads/adi_radar_data"
|
||||
"/amp_radar"
|
||||
"/phaser_amp_4MSPS_500M_300u_256_m3dB.npy"),
|
||||
help="Path to amplified radar .npy")
|
||||
parser.add_argument("--noamp", type=str,
|
||||
default=str(Path.home() / "Downloads/adi_radar_data"
|
||||
"/no_amp_radar"
|
||||
"/phaser_NOamp_4MSPS_500M_300u_256.npy"),
|
||||
help="Path to non-amplified radar .npy")
|
||||
parser.add_argument("--data", type=str, default=None,
|
||||
help="Single dataset mode")
|
||||
parser.add_argument("--label", type=str, default="Custom Data")
|
||||
args = parser.parse_args()
|
||||
|
||||
plt.style.use("dark_background")
|
||||
|
||||
if args.data:
|
||||
data = np.load(args.data)
|
||||
analyze_dataset(data, args.label)
|
||||
plt.show()
|
||||
return
|
||||
|
||||
figs = []
|
||||
for path, label in [(args.amp, "With Amplifier (-3 dB)"),
|
||||
(args.noamp, "No Amplifier")]:
|
||||
if not Path(path).exists():
|
||||
print(f"WARNING: {path} not found, skipping")
|
||||
continue
|
||||
data = np.load(path)
|
||||
fig = analyze_dataset(data, label)
|
||||
figs.append(fig)
|
||||
|
||||
if not figs:
|
||||
print("No data found. Use --amp/--noamp or --data.")
|
||||
sys.exit(1)
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -58,16 +58,16 @@ class TestRadarSettings(unittest.TestCase):
|
||||
|
||||
def test_has_physical_conversion_fields(self):
|
||||
s = _models().RadarSettings()
|
||||
self.assertIsInstance(s.range_resolution, float)
|
||||
self.assertIsInstance(s.range_bin_spacing, float)
|
||||
self.assertIsInstance(s.velocity_resolution, float)
|
||||
self.assertGreater(s.range_resolution, 0)
|
||||
self.assertGreater(s.range_bin_spacing, 0)
|
||||
self.assertGreater(s.velocity_resolution, 0)
|
||||
|
||||
def test_defaults(self):
|
||||
s = _models().RadarSettings()
|
||||
self.assertEqual(s.system_frequency, 10e9)
|
||||
self.assertEqual(s.coverage_radius, 50000)
|
||||
self.assertEqual(s.max_distance, 50000)
|
||||
self.assertEqual(s.system_frequency, 10.5e9)
|
||||
self.assertEqual(s.coverage_radius, 1536)
|
||||
self.assertEqual(s.max_distance, 1536)
|
||||
|
||||
|
||||
class TestGPSData(unittest.TestCase):
|
||||
@@ -425,32 +425,42 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
def test_defaults(self):
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertEqual(wc.sample_rate_hz, 4e6)
|
||||
self.assertEqual(wc.bandwidth_hz, 500e6)
|
||||
self.assertEqual(wc.chirp_duration_s, 300e-6)
|
||||
self.assertEqual(wc.center_freq_hz, 10.525e9)
|
||||
self.assertEqual(wc.sample_rate_hz, 100e6)
|
||||
self.assertEqual(wc.bandwidth_hz, 20e6)
|
||||
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||
self.assertEqual(wc.pri_s, 167e-6)
|
||||
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
||||
self.assertEqual(wc.n_range_bins, 64)
|
||||
self.assertEqual(wc.n_doppler_bins, 32)
|
||||
self.assertEqual(wc.fft_size, 1024)
|
||||
self.assertEqual(wc.decimation_factor, 16)
|
||||
|
||||
def test_range_resolution(self):
|
||||
"""range_resolution_m should be ~5.62 m/bin with ADI defaults."""
|
||||
"""bin_spacing_m should be ~24.0 m/bin with PLFM defaults."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 5.621, places=1)
|
||||
self.assertAlmostEqual(wc.bin_spacing_m, 23.98, places=1)
|
||||
|
||||
def test_range_resolution_physical(self):
|
||||
"""range_resolution_m = c/(2*BW), ~7.5 m at 20 MHz BW."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 7.49, places=1)
|
||||
# 30 MHz BW → 5.0 m resolution
|
||||
wc30 = WaveformConfig(bandwidth_hz=30e6)
|
||||
self.assertAlmostEqual(wc30.range_resolution_m, 4.996, places=1)
|
||||
|
||||
def test_velocity_resolution(self):
|
||||
"""velocity_resolution_mps should be ~1.484 m/s/bin."""
|
||||
"""velocity_resolution_mps should be ~2.67 m/s/bin."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 1.484, places=2)
|
||||
self.assertAlmostEqual(wc.velocity_resolution_mps, 2.67, places=1)
|
||||
|
||||
def test_max_range(self):
|
||||
"""max_range_m = range_resolution * n_range_bins."""
|
||||
"""max_range_m = bin_spacing * n_range_bins."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 64, places=1)
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.bin_spacing_m * 64, places=1)
|
||||
|
||||
def test_max_velocity(self):
|
||||
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
||||
@@ -466,8 +476,9 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
"""Non-default parameters correctly change derived values."""
|
||||
from v7.models import WaveformConfig
|
||||
wc1 = WaveformConfig()
|
||||
wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res
|
||||
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
||||
# Matched-filter: bin_spacing = c/(2*fs)*dec — proportional to 1/fs
|
||||
wc2 = WaveformConfig(sample_rate_hz=200e6) # double fs → halve bin spacing
|
||||
self.assertAlmostEqual(wc2.bin_spacing_m, wc1.bin_spacing_m / 2, places=2)
|
||||
|
||||
def test_zero_center_freq_velocity(self):
|
||||
"""Zero center freq should cause ZeroDivisionError in velocity calc."""
|
||||
@@ -925,18 +936,18 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
||||
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||
from v7.processing import extract_targets_from_frame
|
||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
||||
targets = extract_targets_from_frame(frame, range_resolution=5.621)
|
||||
targets = extract_targets_from_frame(frame, bin_spacing=23.98)
|
||||
self.assertEqual(len(targets), 1)
|
||||
self.assertAlmostEqual(targets[0].range, 10 * 5.621, places=2)
|
||||
self.assertAlmostEqual(targets[0].range, 10 * 23.98, places=1)
|
||||
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||
|
||||
def test_velocity_sign(self):
|
||||
"""Doppler bin < center → negative velocity, > center → positive."""
|
||||
from v7.processing import extract_targets_from_frame
|
||||
frame = self._make_frame(det_cells=[(5, 10), (5, 20)])
|
||||
targets = extract_targets_from_frame(frame, velocity_resolution=1.484)
|
||||
# dbin=10: vel = (10-16)*1.484 = -8.904 (approaching)
|
||||
# dbin=20: vel = (20-16)*1.484 = +5.936 (receding)
|
||||
targets = extract_targets_from_frame(frame, velocity_resolution=2.67)
|
||||
# dbin=10: vel = (10-16)*2.67 = -16.02 (approaching)
|
||||
# dbin=20: vel = (20-16)*2.67 = +10.68 (receding)
|
||||
self.assertLess(targets[0].velocity, 0)
|
||||
self.assertGreater(targets[1].velocity, 0)
|
||||
|
||||
@@ -954,7 +965,7 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
||||
pitch=0.0, heading=90.0)
|
||||
frame = self._make_frame(det_cells=[(10, 16)])
|
||||
targets = extract_targets_from_frame(
|
||||
frame, range_resolution=100.0, gps=gps)
|
||||
frame, bin_spacing=100.0, gps=gps)
|
||||
# Should be roughly east of radar position
|
||||
self.assertAlmostEqual(targets[0].latitude, 41.9, places=2)
|
||||
self.assertGreater(targets[0].longitude, 12.5)
|
||||
|
||||
@@ -98,7 +98,7 @@ class RadarMapWidget(QWidget):
|
||||
)
|
||||
self._targets: list[RadarTarget] = []
|
||||
self._pending_targets: list[RadarTarget] | None = None
|
||||
self._coverage_radius = 50_000 # metres
|
||||
self._coverage_radius = 1_536 # metres (64 bins x 24 m, 3 km mode)
|
||||
self._tile_server = TileServer.OPENSTREETMAP
|
||||
self._show_coverage = True
|
||||
self._show_trails = False
|
||||
|
||||
@@ -105,15 +105,15 @@ class RadarSettings:
|
||||
tab and Opcode enum in radar_protocol.py. This dataclass holds only
|
||||
host-side display/map settings and physical-unit conversion factors.
|
||||
|
||||
range_resolution and velocity_resolution should be calibrated to
|
||||
range_bin_spacing and velocity_resolution should be calibrated to
|
||||
the actual waveform parameters.
|
||||
"""
|
||||
system_frequency: float = 10e9 # Hz (carrier, used for velocity calc)
|
||||
range_resolution: float = 781.25 # Meters per range bin (default: 50km/64)
|
||||
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
||||
max_distance: float = 50000 # Max detection range (m)
|
||||
map_size: float = 50000 # Map display size (m)
|
||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
||||
system_frequency: float = 10.5e9 # Hz (PLFM TX LO, verified from ADF4382 config)
|
||||
range_bin_spacing: float = 24.0 # Meters per decimated range bin (c/(2*100MSPS)*16)
|
||||
velocity_resolution: float = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
|
||||
max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m (3 km mode)
|
||||
map_size: float = 1536 # Map display size (m)
|
||||
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -196,47 +196,61 @@ class TileServer(Enum):
|
||||
class WaveformConfig:
|
||||
"""Physical waveform parameters for converting bins to SI units.
|
||||
|
||||
Encapsulates the radar waveform so that range/velocity resolution
|
||||
Encapsulates the PLFM radar waveform so that range/velocity resolution
|
||||
can be derived automatically instead of hardcoded in RadarSettings.
|
||||
|
||||
Defaults match the ADI CN0566 Phaser capture parameters used in
|
||||
the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp).
|
||||
Defaults match the PLFM hardware: 100 MSPS post-DDC processing rate,
|
||||
20 MHz chirp bandwidth, 30 us long chirp, 167 us PRI, 10.5 GHz carrier.
|
||||
The receiver uses matched-filter pulse compression (NOT deramped FMCW),
|
||||
so range-per-bin = c / (2 * fs_processing) * decimation_factor.
|
||||
"""
|
||||
|
||||
sample_rate_hz: float = 4e6 # ADC sample rate
|
||||
bandwidth_hz: float = 500e6 # Chirp bandwidth
|
||||
chirp_duration_s: float = 300e-6 # Chirp ramp time
|
||||
center_freq_hz: float = 10.525e9 # Carrier frequency
|
||||
n_range_bins: int = 64 # After decimation
|
||||
sample_rate_hz: float = 100e6 # Post-DDC processing rate (400 MSPS / 4)
|
||||
bandwidth_hz: float = 20e6 # Chirp bandwidth (Phase 1 target: 30 MHz)
|
||||
chirp_duration_s: float = 30e-6 # Long chirp ramp (informational only)
|
||||
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
|
||||
center_freq_hz: float = 10.5e9 # TX LO carrier (verified: ADF4382 config)
|
||||
n_range_bins: int = 64 # After decimation (3 km mode)
|
||||
n_doppler_bins: int = 32 # After Doppler FFT
|
||||
fft_size: int = 1024 # Pre-decimation FFT length
|
||||
decimation_factor: int = 16 # 1024 → 64
|
||||
|
||||
@property
|
||||
def range_resolution_m(self) -> float:
|
||||
"""Meters per decimated range bin (FMCW deramped baseband).
|
||||
def bin_spacing_m(self) -> float:
|
||||
"""Meters per decimated range bin (matched-filter receiver).
|
||||
|
||||
For deramped FMCW: bin spacing = c * Fs * T / (2 * N_FFT * BW).
|
||||
For matched-filter pulse compression: bin spacing = c / (2 * fs).
|
||||
After decimation the bin spacing grows by *decimation_factor*.
|
||||
This is independent of chirp bandwidth (BW affects physical
|
||||
resolution, not bin spacing).
|
||||
"""
|
||||
c = 299_792_458.0
|
||||
raw_bin = (
|
||||
c * self.sample_rate_hz * self.chirp_duration_s
|
||||
/ (2.0 * self.fft_size * self.bandwidth_hz)
|
||||
)
|
||||
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||
return raw_bin * self.decimation_factor
|
||||
|
||||
@property
|
||||
def range_resolution_m(self) -> float:
|
||||
"""Physical range resolution in meters, set by chirp bandwidth.
|
||||
|
||||
range_resolution = c / (2 * BW).
|
||||
At 20 MHz BW → 7.5 m; at 30 MHz BW → 5.0 m.
|
||||
This is distinct from bin_spacing_m (which depends on sample rate
|
||||
and decimation factor, not bandwidth).
|
||||
"""
|
||||
c = 299_792_458.0
|
||||
return c / (2.0 * self.bandwidth_hz)
|
||||
|
||||
@property
|
||||
def velocity_resolution_mps(self) -> float:
|
||||
"""m/s per Doppler bin. lambda / (2 * n_doppler * chirp_duration)."""
|
||||
"""m/s per Doppler bin. lambda / (2 * n_doppler * PRI)."""
|
||||
c = 299_792_458.0
|
||||
wavelength = c / self.center_freq_hz
|
||||
return wavelength / (2.0 * self.n_doppler_bins * self.chirp_duration_s)
|
||||
return wavelength / (2.0 * self.n_doppler_bins * self.pri_s)
|
||||
|
||||
@property
|
||||
def max_range_m(self) -> float:
|
||||
"""Maximum unambiguous range in meters."""
|
||||
return self.range_resolution_m * self.n_range_bins
|
||||
return self.bin_spacing_m * self.n_range_bins
|
||||
|
||||
@property
|
||||
def max_velocity_mps(self) -> float:
|
||||
|
||||
@@ -490,7 +490,7 @@ def polar_to_geographic(
|
||||
|
||||
def extract_targets_from_frame(
|
||||
frame,
|
||||
range_resolution: float = 1.0,
|
||||
bin_spacing: float = 1.0,
|
||||
velocity_resolution: float = 1.0,
|
||||
gps: GPSData | None = None,
|
||||
) -> list[RadarTarget]:
|
||||
@@ -503,8 +503,8 @@ def extract_targets_from_frame(
|
||||
----------
|
||||
frame : RadarFrame
|
||||
Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``.
|
||||
range_resolution : float
|
||||
Meters per range bin.
|
||||
bin_spacing : float
|
||||
Meters per range bin (bin spacing, NOT bandwidth-limited resolution).
|
||||
velocity_resolution : float
|
||||
m/s per Doppler bin.
|
||||
gps : GPSData | None
|
||||
@@ -525,7 +525,7 @@ def extract_targets_from_frame(
|
||||
mag = float(frame.magnitude[rbin, dbin])
|
||||
snr = 10.0 * math.log10(max(mag, 1.0)) if mag > 0 else 0.0
|
||||
|
||||
range_m = float(rbin) * range_resolution
|
||||
range_m = float(rbin) * bin_spacing
|
||||
velocity_ms = float(dbin - doppler_center) * velocity_resolution
|
||||
|
||||
lat, lon, azimuth, elevation = 0.0, 0.0, 0.0, 0.0
|
||||
|
||||
@@ -169,7 +169,7 @@ class RadarDataWorker(QThread):
|
||||
The FPGA already does: FFT, MTI, CFAR, DC notch.
|
||||
Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
|
||||
|
||||
Bin-to-physical conversion uses RadarSettings.range_resolution
|
||||
Bin-to-physical conversion uses RadarSettings.range_bin_spacing
|
||||
and velocity_resolution (should be calibrated to actual waveform).
|
||||
"""
|
||||
targets: list[RadarTarget] = []
|
||||
@@ -180,7 +180,7 @@ class RadarDataWorker(QThread):
|
||||
|
||||
# Extract detections from FPGA CFAR flags
|
||||
det_indices = np.argwhere(frame.detections > 0)
|
||||
r_res = self._settings.range_resolution
|
||||
r_res = self._settings.range_bin_spacing
|
||||
v_res = self._settings.velocity_resolution
|
||||
|
||||
for idx in det_indices:
|
||||
@@ -368,7 +368,7 @@ class TargetSimulator(QObject):
|
||||
|
||||
for t in self._targets:
|
||||
new_range = t.range - t.velocity * 0.5
|
||||
if new_range < 500 or new_range > 50000:
|
||||
if new_range < 50 or new_range > 1536:
|
||||
continue # target exits coverage — drop it
|
||||
|
||||
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
||||
@@ -559,7 +559,7 @@ class ReplayWorker(QThread):
|
||||
# Target extraction
|
||||
targets = self._extract_targets(
|
||||
frame,
|
||||
range_resolution=self._waveform.range_resolution_m,
|
||||
bin_spacing=self._waveform.bin_spacing_m,
|
||||
velocity_resolution=self._waveform.velocity_resolution_mps,
|
||||
gps=self._gps,
|
||||
)
|
||||
|
||||
@@ -793,3 +793,51 @@ def parse_stm32_gpio_init(filepath: Path | None = None) -> list[GpioPin]:
|
||||
))
|
||||
|
||||
return pins
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# FPGA radar_params.vh parser
|
||||
# ===================================================================
|
||||
|
||||
def parse_radar_params_vh() -> dict[str, int]:
|
||||
"""
|
||||
Parse `define values from radar_params.vh.
|
||||
|
||||
Returns dict like {"RP_FFT_SIZE": 1024, "RP_DECIMATION_FACTOR": 16, ...}.
|
||||
Only parses defines with simple integer or Verilog literal values.
|
||||
Skips bit-width prefixed literals (e.g. 2'b00) — returns the numeric value.
|
||||
"""
|
||||
path = FPGA_DIR / "radar_params.vh"
|
||||
text = path.read_text()
|
||||
params: dict[str, int] = {}
|
||||
|
||||
for m in re.finditer(
|
||||
r'`define\s+(RP_\w+)\s+(\S+)', text
|
||||
):
|
||||
name = m.group(1)
|
||||
val_str = m.group(2).rstrip()
|
||||
|
||||
# Skip non-numeric defines (like RADAR_PARAMS_VH guard)
|
||||
if name == "RADAR_PARAMS_VH":
|
||||
continue
|
||||
|
||||
# Handle Verilog bit-width literals: 2'b00, 8'h30, etc.
|
||||
verilog_lit = re.match(r"\d+'([bhd])(\w+)", val_str)
|
||||
if verilog_lit:
|
||||
base_char = verilog_lit.group(1)
|
||||
digits = verilog_lit.group(2)
|
||||
base = {"b": 2, "h": 16, "d": 10}[base_char]
|
||||
params[name] = int(digits, base)
|
||||
continue
|
||||
|
||||
# Handle parenthesized expressions like (`RP_X * `RP_Y)
|
||||
if "(" in val_str or "`" in val_str:
|
||||
continue # Skip computed defines
|
||||
|
||||
# Plain integer
|
||||
try:
|
||||
params[name] = int(val_str)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return params
|
||||
|
||||
@@ -27,10 +27,12 @@ layers agree (because both could be wrong).
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -202,6 +204,58 @@ class TestTier1OpcodeContract:
|
||||
f"but ground truth says '{expected_reg}'"
|
||||
)
|
||||
|
||||
def test_opcode_count_exact_match(self):
|
||||
"""
|
||||
Total opcode count must match ground truth exactly.
|
||||
|
||||
This is a CANARY test: if an LLM agent or developer adds/removes
|
||||
an opcode in one layer without updating ground truth, this fails
|
||||
immediately. Update GROUND_TRUTH_OPCODES when intentionally
|
||||
changing the register map.
|
||||
"""
|
||||
expected_count = len(GROUND_TRUTH_OPCODES) # 25
|
||||
py_count = len(cp.parse_python_opcodes())
|
||||
v_count = len(cp.parse_verilog_opcodes())
|
||||
|
||||
assert py_count == expected_count, (
|
||||
f"Python has {py_count} opcodes but ground truth has {expected_count}. "
|
||||
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
|
||||
)
|
||||
assert v_count == expected_count, (
|
||||
f"Verilog has {v_count} opcodes but ground truth has {expected_count}. "
|
||||
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
|
||||
)
|
||||
|
||||
def test_no_extra_verilog_opcodes(self):
|
||||
"""
|
||||
Verilog must not have opcodes absent from ground truth.
|
||||
|
||||
Catches the case where someone adds a new case entry in
|
||||
radar_system_top.v but forgets to update the contract.
|
||||
"""
|
||||
v_opcodes = cp.parse_verilog_opcodes()
|
||||
extra = set(v_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
|
||||
assert not extra, (
|
||||
f"Verilog has opcodes not in ground truth: "
|
||||
f"{[f'0x{x:02X} ({v_opcodes[x].register})' for x in extra]}. "
|
||||
f"Add them to GROUND_TRUTH_OPCODES if intentional."
|
||||
)
|
||||
|
||||
def test_no_extra_python_opcodes(self):
|
||||
"""
|
||||
Python must not have opcodes absent from ground truth.
|
||||
|
||||
Catches phantom opcodes (like the 0x06 incident) added by
|
||||
LLM agents that assume without verifying.
|
||||
"""
|
||||
py_opcodes = cp.parse_python_opcodes()
|
||||
extra = set(py_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
|
||||
assert not extra, (
|
||||
f"Python has opcodes not in ground truth: "
|
||||
f"{[f'0x{x:02X} ({py_opcodes[x].name})' for x in extra]}. "
|
||||
f"Remove phantom opcodes or add to GROUND_TRUTH_OPCODES."
|
||||
)
|
||||
|
||||
|
||||
class TestTier1BitWidths:
|
||||
"""Verify register widths and opcode bit slices match ground truth."""
|
||||
@@ -300,6 +354,122 @@ class TestTier1StatusFieldPositions:
|
||||
)
|
||||
|
||||
|
||||
class TestTier1ArchitecturalParams:
|
||||
"""
|
||||
Verify radar_params.vh (FPGA single source of truth) matches Python
|
||||
WaveformConfig defaults and frozen architectural constants.
|
||||
|
||||
These tests catch:
|
||||
- LLM agents changing FFT size, bin counts, or sample rates in one
|
||||
layer without updating the other
|
||||
- Accidental parameter drift between FPGA and GUI
|
||||
- Unauthorized changes to critical architectural constants
|
||||
|
||||
When intentionally changing a parameter (e.g. FFT 1024→2048),
|
||||
update BOTH radar_params.vh AND WaveformConfig, then update the
|
||||
FROZEN_PARAMS dict below.
|
||||
"""
|
||||
|
||||
# Frozen architectural constants — update deliberately when changing arch
|
||||
FROZEN_PARAMS: ClassVar[dict[str, int]] = {
|
||||
"RP_FFT_SIZE": 1024,
|
||||
"RP_DECIMATION_FACTOR": 16,
|
||||
"RP_BINS_PER_SEGMENT": 64,
|
||||
"RP_OUTPUT_RANGE_BINS_3KM": 64,
|
||||
"RP_DOPPLER_FFT_SIZE": 16,
|
||||
"RP_NUM_DOPPLER_BINS": 32,
|
||||
"RP_CHIRPS_PER_FRAME": 32,
|
||||
"RP_CHIRPS_PER_SUBFRAME": 16,
|
||||
"RP_DATA_WIDTH": 16,
|
||||
"RP_PROCESSING_RATE_MHZ": 100,
|
||||
"RP_RANGE_PER_BIN_DM": 240, # 24.0 m in decimeters
|
||||
}
|
||||
|
||||
def test_radar_params_vh_parseable(self):
|
||||
"""radar_params.vh must exist and parse without error."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
assert len(params) > 10, (
|
||||
f"Only parsed {len(params)} defines from radar_params.vh — "
|
||||
f"expected > 10. Parser may be broken."
|
||||
)
|
||||
|
||||
def test_frozen_constants_unchanged(self):
|
||||
"""
|
||||
Critical architectural constants must match frozen values.
|
||||
|
||||
If this test fails, someone changed a fundamental parameter.
|
||||
Verify the change is intentional, update FROZEN_PARAMS, AND
|
||||
update all downstream consumers (GUI, testbenches, docs).
|
||||
"""
|
||||
params = cp.parse_radar_params_vh()
|
||||
for name, expected in self.FROZEN_PARAMS.items():
|
||||
assert name in params, (
|
||||
f"{name} missing from radar_params.vh! "
|
||||
f"Was it renamed or removed?"
|
||||
)
|
||||
assert params[name] == expected, (
|
||||
f"ARCHITECTURAL CHANGE DETECTED: {name} = {params[name]}, "
|
||||
f"expected {expected}. If intentional, update FROZEN_PARAMS "
|
||||
f"in this test AND all downstream consumers."
|
||||
)
|
||||
|
||||
def test_fpga_python_fft_size_match(self):
|
||||
"""FPGA FFT size must match Python WaveformConfig.fft_size."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_FFT_SIZE"] == wc.fft_size, (
|
||||
f"FFT size mismatch: radar_params.vh={params['RP_FFT_SIZE']}, "
|
||||
f"WaveformConfig={wc.fft_size}"
|
||||
)
|
||||
|
||||
def test_fpga_python_decimation_match(self):
|
||||
"""FPGA decimation factor must match Python WaveformConfig."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_DECIMATION_FACTOR"] == wc.decimation_factor, (
|
||||
f"Decimation mismatch: radar_params.vh={params['RP_DECIMATION_FACTOR']}, "
|
||||
f"WaveformConfig={wc.decimation_factor}"
|
||||
)
|
||||
|
||||
def test_fpga_python_range_bins_match(self):
|
||||
"""FPGA 3km output bins must match Python WaveformConfig.n_range_bins."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_OUTPUT_RANGE_BINS_3KM"] == wc.n_range_bins, (
|
||||
f"Range bins mismatch: radar_params.vh={params['RP_OUTPUT_RANGE_BINS_3KM']}, "
|
||||
f"WaveformConfig={wc.n_range_bins}"
|
||||
)
|
||||
|
||||
def test_fpga_python_doppler_bins_match(self):
|
||||
"""FPGA Doppler bins must match Python WaveformConfig.n_doppler_bins."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
assert params["RP_NUM_DOPPLER_BINS"] == wc.n_doppler_bins, (
|
||||
f"Doppler bins mismatch: radar_params.vh={params['RP_NUM_DOPPLER_BINS']}, "
|
||||
f"WaveformConfig={wc.n_doppler_bins}"
|
||||
)
|
||||
|
||||
def test_fpga_python_sample_rate_match(self):
|
||||
"""FPGA processing rate must match Python WaveformConfig.sample_rate_hz."""
|
||||
params = cp.parse_radar_params_vh()
|
||||
sys.path.insert(0, str(cp.GUI_DIR))
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
fpga_rate_hz = params["RP_PROCESSING_RATE_MHZ"] * 1e6
|
||||
assert fpga_rate_hz == wc.sample_rate_hz, (
|
||||
f"Sample rate mismatch: radar_params.vh={fpga_rate_hz/1e6} MHz, "
|
||||
f"WaveformConfig={wc.sample_rate_hz/1e6} MHz"
|
||||
)
|
||||
|
||||
|
||||
class TestTier1PacketConstants:
|
||||
"""Verify packet header/footer/size constants match across layers."""
|
||||
|
||||
@@ -724,8 +894,8 @@ class TestTier3CStub:
|
||||
"freq_max": 30.0e6,
|
||||
"prf1": 1000.0,
|
||||
"prf2": 2000.0,
|
||||
"max_distance": 50000.0,
|
||||
"map_size": 50000.0,
|
||||
"max_distance": 1536.0,
|
||||
"map_size": 1536.0,
|
||||
}
|
||||
pkt = self._build_settings_packet(values)
|
||||
result = self._run_stub(stub_binary, pkt)
|
||||
@@ -784,11 +954,11 @@ class TestTier3CStub:
|
||||
def test_bad_markers_rejected(self, stub_binary):
|
||||
"""Packet with wrong start/end markers must be rejected."""
|
||||
values = {
|
||||
"system_frequency": 10.0e9, "chirp_duration_1": 30.0e-6,
|
||||
"system_frequency": 10.5e9, "chirp_duration_1": 30.0e-6,
|
||||
"chirp_duration_2": 0.5e-6, "chirps_per_position": 32,
|
||||
"freq_min": 10.0e6, "freq_max": 30.0e6,
|
||||
"prf1": 1000.0, "prf2": 2000.0,
|
||||
"max_distance": 50000.0, "map_size": 50000.0,
|
||||
"max_distance": 1536.0, "map_size": 1536.0,
|
||||
}
|
||||
pkt = self._build_settings_packet(values)
|
||||
|
||||
@@ -826,3 +996,107 @@ class TestTier3CStub:
|
||||
assert result.get("parse_ok") == "true", (
|
||||
f"Boundary values rejected: {result}"
|
||||
)
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# TIER 4: Stale Value Detection (LLM Agent Guardrails)
|
||||
# ===================================================================
|
||||
|
||||
class TestTier4BannedPatterns:
|
||||
"""
|
||||
Scan source files for known-wrong values that have been corrected.
|
||||
|
||||
These patterns are stale ADI Phaser defaults, wrong sample rates,
|
||||
and incorrect range calculations that were cleaned up in commit
|
||||
d259e5c. If an LLM agent reintroduces them, this test catches it.
|
||||
|
||||
IMPORTANT: Allowlist entries exist for legitimate uses (e.g. comments
|
||||
explaining what was wrong, test files checking for these values).
|
||||
"""
|
||||
|
||||
# (regex_pattern, description, file_extensions_to_scan)
|
||||
BANNED_PATTERNS: ClassVar[list[tuple[str, str, tuple[str, ...]]]] = [
|
||||
# Wrong carrier frequency (ADI Phaser default, not PLFM)
|
||||
(r'10[._]?525\s*e\s*9|10\.525\s*GHz|10525000000',
|
||||
"Stale ADI Phaser carrier freq — PLFM uses 10.5 GHz",
|
||||
("*.py", "*.v", "*.vh", "*.cpp", "*.h")),
|
||||
|
||||
# Wrong post-DDC sample rate (ADI Phaser uses 4 MSPS)
|
||||
(r'(?<!\d)4e6(?!\d)|4_?000_?000\.0?\s*#.*(?:sample|samp|rate|fs)',
|
||||
"Stale ADI 4 MSPS rate — PLFM post-DDC is 100 MSPS",
|
||||
("*.py",)),
|
||||
|
||||
# Wrong range per bin values from old calculations
|
||||
(r'(?<!\d)4\.8\s*(?:m/bin|m per|meters?\s*per)',
|
||||
"Stale bin spacing 4.8 m — PLFM is 24.0 m/bin",
|
||||
("*.py", "*.cpp")),
|
||||
|
||||
(r'(?<!\d)5\.6\s*(?:m/bin|m per|meters?\s*per)',
|
||||
"Stale bin spacing 5.6 m — PLFM is 24.0 m/bin",
|
||||
("*.py", "*.cpp")),
|
||||
|
||||
# Wrong range resolution from deramped FMCW formula
|
||||
(r'781\.25',
|
||||
"Stale ADI range value 781.25 — not applicable to PLFM",
|
||||
("*.py",)),
|
||||
]
|
||||
|
||||
# Files that are allowed to contain banned patterns
|
||||
# (this test file, historical docs, comparison scripts)
|
||||
# ADI co-sim files legitimately reference ADI Phaser hardware params
|
||||
# because they process ADI test stimulus data (10.525 GHz, 4 MSPS).
|
||||
ALLOWLIST: ClassVar[set[str]] = {
|
||||
"test_cross_layer_contract.py", # This file — contains the patterns!
|
||||
"CLAUDE.md", # Context doc may reference old values
|
||||
"golden_reference.py", # Processes ADI Phaser test data
|
||||
"tb_fullchain_mti_cfar_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_doppler_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_range_fft_realdata.v", # $display of ADI test stimulus info
|
||||
"tb_fullchain_realdata.v", # $display of ADI test stimulus info
|
||||
}
|
||||
|
||||
def _scan_files(self, pattern_re, extensions):
|
||||
"""Scan firmware source files for a regex pattern."""
|
||||
hits = []
|
||||
firmware_dir = cp.REPO_ROOT / "9_Firmware"
|
||||
|
||||
for ext in extensions:
|
||||
for fpath in firmware_dir.rglob(ext.replace("*", "**/*") if "**" in ext else ext):
|
||||
# Skip allowlisted files
|
||||
if fpath.name in self.ALLOWLIST:
|
||||
continue
|
||||
# Skip __pycache__, .git, etc.
|
||||
parts = set(fpath.parts)
|
||||
if parts & {"__pycache__", ".git", ".venv", "venv", ".ruff_cache"}:
|
||||
continue
|
||||
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
for i, line in enumerate(text.splitlines(), 1):
|
||||
if pattern_re.search(line):
|
||||
hits.append(f"{fpath.relative_to(cp.REPO_ROOT)}:{i}: {line.strip()[:120]}")
|
||||
|
||||
return hits
|
||||
|
||||
def test_no_banned_stale_values(self):
|
||||
"""
|
||||
No source file should contain known-wrong stale values.
|
||||
|
||||
If this fails, an LLM agent likely reintroduced a corrected value.
|
||||
Check the flagged lines and fix them. If a line is a legitimate
|
||||
use (e.g. a comment explaining history), add the file to ALLOWLIST.
|
||||
"""
|
||||
all_hits = []
|
||||
for pattern_str, description, extensions in self.BANNED_PATTERNS:
|
||||
pattern_re = re.compile(pattern_str, re.IGNORECASE)
|
||||
hits = self._scan_files(pattern_re, extensions)
|
||||
all_hits.extend(f"[{description}] {hit}" for hit in hits)
|
||||
|
||||
assert not all_hits, (
|
||||
f"Found {len(all_hits)} stale/banned value(s) in source files:\n"
|
||||
+ "\n".join(f" {h}" for h in all_hits[:20])
|
||||
+ ("\n ... and more" if len(all_hits) > 20 else "")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user