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
+6 -5
View File
@@ -12,7 +12,8 @@ module cdc_adc_to_processing #(
)( )(
input wire src_clk, input wire src_clk,
input wire dst_clk, input wire dst_clk,
input wire reset_n, input wire src_reset_n,
input wire dst_reset_n,
input wire [WIDTH-1:0] src_data, input wire [WIDTH-1:0] src_data,
input wire src_valid, input wire src_valid,
output wire [WIDTH-1:0] dst_data, output wire [WIDTH-1:0] dst_data,
@@ -59,7 +60,7 @@ module cdc_adc_to_processing #(
// Gray encoding is registered in src_clk to avoid combinational logic // Gray encoding is registered in src_clk to avoid combinational logic
// before the first synchronizer FF (fixes CDC-10 violations). // before the first synchronizer FF (fixes CDC-10 violations).
always @(posedge src_clk) begin always @(posedge src_clk) begin
if (!reset_n) begin if (!src_reset_n) begin
src_data_reg <= 0; src_data_reg <= 0;
src_data_gray <= 0; src_data_gray <= 0;
src_toggle <= 2'b00; src_toggle <= 2'b00;
@@ -78,7 +79,7 @@ module cdc_adc_to_processing #(
generate generate
for (i = 0; i < STAGES; i = i + 1) begin : data_sync_chain for (i = 0; i < STAGES; i = i + 1) begin : data_sync_chain
always @(posedge dst_clk) begin always @(posedge dst_clk) begin
if (!reset_n) begin if (!dst_reset_n) begin
dst_data_gray[i] <= 0; dst_data_gray[i] <= 0;
end else begin end else begin
if (i == 0) begin if (i == 0) begin
@@ -93,7 +94,7 @@ module cdc_adc_to_processing #(
for (i = 0; i < STAGES; i = i + 1) begin : toggle_sync_chain for (i = 0; i < STAGES; i = i + 1) begin : toggle_sync_chain
always @(posedge dst_clk) begin always @(posedge dst_clk) begin
if (!reset_n) begin if (!dst_reset_n) begin
dst_toggle_sync[i] <= 2'b00; dst_toggle_sync[i] <= 2'b00;
end else begin end else begin
if (i == 0) begin if (i == 0) begin
@@ -108,7 +109,7 @@ module cdc_adc_to_processing #(
// Detect new data synchronous reset // Detect new data synchronous reset
always @(posedge dst_clk) begin always @(posedge dst_clk) begin
if (!reset_n) begin if (!dst_reset_n) begin
dst_data_reg <= 0; dst_data_reg <= 0;
dst_valid_reg <= 0; dst_valid_reg <= 0;
prev_dst_toggle <= 2'b00; prev_dst_toggle <= 2'b00;
@@ -677,8 +677,11 @@ set_property IOB TRUE [get_cells -hierarchical -filter {NAME =~ *oddr_ft601_clk*
set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_data_out_reg*}] set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_data_out_reg*}]
set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_be_reg*}] set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_be_reg*}]
set_property IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_wr_n_reg*}] set_property IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_wr_n_reg*}]
set_property IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_rd_n_reg*}] # ft601_rd_n and ft601_oe_n are constant-1 (USB read not implemented) —
set_property IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_oe_n_reg*}] # Vivado removes the registers via constant propagation. Use -quiet to
# suppress CRITICAL WARNING [Common 17-55] when the cells don't exist.
set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_rd_n_reg*}]
set_property -quiet IOB TRUE [get_cells -hierarchical -filter {NAME =~ *usb_inst/ft601_oe_n_reg*}]
# ============================================================================ # ============================================================================
# TIMING EXCEPTIONS — CIC DECIMATOR # TIMING EXCEPTIONS — CIC DECIMATOR
+4 -2
View File
@@ -547,7 +547,8 @@ cdc_adc_to_processing #(
)CDC_FIR_i( )CDC_FIR_i(
.src_clk(clk_400m), .src_clk(clk_400m),
.dst_clk(clk_100m), .dst_clk(clk_100m),
.reset_n(reset_n), .src_reset_n(reset_n_400m),
.dst_reset_n(reset_n),
.src_data(cic_i_out), .src_data(cic_i_out),
.src_valid(cic_valid_i), .src_valid(cic_valid_i),
.dst_data(fir_d_in_i), .dst_data(fir_d_in_i),
@@ -560,7 +561,8 @@ cdc_adc_to_processing #(
)CDC_FIR_q( )CDC_FIR_q(
.src_clk(clk_400m), .src_clk(clk_400m),
.dst_clk(clk_100m), .dst_clk(clk_100m),
.reset_n(reset_n), .src_reset_n(reset_n_400m),
.dst_reset_n(reset_n),
.src_data(cic_q_out), .src_data(cic_q_out),
.src_valid(cic_valid_q), .src_valid(cic_valid_q),
.dst_data(fir_d_in_q), .dst_data(fir_d_in_q),
+2 -1
View File
@@ -130,7 +130,8 @@ module fv_cdc_adc;
) dut ( ) dut (
.src_clk (src_clk), .src_clk (src_clk),
.dst_clk (dst_clk), .dst_clk (dst_clk),
.reset_n (reset_n), .src_reset_n(reset_n),
.dst_reset_n(reset_n),
.src_data (src_data), .src_data (src_data),
.src_valid(src_valid), .src_valid(src_valid),
.dst_data (dst_data), .dst_data (dst_data),
+22 -3
View File
@@ -101,9 +101,28 @@ always @(posedge clk or negedge reset_n) begin
end end
end end
// ========== OUTPUTS ========== // ========== BRAM READ (synchronous required for Block RAM inference) ==========
assign data_out = bram[read_ptr]; // Xilinx Block RAMs physically register the read output. An async read
assign valid_out = valid_out_reg; // (assign data_out = bram[addr]) forces Vivado to use distributed LUTRAM
// instead, wasting ~704 LUTs. Registering the read adds 1 cycle of latency,
// compensated by the valid pipeline stage below.
reg [DATA_WIDTH-1:0] data_out_reg;
always @(posedge clk) begin
data_out_reg <= bram[read_ptr];
end
// Pipeline valid_out_reg by 1 cycle to align with registered BRAM read
reg valid_out_pipe;
always @(posedge clk or negedge reset_n) begin
if (!reset_n)
valid_out_pipe <= 1'b0;
else
valid_out_pipe <= valid_out_reg;
end
assign data_out = data_out_reg;
assign valid_out = valid_out_pipe;
+9 -2
View File
@@ -255,13 +255,19 @@ cdc_single_bit #(.STAGES(2)) cdc_ft601_txe_status (
// ============================================================================ // ============================================================================
// CDC for chirp_counter: 6-bit multi-bit Gray-code synchronizer // CDC for chirp_counter: 6-bit multi-bit Gray-code synchronizer
// Source domain is clk_120m_dac, so reset must be synchronized to that domain.
// The cdc_adc_to_processing module uses synchronous reset internally, so
// using sys_reset_120m_n (120m-synchronized) is correct for the source side.
// The destination side will sample it synchronously on dst_clk, which at worst
// delays reset deassertion by 1-2 cycles acceptable for CDC reset.
cdc_adc_to_processing #( cdc_adc_to_processing #(
.WIDTH(6), .WIDTH(6),
.STAGES(3) .STAGES(3)
) cdc_chirp_counter ( ) cdc_chirp_counter (
.src_clk(clk_120m_dac_buf), .src_clk(clk_120m_dac_buf),
.dst_clk(clk_100m_buf), .dst_clk(clk_100m_buf),
.reset_n(sys_reset_n), .src_reset_n(sys_reset_120m_n),
.dst_reset_n(sys_reset_n),
.src_data(tx_current_chirp), .src_data(tx_current_chirp),
.src_valid(1'b1), // Always valid counter updates continuously .src_valid(1'b1), // Always valid counter updates continuously
.dst_data(tx_current_chirp_sync), .dst_data(tx_current_chirp_sync),
@@ -308,7 +314,8 @@ radar_transmitter tx_inst (
// System Clocks // System Clocks
.clk_100m(clk_100m_buf), .clk_100m(clk_100m_buf),
.clk_120m_dac(clk_120m_dac_buf), .clk_120m_dac(clk_120m_dac_buf),
.reset_n(sys_reset_120m_n), // Use 120 MHz-synchronized reset .reset_n(sys_reset_120m_n), // 120 MHz-synchronized reset for DAC-domain logic
.reset_100m_n(sys_reset_n), // 100 MHz-synchronized reset for edge detectors/CDC
// DAC Interface // DAC Interface
.dac_data(dac_data), .dac_data(dac_data),
+56 -9
View File
@@ -22,7 +22,8 @@ module radar_transmitter(
// System Clocks // System Clocks
input wire clk_100m, // System clock input wire clk_100m, // System clock
input wire clk_120m_dac, // 120MHz DAC clock input wire clk_120m_dac, // 120MHz DAC clock
input wire reset_n, input wire reset_n, // Reset synchronized to clk_120m_dac
input wire reset_100m_n, // Reset synchronized to clk_100m (for edge detectors/CDC)
// DAC Interface // DAC Interface
output wire [7:0] dac_data, output wire [7:0] dac_data,
@@ -79,11 +80,28 @@ module radar_transmitter(
); );
// ========== SPI LEVEL SHIFTER PASSTHROUGH ==========
// FPGA bridges 3.3V STM32 SPI bus (Bank 15) to 1.8V ADAR1000 SPI bus (Bank 34).
// The FPGA I/O banks handle the actual voltage translation; these assigns
// route the signals through the fabric.
assign stm32_sclk_1v8 = stm32_sclk_3v3;
assign stm32_mosi_1v8 = stm32_mosi_3v3;
assign stm32_miso_3v3 = stm32_miso_1v8;
assign stm32_cs_adar1_1v8 = stm32_cs_adar1_3v3;
assign stm32_cs_adar2_1v8 = stm32_cs_adar2_3v3;
assign stm32_cs_adar3_1v8 = stm32_cs_adar3_3v3;
assign stm32_cs_adar4_1v8 = stm32_cs_adar4_3v3;
// Edge Detection Signals // Edge Detection Signals
wire new_chirp_pulse; wire new_chirp_pulse;
wire new_elevation_pulse; wire new_elevation_pulse;
wire new_azimuth_pulse; wire new_azimuth_pulse;
// CDC: Synchronized versions of async STM32 GPIO inputs to clk_100m
wire stm32_new_chirp_sync;
wire stm32_new_elevation_sync;
wire stm32_new_azimuth_sync;
// CDC: Synchronized versions of signals crossing clk_100m -> clk_120m_dac // CDC: Synchronized versions of signals crossing clk_100m -> clk_120m_dac
wire mixers_enable_120m; // stm32_mixers_enable sync'd to clk_120m_dac wire mixers_enable_120m; // stm32_mixers_enable sync'd to clk_120m_dac
wire new_chirp_pulse_120m; // new_chirp_pulse (toggle CDC) in clk_120m_dac domain wire new_chirp_pulse_120m; // new_chirp_pulse (toggle CDC) in clk_120m_dac domain
@@ -98,8 +116,8 @@ wire chirp_sequence_done;
// would miss it (120/100 MHz ratio). Toggle CDC converts pulse to level toggle, // would miss it (120/100 MHz ratio). Toggle CDC converts pulse to level toggle,
// syncs the toggle, then detects edges on the destination side. // syncs the toggle, then detects edges on the destination side.
reg chirp_toggle_100m; reg chirp_toggle_100m;
always @(posedge clk_100m or negedge reset_n) begin always @(posedge clk_100m or negedge reset_100m_n) begin
if (!reset_n) if (!reset_100m_n)
chirp_toggle_100m <= 1'b0; chirp_toggle_100m <= 1'b0;
else if (new_chirp_pulse) else if (new_chirp_pulse)
chirp_toggle_100m <= ~chirp_toggle_100m; chirp_toggle_100m <= ~chirp_toggle_100m;
@@ -134,25 +152,54 @@ cdc_single_bit #(.STAGES(3)) cdc_mixers_en_120m (
.dst_signal(mixers_enable_120m) .dst_signal(mixers_enable_120m)
); );
// CDC synchronizers: async STM32 GPIO inputs -> clk_100m domain
// These prevent metastability in the edge detectors. Without these,
// the edge detector's first FF can go metastable, and the XOR output
// can glitch, producing false chirp/elevation/azimuth pulses.
cdc_single_bit #(.STAGES(2)) cdc_stm32_chirp (
.src_clk(clk_100m), // Pseudo-source for async GPIO
.dst_clk(clk_100m),
.reset_n(reset_100m_n),
.src_signal(stm32_new_chirp),
.dst_signal(stm32_new_chirp_sync)
);
cdc_single_bit #(.STAGES(2)) cdc_stm32_elevation (
.src_clk(clk_100m),
.dst_clk(clk_100m),
.reset_n(reset_100m_n),
.src_signal(stm32_new_elevation),
.dst_signal(stm32_new_elevation_sync)
);
cdc_single_bit #(.STAGES(2)) cdc_stm32_azimuth (
.src_clk(clk_100m),
.dst_clk(clk_100m),
.reset_n(reset_100m_n),
.src_signal(stm32_new_azimuth),
.dst_signal(stm32_new_azimuth_sync)
);
// Enhanced STM32 Input Edge Detection with Debouncing // Enhanced STM32 Input Edge Detection with Debouncing
// Inputs are now CDC-synchronized (safe from metastability)
edge_detector_enhanced chirp_edge ( edge_detector_enhanced chirp_edge (
.clk(clk_100m), .clk(clk_100m),
.reset_n(reset_n), .reset_n(reset_100m_n),
.signal_in(stm32_new_chirp), .signal_in(stm32_new_chirp_sync),
.rising_falling_edge(new_chirp_pulse) .rising_falling_edge(new_chirp_pulse)
); );
edge_detector_enhanced elevation_edge ( edge_detector_enhanced elevation_edge (
.clk(clk_100m), .clk(clk_100m),
.reset_n(reset_n), .reset_n(reset_100m_n),
.signal_in(stm32_new_elevation), .signal_in(stm32_new_elevation_sync),
.rising_falling_edge(new_elevation_pulse) .rising_falling_edge(new_elevation_pulse)
); );
edge_detector_enhanced azimuth_edge ( edge_detector_enhanced azimuth_edge (
.clk(clk_100m), .clk(clk_100m),
.reset_n(reset_n), .reset_n(reset_100m_n),
.signal_in(stm32_new_azimuth), .signal_in(stm32_new_azimuth_sync),
.rising_falling_edge(new_azimuth_pulse) .rising_falling_edge(new_azimuth_pulse)
); );
File diff suppressed because it is too large Load Diff
+282 -12
View File
@@ -36,7 +36,8 @@ module tb_cdc_modules;
// //
reg m1_src_clk; reg m1_src_clk;
reg m1_dst_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 [7:0] m1_src_data;
reg m1_src_valid; reg m1_src_valid;
wire [7:0] m1_dst_data; wire [7:0] m1_dst_data;
@@ -51,11 +52,12 @@ module tb_cdc_modules;
) uut_m1 ( ) uut_m1 (
.src_clk (m1_src_clk), .src_clk (m1_src_clk),
.dst_clk (m1_dst_clk), .dst_clk (m1_dst_clk),
.reset_n (m1_reset_n), .src_reset_n (m1_src_reset_n),
.dst_reset_n (m1_dst_reset_n),
.src_data (m1_src_data), .src_data (m1_src_data),
.src_valid(m1_src_valid), .src_valid (m1_src_valid),
.dst_data (m1_dst_data), .dst_data (m1_dst_data),
.dst_valid(m1_dst_valid) .dst_valid (m1_dst_valid)
); );
// //
@@ -116,7 +118,8 @@ module tb_cdc_modules;
$dumpvars(0, tb_cdc_modules); $dumpvars(0, tb_cdc_modules);
// Init all clocks and signals // 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; m1_src_data = 0; m1_src_valid = 0;
m2_src_clk = 0; m2_dst_clk = 0; m2_reset_n = 0; m2_src_clk = 0; m2_dst_clk = 0; m2_reset_n = 0;
m2_src_signal = 0; m2_src_signal = 0;
@@ -130,15 +133,15 @@ module tb_cdc_modules;
$display("\n=== Section A: cdc_adc_to_processing (Gray-code CDC) ==="); $display("\n=== Section A: cdc_adc_to_processing (Gray-code CDC) ===");
// A1: Reset behaviour // A1: Reset behaviour
$display("\n--- A1: Reset Behaviour ---"); $display("\n--- A1: Reset Behaviour (split-domain reset) ---");
m1_reset_n = 0; m1_src_reset_n = 0; m1_dst_reset_n = 0;
#100; // let both clocks run #100; // let both clocks run
check(m1_dst_valid === 1'b0, "M1: dst_valid = 0 during reset"); check(m1_dst_valid === 1'b0, "M1: dst_valid = 0 during reset");
check(m1_dst_data === 8'd0, "M1: dst_data = 0 during reset"); check(m1_dst_data === 8'd0, "M1: dst_data = 0 during reset");
// Release reset // Release reset
@(posedge m1_dst_clk); @(posedge m1_dst_clk);
m1_reset_n = 1; m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk); @(posedge m1_src_clk);
// A2: Single value transfer // A2: Single value transfer
@@ -166,9 +169,9 @@ module tb_cdc_modules;
// A3: Multiple sequential values // A3: Multiple sequential values
$display("\n--- 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; #100;
m1_reset_n = 1; m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk); @(posedge m1_src_clk);
begin : a3_block begin : a3_block
@@ -239,9 +242,9 @@ module tb_cdc_modules;
// A4: Slow sender (one value every 4 dst_clk cycles) // A4: Slow sender (one value every 4 dst_clk cycles)
$display("\n--- A4: Slow Sender ---"); $display("\n--- A4: Slow Sender ---");
m1_reset_n = 0; m1_src_reset_n = 0; m1_dst_reset_n = 0;
#100; #100;
m1_reset_n = 1; m1_src_reset_n = 1; m1_dst_reset_n = 1;
@(posedge m1_src_clk); @(posedge m1_src_clk);
begin : a4_block begin : a4_block
@@ -283,6 +286,176 @@ module tb_cdc_modules;
check(all_match, "M1: Slow-sent values match exactly"); check(all_match, "M1: Slow-sent values match exactly");
end 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 // SECTION B: cdc_single_bit tests
// //
@@ -394,6 +567,25 @@ module tb_cdc_modules;
m2_reset_n = 1; m2_reset_n = 1;
m2_src_signal = 0; 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 // 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"); check(all_match, "M3: Edge-case values (0x0, 0xFFFF, 0x8000, 0x1) correct");
end 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 // Summary
// //
+18 -20
View File
@@ -136,11 +136,13 @@ module tb_latency_buffer;
end end
$display(" First valid output at input sample #%0d (expected ~%0d)", $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. // After LATENCY samples written, buffer_has_data goes high.
// On the NEXT valid_in, valid_out fires. So first valid is at sample LATENCY. // On the NEXT valid_in, valid_out_reg fires. Then valid_out_pipe
check(first_valid_cycle == LATENCY, // (the actual output) fires one cycle later due to BRAM read register.
"First valid output appears at sample LATENCY"); // 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) // TEST GROUP 4: Data integrity (exact delay)
@@ -151,20 +153,10 @@ module tb_latency_buffer;
// Feed samples: value = (i + 100) // Feed samples: value = (i + 100)
// After priming, each valid output should match data_in from LATENCY samples ago. // 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 // The DUT calculates read_ptr from write_ptr, with BRAM read output
// updated on this cycle. Specifically, the read_ptr is set using the // registered for Block RAM inference. This adds 1 cycle of read latency
// current write_ptr value, which points to where the CURRENT sample // beyond the LATENCY parameter. The valid_out pipeline stage tracks this.
// is about to be written. The BRAM read is combinational // The auto-calibration below handles any offset empirically.
// (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.
begin : data_check_block begin : data_check_block
reg all_match; reg all_match;
@@ -242,15 +234,17 @@ module tb_latency_buffer;
@(posedge clk); #1; @(posedge clk); #1;
end 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; valid_in = 0;
data_in = 32'hDEADBEEF; data_in = 32'hDEADBEEF;
// Allow 1 cycle for the valid pipeline to drain
@(posedge clk); #1;
valid_output_count = 0; valid_output_count = 0;
for (i = 0; i < 20; i = i + 1) begin for (i = 0; i < 20; i = i + 1) begin
@(posedge clk); #1; @(posedge clk); #1;
if (valid_out) valid_output_count = valid_output_count + 1; if (valid_out) valid_output_count = valid_output_count + 1;
end 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 // TEST GROUP 6: Intermittent valid_in
@@ -349,6 +343,10 @@ module tb_latency_buffer;
valid_in = 1; valid_in = 1;
@(posedge clk); #1; @(posedge clk); #1;
end 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 // Should be producing outputs now
check(valid_out === 1'b1, "Outputs flowing before mid-op reset"); check(valid_out === 1'b1, "Outputs flowing before mid-op reset");