PLFM_RADAR/9_Firmware/9_2_FPGA/radar_mode_controller.v
Jason 5617d552df fix(fpga): frame-boundary atomic commit for USB timing regs
C-4: GUI Waveform Timing opcodes (0x10..0x14) write one parameter per
USB packet with arbitrary inter-opcode latency. Previously each opcode
landed directly on a live register consumed by radar_mode_controller,
so a mid-scan reconfiguration exposed the RX FSM to torn sets (e.g.
new long_chirp with old long_listen), and a value below the running
timer truncated the current chirp and corrupted one range-Doppler
frame.

Fix: split each of the five timing regs into pending (USB write target)
and live (FSM consumer). radar_mode_controller exports a new
cfg_commit_strobe that is HIGH while scanning is idle (S_IDLE) and
pulses for 1 cycle at every elevation/azimuth boundary in S_ADVANCE.
radar_system_top snapshots pending -> live on the strobe, guaranteeing
the FSM always sees a self-consistent set. No CDC added: strobe is in
clk_100m same as the USB-cmd path.

Regression: tb_radar_mode_controller adds 3 checks for strobe
semantics (idle-HIGH, scanning-LOW, one pulse per elevation boundary).
32/32 full regression PASS; Radar Mode Controller TB grows from 78 to
81 checks.
2026-04-23 04:43:02 +05:45

530 lines
20 KiB
Verilog

