mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-08 22:47:16 +00:00
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:
@@ -35,9 +35,12 @@ USB transports + wire formats (these intentionally diverge):
|
||||
[0xAA][range_q 2B][range_i 2B][dop_re 2B][dop_im 2B][det 1B][0x55]
|
||||
where det byte = {frame_start, 6'b0, cfar_detection}.
|
||||
|
||||
Status (both transports, PR-G v2)
|
||||
[0xBB][7 x 32-bit status_words][0x55] = 30 B. status_words[6] carries
|
||||
2-tier-CFAR telemetry: {detect_count_cand[31:16], detect_threshold_soft[15:0]}.
|
||||
Status (FT2232H production, M-5)
|
||||
[0xBB][8 x 32-bit status_words][0x55] = 34 B. status_words[6] carries
|
||||
2-tier-CFAR telemetry (PR-G); status_words[7] carries
|
||||
{medium_chirp[31:16], medium_listen[15:0]} (M-5). Legacy FT601 path
|
||||
still emits the pre-PR-G 26-byte 6-word layout (no host code reads it
|
||||
today; the production transport is FT2232H).
|
||||
|
||||
RX (Host → FPGA, both transports)
|
||||
4 bytes per command: {opcode[7:0], addr[7:0], value[15:8], value[7:0]}.
|
||||
@@ -71,7 +74,7 @@ STATUS_HEADER_BYTE = 0xBB
|
||||
|
||||
# Packet sizes
|
||||
DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1 (FT601 legacy)
|
||||
STATUS_PACKET_SIZE = 30 # 1 + 28 + 1 (PR-G v2: 7 status_words)
|
||||
STATUS_PACKET_SIZE = 34 # 1 + 32 + 1 (M-5: 8 status_words; was 30 / PR-G 7 words)
|
||||
|
||||
NUM_RANGE_BINS = 512
|
||||
NUM_DOPPLER_BINS = 48 # PR-F/PR-Q: 3 sub-frames * 16 (= FPGA RP_NUM_DOPPLER_BINS)
|
||||
@@ -239,7 +242,7 @@ class RadarFrame:
|
||||
|
||||
@dataclass
|
||||
class StatusResponse:
|
||||
"""Parsed status response from FPGA (PR-G v2: 7-word / 30-byte packet)."""
|
||||
"""Parsed status response from FPGA (M-5: 8-word / 34-byte packet)."""
|
||||
radar_mode: int = 0
|
||||
stream_ctrl: int = 0
|
||||
cfar_threshold: int = 0
|
||||
@@ -265,6 +268,9 @@ class StatusResponse:
|
||||
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)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -337,9 +343,11 @@ class RadarProtocol:
|
||||
"""
|
||||
Parse a status response packet.
|
||||
|
||||
PR-G v2 format: [0xBB] [7 x 4B status_words] [0x55] = 1 + 28 + 1 = 30 bytes.
|
||||
Audit P-3: pre-PR-G GUI used 26 (six words); FPGA `STATUS_PKT_LEN=30` since
|
||||
PR-G added word[6] for 2-tier-CFAR telemetry.
|
||||
M-5 format: [0xBB] [8 x 4B status_words] [0x55] = 1 + 32 + 1 = 34 bytes.
|
||||
History: pre-PR-G was 26 B (6 words); PR-G bumped to 30 B (7 words) for
|
||||
2-tier-CFAR telemetry; M-5 bumped to 34 B (8 words) for medium_chirp /
|
||||
medium_listen readback (PR-G ran out of reserved bits in word 3 to fit
|
||||
a second 16-bit pair).
|
||||
"""
|
||||
if len(raw) < STATUS_PACKET_SIZE:
|
||||
return None
|
||||
@@ -347,7 +355,7 @@ class RadarProtocol:
|
||||
return None
|
||||
|
||||
words = []
|
||||
for i in range(7):
|
||||
for i in range(8):
|
||||
w = struct.unpack_from(">I", raw, 1 + i * 4)[0]
|
||||
words.append(w)
|
||||
|
||||
@@ -386,6 +394,9 @@ class RadarProtocol:
|
||||
# (saturated to 0xFFFF when the 17-bit RTL value exceeds 16-bit range).
|
||||
sr.detect_threshold_soft = words[6] & 0xFFFF
|
||||
sr.detect_count_cand = (words[6] >> 16) & 0xFFFF
|
||||
# Word 7 (M-5 MEDIUM PRI readback): {medium_chirp[31:16], medium_listen[15:0]}
|
||||
sr.medium_listen = words[7] & 0xFFFF
|
||||
sr.medium_chirp = (words[7] >> 16) & 0xFFFF
|
||||
return sr
|
||||
|
||||
@staticmethod
|
||||
@@ -565,10 +576,11 @@ class RadarProtocol:
|
||||
def find_bulk_frame_boundaries(buf: bytes) -> list[tuple[int, int, str]]:
|
||||
"""Scan a byte stream for FT2232H bulk frames and status packets.
|
||||
|
||||
Status packets (0xBB header, 26 B) are unchanged between transports
|
||||
— the WR_STATUS_SEND state in usb_data_interface_ft2232h.v emits the
|
||||
same layout as the legacy FT601 path. Bulk data frames (0xAA header)
|
||||
are variable length per `_bulk_frame_size_from_flags`.
|
||||
Status packets (0xBB header, 34 B post M-5) come from
|
||||
WR_STATUS_SEND in usb_data_interface_ft2232h.v. (Legacy FT601 path
|
||||
emits a different 26-byte status layout but is not used by current
|
||||
host code.) Bulk data frames (0xAA header) are variable length per
|
||||
`_bulk_frame_size_from_flags`.
|
||||
|
||||
Returns a list of (start, end, ptype) tuples like
|
||||
find_packet_boundaries, where ptype is "data" or "status". On a
|
||||
|
||||
@@ -133,8 +133,9 @@ 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,
|
||||
cand_count=0, thr_soft=0, frame_drop=0):
|
||||
"""Build a PR-G v2 30-byte status response matching FPGA format."""
|
||||
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."""
|
||||
pkt = bytearray()
|
||||
pkt.append(STATUS_HEADER_BYTE)
|
||||
|
||||
@@ -174,6 +175,10 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
w6 = ((cand_count & 0xFFFF) << 16) | (thr_soft & 0xFFFF)
|
||||
pkt += struct.pack(">I", w6)
|
||||
|
||||
# Word 7 (M-5 MEDIUM PRI readback): {medium_chirp[31:16], medium_listen[15:0]}
|
||||
w7 = ((medium_chirp & 0xFFFF) << 16) | (medium_listen & 0xFFFF)
|
||||
pkt += struct.pack(">I", w7)
|
||||
|
||||
pkt.append(FOOTER_BYTE)
|
||||
return bytes(pkt)
|
||||
|
||||
@@ -207,8 +212,9 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
self.assertEqual(sr.range_mode, 2)
|
||||
|
||||
def test_parse_status_too_short(self):
|
||||
# Anything under STATUS_PACKET_SIZE (30) must be rejected.
|
||||
self.assertIsNone(RadarProtocol.parse_status_packet(b"\xBB" + b"\x00" * 28))
|
||||
# Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected.
|
||||
# 33-byte input = header + 32 bytes (one short of valid).
|
||||
self.assertIsNone(RadarProtocol.parse_status_packet(b"\xBB" + b"\x00" * 32))
|
||||
|
||||
def test_parse_status_wrong_header(self):
|
||||
raw = self._make_status_packet()
|
||||
@@ -228,6 +234,34 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
self.assertEqual(sr.detect_count_cand, 0x0A5C)
|
||||
self.assertEqual(sr.detect_threshold_soft, 0x1234)
|
||||
|
||||
def test_parse_status_word7_medium_pri_readback(self):
|
||||
"""M-5: word[7] high-half = medium_chirp, low-half = medium_listen.
|
||||
|
||||
Closes the 161-µs MEDIUM PRI visibility gap left by PR-G (status word 3
|
||||
had only 10 reserved bits, not enough for a second 16-bit pair). Default
|
||||
production values: 500 cycles MEDIUM_CHIRP / 15600 cycles MEDIUM_LISTEN
|
||||
per RP_DEF_MEDIUM_*_CYCLES — picked here as the round-trip canary.
|
||||
"""
|
||||
raw = self._make_status_packet(medium_chirp=500, medium_listen=15600)
|
||||
sr = RadarProtocol.parse_status_packet(raw)
|
||||
self.assertIsNotNone(sr)
|
||||
self.assertEqual(sr.medium_chirp, 500)
|
||||
self.assertEqual(sr.medium_listen, 15600)
|
||||
# Defaults for unrelated fields must still be zero — guards against
|
||||
# bit-stealing into word 7 from neighbours.
|
||||
self.assertEqual(sr.detect_count_cand, 0)
|
||||
self.assertEqual(sr.detect_threshold_soft, 0)
|
||||
# Packet length matches new STATUS_PACKET_SIZE.
|
||||
self.assertEqual(len(raw), STATUS_PACKET_SIZE)
|
||||
self.assertEqual(STATUS_PACKET_SIZE, 34)
|
||||
|
||||
def test_parse_status_word7_max_values(self):
|
||||
"""M-5: 16-bit max in both halves of word 7 round-trips clean (no overflow)."""
|
||||
raw = self._make_status_packet(medium_chirp=0xFFFF, medium_listen=0xFFFF)
|
||||
sr = RadarProtocol.parse_status_packet(raw)
|
||||
self.assertEqual(sr.medium_chirp, 0xFFFF)
|
||||
self.assertEqual(sr.medium_listen, 0xFFFF)
|
||||
|
||||
def test_parse_status_word4_layout_co_spec(self):
|
||||
"""GUI-S3: pin status word 4 bit positions to the FPGA word builder.
|
||||
|
||||
@@ -326,11 +360,11 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
self.assertEqual(sr.self_test_detail, 0)
|
||||
self.assertEqual(sr.self_test_busy, 0)
|
||||
|
||||
def test_status_packet_is_30_bytes(self):
|
||||
"""PR-G v2: status packet is 30 bytes (1 + 7*4 + 1)."""
|
||||
def test_status_packet_is_34_bytes(self):
|
||||
"""M-5: status packet is 34 bytes (1 + 8*4 + 1; was 30 / PR-G 7 words)."""
|
||||
raw = self._make_status_packet()
|
||||
self.assertEqual(len(raw), STATUS_PACKET_SIZE)
|
||||
self.assertEqual(len(raw), 30)
|
||||
self.assertEqual(len(raw), 34)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Boundary detection
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user