ffba27a10a
FPGA: - rx_gain_control.v rewritten: per-frame peak/saturation tracking, auto-shift AGC with attack/decay/holdoff, signed gain -7 to +7 - New registers 0x28-0x2C (agc_enable/target/attack/decay/holdoff) - status_words[4] carries AGC metrics (gain, peak, sat_count, enable) - DIG_5 GPIO outputs saturation flag for STM32 outer loop - Both USB interfaces (FT601 + FT2232H) updated with AGC status ports Timing fix (WNS +0.001ns -> +0.045ns, 45x improvement): - CIC max_fanout 4->16 on valid pipeline registers - +200ps setup uncertainty on 400MHz domain - ExtraNetDelay_high placement + AggressiveExplore routing GUI: - AGC opcodes + status parsing in radar_protocol.py - AGC control groups in both tkinter and V7 PyQt dashboards - 11 new AGC tests (103/103 GUI tests pass) Cross-layer: - AGC opcodes/defaults/status assertions added (29/29 pass) - contract_parser.py: fixed comment stripping in concat parser All tests green: 25 FPGA + 103 GUI + 29 cross-layer = 157 pass
264 lines
11 KiB
Verilog
264 lines
11 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 Doppler frame_complete)
|
|
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;
|
|
|
|
// =========================================================================
|
|
// 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;
|
|
|
|
// ---- 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 to output registers
|
|
saturation_count <= frame_sat_count;
|
|
peak_magnitude <= frame_peak[14:7]; // Upper 8 bits of 15-bit peak
|
|
|
|
// 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
|
|
if (frame_sat_count > 8'd0) begin
|
|
// Clipping detected: reduce gain immediately (attack)
|
|
agc_gain <= clamp_gain($signed({1'b0, agc_gain}) -
|
|
$signed({1'b0, agc_attack}));
|
|
holdoff_counter <= agc_holdoff; // Reset holdoff
|
|
end else if (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({1'b0, 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
|