17 Commits

Author SHA1 Message Date
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
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
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
ea2615ef84 doppler: gate S_IDLE→S_ACCUMULATE on frame_start_pulse (AUDIT-S3)
Pre-fix S_IDLE had two independent if-branches: one for frame_start_pulse
(resets pointers) and one for data_valid (transitions to S_ACCUMULATE).
A data_valid arriving before frame_start_pulse would advance the FSM with
whatever pointers happened to be live, and the BRAM write block would write
the sample into mem_write_addr = (write_chirp_index*RANGE_BINS) + 0.

In current operation the race is benign — end-of-S_ACCUMULATE always zeros
write_chirp_index/write_range_bin (line 287-288) and the MF pipeline latency
(~165 µs) is millions of cycles longer than the frame_start CDC latency
(~50 ns), so frame_start always arrives first. But the FSM relies on an
undocumented system-level invariant; a future code path that leaves
pointers stale on entry to S_IDLE would silently corrupt the first sample.

Fix: add a `frame_armed` register set when frame_start_pulse arrives in
S_IDLE, cleared on transition to S_ACCUMULATE. Both the FSM transition and
the BRAM write block gate on `(frame_start_pulse || frame_armed)`. The OR
admits the same-cycle case where both arrive together (write to addr 0
still resolves correctly because both blocks use the same gate).

