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 PASSED

The 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 PASSED confirms 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
endmodule
PASS 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 PASSED

For 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_high and sda_rise & scl_high after 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 release

The testbench adds pullup(sda); so a released line reads as 1, just like the physical resistor. Both master and slave tie into the same sda net, and whichever one asserts 1'b0 wins — a faithful model of a wired-AND bus. Never drive 1'b1 onto 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 edge sh[7:1] holds the 7-bit address and sh[0] holds R/W. On the falling edge after bit 8 the slave compares sh[7:1] to its parameter SADDR; 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_drv low 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 via localparam, 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's T (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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD