Fix CDC reset domain bug (P0), strengthen testbenches with 31 structural assertions

Split cdc_adc_to_processing reset_n into src_reset_n/dst_reset_n so source
and destination clock domains use correctly-synchronized resets. Previously
cdc_chirp_counter's destination-side sync chain (100MHz) was reset by
sys_reset_120m_n (120MHz domain), causing 30 CDC critical warnings.

RTL changes:
- cdc_modules.v: split reset port, source logic uses src_reset_n,
  destination sync chains + output logic use dst_reset_n
- radar_system_top.v: cdc_chirp_counter gets proper per-domain resets
- ddc_400m.v: CDC_FIR_i/q use reset_n_400m (src) and reset_n (dst)
- formal/fv_cdc_adc.v: updated wrapper for new port interface

Build 7 fixes (previously untouched):
- radar_transmitter.v: SPI level-shifter assigns, STM32 GPIO CDC sync
- latency_buffer_2159.v: BRAM read registration
- constraints: ft601 IOB -quiet fix
- tb_latency_buffer.v: updated for BRAM changes

Testbench hardening (tb_cdc_modules.v, +31 new assertions):
- A5-A7: split-domain reset tests (staggered deassertion, independent
  dst reset while src active — catches the P0 bug class)
- A8: port connectivity (no X/Z on outputs)
- B7: cdc_single_bit port connectivity
- C6: cdc_handshake reset recovery + port connectivity

