Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing I2C Communication Protocol on FPGA with Verilog

Tech May 16 1

I2C Bus Fundamentals

The Inter-Integrated Circuit bus (I2C) is a bidirectional two-wire synchronous serial bus that enables communication between integrated circuits. Originally developed by Philips, this serial expansion technology has become ubiquitous in consumer electronics including displays, video equipment, and audio systems. The I2C protocol defines the rules for information exchange between integrated circuits or functional units. The interface utilizes a serial data line (SDA) and a serial clock line (SCL) for data transmission and peripheral device expansion.

Master and Slave Architecture

In I2C communication, the master device orchestrates system task coordination and bus arbitration, while slave devices respond to master commands to perform specific operations. Communication occurs between master and slaves through the shared bus infrastructure.

Data Transfer Rates

The I2C specification supports multiple speed modes: Standard mode at 100 kbit/s, Fast mode at 400 kbit/s, and High-Speed mode reaching 3.4 Mbit/s. Data transfers occur in 8-bit byte units with half-duplex operation, meaning devices alternate between transmit and receive roles.

Device Addressing

Every I2C peripheral possesses a unique device address. Some devices have fixed factory-programmed addresses (e.g., OV7670 image sensor at 0x42), while others provide hardware-configurable address pins allowing user selection during board design (common with I2C EEPROM memory chips).

Protocol Timing Overview

When idle, both SCL and SDA lines remain high due to pull-up resistors. A communication sequence begins when the master generates a START condition by pulling SDA low while SCL is high. Upon detecting this condition, slaves prepare to receive address information. After data transfer completes, the master issues a STOP condition by transitioning SDA from low to high while SCL remains high.

A critical protocol requirement dictates that START and STOP conditions occur exclusively during SCL high periods. Consequently, all data line transitions must happen when SCL is low, with data sampling occurring when SCL is high. This constraint ensures reliable synchronization between communicating devices.

Tri-State Buffer Implementation

The SDA line operates bidirectionally. During master-to-slave transmissions (addresses, data, START/STOP signals), the master drives the line as an output. When slaves transmit acknowledgment bits or data during read operations, the master releases the line to high-impedance state, allowing slaves to drive it as inputs. A tri-state buffer manages this direction switching.

Single-Byte Write Sequence

Writing one byte to an EEPROM (24C64) requires the following sequence:

  1. Generate START condition
  2. Transmit device write address (0xA0)
  3. Receive and verify acknowledgment from EEPROM
  4. Send high byte of memory address
  5. Receive acknowledgment
  6. Send low byte of memory address
  7. Receive acknowledgment
  8. Transmit 8-bit data payload
  9. Receive acknowledgment
  10. Generate STOP condition

Single-Byte Read Sequence

Reading one byte from EEPROM follows this procedure:

  1. Generate START condition
  2. Transmit device write address (0xA0)
  3. Receive acknowledgment
  4. Send high byte of memory address
  5. Receive acknowledgment
  6. Send low byte of memory address
  7. Receive acknowledgment
  8. Generate repeated START condition
  9. Transmit device read address (0xA1)
  10. Receive acknowledgment
  11. Read data byte from EEPROM
  12. Send NACK to indicate read completion
  13. Generate STOP condition

Signal Timing Specifications

The START signal requires SDA to transition high-to-low while SCL remains high. The STOP signal requires SDA to trensition low-to-high during SCL high. Acknowledgment detection involves monitoring SDA—low during SCL high indicates ACK, while high indicates NACK.

For the 24C64 EEPROM, device addresses use the format 1010_000X where X=0 for writes and X=1 for reads. The 64Kbit storage capacity requires 13-bit addressing, extended to 16 bits (high and low bytes) per I2C protocol constraints.

Verilog Implementation

I2C Controller Module

