PLFM_RADAR/9_Firmware/9_2_FPGA/fpga_self_test.v
Jason 853d2a5fd9 AUDIT-S19/S20/S21: replace fpga_self_test tautologies with real arithmetic
Pre-fix Tests 1/2/4 in fpga_self_test.v gave false PASS even on broken
silicon:

  S-19 Test 1 (CIC): `result_flags[1] <= 1'b1` unconditional, comment
       admitted "always true for simple check".
  S-20 Test 2 (FFT): `(16'sd100+16'sd100 == 16'sd200) && (...)` —
       both predicates compile-time-fold to 1; synth reduces to a
       constant write.
  S-21 Test 4 (ADC): PASS once N samples land, regardless of value.
       A stuck-at-0 / stuck-at-MAX / dead LVDS link still PASSed
       provided adc_valid_in toggled.

Fixes:

  Test 1: drive impulse {5,0,0,0,0,0,0} through registered integrator
          y[n]=y[n-1]+x[n]; require accumulator==5 after step
          response. Real adder + register path; sign-extension
          exercised. Detail = 0xC1 on fail.

  Test 2: real radix-2 butterfly with twiddle multiply across 4 FSM
          states. A=8, B=4 (real), W=2+3j -> WB=(8,12), A'=(16,12),
          B'=(0,-12). Forces synth to instantiate signed multiplier
          (DSP slice) + 17-bit signed add/sub. Detail = 0xF2 on fail.

  Test 4: track min/max across 256-sample capture, require
          (max - min) > ADC_RANGE_THRESHOLD (10 LSB). Catches stuck-at
          faults. Does NOT distinguish AD9484 format mismatches
          (audit's per-mode mean check requires SPI, impossible per
          AUDIT-C13). Detail = 0xAD on fail.

Tests:
- tb_fpga_self_test.v existing Group 1-4 (16 PASS) still pass: varied
  ADC counter input gives range >> 10.
- New Group 5: drive constant 0 -> expect Test 4 FAIL + detail=0xAD.
- New Group 6: drive constant 0x7FFF -> expect Test 4 FAIL + detail=0xAD.
- Regression: 41/41 PASS; fpga_self_test 22/22 (was 16/16).
2026-04-29 23:27:15 +05:45

421 lines
17 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

