diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index b33ffe6..14a1663 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -1002,6 +1002,9 @@ end else begin : gen_ft2232h .status_guard(host_guard_cycles), .status_short_chirp(host_short_chirp_cycles), .status_short_listen(host_short_listen_cycles), + // M-5: MEDIUM PRI readback in status_words[7] + .status_medium_chirp(host_medium_chirp_cycles), + .status_medium_listen(host_medium_listen_cycles), .status_chirps_per_elev(host_chirps_per_elev), .status_range_mode(host_range_mode), .status_chirps_mismatch(chirps_mismatch_error), diff --git a/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v b/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v index 1c73997..dcf068b 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v +++ b/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v @@ -347,6 +347,10 @@ module tb_e2e_dsp_to_host; .status_guard (16'd0), .status_short_chirp (16'd0), .status_short_listen (16'd0), + // M-5: medium PRI readback ports (A6 doesn't exercise status reads; + // tied off to keep DUT-port arity in sync). + .status_medium_chirp (16'd0), + .status_medium_listen (16'd0), .status_chirps_per_elev (6'd0), .status_range_mode (2'd0), .status_chirps_mismatch (1'b0), diff --git a/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v b/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v index cb0e1b0..85b0fe0 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v +++ b/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v @@ -137,6 +137,10 @@ module tb_ft2232h_frame_drop; .status_guard(status_guard), .status_short_chirp(status_short_chirp), .status_short_listen(status_short_listen), + // M-5: medium PRI readback (test only exercises frame_drop path, + // medium values left at defaults since this TB doesn't read status). + .status_medium_chirp(16'd0), + .status_medium_listen(16'd0), .status_chirps_per_elev(status_chirps_per_elev), .status_range_mode(status_range_mode), .status_chirps_mismatch(status_chirps_mismatch), diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v b/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v index 77b565d..a97fc15 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v @@ -9,8 +9,9 @@ // reaches the cmd_* outputs of the read FSM with the right byte order. // 2. Bulk frame header v2 — verify byte0=0xAA, byte1=0x02 (version), // byte2=stream flags, bytes3-8=frame_num/range/doppler counts. -// 3. Status packet length — verify 30 bytes (was 26 in v1) and that -// status_words[6] carries detect_count_cand/detect_threshold_soft. +// 3. Status packet length — verify 34 bytes (M-5; was 30 / PR-G; 26 / v1). +// status_words[6] carries detect_count_cand/detect_threshold_soft (PR-G). +// status_words[7] carries medium_chirp/medium_listen (M-5). // 4. PR-G FSM trim — full-frame header/body length consistency. With all // streams enabled, total emitted bytes must equal 9 (hdr) + range×2 + // range×doppler×2 (doppler) + range×doppler×2/8 (detect) + 1 (footer). @@ -78,6 +79,10 @@ module tb_usb_protocol_v2; reg [15:0] status_guard = 16'd0; reg [15:0] status_short_chirp = 16'd0; reg [15:0] status_short_listen = 16'd0; + // M-5: status_words[7] medium PRI readback (test default = production + // RP_DEF_MEDIUM_*_CYCLES so the round-trip canary is the real boot value) + reg [15:0] status_medium_chirp = 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; + reg [15:0] status_medium_listen = 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; reg [5:0] status_chirps_per_elev = 6'd0; reg [1:0] status_range_mode = 2'd0; reg status_chirps_mismatch = 1'b0; @@ -141,6 +146,9 @@ module tb_usb_protocol_v2; .status_guard(status_guard), .status_short_chirp(status_short_chirp), .status_short_listen(status_short_listen), + // M-5: medium PRI readback feeding status_words[7] + .status_medium_chirp(status_medium_chirp), + .status_medium_listen(status_medium_listen), .status_chirps_per_elev(status_chirps_per_elev), .status_range_mode(status_range_mode), .status_chirps_mismatch(status_chirps_mismatch), @@ -271,9 +279,11 @@ module tb_usb_protocol_v2; check_b("T2.8: byte9 = footer 0x55", egress_bytes[9] == 8'h55); // ------------------------------------------------------------- - // TEST 3: Status packet length = 30 bytes; word[6] carries telemetry + // TEST 3: Status packet length = 34 bytes (M-5); + // word[6] carries 2-tier CFAR telemetry (PR-G); + // word[7] carries medium_chirp/medium_listen (M-5). // ------------------------------------------------------------- - $display("\n[TEST 3] Status packet length 30B + word[6] PR-G fields"); + $display("\n[TEST 3] Status packet length 34B + word[6]/word[7] fields"); egress_count = 0; @(posedge clk); status_request = 1'b1; @@ -281,7 +291,8 @@ module tb_usb_protocol_v2; status_request = 1'b0; wait_clk(300); // Wait for status drain check_b("T3.1: byte0 = 0xBB (status header)", egress_bytes[0] == 8'hBB); - check_b("T3.2: byte29 = 0x55 (footer)", egress_bytes[29] == 8'h55); + // M-5: footer moved 29→33 with the 4-byte word[7] insertion. + check_b("T3.2: byte33 = 0x55 (footer)", egress_bytes[33] == 8'h55); check_b("T3.3: status_words[6] count_cand[15:8]=0", egress_bytes[25] == 8'h00); check_b("T3.4: status_words[6] count_cand[7:0]=42", egress_bytes[26] == 8'd42); check_b("T3.5: status_words[6] thr_soft[15:8]=0x0A", egress_bytes[27] == 8'h0A); @@ -294,6 +305,12 @@ module tb_usb_protocol_v2; // word[4][7:0] = {alpha_soft[7:0], range_mode[1:0]} = {8'h18, 2'b00} = 8'h60 check_b("T3.7: status_words[4][7:0] = alpha_soft<<2 = 0x60 (alpha=0x18)", egress_bytes[20] == 8'h60); + // M-5: status_words[7] = {medium_chirp, medium_listen}. + // Defaults from RP_DEF_*: 500 (0x01F4) chirp / 15600 (0x3CF0) listen. + check_b("T3.8: status_words[7] medium_chirp[15:8]=0x01", egress_bytes[29] == 8'h01); + check_b("T3.9: status_words[7] medium_chirp[7:0]=0xF4", egress_bytes[30] == 8'hF4); + check_b("T3.10: status_words[7] medium_listen[15:8]=0x3C", egress_bytes[31] == 8'h3C); + check_b("T3.11: status_words[7] medium_listen[7:0]=0xF0", egress_bytes[32] == 8'hF0); // ------------------------------------------------------------- // TEST 4: full-frame header/body length consistency (PR-G trim) diff --git a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v index 2248edc..b401adc 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -44,11 +44,12 @@ * * Last byte: 0x55 (frame end footer) * - * Status packet (FPGA→Host): 30 bytes (PR-G: was 26 in v1, +4 for soft tier) + * Status packet (FPGA→Host): 34 bytes (M-5: was 30 / PR-G; was 26 / v1) * Byte 0: 0xBB (status header) - * Bytes 1-28: 7 × 32-bit status words, MSB first - * word[6] = {detect_count_cand[15:0], detect_threshold_soft[15:0]} - * Byte 29: 0x55 (footer) + * Bytes 1-32: 8 × 32-bit status words, MSB first + * word[6] = {detect_count_cand[15:0], detect_threshold_soft[15:0]} (PR-G) + * word[7] = {medium_chirp[15:0], medium_listen[15:0]} (M-5) + * Byte 33: 0x55 (footer) * * Command (Host→FPGA): 4 bytes received sequentially (unchanged) * Byte 0: opcode[7:0] (see RP_OP_* in radar_params.vh) @@ -148,6 +149,8 @@ module usb_data_interface_ft2232h ( input wire [15:0] status_guard, input wire [15:0] status_short_chirp, input wire [15:0] status_short_listen, + input wire [15:0] status_medium_chirp, // M-5: status_words[7][31:16] readback + input wire [15:0] status_medium_listen, // M-5: status_words[7][15:0] readback input wire [5:0] status_chirps_per_elev, input wire [1:0] status_range_mode, input wire status_chirps_mismatch, // TX-G: host requested chirps != Doppler FFT size @@ -225,8 +228,9 @@ localparam VALID_DET_BYTES_PER_RANGE = (NUM_DOPPLER_BINS * DETECT_BITS_PER_CELL localparam DETECT_SECTION_BYTES = NUM_RANGE_BINS * VALID_DET_BYTES_PER_RANGE; // 6144 localparam [3:0] DET_BYTE_LAST_PER_RANGE = VALID_DET_BYTES_PER_RANGE[3:0] - 4'd1; // 11 -// Status packet: 30 bytes (PR-G: 7 × 32-bit words + header + footer) -localparam STATUS_PKT_LEN = 5'd30; +// Status packet: 34 bytes (M-5: 8 × 32-bit words + header + footer; PR-G was 7 words / 30 B). +// Width bumped 5→6 bits because 34 doesn't fit in 5 bits. +localparam STATUS_PKT_LEN = 6'd34; // ============================================================================ // WRITE FSM STATES (FPGA → Host, ft_clk domain) @@ -698,8 +702,8 @@ always @(posedge clk or negedge reset_n) begin end end -// --- Status snapshot (ft_clk domain) — PR-G: 7 words (was 6) --- -reg [31:0] status_words [0:6]; +// --- Status snapshot (ft_clk domain) — M-5: 8 words (was 7 / PR-G; was 6 / pre-PR-G) --- +reg [31:0] status_words [0:7]; // Byte counter for write FSM (needs to be wide enough for largest section) reg [15:0] wr_byte_idx; @@ -806,7 +810,7 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin det_thr_soft_sync_1 <= 17'd0; det_count_cand_sync_0 <= 16'd0; det_count_cand_sync_1 <= 16'd0; - for (si = 0; si < 7; si = si + 1) + for (si = 0; si < 8; si = si + 1) status_words[si] <= 32'd0; wr_state <= WR_IDLE; wr_byte_idx <= 16'd0; @@ -904,6 +908,17 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin status_words[6] <= {det_count_cand_sync_1, (det_thr_soft_sync_1[16] ? 16'hFFFF : det_thr_soft_sync_1[15:0])}; + // M-5 word 7: {medium_chirp[15:0], medium_listen[15:0]}. + // Host writes via 0x17/0x18; this readback closes the GUI's + // 161-µs MEDIUM PRI visibility gap that PR-G left when status word + // 3 ran out of reserved bits to fit a second 16-bit pair. + // CDC-wise this follows the same convention as long_chirp / + // long_listen / short_chirp / short_listen above (status_words[1..3]): + // direct sample of the clk-domain register on the ft_clk-domain + // status_req_ft strobe, accepting the same quasi-static-write + // assumption (host writes cycles once during init, no concurrent + // change during a status read). + status_words[7] <= {status_medium_chirp, status_medium_listen}; end // ================================================================ @@ -1199,47 +1214,51 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end end - // ---- Status packet: 30 bytes (PR-G v2: 7 × 32-bit words) ---- + // ---- Status packet: 34 bytes (M-5: 8 × 32-bit words) ---- WR_STATUS_SEND: begin if (!ft_txe_n) begin ft_data_oe <= 1'b1; ft_wr_n <= 1'b0; - case (wr_byte_idx[4:0]) - 5'd0: ft_data_out <= STATUS_HEADER; - 5'd1: ft_data_out <= status_words[0][31:24]; - 5'd2: ft_data_out <= status_words[0][23:16]; - 5'd3: ft_data_out <= status_words[0][15:8]; - 5'd4: ft_data_out <= status_words[0][7:0]; - 5'd5: ft_data_out <= status_words[1][31:24]; - 5'd6: ft_data_out <= status_words[1][23:16]; - 5'd7: ft_data_out <= status_words[1][15:8]; - 5'd8: ft_data_out <= status_words[1][7:0]; - 5'd9: ft_data_out <= status_words[2][31:24]; - 5'd10: ft_data_out <= status_words[2][23:16]; - 5'd11: ft_data_out <= status_words[2][15:8]; - 5'd12: ft_data_out <= status_words[2][7:0]; - 5'd13: ft_data_out <= status_words[3][31:24]; - 5'd14: ft_data_out <= status_words[3][23:16]; - 5'd15: ft_data_out <= status_words[3][15:8]; - 5'd16: ft_data_out <= status_words[3][7:0]; - 5'd17: ft_data_out <= status_words[4][31:24]; - 5'd18: ft_data_out <= status_words[4][23:16]; - 5'd19: ft_data_out <= status_words[4][15:8]; - 5'd20: ft_data_out <= status_words[4][7:0]; - 5'd21: ft_data_out <= status_words[5][31:24]; - 5'd22: ft_data_out <= status_words[5][23:16]; - 5'd23: ft_data_out <= status_words[5][15:8]; - 5'd24: ft_data_out <= status_words[5][7:0]; - 5'd25: ft_data_out <= status_words[6][31:24]; // PR-G - 5'd26: ft_data_out <= status_words[6][23:16]; // PR-G - 5'd27: ft_data_out <= status_words[6][15:8]; // PR-G - 5'd28: ft_data_out <= status_words[6][7:0]; // PR-G - 5'd29: ft_data_out <= FOOTER; + case (wr_byte_idx[5:0]) + 6'd0: ft_data_out <= STATUS_HEADER; + 6'd1: ft_data_out <= status_words[0][31:24]; + 6'd2: ft_data_out <= status_words[0][23:16]; + 6'd3: ft_data_out <= status_words[0][15:8]; + 6'd4: ft_data_out <= status_words[0][7:0]; + 6'd5: ft_data_out <= status_words[1][31:24]; + 6'd6: ft_data_out <= status_words[1][23:16]; + 6'd7: ft_data_out <= status_words[1][15:8]; + 6'd8: ft_data_out <= status_words[1][7:0]; + 6'd9: ft_data_out <= status_words[2][31:24]; + 6'd10: ft_data_out <= status_words[2][23:16]; + 6'd11: ft_data_out <= status_words[2][15:8]; + 6'd12: ft_data_out <= status_words[2][7:0]; + 6'd13: ft_data_out <= status_words[3][31:24]; + 6'd14: ft_data_out <= status_words[3][23:16]; + 6'd15: ft_data_out <= status_words[3][15:8]; + 6'd16: ft_data_out <= status_words[3][7:0]; + 6'd17: ft_data_out <= status_words[4][31:24]; + 6'd18: ft_data_out <= status_words[4][23:16]; + 6'd19: ft_data_out <= status_words[4][15:8]; + 6'd20: ft_data_out <= status_words[4][7:0]; + 6'd21: ft_data_out <= status_words[5][31:24]; + 6'd22: ft_data_out <= status_words[5][23:16]; + 6'd23: ft_data_out <= status_words[5][15:8]; + 6'd24: ft_data_out <= status_words[5][7:0]; + 6'd25: ft_data_out <= status_words[6][31:24]; // PR-G + 6'd26: ft_data_out <= status_words[6][23:16]; // PR-G + 6'd27: ft_data_out <= status_words[6][15:8]; // PR-G + 6'd28: ft_data_out <= status_words[6][7:0]; // PR-G + 6'd29: ft_data_out <= status_words[7][31:24]; // M-5: medium_chirp[15:8] + 6'd30: ft_data_out <= status_words[7][23:16]; // M-5: medium_chirp[7:0] + 6'd31: ft_data_out <= status_words[7][15:8]; // M-5: medium_listen[15:8] + 6'd32: ft_data_out <= status_words[7][7:0]; // M-5: medium_listen[7:0] + 6'd33: ft_data_out <= FOOTER; default: ft_data_out <= 8'h00; endcase - if (wr_byte_idx[4:0] == STATUS_PKT_LEN - 5'd1) begin + if (wr_byte_idx[5:0] == STATUS_PKT_LEN - 6'd1) begin wr_state <= WR_DONE; wr_byte_idx <= 16'd0; end else begin diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index e001abc..8803d00 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -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 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 a7be7ac..1209e0d 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -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 diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py index 2b99252..56fabd0 100644 --- a/9_Firmware/9_3_GUI/test_v7.py +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -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