fix: align all range/carrier/velocity values to PLFM hardware + FPGA bug fixes

- Correct carrier from 10.525/10 GHz to 10.5 GHz (verified ADF4382 config)
- Correct range-per-bin from 4.8/5.6/781.25 m to 24.0 m (matched-filter)
- Correct velocity resolution from 1.484 to 2.67 m/s/bin (PRI-based)
- Correct processing rate from 4 MSPS to 100 MSPS (post-DDC)
- Correct max range from 307/5000/50000 m to 1536 m (64 bins x 24 m)
- Add WaveformConfig.pri_s field (167 us PRI for velocity calculation)
- Fix short chirp chirp_complete deadlock (Bug A)
- Remove dead short_chirp ports, rename long_chirp to ref_chirp (Bug B)
- Fix stale latency comment 2159 -> 3187 cycles (Bug C)
- Create radar_params.vh as single source of truth for FPGA parameters
- Lower RadarSettings.cpp map_size validation bound from 1000 to 100
- Add PLFM hardware constants to golden_reference.py
- Update all GUI versions, tests, and cross-layer contracts

All 244 tests passing (167 Python + 21 MCU + 29 cross-layer + 27 FPGA)
This commit is contained in:
Jason
2026-04-15 10:38:59 +05:45
parent d8d30a6315
commit d259e5c106
26 changed files with 415 additions and 4826 deletions
+4 -1
View File
@@ -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,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
+200
View File
@@ -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
+11 -13
View File
@@ -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),
@@ -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
@@ -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;
+24 -46
View File
@@ -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;
+10 -18
View File
@@ -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...");
+10 -16
View File
@@ -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;
+5 -5
View File
@@ -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
+5 -5
View File
@@ -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"),
] ]
+5 -5
View File
@@ -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 = {}
+5 -5
View File
@@ -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')
] ]
+13 -11
View File
@@ -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
+2 -2
View File
@@ -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)
] ]
-338
View File
@@ -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()
+19 -17
View File
@@ -65,9 +65,9 @@ class TestRadarSettings(unittest.TestCase):
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,26 +425,27 @@ 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.""" """range_resolution_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.range_resolution_m, 23.98, 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 = range_resolution * n_range_bins."""
@@ -466,7 +467,8 @@ 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: range_per_bin = c/(2*fs)*dec — proportional to 1/fs
wc2 = WaveformConfig(sample_rate_hz=200e6) # double fs → halve range res
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2) self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2)
def test_zero_center_freq_velocity(self): def test_zero_center_freq_velocity(self):
@@ -925,18 +927,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, range_resolution=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)
+1 -1
View File
@@ -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
+24 -22
View File
@@ -108,12 +108,12 @@ class RadarSettings:
range_resolution and velocity_resolution should be calibrated to range_resolution 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_resolution: 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,42 +196,44 @@ 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 range_resolution_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 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 @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:
+1 -1
View File
@@ -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)))
@@ -724,8 +724,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 +784,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)