Complete FPGA timing closure across all clock domains after 9 iterative Vivado builds. WNS improved from -48.325ns to +0.018ns (107,886 endpoints). RTL fixes for 400 MHz timing: - NCO: 6-stage pipeline with DSP48E1 phase accumulator, registered LUT index (Fix D splits address decode from ROM read), distributed RAM - CIC: explicit DSP48E1 PCOUT->PCIN cascade for 5 integrator stages, CREG=1 on integrator_0 to eliminate fabric->DSP setup violation - DDC: 400 MHz reset synchronizer (async-assert/sync-deassert), active-high reset register for DSP48E1 RST ports, posedge output stage - FIR: 5-stage binary adder tree pipeline (7-cycle latency) - FFT: 5-cycle butterfly pipeline with registered twiddle index, XPM_MEMORY_TDPRAM for data storage - XDC: CDC false paths, async reset false paths, CIC comb multicycle paths Final Build 9 timing (all MET): adc_dco_p (400 MHz): WNS = +0.278ns clk_100m (100 MHz): WNS = +0.018ns clk_120m_dac (120 MHz): WNS = +0.992ns ft601_clk_in (100 MHz): WNS = +5.229ns Cross-domain (adc_dco_p->clk_100m): WNS = +7.105ns
342 lines
15 KiB
Verilog
342 lines
15 KiB
Verilog
`timescale 1ns / 1ps
|
|
|
|
module tb_nco_400m;
|
|
|
|
// ── Parameters ─────────────────────────────────────────────
|
|
localparam CLK_PERIOD = 2.5; // 400 MHz
|
|
|
|
// Frequency tuning words: f_out = FTW * f_clk / 2^32
|
|
localparam [31:0] FTW_120MHZ = 32'h4CCCCCCD; // 120 MHz
|
|
localparam [31:0] FTW_10MHZ = 32'h06666666; // 10 MHz
|
|
localparam [31:0] FTW_1MHZ = 32'h00A3D70A; // 1 MHz
|
|
|
|
// ── Signals ────────────────────────────────────────────────
|
|
reg clk_400m;
|
|
reg reset_n;
|
|
reg [31:0] frequency_tuning_word;
|
|
reg phase_valid;
|
|
reg [15:0] phase_offset;
|
|
wire signed [15:0] sin_out;
|
|
wire signed [15:0] cos_out;
|
|
wire dds_ready;
|
|
|
|
// ── Test variables (all at module scope for Icarus) ────────
|
|
integer pass_count;
|
|
integer fail_count;
|
|
integer test_num;
|
|
integer csv_file;
|
|
integer sample_count;
|
|
|
|
reg signed [15:0] sin_max, sin_min, cos_max, cos_min;
|
|
reg signed [15:0] sin_no_offset_3;
|
|
reg signed [15:0] sin_offset_3;
|
|
reg signed [15:0] sin_before_gate;
|
|
reg [31:0] mag_sq;
|
|
reg [31:0] mag_sq_min, mag_sq_max;
|
|
|
|
// ── Clock ──────────────────────────────────────────────────
|
|
always #(CLK_PERIOD/2) clk_400m = ~clk_400m;
|
|
|
|
// ── DUT ────────────────────────────────────────────────────
|
|
nco_400m_enhanced uut (
|
|
.clk_400m (clk_400m),
|
|
.reset_n (reset_n),
|
|
.frequency_tuning_word(frequency_tuning_word),
|
|
.phase_valid (phase_valid),
|
|
.phase_offset (phase_offset),
|
|
.sin_out (sin_out),
|
|
.cos_out (cos_out),
|
|
.dds_ready (dds_ready)
|
|
);
|
|
|
|
// ── Check task ─────────────────────────────────────────────
|
|
task check;
|
|
input cond;
|
|
input [511:0] label;
|
|
begin
|
|
test_num = test_num + 1;
|
|
if (cond) begin
|
|
$display("[PASS] Test %0d: %0s", test_num, label);
|
|
pass_count = pass_count + 1;
|
|
end else begin
|
|
$display("[FAIL] Test %0d: %0s", test_num, label);
|
|
fail_count = fail_count + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// ── Stimulus ───────────────────────────────────────────────
|
|
initial begin
|
|
$dumpfile("tb_nco_400m.vcd");
|
|
$dumpvars(0, tb_nco_400m);
|
|
|
|
// Init
|
|
clk_400m = 0;
|
|
reset_n = 0;
|
|
frequency_tuning_word = 32'd0;
|
|
phase_valid = 0;
|
|
phase_offset = 16'd0;
|
|
pass_count = 0;
|
|
fail_count = 0;
|
|
test_num = 0;
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 1: Reset behaviour
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 1: Reset Behaviour ---");
|
|
repeat (4) @(posedge clk_400m);
|
|
#1;
|
|
check(sin_out === 16'h0000, "sin_out = 0 during reset");
|
|
check(cos_out === 16'h7FFF, "cos_out = 0x7FFF during reset");
|
|
check(dds_ready === 1'b0, "dds_ready = 0 during reset");
|
|
|
|
// Release reset
|
|
reset_n = 1;
|
|
@(posedge clk_400m); #1;
|
|
check(dds_ready === 1'b0, "dds_ready stays 0 with phase_valid=0");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 2: Basic operation at 1 MHz
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 2: 1 MHz NCO Operation ---");
|
|
frequency_tuning_word = FTW_1MHZ;
|
|
phase_valid = 1;
|
|
|
|
sin_max = -16'sh7FFF;
|
|
sin_min = 16'sh7FFF;
|
|
cos_max = -16'sh7FFF;
|
|
cos_min = 16'sh7FFF;
|
|
|
|
csv_file = $fopen("nco_1mhz_output.csv", "w");
|
|
$fwrite(csv_file, "sample,sin_out,cos_out,dds_ready\n");
|
|
|
|
for (sample_count = 0; sample_count < 500; sample_count = sample_count + 1) begin
|
|
@(posedge clk_400m); #1;
|
|
$fwrite(csv_file, "%0d,%0d,%0d,%0d\n",
|
|
sample_count, sin_out, cos_out, dds_ready);
|
|
if (dds_ready) begin
|
|
if (sin_out > sin_max) sin_max = sin_out;
|
|
if (sin_out < sin_min) sin_min = sin_out;
|
|
if (cos_out > cos_max) cos_max = cos_out;
|
|
if (cos_out < cos_min) cos_min = cos_out;
|
|
end
|
|
end
|
|
$fclose(csv_file);
|
|
|
|
$display(" 1 MHz: sin range [%0d, %0d], cos range [%0d, %0d]",
|
|
sin_min, sin_max, cos_min, cos_max);
|
|
|
|
check(sin_max > 16'sh1000, "sin has positive amplitude > 0x1000");
|
|
check(sin_min < -16'sh1000, "sin has negative amplitude");
|
|
check(cos_max > 16'sh1000, "cos has positive amplitude > 0x1000");
|
|
check(cos_min < -16'sh1000, "cos has negative amplitude");
|
|
check(dds_ready === 1'b1, "dds_ready asserted during operation");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 3: 120 MHz IF (primary operating frequency)
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 3: 120 MHz NCO Operation ---");
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_120MHZ;
|
|
phase_valid = 1;
|
|
|
|
sin_max = -16'sh7FFF;
|
|
sin_min = 16'sh7FFF;
|
|
cos_max = -16'sh7FFF;
|
|
cos_min = 16'sh7FFF;
|
|
|
|
csv_file = $fopen("nco_120mhz_output.csv", "w");
|
|
$fwrite(csv_file, "sample,sin_out,cos_out,dds_ready\n");
|
|
|
|
for (sample_count = 0; sample_count < 100; sample_count = sample_count + 1) begin
|
|
@(posedge clk_400m); #1;
|
|
$fwrite(csv_file, "%0d,%0d,%0d,%0d\n",
|
|
sample_count, sin_out, cos_out, dds_ready);
|
|
if (dds_ready) begin
|
|
if (sin_out > sin_max) sin_max = sin_out;
|
|
if (sin_out < sin_min) sin_min = sin_out;
|
|
if (cos_out > cos_max) cos_max = cos_out;
|
|
if (cos_out < cos_min) cos_min = cos_out;
|
|
end
|
|
end
|
|
$fclose(csv_file);
|
|
|
|
$display(" 120 MHz: sin range [%0d, %0d], cos range [%0d, %0d]",
|
|
sin_min, sin_max, cos_min, cos_max);
|
|
|
|
check(sin_max > 16'sh1000, "120MHz sin positive swing");
|
|
check(sin_min < -16'sh1000, "120MHz sin negative swing");
|
|
check(cos_max > 16'sh1000, "120MHz cos positive swing");
|
|
check(cos_min < -16'sh1000, "120MHz cos negative swing");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 4: Phase offset
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 4: Phase Offset ---");
|
|
// Use 10 MHz so phase accumulates fast enough for offset to matter
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_10MHZ;
|
|
phase_offset = 16'h0000;
|
|
phase_valid = 1;
|
|
|
|
// Let NCO run long enough for phase to reach a non-trivial region
|
|
repeat (20) @(posedge clk_400m);
|
|
#1; sin_no_offset_3 = sin_out;
|
|
|
|
// Reset and apply 90-degree phase offset
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_10MHZ;
|
|
phase_offset = 16'h4000; // 90 degrees
|
|
phase_valid = 1;
|
|
|
|
repeat (20) @(posedge clk_400m);
|
|
#1; sin_offset_3 = sin_out;
|
|
|
|
$display(" sin(no_offset, t=20) = %0d, sin(+90deg, t=20) = %0d",
|
|
sin_no_offset_3, sin_offset_3);
|
|
check(sin_no_offset_3 !== sin_offset_3,
|
|
"Phase offset changes sin output");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 5: Dynamic frequency change
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 5: Dynamic Frequency Change ---");
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
phase_offset = 16'h0000;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_1MHZ;
|
|
phase_valid = 1;
|
|
repeat (50) @(posedge clk_400m);
|
|
|
|
// Switch to 10 MHz mid-stream
|
|
frequency_tuning_word = FTW_10MHZ;
|
|
|
|
csv_file = $fopen("nco_freq_switch.csv", "w");
|
|
$fwrite(csv_file, "sample,sin_out,cos_out\n");
|
|
|
|
for (sample_count = 0; sample_count < 200; sample_count = sample_count + 1) begin
|
|
@(posedge clk_400m); #1;
|
|
$fwrite(csv_file, "%0d,%0d,%0d\n",
|
|
sample_count, sin_out, cos_out);
|
|
end
|
|
$fclose(csv_file);
|
|
check(1'b1, "Frequency switch completed without error");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 6: phase_valid gating
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 6: phase_valid Gating ---");
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
phase_offset = 16'h0000;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_10MHZ;
|
|
phase_valid = 1;
|
|
|
|
repeat (10) @(posedge clk_400m);
|
|
#1;
|
|
sin_before_gate = sin_out;
|
|
|
|
// Deassert phase_valid — with 6-stage pipeline, dds_ready has 7-cycle latency
|
|
phase_valid = 0;
|
|
repeat (8) @(posedge clk_400m); #1;
|
|
check(dds_ready === 1'b0, "dds_ready deasserts when phase_valid=0");
|
|
|
|
repeat (10) @(posedge clk_400m);
|
|
|
|
// Re-enable — wait for pipeline to refill (7 cycles)
|
|
phase_valid = 1;
|
|
repeat (8) @(posedge clk_400m); #1;
|
|
check(dds_ready === 1'b1, "dds_ready re-asserts when phase_valid=1");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// TEST GROUP 7: Quadrature orthogonality (sin^2+cos^2)
|
|
// ════════════════════════════════════════════════════════
|
|
$display("\n--- Test Group 7: Quadrature Orthogonality ---");
|
|
reset_n = 0;
|
|
phase_valid = 0;
|
|
phase_offset = 16'h0000;
|
|
repeat (4) @(posedge clk_400m);
|
|
reset_n = 1;
|
|
@(posedge clk_400m);
|
|
|
|
frequency_tuning_word = FTW_10MHZ;
|
|
phase_valid = 1;
|
|
|
|
// Skip pipeline warmup (6-stage pipeline + 1 for dds_ready)
|
|
repeat (7) @(posedge clk_400m);
|
|
|
|
mag_sq_min = 32'hFFFFFFFF;
|
|
mag_sq_max = 32'h00000000;
|
|
|
|
csv_file = $fopen("nco_quadrature.csv", "w");
|
|
$fwrite(csv_file, "sample,sin,cos,mag_sq\n");
|
|
|
|
for (sample_count = 0; sample_count < 40; sample_count = sample_count + 1) begin
|
|
@(posedge clk_400m); #1;
|
|
if (dds_ready) begin
|
|
mag_sq = (sin_out * sin_out) + (cos_out * cos_out);
|
|
if (mag_sq < mag_sq_min) mag_sq_min = mag_sq;
|
|
if (mag_sq > mag_sq_max) mag_sq_max = mag_sq;
|
|
$fwrite(csv_file, "%0d,%0d,%0d,%0d\n",
|
|
sample_count, sin_out, cos_out, mag_sq);
|
|
end
|
|
end
|
|
$fclose(csv_file);
|
|
|
|
$display(" |sin|^2+|cos|^2: min=%0d, max=%0d, ratio=%.2f",
|
|
mag_sq_min, mag_sq_max,
|
|
1.0 * mag_sq_max / (mag_sq_min > 0 ? mag_sq_min : 1));
|
|
// With corrected quarter-wave sine LUT, sin^2+cos^2 should be
|
|
// nearly constant (ratio ~1.02x). Using 2x threshold to avoid
|
|
// 32-bit overflow in the multiply (min*5 overflowed before).
|
|
|
|
check(mag_sq_max > 0, "Magnitude squared is non-zero");
|
|
check(mag_sq_min > 0, "Magnitude squared minimum > 0");
|
|
// Strict check: with correct LUT, variance should be < 1.1x
|
|
// Use division to avoid 32-bit overflow: max/min < 2
|
|
check(mag_sq_max < (mag_sq_min * 2), "Quadrature magnitude variance < 2x (near-ideal)");
|
|
|
|
// ════════════════════════════════════════════════════════
|
|
// Summary
|
|
// ════════════════════════════════════════════════════════
|
|
$display("");
|
|
$display("========================================");
|
|
$display(" NCO 400M TESTBENCH RESULTS");
|
|
$display(" PASSED: %0d / %0d", pass_count, test_num);
|
|
$display(" FAILED: %0d / %0d", fail_count, test_num);
|
|
if (fail_count == 0)
|
|
$display(" ** ALL TESTS PASSED **");
|
|
else
|
|
$display(" ** SOME TESTS FAILED **");
|
|
$display("========================================");
|
|
$display("");
|
|
|
|
#100;
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|