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
21bf7a0): medium_chirp_cycles + medium_listen_cycles in word 7
- subframe_enable_snapshot in frame byte 2 bits[5:3] (PR-U / M-8)
- 2-bit cfar_detect_class (PR-F)
- PR-G CFAR telemetry (detect_count_cand + detect_threshold_soft
in status word 6)
- AUDIT-C12 wr_done_toggle handshake + frame_drop_count
- PR-Z A6 Bug C fix (detect_clearing on wr_done_pulse)
- PR-AA fix (BRAM addr advance at phase 0 / MSB, not phase 1)
- PR-Z A6 Bug B preload (detect_rd_addr=1 at WR_DETECT entry)
- Output stage: byte_now combinational generator + 4-lane pack-emit
accumulator. Full words BE=1111, partials BE=0001/0011/0111 at
section ends. Byte-order convention: byte 0 -> ft601_data[7:0],
byte N+1 -> next lane up (preserves MSB-first FT2232H wire order)
- ODDR clock forwarding preserved (ft601_clk_out)
- Vestigial ft601_txe_n / ft601_rxf_n outputs kept tied 1'b1
(200T board has physical pins routed; removing breaks the build)
radar_system_top.v — 9 new connections to the FT601 instance
(no new top-level signals — all driven from already-existing internal
wires the FT2232H instance was already using):
- cfar_detection (1-bit) -> cfar_detect_class (2-bit)
- range_bin_in, doppler_bin_in (rx_detect_valid mux)
- frame_complete, subframe_enable
- status_medium_chirp, status_medium_listen, status_cfar_alpha_soft
- status_detect_threshold_soft, status_detect_count_cand
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.
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.
Pre-fix usb_data_interface.v hardcoded `localparam [14:0] NUM_CELLS =
15'd16384` for the 50T 512-range x 32-doppler layout. On 200T builds
with SUPPORT_LONG_RANGE defined, RP_MAX_OUTPUT_BINS=4096 makes a real
frame 131072 cells, so the fixed value caused two distinct defects:
(a) value: counter wrapped 8x per real frame; bit-7 frame-start
marker fired 8x at incorrect host-frame offsets, silently
desyncing the GUI parser
(b) width: 15 bits could not represent 131072 (needs 17 bits)
Fix: derive NUM_CELLS = RP_MAX_OUTPUT_BINS * RP_NUM_DOPPLER_BINS and
counter width = RP_DOPPLER_MEM_ADDR_W (14 on 50T, 17 on 200T) from
radar_params.vh, so both scale together with the build define.
Tests:
- tb_audit_c16_num_cells.v: standalone counter-block exerciser (T1
reset, T2 increment, T3 wrap at NUM_CELLS-1, T4 exactly 2 markers
across 2*NUM_CELLS ticks, T5 top-bit observability) -- 6/6 PASS at
both 50T (NUM_CELLS=16384, CTR_W=14) and 200T (131072, 17).
- tb_usb_data_interface.v: existing test 7-8 retargeted from the old
hardcoded `>=15` / `==15'd16384` invariant to the new parameterized
one (`==RP_DOPPLER_MEM_ADDR_W` / `==RP_MAX_OUTPUT_BINS*RP_NUM_DOPPLER_BINS`).
Regression: 41/41 PASS (+2 new entries: 50T default + 200T
`+define+SUPPORT_LONG_RANGE`).
gpio_dig5 (PD13) previously OR'd six flags — four signal-saturation
classes (AGC, DDC overflow, DDC saturation, MTI saturation) and two
control-fault classes (range-decimator watchdog from F-6.4, CIC->FIR
CDC overrun from F-1.2). The MCU outer-loop AGC reduces RF gain on
PD13 assertion, which is the wrong response to a watchdog or CDC
stall — it just hides the stall behind a quiet receive chain. gpio_dig7
(PD15) was tied 1'b0 as "reserved".
Split:
gpio_dig5 = signal-saturation only (AGC continues to react correctly)
gpio_dig7 = control-fault classes
Telemetry: status_words[5][6:5] now exposes the two control-fault
classes in BOTH legacy (FT601) and FT2232H USB variants, with 2-FF
level CDC sync from clk_100m to ft601_clk_in / ft_clk. Bit [7] is
reserved. AUDIT-C12's frame_drop_count at [31:25] is preserved.
50T XDC H12 -> gpio_dig7 pin already assigned (audit AUDIT-C15-era);
no XDC change.
Test: tb/tb_audit_s10_gpio_split.v 17/17 PASS — exercises both the
combinational GPIO split and the CDC status-word packing path.
Regression: 39/39 PASS (was 34/34).
cmd_data / cmd_opcode / cmd_addr / cmd_value feed downstream CDC sync
chains; the safety property is that they only change on the cycle
cmd_valid rises (RD_PROCESS), and stay held on every other cycle so the
receiver's 2-FF synchronizer sees a clean payload regardless of where
its sample window lands. The FSM satisfies this implicitly today, but
nothing flagged a regression that introduced a stray write somewhere
in the same always block.
Added an `ifdef SIMULATION block at the bottom of both
usb_data_interface.v (FT601 / ft601_clk_in / ft601_reset_n) and
usb_data_interface_ft2232h.v (FT2232H / ft_clk / ft_reset_n). It
snapshots the payload + cmd_valid each cycle and fires
[ASSERT FAIL] TX-N9: cmd_<field> changed while cmd_valid=0 (old -> new)
on any payload change while cmd_valid is low. Local regs suffixed _n9
to avoid future name collisions. Synthesis-inert.
Quick FPGA regression unchanged: USB Data Interface 91/91 PASS, overall
28/29 (same baseline; the 1 fail is the pre-existing iverilog/Xilinx-IP
RX-NEW-3 gap).
`chirps_mismatch_error` was set in radar_system_top when the host
requested chirps_per_elev != Doppler FFT size, but never wired into the
USB status response — a latent silent failure.
Wired the flag through both USB interfaces (FT601 + FT2232H) into bit
[10] of status word 4 (was reserved). GUI parser exposes it as
StatusResponse.chirps_mismatch.
- usb_data_interface*.v: new status_chirps_mismatch input, packed at [10]
- radar_system_top.v: connect chirps_mismatch_error to both USB instances
- radar_protocol.py + test_GUI_V65_Tk.py: parse new bit, +1 round-trip test
- tb_usb_data_interface.v: drive the new port, update word-4 expectation
Tests: GUI 92/92 (was 91), MCU 75/75, USB TB 91/91, ruff clean repo-wide.
The 2 remaining FPGA regression failures (Receiver Integration, MF Chain)
are the pre-existing iverilog-can't-link-Xilinx-IP issue tracked
separately as the open RX-NEW-3 follow-up.
usb_data_interface.v NUM_CELLS was still 12'd2048 (64 range x 32 doppler)
from the pre-2048-FFT architecture. With 512 range bins x 32 Doppler, the
12-bit counter wrapped every 2048 packets and the host received 8 false
frame-start markers per real frame via the sample_counter==0 bit packed
into the detection byte. Widen counter to 15 bits and set NUM_CELLS to
16384. Sister file usb_data_interface_ft2232h.v was already correct.
Remove three stale testbenches hardcoded to the old 1024-pt / 64-bin
architecture (tb_mf_chain_synth, tb_fullchain_mti_cfar_realdata,
tb_range_fft_realdata). Equivalent current-architecture coverage already
exists in tb_matched_filter_processing_chain, tb_fullchain_realdata,
tb_fft_engine, tb_multiseg_cosim, and tb_mf_cosim.
Hand-merged files modified on both fix/pre-bringup-audit-p0 and
feat/fft-2048-upgrade. Wave 1 (commit 60e49c7) took 20 files from fft
verbatim; this wave resolves the overlap.
- run_regression.sh: 3-way merge. Adopts fft's ${RECEIVER_RTL[@]} array
refactor and drops the self-blessing golden pair from p0. Skip count
bumped to 5.
- usb_data_interface.v (FT601/200T): p0 FSM + clock-loss watchdog kept
wholesale; widened stream_control 3 -> 6 bits to carry fft's extended
mode bits through the CDC sync chain and the 0xFF status word.
- mti_canceller.v: fft's BRAM-inferred 512-range-bin implementation as
the base, with p0's F-6.3 saturation counter grafted onto the d1
pipeline stage. Overflow detection uses the top-two-bits disagreement
on diff_{i,q}_full (DATA_WIDTH+1 signed).
- radar_receiver_final.v: fft's 2048-pt / 512-bin structure + p0
diagnostic plumbing (ADC overrange sticky+CDC, DDC diagnostics,
tx_frame_start edge detector replacing chirp_counter frame sync,
mti_saturation_count, range_decim_watchdog).
- radar_system_top.v: clean 3-way merge, orthogonal regions
(+38 / -27).
- usb_data_interface_ft2232h.v (FT2232H/50T): fft's per-frame bulk BRAM
rewrite kept wholesale. Ported two p0 items that are orthogonal to
the write FSM:
* ft_clk-loss watchdog (heartbeat + 2FF ASYNC_REG sync + 16-bit
timeout) ORed into a 2FF sync'd ft_effective_reset_n for the FSM.
* rd_cmd_complete flag so RD_DEASSERT can distinguish a legitimate
3-byte completion from an ft_rxf_n abort that also zeros
rd_byte_cnt.
Deliberately NOT taken from 2401f5f: cic_decimator_4x_enhanced.v and
ddc_400m.v reset-strategy changes. Those conflict with p0's shipped
registered-sync-reset + max_fanout=25 distribution, which is already
timing-clean on the production build.
Bug 1 (FPGA): status_words[0] was 37 bits (8+3+2+5+3+16), silently
truncated to 32. Restructured to {0xFF, mode[1:0], stream[2:0],
3'b000, threshold[15:0]} = 32 bits exactly. Fixed in both
usb_data_interface_ft2232h.v and usb_data_interface.v.
Bug 2 (Python): radar_mode extracted at bit 21 but was actually at
bit 24 after truncation — always returned 0. Updated shift/mask in
parse_status_packet() to match new layout (mode>>22, stream>>19).
Bug 3 (STM32): parseFromUSB() minimum size check was 74 bytes but
9 doubles + uint32 + markers = 82 bytes. Buffer overread on last
fields when 74-81 bytes passed.
All 166 tests pass (29 cross-layer, 92 GUI, 20 MCU, 25 FPGA).
- usb_data_interface.v: Add 3 self-test status inputs, expand status packet
from 7 words (header + 5 data + footer) to 8 words (header + 6 data + footer).
New status_words[5] carries {busy, detail[7:0], flags[4:0]}.
- radar_system_top.v: Wire self_test_flags_latched, self_test_detail_latched,
self_test_busy to usb_data_interface ports. Add opcode 0x31 as status
readback alias so host can read self-test results.
- tb_usb_data_interface.v: Add self-test port connections, verify word 5 in
Group 16, add Group 18 (busy flag + partial failure variant). 81 checks pass.
- run_regression.sh: Add fpga_self_test.v to PROD_RTL lint list and system-
level compile lists. Add tb_fpga_self_test as Phase 1 unit test.
- 24/24 regression tests pass, lint clean (0 errors, 4 advisory warnings).
usb_data_interface.v: doppler_data_pending and cfar_data_pending were
driven by two always blocks (CDC sync block set them, write FSM cleared
them). Vivado DRC MDRV-1 flagged this as multiple drivers. Moved all
set/clear logic into the write FSM always block using doppler_valid_ft
and cfar_valid_ft edge wires.
adc_clk_mmcm.xdc: changed set_false_path -from to -through for MMCM
LOCKED pin (not a valid timing startpoint). Eliminates CRITICAL WARNING
from Builds 19/20/21.
19/19 FPGA regression pass.
RTL fixes discovered via new end-to-end testbench:
- plfm_chirp_controller: TX/RX mixer enables now mutually exclusive
by FSM state (Fix#4), preventing simultaneous TX+RX activation
- usb_data_interface: stream control reset default 3'b001 (range-only),
added doppler/cfar data_pending sticky flags, write FSM triggers on
range_valid only — eliminates startup deadlock (Fix#5)
- radar_receiver_final: STM32 toggle signals wired through for mode-00
pass-through, dynamic frame detection via host_chirps_per_elev
- radar_system_top: STM32 toggle signal wiring to receiver instance
- chirp_memory_loader_param: explicit readmemh range for short chirp
Test infrastructure:
- New tb_system_e2e.v: 46 checks across 12 groups (reset, TX, safety,
RX, USB R/W, CDC, beam scanning, reset recovery, stream control,
latency budgets, watchdog)
- tb_usb_data_interface: Tests 21/22/56 updated for data_pending
architecture (preload flags, verify consumption instead of state)
- tb_chirp_controller: mixer tests T7.1/T7.2 updated for Fix#4
- run_regression.sh: PASS/FAIL regex fixed to match only [PASS]/[FAIL]
markers, added E2E test entry
- Updated rx_final_doppler_out.csv golden data
CDC fixes across 6 RTL files based on post-implementation report_cdc analysis:
- P0: sync stm32_mixers_enable and new_chirp_pulse to clk_120m via toggle CDC
in radar_transmitter, add ft601 reset synchronizer and USB holding
registers with proper edge detection in usb_data_interface
- P1: add ASYNC_REG to edge_detector, convert new_chirp_frame to toggle CDC,
fix USB valid edge detect to use fully-synced signal
- P2: register Gray encoding in cdc_adc_to_processing source domain, sync
ft601_txe and stm32_mixers_enable for status_reg in radar_system_top
- Safety: add in_bin_count overflow guard in range_bin_decimator to prevent
downstream BRAM corruption
All 13 regression test suites pass (159 individual tests).
- Expand ft601_be from [1:0] to [3:0] across RTL, top-level, testbenches,
and XDC (uncomment be[2:3] pin assignments B21/A21)
- Fix NCO XSim testbench: correct reset check (0x7FFF not 0), add pipeline
warmup and sample skip for DSP48E1 quadrature test
- All local regression tests pass (39/39 USB, 10/10 integration, all co-sim)
Resolve all 4 inter-clock timing violations found in Vivado synthesis
attempt #11 (WNS was -2.552 ns). Changes:
- Add reset synchronizer for clk_120m_dac domain (2-FF chain)
- Add Gray-code CDC for chirp_counter (6-bit, 120MHz->100MHz)
- Add single-bit CDC for new_chirp_frame (3-stage, 120MHz->100MHz)
- Add 2-stage input synchronizers for valid signals in USB module
(clk_100m->ft601_clk_in) with data capture on rising edge
- Fix ft601_clk_out multi-driven net (removed duplicate assignment)
- Update XDC: set_max_delay -datapath_only for CDC, false_path for reset
Result: Vivado attempt #12 passes with 0 errors, 0 timing violations,
and 'All user specified timing constraints are met.' (WNS +0.983 ns)