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/rtl_doppler_*.csv
|
||||||
9_Firmware/9_2_FPGA/tb/cosim/compare_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/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
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# Define parameters
|
# 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
|
Ts = 1 / fs # Sampling time
|
||||||
Tb = 1e-6 # Burst time
|
Tb = 1e-6 # Burst time
|
||||||
Tau = 30e-6 # Pulse repetition time
|
Tau = 30e-6 # Pulse repetition time
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ RadarSettings::RadarSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RadarSettings::resetToDefaults() {
|
void RadarSettings::resetToDefaults() {
|
||||||
system_frequency = 10.0e9; // 10 GHz
|
system_frequency = 10.5e9; // 10.5 GHz (PLFM TX LO, ADF4382 config)
|
||||||
chirp_duration_1 = 30.0e-6; // 30 �s
|
chirp_duration_1 = 30.0e-6; // 30 µs
|
||||||
chirp_duration_2 = 0.5e-6; // 0.5 �s
|
chirp_duration_2 = 0.5e-6; // 0.5 µs
|
||||||
chirps_per_position = 32;
|
chirps_per_position = 32;
|
||||||
freq_min = 10.0e6; // 10 MHz
|
freq_min = 10.0e6; // 10 MHz
|
||||||
freq_max = 30.0e6; // 30 MHz
|
freq_max = 30.0e6; // 30 MHz
|
||||||
prf1 = 1000.0; // 1 kHz
|
prf1 = 1000.0; // 1 kHz
|
||||||
prf2 = 2000.0; // 2 kHz
|
prf2 = 2000.0; // 2 kHz
|
||||||
max_distance = 50000.0; // 50 km
|
max_distance = 1536.0; // 1536 m (64 bins × 24 m, 3 km mode)
|
||||||
map_size = 50000.0; // 50 km
|
map_size = 1536.0; // 1536 m
|
||||||
|
|
||||||
settings_valid = true;
|
settings_valid = true;
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ bool RadarSettings::validateSettings() {
|
|||||||
if (prf1 < 100 || prf1 > 10000) return false;
|
if (prf1 < 100 || prf1 > 10000) return false;
|
||||||
if (prf2 < 100 || prf2 > 10000) return false;
|
if (prf2 < 100 || prf2 > 10000) return false;
|
||||||
if (max_distance < 100 || max_distance > 100000) 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,3 +18,9 @@ test_bug12_pa_cal_loop_inverted
|
|||||||
test_bug13_dac2_adc_buffer_mismatch
|
test_bug13_dac2_adc_buffer_mismatch
|
||||||
test_bug14_diag_section_args
|
test_bug14_diag_section_args
|
||||||
test_bug15_htim3_dangling_extern
|
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_elevation, // Toggle for new elevation (32)
|
||||||
input wire mc_new_azimuth, // Toggle for new azimuth (50)
|
input wire mc_new_azimuth, // Toggle for new azimuth (50)
|
||||||
|
|
||||||
input wire [15:0] long_chirp_real,
|
// Reference chirp (upstream memory loader selects long/short via use_long_chirp)
|
||||||
input wire [15:0] long_chirp_imag,
|
input wire [15:0] ref_chirp_real,
|
||||||
input wire [15:0] short_chirp_real,
|
input wire [15:0] ref_chirp_imag,
|
||||||
input wire [15:0] short_chirp_imag,
|
|
||||||
|
|
||||||
// Memory system interface
|
// Memory system interface
|
||||||
output reg [1:0] segment_request,
|
output reg [1:0] segment_request,
|
||||||
@@ -244,6 +243,7 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
if (!use_long_chirp) begin
|
if (!use_long_chirp) begin
|
||||||
if (chirp_samples_collected >= SHORT_CHIRP_SAMPLES - 1) begin
|
if (chirp_samples_collected >= SHORT_CHIRP_SAMPLES - 1) begin
|
||||||
state <= ST_ZERO_PAD;
|
state <= ST_ZERO_PAD;
|
||||||
|
chirp_complete <= 1; // Bug A fix: mark chirp done so ST_OUTPUT exits to IDLE
|
||||||
`ifdef SIMULATION
|
`ifdef SIMULATION
|
||||||
$display("[MULTI_SEG_FIXED] Short chirp: collected %d samples, starting zero-pad",
|
$display("[MULTI_SEG_FIXED] Short chirp: collected %d samples, starting zero-pad",
|
||||||
chirp_samples_collected + 1);
|
chirp_samples_collected + 1);
|
||||||
@@ -500,11 +500,9 @@ matched_filter_processing_chain m_f_p_c(
|
|||||||
// Chirp Selection
|
// Chirp Selection
|
||||||
.chirp_counter(chirp_counter),
|
.chirp_counter(chirp_counter),
|
||||||
|
|
||||||
// Reference Chirp Memory Interfaces
|
// Reference Chirp Memory Interface (single pair — upstream selects long/short)
|
||||||
.long_chirp_real(long_chirp_real),
|
.ref_chirp_real(ref_chirp_real),
|
||||||
.long_chirp_imag(long_chirp_imag),
|
.ref_chirp_imag(ref_chirp_imag),
|
||||||
.short_chirp_real(short_chirp_real),
|
|
||||||
.short_chirp_imag(short_chirp_imag),
|
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
.range_profile_i(fft_pc_i),
|
.range_profile_i(fft_pc_i),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* .clk, .reset_n
|
* .clk, .reset_n
|
||||||
* .adc_data_i, .adc_data_q, .adc_valid <- from input buffer
|
* .adc_data_i, .adc_data_q, .adc_valid <- from input buffer
|
||||||
* .chirp_counter <- 6-bit frame counter
|
* .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
|
* .range_profile_i, .range_profile_q, .range_profile_valid -> output
|
||||||
* .chain_state -> 4-bit status
|
* .chain_state -> 4-bit status
|
||||||
*
|
*
|
||||||
@@ -48,10 +48,10 @@ module matched_filter_processing_chain (
|
|||||||
input wire [5:0] chirp_counter,
|
input wire [5:0] chirp_counter,
|
||||||
|
|
||||||
// Reference chirp (time-domain, latency-aligned by upstream buffer)
|
// Reference chirp (time-domain, latency-aligned by upstream buffer)
|
||||||
input wire [15:0] long_chirp_real,
|
// Upstream chirp_memory_loader_param selects long/short reference
|
||||||
input wire [15:0] long_chirp_imag,
|
// via use_long_chirp — this single pair carries whichever is active.
|
||||||
input wire [15:0] short_chirp_real,
|
input wire [15:0] ref_chirp_real,
|
||||||
input wire [15:0] short_chirp_imag,
|
input wire [15:0] ref_chirp_imag,
|
||||||
|
|
||||||
// Output: range profile (pulse-compressed)
|
// Output: range profile (pulse-compressed)
|
||||||
output wire signed [15:0] range_profile_i,
|
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)
|
// Store first sample (signal + reference)
|
||||||
fwd_buf_i[0] <= $signed(adc_data_i);
|
fwd_buf_i[0] <= $signed(adc_data_i);
|
||||||
fwd_buf_q[0] <= $signed(adc_data_q);
|
fwd_buf_q[0] <= $signed(adc_data_q);
|
||||||
ref_buf_i[0] <= $signed(long_chirp_real);
|
ref_buf_i[0] <= $signed(ref_chirp_real);
|
||||||
ref_buf_q[0] <= $signed(long_chirp_imag);
|
ref_buf_q[0] <= $signed(ref_chirp_imag);
|
||||||
fwd_in_count <= 1;
|
fwd_in_count <= 1;
|
||||||
state <= ST_FWD_FFT;
|
state <= ST_FWD_FFT;
|
||||||
end
|
end
|
||||||
@@ -205,8 +205,8 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
if (adc_valid && fwd_in_count < FFT_SIZE) begin
|
if (adc_valid && fwd_in_count < FFT_SIZE) begin
|
||||||
fwd_buf_i[fwd_in_count] <= $signed(adc_data_i);
|
fwd_buf_i[fwd_in_count] <= $signed(adc_data_i);
|
||||||
fwd_buf_q[fwd_in_count] <= $signed(adc_data_q);
|
fwd_buf_q[fwd_in_count] <= $signed(adc_data_q);
|
||||||
ref_buf_i[fwd_in_count] <= $signed(long_chirp_real);
|
ref_buf_i[fwd_in_count] <= $signed(ref_chirp_real);
|
||||||
ref_buf_q[fwd_in_count] <= $signed(long_chirp_imag);
|
ref_buf_q[fwd_in_count] <= $signed(ref_chirp_imag);
|
||||||
fwd_in_count <= fwd_in_count + 1;
|
fwd_in_count <= fwd_in_count + 1;
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -775,16 +775,16 @@ always @(posedge clk) begin : ref_bram_port
|
|||||||
if (adc_valid) begin
|
if (adc_valid) begin
|
||||||
we = 1'b1;
|
we = 1'b1;
|
||||||
addr = 0;
|
addr = 0;
|
||||||
wdata_i = $signed(long_chirp_real);
|
wdata_i = $signed(ref_chirp_real);
|
||||||
wdata_q = $signed(long_chirp_imag);
|
wdata_q = $signed(ref_chirp_imag);
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ST_COLLECT: begin
|
ST_COLLECT: begin
|
||||||
if (adc_valid && collect_count < FFT_SIZE) begin
|
if (adc_valid && collect_count < FFT_SIZE) begin
|
||||||
we = 1'b1;
|
we = 1'b1;
|
||||||
addr = collect_count[ADDR_BITS-1:0];
|
addr = collect_count[ADDR_BITS-1:0];
|
||||||
wdata_i = $signed(long_chirp_real);
|
wdata_i = $signed(ref_chirp_real);
|
||||||
wdata_q = $signed(long_chirp_imag);
|
wdata_q = $signed(ref_chirp_imag);
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
ST_REF_FFT: begin
|
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 [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude
|
||||||
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
|
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
|
||||||
|
|
||||||
// Reference signals for the processing chain
|
// Reference signal for the processing chain (carries long OR short ref
|
||||||
wire [15:0] long_chirp_real, long_chirp_imag;
|
// depending on use_long_chirp — selected by chirp_memory_loader_param)
|
||||||
wire [15:0] short_chirp_real, short_chirp_imag;
|
wire [15:0] ref_chirp_real, ref_chirp_imag;
|
||||||
|
|
||||||
// ========== DOPPLER PROCESSING SIGNALS ==========
|
// ========== DOPPLER PROCESSING SIGNALS ==========
|
||||||
wire [31:0] range_data_32bit;
|
wire [31:0] range_data_32bit;
|
||||||
@@ -292,7 +292,8 @@ end
|
|||||||
// sample_addr_wire removed — was unused implicit wire (synthesis warning)
|
// sample_addr_wire removed — was unused implicit wire (synthesis warning)
|
||||||
|
|
||||||
// 4. CRITICAL: Reference Chirp Latency Buffer
|
// 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 [15:0] delayed_ref_i, delayed_ref_q;
|
||||||
wire mem_ready_delayed;
|
wire mem_ready_delayed;
|
||||||
|
|
||||||
@@ -308,11 +309,10 @@ latency_buffer #(
|
|||||||
.valid_out(mem_ready_delayed)
|
.valid_out(mem_ready_delayed)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assign delayed reference signals
|
// Assign delayed reference signals (single pair — chirp_memory_loader_param
|
||||||
assign long_chirp_real = delayed_ref_i;
|
// selects long/short reference upstream via use_long_chirp)
|
||||||
assign long_chirp_imag = delayed_ref_q;
|
assign ref_chirp_real = delayed_ref_i;
|
||||||
assign short_chirp_real = delayed_ref_i;
|
assign ref_chirp_imag = delayed_ref_q;
|
||||||
assign short_chirp_imag = delayed_ref_q;
|
|
||||||
|
|
||||||
// 5. Dual Chirp Matched Filter
|
// 5. Dual Chirp Matched Filter
|
||||||
|
|
||||||
@@ -336,10 +336,8 @@ matched_filter_multi_segment mf_dual (
|
|||||||
.mc_new_chirp(mc_new_chirp),
|
.mc_new_chirp(mc_new_chirp),
|
||||||
.mc_new_elevation(mc_new_elevation),
|
.mc_new_elevation(mc_new_elevation),
|
||||||
.mc_new_azimuth(mc_new_azimuth),
|
.mc_new_azimuth(mc_new_azimuth),
|
||||||
.long_chirp_real(delayed_ref_i), // From latency buffer
|
.ref_chirp_real(delayed_ref_i), // From latency buffer (long or short ref)
|
||||||
.long_chirp_imag(delayed_ref_q),
|
.ref_chirp_imag(delayed_ref_q),
|
||||||
.short_chirp_real(delayed_ref_i), // Same for short chirp
|
|
||||||
.short_chirp_imag(delayed_ref_q),
|
|
||||||
.segment_request(segment_request),
|
.segment_request(segment_request),
|
||||||
.mem_request(mem_request),
|
.mem_request(mem_request),
|
||||||
.sample_addr_out(sample_addr_from_chain),
|
.sample_addr_out(sample_addr_from_chain),
|
||||||
|
|||||||
@@ -253,6 +253,68 @@ run_lint_static() {
|
|||||||
fi
|
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
|
# Helper: compile and run a single testbench
|
||||||
# run_test <name> <vvp_path> <iverilog_args...>
|
# 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
|
doppler_processor.v xfft_16.v fft_engine.v
|
||||||
|
|
||||||
if [[ "$QUICK" -eq 0 ]]; then
|
if [[ "$QUICK" -eq 0 ]]; then
|
||||||
# Golden generate
|
# NOTE: The "Receiver golden generate/compare" pair was REMOVED because
|
||||||
run_test "Receiver (golden generate)" \
|
# it was self-blessing: both passes ran the same RTL with the same
|
||||||
tb/tb_rx_golden_reg.vvp \
|
# deterministic stimulus, so the test always passed regardless of bugs.
|
||||||
-DGOLDEN_GENERATE \
|
# Real co-sim coverage is provided by:
|
||||||
tb/tb_radar_receiver_final.v radar_receiver_final.v \
|
# - tb_doppler_realdata.v (committed Python golden hex, exact match)
|
||||||
radar_mode_controller.v tb/ad9484_interface_400m_stub.v \
|
# - tb_fullchain_realdata.v (committed Python golden hex, exact match)
|
||||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
|
# A proper full-pipeline co-sim (DDC→MF→Decim→Doppler vs Python) is
|
||||||
cdc_modules.v fir_lowpass.v ddc_input_interface.v \
|
# planned as a replacement (Phase C of CI test plan).
|
||||||
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
|
|
||||||
|
|
||||||
# Full system top (monitoring-only, legacy)
|
# Full system top (monitoring-only, legacy)
|
||||||
run_test "System Top (radar_system_tb)" \
|
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 \
|
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
|
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
|
||||||
else
|
else
|
||||||
echo " (skipped receiver golden + system top + E2E — use without --quick)"
|
echo " (skipped system top + E2E — use without --quick)"
|
||||||
SKIP=$((SKIP + 4))
|
SKIP=$((SKIP + 2))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
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
|
# 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
|
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
|
Uses ADI CN0566 Phaser radar data (10.525 GHz, used as test stimulus only) to
|
||||||
the FPGA signal processing pipeline stage by stage:
|
validate the FPGA signal processing pipeline stage by stage:
|
||||||
|
|
||||||
ADC → DDC (NCO+mixer+CIC+FIR) → Range FFT → Doppler FFT → Detection
|
ADC → DDC (NCO+mixer+CIC+FIR) → Range FFT → Doppler FFT → Detection
|
||||||
|
|
||||||
@@ -90,7 +90,8 @@ HAMMING_Q15 = [
|
|||||||
0x3088, 0x1B6D, 0x0E5C, 0x0A3D,
|
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_SAMPLE_RATE = 4e6 # 4 MSPS
|
||||||
ADI_IF_FREQ = 100e3 # 100 kHz IF
|
ADI_IF_FREQ = 100e3 # 100 kHz IF
|
||||||
ADI_RF_FREQ = 9.9e9 # 9.9 GHz
|
ADI_RF_FREQ = 9.9e9 # 9.9 GHz
|
||||||
@@ -99,9 +100,17 @@ ADI_RAMP_TIME = 300e-6 # 300 us
|
|||||||
ADI_NUM_CHIRPS = 256
|
ADI_NUM_CHIRPS = 256
|
||||||
ADI_SAMPLES_PER_CHIRP = 1079
|
ADI_SAMPLES_PER_CHIRP = 1079
|
||||||
|
|
||||||
# AERIS-10 parameters
|
# AERIS-10 hardware parameters (from ADF4382/AD9523/main.cpp configuration)
|
||||||
AERIS_FS = 400e6 # 400 MHz ADC clock
|
AERIS_FS = 400e6 # 400 MHz ADC clock (AD9523 OUT4)
|
||||||
AERIS_IF = 120e6 # 120 MHz IF
|
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
|
# For synthesis: the latency_buffer feeds ref data to the chain via
|
||||||
# chirp_memory_loader_param → latency_buffer → chain.
|
# 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
|
# - mem_request drives valid_in on the latency buffer
|
||||||
# - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles
|
# - 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
|
# 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
|
# input samples). The reference data needs to arrive LATENCY cycles
|
||||||
# after the first mem_request, where LATENCY accounts for:
|
# after the first mem_request, where LATENCY accounts for:
|
||||||
# - The fft_engine pipeline latency from input to output
|
# - 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 [15:0] adc_data_q;
|
||||||
reg adc_valid;
|
reg adc_valid;
|
||||||
reg [5:0] chirp_counter;
|
reg [5:0] chirp_counter;
|
||||||
reg [15:0] long_chirp_real;
|
reg [15:0] ref_chirp_real;
|
||||||
reg [15:0] long_chirp_imag;
|
reg [15:0] ref_chirp_imag;
|
||||||
reg [15:0] short_chirp_real;
|
|
||||||
reg [15:0] short_chirp_imag;
|
|
||||||
wire signed [15:0] range_profile_i;
|
wire signed [15:0] range_profile_i;
|
||||||
wire signed [15:0] range_profile_q;
|
wire signed [15:0] range_profile_q;
|
||||||
wire range_profile_valid;
|
wire range_profile_valid;
|
||||||
@@ -83,10 +81,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
.adc_data_q (adc_data_q),
|
.adc_data_q (adc_data_q),
|
||||||
.adc_valid (adc_valid),
|
.adc_valid (adc_valid),
|
||||||
.chirp_counter (chirp_counter),
|
.chirp_counter (chirp_counter),
|
||||||
.long_chirp_real (long_chirp_real),
|
.ref_chirp_real (ref_chirp_real),
|
||||||
.long_chirp_imag (long_chirp_imag),
|
.ref_chirp_imag (ref_chirp_imag),
|
||||||
.short_chirp_real (short_chirp_real),
|
|
||||||
.short_chirp_imag (short_chirp_imag),
|
|
||||||
.range_profile_i (range_profile_i),
|
.range_profile_i (range_profile_i),
|
||||||
.range_profile_q (range_profile_q),
|
.range_profile_q (range_profile_q),
|
||||||
.range_profile_valid (range_profile_valid),
|
.range_profile_valid (range_profile_valid),
|
||||||
@@ -133,10 +129,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
adc_data_i = 16'd0;
|
adc_data_i = 16'd0;
|
||||||
adc_data_q = 16'd0;
|
adc_data_q = 16'd0;
|
||||||
chirp_counter = 6'd0;
|
chirp_counter = 6'd0;
|
||||||
long_chirp_real = 16'd0;
|
ref_chirp_real = 16'd0;
|
||||||
long_chirp_imag = 16'd0;
|
ref_chirp_imag = 16'd0;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
cap_enable = 0;
|
cap_enable = 0;
|
||||||
cap_count = 0;
|
cap_count = 0;
|
||||||
cap_max_abs = 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);
|
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
|
||||||
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
||||||
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
||||||
long_chirp_real = $rtoi(8000.0 * $cos(angle));
|
ref_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||||
long_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -187,10 +179,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||||
adc_data_i = 16'sh1000;
|
adc_data_i = 16'sh1000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -233,10 +223,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||||
adc_data_i = gold_sig_i[k];
|
adc_data_i = gold_sig_i[k];
|
||||||
adc_data_q = gold_sig_q[k];
|
adc_data_q = gold_sig_q[k];
|
||||||
long_chirp_real = gold_ref_i[k];
|
ref_chirp_real = gold_ref_i[k];
|
||||||
long_chirp_imag = gold_ref_q[k];
|
ref_chirp_imag = gold_ref_q[k];
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -374,10 +362,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'd0;
|
adc_data_i = 16'd0;
|
||||||
adc_data_q = 16'd0;
|
adc_data_q = 16'd0;
|
||||||
long_chirp_real = 16'd0;
|
ref_chirp_real = 16'd0;
|
||||||
long_chirp_imag = 16'd0;
|
ref_chirp_imag = 16'd0;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -449,10 +435,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
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_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
|
||||||
adc_data_q = $rtoi(8000.0 * $sin(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));
|
ref_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));
|
ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -568,10 +552,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'sh7FFF;
|
adc_data_i = 16'sh7FFF;
|
||||||
adc_data_q = 16'sh7FFF;
|
adc_data_q = 16'sh7FFF;
|
||||||
long_chirp_real = 16'sh7FFF;
|
ref_chirp_real = 16'sh7FFF;
|
||||||
long_chirp_imag = 16'sh7FFF;
|
ref_chirp_imag = 16'sh7FFF;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -589,10 +571,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'sh8000;
|
adc_data_i = 16'sh8000;
|
||||||
adc_data_q = 16'sh8000;
|
adc_data_q = 16'sh8000;
|
||||||
long_chirp_real = 16'sh8000;
|
ref_chirp_real = 16'sh8000;
|
||||||
long_chirp_imag = 16'sh8000;
|
ref_chirp_imag = 16'sh8000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -611,16 +591,14 @@ module tb_matched_filter_processing_chain;
|
|||||||
if (i % 2 == 0) begin
|
if (i % 2 == 0) begin
|
||||||
adc_data_i = 16'sh7FFF;
|
adc_data_i = 16'sh7FFF;
|
||||||
adc_data_q = 16'sh7FFF;
|
adc_data_q = 16'sh7FFF;
|
||||||
long_chirp_real = 16'sh7FFF;
|
ref_chirp_real = 16'sh7FFF;
|
||||||
long_chirp_imag = 16'sh7FFF;
|
ref_chirp_imag = 16'sh7FFF;
|
||||||
end else begin
|
end else begin
|
||||||
adc_data_i = 16'sh8000;
|
adc_data_i = 16'sh8000;
|
||||||
adc_data_q = 16'sh8000;
|
adc_data_q = 16'sh8000;
|
||||||
long_chirp_real = 16'sh8000;
|
ref_chirp_real = 16'sh8000;
|
||||||
long_chirp_imag = 16'sh8000;
|
ref_chirp_imag = 16'sh8000;
|
||||||
end
|
end
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -641,10 +619,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < 512; i = i + 1) begin
|
for (i = 0; i < 512; i = i + 1) begin
|
||||||
adc_data_i = 16'sh1000;
|
adc_data_i = 16'sh1000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -683,10 +659,8 @@ module tb_matched_filter_processing_chain;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'sh1000;
|
adc_data_i = 16'sh1000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,8 @@ module tb_mf_chain_synth;
|
|||||||
reg [15:0] adc_data_q;
|
reg [15:0] adc_data_q;
|
||||||
reg adc_valid;
|
reg adc_valid;
|
||||||
reg [5:0] chirp_counter;
|
reg [5:0] chirp_counter;
|
||||||
reg [15:0] long_chirp_real;
|
reg [15:0] ref_chirp_real;
|
||||||
reg [15:0] long_chirp_imag;
|
reg [15:0] ref_chirp_imag;
|
||||||
reg [15:0] short_chirp_real;
|
|
||||||
reg [15:0] short_chirp_imag;
|
|
||||||
wire signed [15:0] range_profile_i;
|
wire signed [15:0] range_profile_i;
|
||||||
wire signed [15:0] range_profile_q;
|
wire signed [15:0] range_profile_q;
|
||||||
wire range_profile_valid;
|
wire range_profile_valid;
|
||||||
@@ -78,10 +76,8 @@ module tb_mf_chain_synth;
|
|||||||
.adc_data_q (adc_data_q),
|
.adc_data_q (adc_data_q),
|
||||||
.adc_valid (adc_valid),
|
.adc_valid (adc_valid),
|
||||||
.chirp_counter (chirp_counter),
|
.chirp_counter (chirp_counter),
|
||||||
.long_chirp_real (long_chirp_real),
|
.ref_chirp_real (ref_chirp_real),
|
||||||
.long_chirp_imag (long_chirp_imag),
|
.ref_chirp_imag (ref_chirp_imag),
|
||||||
.short_chirp_real (short_chirp_real),
|
|
||||||
.short_chirp_imag (short_chirp_imag),
|
|
||||||
.range_profile_i (range_profile_i),
|
.range_profile_i (range_profile_i),
|
||||||
.range_profile_q (range_profile_q),
|
.range_profile_q (range_profile_q),
|
||||||
.range_profile_valid (range_profile_valid),
|
.range_profile_valid (range_profile_valid),
|
||||||
@@ -130,10 +126,8 @@ module tb_mf_chain_synth;
|
|||||||
adc_data_i = 16'd0;
|
adc_data_i = 16'd0;
|
||||||
adc_data_q = 16'd0;
|
adc_data_q = 16'd0;
|
||||||
chirp_counter = 6'd0;
|
chirp_counter = 6'd0;
|
||||||
long_chirp_real = 16'd0;
|
ref_chirp_real = 16'd0;
|
||||||
long_chirp_imag = 16'd0;
|
ref_chirp_imag = 16'd0;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
cap_enable = 0;
|
cap_enable = 0;
|
||||||
cap_count = 0;
|
cap_count = 0;
|
||||||
cap_max_abs = 0;
|
cap_max_abs = 0;
|
||||||
@@ -177,10 +171,8 @@ module tb_mf_chain_synth;
|
|||||||
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
for (k = 0; k < FFT_SIZE; k = k + 1) begin
|
||||||
adc_data_i = 16'sh1000; // +4096
|
adc_data_i = 16'sh1000; // +4096
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -199,10 +191,8 @@ module tb_mf_chain_synth;
|
|||||||
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
|
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
|
||||||
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
adc_data_i = $rtoi(8000.0 * $cos(angle));
|
||||||
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
adc_data_q = $rtoi(8000.0 * $sin(angle));
|
||||||
long_chirp_real = $rtoi(8000.0 * $cos(angle));
|
ref_chirp_real = $rtoi(8000.0 * $cos(angle));
|
||||||
long_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -219,16 +209,14 @@ module tb_mf_chain_synth;
|
|||||||
if (k == 0) begin
|
if (k == 0) begin
|
||||||
adc_data_i = 16'sh4000; // 0.5 in Q15
|
adc_data_i = 16'sh4000; // 0.5 in Q15
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh4000;
|
ref_chirp_real = 16'sh4000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
end else begin
|
end else begin
|
||||||
adc_data_i = 16'sh0000;
|
adc_data_i = 16'sh0000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh0000;
|
ref_chirp_real = 16'sh0000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
end
|
end
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
#1;
|
#1;
|
||||||
@@ -309,10 +297,8 @@ module tb_mf_chain_synth;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'd0;
|
adc_data_i = 16'd0;
|
||||||
adc_data_q = 16'd0;
|
adc_data_q = 16'd0;
|
||||||
long_chirp_real = 16'd0;
|
ref_chirp_real = 16'd0;
|
||||||
long_chirp_imag = 16'd0;
|
ref_chirp_imag = 16'd0;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -379,10 +365,8 @@ module tb_mf_chain_synth;
|
|||||||
for (i = 0; i < 512; i = i + 1) begin
|
for (i = 0; i < 512; i = i + 1) begin
|
||||||
adc_data_i = 16'sh1000;
|
adc_data_i = 16'sh1000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -439,10 +423,8 @@ module tb_mf_chain_synth;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
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_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
|
||||||
adc_data_q = $rtoi(8000.0 * $sin(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));
|
ref_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));
|
ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -469,10 +451,8 @@ module tb_mf_chain_synth;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'sh7FFF;
|
adc_data_i = 16'sh7FFF;
|
||||||
adc_data_q = 16'sh7FFF;
|
adc_data_q = 16'sh7FFF;
|
||||||
long_chirp_real = 16'sh7FFF;
|
ref_chirp_real = 16'sh7FFF;
|
||||||
long_chirp_imag = 16'sh7FFF;
|
ref_chirp_imag = 16'sh7FFF;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
end
|
end
|
||||||
@@ -495,10 +475,8 @@ module tb_mf_chain_synth;
|
|||||||
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
for (i = 0; i < FFT_SIZE; i = i + 1) begin
|
||||||
adc_data_i = 16'sh1000;
|
adc_data_i = 16'sh1000;
|
||||||
adc_data_q = 16'sh0000;
|
adc_data_q = 16'sh0000;
|
||||||
long_chirp_real = 16'sh1000;
|
ref_chirp_real = 16'sh1000;
|
||||||
long_chirp_imag = 16'sh0000;
|
ref_chirp_imag = 16'sh0000;
|
||||||
short_chirp_real = 16'd0;
|
|
||||||
short_chirp_imag = 16'd0;
|
|
||||||
adc_valid = 1'b1;
|
adc_valid = 1'b1;
|
||||||
@(posedge clk); #1;
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
|||||||
@@ -88,10 +88,8 @@ reg [15:0] adc_data_i;
|
|||||||
reg [15:0] adc_data_q;
|
reg [15:0] adc_data_q;
|
||||||
reg adc_valid;
|
reg adc_valid;
|
||||||
reg [5:0] chirp_counter;
|
reg [5:0] chirp_counter;
|
||||||
reg [15:0] long_chirp_real;
|
reg [15:0] ref_chirp_real;
|
||||||
reg [15:0] long_chirp_imag;
|
reg [15:0] ref_chirp_imag;
|
||||||
reg [15:0] short_chirp_real;
|
|
||||||
reg [15:0] short_chirp_imag;
|
|
||||||
|
|
||||||
wire signed [15:0] range_profile_i;
|
wire signed [15:0] range_profile_i;
|
||||||
wire signed [15:0] range_profile_q;
|
wire signed [15:0] range_profile_q;
|
||||||
@@ -108,10 +106,8 @@ matched_filter_processing_chain dut (
|
|||||||
.adc_data_q(adc_data_q),
|
.adc_data_q(adc_data_q),
|
||||||
.adc_valid(adc_valid),
|
.adc_valid(adc_valid),
|
||||||
.chirp_counter(chirp_counter),
|
.chirp_counter(chirp_counter),
|
||||||
.long_chirp_real(long_chirp_real),
|
.ref_chirp_real(ref_chirp_real),
|
||||||
.long_chirp_imag(long_chirp_imag),
|
.ref_chirp_imag(ref_chirp_imag),
|
||||||
.short_chirp_real(short_chirp_real),
|
|
||||||
.short_chirp_imag(short_chirp_imag),
|
|
||||||
.range_profile_i(range_profile_i),
|
.range_profile_i(range_profile_i),
|
||||||
.range_profile_q(range_profile_q),
|
.range_profile_q(range_profile_q),
|
||||||
.range_profile_valid(range_profile_valid),
|
.range_profile_valid(range_profile_valid),
|
||||||
@@ -157,10 +153,8 @@ task apply_reset;
|
|||||||
adc_data_q <= 16'd0;
|
adc_data_q <= 16'd0;
|
||||||
adc_valid <= 1'b0;
|
adc_valid <= 1'b0;
|
||||||
chirp_counter <= 6'd0;
|
chirp_counter <= 6'd0;
|
||||||
long_chirp_real <= 16'd0;
|
ref_chirp_real <= 16'd0;
|
||||||
long_chirp_imag <= 16'd0;
|
ref_chirp_imag <= 16'd0;
|
||||||
short_chirp_real <= 16'd0;
|
|
||||||
short_chirp_imag <= 16'd0;
|
|
||||||
repeat(4) @(posedge clk);
|
repeat(4) @(posedge clk);
|
||||||
reset_n <= 1'b1;
|
reset_n <= 1'b1;
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
@@ -201,18 +195,16 @@ initial begin
|
|||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
adc_data_i <= sig_mem_i[i];
|
adc_data_i <= sig_mem_i[i];
|
||||||
adc_data_q <= sig_mem_q[i];
|
adc_data_q <= sig_mem_q[i];
|
||||||
long_chirp_real <= ref_mem_i[i];
|
ref_chirp_real <= ref_mem_i[i];
|
||||||
long_chirp_imag <= ref_mem_q[i];
|
ref_chirp_imag <= ref_mem_q[i];
|
||||||
short_chirp_real <= 16'd0;
|
|
||||||
short_chirp_imag <= 16'd0;
|
|
||||||
adc_valid <= 1'b1;
|
adc_valid <= 1'b1;
|
||||||
end
|
end
|
||||||
@(posedge clk);
|
@(posedge clk);
|
||||||
adc_valid <= 1'b0;
|
adc_valid <= 1'b0;
|
||||||
adc_data_i <= 16'd0;
|
adc_data_i <= 16'd0;
|
||||||
adc_data_q <= 16'd0;
|
adc_data_q <= 16'd0;
|
||||||
long_chirp_real <= 16'd0;
|
ref_chirp_real <= 16'd0;
|
||||||
long_chirp_imag <= 16'd0;
|
ref_chirp_imag <= 16'd0;
|
||||||
|
|
||||||
$display("All samples fed. Waiting for processing...");
|
$display("All samples fed. Waiting for processing...");
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,8 @@ reg [5:0] chirp_counter;
|
|||||||
reg mc_new_chirp;
|
reg mc_new_chirp;
|
||||||
reg mc_new_elevation;
|
reg mc_new_elevation;
|
||||||
reg mc_new_azimuth;
|
reg mc_new_azimuth;
|
||||||
reg [15:0] long_chirp_real;
|
reg [15:0] ref_chirp_real;
|
||||||
reg [15:0] long_chirp_imag;
|
reg [15:0] ref_chirp_imag;
|
||||||
reg [15:0] short_chirp_real;
|
|
||||||
reg [15:0] short_chirp_imag;
|
|
||||||
reg mem_ready;
|
reg mem_ready;
|
||||||
|
|
||||||
wire signed [15:0] pc_i_w;
|
wire signed [15:0] pc_i_w;
|
||||||
@@ -84,10 +82,8 @@ matched_filter_multi_segment dut (
|
|||||||
.mc_new_chirp(mc_new_chirp),
|
.mc_new_chirp(mc_new_chirp),
|
||||||
.mc_new_elevation(mc_new_elevation),
|
.mc_new_elevation(mc_new_elevation),
|
||||||
.mc_new_azimuth(mc_new_azimuth),
|
.mc_new_azimuth(mc_new_azimuth),
|
||||||
.long_chirp_real(long_chirp_real),
|
.ref_chirp_real(ref_chirp_real),
|
||||||
.long_chirp_imag(long_chirp_imag),
|
.ref_chirp_imag(ref_chirp_imag),
|
||||||
.short_chirp_real(short_chirp_real),
|
|
||||||
.short_chirp_imag(short_chirp_imag),
|
|
||||||
.segment_request(segment_request),
|
.segment_request(segment_request),
|
||||||
.sample_addr_out(sample_addr_out),
|
.sample_addr_out(sample_addr_out),
|
||||||
.mem_request(mem_request),
|
.mem_request(mem_request),
|
||||||
@@ -123,11 +119,11 @@ end
|
|||||||
always @(posedge clk) begin
|
always @(posedge clk) begin
|
||||||
if (mem_request) begin
|
if (mem_request) begin
|
||||||
if (use_long_chirp) begin
|
if (use_long_chirp) begin
|
||||||
long_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}];
|
ref_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}];
|
||||||
long_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}];
|
ref_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}];
|
||||||
end else begin
|
end else begin
|
||||||
short_chirp_real <= ref_mem_i[sample_addr_out];
|
ref_chirp_real <= ref_mem_i[sample_addr_out];
|
||||||
short_chirp_imag <= ref_mem_q[sample_addr_out];
|
ref_chirp_imag <= ref_mem_q[sample_addr_out];
|
||||||
end
|
end
|
||||||
mem_ready <= 1'b1;
|
mem_ready <= 1'b1;
|
||||||
end else begin
|
end else begin
|
||||||
@@ -176,10 +172,8 @@ task apply_reset;
|
|||||||
mc_new_chirp <= 1'b0;
|
mc_new_chirp <= 1'b0;
|
||||||
mc_new_elevation <= 1'b0;
|
mc_new_elevation <= 1'b0;
|
||||||
mc_new_azimuth <= 1'b0;
|
mc_new_azimuth <= 1'b0;
|
||||||
long_chirp_real <= 16'd0;
|
ref_chirp_real <= 16'd0;
|
||||||
long_chirp_imag <= 16'd0;
|
ref_chirp_imag <= 16'd0;
|
||||||
short_chirp_real <= 16'd0;
|
|
||||||
short_chirp_imag <= 16'd0;
|
|
||||||
mem_ready <= 1'b0;
|
mem_ready <= 1'b0;
|
||||||
repeat(10) @(posedge clk);
|
repeat(10) @(posedge clk);
|
||||||
reset_n <= 1'b1;
|
reset_n <= 1'b1;
|
||||||
|
|||||||
@@ -7,43 +7,21 @@
|
|||||||
// -> matched_filter_multi_segment -> range_bin_decimator
|
// -> matched_filter_multi_segment -> range_bin_decimator
|
||||||
// -> doppler_processor_optimized -> doppler_output
|
// -> 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 POINTS:
|
||||||
// Tap 1 (DDC output) - bounds checking only (CDC jitter -> non-deterministic)
|
// 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
|
// 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],
|
// Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0],
|
||||||
// range_bin_out[5: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:
|
// Strategy:
|
||||||
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
|
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
|
||||||
// - Overrides radar_mode_controller timing params for fast simulation
|
// - Overrides radar_mode_controller timing params for fast simulation
|
||||||
// - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband)
|
// - 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
|
// Convention: check task, VCD dump, CSV output, pass/fail summary
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -194,46 +172,6 @@ task check;
|
|||||||
end
|
end
|
||||||
endtask
|
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)
|
// DDC ENERGY ACCUMULATOR (Bounds Check B1)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -257,7 +195,7 @@ always @(posedge clk_100m) begin
|
|||||||
end
|
end
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DOPPLER OUTPUT CAPTURE, GOLDEN COMPARISON, AND DUPLICATE DETECTION
|
// DOPPLER OUTPUT CAPTURE AND DUPLICATE DETECTION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
integer doppler_output_count;
|
integer doppler_output_count;
|
||||||
integer doppler_frame_count;
|
integer doppler_frame_count;
|
||||||
@@ -311,13 +249,6 @@ end
|
|||||||
// Monitor doppler outputs -- only after reset released
|
// Monitor doppler outputs -- only after reset released
|
||||||
always @(posedge clk_100m) begin
|
always @(posedge clk_100m) begin
|
||||||
if (reset_n && doppler_valid) begin : doppler_capture_block
|
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 ----
|
// ---- Magnitude intermediates for B2 ----
|
||||||
reg signed [16:0] mag_i_signed, mag_q_signed;
|
reg signed [16:0] mag_i_signed, mag_q_signed;
|
||||||
integer mag_i, mag_q, mag_sum;
|
integer mag_i, mag_q, mag_sum;
|
||||||
@@ -350,9 +281,6 @@ always @(posedge clk_100m) begin
|
|||||||
if ((doppler_output_count % 256) == 0)
|
if ((doppler_output_count % 256) == 0)
|
||||||
$display("[INFO] %0d doppler outputs so far (t=%0t)", doppler_output_count, $time);
|
$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) ----
|
// ---- Duplicate detection (B5) ----
|
||||||
if (range_bin_out < 64 && doppler_bin < 32) begin
|
if (range_bin_out < 64 && doppler_bin < 32) begin
|
||||||
if (index_seen[range_bin_out][doppler_bin]) 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])
|
if (mag_sum > peak_dbin_mag[range_bin_out])
|
||||||
peak_dbin_mag[range_bin_out] = mag_sum;
|
peak_dbin_mag[range_bin_out] = mag_sum;
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
// Track frame completions via doppler_proc -- only after reset
|
// Track frame completions via doppler_proc -- only after reset
|
||||||
@@ -556,13 +446,6 @@ initial begin
|
|||||||
end
|
end
|
||||||
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
|
// RUN CHECKS
|
||||||
// ================================================================
|
// ================================================================
|
||||||
@@ -649,33 +532,7 @@ initial begin
|
|||||||
"S10: DDC produced substantial output (>100 valid samples)");
|
"S10: DDC produced substantial output (>100 valid samples)");
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// GOLDEN COMPARISON REPORT
|
// BOUNDS CHECKS
|
||||||
// ================================================================
|
|
||||||
`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)
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
$display("");
|
$display("");
|
||||||
$display("------------------------------------------------------------");
|
$display("------------------------------------------------------------");
|
||||||
@@ -748,16 +605,8 @@ initial begin
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
$display("");
|
$display("");
|
||||||
$display("============================================================");
|
$display("============================================================");
|
||||||
$display("INTEGRATION TEST -- GOLDEN COMPARISON + BOUNDS");
|
$display("INTEGRATION TEST -- STRUCTURAL + BOUNDS");
|
||||||
$display("============================================================");
|
$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("Bounds checks:");
|
||||||
$display(" B1: DDC RMS energy in range [%0d, %0d]",
|
$display(" B1: DDC RMS energy in range [%0d, %0d]",
|
||||||
(ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY);
|
(ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY);
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class GPSData:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class RadarSettings:
|
class RadarSettings:
|
||||||
"""Radar system configuration"""
|
"""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_1: float = 30e-6 # Long chirp duration (s)
|
||||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration (s)
|
chirp_duration_2: float = 0.5e-6 # Short chirp duration (s)
|
||||||
chirps_per_position: int = 32
|
chirps_per_position: int = 32
|
||||||
@@ -116,8 +116,8 @@ class RadarSettings:
|
|||||||
freq_max: float = 30e6 # Hz
|
freq_max: float = 30e6 # Hz
|
||||||
prf1: float = 1000 # PRF 1 (Hz)
|
prf1: float = 1000 # PRF 1 (Hz)
|
||||||
prf2: float = 2000 # PRF 2 (Hz)
|
prf2: float = 2000 # PRF 2 (Hz)
|
||||||
max_distance: float = 50000 # Max detection range (m)
|
max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m
|
||||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||||
|
|
||||||
|
|
||||||
class TileServer(Enum):
|
class TileServer(Enum):
|
||||||
@@ -198,7 +198,7 @@ class RadarMapWidget(QWidget):
|
|||||||
pitch=0.0
|
pitch=0.0
|
||||||
)
|
)
|
||||||
self._targets: list[RadarTarget] = []
|
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._tile_server = TileServer.OPENSTREETMAP
|
||||||
self._show_coverage = True
|
self._show_coverage = True
|
||||||
self._show_trails = False
|
self._show_trails = False
|
||||||
@@ -1088,7 +1088,7 @@ class TargetSimulator(QObject):
|
|||||||
new_range = target.range - target.velocity * 0.5 # 0.5 second update
|
new_range = target.range - target.velocity * 0.5 # 0.5 second update
|
||||||
|
|
||||||
# Check if target is still in range
|
# 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
|
# Remove this target and add a new one
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class RadarTarget:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RadarSettings:
|
class RadarSettings:
|
||||||
system_frequency: float = 10e9
|
system_frequency: float = 10.5e9
|
||||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||||
chirps_per_position: int = 32
|
chirps_per_position: int = 32
|
||||||
@@ -89,8 +89,8 @@ class RadarSettings:
|
|||||||
freq_max: float = 30e6
|
freq_max: float = 30e6
|
||||||
prf1: float = 1000
|
prf1: float = 1000
|
||||||
prf2: float = 2000
|
prf2: float = 2000
|
||||||
max_distance: float = 50000
|
max_distance: float = 1536
|
||||||
map_size: float = 50000 # Map size in meters
|
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -1196,8 +1196,8 @@ class RadarGUI:
|
|||||||
("Frequency Max (Hz):", "freq_max", 30e6),
|
("Frequency Max (Hz):", "freq_max", 30e6),
|
||||||
("PRF1 (Hz):", "prf1", 1000),
|
("PRF1 (Hz):", "prf1", 1000),
|
||||||
("PRF2 (Hz):", "prf2", 2000),
|
("PRF2 (Hz):", "prf2", 2000),
|
||||||
("Max Distance (m):", "max_distance", 50000),
|
("Max Distance (m):", "max_distance", 1536),
|
||||||
("Map Size (m):", "map_size", 50000),
|
("Map Size (m):", "map_size", 1536),
|
||||||
("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"),
|
("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class RadarTarget:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RadarSettings:
|
class RadarSettings:
|
||||||
system_frequency: float = 10e9
|
system_frequency: float = 10.5e9
|
||||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||||
chirps_per_position: int = 32
|
chirps_per_position: int = 32
|
||||||
@@ -85,8 +85,8 @@ class RadarSettings:
|
|||||||
freq_max: float = 30e6
|
freq_max: float = 30e6
|
||||||
prf1: float = 1000
|
prf1: float = 1000
|
||||||
prf2: float = 2000
|
prf2: float = 2000
|
||||||
max_distance: float = 50000
|
max_distance: float = 1536
|
||||||
map_size: float = 50000 # Map size in meters
|
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -1254,8 +1254,8 @@ class RadarGUI:
|
|||||||
("Frequency Max (Hz):", "freq_max", 30e6),
|
("Frequency Max (Hz):", "freq_max", 30e6),
|
||||||
("PRF1 (Hz):", "prf1", 1000),
|
("PRF1 (Hz):", "prf1", 1000),
|
||||||
("PRF2 (Hz):", "prf2", 2000),
|
("PRF2 (Hz):", "prf2", 2000),
|
||||||
("Max Distance (m):", "max_distance", 50000),
|
("Max Distance (m):", "max_distance", 1536),
|
||||||
("Map Size (m):", "map_size", 50000),
|
("Map Size (m):", "map_size", 1536),
|
||||||
]
|
]
|
||||||
|
|
||||||
self.settings_vars = {}
|
self.settings_vars = {}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class RadarTarget:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RadarSettings:
|
class RadarSettings:
|
||||||
system_frequency: float = 10e9
|
system_frequency: float = 10.5e9
|
||||||
chirp_duration_1: float = 30e-6 # Long chirp duration
|
chirp_duration_1: float = 30e-6 # Long chirp duration
|
||||||
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
chirp_duration_2: float = 0.5e-6 # Short chirp duration
|
||||||
chirps_per_position: int = 32
|
chirps_per_position: int = 32
|
||||||
@@ -72,8 +72,8 @@ class RadarSettings:
|
|||||||
freq_max: float = 30e6
|
freq_max: float = 30e6
|
||||||
prf1: float = 1000
|
prf1: float = 1000
|
||||||
prf2: float = 2000
|
prf2: float = 2000
|
||||||
max_distance: float = 50000
|
max_distance: float = 1536
|
||||||
map_size: float = 50000 # Map size in meters
|
map_size: float = 1536 # Map size in meters (64 bins x 24 m)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GPSData:
|
class GPSData:
|
||||||
@@ -1653,8 +1653,8 @@ class RadarGUI:
|
|||||||
('Frequency Max (Hz):', 'freq_max', 30e6),
|
('Frequency Max (Hz):', 'freq_max', 30e6),
|
||||||
('PRF1 (Hz):', 'prf1', 1000),
|
('PRF1 (Hz):', 'prf1', 1000),
|
||||||
('PRF2 (Hz):', 'prf2', 2000),
|
('PRF2 (Hz):', 'prf2', 2000),
|
||||||
('Max Distance (m):', 'max_distance', 50000),
|
('Max Distance (m):', 'max_distance', 1536),
|
||||||
('Map Size (m):', 'map_size', 50000),
|
('Map Size (m):', 'map_size', 1536),
|
||||||
('Google Maps API Key:', 'google_maps_api_key', 'YOUR_GOOGLE_MAPS_API_KEY')
|
('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")
|
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
||||||
|
|
||||||
# Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max
|
# Physical range grid: matched-filter receiver, 100 MSPS post-DDC, 16:1 decimation
|
||||||
_RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m
|
# range_per_bin = c / (2 * 100e6) * 16 = 24.0 m
|
||||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 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):
|
def __init__(self, tid: int):
|
||||||
self.id = tid
|
self.id = tid
|
||||||
@@ -187,10 +188,10 @@ class DemoSimulator:
|
|||||||
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
|
||||||
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
|
||||||
|
|
||||||
# Range/Doppler scaling (approximate)
|
# Range/Doppler scaling -- matched-filter receiver, 100 MSPS, 16:1 decimation
|
||||||
range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin
|
range_per_bin = (3e8 / (2 * 100e6)) * 16 # 24.0 m/bin
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
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:
|
for t in targets:
|
||||||
if t.range_m > max_range or t.range_m < 0:
|
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
|
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
|
||||||
|
|
||||||
# Radar parameters used for range-axis scaling.
|
# 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
|
C = 3e8 # m/s — speed of light
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk, connection: FT2232HConnection,
|
def __init__(self, root: tk.Tk, connection: FT2232HConnection,
|
||||||
@@ -514,10 +517,9 @@ class RadarDashboard:
|
|||||||
self._build_log_tab(tab_log)
|
self._build_log_tab(tab_log)
|
||||||
|
|
||||||
def _build_display_tab(self, parent):
|
def _build_display_tab(self, parent):
|
||||||
# Compute physical axis limits
|
# Compute physical axis limits -- matched-filter receiver
|
||||||
range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin
|
# Range per bin: c / (2 * fs_processing) * decimation_factor = 24.0 m
|
||||||
# After decimation 1024→64, each range bin = 16 FFT bins
|
range_per_bin = self.C / (2.0 * 100e6) * 16 # 24.0 m
|
||||||
range_per_bin = range_res * 16
|
|
||||||
max_range = range_per_bin * NUM_RANGE_BINS
|
max_range = range_per_bin * NUM_RANGE_BINS
|
||||||
|
|
||||||
doppler_bin_lo = 0
|
doppler_bin_lo = 0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class RadarSettings:
|
|||||||
range_bins: int = 1024
|
range_bins: int = 1024
|
||||||
doppler_bins: int = 32
|
doppler_bins: int = 32
|
||||||
prf: float = 1000
|
prf: float = 1000
|
||||||
max_range: float = 5000
|
max_range: float = 1536
|
||||||
max_velocity: float = 100
|
max_velocity: float = 100
|
||||||
cfar_threshold: float = 13.0
|
cfar_threshold: float = 13.0
|
||||||
|
|
||||||
@@ -577,7 +577,7 @@ class RadarDemoGUI:
|
|||||||
('Range Bins:', 'range_bins', 1024, 256, 2048),
|
('Range Bins:', 'range_bins', 1024, 256, 2048),
|
||||||
('Doppler Bins:', 'doppler_bins', 32, 8, 128),
|
('Doppler Bins:', 'doppler_bins', 32, 8, 128),
|
||||||
('PRF (Hz):', 'prf', 1000, 100, 10000),
|
('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),
|
('Max Velocity (m/s):', 'max_vel', 100, 10, 500),
|
||||||
('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0)
|
('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):
|
def test_has_physical_conversion_fields(self):
|
||||||
s = _models().RadarSettings()
|
s = _models().RadarSettings()
|
||||||
self.assertIsInstance(s.range_resolution, float)
|
self.assertIsInstance(s.range_bin_spacing, float)
|
||||||
self.assertIsInstance(s.velocity_resolution, 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)
|
self.assertGreater(s.velocity_resolution, 0)
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
s = _models().RadarSettings()
|
s = _models().RadarSettings()
|
||||||
self.assertEqual(s.system_frequency, 10e9)
|
self.assertEqual(s.system_frequency, 10.5e9)
|
||||||
self.assertEqual(s.coverage_radius, 50000)
|
self.assertEqual(s.coverage_radius, 1536)
|
||||||
self.assertEqual(s.max_distance, 50000)
|
self.assertEqual(s.max_distance, 1536)
|
||||||
|
|
||||||
|
|
||||||
class TestGPSData(unittest.TestCase):
|
class TestGPSData(unittest.TestCase):
|
||||||
@@ -425,32 +425,42 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc = WaveformConfig()
|
wc = WaveformConfig()
|
||||||
self.assertEqual(wc.sample_rate_hz, 4e6)
|
self.assertEqual(wc.sample_rate_hz, 100e6)
|
||||||
self.assertEqual(wc.bandwidth_hz, 500e6)
|
self.assertEqual(wc.bandwidth_hz, 20e6)
|
||||||
self.assertEqual(wc.chirp_duration_s, 300e-6)
|
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||||
self.assertEqual(wc.center_freq_hz, 10.525e9)
|
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_range_bins, 64)
|
||||||
self.assertEqual(wc.n_doppler_bins, 32)
|
self.assertEqual(wc.n_doppler_bins, 32)
|
||||||
self.assertEqual(wc.fft_size, 1024)
|
self.assertEqual(wc.fft_size, 1024)
|
||||||
self.assertEqual(wc.decimation_factor, 16)
|
self.assertEqual(wc.decimation_factor, 16)
|
||||||
|
|
||||||
def test_range_resolution(self):
|
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
|
from v7.models import WaveformConfig
|
||||||
wc = 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):
|
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
|
from v7.models import WaveformConfig
|
||||||
wc = 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):
|
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
|
from v7.models import WaveformConfig
|
||||||
wc = 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):
|
def test_max_velocity(self):
|
||||||
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
||||||
@@ -466,8 +476,9 @@ class TestWaveformConfig(unittest.TestCase):
|
|||||||
"""Non-default parameters correctly change derived values."""
|
"""Non-default parameters correctly change derived values."""
|
||||||
from v7.models import WaveformConfig
|
from v7.models import WaveformConfig
|
||||||
wc1 = WaveformConfig()
|
wc1 = WaveformConfig()
|
||||||
wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res
|
# Matched-filter: bin_spacing = c/(2*fs)*dec — proportional to 1/fs
|
||||||
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
|
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):
|
def test_zero_center_freq_velocity(self):
|
||||||
"""Zero center freq should cause ZeroDivisionError in velocity calc."""
|
"""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."""
|
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||||
from v7.processing import extract_targets_from_frame
|
from v7.processing import extract_targets_from_frame
|
||||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
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.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)
|
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||||
|
|
||||||
def test_velocity_sign(self):
|
def test_velocity_sign(self):
|
||||||
"""Doppler bin < center → negative velocity, > center → positive."""
|
"""Doppler bin < center → negative velocity, > center → positive."""
|
||||||
from v7.processing import extract_targets_from_frame
|
from v7.processing import extract_targets_from_frame
|
||||||
frame = self._make_frame(det_cells=[(5, 10), (5, 20)])
|
frame = self._make_frame(det_cells=[(5, 10), (5, 20)])
|
||||||
targets = extract_targets_from_frame(frame, velocity_resolution=1.484)
|
targets = extract_targets_from_frame(frame, velocity_resolution=2.67)
|
||||||
# dbin=10: vel = (10-16)*1.484 = -8.904 (approaching)
|
# dbin=10: vel = (10-16)*2.67 = -16.02 (approaching)
|
||||||
# dbin=20: vel = (20-16)*1.484 = +5.936 (receding)
|
# dbin=20: vel = (20-16)*2.67 = +10.68 (receding)
|
||||||
self.assertLess(targets[0].velocity, 0)
|
self.assertLess(targets[0].velocity, 0)
|
||||||
self.assertGreater(targets[1].velocity, 0)
|
self.assertGreater(targets[1].velocity, 0)
|
||||||
|
|
||||||
@@ -954,7 +965,7 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
|||||||
pitch=0.0, heading=90.0)
|
pitch=0.0, heading=90.0)
|
||||||
frame = self._make_frame(det_cells=[(10, 16)])
|
frame = self._make_frame(det_cells=[(10, 16)])
|
||||||
targets = extract_targets_from_frame(
|
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
|
# Should be roughly east of radar position
|
||||||
self.assertAlmostEqual(targets[0].latitude, 41.9, places=2)
|
self.assertAlmostEqual(targets[0].latitude, 41.9, places=2)
|
||||||
self.assertGreater(targets[0].longitude, 12.5)
|
self.assertGreater(targets[0].longitude, 12.5)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class RadarMapWidget(QWidget):
|
|||||||
)
|
)
|
||||||
self._targets: list[RadarTarget] = []
|
self._targets: list[RadarTarget] = []
|
||||||
self._pending_targets: list[RadarTarget] | None = None
|
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._tile_server = TileServer.OPENSTREETMAP
|
||||||
self._show_coverage = True
|
self._show_coverage = True
|
||||||
self._show_trails = False
|
self._show_trails = False
|
||||||
|
|||||||
@@ -105,15 +105,15 @@ class RadarSettings:
|
|||||||
tab and Opcode enum in radar_protocol.py. This dataclass holds only
|
tab and Opcode enum in radar_protocol.py. This dataclass holds only
|
||||||
host-side display/map settings and physical-unit conversion factors.
|
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.
|
the actual waveform parameters.
|
||||||
"""
|
"""
|
||||||
system_frequency: float = 10e9 # Hz (carrier, used for velocity calc)
|
system_frequency: float = 10.5e9 # Hz (PLFM TX LO, verified from ADF4382 config)
|
||||||
range_resolution: float = 781.25 # Meters per range bin (default: 50km/64)
|
range_bin_spacing: float = 24.0 # Meters per decimated range bin (c/(2*100MSPS)*16)
|
||||||
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
velocity_resolution: float = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
|
||||||
max_distance: float = 50000 # Max detection range (m)
|
max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m (3 km mode)
|
||||||
map_size: float = 50000 # Map display size (m)
|
map_size: float = 1536 # Map display size (m)
|
||||||
coverage_radius: float = 50000 # Map coverage radius (m)
|
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -196,47 +196,61 @@ class TileServer(Enum):
|
|||||||
class WaveformConfig:
|
class WaveformConfig:
|
||||||
"""Physical waveform parameters for converting bins to SI units.
|
"""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.
|
can be derived automatically instead of hardcoded in RadarSettings.
|
||||||
|
|
||||||
Defaults match the ADI CN0566 Phaser capture parameters used in
|
Defaults match the PLFM hardware: 100 MSPS post-DDC processing rate,
|
||||||
the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp).
|
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
|
sample_rate_hz: float = 100e6 # Post-DDC processing rate (400 MSPS / 4)
|
||||||
bandwidth_hz: float = 500e6 # Chirp bandwidth
|
bandwidth_hz: float = 20e6 # Chirp bandwidth (Phase 1 target: 30 MHz)
|
||||||
chirp_duration_s: float = 300e-6 # Chirp ramp time
|
chirp_duration_s: float = 30e-6 # Long chirp ramp (informational only)
|
||||||
center_freq_hz: float = 10.525e9 # Carrier frequency
|
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
|
||||||
n_range_bins: int = 64 # After decimation
|
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
|
n_doppler_bins: int = 32 # After Doppler FFT
|
||||||
fft_size: int = 1024 # Pre-decimation FFT length
|
fft_size: int = 1024 # Pre-decimation FFT length
|
||||||
decimation_factor: int = 16 # 1024 → 64
|
decimation_factor: int = 16 # 1024 → 64
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def range_resolution_m(self) -> float:
|
def bin_spacing_m(self) -> float:
|
||||||
"""Meters per decimated range bin (FMCW deramped baseband).
|
"""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*.
|
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
|
c = 299_792_458.0
|
||||||
raw_bin = (
|
raw_bin = c / (2.0 * self.sample_rate_hz)
|
||||||
c * self.sample_rate_hz * self.chirp_duration_s
|
|
||||||
/ (2.0 * self.fft_size * self.bandwidth_hz)
|
|
||||||
)
|
|
||||||
return raw_bin * self.decimation_factor
|
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
|
@property
|
||||||
def velocity_resolution_mps(self) -> float:
|
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
|
c = 299_792_458.0
|
||||||
wavelength = c / self.center_freq_hz
|
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
|
@property
|
||||||
def max_range_m(self) -> float:
|
def max_range_m(self) -> float:
|
||||||
"""Maximum unambiguous range in meters."""
|
"""Maximum unambiguous range in meters."""
|
||||||
return self.range_resolution_m * self.n_range_bins
|
return self.bin_spacing_m * self.n_range_bins
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_velocity_mps(self) -> float:
|
def max_velocity_mps(self) -> float:
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ def polar_to_geographic(
|
|||||||
|
|
||||||
def extract_targets_from_frame(
|
def extract_targets_from_frame(
|
||||||
frame,
|
frame,
|
||||||
range_resolution: float = 1.0,
|
bin_spacing: float = 1.0,
|
||||||
velocity_resolution: float = 1.0,
|
velocity_resolution: float = 1.0,
|
||||||
gps: GPSData | None = None,
|
gps: GPSData | None = None,
|
||||||
) -> list[RadarTarget]:
|
) -> list[RadarTarget]:
|
||||||
@@ -503,8 +503,8 @@ def extract_targets_from_frame(
|
|||||||
----------
|
----------
|
||||||
frame : RadarFrame
|
frame : RadarFrame
|
||||||
Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``.
|
Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``.
|
||||||
range_resolution : float
|
bin_spacing : float
|
||||||
Meters per range bin.
|
Meters per range bin (bin spacing, NOT bandwidth-limited resolution).
|
||||||
velocity_resolution : float
|
velocity_resolution : float
|
||||||
m/s per Doppler bin.
|
m/s per Doppler bin.
|
||||||
gps : GPSData | None
|
gps : GPSData | None
|
||||||
@@ -525,7 +525,7 @@ def extract_targets_from_frame(
|
|||||||
mag = float(frame.magnitude[rbin, dbin])
|
mag = float(frame.magnitude[rbin, dbin])
|
||||||
snr = 10.0 * math.log10(max(mag, 1.0)) if mag > 0 else 0.0
|
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
|
velocity_ms = float(dbin - doppler_center) * velocity_resolution
|
||||||
|
|
||||||
lat, lon, azimuth, elevation = 0.0, 0.0, 0.0, 0.0
|
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.
|
The FPGA already does: FFT, MTI, CFAR, DC notch.
|
||||||
Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
|
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).
|
and velocity_resolution (should be calibrated to actual waveform).
|
||||||
"""
|
"""
|
||||||
targets: list[RadarTarget] = []
|
targets: list[RadarTarget] = []
|
||||||
@@ -180,7 +180,7 @@ class RadarDataWorker(QThread):
|
|||||||
|
|
||||||
# Extract detections from FPGA CFAR flags
|
# Extract detections from FPGA CFAR flags
|
||||||
det_indices = np.argwhere(frame.detections > 0)
|
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
|
v_res = self._settings.velocity_resolution
|
||||||
|
|
||||||
for idx in det_indices:
|
for idx in det_indices:
|
||||||
@@ -368,7 +368,7 @@ class TargetSimulator(QObject):
|
|||||||
|
|
||||||
for t in self._targets:
|
for t in self._targets:
|
||||||
new_range = t.range - t.velocity * 0.5
|
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
|
continue # target exits coverage — drop it
|
||||||
|
|
||||||
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
|
||||||
@@ -559,7 +559,7 @@ class ReplayWorker(QThread):
|
|||||||
# Target extraction
|
# Target extraction
|
||||||
targets = self._extract_targets(
|
targets = self._extract_targets(
|
||||||
frame,
|
frame,
|
||||||
range_resolution=self._waveform.range_resolution_m,
|
bin_spacing=self._waveform.bin_spacing_m,
|
||||||
velocity_resolution=self._waveform.velocity_resolution_mps,
|
velocity_resolution=self._waveform.velocity_resolution_mps,
|
||||||
gps=self._gps,
|
gps=self._gps,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -793,3 +793,51 @@ def parse_stm32_gpio_init(filepath: Path | None = None) -> list[GpioPin]:
|
|||||||
))
|
))
|
||||||
|
|
||||||
return pins
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -202,6 +204,58 @@ class TestTier1OpcodeContract:
|
|||||||
f"but ground truth says '{expected_reg}'"
|
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:
|
class TestTier1BitWidths:
|
||||||
"""Verify register widths and opcode bit slices match ground truth."""
|
"""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:
|
class TestTier1PacketConstants:
|
||||||
"""Verify packet header/footer/size constants match across layers."""
|
"""Verify packet header/footer/size constants match across layers."""
|
||||||
|
|
||||||
@@ -724,8 +894,8 @@ class TestTier3CStub:
|
|||||||
"freq_max": 30.0e6,
|
"freq_max": 30.0e6,
|
||||||
"prf1": 1000.0,
|
"prf1": 1000.0,
|
||||||
"prf2": 2000.0,
|
"prf2": 2000.0,
|
||||||
"max_distance": 50000.0,
|
"max_distance": 1536.0,
|
||||||
"map_size": 50000.0,
|
"map_size": 1536.0,
|
||||||
}
|
}
|
||||||
pkt = self._build_settings_packet(values)
|
pkt = self._build_settings_packet(values)
|
||||||
result = self._run_stub(stub_binary, pkt)
|
result = self._run_stub(stub_binary, pkt)
|
||||||
@@ -784,11 +954,11 @@ class TestTier3CStub:
|
|||||||
def test_bad_markers_rejected(self, stub_binary):
|
def test_bad_markers_rejected(self, stub_binary):
|
||||||
"""Packet with wrong start/end markers must be rejected."""
|
"""Packet with wrong start/end markers must be rejected."""
|
||||||
values = {
|
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,
|
"chirp_duration_2": 0.5e-6, "chirps_per_position": 32,
|
||||||
"freq_min": 10.0e6, "freq_max": 30.0e6,
|
"freq_min": 10.0e6, "freq_max": 30.0e6,
|
||||||
"prf1": 1000.0, "prf2": 2000.0,
|
"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)
|
pkt = self._build_settings_packet(values)
|
||||||
|
|
||||||
@@ -826,3 +996,107 @@ class TestTier3CStub:
|
|||||||
assert result.get("parse_ok") == "true", (
|
assert result.get("parse_ok") == "true", (
|
||||||
f"Boundary values rejected: {result}"
|
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