详解IIC通信协议以及FPGA实现

一、IIC简介

IIC也称为I2C(Inter-Integrated Circuit)由飞利浦公司(现在的恩智浦半导体)开发,是一种用于短距离数字通信的串行同步半双工通信接口协议;传输在标准模式下可以达到100kbit/s,在快速模式下可以达到400Kbit/s, 在快速模式增强模式下可以达到1Mbit/s,在高速模式下可以达到3.4Mbit/s。

I2C通信协议使用两根线(串行数据线SDA和串行时钟线SCL)进行通信,其中SDA用于传输数据,SCL用于传输时钟信号;支持多主设备和多从设备的通信,通过地址来识别不同的设备,并支持数据的读取和写入操作。

I2C通信协议在很多嵌入式系统和电子设备中被广泛应用,例如传感器、存储器、显示屏等设备之间的通信;由于其简单的硬件连接和灵活的设备支持,I2C通信协议在许多应用中都具有很好的适用性,通信框图如下:

二、IIC通信协议

由于挂在I2C总线上的设备可以有多个,因此每个挂在I2C总线的设备都有唯一的地址来标识。有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C 总线上。

2.1 协议层

I2C整体的时序如下图所示:

  1. 空闲状态:在I2C空闲时因为串行时钟线 SCL 和串行数据线SDA 线都接有上拉电阻而处于高电平状态。
  2. 起始信号:传输开始前,主机设备会将SDA拉低(当总线空闲时,SDA 和 SCL 都处于高电平状态),然后所有从机设备就会知道传输即将开始。如果两个 master 设备在同一时刻都希望获得总线的所有权,那么谁先将 SDA 拉低,谁就赢得了总线的控制权。
  3. 传输状态:主机可以向从机写数据或者读数据。
  4. 停止信号:当数据传输完成,主机在SCL为高电平时,拉高SDA线就表示本次传输完成。

为了保证传输过程稳定,I2C总线协议规定:每次传输数据必须为一个字节(即8bit,高位在前);在串行时钟线 SCL 为低电平状态时,SDA 允许改变传输的数据位(1 为高电平,0 为低电平)。经过 8 个时钟周期后,传输了 8bit 数据,即一个字节。第 8 个时钟周期末,主机释放 SDA 以使从机应答,在第 9 个时钟周期,从机将 SDA 拉低以应答;如果第 9 个时钟周期,SCL 为高电平时,SDA 未被检测到为低电平,视为非应答,表明此次数据传输失败。第 9 个时钟周期末,从机释放 SDA 以使主机继续传输数据,如果主机发送停止信号,此次传输结束。

2.2 器件地址

地址帧总是在一次通信的最开始出现。一个 7bit 的地址是从最高位(MSB)开始发送的,这个地址后面会紧跟 1bit 的操作符,(1 表示读操作,0 表示写操作) 。地址格式如下所示:

2.3 写时序

主机发送完器件地址加上写命令0,从机正确应答后就处于接收数据的状态;此时,主机向器件发送要写数据的起始地址,从机正确应答后,就开始向该地址写数据了,写时序如下:

单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,字节写完成。

2.4 读时序

主机发送完器件地址加上读命令1,从机正确应答后就处于发送数据的状态;此时从机向主机发送单字节数据,读时序如下:

三、FPGA实现

本次实现使用I2C协议对EEPROM进行读写操作,在EEPROM的地址0到255,写入0到255的累加数,然后再按照顺序读出来,并使用ILA连续捕获出来看数据是否正确

3.1 结构框图

系统整体流程就是,I2C_ctrl模块负责把要读写的地址和写数据给I2C_interface,I2C_interface模块负责实现I2C协议部分,然后把读出的数据给上游。

3.2 详解I2C_interface模块

3.2.1 接口部分

例化接口代码:

