Commit Graph

464 Commits

Author SHA1 Message Date
Jason
eb9be337b1 sim(antenna): add 4x4 probe-fed array model — mutual coupling characterisation
Single-port-driven, 15-port-terminated FDTD sim built on the same v3 patch
design point (W=7.854, L=6.56, FEED_OFFSET=2.14) at the Gerber pitch
(14.27 mm X / 15.01 mm Y). Reads out S_jd for all elements when port
(DRIVEN_X, DRIVEN_Y) is excited. Configurable array size + driven port via
env vars; default = 4x4, drive inner element (1,1).

Result at PROFILE=balanced, drive (1,1):
  Active S11 @ 10.5 GHz : -16.7 dB,  Z = 45.7 + j13.5 Ω
  Active -10 dB BW      : 190 MHz (10.44-10.63 GHz; ≈ single-element 180)
  Nearest-neighbour coupling:
    -x edge neighbour (0,1): -18.3 dB  (strongest — edge element loads less)
    -y/+y in-array        : -22 dB
    +x interior neighbour : -24.7 dB
  Diagonal               : -32 dB
  Far corners            : -45 to -53 dB

Outputs: coupling_grid.png heatmap + S_matrix.csv (all S_jd full sweep) +
S11_data.csv (driven-port only).

Known limitation: port box must align with mesh lines; SmoothMeshLines may
sub-cell-shift seed lines, causing some DRIVEN_X/DRIVEN_Y choices to give
"Unused primitive" warning + zero excitation. Default (1,1) is verified;
other choices are best-effort. Documented inline.
2026-05-03 16:18:42 +05:45
Jason
3f3846b514 sim(antenna): add probe-fed 2-layer patch model — 180 MHz BW vs aperture-coupled 60 MHz
Single-element OpenEMS sim for a 2-layer probe-fed patch on 0.508 mm RO4350B.
Verified at PROFILE=balanced (λ/25 mesh): f_res = 10.51 GHz, S11 = -21.8 dB,
VSWR 1.18, -10 dB BW = 180 MHz (10.40-10.58 GHz). Direct 50 Ω match — no port
matching cap needed.

Design point baked into defaults:
  PATCH_W      = 7.854 mm   (preserved from old 8x16 Gerber → array compatible)
  PATCH_L      = 6.56 mm    (tuned for f_res = 10.5 GHz at 0.508 mm sub)
  FEED_OFFSET  = 2.14 mm    (probe via, from -y radiating edge)

Why probe-fed: aperture-coupled v2 (4-layer Stack_Hybrid) capped at ~60 MHz BW
because the 0.11 mm L4 acts as a near-short reflector — beneficial for slot
coupling but creates an L2-L4 cavity that's the BW ceiling. Tested removing L4
(open-back aperture-coupled): coupling collapsed, R stayed >1000 Ω regardless
of patch tuning. Probe-fed has no slot bottleneck; physics BW = 1.7% on h=0.508
matches sim 180 MHz directly.

Hardware change required to deploy: 2-layer stackup (patch on top, ground on
bottom, probe vias with antipads). Old 8x16 Gerber was edge-fed; v3 is probe-
fed → top-layer feed network goes away, ADAR1000 carrier on a separate board
with SMP RF launches. Stackup signoff with antenna designer needed before PCB.

aperture_coupled_v2 retained as the 4-layer fallback (with the 0.043 pF cap)
if 2-layer redesign isn't approved.
2026-05-03 15:28:57 +05:45
Jason
e01c2ae424 sim(antenna): tune aperture-coupled v2 to design point — R matched, X residual documented
10-iteration analytic-tune sweep (no DE optimizer — that was a wrong
direction for this topology) on the Stack_Hybrid 4-layer stackup. Key
findings from the sweep are baked into the script defaults and header.

Best design point @ 10.5 GHz (now the env-var defaults):
  patch  W=9.55 mm  L=7.77 mm
  slot   L=3.00 mm  W=0.50 mm  (centered under patch)
  stub   L=4.16 mm  (= λ_g/4 in feed sub at f0)
  feed   lead=12.34 mm  W=0.25 mm
         total feed length = 1·λ_g at 10.5 GHz, line transparent at f0

Result: Z ≈ R + j350 Ω at 10.5 GHz, R = 33–51 across reruns (sanity-profile
convergence drift; X is stable). R is within matching range; the +j350
inductive residual is fundamental to the topology (L4 backshort under the
antenna footprint), not a tuning artifact. Two production-grade fixes:
  (a) Series cap at port C ≈ 0.043 pF — drops S11 to ≈ -40 dB
  (b) Open L4 backshort under antenna — restores standard open-back
      aperture-coupled, stub naturally tunes X

Three real bugs fixed in the underlying sim (without these, the baseline
script was measuring artifacts, not the antenna):

1. Z mesh under-density. Default SmoothMeshLines at λ/18 gave the 0.11 mm
   feed substrate ~0.1 cells vertically — microstrip Z0 was a pure mesh
   artifact. Now built explicitly with ≥5 substrate-interior lines per
   dielectric (bypassing SmoothMeshLines collapse for z-axis only). Fine
   substrate cells visible via MESH_DEBUG=1.

2. SLOT_Y_OFF_MM env var was read but never applied to the L2 slot box,
   silently making the parameter inert. Slot is now correctly offset in y.

3. FEED_LEAD_L was hardcoded at 14.0 mm, making total feed length 18.16 mm
   = exactly 1·λ_g at 9.5 GHz (in feed substrate). This created a parasitic
   feed-line full-wave standing wave in-band that masked the patch as a
   persistent "9.4 GHz resonance" regardless of patch dimensions. Now
   parameterized via FEED_LEAD_L_MM with default 12.34 mm so total feed =
   1·λ_g at 10.5 GHz instead — line is transparent at the operating freq
   and sim measures the actual antenna impedance at the port.

Sweep grids updated to span ±1 step around the iter #6 design point.
2026-05-03 00:26:41 +05:45
Jason
42056b8331 sim(antenna): add OpenEMS aperture-coupled patch model for Stack_Hybrid v2
Re-simulation infrastructure for the new 4-layer aperture-coupled antenna
stackup (Stack_Hybrid.png, committed 1de2296). Renders the full multilayer
geometry (L1 patch / L2 slot ground / L3 microstrip feed / L4 backplane)
on the actual substrate stack (RO4350B 0.508 mm + RO4450F 1.2 mm + RO4350B
0.11 mm) and runs FDTD via openEMS to characterize S11 vs frequency.

Geometry interpolated from existing 2-layer Gerber:
  patch W from Gerber (7.854 mm, set by εr only)
  patch L re-tunable for new substrate (env var PATCH_L_MM, default 7.25 mm)
  slot/feed/stub fully parameterized via env vars

Run modes:
  PROFILE=sanity    : single run, λ/18 mesh, ~30 s
  PROFILE=balanced  : single run, λ/25 mesh, ~60 s
  PROFILE=sweep     : 5×5 grid over slot_L × stub_L, ~25 min

Env overrides for parametric exploration:
  PATCH_W_MM, PATCH_L_MM, SLOT_L_MM, SLOT_W_MM, STUB_L_MM, SLOT_Y_OFF_MM

Setup: openEMS Python bindings built from source against /opt/openEMS for
Python 3.12 in radar_venv; run with
  DYLD_LIBRARY_PATH=~/opt/openEMS/lib ~/radar_venv/bin/python
under cwd != openEMS-Project/openEMS/python (avoids CSXCAD shadow import).

Status: simulation infrastructure verified end-to-end; patch resonates at
10.4 GHz with W=9.0 / L=5.5–6.0 mm. Full 50 Ω impedance match not yet
converged — the L4 backplane (non-standard for aperture-coupled) reduces
slot coupling vs textbook formulas; needs proper EM optimizer (scipy
wrapping or CST/HFSS handoff) to fully tune. ~150 MHz baseline BW
predicted from substrate physics; 30/40 MHz chirp target comfortably
under any plausible match outcome.
2026-05-02 22:37:25 +05:45
Jason
e5d98533ca Merge remote-tracking branch 'origin/main' into feat/dual-range-v2 2026-05-02 19:33:50 +05:45
Jason
ef32345b26 feat(rtl,gui): PR-U / M-8 — sub-frame enable mask routed end-to-end (C-5 hardening)
The chirp_scheduler had a 3-bit host_subframe_enable input {LONG, MEDIUM, SHORT}
that was tied to the constant RP_DEF_SUBFRAME_ENABLE at the receiver instance,
so the host could neither change it nor know what mask was active. With the
mask not at 3'b111 the scheduler skips a sub-frame at TX but doppler_processor
still writes 48 chirp slots, so the host CRT (`dbin // 16 → {SHORT, MED, LONG}`)
silently mis-attributes the SF axis and unfolds to the wrong velocity.

Plumb the mask through:

- radar_system_top.v: new reg [2:0] host_subframe_enable, cold-reset
  RP_DEF_SUBFRAME_ENABLE, opcode 0x19 setter, wired to rx_inst and usb_inst.
- radar_receiver_final.v: new host_subframe_enable[2:0] input port; the
  chirp_scheduler instance is untied from the constant.
- usb_data_interface_ft2232h.v: new subframe_enable[2:0] input + per-frame
  snapshot reg latched at frame_complete (stable for ft_clk read, same
  pattern as stream_flags_snapshot). Byte 2 emission is now
  {2'b00, subframe_enable[2:0], stream_flags[2:0]} — was {5'b00000, stream}.
- radar_protocol.py: Opcode.SUBFRAME_ENABLE = 0x19; RadarFrame.subframe_enable
  field; parse_bulk_frame surfaces bits[5:3]; reserved-mask 0xF8 → 0xC0.
  Bulk-frame mock encodes the mask in its emit so dashboard replay is correct.
- v7/processing.py: extract_targets_from_frame_crt forces every target to
  AMBIGUOUS when frame.subframe_enable != 0b111. Operator sees the red `?`
  flag in the targets table instead of a silently-wrong velocity.
- v7/software_fpga.py + v7/dashboard.py: subframe_enable mirror + setter, and
  replay dispatch routes 0x19 to set_subframe_enable.

Tests (test_v7.py): TestSubframeEnableRoundTrip (4), TestSoftwareFpgaSubframeEnable
(2), TestCrtSubframeMaskGating (3), 0x19 added to TestOpcodeEnumFillIn and
TestReplayOpcodeDispatch. Existing test_full_frame_round_trip updated to expect
byte 2 = 0x3F (mask 0b111 default + stream 0x07).

Cosim TBs (tb/tb_usb_protocol_v2.v, tb/tb_ft2232h_frame_drop.v) drive the new
input with 3'b111 and assert the new byte-2 layout (T2.3: 0x00 → 0x38).

Regression: test_v7 146/146, test_GUI_V65_Tk 117/117, ruff clean.
iverilog: tb_usb_protocol_v2 27/27 PASS, tb_ft2232h_frame_drop 10/10 PASS.
2026-05-02 17:49:16 +05:45
Jason
8ebb7016de 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.
2026-05-02 17:13:12 +05:45
Jason
c2637251b0 feat(gui): PR-R — host control surface fill-in (audit M-2/M-3/M-4/M-6/M-7)
The RTL has been ahead of the host opcode/widget surface since PR-G:
several runtime knobs (MEDIUM PRI, soft-CFAR alpha, ADC power-down) are
fully wired in radar_system_top.v but had no enum / spinbox path, so
the operator could only reach them via raw _send_custom_command. This
PR closes the gap for everything except M-5 (status-packet medium PRI
readback, which needs an RTL change to add a status word).

M-2 — Opcode enum gains MEDIUM_CHIRP=0x17, MEDIUM_LISTEN=0x18,
       CFAR_ALPHA_SOFT=0x2D. Truth-table docstring refreshed.
       Two new spinboxes in Waveform Timing ("Medium Chirp Cycles",
       "Medium Listen Cycles") with the V2 defaults 500 / 15600 (5 us
       chirp, 161 us PRI). One new spinbox in Detection (CFAR)
       ("CFAR Alpha Soft (Q4.4)") with the RP_DEF_CFAR_ALPHA_SOFT=0x18
       default.

M-3 — ADC_PWDN=0x32 added to the enum (was previously commented as
       "reserved for S-25"; the fix landed at radar_system_top.v:1152
       routing to the physical adc_pwdn pin). New "ADC (AD9484)"
       group on the right column with two buttons: ADC Normal (0x32=0)
       and ADC Power Down (0x32=1). Buttons rather than a spinbox
       prevent accidental non-{0,1} values.

M-4 — ADC_FORMAT widget added to the same ADC group: a 2-choice combo
       ("Offset-binary (SJ1 1-2)" vs "Two's-complement (SJ1 2-3)") with
       a Set button, since AD9484 SPI is tied off (CSB high) and the
       only way to flip sign convention is via this opcode.

M-6 — Replay opcode dispatch in _dispatch_to_software_fpga() expanded:
       SoftwareFPGA gains cfar_alpha_soft mirror + setter; 0x2D wired
       through. RTL-only opcodes (chirp timing, range mode, ADC strap,
       self-test, status_request) are no longer silently dropped — they
       log at info-level "acknowledged (no effect on replay — RTL-only
       state)" so the operator gets visible feedback.

M-7 — Chirps Per Elevation widget default 32 -> 48; hint changed from
       "1-32, clamped" to "must be 48 (RTL clamps)". RTL latches
       chirps_mismatch_error in status word 4 bit 10 for any value != 48
       since PR-F. Bonus: SHORT defaults bumped 50/17450 -> 100/17400 to
       match RP_DEF_SHORT_*_CYCLES_V2 (PR-E 1-us SHORT chirp width).

Tests: +10 (TestOpcodeEnumFillIn 5, TestSoftwareFpgaCfarAlphaSoft 2,
       TestReplayOpcodeDispatch 3). 247/247 PASS. Ruff clean.

M-5 (status packet medium_chirp/medium_listen readback) deferred —
needs an RTL change to extend status_words from 7 to 8 (current word 3
has only 10 reserved bits, not enough for two 16-bit fields).
2026-05-02 17:03:09 +05:45
Jason
115c5f0778 feat(gui): M-1 / PR-Q.7 — dashboard CRT confidence column + alias-fold tooltip (C-5)
The CRT extractor (PR-Q.5/PR-Q.6) tags every target with a velocity_confidence
("CONFIRMED" / "LIKELY" / "AMBIGUOUS" / "UNKNOWN") and an optional alias_set
of candidate v_true folds. Until now the operator-facing targets table on the
Main View tab dropped that signal, so a single-PRI-only AMBIGUOUS reading
looked identical to a 3-PRI CONFIRMED one.

Changes:
  - Targets table column count 5 -> 6; new "Confidence" column between
    Velocity and Magnitude.
  - Module helper _confidence_display(label) -> (text, QColor):
      CONFIRMED  green  (DARK_SUCCESS)
      LIKELY     amber  (DARK_WARNING)
      AMBIGUOUS  red    (DARK_ERROR), prefixed with "? " so the row stands
                        out even when the operator's eyes skip the colour.
      UNKNOWN    gray   (DARK_TEXT) — legacy 32-bin / no CRT.
    Unrecognised future labels fall through to UNKNOWN.
  - Velocity cell carries a tooltip listing the CRT alias_set folds when
    present, so hovering reveals all plausible v_true candidates.
  - QColor pulled in from PyQt6.QtGui for the foreground tint.

Tests (TestDashboardConfidenceDisplay, +5):
  - CONFIRMED/LIKELY/AMBIGUOUS/UNKNOWN each map to expected text + colour.
  - AMBIGUOUS leads with "?" so it's visible without colour.
  - Unrecognised label "BANANA" falls back to UNKNOWN/gray.

Regression: 237/237 (test_v7 120 + test_GUI_V65_Tk 117). Ruff clean.

This closes audit M-1 / task PR-Q.7. The C-5 thread is end-to-end functional:
RTL emits 3 sub-frames (PR-Q.1) -> cosim agrees (PR-Q.2) -> v7 models carry
per-subframe params (PR-Q.4) -> processing.py runs CRT (PR-Q.5) -> workers
route through it (PR-Q.6) -> dashboard surfaces the confidence (PR-Q.7).
2026-05-02 16:51:58 +05:45
Jason
3401d05eca fix(gui): P-6 / PR-Q.6 — workers route detections through CRT extractor (C-5)
Both live and replay paths used the legacy single-PRI extractor with the
LONG-PRI v_res placeholder, which yielded wrong velocities for the SHORT
and MEDIUM sub-frames. PR-Q.5 already provided extract_targets_from_frame_crt
(48-bin, 3-PRI Chinese-Remainder-Theorem unfolding) — this PR wires it in.

Changes:
  - workers.py imports extract_targets_from_frame_crt at module scope.
  - RadarDataWorker._run_host_dsp delegates to the CRT extractor and then
    applies GPS pitch correction + DBSCAN clustering + Kalman tracking
    on the returned targets. Inline det_indices loop and
    velocity_resolution_long_mps placeholder removed.
  - ReplayWorker.__init__ binds _extract_targets to the CRT extractor;
    _emit_frame call simplifies to (frame, waveform, gps=).
  - 32-bin legacy recordings still work via the CRT extractor's internal
    fallback to extract_targets_from_frame.
  - Module docstring stale "(64x32)" -> "(512x48)".
  - Dropped unused `import numpy as np` from workers.py (no remaining users).

Tests (TestWorkersRouteThroughCrt, +4):
  - 3-PRI detection produces CONFIRMED + alias_set (was UNKNOWN before).
  - GPS pitch correction applied post-CRT to elevation.
  - Both clustering+tracking off → returns [] (no DSP work).
  - ReplayWorker._extract_targets is exactly the CRT function reference.

Regression: 232/232 (test_v7 115 + test_GUI_V65_Tk 117). Ruff clean.
Closes audit P-6 / task PR-Q.6 — C-5 host wiring complete (PR-Q.7 dashboard
display column is the remaining piece).
2026-05-02 16:47:05 +05:45
Jason
b505266f33 fix(mcu): P-5 — align radar params with PR-F/PR-Q.1; document mode-01 production stance
main.cpp pre-PR-F constants caused two issues:
  - m_max = 32 disagreed with RP_CHIRPS_PER_FRAME = 48 (3 sub-frames * 16);
    getStatusString reported "32 chirps/position" to the GUI, false telemetry.
  - PRI MEDIUM = 161 us (PR-Q.1 stagger) was missing entirely; the MCU only
    knew SHORT=175 / LONG=167. T2 was also stuck at the pre-PR-E 0.5 us
    SHORT chirp width; PR-E switched to 1.0 us.

Fixes:
  - m_max 32 -> 48; T2 0.5 -> 1.0; new T_MEDIUM=5.0, PRI_MEDIUM=161.0 constants.
  - Big doc-comment above runRadarPulseSequence states the production stance:
    FPGA cold-resets to mode 2'b01 (auto-scan) so the MCU's chirp GPIO toggles
    are no-ops; pass-through mode 2'b00 needs a 3-PRI loop the MCU does not
    yet emit, so mode-00 is operationally unsupported until that's built.
  - Removed the redundant /* */ block-comment shadow of the same constants
    that had `T2` defined twice (typo for `PRI2`); pure dead-code cleanup.
  - test_bug16_runradar_shadows_globals.c m_max 32 -> 48 with refreshed
    arithmetic comment; binary still PASSes all 4 checks (g_m wraps to 1
    each iter regardless of m_max value).

No GPIO timing change (would need hardware verification). Audit P-5 closes
with the documented mode-01 stance; rebuilding the loop for mode-00 stays
on the backlog if/when pass-through becomes a deployment requirement.
2026-05-02 16:40:32 +05:45
Jason
8004c59674 fix(gui): P-4 — dashboard NUM_RANGE_BINS 64 → 512 (import from radar_protocol)
dashboard.py:77 had a stale `NUM_RANGE_BINS = 64` literal from pre-PR-F.
Range-Doppler canvas was mis-sized: parser at radar_protocol.py:425
already enforces 512, so production frames would never have rendered
correctly even with P-2/P-3 in place.

Fix: drop the local literals; re-export NUM_RANGE_BINS / NUM_DOPPLER_BINS
through v7/hardware.py (which already re-exports the rest of
radar_protocol) and import them in dashboard.py. Single source of truth.

Also fixed two stale "(64x32)" docstrings: module-header tab description
and `_on_frame_ready` docstring.

Regression: 228/228 (test_v7 111 + test_GUI_V65_Tk 117). Ruff clean.
2026-05-02 16:34:20 +05:45
Jason
9fbb7150b0 fix(gui): P-2/P-3 — bulk-frame parser + status packet caught up to PR-G v2
Audit P-2 and P-3 (2026-05-02): GUI radar_protocol.py was still on the
pre-PR-G wire format. Production frames were rejected 100% before they
reached the dashboard.

Bulk frame (P-2):
- BULK_FRAME_HEADER_SIZE 8 -> 9 (FPGA emits byte[1] = RP_USB_PROTOCOL_VERSION
  = 0x02). All field offsets shift by 1 (frame_num at +3,+4; n_range at
  +5,+6; n_doppler at +7,+8). Parser now validates the version byte.
- Detect packing: 1 bit/cell (np.unpackbits) -> 2 bits/cell, 4 cells per
  byte MSB-first per PR-F. BULK_DETECT_DENSE_BYTES 3072 -> 6144 (= 512 *
  ceil(48*2/8)). New _unpack_detect_2bit returns uint8 codes 0..3
  (NONE/CAND/CONFIRM/RSVD) instead of a 0/1 bitmap.
- Reserved-bit mask 0xC0 -> 0xF8 (only low 3 stream-enable bits valid;
  bits 3-7 reserved). Drop dead BULK_FLAG_MAG_ONLY/SPARSE_DET constants
  and the rejection logic gated on them — the FPGA emit path always emits
  mag-only / dense, so flag-driven variants were never on the wire.
- find_bulk_frame_boundaries: 9-byte minimum, validate version, bin
  counts at +5,+6 and +7,+8.
- _mock_read updated to emit v2 frames so FT2232HConnection(mock=True)
  produces parseable data for tests and replay.

Status (P-3):
- STATUS_PACKET_SIZE 26 -> 30 (PR-G adds status_words[6] for 2-tier CFAR
  telemetry: detect_count_cand[31:16] + detect_threshold_soft[15:0]).
  StatusResponse gains detect_count_cand, detect_threshold_soft, and
  frame_drop_count fields.

Bonus: m-3 fixed in passing — Opcode docstring line refs were stale
(902-944 -> current ranges), now also documents 0x17/0x18/0x2D/0x32 as
"M-2/M-3 — no enum yet" so a reader knows what's wired but unreachable.
RadarFrame docstring "(64 range x 32 Doppler)" -> production dims.

Tests:
- TestBulkFrameV2RoundTrip (5 cases) — synthetic v2 frame round-trip,
  version-byte rejection, reserved-bit rejection, 2-bit code decode,
  back-to-back boundary scan.
- TestStatusPacketV2RoundTrip (4 cases) — 30-byte size, word[6] decode,
  short-packet rejection, legacy-26B packet rejection.
- test_GUI_V65_Tk: _make_status_packet emits 30 B w/ word[6];
  _build_bulk_frame emits v2 w/ version byte + 2-bit detect packing.
  Pre-PR-G assertions on MAG_ONLY/SPARSE_DET dropped; new
  test_reject_wrong_version_byte + test_parse_status_word6_2tier_cfar.

Test result: test_v7 111/111 + test_GUI_V65_Tk 117/117 = 228/228 PASS in
radar_venv. Ruff clean.
2026-05-02 16:24:51 +05:45
Jason
fcbf243aba fix(gui): P-1 — RadarDataWorker __init__ initialises runtime attrs
Audit P-1 (2026-05-02): _frame_queue, _acquisition, and frame counters
were stranded inside set_waveform() due to indentation drift.  The
dashboard constructs RadarDataWorker and calls .start() directly
without ever calling set_waveform, so live FT2232H acquisition crashes
with AttributeError on first frame access in run().

Move the init block back into __init__; set_waveform now only sets
self._waveform.  Add TestRadarDataWorkerInit covering both:
- attrs present after bare __init__ (no set_waveform required)
- set_waveform does not reset runtime counters

Test result: test_v7 102/102 PASS in radar_venv (was 100/100 + 2 new).
2026-05-02 16:08:24 +05:45
Jason
3d2ffc3f2c chore(repo): cosim_dir replay revival + ruff lint cleanup
cosim_dir revival:
- gen_realdata_hex.py: also emit decimated_range_{i,q}.npy (48x512)
  and doppler_map_{i,q}.npy (512x48) at production dimensions; the
  same Python pipeline that produces the RTL .hex stimuli now writes
  the .npy intermediates v7.replay COSIM_DIR loads. Replaces the
  workflow lost when golden_reference.py was deleted in e8b495c
