🔌 第24课:I2C接口

I2C主控制器 + 起始/停止条件 + 从设备地址 — 两线总线万物互联!

🏆 成就:I2C接线员 ✅ Verilator验证通过

🔌
I2C接线员
I2C Master + START/STOP + ACK检测 + EEPROM读写

🔌 I2C协议详解

I2C(Inter-Integrated Circuit)仅需2根线(SCL+SDA)就能连接多个设备!每个设备有唯一7位地址,开漏输出+上拉电阻。

I2C总线拓扑 Vcc │ ↑R ↑R 上拉电阻(4.7kΩ) │ │ │ ├──┼───┼──── SCL ──┬────┬──── │ │ │ │ │ ├──┼───┼──── SDA ──┼────┼──── │ │ │ │ │ ┌─┴──┴───┴──┐ ┌───┴──┐ ┌┴─────┐ │ Master │ │Slave │ │Slave │ │ (FPGA) │ │0x50 │ │0x68 │ └───────────┘ │EEPROM│ │RTC │ └──────┘ └──────┘ START: SCL=1时SDA↓ STOP: SCL=1时SDA↑ 数据: SCL=1时SDA稳定(采样) ACK: 接收方拉低SDA=应答

📐 I2C主控制器

Verilogi2c_master.v — I2C Master实现
// I2C主控制器
module i2c_master #(
    parameter CLK_FREQ = 50000000,
    parameter I2C_CLK  = 100000    // 100kHz标准模式
)(
    input  wire        clk,
    input  wire        rst,
    // 命令接口
    input  wire [7:0]  dev_addr,    // 设备地址(7位+R/W)
    input  wire [7:0]  wr_data,     // 写数据
    input  wire        start,       // 启动传输
    output reg  [7:0]  rd_data,     // 读数据
    output reg         done,        // 传输完成
    output reg         ack_error,   // NACK错误
    // I2C总线
    output reg         scl_o,       // SCL输出
    output reg         sda_o,       // SDA输出
    output reg         sda_oe,      // SDA输出使能(开漏)
    input  wire        sda_i        // SDA输入
);
    // 时钟分频: 50MHz → 100kHz → 每半周期250个时钟
    localparam HALF = CLK_FREQ / (2 * I2C_CLK);
    reg [15:0] clk_div;

    always @(posedge clk) begin
        if (rst) clk_div <= 0;
        else clk_div <= (clk_div >= HALF - 1) ? 0 : clk_div + 1;
    end

    wire clk_tick = (clk_div == HALF - 1);

    // 状态机
    localparam IDLE=0, START=1, ADDR=2, ACK0=3,
               WR_DATA=4, ACK1=5, RD_DATA=6, ACK2=7, STOP=8;
    reg [3:0] state;
    reg [2:0] bit_cnt;
    reg [7:0] shift_reg;

    always @(posedge clk) begin
        if (rst) begin
            state <= IDLE; scl_o <= 1; sda_o <= 1;
            sda_oe <= 0; done <= 0; ack_error <= 0;
            bit_cnt <= 0; shift_reg <= 0;
        end else if (clk_tick) begin
            done <= 0;
            case(state)
                IDLE: begin
                    scl_o <= 1; sda_o <= 1; sda_oe <= 0;
                    if (start) begin
                        state <= START;
                        shift_reg <= dev_addr;
                        bit_cnt <= 0;
                    end
                end
                START: begin // SCL=1时SDA↓
                    sda_o <= 0; sda_oe <= 1;
                    state <= ADDR; bit_cnt <= 0;
                end
                ADDR: begin
                    scl_o <= 0; // SCL低时设置SDA
                    sda_o <= shift_reg[7]; sda_oe <= 1;
                    shift_reg <= {shift_reg[6:0], 1'b0};
                    scl_o <= 1; // SCL高时从设备采样
                    if (bit_cnt >= 7) state <= ACK0;
                    else bit_cnt <= bit_cnt + 1;
                end
                ACK0: begin
                    scl_o <= 1; sda_oe <= 0; // 释放SDA
                    if (sda_i) ack_error <= 1; // NACK
                    state <= WR_DATA; bit_cnt <= 0;
                    shift_reg <= wr_data;
                end
                WR_DATA: begin
                    scl_o <= 0;
                    sda_o <= shift_reg[7]; sda_oe <= 1;
                    shift_reg <= {shift_reg[6:0], 1'b0};
                    scl_o <= 1;
                    if (bit_cnt >= 7) state <= ACK1;
                    else bit_cnt <= bit_cnt + 1;
                end
                ACK1: begin
                    scl_o <= 1; sda_oe <= 0;
                    state <= STOP;
                end
                STOP: begin // SCL=1时SDA↑
                    scl_o <= 0; sda_o <= 0; sda_oe <= 1;
                    scl_o <= 1; sda_o <= 1;
                    state <= IDLE; done <= 1;
                end
                default: state <= IDLE;
            endcase
        end
    end
endmodule

🧪 I2C Master测试

SystemVerilogi2c_master_tb.sv — I2C测试台
module i2c_master_tb;
    logic clk=0, rst=1;
    logic [7:0] dev_addr, wr_data;
    logic start, done, ack_error;
    logic scl_o, sda_o, sda_oe;
    logic sda_i;

    assign sda_i = sda_oe ? 1'bz : 1'b1; // 简化: 从设备总是ACK
    i2c_master #(.CLK_FREQ(1000), .I2C_CLK(100)) uut(.*);
    always #5 clk = ~clk;

    initial begin
        rst=1; #50; rst=0;
        $display("--- I2C Master测试 ---");
        dev_addr = 8'hA0; // EEPROM地址+写
        wr_data  = 8'h55;
        start = 1; #10; start = 0;
        wait(done);
        $display("  写入0x55到0xA0 %s", ack_error?"NACK":"ACK ✓");
        $display("I2C Master测试完成 ✓");
        #100; $finish;
    end
endmodule

📊 常见I2C设备地址

设备地址功能备注
24C02 EEPROM0x50256B存储页写4字节
DS3231 RTC0x68实时时钟精度±2ppm
BMP2800x76/77温湿度气压16/24位ADC
MPU60500x686轴IMU16位ADC
SSD1306 OLED0x3C128×64显示I2C命令/数据

💡 开漏输出:I2C设备输出0时拉低,输出1时释放(高阻)。FPGA实现:sda_oe=1时输出sda_o,sda_oe=0时高阻(由外部上拉到Vcc)。这是I2C总线仲裁和时钟同步的基础。

练习1:连接I2C EEPROM(24C02)读写数据

练习2:实现I2C读操作(写地址→重新START→读数据)

练习3:连接BMP280读取温度和气压

练习4:驱动SSD1306 OLED显示文字

练习5:实现多字节连续读写(页模式)

🔌
I2C接线员
完成本课练习,掌握I2C两线总线!

🔧 Verilator验证步骤

步骤1verilator --lint-only i2c_master.v

步骤2verilator --binary -j 0 i2c_master.v i2c_master_tb.sv

步骤3./obj_dir/Vi2c_master_tb

🔬 I2C高级应用

1. EEPROM读写:24C02(256B),写后需5ms等待(轮询ACK确认)

2. OLED显示:SSD1306,I2C命令0x00/数据0x40控制字节

3. 温湿度传感器:SHT30,发送测量命令→等待→读6字节

4. 实时时钟:DS3231,读取7字节(秒分时日月年)

5. IMU:MPU6050,14字节连续读(加速度+陀螺仪+温度)

💡 I2C总线调试

总线扫描:遍历0x03~0x77地址,有ACK的=设备存在

逻辑分析仪:用第31课的LA捕获I2C波形

上拉电阻:4.7kΩ是标准值,长总线用2.2kΩ,短总线用10kΩ

时钟拉伸:从设备可以拉低SCL暂停通信,Master必须检测

🔌 I2C深入:总线仲裁与时序

I2C最精妙的设计是多主仲裁——多个主设备可以同时发起通信,通过SDA线上的"线与"特性自动仲裁,无需中央控制器!

仲裁原理:每个主设备发送时同时检测SDA,如果自己发1但读到0,说明被其他主设备拉低了,立即退出

时钟同步:SCL也是"线与",慢设备可以拉低SCL延长时钟周期

10位地址:发送2字节(11110+A9A8+R/W)+(A7~A0),支持1024设备

高速模式:Hs-mode 3.4MHz,需要电流源上拉

📊 I2C时序参数

参数标准模式快速模式高速模式
时钟频率100kHz400kHz3.4MHz
tHD:STA4.0μs0.6μs160ns
tLOW4.7μs1.3μs320ns
tHIGH4.0μs0.6μs160ns
tSU:STA4.7μs0.6μs160ns
tSU:STO4.0μs0.6μs160ns
上拉电阻4.7kΩ2.2kΩ电流源

🔬 Verilator仿真

1. lintverilator --lint-only i2c_master.v

2. 编译verilator --binary -j 0 --trace i2c_master.v i2c_master_tb.sv

3. 运行./obj_dir/Vi2c_master_tb