Full regression: 13/13 test suites pass (257 total assertions).
This commit is contained in:
Jason
2026-03-17 19:38:09 +02:00
parent 6fc5a10785
commit fcf3999e39
10 changed files with 2479 additions and 2131 deletions
File diff suppressed because it is too large Load Diff
+286 -16
View File
@@ -36,7 +36,8 @@ module tb_cdc_modules;
//
reg m1_src_clk;
reg m1_dst_clk;
reg m1_reset_n;
reg m1_src_reset_n;
reg m1_dst_reset_n;
reg [7:0] m1_src_data;
reg m1_src_valid;
wire [7:0] m1_dst_data;
@@ -49,13 +50,14 @@ module tb_cdc_modules;
.WIDTH(8),
.STAGES(3)
) uut_m1 (
.src_clk (m1_src_clk),
.dst_clk (m1_dst_clk),
.reset_n (m1_reset_n),
.src_data (m1_src_data),
.src_valid(m1_src_valid),
.dst_data (m1_dst_data),
.dst_valid(m1_dst_valid)
.src_clk (m1_src_clk),
.dst_clk (m1_dst_clk),
.src_reset_n (m1_src_reset_n),
.dst_reset_n (m1_dst_reset_n),
.src_data (m1_src_data),
.src_valid (m1_src_valid),
.dst_data (m1_dst_data),
.dst_valid (m1_dst_valid)
);
//
@@ -116,7 +118,8 @@ module tb_cdc_modules;
$dumpvars(0, tb_cdc_modules);
// Init all clocks and signals
m1_src_clk = 0; m1_dst_clk = 0; m1_reset_n = 0;
m1_src_clk = 0; m1_dst_clk = 0;
m1_src_reset_n = 0; m1_dst_reset_n = 0;
m1_src_data = 0; m1_src_valid = 0;
m2_src_clk = 0; m2_dst_clk = 0; m2_reset_n = 0;
m2_src_signal = 0;
@@ -130,15 +133,15 @@ module tb_cdc_modules;
$display("\n=== Section A: cdc_adc_to_processing (Gray-code CDC) ===");
// A1: Reset behaviour
$display("\n--- A1: Reset Behaviour ---");
m1_reset_n = 0;
$display("\n--- A1: Reset Behaviour (split-domain reset) ---");
m1_src_reset_n = 0; m1_dst_reset_n = 0;
#100; // let both clocks run
check(m1_dst_valid === 1'b0, "M1: dst_valid = 0 during reset");
check(m1_dst_data === 8'd0, "M1: dst_data = 0 during reset");
// Release reset
@(posedge m1_dst_clk);
m1_reset_n = 1;
m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk);
// A2: Single value transfer
@@ -166,9 +169,9 @@ module tb_cdc_modules;
// A3: Multiple sequential values
$display("\n--- A3: Multiple Sequential Values ---");
m1_reset_n = 0;
m1_src_reset_n = 0; m1_dst_reset_n = 0;
#100;
m1_reset_n = 1;
m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk);
begin : a3_block
@@ -239,9 +242,9 @@ module tb_cdc_modules;
// A4: Slow sender (one value every 4 dst_clk cycles)
$display("\n--- A4: Slow Sender ---");
m1_reset_n = 0;
m1_src_reset_n = 0; m1_dst_reset_n = 0;
#100;
m1_reset_n = 1;
m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk);
begin : a4_block
@@ -283,6 +286,176 @@ module tb_cdc_modules;
check(all_match, "M1: Slow-sent values match exactly");
end
// A5: Split-Domain Reset Src resets while dst stays active
$display("\n--- A5: Split-Domain Reset (src resets, dst active) ---");
m1_src_reset_n = 0; m1_dst_reset_n = 0;
m1_src_data = 0; m1_src_valid = 0;
#100;
// Release dst_reset_n first, src stays in reset
m1_dst_reset_n = 1;
begin : a5_dst_idle
integer wait_cycles;
reg saw_valid;
saw_valid = 0;
for (wait_cycles = 0; wait_cycles < 10; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) saw_valid = 1;
end
check(!saw_valid, "M1: dst_valid stays 0 while src is in reset");
end
// Now release src_reset_n
m1_src_reset_n = 1;
@(posedge m1_src_clk);
// Send data and verify transfer works after staggered reset
m1_src_data = 8'h3C;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
begin : a5_wait
integer wait_cycles;
for (wait_cycles = 0; wait_cycles < 20; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) disable a5_wait;
end
end
check(m1_dst_valid === 1'b1, "M1: dst_valid asserts after staggered src reset release");
check(m1_dst_data === 8'h3C, "M1: data 0x3C correct after staggered src reset");
// A6: Split-Domain Reset Dst resets while src stays active
// KEY test: catches the original P0 bug where a single reset from
// the src domain was used to reset dst-domain registers.
$display("\n--- A6: Split-Domain Reset (dst resets, src active) ---");
m1_src_reset_n = 1; m1_dst_reset_n = 1;
m1_src_data = 0; m1_src_valid = 0;
@(posedge m1_src_clk);
// Send data and verify it arrives (baseline)
m1_src_data = 8'hF0;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
begin : a6_baseline
integer wait_cycles;
for (wait_cycles = 0; wait_cycles < 20; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) disable a6_baseline;
end
end
check(m1_dst_data === 8'hF0, "M1: Baseline data 0xF0 received before dst-only reset");
// Assert ONLY dst_reset_n (src keeps running)
m1_dst_reset_n = 0;
begin : a6_check_reset
integer wait_cycles;
reg dst_cleared;
dst_cleared = 0;
for (wait_cycles = 0; wait_cycles < 10; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_data === 8'd0 && m1_dst_valid === 1'b0)
dst_cleared = 1;
end
check(dst_cleared, "M1: dst_data=0 and dst_valid=0 after dst-only reset");
end
// Deassert dst_reset_n
m1_dst_reset_n = 1;
repeat (3) @(posedge m1_dst_clk);
// Send new data from src, verify it arrives correctly
m1_src_data = 8'h55;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
begin : a6_recovery
integer wait_cycles;
for (wait_cycles = 0; wait_cycles < 20; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) disable a6_recovery;
end
end
check(m1_dst_valid === 1'b1, "M1: dst_valid asserts after dst-only reset recovery");
check(m1_dst_data === 8'h55, "M1: data 0x55 correct after dst-only reset recovery");
// A7: Staggered Reset Deassertion
$display("\n--- A7: Staggered Reset Deassertion ---");
m1_src_reset_n = 0; m1_dst_reset_n = 0;
m1_src_data = 0; m1_src_valid = 0;
#100;
// Release src_reset_n first, start sending data immediately
m1_src_reset_n = 1;
@(posedge m1_src_clk);
m1_src_data = 8'hBB;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
// Wait 50ns with dst_reset_n still asserted
#50;
// Release dst_reset_n
m1_dst_reset_n = 1;
// Let sync chain clear through a few dst_clk cycles first
repeat (5) @(posedge m1_dst_clk);
// Src sends another value so dst can capture it fresh
@(posedge m1_src_clk);
m1_src_data = 8'hCC;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
begin : a7_wait
integer wait_cycles;
for (wait_cycles = 0; wait_cycles < 40; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) disable a7_wait;
end
end
check(m1_dst_valid === 1'b1, "M1: dst_valid asserts after staggered deassertion");
// Accept either 0xBB (if pipeline retained) or 0xCC (if fresh capture)
check(m1_dst_data === 8'hBB || m1_dst_data === 8'hCC,
"M1: Data not corrupted after staggered deassertion");
// A8: Port Connectivity Check
$display("\n--- A8: Port Connectivity Check ---");
m1_src_reset_n = 0; m1_dst_reset_n = 0;
m1_src_data = 0; m1_src_valid = 0;
#100;
m1_src_reset_n = 1; m1_dst_reset_n = 1;
repeat (5) @(posedge m1_dst_clk); #1;
// After reset deassertion, outputs should not be X or Z
check(m1_dst_data !== 8'bxxxxxxxx, "M1: dst_data is not X after reset");
check(m1_dst_data !== 8'bzzzzzzzz, "M1: dst_data is not Z after reset");
check(m1_dst_valid !== 1'bx, "M1: dst_valid is not X after reset");
check(m1_dst_valid !== 1'bz, "M1: dst_valid is not Z after reset");
// After a transfer, check again
m1_src_data = 8'h99;
m1_src_valid = 1;
@(posedge m1_src_clk); #1;
m1_src_valid = 0;
begin : a8_wait
integer wait_cycles;
for (wait_cycles = 0; wait_cycles < 20; wait_cycles = wait_cycles + 1) begin
@(posedge m1_dst_clk); #1;
if (m1_dst_valid) disable a8_wait;
end
end
check(m1_dst_data !== 8'bxxxxxxxx, "M1: dst_data is not X after transfer");
check(m1_dst_data !== 8'bzzzzzzzz, "M1: dst_data is not Z after transfer");
check(m1_dst_valid !== 1'bx, "M1: dst_valid is not X after transfer");
check(m1_dst_valid !== 1'bz, "M1: dst_valid is not Z after transfer");
//
// SECTION B: cdc_single_bit tests
//
@@ -394,6 +567,25 @@ module tb_cdc_modules;
m2_reset_n = 1;
m2_src_signal = 0;
// B7: Port Connectivity
$display("\n--- B7: Port Connectivity ---");
m2_reset_n = 0;
m2_src_signal = 0;
#100;
m2_reset_n = 1;
repeat (5) @(posedge m2_dst_clk); #1;
check(m2_dst_signal !== 1'bx, "M2: dst_signal is not X after reset");
check(m2_dst_signal !== 1'bz, "M2: dst_signal is not Z after reset");
// Drive signal high and verify after propagation
m2_src_signal = 1;
repeat (8) @(posedge m2_dst_clk); #1;
check(m2_dst_signal !== 1'bx, "M2: dst_signal is not X after propagation");
check(m2_dst_signal !== 1'bz, "M2: dst_signal is not Z after propagation");
m2_src_signal = 0;
repeat (8) @(posedge m2_dst_clk);
//
// SECTION C: cdc_handshake tests
//
@@ -604,6 +796,84 @@ module tb_cdc_modules;
check(all_match, "M3: Edge-case values (0x0, 0xFFFF, 0x8000, 0x1) correct");
end
// C6: Port Connectivity + Reset Recovery
$display("\n--- C6: Port Connectivity + Reset Recovery ---");
m3_reset_n = 0;
m3_src_valid = 0;
m3_dst_ready = 0;
#200;
m3_reset_n = 1;
repeat (5) @(posedge m3_src_clk); #1;
// Port connectivity: outputs should not be X or Z after reset
check(m3_src_ready !== 1'bx, "M3: src_ready is not X after reset");
check(m3_src_ready !== 1'bz, "M3: src_ready is not Z after reset");
check(m3_dst_valid !== 1'bx, "M3: dst_valid is not X after reset");
check(m3_dst_valid !== 1'bz, "M3: dst_valid is not Z after reset");
check(m3_dst_data !== 32'hxxxxxxxx, "M3: dst_data is not X after reset");
check(m3_dst_data !== 32'hzzzzzzzz, "M3: dst_data is not Z after reset");
// Reset during active transfer: start a transfer, then assert reset mid-flight
m3_dst_ready = 1;
// Wait for src_ready
begin : c6_wait_ready
integer wait_cnt;
for (wait_cnt = 0; wait_cnt < 30; wait_cnt = wait_cnt + 1) begin
@(posedge m3_src_clk); #1;
if (m3_src_ready) disable c6_wait_ready;
end
end
m3_src_data = 32'h12345678;
m3_src_valid = 1;
@(posedge m3_src_clk); #1;
m3_src_valid = 0;
// Wait a few cycles for transfer to be in-flight, then reset
repeat (3) @(posedge m3_dst_clk);
m3_reset_n = 0;
#200;
// Verify outputs are clean during reset
check(m3_dst_valid === 1'b0, "M3: dst_valid = 0 after mid-transfer reset");
// Release reset and verify recovery
m3_reset_n = 1;
@(posedge m3_src_clk);
m3_dst_ready = 1;
// Wait for src_ready to reassert (module recovered)
begin : c6_recovery_wait
integer wait_cnt;
reg recovered;
recovered = 0;
for (wait_cnt = 0; wait_cnt < 50; wait_cnt = wait_cnt + 1) begin
@(posedge m3_src_clk); #1;
if (m3_src_ready) begin
recovered = 1;
disable c6_recovery_wait;
end
end
check(recovered, "M3: src_ready reasserts after mid-transfer reset recovery");
end
// Verify a new transfer works after recovery
m3_src_data = 32'hABCD0000;
m3_src_valid = 1;
@(posedge m3_src_clk); #1;
m3_src_valid = 0;
begin : c6_post_reset_xfer
integer wait_cnt;
for (wait_cnt = 0; wait_cnt < 30; wait_cnt = wait_cnt + 1) begin
@(posedge m3_dst_clk); #1;
if (m3_dst_valid) disable c6_post_reset_xfer;
end
end
check(m3_dst_valid === 1'b1, "M3: dst_valid asserts for post-recovery transfer");
check(m3_dst_data === 32'hABCD0000, "M3: data 0xABCD0000 correct after reset recovery");
//
// Summary
//
+18 -20
View File
@@ -136,11 +136,13 @@ module tb_latency_buffer;
end
$display(" First valid output at input sample #%0d (expected ~%0d)",
first_valid_cycle, LATENCY);
first_valid_cycle, LATENCY + 1);
// After LATENCY samples written, buffer_has_data goes high.
// On the NEXT valid_in, valid_out fires. So first valid is at sample LATENCY.
check(first_valid_cycle == LATENCY,
"First valid output appears at sample LATENCY");
// On the NEXT valid_in, valid_out_reg fires. Then valid_out_pipe
// (the actual output) fires one cycle later due to BRAM read register.
// So first valid is at sample LATENCY + 1.
check(first_valid_cycle == LATENCY + 1,
"First valid output appears at sample LATENCY+1 (BRAM read pipeline)");
//
// TEST GROUP 4: Data integrity (exact delay)
@@ -151,20 +153,10 @@ module tb_latency_buffer;
// Feed samples: value = (i + 100)
// After priming, each valid output should match data_in from LATENCY samples ago.
//
// NOTE: The DUT calculates read_ptr from write_ptr BEFORE write_ptr is
// updated on this cycle. Specifically, the read_ptr is set using the
// current write_ptr value, which points to where the CURRENT sample
// is about to be written. The BRAM read is combinational
// (data_out = bram[read_ptr]).
//
// When buffer_has_data && valid_in:
// read_ptr <= write_ptr - LATENCY (mod 4096)
// But write_ptr hasn't incremented yet this cycle. So read_ptr will
// point to (old_write_ptr - LATENCY). The output appears one cycle later
// because read_ptr is registered, and BRAM read is combinational on read_ptr.
//
// Net effect: output at input cycle K has value of input cycle (K - LATENCY - 1).
// We verify this empirically.
// The DUT calculates read_ptr from write_ptr, with BRAM read output
// registered for Block RAM inference. This adds 1 cycle of read latency
// beyond the LATENCY parameter. The valid_out pipeline stage tracks this.
// The auto-calibration below handles any offset empirically.
begin : data_check_block
reg all_match;
@@ -242,15 +234,17 @@ module tb_latency_buffer;
@(posedge clk); #1;
end
// Now de-assert valid_in no more outputs expected
// Now de-assert valid_in after pipeline drains (1 cycle), no more outputs
valid_in = 0;
data_in = 32'hDEADBEEF;
// Allow 1 cycle for the valid pipeline to drain
@(posedge clk); #1;
valid_output_count = 0;
for (i = 0; i < 20; i = i + 1) begin
@(posedge clk); #1;
if (valid_out) valid_output_count = valid_output_count + 1;
end
check(valid_output_count == 0, "No output when valid_in deasserted");
check(valid_output_count == 0, "No output when valid_in deasserted (after pipeline drain)");
//
// TEST GROUP 6: Intermittent valid_in
@@ -349,6 +343,10 @@ module tb_latency_buffer;
valid_in = 1;
@(posedge clk); #1;
end
// Feed one more cycle to ensure pipeline has flushed
data_in = 32'hFFFF;
valid_in = 1;
@(posedge clk); #1;
// Should be producing outputs now
check(valid_out === 1'b1, "Outputs flowing before mid-op reset");