Design and Verification of
Inter Integrated Circuit
(I2C) protocol using
SystemVerilog
(by Ch. Anirudh)
1. Introduction:
The Inter-Integrated Circuit (I2C) protocol is a widely used serial
communication protocol developed by Philips (now NXP Semiconductors). It is
primarily used for short-distance, low-speed communication between multiple
devices within an embedded system. The protocol supports multiple masters
and slaves, using only two wires: SCL (Serial Clock Line) and SDA (Serial Data
Line), making it a highly efficient and cost-effective communication standard.
I2C operates in three main speed modes: Standard Mode (100 kbps), Fast
Mode (400 kbps), and High-Speed Mode (3.4 Mbps). It employs synchronous
communication, meaning data is transmitted and received with respect to the
clock signal generated by the master. The master controls communication by
initiating data transfers, while slaves respond accordingly based on their
assigned 7-bit or 10-bit addresses.
This project focuses on implementing an I2C Master-Slave communication
system using System Verilog. The design includes both the Master and Slave
modules, capable of handling read and write operations. The simulation is
performed using Vivado , and the results are verified using waveform analysis.
The implementation demonstrates data transfer between an I2C Master and
multiple Slave devices, ensuring correct protocol behavior.
2. Overview of I2C Protocol:
I2C is a multi-master, multi-slave, half-duplex communication protocol where
multiple devices can share the same bus. It operates using two bidirectional
lines:
SCL (Serial Clock Line): Carries the clock signal generated by the master.
SDA (Serial Data Line): Transfers data between master and slaves.
Key Features of I2C:
Simple two-wire interface for connecting multiple devices.
Supports multi-master and multi-slave configurations.
Provides synchronous communication with clock-based control.
Uses ACK/NACK mechanisms for data verification.
Supports multiple speed modes (Standard, Fast, and High-Speed)
Fig(a): I2C Protocol Master & Slave
3. Basic I2C Communication Process:
Master initiates communication by sending a Start Condition (SDA goes
LOW while SCL is HIGH).
Master sends the 7-bit Slave Address + Read/Write bit.
Slave acknowledges (ACK) if it recognizes its address.
Master sends or receives data based on the operation.
Communication ends with a Stop Condition (SDA goes HIGH while SCL is
High)
Fig(b): Working of I2C Protocol
4. Design and Implementation:
The I2C Master and Slave modules are implemented in SystemVerilog using
Finite State Machines (FSMs) to manage communication.
Design Components:
I2C Master Module
o Generates Start & Stop conditions.
o Controls SCL clock signal.
o Handles addressing and data transfer.
o Implements ACK/NACK handling.
I2C Slave Module
o Detects Start & Stop conditions.
o Recognizes its assigned address.
o Responds with ACK/NACK.
o Reads/writes data based on master commands.
Testbench for Simulation
o Generates clock (SCL) and data (SDA) signals.
o Stimulates Master-Slave communication.
o Verifies data transfer correctness.
5. Operations of I2C protocol:
1. Write Operation (Master to Slave)
Master sends START condition.
Sends slave address + write bit (0).
Slave acknowledges (ACK).
Master sends data byte-by-byte.
Slave acknowledges each byte.
Master sends STOP condition.
2. Read Operation (Slave to Master)
Master sends START condition.
Sends slave address + read bit (1).
Slave acknowledges and sends data.
Master acknowledges received bytes.
Master sends STOP condition.
6. RTL Code:
I2C Master:
module I2C_master(
input wire clk, //clock signal
input wire rst_n, // Active-low reset signal
output reg scl, // I2C Serial clock line
inout wire sda, // I2C serial data line (bidirectional)
input wire start, // start signal to initiate I2C transaction
input wire [6:0] addr, // 7-bit slave address
input wire rw, // read / write bit (0: write, 1: read)
input wire [7:0] data_wr, // data to write to the slave
output reg [7:0] data_rd, // data read from the slave
output reg busy, // busy signal (1: busy, 0: idle)
output reg ack // acknowledge signal
);
//I2C state machine for internal states
typedef enum logic [2:0] {
IDLE, // Idle state
START, // we need to start condition state
ADDR, // address transmission state
WRITE, // write state for data
READ, // data read store
STOP // at last we need to stop condition state
} state_t;
state_t state;
//I2C state machine current state
reg [7:0] shift_reg; // for data transmission / reception
reg [2:0] bit_cnt; // bit counter for tracking bits in a byte
reg sda_out; // internal SDA output signal
reg sda_oe; // SDA output enable(1: drive SDA, 0: Tri-state)
// Tri-state buuffer for SDA
assign sda = sda_oe ? sda_out : 1'bz;
// clock divider for generating SCL
reg [7:0] clk_div; // used to divide the system clock to generate the SCL signal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_div <= 8'b0;
scl <= 1'b1;
end else begin
if (clk_div == 8'd199) begin // assuming a 100MHz clock, divide by 200 for 400khz SCL
clk_div <= 8'b0;
scl <= ~scl; // counters reaches 199 reset to '0' and toogle SCL
end else begin
clk_div <= clk_div + 8'd1;
end
end
end
// I2C state machine logic
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
busy <= 1'b0;
ack <= 1'b0;
sda_out <= 1'b1; // it is high because in idle state
sda_oe <= 1'b0;
bit_cnt <= 3'd0;
shift_reg <= 8'd0;
data_rd <= 8'd0;
end else begin
case (state)
IDLE: begin
busy <= 1'b0; // master is not busy
sda_out <= 1'b1; // SDA is high
sda_oe <= 1'b0; // SDA is tri-stated beacuse not driven by master
if (start) begin
state <= START;
busy <= 1'b1; // when start signal start master is busy
end
end
START: begin
sda_out <= 1'b0; // it is low (start condition)
sda_oe <= 1'b1; // drive SDA
if (scl == 1'b1) begin // wait for SCl high
state <= ADDR;
shift_reg <= {addr, rw};
bit_cnt <= 3'd7; // initialize bit counter to 7 MSB first
end
end
ADDR: begin
sda_out <= shift_reg[bit_cnt]; // need to transmit current bit of address
sda_oe <= 1'b1; // drive SDA by master
if (scl == 1'b0) begin // wait for SCL to be low
if (bit_cnt == 3'd0) begin // all bits are transmitted (7-bit address + R/W bit)
state <= WRITE; // transition to write state
bit_cnt <= 3'd7; // reset bit counter for data byte
end else begin
bit_cnt <= bit_cnt - 3'd1; // decrement bit counter
end
end
end
WRITE: begin
sda_out <= data_wr[bit_cnt]; // transmit current bit of data
sda_oe <= 1'b1; // drive SDA
if (scl == 1'b0) begin
if (bit_cnt == 3'd0) begin // if all bits are transmitted go to STOP state
state <= STOP;
ack <= sda; // wew need to capture ACK / NACK
end else begin
bit_cnt <= bit_cnt - 3'd1;
end
end
end
STOP: begin
sda_out <= 1'b0; // pull SDA to low
sda_oe <= 1'b1; //drive SDA
if (scl == 1'b1) begin // wait for SCL to begin high
sda_out <= 1'b1; // pill SDA to high
state <= IDLE; // transition to idle state
end
end
default: state <= IDLE;
endcase
end
end
endmodule
I2C Slave:
module I2C_slave(
input wire clk, //system clock
input wire rst_n, // active-low reset
input wire scl, // I2C serial clock line
inout wire sda, // I2C serial data line
input wire [6:0] addr, // 7-bit slave address
output reg [7:0] data_rd, // data read from master
output reg ack // Acknowledgement signal
);
// internal states for I2C slave state machine
typedef enum logic[2:0] {IDLE, ADDR, READ, WRITE, ACK} state_t;
state_t state; //current state of the I2C slave state machine
reg [7:0] shift_reg; //shift register for data transmission / reception
reg [2:0] bit_cnt; //bit counter for tracking bits in a byte
reg sda_out; // internal SDA output signal
reg sda_oe; // SDA output enable (1: drive SDA, 0: Tri-state)
// Tri-state buffer for SDA
assign sda = sda_oe ? sda_out : 1'bz;
// I2C slave state machine
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
data_rd <= 8'b0;
ack <= 1'b0;
sda_out <= 1'b1;
sda_oe <= 1'b0;
bit_cnt <= 3'd0;
shift_reg <= 8'b0;
end else begin
case (state)
IDLE: begin
sda_out <= 1'b1;
sda_oe <= 1'b0;
if (scl == 1'b1 && sda == 1'b0) begin // detect start condition
state <= ADDR;
bit_cnt <= 3'd7;
end
end
ADDR: begin
if (scl == 1'b1) begin
shift_reg[bit_cnt] <= sda; //sample address bit
if(bit_cnt == 3'b0) begin
if (shift_reg[7:1] == addr) begin // check address match
state <= ACK;
ack <= 1'b1; // acknowledge address match
end else begin
state <= IDLE; // adress mismatch, return to idle
end
end else begin
bit_cnt <= bit_cnt - 3'd1;
end
end
end
READ: begin
if (scl == 1'b1) begin
data_rd[bit_cnt] <= sda; // sample data bit
if (bit_cnt == 3'b0) begin
state <= ACK;
ack <= 1'b1; // acknowledge data reception
end else begin
bit_cnt <= bit_cnt - 3'd1;
end
end
end
WRITE: begin
if (scl == 1'b1) begin
sda_out <= shift_reg[bit_cnt]; // transmit the data bit
sda_oe <= 1'b1;
if (bit_cnt == 3'd0) begin
state <= ACK;
end else begin
bit_cnt <= bit_cnt - 3'd1;
end
end
end
ACK: begin
if (scl == 1'b1) begin
sda_out <= 1'b0; //drive sda low for ACk
sda_oe <= 1'b1;
if (shift_reg[0] == 1'b0) begin //check R / W bit
state <= READ; // master wants to read
end else begin
state <= WRITE; // master wants to write
end
end
end
default: state <= IDLE;
endcase
end
end
endmodule
I2C Top:
module I2C_top(
input wire clk, //system clock
input wire rst_n, // active-low reset
input wire start, // start signal from the master
input wire [6:0] addr, // 7-bit slave address
input wire rw, // read / write bit (0: write, 1: read)
input wire [7:0] data_wr, // data to write master to slave
output wire [7:0] data_rd_master, // data read by master to slave
output wire [7:0] data_rd_slave, // data read by slave to master
output wire ack_master, // acknowledge signal from master
output wire ack_slave // acknowledge signal from slave
);
// internal signals from I2C communication
wire scl; // I2C serial clock line
wire sda; // I2C serial data line
// instantiate the I2C master
I2C_master master(
.clk(clk),
.rst_n(rst_n),
.start(start),
.addr(addr),
.rw(rw),
.data_wr(data_wr),
.scl(scl),
.sda(sda),
.busy(),
.ack(ack_master),
.data_rd(data_rd_master)
);
// instantiate I2C slave
I2C_slave slave (
.clk(clk),
.rst_n(rst_n),
.scl(scl),
.sda(sda),
.addr(addr),
.data_rd(data_rd_slave),
.ack(ack_slave)
);
endmodule
7. Testbench Simulation:
I2C_top_tb:
module I2C_top_tb;
// Inputs
reg clk;
reg rst_n;
reg start;
reg [6:0] addr;
reg rw;
reg [7:0] data_wr;
// Outputs
wire [7:0] data_rd_master;
wire [7:0] data_rd_slave;
wire ack_master;
wire ack_slave;
// Instantiate the top-level module
I2C_top uut (
.clk(clk),
.rst_n(rst_n),
.start(start),
.addr(addr),
.rw(rw),
.data_wr(data_wr),
.data_rd_master(data_rd_master),
.data_rd_slave(data_rd_slave),
.ack_master(ack_master),
.ack_slave(ack_slave)
);
// Clock generation
always #5 clk = ~clk; // 100 MHz clock
// Testbench logic
initial begin
// Initialize inputs
clk = 0;
rst_n = 0;
start = 0;
addr = 7'b0;
rw = 0;
data_wr = 8'b0;
// Apply reset
#10 rst_n = 1;
// Test 1: Write data from master to slave
$display("Test 1: Write data from master to slave");
addr = 7'b1010101; // Slave address
rw = 0; // Write operation
data_wr = 8'b11001100; // Data to write
start = 1; // Start transaction
#20 start = 0; // Deassert start
wait (ack_master); // Wait for acknowledgment
$display("Data written by master: %b", data_wr);
$display("Data received by slave: %b", data_rd_slave);
#100;
// Test 2: Read data from slave to master
$display("Test 2: Read data from slave to master");
addr = 7'b1010101; // Slave address
rw = 1; // Read operation
start = 1; // Start transaction
#20 start = 0; // Deassert start
wait (ack_master); // Wait for acknowledgment
$display("Data read by master: %b", data_rd_master);
#100;
// Test 3: Randomized test with error injection
$display("Test 3: Randomized test with error injection");
repeat (10) begin
addr = $random; // Random slave address
rw = $random; // Random read/write operation
data_wr = $random; // Random data
start = 1; // Start transaction
#20 start = 0; // Deassert start
wait (ack_master); // Wait for acknowledgment
if (addr == 7'b1010101) begin
$display("Valid transaction: Addr = %b, R/W = %b, Data = %b", addr, rw, data_wr);
if (rw == 0) begin
$display("Data written by master: %b", data_wr);
$display("Data received by slave: %b", data_rd_slave);
end else begin
$display("Data read by master: %b", data_rd_master);
end
end else begin
$display("Invalid transaction: Addr = %b (no acknowledgment)", addr);
end
#100;
end
// End simulation
$display("Simulation completed.");
$finish;
end
// Monitor signals
initial begin
$monitor("Time = %0t: SCL = %b, SDA = %b, Ack Master = %b, Ack Slave = %b",
$time, uut.scl, uut.sda, ack_master, ack_slave);
end
endmodule
8. Elaborated Design:
Fig(c): I2C Elaborated design
9. Simulation:
Fig(d): I2C Simulation
10.Synthesis:
Fig(e): I2C Synthesized Design
11.Applications:
I2C is widely used in embedded systems and digital electronics for interfacing
low-speed peripherals.
Microcontrollers & Sensors: Temperature sensors, accelerometers, and
EEPROMs.
Real-Time Clocks (RTC): Timekeeping modules in embedded systems.
Display Interfaces: OLED, LCD, and LED drivers.
Power Management ICs: Battery monitoring and power regulation.
FPGA Communication: Connecting multiple ICs within FPGA designs.
12. Advantages & Disadvantages:
1. Uses only two wires (SCL & SDA), reducing pin count.
2. Multi-master and multi-slave support allows flexible system design.
3. Efficient communication with error detection (ACK/NACK).
4. Variable data transfer rates (100 kbps to 3.4 Mbps).
Disadv:
Slower than SPI due to clock-based control.
Requires pull-up resistors, adding hardware complexity.
Limited data transfer rate for high-speed applications.
13.Conclusion:
This project successfully demonstrates the I2C protocol implementation using
SystemVerilog. The Master and Slave modules perform correct read/write
operations, verified through functional simulation. The results confirm the
proper working of I2C communication, making it suitable for sensor
interfacing, memory communication, and embedded system designs.
------The End-----