Lesson 12

📡 I2C通信

🏆 起始+地址+ACK正确
✅ Verilator仿真验证通过

📖 实验描述

实现I2C主机,完成起始条件、7位地址+读写位、ACK检测的基本写操作。

I2C(Inter-Integrated Circuit)是两线制同步半双工总线。 2线制:SCL(时钟)、SDA(数据)。SDA是开漏输出,需外部上拉电阻。 I2C通信流程(写操作): 1. 起始条件:SCL高时SDA下降沿 2. 发送7位从机地址+读写位(0=写) 3. 等待从机ACK(SDA=0) 4. 发送8位数据 5. 等待ACK 6. 停止条件:SCL高时SDA上升沿 I2C是半双工:同一SDA线分时用于发送和接收。标准模式100kHz,快速模式400kHz。 I2C总线可挂载多个从设备,每个有唯一7位地址。

🧠 核心概念

📐 电路结构

电路与状态图
I2C写操作时序:

  SCL  ──┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─
          └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
  SDA  ──┐  A6  A5  A4  A3  A2  A1  A0  R/W ACK
          └──────────────────────────────────
         START                               

  START: SCL=1, SDA 1→0
  STOP:  SCL=1, SDA 0→1

📝 设计步骤

  1. 1生成起始条件(SCL高时SDA下降沿)
  2. 2发送7位地址+读写位(MSB first)
  3. 3SCL每个高电平期间SDA稳定
  4. 4第9个时钟读取ACK(SDA=0为应答)
  5. 5发送8位数据字节
  6. 6生成停止条件(SCL高时SDA上升沿)

💻 Verilog实现

i2c.svSystemVerilog · Verilator 5.020
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
// I2C Master - I2C主机
// 7位地址, 写操作, 标准模式100kHz
module i2c_master(
    input  wire       clk,       // 50MHz
    input  wire       rst,
    input  wire [6:0] addr,      // 7位从机地址
    input  wire [7:0] wr_data,   // 写入数据
    input  wire       start,     // 开始传输
    output reg        done,      // 传输完成
    output reg        ack_ok,    // 从机ACK正常
    // I2C bus
    output reg        scl_o,     // SCL输出
    output reg        sda_o,     // SDA输出
    input  wire       sda_i      // SDA输入(读ACK)
);

localparam DIV = 16'd250;  // 50MHz/(4*250) = 50kHz (SCL=25kHz)
localparam HALF_DIV = DIV / 2;

typedef enum logic [3:0] {
    S_IDLE   = 4'd0,
    S_START  = 4'd1,
    S_ADDR   = 4'd2,
    S_ACK1   = 4'd3,
    S_DATA   = 4'd4,
    S_ACK2   = 4'd5,
    S_STOP   = 4'd6,
    S_DONE   = 4'd7
} state_t;

state_t state;
reg [15:0] clk_cnt;
reg [3:0]  bit_idx;
reg [7:0]  shift_reg;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        state     <= S_IDLE;
        scl_o     <= 1'b1;
        sda_o     <= 1'b1;
        done      <= 1'b0;
        ack_ok    <= 1'b0;
        clk_cnt   <= 16'd0;
        bit_idx   <= 4'd0;
        shift_reg <= 8'd0;
    end else begin
        done <= 1'b0;

        case (state)
            S_IDLE: begin
                scl_o <= 1'b1;
                sda_o <= 1'b1;
                if (start) begin
                    state     <= S_START;
                    shift_reg <= {addr, 1'b0};  // Address + Write bit
                    bit_idx   <= 4'd0;
                    clk_cnt   <= 16'd0;
                end
            end
            S_START: begin
                // START condition: SCL=1, SDA 1→0
                if (clk_cnt < HALF_DIV) begin
                    sda_o   <= 1'b0;  // Pull SDA low while SCL high
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o   <= 1'b0;  // Then pull SCL low
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    clk_cnt <= 16'd0;
                    state   <= S_ADDR;
                    bit_idx <= 4'd0;
                end
            end
            S_ADDR: begin
                // Send 8 bits (7-bit addr + R/W)
                if (clk_cnt < HALF_DIV) begin
                    scl_o   <= 1'b0;
                    sda_o   <= shift_reg[7];
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o   <= 1'b1;
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    clk_cnt   <= 16'd0;
                    shift_reg <= {shift_reg[6:0], 1'b0};
                    if (bit_idx == 4'd7) begin
                        state <= S_ACK1;
                    end else begin
                        bit_idx <= bit_idx + 4'd1;
                    end
                end
            end
            S_ACK1: begin
                // Read ACK from slave
                if (clk_cnt < HALF_DIV) begin
                    scl_o <= 1'b0;
                    sda_o <= 1'b1;  // Release SDA
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o   <= 1'b1;
                    ack_ok  <= ~sda_i;  // ACK if SDA=0
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    clk_cnt   <= 16'd0;
                    shift_reg <= wr_data;
                    bit_idx   <= 4'd0;
                    state     <= S_DATA;
                end
            end
            S_DATA: begin
                // Send 8 data bits
                if (clk_cnt < HALF_DIV) begin
                    scl_o   <= 1'b0;
                    sda_o   <= shift_reg[7];
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o   <= 1'b1;
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    clk_cnt   <= 16'd0;
                    shift_reg <= {shift_reg[6:0], 1'b0};
                    if (bit_idx == 4'd7) begin
                        state <= S_ACK2;
                    end else begin
                        bit_idx <= bit_idx + 4'd1;
                    end
                end
            end
            S_ACK2: begin
                // Read ACK for data
                if (clk_cnt < HALF_DIV) begin
                    scl_o <= 1'b0;
                    sda_o <= 1'b1;
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o   <= 1'b1;
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    clk_cnt <= 16'd0;
                    state   <= S_STOP;
                end
            end
            S_STOP: begin
                // STOP condition: SCL=1, SDA 0→1
                if (clk_cnt < HALF_DIV) begin
                    scl_o   <= 1'b0;
                    sda_o   <= 1'b0;
                    clk_cnt <= clk_cnt + 16'd1;
                end else if (clk_cnt < DIV) begin
                    scl_o <= 1'b1;
                    sda_o <= 1'b0;  // SDA still low
                    clk_cnt <= clk_cnt + 16'd1;
                end else begin
                    sda_o   <= 1'b1;  // SDA goes high - STOP!
                    done    <= 1'b1;
                    state   <= S_DONE;
                end
            end
            S_DONE: begin
                scl_o <= 1'b1;
                sda_o <= 1'b1;
                state <= S_IDLE;
            end
            default: state <= S_IDLE;
        endcase
    end
end

endmodule

🔬 仿真说明

仿真环境与策略

测试:start→START条件(SCL高时SDA下降沿)→7位地址0x50+写位→ACK→数据0xAB→ACK→STOP条件(SCL高时SDA上升沿)。验证时序符合I2C标准。

✅ 验证结果

Verilator 5.020 仿真通过

本实验所有Verilog代码已通过Verilator编译验证,功能行为正确。测试用例覆盖核心功能路径,确保设计满足规格要求。

⚠️ 常见错误

🌍 真实世界

工业应用

I2C连接了几乎所有的传感器、EEPROM、RTC等低速外设。一条I2C总线可挂载127个从设备。现代服务器使用I2C进行温度监控和电源管理(SMBus就是I2C的变体)。

💡 扩展挑战

🚀 自己动手

# 编译并运行
verilator --cc i2c.sv --exe i2c_tb.cpp --build -j 0
./obj_dir/Vi2c

🎯 试一试

修改参数

尝试修改代码中的关键参数,观察仿真结果变化:

  • 调整位宽,观察不同数据范围
  • 修改初始值/种子,观察不同起始条件
  • 改变时钟分频,测试不同速度

💡 Verilator会在位宽不匹配时给出Warning,这是学习的好机会

添加功能

在现有基础上增加新功能:

  • 添加新的输入信号和控制逻辑
  • 增加状态或修改状态转移条件
  • 扩展输出,增加更多显示信息

🔧 增量开发:每次只改一个地方,验证通过后再改下一个

📋 Verilog速查

语法说明示例
reg [7:0]8位寄存器reg [7:0] data;
wire组合逻辑连线wire valid = cnt > 5;
always @(posedge clk)时序逻辑上升沿触发
always @(*)组合逻辑敏感列表自动推导
localparam局部常量localparam DIV = 50000000;
case多分支选择注意default分支
$display仿真打印不可综合,仅仿真用

📊 性能指标

资源估算(FPGA参考)

本设计在典型FPGA上的资源占用估算:LUT约20-120个,FF约30-100个,无BRAM/DSP依赖(特殊模块除外)。时钟频率可达50-100MHz+。Verilator仿真速度约5-10M周期/秒。

← 上一课 📚 目录 下一课 →