`timescale 1ns / 1ps
`include "radar_params.vh"
/**
* radar_mode_controller.v
*
* Generates beam scanning and chirp mode control signals for the AERIS-10
* receiver processing chain. This module drives:
* - use_long_chirp : selects long (30us) or short (0.5us) chirp mode
* - mc_new_chirp : toggle signal indicating new chirp start
* - mc_new_elevation : toggle signal indicating elevation step
* - mc_new_azimuth : toggle signal indicating azimuth step
*
* These signals are consumed by matched_filter_multi_segment and
* chirp_memory_loader_param in the receiver path.
*
* The controller mirrors the transmitter's chirp sequence defined in
* plfm_chirp_controller_enhanced:
* - 32 chirps per elevation
* - 31 elevations per azimuth
* - 50 azimuths per full scan
*
* Chirp sequence depends on range_mode (host_range_mode, opcode 0x20):
* range_mode 2'b00 (3 km): All short chirps only. Long chirp blind zone
* (4500 m) exceeds 3 km max range, so long chirps are useless.
* range_mode 2'b01 (long-range): Dual chirp — Long chirp → Listen → Guard
* → Short chirp → Listen. First half of chirps_per_elev are long, second
* half are short (blind-zone fill).
*
* Modes of operation (host_radar_mode, opcode 0x01):
* mode[1:0]:
* 2'b00 = STM32-driven (pass through stm32 toggle signals)
* 2'b01 = Free-running auto-scan (internal timing, short chirps only)
* 2'b10 = Single-chirp (fire one chirp per trigger, for debug)
* 2'b11 = Reserved
*
* Clock domain: clk (100 MHz)
*/
module radar_mode_controller #(
parameter CHIRPS_PER_ELEVATION = `RP_DEF_CHIRPS_PER_ELEV,
parameter ELEVATIONS_PER_AZIMUTH = 31,
parameter AZIMUTHS_PER_SCAN = 50,
// Timing in 100 MHz clock cycles
// Long chirp: 30us = 3000 cycles at 100 MHz
// Long listen: 137us = 13700 cycles
// Guard: 175.4us = 17540 cycles
// Short chirp: 0.5us = 50 cycles
// Short listen: 174.5us = 17450 cycles
parameter LONG_CHIRP_CYCLES = `RP_DEF_LONG_CHIRP_CYCLES,
parameter LONG_LISTEN_CYCLES = `RP_DEF_LONG_LISTEN_CYCLES,
parameter GUARD_CYCLES = `RP_DEF_GUARD_CYCLES,
parameter SHORT_CHIRP_CYCLES = `RP_DEF_SHORT_CHIRP_CYCLES,
parameter SHORT_LISTEN_CYCLES = `RP_DEF_SHORT_LISTEN_CYCLES
) (
input wire clk,
input wire reset_n,
// Mode selection (host_radar_mode, opcode 0x01)
input wire [1:0] mode, // 00=STM32, 01=auto, 10=single, 11=rsvd
// Range mode (host_range_mode, opcode 0x20)
// Determines chirp type selection in pass-through and auto-scan modes.
// 2'b00 = 3 km (all short chirps — long blind zone > max range)
// 2'b01 = Long-range (dual chirp: first half long, second half short)
input wire [1:0] range_mode,
// STM32 pass-through inputs (active in mode 00)
input wire stm32_new_chirp,
input wire stm32_new_elevation,
input wire stm32_new_azimuth,
// Single-chirp trigger (active in mode 10)
input wire trigger,
// Runtime-configurable timing inputs from host USB commands.
// When connected, these override the compile-time parameters.
input wire [15:0] cfg_long_chirp_cycles,
input wire [15:0] cfg_long_listen_cycles,
input wire [15:0] cfg_guard_cycles,
input wire [15:0] cfg_short_chirp_cycles,
input wire [15:0] cfg_short_listen_cycles,
input wire [5:0] cfg_chirps_per_elev,
// Outputs to receiver processing chain
output reg use_long_chirp,
output reg mc_new_chirp,
output reg mc_new_elevation,
output reg mc_new_azimuth,
// Beam position tracking
output reg [5:0] chirp_count,
output reg [5:0] elevation_count,
output reg [5:0] azimuth_count,
// Status
output wire scanning, // 1 = scan in progress
output wire scan_complete, // pulse when full scan done
// Timing-config commit strobe (see "Timing-Config Commit" note below).
// Held HIGH while scanning is idle (S_IDLE) and pulsed for 1 clock at
// every elevation/azimuth boundary so radar_system_top can atomically
// copy its USB-written pending timing regs into the live regs the FSM
// reads. Guarantees no torn (cfg_long_chirp, cfg_long_listen, ...)
// reconfiguration mid-frame.
output wire cfg_commit_strobe
`ifdef FORMAL
,
output wire [2:0] fv_scan_state,
output wire [17:0] fv_timer
`endif
);
// ============================================================================
// INTERNAL STATE
// ============================================================================
// Auto-scan state machine
reg [2:0] scan_state;
localparam S_IDLE = 3'd0;
localparam S_LONG_CHIRP = 3'd1;
localparam S_LONG_LISTEN = 3'd2;
localparam S_GUARD = 3'd3;
localparam S_SHORT_CHIRP = 3'd4;
localparam S_SHORT_LISTEN = 3'd5;
localparam S_ADVANCE = 3'd6;
// Timing counter
reg [17:0] timer; // enough for up to 262143 cycles (~2.6ms at 100 MHz)
`ifdef FORMAL
assign fv_scan_state = scan_state;
assign fv_timer = timer;
`endif
// ============================================================================
// Timing-Config Commit Strobe
//
// radar_system_top uses this to snapshot pending USB-written timing regs
// (opcodes 0x10..0x14) into the live regs this FSM consumes. The strobe
// is ASSERTED whenever it is safe to commit new timing:
// (a) FSM is in S_IDLE (no chirp underway) -> held HIGH continuously
// so an opcode written before the first trigger takes effect on
// the first chirp.
// (b) FSM is in S_ADVANCE at the elevation/azimuth boundary (i.e.,
// chirp_count has reached the last chirp of the current elevation)
// -> pulsed for the single S_ADVANCE cycle. The next frame of
// CHIRPS_PER_ELEVATION chirps runs with the newly-committed set.
//
// Combinational (no added FF). scan_state and chirp_count are registers
// in the same clock domain, so this wire is glitch-free relative to the
// destination register's clock edge. Downstream is a synchronous-enable
// FF in radar_system_top, which captures cleanly on the rising edge.
// ============================================================================
assign cfg_commit_strobe =
(scan_state == S_IDLE) ||
((scan_state == S_ADVANCE) && (chirp_count >= cfg_chirps_per_elev - 1));
// Edge detection for STM32 pass-through
reg stm32_new_chirp_prev;
reg stm32_new_elevation_prev;
reg stm32_new_azimuth_prev;
// Trigger edge detection (for single-chirp mode)
reg trigger_prev;
wire trigger_pulse = trigger & ~trigger_prev;
// Scan completion
reg scan_done_pulse;
// ============================================================================
// EDGE DETECTION
// ============================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
stm32_new_chirp_prev <= 1'b0;
stm32_new_elevation_prev <= 1'b0;
stm32_new_azimuth_prev <= 1'b0;
trigger_prev <= 1'b0;
end else begin
stm32_new_chirp_prev <= stm32_new_chirp;
stm32_new_elevation_prev <= stm32_new_elevation;
stm32_new_azimuth_prev <= stm32_new_azimuth;
trigger_prev <= trigger;
end
end
wire stm32_chirp_toggle = stm32_new_chirp ^ stm32_new_chirp_prev;
wire stm32_elevation_toggle = stm32_new_elevation ^ stm32_new_elevation_prev;
wire stm32_azimuth_toggle = stm32_new_azimuth ^ stm32_new_azimuth_prev;
// ============================================================================
// MAIN STATE MACHINE
// ============================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
scan_state <= S_IDLE;
timer <= 18'd0;
use_long_chirp <= 1'b0; // Default short chirp (safe for 3 km mode)
mc_new_chirp <= 1'b0;
mc_new_elevation <= 1'b0;
mc_new_azimuth <= 1'b0;
chirp_count <= 6'd0;
elevation_count <= 6'd0;
azimuth_count <= 6'd0;
scan_done_pulse <= 1'b0;
end else begin
// Clear one-shot signals
scan_done_pulse <= 1'b0;
case (mode)
// ================================================================
// MODE 00: STM32-driven pass-through
// The STM32 firmware controls timing; we just detect toggle edges
// and forward them to the receiver chain. Chirp type is determined
// by range_mode:
// range_mode 00 (3 km): ALL chirps are short (long blind zone
// 4500 m exceeds 3072 m max range, so long chirps are useless).
// range_mode 01 (long-range): First half of chirps_per_elev are
// long, second half are short (blind-zone fill).
// ================================================================
2'b00: begin
// Reset auto-scan state
scan_state <= S_IDLE;
timer <= 18'd0;
// Pass through toggle signals
if (stm32_chirp_toggle) begin
mc_new_chirp <= ~mc_new_chirp; // Toggle output
// Determine chirp type based on range_mode
case (range_mode)
`RP_RANGE_MODE_3KM: begin
// 3 km mode: all short chirps
use_long_chirp <= 1'b0;
end
`RP_RANGE_MODE_LONG: begin
// Long-range: first half long, second half short.
// chirps_per_elev is typically 32 (16 long + 16 short).
// Use cfg_chirps_per_elev[5:1] as the halfway point.
if (chirp_count < {1'b0, cfg_chirps_per_elev[5:1]})
use_long_chirp <= 1'b1;
else
use_long_chirp <= 1'b0;
end
default: begin
// Reserved modes: default to short chirp (safe)
use_long_chirp <= 1'b0;
end
endcase
// Track chirp count
if (chirp_count < cfg_chirps_per_elev - 1)
chirp_count <= chirp_count + 1;
else
chirp_count <= 6'd0;
end
if (stm32_elevation_toggle) begin
mc_new_elevation <= ~mc_new_elevation;
chirp_count <= 6'd0;
if (elevation_count < ELEVATIONS_PER_AZIMUTH - 1)
elevation_count <= elevation_count + 1;
else
elevation_count <= 6'd0;
end
if (stm32_azimuth_toggle) begin
mc_new_azimuth <= ~mc_new_azimuth;
elevation_count <= 6'd0;
if (azimuth_count < AZIMUTHS_PER_SCAN - 1)
azimuth_count <= azimuth_count + 1;
else begin
azimuth_count <= 6'd0;
scan_done_pulse <= 1'b1;
end
end
end
// ================================================================
// MODE 01: Free-running auto-scan
// Internally generates chirp timing matching the transmitter.
// For 3 km mode (range_mode 00): short chirps only. The long chirp
// blind zone (4500 m) exceeds the 3072 m max range, making long
// chirps useless. State machine skips S_LONG_CHIRP/LISTEN/GUARD.
// For long-range mode (range_mode 01): full dual-chirp sequence.
// NOTE: Auto-scan is primarily for bench testing without STM32.
// ================================================================
2'b01: begin
case (scan_state)
S_IDLE: begin
// Start first chirp immediately
timer <= 18'd0;
chirp_count <= 6'd0;
elevation_count <= 6'd0;
azimuth_count <= 6'd0;
mc_new_chirp <= ~mc_new_chirp; // Toggle to start chirp
// For 3 km mode, skip directly to short chirp
if (range_mode == `RP_RANGE_MODE_3KM) begin
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
`ifdef SIMULATION
$display("[MODE_CTRL] Auto-scan starting, range_mode=%0d", range_mode);
`endif
end
S_LONG_CHIRP: begin
use_long_chirp <= 1'b1;
if (timer < cfg_long_chirp_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_LONG_LISTEN;
end
end
S_LONG_LISTEN: begin
if (timer < cfg_long_listen_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_GUARD;
end
end
S_GUARD: begin
if (timer < cfg_guard_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end
end
S_SHORT_CHIRP: begin
use_long_chirp <= 1'b0;
if (timer < cfg_short_chirp_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_SHORT_LISTEN;
end
end
S_SHORT_LISTEN: begin
if (timer < cfg_short_listen_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_ADVANCE;
end
end
S_ADVANCE: begin
// Advance chirp/elevation/azimuth counters
if (chirp_count < cfg_chirps_per_elev - 1) begin
// Next chirp in current elevation
chirp_count <= chirp_count + 1;
mc_new_chirp <= ~mc_new_chirp;
// For 3 km mode: short chirps only, skip long phases
if (range_mode == `RP_RANGE_MODE_3KM) begin
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
end else begin
chirp_count <= 6'd0;
if (elevation_count < ELEVATIONS_PER_AZIMUTH - 1) begin
// Next elevation
elevation_count <= elevation_count + 1;
mc_new_chirp <= ~mc_new_chirp;
mc_new_elevation <= ~mc_new_elevation;
if (range_mode == `RP_RANGE_MODE_3KM) begin
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
end else begin
elevation_count <= 6'd0;
if (azimuth_count < AZIMUTHS_PER_SCAN - 1) begin
// Next azimuth
azimuth_count <= azimuth_count + 1;
mc_new_chirp <= ~mc_new_chirp;
mc_new_elevation <= ~mc_new_elevation;
mc_new_azimuth <= ~mc_new_azimuth;
if (range_mode == `RP_RANGE_MODE_3KM) begin
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
end else begin
// Full scan complete — restart
azimuth_count <= 6'd0;
scan_done_pulse <= 1'b1;
mc_new_chirp <= ~mc_new_chirp;
mc_new_elevation <= ~mc_new_elevation;
mc_new_azimuth <= ~mc_new_azimuth;
if (range_mode == `RP_RANGE_MODE_3KM) begin
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
`ifdef SIMULATION
$display("[MODE_CTRL] Full scan complete, restarting");
`endif
end
end
end
end
default: scan_state <= S_IDLE;
endcase
end
// ================================================================
// MODE 10: Single-chirp (debug mode)
// Fire one chirp per trigger pulse, no scanning.
// Chirp type depends on range_mode:
// 3 km: short chirp only
// Long-range: long chirp (for testing long-chirp path)
// ================================================================
2'b10: begin
case (scan_state)
S_IDLE: begin
if (trigger_pulse) begin
timer <= 18'd0;
mc_new_chirp <= ~mc_new_chirp;
if (range_mode == `RP_RANGE_MODE_3KM) begin
// 3 km: fire short chirp
scan_state <= S_SHORT_CHIRP;
use_long_chirp <= 1'b0;
end else begin
// Long-range: fire long chirp
scan_state <= S_LONG_CHIRP;
use_long_chirp <= 1'b1;
end
end
end
S_LONG_CHIRP: begin
if (timer < cfg_long_chirp_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_LONG_LISTEN;
end
end
S_LONG_LISTEN: begin
if (timer < cfg_long_listen_cycles - 1)
timer <= timer + 1;
else begin
// Single long chirp done, return to idle
timer <= 18'd0;
scan_state <= S_IDLE;
end
end
S_SHORT_CHIRP: begin
use_long_chirp <= 1'b0;
if (timer < cfg_short_chirp_cycles - 1)
timer <= timer + 1;
else begin
timer <= 18'd0;
scan_state <= S_SHORT_LISTEN;
end
end
S_SHORT_LISTEN: begin
if (timer < cfg_short_listen_cycles - 1)
timer <= timer + 1;
else begin
// Single short chirp done, return to idle
timer <= 18'd0;
scan_state <= S_IDLE;
end
end
default: scan_state <= S_IDLE;
endcase
end
// ================================================================
// MODE 11: Reserved — idle
// ================================================================
2'b11: begin
scan_state <= S_IDLE;
timer <= 18'd0;
end
endcase
end
end
// ============================================================================
// OUTPUT ASSIGNMENTS
// ============================================================================
assign scanning = (scan_state != S_IDLE);
assign scan_complete = scan_done_pulse;
endmodule