preloader

I2C Protocol Verilog Implementation using FSM

NameI2C Protocol
DescriptionVerilog Implementation of I2C Protocol using Finite State Machine (FSM) design
Start06 Nov 2024
RepositoryI2CV🔗
TypeSOLO
LevelBeginner
SkillsHDL, Protocols, Programming
Tools UsedVerilog, Icarus, Xilinx
Current StatusOn Hold

This project implements the I2C protocol in Verilog with various versions and configurations. Below is a summary of each version:

v1.0.2 - Simple Master-Slave (No Clock Stretching)

  • Description: This version implements a simple I2C protocol with one master and one slave device. It does not support clock stretching.
  • Features:
    • Basic I2C communication between a single master and slave.
    • No handling of clock stretching; both master and slave devices operate with the same clock frequency.
    • NACK will be raised indicating that the given address for slave is wrong.

v2.0.3 - Clock Stretching with Fixed Master Delay

  • Description: This version adds support for clock stretching. The master introduces a fixed delay after sending each data frame, and the SCL line will be held low (clock stretching) while waiting for the slave.
  • Features:
    • Clock stretching is supported for managing communication timing.
    • The master introduces a fixed delay to simulate real-world clock stretching scenarios.

v2.0.4 - Clock Stretching with Configurable Master Delay

  • Description: This version builds on v2.0.3 by adding a configurable delay from the testbench. The delay allows adjusting the time period that the SCL line waits, providing greater flexibility.
  • Features:
    • Configurable master delay, adjustable from the testbench.
    • SCL waiting period comparison between the master and slave devices.
    • Enhanced clock stretching handling with configurable delays.

v3.0.1 - Multi-Slave Single Master Configuration

  • Description: This version introduces a configuration with a single master and multiple slave devices. The master can communicate with any of the slaves, supporting multi-slave communication.
  • Features:
    • One master can communicate with multiple slaves.
    • The master can address and select any slave for communication.
    • Basic multi-slave handling without clock stretching.

v3.1.1 - Multi-Slave Multi-Master Configuration

  • Description: The latest version supports a multi-master, multi-slave configuration, where both the master and slaves can initiate communication. It adds complexity to handle multiple devices that can take control of the bus.
  • Features:
    • Multiple masters can communicate with multiple slaves.
    • Advanced bus arbitration is implemented to handle multiple masters trying to access the bus at the same time.
    • Suitable for complex systems requiring both multiple masters and multiple slaves.

I2C combines the strengths of both UART and SPI. It operates using just two wires, like asynchronous serial, yet supports communication with up to 1,008 peripheral devices. Unlike SPI, I2C accommodates multi-controller systems, allowing more than one controller to communicate with all peripheral devices on the bus (although the controllers must take turns using the bus lines).

I2C data rates fall between those of asynchronous serial and SPI, with most devices communicating at 100 kHz or 400 kHz. While there is some overhead—requiring one additional acknowledgment (ACK/NACK) bit for every 8 bits of data transmitted—I2C remains efficient. Although implementing I2C requires more complex hardware than SPI, it is still simpler than asynchronous serial and can be easily realized in software.

Block Diagram of an I2C System
Block Diagram of an I2C System

Physical layer

Two-Wire Communication

An I2C system utilizes two shared communication lines for all devices on the bus. These two lines facilitate bidirectional, half-duplex communication. I2C supports multiple controllers and multiple target devices, making it a flexible choice for various applications. It is essential to use pull-up resistors on both of these lines to ensure proper operation. Fig 2.4{reference-type=“ref” reference=“fig:i2c_implementation”} shows a typical implementation of the I2C physical layer.

Typical I2C Implementation
Typical I2C Implementation

One of the main reasons that I2C is a widely adopted protocol is due to its requirement of only two lines for communication. The first line, SCL, is the serial clock line, primarily controlled by the controller device. SCL is responsible for synchronously clocking data in or out of the target device. The second line, SDA, is the serial data line, used to transmit data to or from the target devices. For instance, a controller device can send configuration data and output codes to a target digital-to-analog converter (DAC), or a target analog-to-digital converter (ADC) can send conversion data back to the controller device.

I2C operates as a half-duplex communication protocol, meaning that only one controller or target device can send data on the bus at any given time. In contrast, the Serial Peripheral Interface (SPI) is a full-duplex protocol that allows data to be sent and received simultaneously, requiring four lines for communication: two data lines for sending and receiving data, along with a serial clock and a unique SPI chip select line to select the device for communication.

An I2C controller device initiates and terminates communication, which eliminates potential issues related to bus contention. Communication with a target device is established through a unique address on the bus, allowing multiple controllers and multiple target devices to coexist on the I2C bus.

The SDA and SCL lines have an open-drain connection to all devices on the bus, necessitating a pull-up resistor connected to a common voltage supply.

Open-Drain Connection

The open-drain connections are employed on both the SDA and SCL lines and are linked to an NMOS transistor. This open-drain configuration manages the I2C communication line by either pulling the line low or allowing it to rise to a high state. The term "open-drain" refers to the NMOS bus connection when the NMOS is turned OFF. Figure 2.5{reference-type=“ref” reference=“fig:open_drain_connection”} illustrates the open-drain connection when the NMOS is turned ON.

Open-Drain Connection Pulls Line Low When NMOS is Turned On
Open-Drain Connection Pulls Line Low When NMOS is Turned On

To establish the voltage level of the SDA or SCL line, the NMOS transistor is either switched ON or OFF. When the NMOS is ON, it allows current to flow through the resistor to ground, effectively pulling the open-drain line low. This transition from high to low is typically rapid, as the NMOS quickly discharges any capacitance on the SDA or SCL lines.

When the NMOS turns OFF, the device ceases to pull current, and the pull-up resistor subsequently raises the SDA or SCL line back to VDD. Figure 2.6{reference-type=“ref” reference=“fig:open_drain_off”} shows the open-drain line when the NMOS is turned OFF, illustrating how the pull-up resistor brings the line high.

Open-Drain Line with NMOS Turned Off
Open-Drain Line with NMOS Turned Off

The transition of the open-drain line to a high state is slower because the line is pulled up against the bus capacitance, rather than being actively driven high.

I2C Protocol

Communication over I2C requires a specific signaling protocol to ensure that devices on the bus recognize valid I2C transmissions. While this process is more intricate than UART or SPI, most I2C-compatible devices handle the finer protocol details internally, allowing developers to focus primarily on data exchange.

SDA and SCL Lines: The I2C bus operates with two main lines: SDA (Serial Data Line) and SCL (Serial Clock Line). Data is transmitted over the SDA line in sync with clock pulses on the SCL line. Generally, data is placed on SDA when SCL is low, and devices sample this data when SCL goes high. If needed, multiple internal registers may control data handling, especially in complex devices.

Protocol Components:

1. Start Condition: To initiate communication, the controller sets SCL high and then pulls SDA low. This signals all peripheral devices on the bus that a transmission is starting. In cases where multiple controllers attempt to start communication simultaneously, the first device to pull SDA low gains control. If necessary, the controller can issue repeated start conditions to maintain bus control without releasing it.

2. Address Frame: Every I2C transmission begins with an address frame to specify the target peripheral. This frame consists of a 7-bit address, sent MSB (most significant bit) first, followed by a R/W bit indicating the operation type (read or write).

After this, the 9th bit, known as the ACK/NACK bit, is used by the receiving device to confirm reception. If the device pulls SDA low before the 9th clock pulse (ACK), communication continues. If not (NACK), it indicates either unrecognized data or an issue in reception, prompting the controller to decide the next steps.

3. Data Frames: Following the address frame, one or more data frames are sent over the SDA line. Each data frame is 8 bits, and data is transferred from the controller to the peripheral or vice versa, based on the R/W bit in the address frame.

Many peripheral devices have auto-incrementing internal registers, enabling data to continue from consecutive registers without the need to re-specify the register address.

4. Stop Condition: The controller ends communication by generating a stop condition. This is done by transitioning SDA from low to high after a high-to-low transition on SCL, with SCL held high during the stop sequence. To avoid false stop conditions, the value on SDA should not change while SCL is high during regular data transmission.

The I2C protocol divides communication into structured frames. Each communication sequence begins with a START condition, initiated by the controller, followed by an address frame and then one or more data frames. Every frame also includes an acknowledgment (ACK) bit, signaling that the frame has been received successfully by the intended device. Figure 3-3 illustrates the structure of two I2C communication frames, showing both address and data frames in detail.

I2C Address and Data Frames
I2C Address and Data Frames

In an I2C transaction, the controller first sends a START condition by pulling the SDA line low, followed by the SCL line. This sequence asserts control over the bus, preventing other devices from interfering. Each target device on the I2C bus has a unique 7-bit address, allowing the controller to specify which target device it intends to communicate with.

Once the address is set on SDA while SCL acts as the clock, the 8th bit (R/W bit) indicates the intended operation type: read (1) or write (0). This initial address and R/W bit are followed by an ACK bit, sent by the target device to confirm receipt. If the target device receives the address successfully, it pulls SDA low during the next SCL pulse, signaling an ACK. If no device acknowledges, the line remains high, signaling a NACK.

After the address frame, one or more data frames follow. Each data frame contains 8 bits of data, which are acknowledged (ACK) in the 9th bit. If the data frame is a write operation, the target device pulls SDA low to confirm data receipt. For read operations, the controller pulls SDA low to acknowledge receipt of the data. The presence or absence of the ACK is essential for troubleshooting, as a missing ACK may indicate an addressing error or transmission failure.

Finally, the communication ends with a STOP condition, where the controller releases SCL first, followed by SDA. This action releases the I2C bus for other devices to use, completing the communication cycle.

This structured protocol allows for the transmission of multiple bytes within one communication sequence. In cases where a target device has multiple internal registers, a write operation can specify the register to read or write data to, enhancing flexibility and enabling complex data transactions.

Module Specifications

MASTER MODULE

