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:
2026-06-29 20:10:50 -04:00
commit ec82764bef
2462 changed files with 2174303 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
// ============================================================================
// I2C_Controller.v — Fixed-frame I2C master (3-byte write transactions)
// ============================================================================
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 retroDE contributors
//
// Clean-room implementation, not derived from any GPL upstream. Released
// under the MIT license to allow reuse outside the retroDE project. The
// retroDE project as a whole is distributed under GPLv3 — see ../LICENSE
// for the combined-work terms. See ../LICENSES/MIT.txt for the full MIT
// text.
//
// ----------------------------------------------------------------------------
// Purpose
// Simple I2C master that sends a fixed 24-bit frame per transaction:
// I2C_DATA = { slave_addr[6:0], rw, reg_addr[7:0], data[7:0] }
// Slave ACK is sampled after each of the three bytes. A STOP condition is
// generated at the end of the frame.
//
// Timing contract (compatible with legacy I2C_HDMI_Config parent)
// CLK Fabric clock; state is registered on its rising edge.
// CLK_EN One-cycle pulse at the desired I2C bit rate. State only advances
// when CLK_EN is asserted.
// CLK_PHASE Square wave at the SCL rate. During actively clocked bit cells,
// I2C_SCLK is driven as ~CLK_PHASE. Dedicated START/STOP hold
// phases force SCL high or low for a full cell.
// I2C_SDAT Open-drain: driven low via 1'b0 or released to 1'bz. An
// external or FPGA internal pull-up is required on this line.
// I2C_SCLK Actively driven (not open-drain). This matches the known-good
// DE25-Nano HDMI path and avoids relying on board-side pull-ups.
//
// Interface
// Port list is preserved verbatim from the legacy module so this file is
// a drop-in replacement. W_R is retained as a no-op input for source-
// compatibility; direction is encoded in I2C_DATA[16] by convention.
// SD_COUNTER and SDO are exposed for debug/observation only.
//
// Implementation note
// The transaction is modeled as explicit phases rather than implicit state
// updates. This keeps the START, bit, ACK, and STOP cells easy to inspect
// while preserving the known-good bus waveform used by retroDE_splash.
// ============================================================================
`timescale 1ns/1ps
module I2C_Controller (
input wire CLK,
input wire CLK_EN,
input wire CLK_PHASE,
output wire I2C_SCLK,
inout wire I2C_SDAT,
input wire [23:0] I2C_DATA,
input wire GO,
output reg END,
input wire W_R, // retained for interface compat; unused
output wire ACK,
input wire RESET,
output wire [5:0] SD_COUNTER, // debug: current transaction phase
output wire SDO // debug: current SDA release state
);
localparam [5:0] PH_IDLE = 6'd0,
PH_START_HOLD = 6'd1,
PH_START_LOW = 6'd2,
PH_B0_7 = 6'd3,
PH_B0_6 = 6'd4,
PH_B0_5 = 6'd5,
PH_B0_4 = 6'd6,
PH_B0_3 = 6'd7,
PH_B0_2 = 6'd8,
PH_B0_1 = 6'd9,
PH_B0_0 = 6'd10,
PH_ACK0 = 6'd11,
PH_B1_7 = 6'd12,
PH_B1_6 = 6'd13,
PH_B1_5 = 6'd14,
PH_B1_4 = 6'd15,
PH_B1_3 = 6'd16,
PH_B1_2 = 6'd17,
PH_B1_1 = 6'd18,
PH_B1_0 = 6'd19,
PH_ACK1 = 6'd20,
PH_B2_7 = 6'd21,
PH_B2_6 = 6'd22,
PH_B2_5 = 6'd23,
PH_B2_4 = 6'd24,
PH_B2_3 = 6'd25,
PH_B2_2 = 6'd26,
PH_B2_1 = 6'd27,
PH_B2_0 = 6'd28,
PH_ACK2 = 6'd29,
PH_STOP_LOW = 6'd30,
PH_STOP_HIGH = 6'd31,
PH_DONE = 6'd32;
reg [5:0] phase;
reg [23:0] frame_data;
reg [2:0] ack_bits;
reg sda_release;
reg [5:0] phase_next;
reg [23:0] frame_data_next;
reg [2:0] ack_bits_next;
reg sda_release_next;
reg end_next;
assign I2C_SCLK =
(phase == PH_IDLE || phase == PH_START_HOLD || phase == PH_STOP_HIGH || phase == PH_DONE) ? 1'b1 :
(phase == PH_START_LOW || phase == PH_STOP_LOW) ? 1'b0 :
~CLK_PHASE;
assign I2C_SDAT = sda_release ? 1'bz : 1'b0;
assign ACK = |ack_bits;
assign SDO = sda_release;
assign SD_COUNTER = phase;
always @(*) begin
phase_next = phase;
frame_data_next = frame_data;
ack_bits_next = ack_bits;
sda_release_next = sda_release;
end_next = END;
case (phase)
PH_IDLE: begin
end_next = 1'b0;
sda_release_next = 1'b1;
if (GO) begin
phase_next = PH_START_HOLD;
frame_data_next = I2C_DATA;
ack_bits_next = 3'd0;
sda_release_next = 1'b0;
end
end
PH_START_HOLD: begin
phase_next = PH_START_LOW;
sda_release_next = 1'b0;
end
PH_START_LOW: begin
phase_next = PH_B0_7;
sda_release_next = frame_data[23];
end
PH_B0_7: begin phase_next = PH_B0_6; sda_release_next = frame_data[22]; end
PH_B0_6: begin phase_next = PH_B0_5; sda_release_next = frame_data[21]; end
PH_B0_5: begin phase_next = PH_B0_4; sda_release_next = frame_data[20]; end
PH_B0_4: begin phase_next = PH_B0_3; sda_release_next = frame_data[19]; end
PH_B0_3: begin phase_next = PH_B0_2; sda_release_next = frame_data[18]; end
PH_B0_2: begin phase_next = PH_B0_1; sda_release_next = frame_data[17]; end
PH_B0_1: begin phase_next = PH_B0_0; sda_release_next = frame_data[16]; end
PH_B0_0: begin phase_next = PH_ACK0; sda_release_next = 1'b1; end
PH_ACK0: begin
phase_next = PH_B1_7;
ack_bits_next[0] = I2C_SDAT;
sda_release_next = frame_data[15];
end
PH_B1_7: begin phase_next = PH_B1_6; sda_release_next = frame_data[14]; end
PH_B1_6: begin phase_next = PH_B1_5; sda_release_next = frame_data[13]; end
PH_B1_5: begin phase_next = PH_B1_4; sda_release_next = frame_data[12]; end
PH_B1_4: begin phase_next = PH_B1_3; sda_release_next = frame_data[11]; end
PH_B1_3: begin phase_next = PH_B1_2; sda_release_next = frame_data[10]; end
PH_B1_2: begin phase_next = PH_B1_1; sda_release_next = frame_data[9]; end
PH_B1_1: begin phase_next = PH_B1_0; sda_release_next = frame_data[8]; end
PH_B1_0: begin phase_next = PH_ACK1; sda_release_next = 1'b1; end
PH_ACK1: begin
phase_next = PH_B2_7;
ack_bits_next[1] = I2C_SDAT;
sda_release_next = frame_data[7];
end
PH_B2_7: begin phase_next = PH_B2_6; sda_release_next = frame_data[6]; end
PH_B2_6: begin phase_next = PH_B2_5; sda_release_next = frame_data[5]; end
PH_B2_5: begin phase_next = PH_B2_4; sda_release_next = frame_data[4]; end
PH_B2_4: begin phase_next = PH_B2_3; sda_release_next = frame_data[3]; end
PH_B2_3: begin phase_next = PH_B2_2; sda_release_next = frame_data[2]; end
PH_B2_2: begin phase_next = PH_B2_1; sda_release_next = frame_data[1]; end
PH_B2_1: begin phase_next = PH_B2_0; sda_release_next = frame_data[0]; end
PH_B2_0: begin phase_next = PH_ACK2; sda_release_next = 1'b1; end
PH_ACK2: begin
phase_next = PH_STOP_LOW;
ack_bits_next[2] = I2C_SDAT;
sda_release_next = 1'b0;
end
PH_STOP_LOW: begin
phase_next = PH_STOP_HIGH;
sda_release_next = 1'b0;
end
PH_STOP_HIGH: begin
phase_next = PH_DONE;
sda_release_next = 1'b1;
end
PH_DONE: begin
end_next = 1'b1;
sda_release_next = 1'b1;
if (!GO)
phase_next = PH_IDLE;
end
default: begin
phase_next = PH_IDLE;
sda_release_next = 1'b1;
end_next = 1'b0;
end
endcase
end
always @(posedge CLK or negedge RESET) begin
if (!RESET) begin
phase <= PH_IDLE;
frame_data <= 24'd0;
ack_bits <= 3'd0;
sda_release <= 1'b1;
END <= 1'b0;
end
else if (CLK_EN) begin
phase <= phase_next;
frame_data <= frame_data_next;
ack_bits <= ack_bits_next;
sda_release <= sda_release_next;
END <= end_next;
end
end
endmodule
+236
View File
@@ -0,0 +1,236 @@
// ============================================================================
// I2C_HDMI_Config.v — ADV7513 HDMI transmitter configuration via I2C
// ============================================================================
//
// Derived from Terasic DE-series reference design (I2C_HDMI_Config.v).
// Original copyright belongs to Terasic Technologies Inc.; this file is
// distributed under the terms of the Terasic Reference Design license that
// ships with the DE25-Nano System CD (free use on Terasic hardware,
// copyright notice retained).
//
// retroDE modifications (2025-2026):
// - LUT_SIZE expanded to 38 entries
// - Audio configuration for I2S input @ 48 kHz, MCLK 12.288 MHz
// - HPD override (0xD6 = 0xC0) for monitors that misreport hot-plug
// - AVI InfoFrame configured for full-range RGB 444 output
// - Comments documenting each ADV7513 register write
//
// ============================================================================
`timescale 1ns/1ps
module I2C_HDMI_Config ( // Host Side
iCLK,
iRST_N,
// I2C Side
I2C_SCLK,
I2C_SDAT,
HDMI_TX_INT,
READY,
// Ch166: sticky NACK watchdog
ERROR
);
// Host Side
input iCLK;
input iRST_N;
// I2C Side: SCL is actively driven by the master; SDA is open-drain
// (master drives low / releases to 1'bz; slave drives ACK).
output I2C_SCLK;
inout I2C_SDAT;
input HDMI_TX_INT;
output READY ;
// Ch166: ERROR latches HIGH if the same LUT entry NACKs
// NACK_LIMIT consecutive times (chip absent, address wrong,
// bus shorted). Sticky until iRST_N. Cleared on reset.
output ERROR;
// Internal Registers/Wires
reg [15:0] mI2C_CLK_DIV;
reg [23:0] mI2C_DATA;
reg mI2C_CTRL_CLK;
reg mI2C_GO;
wire mI2C_END;
wire mI2C_ACK;
reg [15:0] LUT_DATA;
reg [5:0] LUT_INDEX;
reg [3:0] mSetup_ST;
reg READY ;
// Clock Setting
parameter CLK_Freq = 50000000; // 50 MHz
parameter I2C_Freq = 20000; // 20 KHz
// LUT Data Number
parameter LUT_SIZE = 38;
// Ch166 - NACK watchdog threshold (consecutive retries on the
// same LUT entry before ERROR latches). At I2C_Freq=20 kHz a
// full byte transaction is ~1.5 ms, so 16 retries ~= 24 ms before
// we declare the bus dead - generous enough for real-world bus
// settling but well short of a stuck-LED user complaint.
parameter NACK_LIMIT = 16;
///////////////////// I2C Control Clock ////////////////////////
always@(posedge iCLK or negedge iRST_N)
begin
if(!iRST_N)
begin
mI2C_CTRL_CLK <= 0;
mI2C_CLK_DIV <= 0;
end
else
begin
if( mI2C_CLK_DIV < (CLK_Freq/I2C_Freq) )
mI2C_CLK_DIV <= mI2C_CLK_DIV+1;
else
begin
mI2C_CLK_DIV <= 0;
mI2C_CTRL_CLK <= ~mI2C_CTRL_CLK;
end
end
end
////////////////////////////////////////////////////////////////////
I2C_Controller u0 ( .CLK(mI2C_CTRL_CLK), // Controller work clock
.CLK_EN(1'b1), // Advance every controller clock
.CLK_PHASE(mI2C_CTRL_CLK), // Phase for SCL generation
.I2C_SCLK(I2C_SCLK), // I2C CLOCK
.I2C_SDAT(I2C_SDAT), // I2C DATA
.I2C_DATA(mI2C_DATA), // DATA:[SLAVE_ADDR,SUB_ADDR,DATA]
.GO(mI2C_GO), // GO transfor
.END(mI2C_END), // END transfor
.W_R(1'b0), // Ch165 audit Low tie retained-compat port off (always WRITE)
.ACK(mI2C_ACK), // ACK
.RESET(iRST_N) );
////////////////////////////////////////////////////////////////////
////////////////////// Config Control ////////////////////////////
always@(posedge mI2C_CTRL_CLK or negedge iRST_N)
begin
if(!iRST_N)
begin
READY <= 0;
LUT_INDEX <= 0;
mSetup_ST <= 0;
mI2C_GO <= 0;
end
else
begin
if(LUT_INDEX<LUT_SIZE)
begin
READY<=0;
case(mSetup_ST)
0: begin
mI2C_DATA <= {8'h72,LUT_DATA};
mI2C_GO <= 1;
mSetup_ST <= 1;
end
1: begin
if(mI2C_END)
begin
if(!mI2C_ACK)
mSetup_ST <= 2;
else
mSetup_ST <= 0;
mI2C_GO <= 0;
end
end
2: begin
LUT_INDEX <= LUT_INDEX+1;
mSetup_ST <= 0;
end
endcase
end
else
begin
READY<=1;
if(!HDMI_TX_INT)
begin
LUT_INDEX <= 0;
end
else
LUT_INDEX <= LUT_INDEX;
end
end
end
////////////////////////////////////////////////////////////////////
////////////////// Ch166 NACK watchdog (sticky) //////////////////
//
// Counts consecutive NACK retries on the *current* LUT entry.
// In the config FSM above, state 1 sees mI2C_END at the end of
// each I2C transaction; if mI2C_ACK is HIGH (slave didn't drive
// the ACK bit LOW), the FSM bounces back to state 0 and retries
// the same LUT_DATA. State 2 means the byte ACKed and LUT_INDEX
// is about to advance, so we clear the retry count there. Once
// the count hits NACK_LIMIT, ERROR latches HIGH (sticky until
// iRST_N) so the top level can surface a stuck bus on an LED.
reg [7:0] nack_retries;
reg error_latched;
always @(posedge mI2C_CTRL_CLK or negedge iRST_N)
begin
if (!iRST_N)
begin
nack_retries <= 0;
error_latched <= 1'b0;
end
else
begin
if (mSetup_ST == 1 && mI2C_END && mI2C_ACK)
begin
nack_retries <= nack_retries + 1;
if (nack_retries == NACK_LIMIT - 1)
error_latched <= 1'b1;
end
else if (mSetup_ST == 2)
begin
nack_retries <= 0;
end
end
end
assign ERROR = error_latched;
////////////////////////////////////////////////////////////////////
///////////////////// Config Data LUT //////////////////////////
always@(*)
begin
case(LUT_INDEX)
// Video Config Data
00 : LUT_DATA <= 16'h9803; //Must be set to 0x03 for proper operation
01 : LUT_DATA <= 16'hD6C0; //HPD override: force HPD always-high (bits[7:6]=11)
02 : LUT_DATA <= 16'h0100; //Set 'N' value at 6144
03 : LUT_DATA <= 16'h0218; //Set 'N' value at 6144
04 : LUT_DATA <= 16'h0300; //Set 'N' value at 6144
05 : LUT_DATA <= 16'h0a01; //MCLK ratio = 256x fs (12.288 MHz / 48 kHz)
06 : LUT_DATA <= 16'h0b2e; //MCLK Active
07 : LUT_DATA <= 16'h0cbc; //Serial Audio standard i2s, R0x0C[1:0] = '00
08 : LUT_DATA <= 16'h1402; //Audio Word Length 16 bit, stereo (2 channels)
09 : LUT_DATA <= 16'h1520; //Input 444 (RGB or YCrCb) with Separate Syncs, 48kHz fs
10 : LUT_DATA <= 16'h1630; //Output format 444, 24-bit input
11 : LUT_DATA <= 16'h1846; //Disable CSC
12 : LUT_DATA <= 16'h4080; //General control packet enable
13 : LUT_DATA <= 16'h4110; //Power down control
14 : LUT_DATA <= 16'h49A8; //Set dither mode - 12-to-10 bit
15 : LUT_DATA <= 16'h5510; //AVI InfoFrame byte 1: Y=RGB, A0=active fmt valid
16 : LUT_DATA <= 16'h5608; //AVI InfoFrame byte 2: active format aspect
17 : LUT_DATA <= 16'h5708; //AVI InfoFrame byte 3: Q=10 (full range RGB 0-255)
18 : LUT_DATA <= 16'h94C0; //INT enable 1: HPD + monitor sense only
19 : LUT_DATA <= 16'h9500; //INT enable 2: all disabled
20 : LUT_DATA <= 16'h96C0; //Clear HPD + monitor sense status (matches 0x94 enable mask)
21 : LUT_DATA <= 16'h7301; //Info frame Ch count = 2 (stereo)
22 : LUT_DATA <= 16'h7600; //Speaker allocation: FL+FR (stereo)
23 : LUT_DATA <= 16'h9803; //Must be set to 0x03 for proper operation
24 : LUT_DATA <= 16'h9902; //Must be set to Default Value
25 : LUT_DATA <= 16'h9ae0; //Must be set to 0b1110000
26 : LUT_DATA <= 16'h9c30; //PLL filter R1 value
27 : LUT_DATA <= 16'h9d61; //Set clock divide
28 : LUT_DATA <= 16'ha2a4; //Must be set to 0xA4 for proper operation
29 : LUT_DATA <= 16'ha3a4; //Must be set to 0xA4 for proper operation
30 : LUT_DATA <= 16'ha504; //Must be set to Default Value
31 : LUT_DATA <= 16'hab40; //Must be set to Default Value
32 : LUT_DATA <= 16'haf16; //Select HDMI mode
33 : LUT_DATA <= 16'hba60; //No clock delay
34 : LUT_DATA <= 16'hd1ff; //Must be set to Default Value
35 : LUT_DATA <= 16'hde10; //Must be set to Default for proper operation
36 : LUT_DATA <= 16'he460; //Must be set to Default Value
37 : LUT_DATA <= 16'hfa7d; //Nbr of times to look for good phase
default: LUT_DATA <= 16'h9803;
endcase
end
////////////////////////////////////////////////////////////////////
endmodule
+29
View File
@@ -0,0 +1,29 @@
# rtl/platform
retroDE-specific platform integration. Matches `docs/contracts/platform.md`.
## Wave 1 contents
- `platform_video_stub.sv` — free-running raster generator. Default VGA
640x480 timing (overridable per-testbench to tiny values for fast sim).
Takes `bg_{r,g,b}` from `gs_stub` and flood-fills the active region.
Emits one `EV_MODE` per completed frame so testbenches can count frames
without sampling raw video.
## Scope boundary
This directory owns:
- clock/reset sequencing entry points,
- retroDE-facing video and audio adaptation,
- HPS bridge plumbing (future),
- top-level wrappers not belonging inside PS2 subsystems.
It does **not** own GS/PCRTC semantics (that's `rtl/gif_gs/`), SPU2 audio
synthesis (`rtl/spu2/`), or any PS2 register behavior.
## Replacement path
`platform_video_stub` stays as the platform adaptation layer. What changes
is the upstream pixel source: Wave 1 → flat BGCOLOR from `gs_stub`,
later waves → fuller GS/PCRTC output including framebuffer scan-out.
+1
View File
@@ -0,0 +1 @@
/home/ubuntu/FPGA_Projects/retroDE_splash/rtl/platform/cp437_8x8.mem
+163
View File
@@ -0,0 +1,163 @@
// retroDE_ps2 — platform_video_stub
//
// Smallest retroDE-facing video adapter needed for Milestone A. Accepts a
// flat pixel source (bg_{r,g,b}) from gs_stub and generates a free-running
// VGA-style raster with configurable timing. Wave 1 produces a flood-fill
// frame at the current BGCOLOR — enough to prove the platform video path
// end-to-end without waiting for real GS/PCRTC behavior.
//
// Contract refs:
// docs/stub_module_plan.md (Wave 1, item 5)
// docs/contracts/platform.md
//
// Default timing is VGA 640x480 @ 25.175 MHz pixel clock. Testbenches
// typically override to tiny values (e.g. 16x8 with minimal porches) to
// keep simulation turnaround short.
//
// Replacement path: this module remains as the platform adaptation layer
// while the upstream pixel source evolves from gs_stub to fuller GS/PCRTC
// output.
//
// Trace payload schema:
// PLAT MODE arg0=frame_number arg1=pixels_per_frame arg2=- arg3=-
// emitted once per frame on vsync rising edge, so testbenches can count
// frames without sampling raw video signals.
`timescale 1ns/1ps
module platform_video_stub
import trace_pkg::*;
#(
// Horizontal timing (in pixel clocks)
parameter int H_ACTIVE = 640,
parameter int H_FRONT = 16,
parameter int H_SYNC = 96,
parameter int H_BACK = 48,
// Vertical timing (in line counts)
parameter int V_ACTIVE = 480,
parameter int V_FRONT = 10,
parameter int V_SYNC = 2,
parameter int V_BACK = 33,
// Sync polarity. VGA 640x480 is active-low on both.
parameter bit HSYNC_ACTIVE_LOW = 1'b1,
parameter bit VSYNC_ACTIVE_LOW = 1'b1
) (
input logic clk, // pixel clock
input logic rst_n,
// Pixel source from gs_stub
input logic [7:0] bg_r,
input logic [7:0] bg_g,
input logic [7:0] bg_b,
// Platform-facing video
output logic hsync,
output logic vsync,
output logic de,
output logic [7:0] r,
output logic [7:0] g,
output logic [7:0] b,
// Trace
output logic ev_valid,
output subsys_e ev_subsys,
output event_e ev_event,
output logic [63:0] ev_arg0,
output logic [63:0] ev_arg1,
output logic [63:0] ev_arg2,
output logic [63:0] ev_arg3,
output logic [31:0] ev_flags
);
localparam int H_TOTAL = H_ACTIVE + H_FRONT + H_SYNC + H_BACK;
localparam int V_TOTAL = V_ACTIVE + V_FRONT + V_SYNC + V_BACK;
localparam int H_SYNC_START = H_ACTIVE + H_FRONT;
localparam int H_SYNC_END = H_SYNC_START + H_SYNC;
localparam int V_SYNC_START = V_ACTIVE + V_FRONT;
localparam int V_SYNC_END = V_SYNC_START + V_SYNC;
localparam int HCNT_W = $clog2(H_TOTAL);
localparam int VCNT_W = $clog2(V_TOTAL);
logic [HCNT_W-1:0] hcnt;
logic [VCNT_W-1:0] vcnt;
// ------------------------------------------------------------------
// Raster counters
// ------------------------------------------------------------------
logic end_of_line;
logic end_of_frame;
assign end_of_line = (hcnt == HCNT_W'(H_TOTAL - 1));
assign end_of_frame = end_of_line && (vcnt == VCNT_W'(V_TOTAL - 1));
always_ff @(posedge clk) begin
if (!rst_n) begin
hcnt <= '0;
vcnt <= '0;
end else if (end_of_line) begin
hcnt <= '0;
vcnt <= end_of_frame ? '0 : (vcnt + VCNT_W'(1));
end else begin
hcnt <= hcnt + HCNT_W'(1);
end
end
// ------------------------------------------------------------------
// Sync + data-enable + pixel colour
// ------------------------------------------------------------------
logic active_h;
logic active_v;
logic in_hsync;
logic in_vsync;
assign active_h = (hcnt < HCNT_W'(H_ACTIVE));
assign active_v = (vcnt < VCNT_W'(V_ACTIVE));
assign in_hsync = (hcnt >= HCNT_W'(H_SYNC_START)) && (hcnt < HCNT_W'(H_SYNC_END));
assign in_vsync = (vcnt >= VCNT_W'(V_SYNC_START)) && (vcnt < VCNT_W'(V_SYNC_END));
assign hsync = HSYNC_ACTIVE_LOW ? ~in_hsync : in_hsync;
assign vsync = VSYNC_ACTIVE_LOW ? ~in_vsync : in_vsync;
assign de = active_h && active_v;
assign r = de ? bg_r : 8'd0;
assign g = de ? bg_g : 8'd0;
assign b = de ? bg_b : 8'd0;
// ------------------------------------------------------------------
// Trace: one EV_MODE pulse per completed frame.
// ------------------------------------------------------------------
logic [31:0] frame_count;
always_ff @(posedge clk) begin
if (!rst_n) begin
frame_count <= 32'd0;
ev_valid <= 1'b0;
ev_subsys <= SUBSYS_PLAT;
ev_event <= EV_MODE;
ev_arg0 <= 64'd0;
ev_arg1 <= 64'd0;
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else if (end_of_frame) begin
frame_count <= frame_count + 32'd1;
ev_valid <= 1'b1;
ev_subsys <= SUBSYS_PLAT;
ev_event <= EV_MODE;
ev_arg0 <= {32'd0, frame_count};
ev_arg1 <= {{(64-32){1'b0}}, 32'(H_ACTIVE * V_ACTIVE)};
ev_arg2 <= 64'd0;
ev_arg3 <= 64'd0;
ev_flags <= 32'd0;
end else begin
ev_valid <= 1'b0;
end
end
endmodule : platform_video_stub
File diff suppressed because it is too large Load Diff
+222
View File
@@ -0,0 +1,222 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 retroDE contributors
// ============================================================================
// ps2_hps_bridge_null — minimal AXI4 slave for the PS2 core's Ch170 shell
// ============================================================================
//
// Purpose: present an AXI4 slave endpoint to the HPS hps2fpga bridge that
// (a) does proper AXI handshake so HPS transactions can't stall the bus,
// and (b) exposes a minimal "core identity" register window at 0x000-0x00F
// so retrodesd / probing utilities can read back who loaded.
//
// This is the Ch170 placeholder — when a real ps2_hps_bridge.sv lands (with
// HPS-driven core_reset, status mirrors, ROM staging, etc.), it should keep
// the same AXI4 port signature so the top-wrapper instantiation doesn't
// need to change.
//
// AXI4 subset (matches splash_hps_bridge.sv):
// - 128-bit data bus with byte-lane selection via {awaddr[3:2] / araddr[3:2]}
// - Single-beat only (awlen=0, arlen=0)
// - 4-bit ID echo
// - 38-bit address
//
// Identity register map (ABI v1.0 — read-only):
// 0x000 CORE_ID = 32'h70533200 ("pS2\0" — placeholder, refine later)
// 0x004 ABI_VERSION = 32'h00000100 (v1.0)
// 0x008 CORE_STATUS = 32'h00000001 (bit 0 = loaded)
// 0x00C CORE_CAPS = 32'h00000000 (no caps advertised)
//
// Everything else: reads return 0, writes ACK'd and discarded.
// ============================================================================
`timescale 1ns/1ps
module ps2_hps_bridge_null (
input logic clk, // qsys clk_100_clk domain
input logic reset_n,
input logic h2f_reset, // HPS-driven fabric reset (active high) — unused; reserved
// AXI4 slave — write address channel
input logic [3:0] s_axi_awid,
input logic [37:0] s_axi_awaddr,
input logic [7:0] s_axi_awlen,
input logic [2:0] s_axi_awsize,
input logic [1:0] s_axi_awburst,
input logic s_axi_awlock,
input logic [3:0] s_axi_awcache,
input logic [2:0] s_axi_awprot,
input logic s_axi_awvalid,
output logic s_axi_awready,
// AXI4 slave — write data channel
input logic [127:0] s_axi_wdata,
input logic [15:0] s_axi_wstrb,
input logic s_axi_wlast,
input logic s_axi_wvalid,
output logic s_axi_wready,
// AXI4 slave — write response channel
output logic [3:0] s_axi_bid,
output logic [1:0] s_axi_bresp,
output logic s_axi_bvalid,
input logic s_axi_bready,
// AXI4 slave — read address channel
input logic [3:0] s_axi_arid,
input logic [37:0] s_axi_araddr,
input logic [7:0] s_axi_arlen,
input logic [2:0] s_axi_arsize,
input logic [1:0] s_axi_arburst,
input logic s_axi_arlock,
input logic [3:0] s_axi_arcache,
input logic [2:0] s_axi_arprot,
input logic s_axi_arvalid,
output logic s_axi_arready,
// AXI4 slave — read data channel
output logic [3:0] s_axi_rid,
output logic [127:0] s_axi_rdata,
output logic [1:0] s_axi_rresp,
output logic s_axi_rlast,
output logic s_axi_rvalid,
input logic s_axi_rready
);
// ----------------------------------------------------------------
// Identity register window (Ch170 ABI v1.0).
// ----------------------------------------------------------------
localparam logic [31:0] CORE_ID = 32'h70533200;
localparam logic [31:0] ABI_VERSION = 32'h00000100;
localparam logic [31:0] CORE_STATUS = 32'h00000001;
localparam logic [31:0] CORE_CAPS = 32'h00000000;
function automatic logic [31:0] identity_lookup(input logic [37:0] addr);
// Identity registers live in the first 16 bytes of the bridge map.
// Anything else returns 0. addr[3:2] picks one of four 32-bit slots.
if (addr[37:4] != '0)
return 32'd0;
case (addr[3:2])
2'b00: identity_lookup = CORE_ID;
2'b01: identity_lookup = ABI_VERSION;
2'b10: identity_lookup = CORE_STATUS;
default: identity_lookup = CORE_CAPS;
endcase
endfunction
// ----------------------------------------------------------------
// Write FSM. Single-beat: accept awvalid + wvalid together, hold
// them ready for one cycle each, then emit bvalid. Stays in the
// BRESP state until bready, so multi-cycle bready timing from
// qsys still completes cleanly.
// ----------------------------------------------------------------
typedef enum logic [1:0] { W_IDLE, W_DATA, W_RESP } w_state_t;
w_state_t w_state;
logic [3:0] aw_id_q;
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
w_state <= W_IDLE;
aw_id_q <= '0;
s_axi_bvalid <= 1'b0;
end else begin
case (w_state)
W_IDLE: begin
s_axi_bvalid <= 1'b0;
if (s_axi_awvalid && s_axi_awready) begin
aw_id_q <= s_axi_awid;
w_state <= W_DATA;
end
end
W_DATA: begin
if (s_axi_wvalid && s_axi_wready) begin
s_axi_bvalid <= 1'b1;
w_state <= W_RESP;
end
end
W_RESP: begin
if (s_axi_bready) begin
s_axi_bvalid <= 1'b0;
w_state <= W_IDLE;
end
end
default: w_state <= W_IDLE;
endcase
end
end
assign s_axi_awready = (w_state == W_IDLE);
assign s_axi_wready = (w_state == W_DATA);
assign s_axi_bid = aw_id_q;
assign s_axi_bresp = 2'b00; // OKAY
// ----------------------------------------------------------------
// Read FSM. Same shape — accept arvalid, drive rdata + rvalid,
// hold until rready.
// ----------------------------------------------------------------
typedef enum logic [0:0] { R_IDLE, R_RESP } r_state_t;
r_state_t r_state;
logic [3:0] ar_id_q;
logic [37:0] ar_addr_q;
logic [127:0] rdata_q;
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
r_state <= R_IDLE;
ar_id_q <= '0;
ar_addr_q <= '0;
rdata_q <= '0;
s_axi_rvalid <= 1'b0;
end else begin
case (r_state)
R_IDLE: begin
s_axi_rvalid <= 1'b0;
if (s_axi_arvalid && s_axi_arready) begin
ar_id_q <= s_axi_arid;
ar_addr_q <= s_axi_araddr;
// Replicate the 32-bit identity word into the
// matching 32-bit lane of the 128-bit response,
// mirroring splash_hps_bridge's lane semantics.
case (s_axi_araddr[3:2])
2'b00: rdata_q <= {96'd0, identity_lookup(s_axi_araddr)};
2'b01: rdata_q <= {64'd0, identity_lookup(s_axi_araddr), 32'd0};
2'b10: rdata_q <= {32'd0, identity_lookup(s_axi_araddr), 64'd0};
default: rdata_q <= {identity_lookup(s_axi_araddr), 96'd0};
endcase
s_axi_rvalid <= 1'b1;
r_state <= R_RESP;
end
end
R_RESP: begin
if (s_axi_rready) begin
s_axi_rvalid <= 1'b0;
r_state <= R_IDLE;
end
end
default: r_state <= R_IDLE;
endcase
end
end
assign s_axi_arready = (r_state == R_IDLE);
assign s_axi_rid = ar_id_q;
assign s_axi_rdata = rdata_q;
assign s_axi_rresp = 2'b00; // OKAY
assign s_axi_rlast = 1'b1; // single-beat
// ----------------------------------------------------------------
// Tie off the AXI4 fields we don't consume so Quartus doesn't
// emit lint warnings: awlen/awsize/awburst/awlock/awcache/awprot,
// wstrb/wlast, arlen/arsize/arburst/arlock/arcache/arprot, h2f_reset.
// ----------------------------------------------------------------
// verilator lint_off UNUSED
wire _unused_ok = &{ 1'b0,
s_axi_awlen, s_axi_awsize, s_axi_awburst,
s_axi_awlock, s_axi_awcache, s_axi_awprot,
s_axi_wdata, s_axi_wstrb, s_axi_wlast,
s_axi_arlen, s_axi_arsize, s_axi_arburst,
s_axi_arlock, s_axi_arcache, s_axi_arprot,
h2f_reset,
1'b0 };
// verilator lint_on UNUSED
endmodule : ps2_hps_bridge_null
+173
View File
@@ -0,0 +1,173 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2025-2026 retroDE contributors
// ============================================================================
// tile_ram_cdc — Ch229 bridge-clock → design-clock tile-RAM shadow
// ============================================================================
// Implements the design-domain side of the Ch229 tile-RAM CDC. Owns a
// 1024 × 32-bit shadow memory in the design clock domain. Bridge-side
// writes arrive as a toggle-based "event" signal plus latched index +
// data; a 2-FF synchronizer + XOR edge detector turns each toggle edge
// into a 1-cycle write pulse against the shadow RAM. Read port is
// purely combinational (the consumer is the Ch245 platform-OSD
// char-BRAM read adapter in the top, which selects high/low 16-bit
// cells from each 32-bit shadow word and feeds them to the platform
// `osd_overlay`. Pre-Ch245 the consumer was the now-retired
// PS2-local `osd_overlay_stub`). No back-pressure — the bridge is assumed
// to space tile writes far enough apart for the sync chain to keep up.
//
// **CDC contract (read carefully before refactoring):**
// - The bridge updates `bclk_wr_toggle`, `bclk_wr_index`, `bclk_wr_data`
// at the same `bclk` edge (one bridge clock cycle).
// - The receiver sees the toggle through a 2-FF synchronizer; the
// edge-detection wire `wr_pulse` fires on the dclk cycle where the
// synchronized toggle has FULLY settled. That guarantees ≥ 2 dclk
// periods of stability on `bclk_wr_index/data` before they're
// sampled into the shadow memory.
// - Multiple bridge writes faster than ~3 dclk periods apart will
// race and may drop or merge events. For the Ch229 use case
// (retrodesd OSD updates at ≤ 1 kHz, design_clk at 2550 MHz),
// this is many orders of magnitude of slack. **Do not** wire a
// fast-cycling source (e.g. a counter) into the bridge's tile
// write path without first replacing this CDC with an async FIFO.
//
// **Reset behavior:**
// - On `breset_n` deasserted: bridge clears `bclk_wr_toggle` to 0
// (matching the receiver's post-reset state). When both domains
// reset together (the normal case on FPGA configure), no spurious
// edge fires after release.
// - On `dreset_n` deasserted: synchronizer chain clears to 0;
// shadow memory contents are NOT cleared (matches Ch227 retention
// semantics — sim `initial` block zeroes for determinism, hardware
// power-up is undefined). The Ch229 contract is "tile RAM survives
// warm reset"; rebooting both sides is a power-cycle scenario and
// the bridge will re-broadcast any written tiles via the next set
// of AXI writes from HPS.
// ============================================================================
`timescale 1ns/1ps
module tile_ram_cdc (
// ---- Bridge clock domain (write port) ----
input logic bclk,
input logic breset_n,
input logic bclk_wr_toggle,
input logic [9:0] bclk_wr_index,
input logic [31:0] bclk_wr_data,
// ---- Design clock domain (read port) ----
input logic dclk,
input logic dreset_n,
input logic [9:0] dclk_rd_index,
output logic [31:0] dclk_rd_data,
// ---- Ch230 design-domain diagnostic counter ----
// Saturating count of "tile writes too close" events — successive
// wr_pulse events fewer than MIN_DCLK_GAP dclk cycles apart.
// Exposed as an output so the top can route it to a reverse-CDC +
// bridge-readable diagnostic register in a future chapter (Ch231+).
// For Ch230 the top leaves it unconnected; the counter still exists
// in the design domain as a synthesis artifact ready for hookup.
output logic [15:0] tile_wr_too_close_count
);
// Shadow RAM lives in the design clock domain. Matched-size with
// the bridge-side `ps2_hps_bridge.tile_mem` (1024 × 32-bit). The
// `ramstyle = "M20K"` attribute (added in the Ch232 hardware
// bring-up hotfix) forces Quartus to use a single M20K block
// instead of distributing the storage across LABs.
(* ramstyle = "M20K" *) logic [31:0] shadow_mem [0:1023];
initial begin
for (int i = 0; i < 1024; i++)
shadow_mem[i] = 32'd0;
end
// 2-FF synchronizer on the bridge toggle into the design clock.
// Three stages let us compute an edge detector against the
// already-resampled bits ([2] ^ [1]), giving the wr_pulse a full
// dclk cycle of bclk_wr_index/data stability before we sample.
logic [2:0] toggle_sync;
always_ff @(posedge dclk or negedge dreset_n) begin
if (!dreset_n)
toggle_sync <= 3'b000;
else
toggle_sync <= {toggle_sync[1:0], bclk_wr_toggle};
end
wire wr_pulse = toggle_sync[2] ^ toggle_sync[1];
// Shadow write port. At the dclk edge where wr_pulse fires,
// sample bclk_wr_index + bclk_wr_data. Both have been stable for
// ≥ 2 dclk cycles by construction of the CDC contract above.
always_ff @(posedge dclk) begin
if (wr_pulse)
shadow_mem[bclk_wr_index] <= bclk_wr_data;
end
// Read port: combinational lookup. The consumer pulls index from
// its pixel position and uses the data to decide overlay vs
// transparent for each pixel.
assign dclk_rd_data = shadow_mem[dclk_rd_index];
// ---- Ch229 / Ch230 tile-write rate watchdog ----
// The CDC contract requires writes to be spaced far enough apart
// that each toggle edge passes through the sync chain cleanly.
// Two consecutive bridge writes that both flip toggle within one
// dclk of each other can be merged into a single transition at
// sync[0] — the first write's bclk_wr_index/bclk_wr_data are
// overwritten before the receiver samples them, and the write is
// silently lost.
//
// The actual minimum gap is ≥ 3 dclk between successive
// wr_pulse events at the receiver:
// - 1 dclk for the synchronizer to fully settle (so the
// second edge is visible as a distinct transition)
// - 1 dclk for the receiver to fire wr_pulse for write 1
// - 1 dclk of margin for jitter / setup time
//
// Production rate enforcer is software-side (retrodesd OSD
// updates at ≤ 1 kHz ≫ 3 dclk @ 25 MHz = 120 ns); the bridge
// does not back-pressure AXI on this constraint. Ch229 added a
// sim-only `$display` warning; Ch230 promotes the gap-tracker to
// a real **saturating counter** (16-bit) exposed as
// `tile_wr_too_close_count` so a future chapter can route it
// through a reverse CDC into a bridge-readable register
// (HDMI_DIAG upper bits or a new diagnostic offset). The
// `$display` aid remains in `\`ifndef SYNTHESIS` for pre-silicon
// log visibility.
localparam int unsigned MIN_DCLK_GAP = 3;
logic [31:0] dclk_since_last_pulse;
wire too_close = wr_pulse && (dclk_since_last_pulse < MIN_DCLK_GAP);
always_ff @(posedge dclk or negedge dreset_n) begin
if (!dreset_n) begin
dclk_since_last_pulse <= 32'hFFFF_FFFF;
tile_wr_too_close_count <= 16'd0;
end else begin
if (wr_pulse)
dclk_since_last_pulse <= 32'd0;
else if (dclk_since_last_pulse != 32'hFFFF_FFFF)
dclk_since_last_pulse <= dclk_since_last_pulse + 32'd1;
if (too_close && (tile_wr_too_close_count != 16'hFFFF))
tile_wr_too_close_count <= tile_wr_too_close_count + 16'd1;
end
end
`ifndef SYNTHESIS
always_ff @(posedge dclk) begin
if (dreset_n && too_close) begin
$display(
"[tile_ram_cdc] WARN time=%0t: tile writes too close - %0d dclk cycles between toggle edges (CDC needs >= %0d for safe sample).",
$time, dclk_since_last_pulse, MIN_DCLK_GAP);
end
end
`endif
// ---- Lint: bclk + breset_n are intentionally referenced ONLY
// via the bclk_wr_toggle path. Tie a placeholder reference
// to silence "unused" warnings on tools that don't trace
// through the upstream toggle source.
// verilator lint_off UNUSED
wire _unused_ok = &{1'b0, bclk, breset_n, 1'b0};
// verilator lint_on UNUSED
endmodule : tile_ram_cdc