Verification: tb_doppler_frame_start_gate 21/21 PASS, quick regression
32/32 PASS (was 31/31; +1 new test, 0 regressions). tb_doppler_realdata
(full FFT pipeline) still passes — gate transparent to normal operation.
2026-04-29 18:36:31 +05:45
Jason
9d1eb4b11c fix(radar): RX chain corrections, GUI bin alignment, MCU boot ordering
FPGA — RX chain
  matched_filter_multi_segment.v: drop the gratuitous /4 scaling on
    DDC sign-extended input (was ddc_i[17:2] + ddc_i[1]); use
    ddc_i[15:0] directly. fft_engine has INTERNAL_W=32 with
    saturating 16-bit output, so full 16-bit input is safe. Restores
    ~12 dB of MF input dynamic range.
  radar_receiver_final.v: remove latency_buffer (count-N-pulses-then-
    prime FIFO that left frame 1 with all-zero ref). Replaced with
    a single-FF alignment register on ref_i/ref_q that matches the
    1-FF stage multi_segment ST_PROCESSING uses on adc_data.
    Verified by tb/tb_rxb_fullchain_latency.v — autocorrelation peak
    at bin 0 with peak/mean ~88x.
  doppler_processor.v / mti_canceller.v / cfar_ca.v /
    range_bin_decimator.v / radar_receiver_final.v / radar_system_top.v
    / usb_data_interface_ft2232h.v: switch port and parameter widths
    from RP_NUM_RANGE_BINS / RP_RANGE_BIN_BITS (always 512 / 9-bit)
    to RP_MAX_OUTPUT_BINS / RP_RANGE_BIN_WIDTH_MAX (auto-scales:
    50T 512 / 9-bit, 200T 4096 / 12-bit). Unblocks 200T 20 km mode
    at the RX module boundary; USB wire-protocol extension still
    pending.
  radar_receiver_final.v: doppler_frame_done_prev reset value 0 -> 1
    to prevent false done pulse on cycle 1 when level signal is
    HIGH at reset.
  matched_filter_processing_chain.v: delete the broken `ifdef
    SIMULATION inline behavioural FFT (482 lines removed). It
    produced wrong-bin peaks and 100-1000x weak magnitudes. Chain
    now uses production fft_engine.v + frequency_matched_filter.v
    in both iverilog and Vivado. Iverilog tests are ~38x slower per
    chain pass but produce correct results. Misleading "OK with
    Xilinx IP" comments at three test sites updated since the FFT
    is in-house, not an IP placeholder.

FPGA — testbenches
  tb/tb_rxb_latency_measure.v (new): measures chain internal pipeline
    depth (~2057 cycles, chirp-agnostic).
  tb/tb_rxb_fullchain_latency.v (new): full-chain autocorrelation
    verification — drives ddc with the same chirp samples the loader
    serves as ref, finds peak position and peak/mean.
  tb/tb_matched_filter_processing_chain.v: wait timeouts bumped
    50000 -> 500000 cycles to accommodate production FFT pipeline.

MCU
  main.cpp checkSystemHealthStatus: latch system_emergency_state on
    the error_count > 10 path so the SAFE-MODE blink loop in main()
    actually engages (was bypassed because predicate was false).
  main.cpp: move FPGA reset BEFORE the if(PowerAmplifier) block so
    adar_tr_x is driven LOW (RX commanded externally) before PA Vdd
    reaches 22 V. Old reset block at the original location removed.
  main.cpp MX_GPIO_Init: add GPIO_PIN_12 (FPGA reset) to the
    explicit WritePin(LOW) list so the safe initial state is no
    longer implicit.
  main.cpp checkSystemHealth: rate-limit ADAR1000
    verifyDeviceCommunication (HAL_Delay 1ms x 4 devices = 4 ms
    blocking SPI burst per main-loop iteration) from every-loop to
    every 2 s. readTemperature stays per-loop so over-temp
    detection latency is unchanged.
  USBHandler.cpp processSettingsData: dispatch threshold bumped
    74 -> 82 (matches parser minimum); buffer drained after parse
    attempt (slide remaining bytes left) so a false END find no
    longer sticks the buffer until 256-byte overflow.

GUI
  radar_protocol.py: NUM_RANGE_BINS 64 -> 512 (matches FPGA
    RP_NUM_RANGE_BINS); NUM_CELLS 2048 -> 16384.
  radar_protocol.py _ingest_sample: honor FPGA frame_start bit for
    resync after a USB drop; capture range_profile[rbin] once per
    range bin at dbin == 0 (FPGA emits the same range_i/range_q for
    all 32 Doppler cells of a given range bin; previous accumulator
    inflated the profile 32x).
  v7/models.py RadarSettings: range_resolution 24 -> 6 m (matches
    c/(2*100MHz)*4); max_distance and coverage_radius 1536 -> 3072 m;
    map_size 2000 -> 4000.
  v7/models.py WaveformConfig: n_range_bins 64 -> 512, fft_size
    1024 -> 2048, decimation_factor 16 -> 4.
  GUI_V65_Tk.py: _RANGE_PER_BIN math and stale "~24 m / ~1536 m"
    comments updated.
  test_v7.py: assertion values updated to match new defaults.

Tests
  test_ddc_cosim_fuzz.py: remove unused os/tempfile imports, wrap
    three long lines for ruff E501 compliance.
2026-04-23 05:56:52 +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
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
a577b7628b Fix staggered-PRF Doppler processing with dual 16-point FFTs 2026-03-27 23:05:28 +02:00
Jason
49c9aa28ad Fix Bug #11 (platform SPI transmit-only), FPGA B2 (chirp BRAM migration), FPGA B3 (DSP48 pipelining)
Bug #11: platform_noos_stm32.c used HAL_SPI_Transmit instead of
HAL_SPI_TransmitReceive — reads returned garbage. Changed to in-place
full-duplex. Dead code (never called), fixed per audit recommendation.
Test added: test_bug11_platform_spi_transmit_only.c. Mock infrastructure
updated with SPI spy types. All 11 firmware tests pass.

FPGA B2: Migrated long_chirp_lut[0:3599] from ~700 lines of hardcoded
assignments to BRAM with (* ram_style = "block" *) attribute and
$readmemh("long_chirp_lut.mem"). Added sync-only read block for proper
BRAM inference. 1-cycle read latency introduced. short_chirp_lut left
as distributed RAM (60 entries, too small for BRAM).

FPGA B3: Added BREG (window_val_reg) and MREG (mult_i_raw/mult_q_raw)
pipeline stages to doppler_processor.v. Eliminates DPIP-1 and DPOP-2
DRC warnings. S_LOAD_FFT retimed: fft_input_valid starts at sub=2,
+1 cycle total latency. BREG primed in S_PRE_READ at no extra cost.
Both FPGA files compile clean with Icarus Verilog.
2026-03-19 10:31:16 +02:00
Jason
254c0e6f03 Improve timing margins with targeted datapath register tuning
Reduce routing pressure on CIC/NCO critical paths and move Doppler BRAM read-address registers to sync-reset datapath logic so Build 13 closes with stronger setup/hold slack while preserving functional behavior.
2026-03-17 23:51:04 +02:00
Jason
1558f17d05 Convert async→sync reset on DSP/BRAM datapath registers for timing closure
P1-CRITICAL: doppler_processor.v — split FSM into control (async reset)
and BRAM/DSP datapath (sync reset) blocks. Fixes REQP-1839/1840 BRAM
address register corruption risk; enables DSP48 absorption of window
multipliers (mult_i/q).

P1-CRITICAL: frequency_matched_filter.v — convert all 4 pipeline stages
(input capture, multiply, add, saturate) from async to sync reset.
Enables DSP48E1 absorption of complex multiplier registers.

P1-HIGH: fir_lowpass.v — convert adder tree (L0-L4), output stage, and
valid pipeline from async to sync reset. Fixes 856 DPOR-1 warnings
(428 per FIR instance × 2 I/Q channels), enabling DSP48 absorption
of the entire pipelined adder tree.

Expected impact: eliminate ~1000 DRC warnings, improve WNS from +0.019ns
by enabling Vivado to absorb hundreds of registers into DSP48E1/BRAM
hard blocks. Full regression: 13/13 test suites pass (257+ assertions).
2026-03-17 20:11:13 +02:00
Jason
fb59e98737 Add SymbiYosys formal verification for 6 modules, fix 2 doppler bugs
Formal verification (SymbiYosys + smtbmc/z3):
- cdc_single_bit: BMC PASS depth 80, cover PASS 3/3
- cdc_handshake: BMC PASS depth 100, cover PASS 4/4
- cdc_adc_to_processing: BMC PASS depth 80, cover PASS
- radar_mode_controller: BMC PASS depth 200, cover PASS 8/8
- range_bin_decimator: cover PASS 7/7, BMC running (step 61+)
- doppler_processor: cover running (step 133/150), BMC running (step 35+)

DUT bug fixes found by formal:
- doppler_processor: write_chirp_index overflow past CHIRPS_PER_FRAME-1
  in S_ACCUMULATE frame-complete branch (reset to 0)
- doppler_processor: read_doppler_index unclamped prefetch in S_LOAD_FFT
  causing OOB BRAM reads (clamped to DOPPLER_FFT_SIZE-1)

CDC fix (prior session, included):
- cdc_modules: async reset changed to sync reset on all CDC sync chains
  to prevent metastability on reset deassertion

RTL changes for formal observability:
- Added ifdef FORMAL output ports to cdc_handshake (6), cdc_adc (2),
  radar_mode_controller (2), range_bin_decimator (5), doppler_processor (11)
2026-03-17 12:47:22 +02:00
Jason
39f78d4349 Fix RTL Bug #3: S_IDLE->S_ACCUMULATE now writes first sample immediately
Previously the S_IDLE->S_ACCUMULATE transition consumed one data_valid
cycle without writing to BRAM, losing the first sample. The testbench
worked around this by sending sample[0] twice.

Fix: drive mem_we + data capture in S_IDLE on the transition cycle and
advance write_range_bin to 1. Testbench workaround removed.

Verified: 3/3 Doppler co-sim BIT-PERFECT, integration test 10/10 PASS.
2026-03-16 19:08:16 +02:00
Jason
17731dd482 Fix doppler_processor windowing pipeline bugs + multi-segment buffer_write_ptr bug, add co-sim suites
RTL bug fixes:
- doppler_processor.v: Add S_PRE_READ state to prime BRAM pipeline, restructure
  S_LOAD_FFT with sub-counter staging, fix BRAM address off-by-one
  (read_doppler_index <= fft_sample_counter + 2, was +1). All 3 Doppler
  co-sim scenarios now achieve BIT-PERFECT match (correlation=1.0, energy=1.0).
- matched_filter_multi_segment.v: Move buffer_write_ptr >= SEGMENT_ADVANCE check
  outside if(ddc_valid) block to prevent FSM deadlock. 32/32 tests PASS.

New co-simulation infrastructure:
- Doppler co-sim: tb_doppler_cosim.v (14/14 structural checks),
  gen_doppler_golden.py (3 scenarios: stationary/moving/two_targets),
  compare_doppler.py (bit-perfect thresholds)
- Multi-segment co-sim: tb_multiseg_cosim.v (32/32), gen_multiseg_golden.py
  with short and long test vector suites
2026-03-16 18:09:26 +02:00
Jason
692b6a3bfa Replace FFT stubs with synthesizable radix-2 DIT engine, fix BRAM inference
Implement iterative single-butterfly FFT engine (fft_engine.v) supporting
1024-pt and 32-pt transforms with quarter-wave twiddle ROM, XPM_MEMORY_TDPRAM
for guaranteed BRAM mapping in Vivado, and behavioral model for simulation.

Add xfft_32.v AXI-Stream wrapper for doppler_processor integration and
dual-branch matched_filter_processing_chain.v (behavioral + synthesis paths).

Fix placement failure caused by 68K+ registers from dissolved memory arrays:
- doppler_processor.v: extract mem writes to sync-only always block for BRAM
- xfft_32.v: extract buffer writes to sync-only always block for LUTRAM

Post-implementation: 37K regs (29%), 23K LUTs (37%), 10 BRAM (7%), fully routed.
All testbenches pass: fft_engine 12/12, xfft_32 10/10, mf_chain 27/27.
2026-03-16 10:25:07 +02:00
NawfalMotii79
5fbe97fa5f
Add files via upload 2026-03-09 00:17:39 +00:00