mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-08 22:47:16 +00:00
bb6952753da5b46d243be21704cfd1e88ed2842a
409 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bb6952753d |
AUDIT-C7: document GO/SO edge-bin Pfa drift in cfar_ca header
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. |
||
|
|
534905263f |
mcu(health): poll PD15 + dispatch ERROR_FPGA_DSP_STALL (AUDIT-S10 follow-up)
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). |
||
|
|
853d2a5fd9 |
AUDIT-S19/S20/S21: replace fpga_self_test tautologies with real arithmetic
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).
|
||
|
|
9bed35287a |
AUDIT-C16: parameterize NUM_CELLS + sample_counter width for 200T
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`).
|
||
|
|
1b1b5f4fb2 |
mcu(health): commit rate-limit window before early returns (AUDIT-CAL follow-up)
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
|
||
|
|
1f307f77a9 |
cosim: refresh stale baselines (FFT-2048 + chirp realign)
Two stale-baseline events were never captured in earlier commits: 1. The FFT-1024 -> FFT-2048 merge ( |
||
|
|
58154a6bf1 |
fpga: split gpio_dig5/dig7 by fault class (AUDIT-S10)
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). |
||
|
|
59f3c82fbb |
fpga: wire AD9484 PWDN to host opcode 0x32 (AUDIT-S25)
`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
|
||
|
|
95aed35d89 |
mcu(bmp180): call cal-coefficient init at boot + watchdog cadence fix (AUDIT-CAL)
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
|
||
|
|
4b142166be |
mcu(bmp180): replace in-band sentinel + fix uint16->int16 narrowing (AUDIT-C17)
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.
|
||
|
|
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. |
||
|
|
53c7f416a7 |
cfar_ca: reset detect_count per frame (AUDIT-C6)
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. |
||
|
|
e67368d621 |
ft2232h: add frame drop counter (AUDIT-C12) + cfar RMW cadence guard (AUDIT-S22)
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). |
||
|
|
0c82de54a2 |
fft_engine_axi_bridge: respect axi_din_tready with 1-deep skid buffer
Bug: bridge advanced in_count and asserted tlast on din_valid alone, ignoring the IP's tready handshake. With LogiCORE FFT v9.1 in nonrealtime throttle mode (per .xci), tready can deassert briefly during BFP normalization or pipeline events, silently dropping input samples and shifting tlast off-by-N. Fix: add 1-deep skid buffer + AXI-correct handshake. Phase 1 drains the active beat when the IP accepts it (and shifts skid up); Phase 2 loads new upstream samples respecting post-handshake slot availability. Track accept_count separately from in_count to drive the S_FEED->S_DRAIN transition on the Nth accepted beat. Sustained 2+ cycle backpressure exhausts the skid and sets overflow_sticky for debug visibility. Audit cross-refs (AUDIT-C10): - "tready ignored" - CONFIRMED, fixed here - "SCALE_SCH unset" - REFUTED (BFP mode uses tuser, not cfg_tdata) - "output ordering not configured" - REFUTED (.xci natural_order) Verification: new tb_fft_engine_axi_bridge.v with stub xfft_2048 exercises 4 backpressure patterns (none / dip-at-3 / dip-at-100 / 3-cycle sustained). Quick regression 30/30 PASS. |
||
|
|
b3b4580e9c |
xdc(200t): pin AD9484 OR LVDS pair to U20/V20 (L11_T1_SRCC_14)
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). |
||
|
|
79a9353456 |
fix(usb): C-9 — GUI bulk-frame parser for FT2232H + clamp inert flag bits
The GUI's radar_protocol.py parsed 11-byte legacy packets only. The
production board (50T, USB_MODE=1) emits ~35 KB bulk frames from
usb_data_interface_ft2232h.v, so the legacy parser saw a random walk
of false 11-byte boundaries through bulk data — no usable display on
production hardware.
Bulk parser added (radar_protocol.py):
- parse_bulk_frame validates header, reserved bits, n_range=512,
n_doppler=32, footer-at-flag-derived-offset; unpacks range_profile
/ doppler_mag / cfar_dense per the format-flags byte.
- find_bulk_frame_boundaries is the bulk counterpart of
find_packet_boundaries; status packets (0xBB) handled in the same
stream since FT2232H emits them too.
- RadarAcquisition dispatches on isinstance(conn, FT2232HConnection):
bulk path skips the per-sample state machine and fills RadarFrame
in one shot. FT601 / 200T keeps legacy 11-byte (USB 3.0 has 50x
bandwidth headroom; per-sample format is correct and already works).
- RadarFrame.mag_only flag carries the wire's mag_only bit so
downstream consumers can skip I/Q panels cleanly.
- FT2232HConnection._mock_read now emits synthetic bulk frames
(was misleading legacy 11-byte).
RTL alignment (AUDIT-C9 RTL stub option):
- usb_data_interface_ft2232h.v header no longer promises the
unimplemented mag_only=0 (full-I/Q) and sparse_det=1 paths;
explicit INERT FLAGS note distinguishes the two reasons:
* Full-I/Q is constrained by hardware — needs ~28-BRAM18 I/Q
buffer (50T currently 78% BRAM utilised after FFT IP) AND
USB 2.0 bandwidth (12.21 MB/s vs 8 MB/s conservative budget).
* Sparse-list is feasible — smaller than dense for typical
scenes (<341 detections), ~1 BRAM18 cost. Just unimplemented
RTL work (small list BRAM + new WR_DETECT_SPARSE state).
- New SIMULATION-only assertion fires if stream_mag_only ever
becomes 0 or stream_sparse_det ever becomes 1 — backstop for
any future regression that bypasses the host-register clamp.
- radar_system_top.v opcode 0x04 force-clamps mag_only=1 and
sparse_det=0 in host_stream_control when USB_MODE=1, so a
Custom-Command host write can't push the FPGA into a wire-format
vs FSM divergence.
Bandwidth math (verified for 27c9c22+):
Frame rate = 1 / (16x167 us + 175.4 us + 16x175 us) = ~178 fps
Mag-only frame = 8+1024+32768+2048+1 = 35849 B = 6.38 MB/s
FT2232H 245-Sync-FIFO sustained budget (FTDI AN_232B-04
conservative): 8 MB/s. Headroom 20%.
Tests: test_GUI_V65_Tk.py TestBulkFrameParser — 18 new cases covering
round-trip per stream-flag combo, header/footer/n_range/n_doppler/
reserved-bit/truncation rejection, multi-frame boundaries, bulk+status
mixed streams, byte-drop resync, dispatch-by-connection-type,
ingest-to-RadarFrame end-to-end. GUI 117/117 PASS, v7 83/83 PASS,
FPGA quick regression 29/29 PASS, ruff clean.
Refs: AUDIT-C9 (GUI parses legacy 11-byte vs FT2232H bulk).
Follow-ups (separate patches):
- Sparse-detection write FSM (~1 BRAM18 + ~100 RTL lines).
Bandwidth- and memory-feasible; just unimplemented work.
- Full-I/Q write FSM. Constrained: needs ~28-BRAM18 I/Q buffer
AND USB 2.0 bandwidth headroom (50T post-FFT-IP at 78% BRAM).
|
||
|
|
24ef5e7251 |
fix(fpga): C-3 — parameterize DDC ADC sign-conversion via host opcode 0x33
The DDC hard-coded an offset-binary->2C subtract on the AD9484 path. The
chip's output format is selected by the SCLK/DFS strap (jumper SJ1 on
RADAR_Main_Board.sch), and CSB is hard-tied HIGH so SPI cannot be used
to confirm or change it from firmware. If the board is assembled with
SJ1 on pins 2-3 (two's-complement), the existing RTL silently mis-
converts every sample.
Add a 2-bit adc_format input to ddc_400m_enhanced (2-FF synchronized
clk_100m -> clk_400m, ASYNC_REG attribute), drive it from a new top-
level register host_adc_format written by host opcode 0x33, and wire
it through radar_receiver_final. Default 2'b00 matches the SJ1 default
strap (offset-binary) and preserves pre-patch behavior. Opcode 0x32 is
intentionally left unused; reserved for the future S-25 fix
(host-driven adc_pwdn).
Tests: tb/tb_ddc_400m.v Test Group 5 — 7 new assertions covering
offset-binary at {0x80, 0x00, 0xFF}, two's-complement at
{0x00, 0x80, 0x7F}, and reserved 2'b10 fallback. 14/14 PASS.
Refs: AUDIT-C3 (DDC offset-binary hardcoded).
Schematic ref: RADAR_Main_Board.sch:46719 (CSB on +1V8_CLOCK_F),
:46845 (SCLK/DFS via SJ1).
|
||
|
|
4f0b82de6e |
test(fpga): receiver-integration — fix tb wiring + skip-guard XSim-only checks
tb_radar_receiver_final had three pre-existing issues that all surfaced as
fails in regression (32 passed, 2 failed before; 34 passed, 0 after):
1. host_range_mode was undriven (floating 2'bzz); rmc log confirmed
"Auto-scan starting, range_mode=z". Add explicit 2'b01 (long-range
dual-chirp) for the test scenario.
2. DDC_MAX_ENERGY threshold (2^56) was sized for an unspecified earlier
stimulus; the test feeds a deliberately-loud 120 MHz sawtooth that
produces ~1.27e17 energy over 2M samples. Raised to 2^60 (~10x
observed) so B1b catches true overflow without false-firing.
3. The 9 doppler-frame-dependent checks (S4-S9, G1, B2a, B3, B4) need
~108 ms simulated time to fill a 32-chirp Doppler frame because the
in-house fft_engine takes ~340 K cycles per multi-segment chirp
(RX-NEW-3, commit
|
||
|
|
5ff5671fe2 |
fix(fpga): TX-I — align matched-filter reference with actual post-DDC band
The DAC short/long chirp LUTs are 10..30 MHz upchirps (Hilbert-confirmed). With TX_LO=10.500 GHz, RX_LO=10.380 GHz (adf4382a_manager.h) and the 120 MHz DDC NCO (ddc_400m.v), high-side mixing places the post-DDC echo at 10..30 MHz baseband. The matched-filter reference (gen_chirp_mem.py) was generating 0..20 MHz, implicitly assuming the chirp's low edge mixed to DC. This caused a 10 MHz spectral offset and ~5 dB matched-filter loss. Adds F_BASEBAND_LOW=10e6 in both gen_chirp_mem.py and radar_scene.py, with phase formula 2*pi*F_BASEBAND_LOW*t + pi*rate*t^2 in all chirp generators. Regenerates the 6 .mem files. Adds analyze_short_chirp_mismatch.py for the Hilbert-based diagnosis. Fixes the misleading "30MHz to 10MHz" comment in plfm_chirp_controller.v and adds an end-to-end frequency plan in the LUT header. Sideband orientation (high-side at both mixers) is the conventional choice and consistent with antenna match (10.25..10.75 GHz, 8x16 patch designed at 10.5 GHz). Loopback capture would settle definitively; if either mixer is low-side the F_BASEBAND_LOW sign flips and/or chirp direction reverses. |
||
|
|
b7ac2de1a4 |
chore: delete dead latency_buffer; doc cleanup for two stale comments
latency_buffer.v has had zero non-tb instantiations since RX-B (2026-04-23)
replaced its hookup in radar_receiver_final with a 1-FF alignment register.
The module was being kept "for potential future use" — exactly the kind of
dead weight the codebase does not need. Deleted, along with all build /
test infrastructure that dragged it along:
- 9_Firmware/9_2_FPGA/latency_buffer.v
- 9_Firmware/9_2_FPGA/tb/tb_latency_buffer.v
- run_regression.sh: removed from RTL_FILES and RECEIVER_RTL
- scripts/200t/build_200t.tcl: removed from synthesis source list
- tb/tb_system_e2e.v: removed from header compile-string example
- tb/cosim/validate_mem_files.py: deleted test_latency_buffer() (~75 lines),
its call site, and the corresponding entry in the module docstring
Historical RX-B comments referencing latency_buffer in radar_receiver_final.v,
tb_rxb_fullchain_latency.v, and tb_rxb_latency_measure.v are kept — they
explain WHY the module was removed, which is still useful design archaeology.
Two doc-only housekeeping touches bundled in:
- plfm_chirp_controller.v: replaced two empty "CRITICAL FIX: Generate
valid signal" labels at LONG_CHIRP and SHORT_CHIRP with one shared
chirp_valid policy comment block above LONG_CHIRP that explains the
actual rationale (downstream FIFO underrun on trailing samples).
- v7/models.py: replaced the "range_resolution and velocity_resolution
should be calibrated" docstring (sounded like an open TODO but was a
documented placeholder) with a clear pointer to the GUI-C3 fix in
workers.py:RadarDataWorker so future readers know the live path
derives correct values from WaveformConfig.
FPGA quick regression unchanged: 28/29 (1 fail is the unrelated iverilog/
Xilinx-IP RX-NEW-3 gap). GUI suite 180/180. Ruff clean.
|
||
|
|
c49092f52b |
test(gui): GUI-S3 — pin status word 4 bit layout in a co-spec test
Cross-verified status word 4 bit positions against the FPGA word builder (usb_data_interface.v:376-380, usb_data_interface_ft2232h.v:675-679) and the GUI parser (radar_protocol.py:252-257) — all positions match. No production change needed; the gap was that nothing in the test suite caught a future drift between the two sides. Added test_parse_status_word4_layout_co_spec: a single canonical layout table is the source of truth; for each field the test sets only that field to its max value, builds the status packet via the existing FPGA-builder-mirror _make_status_packet, parses, and asserts the field round-trips exactly AND every other field reads back zero. Catches both LSB drift and width drift on either side of the wire. Pre-checks that widths plus the reserved [9:2] gap sum to 32 and that no two fields overlap. Also fixed test_default_shapes — stale (64,32)/(64,) literals predated the GUI-C1 / Q3 alignment that bumped NUM_RANGE_BINS 64 -> 512. test_v7 was updated at the time, this one was missed. Replaced with references to NUM_RANGE_BINS/NUM_DOPPLER_BINS so any future bin-count change auto-updates the assertion. Suite now 180/180 PASS. |
||
|
|
5d334bfdd6 |
fix(fpga): TX-N9 — sim-only payload-hold checker on cmd CDC
cmd_data / cmd_opcode / cmd_addr / cmd_value feed downstream CDC sync chains; the safety property is that they only change on the cycle cmd_valid rises (RD_PROCESS), and stay held on every other cycle so the receiver's 2-FF synchronizer sees a clean payload regardless of where its sample window lands. The FSM satisfies this implicitly today, but nothing flagged a regression that introduced a stray write somewhere in the same always block. Added an `ifdef SIMULATION block at the bottom of both usb_data_interface.v (FT601 / ft601_clk_in / ft601_reset_n) and usb_data_interface_ft2232h.v (FT2232H / ft_clk / ft_reset_n). It snapshots the payload + cmd_valid each cycle and fires [ASSERT FAIL] TX-N9: cmd_<field> changed while cmd_valid=0 (old -> new) on any payload change while cmd_valid is low. Local regs suffixed _n9 to avoid future name collisions. Synthesis-inert. Quick FPGA regression unchanged: USB Data Interface 91/91 PASS, overall 28/29 (same baseline; the 1 fail is the pre-existing iverilog/Xilinx-IP RX-NEW-3 gap). |
||
|
|
26f8d1fa72 |
fix(mcu): MCU-A4 — BKPSRAM warm-restart bypass for OCXO 180 s warmup
Every boot waited the full 180 s OCXO warmup soak — even an IWDG/SYSRESETREQ reset that takes seconds and leaves the OCXO oven hot lost three minutes of bringup time. Added BKPSRAM slot 3 (magic 0xCA1C1F1E) with warmup_persist_set/check helpers next to the existing MCU-A2/A7 BKPSRAM block. Cold-boot path now arms the flag at the end of the full 180 s soak; subsequent boots that find the flag still set know the OCXO oven is still hot and the crystal is settled, so they wait 5 s and move on. Power-cycle clears BKPSRAM and forces the full soak again — safe default, operator can't accidentally skip the warmup by yanking and re-applying power. Added test_mcu_a4_ocxo_warm_restart (7 cases): cold boot soaks 180 s and sets the flag; warm reset is 5 s; 5 consecutive warm resets stay fast; power-cycle restores the cold path; cold-after-power-cycle re-arms the bypass; pre-fix regression confirms 10 warm restarts save 1750 s vs the old always-180-s path. MCU regression now 82/82. |
||
|
|
0a49320e31 |
fix(mcu): MCU-A2 — site-configurable mag declination, persisted in BKPSRAM
The magnetometer yaw correction used a hardcoded -0.61 deg literal baked in for one deployment site. Yaw_Sensor was wrong by (site_decl + 0.61) deg at every other site whenever the UM982 dual-antenna heading was unavailable. Backed the value with BKPSRAM (slots 1+2 — slot 0 is the MCU-A7 emergency flag) and exposed set_mag_declination_deg / get_mag_declination_deg. Default returns the legacy -0.61 deg when no override has been written so the original site stays correct out of the box; a host command (or future GPS-derived auto-calibration) writes the new site value once and it persists across every reset path until main-power removal. Hardened with a +/-30 deg range clamp on both write AND read paths — real magnetic declinations are roughly +/-25 deg worldwide, so a wider value indicates a calibration error or BKPSRAM corruption (VBAT brown-out, bit flip) rather than a legitimate site. Defensive read-side clamp prevents a corrupted slot from propagating a wild heading offset. Replaced the single use site at the magnetometer yaw computation with the getter; legacy global Mag_Declination retained and kept in sync by the setter for any external linkage. Added test_mcu_a2_mag_declination (10 cases): default, set/get, persistence across reset, power-cycle clear, write-side clamp both directions, plausible-site passthrough, defensive read-side clamp on corruption, wrong-magic fallback, pre-fix bearing-error regression. MCU regression now 81/81. |
||
|
|
4a102e30fe |
fix(mcu): MCU-A6 — recovery handlers for AD9523_CLOCK and FPGA_COMM
attemptErrorRecovery() previously fell through to the default log-only branch for both ERROR_AD9523_CLOCK and ERROR_FPGA_COMM. checkSystemHealth keeps re-firing the same error every pass with no recovery action ever attempted, so the system limps along until escalation kicks in. ERROR_AD9523_CLOCK: AD9523_RESET_ASSERT, 10 ms settle, then re-run configure_ad9523() (releases reset, selects REFB, reprograms, waits for lock). On second failure we log and let the next health pass re-fire so a transient brown-out on the 100 MHz reference does not drop straight into Emergency_Stop. ERROR_FPGA_COMM: pulse PD12 LOW->10 ms->HIGH (matches the boot reset pattern). PA rails left untouched at runtime; brief adar_tr_x undefined window is acceptable vs. losing the radar entirely. Added test_mcu_a6_recovery_dispatch (11 cases) covering both new handlers, all existing routes, the default branch, a pre-fix regression check, and an explicit assertion that RF_PA_OVERCURRENT escalates upstream (handleSystemError) rather than recovering inline. MCU regression now 80/80. |
||
|
|
1317a91e01 |
fix(mcu): MCU-A5 — gate Idq health-window during PA calibration walk
The boot-time Idq calibration walks DAC_val from 126 down toward the 1.680 A target. Mid-walk readings sit well above the 2.5 A overcurrent threshold by design, and a channel that hits the safety_counter timeout (50 iters) can be left above the window. Without a gate, the next checkSystemHealth() pass would trip ERROR_RF_PA_OVERCURRENT and route straight into Emergency_Stop, killing the system mid-bringup. Added a `pa_calibration_in_progress` flag set TRUE around both DAC1 and DAC2 cal walks. checkSystemHealth's Idq window short-circuits while the flag is set; bias-fault and overcurrent thresholds remain fully active once the walk completes, so any genuinely stuck-high channel surfaces on the very next health pass and routes through the normal handler. Other health checks (lock, comm, temperature, watchdog) stay live during cal — no behavioural change to anything except the Idq window. Added test_mcu_a5_pa_cal_gate (7 cases): mid-walk masking, post-cal re-arming, stuck-high channel surfacing after gate clears, bias-fault gating, PowerAmplifier=false short-circuit, and a pre-fix regression case showing the buggy path would have tripped overcurrent mid-walk. MCU regression now 79/79. |
||
|
|
f28a0eaa80 |
fix(mcu): MCU-A7 — persist emergency state across MCU resets in BKPSRAM
Emergency_Stop's hold loop refreshed IWDG forever, so any reset path that
DID fire (SYSRESETREQ from another fault, brown-out) would re-run
startup and re-energize the PA rails — there was no record that the
system had been in emergency state. Watchdog defeat in the hold loop
masked the problem.
BKPSRAM gives us a flag that survives every reset path but is lost on
main-power removal — exactly the recovery semantics we want:
power-cycle is the deliberate operator action that clears emergency,
every other reset stays in safe-hold.
- Added emergency_persist_set/check helpers (BKPSRAM @ 0x40024000,
magic 0xDEAD5A5A); enable PWR + backup-access + BKPSRAM clock.
- Emergency_Stop now writes the flag BEFORE the rail-cut sequence so
even an interrupted shutdown still leaves the persisted state set.
- main() checks the flag immediately after MX_IWDG_Init and before
any PA enable code; if set, calls Emergency_Stop directly. GPIO
init has already forced all PA enables LOW, so the safe-hold path
is reached without a single PA rail going hot.
Hold-loop IWDG refresh kept intentionally: a healthy hold loop does not
need to cycle the MCU, but if the loop itself wedges (stack corruption,
bus fault), refresh stops, IWDG fires, and the persist flag routes the
reset right back into safe-hold.
Added test_mcu_a7_emergency_persist (6 cases) modelling BKPSRAM
persistence vs power-cycle, including a regression check that exercises
the pre-fix "no persistence" boot to confirm it would have re-energized
the PAs. MCU regression now 78/78.
|
||
|
|
df0b2fd469 |
fix(mcu): MCU-A1 — replace 25 C cooling stub with 70/60 C hysteresis
Cooling-fan trip in main.cpp's periodic temperature block was a 25 C dev stub that latched the fan ON at room temperature on every boot. Replaced with production thermal control: ON at 70 C, OFF at 60 C. The 10 C dead-band prevents relay/fan chatter near the threshold; the 70 C ON point sits below the 75 C SAFE-mode gate in checkSystemHealth() so the fan engages before the system shuts down. Driven from the existing `temperature` global (max of 8 sensors, populated just above by the GAP-3 fix) instead of re-OR'ing the eight Temperature_N variables — single source of truth, and the diag now prints the actual peak temperature on each transition. Added test_mcu_a1_cooling_hysteresis (9 cases) covering cold-start, upward crossing, dead-band hold, downward crossing, and a regression guard at 30 C that would have engaged the fan under the old stub. MCU regression now 77/77. |
||
|
|
0b8b933e27 |
cleanup(fpga): RX-A1 — drop dead chirp_counter port from MF chain
matched_filter_processing_chain declared `input wire [5:0] chirp_counter` but never read it inside the module. matched_filter_multi_segment passed its own chirp_counter through to that dead port. Removed the port from the chain and the corresponding hookup at the multi_segment instantiation site. Five testbenches also referenced the port (tb_mf_cosim, tb_matched_filter_processing_chain, tb_rxb_latency _measure plus the four MF cosim variants that share tb_mf_cosim) — the reg/connection/init lines were dropped, and the now-stale "Test Group 8: Chirp Counter Passthrough" was repurposed as a port-removal smoke test that confirms the chain still produces FFT_SIZE outputs without that input. multi_segment.chirp_counter input remains on the port list (it could plausibly be wired to per-chirp logic in the future); it is now formally unused but iverilog/Vivado do not flag unused module inputs. Quick regression: 28/29 PASS (same as baseline; the 1 fail is the known iverilog/Xilinx-IP RX-NEW-3 gap unchanged by this commit). |
||
|
|
2c34323bcb |
fix(mcu): MCU-N5/C4 — runRadarPulseSequence stops shadowing m/n/y globals
runRadarPulseSequence was redeclaring `int m, n, y` at function scope, which shadowed the file-scope `uint8_t m, n, y` globals at lines ~190-192 that getStatusString reports to the GUI as BeamPos|Azimuth|ChirpCount. The function's increments updated only the locals, then discarded them — so telemetry was permanently frozen at "BeamPos:1|Azimuth:1|ChirpCount:1" no matter how many beam positions or revolutions had elapsed. Fix: drop the three local declarations; the body already references m/n/y by name, so removing the locals lets the writes hit the globals. A comment documents the pitfall so the locals do not get re-added by a future cleanup. Numeric ranges are safe (m_max=32, n_max=31, y_max=50, all fit in uint8_t). Test: new standalone test_bug16_runradar_shadows_globals.c reproduces both the buggy (locals shadow globals) and fixed (globals advance) patterns and asserts the expected post-sweep values (g_n=16, g_m=1 wraps each iter, g_y=2 after one revolution). MCU regression: 76/76 (was 75). |
||
|
|
e9e301dc50 |
fix(gui): GUI-S1 — structural validation in find_packet_boundaries
The packet-boundary scanner only checked header + footer bytes, so any
payload byte that happened to be 0xAA (or 0xBB) and which lined up with
a 0x55 at offset+10 (or +25) was accepted as a packet. A single corrupt
byte could permanently shift the binning until the next frame_start
re-sync.
Added two structural sentinel checks against fixed bits the FPGA
emitter always drives to known values:
- data byte 9 = {frame_start, 6'b0, cfar_detection} -> bits[6:1]==0
- status byte 1 = high byte of status_words[0] -> 0xFF
Combined with the existing footer check, false-match probability drops
from ~1/256 to ~1/16384 (data) and ~1/65536 (status). Mock generators
already produce conformant bit patterns, so existing parser/mock-read
tests pass unchanged.
New tests:
- test_find_boundaries_rejects_false_data_header (forged 0xAA...0x55)
- test_find_boundaries_rejects_false_status_header (forged 0xBB...0x55)
- test_find_boundaries_recovers_after_byte_drop (single-byte loss)
Tests: GUI 96/96 (was 93), test_v7 83/83, MCU 75/75, ruff clean.
No RTL change -- wire format is unchanged; this hardens the parser only.
|
||
|
|
760288037f |
fix(gui): GUI-S2 — DataRecorder snapshots arrays before HDF5 write
The same RadarFrame is enqueued for the display consumer and handed to DataRecorder.record_frame on the producer thread. h5py releases the GIL during gzip compression, so any in-place mutation by the consumer (or a future scaling/normalisation step) would tear the on-disk frame. record_frame now copies all five numpy arrays into local snapshots before passing them to h5py.create_dataset. Disk integrity no longer depends on consumer behaviour. New test test_record_frame_isolates_from_post_call_mutation asserts that mutating every array in place after record_frame returns leaves the HDF5 contents untouched. Tests: GUI 93/93 (was 92), ruff clean repo-wide. |
||
|
|
ca2b6e527d |
fix(fpga): TX-G — surface chirps_mismatch_error to host status
`chirps_mismatch_error` was set in radar_system_top when the host requested chirps_per_elev != Doppler FFT size, but never wired into the USB status response — a latent silent failure. Wired the flag through both USB interfaces (FT601 + FT2232H) into bit [10] of status word 4 (was reserved). GUI parser exposes it as StatusResponse.chirps_mismatch. - usb_data_interface*.v: new status_chirps_mismatch input, packed at [10] - radar_system_top.v: connect chirps_mismatch_error to both USB instances - radar_protocol.py + test_GUI_V65_Tk.py: parse new bit, +1 round-trip test - tb_usb_data_interface.v: drive the new port, update word-4 expectation Tests: GUI 92/92 (was 91), MCU 75/75, USB TB 91/91, ruff clean repo-wide. The 2 remaining FPGA regression failures (Receiver Integration, MF Chain) are the pre-existing iverilog-can't-link-Xilinx-IP issue tracked separately as the open RX-NEW-3 follow-up. |
||
|
|
89dc9156c7 |
fix(fpga): RX-F — MTI exits mute on chirp boundary, not just last bin
mti_canceller previously armed has_previous and refreshed prev_chirp_was_long only when range_bin_d1 == NUM_RANGE_BINS - 1. range_bin_decimator can early-terminate a chirp before reaching the last bin (overflow guard at range_bin_decimator.v:306, watchdog at :314), so on every such chirp MTI never armed and stayed muted forever on every subsequent chirp until reset. Detect chirp boundary internally using bin-0 arrival after at least one non-zero bin in the prior chirp. effective_has_previous lifts has_previous=1 the cycle chirp_boundary fires so the new chirp's bin-0 is subtracted (read-before-write on prev[0] correctly returns the previous chirp's bin-0). prev_chirp_was_long now updates on every range_valid_d1 (no-op within a chirp; OLD value still visible at the chirp_boundary cycle for the waveform_changed compare). Pass-through clears saw_nonzero_bin_in_chirp so the first MTI-enabled chirp after a pass-through run is correctly muted. No port changes. tb_mti_canceller T13 added: feed a 32/64-bin partial chirp followed by a full chirp, verify the second chirp is NOT muted (would fail without the fix). MTI Canceller goes from 40 -> 43 checks, all passing. Local regression: 32/34 PASS (same as baseline; the two failing tests are pre-existing RX-NEW-3 FFT throughput). |
||
|
|
5c8cc8c96a |
feat(fpga): swap matched-filter chain to Xilinx LogiCORE FFT v9.1 IP
Replaces the in-house iterative fft_engine.v in the matched-filter chain
with the Pipelined Streaming Xilinx FFT IP, closing RX-NEW-3 (FFT chain
~11x too slow vs PRI budget).
Components:
* ip/xfft_2048_ip/xfft_2048_ip.xci — committed IP definition
(16-bit fixed point, BFP scaling, convergent rounding, natural order,
pipelined-streaming, BRAM data/reorder/phase factors). Vivado
regenerates .dcp / sim-netlist from this on each build.
* scripts/50t/gen_xfft_2048_ip.tcl — IP-Catalog generation script
* scripts/50t/run_xfft_xsim.sh — XSim batch runner for tb_xfft_2048_xsim
* xfft_2048.v — AXI-Stream wrapper. FFT_USE_XILINX_IP define routes to
real LogiCORE for synth/XSim; falls back to fft_engine batched
one-shot for iverilog (unit coverage only).
* fft_engine_axi_bridge.v — exposes legacy fft_engine port surface on
top of the xfft_2048 AXI wrapper, so the chain swap is a 1-line
module-name change.
* matched_filter_processing_chain.v — fft_engine -> fft_engine_axi_bridge
* scripts/50t/build_50t.tcl — read_ip + generate_target + synth_ip;
adds FFT_USE_XILINX_IP to verilog defines.
* tb/tb_xfft_2048_xsim.v — XSim verification (DC, impulse, tone bin 128).
All 5 assertions PASS on remote with the real IP; tuser=0x0a (BLK_EXP=10)
confirms BFP scaling working.
Local iverilog regression: 32/34 PASS — identical to baseline. Same two
RX-NEW-3 failures (Receiver Integration, Matched Filter Chain) — these
only resolve in remote XSim with the real IP, since iverilog uses the
fft_engine fallback inside xfft_2048 (~150K cycles/pass, not the
~2200-cycle Pipelined Streaming throughput). MF cosim 4/4 PASS confirms
bridge bit-exact in fallback mode.
Pending: remote XSim of tb_radar_receiver_final to demonstrate Doppler
frames produced within PRI budget; remote synth to confirm DSP/timing
post-IP.
|
||
|
|
cc6691dec9 |
perf(fpga): move CIC comb stages to fabric — 80→70 DSPs (-10)
Strip the explicit DSP48E1 instance from comb stage 0 and the (* use_dsp = "yes" *) attribute from comb stages 1-4. The combs are gated by data_valid_comb_pipe (fires once every 4 clk_400m cycles post-decimation), so a multicycle path of 4 -setup / 3 -hold scoped to the comb registers in xc7a50t_ftg256.xdc gives STA 10 ns of slack for fabric carry-chain to close 28-bit subtracts comfortably. Pipeline depth and bit-widths unchanged: the new fabric model mirrors the prior CREG+AREG+BREG+PREG structure exactly, so data_valid_comb_0_out alignment and downstream stages 1-4 see bit-identical samples. CIC behavioral simulation model now lives outside the SIMULATION ifdef branch (used unconditionally) since there is no longer a synthesis-only DSP48E1 to replace. 50T post-impl results (Vivado 2025.2): DSPs: 80 → 70 / 120 (66.7% → 58.3%, freed 10) LUTs: 22114 / 32600 (67.8%) BRAM: 55.5 / 75 (74.0%, unchanged) adc_dco_p WNS: +0.022 ns → +0.906 ns (margin improved) All clocks meet timing, 0 failing endpoints. Local regression: 32/34 PASS — same as baseline; the two failures (Receiver Integration, Matched Filter Chain) are pre-existing RX-NEW-3 (FFT throughput) and unaffected by this change. Bit-exact through DDC chain (NCO→CIC→FIR) and MF cosim verified. Cumulative DSP savings today: 112 → 70 (freed 42), enough headroom for Xilinx LogiCORE FFT Pipelined Streaming swap (~33 DSPs for the 3-instance matched-filter chain) with 17 DSPs to spare. |
||
|
|
0b2f75620e |
perf(fpga): symmetric pre-adder FIR — 32→16 DSPs/channel (-32 total)
Re-group the 32-tap symmetric lowpass into 16 (D+A)*B operations using the DSP48E1 pre-adder, exploiting coeff[k] == coeff[31-k]. Production silicon (XC7A50T) drops from 112/120 DSPs (93.3%) to 80/120 (66.7%), freeing the budget needed for the matched-filter FFT swap (RX-NEW-3). Bit-exact contract preserved at non-saturating signal levels: DC=5000 → 8847 and 45 MHz tone → ±16 LSB match the unfolded design and the Python golden model. Throughput unchanged (1 sample/cycle, 100 MSPS); latency +2 cycles for the pre-adder stage. Saturation thresholds rebuilt via bit concatenation to dodge the Verilog 32-bit-literal trap (1 <<< 34 silently wraps to 0, which made the earlier symmetric draft assert positive saturation on all non-negative accumulator values). Local regression: 32/34 PASS — same as baseline; the two failures (Receiver Integration, Matched Filter Chain) are pre-existing RX-NEW-3 (FFT throughput) and unaffected by this change. |
||
|
|
977434a5f6 |
docs(fpga): correct fir_lowpass.v rate comment + flag rate/coeff mismatch
The header had two claims that "valid samples arrive every ~4 cycles" at the FIR boundary. That is false in the production wiring: the CIC `_4x` decimator turns clk_400m into a 100 M-pulse-per-second stream, then cdc_adc_to_processing crosses that into clk_100m where dst_valid asserts every cycle in steady state. The 4:1 ratio applies between the two clock domains, not as further sub-sampling inside clk_100m. This matters because the 32-tap coefficients were designed for the 25 MSPS rate the wrong comment described, but the FIR is actually being driven at 100 MSPS. The cutoff sits 4x higher than intended; existing tests pass because the 36-bit accumulator silently wraps on large sustained inputs (see RX-NEW-3 in the project ledger). Comment-only commit. No RTL behaviour change. Any future DSP-saving rework — symmetric pre-adder, 4:1 fold, Xilinx FIR Compiler — needs a designer call on whether to redesign coefficients for 100 MSPS, add a real decimation stage to hit 25 MSPS, or keep the current accidental behaviour. |
||
|
|
6f68f3263a |
fix: MCU-N4 delay_us bound; GUI-S4 STREAM_CONTROL comment
MCU-N4: delay_us(us) reset TIM1 then waited for the counter to reach `us`, but TIM1 ARR is 0xffff-1 (~65 ms at the 1 MHz tick). Any caller passing us > 65534 spun forever after the first wrap — a real hazard with the PA energized. Chunk requests larger than ARR into ARR-sized waits, then the remainder in the existing single wait. Current callers (T1, PRI1-T1, Guard, 500us spots) are all well under the bound; this is defensive. GUI-S4: radar_protocol.STREAM_CONTROL was annotated "3-bit stream enable mask"; the FPGA accepts usb_cmd_value[5:0] = 6 bits. The wire protocol already carried the full 32-bit value field, so the upper bits were reachable via Custom Command — only the comment was wrong. Updated to match radar_system_top.v:1004. Verified: 75/75 MCU tests pass; 83/83 v7 GUI tests pass (covered by GUI-C3 commit). |
||
|
|
bf39941074 |
fix(fpga): RX-NEW-2 — replace impossible peak/mean assertions with flatness bounds
The Group 3 (tone autocorrelation), Group 10 (golden DC autocorr), and
Group 11 (golden tone autocorr) tests asserted cap_max_abs > mean_abs * 2,
which is mathematically impossible for those stimuli regardless of FFT
precision:
- DC autocorrelation produces a constant-magnitude time-domain output
(peak/mean ≡ 1.0 by definition).
- Single-tone autocorrelation produces a constant-magnitude rotating
phasor; |I|+|Q| envelope varies in [|X|^2, sqrt(2)*|X|^2], so
peak/mean is bounded by ~1.41x.
Empirical RTL output ratios from this regression: DC=1.07x, Tone5=1.18x,
Chirp=3.14x, Impulse=2015x — confirming theory and confirming the FFT
engine is correct for narrow-spectrum inputs.
Replace each ">2x" check with mean>0 && peak<=mean*2 (flatness bound).
Still catches flat-zero output (mean=0) but admits the correct constant-
magnitude result.
Matched Filter Chain regression: 5 failures -> 2 failures.
|
||
|
|
875642f512 |
fix(gui): GUI-C3 — derive live-mode range/velocity resolution from waveform
RadarDataWorker (live capture path) was converting bin indices to physical units using RadarSettings placeholders (range_resolution=6.0, velocity_resolution=1.0). The 1.0 m/s velocity figure was a stub — the correct value at 167us PRI / 16-chirp sub-frame / 10.5 GHz carrier is ~5.34 m/s, so reported velocities were off by ~5.3x. ReplayWorker already used WaveformConfig.range_resolution_m and velocity_resolution_mps; this commit applies the same pattern to the live worker. WaveformConfig defaults match the AERIS-10 3 km mode parameters. A set_waveform() hook is provided so the dashboard can swap configs when range-mode switching is wired through. Verified: 83/83 v7 GUI tests pass. |
||
|
|
f1f69ca623 |
ci(fpga): wire RX-B latency tests; fix downstream compile after inline-FFT removal
- run_regression.sh: add frequency_matched_filter.v to PROD_RTL and RECEIVER_RTL compile groups (was implicitly required after inline behavioural FFT in matched_filter_processing_chain.v was removed); empty EXTRA_RTL with set -u guards; bump Matched Filter Chain timeout to 600s. - run_regression.sh: add two PHASE 3 tests — tb_rxb_latency_measure (chain pipeline depth) and tb_rxb_fullchain_latency (multi-segment + chain). - radar_receiver_final.v: replace dangling delayed_ref_i/q references (left over from latency_buffer removal) with ref_chirp_real/imag. - tb/tb_radar_receiver_final.v: chain-state debug uses production collect_count/out_count signals instead of the deleted SIMULATION-only fwd_in_count. - tb/tb_rxb_latency_measure.v: add explicit [PASS]/[FAIL] markers around the 2007..2107 cycle expected-latency window. |
||
|
|
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.
|
||
|
|
27c9c22ad2 |
test(fpga): regression coverage for C-3 and USB NUM_CELLS bugs
Two bugs fixed recently had no tests that would have failed before the fix. Add direct regressions so either cannot silently return: 1. tb_chirp_controller Group 3b (multi-frame, C-3): run a second full frame back-to-back after DONE and assert chirp_counter returns to 0, frame 2 reaches GUARD_TIME after exactly CHIRP_MAX/2 long chirps, and frame 2 reaches DONE. Before the fix, chirp_counter held at CHIRP_MAX after frame 1, the LONG_LISTEN -> GUARD guard (=CHIRP_MAX/2-1) never matched, and frame 2 ran extra chirps until the 6-bit counter wrapped — these checks fail loudly if that regresses. 2. tb_usb_data_interface frame-sync width + value pins: assert $bits(uut.sample_counter) >= 15 and uut.NUM_CELLS == 15'd16384. Protects against reintroducing the 12-bit / 2048-cell constants that fired 8 false frame-start markers per real 512 x 32 frame. Regression: 32/32 PASS; USB TB 89 -> 91 checks. |
||
|
|
3d0ee50999 |
fix(fpga): reset chirp_counter at DONE; source CHIRP_MAX from radar_params
C-3: plfm_chirp_controller_enhanced never reset chirp_counter when the frame completed. Counter sat at CHIRP_MAX after frame 1, so the LONG_LISTEN -> GUARD transition guard (== CHIRP_MAX/2-1) never matched correctly on subsequent frames and frame 2+ ran extra chirps until the 6-bit counter wrapped. Reset chirp_counter in the DONE state. S-2: Replace hardcoded CHIRP_MAX = 32 with RP_CHIRPS_PER_FRAME from radar_params.vh so the TX FSM tracks the single source of truth. S-1: Correct misleading labels in tb_system_e2e G14.1-G14.3. Per radar_params.vh the range_mode encoding is 2'b00 = 3 km, 2'b01 = long-range, 2'b10/2'b11 = reserved. The TB strings previously called 2'b01 "short" and 2'b10 "long", which is inverted and inconsistent with the RTL comments in radar_mode_controller.v. Regression: 32/32 PASS. |
||
|
|
21aaa5ac33 |
fix(fpga): correct USB frame-sync counter for 512x32 cell grid
usb_data_interface.v NUM_CELLS was still 12'd2048 (64 range x 32 doppler) from the pre-2048-FFT architecture. With 512 range bins x 32 Doppler, the 12-bit counter wrapped every 2048 packets and the host received 8 false frame-start markers per real frame via the sample_counter==0 bit packed into the detection byte. Widen counter to 15 bits and set NUM_CELLS to 16384. Sister file usb_data_interface_ft2232h.v was already correct. Remove three stale testbenches hardcoded to the old 1024-pt / 64-bin architecture (tb_mf_chain_synth, tb_fullchain_mti_cfar_realdata, tb_range_fft_realdata). Equivalent current-architecture coverage already exists in tb_matched_filter_processing_chain, tb_fullchain_realdata, tb_fft_engine, tb_multiseg_cosim, and tb_mf_cosim. |
||
|
|
f39a78cb1e |
chore(fpga): untrack TB-generated CSV, ignore a.out
rx_final_doppler_out.csv is written by tb_radar_receiver_final.v on every run via $fopen — it is a test-run artifact, not an oracle. It was mistakenly tracked in an earlier commit, causing unnecessary churn on every sim. Remove from the index and ignore going forward. Also ignore stray a.out from iverilog one-shot compiles. Golden references (.hex, .mem, doppler_golden_py_*.csv) remain tracked — they are load-bearing oracles used by MF / Doppler / receiver cosim testbenches. |
||
|
|
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. |
||
|
|
c668652ba8 |
merge(wave3/tier2): port testbenches and cosim goldens for fft-2048
Regression goes from 21/32 -> 27/32 passing.
TB files updated from feat/fft-2048-upgrade (FFT_SIZE=2048 / 512 range
bins / Manhattan magnitude / 2-segment matched filter):
- tb/tb_mf_cosim.v (range_profile_{i,q} port names)
- tb/tb_matched_filter_processing_chain.v (long_chirp port names)
- tb/tb_range_bin_decimator.v (new 2048->512 DUT)
- tb/tb_radar_mode_controller.v (XOR edge detector)
- tb/tb_doppler_cosim.v (2048-deep inputs)
- tb/tb_multiseg_cosim.v
- tb/tb_mf_chain_synth.v
Cosim infrastructure regenerated with FFT_SIZE=2048:
- tb/cosim/gen_mf_cosim_golden.py
- tb/cosim/gen_doppler_golden.py
- tb/cosim/compare_mf.py, compare_doppler.py
- tb/cosim/fpga_model.py
- All mf_* and doppler_* goldens/inputs regenerated
Deliberately NOT taken:
- tb/tb_radar_receiver_final.v — kept p0's version because the merged
radar_receiver_final requires tx_frame_start + adc_or_p/n inputs
that fft's TB does not drive. Its 3 failures (G1 golden mismatch,
B3/B5 hardcoded 64-bin limits) are tracked as known issues; TB
needs a 64->512 bin rewrite + golden regen against merged RTL.
Known remaining failures (5/32):
- Doppler Co-Sim x3: python compare mismatch — goldens generated
against fft's reset/DDC behavior; merged RTL uses p0's reset
strategy. Needs golden regen against merged RTL.
- Receiver Integration: TB has stale 64-bin localparams/widths.
- Matched Filter Chain: 3/40 "peak magnitude > 0" checks fail on
behavioral-FFT cases. Pre-existing on fft branch (known brittle).
|
||
|
|
5f3002a4d1 |
merge(wave2): manual resolution of 6 shared files — fft-2048 × p0 audit
Hand-merged files modified on both fix/pre-bringup-audit-p0 and feat/fft-2048-upgrade. Wave 1 (commit |