mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-11 07:51:17 +00:00
feat(rtl,gui): PR-U / M-8 — sub-frame enable mask routed end-to-end (C-5 hardening)
The chirp_scheduler had a 3-bit host_subframe_enable input {LONG, MEDIUM, SHORT}
that was tied to the constant RP_DEF_SUBFRAME_ENABLE at the receiver instance,
so the host could neither change it nor know what mask was active. With the
mask not at 3'b111 the scheduler skips a sub-frame at TX but doppler_processor
still writes 48 chirp slots, so the host CRT (`dbin // 16 → {SHORT, MED, LONG}`)
silently mis-attributes the SF axis and unfolds to the wrong velocity.
Plumb the mask through:
- radar_system_top.v: new reg [2:0] host_subframe_enable, cold-reset
RP_DEF_SUBFRAME_ENABLE, opcode 0x19 setter, wired to rx_inst and usb_inst.
- radar_receiver_final.v: new host_subframe_enable[2:0] input port; the
chirp_scheduler instance is untied from the constant.
- usb_data_interface_ft2232h.v: new subframe_enable[2:0] input + per-frame
snapshot reg latched at frame_complete (stable for ft_clk read, same
pattern as stream_flags_snapshot). Byte 2 emission is now
{2'b00, subframe_enable[2:0], stream_flags[2:0]} — was {5'b00000, stream}.
- radar_protocol.py: Opcode.SUBFRAME_ENABLE = 0x19; RadarFrame.subframe_enable
field; parse_bulk_frame surfaces bits[5:3]; reserved-mask 0xF8 → 0xC0.
Bulk-frame mock encodes the mask in its emit so dashboard replay is correct.
- v7/processing.py: extract_targets_from_frame_crt forces every target to
AMBIGUOUS when frame.subframe_enable != 0b111. Operator sees the red `?`
flag in the targets table instead of a silently-wrong velocity.
- v7/software_fpga.py + v7/dashboard.py: subframe_enable mirror + setter, and
replay dispatch routes 0x19 to set_subframe_enable.
Tests (test_v7.py): TestSubframeEnableRoundTrip (4), TestSoftwareFpgaSubframeEnable
(2), TestCrtSubframeMaskGating (3), 0x19 added to TestOpcodeEnumFillIn and
TestReplayOpcodeDispatch. Existing test_full_frame_round_trip updated to expect
byte 2 = 0x3F (mask 0b111 default + stream 0x07).
Cosim TBs (tb/tb_usb_protocol_v2.v, tb/tb_ft2232h_frame_drop.v) drive the new
input with 3'b111 and assert the new byte-2 layout (T2.3: 0x00 → 0x38).
Regression: test_v7 146/146, test_GUI_V65_Tk 117/117, ruff clean.
iverilog: tb_usb_protocol_v2 27/27 PASS, tb_ft2232h_frame_drop 10/10 PASS.
This commit is contained in:
@@ -51,6 +51,10 @@ module radar_receiver_final (
|
|||||||
input wire [15:0] host_medium_chirp_cycles,
|
input wire [15:0] host_medium_chirp_cycles,
|
||||||
input wire [15:0] host_medium_listen_cycles,
|
input wire [15:0] host_medium_listen_cycles,
|
||||||
input wire [5:0] host_chirps_per_elev,
|
input wire [5:0] host_chirps_per_elev,
|
||||||
|
// PR-U / M-8: sub-frame enable mask {LONG, MEDIUM, SHORT}. Was tied to
|
||||||
|
// RP_DEF_SUBFRAME_ENABLE here at the chirp_scheduler instance; routed
|
||||||
|
// through radar_system_top opcode 0x19 so the host owns the mask.
|
||||||
|
input wire [2:0] host_subframe_enable,
|
||||||
|
|
||||||
// Digital gain control (Fix 3: between DDC output and matched filter)
|
// Digital gain control (Fix 3: between DDC output and matched filter)
|
||||||
// [3]=direction: 0=amplify(left shift), 1=attenuate(right shift)
|
// [3]=direction: 0=amplify(left shift), 1=attenuate(right shift)
|
||||||
@@ -236,7 +240,9 @@ chirp_scheduler sched (
|
|||||||
.clk(clk),
|
.clk(clk),
|
||||||
.reset_n(reset_n),
|
.reset_n(reset_n),
|
||||||
.host_mode(host_mode),
|
.host_mode(host_mode),
|
||||||
.host_subframe_enable(`RP_DEF_SUBFRAME_ENABLE),
|
// PR-U / M-8: routed from radar_system_top opcode 0x19 (was the
|
||||||
|
// RP_DEF_SUBFRAME_ENABLE constant — host had no way to mask sub-frames).
|
||||||
|
.host_subframe_enable(host_subframe_enable),
|
||||||
.host_short_chirp_cycles (host_short_chirp_cycles),
|
.host_short_chirp_cycles (host_short_chirp_cycles),
|
||||||
.host_short_listen_cycles(host_short_listen_cycles),
|
.host_short_listen_cycles(host_short_listen_cycles),
|
||||||
// PR-G G2: MEDIUM now flows from radar_system_top opcodes 0x17/0x18.
|
// PR-G G2: MEDIUM now flows from radar_system_top opcodes 0x17/0x18.
|
||||||
|
|||||||
@@ -272,6 +272,11 @@ reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17400, V2)
|
|||||||
reg [15:0] host_medium_chirp_cycles; // Opcode 0x17 (default 500, PR-G G2)
|
reg [15:0] host_medium_chirp_cycles; // Opcode 0x17 (default 500, PR-G G2)
|
||||||
reg [15:0] host_medium_listen_cycles; // Opcode 0x18 (default 15600, PR-Q staggered PRI)
|
reg [15:0] host_medium_listen_cycles; // Opcode 0x18 (default 15600, PR-Q staggered PRI)
|
||||||
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER_FRAME, PR-F)
|
reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER_FRAME, PR-F)
|
||||||
|
// PR-U / M-8: per-sub-frame enable mask routed end-to-end so the host knows
|
||||||
|
// which sub-frames the chirp_scheduler emitted for a given frame. Bit 0 SHORT,
|
||||||
|
// bit 1 MEDIUM, bit 2 LONG. Default 3'b111 keeps the production 3-PRI ladder.
|
||||||
|
// Mirrored into v2 frame byte 2 bits[5:3] (usb_data_interface_ft2232h.v).
|
||||||
|
reg [2:0] host_subframe_enable; // Opcode 0x19 (default RP_DEF_SUBFRAME_ENABLE = 3'b111)
|
||||||
reg host_status_request; // Opcode 0xFF (self-clearing pulse)
|
reg host_status_request; // Opcode 0xFF (self-clearing pulse)
|
||||||
|
|
||||||
// Fix 4: Doppler/chirps mismatch protection
|
// Fix 4: Doppler/chirps mismatch protection
|
||||||
@@ -604,6 +609,9 @@ radar_receiver_final rx_inst (
|
|||||||
.host_medium_chirp_cycles(host_medium_chirp_cycles),
|
.host_medium_chirp_cycles(host_medium_chirp_cycles),
|
||||||
.host_medium_listen_cycles(host_medium_listen_cycles),
|
.host_medium_listen_cycles(host_medium_listen_cycles),
|
||||||
.host_chirps_per_elev(host_chirps_per_elev),
|
.host_chirps_per_elev(host_chirps_per_elev),
|
||||||
|
// PR-U / M-8: sub-frame enable mask, was tied to RP_DEF_SUBFRAME_ENABLE
|
||||||
|
// inside radar_receiver_final at the chirp_scheduler instance.
|
||||||
|
.host_subframe_enable(host_subframe_enable),
|
||||||
// Fix 3: digital gain control
|
// Fix 3: digital gain control
|
||||||
.host_gain_shift(host_gain_shift),
|
.host_gain_shift(host_gain_shift),
|
||||||
// AGC configuration (opcodes 0x28-0x2C)
|
// AGC configuration (opcodes 0x28-0x2C)
|
||||||
@@ -933,6 +941,11 @@ end else begin : gen_ft2232h
|
|||||||
// Stream control
|
// Stream control
|
||||||
.stream_control(host_stream_control),
|
.stream_control(host_stream_control),
|
||||||
|
|
||||||
|
// PR-U / M-8: per-frame snapshot of host_subframe_enable echoed in
|
||||||
|
// v2 frame byte 2 bits[5:3]. Lets the host detect when an operator
|
||||||
|
// disabled a sub-frame and downgrade CRT confidence accordingly.
|
||||||
|
.subframe_enable(host_subframe_enable),
|
||||||
|
|
||||||
// Status readback inputs
|
// Status readback inputs
|
||||||
.status_request(host_status_request),
|
.status_request(host_status_request),
|
||||||
.status_cfar_threshold(host_detect_threshold),
|
.status_cfar_threshold(host_detect_threshold),
|
||||||
@@ -1060,6 +1073,8 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
|
|||||||
// chirps_per_elev register is echoed in status word 3 and used by host
|
// chirps_per_elev register is echoed in status word 3 and used by host
|
||||||
// sanity-checking. Keep cold-reset value in lockstep with the truth.
|
// sanity-checking. Keep cold-reset value in lockstep with the truth.
|
||||||
host_chirps_per_elev <= 6'd48;
|
host_chirps_per_elev <= 6'd48;
|
||||||
|
// PR-U / M-8: 3'b111 = SHORT|MEDIUM|LONG all on (production 3-PRI ladder).
|
||||||
|
host_subframe_enable <= `RP_DEF_SUBFRAME_ENABLE;
|
||||||
host_status_request <= 1'b0;
|
host_status_request <= 1'b0;
|
||||||
chirps_mismatch_error <= 1'b0;
|
chirps_mismatch_error <= 1'b0;
|
||||||
host_range_mode <= 2'b00; // Default: 3 km mode (all short chirps)
|
host_range_mode <= 2'b00; // Default: 3 km mode (all short chirps)
|
||||||
@@ -1111,6 +1126,11 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
|
|||||||
// PR-G G2: MEDIUM ladder timings
|
// PR-G G2: MEDIUM ladder timings
|
||||||
8'h17: host_medium_chirp_cycles <= usb_cmd_value;
|
8'h17: host_medium_chirp_cycles <= usb_cmd_value;
|
||||||
8'h18: host_medium_listen_cycles <= usb_cmd_value;
|
8'h18: host_medium_listen_cycles <= usb_cmd_value;
|
||||||
|
// PR-U / M-8: sub-frame enable mask {LONG, MEDIUM, SHORT} =
|
||||||
|
// {value[2], value[1], value[0]}. Surfaced in v2 frame byte 2
|
||||||
|
// bits[5:3] so host CRT can detect mask != 3'b111 and degrade
|
||||||
|
// confidence rather than mis-attribute the SF axis.
|
||||||
|
8'h19: host_subframe_enable <= usb_cmd_value[2:0];
|
||||||
8'h15: begin
|
8'h15: begin
|
||||||
// Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size.
|
// Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size.
|
||||||
// If host requests a different value, clamp and set error flag.
|
// If host requests a different value, clamp and set error flag.
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ module tb_ft2232h_frame_drop;
|
|||||||
// PR-G: stream bits [2:0] all off → WR FSM: HDR → FOOTER → DONE
|
// PR-G: stream bits [2:0] all off → WR FSM: HDR → FOOTER → DONE
|
||||||
// = fast deterministic drain. Bits [5:3] are reserved=0 in v2.
|
// = fast deterministic drain. Bits [5:3] are reserved=0 in v2.
|
||||||
reg [5:0] stream_control = 6'b000_000;
|
reg [5:0] stream_control = 6'b000_000;
|
||||||
|
// PR-U / M-8: production 3-PRI ladder.
|
||||||
|
reg [2:0] subframe_enable = 3'b111;
|
||||||
|
|
||||||
// Status inputs (irrelevant for this test)
|
// Status inputs (irrelevant for this test)
|
||||||
reg status_request = 1'b0;
|
reg status_request = 1'b0;
|
||||||
@@ -124,6 +126,8 @@ module tb_ft2232h_frame_drop;
|
|||||||
.cmd_addr(cmd_addr),
|
.cmd_addr(cmd_addr),
|
||||||
.cmd_value(cmd_value),
|
.cmd_value(cmd_value),
|
||||||
.stream_control(stream_control),
|
.stream_control(stream_control),
|
||||||
|
// PR-U / M-8: per-frame snapshot of host_subframe_enable.
|
||||||
|
.subframe_enable(subframe_enable),
|
||||||
.status_request(status_request),
|
.status_request(status_request),
|
||||||
.status_cfar_threshold(status_cfar_threshold),
|
.status_cfar_threshold(status_cfar_threshold),
|
||||||
.status_stream_ctrl(status_stream_ctrl),
|
.status_stream_ctrl(status_stream_ctrl),
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ module tb_usb_protocol_v2;
|
|||||||
// PR-G v2: enable all 3 streams (range|doppler|cfar). Bits [5:3] reserved=0.
|
// PR-G v2: enable all 3 streams (range|doppler|cfar). Bits [5:3] reserved=0.
|
||||||
reg [5:0] stream_control = 6'b000_111;
|
reg [5:0] stream_control = 6'b000_111;
|
||||||
reg [5:0] status_stream_ctrl = 6'b000_111;
|
reg [5:0] status_stream_ctrl = 6'b000_111;
|
||||||
|
// PR-U / M-8: production 3-PRI ladder (SHORT|MEDIUM|LONG).
|
||||||
|
reg [2:0] subframe_enable = 3'b111;
|
||||||
|
|
||||||
// Status inputs (mostly tied off; PR-G additions below)
|
// Status inputs (mostly tied off; PR-G additions below)
|
||||||
reg status_request = 1'b0;
|
reg status_request = 1'b0;
|
||||||
@@ -127,6 +129,9 @@ module tb_usb_protocol_v2;
|
|||||||
.cmd_addr(cmd_addr),
|
.cmd_addr(cmd_addr),
|
||||||
.cmd_value(cmd_value),
|
.cmd_value(cmd_value),
|
||||||
.stream_control(stream_control),
|
.stream_control(stream_control),
|
||||||
|
// PR-U / M-8: per-frame snapshot of host_subframe_enable echoed in
|
||||||
|
// v2 frame byte 2 bits[5:3].
|
||||||
|
.subframe_enable(subframe_enable),
|
||||||
.status_request(status_request),
|
.status_request(status_request),
|
||||||
.status_cfar_threshold(status_cfar_threshold),
|
.status_cfar_threshold(status_cfar_threshold),
|
||||||
.status_stream_ctrl(status_stream_ctrl),
|
.status_stream_ctrl(status_stream_ctrl),
|
||||||
@@ -251,7 +256,10 @@ module tb_usb_protocol_v2;
|
|||||||
wait_clk(150);
|
wait_clk(150);
|
||||||
check_b("T2.1: byte0 = 0xAA", egress_bytes[0] == 8'hAA);
|
check_b("T2.1: byte0 = 0xAA", egress_bytes[0] == 8'hAA);
|
||||||
check_b("T2.2: byte1 = 0x02 (ver)", egress_bytes[1] == `RP_USB_PROTOCOL_VERSION);
|
check_b("T2.2: byte1 = 0x02 (ver)", egress_bytes[1] == `RP_USB_PROTOCOL_VERSION);
|
||||||
check_b("T2.3: byte2 = stream flags=0", egress_bytes[2] == 8'h00);
|
// PR-U / M-8: byte 2 = {2'b00, subframe_enable[2:0], stream[2:0]}.
|
||||||
|
// subframe_enable defaults to 3'b111 → byte 2 = (0b111 << 3) | 0 = 0x38.
|
||||||
|
check_b("T2.3: byte2 = {00, sf=111, stream=0} = 0x38",
|
||||||
|
egress_bytes[2] == 8'h38);
|
||||||
// Byte 3-4 = frame_number snapshot. snapshot latches OLD frame_number
|
// Byte 3-4 = frame_number snapshot. snapshot latches OLD frame_number
|
||||||
// at frame_complete (NBA), so first frame emitted carries fn=0.
|
// at frame_complete (NBA), so first frame emitted carries fn=0.
|
||||||
check_b("T2.4: byte3 = fn[15:8]=0", egress_bytes[3] == 8'h00);
|
check_b("T2.4: byte3 = fn[15:8]=0", egress_bytes[3] == 8'h00);
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
* Frame packet (FPGA→Host): variable length, up to 74,762 bytes
|
* Frame packet (FPGA→Host): variable length, up to 74,762 bytes
|
||||||
* Byte 0: 0xAA (frame start header)
|
* Byte 0: 0xAA (frame start header)
|
||||||
* Byte 1: 0x02 (PROTOCOL VERSION — host MUST reject any other value)
|
* Byte 1: 0x02 (PROTOCOL VERSION — host MUST reject any other value)
|
||||||
* Byte 2: Stream flags {5'b0, stream_cfar, stream_doppler, stream_range}
|
* Byte 2: Flags byte. Layout (PR-U / M-8 widened bits[5:3]):
|
||||||
|
* bits[7:6] = 2'b00 reserved
|
||||||
|
* bits[5:3] = subframe_enable[2:0] = {LONG, MEDIUM, SHORT}
|
||||||
|
* (host_subframe_enable snapshot at frame_complete)
|
||||||
|
* bits[2:0] = {stream_cfar, stream_doppler, stream_range}
|
||||||
* Bytes 3-4: Frame number (uint16, MSB first)
|
* Bytes 3-4: Frame number (uint16, MSB first)
|
||||||
* Bytes 5-6: Range bin count (uint16, MSB first) = `RP_NUM_RANGE_BINS` (512)
|
* Bytes 5-6: Range bin count (uint16, MSB first) = `RP_NUM_RANGE_BINS` (512)
|
||||||
* Bytes 7-8: Doppler bin count (uint16, MSB first) = `RP_NUM_DOPPLER_BINS` (48)
|
* Bytes 7-8: Doppler bin count (uint16, MSB first) = `RP_NUM_DOPPLER_BINS` (48)
|
||||||
@@ -127,6 +131,13 @@ module usb_data_interface_ft2232h (
|
|||||||
// Stream control input (clk domain, CDC'd internally)
|
// Stream control input (clk domain, CDC'd internally)
|
||||||
input wire [5:0] stream_control,
|
input wire [5:0] stream_control,
|
||||||
|
|
||||||
|
// PR-U / M-8: per-frame sub-frame enable mask (clk domain, CDC'd
|
||||||
|
// internally, snapshotted at frame_complete). {LONG, MEDIUM, SHORT}.
|
||||||
|
// Echoed in v2 frame byte 2 bits[5:3] so the host CRT can detect
|
||||||
|
// when an operator disables a sub-frame and downgrade confidence
|
||||||
|
// (default 3'b111 keeps the production 3-PRI ladder behavior).
|
||||||
|
input wire [2:0] subframe_enable,
|
||||||
|
|
||||||
// Status readback inputs (clk domain, CDC'd internally)
|
// Status readback inputs (clk domain, CDC'd internally)
|
||||||
input wire status_request,
|
input wire status_request,
|
||||||
input wire [15:0] status_cfar_threshold,
|
input wire [15:0] status_cfar_threshold,
|
||||||
@@ -644,14 +655,20 @@ wire stream_cfar_en = stream_ctrl_sync_1[2];
|
|||||||
// --- Frame metadata snapshot (latched in clk domain, stable for ft_clk read) ---
|
// --- Frame metadata snapshot (latched in clk domain, stable for ft_clk read) ---
|
||||||
reg [15:0] frame_number_snapshot;
|
reg [15:0] frame_number_snapshot;
|
||||||
reg [2:0] stream_flags_snapshot; // PR-G: 3 bits used (range/doppler/cfar)
|
reg [2:0] stream_flags_snapshot; // PR-G: 3 bits used (range/doppler/cfar)
|
||||||
|
// PR-U / M-8: snapshot of host_subframe_enable taken at frame_complete so the
|
||||||
|
// host parser sees the mask that was active for THIS frame (atomic per-frame).
|
||||||
|
// Stable when ft_clk reads it via the frame_ready toggle synchronizer.
|
||||||
|
reg [2:0] subframe_enable_snapshot; // {LONG, MEDIUM, SHORT}
|
||||||
|
|
||||||
always @(posedge clk or negedge reset_n) begin
|
always @(posedge clk or negedge reset_n) begin
|
||||||
if (!reset_n) begin
|
if (!reset_n) begin
|
||||||
frame_number_snapshot <= 16'd0;
|
frame_number_snapshot <= 16'd0;
|
||||||
stream_flags_snapshot <= 3'b111; // PR-G: all 3 streams on (range|doppler|cfar)
|
stream_flags_snapshot <= 3'b111; // PR-G: all 3 streams on (range|doppler|cfar)
|
||||||
|
subframe_enable_snapshot <= 3'b111; // PR-U: all 3 sub-frames on (production default)
|
||||||
end else if (frame_complete) begin
|
end else if (frame_complete) begin
|
||||||
frame_number_snapshot <= frame_number;
|
frame_number_snapshot <= frame_number;
|
||||||
stream_flags_snapshot <= stream_control[2:0]; // PR-G: ignore reserved [5:3]
|
stream_flags_snapshot <= stream_control[2:0]; // PR-G: ignore reserved [5:3]
|
||||||
|
subframe_enable_snapshot <= subframe_enable;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -968,7 +985,10 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
|
|||||||
case (wr_byte_idx[3:0])
|
case (wr_byte_idx[3:0])
|
||||||
4'd0: ft_data_out <= HEADER;
|
4'd0: ft_data_out <= HEADER;
|
||||||
4'd1: ft_data_out <= `RP_USB_PROTOCOL_VERSION; // 0x02
|
4'd1: ft_data_out <= `RP_USB_PROTOCOL_VERSION; // 0x02
|
||||||
4'd2: ft_data_out <= {5'b00000, stream_flags_snapshot};
|
// PR-U / M-8: byte 2 = {2'b00, subframe_enable[2:0], stream_flags[2:0]}.
|
||||||
|
// Was {5'b00000, stream_flags_snapshot}; bits[5:3] now carry
|
||||||
|
// the per-frame sub-frame mask snapshot {LONG, MEDIUM, SHORT}.
|
||||||
|
4'd2: ft_data_out <= {2'b00, subframe_enable_snapshot, stream_flags_snapshot};
|
||||||
4'd3: ft_data_out <= frame_number_snapshot[15:8];
|
4'd3: ft_data_out <= frame_number_snapshot[15:8];
|
||||||
4'd4: ft_data_out <= frame_number_snapshot[7:0];
|
4'd4: ft_data_out <= frame_number_snapshot[7:0];
|
||||||
4'd5: ft_data_out <= NUM_RANGE_BINS[15:8]; // 512 >> 8 = 2
|
4'd5: ft_data_out <= NUM_RANGE_BINS[15:8]; // 512 >> 8 = 2
|
||||||
|
|||||||
@@ -99,12 +99,22 @@ BULK_FRAME_MAX_SIZE = (BULK_FRAME_HEADER_SIZE + BULK_RANGE_SECTION_BYTES
|
|||||||
+ BULK_DOPPLER_MAG_BYTES + BULK_DETECT_DENSE_BYTES
|
+ BULK_DOPPLER_MAG_BYTES + BULK_DETECT_DENSE_BYTES
|
||||||
+ BULK_FOOTER_SIZE) # 56330
|
+ BULK_FOOTER_SIZE) # 56330
|
||||||
|
|
||||||
# Bulk-frame format flag bits (matches stream_ctrl_sync_1 layout in RTL).
|
# Bulk-frame format flag bits.
|
||||||
# Only the low 3 bits are used on the wire; bits [7:3] are reserved-zero.
|
# Layout (PR-U / M-8):
|
||||||
|
# bits[2:0] = stream flags {cfar, doppler, range} (matches stream_ctrl_sync_1)
|
||||||
|
# bits[5:3] = subframe_enable mask {LONG, MEDIUM, SHORT}
|
||||||
|
# snapshot of host_subframe_enable at frame_complete (FPGA opcode 0x19).
|
||||||
|
# Default 3'b111 keeps the production 3-PRI ladder; mask != 3'b111
|
||||||
|
# means an operator disabled a sub-frame and the host should
|
||||||
|
# downgrade CRT confidence (dbin // 16 attribution would mis-bin).
|
||||||
|
# bits[7:6] = reserved-zero — any non-zero in this mask rejects the frame.
|
||||||
BULK_FLAG_STREAM_RANGE = 0x01
|
BULK_FLAG_STREAM_RANGE = 0x01
|
||||||
BULK_FLAG_STREAM_DOPPLER = 0x02
|
BULK_FLAG_STREAM_DOPPLER = 0x02
|
||||||
BULK_FLAG_STREAM_CFAR = 0x04
|
BULK_FLAG_STREAM_CFAR = 0x04
|
||||||
BULK_FLAGS_RESERVED_MASK = 0xF8 # any bit in this mask set → reject frame
|
BULK_SUBFRAME_ENABLE_MASK = 0x38 # bits[5:3] = subframe_enable[2:0]
|
||||||
|
BULK_SUBFRAME_ENABLE_SHIFT = 3
|
||||||
|
BULK_SUBFRAME_ENABLE_ALL = 0b111 # SHORT | MEDIUM | LONG
|
||||||
|
BULK_FLAGS_RESERVED_MASK = 0xC0 # any bit in this mask set → reject frame
|
||||||
|
|
||||||
|
|
||||||
class Opcode(IntEnum):
|
class Opcode(IntEnum):
|
||||||
@@ -124,6 +134,7 @@ class Opcode(IntEnum):
|
|||||||
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)
|
||||||
"""
|
"""
|
||||||
# --- Basic control (0x01-0x04) ---
|
# --- Basic control (0x01-0x04) ---
|
||||||
RADAR_MODE = 0x01 # 2-bit mode select
|
RADAR_MODE = 0x01 # 2-bit mode select
|
||||||
@@ -146,6 +157,13 @@ class Opcode(IntEnum):
|
|||||||
MEDIUM_CHIRP = 0x17
|
MEDIUM_CHIRP = 0x17
|
||||||
MEDIUM_LISTEN = 0x18
|
MEDIUM_LISTEN = 0x18
|
||||||
|
|
||||||
|
# PR-U / M-8: 3-bit sub-frame enable mask {LONG, MEDIUM, SHORT}. Default
|
||||||
|
# 3'b111 = all on. Setting != 3'b111 disables a sub-frame at the chirp
|
||||||
|
# scheduler; the FPGA echoes the mask in v2 frame byte 2 bits[5:3] so the
|
||||||
|
# host CRT downgrades confidence to UNKNOWN (dbin // 16 attribution would
|
||||||
|
# otherwise be wrong when the scheduler skips a sub-frame).
|
||||||
|
SUBFRAME_ENABLE = 0x19
|
||||||
|
|
||||||
# --- Signal processing (0x20-0x27) ---
|
# --- Signal processing (0x20-0x27) ---
|
||||||
RANGE_MODE = 0x20
|
RANGE_MODE = 0x20
|
||||||
CFAR_GUARD = 0x21
|
CFAR_GUARD = 0x21
|
||||||
@@ -209,6 +227,14 @@ class RadarFrame:
|
|||||||
# mag_only=1 (the only mode FPGA emits today). I/Q arrays will be zero;
|
# mag_only=1 (the only mode FPGA emits today). I/Q arrays will be zero;
|
||||||
# `magnitude` carries the per-cell Manhattan magnitude from the FPGA.
|
# `magnitude` carries the per-cell Manhattan magnitude from the FPGA.
|
||||||
mag_only: bool = False
|
mag_only: bool = False
|
||||||
|
# PR-U / M-8: 3-bit sub-frame mask {LONG, MEDIUM, SHORT} snapshot from
|
||||||
|
# the FPGA at frame_complete (v2 frame byte 2 bits[5:3]). Default 0b111
|
||||||
|
# is the production 3-PRI ladder. Anything else means an operator
|
||||||
|
# disabled a sub-frame and the host CRT must downgrade confidence —
|
||||||
|
# `dbin // 16 → {SHORT, MED, LONG}` no longer attributes correctly when
|
||||||
|
# the chirp scheduler runs only the enabled sub-frames into 48 chirp
|
||||||
|
# slots in the doppler_processor.
|
||||||
|
subframe_enable: int = 0b111
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -456,10 +482,14 @@ class RadarProtocol:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
flags = raw[offset + 2]
|
flags = raw[offset + 2]
|
||||||
# Only the low 3 bits are defined (range/doppler/cfar). Any reserved
|
# bits[2:0] = stream {cfar,doppler,range}; bits[5:3] = subframe_enable;
|
||||||
# bit set means a future revision or corruption — reject and resync.
|
# bits[7:6] reserved-zero. Any reserved bit set means a future revision
|
||||||
|
# or corruption — reject and resync.
|
||||||
if flags & BULK_FLAGS_RESERVED_MASK:
|
if flags & BULK_FLAGS_RESERVED_MASK:
|
||||||
return None
|
return None
|
||||||
|
# PR-U / M-8: surface the per-frame sub-frame mask so the host CRT can
|
||||||
|
# detect mask != 0b111 and degrade rather than mis-attribute the SF axis.
|
||||||
|
subframe_enable = (flags & BULK_SUBFRAME_ENABLE_MASK) >> BULK_SUBFRAME_ENABLE_SHIFT
|
||||||
|
|
||||||
frame_number = (raw[offset + 3] << 8) | raw[offset + 4]
|
frame_number = (raw[offset + 3] << 8) | raw[offset + 4]
|
||||||
n_range = (raw[offset + 5] << 8) | raw[offset + 6]
|
n_range = (raw[offset + 5] << 8) | raw[offset + 6]
|
||||||
@@ -499,6 +529,7 @@ class RadarProtocol:
|
|||||||
return {
|
return {
|
||||||
"frame_number": frame_number,
|
"frame_number": frame_number,
|
||||||
"flags": flags,
|
"flags": flags,
|
||||||
|
"subframe_enable": subframe_enable,
|
||||||
"n_range": n_range,
|
"n_range": n_range,
|
||||||
"n_doppler": n_doppler,
|
"n_doppler": n_doppler,
|
||||||
"range_profile": range_profile,
|
"range_profile": range_profile,
|
||||||
@@ -732,7 +763,11 @@ class FT2232HConnection:
|
|||||||
buf = bytearray(BULK_FRAME_MAX_SIZE)
|
buf = bytearray(BULK_FRAME_MAX_SIZE)
|
||||||
buf[0] = HEADER_BYTE
|
buf[0] = HEADER_BYTE
|
||||||
buf[1] = RP_USB_PROTOCOL_VERSION
|
buf[1] = RP_USB_PROTOCOL_VERSION
|
||||||
buf[2] = flags & 0x07 # only 3 stream-enable bits valid; reserved zero
|
# PR-U / M-8: byte 2 = bits[2:0] stream + bits[5:3] subframe_enable +
|
||||||
|
# bits[7:6] reserved-zero. Mock emits the production 3-PRI ladder
|
||||||
|
# (mask = 0b111) so dashboards see CONFIRMED CRT confidence.
|
||||||
|
buf[2] = ((BULK_SUBFRAME_ENABLE_ALL << BULK_SUBFRAME_ENABLE_SHIFT)
|
||||||
|
| (flags & 0x07))
|
||||||
buf[3] = (self._mock_frame_num >> 8) & 0xFF
|
buf[3] = (self._mock_frame_num >> 8) & 0xFF
|
||||||
buf[4] = self._mock_frame_num & 0xFF
|
buf[4] = self._mock_frame_num & 0xFF
|
||||||
buf[5] = (NUM_RANGE_BINS >> 8) & 0xFF
|
buf[5] = (NUM_RANGE_BINS >> 8) & 0xFF
|
||||||
@@ -1120,6 +1155,9 @@ class RadarAcquisition(threading.Thread):
|
|||||||
# path implemented in the FPGA write FSM), so flag this for downstream
|
# path implemented in the FPGA write FSM), so flag this for downstream
|
||||||
# consumers that expect mag-only when reading from bulk.
|
# consumers that expect mag-only when reading from bulk.
|
||||||
frame.mag_only = True
|
frame.mag_only = True
|
||||||
|
# PR-U / M-8: per-frame snapshot of host_subframe_enable (FPGA opcode
|
||||||
|
# 0x19, default 0b111). The CRT extractor uses this to gate confidence.
|
||||||
|
frame.subframe_enable = int(parsed.get("subframe_enable", 0b111)) & 0x07
|
||||||
|
|
||||||
rprof = parsed["range_profile"]
|
rprof = parsed["range_profile"]
|
||||||
if rprof is not None:
|
if rprof is not None:
|
||||||
|
|||||||
@@ -370,16 +370,26 @@ class TestBulkFrameV2RoundTrip(unittest.TestCase):
|
|||||||
def _build_v2_frame(self, flags: int, frame_num: int = 0,
|
def _build_v2_frame(self, flags: int, frame_num: int = 0,
|
||||||
doppler: np.ndarray | None = None,
|
doppler: np.ndarray | None = None,
|
||||||
cfar_codes: np.ndarray | None = None,
|
cfar_codes: np.ndarray | None = None,
|
||||||
range_profile: np.ndarray | None = None) -> bytes:
|
range_profile: np.ndarray | None = None,
|
||||||
"""Construct a v2 frame the way usb_data_interface_ft2232h.v emits."""
|
subframe_enable: int = 0b111) -> bytes:
|
||||||
|
"""Construct a v2 frame the way usb_data_interface_ft2232h.v emits.
|
||||||
|
|
||||||
|
``subframe_enable`` lands in byte 2 bits[5:3] (PR-U / M-8). Caller
|
||||||
|
passes raw stream bits in ``flags`` (low 3 bits); helper composes the
|
||||||
|
full byte 2 = {2'b00, subframe_enable[2:0], stream[2:0]}.
|
||||||
|
"""
|
||||||
from radar_protocol import (
|
from radar_protocol import (
|
||||||
HEADER_BYTE, FOOTER_BYTE, RP_USB_PROTOCOL_VERSION,
|
HEADER_BYTE, FOOTER_BYTE, RP_USB_PROTOCOL_VERSION,
|
||||||
NUM_RANGE_BINS, NUM_DOPPLER_BINS,
|
NUM_RANGE_BINS, NUM_DOPPLER_BINS,
|
||||||
BULK_FLAG_STREAM_RANGE, BULK_FLAG_STREAM_DOPPLER, BULK_FLAG_STREAM_CFAR,
|
BULK_FLAG_STREAM_RANGE, BULK_FLAG_STREAM_DOPPLER, BULK_FLAG_STREAM_CFAR,
|
||||||
BULK_DETECT_BYTES_PER_RANGE,
|
BULK_DETECT_BYTES_PER_RANGE,
|
||||||
|
BULK_SUBFRAME_ENABLE_SHIFT,
|
||||||
)
|
)
|
||||||
|
flags_byte = (((subframe_enable & 0x07) << BULK_SUBFRAME_ENABLE_SHIFT)
|
||||||
|
| (flags & 0x07)
|
||||||
|
| (flags & 0xC0)) # preserve reserved bits if caller injects them
|
||||||
parts = [
|
parts = [
|
||||||
bytes([HEADER_BYTE, RP_USB_PROTOCOL_VERSION, flags & 0xFF]),
|
bytes([HEADER_BYTE, RP_USB_PROTOCOL_VERSION, flags_byte & 0xFF]),
|
||||||
struct.pack(">H", frame_num),
|
struct.pack(">H", frame_num),
|
||||||
struct.pack(">H", NUM_RANGE_BINS),
|
struct.pack(">H", NUM_RANGE_BINS),
|
||||||
struct.pack(">H", NUM_DOPPLER_BINS),
|
struct.pack(">H", NUM_DOPPLER_BINS),
|
||||||
@@ -431,7 +441,11 @@ class TestBulkFrameV2RoundTrip(unittest.TestCase):
|
|||||||
parsed = RadarProtocol.parse_bulk_frame(frame)
|
parsed = RadarProtocol.parse_bulk_frame(frame)
|
||||||
self.assertIsNotNone(parsed)
|
self.assertIsNotNone(parsed)
|
||||||
self.assertEqual(parsed["frame_number"], 42)
|
self.assertEqual(parsed["frame_number"], 42)
|
||||||
self.assertEqual(parsed["flags"], flags)
|
# PR-U / M-8: byte 2 now packs subframe_enable into bits[5:3]; helper
|
||||||
|
# defaults to 0b111 (production 3-PRI ladder) so the wire flags byte
|
||||||
|
# is (0b111 << 3) | 0x07 = 0x3F.
|
||||||
|
self.assertEqual(parsed["flags"], flags | (0b111 << 3))
|
||||||
|
self.assertEqual(parsed["subframe_enable"], 0b111)
|
||||||
self.assertEqual(parsed["n_range"], NUM_RANGE_BINS)
|
self.assertEqual(parsed["n_range"], NUM_RANGE_BINS)
|
||||||
self.assertEqual(parsed["n_doppler"], NUM_DOPPLER_BINS)
|
self.assertEqual(parsed["n_doppler"], NUM_DOPPLER_BINS)
|
||||||
np.testing.assert_array_equal(parsed["range_profile"], rp)
|
np.testing.assert_array_equal(parsed["range_profile"], rp)
|
||||||
@@ -484,6 +498,56 @@ class TestBulkFrameV2RoundTrip(unittest.TestCase):
|
|||||||
self.assertEqual(boundaries[1], (len(f1), len(f1) + len(f2), "data"))
|
self.assertEqual(boundaries[1], (len(f1), len(f1) + len(f2), "data"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestSubframeEnableRoundTrip(TestBulkFrameV2RoundTrip):
|
||||||
|
"""PR-U / M-8: byte 2 bits[5:3] carry the per-frame sub-frame mask."""
|
||||||
|
|
||||||
|
def test_default_mask_round_trip(self):
|
||||||
|
"""Production default 0b111 round-trips and is the helper default."""
|
||||||
|
from radar_protocol import (
|
||||||
|
RadarProtocol, BULK_FLAG_STREAM_DOPPLER,
|
||||||
|
)
|
||||||
|
frame = self._build_v2_frame(BULK_FLAG_STREAM_DOPPLER, frame_num=1)
|
||||||
|
parsed = RadarProtocol.parse_bulk_frame(frame)
|
||||||
|
self.assertIsNotNone(parsed)
|
||||||
|
self.assertEqual(parsed["subframe_enable"], 0b111)
|
||||||
|
|
||||||
|
def test_short_disabled_mask(self):
|
||||||
|
"""subframe_enable = 0b110 (LONG|MEDIUM, no SHORT) survives the wire."""
|
||||||
|
from radar_protocol import (
|
||||||
|
RadarProtocol, BULK_FLAG_STREAM_DOPPLER,
|
||||||
|
)
|
||||||
|
frame = self._build_v2_frame(BULK_FLAG_STREAM_DOPPLER, frame_num=1,
|
||||||
|
subframe_enable=0b110)
|
||||||
|
parsed = RadarProtocol.parse_bulk_frame(frame)
|
||||||
|
self.assertIsNotNone(parsed)
|
||||||
|
self.assertEqual(parsed["subframe_enable"], 0b110)
|
||||||
|
|
||||||
|
def test_short_only_mask(self):
|
||||||
|
"""subframe_enable = 0b001 (SHORT only) survives the wire."""
|
||||||
|
from radar_protocol import (
|
||||||
|
RadarProtocol, BULK_FLAG_STREAM_DOPPLER,
|
||||||
|
)
|
||||||
|
frame = self._build_v2_frame(BULK_FLAG_STREAM_DOPPLER, frame_num=2,
|
||||||
|
subframe_enable=0b001)
|
||||||
|
parsed = RadarProtocol.parse_bulk_frame(frame)
|
||||||
|
self.assertIsNotNone(parsed)
|
||||||
|
self.assertEqual(parsed["subframe_enable"], 0b001)
|
||||||
|
|
||||||
|
def test_subframe_bits_no_longer_in_reserved_mask(self):
|
||||||
|
"""Bits[5:3] are now valid SF mask, not reserved — must NOT reject."""
|
||||||
|
from radar_protocol import (
|
||||||
|
RadarProtocol, BULK_FLAGS_RESERVED_MASK,
|
||||||
|
BULK_SUBFRAME_ENABLE_MASK,
|
||||||
|
)
|
||||||
|
# The new reserved mask must not overlap the SF-enable bit field.
|
||||||
|
self.assertEqual(BULK_FLAGS_RESERVED_MASK & BULK_SUBFRAME_ENABLE_MASK, 0)
|
||||||
|
# And bit 6 (top of new reserved mask) STILL rejects.
|
||||||
|
from radar_protocol import BULK_FLAG_STREAM_RANGE
|
||||||
|
frame = self._build_v2_frame(BULK_FLAG_STREAM_RANGE | 0x40)
|
||||||
|
bad = bytes([frame[0], frame[1], frame[2] | 0x40]) + frame[3:]
|
||||||
|
self.assertIsNone(RadarProtocol.parse_bulk_frame(bad))
|
||||||
|
|
||||||
|
|
||||||
class TestStatusPacketV2RoundTrip(unittest.TestCase):
|
class TestStatusPacketV2RoundTrip(unittest.TestCase):
|
||||||
"""PR-G v2 status packet: 7 status_words / 30 bytes."""
|
"""PR-G v2 status packet: 7 status_words / 30 bytes."""
|
||||||
|
|
||||||
@@ -1492,6 +1556,52 @@ class TestExtractTargetsFromFrameCrt(unittest.TestCase):
|
|||||||
self.assertGreater(targets[0].longitude, 12.5)
|
self.assertGreater(targets[0].longitude, 12.5)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrtSubframeMaskGating(unittest.TestCase):
|
||||||
|
"""PR-U / M-8: CRT downgrades confidence to AMBIGUOUS when SF mask != 0b111."""
|
||||||
|
|
||||||
|
def _make_3pri_frame(self, subframe_enable: int):
|
||||||
|
from radar_protocol import RadarFrame
|
||||||
|
frame = RadarFrame()
|
||||||
|
# Detection at rbin=10 in all 3 sub-frames at bin 3 — would normally
|
||||||
|
# CONFIRM, but a non-default mask must force AMBIGUOUS.
|
||||||
|
for rbin, dbin, mag in [(10, 3, 1000.0), (10, 19, 800.0), (10, 35, 1200.0)]:
|
||||||
|
frame.detections[rbin, dbin] = 1
|
||||||
|
frame.magnitude[rbin, dbin] = mag
|
||||||
|
frame.detection_count = int(frame.detections.sum())
|
||||||
|
frame.timestamp = 1.0
|
||||||
|
frame.subframe_enable = subframe_enable
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def test_default_mask_keeps_confirmed_path(self):
|
||||||
|
from v7.processing import extract_targets_from_frame_crt
|
||||||
|
from v7.models import WaveformConfig
|
||||||
|
wc = WaveformConfig()
|
||||||
|
frame = self._make_3pri_frame(0b111)
|
||||||
|
targets = extract_targets_from_frame_crt(frame, wc)
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertEqual(targets[0].velocity_confidence, "CONFIRMED")
|
||||||
|
|
||||||
|
def test_short_disabled_forces_ambiguous(self):
|
||||||
|
"""SHORT off → CRT can't trust dbin // 16 attribution → AMBIGUOUS."""
|
||||||
|
from v7.processing import extract_targets_from_frame_crt
|
||||||
|
from v7.models import WaveformConfig
|
||||||
|
wc = WaveformConfig()
|
||||||
|
frame = self._make_3pri_frame(0b110)
|
||||||
|
targets = extract_targets_from_frame_crt(frame, wc)
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertEqual(targets[0].velocity_confidence, "AMBIGUOUS")
|
||||||
|
|
||||||
|
def test_long_only_forces_ambiguous(self):
|
||||||
|
"""LONG only mask: scheduler skips SHORT+MEDIUM, all targets AMBIGUOUS."""
|
||||||
|
from v7.processing import extract_targets_from_frame_crt
|
||||||
|
from v7.models import WaveformConfig
|
||||||
|
wc = WaveformConfig()
|
||||||
|
frame = self._make_3pri_frame(0b100)
|
||||||
|
targets = extract_targets_from_frame_crt(frame, wc)
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertEqual(targets[0].velocity_confidence, "AMBIGUOUS")
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Test: PR-Q.6 — workers route through extract_targets_from_frame_crt
|
# Test: PR-Q.6 — workers route through extract_targets_from_frame_crt
|
||||||
# RadarDataWorker._run_host_dsp + ReplayWorker._extract_targets must use the
|
# RadarDataWorker._run_host_dsp + ReplayWorker._extract_targets must use the
|
||||||
@@ -1651,6 +1761,11 @@ class TestOpcodeEnumFillIn(unittest.TestCase):
|
|||||||
from radar_protocol import Opcode
|
from radar_protocol import Opcode
|
||||||
self.assertEqual(Opcode.ADC_FORMAT.value, 0x33)
|
self.assertEqual(Opcode.ADC_FORMAT.value, 0x33)
|
||||||
|
|
||||||
|
def test_subframe_enable_opcode(self):
|
||||||
|
"""PR-U / M-8: 0x19 sets host_subframe_enable mask."""
|
||||||
|
from radar_protocol import Opcode
|
||||||
|
self.assertEqual(Opcode.SUBFRAME_ENABLE.value, 0x19)
|
||||||
|
|
||||||
def test_no_duplicate_opcodes(self):
|
def test_no_duplicate_opcodes(self):
|
||||||
"""All Opcode values are unique (catches accidental collisions)."""
|
"""All Opcode values are unique (catches accidental collisions)."""
|
||||||
from radar_protocol import Opcode
|
from radar_protocol import Opcode
|
||||||
@@ -1674,6 +1789,21 @@ class TestSoftwareFpgaCfarAlphaSoft(unittest.TestCase):
|
|||||||
self.assertEqual(fpga.cfar_alpha_soft, 0x34)
|
self.assertEqual(fpga.cfar_alpha_soft, 0x34)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSoftwareFpgaSubframeEnable(unittest.TestCase):
|
||||||
|
"""PR-U / M-8: SoftwareFPGA mirrors host_subframe_enable, masks to 3 bits."""
|
||||||
|
|
||||||
|
def test_default(self):
|
||||||
|
from v7.software_fpga import SoftwareFPGA
|
||||||
|
fpga = SoftwareFPGA()
|
||||||
|
self.assertEqual(fpga.subframe_enable, 0b111) # RP_DEF_SUBFRAME_ENABLE
|
||||||
|
|
||||||
|
def test_setter_masks_to_3_bits(self):
|
||||||
|
from v7.software_fpga import SoftwareFPGA
|
||||||
|
fpga = SoftwareFPGA()
|
||||||
|
fpga.set_subframe_enable(0xFE)
|
||||||
|
self.assertEqual(fpga.subframe_enable, 0b110)
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(_pyqt6_available(), "PyQt6 not installed")
|
@unittest.skipUnless(_pyqt6_available(), "PyQt6 not installed")
|
||||||
class TestReplayOpcodeDispatch(unittest.TestCase):
|
class TestReplayOpcodeDispatch(unittest.TestCase):
|
||||||
"""M-6: replay dispatch routes 0x2D to SoftwareFPGA + acknowledges inert opcodes."""
|
"""M-6: replay dispatch routes 0x2D to SoftwareFPGA + acknowledges inert opcodes."""
|
||||||
@@ -1695,6 +1825,12 @@ class TestReplayOpcodeDispatch(unittest.TestCase):
|
|||||||
dispatch(fake, 0x2D, 42)
|
dispatch(fake, 0x2D, 42)
|
||||||
self.assertEqual(fake._software_fpga.cfar_alpha_soft, 42)
|
self.assertEqual(fake._software_fpga.cfar_alpha_soft, 42)
|
||||||
|
|
||||||
|
def test_0x19_routed_to_set_subframe_enable(self):
|
||||||
|
"""PR-U / M-8: 0x19 lands on SoftwareFPGA.set_subframe_enable."""
|
||||||
|
dispatch, fake = self._dashboard_with_replay()
|
||||||
|
dispatch(fake, 0x19, 0b101)
|
||||||
|
self.assertEqual(fake._software_fpga.subframe_enable, 0b101)
|
||||||
|
|
||||||
def test_inert_opcode_does_not_raise(self):
|
def test_inert_opcode_does_not_raise(self):
|
||||||
"""Inert opcodes (e.g. 0x32 ADC_PWDN) accepted without exception."""
|
"""Inert opcodes (e.g. 0x32 ADC_PWDN) accepted without exception."""
|
||||||
dispatch, fake = self._dashboard_with_replay()
|
dispatch, fake = self._dashboard_with_replay()
|
||||||
|
|||||||
@@ -1676,6 +1676,10 @@ class RadarDashboard(QMainWindow):
|
|||||||
0x2B: lambda v: fpga.set_agc_params(decay=v),
|
0x2B: lambda v: fpga.set_agc_params(decay=v),
|
||||||
0x2C: lambda v: fpga.set_agc_params(holdoff=v),
|
0x2C: lambda v: fpga.set_agc_params(holdoff=v),
|
||||||
0x2D: lambda v: fpga.set_cfar_alpha_soft(v),
|
0x2D: lambda v: fpga.set_cfar_alpha_soft(v),
|
||||||
|
# PR-U / M-8: track the operator's sub-frame mask so subsequent
|
||||||
|
# frames the host parses use the correct CRT confidence rules
|
||||||
|
# (replay frames carry the mask the FPGA echoed at capture time).
|
||||||
|
0x19: lambda v: fpga.set_subframe_enable(v),
|
||||||
}
|
}
|
||||||
# Inert in replay: RTL-only chirp timing / range mode / self-test /
|
# Inert in replay: RTL-only chirp timing / range mode / self-test /
|
||||||
# status / ADC strap. The recorded I/Q already reflects whatever
|
# status / ADC strap. The recorded I/Q already reflects whatever
|
||||||
|
|||||||
@@ -698,6 +698,15 @@ def extract_targets_from_frame_crt(
|
|||||||
gps=gps,
|
gps=gps,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# PR-U / M-8: when the operator disabled a sub-frame at the FPGA, the
|
||||||
|
# chirp_scheduler runs only the enabled SFs but doppler_processor still
|
||||||
|
# emits 48 chirp slots — `dbin // 16 → {SHORT, MED, LONG}` no longer
|
||||||
|
# attributes correctly. Force AMBIGUOUS for every target so the dashboard
|
||||||
|
# column flags it red. Default 0b111 keeps the production happy path on
|
||||||
|
# the CONFIRMED branch via the normal CRT logic.
|
||||||
|
sf_mask = getattr(frame, "subframe_enable", 0b111) & 0x07
|
||||||
|
sf_mask_invalid = (sf_mask != 0b111)
|
||||||
|
|
||||||
chirps_per_sf = waveform.chirps_per_subframe # 16
|
chirps_per_sf = waveform.chirps_per_subframe # 16
|
||||||
v_res_per_sf_all = [
|
v_res_per_sf_all = [
|
||||||
waveform.velocity_resolution_short_mps,
|
waveform.velocity_resolution_short_mps,
|
||||||
@@ -746,6 +755,8 @@ def extract_targets_from_frame_crt(
|
|||||||
v_est, confidence, alias_set = unfold_velocity_crt(
|
v_est, confidence, alias_set = unfold_velocity_crt(
|
||||||
v_meas_list, v_unamb_list, v_res_list, max_alias_k=max_alias_k,
|
v_meas_list, v_unamb_list, v_res_list, max_alias_k=max_alias_k,
|
||||||
)
|
)
|
||||||
|
if sf_mask_invalid:
|
||||||
|
confidence = "AMBIGUOUS"
|
||||||
|
|
||||||
range_m = float(rbin) * range_resolution
|
range_m = float(rbin) * range_resolution
|
||||||
snr = 10.0 * math.log10(max(peak_mag, 1.0)) if peak_mag > 0 else 0.0
|
snr = 10.0 * math.log10(max(peak_mag, 1.0)) if peak_mag > 0 else 0.0
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ class SoftwareFPGA:
|
|||||||
self.agc_decay: int = 1 # 0x2B
|
self.agc_decay: int = 1 # 0x2B
|
||||||
self.agc_holdoff: int = 4 # 0x2C
|
self.agc_holdoff: int = 4 # 0x2C
|
||||||
|
|
||||||
|
# PR-U / M-8: 3-bit sub-frame mask {LONG, MEDIUM, SHORT}. Default 0b111
|
||||||
|
# = production 3-PRI ladder. Tracked only — replay frames are already
|
||||||
|
# rendered, so the mask doesn't affect playback math here. Surfaces in
|
||||||
|
# the parsed RadarFrame from radar_protocol so the CRT extractor sees it.
|
||||||
|
self.subframe_enable: int = 0b111 # 0x19
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Register setters (same interface as UART commands to real FPGA)
|
# Register setters (same interface as UART commands to real FPGA)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -133,6 +139,9 @@ class SoftwareFPGA:
|
|||||||
def set_cfar_alpha_soft(self, val: int) -> None:
|
def set_cfar_alpha_soft(self, val: int) -> None:
|
||||||
self.cfar_alpha_soft = int(val) & 0xFF
|
self.cfar_alpha_soft = int(val) & 0xFF
|
||||||
|
|
||||||
|
def set_subframe_enable(self, val: int) -> None:
|
||||||
|
self.subframe_enable = int(val) & 0x07
|
||||||
|
|
||||||
def set_cfar_mode(self, val: int) -> None:
|
def set_cfar_mode(self, val: int) -> None:
|
||||||
self.cfar_mode = int(val) & 0x03
|
self.cfar_mode = int(val) & 0x03
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user