FPGA--I2C通信协议

I2C (Inter-Integrated Circuit)

概述 I²C总线由飞利浦公司(现NXP)开发,是一种同步、半双工、多主多从的串行总线。仅用两根线即可连接多个设备,非常适合板内设备间的近距离、低速通信。

物理层与信号线

2根线: 

`SCL` (Serial Clock): 串行时钟线。

`SDA` (Serial Data): 串行数据线。

开漏输出 (Open-Drain): I2C设备连接到总线的引脚都是开漏(或开集)结构。这意味着设备只能将线拉低,不能主动拉高。因此,SCL和SDA线路上都必须接一个上拉电阻,以在总线空闲时将其拉到高电平。这个特性是I2C实现多主仲裁和时钟拉伸的基础。

工作原理

I2C通过一套严格的地址和应答机制工作。每个从设备都有一个唯一的7位(或10位)地址。主设备发起通信时,首先会广播它想通信的从设备的地址。

通信协议/传输步骤

1. 起始条件 (START Condition): SCL保持高电平期间,SDA出现一个下降沿。这标志着通信的开始,所有从设备开始监听总线。

2. 地址帧 (Address Frame): 主设备发送7位从机地址, followed by a 读/写(R/W)位 (0: Write, 1: Read)。

3. 应答位 (Acknowledge, ACK): 在第9个时钟周期,被寻址到的从设备如果存在且正常,会将SDA线拉低作为应答(ACK)。如果主设备没有收到ACK(SDA保持高电平,即NACK),则表示通信失败。

4. 数据帧 (Data Frame): 主从双方开始传输数据,每帧8位。每传输一个字节后,接收方都必须在第9个时钟周期回复一个ACK/NACK。

5. 停止条件 (STOP Condition): SCL保持高电平期间,SDA出现一个上升沿。这标志着本次通信的结束。

关键特性

时钟拉伸 (Clock Stretching): 如果从设备需要更多时间来准备数据,它可以将SCL线强制拉低,暂停通信,直到它准备好为止。

多主仲裁: 如果两个主设备同时试图启动通信,它们会通过SDA线的线与逻辑进行仲裁。谁先发送了一个高电平(释放总线)而检测到总线仍然是低电平(被对方拉低),谁就输掉了仲裁并退出。 

第三部分:Verilog 代码实现

1. `i2c_master.v`