c 复制代码
module I2C_interface #
(
    parameter                                           SYS_CLK_FREQ    = 'd50_000_000,         //系统时钟频率
    parameter                                           I2C_CLK         = 'd100_000,            //I2C输出时钟
    parameter                                           SLAVE_ADDR      = 7'b1010000            //从机地址
)
(
    input                                               sys_clk ,
    input                                               rst_n   ,

    input                                               i2c_start   ,			//I2C开始信号
    input                                               i2c_byte_addr_ctrl  ,   //为1时表示要写入的内存地址是16位的,1表示8位
    input           [15:0]                              i2c_byte_addr   ,		//写入从机内存地址
    input                                               i2c_wr_rd_en    ,		//读写使能,0表示写,1表示读
    input           [7:0]                               i2c_wr_data ,			//写入从机的数据
    output  reg     [ 7:0]                              i2c_rd_data  ,  		//从从机读出的数据
    output  reg                                         i2c_rd_data_valid,		//读数据有效信号
    output  reg                                         i2c_done    ,  			//本次I2C操作完成信号
    output  reg                                         i2c_ack , 				//从机给的ack信号
    output  reg                                         scl ,  					//IIC协议的SCL
    inout                                               sda  ,					//IIC协议的SDA
    output  reg                                         i2c_drive_clk  			//却驱动I2C模块的驱动时钟

);

3.2.2 定义状态机

使用状态机来实现I2C协议,整个状态机跳转流程图如下:

代码如下:

c 复制代码
	localparam                                          Idle                      = 8'b0000_0001; //空闲状态
    localparam                                          Write_slave_addr_state    = 8'b0000_0010; //发送从机器件地址写状态
    localparam                                          Write_byte_addr16_state   = 8'b0000_0100; //发送16位内存地址状态
    localparam                                          Write_byte_addr8_state    = 8'b0000_1000; //发送8位内存地址状态
    localparam                                          Write_data_state          = 8'b0001_0000; //写数据(8 bit)状态
    localparam                                          Write_rd_addr_state       = 8'b0010_0000; //发送器件地址读状态
    localparam                                          Read_data_state           = 8'b0100_0000; //读数据(8 bit)状态
    localparam                                          Stop                      = 8'b1000_0000; //结束I2C操作

3.2.3 实现I2C模块的驱动时钟部分

使用实际IIC通信的四倍频时钟来驱动整个模块,至于为什么是四倍频,下面会讲到

c 复制代码
localparam                                          clk_div_cnt_max           = SYS_CLK_FREQ/I2C_CLK/4;//I2C四倍频时钟计数最大值
reg             [14:0]                              clk_cnt ;               //分频时钟需要的计数器

//生成SCL的四倍频时钟
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        i2c_drive_clk <= 1'b0;
        clk_cnt <= 'd0;
    end
    else if(clk_cnt == clk_div_cnt_max[14:1] - 1)begin
        i2c_drive_clk <= ~i2c_drive_clk;
        clk_cnt <= 'd0;
    end
    else begin
        i2c_drive_clk <= i2c_drive_clk;
        clk_cnt <= clk_cnt + 1'b1;
    end
end

3.2.4 实现控制SDA方向部分

c 复制代码
reg                                                 sda_ctrl    ;           //控制sda方向,1的时候sda为输出;0的时候sda为输入
reg                                                 sda_out ;               //sda输出信号
wire                                                sda_in  ;				//sda输入信号

//控制SDA
assign  sda             = sda_ctrl ? sda_out : 1'bz;
assign  sda_in          = sda;

3.2.5 实现I2C协议的三段式状态机

3.2.5.1 三段式状态机第一段
c 复制代码
reg             [7:0]                               cur_state   = Idle;     //状态机当前状态
reg             [7:0]                               next_state  = Idle;     //状态机下一状态

