🔗 第13课:I2C通信

双线协议 + 起始/停止条件 + ACK

🏆 成就:I2C使者 ✅ Verilator验证

🔗
I2C使者
Verilator仿真I2C主机与设备通信

🔌 I2C只需2根线

SCL(时钟)+SDA(数据)。开漏输出+上拉电阻=线与逻辑。7位地址寻址,每字节后ACK握手。

START条件: SCL=1时SDA 1→0 STOP条件: SCL=1时SDA 0→1 数据有效性: SCL=1时SDA必须稳定 写时序: START + 地址+W + ACK + 数据 + ACK + STOP
Verilogi2c_master.v核心
localparam I2C_DIV=250; // 50MHz→100kHz
always @(posedge clk) case(state)
    0: if(i2c_start) begin // START: SCL=1,SDA 1→0
        sda_out<=0; sda_oe<=1;
        shift_reg<={addr,1'b0}; // 地址+写位
        state<=1; end
    1: begin // 发送8位地址
        if(clk_div==I2C_DIV-1) begin
            sclk<=~sclk_phase; sclk_phase<=~sclk_phase;
            if(!sclk_phase) begin sda_out<=shift_reg[7]; sda_oe<=1; end
            else begin shift_reg<=shift_reg<<1;
                if(bit_cnt==7) state<=2; end
        end else clk_div<=clk_div+1; end
    2: begin // ACK时隙
        sda_oe<=0; // 释放SDA让从机驱动
        if(sda_in==0) ack_ok<=1; // ACK!
        state<=3; end // → 发数据
    // ... 数据发送 + 最后STOP
endcase

📐 I2C写操作完整时序

I2C写0xAB到地址0x50: SCL ┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐ ┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └ SDA ─┐ A 0 1 0 0 0 0 0 A 1 0 1 0 1 0 1 1 ┌─ │ ↑ ↑ ↑ ↑ ↑ ↑ │ START──┘S ←── 7位地址 ──→ W │A ←── 8位数据 ──→ A A P──┘ │ 1 0 1 0 0 0 0 │C 0 0 1 0 1 0 1 1 C K K K S=START: SCL=1时SDA↓ P=STOP: SCL=1时SDA↑ A=ACK: 从机拉低SDA 每字节9个时钟(8数据+1ACK)

🔑 I2C核心概念

START:SCL=1时SDA从1→0(唯一的SCL=1时SDA下降情况)

STOP:SCL=1时SDA从0→1(唯一的SCL=1时SDA上升情况)

数据有效性:SCL=1时SDA必须稳定,数据在SCL上升沿被采样

ACK/NACK:每8位后第9个时钟由接收方拉低SDA表示ACK

开漏输出:设备只能拉低,上拉电阻负责拉高→"线与"逻辑

🧪 Verilator测试台

SystemVerilogi2c_master_tb.sv
module i2c_master_tb;
    logic clk,rst_n; logic [6:0] addr;
    logic [7:0] wr_data; logic i2c_start,i2c_busy,i2c_done,ack_ok;
    logic scl_out,sda_out,sda_oe,sda_in;
    i2c_master uut(.*);
    initial clk=0; always #10 clk=~clk;

    // 从机模拟: 地址0x50, 收到后ACK
    reg [7:0] slave_reg; reg [3:0] s_bit_cnt;
    always @(posedge clk) begin
        if(scl_out && !$past(scl_out) && !sda_oe) begin
            slave_reg <= {slave_reg[6:0],sda_out};
            s_bit_cnt <= s_bit_cnt + 1;
        end
    end
    assign sda_in = (s_bit_cnt==8||s_bit_cnt==17) ? 1'b0 : 1'b1;

    task i2c_write(input [6:0] a, input [7:0] d, input string desc);
        @(posedge clk); addr=a; wr_data=d; i2c_start=1;
        @(posedge clk); i2c_start=0;
        wait(i2c_done);
        $display("✓ %s: addr=0x%02H, data=0x%02H, ACK=%s",
            desc, a, d, ack_ok?"OK":"FAIL");
        #5000;
    endtask

    initial begin
        rst_n=0;#500;rst_n=1; s_bit_cnt=0;
        $display("========== I2C验证 ==========");
        i2c_write(7'h50, 8'hAB, "EEPROM写");
        i2c_write(7'h50, 8'hCD, "EEPROM写2");
        $display("==========================================");
        $finish;
    end
    initial #20_000_000 $finish;
endmodule

📐 I2C总线拓扑

I2C总线连接(一主多从): VCC │ ├─────┬─────┬─────┐ │ Rp │ Rp │ │ │ ↑ │ ↑ │ │ │ │ │ │ │ │ ├─┴───┴─┴───┴─────┤ │ │ SCL ──┬────┬────┬── (时钟线) │ │ │ SDA ──┼────┼────┼── (数据线) │ │ │ Master Slave1 Slave2 (FPGA) (0x50) (0x68) 关键:所有设备共享同一SCL/SDA线 上拉电阻Rp负责把线拉高 设备只能拉低 → "线与"逻辑 多设备同时拉低不会短路!

🎯 I2C实际应用

1. EEPROM — AT24C02/AT24C64,非易失存储

2. 温度传感器 — TMP102, LM75

3. 加速度计 — MPU6050(6轴IMU)

4. RTC时钟 — DS3231(高精度)

5. OLED显示 — SSD1306 I2C模式

🛠️ 编译运行

Bash编译运行步骤
verilator --binary -j 0 --trace i2c_master.v i2c_master_tb.sv
./obj_dir/Vi2c_master_tb

# 预期输出:
# ========== I2C验证 ==========
# ✓ EEPROM写: addr=0x50, data=0xAB, ACK=OK
# ✓ EEPROM写2: addr=0x50, data=0xCD, ACK=OK
# ==========================================

# 提示:在GTKWave中查看SCL和SDA信号
# 可以看到START条件(SCL=1时SDA下降)和STOP条件(SCL=1时SDA上升)

🧠 概念检查清单

✅ I2C只需要哪2根线?

✅ START和STOP条件分别是什么?

✅ 7位地址+读写位如何组成第一个字节?

✅ ACK/NACK握手如何工作?

✅ 为什么I2C使用开漏输出+上拉电阻?

✅ I2C和SPI最关键的区别?

✅ 多设备如何共用总线?

🔮 下一课预告

最后一课学习七段数码管——多路复用扫描、七段编码、字符映射。在数码管上显示"HELLO"!🎉

🐛 常见问题排查

Q: 从机不ACK? 检查地址是否正确(7位地址不含R/W位),上拉电阻是否连接。

Q: SDA无法拉高? 开漏输出+FPGA只能拉低!释放SDA:sda_oe=0让上拉电阻拉高。

Q: 总线卡死? 从机可能卡在发送状态。发送9个额外SCL脉冲+STOP条件恢复。

📏 I2C关键参数

参数说明
信号线2根SCL+SDA
标准速率100kHzStandard Mode
快速速率400kHzFast Mode
高速速率3.4MHzHigh Speed
地址宽度7位或10位7位=128设备
输出方式开漏只能拉低
上拉电阻4.7kΩ(标准)/1kΩ(快速)VCC到SDA/SCL
应答方式ACK/NACK接收方拉低SDA=ACK

💡 I2C为什么最省线?:只需要2根线就能连接128个设备!秘诀是开漏输出+地址寻址。每个设备有唯一地址,主机广播地址后只有匹配的设备回应。代价是速度慢——每个字节需要9个时钟(8数据+1ACK)且半双工。

🔑 I2C核心概念

START:SCL=1时SDA从1→0

STOP:SCL=1时SDA从0→1

数据有效性:SCL=1时SDA必须稳定

ACK:每8位后第9个时钟由接收方拉低SDA

🧪 Verilator测试台

SystemVerilogi2c_master_tb.sv片段
// 简单I2C从机模拟:地址0x50
reg [7:0] slave_reg; reg [3:0] s_bit_cnt;
always @(posedge sclk_out) if(!sda_oe) begin
    slave_reg<={slave_reg[6:0],sda_out};
    s_bit_cnt<=s_bit_cnt+1;
end
// 从机ACK时隙拉低SDA
assign sda_in=(s_bit_cnt==8||s_bit_cnt==17)?1'b0:1'b1;

💡 开漏输出:I2C设备只能拉低SDA/SCL,上拉电阻负责拉高。多设备同时驱动不怕短路——"线与"逻辑!

📐 I2C写操作完整时序

I2C写0xAB到地址0x50: SCL ┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐┌─┐ ┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └ SDA ─┐ A 0 1 0 0 0 0 0 A 1 0 1 0 1 0 1 1 ┌─ │ ↑ ↑ ↑ ↑ ↑ ↑ │ START──┘S ←── 7位地址 ──→ W │A ←── 8位数据 ──→ A A P──┘ │ 1 0 1 0 0 0 0 │C 0 0 1 0 1 0 1 1 C K K K S=START: SCL=1,SDA↓ P=STOP: SCL=1,SDA↑ A=ACK: 从机拉低SDA 每字节9个时钟(8数据+1ACK)