`timescale 1ns / 1ps

// I2C Master Controller
module i2c_master #(
    parameter SYS_CLK_FREQ = 50_000_000,
    parameter I2C_CLK_FREQ = 100_000     // 100kHz standard mode
)(
    input  wire i_clk,
    input  wire i_rst_n,

    // User Interface
    input  wire       i_start,        // Start a transaction
    input  wire       i_rw,           // 0 for write, 1 for read
    input  wire [6:0] i_slave_addr,   // 7-bit slave address
    input  wire [7:0] i_reg_addr,     // Internal register address of slave
    input  wire [7:0] i_data_wr,      // Data to write
    output wire       o_busy,
    output wire       o_ack_error,
    output wire [7:0] o_data_rd,
    output wire       o_data_rd_dv,

    // I2C Bus Interface
    inout  wire sda,
    inout  wire scl
);

    // FSM States
    localparam S_IDLE        = 4'h0;
    localparam S_START       = 4'h1;
    localparam S_SEND_SADDR  = 4'h2;
    localparam S_WAIT_ACK1   = 4'h3;
    localparam S_SEND_RADDR  = 4'h4;
    localparam S_WAIT_ACK2   = 4'h5;
    localparam S_SEND_DATA   = 4'h6;
    localparam S_WAIT_ACK3   = 4'h7;
    localparam S_REP_START   = 4'h8;
    localparam S_SEND_SADDR_R= 4'h9;
    localparam S_WAIT_ACK4   = 4'hA;
    localparam S_READ_DATA   = 4'hB;
    localparam S_SEND_NACK   = 4'hC;
    localparam S_STOP        = 4'hD;
   
    reg [3:0] r_state = S_IDLE;
   
    // Clock Divider for SCL
    localparam DIVIDER = SYS_CLK_FREQ / (I2C_CLK_FREQ * 4); // Quarter period
    reg [$clog2(DIVIDER)-1:0] r_clk_cnt = 0;
    reg [1:0] r_scl_phase = 0;
    reg r_scl_out = 1;
    reg r_scl_en = 0;

    // Data path registers
    reg [7:0] r_shift_reg;
    reg [2:0] r_bit_cnt;
    reg r_sda_out = 1;
    reg r_sda_en = 0;
   
    // Status registers
    reg r_busy = 0;
    reg r_ack_error = 0;
    reg [7:0] r_data_rd = 0;
    reg r_data_rd_dv = 0;
   
    // I/O assignments for open-drain simulation
    assign scl = r_scl_en ? r_scl_out : 1'bz;
    assign sda = r_sda_en ? r_sda_out : 1'bz;
    assign o_busy = r_busy;
    assign o_ack_error = r_ack_error;
    assign o_data_rd = r_data_rd;
    assign o_data_rd_dv = r_data_rd_dv;

    // SCL Generation Logic
    always @(posedge i_clk) begin
        if (r_scl_en) begin
            if (r_clk_cnt == DIVIDER - 1) begin
                r_clk_cnt <= 0;
                r_scl_phase <= r_scl_phase + 1;
                if (r_scl_phase == 1) r_scl_out <= 0; // SCL low
                if (r_scl_phase == 3) r_scl_out <= 1; // SCL high
            end else begin
                r_clk_cnt <= r_clk_cnt + 1;
            end
        end else begin
            r_scl_out <= 1;
            r_clk_cnt <= 0;
            r_scl_phase <= 0;
        end
    end

    // Main FSM
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            r_state <= S_IDLE;
            r_busy <= 0;
            //... reset all regs
        end else begin
            r_data_rd_dv <= 0;
           
            case (r_state)
                S_IDLE: begin
                    r_busy <= 0;
                    r_ack_error <= 0;
                    r_scl_en <= 0;
                    r_sda_en <= 0;
                    if (i_start) begin
                        r_busy <= 1;
                        r_shift_reg <= {i_slave_addr, i_rw};
                        r_state <= S_START;
                    end
                end
               
                // --- Common Sequence for Write/Read ---
                S_START: begin // Generate START condition
                    if (r_scl_out) begin
                        r_sda_out <= 0;
                        r_sda_en <= 1;
                        r_scl_en <= 1;
                        r_state <= S_SEND_SADDR;
                    end
                end
               
                S_SEND_SADDR: begin // Send Slave Address + R/W
                    if (r_scl_phase == 0) begin // SCL is high, data stable
                        r_bit_cnt <= 7;
                        r_state <= S_SEND_SADDR + 100; // Temp state
                    end
                end
                S_SEND_SADDR + 100: begin // SCL goes low
                    if (r_scl_phase == 2) begin
                        r_sda_out <= r_shift_reg[r_bit_cnt];
                        r_state <= S_SEND_SADDR + 200;
                    end
                end
                S_SEND_SADDR + 200: begin // SCL goes high
                     if (r_scl_phase == 0) begin
                        if (r_bit_cnt == 0) r_state <= S_WAIT_ACK1;
                        else begin
                            r_bit_cnt <= r_bit_cnt - 1;
                            r_state <= S_SEND_SADDR + 100;
                        end
                     end
                end
               
                S_WAIT_ACK1: begin // Release SDA and check for ACK
                    if (r_scl_phase == 2) begin
                        r_sda_en <= 0; // Master releases SDA
                        r_state <= S_WAIT_ACK1 + 100;
                    end
                end
                S_WAIT_ACK1 + 100: begin
                    if (r_scl_phase == 0) begin // Sample ACK on SCL rising
                        if (sda) begin
                            r_ack_error <= 1;
                            r_state <= S_STOP;
                        end else begin // ACK received
                            r_shift_reg <= i_reg_addr;
                            r_state <= S_SEND_RADDR;
                        end
                    end
                end
               
                S_STOP: begin
                     if (r_scl_out) begin
                        r_sda_out <= 1;
                        r_state <= S_IDLE;
                    end
                end

                default: r_state <= S_IDLE;
            endcase
        end
    end

endmodule

2. `i2c_slave.v`

`timescale 1ns / 1ps

