mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-10 23:41:18 +00:00
chirp-v2 PR-C: chirp_reference_rom replaces chirp_memory_loader_param
Drop the chirp-v1 1-bit use_long_chirp memory loader and its 6 .mem files;
introduce chirp_reference_rom — wave_sel-native, single 8192x16 BRAM array
per Q15 lane, 4-region init (SHORT, MEDIUM, LONG seg0/seg1) loaded from the
PR-B mem files. Same 1-clk read latency as the legacy module so the RX-B
autocorrelation alignment fix carries through unchanged.
Receiver-side wave_sel shim added in radar_receiver_final.v:
wire [1:0] wave_sel = use_long_chirp ? RP_WAVE_LONG : RP_WAVE_SHORT;
This is a 1-line transitional bridge while radar_mode_controller still
emits 1-bit use_long_chirp; PR-D deletes the shim and wires chirp_scheduler
straight through. MEDIUM is loaded into the ROM but unreachable through
the production path until PR-D.
BRAM cost: 8 RAMB18 (was 6 in chirp-v1). +2 BRAM is the cost of adding
MEDIUM to the waveform set; not avoidable.
Files added:
- chirp_reference_rom.v
Files removed:
- chirp_memory_loader_param.v
- long_chirp_seg{0,1}_{i,q}.mem (4 files)
- short_chirp_{i,q}.mem (2 files)
- tb/cosim/validate_mem_files.py (legacy file-set validator; replaced by
gen_chirp_mem.py's internal verify_phase_match)
- tb/cosim/analyze_short_chirp_mismatch.py (one-shot tool from the
chirp-v1 TX-I investigation; finding incorporated, references the
deleted short_chirp_*.mem files)
Files updated for module rename:
- radar_receiver_final.v — instance, comments, wave_sel shim
- radar_mode_controller.v — header comment
- matched_filter_processing_chain.v — header comment
- scripts/200t/build_200t.tcl — explicit RTL list
- run_regression.sh — 5 spots
- tb/tb_rxb_fullchain_latency.v — instance, wave_sel shim, mem filenames,
SHORT_LEN 50 → 100 (1 µs at 100 MHz)
- tb/tb_system_e2e.v — header comment
Verification:
- chirp_reference_rom standalone iverilog compile: clean
- Full receiver chain compile (21 RTL files): clean
- tb_rxb_fullchain_latency runs end-to-end with new ROM + new mem files
+ 100-sample SHORT chirp; autocorrelation peak at bin 0, peak |I|+|Q|
= 15115. Confirms 1-clk ROM read latency is preserved and the RX-B
direct-wire-with-1-FF alignment still holds.
- 50T build script (scripts/50t/build_50t.tcl) uses glob *.v — no edit
needed; it picks up the new file automatically.
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
`timescale 1ns / 1ps
|
||||
module chirp_memory_loader_param #(
|
||||
parameter LONG_I_FILE_SEG0 = "long_chirp_seg0_i.mem",
|
||||
parameter LONG_Q_FILE_SEG0 = "long_chirp_seg0_q.mem",
|
||||
parameter LONG_I_FILE_SEG1 = "long_chirp_seg1_i.mem",
|
||||
parameter LONG_Q_FILE_SEG1 = "long_chirp_seg1_q.mem",
|
||||
parameter SHORT_I_FILE = "short_chirp_i.mem",
|
||||
parameter SHORT_Q_FILE = "short_chirp_q.mem",
|
||||
parameter DEBUG = 1
|
||||
)(
|
||||
input wire clk,
|
||||
input wire reset_n,
|
||||
input wire [1:0] segment_select,
|
||||
input wire mem_request,
|
||||
input wire use_long_chirp,
|
||||
input wire [10:0] sample_addr,
|
||||
output reg [15:0] ref_i,
|
||||
output reg [15:0] ref_q,
|
||||
output reg mem_ready
|
||||
);
|
||||
|
||||
// Memory declarations — 2 long segments × 2048 = 4096 samples
|
||||
(* ram_style = "block" *) reg [15:0] long_chirp_i [0:4095];
|
||||
(* ram_style = "block" *) reg [15:0] long_chirp_q [0:4095];
|
||||
(* ram_style = "block" *) reg [15:0] short_chirp_i [0:2047];
|
||||
(* ram_style = "block" *) reg [15:0] short_chirp_q [0:2047];
|
||||
|
||||
// Initialize memory
|
||||
integer i;
|
||||
|
||||
initial begin
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG) begin
|
||||
$display("[MEM] Starting memory initialization for 2 long chirp segments");
|
||||
end
|
||||
`endif
|
||||
|
||||
// === LOAD LONG CHIRP — 2 SEGMENTS ===
|
||||
// Segment 0 (addresses 0-2047)
|
||||
$readmemh(LONG_I_FILE_SEG0, long_chirp_i, 0, 2047);
|
||||
$readmemh(LONG_Q_FILE_SEG0, long_chirp_q, 0, 2047);
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG) $display("[MEM] Loaded long chirp segment 0 (0-2047)");
|
||||
`endif
|
||||
|
||||
// Segment 1 (addresses 2048-4095)
|
||||
$readmemh(LONG_I_FILE_SEG1, long_chirp_i, 2048, 4095);
|
||||
$readmemh(LONG_Q_FILE_SEG1, long_chirp_q, 2048, 4095);
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG) $display("[MEM] Loaded long chirp segment 1 (2048-4095)");
|
||||
`endif
|
||||
|
||||
// === LOAD SHORT CHIRP ===
|
||||
// Load first 50 samples (0-49)
|
||||
$readmemh(SHORT_I_FILE, short_chirp_i, 0, 49);
|
||||
$readmemh(SHORT_Q_FILE, short_chirp_q, 0, 49);
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG) $display("[MEM] Loaded short chirp (0-49)");
|
||||
`endif
|
||||
|
||||
// Zero pad remaining samples (50-2047)
|
||||
for (i = 50; i < 2048; i = i + 1) begin
|
||||
short_chirp_i[i] = 16'h0000;
|
||||
short_chirp_q[i] = 16'h0000;
|
||||
end
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG) $display("[MEM] Zero-padded short chirp from 50-2047");
|
||||
|
||||
// === VERIFICATION ===
|
||||
if (DEBUG) begin
|
||||
$display("[MEM] Memory loading complete. Verification samples:");
|
||||
$display(" Long[0]: I=%h Q=%h", long_chirp_i[0], long_chirp_q[0]);
|
||||
$display(" Long[2047]: I=%h Q=%h", long_chirp_i[2047], long_chirp_q[2047]);
|
||||
$display(" Long[2048]: I=%h Q=%h", long_chirp_i[2048], long_chirp_q[2048]);
|
||||
$display(" Long[4095]: I=%h Q=%h", long_chirp_i[4095], long_chirp_q[4095]);
|
||||
$display(" Short[0]: I=%h Q=%h", short_chirp_i[0], short_chirp_q[0]);
|
||||
$display(" Short[49]: I=%h Q=%h", short_chirp_i[49], short_chirp_q[49]);
|
||||
$display(" Short[50]: I=%h Q=%h (zero-padded)", short_chirp_i[50], short_chirp_q[50]);
|
||||
end
|
||||
`endif
|
||||
end
|
||||
|
||||
// Memory access logic
|
||||
// long_addr: segment_select[0] selects segment (0 or 1), sample_addr[10:0] selects within
|
||||
wire [11:0] long_addr = {segment_select[0], sample_addr};
|
||||
|
||||
// ---- BRAM read block (sync-only, sync reset) ----
|
||||
// REQP-1839/1840 fix: BRAM output registers cannot have async resets.
|
||||
// We use a synchronous reset instead, which Vivado maps to the BRAM
|
||||
// RSTREGB port (supported by 7-series BRAM primitives).
|
||||
always @(posedge clk) begin
|
||||
if (!reset_n) begin
|
||||
ref_i <= 16'd0;
|
||||
ref_q <= 16'd0;
|
||||
end else if (mem_request) begin
|
||||
if (use_long_chirp) begin
|
||||
ref_i <= long_chirp_i[long_addr];
|
||||
ref_q <= long_chirp_q[long_addr];
|
||||
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG && $time < 100) begin
|
||||
$display("[MEM @%0t] Long chirp: seg=%b, addr=%d, I=%h, Q=%h",
|
||||
$time, segment_select, long_addr,
|
||||
long_chirp_i[long_addr], long_chirp_q[long_addr]);
|
||||
end
|
||||
`endif
|
||||
end else begin
|
||||
// Short chirp (0-2047)
|
||||
ref_i <= short_chirp_i[sample_addr];
|
||||
ref_q <= short_chirp_q[sample_addr];
|
||||
|
||||
`ifdef SIMULATION
|
||||
if (DEBUG && $time < 100) begin
|
||||
$display("[MEM @%0t] Short chirp: addr=%d, I=%h, Q=%h",
|
||||
$time, sample_addr, short_chirp_i[sample_addr], short_chirp_q[sample_addr]);
|
||||
end
|
||||
`endif
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ---- Control block (async reset for mem_ready only) ----
|
||||
always @(posedge clk or negedge reset_n) begin
|
||||
if (!reset_n) begin
|
||||
mem_ready <= 1'b0;
|
||||
end else begin
|
||||
mem_ready <= mem_request;
|
||||
end
|
||||
end
|
||||
|
||||
endmodule
|
||||
@@ -0,0 +1,121 @@
|
||||
`timescale 1ns / 1ps
|
||||
|
||||
`include "radar_params.vh"
|
||||
|
||||
// ============================================================================
|
||||
// chirp_reference_rom.v — 3-waveform matched-filter reference ROM (RX side)
|
||||
// ============================================================================
|
||||
// Replaces the chirp-v1 chirp_memory_loader_param.v (1-bit `use_long_chirp`,
|
||||
// 6 .mem files, separate BRAMs for long/short).
|
||||
//
|
||||
// Carries one of {SHORT, MEDIUM, LONG} via wave_sel[1:0] — see RP_WAVE_*
|
||||
// in radar_params.vh. The .mem files (PR-B) are uniformly 2048 entries each
|
||||
// in Q15 I/Q hex; LONG occupies two 2048 segments; SHORT and MEDIUM each
|
||||
// occupy a single 2048 segment with internal zero-pad past the chirp end.
|
||||
//
|
||||
// BRAM layout (single 8192x16 array per lane — Vivado infers 4 RAMB18/lane,
|
||||
// 8 RAMB18 total. Same cost as chirp-v1 dual-array layout because LONG
|
||||
// already needed 4 RAMB18; folding SHORT and MEDIUM into the same address
|
||||
// space costs the 4 BRAMs we'd add for medium anyway):
|
||||
//
|
||||
// addr[12:11] region source files
|
||||
// --------- --------------------- --------------------------------
|
||||
// 2'b00 SHORT ([0..2047]) rx_short_{i,q}.mem
|
||||
// 2'b01 MEDIUM ([0..2047]) rx_medium_{i,q}.mem
|
||||
// 2'b10 LONG seg0 ([0..2047]) rx_long_seg0_{i,q}.mem
|
||||
// 2'b11 LONG seg1 ([0..2047]) rx_long_seg1_{i,q}.mem
|
||||
//
|
||||
// Read addressing:
|
||||
// case (wave_sel)
|
||||
// RP_WAVE_SHORT: full_addr = {2'b00, sample_addr}
|
||||
// RP_WAVE_MEDIUM: full_addr = {2'b01, sample_addr}
|
||||
// RP_WAVE_LONG: full_addr = {1'b1, segment_select[0], sample_addr}
|
||||
// default: (RP_WAVE_RESERVED) zero-output, mem_ready still pulses
|
||||
// endcase
|
||||
//
|
||||
// Output semantics — drop-in compatible with chirp_memory_loader_param:
|
||||
// - Synchronous read: ref_i / ref_q valid 1 clk after mem_request.
|
||||
// - mem_ready pulses with ref data (1 clk after mem_request).
|
||||
// - SAME 1-cycle latency as the legacy module (preserves RX-B autocorrelation
|
||||
// peak alignment validated by tb_rxb_fullchain_latency).
|
||||
//
|
||||
// REQP-1839/1840 compliance (BRAM output registers cannot have async resets):
|
||||
// - The BRAM read block uses a SYNCHRONOUS reset, which Vivado maps to the
|
||||
// RAMB18 RSTREGB port. mem_ready (a non-BRAM control register) keeps the
|
||||
// async reset for clean post-reset behavior. Same split as the legacy
|
||||
// chirp_memory_loader_param.v.
|
||||
// ============================================================================
|
||||
module chirp_reference_rom (
|
||||
input wire clk,
|
||||
input wire reset_n,
|
||||
input wire [1:0] wave_sel, // RP_WAVE_{SHORT,MEDIUM,LONG}
|
||||
input wire [1:0] segment_select, // [0]=LONG seg index; ignored for SHORT/MEDIUM
|
||||
input wire mem_request,
|
||||
input wire [10:0] sample_addr, // 0..2047 within the active waveform/segment
|
||||
output reg [15:0] ref_i,
|
||||
output reg [15:0] ref_q,
|
||||
output reg mem_ready
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// BRAM arrays (one per Q15 lane). Vivado infers RAMB18 with sync read.
|
||||
// -----------------------------------------------------------------------
|
||||
(* ram_style = "block" *) reg [15:0] mem_i [0:8191];
|
||||
(* ram_style = "block" *) reg [15:0] mem_q [0:8191];
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Initialization — load 4 distinct .mem files into 4 contiguous regions
|
||||
// of the unified BRAM. $readmemh range form lets us target each 2048-cell
|
||||
// segment independently. Vivado honors these for RAMB18 init contents.
|
||||
// -----------------------------------------------------------------------
|
||||
initial begin
|
||||
$readmemh("rx_short_i.mem", mem_i, 0, 2047);
|
||||
$readmemh("rx_short_q.mem", mem_q, 0, 2047);
|
||||
$readmemh("rx_medium_i.mem", mem_i, 2048, 4095);
|
||||
$readmemh("rx_medium_q.mem", mem_q, 2048, 4095);
|
||||
$readmemh("rx_long_seg0_i.mem", mem_i, 4096, 6143);
|
||||
$readmemh("rx_long_seg0_q.mem", mem_q, 4096, 6143);
|
||||
$readmemh("rx_long_seg1_i.mem", mem_i, 6144, 8191);
|
||||
$readmemh("rx_long_seg1_q.mem", mem_q, 6144, 8191);
|
||||
end
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Address mux — combinational. Encodes the region select into addr[12:11]
|
||||
// and passes sample_addr through addr[10:0].
|
||||
// -----------------------------------------------------------------------
|
||||
reg [12:0] full_addr;
|
||||
always @(*) begin
|
||||
case (wave_sel)
|
||||
`RP_WAVE_SHORT: full_addr = {2'b00, sample_addr};
|
||||
`RP_WAVE_MEDIUM: full_addr = {2'b01, sample_addr};
|
||||
`RP_WAVE_LONG: full_addr = {1'b1, segment_select[0], sample_addr};
|
||||
default: full_addr = 13'd0; // RP_WAVE_RESERVED — read-zero region
|
||||
endcase
|
||||
end
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// BRAM read block — sync-only, sync reset (REQP-1839/1840). Single stage:
|
||||
// ref_i / ref_q valid 1 clk after mem_request, matching legacy timing.
|
||||
// -----------------------------------------------------------------------
|
||||
always @(posedge clk) begin
|
||||
if (!reset_n) begin
|
||||
ref_i <= 16'd0;
|
||||
ref_q <= 16'd0;
|
||||
end else if (mem_request) begin
|
||||
ref_i <= mem_i[full_addr];
|
||||
ref_q <= mem_q[full_addr];
|
||||
end
|
||||
end
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Control register — async-resettable. mem_ready follows mem_request by
|
||||
// 1 clk to match the BRAM read latency.
|
||||
// -----------------------------------------------------------------------
|
||||
always @(posedge clk or negedge reset_n) begin
|
||||
if (!reset_n)
|
||||
mem_ready <= 1'b0;
|
||||
else
|
||||
mem_ready <= mem_request;
|
||||
end
|
||||
|
||||
endmodule
|
||||
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
@@ -63,8 +63,8 @@ module matched_filter_processing_chain (
|
||||
// inside the chain. multi_segment passed it through to nothing.
|
||||
|
||||
// Reference chirp (time-domain, latency-aligned by upstream buffer)
|
||||
// Upstream chirp_memory_loader_param selects long/short reference
|
||||
// via use_long_chirp — this single pair carries whichever is active.
|
||||
// Upstream chirp_reference_rom selects SHORT/MEDIUM/LONG reference
|
||||
// via wave_sel[1:0] — this single pair carries whichever is active.
|
||||
input wire [15:0] ref_chirp_real,
|
||||
input wire [15:0] ref_chirp_imag,
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
* - mc_new_azimuth : toggle signal indicating azimuth step
|
||||
*
|
||||
* These signals are consumed by matched_filter_multi_segment and
|
||||
* chirp_memory_loader_param in the receiver path.
|
||||
* chirp_reference_rom in the receiver path. (chirp-v2 PR-D will replace
|
||||
* this entire module with chirp_scheduler.)
|
||||
*
|
||||
* The controller mirrors the transmitter's chirp sequence defined in
|
||||
* plfm_chirp_controller_enhanced:
|
||||
|
||||
@@ -148,10 +148,16 @@ wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter
|
||||
wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude
|
||||
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
|
||||
|
||||
// Reference signal for the processing chain (carries long OR short ref
|
||||
// depending on use_long_chirp — selected by chirp_memory_loader_param)
|
||||
// Reference signal for the processing chain (carries SHORT/MEDIUM/LONG ref
|
||||
// depending on wave_sel — selected by chirp_reference_rom).
|
||||
wire [15:0] ref_chirp_real, ref_chirp_imag;
|
||||
|
||||
// chirp-v2 PR-C: wave_sel shim. radar_mode_controller still produces a
|
||||
// 1-bit use_long_chirp output (replaced in PR-D by chirp_scheduler whose
|
||||
// native output is wave_sel[1:0]). MEDIUM is unreachable through the
|
||||
// 1-bit path; the rom serves SHORT or LONG only until PR-D lands.
|
||||
wire [1:0] wave_sel = use_long_chirp ? `RP_WAVE_LONG : `RP_WAVE_SHORT;
|
||||
|
||||
// ========== DOPPLER PROCESSING SIGNALS ==========
|
||||
wire [31:0] range_data_32bit;
|
||||
wire range_data_valid;
|
||||
@@ -390,16 +396,16 @@ rx_gain_control gain_ctrl (
|
||||
.current_gain(gc_current_gain)
|
||||
);
|
||||
|
||||
// 3. Dual Chirp Memory Loader
|
||||
// 3. Chirp reference ROM (chirp-v2 PR-C)
|
||||
wire [10:0] sample_addr_from_chain;
|
||||
|
||||
chirp_memory_loader_param chirp_mem (
|
||||
chirp_reference_rom chirp_rom (
|
||||
.clk(clk),
|
||||
.reset_n(reset_n),
|
||||
.wave_sel(wave_sel),
|
||||
.segment_select(segment_request),
|
||||
.mem_request(mem_request),
|
||||
.use_long_chirp(use_long_chirp),
|
||||
.sample_addr(sample_addr_from_chain),
|
||||
.sample_addr(sample_addr_from_chain),
|
||||
.ref_i(ref_i),
|
||||
.ref_q(ref_q),
|
||||
.mem_ready(mem_ready)
|
||||
@@ -414,7 +420,7 @@ chirp_memory_loader_param chirp_mem (
|
||||
//
|
||||
// Why the 1-FF stage: multi_segment ST_PROCESSING latches `adc_data` through
|
||||
// one register stage (`fft_input_i <= buf_rdata_i`) before it reaches the
|
||||
// chain. The ref path from chirp_memory_loader is combinational into the
|
||||
// chain. The ref path from chirp_reference_rom is combinational into the
|
||||
// chain. Without compensation, ref leads sig by 1 cycle → autocorrelation
|
||||
// peak at bin 1 instead of bin 0 (verified in tb/tb_rxb_fullchain_latency.v
|
||||
// against fft_engine.v synthesis path: peak/mean ratio ~80× confirms clean
|
||||
|
||||
@@ -62,7 +62,7 @@ PROD_RTL=(
|
||||
cdc_async_fifo.v
|
||||
fir_lowpass.v
|
||||
ddc_input_interface.v
|
||||
chirp_memory_loader_param.v
|
||||
chirp_reference_rom.v
|
||||
matched_filter_multi_segment.v
|
||||
matched_filter_processing_chain.v
|
||||
range_bin_decimator.v
|
||||
@@ -101,7 +101,7 @@ RECEIVER_RTL=(
|
||||
tb/ad9484_interface_400m_stub.v
|
||||
ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v
|
||||
cdc_modules.v cdc_async_fifo.v fir_lowpass.v ddc_input_interface.v
|
||||
chirp_memory_loader_param.v
|
||||
chirp_reference_rom.v
|
||||
matched_filter_multi_segment.v matched_filter_processing_chain.v
|
||||
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v
|
||||
xfft_2048.v fft_engine_axi_bridge.v
|
||||
@@ -285,7 +285,7 @@ run_mf_cosim() {
|
||||
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 xfft_2048.v fft_engine_axi_bridge.v frequency_matched_filter.v chirp_memory_loader_param.v"
|
||||
cmd="$cmd -o $vvp tb/tb_mf_cosim.v matched_filter_processing_chain.v fft_engine.v xfft_2048.v fft_engine_axi_bridge.v frequency_matched_filter.v chirp_reference_rom.v"
|
||||
|
||||
if ! eval "$cmd" 2>/tmp/iverilog_err_$$; then
|
||||
echo -e "${RED}COMPILE FAIL${NC}"
|
||||
@@ -677,7 +677,7 @@ run_test --timeout=600 "Matched Filter Chain" \
|
||||
tb/tb_mf_reg.vvp \
|
||||
tb/tb_matched_filter_processing_chain.v matched_filter_processing_chain.v \
|
||||
fft_engine.v xfft_2048.v fft_engine_axi_bridge.v \
|
||||
chirp_memory_loader_param.v frequency_matched_filter.v
|
||||
chirp_reference_rom.v frequency_matched_filter.v
|
||||
|
||||
# RX-B regression coverage: chain pipeline depth + full-chain
|
||||
# autocorrelation peak position. Both run the production fft_engine
|
||||
@@ -693,7 +693,7 @@ run_test --timeout=600 "RX-B Full-Chain Autocorrelation (tb_rxb_fullchain_latenc
|
||||
tb/tb_rxb_fullchain_latency.v matched_filter_multi_segment.v \
|
||||
matched_filter_processing_chain.v fft_engine.v xfft_2048.v \
|
||||
fft_engine_axi_bridge.v frequency_matched_filter.v \
|
||||
chirp_memory_loader_param.v
|
||||
chirp_reference_rom.v
|
||||
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ set rtl_files [list \
|
||||
"${rtl_dir}/adc_clk_mmcm.v" \
|
||||
"${rtl_dir}/ad9484_interface_400m.v" \
|
||||
"${rtl_dir}/cdc_modules.v" \
|
||||
"${rtl_dir}/chirp_memory_loader_param.v" \
|
||||
"${rtl_dir}/chirp_reference_rom.v" \
|
||||
"${rtl_dir}/cic_decimator_4x_enhanced.v" \
|
||||
"${rtl_dir}/dac_interface_single.v" \
|
||||
"${rtl_dir}/ddc_400m.v" \
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
7332
|
||||
5c56
|
||||
1e0c
|
||||
d044
|
||||
9729
|
||||
9271
|
||||
c9c7
|
||||
2238
|
||||
679c
|
||||
6a91
|
||||
2399
|
||||
c10f
|
||||
8d21
|
||||
b576
|
||||
1e0c
|
||||
6d8f
|
||||
57d5
|
||||
ebd7
|
||||
92e6
|
||||
ad06
|
||||
2399
|
||||
7276
|
||||
38c3
|
||||
b7b1
|
||||
92e6
|
||||
0000
|
||||
6dff
|
||||
3ef1
|
||||
b234
|
||||
9bc2
|
||||
2399
|
||||
7219
|
||||
0173
|
||||
8de7
|
||||
e4c2
|
||||
6d8f
|
||||
290f
|
||||
956f
|
||||
d440
|
||||
6ba2
|
||||
2399
|
||||
9012
|
||||
f021
|
||||
7330
|
||||
f021
|
||||
9271
|
||||
38c3
|
||||
54f5
|
||||
9f85
|
||||
ddc8
|
||||
@@ -1,50 +0,0 @@
|
||||
0000
|
||||
44e0
|
||||
6f35
|
||||
68d7
|
||||
2fbc
|
||||
dc67
|
||||
9a5d
|
||||
9201
|
||||
cda5
|
||||
2bc0
|
||||
6d8f
|
||||
607b
|
||||
08ad
|
||||
a82b
|
||||
90cb
|
||||
dc67
|
||||
4a8a
|
||||
716b
|
||||
24f9
|
||||
b018
|
||||
9271
|
||||
f300
|
||||
643e
|
||||
59ad
|
||||
db07
|
||||
8cce
|
||||
ddc8
|
||||
607b
|
||||
54f5
|
||||
c73d
|
||||
9271
|
||||
0fdf
|
||||
7330
|
||||
0fdf
|
||||
9012
|
||||
dc67
|
||||
6ba2
|
||||
2bc0
|
||||
956f
|
||||
d6f1
|
||||
6d8f
|
||||
1b3e
|
||||
8de7
|
||||
fe8d
|
||||
7219
|
||||
dc67
|
||||
9bc2
|
||||
4dcc
|
||||
3ef1
|
||||
9201
|
||||
@@ -1,188 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# ruff: noqa: T201
|
||||
"""
|
||||
analyze_short_chirp_mismatch.py — quantify TX-I matched-filter mismatch loss.
|
||||
|
||||
Background
|
||||
----------
|
||||
TX path (`plfm_chirp_controller.v:74,118-127`):
|
||||
60-sample inline LUT, 8-bit unsigned offset binary (DAC center = 128),
|
||||
played at fs_tx = 120 MHz over 0.5 us. Real-valued passband chirp.
|
||||
Module comment claims "30 MHz to 10 MHz" downchirp.
|
||||
|
||||
RX matched-filter reference (`gen_chirp_mem.py:81-101` -> `short_chirp_{i,q}.mem`):
|
||||
50-sample complex baseband, Q15, fs_rx = 100 MHz over 0.5 us.
|
||||
Generated as a 0 -> +20 MHz baseband upchirp:
|
||||
phi(t) = pi * (BW/T) * t^2, BW = 20 MHz, T = 0.5 us
|
||||
I(n) = cos(phi), Q(n) = sin(phi), scaled by 0.9*Q15
|
||||
|
||||
These are claimed by the ledger to be ~2-3 dB mismatched. This script
|
||||
derives the implied baseband chirp from the TX LUT (modeling the IF chain
|
||||
and DDC by NCO at 120 MHz, decimation 4x to 100 MHz), then computes the
|
||||
true matched-filter peak power lost to template mismatch by:
|
||||
|
||||
1. Loading the TX LUT, computing the analytic signal (Hilbert),
|
||||
verifying instantaneous-frequency trajectory + claimed bandwidth.
|
||||
2. Modeling the DDC: mix by 120 MHz NCO at 400 MHz ADC sample rate,
|
||||
low-pass + decimate 4x to recover 100 MHz baseband. Since the TX
|
||||
LUT is only at 120 MHz, we upsample 120->400 first via zero-stuff +
|
||||
filter (the radar's analog chain does this naturally).
|
||||
3. Producing the implied 50-sample Q15 baseband reference.
|
||||
4. Computing the ambiguity peak between
|
||||
a) implied-from-TX reference cross-correlated with itself
|
||||
b) implied-from-TX reference cross-correlated with the existing
|
||||
short_chirp_{i,q}.mem
|
||||
The dB ratio of (b) peak / (a) peak is the mismatch loss.
|
||||
|
||||
Output: report only. Does not modify any .mem files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import hilbert, resample_poly
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
RTL_DIR = os.path.join(THIS_DIR, "..", "..")
|
||||
|
||||
FS_TX = 120e6 # DAC sample rate
|
||||
FS_RX = 100e6 # post-DDC processing rate
|
||||
T_CHIRP = 0.5e-6
|
||||
N_TX = 60 # samples in TX LUT
|
||||
N_RX = 50 # samples in RX reference
|
||||
|
||||
# --- Parse TX LUT inline-coded in plfm_chirp_controller.v ----------------
|
||||
def read_tx_lut() -> np.ndarray:
|
||||
path = os.path.join(RTL_DIR, "plfm_chirp_controller.v")
|
||||
with open(path) as f:
|
||||
src = f.read()
|
||||
# Capture every "short_chirp_lut[<idx>] = 8'd<value>;"
|
||||
pairs = re.findall(r"short_chirp_lut\[\s*(\d+)\s*\]\s*=\s*8'd\s*(\d+)\s*;", src)
|
||||
if len(pairs) != N_TX:
|
||||
sys.exit(f"expected {N_TX} TX LUT entries, got {len(pairs)}")
|
||||
arr = np.zeros(N_TX, dtype=np.int32)
|
||||
for idx_s, val_s in pairs:
|
||||
arr[int(idx_s)] = int(val_s)
|
||||
# Convert from 8-bit unsigned offset binary (DAC center = 128) to signed.
|
||||
return arr - 128 # int range roughly [-128, +127]
|
||||
|
||||
|
||||
# --- Parse existing RX reference .mem files -------------------------------
|
||||
def read_q15_mem(name: str) -> np.ndarray:
|
||||
path = os.path.join(RTL_DIR, name)
|
||||
out = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
v = int(line, 16)
|
||||
if v >= 0x8000:
|
||||
v -= 0x10000
|
||||
out.append(v)
|
||||
return np.array(out, dtype=np.int32)
|
||||
|
||||
|
||||
# --- Derive implied 50-sample baseband reference from the TX LUT ---------
|
||||
def derive_baseband_from_tx(tx: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
1) Treat tx as fs=120 MHz real samples.
|
||||
2) Compute analytic signal (Hilbert) -> single-sided spectrum copy.
|
||||
3) Find the chirp's center frequency from the analytic signal's
|
||||
mean instantaneous frequency, then mix it down to baseband by
|
||||
multiplying by exp(-j*2*pi*fc*t).
|
||||
4) Resample 120 -> 100 MHz to get exactly N_RX = 50 samples
|
||||
(matching the existing reference grid).
|
||||
5) Return as complex float64.
|
||||
"""
|
||||
x = tx.astype(np.float64)
|
||||
z = hilbert(x) # complex analytic, fs=120 MHz
|
||||
n = np.arange(len(z))
|
||||
# Instantaneous phase + frequency
|
||||
inst_phase = np.unwrap(np.angle(z))
|
||||
inst_freq = np.diff(inst_phase) * FS_TX / (2 * np.pi)
|
||||
fc = float(np.mean(inst_freq)) # rough center frequency in Hz
|
||||
# Mix to baseband
|
||||
bb_120 = z * np.exp(-1j * 2 * np.pi * fc * n / FS_TX)
|
||||
# Resample 120 MHz -> 100 MHz: use up=5, down=6 (5/6 = 100/120).
|
||||
bb_100 = resample_poly(bb_120, up=5, down=6)
|
||||
# Trim/pad to exactly N_RX samples
|
||||
if len(bb_100) >= N_RX:
|
||||
bb_100 = bb_100[:N_RX]
|
||||
else:
|
||||
bb_100 = np.concatenate([bb_100, np.zeros(N_RX - len(bb_100), dtype=complex)])
|
||||
return bb_100, fc, inst_freq
|
||||
|
||||
|
||||
# --- Mismatch loss in dB --------------------------------------------------
|
||||
def peak_corr_db(ref: np.ndarray, sig: np.ndarray) -> float:
|
||||
"""Peak |ref dot conj(sig_shifted)| over all integer shifts, normalised."""
|
||||
# Both arrays equal length; cross-correlate.
|
||||
c = np.correlate(sig, ref, mode="full")
|
||||
return 20 * np.log10(np.max(np.abs(c)) + 1e-30)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tx = read_tx_lut()
|
||||
rx_i = read_q15_mem("short_chirp_i.mem")
|
||||
rx_q = read_q15_mem("short_chirp_q.mem")
|
||||
if len(rx_i) != N_RX or len(rx_q) != N_RX:
|
||||
sys.exit(f"RX .mem files expected {N_RX} samples, got I={len(rx_i)} Q={len(rx_q)}")
|
||||
rx = (rx_i + 1j * rx_q).astype(complex)
|
||||
|
||||
# Derive implied baseband reference from TX LUT
|
||||
bb, fc, inst_freq = derive_baseband_from_tx(tx)
|
||||
|
||||
# Bandwidth check from instantaneous frequency
|
||||
f_lo, f_hi = float(np.min(inst_freq)), float(np.max(inst_freq))
|
||||
bw = f_hi - f_lo
|
||||
|
||||
print("=== TX LUT analysis ===")
|
||||
print(f" samples: {N_TX} @ {FS_TX/1e6:.0f} MHz, duration {N_TX/FS_TX*1e6:.3f} us")
|
||||
print(f" inst-freq range: {f_lo/1e6:+7.2f} MHz .. {f_hi/1e6:+7.2f} MHz")
|
||||
print(f" bandwidth swept: {bw/1e6:6.2f} MHz")
|
||||
print(f" center frequency: {fc/1e6:+7.2f} MHz (inferred from mean inst freq)")
|
||||
sweep_dir = "UP" if inst_freq[-1] > inst_freq[0] else "DOWN"
|
||||
print(f" sweep direction: {sweep_dir} (start={inst_freq[0]/1e6:+.2f} MHz, "
|
||||
f"end={inst_freq[-1]/1e6:+.2f} MHz)")
|
||||
|
||||
print()
|
||||
print("=== Existing RX reference (short_chirp_{i,q}.mem) ===")
|
||||
rx_phase = np.unwrap(np.angle(rx + 1e-30))
|
||||
rx_inst_freq = np.diff(rx_phase) * FS_RX / (2 * np.pi)
|
||||
rx_lo, rx_hi = float(np.min(rx_inst_freq)), float(np.max(rx_inst_freq))
|
||||
print(f" samples: {N_RX} @ {FS_RX/1e6:.0f} MHz")
|
||||
print(f" inst-freq range: {rx_lo/1e6:+7.2f} MHz .. {rx_hi/1e6:+7.2f} MHz")
|
||||
print(f" bandwidth swept: {(rx_hi - rx_lo)/1e6:6.2f} MHz")
|
||||
rx_sweep = "UP" if rx_inst_freq[-1] > rx_inst_freq[0] else "DOWN"
|
||||
print(f" sweep direction: {rx_sweep}")
|
||||
|
||||
print()
|
||||
print("=== Mismatch loss (matched-filter peak: implied-vs-existing) ===")
|
||||
# Normalise both to unit energy so the only thing the ratio reflects is shape.
|
||||
bb_n = bb / np.sqrt(np.sum(np.abs(bb) ** 2) + 1e-30)
|
||||
rx_n = rx / np.sqrt(np.sum(np.abs(rx) ** 2) + 1e-30)
|
||||
auto_db = peak_corr_db(bb_n, bb_n)
|
||||
cross_db = peak_corr_db(bb_n, rx_n)
|
||||
loss_db = auto_db - cross_db
|
||||
print(f" auto-correlation peak (implied vs implied): {auto_db:+6.2f} dB")
|
||||
print(f" cross-corr peak (implied vs existing RX): {cross_db:+6.2f} dB")
|
||||
print(f" MISMATCH LOSS (matched filter): {loss_db:6.2f} dB")
|
||||
print()
|
||||
|
||||
# Decision aid
|
||||
if loss_db < 0.5:
|
||||
verdict = "AGREEMENT — TX LUT and RX reference are consistent within 0.5 dB."
|
||||
elif loss_db < 2.0:
|
||||
verdict = ("MILD MISMATCH — within ledger's 2-3 dB note; refresh "
|
||||
"recommended but not blocking.")
|
||||
else:
|
||||
verdict = "SIGNIFICANT MISMATCH — RX reference should be regenerated from TX LUT."
|
||||
print(f"VERDICT: {verdict}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,492 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
validate_mem_files.py — Validate all .mem files against AERIS-10 radar parameters.
|
||||
|
||||
Checks:
|
||||
1. Structural: line counts, hex format, value ranges for all 12 .mem files
|
||||
2. FFT twiddle files: bit-exact match against cos(2*pi*k/N) in Q15
|
||||
3. Long chirp .mem files: reverse-engineer parameters, check for chirp structure
|
||||
4. Short chirp .mem files: check length, value range, spectral content
|
||||
|
||||
Usage:
|
||||
python3 validate_mem_files.py
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ============================================================================
|
||||
# AERIS-10 System Parameters (from radar_scene.py)
|
||||
# ============================================================================
|
||||
F_CARRIER = 10.5e9 # 10.5 GHz carrier
|
||||
C_LIGHT = 3.0e8
|
||||
F_IF = 120e6 # IF frequency
|
||||
CHIRP_BW = 20e6 # 20 MHz sweep
|
||||
FS_ADC = 400e6 # ADC sample rate
|
||||
FS_SYS = 100e6 # System clock (100 MHz, after CIC 4x)
|
||||
T_LONG_CHIRP = 30e-6 # 30 us long chirp
|
||||
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
|
||||
CIC_DECIMATION = 4
|
||||
FFT_SIZE = 1024
|
||||
DOPPLER_FFT_SIZE = 16
|
||||
LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000 at 100 MHz
|
||||
|
||||
# Overlap-save parameters
|
||||
OVERLAP_SAMPLES = 128
|
||||
SEGMENT_ADVANCE = FFT_SIZE - OVERLAP_SAMPLES # 896
|
||||
LONG_SEGMENTS = 4
|
||||
|
||||
MEM_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
|
||||
|
||||
pass_count = 0
|
||||
fail_count = 0
|
||||
warn_count = 0
|
||||
|
||||
def check(condition, _label):
|
||||
global pass_count, fail_count
|
||||
if condition:
|
||||
pass_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
def warn(_label):
|
||||
global warn_count
|
||||
warn_count += 1
|
||||
|
||||
def read_mem_hex(filename):
|
||||
"""Read a .mem file, return list of integer values (16-bit signed)."""
|
||||
path = os.path.join(MEM_DIR, filename)
|
||||
values = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
continue
|
||||
val = int(line, 16)
|
||||
# Interpret as 16-bit signed
|
||||
if val >= 0x8000:
|
||||
val -= 0x10000
|
||||
values.append(val)
|
||||
return values
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 1: Structural validation of all .mem files
|
||||
# ============================================================================
|
||||
def test_structural():
|
||||
|
||||
expected = {
|
||||
# FFT twiddle files (quarter-wave cosine ROMs)
|
||||
'fft_twiddle_1024.mem': {'lines': 256, 'desc': '1024-pt FFT quarter-wave cos ROM'},
|
||||
'fft_twiddle_16.mem': {'lines': 4, 'desc': '16-pt FFT quarter-wave cos ROM'},
|
||||
# Long chirp segments (4 segments x 1024 samples each)
|
||||
'long_chirp_seg0_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 I'},
|
||||
'long_chirp_seg0_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 Q'},
|
||||
'long_chirp_seg1_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 I'},
|
||||
'long_chirp_seg1_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 Q'},
|
||||
'long_chirp_seg2_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 I'},
|
||||
'long_chirp_seg2_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 Q'},
|
||||
'long_chirp_seg3_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 I'},
|
||||
'long_chirp_seg3_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 Q'},
|
||||
# Short chirp (50 samples)
|
||||
'short_chirp_i.mem': {'lines': 50, 'desc': 'Short chirp I'},
|
||||
'short_chirp_q.mem': {'lines': 50, 'desc': 'Short chirp Q'},
|
||||
}
|
||||
|
||||
for fname, info in expected.items():
|
||||
path = os.path.join(MEM_DIR, fname)
|
||||
exists = os.path.isfile(path)
|
||||
check(exists, f"{fname} exists")
|
||||
if not exists:
|
||||
continue
|
||||
|
||||
vals = read_mem_hex(fname)
|
||||
check(len(vals) == info['lines'],
|
||||
f"{fname}: {len(vals)} data lines (expected {info['lines']})")
|
||||
|
||||
# Check all values are in 16-bit signed range
|
||||
in_range = all(-32768 <= v <= 32767 for v in vals)
|
||||
check(in_range, f"{fname}: all values in [-32768, 32767]")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 2: FFT Twiddle Factor Validation
|
||||
# ============================================================================
|
||||
def test_twiddle_1024():
|
||||
vals = read_mem_hex('fft_twiddle_1024.mem')
|
||||
|
||||
max_err = 0
|
||||
err_details = []
|
||||
for k in range(min(256, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 1024.0
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
expected = max(-32768, min(32767, expected))
|
||||
actual = vals[k]
|
||||
err = abs(actual - expected)
|
||||
if err > max_err:
|
||||
max_err = err
|
||||
if err > 1:
|
||||
err_details.append((k, actual, expected, err))
|
||||
|
||||
check(max_err <= 1,
|
||||
f"fft_twiddle_1024.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
|
||||
if err_details:
|
||||
for _, _act, _exp, _e in err_details[:5]:
|
||||
pass
|
||||
|
||||
|
||||
def test_twiddle_16():
|
||||
vals = read_mem_hex('fft_twiddle_16.mem')
|
||||
|
||||
max_err = 0
|
||||
for k in range(min(4, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 16.0
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
expected = max(-32768, min(32767, expected))
|
||||
actual = vals[k]
|
||||
err = abs(actual - expected)
|
||||
if err > max_err:
|
||||
max_err = err
|
||||
|
||||
check(max_err <= 1,
|
||||
f"fft_twiddle_16.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
|
||||
|
||||
# Print all 4 entries for reference
|
||||
for k in range(min(4, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 16.0
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 3: Long Chirp .mem File Analysis
|
||||
# ============================================================================
|
||||
def test_long_chirp():
|
||||
|
||||
# Load all 4 segments
|
||||
all_i = []
|
||||
all_q = []
|
||||
for seg in range(4):
|
||||
seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem')
|
||||
seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem')
|
||||
all_i.extend(seg_i)
|
||||
all_q.extend(seg_q)
|
||||
|
||||
total_samples = len(all_i)
|
||||
check(total_samples == 4096,
|
||||
f"Total long chirp samples: {total_samples} (expected 4096 = 4 segs x 1024)")
|
||||
|
||||
# Compute magnitude envelope
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q, strict=False)]
|
||||
max_mag = max(magnitudes)
|
||||
min(magnitudes)
|
||||
sum(magnitudes) / len(magnitudes)
|
||||
|
||||
|
||||
# Check if this looks like it came from generate_reference_chirp_q15
|
||||
# That function uses 32767 * 0.9 scaling => max magnitude ~29490
|
||||
expected_max_from_model = 32767 * 0.9
|
||||
uses_model_scaling = max_mag > expected_max_from_model * 0.8
|
||||
if uses_model_scaling:
|
||||
pass
|
||||
else:
|
||||
warn(f"Magnitude ({max_mag:.0f}) is much lower than expected from Python model "
|
||||
f"({expected_max_from_model:.0f}). .mem files may have unknown provenance.")
|
||||
|
||||
# Check non-zero content: how many samples are non-zero?
|
||||
sum(1 for v in all_i if v != 0)
|
||||
sum(1 for v in all_q if v != 0)
|
||||
|
||||
# Analyze instantaneous frequency via phase differences
|
||||
phases = []
|
||||
for i_val, q_val in zip(all_i, all_q, strict=False):
|
||||
if abs(i_val) > 5 or abs(q_val) > 5: # Skip near-zero samples
|
||||
phases.append(math.atan2(q_val, i_val))
|
||||
else:
|
||||
phases.append(None)
|
||||
|
||||
# Compute phase differences (instantaneous frequency)
|
||||
freq_estimates = []
|
||||
for n in range(1, len(phases)):
|
||||
if phases[n] is not None and phases[n-1] is not None:
|
||||
dp = phases[n] - phases[n-1]
|
||||
# Unwrap
|
||||
while dp > math.pi:
|
||||
dp -= 2 * math.pi
|
||||
while dp < -math.pi:
|
||||
dp += 2 * math.pi
|
||||
# Frequency in Hz (at 100 MHz sample rate, since these are post-DDC)
|
||||
f_inst = dp * FS_SYS / (2 * math.pi)
|
||||
freq_estimates.append(f_inst)
|
||||
|
||||
if freq_estimates:
|
||||
sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0]
|
||||
sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1]
|
||||
f_min = min(freq_estimates)
|
||||
f_max = max(freq_estimates)
|
||||
f_range = f_max - f_min
|
||||
|
||||
|
||||
# A chirp should show frequency sweep
|
||||
is_chirp = f_range > 0.5e6 # At least 0.5 MHz sweep
|
||||
check(is_chirp,
|
||||
f"Long chirp shows frequency sweep ({f_range/1e6:.2f} MHz > 0.5 MHz)")
|
||||
|
||||
# Check if bandwidth roughly matches expected
|
||||
bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50%
|
||||
if bw_match:
|
||||
pass
|
||||
else:
|
||||
warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz")
|
||||
|
||||
# Compare segment boundaries for overlap-save consistency
|
||||
# In proper overlap-save, the chirp data should be segmented at 896-sample boundaries
|
||||
# with segments being 1024-sample FFT blocks
|
||||
for seg in range(4):
|
||||
seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem')
|
||||
seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem')
|
||||
seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q, strict=False)]
|
||||
sum(seg_mags) / len(seg_mags)
|
||||
max(seg_mags)
|
||||
|
||||
# Check segment 3 zero-padding (chirp is 3000 samples, seg3 starts at 3072)
|
||||
# Samples 3000-4095 should be zero (or near-zero) if chirp is exactly 3000 samples
|
||||
if seg == 3:
|
||||
# Seg3 covers chirp samples 3072..4095
|
||||
# If chirp is only 3000 samples, then only samples 0..(3000-3072) = NONE are valid
|
||||
# Actually chirp has 3000 samples total. Seg3 starts at index 3*1024=3072.
|
||||
# So seg3 should only have 3000-3072 = -72 -> no valid chirp data!
|
||||
# Wait, but the .mem files have 1024 lines with non-trivial data...
|
||||
# Let's check if seg3 has significant data
|
||||
zero_count = sum(1 for m in seg_mags if m < 2)
|
||||
if zero_count > 500:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 4: Short Chirp .mem File Analysis
|
||||
# ============================================================================
|
||||
def test_short_chirp():
|
||||
|
||||
short_i = read_mem_hex('short_chirp_i.mem')
|
||||
short_q = read_mem_hex('short_chirp_q.mem')
|
||||
|
||||
check(len(short_i) == 50, f"Short chirp I: {len(short_i)} samples (expected 50)")
|
||||
check(len(short_q) == 50, f"Short chirp Q: {len(short_q)} samples (expected 50)")
|
||||
|
||||
# Expected: 0.5 us chirp at 100 MHz = 50 samples
|
||||
expected_samples = int(T_SHORT_CHIRP * FS_SYS)
|
||||
check(len(short_i) == expected_samples,
|
||||
f"Short chirp length matches T_SHORT_CHIRP * FS_SYS = {expected_samples}")
|
||||
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q, strict=False)]
|
||||
max(magnitudes)
|
||||
sum(magnitudes) / len(magnitudes)
|
||||
|
||||
|
||||
# Check non-zero
|
||||
nonzero = sum(1 for m in magnitudes if m > 1)
|
||||
check(nonzero == len(short_i), f"All {nonzero}/{len(short_i)} samples non-zero")
|
||||
|
||||
# Check it looks like a chirp (phase should be quadratic)
|
||||
phases = [math.atan2(q, i) for i, q in zip(short_i, short_q, strict=False)]
|
||||
freq_est = []
|
||||
for n in range(1, len(phases)):
|
||||
dp = phases[n] - phases[n-1]
|
||||
while dp > math.pi:
|
||||
dp -= 2 * math.pi
|
||||
while dp < -math.pi:
|
||||
dp += 2 * math.pi
|
||||
freq_est.append(dp * FS_SYS / (2 * math.pi))
|
||||
|
||||
if freq_est:
|
||||
freq_est[0]
|
||||
freq_est[-1]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 5: Generate Expected Chirp .mem and Compare
|
||||
# ============================================================================
|
||||
def test_chirp_vs_model():
|
||||
|
||||
# Generate reference using the same method as radar_scene.py
|
||||
chirp_rate = CHIRP_BW / T_LONG_CHIRP # Hz/s
|
||||
|
||||
model_i = []
|
||||
model_q = []
|
||||
n_chirp = min(FFT_SIZE, LONG_CHIRP_SAMPLES) # 1024
|
||||
|
||||
for n in range(n_chirp):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
re_val = round(32767 * 0.9 * math.cos(phase))
|
||||
im_val = round(32767 * 0.9 * math.sin(phase))
|
||||
model_i.append(max(-32768, min(32767, re_val)))
|
||||
model_q.append(max(-32768, min(32767, im_val)))
|
||||
|
||||
# Read seg0 from .mem
|
||||
mem_i = read_mem_hex('long_chirp_seg0_i.mem')
|
||||
mem_q = read_mem_hex('long_chirp_seg0_q.mem')
|
||||
|
||||
# Compare magnitudes
|
||||
model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q, strict=False)]
|
||||
mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q, strict=False)]
|
||||
|
||||
model_max = max(model_mags)
|
||||
mem_max = max(mem_mags)
|
||||
|
||||
|
||||
# Check if they match (they almost certainly won't based on magnitude analysis)
|
||||
matches = sum(1 for a, b in zip(model_i, mem_i, strict=False) if a == b)
|
||||
|
||||
if matches > len(model_i) * 0.9:
|
||||
pass
|
||||
else:
|
||||
warn(".mem files do NOT match Python model. They likely have different provenance.")
|
||||
# Try to detect scaling
|
||||
if mem_max > 0:
|
||||
model_max / mem_max
|
||||
|
||||
# Check phase correlation (shape match regardless of scaling)
|
||||
model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q, strict=False)]
|
||||
mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q, strict=False)]
|
||||
|
||||
# Compute phase differences
|
||||
phase_diffs = []
|
||||
for mp, fp in zip(model_phases, mem_phases, strict=False):
|
||||
d = mp - fp
|
||||
while d > math.pi:
|
||||
d -= 2 * math.pi
|
||||
while d < -math.pi:
|
||||
d += 2 * math.pi
|
||||
phase_diffs.append(d)
|
||||
|
||||
sum(phase_diffs) / len(phase_diffs)
|
||||
max_phase_diff = max(abs(d) for d in phase_diffs)
|
||||
|
||||
|
||||
phase_match = max_phase_diff < 0.5 # within 0.5 rad
|
||||
check(
|
||||
phase_match,
|
||||
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg "
|
||||
f"(tolerance: 28.6 deg)",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 7: Cross-check chirp memory loader addressing
|
||||
# ============================================================================
|
||||
def test_memory_addressing():
|
||||
|
||||
# chirp_memory_loader_param uses: long_addr = {segment_select[1:0], sample_addr[9:0]}
|
||||
# This creates a 12-bit address: seg[1:0] ++ addr[9:0]
|
||||
# Segment 0: addresses 0x000..0x3FF (0..1023)
|
||||
# Segment 1: addresses 0x400..0x7FF (1024..2047)
|
||||
# Segment 2: addresses 0x800..0xBFF (2048..3071)
|
||||
# Segment 3: addresses 0xC00..0xFFF (3072..4095)
|
||||
|
||||
for seg in range(4):
|
||||
base = seg * 1024
|
||||
end = base + 1023
|
||||
addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0}
|
||||
addr_end = (seg << 10) | 1023
|
||||
|
||||
check(
|
||||
addr_from_concat == base,
|
||||
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} "
|
||||
f"(expected {base})",
|
||||
)
|
||||
check(addr_end == end,
|
||||
f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})")
|
||||
|
||||
# Memory is declared as: reg [15:0] long_chirp_i [0:4095]
|
||||
# $readmemh loads seg0 to [0:1023], seg1 to [1024:2047], etc.
|
||||
# Addressing via {segment_select, sample_addr} maps correctly.
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 8: Seg3 zero-padding analysis
|
||||
# ============================================================================
|
||||
def test_seg3_padding():
|
||||
|
||||
# The long chirp has 3000 samples (30 us at 100 MHz).
|
||||
# With 4 segments of 1024 samples = 4096 total memory slots.
|
||||
# Segments are loaded contiguously into memory:
|
||||
# Seg0: chirp samples 0..1023
|
||||
# Seg1: chirp samples 1024..2047
|
||||
# Seg2: chirp samples 2048..3071
|
||||
# Seg3: chirp samples 3072..4095
|
||||
#
|
||||
# But the chirp only has 3000 samples! So seg3 should have:
|
||||
# Valid chirp data at indices 0..(3000-3072-1) = NEGATIVE
|
||||
# Wait — 3072 > 3000, so seg3 has NO valid chirp samples if chirp is exactly 3000.
|
||||
#
|
||||
# However, the overlap-save algorithm in matched_filter_multi_segment.v
|
||||
# collects data differently:
|
||||
# Seg0: collect 896 DDC samples, buffer[0:895], zero-pad [896:1023]
|
||||
# Seg1: overlap from seg0[768:895] → buffer[0:127], collect 896 → buffer[128:1023]
|
||||
# ...
|
||||
# The chirp reference is indexed by segment_select + sample_addr,
|
||||
# so it reads ALL 1024 values for each segment regardless.
|
||||
#
|
||||
# If the chirp is 3000 samples but only 4*1024=4096 slots exist,
|
||||
# the question is: do the .mem files contain 3000 samples of real chirp
|
||||
# data spread across 4096 slots, or something else?
|
||||
|
||||
seg3_i = read_mem_hex('long_chirp_seg3_i.mem')
|
||||
seg3_q = read_mem_hex('long_chirp_seg3_q.mem')
|
||||
|
||||
mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q, strict=False)]
|
||||
|
||||
# Count trailing zeros (samples after chirp ends)
|
||||
trailing_zeros = 0
|
||||
for m in reversed(mags):
|
||||
if m < 2:
|
||||
trailing_zeros += 1
|
||||
else:
|
||||
break
|
||||
|
||||
nonzero = sum(1 for m in mags if m > 2)
|
||||
|
||||
|
||||
if nonzero == 1024:
|
||||
# This means the .mem files encode 4096 chirp samples, not 3000
|
||||
# The chirp duration used for .mem generation was different from T_LONG_CHIRP
|
||||
actual_chirp_samples = 4 * 1024 # = 4096
|
||||
actual_duration = actual_chirp_samples / FS_SYS
|
||||
warn(f"Chirp in .mem files appears to be {actual_chirp_samples} samples "
|
||||
f"({actual_duration*1e6:.1f} us), not {LONG_CHIRP_SAMPLES} samples "
|
||||
f"({T_LONG_CHIRP*1e6:.1f} us)")
|
||||
elif trailing_zeros > 100:
|
||||
# Some padding at end
|
||||
3072 + (1024 - trailing_zeros)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
def main():
|
||||
|
||||
test_structural()
|
||||
test_twiddle_1024()
|
||||
test_twiddle_16()
|
||||
test_long_chirp()
|
||||
test_short_chirp()
|
||||
test_chirp_vs_model()
|
||||
test_memory_addressing()
|
||||
test_seg3_padding()
|
||||
|
||||
if fail_count == 0:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
return 0 if fail_count == 0 else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -8,22 +8,28 @@
|
||||
//
|
||||
// Production wiring this TB mirrors:
|
||||
// ddc_i/q (test stimulus) -> matched_filter_multi_segment -> chain
|
||||
// chirp_memory_loader -----direct wire--------------------> chain ref
|
||||
// chirp_reference_rom ---- direct wire -------------------> chain ref
|
||||
//
|
||||
// Tests:
|
||||
// 1) Pipeline timing: report cycle counts (first ddc_valid -> first
|
||||
// pc_valid). Confirms FSM advances and produces output.
|
||||
// 2) Autocorrelation peak position: drive ddc with the SAME short-chirp
|
||||
// samples that the loader serves up as ref. Output is the chirp
|
||||
// autocorrelation. Peak should be at bin 0 if ref/signal are aligned
|
||||
// at the chain. Any shift indicates an alignment error of N cycles.
|
||||
// samples the ROM serves up as ref. Output is the chirp autocorrelation.
|
||||
// Peak should be at bin 0 if ref/signal are aligned at the chain.
|
||||
//
|
||||
// chirp-v2 PR-C: ROM swapped from chirp_memory_loader_param to
|
||||
// chirp_reference_rom. Stim now reads rx_short_{i,q}.mem (100 active samples,
|
||||
// 1 µs at 100 MHz) instead of the legacy short_chirp_*.mem (50 samples,
|
||||
// 0.5 µs); SHORT_LEN tracks the new active-sample count. The ROM and the
|
||||
// stim always read from the same file, so the autocorrelation invariant
|
||||
// (peak at bin 0) holds without further coordination.
|
||||
// ============================================================================
|
||||
|
||||
module tb_rxb_fullchain_latency;
|
||||
|
||||
localparam CLK_PERIOD = 10.0; // 100 MHz
|
||||
localparam FFT_SIZE = `RP_FFT_SIZE; // 2048
|
||||
localparam SHORT_LEN = 50; // matches RP_SHORT_CHIRP_SAMPLES
|
||||
localparam SHORT_LEN = 100; // matches RP_DEF_SHORT_CHIRP_CYCLES_V2 (1 µs)
|
||||
|
||||
reg clk;
|
||||
reg reset_n;
|
||||
@@ -38,13 +44,13 @@ module tb_rxb_fullchain_latency;
|
||||
reg mc_new_elevation;
|
||||
reg mc_new_azimuth;
|
||||
|
||||
// multi_segment <-> memory loader interconnect
|
||||
// multi_segment <-> chirp_reference_rom interconnect
|
||||
wire [1:0] segment_request;
|
||||
wire [10:0] sample_addr_out;
|
||||
wire mem_request;
|
||||
wire mem_ready_loader; // direct from loader
|
||||
wire mem_ready_loader; // direct from rom
|
||||
|
||||
// Loader outputs (direct-wired to chain via multi_segment ports)
|
||||
// ROM outputs (direct-wired to chain via multi_segment ports)
|
||||
wire [15:0] ref_i_raw;
|
||||
wire [15:0] ref_q_raw;
|
||||
|
||||
@@ -54,15 +60,16 @@ module tb_rxb_fullchain_latency;
|
||||
wire pc_valid;
|
||||
wire [3:0] ms_status;
|
||||
|
||||
// ----- Memory loader -----
|
||||
chirp_memory_loader_param #(
|
||||
.DEBUG(0)
|
||||
) chirp_mem (
|
||||
// wave_sel shim — matches radar_receiver_final.v PR-C transitional wiring.
|
||||
wire [1:0] wave_sel = use_long_chirp ? `RP_WAVE_LONG : `RP_WAVE_SHORT;
|
||||
|
||||
// ----- Chirp reference ROM (chirp-v2 PR-C) -----
|
||||
chirp_reference_rom chirp_rom (
|
||||
.clk (clk),
|
||||
.reset_n (reset_n),
|
||||
.wave_sel (wave_sel),
|
||||
.segment_select (segment_request),
|
||||
.mem_request (mem_request),
|
||||
.use_long_chirp (use_long_chirp),
|
||||
.sample_addr (sample_addr_out),
|
||||
.ref_i (ref_i_raw),
|
||||
.ref_q (ref_q_raw),
|
||||
@@ -224,11 +231,11 @@ module tb_rxb_fullchain_latency;
|
||||
mc_new_elevation = 1'b0;
|
||||
mc_new_azimuth = 1'b0;
|
||||
|
||||
// Load the same short-chirp samples the loader will serve as ref,
|
||||
// Load the same short-chirp samples the ROM will serve as ref,
|
||||
// so signal == ref → autocorrelation. Peak should be at bin 0 if
|
||||
// ref/signal alignment is correct.
|
||||
$readmemh("short_chirp_i.mem", stim_chirp_i, 0, SHORT_LEN-1);
|
||||
$readmemh("short_chirp_q.mem", stim_chirp_q, 0, SHORT_LEN-1);
|
||||
$readmemh("rx_short_i.mem", stim_chirp_i, 0, SHORT_LEN-1);
|
||||
$readmemh("rx_short_q.mem", stim_chirp_q, 0, SHORT_LEN-1);
|
||||
$display("[TB] Loaded %0d short-chirp samples for stimulus", SHORT_LEN);
|
||||
|
||||
repeat (8) @(posedge clk);
|
||||
@@ -237,7 +244,7 @@ module tb_rxb_fullchain_latency;
|
||||
|
||||
$display("\n=== RX-B Option A verification ===");
|
||||
$display("Configuration: latency_buffer REMOVED, ref direct-wired");
|
||||
$display("Path: chirp_memory_loader.ref_i ----> multi_segment.ref_chirp_real");
|
||||
$display("Path: chirp_reference_rom.ref_i ----> multi_segment.ref_chirp_real");
|
||||
$display("FFT_SIZE: %0d, SHORT_LEN: %0d", FFT_SIZE, SHORT_LEN);
|
||||
$display("");
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
* radar_receiver_final.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 \
|
||||
* chirp_reference_rom.v \
|
||||
* matched_filter_multi_segment.v matched_filter_processing_chain.v \
|
||||
* range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
|
||||
* usb_data_interface.v edge_detector.v radar_mode_controller.v
|
||||
|
||||
Reference in New Issue
Block a user