//三段式第一段,时序电路描述状态转移
always @(posedge i2c_drive_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cur_state <= Idle;
    else
        cur_state <= next_state;
end
3.2.5.2 三段式状态机第二段
c 复制代码
reg                                                 state_done   ;          //状态机跳转信号

//三段式第二段,组合逻辑描述状态转移条件
always @(*) begin
    case (cur_state)
        Idle: begin
            if(i2c_start == 1'b1)begin							//如果i2c_start 来临时,就跳入到发送从机地址状态
                next_state = Write_slave_addr_state;
            end
            else begin
                next_state = Idle;
            end
        end

        Write_slave_addr_state:begin							//如果从机地址写完成,判断写入的内存地址是16位还是8位
            if(state_done == 1'b1)begin
                if(i2c_byte_addr_ctrl == 1'b1)begin
                    next_state = Write_byte_addr16_state;
                end
                else begin
                    next_state = Write_byte_addr8_state;
                end
            end
            else begin
                next_state = Write_slave_addr_state;
            end
        end

        Write_byte_addr16_state:begin							//写完内存地址的高8位后,跳转到写低8位状态
            if(state_done == 1'b1)begin
                next_state = Write_byte_addr8_state;
            end
            else begin
                next_state = Write_byte_addr16_state;
            end
        end

        Write_byte_addr8_state:begin							//内存地址低8位写完后,判断是读还是写操作
            if(state_done == 1'b1)begin	
                if(wr_rd_flag == 1'b1)
                    next_state = Write_rd_addr_state;
                else
                    next_state = Write_data_state;
            end
            else 
                next_state = Write_byte_addr8_state;
        end

        Write_data_state:begin									//写数据状态,写完成后跳转到停止状态
            if(state_done == 1'b1)begin
                next_state = Stop;
            end
            else begin
                next_state = Write_data_state;
            end
        end

        Write_rd_addr_state:begin								//由于是读操作,因此还要产生一次虚写操作,就是再发送一次7位从机地址+1 
            if(state_done == 1'b1)begin
                next_state = Read_data_state;
            end
            else begin
                next_state = Write_rd_addr_state;
            end
        end

        Read_data_state:begin									//接收从机发送的数据
            if(state_done == 1'b1)begin
                next_state = Stop;
            end
            else begin
                next_state = Read_data_state;
            end
        end

        Stop:begin												//产生停止信号,然后跳转到空闲状态
            if(state_done == 1'b1)begin
                next_state = Idle;
            end
            else begin
                next_state = Stop;
            end
        end
        default: next_state = Idle;
    endcase
end
3.2.5.3 三段式状态机第三段

由于I2C协议规定,sda上的数据需要在scl为高电平的时候保持稳定,只能在scl为低电平的时候改变。因此我们产生一个四倍频的驱动时钟再加一个四倍频时钟的计数器,就能够实现在任何位置改变数据,波形图如下:

从波形图可以看出,在cnt=1时scl为高电平,此时拉低sda就表示了开始信号;然后在cnt=3时,拉低scl;在cnt=4的时候给sda赋值,在cnt=5的时候,拉高scl。后面依次类推。这样就利用scl四倍频的时钟产生了scl与sda,程序如下:

c 复制代码
reg                                                 wr_rd_flag ;            //读写信号,0的时候为写;1的时候为读   
reg             [6:0]                               clk4_cnt ;              //I2C四倍频时钟周期计数器
reg             [15:0]                              i2c_byte_addr_reg   ;   //字地址缓存
reg             [7:0]                               i2c_wr_data_reg   ;     //I2C写数据缓存
reg             [7:0]                               i2c_rd_data_temp   ;    //I2C读数据临时数据

//三段式第三段,时序逻辑描述状态输出
always @(posedge i2c_drive_clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        i2c_rd_data <= 'd0;
        i2c_rd_data_valid <= 1'b0;
        i2c_done <= 1'b0;
        i2c_ack <= 1'b0;
        scl <=  1'b1;
        sda_ctrl <= 1'b1;       
        sda_out <= 1'b1;           
        state_done   <= 1'b0;      
        wr_rd_flag <= 1'b0;      
        clk4_cnt <= 'd0;          
        i2c_byte_addr_reg <= 'd0;
        i2c_wr_data_reg <= 'd0; 
        i2c_rd_data_temp <= 'd0;
    end
    else begin
        clk4_cnt <= clk4_cnt + 1'b1;
        case (cur_state)
            Idle: begin
                if(i2c_start ==1'b1)begin               //开始传输时,把读写使能,地址,数据都暂存下来
                    wr_rd_flag <= i2c_wr_rd_en;
                    i2c_byte_addr_reg <= i2c_byte_addr;
                    i2c_wr_data_reg <= i2c_wr_data;
                    clk4_cnt <= 'd0;
                    i2c_rd_data_valid <= 1'b0;
                end
                else begin
                    clk4_cnt <= 'd0;
                    scl <=  1'b1;
                    sda_ctrl <= 1'b1;
                    sda_out <= 1'b1; 
                    i2c_done <= 1'b0;
                    i2c_ack <= 1'b0;
                    i2c_rd_data_valid <= 1'b0;
                end
            end

            Write_slave_addr_state:begin
                case (clk4_cnt)
                    'd1:    sda_out <= 1'b0;            //SCL为高电平时,拉低SDA为起始信号
                    'd3:    scl <= 1'b0;
                    'd4:    sda_out <= SLAVE_ADDR[6];   //低电平时传输器件地址低6位
                    'd5:    scl <= 1'b1;
                    'd7:    scl <= 1'b0;
                    'd8:    sda_out <= SLAVE_ADDR[5];   //低电平时传输器件地址低5位
                    'd9:    scl <= 1'b1;
                    'd11:   scl <= 1'b0;
                    'd12:   sda_out <= SLAVE_ADDR[4];   //低电平时传输器件地址低4位
                    'd13:   scl <= 1'b1;
                    'd15:   scl <= 1'b0;
                    'd16:   sda_out <= SLAVE_ADDR[3];   //低电平时传输器件地址低3位
                    'd17:   scl <= 1'b1;
                    'd19:   scl <= 1'b0;
                    'd20:   sda_out <= SLAVE_ADDR[2];   //低电平时传输器件地址低2位
                    'd21:   scl <= 1'b1;
                    'd23:   scl <= 1'b0;
                    'd24:   sda_out <= SLAVE_ADDR[1];   //低电平时传输器件地址低1位
                    'd25:   scl <= 1'b1;
                    'd27:   scl <= 1'b0;
                    'd28:   sda_out <= SLAVE_ADDR[0];   //低电平时传输器件地址低0位
                    'd29:   scl <= 1'b1;
                    'd31:   scl <= 1'b0;
                    'd32:   sda_out <= 1'b0;            //写命令
                    'd33:   scl <= 1'b1;
                    'd35:   scl <= 1'b0;
                    'd36:   begin
                            sda_out <= 1'b1;            
                            sda_ctrl<= 1'b0;            //释放sda总线 
                    end
                    'd37:   scl <= 1'b1;
                    'd38:   begin
                            state_done <= 1'b1;
                            if(sda_in == 1'b1)          //如果第9位sda为低电平,则表示从机应答成功,状态机跳转 
                                i2c_ack <= 1'b0;
                            else
                                i2c_ack <= 1'b1;
                    end
                    'd39:   begin
                            state_done <= 1'b0;         //拉低状态机跳转信号
                            clk4_cnt <= 'd0;            //计数器清零
                            scl <= 1'b0;
                            i2c_ack <= 1'b0;
                    end
                    default: ;
                endcase
            end

            Write_byte_addr16_state:begin
                case (clk4_cnt)
                    'd0 :begin
                        sda_ctrl<= 1'b1;                        //拿回SDA控制权,并且传开始输内存地址的高8位
                        sda_out <= i2c_byte_addr_reg[15];       
                    end       
                    'd1 :   scl <= 1'b1;
                    'd3 :   scl <= 1'b0;
                    'd4 :   sda_out <= i2c_byte_addr_reg[14];
                    'd5 :   scl <= 1'b1;
                    'd7 :   scl <= 1'b0;
                    'd8 :   sda_out <= i2c_byte_addr_reg[13];
                    'd9 :   scl <= 1'b1;
                    'd11:   scl <= 1'b0; 
                    'd12:   sda_out <= i2c_byte_addr_reg[12];  
                    'd13:   scl <= 1'b1;  
                    'd15:   scl <= 1'b0;  
                    'd16:   sda_out <= i2c_byte_addr_reg[11];
                    'd17:   scl <= 1'b1;  
                    'd19:   scl <= 1'b0;  
                    'd20:   sda_out <= i2c_byte_addr_reg[10];
                    'd21:   scl <= 1'b1;  
                    'd23:   scl <= 1'b0;  
                    'd24:   sda_out <= i2c_byte_addr_reg[9];
                    'd25:   scl <= 1'b1;  
                    'd27:   scl <= 1'b0;  
                    'd28:   sda_out <= i2c_byte_addr_reg[8];
                    'd29:   scl <= 1'b1;
                    'd31:   scl <= 1'b0; 
                    'd32:   begin
                            sda_out <= 1'b1;
                            sda_ctrl<= 1'b0;            //释放sda总线 
                    end
                    'd33:   scl <= 1'b1;
                    'd34:   begin
                            state_done <= 1'b1;
                            if(sda_in == 1'b1)
                                i2c_ack <= 1'b0;
                            else
                                i2c_ack <= 1'b1;    
                    end
                    'd35:  begin
                            state_done <= 1'b0;
                            scl <= 1'b0;
                            clk4_cnt <= 'd0;
                            i2c_ack <= 1'b0;
                    end         
                    default: ;
                endcase
            end

            Write_byte_addr8_state:begin
                case (clk4_cnt)
                    'd0 :begin
                            sda_ctrl<= 1'b1;                //拿回SDA控制权,并且传开始输内存地址的低8位
                            sda_out <= i2c_byte_addr_reg[7];
                    end
                    'd1 :   scl <= 1'b1;
                    'd3 :   scl <= 1'b0;
                    'd4 :   sda_out <= i2c_byte_addr_reg[6];
                    'd5 :   scl <= 1'b1;
                    'd7 :   scl <= 1'b0;
                    'd8 :   sda_out <= i2c_byte_addr_reg[5];
                    'd9 :   scl <= 1'b1;
                    'd11:   scl <= 1'b0; 
                    'd12:   sda_out <= i2c_byte_addr_reg[4];  
                    'd13:   scl <= 1'b1;  
                    'd15:   scl <= 1'b0;  
                    'd16:   sda_out <= i2c_byte_addr_reg[3];
                    'd17:   scl <= 1'b1;  
                    'd19:   scl <= 1'b0;  
                    'd20:   sda_out <= i2c_byte_addr_reg[2];
                    'd21:   scl <= 1'b1;  
                    'd23:   scl <= 1'b0;  
                    'd24:   sda_out <= i2c_byte_addr_reg[1];
                    'd25:   scl <= 1'b1;  
                    'd27:   scl <= 1'b0;  
                    'd28:   sda_out <= i2c_byte_addr_reg[0];
                    'd29:   scl <= 1'b1;
                    'd31:   scl <= 1'b0; 
                    'd32:   begin
                            sda_out <= 1'b1;
                            sda_ctrl<= 1'b0;                //释放sda总线 
                    end
                    'd33:   scl <= 1'b1;
                    'd34:   begin
                            state_done <= 1'b1;
                            if(sda_in == 1'b1)
                                i2c_ack <= 1'b0;
                            else
                                i2c_ack <= 1'b1;    
                    end
                    'd35:  begin
                            state_done <= 1'b0;
                            scl <= 1'b0;
                            clk4_cnt <= 'd0;
                            i2c_ack <= 1'b0;
                    end         
                    default: ;
                endcase
            end

            Write_data_state:begin
                case (clk4_cnt)
                    'd0 :begin
                            sda_ctrl<= 1'b1;                //拿回SDA控制权,并且传开始输8位写数据
                            sda_out <= i2c_wr_data_reg[7];
                    end
                    'd1 :   scl <= 1'b1;
                    'd3 :   scl <= 1'b0;
                    'd4 :   sda_out <= i2c_wr_data_reg[6];
                    'd5 :   scl <= 1'b1;
                    'd7 :   scl <= 1'b0;
                    'd8 :   sda_out <= i2c_wr_data_reg[5];
                    'd9 :   scl <= 1'b1;
                    'd11:   scl <= 1'b0; 
                    'd12:   sda_out <= i2c_wr_data_reg[4];  
                    'd13:   scl <= 1'b1;  
                    'd15:   scl <= 1'b0;  
                    'd16:   sda_out <= i2c_wr_data_reg[3];
                    'd17:   scl <= 1'b1;  
                    'd19:   scl <= 1'b0;  
                    'd20:   sda_out <= i2c_wr_data_reg[2];
                    'd21:   scl <= 1'b1;  
                    'd23:   scl <= 1'b0;  
                    'd24:   sda_out <= i2c_wr_data_reg[1];
                    'd25:   scl <= 1'b1;  
                    'd27:   scl <= 1'b0;  
                    'd28:   sda_out <= i2c_wr_data_reg[0];
                    'd29:   scl <= 1'b1;
                    'd31:   scl <= 1'b0; 
                    'd32:   begin
                            sda_out <= 1'b1;
                            sda_ctrl<= 1'b0;            //释放sda总线 
                    end
                    'd33:   scl <= 1'b1;
                    'd34:   begin
                            state_done <= 1'b1;
                            if(sda_in == 1'b1)
                                i2c_ack <= 1'b0;
                            else
                                i2c_ack <= 1'b1;    
                    end
                    'd35:  begin
                            state_done <= 1'b0;
                            scl <= 1'b0;
                            clk4_cnt <= 'd0;
                            i2c_ack <= 1'b0;
                    end         
                    default: ;
                endcase
            end

            Write_rd_addr_state:begin
                case (clk4_cnt)
                    'd0:begin    
                            sda_ctrl<= 1'b1;                //拿回SDA控制权,开始传输器件地址加上读命令
                            sda_out<= 1'b0;
                    end
                    'd1:    scl <= 1'b1;
                    'd2:    sda_out<= 1'b1;             //停止信号
                    'd4:    sda_out<= 1'b0;             //开始信号
                    'd6:    scl <= 1'b0;
                    'd7:    sda_out <= SLAVE_ADDR[6];
                    'd8:    scl <= 1'b1;
                    'd10:   scl <= 1'b0;
                    'd11:   sda_out <= SLAVE_ADDR[5];
                    'd12:   scl <= 1'b1;
                    'd14:   scl <= 1'b0;
                    'd15:   sda_out <= SLAVE_ADDR[4];
                    'd16:   scl <= 1'b1;
                    'd18:   scl <= 1'b0;
                    'd19:   sda_out <= SLAVE_ADDR[3];
                    'd20:   scl <= 1'b1;
                    'd22:   scl <= 1'b0;
                    'd23:   sda_out <= SLAVE_ADDR[2];
                    'd24:   scl <= 1'b1;
                    'd26:   scl <= 1'b0;
                    'd27:   sda_out <= SLAVE_ADDR[1];
                    'd28:   scl <= 1'b1;
                    'd30:   scl <= 1'b0;
                    'd31:   sda_out <= SLAVE_ADDR[0];
                    'd32:   scl <= 1'b1;
                    'd34:   scl <= 1'b0;
                    'd35:   sda_out <= 1'b1;                //读命令
                    'd36:   scl <= 1'b1;
                    'd38:   scl <= 1'b0;
                    'd39:   begin
                            sda_out <= 1'b1;
                            sda_ctrl<= 1'b0;                //释放sda控制权
                    end
                    'd40:   scl <= 1'b1;
                    'd41:   begin
                            state_done <= 1'b1;
                            if(sda_in == 1'b1)
                                i2c_ack <= 1'b0;
                            else
                                i2c_ack <= 1'b1;
                    end
                    'd42:   begin
                            state_done <= 1'b0;
                            clk4_cnt <= 'd0;
                            scl <= 1'b0;
                            i2c_ack <= 1'b0;
                    end     
                    default: ;
                endcase
            end

            Read_data_state:begin
                case (clk4_cnt)
                    'd1:    scl <= 1'b1;
                    'd2:    i2c_rd_data_temp[7] <= sda_in;
                    'd3:    scl <= 1'b0;
                    'd5:    scl <= 1'b1;
                    'd6:    i2c_rd_data_temp[6] <= sda_in;
                    'd7:    scl <= 1'b0;
                    'd9:    scl <= 1'b1;
                    'd10:   i2c_rd_data_temp[5] <= sda_in;
                    'd11:   scl <= 1'b0;
                    'd13:   scl <= 1'b1;
                    'd14:   i2c_rd_data_temp[4] <= sda_in;
                    'd15:   scl <= 1'b0;
                    'd17:   scl <= 1'b1;
                    'd18:   i2c_rd_data_temp[3] <= sda_in;
                    'd19:   scl <= 1'b0;
                    'd21:   scl <= 1'b1;
                    'd22:   i2c_rd_data_temp[2] <= sda_in;
                    'd23:   scl <= 1'b0;
                    'd25:   scl <= 1'b1;
                    'd26:   i2c_rd_data_temp[1] <= sda_in;
                    'd27:   scl <= 1'b0;
                    'd29:   scl <= 1'b1;
                    'd30:   i2c_rd_data_temp[0] <= sda_in;
                    'd31:   scl <= 1'b0;
                    'd32:   begin
                            sda_ctrl<= 1'b1;
                            sda_out <= 1'b1;
                    end
                    'd33:   scl <= 1'b1;
                    'd34:   state_done <= 1'b1;
                    'd35:   begin
                            state_done <= 0;
                            scl <= 1'b0;
                            clk4_cnt <= 'd0;
                            i2c_rd_data <= i2c_rd_data_temp;
                            i2c_rd_data_valid <= 1'b1;
                    end
                    default: ;
                endcase
            end

            Stop:begin
                case (clk4_cnt)
                    'd0:    begin
                        sda_ctrl<= 1'b1;
                        sda_out <= 1'b0;
                    end
                    'd1:    scl <= 1'b1;            //在scl为高电平时候,拉高sda代表停止信号
                    'd3:    sda_out <= 1'b1;
                    'd10:   state_done <= 1'b1;
                    'd11:   begin
                             clk4_cnt <= 'd0;
                             i2c_done <= 1'b1;
                             state_done <= 1'b0;
                    end
                    default: ;
                endcase
            end
            default: ;
        endcase
    end
end

endmodule

3.3 详解I2C_ctrl模块

该模块主要产生要读和要写的数据和地址,本次想要在EEPROM的0-255地址写入0-255的数字。因为本次板卡上的EEPROM手册要求每次操作完一次后,需要等待500ms,因此本模块也是有状态机完成,每次跳转等待500ms,知道读写完256个数字,代码如下:

c 复制代码
`timescale 1ns/1ns
module I2C_ctrl
(
    input                                               clk ,
    input                                               rst_n   ,
    output  reg                                         i2c_wr_rd_en   , //I2C读写控制信号
    output  reg                                         i2c_start    , //I2C触发执行信号
    output  reg     [15:0]                              i2c_byte_addr    , //I2C器件内地址
    output  reg     [ 7:0]                              i2c_wr_data  , //I2C要写的数据
    input           [ 7:0]                              i2c_rd_data  , //I2C读出的数据
    input                                               i2c_done    , //I2C一次操作完成
    input                                               i2c_ack,          //I2C应答标志
    output  reg     [3:0]                               led 
);
    localparam                                          delay_cnt_max   = 200000;

    reg             [25:0]                              delay_cnt   ;
    reg             [3:0]                               state   ;
    reg                                                 rw_done ;


always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        state <= 'd0;
        delay_cnt <= 'd0;
        i2c_start <= 1'b0;   
        i2c_byte_addr <= 'd0;   
        i2c_wr_rd_en <= 1'b0 ;    
        i2c_wr_data <='d0; 
        rw_done <= 1'b0;
        led <= 4'b1111;
    end
    else begin
        i2c_start <= 1'b0;
        rw_done <= 1'b0;
        case (state)
            0: begin
                delay_cnt <= delay_cnt + 1'b1;
                if(delay_cnt == delay_cnt_max -1)begin
                    delay_cnt <= 'd0;
                    if(i2c_byte_addr == 255)begin
                        i2c_byte_addr <= 'd0;
                        i2c_wr_rd_en <= 1'b1;
                        state <= 2;
                    end
                    else begin
                        state <= 1;
                        i2c_start <= 1'b1;   
                    end
                end
                else begin
                    state <= 'd0;
                end
            end

            1:begin
                if(i2c_done == 1'b1)begin
                    state <= 0;
                    i2c_byte_addr <= i2c_byte_addr + 1'b1;
                    i2c_wr_data <= i2c_wr_data + 1'b1;
                end
                else
                    state <= 'd1;
            end

            2: begin
                i2c_start <= 1'b1; 
                state <= 3;
            end

            3:begin
               delay_cnt <= delay_cnt + 1'b1;
                if(delay_cnt == delay_cnt_max -1)begin
                    delay_cnt <= 'd0;
                    if(i2c_byte_addr == 255)begin
                        rw_done <= 1'b1;
                        led<= 4'b0000;
                        state <= 0;
                    end
                    else begin
                        state <= 2;
                        i2c_byte_addr <= i2c_byte_addr +1'b1;
                    end
                end
                else begin
                    state <= 'd3;
                end
            end
            default: ;
        endcase
    end
end

endmodule

3.4 仿真测试

顶层接口就如同系统框图所示,只有sys_clk,rst_n,以及scl,sda。因此tb文件只需要写入一个时钟和复位即可,然后打开仿真,由于每次操作延时了500ms,可以在仿真的时候修改sys_clk的值来加快仿真,打开仿真如下所示:

  我们可以看到,每次请求iic传输时,给的地址和数据都是累加的,我们放大局部来看具体细节:

当收到i2c_start信号的时候,首先传输开始信号,然后传输设备地址,本次EEPROM地址为1010000,然后跟上写操作0,第九位释放总线,判断ack。其它的同理。

3.5 上板验证

我们添加ila然后打开capture mode抓取信号,观察读出来的数据是否连续,关于capture mode怎么使用,请看《Vivado ILA Capture Control 模式与 Advanced Trigger的功能使用以及TSM(触发状态机)的编写》这篇文章,下载bit文件后,打开ila窗口,设置捕捉信号为,rd_data_valid上升沿,观察波形如下:

我们可以看到,读出来的数据是0-255循环累加的数,验证成功。

相关推荐
cycf11 小时前
CRC校验
fpga开发
landyjzlai13 小时前
AMBA总线(15)关于AXI-stream(sg模式)
arm开发·fpga开发·amba
白狐_79813 小时前
Quartus Prime 新手完全使用指南
fpga开发
Aaron15881 天前
三种主流接收机架构(超外差、零中频、射频直采)对比及发展趋势浅析
c语言·人工智能·算法·fpga开发·架构·硬件架构·信号处理
博览鸿蒙1 天前
一颗数字系统是如何在 FPGA 上“跑起来”的?
fpga开发
雨洛lhw1 天前
FPGA JTAG接口设计全解析
fpga开发·jtag
minglie12 天前
iverilog 配合 Makefile 搭建 Verilog 仿真工程
fpga开发
芒果树技术2 天前
MangoTree案例分享:基于AtomRIO FPGA平台,客户实现自适应主动减振
测试工具·fpga开发·模块测试
雨洛lhw2 天前
按键电路设计的细节
fpga开发
minglie12 天前
vio_uart的浏览器版上位机
fpga开发