Files
retroDE_ps2/sim/tb/platform/tb_tile_ram_cdc.sv
T
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

260 lines
11 KiB
Systemverilog

// 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