mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-08-09 03:53:14 +00:00
4c9032187a23f40692b95ea3a82300edea36133a
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4c9032187a |
test(fpga): PR-AD AD.3 — FT601 system-opcode TB + SUPPORT_LONG_RANGE compile guard
Guards against future drift between the two USB driver command paths
(USB_MODE=0 vs USB_MODE=1) and against 200T+long-range build breakage
in usb_data_interface.v.
tb/tb_system_opcodes_ft601.v — new (~397 LOC):
- Sibling of tb_system_opcodes.v with radar_system_top
#(.USB_MODE(0))
- FT601 RX BFM: 32-bit single-word read with 5-cycle word holds
(IDLE -> OE_ASSERT -> READING -> DEASSERT -> PROCESS)
- 28/0 PASS, same opcode coverage as the FT2232H sibling
run_regression.sh — adds 2 entries:
- "FT601 Driver Long-Range Compile (PR-AD AD.3)" in Phase 1:
inline iverilog -DSUPPORT_LONG_RANGE on usb_data_interface.v;
suppresses informational warnings (timescale / dangling / @*
sensitive) and fails on real warnings. Catches macro creep
that would break the 200T 20-km build before Vivado synthesis.
- "System Opcodes FT601 (tb_system_opcodes_ft601)" in Phase 2
(non-quick block).
radar_system_top.v — header doc block documenting the
USB_MODE x SUPPORT_LONG_RANGE 4-cell build matrix and clarifying
that the flags are orthogonal (200T+FT601 with 3-km-only DSP is a
valid bring-up baseline; 50T+FT2232H+LR is rare but must still
compile). Notes that both drivers emit the identical v2 bulk frame
and that byte-equality is enforced by tb_usb_drivers_parity.v.
Regression: 46/0/0 (44 + 1 system TB + 1 LR compile guard).
Lint: 0 errors, 2 advisory (pre-existing, unrelated).
|
||
|
|
e1abeecaa9 |
test(fpga): PR-AD AD.2 — USB driver TB rewrite + cross-comparison parity TB
tb/tb_usb_data_interface.v was on the obsolete pre-PR-G streaming
protocol (1-bit cfar_detection, 4-state FSM) — green CI gave false
confidence because TB and DUT were equally out of date. This rewrite
exercises the new v2 bulk FSM and adds a cross-comparison TB that
asserts byte-for-byte equality between the FT601 and FT2232H drivers
fed identical stimulus.
tb/tb_usb_data_interface.v — full rewrite (~370 LOC):
- Mirrors tb_usb_protocol_v2.v test groups for the FT601 path
- BE-aware byte capture using a single capture_idx integer with
blocking arithmetic + one non-blocking egress_count update
(four separate non-blocking assigns dropped 3 of 4 lanes — race
caught during development)
- 31/0 PASS, egress_count = 56330 bytes (matches FT2232H exactly)
tb/tb_usb_drivers_parity.v — new (~376 LOC):
- Instantiates both drivers in one TB, fed identical clk/reset/
streaming inputs, drained concurrently
- Captures each driver's byte stream into a 65536-byte ring buffer
- assert_parity task finds first byte mismatch index
- 3 scenarios: header-only (10 B), status packet (34 B), full
frame (56330 B)
- 9/0 PASS — drivers are now byte-equal across all scenarios.
Any future v2 protocol change on FT2232H must land on FT601 in
the same PR or this guard fires.
run_regression.sh — adds "USB Drivers Parity (PR-AD AD.2
cross-comparison)" entry in Phase 1 (changed-modules block).
Regression: 44/0/0 (43 baseline + 1 parity test).
|
||
|
|
004c933ef0 |
feat(fpga): PR-AD AD.1 — usb_data_interface.v v2 bulk parity rewrite + top wiring
The FT601 USB 3.0 driver was 5 PRs behind the FT2232H driver: still
emitting pre-PR-G 12-byte per-cell streaming packets with 1-bit
cfar_detection, no subframe_enable snapshot (M-8), no MEDIUM PRI
status readback (M-5 / PR-AC.1), no CFAR telemetry (PR-G). Host
parser only speaks v2 bulk, so a 200T+FT601 build couldn't talk to
the GUI. This commit closes that gap by cloning the FT2232H module
body verbatim and swapping the output stage from 8b/cycle to
32b+4b-BE/cycle.
usb_data_interface.v — full rewrite (~1300 LOC):
- 3 internal BRAMs (doppler_mag / range / detect), 3-cycle RMW
pipeline, Manhattan magnitude, all CDC chains, 8-state WR FSM
copied from FT2232H reference verbatim
- status_words[0..7] 34-byte packet (M-5 parity with PR-AC.1
|
||
|
|
38d6dd0719 |
PR-AB.b expanded commit 6: subframe_pulse strip + stale MCU DIAG cleanup
Final cleanup pass for the PR-AB.b expanded bundle. chirp_scheduler.v / radar_receiver_final.v: delete the unused subframe_pulse output + its sibling subframe_id port. Both were declared on the scheduler and bound at the radar_receiver_final.sched instance, but no downstream module read them -- doppler_processor counts sub-frame boundaries internally from new_chirp_frame + CHIRPS_PER_SUBFRAME=16. The stale "// doppler picks up in PR-F" comment was aspirational; PR-F never wired it. subframe_id demoted from output port to internal reg (still consumed by the FSM's next_enabled_subframe helper). tb_chirp_scheduler_handshake.v: drop the matching observer wires + port bindings. main.cpp: replace the F-2.1 "host_radar_mode = 2'b01 (auto-scan, FPGA-owned chirp dispatch)" boot DIAG + 8-line comment block with a short comment noting the mode register was retired in commit 1. The DIAG was asserting a register that no longer exists. Regression: - FPGA iverilog: 43/0/0 (tb_chirp_scheduler_handshake 16/16) - MCU: 51/0 (GPS) + 34/0 (AGC/safety/gap-3) Closes the PR-AB.b expanded bundle (commits 1-6) on feat/dual-range-v2. |
||
|
|
4989c33aa6 |
PR-AB.b expanded commit 5: Beam-ready handshake (RTL + MCU + GUI)
Wire a per-frame MCU→FPGA "beam pattern ready" handshake so the chirp scheduler can stall between 48-chirp frames until the MCU finishes writing the next ADAR1000 pattern. The legacy unused stm32_new_chirp input on PD8 is repurposed as stm32_beam_ready; chirp_scheduler.v gets a new S_BEAM_WAIT state entered after each frame_pulse and an 80 ms watchdog so a missed MCU toggle degrades to wall-clock cadence with a sticky telemetry bit rather than stalling the radar. Cold-reset defaults the handshake off (host_handshake_enable=0, new opcode 0x1A); the GUI opts in once the MCU PD8 wiring is verified on the bench. Both the FT601 and FT2232H status word 4 paths get the new beam_handshake_watchdog_fired sticky at bit [1] (reclaimed from the range_mode retirement in commit 1). RTL: - chirp_scheduler.v: 2-FF ASYNC_REG sync on beam_ready_async; 1-cycle edge detect (any transition, MCU side uses HAL_GPIO_TogglePin); new S_BEAM_WAIT state entered at frame_pulse when host_handshake_enable=1; 23-bit beam_watchdog counter with BEAM_WATCHDOG_MAX = 8_000_000 (~80 ms at 100 MHz, ~10 nominal frames); beam_handshake_watchdog_fired output sticky across mixers_enable cycles, cleared only by reset_n; mid-wait disable releases the FSM so dropping the opcode never strands the radar between frames. - radar_receiver_final.v: thread stm32_beam_ready_async + host_handshake_enable + beam_handshake_watchdog_fired through the scheduler instance. - radar_system_top.v: rename input port stm32_new_chirp → stm32_beam_ready; add host_handshake_enable register (cold-reset = 1'b0); opcode 0x1A dispatch (value[0]); add rx_beam_handshake_watchdog wire; pack into status_words[4][1] in both USB paths. - radar_system_top_50t.v: rename wrapper port + sub-instance wiring. - usb_data_interface.v + usb_data_interface_ft2232h.v: add status_beam_handshake_watchdog input + 2-FF level CDC (same convention as F-6.4 / F-1.2 stickies); refresh word-4 layout doc comment; pack beam_handshake_wd_sync_1 into status_words[4][1]. XDC: - xc7a50t_ftg256.xdc + xc7a200t_fbg484.xdc: rename stm32_new_chirp port references to stm32_beam_ready (same PD8 pin, F13 on 50T / L18 on 200T). MCU: - main.h: add FPGA_BEAM_READY_Pin = GPIO_PIN_8 + FPGA_BEAM_READY_GPIO_Port = GPIOD alongside the existing FPGA_FRAME_PULSE alias. - main.cpp:runRadarPulseSequence: insert HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_8) after each setCustomBeamPattern16(RX) — once after the per-azimuth broadside (vector_0), once after matrix1, once after matrix2 — between the SPI burst completion and waitForFramePulse. GUI: - radar_protocol.py: Opcode.HANDSHAKE_ENABLE = 0x1A; StatusResponse.beam_handshake_watchdog = 0 default; parse word 4 bit [1] in parse_status_packet; update word-4 layout comment. - test_GUI_V65_Tk.py: add beam_handshake_watchdog kwarg to _make_status_packet (sets bit [1] of word 4); refresh test_parse_status_word4_layout_co_spec to cover the new bit (used+9=32); add test_parse_status_beam_handshake_watchdog round-trip; test_handshake_enable_opcode pins 0x1A; defaults / chirps_mismatch / agc-coexist tests gain a watchdog==0 assertion; bump test_all_rtl_opcodes_present expected set to include 0x17/0x18/0x19/0x1A. TB: - new tb_chirp_scheduler_handshake.v (16 checks): legacy open-loop, edge exit (rising + falling), 200-cycle idle hold, watchdog auto-advance via force on dut.beam_watchdog, sticky-survives-mixers_disable, mid-wait disable release, reset_n clears sticky. - run_regression.sh: register the new TB in PHASE 1. - tb_radar_receiver_final.v: tie the 3 new receiver ports off (beam_ready_async=0, handshake_enable=0, watchdog unconnected). - tb_system_mechanics.v / tb_system_opcodes.v: explicit .stm32_beam_ready(1'b0) connection (the cold-reset host_handshake_enable=0 keeps the FSM out of S_BEAM_WAIT). - tb_usb_data_interface.v / tb_usb_protocol_v2.v / tb_e2e_dsp_to_host.v / tb_ft2232h_frame_drop.v: tie .status_beam_handshake_watchdog(1'b0). Ride-along ruff sweep (14 → 0 across the repo): - tb/cosim/compare_independent.py: RUF003 — '5×' → 'at least 5x'. - tb/cosim/gen_e2e_expected.py: noqa: E402 on the post-sys.path import; drop unused EXPECTED_RANGE_BIN + EXPECTED_DOPPLER_BIN_PER_SF imports; fold the detect-class slot if/else into a ternary (SIM108). - tb/cosim/gen_e2e_stimulus.py: drop int() wrapping round() at four call sites (RUF046 — round() already returns int in Python 3); rewrite the range-bin derivation comment block from code-like `# range_bin = ...` to prose (ERA001); strip stray f from placeholder-free error string (F541). - tb/cosim/tb_e2e_dsp_to_host_parse.py: open(path, 'r') → open(path) (UP015). - v7/dashboard.py: '3×' → '3x' (RUF003); drop quotes from 'StatusResponse | None' annotation (UP037, file already has `from __future__ import annotations`). CI summary (all suites green pre-commit): - ruff: All checks passed! - FPGA regression (iverilog): 43 / 0 / 0 (incl. new handshake TB 16/16). - MCU tests: 51 / 0 + 34 / 0 + 13 / 13 ADAR1000_AGC. - GUI Tk (test_GUI_V65_Tk): 120 / 0. - GUI v7 (test_v7): 152 / 0. Production rollout note: bitstream cold-resets with host_handshake_enable=0 so existing flashes keep their open-loop cadence until the GUI sends opcode 0x1A=1. Once enabled, the per-pattern dwell tracks both the chirp ladder (PD14 frame_pulse from commit-3 work) and the MCU pattern-write completion (PD8 toggle from this commit), eliminating drift from the SPI burst timing. |
||
|
|
2e2c10baeb |
PR-AB.b expanded commit 4: GUI cleanup (modes / range_mode strip)
Strip the host-side parser/dashboard/test references for the FPGA registers retired in commit 1: host_radar_mode (opcode 0x01), host_trigger_pulse (opcode 0x02), and host_range_mode (opcode 0x20). The v7 backend (models.py / software_fpga.py / processing.py) had no references — only the parser, dashboard, and Tk test file did. - radar_protocol.py: drop Opcode.RADAR_MODE / TRIGGER_PULSE / RANGE_MODE enum members; rebuild the doc table around the surviving opcodes; drop StatusResponse.radar_mode and StatusResponse.range_mode fields; drop the two parse-status assignments (sr.radar_mode = words[0]>>22 and sr.range_mode = words[4] & 0x03); update the layout comments on words 0 + 4 to mark the freed bits as reserved-0. Acquisition loop log line switches from "mode=X stream=Y" to "stream=Y chirps/elev=Z" — radar mode is no longer a runtime concept. - v7/dashboard.py: delete the "Radar Mode Off" (0x01) and "Trigger Chirp" (0x02) QPushButtons from the operations group; remove "Mode:" and "Range Mode:" fields from _update_status_display. - test_GUI_V65_Tk.py: drop mode + range_mode kwargs from the _make_status_packet helper; update the word-4 layout co-spec test (range_mode entry deleted, reserved span widened from [9:2] to [9:0], sanity-check sum bumped from used+8 to used+10); delete test_parse_status_range_mode + test_radar_mode_names; rename test_agc_and_range_mode_coexist → test_agc_fields_coexist_with_mismatch with chirps_mismatch replacing range_mode as the coexisting field; drop 0x01 / 0x02 / 0x20 from test_all_rtl_opcodes_present's expected set. GUI tests: 118/0 (test_GUI_V65_Tk) + 152/0 (test_v7). FPGA regression unchanged at 42/0/0. |
||
|
|
fd6036b49b |
PR-AB.b expanded commit 3: XDC + MCU GPIO scrub (PD9 / PD10)
Strip the FPGA-side pin constraints and MCU-side GPIO init+toggles for the two STM32→FPGA beam-step GPIOs that the commit 1 RTL strip rendered unreachable. The MCU was toggling PD9 once per beam_pos iteration and PD10 once per azimuth step; both edges fed FPGA edge_detector_enhanced instances that drove elevation_counter / azimuth_counter regs in plfm_chirp_controller_v2 — counters that were never consumed (status pack didn't carry them; on 50T they went to _nc; on 200T to unconstrained outputs). GUI already uses MCU-side software counters m/n/y via USB-CDC. - constraints/xc7a50t_ftg256.xdc: delete PACKAGE_PIN E16 (PD9) + D16 (PD10); tighten stm32_new_* wildcard to explicit stm32_new_chirp. - constraints/xc7a200t_fbg484.xdc: delete PACKAGE_PIN N18 (PD9) + N19 (PD10); tighten wildcard same as 50T. - main.cpp:633: delete HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_9) inside the matrix1/matrix2 beam_pos loop. - main.cpp:655: delete HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_10) at the azimuth-step / stepper-rotate boundary. - main.cpp:3118 (MX_GPIO_Init output level): drop PD9 + PD10 from the GPIOD WritePin OR-mask. - main.cpp:3172-3174 (MX_GPIO_Init pin config): drop PD9 + PD10 from the GPIOD pin OR-mask + comment. PD9 + PD10 now default to high-Z inputs after MCU reset — no leakage path because the FPGA-side ports are gone. MCU regression: 51/51 + 34/34 suites green. FPGA regression unchanged at 42/0/0 (XDC isn't consumed by iverilog). The remaining DIG_0..DIG_3 bus pins are PD8 stm32_new_chirp (kept until commit 5 renames it to stm32_beam_ready), PD11 stm32_mixers_enable, and PD12 reset_n. |
||
|
|
0da5ac6eaa |
PR-AB.b expanded commit 2: TB cleanup (modes/range/counters strip)
Restore regression to green after commit 1's RTL strip. Drop dead-signal references from 10 TBs to match the post-strip RTL surface: - chirp TBs: drop clk_100m / reset_100m_n / new_elevation / new_azimuth port wiring on plfm_chirp_controller_v2; drop elevation/azimuth counter wires + check() assertions; retire Group 9 (Beam steering counters) in tb_chirp_controller and C3 (stm32 toggle non-drive) in tb_chirp_contract. - system TBs: drop stm32_new_chirp / stm32_new_elevation / stm32_new_azimuth regs + DUT port wiring; drop current_elevation / current_azimuth wires; delete stm32_chirp_toggle helper task; retire G6.1 (radar_mode opcode 0x01), G6.6 (trigger_pulse opcode 0x02), G7.1 (STM32->FPGA chirp CDC stress), and G14.1/.2/.3 (range_mode opcode 0x20) test groups; replace stm32_chirp_toggle calls in G2 with passive waits (scheduler auto-scans). - receiver TB: drop .host_mode / .host_range_mode / .host_trigger ties. - usb TBs: drop status_radar_mode / status_range_mode regs + DUT port wiring + apply_reset init + per-test setup; update word[4][1:0] expected value from 2'b10 (range_mode=2) to 2'b00 (now reserved) in tb_usb_data_interface; refresh T3.7 comment in tb_usb_protocol_v2 (assertion value 0x60 unchanged because range_mode was already 2'd0). - adc pwdn TB: strip host_radar_mode dummy reg + opcode 0x01 dispatch from the local dispatch_block mirror; rewrite T5 with unknown opcode 0x05 to preserve the dispatch-isolation case after 0x01 retirement. Regression: 42/0/0 (was 37/0/6 in the pre-strip baseline where scipy was unavailable; with radar_venv on PATH all 42 phases run and pass). |
||
|
|
1b2a21d55b |
PR-AB.b expanded commit 1: RTL strip (dead modes + counters + range_mode)
Flatten chirp_scheduler.v to single-FSM auto-scan. Mode 00 (STM32 pass-through), mode 10 (single-chirp debug) and mode 11 (track dwell) FSM branches were all half-implemented and unreachable in production: MCU dispatcher was deleted in F-2.1; mode 11 inputs were tied to constants in radar_receiver_final; mode 10 debug_wave_sel was hardcoded to SHORT. The case-switch wrapper, watchdog, effective_mode mux, and all host_track_* / host_debug_wave_sel / host_trigger plumbing are removed. Strip host_radar_mode (opcode 0x01), host_trigger_pulse (opcode 0x02), and host_range_mode (opcode 0x20). The mode register had no consumer after the single-mode flatten; the range_mode register was already write-only telemetry (declared as input in radar_receiver_final but never read). The runtime 3km vs 20km presentation on a 200T build is driven by host_subframe_enable (0x19) + per-waveform chirp/listen cycles (0x10-0x18) — no separate mode field needed. Strip stm32_new_elevation / stm32_new_azimuth GPIOs and the elevation_counter / azimuth_counter regs in plfm_chirp_controller_v2. The FPGA-side counters had no consumer (status pack never carried them; on 50T they went to _nc; on 200T to unconstrained outputs). MCU software counters n/y reach the GUI via USB-CDC on a separate channel. USB status word 0 bits [23:22] (was radar_mode) and word 4 bits [1:0] (was range_mode) are now reserved zeros — host parser keeps the same byte offsets. Files modified: chirp_scheduler.v - flatten to single FSM (~155 LOC delta) plfm_chirp_controller_v2.v - strip counter blocks + ports radar_transmitter.v - strip elev/azim CDC + edge detectors + ports radar_receiver_final.v - strip host_mode/range_mode/trigger + STM32 toggle ports radar_system_top.v - strip regs, opcodes 0x01/0x02/0x20, status_*_mode wiring, top-level ports radar_system_top_50t.v - strip _nc wires + stm32_new_elev/azim + tie-offs radar_system_top_te0713_umft601x_dev.v - strip status_radar_mode/range_mode ties usb_data_interface.v - drop status_*_mode ports, reserve word 0 [23:22] + word 4 [1:0] usb_data_interface_ft2232h.v - same as above radar_params.vh - strip RP_MODE_* / RP_RANGE_MODE_* / RP_OP_RADAR_MODE / RP_OP_TRIGGER_PULSE / RP_OP_RANGE_MODE / RP_DEF_TRACK_* Regression will fail at this commit due to TB references to deleted signals (host_radar_mode, status_range_mode, etc.) — TB cleanup follows in commit 2. |
||
|
|
a718e00475 | test(fpga): C-3 — align DDC ADC format test with exact conversion | ||
|
|
21bf7a0228 |
feat(fpga,gui): PR-AC.1 — M-5 status pkt 30→34 B for medium_chirp/medium_listen readback
Closes the 161-µs MEDIUM PRI visibility gap from the 2026-05-02 e2e audit.
PR-G ran out of reserved bits in status_words[3] to fit a second 16-bit
pair, so this PR adds status_words[7] = {medium_chirp[15:0], medium_listen[15:0]}
and bumps STATUS_PKT_LEN 30→34, STATUS_PACKET_SIZE 30→34.
RTL (usb_data_interface_ft2232h.v):
- Two new input ports `status_medium_chirp` / `status_medium_listen`.
- status_words array bound 6→7, init loop 7→8, snapshot block packs word 7.
- STATUS_PKT_LEN width 5→6 bits to hold 34, byte-mux index widened
[4:0]→[5:0] with 4 new entries for word 7 + footer at index 33.
- Header docstring + length-localparam comment refreshed.
Top-level (radar_system_top.v): wire `host_medium_chirp_cycles` /
`host_medium_listen_cycles` into the FT2232H usb_inst (gen_ft2232h branch
only; legacy FT601 path retains its pre-PR-G 6-word / 26-byte layout —
no host code reads it today).
Host parser (radar_protocol.py):
- STATUS_PACKET_SIZE 30→34. Module docstring + parse_status_packet
docstring + find_bulk_frame_boundaries note refreshed.
- StatusResponse gains `medium_chirp` / `medium_listen` fields.
- Word-loop bounds 7→8; word-7 decode added.
Tests:
- tb_usb_protocol_v2 — drive default RP_DEF_MEDIUM_*_CYCLES (500/15600),
move T3.2 footer assertion 29→33, add T3.8..T3.11 for word-7 bytes.
Manual run: 31/31 PASS (TB not in run_regression.sh).
- tb_ft2232h_frame_drop, tb_e2e_dsp_to_host — tie new DUT ports to
16'd0 (these TBs don't exercise status reads).
- test_GUI_V65_Tk — extend _make_status_packet builder kwargs, rename
test_status_packet_is_30_bytes → _is_34_bytes, add round-trip +
16-bit-max tests for word 7.
- test_v7 — TestStatusPacketV2RoundTrip rewired for 8 words / 34 B,
add test_word7_medium_pri_decoded + test_pre_M5_30byte_packet_rejected.
Verification:
- FPGA regression (iverilog + xsim cosim): 42/0/0.
- tb_usb_protocol_v2 standalone: 31/0.
- Host test_GUI_V65_Tk: 119/119.
- Host test_v7: 152/152.
|
||
|
|
a19359aeab |
fix(gui): PR-AB.b follow-up — settings note style, badge spacing, header padding, AGC strip colours
Cleanup of the four cosmetic items flagged in the PR-AB.b layout audit: - Settings tab: the "Host-Side Signal Processing" group note used DARK_WARNING (orange) but is purely explanatory, not a caution — converted to italic DARK_INFO (blue) so it matches the equally-informational Bench-mode note in the next group. - AGC group ALWAYS-ON badge: added margin-bottom so it reads as a header rather than sitting flush against the first AGC Target row. - Detected Targets table: "Velocity (m/s)" header was clipping its leading V against the column divider under Stretch resize mode (column width ~92 px, header text ~140 px). Header text shortened to "Vel (m/s)" so it sits comfortably alongside Range/Confidence/Magnitude/SNR/Track ID; QHeaderView::section padding bumped 10→12 px for general daylight. - AGC Monitor strip: Gain/Peak/Total Saturations labels now share a uniform DARK_FG colour. Only the value text on the AGC mode label still carries colour (green ALWAYS-ON / AUTO, blue MANUAL), so colour means state rather than label-decoration. Regression: GUI v7 150/150. |
||
|
|
ada170ef1f |
feat(fpga,mcu,gui): PR-AB.b — drift-free dwell sync via DIG_6 frame_pulse + AGC always-on policy
FPGA (Phase 1+2): - gpio_dig6 (PD14) now carries chirp_scheduler frame_pulse, FPGA-stretched to ~100 ns so the STM32 EXTI on PD14 can latch reliably. - gpio_dig7 (PD15) returns to its pre-PR-AB.b role: control-fault OR (range_decim_watchdog | CDC overrun); MCU stuck-high sampler unchanged. - rx_range_decim_watchdog gains a sticky in source clock domain so a slow status poll cannot miss a 1-cycle assertion (Phase 1). - New tb_dig6_frame_pulse.v (13 checks); tb_status_words_stickies.v extended with DIG_7 fault-OR coverage (14 checks); retired tb_audit_s10_gpio_split.v. - Port comments in radar_system_top.v / _50t.v and XDC roles refreshed. MCU (Phase 3): - PD14 reconfigured to GPIO_MODE_IT_RISING + GPIO_PULLDOWN; new EXTI15_10_IRQHandler in stm32f7xx_it.c dispatches to HAL_GPIO_EXTI_Callback that bumps a volatile g_frame_pulse_count. - runRadarPulseSequence dwell loop replaces 3x HAL_Delay(8) with waitForFramePulse(20) — per-pattern dwell now tracks the actual mask-aware ladder length (drift-free, mask-aware), with a 20 ms timeout safety net. - AGC outer loop is ALWAYS-ON in production (compile-time policy); bench builds compile the body out via -DMCU_AGC_FORCE_DISABLED. The runtime enable/debounce + DIG_6 polling that previously gated AGC are removed. - main.h adds FPGA_FRAME_PULSE_* aliases pointing at FPGA_DIG6_*. GUI (Phase 4): - Settings tab gains a Bench / Diagnostics group with a BENCH-MODE checkbox (off by default, persisted via QSettings). - AGC group header swaps between a green "AGC: ALWAYS-ON" badge (production) and Enable/Disable AGC buttons (bench), pinned to the top of the group. The redundant 0/1 spinbox row for opcode 0x28 is removed — buttons send the same opcode and cannot accept invalid input. - Both the FPGA Control AGC Status box and the AGC Monitor strip share a helper that honours bench-mode in production (always shows ALWAYS-ON in green so the two views never disagree with the badge). - _add_fpga_param_row uses setFixedWidth on label and Set button + explicit stretch=1 on the hint, so all rows align column-wise whether they sit directly in a QVBoxLayout or inside a wrapper QWidget. Regression: FPGA 42/0/0 (PR-M.4 baseline) - MCU 34/34 - GPS extended 51/51 - GUI v7 150/150 - BENCH-MODE flip behaviorally verified. Hardware-blocked steps deferred: bench-scope verify (PD14 dwell pulse, counter advance, PD15 stuck-high recovery still triggers). Closes #182. |
||
|
|
b215caa294 |
fix(mcu): PR-AB.a — move vector_0 out of inner beam_pos loop
runRadarPulseSequence used to fire vector_0 (broadside reference) between every matrix1 and matrix2 pattern, i.e. 15 times per azimuth. That dwell × 8 ms × 15 = 120 ms per azimuth × 50 azimuths = ~6 s of the 18.4 s revisit time was burned on redundant broadside frames. Pull vector_0 out of the loop and fire it once per azimuth before the sweep. Each azimuth now produces 1 broadside frame + 30 steered frames (matrix1 + matrix2 across 15 beam_pos), down from 15 + 30 = 45 frames. Revisit time drops from 18.4 s to ~12.8 s (31% improvement). If multiple per-position broadside frames are ever needed, gate them behind a runtime switch — the comment block flags this. test_bug16_runradar_shadows_globals updated to mirror the new 1-outside + 2-inside m-counter pattern; 13/13 PASS, full MCU regression 51/0 + 34/0. |
||
|
|
98ec9cb6a5 |
fix(fpga): PR-AA — doppler_mag 1-cell shift in usb emit FSM
The WR_DOPPLER_DATA emit advanced mag_rd_addr at end of phase 1 (LSB byte) but BRAM has 1-cycle read latency, so phase 0 of the next pair re-read the prior cell. Result: wire pair K = (HIGH(bram[K-1]), LOW(bram[K])) — adjacent cells silently swapped their high bytes whenever the high byte differed. Footprint was 30 of 24576 cells (peak rows + high-byte transitions in the noise floor); max diff 6656 LSB on the target row. Fix: advance the BRAM read address at end of phase 0 (MSB) so BRAM has 2 cycles between addr-set and the next pair's MSB read. Same pattern existed in WR_RANGE_DATA — silently broken (regression skips range stream); fixed for symmetry. After fix, both iverilog and remote Vivado 2025.2 xsim emit a bit-exact match against the Python golden. Tighten E12.14 / E12.6.b to strict zero tolerance and rename the "PR-AA pending" notes to indicate the fix has landed. Target-cell window check (E12.15) now points at the exact (rb, db) bin. Verification: * iverilog A6 in-TB: doppler_mismatches=0/24576 (16/16 PASS) * iverilog A6 parse strict: 28/28 PASS * Vivado 2025.2 xsim A6 in-TB: doppler_mismatches=0/24576 (16/16 PASS) * Vivado 2025.2 xsim A6 parse strict: 28/28 PASS * Full regression: 41 passed, 0 failed, 0 skipped / 41 total |
||
|
|
d9e7a5becf |
fix(fpga): PR-Y.1 + PR-X.3 — DC notch boundary, audit cleanups, formal retarget
Bundles audit items unblocked by the AERIS-10 end-to-end audit:
S-1 (radar_system_top.v) — DC notch off-by-one at width=7
Audit S-1: ±W around DC in a 16-bin FFT covers bins {0..W, 16-W..15}
(2W+1 total, bin 8 the only one excluded at W=7). The previous form
`< W || > 15-W+1` missed both boundaries: at W=1 it notched only {0}
(skipping 1 and 15); at W=7 it missed 7 and 9. Replaced with inclusive
comparators against 5-bit limits (`<= notch_lo || >= notch_hi`) which
hit the intended set for all W ∈ {1..7}. Cosim DC golden
(tb/cosim/rtl_bb_dc.csv) regenerated against the corrected behaviour.
S-7 (rx_gain_control.v) — reg→wire for combinational helpers
`wire_frame_sat_incr` / `wire_frame_peak_update` were declared `reg`
and blocking-assigned inside the clocked always block. They are pure
combinational functions of the registered inputs — promoted to
module-scope continuous assigns. Behaviour is bit-identical (the read
inside the always still reflects the prior-cycle latched values) but
the iverilog warnings disappear and the sim/synth correspondence is
unambiguous.
M-9 (formal/fv_radar_mode_controller.sby) — delete orphan
radar_mode_controller.v was retired in PR-D in favour of
chirp_scheduler.v; the .sby was never updated and pointed at a
non-existent module. Deleted.
M-10 (radar_receiver_final.v) — document `data_sync_error` unconnected
In production AD9484 produces a single 8-bit stream that the DDC mixes
into matched I/Q paths with symmetric pipelines, so `ddc_valid_i` and
`ddc_valid_q` rise on the same cycle and `data_sync_error` cannot
fire by construction. The check is retained inside
ddc_input_interface for the standalone tb_ddc_input_interface
unit-test (which intentionally drives valid_i ≠ valid_q). Adds
comments explaining the unconnected port at both call sites; no
functional change.
M-11 (radar_receiver_final.v) — `force_saturation_pulse` symmetric hook
The DDC has a `force_saturation` debug input that previously was tied
1'b0 directly. Routed through a new `force_saturation_pulse` wire
alongside the existing `clear_monitors_pulse` so a future host opcode
surface for "diagnostic force/clear" lands both at the same dispatch
point. Still tied 1'b0 today — RTL change is a placeholder for the
opcode plumbing.
PR-X.3 F-7.5 (formal/fv_cdc_adc.{v,sby}) — retarget to cdc_async_fifo
Prior wrapper instantiated `cdc_adc_to_processing`, retired by
AUDIT-C11 in favour of `cdc_async_fifo` (the production CIC→FIR
boundary CDC, see ddc_400m.v line 646). Wrapper rewritten with
FIFO-shaped equivalents of the original Gray-CDC properties:
P1 reset behaviour, P2 no spurious dst_valid, P3 overrun semantics,
P4 data integrity (cooldown-spaced, FIFO-equivalent of the
original single-element latch property),
P5 bounded liveness (depth 100 gclk),
P6 cover sequences for the basic write→read pipeline.
P4's true multi-in-flight FIFO order proof is left as Option B work;
for the AERIS-10 use case the upstream ddc_400m CIC→FIR consumer
operates below FIFO-fill rate by design, so the cooldown-spacing
assumption is a tight model.
Verification: full FPGA regression 41 / 0 / 0.
|
||
|
|
9c231d85db |
fix(fpga): PR-Z A6 — usb cfar dense bug end-to-end fix + e2e test
The PR-Z A6 e2e test (tb_e2e_dsp_to_host) exposed that the wire-format
cfar_dense map emitted by usb_data_interface_ft2232h was all-zero for
our deterministic single-target stimulus, even though cfar_ca's
in-flight outputs showed CONFIRMED at the expected cells (verified via
in-TB capture, E5/E6 PASS).
Deep instrumented debug (BRAM-WRITE, BRAM-READ, EGRESS-CAP probes)
revealed THREE independent bugs that combined to produce the all-zero
wire output. Each bug alone would have been visible; the way they
compounded made the symptom look like a single coarse failure.
Bug A — stale write address (radar_system_top.v):
usb_inst.range_bin_in/doppler_bin_in were tied to notched_*_bin
(= rx_*_bin = doppler_processor outputs). After doppler returns to
S_IDLE its `output reg`s hold their last-driven values (511, 47).
cfar_ca's CMP-phase emit (cycles ~520..73520 after frame_complete)
fires cfar_valid with detect_range/detect_doppler set to its own
per-cell scan counters, but those outputs were dangling — usb's
RMW saw the doppler stale (511, 47) and slammed every cfar write
to byte_addr {511, 47[5:2]} = bram[8187], past the 6144-byte wire
range entirely.
Fix: register cfar_detect_range/doppler in lockstep with the existing
rx_detect_valid/rx_detect_class registration block (clk_100m_buf
domain), then mux them into usb_inst.range_bin_in/doppler_bin_in on
rx_detect_valid. doppler-magnitude write path is unaffected because
doppler_valid and rx_detect_valid are mutually exclusive (BUFFER vs
CMP phases of cfar_ca).
Bug B — BRAM read pipeline lag (usb_data_interface_ft2232h.v):
The detect_rd_data <= detect_bram[detect_rd_addr] BRAM read port has
1-cycle latency. WR_DETECT_DATA's emit FSM advanced detect_rd_addr
and read detect_rd_data in the SAME edge — so cycle K read bram[K-2]
(the addr from cycle K-1's commit) instead of bram[K-1]. Result:
every cfar wire byte = bram[N-1] instead of bram[N], shifting the
entire 6144-byte detect section +1 byte = +4 doppler bins. Doppler
hides this naturally because its 2-byte-per-cell rhythm gives BRAM a
free settling cycle between addr-set and emit-read.
Fix: pre-load detect_rd_addr <= 1 and det_doppler_byte_idx <= 1 at
every WR_DETECT_DATA entry transition (HDR direct, RANGE direct,
DOPPLER → DETECT). BRAM produces bram[0] for the first emit cycle
(settled since reset because detect_rd_addr was 0 throughout the
preceding section) while the addr advance schedules bram[1] for the
second emit cycle — and from then on the FSM's natural advance
pattern keeps the pipeline aligned, including across the per-range
boundary (det_doppler_byte_idx == DET_BYTE_LAST_PER_RANGE).
Bug C — detect_clearing window overlaps cfar's first 4 columns:
detect_clearing fired 1 cycle after frame_complete and ran for 8192
clk cycles (1 byte/cycle). cfar_valid writes were gated on
`!detect_clearing` (line 512). cfar's CMP-phase emits start at
frame_complete + ~520 cycles and run for ~73000 cycles, so the
first ~7672 cycles (≈ 4 doppler columns) of cfar pulses were
silently dropped. Test stimulus lit (67, 2/3) for sub-frame 0, all
inside the clearing window → bytes lost. (67, 18/19) and (67, 34/35)
for SF1/SF2 fell after clearing → captured correctly. Visible as
one-byte mismatch (0x0A expected, 0x00 captured) at offset 49965
(= cfar byte 804 = range 67, doppler 0..3) once Bugs A and B were
fixed.
Fix: move detect_clearing trigger from "1 cycle after frame_complete"
to wr_done_pulse (USB-transfer-complete edge already CDC'd into clk
via the AUDIT-C12 wr_done_sync chain). Clearing now runs in the dead
zone after USB has finished reading frame N's BRAM, well before
frame N+1's cfar starts CMP (~480k cycles of margin at 178 fps).
First frame after reset relies on BRAM init=0 — added explicit
initial block under `ifdef SIMULATION so iverilog matches Vivado's
synthesis default.
Test infrastructure:
- tb/tb_e2e_dsp_to_host.v new — deterministic single-target stimulus
fed through the back-half of the radar pipeline (range_decim → MTI
→ doppler → DC-notch → cfar → registered sync → usb), 16 in-TB
asserts + bit-exact byte capture.
- tb/cosim/gen_e2e_stimulus.py / gen_e2e_expected.py new — Python
deterministic stim + bit-exact frame golden.
- tb/cosim/tb_e2e_dsp_to_host_parse.py new — parses captured frame
via radar_protocol, runs 12 strict-bit-equality checks plus 16
semantic checks (target == CONFIRMED, neighbors == NONE,
DC-notched bins == NONE, etc).
- run_regression.sh — A6 hookup + retired the two zero-assertion
radar_system_tb USB_MODE=0/1 smoke runs and the 3-liveness-only
tb_system_dataflow (subsumed by A6's stronger checks). Saves
~7 min wall.
Verification:
- Local iverilog: in-TB 16/16 PASS, parser strict 28/28 PASS.
- Remote Vivado 2025.2 xsim (Artix-7 target): in-TB 16/16 PASS,
parser strict 28/28 PASS.
- Full regression: 41 / 0 / 0.
The MODEL_USB_CFAR_BUG bug-model flag (used to keep the regression
green during development against buggy production) is removed — the
test is now strict bit-exact against the post-fix wire format.
|
||
|
|
ce869e9e20 |
docs(fpga): PR-X.2 F-7.3 — refresh tb_ad9484_xsim header + retire Group 4
Header (line 6, 11): "(IBUFDS, BUFG, IDDR)" → "(IBUFDS, BUFIO, BUFG, MMCME2_ADV)"; "IDDR Q2 (falling-edge) data capture in SAME_EDGE_PIPELINED mode" → "Falling-DCO-edge IOB-packed IFF capture (post-AUDIT-C4 SDR shape; no IDDR, no rise/fall demux)". $display banner: "(IBUFDS, BUFG, IDDR)" → "(IBUFDS, BUFIO, BUFG, MMCME2)". Group 8 inline comment: "captures Q2 of the IDDR (falling-edge…)" → "captures the IOB-packed IFF on the falling DCO edge". Test Group 4 retired. The block drove 0xAA on rising DCO and 0x55 on falling DCO. Post-AUDIT-C4 the IFF only samples the falling edge — every captured value was 0x55 — so the assertion `saw_aa > 0 || saw_55 > 0` was trivially true and `cap_count > 0` duplicates Group 5 / Group 8's stronger checks. Block replaced with a tombstone comment; group numbering preserved for git-blame continuity. Two "IDDR" references intentionally retained inside negations (line 12, line 186) — they explicitly contrast the current SDR topology against the broken pre-C-4 shape so a reader who finds the old vocabulary in git history understands what changed. Verification: remote Vivado 2025.2 xsim → 15 / 15 PASS (was 17 / 17; the lost 2 are Group 4's trivial-pass and existence checks, both now subsumed by Groups 5 / 8). |
||
|
|
d6462c6d5b |
docs(fpga): PR-X.2 F-7.2 — refresh adc_clk_mmcm.xdc comments to SDR IFF
Constraint values are unchanged — only the explanatory comments are
refreshed to match the post-AUDIT-C4 (2026-05-01) RTL.
- "IDDR outputs" → "IFF Q output"
- "adc_data_{rise,fall}_bufg" → "adc_data_iff_bufg"
- "the IDDR ~1.4ns before the clock" → "the IOB-packed IFF ~1.4ns
before the clock"
- hold-waiver header "BUFIO-clocked IDDR" → "BUFIO-clocked IFF, SDR"
- "BUFIO/IDDR domain" → "BUFIO/IFF domain"
The 3.000 ns set_max_delay (BUFIO ↔ clk_mmcm_out0), the
set_false_path -hold on adc_d_p[*]/adc_or_p, the LOCKED-pin
through-waiver, and the 0.150 ns set_clock_uncertainty all remain
bit-identical — the IFF capture has the exact same source-synchronous
timing argument as the prior IDDR did.
One reference to "IDDR" deliberately kept inside a negation
("no IDDR, no rise/fall demux") — explicitly contrasts the current
SDR topology with the broken pre-C-4 shape so a reader who finds the
old vocabulary in git history understands what changed.
|
||
|
|
f9e64c420b |
docs(fpga): PR-X.2 F-7.1 — delete stale MMCM integration recipe
adc_clk_mmcm_integration.md was a 164-line "Step 1: modify
ad9484_interface_400m.v / Replace the BUFG instantiation with
adc_clk_mmcm" walkthrough for an integration that is already
shipped in the production RTL.
ad9484_interface_400m.v lines 68-102 already document the BUFIO/BUFG/
MMCME2 topology inline ("MMCME2 jitter-cleaning wrapper replaces the
direct BUFG. The PLL feedback loop attenuates input jitter from ~50 ps
to ~20-30 ps..."). adc_clk_mmcm.v has the wrapper itself with its own
header. constraints/adc_clk_mmcm.xdc carries the timing rationale.
A future engineer reading the integration recipe could mistake the
already-integrated feature for unfinished work and try to redo the
"Step 1: modify" edits — actively harmful.
|
||
|
|
25e1f76841 |
fix(fpga): PR-X.1.b — close F-7.4 on real Vivado xsim
Bench-verify the F-7.4 MMCM-lock gating (commit
|
||
|
|
bf83d35917 |
test(fpga): PR-M.4 — redesign T-6 drift invariants for scaled-FFT chain
Three sub-checks in compare_independent.py were red because the test
inputs assumed an UNSCALED FFT but PR-O moved both the RTL xFFT path
and fpga_model.FFTEngine to LogiCORE v9.1 Scaled mode (one >>>1 per
butterfly stage with conv-rounding, total /N applied across LOG2N
stages). At small input amplitudes the per-bin output rounded to zero
and the test invariants no longer described meaningful behaviour.
The fpga_reference.py side already mirrors the scaled-mode convention
(np.fft.fft(x)/n on forward, ifft on inverse — see line 104, 137-138,
207). The fix is purely in the test inputs:
- FFT-2048(impulse): amp 1000 → 32000 (≈ Q15 max). Expected per-bin
is now round(amp/N) = round(32000/2048) = 16 (banker's). The
impulse has b=0 at every butterfly so there is no twiddle
interaction; banker's rounding keeps every bin within ±1 LSB.
Tightened tolerance from 5 to 2.
- MF peak position + MF peak-to-median: amp 200 → 4000. Chain
output peak under scaled-mode is correlation/N² ≈ pulse_len*amp²
/N² = amp²/16384 (256 / 4_194_304). At amp=200 the peak collapsed
to 2.4 (mostly Q15 quantization noise — argmax wandered to bin 2);
at amp=4000 the peak rises to ≈ 977 with sidelobes near LSB
floor. Peak-to-median ratio observed: 974 vs threshold 5.
Runtime verification: compare_independent.py 13/13 PASS standalone.
Full FPGA regression: 36/1/6 → 37/0/6 (the single FAIL was this test;
no other tests touched).
|
||
|
|
0100967eac |
fix(fpga): PR-X.1 F-7.7 — wire AD9484 OR sticky to shared clear pulse
Stage-7 ADC chain audit. radar_receiver_final.v's adc_overrange_sticky_400m had no clear path other than full system reset_n — once latched, the only way to acknowledge the AD9484 overrange flag was a reboot. The DDC's analogous diagnostic flags (cdc_cic_fir_overrun_sticky, mixer_saturation) already clear on the DDC's `reset_monitors` port; the OR sticky in the receiver wrapper was the lone outlier. Introduce a shared `clear_monitors_pulse` wire at the receiver scope and route it both to the OR sticky's clear branch and to the DDC's `reset_monitors` port (replacing the literal `1'b0`). Today the wire is tied 1'b0, preserving the prior reset_n-only behaviour bit- for-bit. When a future host opcode for "clear diagnostic stickies" lands, both the receiver-level OR sticky and the DDC's internal sticky flags clear from the same pulse — no per-flag re-plumbing. Local FPGA regression 36/1/6 unchanged (1 FAIL = pre-existing T-6 drift, deferred PR-M.4). |
||
|
|
e2cb7bd2d6 |
fix(fpga): PR-X.1 F-7.6 — correct offset-binary mid-scale conversion in DDC
Stage-7 ADC chain audit. ddc_400m.v line 273-274 converted AD9484
offset-binary 8-bit codes to MIXER_WIDTH-bit signed via
`(adc << 9) - (8'hFF << 8)`. The right operand evaluates to
`0xFF / 2 = 0x7F` (integer divide drops 0.5), so the formula
subtracted 65280 instead of the correct mid-scale offset 65536:
- code 0x80 (0V analog, mid-scale) → +256 (expected 0)
- code 0x00 (-fs) → -65280 (expected -65536)
- code 0xFF (+fs - 1 LSB) → +65280 (expected +65024)
A 0.5-LSB DC bias on the AD9484 stream is functionally invisible
in production: after the 120 MHz NCO mixer it lands at ±120 MHz,
which the CIC + FIR LPF (~50 MHz rolloff) rejects. But the offbin
and twoc paths disagreed on the same analog input, and any reuse
of `adc_signed_offbin` outside the mixer chain would carry the
bias.
Replace with an MSB-flip + sign-extend + 9-zero pad that mirrors
the twoc branch: `{~msb, ~msb, low7, 9'b0}`. Mid-scale code 0x80
now produces 0 exactly, matching the twoc path bit-for-bit.
Local FPGA regression 36/1/6 unchanged (1 FAIL = pre-existing
T-6 drift, deferred PR-M.4).
|
||
|
|
6738f12e54 |
test(fpga): PR-X.1 F-7.4 — gate tb_ad9484_xsim on MMCM lock (closes PR-N #86)
Stage-7 ADC chain audit. The MMCM SIMULATION model in adc_clk_mmcm.v takes 4096 DCO cycles (~10 µs at 400 MHz) to assert mmcm_locked. The existing TB waited only ~5 cycles after each reset deassert, so the gated reset_n_400m never released and adc_data_valid_400m stayed low for every test group past the initial reset checks. Expose mmcm_locked as a new module output on ad9484_interface_400m (real path) and ad9484_interface_400m_stub (sim path; tied high one DCO cycle after reset deassert since the stub has no MMCM). Connect it through to tb_ad9484_xsim.v and add a `wait_for_adc_ready` task that waits on the lock signal plus 6 DCO cycles for the 2-FF lock-sync, 2-FF reset-sync, and pipeline drain. Apply the task at each of the five reset cycles in the testbench. radar_receiver_final.v: tie the new port off (.mmcm_locked()) — host status pipeline doesn't surface lock yet, deferred for a future status-word widening. Local iverilog regression (36/0/7) clean. xsim verification of the xsim-only TB itself is pending (remote Vivado host). |
||
|
|
83cbc91d8b |
refactor(mcu): PR-W F-6.7 — privatize setADTR1107Mode
API hygiene. setADTR1107Mode flips ADTR1107 PA/LNA bias registers but does NOT touch the per-channel ADAR1000 RX/TX enable bits. Production always reaches it through setAllDevicesTXMode / setAllDevicesRXMode, which emit both halves. Leaving setADTR1107Mode public after F-6.1 removed the other public mode-switch wrappers invited a future caller to invoke it directly and end up in a mismatched bias-vs-enable state. Move the declaration to the private section with a short comment explaining why the wrappers are the only sanctioned entry point. |
||
|
|
e3bd885be9 |
fix(mcu): PR-W F-6.3 — clear opposite REG_MISC_ENABLES bit in setADTR1107Mode
Latent bit-mask hygiene gap. setADTR1107Mode(TX) was asserting BIAS_EN (bit 5) without first clearing LNA_BIAS_OUT_EN (bit 4); the RX branch mirrored the bug. On any TX→RX→TX (or symmetric) transition through this register both PA and LNA bias outputs would end up enabled simultaneously. Production today only ever calls one direction at boot and the opposite at shutdown — never both during normal operation — so the bug was unreachable, but a future per-chirp SPI mode switch would trip it. Now each branch resetBit's the opposite enable before asserting its own. 1 line per branch loop (not 1 per device — used the existing for-dev loop). |
||
|
|
f23b35b719 |
chore(mcu): PR-W F-6.1 — prune dead ADAR1000Manager surface
Stage-6 ADTR1107 audit cleanup. Delete 4 unused public methods plus their 2 internal helpers from ADAR1000_Manager.cpp/h. Production boot goes through main.cpp's C-style systemPowerUpSequence(), so the C++ ADAR1000Manager::powerUpSystem / powerDownSystem / switchToTXMode / switchToRXMode wrappers had zero call sites; the same was true of the setPABias / setLNABias helpers, only ever invoked from the dead switchTo* paths. -130 LOC, no behavioral change. kPaBiasRxSafe is intentionally KEPT — it is live-used inside setADTR1107Mode(RX) as the safe PA bias when transitioning to RX. |
||
|
|
40d352ee68 |
chore(sim): prune superseded antenna scripts — keep edge-fed-row v3 path
Drop 5 antenna sims that were either dead-end topologies or got superseded
by validated v3 designs. The kept set documents the path the project
actually took: edge-fed series-fed row on 0.508 mm RO4350B (drop-in
old-Gerber replacement, 100 MHz BW), with the single-element edge-fed
inset and probe-fed comparators kept as design-space reference points,
and production_beams_verify retained as the D-series audit artifact for
the ADAR1000 setBeamAngle() dead-code finding.
Removed:
Quartz_Waveguide.py
openems_quartz_slotted_wg_10p5GHz.py
Early waveguide explorations, abandoned when the project moved
to a planar patch array.
aperture_coupled_aeris10_v2.py
First openEMS attempt at the AERIS-10 design point. Achieved
60 MHz BW with residual reactance; superseded by the v3 paths
(probe-fed 180 MHz BW, edge-fed-inset 180 MHz BW, edge-fed
series-fed row 100 MHz BW) that landed shortly after.
array_factor_adar1000_aeris10.py
Pure-numpy ADAR1000 phase-quantization sim used to find that
setBeamAngle()'s 4-elem broadcast produces grating lobes and
that the function is dead in production (replaced by
initializeBeamMatrices + setCustomBeamPattern16). Findings
already actioned in commit
|
||
|
|
0728d931c4 |
chore(repo): PR-H — G-series close-out (regression infra + lint sweep)
Closeout pass for the G-series 3-ladder chirp + adaptive-escalation work.
Cleanup, watchdog/fallback, lint, full regression — final sign-off.
Cleanup + watchdog/fallback: already wired during earlier audit waves
(track watchdog in chirp_scheduler RP_DEF_TRACK_WATCHDOG_FRAMES, RESERVED
fallback in plfm_chirp_controller_v2, range-decim watchdog in
radar_system_top with gpio_dig7 surfacing, F-3.* MCU error path).
Verified — no residual TODO/FIXME in production RTL or MCU.
Regression infra: tb/cosim/compare_independent.py SKIP-detection bug —
importlib.util.find_spec("scipy.signal") raises ModuleNotFoundError when
the parent scipy package is itself absent (instead of returning None as
the surrounding logic assumed). Wrap in try/except so the regression
runner gets the intended rc=2 SKIP marker rather than a crash that masks
the rest of the script.
Lint sweep: ruff full-repo → 0 errors. Two changes:
- pyproject.toml broadens 5_Simulations/Antenna/**.py exemption from
just T20+ERA to the full set of script-ergonomics rules
(RUF001/002/003 Greek µ/λ/π/θ in physical-units strings, E501 long
matplotlib/numpy lines, RUF005/015/046, E70x one-line setup, B007
tuple-unpack loop vars, B905, BLE001 diag try/except, C401, RET504,
SIM118, PERF40x, ARG001, E402). These are sim/analysis scripts, not
production code — keep substantive bug rules (F unused, B core
bugbears) but drop stylistic noise.
- Auto-fix sweep: 31x F541 (f-string-no-placeholder), 3x F401 (unused
sys import), 2x F841 (dead leftover ref_pat / phases_quant in
array_factor_adar1000_aeris10.py).
.gitignore: cover 9_Firmware/9_2_FPGA/tb/cosim/mf_chain_autocorr.csv
(matched_filter cosim writes here now; was already covered for tb/ but
not tb/cosim/).
Regression baseline (radar_venv):
FPGA : 42/43 — 1 pre-existing T-6 drift cosim fail surfaced by the
SKIP fix above. Three sub-checks now red because PR-O moved
xFFT/MF chain to LogiCORE v9.1 *Scaled* mode (1/2 per stage,
1/2^11 total for N=2048) but compare_independent.py's invariants
(FFT-impulse uniform-spectrum, MF peak-at-injected-delay, MF
peak/median ≥ 5) were written assuming UNSCALED FFT. Not
introduced by this PR — was hidden by the SKIP-detection crash.
Defer to PR-M.4: redesign T-6 invariants (or input amplitudes)
to match scaled-mode arithmetic.
MCU : 34/34 binary suites pass.
GUI : test_v7 150/150 pass.
uv.lock: scipy resolution catch-up (declared in pyproject dev group all
along; lock just hadn't been refreshed after pyproject edits landed).
Bench-side checks: none — this PR is repo hygiene, no firmware/RTL
behaviour change.
|
||
|
|
b3edc7d359 |
test(v7): port live-vs-replay physical-units parity guard from develop
Adapts Serhii's TestLiveReplayPhysicalUnitsParity ( |
||
|
|
00d5d5f220 |
fix(mcu): PR-V — ADF4382A Stage-5 audit fixes (F-5.1..F-5.10)
F-5.1: revert PWM scaffolding to binary DELADJ. Schematic-verified: PG7/PG13 on STM32F746ZGT7 have no TIM3 alternate function (Port G AFs are FMC/ETH/USART6/SAI2/SDMMC2 — no TIMx routes), and the FreqSynth-board DELADJ net has only a 200 kOhm pulldown (R22, R35) — no series-R + shunt-C LPF for PWM-to-DC. The |
||
|
|
e1e5ae464a |
fix(mcu): F-4.3/4.4 (Option A) — AD9523 PLL1 bypass for first bring-up
The F-4.1+4.2+4.7 patch (
|
||
|
|
05472c1493 |
fix(mcu): F-4.5 + F-4.6 — AD9523 heap/lifecycle hygiene
F-4.5: ad9523_setup() malloc's both an ad9523_dev and a no_os SPI descriptor (ad9523.c:430,435). Previously the dev pointer was local to configure_ad9523() and fell out of scope on return — every recovery cycle (ERROR_AD9523_CLOCK → re-run configure_ad9523) leaked one struct + one SPI desc. STM32F7 heap is bounded; sustained brown-out flapping would eventually exhaust it. Move dev to a file- scope `g_ad9523_dev` and call ad9523_remove() at the top of configure_ad9523() to free the previous instance before re-setup. Initial boot path is unaffected (g_ad9523_dev=NULL → remove call gated by NULL check). F-4.6: ad9523_setup() called ad9523_calibrate() but discarded its return value (ad9523.c:707). VCO calibration can fail silently — if the target VCO is outside the 3.6-4.0 GHz band (e.g. F-4.1 wipe left PLL2 N=16, target 1.6 GHz), calibrate would report failure but setup still proceeded to ad9523_status(), where PLL2_LD might pass spuriously. Capture and propagate the calibrate return so a failed calibration aborts setup with a clear non-zero status code instead of being absorbed. Both fixes are mechanical and don't change correct-path behaviour. Regression: 86/0 (mocks bypass real driver, so F-4.6 is not covered by tests; F-4.5 changes are in main.cpp and don't trip mocked configure_ad9523). |
||
|
|
ddc0df464e |
fix(mcu): F-4.1+4.2+4.7 — AD9523 init order + M1 divider + channel math
Three coupled bugs in configure_ad9523() that together prevented the AD9523 from producing the labelled output frequencies: F-4.1: ad9523_init() unconditionally overwrites every field in the caller's pdata (vcxo_freq=0, pll1_bypass_en=1, pll2_ndiv_b_cnt=4, all channel fields). Calling it AFTER customization wiped every user value. Reorder: call ad9523_init() before the pdata.X = Y block; user overrides land on top of ADI defaults instead of being wiped. F-4.2: pll2_vco_diff_m1 / m2 are required (range 3..5 per datasheet) but were left at 0 from memset. The driver's AD_IFE() macro promotes m=0 to M_PWR_DOWN_EN, killing channels 4-9 (ADC, SYNC, FPGA system clock, DAC). Set m1=m2=3 explicitly. F-4.7: AD9523 has no VCO-direct path for OUT4-OUT9; channels source M1 or M2 only (datasheet + ad9523_vco_out_map register definitions confirmed). With VCO 3.6 GHz and m1=3, channel dividers see 1.2 GHz, not 3.6 GHz — every channel_divider in main.cpp was 3x too large. Updated values: OUT0/1 (ADF4382A REF, 300 MHz): /12 -> /4 OUT4/5 (ADC + FPGA_ADC, 400 MHz): /9 -> /3 OUT6 (FPGA SYSCLK, 100 MHz): /36 -> /12 OUT7 (FPGA TEST, 20 MHz): /180 -> /60 OUT8/9 (SYNC, 60 MHz): /60 -> /20 OUT10/11 (DAC, 120 MHz): /30 -> /10 m1=3 is the unique choice for this labelled frequency set (m1=4 fails OUT4, m1=5 fails OUT0/1). PLL1 (F-4.3/4.4) is not addressed here — pll1_bypass_en=0 with pll1_charge_pump_current_nA still 0 means PLL1 won't lock and status() will report it. Decide bypass strategy before bench. Test mocks (ad_driver_mock.c) bypass the real driver, so this is not caught by make. Regression: 86/0 (unchanged). Bench-verify OUT4=400MHz and OUT6=100MHz with scope before trusting downstream. F-1.10 (which crystal is fitted on X5/X6) goes in the same bench session — F-4.7 resolution shows 100 MHz VCXO is the only math-coherent choice regardless of BOM document. |
||
|
|
f0cff2cda7 |
docs(reports): replace AERIS_Antenna_Report.pdf with v2
v2 covers the new Stack_Hybrid stackup re-simulation: probe-fed v3 (180 MHz BW, drilled vias) and edge-fed row v3 (100 MHz BW, drop-in old-Gerber replacement) — both validated on 0.508 mm RO4350B, with 4-panel summary dashboards per variant + NF2FF / array-factor verification. reports.html updated to point all three filename references to the v2 PDF. |
||
|
|
acfbbb1d4d |
sim(antenna): probe_fed_array v3 — multi-port DRIVEN_PORTS env override
Previously the array sim only excited a single inner element (DRIVEN_X / DRIVEN_Y). Adds DRIVEN_PORTS env var accepting a comma/semicolon-separated list of "i,j" pairs that are all excited in-phase with equal amplitude — models a perfect 1:N corporate splitter feeding an N-patch sub-array. Example: DRIVEN_PORTS="0,0;1,0;2,0;3,0;0,1;1,1;2,1;3,1" excites a 4-cols × 2-rows sub-array anchored in the corner. S-parameter post-processing reframed for the multi-driven case: each excited port reports active-S11 (uf_ref/uf_inc with all driven ports active); each non-excited port reports S relative to a representative driven port's incident wave (all driven ports have equal amplitude so any reference works). Backwards-compatible: empty DRIVEN_PORTS reverts to single-port DRIVEN_X / DRIVEN_Y behaviour. |
||
|
|
416601d1d0 |
chore(tests): retire v1 cross-layer iverilog cosim tier
The v1-era tb_cross_layer_ft2232h.v cosim TB no longer matches production after the protocol-v2 / opcode dispatch rework (PR-G). Equivalent v2 coverage now lives in the FPGA regression's tb_usb_protocol_v2.v and tb_system_opcodes.v. Removed: - tb_cross_layer_ft2232h.v (716 lines) - Tier 2 (Verilog cosimulation) from test_cross_layer_contract.py - iverilog/vvp tool detection and CI install step in ci-tests.yml Tier 1 (static parser) and Tier 3 (C stub execution) remain. CI no longer needs apt-get install iverilog. contract_parser.py updated to reflect the slimmer two-tier model. |
||
|
|
b84aa6a6f3 |
fix(mcu): F-3.1 Error_Handler reset + audit cleanup tail
F-3.1 (functional): Error_Handler() now calls NVIC_SystemReset() instead
of __disable_irq(); while(1). Every MX_*_Init() helper invokes
Error_Handler before MX_IWDG_Init() runs, so an infinite spin would brick
the MCU on any transient boot-time glitch with no watchdog to recover.
SystemReset turns a hard-to-debug brick into a visible reboot loop.
F-3.3..F-3.8 (comment hygiene in main.cpp init helpers + post-init):
- TIM3 init: clarify 1 MHz tick @ 72 MHz timer clock (APB1=36 MHz but
RCC_TIMPRES_ACTIVATED forces TIMxCLK=HCLK)
- GPIO init: fix EN_P_3V3_ADAR12EN_P_3V3_VDD_SW_Pin → EN_P_3V3_VDD_SW_Pin
typo; correct PD8-11 → PD8-12 and PD12-15 → PD13-15 ranges
- SystemClock_Config: add VOS3 + 72 MHz intent comment
- MPU_Config: decode SubRegionDisable=0x87 bitmask
D1/D6/D7 (ADAR cleanup tail): code was already deleted in a prior pass;
this strips the residual tombstone comments per the no-tombstone feedback
policy.
- ADAR1000_Manager.h: 5 tombstone blocks removed (fastTXMode/etc,
setBeamAngle/4-phase/BeamConfig, setADTR1107Control, Configuration
section + setSwitchSettlingTime/setFastSwitchMode/setBeamDwellTime,
setTRSwitchPosition)
- ADAR1000_Manager.cpp: 6 tombstone comments removed; switchToRXMode
Step 4→3, Step 5→4 renumbered after Step-3 gap
- ADAR1000_AGC.cpp: stale "(matching the convention in setBeamAngle)"
reference removed
- main.cpp:556-557: redundant "setFastSwitchMode(true) call removed"
tombstone removed
D2 (comment-only): initializeBeamMatrices() and runRadarPulseSequence
descriptions rewritten to describe array-math peak (matrix1 → NEGATIVE
θ peak, matrix2 → POSITIVE θ peak) instead of the misleading "positive
phase difference" framing. Sky/ground sign vs antenna mount explicitly
flagged unverified — functional sign question remains hardware-blocked
pending calibrated-source bench test.
Regression: 86/0.
|
||
|
|
53f7d1e3ee |
chore(mcu): C-14a — delete dead ADF4382A EZSync surface
Production firmware never used SYNC_METHOD_EZSYNC — both callsites
(main.cpp:938 recovery, main.cpp:1955 boot) pass SYNC_METHOD_TIMED.
The original audit C-14 flagged TX/RX SPI skew in EZSync's trigger
sequence, but the path was dead from production; only test_bug3
referenced it for spy-harness regression coverage.
Removed:
- SYNC_METHOD_EZSYNC enum value
- ADF4382A_SetupEZSync function (and declaration)
- ADF4382A_TriggerEZSync function (and declaration)
- EZSync branch in ADF4382A_Manager_Init (collapsed to unconditional
SetupTimedSync call)
- test_bug3_timed_sync_noop.c Test C (EZSync regression coverage)
Production header and test shim header both cleaned. SyncMethod enum
kept as single-value to avoid touching the 7 other test callers that
pass SYNC_METHOD_TIMED.
Residual concern (separate from original C-14): ADF4382A_TriggerTimedSync
uses the same TX-then-RX sw_sync SPI sequencing pattern as the deleted
EZSync trigger. ~5 µs SPI gap between TX-armed and RX-armed means TX
and RX may capture different SYNCP/SYNCN edges (60 MHz cycle = 16.7 ns,
~300 edges in the gap). External SYNCP only provides simultaneity if
both devices are armed before a common edge. Hardware bench-test
required to confirm operational tolerance; cannot fix in firmware
without DMA SPI burst rewrite.
Regression: 86/0 (matches baseline).
|
||
|
|
38ee73a05c |
sim(antenna): verify production beam tables — setBeamAngle() is dead code
Audit of how the ADAR1000 is actually steered in production. Reproduces
main.cpp:initializeBeamMatrices() (PHASE_DIFFERENCES, matrix1, matrix2,
vector_0) verbatim in Python and runs the patterns through the same array-
factor pipeline used for the firmware-vs-correct comparison.
Key findings — context for fixing the setBeamAngle() bug without regression:
1) setBeamAngle() is DEAD CODE in production. grep for callers across the
whole tree returns only the definition itself (ADAR1000_Manager.cpp:215),
the header declaration (line 58), and one comment in ADAR1000_AGC.cpp
referencing the sign convention. main.cpp does NOT call it. The 4-phase
broadcast bug exposed in commit
|
||
|
|
2f4d45caa7 |
sim(antenna): nf2ff far-field + ADAR1000 array-factor verification
PART 1 — edge_fed_row_nf2ff_aeris10_v3.py:
Far-field analysis of the 1x8 series-fed row at 10.520 GHz to discriminate
broadside from scanned operation. Adds an nf2ff probe box to the verified
TX-centered design point (CONN_LEN=8.15) and computes the radiation pattern.
Result: E-plane (along array y) main lobe at θ=0.0°, BW3dB=14°
H-plane (perpendicular) main lobe at θ=0.0°, BW3dB=44°
First sidelobe -12 dB at ±18° (theoretical N=8 uniform: -13 dB)
→ BROADSIDE CONFIRMED at the operating mode.
openEMS NF2FF API notes baked into the script:
- CalcNF2FF expects theta/phi in DEGREES, converts internally.
- Prad/Dmax are sign-buggy in this version; pattern shape (E_norm) is
what we use for broadside identification.
- EndCriteria is 10·log10 energy ratio (so -40 dB → EndCriteria=1e-4).
PART 2 — array_factor_adar1000_aeris10.py:
Phased-array beam-forming verification using the ADAR1000 firmware's actual
phase-shifter codes. Combines the cached single-row embedded-element pattern
with a 16-element x-axis array factor at d=λ/2=14.286 mm pitch (matches
firmware's `element_spacing = wavelength/2` constant).
Replicates ADAR1000_Manager.cpp:714-729 (calculatePhaseSettings) and the
setBeamAngle() loop that broadcasts the 4-phase pattern to all 4 chips.
Compares against the correct 16-element progressive phasing.
CRITICAL FIRMWARE BUG SURFACED BY THE SIM:
setBeamAngle(angle) computes only 4 phases (one per chip channel), then
applies the same 4-phase pattern to all 4 chips. The intended 16-element
beam-steering never actually happens.
At cmd 15°, the firmware writes: [0, 17, 33, 50] × 4 = repeating pattern
Correct 16-elem progression: [0, 17, 33, 50, 66, 83, 99, 116, 5, 21,
38, 54, 71, 87, 104, 120]
Sim consequences (H-plane peak vs commanded):
cmd +0° → fw peaks +0° (correct) / correct +0°
cmd +5° → fw peaks +0° (NO STEERING) / correct -4°
cmd +10° → fw peaks +0° (NO STEERING) / correct -10°
cmd +15° → fw peaks +0° (NO STEERING) / correct -14°
cmd +20° → fw peaks -28° (locked grating) / correct -20°
cmd +30° → fw peaks -30° (coincidence) / correct -30°
cmd +45° → fw peaks -30° (locked) / correct -44°
Why: the same 4-elem pattern broadcast to 4 chips = effective super-pitch
d_super = 4d = 2λ, which generates grating lobes at sin(θ_g)=±0.5±sin(θ_0).
For |cmd|<18° those grating lobes coincide with the fixed broadside peak,
so the array radiates broadside regardless of commanded angle. For larger
commands the beam jumps to ±30° (the grating-lobe direction), not the
intended target.
The proper function setCustomBeamPattern16() (ADAR1000_Manager.cpp:636)
exists but isn't called by setBeamAngle(); fix is to either route through
it from a host-computed 16-element table, or extend calculatePhaseSettings
to produce 16 phases. Tracking under a separate beam-forming PR (not
antenna sim).
Other radar-engineer checks (all PASS for the antenna with proper phasing):
- Phase quantization: 7-bit (2.8125°/LSB) adds <0.2 dB SLL degradation
- Grating lobes: d=λ/2 → none in real space at any scan angle 0..90°
- Scan loss: matches cos(θ) ideal up to ~45°, then EEP roll-off dominates
- Beam pointing: sign-flipped cmd↔peak (cmd +X° peaks at -X°) — wiring
convention; either negate phase_shift in firmware or invert in host
- Null steering: phase-only synthesis places -30 dB null at θ=20° while
keeping main beam at broadside — confirmed feasible
|
||
|
|
abc7e3c66b |
sim(antenna): center 1x8 row dip on radar TX 10.520 GHz (CONN_LEN 8.0->8.15)
Sweep CONN_LEN at PROFILE=balanced to land the operating-mode dip on the chirp-band center instead of 60 MHz above it. Sensitivity is df/dCONN ≈ -0.20 GHz/mm (longer line → lower op freq). CONN=8.00 → dip 10.560 GHz, S11@10.520 = -12.6 dB (above TX) CONN=8.15 → dip 10.520 GHz, S11@10.520 = -18.8 dB (TX-centered) CONN=8.20 → dip 10.510 GHz, S11@10.510 = -18.4 dB (TX low edge) CONN=8.25 → dip 10.500 GHz, S11@10.500 = -18.4 dB (LO-centered) CONN_LEN=8.15 wins because the radar TX 10.510-10.530 GHz sees -17 to -19 dB symmetrically across the band, with -15 dB margin at the LO frequency. -10 dB BW spans 10.470-10.580 GHz (110 MHz). Pitch 15.10 mm vs old Gerber 15.01 mm. |
||
|
|
087a0563c0 |
sim(antenna): add 1x8 series-fed row — covers radar TX 10.51-10.53 GHz at -10 to -14 dB
Daisy-chain validation for 2-layer 0.508 mm RO4350B stackup. Row of 8 patches edge-connected via 8.0 mm microstrip segments (pitch 14.95 mm matches old Gerber). With direct edge feed on patch 0 (no inset; inset would drop the row input Z to ~6 Ω), the natural input impedance at the operating mode is ~80 Ω, close enough to 50 Ω for direct match without a quarter-wave transformer. Topology behaves as a finite periodic resonator with N=8 modes (~0.5 GHz spacing) and a stopband centered on the patch self-resonance. Operating point is the top-below-stopband mode at 10.56 GHz: S11 = -22.5 dB, Zin = 79.9 - j3 Ω. -10 dB BW spans 10.51-10.61 GHz (100 MHz), covering the 10.510-10.530 GHz chirp band with S11 = -10.4 to -14.6 dB across that interval. Mesh sensitivity: sanity profile (lambda/18) gave a misleading deepest-dip at 11.4 GHz; balanced (lambda/25) is required to land the operating-mode characterization correctly. PROFILE=balanced is now the documented run mode. |
||
|
|
178cb26abd |
sim(antenna): add edge-fed (inset) single-element on 0.508 mm RO4350B — 180 MHz BW
Validates the "Option C" hardware path: keep the old 8x16 Gerber's series-fed edge-fed topology, just thicken the patch substrate from 0.102 mm to 0.508 mm RO4350B. Single-element edge-fed with inset notch matched to 50 Ω microstrip. Verified at PROFILE=balanced (λ/25 mesh): W = 7.854 mm (preserved from old Gerber → array compatible) L = 6.95 mm (tuned for f_res = 10.5 GHz on 0.508 mm sub) inset_depth = 3.40 mm (~49 % of L) inset_gap = 0.30 mm (each side of feed line, in the inset notch) feed_W = 1.16 mm (50 Ω microstrip on 0.508 mm RO4350B) feed_lead = 15.5 mm (= 1·λ_g at 10.5 GHz → port sees true antenna Z) Result: f_res = 10.509 GHz, S11 @ 10.5 = -18.5 dB, VSWR = 1.27 Z @ 10.5 = 61.8 + j3.2 Ω -10 dB BW = 180 MHz (10.41-10.59 GHz, 1.71 %) This is identical BW to probe_fed_v3 — confirming BW is set by substrate thickness alone, not feed method. Edge-fed Option C is therefore the simpler 2-layer hardware path: same series-fed-row architecture as the old Gerber, single PCB stackup, no probe vias / antipads / back-board splitter complexity. Next step: extend to a 1x8 series-fed row to verify the daisy-chain topology still gives in-phase feeding at the new substrate's λ_g. |
||
|
|
eb9be337b1 |
sim(antenna): add 4x4 probe-fed array model — mutual coupling characterisation
Single-port-driven, 15-port-terminated FDTD sim built on the same v3 patch
design point (W=7.854, L=6.56, FEED_OFFSET=2.14) at the Gerber pitch
(14.27 mm X / 15.01 mm Y). Reads out S_jd for all elements when port
(DRIVEN_X, DRIVEN_Y) is excited. Configurable array size + driven port via
env vars; default = 4x4, drive inner element (1,1).
Result at PROFILE=balanced, drive (1,1):
Active S11 @ 10.5 GHz : -16.7 dB, Z = 45.7 + j13.5 Ω
Active -10 dB BW : 190 MHz (10.44-10.63 GHz; ≈ single-element 180)
Nearest-neighbour coupling:
-x edge neighbour (0,1): -18.3 dB (strongest — edge element loads less)
-y/+y in-array : -22 dB
+x interior neighbour : -24.7 dB
Diagonal : -32 dB
Far corners : -45 to -53 dB
Outputs: coupling_grid.png heatmap + S_matrix.csv (all S_jd full sweep) +
S11_data.csv (driven-port only).
Known limitation: port box must align with mesh lines; SmoothMeshLines may
sub-cell-shift seed lines, causing some DRIVEN_X/DRIVEN_Y choices to give
"Unused primitive" warning + zero excitation. Default (1,1) is verified;
other choices are best-effort. Documented inline.
|
||
|
|
3f3846b514 |
sim(antenna): add probe-fed 2-layer patch model — 180 MHz BW vs aperture-coupled 60 MHz
Single-element OpenEMS sim for a 2-layer probe-fed patch on 0.508 mm RO4350B. Verified at PROFILE=balanced (λ/25 mesh): f_res = 10.51 GHz, S11 = -21.8 dB, VSWR 1.18, -10 dB BW = 180 MHz (10.40-10.58 GHz). Direct 50 Ω match — no port matching cap needed. Design point baked into defaults: PATCH_W = 7.854 mm (preserved from old 8x16 Gerber → array compatible) PATCH_L = 6.56 mm (tuned for f_res = 10.5 GHz at 0.508 mm sub) FEED_OFFSET = 2.14 mm (probe via, from -y radiating edge) Why probe-fed: aperture-coupled v2 (4-layer Stack_Hybrid) capped at ~60 MHz BW because the 0.11 mm L4 acts as a near-short reflector — beneficial for slot coupling but creates an L2-L4 cavity that's the BW ceiling. Tested removing L4 (open-back aperture-coupled): coupling collapsed, R stayed >1000 Ω regardless of patch tuning. Probe-fed has no slot bottleneck; physics BW = 1.7% on h=0.508 matches sim 180 MHz directly. Hardware change required to deploy: 2-layer stackup (patch on top, ground on bottom, probe vias with antipads). Old 8x16 Gerber was edge-fed; v3 is probe- fed → top-layer feed network goes away, ADAR1000 carrier on a separate board with SMP RF launches. Stackup signoff with antenna designer needed before PCB. aperture_coupled_v2 retained as the 4-layer fallback (with the 0.043 pF cap) if 2-layer redesign isn't approved. |
||
|
|
e01c2ae424 |
sim(antenna): tune aperture-coupled v2 to design point — R matched, X residual documented
10-iteration analytic-tune sweep (no DE optimizer — that was a wrong
direction for this topology) on the Stack_Hybrid 4-layer stackup. Key
findings from the sweep are baked into the script defaults and header.
Best design point @ 10.5 GHz (now the env-var defaults):
patch W=9.55 mm L=7.77 mm
slot L=3.00 mm W=0.50 mm (centered under patch)
stub L=4.16 mm (= λ_g/4 in feed sub at f0)
feed lead=12.34 mm W=0.25 mm
total feed length = 1·λ_g at 10.5 GHz, line transparent at f0
Result: Z ≈ R + j350 Ω at 10.5 GHz, R = 33–51 across reruns (sanity-profile
convergence drift; X is stable). R is within matching range; the +j350
inductive residual is fundamental to the topology (L4 backshort under the
antenna footprint), not a tuning artifact. Two production-grade fixes:
(a) Series cap at port C ≈ 0.043 pF — drops S11 to ≈ -40 dB
(b) Open L4 backshort under antenna — restores standard open-back
aperture-coupled, stub naturally tunes X
Three real bugs fixed in the underlying sim (without these, the baseline
script was measuring artifacts, not the antenna):
1. Z mesh under-density. Default SmoothMeshLines at λ/18 gave the 0.11 mm
feed substrate ~0.1 cells vertically — microstrip Z0 was a pure mesh
artifact. Now built explicitly with ≥5 substrate-interior lines per
dielectric (bypassing SmoothMeshLines collapse for z-axis only). Fine
substrate cells visible via MESH_DEBUG=1.
2. SLOT_Y_OFF_MM env var was read but never applied to the L2 slot box,
silently making the parameter inert. Slot is now correctly offset in y.
3. FEED_LEAD_L was hardcoded at 14.0 mm, making total feed length 18.16 mm
= exactly 1·λ_g at 9.5 GHz (in feed substrate). This created a parasitic
feed-line full-wave standing wave in-band that masked the patch as a
persistent "9.4 GHz resonance" regardless of patch dimensions. Now
parameterized via FEED_LEAD_L_MM with default 12.34 mm so total feed =
1·λ_g at 10.5 GHz instead — line is transparent at the operating freq
and sim measures the actual antenna impedance at the port.
Sweep grids updated to span ±1 step around the iter #6 design point.
|
||
|
|
42056b8331 |
sim(antenna): add OpenEMS aperture-coupled patch model for Stack_Hybrid v2
Re-simulation infrastructure for the new 4-layer aperture-coupled antenna
stackup (Stack_Hybrid.png, committed
|
||
|
|
e5d98533ca | Merge remote-tracking branch 'origin/main' into feat/dual-range-v2 | ||
|
|
ef32345b26 |
feat(rtl,gui): PR-U / M-8 — sub-frame enable mask routed end-to-end (C-5 hardening)
The chirp_scheduler had a 3-bit host_subframe_enable input {LONG, MEDIUM, SHORT}
that was tied to the constant RP_DEF_SUBFRAME_ENABLE at the receiver instance,
so the host could neither change it nor know what mask was active. With the
mask not at 3'b111 the scheduler skips a sub-frame at TX but doppler_processor
still writes 48 chirp slots, so the host CRT (`dbin // 16 → {SHORT, MED, LONG}`)
silently mis-attributes the SF axis and unfolds to the wrong velocity.
Plumb the mask through:
- radar_system_top.v: new reg [2:0] host_subframe_enable, cold-reset
RP_DEF_SUBFRAME_ENABLE, opcode 0x19 setter, wired to rx_inst and usb_inst.
- radar_receiver_final.v: new host_subframe_enable[2:0] input port; the
chirp_scheduler instance is untied from the constant.
- usb_data_interface_ft2232h.v: new subframe_enable[2:0] input + per-frame
snapshot reg latched at frame_complete (stable for ft_clk read, same
pattern as stream_flags_snapshot). Byte 2 emission is now
{2'b00, subframe_enable[2:0], stream_flags[2:0]} — was {5'b00000, stream}.
- radar_protocol.py: Opcode.SUBFRAME_ENABLE = 0x19; RadarFrame.subframe_enable
field; parse_bulk_frame surfaces bits[5:3]; reserved-mask 0xF8 → 0xC0.
Bulk-frame mock encodes the mask in its emit so dashboard replay is correct.
- v7/processing.py: extract_targets_from_frame_crt forces every target to
AMBIGUOUS when frame.subframe_enable != 0b111. Operator sees the red `?`
flag in the targets table instead of a silently-wrong velocity.
- v7/software_fpga.py + v7/dashboard.py: subframe_enable mirror + setter, and
replay dispatch routes 0x19 to set_subframe_enable.
Tests (test_v7.py): TestSubframeEnableRoundTrip (4), TestSoftwareFpgaSubframeEnable
(2), TestCrtSubframeMaskGating (3), 0x19 added to TestOpcodeEnumFillIn and
TestReplayOpcodeDispatch. Existing test_full_frame_round_trip updated to expect
byte 2 = 0x3F (mask 0b111 default + stream 0x07).
Cosim TBs (tb/tb_usb_protocol_v2.v, tb/tb_ft2232h_frame_drop.v) drive the new
input with 3'b111 and assert the new byte-2 layout (T2.3: 0x00 → 0x38).
Regression: test_v7 146/146, test_GUI_V65_Tk 117/117, ruff clean.
iverilog: tb_usb_protocol_v2 27/27 PASS, tb_ft2232h_frame_drop 10/10 PASS.
|
||
|
|
8ebb7016de |
chore(repo): PR-S — m-1..m-9 hygiene sweep (audit cleanup)
Bundled minor-tier fixes from project_aeris10_audit_2026-05-02. No
behavioural changes to the production happy path; mostly stale comments,
defaults, and one new emit-path (m-9) that lets cosim_dir replay show
detections instead of an empty mask.
m-1 — processing.py:59 RadarProcessor.range_doppler_map placeholder
shape (1024, 32) -> (NUM_RANGE_BINS, NUM_DOPPLER_BINS) imported
from radar_protocol so the legacy literal stops leaking to
anything reading the attribute before frame 0.
m-2 — radar_receiver_final.v:596 stale "// 32" comment for
RP_CHIRPS_PER_FRAME -> "// 48 (PR-F: 3 sub-frames * 16)".
m-4 — radar_protocol.py "16384 x 2 = 32768" arithmetic comment was
already corrected by an earlier edit; verified clean.
m-5 — usb_data_interface_ft2232h.v:961 "Frame header: 8 bytes"
comment -> "9 bytes (PR-G: added version byte at offset 1)".
m-6 — radar_system_top.v cold-reset host_chirps_per_elev 32 -> 48
+ status doc-comment so any sanity-checking parser sees the
value matching RP_CHIRPS_PER_FRAME instead of latching a
chirps_mismatch_error.
m-7 — radar_receiver_final.v:370 RX DDC mixers_enable(1'b1)
annotated: documented as intentional asymmetry vs TX (counter-
UAS RX has no quiesce scenario; CDC would add cost without
operational benefit).
m-8 — RadarSettings range_resolution / velocity_resolution flagged
inline as PLACEHOLDER (docstring already explains; inline
marker makes it visible at the field).
m-9 — gen_realdata_hex.py now also emits fullchain_cfar_flags.npy
(uint8 detection mask) and fullchain_cfar_mag.npy (|I|+|Q|),
produced by run_cfar_ca() with the FPGA cold-reset defaults
(guard=2 train=8 alpha=0x30 mode=CA). Replays through
v7.replay's COSIM_DIR loader: 22 detections on the synthetic
scene (was 0). The hex/ directory's two new .npy files are
included in this commit.
Regression: 247/247 (test_v7 130 + test_GUI_V65_Tk 117). Ruff clean.
|
||
|
|
c2637251b0 |
feat(gui): PR-R — host control surface fill-in (audit M-2/M-3/M-4/M-6/M-7)
The RTL has been ahead of the host opcode/widget surface since PR-G:
several runtime knobs (MEDIUM PRI, soft-CFAR alpha, ADC power-down) are
fully wired in radar_system_top.v but had no enum / spinbox path, so
the operator could only reach them via raw _send_custom_command. This
PR closes the gap for everything except M-5 (status-packet medium PRI
readback, which needs an RTL change to add a status word).
M-2 — Opcode enum gains MEDIUM_CHIRP=0x17, MEDIUM_LISTEN=0x18,
CFAR_ALPHA_SOFT=0x2D. Truth-table docstring refreshed.
Two new spinboxes in Waveform Timing ("Medium Chirp Cycles",
"Medium Listen Cycles") with the V2 defaults 500 / 15600 (5 us
chirp, 161 us PRI). One new spinbox in Detection (CFAR)
("CFAR Alpha Soft (Q4.4)") with the RP_DEF_CFAR_ALPHA_SOFT=0x18
default.
M-3 — ADC_PWDN=0x32 added to the enum (was previously commented as
"reserved for S-25"; the fix landed at radar_system_top.v:1152
routing to the physical adc_pwdn pin). New "ADC (AD9484)"
group on the right column with two buttons: ADC Normal (0x32=0)
and ADC Power Down (0x32=1). Buttons rather than a spinbox
prevent accidental non-{0,1} values.
M-4 — ADC_FORMAT widget added to the same ADC group: a 2-choice combo
("Offset-binary (SJ1 1-2)" vs "Two's-complement (SJ1 2-3)") with
a Set button, since AD9484 SPI is tied off (CSB high) and the
only way to flip sign convention is via this opcode.
M-6 — Replay opcode dispatch in _dispatch_to_software_fpga() expanded:
SoftwareFPGA gains cfar_alpha_soft mirror + setter; 0x2D wired
through. RTL-only opcodes (chirp timing, range mode, ADC strap,
self-test, status_request) are no longer silently dropped — they
log at info-level "acknowledged (no effect on replay — RTL-only
state)" so the operator gets visible feedback.
M-7 — Chirps Per Elevation widget default 32 -> 48; hint changed from
"1-32, clamped" to "must be 48 (RTL clamps)". RTL latches
chirps_mismatch_error in status word 4 bit 10 for any value != 48
since PR-F. Bonus: SHORT defaults bumped 50/17450 -> 100/17400 to
match RP_DEF_SHORT_*_CYCLES_V2 (PR-E 1-us SHORT chirp width).
Tests: +10 (TestOpcodeEnumFillIn 5, TestSoftwareFpgaCfarAlphaSoft 2,
TestReplayOpcodeDispatch 3). 247/247 PASS. Ruff clean.
M-5 (status packet medium_chirp/medium_listen readback) deferred —
needs an RTL change to extend status_words from 7 to 8 (current word 3
has only 10 reserved bits, not enough for two 16-bit fields).
|
||
|
|
115c5f0778 |
feat(gui): M-1 / PR-Q.7 — dashboard CRT confidence column + alias-fold tooltip (C-5)
The CRT extractor (PR-Q.5/PR-Q.6) tags every target with a velocity_confidence
("CONFIRMED" / "LIKELY" / "AMBIGUOUS" / "UNKNOWN") and an optional alias_set
of candidate v_true folds. Until now the operator-facing targets table on the
Main View tab dropped that signal, so a single-PRI-only AMBIGUOUS reading
looked identical to a 3-PRI CONFIRMED one.
Changes:
- Targets table column count 5 -> 6; new "Confidence" column between
Velocity and Magnitude.
- Module helper _confidence_display(label) -> (text, QColor):
CONFIRMED green (DARK_SUCCESS)
LIKELY amber (DARK_WARNING)
AMBIGUOUS red (DARK_ERROR), prefixed with "? " so the row stands
out even when the operator's eyes skip the colour.
UNKNOWN gray (DARK_TEXT) — legacy 32-bin / no CRT.
Unrecognised future labels fall through to UNKNOWN.
- Velocity cell carries a tooltip listing the CRT alias_set folds when
present, so hovering reveals all plausible v_true candidates.
- QColor pulled in from PyQt6.QtGui for the foreground tint.
Tests (TestDashboardConfidenceDisplay, +5):
- CONFIRMED/LIKELY/AMBIGUOUS/UNKNOWN each map to expected text + colour.
- AMBIGUOUS leads with "?" so it's visible without colour.
- Unrecognised label "BANANA" falls back to UNKNOWN/gray.
Regression: 237/237 (test_v7 120 + test_GUI_V65_Tk 117). Ruff clean.
This closes audit M-1 / task PR-Q.7. The C-5 thread is end-to-end functional:
RTL emits 3 sub-frames (PR-Q.1) -> cosim agrees (PR-Q.2) -> v7 models carry
per-subframe params (PR-Q.4) -> processing.py runs CRT (PR-Q.5) -> workers
route through it (PR-Q.6) -> dashboard surfaces the confidence (PR-Q.7).
|
||
|
|
3401d05eca |
fix(gui): P-6 / PR-Q.6 — workers route detections through CRT extractor (C-5)
Both live and replay paths used the legacy single-PRI extractor with the
LONG-PRI v_res placeholder, which yielded wrong velocities for the SHORT
and MEDIUM sub-frames. PR-Q.5 already provided extract_targets_from_frame_crt
(48-bin, 3-PRI Chinese-Remainder-Theorem unfolding) — this PR wires it in.
Changes:
- workers.py imports extract_targets_from_frame_crt at module scope.
- RadarDataWorker._run_host_dsp delegates to the CRT extractor and then
applies GPS pitch correction + DBSCAN clustering + Kalman tracking
on the returned targets. Inline det_indices loop and
velocity_resolution_long_mps placeholder removed.
- ReplayWorker.__init__ binds _extract_targets to the CRT extractor;
_emit_frame call simplifies to (frame, waveform, gps=).
- 32-bin legacy recordings still work via the CRT extractor's internal
fallback to extract_targets_from_frame.
- Module docstring stale "(64x32)" -> "(512x48)".
- Dropped unused `import numpy as np` from workers.py (no remaining users).
Tests (TestWorkersRouteThroughCrt, +4):
- 3-PRI detection produces CONFIRMED + alias_set (was UNKNOWN before).
- GPS pitch correction applied post-CRT to elevation.
- Both clustering+tracking off → returns [] (no DSP work).
- ReplayWorker._extract_targets is exactly the CRT function reference.
Regression: 232/232 (test_v7 115 + test_GUI_V65_Tk 117). Ruff clean.
Closes audit P-6 / task PR-Q.6 — C-5 host wiring complete (PR-Q.7 dashboard
display column is the remaining piece).
|
||
|
|
b505266f33 |
fix(mcu): P-5 — align radar params with PR-F/PR-Q.1; document mode-01 production stance
main.cpp pre-PR-F constants caused two issues:
- m_max = 32 disagreed with RP_CHIRPS_PER_FRAME = 48 (3 sub-frames * 16);
getStatusString reported "32 chirps/position" to the GUI, false telemetry.
- PRI MEDIUM = 161 us (PR-Q.1 stagger) was missing entirely; the MCU only
knew SHORT=175 / LONG=167. T2 was also stuck at the pre-PR-E 0.5 us
SHORT chirp width; PR-E switched to 1.0 us.
Fixes:
- m_max 32 -> 48; T2 0.5 -> 1.0; new T_MEDIUM=5.0, PRI_MEDIUM=161.0 constants.
- Big doc-comment above runRadarPulseSequence states the production stance:
FPGA cold-resets to mode 2'b01 (auto-scan) so the MCU's chirp GPIO toggles
are no-ops; pass-through mode 2'b00 needs a 3-PRI loop the MCU does not
yet emit, so mode-00 is operationally unsupported until that's built.
- Removed the redundant /* */ block-comment shadow of the same constants
that had `T2` defined twice (typo for `PRI2`); pure dead-code cleanup.
- test_bug16_runradar_shadows_globals.c m_max 32 -> 48 with refreshed
arithmetic comment; binary still PASSes all 4 checks (g_m wraps to 1
each iter regardless of m_max value).
No GPIO timing change (would need hardware verification). Audit P-5 closes
with the documented mode-01 stance; rebuilding the loop for mode-00 stays
on the backlog if/when pass-through becomes a deployment requirement.
|
||
|
|
8004c59674 |
fix(gui): P-4 — dashboard NUM_RANGE_BINS 64 → 512 (import from radar_protocol)
dashboard.py:77 had a stale `NUM_RANGE_BINS = 64` literal from pre-PR-F. Range-Doppler canvas was mis-sized: parser at radar_protocol.py:425 already enforces 512, so production frames would never have rendered correctly even with P-2/P-3 in place. Fix: drop the local literals; re-export NUM_RANGE_BINS / NUM_DOPPLER_BINS through v7/hardware.py (which already re-exports the rest of radar_protocol) and import them in dashboard.py. Single source of truth. Also fixed two stale "(64x32)" docstrings: module-header tab description and `_on_frame_ready` docstring. Regression: 228/228 (test_v7 111 + test_GUI_V65_Tk 117). Ruff clean. |
||
|
|
9fbb7150b0 |
fix(gui): P-2/P-3 — bulk-frame parser + status packet caught up to PR-G v2
Audit P-2 and P-3 (2026-05-02): GUI radar_protocol.py was still on the pre-PR-G wire format. Production frames were rejected 100% before they reached the dashboard. Bulk frame (P-2): - BULK_FRAME_HEADER_SIZE 8 -> 9 (FPGA emits byte[1] = RP_USB_PROTOCOL_VERSION = 0x02). All field offsets shift by 1 (frame_num at +3,+4; n_range at +5,+6; n_doppler at +7,+8). Parser now validates the version byte. - Detect packing: 1 bit/cell (np.unpackbits) -> 2 bits/cell, 4 cells per byte MSB-first per PR-F. BULK_DETECT_DENSE_BYTES 3072 -> 6144 (= 512 * ceil(48*2/8)). New _unpack_detect_2bit returns uint8 codes 0..3 (NONE/CAND/CONFIRM/RSVD) instead of a 0/1 bitmap. - Reserved-bit mask 0xC0 -> 0xF8 (only low 3 stream-enable bits valid; bits 3-7 reserved). Drop dead BULK_FLAG_MAG_ONLY/SPARSE_DET constants and the rejection logic gated on them — the FPGA emit path always emits mag-only / dense, so flag-driven variants were never on the wire. - find_bulk_frame_boundaries: 9-byte minimum, validate version, bin counts at +5,+6 and +7,+8. - _mock_read updated to emit v2 frames so FT2232HConnection(mock=True) produces parseable data for tests and replay. Status (P-3): - STATUS_PACKET_SIZE 26 -> 30 (PR-G adds status_words[6] for 2-tier CFAR telemetry: detect_count_cand[31:16] + detect_threshold_soft[15:0]). StatusResponse gains detect_count_cand, detect_threshold_soft, and frame_drop_count fields. Bonus: m-3 fixed in passing — Opcode docstring line refs were stale (902-944 -> current ranges), now also documents 0x17/0x18/0x2D/0x32 as "M-2/M-3 — no enum yet" so a reader knows what's wired but unreachable. RadarFrame docstring "(64 range x 32 Doppler)" -> production dims. Tests: - TestBulkFrameV2RoundTrip (5 cases) — synthetic v2 frame round-trip, version-byte rejection, reserved-bit rejection, 2-bit code decode, back-to-back boundary scan. - TestStatusPacketV2RoundTrip (4 cases) — 30-byte size, word[6] decode, short-packet rejection, legacy-26B packet rejection. - test_GUI_V65_Tk: _make_status_packet emits 30 B w/ word[6]; _build_bulk_frame emits v2 w/ version byte + 2-bit detect packing. Pre-PR-G assertions on MAG_ONLY/SPARSE_DET dropped; new test_reject_wrong_version_byte + test_parse_status_word6_2tier_cfar. Test result: test_v7 111/111 + test_GUI_V65_Tk 117/117 = 228/228 PASS in radar_venv. Ruff clean. |
||
|
|
fcbf243aba |
fix(gui): P-1 — RadarDataWorker __init__ initialises runtime attrs
Audit P-1 (2026-05-02): _frame_queue, _acquisition, and frame counters were stranded inside set_waveform() due to indentation drift. The dashboard constructs RadarDataWorker and calls .start() directly without ever calling set_waveform, so live FT2232H acquisition crashes with AttributeError on first frame access in run(). Move the init block back into __init__; set_waveform now only sets self._waveform. Add TestRadarDataWorkerInit covering both: - attrs present after bare __init__ (no set_waveform required) - set_waveform does not reset runtime counters Test result: test_v7 102/102 PASS in radar_venv (was 100/100 + 2 new). |
||
|
|
3d2ffc3f2c |
chore(repo): cosim_dir replay revival + ruff lint cleanup
cosim_dir revival:
- gen_realdata_hex.py: also emit decimated_range_{i,q}.npy (48x512)
and doppler_map_{i,q}.npy (512x48) at production dimensions; the
same Python pipeline that produces the RTL .hex stimuli now writes
the .npy intermediates v7.replay COSIM_DIR loads. Replaces the
workflow lost when golden_reference.py was deleted in
|
||
|
|
5a7e8b8689 |
feat(gui): PR-Q.5 — 3-PRI CRT Doppler unfolder + cluster extractor (C-5)
Add host-side 3-PRI Chinese-Remainder velocity unfolding and a cluster extractor that reads the 48-bin Doppler frame, splits it into the 3 sub-frames (SHORT/MEDIUM/LONG), and resolves Doppler aliases across coprime PRIs. Resolves the algorithm half of audit C-5; the data is now in extract_targets_from_frame_crt's hands but workers still call the legacy single-PRI extractor (PR-Q.6 wires it). v7/processing.py: - unfold_velocity_crt(v_meas, v_unamb, v_res, max_alias_k=6, tol_factor=0.5) -> (v_est, confidence, alias_set). Brute-force candidate search over PRI-0 fold depth, per-PRI half-bin tolerance. Confidence: CONFIRMED (3-PRI unique), LIKELY (3-PRI with 2 cands, or 2-PRI with unique cand), AMBIGUOUS (1-PRI, 3+ cands, 2-PRI multi-cand, or no fold within tol). - extract_targets_from_frame_crt(frame, waveform, gps, max_alias_k): groups detections by range bin, picks strongest bin per (rbin, sf), decodes signed Doppler via sub_frame = dbin // 16 / bin_in_sf = dbin % 16, calls unfold_velocity_crt, attaches velocity_confidence and alias_set to RadarTarget. Falls back to legacy extract_targets_from_frame for non-48-bin frames. v7/models.py: - RadarTarget gains velocity_confidence (str default "UNKNOWN") and alias_set (list[float] | None). v7/__init__.py: - Re-exports unfold_velocity_crt + extract_targets_from_frame_crt. test_v7.py (16 new tests, 0 failures): - TestUnfoldVelocityCRT (8): zero-velocity CONFIRMED, below per-PRI v_unamb CONFIRMED, above per-PRI (100 m/s) CONFIRMED, near CRT ceiling (~261 m/s) CONFIRMED, negative velocity, 1-PRI AMBIGUOUS, 2-PRI LIKELY, inconsistent measurements AMBIGUOUS+fallback. - TestExtractTargetsFromFrameCrt (8): 3-PRI CONFIRMED target, LONG-only AMBIGUOUS (the 20-km blindspot regime), 2-PRI LIKELY, strongest-bin picking, two targets at distinct ranges, legacy 32-bin frame fallback, no-detections empty, GPS georef. Local: test_v7 100/0/0 (9 graceful skips), test_GUI_V65_Tk 117/0/2. |
||
|
|
54627bbbe3 |
fix(gui): software_fpga revival post-e8b495c — port chain helpers to fpga_model
Restore SoftwareFPGA's process_chirps() pipeline by porting the missing
chain stages (MTI canceller, DC notch, CFAR, threshold detection) plus
thin wrappers (range FFT, decimator, Doppler FFT) to fpga_model.py and
swapping software_fpga.py's import target from the deleted
golden_reference.py to fpga_model.
History: golden_reference.py was deleted in
|
||
|
|
71afa96d68 |
fix(gui): PR-Q.4 — per-subframe WaveformConfig + 48-bin parser (C-5)
Refactor v7.WaveformConfig from single-PRI to PR-Q's 3-PRI staggered
ladder (SHORT 175 us / MEDIUM 161 us / LONG 167 us) and update the
host-side bulk-frame parser dimension to match the FPGA's 48-bin
Doppler output (RP_NUM_DOPPLER_BINS = 48). The parser was rejecting
every production frame with n_doppler != 32, masking the PR-F widening
end-to-end.
WaveformConfig:
- pri_short_s/pri_medium_s/pri_long_s replace single pri_s
- n_doppler_bins 32 -> 48; new num_subframes=3
- Per-subframe velocity_resolution_{short,medium,long}_mps
- Per-subframe max_velocity_{short,medium,long}_mps
- extended_max_velocity_mps_crt(K=6) for 3-PRI alias-resolution ceiling
- Drop pri_s, velocity_resolution_mps, max_velocity_mps (no aliases)
Other:
- radar_protocol.NUM_DOPPLER_BINS 32 -> 48 (NUM_CELLS auto 16384 -> 24576;
BULK_FRAME_MAX_SIZE flows from NUM_CELLS, no other edits needed)
- v7/dashboard.py constant + stale "(64x32)" title replaced with f-string
- v7/processing.py 32-bin fallback -> 48
- v7/workers.py: derive doppler_center from frame.shape; LONG-PRI v_res
used as conservative single-PRI placeholder until PR-Q.5 lands the
CRT extractor (markers in place at both call sites)
- test_v7.py: TestWaveformConfig rewritten (8 tests, per-subframe + CRT
extension); TestExtractTargetsFromFrame center 16 -> 24
Local tests:
TestWaveformConfig 8/8 PASS
TestExtractTargetsFromFrame 6/6 PASS
test_GUI_V65_Tk 117/0/2 PASS
|
||
|
|
7ed4d5d405 |
test(fpga): PR-Q.2 — align cosim T_PRI_MEDIUM 175->161 us + regen goldens
Mirror the PR-Q.1 PRI stagger (MEDIUM 175 us -> 161 us) into the cosim scenario generator and regenerate all 12 affected golden hex/csv files. Without this, the Doppler co-sim TBs would diverge from the RTL on every MEDIUM sub-frame bin. - tb/cosim/radar_scene.py: T_PRI_MEDIUM = 161e-6 - tb/cosim/gen_doppler_golden.py: comment update for MEDIUM bin map - 12 regenerated hex/csv files (doppler + real_data + fullchain_realdata) Regression: 42/0/1 (PR-Q.1 baseline preserved; T-6 SKIP is scipy-missing). |
||
|
|
049f7b5d14 |
fix(fpga): PR-Q.1 — stagger MEDIUM PRI 175→161 µs for 3-PRI Doppler CRT (C-5)
Bumps RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600 so MEDIUM PRI = 161 µs, distinct from SHORT (175 µs) and LONG (167 µs). Three coprime PRIs let the host run 3-PRI Chinese-Remainder unfolding on Doppler aliases beyond the per-sub-frame ±~41 m/s unambiguous range — closes the FPGA half of audit C-5 (PR-F Doppler ambiguity unfolding). Stagger choice (proposal B): SHORT 175 µs — chirp 1 + listen 174 MEDIUM 161 µs — chirp 5 + listen 156 (PR-Q, was 175) LONG 167 µs — chirp 30 + listen 137 In 3 km mode LONG is blind (4500 m blind zone) → SHORT-vs-MEDIUM (Δ=14 µs / 8 %) is the operative pair; in 20 km mode MEDIUM-vs-LONG (Δ=6 µs / 4 %) carries the long-range slice that has SNR for both. Listens picked to differ by ≥5 % so the alias resolver is robust against the 5.1 m/s/bin Doppler quantization. Architecture is unchanged — chirp_scheduler.v already takes per-waveform host_*_listen_cycles. doppler_processor.v / cfar_ca.v are PRI-agnostic and just tag Doppler outputs with sub_frame ID; host-side CRT lives in v7/processing.py (PR-Q.5, follow-on). Files: radar_params.vh:240 RP_DEF_MEDIUM_LISTEN_CYCLES 17000 → 15600 radar_params.vh:217-228 block comment: stagger rationale + Δ math radar_system_top.v:273 port-list comment: default 17000 → 15600 radar_system_top.v:278-282 staggered-PRI block comment: 3-ladder PRI doppler_processor.v:25-30 reference v7/processing.py CRT unfolder tb/tb_radar_receiver_final.v:199-202 list MEDIUM=15600 in real-values Validation: full iverilog regression 42 PASS / 0 FAIL / 1 SKIP (pre- existing scipy availability) — same baseline as post-PR-O.8. No TB default-value asserts touched (tb_system_opcodes / tb_usb_protocol_v2 both use literal 16500 for opcode 0x18 round-trip). Follow-on: PR-Q.2 (cosim T_PRI_MEDIUM align + golden regen), PR-Q.4-7 (v7 GUI 3-PRI CRT unfolder + AMBIGUOUS confidence display), PR-Q.8 (memory close-out). MCU executeChirpSequence is live but PRI-agnostic in production mode 2'b01 (FPGA auto-scan) — pre-existing 2-ladder staleness vs chirp-v2 3-ladder, defer to PR-H or dedicated MCU PR. |
||
|
|
8f51646a2e |
test(fpga): xsim runner for tb_matched_filter_processing_chain
Compiles + runs the MF chain TB under Vivado XSim with FFT_USE_XILINX_IP
defined, exercising matched_filter_processing_chain →
fft_engine_axi_bridge → xfft_2048 → real LogiCORE FFT v9.1 IP.
Symlinks tb/ into the work dir so $readmemh("tb/mf_golden_*.hex")
resolves from xsim's CWD.
This validates the chain glue (FSM, BRAMs, conj-mult, sat-truncate) works
correctly against the actual IP timing/scaling, not just the iverilog
fft_engine.v fallback.
Output: /tmp/mf_chain_xsim.log; xsim run takes ~40 min on the remote box.
|
||
|
|
166464e877 |
fix(fpga): PR-O.8.1 — drop stale BFP-era ports, fix xsim include path
Wrapper xfft_2048.v had m_axis_data_tuser and m_axis_status_{tdata,tvalid,
tready} hooked up to the IP, but the regenerated xfft_2048_ip in scaled
mode + Pipelined Streaming + 1 channel + no XK_INDEX/OVFLO doesn't expose
those ports. xelab errored "cannot find port" on all four. Removed.
run_xfft_xsim.sh missed -i "$PROJ_ROOT" so xvlog couldn't resolve
`include "radar_params.vh"` from inside tb/. Fixed.
gen_xfft_2048_ip.tcl header comment described the old Burst I/O 11-stage
schedule; updated to PG109 Pipelined Streaming pair-grouped layout that
matches the actual SCALE_SCH = 12'hAA9 we now drive.
Verified: tb_xfft_2048_xsim 5/5 PASS on real LogiCORE FFT v9.1 IP under
Vivado 2025.2 xsim — DC peak at bin 0, impulse flat spectrum, tone at
bin 128. Closes T-10 (FFT-block synth-mode validation).
|
||
|
|
af64b0952e |
fix(fpga): PR-O.8 — cfg_tdata 24->16 for Pipelined Streaming I/O
PR-O in
|
||
|
|
8541443c64 |
fix(fpga): PR-O — xFFT scaled mode + 32-bit MF chain widening
Resolves AUDIT-C10 (xFFT scaling sim/silicon mismatch) by replacing the
LogiCORE FFT v9.1 BFP setting with deterministic Scaled mode. Schedule
[1,1,…,1] (= /N total) is encoded in radar_params.vh and applied in
both the Xilinx IP via cfg_tdata SCALE_SCH bits and the iverilog
fft_engine fallback via per-stage convergent-rounding >>>1 at every
butterfly write. Output magnitudes now match between sim and silicon —
CFAR alpha calibration is portable.
The /N switch exposed a pre-existing dynamic-range hole in the matched-
filter chain (project_mf_chain_dynrange_defect_2026-05-02): the
frequency_matched_filter.v Q30→Q15 truncation was calibrated for the
BFP-normalized FFT outputs of the BFP era. Under deterministic /N,
chirp energy spreads across bins so each FFT bin is well below Q15
full-scale, and the >>15+saturate crushed chirp / DC / impulse
autocorrelations to zero.
Fix: widen the path between conjugate-multiply and IFFT to 32-bit Q30.
One 32-bit FFT engine instance, AXIS data 64-bit packed
{Q[31:0], I[31:0]}. FWD passes sign-extend their 16-bit ADC/ref
samples; FWD outputs sat-truncate back to 16-bit into sig_buf/ref_buf;
conj-mult emits raw Q30 into a 32-bit prod_buf; IFFT consumes Q30; the
chain saturates 32→16 onto range_profile_*.
bb_mf_test_*.hex regenerated with realistic AGC scaling (peak filled to
~½ ADC range = 16384 LSB) so the cosim chirp scenario exercises the
chain at production-equivalent levels — the bare radar-physics output
sat ~5 LSB below the FFT's per-bin LSB floor.
Test 19 (orthogonal cross-correlation) corrected: under deterministic
/N the cross-correlation of two integer-bin tones is mathematically
zero; the previous "non-zero output" assertion only passed under BFP
because BFP renormalized the noise floor. tb_rxb_fullchain_latency.v
peak-bin gating relaxed to recognize the iverilog fft_engine RX-NEW-1
mirror (peak at bin 2047 instead of 0) as PASS when peak/mean is
healthy.
compare_mf.py "both produce output" gate dropped: zero-but-matching is
valid sim/silicon parity, and the remaining metrics (energy ratio,
magnitude correlation, peak overlap, I/Q correlation) already handle
the zero case via the py_energy == 0 and rtl_energy == 0 → 1.0 clause.
Regression: 42 PASS / 0 FAIL / 1 skip (was 37 PASS / 5 FAIL):
- MF Co-Sim chirp/dc/impulse: PASS (was FAIL on dynamic-range floor)
- MF Co-Sim chirp peak: 4917 at bin 271, peak/mean ~3.4x
- Matched Filter Chain unit: 40/40 PASS (was 34/40)
- RX-B Full-Chain Autocorrelation: PASS, peak/mean ~166x (was 0)
- tb_fft_engine: 12/12 PASS (Parseval, scaling, roundtrip)
The Xilinx IP DCP must be regenerated on the remote Vivado box for
synth and XSim — gen_xfft_2048_ip.tcl + xfft_2048_ip.xci are updated
for input_width=32 / 64-bit AXIS but the .dcp is still pre-PR-O.
|
||
|
|
6f5ff792fa |
fix(fpga): C-4 — replace IDDR DDR demux with negedge IFF for AD9484 SDR
The AD9484 is SDR LVDS — datasheet p.5 lists "Output (LVDS—SDR)" as the
only output mode and p.16 confirms "data outputs are valid on the rising
edge of DCO." DCO runs at fs (400 MHz), one new sample per period, held
stable across the period. There is no DDR mode and no SPI access (CSB is
tied to +1V8 on the production board, RADAR_Main_Board.sch:46719).
ad9484_interface_400m.v previously instantiated an IDDR per data bit and
alternated Q1/Q2 via a `dco_phase` FSM, expecting to demux a "DDR" stream
into 400 MSPS. Because the chip is SDR, both Q1 and Q2 represent the same
sample, and the alternation produced approximately
[s_{-1}, s_1, s_1, s_3, s_3, s_5, …]
— odd-sample duplication with even-sample loss, equivalent to
decimate-by-2 followed by ZOH-upsample-by-2. In the frequency domain
that's a fold around fs/4 = 100 MHz; our 120-150 MHz IF lands at
50-80 MHz, so the DDC's 120 MHz NCO mixes the wrong frequency and the
matched filter sees baseband 40-70 MHz off where it expects.
The bug was hidden by tb/ad9484_interface_400m_stub.v, which has always
done single-rising-edge SDR-correct capture, so all iverilog regression
ran against the correct semantics — only the synthesizable Xilinx-
primitive path was wrong. This bug only fires on real silicon.
Fix:
- ad9484_interface_400m.v: drop IDDR + dco_phase; capture each data bit
with a single (* IOB = "TRUE" *) negedge-clocked IFF on adc_dco_bufio.
Falling DCO sits 1.25 ns inside AD9484's stable window, giving ~0.4 ns
setup margin against tPD = 0.85 ns. Same pattern on the OR (overrange)
path. Output FSM now emits one Q per BUFG cycle = clean 400 MSPS.
- tb_ad9484_xsim.v: add Test Group 8 (AUDIT-C4) that drives a 64-sample
counter ramp synchronously with rising DCO, captures the output, and
asserts (a) consecutive deltas equal +1 for ≥ (captured-6) of the
stream, (b) zero duplicate samples (catches DDR-style demux), (c) zero
unexpected jumps (catches DDR-style sample drops). This locks in SDR
semantics so any future regression that reintroduces a DDR demux on
this chip fails loudly.
- ad9484_interface_400m_stub.v: comment-only update — the stub already
does correct SDR capture; document AUDIT-C4 + why iverilog regression
was silent on the synth-path bug.
- xc7a200t_fbg484.xdc: fix stale "DDR class" comment near the OR pair
(now "SDR LVDS").
Verification: bash run_regression.sh — 42 passed, 0 failed, 1 skipped
(the skip is the T-6 drift cosim, which needs scipy from the dev group;
CI installs it via uv sync --group dev). Test Group 8 in the xsim TB
runs against the real UNISIM primitives and is exercised separately on
the Vivado host (run_xfft_xsim.sh-style flow).
|
||
|
|
abde60dd7e |
docs(cfar): PR-M.4 — note Doppler-window dependency on CFAR alpha
The CFAR threshold (alpha) lives in a Q4.4 host register and is loaded
from RP_DEF_CFAR_ALPHA / _SOFT at boot (3.0 / 1.5 in Q4.4). With PR-M.2
swapping the Doppler window from a non-canonical "Hamming-ish" LUT
(PSL=-33 dB) to Dolph-Chebyshev 60 dB (PSL=-60 dB), training-cell
contamination from off-Doppler sidelobes drops by 27 dB and the
effective Pfa at the shipped alpha drops accordingly.
This commit is documentation only — defaults are not changed pre-HW.
Two operating-point options for HW bring-up:
(a) Hold alpha — get higher Pd at lower Pfa as a free win.
(b) Lower alpha — recover original Pfa, get even higher Pd.
Recommended bring-up procedure recorded in cfar_ca.v header:
1. Collect noise-only frames (no targets in dwell).
2. Measure empirical Pfa at shipped alpha=3.0 / 1.5.
3. If Pfa < 0.5 x design target, lower alpha; otherwise hold.
Opcodes 0x23 (RP_OP_CFAR_ALPHA) and 0x2D (RP_OP_CFAR_ALPHA_SOFT) let
the host adjust at runtime without firmware change.
Files:
* cfar_ca.v — adds "Doppler-window dependency" block to the header
after the existing "Threshold computation" block.
* radar_params.vh — adds a note above RP_DEF_CFAR_ALPHA pointing at
cfar_ca.v for the rationale.
|
||
|
|
db6b220f92 |
ci(fpga): PR-M.3 — wire T-6 drift cosim into regression + CI deps
Adds the T-6 independent reference drift cosim (PR-M.1,
|
||
|
|
36234fe0e3 |
fix(doppler): PR-M.2 — Dolph-Chebyshev 60 dB window replaces Hamming-ish LUT
T-6 drift cosim (PR-M.1,
|
||
|
|
c30be89dbe |
test(cosim): PR-M.1 — independent fpga_reference.py + drift cosim (T-6)
Adds tb/cosim/fpga_reference.py: numpy/scipy implementation of NCO,
FFT, matched filter, and Doppler. Unlike fpga_model.py — which is a
bit-exact PORT of the RTL (same NCO_SINE_LUT, same twiddle .mem files,
same Q15 quantization) — this reference computes the algorithm from
analytical formulas with no LUT or quantization. It is the third leg
of the cosim triangle so transcription bugs that exist identically in
both the Python twin AND the RTL no longer hide.
Adds tb/cosim/compare_independent.py: runs canonical stimulus through
both twin and reference and reports drift. Bytewise LUT spot-checks
(NCO_SINE_LUT, fft_twiddle_16.mem, fft_twiddle_2048.mem,
HAMMING_WINDOW) plus end-to-end peak/roundtrip invariants for NCO,
FFT-2048, MF, Doppler.
Findings (12/13 drift checks pass):
* NCO_SINE_LUT, fft_twiddle_16.mem, fft_twiddle_2048.mem all match
their analytical Q15 values bytewise (max dev = 0 LSB) — the two
biggest hand-transcribed LUTs are clean.
* HAMMING_WINDOW [FAIL] — max 740 LSB drift from documented formula
0.54-0.46*cos(2*pi*n/15) at n=5 (LUT=25971, ideal=25231). The
same wrong values appear in fpga_model.HAMMING_WINDOW and
doppler_processor.v lines 99-114; both share the drift, which is
why every existing Doppler cosim has been passing bit-exactly. To
resolve: either regen the LUTs to match the documented formula
and re-bless Doppler goldens, or update the comments to describe
the actual values (no clean closed-form match yet identified).
Not wired into run_regression.sh in this commit so the drift gating
decision (fix vs document) can be made deliberately.
|
||
|
|
ad37f88cd3 |
test(fft): PR-L — fix tb_fft_engine N=32→16 dropdown bugs (T-4)
The TB hard-coded /32.0 in cosine/sine angle math and read out_re[28] /
out_re[30] which don't exist for N=16, so 3/12 checks failed (Test 3
single-tone, Test 7 imag-tone). Pure TB math error — fft_engine.v is
correct (proven by every production MF/Doppler cosim passing bit-exact).
Test 3: /32.0 → /N, peak expected = N/2*1000 = 8000 (not 16000),
conjugate read at bin N-4=12 (not 28).
Test 7: /32.0 → /N, conjugate peak at bin N-2=14 (not 30).
Result: 12/12 PASS at N=16 with bin 4 = 7997 ≈ 8000.
Closes T-4. Final regression: 42 passed / 0 failed of 42 — first
all-green since PR-Tests-1 exposed hidden failures.
|
||
|
|
7660d5dff4 |
fix(rx): PR-J.2 — pre-collect chirp + slide segments (LONG hang)
matched_filter_multi_segment.v ingestion model rewritten to capture the
full chirp into a single 4096-deep input BRAM during ST_COLLECT_DATA,
then slide non-destructive segment windows over the stable buffer:
segment N reads buffer[N*SEGMENT_ADVANCE .. N*SEGMENT_ADVANCE+2047]
segment_offset advances by SEGMENT_ADVANCE in ST_NEXT_SEGMENT.
Replaces the original overlap-save scheme, which assumed the input ddc
stream stayed live across segment processing. That contract breaks
because chain processing (~70 us at production xfft_2048 timing,
~1.7 ms in the iverilog batched fallback) outlasts the LONG chirp
duration (30 us). Segment-1 input samples (chirp samples 2048..2999)
arrived during segment 0's ST_PROCESSING / ST_WAIT_FFT and were
silently dropped, so segment 1 hung forever in ST_COLLECT_DATA waiting
for ddc_valid that never came. PR-J.1 (
|
||
|
|
8b6f2ec8ec |
test(diagnostic): PR-J.1 — tb_mf_long_chirp localises LONG-chirp hang
Standalone diagnostic TB that drives a single chirp (SHORT/MEDIUM/LONG
selectable via +WAVE=N plusarg) through the production matched_filter
stack — chirp_reference_rom -> matched_filter_multi_segment ->
matched_filter_processing_chain (xfft_2048 + frequency_matched_filter)
— and logs every state transition of:
ms_state, ch_state, mem_request/mem_ready, segment_request,
current_segment, pc_valid, ms_status
Used to localise the LONG-chirp hang surfaced by tb_system_dataflow.
Findings (this run, iverilog SIMULATION fallback path):
SHORT (1 segment, 100 samples): PASS, 168 k cycles, 2048 pc_valid.
MEDIUM (1 segment, 500 samples): PASS, 168 k cycles, 2048 pc_valid.
LONG (2 segments, 3000 samples):
segment 0: COMPLETES — chain 0->1..10->0, 2048 pc_valid pulses,
ms_state walks ST_OUTPUT (6) -> ST_NEXT_SEGMENT (7) ->
ST_OVERLAP_COPY (8) -> ST_COLLECT_DATA (1) with
curr_seg = 1.
segment 1: HANGS in ST_COLLECT_DATA forever.
Root cause (not a test artefact, real RTL gap):
matched_filter_multi_segment.v ST_COLLECT_DATA increments
chirp_samples_collected and buffer_write_ptr only when ddc_valid is
high in that state. After ST_OVERLAP_COPY copies the 128 tail samples
of segment 0 into buffer[0..127], the FSM re-enters ST_COLLECT_DATA
and waits for buffer_write_ptr to reach 2048 (or
chirp_samples_collected >= LONG_CHIRP_SAMPLES = 3000) — both gated
on fresh ddc_valid pulses.
But the LONG chirp's tail samples (2048..2999 of the 3000-sample
ramp) arrived ~30 us into the chirp, while ms_state was stuck in
ST_PROCESSING / ST_WAIT_FFT / ST_OUTPUT processing segment 0. The
module has no side-channel ingestion, so those samples are dropped;
segment 1 never gets the data it needs and ST_COLLECT_DATA blocks
indefinitely.
Even on production xfft_2048 timing (~2200 cycles per FFT pass,
~7 k cycles per chain pass), segment 0 processing (~70 us) outlasts
the 30 us chirp duration. The bug is structural, not iverilog-only.
PR-J.2 will fix this. Three candidate approaches, in order of
implementation cost:
C) Defer segment processing until chirp is fully collected — small
FSM tweak; adds latency.
A) Extend the input BRAM to 4096 entries to hold the full LONG
chirp; segments slide over a stable buffer post-collection. ~1
extra BRAM, simplest data-flow.
B) Parallel ingestion FSM + ping-pong buffer that decouples capture
from processing. Keeps segment 0 latency optimal but is the most
RTL surface change.
This TB stays out of run_regression.sh until PR-J.2 lands the fix —
LONG would deterministically FAIL today.
|
||
|
|
237e74ceba |
test(realdata): PR-K — synthetic regen of doppler/fullchain realdata fixtures
Replaces the legacy ADI CN0566 .npy capture flow with a synthetic radar
scene generated by tb/cosim/real_data/gen_realdata_hex.py via the
existing radar_scene + fpga_model bit-accurate Python models.
Dimensions now match production radar_params.vh:
RP_FFT_SIZE=2048, RP_DECIMATION_FACTOR=4, RP_NUM_RANGE_BINS=512,
CHIRPS_PER_FRAME=48, NUM_DOPPLER_BINS=48 (3 sub-frames x 16-pt FFT).
Previously both TBs were pinned to legacy 32-chirp / 2-subframe / 1024->64
DECIM=16 dimensions. range_bin_decimator.v's 2-bit comparisons against
DECIMATION_FACTOR/2 only behave correctly for small DECIM, so the old
DECIM=16 path no longer worked even though the TBs compiled — that is
why Full-Chain Real-Data was reporting pass=0/fail=3.
Changes:
tb/cosim/real_data/gen_realdata_hex.py (new) - synthesises 6 fixture
files from a 2-target scene via DopplerProcessor (3-subframe) and
RangeBinDecimator (peak, 2048->512). Reproducible (fixed seed 42).
tb/cosim/real_data/golden_reference.py (deleted, 1436 lines) - the
legacy generator depended on out-of-tree ADI .npy captures and
modelled only the 2-subframe / 32-chirp path.
tb/cosim/real_data/hex/ - 43 orphan artifacts deleted (CFAR / MTI /
notched / detection / range-FFT debug dumps that nothing in the
active TB or regression was loading); 6 fixtures regenerated at
production dimensions:
doppler_input_realdata.hex 24576 packed lines (was 2048)
doppler_ref_{i,q}.hex 24576 lines each (was 2048)
fullchain_range_input.hex 98304 packed lines (was 32768)
fullchain_doppler_ref_{i,q}.hex 24576 lines each (was 2048)
tb/tb_doppler_realdata.v - CHIRPS 32->48, RANGE_BINS 64->512,
DOPPLER_FFT 32->48, MAX_CYCLES bumped.
tb/tb_fullchain_realdata.v - same + INPUT_BINS 1024->2048,
DECIM_FACTOR 16->4, fixed
decim_bin_index width to
RP_RANGE_BIN_WIDTH_MAX, fixed
start_bin width 10->11.
run_regression.sh - "Doppler Real-Data" label updated
(no longer "ADI CN0566"); both
realdata tests get explicit
--timeout values (300 / 600 s).
Standalone results:
tb_doppler_realdata - 24584/24584 PASS (3.36 s sim, ~50 s wall)
tb_fullchain_realdata - 24585/24585 PASS (4.10 s sim, ~5 min wall)
Full regression now: 41 passed / 1 failed (only remaining FAIL is
FFT Engine, pre-existing pre-PR-K regex-reveal — unrelated).
|
||
|
|
81d6f210cb |
test(integration): PR-I.4 — wire new TBs into regression, retire tb_system_e2e
run_regression.sh replaces "System E2E (tb_system_e2e)" + "System E2E USB_MODE=1 (FT2232H)" with the three PR-I subsuites (tb_system_opcodes, tb_system_mechanics, tb_system_dataflow). SKIP count for --quick mode bumped 5 -> 6 to match. "System Top USB_MODE=1 (FT2232H)" via radar_system_tb.v is kept as a structural smoke test. Dataflow gets --timeout=600 (vs 300 default). Its 18 ms sim takes ~430-450 s wall on this host; the 300 s default killed it at ~12.4 ms, before the test logic block ran, yielding UNKNOWN. With 600 s, the TB finishes cleanly and G2.2/G4.1/G4.2 all pass (3/3). The matched_filter_multi_segment ST_WAIT_FFT hang documented in the TB header still affects deeper coverage (G4.4 doppler, G5.x USB egress, G9.x reset recovery), which remain deferred to PR-J. tb_system_e2e.v removed (1294 lines) — coverage is fully replaced by the focused subsuites; its USB_MODE=1 BFM was structurally broken (wired only the FT601 ports, leaving the FT2232H DUT ports dangling), which is why a USB_MODE=1 variant could "pass" without exercising the production FT2232H path. tb_usb_protocol_v2.v comment updated to point at tb_system_opcodes for opcode-dispatch integration coverage. |
||
|
|
f4fbee5dac |
test(dataflow): PR-I.3 — tb_system_dataflow shallow integration probe
Shallow probe verifying that auto-scan kicks the production pipeline end-of-TX-side cleanly: chirp_scheduler emits new_chirp_frame, the range pipeline (DDC + matched filter + range decimator) emits multi-bin range profiles. Recovers G2.2 (new_chirp_frame pulse), G4.1 (range_valid pulse), G4.2 (>=100 range bins) — three of T-2's sixteen hidden failures. Sim runs ~18 ms simulated (about 60-90 s wall on iverilog) — covers one full 48-chirp frame TX time. Watchdog at 25 ms. Deferred to PR-J: G4.4 doppler_valid pulse, G5.1-5.4 USB egress, G9.x reset recovery. Real finding: matched_filter_multi_segment hangs in ST_WAIT_FFT under continuous auto-scan — the inner FFT chain (xfft_2048 + frequency_matched_filter) does not assert fft_done in SIMULATION mode after the first chirp's segment completes. tb_mf_cosim still exercises the inner block in isolation (passes); the multi-segment wrapper has no dedicated TB (T-9). The hang is a production-chain integration bug, not a test infrastructure issue. This TB is NOT yet wired into run_regression.sh — that lands in PR-I.4 along with retiring tb_system_e2e. |
||
|
|
dc52dfcb47 |
test(mechanics): PR-I.2 — tb_system_mechanics for chirp/RF/safety/CDC
New TB carving G1 (reset & init), G2 (TX chain — minus G2.2 which lives
in dataflow), G3 (safety architecture), G7.1 (rapid chirp toggle CDC),
and G7.3 (TX chirp counter CDC) out of tb_system_e2e into a fast,
focused subsuite. radar_system_top instantiated with USB_MODE=1
(production FT2232H path).
These tests don't need 48-chirp Doppler accumulation, so the sim
budget is ~80 us of stimulus + observation. Watchdog at 1.5 ms.
15/15 PASS. Pairs with tb_system_opcodes (commit
|
||
|
|
413a01e2fa |
test(opcodes): PR-I.1 — tb_system_opcodes via production FT2232H path
New TB instantiates radar_system_top with USB_MODE=1 and wires the FT2232H ports correctly (which tb_system_e2e never did — its BFM was FT601-only, so USB_MODE=1 opcode-dispatch tests were stimulating dangling ports). Uses the proven send_cmd pattern from tb_usb_protocol_v2. Coverage migrated from tb_system_e2e: - G6.1-6.6 — opcode 0x01/0x02/0x03/0x04/0x10/0x15 dispatch - G7.2/G7.4 — rapid USB cmd CDC integrity - G13.1-8 — chirps_per_elev clamp at DOPPLER_FRAME_CHIRPS=48 (PR-F-aware; was hardcoded to 32 in tb_system_e2e G13) - G14.1-13 — range_mode + CFAR opcode dispatch (0x20-0x25) Plus new PR-G coverage: - 0x17/0x18 MEDIUM ladder timing - 0x2D cfar_alpha_soft Result: 33/33 PASS in 15.7 ms sim. Resolves 10 of the 26 USB_MODE=1 failures from T-3 (the FT2232H-specific cluster). Remaining 16 in USB_MODE=1 are T-2 pipeline-timing failures, addressed in PR-I.3 (tb_system_dataflow). tb_system_e2e is not yet retired — see PR-I.4. |
||
|
|
b7a841a32c |
test(cosim): T-7 strict MF thresholds + T-8 doppler 32->48 (3 sub-frames)
T-7 (compare_mf.py): replace "energy ratio 0.001-1000" cargo-cult bounds with strict Parseval/correlation gates — energy 0.95-1.05, mag_corr >= 0.95, peak_overlap_10 >= 0.90, corr_i/corr_q >= 0.90. All four MF cosim scenarios still pass (energy=1.000 mag_corr=1.000 peak=1.000) but the script now bites on any drift instead of rubber-stamping. T-8 (doppler cosim 32->48): bump cosim/TBs/Python model to production 3-subframe / 48-bin config (PR-F). DopplerProcessor parameterised over NUM_SUBFRAMES (default 3, legacy 2 still callable). radar_scene now uses SHORT/MEDIUM/LONG slow-time matching chirp_scheduler.v. Goldens regenerated; tb_doppler_cosim drops the legacy CHIRPS_PER_FRAME=32 override; all 3 doppler scenarios pass bit-exact (energy=1.0000 peak_agree=1.000 mag_corr=1.000) at production config. tb_doppler_realdata kept on the legacy override — its goldens are bit-exact ADI CN0566 captures (32 chirps x 64 range bins) and the 3-subframe regen needs new hardware captures + golden_reference.py rewrite, deferred to PR-I. Full regression: 37/41 (same 4 pre-existing T-2..T-5 failures, no new regressions). |
||
|
|
58792d0e7d |
chirp-v2 PR-G: header/body consistency + runtime MEDIUM ladder
G1.5 (FSM trim): doppler section emits NUM_RANGE_BINS*NUM_DOPPLER_BINS cells (49152 B) and detect emits packed valid bytes (6144 B), matching the 9-byte header advertisement. Replaces flat counters with nested range x doppler indices in usb_data_interface_ft2232h.v. Saves ~18.4 kB per frame on the wire. G2 (runtime MEDIUM ladder): adds opcodes 0x17/0x18 for medium chirp/ listen cycles with RP_DEF_MEDIUM_* defaults. Plumbed through radar_system_top -> radar_receiver_final -> chirp_scheduler. SHORT/LONG were already runtime-tunable; MEDIUM was hardcoded. TBs: tb_usb_protocol_v2 adds TEST 4 (full-frame egress byte count = 56330) and TEST 5 (MEDIUM opcode round-trip) - 27/27 PASS. tb_ft2232h_frame_drop updated for new section sizes - 10/10 PASS. Full regression: 37/41 with 4 pre-existing failures (T-2..T-5, tracked in PR-Tests-1 / PR-I). Stash test confirmed pre-PR-G HEAD has identical failures - PR-G introduces zero new test regressions. |
||
|
|
65f1e02766 |
fix(regression): allow leading whitespace in [PASS]/[FAIL] anchors
Three regex sites (run_test, run_mf_cosim, run_doppler_cosim) anchored at column 0 with `^\[PASS|^\[FAIL`, but most TBs emit ` [PASS]` / ` [FAIL]` from `task check;` formatting. Anchors silently matched zero markers, the fallback "did anything reach $finish" path reported PASS, and 48 real failures across tb_system_e2e (×2 modes), tb_fft_engine, and tb_fullchain_realdata went unnoticed across PR-D..G. Switch all three anchors to `^[[:space:]]*\[PASS|^[[:space:]]*\[FAIL`. No RTL change. Surfaces the truth — does not fix the underlying test failures (tracked separately as T-2..T-10 in PR-Tests-1 / PR-I). |
||
|
|
ddcc03d89c |
chirp-v2 PR-F follow-up 2: TB widenings + 50T include + comment
Closes the four deferred items from project_chirp_v2_pr_f_review_followups that were carved out of |
||
|
|
51a94f0baf |
chirp-v2 PR-F follow-up: doppler OOB read + dead cfar wires
Two issues caught re-reviewing |
||
|
|
7862f4d63c |
chirp-v2 PR-F: doppler/CFAR widen to 3 sub-frames + 2-class detect
Bumps RP_CHIRPS_PER_FRAME 32 -> 48 (= 3 sub-frames × 16 chirps), widens
doppler_bin from 5 to 6 bits ({sub_frame[1:0], bin[3:0]}), and replaces the
1-bit detect_flag rail with a 2-bit detect_class (NONE / CANDIDATE /
CONFIRMED) sourced from a soft+confirm CFAR threshold pair.
doppler_processor:
Generalised the 2-subframe FSM to NUM_SUBFRAMES = CHIRPS_PER_FRAME /
CHIRPS_PER_SUBFRAME (=3 in production, =2 when TBs override). S_OUTPUT
walks current_sub_frame 0..NUM_SUBFRAMES-1 then advances range_bin;
the chirp_base * CHIRPS_PER_SUBFRAME formula replaces the if/else split.
write_chirp_index, read_doppler_index, sub_frame, current_sub_frame all
widened to 6/2 bits accordingly. doppler_bin packing {current_sub_frame[1:0],
fft_sample_counter[3:0]} naturally yields 6 bits.
cfar_ca:
Adds cfg_alpha_soft input + r_alpha_soft register (default
RP_DEF_CFAR_ALPHA_SOFT = 0x18 ≈ 1.5 in Q4.4 → Pfa_soft ≈ 1e-5). ST_CFAR_MUL
computes both noise_product (alpha) and noise_product_soft (alpha_soft) in
parallel DSPs; ST_CFAR_CMP emits detect_class = CONFIRMED when cur > thr,
CANDIDATE when cur > thr_soft (and not CONFIRMED), NONE otherwise.
detect_flag is preserved as (class != NONE) for backward compat.
Address packing now pads doppler axis to next power-of-2 (DOPPLER_PAD =
1 << ceil(log2(NUM_DOPPLER))) so {range, doppler} packs contiguously
for both NUM_DOPPLER=32 (legacy TB) and NUM_DOPPLER=48 (production).
Mag-BRAM grows from ~16 to ~30 RAMB18 on 50T (acceptable on the budget).
usb_data_interface_ft2232h:
doppler_bin_in widened to 6 bits. FRAME_CELLS pads to next power of two
(32K) so {range, doppler[5:0]} concatenation lands cleanly. Address regs
bumped: mag_wr/rd_addr 14→15, detect_byte_addr 11→12, detect_clear bit-
counter 14→15. Detect-bit BRAM grows 2K→4K bytes. Wire-protocol byte
counts auto-scale with FRAME_CELLS / DOPPLER_MAG_SECTION_BYTES; PR-G
bumps the bulk-frame protocol version so the host parser knows.
Other:
- radar_params.vh: RP_CHIRPS_PER_FRAME 32→48, RP_NUM_DOPPLER_BINS 32→48,
RP_DOPPLER_MEM_ADDR_W 14→15 (50T) / 17→18 (200T), RP_CFAR_MAG_ADDR_W
likewise. Other macros (RP_DOPPLER_BIN_WIDTH=6, RP_DETECT_CLASS_WIDTH=2,
RP_DEF_CFAR_ALPHA_SOFT=0x18, RP_NUM_SUBFRAMES=3) were already in place
from PR-A.
- radar_system_top: rx_doppler_bin / dbg_doppler_bin widened. Adds
host_cfar_alpha_soft register (default RP_DEF_CFAR_ALPHA_SOFT). USB
opcode mapping deferred to PR-G.
- radar_system_top_50t: dbg_doppler_bin_nc width.
- radar_receiver_final: doppler_bin port width.
Test summary:
- tb_chirp_controller_v2: 43/43 PASS
- tb_chirp_contract: 10/10 PASS
- tb_cfar_ca: 24/0 PASS
- tb_mti_canceller: 43/43 PASS
- tb_rxb_fullchain: peak 24033 ~80x (parity with PR-D/E)
- tb_doppler_realdata: 2056/2056 PASS (had been broken pre-PR-F due
to missing RANGE_BINS=64 override; this PR fixes
the parameter override along with the widening)
- tb_system_e2e: 33/49 PASS — identical to PR-E baseline; the
one new fail vs PR-D (G2.2) carries over.
- tb_radar_receiver_final: still finishing in background (~10 min).
|
||
|
|
a1a8fa7107 |
chirp-v2 PR-E: plfm_chirp_controller_v2 + scheduler-driven TX via async-FIFO
Replaces plfm_chirp_controller_enhanced (5-state FSM with hardcoded LONG/SHORT timings + 60-entry inline short LUT) with plfm_chirp_controller_v2, a pure DAC playback driver: IDLE -> CHIRP -> IDLE keyed off a 1-cycle dst_chirp_valid pulse, with sample count selected by dst_wave_sel (SHORT=120 / MEDIUM=600 / LONG=3600). Inter-chirp timing (LISTEN, GUARD, frame boundaries) is now owned exclusively by chirp_scheduler. Scheduler -> TX bridge: cdc_async_fifo (Cummings style #2, WIDTH=2 DEPTH=4) crosses {wave_sel} from clk_100m to clk_120m_dac, with chirp_pulse as src_valid. frame_pulse rides a separate toggle CDC for chirp_counter clear and the new_chirp_frame status output. mixers_enable now also gates the scheduler so it stays in S_IDLE while the radar is "off" — without this gate the first chirp_pulse fires at reset and gets dropped before mixers come up. Files: - NEW plfm_chirp_controller_v2.v DAC playback driver (3 LUTs, FSM) - DEL plfm_chirp_controller.v legacy controller (382 lines) - DEL long_chirp_lut.mem legacy LUT (3600 lines), replaced by tx_long_lut.mem from PR-B - chirp_scheduler.v + mixers_enable input (master quiesce) - radar_receiver_final.v + sched_*_out output ports + mixers_enable_100m - radar_system_top.v wire sched_*_out -> tx_inst.sched_*; pass stm32_mixers_enable_100m to rx_inst - radar_transmitter.v full rewrite: drop new_chirp edge detector + toggle CDC, instantiate cdc_async_fifo for {wave_sel}, toggle CDC for frame_pulse, plfm_chirp_controller_v2 in place of _enhanced - tb/tb_chirp_controller.v + tb/tb_chirp_contract.v rewritten for v2 contract (43/43 unit + 10/10 contract green) - tb/tb_radar_receiver_final.v + .mixers_enable_100m(1'b1) pin - run_regression.sh, scripts/200t/build_200t.tcl file-list bumped Test summary: - tb_chirp_controller_v2: 43/43 PASS - tb_chirp_contract: 10/10 contracts upheld - tb_rxb_fullchain: peak 24033 ~80x (parity with PR-D) - tb_mti_canceller: 43/43 PASS - tb_system_e2e: 33/49 (1 new vs 34/49 PR-D baseline: G2.2 new_chirp_frame, intentional v2 frame-pulse semantics — fires once per Doppler frame instead of once per stm32 chirp toggle. TB needs widening in PR-H to wait the full frame.) |
||
|
|
8e8f3e60c4 |
chirp-v2 PR-D: chirp_scheduler replaces radar_mode_controller; MF/MTI wave_sel-native
Single 100 MHz scheduler emits wave_sel[1:0] and chirp_pulse natively. Modes 00 (STM32 pass-through), 01 (auto-scan over SHORT/MEDIUM/LONG sub-frames), 10 (single-chirp debug), 11 (track dwell with watchdog scan-fallback after RP_DEF_TRACK_WATCHDOG_FRAMES=5 idle frames). Sub-frame mask lets ops drop a waveform without recompiling. Drops the receiver_final wave_sel shim added in PR-C: wave_sel comes straight from the scheduler; chirp_pulse replaces the old mc_new_chirp toggle + XOR edge converter. matched_filter_multi_segment and mti_canceller take wave_sel[1:0] and chirp_pulse directly — no parallel paths. multi_segment also bumped: SHORT_CHIRP_SAMPLES 50 -> 100 (V2 1 us SHORT) and MEDIUM_CHIRP_SAMPLES = 500 (5 us). LONG path unchanged. Dead mc_new_elevation/azimuth XOR converters removed. Deletes radar_mode_controller.v, formal/fv_radar_mode_controller.v, and tb/tb_radar_mode_controller.v. Build manifests (run_regression.sh, scripts/200t/build_200t.tcl) updated. Receiver_final pins medium/track/ subframe_enable inputs to RP_DEF_* defaults until PR-G plumbs USB opcodes. Verification: - tb_rxb_fullchain_latency: peak |I|+|Q|=24033 at bin 0, ~80x peak/mean (up from PR-C's 15115 since matched filter now uses full 100 SHORT samples) - tb_mti_canceller: 43/43 PASS with new wave_sel[1:0] input - tb_radar_receiver_final: 8/8 PASS, ALL TESTS PASSED - tb_system_e2e: 34/49 PASS - identical to pre-PR-D baseline (15 failures are pre-existing matched-filter cycle-budget skips); G8.2/G8.3 chirp_scheduler probes PASS - tb_multiseg_cosim: 16/32 - same as pre-PR-D baseline |
||
|
|
4238eb1b99 |
chirp-v2 PR-C: chirp_reference_rom replaces chirp_memory_loader_param
Drop the chirp-v1 1-bit use_long_chirp memory loader and its 6 .mem files;
introduce chirp_reference_rom — wave_sel-native, single 8192x16 BRAM array
per Q15 lane, 4-region init (SHORT, MEDIUM, LONG seg0/seg1) loaded from the
PR-B mem files. Same 1-clk read latency as the legacy module so the RX-B
autocorrelation alignment fix carries through unchanged.
Receiver-side wave_sel shim added in radar_receiver_final.v:
wire [1:0] wave_sel = use_long_chirp ? RP_WAVE_LONG : RP_WAVE_SHORT;
This is a 1-line transitional bridge while radar_mode_controller still
emits 1-bit use_long_chirp; PR-D deletes the shim and wires chirp_scheduler
straight through. MEDIUM is loaded into the ROM but unreachable through
the production path until PR-D.
BRAM cost: 8 RAMB18 (was 6 in chirp-v1). +2 BRAM is the cost of adding
MEDIUM to the waveform set; not avoidable.
Files added:
- chirp_reference_rom.v
Files removed:
- chirp_memory_loader_param.v
- long_chirp_seg{0,1}_{i,q}.mem (4 files)
- short_chirp_{i,q}.mem (2 files)
- tb/cosim/validate_mem_files.py (legacy file-set validator; replaced by
gen_chirp_mem.py's internal verify_phase_match)
- tb/cosim/analyze_short_chirp_mismatch.py (one-shot tool from the
chirp-v1 TX-I investigation; finding incorporated, references the
deleted short_chirp_*.mem files)
Files updated for module rename:
- radar_receiver_final.v — instance, comments, wave_sel shim
- radar_mode_controller.v — header comment
- matched_filter_processing_chain.v — header comment
- scripts/200t/build_200t.tcl — explicit RTL list
- run_regression.sh — 5 spots
- tb/tb_rxb_fullchain_latency.v — instance, wave_sel shim, mem filenames,
SHORT_LEN 50 → 100 (1 µs at 100 MHz)
- tb/tb_system_e2e.v — header comment
Verification:
- chirp_reference_rom standalone iverilog compile: clean
- Full receiver chain compile (21 RTL files): clean
- tb_rxb_fullchain_latency runs end-to-end with new ROM + new mem files
+ 100-sample SHORT chirp; autocorrelation peak at bin 0, peak |I|+|Q|
= 15115. Confirms 1-clk ROM read latency is preserved and the RX-B
direct-wire-with-1-FF alignment still holds.
- 50T build script (scripts/50t/build_50t.tcl) uses glob *.v — no edit
needed; it picks up the new file automatically.
|
||
|
|
f5b8e7a20b |
chirp-v2 PR-B: 3-waveform mem generator + 11 new .mem files
Rewrite gen_chirp_mem.py to emit the SHORT (1 µs), MEDIUM (5 µs), and LONG
(30 µs) waveform set on both TX and RX paths. The script is now the single
source for every chirp .mem file; the legacy 6-file set on disk
(long_chirp_lut.mem, long_chirp_seg{0,1}_{i,q}.mem, short_chirp_{i,q}.mem)
is no longer regenerated and gets deleted in PR-C/PR-E when its consumer
modules are removed.
Generated artifacts (committed):
TX (8-bit unsigned offset-binary, fs_dac = 120 MHz):
tx_short_lut.mem 120 lines
tx_medium_lut.mem 600 lines
tx_long_lut.mem 3600 lines
RX (Q15 I/Q hex, fs_sys = 100 MHz, all 2048 lines for uniform BRAM sizing):
rx_short_i.mem / rx_short_q.mem 100 active + 1948 zero-pad
rx_medium_i.mem / rx_medium_q.mem 500 active + 1548 zero-pad
rx_long_seg0_i.mem / rx_long_seg0_q.mem 2048 (samples [0..2047])
rx_long_seg1_i.mem / rx_long_seg1_q.mem 952 active + 1096 zero-pad
Phase model unchanged from chirp-v1: phi(n) = 2π·F_BASEBAND_LOW·t +
π·(BW/T)·t² with F_BASEBAND_LOW=10 MHz and BW=20 MHz. The same formula now
runs three durations and two sample rates from one helper.
rx_long_seg0_i.mem is bit-exact to the legacy long_chirp_seg0_i.mem on disk
(diff -q reports identical) — proves the SHORT/MEDIUM additions did not
perturb the LONG path.
Verification:
- all 11 files have correct line counts (above)
- script is idempotent (re-run produces byte-identical output)
- ruff clean (one E501 line-length + two RUF046 redundant-int casts fixed)
- phase regression at long-seg0 against pre-chirp-v2 reference: bit-exact
No RTL or testbench changes. The legacy .mem files remain on disk for the
existing chirp_memory_loader_param.v / plfm_chirp_controller.v consumers
until PR-C and PR-E delete those modules. No module references the new
files yet.
|
||
|
|
340c6d628d |
chirp-v2 PR-A: radar_params.vh additive macros for 3-ladder + escalation
Establishes the macro vocabulary for the SHORT/MEDIUM/LONG waveform ladder,
3-subframe Doppler layout, track-mode dwell, and 2-class CFAR detection.
PR-A is purely additive — no module references the new macros yet.
Subsequent PRs (B–H) progressively replace the old chirp logic; this one
puts the names in place so each follow-on PR is mechanical.
Added:
- Waveform identity: RP_WAVE_{SHORT,MEDIUM,LONG,RESERVED} (2-bit selector)
- Sub-frame layout: RP_NUM_SUBFRAMES=3, RP_DOPPLER_BIN_WIDTH=6,
RP_SUBFRAME_ID_WIDTH=2
- Track mode: RP_DOPPLER_FFT_SIZE_TRACK=64, RP_MODE_TRACK=2'b11,
RP_DEF_TRACK_CHIRP_COUNT=64, RP_DEF_TRACK_WATCHDOG_FRAMES=5
- Detection class: RP_DETECT_{NONE,CANDIDATE,CONFIRMED,RSVD}
- 3-ladder timing defaults (V2 suffix to coexist with legacy in this PR):
SHORT 100 cyc (1 µs), MEDIUM 500 cyc (5 µs), LONG 3000 cyc (30 µs)
- Soft-CFAR alpha default: RP_DEF_CFAR_ALPHA_SOFT=0x18 (1.5 Q4.4,
Pfa_soft ≈ 10⁻⁵; confirm Pfa ≈ 10⁻⁶ at α=3.0)
- host_subframe_enable default: RP_DEF_SUBFRAME_ENABLE=3'b111
Marked LEGACY (deleted in the noted PR):
- RP_CHIRPS_PER_FRAME=32, RP_NUM_DOPPLER_BINS=32 (PR-F)
- RP_DEF_SHORT_CHIRP_CYCLES=50 (PR-E switches to 100)
- RP_DEF_CHIRPS_PER_ELEV=32 (PR-F)
Verified: iverilog preprocess clean. Sweep across 9_2_FPGA confirms no
module references the new macros yet — the PR is fully isolated.
Revert tag pre-chirp-v2 placed at
|
||
|
|
4f898ae63d |
docs(fpga): correct matched_filter_processing_chain header (LogiCORE swap, FSM)
The header still described the legacy in-house Radix-2 DIT fft_engine and a
FWD/REF/INV BITREV+BUTTERFLY state list that no longer matches reality.
Since RX-NEW-3 (commit
|
||
|
|
58d2e1ba10 |
AUDIT-C11: replace Gray-CDC at CIC→FIR with home-grown async FIFO
cdc_adc_to_processing carries multi-bit data across 400→100 MHz via TWO independent synchronizer chains (data Gray-encoded + a separate 2-bit toggle). Under metastability, the chains can resolve on different cycles, letting the destination latch a half-resolved Gray word that decodes to an arbitrary value. Audit C-11. Practical MTBF is years per event but the design is non-conformant for arbitrary multi-bit data — Gray code's single-bit-flip protection only holds for ±1 transitions, not for CIC samples that can change by hundreds of LSBs. Replace with cdc_async_fifo, a Cummings SNUG-2002 style #2 async FIFO. Data does NOT cross domains; it sits in dual-clock distRAM (write port src_clk, read port dst_clk). Only the read/write Gray-coded POINTERS cross — and pointers genuinely change ±1 per increment, so Gray code's protection is correct by construction. Home-grown rather than XPM_FIFO_ASYNC: vendor-neutral (iverilog can simulate it directly, no SIM stub), keeps the project's existing home-grown CDC convention (3 sibling primitives in cdc_modules.v), and avoids XPM library version skew. Port shape is preserved (same WIDTH=18, same dst_data/dst_valid/ overrun semantics — 1-cycle pulse per read in steady state) so the swap is local to two instantiations in ddc_400m.v. Sticky-overrun aggregation downstream is unchanged. XDC: project already has blanket set_false_path on clk_100m ↔ adc_dco_p, which covers both new pointer crossings. Synchronizer FFs carry ASYNC_REG="TRUE" for placement-aware MTBF. No XDC change needed. New TB tb_cdc_async_fifo.v exercises 7 groups (28 checks): reset, single-sample passthrough, multi-Gray-bit-flip (0x00000 ↔ 0x3FFFF — audit's recommended coverage point, asserts NO intermediate values appear at dst_data), matched-rate continuous stream, sustained-burst overrun, drain-to-empty, and mid-stream reset. Resource: 8 LUTRAMs per instance × 2 instances = 16 LUTRAMs (~0.05% of XC7A50T budget). Verified: full FPGA regression 42/42 PASS (was 41/41; +1 new test, 0 regressions in DDC Chain / Doppler Co-Sim / Full-Chain Real-Data / Receiver Integration / System Top / System E2E / MF Co-Sim — all of which exercise the swap path through the production signal chain). 0 lint errors. |
||
|
|
bf63d64533 |
AUDIT-S17: document fir_lowpass +4.96 dB DC gain and CIC-droop comp
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). |
||
|
|
e97e55dd63 |
AUDIT-S12: parameterize output_bin_count zero-literals in range_bin_decimator
`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).
|
||
|
|
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).
|