C0d3
`timescale 1ns / 1ps

// Main module declaration
module i2c_master(
    input wire clk,                // System clock
    input wire rst,                // Reset signal
    input wire [6:0] addr,         // 7-bit I2C slave address
    input wire [7:0] data_in,      // Data to send to slave in write mode
    input wire enable,             // Start signal for I2C communication
    input wire rw,                 // Read/Write control (0 for write, 1 for read)
    output reg [7:0] data_out,     // Data received from slave in read mode
    output wire ready,             // Indicates when the master is ready for a new transaction
    inout i2c_sda,                 // I2C data line (SDA) - bidirectional
    inout wire i2c_scl             // I2C clock line (SCL) - bidirectional
);

    // Define states for I2C master FSM
    localparam IDLE = 0;
    localparam START = 1;
    localparam ADDRESS = 2;
    localparam READ_ACK = 3;
    localparam WRITE_DATA = 4;
    localparam WRITE_ACK = 5;
    localparam READ_DATA = 6;
    localparam READ_ACK2 = 7;
    localparam STOP = 8;

    localparam DIVIDE_BY = 4;      // Clock divider to generate I2C clock from system clock

    reg [7:0] state;               // Current state of the FSM
    reg [7:0] saved_addr;          // Stores the 7-bit address and RW bit for the current transaction
    reg [7:0] saved_data;          // Data to be sent in write transactions
    reg [7:0] counter;             // Bit counter for data/address transmission
    reg [7:0] counter2 = 0;        // Divider counter for generating i2c_clk
    reg write_enable;              // Controls whether the master drives SDA line
    reg sda_out;                   // Data to output on SDA line when write_enable is 1
    reg i2c_scl_enable = 0;        // Controls the state of the i2c_scl line (enabled or high)
    reg i2c_clk = 1;               // Internal I2C clock signal

    // Ready signal is high when the master is idle and not in reset
    assign ready = ((rst == 0) && (state == IDLE)) ? 1 : 0;

    // I2C SCL signal: High when i2c_scl_enable is low; otherwise, driven by i2c_clk
    assign i2c_scl = (i2c_scl_enable == 0) ? 1 : i2c_clk;

    // SDA line is driven by sda_out when write_enable is high; otherwise, it's in high-impedance
    assign i2c_sda = (write_enable == 1) ? sda_out : 'bz;

    // I2C clock divider: Divides system clock to generate i2c_clk
    always @(posedge clk) begin
        if (counter2 == (DIVIDE_BY / 2) - 1) begin
            i2c_clk <= ~i2c_clk;    // Toggle i2c_clk when half period is reached
            counter2 <= 0;          // Reset the divider counter
        end else begin
            counter2 <= counter2 + 1; // Increment the divider counter
        end
    end

    // Enable/disable I2C clock based on current state
    always @(negedge i2c_clk or posedge rst) begin
        if (rst == 1) begin
            i2c_scl_enable <= 0;    // Disable SCL on reset
        end else begin
            if ((state == IDLE) || (state == START) || (state == STOP)) begin
                i2c_scl_enable <= 0; // SCL is disabled in IDLE, START, and STOP states
            end else begin
                i2c_scl_enable <= 1; // Enable SCL in other states
            end
        end
    end

    // State machine for controlling the I2C master operation
    always @(posedge i2c_clk or posedge rst) begin
        if (rst == 1) begin
            state <= IDLE;          // Reset state to IDLE on reset
        end else begin
            case (state)
                
                IDLE: begin
                    if (enable) begin
                        state <= START;  // Start I2C transaction when enable is high
                        saved_addr <= {addr, rw};  // Save the 7-bit address and RW bit
                        saved_data <= data_in;     // Save the data to be sent (in write mode)
                    end
                end

                START: begin
                    counter <= 7;          // Initialize bit counter to 7 for 8-bit transmission
                    state <= ADDRESS;      // Move to ADDRESS state
                end

                ADDRESS: begin
                    if (counter == 0) begin 
                        state <= READ_ACK;  // Move to ACK check after sending address and RW bit
                    end else begin
                        counter <= counter - 1;  // Transmit address bits, count down
                    end
                end

                READ_ACK: begin
                    if (i2c_sda == 0) begin  // ACK received (SDA pulled low by slave)
                        counter <= 7;       // Reset bit counter
                        if (saved_addr[0] == 0) state <= WRITE_DATA; // If RW=0, go to write mode
                        else state <= READ_DATA;                     // If RW=1, go to read mode
                    end else begin
                        state <= STOP;      // NACK received, move to STOP state
                    end
                end

                WRITE_DATA: begin
                    if (counter == 0) begin
                        state <= READ_ACK2; // Move to second ACK check after data transmission
                    end else begin
                        counter <= counter - 1; // Transmit data bits, count down
                    end
                end

                READ_ACK2: begin
                    if ((i2c_sda == 0) && (enable == 1)) state <= IDLE; // Return to IDLE on ACK
                    else state <= STOP;  // If NACK received or enable low, go to STOP
                end

                READ_DATA: begin
                    data_out[counter] <= i2c_sda;  // Capture data bit from SDA line
                    if (counter == 0) state <= WRITE_ACK; // After last bit, go to WRITE_ACK
                    else counter <= counter - 1; // Count down for each bit received
                end

                WRITE_ACK: begin
                    state <= STOP;  // Go to STOP after sending ACK
                end

                STOP: begin
                    state <= IDLE;  // Go back to IDLE after STOP condition
                end
            endcase
        end
    end

    // SDA output logic based on the current state
    always @(negedge i2c_clk or posedge rst) begin
        if (rst == 1) begin
            write_enable <= 1;       // Drive SDA high on reset
            sda_out <= 1;
        end else begin
            case (state)
                
                START: begin
                    write_enable <= 1;  // Enable SDA for start condition
                    sda_out <= 0;       // Pull SDA low for start condition
                end
                
                ADDRESS: begin
                    sda_out <= saved_addr[counter]; // Send each bit of the address and RW bit
                end
                
                READ_ACK: begin
                    write_enable <= 0;  // Release SDA to allow slave to drive ACK/NACK
                end
                
                WRITE_DATA: begin 
                    write_enable <= 1;  // Enable SDA for data transmission
                    sda_out <= saved_data[counter]; // Output each bit of data to SDA
                end
                
                WRITE_ACK: begin
                    write_enable <= 1;  // Enable SDA for ACK transmission
                    sda_out <= 0;       // Send ACK by pulling SDA low
                end
                
                READ_DATA: begin
                    write_enable <= 0;  // Release SDA to read data from slave
                end
                
                STOP: begin
                    write_enable <= 1;  // Enable SDA for stop condition
                    sda_out <= 1;       // Release SDA to indicate stop
                end
            endcase
        end
    end

endmodule
Explanation

The provided code is a Verilog implementation of an I2C Master Module. This module enables communication with I2C-compatible devices through the I2C protocol by implementing the necessary operations to generate I2C signals and manage data transfer. Let’s break down each section of the code:

Module Declaration

The code begins with the module declaration:

module i2c_master(
    input wire clk,               // System clock
    input wire rst,               // Synchronous reset
    input wire [6:0] addr,        // 7-bit I2C address
    input wire [7:0] data_in,     // Data to be transmitted
    input wire enable,            // Enable signal to start I2C transaction
    input wire rw,                // Read/Write control (0 = Write, 1 = Read)
    output reg [7:0] data_out,    // Data received from I2C
    output wire ready,            // Ready signal when module is idle
    inout i2c_sda,                // I2C data line (SDA)
    inout wire i2c_scl            // I2C clock line (SCL)
);

This module contains inputs for the system clock (clk), reset (rst), I2C address (addr), data to be sent (data_in), an enable signal (enable), and a Read/Write control (rw). It also provides outputs for data received (data_out), a ready status signal (ready), and bidirectional I2C lines, i2c_sda and i2c_scl.

State Machine Definition

The code defines several states representing stages in the I2C transaction:

localparam IDLE = 0;
localparam START = 1;
localparam ADDRESS = 2;
localparam READ_ACK = 3;
localparam WRITE_DATA = 4;
localparam WRITE_ACK = 5;
localparam READ_DATA = 6;
localparam READ_ACK2 = 7;
localparam STOP = 8;

Each localparam corresponds to a state in the Finite State Machine (FSM), controlling the I2C protocol flow, including start, address transmission, acknowledgment (ACK) reception, data transfer, and stop condition generation.

Clock Divider

To generate a slower clock for the I2C operations, a clock divider is implemented:

always @(posedge clk) begin
    if (counter2 == (DIVIDE_BY / 2) - 1) begin
        i2c_clk <= ~i2c_clk;
        counter2 <= 0;
    end else counter2 <= counter2 + 1;
end

This block toggles i2c_clk at a lower frequency than the system clock, clk, using a counter counter2 with a division factor defined by DIVIDE_BY.

SDA and SCL Control

To control the i2c_sda and i2c_scl lines based on the module’s state:

assign ready = ((rst == 0) && (state == IDLE)) ? 1 : 0;
assign i2c_scl = (i2c_scl_enable == 0) ? 1 : i2c_clk;
assign i2c_sda = (write_enable == 1) ? sda_out : 'bz;
  • ready is high when the reset is inactive and the state is IDLE.

  • i2c_scl is either high (idle state) or follows the divided i2c_clk signal.

  • i2c_sda outputs the value of sda_out when write_enable is active. When write_enable is inactive, i2c_sda goes to high-impedance (’bz) for reading data.

Finite State Machine (FSM)

The FSM controls the I2C communication process, progressing through states based on the I2C protocol requirements:

always @(posedge i2c_clk or posedge rst) begin
    if (rst == 1) begin
        state <= IDLE;
    end else begin
        case (state)
            IDLE: begin
                if (enable) begin
                    state <= START;
                    saved_addr <= {addr, rw};
                    saved_data <= data_in;
                end
            end
            START: begin
                counter <= 7;
                state <= ADDRESS;
            end
            ...
            STOP: begin
                state <= IDLE;
            end
        endcase
    end
end

Each state corresponds to an I2C operation:

  • IDLE: Waits for enable signal to initiate communication.

  • START: Prepares a start condition by asserting sda_out low.

  • ADDRESS: Sends the address and R/W bit.

  • READ_ACK and READ_ACK2: Verifies acknowledgment (ACK) from the slave.

  • WRITE_DATA and WRITE_ACK: Transfers data to the slave and waits for ACK.

  • READ_DATA: Receives data from the slave.

  • STOP: Generates a stop condition and returns to IDLE.

SDA Output Logic

The logic for controlling the i2c_sda line, depending on the FSM state, is implemented as follows:

always @(negedge i2c_clk or posedge rst) begin
    if (rst == 1) begin
        write_enable <= 1;
        sda_out <= 1;
    end else begin
        case (state)
            START: begin
                write_enable <= 1;
                sda_out <= 0;
            end
            ADDRESS: begin
                sda_out <= saved_addr[counter];
            end
            ...
            STOP: begin
                write_enable <= 1;
                sda_out <= 1;
            end
        endcase
    end
end
  • In the START state, sda_out goes low to generate a start condition.

  • In the ADDRESS and WRITE_DATA states, sda_out sends the bits of saved_addr or saved_data.

  • In STOP, sda_out goes high to signify the end of the transmission.

This Verilog module effectively implements an I2C Master communication sequence by controlling the i2c_sda and i2c_scl lines according to the I2C protocol.

SLAVE MODULE

C0d3
module i2c_slave(
    input [6:0] addr_in,   // Slave address to respond to (dynamic address input)
    inout sda,             // I2C data line (SDA) - bidirectional
    inout scl              // I2C clock line (SCL)
);

    // Define states for the I2C slave FSM
    localparam READ_ADDR = 0;   // State for reading the address from the master
    localparam SEND_ACK = 1;    // State for sending ACK after receiving a matching address
    localparam READ_DATA = 2;   // State for reading data from the master
    localparam WRITE_DATA = 3;  // State for sending data to the master
    localparam SEND_ACK2 = 4;   // State for sending ACK after receiving data from the master

    reg [7:0] addr;             // Register to store the address received from the master
    reg [7:0] counter;          // Bit counter for data/address transmission
    reg [7:0] state = 0;        // Current state of the FSM
    reg [7:0] data_in = 0;      // Register to store data received from the master
    reg [7:0] data_out = 8'b11001100;  // Data to be sent to the master in read mode
    reg sda_out = 0;            // Data to drive onto SDA when write_enable is high
    reg sda_in = 0;             // Register to capture SDA input data
    reg start = 0;              // Flag to indicate the start condition (SDA goes low while SCL is high)
    reg write_enable = 0;       // Controls whether the slave drives the SDA line

    // Tri-state SDA line: driven by sda_out when write_enable is high, otherwise high-impedance
    assign sda = (write_enable == 1) ? sda_out : 'bz;

    // Detect start condition on SDA falling edge when SCL is high
    always @(negedge sda) begin
        if ((start == 0) && (scl == 1)) begin
            start <= 1;           // Set start flag
            counter <= 7;         // Initialize counter to read 8 bits (address or data)
        end
    end

    // Detect stop condition on SDA rising edge when SCL is high
    always @(posedge sda) begin
        if ((start == 1) && (scl == 1)) begin
            state <= READ_ADDR;   // Go to READ_ADDR state to read the address from master
            start <= 0;           // Clear start flag
            write_enable <= 0;    // Release SDA line
        end
    end

    // State machine for I2C slave behavior, triggered on rising edge of SCL
    always @(posedge scl) begin
        if (start == 1) begin     // Only proceed if start condition was detected
            case(state)
                
                READ_ADDR: begin
                    addr[counter] <= sda;      // Capture address bit from SDA
                    if(counter == 0) begin
                        state <= SEND_ACK;     // Move to SEND_ACK after receiving full address
                    end else begin
                        counter <= counter - 1; // Count down to receive 8 bits
                    end
                end
                
                SEND_ACK: begin
                    // Check if received address matches slave address (addr_in)
                    if(addr[7:1] == addr_in) begin
                        counter <= 7;          // Reset bit counter for next data frame
                        // Determine next state based on R/W bit (addr[0])
                        if(addr[0] == 0) begin 
                            state <= READ_DATA; // If R/W=0, master wants to write, go to READ_DATA
                        end else begin
                            state <= WRITE_DATA; // If R/W=1, master wants to read, go to WRITE_DATA
                        end
                    end else begin
                        state <= READ_ADDR;    // Address mismatch, go back to READ_ADDR
                    end
                end
                
                READ_DATA: begin
                    data_in[counter] <= sda;   // Capture data bit from SDA
                    if(counter == 0) begin
                        state <= SEND_ACK2;    // Move to SEND_ACK2 after receiving full byte
                    end else begin
                        counter <= counter - 1; // Count down to receive 8 bits
                    end
                end
                
                SEND_ACK2: begin
                    state <= READ_ADDR;        // Go back to READ_ADDR to listen for next address
                end
                
                WRITE_DATA: begin
                    // Transmit data_out to master one bit at a time
                    if(counter == 0) begin
                        state <= READ_ADDR;    // After last bit, go back to READ_ADDR
                    end else begin
                        counter <= counter - 1; // Count down for each bit sent
                    end
                end
                
            endcase
        end
    end

    // Control SDA output behavior on falling edge of SCL, depending on the state
    always @(negedge scl) begin
        case(state)
            
            READ_ADDR: begin
                write_enable <= 0;           // Release SDA while reading address
            end
            
            SEND_ACK: begin
                sda_out <= (addr[7:1] == addr_in) ? 0 : 1; // Send ACK (low) if address matches, else NACK (high)
                write_enable <= 1;           // Enable SDA to drive ACK/NACK
            end
            
            READ_DATA: begin
                write_enable <= 0;           // Release SDA while reading data
            end
            
            WRITE_DATA: begin
                sda_out <= data_out[counter]; // Send each bit of data_out on SDA
                write_enable <= 1;           // Enable SDA to drive data
            end
            
            SEND_ACK2: begin
                sda_out <= 0;                // Send ACK (low) after receiving data
                write_enable <= 1;           // Enable SDA to drive ACK
            end
        endcase
    end
endmodule
Explanation

The Verilog code presented is for an I2C Slave Module that implements the core logic for an I2C slave device capable of receiving and transmitting data over the I2C protocol. Let’s break down the components of the code and their functionality:

Module Declaration

The module begins with the declaration of inputs and outputs:

module i2c_slave(
    input [6:0] addr_in,   // Dynamic address input for I2C slave
    inout sda,             // I2C data line (SDA)
    inout scl              // I2C clock line (SCL)
);

The inputs include a 7-bit address (addr_in) for the slave device, along with the bidirectional sda and scl lines for data and clock signals respectively.

State Machine Definition

The I2C protocol relies on a finite state machine (FSM) to control the data transfer sequence. The FSM is represented by five states:

localparam READ_ADDR = 0;
localparam SEND_ACK = 1;
localparam READ_DATA = 2;
localparam WRITE_DATA = 3;
localparam SEND_ACK2 = 4;

Each state corresponds to a particular phase of the I2C communication: - READ_ADDR: Reads the I2C address and R/W bit. - SEND_ACK: Sends acknowledgment (ACK) if the address matches. - READ_DATA: Receives data from the master. - WRITE_DATA: Sends data to the master. - SEND_ACK2: Sends a second ACK after data reception.

Internal Registers and Signals

Several internal registers and signals are declared to support the functionality of the I2C slave: - addr holds the slave address and R/W bit. - counter is used to count bits during transmission. - state holds the current FSM state. - data_in and data_out store the incoming and outgoing data, respectively. - sda_out and sda_in control the data line (SDA). - start flags the detection of the I2C start condition. - write_enable controls whether the slave can drive the SDA line.

SDA Line Control

The assignment of the sda line is conditional on the write_enable signal:

assign sda = (write_enable == 1) ? sda_out : 'bz;

This means that the slave drives the sda line when write_enable is active, otherwise, the line is in high-impedance state (‘bz).

Start and Stop Condition Detection

The start condition is detected when there is a falling edge on the sda line while the scl line is high, and the stop condition is detected when there is a rising edge on the sda line while scl is high. These conditions trigger transitions in the FSM.

always @(negedge sda) begin
    if ((start == 0) && (scl == 1)) begin
        start <= 1;           // Set start flag
        counter <= 7;         // Initialize bit counter
    end
end

always @(posedge sda) begin
    if ((start == 1) && (scl == 1)) begin
        state <= READ_ADDR;   // Transition to READ_ADDR state
        start <= 0;           // Reset start flag
        write_enable <= 0;    // Disable write
    end
end

These blocks capture the start and stop conditions and manage the FSM transitions accordingly.

FSM Logic for Data Transfer

The FSM operates on the rising edge of the scl signal, progressing through various states based on the detected conditions:

always @(posedge scl) begin
    if (start == 1) begin
        case(state)
            READ_ADDR: begin
                addr[counter] <= sda;
                if(counter == 0) state <= SEND_ACK;
                else counter <= counter - 1;
            end
            SEND_ACK: begin
                if(addr[7:1] == addr_in) begin
                    counter <= 7;
                    if(addr[0] == 0) begin 
                        state <= READ_DATA; // If write mode, move to READ_DATA
                    end else state <= WRITE_DATA; // Else move to WRITE_DATA
                end else state <= READ_ADDR;
            end
            ...
        endcase
    end
end

The state transitions depend on whether the address matches, the R/W bit, and whether data is being read or written. The SEND_ACK state sends an acknowledgment if the address is correct, while the READ_DATA and WRITE_DATA states handle data reception and transmission respectively.

SDA Output Logic

The logic for controlling the sda output during the FSM states is defined in the following block:

always @(negedge scl) begin
    case(state)
        READ_ADDR: begin
            write_enable <= 0;            // Disable writing during address read
        end
        SEND_ACK: begin
            sda_out <= (addr[7:1] == addr_in) ? 0 : 1;  // Send ACK (0) or NACK (1)
            write_enable <= 1;    
        end
        READ_DATA: begin
            write_enable <= 0;           // Disable writing during data read
        end
        WRITE_DATA: begin
            sda_out <= data_out[counter]; // Output data bit by bit
            write_enable <= 1;
        end
        SEND_ACK2: begin
            sda_out <= 0;                // Send ACK (0) after data reception
            write_enable <= 1;
        end
    endcase
end

Each state manipulates the sda_out signal to either send an acknowledgment (ACK) or transmit the data bit by bit. The SEND_ACK state checks the address match and sends either an ACK or NACK. The WRITE_DATA state sends the data, while the SEND_ACK2 state sends an ACK after data reception.

Summary

This Verilog code implements a simple I2C Slave module that can handle basic I2C communication. It includes start/stop condition detection, address matching, data reception, and data transmission using an FSM. The module can receive data from the I2C master, send data to it, and properly acknowledge the master at each step in the communication process.

TOP-LEVEL MODULE

C0d3
`timescale 1ns / 1ps

// Top module to integrate i2c_master and i2c_slave
// Top module to integrate i2c_master and i2c_slave
module top(
    input wire clk,                 // System clock
    input wire rst,                 // Reset signal
    input wire [6:0] addr,          // 7-bit I2C address for the master to communicate with
    input wire [7:0] data_in,       // Data to be sent from the master to the slave
    input wire enable,              // Enable signal to initiate I2C communication
    input wire rw,                  // Read/Write signal (0 = Write, 1 = Read)
    output wire [7:0] data_out,     // Data received by the master from the slave
    output wire ready,              // Signal indicating the master is ready for a new operation
    inout wire i2c_sda,             // I2C data line (SDA) - bidirectional
    inout wire i2c_scl              // I2C clock line (SCL)
);

    // Internal register to store the address the slave will respond to.
    // This is the fixed address of the slave in this example.
    reg [6:0] slave_address = 7'b0101010;  // Example default slave address

    // Instantiate the I2C slave module
    i2c_slave slave_inst (
        .addr_in(slave_address),   // Provide the fixed slave address to the slave instance
        .sda(i2c_sda),             // Connect the slave's SDA line to the top-level SDA
        .scl(i2c_scl)              // Connect the slave's SCL line to the top-level SCL
    );

    // Instantiate the I2C master module
    i2c_master master_inst (
        .clk(clk),                 // Connect the system clock to the master
        .rst(rst),                 // Connect the reset signal to the master
        .addr(addr),               // Provide the I2C address the master should communicate with
        .data_in(data_in),         // Data to be sent to the slave (if writing)
        .enable(enable),           // Enable signal to start the I2C transaction
        .rw(rw),                   // Read/Write signal (0 = Write, 1 = Read)
        .data_out(data_out),       // Data received from the slave (if reading)
        .ready(ready),             // Master ready signal indicating it's idle or ready for a new transaction
        .i2c_sda(i2c_sda),         // Connect the master's SDA line to the top-level SDA
        .i2c_scl(i2c_scl)          // Connect the master's SCL line to the top-level SCL
    );

