HomeFPGA from ScratchDay 22
DAY 22 · SERIAL COMMUNICATION

I2C Master Controller

By EcrioniX · Updated Jun 11, 2026

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.

1. I2C bus fundamentals

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.

START and STOP conditions

2. A write transaction sequence

To write byte 0xAB to a device at address 0x48:

  1. Generate START condition (SDA↓ while SCL↑)
  2. Send address byte: 0x48 shifted left + W bit (0) = 0x90
  3. Check ACK (device pulls SDA low after 9th clock)
  4. Send data byte: 0xAB
  5. Check ACK
  6. Generate STOP condition (SDA↑ while SCL↑)

3. Port table — i2c_master

PortDirWidthDescription
clkIN1System clock (e.g. 100 MHz)
rstIN1Synchronous reset
start_txIN1Pulse high to initiate a write transaction
addrIN77-bit slave address
data_byteIN8Data byte to write after address phase
sclOUT1I2C clock (open-drain model: drives 0 or releases)
sda_outOUT1SDA drive value (0=pull low, 1=release/high-Z)
sda_inIN1SDA bus value (read for ACK)
busyOUT1High while transaction is in progress
doneOUT1Pulses high when transaction completes
ack_errOUT1Pulses high if slave did not acknowledge (NACK)

4. i2c_master.v

i2c_master.v
// 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

5. Testbench — tb_i2c_master.v

tb_i2c_master.v
// 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

6. Expected output

[... 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)

Key Takeaways

Frequently Asked Questions

What makes I2C different from SPI?

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.

What is the I2C open-drain requirement?

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.

What are START and STOP conditions?

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.

← Previous
Day 21: SPI Master