IIC协议驱动EEPROM的Verilog实现与竞争冒险与下降沿触发、多重驱动

一、前言

  笔者是一名FPGA资深小白,跟着正点原子已经编写了不少Verilog代码,但是始终没有找到Verilog的感觉。在大佬的推荐下,遂决定按照自己的思路重新进行历程的编写,以增强对Verilog的应用能力。恰巧正点原子的历程中EEPROM未曾亲手编写,于是决定自己设计信号以实现iic协议。
  iic协议并不复杂,但笔者近乎用了半个月的时间(别说了,我太菜了!!!)才彻底实现EEPROM的随机读/写操作。本文会记录笔者的思路以及实现方式,故会贴出笔者写过的但不能用的Verilog代码。最终可以使用的iic协议代码会贴在文章最末,以供指正批评
  虽然本代码是笔者自己设计实现的,但参考了多种历程的思路与实现方式,笔者会在文中提及处注明所参考文章的链接。iic协议并不复杂,实现方式也多种多样,但通过使用Verilog描述iic的硬件逻辑电路确实是一件很有意思的事。

二、软硬件平台

软件平台

  1. 操作系统:win10
  2. 开发套件:Vivado 2019.2
  3. 仿真工具:Vivado自带仿真工具

硬件平台

  1. FPGA型号:Xilinx公司的zynq7020
  2. EEPROM型号:Microchip公司的AT24LC04B

三、IIC与EEPROM

IIC简介

  IIC(Inter-Integrated Circuit) 其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在上世纪80年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I2C串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL
  所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。
  IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)

1. 写操作大致步骤

  • 主机发送起始信号给从机
  • 主机发送数据
  • 主机发送结束信号

2. 读操作大致步骤

  • 主机发送起始信号给从机
  • 主机发送要读取的数据的地址
  • 主机接收从机发送的数据
  • 主机发送结束信号

3. IIC总线有以下几种状态

1. 空闲状态

  IIC总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。笔者也是在搜集资料的过程中了解到空闲状态必须是要两条信号线均处于高电平,原本还想保持SCL正常输出,只改变SDA信号线进行相应操作,遂作罢。

2. 起始信号和结束信号

  这里直接上图来得方便:

  • 起始信号是要求在SCL时钟线保持高电平的期间,数据线SDA由高电平被拉低。起始信号标志着一次数据传输的开始
  • 停止信号则是要求SCL时钟线保持高电平的期间,数据线SDA由低电平被拉高。停止信号标志着一次数据传输的结束

3. 数据传输状态

  这里以传输一个字节为例给出一个完整的IIC传输时序图

  可以看到,IIC总线希望我们在 SCL高电平期间保持SDA不发生改变 。在SCL时钟的配合下,数据按照以8bit即一个字节为单位串行发出,其发送顺序是字节高位到低位。那在结束传送之后我们怎么确保从机收到了我们的指令呢,这当然是要靠从机给我们一个答复,即上图的应答信号

4. 应答信号/非应答信号

  IIC总线上的所有数据都是以8bit(1个字节)传送的,主机每发送一个字节,就在第9个时钟脉冲期间释放数据线(即不再对SDA进行输出,转为接收的状态),由从机反馈一个应答信号。
  应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。但这里有一个例外,主机从从机中读取数据的过程中,主机不想再接收数据,主机会给从机反馈一个非应答位(NACK)。可以在上图中看到应答信号的产生时间

4. 从机地址

  那么,现在还有一个问题就是:当多个IIC从机挂在总线上时,怎么才能只与我们想要传输数据部件进行通信呢。这里有一个称为器件地址(从机地址:SLAVE ADDRESS)的东西。
  每个IIC器件都有一个器件地址,有些IIC器件的器件地址是固定的,而有些IIC器件的器件地址由
一个固定部分和一个可编程的部分
构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C总线上(本段摘自正点原子开发指南)

EEPROM简介

  这里我要先介绍一下正点原子家的EEPROM的讲解,笔者是按照正点原子的教程进行学习的(你问我为啥不看黑金的,黑金写的太大道至简了)。总之,笔者在这里会以正点原子家的EEPROM工程为例,逐步的阐述所遇到的知识点(keng)。
  笔者不准备对EEPROM这个东西做出过多的讲解(其实是懒得复制了),这里只需要知道这是一个带电可擦可编程只读存储器,说白了就是你写入的数据掉电不丢失,再简单一点,就是一个简单版的U盘(只是没有文件系统等等等等。。。)
  正点原子家使用的EEPROM型号是AT24C64,64Kbit(注意是bit),其内部分成256页,每页32字节,共8192个字节。这里有一个很形象的描述:可以把AT24C64看作一本书,那么这本书有256页,每页有32行,每行有8个字,总共有256*32*8=65536个字,对应着AT24C64的64*1024=65536个bit(摘自正点原子开发手册)。

EEPROM从机地址

  接下来要说明的就是EEPROM的从机地址了,对于AT24C64而言,其器件地址为1010加3位的可编程地址,3位可编程地址由器件上的3个管脚E2、E1、E0(下面图中A2、A1、A0只是换了个符号)的硬件连接决定(如下图所示)。当硬件电路上分别将这三个管脚连接到GND或VCC时,就可以设置不同的可编程地址(这是正点原子对于AT24C64的描述)。

  进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0时表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。对于AT24C64来说,其传输器件地址格式如下图所示:

  所以具体器件地址是什么样的,则要参考这部分的硬件电路,这里我也将正点原子领航者的EEPROM部分的硬件电路贴出来:

  可以看到A2、A1、A0三个引脚全部置零,可以得到器件地址为1010000(7bit)。这部分黑金板载EEPROM也是如此,故不再贴出具体电路。

EEPROM数据地址

  实验用到的EEPROM存储器,内部就是一系列顺序编址的存储单元。所以,当我们对一个器件中的存储单元(包括寄存器)进行读写时,在发送完成器件地址后,要指定存储单元的地址即字地址,而地址需要几bit则要根据存储器的容量进行计算。
  对于AT24C64来说,其存储单元容量为64Kb=8KB需要13位(2^13=8KB)的地址位,而IIC又是以字节为单位进行传输的,所以需要用两个字节地址来寻址整个存储单元。下面是单字节寻址和双字节寻址的示意图:

单字节地址

