ec82764bef
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>
260 lines
11 KiB
Systemverilog
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
|