Commit Graph

57 Commits

Author SHA1 Message Date
Jason 4989c33aa6 PR-AB.b expanded commit 5: Beam-ready handshake (RTL + MCU + GUI)
Wire a per-frame MCU→FPGA "beam pattern ready" handshake so the chirp
scheduler can stall between 48-chirp frames until the MCU finishes
writing the next ADAR1000 pattern. The legacy unused stm32_new_chirp
input on PD8 is repurposed as stm32_beam_ready; chirp_scheduler.v gets
a new S_BEAM_WAIT state entered after each frame_pulse and an 80 ms
watchdog so a missed MCU toggle degrades to wall-clock cadence with a
sticky telemetry bit rather than stalling the radar.

Cold-reset defaults the handshake off (host_handshake_enable=0, new
opcode 0x1A); the GUI opts in once the MCU PD8 wiring is verified on
the bench. Both the FT601 and FT2232H status word 4 paths get the new
beam_handshake_watchdog_fired sticky at bit [1] (reclaimed from the
range_mode retirement in commit 1).

RTL:
- chirp_scheduler.v: 2-FF ASYNC_REG sync on beam_ready_async; 1-cycle
  edge detect (any transition, MCU side uses HAL_GPIO_TogglePin); new
  S_BEAM_WAIT state entered at frame_pulse when host_handshake_enable=1;
  23-bit beam_watchdog counter with BEAM_WATCHDOG_MAX = 8_000_000 (~80 ms
  at 100 MHz, ~10 nominal frames); beam_handshake_watchdog_fired output
  sticky across mixers_enable cycles, cleared only by reset_n; mid-wait
  disable releases the FSM so dropping the opcode never strands the
  radar between frames.
- radar_receiver_final.v: thread stm32_beam_ready_async +
  host_handshake_enable + beam_handshake_watchdog_fired through the
  scheduler instance.
