侧边栏壁纸
    • 累计撰写 302 篇文章
    • 累计收到 527 条评论
    I2C协议及verilog实现-串口读写 EEPROM
    我的学记|刘航宇的博客

    I2C协议及verilog实现-串口读写 EEPROM

    刘航宇
    2023-01-01 / 0 评论 / 920 阅读 / 正在检测是否收录...

    I2C 基本概念

    I2C 总线(I2C bus,Inter-IC bus)是一个双向的两线连续总线,提供集成电路(ICs)之间的通信线路。I2C 总线是一种串行扩展技术,最早由 Philips 公司推出,广泛应用于电视,录像机和音频设备。I2C 总线的意思是“完成集成电路或功能单元之间信息交换的规范或协议”。Philips 公司推出的 I2C 总线采用一条数据线(SDA),加一条时钟线(SCL)来完成数据的传输及外围器件的扩展。I2C 总线物理拓扑结构如图 30.1 所示。

    I2C 总线在物理连接上比较简单,分别由 SDA(串行数据线)和 SCL(串行时钟线)两条总线及上拉电阻组成。通信的原理是通过控制 SCL 和 SDA 的时序,使其满足 I2C 的总线协议从而进行数据的传输。
    I2C 总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(可以从 I2C 器件数据手册得知),主从设备之间就是通过这个地址来确定与哪个器件进行通信。本次实验我们把 FPGA 作为主设备,把挂载在总线上的其他设备(如 EEPROM、PFC8563 等 I2C 器件)作为从设备。
    I2C 总线数据传输速率在标准模式下可达 100kbit/s,快速模式下可达 400kbit/s,高速模式下可达 3.4Mbit/s。I2C 总线上的主设备与从设备之间以字节(8 位)为单位进行双向的数据传输。

    I2C 协议时序介绍

    I2C 协议整体时序图如图 30.2 所示

    I2C 协议整体时序说明如下:
    (1) 总线空闲状态:SDA 为高电平,SCL 为高电平;
    (2) I2C 协议起始位:SCL 为高电平时,SDA 出现下降沿,产生一个起始位;
    (3) I2C 协议结束位:SCL 为高电平时,SDA 出现上升沿,产生一个结束位;
    (4) I2C 读写数据状态:主要包括数据的串行输出输入和数据接收方对数据发送方的响应信号。具体时序如图 30.3 所示。

    当 I2C 主机(后面简称主机)向 I2C 从机(后面简称从机)写入数据时,SDA 上的每一位数据在 SCL 的高电平期间被写入从机中。从主机角度来看,需要在 SCL 低电平期间改变要写入的数据。而当主机读取从机中数据时,从机在 SCL 低电平期间将数据输出到 SDA 总线上,在 SCL 的高电平期间保持数据稳定,从主机角度来看,需要在 SCL 的高电平期间将 SDA 线上的数据读取并存储。
    每当一个字节的数据或命令传输完成时,数据接收方都会向发送方响应一位应答位。在响应应答位时,数据发出方将 SDA 总线设置为三态输入,由于 I2C 总线上都有上拉电阻,因此此时总线默认为高电平,若数据接收方正确接收到数据,则数据接收方将 SDA 总线拉低,以示正确应答。例如当主机向从机写入数据或命令时,每个字节都需要从机产生应答信号以告诉主机此次的数据或命令是否成功被写入。所以,当主机将一字节的数据或命令传出后,会将 SDA 信号设置为三态输入,等待从机应答(等待 SDA 被从机拉低为低电平),若从机正确应答,表明当前数据或命令传输成功,可以结束或开始下一个数据或命令的传输,否则表明数据或命令写入失败,主机就可以决定是否放弃写入或者重新发起写入。

    inout信号原理


    I2C 器件地址

    每个 I2C 器件都有一个器件地址,有的器件地址在出厂时地址就设置好了,用户不可以更改(例如 OV7670 器件地址为固定的 0x42),有的确定了几位,剩下几位由硬件确定(比如常见的 I2C 接口的 EEPROM 存储器,留有 3 个控制地址的引脚,由用户自己在硬件设计时确定)。严格讲,主机不是直接向从机发送地址,而是主机往总线上发送地址,所有的从机都能接收到主机发出的地址,然后每个从机都将主机发出的地址与自己的地址比较,如
    果匹配上了,这个从机就会向主机发出一个响应信号。主机收到响应信号后,开始向总线上发送数据,与这个从机的通讯就建立起来了。如果主机没有收到响应信号,则表示寻址失败。
    通常情况下,主从器件的角色是确定的,也就是说从机一直工作在从机模式。不同器件定义地址的方式是不同的,有的是软件定义,有的是硬件定义。例如某些单片机的I2C 接口作为从机时,其器件地址是可以通过软件修改从机地址寄存器确定的。而对于一些其他器件,如 CMOS 图像传感器、EEPROM 存储器,其器件地址在出厂时就已经设定好了,具体值可以在对应的数据手册中查到。
    对于 AT24C64 这样一颗 EEPROM 器件,其器件地址为 1010 加 3 位的片选信号。3位片选信号由硬件连接决定。例如 SOIC 封装的该芯片 PIN1、PIN2、PIN3 为片选地址。当硬件电路上分别将这三个 pin 连接到 GND 或 VCC 时,就可以设置不同的片选地址。I2C 协议在进行数据传输时,主机需要首先向总线上发出控制命令,其中,控制命令就包含了从机地址/片选信号+读写控制。然后等待从机响应。如图 30.4 所示为 I2C 控制命令传输的数据格式。

    I2C 传输时,按照从高到低的位序进行传输。控制字节的最低位为读写控制位,当该位为 0 时表示主机对从机进行写操作,当该位为 1 时表示主机对从机进行读操作。例如,当需要对片选地址为 100 的 AT24LC64 发起写操作,则控制字节应该为 1010_100_0。若进行读操作,则控制字节应该为 1010_100_1。

    I2C 存储器地址

    每个支持 I2C 协议的器件,内部总会有一些可供读写的寄存器或存储器,例如,对于我们用到的 EEPROM 存储器,内部就是顺序编址的一系列存储单元。对于我们常接触的 CMOS 摄像头如 OV7670(OV7670 的该接口叫 SCCB 接口,其实质也是一种特殊的 I2C协议,可以直接兼容 I2C 协议),其内部就是一系列编址的可供读写的寄存器。因此,我们要对一个器件中的存储单元(寄存器和存储器以下简称存储单元)进行读写,就必须要能够指定存储单元的地址。I2C 协议设计了有从机存储单元寻址地址段,该地址段为一个字或两个字节长度,在主机确认收到从机返回的控制字节响应后由主机发出。地址段长度视不同的器件类型,长度不同,例如同是 EEPROM 存储器,AT24C04 的址段长度为一个字节,而 AT24C64 的地址段长度为两个字节。具体是一个字节还是两个字节,与器件的存储单元数量有关。如图 30.5 和图 30.6 分别为 1 字节地址和 2 字节地址器件的地址分布图,其中 1 字节地址的器件是以内存为 1kbit 的 EEPROM 存储器 AT24C01 举例,2 字节地址的器件是以内存为 64kbit 的 EEPROM 存储器 AT24C64 举例的。

    I2C 单字节写时序

    根据前面讲的,不同器件,I2C 器件地址字节不同,这样对于 I2C 单字节写时序就会有所差别,图 30.7 和图 30.8 分别为 1 字节地址段器件和 2 字节地址段器件单字节写时序图。


    根据时序图,从主机角度来描述一次写入单字节数据过程如下:
    a. 主机设置 SDA 为输出;
    b. 主机发起起始信号;
    c. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
    d. 主机设置 SDA 为三态门输入,读取从机应答信号;
    e. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
    f. 主机设置 SDA 为三态门输入,读取从机应答信号;
    g. 读取应答信号成功,对于两字节地址段器件,传输地址数据低字节,对于 1 字节地址段器件,主机设置 SDA 为输出,传输待写入的数据;
    h. 设置 SDA 为三态门输入,读取从机应答信号,对于两字节地址段器件,接着步骤 i;对于 1 字节地址段器件,直接跳转到步骤 k;
    i. 读取应答信号成功,主机设置 SDA 为输出,传输待写入的数据(对于两字节地址段器件);
    j. 设置 SDA 为三态门输入,读取从机应答信号(两字节地址段器件);
    k. 读取应答信号成功,主机产生 STOP 位,终止传输。

    I2C 连续写时序(页写时序)

    注:I2C 连续写时序仅部分器件支持。
    连续写是主机连续写多个字节数据到从机,这个和单字节写操作类似,连续多字节写操作也是分为 1 字节地址段器件和 2 字节地址段器件的写操作,图 30.9 和图 30.10 分别为 1 字节地址段器件和 2 字节地址段器件连续多字节写时序图。

    根据时序图,从主机角度来描述一次写入多字节数据过程如下:
    a. 主机设置 SDA 为输出;
    b. 主机发起起始信号;
    c. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
    d. 主机设置 SDA 为三态门输入,读取从机应答信号;
    e. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
    f. 主机设置 SDA 为三态门输入,读取从机应答信号;
    g. 读取应答信号成功后,主机设置 SDA 为输出,对于两字节地址段器件,传输低字节地址数据,对于 1 字节地址段器件,传输待写入的第 1 个数据
    h. 设置 SDA 为三态门输入,读取从机应答信号,对于两字节地址段器件,接着步骤 i;对于 1 字节地址段器件,直接跳转到步骤 k;
    i. 读取应答信号成功后,主机设置 SDA 为输出,传输待写入的第 1 个数据(两字节地址段器件);
    j. 设置 SDA 为三态门输入,读取从机应答信号(两字节地址段器件);
    k. 读取应答信号成功后,主机设置 SDA 为输出,传输待写入的下一个数据;
    l. 设置 SDA 为三态门输入,读取从机应答信号;n 个数据被写完,转到步骤 m,若数据未被写完,转到步骤 k;
    m. 读取应答信号成功后,主机产生 STOP 位,终止传输。
    注:对于 AT24Cxx 系列的 EEPROM 存储器,一次可写入的最大长度为 32 字节。

    I2C 单字节读时序

    同样的,I2C 读操作时序根据不同 I2C 器件具有不同的器件地址字节数,单字节读操作分为 1 字节地址段器件单节数据读操作和 2 字节地址段器件单节数据读操作。图30.11 和图 30.12 分别为
    不同情况的时序图。

    根据时序图,从主机角度描述一次读数据过程,如下:
    a. 主机设置 SDA 为输出;
    b. 主机发起起始信号;
    c. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
    d. 主机设置 SDA 为三态门输入,读取从机应答信号;
    e. 读取应答信号成功,主机设置 SDA 输出,传输 1 字节地址数据;
    f. 主机设置 SDA 为三态门输入,读取从机应答信号;
    g. 读取应答信号成功,主机设置 SDA 输出,对于两字节地址段器件,传输低字节地址数据;对于 1 字节地址段器件,无此步骤,直接跳转到步骤 h;
    h. 主机发起起始信号;
    i. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
    j. 设置 SDA 为三态门输入,读取从机应答信号;
    k. 读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的一个字节的数据;
    l. 产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);m. 主机产生 STOP 位,终止传输。

    I2C 连续读时序(页读取)

    连续读是主机连续从从机读取多个字节数据,这个和单字节读操作类似,连续多字节读操作也是分为 1 字节地址段器件和 2 字节地址段器件的读操作,图 30.13 和图 30.14 分别为 1 字节地址段器件和 2 字节地址段器件连续多字节读时序图。

    根据时序图,从主机角度描述多字节数据读取过程如下:
    a. 主机设置 SDA 为输出
    b. 主机发起起始信号
    c. 主机传输器件地址字节,其中最低位为 0,表明为写操作。
    d. 主机设置 SDA 为三态门输入,读取从机应答信号。
    e. 读取应答信号成功,主机设置 SDA 输出,传输 1 字节地址数据
    f. 主机设置 SDA 为三态门输入,读取从机应答信号。
    g. 读取应答信号成功,主机设置 SDA 输出,对于两字节地址段器件,传输低字节地址数据;对于 1 字节地址段器件,无此步骤;直接跳转到步骤h;
    h. 主机发起起始信号;
    i. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
    j. 设置 SDA 为三态门输入,读取从机应答信号;
    k. 设置 SDA 为三态门输入,读取 SDA 总线上的第 1 个字节的数据;
    l. 主机设置 SDA 输出,发送一位应答信号;
    m. 设置 SDA 为三态门输入,读取 SDA 总线上的下一个字节的数据;若 n 个字节数据读完成,跳转到步骤 n,若数据未读完,跳转到步骤 l;(对于 AT24Cxx,一次读取长度最大为 32 字节,即 n 不大于 32)
    n. 主机设置 SDA 输出,产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
    o. 主机产生 STOP 位,终止传输。

    I2C 读写器件控制程序

    通过上述的讲述,对 I2C 读写器件数据时序有了一定的了解,下面将开始进行控制程序的设计。根据上面 I2C 的基本概念中有关读写时 SDA 与 SCL 时序,不管对于从机还是主机,SDA 上的每一位数据在 SCL 的高电平期间保持不变,而数据的改变总是在 SCL的低电平期间发生。因此,我们可以选用 2 个标志位对时钟 SCL 的高电平和低电平进行标记,如下图所示:scl_high 对 SCL 高电平期间进行标志,scl_low 对 SCL 低电平期间进行标志。这样就可以在 scl_high 有效时读 SDA 数据,在 scl_low 有效时改变数据。scl_high和 scl_low 产生的时序图如图 30.15 所示。

    在本实验中,时钟信号 SCL 采用计数器方法产生,计数器最大计数值为系统时钟频率除以 SCL 时钟频率,即:SCL_CNT_M = SYS_CLOCK/SCL_CLOCK。对于 scl_high 和 scl_low则只需要分别在计数到四分之一的最大值和四分之三的最大值时产生标志位即可,具体的时钟信号 SCL 和标志信号 scl_high、scl_low 产生实现代码如下:

    //系统时钟采用 50MHz
    parameter SYS_CLOCK = 50_000_000;
    //SCL 总线时钟采用 400kHz
    parameter SCL_CLOCK = 400_000;
    //产生时钟 SCL 计数器最大值
    localparam SCL_CNT_M = SYS_CLOCK/SCL_CLOCK;
     reg [15:0]scl_cnt; //SCL 时钟计数器
     reg scl_vaild; //I2C 非空闲时期
     reg scl_high; //SCL 时钟高电平中部标志位
     reg scl_low; //SCL 时钟低电平中部标志位
     
     //I2C 非空闲时期 scl_vaild 的产生
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     scl_vaild <= 1'b0;
     else if(Wr | Rd)
     scl_vaild <= 1'b1;
     else if(Done)
     scl_vaild <= 1'b0;
     else
     scl_vaild <= scl_vaild;
     end
     //scl 时钟计数器
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     scl_cnt <= 16'd0;
     else if(scl_vaild)begin
     if(scl_cnt == SCL_CNT_M - 1)
     scl_cnt <= 16'd0;
     else
     scl_cnt <= scl_cnt + 16'd1;
     end
     else
     scl_cnt <= 16'd0;
     end
     //scl 时钟,在计数器值到达最大值一半和 0 时翻转
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Scl <= 1'b1;
     else if(scl_cnt == SCL_CNT_M >>1)
     Scl <= 1'b0;
     else if(scl_cnt == 16'd0)
     Scl <= 1'b1;
     else
     Scl <= Scl;
     end
     //scl 时钟高低电平中部标志位
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     scl_high <= 1'b0;
     else if(scl_cnt == (SCL_CNT_M>>2))
     scl_high <= 1'b1;
     else
     scl_high <= 1'b0;
     end
     //scl 时钟低电平中部标志位
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     scl_low <= 1'b0;
     else if(scl_cnt == (SCL_CNT_M>>1)+(SCL_CNT_M>>2))
     scl_low <= 1'b1;
     else
     scl_low <= 1'b0;
     end

    上述代码中 Wr 和 Rd 信号为 I2C 进行一次写和读操作的门控使能信号,Done 信号为一次I2C写和读操作完成标志位。(SCL_CNT_M>>2)和(SCL_CNT_M>>1)+(SCL_CNT_M>>2)分别为 1/2 的 SCL_CNT_M 和 3/4 的 SCL_CNT_M 的计数值。
    在 SCL 时钟总线以及其高低电平标志位产生完成后,接下来的事情就是 SDA 数据线的产生,这个需要根据具体的读写操作时序完成。本实验主要采用状态机实现,根据上面讲述的读写数据的时序关系,设计了如图 30.16 所示的状态转移图,其状态机状态编码采用独热编码,若需要改变状态编码形式,只需改变程序中的 parameter 定义即可。

    根据上面 I2C 基本概念可知,不同的器件其寄存器地址字节数分为 1 字节或和 2 字节地址段,并且有些 I2C 器件是支持多字节的数据读写,所以在设计时考虑到该 I2C 控制器的通用性,我们将设计寄存器地址字节和读取数据个数均可自行设置的 I2C 控制器,用户可根据自己的实际应用情况设置选择与器件对应的寄存器地址字节数或是读写数据的字节数。寄存器地址字节数的可变主要是通过一个计数器对字节数进行计数,当计数值达到指定值后跳转到下一状态,具体的可参见代码。
    在状态机中,从主机角度来看,SDA 数据线上在写控制、写数据、读控制状态过程是需要串行输出数据,而在读数据状态过程是需要串行输入数据。根据数据在时钟高电平期间保持不变,改变数据在低电平时期的规则,本设计对时钟信号的高低电平进行计数,从而在指定的计数值进行输出或读取数据实现数据的串行输出和串行输入。串行输出和串行输入数据采用任务的形式进行表示,便于在主状态机中多次的调用。图 30.17为计数的过程以及特定状态变化的时序图,这里的特定状态主要是指读/写控制、读/写地址和读/写数据状态。

    图 30.17 中计数器 halfbit_cnt 和数据接收方对发送的响应检测标志位 ack 以及串行输出、输入数据任务的具体代码如下:

     //sda 串行接收与发送时 scl 高低电平计数器
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     halfbit_cnt <= 8'd0;
     else if((main_state == WR_CTRL)||
     (main_state == WR_WADDR)||
     (main_state == WR_DATA)||
     (main_state == RD_CTRL)||
     (main_state == RD_DATA))begin
     if(scl_low | scl_high)begin
     if(halfbit_cnt == 8'd17)
     halfbit_cnt <= 8'd0;
     else
     halfbit_cnt <= halfbit_cnt + 8'd1;
     end
     else
     halfbit_cnt <= halfbit_cnt;
     end
     else
     halfbit_cnt <= 8'd0;
     end
     //数据接收方对发送的响应检测标志位
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     ack <= 1'b0;
     else if((halfbit_cnt == 8'd16)&&scl_high&&(Sda==1'b0))
     ack <= 1'b1;
     else if((halfbit_cnt == 8'd17)&&scl_low)
     ack <= 1'b0;
     else
     ack <= ack;
     end
     //输出串行数据任务
     task send_8bit_data;
     if(scl_high && (halfbit_cnt == 8'd16))
     FF <= 1;
     else if(halfbit_cnt < 8'd17)begin
     sda_reg <= sda_data_out[7];
     if(scl_low)
     sda_data_out <= {sda_data_out[6:0],1'b0};
     else
     sda_data_out <= sda_data_out;
     end
     else
     ;
     endtask
     //串行数据输入任务
     task receive_8bit_data;
     if(scl_low && (halfbit_cnt == 8'd15))
     FF <= 1;
     else if((halfbit_cnt < 8'd15))begin
     if(scl_high)
     sda_data_in <= {sda_data_in[6:0],Sda};
     else begin
     sda_data_in <= sda_data_in;
     end
     end
     else
     ;
     endtask

    对于计数器 halfbit_cnt 只在写控制、写数据、读控制、读数据状态下才进行计数,其他状态为零。代码中 FF 是进行串行输出或输入任务的标志位,当 FF 为 1 时表示退出任务,FF 为 0 时表示进入任务。这样便于在状态机中对任务的调用,以及在指定的时间退出任务。
    接下来就是主状态机的设计,主状态机的状态转移图上面已经给出,具体转移过程是依据 I2C 读写时序进行的,代码如下:

    //主状态机
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)begin
     main_state <= IDLE;
     sda_reg <= 1'b1;
     W_flag <= 1'b0;
     R_flag <= 1'b0;
     Done <= 1'b0;
     waddr_cnt <= 2'd1;
     wdata_cnt <= 8'd1;
     rdata_cnt <= 8'd1;
     end
     else begin
     case(main_state)
     IDLE:begin
     sda_reg <= 1'b1;
     W_flag <= 1'b0;
     R_flag <= 1'b0;
     Done <= 1'b0;
     waddr_cnt <= 2'd1;
     wdata_cnt <= 8'd1;
     rdata_cnt <= 8'd1;
     if(Wr)begin
     main_state <= WR_START;
     W_flag <= 1'b1;
     end
     else if(Rd)begin
     main_state <= WR_START;
     R_flag <= 1'b1;
     end
     else
     main_state <= IDLE;
     end
     WR_START:begin
     if(scl_low)begin
     main_state <= WR_CTRL;
     sda_data_out <= wr_ctrl_word;
     FF <= 1'b0;
     end
     else if(scl_high)begin
     sda_reg <= 1'b0;
     main_state <= WR_START;
     end
     else
     main_state <= WR_START;
     end
     WR_CTRL:begin
     if(FF == 1'b0)
     send_8bit_data;
     else begin
     if(ack == 1'b1) begin//收到响应
     if(scl_low)begin
     main_state <= WR_WADDR;
     FF <= 1'b0;
     if(Wdaddr_num == 2'b1)
     sda_data_out <= Word_addr[7:0];
     else
     sda_data_out <= Word_addr[15:8];
     end
    else
     main_state <= WR_CTRL;
     end
     else//未收到响应
     main_state <= IDLE;
     end
     end
     WR_WADDR:begin
     if(FF == 1'b0)
     send_8bit_data;
     else begin
     if(ack == 1'b1) begin//收到响应
     if(waddr_cnt == Wdaddr_num)begin
     if(W_flag && scl_low)begin
     main_state <= WR_DATA;
     sda_data_out <= Wr_data;
     waddr_cnt <= 2'd1;
     FF <= 1'b0;
     end
    else if(R_flag && scl_low)begin
     main_state <= RD_START;
     sda_reg <= 1'b1;
     end
    else
     main_state <= WR_WADDR;
     end
    else begin
     if(scl_low)begin
     waddr_cnt <= waddr_cnt + 2'd1;
     main_state <= WR_WADDR;
     sda_data_out <= Word_addr[7:0];
     FF <= 1'b0;
     end
    else
     main_state <= WR_WADDR;
     end
     end
     else//未收到响应
     main_state <= IDLE;
     end
     end
     WR_DATA:begin
     if(FF == 1'b0)
     send_8bit_data;
     else begin
     if(ack == 1'b1) begin//收到响应
     if(wdata_cnt == Wrdata_num)begin
     if(scl_low)begin
     main_state <= STOP;
     sda_reg <= 1'b0;
     wdata_cnt <= 8'd1;
     end
    else
     main_state <= WR_DATA;
     end
    else begin
     if(scl_low)begin
     wdata_cnt <= wdata_cnt + 8'd1;
     main_state <= WR_DATA;
     sda_data_out <= Wr_data;
     FF <= 1'b0;
     end
     else
     main_state <= WR_DATA;
     end
     end
     else//未收到响应
     main_state <= IDLE;
     end
     end
     RD_START:begin
     if(scl_low)begin
     main_state <= RD_CTRL;
     sda_data_out <= rd_ctrl_word;
     FF <= 1'b0;
     end
     else if(scl_high)begin
     main_state <= RD_START;
     sda_reg <= 1'b0;
     end
     else
     main_state <= RD_START;
     end
     RD_CTRL:begin
     if(FF == 1'b0)
     send_8bit_data;
     else begin
     if(ack == 1'b1) begin//收到响应
     if(scl_low)begin
     main_state <= RD_DATA;
     FF <= 1'b0;
     end
    else
     main_state <= RD_CTRL;
     end
     else//未收到响应
     main_state <= IDLE;
     end
     end
     
     RD_DATA:begin
     if(FF == 1'b0)
     receive_8bit_data;
     else begin
     if(rdata_cnt == Rddata_num)begin
     sda_reg <= 1'b1;
     if(scl_low)begin
     main_state <= STOP;
     sda_reg <= 1'b0;
     end
    else
     main_state <= RD_DATA;
     end
     else begin
     sda_reg <= 1'b0;
     if(scl_low)begin
     rdata_cnt <= rdata_cnt + 8'd1;
     main_state <= RD_DATA;
     FF <= 1'b0;
     end
    else
     main_state <= RD_DATA;
     end
     end
     end
     STOP:begin//结束操作
     if(scl_high)begin
     sda_reg <= 1'b1;
     main_state <= IDLE;
     Done <= 1'b1;
     end
     else
     main_state <= STOP;
     end
     
     default:
     main_state <= IDLE;
     endcase
     end
     end

    主状态机完成后,I2C 控制器设计的大块就解决了,剩下的就是 SDA 数据线的输出了,该数据线采用三态使能输出,具体代码如下:

    assign Sda = sda_en ? sda_reg : 1'bz;

    对于使能信号 sda_en 按照上面的时序关系图可知,该信号在不同的状态,其高低电平变化的时刻是有差别的,比如在开始和结束状态,它是一直为高电平的,在写控制、写数据、读控制状态,它是在串行输出一字节数据期间(即 halfbit_cnt < 16 时)为高电平,之外的一个数据为位低电平,而在读数据状态时串行输入一字节数据期间(即halfbit_cnt < 16 时)为低电平电平,之外的一个数据位为高电平。具体代码如下:

     //SDA 三态使能信号 sda_en
     always@(*)
     begin
     case(main_state)
     IDLE:
     sda_en = 1'b0;
     WR_START,RD_START,STOP:
     sda_en = 1'b1;
     WR_CTRL,WR_WADDR,WR_DATA,RD_CTRL:
     if(halfbit_cnt < 16)
     sda_en = 1'b1;
     else
     sda_en = 1'b0;
     
     RD_DATA:
     if(halfbit_cnt < 16)
     sda_en = 1'b0;
     else
     sda_en = 1'b1;
     default:
     sda_en = 1'b0;
     endcase
     end

    本实验设计考虑到了多字节数据的读取情况,所以增加了数据读取和数据写入时的
    有效标志位信号。主要是标志读取数据时数据有效时刻和写数据时提供待写入数据时刻。
    具体代码如下:

     //写数据有效标志位
     assign Wr_data_vaild = ((main_state==WR_WADDR)&&
     (waddr_cnt==Wdaddr_num)&&
     (W_flag && scl_low)&&
     (ack == 1'b1))||
     ((main_state == WR_DATA)&&
     (ack == 1'b1)&&(scl_low)&&
     (wdata_cnt != Wrdata_num));
     //读数据有效标志位前寄存器
     assign rdata_vaild_r = (main_state == RD_DATA)
     &&(halfbit_cnt == 8'd15)&&scl_low;
     //读出数据有效标志位
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Rd_data_vaild <= 1'b0;
     else if(rdata_vaild_r)
     Rd_data_vaild <= 1'b1;
     else
     Rd_data_vaild <= 1'b0;
     end
     //读出的有效数据
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Rd_data <= 8'd0;
     else if(rdata_vaild_r)
     Rd_data <= sda_data_in;
     else
     Rd_data <= Rd_data;
     end

    到目前为止,整个 I2C 控制器的设计就完成了,接下来就进入仿真环节,我们需要EEPROM 的仿真模型进行仿真,这样能更好的检验 I2C 控制器设计的是否存在问题,以便后面进行优化改进。本实验采用的是镁光官网提供的 EEPROM 仿真模型,具体下载网
    址 为 http://www.microchip.com/zh/design-centers/memory/serial-eeprom/verilog-ibis-models,我们选择两个具有代表性的模型进行仿真,分别为 1 字节寄存器地址段的 24LC04B 和 2 字节寄存器地址段的 24LC64 仿真模型,利用这两个模型对我们设计的 I2C 控制器进行仿真。如图 30.18 为仿真验证的结构框图。

    这里我们对 1 字节寄存器地址段的 24LC04B 和 2 字节寄存器地址段的 24LC64 仿真模型的器件地址{A3,A2,A1}分别设置为 3’b001 和 3’b000,仿真时为了能分别对不同型号的模型进行仿真,在编写的 testbench 文件中采用了申明方法去选择当前使用哪个模型进行仿真,具体代码如下:

    `timescale 1ns/1ns
    `define CLK_PERIOD 20
    //仿真模型选择
    //`define TEST_M24LC64 //24LC64
    `define TEST_M24LC04 //24LC04
    module I2C_tb;
     reg Clk; //系统时钟
     reg Rst_n; //系统复位信号
     reg [15:0] Word_addr; //I2C 器件寄存器地址
     reg Wr; //I2C 器件写使能
     reg [7:0] Wr_data; //I2C 器件写数据
     wire Wr_data_vaild;//I2C 器件写数据有效标志位
     reg Rd; //I2C 器件读使能
     wire[7:0] Rd_data; //I2C 器件读数据
     wire Rd_data_vaild;//I2C 器件读数据有效标志位
     wire Scl; //I2C 时钟线 
     wire Sda; //I2C 数据线
     wire Done; //对 I2C 器件读写完成标识位
    localparam NUM = 6'd4; //单次读写数据字节数
    `ifdef TEST_M24LC64
     localparam DevAddr = 3'b000; //I2C 器件的器件地址
     localparam WdAr_NUM= 2; //I2C 器件的存储器地址字节数
    `elsif TEST_M24LC04
     localparam DevAddr = 3'b001; //I2C 器件的器件地址
     localparam WdAr_NUM= 1; //I2C 器件的存储器地址字节数
    `endif 
     I2C I2C(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Rddata_num(NUM),
     .Wrdata_num(NUM),
     .Wdaddr_num(WdAr_NUM),
     .Device_addr(DevAddr),
     .Word_addr(Word_addr),
     .Wr(Wr),
     .Wr_data(Wr_data),
     .Wr_data_vaild(Wr_data_vaild),
     .Rd(Rd),
     .Rd_data(Rd_data),
     .Rd_data_vaild(Rd_data_vaild),
     .Scl(Scl),
     .Sda(Sda),
     .Done(Done)
     );
    `ifdef TEST_M24LC64
     M24LC64 M24LC64(
     .A0(1'b0),
     .A1(1'b0),
     .A2(1'b0),
     .WP(1'b0),
     .SDA(Sda),
     .SCL(Scl),
     .RESET(!Rst_n)
     );
    `elsif TEST_M24LC04
     M24LC04B M24LC04(
     .A0(1'b1),
     .A1(1'b0),
     .A2(1'b0),
     .WP(1'b0),
     .SDA(Sda),
     .SCL(Scl),
     .RESET(!Rst_n)
     );
    `endif
     //系统时钟产生
     initial Clk = 1'b1;
     always #(`CLK_PERIOD/2)Clk = ~Clk;
     initial
     begin
     Rst_n = 0;
     Word_addr = 0;
     Wr = 0;
     Wr_data = 0;
     Rd = 0;
     #(`CLK_PERIOD*200 + 1)
     Rst_n = 1;
     #200;
    `ifdef TEST_M24LC64 //仿真验证 24LC64 模型
     //写入 20 组数据
     Word_addr = 0;
     Wr_data = 0;
     repeat(20)begin
     Wr = 1'b1;
     #(`CLK_PERIOD);
     Wr = 1'b0;
     repeat(NUM)begin //在写数据有效前给待写入数据
     @(posedge Wr_data_vaild)
     Wr_data = Wr_data + 1;
     end
     
     @(posedge Done);
     #2000;
     Word_addr = Word_addr + NUM;
     end
     #2000;
     //读出刚写入的 20 组数据
     Word_addr = 0; 
     repeat(20)begin
     Rd = 1'b1;
     #(`CLK_PERIOD);
     Rd = 1'b0;
     
     @(posedge Done);
     #2000;
     Word_addr = Word_addr + NUM;
     end
    `elsif TEST_M24LC04 //仿真验证 24LC04 模型
     //写入 20 组数据
     Word_addr = 100;
     Wr_data = 100;
     repeat(20)begin
     Wr = 1'b1;
     #(`CLK_PERIOD);
     Wr = 1'b0;
     repeat(NUM)begin //在写数据有效前给待写入数据
     @(posedge Wr_data_vaild)
     Wr_data = Wr_data + 1;
     end
     @(posedge Done);
     #2000;
     Word_addr = Word_addr + NUM;
     end
     #2000;
     //读出刚写入的 20 组数据
     Word_addr = 100;
     repeat(20)begin
     Rd = 1'b1;
     #(`CLK_PERIOD);
     Rd = 1'b0;
     
     @(posedge Done);
     #2000;
     Word_addr = Word_addr + NUM;
     end
     
    `endif
     #5000;
     $stop;
     end
    endmodule

    在 testbench 文件中,通过对 EEPROM 模型进行 20 写操作,每次写字节数为NUM,然后对 EEPROM 模型在刚写入数据的地址段进行读操作,通过比较读出和写入的数据验证 I2C 控制器设计是否正确。这里分别通过申明选择 TEST_M24LC64 或TEST_M24LC04 来作为当前的仿真模型。如图 30.19 所示为 2 字节地址的 EEPROM模型 24LC64 仿真结果。图 30.20 和图 30.21 分别为型号 24LC64 仿真模型写操作时序和读操作时序放大后的波形图。


    同样的方式选择 24LC04 型号 EEPROM 模型进行仿真,如图 30.22 所示为 1 字节地址的 EEPROM 模型 24LC604 仿真结果。图 30.23 和图 30.24 分别为型号24LC604 仿真模型写操作时序和读操作时序放大后的波形图。

    通过观察图 30.21 和图 30.24 的时序波形发现,在读操作时序结果中,读出的数据中某些位是高阻态,仔细观察波形可知,高阻态的位置正好是需要输出高电平的位置,这个的原因是 EEPROM 的仿真模型是完全与实际的器件是一样的,对于器件来说,只在输出 0 时将数据线拉低,而在高阻态或本应该为高电平的时刻都是设置为高阻态的,这个在仿真模型的代码中也有体现,具体体现这一点的代码如下:

     bufif1 (SDA, 1'b0, SDA_DriveEnableDlyd);


    其中,器件地址包括器件的地址字节数和 3 位的器件地址,具体分配如下:

    功能码主要是区分是写数据操作还是读数据操作,为了方便,我们直接规定,功能码为 0xf1 表示写数据操作,0xf2 表示读数据操作;起始地址是我们要读写数据的第一个地址;数据字节数表示要写入或读取的数据的字节个数,后面的数据 1 到数据n 表示要写入的 n 个数据,对于读操作没有这部分。如图 30.25 为该实验整体的设计框图。

    有关串口发送和接收以及 fifo 模块在前面章节都已经进行了讲解,这里就不重复讲解了,这里重点讲解命令解析模块的设计,命令解析模块的主要作用是对串口接收的数据进行解析,通过对接收的数据进行解析分析判断出是进行何种操作。根据我们自己规定的数据协议,进行如下的设计,首先我们将串口发送的数据的前 4 个数据存入一个缓冲区数据内,通过对功能码识别判断是写操作还是读操作,如果是写操作,就将后面待写入的数据存入 fifo 中;同时将器件地址、地址字节数、起始地址、数据字节数赋值给 I2C 对应的信号线;如果是读操作,就在前 4 个字节接收完成后将器件地址、地址字节数、起始 地址、数据字节数赋值给 I2C 对应的信号线,同时给 I2C 控制器模块的读使能给一个时钟周期的门控信号,使能 I2C 的读操作。读出的数据同样也是先存放在另外一个 fifo 中,然后送给串口发送模块发出。具体实现代码如下:

    module cmd_analysis(
     Clk,
     Rst_n,
     Rx_done,
     Rx_data,
     Wfifo_req,
     Wfifo_data,
     Rddata_num,
     Wrdata_num,
     Wdaddr_num,
     Device_addr,
     Word_addr,
     Rd
    );
     input Clk; //系统时钟
     input Rst_n; //系统复位
     input Rx_done; //串口接收一字节数据完成
     input[7:0] Rx_data; //串口接收一字节数据
     output reg Wfifo_req; //写 fifo 请求信号
     output reg[7:0] Wfifo_data; //写 fifo 数据
     output reg[5:0] Rddata_num; //I2C 总线连续读取数据字节数
     output reg[5:0] Wrdata_num; //I2C 总线连续读取数据字节数
     output reg[1:0] Wdaddr_num; //I2C 器件数据地址字节数
     output reg[2:0] Device_addr; //EEPROM 器件地址
     output reg[15:0] Word_addr; //EEPROM 寄存器地址
     output reg Rd; //EEPROM 读请求信号
     
     reg [7:0] buff_data[4:0];//串口接收数据缓存器
     //串口接收数据计数器
     reg [7:0]byte_cnt;
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     byte_cnt <= 8'd0;
     else if(Rx_done && byte_cnt==8'd4)begin
     if(buff_data[1]==8'hf2) //读数据指令
     byte_cnt <= 8'd0;
     else if(buff_data[1]==8'hf1) //写数据指令
     byte_cnt <= byte_cnt + 8'd1;
     else
     byte_cnt <= 8'd0; //错误指令
     end 
     else if(Rx_done)begin
     if(byte_cnt == 8'd4 + buff_data[4])
     byte_cnt <= 8'd0;
     else
     byte_cnt <= byte_cnt + 8'd1;
     end
     else
     byte_cnt <= byte_cnt;
     end
     //串口接收数据缓存器
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)begin
     buff_data[0] <= 8'h00;
     buff_data[1] <= 8'h00;
     buff_data[2] <= 8'h00;
     buff_data[3] <= 8'h00;
     buff_data[4] <= 8'h00;
     end
     else if(Rx_done && byte_cnt<5)
     buff_data[byte_cnt] <= Rx_data;
     else
     ;
     end
     //写 fifo 请求信号 Wfifo_req
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Wfifo_req <= 1'b0;
     else if(byte_cnt >8'd4 && Rx_done)
     Wfifo_req <= 1'b1;
     else
     Wfifo_req <= 1'b0;
     end
     //写 fifo 数据 Wfifo_data
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Wfifo_data <= 8'd0;
     else if(byte_cnt > 8'd4 && Rx_done)
     Wfifo_data <= Rx_data;
     else
     Wfifo_data <= Wfifo_data;
     end
     //EEPROM 读请求信号 Rd
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     Rd <= 1'b0;
     else if(byte_cnt == 8'd4 && Rx_done
     && buff_data[1]==8'hf2)
     Rd <= 1'b1;
     else
     Rd <= 1'b0;
     end
     //指令完成标志位
     reg cmd_flag;
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)
     cmd_flag <= 1'b0;
     else if((byte_cnt == 8'd4)&& Rx_done)
     cmd_flag <= 1'b1;
     else
     cmd_flag <= 1'b0;
     end
     //EEPROM 读写数据、寄存器地址字节数,器件地址,寄存器地址
     always@(posedge Clk or negedge Rst_n)
     begin
     if(!Rst_n)begin
     Rddata_num <= 6'd0;
     Wrdata_num <= 6'd0;
     Wdaddr_num <= 2'd0;
     Device_addr <= 3'd0;
     Word_addr <= 16'd0;
     end 
     else if(cmd_flag == 1'b1)begin
     Rddata_num <= buff_data[4][5:0];
     Wrdata_num <= buff_data[4][5:0];
     Wdaddr_num <= buff_data[0][5:4];
     Device_addr <= buff_data[0][2:0];
     Word_addr <= {buff_data[2],buff_data[3]};
     end
     else
     ;
     end
    endmodule

    下面编写 testbench 测试文件对设计的命令解析模块进行仿真验证,该仿真主要是分别模拟发送写数据操作和读数据操作指令,来观察相应的输出时序波形结果。通过仿真时序结果可以对该模块进行优化改进,具体的 testbench 文件如下:

    `timescale 1ns/1ns
    `define CLK_PERIOD 20
    module cmd_analysis_tb;
     reg Clk;
     reg Rst_n;
     reg Rx_done;
     reg [7:0] Rx_data;
     wire Wfifo_req;
     wire[7:0] Wfifo_data;
     wire[5:0] Rddata_num;
     wire[5:0] Wrdata_num;
     wire[1:0] Wdaddr_num;
     wire[2:0] Device_addr;
     wire[15:0] Word_addr;
     wire Rd;
     
     reg [15:0] addr;
     reg [7:0] data_num;
     reg [7:0] wr_data;
     reg [39:0] wdata_cmd;
     reg [39:0] rdata_cmd;
     cmd_analysis cmd_analysis(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Rx_done(Rx_done),
     .Rx_data(Rx_data),
     .Wfifo_req(Wfifo_req),
     .Wfifo_data(Wfifo_data),
     .Rddata_num(Rddata_num),
     .Wrdata_num(Wrdata_num),
     .Wdaddr_num(Wdaddr_num),
     .Device_addr(Device_addr),
     .Word_addr(Word_addr),
     .Rd(Rd)
     );
     //写 FIFO 模块例化
     fifo_wr wr(
     .clock(Clk),
     .data(Wfifo_data),
     .rdreq(),
     .wrreq(Wfifo_req),
     .empty(),
     .full(),
     .q(),
     .usedw()
     );
     //系统时钟产生
     initial Clk = 1'b1;
     always #(`CLK_PERIOD/2)Clk = ~Clk;
     initial
     begin
     Rst_n = 0;
     Rx_done = 0;
     Rx_data = 0;
     addr = 0;
     data_num = 0;
     wr_data = 0;
     wdata_cmd = 0;
     rdata_cmd = 0;
     #(`CLK_PERIOD*200 + 1)
     Rst_n = 1;
     #200;
     addr = 0;
     data_num = 4;
     wr_data = 0;
     send_uart_data_wr; //写数据
     #500;
     send_uart_data_rd; //读数据
     #500;
     addr = 4;
     data_num = 8;
     wr_data = 20;
     send_uart_data_wr; //写数据
     #500;
     send_uart_data_rd; //读数据
     #500;
     $stop;
     end
     //串口发送写数据命令和待写入数据任务
     task send_uart_data_wr;
     begin
     //写数据指令
     wdata_cmd = {8'h21,8'hf1,addr[15:8],addr[7:0],data_num};
     //发送写数据指令
     repeat(5)begin
     Rx_done = 1;
     Rx_data = wdata_cmd[39:32];
     #(`CLK_PERIOD)
     Rx_done = 0;
     #500;
     wdata_cmd = {wdata_cmd[31:0],8'h00};
     end
     //待写入数据
     Rx_data = wr_data;
     repeat(data_num)begin
     Rx_done = 1; 
     Rx_data = Rx_data + 1;
     #(`CLK_PERIOD)
     Rx_done = 0;
     #500;
     end
     end
     endtask
     //串口发送读数据命令任务
     task send_uart_data_rd;
     begin
     //读数据指令
     rdata_cmd = {8'h21,8'hf2,addr[15:8],addr[7:0],data_num};
     //发送读数据指令 
     repeat(5)begin
     Rx_done = 1; 
     Rx_data = rdata_cmd[39:32];
     #(`CLK_PERIOD)
     Rx_done = 0;
     #500;
     rdata_cmd = {rdata_cmd[31:0],8'h00};
     end
     end
     endtask
    endmodule

    仿真过程主要是模拟命令解析模块接收到写或读 EEPROM 操作指令后的操作是否和我们设计想要达到的目标是否一致。这里的写或读数据指令均采用任务的形式,写数据指令下待写入的数据是采用给定一个值的基础上递增进行赋值的。命令解析模块仿真的结果如图 30.26 所示。

    根据波形时序图,在模拟发送写操作命令 0x21,0xf1,0x00,0x00,0x04,0x06,0x07,0x08,0x09 时,在接收完数据字节数这个数据后,后面收到的数据就存入到 fifo 中去了,与我们设计的是一致的,同理可以分析读数据操作命令,也是没有问题的,这样命令解析模块就设计完成了。
    下面就是整个系统的设计,如图 30.25 整体设计框图中,在 I2C 写数据操作之前和读数据后分别加入了 fifo 模块,因为串口读写速度和 I2C 读写速度不一样,在这之间加入的 fifo 模块具有读写时钟不一致的特点,可以对数据进行一个缓存,这样就能解决前后速度不一样的问题,这里的两个 fifo 均设置的是读取数据在读请求前有效,这样设计的目的是为了与其他模块待输入数据与使能信号相匹配,里面的 fifo模块都是通过 QuartusII 软件生成的 IP 核,fifo 输入输出数据位宽均设置为 8位,深度设置为 64(多字节读取最多支持 32 字节,稍大于这个数就可以了)。在各模块均设计完成后,整个系统的顶层电路设计就显得比较简单了,根据设计的系统框图进行整合就可以了。整个系统设计的代码如下:

    module uart_eeprom(
     Clk,
     Rst_n,
     
     Uart_rx,
     Uart_tx,
     
     Sda,
     Scl
    );
    parameter Baud_set = 3'd4;//波特率设置,这里设置为 115200
     input Clk; //系统时钟
     input Rst_n; //系统复位
     
     input Uart_rx; //串口接收
     output Uart_tx; //串口发送
     
     inout Sda; //I2C 时钟线
     output Scl; //I2C 数据线
     wire [7:0] Rx_data; //串口接收一字节数据
     wire Rx_done; //串口接收一字节数据完成
     wire wfifo_req; //写 FIFO 模块写请求
     wire [7:0] wfifo_data; //写 FIFO 模块写数据
     wire [5:0] wfifo_usedw; //写 FIFO 模块已写数据量
     wire [5:0] rfifo_usedw; //读 FIFO 模块可读数据量
     wire rfifo_rdreq; //读 FIFO 模块读请求
     wire [5:0] Rddata_num; //I2C 总线连续读取数据字节数
     wire [5:0] Wrdata_num; //I2C 总线连续写取数据字节数
     wire [1:0] Wdaddr_num; //EEPROM 数据地址字节数
     wire [2:0] Device_addr; //EEPROM 地址
     wire [15:0] Word_addr; //EEPROM 寄存器地址
     wire Wr; //EEPROM 写使能
     wire [7:0] Wr_data; //EEPROM 写数据
     wire Wr_data_vaild;//EEPROM 写数据有效标志位
     wire Rd; //EEPROM 读使能
     wire [7:0] Rd_data; //EEPROM 读数据
     wire Rd_data_vaild;//EEPROM 读数据有效标志位
     wire Done; //EEPRO 读写完成标识位
     wire tx_en; //串口发送使能
     wire [7:0] tx_data; //串口待发送数据
     wire tx_done ; //一次串口发送完成标志位
     //串口接收模块例化
     uart_byte_rx uart_rx(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Rs232_rx(Uart_rx),
     .baud_set(Baud_set),
     .Data_Byte(Rx_data),
     .Rx_Done(Rx_done)
     );
     //指令解析模块例化
     cmd_analysis cmd_analysis(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Rx_done(Rx_done),
     .Rx_data(Rx_data),
     .Wfifo_req(wfifo_req),
     .Wfifo_data(wfifo_data),
     .Rddata_num(Rddata_num),
     .Wrdata_num(Wrdata_num),
     .Wdaddr_num(Wdaddr_num),
     .Device_addr(Device_addr),
     .Word_addr(Word_addr),
     .Rd(Rd)
     );
     //写缓存 fifo 模块例化
     fifo_wr fifo_wr(
     .clock(Clk),
     .data(wfifo_data),
     .rdreq(Wr_data_vaild),
     .wrreq(wfifo_req),
     .empty(),
     .full(),
     .q(Wr_data),
     .usedw(wfifo_usedw)
     );
     //EEPROM 写使能
     assign Wr = (wfifo_usedw == Wrdata_num)&&
     (wfifo_usedw != 6'd0);
     //I2C 控制模块例化
     I2C I2C(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Rddata_num(Rddata_num),
     .Wrdata_num(Wrdata_num),
     .Wdaddr_num(Wdaddr_num),
     .Device_addr(Device_addr),
     .Word_addr(Word_addr),
     .Wr(Wr),
     .Wr_data(Wr_data),
     .Wr_data_vaild(Wr_data_vaild),
     .Rd(Rd),
     .Rd_data(Rd_data),
     .Rd_data_vaild(Rd_data_vaild),
     .Scl(Scl),
     .Sda(Sda),
     .Done(Done)
     );
     //读缓存 fifo 模块例化
     fifo_rd fifo_rd(
     .clock(Clk),
     .data(Rd_data),
     .rdreq(rfifo_rdreq),
     .wrreq(Rd_data_vaild),
     .empty(),
     .full(),
     .q(tx_data),
     .usedw(rfifo_usedw)
     );
     //串口发送使能
     assign tx_en = ((rfifo_usedw == Rddata_num)&&Done)||
     ((rfifo_usedw < Rddata_num)&&
     (rfifo_usedw >0)&&tx_done);
     //读 FIFO 模块读请求 
     assign rfifo_rdreq = tx_en;
     //串口发送模块例化
     uart_byte_tx uart_tx(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .send_en(tx_en),
     .baud_set(Baud_set),
     .Data_Byte(tx_data),
     .Rs232_Tx(Uart_tx),
     .Tx_Done(tx_done),
     .uart_state()
     ); 
    endmodule

    下面对设计的整个系统进行编写 testbench 文件来仿真验证,整个仿真主要是通过串口发送模块模拟对该系统发送指令进行仿真验证,这里就只选用 M24LC64 这个仿真模型进行仿真,M24LC04 的仿真模型类似(读者可尝试对该模型进行仿真),编写的 testbench 文件具体代码如下:

    `timescale 1ns/1ns
    `define CLK_PERIOD 20
    module uart_eeprom_tb;
     reg Clk;
     reg Rst_n;
     reg tx_en;
     reg [7:0] tx_data;
     wire tx_done;
     wire Uart_rx;
     wire Uart_tx;
     wire Sda;
     wire Scl;
     reg [15:0] addr;
     reg [7:0] data_num;
     reg [7:0] wr_data;
     reg [39:0] wdata_cmd;
     reg [39:0] rdata_cmd;
    localparam Baud_set = 3'd4; //波特率设置,这里设置为 115200
    localparam DevAddr = 3'b000;//I2C 器件的器件地址
    localparam WdAr_NUM = 2'd2; //I2C 器件的存储器地址字节数
     //串口发送模块例化
     uart_byte_tx uart_tx(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .send_en(tx_en),
     .baud_set(Baud_set),
     .Data_Byte(tx_data),
     .Rs232_Tx(Uart_rx),
     .Tx_Done(tx_done),
     .uart_state()
     );
     //串口读写 EEPROM 模块例化
     uart_eeprom #(.Baud_set(Baud_set))
     uart_eeprom(
     .Clk(Clk),
     .Rst_n(Rst_n),
     .Uart_rx(Uart_rx),
     .Uart_tx(Uart_tx),
     .Sda(Sda),
     .Scl(Scl)
     );
     //EEPROM 模型例化
     M24LC64 M24LC64(
     .A0(1'b0),
     .A1(1'b0),
     .A2(1'b0),
     .WP(1'b0),
     .SDA(Sda),
     .SCL(Scl),
     .RESET(!Rst_n)
     );
     //系统时钟产生
     initial Clk = 1'b1;
     always #(`CLK_PERIOD/2)Clk = ~Clk;
     initial
     begin
     Rst_n = 0;
     tx_data = 0;
     tx_en = 0;
     addr = 0;
     data_num = 0;
     wr_data = 0;
     wdata_cmd = 0;
     rdata_cmd = 0;
     #(`CLK_PERIOD*200 + 1)
     Rst_n = 1;
     #200;
     addr = 0;
     data_num = 4;
     wr_data = 0;
     send_uart_data_wr;//写数据
     @(posedge uart_eeprom.I2C.Done);
     #500;
     send_uart_data_rd;//读数据
     @(posedge uart_eeprom.I2C.Done);
     #500;
     addr = 4;
     data_num = 8;
     wr_data = 20;
     send_uart_data_wr;//写数据
     @(posedge uart_eeprom.I2C.Done);
     #500;
     send_uart_data_rd;//读数据
     @(posedge uart_eeprom.I2C.Done);
     //从 EEPROM 读出的数据串口发送出去,等待发送完成
     repeat(data_num)begin 
     @(posedge uart_eeprom.tx_done);
     end
     #5000;
     $stop;
     end
     //串口发送写数据命令和待写入数据任务
     task send_uart_data_wr;
     begin
     //写数据指令
     wdata_cmd = {{2'b00,WdAr_NUM,1'b0,DevAddr},8'hf1,
     addr[15:8],addr[7:0],data_num};
     //发送写数据指令
     repeat(5)begin
     tx_en = 1;
     tx_data = wdata_cmd[39:32];
     #(`CLK_PERIOD)
     tx_en = 0;
     @(posedge tx_done)
     #100;
     wdata_cmd = {wdata_cmd[31:0],8'h00};
     end
     //待写入数据
     tx_data = wr_data;
     repeat(data_num)begin
     tx_en = 1;
     tx_data = tx_data + 1;
     #(`CLK_PERIOD)
     tx_en = 0;
     @(posedge tx_done)
     #100;
     end
     end
     endtask
     //串口发送读数据命令任务
     task send_uart_data_rd;
     begin
     //读数据指令
     rdata_cmd = {{2'b00,WdAr_NUM,1'b0,DevAddr},8'hf2,
     addr[15:8],addr[7:0],data_num};
     //发送读数据指令
     repeat(5)begin
     tx_en = 1;
     tx_data = rdata_cmd[39:32];
     #(`CLK_PERIOD)
     tx_en = 0;
     @(posedge tx_done)
     #100;
     rdata_cmd = {rdata_cmd[31:0],8'h00};
     end
     end
     endtask
    endmodule

    仿真过程主要是通过串口发送模块模拟对该系统发送读写指令进行仿真验证,分别进行了 2 次写和读指令的发送,读指令主要是对刚写入地址的数据进行读出,我们可以通过观察时序波形图,比较写入数据与读出数据是否一致来验证系统设计的正确性。如图 30.27 为系统仿真波形时序图。

    分别对一次写数据指令和一次读数据指令的波形进行放大后观察分析,如图 30.28 和图30.29 分别为一次对 EEPROM 写数据过程和读数据过程的波形时序图。

    这次写数据过程的仿真是通过串口发送模块模拟发送指令数据 0x21,0xf1,0x00,0x00,0x04,0x05,0x06,0x07,0x08,向 EEPROM 中起始地址为 0 开始写入 0x05,0x06,0x07,0x08 四个数据,可以通过观察图 30.28 的中 SDA 和 SCL 波形图分析得知,写入的数据确实是这四个数据,说明系统中串口写 EEPROM 部分是没有问题的。
    读数据过程的仿真是通过串口发送模块模拟发送指令数据 0x21,0xf2,0x00,0x00,0x04,向 EEPROM 中起始地址为 0 开始读出四个数据,通过观察图 30.29 的中 SDA和 SCL 波形图分析得知,读出的四个数据为 0x05,0x06,0x07,0x08,与之前在这些地址写入的数据是一致的,说明这次的读数据时没有问题的,同样的方式经过多次的验证,串口读写 EEPROM 系统是可以正常工作的。仿真验证进行完成后,接下来就是进行板级验证,先是引脚的分配,本实验板级验证平台是 AC620 开发板,根据开发板的引脚分配表对本实验所用引脚进行分配。如图 30.30 是 AC620 开发板上 EEPROM 部分的硬件原理图,在引脚分配之前,是需要对这一块硬件电路有所了解的。

    细心的读者会发现,I2C 时钟线 SCL 和 I2C 数据线没有进行硬件上拉处理,与前面讲解的需要上拉处理不一样,可能会猜想是硬件设计的问题。这里我说明一下,硬件设计是没有问题的,因为对于 FPGA 是可以通过软件对引脚进行上拉处理的,这个也是本实验包含的一个知识点。通过 Quartus II 软件将管脚设置为上拉电阻(弱上拉)的方法,具体的步骤如下:
    1.在菜单 Assignments 中选择 Pin Planner,也可以直接点击面板上引脚分配的图标

    2.这样进入引脚分配的界面,在弹出的 Pin Planner 界面的 All Pins 区域里点击鼠标右键,找到 Customize Columns。

    1. 在弹出的 Customize Columns 对 话 框的 左 列表 框 选 择 Weak Pull-Up Resistor,再点击,把 Weak Pull-Up Resistor 添加到右列表框,这样在 Pin Planner 的 All Pins 区域里就有一列 Weak Pull-Up Resistor 的设置项。
    2. 再把需要上拉的 SDA 和 SCL 在其对应的 Weak Pull-Up Resistor 列的位置双极鼠标左键,就会弹出一个 Off/On 的选项,选上 On 就可以了。最后完成后的设置如下:

      引脚分配和上拉设置完成后,对我们设计的系统顶层文件进行综合布局布线生成 sof 下载文件,然后下载到 AC620 实验平台进行板级验证。板级验证需要用到串口软件工具,
      这里使用的是名叫格西烽火的串口工具,选择对应的开发板的串口号和波特率,系统设计的波特率在顶层文件 uart_eeprom 中可进行设置,这里我们设置的波特率为115200bps。
      板级验证时,我们先往 EEPROM 里写入一些数据,也就是在串口发送 6 组数据,每组数据写入四个数据;写完后发送读数据命令,读出写入的数据,分两次读,一次读 20个数据,一次读 4 个数据,具体的发送读写指令操作和串口接收的数据如下图:

      从串口发送和接受的数据分析可得,从 EEPROM 读出的 20 个数据和写入的 20 个数据是一样的,这就说明,设计的整个系统是工作正常的,读者可以多次进行多组测试来验证设计的完整性,整个的测试还可以利用 Quartus II 软件中的 SignalTap II Logic Analyzer 工具对 SDA 和 SCL 的波形进行抓取分析系统设计的正确性,这里就不详细的说明了,读者可以自己进行尝试。整个串口读写 EEPROM 的系统设计就算完成了,里面还有很多需要完善的地方,读者可以在学习完成后,对本实验进行改进和拓展。

      本节小结

      本节主要从二线制 I2C 协议的基本知识和概念、 I2C 器件之间数据通信过程,比较全面的了解了 I2C 协议,并以此设计一个可进行读写操作的 I2C 控制器,结合前面设计的串口收发模块和 FIFO 模块设计了一个简易应用,实现了串口对 EEPROM 存储器的读写功能,并进行相应的板级验证。读者可以在此基础上进行一定的优化和拓展,AC620实验开发板上还有音频模块和时钟模块挂在在 I2C 总线上,读者可以利用这些模块实现更丰富的应用

    3
    Design Compile(DC)使用简版
    « 上一篇 2023-01-14
    LoRa码元调制、编码与解调
    下一篇 » 2022-12-31

    评论 (0)

    取消