Files
PLFM_RADAR/9_Firmware/9_2_FPGA/rx_gain_control.v
T
Jason e9705e40b7 feat: 2048-pt FFT upgrade with decimation=4, 512 output bins, 6m spacing
Complete cross-layer upgrade from 1024-pt/64-bin to 2048-pt/512-bin FFT:

FPGA RTL (14+ modules):
- radar_params.vh: FFT_SIZE=2048, RANGE_BINS=512, 9-bit range, 6-bit stream
- fft_engine.v: 2048-pt FFT with XPM BRAM
- chirp_memory_loader_param.v: 2 segments x 2048 (was 4 x 1024)
- matched_filter_multi_segment.v: BRAM inference for overlap_cache, explicit ov_waddr
- mti_canceller.v: BRAM inference for prev_i/q arrays (was fabric FFs)
- doppler_processor.v: 16384-deep memory, 14-bit addressing
- cfar_ca.v: 512 rows, indentation fix
- radar_receiver_final.v: rising-edge detector for frame_complete, 11-bit sample_addr
- range_bin_decimator.v: 512 output bins
- usb_data_interface_ft2232h.v: bulk per-frame with Manhattan magnitude
- radar_mode_controller.v: XOR edge detector for toggle signals
- rx_gain_control.v: updated for new bin count

