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.