feat(fpga,gui): PR-AC.1 — M-5 status pkt 30→34 B for medium_chirp/medium_listen readback

Closes the 161-µs MEDIUM PRI visibility gap from the 2026-05-02 e2e audit.
PR-G ran out of reserved bits in status_words[3] to fit a second 16-bit
pair, so this PR adds status_words[7] = {medium_chirp[15:0], medium_listen[15:0]}
and bumps STATUS_PKT_LEN 30→34, STATUS_PACKET_SIZE 30→34.

RTL (usb_data_interface_ft2232h.v):
  - Two new input ports `status_medium_chirp` / `status_medium_listen`.
  - status_words array bound 6→7, init loop 7→8, snapshot block packs word 7.
  - STATUS_PKT_LEN width 5→6 bits to hold 34, byte-mux index widened
    [4:0]→[5:0] with 4 new entries for word 7 + footer at index 33.
  - Header docstring + length-localparam comment refreshed.

Top-level (radar_system_top.v): wire `host_medium_chirp_cycles` /
`host_medium_listen_cycles` into the FT2232H usb_inst (gen_ft2232h branch
only; legacy FT601 path retains its pre-PR-G 6-word / 26-byte layout —
no host code reads it today).

Host parser (radar_protocol.py):
  - STATUS_PACKET_SIZE 30→34. Module docstring + parse_status_packet
    docstring + find_bulk_frame_boundaries note refreshed.
  - StatusResponse gains `medium_chirp` / `medium_listen` fields.
  - Word-loop bounds 7→8; word-7 decode added.

Tests:
  - tb_usb_protocol_v2 — drive default RP_DEF_MEDIUM_*_CYCLES (500/15600),
    move T3.2 footer assertion 29→33, add T3.8..T3.11 for word-7 bytes.
    Manual run: 31/31 PASS (TB not in run_regression.sh).
  - tb_ft2232h_frame_drop, tb_e2e_dsp_to_host — tie new DUT ports to
    16'd0 (these TBs don't exercise status reads).
  - test_GUI_V65_Tk — extend _make_status_packet builder kwargs, rename
    test_status_packet_is_30_bytes → _is_34_bytes, add round-trip +
    16-bit-max tests for word 7.
  - test_v7 — TestStatusPacketV2RoundTrip rewired for 8 words / 34 B,
    add test_word7_medium_pri_decoded + test_pre_M5_30byte_packet_rejected.

Verification:
  - FPGA regression (iverilog + xsim cosim): 42/0/0.
  - tb_usb_protocol_v2 standalone: 31/0.
  - Host test_GUI_V65_Tk: 119/119.
  - Host test_v7: 152/152.
This commit is contained in:
Jason
2026-05-07 15:20:09 +05:45
parent a19359aeab
commit 21bf7a0228
8 changed files with 187 additions and 75 deletions
+27 -8
View File
@@ -549,33 +549,44 @@ class TestSubframeEnableRoundTrip(TestBulkFrameV2RoundTrip):
class TestStatusPacketV2RoundTrip(unittest.TestCase):
"""PR-G v2 status packet: 7 status_words / 30 bytes."""
"""M-5 status packet: 8 status_words / 34 bytes."""
def _build_status(self, words: list[int]) -> bytes:
from radar_protocol import STATUS_HEADER_BYTE, FOOTER_BYTE
assert len(words) == 7
assert len(words) == 8
body = b"".join(struct.pack(">I", w & 0xFFFFFFFF) for w in words)
return bytes([STATUS_HEADER_BYTE]) + body + bytes([FOOTER_BYTE])
def test_size_is_30(self):
def test_size_is_34(self):
from radar_protocol import STATUS_PACKET_SIZE
self.assertEqual(STATUS_PACKET_SIZE, 30)
pkt = self._build_status([0] * 7)
self.assertEqual(len(pkt), 30)
self.assertEqual(STATUS_PACKET_SIZE, 34)
pkt = self._build_status([0] * 8)
self.assertEqual(len(pkt), 34)
def test_word6_telemetry_decoded(self):
"""word[6] = {detect_count_cand[31:16], detect_threshold_soft[15:0]}"""
from radar_protocol import RadarProtocol
word6 = (0x1234 << 16) | 0xABCD # cand=0x1234, thr_soft=0xABCD
pkt = self._build_status([0, 0, 0, 0, 0, 0, word6])
pkt = self._build_status([0, 0, 0, 0, 0, 0, word6, 0])
sr = RadarProtocol.parse_status_packet(pkt)
self.assertIsNotNone(sr)
self.assertEqual(sr.detect_count_cand, 0x1234)
self.assertEqual(sr.detect_threshold_soft, 0xABCD)
def test_word7_medium_pri_decoded(self):
"""M-5: word[7] = {medium_chirp[31:16], medium_listen[15:0]}"""
from radar_protocol import RadarProtocol
# Production defaults: 500 chirp / 15600 listen (RP_DEF_MEDIUM_*).
word7 = (500 << 16) | 15600
pkt = self._build_status([0, 0, 0, 0, 0, 0, 0, word7])
sr = RadarProtocol.parse_status_packet(pkt)
self.assertIsNotNone(sr)
self.assertEqual(sr.medium_chirp, 500)
self.assertEqual(sr.medium_listen, 15600)
def test_short_packet_returns_none(self):
from radar_protocol import RadarProtocol
pkt = self._build_status([0] * 7)
pkt = self._build_status([0] * 8)
self.assertIsNone(RadarProtocol.parse_status_packet(pkt[:25]))
def test_pre_PR_G_26byte_packet_rejected(self):
@@ -587,6 +598,14 @@ class TestStatusPacketV2RoundTrip(unittest.TestCase):
self.assertEqual(len(old_pkt), 26)
self.assertIsNone(RadarProtocol.parse_status_packet(old_pkt))
def test_pre_M5_30byte_packet_rejected(self):
"""Pre-M-5 30-byte (PR-G 7-word) status packets must reject after the bump."""
from radar_protocol import RadarProtocol, STATUS_HEADER_BYTE, FOOTER_BYTE
old_pkt = (bytes([STATUS_HEADER_BYTE])
+ b"\x00" * 28 + bytes([FOOTER_BYTE]))
self.assertEqual(len(old_pkt), 30)
self.assertIsNone(RadarProtocol.parse_status_packet(old_pkt))
# =============================================================================
# Test: v7.__init__ — clean exports