module i2c_controller #(
    parameter SYS_CLK_FREQ = 50_000_000,
    parameter SCL_FREQ = 400_000,
    parameter DEVICE_ADDR = 7'b1010_000
)(
    input wire clk,
    input wire rst_n,
    
    input wire wr_req,
    input wire rd_req,
    input wire start_pulse,
    input wire addr_width_sel,
    input wire [15:0] mem_addr,
    input wire [7:0] tx_byte,
    
    output reg i2c_done,
    output reg [7:0] rx_byte,
    output wire scl_out,
    inout wire sda_io
);

    localparam SCL_DIVIDER = SYS_CLK_FREQ / SCL_FREQ;
    localparam HALF_PERIOD = SCL_DIVIDER >> 1;
    
    // State machine definitions
    localparam [4:0]
        ST_IDLE         = 5'd0,
        ST_START        = 5'd1,
        ST_DEV_ADDR_W   = 5'd2,
        ST_ACK1         = 5'd3,
        ST_ADDR_H       = 5'd4,
        ST_ACK2         = 5'd5,
        ST_ADDR_L       = 5'd6,
        ST_ACK3         = 5'd7,
        ST_WRITE_DATA   = 5'd8,
        ST_ACK4         = 5'd9,
        ST_RESTART      = 5'd10,
        ST_DEV_ADDR_R   = 5'd11,
        ST_ACK5         = 5'd12,
        ST_READ_DATA    = 5'd13,
        ST_SEND_NACK    = 5'd14,
        ST_STOP         = 5'd15;
    
    reg [4:0] curr_state, next_state;
    reg [7:0] clk_count;
    reg scl_clock;
    reg sda_drive_en;
    reg sda_out_reg;
    reg [3:0] bit_index;
    reg [7:0] shift_reg;
    
    // SCL clock generation
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_count <= 8'd0;
            scl_clock <= 1'b1;
        end else if (clk_count == SCL_DIVIDER - 1) begin
            clk_count <= 8'd0;
            scl_clock <= 1'b1;
        end else if (clk_count == HALF_PERIOD - 1) begin
            clk_count <= clk_count + 1'b1;
            scl_clock <= 1'b0;
        end else begin
            clk_count <= clk_count + 1'b1;
        end
    end
    
    // Clock phase markers
    wire scl_high_mid = (clk_count == HALF_PERIOD);
    wire scl_low_mid = (clk_count == SCL_DIVIDER - 1);
    
    // State transition on SCL falling edge
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            curr_state <= ST_IDLE;
        else if (scl_low_mid)
            curr_state <= next_state;
    end
    
    // Next state logic
    always @(*) begin
        next_state = ST_IDLE;
        case (curr_state)
            ST_IDLE: begin
                if (start_pulse)
                    next_state = ST_START;
                else
                    next_state = ST_IDLE;
            end
            
            ST_START: begin
                if (scl_low_mid)
                    next_state = ST_DEV_ADDR_W;
                else
                    next_state = ST_START;
            end
            
            ST_DEV_ADDR_W: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_ACK1;
                else
                    next_state = ST_DEV_ADDR_W;
            end
            
            ST_ACK1: begin
                if (scl_low_mid) begin
                    if (addr_width_sel)
                        next_state = ST_ADDR_H;
                    else
                        next_state = ST_ADDR_L;
                end else
                    next_state = ST_ACK1;
            end
            
            ST_ADDR_H: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_ACK2;
                else
                    next_state = ST_ADDR_H;
            end
            
            ST_ACK2: begin
                if (scl_low_mid)
                    next_state = ST_ADDR_L;
                else
                    next_state = ST_ACK2;
            end
            
            ST_ADDR_L: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_ACK3;
                else
                    next_state = ST_ADDR_L;
            end
            
            ST_ACK3: begin
                if (scl_low_mid) begin
                    if (wr_req)
                        next_state = ST_WRITE_DATA;
                    else if (rd_req)
                        next_state = ST_RESTART;
                    else
                        next_state = ST_ACK3;
                end else
                    next_state = ST_ACK3;
            end
            
            ST_WRITE_DATA: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_ACK4;
                else
                    next_state = ST_WRITE_DATA;
            end
            
            ST_ACK4: begin
                if (scl_low_mid)
                    next_state = ST_STOP;
                else
                    next_state = ST_ACK4;
            end
            
            ST_RESTART: begin
                if (scl_low_mid)
                    next_state = ST_DEV_ADDR_R;
                else
                    next_state = ST_RESTART;
            end
            
            ST_DEV_ADDR_R: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_ACK5;
                else
                    next_state = ST_DEV_ADDR_R;
            end
            
            ST_ACK5: begin
                if (scl_low_mid)
                    next_state = ST_READ_DATA;
                else
                    next_state = ST_ACK5;
            end
            
            ST_READ_DATA: begin
                if (bit_index == 4'd8 && scl_low_mid)
                    next_state = ST_SEND_NACK;
                else
                    next_state = ST_READ_DATA;
            end
            
            ST_SEND_NACK: begin
                if (scl_low_mid)
                    next_state = ST_STOP;
                else
                    next_state = ST_SEND_NACK;
            end
            
            ST_STOP: begin
                if (scl_low_mid)
                    next_state = ST_IDLE;
                else
                    next_state = ST_STOP;
            end
            
            default: next_state = ST_IDLE;
        endcase
    end
    
    // Output and control logic
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sda_out_reg <= 1'b1;
            sda_drive_en <= 1'b1;
            bit_index <= 4'd0;
            shift_reg <= 8'd0;
            rx_byte <= 8'd0;
            i2c_done <= 1'b0;
        end else if (scl_low_mid) begin
            case (next_state)
                ST_IDLE: begin
                    sda_out_reg <= 1'b1;
                    sda_drive_en <= 1'b1;
                    i2c_done <= 1'b0;
                end
                
                ST_START: begin
                    bit_index <= 4'd0;
                    sda_out_reg <= 1'b0;
                    sda_drive_en <= 1'b1;
                    shift_reg <= {DEVICE_ADDR, 1'b0};
                end
                
                ST_DEV_ADDR_W: begin
                    bit_index <= bit_index + 1'b1;
                    sda_out_reg <= shift_reg[7 - bit_index];
                    sda_drive_en <= 1'b1;
                end
                
                ST_ACK1: begin
                    bit_index <= 4'd0;
                    sda_drive_en <= 1'b0;
                    shift_reg <= mem_addr[15:8];
                end
                
                ST_ADDR_H: begin
                    bit_index <= bit_index + 1'b1;
                    sda_out_reg <= shift_reg[7 - bit_index];
                    sda_drive_en <= 1'b1;
                end
                
                ST_ACK2: begin
                    bit_index <= 4'd0;
                    sda_drive_en <= 1'b0;
                    shift_reg <= mem_addr[7:0];
                end
                
                ST_ADDR_L: begin
                    bit_index <= bit_index + 1'b1;
                    sda_out_reg <= shift_reg[7 - bit_index];
                    sda_drive_en <= 1'b1;
                end
                
                ST_ACK3: begin
                    bit_index <= 4'd0;
                    sda_drive_en <= 1'b0;
                    shift_reg <= tx_byte;
                end
                
                ST_WRITE_DATA: begin
                    bit_index <= bit_index + 1'b1;
                    sda_out_reg <= shift_reg[7 - bit_index];
                    sda_drive_en <= 1'b1;
                end
                
                ST_ACK4: begin
                    bit_index <= 4'd0;
                    sda_drive_en <= 1'b0;
                end
                
                ST_RESTART: begin
                    bit_index <= 4'd0;
                    sda_out_reg <= 1'b1;
                    sda_drive_en <= 1'b1;
                end
                
                ST_DEV_ADDR_R: begin
                    bit_index <= bit_index + 1'b1;
                    sda_out_reg <= shift_reg[7 - bit_index];
                    sda_drive_en <= 1'b1;
                end
                
                ST_ACK5: begin
                    bit_index <= 4'd0;
                    sda_drive_en <= 1'b0;
                end
                
                ST_READ_DATA: begin
                    bit_index <= bit_index + 1'b1;
                end
                
                ST_SEND_NACK: begin
                    sda_out_reg <= 1'b1;
                    sda_drive_en <= 1'b1;
                end
                
                ST_STOP: begin
                    sda_out_reg <= 1'b1;
                    sda_drive_en <= 1'b1;
                end
            endcase
        end
    end
    
    // Data reception on SCL rising edge
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rx_byte <= 8'd0;
        end else if (scl_high_mid) begin
            if (curr_state == ST_READ_DATA)
                rx_byte[7 - bit_index] <= sda_io;
        end
    end
    
    // Completion flag
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            i2c_done <= 1'b0;
        else if (curr_state == ST_STOP && scl_low_mid)
            i2c_done <= 1'b1;
        else
            i2c_done <= 1'b0;
    end
    
    // SCL output control
    assign scl_out = ((curr_state >= ST_DEV_ADDR_W && curr_state <= ST_SEND_NACK)) 
                      ? scl_clock : 1'b1;
    
    // SDA tri-state control
    assign sda_io = sda_drive_en ? sda_out_reg : 1'bz;
    
endmodule

Verification Testbench

`timescale 1ns/1ns

module i2c_controller_tb;
    reg clk;
    reg rst_n;
    reg wr_req;
    reg rd_req;
    reg start_pulse;
    reg addr_width_sel;
    reg [15:0] mem_addr;
    reg [7:0] tx_byte;
    wire i2c_done;
    wire [7:0] rx_byte;
    wire scl_out;
    wire sda_io;
    
    // Simulated EEPROM slave
    reg [7:0] eeprom_mem [0:255];
    integer i;
    initial begin
        for (i = 0; i < 256; i = i + 1)
            eeprom_mem[i] = 8'h00;
        eeprom_mem[8'h5D] = 8'h35;
    end
    
    // Simple I2C slave model
    reg [7:0] slave_addr;
    reg [15:0] slave_mem_addr;
    reg [7:0] slave_data;
    reg [3:0] slave_bit_cnt;
    reg slave_write_mode;
    reg [2:0] slave_state;
    
    always @(negedge scl_out) begin
        if (rst_n) begin
            if (slave_state == 0) begin
                if (!sda_io)
                    slave_state <= 1;
            end else if (slave_state == 1) begin
                slave_bit_cnt <= slave_bit_cnt + 1;
                slave_addr <= {slave_addr[6:0], sda_io};
                if (slave_bit_cnt == 7)
                    slave_state <= 2;
            end else if (slave_state == 2) begin
                slave_state <= 3;
            end
        end
    end
    
    // I2C controller instantiation
    i2c_controller #(
        .SYS_CLK_FREQ(50_000_000),
        .SCL_FREQ(400_000),
        .DEVICE_ADDR(7'b1010_000)
    ) uut (
        .clk(clk),
        .rst_n(rst_n),
        .wr_req(wr_req),
        .rd_req(rd_req),
        .start_pulse(start_pulse),
        .addr_width_sel(addr_width_sel),
        .mem_addr(mem_addr),
        .tx_byte(tx_byte),
        .i2c_done(i2c_done),
        .rx_byte(rx_byte),
        .scl_out(scl_out),
        .sda_io(sda_io)
    );
    
    // Pull-up for SDA
    pullup (sda_io);
    
    // Clock generation
    initial begin
        clk = 0;
        forever #10 clk = ~clk;
    end
    
    // Test sequence
    initial begin
        rst_n = 0;
        wr_req = 0;
        rd_req = 0;
        start_pulse = 0;
        addr_width_sel = 1;
        mem_addr = 16'd0;
        tx_byte = 8'd0;
        slave_state = 0;
        slave_bit_cnt = 0;
        
        #100;
        rst_n = 1;
        #100;
        
        // Write operation to address 0x5D
        @(posedge clk);
        wr_req = 1;
        start_pulse = 1;
        addr_width_sel = 1;
        mem_addr = 16'h005D;
        tx_byte = 8'hA5;
        #20;
        start_pulse = 0;
        wr_req = 0;
        
        wait(i2c_done);
        #1000;
        
        // Read operation from address 0x5D
        @(posedge clk);
        rd_req = 1;
        start_pulse = 1;
        addr_width_sel = 1;
        mem_addr = 16'h005D;
        #20;
        start_pulse = 0;
        rd_req = 0;
        
        wait(i2c_done);
        #1000;
        
        $display("Read data: %h", rx_byte);
        $finish;
    end
    
endmodule

Timing Verification Notes

Successful I2C implementations require precise timing control. The START condition must pull SDA low during SCL high, and the STOP condition must release SDA high during SCL high. Data changes should occur on SCL low transitions, with stable data sampling during SCL high periods. The midpoint of each SCL phase provides optimal sampling and transition windows for maximum timing margin.

The tri-state buffer control logic determines whether the FPGA drives SDA (during adress transmission, ACK generation, and data transmission as master) or releases it for slave response (during slave ACK and read data reception). Proper synchronization between SCL clock generation and state machine progression ensures reliable communication across various I2C slave devices.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.