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>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// retroDE_ps2 — tb_osd_platform_cell_adapter (Ch245)
|
||||
// ============================================================================
|
||||
// Focused unit-test for the Ch245 char-BRAM read adapter that translates
|
||||
// platform osd_overlay's 11-bit cell address into our existing
|
||||
// tile_ram_cdc 10-bit 32-bit-word index + low/high cell select.
|
||||
//
|
||||
// The adapter logic lives inline in
|
||||
// `rtl/top/de25_nano_psmct32_raster_demo_top.sv`. This TB replicates
|
||||
// it exactly inside the TB body and exercises it against a small
|
||||
// shadow memory pre-populated with known cells.
|
||||
//
|
||||
// Pack convention (`set_cell` helper that lived in the retired
|
||||
// tb_osd_overlay_stub.sv used this exact mapping; the bridge tile-
|
||||
// write decode still produces the same packed layout, so retrodesd
|
||||
// continues to write into 32-bit words that we unpack here):
|
||||
// word_idx = row * 32 + (col >> 1)
|
||||
// col[0]=0 → cell lives in word[15:0] (low half)
|
||||
// col[0]=1 → cell lives in word[31:16] (high half)
|
||||
//
|
||||
// Adapter behavior (matches the inline logic at de25_nano_*.sv):
|
||||
// shadow_word_idx = char_rd_addr[10:1]
|
||||
// cell_half_sel = char_rd_addr[0]
|
||||
// cell_data_w = cell_half_sel ? word[31:16] : word[15:0]
|
||||
// char_rd_data = registered(cell_data_w) -- 1-cycle latency
|
||||
//
|
||||
// iverilog 12 in this codebase doesn't support task-local declarations,
|
||||
// so the TB body inlines the writes and reads. Each cell write
|
||||
// directly mutates `shadow_mem`; each read drives `char_rd_addr` and
|
||||
// then samples `char_rd_data` after a clock.
|
||||
// ============================================================================
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module tb_osd_platform_cell_adapter;
|
||||
|
||||
logic clk = 1'b0;
|
||||
always #5 clk = ~clk; // 100 MHz
|
||||
|
||||
// Mock 32-bit shadow (the design-domain side of tile_ram_cdc).
|
||||
logic [31:0] shadow_mem [0:1023];
|
||||
|
||||
// Adapter inputs (from osd_overlay.char_rd_addr).
|
||||
logic [10:0] char_rd_addr;
|
||||
|
||||
// Adapter outputs (drives osd_overlay.char_rd_data).
|
||||
wire [9:0] word_idx = char_rd_addr[10:1];
|
||||
wire half_sel = char_rd_addr[0];
|
||||
wire [31:0] word_data_w = shadow_mem[word_idx];
|
||||
wire [15:0] cell_data_w = half_sel ? word_data_w[31:16]
|
||||
: word_data_w[15:0];
|
||||
logic [15:0] char_rd_data;
|
||||
always_ff @(posedge clk) char_rd_data <= cell_data_w;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Assertion helper
|
||||
// ------------------------------------------------------------------
|
||||
int errors;
|
||||
task automatic check_eq16(input string label, input logic [15:0] got, input logic [15:0] expected);
|
||||
if (got !== expected) begin
|
||||
$error("[%s] got 0x%04x expected 0x%04x", label, got, expected);
|
||||
errors = errors + 1;
|
||||
end
|
||||
endtask
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Test sequence — pack convention writes are done by directly
|
||||
// computing the word index + half-select inline. Reads drive
|
||||
// char_rd_addr and wait one clock for the adapter register.
|
||||
// ------------------------------------------------------------------
|
||||
integer i;
|
||||
|
||||
initial begin
|
||||
char_rd_addr = '0;
|
||||
errors = 0;
|
||||
for (i = 0; i < 1024; i = i + 1) shadow_mem[i] = 32'd0;
|
||||
|
||||
repeat (4) @(posedge clk);
|
||||
|
||||
// §1 — (col=0, row=0) low half. attr=0x0F (white), char=0xC9.
|
||||
// word_idx = 0*32 + 0 = 0. Pack into low half.
|
||||
shadow_mem[0] = {16'h0000, 16'h0FC9};
|
||||
|
||||
// §2 — (col=1, row=0) high half. attr=0x8F (transp_bg=1), char=0x20.
|
||||
// word_idx = 0*32 + 0 = 0 (col>>1=0). Pack into high half
|
||||
// WITHOUT disturbing the low half from §1.
|
||||
shadow_mem[0] = {16'h8F20, shadow_mem[0][15:0]};
|
||||
|
||||
// §3 — (col=2, row=3) low half. word_idx = 3*32 + 1 = 97.
|
||||
// Cell = 16'h1234.
|
||||
shadow_mem[97] = {16'h0000, 16'h1234};
|
||||
|
||||
// §4 — (col=31, row=7) high half (col[0]=1). word_idx = 7*32 + 15 = 239.
|
||||
// Cell = 16'hFEED.
|
||||
shadow_mem[239] = {16'hFEED, 16'h0000};
|
||||
|
||||
// Read §1 — char_rd_addr = {row=0, col=0} = 11'd0. Expect 0x0FC9.
|
||||
@(negedge clk); char_rd_addr = 11'd0;
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(0,0)_low_half", char_rd_data, 16'h0FC9);
|
||||
|
||||
// Read §2 — char_rd_addr = {row=0, col=1} = 11'd1. Expect 0x8F20.
|
||||
@(negedge clk); char_rd_addr = 11'd1;
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(1,0)_high_half", char_rd_data, 16'h8F20);
|
||||
|
||||
// Read §3 — char_rd_addr = {row=3, col=2} = {5'd3, 6'd2} = 11'h0C2.
|
||||
@(negedge clk); char_rd_addr = {5'd3, 6'd2};
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(2,3)_low_half_row3", char_rd_data, 16'h1234);
|
||||
|
||||
// Read §4 — char_rd_addr = {row=7, col=31} = {5'd7, 6'd31}.
|
||||
@(negedge clk); char_rd_addr = {5'd7, 6'd31};
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(31,7)_high_half_row7_corner", char_rd_data, 16'hFEED);
|
||||
|
||||
// Adjacent unwritten cells must read 0.
|
||||
@(negedge clk); char_rd_addr = {5'd1, 6'd1};
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(1,1)_unwritten", char_rd_data, 16'h0000);
|
||||
|
||||
@(negedge clk); char_rd_addr = {5'd7, 6'd30};
|
||||
@(posedge clk); #1;
|
||||
check_eq16("cell(30,7)_low_neighbor_of_corner", char_rd_data, 16'h0000);
|
||||
|
||||
$display("[tb_osd_platform_cell_adapter] errors=%0d", errors);
|
||||
if (errors == 0) $display("[tb_osd_platform_cell_adapter] PASS");
|
||||
else $display("[tb_osd_platform_cell_adapter] FAIL");
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#2_000_000;
|
||||
$error("[tb_osd_platform_cell_adapter] TIMEOUT");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule : tb_osd_platform_cell_adapter
|
||||
@@ -0,0 +1,248 @@
|
||||
// retroDE_ps2 — tb_platform_video_stub
|
||||
//
|
||||
// Milestone A integration: gs_stub → platform_video_stub. Verifies that a
|
||||
// BGCOLOR write at the GS privileged-register port propagates into the
|
||||
// raster output, that hsync/vsync behave sensibly, and that the platform
|
||||
// emits one EV_MODE trace per completed frame.
|
||||
//
|
||||
// Uses tiny timing overrides so each frame is ~112 cycles (vs. ~420k for
|
||||
// default VGA 640x480). Turnaround stays fast while the full active/front/
|
||||
// sync/back-porch structure is still exercised.
|
||||
//
|
||||
// Plan ref: docs/stub_module_plan.md. Milestone A ref:
|
||||
// docs/decisions/0004-first-visible-milestone.md.
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module tb_platform_video_stub;
|
||||
|
||||
// Tiny timing. Frame = H_TOTAL * V_TOTAL = 14 * 8 = 112 cycles.
|
||||
localparam int H_ACTIVE = 8;
|
||||
localparam int H_FRONT = 2;
|
||||
localparam int H_SYNC = 2;
|
||||
localparam int H_BACK = 2;
|
||||
localparam int V_ACTIVE = 4;
|
||||
localparam int V_FRONT = 1;
|
||||
localparam int V_SYNC = 2;
|
||||
localparam int V_BACK = 1;
|
||||
|
||||
logic clk;
|
||||
logic rst_n;
|
||||
|
||||
initial clk = 1'b0;
|
||||
always #5 clk = ~clk;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// gs_stub
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic gs_reg_wr_en;
|
||||
logic [15:0] gs_reg_wr_addr;
|
||||
logic [63:0] gs_reg_wr_data;
|
||||
|
||||
logic [7:0] bg_r, bg_g, bg_b;
|
||||
|
||||
logic gs_ev_valid;
|
||||
trace_pkg::subsys_e gs_ev_subsys;
|
||||
trace_pkg::event_e gs_ev_event;
|
||||
logic [63:0] gs_ev_arg0, gs_ev_arg1, gs_ev_arg2, gs_ev_arg3;
|
||||
logic [31:0] gs_ev_flags;
|
||||
|
||||
gs_stub u_gs (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.reg_wr_en(gs_reg_wr_en), .reg_wr_addr(gs_reg_wr_addr),
|
||||
.reg_wr_data(gs_reg_wr_data),
|
||||
.gif_reg_wr_en(1'b0), .gif_reg_num(8'd0), .gif_reg_data(64'd0),
|
||||
.prim_q(), .rgbaq_q(), .xyz2_q(), .xyzf2_q(), .frame_1_q(), .zbuf_1_q(),
|
||||
.prim_complete(), .prim_complete_count(),
|
||||
.prim_v0_q(), .prim_v1_q(), .prim_v2_q(),
|
||||
.prim_color_q(),
|
||||
.prim_color_v0_q(), .prim_color_v1_q(), .prim_color_v2_q(),
|
||||
.prim_v0_decoded_q(), .prim_v1_decoded_q(), .prim_v2_decoded_q(),
|
||||
.prim_v0_color_decoded_q(), .prim_v1_color_decoded_q(), .prim_v2_color_decoded_q(),
|
||||
.pixel_emit(), .pixel_emit_count(),
|
||||
.pixel_x_q(), .pixel_y_q(),
|
||||
.pixel_color_q(),
|
||||
.pixel_fbp_q(), .pixel_fbw_q(), .pixel_psm_q(),
|
||||
.pixel_fb_addr_q(),
|
||||
.raster_pixel_emit(), .raster_pixel_emit_count(),
|
||||
.raster_pixel_x_q(), .raster_pixel_y_q(),
|
||||
.raster_pixel_color_q(),
|
||||
.raster_pixel_fb_addr_q(),
|
||||
.raster_active(), .raster_overflow(),
|
||||
.raster_degenerate(),
|
||||
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
|
||||
.ev_valid(gs_ev_valid), .ev_subsys(gs_ev_subsys), .ev_event(gs_ev_event),
|
||||
.ev_arg0(gs_ev_arg0), .ev_arg1(gs_ev_arg1),
|
||||
.ev_arg2(gs_ev_arg2), .ev_arg3(gs_ev_arg3),
|
||||
.ev_flags(gs_ev_flags)
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// platform_video_stub
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
logic hsync, vsync, de;
|
||||
logic [7:0] r, g, b;
|
||||
|
||||
logic plat_ev_valid;
|
||||
trace_pkg::subsys_e plat_ev_subsys;
|
||||
trace_pkg::event_e plat_ev_event;
|
||||
logic [63:0] plat_ev_arg0, plat_ev_arg1, plat_ev_arg2, plat_ev_arg3;
|
||||
logic [31:0] plat_ev_flags;
|
||||
|
||||
platform_video_stub #(
|
||||
.H_ACTIVE(H_ACTIVE), .H_FRONT(H_FRONT), .H_SYNC(H_SYNC), .H_BACK(H_BACK),
|
||||
.V_ACTIVE(V_ACTIVE), .V_FRONT(V_FRONT), .V_SYNC(V_SYNC), .V_BACK(V_BACK)
|
||||
) u_plat (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.bg_r(bg_r), .bg_g(bg_g), .bg_b(bg_b),
|
||||
.hsync(hsync), .vsync(vsync), .de(de),
|
||||
.r(r), .g(g), .b(b),
|
||||
.ev_valid(plat_ev_valid), .ev_subsys(plat_ev_subsys), .ev_event(plat_ev_event),
|
||||
.ev_arg0(plat_ev_arg0), .ev_arg1(plat_ev_arg1),
|
||||
.ev_arg2(plat_ev_arg2), .ev_arg3(plat_ev_arg3),
|
||||
.ev_flags(plat_ev_flags)
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Trace sinks (one per subsystem)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
trace_sink_stub #(.FILENAME("milestone_a_gs.trace"), .SINK_LABEL("gs"))
|
||||
u_trace_gs (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.ev_valid(gs_ev_valid), .ev_subsys(gs_ev_subsys), .ev_event(gs_ev_event),
|
||||
.ev_arg0(gs_ev_arg0), .ev_arg1(gs_ev_arg1),
|
||||
.ev_arg2(gs_ev_arg2), .ev_arg3(gs_ev_arg3),
|
||||
.ev_flags(gs_ev_flags)
|
||||
);
|
||||
|
||||
trace_sink_stub #(.FILENAME("milestone_a_plat.trace"), .SINK_LABEL("plat"))
|
||||
u_trace_plat (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.ev_valid(plat_ev_valid), .ev_subsys(plat_ev_subsys), .ev_event(plat_ev_event),
|
||||
.ev_arg0(plat_ev_arg0), .ev_arg1(plat_ev_arg1),
|
||||
.ev_arg2(plat_ev_arg2), .ev_arg3(plat_ev_arg3),
|
||||
.ev_flags(plat_ev_flags)
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Checkers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
int active_pixels_seen;
|
||||
int frame_events;
|
||||
int hsync_pulses;
|
||||
int vsync_pulses;
|
||||
int errors;
|
||||
|
||||
logic hsync_d;
|
||||
logic vsync_d;
|
||||
|
||||
initial begin
|
||||
active_pixels_seen = 0;
|
||||
frame_events = 0;
|
||||
hsync_pulses = 0;
|
||||
vsync_pulses = 0;
|
||||
errors = 0;
|
||||
hsync_d = 1'b1;
|
||||
vsync_d = 1'b1;
|
||||
end
|
||||
|
||||
// Every active pixel must equal the current bg_{r,g,b}.
|
||||
always_ff @(posedge clk) begin
|
||||
if (rst_n && de) begin
|
||||
active_pixels_seen <= active_pixels_seen + 1;
|
||||
if (r !== bg_r || g !== bg_g || b !== bg_b) begin
|
||||
$error("[tb_platform_video_stub] pixel mismatch: rgb=(%02h,%02h,%02h) bg=(%02h,%02h,%02h)",
|
||||
r, g, b, bg_r, bg_g, bg_b);
|
||||
errors <= errors + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Count sync pulses by falling edges (active-low default).
|
||||
always_ff @(posedge clk) begin
|
||||
hsync_d <= hsync;
|
||||
vsync_d <= vsync;
|
||||
if (rst_n) begin
|
||||
if (hsync_d && !hsync) hsync_pulses <= hsync_pulses + 1;
|
||||
if (vsync_d && !vsync) vsync_pulses <= vsync_pulses + 1;
|
||||
end
|
||||
end
|
||||
|
||||
// Count frame-completion events from platform trace.
|
||||
always_ff @(posedge clk) begin
|
||||
if (rst_n && plat_ev_valid && plat_ev_event == trace_pkg::EV_MODE) begin
|
||||
frame_events <= frame_events + 1;
|
||||
end
|
||||
end
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Stimulus: four frame windows — one at the reset-default mid-grey,
|
||||
// then three BGCOLOR writes (red, green, blue).
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
task automatic write_bgcolor(input logic [23:0] rgb);
|
||||
@(negedge clk);
|
||||
gs_reg_wr_en = 1'b1;
|
||||
gs_reg_wr_addr = 16'h00E0;
|
||||
gs_reg_wr_data = {40'd0, rgb};
|
||||
@(negedge clk);
|
||||
gs_reg_wr_en = 1'b0;
|
||||
gs_reg_wr_addr = 16'd0;
|
||||
gs_reg_wr_data = 64'd0;
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
rst_n = 1'b0;
|
||||
gs_reg_wr_en = 1'b0;
|
||||
gs_reg_wr_addr = 16'd0;
|
||||
gs_reg_wr_data = 64'd0;
|
||||
|
||||
repeat (4) @(posedge clk);
|
||||
rst_n = 1'b1;
|
||||
|
||||
// Frame 0: default mid-grey from gs_stub reset value.
|
||||
repeat (120) @(posedge clk);
|
||||
|
||||
// Frame 1: primary-red test.
|
||||
write_bgcolor(24'hFF_0000);
|
||||
repeat (120) @(posedge clk);
|
||||
|
||||
// Frame 2: primary-green test.
|
||||
write_bgcolor(24'h00_FF00);
|
||||
repeat (120) @(posedge clk);
|
||||
|
||||
// Frame 3: primary-blue test.
|
||||
write_bgcolor(24'h00_00FF);
|
||||
repeat (120) @(posedge clk);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
$display("[tb_platform_video_stub] frames=%0d active_pixels=%0d hsync_pulses=%0d vsync_pulses=%0d errors=%0d",
|
||||
frame_events, active_pixels_seen, hsync_pulses, vsync_pulses, errors);
|
||||
|
||||
if (frame_events < 3)
|
||||
$error("[tb_platform_video_stub] expected >= 3 frame events, got %0d", frame_events);
|
||||
if (active_pixels_seen < (3 * H_ACTIVE * V_ACTIVE))
|
||||
$error("[tb_platform_video_stub] expected >= %0d active pixels, got %0d",
|
||||
3 * H_ACTIVE * V_ACTIVE, active_pixels_seen);
|
||||
if (vsync_pulses < 3)
|
||||
$error("[tb_platform_video_stub] expected >= 3 vsync pulses, got %0d", vsync_pulses);
|
||||
|
||||
if (errors == 0 && frame_events >= 3 && vsync_pulses >= 3)
|
||||
$display("[tb_platform_video_stub] PASS");
|
||||
else
|
||||
$display("[tb_platform_video_stub] FAIL");
|
||||
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#200000;
|
||||
$error("[tb_platform_video_stub] timeout");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule : tb_platform_video_stub
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user