chore(repo): PR-S — m-1..m-9 hygiene sweep (audit cleanup)

Bundled minor-tier fixes from project_aeris10_audit_2026-05-02. No
behavioural changes to the production happy path; mostly stale comments,
defaults, and one new emit-path (m-9) that lets cosim_dir replay show
detections instead of an empty mask.

  m-1 — processing.py:59 RadarProcessor.range_doppler_map placeholder
        shape (1024, 32) -> (NUM_RANGE_BINS, NUM_DOPPLER_BINS) imported
        from radar_protocol so the legacy literal stops leaking to
        anything reading the attribute before frame 0.
  m-2 — radar_receiver_final.v:596 stale "// 32" comment for
        RP_CHIRPS_PER_FRAME -> "// 48 (PR-F: 3 sub-frames * 16)".
  m-4 — radar_protocol.py "16384 x 2 = 32768" arithmetic comment was
        already corrected by an earlier edit; verified clean.
  m-5 — usb_data_interface_ft2232h.v:961 "Frame header: 8 bytes"
        comment -> "9 bytes (PR-G: added version byte at offset 1)".
  m-6 — radar_system_top.v cold-reset host_chirps_per_elev 32 -> 48
        + status doc-comment so any sanity-checking parser sees the
        value matching RP_CHIRPS_PER_FRAME instead of latching a
        chirps_mismatch_error.
  m-7 — radar_receiver_final.v:370 RX DDC mixers_enable(1'b1)
        annotated: documented as intentional asymmetry vs TX (counter-
        UAS RX has no quiesce scenario; CDC would add cost without
        operational benefit).
  m-8 — RadarSettings range_resolution / velocity_resolution flagged
        inline as PLACEHOLDER (docstring already explains; inline
        marker makes it visible at the field).
  m-9 — gen_realdata_hex.py now also emits fullchain_cfar_flags.npy
        (uint8 detection mask) and fullchain_cfar_mag.npy (|I|+|Q|),
        produced by run_cfar_ca() with the FPGA cold-reset defaults
        (guard=2 train=8 alpha=0x30 mode=CA). Replays through
        v7.replay's COSIM_DIR loader: 22 detections on the synthetic
        scene (was 0). The hex/ directory's two new .npy files are
        included in this commit.

Regression: 247/247 (test_v7 130 + test_GUI_V65_Tk 117). Ruff clean.
This commit is contained in:
Jason
2026-05-02 17:13:12 +05:45
parent c2637251b0
commit 8ebb7016de
8 changed files with 50 additions and 18 deletions
+5 -1
View File
@@ -367,6 +367,10 @@ ddc_400m_enhanced ddc(
.baseband_q(ddc_out_q), // Q output at 100MHz
.baseband_valid_i(ddc_valid_i), // Valid at 100MHz
.baseband_valid_q(ddc_valid_q),
// RX DDC mixer is always enabled asymmetric vs the TX path which CDCs
// stm32_mixers_enable into clk_120m_dac (radar_transmitter.v:175-183).
// Counter-UAS RX has no operational scenario where the digital DDC NCO
// should be quiesced while the system is running; tie-1 is intentional.
.mixers_enable(1'b1),
// Diagnostics (audit F-6.1) — previously all unconnected
.ddc_status(ddc_status_w),
@@ -593,7 +597,7 @@ assign range_data_valid = mti_range_valid;
doppler_processor_optimized #(
.DOPPLER_FFT_SIZE(`RP_DOPPLER_FFT_SIZE), // 16
.RANGE_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.CHIRPS_PER_FRAME(`RP_CHIRPS_PER_FRAME), // 32
.CHIRPS_PER_FRAME(`RP_CHIRPS_PER_FRAME), // 48 (PR-F: 3 sub-frames * 16)
.CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME) // 16
) doppler_proc (
.clk(clk),
+5 -2
View File
@@ -271,7 +271,7 @@ reg [15:0] host_short_chirp_cycles; // Opcode 0x13 (default 100, V2)
reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17400, V2)
reg [15:0] host_medium_chirp_cycles; // Opcode 0x17 (default 500, PR-G G2)
reg [15:0] host_medium_listen_cycles; // Opcode 0x18 (default 15600, PR-Q staggered PRI)
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32)
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER_FRAME, PR-F)
reg host_status_request; // Opcode 0xFF (self-clearing pulse)
// Fix 4: Doppler/chirps mismatch protection
@@ -1056,7 +1056,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
host_short_listen_cycles <= 16'd`RP_DEF_SHORT_LISTEN_CYCLES_V2;
host_medium_chirp_cycles <= 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; // PR-G G2
host_medium_listen_cycles <= 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; // PR-G G2
host_chirps_per_elev <= 6'd32;
// PR-F bumped RP_CHIRPS_PER_FRAME 32 -> 48 (3 sub-frames * 16); the
// chirps_per_elev register is echoed in status word 3 and used by host
// sanity-checking. Keep cold-reset value in lockstep with the truth.
host_chirps_per_elev <= 6'd48;
host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0;
host_range_mode <= 2'b00; // Default: 3 km mode (all short chirps)
@@ -18,8 +18,10 @@ Outputs (all under tb/cosim/real_data/hex/):
fullchain_doppler_ref_q.hex same shape as doppler_ref_*
GUI replay intermediates (.npy, COSIM_DIR ReplayFormat in v7.replay):
decimated_range_i.npy / _q.npy (48, 512) post range_bin_decimator
doppler_map_i.npy / _q.npy (512, 48) post doppler_processor
decimated_range_i.npy / _q.npy (48, 512) post range_bin_decimator
doppler_map_i.npy / _q.npy (512, 48) post doppler_processor
fullchain_cfar_flags.npy (512, 48) uint8 CA-CFAR detection mask
fullchain_cfar_mag.npy (512, 48) |I|+|Q| magnitudes (CFAR input)
Dimensions match production (radar_params.vh: RP_FFT_SIZE=2048,
RP_DECIMATION_FACTOR=4, RP_NUM_RANGE_BINS=512, RP_NUM_DOPPLER_BINS=48).
@@ -39,7 +41,7 @@ import numpy as np
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fpga_model import DopplerProcessor, RangeBinDecimator
from fpga_model import DopplerProcessor, RangeBinDecimator, run_cfar_ca
from radar_scene import Target, generate_doppler_frame
@@ -164,19 +166,33 @@ def gen_fullchain_realdata():
write_hex_16(os.path.join(OUT_DIR, "fullchain_doppler_ref_q.hex"), flat_q)
# Same arrays serialized for v7.replay COSIM_DIR format (GUI replay).
np.save(os.path.join(OUT_DIR, "decimated_range_i.npy"),
np.asarray(decim_i_2d, dtype=np.int32))
np.save(os.path.join(OUT_DIR, "decimated_range_q.npy"),
np.asarray(decim_q_2d, dtype=np.int32))
np.save(os.path.join(OUT_DIR, "doppler_map_i.npy"),
np.asarray(doppler_i, dtype=np.int32))
np.save(os.path.join(OUT_DIR, "doppler_map_q.npy"),
np.asarray(doppler_q, dtype=np.int32))
decim_i_arr = np.asarray(decim_i_2d, dtype=np.int32)
decim_q_arr = np.asarray(decim_q_2d, dtype=np.int32)
doppler_i_arr = np.asarray(doppler_i, dtype=np.int32)
doppler_q_arr = np.asarray(doppler_q, dtype=np.int32)
np.save(os.path.join(OUT_DIR, "decimated_range_i.npy"), decim_i_arr)
np.save(os.path.join(OUT_DIR, "decimated_range_q.npy"), decim_q_arr)
np.save(os.path.join(OUT_DIR, "doppler_map_i.npy"), doppler_i_arr)
np.save(os.path.join(OUT_DIR, "doppler_map_q.npy"), doppler_q_arr)
# m-9: Run the CA-CFAR model on the doppler map so v7.replay shows
# detections instead of falling through to zeros. Defaults match the FPGA
# cold-reset (RP_DEF_CFAR_*: guard=2, train=8, alpha=0x30=3.0 Q4.4, mode=CA).
cfar_flags, cfar_mag, _thr = run_cfar_ca(
doppler_i_arr, doppler_q_arr,
guard=2, train=8, alpha_q44=0x30, mode='CA',
)
np.save(os.path.join(OUT_DIR, "fullchain_cfar_flags.npy"),
cfar_flags.astype(np.uint8))
np.save(os.path.join(OUT_DIR, "fullchain_cfar_mag.npy"),
cfar_mag.astype(np.int64))
print(f" stimulus: {len(stim)} packed lines "
f"(expected {CHIRPS_PER_FRAME * FULLCHAIN_INPUT_BINS})")
print(f" golden: {len(flat_i)} lines i / {len(flat_q)} lines q "
f"(expected {DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS})")
print(f" cfar: {int(cfar_flags.sum())} detections "
f"(across {DOPPLER_RANGE_BINS}x{DOPPLER_TOTAL_BINS} cells)")
def main():
@@ -204,6 +220,8 @@ def main():
"decimated_range_q.npy",
"doppler_map_i.npy",
"doppler_map_q.npy",
"fullchain_cfar_flags.npy",
"fullchain_cfar_mag.npy",
)
for f in npy_files:
path = os.path.join(OUT_DIR, f)
@@ -958,7 +958,7 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
end
end
// ---- Frame header: 8 bytes ----
// ---- Frame header: 9 bytes (PR-G: added version byte at offset 1) ----
WR_FRAME_HDR: begin
if (!ft_txe_n) begin
ft_data_oe <= 1'b1;
+5 -2
View File
@@ -121,8 +121,11 @@ class RadarSettings:
not be relied on for physics-accurate display.
"""
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc)
range_resolution: float = 6.0 # Meters per range bin (c/(2*Fs)*decim = 1.5*4)
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
# PLACEHOLDER — see class docstring. Live workers derive the real values
# from WaveformConfig (per-subframe LONG/MEDIUM/SHORT v_res). Do not use
# these literals for physics-accurate display.
range_resolution: float = 6.0 # placeholder; legacy 1.5 m * decim=4
velocity_resolution: float = 1.0 # placeholder; legacy 167 us / 16-chirp / 10.5 GHz
max_distance: float = 3072 # Max detection range (m), 3 km mode
map_size: float = 4000 # Map display size (m)
coverage_radius: float = 3072 # Map coverage radius (m), 3 km mode
+5 -1
View File
@@ -56,7 +56,11 @@ class RadarProcessor:
"""Full radar processing pipeline: fusion, clustering, association, tracking."""
def __init__(self):
self.range_doppler_map = np.zeros((1024, 32))
# Placeholder shape matches production (NUM_RANGE_BINS x NUM_DOPPLER_BINS
# = 512 x 48 from radar_protocol). Overwritten on every frame; pre-frame
# readers see the correct shape rather than a legacy (1024, 32).
from radar_protocol import NUM_RANGE_BINS, NUM_DOPPLER_BINS
self.range_doppler_map = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS))
self.detected_targets: list[RadarTarget] = []
self.track_id_counter: int = 0
self.tracks: dict[int, dict] = {}