`timescale 1ns / 1ps
// fpga_self_test.v — Board Bring-Up Smoke Test Controller
//
// Triggered by host opcode 0x30. Exercises each subsystem independently:
// Test 0: BRAM write/read pattern (walking 1s)
// Test 1: CIC impulse response check (known input → expected output)
// Test 2: FFT known-input test (DC input → bin 0 peak)
// Test 3: Arithmetic / saturating-add check
// Test 4: ADC raw data capture (dump N samples to host)
//
// Results reported back via a status register readable by host (opcode 0x31).
// Each test produces a PASS/FAIL bit in result_flags[4:0].
//
// Integration: radar_system_top.v wires host_self_test_trigger (from opcode 0x30)
// to this module's `trigger` input, and reads `result_flags` / `result_valid`
// via opcode 0x31.
//
// Resource cost: ~200 LUTs, 1 BRAM (test pattern), 0 DSP.
module fpga_self_test (
input wire clk,
input wire reset_n,
// Control
input wire trigger, // 1-cycle pulse from host (opcode 0x30)
output reg busy, // High while tests are running
output reg result_valid, // Pulses when all tests complete
output reg [4:0] result_flags, // Per-test PASS(1)/FAIL(0)
output reg [7:0] result_detail, // Diagnostic detail (first failing test ID + info)
// ADC raw capture interface (active during Test 4)
input wire [15:0] adc_data_in, // Raw ADC sample (from ad9484_interface)
input wire adc_valid_in, // ADC sample valid
output reg capture_active, // High during ADC capture window
output reg [15:0] capture_data, // Captured ADC sample for USB readout
output reg capture_valid // Pulse: new captured sample available
);
// ============================================================================
// FSM States
// ============================================================================
localparam [3:0] ST_IDLE = 4'd0,
ST_BRAM_WR = 4'd1,
ST_BRAM_GAP = 4'd2, // 1-cycle gap: let last write complete
ST_BRAM_RD = 4'd3,
ST_BRAM_CHK = 4'd4,
ST_CIC_SETUP = 4'd5,
ST_CIC_CHECK = 4'd6,
ST_FFT_SETUP = 4'd7,
ST_FFT_CHECK = 4'd8,
ST_ARITH = 4'd9,
ST_ADC_CAP = 4'd10,
ST_DONE = 4'd11;
reg [3:0] state;
// ============================================================================
// Test 0: BRAM Write/Read Pattern
// ============================================================================
// Uses a small embedded BRAM (64×16) with walking-1 pattern.
localparam BRAM_DEPTH = 64;
localparam BRAM_AW = 6;
reg [15:0] test_bram [0:BRAM_DEPTH-1];
reg [BRAM_AW-1:0] bram_addr;
reg [15:0] bram_wr_data;
reg [15:0] bram_rd_data;
reg bram_pass;
// Synchronous BRAM write — use walking_one directly to avoid pipeline lag
always @(posedge clk) begin
if (state == ST_BRAM_WR)
test_bram[bram_addr] <= walking_one(bram_addr);
end
// Synchronous BRAM read (1-cycle latency)
always @(posedge clk) begin
bram_rd_data <= test_bram[bram_addr];
end
// Walking-1 pattern: address → (1 << (addr % 16))
function [15:0] walking_one;
input [BRAM_AW-1:0] addr;
begin
walking_one = 16'd1 << (addr[3:0]);
end
endfunction
// ============================================================================
// Test 3: Arithmetic Check
// ============================================================================
// Verify saturating signed add (same logic as mti_canceller.v)
function [15:0] sat_add;
input signed [15:0] a;
input signed [15:0] b;
reg signed [16:0] sum_full;
begin
sum_full = {a[15], a} + {b[15], b};
if (sum_full > 17'sd32767)
sat_add = 16'sd32767;
else if (sum_full < -17'sd32768)
sat_add = -16'sd32768;
else
sat_add = sum_full[15:0];
end
endfunction
reg arith_pass;
// ============================================================================
// Counter / Control
// ============================================================================
reg [9:0] step_cnt; // General-purpose step counter (up to 1024)
reg [9:0] adc_cap_cnt;
localparam ADC_CAP_SAMPLES = 256; // Number of raw ADC samples to capture
// Pipeline register for BRAM read verification (accounts for 1-cycle read latency)
reg [BRAM_AW-1:0] bram_rd_addr_d;
reg bram_rd_valid;
// ============================================================================
// AUDIT-S19/S20/S21: real Test 1/2/4 state (replaces pre-fix tautologies)
// ============================================================================
// Test 1 (CIC integrator impulse response)
reg signed [31:0] cic_accum;
reg signed [15:0] cic_input;
// Test 2 (radix-2 butterfly with complex twiddle)
reg signed [15:0] fft_a_re, fft_b_re, fft_w_re, fft_w_im;
reg signed [31:0] fft_wb_re, fft_wb_im;
reg signed [16:0] fft_aprime_re, fft_aprime_im;
reg signed [16:0] fft_bprime_re, fft_bprime_im;
// Test 4 (ADC activity check — min/max over capture window)
reg signed [15:0] adc_min, adc_max;
localparam signed [15:0] ADC_RANGE_THRESHOLD = 16'sd10;
// ============================================================================
// Main FSM
// ============================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= ST_IDLE;
busy <= 1'b0;
result_valid <= 1'b0;
result_flags <= 5'b00000;
result_detail <= 8'd0;
bram_addr <= 0;
bram_wr_data <= 16'd0;
bram_pass <= 1'b1;
arith_pass <= 1'b1;
step_cnt <= 0;
capture_active <= 1'b0;
capture_data <= 16'd0;
capture_valid <= 1'b0;
adc_cap_cnt <= 0;
bram_rd_addr_d <= 0;
bram_rd_valid <= 1'b0;
// AUDIT-S19/S20/S21
cic_accum <= 32'sd0;
cic_input <= 16'sd0;
fft_a_re <= 16'sd0;
fft_b_re <= 16'sd0;
fft_w_re <= 16'sd0;
fft_w_im <= 16'sd0;
fft_wb_re <= 32'sd0;
fft_wb_im <= 32'sd0;
fft_aprime_re <= 17'sd0;
fft_aprime_im <= 17'sd0;
fft_bprime_re <= 17'sd0;
fft_bprime_im <= 17'sd0;
adc_min <= 16'sd0;
adc_max <= 16'sd0;
end else begin
// Default one-shot signals
result_valid <= 1'b0;
capture_valid <= 1'b0;
bram_rd_valid <= 1'b0;
case (state)
// ============================================================
// IDLE: Wait for trigger
// ============================================================
ST_IDLE: begin
if (trigger) begin
busy <= 1'b1;
result_flags <= 5'b00000;
result_detail <= 8'd0;
bram_pass <= 1'b1;
arith_pass <= 1'b1;
bram_addr <= 0;
step_cnt <= 0;
state <= ST_BRAM_WR;
end
end
// ============================================================
// Test 0: BRAM Write Phase — write walking-1 pattern
// ============================================================
ST_BRAM_WR: begin
if (bram_addr == BRAM_DEPTH - 1) begin
bram_addr <= 0;
state <= ST_BRAM_GAP;
end else begin
bram_addr <= bram_addr + 1;
end
end
// 1-cycle gap: ensures last BRAM write completes before reads begin
ST_BRAM_GAP: begin
bram_addr <= 0;
state <= ST_BRAM_RD;
end
// ============================================================
// Test 0: BRAM Read Phase — issue reads
// ============================================================
ST_BRAM_RD: begin
// BRAM read has 1-cycle latency: issue address, check next cycle
bram_rd_addr_d <= bram_addr;
bram_rd_valid <= 1'b1;
if (bram_addr == BRAM_DEPTH - 1) begin
state <= ST_BRAM_CHK;
end else begin
bram_addr <= bram_addr + 1;
end
end
// ============================================================
// Test 0: BRAM Check — verify last read, finalize
// ============================================================
ST_BRAM_CHK: begin
// Check final read (pipeline delay)
if (bram_rd_data != walking_one(bram_rd_addr_d)) begin
bram_pass <= 1'b0;
result_detail <= {4'd0, bram_rd_addr_d[3:0]};
end
result_flags[0] <= bram_pass;
state <= ST_CIC_SETUP;
step_cnt <= 0;
end
// ============================================================
// Test 1: CIC integrator impulse response (AUDIT-S19 fix)
// ============================================================
// Pre-fix this state set `result_flags[1] <= 1'b1` unconditionally
// ("always true for simple check") so a broken integrator path on
// the silicon would still PASS. Now drives a real impulse {5,0,0,...}
// through y[n] = y[n-1] + x[n] and checks the registered accumulator
// value at the end. Catches stuck-at, broken adder, or sign-extension
// bug in the arithmetic path.
ST_CIC_SETUP: begin
if (step_cnt == 0) begin
cic_accum <= 32'sd0;
cic_input <= 16'sd5; // impulse value
step_cnt <= 1;
end else if (step_cnt < 8) begin
cic_accum <= cic_accum +
{{16{cic_input[15]}}, cic_input}; // sign-extend
cic_input <= 16'sd0; // zero-pad after impulse
step_cnt <= step_cnt + 1;
end else begin
// After impulse + 6 zeros, integrator holds at 5 (step response)
if (cic_accum == 32'sd5) begin
result_flags[1] <= 1'b1;
end else begin
result_flags[1] <= 1'b0;
result_detail <= 8'hC1; // CIC fail marker
end
state <= ST_FFT_SETUP;
step_cnt <= 0;
end
end
// ============================================================
// Test 2: Radix-2 butterfly with twiddle multiply (AUDIT-S20 fix)
// ============================================================
// Pre-fix this evaluated `(16'sd100+16'sd100 == 16'sd200) &&
// (16'sd100-16'sd100 == 16'sd0)` — both predicates compile-time-fold
// to 1'b1, so synth reduces the whole test to `result_flags[2] <= 1'b1`.
// Replaced with a real radix-2 butterfly that exercises signed
// multiplications + adds across multiple FSM states with register
// dataflow (synth must instantiate DSP/multiplier resources).
//
// Inputs: A = 8 (real), B = 4 (real), W = 2 + 3j
// Step 1: WB = W*B (with B_im=0, so only 2 mults)
// WB_re = W_re * B_re = 2 * 4 = 8
// WB_im = W_im * B_re = 3 * 4 = 12
// Step 2: Butterfly:
// A' = A + WB = (8+8, 0+12) = (16, 12)
// B' = A - WB = (8-8, 0-12) = (0, -12)
// Step 3: Compare against golden.
ST_FFT_SETUP: begin
if (step_cnt == 0) begin
fft_a_re <= 16'sd8;
fft_b_re <= 16'sd4;
fft_w_re <= 16'sd2;
fft_w_im <= 16'sd3;
step_cnt <= 1;
end else if (step_cnt == 1) begin
fft_wb_re <= fft_w_re * fft_b_re; // 2*4 = 8
fft_wb_im <= fft_w_im * fft_b_re; // 3*4 = 12
step_cnt <= 2;
end else if (step_cnt == 2) begin
fft_aprime_re <= {fft_a_re[15], fft_a_re} + fft_wb_re[16:0];
fft_aprime_im <= 17'sd0 + fft_wb_im[16:0];
fft_bprime_re <= {fft_a_re[15], fft_a_re} - fft_wb_re[16:0];
fft_bprime_im <= 17'sd0 - fft_wb_im[16:0];
step_cnt <= 3;
end else begin
if (fft_aprime_re == 17'sd16 && fft_aprime_im == 17'sd12 &&
fft_bprime_re == 17'sd0 && fft_bprime_im == -17'sd12) begin
result_flags[2] <= 1'b1;
end else begin
result_flags[2] <= 1'b0;
result_detail <= 8'hF2; // FFT fail marker
end
state <= ST_ARITH;
step_cnt <= 0;
end
end
// ============================================================
// Test 3: Saturating Arithmetic
// ============================================================
ST_ARITH: begin
// Test cases for sat_add:
// 32767 + 1 should saturate to 32767 (not wrap to -32768)
// -32768 + (-1) should saturate to -32768
// 100 + 200 = 300
if (step_cnt == 0) begin
if (sat_add(16'sd32767, 16'sd1) != 16'sd32767)
arith_pass <= 1'b0;
step_cnt <= 1;
end else if (step_cnt == 1) begin
if (sat_add(-16'sd32768, -16'sd1) != -16'sd32768)
arith_pass <= 1'b0;
step_cnt <= 2;
end else if (step_cnt == 2) begin
if (sat_add(16'sd100, 16'sd200) != 16'sd300)
arith_pass <= 1'b0;
step_cnt <= 3;
end else begin
result_flags[3] <= arith_pass;
state <= ST_ADC_CAP;
step_cnt <= 0;
adc_cap_cnt <= 0;
end
end
// ============================================================
// Test 4: ADC activity (min/max range) check (AUDIT-S21 fix)
// ============================================================
// Pre-fix this set `result_flags[4] <= 1'b1` once N samples were
// observed, regardless of value. A stuck-at-0 ADC (broken LVDS link,
// wrong AD9484 mode per AUDIT-C3, dead sample-and-hold) would still
// PASS as long as adc_valid_in toggled. Now tracks min/max across the
// capture window and requires range > ADC_RANGE_THRESHOLD (10 LSB).
// Catches stuck-at faults; does NOT distinguish AD9484 format
// mismatches (audit's per-mode mean check requires AD9484 SPI which
// is impossible on production HW per AUDIT-C13).
ST_ADC_CAP: begin
capture_active <= 1'b1;
if (adc_valid_in) begin
capture_data <= adc_data_in;
capture_valid <= 1'b1;
// Activity tracking: seed min/max on first sample, then update
if (adc_cap_cnt == 0) begin
adc_min <= adc_data_in;
adc_max <= adc_data_in;
end else begin
if ($signed(adc_data_in) < $signed(adc_min)) adc_min <= adc_data_in;
if ($signed(adc_data_in) > $signed(adc_max)) adc_max <= adc_data_in;
end
adc_cap_cnt <= adc_cap_cnt + 1;
if (adc_cap_cnt >= ADC_CAP_SAMPLES - 1) begin
// PASS if observed range exceeds stuck-at threshold
if (($signed(adc_max) - $signed(adc_min)) > ADC_RANGE_THRESHOLD) begin
result_flags[4] <= 1'b1;
end else begin
result_flags[4] <= 1'b0;
result_detail <= 8'hAD; // stuck-at / no-activity marker
end
capture_active <= 1'b0;
state <= ST_DONE;
end
end
// Timeout: if no ADC data after 1000 cycles (10 us @ 100 MHz), FAIL
step_cnt <= step_cnt + 1;
if (step_cnt >= 10'd1000 && adc_cap_cnt == 0) begin
result_flags[4] <= 1'b0;
result_detail <= 8'hAD; // ADC timeout marker
capture_active <= 1'b0;
state <= ST_DONE;
end
end
// ============================================================
// DONE: Report results
// ============================================================
ST_DONE: begin
busy <= 1'b0;
result_valid <= 1'b1;
state <= ST_IDLE;
end
default: state <= ST_IDLE;
endcase
// Pipeline: check BRAM read data vs expected (during ST_BRAM_RD)
if (bram_rd_valid) begin
if (bram_rd_data != walking_one(bram_rd_addr_d)) begin
bram_pass <= 1'b0;
result_detail <= {4'd0, bram_rd_addr_d[3:0]};
end
end
end
end
endmodule