module i2c_slave #(
    parameter I2C_ADDR = 7'h2A
)(
    inout wire sda,
    inout wire scl
);

    // FSM States
    localparam [2:0] S_IDLE      = 3'b000;
    localparam [2:0] S_ADDR      = 3'b001;
    localparam [2:0] S_ACK_ADDR  = 3'b010;
    localparam [2:0] S_GET_RADDR = 3'b011;
    localparam [2:0] S_ACK_RADDR = 3'b100;
    localparam [2:0] S_GET_DATA  = 3'b101;
    localparam [2:0] S_ACK_DATA  = 3'b110;
    localparam [2:0] S_SEND_DATA = 3'b111;

    reg [2:0] r_state = S_IDLE;

    // Bus Signal Registers
    reg r_scl, r_sda;
    reg r_scl_prev, r_sda_prev;

    // Shift registers and counters
    reg [7:0] r_shift_reg;
    reg [2:0] r_bit_cnt;
    reg r_rw_bit;
    
    // Internal Memory
    reg [7:0] r_reg_addr;
    reg [7:0] r_memory [0:3];

    // Open-drain control
    reg r_sda_en = 0; // 1 to drive SDA low
    assign sda = r_sda_en ? 1'b0 : 1'bz;

    // Detect START and STOP conditions
    wire w_start_cond = (r_scl == 1'b1) && (r_sda_prev == 1'b1) && (r_sda == 1'b0);
    wire w_stop_cond  = (r_scl == 1'b1) && (r_sda_prev == 1'b0) && (r_sda == 1'b1);

    // Synchronize bus signals to internal clock (not shown, assuming ideal for simplicity)
    // In a real design, you would use a system clock to sample SCL/SDA
    always @(negedge scl or posedge scl) begin
        r_sda <= sda;
        r_scl <= scl;
        r_sda_prev <= r_sda;
        r_scl_prev <= r_scl;

        if (w_stop_cond || w_start_cond) begin
            r_state <= S_IDLE;
        end else if (r_state == S_IDLE && w_start_cond) begin
            r_state <= S_ADDR;
            r_bit_cnt <= 7;
        end else @(negedge scl) begin // All state transitions happen on SCL falling edge
            case (r_state)
                S_ADDR: begin
                    r_shift_reg[r_bit_cnt] <= r_sda;
                    if (r_bit_cnt == 0) begin
                        r_rw_bit <= r_sda; // last bit is R/W
                        r_state <= S_ACK_ADDR;
                    end else begin
                        r_bit_cnt <= r_bit_cnt - 1;
                    end
                end
                
                S_ACK_ADDR: begin
                    if (r_shift_reg[7:1] == I2C_ADDR) begin
                        r_sda_en <= 1; // ACK
                        if(r_rw_bit) r_state <= S_SEND_DATA;
                        else r_state <= S_GET_RADDR;
                    end else r_state <= S_IDLE;
                end
                
                S_GET_RADDR: begin
                    r_sda_en <= 0; // Release for ACK
                    r_shift_reg[r_bit_cnt] <= r_sda;
                    if (r_bit_cnt == 0) r_state <= S_ACK_RADDR;
                    else r_bit_cnt <= r_bit_cnt - 1;
                end
                
                S_ACK_RADDR: begin
                    r_reg_addr <= r_shift_reg;
                    r_sda_en <= 1; // ACK
                    r_state <= S_GET_DATA;
                end

                S_GET_DATA: begin
                    r_sda_en <= 0;
                    r_shift_reg[r_bit_cnt] <= r_sda;
                     if (r_bit_cnt == 0) r_state <= S_ACK_DATA;
                    else r_bit_cnt <= r_bit_cnt - 1;
                end
                
                S_ACK_DATA: begin
                    r_memory[r_reg_addr] <= r_shift_reg;
                    r_sda_en <= 1; // ACK
                    r_state <= S_IDLE; // Simplified: expect STOP
                end

                S_SEND_DATA: begin
                    r_sda_en <= 0; // Release ACK
                    //... logic to send data from r_memory[r_reg_addr]
                end

                default: r_state <= S_IDLE;
            endcase
        end
    end
endmodule

3. `i2c_top.v`

`timescale 1ns / 1ps

module i2c_top(
    input wire i_clk,
    input wire i_rst_n,
    // Testbench control signals
    input  wire i_start,
    input  wire i_rw,
    input  wire [7:0] i_reg_addr,
    input  wire [7:0] i_data_wr,
    output wire o_busy,
    output wire o_ack_error,
    output wire [7:0] o_data_rd,
    output wire o_data_rd_dv
);

    // I2C Bus wires
    wire sda;
    wire scl;

    // Simulate pull-up resistors on the bus
    pullup(sda);
    pullup(scl);
    
    // Instantiate Master
    i2c_master u_i2c_master (
        .i_clk(i_clk),
        .i_rst_n(i_rst_n),
        .i_start(i_start),
        .i_rw(i_rw),
        .i_slave_addr(7'h2A), // Hardcode slave address for this test
        .i_reg_addr(i_reg_addr),
        .i_data_wr(i_data_wr),
        .o_busy(o_busy),
        .o_ack_error(o_ack_error),
        .o_data_rd(o_data_rd),
        .o_data_rd_dv(o_data_rd_dv),
        .sda(sda),
        .scl(scl)
    );

    // Instantiate Slave
    i2c_slave #(
        .I2C_ADDR(7'h2A)
    ) u_i2c_slave (
        .sda(sda),
        .scl(scl)
    );

endmodule

4. `tb_i2c_top.v`

`timescale 1ns / 1ps

module tb_i2c_top;

    localparam CLK_PERIOD = 20; // 50MHz

    // --- Signals ---
    reg  r_clk;
    reg  r_rst_n;
    reg  r_start;
    reg  r_rw;
    reg  [7:0] r_reg_addr;
    reg  [7:0] r_data_wr;
    wire w_busy;
    wire w_ack_error;
    wire [7:0] w_data_rd;
    wire w_data_rd_dv;

    // --- DUT Instantiation ---
    i2c_top u_dut (
        .i_clk(r_clk),
        .i_rst_n(r_rst_n),
        .i_start(r_start),
        .i_rw(r_rw),
        .i_reg_addr(r_reg_addr),
        .i_data_wr(r_data_wr),
        .o_busy(w_busy),
        .o_ack_error(w_ack_error),
        .o_data_rd(w_data_rd),
        .o_data_rd_dv(w_data_rd_dv)
    );

    // --- Clock and Reset ---
    initial begin
        r_clk = 0;
        forever #(CLK_PERIOD / 2) r_clk = ~r_clk;
    end

    initial begin
        r_rst_n = 0;
        r_start = 0;
        r_rw = 0;
        r_reg_addr = 0;
        r_data_wr = 0;
        #200;
        r_rst_n = 1;
    end
    
    // --- Main Test Sequence ---
    initial begin
        // Wait for reset
        @(posedge r_rst_n);
        #1000;

        $display("-----------------------------------------");
        $display("TB INFO: Starting I2C Communication Test...");
        $display("-----------------------------------------");

        // --- Test Case 1: Write 0xC3 to slave register 0x01 ---
        $display("TB INFO: Test Case 1: Writing 0xC3 to slave reg 0x01...");
        r_rw <= 0; // Write
        r_reg_addr <= 8'h01;
        r_data_wr  <= 8'hC3;
        r_start <= 1;
        @(posedge r_clk);
        r_start <= 0;

        // Wait for transaction to complete
        @(posedge w_busy);
        @(negedge w_busy);
        
        if (w_ack_error) begin
             $display("TB FAIL: Test Case 1 Failed. ACK Error during write.");
             $finish;
        end else begin
             $display("TB INFO: Write transaction complete without ACK errors.");
        end
        #2000;

        // --- Test Case 2: Read back from slave register 0x01 ---
        $display("-----------------------------------------");
        $display("TB INFO: Test Case 2: Reading from slave reg 0x01...");
        r_rw <= 1; // Read
        r_reg_addr <= 8'h01; // We need to specify which reg to read from
        r_start <= 1;
        @(posedge r_clk);
        r_start <= 0;
        
        // Wait for read data to be valid
        @(posedge w_data_rd_dv);
        
        if (w_data_rd == 8'hC3) begin
            $display("TB PASS: Test Case 2 Passed. Read back expected data: 0x%0h", w_data_rd);
        end else begin
            $display("TB FAIL: Test Case 2 Failed. Expected 0xC3, but read 0x%0h", w_data_rd);
        end

        $display("-----------------------------------------");
        $display("TB INFO: All tests completed.");
        $finish;
    end

endmodule


原文链接:,转发请注明来源!