- radar_system_top.v: rename input port stm32_new_chirp → stm32_beam_ready;
  add host_handshake_enable register (cold-reset = 1'b0); opcode 0x1A
  dispatch (value[0]); add rx_beam_handshake_watchdog wire; pack into
  status_words[4][1] in both USB paths.
- radar_system_top_50t.v: rename wrapper port + sub-instance wiring.
- usb_data_interface.v + usb_data_interface_ft2232h.v: add
  status_beam_handshake_watchdog input + 2-FF level CDC (same convention
  as F-6.4 / F-1.2 stickies); refresh word-4 layout doc comment; pack
  beam_handshake_wd_sync_1 into status_words[4][1].

XDC:
- xc7a50t_ftg256.xdc + xc7a200t_fbg484.xdc: rename stm32_new_chirp port
  references to stm32_beam_ready (same PD8 pin, F13 on 50T / L18 on
  200T).

MCU:
- main.h: add FPGA_BEAM_READY_Pin = GPIO_PIN_8 + FPGA_BEAM_READY_GPIO_Port
  = GPIOD alongside the existing FPGA_FRAME_PULSE alias.
- main.cpp:runRadarPulseSequence: insert HAL_GPIO_TogglePin(GPIOD,
  GPIO_PIN_8) after each setCustomBeamPattern16(RX) — once after the
  per-azimuth broadside (vector_0), once after matrix1, once after
  matrix2 — between the SPI burst completion and waitForFramePulse.

GUI:
- radar_protocol.py: Opcode.HANDSHAKE_ENABLE = 0x1A;
  StatusResponse.beam_handshake_watchdog = 0 default; parse word 4 bit
  [1] in parse_status_packet; update word-4 layout comment.
- test_GUI_V65_Tk.py: add beam_handshake_watchdog kwarg to
  _make_status_packet (sets bit [1] of word 4); refresh
  test_parse_status_word4_layout_co_spec to cover the new bit (used+9=32);
  add test_parse_status_beam_handshake_watchdog round-trip;
  test_handshake_enable_opcode pins 0x1A; defaults / chirps_mismatch /
  agc-coexist tests gain a watchdog==0 assertion; bump
  test_all_rtl_opcodes_present expected set to include 0x17/0x18/0x19/0x1A.

TB:
- new tb_chirp_scheduler_handshake.v (16 checks): legacy open-loop, edge
  exit (rising + falling), 200-cycle idle hold, watchdog auto-advance
  via force on dut.beam_watchdog, sticky-survives-mixers_disable,
  mid-wait disable release, reset_n clears sticky.
- run_regression.sh: register the new TB in PHASE 1.
- tb_radar_receiver_final.v: tie the 3 new receiver ports off
  (beam_ready_async=0, handshake_enable=0, watchdog unconnected).
- tb_system_mechanics.v / tb_system_opcodes.v: explicit
  .stm32_beam_ready(1'b0) connection (the cold-reset
  host_handshake_enable=0 keeps the FSM out of S_BEAM_WAIT).
- tb_usb_data_interface.v / tb_usb_protocol_v2.v / tb_e2e_dsp_to_host.v /
  tb_ft2232h_frame_drop.v: tie .status_beam_handshake_watchdog(1'b0).

Ride-along ruff sweep (14 → 0 across the repo):
- tb/cosim/compare_independent.py: RUF003 — '5×' → 'at least 5x'.
- tb/cosim/gen_e2e_expected.py: noqa: E402 on the post-sys.path import;
  drop unused EXPECTED_RANGE_BIN + EXPECTED_DOPPLER_BIN_PER_SF imports;
  fold the detect-class slot if/else into a ternary (SIM108).
- tb/cosim/gen_e2e_stimulus.py: drop int() wrapping round() at four
  call sites (RUF046 — round() already returns int in Python 3);
  rewrite the range-bin derivation comment block from code-like
  `# range_bin = ...` to prose (ERA001); strip stray f from
  placeholder-free error string (F541).
- tb/cosim/tb_e2e_dsp_to_host_parse.py: open(path, 'r') → open(path)
  (UP015).
- v7/dashboard.py: '3×' → '3x' (RUF003); drop quotes from
  'StatusResponse | None' annotation (UP037, file already has
  `from __future__ import annotations`).

CI summary (all suites green pre-commit):
- ruff: All checks passed!
- FPGA regression (iverilog): 43 / 0 / 0 (incl. new handshake TB 16/16).
- MCU tests: 51 / 0 + 34 / 0 + 13 / 13 ADAR1000_AGC.
- GUI Tk (test_GUI_V65_Tk): 120 / 0.
- GUI v7 (test_v7): 152 / 0.

Production rollout note: bitstream cold-resets with host_handshake_enable=0
so existing flashes keep their open-loop cadence until the GUI sends
opcode 0x1A=1. Once enabled, the per-pattern dwell tracks both the chirp
ladder (PD14 frame_pulse from commit-3 work) and the MCU pattern-write
completion (PD8 toggle from this commit), eliminating drift from the SPI
burst timing.
2026-05-11 12:07:08 +05:45
Jason 98ec9cb6a5 fix(fpga): PR-AA — doppler_mag 1-cell shift in usb emit FSM
The WR_DOPPLER_DATA emit advanced mag_rd_addr at end of phase 1 (LSB byte)
but BRAM has 1-cycle read latency, so phase 0 of the next pair re-read
the prior cell. Result: wire pair K = (HIGH(bram[K-1]), LOW(bram[K])) —
adjacent cells silently swapped their high bytes whenever the high byte
differed. Footprint was 30 of 24576 cells (peak rows + high-byte
transitions in the noise floor); max diff 6656 LSB on the target row.

Fix: advance the BRAM read address at end of phase 0 (MSB) so BRAM has
2 cycles between addr-set and the next pair's MSB read. Same pattern
existed in WR_RANGE_DATA — silently broken (regression skips range
stream); fixed for symmetry. After fix, both iverilog and remote
Vivado 2025.2 xsim emit a bit-exact match against the Python golden.

Tighten E12.14 / E12.6.b to strict zero tolerance and rename the
"PR-AA pending" notes to indicate the fix has landed. Target-cell
window check (E12.15) now points at the exact (rb, db) bin.

Verification:
  * iverilog A6 in-TB: doppler_mismatches=0/24576 (16/16 PASS)
  * iverilog A6 parse strict: 28/28 PASS
  * Vivado 2025.2 xsim A6 in-TB: doppler_mismatches=0/24576 (16/16 PASS)
  * Vivado 2025.2 xsim A6 parse strict: 28/28 PASS
  * Full regression: 41 passed, 0 failed, 0 skipped / 41 total
2026-05-06 10:30:54 +05:45
Jason d9e7a5becf fix(fpga): PR-Y.1 + PR-X.3 — DC notch boundary, audit cleanups, formal retarget
Bundles audit items unblocked by the AERIS-10 end-to-end audit:

S-1 (radar_system_top.v) — DC notch off-by-one at width=7

  Audit S-1: ±W around DC in a 16-bin FFT covers bins {0..W, 16-W..15}
  (2W+1 total, bin 8 the only one excluded at W=7). The previous form
  `< W || > 15-W+1` missed both boundaries: at W=1 it notched only {0}
  (skipping 1 and 15); at W=7 it missed 7 and 9. Replaced with inclusive
  comparators against 5-bit limits (`<= notch_lo || >= notch_hi`) which
  hit the intended set for all W ∈ {1..7}. Cosim DC golden
  (tb/cosim/rtl_bb_dc.csv) regenerated against the corrected behaviour.

S-7 (rx_gain_control.v) — reg→wire for combinational helpers

  `wire_frame_sat_incr` / `wire_frame_peak_update` were declared `reg`
  and blocking-assigned inside the clocked always block. They are pure
  combinational functions of the registered inputs — promoted to
  module-scope continuous assigns. Behaviour is bit-identical (the read
  inside the always still reflects the prior-cycle latched values) but
  the iverilog warnings disappear and the sim/synth correspondence is
  unambiguous.

M-9 (formal/fv_radar_mode_controller.sby) — delete orphan

  radar_mode_controller.v was retired in PR-D in favour of
  chirp_scheduler.v; the .sby was never updated and pointed at a
  non-existent module. Deleted.

M-10 (radar_receiver_final.v) — document `data_sync_error` unconnected

  In production AD9484 produces a single 8-bit stream that the DDC mixes
  into matched I/Q paths with symmetric pipelines, so `ddc_valid_i` and
  `ddc_valid_q` rise on the same cycle and `data_sync_error` cannot
  fire by construction. The check is retained inside
  ddc_input_interface for the standalone tb_ddc_input_interface
  unit-test (which intentionally drives valid_i ≠ valid_q). Adds
  comments explaining the unconnected port at both call sites; no
  functional change.

M-11 (radar_receiver_final.v) — `force_saturation_pulse` symmetric hook

  The DDC has a `force_saturation` debug input that previously was tied
  1'b0 directly. Routed through a new `force_saturation_pulse` wire
  alongside the existing `clear_monitors_pulse` so a future host opcode
  surface for "diagnostic force/clear" lands both at the same dispatch
  point. Still tied 1'b0 today — RTL change is a placeholder for the
  opcode plumbing.

PR-X.3 F-7.5 (formal/fv_cdc_adc.{v,sby}) — retarget to cdc_async_fifo

  Prior wrapper instantiated `cdc_adc_to_processing`, retired by
  AUDIT-C11 in favour of `cdc_async_fifo` (the production CIC→FIR
  boundary CDC, see ddc_400m.v line 646). Wrapper rewritten with
  FIFO-shaped equivalents of the original Gray-CDC properties:

    P1 reset behaviour, P2 no spurious dst_valid, P3 overrun semantics,
    P4 data integrity (cooldown-spaced, FIFO-equivalent of the
       original single-element latch property),
    P5 bounded liveness (depth 100 gclk),
    P6 cover sequences for the basic write→read pipeline.

  P4's true multi-in-flight FIFO order proof is left as Option B work;
  for the AERIS-10 use case the upstream ddc_400m CIC→FIR consumer
  operates below FIFO-fill rate by design, so the cooldown-spacing
  assumption is a tight model.

Verification: full FPGA regression 41 / 0 / 0.
2026-05-06 01:23:21 +05:45
Jason 9c231d85db fix(fpga): PR-Z A6 — usb cfar dense bug end-to-end fix + e2e test
The PR-Z A6 e2e test (tb_e2e_dsp_to_host) exposed that the wire-format
cfar_dense map emitted by usb_data_interface_ft2232h was all-zero for
our deterministic single-target stimulus, even though cfar_ca's
in-flight outputs showed CONFIRMED at the expected cells (verified via
in-TB capture, E5/E6 PASS).

Deep instrumented debug (BRAM-WRITE, BRAM-READ, EGRESS-CAP probes)
revealed THREE independent bugs that combined to produce the all-zero
wire output. Each bug alone would have been visible; the way they
compounded made the symptom look like a single coarse failure.

Bug A — stale write address (radar_system_top.v):

  usb_inst.range_bin_in/doppler_bin_in were tied to notched_*_bin
  (= rx_*_bin = doppler_processor outputs). After doppler returns to
  S_IDLE its `output reg`s hold their last-driven values (511, 47).
  cfar_ca's CMP-phase emit (cycles ~520..73520 after frame_complete)
  fires cfar_valid with detect_range/detect_doppler set to its own
  per-cell scan counters, but those outputs were dangling — usb's
  RMW saw the doppler stale (511, 47) and slammed every cfar write
  to byte_addr {511, 47[5:2]} = bram[8187], past the 6144-byte wire
  range entirely.

  Fix: register cfar_detect_range/doppler in lockstep with the existing
  rx_detect_valid/rx_detect_class registration block (clk_100m_buf
  domain), then mux them into usb_inst.range_bin_in/doppler_bin_in on
  rx_detect_valid. doppler-magnitude write path is unaffected because
  doppler_valid and rx_detect_valid are mutually exclusive (BUFFER vs
  CMP phases of cfar_ca).

Bug B — BRAM read pipeline lag (usb_data_interface_ft2232h.v):

  The detect_rd_data <= detect_bram[detect_rd_addr] BRAM read port has
  1-cycle latency. WR_DETECT_DATA's emit FSM advanced detect_rd_addr
  and read detect_rd_data in the SAME edge — so cycle K read bram[K-2]
  (the addr from cycle K-1's commit) instead of bram[K-1]. Result:
  every cfar wire byte = bram[N-1] instead of bram[N], shifting the
  entire 6144-byte detect section +1 byte = +4 doppler bins. Doppler
  hides this naturally because its 2-byte-per-cell rhythm gives BRAM a
  free settling cycle between addr-set and emit-read.

  Fix: pre-load detect_rd_addr <= 1 and det_doppler_byte_idx <= 1 at
  every WR_DETECT_DATA entry transition (HDR direct, RANGE direct,
  DOPPLER → DETECT). BRAM produces bram[0] for the first emit cycle
  (settled since reset because detect_rd_addr was 0 throughout the
  preceding section) while the addr advance schedules bram[1] for the
  second emit cycle — and from then on the FSM's natural advance
  pattern keeps the pipeline aligned, including across the per-range
  boundary (det_doppler_byte_idx == DET_BYTE_LAST_PER_RANGE).

Bug C — detect_clearing window overlaps cfar's first 4 columns:

  detect_clearing fired 1 cycle after frame_complete and ran for 8192
  clk cycles (1 byte/cycle). cfar_valid writes were gated on
  `!detect_clearing` (line 512). cfar's CMP-phase emits start at
  frame_complete + ~520 cycles and run for ~73000 cycles, so the
  first ~7672 cycles (≈ 4 doppler columns) of cfar pulses were
  silently dropped. Test stimulus lit (67, 2/3) for sub-frame 0, all
  inside the clearing window → bytes lost. (67, 18/19) and (67, 34/35)
  for SF1/SF2 fell after clearing → captured correctly. Visible as
  one-byte mismatch (0x0A expected, 0x00 captured) at offset 49965
  (= cfar byte 804 = range 67, doppler 0..3) once Bugs A and B were
  fixed.

  Fix: move detect_clearing trigger from "1 cycle after frame_complete"
  to wr_done_pulse (USB-transfer-complete edge already CDC'd into clk
  via the AUDIT-C12 wr_done_sync chain). Clearing now runs in the dead
  zone after USB has finished reading frame N's BRAM, well before
  frame N+1's cfar starts CMP (~480k cycles of margin at 178 fps).
  First frame after reset relies on BRAM init=0 — added explicit
  initial block under `ifdef SIMULATION so iverilog matches Vivado's
  synthesis default.

Test infrastructure:

  - tb/tb_e2e_dsp_to_host.v new — deterministic single-target stimulus
    fed through the back-half of the radar pipeline (range_decim → MTI
    → doppler → DC-notch → cfar → registered sync → usb), 16 in-TB
    asserts + bit-exact byte capture.
  - tb/cosim/gen_e2e_stimulus.py / gen_e2e_expected.py new — Python
    deterministic stim + bit-exact frame golden.
  - tb/cosim/tb_e2e_dsp_to_host_parse.py new — parses captured frame
    via radar_protocol, runs 12 strict-bit-equality checks plus 16
    semantic checks (target == CONFIRMED, neighbors == NONE,
    DC-notched bins == NONE, etc).
  - run_regression.sh — A6 hookup + retired the two zero-assertion
    radar_system_tb USB_MODE=0/1 smoke runs and the 3-liveness-only
    tb_system_dataflow (subsumed by A6's stronger checks). Saves
    ~7 min wall.

Verification:

  - Local iverilog: in-TB 16/16 PASS, parser strict 28/28 PASS.
  - Remote Vivado 2025.2 xsim (Artix-7 target): in-TB 16/16 PASS,
    parser strict 28/28 PASS.
  - Full regression: 41 / 0 / 0.

The MODEL_USB_CFAR_BUG bug-model flag (used to keep the regression
green during development against buggy production) is removed — the
test is now strict bit-exact against the post-fix wire format.
2026-05-06 01:20:19 +05:45
Jason bf83d35917 test(fpga): PR-M.4 — redesign T-6 drift invariants for scaled-FFT chain
Three sub-checks in compare_independent.py were red because the test
inputs assumed an UNSCALED FFT but PR-O moved both the RTL xFFT path
and fpga_model.FFTEngine to LogiCORE v9.1 Scaled mode (one >>>1 per
butterfly stage with conv-rounding, total /N applied across LOG2N
stages). At small input amplitudes the per-bin output rounded to zero
and the test invariants no longer described meaningful behaviour.

The fpga_reference.py side already mirrors the scaled-mode convention
(np.fft.fft(x)/n on forward, ifft on inverse — see line 104, 137-138,
207). The fix is purely in the test inputs:

  - FFT-2048(impulse): amp 1000 → 32000 (≈ Q15 max). Expected per-bin
    is now round(amp/N) = round(32000/2048) = 16 (banker's). The
    impulse has b=0 at every butterfly so there is no twiddle
    interaction; banker's rounding keeps every bin within ±1 LSB.
    Tightened tolerance from 5 to 2.

  - MF peak position + MF peak-to-median: amp 200 → 4000. Chain
    output peak under scaled-mode is correlation/N² ≈ pulse_len*amp²
    /N² = amp²/16384 (256 / 4_194_304). At amp=200 the peak collapsed
    to 2.4 (mostly Q15 quantization noise — argmax wandered to bin 2);
    at amp=4000 the peak rises to ≈ 977 with sidelobes near LSB
    floor. Peak-to-median ratio observed: 974 vs threshold 5.

Runtime verification: compare_independent.py 13/13 PASS standalone.
Full FPGA regression: 36/1/6 → 37/0/6 (the single FAIL was this test;
no other tests touched).
2026-05-05 12:37:26 +05:45
Jason 0728d931c4 chore(repo): PR-H — G-series close-out (regression infra + lint sweep)
Closeout pass for the G-series 3-ladder chirp + adaptive-escalation work.
Cleanup, watchdog/fallback, lint, full regression — final sign-off.

Cleanup + watchdog/fallback: already wired during earlier audit waves
(track watchdog in chirp_scheduler RP_DEF_TRACK_WATCHDOG_FRAMES, RESERVED
fallback in plfm_chirp_controller_v2, range-decim watchdog in
radar_system_top with gpio_dig7 surfacing, F-3.* MCU error path).
Verified — no residual TODO/FIXME in production RTL or MCU.

Regression infra: tb/cosim/compare_independent.py SKIP-detection bug —
importlib.util.find_spec("scipy.signal") raises ModuleNotFoundError when
the parent scipy package is itself absent (instead of returning None as
the surrounding logic assumed). Wrap in try/except so the regression
runner gets the intended rc=2 SKIP marker rather than a crash that masks
the rest of the script.

Lint sweep: ruff full-repo → 0 errors. Two changes:
  - pyproject.toml broadens 5_Simulations/Antenna/**.py exemption from
    just T20+ERA to the full set of script-ergonomics rules
    (RUF001/002/003 Greek µ/λ/π/θ in physical-units strings, E501 long
    matplotlib/numpy lines, RUF005/015/046, E70x one-line setup, B007
    tuple-unpack loop vars, B905, BLE001 diag try/except, C401, RET504,
    SIM118, PERF40x, ARG001, E402). These are sim/analysis scripts, not
    production code — keep substantive bug rules (F unused, B core
    bugbears) but drop stylistic noise.
  - Auto-fix sweep: 31x F541 (f-string-no-placeholder), 3x F401 (unused
    sys import), 2x F841 (dead leftover ref_pat / phases_quant in
    array_factor_adar1000_aeris10.py).

.gitignore: cover 9_Firmware/9_2_FPGA/tb/cosim/mf_chain_autocorr.csv
(matched_filter cosim writes here now; was already covered for tb/ but
not tb/cosim/).

Regression baseline (radar_venv):
  FPGA  : 42/43 — 1 pre-existing T-6 drift cosim fail surfaced by the
          SKIP fix above. Three sub-checks now red because PR-O moved
          xFFT/MF chain to LogiCORE v9.1 *Scaled* mode (1/2 per stage,
          1/2^11 total for N=2048) but compare_independent.py's invariants
          (FFT-impulse uniform-spectrum, MF peak-at-injected-delay, MF
          peak/median ≥ 5) were written assuming UNSCALED FFT. Not
          introduced by this PR — was hidden by the SKIP-detection crash.
          Defer to PR-M.4: redesign T-6 invariants (or input amplitudes)
          to match scaled-mode arithmetic.
  MCU   : 34/34 binary suites pass.
  GUI   : test_v7 150/150 pass.

uv.lock: scipy resolution catch-up (declared in pyproject dev group all
along; lock just hadn't been refreshed after pyproject edits landed).

Bench-side checks: none — this PR is repo hygiene, no firmware/RTL
behaviour change.
2026-05-05 10:39:57 +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 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 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 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 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 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 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 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 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 1f307f77a9 cosim: refresh stale baselines (FFT-2048 + chirp realign)
Two stale-baseline events were never captured in earlier commits:

1. The FFT-1024 -> FFT-2048 merge (c668652) updated the testbench and
   gen_mf_cosim_golden.py but left radar_scene.py FFT_SIZE=1024. When
   FFT_SIZE was later bumped to 2048, the input vectors written by
   generate_baseband_samples (bb_mf_test_*.hex, ref_chirp_*.hex) grew
   from 1024 to 2048 samples but were never re-exported.

2. The TX-I matched-filter realignment (5ff5671) changed the ADC chirp
   phase from 2*pi*F_IF*t to 2*pi*(F_IF+F_BASEBAND_LOW)*t. ADC sample
   values shifted from sample ~1336 onward but adc_*.hex was never
   re-exported.

Result: every regression run produced a "dirty" working tree as the
regen reproduced post-merge values that disagreed with the committed
baselines. Two consecutive regen runs are bit-exact identical
(LCG seed=42 + deterministic chirp math) — verified via diff -q on
two output dirs. There is no actual non-determinism; only stale
artifacts.

This commit refreshes all 15 affected files in one shot:
- 6 input hex (adc_*_target.hex, bb_mf_test_*.hex, ref_chirp_*.hex)
- 5 RTL output csv (rtl_*.csv from current RTL)
- 4 compare csv (compare_mf_*.csv = py vs rtl side-by-side)

Verification: full regression 39/39 PASS on the refreshed inputs.
After this commit, regression runs should leave the working tree clean.
2026-04-29 20:33:55 +05:45
Jason 5ff5671fe2 fix(fpga): TX-I — align matched-filter reference with actual post-DDC band
The DAC short/long chirp LUTs are 10..30 MHz upchirps (Hilbert-confirmed).
With TX_LO=10.500 GHz, RX_LO=10.380 GHz (adf4382a_manager.h) and the
120 MHz DDC NCO (ddc_400m.v), high-side mixing places the post-DDC echo
at 10..30 MHz baseband. The matched-filter reference (gen_chirp_mem.py)
was generating 0..20 MHz, implicitly assuming the chirp's low edge mixed
to DC. This caused a 10 MHz spectral offset and ~5 dB matched-filter loss.

Adds F_BASEBAND_LOW=10e6 in both gen_chirp_mem.py and radar_scene.py,
with phase formula 2*pi*F_BASEBAND_LOW*t + pi*rate*t^2 in all chirp
generators. Regenerates the 6 .mem files. Adds analyze_short_chirp_mismatch.py
for the Hilbert-based diagnosis. Fixes the misleading "30MHz to 10MHz"
comment in plfm_chirp_controller.v and adds an end-to-end frequency plan
in the LUT header.

Sideband orientation (high-side at both mixers) is the conventional choice
and consistent with antenna match (10.25..10.75 GHz, 8x16 patch designed
at 10.5 GHz). Loopback capture would settle definitively; if either mixer
is low-side the F_BASEBAND_LOW sign flips and/or chirp direction reverses.
2026-04-29 11:41:19 +05:45
Jason b7ac2de1a4 chore: delete dead latency_buffer; doc cleanup for two stale comments
latency_buffer.v has had zero non-tb instantiations since RX-B (2026-04-23)
replaced its hookup in radar_receiver_final with a 1-FF alignment register.
The module was being kept "for potential future use" — exactly the kind of
dead weight the codebase does not need. Deleted, along with all build /
test infrastructure that dragged it along:

  - 9_Firmware/9_2_FPGA/latency_buffer.v
  - 9_Firmware/9_2_FPGA/tb/tb_latency_buffer.v
  - run_regression.sh: removed from RTL_FILES and RECEIVER_RTL
  - scripts/200t/build_200t.tcl: removed from synthesis source list
  - tb/tb_system_e2e.v: removed from header compile-string example
  - tb/cosim/validate_mem_files.py: deleted test_latency_buffer() (~75 lines),
    its call site, and the corresponding entry in the module docstring

Historical RX-B comments referencing latency_buffer in radar_receiver_final.v,
tb_rxb_fullchain_latency.v, and tb_rxb_latency_measure.v are kept — they
explain WHY the module was removed, which is still useful design archaeology.

Two doc-only housekeeping touches bundled in:

  - plfm_chirp_controller.v: replaced two empty "CRITICAL FIX: Generate
    valid signal" labels at LONG_CHIRP and SHORT_CHIRP with one shared
    chirp_valid policy comment block above LONG_CHIRP that explains the
    actual rationale (downstream FIFO underrun on trailing samples).

  - v7/models.py: replaced the "range_resolution and velocity_resolution
    should be calibrated" docstring (sounded like an open TODO but was a
    documented placeholder) with a clear pointer to the GUI-C3 fix in
    workers.py:RadarDataWorker so future readers know the live path
    derives correct values from WaveformConfig.

FPGA quick regression unchanged: 28/29 (1 fail is the unrelated iverilog/
Xilinx-IP RX-NEW-3 gap). GUI suite 180/180. Ruff clean.
2026-04-28 12:52:13 +05:45
Jason f39a78cb1e chore(fpga): untrack TB-generated CSV, ignore a.out
rx_final_doppler_out.csv is written by tb_radar_receiver_final.v on
every run via $fopen — it is a test-run artifact, not an oracle. It
was mistakenly tracked in an earlier commit, causing unnecessary
churn on every sim. Remove from the index and ignore going forward.

Also ignore stray a.out from iverilog one-shot compiles.

Golden references (.hex, .mem, doppler_golden_py_*.csv) remain
tracked — they are load-bearing oracles used by MF / Doppler /
receiver cosim testbenches.
2026-04-22 13:36:03 +05:45
Jason 8865e9a0ef fix(fpga): pre-bringup RTL hardening + test-suite hardening
RTL (P0 pre-bringup findings R-1/R-2/R-3/R-5/R-6):

- mti_canceller: add use_long_chirp input and waveform-boundary mute
  so the long->short transition in mode 01 no longer subtracts across
  heterogeneous waveforms (R-1). Prev buffer is overwritten in-flight
  at the boundary so the next same-waveform chirp subtracts cleanly.
- ad9484_interface_400m: 2FF sync of mmcm_locked into the 400 MHz
  domain before gating reset_n_gated (R-6).
- cic_decimator_4x_enhanced: correct max_fanout narrative (R-3).
- ad9484_interface_400m: strip stale pblock comment, note 3.0 ns
  max_delay instead (R-2).
- mti_canceller / doppler_processor: 200T-20km WARNING banners
  flagging the broken 4096-bin path (R-5). 9-bit BRAM address aliases
  silently until rewritten.
- adc_clk_mmcm.xdc: relax set_max_delay from 2.700 -> 3.000 ns,
  closes WNS with headroom on 50T build.
- radar_receiver_final: wire use_long_chirp into mti_inst.

Architecture-bump finalization (2048-pt range FFT, 512 range bins,
32 Doppler bins -> 16384 output cells per frame):

- tb/cosim/radar_scene.py: FFT_SIZE 1024 -> 2048, RANGE_BINS 64 -> 512.
- tb/gen_mf_golden_ref.py: N 1024 -> 2048.
- Regenerate all affected hex goldens (MF cases 1-4, Doppler inputs
  + py goldens, receiver integration golden_doppler.mem 2048 -> 16384).
- tb_radar_receiver_final: widen range_bin_out 6 -> 9 bits, bump
  GOLDEN_ENTRIES 2048 -> 16384, expand bitmaps/arrays to 512 bins,
  update all check messages and thresholds.
- tb_mti_canceller, tb_fullchain_mti_cfar_realdata: tie/pass
  use_long_chirp so compile still works after RTL port add.

Test-suite hardening (coverage audit findings):

- tb_mti_canceller T12: 10 new assertions exercising R-1 waveform-
  boundary mute across a long/long/short/short/long sequence. Catches
  a regression that re-enables subtraction across the boundary.
- tb_fir_lowpass: replace tautological check(1'b1, ...) on coefficient
  symmetry with a real hierarchical check coeff[k]===coeff[31-k];
  replace always-pass overflow check with a well-driven (not X/Z)
  assertion on filter_overflow.
- tb_matched_filter_processing_chain: replace three always-pass peak-
  bin placeholders with peak-to-mean-|out| > 2x ratio checks (catches
  flat/zero output that the old tautologies silently accepted).
- tb_cdc_modules M2: replace always-pass narrow-pulse check with a
  well-defined-output assertion on the synchronizer.
- tb_nco_400m: replace always-pass freq-switch check with a swing +
  no-X assertion across 200 post-switch samples.
- tb_system_e2e G12.1: replace check(1, ...) with test_num > 20 so
  it catches a stalled TB that skipped prior groups.
- tb_multiseg_cosim TEST 4: replace always-pass placeholder with a
  bitmap that asserts segment_request visited all 4 values.
- tb_mf_chain_synth and tb_fullchain_mti_cfar_realdata: add DEPRECATED
  headers plus \$fatal guards (ifndef ALLOW_STALE_*) so they cannot
  be silently re-enabled in CI with stale 1024-bin goldens against
  current 2048-pt RTL.

Regression: 32 passed, 0 failed. MTI TB grew 30 -> 39 checks;
receiver integration grew 17 -> 18 checks with 16384/16384 golden
match at tolerance +/- 2 LSB.
2026-04-22 13:23:38 +05:45
Jason c668652ba8 merge(wave3/tier2): port testbenches and cosim goldens for fft-2048
Regression goes from 21/32 -> 27/32 passing.

TB files updated from feat/fft-2048-upgrade (FFT_SIZE=2048 / 512 range
bins / Manhattan magnitude / 2-segment matched filter):
  - tb/tb_mf_cosim.v            (range_profile_{i,q} port names)
  - tb/tb_matched_filter_processing_chain.v  (long_chirp port names)
  - tb/tb_range_bin_decimator.v (new 2048->512 DUT)
  - tb/tb_radar_mode_controller.v (XOR edge detector)
  - tb/tb_doppler_cosim.v       (2048-deep inputs)
  - tb/tb_multiseg_cosim.v
  - tb/tb_mf_chain_synth.v

Cosim infrastructure regenerated with FFT_SIZE=2048:
  - tb/cosim/gen_mf_cosim_golden.py
  - tb/cosim/gen_doppler_golden.py
  - tb/cosim/compare_mf.py, compare_doppler.py
  - tb/cosim/fpga_model.py
  - All mf_* and doppler_* goldens/inputs regenerated

Deliberately NOT taken:
  - tb/tb_radar_receiver_final.v — kept p0's version because the merged
    radar_receiver_final requires tx_frame_start + adc_or_p/n inputs
    that fft's TB does not drive. Its 3 failures (G1 golden mismatch,
    B3/B5 hardcoded 64-bin limits) are tracked as known issues; TB
    needs a 64->512 bin rewrite + golden regen against merged RTL.

Known remaining failures (5/32):
  - Doppler Co-Sim x3: python compare mismatch — goldens generated
    against fft's reset/DDC behavior; merged RTL uses p0's reset
    strategy. Needs golden regen against merged RTL.
  - Receiver Integration: TB has stale 64-bin localparams/widths.
  - Matched Filter Chain: 3/40 "peak magnitude > 0" checks fail on
    behavioral-FFT cases. Pre-existing on fft branch (known brittle).
2026-04-21 03:04:52 +05:45
Jason 60e49c7da6 feat(fpga): integrate 2048-pt FFT upgrade — non-conflicting RTL (wave 1/3)
File-scoped cherry-pick from feat/fft-2048-upgrade (e9705e4) for modules
that only the fft branch modified:

  RTL:
    cfar_ca.v                        512-row CFAR
    chirp_memory_loader_param.v      2-segment × 2048-sample loader
    doppler_processor.v              16384-deep doppler memory
    fft_engine.v                     2048-pt FFT
    matched_filter_multi_segment.v   2-seg overlap-save, BRAM overlap_cache
    matched_filter_processing_chain.v
    radar_mode_controller.v          XOR edge detector
    radar_params.vh                  (new) single source of truth
    range_bin_decimator.v            2048 -> 512 output bins
    rx_gain_control.v

  Memory:
    fft_twiddle_2048.mem             (new) 2048-pt FFT twiddles
    long_chirp_seg0_{i,q}.mem        2048-sample seg 0 (was 1024)
    long_chirp_seg1_{i,q}.mem        2048-sample seg 1 (was 1024)
    long_chirp_seg{2,3}_{i,q}.mem    deleted (4-seg -> 2-seg collapse)

  Gen:
    tb/cosim/gen_chirp_mem.py        regen script for mem files above

Waves 2 and 3 follow: manual merge for dual-modified files
(radar_system_top, usb_data_interface_ft2232h, mti_canceller,
radar_receiver_final), and CFAR pipeline from 2401f5f keeping p0's
CIC/DDC reset strategy.
2026-04-21 01:52:32 +05:45
Jason d0b3a4c969 fix(fpga): registered reset fan-out at 400 MHz; default USB to FT2232H
Replace direct !reset_n async sense with a registered active-high reset_h
(max_fanout=50) in nco_400m_enhanced, cic_decimator_4x_enhanced, and
ddc_400m.  The prior single-LUT1 / 700+ load net was the root cause of
WNS=-0.626 ns in the 400 MHz clock domain on the xc7a50t build.  Vivado
replicates the constrained register into ≈14 regional copies, each driving
≤50 loads, closing timing at 2.5 ns.

Change radar_system_top default USB_MODE from 0 (FT601) to 1 (FT2232H).
FT601 remains available for the 200T premium board via explicit parameter
override; the 50T production wrapper already hard-codes USB_MODE=1.

Regression: add usb_data_interface_ft2232h.v to PROD_RTL lint list and
both system-top TB compile commands; fix legacy radar_system_tb hierarchical
probe from gen_ft601.usb_inst to gen_ft2232h.usb_inst.

Golden reference files (rtl_bb_dc.csv, rx_final_doppler_out.csv,
golden_doppler.mem) regenerated to reflect the +1-cycle registered-reset
boundary behaviour; Receiver golden-compare passes 18/18 checks.

All 25 regression tests pass (0 failures, 0 skipped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 20:34:52 +05:45
Jason 15a9cde274 review(cosim): fix stale comment and wrong docstring derivation
golden_reference.py: update comment from 'Simplified' to 'Exact' to
match shaun0927's corrected formula.

fpga_model.py: fix adc_to_signed docstring that incorrectly derived
0x7F80 instead of 0xFF00. Verilog '/' binds tighter than '-', so
{1'b0,8'hFF,9'b0}/2 = 0x1FE00/2 = 0xFF00, not 0xFF<<8 = 0x7F80.
2026-04-16 11:07:56 +05:45
JunghwanNA a9ceb3c851 fix(cosim): align golden_reference ADC sign conversion with RTL
The golden reference used (adc_val - 128) << 9 which subtracts 65536,
but the Verilog RTL computes {1'b0,adc,9'b0} - {1'b0,8'hFF,9'b0}/2
which subtracts 0xFF00 = 65280. This creates a constant 256-LSB DC
offset between the golden reference and RTL for all 256 ADC values.

The bit-accurate model in fpga_model.py already uses the correct RTL
formula. This aligns golden_reference.py to match.

Verified: all 256 ADC input values now produce zero offset against
fpga_model.py.
2026-04-16 12:27:02 +09:00
Jason 063fa081fe fix: FPGA timing margins (WNS +0.002→+0.080ns) + 11 bug fixes from code review
FPGA timing (400MHz domain WNS: +0.339ns, was +0.002ns):
- DONT_TOUCH on BUFG to prevent AggressiveExplore cascade replication
- NCO→mixer pipeline registers break critical 1.5ns route
- Clock uncertainty reduced 200ps→100ps (adequate guardband)
- Updated golden/cosim references for +1 cycle pipeline latency

STM32 bug fixes:
- Guard uint32_t underflow in processStartFlag (length<4)
- Replace unbounded strcat in getSystemStatusForGUI with snprintf
- Early-return error masking in checkSystemHealth
- Add HAL_Delay in emergency blink loop

GUI bug fixes:
- Remove 0x03 from _HARDWARE_ONLY_OPCODES (was in both sets)
- Wire real error count in V7 diagnostics panel
- Fix _stop_demo showing 'Live' label during replay mode

FPGA comment fixes + CI: add test_v7.py to pytest command

Vivado build 50t passed: 0 failing endpoints, WHS=+0.056ns
2026-04-14 00:08:26 +05:45
Jason 2106e24952 fix: enforce strict ruff lint (17 rule sets) across entire repo
- Expand ruff config from E/F to 17 rule sets (B, RUF, SIM, PIE, T20,
  ARG, ERA, A, BLE, RET, ISC, TCH, UP, C4, PERF)
- Fix 907 lint errors across all Python files (GUI, FPGA cosim,
  schematics scripts, simulations, utilities, tools)
- Replace all blind except-Exception with specific exception types
- Remove commented-out dead code (ERA001) from cosim/simulation files
- Modernize typing: deprecated typing.List/Dict/Tuple to builtins
- Fix unused args/loop vars, ambiguous unicode, perf anti-patterns
- Delete legacy GUI files V1-V4
- Add V7 test suite, requirements files
- All CI jobs pass: ruff (0 errors), py_compile, pytest (92/92),
  MCU tests (20/20), FPGA regression (25/25)
2026-04-12 14:21:03 +05:45
Jason 519c95f452 fix: regenerate golden hex for dual-16pt Doppler and add real-data TBs to regression
Regenerate all real-data golden reference hex files against the current
dual 16-point FFT Doppler architecture (staggered-PRI sub-frames).
The old hex files were generated against the previous 32-point single-FFT
architecture and caused 2048/2048 mismatches in both strict real-data TBs.

Changes:
- Regenerate doppler_ref_i/q.hex, fullchain_doppler_ref_i/q.hex, and all
  downstream golden files (MTI, DC notch, CFAR) via golden_reference.py
- Add tb_doppler_realdata (exact-match, ADI CN0566 data) to regression
- Add tb_fullchain_realdata (exact-match, decim->Doppler chain) to regression
- Both TBs now pass: 2048/2048 bins exact match, MAX_ERROR=0
- Update CI comment: 23 -> 25 testbenches
- Fill in STALE_NOTICE.md with regeneration instructions

Regression: 25/25 pass, 0 fail, 0 skip. ruff check: 0 errors.
2026-04-09 02:36:14 +03:00
Jason 11aa590cf2 fix: full-repo ruff lint cleanup and CI migration to uv
Resolve all 374 ruff errors across 36 Python files (E501, E702, E722,
E741, F821, F841, invalid-syntax) bringing `ruff check .` to zero
errors repo-wide with line-length=100.

Rewrite CI workflow to use uv for dependency management, whole-repo
`ruff check .`, py_compile syntax gate, and merged python-tests job.
Add pyproject.toml with ruff config and uv dependency groups.

CI structure proposed by hcm444.
2026-04-09 02:05:34 +03:00
Jason 57de32b172 fix: resolve all ruff lint errors across V6+ GUIs, v7 module, and FPGA cosim scripts
Fixes 25 remaining manual lint errors after auto-fix pass (94 auto-fixed earlier):
- GUI_V6.py: noqa on availability imports, bare except, unused vars, F811 redefs
- GUI_V6_Demo.py: unused app variable
- v7/models.py: noqa F401 on 8 try/except availability-check imports
- FPGA cosim: unused header/status/span vars, ambiguous 'l' renamed to 'line',
  E701 while-on-one-line split, F841 padding vars annotated

Also adds v7/ module, GUI_PyQt_Map.py, and GUI_V7_PyQt.py to version control.
Expands CI lint job to cover all 21 maintained Python files (was 4).

All 58 Python tests pass. Zero ruff errors on all target files.
2026-04-08 19:11:40 +03:00
Jason 1e284767cd fix(test,docs): remove dead xfft_32 files, update test infra for dual-16 FFT, add regression guide
- Remove xfft_32.v, tb_xfft_32.v, and fft_twiddle_32.mem (dead code
  since PR #33 moved Doppler to dual 16-pt FFT architecture)
- Update run_regression.sh: xfft_16 in PROD_RTL, remove xfft_32 from
  EXTRA_RTL and all compile commands
- Update tb_fft_engine.v to test with N=16 / fft_twiddle_16.mem
- Update validate_mem_files.py: validate fft_twiddle_16.mem instead of 32
- Update testbenches and golden data from main_cleanup branch to match
  dual-16 architecture (tb_doppler_cosim, tb_doppler_realdata,
  tb_fullchain_realdata, tb_fullchain_mti_cfar_realdata, tb_system_e2e,
  radar_receiver_final, golden_doppler.mem)
- Update CONTRIBUTING.md with full regression test instructions covering
  FPGA, MCU, GUI, co-simulation, and formal verification

Regression: 23/23 FPGA, 20/20 MCU, 57/58 GUI, 56/56 mem validation,
all co-sim scenarios PASS.
2026-04-07 02:51:48 +03:00
Serhii ffc89f0bbd fix(rtl,gui,cosim,formal): adapt surrounding files for dual 16-pt FFT (follow-up to PR #33)
- radar_system_top.v: DC notch now masks to dop_bin[3:0] per sub-frame so both sub-frames get their DC zeroed correctly; rename DOPPLER_FFT_SIZE → DOPPLER_FRAME_CHIRPS to avoid confusion with the per-FFT size (now 16)
- radar_dashboard.py: remove fftshift (crosses sub-frame boundary), display raw Doppler bins, remove dead velocity constants
- golden_reference.py: model dual 16-pt FFT with per-sub-frame Hamming window, update DC notch and CFAR to match RTL
- fv_doppler_processor.sby: reference xfft_16.v / fft_twiddle_16.mem, raise BMC depth to 512 and cover to 1024
- fv_radar_mode_controller.sby: raise cover depth to 600
- fv_radar_mode_controller.v: pin cfg_* to reduced constants (documented as single-config proof), fix Property 5 mode guard, strengthen Cover 1
- STALE_NOTICE.md: document that real-data hex files are stale and need regeneration with external dataset

Closes #39
2026-04-06 23:15:50 +03:00
Jason a577b7628b Fix staggered-PRF Doppler processing with dual 16-point FFTs 2026-03-27 23:05:28 +02:00
Jason 4985eccbae Wire self-test results (0x31) to USB status readback path, add fpga_self_test to regression
- usb_data_interface.v: Add 3 self-test status inputs, expand status packet
  from 7 words (header + 5 data + footer) to 8 words (header + 6 data + footer).
  New status_words[5] carries {busy, detail[7:0], flags[4:0]}.
- radar_system_top.v: Wire self_test_flags_latched, self_test_detail_latched,
  self_test_busy to usb_data_interface ports. Add opcode 0x31 as status
  readback alias so host can read self-test results.
- tb_usb_data_interface.v: Add self-test port connections, verify word 5 in
  Group 16, add Group 18 (busy flag + partial failure variant). 81 checks pass.
- run_regression.sh: Add fpga_self_test.v to PROD_RTL lint list and system-
  level compile lists. Add tb_fpga_self_test as Phase 1 unit test.
- 24/24 regression tests pass, lint clean (0 errors, 4 advisory warnings).
2026-03-20 20:03:11 +02:00
Jason f8d80cc96e Add radar dashboard GUI with replay mode for real ADI CN0566 data visualization, FPGA self-test module, and co-sim npy arrays 2026-03-20 19:02:06 +02:00
Jason 7a44f19432 Full-chain MTI+CFAR real-data co-simulation: bit-exact match across all 10247 checkpoints (decim->MTI->Doppler->DC notch->CFAR) using ADI CN0566 data 2026-03-20 17:16:12 +02:00
Jason e93bc33c6c Production fixes 1-7: detection bugs, cfar→threshold rename, digital gain control, Doppler mismatch protection, decimator watchdog, bypass_mode dead code removal, range-mode register (21/21 regression PASS)
Fix 1: Combinational magnitude + non-sticky detection flag (tb: 23/23)
Fix 2: Rename all cfar_* signals to detect_*/threshold_* (honest naming)
Fix 3: New rx_gain_control.v between DDC and FFT, opcode 0x16 (tb: 33/33)
Fix 4: Clamp host_chirps_per_elev to DOPPLER_FFT_SIZE, error flag (E2E: 54/54)
Fix 5: Decimator watchdog timeout, 256-cycle limit (tb: 63/63)
Fix 6: Remove bypass_mode dead code from ddc_400m.v (DDC tb: 21/21)
Fix 7: Range-mode register 0x20 with status readback (USB tb: 77/77)
2026-03-20 04:38:35 +02:00
Jason 0b0643619c Real-data co-simulation: range FFT, Doppler, full-chain all 2048/2048 exact match
ADI CN0566 Phaser 10.525 GHz X-band radar data validation:
- golden_reference.py: bit-accurate Python model with range_bin_decimator
- tb_range_fft_realdata.v: 1024/1024 exact match
- tb_doppler_realdata.v: 2048/2048 exact match
- tb_fullchain_realdata.v: decimator+Doppler 2048/2048 exact match
- 19/19 FPGA regression unaffected
2026-03-20 03:19:22 +02:00
Jason 0773001708 E2E integration test + RTL fixes: mixer sequencing, USB data-pending flags, receiver toggle wiring (19/19 FPGA)
RTL fixes discovered via new end-to-end testbench:
- plfm_chirp_controller: TX/RX mixer enables now mutually exclusive
  by FSM state (Fix #4), preventing simultaneous TX+RX activation
- usb_data_interface: stream control reset default 3'b001 (range-only),
  added doppler/cfar data_pending sticky flags, write FSM triggers on
  range_valid only — eliminates startup deadlock (Fix #5)
- radar_receiver_final: STM32 toggle signals wired through for mode-00
  pass-through, dynamic frame detection via host_chirps_per_elev
- radar_system_top: STM32 toggle signal wiring to receiver instance
- chirp_memory_loader_param: explicit readmemh range for short chirp

Test infrastructure:
- New tb_system_e2e.v: 46 checks across 12 groups (reset, TX, safety,
  RX, USB R/W, CDC, beam scanning, reset recovery, stream control,
  latency budgets, watchdog)
- tb_usb_data_interface: Tests 21/22/56 updated for data_pending
  architecture (preload flags, verify consumption instead of state)
- tb_chirp_controller: mixer tests T7.1/T7.2 updated for Fix #4
- run_regression.sh: PASS/FAIL regex fixed to match only [PASS]/[FAIL]
  markers, added E2E test entry
- Updated rx_final_doppler_out.csv golden data
2026-03-20 01:45:00 +02:00
Jason c6103b37de Gap 7 MMCM jitter cleaner + CIC comb CREG pipeline + XDC clock-name fix
MMCM (Gap 7):
- Add adc_clk_mmcm.v: MMCME2_ADV wrapper (VCO=800MHz, CLKOUT0=400MHz)
- Modify ad9484_interface_400m.v: replace BUFG with MMCM path, gate reset on mmcm_locked
- Add adc_clk_mmcm.xdc: CDC false paths for clk_mmcm_out0 <-> clk_100m

XDC Fix (Build 19 WNS=-0.011 root cause):
- Remove conflicting create_generated_clock -name clk_400m_mmcm
- Replace all clk_400m_mmcm references with Vivado auto-generated clk_mmcm_out0
- CDC false paths now correctly apply to actual timing paths

CIC CREG Pipeline (Build 18 critical path fix):
- Explicit DSP48E1 for comb[0] with CREG=1/AREG=1/BREG=1/PREG=1
- Absorbs integrator_sampled_comb fabric FDRE into DSP48 C-port register
- Eliminates 0.643ns fabric->DSP routing delay (Build 18 tightest path)
- +1 cycle comb latency via data_valid_comb_0_out pipeline
- Move shared register declarations above ifndef SIMULATION (iverilog fix)
- Update golden data for +1 cycle CIC pipeline shift

Build scripts: build19_mmcm.tcl, build20_mmcm_creg.tcl
Regression: 18/18 FPGA pass, 20/20 MCU pass
Build 20 launched on remote Vivado (pending results)
2026-03-19 22:59:46 +02:00
Jason 94ffdb8f77 Add Phase 0 Vivado-style lint to regression runner, update golden data
Adds two-layer lint pass (iverilog -Wall + custom static checks) that
catches part-select OOB errors and case-without-default warnings before
pushing to remote Vivado. Catches the exact Synth 8-524 class error that
broke Build 18 initial attempt. Lint errors abort regression; warnings
are advisory. Regenerated golden data for BRAM-migrated matched filter.
2026-03-19 21:19:07 +02:00
Jason ed6f79c6d3 FIR DSP48 pipelining (BREG+MREG) + matched filter BRAM migration with overlap cache
FIR: Add coeff_reg/mult_reg pipeline stages to fix 68 DPIP-1 + 35 DPOP-2
DRC warnings. Valid pipeline widened 7→9 bits (+2 cycle latency).

Matched filter: Migrate input_buffer_i/q from register arrays to BRAM
(~33K FF savings). Overlap-save uses register cache captured during
ST_PROCESSING to avoid BRAM read/write conflicts during overlap copy.
New ST_OVERLAP_COPY state writes cached tail samples back sequentially.

Both changes pass 18/18 FPGA regression. Golden data regenerated for
+2 FIR latency baseline.
2026-03-19 20:39:01 +02:00
Jason 463ebef554 CIC comb pipeline registers, BUFG sim guard, system TB fix, regression runner
- cic_decimator_4x_enhanced.v: Add integrator_sampled_comb and
  data_valid_comb_pipe pipeline stages between integrator sampling and
  comb computation to break the critical path (matches remote 40cda0f)
- radar_system_top.v: Wrap 3 BUFG instances in ifdef SIMULATION guard
  with pass-through assigns for iverilog compatibility
- radar_system_tb.v: Convert generate_radar_echo function to task and
  move sin_lut declaration before task (iverilog declaration-order fix),
  add modular index clamping to prevent LUT out-of-bounds
- run_regression.sh: Automated regression runner for all 18 FPGA
  testbenches with --quick mode. Results: 17 pass, 1 pre-existing fail
- .gitignore: Exclude *.vvp, *.vcd simulation artifacts
2026-03-19 11:31:46 +02:00
Jason f6877aab64 Phase 1 hardware bring-up prep: ILA debug probes, CDC waivers, programming scripts
- Rename latency_buffer_2159 -> latency_buffer (module + file + all refs)
- Add CDC waivers for 5 verified false-positive criticals to XDC
- Add ILA debug probe insertion script (4 cores, 126 probe bits, 2 clock domains)
- Add FPGA programming script (7-step flow with DONE pin verification)
- Add ILA capture script (4 scenarios + health check, CSV export)
- Add debug_ila.xdc with MARK_DEBUG fallback attributes
- Full regression clean: 13/13 suites, 266/266 checks, 2048/2048 golden match
2026-03-18 01:28:42 +02:00
Jason fcf3999e39 Fix CDC reset domain bug (P0), strengthen testbenches with 31 structural assertions
Split cdc_adc_to_processing reset_n into src_reset_n/dst_reset_n so source
and destination clock domains use correctly-synchronized resets. Previously
cdc_chirp_counter's destination-side sync chain (100MHz) was reset by
sys_reset_120m_n (120MHz domain), causing 30 CDC critical warnings.

RTL changes:
- cdc_modules.v: split reset port, source logic uses src_reset_n,
  destination sync chains + output logic use dst_reset_n
- radar_system_top.v: cdc_chirp_counter gets proper per-domain resets
- ddc_400m.v: CDC_FIR_i/q use reset_n_400m (src) and reset_n (dst)
- formal/fv_cdc_adc.v: updated wrapper for new port interface

Build 7 fixes (previously untouched):
- radar_transmitter.v: SPI level-shifter assigns, STM32 GPIO CDC sync
- latency_buffer_2159.v: BRAM read registration
- constraints: ft601 IOB -quiet fix
- tb_latency_buffer.v: updated for BRAM changes

Testbench hardening (tb_cdc_modules.v, +31 new assertions):
- A5-A7: split-domain reset tests (staggered deassertion, independent
  dst reset while src active — catches the P0 bug class)
- A8: port connectivity (no X/Z on outputs)
- B7: cdc_single_bit port connectivity
- C6: cdc_handshake reset recovery + port connectivity

Full regression: 13/13 test suites pass (257 total assertions).
2026-03-17 19:38:09 +02:00
Jason 6fc5a10785 Fix range_bin_decimator overflow guard priority bug: group completion now takes precedence over overflow guard in ST_PROCESS, ensuring all OUTPUT_BINS outputs are emitted when sufficient input samples exist. Split formal property 5 into 5a (upper bound) and 5b (exact count when start_bin=0), added Cover 4 for overflow guard path, reduced BMC depth to 50. 2026-03-17 15:41:06 +02:00
Jason 5fd632bc47 Fix all 10 CDC bugs from report_cdc audit, add overflow guard in range_bin_decimator
CDC fixes across 6 RTL files based on post-implementation report_cdc analysis:
- P0: sync stm32_mixers_enable and new_chirp_pulse to clk_120m via toggle CDC
       in radar_transmitter, add ft601 reset synchronizer and USB holding
       registers with proper edge detection in usb_data_interface
- P1: add ASYNC_REG to edge_detector, convert new_chirp_frame to toggle CDC,
       fix USB valid edge detect to use fully-synced signal
- P2: register Gray encoding in cdc_adc_to_processing source domain, sync
       ft601_txe and stm32_mixers_enable for status_reg in radar_system_top
- Safety: add in_bin_count overflow guard in range_bin_decimator to prevent
          downstream BRAM corruption

All 13 regression test suites pass (159 individual tests).
2026-03-17 13:48:47 +02:00