0
0
VerilogHow-ToIntermediate · 4 min read

Verilog Code for I2C Slave: Syntax, Example, and Tips

An I2C slave in Verilog listens to the SCL clock and SDA data lines, responding to its address and handling read/write operations. The code typically includes state machines to detect start/stop conditions, address matching, and data transfer. You can implement it by monitoring signals and controlling SDA as an open-drain line.
📐

Syntax

An I2C slave module in Verilog usually has inputs for SCL (clock), SDA (data line, bidirectional), and a device address. It outputs data when addressed and acknowledges communication. The module uses a state machine to track start/stop conditions, address recognition, and data read/write phases.

Key parts include:

  • Start/Stop detection: Detect edges on SDA while SCL is high.
  • Address matching: Compare incoming bits to the device address.
  • Data transfer: Read or write bytes bit by bit synchronized with SCL.
  • Acknowledge: Drive SDA low to acknowledge receipt.
verilog
module i2c_slave(
    input wire scl,
    inout wire sda,
    input wire rst_n,
    input wire [6:0] slave_addr,
    output reg [7:0] data_out,
    input wire [7:0] data_in,
    output reg data_ready
);

// Internal signals
reg sda_out;
reg sda_oe; // Output enable for SDA (open-drain)
reg [3:0] bit_cnt;
reg [7:0] shift_reg;
reg [6:0] addr;
reg rw_flag; // 0 = write, 1 = read
reg start_detected;
reg stop_detected;
reg [1:0] state;

// SDA line control
assign sda = sda_oe ? 1'b0 : 1'bz;

// States
localparam IDLE = 2'b00;
localparam ADDR = 2'b01;
localparam DATA = 2'b10;

// Start and stop detection logic
reg sda_d, scl_d;
always @(posedge scl or negedge rst_n) begin
    if (!rst_n) begin
        sda_d <= 1'b1;
        scl_d <= 1'b1;
        start_detected <= 1'b0;
        stop_detected <= 1'b0;
    end else begin
        sda_d <= sda;
        scl_d <= scl;
        // Start condition: SDA falls while SCL is high
        start_detected <= (sda_d == 1'b1 && sda == 1'b0 && scl == 1'b1);
        // Stop condition: SDA rises while SCL is high
        stop_detected <= (sda_d == 1'b0 && sda == 1'b1 && scl == 1'b1);
    end
end

// Main state machine
always @(negedge scl or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
        bit_cnt <= 4'd0;
        sda_oe <= 1'b0;
        data_ready <= 1'b0;
    end else begin
        if (start_detected) begin
            state <= ADDR;
            bit_cnt <= 4'd7;
            data_ready <= 1'b0;
        end else if (stop_detected) begin
            state <= IDLE;
            sda_oe <= 1'b0;
            data_ready <= 1'b0;
        end else begin
            case (state)
                IDLE: begin
                    sda_oe <= 1'b0;
                end
                ADDR: begin
                    shift_reg[bit_cnt] <= sda;
                    if (bit_cnt == 0) begin
                        addr <= shift_reg[7:1];
                        rw_flag <= shift_reg[0];
                        if (shift_reg[7:1] == slave_addr) begin
                            sda_oe <= 1'b1; // ACK
                        end else begin
                            sda_oe <= 1'b0; // No ACK
                            state <= IDLE;
                        end
                        bit_cnt <= 4'd7;
                        if (shift_reg[7:1] == slave_addr) state <= DATA;
                    end else begin
                        bit_cnt <= bit_cnt - 1;
                    end
                end
                DATA: begin
                    if (rw_flag == 0) begin
                        // Master writes data to slave
                        shift_reg[bit_cnt] <= sda;
                        if (bit_cnt == 0) begin
                            data_out <= shift_reg;
                            data_ready <= 1'b1;
                            sda_oe <= 1'b1; // ACK
                            bit_cnt <= 4'd7;
                        end else begin
                            bit_cnt <= bit_cnt - 1;
                        end
                    end else begin
                        // Slave sends data to master
                        sda_oe <= 1'b1;
                        sda_out <= data_in[bit_cnt];
                        if (bit_cnt == 0) begin
                            bit_cnt <= 4'd7;
                            sda_oe <= 1'b0; // Release SDA for ACK from master
                        end else begin
                            bit_cnt <= bit_cnt - 1;
                        end
                    end
                end
            endcase
        end
    end
end

endmodule
💻

Example

This example shows a simple I2C slave that listens for its 7-bit address 0x42. It detects start and stop conditions, acknowledges the address, and receives one byte of data from the master. When data is received, it sets data_ready high and stores the byte in data_out.

verilog
module i2c_slave_example(
    input wire scl,
    inout wire sda,
    input wire rst_n,
    output reg [7:0] data_out,
    output reg data_ready
);

    wire [6:0] slave_addr = 7'h42;
    reg [7:0] data_in = 8'h00; // Not used in this example

    i2c_slave slave_inst(
        .scl(scl),
        .sda(sda),
        .rst_n(rst_n),
        .slave_addr(slave_addr),
        .data_out(data_out),
        .data_in(data_in),
        .data_ready(data_ready)
    );

endmodule
Output
No textual output; hardware signals respond to I2C bus activity.
⚠️

Common Pitfalls

Common mistakes when coding an I2C slave in Verilog include:

  • Not detecting start and stop conditions correctly, which causes the slave to miss communication.
  • Forgetting that SDA is open-drain and must be driven low or released (high impedance), never driven high directly.
  • Incorrect bit counting or shifting, leading to wrong address or data reception.
  • Not handling the acknowledge bit properly, causing the master to timeout.

Always synchronize SCL and SDA signals properly and test with real I2C traffic.

verilog
/* Wrong way: Driving SDA high directly */
assign sda = sda_oe ? 1'b1 : 1'bz; // Incorrect for open-drain

/* Right way: Open-drain style */
assign sda = sda_oe ? 1'b0 : 1'bz; // Correct: drive low or release line
📊

Quick Reference

Tips for writing an I2C slave in Verilog:

  • Use edge detection on SDA and SCL to find start/stop conditions.
  • Implement a state machine to handle address and data phases.
  • Drive SDA low to acknowledge; otherwise, release it (high impedance).
  • Keep bit counters to track byte reception and transmission.
  • Test with an I2C master simulator or hardware to verify timing and behavior.

Key Takeaways

Detect start and stop conditions by monitoring SDA changes while SCL is high.
Use an open-drain approach for SDA: drive low or release line, never drive high directly.
Implement a state machine to manage address recognition and data transfer phases.
Acknowledge the master by pulling SDA low after receiving address or data bytes.
Test your I2C slave design with real or simulated I2C traffic for correct timing.