Refactor v7.WaveformConfig from single-PRI to PR-Q's 3-PRI staggered
ladder (SHORT 175 us / MEDIUM 161 us / LONG 167 us) and update the
host-side bulk-frame parser dimension to match the FPGA's 48-bin
Doppler output (RP_NUM_DOPPLER_BINS = 48). The parser was rejecting
every production frame with n_doppler != 32, masking the PR-F widening
end-to-end.
WaveformConfig:
- pri_short_s/pri_medium_s/pri_long_s replace single pri_s
- n_doppler_bins 32 -> 48; new num_subframes=3
- Per-subframe velocity_resolution_{short,medium,long}_mps
- Per-subframe max_velocity_{short,medium,long}_mps
- extended_max_velocity_mps_crt(K=6) for 3-PRI alias-resolution ceiling
- Drop pri_s, velocity_resolution_mps, max_velocity_mps (no aliases)
Other:
- radar_protocol.NUM_DOPPLER_BINS 32 -> 48 (NUM_CELLS auto 16384 -> 24576;
BULK_FRAME_MAX_SIZE flows from NUM_CELLS, no other edits needed)
- v7/dashboard.py constant + stale "(64x32)" title replaced with f-string
- v7/processing.py 32-bin fallback -> 48
- v7/workers.py: derive doppler_center from frame.shape; LONG-PRI v_res
used as conservative single-PRI placeholder until PR-Q.5 lands the
CRT extractor (markers in place at both call sites)
- test_v7.py: TestWaveformConfig rewritten (8 tests, per-subframe + CRT
extension); TestExtractTargetsFromFrame center 16 -> 24
Local tests:
TestWaveformConfig 8/8 PASS
TestExtractTargetsFromFrame 6/6 PASS
test_GUI_V65_Tk 117/0/2 PASS
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).
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.
Compiles + runs the MF chain TB under Vivado XSim with FFT_USE_XILINX_IP
defined, exercising matched_filter_processing_chain →
fft_engine_axi_bridge → xfft_2048 → real LogiCORE FFT v9.1 IP.
Symlinks tb/ into the work dir so $readmemh("tb/mf_golden_*.hex")
resolves from xsim's CWD.
This validates the chain glue (FSM, BRAMs, conj-mult, sat-truncate) works
correctly against the actual IP timing/scaling, not just the iverilog
fft_engine.v fallback.
Output: /tmp/mf_chain_xsim.log; xsim run takes ~40 min on the remote box.
Wrapper xfft_2048.v had m_axis_data_tuser and m_axis_status_{tdata,tvalid,
tready} hooked up to the IP, but the regenerated xfft_2048_ip in scaled
mode + Pipelined Streaming + 1 channel + no XK_INDEX/OVFLO doesn't expose
those ports. xelab errored "cannot find port" on all four. Removed.
run_xfft_xsim.sh missed -i "$PROJ_ROOT" so xvlog couldn't resolve
`include "radar_params.vh"` from inside tb/. Fixed.
gen_xfft_2048_ip.tcl header comment described the old Burst I/O 11-stage
schedule; updated to PG109 Pipelined Streaming pair-grouped layout that
matches the actual SCALE_SCH = 12'hAA9 we now drive.
Verified: tb_xfft_2048_xsim 5/5 PASS on real LogiCORE FFT v9.1 IP under
Vivado 2025.2 xsim — DC peak at bin 0, impulse flat spectrum, tone at
bin 128. Closes T-10 (FFT-block synth-mode validation).
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.
The AD9484 is SDR LVDS — datasheet p.5 lists "Output (LVDS—SDR)" as the
only output mode and p.16 confirms "data outputs are valid on the rising
edge of DCO." DCO runs at fs (400 MHz), one new sample per period, held
stable across the period. There is no DDR mode and no SPI access (CSB is
tied to +1V8 on the production board, RADAR_Main_Board.sch:46719).
ad9484_interface_400m.v previously instantiated an IDDR per data bit and
alternated Q1/Q2 via a `dco_phase` FSM, expecting to demux a "DDR" stream
into 400 MSPS. Because the chip is SDR, both Q1 and Q2 represent the same
sample, and the alternation produced approximately
[s_{-1}, s_1, s_1, s_3, s_3, s_5, …]
— odd-sample duplication with even-sample loss, equivalent to
decimate-by-2 followed by ZOH-upsample-by-2. In the frequency domain
that's a fold around fs/4 = 100 MHz; our 120-150 MHz IF lands at
50-80 MHz, so the DDC's 120 MHz NCO mixes the wrong frequency and the
matched filter sees baseband 40-70 MHz off where it expects.
The bug was hidden by tb/ad9484_interface_400m_stub.v, which has always
done single-rising-edge SDR-correct capture, so all iverilog regression
ran against the correct semantics — only the synthesizable Xilinx-
primitive path was wrong. This bug only fires on real silicon.
Fix:
- ad9484_interface_400m.v: drop IDDR + dco_phase; capture each data bit
with a single (* IOB = "TRUE" *) negedge-clocked IFF on adc_dco_bufio.
Falling DCO sits 1.25 ns inside AD9484's stable window, giving ~0.4 ns
setup margin against tPD = 0.85 ns. Same pattern on the OR (overrange)
path. Output FSM now emits one Q per BUFG cycle = clean 400 MSPS.
- tb_ad9484_xsim.v: add Test Group 8 (AUDIT-C4) that drives a 64-sample
counter ramp synchronously with rising DCO, captures the output, and
asserts (a) consecutive deltas equal +1 for ≥ (captured-6) of the
stream, (b) zero duplicate samples (catches DDR-style demux), (c) zero
unexpected jumps (catches DDR-style sample drops). This locks in SDR
semantics so any future regression that reintroduces a DDR demux on
this chip fails loudly.
- ad9484_interface_400m_stub.v: comment-only update — the stub already
does correct SDR capture; document AUDIT-C4 + why iverilog regression
was silent on the synth-path bug.
- xc7a200t_fbg484.xdc: fix stale "DDR class" comment near the OR pair
(now "SDR LVDS").
Verification: bash run_regression.sh — 42 passed, 0 failed, 1 skipped
(the skip is the T-6 drift cosim, which needs scipy from the dev group;
CI installs it via uv sync --group dev). Test Group 8 in the xsim TB
runs against the real UNISIM primitives and is exercised separately on
the Vivado host (run_xfft_xsim.sh-style flow).
The CFAR threshold (alpha) lives in a Q4.4 host register and is loaded
from RP_DEF_CFAR_ALPHA / _SOFT at boot (3.0 / 1.5 in Q4.4). With PR-M.2
swapping the Doppler window from a non-canonical "Hamming-ish" LUT
(PSL=-33 dB) to Dolph-Chebyshev 60 dB (PSL=-60 dB), training-cell
contamination from off-Doppler sidelobes drops by 27 dB and the
effective Pfa at the shipped alpha drops accordingly.
This commit is documentation only — defaults are not changed pre-HW.
Two operating-point options for HW bring-up:
(a) Hold alpha — get higher Pd at lower Pfa as a free win.
(b) Lower alpha — recover original Pfa, get even higher Pd.
Recommended bring-up procedure recorded in cfar_ca.v header:
1. Collect noise-only frames (no targets in dwell).
2. Measure empirical Pfa at shipped alpha=3.0 / 1.5.
3. If Pfa < 0.5 x design target, lower alpha; otherwise hold.
Opcodes 0x23 (RP_OP_CFAR_ALPHA) and 0x2D (RP_OP_CFAR_ALPHA_SOFT) let
the host adjust at runtime without firmware change.
Files:
* cfar_ca.v — adds "Doppler-window dependency" block to the header
after the existing "Threshold computation" block.
* radar_params.vh — adds a note above RP_DEF_CFAR_ALPHA pointing at
cfar_ca.v for the rationale.
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
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).
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.
The TB hard-coded /32.0 in cosine/sine angle math and read out_re[28] /
out_re[30] which don't exist for N=16, so 3/12 checks failed (Test 3
single-tone, Test 7 imag-tone). Pure TB math error — fft_engine.v is
correct (proven by every production MF/Doppler cosim passing bit-exact).
Test 3: /32.0 → /N, peak expected = N/2*1000 = 8000 (not 16000),
conjugate read at bin N-4=12 (not 28).
Test 7: /32.0 → /N, conjugate peak at bin N-2=14 (not 30).
Result: 12/12 PASS at N=16 with bin 4 = 7997 ≈ 8000.
Closes T-4. Final regression: 42 passed / 0 failed of 42 — first
all-green since PR-Tests-1 exposed hidden failures.
matched_filter_multi_segment.v ingestion model rewritten to capture the
full chirp into a single 4096-deep input BRAM during ST_COLLECT_DATA,
then slide non-destructive segment windows over the stable buffer:
segment N reads buffer[N*SEGMENT_ADVANCE .. N*SEGMENT_ADVANCE+2047]
segment_offset advances by SEGMENT_ADVANCE in ST_NEXT_SEGMENT.
Replaces the original overlap-save scheme, which assumed the input ddc
stream stayed live across segment processing. That contract breaks
because chain processing (~70 us at production xfft_2048 timing,
~1.7 ms in the iverilog batched fallback) outlasts the LONG chirp
duration (30 us). Segment-1 input samples (chirp samples 2048..2999)
arrived during segment 0's ST_PROCESSING / ST_WAIT_FFT and were
silently dropped, so segment 1 hung forever in ST_COLLECT_DATA waiting
for ddc_valid that never came. PR-J.1 (8b6f2ec) localised the failure;
this is the fix.
Removed:
ST_OVERLAP_COPY state (state 8)
overlap_cache_i/q (128-entry distributed RAM)
overlap_copy_count, ov_we / ov_waddr / ov_wdata signals
overlap_cache write port + accompanying always block
ST_PROCESSING's mid-stream tail-cache writes
Added:
segment_offset (12-bit, advances by SEGMENT_ADVANCE per segment)
samples_fed (12-bit per-segment FFT-input counter)
LONG_FILL_END parameter ((LONG_SEGMENTS-1)*SEGMENT_ADVANCE +
BUFFER_SIZE = 3968 for 50T)
Address-width changes:
buffer_write_ptr / buffer_read_ptr / buf_waddr / buf_raddr 11-bit
-> 12-bit (INPUT_BUF_ADDR_W)
sample_addr_out (port to chirp_reference_rom) now driven from
samples_fed[10:0] — per-segment 0..2047 contract preserved.
State machine summary:
ST_IDLE -> ST_COLLECT_DATA on chirp_pulse
ST_COLLECT_DATA -> ST_ZERO_PAD when full chirp ingested
ST_ZERO_PAD -> ST_WAIT_REF (segment 0)
ST_WAIT_REF -> ST_PROCESSING (mem_ready, buf_raddr presented at
segment_offset)
ST_PROCESSING -> ST_WAIT_FFT after FFT_SIZE samples fed
ST_WAIT_FFT -> ST_OUTPUT on chain idle + saw_chain_output
ST_OUTPUT -> ST_NEXT_SEGMENT (more segments) | ST_IDLE (done)
ST_NEXT_SEGMENT -> ST_WAIT_REF (segment_offset += SEGMENT_ADVANCE,
segment_request bumped, mem_request)
Verification (tb_mf_long_chirp, +WAVE=N):
SHORT (1 segment): 2048/2048 pc_valid pulses, 167997 cycles
MEDIUM (1 segment): 2048/2048 pc_valid pulses, 167997 cycles
LONG (2 segments): 4096/4096 pc_valid pulses, 335858 cycles
-- vs pre-PR-J.2 LONG: hung in ST_COLLECT_DATA, 2048/4096.
Full regression: 41 passed / 1 failed (only failure is the pre-existing
FFT Engine test, unrelated to this PR — same baseline as pre-PR-J.2).
200T (SUPPORT_LONG_RANGE) variant will need INPUT_BUF_DEPTH bumped to
16384; a runtime parameter or `ifdef can wire that when 200T is
actually built. tb_mf_long_chirp HARD_BUDGET_CYCLES bumped 200k -> 500k
to fit two iverilog-fallback FFT passes.
Standalone diagnostic TB that drives a single chirp (SHORT/MEDIUM/LONG
selectable via +WAVE=N plusarg) through the production matched_filter
stack — chirp_reference_rom -> matched_filter_multi_segment ->
matched_filter_processing_chain (xfft_2048 + frequency_matched_filter)
— and logs every state transition of:
ms_state, ch_state, mem_request/mem_ready, segment_request,
current_segment, pc_valid, ms_status
Used to localise the LONG-chirp hang surfaced by tb_system_dataflow.
Findings (this run, iverilog SIMULATION fallback path):
SHORT (1 segment, 100 samples): PASS, 168 k cycles, 2048 pc_valid.
MEDIUM (1 segment, 500 samples): PASS, 168 k cycles, 2048 pc_valid.
LONG (2 segments, 3000 samples):
segment 0: COMPLETES — chain 0->1..10->0, 2048 pc_valid pulses,
ms_state walks ST_OUTPUT (6) -> ST_NEXT_SEGMENT (7) ->
ST_OVERLAP_COPY (8) -> ST_COLLECT_DATA (1) with
curr_seg = 1.
segment 1: HANGS in ST_COLLECT_DATA forever.
Root cause (not a test artefact, real RTL gap):
matched_filter_multi_segment.v ST_COLLECT_DATA increments
chirp_samples_collected and buffer_write_ptr only when ddc_valid is
high in that state. After ST_OVERLAP_COPY copies the 128 tail samples
of segment 0 into buffer[0..127], the FSM re-enters ST_COLLECT_DATA
and waits for buffer_write_ptr to reach 2048 (or
chirp_samples_collected >= LONG_CHIRP_SAMPLES = 3000) — both gated
on fresh ddc_valid pulses.
But the LONG chirp's tail samples (2048..2999 of the 3000-sample
ramp) arrived ~30 us into the chirp, while ms_state was stuck in
ST_PROCESSING / ST_WAIT_FFT / ST_OUTPUT processing segment 0. The
module has no side-channel ingestion, so those samples are dropped;
segment 1 never gets the data it needs and ST_COLLECT_DATA blocks
indefinitely.
Even on production xfft_2048 timing (~2200 cycles per FFT pass,
~7 k cycles per chain pass), segment 0 processing (~70 us) outlasts
the 30 us chirp duration. The bug is structural, not iverilog-only.
PR-J.2 will fix this. Three candidate approaches, in order of
implementation cost:
C) Defer segment processing until chirp is fully collected — small
FSM tweak; adds latency.
A) Extend the input BRAM to 4096 entries to hold the full LONG
chirp; segments slide over a stable buffer post-collection. ~1
extra BRAM, simplest data-flow.
B) Parallel ingestion FSM + ping-pong buffer that decouples capture
from processing. Keeps segment 0 latency optimal but is the most
RTL surface change.
This TB stays out of run_regression.sh until PR-J.2 lands the fix —
LONG would deterministically FAIL today.
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).
run_regression.sh replaces "System E2E (tb_system_e2e)" + "System E2E
USB_MODE=1 (FT2232H)" with the three PR-I subsuites (tb_system_opcodes,
tb_system_mechanics, tb_system_dataflow). SKIP count for --quick mode
bumped 5 -> 6 to match. "System Top USB_MODE=1 (FT2232H)" via
radar_system_tb.v is kept as a structural smoke test.
Dataflow gets --timeout=600 (vs 300 default). Its 18 ms sim takes
~430-450 s wall on this host; the 300 s default killed it at ~12.4 ms,
before the test logic block ran, yielding UNKNOWN. With 600 s, the TB
finishes cleanly and G2.2/G4.1/G4.2 all pass (3/3). The
matched_filter_multi_segment ST_WAIT_FFT hang documented in the TB
header still affects deeper coverage (G4.4 doppler, G5.x USB egress,
G9.x reset recovery), which remain deferred to PR-J.
tb_system_e2e.v removed (1294 lines) — coverage is fully replaced by
the focused subsuites; its USB_MODE=1 BFM was structurally broken
(wired only the FT601 ports, leaving the FT2232H DUT ports dangling),
which is why a USB_MODE=1 variant could "pass" without exercising the
production FT2232H path.
tb_usb_protocol_v2.v comment updated to point at tb_system_opcodes
for opcode-dispatch integration coverage.
Shallow probe verifying that auto-scan kicks the production pipeline
end-of-TX-side cleanly: chirp_scheduler emits new_chirp_frame, the
range pipeline (DDC + matched filter + range decimator) emits
multi-bin range profiles. Recovers G2.2 (new_chirp_frame pulse),
G4.1 (range_valid pulse), G4.2 (>=100 range bins) — three of T-2's
sixteen hidden failures.
Sim runs ~18 ms simulated (about 60-90 s wall on iverilog) — covers
one full 48-chirp frame TX time. Watchdog at 25 ms.
Deferred to PR-J:
G4.4 doppler_valid pulse, G5.1-5.4 USB egress, G9.x reset recovery.
Real finding: matched_filter_multi_segment hangs in ST_WAIT_FFT
under continuous auto-scan — the inner FFT chain (xfft_2048 +
frequency_matched_filter) does not assert fft_done in SIMULATION
mode after the first chirp's segment completes. tb_mf_cosim still
exercises the inner block in isolation (passes); the multi-segment
wrapper has no dedicated TB (T-9). The hang is a production-chain
integration bug, not a test infrastructure issue.
This TB is NOT yet wired into run_regression.sh — that lands in
PR-I.4 along with retiring tb_system_e2e.
New TB carving G1 (reset & init), G2 (TX chain — minus G2.2 which lives
in dataflow), G3 (safety architecture), G7.1 (rapid chirp toggle CDC),
and G7.3 (TX chirp counter CDC) out of tb_system_e2e into a fast,
focused subsuite. radar_system_top instantiated with USB_MODE=1
(production FT2232H path).
These tests don't need 48-chirp Doppler accumulation, so the sim
budget is ~80 us of stimulus + observation. Watchdog at 1.5 ms.
15/15 PASS. Pairs with tb_system_opcodes (commit 413a01e) to cover
~half of what tb_system_e2e exercised; the heavy data-flow / reset-
recovery groups (G2.2, G4, G5, G8, G9) move to tb_system_dataflow
in PR-I.3.
New TB instantiates radar_system_top with USB_MODE=1 and wires the
FT2232H ports correctly (which tb_system_e2e never did — its BFM was
FT601-only, so USB_MODE=1 opcode-dispatch tests were stimulating
dangling ports). Uses the proven send_cmd pattern from
tb_usb_protocol_v2.
Coverage migrated from tb_system_e2e:
- G6.1-6.6 — opcode 0x01/0x02/0x03/0x04/0x10/0x15 dispatch
- G7.2/G7.4 — rapid USB cmd CDC integrity
- G13.1-8 — chirps_per_elev clamp at DOPPLER_FRAME_CHIRPS=48 (PR-F-aware;
was hardcoded to 32 in tb_system_e2e G13)
- G14.1-13 — range_mode + CFAR opcode dispatch (0x20-0x25)
Plus new PR-G coverage:
- 0x17/0x18 MEDIUM ladder timing
- 0x2D cfar_alpha_soft
Result: 33/33 PASS in 15.7 ms sim. Resolves 10 of the 26 USB_MODE=1
failures from T-3 (the FT2232H-specific cluster). Remaining 16 in
USB_MODE=1 are T-2 pipeline-timing failures, addressed in PR-I.3
(tb_system_dataflow). tb_system_e2e is not yet retired — see PR-I.4.
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).
G1.5 (FSM trim): doppler section emits NUM_RANGE_BINS*NUM_DOPPLER_BINS
cells (49152 B) and detect emits packed valid bytes (6144 B), matching
the 9-byte header advertisement. Replaces flat counters with nested
range x doppler indices in usb_data_interface_ft2232h.v. Saves ~18.4 kB
per frame on the wire.
G2 (runtime MEDIUM ladder): adds opcodes 0x17/0x18 for medium chirp/
listen cycles with RP_DEF_MEDIUM_* defaults. Plumbed through
radar_system_top -> radar_receiver_final -> chirp_scheduler. SHORT/LONG
were already runtime-tunable; MEDIUM was hardcoded.
TBs: tb_usb_protocol_v2 adds TEST 4 (full-frame egress byte count =
56330) and TEST 5 (MEDIUM opcode round-trip) - 27/27 PASS.
tb_ft2232h_frame_drop updated for new section sizes - 10/10 PASS.
Full regression: 37/41 with 4 pre-existing failures (T-2..T-5,
tracked in PR-Tests-1 / PR-I). Stash test confirmed pre-PR-G HEAD has
identical failures - PR-G introduces zero new test regressions.
Three regex sites (run_test, run_mf_cosim, run_doppler_cosim) anchored
at column 0 with `^\[PASS|^\[FAIL`, but most TBs emit ` [PASS]` /
` [FAIL]` from `task check;` formatting. Anchors silently matched
zero markers, the fallback "did anything reach $finish" path
reported PASS, and 48 real failures across tb_system_e2e (×2 modes),
tb_fft_engine, and tb_fullchain_realdata went unnoticed across PR-D..G.
Switch all three anchors to `^[[:space:]]*\[PASS|^[[:space:]]*\[FAIL`.
No RTL change. Surfaces the truth — does not fix the underlying
test failures (tracked separately as T-2..T-10 in PR-Tests-1 / PR-I).
Closes the four deferred items from project_chirp_v2_pr_f_review_followups
that were carved out of 51a94f0 to keep that diff narrow.
A. TB doppler_bin / dbg_doppler_bin / dbg_range_bin still 5 / 6 bits,
ports widened to 6 / 9 in PR-F:
- tb/tb_doppler_cosim.v
- tb/tb_doppler_frame_start_gate.v
- tb/tb_system_e2e.v
- tb/radar_system_tb.v
- tb/tb_radar_receiver_final.v
All five files now include radar_params.vh and use
`RP_DOPPLER_BIN_WIDTH / `RP_RANGE_BIN_WIDTH_MAX. tb_doppler_cosim.v
was already structured around CHIRPS=32 and would have stalled
forever against the new 48-chirp default — added explicit parameter
overrides (CHIRPS_PER_FRAME=32, CHIRPS_PER_SUBFRAME=16, RANGE_BINS=512)
to keep its legacy 2-subframe golden vectors valid, mirroring the
pattern already used by tb_doppler_realdata / tb_fullchain_realdata.
B. tb_radar_receiver_final hardcoded NUM_DOPPLER_BINS=32 across the
golden buffer, the per-range bitmap, the duplicate-detect mask, the
gidx multiplier, and the S5/S6/S7/B3/B4 expected counts. All bumped
to `RP_NUM_DOPPLER_BINS (=48) via NUM_DBINS / NUM_RBINS / GOLDEN_ENTRIES
localparams; per-range index_seen widened to 64-bit so
`(64'd1 << doppler_bin)` covers bins 32..47. Note: under iverilog the
doppler-frame checks (S4-S9, B2a, B3, B4, G1) remain gated on
FFT_USE_XILINX_IP — the in-house fft_engine is too slow to land a
48-chirp Doppler frame in 20 ms sim; under XSim with the IP the
widened logic now exercises the full 24576-cell output (was 16384).
The 8-test active subset under iverilog is unchanged.
C. radar_system_top_50t.v adds `\`include "radar_params.vh"`, which is
needed for the `\`RP_DOPPLER_BIN_WIDTH-1:0]` reference added in PR-F.
Previously worked only because alphabetical Vivado file ordering
processes radar_system_top.v (which does include) first and the
macros leak across the same compilation unit. While here, also bumps
the dbg_range_bin_nc tie-off wire from a literal [5:0] to
`RP_RANGE_BIN_WIDTH_MAX-1:0] so the wrapper width matches the port.
D. usb_data_interface_ft2232h.v:392 stale comment ("FRAME_CELLS = 24576
< 32768") rewritten to reflect that PR-F's pad-to-power-of-2 makes
FRAME_CELLS = NUM_RANGE_BINS * (1<<DOPPLER_BIN_BITS) = 32768 (the
full 15-bit address space).
Tests (parity with PR-F baseline numbers in 7862f4d / 51a94f0):
- tb_doppler_cosim (3 scenarios): 14/14 each + Python golden compare PASS
- tb_doppler_frame_start_gate: 21/21 PASS
- tb_doppler_realdata: 2056/2056 PASS
- tb_cfar_ca: 24/0 PASS
- tb_chirp_controller: 43/43 PASS
- tb_chirp_contract: 10/10 PASS
- tb_mti_canceller: 43/43 PASS
- tb_radar_receiver_final: 8/8 PASS
- tb_system_e2e: 33/49 PASS
- radar_system_tb (USB_MODE=1): smoke (no PASS/FAIL markers; runs to $finish)
Lint (iverilog -Wall on full PROD_RTL + 50t wrapper): no new
width / Padding / Truncating warnings introduced.
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)
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).
Replaces plfm_chirp_controller_enhanced (5-state FSM with hardcoded
LONG/SHORT timings + 60-entry inline short LUT) with plfm_chirp_controller_v2,
a pure DAC playback driver: IDLE -> CHIRP -> IDLE keyed off a 1-cycle
dst_chirp_valid pulse, with sample count selected by dst_wave_sel
(SHORT=120 / MEDIUM=600 / LONG=3600). Inter-chirp timing (LISTEN, GUARD,
frame boundaries) is now owned exclusively by chirp_scheduler.
Scheduler -> TX bridge: cdc_async_fifo (Cummings style #2, WIDTH=2 DEPTH=4)
crosses {wave_sel} from clk_100m to clk_120m_dac, with chirp_pulse as
src_valid. frame_pulse rides a separate toggle CDC for chirp_counter
clear and the new_chirp_frame status output. mixers_enable now also gates
the scheduler so it stays in S_IDLE while the radar is "off" — without
this gate the first chirp_pulse fires at reset and gets dropped before
mixers come up.
Files:
- NEW plfm_chirp_controller_v2.v DAC playback driver (3 LUTs, FSM)
- DEL plfm_chirp_controller.v legacy controller (382 lines)
- DEL long_chirp_lut.mem legacy LUT (3600 lines), replaced
by tx_long_lut.mem from PR-B
- chirp_scheduler.v + mixers_enable input (master quiesce)
- radar_receiver_final.v + sched_*_out output ports + mixers_enable_100m
- radar_system_top.v wire sched_*_out -> tx_inst.sched_*; pass
stm32_mixers_enable_100m to rx_inst
- radar_transmitter.v full rewrite: drop new_chirp edge detector +
toggle CDC, instantiate cdc_async_fifo for
{wave_sel}, toggle CDC for frame_pulse,
plfm_chirp_controller_v2 in place of _enhanced
- tb/tb_chirp_controller.v + tb/tb_chirp_contract.v rewritten for v2
contract (43/43 unit + 10/10 contract green)
- tb/tb_radar_receiver_final.v + .mixers_enable_100m(1'b1) pin
- run_regression.sh, scripts/200t/build_200t.tcl file-list bumped
Test summary:
- tb_chirp_controller_v2: 43/43 PASS
- tb_chirp_contract: 10/10 contracts upheld
- tb_rxb_fullchain: peak 24033 ~80x (parity with PR-D)
- tb_mti_canceller: 43/43 PASS
- tb_system_e2e: 33/49 (1 new vs 34/49 PR-D baseline: G2.2
new_chirp_frame, intentional v2 frame-pulse
semantics — fires once per Doppler frame
instead of once per stm32 chirp toggle.
TB needs widening in PR-H to wait the full
frame.)
Single 100 MHz scheduler emits wave_sel[1:0] and chirp_pulse natively. Modes
00 (STM32 pass-through), 01 (auto-scan over SHORT/MEDIUM/LONG sub-frames),
10 (single-chirp debug), 11 (track dwell with watchdog scan-fallback after
RP_DEF_TRACK_WATCHDOG_FRAMES=5 idle frames). Sub-frame mask lets ops drop a
waveform without recompiling.
Drops the receiver_final wave_sel shim added in PR-C: wave_sel comes
straight from the scheduler; chirp_pulse replaces the old mc_new_chirp
toggle + XOR edge converter. matched_filter_multi_segment and mti_canceller
take wave_sel[1:0] and chirp_pulse directly — no parallel paths.
multi_segment also bumped: SHORT_CHIRP_SAMPLES 50 -> 100 (V2 1 us SHORT)
and MEDIUM_CHIRP_SAMPLES = 500 (5 us). LONG path unchanged. Dead
mc_new_elevation/azimuth XOR converters removed.
Deletes radar_mode_controller.v, formal/fv_radar_mode_controller.v, and
tb/tb_radar_mode_controller.v. Build manifests (run_regression.sh,
scripts/200t/build_200t.tcl) updated. Receiver_final pins medium/track/
subframe_enable inputs to RP_DEF_* defaults until PR-G plumbs USB opcodes.
Verification:
- tb_rxb_fullchain_latency: peak |I|+|Q|=24033 at bin 0, ~80x peak/mean
(up from PR-C's 15115 since matched filter now uses full 100 SHORT samples)
- tb_mti_canceller: 43/43 PASS with new wave_sel[1:0] input
- tb_radar_receiver_final: 8/8 PASS, ALL TESTS PASSED
- tb_system_e2e: 34/49 PASS - identical to pre-PR-D baseline (15 failures
are pre-existing matched-filter cycle-budget skips); G8.2/G8.3 chirp_scheduler
probes PASS
- tb_multiseg_cosim: 16/32 - same as pre-PR-D baseline
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.
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.
Establishes the macro vocabulary for the SHORT/MEDIUM/LONG waveform ladder,
3-subframe Doppler layout, track-mode dwell, and 2-class CFAR detection.
PR-A is purely additive — no module references the new macros yet.
Subsequent PRs (B–H) progressively replace the old chirp logic; this one
puts the names in place so each follow-on PR is mechanical.
Added:
- Waveform identity: RP_WAVE_{SHORT,MEDIUM,LONG,RESERVED} (2-bit selector)
- Sub-frame layout: RP_NUM_SUBFRAMES=3, RP_DOPPLER_BIN_WIDTH=6,
RP_SUBFRAME_ID_WIDTH=2
- Track mode: RP_DOPPLER_FFT_SIZE_TRACK=64, RP_MODE_TRACK=2'b11,
RP_DEF_TRACK_CHIRP_COUNT=64, RP_DEF_TRACK_WATCHDOG_FRAMES=5
- Detection class: RP_DETECT_{NONE,CANDIDATE,CONFIRMED,RSVD}
- 3-ladder timing defaults (V2 suffix to coexist with legacy in this PR):
SHORT 100 cyc (1 µs), MEDIUM 500 cyc (5 µs), LONG 3000 cyc (30 µs)
- Soft-CFAR alpha default: RP_DEF_CFAR_ALPHA_SOFT=0x18 (1.5 Q4.4,
Pfa_soft ≈ 10⁻⁵; confirm Pfa ≈ 10⁻⁶ at α=3.0)
- host_subframe_enable default: RP_DEF_SUBFRAME_ENABLE=3'b111
Marked LEGACY (deleted in the noted PR):
- RP_CHIRPS_PER_FRAME=32, RP_NUM_DOPPLER_BINS=32 (PR-F)
- RP_DEF_SHORT_CHIRP_CYCLES=50 (PR-E switches to 100)
- RP_DEF_CHIRPS_PER_ELEV=32 (PR-F)
Verified: iverilog preprocess clean. Sweep across 9_2_FPGA confirms no
module references the new macros yet — the PR is fully isolated.
Revert tag pre-chirp-v2 placed at 4f898ae for the chirp-v2 series.
The header still described the legacy in-house Radix-2 DIT fft_engine and a
FWD/REF/INV BITREV+BUTTERFLY state list that no longer matches reality.
Since RX-NEW-3 (commit 5c8cc8c), the chain instantiates fft_engine_axi_bridge,
which wraps xfft_2048 — LogiCORE FFT v9.1 (Pipelined Streaming) in synth/XSim
when FFT_USE_XILINX_IP is defined, in-house fft_engine fallback in iverilog.
Bit-reversal is now handled inside the IP (and the fallback), so the FSM has
COLLECT → SIG_FFT/CAP → REF_FFT/CAP → MULTIPLY → INV_FFT/CAP → OUTPUT → DONE.
No RTL changes. Header comment updates only.
cdc_adc_to_processing carries multi-bit data across 400→100 MHz via
TWO independent synchronizer chains (data Gray-encoded + a separate
2-bit toggle). Under metastability, the chains can resolve on
different cycles, letting the destination latch a half-resolved Gray
word that decodes to an arbitrary value. Audit C-11. Practical MTBF
is years per event but the design is non-conformant for arbitrary
multi-bit data — Gray code's single-bit-flip protection only holds
for ±1 transitions, not for CIC samples that can change by hundreds
of LSBs.
Replace with cdc_async_fifo, a Cummings SNUG-2002 style #2 async
FIFO. Data does NOT cross domains; it sits in dual-clock distRAM
(write port src_clk, read port dst_clk). Only the read/write
Gray-coded POINTERS cross — and pointers genuinely change ±1 per
increment, so Gray code's protection is correct by construction.
Home-grown rather than XPM_FIFO_ASYNC: vendor-neutral (iverilog can
simulate it directly, no SIM stub), keeps the project's existing
home-grown CDC convention (3 sibling primitives in cdc_modules.v),
and avoids XPM library version skew.
Port shape is preserved (same WIDTH=18, same dst_data/dst_valid/
overrun semantics — 1-cycle pulse per read in steady state) so the
swap is local to two instantiations in ddc_400m.v. Sticky-overrun
aggregation downstream is unchanged.
XDC: project already has blanket set_false_path on
clk_100m ↔ adc_dco_p, which covers both new pointer crossings.
Synchronizer FFs carry ASYNC_REG="TRUE" for placement-aware MTBF.
No XDC change needed.
New TB tb_cdc_async_fifo.v exercises 7 groups (28 checks): reset,
single-sample passthrough, multi-Gray-bit-flip (0x00000 ↔ 0x3FFFF —
audit's recommended coverage point, asserts NO intermediate values
appear at dst_data), matched-rate continuous stream, sustained-burst
overrun, drain-to-empty, and mid-stream reset.
Resource: 8 LUTRAMs per instance × 2 instances = 16 LUTRAMs (~0.05%
of XC7A50T budget).
Verified: full FPGA regression 42/42 PASS (was 41/41; +1 new test,
0 regressions in DDC Chain / Doppler Co-Sim / Full-Chain Real-Data
/ Receiver Integration / System Top / System E2E / MF Co-Sim — all
of which exercise the swap path through the production signal
chain). 0 lint errors.
The coefficient ROM has a deliberate positive DC pre-emphasis. Sum of
32 signed coefficients = 231,944; with the output slice at
accumulator[34:17] (effective Q17), DC gain = 231944 / 2^17 = 1.7696
= +4.96 dB. Bit-exact against the in-header golden-model line
(DC=5000 → 8847).
The +4.96 dB pre-emphasis compensates the upstream 4-stage CIC's
~3-4 dB passband droop. Without this note in the header, a future
engineer rebuilding the filter from a clean FIR design tool would
silently lose the pre-emphasis; AGC/saturation budgets in downstream
stages must also account for the +4.96 dB rather than assume 0 dB.
Audit's original "+7 dB" estimate was directionally correct but
quantitatively wrong (no Q-format reconciles to +7 dB; Q15 → +17 dB,
Q16 → +11 dB, Q17 → +4.96 dB). Documented at the verified +4.96 dB.
No coefficient or RTL change. Verified: full FPGA regression
41/41 PASS, 0 lint errors (FIR Lowpass: 13 checks PASS).
`output_bin_count` is declared `reg [RP_RANGE_BIN_WIDTH_MAX-1:0]`
(9 bits on 50T, 12 bits on 200T), but the reset and ST_IDLE assignments
used the literal `9'd0`. Vivado zero-extends with a width-mismatch
warning on 200T. The FORMAL port `fv_output_bin_count` was also
hardcoded `[8:0]`.
Replace all three sites with `{RP_RANGE_BIN_WIDTH_MAX{1'b0}}` /
parameterized port width — same pattern already used for the
`range_bin_index` reset in this module.
No functional change. Verified by full FPGA regression: 41/41 PASS,
0 lint errors (Range Bin Decimator: 63 checks PASS).
cfar_ca.v's GO/SO modes correctly cross-multiply to pick the side with
the greater (GO) or lesser (SO) per-cell average, but return that
side's RAW SUM as the noise estimate -- not the average. Combined with
alpha being pre-baked for the interior training-cell count, this means
at edges where the picked side is truncated, effective Pfa shifts by
the count ratio (up to ~2x in the first/last r_train bins). CA mode's
edge behavior was already documented; GO/SO's was not.
Documentation only -- no RTL behavior change. The audit's preferred
fix (divide noise_sum by selected_count) is explicitly NOT applied:
per-CUT integer divide is expensive in 50T fabric and the affected
bins are platform clutter (0..60 m) or noise floor (3012..3072 m)
where edge errors are masked by other effects. Operators tuning Pfa
have three documented options: (a) accept the asymmetry, (b) host-side
skip GO/SO outside r_train..NRANGE-r_train and fall back to CA there,
(c) hand-tune alpha per-mode based on observed Pfa drift.
Changes:
- cfar_ca.v header "CFAR Modes" table: GO/SO now explicitly note that
selection is by average but return value is raw sum.
- cfar_ca.v header "Edge handling": new GO/SO caveat paragraph.
- cfar_ca.v ST_CFAR_THR mode 2'b01/2'b10 selectors: inline AUDIT-C7
comment pointing to header.
Verification: full regression 41/41 PASS, 0 lint regressions.
AUDIT-S10 (commit `58154a6`) split the FPGA's six-flag aggregate
gpio_dig5 into two MCU-visible bits: gpio_dig5 keeps signal-saturation
(AGC reacts), gpio_dig7 (PD15) carries control-fault classes
(range_decim_watchdog | cic_fir_overrun). Until now the MCU did NOT
poll PD15, so DSP control faults were invisible to the recovery
dispatcher.
Changes:
- New `ERROR_FPGA_DSP_STALL` enum value placed AFTER ERROR_WATCHDOG_TIMEOUT
so the dispatcher routes to attemptErrorRecovery (FPGA reset pulse) not
Emergency_Stop. Updated error_strings[] in lockstep (static_assert
enforces).
- checkSystemHealth section 10 polls PD15 at 1 Hz with 2-sample debounce.
`last_dsp_check` is committed BEFORE the early return per AUDIT-CAL
pattern, so a flapping fault never bypasses the rate-limit. Streak
counter resets to 0 after firing (armed for next post-recovery
assertion) AND resets naturally when PD15 returns LOW.
- attemptErrorRecovery: ERROR_FPGA_DSP_STALL fans into the existing
ERROR_FPGA_COMM PD12 reset case (stacked case labels, same body). No
MCU-driven reset_monitors path exists; full bitstream reload clears
all sticky monitors as a side effect.
Tests:
- tests/test_audit_s10_dsp_stall_polling.c (NEW, 7 scenarios, 7/7 PASS):
T1 healthy 60s, T2 single-sample glitch blocked by debounce, T3
sustained fault fires once, T4 post-fire rate-limit holds within
window, T5 sustained fault rate bounded (29 errors / 60s -- MCU-N1
latch at error_count>10 fires in ~22s, gives operator time to
intervene), T6 counter-test demos no-debounce false-positive on
glitch, T7 HAL_GetTick 32-bit wrap.
- MCU host suite 35/35 PASS (was 34/34; +1 new, 0 regressions).
Pre-fix Tests 1/2/4 in fpga_self_test.v gave false PASS even on broken
silicon:
S-19 Test 1 (CIC): `result_flags[1] <= 1'b1` unconditional, comment
admitted "always true for simple check".
S-20 Test 2 (FFT): `(16'sd100+16'sd100 == 16'sd200) && (...)` —
both predicates compile-time-fold to 1; synth reduces to a
constant write.
S-21 Test 4 (ADC): PASS once N samples land, regardless of value.
A stuck-at-0 / stuck-at-MAX / dead LVDS link still PASSed
provided adc_valid_in toggled.
Fixes:
Test 1: drive impulse {5,0,0,0,0,0,0} through registered integrator
y[n]=y[n-1]+x[n]; require accumulator==5 after step
response. Real adder + register path; sign-extension
exercised. Detail = 0xC1 on fail.
Test 2: real radix-2 butterfly with twiddle multiply across 4 FSM
states. A=8, B=4 (real), W=2+3j -> WB=(8,12), A'=(16,12),
B'=(0,-12). Forces synth to instantiate signed multiplier
(DSP slice) + 17-bit signed add/sub. Detail = 0xF2 on fail.
Test 4: track min/max across 256-sample capture, require
(max - min) > ADC_RANGE_THRESHOLD (10 LSB). Catches stuck-at
faults. Does NOT distinguish AD9484 format mismatches
(audit's per-mode mean check requires SPI, impossible per
AUDIT-C13). Detail = 0xAD on fail.
Tests:
- tb_fpga_self_test.v existing Group 1-4 (16 PASS) still pass: varied
ADC counter input gives range >> 10.
- New Group 5: drive constant 0 -> expect Test 4 FAIL + detail=0xAD.
- New Group 6: drive constant 0x7FFF -> expect Test 4 FAIL + detail=0xAD.
- Regression: 41/41 PASS; fpga_self_test 22/22 (was 16/16).
Pre-fix usb_data_interface.v hardcoded `localparam [14:0] NUM_CELLS =
15'd16384` for the 50T 512-range x 32-doppler layout. On 200T builds
with SUPPORT_LONG_RANGE defined, RP_MAX_OUTPUT_BINS=4096 makes a real
frame 131072 cells, so the fixed value caused two distinct defects:
(a) value: counter wrapped 8x per real frame; bit-7 frame-start
marker fired 8x at incorrect host-frame offsets, silently
desyncing the GUI parser
(b) width: 15 bits could not represent 131072 (needs 17 bits)
Fix: derive NUM_CELLS = RP_MAX_OUTPUT_BINS * RP_NUM_DOPPLER_BINS and
counter width = RP_DOPPLER_MEM_ADDR_W (14 on 50T, 17 on 200T) from
radar_params.vh, so both scale together with the build define.
Tests:
- tb_audit_c16_num_cells.v: standalone counter-block exerciser (T1
reset, T2 increment, T3 wrap at NUM_CELLS-1, T4 exactly 2 markers
across 2*NUM_CELLS ticks, T5 top-bit observability) -- 6/6 PASS at
both 50T (NUM_CELLS=16384, CTR_W=14) and 200T (131072, 17).
- tb_usb_data_interface.v: existing test 7-8 retargeted from the old
hardcoded `>=15` / `==15'd16384` invariant to the new parameterized
one (`==RP_DOPPLER_MEM_ADDR_W` / `==RP_MAX_OUTPUT_BINS*RP_NUM_DOPPLER_BINS`).
Regression: 41/41 PASS (+2 new entries: 50T default + 200T
`+define+SUPPORT_LONG_RANGE`).
checkSystemHealth() had three watchdog blocks with the identical
"last_X_check not updated on error path" bug — same root cause as
AUDIT-CAL (BMP180 fix in commit 95aed35), distinct sites:
AD9523 clock check (5 s) main.cpp:693-705
ADAR1000 comm check (2 s) main.cpp:729-749
IMU comm check (10 s) main.cpp:752-760
Pre-fix, each block placed `last_X_check = HAL_GetTick();` below the
early-return path, so once the underlying check (STATUS0/1 RESET,
SCRATCHPAD verify fail, GY85_Update false) started failing, the
rate-limit window never engaged. Every subsequent iteration of the
main while(1) loop re-fired the corresponding ERROR_*. With
error_count > 10 latching system_emergency_state per MCU-N1, the
radar would trip into SAFE-MODE within ~10 main-loop iterations of
the first transient — far short of the intended ~100-150 s grace
window meant for operator intervention or attemptErrorRecovery
to succeed. ADAR1000 comm-failure also re-ran the 16 ms blocking
SPI verify (4 devices × 4 ms HAL_Delay) per iteration → chirp jitter.
Fix at all three sites: move the timestamp update INTO the if-block
and BEFORE any sub-check call. Mirrors the AUDIT-CAL post-fix
BMP180 block at main.cpp:771-780. ADAR1000 overtemp check stays
per-loop (unchanged) — over-temperature must remain responsive.
Test: tests/test_audit_imu_watchdog_cadence.c (6 tests, 6/6 PASS)
exercises the post-fix predicate against simulated HAL_GetTick()
ticks and a controllable GY85_Update() mock; counter-test runs the
pre-fix predicate to demonstrate the regression. Test uses IMU as
representative; AD9523 (5 s) and ADAR1000 (2 s) sites have identical
control flow.
Verification: full MCU host suite 34/34 PASS (was 33/33; +1 new test,
0 regressions).
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.
gpio_dig5 (PD13) previously OR'd six flags — four signal-saturation
classes (AGC, DDC overflow, DDC saturation, MTI saturation) and two
control-fault classes (range-decimator watchdog from F-6.4, CIC->FIR
CDC overrun from F-1.2). The MCU outer-loop AGC reduces RF gain on
PD13 assertion, which is the wrong response to a watchdog or CDC
stall — it just hides the stall behind a quiet receive chain. gpio_dig7
(PD15) was tied 1'b0 as "reserved".
Split:
gpio_dig5 = signal-saturation only (AGC continues to react correctly)
gpio_dig7 = control-fault classes
Telemetry: status_words[5][6:5] now exposes the two control-fault
classes in BOTH legacy (FT601) and FT2232H USB variants, with 2-FF
level CDC sync from clk_100m to ft601_clk_in / ft_clk. Bit [7] is
reserved. AUDIT-C12's frame_drop_count at [31:25] is preserved.
50T XDC H12 -> gpio_dig7 pin already assigned (audit AUDIT-C15-era);
no XDC change.
Test: tb/tb_audit_s10_gpio_split.v 17/17 PASS — exercises both the
combinational GPIO split and the CDC status-word packing path.
Regression: 39/39 PASS (was 34/34).
`radar_receiver_final.v:246` had `assign adc_pwdn = 1'b0;` -- the AD9484
PWDN pin was hard-tied LOW with no path for the host or MCU to assert
it. Combined with AUDIT-C13 (CSB hard-tied HIGH on the production board,
no SPI access to the AD9484), the ADC was fully un-recoverable from a
stuck state without dropping main power -- which also drops the
VBAT-backed BKPSRAM persistence (MCU-A4 OCXO warmup, MCU-A7 emergency
flag) and forces a 180 s warmup soak.
Opcode 0x32 was reserved during the AUDIT-C3 fix (commit 24ef5e7) for
exactly this purpose. Wire it through:
- `radar_system_top.v` adds `reg host_adc_pwdn` next to `host_adc_format`,
resets to 1'b0 (matches historical hard-tied state -- preserves
bringup behavior), latches `usb_cmd_value[0]` on opcode 0x32, drives
the new receiver input port.
- `radar_receiver_final.v` adds `input wire host_adc_pwdn`, replaces the
hard-coded `assign adc_pwdn = 1'b0` with `assign adc_pwdn = host_adc_pwdn`.
- No CDC: `host_adc_pwdn` is a stable single-bit level driven from the
clk_100m register straight to the I/O pad. AD9484 PWDN is asynchronous
w.r.t. the ADC clock; the chip re-acquires its DLL on PWDN deassert.
XDC pin assignments were already in place from AUDIT-C15 (50T:T5,
200T:P20, both LVCMOS25 driving the AD9484 PWDN net via the R36/R37
divider on the Main Board).
Verification:
- new tb/tb_adc_pwdn_opcode.v, 15/15 PASS:
T1 reset -> host_adc_pwdn=0, adc_pwdn pin=0 (ADC powered up)
T2 opcode 0x32 val=1 -> host_adc_pwdn=1, pin=1 (PWDN asserted)
T3 opcode 0x32 val=0 -> cleared
T4 only bit[0] consumed (upper bits ignored)
T5 unrelated opcodes (0x33, 0x01) don't disturb host_adc_pwdn
T6 cmd_valid_100m gating works
- Quick regression 33/33 PASS (was 32/32; +1 new test, 0 regressions)
- Lint: 0 errors
The BMP180 driver had no public init method and never called
readCalibrationCoefficients() from anywhere -- _calCoeff ran at the
C++ in-class member-initializer defaults (all zeros) at runtime.
Consequence chain:
- computeB5(UT) short-circuited via 0/0 (Cortex-M7 SDIV with
SCB->CCR.DIV_0_TRP=0 returns 0 silently -- system_stm32f7xx.c does
not enable the trap)
- getPressure() always tripped the `if (B4 == 0)` guard, returning
the I2C-error sentinel (post-AUDIT-C17: INT32_MIN; pre-: 255)
- health watchdog at main.cpp:758 fired ERROR_BMP180_COMM every
main-loop iteration because last_bmp_check was only updated on the
success path, so the 15 s rate-limit never engaged once the check
started failing
- error_count > 10 latched system_emergency_state = true (per the
MCU-N1 fix), driving SAFE-MODE within ~25 s of every boot
Fix:
- Added BMP180::begin() public method: probes chip ID, then reads the
11 factory cal coefficients (registers 0xAA..0xBE step 2). Returns
true only on full success; false on chip-ID mismatch or any I2C
failure mid-loop.
- main.cpp BAROMETER INIT calls myBMP.begin() with up to 3 retries
(50 ms backoff) and sets a file-scope bmp180_operational flag.
Altitude-baseline loop now gated on success -- failure path leaves
RADAR_Altitude at 0.0f instead of letting pow(negative, fractional)
propagate NaN into gps_data telemetry.
- Health watchdog gates BMP180 check on bmp180_operational AND
updates last_bmp_check regardless of the error path. A single bad
pressure reading no longer tight-loops into SAFE-MODE; legit sensor
failure now takes the intended ~150 s (10 errors x 15 s) before
the MCU-N1 latch trips, giving the operator time to intervene.
Verification:
- new test_audit_cal_bmp180_begin.c, 3/3 PASS:
T1 every coefficient loaded in order with correct signed/unsigned types
T2 chip-mismatch and I2C-fail short-circuit semantics correct
T3 regression demo: zero-cal computeB5 returns 0 for any UT (the
silent-fail mode); datasheet cal reproduces 15.0 C
- full MCU regression 33/33 PASS (was 32/32; +1 new test, 0 regressions)
Bug introduced in 5fbe97f (initial upload of the driver from the
Arduino enjoyneering79 BMP180 library -- the begin()/init pattern from
the upstream Arduino version was lost in the STM32 port). Latent until
this audit cycle.
BMP180_ERROR=255 was an in-band sentinel returned by uint16_t I/O helpers
(read16, readRawTemperature) on I2C failure. 255 is also a valid uint16
register reading (0x00FF appears across the calibration block and is
reachable as a raw temperature/pressure sample), so a sensor failure was
indistinguishable from a real reading.
getTemperature() additionally narrowed the uint16_t raw read to int16_t
before passing to computeB5(). Raw bit-patterns >= 0x8000 (reachable across
the BMP180 -40..+85 C operating window) flipped to negative int16_t and
sign-extended into computeB5(), producing temperature errors of order
100s of C (e.g. -347 C instead of +51 C for raw UT = 0x8000).
Fix:
- Internal I/O helpers (read8/read16/readRawTemperature/readRawPressure)
now return bool and pass the value through an out-param. None of the
new sentinels collide with valid sensor output:
* getTemperature -> NaN on error
* getPressure -> INT32_MIN on error
* getSeaLevelPressure -> INT32_MIN on error
- getTemperature() keeps raw as uint16_t and widens value-preservingly
via (int32_t)raw before computeB5().
- readRawPressure() reads XLSB through the bool-out-param contract;
previously OR'd in 0xFF on I2C fail, silently corrupting the LSB.
Verification: test_audit_c17_bmp180_sentinel_and_cast 4/4 PASS, including
datasheet UT=27898 -> 15.0 C reproduction and 64/64 finite outputs across
a full uint16 sweep (vs 32/32 collapses in the upper half under the buggy
narrowing). Full MCU regression 32/32 PASS.
Caller-side: no external code references BMP180_ERROR; main.cpp's existing
range check at the health-watchdog catches INT32_MIN via the < 30000.0
branch.
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.
Bug: 16-bit detect_count was reset only on power-on; increments at three
sites (ST_IDLE/ST_BUFFER simple-threshold paths and ST_CFAR_CMP) accumulate
across frames. At 178 fps with even 2-3 average detections per frame the
counter wraps in 100-180 seconds, breaking any rate-based host telemetry
or health check that reads it.
Fix: add `detect_count <= 16'd0` in ST_DONE so the counter represents
"detections this frame" instead of cumulative-since-boot. Updated $display
wording from "total detections" to "frame detections".
T13 flipped from "count keeps growing" to "identical-scene frames produce
identical counts" (the actual contract a per-frame counter must satisfy).
TB snapshots detect_count during ST_DONE because cfar_busy only goes low
on ST_IDLE entry — after the reset has fired.
Verification: tb_cfar_ca 24/24 PASS, quick regression 31/31 PASS.
Note: detect_count output port is now "live" (accumulates during frame,
0 between frames). Audit confirmed no current host telemetry consumes
this port. If future host code needs a stable last-frame total, add a
detect_count_last_frame snapshot register then.
AUDIT-C12: usb_data_interface_ft2232h had a misleading single-buffer comment
that overstated the timing slack and referenced a frame_ack_toggle CDC that
was never implemented. Re-verified actual numbers: at 178 fps the slack is
1.14 ms (20%), not "much shorter than gap". No data corruption today (write
order matches read order, addresses don't collide), but frame_complete
firing while WR_FSM is still draining the previous frame causes silent
frame drops via the missed frame_ready_toggle edge.
Fix is instrumentation, not architectural rework: add wr_done_toggle
(ft_clk -> clk CDC) on WR_DONE -> WR_IDLE, track frame_pending in clk
domain, count drops in 7-bit saturating frame_drop_count, surface in
unused upper 7 bits of status_words[5]. Host now has visibility into the
failure mode if margin ever shrinks (faster frame rate or USB bandwidth
shortfall). Replaced misleading comment with corrected timing breakdown.
AUDIT-S22: cfar_ca emits one detection per 3 cycles (THR/MUL/CMP); the
detection RMW takes 3 cycles. Match by construction today, fragile against
any CFAR speedup. Added a header comment in cfar_ca.v documenting the
dependency, and a SIMULATION-only assertion in usb_data_interface_ft2232h.v
that fires [ASSERT FAIL] AUDIT-S22 if cfar_valid arrives while RMW busy.
Catches silent-drop regressions in the test suite.
Verification: new tb_ft2232h_frame_drop.v with 5 scenarios (no drops /
stalled drops / multi-drop / recovery / saturation at 127) - 10/10 PASS.
Quick regression 31/31 PASS (was 30/30; +1 new test, 0 regressions).
Closes AUDIT-C15. The 200T XDC `adc_or_p/n` PACKAGE_PIN was a TODO
placeholder that blocked all 200T synth/impl with unplaced-IO errors.
Pins U20/V20 are L11P/L11N_T1_SRCC_14 — same T1 clock tile as adc_dco_p
on L12_MRCC (W19/W20), so OR captures with the same IBUFDS->BUFIO->IDDR
source-synchronous topology as adc_d_p[*]. Free-pair confirmed by Vivado
get_package_pins query against xc7a200tfbg484-2.
Adds DIFF_TERM TRUE on adc_or_n (was only on the p-side; explicit on
both is safer). Adds input_delay constraints mirroring adc_d_p
(max 1.0 ns / min 0.2 ns on both edges).
Header pin counts updated: Bank 14 21/50 used, total 184/285.
This is the FPGA-team RECOMMENDATION for the production PCB (NEW
design); the PCB designer must route AD9484 OR+ -> U20 and OR- -> V20.
Validation:
- read_xdc + link_design -part xc7a200tfbg484-2 -> READ OK on both
xc7a200t_fbg484.xdc and adc_clk_mmcm.xdc; no PACKAGE_PIN errors.
- ./run_regression.sh --quick: 29/29 PASS (RTL untouched).