Skip to main content
Back to Templates

I2C 主机控制器I2C Master Controller

基于 alexforencich/verilog-i2c 的简化 I2C 主机 FSM,支持 START/STOP/ACK 完整协议流程。

I2Cmasterverilog
Open Source Notice
License: MIT
Author: Alex Forencich
Copyright (c) 2015-2017 Alex Forencich

Key Points

  • - FSM implements I2C protocol state machine
  • - Prescaler generates SCL clock from system clock
  • - Open-drain SDA/SCL emulation via tri-state
  • - 7-bit addressing with R/W bit

RTL Source

// Copyright (c) 2015-2017 Alex Forencich
// SPDX-License-Identifier: MIT
// Source: https://github.com/alexforencich/verilog-i2c
// Simplified I2C Master FSM for educational use
// Based on i2c_master module from verilog-i2c project
// 中文:简化 I2C 主机 FSM,基于 alexforencich/verilog-i2c 项目

`timescale 1ns / 1ps

// I2C Master FSM Module
// 中文:I2C 主机控制器,实现 START/STOP/ACK 完整协议
// Implements I2C protocol: START, data transfer, ACK/NACK, STOP
module i2c_master_fsm #(
    parameter CLK_FREQ = 50_000_000,  // System clock frequency (Hz)
    parameter I2C_FREQ = 100_000      // I2C clock frequency (Hz)
)(
    input  wire       clk,            // System clock
    input  wire       rst,            // Synchronous reset (active high)
    // Control interface
    input  wire       start,          // Start transaction
    input  wire [6:0] slave_addr,     // 7-bit slave address
    input  wire       rw,             // 0=write, 1=read
    input  wire [7:0] tx_data,        // Data to transmit
    output reg  [7:0] rx_data,        // Received data
    output reg        busy,           // Transaction in progress
    output reg        done,           // Transaction complete
    output reg        error,          // NACK or error occurred
    // I2C bus interface
    output reg        scl_o,          // SCL output (active low via tri-state)
    inout  wire       sda             // SDA bidirectional (active low via tri-state)
);

    // Prescaler calculation for I2C clock generation
    // Quarter period in clock cycles
    localparam PRESCALE = (CLK_FREQ / (4 * I2C_FREQ)) - 1;

    // FSM state encoding
    localparam [3:0] S_IDLE    = 4'd0,  // Idle state
                     S_START   = 4'd1,  // Generate START condition
                     S_ADDR    = 4'd2,  // Send address byte
                     S_ADDR_ACK= 4'd3,  // Wait for ACK after address
                     S_TX      = 4'd4,  // Send data byte
                     S_TX_ACK  = 4'd5,  // Wait for ACK after data
                     S_RX      = 4'd6,  // Receive data byte
                     S_RX_ACK  = 4'd7,  // Send ACK/NACK after receive
                     S_STOP    = 4'd8,  // Generate STOP condition
                     S_ERROR   = 4'd9;  // Error state

    reg [3:0]  state, next_state;
    reg [15:0] prescale_cnt;      // Prescaler counter for SCL timing
    reg [3:0]  bit_cnt;           // Bit counter (0-7 for data bytes)
    reg        scl_en;            // SCL enable
    reg        sda_out;           // SDA output value
    reg [7:0]  shift_reg;         // Shift register for serial data
    reg        sda_oe;            // SDA output enable (active = drive low)
    reg        scl_oe;            // SCL output enable (active = drive low)

    // SCL quarter-period tick
    wire scl_tick = (prescale_cnt >= PRESCALE);

    // Sequential: state register and counters
    always @(posedge clk) begin
        if (rst) begin
            state         <= S_IDLE;
            prescale_cnt  <= 0;
            bit_cnt       <= 0;
            shift_reg     <= 0;
            rx_data       <= 0;
            busy          <= 0;
            done          <= 0;
            error         <= 0;
            scl_oe        <= 0;
            sda_oe        <= 0;
            sda_out       <= 1;
        end else begin
            state <= next_state;
            // Prescaler auto-reload
            if (scl_tick)
                prescale_cnt <= 0;
            else
                prescale_cnt <= prescale_cnt + 1;
        end
    end

    // Combinational: next-state logic and output control
    always @(*) begin
        next_state = state;
        // Default: hold outputs
        scl_oe  = scl_oe;
        sda_oe  = sda_oe;
        sda_out = sda_out;
        busy    = busy;
        done    = 0;
        error   = error;

        case (state)
            S_IDLE: begin
                busy = 0;
                if (start) begin
                    next_state = S_START;
                    busy       = 1;
                    error      = 0;
                end
            end

            S_START: begin
                // START condition: SDA falls while SCL is high
                if (scl_tick) begin
                    sda_out = 0;       // Pull SDA low
                    sda_oe  = 1;
                    next_state = S_ADDR;
                end
            end

            S_ADDR: begin
                // Shift out 7-bit address + R/W bit
                if (scl_tick) begin
                    if (bit_cnt < 7) begin
                        sda_out = slave_addr[6 - bit_cnt];
                        bit_cnt = bit_cnt + 1;
                    end else begin
                        sda_out = rw;
                        next_state = S_ADDR_ACK;
                    end
                end
            end

            S_ADDR_ACK: begin
                // Release SDA and check for ACK (slave pulls low)
                if (scl_tick) begin
                    sda_oe = 0;  // Release SDA
                    if (sda == 1) begin
                        next_state = S_ERROR;  // NACK received
                    end else begin
                        next_state = S_TX;
                    end
                end
            end

            S_TX: begin
                // Shift out 8 data bits (MSB first)
                if (scl_tick) begin
                    if (bit_cnt < 8) begin
                        sda_out = tx_data[7 - bit_cnt];
                        bit_cnt = bit_cnt + 1;
                    end else begin
                        next_state = S_TX_ACK;
                    end
                end
            end

            S_TX_ACK: begin
                // Wait for ACK after data byte
                if (scl_tick) begin
                    sda_oe = 0;
                    if (sda == 1) begin
                        next_state = S_ERROR;
                    end else begin
                        next_state = S_STOP;
                    end
                end
            end

            S_RX: begin
                // Receive 8 data bits (MSB first)
                if (scl_tick) begin
                    sda_oe = 0;  // Release SDA for slave to drive
                    if (bit_cnt < 8) begin
                        bit_cnt = bit_cnt + 1;
                    end else begin
                        next_state = S_RX_ACK;
                    end
                end
            end

            S_RX_ACK: begin
                // Send NACK (master doesn't ACK last byte)
                if (scl_tick) begin
                    sda_out = 1;  // NACK
                    sda_oe  = 1;
                    next_state = S_STOP;
                end
            end

            S_STOP: begin
                // STOP condition: SDA rises while SCL is high
                if (scl_tick) begin
                    sda_oe = 0;  // Release SDA (pulled high externally)
                    next_state = S_IDLE;
                    done       = 1;
                end
            end

            S_ERROR: begin
                error = 1;
                done  = 1;
                busy  = 0;
                next_state = S_IDLE;
            end

            default: next_state = S_IDLE;
        endcase
    end

    // SCL output control (toggle on prescaler ticks)
    always @(posedge clk) begin
        if (rst) begin
            scl_oe <= 0;
        end else if (state != S_IDLE && state != S_ERROR) begin
            if (scl_tick)
                scl_oe <= ~scl_oe;  // Toggle SCL
        end else begin
            scl_oe <= 0;
        end
    end

    // SDA output (active low, open-drain emulation)
    assign sda = sda_oe ? 1'b0 : 1'bz;

endmodule