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