mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-08-07 19:23:11 +00:00
PR-AB.b expanded commit 5: Beam-ready handshake (RTL + MCU + GUI)
Wire a per-frame MCU→FPGA "beam pattern ready" handshake so the chirp scheduler can stall between 48-chirp frames until the MCU finishes writing the next ADAR1000 pattern. The legacy unused stm32_new_chirp input on PD8 is repurposed as stm32_beam_ready; chirp_scheduler.v gets a new S_BEAM_WAIT state entered after each frame_pulse and an 80 ms watchdog so a missed MCU toggle degrades to wall-clock cadence with a sticky telemetry bit rather than stalling the radar. Cold-reset defaults the handshake off (host_handshake_enable=0, new opcode 0x1A); the GUI opts in once the MCU PD8 wiring is verified on the bench. Both the FT601 and FT2232H status word 4 paths get the new beam_handshake_watchdog_fired sticky at bit [1] (reclaimed from the range_mode retirement in commit 1). RTL: - chirp_scheduler.v: 2-FF ASYNC_REG sync on beam_ready_async; 1-cycle edge detect (any transition, MCU side uses HAL_GPIO_TogglePin); new S_BEAM_WAIT state entered at frame_pulse when host_handshake_enable=1; 23-bit beam_watchdog counter with BEAM_WATCHDOG_MAX = 8_000_000 (~80 ms at 100 MHz, ~10 nominal frames); beam_handshake_watchdog_fired output sticky across mixers_enable cycles, cleared only by reset_n; mid-wait disable releases the FSM so dropping the opcode never strands the radar between frames. - radar_receiver_final.v: thread stm32_beam_ready_async + host_handshake_enable + beam_handshake_watchdog_fired through the scheduler instance. - radar_system_top.v: rename input port stm32_new_chirp → stm32_beam_ready; add host_handshake_enable register (cold-reset = 1'b0); opcode 0x1A dispatch (value[0]); add rx_beam_handshake_watchdog wire; pack into status_words[4][1] in both USB paths. - radar_system_top_50t.v: rename wrapper port + sub-instance wiring. - usb_data_interface.v + usb_data_interface_ft2232h.v: add status_beam_handshake_watchdog input + 2-FF level CDC (same convention as F-6.4 / F-1.2 stickies); refresh word-4 layout doc comment; pack beam_handshake_wd_sync_1 into status_words[4][1]. XDC: - xc7a50t_ftg256.xdc + xc7a200t_fbg484.xdc: rename stm32_new_chirp port references to stm32_beam_ready (same PD8 pin, F13 on 50T / L18 on 200T). MCU: - main.h: add FPGA_BEAM_READY_Pin = GPIO_PIN_8 + FPGA_BEAM_READY_GPIO_Port = GPIOD alongside the existing FPGA_FRAME_PULSE alias. - main.cpp:runRadarPulseSequence: insert HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_8) after each setCustomBeamPattern16(RX) — once after the per-azimuth broadside (vector_0), once after matrix1, once after matrix2 — between the SPI burst completion and waitForFramePulse. GUI: - radar_protocol.py: Opcode.HANDSHAKE_ENABLE = 0x1A; StatusResponse.beam_handshake_watchdog = 0 default; parse word 4 bit [1] in parse_status_packet; update word-4 layout comment. - test_GUI_V65_Tk.py: add beam_handshake_watchdog kwarg to _make_status_packet (sets bit [1] of word 4); refresh test_parse_status_word4_layout_co_spec to cover the new bit (used+9=32); add test_parse_status_beam_handshake_watchdog round-trip; test_handshake_enable_opcode pins 0x1A; defaults / chirps_mismatch / agc-coexist tests gain a watchdog==0 assertion; bump test_all_rtl_opcodes_present expected set to include 0x17/0x18/0x19/0x1A. TB: - new tb_chirp_scheduler_handshake.v (16 checks): legacy open-loop, edge exit (rising + falling), 200-cycle idle hold, watchdog auto-advance via force on dut.beam_watchdog, sticky-survives-mixers_disable, mid-wait disable release, reset_n clears sticky. - run_regression.sh: register the new TB in PHASE 1. - tb_radar_receiver_final.v: tie the 3 new receiver ports off (beam_ready_async=0, handshake_enable=0, watchdog unconnected). - tb_system_mechanics.v / tb_system_opcodes.v: explicit .stm32_beam_ready(1'b0) connection (the cold-reset host_handshake_enable=0 keeps the FSM out of S_BEAM_WAIT). - tb_usb_data_interface.v / tb_usb_protocol_v2.v / tb_e2e_dsp_to_host.v / tb_ft2232h_frame_drop.v: tie .status_beam_handshake_watchdog(1'b0). Ride-along ruff sweep (14 → 0 across the repo): - tb/cosim/compare_independent.py: RUF003 — '5×' → 'at least 5x'. - tb/cosim/gen_e2e_expected.py: noqa: E402 on the post-sys.path import; drop unused EXPECTED_RANGE_BIN + EXPECTED_DOPPLER_BIN_PER_SF imports; fold the detect-class slot if/else into a ternary (SIM108). - tb/cosim/gen_e2e_stimulus.py: drop int() wrapping round() at four call sites (RUF046 — round() already returns int in Python 3); rewrite the range-bin derivation comment block from code-like `# range_bin = ...` to prose (ERA001); strip stray f from placeholder-free error string (F541). - tb/cosim/tb_e2e_dsp_to_host_parse.py: open(path, 'r') → open(path) (UP015). - v7/dashboard.py: '3×' → '3x' (RUF003); drop quotes from 'StatusResponse | None' annotation (UP037, file already has `from __future__ import annotations`). CI summary (all suites green pre-commit): - ruff: All checks passed! - FPGA regression (iverilog): 43 / 0 / 0 (incl. new handshake TB 16/16). - MCU tests: 51 / 0 + 34 / 0 + 13 / 13 ADAR1000_AGC. - GUI Tk (test_GUI_V65_Tk): 120 / 0. - GUI v7 (test_v7): 152 / 0. Production rollout note: bitstream cold-resets with host_handshake_enable=0 so existing flashes keep their open-loop cadence until the GUI sends opcode 0x1A=1. Once enabled, the per-pattern dwell tracks both the chirp ladder (PD14 frame_pulse from commit-3 work) and the MCU pattern-write completion (PD8 toggle from this commit), eliminating drift from the SPI burst timing.
This commit is contained in:
@@ -136,6 +136,7 @@ class Opcode(IntEnum):
|
||||
0x17 host_medium_chirp_cycles (PR-G G2)
|
||||
0x18 host_medium_listen_cycles (PR-G G2)
|
||||
0x19 host_subframe_enable (PR-U / M-8 — 3-bit {LONG, MED, SHORT} mask)
|
||||
0x1A host_handshake_enable (PR-AB.b expanded commit 5 — beam-ready stall)
|
||||
|
||||
PR-AB.b expanded retired opcodes 0x01 (host_radar_mode),
|
||||
0x02 (host_trigger_pulse), 0x20 (host_range_mode).
|
||||
@@ -165,6 +166,12 @@ class Opcode(IntEnum):
|
||||
# host CRT downgrades confidence to UNKNOWN (dbin // 16 attribution would
|
||||
# otherwise be wrong when the scheduler skips a sub-frame).
|
||||
SUBFRAME_ENABLE = 0x19
|
||||
# PR-AB.b expanded commit 5: beam-ready handshake enable. value[0]=1 makes
|
||||
# chirp_scheduler stall in S_BEAM_WAIT after frame_pulse until the MCU
|
||||
# toggles PD8, or the ~80 ms watchdog expires (status word 4 bit [1] is
|
||||
# the sticky watchdog flag). FPGA cold-reset = 0 — host opts in once the
|
||||
# MCU PD8 wiring is verified on the bench.
|
||||
HANDSHAKE_ENABLE = 0x1A
|
||||
|
||||
# --- Signal processing (0x21-0x27;
|
||||
# 0x20 host_range_mode retired in PR-AB.b expanded) ---
|
||||
@@ -260,14 +267,20 @@ class StatusResponse:
|
||||
agc_saturation_count: int = 0 # 8-bit saturation count [7:0]
|
||||
agc_enable: int = 0 # 1-bit AGC enable readback
|
||||
chirps_mismatch: int = 0 # TX-G: 1 if FPGA clamped/rejected host chirps_per_elev
|
||||
# PR-AB.b expanded commit 5: sticky watchdog from chirp_scheduler S_BEAM_WAIT.
|
||||
# 1 means at least one frame elapsed without an MCU PD8 ack within ~80 ms;
|
||||
# cleared only by full reset_n on the FPGA. Reserved 0 when host has not
|
||||
# enabled the handshake (opcode 0x1A=1).
|
||||
beam_handshake_watchdog: int = 0 # word 4 bit [1]
|
||||
# PR-G 2-tier CFAR telemetry (word 6)
|
||||
detect_count_cand: int = 0 # 16-bit count of CAND-tier detections per frame
|
||||
detect_threshold_soft: int = 0 # 16-bit soft-CFAR threshold readback (saturates 0xFFFF)
|
||||
# AUDIT-S10 control-fault flags (word 5 high half)
|
||||
frame_drop_count: int = 0 # frame-drop counter from RTL
|
||||
# M-5 MEDIUM PRI readback (word 7) — closes 161-µs MEDIUM visibility gap.
|
||||
medium_chirp: int = 0 # opcode 0x17 readback (16-bit, default RP_DEF_MEDIUM_CHIRP_CYCLES)
|
||||
medium_listen: int = 0 # opcode 0x18 readback (16-bit, default RP_DEF_MEDIUM_LISTEN_CYCLES)
|
||||
# opcode 0x17/0x18 readback (16-bit each, default RP_DEF_MEDIUM_*_CYCLES).
|
||||
medium_chirp: int = 0
|
||||
medium_listen: int = 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -373,9 +386,12 @@ class RadarProtocol:
|
||||
# Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]}
|
||||
sr.chirps_per_elev = words[3] & 0x3F
|
||||
sr.short_listen = (words[3] >> 16) & 0xFFFF
|
||||
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] reserved[1:0]
|
||||
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
|
||||
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11]
|
||||
# mismatch[10] alpha_soft[9:2] beam_handshake_watchdog[1] reserved[0]
|
||||
# PR-AB.b expanded commit 5 reclaimed bit [1] for the handshake watchdog
|
||||
# sticky (FPGA chirp_scheduler S_BEAM_WAIT, ~80 ms timeout).
|
||||
sr.chirps_mismatch = (words[4] >> 10) & 0x01
|
||||
sr.beam_handshake_watchdog = (words[4] >> 1) & 0x01
|
||||
sr.agc_enable = (words[4] >> 11) & 0x01
|
||||
sr.agc_saturation_count = (words[4] >> 12) & 0xFF
|
||||
sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF
|
||||
|
||||
@@ -133,6 +133,7 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
st_flags=0, st_detail=0, st_busy=0,
|
||||
agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0,
|
||||
chirps_mismatch=0,
|
||||
beam_handshake_watchdog=0,
|
||||
cand_count=0, thr_soft=0, frame_drop=0,
|
||||
medium_chirp=0, medium_listen=0):
|
||||
"""Build an M-5 34-byte status response matching FPGA format."""
|
||||
@@ -156,13 +157,18 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F)
|
||||
pkt += struct.pack(">I", w3)
|
||||
|
||||
# Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0],
|
||||
# agc_saturation_count[7:0], agc_enable,
|
||||
# chirps_mismatch[10], 10'd0 reserved [9:0]}
|
||||
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
|
||||
# Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20],
|
||||
# agc_saturation_count[19:12], agc_enable[11],
|
||||
# chirps_mismatch[10], reserved[9:2],
|
||||
# beam_handshake_watchdog[1], reserved[0]}
|
||||
# PR-AB.b expanded commit 5: bit [1] is the chirp_scheduler
|
||||
# S_BEAM_WAIT sticky watchdog flag (reset_n clear). The 8-bit
|
||||
# alpha_soft echo at [9:2] is FT2232H-only; this co-spec
|
||||
# builder leaves [9:2] reserved-0 like the FT601 path.
|
||||
w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) |
|
||||
((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) |
|
||||
((chirps_mismatch & 0x01) << 10))
|
||||
((chirps_mismatch & 0x01) << 10) |
|
||||
((beam_handshake_watchdog & 0x01) << 1))
|
||||
pkt += struct.pack(">I", w4)
|
||||
|
||||
# Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0,
|
||||
@@ -196,6 +202,7 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
self.assertEqual(sr.short_listen, 17450)
|
||||
self.assertEqual(sr.chirps_per_elev, 32)
|
||||
self.assertEqual(sr.chirps_mismatch, 0)
|
||||
self.assertEqual(sr.beam_handshake_watchdog, 0)
|
||||
|
||||
def test_parse_status_chirps_mismatch(self):
|
||||
# TX-G: bit 10 of word 4 must round-trip without disturbing neighbours.
|
||||
@@ -203,6 +210,16 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
sr = RadarProtocol.parse_status_packet(raw)
|
||||
self.assertEqual(sr.chirps_mismatch, 1)
|
||||
self.assertEqual(sr.agc_enable, 1)
|
||||
self.assertEqual(sr.beam_handshake_watchdog, 0)
|
||||
|
||||
def test_parse_status_beam_handshake_watchdog(self):
|
||||
# PR-AB.b expanded commit 5: bit 1 of word 4 must round-trip without
|
||||
# bleeding into chirps_mismatch (bit 10) or the reserved [0] bit.
|
||||
raw = self._make_status_packet(beam_handshake_watchdog=1)
|
||||
sr = RadarProtocol.parse_status_packet(raw)
|
||||
self.assertEqual(sr.beam_handshake_watchdog, 1)
|
||||
self.assertEqual(sr.chirps_mismatch, 0)
|
||||
self.assertEqual(sr.agc_enable, 0)
|
||||
|
||||
def test_parse_status_too_short(self):
|
||||
# Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected.
|
||||
@@ -262,14 +279,20 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
usb_data_interface_ft2232h.v:675-679 — exactly one source of truth
|
||||
in this test, so any future drift between FPGA and GUI trips here:
|
||||
|
||||
[31:28] agc_current_gain (4-bit)
|
||||
[27:20] agc_peak_magnitude (8-bit)
|
||||
[19:12] agc_saturation_count (8-bit)
|
||||
[11] agc_enable (1-bit)
|
||||
[10] chirps_mismatch (1-bit, TX-G)
|
||||
[9:0] reserved (10 bits, must be zero from builder)
|
||||
(was [9:2] + range_mode[1:0]; range_mode retired in
|
||||
PR-AB.b expanded)
|
||||
[31:28] agc_current_gain (4-bit)
|
||||
[27:20] agc_peak_magnitude (8-bit)
|
||||
[19:12] agc_saturation_count (8-bit)
|
||||
[11] agc_enable (1-bit)
|
||||
[10] chirps_mismatch (1-bit, TX-G)
|
||||
[9:2] reserved / alpha_soft (8 bits — FT601 leaves zero; FT2232H
|
||||
echoes host_cfar_alpha_soft. This
|
||||
co-spec builder uses the FT601
|
||||
layout so the builder remains
|
||||
path-agnostic.)
|
||||
[1] beam_handshake_watchdog (1-bit sticky, PR-AB.b expanded
|
||||
commit 5)
|
||||
[0] reserved (1-bit; was range_mode[0], retired
|
||||
in PR-AB.b expanded)
|
||||
|
||||
For each field we set ONLY that field to its max, build the packet,
|
||||
parse, and assert (a) the field reads back correctly and (b) every
|
||||
@@ -278,16 +301,19 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
"""
|
||||
layout = [
|
||||
# (field_name, builder_kwarg, lsb, width, parsed_attr)
|
||||
("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"),
|
||||
("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"),
|
||||
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
|
||||
("agc_enable", "agc_enable", 11, 1, "agc_enable"),
|
||||
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
|
||||
("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"),
|
||||
("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"),
|
||||
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
|
||||
("agc_enable", "agc_enable", 11, 1, "agc_enable"),
|
||||
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
|
||||
("beam_handshake_watchdog", "beam_handshake_watchdog",
|
||||
1, 1, "beam_handshake_watchdog"),
|
||||
]
|
||||
# Sanity: layout fields + reserved [9:0] must cover exactly 32 bits.
|
||||
# Sanity: layout fields + reserved [9:2] (8 bits) + reserved [0] (1 bit)
|
||||
# must cover exactly 32 bits.
|
||||
used = sum(width for _, _, _, width, _ in layout)
|
||||
self.assertEqual(used + 10, 32,
|
||||
"word 4 layout (incl. reserved [9:0]) must total 32 bits")
|
||||
self.assertEqual(used + 9, 32,
|
||||
"word 4 layout (incl. reserved [9:2] + [0]) must total 32 bits")
|
||||
|
||||
# No two fields may overlap.
|
||||
occupied = set()
|
||||
@@ -1002,6 +1028,10 @@ class TestOpcodeEnum(unittest.TestCase):
|
||||
self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03)
|
||||
self.assertEqual(Opcode.STREAM_CONTROL, 0x04)
|
||||
|
||||
def test_handshake_enable_opcode(self):
|
||||
"""PR-AB.b expanded commit 5: beam-ready handshake opcode = 0x1A."""
|
||||
self.assertEqual(Opcode.HANDSHAKE_ENABLE, 0x1A)
|
||||
|
||||
def test_all_rtl_opcodes_present(self):
|
||||
"""Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.
|
||||
|
||||
@@ -1010,6 +1040,7 @@ class TestOpcodeEnum(unittest.TestCase):
|
||||
"""
|
||||
expected = {0x03, 0x04,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0x1A,
|
||||
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
|
||||
0x28, 0x29, 0x2A, 0x2B, 0x2C,
|
||||
0x30, 0x31, 0xFF}
|
||||
|
||||
@@ -994,7 +994,7 @@ class RadarDashboard(QMainWindow):
|
||||
spin.setValue(default)
|
||||
# PR-AB.b: setFixedWidth (not min/max) — QHBoxLayout would otherwise
|
||||
# squeeze the spinbox toward its minimum on rows where the hint is
|
||||
# longer than its peers (the AGC Enable hint is ~3× longer than the
|
||||
# longer than its peers (the AGC Enable hint is ~3x longer than the
|
||||
# others and was rendering at ~90 px while siblings hit ~160).
|
||||
spin.setFixedWidth(120)
|
||||
row.addWidget(spin)
|
||||
@@ -2119,7 +2119,7 @@ class RadarDashboard(QMainWindow):
|
||||
# the last StatusResponse if any, otherwise the static defaults.
|
||||
self._refresh_agc_mode_labels(self._last_status)
|
||||
|
||||
def _refresh_agc_mode_labels(self, st: "StatusResponse | None"):
|
||||
def _refresh_agc_mode_labels(self, st: StatusResponse | None):
|
||||
"""Update the AGC enable text on both the FPGA Control Status box
|
||||
(self._agc_labels['enable']) and the AGC Monitor strip
|
||||
(self._agc_mode_lbl). In production the firmware ignores the FPGA
|
||||
|
||||
Reference in New Issue
Block a user