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,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
|
||||
Executable
+236
@@ -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
|
||||
@@ -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.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/home/ubuntu/FPGA_Projects/retroDE_splash/rtl/platform/cp437_8x8.mem
|
||||
@@ -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
@@ -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
|
||||
@@ -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 25–50 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
|
||||
Reference in New Issue
Block a user