I2C is the go-to protocol for sensor boards — one pair of wires connects dozens of devices, each with its own 7-bit address. Temperature sensors, EEPROM, IMUs, and RTC chips all speak I2C. Building a working I2C master requires understanding the peculiar open-drain bus model and the strict rules about when SDA may change. This lesson constructs a complete write-capable I2C master with ACK detection.
I2C uses two bidirectional open-drain lines: SCL (clock) and SDA (data). "Open-drain" means every device can only pull a line low — releasing it lets the pull-up resistor bring it high. Any device can hold the bus low; the bus is high only when everyone releases it.
To write byte 0xAB to a device at address 0x48:
| Port | Dir | Width | Description |
|---|---|---|---|
| clk | IN | 1 | System clock (e.g. 100 MHz) |
| rst | IN | 1 | Synchronous reset |
| start_tx | IN | 1 | Pulse high to initiate a write transaction |
| addr | IN | 7 | 7-bit slave address |
| data_byte | IN | 8 | Data byte to write after address phase |
| scl | OUT | 1 | I2C clock (open-drain model: drives 0 or releases) |
| sda_out | OUT | 1 | SDA drive value (0=pull low, 1=release/high-Z) |
| sda_in | IN | 1 | SDA bus value (read for ACK) |
| busy | OUT | 1 | High while transaction is in progress |
| done | OUT | 1 | Pulses high when transaction completes |
| ack_err | OUT | 1 | Pulses high if slave did not acknowledge (NACK) |
// i2c_master.v — I2C Master: write 1 byte to addressed slave
// CLK_DIV: system_clk / (4 * i2c_clk)
// For 100 kHz I2C from 100 MHz: CLK_DIV = 250
// Each I2C bit takes 4 CLK_DIV periods (quarter-bit resolution)
module i2c_master #(
parameter CLK_DIV = 250 // 100 kHz I2C from 100 MHz
)(
input wire clk,
input wire rst,
input wire start_tx,
input wire [6:0] addr,
input wire [7:0] data_byte,
output reg scl,
output reg sda_out, // 1=release(high), 0=pull-low
input wire sda_in, // actual SDA line value
output reg busy,
output reg done,
output reg ack_err
);
// Quarter-bit counter
reg [9:0] div_cnt;
reg qbit; // quarter-bit tick
always @(posedge clk) begin
if (rst) begin div_cnt <= 0; qbit <= 0; end
else begin
qbit <= 0;
if (div_cnt == CLK_DIV - 1) begin
div_cnt <= 0;
qbit <= 1;
end else div_cnt <= div_cnt + 1;
end
end
// FSM — each state advances on qbit ticks
// We drive SCL with 4 qbits per bit period (LOW-LOW-HIGH-HIGH)
localparam [4:0]
S_IDLE = 0,
S_START1 = 1, // SDA=1, SCL=1 → SDA=0 (START: SDA↓ while SCL↑)
S_START2 = 2, // SCL=0 (pull clock low after START)
S_ADDR = 3, // send 7-bit address MSB first (8 bits incl. W)
S_ACK1 = 4, // release SDA, clock high for ACK
S_DATA = 5, // send 8-bit data
S_ACK2 = 6, // ACK for data byte
S_STOP1 = 7, // SCL↑ then SDA↑ (STOP condition)
S_DONE = 8;
reg [4:0] state;
reg [3:0] bit_cnt; // bit counter
reg [7:0] shift; // current byte being sent
reg [1:0] qcnt; // which quarter of the bit we are in
reg ack_ok;
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
scl <= 1; sda_out <= 1;
busy <= 0; done <= 0; ack_err <= 0;
bit_cnt <= 0; shift <= 0; qcnt <= 0; ack_ok <= 0;
end else begin
done <= 0;
ack_err <= 0;
case (state)
S_IDLE: begin
scl <= 1; sda_out <= 1; busy <= 0;
if (start_tx) begin
busy <= 1;
state <= S_START1;
qcnt <= 0;
end
end
// START: SDA falls while SCL is high
S_START1: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin scl <= 1; sda_out <= 1; end
1: begin scl <= 1; sda_out <= 0; end // SDA↓ while SCL=1 = START
2: begin scl <= 0; sda_out <= 0; end // pull SCL low
3: begin
// load address byte (7-bit addr + W=0) MSB first
shift <= {addr, 1'b0};
bit_cnt <= 7;
qcnt <= 0;
state <= S_ADDR;
end
endcase
end
// Send 8 address bits (addr[6:0] + W=0)
S_ADDR: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin sda_out <= shift[7]; scl <= 0; end // set data, SCL low
1: begin scl <= 0; end
2: begin scl <= 1; end // SCL rises — slave samples SDA
3: begin
scl <= 1;
qcnt <= 0;
if (bit_cnt == 0) begin
state <= S_ACK1; // move to ACK phase
end else begin
shift <= {shift[6:0], 1'b0};
bit_cnt <= bit_cnt - 1;
end
end
endcase
end
// ACK1: release SDA, clock one bit, check if slave pulls SDA low
S_ACK1: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin sda_out <= 1; scl <= 0; end // release SDA for slave ACK
1: begin scl <= 0; end
2: begin scl <= 1; end
3: begin
ack_ok <= ~sda_in; // 0=ACK, 1=NACK
scl <= 1;
shift <= data_byte;
bit_cnt <= 7;
qcnt <= 0;
state <= S_DATA;
end
endcase
end
// Send 8 data bits
S_DATA: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin sda_out <= shift[7]; scl <= 0; end
1: begin scl <= 0; end
2: begin scl <= 1; end
3: begin
scl <= 1; qcnt <= 0;
if (bit_cnt == 0) begin
state <= S_ACK2;
end else begin
shift <= {shift[6:0], 1'b0};
bit_cnt <= bit_cnt - 1;
end
end
endcase
end
// ACK2: check data byte ACK
S_ACK2: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin sda_out <= 1; scl <= 0; end
1: begin scl <= 0; end
2: begin scl <= 1; end
3: begin
if (~sda_in) ack_ok <= 1; else ack_ok <= 0;
scl <= 0;
qcnt <= 0;
state <= S_STOP1;
end
endcase
end
// STOP: SCL↑ then SDA↑ while SCL high
S_STOP1: if (qbit) begin
qcnt <= qcnt + 1;
case (qcnt)
0: begin sda_out <= 0; scl <= 0; end
1: begin scl <= 1; end
2: begin sda_out <= 1; end // SDA↑ while SCL=1 = STOP
3: begin
scl <= 1; qcnt <= 0;
state <= S_DONE;
end
endcase
end
S_DONE: begin
done <= 1;
ack_err <= ~ack_ok;
busy <= 0;
state <= S_IDLE;
end
endcase
end
end
endmodule
// tb_i2c_master.v — verifies START/STOP and ACK for a write transaction
`timescale 1ns/1ps
module tb_i2c_master;
parameter CLK_DIV = 4; // fast for simulation
reg clk = 0;
reg rst = 1;
reg start_tx = 0;
reg [6:0] addr = 7'h48;
reg [7:0] data_byte = 8'hAB;
wire scl;
wire sda_out;
reg sda_in = 1; // simulate slave pulling ACK
wire busy, done, ack_err;
i2c_master #(.CLK_DIV(CLK_DIV)) dut (
.clk(clk),.rst(rst),.start_tx(start_tx),
.addr(addr),.data_byte(data_byte),
.scl(scl),.sda_out(sda_out),.sda_in(sda_in),
.busy(busy),.done(done),.ack_err(ack_err)
);
always #5 clk = ~clk;
integer pass_cnt = 0, fail_cnt = 0;
integer scl_rise = 0;
reg start_detected = 0;
reg stop_detected = 0;
reg sda_prev = 1;
reg scl_prev = 1;
// Detect START and STOP conditions
always @(sda_out or scl) begin
if (scl && ~sda_out && sda_prev) begin
start_detected = 1;
$display("[%0t ns] START condition detected", $time);
end
if (scl && sda_out && ~sda_prev) begin
stop_detected = 1;
$display("[%0t ns] STOP condition detected", $time);
end
sda_prev = sda_out;
end
// Simulate slave ACK: pull SDA low during ACK clocks
// (simplified: always ACK by tying sda_in=0 during ACK windows)
initial begin
$dumpfile("tb_i2c_master.vcd");
$dumpvars(0, tb_i2c_master);
repeat(4) @(posedge clk);
rst = 0;
repeat(2) @(posedge clk);
// Send a write transaction: addr=0x48, data=0xAB
// Slave will ACK (sda_in=0 simulated below)
@(posedge clk);
start_tx = 1;
@(posedge clk);
start_tx = 0;
// Simulate slave pulling SDA low for both ACK windows
// (Hold sda_in=0 throughout for simplicity — a real slave would
// only pull during the ACK bit. For testbench purposes this works.)
sda_in = 0;
// Wait for done
@(posedge done);
@(posedge clk);
// Release sda_in
sda_in = 1;
// Check: START was detected
if (start_detected) begin
$display("PASS: START condition generated"); pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: START condition NOT detected"); fail_cnt = fail_cnt + 1;
end
// Check: STOP was detected
if (stop_detected) begin
$display("PASS: STOP condition generated"); pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: STOP condition NOT detected"); fail_cnt = fail_cnt + 1;
end
// Check: no ack_err (slave acknowledged)
if (!ack_err) begin
$display("PASS: ACK received (no ack_err)"); pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: ack_err asserted unexpectedly"); fail_cnt = fail_cnt + 1;
end
// Check busy released
if (!busy) begin
$display("PASS: busy deasserted after done"); pass_cnt = pass_cnt + 1;
end else begin
$display("FAIL: busy still high after done"); fail_cnt = fail_cnt + 1;
end
if (fail_cnt == 0)
$display("\nALL TESTS PASSED (%0d/%0d)", pass_cnt, pass_cnt+fail_cnt);
else
$display("\nFAILED: %0d passed, %0d failed", pass_cnt, fail_cnt);
$finish;
end
initial #500000 begin $display("TIMEOUT"); $finish; end
endmodule
[... ns] START condition detected [... ns] STOP condition detected PASS: START condition generated PASS: STOP condition generated PASS: ACK received (no ack_err) PASS: busy deasserted after done ALL TESTS PASSED (4/4)
I2C uses 2 wires and supports 127 devices on one bus via 7-bit addressing. SPI needs 4 wires plus a CS pin per device. I2C is slower (100–400 kHz typical) but far more wiring-efficient for multi-device boards.
Devices can only pull I2C lines low — they cannot drive high. The line goes high via an external pull-up resistor. In Verilog model this as: 0 to pull low, 1 (or high-Z) to release. This allows multiple devices to share the bus without contention.
START: SDA falls while SCL is high — initiates a transaction. STOP: SDA rises while SCL is high — ends a transaction. All data transitions happen only while SCL is low. Any SDA change while SCL is high is a bus control event.