mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-08 14:44:56 +00:00
d9e7a5becf9cf89e9c677b67ba85d1322d84a530
494 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |