PR-AB.b expanded commit 4: GUI cleanup (modes / range_mode strip)

Strip the host-side parser/dashboard/test references for the FPGA
registers retired in commit 1: host_radar_mode (opcode 0x01),
host_trigger_pulse (opcode 0x02), and host_range_mode (opcode 0x20).
The v7 backend (models.py / software_fpga.py / processing.py) had no
references — only the parser, dashboard, and Tk test file did.

- radar_protocol.py: drop Opcode.RADAR_MODE / TRIGGER_PULSE / RANGE_MODE
  enum members; rebuild the doc table around the surviving opcodes; drop
  StatusResponse.radar_mode and StatusResponse.range_mode fields; drop
  the two parse-status assignments (sr.radar_mode = words[0]>>22 and
  sr.range_mode = words[4] & 0x03); update the layout comments on words
  0 + 4 to mark the freed bits as reserved-0. Acquisition loop log line
  switches from "mode=X stream=Y" to "stream=Y chirps/elev=Z" — radar
  mode is no longer a runtime concept.
- v7/dashboard.py: delete the "Radar Mode Off" (0x01) and "Trigger Chirp"
  (0x02) QPushButtons from the operations group; remove "Mode:" and
  "Range Mode:" fields from _update_status_display.
- test_GUI_V65_Tk.py: drop mode + range_mode kwargs from the
  _make_status_packet helper; update the word-4 layout co-spec test
  (range_mode entry deleted, reserved span widened from [9:2] to [9:0],
  sanity-check sum bumped from used+8 to used+10); delete
  test_parse_status_range_mode + test_radar_mode_names; rename
  test_agc_and_range_mode_coexist → test_agc_fields_coexist_with_mismatch
  with chirps_mismatch replacing range_mode as the coexisting field;
  drop 0x01 / 0x02 / 0x20 from test_all_rtl_opcodes_present's expected
  set.

