diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 8803d00..52f7de6 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -124,24 +124,23 @@ class Opcode(IntEnum): """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): - 0x01 host_radar_mode 0x20 host_range_mode - 0x02 host_trigger_pulse 0x21-0x27 CFAR / MTI / DC-notch - 0x03 host_detect_threshold 0x28-0x2C AGC control - 0x04 host_stream_control 0x2D host_cfar_alpha_soft - 0x10 host_long_chirp_cycles 0x30 host_self_test_trigger - 0x11 host_long_listen_cycles 0x31/0xFF host_status_request - 0x12 host_guard_cycles 0x32 host_adc_pwdn - 0x13 host_short_chirp_cycles 0x33 host_adc_format - 0x14 host_short_listen_cycles + 0x03 host_detect_threshold 0x21-0x27 CFAR / MTI / DC-notch + 0x04 host_stream_control 0x28-0x2C AGC control + 0x10 host_long_chirp_cycles 0x2D host_cfar_alpha_soft + 0x11 host_long_listen_cycles 0x30 host_self_test_trigger + 0x12 host_guard_cycles 0x31/0xFF host_status_request + 0x13 host_short_chirp_cycles 0x32 host_adc_pwdn + 0x14 host_short_listen_cycles 0x33 host_adc_format 0x15 host_chirps_per_elev 0x16 host_gain_shift 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) + + PR-AB.b expanded retired opcodes 0x01 (host_radar_mode), + 0x02 (host_trigger_pulse), 0x20 (host_range_mode). """ - # --- Basic control (0x01-0x04) --- - RADAR_MODE = 0x01 # 2-bit mode select - TRIGGER_PULSE = 0x02 # self-clearing one-shot trigger + # --- Basic control (0x03-0x04) --- DETECT_THRESHOLD = 0x03 # 16-bit detection threshold value 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). SUBFRAME_ENABLE = 0x19 - # --- Signal processing (0x20-0x27) --- - RANGE_MODE = 0x20 + # --- Signal processing (0x21-0x27; + # 0x20 host_range_mode retired in PR-AB.b expanded) --- CFAR_GUARD = 0x21 CFAR_TRAIN = 0x22 CFAR_ALPHA = 0x23 @@ -243,7 +242,6 @@ class RadarFrame: @dataclass class StatusResponse: """Parsed status response from FPGA (M-5: 8-word / 34-byte packet).""" - radar_mode: int = 0 stream_ctrl: int = 0 cfar_threshold: int = 0 long_chirp: int = 0 @@ -252,7 +250,6 @@ class StatusResponse: short_chirp: int = 0 short_listen: int = 0 chirps_per_elev: int = 0 - range_mode: int = 0 # Self-test results (word 5, added in Build 26) self_test_flags: int = 0 # 5-bit result flags [4:0] self_test_detail: int = 0 # 8-bit detail code [7:0] @@ -363,10 +360,10 @@ class RadarProtocol: return None 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.stream_ctrl = (words[0] >> 19) & 0x07 - sr.radar_mode = (words[0] >> 22) & 0x03 # Word 1: {long_chirp[31:16], long_listen[15:0]} sr.long_listen = words[1] & 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]} 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] mode[1:0] - sr.range_mode = words[4] & 0x03 + # 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. sr.chirps_mismatch = (words[4] >> 10) & 0x01 sr.agc_enable = (words[4] >> 11) & 0x01 sr.agc_saturation_count = (words[4] >> 12) & 0xFF @@ -1144,8 +1141,8 @@ class RadarAcquisition(threading.Thread): elif ptype == "status": status = RadarProtocol.parse_status_packet(raw[start:end]) if status is not None: - log.info(f"Status: mode={status.radar_mode} " - f"stream={status.stream_ctrl}") + log.info(f"Status: stream={status.stream_ctrl} " + f"chirps/elev={status.chirps_per_elev}") if status.self_test_busy or status.self_test_flags: log.info(f"Self-test: busy={status.self_test_busy} " f"flags=0b{status.self_test_flags:05b} " diff --git a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py index 1209e0d..4838969 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -126,10 +126,10 @@ class TestRadarProtocol(unittest.TestCase): # ---------------------------------------------------------------- # 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, 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, agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0, chirps_mismatch=0, @@ -139,8 +139,9 @@ class TestRadarProtocol(unittest.TestCase): pkt = bytearray() pkt.append(STATUS_HEADER_BYTE) - # Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} - w0 = (0xFF << 24) | ((mode & 0x03) << 22) | ((stream & 0x07) << 19) | (threshold & 0xFFFF) + # 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. + w0 = (0xFF << 24) | ((stream & 0x07) << 19) | (threshold & 0xFFFF) pkt += struct.pack(">I", w0) # 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], # 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) | ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | - ((chirps_mismatch & 0x01) << 10) | - (range_mode & 0x03)) + ((chirps_mismatch & 0x01) << 10)) pkt += struct.pack(">I", w4) # 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() sr = RadarProtocol.parse_status_packet(raw) self.assertIsNotNone(sr) - self.assertEqual(sr.radar_mode, 1) self.assertEqual(sr.stream_ctrl, 7) self.assertEqual(sr.cfar_threshold, 10000) self.assertEqual(sr.long_chirp, 3000) @@ -195,21 +195,14 @@ class TestRadarProtocol(unittest.TestCase): self.assertEqual(sr.short_chirp, 50) self.assertEqual(sr.short_listen, 17450) self.assertEqual(sr.chirps_per_elev, 32) - self.assertEqual(sr.range_mode, 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): # 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) self.assertEqual(sr.chirps_mismatch, 1) self.assertEqual(sr.agc_enable, 1) - self.assertEqual(sr.range_mode, 2) def test_parse_status_too_short(self): # 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) [11] agc_enable (1-bit) [10] chirps_mismatch (1-bit, TX-G) - [9:2] reserved (8 bits, must be zero from builder) - [1:0] range_mode (2-bit) + [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) 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 @@ -289,12 +283,11 @@ class TestRadarProtocol(unittest.TestCase): ("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"), - ("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) - self.assertEqual(used + 8, 32, - "word 4 layout (incl. reserved [9:2]) must total 32 bits") + self.assertEqual(used + 10, 32, + "word 4 layout (incl. reserved [9:0]) must total 32 bits") # No two fields may overlap. occupied = set() @@ -1000,18 +993,24 @@ class TestOpcodeEnum(unittest.TestCase): self.assertFalse(hasattr(Opcode, name), f"Legacy alias Opcode.{name} should not exist") - def test_radar_mode_names(self): - """New canonical names must exist and match FPGA opcodes.""" - self.assertEqual(Opcode.RADAR_MODE, 0x01) - self.assertEqual(Opcode.TRIGGER_PULSE, 0x02) + def test_basic_control_opcodes(self): + """Canonical basic-control opcodes (0x03/0x04) match FPGA RTL. + + 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.STREAM_CONTROL, 0x04) def test_all_rtl_opcodes_present(self): - """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.""" - expected = {0x01, 0x02, 0x03, 0x04, + """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member. + + 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, - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x30, 0x31, 0xFF} 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_enable, 1) - def test_agc_and_range_mode_coexist(self): - """AGC fields and range_mode occupy the same word without conflict.""" + def test_agc_fields_coexist_with_mismatch(self): + """AGC fields and chirps_mismatch occupy the same word without conflict.""" raw = self._make_status_packet(agc_gain=5, agc_peak=128, agc_sat=42, agc_enable=1, - range_mode=2) + chirps_mismatch=1) sr = RadarProtocol.parse_status_packet(raw) self.assertEqual(sr.agc_current_gain, 5) self.assertEqual(sr.agc_peak_magnitude, 128) self.assertEqual(sr.agc_saturation_count, 42) self.assertEqual(sr.agc_enable, 1) - self.assertEqual(sr.range_mode, 2) + self.assertEqual(sr.chirps_mismatch, 1) class TestAGCStatusResponseDefaults(unittest.TestCase): diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 8f07eda..05acf33 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -697,13 +697,9 @@ class RadarDashboard(QMainWindow): btn_mode_on.clicked.connect(lambda: self._send_fpga_cmd(0x01, 1)) op_layout.addWidget(btn_mode_on) - btn_mode_off = QPushButton("Radar Mode Off") - btn_mode_off.clicked.connect(lambda: self._send_fpga_cmd(0x01, 0)) - op_layout.addWidget(btn_mode_off) - - btn_trigger = QPushButton("Trigger Chirp") - btn_trigger.clicked.connect(lambda: self._send_fpga_cmd(0x02, 1)) - op_layout.addWidget(btn_trigger) + # PR-AB.b expanded: "Radar Mode Off" (opcode 0x01) and "Trigger Chirp" + # (opcode 0x02) buttons retired — the FPGA-side host_radar_mode and + # host_trigger_pulse registers are gone. Auto-scan is the only mode. # Stream Control (3-bit mask) self._add_fpga_param_row(op_layout, "Stream Control", 0x04, 7, 3, @@ -1951,12 +1947,11 @@ class RadarDashboard(QMainWindow): """Update FPGA status readback labels.""" # Diagnostics tab lines = [ - f"Mode: {st.radar_mode} Stream: {st.stream_ctrl:03b} " - f"Thresh: {st.cfar_threshold}", + f"Stream: {st.stream_ctrl:03b} Thresh: {st.cfar_threshold}", f"Long Chirp: {st.long_chirp} Listen: {st.long_listen}", f"Guard: {st.guard} Short Chirp: {st.short_chirp} " 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))