Files
retroDE_ps2/rtl/platform/I2C_Controller.v
T
thejayman77 ec82764bef 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>
2026-06-29 20:10:50 -04:00

235 lines
9.6 KiB
Verilog

// ============================================================================
// 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