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,221 @@
|
||||
// retroDE_ps2 — tb_ee_bootstrap_mmio
|
||||
//
|
||||
// Chapter 8 targeted TB for ee_bootstrap_mmio_stub. Proves the three
|
||||
// semantics that tb_ee_core_bios_smoke relies on for chapter 7.99's
|
||||
// DEADBEEF-poisoning fix to hold:
|
||||
//
|
||||
// 1) Reset-init to zero. A read from any register immediately after
|
||||
// reset returns 0, not X. (The real BIOS does read-modify-write:
|
||||
// if a fresh read returns X, the OR-in-bit write corrupts the
|
||||
// register even though it "looks" like a normal bit-set.)
|
||||
//
|
||||
// 2) Write-then-read at the same offset returns the written value.
|
||||
// 1-cycle read latency to match the rest of the stub ecosystem.
|
||||
//
|
||||
// 3) Writes at different offsets don't collide. A sentinel test
|
||||
// pattern across a handful of offsets verifies the latch is a
|
||||
// real register file, not a single shared latch.
|
||||
//
|
||||
// Runs in sub-microseconds. No BIOS dump required.
|
||||
|
||||
`timescale 1ns/1ps
|
||||
|
||||
module tb_ee_bootstrap_mmio;
|
||||
|
||||
logic clk;
|
||||
logic rst_n;
|
||||
|
||||
initial clk = 1'b0;
|
||||
always #5 clk = ~clk;
|
||||
|
||||
// DUT ports
|
||||
logic reg_wr_en;
|
||||
logic [15:0] reg_wr_addr;
|
||||
logic [31:0] reg_wr_data;
|
||||
logic [3:0] reg_wr_be;
|
||||
logic reg_rd_en;
|
||||
logic [15:0] reg_rd_addr;
|
||||
logic [31:0] reg_rd_data;
|
||||
logic reg_rd_valid;
|
||||
|
||||
logic ev_valid;
|
||||
trace_pkg::subsys_e ev_subsys;
|
||||
trace_pkg::event_e ev_event;
|
||||
logic [63:0] ev_arg0, ev_arg1, ev_arg2, ev_arg3;
|
||||
logic [31:0] ev_flags;
|
||||
|
||||
ee_bootstrap_mmio_stub dut (
|
||||
.clk(clk), .rst_n(rst_n),
|
||||
.reg_wr_en(reg_wr_en), .reg_wr_addr(reg_wr_addr),
|
||||
.reg_wr_data(reg_wr_data), .reg_wr_be(reg_wr_be),
|
||||
.reg_rd_en(reg_rd_en), .reg_rd_addr(reg_rd_addr),
|
||||
.reg_rd_data(reg_rd_data), .reg_rd_valid(reg_rd_valid),
|
||||
// Ch259 — no synthetic IOP INTC source in this targeted TB.
|
||||
.iop_intc_inject_src_i(16'd0),
|
||||
.ev_valid(ev_valid), .ev_subsys(ev_subsys), .ev_event(ev_event),
|
||||
.ev_arg0(ev_arg0), .ev_arg1(ev_arg1),
|
||||
.ev_arg2(ev_arg2), .ev_arg3(ev_arg3),
|
||||
.ev_flags(ev_flags)
|
||||
);
|
||||
|
||||
int errors = 0;
|
||||
|
||||
task automatic do_write(input logic [15:0] addr, input logic [31:0] data);
|
||||
do_write_be(addr, data, 4'b1111);
|
||||
endtask
|
||||
|
||||
task automatic do_write_be(input logic [15:0] addr,
|
||||
input logic [31:0] data,
|
||||
input logic [3:0] be);
|
||||
@(negedge clk);
|
||||
reg_wr_en = 1'b1;
|
||||
reg_wr_addr = addr;
|
||||
reg_wr_data = data;
|
||||
reg_wr_be = be;
|
||||
@(negedge clk);
|
||||
reg_wr_en = 1'b0;
|
||||
reg_wr_addr = 16'd0;
|
||||
reg_wr_data = 32'd0;
|
||||
reg_wr_be = 4'b0000;
|
||||
endtask
|
||||
|
||||
task automatic do_read(input logic [15:0] addr,
|
||||
output logic [31:0] data_out);
|
||||
@(negedge clk);
|
||||
reg_rd_en = 1'b1;
|
||||
reg_rd_addr = addr;
|
||||
@(negedge clk);
|
||||
reg_rd_en = 1'b0;
|
||||
reg_rd_addr = 16'd0;
|
||||
// reg_rd_data is registered; sample on the following posedge
|
||||
@(posedge clk);
|
||||
data_out = reg_rd_data;
|
||||
endtask
|
||||
|
||||
task automatic check(input string tag,
|
||||
input logic [31:0] got,
|
||||
input logic [31:0] exp);
|
||||
if (got !== exp) begin
|
||||
$display("[tb_ee_bootstrap_mmio] FAIL %s got=0x%08h exp=0x%08h",
|
||||
tag, got, exp);
|
||||
errors++;
|
||||
end else begin
|
||||
$display("[tb_ee_bootstrap_mmio] ok %s = 0x%08h", tag, got);
|
||||
end
|
||||
endtask
|
||||
|
||||
logic [31:0] rd0;
|
||||
|
||||
initial begin
|
||||
rst_n = 1'b0;
|
||||
reg_wr_en = 1'b0;
|
||||
reg_wr_addr = 16'd0;
|
||||
reg_wr_data = 32'd0;
|
||||
reg_wr_be = 4'b0000;
|
||||
reg_rd_en = 1'b0;
|
||||
reg_rd_addr = 16'd0;
|
||||
|
||||
repeat (4) @(posedge clk);
|
||||
rst_n = 1'b1;
|
||||
@(posedge clk);
|
||||
|
||||
// 1) Reset-init: every probed offset reads 0.
|
||||
do_read(16'h0010, rd0); check("reset_read_0x0010", rd0, 32'd0);
|
||||
do_read(16'h1000, rd0); check("reset_read_0x1000", rd0, 32'd0);
|
||||
do_read(16'hF008, rd0); check("reset_read_0xF008", rd0, 32'd0);
|
||||
|
||||
// 2) Write-then-read same offset returns the written value.
|
||||
do_write(16'h0010, 32'hAABB_CCDD);
|
||||
do_read (16'h0010, rd0); check("wr_rd_same_off", rd0, 32'hAABB_CCDD);
|
||||
|
||||
// 3) Distinct offsets do not collide.
|
||||
do_write(16'h0100, 32'h1111_1111);
|
||||
do_write(16'h0200, 32'h2222_2222);
|
||||
do_write(16'h0300, 32'h3333_3333);
|
||||
do_write(16'hFFFC, 32'hDEAD_F00D);
|
||||
|
||||
do_read(16'h0100, rd0); check("distinct_0x0100", rd0, 32'h1111_1111);
|
||||
do_read(16'h0200, rd0); check("distinct_0x0200", rd0, 32'h2222_2222);
|
||||
do_read(16'h0300, rd0); check("distinct_0x0300", rd0, 32'h3333_3333);
|
||||
do_read(16'hFFFC, rd0); check("distinct_0xFFFC", rd0, 32'hDEAD_F00D);
|
||||
do_read(16'h0010, rd0); check("prior_wr_preserved", rd0, 32'hAABB_CCDD);
|
||||
|
||||
// 4) Overwrite replaces prior value.
|
||||
do_write(16'h0010, 32'h5555_5555);
|
||||
do_read (16'h0010, rd0); check("overwrite", rd0, 32'h5555_5555);
|
||||
|
||||
// 5) Word-index aliasing — low 2 bits ignored (word-addressed).
|
||||
do_write(16'h0400, 32'hCAFE_BABE);
|
||||
do_read (16'h0401, rd0); check("byte_addr_aliases_to_word", rd0, 32'hCAFE_BABE);
|
||||
do_read (16'h0402, rd0); check("byte_addr_aliases_to_word2", rd0, 32'hCAFE_BABE);
|
||||
do_read (16'h0403, rd0); check("byte_addr_aliases_to_word3", rd0, 32'hCAFE_BABE);
|
||||
|
||||
// 6) Byte-enable preservation (SB-style). Seed word with a
|
||||
// known pattern via full-word write, then SB to each lane and
|
||||
// verify the other 3 lanes are preserved.
|
||||
do_write(16'h0500, 32'h11223344);
|
||||
do_write_be(16'h0500, 32'h000000AA, 4'b0001); // SB byte 0
|
||||
do_read(16'h0500, rd0); check("sb_lane0", rd0, 32'h112233AA);
|
||||
|
||||
do_write(16'h0504, 32'h11223344);
|
||||
do_write_be(16'h0504, 32'h0000BB00, 4'b0010); // SB byte 1
|
||||
do_read(16'h0504, rd0); check("sb_lane1", rd0, 32'h1122BB44);
|
||||
|
||||
do_write(16'h0508, 32'h11223344);
|
||||
do_write_be(16'h0508, 32'h00CC0000, 4'b0100); // SB byte 2
|
||||
do_read(16'h0508, rd0); check("sb_lane2", rd0, 32'h11CC3344);
|
||||
|
||||
do_write(16'h050C, 32'h11223344);
|
||||
do_write_be(16'h050C, 32'hDD000000, 4'b1000); // SB byte 3
|
||||
do_read(16'h050C, rd0); check("sb_lane3", rd0, 32'hDD223344);
|
||||
|
||||
// 7) Halfword-enable preservation (SH-style).
|
||||
do_write(16'h0600, 32'h11223344);
|
||||
do_write_be(16'h0600, 32'h0000AABB, 4'b0011); // SH low halfword
|
||||
do_read(16'h0600, rd0); check("sh_low_half", rd0, 32'h1122AABB);
|
||||
|
||||
do_write(16'h0604, 32'h11223344);
|
||||
do_write_be(16'h0604, 32'hCCDD0000, 4'b1100); // SH high halfword
|
||||
do_read(16'h0604, rd0); check("sh_high_half", rd0, 32'hCCDD3344);
|
||||
|
||||
// 8) Zero be == no change.
|
||||
do_write(16'h0700, 32'hDEAD_BEEF);
|
||||
do_write_be(16'h0700, 32'hFFFF_FFFF, 4'b0000); // nothing latched
|
||||
do_read(16'h0700, rd0); check("be_zero_noop", rd0, 32'hDEAD_BEEF);
|
||||
|
||||
// 9) Ch202 — offset 0x1814 returns MMIO_1814_RDY_VALUE
|
||||
// (defaulted to 0xFFFFFFFF in the stub) regardless of writes.
|
||||
// The BIOS at PC=0xbfc4fb04..fb30 polls this address waiting
|
||||
// for ($read & mask=0x10000000) != 0. The stub's narrow
|
||||
// read-only return satisfies the poll without enabling a
|
||||
// full-window all-ones default (which would risk side-effects
|
||||
// for other offsets that DO need writable semantics).
|
||||
do_read (16'h1814, rd0); check("ch202_rdy_reset", rd0, 32'hFFFF_FFFF);
|
||||
// BIOS-observed mask satisfied?
|
||||
check("ch202_mask_satisfied",
|
||||
(rd0 & 32'h1000_0000) != 32'd0 ? 32'd1 : 32'd0, 32'd1);
|
||||
// Even after a write, reads still return the ready value
|
||||
// (write latches into regs[] but the read intercept wins).
|
||||
do_write(16'h1814, 32'hDEAD_BEEF);
|
||||
do_read (16'h1814, rd0); check("ch202_rdy_after_wr", rd0, 32'hFFFF_FFFF);
|
||||
// Neighbor offsets are NOT special-cased and behave normally.
|
||||
do_write(16'h1810, 32'hAAAA_AAAA);
|
||||
do_read (16'h1810, rd0); check("ch202_neighbor_lo", rd0, 32'hAAAA_AAAA);
|
||||
do_write(16'h1818, 32'hBBBB_BBBB);
|
||||
do_read (16'h1818, rd0); check("ch202_neighbor_hi", rd0, 32'hBBBB_BBBB);
|
||||
|
||||
if (errors == 0)
|
||||
$display("[tb_ee_bootstrap_mmio] PASS");
|
||||
else
|
||||
$display("[tb_ee_bootstrap_mmio] FAIL errors=%0d", errors);
|
||||
|
||||
$finish;
|
||||
end
|
||||
|
||||
initial begin
|
||||
#5_000_000;
|
||||
$display("[tb_ee_bootstrap_mmio] TIMEOUT");
|
||||
$finish;
|
||||
end
|
||||
|
||||
endmodule : tb_ee_bootstrap_mmio
|
||||
Reference in New Issue
Block a user