PR-AB.b expanded commit 5: Beam-ready handshake (RTL + MCU + GUI)

Wire a per-frame MCU→FPGA "beam pattern ready" handshake so the chirp
scheduler can stall between 48-chirp frames until the MCU finishes
writing the next ADAR1000 pattern. The legacy unused stm32_new_chirp
input on PD8 is repurposed as stm32_beam_ready; chirp_scheduler.v gets
a new S_BEAM_WAIT state entered after each frame_pulse and an 80 ms
watchdog so a missed MCU toggle degrades to wall-clock cadence with a
sticky telemetry bit rather than stalling the radar.

Cold-reset defaults the handshake off (host_handshake_enable=0, new
opcode 0x1A); the GUI opts in once the MCU PD8 wiring is verified on
the bench. Both the FT601 and FT2232H status word 4 paths get the new
beam_handshake_watchdog_fired sticky at bit [1] (reclaimed from the
range_mode retirement in commit 1).

RTL:
- chirp_scheduler.v: 2-FF ASYNC_REG sync on beam_ready_async; 1-cycle
  edge detect (any transition, MCU side uses HAL_GPIO_TogglePin); new
  S_BEAM_WAIT state entered at frame_pulse when host_handshake_enable=1;
  23-bit beam_watchdog counter with BEAM_WATCHDOG_MAX = 8_000_000 (~80 ms
  at 100 MHz, ~10 nominal frames); beam_handshake_watchdog_fired output
  sticky across mixers_enable cycles, cleared only by reset_n; mid-wait
  disable releases the FSM so dropping the opcode never strands the
  radar between frames.
- radar_receiver_final.v: thread stm32_beam_ready_async +
  host_handshake_enable + beam_handshake_watchdog_fired through the
  scheduler instance.
