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
+3
View File
@@ -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),
@@ -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),
@@ -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),
+22 -5
View File
@@ -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)
@@ -44,11 +44,12 @@
*
* Last byte: 0x55 (frame end footer)
*
* Status packet (FPGAHost): 30 bytes (PR-G: was 26 in v1, +4 for soft tier)
* Status packet (FPGAHost): 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 (HostFPGA): 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
+25 -13
View File
@@ -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
+41 -7
View File
@@ -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
+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