I2C Slave in Verilog: Address Match & ACK with Code
An I2C slave is a synchronous peripheral that listens on the two-wire I2C bus (SCL clock and SDA data), recognizes a START condition, shifts in the address byte, compares it to its own address, and acknowledges by pulling SDA low. This page gives you clean, synthesizable i2c slave verilog code for both write and read transactions, each paired with a self-checking testbench that plays the role of the I2C master.
Both designs oversample SCL and SDA with a fast system clock, which keeps START/STOP detection, address matching, and ACK generation inside a single clocked process — the robust, synthesizable way to build an I2C slave in Verilog. Every example below is compiled and run under Icarus Verilog (iverilog -g2005); the simulation log is captured live.
This I2C slave matches a parameterized 7-bit address and receives one data byte. The testbench generates a real START, drives the address plus R/W bit, checks that the slave pulls SDA low for the ACK, sends a data byte, and issues a STOP — then self-checks the captured address-match and data value, and that a wrong address is NACKed.
// I2C slave: 7-bit address match + receive one data byte (write transaction)
module i2c_slave #(parameter [6:0] SADDR = 7'h42) (
input wire clk, // oversampling clock (>> SCL)
input wire rst, // synchronous reset, active high
input wire scl, // I2C clock line (driven by master)
inout wire sda, // I2C data line (open-drain, bidirectional)
output reg [7:0] data_out, // last received data byte
output reg data_valid, // 1-clk pulse when data_out updates
output reg addr_hit // high once our address matched
);
// Synchronize bus lines and build 3-deep history for edge detection
reg [2:0] sclq, sdaq;
always @(posedge clk) begin
sclq <= {sclq[1:0], scl};
sdaq <= {sdaq[1:0], sda};
end
wire scl_rise = (sclq[2:1] == 2'b01);
wire scl_fall = (sclq[2:1] == 2'b10);
wire sda_rise = (sdaq[2:1] == 2'b01);
wire sda_fall = (sdaq[2:1] == 2'b10);
wire scl_high = sclq[1];
// START = SDA falls while SCL high; STOP = SDA rises while SCL high
wire start_cond = sda_fall & scl_high;
wire stop_cond = sda_rise & scl_high;
localparam IDLE = 3'd0, ADDR = 3'd1, AACK = 3'd2, RX = 3'd3, RACK = 3'd4;
reg [2:0] state;
reg [3:0] bitc;
reg [7:0] sh;
reg sda_drv; // 1 => actively pull SDA low (open-drain)
assign sda = sda_drv ? 1'b0 : 1'bz;
always @(posedge clk) begin
if (rst) begin
state <= IDLE; sda_drv <= 1'b0; data_valid <= 1'b0;
addr_hit <= 1'b0; bitc <= 4'd0;
end else begin
data_valid <= 1'b0;
if (start_cond) begin
state <= ADDR; bitc <= 4'd0; sda_drv <= 1'b0;
end else if (stop_cond) begin
state <= IDLE; sda_drv <= 1'b0;
end else begin
case (state)
ADDR: begin
if (scl_rise) begin sh <= {sh[6:0], sda}; bitc <= bitc + 4'd1; end
if (scl_fall && bitc == 4'd8) begin
if (sh[7:1] == SADDR) begin
addr_hit <= 1'b1; sda_drv <= 1'b1; state <= AACK; // ACK
end else begin
addr_hit <= 1'b0; state <= IDLE; // NACK
end
end
end
AACK: if (scl_fall) begin sda_drv <= 1'b0; bitc <= 4'd0; state <= RX; end
RX: begin
if (scl_rise) begin sh <= {sh[6:0], sda}; bitc <= bitc + 4'd1; end
if (scl_fall && bitc == 4'd8) begin
data_out <= sh; data_valid <= 1'b1; sda_drv <= 1'b1; state <= RACK;
end
end
RACK: if (scl_fall) begin sda_drv <= 1'b0; bitc <= 4'd0; state <= RX; end
default: state <= IDLE;
endcase
end
end
end
endmodule
// Testbench acts as an I2C master and self-checks the slave
module tb;
localparam integer Q = 10; // SCL phase in clk ticks
reg clk = 1'b0;
always #1 clk = ~clk; // 2-tick period oversampling clock
reg m_scl = 1'b1; // master drives SCL push-pull
reg m_oe = 1'b0; // 1 => master pulls SDA low
wire scl = m_scl;
wire sda;
pullup(sda); // bus idle-high resistor
assign sda = m_oe ? 1'b0 : 1'bz;
reg rst = 1'b1;
wire [7:0] data_out;
wire data_valid, addr_hit;
i2c_slave #(.SADDR(7'h42)) dut (
.clk(clk), .rst(rst), .scl(scl), .sda(sda),
.data_out(data_out), .data_valid(data_valid), .addr_hit(addr_hit)
);
task m_start; begin
m_oe = 1'b0; m_scl = 1'b1; #Q; m_oe = 1'b1; #Q; m_scl = 1'b0; #Q;
end endtask
task m_bit(input b); begin
m_scl = 1'b0; m_oe = ~b; #Q; // set SDA while SCL low
m_scl = 1'b1; #Q; // clock it high (slave samples)
m_scl = 1'b0; #Q;
end endtask
task m_byte(input [7:0] d); integer i; begin
for (i = 7; i >= 0; i = i - 1) m_bit(d[i]);
end endtask
task m_getack(output a); begin
m_scl = 1'b0; m_oe = 1'b0; #Q; // release SDA so slave can ACK
m_scl = 1'b1; #(Q/2); a = sda; #(Q/2);
m_scl = 1'b0; #Q;
end endtask
task m_stop; begin
m_scl = 1'b0; m_oe = 1'b1; #Q; // SDA low while SCL low
m_scl = 1'b1; #Q; m_oe = 1'b0; #Q; // SDA rises while SCL high = STOP
end endtask
integer errors = 0;
reg ack_addr, ack_data;
initial begin
#4 rst = 1'b0;
#Q;
// --- Write transaction to address 0x42, R/W=0, data 0xA5 ---
m_start;
m_byte({7'h42, 1'b0}); // address byte 0x84
m_getack(ack_addr);
m_byte(8'hA5); // one data byte
m_getack(ack_data);
m_stop;
#Q;
if (addr_hit !== 1'b1) begin errors = errors + 1; $display("FAIL addr_hit=%b exp 1", addr_hit); end
else $display("PASS address 0x42 matched (addr_hit=1)");
if (ack_addr !== 1'b0) begin errors = errors + 1; $display("FAIL address ACK=%b exp 0", ack_addr); end
else $display("PASS slave pulled SDA low for address ACK");
if (ack_data !== 1'b0) begin errors = errors + 1; $display("FAIL data ACK=%b exp 0", ack_data); end
else $display("PASS slave pulled SDA low for data ACK");
if (data_out !== 8'hA5) begin errors = errors + 1; $display("FAIL data_out=%02x exp A5", data_out); end
else $display("PASS received data byte 0x%02x", data_out);
// --- Wrong address 0x11 must NOT be acked ---
m_start;
m_byte({7'h11, 1'b0});
m_getack(ack_addr);
m_stop;
#Q;
if (ack_addr !== 1'b1) begin errors = errors + 1; $display("FAIL wrong-addr ACK=%b exp 1(NACK)", ack_addr); end
else $display("PASS unmatched address 0x11 got NACK (SDA stayed high)");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d CHECK(S) FAILED", errors);
$finish;
end
endmodule
PASS address 0x42 matched (addr_hit=1) PASS slave pulled SDA low for address ACK PASS slave pulled SDA low for data ACK PASS received data byte 0xa5 PASS unmatched address 0x11 got NACK (SDA stayed high) ALL TESTS PASSEDThe log shows the slave matching address 0x42, pulling SDA low to ACK both the address and the data phase, capturing data byte 0xA5, and correctly NACKing an unmatched address 0x11. A real bug (wrong match, missing ACK, or corrupted byte) would print a FAIL line, so
ALL TESTS PASSEDconfirms the protocol behavior end to end.This variant exposes a single 8-bit register that the master can write and then read back. On a read transaction the slave transmits the register byte MSB-first by driving SDA, and the testbench sweeps three values (0x3C, 0x5A, 0xFF), writing each and reading it back against a golden copy.
// I2C slave exposing one 8-bit register: master can WRITE it then READ it back. module i2c_slave_reg #(parameter [6:0] SADDR = 7'h50) ( input wire clk, input wire rst, input wire scl, inout wire sda, output reg [7:0] reg_data // the register, visible to the system ); reg [2:0] sclq, sdaq; always @(posedge clk) begin sclq <= {sclq[1:0], scl}; sdaq <= {sdaq[1:0], sda}; end wire scl_rise = (sclq[2:1] == 2'b01); wire scl_fall = (sclq[2:1] == 2'b10); wire sda_rise = (sdaq[2:1] == 2'b01); wire sda_fall = (sdaq[2:1] == 2'b10); wire scl_high = sclq[1]; wire start_cond = sda_fall & scl_high; wire stop_cond = sda_rise & scl_high; localparam IDLE=3'd0, ADDR=3'd1, AACK=3'd2, RX=3'd3, RACK=3'd4, TX=3'd5, TACK=3'd6; reg [2:0] state; reg [3:0] bitc; reg [7:0] sh, txsh; reg rw; // latched R/W bit (1 = master reads) reg sda_drv; assign sda = sda_drv ? 1'b0 : 1'bz; always @(posedge clk) begin if (rst) begin state <= IDLE; sda_drv <= 1'b0; bitc <= 4'd0; reg_data <= 8'h00; end else if (start_cond) begin state <= ADDR; bitc <= 4'd0; sda_drv <= 1'b0; end else if (stop_cond) begin state <= IDLE; sda_drv <= 1'b0; end else begin case (state) ADDR: begin if (scl_rise) begin sh <= {sh[6:0], sda}; bitc <= bitc + 4'd1; end if (scl_fall && bitc == 4'd8) begin if (sh[7:1] == SADDR) begin rw <= sh[0]; sda_drv <= 1'b1; state <= AACK; // ACK address end else state <= IDLE; // NACK end end AACK: if (scl_fall) begin bitc <= 4'd0; if (sh[0]) begin // read: start driving MSB first txsh <= reg_data; sda_drv <= ~reg_data[7]; bitc <= 4'd1; state <= TX; end else begin // write: release, receive a byte sda_drv <= 1'b0; state <= RX; end end RX: begin if (scl_rise) begin sh <= {sh[6:0], sda}; bitc <= bitc + 4'd1; end if (scl_fall && bitc == 4'd8) begin reg_data <= sh; sda_drv <= 1'b1; state <= RACK; // store + ACK end end RACK: if (scl_fall) begin sda_drv <= 1'b0; bitc <= 4'd0; state <= RX; end TX: if (scl_fall) begin if (bitc == 4'd8) begin sda_drv <= 1'b0; state <= TACK; end // release for master ACK/NACK else begin txsh <= {txsh[6:0], 1'b0}; sda_drv <= ~txsh[6]; bitc <= bitc + 4'd1; end end TACK: if (scl_fall) begin sda_drv <= 1'b0; state <= IDLE; end default: state <= IDLE; endcase end end endmodule module tb; localparam integer Q = 10; reg clk = 1'b0; always #1 clk = ~clk; reg m_scl = 1'b1, m_oe = 1'b0; wire scl = m_scl; wire sda; pullup(sda); assign sda = m_oe ? 1'b0 : 1'bz; reg rst = 1'b1; wire [7:0] reg_data; i2c_slave_reg #(.SADDR(7'h50)) dut ( .clk(clk), .rst(rst), .scl(scl), .sda(sda), .reg_data(reg_data) ); task m_start; begin m_oe=1'b0; m_scl=1'b1; #Q; m_oe=1'b1; #Q; m_scl=1'b0; #Q; end endtask task m_bit(input b); begin m_scl=1'b0; m_oe=~b; #Q; m_scl=1'b1; #Q; m_scl=1'b0; #Q; end endtask task m_byte(input [7:0] d); integer i; begin for (i=7;i>=0;i=i-1) m_bit(d[i]); end endtask task m_getack(output a); begin m_scl=1'b0; m_oe=1'b0; #Q; m_scl=1'b1; #(Q/2); a=sda; #(Q/2); m_scl=1'b0; #Q; end endtask task m_rbyte(output [7:0] d); integer i; begin d = 8'h00; for (i=7;i>=0;i=i-1) begin m_scl=1'b0; m_oe=1'b0; #Q; // release so slave drives m_scl=1'b1; #(Q/2); d[i]=sda; #(Q/2); m_scl=1'b0; #Q; end end endtask task m_nack; begin // master NACKs the last byte m_scl=1'b0; m_oe=1'b0; #Q; m_scl=1'b1; #Q; m_scl=1'b0; #Q; end endtask task m_stop; begin m_scl=1'b0; m_oe=1'b1; #Q; m_scl=1'b1; #Q; m_oe=1'b0; #Q; end endtask integer errors = 0, k; reg a; reg [7:0] rd; reg [7:0] vec [0:2]; task write_reg(input [7:0] v); begin m_start; m_byte({7'h50,1'b0}); m_getack(a); m_byte(v); m_getack(a); m_stop; #Q; end endtask task read_reg(output [7:0] v); begin m_start; m_byte({7'h50,1'b1}); m_getack(a); m_rbyte(v); m_nack; m_stop; #Q; end endtask initial begin vec[0]=8'h3C; vec[1]=8'h5A; vec[2]=8'hFF; #4 rst=1'b0; #Q; for (k=0;k<3;k=k+1) begin write_reg(vec[k]); if (reg_data !== vec[k]) begin errors=errors+1; $display("FAIL reg_data=%02x exp %02x", reg_data, vec[k]); end else $display("PASS wrote 0x%02x into slave register", vec[k]); read_reg(rd); if (rd !== vec[k]) begin errors=errors+1; $display("FAIL read-back=%02x exp %02x", rd, vec[k]); end else $display("PASS read back 0x%02x over I2C (matches golden)", rd); end if (errors==0) $display("ALL TESTS PASSED"); else $display("%0d CHECK(S) FAILED", errors); $finish; end endmodulePASS wrote 0x3c into slave register PASS read back 0x3c over I2C (matches golden) PASS wrote 0x5a into slave register PASS read back 0x5a over I2C (matches golden) PASS wrote 0xff into slave register PASS read back 0xff over I2C (matches golden) ALL TESTS PASSEDFor each of the three vectors the log confirms the written value landed in the slave register and that the byte clocked back out over I2C matches the golden value bit-for-bit. Because the read path drives SDA only when transmitting a 0 (open-drain), any timing or shift-direction bug would corrupt the read-back and trip a FAIL.
The I2C START, STOP and ACK protocol
I2C is a two-wire, multi-drop bus. SCL carries the clock from the master; SDA carries data in both directions. Two special conditions frame every transaction and are the only times SDA is allowed to change while SCL is high:
- START: SDA goes high→low while SCL is high. It begins a transaction and makes every slave on the bus start listening.
- STOP: SDA goes low→high while SCL is high. It releases the bus back to idle.
During data bits, SDA may only change while SCL is low; the receiver samples SDA on the rising edge of SCL. After every 8 data bits the transmitter releases SDA for one clock and the receiver drives the 9th bit: a low is ACK (acknowledge), a high left by the pull-up is NACK. In the code, the slave detects START/STOP with
sda_fall & scl_highandsda_rise & scl_highafter synchronizing both lines.Open-drain SDA and why we model tri-state
Real I2C lines are open-drain: a device can only pull the line low or release it to a pull-up resistor; no device ever drives a hard 1. This is what lets ACK work — the master releases SDA and the slave pulls it low. The Verilog models this exactly with a driven-low / high-Z pattern:
reg sda_drv; assign sda = sda_drv ? 1'b0 : 1'bz; // pull low, else releaseThe testbench adds
pullup(sda);so a released line reads as 1, just like the physical resistor. Both master and slave tie into the samesdanet, and whichever one asserts1'b0wins — a faithful model of a wired-AND bus. Never drive1'b1onto SDA; doing so would model a bus-contention short that cannot happen in hardware.Address matching
The first byte after START is the address frame: 7 address bits MSB-first followed by the R/W bit (0 = master writes, 1 = master reads). The slave shifts all 8 bits into
sh, so after the 8th rising edgesh[7:1]holds the 7-bit address andsh[0]holds R/W. On the falling edge after bit 8 the slave comparessh[7:1]to its parameterSADDR; on a match it pulls SDA low for the ACK and latches the direction, otherwise it returns to idle and leaves SDA high (NACK). Parameterizing the address (#(parameter [6:0] SADDR = 7'h42)) lets you instantiate several slaves on one bus, each answering to a different address.Clock stretching
I2C lets a slave that needs more time hold SCL low after an ACK; the master, which only releases SCL rather than forcing it high in a true open-drain design, waits until the line rises again. This is clock stretching. The slaves on this page do not stretch — they keep pace with the oversampling clock — but the hook is simple: make SCL open-drain too and let the slave assert
scl_drvlow until its data or register access is ready. For a self-contained, deterministic testbench we drive SCL push-pull from the master, which is why stretching is not exercised here.Common pitfalls
- Driving SDA high. Use high-Z plus a pull-up, never
assign sda = value;with a 1. Hard-driving 1 breaks ACK and models an illegal short.- Changing SDA while SCL is high. Outside of START/STOP this corrupts the frame. Set up data only while SCL is low.
- Off-by-one on the ACK clock. ACK is the 9th clock, driven on the falling edge after bit 8 and released after the 9th falling edge. Miscounting leaves SDA low into the next data bit.
- Metastability. SCL and SDA are asynchronous to your system clock; always synchronize them (here, 3-deep shift registers) before edge detection, or glitches will fire false START/STOP events.
- Sampling SDA on the wrong edge. Receivers sample on the SCL rising edge; transmitters change data on the falling edge.
Synthesis notes
Both modules are written for synthesis: a single
always @(posedge clk)process, a one-hot-friendly state enum vialocalparam, synchronous reset, and no latches (every branch assigns the registers it needs or inherits the default). The only tri-state is the SDA output, which maps to a bidirectional I/O buffer with an output-enable — on an FPGA that is the pad'sT(tri-state) control, on an ASIC an open-drain pad cell. Pick an oversampling clock at least ~8–16× the maximum SCL (e.g. 10 MHz clock for 400 kHz fast-mode I2C) so edge detection has margin. For production, add input glitch filtering and optional clock stretching.FAQ
Why oversample instead of using SCL as a clock directly?
Clocking flip-flops on SCL (a slow, noisy, externally driven line) is fragile and makes START/STOP detection — which happens while SCL is steady — awkward. Oversampling with a clean system clock keeps the whole slave in one clock domain and is the standard synthesizable approach.
How does the slave know a read from a write?
The R/W bit is the 8th bit of the address frame. The code latches it as
rw/sh[0]and branches to the receive path (write) or transmit path (read) after the address ACK.Can I put several of these on one bus?
Yes — give each a different
SADDR. Only the addressed slave drives the ACK; the rest return to idle on an address mismatch.Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres