Implementing I2C Communication Protocol on FPGA with Verilog
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:
- Generate START condition
- Transmit device write address (0xA0)
- Receive and verify acknowledgment from EEPROM
- Send high byte of memory address
- Receive acknowledgment
- Send low byte of memory address
- Receive acknowledgment
- Transmit 8-bit data payload
- Receive acknowledgment
- Generate STOP condition
Single-Byte Read Sequence
Reading one byte from EEPROM follows this procedure:
- Generate START condition
- Transmit device write address (0xA0)
- Receive acknowledgment
- Send high byte of memory address
- Receive acknowledgment
- Send low byte of memory address
- Receive acknowledgment
- Generate repeated START condition
- Transmit device read address (0xA1)
- Receive acknowledgment
- Read data byte from EEPROM
- Send NACK to indicate read completion
- 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.