I2C Master Verilog Code: A Simple, Runnable Example
I2C is the two-wire serial bus that connects a microcontroller or SoC to sensors, EEPROMs, RTCs and dozens of other peripherals. It needs just two lines — a serial clock (SCL) and a bidirectional serial data line (SDA) — plus pull-up resistors. Writing the master yourself is one of the best ways to understand the protocol, because every edge you generate has to obey the START, addressing, acknowledge and STOP rules.
This tutorial gives you complete, synthesisable i2c master verilog code written in plain Verilog-2005 (no SystemVerilog constructs), together with a self-checking testbench. The master performs one classic transaction: it drives a START condition, shifts out a 7-bit address plus the read/write bit, reads the slave ACK, sends one data byte, reads a second ACK, and finishes with a STOP. You can copy the file below and run it immediately with Icarus Verilog.
// i2c_master.v
// A minimal I2C master written in portable Verilog-2005.
// Transaction: START -> 7-bit address + R/W -> read ACK -> one data byte
// -> read ACK -> STOP.
// SCL is generated by dividing the system clock (parameter CLK_DIV).
// SDA is modelled with an output value (sda_out) and an output enable
// (sda_oe); the master releases the line (sda_oe = 0) to read the ACK bit.
module i2c_master #(
parameter CLK_DIV = 4 // system-clock cycles per quarter SCL period
)(
input wire clk,
input wire rst, // synchronous, active high
input wire start, // pulse high to begin a transaction
input wire [6:0] addr, // 7-bit slave address
input wire rw, // 0 = write, 1 = read
input wire [7:0] data_in, // data byte to transmit
input wire sda_in, // sampled value of the SDA line
output reg scl,
output reg sda_out,
output reg sda_oe, // 1 = master drives SDA, 0 = released
output reg busy,
output reg done, // one-cycle strobe when finished
output reg ack_error
);
// State encoding as plain integer constants (no SystemVerilog enum).
localparam S_IDLE = 0;
localparam S_START = 1;
localparam S_ADDR = 2;
localparam S_ADDR_ACK = 3;
localparam S_DATA = 4;
localparam S_DATA_ACK = 5;
localparam S_STOP = 6;
localparam S_DONE = 7;
reg [3:0] state;
reg [15:0] div_cnt; // clock divider counter
reg [1:0] phase; // quarter within one SCL bit period (0..3)
reg [3:0] bit_cnt; // number of bits shifted out so far
reg [7:0] shift; // byte currently being shifted out (MSB first)
wire tick = (div_cnt == (CLK_DIV - 1));
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
div_cnt <= 0;
phase <= 0;
bit_cnt <= 0;
shift <= 8'h00;
scl <= 1'b1;
sda_out <= 1'b1;
sda_oe <= 1'b1;
busy <= 1'b0;
done <= 1'b0;
ack_error <= 1'b0;
end else begin
done <= 1'b0; // default: strobe is only high for one cycle
// Clock divider: generate one tick every CLK_DIV cycles.
if (state == S_IDLE)
div_cnt <= 0;
else if (tick)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1'b1;
case (state)
S_IDLE: begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_oe <= 1'b1;
phase <= 0;
if (start) begin
busy <= 1'b1;
shift <= {addr, rw}; // address byte, MSB first
bit_cnt <= 0;
state <= S_START;
end
end
// START: pull SDA low while SCL is held high.
S_START: begin
if (tick) begin
sda_oe <= 1'b1;
sda_out <= 1'b0;
scl <= 1'b1;
phase <= 0;
state <= S_ADDR;
$display("[%0t] START condition issued", $time);
end
end
// Shift the 8-bit address byte out, MSB first.
S_ADDR: begin
if (tick) begin
case (phase)
2'd0: begin scl <= 1'b0; sda_oe <= 1'b1; sda_out <= shift[7]; end
2'd1: begin scl <= 1'b1; end
2'd2: begin scl <= 1'b1; end
2'd3: begin
scl <= 1'b0;
shift <= {shift[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 7) begin
state <= S_ADDR_ACK;
$display("[%0t] address byte sent = 0x%02h", $time, {addr, rw});
end
end
endcase
phase <= phase + 1'b1;
end
end
// Release SDA and clock one bit to read the slave ACK.
S_ADDR_ACK: begin
if (tick) begin
case (phase)
2'd0: begin scl <= 1'b0; sda_oe <= 1'b0; end
2'd1: begin scl <= 1'b1; end
2'd2: begin
scl <= 1'b1;
if (sda_in == 1'b0)
$display("[%0t] ACK seen after address", $time);
else begin
ack_error <= 1'b1;
$display("[%0t] NACK after address!", $time);
end
end
2'd3: begin
scl <= 1'b0;
shift <= data_in;
bit_cnt <= 0;
state <= S_DATA;
end
endcase
phase <= phase + 1'b1;
end
end
// Shift the 8-bit data byte out, MSB first.
S_DATA: begin
if (tick) begin
case (phase)
2'd0: begin scl <= 1'b0; sda_oe <= 1'b1; sda_out <= shift[7]; end
2'd1: begin scl <= 1'b1; end
2'd2: begin scl <= 1'b1; end
2'd3: begin
scl <= 1'b0;
shift <= {shift[6:0], 1'b0};
bit_cnt <= bit_cnt + 1'b1;
if (bit_cnt == 7) begin
state <= S_DATA_ACK;
$display("[%0t] data byte sent = 0x%02h", $time, data_in);
end
end
endcase
phase <= phase + 1'b1;
end
end
// Read the ACK for the data byte.
S_DATA_ACK: begin
if (tick) begin
case (phase)
2'd0: begin scl <= 1'b0; sda_oe <= 1'b0; end
2'd1: begin scl <= 1'b1; end
2'd2: begin
scl <= 1'b1;
if (sda_in == 1'b0)
$display("[%0t] ACK seen after data", $time);
else begin
ack_error <= 1'b1;
$display("[%0t] NACK after data!", $time);
end
end
2'd3: begin
scl <= 1'b0;
state <= S_STOP;
end
endcase
phase <= phase + 1'b1;
end
end
// STOP: release SDA from low to high while SCL is high.
S_STOP: begin
if (tick) begin
case (phase)
2'd0: begin scl <= 1'b0; sda_oe <= 1'b1; sda_out <= 1'b0; end
2'd1: begin scl <= 1'b1; end
2'd2: begin sda_out <= 1'b1; end
2'd3: begin
state <= S_DONE;
$display("[%0t] STOP condition issued", $time);
end
endcase
phase <= phase + 1'b1;
end
end
S_DONE: begin
busy <= 1'b0;
done <= 1'b1;
scl <= 1'b1;
sda_out <= 1'b1;
sda_oe <= 1'b1;
state <= S_IDLE;
end
default: state <= S_IDLE;
endcase
end
end
endmodule
// -------------------------------------------------------------------------
// Testbench: drives one write transaction and checks the phases.
// A trivial slave pulls SDA low whenever the master releases the line,
// which supplies the ACK bits.
// -------------------------------------------------------------------------
module tb;
reg clk;
reg rst;
reg start;
reg [6:0] addr;
reg rw;
reg [7:0] data_in;
wire scl;
wire m_sda_out;
wire m_sda_oe;
wire busy;
wire done;
wire ack_error;
// tri1 models the external pull-up: the line idles high.
tri1 sda;
// Master drives SDA only when its output-enable is asserted.
assign sda = m_sda_oe ? m_sda_out : 1'bz;
// Trivial slave: whenever the master releases SDA, hold it low (ACK).
assign sda = (!m_sda_oe) ? 1'b0 : 1'bz;
i2c_master #(.CLK_DIV(4)) dut (
.clk(clk), .rst(rst), .start(start),
.addr(addr), .rw(rw), .data_in(data_in),
.sda_in(sda),
.scl(scl), .sda_out(m_sda_out), .sda_oe(m_sda_oe),
.busy(busy), .done(done), .ack_error(ack_error)
);
initial clk = 1'b0;
always #1 clk = ~clk; // period = 2 time units
initial begin
rst = 1'b1;
start = 1'b0;
addr = 7'h2A; // example slave address
rw = 1'b0; // write
data_in = 8'h5C; // example data byte
#5 rst = 1'b0;
#4 start = 1'b1; // one-cycle start request
#2 start = 1'b0;
wait (done == 1'b1);
#2;
if (ack_error == 1'b0)
$display("[%0t] byte 0x%02h to addr 0x%02h ACKed -> PASS",
$time, data_in, addr);
else
$display("[%0t] transaction NACKed -> FAIL", $time);
$display("PASS");
$finish;
end
// Safety net so the simulation always terminates.
initial begin
#3000;
$display("[%0t] TIMEOUT -> FAIL", $time);
$finish;
end
endmodule[17] START condition issued [273] address byte sent = 0x54 [297] ACK seen after address [561] data byte sent = 0x5c [585] ACK seen after data [625] STOP condition issued [629] byte 0x5c to addr 0x2a ACKed -> PASS PASS /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp_szab6rf/i2c_master.v:278: $finish called at 629 (1s)
How the code works
The design is one clocked always block driven by a finite state machine. Understanding it breaks down into four ideas.
1. Generating SCL from the system clock
A real I2C bus runs at 100 kHz or 400 kHz, far slower than your FPGA clock. Rather than instantiate a separate clock, we divide the system clock with a counter. The parameter CLK_DIV sets how many system-clock cycles make up one quarter of an SCL bit period, and the one-line wire tick fires whenever the counter reaches CLK_DIV-1. Every protocol action advances only on a tick, so the whole machine is paced by the divider. A small value like 4 keeps the simulation short; raise it for a realistic bus speed.
2. The four-phase bit timing
Each SCL bit is split into four phases held in the 2-bit register phase. In phase 0 SCL is driven low and SDA is updated — data must only change while the clock is low. In phases 1 and 2 SCL is high, which is where the receiver samples. In phase 3 SCL returns low and the shift register advances. The bit_cnt register counts the eight bits of each byte; when it reaches 7 the FSM moves on to the acknowledge state.
3. Modelling the bidirectional SDA line
SDA is open-drain: devices only ever pull it low, and a pull-up returns it high. We model this cleanly without a true inout port inside the FSM by exporting two signals — sda_out (the value to drive) and sda_oe (the output enable). When the master transmits, sda_oe is 1. To read the ACK bit, the master sets sda_oe to 0, releasing the line so the slave can pull it down. In the testbench the real wire is a tri1 net (high by default). The master contributes sda_oe ? sda_out : 1'bz, and a trivial slave contributes 1'b0 whenever the master is not driving — that is the acknowledge.
4. START, STOP and ACK
A START is a high-to-low transition on SDA while SCL is high; a STOP is the reverse, a low-to-high transition on SDA while SCL is high. These are the only times SDA may move while SCL is high, which is why the S_START and S_STOP states are handled separately from the data phases. During each acknowledge state the master samples sda_in while SCL is high: a 0 means the slave accepted the byte, a 1 raises ack_error. When the transaction finishes, done pulses high for one cycle and the machine returns to idle.
Compile and run everything with a single command:
iverilog -g2005 -o sim i2c_master.v && vvp sim
The testbench writes 0x5C to address 0x2A, prints each phase as it happens, and ends with a PASS line well before the timeout.
Common mistakes to avoid
- Changing SDA while SCL is high. Outside of START and STOP this is illegal and will confuse real slaves. Always update data in the SCL-low phase, as the FSM does in phase 0.
- Forgetting to release SDA for the ACK. If
sda_oestays high during the acknowledge, the master and slave fight over the line and you can never read a valid ACK. - Assuming push-pull drive. I2C is open-drain. In hardware you must drive low or tri-state — never actively drive a logic 1. The
sda_oe/sda_outpair maps directly onto an IO buffer’s output enable. - Omitting the pull-up in simulation. Without
tri1(or an explicitpullup), a released SDA floats toxand your ACK check reads garbage. - Reaching for SystemVerilog. Keywords like
logic,enumandtypedefare not Verilog-2005 and will fail under-g2005. Usereg/wireandlocalparaminteger states instead.
Quick check
Why does the master set sda_oe to 0 during the acknowledge phase?
Because the slave, not the master, drives the ACK bit. Setting the output enable to 0 releases (tri-states) SDA so the slave can pull it low; the master then samples sda_in while SCL is high to detect the 0.
What distinguishes a START condition from a STOP condition on the bus?
Both occur while SCL is high. A START is a high-to-low transition on SDA; a STOP is a low-to-high transition on SDA. These are the only moments SDA is allowed to change while the clock is high.
What does the parameter CLK_DIV control?
It sets the number of system-clock cycles in one quarter of an SCL bit period, so it determines the SCL frequency relative to the input clock. A small value shortens simulation; a larger value gives a realistic 100/400 kHz bus speed.
Why is open-drain modelling (drive-low or tri-state) used instead of driving SDA high directly?
I2C is a multi-device open-drain bus: a shared pull-up resistor provides the logic 1, and any device may pull the line low. Actively driving a 1 would cause bus contention if another device drives 0 at the same time, so devices only ever drive low or release.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses