[HDL设计] 片外串行总线-IIC

文章目录

引言

IIC(Inter-Integrated Circuit),也被称为 I2C 或 I²C,是由飞利浦半导体(现 NXP 半导体)于 1982 年发明的一种串行通信总线。虽然它已经诞生了 40 多年,但凭借其简单性和低成本,它依然是嵌入式系统中连接处理器与低速外设(如传感器、EEPROM、ADC/DAC、RTC 时钟等)的首选方案。

它的核心优势在于极其简洁的硬件需求:

  • SDA (Serial Data Line):串行数据线,用于传输数据。
  • SCL (Serial Clock Line):串行时钟线,用于同步数据传输的时序。

仅需这两根线(加上电源和地),就可以在同一块电路板上挂载多个设备,实现灵活的通信。

💡 冷知识: 尽管我们习惯称其为"主机/从机"(Master/Slave),但在 2021 年发布的 I2C 规范 Rev 7 中,为了术语的中性化,官方名称已更新为 "控制器(Controller)" 和 "目标(Target)"。不过为了方便理解,本文仍会涵盖这两个概念。

IIC 主机 / 控制器 (IIC Master / Controller)

在 IIC 总线上,主机是整个通信过程的"指挥官"。

  • 核心职责:
    • 生成时钟:主机负责产生 SCL 时钟信号,这是数据传输的心跳。
    • 发起与结束通信:只有主机可以发送 起始信号(START) 和 停止信号(STOP)。起始信号是 SDA 在 SCL 为高电平时由高变低;停止信号则是 SDA 在 SCL 为高电平时由低变高。
    • 寻址:通信开始后,主机发送 7 位(或 10 位)的目标设备地址,并指定读写方向(R/W 位)。
  • 工作模式: 主机并不总是"发送者"。根据需求,它可以处于 主机发送模式 (Master Transmit) 或 主机接收模式 (Master Receive)。例如,读取传感器数据时,主机在发送完地址后,就会切换为接收模式,等待从机发回数据。
  • 多主机仲裁 (Arbitration): IIC 支持多主机模式。如果两个主机同时尝试发起通信怎么办?IIC 拥有一套确定性的仲裁机制。
    • 主机在发送数据的同时会监测 SDA 线上的电平。
    • 因为是"线与"逻辑(Wired-AND),当一个主机发送"1"(高阻态)而另一个发送"0"(拉低)时,总线会呈现低电平。
    • 发送"1"的主机检测到总线是"0",就知道自己"输了"(Lost Arbitration),它会立即停止发送并退出,而赢得仲裁的主机继续通信,数据不会损坏。

IIC 从机 / 目标 (IIC Slave / Target)

从机是总线上的"响应者",它们静默监听总线,直到被点名。

  • 核心职责:
    • 地址匹配:每个从机都有一个唯一的地址(通常是 7 位,也有 10 位扩展)。当主机发送的地址与自己匹配时,从机必须在第 9 个时钟周期拉低 SDA 线,发送 ACK(应答信号)。
    • 数据收发:根据主机的读写指令,从机处于 从机接收 (Slave Receive) 或 从机发送 (Slave Transmit) 模式。
  • 时钟拉伸 (Clock Stretching): 这是从机控制总线节奏的"杀手锏"。
    • 如果从机(例如一个处理速度较慢的单片机)来不及处理接收到的字节,或者没准备好发送下一个字节,它可以将 SCL 线强制拉低。
    • 由于 SCL 是开漏结构,主机发现 SCL 被拉低后,必须暂停传输,进入等待状态(Wait State),直到从机释放 SCL,使时钟线恢复高电平。
    • 这是一种从机向主机发出的"流控"信号,告诉主机:"慢点,等我一下"。

使用 FPGA 实现 IIC 控制器的 N 种方法

在 Xilinx FPGA 开发中,实现 IIC 通常有几种方式:

  1. 使用 AXI IIC IP 核: 适合 MicroBlaze 或 Zynq PL 端扩展。
    • 优点:官方 IP,驱动完善(Vitis/SDK 中直接有现成驱动)。支持多主模式(Multi-Master)和动态地址,功能最全。
    • 缺点:资源消耗:会消耗几百个 LUT 和寄存器,比手写 RTL 臃肿。交互繁琐:通过 AXI4-Lite 总线配置寄存器,软件开销大,不适合极低延迟场景。
  2. 使用 Zynq/MPSoC PS 端的 MIO IIC:
    • 优点:零逻辑资源:完全不占用 PL 端(FPGA 逻辑)资源。稳定性好:硬核电路,不受时序收敛影响。
    • 缺点:引脚受限:只能映射到固定的 MIO 引脚(通常是 Bank 500/501),无法随意分配管脚。
  3. 使用 Zynq/MPSoC PS 端的 EMIO IIC:
    • 优点:灵活性极高:解决了 MIO 引脚锁定的问题,可以映射到 FPGA 的任意管脚。继承硬核优势:软件驱动与 MIO 模式完全通用。
    • 缺点:会占用少量的 PL 布线资源。
  4. MicroBlaze 或者 PS 端 模拟 IIC时序:
    • 优点:应急之选:当硬核被占光,或者不想消耗 AXI IP 资源时使用。任意管脚:只要是个 GPIO 就能跑。
    • 缺点:效率极低:极其消耗 CPU 算力,CPU 需要不断轮询延时。时序风险:由于操作系统调度或中断影响,波形容易不均匀,难以达到高速(400k 以上很难稳定)。
  5. 纯 RTL 手写实现(本文重点):灵活性最高。
    • Slave 模式刚需:Zynq PS 端的 I2C 虽然支持 Slave 模式,但在实际工程中,FPGA 充当外部 MCU 的 I2C 从机(用于参数配置、数据交互)时,纯 RTL 状态机是响应最快、最可控的方案。
    • 超低延迟:不需要 CPU 介入,直接逻辑处理,纳秒级响应。
    • 特殊协议适配:有些芯片的 I2C 是"变种"(比如 16 位地址、非标时序),IP 核和硬核无法调整,只有 RTL 能随心所欲修改。
    • 独立性:不依赖 ARM 或 MicroBlaze,系统挂了它还能跑,适合做看门狗或电源管理接口。

Verilog实现 IIC 主从机

IIC 主机

该iic主机模块具备以下简单功能:

  • 主机模式(Master Mode): 模块作为I2C总线的主机,负责产生时钟(SCL)和启动/停止信号。
  • 可配置时钟频率: 支持通过参数 sysclk_num 和 iic_clk_num 灵活配置系统时钟和IIC通信速率(默认为100MHz系统时钟,400kHz IIC速率)。
  • 7位设备寻址: 支持标准的7位从机地址通信。
  • 可变宽度的寄存器地址: 通过 i_addr_num 信号,支持访问 8位(1字节)或 16位(2字节)宽度的从机寄存器地址。
  • 连续读写功能:
    • 写操作: 支持向指定寄存器写入单个或多个字节的数据(通过 i_wrdata_num 控制)。
    • 读操作: 支持从指定寄存器读取多个字节的数据(通过 i_rddata_num 控制)。
  • 基本握手信号: 提供了 o_wrdata_done(写完一个字节)、o_rddata_done(读到一个数据)和 o_done(整个事务结束)标志,便于上层模块进行时序控制。

【设计文件】iic_master.v

c 复制代码
module iic_master (
     input                     i_clk           // 系统时钟
    ,input                     i_rst           // 系统复位 (高电平有效)
    ,input                     i_wr            // 触发写操作脉冲
    ,input                     i_rd            // 触发读操作脉冲
    ,input       [5:0]         i_wrdata_num    // 写数据字节数 (Base 1)
    ,input       [5:0]         i_rddata_num    // 读数据字节数 (Base 1)
    ,input       [1:0]         i_addr_num      // 寄存器地址字节数 (1或2)
    ,input       [6:0]         i_device_addr   // 器件物理地址
    ,input       [15:0]        i_reg_addr      // 寄存器地址
    ,input       [7:0]         i_wrdata        // 待写入的数据
    
    ,output reg                o_wrdata_done   // 写数据完成标志 (每写完一个字节拉高一次)
    ,output reg                o_rddata_valid  // 读数据有效标志
    ,output reg  [7:0]         o_rddata        // 读到的数据
    ,output reg                o_done          // IIC 整个事务结束标志
    ,output reg                o_ack_error     // ACK错误标志
    
    ,output reg                o_scl           // IIC SCL 时钟
    ,inout                     o_sda           // IIC SDA 数据线
);

//***************************************************************************
// Parameters
//***************************************************************************
    parameter   SYS_CLK_FREQ = 100_000_000;
    parameter   IIC_CLK_FREQ = 400_000;
    
    localparam  SCL_CNT_MAX  = SYS_CLK_FREQ / IIC_CLK_FREQ;
    
    localparam  IDLE            = 4'd0;
    localparam  START           = 4'd1;
    localparam  SEND_DEV_ADDR   = 4'd2;
    localparam  SEND_REG_ADDR_H = 4'd3;
    localparam  SEND_REG_ADDR_L = 4'd4;
    localparam  WRITE_DATA      = 4'd5;
    localparam  START_READ      = 4'd6;
    localparam  SEND_DEV_ADDR_R = 4'd7;
    localparam  READ_DATA       = 4'd8;
    localparam  SEND_ACK        = 4'd9;
    localparam  STOP            = 4'd10;

//***************************************************************************
// Internal Signals
//***************************************************************************
    reg [15:0]  scl_cnt;
    reg         iic_clk_en;
    
    reg [3:0]   state;
    reg [3:0]   bit_cnt;        
    reg [5:0]   byte_cnt;
    
    reg [7:0]   tx_shift_reg;
    reg [7:0]   rx_shift_reg;
    
    reg         sda_out_reg;
    reg         sda_out_en;
    reg         ack_bit;

    // 锁存寄存器
    reg         r_wr_en;
    reg [1:0]   r_addr_num;
    reg [5:0]   r_wrdata_num;
    reg [5:0]   r_rddata_num;
    reg [6:0]   r_device_addr;
    reg [15:0]  r_reg_addr;

    wire scl_low_mid  = (scl_cnt == (SCL_CNT_MAX/4));
    wire scl_high_mid = (scl_cnt == (SCL_CNT_MAX/4)*3);
    wire scl_fall     = (scl_cnt == SCL_CNT_MAX - 1);

    assign o_sda = sda_out_en ? sda_out_reg : 1'bz;
    wire   sda_in = o_sda; 

//***************************************************************************
// Code
//***************************************************************************
    always @(posedge i_clk) begin
        if (i_rst) begin
            scl_cnt <= 16'd0;
            o_scl   <= 1'b1;
        end else begin
            if (state != IDLE && state != START) begin
                if (iic_clk_en) begin
                    if (scl_cnt == SCL_CNT_MAX - 1) begin
                        o_scl   <= 1'b0; 
                    end else begin
                        if (scl_cnt < (SCL_CNT_MAX/2)) o_scl <= 1'b0;
                        else o_scl <= 1'b1;
                    end
                end else begin
                    o_scl   <= 1'b1;
                end
            end else begin
                o_scl   <= 1'b1;
            end

            if (iic_clk_en) begin
                if (scl_cnt == SCL_CNT_MAX - 1) begin
                    scl_cnt <= 16'd0;
                end else begin
                    scl_cnt <= scl_cnt + 1'b1;
                end
            end else begin
                scl_cnt <= 16'd0;
            end
        end
    end

    // FSM
    always @(posedge i_clk) begin
        if (i_rst) begin
            state           <= IDLE;
            iic_clk_en      <= 1'b0;
            bit_cnt         <= 4'd0;
            byte_cnt        <= 6'd0;
            sda_out_reg     <= 1'b1;
            sda_out_en      <= 1'b0;
            o_done          <= 1'b0;
            o_wrdata_done   <= 1'b0;
            o_rddata_valid  <= 1'b0;
            o_rddata        <= 8'd0;
            o_ack_error     <= 1'b0;
            tx_shift_reg    <= 8'd0;
            ack_bit         <= 1'b1;
            
            r_wr_en         <= 1'b0;
            r_addr_num      <= 2'd0;
            r_wrdata_num    <= 6'd0;
            r_rddata_num    <= 6'd0;
            r_device_addr   <= 7'd0;
            r_reg_addr      <= 16'd0;

        end else begin
            o_wrdata_done   <= 1'b0;
            o_rddata_valid  <= 1'b0;
            o_done          <= 1'b0;

            case (state)
                IDLE: begin
                    iic_clk_en  <= 1'b0;
                    sda_out_en  <= 1'b0;
                    sda_out_reg <= 1'b1;
                    bit_cnt     <= 4'd0;
                    byte_cnt    <= 6'd0;
                    o_ack_error <= 1'b0;
                    
                    if (i_wr || i_rd) begin
                        state           <= START;
                        iic_clk_en      <= 1'b1;
                        
                        r_wr_en         <= i_wr;
                        r_addr_num      <= i_addr_num;
                        r_wrdata_num    <= i_wrdata_num;
                        r_rddata_num    <= i_rddata_num;
                        r_device_addr   <= i_device_addr;
                        r_reg_addr      <= i_reg_addr;
                    end
                end

                START: begin
                    if (scl_high_mid) begin 
                        sda_out_en  <= 1'b1;
                        sda_out_reg <= 1'b0; 
                    end

                    if (scl_fall) begin
                        state        <= SEND_DEV_ADDR;
                        tx_shift_reg <= {r_device_addr, 1'b0}; 
                        bit_cnt      <= 0;
                    end
                end

                SEND_DEV_ADDR: begin
                    if (scl_low_mid) begin
                        if (bit_cnt < 8) begin
                            sda_out_en  <= 1'b1;
                            sda_out_reg <= tx_shift_reg[7-bit_cnt];
                        end else begin
                            sda_out_en  <= 1'b0; 
                        end
                    end
                    
                    if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;

                    if (scl_fall) begin
                        if (bit_cnt == 8) begin
                            if (ack_bit == 1'b1) o_ack_error <= 1'b1;
                            bit_cnt <= 0;
                            if (r_addr_num == 2) begin
                                state <= SEND_REG_ADDR_H;
                                tx_shift_reg <= r_reg_addr[15:8];
                            end else begin
                                state <= SEND_REG_ADDR_L;
                                tx_shift_reg <= r_reg_addr[7:0];
                            end
                        end else begin
                            bit_cnt <= bit_cnt + 1'b1;
                        end
                    end
                end

                SEND_REG_ADDR_H: begin
                    if (scl_low_mid) begin
                        if (bit_cnt < 8) begin
                            sda_out_en <= 1'b1;
                            sda_out_reg <= tx_shift_reg[7-bit_cnt];
                        end else 
                            sda_out_en <= 1'b0; 
                    end
                    
                    if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;

                    if (scl_fall) begin
                        if (bit_cnt == 8) begin
                            if (ack_bit == 1'b1) o_ack_error <= 1'b1;
                            bit_cnt <= 0;
                            state   <= SEND_REG_ADDR_L;
                            tx_shift_reg <= r_reg_addr[7:0];
                        end else 
                            bit_cnt <= bit_cnt + 1'b1;
                    end
                end

                SEND_REG_ADDR_L: begin
                    if (scl_low_mid) begin
                        if (bit_cnt < 8) begin
                            sda_out_en <= 1'b1;
                            sda_out_reg <= tx_shift_reg[7-bit_cnt];
                        end else 
                            sda_out_en <= 1'b0; 
                    end

                    if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;

                    if (scl_fall) begin
                        if (bit_cnt == 8) begin
                            if (ack_bit == 1'b1) o_ack_error <= 1'b1;
                            bit_cnt <= 0;
                            
                            if (r_wr_en) begin
                                state    <= WRITE_DATA;
                                byte_cnt <= 0;
                                tx_shift_reg <= i_wrdata; 
                            end else begin
                                state <= START_READ; 
                            end
                        end else 
                            bit_cnt <= bit_cnt + 1'b1;
                    end
                end

                WRITE_DATA: begin
                    if (scl_low_mid) begin
                        if (bit_cnt < 8) begin
                            sda_out_en  <= 1'b1;
                            sda_out_reg <= tx_shift_reg[7-bit_cnt];
                        end else 
                            sda_out_en  <= 1'b0; 
                    end

                    if (scl_low_mid && bit_cnt == 8) begin
                        o_wrdata_done <= 1'b1; 
                    end

                    if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;

                    if (scl_fall) begin
                        if (bit_cnt == 8) begin
                            if (ack_bit == 1'b1) o_ack_error <= 1'b1;
                            bit_cnt <= 0;
                            
                            if (byte_cnt == r_wrdata_num - 1) begin
                                state <= STOP;
                            end else begin
                                byte_cnt <= byte_cnt + 1'b1;
                                state    <= WRITE_DATA;
                                tx_shift_reg <= i_wrdata; 
                            end
                        end else 
                            bit_cnt <= bit_cnt + 1'b1;
                    end
                end

                START_READ: begin
                    if (scl_low_mid) begin
                        sda_out_en  <= 1'b1;
                        sda_out_reg <= 1'b1; 
                    end else if (scl_high_mid) begin
                        sda_out_reg <= 1'b0; 
                    end

                    if (scl_fall) begin
                        state        <= SEND_DEV_ADDR_R;
                        tx_shift_reg <= {r_device_addr, 1'b1};
                        bit_cnt      <= 0;
                    end
                end

                SEND_DEV_ADDR_R: begin
                    if (scl_low_mid) begin
                        if (bit_cnt < 8) begin
                            sda_out_en <= 1'b1;
                            sda_out_reg <= tx_shift_reg[7-bit_cnt];
                        end else 
                            sda_out_en <= 1'b0; 
                    end
                    
                    if (scl_high_mid && bit_cnt == 8) ack_bit <= sda_in;

                    if (scl_fall) begin
                        if (bit_cnt == 8) begin
                            if (ack_bit == 1'b1) o_ack_error <= 1'b1;
                            bit_cnt <= 0;
                            state   <= READ_DATA;
                        end else 
                            bit_cnt <= bit_cnt + 1'b1;
                    end
                end

                READ_DATA: begin
                    sda_out_en <= 1'b0; 
                    
                    if (scl_high_mid && bit_cnt < 8) begin
                        rx_shift_reg[7-bit_cnt] <= sda_in;
                    end

                    if (scl_fall) begin
                        if (bit_cnt == 7) begin
                            state           <= SEND_ACK; 
                            bit_cnt         <= 0; 
                            o_rddata        <= rx_shift_reg;
                            o_rddata_valid  <= 1'b1; 
                        end else begin
                            bit_cnt <= bit_cnt + 1'b1;
                        end
                    end
                end

                SEND_ACK: begin
                    if (scl_low_mid) begin
                        sda_out_en <= 1'b1;
                        if (byte_cnt == r_rddata_num - 1) 
                            sda_out_reg <= 1'b1; 
                        else 
                            sda_out_reg <= 1'b0; 
                    end
                    
                    if (scl_fall) begin
                        if (byte_cnt == r_rddata_num - 1) begin
                            state <= STOP;
                        end else begin
                            byte_cnt <= byte_cnt + 1'b1;
                            state    <= READ_DATA;
                        end
                    end
                end

                STOP: begin
                    if (scl_low_mid) begin
                        sda_out_en  <= 1'b1;
                        sda_out_reg <= 1'b0; 
                    end else if (scl_high_mid) begin
                        sda_out_reg <= 1'b1; 
                        o_done      <= 1'b1;
                    end

                    if (scl_fall) begin
                        state      <= IDLE;
                        iic_clk_en <= 1'b0;
                    end
                end
                
                default: state <= IDLE;
            endcase
        end
    end

endmodule

【仿真文件】tb_iic_master.v

c 复制代码
`timescale 1ns / 1ps
`define clk_period 10  // 100MHz = 10ns周期

module tb_iic_master();

    // 1. 信号定义
    reg             i_clk           ;
    reg             i_rst           ;
    reg             i_wr            ;
    reg             i_rd            ;
    reg  [5:0]      i_wrdata_num    ;
    reg  [5:0]      i_rddata_num    ;
    reg  [1:0]      i_addr_num      ;
    reg  [6:0]      i_device_addr   ;
    reg  [15:0]     i_reg_addr      ;
    reg  [7:0]      i_wrdata        ;

    wire            o_wrdata_done   ; // 注意:这是用来请求下一个写数据的
    wire            o_rddata_valid  ; // 注意:这是读数据有效标志
    wire [7:0]      o_rddata        ;
    wire            o_done          ;
    wire            o_scl           ;
    wire            o_sda           ;

    // 2. 例化优化后的 IIC Master
    iic_master #(
        .SYS_CLK_FREQ(100_000_000),
        .IIC_CLK_FREQ(400_000)
    ) u_iic_master (
        .i_clk          (i_clk          ),
        .i_rst          (i_rst          ),
        .i_wr           (i_wr           ),
        .i_rd           (i_rd           ),
        .i_wrdata_num   (i_wrdata_num   ),
        .i_rddata_num   (i_rddata_num   ),
        .i_addr_num     (i_addr_num     ),
        .i_device_addr  (i_device_addr  ),
        .i_reg_addr     (i_reg_addr     ),
        .i_wrdata       (i_wrdata       ),
        
        .o_wrdata_done  (o_wrdata_done  ),
        .o_rddata_valid (o_rddata_valid ),
        .o_rddata       (o_rddata       ),
        .o_done         (o_done         ),
        
        .o_scl          (o_scl          ),
        .o_sda          (o_sda          )
    );

    // I2C 上拉电阻模拟
    pullup(o_sda);
    pullup(o_scl);

    // 3. 简单的 I2C Slave 仿真模型 (替代 M24LC64)
    // 这是一个简化的行为级模型,仅用于验证主机波形是否正确
    // 它会自动应答 ACK,并在读操作时返回固定的计数数据
    M24LC04B U_M24LC04B(
        .A0                                 (1                         ),
        .A1                                 (0                         ),
        .A2                                 (0                         ),
        .WP                                 (0                         ),
        .SDA                                (o_sda                     ),
        .SCL                                (o_scl                     ),
        .RESET                              (i_rst                     ) 
    );

    // 4. 时钟生成
    initial i_clk = 0;
    always #(`clk_period/2) i_clk = ~i_clk;

    // 5. 自动打印读到的数据
    always @(posedge i_clk) begin
        if (o_rddata_valid) begin
            $display("Time %t: READ DATA VALID: 8'h%h", $time, o_rddata);
        end
    end

    // 6. 测试激励
    initial begin
        // 初始化信号
        i_rst = 1; // 保持复位 (高电平有效)
        i_wr = 0;
        i_rd = 0;
        i_addr_num = 0;
        i_wrdata_num = 0;
        i_rddata_num = 0;
        // i_device_addr = 7'b1010000; // 标准EEPROM地址
        i_device_addr = 7'b1010001; // 标准EEPROM地址
        i_reg_addr = 0;
        i_wrdata = 0;

        #(`clk_period * 10);
        i_rst = 0; // 释放复位
        #(`clk_period * 10);

        // --- Case 1: 写 1 个字节 ---
        // 地址: 16'h0102, 数据: 8'hAA
        $display("\n--- TEST CASE 1: WRITE 1 BYTE ---");
        i_addr_num = 2;         // 16位地址
        i_wrdata_num = 1;       // 写1个数据
        // i_device_addr = 7'b1010000; // 标准EEPROM地址
        i_device_addr = 7'b1010001; // 标准EEPROM地址
        
        // 调用写任务
        wr_single_byte(16'h0102, 8'hAA);
        
        #(`clk_period * 6000); // 等待一段时间

        // --- Case 2: 读 1 个字节 ---
        $display("\n--- TEST CASE 2: READ 1 BYTE ---");
        i_addr_num = 2;
        i_rddata_num = 1;
        rd_task(16'h0102);

        #(`clk_period * 500);

        $stop;
    end 

    // ---------------------------------------------
    // 任务定义
    // ---------------------------------------------
    
    // 单字节写任务
    task wr_single_byte(
        input [15:0] addr,
        input [7:0]  data
    );
        begin
            i_wr = 1;
            i_reg_addr = addr;
            i_wrdata = data;
            #(`clk_period);
            i_wr = 0;
            @(posedge o_done); // 等待结束
            #(`clk_period);
        end
    endtask

    // 读任务 (通用)
    task rd_task(
         input [15:0] addr
    );
        begin
            i_rd = 1;
            i_reg_addr = addr;
            #(`clk_period);
            i_rd = 0;
            @(posedge o_done);
            #(`clk_period);
        end
    endtask

endmodule

【仿真时序】

【仿真文件】M24LC04B.v

c 复制代码
`timescale 1ns/10ps
module M24LC04B (A0, A1, A2, WP, SDA, SCL, RESET);

   input       A0;            // unconnected pin
   input       A1;            // unconnected pin
   input       A2;            // unconnected pin

   input    WP;            // write protect pin

   inout    SDA;       // serial data I/O
   input    SCL;       // serial data clock

   input    RESET;     // system reset

// *******************************************************************************************************
// **   DECLARATIONS                                                                   **
// *******************************************************************************************************

   reg               SDA_DO;                     // serial data - output
   reg            SDA_OE;                     // serial data - output enable

   wire              SDA_DriveEnable;        // serial data output enable
   reg               SDA_DriveEnableDlyd;    // serial data output enable - delayed

   wire [02:00]   ChipAddress;            // hardwired chip address

   reg   [03:00]     BitCounter;             // serial bit counter

   reg               START_Rcvd;             // START bit received flag
   reg               STOP_Rcvd;              // STOP bit received flag
   reg               CTRL_Rcvd;                 // control byte received flag
   reg               ADDR_Rcvd;              // byte address received flag
   reg               MACK_Rcvd;                 // master acknowledge received flag

   reg               WrCycle;                 // memory write cycle
   reg               RdCycle;                 // memory read cycle

   reg   [07:00]     ShiftRegister;          // input data shift register

   reg  [07:00]      ControlByte;               // control byte register
   wire              BlockSelect;               // memory block select
   wire              RdWrBit;                 // read/write control bit

   reg   [08:00]     StartAddress;              // memory access starting address
   reg   [03:00]     PageAddress;               // memory page address

   reg   [07:00]     WrDataByte [0:15];      // memory write data buffer
   wire  [07:00]     RdDataByte;               // memory read data

   reg   [15:00]     WrCounter;                // write buffer counter

   reg   [03:00]     WrPointer;                // write buffer pointer
   reg   [08:00]     RdPointer;                // read address pointer

   reg               WriteActive;               // memory write cycle active

   reg   [07:00]     MemoryBlock0 [0:255];      // EEPROM data memory array
   reg   [07:00]     MemoryBlock1 [0:255];      // EEPROM data memory array

   integer         LoopIndex;             // iterative loop index

   integer         tAA;                      // timing parameter
   integer         tWC;                      // timing parameter

// *******************************************************************************************************
// **   INITIALIZATION                                                                    **
// *******************************************************************************************************

   initial tAA = 900;                                   // SCL to SDA output delay
//   initial tWC = 5000000;                               // memory write cycle time
   initial tWC = 50000;                                 // memory write cycle time

   initial begin
      SDA_DO = 0;
      SDA_OE = 0;
   end

   initial begin
      START_Rcvd = 0;
      STOP_Rcvd  = 0;
      CTRL_Rcvd  = 0;
      ADDR_Rcvd  = 0;
      MACK_Rcvd  = 0;
   end

   initial begin
      BitCounter  = 0;
      ControlByte = 0;
   end

   initial begin
      WrCycle = 0;
      RdCycle = 0;

      WriteActive = 0;
   end
   
   assign ChipAddress = {A2,A1,A0};

// *******************************************************************************************************
// **   CORE LOGIC                                                                  **
// *******************************************************************************************************
// -------------------------------------------------------------------------------------------------------
//      1.01:  START Bit Detection
// -------------------------------------------------------------------------------------------------------

   always @(negedge SDA) begin
      if (SCL == 1) begin
         START_Rcvd <= 1;
         STOP_Rcvd  <= 0;
         CTRL_Rcvd  <= 0;
         ADDR_Rcvd  <= 0;
         MACK_Rcvd  <= 0;

         WrCycle <= #1 0;
         RdCycle <= #1 0;

         BitCounter <= 0;
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.02:  STOP Bit Detection
// -------------------------------------------------------------------------------------------------------

   always @(posedge SDA) begin
      if (SCL == 1) begin
         START_Rcvd <= 0;
         STOP_Rcvd  <= 1;
         CTRL_Rcvd  <= 0;
         ADDR_Rcvd  <= 0;
         MACK_Rcvd  <= 0;

         WrCycle <= #1 0;
         RdCycle <= #1 0;

         BitCounter <= 10;
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.03:  Input Shift Register
// -------------------------------------------------------------------------------------------------------

   always @(posedge SCL) begin
      ShiftRegister[00] <= SDA;
      ShiftRegister[01] <= ShiftRegister[00];
      ShiftRegister[02] <= ShiftRegister[01];
      ShiftRegister[03] <= ShiftRegister[02];
      ShiftRegister[04] <= ShiftRegister[03];
      ShiftRegister[05] <= ShiftRegister[04];
      ShiftRegister[06] <= ShiftRegister[05];
      ShiftRegister[07] <= ShiftRegister[06];
   end

// -------------------------------------------------------------------------------------------------------
//      1.04:  Input Bit Counter
// -------------------------------------------------------------------------------------------------------

   always @(posedge SCL) begin
      if (BitCounter < 10) BitCounter <= BitCounter + 1;
   end

// -------------------------------------------------------------------------------------------------------
//      1.05:  Control Byte Register
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (START_Rcvd & (BitCounter == 8)) begin
         if (!WriteActive & (ShiftRegister[07:01] == {4'b1010,ChipAddress[02:00]})) begin
            if (ShiftRegister[00] == 0) WrCycle <= 1;
            if (ShiftRegister[00] == 1) RdCycle <= 1;

            ControlByte <= ShiftRegister[07:00];

            CTRL_Rcvd <= 1;
         end

         START_Rcvd <= 0;
      end
   end

   assign BlockSelect = ControlByte[01];
   assign RdWrBit     = ControlByte[00];

// -------------------------------------------------------------------------------------------------------
//      1.06:  Byte Address Register
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (CTRL_Rcvd & (BitCounter == 8)) begin
         if (RdWrBit == 0) begin
            StartAddress <= {BlockSelect,ShiftRegister[07:00]};
            RdPointer    <= {BlockSelect,ShiftRegister[07:00]};

            ADDR_Rcvd <= 1;
         end

         WrCounter <= 0;
         WrPointer <= 0;

         CTRL_Rcvd <= 0;
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.07:  Write Data Buffer
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (ADDR_Rcvd & (BitCounter == 8)) begin
         if ((WP == 0) & (RdWrBit == 0)) begin
            WrDataByte[WrPointer] <= ShiftRegister[07:00];

            WrCounter <= WrCounter + 1;
            WrPointer <= WrPointer + 1;
         end
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.08:  Acknowledge Generator
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (!WriteActive) begin
         if (BitCounter == 8) begin
            if (WrCycle | (START_Rcvd & (ShiftRegister[07:01] == {4'b1010,ChipAddress[02:00]}))) begin
               SDA_DO <= 0;
               SDA_OE <= 1;
            end 
         end
         if (BitCounter == 9) begin
            BitCounter <= 0;

            if (!RdCycle) begin
               SDA_DO <= 0;
               SDA_OE <= 0;
            end
         end
      end
   end 

// -------------------------------------------------------------------------------------------------------
//      1.09:  Acknowledge Detect
// -------------------------------------------------------------------------------------------------------

   always @(posedge SCL) begin
      if (RdCycle & (BitCounter == 8)) begin
         if ((SDA == 0) & (SDA_OE == 0)) MACK_Rcvd <= 1;
      end
   end

   always @(negedge SCL) MACK_Rcvd <= 0;

// -------------------------------------------------------------------------------------------------------
//      1.10:  Write Cycle Timer
// -------------------------------------------------------------------------------------------------------

   always @(posedge STOP_Rcvd) begin
      if (WrCycle & (WP == 0) & (WrCounter > 0)) begin
         WriteActive = 1;
         #(tWC);
         WriteActive = 0;
      end
   end

   always @(posedge STOP_Rcvd) begin
      #(1.0);
      STOP_Rcvd = 0;
   end

// -------------------------------------------------------------------------------------------------------
//      1.11:  Write Cycle Processor
// -------------------------------------------------------------------------------------------------------

   always @(negedge WriteActive) begin
      for (LoopIndex = 0; LoopIndex < WrCounter; LoopIndex = LoopIndex + 1) begin
         if (StartAddress[08] == 0) begin
            PageAddress = StartAddress[03:00] + LoopIndex;

            MemoryBlock0[{StartAddress[07:04],PageAddress[03:00]}] = WrDataByte[LoopIndex[03:00]];
         end
         if (StartAddress[08] == 1) begin
            PageAddress = StartAddress[03:00] + LoopIndex;

            MemoryBlock1[{StartAddress[07:04],PageAddress[03:00]}] = WrDataByte[LoopIndex[03:00]];
         end
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.12:  Read Data Multiplexor
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (BitCounter == 8) begin
         if (WrCycle & ADDR_Rcvd) begin
            RdPointer <= StartAddress + WrPointer + 1;
         end
         if (RdCycle) begin
            RdPointer <= RdPointer + 1;
         end
      end
   end

   assign RdDataByte = RdPointer[08] ? MemoryBlock1[RdPointer[07:00]] : MemoryBlock0[RdPointer[07:00]];

// -------------------------------------------------------------------------------------------------------
//      1.13:  Read Data Processor
// -------------------------------------------------------------------------------------------------------

   always @(negedge SCL) begin
      if (RdCycle) begin
         if (BitCounter == 8) begin
            SDA_DO <= 0;
            SDA_OE <= 0;
         end
         else if (BitCounter == 9) begin
            SDA_DO <= RdDataByte[07];

            if (MACK_Rcvd) SDA_OE <= 1;
         end
         else begin
            SDA_DO <= RdDataByte[7-BitCounter];
         end
      end
   end

// -------------------------------------------------------------------------------------------------------
//      1.14:  SDA Data I/O Buffer
// -------------------------------------------------------------------------------------------------------

   bufif1 (SDA, 1'b0, SDA_DriveEnableDlyd);

   assign SDA_DriveEnable = !SDA_DO & SDA_OE;
   always @(SDA_DriveEnable) SDA_DriveEnableDlyd <= #(tAA) SDA_DriveEnable;

// *******************************************************************************************************
// **   DEBUG LOGIC                                                                       **
// *******************************************************************************************************
// -------------------------------------------------------------------------------------------------------
//      2.01:  Memory Data Bytes
// -------------------------------------------------------------------------------------------------------

   wire [07:00]   MemoryByte0_00 = MemoryBlock0[00];
   wire [07:00]   MemoryByte0_01 = MemoryBlock0[01];
   wire [07:00]   MemoryByte0_02 = MemoryBlock0[02];
   wire [07:00]   MemoryByte0_03 = MemoryBlock0[03];
   wire [07:00]   MemoryByte0_04 = MemoryBlock0[04];
   wire [07:00]   MemoryByte0_05 = MemoryBlock0[05];
   wire [07:00]   MemoryByte0_06 = MemoryBlock0[06];
   wire [07:00]   MemoryByte0_07 = MemoryBlock0[07];

   wire [07:00]   MemoryByte0_08 = MemoryBlock0[08];
   wire [07:00]   MemoryByte0_09 = MemoryBlock0[09];
   wire [07:00]   MemoryByte0_0A = MemoryBlock0[10];
   wire [07:00]   MemoryByte0_0B = MemoryBlock0[11];
   wire [07:00]   MemoryByte0_0C = MemoryBlock0[12];
   wire [07:00]   MemoryByte0_0D = MemoryBlock0[13];
   wire [07:00]   MemoryByte0_0E = MemoryBlock0[14];
   wire [07:00]   MemoryByte0_0F = MemoryBlock0[15];

   wire [07:00]   MemoryByte1_00 = MemoryBlock1[00];
   wire [07:00]   MemoryByte1_01 = MemoryBlock1[01];
   wire [07:00]   MemoryByte1_02 = MemoryBlock1[02];
   wire [07:00]   MemoryByte1_03 = MemoryBlock1[03];
   wire [07:00]   MemoryByte1_04 = MemoryBlock1[04];
   wire [07:00]   MemoryByte1_05 = MemoryBlock1[05];
   wire [07:00]   MemoryByte1_06 = MemoryBlock1[06];
   wire [07:00]   MemoryByte1_07 = MemoryBlock1[07];

   wire [07:00]   MemoryByte1_08 = MemoryBlock1[08];
   wire [07:00]   MemoryByte1_09 = MemoryBlock1[09];
   wire [07:00]   MemoryByte1_0A = MemoryBlock1[10];
   wire [07:00]   MemoryByte1_0B = MemoryBlock1[11];
   wire [07:00]   MemoryByte1_0C = MemoryBlock1[12];
   wire [07:00]   MemoryByte1_0D = MemoryBlock1[13];
   wire [07:00]   MemoryByte1_0E = MemoryBlock1[14];
   wire [07:00]   MemoryByte1_0F = MemoryBlock1[15];

// -------------------------------------------------------------------------------------------------------
//      2.02:  Write Data Buffer
// -------------------------------------------------------------------------------------------------------

   wire [07:00]   WriteData_0 = WrDataByte[00];
   wire [07:00]   WriteData_1 = WrDataByte[01];
   wire [07:00]   WriteData_2 = WrDataByte[02];
   wire [07:00]   WriteData_3 = WrDataByte[03];
   wire [07:00]   WriteData_4 = WrDataByte[04];
   wire [07:00]   WriteData_5 = WrDataByte[05];
   wire [07:00]   WriteData_6 = WrDataByte[06];
   wire [07:00]   WriteData_7 = WrDataByte[07];
   wire [07:00]   WriteData_8 = WrDataByte[08];
   wire [07:00]   WriteData_9 = WrDataByte[09];
   wire [07:00]   WriteData_A = WrDataByte[10];
   wire [07:00]   WriteData_B = WrDataByte[11];
   wire [07:00]   WriteData_C = WrDataByte[12];
   wire [07:00]   WriteData_D = WrDataByte[13];
   wire [07:00]   WriteData_E = WrDataByte[14];
   wire [07:00]   WriteData_F = WrDataByte[15];

// *******************************************************************************************************
// **   TIMING CHECKS                                                                  **
// *******************************************************************************************************

   wire TimingCheckEnable = (RESET == 0) & (SDA_OE == 0);

   specify
      specparam
         tHI = 600,                                     // SCL pulse width - high
         tLO = 1300,                                    // SCL pulse width - low
            tSU_STA = 600,                                 // SCL to SDA setup time
         tHD_STA = 600,                                 // SCL to SDA hold time
         tSU_DAT = 100,                                 // SDA to SCL setup time
         tSU_STO = 600,                                 // SCL to SDA setup time
         tBUF = 1300;                                   // Bus free time
         
      $width (posedge SCL, tHI);
      $width (negedge SCL, tLO);

      $width (posedge SDA &&& SCL, tBUF);

      $setup (posedge SCL, negedge SDA &&& TimingCheckEnable, tSU_STA);
      $setup (SDA, posedge SCL &&& TimingCheckEnable, tSU_DAT);
      $setup (posedge SCL, posedge SDA &&& TimingCheckEnable, tSU_STO);

      $hold  (negedge SDA &&& TimingCheckEnable, negedge SCL, tHD_STA);
   endspecify

endmodule

IIC 从机

该iic从机模块具备以下简单功能:

  • 7-bit 从机地址寻址:支持标准的 7 位地址模式,不支持 10-bit 地址。
  • 支持多种速率:由于采用高频系统时钟(如 50MHz+)采样,该模块轻松支持 Standard Mode (100 kbps) 和 Fast Mode (400 kbps)。
  • Repeated Start (重复起始条件):代码中的 start_detect 逻辑具有高优先级,允许主机在不发送 STOP 的情况下直接发送新的 START。这是实现I2C 随机读(Random Read)的必要条件。
  • 单字节/多字节写 (Write / Burst Write):主机发送寄存器地址 -> 写入数据 1 -> 写入数据 2 ...
  • 随机读 (Random Read) ------ 最常用的读模式
    • 功能:主机读取指定寄存器地址的数据。
    • 流程:
      1. 主机写操作(发送寄存器地址)。
      2. 主机发送 Repeated Start。
      3. 主机发送读命令(设备地址 + Read位)。
      4. 从机返回该地址的数据。
    • 特性:完美处理了写操作切换到读操作中间的状态跳转。
  • 连续读 (Sequential Read / Burst Read)
    • 功能:主机从当前地址开始,连续读取多个字节。
    • 特性:
      • 地址自增:当从机检测到主机回复 ACK(表示还要数据)时,内部地址自动加 1。
      • 数据预取(Prefetch):这是代码中最复杂的优化点。在主机还在发送 ACK 的时候,模块就已经开始从 RAM 读取下一个地址的数据了,解决了 RAM 的读延迟(Latency)问题,保证连续读取时数据不发生错位。
  • Xilinx BRAM 原生适配:
    • 接口信号(addr, di, do, en, we)直接对应 Xilinx Block RAM IP 核的端口定义。
    • 专门针对 BRAM 的 Read Latency 进行了时序补偿,确保读出的数据是正确的。
  • 忙状态指示 (o_slave_busy):
    • 提供给后级逻辑判断当前 I2C 总线是否正在进行数据传输。

【设计文件】iic_slave.v

c 复制代码
module iic_slave #(
     parameter                           SLAVE_ADDR_DEFAULT          = 7'b1010000
    ,parameter                           DEBOUNCE_CNT                = 4                    // 滤波深度
)(
     input                               i_clk                      // 系统时钟
    ,input                               i_rst

    // I2C 物理接口
    ,input                               i_scl
    ,inout                               i_sda
    ,input                [   6: 0]      i2c_slave_add

    // 用户后端接口 (适配 Xilinx RAM)
    ,input                [   7: 0]      i_ram_do
    ,output reg           [   7: 0]      o_ram_di
    ,output reg           [   7: 0]      o_ram_addr
    ,output reg                          o_ram_rw                   // 0: Write, 1: Read
    ,output reg                          o_ram_en
    ,output                              o_slave_busy
);

    //===========================================================================
    // 1. 信号同步与滤波
    //===========================================================================
    reg [DEBOUNCE_CNT-1:0] scl_pipe;
    reg [DEBOUNCE_CNT-1:0] sda_pipe;
    reg scl_stable, sda_stable;
    reg scl_stable_d, sda_stable_d;

    always @(posedge i_clk) begin
        if (i_rst) begin
            scl_pipe <= {DEBOUNCE_CNT{1'b1}};
            sda_pipe <= {DEBOUNCE_CNT{1'b1}};
            scl_stable <= 1'b1;
            sda_stable <= 1'b1;
        end else begin
            scl_pipe <= {scl_pipe[DEBOUNCE_CNT-2:0], i_scl};
            sda_pipe <= {sda_pipe[DEBOUNCE_CNT-2:0], i_sda};

            if (&scl_pipe) scl_stable <= 1'b1;
            else if (~|scl_pipe) scl_stable <= 1'b0;

            if (&sda_pipe) sda_stable <= 1'b1;
            else if (~|sda_pipe) sda_stable <= 1'b0;
        end
    end

    // 边沿检测
    always @(posedge i_clk) begin
        scl_stable_d <= scl_stable;
        sda_stable_d <= sda_stable;
    end

    wire scl_rise = (scl_stable && !scl_stable_d);
    wire scl_fall = (!scl_stable && scl_stable_d);

    // Start/Stop 检测
    wire start_detect = (scl_stable && !sda_stable && sda_stable_d);
    wire stop_detect  = (scl_stable && sda_stable && !sda_stable_d);

    //===========================================================================
    // 2. 状态机定义
    //===========================================================================
    localparam S_IDLE       = 0;
    localparam S_ADDR       = 1;
    localparam S_ACK_ADDR   = 2;
    localparam S_REG_ADDR   = 3;
    localparam S_ACK_REG    = 4;
    localparam S_RX_DATA    = 5;
    localparam S_ACK_RX     = 6;
    localparam S_TX_DATA    = 7;
    localparam S_ACK_TX     = 8;

    reg [3:0] state;
    reg [2:0] bit_cnt;
    reg [7:0] shift_reg;
    reg       flag_read;
    reg       bit_sampled;   // 采样标志位

    wire addr_match = (shift_reg[7:1] == i2c_slave_add);
    wire rw_bit     = shift_reg[0];

    reg sda_out_en;
    reg sda_out_val;

    assign i_sda = (sda_out_en) ? sda_out_val : 1'bz;
    assign o_slave_busy = (state != S_IDLE);

    //===========================================================================
    // 3. 状态机逻辑 (Single Process)
    //===========================================================================
    // 将所有对 shift_reg 的赋值都包含在这个 always 块中,解决 multi-driven 问题
    always @(posedge i_clk) begin
        if (i_rst) begin
            state       <= S_IDLE;
            bit_cnt     <= 3'd7;
            shift_reg   <= 8'd0;
            sda_out_en  <= 1'b0;
            sda_out_val <= 1'b0;
            o_ram_addr  <= 8'd0;
            o_ram_di    <= 8'd0;
            o_ram_rw    <= 1'b1;
            o_ram_en    <= 1'b0;
            bit_sampled <= 1'b0;
        end else begin
            // 默认恢复读状态
            o_ram_rw <= 1'b1;

            if (start_detect) begin
                state       <= S_ADDR;
                bit_cnt     <= 3'd7;
                sda_out_en  <= 1'b0;
                bit_sampled <= 1'b0;
            end else if (stop_detect) begin
                state       <= S_IDLE;
                sda_out_en  <= 1'b0;
                o_ram_en    <= 1'b0;
            end else begin

                case (state)
                    S_IDLE: begin
                        sda_out_en <= 1'b0;
                        o_ram_en   <= 1'b0;
                    end

                    //-----------------------------------------------------
                    // S_ADDR: 接收设备地址
                    //-----------------------------------------------------
                    S_ADDR: begin
                        if (scl_rise) begin
                            shift_reg   <= {shift_reg[6:0], sda_stable};
                            bit_sampled <= 1'b1;
                        end else if (scl_fall) begin
                            if (bit_sampled) begin
                                if (bit_cnt == 0) begin
                                    state   <= S_ACK_ADDR;
                                    bit_cnt <= 3'd7;
                                end else begin
                                    bit_cnt <= bit_cnt - 1;
                                    bit_sampled <= 1'b0;
                                end
                            end
                        end
                    end

                    //-----------------------------------------------------
                    // S_ACK_ADDR: 地址 ACK
                    //-----------------------------------------------------
                    S_ACK_ADDR: begin
                        if (addr_match) begin
                            sda_out_en  <= 1'b1;
                            sda_out_val <= 1'b0;
                            flag_read   <= rw_bit;
                            if (rw_bit) o_ram_en <= 1'b1; // 读预取
                        end else begin
                            sda_out_en  <= 1'b0;
                        end

                        if (scl_fall) begin
                            if (addr_match) begin
                                if (rw_bit) begin
                                    state <= S_TX_DATA;
                                    // 读操作:加载第一个字节数据
                                    shift_reg <= i_ram_do;
                                    bit_sampled <= 1'b0;
                                end else begin
                                    state <= S_REG_ADDR;
                                    bit_sampled <= 1'b0;
                                end
                            end else begin
                                state <= S_IDLE;
                            end
                            sda_out_en <= 1'b0;
                        end
                    end

                    //-----------------------------------------------------
                    // S_REG_ADDR: 接收寄存器地址
                    //-----------------------------------------------------
                    S_REG_ADDR: begin
                        sda_out_en <= 1'b0;
                        if (scl_rise) begin
                            shift_reg <= {shift_reg[6:0], sda_stable};
                            bit_sampled <= 1'b1;
                        end else if (scl_fall) begin
                            if (bit_sampled) begin
                                if (bit_cnt == 0) begin
                                    state   <= S_ACK_REG;
                                    bit_cnt <= 3'd7;
                                end else begin
                                    bit_cnt <= bit_cnt - 1;
                                    bit_sampled <= 1'b0;
                                end
                            end
                        end
                    end

                    //-----------------------------------------------------
                    // S_ACK_REG: 寄存器地址 ACK
                    //-----------------------------------------------------
                    S_ACK_REG: begin
                        sda_out_en  <= 1'b1;
                        sda_out_val <= 1'b0;

                        if (scl_fall) begin
                            o_ram_addr  <= shift_reg;
                            state       <= S_RX_DATA;
                            sda_out_en  <= 1'b0;
                            bit_sampled <= 1'b0;
                        end
                    end

                    //-----------------------------------------------------
                    // S_RX_DATA: 接收数据
                    //-----------------------------------------------------
                    S_RX_DATA: begin
                        sda_out_en <= 1'b0;
                        if (scl_rise) begin
                            shift_reg <= {shift_reg[6:0], sda_stable};
                            bit_sampled <= 1'b1;
                        end else if (scl_fall) begin
                            if (bit_sampled) begin
                                if (bit_cnt == 0) begin
                                    state   <= S_ACK_RX;
                                    bit_cnt <= 3'd7;
                                end else begin
                                    bit_cnt <= bit_cnt - 1;
                                    bit_sampled <= 1'b0;
                                end
                            end
                        end
                    end

                    //-----------------------------------------------------
                    // S_ACK_RX: 写数据 ACK
                    //-----------------------------------------------------
                    S_ACK_RX: begin
                        sda_out_en  <= 1'b1;
                        sda_out_val <= 1'b0;

                        // 写 RAM
                        o_ram_rw <= 1'b0;
                        o_ram_en <= 1'b1;
                        o_ram_di <= shift_reg;

                        if (scl_fall) begin
                            state      <= S_RX_DATA;
                            sda_out_en <= 1'b0;
                            o_ram_addr <= o_ram_addr + 1'b1;
                            o_ram_rw   <= 1'b1;
                            bit_sampled <= 1'b0;
                        end
                    end

                    //-----------------------------------------------------
                    // S_TX_DATA: 发送数据 (包含连续读逻辑)
                    //-----------------------------------------------------
                    S_TX_DATA: begin
                        o_ram_en <= 1'b1;

                        if (bit_cnt == 3'd7 && !bit_sampled) begin
                            shift_reg   <= i_ram_do;       // 持续装载新数据
                            sda_out_val <= i_ram_do[7];    // 直接输出RAM最高位,避免旧shift_reg数据干扰
                            sda_out_en  <= 1'b1;
                        end else begin
                            // 正常移位输出
                            sda_out_val <= shift_reg[7];
                            sda_out_en  <= 1'b1;
                        end

                        if (scl_rise) begin
                            bit_sampled <= 1'b1; // 标记主机已读取
                        end else if (scl_fall) begin
                            if (bit_sampled) begin
                                if (bit_cnt == 0) begin
                                    state   <= S_ACK_TX;
                                    bit_cnt <= 3'd7;
                                    sda_out_en <= 1'b0;
                                end else begin
                                    shift_reg <= {shift_reg[6:0], 1'b0};
                                    bit_cnt   <= bit_cnt - 1;
                                    bit_sampled <= 1'b0;
                                end
                            end
                        end
                    end

                    //-----------------------------------------------------
                    // S_ACK_TX: 主机 ACK 检测
                    //-----------------------------------------------------
                    S_ACK_TX: begin
                        sda_out_en <= 1'b0;
                        o_ram_en   <= 1'b1;

                        if (scl_fall) begin
                            // SDA=0: ACK (继续), SDA=1: NACK (停止)
                            if (sda_stable == 1'b0) begin
                                state <= S_TX_DATA;
                                o_ram_addr <= o_ram_addr + 1'b1; // 地址自增
                                bit_sampled <= 1'b0;
                                // 注意:此处不给 shift_reg 赋值,而是由 S_TX_DATA
                                // 前半段逻辑负责装载新地址的数据
                            end else begin
                                state <= S_IDLE;
                            end
                        end
                    end

                    default: state <= S_IDLE;
                endcase
            end
        end
    end

endmodule

【仿真时序】

【仿真文件】tb_iic_slave.v

c 复制代码
`timescale 1ns / 1ps
`define clk_period 10  // 100MHz = 10ns周期

module tb_iic_slave();

    // 1. 信号定义
    reg             i_clk           ;
    reg             i_rst           ;
    reg             i_wr            ;
    reg             i_rd            ;
    reg  [5:0]      i_wrdata_num    ;
    reg  [5:0]      i_rddata_num    ;
    reg  [1:0]      i_addr_num      ;
    reg  [6:0]      i_device_addr   ;
    reg  [15:0]     i_reg_addr      ;
    reg  [7:0]      i_wrdata        ;

    wire            o_wrdata_done   ; // 注意:这是用来请求下一个写数据的
    wire            o_rddata_valid  ; // 注意:这是读数据有效标志
    wire [7:0]      o_rddata        ;
    wire            o_done          ;
    wire            o_scl           ;
    wire            o_sda           ;

    // 2. 例化优化后的 IIC Master
    iic_master #(
        .SYS_CLK_FREQ(100_000_000),
        .IIC_CLK_FREQ(400_000)
    ) u_iic_master (
        .i_clk          (i_clk          ),
        .i_rst          (i_rst          ),

        .i_wr           (i_wr           ),
        .i_rd           (i_rd           ),
        .i_wrdata_num   (i_wrdata_num   ),
        .i_rddata_num   (i_rddata_num   ),
        .i_addr_num     (i_addr_num     ),
        .i_device_addr  (i_device_addr  ),
        .i_reg_addr     (i_reg_addr     ),
        .i_wrdata       (i_wrdata       ),
        
        .o_wrdata_done  (o_wrdata_done  ),
        .o_rddata_valid (o_rddata_valid ),
        .o_rddata       (o_rddata       ),
        .o_done         (o_done         ),
        
        .o_scl          (o_scl          ),
        .o_sda          (o_sda          )
    );

    wire                                o_slave_busy                ;
    wire               [   7: 0]        i_ram_do                    ;
    wire               [   7: 0]        o_ram_di                    ;
    wire               [   7: 0]        o_ram_addr                  ;
    wire                                o_ram_rw                    ;
    wire                                o_ram_en                    ;

    iic_slave #(
        .SLAVE_ADDR_DEFAULT                 ( 7'b1010000                ) 
    ) u_iic_slave (
        .i_clk                              (i_clk                     ),
        .i_rst                              (i_rst                     ),// 假设高电平复位

        .i_scl                              (o_scl                     ),
        .i_sda                              (o_sda                     ),
        .i2c_slave_add                      (i_device_addr             ),// 可动态配置的从机地址

        .i_ram_do                           (i_ram_do                  ),// RAM读数据 (从RAM读出发送给主机)
        .o_ram_di                           (o_ram_di                  ),// RAM写数据 (从主机写入RAM)
        .o_ram_addr                         (o_ram_addr                ),// RAM地址
        .o_ram_rw                           (o_ram_rw                  ),// 0: Write, 1: Read
        .o_ram_en                           (o_ram_en                  ),// RAM使能脉冲
        .o_slave_busy                       (o_slave_busy              ) // 忙信号
    );

    blk_mem_gen_0 u_blk_mem_gen_0 (
        .clka                               (i_clk                     ), // input wire clka
        .ena                                (o_ram_en                  ), // input wire ena
        .wea                                (o_ram_rw                  ), // input wire [0 : 0] wea
        .addra                              (o_ram_addr                ), // input wire [7 : 0] addra
        .dina                               (o_ram_di                  ), // input wire [7 : 0] dina
        .douta                              (i_ram_do                  )  // output wire [7 : 0] douta
    );

    // I2C 上拉电阻模拟
    pullup(o_sda);
    pullup(o_scl);

    // 3. 简单的 I2C Slave 仿真模型 (替代 M24LC64)
    // 这是一个简化的行为级模型,仅用于验证主机波形是否正确
    // 它会自动应答 ACK,并在读操作时返回固定的计数数据
//    M24LC04B U_M24LC04B(
//        .A0                                 (1                         ),
//        .A1                                 (0                         ),
//        .A2                                 (0                         ),
//        .WP                                 (0                         ),
//        .SDA                                (o_sda                     ),
//        .SCL                                (o_scl                     ),
//        .RESET                              (i_rst                     ) 
//    );

    // 4. 时钟生成
    initial i_clk = 0;
    always #(`clk_period/2) i_clk = ~i_clk;

    // 5. 自动打印读到的数据
    always @(posedge i_clk) begin
        if (o_rddata_valid) begin
            $display("Time %t: READ DATA VALID: 8'h%h", $time, o_rddata);
        end
    end

    // 6. 测试激励
    initial begin
        // 初始化信号
        i_rst = 1; // 保持复位 (高电平有效)
        i_wr = 0;
        i_rd = 0;
        i_addr_num = 0;
        i_wrdata_num = 0;
        i_rddata_num = 0;
        // i_device_addr = 7'b1010000; // 标准EEPROM地址
        i_device_addr = 7'b1010001; // 标准EEPROM地址
        i_reg_addr = 0;
        i_wrdata = 0;

        #(`clk_period * 10);
        i_rst = 0; // 释放复位
        #(`clk_period * 10);

        // --- Case 1: 写 1 个字节 ---
        // 地址: 16'h0102, 数据: 8'hAA
        $display("\n--- TEST CASE 1: WRITE 1 BYTE ---");
        i_addr_num = 1;         // 16位地址
        i_wrdata_num = 1;       // 写1个数据
        // i_device_addr = 7'b1010000; // 标准EEPROM地址
        i_device_addr = 7'b1010001; // 标准EEPROM地址
        
        // 调用写任务
//        wr_single_byte(16'h0102, 8'hAA);
        wr_single_byte(8'h02, 8'hAA);
        
        #(`clk_period * 3000); // 等待一段时间

        // --- Case 2: 读 1 个字节 ---
        $display("\n--- TEST CASE 2: READ 1 BYTE ---");
        i_addr_num = 1;
        i_rddata_num = 1;
        rd_task(8'h02);

        #(`clk_period * 500);

        $stop;
    end 

    // ---------------------------------------------
    // 任务定义
    // ---------------------------------------------
    
    // 单字节写任务
    task wr_single_byte(
        input [15:0] addr,
        input [7:0]  data
    );
        begin
            i_wr = 1;
            i_reg_addr = addr;
            i_wrdata = data;
            #(`clk_period);
            i_wr = 0;
            @(posedge o_done); // 等待结束
            #(`clk_period);
        end
    endtask

    // 读任务 (通用)
    task rd_task(
         input [15:0] addr
    );
        begin
            i_rd = 1;
            i_reg_addr = addr;
            #(`clk_period);
            i_rd = 0;
            @(posedge o_done);
            #(`clk_period);
        end
    endtask

endmodule

优点 & 缺点

IIC 接口总线的优点:

  1. 硬件连接简单,节省资源
    • 仅需两根线:IIC 只需要两根总线线路:串行数据线 (SDA) 和串行时钟线 (SCL)。
    • 减少引脚和 PCB 空间:相比于像 SPI 这样需要为每个设备提供独立片选信号(Chip Select)的总线,IIC 极大地减少了微控制器 I/O 引脚的占用,同时也减少了 PCB 上的布线数量和面积,从而降低了制造成本。
    • 无需额外接口逻辑:IIC 接口已集成在芯片内部,设备可以直接"挂"在总线上,无需额外的地址译码器或逻辑电路。
  2. 灵活性与扩展性强
    • 多主机(Multi-Controller)支持:IIC 是真正的多主机总线,允许多个控制器同时存在。它拥有冲突检测和仲裁机制(Arbitration),防止多个主机同时发送数据时导致数据损坏。
    • 易于添加/移除设备:可以在不影响总线上其他设备的情况下,轻松地将新设备接入或从系统中移除。这使得系统升级或修改变得非常简单。
    • 软件寻址:每个设备都有唯一的地址,通信完全由软件定义。标准使用 7 位地址,也支持 10 位扩展地址,理论上可挂载大量设备。
  3. 完善的流控与兼容性
    • 时钟拉伸(Clock Stretching):从机可以通过拉低 SCL 线来暂停通信,迫使主机进入等待状态。这允许处理速度较慢的设备与快速主机同步,防止数据丢失。
    • 多种速度模式:IIC 提供多种速度等级以适应不同需求,包括标准模式(100 kbit/s)、快速模式(400 kbit/s)、快速模式+(1 Mbit/s)以及高速模式(3.4 Mbit/s),且通常向下兼容。
    • 抗干扰与低功耗:IIC 兼容的 IC 通常具有高噪声免疫力、宽电压工作范围以及极低的电流消耗,非常适合电池供电系统。

IIC 接口总线的缺点:

  1. 传输速度与效率限制
    • 速度相对较慢:相比于 SPI 等总线,IIC 的传输速率较低。虽然有高速模式,但大多数应用仍停留在 400 kbit/s 或更低。
    • 数据吞吐量打折:标称的比特率(如 400 kbit/s)并不等于有效数据传输率。由于协议中包含地址帧、ACK/NACK 应答位等开销,实际用户数据的传输速率会低于标称值。
    • 半双工通信:数据传输是半双工的,同一时间只能单向传输。
  2. 物理层与距离限制
    • 传输距离短:受限于总线电容(通常限制在 400 pF),IIC 仅适用于板级或短距离通信(通常在几米以内,甚至仅限 30 厘米内的 PCB 板上)。
    • 开漏输出的局限:IIC 使用开漏(Open-Drain)结构,必须接上拉电阻。这导致信号上升时间受电阻和总线电容影响,限制了速度,且在低功耗系统中,上拉电阻会消耗额外功率。
  3. 地址与管理复杂性
    • 地址冲突:7 位地址空间有限,且许多设备的地址由厂商固定或仅有少量可配置引脚,容易导致地址冲突(Address Collisions)。
    • 总线锁死风险:如果某个设备发生故障将 SDA 或 SCL 线一直拉低,会导致整个总线挂起(Hang),主机无法发送停止信号来重置总线。通常需要额外的硬件复位信号或重新上电来解决。
    • 配置管理困难:IIC 缺乏自动化的设备发现机制(即插即用能力较弱),主机通常需要预先知道总线上挂载了哪些设备及其地址。
  4. 一致性问题
    • 时钟拉伸的副作用:虽然时钟拉伸是优点,但也可能被滥用。如果某个设备过长时间拉伸时钟,会阻塞总线带宽,增加其他设备的通信延迟。
    • 部分实现不完整:市场上部分标称 "Two-wire interface" 的设备可能不支持完整的 IIC 规范(如不支持时钟拉伸或仲裁),导致兼容性问题。

7-bit addressing

10-bit addressing

IIC时序

相关推荐
ZPC82104 天前
docker 镜像备份
人工智能·算法·fpga开发·机器人
ZPC82104 天前
docker 使用GUI ROS2
人工智能·算法·fpga开发·机器人
tiantianuser4 天前
RDMA设计53:构建RoCE v2 高速数据传输系统板级测试平台2
fpga开发·rdma·高速传输·cmac·roce v2
博览鸿蒙4 天前
FPGA 和 IC,哪个前景更好?怎么选?
fpga开发
FPGA_小田老师4 天前
xilinx原语:ISERDESE2原语详解(串并转换器)
fpga开发·iserdese2·原语·串并转换
tiantianuser4 天前
RDMA设计50: 如何验证网络嗅探功能?
网络·fpga开发·rdma·高速传输·cmac·roce v2
Lzy金壳bing4 天前
基于Vivado平台对Xilinx-7K325t FPGA芯片进行程序在线更新升级
fpga开发·vivado·xilinx
unicrom_深圳市由你创科技4 天前
医疗设备专用图像处理板卡定制
图像处理·人工智能·fpga开发
tiantianuser4 天前
RDMA设计52:构建RoCE v2 高速数据传输系统板级测试平台
fpga开发·rdma·高速传输·cmac·roce v2
luoganttcc5 天前
Taalas 将人工智能模型蚀刻到晶体管上,以提升推理能力
人工智能·fpga开发