Python GUI + Protocol (8 files):
- radar_protocol.py: 512-bin bulk frame parser, LSB-first bitmap
- GUI_V65_Tk.py, v7/*.py: updated for 512 bins, 6m range resolution

Golden data + tests:
- All .hex/.csv/.npy golden references regenerated for 2048/512
- fft_twiddle_2048.mem added
- Deleted stale seg2/seg3 chirp mem files
- 9 new bulk frame cross-layer tests, deleted 6 stale per-sample tests
- Deleted stale tb_cross_layer_ft2232h.v and dead contract_parser functions
- Updated validate_mem_files.py for 2048/2-segment config

MCU: RadarSettings.cpp max_distance/map_size 1536->3072

All 4 CI jobs pass: 285 tests, 0 failures, 0 skips
2026-04-16 17:27:55 +05:45

284 lines
12 KiB
Verilog

`timescale 1ns / 1ps
/**
* rx_gain_control.v
*
* Digital gain control with optional per-frame automatic gain control (AGC)
* for the receive path. Placed between DDC output and matched filter input.
*
* Manual mode (agc_enable=0):
* - Uses host_gain_shift directly (backward-compatible, no behavioral change)
* - gain_shift[3] = direction: 0 = left shift (amplify), 1 = right shift (attenuate)
* - gain_shift[2:0] = amount: 0..7 bits
* - Symmetric saturation to ±32767 on overflow
*
* AGC mode (agc_enable=1):
* - Per-frame automatic gain adjustment based on peak/saturation metrics
* - Internal signed gain: -7 (max attenuation) to +7 (max amplification)
* - On frame_boundary:
* * If saturation detected: gain -= agc_attack (fast, immediate)
* * Else if peak < target after holdoff frames: gain += agc_decay (slow)
* * Else: hold current gain
* - host_gain_shift serves as initial gain when AGC first enabled
*
* Status outputs (for readback via status_words):
* - current_gain[3:0]: effective gain_shift encoding (manual or AGC)
* - peak_magnitude[7:0]: per-frame peak |sample| (upper 8 bits of 15-bit value)
* - saturation_count[7:0]: per-frame clipped sample count (capped at 255)
*
* Timing: 1-cycle data latency, valid-in/valid-out pipeline.
*
* Insertion point in radar_receiver_final.v:
* ddc_input_interface → rx_gain_control → matched_filter_multi_segment
*/
module rx_gain_control (
input wire clk,
input wire reset_n,
// Data input (from DDC / ddc_input_interface)
input wire signed [15:0] data_i_in,
input wire signed [15:0] data_q_in,
input wire valid_in,
// Host gain configuration (from USB command opcode 0x16)
// [3]=direction: 0=amplify (left shift), 1=attenuate (right shift)
// [2:0]=shift amount: 0..7 bits. Default 0x00 = pass-through.
// In AGC mode: serves as initial gain on AGC enable transition.
input wire [3:0] gain_shift,
// AGC configuration inputs (from host via USB, opcodes 0x28-0x2C)
input wire agc_enable, // 0x28: 0=manual gain, 1=auto AGC
input wire [7:0] agc_target, // 0x29: target peak magnitude (unsigned, default 200)
input wire [3:0] agc_attack, // 0x2A: attenuation step on clipping (default 1)
input wire [3:0] agc_decay, // 0x2B: amplification step when weak (default 1)
input wire [3:0] agc_holdoff, // 0x2C: frames to wait before gain-up (default 4)
// Frame boundary pulse (1 clk cycle, from edge detector in radar_receiver_final)
input wire frame_boundary,
// Data output (to matched filter)
output reg signed [15:0] data_i_out,
output reg signed [15:0] data_q_out,
output reg valid_out,
// Diagnostics / status readback
output reg [7:0] saturation_count, // Per-frame clipped sample count (capped at 255)
output reg [7:0] peak_magnitude, // Per-frame peak |sample| (upper 8 bits of 15-bit)
output reg [3:0] current_gain // Current effective gain_shift (for status readback)
);
// =========================================================================
// INTERNAL AGC STATE
// =========================================================================
// Signed internal gain: -7 (max attenuation) to +7 (max amplification)
// Stored as 4-bit signed (range -8..+7, clamped to -7..+7)
reg signed [3:0] agc_gain;
// Holdoff counter: counts frames without saturation before allowing gain-up
reg [3:0] holdoff_counter;
// Per-frame accumulators (running, reset on frame_boundary)
reg [7:0] frame_sat_count; // Clipped samples this frame
reg [14:0] frame_peak; // Peak |sample| this frame (15-bit unsigned)
// Previous AGC enable state (for detecting 0→1 transition)
reg agc_enable_prev;
// Combinational helpers for inclusive frame-boundary snapshot
// (used when valid_in and frame_boundary coincide)
reg wire_frame_sat_incr;
reg wire_frame_peak_update;
// =========================================================================
// EFFECTIVE GAIN SELECTION
// =========================================================================
// Convert between signed internal gain and the gain_shift[3:0] encoding.
// gain_shift[3]=0, [2:0]=N → amplify by N bits (internal gain = +N)
// gain_shift[3]=1, [2:0]=N → attenuate by N bits (internal gain = -N)
// Effective gain_shift used for the actual shift operation
wire [3:0] effective_gain;
assign effective_gain = agc_enable ? current_gain : gain_shift;
// Decompose effective gain for shift logic
wire shift_right = effective_gain[3];
wire [2:0] shift_amt = effective_gain[2:0];
// =========================================================================
// COMBINATIONAL SHIFT + SATURATION
// =========================================================================
// Use wider intermediates to detect overflow on left shift.
// 24 bits is enough: 16 + 7 shift = 23 significant bits max.
wire signed [23:0] shifted_i;
wire signed [23:0] shifted_q;
assign shifted_i = shift_right ? (data_i_in >>> shift_amt)
: (data_i_in <<< shift_amt);
assign shifted_q = shift_right ? (data_q_in >>> shift_amt)
: (data_q_in <<< shift_amt);
// Saturation: clamp to signed 16-bit range [-32768, +32767]
wire overflow_i = (shifted_i > 24'sd32767) || (shifted_i < -24'sd32768);
wire overflow_q = (shifted_q > 24'sd32767) || (shifted_q < -24'sd32768);
wire signed [15:0] sat_i = overflow_i ? (shifted_i[23] ? -16'sd32768 : 16'sd32767)
: shifted_i[15:0];
wire signed [15:0] sat_q = overflow_q ? (shifted_q[23] ? -16'sd32768 : 16'sd32767)
: shifted_q[15:0];
// =========================================================================
// PEAK MAGNITUDE TRACKING (combinational)
// =========================================================================
// Absolute value of signed 16-bit: flip sign bit if negative.
// Result is 15-bit unsigned [0, 32767]. (We ignore -32768 → 32767 edge case.)
wire [14:0] abs_i = data_i_in[15] ? (~data_i_in[14:0] + 15'd1) : data_i_in[14:0];
wire [14:0] abs_q = data_q_in[15] ? (~data_q_in[14:0] + 15'd1) : data_q_in[14:0];
wire [14:0] max_iq = (abs_i > abs_q) ? abs_i : abs_q;
// =========================================================================
// SIGNED GAIN ↔ GAIN_SHIFT ENCODING CONVERSION
// =========================================================================
// Convert signed agc_gain to gain_shift[3:0] encoding
function [3:0] signed_to_encoding;
input signed [3:0] g;
begin
if (g >= 0)
signed_to_encoding = {1'b0, g[2:0]}; // amplify
else
signed_to_encoding = {1'b1, (~g[2:0]) + 3'd1}; // attenuate: -g
end
endfunction
// Convert gain_shift[3:0] encoding to signed gain
function signed [3:0] encoding_to_signed;
input [3:0] enc;
begin
if (enc[3] == 1'b0)
encoding_to_signed = {1'b0, enc[2:0]}; // +0..+7
else
encoding_to_signed = -$signed({1'b0, enc[2:0]}); // -1..-7
end
endfunction
// =========================================================================
// CLAMPING HELPER
// =========================================================================
// Clamp a wider signed value to [-7, +7]
function signed [3:0] clamp_gain;
input signed [4:0] val; // 5-bit to handle overflow from add
begin
if (val > 5'sd7)
clamp_gain = 4'sd7;
else if (val < -5'sd7)
clamp_gain = -4'sd7;
else
clamp_gain = val[3:0];
end
endfunction
// =========================================================================
// REGISTERED OUTPUT + AGC STATE MACHINE
// =========================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
// Data path
data_i_out <= 16'sd0;
data_q_out <= 16'sd0;
valid_out <= 1'b0;
// Status outputs
saturation_count <= 8'd0;
peak_magnitude <= 8'd0;
current_gain <= 4'd0;
// AGC internal state
agc_gain <= 4'sd0;
holdoff_counter <= 4'd0;
frame_sat_count <= 8'd0;
frame_peak <= 15'd0;
agc_enable_prev <= 1'b0;
end else begin
// Track AGC enable transitions
agc_enable_prev <= agc_enable;
// Compute inclusive metrics: if valid_in fires this cycle,
// include current sample in the snapshot taken at frame_boundary.
// This avoids losing the last sample when valid_in and
// frame_boundary coincide (NBA last-write-wins would otherwise
// snapshot stale values then reset, dropping the sample entirely).
wire_frame_sat_incr = (valid_in && (overflow_i || overflow_q)
&& (frame_sat_count != 8'hFF));
wire_frame_peak_update = (valid_in && (max_iq > frame_peak));
// ---- Data pipeline (1-cycle latency) ----
valid_out <= valid_in;
if (valid_in) begin
data_i_out <= sat_i;
data_q_out <= sat_q;
// Per-frame saturation counting
if ((overflow_i || overflow_q) && (frame_sat_count != 8'hFF))
frame_sat_count <= frame_sat_count + 8'd1;
// Per-frame peak tracking (pre-gain, measures input signal level)
if (max_iq > frame_peak)
frame_peak <= max_iq;
end
// ---- Frame boundary: AGC update + metric snapshot ----
if (frame_boundary) begin
// Snapshot per-frame metrics INCLUDING current sample if valid_in
saturation_count <= wire_frame_sat_incr
? (frame_sat_count + 8'd1)
: frame_sat_count;
peak_magnitude <= wire_frame_peak_update
? max_iq[14:7]
: frame_peak[14:7];
// Reset per-frame accumulators for next frame
frame_sat_count <= 8'd0;
frame_peak <= 15'd0;
if (agc_enable) begin
// AGC auto-adjustment at frame boundary
// Use inclusive counts/peaks (accounting for simultaneous valid_in)
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
// Clipping detected: reduce gain immediately (attack)
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) -
$signed({1'b0, agc_attack}));
holdoff_counter <= agc_holdoff; // Reset holdoff
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
< agc_target) begin
// Signal too weak: increase gain after holdoff expires
if (holdoff_counter == 4'd0) begin
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) +
$signed({1'b0, agc_decay}));
end else begin
holdoff_counter <= holdoff_counter - 4'd1;
end
end else begin
// Signal in good range, no saturation: hold gain
// Reset holdoff so next weak frame has to wait again
holdoff_counter <= agc_holdoff;
end
end
end
// ---- AGC enable transition: initialize from host gain ----
if (agc_enable && !agc_enable_prev) begin
agc_gain <= encoding_to_signed(gain_shift);
holdoff_counter <= agc_holdoff;
end
// ---- Update current_gain output ----
if (agc_enable)
current_gain <= signed_to_encoding(agc_gain);
else
current_gain <= gain_shift;
end
end
endmodule