mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-10 23:41:18 +00:00
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:
@@ -626,6 +626,10 @@ void runRadarPulseSequence() {
|
||||
DIAG("SYS", "Broadside reference (vector_0) — 1× per azimuth");
|
||||
adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::TX);
|
||||
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);
|
||||
m += m_max/2;
|
||||
|
||||
@@ -636,12 +640,14 @@ void runRadarPulseSequence() {
|
||||
// Pattern 1: matrix1 (negative-θ scan, peak at -62°..-3°)
|
||||
adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::TX);
|
||||
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);
|
||||
m += m_max/2;
|
||||
|
||||
// Pattern 2: matrix2 (positive-θ scan, peak at +3°..+62°)
|
||||
adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::TX);
|
||||
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);
|
||||
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_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_GPIO_Port GPIOG
|
||||
#define ADF4382_RX_CS_Pin GPIO_PIN_10
|
||||
|
||||
@@ -24,6 +24,15 @@
|
||||
* Pulse outputs (chirp_pulse, subframe_pulse, frame_pulse) are 1-cycle
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -55,6 +64,15 @@ module chirp_scheduler (
|
||||
// TX-side cdc_async_fifo before mixers come up.
|
||||
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 ======
|
||||
output reg [1:0] wave_sel, // canonical waveform identity
|
||||
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)
|
||||
output wire [15:0] cfg_chirp_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,17 +162,46 @@ assign cfg_chirp_cycles = sel_chirp_cycles;
|
||||
assign cfg_listen_cycles = sel_listen_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.
|
||||
// ============================================================================
|
||||
localparam S_IDLE = 3'd0;
|
||||
localparam S_CHIRP = 3'd1;
|
||||
localparam S_LISTEN = 3'd2;
|
||||
localparam S_GUARD = 3'd3;
|
||||
localparam S_ADVANCE = 3'd4;
|
||||
localparam S_IDLE = 3'd0;
|
||||
localparam S_CHIRP = 3'd1;
|
||||
localparam S_LISTEN = 3'd2;
|
||||
localparam S_GUARD = 3'd3;
|
||||
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 [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
|
||||
// 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;
|
||||
chirp_counter <= 6'd0;
|
||||
subframe_id <= 2'd0;
|
||||
beam_watchdog <= 23'd0;
|
||||
beam_handshake_watchdog_fired <= 1'b0;
|
||||
end else if (!mixers_enable) begin
|
||||
// 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;
|
||||
timer <= 17'd0;
|
||||
chirp_pulse <= 1'b0;
|
||||
subframe_pulse <= 1'b0;
|
||||
frame_pulse <= 1'b0;
|
||||
beam_watchdog <= 23'd0;
|
||||
end else begin
|
||||
// Pulses default low — set high for one cycle on relevant transitions.
|
||||
chirp_pulse <= 1'b0;
|
||||
@@ -219,10 +275,39 @@ always @(posedge clk or negedge reset_n) begin
|
||||
subframe_pulse <= 1'b1;
|
||||
subframe_id <= 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;
|
||||
chirp_pulse <= 1'b1;
|
||||
state <= S_CHIRP;
|
||||
if (host_handshake_enable) begin
|
||||
beam_watchdog <= 23'd0;
|
||||
state <= S_BEAM_WAIT;
|
||||
end else begin
|
||||
chirp_pulse <= 1'b1;
|
||||
state <= S_CHIRP;
|
||||
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
|
||||
default: state <= S_IDLE;
|
||||
|
||||
@@ -229,13 +229,13 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_*_3v3}]
|
||||
# STM32 DIG BUS — Control signals (Bank 15, VCCO = 3.3V)
|
||||
# ============================================================================
|
||||
# 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 /
|
||||
# stm32_new_azimuth. MCU side init is also stripped (see commit 3); the
|
||||
# pins default to high-Z inputs after MCU reset.
|
||||
# Pin: N20 = IO_L18P_T2_A24_15
|
||||
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}]
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -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_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
|
||||
# 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.
|
||||
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}]
|
||||
# reset_n is DIG_4 (PD12) — constrained above in the RESET section
|
||||
|
||||
|
||||
@@ -67,6 +67,16 @@ module radar_receiver_final (
|
||||
// so it stays in S_IDLE until the operator turns the radar on.
|
||||
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
|
||||
output wire doppler_frame_done_out,
|
||||
|
||||
@@ -236,6 +246,10 @@ chirp_scheduler sched (
|
||||
.host_long_listen_cycles(host_long_listen_cycles),
|
||||
.host_guard_cycles(host_guard_cycles),
|
||||
.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),
|
||||
.chirp_pulse(chirp_pulse),
|
||||
.subframe_pulse(subframe_pulse),
|
||||
@@ -244,7 +258,8 @@ chirp_scheduler sched (
|
||||
.subframe_id(sched_subframe_id),
|
||||
.cfg_chirp_cycles (sched_cfg_chirp_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
|
||||
|
||||
@@ -78,9 +78,12 @@ module radar_system_top (
|
||||
|
||||
// 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
|
||||
// XDC + MCU GPIO scrub. stm32_new_chirp will be renamed to
|
||||
// stm32_beam_ready in commit 5 (beam-ready handshake).
|
||||
input wire stm32_new_chirp,
|
||||
// XDC + MCU GPIO scrub. stm32_beam_ready (PD8) is the MCU's per-pattern
|
||||
// ready toggle for the beam-ready handshake added in PR-AB.b expanded
|
||||
// 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,
|
||||
|
||||
// ========== FT601 USB 3.0 INTERFACE ==========
|
||||
@@ -248,6 +251,12 @@ end
|
||||
// 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.
|
||||
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
|
||||
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.
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
// Fix 4: Doppler/chirps mismatch protection
|
||||
@@ -627,6 +642,14 @@ radar_receiver_final rx_inst (
|
||||
.host_agc_holdoff(host_agc_holdoff),
|
||||
// PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above)
|
||||
.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
|
||||
.doppler_frame_done_out(rx_frame_complete),
|
||||
// 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
|
||||
// source-domain pulse. F-1.2 overrun is already sticky in source.
|
||||
.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
|
||||
@@ -1002,7 +1031,15 @@ end else begin : gen_ft2232h
|
||||
// PR-G: 2-tier CFAR telemetry (status_words[6])
|
||||
.status_cfar_alpha_soft(host_cfar_alpha_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
|
||||
@@ -1096,6 +1133,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
|
||||
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;
|
||||
// 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;
|
||||
chirps_mismatch_error <= 1'b0;
|
||||
// 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
|
||||
// confidence rather than mis-attribute the SF axis.
|
||||
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
|
||||
// Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size.
|
||||
// If host requests a different value, clamp and set error flag.
|
||||
|
||||
@@ -68,8 +68,10 @@ module radar_system_top_50t (
|
||||
|
||||
// ===== STM32 Control (Bank 15: 3.3V) =====
|
||||
// PR-AB.b expanded: stm32_new_elevation/azimuth retired (no consumer).
|
||||
// stm32_new_chirp becomes stm32_beam_ready in commit 5.
|
||||
input wire stm32_new_chirp,
|
||||
// stm32_beam_ready (PD8) is the MCU per-pattern ready toggle for the
|
||||
// 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,
|
||||
|
||||
// ===== FT2232H USB 2.0 Interface (Bank 35: 3.3V) =====
|
||||
@@ -178,7 +180,7 @@ module radar_system_top_50t (
|
||||
.adc_pwdn (adc_pwdn),
|
||||
|
||||
// ----- STM32 Control -----
|
||||
.stm32_new_chirp (stm32_new_chirp),
|
||||
.stm32_beam_ready (stm32_beam_ready),
|
||||
.stm32_mixers_enable (stm32_mixers_enable),
|
||||
|
||||
// ----- FT2232H USB 2.0 (active on 50T, USB_MODE=1) -----
|
||||
|
||||
@@ -574,6 +574,10 @@ run_test "DIG6 frame-pulse stretcher (PR-AB.b)" \
|
||||
tb/tb_dig6_frame_pulse.vvp \
|
||||
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)" \
|
||||
tb/tb_audit_c16_num_cells_50t.vvp \
|
||||
tb/tb_audit_c16_num_cells.v
|
||||
|
||||
@@ -358,7 +358,7 @@ def check_mf_invariants(result: CheckResult):
|
||||
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
|
||||
# near the LSB floor, easily clearing the threshold.
|
||||
twin_peak_val = float(twin_mag[delay])
|
||||
|
||||
@@ -62,7 +62,7 @@ import numpy as np
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
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.
|
||||
from gen_e2e_stimulus import ( # noqa: E402
|
||||
@@ -72,8 +72,6 @@ from gen_e2e_stimulus import ( # noqa: E402
|
||||
CHIRPS_PER_FRAME,
|
||||
RANGE_BINS,
|
||||
HOST_DC_NOTCH_WIDTH,
|
||||
EXPECTED_RANGE_BIN,
|
||||
EXPECTED_DOPPLER_BIN_PER_SF,
|
||||
EXPECTED_DETECT_CELLS,
|
||||
)
|
||||
|
||||
@@ -263,10 +261,8 @@ def pack_bulk_frame(frame_number: int, flags: int,
|
||||
packed = 0
|
||||
for slot in range(4):
|
||||
db = byte_idx * 4 + slot
|
||||
if db < DOPPLER_TOTAL_BINS:
|
||||
code = int(cfar_class[rb, db]) & 0x3
|
||||
else:
|
||||
code = 0 # padding
|
||||
# padding for db >= DOPPLER_TOTAL_BINS lands on slot 3
|
||||
code = int(cfar_class[rb, db]) & 0x3 if db < DOPPLER_TOTAL_BINS else 0
|
||||
packed |= code << ((3 - slot) * 2)
|
||||
out.append(packed)
|
||||
|
||||
|
||||
@@ -98,16 +98,15 @@ HOST_DC_NOTCH_WIDTH = 1
|
||||
# ============================================================================
|
||||
# Target placement -> expected bin coordinates
|
||||
# ============================================================================
|
||||
# range_bin = round(2 * R / c * fs / decim)
|
||||
# = round(2 * 100 / 3e8 * 400e6 / 4)
|
||||
# = round(66.667) = 67
|
||||
EXPECTED_RANGE_BIN = int(round(2.0 * TARGET_RANGE_M / C_LIGHT * RANGE_BIN_HZ))
|
||||
# Range bin formula: round(2 * R / c * fs / decim). For R=100m, fs=400 MHz,
|
||||
# decim=4 -> round(2 * 100 / 3e8 * 100e6) = round(66.667) = 67.
|
||||
EXPECTED_RANGE_BIN = 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
|
||||
# this is intentionally non-folding -> 1 in all three sub-frames.
|
||||
F_DOPPLER_HZ = 2.0 * TARGET_VEL_MPS * F_CARRIER / C_LIGHT
|
||||
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
|
||||
)
|
||||
# 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.
|
||||
phi = _target_phase_rad(c)
|
||||
sig_i = int(round(TARGET_AMPLITUDE * np.cos(phi)))
|
||||
sig_q = int(round(TARGET_AMPLITUDE * np.sin(phi)))
|
||||
sig_i = round(TARGET_AMPLITUDE * np.cos(phi))
|
||||
sig_q = round(TARGET_AMPLITUDE * np.sin(phi))
|
||||
frame_i[c, EXPECTED_RANGE_BIN] += sig_i
|
||||
frame_q[c, EXPECTED_RANGE_BIN] += sig_q
|
||||
|
||||
@@ -231,7 +230,7 @@ def main() -> int:
|
||||
f"shape={frame_i.shape}")
|
||||
|
||||
if n_lines != expected_lines:
|
||||
print(f" ERROR: line count mismatch", file=sys.stderr)
|
||||
print(" ERROR: line count mismatch", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Sanity: target peak should dominate at the expected range bin.
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestState:
|
||||
def load_captured_frame_hex(path: str) -> bytes:
|
||||
"""Read iverilog $writememh output (one byte per line, 2-hex-digit)."""
|
||||
out = bytearray()
|
||||
with open(path, 'r') as f:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
tok = line.strip()
|
||||
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_range_decim_watchdog(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_detect_threshold_soft(17'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)
|
||||
.status_range_decim_watchdog(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
|
||||
.status_cfar_alpha_soft(8'h18), // RP_DEF_CFAR_ALPHA_SOFT
|
||||
.status_detect_threshold_soft(17'd0),
|
||||
|
||||
@@ -185,7 +185,14 @@ radar_receiver_final dut (
|
||||
.doppler_frame_done_out(),
|
||||
|
||||
// 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()
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
// mixer-disable propagation)
|
||||
// G7.3 TX chirp counter CDC (120MHz -> 100MHz)
|
||||
// — 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
|
||||
// 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_pwdn(adc_pwdn),
|
||||
|
||||
.stm32_beam_ready(1'b0), // commit 5: handshake disabled by cold-reset default
|
||||
.stm32_mixers_enable(stm32_mixers_enable),
|
||||
|
||||
.ft601_data(ft601_data),
|
||||
|
||||
@@ -158,6 +158,7 @@ radar_system_top #(
|
||||
.adc_or_p(1'b0), .adc_or_n(1'b1),
|
||||
.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),
|
||||
|
||||
// FT601 ports — tied off / unused in USB_MODE=1
|
||||
|
||||
@@ -144,7 +144,9 @@ module tb_usb_data_interface;
|
||||
.status_agc_enable (status_agc_enable),
|
||||
// AUDIT-S10: control-fault flags tied off (pre-existing TB scope)
|
||||
.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 ───────────────────────────────────────
|
||||
|
||||
@@ -157,6 +157,10 @@ module tb_usb_protocol_v2;
|
||||
.status_agc_enable(status_agc_enable),
|
||||
.status_range_decim_watchdog(status_range_decim_watchdog),
|
||||
.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_detect_threshold_soft(status_detect_threshold_soft),
|
||||
.status_detect_count_cand(status_detect_count_cand)
|
||||
|
||||
@@ -113,7 +113,12 @@ module usb_data_interface (
|
||||
// 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.
|
||||
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)
|
||||
@@ -339,6 +344,9 @@ reg status_req_sync_prev;
|
||||
reg range_decim_watchdog_sync_1;
|
||||
(* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
ddc_cic_fir_overrun_sync_0 <= 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
|
||||
// Synchronize valid strobes (2-stage sync chain)
|
||||
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;
|
||||
ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun;
|
||||
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
|
||||
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};
|
||||
// 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};
|
||||
// Word 4: AGC metrics (range_mode retired in PR-AB.b expanded)
|
||||
status_words[4] <= {status_agc_current_gain, // [31:28]
|
||||
status_agc_peak_magnitude, // [27:20]
|
||||
status_agc_saturation_count, // [19:12] 8-bit saturation count
|
||||
status_agc_enable, // [11]
|
||||
status_chirps_mismatch, // [10] TX-G mismatch flag
|
||||
8'd0, // [9:2] reserved
|
||||
2'd0}; // [1:0] reserved (was range_mode)
|
||||
// Word 4 layout (post PR-AB.b expanded commit 5):
|
||||
// [31:28] agc_current_gain
|
||||
// [27:20] agc_peak_magnitude
|
||||
// [19:12] agc_saturation_count
|
||||
// [11] agc_enable
|
||||
// [10] chirps_mismatch (TX-G)
|
||||
// [9:2] reserved 0 (alpha_soft echo lives in ft2232h-only path)
|
||||
// [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],
|
||||
// self_test_detail[15:8], reserved[7], cic_fir_overrun[6],
|
||||
// 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_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]).
|
||||
// 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)
|
||||
@@ -674,6 +679,10 @@ reg [6:0] frame_drop_sync_1;
|
||||
reg range_decim_watchdog_sync_1;
|
||||
(* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0;
|
||||
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_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;
|
||||
ddc_cic_fir_overrun_sync_0 <= 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
|
||||
alpha_soft_sync_0 <= 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;
|
||||
ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun;
|
||||
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)
|
||||
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[2] <= {status_guard, status_short_chirp};
|
||||
status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev};
|
||||
status_words[4] <= {status_agc_current_gain, // [31:28]
|
||||
status_agc_peak_magnitude, // [27:20]
|
||||
status_agc_saturation_count, // [19:12]
|
||||
status_agc_enable, // [11]
|
||||
status_chirps_mismatch, // [10] TX-G mismatch flag
|
||||
alpha_soft_sync_1, // [9:2] PR-G: host_cfar_alpha_soft echo (Q4.4)
|
||||
2'd0}; // [1:0] reserved (was range_mode, retired PR-AB.b expanded)
|
||||
// Word 4 layout (post PR-AB.b expanded commit 5):
|
||||
// [31:28] agc_current_gain
|
||||
// [27:20] agc_peak_magnitude
|
||||
// [19:12] agc_saturation_count
|
||||
// [11] agc_enable
|
||||
// [10] chirps_mismatch (TX-G)
|
||||
// [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],
|
||||
// reserved[23:16], self_test_detail[15:8], reserved[7],
|
||||
// cic_fir_overrun[6], range_decim_watchdog[5],
|
||||
|
||||
@@ -136,6 +136,7 @@ class Opcode(IntEnum):
|
||||
0x17 host_medium_chirp_cycles (PR-G G2)
|
||||
0x18 host_medium_listen_cycles (PR-G G2)
|
||||
0x19 host_subframe_enable (PR-U / M-8 — 3-bit {LONG, MED, SHORT} mask)
|
||||
0x1A host_handshake_enable (PR-AB.b expanded commit 5 — beam-ready stall)
|
||||
|
||||
PR-AB.b expanded retired opcodes 0x01 (host_radar_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
|
||||
# otherwise be wrong when the scheduler skips a sub-frame).
|
||||
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;
|
||||
# 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_enable: int = 0 # 1-bit AGC enable readback
|
||||
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)
|
||||
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)
|
||||
# AUDIT-S10 control-fault flags (word 5 high half)
|
||||
frame_drop_count: int = 0 # frame-drop counter from RTL
|
||||
# M-5 MEDIUM PRI readback (word 7) — closes 161-µs MEDIUM visibility gap.
|
||||
medium_chirp: int = 0 # opcode 0x17 readback (16-bit, default RP_DEF_MEDIUM_CHIRP_CYCLES)
|
||||
medium_listen: int = 0 # opcode 0x18 readback (16-bit, default RP_DEF_MEDIUM_LISTEN_CYCLES)
|
||||
# opcode 0x17/0x18 readback (16-bit each, default RP_DEF_MEDIUM_*_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]}
|
||||
sr.chirps_per_elev = words[3] & 0x3F
|
||||
sr.short_listen = (words[3] >> 16) & 0xFFFF
|
||||
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] reserved[1:0]
|
||||
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
|
||||
# Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11]
|
||||
# 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.beam_handshake_watchdog = (words[4] >> 1) & 0x01
|
||||
sr.agc_enable = (words[4] >> 11) & 0x01
|
||||
sr.agc_saturation_count = (words[4] >> 12) & 0xFF
|
||||
sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF
|
||||
|
||||
@@ -133,6 +133,7 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
st_flags=0, st_detail=0, st_busy=0,
|
||||
agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0,
|
||||
chirps_mismatch=0,
|
||||
beam_handshake_watchdog=0,
|
||||
cand_count=0, thr_soft=0, frame_drop=0,
|
||||
medium_chirp=0, medium_listen=0):
|
||||
"""Build an M-5 34-byte status response matching FPGA format."""
|
||||
@@ -156,13 +157,18 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F)
|
||||
pkt += struct.pack(">I", w3)
|
||||
|
||||
# Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0],
|
||||
# agc_saturation_count[7:0], agc_enable,
|
||||
# chirps_mismatch[10], 10'd0 reserved [9:0]}
|
||||
# PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0.
|
||||
# Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20],
|
||||
# agc_saturation_count[19:12], agc_enable[11],
|
||||
# chirps_mismatch[10], reserved[9:2],
|
||||
# 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) |
|
||||
((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)
|
||||
|
||||
# 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.chirps_per_elev, 32)
|
||||
self.assertEqual(sr.chirps_mismatch, 0)
|
||||
self.assertEqual(sr.beam_handshake_watchdog, 0)
|
||||
|
||||
def test_parse_status_chirps_mismatch(self):
|
||||
# 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)
|
||||
self.assertEqual(sr.chirps_mismatch, 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):
|
||||
# Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected.
|
||||
@@ -262,14 +279,20 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
usb_data_interface_ft2232h.v:675-679 — exactly one source of truth
|
||||
in this test, so any future drift between FPGA and GUI trips here:
|
||||
|
||||
[31:28] agc_current_gain (4-bit)
|
||||
[27:20] agc_peak_magnitude (8-bit)
|
||||
[19:12] agc_saturation_count (8-bit)
|
||||
[11] agc_enable (1-bit)
|
||||
[10] chirps_mismatch (1-bit, TX-G)
|
||||
[9:0] reserved (10 bits, must be zero from builder)
|
||||
(was [9:2] + range_mode[1:0]; range_mode retired in
|
||||
PR-AB.b expanded)
|
||||
[31:28] agc_current_gain (4-bit)
|
||||
[27:20] agc_peak_magnitude (8-bit)
|
||||
[19:12] agc_saturation_count (8-bit)
|
||||
[11] agc_enable (1-bit)
|
||||
[10] chirps_mismatch (1-bit, TX-G)
|
||||
[9:2] reserved / alpha_soft (8 bits — FT601 leaves zero; FT2232H
|
||||
echoes host_cfar_alpha_soft. This
|
||||
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,
|
||||
parse, and assert (a) the field reads back correctly and (b) every
|
||||
@@ -278,16 +301,19 @@ class TestRadarProtocol(unittest.TestCase):
|
||||
"""
|
||||
layout = [
|
||||
# (field_name, builder_kwarg, lsb, width, parsed_attr)
|
||||
("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"),
|
||||
("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"),
|
||||
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
|
||||
("agc_enable", "agc_enable", 11, 1, "agc_enable"),
|
||||
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
|
||||
("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"),
|
||||
("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"),
|
||||
("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"),
|
||||
("agc_enable", "agc_enable", 11, 1, "agc_enable"),
|
||||
("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"),
|
||||
("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)
|
||||
self.assertEqual(used + 10, 32,
|
||||
"word 4 layout (incl. reserved [9:0]) must total 32 bits")
|
||||
self.assertEqual(used + 9, 32,
|
||||
"word 4 layout (incl. reserved [9:2] + [0]) must total 32 bits")
|
||||
|
||||
# No two fields may overlap.
|
||||
occupied = set()
|
||||
@@ -1002,6 +1028,10 @@ class TestOpcodeEnum(unittest.TestCase):
|
||||
self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03)
|
||||
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):
|
||||
"""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,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0x1A,
|
||||
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
|
||||
0x28, 0x29, 0x2A, 0x2B, 0x2C,
|
||||
0x30, 0x31, 0xFF}
|
||||
|
||||
@@ -994,7 +994,7 @@ class RadarDashboard(QMainWindow):
|
||||
spin.setValue(default)
|
||||
# PR-AB.b: setFixedWidth (not min/max) — QHBoxLayout would otherwise
|
||||
# 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).
|
||||
spin.setFixedWidth(120)
|
||||
row.addWidget(spin)
|
||||
@@ -2119,7 +2119,7 @@ class RadarDashboard(QMainWindow):
|
||||
# the last StatusResponse if any, otherwise the static defaults.
|
||||
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
|
||||
(self._agc_labels['enable']) and the AGC Monitor strip
|
||||
(self._agc_mode_lbl). In production the firmware ignores the FPGA
|
||||
|
||||
Reference in New Issue
Block a user