// retroDE_ps2 — tb_tile_ram_cdc (Ch229) // ============================================================================ // Focused unit-test TB for the design-domain side of the Ch229 tile-RAM // CDC. Drives synthetic bridge-side signals at one clock and verifies the // shadow RAM in the design clock domain receives the write correctly. // // Uses two distinct clocks at different frequencies (100 MHz bclk, // 33 MHz dclk) to exercise a real CDC scenario without taking forever. // Bridge writes are spaced ≥ 6 dclk cycles apart (well above the 2-FF // synchronizer's settling time) per the CDC contract documented in the // `tile_ram_cdc.sv` header. // // Verifies: // 1. Reset state: shadow RAM reads return 0 everywhere. // 2. Single write at base (index 0) → propagates to shadow read. // 3. Single write at mid (index 0x200) → propagates. // 4. Single write at end (index 0x3FF) → propagates. // 5. Multiple writes to the same index → final value wins. // 6. Multiple writes to distinct indices → each lands in its slot. // 7. Read of unwritten indices stays 0 across all the above. // 8. Toggle-based event reception: writes happen on toggle EDGES, // not on toggle value; the receiver doesn't double-count a static // toggle value. // ============================================================================ `timescale 1ns/1ps module tb_tile_ram_cdc; // Two distinct clocks: 100 MHz bridge, 33 MHz design (3x ratio). logic bclk; logic dclk; initial bclk = 1'b0; initial dclk = 1'b0; always #5 bclk = ~bclk; // 100 MHz always #15 dclk = ~dclk; // 33 MHz logic breset_n; logic dreset_n; // Bridge-side write ports. logic bclk_wr_toggle; logic [9:0] bclk_wr_index; logic [31:0] bclk_wr_data; // Design-side read ports. logic [9:0] dclk_rd_index; wire [31:0] dclk_rd_data; // Ch230 — diagnostic counter output. wire [15:0] too_close_count; tile_ram_cdc u_dut ( .bclk (bclk), .breset_n (breset_n), .bclk_wr_toggle (bclk_wr_toggle), .bclk_wr_index (bclk_wr_index), .bclk_wr_data (bclk_wr_data), .dclk (dclk), .dreset_n (dreset_n), .dclk_rd_index (dclk_rd_index), .dclk_rd_data (dclk_rd_data), .tile_wr_too_close_count(too_close_count) ); int errors; task automatic check_eq(input string label, input logic [31:0] got, input logic [31:0] expected); if (got !== expected) begin $error("[%s] got 0x%08x expected 0x%08x", label, got, expected); errors = errors + 1; end endtask // Read shadow_mem[idx] via the read port. Lookup is combinational, // so just drive the index and sample one dclk cycle later (to // mirror the read-pipeline timing the overlay uses). task automatic shadow_read(input logic [9:0] idx, output logic [31:0] data); dclk_rd_index = idx; @(posedge dclk); data = dclk_rd_data; endtask // Bridge-side write: latch index + data, then toggle. Hold for // several dclk cycles to give the 2-FF synchronizer + edge // detector time to fire and the shadow write to land. task automatic bridge_write(input logic [9:0] idx, input logic [31:0] data); @(posedge bclk); bclk_wr_index <= idx; bclk_wr_data <= data; bclk_wr_toggle <= ~bclk_wr_toggle; // Wait long enough for the dclk side to see + sample the // toggle edge: 2-FF sync (2 dclk) + 1 extra cycle for the // edge detector + 1 for the shadow write. Round up to 6 // dclk cycles for slack. repeat (6) @(posedge dclk); endtask logic [31:0] rd; logic [15:0] count_before_burst; initial begin errors = 0; breset_n = 1'b0; dreset_n = 1'b0; bclk_wr_toggle = 1'b0; bclk_wr_index = 10'd0; bclk_wr_data = 32'd0; dclk_rd_index = 10'd0; repeat (4) @(posedge bclk); breset_n = 1'b1; @(posedge dclk); dreset_n = 1'b1; repeat (4) @(posedge dclk); // ---------------------------------------------------------- // 1. Reset state: every shadow read returns 0. // ---------------------------------------------------------- shadow_read(10'h000, rd); check_eq("rst@0x000", rd, 32'd0); shadow_read(10'h001, rd); check_eq("rst@0x001", rd, 32'd0); shadow_read(10'h200, rd); check_eq("rst@0x200", rd, 32'd0); shadow_read(10'h3FF, rd); check_eq("rst@0x3FF", rd, 32'd0); // ---------------------------------------------------------- // 2. Single write at base. // ---------------------------------------------------------- bridge_write(10'h000, 32'hDEADBEEF); shadow_read(10'h000, rd); check_eq("wr@0x000", rd, 32'hDEADBEEF); shadow_read(10'h001, rd); check_eq("nb@0x001", rd, 32'd0); shadow_read(10'h3FF, rd); check_eq("nb@0x3FF", rd, 32'd0); // ---------------------------------------------------------- // 3. Single write at mid. // ---------------------------------------------------------- bridge_write(10'h200, 32'h12345678); shadow_read(10'h200, rd); check_eq("wr@0x200", rd, 32'h12345678); // Previous write still there. shadow_read(10'h000, rd); check_eq("wr@0x000_persist", rd, 32'hDEADBEEF); // ---------------------------------------------------------- // 4. Single write at end. // ---------------------------------------------------------- bridge_write(10'h3FF, 32'hCAFEF00D); shadow_read(10'h3FF, rd); check_eq("wr@0x3FF", rd, 32'hCAFEF00D); // Previous writes still there. shadow_read(10'h000, rd); check_eq("wr@0x000_persist2", rd, 32'hDEADBEEF); shadow_read(10'h200, rd); check_eq("wr@0x200_persist", rd, 32'h12345678); // ---------------------------------------------------------- // 5. Multiple writes to the same index → last wins. // ---------------------------------------------------------- bridge_write(10'h100, 32'h11111111); bridge_write(10'h100, 32'h22222222); bridge_write(10'h100, 32'h33333333); shadow_read(10'h100, rd); check_eq("wr@0x100_last_wins", rd, 32'h33333333); // ---------------------------------------------------------- // 6. Many distinct writes (eight different slots). // ---------------------------------------------------------- bridge_write(10'h010, 32'hAAAA0010); bridge_write(10'h020, 32'hAAAA0020); bridge_write(10'h040, 32'hAAAA0040); bridge_write(10'h080, 32'hAAAA0080); bridge_write(10'h150, 32'hAAAA0150); bridge_write(10'h153, 32'hAAAA0153); bridge_write(10'h333, 32'hAAAA0333); bridge_write(10'h3FE, 32'hAAAA03FE); shadow_read(10'h010, rd); check_eq("wr@0x010", rd, 32'hAAAA0010); shadow_read(10'h020, rd); check_eq("wr@0x020", rd, 32'hAAAA0020); shadow_read(10'h040, rd); check_eq("wr@0x040", rd, 32'hAAAA0040); shadow_read(10'h080, rd); check_eq("wr@0x080", rd, 32'hAAAA0080); shadow_read(10'h150, rd); check_eq("wr@0x150", rd, 32'hAAAA0150); shadow_read(10'h153, rd); check_eq("wr@0x153", rd, 32'hAAAA0153); shadow_read(10'h333, rd); check_eq("wr@0x333", rd, 32'hAAAA0333); shadow_read(10'h3FE, rd); check_eq("wr@0x3FE", rd, 32'hAAAA03FE); // ---------------------------------------------------------- // 7. Unwritten slots are still 0 amid all the writes above. // ---------------------------------------------------------- shadow_read(10'h007, rd); check_eq("nb@0x007", rd, 32'd0); shadow_read(10'h0FF, rd); check_eq("nb@0x0FF", rd, 32'd0); shadow_read(10'h2FF, rd); check_eq("nb@0x2FF", rd, 32'd0); // ---------------------------------------------------------- // 8. Toggle is event-based — a static toggle value does not // re-trigger writes. Drive bclk_wr_index to a fresh value // but leave the toggle unchanged → shadow MUST NOT update. // ---------------------------------------------------------- @(posedge bclk); bclk_wr_index <= 10'h000; bclk_wr_data <= 32'hBEEFCAFE; // Do NOT toggle. Wait several cycles. repeat (10) @(posedge dclk); // Slot 0x000 still holds DEADBEEF. shadow_read(10'h000, rd); check_eq("static_toggle_no_write", rd, 32'hDEADBEEF); // ---------------------------------------------------------- // 9. Ch230 — too-close-write diagnostic counter. Up to this // point every bridge_write() spaced its toggle at least // 6 dclk apart, well above MIN_DCLK_GAP=3, so the // counter must still be 0. // ---------------------------------------------------------- check_eq("too_close_count@safe_writes", {16'd0, too_close_count}, 32'd0); // Now deliberately violate the rate: flip toggle on // consecutive bclk edges (1 bclk apart) ten times. Each // resulting wr_pulse on dclk lands within < 3 dclk of the // previous one (because 1 bclk @ 100 MHz = 10 ns vs 1 dclk // @ 33 MHz ≈ 30 ns), incrementing the saturating counter. // The number of counted "too close" events is timing- // dependent (some bclk-domain flips will merge in the // synchronizer), so check the count is non-zero rather than // a specific value. for (int i = 0; i < 10; i++) begin @(posedge bclk); bclk_wr_index <= 10'h300 + i[9:0]; bclk_wr_data <= 32'h0BADCAFE + 32'(i); bclk_wr_toggle <= ~bclk_wr_toggle; end repeat (10) @(posedge dclk); if (too_close_count == 16'd0) begin $error("[too_close_count_nonzero] counter stayed at 0 after fast writes"); errors = errors + 1; end // Saturation behavior: counter must not wrap. Push it past // many more fast-write events and confirm it stops at // 0xFFFF eventually (or stays at its current value if // saturated). Easier to assert: monotonic non-decreasing // across additional bursts. count_before_burst = too_close_count; for (int j = 0; j < 20; j++) begin @(posedge bclk); bclk_wr_toggle <= ~bclk_wr_toggle; end repeat (10) @(posedge dclk); if (too_close_count < count_before_burst) begin $error("[too_close_count_monotonic] counter decreased: was 0x%04x, now 0x%04x", count_before_burst, too_close_count); errors = errors + 1; end // ---------------------------------------------------------- // Done. // ---------------------------------------------------------- $display("[tb_tile_ram_cdc] errors=%0d", errors); if (errors == 0) $display("[tb_tile_ram_cdc] PASS"); else $display("[tb_tile_ram_cdc] FAIL"); $finish; end initial begin #5_000_000; $error("[tb_tile_ram_cdc] TIMEOUT"); $finish; end endmodule : tb_tile_ram_cdc