- test_v7.py: update test_get_frame_cosim shape from pre-PR-O.6
  (64,32) to (NUM_RANGE_BINS, NUM_DOPPLER_BINS)
- check in 4 .npy reference files (~400 KB, deterministic SCENE_SEED=42)

Ruff lint cleanup (was 66 errors; now 0):
- pyproject.toml: ignore T20 in tb/cosim/**.py (CLI tools)
- compare_independent.py: drop redundant int() casts (RUF046),
  swap try/except scipy import for importlib.util.find_spec,
  remove dead duplicate np import, ASCII-ize comment unicode,
  wrap E501 format strings
- fpga_reference.py: drop unused fs arg from nco_reference,
  collapse if/else to ternary, mark _out_im unused
- v7/processing.py: ASCII-ize x in docstring, collapse if-branches
- {dashboard,software_fpga,workers,radar_protocol}.py: wrap E501
- test_v7.py: ASCII-ize comment unicode, _alias renames where unused

Result: test_v7 100/100 (0 skips on radar_venv, was 9 graceful
skips); 5 cosim_dir orphan tests now active and passing.
2026-05-02 15:45:56 +05:45
Jason
5a7e8b8689 feat(gui): PR-Q.5 — 3-PRI CRT Doppler unfolder + cluster extractor (C-5)
Add host-side 3-PRI Chinese-Remainder velocity unfolding and a cluster
extractor that reads the 48-bin Doppler frame, splits it into the 3
sub-frames (SHORT/MEDIUM/LONG), and resolves Doppler aliases across
coprime PRIs.  Resolves the algorithm half of audit C-5; the data is
now in extract_targets_from_frame_crt's hands but workers still call
the legacy single-PRI extractor (PR-Q.6 wires it).

v7/processing.py:
- unfold_velocity_crt(v_meas, v_unamb, v_res, max_alias_k=6,
  tol_factor=0.5) -> (v_est, confidence, alias_set).  Brute-force
  candidate search over PRI-0 fold depth, per-PRI half-bin
  tolerance.  Confidence: CONFIRMED (3-PRI unique), LIKELY (3-PRI
  with 2 cands, or 2-PRI with unique cand), AMBIGUOUS (1-PRI, 3+
  cands, 2-PRI multi-cand, or no fold within tol).
- extract_targets_from_frame_crt(frame, waveform, gps, max_alias_k):
  groups detections by range bin, picks strongest bin per
  (rbin, sf), decodes signed Doppler via sub_frame = dbin // 16 /
  bin_in_sf = dbin % 16, calls unfold_velocity_crt, attaches
  velocity_confidence and alias_set to RadarTarget.  Falls back to
  legacy extract_targets_from_frame for non-48-bin frames.

v7/models.py:
- RadarTarget gains velocity_confidence (str default "UNKNOWN") and
  alias_set (list[float] | None).

v7/__init__.py:
- Re-exports unfold_velocity_crt + extract_targets_from_frame_crt.

test_v7.py (16 new tests, 0 failures):
- TestUnfoldVelocityCRT (8): zero-velocity CONFIRMED, below per-PRI
  v_unamb CONFIRMED, above per-PRI (100 m/s) CONFIRMED, near CRT
  ceiling (~261 m/s) CONFIRMED, negative velocity, 1-PRI AMBIGUOUS,
  2-PRI LIKELY, inconsistent measurements AMBIGUOUS+fallback.
- TestExtractTargetsFromFrameCrt (8): 3-PRI CONFIRMED target,
  LONG-only AMBIGUOUS (the 20-km blindspot regime), 2-PRI LIKELY,
  strongest-bin picking, two targets at distinct ranges, legacy
  32-bin frame fallback, no-detections empty, GPS georef.

Local: test_v7 100/0/0 (9 graceful skips), test_GUI_V65_Tk 117/0/2.
2026-05-02 15:23:17 +05:45
Jason
54627bbbe3 fix(gui): software_fpga revival post-e8b495c — port chain helpers to fpga_model
Restore SoftwareFPGA's process_chirps() pipeline by porting the missing
chain stages (MTI canceller, DC notch, CFAR, threshold detection) plus
thin wrappers (range FFT, decimator, Doppler FFT) to fpga_model.py and
swapping software_fpga.py's import target from the deleted
golden_reference.py to fpga_model.

History: golden_reference.py was deleted in e8b495c (the "dead golden
code cleanup") but software_fpga.py kept importing from it.  The
ImportError was swallowed at v7/__init__.py:49-52 so package load
succeeded, but every direct `from v7.software_fpga import SoftwareFPGA`
hit the import-time failure — masking 21 broken tests as
"ModuleNotFoundError" instead of surfacing the real issue.

This was actively breaking the GUI replay-from-raw-IQ feature
(dashboard.py:1334-1347, 1577 + GUI_V65_Tk.py:271-300, 1106-1129):
opening a .npy SDR capture instantiates SoftwareFPGA + ReplayEngine;
the dashboard's opcode dual-dispatch routes spinbox changes to the
SoftwareFPGA setters so re-processing reflects live param tweaks.
With the import broken since April, that path silently dies.

fpga_model.py:
- New top-level constants: FFT_SIZE=2048, NUM_RANGE_BINS=512 (from
  RangeBinDecimator.OUTPUT_BINS), DOPPLER_CHIRPS=48,
  DOPPLER_TOTAL_BINS=48 (track current production: PR-O.6 / PR-F).
- run_range_fft(iq_i, iq_q, twiddle_file): N inferred from input
  length; works for legacy 1024-pt and production 2048-pt callers.
- run_range_bin_decimator(range_i, range_q, mode): per-frame wrapper
  over RangeBinDecimator.decimate (4x decim -> 512 bins).
- run_mti_canceller(decim_i, decim_q, enable): 2-pulse canceller,
  ported verbatim from golden_reference @ commit 237e74c~1.
- run_doppler_fft(mti_i, mti_q): num_subframes inferred from chirp
  count; RANGE_BINS overridden per input shape so legacy
  2-sub-frame (32-chirp) and production 3-sub-frame (48-chirp)
  callers both work.
- run_dc_notch(doppler_i, doppler_q, width): per-bin DC notch,
  generalised to any sub-frame count.
- run_cfar_ca(...): CA / GO / SO modes with bit-accurate alpha-q44
  threshold + 17-bit saturation, ported from golden_reference.
- run_detection(doppler_i, doppler_q, threshold): |I|+|Q| L1 magnitude
  threshold detection.

software_fpga.py:
- _GOLDEN_REF_DIR (cosim/real_data/) -> _FPGA_COSIM_DIR (cosim/)
- `from golden_reference import (...)` -> `from fpga_model import (...)`
- TWIDDLE_1024 -> TWIDDLE_2048 (production 2048-pt range FFT).
- Stage 1 comment: "Range bin decimation (1024 -> 64)" ->
  "(production 2048 -> 512)".
- Stage 1 twiddle path picks fft_twiddle_2048.mem only when
  n_samples=2048 matches; otherwise None to fall back to math-
  generated twiddles for legacy callers.
- Module docstring updated to reflect post-cleanup history.

test_v7.py — modernise three tests to current production dimensions:
- test_process_chirps_returns_radar_frame: pad input to 2048 samples;
  assertions reference NUM_RANGE_BINS / NUM_DOPPLER_BINS from
  radar_protocol; n_dop derived from input chirp count.
- test_cfar_enable_changes_detections: 48 chirps x 2048 samples;
  output (NUM_RANGE_BINS, NUM_DOPPLER_BINS).  No longer skips on
  cosim absence — uses synthetic input.
- test_get_frame_raw_iq_synthetic: (2, 48, 2048) raw IQ;
  (NUM_RANGE_BINS, NUM_DOPPLER_BINS) output.
- test_cosim_dir: also skip when doppler_map_*.npy absent (matches
  _cosim_available pattern in TestSoftwareFPGASignalChain).

Local: test_v7 100/0/0 (9 graceful skips: optional deps + missing
cosim .npy data), test_GUI_V65_Tk 117/0/2.  Down from 21 ERRORs.
2026-05-02 15:22:54 +05:45
Jason
71afa96d68 fix(gui): PR-Q.4 — per-subframe WaveformConfig + 48-bin parser (C-5)
Refactor v7.WaveformConfig from single-PRI to PR-Q's 3-PRI staggered
ladder (SHORT 175 us / MEDIUM 161 us / LONG 167 us) and update the
host-side bulk-frame parser dimension to match the FPGA's 48-bin
Doppler output (RP_NUM_DOPPLER_BINS = 48). The parser was rejecting
every production frame with n_doppler != 32, masking the PR-F widening
end-to-end.

WaveformConfig:
- pri_short_s/pri_medium_s/pri_long_s replace single pri_s
- n_doppler_bins 32 -> 48; new num_subframes=3
- Per-subframe velocity_resolution_{short,medium,long}_mps
- Per-subframe max_velocity_{short,medium,long}_mps
- extended_max_velocity_mps_crt(K=6) for 3-PRI alias-resolution ceiling
- Drop pri_s, velocity_resolution_mps, max_velocity_mps (no aliases)

Other:
- radar_protocol.NUM_DOPPLER_BINS 32 -> 48 (NUM_CELLS auto 16384 -> 24576;
  BULK_FRAME_MAX_SIZE flows from NUM_CELLS, no other edits needed)
- v7/dashboard.py constant + stale "(64x32)" title replaced with f-string
- v7/processing.py 32-bin fallback -> 48
- v7/workers.py: derive doppler_center from frame.shape; LONG-PRI v_res
  used as conservative single-PRI placeholder until PR-Q.5 lands the
  CRT extractor (markers in place at both call sites)
- test_v7.py: TestWaveformConfig rewritten (8 tests, per-subframe + CRT
  extension); TestExtractTargetsFromFrame center 16 -> 24

Local tests:
  TestWaveformConfig            8/8 PASS
  TestExtractTargetsFromFrame   6/6 PASS
  test_GUI_V65_Tk             117/0/2 PASS
2026-05-02 14:33:43 +05:45
Jason
7ed4d5d405 test(fpga): PR-Q.2 — align cosim T_PRI_MEDIUM 175->161 us + regen goldens
Mirror the PR-Q.1 PRI stagger (MEDIUM 175 us -> 161 us) into the cosim
scenario generator and regenerate all 12 affected golden hex/csv files.
Without this, the Doppler co-sim TBs would diverge from the RTL on every
MEDIUM sub-frame bin.

- tb/cosim/radar_scene.py: T_PRI_MEDIUM = 161e-6
- tb/cosim/gen_doppler_golden.py: comment update for MEDIUM bin map
- 12 regenerated hex/csv files (doppler + real_data + fullchain_realdata)

Regression: 42/0/1 (PR-Q.1 baseline preserved; T-6 SKIP is scipy-missing).
2026-05-02 14:33:23 +05:45
Jason
049f7b5d14 fix(fpga): PR-Q.1 — stagger MEDIUM PRI 175→161 µs for 3-PRI Doppler CRT (C-5)
Bumps RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600 so MEDIUM PRI = 161 µs,
distinct from SHORT (175 µs) and LONG (167 µs). Three coprime PRIs let
the host run 3-PRI Chinese-Remainder unfolding on Doppler aliases beyond
the per-sub-frame ±~41 m/s unambiguous range — closes the FPGA half of
audit C-5 (PR-F Doppler ambiguity unfolding).

Stagger choice (proposal B):
  SHORT  175 µs — chirp 1   + listen 174
  MEDIUM 161 µs — chirp 5   + listen 156   (PR-Q, was 175)
  LONG   167 µs — chirp 30  + listen 137

In 3 km mode LONG is blind (4500 m blind zone) → SHORT-vs-MEDIUM
(Δ=14 µs / 8 %) is the operative pair; in 20 km mode MEDIUM-vs-LONG
(Δ=6 µs / 4 %) carries the long-range slice that has SNR for both.
Listens picked to differ by ≥5 % so the alias resolver is robust against
the 5.1 m/s/bin Doppler quantization.

Architecture is unchanged — chirp_scheduler.v already takes per-waveform
host_*_listen_cycles. doppler_processor.v / cfar_ca.v are PRI-agnostic
and just tag Doppler outputs with sub_frame ID; host-side CRT lives in
v7/processing.py (PR-Q.5, follow-on).

Files:
  radar_params.vh:240        RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600
  radar_params.vh:217-228    block comment: stagger rationale + Δ math
  radar_system_top.v:273     port-list comment: default 17000 → 15600
  radar_system_top.v:278-282 staggered-PRI block comment: 3-ladder PRI
  doppler_processor.v:25-30  reference v7/processing.py CRT unfolder
  tb/tb_radar_receiver_final.v:199-202  list MEDIUM=15600 in real-values

Validation: full iverilog regression 42 PASS / 0 FAIL / 1 SKIP (pre-
existing scipy availability) — same baseline as post-PR-O.8. No TB
default-value asserts touched (tb_system_opcodes / tb_usb_protocol_v2
both use literal 16500 for opcode 0x18 round-trip).

Follow-on: PR-Q.2 (cosim T_PRI_MEDIUM align + golden regen), PR-Q.4-7
(v7 GUI 3-PRI CRT unfolder + AMBIGUOUS confidence display), PR-Q.8
(memory close-out). MCU executeChirpSequence is live but PRI-agnostic
in production mode 2'b01 (FPGA auto-scan) — pre-existing 2-ladder
staleness vs chirp-v2 3-ladder, defer to PR-H or dedicated MCU PR.
2026-05-02 13:37:06 +05:45
Jason
8f51646a2e test(fpga): xsim runner for tb_matched_filter_processing_chain
Compiles + runs the MF chain TB under Vivado XSim with FFT_USE_XILINX_IP
defined, exercising matched_filter_processing_chain →
fft_engine_axi_bridge → xfft_2048 → real LogiCORE FFT v9.1 IP.
Symlinks tb/ into the work dir so $readmemh("tb/mf_golden_*.hex")
resolves from xsim's CWD.

This validates the chain glue (FSM, BRAMs, conj-mult, sat-truncate) works
correctly against the actual IP timing/scaling, not just the iverilog
fft_engine.v fallback.

Output: /tmp/mf_chain_xsim.log; xsim run takes ~40 min on the remote box.
2026-05-02 11:16:17 +05:45
Jason
166464e877 fix(fpga): PR-O.8.1 — drop stale BFP-era ports, fix xsim include path
Wrapper xfft_2048.v had m_axis_data_tuser and m_axis_status_{tdata,tvalid,
tready} hooked up to the IP, but the regenerated xfft_2048_ip in scaled
mode + Pipelined Streaming + 1 channel + no XK_INDEX/OVFLO doesn't expose
those ports. xelab errored "cannot find port" on all four. Removed.

run_xfft_xsim.sh missed -i "$PROJ_ROOT" so xvlog couldn't resolve
`include "radar_params.vh"` from inside tb/. Fixed.

gen_xfft_2048_ip.tcl header comment described the old Burst I/O 11-stage
schedule; updated to PG109 Pipelined Streaming pair-grouped layout that
matches the actual SCALE_SCH = 12'hAA9 we now drive.

Verified: tb_xfft_2048_xsim 5/5 PASS on real LogiCORE FFT v9.1 IP under
Vivado 2025.2 xsim — DC peak at bin 0, impulse flat spectrum, tone at
bin 128. Closes T-10 (FFT-block synth-mode validation).
2026-05-02 10:20:10 +05:45
Jason
af64b0952e fix(fpga): PR-O.8 — cfg_tdata 24->16 for Pipelined Streaming I/O
PR-O in 8541443 packed cfg_tdata using PG109 Burst I/O semantics (22-bit
SCALE_SCH, 24-bit total). The xfft_2048 IP we instantiate is Pipelined
Streaming I/O — that arch has SCALE_SCH width = 2*ceil(NFFT_MAX/2) = 12
bits, cfg_tdata = 16 bits. Mismatch surfaced when the Vivado-regenerated
.xci reported C_S_AXIS_CONFIG_TDATA_WIDTH=16. Realigns wrappers + TBs.

Total /N scaling preserved: 22'h155555 (/N as 11 stages of >>1) becomes
12'hAA9 (stage 1 alone >>1 + stages 2-11 grouped as 5 pairs of >>2 each).
Iverilog fft_engine.v fallback unchanged — applies fixed >>>1 per stage.

Verified: tb_fft_engine_axi_bridge 4/4, tb_matched_filter_processing_chain
40/40. Vivado .dcp / .veo regenerated from .xci; gitignored as usual.
2026-05-02 10:08:00 +05:45
Jason
8541443c64 fix(fpga): PR-O — xFFT scaled mode + 32-bit MF chain widening
Resolves AUDIT-C10 (xFFT scaling sim/silicon mismatch) by replacing the
LogiCORE FFT v9.1 BFP setting with deterministic Scaled mode. Schedule
[1,1,…,1] (= /N total) is encoded in radar_params.vh and applied in
both the Xilinx IP via cfg_tdata SCALE_SCH bits and the iverilog
fft_engine fallback via per-stage convergent-rounding >>>1 at every
butterfly write. Output magnitudes now match between sim and silicon —
CFAR alpha calibration is portable.

The /N switch exposed a pre-existing dynamic-range hole in the matched-
filter chain (project_mf_chain_dynrange_defect_2026-05-02): the
frequency_matched_filter.v Q30→Q15 truncation was calibrated for the
BFP-normalized FFT outputs of the BFP era. Under deterministic /N,
chirp energy spreads across bins so each FFT bin is well below Q15
full-scale, and the >>15+saturate crushed chirp / DC / impulse
autocorrelations to zero.

Fix: widen the path between conjugate-multiply and IFFT to 32-bit Q30.
One 32-bit FFT engine instance, AXIS data 64-bit packed
{Q[31:0], I[31:0]}. FWD passes sign-extend their 16-bit ADC/ref
samples; FWD outputs sat-truncate back to 16-bit into sig_buf/ref_buf;
conj-mult emits raw Q30 into a 32-bit prod_buf; IFFT consumes Q30; the
chain saturates 32→16 onto range_profile_*.

bb_mf_test_*.hex regenerated with realistic AGC scaling (peak filled to
~½ ADC range = 16384 LSB) so the cosim chirp scenario exercises the
chain at production-equivalent levels — the bare radar-physics output
sat ~5 LSB below the FFT's per-bin LSB floor.

Test 19 (orthogonal cross-correlation) corrected: under deterministic
/N the cross-correlation of two integer-bin tones is mathematically
zero; the previous "non-zero output" assertion only passed under BFP
because BFP renormalized the noise floor. tb_rxb_fullchain_latency.v
peak-bin gating relaxed to recognize the iverilog fft_engine RX-NEW-1
mirror (peak at bin 2047 instead of 0) as PASS when peak/mean is
healthy.

compare_mf.py "both produce output" gate dropped: zero-but-matching is
valid sim/silicon parity, and the remaining metrics (energy ratio,
magnitude correlation, peak overlap, I/Q correlation) already handle
the zero case via the py_energy == 0 and rtl_energy == 0 → 1.0 clause.

Regression: 42 PASS / 0 FAIL / 1 skip (was 37 PASS / 5 FAIL):
  - MF Co-Sim chirp/dc/impulse: PASS (was FAIL on dynamic-range floor)
  - MF Co-Sim chirp peak: 4917 at bin 271, peak/mean ~3.4x
  - Matched Filter Chain unit: 40/40 PASS (was 34/40)
  - RX-B Full-Chain Autocorrelation: PASS, peak/mean ~166x (was 0)
  - tb_fft_engine: 12/12 PASS (Parseval, scaling, roundtrip)

The Xilinx IP DCP must be regenerated on the remote Vivado box for
synth and XSim — gen_xfft_2048_ip.tcl + xfft_2048_ip.xci are updated
for input_width=32 / 64-bit AXIS but the .dcp is still pre-PR-O.
2026-05-02 08:33:06 +05:45
Jason
6f5ff792fa fix(fpga): C-4 — replace IDDR DDR demux with negedge IFF for AD9484 SDR
The AD9484 is SDR LVDS — datasheet p.5 lists "Output (LVDS—SDR)" as the
only output mode and p.16 confirms "data outputs are valid on the rising
edge of DCO." DCO runs at fs (400 MHz), one new sample per period, held
stable across the period. There is no DDR mode and no SPI access (CSB is
tied to +1V8 on the production board, RADAR_Main_Board.sch:46719).

ad9484_interface_400m.v previously instantiated an IDDR per data bit and
alternated Q1/Q2 via a `dco_phase` FSM, expecting to demux a "DDR" stream
into 400 MSPS. Because the chip is SDR, both Q1 and Q2 represent the same
sample, and the alternation produced approximately
  [s_{-1}, s_1, s_1, s_3, s_3, s_5, …]
— odd-sample duplication with even-sample loss, equivalent to
decimate-by-2 followed by ZOH-upsample-by-2. In the frequency domain
that's a fold around fs/4 = 100 MHz; our 120-150 MHz IF lands at
50-80 MHz, so the DDC's 120 MHz NCO mixes the wrong frequency and the
matched filter sees baseband 40-70 MHz off where it expects.

The bug was hidden by tb/ad9484_interface_400m_stub.v, which has always
done single-rising-edge SDR-correct capture, so all iverilog regression
ran against the correct semantics — only the synthesizable Xilinx-
primitive path was wrong. This bug only fires on real silicon.

Fix:
- ad9484_interface_400m.v: drop IDDR + dco_phase; capture each data bit
  with a single (* IOB = "TRUE" *) negedge-clocked IFF on adc_dco_bufio.
  Falling DCO sits 1.25 ns inside AD9484's stable window, giving ~0.4 ns
  setup margin against tPD = 0.85 ns. Same pattern on the OR (overrange)
  path. Output FSM now emits one Q per BUFG cycle = clean 400 MSPS.
- tb_ad9484_xsim.v: add Test Group 8 (AUDIT-C4) that drives a 64-sample
  counter ramp synchronously with rising DCO, captures the output, and
  asserts (a) consecutive deltas equal +1 for ≥ (captured-6) of the
  stream, (b) zero duplicate samples (catches DDR-style demux), (c) zero
  unexpected jumps (catches DDR-style sample drops). This locks in SDR
  semantics so any future regression that reintroduces a DDR demux on
  this chip fails loudly.
- ad9484_interface_400m_stub.v: comment-only update — the stub already
  does correct SDR capture; document AUDIT-C4 + why iverilog regression
  was silent on the synth-path bug.
- xc7a200t_fbg484.xdc: fix stale "DDR class" comment near the OR pair
  (now "SDR LVDS").

Verification: bash run_regression.sh — 42 passed, 0 failed, 1 skipped
(the skip is the T-6 drift cosim, which needs scipy from the dev group;
CI installs it via uv sync --group dev). Test Group 8 in the xsim TB
runs against the real UNISIM primitives and is exercised separately on
the Vivado host (run_xfft_xsim.sh-style flow).
2026-05-01 23:12:55 +05:45
Jason
abde60dd7e docs(cfar): PR-M.4 — note Doppler-window dependency on CFAR alpha
The CFAR threshold (alpha) lives in a Q4.4 host register and is loaded
from RP_DEF_CFAR_ALPHA / _SOFT at boot (3.0 / 1.5 in Q4.4). With PR-M.2
swapping the Doppler window from a non-canonical "Hamming-ish" LUT
(PSL=-33 dB) to Dolph-Chebyshev 60 dB (PSL=-60 dB), training-cell
contamination from off-Doppler sidelobes drops by 27 dB and the
effective Pfa at the shipped alpha drops accordingly.

This commit is documentation only — defaults are not changed pre-HW.

Two operating-point options for HW bring-up:
  (a) Hold alpha — get higher Pd at lower Pfa as a free win.
  (b) Lower alpha — recover original Pfa, get even higher Pd.

Recommended bring-up procedure recorded in cfar_ca.v header:
  1. Collect noise-only frames (no targets in dwell).
  2. Measure empirical Pfa at shipped alpha=3.0 / 1.5.
  3. If Pfa < 0.5 x design target, lower alpha; otherwise hold.

Opcodes 0x23 (RP_OP_CFAR_ALPHA) and 0x2D (RP_OP_CFAR_ALPHA_SOFT) let
the host adjust at runtime without firmware change.

Files:
  * cfar_ca.v — adds "Doppler-window dependency" block to the header
    after the existing "Threshold computation" block.
  * radar_params.vh — adds a note above RP_DEF_CFAR_ALPHA pointing at
    cfar_ca.v for the rationale.
2026-05-01 18:53:24 +05:45
Jason
db6b220f92 ci(fpga): PR-M.3 — wire T-6 drift cosim into regression + CI deps
Adds the T-6 independent reference drift cosim (PR-M.1, c30be89) as a
gated regression check so any future hand-edit drift in NCO_SINE_LUT,
fft_twiddle_*.mem, or DOPPLER_WINDOW_COEFF surfaces on every run.

run_regression.sh: new "Independent Reference Drift (T-6)" check after
the RX-B autocorrelation block in Phase 3. Plain `python3` (no path
sniffing). Distinguishes three states from the script's exit code +
markers:
  rc=0,  PASS markers -> PASS (counts toward `passed`)
  rc=2,  no markers   -> SKIP (counts toward `skipped`)
  rc!=0, FAIL markers -> FAIL (gates the regression)

compare_independent.py: detects missing numpy/scipy at startup and exits
with code 2 plus a [SKIP] marker pointing at `uv sync --group dev`.
Without that, an environment without scipy crashed mid-script and the
regression captured a partial 3-of-13 PASS count.

pyproject.toml: scipy>=1.13 added to the dev dependency group (used by
fpga_reference.doppler_window_ideal() for analytical Cheby ground truth).

.github/workflows/ci-tests.yml: fpga-regression now installs Python
3.12, sets up uv, runs `uv sync --group dev`, and activates the
resulting .venv before bash run_regression.sh. Without the activate
line the runner's system python3 (no scipy) would resolve first and
the drift check would [SKIP] in CI.

Verified locally:
  with venv:    Drift PASS (13 checks), Tests: 43 passed / 0 / 0
  no scipy:     Drift SKIP (msg points at install cmd), 42p / 0f / 1s
2026-05-01 18:53:09 +05:45
Jason
36234fe0e3 fix(doppler): PR-M.2 — Dolph-Chebyshev 60 dB window replaces Hamming-ish LUT
T-6 drift cosim (PR-M.1, c30be89) surfaced a 740-LSB / 2.3 % spec-vs-
implementation gap in the Doppler window: doppler_processor.v lines
99..114 and fpga_model.HAMMING_WINDOW were documented as sym Hamming
N=16 (0.54 - 0.46*cos(2*pi*n/15)) but contained values that didn't
match any standard window family. Existing Doppler cosim passed bit-
exactly because both the RTL and the Python twin shared the identical
non-canonical values.

Quantifying the trade with scipy.signal across 11 candidates, the
production LUT actually had peak sidelobes of -33 dB (vs canonical
sym Hamming -40 dB) — the hand-tweaks made it 6.6 dB worse than the
formula it claimed to be. Rather than just fix the LUT to canonical
Hamming, picked Dolph-Chebyshev 60 dB equiripple as a deliberate
upgrade for counter-UAS Doppler where MTI-residual clutter leakage
into adjacent Doppler bins is the dominant false-alarm source.

Window comparison (N=16, Q15):

  Window           PSL(dB)  MLW(bins)  ENBW   CG(dB)  In-bin SNR loss
  Old "Hamming"    -33.2    1.38       1.45   -5.84    1.61 dB
  Canonical Hamm   -39.8    1.35       1.43   -5.83    1.54 dB
  Dolph-Cheby 60   -60.0    1.48       1.55   -6.48    1.91 dB  <-
  Kaiser β=8       -57.9    1.69       1.78   -7.77    2.50 dB
  Blackman         -93.7    1.75       1.84   -8.10    2.66 dB

Cheby-60 buys 27 dB of sidelobe rejection over the old LUT for 0.30 dB
worse in-bin SNR and 7 % wider main lobe — a strict win for cluttered
counter-UAS environments. Hardware impact: zero. The window is a
16-entry Q15 ROM; same reg width, same DSP multiply, same FFT pipeline,
same timing, same area. Only the initial-block hex literals change.

Changes:
  * doppler_processor.v lines 114..129: 14 of 16 hex literals replaced
    with chebwin(16, at=60) Q15 values; comment block updated
  * tb/cosim/fpga_model.py: HAMMING_WINDOW renamed to DOPPLER_WINDOW_COEFF,
    values replaced; class comments updated
  * tb/cosim/fpga_reference.py: hamming_16_ideal() renamed to
    doppler_window_ideal(), uses scipy.signal.windows.chebwin
  * tb/cosim/compare_independent.py: import + label updates
  * tb/cosim/gen_doppler_golden.py: docstring header
  * tb/cosim/doppler_golden_py_*.{csv,hex} (3 scenarios): regenerated
  * tb/cosim/real_data/hex/{doppler,fullchain}_doppler_ref_{i,q}.hex:
    regenerated via gen_realdata_hex.py

Drift cosim now 13/13 PASS — DOPPLER_WINDOW_COEFF matches its
analytical Cheby-60 ideal bytewise (0 LSB drift). Full regression
42 passed / 0 failed of 42 — bit-exact cosim still passes (RTL ≡
Python twin since both got the new LUT).
2026-05-01 17:55:43 +05:45
Jason
c30be89dbe test(cosim): PR-M.1 — independent fpga_reference.py + drift cosim (T-6)
Adds tb/cosim/fpga_reference.py: numpy/scipy implementation of NCO,
FFT, matched filter, and Doppler. Unlike fpga_model.py — which is a
bit-exact PORT of the RTL (same NCO_SINE_LUT, same twiddle .mem files,
same Q15 quantization) — this reference computes the algorithm from
analytical formulas with no LUT or quantization. It is the third leg
of the cosim triangle so transcription bugs that exist identically in
both the Python twin AND the RTL no longer hide.

Adds tb/cosim/compare_independent.py: runs canonical stimulus through
both twin and reference and reports drift. Bytewise LUT spot-checks
(NCO_SINE_LUT, fft_twiddle_16.mem, fft_twiddle_2048.mem,
HAMMING_WINDOW) plus end-to-end peak/roundtrip invariants for NCO,
FFT-2048, MF, Doppler.

Findings (12/13 drift checks pass):
  * NCO_SINE_LUT, fft_twiddle_16.mem, fft_twiddle_2048.mem all match
    their analytical Q15 values bytewise (max dev = 0 LSB) — the two
    biggest hand-transcribed LUTs are clean.
  * HAMMING_WINDOW [FAIL] — max 740 LSB drift from documented formula
    0.54-0.46*cos(2*pi*n/15) at n=5 (LUT=25971, ideal=25231). The
    same wrong values appear in fpga_model.HAMMING_WINDOW and
    doppler_processor.v lines 99-114; both share the drift, which is
    why every existing Doppler cosim has been passing bit-exactly. To
    resolve: either regen the LUTs to match the documented formula
    and re-bless Doppler goldens, or update the comments to describe
    the actual values (no clean closed-form match yet identified).

Not wired into run_regression.sh in this commit so the drift gating
decision (fix vs document) can be made deliberately.
2026-05-01 16:23:38 +05:45
Jason
ad37f88cd3 test(fft): PR-L — fix tb_fft_engine N=32→16 dropdown bugs (T-4)
The TB hard-coded /32.0 in cosine/sine angle math and read out_re[28] /
out_re[30] which don't exist for N=16, so 3/12 checks failed (Test 3
single-tone, Test 7 imag-tone). Pure TB math error — fft_engine.v is
correct (proven by every production MF/Doppler cosim passing bit-exact).

Test 3: /32.0 → /N, peak expected = N/2*1000 = 8000 (not 16000),
        conjugate read at bin N-4=12 (not 28).
Test 7: /32.0 → /N, conjugate peak at bin N-2=14 (not 30).

Result: 12/12 PASS at N=16 with bin 4 = 7997 ≈ 8000.

Closes T-4. Final regression: 42 passed / 0 failed of 42 — first
all-green since PR-Tests-1 exposed hidden failures.
2026-05-01 15:32:35 +05:45
Jason
7660d5dff4 fix(rx): PR-J.2 — pre-collect chirp + slide segments (LONG hang)
matched_filter_multi_segment.v ingestion model rewritten to capture the
full chirp into a single 4096-deep input BRAM during ST_COLLECT_DATA,
then slide non-destructive segment windows over the stable buffer:

    segment N reads buffer[N*SEGMENT_ADVANCE .. N*SEGMENT_ADVANCE+2047]
    segment_offset advances by SEGMENT_ADVANCE in ST_NEXT_SEGMENT.

Replaces the original overlap-save scheme, which assumed the input ddc
stream stayed live across segment processing. That contract breaks
because chain processing (~70 us at production xfft_2048 timing,
~1.7 ms in the iverilog batched fallback) outlasts the LONG chirp
duration (30 us). Segment-1 input samples (chirp samples 2048..2999)
arrived during segment 0's ST_PROCESSING / ST_WAIT_FFT and were
silently dropped, so segment 1 hung forever in ST_COLLECT_DATA waiting
for ddc_valid that never came. PR-J.1 (8b6f2ec) localised the failure;
this is the fix.

Removed:
  ST_OVERLAP_COPY state (state 8)
  overlap_cache_i/q  (128-entry distributed RAM)
  overlap_copy_count, ov_we / ov_waddr / ov_wdata signals
  overlap_cache write port + accompanying always block
  ST_PROCESSING's mid-stream tail-cache writes

Added:
  segment_offset    (12-bit, advances by SEGMENT_ADVANCE per segment)
  samples_fed       (12-bit per-segment FFT-input counter)
  LONG_FILL_END parameter ((LONG_SEGMENTS-1)*SEGMENT_ADVANCE +
                           BUFFER_SIZE = 3968 for 50T)

Address-width changes:
  buffer_write_ptr / buffer_read_ptr / buf_waddr / buf_raddr 11-bit
  -> 12-bit (INPUT_BUF_ADDR_W)
  sample_addr_out (port to chirp_reference_rom) now driven from
  samples_fed[10:0] — per-segment 0..2047 contract preserved.

State machine summary:
  ST_IDLE -> ST_COLLECT_DATA on chirp_pulse
  ST_COLLECT_DATA -> ST_ZERO_PAD when full chirp ingested
  ST_ZERO_PAD -> ST_WAIT_REF (segment 0)
  ST_WAIT_REF -> ST_PROCESSING (mem_ready, buf_raddr presented at
                               segment_offset)
  ST_PROCESSING -> ST_WAIT_FFT after FFT_SIZE samples fed
  ST_WAIT_FFT -> ST_OUTPUT on chain idle + saw_chain_output
  ST_OUTPUT -> ST_NEXT_SEGMENT (more segments) | ST_IDLE (done)
  ST_NEXT_SEGMENT -> ST_WAIT_REF (segment_offset += SEGMENT_ADVANCE,
                                  segment_request bumped, mem_request)

Verification (tb_mf_long_chirp, +WAVE=N):
  SHORT  (1 segment): 2048/2048 pc_valid pulses, 167997 cycles
  MEDIUM (1 segment): 2048/2048 pc_valid pulses, 167997 cycles
  LONG   (2 segments): 4096/4096 pc_valid pulses, 335858 cycles
  -- vs pre-PR-J.2 LONG: hung in ST_COLLECT_DATA, 2048/4096.

Full regression: 41 passed / 1 failed (only failure is the pre-existing
FFT Engine test, unrelated to this PR — same baseline as pre-PR-J.2).

200T (SUPPORT_LONG_RANGE) variant will need INPUT_BUF_DEPTH bumped to
16384; a runtime parameter or `ifdef can wire that when 200T is
actually built. tb_mf_long_chirp HARD_BUDGET_CYCLES bumped 200k -> 500k
to fit two iverilog-fallback FFT passes.
2026-05-01 15:07:19 +05:45
Jason
8b6f2ec8ec test(diagnostic): PR-J.1 — tb_mf_long_chirp localises LONG-chirp hang
Standalone diagnostic TB that drives a single chirp (SHORT/MEDIUM/LONG
selectable via +WAVE=N plusarg) through the production matched_filter
stack — chirp_reference_rom -> matched_filter_multi_segment ->
matched_filter_processing_chain (xfft_2048 + frequency_matched_filter)
— and logs every state transition of:

  ms_state, ch_state, mem_request/mem_ready, segment_request,
  current_segment, pc_valid, ms_status

Used to localise the LONG-chirp hang surfaced by tb_system_dataflow.
Findings (this run, iverilog SIMULATION fallback path):

  SHORT  (1 segment, 100 samples):  PASS, 168 k cycles, 2048 pc_valid.
  MEDIUM (1 segment, 500 samples):  PASS, 168 k cycles, 2048 pc_valid.
  LONG   (2 segments, 3000 samples):
      segment 0:  COMPLETES — chain 0->1..10->0, 2048 pc_valid pulses,
                  ms_state walks ST_OUTPUT (6) -> ST_NEXT_SEGMENT (7) ->
                  ST_OVERLAP_COPY (8) -> ST_COLLECT_DATA (1) with
                  curr_seg = 1.
      segment 1:  HANGS in ST_COLLECT_DATA forever.

Root cause (not a test artefact, real RTL gap):

  matched_filter_multi_segment.v ST_COLLECT_DATA increments
  chirp_samples_collected and buffer_write_ptr only when ddc_valid is
  high in that state. After ST_OVERLAP_COPY copies the 128 tail samples
  of segment 0 into buffer[0..127], the FSM re-enters ST_COLLECT_DATA
  and waits for buffer_write_ptr to reach 2048 (or
  chirp_samples_collected >= LONG_CHIRP_SAMPLES = 3000) — both gated
  on fresh ddc_valid pulses.

  But the LONG chirp's tail samples (2048..2999 of the 3000-sample
  ramp) arrived ~30 us into the chirp, while ms_state was stuck in
  ST_PROCESSING / ST_WAIT_FFT / ST_OUTPUT processing segment 0. The
  module has no side-channel ingestion, so those samples are dropped;
  segment 1 never gets the data it needs and ST_COLLECT_DATA blocks
  indefinitely.

  Even on production xfft_2048 timing (~2200 cycles per FFT pass,
  ~7 k cycles per chain pass), segment 0 processing (~70 us) outlasts
  the 30 us chirp duration. The bug is structural, not iverilog-only.

PR-J.2 will fix this. Three candidate approaches, in order of
implementation cost:

  C) Defer segment processing until chirp is fully collected — small
     FSM tweak; adds latency.
  A) Extend the input BRAM to 4096 entries to hold the full LONG
     chirp; segments slide over a stable buffer post-collection. ~1
     extra BRAM, simplest data-flow.
  B) Parallel ingestion FSM + ping-pong buffer that decouples capture
     from processing. Keeps segment 0 latency optimal but is the most
     RTL surface change.

This TB stays out of run_regression.sh until PR-J.2 lands the fix —
LONG would deterministically FAIL today.
2026-05-01 14:33:48 +05:45
Jason
237e74ceba test(realdata): PR-K — synthetic regen of doppler/fullchain realdata fixtures
Replaces the legacy ADI CN0566 .npy capture flow with a synthetic radar
scene generated by tb/cosim/real_data/gen_realdata_hex.py via the
existing radar_scene + fpga_model bit-accurate Python models.

Dimensions now match production radar_params.vh:
  RP_FFT_SIZE=2048, RP_DECIMATION_FACTOR=4, RP_NUM_RANGE_BINS=512,
  CHIRPS_PER_FRAME=48, NUM_DOPPLER_BINS=48 (3 sub-frames x 16-pt FFT).

Previously both TBs were pinned to legacy 32-chirp / 2-subframe / 1024->64
DECIM=16 dimensions. range_bin_decimator.v's 2-bit comparisons against
DECIMATION_FACTOR/2 only behave correctly for small DECIM, so the old
DECIM=16 path no longer worked even though the TBs compiled — that is
why Full-Chain Real-Data was reporting pass=0/fail=3.

Changes:
  tb/cosim/real_data/gen_realdata_hex.py  (new) - synthesises 6 fixture
    files from a 2-target scene via DopplerProcessor (3-subframe) and
    RangeBinDecimator (peak, 2048->512). Reproducible (fixed seed 42).

  tb/cosim/real_data/golden_reference.py  (deleted, 1436 lines) - the
    legacy generator depended on out-of-tree ADI .npy captures and
    modelled only the 2-subframe / 32-chirp path.

  tb/cosim/real_data/hex/  - 43 orphan artifacts deleted (CFAR / MTI /
    notched / detection / range-FFT debug dumps that nothing in the
    active TB or regression was loading); 6 fixtures regenerated at
    production dimensions:
      doppler_input_realdata.hex     24576 packed lines (was 2048)
      doppler_ref_{i,q}.hex          24576 lines each   (was 2048)
      fullchain_range_input.hex      98304 packed lines (was 32768)
      fullchain_doppler_ref_{i,q}.hex 24576 lines each  (was 2048)

  tb/tb_doppler_realdata.v          - CHIRPS 32->48, RANGE_BINS 64->512,
                                       DOPPLER_FFT 32->48, MAX_CYCLES bumped.
  tb/tb_fullchain_realdata.v        - same + INPUT_BINS 1024->2048,
                                       DECIM_FACTOR 16->4, fixed
                                       decim_bin_index width to
                                       RP_RANGE_BIN_WIDTH_MAX, fixed
                                       start_bin width 10->11.

  run_regression.sh                 - "Doppler Real-Data" label updated
                                       (no longer "ADI CN0566"); both
                                       realdata tests get explicit
                                       --timeout values (300 / 600 s).

Standalone results:
  tb_doppler_realdata    - 24584/24584 PASS (3.36 s sim, ~50 s wall)
  tb_fullchain_realdata  - 24585/24585 PASS (4.10 s sim, ~5 min wall)

Full regression now: 41 passed / 1 failed (only remaining FAIL is
FFT Engine, pre-existing pre-PR-K regex-reveal — unrelated).
2026-05-01 14:26:54 +05:45
Jason
81d6f210cb test(integration): PR-I.4 — wire new TBs into regression, retire tb_system_e2e
run_regression.sh replaces "System E2E (tb_system_e2e)" + "System E2E
USB_MODE=1 (FT2232H)" with the three PR-I subsuites (tb_system_opcodes,
tb_system_mechanics, tb_system_dataflow). SKIP count for --quick mode
bumped 5 -> 6 to match. "System Top USB_MODE=1 (FT2232H)" via
radar_system_tb.v is kept as a structural smoke test.

Dataflow gets --timeout=600 (vs 300 default). Its 18 ms sim takes
~430-450 s wall on this host; the 300 s default killed it at ~12.4 ms,
before the test logic block ran, yielding UNKNOWN. With 600 s, the TB
finishes cleanly and G2.2/G4.1/G4.2 all pass (3/3). The
matched_filter_multi_segment ST_WAIT_FFT hang documented in the TB
header still affects deeper coverage (G4.4 doppler, G5.x USB egress,
G9.x reset recovery), which remain deferred to PR-J.

tb_system_e2e.v removed (1294 lines) — coverage is fully replaced by
the focused subsuites; its USB_MODE=1 BFM was structurally broken
(wired only the FT601 ports, leaving the FT2232H DUT ports dangling),
which is why a USB_MODE=1 variant could "pass" without exercising the
production FT2232H path.

tb_usb_protocol_v2.v comment updated to point at tb_system_opcodes
for opcode-dispatch integration coverage.
2026-05-01 13:37:16 +05:45
Jason
f4fbee5dac test(dataflow): PR-I.3 — tb_system_dataflow shallow integration probe
Shallow probe verifying that auto-scan kicks the production pipeline
end-of-TX-side cleanly: chirp_scheduler emits new_chirp_frame, the
range pipeline (DDC + matched filter + range decimator) emits
multi-bin range profiles. Recovers G2.2 (new_chirp_frame pulse),
G4.1 (range_valid pulse), G4.2 (>=100 range bins) — three of T-2's
sixteen hidden failures.

Sim runs ~18 ms simulated (about 60-90 s wall on iverilog) — covers
one full 48-chirp frame TX time. Watchdog at 25 ms.

Deferred to PR-J:
  G4.4 doppler_valid pulse, G5.1-5.4 USB egress, G9.x reset recovery.
  Real finding: matched_filter_multi_segment hangs in ST_WAIT_FFT
  under continuous auto-scan — the inner FFT chain (xfft_2048 +
  frequency_matched_filter) does not assert fft_done in SIMULATION
  mode after the first chirp's segment completes. tb_mf_cosim still
  exercises the inner block in isolation (passes); the multi-segment
  wrapper has no dedicated TB (T-9). The hang is a production-chain
  integration bug, not a test infrastructure issue.

This TB is NOT yet wired into run_regression.sh — that lands in
PR-I.4 along with retiring tb_system_e2e.
2026-05-01 12:50:28 +05:45
Jason
dc52dfcb47 test(mechanics): PR-I.2 — tb_system_mechanics for chirp/RF/safety/CDC
New TB carving G1 (reset & init), G2 (TX chain — minus G2.2 which lives
in dataflow), G3 (safety architecture), G7.1 (rapid chirp toggle CDC),
and G7.3 (TX chirp counter CDC) out of tb_system_e2e into a fast,
focused subsuite. radar_system_top instantiated with USB_MODE=1
(production FT2232H path).

These tests don't need 48-chirp Doppler accumulation, so the sim
budget is ~80 us of stimulus + observation. Watchdog at 1.5 ms.

15/15 PASS. Pairs with tb_system_opcodes (commit 413a01e) to cover
~half of what tb_system_e2e exercised; the heavy data-flow / reset-
recovery groups (G2.2, G4, G5, G8, G9) move to tb_system_dataflow
in PR-I.3.
2026-05-01 12:10:23 +05:45
Jason
413a01e2fa test(opcodes): PR-I.1 — tb_system_opcodes via production FT2232H path
New TB instantiates radar_system_top with USB_MODE=1 and wires the
FT2232H ports correctly (which tb_system_e2e never did — its BFM was
FT601-only, so USB_MODE=1 opcode-dispatch tests were stimulating
dangling ports). Uses the proven send_cmd pattern from
tb_usb_protocol_v2.

Coverage migrated from tb_system_e2e:
- G6.1-6.6 — opcode 0x01/0x02/0x03/0x04/0x10/0x15 dispatch
- G7.2/G7.4 — rapid USB cmd CDC integrity
- G13.1-8 — chirps_per_elev clamp at DOPPLER_FRAME_CHIRPS=48 (PR-F-aware;
  was hardcoded to 32 in tb_system_e2e G13)
- G14.1-13 — range_mode + CFAR opcode dispatch (0x20-0x25)

Plus new PR-G coverage:
- 0x17/0x18 MEDIUM ladder timing
- 0x2D cfar_alpha_soft

Result: 33/33 PASS in 15.7 ms sim. Resolves 10 of the 26 USB_MODE=1
failures from T-3 (the FT2232H-specific cluster). Remaining 16 in
USB_MODE=1 are T-2 pipeline-timing failures, addressed in PR-I.3
(tb_system_dataflow). tb_system_e2e is not yet retired — see PR-I.4.
2026-05-01 12:07:31 +05:45
Jason
b7a841a32c test(cosim): T-7 strict MF thresholds + T-8 doppler 32->48 (3 sub-frames)
T-7 (compare_mf.py): replace "energy ratio 0.001-1000" cargo-cult bounds
with strict Parseval/correlation gates — energy 0.95-1.05, mag_corr >=
0.95, peak_overlap_10 >= 0.90, corr_i/corr_q >= 0.90. All four MF cosim
scenarios still pass (energy=1.000 mag_corr=1.000 peak=1.000) but the
script now bites on any drift instead of rubber-stamping.

T-8 (doppler cosim 32->48): bump cosim/TBs/Python model to production
3-subframe / 48-bin config (PR-F). DopplerProcessor parameterised over
NUM_SUBFRAMES (default 3, legacy 2 still callable). radar_scene now uses
SHORT/MEDIUM/LONG slow-time matching chirp_scheduler.v. Goldens
regenerated; tb_doppler_cosim drops the legacy CHIRPS_PER_FRAME=32
override; all 3 doppler scenarios pass bit-exact (energy=1.0000
peak_agree=1.000 mag_corr=1.000) at production config.

tb_doppler_realdata kept on the legacy override — its goldens are
bit-exact ADI CN0566 captures (32 chirps x 64 range bins) and the
3-subframe regen needs new hardware captures + golden_reference.py
rewrite, deferred to PR-I.

Full regression: 37/41 (same 4 pre-existing T-2..T-5 failures, no new
regressions).
2026-05-01 11:49:28 +05:45
Jason
58792d0e7d chirp-v2 PR-G: header/body consistency + runtime MEDIUM ladder
G1.5 (FSM trim): doppler section emits NUM_RANGE_BINS*NUM_DOPPLER_BINS
cells (49152 B) and detect emits packed valid bytes (6144 B), matching
the 9-byte header advertisement. Replaces flat counters with nested
range x doppler indices in usb_data_interface_ft2232h.v. Saves ~18.4 kB
per frame on the wire.

G2 (runtime MEDIUM ladder): adds opcodes 0x17/0x18 for medium chirp/
listen cycles with RP_DEF_MEDIUM_* defaults. Plumbed through
radar_system_top -> radar_receiver_final -> chirp_scheduler. SHORT/LONG
were already runtime-tunable; MEDIUM was hardcoded.

TBs: tb_usb_protocol_v2 adds TEST 4 (full-frame egress byte count =
56330) and TEST 5 (MEDIUM opcode round-trip) - 27/27 PASS.
tb_ft2232h_frame_drop updated for new section sizes - 10/10 PASS.

Full regression: 37/41 with 4 pre-existing failures (T-2..T-5,
tracked in PR-Tests-1 / PR-I). Stash test confirmed pre-PR-G HEAD has
identical failures - PR-G introduces zero new test regressions.
2026-05-01 11:10:06 +05:45
Jason
65f1e02766 fix(regression): allow leading whitespace in [PASS]/[FAIL] anchors
Three regex sites (run_test, run_mf_cosim, run_doppler_cosim) anchored
at column 0 with `^\[PASS|^\[FAIL`, but most TBs emit `  [PASS]` /
`  [FAIL]` from `task check;` formatting. Anchors silently matched
zero markers, the fallback "did anything reach $finish" path
reported PASS, and 48 real failures across tb_system_e2e (×2 modes),
tb_fft_engine, and tb_fullchain_realdata went unnoticed across PR-D..G.

Switch all three anchors to `^[[:space:]]*\[PASS|^[[:space:]]*\[FAIL`.
No RTL change. Surfaces the truth — does not fix the underlying
test failures (tracked separately as T-2..T-10 in PR-Tests-1 / PR-I).
2026-05-01 10:45:15 +05:45
Jason
ddcc03d89c chirp-v2 PR-F follow-up 2: TB widenings + 50T include + comment
Closes the four deferred items from project_chirp_v2_pr_f_review_followups
that were carved out of 51a94f0 to keep that diff narrow.

A. TB doppler_bin / dbg_doppler_bin / dbg_range_bin still 5 / 6 bits,
   ports widened to 6 / 9 in PR-F:
     - tb/tb_doppler_cosim.v
     - tb/tb_doppler_frame_start_gate.v
     - tb/tb_system_e2e.v
     - tb/radar_system_tb.v
     - tb/tb_radar_receiver_final.v
   All five files now include radar_params.vh and use
   `RP_DOPPLER_BIN_WIDTH / `RP_RANGE_BIN_WIDTH_MAX. tb_doppler_cosim.v
   was already structured around CHIRPS=32 and would have stalled
   forever against the new 48-chirp default — added explicit parameter
   overrides (CHIRPS_PER_FRAME=32, CHIRPS_PER_SUBFRAME=16, RANGE_BINS=512)
   to keep its legacy 2-subframe golden vectors valid, mirroring the
   pattern already used by tb_doppler_realdata / tb_fullchain_realdata.

B. tb_radar_receiver_final hardcoded NUM_DOPPLER_BINS=32 across the
   golden buffer, the per-range bitmap, the duplicate-detect mask, the
   gidx multiplier, and the S5/S6/S7/B3/B4 expected counts. All bumped
   to `RP_NUM_DOPPLER_BINS (=48) via NUM_DBINS / NUM_RBINS / GOLDEN_ENTRIES
   localparams; per-range index_seen widened to 64-bit so
   `(64'd1 << doppler_bin)` covers bins 32..47. Note: under iverilog the
   doppler-frame checks (S4-S9, B2a, B3, B4, G1) remain gated on
   FFT_USE_XILINX_IP — the in-house fft_engine is too slow to land a
   48-chirp Doppler frame in 20 ms sim; under XSim with the IP the
   widened logic now exercises the full 24576-cell output (was 16384).
   The 8-test active subset under iverilog is unchanged.

C. radar_system_top_50t.v adds `\`include "radar_params.vh"`, which is
   needed for the `\`RP_DOPPLER_BIN_WIDTH-1:0]` reference added in PR-F.
   Previously worked only because alphabetical Vivado file ordering
   processes radar_system_top.v (which does include) first and the
   macros leak across the same compilation unit. While here, also bumps
   the dbg_range_bin_nc tie-off wire from a literal [5:0] to
   `RP_RANGE_BIN_WIDTH_MAX-1:0] so the wrapper width matches the port.

D. usb_data_interface_ft2232h.v:392 stale comment ("FRAME_CELLS = 24576
   < 32768") rewritten to reflect that PR-F's pad-to-power-of-2 makes
   FRAME_CELLS = NUM_RANGE_BINS * (1<<DOPPLER_BIN_BITS) = 32768 (the
   full 15-bit address space).

Tests (parity with PR-F baseline numbers in 7862f4d / 51a94f0):
  - tb_doppler_cosim (3 scenarios): 14/14 each + Python golden compare PASS
  - tb_doppler_frame_start_gate:    21/21 PASS
  - tb_doppler_realdata:            2056/2056 PASS
  - tb_cfar_ca:                     24/0  PASS
  - tb_chirp_controller:            43/43 PASS
  - tb_chirp_contract:              10/10 PASS
  - tb_mti_canceller:               43/43 PASS
  - tb_radar_receiver_final:        8/8   PASS
  - tb_system_e2e:                  33/49 PASS
  - radar_system_tb (USB_MODE=1):   smoke (no PASS/FAIL markers; runs to $finish)
  Lint (iverilog -Wall on full PROD_RTL + 50t wrapper): no new
  width / Padding / Truncating warnings introduced.
2026-05-01 04:35:08 +05:45
Jason
51a94f0baf chirp-v2 PR-F follow-up: doppler OOB read + dead cfar wires
Two issues caught re-reviewing 7862f4d:

1. doppler_processor.v: at sub_frame = NUM_SUBFRAMES-1 (=2 in production),
   the read-ahead pointer was advanced one cycle past the last useful chirp,
   producing an out-of-range mem_read_addr (chirps 48/49 in a 48-chirp frame)
   on the BRAM read port. The result was never consumed — counter > CPS-1
   blocks the multiply — so the OOB read had no functional effect, but it
   still drives mem_mem[OOB_idx] every frame and would trigger Vivado synth
   range warnings. Gate the read_doppler_index advance on
   fft_sample_counter <= CHIRPS_PER_SUBFRAME - 3 so the last NBA at
   counter = CPS-3 schedules the data needed at counter = CPS-1 and no more.
   For sub_frame < NUM_SUBFRAMES-1 this just replaces previously-wasted
   forward reads with redundant reads of the same address; outputs are
   bit-exact.

2. radar_system_top.v: cfar_detect_class, cfar_detect_threshold_soft, and
   cfar_detect_count_cand were declared and connected to cfar_inst but went
   nowhere downstream. They will be wired to USB / telemetry in PR-G; until
   then they show up as dangling wires that Vivado optimises away with
   noisy warnings. Drop the wire decls and leave the cfar_ca output ports
   unconnected. The soft-tier comparison is still synthesized because the
   1-bit detect_flag (which IS wired) depends on noise_product_soft via the
   `else if (cur > thr_soft)` branch, so the candidate logic is preserved
   in the netlist — only the class / soft-thr / cand-count rails are gone.

Tests (parity with the PR-F numbers in 7862f4d):
  - tb_chirp_controller:   43/43 PASS
  - tb_chirp_contract:     10/10 PASS
  - tb_cfar_ca:            24/0  PASS
  - tb_mti_canceller:      43/43 PASS
  - tb_doppler_realdata:   2056/2056 PASS
  - tb_doppler_frame_start_gate: 21/21 PASS
  - tb_system_e2e:         33/49 PASS (PR-F baseline parity)
2026-05-01 04:06:58 +05:45
Jason
7862f4d63c chirp-v2 PR-F: doppler/CFAR widen to 3 sub-frames + 2-class detect
Bumps RP_CHIRPS_PER_FRAME 32 -> 48 (= 3 sub-frames × 16 chirps), widens
doppler_bin from 5 to 6 bits ({sub_frame[1:0], bin[3:0]}), and replaces the
1-bit detect_flag rail with a 2-bit detect_class (NONE / CANDIDATE /
CONFIRMED) sourced from a soft+confirm CFAR threshold pair.

doppler_processor:
  Generalised the 2-subframe FSM to NUM_SUBFRAMES = CHIRPS_PER_FRAME /
  CHIRPS_PER_SUBFRAME (=3 in production, =2 when TBs override). S_OUTPUT
  walks current_sub_frame 0..NUM_SUBFRAMES-1 then advances range_bin;
  the chirp_base * CHIRPS_PER_SUBFRAME formula replaces the if/else split.
  write_chirp_index, read_doppler_index, sub_frame, current_sub_frame all
  widened to 6/2 bits accordingly. doppler_bin packing {current_sub_frame[1:0],
  fft_sample_counter[3:0]} naturally yields 6 bits.

cfar_ca:
  Adds cfg_alpha_soft input + r_alpha_soft register (default
  RP_DEF_CFAR_ALPHA_SOFT = 0x18 ≈ 1.5 in Q4.4 → Pfa_soft ≈ 1e-5). ST_CFAR_MUL
  computes both noise_product (alpha) and noise_product_soft (alpha_soft) in
  parallel DSPs; ST_CFAR_CMP emits detect_class = CONFIRMED when cur > thr,
  CANDIDATE when cur > thr_soft (and not CONFIRMED), NONE otherwise.
  detect_flag is preserved as (class != NONE) for backward compat.
  Address packing now pads doppler axis to next power-of-2 (DOPPLER_PAD =
  1 << ceil(log2(NUM_DOPPLER))) so {range, doppler} packs contiguously
  for both NUM_DOPPLER=32 (legacy TB) and NUM_DOPPLER=48 (production).
  Mag-BRAM grows from ~16 to ~30 RAMB18 on 50T (acceptable on the budget).

usb_data_interface_ft2232h:
  doppler_bin_in widened to 6 bits. FRAME_CELLS pads to next power of two
  (32K) so {range, doppler[5:0]} concatenation lands cleanly. Address regs
  bumped: mag_wr/rd_addr 14→15, detect_byte_addr 11→12, detect_clear bit-
  counter 14→15. Detect-bit BRAM grows 2K→4K bytes. Wire-protocol byte
  counts auto-scale with FRAME_CELLS / DOPPLER_MAG_SECTION_BYTES; PR-G
  bumps the bulk-frame protocol version so the host parser knows.

Other:
  - radar_params.vh: RP_CHIRPS_PER_FRAME 32→48, RP_NUM_DOPPLER_BINS 32→48,
    RP_DOPPLER_MEM_ADDR_W 14→15 (50T) / 17→18 (200T), RP_CFAR_MAG_ADDR_W
    likewise. Other macros (RP_DOPPLER_BIN_WIDTH=6, RP_DETECT_CLASS_WIDTH=2,
    RP_DEF_CFAR_ALPHA_SOFT=0x18, RP_NUM_SUBFRAMES=3) were already in place
    from PR-A.
  - radar_system_top: rx_doppler_bin / dbg_doppler_bin widened. Adds
    host_cfar_alpha_soft register (default RP_DEF_CFAR_ALPHA_SOFT). USB
    opcode mapping deferred to PR-G.
  - radar_system_top_50t: dbg_doppler_bin_nc width.
  - radar_receiver_final: doppler_bin port width.

Test summary:
  - tb_chirp_controller_v2:  43/43 PASS
  - tb_chirp_contract:       10/10 PASS
  - tb_cfar_ca:              24/0 PASS
  - tb_mti_canceller:        43/43 PASS
  - tb_rxb_fullchain:        peak 24033 ~80x (parity with PR-D/E)
  - tb_doppler_realdata:     2056/2056 PASS  (had been broken pre-PR-F due
                             to missing RANGE_BINS=64 override; this PR fixes
                             the parameter override along with the widening)
  - tb_system_e2e:           33/49 PASS — identical to PR-E baseline; the
                             one new fail vs PR-D (G2.2) carries over.
  - tb_radar_receiver_final: still finishing in background (~10 min).
2026-05-01 03:36:03 +05:45
Jason
a1a8fa7107 chirp-v2 PR-E: plfm_chirp_controller_v2 + scheduler-driven TX via async-FIFO
Replaces plfm_chirp_controller_enhanced (5-state FSM with hardcoded
LONG/SHORT timings + 60-entry inline short LUT) with plfm_chirp_controller_v2,
a pure DAC playback driver: IDLE -> CHIRP -> IDLE keyed off a 1-cycle
dst_chirp_valid pulse, with sample count selected by dst_wave_sel
(SHORT=120 / MEDIUM=600 / LONG=3600). Inter-chirp timing (LISTEN, GUARD,
frame boundaries) is now owned exclusively by chirp_scheduler.

Scheduler -> TX bridge: cdc_async_fifo (Cummings style #2, WIDTH=2 DEPTH=4)
crosses {wave_sel} from clk_100m to clk_120m_dac, with chirp_pulse as
src_valid. frame_pulse rides a separate toggle CDC for chirp_counter
clear and the new_chirp_frame status output. mixers_enable now also gates
the scheduler so it stays in S_IDLE while the radar is "off" — without
this gate the first chirp_pulse fires at reset and gets dropped before
mixers come up.

Files:
- NEW  plfm_chirp_controller_v2.v      DAC playback driver (3 LUTs, FSM)
- DEL  plfm_chirp_controller.v         legacy controller (382 lines)
- DEL  long_chirp_lut.mem              legacy LUT (3600 lines), replaced
                                       by tx_long_lut.mem from PR-B
- chirp_scheduler.v       + mixers_enable input (master quiesce)
- radar_receiver_final.v  + sched_*_out output ports + mixers_enable_100m
- radar_system_top.v      wire sched_*_out -> tx_inst.sched_*; pass
                          stm32_mixers_enable_100m to rx_inst
- radar_transmitter.v     full rewrite: drop new_chirp edge detector +
                          toggle CDC, instantiate cdc_async_fifo for
                          {wave_sel}, toggle CDC for frame_pulse,
                          plfm_chirp_controller_v2 in place of _enhanced
- tb/tb_chirp_controller.v  + tb/tb_chirp_contract.v  rewritten for v2
                          contract (43/43 unit + 10/10 contract green)
- tb/tb_radar_receiver_final.v  + .mixers_enable_100m(1'b1) pin
- run_regression.sh, scripts/200t/build_200t.tcl  file-list bumped

Test summary:
- tb_chirp_controller_v2:   43/43 PASS
- tb_chirp_contract:        10/10 contracts upheld
- tb_rxb_fullchain:         peak 24033 ~80x (parity with PR-D)
- tb_mti_canceller:         43/43 PASS
- tb_system_e2e:            33/49 (1 new vs 34/49 PR-D baseline: G2.2
                            new_chirp_frame, intentional v2 frame-pulse
                            semantics — fires once per Doppler frame
                            instead of once per stm32 chirp toggle.
                            TB needs widening in PR-H to wait the full
                            frame.)
2026-04-30 21:51:46 +05:45
Jason
8e8f3e60c4 chirp-v2 PR-D: chirp_scheduler replaces radar_mode_controller; MF/MTI wave_sel-native
Single 100 MHz scheduler emits wave_sel[1:0] and chirp_pulse natively. Modes
00 (STM32 pass-through), 01 (auto-scan over SHORT/MEDIUM/LONG sub-frames),
10 (single-chirp debug), 11 (track dwell with watchdog scan-fallback after
RP_DEF_TRACK_WATCHDOG_FRAMES=5 idle frames). Sub-frame mask lets ops drop a
waveform without recompiling.

Drops the receiver_final wave_sel shim added in PR-C: wave_sel comes
straight from the scheduler; chirp_pulse replaces the old mc_new_chirp
toggle + XOR edge converter. matched_filter_multi_segment and mti_canceller
take wave_sel[1:0] and chirp_pulse directly — no parallel paths.

multi_segment also bumped: SHORT_CHIRP_SAMPLES 50 -> 100 (V2 1 us SHORT)
and MEDIUM_CHIRP_SAMPLES = 500 (5 us). LONG path unchanged. Dead
mc_new_elevation/azimuth XOR converters removed.

Deletes radar_mode_controller.v, formal/fv_radar_mode_controller.v, and
tb/tb_radar_mode_controller.v. Build manifests (run_regression.sh,
scripts/200t/build_200t.tcl) updated. Receiver_final pins medium/track/
subframe_enable inputs to RP_DEF_* defaults until PR-G plumbs USB opcodes.

Verification:
- tb_rxb_fullchain_latency: peak |I|+|Q|=24033 at bin 0, ~80x peak/mean
  (up from PR-C's 15115 since matched filter now uses full 100 SHORT samples)
- tb_mti_canceller: 43/43 PASS with new wave_sel[1:0] input
- tb_radar_receiver_final: 8/8 PASS, ALL TESTS PASSED
- tb_system_e2e: 34/49 PASS - identical to pre-PR-D baseline (15 failures
  are pre-existing matched-filter cycle-budget skips); G8.2/G8.3 chirp_scheduler
  probes PASS
- tb_multiseg_cosim: 16/32 - same as pre-PR-D baseline
2026-04-30 20:52:32 +05:45
Jason
4238eb1b99 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.
2026-04-30 19:37:43 +05:45
Jason
f5b8e7a20b chirp-v2 PR-B: 3-waveform mem generator + 11 new .mem files
Rewrite gen_chirp_mem.py to emit the SHORT (1 µs), MEDIUM (5 µs), and LONG
(30 µs) waveform set on both TX and RX paths. The script is now the single
source for every chirp .mem file; the legacy 6-file set on disk
(long_chirp_lut.mem, long_chirp_seg{0,1}_{i,q}.mem, short_chirp_{i,q}.mem)
is no longer regenerated and gets deleted in PR-C/PR-E when its consumer
modules are removed.

Generated artifacts (committed):
  TX (8-bit unsigned offset-binary, fs_dac = 120 MHz):
    tx_short_lut.mem    120  lines
    tx_medium_lut.mem   600  lines
    tx_long_lut.mem     3600 lines
  RX (Q15 I/Q hex, fs_sys = 100 MHz, all 2048 lines for uniform BRAM sizing):
    rx_short_i.mem  / rx_short_q.mem    100  active + 1948 zero-pad
    rx_medium_i.mem / rx_medium_q.mem   500  active + 1548 zero-pad
    rx_long_seg0_i.mem  / rx_long_seg0_q.mem   2048 (samples [0..2047])
    rx_long_seg1_i.mem  / rx_long_seg1_q.mem   952 active + 1096 zero-pad

Phase model unchanged from chirp-v1: phi(n) = 2π·F_BASEBAND_LOW·t +
π·(BW/T)·t² with F_BASEBAND_LOW=10 MHz and BW=20 MHz. The same formula now
runs three durations and two sample rates from one helper.
rx_long_seg0_i.mem is bit-exact to the legacy long_chirp_seg0_i.mem on disk
(diff -q reports identical) — proves the SHORT/MEDIUM additions did not
perturb the LONG path.

Verification:
  - all 11 files have correct line counts (above)
  - script is idempotent (re-run produces byte-identical output)
  - ruff clean (one E501 line-length + two RUF046 redundant-int casts fixed)
  - phase regression at long-seg0 against pre-chirp-v2 reference: bit-exact

No RTL or testbench changes. The legacy .mem files remain on disk for the
existing chirp_memory_loader_param.v / plfm_chirp_controller.v consumers
until PR-C and PR-E delete those modules. No module references the new
files yet.
2026-04-30 17:46:08 +05:45
Jason
340c6d628d chirp-v2 PR-A: radar_params.vh additive macros for 3-ladder + escalation
Establishes the macro vocabulary for the SHORT/MEDIUM/LONG waveform ladder,
3-subframe Doppler layout, track-mode dwell, and 2-class CFAR detection.
PR-A is purely additive — no module references the new macros yet.
Subsequent PRs (B–H) progressively replace the old chirp logic; this one
puts the names in place so each follow-on PR is mechanical.

Added:
  - Waveform identity: RP_WAVE_{SHORT,MEDIUM,LONG,RESERVED} (2-bit selector)
  - Sub-frame layout: RP_NUM_SUBFRAMES=3, RP_DOPPLER_BIN_WIDTH=6,
    RP_SUBFRAME_ID_WIDTH=2
  - Track mode: RP_DOPPLER_FFT_SIZE_TRACK=64, RP_MODE_TRACK=2'b11,
    RP_DEF_TRACK_CHIRP_COUNT=64, RP_DEF_TRACK_WATCHDOG_FRAMES=5
  - Detection class: RP_DETECT_{NONE,CANDIDATE,CONFIRMED,RSVD}
  - 3-ladder timing defaults (V2 suffix to coexist with legacy in this PR):
    SHORT 100 cyc (1 µs), MEDIUM 500 cyc (5 µs), LONG 3000 cyc (30 µs)
  - Soft-CFAR alpha default: RP_DEF_CFAR_ALPHA_SOFT=0x18 (1.5 Q4.4,
    Pfa_soft ≈ 10⁻⁵; confirm Pfa ≈ 10⁻⁶ at α=3.0)
  - host_subframe_enable default: RP_DEF_SUBFRAME_ENABLE=3'b111

Marked LEGACY (deleted in the noted PR):
  - RP_CHIRPS_PER_FRAME=32, RP_NUM_DOPPLER_BINS=32 (PR-F)
  - RP_DEF_SHORT_CHIRP_CYCLES=50 (PR-E switches to 100)
  - RP_DEF_CHIRPS_PER_ELEV=32 (PR-F)

Verified: iverilog preprocess clean. Sweep across 9_2_FPGA confirms no
module references the new macros yet — the PR is fully isolated.

Revert tag pre-chirp-v2 placed at 4f898ae for the chirp-v2 series.
2026-04-30 17:40:15 +05:45
Jason
4f898ae63d docs(fpga): correct matched_filter_processing_chain header (LogiCORE swap, FSM)
The header still described the legacy in-house Radix-2 DIT fft_engine and a
FWD/REF/INV BITREV+BUTTERFLY state list that no longer matches reality.

Since RX-NEW-3 (commit 5c8cc8c), the chain instantiates fft_engine_axi_bridge,
which wraps xfft_2048 — LogiCORE FFT v9.1 (Pipelined Streaming) in synth/XSim
when FFT_USE_XILINX_IP is defined, in-house fft_engine fallback in iverilog.
Bit-reversal is now handled inside the IP (and the fallback), so the FSM has
COLLECT → SIG_FFT/CAP → REF_FFT/CAP → MULTIPLY → INV_FFT/CAP → OUTPUT → DONE.

No RTL changes. Header comment updates only.
2026-04-30 13:59:37 +05:45
Jason
58d2e1ba10 AUDIT-C11: replace Gray-CDC at CIC→FIR with home-grown async FIFO
cdc_adc_to_processing carries multi-bit data across 400→100 MHz via
TWO independent synchronizer chains (data Gray-encoded + a separate
2-bit toggle). Under metastability, the chains can resolve on
different cycles, letting the destination latch a half-resolved Gray
word that decodes to an arbitrary value. Audit C-11. Practical MTBF
is years per event but the design is non-conformant for arbitrary
multi-bit data — Gray code's single-bit-flip protection only holds
for ±1 transitions, not for CIC samples that can change by hundreds
of LSBs.

Replace with cdc_async_fifo, a Cummings SNUG-2002 style #2 async
FIFO. Data does NOT cross domains; it sits in dual-clock distRAM
(write port src_clk, read port dst_clk). Only the read/write
Gray-coded POINTERS cross — and pointers genuinely change ±1 per
increment, so Gray code's protection is correct by construction.
Home-grown rather than XPM_FIFO_ASYNC: vendor-neutral (iverilog can
simulate it directly, no SIM stub), keeps the project's existing
home-grown CDC convention (3 sibling primitives in cdc_modules.v),
and avoids XPM library version skew.

Port shape is preserved (same WIDTH=18, same dst_data/dst_valid/
overrun semantics — 1-cycle pulse per read in steady state) so the
swap is local to two instantiations in ddc_400m.v. Sticky-overrun
aggregation downstream is unchanged.

XDC: project already has blanket set_false_path on
clk_100m ↔ adc_dco_p, which covers both new pointer crossings.
Synchronizer FFs carry ASYNC_REG="TRUE" for placement-aware MTBF.
No XDC change needed.

New TB tb_cdc_async_fifo.v exercises 7 groups (28 checks): reset,
single-sample passthrough, multi-Gray-bit-flip (0x00000 ↔ 0x3FFFF —
audit's recommended coverage point, asserts NO intermediate values
appear at dst_data), matched-rate continuous stream, sustained-burst
overrun, drain-to-empty, and mid-stream reset.

Resource: 8 LUTRAMs per instance × 2 instances = 16 LUTRAMs (~0.05%
of XC7A50T budget).

Verified: full FPGA regression 42/42 PASS (was 41/41; +1 new test,
0 regressions in DDC Chain / Doppler Co-Sim / Full-Chain Real-Data
/ Receiver Integration / System Top / System E2E / MF Co-Sim — all
of which exercise the swap path through the production signal
chain). 0 lint errors.
2026-04-30 10:47:31 +05:45