双字节地址

  注意: 在这里笔者遇到了第一个bug,正点原子讲得很对,通常情况下也的确是这样。由于笔者所用的FPGA开发板是黑金的,所以硬件电路部分自然是要以黑金为准。
  首先是器件地址(SLAVE ADDRESS),型号为24LC04B的EEPROM与上述所说并无差别,同样具有A2、A1、A0三个可编程地址管脚:

  24LC04B芯片的大小为4Kbit,即512Byte,这样算来其地址有9bit(2^9=512),是双字节地址。但笔者在之后的实现过程中,始终出现问题,即上述的应答型号,从机并没有如期返回低电平,而是一直返回高电平。笔者无奈只能查阅黑金家的官方例程,终于让我找到了问题所在:

  在黑金的官方例程中所使用的IIC驱动模块,有一个输入bit用以确定IIC所要驱动的器件是几字节的地址,下图是历程在例化此模块的输入:

    .i2c_addr_2byte     (1'b0), //register address is 1 byte

  看到这里笔者不由自主地发出“卧槽”的疑问,遂查阅24LC04B的datasheet,让我找到了猫腻:

  这里是说,24LC04B器件地址的后三位那,和我们之前所说的可编程器件地址毛关系都没有,其器件地址即为固定的1010,而后三位(后面称其为B2、B1、B0)是Block选择位,那么这是啥,给出datasheet中的一段描述性原文:
The device is organized as two blocks of 256 x 8-bit memory with a 2-wire serial interface.
  笔者突然想起计算机原理所学的存储体和字扩展的概念,遂理解了上述文字所表达的意思。通俗的来说,24LC04B EEPROM是由两个模块构成的,每个模块有256Byte的容量,而通过控制图中Block Select,我们可以选择要对哪一块进行操作,而Block Select则是器件地址的后三位
  图中文字说的是Block Select共有三位,对于24LC04B来说,由于它只有两个模块构成,所以其B2、B1位可以是任意值,最后一位B0决定了选择哪一块进行操作。笔者还查阅了24LC08B的手册,它是由4个模块构成的,所以要使用B1、B0两位进行选择。
  由于每个模块为256Byte,所以地址自然而然就是8bit了,所以要使用 单地址模式。那么这个故事告诉我们,使用的硬件和参考的教程不匹配时,还是乖乖查一查数据手册来的保险一些。

EEPROM的五访问方式

1. 字节写(BYTE WRITE)

  这应该是最常用的EEPROM的操作方式了,即向EEPROM的任意地址写入一个数据,具体的操作流程如下图所示

  上图展现的是单字节地址写入的流程,即在发送完成地址位后发送数据即可,那么双字节地址字节写入方式也就和这个大同小异了。下图给出双字节地址字节写入方式的时序图

2. 页写(PAGE WRITE)

  页写也叫连续写的概念在上文中提及过,这里不再赘述。(忘了的话给你个超链接)总之,在文中提到的正点原子开发板上的AT24C64每一页是32个字节,即可以连续写入32个字节。笔者所用的24LC04中,每页的大小16个字节,这里是示意图:

  同样给出双字节地址的时序图:

  另外值得注意的是,在进行连续写时,当写完一页的最后一个单元,地址指针会指向该页的开头,如果继续再写入数据,就会覆盖该页的起始数据。那么此刻细心的你会不会有这样的疑问,页写的地址是随意的吗?
  在阅读24LC04数据手册页写入的部分时,我看到这样一份提示,在这里贴出来:

  其大意是这样的:不论实际进行写入的数据是多少,页写入操作都会被限制在一个单个的物理页。物理页的起始边界地址是【页面大小】(即16个字节)的整数倍(即0、16、32等等),结束边界是【页面大小-1】的整数倍(这里应该有误,笔者感觉应该是页面大小的整数倍-1,即每一个物理页的地址范围是0-15,16-31、32-47等等)。如果一个页写入的操作试图跨越物理页面的边界,结果是 数据并不会被写入到下一个物理页,而是会从当前页的起始位置开始覆盖写入。 这也就是为什么应用软件必须要设法避免这种情况的发生。

3. 当前地址读(CURRENT ADDRESS READ)

  笔者不打算过多介绍这种读取数据的方式,当前地址读是指在一次读或写操作后发起读操作。由于IIC器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一个字地址的数据。也就是说上次读或写操作的单元地址为02时,当前地址读的内容就是地址03处的单元数据。可以看出当前地址读极不方便读取任意的地址单元,所以有随机读

4. 随机读(RANDOM READ)

  即从任意地址读取数据。话不多说,直接上图:

  这里的流程是有点奇怪的,首先发送了一次写操作(S/W=0为写操作),这里我们称为Dummy Write虚写操作,称为虚写是因为并没有写入数据,而是通过这种操作将地址指针指向虚写操作中的地址,等从机应答后,就可以使用当前地址读的方式进行读操作了。同样给出时序图:

  注意:在发送完地址之后要重新发送一个起始信号,结束时是主机进行非应答响应,即主机将SDA拉高发送1

4. 连续读(SEQUENTIAL READ)

  这次是真的连续,连续读操作可以允许读遍整个模块(意即上文所说的EEPROM由两个模块组成的那个模块),这里的连续读操作即为在上述随机读操作的基础上,最后主机不进行非应答响应,而是进行应答响应,不难理解,不再赘述:

EEPROM写入周期

  这是一个笔者在之后遇到的问题,出现的原因是笔者在对EEPROM循环写入很多数据(全部是字节写操作,只是循环多次)时,发现只有第一个数据可以写入,后面的数据EEPROM均未产生从机应答的低电平信号,笔者对比波形图发现没有问题,最终在对比两份历程(笔者用的是黑金的开发板,所以黑金的程序必然可以使用;但笔者参考的是正点原子的框架,所以部分参数抄的正点原子的)发现两次字节写入的时间间隔不同,在数据手册中的写字节部分并没有提及写入的时延问题,笔者在数据手册的交流特性处找到了一栏:

  总之笔者在这里就是想说明,两次字节写操作或两次连续写操作之间都是有时间限制的,但是两次读操作之间是不需要延时的。这一点希望引起重视。

四、模块设计

功能目标

  笔者所设计的IIC驱动程序实现了字节写和随机读两种功能,基本上这是最常用的两种功能,通过字节写和随机读,可以对EEPROM的所有地址进行写操作和读操作。笔者希望能够尽量简单的实现这两个功能(别写太多行!)。
  笔者所实现的IIC的传输速率是100kb/s(即100kHZ的SCL时钟),注意这里不是想多少就是多少,在24LC04 EEPROM的数据手册中写明了可以使用100kHZ和 400kHZ的时钟,不同的时钟频率对电压有要求,只有Vcc ≥ 2.5V才可以使用400kHZ的时钟。
  那么在完成字节写和字节读的IIC驱动编写后,笔者则对EEPROM写入256个自己的数据,然后再依次读出,看和写入的是否一致

参考资料

  既然要设计IIC的实现方式,自然要参考两大卖家所给出的历程。这里首先要介绍黑金关于IIC模块所提供的方式:
  I2C时序虽然简单,但是写的不好也会出现很多问题,在开源网站 http://opencores.org/ 上我们可以找到很多非常好的代码,这些代码大部分都提供详的文档和仿真。俗话说,他山之石,可以攻玉,恰当的使用开源代码,不光能提升我们的开发效率,也能学习别人的开发思路。由于代码大部分都是经过很长时间反复修改,反复精炼后的,所以有些代码理解起来可能比较困难,在不能很好的理解别人代码的时候,最好的办法就是仿真。(——黑金动力)
  笔者作为一名小白,自然也是第一次听说黑金所提到的开源IP核网站,遂立即注册(网站的注册并不麻烦,但要等一天才能收到网站的回执邮件),网站里有各种各样别人写好的Verilog代码,在网站的Communication controller栏目下,可以找到:

  笔者下载下来看了一下里面的代码,嗯,看不懂!开源IIC代码写的非常完善,给用户提供了很多的输入输出信号,但当下阶段笔者还处于只是想简单的实现一下的水平(先压到目标知识栈里),遂翻阅正点原子所提供的历程。
  笔者阅读这份历程的过程中,有很多疑惑的地方,但也借鉴了很多地方,这里逐一展示正点原子的部分代码实现,以及作者的思考过程。

IIC驱动模块(iic_driver)

IIC驱动模块的接口定义

    input                clk        ,    
    input                rst_n      ,   
                                         
    //i2c interface                      
    input                i2c_exec   ,  //I2C触发执行信号
    input                bit_ctrl   ,  //字地址位控制(16b/8b)
    input                i2c_rh_wl  ,  //I2C读写控制信号
    input        [15:0]  i2c_addr   ,  //I2C器件内地址
    input        [ 7:0]  i2c_data_w ,  //I2C要写的数据
    output  reg  [ 7:0]  i2c_data_r ,  //I2C读出的数据
    output  reg          i2c_done   ,  //I2C一次操作完成
    output  reg          i2c_ack    ,  //I2C应答标志 0:应答 1:未应答
    output  reg          scl        ,  //I2C的SCL时钟信号
    inout                sda        ,  //I2C的SDA信号
                                       
    //user interface                   
    output  reg          dri_clk       //驱动I2C操作的驱动时钟

  这里笔者着重说一下此模块所流出的接口 dri_clk 产生这个接口的原因,是此模块中主要的always模块都是由dri_clk所驱动的,整个dri_clk的频率是100kHZ的四倍,是一个四倍频的时钟
  在这个四倍频的时钟的每一个上升沿去修改SCL、SDA两个信号所需要输出的点平。所以将这个时钟信号引了出来,用户可以通过这个时钟信号来驱动IIC模块进行相应操作。
  由于IIC驱动模块的always块时钟是400kHZ,而FPGA的系统时钟往往要高很多,上述引出的这个时钟信号就是为了能方便地为IIC驱动模块提供开始信号(即拉高i2c_exec),方便地接收完成信号(i2c_done)。
  不过由于四倍频的原因,操作起来相对繁琐一些,笔者将发送器件地址的部分代码贴出来,各位可以感受一下:

st_sladdr: 
    begin                         //写地址(器件地址和字地址)
        case(cnt)                            
            7'd1 : sda_out <= 1'b0;          //开始I2C
            7'd3 : scl <= 1'b0;              
            7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
            7'd5 : scl <= 1'b1;              
            7'd7 : scl <= 1'b0;              
            7'd8 : sda_out <= SLAVE_ADDR[5]; 
            7'd9 : scl <= 1'b1;              
            7'd11: scl <= 1'b0;              
            7'd12: sda_out <= SLAVE_ADDR[4]; 
            7'd13: scl <= 1'b1;              
            7'd15: scl <= 1'b0;              
            7'd16: sda_out <= SLAVE_ADDR[3]; 
            7'd17: scl <= 1'b1;              
            7'd19: scl <= 1'b0;              
            7'd20: sda_out <= SLAVE_ADDR[2]; 
            7'd21: scl <= 1'b1;              
            7'd23: scl <= 1'b0;              
            7'd24: sda_out <= SLAVE_ADDR[1]; 
            7'd25: scl <= 1'b1;              
            7'd27: scl <= 1'b0;              
            7'd28: sda_out <= SLAVE_ADDR[0]; 
            7'd29: scl <= 1'b1;              
            7'd31: scl <= 1'b0;              
            7'd32: sda_out <= 1'b0;          //0:写
            7'd33: scl <= 1'b1;              
            7'd35: scl <= 1'b0;              
            7'd36: 
                begin                     
                    sda_dir <= 1'b0;             
                    sda_out <= 1'b1;                         
                end                              
            7'd37: scl     <= 1'b1;            
            7'd38: 
                begin                     //从机应答 
                    st_done <= 1'b1;
                    if(sda_in == 1'b1)           //高电平表示未应答
                        i2c_ack <= 1'b1;         //拉高应答标志位     
                end                                          
            7'd39: 
                begin                     
                    scl <= 1'b0;                 
                    cnt <= 1'b0;                 
                end                              
            default :  ;                     
        endcase                              
    end          

  通过对时钟的计数判断每个时刻应该产生什么样的信号,笔者认为正点原子的代码应该不难理解,而且其官网亦有相应的开源教程可供学习。笔者在一开始读到这段代码时并不明白为什么要用四倍频的时钟去驱动,这也就引起了笔者一系列的方案测试。这部分内容笔者会在之后的进行详细的阐述。笔者首先希望借鉴的是其状态机设计。这里正点原子使用的是标准的三段式状态机,这里贴出每个状态的示意图及代码:

    //localparam define
    localparam  st_idle     = 8'b0000_0001; //空闲状态
    localparam  st_sladdr   = 8'b0000_0010; //发送器件地址(slave address)
    localparam  st_addr16   = 8'b0000_0100; //发送16位字地址
    localparam  st_addr8    = 8'b0000_1000; //发送8位字地址
    localparam  st_data_wr  = 8'b0001_0000; //写数据(8 bit)
    localparam  st_addr_rd  = 8'b0010_0000; //发送器件地址读
    localparam  st_data_rd  = 8'b0100_0000; //读数据(8 bit)
    localparam  st_stop     = 8'b1000_0000; //结束I2C操作

  这里可以看出,通过使能i2c_exec,触发IIC驱动进入发送状态,根据bit_ctrl位决定字地址的长度,根据wr_flag位决定执行什么种类的操作。说实话,笔者是第一次写典型的三段式状态机,所以出了一些问题,这里也权当闲谈,记录一下。
  正常的三段式状态机,通常来说一段用时序电路描述状态切换,一段用组合逻辑电路描述状态转移,第三段则是时序电路描述输出。这里有个很关键的细节,笔者认为正点原子的代码出了点问题,造成当时笔者在进行状态机编写时实在很费脑筋。
  这个问题出在第三段时序电路描述输出这里。在正点原子的代码里面,第三段时序电路描述的case敏感表现态寄存器current_state。这样做不是不可以,只是思考起来会有点费劲。当条件发生变化时,首先次态寄存器next_state会发生变化,但由于次态寄存器转移到现态寄存器需要一个时钟周期的延迟,所以如果以现态寄存器current_state为case敏感表,就会出现明明次态已经发生变化,笔者却还要在现态描述输出。嘛,总之就是这样有点别扭。
  笔者在这里贴出标准的三段式状态机的verilog代码(注意第三段的always模块里,case语句的敏感参数是next_state):

module state(
    input       rst_n,
    input       clk,
    input       i1,
    input       i2,
    output reg  o1,
    output reg  o2,
    output reg  err
);

reg [2:0] next_state,current_state;
parameter [2:0] 
    IDLE    = 3'b000,
    S1      = 3'b001,
    S2      = 3'b010,
    ERROR   = 3'b100,

//1st always block,sequential state transition
always@(posedge clk or negedge rst_n)
    if(!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;

//2nd always block,sequential state transition
always@(*)
begin
    next_state = 3'bx;
    case(current_state)
        IDLE:
            begin
                if(~i1)         next_state = IDLE;
                if(i1 && i2)    next_state = S1;
                if(i1 && ~i2)   next_state = ERROR;
            end
        S1:
            begin
                if(~i2)         next_state = S1;
                if(i2 && i1)    next_state = S2;
                if(i2 && ~i1)   next_state = ERROR;
            end
        S2:
            begin
                if(i2)          next_state = S2;
                if(~i2 && i1)   next_state = IDLE;
                if(~i2 && ~i1)  next_state = ERROR;
            end 
        ERROR:
            begin
                if(i1)          next_state = ERROR;
                if(~i1)         next_state = IDLE;
            end
    endcase     
end
//3rd always block,the sequential FSM output
always@(posedge clk or negedge rst_n)
    if(!rst_n)
        {o1,o2,err} <= 3'b000;
    else
        begin
            {o1,o2,err} <= 3'b000;
            case(next_state)
                IDLE: {o1,o2,err} <= 3'b000;
                S1:   {o1,o2,err} <= 3'b100;
                S2:   {o1,o2,err} <= 3'b010;
                ERROR:{o1,o2,err} <= 3'b111;  
            endcase
        end
endmodule

IIC驱动模块信号设计

  我们继续来讨论四倍频的问题,由于笔者才疏学浅,一开始实在不明白为什么要四倍频,笔者认为就拿SCL的时钟频率作为always模块的驱动不可以吗?笔者说干就干,这里首先贴上笔者用TimeGen设计的时序信号图:

  上图中iic_scl是模块的SCL输出信号,笔者首先先做了一个和其同频的时钟iic_clk,然后在iic_en为1的时候,就把iic_scl和iic_clk(的反)连接起来,iic_sda则是SDA信号,它是一个inout类型的,通过控制sda_dir来控制iic_sda是输入还是输出(学过单片机的应该知道,使用iic协议的时候要频繁的切换I/O引脚的输入输出状态,FPGA里也一样),这里结合代码来看:

assign iic_scl = scl_en?   (~iic_clk):1'd1;//使能后开始驱动iic(1: 使能)
    
/***sda 控制 ***/
reg sda_dir;

assign  iic_sda = sda_dir ? sda_out : 1'bz  ;     //SDA数据输出或高阻

assign  sda_in  = iic_sda ;                          //SDA数据输入     

  笔者在连接iic_clk和iic_scl信号的时候取了一个反,这样在iic_clk是上升沿的时候iic_scl是下降沿,那么在这个时候改变数据刚好可以满足SCL在高电平期间SDA信号不发生改变
  念及此,笔者便立刻按照这个思路进行设计,在这个过程中笔者遇到了第一个问题,引发了笔者深刻的思考。

竞争冒险

    assign iic_scl = scl_en?   (~iic_clk):1'd1;//使能后开始驱动iic(1: 使能)

  对于左侧的图来说,由于iic_scl原本是高电平,在iic_en使能的时候,此时的iic_clk为高电平,取反之后iic_scl被拉低,这没毛病。但是对于右侧图,在红框中的那个iic_clk的上升沿,iic_en的电平被拉低,此时iic_scl就会被置为持续性的1,可是在上升沿的这个瞬间,iic_scl仍然和(~iic_clk)连接到一起,他应该是下降沿啊。
  走到了这一步,笔者立刻进行进一步的仿真和询问大佬,这里先展示一下仿真的结果:

  其中第一幅图是使用Vivado的仿真(应该是功能仿真)所测试的结果,可以看到,基本没有奇怪的感觉,下图是使用非功能仿真的测试结果,可以看到有个小尖峰,看到这里基本可以理解信号发生变化的先后顺序了,由于iic_clk的上升沿,iic_scl会首先向低电平反转,这两者是同时的。由于always块是iic_clk的上升沿触发,在这个always块里,iic_en被置为0,iic_scl又迅速被拉高,这发生在iic_clk上升沿之后,因此产生了一个小尖峰
  这里是大佬给出的建议:只要产生尖峰,就存在竞争冒险,造成亚稳态,程序是可以下载到FPGA,但是工作会不稳定,出现故障几率变大,而且故障是偶发不可预测的,工程大了后很难排查,所以在设计时就要避免。
  大佬推荐笔者去了解数字电路的竞争冒险怎么产生的,如何避免(笔者基本没有学过数电,所以实在不知道这些知识),笔者会在之后的博客中提及这个问题。

为什么要倍频

  只要是在iic_clk的上升沿失能iic_en信号,就必然会产生这个问题。 笔者自然而然产生了在iic_clk的下降沿失能iic_en的信号。其实即使笔者没有留意到上述所产生的问题,笔者依然需要在iic_clk的下降沿触发always块
  根据之前笔者提到的应答信号,如下图可知,笔者必须在应答期内SCL信号的的上升沿或下降沿进行读取,这样就遇到了一个很尴尬的问题:时钟频率不够

  为什么说时钟频率不够那,这是因为iic_sda转换为输入模式(就是转换成高阻态需要一个时钟周期,如上图框中1处iic_clk的下降沿(即驱动时钟iic_clk的上升沿),笔者需要在此处将iic_dir拉低以使SDA进入输入模式:

    assign  iic_sda = sda_dir ? sda_out : 1'bz  ;     //SDA数据输出或高阻

  这样就不得不在第二个上升沿(即上图中2处位置)对数据进行读取判断,可是这时应答期已经结束,已经来不及读取了。到这时笔者终于明白正点原子的历程为什么要倍频了,就是为了解决这个问题。

下降沿触发与多重驱动

  不过笔者想到这里,也在疑问:如果iic_clk上升沿不行,也可以使用下降沿啊。笔者已经打算一条路走到黑了,遂使用此思路将代码进行完善。笔者另外写了一个always模块专门处理下降沿触发的情况,这样一切问题都迎刃而解了,这里笔者将仿真波形图贴上:

  其中iic_sda蓝色部分表示iic_sda为高阻态(输入模式),笔者在读取判断完数据后,便将其重新置为输出低电平模式。美滋滋!!!
  可是好景不长,笔者在综合的时候出现critical warning,紧接着实现便出现error

[Synth 8-6859] multi-driven net on pin…

  直译过来就是我有麻烦了多重驱动(我不是!我没有!),在浏览了一部分文章之后笔者才理解到何为多重驱动,简单来说就是有一个reg类型或者wire类型的变量,你在多个always块里面对其进行赋值,那这样是不行的。而笔者产生这个问题的原因则是,在上述的iic_clk的上升沿always模块和下降沿always模块中,笔者对同一个信号都进行赋值
  可是这是同一个时钟啊,那么我干脆写道同一个always块里,写成:

    always@(posedge iic_clk or negedge iic_clk)

  笔者早就想那么干了!!那么这样做肯定是不行的,笔者在某一篇文章中找到很好的解释:

https://blog.csdn.net/Reborn_Lee/article/details/90237172

  这里把大佬的链接供出来,希望对这个感兴趣的读者可以认真浏览一下,文中引用了Xilinx官方的回复,笔者觉得受益匪浅,这里把其中的部分内容贴出来,并进行适当的翻译:

You have to always remember that you are using Register Transfer Language (RTL) to have the synthesis infer hardware.

  您必须始终记住,您正在使用 寄存器传输语言(RTL) 来进行综合推断生成硬件。

Verilog HDL can do pretty much anything - the syntax of the always @(< whatever >) is very flexible. However, when you go to synthesize the design, the code has to map to an available piece of hardware on the FPGA.

  Verilog HDL语言几乎可以做任何事情,因为always @(任何信号)的语法非常灵活。但是,在进行综合设计时,代码的设计必须映射到FPGA上的可用硬件。

On the FPGA we have combinatorial logic (LUTs, MUXes, etc…). These are inferred a variety of ways, including the always @(*) construct.

  在FPGA中,我们有实现组合逻辑的器件( LUTs(查找表)、MUXes(多路复用器) 等等),可以通过多种方式进行推断(硬件),包括always @(*)构造。

In addition we have flip-flops. A flip-flop on an FPGA (and pretty much anywhere else) is a device that has one clock and is sensitive to only one edge of that clock. So, the synthesis tool can only map to this device when the sensitivity list is always @(posedge clk). We have variants of the flip-flop that can do asynchronous preset/clear, so will map to always @(posedge clk or <posedge/negedge> rst), but that’s it.

  此外,我们还有flip-flops(触发器)。FPGA上的触发器(以及其他任何地方的触发器)是一个只有一个时钟的设备,并且只对该时钟的一个边沿敏感。因此,合成工具只能将灵敏度列表为always@(posedge clk)的模块进行映射。我们有各种各样的触发器,可以做异步预设/清零,所以会和always@(posedge clk or<posedge/negedge> rst)构成映射,但仅此而已。

There is no real hardware device that can do the equivalent of what you are describing - always @(posedge clk or negedge clk).

  没有真正的硬件设备,可以做被你这样描述的事情——always@(posedge clk or negedge clk)。

The only exception (sort of) are the IDDR and ODDR, and these need to be instantiated - they cannot be inferred from an HDL description.

  唯一的例外是IDDR和ODDR,它们需要(从ip核中)实例化—它们不能从HDL描述中被推断出来。

So, no, this is not synthesizable Verilog.

What exactly do you want to do? You want to clock your design at twice the rate of the incoming clock? If so, then use an MMCM to double your clock, and use that new high speed clock to clock your logic…

  所以,这些Verilog代码是不可被综合(这里指上述多时钟触发的always块)

you have to realize that a whole bunch of different things are written in Verilog

  • synthesizable RTL - this must use only the synthesizable portion of Verilog

  • testbench code - this can use the entier Verilog HDL language

  • models for library cells (see below)

  您必须认识到,Verilog可以编写很多不同的事物:

  • 可综合的RTL级Verilog代码: 这些必须是Verilog代码中可被综合的语句构成
  • 仿真代码: 仿真代码可以使用全部的Verilog语句
  • 库单元

If you are seeing something like always @(posedge clk or negedge clk) it is pretty much guaranteed that it is going to be in one of the last two (testbench code or library cell models) - these are not synthesizable and hence can use the entire Verilog language.

  如果你看到类似always@(posedge clk或negedge clk)这样的语句,那么几乎可以肯定它只会出现在最后两种情况 (testbench code or library cell models) 中的一个里面——而这两种Verilog编写的代码都是不可综合的,因此可以使用全部的Verilog语句。

So, what is a library cell model… When we synthesize an RTL design, we end up with a netlist of Xilinx primitive devices. These primitive devices are really the set of transistors that physically exist on the Xilinx die. However, for simulation purposes, we must have simulation models for these cells.

  那么什么是library cell model(库单元模块) 呢,当我们综合一个RTL设计时,我们 最终得到一个Xilinx原始器件的netlist(网表)。这些原始的器件实际上是在Xilinx芯片上,物理上实际存在的一系列晶体管。 然而,出于仿真的目的,我们必须有这些cell的仿真模块。(译者注:这里的cell应意指这些原始器件,这些原始器件就是之前所说的查找表、触发器等等)

For some cells (like the LUTs), these are pretty simple - the simulation model of the LUT has to perform the same Boolean operations that will end up happening in the LUT on the die (which is a purely digital thing).

  对于某些cell(比如LUT),这些很好理解——LUT的仿真模块必须执行这样一种布尔操作,即与最终将在晶圆体上的LUT中执行的,相同的操作。(译者注:文中的die笔者查阅了部分资料,称为晶圆体,与芯片的构造有关。文中想要表达的意思就是说我们所编写的LUT仿真模块,必须与实际芯片中物理存在的LUT执行相同的操作。

For other cells, like the GTX, the silicon is very highly complex sets of analog and digital functionality. To simulate this, Xilinx provides a GTX simulation model that (at least grossly) describes the functionality of the GTX from a digital point of view - things like the PLLs and clock recovery and other stuff is abstracted so that the result mimics the digital functionality of the underlying silicon. In this model, again, the entire Verilog HDL language can be (and is used) to describe the functionality. However, this description is not (and doesn’t need to be/shouldn’t be) synthesizable…
  对于一些其他的cell,比如GTX(高速串行模块),硅片是非常复杂的模拟和数字功能的集合。为了仿真GTX,Xilinx也提供了一个GTX模块,该模块(至少大致上)从数字的角度描述了GTX的功能——像锁相环和时钟恢复以及其他一些抽象的东西,以便仿真底层硅片的数字功能。同样,在这个模块中,全部的Verilog HDL语法可以(并且也被用于)描述功能。 然而,这个描述(即Verilog语言)不是(并不需要/也不应该是)可以被综合的。

IIC多时钟域数据同步

  这里笔者想要插一段题外话,笔者在微信公众号“FPGA之家”中发现了一篇文章,笔者认为在这里很适合这个问题的讨论。笔者在上文中提到,正点原子的历程留出了一个时钟接口,那么怎么用这个时钟接口那,只要我们使用频率不高于系统时钟的时钟来驱动iic模块,都会面对这个问题。这个问题的表现在于如果我们用系统时钟给iic模块发送一个周期的开始信号,模块很难接收到这个开始信号。如果我们在外部也只用iic提供的时钟接口,那么就会整体的时钟频率都会被拉低。如果在外部即使用iic提供的时钟接口,也使用系统时钟,就很容易产生我们上面所说的多重驱动的问题。
  笔者在这篇文章中找到了答案。对于像我们这种快时钟域向慢时钟域传递一个信号的情况,可以采用如下模块:

module fast2low(
    input   clk,
    input   asyn_in,
    output  syn_out
)
//clk:是慢时钟域时钟
//asyn_in:快时钟域想要发出的信号
//syn_out:发送给慢时钟域的信号
reg q1,q2,q3;
alwasy@(posedge asyn_in or posedge q3)
    if(q3)
        q1 <= 0;
    else
        q1 <= 1;

always@(posedge clk)
        q2 <= q1;

alwas@(posedge clk)
        q3 <= q2;

assign syn_out = !q2&q3; 
endmodule

  这里也同样附上仿真的示意图,结合图示可能更好理解一些:

  文章中说这种方式并不适合多个数据的传输,并提供了一些其他的方法,有兴趣可以看一下,这里笔者也附上FPGA之家的微信公众号的微信号:zhuyandz(主要是文章连接太长了。。。)

IIC驱动模块(iic_driver)最终实现

  这也不行,那也不行,可是笔者就是不想倍频怎么办,笔者甚至都不想用SCL的时钟,能不能直接用系统时钟呢(嘛,其实这也可以理解为最大的倍频)!!答案是肯定的,笔者在这篇文章中找到了:

https://www.cnblogs.com/liujinggang/p/9656358.html

  其思路概括起来是这样的:我们既然要使用高频的系统时钟,我们就要在茫茫的时钟周期里找到,哪一个幸运的上升沿我们要读取数据,哪一个幸运的上升沿我们要改变数据,只要找到这两个标志性的上升沿,岂不是一切问题都解决了。那么怎么才能找到这两个标志的上升沿呢,设计两个标志性的信号拉高不就可以了!!
  注意,下述所有代码复制到一个verilog文件中并在最后附上endmodule即可使用,这里首先给出输入输出端口的定义和一部分时钟

module iic_driver #(
    parameter SLAVE_ADDR = 7'b101_0000,
    parameter SYS_FRE = 50_000_000, //系统时钟
    parameter IIC_FRE = 100_000     //iic速度为 100kb/s
    )
    (
    input   rst_n,
    input   sys_clk,                //系统时钟      
    //interface
    input               iic_start,  //开始标志
    input       [15:0]  iic_addr,   //数据地址
    input       [ 7:0]  iic_data_w, //要写入的数据
    input               iic_control,//读写控制(1:写 0:读)
    input               iic_addr_byte,//地址位数(1:16bit 0:8bit)
    output  reg         iic_done,   //完成标志
    output  reg [ 7:0]  iic_data_r, //读出的数据
    output  reg         iic_ack,    //是否应答
    output  reg         iic_scl,
    inout               iic_sda
    );

    localparam clk_cnt  = SYS_FRE/(IIC_FRE*2);//时钟
     
    reg [ 7:0]  iic_data_wr_s;//要写入的数据暂存
    reg [ 7:0]  iic_data_rd_s;//读取的数据暂存
    reg [15:0]  iic_addr_s;   //数据地址储存
    reg [ 7:0]  cnt       ;   //发送的位数计数

    /******************/
    reg [15:0]  scl_cnt;
    reg [15:0]  rd_cnt ;
    reg [15:0]  wr_cnt ;
    
    reg flag_rw ;//寄存读写标志
    reg bit_ctrl;//地址长度位寄存
    /***iic时钟部分****/
    reg iic_rd_clk,
        iic_wr_clk,
        iic_en,     //scl驱动使能
        iic_op;     //iic 使能
    reg rd_flag;
    reg wr_flag;


    /******三段式状态机部分*****/
    localparam  st_idle   = 8'd0,//空闲状态
                st_sladdr = 8'd1,//发送器件地址
                st_addrh8 = 8'd2,//发送高八位
                st_addrl8 = 8'd3,//发送低八位
                st_data_wr= 8'd4,//写数据
                st_addr_wr= 8'd5,//读数据前写地址
                st_data_rd= 8'd6,//写完地址读数据
                st_end    = 8'd7;//结束操作 
    reg st_done;          //标志这一状态结束
    reg [8:0] next_state; //下一个状态
    reg [8:0] cur_state;
    
    
    /***sda 控制 ***/
    reg         sda_dir;
    reg         sda_out; //SDA输出信号
    wire        sda_in ; //SDA输入信号
    
    assign  iic_sda = sda_dir ? sda_out : 1'bz  ;     //SDA数据输出或高阻
    assign  sda_in  = iic_sda ;                          //SDA数据输入 

  这里是笔者基于此思路设计的信号时序图:

  图中标识即为系统时钟计数clk_cnt次的长度,iic_clk即为sys_clk是系统时钟,rd_clk是用来标识在这个位置读取sda,wr_clk是用来标识在这个位置修改sda的状态,这里在图中可以看到,笔者希望SDA在SCL的低电平中间被修改,在SCL的高电平中间读取数据。通过控制计数的不同控制信号产生的时间。
  iic_en则是用来控制SCL什么时候开始产生翻转,iic_op是用来控制什么时候停止rd_clk、wr_clk的产生。贴出这三个信号产生的代码:

/************产生scl时钟*********/
    always@(posedge sys_clk or negedge rst_n)
    begin
        if(!rst_n)
        begin
            scl_cnt     <= 16'd0;
            iic_scl     <= 1'd1;    //持续高电平
        end
        else if(scl_cnt == clk_cnt-1)
        begin
            scl_cnt <= 1'd0;
            iic_scl <= ~ iic_scl;
        end
        else if(iic_en)
            scl_cnt <= scl_cnt + 1'd1; 
        else
            scl_cnt <= 1'd0; 
    end
    /**********************************/
    /************产生写入时钟***********/
    always@(posedge sys_clk or negedge rst_n)
    begin
        if(!rst_n)
        begin
            wr_flag     <= 1'd0;
            iic_wr_clk  <= 1'd0;
            wr_cnt      <= 16'd0;
        end
        else
            if(iic_op)
            begin
                iic_wr_clk  <= 1'd0;
                if(!wr_flag&&wr_cnt == clk_cnt*3/2-1)//可以看时序图想明白
                begin
                    wr_cnt      <= 1'd0;
                    iic_wr_clk  <= 1'd1;
                    wr_flag     <= 1'd1;
                end
                else if(wr_flag)
                begin
                    if(wr_cnt == clk_cnt*2-1)
                    begin
                        wr_cnt      <= 1'd0;
                        iic_wr_clk  <= 1'd1;
                    end
                    else
                        wr_cnt  <=  wr_cnt + 1'd1;
                end
                else   
                    wr_cnt  <=  wr_cnt + 1'd1;
            end
            else
            begin
                wr_flag     <= 1'd0;
                iic_wr_clk  <= 1'd0;
                wr_cnt      <= 16'd0;
            end    
    end
    /*********************************/
    /********产生读取时钟*************/
    always@(posedge sys_clk or negedge rst_n)
    begin
        if(!rst_n)
        begin
            rd_flag     <= 1'd0;
            iic_rd_clk  <= 1'd0;
            rd_cnt      <= 16'd0;
        end
        else
            if(iic_op)
            begin
                iic_rd_clk  <= 1'd0;
                if(!rd_flag&&rd_cnt == clk_cnt/2*5-1)//可以看时序图想明白
                begin
                    rd_cnt      <= 1'd0;
                    iic_rd_clk  <= 1'd1;
                    rd_flag     <= 1'd1;
                end
                else if(rd_flag)
                begin
                    if(rd_cnt == clk_cnt*2-1)
                    begin
                        rd_cnt      <= 1'd0;
                        iic_rd_clk  <= 1'd1;
                    end
                    else
                        rd_cnt  <=  rd_cnt + 1'd1;
                end
                else   
                    rd_cnt  <=  rd_cnt + 1'd1;
            end
            else
            begin
                rd_flag     <= 1'd0;
                iic_rd_clk  <= 1'd0;
                rd_cnt      <= 16'd0;
            end    
    end

  代码中关于两个标记信号的计数参数,均可以在上面那个时序图,根据clk_cnt的长度推出笔者不才,写的有些长,之后就是三段状态机的部分了,这一部分代码写得很长,但是思路很容易理解,只需要在该输出的时候输出,该输入的时候输入即可。

//第一段,状态切换
always@(posedge sys_clk or negedge rst_n)
    begin
        if(!rst_n)
            cur_state <= st_idle;
        else    
            cur_state <= next_state;
    end

//第二段,状态转移
always@(*)
    begin
        next_state = 8'bx;     
        case (cur_state)
            st_idle:
            begin
                if(st_done)
                    next_state = st_sladdr;//传输地址
                else
                    next_state = st_idle;
            end
            st_sladdr:
            begin
                if (st_done) 
                    if(bit_ctrl)   //地址位数
                        next_state = st_addrh8;
                    else
                        next_state = st_addrl8;
                else
                    next_state = st_sladdr;
            end
            st_addrh8:
            begin
                if(st_done)
                    next_state = st_addrl8;//传输低八位
                else
                    next_state = st_addrh8;  
            end         
            st_addrl8:
            begin
                if(st_done)
                    if (flag_rw == 1)//写数据 
                        next_state = st_data_wr;
                    else //读数据
                        next_state = st_addr_wr;
                else
                    next_state = st_addrl8;
            end
            st_data_wr: //写数据
                if(st_done)
                    next_state = st_end;
                else
                    next_state = st_data_wr;
            st_addr_wr://写地址
                if (st_done) 
                    next_state = st_data_rd;
                else
                    next_state = st_addr_wr;
            st_data_rd:
                if(st_done)
                    next_state = st_end;
                else    
                    next_state = st_data_rd;
            st_end:
                if (st_done) //用来产生 操作结束信号
                    next_state = st_idle;
                else    
                    next_state = st_end;

            default: next_state = st_idle;

        endcase
    end

//第三段,描述输出
always@(posedge sys_clk or negedge rst_n)//用来产生输出
    begin
        if(!rst_n)
        begin
            iic_data_wr_s   <= 8'd0;
            sda_out         <= 1'd1;//sda初始输出高
            sda_dir         <= 1'd1;
            st_done         <= 1'b0;
            flag_rw         <= 1'bx;
            iic_done        <= 1'd0;
            iic_ack         <= 1'd0;
            cnt             <= 8'd0;//计数器0
            iic_en          <= 1'd0;
            iic_op          <= 1'd0;//iic开始运行
        end
        else    
        begin
            st_done <= 1'd0;
            case (next_state)
                st_idle:
                begin
                    if(iic_start)
                    begin
                        flag_rw         <= iic_control;
                        iic_addr_s      <= iic_addr; 
                        iic_data_wr_s   <= iic_data_w;
                        bit_ctrl        <= iic_addr_byte;
                        iic_ack         <= 1'd0;
                        sda_out         <= 1'd0;//开始通信
                        iic_en          <= 1'd1;//时钟使能
                        iic_op          <= 1'd1;//iic开始运行
                        st_done         <= 1'd1;//完成
                    end
                    else
                    begin
                        sda_out         <= 1'b1;
                        iic_done        <= 1'b0;
                        cnt             <= 8'd0;
                        sda_dir         <= 1'd1;
                        iic_data_rd_s   <= 8'd0;
                        iic_data_r      <= 8'd0;
                        bit_ctrl        <= 1'd0;     
                    end
                end
                st_sladdr:
                begin
                    if(iic_wr_clk)
                    begin
                        sda_dir <=  1'd1;
                        cnt     <=  cnt + 1'd1;
                        case (cnt)
                            8'd7: 
                            begin
                                sda_out <= 1'd0;//随机读写均需先写
                            end
                            8'd8:
                            begin
                                sda_dir <= 1'd0;//改变方向 开始读取
                                sda_out <= 1'd1;
                            end
                            default: 
                                sda_out <= SLAVE_ADDR[6-cnt];
                        endcase 
                    end
                    else if(iic_rd_clk && cnt == 8'd9)
                    begin
                        st_done <= 1'd1;
                        cnt     <= 8'd0;
                        if(sda_in)
                            iic_ack <= 1;
                        else
                            iic_ack <= iic_ack;
                    end
                    else
                        cnt <= cnt;
                end
                st_addrh8:
                begin
                    if(iic_wr_clk)
                    begin
                        sda_dir <= 1'd1;
                        cnt     <= cnt + 1'd1;  
                        case (cnt)
                            8'd8:
                            begin 
                                sda_dir <= 1'd0;
                                sda_out <= 1'd1;
                            end
                            default:
                                sda_out <= iic_addr_s[15-cnt]; 
                        endcase 
                    end
                    else if(iic_rd_clk && cnt ==8'd9)
                        begin
                            cnt     <= 8'd0;
                            st_done <= 1'd1;
                            if(sda_in)
                                iic_ack <= 1;
                            else
                                iic_ack <= iic_ack;
                        end
                else
                    cnt <= cnt;
                end 
                st_addrl8:
                begin
                    if (iic_wr_clk) 
                    begin
                        sda_dir <= 1'd1;
                        cnt     <= cnt + 1'd1;  
                        case(cnt)
                            8'd8:
                            begin 
                                sda_dir <= 1'd0;
                                sda_out <= 1'd1;
                            end
                            default: 
                                sda_out <= iic_addr_s[7-cnt];
                        endcase
                    end
                    else if(iic_rd_clk && cnt == 9)
                        begin
                            cnt     <= 8'd0;
                            st_done <= 1'd1;
                            if(sda_in)
                                iic_ack <= 1;
                            else
                                iic_ack <= iic_ack;
                        end
                    else
                        cnt <= cnt;
                end
                st_data_wr:
                begin
                    if (iic_wr_clk) 
                    begin
                        sda_dir <= 1'd1;
                        cnt     <= cnt + 1'd1;
                        case (cnt)
                            8'd8: 
                            begin 
                                sda_dir <= 1'd0;
                                sda_out <= 1'd1;
                            end
                            default:
                                sda_out <= iic_data_wr_s[7-cnt];
                        endcase 
                    end
                    else if(iic_rd_clk && cnt == 9)
                        begin
                            st_done <= 1'd1;
                            cnt     <= 8'd0;
                            if(sda_in)
                                iic_ack <= 1;
                            else
                                iic_ack <= iic_ack;
                        end
                    else
                        cnt <= cnt;    
                end
                st_addr_wr:
                begin
                    if(iic_rd_clk)
                    begin
                        if(cnt == 8'd1)
                            sda_out <= 1'd0;    //先发送一个起始位
                        else if(cnt == 8'hA)
                        begin
                            cnt     <= 8'd0;
                            st_done <= 1'd1;
                            if(sda_in)
                                iic_ack <= 1;
                            else
                                iic_ack <= iic_ack;
                        end
                        else
                            cnt <= cnt;
                    end
                    else if (iic_wr_clk) 
                    begin
                        cnt <= cnt + 1'd1;
                        case (cnt)
                            8'd0:               //先发送一个开始位
                            begin
                                sda_dir <= 1'd1; //起始位
                                sda_out <= 1'd1;     
                            end
                            8'd8:
                            begin
                                sda_out <= 1'd1;//读操作   
                            end
                            8'd9:
                            begin
                                sda_dir <= 1'd0;
                                sda_out <= 1'd1;    
                            end 
                            default:
                                sda_out <= SLAVE_ADDR[7-cnt]; 
                        endcase             
                    end       
                end
                st_data_rd:
                begin
                    if(iic_rd_clk)
                    begin
                        cnt <= cnt + 1'd1;
                        if(cnt <= 7)
                            iic_data_rd_s[7-cnt] <= sda_in;
                        else    
                        begin if(cnt == 8)
                            st_done     <= 1'd1;
                            iic_data_r  <=iic_data_rd_s;
                            cnt     <= 8'd0;
                        end
                    end
                    else if (iic_wr_clk) //先发送一个开始位
                    begin
                        if(cnt == 0)
                        begin
                            sda_dir <= 1'd0; 
                            sda_out <= 1'd1;    
                        end
                        else if(cnt == 8)
                        begin
                            sda_dir <= 1'd1; //非应答
                            sda_out <= 1'd1;
                        end
                        else
                        begin
                            sda_dir <= sda_dir; 
                            sda_out <= sda_out;     
                        end
                    end   
                end
                st_end:
                    begin
                    if (iic_wr_clk) 
                    begin
                        cnt <= cnt + 1'd1;  
                        if(cnt == 0)
                        begin
                            sda_dir <= 1; 
                            sda_out <= 1'd0; //停止信号    
                        end
                        else if(cnt == 1)
                        begin
                            st_done <= 1'd1;
                            iic_done<= 1'd1;
                            iic_op  <= 1'd0;
                            cnt     <= 1'd0;
                        end
                    end
                    else if(iic_rd_clk)
                    begin
                        if(cnt == 1)
                        begin
                            sda_out <= 1'd1; //停止信号
                            iic_en  <= 1'd0; //时钟失能 
                        end
                    end
                    else
                        cnt <= cnt;
                end
            endcase
        end
    end

五、仿真与测试

仿真测试

  笔者在器件地址为1010000的EEPROM模块上面写入256个数据,写入与地址数值相同的数据(即在地址0x01写入0x01、地址0x02写入0x02以此类推),首先展示写操作的仿真图(这里展示写入0x15:0001 0101):

  然后接下来是读操作的仿真图(这里读的地址也是0x15:0001 0101):

  注意应答位和非应答位的区别。最后笔者便在开发板上进行进一步的采样测试,使用ila(IP核)可以对FPGA中的数据进行采样,这里是捕捉到的写操作地址为0x15的一部分

  可以看到,除了图中画框的位置,后面还有两处EEPROM也给予了ACK响应,那么后面我们同样读0x15这个地址的数据,看是否数据也是0x15,如此进行验证,是否写入成功:

  这里笔者已经尽可能截取了完整的一部分时序图,可以清楚的看到每个时钟周期数据的改变情况,整体来说还是比较顺利的。

六、总结

  整个过程的确花费了笔者很长一段时间,事情的起因只是因为不想用四倍频的时钟,没想到折腾出来那么多的事情,也的确学到了不少知识。总算是对Verilog有那么一点点的感觉了(有内味了!)。
  之后笔者会写关于SCCB协议的博客,以期在FPGA上驱动ov7725(摄像头才是梦想),笔者发现双目摄像头历程大多都是使用PS+PL的处理方式进行完成,SCCB也是采用ARM部分进行控制,不过FPGA也未尝不可以做这件事情,尽请期待吧!!

发布了1 篇原创文章 · 获赞 4 · 访问量 208

猜你喜欢

转载自blog.csdn.net/qq_38097063/article/details/104655226