GUI tests: 118/0 (test_GUI_V65_Tk) + 152/0 (test_v7). FPGA regression
unchanged at 42/0/0.
This commit is contained in:
Jason
2026-05-11 11:11:49 +05:45
parent fd6036b49b
commit 2e2c10baeb
3 changed files with 56 additions and 65 deletions
+19 -22
View File
@@ -124,24 +124,23 @@ class Opcode(IntEnum):
"""Host register opcodes — must match radar_system_top.v case(usb_cmd_opcode). """Host register opcodes — must match radar_system_top.v case(usb_cmd_opcode).
FPGA truth table (from radar_system_top.v opcode dispatch case-block): FPGA truth table (from radar_system_top.v opcode dispatch case-block):
0x01 host_radar_mode 0x20 host_range_mode 0x03 host_detect_threshold 0x21-0x27 CFAR / MTI / DC-notch
0x02 host_trigger_pulse 0x21-0x27 CFAR / MTI / DC-notch 0x04 host_stream_control 0x28-0x2C AGC control
0x03 host_detect_threshold 0x28-0x2C AGC control 0x10 host_long_chirp_cycles 0x2D host_cfar_alpha_soft
0x04 host_stream_control 0x2D host_cfar_alpha_soft 0x11 host_long_listen_cycles 0x30 host_self_test_trigger
0x10 host_long_chirp_cycles 0x30 host_self_test_trigger 0x12 host_guard_cycles 0x31/0xFF host_status_request
0x11 host_long_listen_cycles 0x31/0xFF host_status_request 0x13 host_short_chirp_cycles 0x32 host_adc_pwdn
0x12 host_guard_cycles 0x32 host_adc_pwdn 0x14 host_short_listen_cycles 0x33 host_adc_format
0x13 host_short_chirp_cycles 0x33 host_adc_format
0x14 host_short_listen_cycles
0x15 host_chirps_per_elev 0x15 host_chirps_per_elev
0x16 host_gain_shift 0x16 host_gain_shift
0x17 host_medium_chirp_cycles (PR-G G2) 0x17 host_medium_chirp_cycles (PR-G G2)
0x18 host_medium_listen_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) 0x19 host_subframe_enable (PR-U / M-8 — 3-bit {LONG, MED, SHORT} mask)
PR-AB.b expanded retired opcodes 0x01 (host_radar_mode),
0x02 (host_trigger_pulse), 0x20 (host_range_mode).
""" """
# --- Basic control (0x01-0x04) --- # --- Basic control (0x03-0x04) ---
RADAR_MODE = 0x01 # 2-bit mode select
TRIGGER_PULSE = 0x02 # self-clearing one-shot trigger
DETECT_THRESHOLD = 0x03 # 16-bit detection threshold value DETECT_THRESHOLD = 0x03 # 16-bit detection threshold value
STREAM_CONTROL = 0x04 # 6-bit stream enable mask (FPGA: usb_cmd_value[5:0]) STREAM_CONTROL = 0x04 # 6-bit stream enable mask (FPGA: usb_cmd_value[5:0])
@@ -167,8 +166,8 @@ class Opcode(IntEnum):
# otherwise be wrong when the scheduler skips a sub-frame). # otherwise be wrong when the scheduler skips a sub-frame).
SUBFRAME_ENABLE = 0x19 SUBFRAME_ENABLE = 0x19
# --- Signal processing (0x20-0x27) --- # --- Signal processing (0x21-0x27;
RANGE_MODE = 0x20 # 0x20 host_range_mode retired in PR-AB.b expanded) ---
CFAR_GUARD = 0x21 CFAR_GUARD = 0x21
CFAR_TRAIN = 0x22 CFAR_TRAIN = 0x22
CFAR_ALPHA = 0x23 CFAR_ALPHA = 0x23
@@ -243,7 +242,6 @@ class RadarFrame:
@dataclass @dataclass
class StatusResponse: class StatusResponse:
"""Parsed status response from FPGA (M-5: 8-word / 34-byte packet).""" """Parsed status response from FPGA (M-5: 8-word / 34-byte packet)."""
radar_mode: int = 0
stream_ctrl: int = 0 stream_ctrl: int = 0
cfar_threshold: int = 0 cfar_threshold: int = 0
long_chirp: int = 0 long_chirp: int = 0
@@ -252,7 +250,6 @@ class StatusResponse:
short_chirp: int = 0 short_chirp: int = 0
short_listen: int = 0 short_listen: int = 0
chirps_per_elev: int = 0 chirps_per_elev: int = 0
range_mode: int = 0
# Self-test results (word 5, added in Build 26) # Self-test results (word 5, added in Build 26)
self_test_flags: int = 0 # 5-bit result flags [4:0] self_test_flags: int = 0 # 5-bit result flags [4:0]
self_test_detail: int = 0 # 8-bit detail code [7:0] self_test_detail: int = 0 # 8-bit detail code [7:0]
@@ -363,10 +360,10 @@ class RadarProtocol:
return None return None
sr = StatusResponse() sr = StatusResponse()
# Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} # Word 0: {0xFF[31:24], reserved[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
# PR-AB.b expanded: bits [23:22] formerly radar_mode, now reserved 0.
sr.cfar_threshold = words[0] & 0xFFFF sr.cfar_threshold = words[0] & 0xFFFF
sr.stream_ctrl = (words[0] >> 19) & 0x07 sr.stream_ctrl = (words[0] >> 19) & 0x07
sr.radar_mode = (words[0] >> 22) & 0x03
# Word 1: {long_chirp[31:16], long_listen[15:0]} # Word 1: {long_chirp[31:16], long_listen[15:0]}
sr.long_listen = words[1] & 0xFFFF sr.long_listen = words[1] & 0xFFFF
sr.long_chirp = (words[1] >> 16) & 0xFFFF sr.long_chirp = (words[1] >> 16) & 0xFFFF
@@ -376,8 +373,8 @@ class RadarProtocol:
# Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]} # Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]}
sr.chirps_per_elev = words[3] & 0x3F sr.chirps_per_elev = words[3] & 0x3F
sr.short_listen = (words[3] >> 16) & 0xFFFF sr.short_listen = (words[3] >> 16) & 0xFFFF
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] mode[1:0] # Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] reserved[1:0]
sr.range_mode = words[4] & 0x03 # PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
sr.chirps_mismatch = (words[4] >> 10) & 0x01 sr.chirps_mismatch = (words[4] >> 10) & 0x01
sr.agc_enable = (words[4] >> 11) & 0x01 sr.agc_enable = (words[4] >> 11) & 0x01
sr.agc_saturation_count = (words[4] >> 12) & 0xFF sr.agc_saturation_count = (words[4] >> 12) & 0xFF
@@ -1144,8 +1141,8 @@ class RadarAcquisition(threading.Thread):
elif ptype == "status": elif ptype == "status":
status = RadarProtocol.parse_status_packet(raw[start:end]) status = RadarProtocol.parse_status_packet(raw[start:end])
if status is not None: if status is not None:
log.info(f"Status: mode={status.radar_mode} " log.info(f"Status: stream={status.stream_ctrl} "
f"stream={status.stream_ctrl}") f"chirps/elev={status.chirps_per_elev}")
if status.self_test_busy or status.self_test_flags: if status.self_test_busy or status.self_test_flags:
log.info(f"Self-test: busy={status.self_test_busy} " log.info(f"Self-test: busy={status.self_test_busy} "
f"flags=0b{status.self_test_flags:05b} " f"flags=0b{status.self_test_flags:05b} "
+32 -33
View File
@@ -126,10 +126,10 @@ class TestRadarProtocol(unittest.TestCase):
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# Status packet parsing # Status packet parsing
# ---------------------------------------------------------------- # ----------------------------------------------------------------
def _make_status_packet(self, mode=1, stream=7, threshold=10000, def _make_status_packet(self, stream=7, threshold=10000,
long_chirp=3000, long_listen=13700, long_chirp=3000, long_listen=13700,
guard=17540, short_chirp=50, guard=17540, short_chirp=50,
short_listen=17450, chirps=32, range_mode=0, short_listen=17450, chirps=32,
st_flags=0, st_detail=0, st_busy=0, st_flags=0, st_detail=0, st_busy=0,
agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0, agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0,
chirps_mismatch=0, chirps_mismatch=0,
@@ -139,8 +139,9 @@ class TestRadarProtocol(unittest.TestCase):
pkt = bytearray() pkt = bytearray()
pkt.append(STATUS_HEADER_BYTE) pkt.append(STATUS_HEADER_BYTE)
# Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} # Word 0: {0xFF[31:24], reserved[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
w0 = (0xFF << 24) | ((mode & 0x03) << 22) | ((stream & 0x07) << 19) | (threshold & 0xFFFF) # PR-AB.b expanded: bits [23:22] formerly radar_mode, now reserved 0.
w0 = (0xFF << 24) | ((stream & 0x07) << 19) | (threshold & 0xFFFF)
pkt += struct.pack(">I", w0) pkt += struct.pack(">I", w0)
# Word 1: {long_chirp, long_listen} # Word 1: {long_chirp, long_listen}
@@ -157,11 +158,11 @@ class TestRadarProtocol(unittest.TestCase):
# Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0], # Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0],
# agc_saturation_count[7:0], agc_enable, # agc_saturation_count[7:0], agc_enable,
# chirps_mismatch[10], 8'd0, range_mode[1:0]} # chirps_mismatch[10], 10'd0 reserved [9:0]}
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) | w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) |
((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) |
((chirps_mismatch & 0x01) << 10) | ((chirps_mismatch & 0x01) << 10))
(range_mode & 0x03))
pkt += struct.pack(">I", w4) pkt += struct.pack(">I", w4)
# Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0, # Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0,
@@ -186,7 +187,6 @@ class TestRadarProtocol(unittest.TestCase):
raw = self._make_status_packet() raw = self._make_status_packet()
sr = RadarProtocol.parse_status_packet(raw) sr = RadarProtocol.parse_status_packet(raw)
self.assertIsNotNone(sr) self.assertIsNotNone(sr)
self.assertEqual(sr.radar_mode, 1)
self.assertEqual(sr.stream_ctrl, 7) self.assertEqual(sr.stream_ctrl, 7)
self.assertEqual(sr.cfar_threshold, 10000) self.assertEqual(sr.cfar_threshold, 10000)
self.assertEqual(sr.long_chirp, 3000) self.assertEqual(sr.long_chirp, 3000)
@@ -195,21 +195,14 @@ class TestRadarProtocol(unittest.TestCase):
self.assertEqual(sr.short_chirp, 50) self.assertEqual(sr.short_chirp, 50)
self.assertEqual(sr.short_listen, 17450) self.assertEqual(sr.short_listen, 17450)
self.assertEqual(sr.chirps_per_elev, 32) self.assertEqual(sr.chirps_per_elev, 32)
self.assertEqual(sr.range_mode, 0)
self.assertEqual(sr.chirps_mismatch, 0) self.assertEqual(sr.chirps_mismatch, 0)
def test_parse_status_range_mode(self):
raw = self._make_status_packet(range_mode=2)
sr = RadarProtocol.parse_status_packet(raw)
self.assertEqual(sr.range_mode, 2)
def test_parse_status_chirps_mismatch(self): def test_parse_status_chirps_mismatch(self):
# TX-G: bit 10 of word 4 must round-trip without disturbing neighbours. # TX-G: bit 10 of word 4 must round-trip without disturbing neighbours.
raw = self._make_status_packet(chirps_mismatch=1, agc_enable=1, range_mode=2) raw = self._make_status_packet(chirps_mismatch=1, agc_enable=1)
sr = RadarProtocol.parse_status_packet(raw) sr = RadarProtocol.parse_status_packet(raw)
self.assertEqual(sr.chirps_mismatch, 1) self.assertEqual(sr.chirps_mismatch, 1)
self.assertEqual(sr.agc_enable, 1) self.assertEqual(sr.agc_enable, 1)
self.assertEqual(sr.range_mode, 2)
def test_parse_status_too_short(self): def test_parse_status_too_short(self):
# Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected. # Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected.
@@ -274,8 +267,9 @@ class TestRadarProtocol(unittest.TestCase):
[19:12] agc_saturation_count (8-bit) [19:12] agc_saturation_count (8-bit)
[11] agc_enable (1-bit) [11] agc_enable (1-bit)
[10] chirps_mismatch (1-bit, TX-G) [10] chirps_mismatch (1-bit, TX-G)
[9:2] reserved (8 bits, must be zero from builder) [9:0] reserved (10 bits, must be zero from builder)
[1:0] range_mode (2-bit) (was [9:2] + range_mode[1:0]; range_mode retired in
PR-AB.b expanded)
For each field we set ONLY that field to its max, build the packet, 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 parse, and assert (a) the field reads back correctly and (b) every
@@ -289,12 +283,11 @@ class TestRadarProtocol(unittest.TestCase):
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"), ("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
("agc_enable", "agc_enable", 11, 1, "agc_enable"), ("agc_enable", "agc_enable", 11, 1, "agc_enable"),
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"), ("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
("range_mode", "range_mode", 0, 2, "range_mode"),
] ]
# Sanity: layout fields + reserved [9:2] must cover exactly 32 bits. # Sanity: layout fields + reserved [9:0] must cover exactly 32 bits.
used = sum(width for _, _, _, width, _ in layout) used = sum(width for _, _, _, width, _ in layout)
self.assertEqual(used + 8, 32, self.assertEqual(used + 10, 32,
"word 4 layout (incl. reserved [9:2]) must total 32 bits") "word 4 layout (incl. reserved [9:0]) must total 32 bits")
# No two fields may overlap. # No two fields may overlap.
occupied = set() occupied = set()
@@ -1000,18 +993,24 @@ class TestOpcodeEnum(unittest.TestCase):
self.assertFalse(hasattr(Opcode, name), self.assertFalse(hasattr(Opcode, name),
f"Legacy alias Opcode.{name} should not exist") f"Legacy alias Opcode.{name} should not exist")
def test_radar_mode_names(self): def test_basic_control_opcodes(self):
"""New canonical names must exist and match FPGA opcodes.""" """Canonical basic-control opcodes (0x03/0x04) match FPGA RTL.
self.assertEqual(Opcode.RADAR_MODE, 0x01)
self.assertEqual(Opcode.TRIGGER_PULSE, 0x02) PR-AB.b expanded retired RADAR_MODE (0x01) and TRIGGER_PULSE (0x02);
the legacy-alias test below ensures they stay absent.
"""
self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03) self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03)
self.assertEqual(Opcode.STREAM_CONTROL, 0x04) self.assertEqual(Opcode.STREAM_CONTROL, 0x04)
def test_all_rtl_opcodes_present(self): def test_all_rtl_opcodes_present(self):
"""Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.""" """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.
expected = {0x01, 0x02, 0x03, 0x04,
PR-AB.b expanded: 0x01 / 0x02 / 0x20 retired from the FPGA dispatch
case-block; they must NOT reappear in the Opcode enum.
"""
expected = {0x03, 0x04,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x28, 0x29, 0x2A, 0x2B, 0x2C,
0x30, 0x31, 0xFF} 0x30, 0x31, 0xFF}
enum_values = {int(m) for m in Opcode} enum_values = {int(m) for m in Opcode}
@@ -1093,17 +1092,17 @@ class TestAGCStatusParsing(unittest.TestCase):
self.assertEqual(sr.agc_saturation_count, 255) self.assertEqual(sr.agc_saturation_count, 255)
self.assertEqual(sr.agc_enable, 1) self.assertEqual(sr.agc_enable, 1)
def test_agc_and_range_mode_coexist(self): def test_agc_fields_coexist_with_mismatch(self):
"""AGC fields and range_mode occupy the same word without conflict.""" """AGC fields and chirps_mismatch occupy the same word without conflict."""
raw = self._make_status_packet(agc_gain=5, agc_peak=128, raw = self._make_status_packet(agc_gain=5, agc_peak=128,
agc_sat=42, agc_enable=1, agc_sat=42, agc_enable=1,
range_mode=2) chirps_mismatch=1)
sr = RadarProtocol.parse_status_packet(raw) sr = RadarProtocol.parse_status_packet(raw)
self.assertEqual(sr.agc_current_gain, 5) self.assertEqual(sr.agc_current_gain, 5)
self.assertEqual(sr.agc_peak_magnitude, 128) self.assertEqual(sr.agc_peak_magnitude, 128)
self.assertEqual(sr.agc_saturation_count, 42) self.assertEqual(sr.agc_saturation_count, 42)
self.assertEqual(sr.agc_enable, 1) self.assertEqual(sr.agc_enable, 1)
self.assertEqual(sr.range_mode, 2) self.assertEqual(sr.chirps_mismatch, 1)
class TestAGCStatusResponseDefaults(unittest.TestCase): class TestAGCStatusResponseDefaults(unittest.TestCase):
+5 -10
View File
@@ -697,13 +697,9 @@ class RadarDashboard(QMainWindow):
btn_mode_on.clicked.connect(lambda: self._send_fpga_cmd(0x01, 1)) btn_mode_on.clicked.connect(lambda: self._send_fpga_cmd(0x01, 1))
op_layout.addWidget(btn_mode_on) op_layout.addWidget(btn_mode_on)
btn_mode_off = QPushButton("Radar Mode Off") # PR-AB.b expanded: "Radar Mode Off" (opcode 0x01) and "Trigger Chirp"
btn_mode_off.clicked.connect(lambda: self._send_fpga_cmd(0x01, 0)) # (opcode 0x02) buttons retired — the FPGA-side host_radar_mode and
op_layout.addWidget(btn_mode_off) # host_trigger_pulse registers are gone. Auto-scan is the only mode.
btn_trigger = QPushButton("Trigger Chirp")
btn_trigger.clicked.connect(lambda: self._send_fpga_cmd(0x02, 1))
op_layout.addWidget(btn_trigger)
# Stream Control (3-bit mask) # Stream Control (3-bit mask)
self._add_fpga_param_row(op_layout, "Stream Control", 0x04, 7, 3, self._add_fpga_param_row(op_layout, "Stream Control", 0x04, 7, 3,
@@ -1951,12 +1947,11 @@ class RadarDashboard(QMainWindow):
"""Update FPGA status readback labels.""" """Update FPGA status readback labels."""
# Diagnostics tab # Diagnostics tab
lines = [ lines = [
f"Mode: {st.radar_mode} Stream: {st.stream_ctrl:03b} " f"Stream: {st.stream_ctrl:03b} Thresh: {st.cfar_threshold}",
f"Thresh: {st.cfar_threshold}",
f"Long Chirp: {st.long_chirp} Listen: {st.long_listen}", f"Long Chirp: {st.long_chirp} Listen: {st.long_listen}",
f"Guard: {st.guard} Short Chirp: {st.short_chirp} " f"Guard: {st.guard} Short Chirp: {st.short_chirp} "
f"Listen: {st.short_listen}", f"Listen: {st.short_listen}",
f"Chirps/Elev: {st.chirps_per_elev} Range Mode: {st.range_mode}", f"Chirps/Elev: {st.chirps_per_elev}",
] ]
self._fpga_status_label.setText("\n".join(lines)) self._fpga_status_label.setText("\n".join(lines))