- radar_system_top.v: rename input port stm32_new_chirp → stm32_beam_ready;
  add host_handshake_enable register (cold-reset = 1'b0); opcode 0x1A
  dispatch (value[0]); add rx_beam_handshake_watchdog wire; pack into
  status_words[4][1] in both USB paths.
- radar_system_top_50t.v: rename wrapper port + sub-instance wiring.
- usb_data_interface.v + usb_data_interface_ft2232h.v: add
  status_beam_handshake_watchdog input + 2-FF level CDC (same convention
  as F-6.4 / F-1.2 stickies); refresh word-4 layout doc comment; pack
  beam_handshake_wd_sync_1 into status_words[4][1].

XDC:
- xc7a50t_ftg256.xdc + xc7a200t_fbg484.xdc: rename stm32_new_chirp port
  references to stm32_beam_ready (same PD8 pin, F13 on 50T / L18 on
  200T).

MCU:
- main.h: add FPGA_BEAM_READY_Pin = GPIO_PIN_8 + FPGA_BEAM_READY_GPIO_Port
  = GPIOD alongside the existing FPGA_FRAME_PULSE alias.
- main.cpp:runRadarPulseSequence: insert HAL_GPIO_TogglePin(GPIOD,
  GPIO_PIN_8) after each setCustomBeamPattern16(RX) — once after the
  per-azimuth broadside (vector_0), once after matrix1, once after
  matrix2 — between the SPI burst completion and waitForFramePulse.

GUI:
- radar_protocol.py: Opcode.HANDSHAKE_ENABLE = 0x1A;
  StatusResponse.beam_handshake_watchdog = 0 default; parse word 4 bit
  [1] in parse_status_packet; update word-4 layout comment.
- test_GUI_V65_Tk.py: add beam_handshake_watchdog kwarg to
  _make_status_packet (sets bit [1] of word 4); refresh
  test_parse_status_word4_layout_co_spec to cover the new bit (used+9=32);
  add test_parse_status_beam_handshake_watchdog round-trip;
  test_handshake_enable_opcode pins 0x1A; defaults / chirps_mismatch /
  agc-coexist tests gain a watchdog==0 assertion; bump
  test_all_rtl_opcodes_present expected set to include 0x17/0x18/0x19/0x1A.

TB:
- new tb_chirp_scheduler_handshake.v (16 checks): legacy open-loop, edge
  exit (rising + falling), 200-cycle idle hold, watchdog auto-advance
  via force on dut.beam_watchdog, sticky-survives-mixers_disable,
  mid-wait disable release, reset_n clears sticky.
- run_regression.sh: register the new TB in PHASE 1.
- tb_radar_receiver_final.v: tie the 3 new receiver ports off
  (beam_ready_async=0, handshake_enable=0, watchdog unconnected).
- tb_system_mechanics.v / tb_system_opcodes.v: explicit
  .stm32_beam_ready(1'b0) connection (the cold-reset
  host_handshake_enable=0 keeps the FSM out of S_BEAM_WAIT).
- tb_usb_data_interface.v / tb_usb_protocol_v2.v / tb_e2e_dsp_to_host.v /
  tb_ft2232h_frame_drop.v: tie .status_beam_handshake_watchdog(1'b0).

Ride-along ruff sweep (14 → 0 across the repo):
- tb/cosim/compare_independent.py: RUF003 — '5×' → 'at least 5x'.
- tb/cosim/gen_e2e_expected.py: noqa: E402 on the post-sys.path import;
  drop unused EXPECTED_RANGE_BIN + EXPECTED_DOPPLER_BIN_PER_SF imports;
  fold the detect-class slot if/else into a ternary (SIM108).
- tb/cosim/gen_e2e_stimulus.py: drop int() wrapping round() at four
  call sites (RUF046 — round() already returns int in Python 3);
  rewrite the range-bin derivation comment block from code-like
  `# range_bin = ...` to prose (ERA001); strip stray f from
  placeholder-free error string (F541).
- tb/cosim/tb_e2e_dsp_to_host_parse.py: open(path, 'r') → open(path)
  (UP015).
- v7/dashboard.py: '3×' → '3x' (RUF003); drop quotes from
  'StatusResponse | None' annotation (UP037, file already has
  `from __future__ import annotations`).

CI summary (all suites green pre-commit):
- ruff: All checks passed!
- FPGA regression (iverilog): 43 / 0 / 0 (incl. new handshake TB 16/16).
- MCU tests: 51 / 0 + 34 / 0 + 13 / 13 ADAR1000_AGC.
- GUI Tk (test_GUI_V65_Tk): 120 / 0.
- GUI v7 (test_v7): 152 / 0.

Production rollout note: bitstream cold-resets with host_handshake_enable=0
so existing flashes keep their open-loop cadence until the GUI sends
opcode 0x1A=1. Once enabled, the per-pattern dwell tracks both the chirp
ladder (PD14 frame_pulse from commit-3 work) and the MCU pattern-write
completion (PD8 toggle from this commit), eliminating drift from the SPI
burst timing.
This commit is contained in:
Jason
2026-05-11 12:07:08 +05:45
parent 2e2c10baeb
commit 4989c33aa6
26 changed files with 642 additions and 86 deletions
@@ -626,6 +626,10 @@ void runRadarPulseSequence() {
DIAG("SYS", "Broadside reference (vector_0) — 1× per azimuth"); DIAG("SYS", "Broadside reference (vector_0) — 1× per azimuth");
adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::TX);
adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::RX); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::RX);
// PR-AB.b expanded commit 5: announce "beam pattern ready" to the FPGA so
// chirp_scheduler can release S_BEAM_WAIT. Harmless when the FPGA's
// host_handshake_enable=0 (legacy open-loop default).
HAL_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin);
waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); waitForFramePulse(FRAME_PULSE_TIMEOUT_MS);
m += m_max/2; m += m_max/2;
@@ -636,12 +640,14 @@ void runRadarPulseSequence() {
// Pattern 1: matrix1 (negative-θ scan, peak at -62°..-3°) // Pattern 1: matrix1 (negative-θ scan, peak at -62°..-3°)
adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::TX);
adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::RX); adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::RX);
HAL_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin); // commit 5 handshake
waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); waitForFramePulse(FRAME_PULSE_TIMEOUT_MS);
m += m_max/2; m += m_max/2;
// Pattern 2: matrix2 (positive-θ scan, peak at +3°..+62°) // Pattern 2: matrix2 (positive-θ scan, peak at +3°..+62°)
adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::TX);
adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::RX); adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::RX);
HAL_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin); // commit 5 handshake
waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); waitForFramePulse(FRAME_PULSE_TIMEOUT_MS);
m += m_max/2; m += m_max/2;
@@ -162,6 +162,17 @@ void Error_Handler(void);
#define FPGA_FRAME_PULSE_GPIO_Port FPGA_DIG6_GPIO_Port #define FPGA_FRAME_PULSE_GPIO_Port FPGA_DIG6_GPIO_Port
#define FPGA_FRAME_PULSE_EXTI_IRQn EXTI15_10_IRQn #define FPGA_FRAME_PULSE_EXTI_IRQn EXTI15_10_IRQn
/* PR-AB.b expanded commit 5: DIG_0 (PD8) carries the MCU per-beam-pattern
* ready toggle into the FPGA's chirp_scheduler beam-ready handshake.
* runRadarPulseSequence toggles this pin once per setCustomBeamPattern16(RX)
* call after the SPI burst completes, so the FPGA only starts the next 48-
* chirp frame once the new beam pattern is in place. The FPGA-side stall is
* gated by opcode 0x1A (host_handshake_enable, cold-reset = 0) so flashing
* the bitstream before the GUI enables the feature leaves the radar in its
* legacy open-loop cadence. */
#define FPGA_BEAM_READY_Pin GPIO_PIN_8
#define FPGA_BEAM_READY_GPIO_Port GPIOD
#define ADF4382_RX_CE_Pin GPIO_PIN_9 #define ADF4382_RX_CE_Pin GPIO_PIN_9
#define ADF4382_RX_CE_GPIO_Port GPIOG #define ADF4382_RX_CE_GPIO_Port GPIOG
#define ADF4382_RX_CS_Pin GPIO_PIN_10 #define ADF4382_RX_CS_Pin GPIO_PIN_10
+88 -3
View File
@@ -24,6 +24,15 @@
* Pulse outputs (chirp_pulse, subframe_pulse, frame_pulse) are 1-cycle * Pulse outputs (chirp_pulse, subframe_pulse, frame_pulse) are 1-cycle
* positive pulses, not toggles. * positive pulses, not toggles.
* *
* PR-AB.b expanded commit 5 beam-ready handshake: when
* host_handshake_enable=1, the FSM enters S_BEAM_WAIT after frame_pulse
* and only fires the next frame's first chirp once it observes an edge on
* beam_ready_async (MCU PD8 toggle) or the ~80 ms watchdog expires.
* Watchdog timeout sets the sticky output beam_handshake_watchdog_fired
* (cleared only by reset_n) so the host can spot patterns falling behind
* the chirp ladder. host_handshake_enable=0 preserves the legacy
* always-on chirp cadence.
*
* Clock domain: clk (100 MHz), async-low reset. * Clock domain: clk (100 MHz), async-low reset.
*/ */
@@ -55,6 +64,15 @@ module chirp_scheduler (
// TX-side cdc_async_fifo before mixers come up. // TX-side cdc_async_fifo before mixers come up.
input wire mixers_enable, input wire mixers_enable,
// PR-AB.b expanded commit 5: beam-ready handshake. beam_ready_async is the
// raw MCU PD8 GPIO toggle (CDC-synchronized inside this module on `clk`).
// host_handshake_enable gates whether the FSM stalls in S_BEAM_WAIT after
// each frame_pulse. Cold-reset default at the top level is 1'b0 (legacy
// open-loop cadence) — host enables via opcode 0x1A once the MCU's PD8
// toggle wiring is in place.
input wire beam_ready_async,
input wire host_handshake_enable,
// ====== Outputs ====== // ====== Outputs ======
output reg [1:0] wave_sel, // canonical waveform identity output reg [1:0] wave_sel, // canonical waveform identity
output reg chirp_pulse, // 1-cycle pulse: chirp begins this clk output reg chirp_pulse, // 1-cycle pulse: chirp begins this clk
@@ -66,7 +84,11 @@ module chirp_scheduler (
// Currently selected timing for the in-flight chirp (PR-E TX async FIFO) // Currently selected timing for the in-flight chirp (PR-E TX async FIFO)
output wire [15:0] cfg_chirp_cycles, output wire [15:0] cfg_chirp_cycles,
output wire [15:0] cfg_listen_cycles, output wire [15:0] cfg_listen_cycles,
output wire [15:0] cfg_guard_cycles output wire [15:0] cfg_guard_cycles,
// PR-AB.b expanded commit 5: sticky handshake watchdog flag, cleared
// only by reset_n. Plumbed into status_words[4][1] at the top level.
output reg beam_handshake_watchdog_fired
); );
// ============================================================================ // ============================================================================
@@ -140,6 +162,27 @@ assign cfg_chirp_cycles = sel_chirp_cycles;
assign cfg_listen_cycles = sel_listen_cycles; assign cfg_listen_cycles = sel_listen_cycles;
assign cfg_guard_cycles = host_guard_cycles; assign cfg_guard_cycles = host_guard_cycles;
// ============================================================================
// Beam-ready CDC + edge detection (PR-AB.b expanded commit 5).
// beam_ready_async is a slow MCU GPIO toggle (PD8). Two ASYNC_REG flops bring
// it into clk, then a one-cycle delay lets us detect any transition (rising or
// falling) — the MCU drives via HAL_GPIO_TogglePin once per beam pattern, so
// successive frames see alternating polarities.
// ============================================================================
(* ASYNC_REG = "TRUE" *) reg [1:0] beam_ready_sync;
reg beam_ready_q_prev;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
beam_ready_sync <= 2'b00;
beam_ready_q_prev <= 1'b0;
end else begin
beam_ready_sync <= {beam_ready_sync[0], beam_ready_async};
beam_ready_q_prev <= beam_ready_sync[1];
end
end
wire beam_ready_q = beam_ready_sync[1];
wire beam_ready_edge = (beam_ready_q != beam_ready_q_prev);
// ============================================================================ // ============================================================================
// Main FSM — auto-scan over enabled sub-frames. // Main FSM — auto-scan over enabled sub-frames.
// ============================================================================ // ============================================================================
@@ -148,9 +191,17 @@ localparam S_CHIRP = 3'd1;
localparam S_LISTEN = 3'd2; localparam S_LISTEN = 3'd2;
localparam S_GUARD = 3'd3; localparam S_GUARD = 3'd3;
localparam S_ADVANCE = 3'd4; localparam S_ADVANCE = 3'd4;
localparam S_BEAM_WAIT = 3'd5;
// Beam-ready watchdog: 23 bits at 100 MHz → ~83.9 ms = ~8 nominal frames
// (frame ≈ 8.05 ms full 3-PRI ladder, less when subframes are masked). Long
// enough to absorb MCU SPI bursts + scheduling jitter without auto-advancing,
// short enough to keep the radar moving when a pattern write actually drops.
localparam [22:0] BEAM_WATCHDOG_MAX = 23'd8_000_000;
reg [2:0] state; reg [2:0] state;
reg [16:0] timer; // 17 bits cover LONG+listen+guard worst case reg [16:0] timer; // 17 bits cover LONG+listen+guard worst case
reg [22:0] beam_watchdog; // counts clk cycles while in S_BEAM_WAIT
// Pre-computed wires used inside the FSM advance logic so non-blocking // Pre-computed wires used inside the FSM advance logic so non-blocking
// updates to subframe_id / wave_sel see the correct next value in the same // updates to subframe_id / wave_sel see the correct next value in the same
@@ -168,14 +219,19 @@ always @(posedge clk or negedge reset_n) begin
frame_pulse <= 1'b0; frame_pulse <= 1'b0;
chirp_counter <= 6'd0; chirp_counter <= 6'd0;
subframe_id <= 2'd0; subframe_id <= 2'd0;
beam_watchdog <= 23'd0;
beam_handshake_watchdog_fired <= 1'b0;
end else if (!mixers_enable) begin end else if (!mixers_enable) begin
// Master disable — quiesce the FSM so chirp_pulse never asserts and // Master disable — quiesce the FSM so chirp_pulse never asserts and
// the TX side stays at idle. // the TX side stays at idle. beam_handshake_watchdog_fired is sticky
// across mixers_enable cycles so the host can see late patterns even
// after a soft restart.
state <= S_IDLE; state <= S_IDLE;
timer <= 17'd0; timer <= 17'd0;
chirp_pulse <= 1'b0; chirp_pulse <= 1'b0;
subframe_pulse <= 1'b0; subframe_pulse <= 1'b0;
frame_pulse <= 1'b0; frame_pulse <= 1'b0;
beam_watchdog <= 23'd0;
end else begin end else begin
// Pulses default low — set high for one cycle on relevant transitions. // Pulses default low — set high for one cycle on relevant transitions.
chirp_pulse <= 1'b0; chirp_pulse <= 1'b0;
@@ -219,11 +275,40 @@ always @(posedge clk or negedge reset_n) begin
subframe_pulse <= 1'b1; subframe_pulse <= 1'b1;
subframe_id <= next_sf; subframe_id <= next_sf;
wave_sel <= subframe_to_wave(next_sf); wave_sel <= subframe_to_wave(next_sf);
if (next_sf == first_sf) if (next_sf == first_sf) begin
// Frame wrap — emit frame_pulse and (if enabled) stall
// in S_BEAM_WAIT until the MCU acknowledges via PD8.
frame_pulse <= 1'b1; frame_pulse <= 1'b1;
if (host_handshake_enable) begin
beam_watchdog <= 23'd0;
state <= S_BEAM_WAIT;
end else begin
chirp_pulse <= 1'b1; chirp_pulse <= 1'b1;
state <= S_CHIRP; state <= S_CHIRP;
end end
end else begin
chirp_pulse <= 1'b1;
state <= S_CHIRP;
end
end
end
S_BEAM_WAIT: begin
// Wait for an MCU PD8 toggle (any edge) OR the watchdog.
// host_handshake_enable can drop mid-wait — release the FSM in
// that case so disabling the handshake never strands the
// radar between frames.
if (beam_ready_edge || !host_handshake_enable) begin
beam_watchdog <= 23'd0;
chirp_pulse <= 1'b1;
state <= S_CHIRP;
end else if (beam_watchdog >= BEAM_WATCHDOG_MAX) begin
beam_handshake_watchdog_fired <= 1'b1;
beam_watchdog <= 23'd0;
chirp_pulse <= 1'b1;
state <= S_CHIRP;
end else begin
beam_watchdog <= beam_watchdog + 23'd1;
end
end end
default: state <= S_IDLE; default: state <= S_IDLE;
endcase endcase
@@ -229,13 +229,13 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_*_3v3}]
# STM32 DIG BUS — Control signals (Bank 15, VCCO = 3.3V) # STM32 DIG BUS — Control signals (Bank 15, VCCO = 3.3V)
# ============================================================================ # ============================================================================
# Pin: L18 = IO_L16N_T2_A27_15 # Pin: L18 = IO_L16N_T2_A27_15
set_property PACKAGE_PIN L18 [get_ports {stm32_new_chirp}] set_property PACKAGE_PIN L18 [get_ports {stm32_beam_ready}]
# N18 / N19 retired in PR-AB.b expanded — formerly stm32_new_elevation / # N18 / N19 retired in PR-AB.b expanded — formerly stm32_new_elevation /
# stm32_new_azimuth. MCU side init is also stripped (see commit 3); the # stm32_new_azimuth. MCU side init is also stripped (see commit 3); the
# pins default to high-Z inputs after MCU reset. # pins default to high-Z inputs after MCU reset.
# Pin: N20 = IO_L18P_T2_A24_15 # Pin: N20 = IO_L18P_T2_A24_15
set_property PACKAGE_PIN N20 [get_ports {stm32_mixers_enable}] set_property PACKAGE_PIN N20 [get_ports {stm32_mixers_enable}]
set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_chirp}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_beam_ready}]
set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}]
# ============================================================================ # ============================================================================
@@ -222,12 +222,12 @@ set_property IOSTANDARD LVCMOS18 [get_ports {stm32_*_1v8}]
# DIG_0..DIG_4 are STM32 outputs (PD8-PD12) → FPGA inputs (control) # DIG_0..DIG_4 are STM32 outputs (PD8-PD12) → FPGA inputs (control)
# DIG_5..DIG_7 are STM32 inputs (PD13-PD15) ← FPGA outputs (status / sync) # DIG_5..DIG_7 are STM32 inputs (PD13-PD15) ← FPGA outputs (status / sync)
set_property PACKAGE_PIN F13 [get_ports {stm32_new_chirp}] ;# DIG_0 (PD8) set_property PACKAGE_PIN F13 [get_ports {stm32_beam_ready}] ;# DIG_0 (PD8)
# DIG_1 (PD9) + DIG_2 (PD10) retired in PR-AB.b expanded — formerly # DIG_1 (PD9) + DIG_2 (PD10) retired in PR-AB.b expanded — formerly
# stm32_new_elevation / stm32_new_azimuth. MCU side init is also stripped # stm32_new_elevation / stm32_new_azimuth. MCU side init is also stripped
# (see commit 3); the pins default to high-Z inputs after MCU reset. # (see commit 3); the pins default to high-Z inputs after MCU reset.
set_property PACKAGE_PIN F15 [get_ports {stm32_mixers_enable}] ;# DIG_3 (PD11) set_property PACKAGE_PIN F15 [get_ports {stm32_mixers_enable}] ;# DIG_3 (PD11)
set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_chirp}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_beam_ready}]
set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}]
# reset_n is DIG_4 (PD12) — constrained above in the RESET section # reset_n is DIG_4 (PD12) — constrained above in the RESET section
+16 -1
View File
@@ -67,6 +67,16 @@ module radar_receiver_final (
// so it stays in S_IDLE until the operator turns the radar on. // so it stays in S_IDLE until the operator turns the radar on.
input wire mixers_enable_100m, input wire mixers_enable_100m,
// PR-AB.b expanded commit 5: beam-ready handshake plumbing.
// stm32_beam_ready_async is the raw MCU PD8 toggle (CDC-synchronized
// inside chirp_scheduler on `clk`). host_handshake_enable gates whether
// the scheduler stalls in S_BEAM_WAIT after each frame_pulse.
// beam_handshake_watchdog_fired is sticky (cleared only on reset_n) and
// surfaces in status_words[4][1] at the top level.
input wire stm32_beam_ready_async,
input wire host_handshake_enable,
output wire beam_handshake_watchdog_fired,
// CFAR integration: expose Doppler frame_complete to top level // CFAR integration: expose Doppler frame_complete to top level
output wire doppler_frame_done_out, output wire doppler_frame_done_out,
@@ -236,6 +246,10 @@ chirp_scheduler sched (
.host_long_listen_cycles(host_long_listen_cycles), .host_long_listen_cycles(host_long_listen_cycles),
.host_guard_cycles(host_guard_cycles), .host_guard_cycles(host_guard_cycles),
.host_chirps_per_subframe(6'd`RP_DEF_CHIRPS_PER_SUBFRAME), .host_chirps_per_subframe(6'd`RP_DEF_CHIRPS_PER_SUBFRAME),
// PR-AB.b expanded commit 5: beam-ready handshake. CDC-sync happens
// inside chirp_scheduler so the raw async GPIO can be routed directly.
.beam_ready_async(stm32_beam_ready_async),
.host_handshake_enable(host_handshake_enable),
.wave_sel(wave_sel), .wave_sel(wave_sel),
.chirp_pulse(chirp_pulse), .chirp_pulse(chirp_pulse),
.subframe_pulse(subframe_pulse), .subframe_pulse(subframe_pulse),
@@ -244,7 +258,8 @@ chirp_scheduler sched (
.subframe_id(sched_subframe_id), .subframe_id(sched_subframe_id),
.cfg_chirp_cycles (sched_cfg_chirp_cycles), .cfg_chirp_cycles (sched_cfg_chirp_cycles),
.cfg_listen_cycles(sched_cfg_listen_cycles), .cfg_listen_cycles(sched_cfg_listen_cycles),
.cfg_guard_cycles (sched_cfg_guard_cycles) .cfg_guard_cycles (sched_cfg_guard_cycles),
.beam_handshake_watchdog_fired(beam_handshake_watchdog_fired)
); );
// PR-E: forward scheduler pulses + wave_sel to the TX-side CDC bridge in // PR-E: forward scheduler pulses + wave_sel to the TX-side CDC bridge in
+51 -5
View File
@@ -78,9 +78,12 @@ module radar_system_top (
// STM32 → FPGA control (PD8/PD11). PD9/PD10 (was stm32_new_elevation / // STM32 → FPGA control (PD8/PD11). PD9/PD10 (was stm32_new_elevation /
// stm32_new_azimuth) retired in PR-AB.b expanded — see commit 3 for the // stm32_new_azimuth) retired in PR-AB.b expanded — see commit 3 for the
// XDC + MCU GPIO scrub. stm32_new_chirp will be renamed to // XDC + MCU GPIO scrub. stm32_beam_ready (PD8) is the MCU's per-pattern
// stm32_beam_ready in commit 5 (beam-ready handshake). // ready toggle for the beam-ready handshake added in PR-AB.b expanded
input wire stm32_new_chirp, // commit 5; CDC-sync is owned by chirp_scheduler.v on `clk`. The
// handshake itself is gated by opcode 0x1A (host_handshake_enable);
// cold-reset default 1'b0 keeps the open-loop legacy cadence in place.
input wire stm32_beam_ready,
input wire stm32_mixers_enable, input wire stm32_mixers_enable,
// ========== FT601 USB 3.0 INTERFACE ========== // ========== FT601 USB 3.0 INTERFACE ==========
@@ -248,6 +251,12 @@ end
// sample has been silently dropped between the 400 MHz CIC and 100 MHz FIR. // sample has been silently dropped between the 400 MHz CIC and 100 MHz FIR.
// Already sticky in source (ddc_400m.v:697-702), no extra latch needed. // Already sticky in source (ddc_400m.v:697-702), no extra latch needed.
wire rx_ddc_cic_fir_overrun; wire rx_ddc_cic_fir_overrun;
// PR-AB.b expanded commit 5: sticky from chirp_scheduler. High = at least one
// frame fired without an MCU PD8 ack within the ~80 ms watchdog. Packed into
// status_words[4][1] so the host can spot patterns falling behind. Sticky is
// owned by the scheduler (clk_100m source domain); CDC across to ft_clk is
// done inside usb_data_interface_ft2232h alongside the other status stickies.
wire rx_beam_handshake_watchdog;
// Data packing for USB // Data packing for USB
wire [31:0] usb_range_profile; wire [31:0] usb_range_profile;
@@ -297,6 +306,12 @@ reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER
// bit 1 MEDIUM, bit 2 LONG. Default 3'b111 keeps the production 3-PRI ladder. // 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). // 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 [2:0] host_subframe_enable; // Opcode 0x19 (default RP_DEF_SUBFRAME_ENABLE = 3'b111)
// PR-AB.b expanded commit 5: opcode 0x1A enables the beam-ready handshake
// inside chirp_scheduler. Cold-reset default = 1'b0 (legacy open-loop
// cadence) so existing TBs and any production deployment without the MCU
// PD8 toggle wired up keep their old behavior. Host (GUI) writes 1 once
// the MCU has been verified to be toggling PD8 each beam pattern.
reg host_handshake_enable;
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
@@ -627,6 +642,14 @@ radar_receiver_final rx_inst (
.host_agc_holdoff(host_agc_holdoff), .host_agc_holdoff(host_agc_holdoff),
// PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above) // PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above)
.mixers_enable_100m(stm32_mixers_enable_100m), .mixers_enable_100m(stm32_mixers_enable_100m),
// PR-AB.b expanded commit 5: beam-ready handshake plumbing. stm32_beam_ready
// is the raw MCU PD8 GPIO (sync'd inside chirp_scheduler on `clk`).
// host_handshake_enable is the opcode-0x1A register, sampled on clk_100m
// (quasi-static, no extra CDC). beam_handshake_watchdog_fired is sticky in
// the source domain and packed into status_words[4][1] downstream.
.stm32_beam_ready_async(stm32_beam_ready),
.host_handshake_enable(host_handshake_enable),
.beam_handshake_watchdog_fired(rx_beam_handshake_watchdog),
// CFAR: Doppler frame-complete pulse // CFAR: Doppler frame-complete pulse
.doppler_frame_done_out(rx_frame_complete), .doppler_frame_done_out(rx_frame_complete),
// Ground clutter removal // Ground clutter removal
@@ -903,7 +926,13 @@ if (USB_MODE == 0) begin : gen_ft601
// (level signal) so the ft_clk synchronizer cannot miss a 10 ns // (level signal) so the ft_clk synchronizer cannot miss a 10 ns
// source-domain pulse. F-1.2 overrun is already sticky in source. // source-domain pulse. F-1.2 overrun is already sticky in source.
.status_range_decim_watchdog(rx_range_decim_watchdog_sticky), .status_range_decim_watchdog(rx_range_decim_watchdog_sticky),
.status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun) .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun),
// PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky.
// Packed into status_words[4][1] in the FT601 path too so the host
// sees the same fault bit regardless of which USB build it's talking
// to (FT601 200T premium vs FT2232H 50T production).
.status_beam_handshake_watchdog(rx_beam_handshake_watchdog)
); );
// FT2232H ports unused in FT601 mode — tie off // FT2232H ports unused in FT601 mode — tie off
@@ -1002,7 +1031,15 @@ end else begin : gen_ft2232h
// PR-G: 2-tier CFAR telemetry (status_words[6]) // PR-G: 2-tier CFAR telemetry (status_words[6])
.status_cfar_alpha_soft(host_cfar_alpha_soft), .status_cfar_alpha_soft(host_cfar_alpha_soft),
.status_detect_threshold_soft(cfar_detect_threshold_soft), .status_detect_threshold_soft(cfar_detect_threshold_soft),
.status_detect_count_cand(cfar_detect_count_cand) .status_detect_count_cand(cfar_detect_count_cand),
// PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky.
// Source domain is clk_100m; usb_data_interface_ft2232h hands it to
// ft_clk through the same status-bit CDC convention used for the
// F-6.4 watchdog and F-1.2 overrun stickies above. Packed into
// status_words[4][1] downstream — see word-4 layout note in
// usb_data_interface_ft2232h.v.
.status_beam_handshake_watchdog(rx_beam_handshake_watchdog)
); );
// FT601 ports unused in FT2232H mode — tie off // FT601 ports unused in FT2232H mode — tie off
@@ -1096,6 +1133,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
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). // PR-U / M-8: 3'b111 = SHORT|MEDIUM|LONG all on (production 3-PRI ladder).
host_subframe_enable <= `RP_DEF_SUBFRAME_ENABLE; host_subframe_enable <= `RP_DEF_SUBFRAME_ENABLE;
// PR-AB.b expanded commit 5: handshake disabled at cold-reset so any
// host or TB that doesn't drive PD8 keeps the legacy open-loop cadence.
// Production GUI writes 0x1A=1 after MCU has been verified.
host_handshake_enable <= 1'b0;
host_status_request <= 1'b0; host_status_request <= 1'b0;
chirps_mismatch_error <= 1'b0; chirps_mismatch_error <= 1'b0;
// CFAR defaults (disabled by default — backward-compatible) // CFAR defaults (disabled by default — backward-compatible)
@@ -1150,6 +1191,11 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
// bits[5:3] so host CRT can detect mask != 3'b111 and degrade // bits[5:3] so host CRT can detect mask != 3'b111 and degrade
// confidence rather than mis-attribute the SF axis. // confidence rather than mis-attribute the SF axis.
8'h19: host_subframe_enable <= usb_cmd_value[2:0]; 8'h19: host_subframe_enable <= usb_cmd_value[2:0];
// PR-AB.b expanded commit 5: beam-ready handshake enable.
// value[0]=1 makes chirp_scheduler stall in S_BEAM_WAIT after
// each frame_pulse until the MCU toggles PD8 (or watchdog
// ~80 ms). value[0]=0 reverts to the legacy open-loop cadence.
8'h1A: host_handshake_enable <= usb_cmd_value[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.
+5 -3
View File
@@ -68,8 +68,10 @@ module radar_system_top_50t (
// ===== STM32 Control (Bank 15: 3.3V) ===== // ===== STM32 Control (Bank 15: 3.3V) =====
// PR-AB.b expanded: stm32_new_elevation/azimuth retired (no consumer). // PR-AB.b expanded: stm32_new_elevation/azimuth retired (no consumer).
// stm32_new_chirp becomes stm32_beam_ready in commit 5. // stm32_beam_ready (PD8) is the MCU per-pattern ready toggle for the
input wire stm32_new_chirp, // beam-ready handshake landed in commit 5; gated by opcode 0x1A inside
// radar_system_top (host_handshake_enable). Cold-reset = legacy off.
input wire stm32_beam_ready,
input wire stm32_mixers_enable, input wire stm32_mixers_enable,
// ===== FT2232H USB 2.0 Interface (Bank 35: 3.3V) ===== // ===== FT2232H USB 2.0 Interface (Bank 35: 3.3V) =====
@@ -178,7 +180,7 @@ module radar_system_top_50t (
.adc_pwdn (adc_pwdn), .adc_pwdn (adc_pwdn),
// ----- STM32 Control ----- // ----- STM32 Control -----
.stm32_new_chirp (stm32_new_chirp), .stm32_beam_ready (stm32_beam_ready),
.stm32_mixers_enable (stm32_mixers_enable), .stm32_mixers_enable (stm32_mixers_enable),
// ----- FT2232H USB 2.0 (active on 50T, USB_MODE=1) ----- // ----- FT2232H USB 2.0 (active on 50T, USB_MODE=1) -----
+4
View File
@@ -574,6 +574,10 @@ run_test "DIG6 frame-pulse stretcher (PR-AB.b)" \
tb/tb_dig6_frame_pulse.vvp \ tb/tb_dig6_frame_pulse.vvp \
tb/tb_dig6_frame_pulse.v tb/tb_dig6_frame_pulse.v
run_test "Chirp Scheduler beam-ready handshake (PR-AB.b expanded c5)" \
tb/tb_chirp_scheduler_handshake.vvp \
tb/tb_chirp_scheduler_handshake.v chirp_scheduler.v
run_test "NUM_CELLS sizing 50T (AUDIT-C16)" \ run_test "NUM_CELLS sizing 50T (AUDIT-C16)" \
tb/tb_audit_c16_num_cells_50t.vvp \ tb/tb_audit_c16_num_cells_50t.vvp \
tb/tb_audit_c16_num_cells.v tb/tb_audit_c16_num_cells.v
@@ -358,7 +358,7 @@ def check_mf_invariants(result: CheckResult):
f"twin={twin_peak}, ref={ref_peak}" f"twin={twin_peak}, ref={ref_peak}"
) )
# Sidelobe behaviour: peak should be 5× the median magnitude. Under # Sidelobe behaviour: peak should be at least 5x the median magnitude. Under
# scaled-mode at amp=4000 the peak rises to ~977 while sidelobes stay # scaled-mode at amp=4000 the peak rises to ~977 while sidelobes stay
# near the LSB floor, easily clearing the threshold. # near the LSB floor, easily clearing the threshold.
twin_peak_val = float(twin_mag[delay]) twin_peak_val = float(twin_mag[delay])
@@ -62,7 +62,7 @@ import numpy as np
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) THIS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, THIS_DIR) sys.path.insert(0, THIS_DIR)
from fpga_model import DopplerProcessor, run_cfar_ca from fpga_model import DopplerProcessor, run_cfar_ca # noqa: E402
# Pull stimulus configuration verbatim so dimensions stay aligned. # Pull stimulus configuration verbatim so dimensions stay aligned.
from gen_e2e_stimulus import ( # noqa: E402 from gen_e2e_stimulus import ( # noqa: E402
@@ -72,8 +72,6 @@ from gen_e2e_stimulus import ( # noqa: E402
CHIRPS_PER_FRAME, CHIRPS_PER_FRAME,
RANGE_BINS, RANGE_BINS,
HOST_DC_NOTCH_WIDTH, HOST_DC_NOTCH_WIDTH,
EXPECTED_RANGE_BIN,
EXPECTED_DOPPLER_BIN_PER_SF,
EXPECTED_DETECT_CELLS, EXPECTED_DETECT_CELLS,
) )
@@ -263,10 +261,8 @@ def pack_bulk_frame(frame_number: int, flags: int,
packed = 0 packed = 0
for slot in range(4): for slot in range(4):
db = byte_idx * 4 + slot db = byte_idx * 4 + slot
if db < DOPPLER_TOTAL_BINS: # padding for db >= DOPPLER_TOTAL_BINS lands on slot 3
code = int(cfar_class[rb, db]) & 0x3 code = int(cfar_class[rb, db]) & 0x3 if db < DOPPLER_TOTAL_BINS else 0
else:
code = 0 # padding
packed |= code << ((3 - slot) * 2) packed |= code << ((3 - slot) * 2)
out.append(packed) out.append(packed)
@@ -98,16 +98,15 @@ HOST_DC_NOTCH_WIDTH = 1
# ============================================================================ # ============================================================================
# Target placement -> expected bin coordinates # Target placement -> expected bin coordinates
# ============================================================================ # ============================================================================
# range_bin = round(2 * R / c * fs / decim) # Range bin formula: round(2 * R / c * fs / decim). For R=100m, fs=400 MHz,
# = round(2 * 100 / 3e8 * 400e6 / 4) # decim=4 -> round(2 * 100 / 3e8 * 100e6) = round(66.667) = 67.
# = round(66.667) = 67 EXPECTED_RANGE_BIN = round(2.0 * TARGET_RANGE_M / C_LIGHT * RANGE_BIN_HZ)
EXPECTED_RANGE_BIN = int(round(2.0 * TARGET_RANGE_M / C_LIGHT * RANGE_BIN_HZ))
# Per-sub-frame doppler bin (folding into 16-pt FFT). For our 5 m/s target # Per-sub-frame doppler bin (folding into 16-pt FFT). For our 5 m/s target
# this is intentionally non-folding -> 1 in all three sub-frames. # this is intentionally non-folding -> 1 in all three sub-frames.
F_DOPPLER_HZ = 2.0 * TARGET_VEL_MPS * F_CARRIER / C_LIGHT F_DOPPLER_HZ = 2.0 * TARGET_VEL_MPS * F_CARRIER / C_LIGHT
EXPECTED_DOPPLER_BIN_PER_SF = tuple( EXPECTED_DOPPLER_BIN_PER_SF = tuple(
int(round(F_DOPPLER_HZ * DOPPLER_FFT_SIZE * pri)) % DOPPLER_FFT_SIZE round(F_DOPPLER_HZ * DOPPLER_FFT_SIZE * pri) % DOPPLER_FFT_SIZE
for pri in PRI_BY_SF for pri in PRI_BY_SF
) )
# Flat 48-bin doppler-axis expected cells (sub_frame << 4 | bin). # Flat 48-bin doppler-axis expected cells (sub_frame << 4 | bin).
@@ -160,8 +159,8 @@ def generate_range_decim_frame(seed: int = SCENE_SEED) -> tuple[np.ndarray, np.n
# Target injection at the expected range bin. # Target injection at the expected range bin.
phi = _target_phase_rad(c) phi = _target_phase_rad(c)
sig_i = int(round(TARGET_AMPLITUDE * np.cos(phi))) sig_i = round(TARGET_AMPLITUDE * np.cos(phi))
sig_q = int(round(TARGET_AMPLITUDE * np.sin(phi))) sig_q = round(TARGET_AMPLITUDE * np.sin(phi))
frame_i[c, EXPECTED_RANGE_BIN] += sig_i frame_i[c, EXPECTED_RANGE_BIN] += sig_i
frame_q[c, EXPECTED_RANGE_BIN] += sig_q frame_q[c, EXPECTED_RANGE_BIN] += sig_q
@@ -231,7 +230,7 @@ def main() -> int:
f"shape={frame_i.shape}") f"shape={frame_i.shape}")
if n_lines != expected_lines: if n_lines != expected_lines:
print(f" ERROR: line count mismatch", file=sys.stderr) print(" ERROR: line count mismatch", file=sys.stderr)
return 1 return 1
# Sanity: target peak should dominate at the expected range bin. # Sanity: target peak should dominate at the expected range bin.
@@ -95,7 +95,7 @@ class TestState:
def load_captured_frame_hex(path: str) -> bytes: def load_captured_frame_hex(path: str) -> bytes:
"""Read iverilog $writememh output (one byte per line, 2-hex-digit).""" """Read iverilog $writememh output (one byte per line, 2-hex-digit)."""
out = bytearray() out = bytearray()
with open(path, 'r') as f: with open(path) as f:
for line in f: for line in f:
tok = line.strip() tok = line.strip()
if not tok or tok.startswith('//'): if not tok or tok.startswith('//'):
@@ -0,0 +1,279 @@
`timescale 1ns / 1ps
// ============================================================================
// tb_chirp_scheduler_handshake.v — PR-AB.b expanded commit 5 unit TB
//
// Exercises the beam-ready handshake added to chirp_scheduler.v:
// - S_BEAM_WAIT entered on frame_pulse when host_handshake_enable=1
// - Exit on any beam_ready_async edge (toggle semantic)
// - Watchdog auto-advances + sets beam_handshake_watchdog_fired sticky
// - host_handshake_enable=0 keeps legacy open-loop cadence
// - Mid-wait disable releases the FSM
// - Reset clears the sticky
//
// Uses compressed cycle counts so a full 48-chirp frame completes in <1 ms
// of sim time. The production cycle counts (175/161/167 µs PRIs) would push
// the sim past the iverilog regression budget; the FSM logic exercised here
// is independent of the cycle-count values.
// ============================================================================
`include "radar_params.vh"
module tb_chirp_scheduler_handshake;
// ---- Clock (100 MHz, 10 ns) ----
reg clk = 1'b0;
always #5 clk = ~clk;
// ---- Compressed timing — 6 chirp/listen/guard cycles each per PRI ----
localparam [15:0] T_CHIRP = 16'd6;
localparam [15:0] T_LISTEN = 16'd6;
localparam [15:0] T_GUARD = 16'd6;
// Single chirp + listen + guard ≈ 20 cycles. With chirps_per_subframe=2 and
// 3 sub-frames active, a frame lands at ~120 cycles → 1.2 µs/frame in sim.
localparam [5:0] CHIRPS_PER_SUBFRAME = 6'd2;
// ---- DUT signals ----
reg reset_n = 1'b0;
reg mixers_enable = 1'b0;
reg [2:0] subframe_enable = 3'b111;
reg beam_ready_async = 1'b0;
reg handshake_enable = 1'b0;
wire [1:0] wave_sel;
wire chirp_pulse;
wire subframe_pulse;
wire frame_pulse;
wire [5:0] chirp_counter;
wire [1:0] subframe_id;
wire [15:0] cfg_chirp_cycles;
wire [15:0] cfg_listen_cycles;
wire [15:0] cfg_guard_cycles;
wire watchdog_fired;
chirp_scheduler dut (
.clk (clk),
.reset_n (reset_n),
.host_subframe_enable (subframe_enable),
.host_short_chirp_cycles (T_CHIRP),
.host_short_listen_cycles (T_LISTEN),
.host_medium_chirp_cycles (T_CHIRP),
.host_medium_listen_cycles (T_LISTEN),
.host_long_chirp_cycles (T_CHIRP),
.host_long_listen_cycles (T_LISTEN),
.host_guard_cycles (T_GUARD),
.host_chirps_per_subframe (CHIRPS_PER_SUBFRAME),
.mixers_enable (mixers_enable),
.beam_ready_async (beam_ready_async),
.host_handshake_enable (handshake_enable),
.wave_sel (wave_sel),
.chirp_pulse (chirp_pulse),
.subframe_pulse (subframe_pulse),
.frame_pulse (frame_pulse),
.chirp_counter (chirp_counter),
.subframe_id (subframe_id),
.cfg_chirp_cycles (cfg_chirp_cycles),
.cfg_listen_cycles (cfg_listen_cycles),
.cfg_guard_cycles (cfg_guard_cycles),
.beam_handshake_watchdog_fired(watchdog_fired)
);
// ---- Bookkeeping ----
integer pass = 0;
integer fail = 0;
integer frame_pulse_count = 0;
always @(posedge clk) if (frame_pulse) frame_pulse_count = frame_pulse_count + 1;
task check;
input [255:0] label;
input cond;
begin
if (cond) begin
$display(" [PASS] %0s", label);
pass = pass + 1;
end else begin
$display(" [FAIL] %0s", label);
fail = fail + 1;
end
end
endtask
// Wait for the FSM to enter S_BEAM_WAIT (state == 3'd5).
task wait_for_beam_wait;
input integer timeout_cycles;
integer i;
begin
i = 0;
while (dut.state !== 3'd5 && i < timeout_cycles) begin
@(posedge clk);
i = i + 1;
end
end
endtask
// Wait for state to leave S_BEAM_WAIT (timeout reports failure to caller).
task wait_for_beam_wait_exit;
input integer timeout_cycles;
integer i;
begin
i = 0;
while (dut.state === 3'd5 && i < timeout_cycles) begin
@(posedge clk);
i = i + 1;
end
end
endtask
// Wait for at least N frames to complete (frame_pulse_count >= target).
task wait_frames;
input integer target;
input integer timeout_cycles;
integer i;
begin
i = 0;
while (frame_pulse_count < target && i < timeout_cycles) begin
@(posedge clk);
i = i + 1;
end
end
endtask
// =========================================================================
// MAIN
// =========================================================================
initial begin
$dumpfile("tb_chirp_scheduler_handshake.vcd");
$dumpvars(0, tb_chirp_scheduler_handshake);
$display("============================================================");
$display(" CHIRP_SCHEDULER beam-ready handshake (PR-AB.b expanded c5)");
$display("============================================================");
// Reset
reset_n = 1'b0;
mixers_enable = 1'b0;
handshake_enable = 1'b0;
beam_ready_async = 1'b0;
repeat (4) @(posedge clk);
reset_n = 1'b1;
@(posedge clk);
check("T1: post-reset watchdog sticky low", watchdog_fired == 1'b0);
// ====================================================================
// T2: Legacy mode (handshake_enable=0) — frames advance back-to-back,
// S_BEAM_WAIT must never be visited.
// ====================================================================
$display("--- T2: legacy open-loop (handshake_enable=0) ---");
mixers_enable = 1'b1;
frame_pulse_count = 0;
wait_frames(3, 5000);
check("T2: at least 3 frames fired without handshake", frame_pulse_count >= 3);
check("T2: watchdog sticky still low", watchdog_fired == 1'b0);
// Park the scheduler in IDLE so the next test starts clean.
mixers_enable = 1'b0;
repeat (5) @(posedge clk);
frame_pulse_count = 0;
// ====================================================================
// T3: Handshake enabled — scheduler should enter S_BEAM_WAIT after
// the next frame_pulse and only exit on a beam_ready edge.
// ====================================================================
$display("--- T3: handshake enabled, MCU toggles before watchdog ---");
handshake_enable = 1'b1;
mixers_enable = 1'b1;
wait_for_beam_wait(5000);
check("T3a: FSM entered S_BEAM_WAIT after a frame_pulse",
dut.state == 3'd5);
// Sit in S_BEAM_WAIT for a deliberate number of cycles; the scheduler
// must stay parked until we toggle beam_ready_async.
repeat (200) @(posedge clk);
check("T3b: FSM still in S_BEAM_WAIT after 200 idle cycles",
dut.state == 3'd5);
check("T3b: watchdog has not fired", watchdog_fired == 1'b0);
// Toggle beam_ready_async and verify exit within a handful of clk
// edges (2-FF sync + 1-cycle edge latch + S_BEAM_WAIT → S_CHIRP).
@(posedge clk); beam_ready_async = 1'b1;
wait_for_beam_wait_exit(50);
check("T3c: FSM left S_BEAM_WAIT after PD8 toggle (rising)",
dut.state != 3'd5);
// ====================================================================
// T4: Second toggle exits a later wait (verifies edge-detect handles
// falling edges symmetrically — HAL_GPIO_TogglePin gives both).
// ====================================================================
$display("--- T4: second wait, falling-edge ack ---");
wait_for_beam_wait(5000);
check("T4a: FSM re-entered S_BEAM_WAIT on next frame", dut.state == 3'd5);
@(posedge clk); beam_ready_async = 1'b0; // falling edge
wait_for_beam_wait_exit(50);
check("T4b: FSM left S_BEAM_WAIT after PD8 toggle (falling)",
dut.state != 3'd5);
// ====================================================================
// T5: Watchdog timeout. The real 23-bit terminal value (8M cycles ≈
// 80 ms) is unreachable in iverilog sim time, so we force the
// FSM's counter to the terminal value and let one clk edge
// resolve the (counter >= BEAM_WATCHDOG_MAX) branch.
// ====================================================================
$display("--- T5: watchdog auto-advance + sticky latch ---");
wait_for_beam_wait(5000);
check("T5a: FSM in S_BEAM_WAIT (pre-force)", dut.state == 3'd5);
// Force the counter to BEAM_WATCHDOG_MAX so the FSM's >= comparison
// trips on the next posedge; then release immediately so the always
// block can drive normally on the same edge.
@(negedge clk);
force dut.beam_watchdog = 23'd8_000_000;
@(posedge clk); #1;
release dut.beam_watchdog;
check("T5b: watchdog sticky latched after timeout",
watchdog_fired == 1'b1);
check("T5c: FSM left S_BEAM_WAIT after watchdog",
dut.state != 3'd5);
// ====================================================================
// T6: Sticky survives mixers_enable=0 cycle (only reset_n clears it).
// ====================================================================
$display("--- T6: sticky watchdog is reset-only ---");
mixers_enable = 1'b0;
repeat (20) @(posedge clk);
check("T6a: watchdog sticky stays high across mixers_enable=0",
watchdog_fired == 1'b1);
mixers_enable = 1'b1;
// ====================================================================
// T7: Mid-wait disable releases the FSM (handshake_enable→0).
// ====================================================================
$display("--- T7: mid-wait host_handshake_enable=0 releases FSM ---");
wait_for_beam_wait(5000);
check("T7a: FSM in S_BEAM_WAIT for disable test", dut.state == 3'd5);
@(posedge clk); handshake_enable = 1'b0;
wait_for_beam_wait_exit(20);
check("T7b: FSM left S_BEAM_WAIT after handshake disable",
dut.state != 3'd5);
// ====================================================================
// T8: Full reset clears sticky.
// ====================================================================
$display("--- T8: reset_n clears watchdog sticky ---");
reset_n = 1'b0;
repeat (4) @(posedge clk);
check("T8: watchdog sticky cleared by reset_n", watchdog_fired == 1'b0);
reset_n = 1'b1;
repeat (4) @(posedge clk);
$display("============================================================");
$display("RESULTS: pass=%0d fail=%0d", pass, fail);
$display("============================================================");
if (fail == 0) $display("[OVERALL] PASS");
else $display("[OVERALL] FAIL");
$finish;
end
initial begin
#1_000_000; // 1 ms wall-clock safety
$display("[FATAL] timeout");
$finish;
end
endmodule
@@ -361,6 +361,7 @@ module tb_e2e_dsp_to_host;
.status_agc_enable (1'b0), .status_agc_enable (1'b0),
.status_range_decim_watchdog(1'b0), .status_range_decim_watchdog(1'b0),
.status_ddc_cic_fir_overrun (1'b0), .status_ddc_cic_fir_overrun (1'b0),
.status_beam_handshake_watchdog(1'b0), // commit 5 tied off, e2e path doesn't model handshake
.status_cfar_alpha_soft (TEST_CFAR_ALPHA_SOFT), .status_cfar_alpha_soft (TEST_CFAR_ALPHA_SOFT),
.status_detect_threshold_soft(17'd0), .status_detect_threshold_soft(17'd0),
.status_detect_count_cand (16'd0) .status_detect_count_cand (16'd0)
@@ -150,6 +150,8 @@ module tb_ft2232h_frame_drop;
// AUDIT-S10: control-fault flags tied off (frame-drop TB scope) // AUDIT-S10: control-fault flags tied off (frame-drop TB scope)
.status_range_decim_watchdog(1'b0), .status_range_decim_watchdog(1'b0),
.status_ddc_cic_fir_overrun(1'b0), .status_ddc_cic_fir_overrun(1'b0),
// PR-AB.b expanded commit 5: beam-handshake watchdog tied off
.status_beam_handshake_watchdog(1'b0),
// PR-G: 2-tier CFAR telemetry tied off // PR-G: 2-tier CFAR telemetry tied off
.status_cfar_alpha_soft(8'h18), // RP_DEF_CFAR_ALPHA_SOFT .status_cfar_alpha_soft(8'h18), // RP_DEF_CFAR_ALPHA_SOFT
.status_detect_threshold_soft(17'd0), .status_detect_threshold_soft(17'd0),
@@ -185,7 +185,14 @@ radar_receiver_final dut (
.doppler_frame_done_out(), .doppler_frame_done_out(),
// PR-E: pin mixers_enable HIGH so the scheduler runs in this TB // PR-E: pin mixers_enable HIGH so the scheduler runs in this TB
.mixers_enable_100m(1'b1) .mixers_enable_100m(1'b1),
// PR-AB.b expanded commit 5: beam-ready handshake — tie off for this TB
// (legacy open-loop cadence). Tests for the handshake live in their own
// dedicated tb_chirp_scheduler_handshake.v unit TB.
.stm32_beam_ready_async(1'b0),
.host_handshake_enable(1'b0),
.beam_handshake_watchdog_fired()
); );
// ============================================================================ // ============================================================================
+6 -1
View File
@@ -16,7 +16,11 @@
// mixer-disable propagation) // mixer-disable propagation)
// G7.3 TX chirp counter CDC (120MHz -> 100MHz) // G7.3 TX chirp counter CDC (120MHz -> 100MHz)
// — G7.1 (STM32→FPGA chirp toggle CDC stress) retired in PR-AB.b // — G7.1 (STM32→FPGA chirp toggle CDC stress) retired in PR-AB.b
// expanded; stm32_new_chirp port is gone. // expanded; the stm32_new_chirp port was renamed to
// stm32_beam_ready in commit 5 (beam-ready handshake) and is
// tied 1'b0 here so the cold-reset default of host_handshake_enable
// (=0) keeps the FSM out of S_BEAM_WAIT and the open-loop cadence
// intact.
// //
// DUT is radar_system_top with USB_MODE=1 (production FT2232H path); the // DUT is radar_system_top with USB_MODE=1 (production FT2232H path); the
// FT2232H ports are wired so a stream_control opcode (0x04) can be sent at // FT2232H ports are wired so a stream_control opcode (0x04) can be sent at
@@ -155,6 +159,7 @@ radar_system_top #(.USB_MODE(1)) dut (
.adc_or_p(1'b0), .adc_or_n(1'b1), .adc_or_p(1'b0), .adc_or_n(1'b1),
.adc_pwdn(adc_pwdn), .adc_pwdn(adc_pwdn),
.stm32_beam_ready(1'b0), // commit 5: handshake disabled by cold-reset default
.stm32_mixers_enable(stm32_mixers_enable), .stm32_mixers_enable(stm32_mixers_enable),
.ft601_data(ft601_data), .ft601_data(ft601_data),
@@ -158,6 +158,7 @@ radar_system_top #(
.adc_or_p(1'b0), .adc_or_n(1'b1), .adc_or_p(1'b0), .adc_or_n(1'b1),
.adc_pwdn(adc_pwdn), .adc_pwdn(adc_pwdn),
.stm32_beam_ready(1'b0), // commit 5: handshake gated off by host_handshake_enable cold-reset = 0
.stm32_mixers_enable(stm32_mixers_enable), .stm32_mixers_enable(stm32_mixers_enable),
// FT601 ports — tied off / unused in USB_MODE=1 // FT601 ports — tied off / unused in USB_MODE=1
@@ -144,7 +144,9 @@ module tb_usb_data_interface;
.status_agc_enable (status_agc_enable), .status_agc_enable (status_agc_enable),
// AUDIT-S10: control-fault flags tied off (pre-existing TB scope) // AUDIT-S10: control-fault flags tied off (pre-existing TB scope)
.status_range_decim_watchdog(1'b0), .status_range_decim_watchdog(1'b0),
.status_ddc_cic_fir_overrun (1'b0) .status_ddc_cic_fir_overrun (1'b0),
// PR-AB.b expanded commit 5: beam-handshake watchdog tied off
.status_beam_handshake_watchdog(1'b0)
); );
// ── Test bookkeeping ─────────────────────────────────────── // ── Test bookkeeping ───────────────────────────────────────
@@ -157,6 +157,10 @@ module tb_usb_protocol_v2;
.status_agc_enable(status_agc_enable), .status_agc_enable(status_agc_enable),
.status_range_decim_watchdog(status_range_decim_watchdog), .status_range_decim_watchdog(status_range_decim_watchdog),
.status_ddc_cic_fir_overrun(status_ddc_cic_fir_overrun), .status_ddc_cic_fir_overrun(status_ddc_cic_fir_overrun),
// PR-AB.b expanded commit 5: beam-handshake watchdog tied off here;
// exercised by tb_chirp_scheduler_handshake.v and word-4 layout test
// refreshed below.
.status_beam_handshake_watchdog(1'b0),
.status_cfar_alpha_soft(status_cfar_alpha_soft), .status_cfar_alpha_soft(status_cfar_alpha_soft),
.status_detect_threshold_soft(status_detect_threshold_soft), .status_detect_threshold_soft(status_detect_threshold_soft),
.status_detect_count_cand(status_detect_count_cand) .status_detect_count_cand(status_detect_count_cand)
+30 -9
View File
@@ -113,7 +113,12 @@ module usb_data_interface (
// sticky/slow-changing in the source domain (set on event, cleared by // sticky/slow-changing in the source domain (set on event, cleared by
// monitor reset), so 2-stage level CDC into ft601_clk_in is sufficient. // monitor reset), so 2-stage level CDC into ft601_clk_in is sufficient.
input wire status_range_decim_watchdog, // audit F-6.4 input wire status_range_decim_watchdog, // audit F-6.4
input wire status_ddc_cic_fir_overrun // audit F-1.2 input wire status_ddc_cic_fir_overrun, // audit F-1.2
// PR-AB.b expanded commit 5: beam-ready handshake watchdog (clk domain).
// Sticky in chirp_scheduler; same 2-FF level CDC convention as the F-6.4
// and F-1.2 stickies above. Packed into status_words[4][1] downstream.
input wire status_beam_handshake_watchdog
); );
// USB packet structure (same as before) // USB packet structure (same as before)
@@ -339,6 +344,9 @@ reg status_req_sync_prev;
reg range_decim_watchdog_sync_1; reg range_decim_watchdog_sync_1;
(* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0; (* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0;
reg ddc_cic_fir_overrun_sync_1; reg ddc_cic_fir_overrun_sync_1;
// PR-AB.b expanded commit 5: 2-FF level CDC for beam-handshake watchdog sticky.
(* ASYNC_REG = "TRUE" *) reg beam_handshake_wd_sync_0;
reg beam_handshake_wd_sync_1;
wire status_req_ft601 = status_req_sync[1] ^ status_req_sync_prev; wire status_req_ft601 = status_req_sync[1] ^ status_req_sync_prev;
// Status snapshot: captured in ft601_clk domain when status request arrives. // Status snapshot: captured in ft601_clk domain when status request arrives.
@@ -376,6 +384,8 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
range_decim_watchdog_sync_1 <= 1'b0; range_decim_watchdog_sync_1 <= 1'b0;
ddc_cic_fir_overrun_sync_0 <= 1'b0; ddc_cic_fir_overrun_sync_0 <= 1'b0;
ddc_cic_fir_overrun_sync_1 <= 1'b0; ddc_cic_fir_overrun_sync_1 <= 1'b0;
beam_handshake_wd_sync_0 <= 1'b0;
beam_handshake_wd_sync_1 <= 1'b0;
end else begin end else begin
// Synchronize valid strobes (2-stage sync chain) // Synchronize valid strobes (2-stage sync chain)
range_valid_sync <= {range_valid_sync[0], range_valid}; range_valid_sync <= {range_valid_sync[0], range_valid};
@@ -395,6 +405,8 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0; range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0;
ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun; ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun;
ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0; ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0;
beam_handshake_wd_sync_0 <= status_beam_handshake_watchdog;
beam_handshake_wd_sync_1 <= beam_handshake_wd_sync_0;
// Gap 2: Capture status snapshot when request arrives in ft601 domain // Gap 2: Capture status snapshot when request arrives in ft601 domain
if (status_req_ft601) begin if (status_req_ft601) begin
@@ -409,14 +421,23 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
status_words[2] <= {status_guard, status_short_chirp}; status_words[2] <= {status_guard, status_short_chirp};
// Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0} // Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0}
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
// Word 4: AGC metrics (range_mode retired in PR-AB.b expanded) // Word 4 layout (post PR-AB.b expanded commit 5):
status_words[4] <= {status_agc_current_gain, // [31:28] // [31:28] agc_current_gain
status_agc_peak_magnitude, // [27:20] // [27:20] agc_peak_magnitude
status_agc_saturation_count, // [19:12] 8-bit saturation count // [19:12] agc_saturation_count
status_agc_enable, // [11] // [11] agc_enable
status_chirps_mismatch, // [10] TX-G mismatch flag // [10] chirps_mismatch (TX-G)
8'd0, // [9:2] reserved // [9:2] reserved 0 (alpha_soft echo lives in ft2232h-only path)
2'd0}; // [1:0] reserved (was range_mode) // [1] beam_handshake_watchdog_fired (sticky, reset_n clear)
// [0] reserved 0 (range_mode bit retired PR-AB.b expanded)
status_words[4] <= {status_agc_current_gain,
status_agc_peak_magnitude,
status_agc_saturation_count,
status_agc_enable,
status_chirps_mismatch,
8'd0,
beam_handshake_wd_sync_1,
1'd0};
// Word 5: {reserved[6:0], self_test_busy[24], reserved[23:16], // Word 5: {reserved[6:0], self_test_busy[24], reserved[23:16],
// self_test_detail[15:8], reserved[7], cic_fir_overrun[6], // self_test_detail[15:8], reserved[7], cic_fir_overrun[6],
// range_decim_watchdog[5], self_test_flags[4:0]} // range_decim_watchdog[5], self_test_flags[4:0]}
@@ -173,6 +173,11 @@ module usb_data_interface_ft2232h (
input wire status_range_decim_watchdog, // audit F-6.4 input wire status_range_decim_watchdog, // audit F-6.4
input wire status_ddc_cic_fir_overrun, // audit F-1.2 input wire status_ddc_cic_fir_overrun, // audit F-1.2
// PR-AB.b expanded commit 5: beam-ready handshake watchdog (clk domain).
// Sticky in chirp_scheduler; 2-FF level CDC into ft_clk. Packed into
// status_words[4][1] — see word-4 layout below.
input wire status_beam_handshake_watchdog,
// PR-G: 2-tier CFAR telemetry (clk domain → status_words[6]). // PR-G: 2-tier CFAR telemetry (clk domain → status_words[6]).
// Slow-changing per-frame values; 2-stage level CDC into ft_clk. // Slow-changing per-frame values; 2-stage level CDC into ft_clk.
input wire [7:0] status_cfar_alpha_soft, // current host_cfar_alpha_soft (Q4.4) input wire [7:0] status_cfar_alpha_soft, // current host_cfar_alpha_soft (Q4.4)
@@ -674,6 +679,10 @@ reg [6:0] frame_drop_sync_1;
reg range_decim_watchdog_sync_1; reg range_decim_watchdog_sync_1;
(* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0; (* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0;
reg ddc_cic_fir_overrun_sync_1; reg ddc_cic_fir_overrun_sync_1;
// PR-AB.b expanded commit 5: 2-FF level CDC for the beam-handshake watchdog
// sticky. Same convention as the F-6.4 / F-1.2 stickies above.
(* ASYNC_REG = "TRUE" *) reg beam_handshake_wd_sync_0;
reg beam_handshake_wd_sync_1;
wire stream_range_en = stream_ctrl_sync_1[0]; wire stream_range_en = stream_ctrl_sync_1[0];
wire stream_doppler_en = stream_ctrl_sync_1[1]; wire stream_doppler_en = stream_ctrl_sync_1[1];
@@ -803,6 +812,8 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
range_decim_watchdog_sync_1 <= 1'b0; range_decim_watchdog_sync_1 <= 1'b0;
ddc_cic_fir_overrun_sync_0 <= 1'b0; ddc_cic_fir_overrun_sync_0 <= 1'b0;
ddc_cic_fir_overrun_sync_1 <= 1'b0; ddc_cic_fir_overrun_sync_1 <= 1'b0;
beam_handshake_wd_sync_0 <= 1'b0;
beam_handshake_wd_sync_1 <= 1'b0;
// PR-G: 2-tier CFAR telemetry CDC reset // PR-G: 2-tier CFAR telemetry CDC reset
alpha_soft_sync_0 <= 8'd0; alpha_soft_sync_0 <= 8'd0;
alpha_soft_sync_1 <= 8'd0; alpha_soft_sync_1 <= 8'd0;
@@ -861,6 +872,8 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0; range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0;
ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun; ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun;
ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0; ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0;
beam_handshake_wd_sync_0 <= status_beam_handshake_watchdog;
beam_handshake_wd_sync_1 <= beam_handshake_wd_sync_0;
// PR-G: 2-tier CFAR telemetry CDC (clk → ft_clk for status read) // PR-G: 2-tier CFAR telemetry CDC (clk → ft_clk for status read)
alpha_soft_sync_0 <= status_cfar_alpha_soft; alpha_soft_sync_0 <= status_cfar_alpha_soft;
@@ -879,13 +892,23 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
status_words[1] <= {status_long_chirp, status_long_listen}; status_words[1] <= {status_long_chirp, status_long_listen};
status_words[2] <= {status_guard, status_short_chirp}; status_words[2] <= {status_guard, status_short_chirp};
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
status_words[4] <= {status_agc_current_gain, // [31:28] // Word 4 layout (post PR-AB.b expanded commit 5):
status_agc_peak_magnitude, // [27:20] // [31:28] agc_current_gain
status_agc_saturation_count, // [19:12] // [27:20] agc_peak_magnitude
status_agc_enable, // [11] // [19:12] agc_saturation_count
status_chirps_mismatch, // [10] TX-G mismatch flag // [11] agc_enable
alpha_soft_sync_1, // [9:2] PR-G: host_cfar_alpha_soft echo (Q4.4) // [10] chirps_mismatch (TX-G)
2'd0}; // [1:0] reserved (was range_mode, retired PR-AB.b expanded) // [9:2] alpha_soft echo (Q4.4)
// [1] beam_handshake_watchdog_fired (sticky, reset_n clear)
// [0] reserved 0 (range_mode bit retired PR-AB.b expanded)
status_words[4] <= {status_agc_current_gain,
status_agc_peak_magnitude,
status_agc_saturation_count,
status_agc_enable,
status_chirps_mismatch,
alpha_soft_sync_1,
beam_handshake_wd_sync_1,
1'd0};
// Word 5: {frame_drop_count[31:25], self_test_busy[24], // Word 5: {frame_drop_count[31:25], self_test_busy[24],
// reserved[23:16], self_test_detail[15:8], reserved[7], // reserved[23:16], self_test_detail[15:8], reserved[7],
// cic_fir_overrun[6], range_decim_watchdog[5], // cic_fir_overrun[6], range_decim_watchdog[5],
+20 -4
View File
@@ -136,6 +136,7 @@ class Opcode(IntEnum):
0x17 host_medium_chirp_cycles (PR-G G2) 0x17 host_medium_chirp_cycles (PR-G G2)
0x18 host_medium_listen_cycles (PR-G G2) 0x18 host_medium_listen_cycles (PR-G G2)
0x19 host_subframe_enable (PR-U / M-8 3-bit {LONG, MED, SHORT} mask) 0x19 host_subframe_enable (PR-U / M-8 3-bit {LONG, MED, SHORT} mask)
0x1A host_handshake_enable (PR-AB.b expanded commit 5 beam-ready stall)
PR-AB.b expanded retired opcodes 0x01 (host_radar_mode), PR-AB.b expanded retired opcodes 0x01 (host_radar_mode),
0x02 (host_trigger_pulse), 0x20 (host_range_mode). 0x02 (host_trigger_pulse), 0x20 (host_range_mode).
@@ -165,6 +166,12 @@ class Opcode(IntEnum):
# host CRT downgrades confidence to UNKNOWN (dbin // 16 attribution would # host CRT downgrades confidence to UNKNOWN (dbin // 16 attribution would
# otherwise be wrong when the scheduler skips a sub-frame). # otherwise be wrong when the scheduler skips a sub-frame).
SUBFRAME_ENABLE = 0x19 SUBFRAME_ENABLE = 0x19
# PR-AB.b expanded commit 5: beam-ready handshake enable. value[0]=1 makes
# chirp_scheduler stall in S_BEAM_WAIT after frame_pulse until the MCU
# toggles PD8, or the ~80 ms watchdog expires (status word 4 bit [1] is
# the sticky watchdog flag). FPGA cold-reset = 0 — host opts in once the
# MCU PD8 wiring is verified on the bench.
HANDSHAKE_ENABLE = 0x1A
# --- Signal processing (0x21-0x27; # --- Signal processing (0x21-0x27;
# 0x20 host_range_mode retired in PR-AB.b expanded) --- # 0x20 host_range_mode retired in PR-AB.b expanded) ---
@@ -260,14 +267,20 @@ class StatusResponse:
agc_saturation_count: int = 0 # 8-bit saturation count [7:0] agc_saturation_count: int = 0 # 8-bit saturation count [7:0]
agc_enable: int = 0 # 1-bit AGC enable readback agc_enable: int = 0 # 1-bit AGC enable readback
chirps_mismatch: int = 0 # TX-G: 1 if FPGA clamped/rejected host chirps_per_elev chirps_mismatch: int = 0 # TX-G: 1 if FPGA clamped/rejected host chirps_per_elev
# PR-AB.b expanded commit 5: sticky watchdog from chirp_scheduler S_BEAM_WAIT.
# 1 means at least one frame elapsed without an MCU PD8 ack within ~80 ms;
# cleared only by full reset_n on the FPGA. Reserved 0 when host has not
# enabled the handshake (opcode 0x1A=1).
beam_handshake_watchdog: int = 0 # word 4 bit [1]
# PR-G 2-tier CFAR telemetry (word 6) # PR-G 2-tier CFAR telemetry (word 6)
detect_count_cand: int = 0 # 16-bit count of CAND-tier detections per frame detect_count_cand: int = 0 # 16-bit count of CAND-tier detections per frame
detect_threshold_soft: int = 0 # 16-bit soft-CFAR threshold readback (saturates 0xFFFF) detect_threshold_soft: int = 0 # 16-bit soft-CFAR threshold readback (saturates 0xFFFF)
# AUDIT-S10 control-fault flags (word 5 high half) # AUDIT-S10 control-fault flags (word 5 high half)
frame_drop_count: int = 0 # frame-drop counter from RTL frame_drop_count: int = 0 # frame-drop counter from RTL
# M-5 MEDIUM PRI readback (word 7) — closes 161-µs MEDIUM visibility gap. # 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) # opcode 0x17/0x18 readback (16-bit each, default RP_DEF_MEDIUM_*_CYCLES).
medium_listen: int = 0 # opcode 0x18 readback (16-bit, default RP_DEF_MEDIUM_LISTEN_CYCLES) medium_chirp: int = 0
medium_listen: int = 0
# ============================================================================ # ============================================================================
@@ -373,9 +386,12 @@ class RadarProtocol:
# Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]} # Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]}
sr.chirps_per_elev = words[3] & 0x3F sr.chirps_per_elev = words[3] & 0x3F
sr.short_listen = (words[3] >> 16) & 0xFFFF sr.short_listen = (words[3] >> 16) & 0xFFFF
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] reserved[1:0] # Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11]
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0. # mismatch[10] alpha_soft[9:2] beam_handshake_watchdog[1] reserved[0]
# PR-AB.b expanded commit 5 reclaimed bit [1] for the handshake watchdog
# sticky (FPGA chirp_scheduler S_BEAM_WAIT, ~80 ms timeout).
sr.chirps_mismatch = (words[4] >> 10) & 0x01 sr.chirps_mismatch = (words[4] >> 10) & 0x01
sr.beam_handshake_watchdog = (words[4] >> 1) & 0x01
sr.agc_enable = (words[4] >> 11) & 0x01 sr.agc_enable = (words[4] >> 11) & 0x01
sr.agc_saturation_count = (words[4] >> 12) & 0xFF sr.agc_saturation_count = (words[4] >> 12) & 0xFF
sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF
+42 -11
View File
@@ -133,6 +133,7 @@ class TestRadarProtocol(unittest.TestCase):
st_flags=0, st_detail=0, st_busy=0, st_flags=0, st_detail=0, st_busy=0,
agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0, agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0,
chirps_mismatch=0, chirps_mismatch=0,
beam_handshake_watchdog=0,
cand_count=0, thr_soft=0, frame_drop=0, cand_count=0, thr_soft=0, frame_drop=0,
medium_chirp=0, medium_listen=0): medium_chirp=0, medium_listen=0):
"""Build an M-5 34-byte status response matching FPGA format.""" """Build an M-5 34-byte status response matching FPGA format."""
@@ -156,13 +157,18 @@ class TestRadarProtocol(unittest.TestCase):
w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F) w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F)
pkt += struct.pack(">I", w3) pkt += struct.pack(">I", w3)
# Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0], # Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20],
# agc_saturation_count[7:0], agc_enable, # agc_saturation_count[19:12], agc_enable[11],
# chirps_mismatch[10], 10'd0 reserved [9:0]} # chirps_mismatch[10], reserved[9:2],
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0. # beam_handshake_watchdog[1], reserved[0]}
# PR-AB.b expanded commit 5: bit [1] is the chirp_scheduler
# S_BEAM_WAIT sticky watchdog flag (reset_n clear). The 8-bit
# alpha_soft echo at [9:2] is FT2232H-only; this co-spec
# builder leaves [9:2] reserved-0 like the FT601 path.
w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) | w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) |
((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) |
((chirps_mismatch & 0x01) << 10)) ((chirps_mismatch & 0x01) << 10) |
((beam_handshake_watchdog & 0x01) << 1))
pkt += struct.pack(">I", w4) pkt += struct.pack(">I", w4)
# Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0, # Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0,
@@ -196,6 +202,7 @@ class TestRadarProtocol(unittest.TestCase):
self.assertEqual(sr.short_listen, 17450) self.assertEqual(sr.short_listen, 17450)
self.assertEqual(sr.chirps_per_elev, 32) self.assertEqual(sr.chirps_per_elev, 32)
self.assertEqual(sr.chirps_mismatch, 0) self.assertEqual(sr.chirps_mismatch, 0)
self.assertEqual(sr.beam_handshake_watchdog, 0)
def test_parse_status_chirps_mismatch(self): def test_parse_status_chirps_mismatch(self):
# TX-G: bit 10 of word 4 must round-trip without disturbing neighbours. # TX-G: bit 10 of word 4 must round-trip without disturbing neighbours.
@@ -203,6 +210,16 @@ class TestRadarProtocol(unittest.TestCase):
sr = RadarProtocol.parse_status_packet(raw) sr = RadarProtocol.parse_status_packet(raw)
self.assertEqual(sr.chirps_mismatch, 1) self.assertEqual(sr.chirps_mismatch, 1)
self.assertEqual(sr.agc_enable, 1) self.assertEqual(sr.agc_enable, 1)
self.assertEqual(sr.beam_handshake_watchdog, 0)
def test_parse_status_beam_handshake_watchdog(self):
# PR-AB.b expanded commit 5: bit 1 of word 4 must round-trip without
# bleeding into chirps_mismatch (bit 10) or the reserved [0] bit.
raw = self._make_status_packet(beam_handshake_watchdog=1)
sr = RadarProtocol.parse_status_packet(raw)
self.assertEqual(sr.beam_handshake_watchdog, 1)
self.assertEqual(sr.chirps_mismatch, 0)
self.assertEqual(sr.agc_enable, 0)
def test_parse_status_too_short(self): def test_parse_status_too_short(self):
# Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected. # Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected.
@@ -267,9 +284,15 @@ class TestRadarProtocol(unittest.TestCase):
[19:12] agc_saturation_count (8-bit) [19:12] agc_saturation_count (8-bit)
[11] agc_enable (1-bit) [11] agc_enable (1-bit)
[10] chirps_mismatch (1-bit, TX-G) [10] chirps_mismatch (1-bit, TX-G)
[9:0] reserved (10 bits, must be zero from builder) [9:2] reserved / alpha_soft (8 bits FT601 leaves zero; FT2232H
(was [9:2] + range_mode[1:0]; range_mode retired in echoes host_cfar_alpha_soft. This
PR-AB.b expanded) co-spec builder uses the FT601
layout so the builder remains
path-agnostic.)
[1] beam_handshake_watchdog (1-bit sticky, PR-AB.b expanded
commit 5)
[0] reserved (1-bit; was range_mode[0], retired
in PR-AB.b expanded)
For each field we set ONLY that field to its max, build the packet, For each field we set ONLY that field to its max, build the packet,
parse, and assert (a) the field reads back correctly and (b) every parse, and assert (a) the field reads back correctly and (b) every
@@ -283,11 +306,14 @@ class TestRadarProtocol(unittest.TestCase):
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"), ("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
("agc_enable", "agc_enable", 11, 1, "agc_enable"), ("agc_enable", "agc_enable", 11, 1, "agc_enable"),
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"), ("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
("beam_handshake_watchdog", "beam_handshake_watchdog",
1, 1, "beam_handshake_watchdog"),
] ]
# Sanity: layout fields + reserved [9:0] must cover exactly 32 bits. # Sanity: layout fields + reserved [9:2] (8 bits) + reserved [0] (1 bit)
# must cover exactly 32 bits.
used = sum(width for _, _, _, width, _ in layout) used = sum(width for _, _, _, width, _ in layout)
self.assertEqual(used + 10, 32, self.assertEqual(used + 9, 32,
"word 4 layout (incl. reserved [9:0]) must total 32 bits") "word 4 layout (incl. reserved [9:2] + [0]) must total 32 bits")
# No two fields may overlap. # No two fields may overlap.
occupied = set() occupied = set()
@@ -1002,6 +1028,10 @@ class TestOpcodeEnum(unittest.TestCase):
self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03) self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03)
self.assertEqual(Opcode.STREAM_CONTROL, 0x04) self.assertEqual(Opcode.STREAM_CONTROL, 0x04)
def test_handshake_enable_opcode(self):
"""PR-AB.b expanded commit 5: beam-ready handshake opcode = 0x1A."""
self.assertEqual(Opcode.HANDSHAKE_ENABLE, 0x1A)
def test_all_rtl_opcodes_present(self): def test_all_rtl_opcodes_present(self):
"""Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member. """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.
@@ -1010,6 +1040,7 @@ class TestOpcodeEnum(unittest.TestCase):
""" """
expected = {0x03, 0x04, expected = {0x03, 0x04,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0x1A,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x28, 0x29, 0x2A, 0x2B, 0x2C,
0x30, 0x31, 0xFF} 0x30, 0x31, 0xFF}
+2 -2
View File
@@ -994,7 +994,7 @@ class RadarDashboard(QMainWindow):
spin.setValue(default) spin.setValue(default)
# PR-AB.b: setFixedWidth (not min/max) — QHBoxLayout would otherwise # PR-AB.b: setFixedWidth (not min/max) — QHBoxLayout would otherwise
# squeeze the spinbox toward its minimum on rows where the hint is # squeeze the spinbox toward its minimum on rows where the hint is
# longer than its peers (the AGC Enable hint is ~3× longer than the # longer than its peers (the AGC Enable hint is ~3x longer than the
# others and was rendering at ~90 px while siblings hit ~160). # others and was rendering at ~90 px while siblings hit ~160).
spin.setFixedWidth(120) spin.setFixedWidth(120)
row.addWidget(spin) row.addWidget(spin)
@@ -2119,7 +2119,7 @@ class RadarDashboard(QMainWindow):
# the last StatusResponse if any, otherwise the static defaults. # the last StatusResponse if any, otherwise the static defaults.
self._refresh_agc_mode_labels(self._last_status) self._refresh_agc_mode_labels(self._last_status)
def _refresh_agc_mode_labels(self, st: "StatusResponse | None"): def _refresh_agc_mode_labels(self, st: StatusResponse | None):
"""Update the AGC enable text on both the FPGA Control Status box """Update the AGC enable text on both the FPGA Control Status box
(self._agc_labels['enable']) and the AGC Monitor strip (self._agc_labels['enable']) and the AGC Monitor strip
(self._agc_mode_lbl). In production the firmware ignores the FPGA (self._agc_mode_lbl). In production the firmware ignores the FPGA