endmodule
Explanation
  • The top module connects an I2C master and slave module on shared i2c_sda and i2c_scl lines.

  • The slave_address register holds a predefined address used by the slave.

  • The i2c_slave and i2c_master modules are instantiated and connected to share the I2C lines and control signals.

TESTBENCH MODULE

C0d3
`timescale 1ns / 1ps

module i2c_controller_tb();

    // Inputs
    reg clk;                  // System clock
    reg rst;                  // Reset signal
    reg [6:0] addr;           // Address for the master to communicate with
    reg [7:0] data_in;        // Data to be sent from the master to the slave
    reg enable;               // Enable signal to start communication
    reg rw;                   // Read/Write control (0 = Write, 1 = Read)

    // Outputs
    wire [7:0] data_out;      // Data received by the master from the slave
    wire ready;               // Ready signal indicating the master is ready for a new operation

    // Bidirectional wires
    wire i2c_sda;             // I2C data line (SDA) - shared between master and slave
    wire i2c_scl;             // I2C clock line (SCL) - shared between master and slave

    // Instantiate the Top Module (Device Under Test - DUT)
    top uut (
        .clk(clk),           // Connect system clock to DUT
        .rst(rst),           // Connect reset signal to DUT
        .addr(addr),         // Connect address input to DUT
        .data_in(data_in),   // Connect data to be sent by master to DUT
        .enable(enable),     // Connect enable signal to DUT
        .rw(rw),             // Connect read/write control to DUT
        .data_out(data_out), // Receive data read by master from DUT
        .ready(ready),       // Receive ready signal from DUT
        .i2c_sda(i2c_sda),   // Connect bidirectional SDA line
        .i2c_scl(i2c_scl)    // Connect bidirectional SCL line
    );

    // Clock generation
    initial begin
        clk = 0;
        forever #1 clk = ~clk;  // Toggle clock every 1 ns to generate a 2 ns period clock (500 MHz)
    end

    // Test sequence to simulate I2C operations
    initial begin
        // Set up VCD file for waveform dumping
        $dumpfile("i2c_controller_tb.vcd");  // Name of the VCD file for waveform output
        $dumpvars(0, i2c_controller_tb);     // Dump all variables in this module for waveform analysis

        // Initialize Inputs
        rst = 1;           // Assert reset to initialize the system
        enable = 0;        // Initially disable communication
        addr = 7'b0000000; // Set an initial address (not used immediately)
        data_in = 8'b0;    // Set initial data (not used immediately)
        rw = 0;            // Set initial operation to write (0 = Write, 1 = Read)

        // Wait for reset to complete
        #10;
        rst = 0;           // Deassert reset after 10 ns to start normal operation

        // Test Case 1: Write operation with matching address (Expect ACK from slave)
        addr = 7'b0101010;       // Set address to match the slave address
        data_in = 8'b10101010;   // Data to be sent to the slave
        rw = 0;                  // Set operation to write
        enable = 1;              // Assert enable to start the I2C communication
        #20 enable = 0;          // Deassert enable after 20 ns to complete the command

        // Wait and observe response (slave should ACK the address and receive data)
        #100;

        // Test Case 2: Write operation with non-matching address (Expect NACK from slave)
        addr = 7'b1111111;       // Set address to a non-matching address for the slave
        data_in = 8'b11001100;   // Different data to be sent to the slave
        rw = 0;                  // Set operation to write
        enable = 1;              // Assert enable to start the I2C communication
        #20 enable = 0;          // Deassert enable after 20 ns

        // Wait and observe response (slave should NACK the address since it does not match)
        #100;

        // Test Case 3: Read operation with matching address (Expect ACK from slave and read data)
        addr = 7'b0101010;       // Set address to match the slave address
        rw = 1;                  // Set operation to read
        enable = 1;              // Assert enable to start the I2C communication
        #20 enable = 0;          // Deassert enable after 20 ns

        // Wait and observe response (slave should ACK the address and send data to master)
        #100;

        #200
        $finish;  // End the simulation after 200 ns
    end

endmodule
Explanation
  • i2c_controller_tb: Testbench module for the top module integrating the master-slave I2C communication.

  • A clock signal is generated using a continuous initial block.

  • Test cases:

    • Test Case 1: Matches the slave address, expecting an ACK.

    • Test Case 2: Uses a non-matching address, expecting a NACK.

    • Test Case 3: Matches the address and tests a read operation.

  • At the end of the test cases, the simulation finishes with $finish.

Results

Test Case 1: Address Match with Write Operation

Test Case 1
Test Case 2

The waveform in this test shows the master sending an address 7’b0101010, which matches the slave’s configured address. Since the address matches, the slave acknowledges the communication by sending an ACK (acknowledgment) signal. After receiving the ACK, the master initiates a write operation, transmitting the data 8’b10101010 to the slave. The enable signal is set to initiate communication and deasserted after a delay, allowing the transmission to complete. The presence of the ACK in this waveform confirms that the address was successfully matched, allowing data transfer.

Test Case 2

Test Case 2: Address Mismatch with Write Operation (Expect NACK)

Test Case 3

In this test case, the master sends a non-matching address 7’b1111111, which does not correspond to the slave’s preset address. The waveform shows the absence of an acknowledgment signal (NACK) from the slave, indicating that the address verification failed and the data transfer cannot proceed. This NACK response is expected behavior when the slave does not recognize the transmitted address. The enable signal initiates the communication, but with no matching address and no ACK received, data transfer is not established.

Test Case 3

Test Case 3: Address Match with Read Operation

Test Case 3

The final waveform demonstrates a read operation with the master sending a matching address 7’b0101010. Upon recognizing the address, the slave responds with an ACK signal, confirming the communication link. Following the acknowledgment, the master initiates a read operation, and the slave provides the preloaded data 8’b11001100 to the master. The enable signal triggers the start of communication and is deasserted after a delay, allowing the master to read the data. This successful transmission and receipt of data verify correct read functionality with an address match.

Challenges and Risk Analysis

Potential Issues and Solutions

In the development of the I2C communication project, I encountered several challenges, starting with the design of basic Verilog modules for both master and slave entities capable of fundamental read and write operations. After an initial review of the I2C protocol from various online resources and Verilog syntax, I designed a preliminary testbench module to simulate and validate the basic functionality.

As the project progressed, I introduced additional functionality, including ACK and NACK flags, to handle incorrect slave addresses and refined the finite state machine (FSM) logic for more reliable state transitions. To make the slave module independent of global configurations, I designed a top-level module that instantiated both the master and slave modules and added an extra layer of address validation.

For synchronization, I implemented clock stretching between address and data frames. This addition worked effectively during address transmission but revealed timing issues during data frame read operations. After extensive troubleshooting, I identified limitations in the initial approach and modified the design accordingly.

I also explored a multi-master, multi-slave configuration, assigning slave addresses in the format 10101XY (with XY values as 00, 01, 10, and 11 for slaves 1 through 4) and a master select line in the testbench to choose the active master. Preloaded data in each slave was structured as 110011XY to streamline read operations. However, unresolved synchronization issues in the multi-master setup led me to scale back to a single-master, single-slave model, excluding clock stretching and multiple nodes. The final design focuses on single-point communication, with code attempts for the multi-master setup included in the project appendix on GitHub.

Risk Management

Several potential risks emerged during the design and integration phases:

  • Design Complexity: The complexity of the I2C protocol and multi-node configuration posed unforeseen design challenges. A modular testing approach mitigated these risks by allowing iterative refinements.

  • Timing Issues: Timing mismatches, particularly in multi-master configurations, impacted protocol accuracy. While resolved in the single-master model, this remains an area for future improvement.

  • FSM Complexity: Introducing ACK/NACK handling increased the complexity of the FSM, raising the potential for state transition errors. Comprehensive simulation and debugging minimized these risks.

In this project, I implemented the clock stretching functionality and addressed timing issues in data frame read operations. Starting with basic I2C modules, I managed the synchronization aspect by incorporating clock stretching between address and data frames, a mechanism crucial for addressing timing issues in the protocol. Despite progress, some discrepancies remain in the data frame during read operations, which are planned for future enhancements.

Additionally, I expanded the basic modules by introducing acknowledgment (ACK) and negative acknowledgment (NACK) flags to handle erroneous addresses. I refined the FSM logic to improve state transitions, address handling, and protocol management complexities. While the initial goal was a multi-master configuration with selectable slave nodes, unresolved timing issues led to a focus on a single-master, single-slave model, effectively capturing the core aspects of I2C communication in the final implementation.

Future Work and Improvements

Suggested Enhancements

Future enhancements to this project could include adding more registers to each slave, allowing for more sophisticated data handling. Additional registers would enable more extensive data storage and retrieval options in each slave device, making the project closer to real-world I2C applications.

Alternative Designs

Exploring alternative FSM architectures could improve the efficiency and stability of the I2C protocol, especially for multi-master configurations. Further, advanced data synchronization techniques, possibly through modified clock stretching or data frame timing adjustments, could address the current timing issues. Replacing the current point-to-point master-slave setup with a robust multi-node configuration, if resolved, could significantly enhance the protocol’s scalability.

Appendices

Verilog Code Listings

The complete Verilog code for the I2C Master module, including support for multi-master/slave configuration and clock stretching, is available in the following GitHub repository:

Repository: https://github.com/Mummanajagadeesh/I2C-protocol-verilog

References

  1. Texas Instruments, A Basic Guide to I2C, Available at: https://www.ti.com/lit/pdf/sbaa565

  2. Prodigy Technoinnovations, I2C Protocol, Available at: https://www.prodigytechno.com/i2c-protocol

  3. SparkFun, I2C Tutorial, Available at: https://learn.sparkfun.com/tutorials/i2c/all

  4. Class Lectures, Verilog Code Syntax

Most images in this document are adapted from the above resources. All images are copyrighted by their respective owners; no ownership rights are claimed.