完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本帖最后由 ysssssssy 于 2020-6-4 21:42 编辑 通信在电子设备中非常广泛,没有通信的电子设备少之又少,通信方式也是比较丰富,常见的有线通信有以太网,USB,UART,IIC,SPI,亦或是ARM芯片内部的总线通信如AMBA也是一种有线通信,但其中简单方便又常用的就是UART,SPI和IIC了,但前二者在FPGA中都较为容易实现因为它们的发送和接收是独立分开的,而IIC最大的难题就在于数据线又作输入又作输出,这使得设计起来存在一定的麻烦。一提到IIC通信,与之经典的案例就是与EEPROM的通信了,这里使用24LC64作为介绍,注意是LC不是24C64,前者的最大速率400KHz,后者的可以到1MHz。 首先先从官方器件手册查看其基本通讯时序: 涉及到基本通信要素有通信的开始,通信的进行以及通信的结束,其中通信的开始也就是START信号的产生要求在通信时钟SCLK的高电平期间将数据线拉低(数据线的电平状态由高电平变为低电平),结束信号STOP的产生要求在SCLK的高电平期间将数据线拉高(数据线的电平状态由低电平变为高电平),这两个信号的变化时刻都要求是在时钟的高电平期间,这个原因也决定了我们要传输的数据只能在低电平期间发生改变,在高电平期间保持稳定供采样,如果数据在高电平期间不稳定或发生了改变会被误判为起始信号或者是结束信号导致通信失败。这其中发出起始信号的一般称为主设备(主机),响应主设备的称为从设备(从机)。 传输数据时下图以传输101为例。
可以看出要传输的数据须在时钟的低电平时刻发生变化,且在下一个高电平整个期间保持稳定,这样传输的数据就可以被主/从设备稳定的采样到。 除了这三类基本信号,还剩下一类信号也就是应答或者不应答信号,它们的作用就是用来响应,比如主设备发出一个从设备地址寻找该设备,那么主机如何知道是否有这么个设备或者这个设备的状态是否是忙还是不忙,就靠这个响应信号,如果主设备接收到了一个应答信号那么就说明有这个设备或者这个设备处于空闲状态可以与主机进行通信,如果没有应答信号则认为没有这个或者这个设备正忙中。 应答信号就是在一次数据传输结束后,将数据线拉低,表示应答信号,不应答信号就是在一次数据传输结束后将数据线拉高,表示不应答。 IICC传输数据时以字节为单位,每次一个字节,即8个bit位,IIC通信并不是一对一的,是多对多的,所有要通信的设备可以挂在IIC总线上,挂载在总线上的每一个设备都有一个独一无二的地址,通过这个地址就可以来寻找这个设备。一般的8位地址减去一个广播地址还剩7位对多可以同时挂载127个设备,在IIC空闲时这些设备间没有主从之分,发起通讯要求的那个设备称为主设备,响应通讯的那个设备称为从设备。 介绍了IIC的基本通信操作后接下来就来看看24LC64的基本通信时序,24LC64的读写有三类,一是字节写也就是每次通信向24LC64内部指定地址写入一个字节的数据,二是页写每次一页一页的写,24LC64一页有32个字节,也就是每次写入32个字节,三是保护写这个方式不常用我自己也不是很清楚,读的方式也有三种,一是当前地址读,就是直接读取当前地址的一个字节的数据,二是随机读取 ,这个随机读并不是说随机读取某个地址空间内的一个字节的数据,它跟字节写的道理一样,也是靠发送地址读取24LC64内部指定地址的数据,三是连续读,读取内部指定地址为起始地址,读完一次后只要发送ACK应答信号就继续读取,直到发送不应答信号则结束读取。 官方手册介绍如下: 在本次实验中,我以最常用的字节写和随机读作为读写方式进行试验。 下面进入编程正题,首先先做驱动模块分析,画一个模块驱动图,分析出需要哪些输入信号以及输出信号。 通过模块的分析得出需要的基本输入信号有系统输入时钟CLK,系统复位信号RST_N,读写控制信号R_W,要写入的八位数据WRITE_DATA,以及要读写24LC64的内部空间地址地址信号ADDR(这里24LC64本身的地址是固定的就不需要做输入了,直接在程序中给出即可),要输出的信号有用与IIC通信用的时钟信号SCLK以及读出的数据READ_DATA,还剩下一个最为特殊的信号也就是双向IOIIC的数据线SDA,即作输入也作输出。 (24LC64器件存储容量为64KBit=8KB=2^13,所以表示内部地址的信号addr的宽度为13位,但是IIC在发送数据时是以一个字节为单位进行传输的,所以在传输内部地址时将分为两次经行传输,先传地址的高五位,再传地址的低八位,其中如上读写时序图一样,高5位地址前面有三个don't care 位一起组成一个字节经行传输的,也就是那三位传0或者传1都无所谓,在本程序中我将将地址的前三位拼接了三个0组成两个字节进行传输。)
上面就是我定义的端口列表,这里我还额外添加了一个读写完成信号done便于后面仿真或用于其他调试,这个信号不用也可以,或者可以将其换做IIC总线状态信号用于表示IIC总线处于空闲状态中或者忙中。 下面就该做状态分析了,便于写出状态机,但是在分析状态转移之前先来解决inout 类型该如何使用,inout类型定义好了,不用对她作类型描述,比如reg型或者是wire型,inout变量也不能直接用来赋值输出,要想使用它就先要知道双向端口的原理,双向端口又跟一个熟悉的电路有关,就是三态门,双向端口的正常使用是离不开三态门的,三态门正如它的名字一样有三个状态,也就比普通的信号多了一个状态,高阻态,加上高低电平状态共三个状态。 当EN有效即EN为高电平时电路导通,输出OUT等于输入的IN,当EN为低电平时电路不导通,OUT端呈高阻态,此时输入IN无效,也就是说此时去读OUT端的电平是不会受到输入端的影响的,也就是说此时OUT端可以作为输入来使用去读取OUT端的电平了。这里的OUT也就相当于IIC的SDA线了,所以IN端就可以接一个中间信号来用来输出,比如定义一个reg iic_sda_out,当EN=1时,iic_sda就可以输出即iic_sda = iic_sda_out,当EN = 0,让iic_sda成为高阻态即可用作输入,即iic_sda = 1'bz。故inout类型变量的使用方法便是:
当需要iic_sda作输出时只需要让iic_en变量为1,在给iic_sda_out赋想要输出的值即可,当想要使用iic_sda作为输入时,只需要令iic_en为0,然后去读iic_sda的电平即可。 但是需要注意三态门或者说时双向端口定义最好只定义在顶层文件中,顶层文件例化的子模块中最好不要定义双向端口,因为子模块的端口列表中定义了双向IO后,它的上一层文件至少有一个输入口和一个输出口连接到该双向口上,则发生两个内部输出单元连接到同一信号,往往在综合的时候会报错。考虑到在后面实际的板上验证时,会使用按键来控制数据的输入或者数据的读出,还需要使用ISSP等系统提供的IP来在线调试和探取数据,所以需要一个顶层来糅合在一起,故实际的IIC驱动模块中我取消了双向端口的定义,改成了一个SDA输入信号和一个SDA输出信号,以及再输出一个EN信号便于顶层的三态门调用,也就是说IIC模块中在需要输出时直接给SDA_OUT赋值即可并且将EN置为高电平表示输出,在需要输入时直接去读SDA_IN的数据并且将EN拉低是顶层的三态门呈高阻态,然后在顶层模块中定义一个双向IO IIC_SDA即可。
顶层模块:
顶层模块中inout端口的使用以及IIC模块的例化:
然后再来解决一些简单的问题,比如一次读写操作使能信号的产生及IIC时钟信号SCLK的产生:
由于后期我会将r_w信号替换为按键消抖模块的输出,我的按键消抖模块的输出是确认按键按下都会输出一个系统时钟的高脉冲用来表示按键按下过一次,所以不能直接用来控制IIC的全程读写,故用两个读写使能信号来进行控制。 (这个第三行那个if我怎么添加代码它就是要给我大写)
这里分频时钟的产生不必多说,CNT_MAX = 250,我的实验板的系统时钟为50MHz,对其进行250分频用于产生一个200KHz的时钟信号来作为IIC的时钟信号,其次我设定只有在传输使能期间产生时钟信号,没有传输使能时时钟信号和数据信号都保持高电平表示空闲状态。 根据前面的分析得知数据的变化需要在时钟的低电平期间且高电平期间要保持绝对的稳定以供相应的设备读取到稳定的数据,然后起始信号以及结束信号需要在时钟的高电平期间发生变化,所以这里既需要关注时钟的高电平时刻也需要关注时钟的低电平时刻。
这里定义了两个脉冲信号用来表示SCLK处于低电平中间时刻以及SCLK处于高电平的中间时刻,这样就方便数据的改变和数据的稳定读取亦或是响应或者非响应信号以及起始和结束信号的变化可以在理想时刻发生改变,仿真效果图如下: 可以看出SDA在高电平的一个中间时刻拉低表示了起始信号(前面较长一截的高电平状态是因为前面IIC处于空闲状态),后面传输数据时都是在低电平的中间时刻发生改变,整个高电平期间数据都保持的很稳定。 接下来就该进行状态转移分析了,分析出状态转移关系,写出状态机整个实验就算完成一大半了,起初我的思路也不是很清晰,就采取了一个比较蠢的方法,直接列出三个状态,空闲态,写数据态和读数据态,然后写数据态或读数据态根据24LC64的字节写时序图或者是随机读时序图,按照上面的顺序来一步一步的写也就是序列机的方式来写,虽然写起来显得较为蠢且代码也显得臃肿但它确实很顶用,直接一次就成了,下面对这种方式以字节写作为示例进行分析: 观察时序图得知,在写状态时第一个要做的就是等待时钟的高电平中间时刻把数据线拉低也就是产生一个下降沿,然后进入下一个状态即可。 接下来的7个状态就是发送24LC***的7位地址了,也就是在每一个低电平的中间时刻使数据线输出24LC64的地址,从高字节到低字节的顺序输出,实例代码如下:
0号状态就是发送起始信号,接下来的七个状态就是发送器件的地址,然后下一个第8状态就是发送读写控制位了,这里进行的写操作,根据时序图得知应该发送一个0。
这里发完读写控制位后下一个需要做的事情即使等待24LC64的应答信号来了,所以需要在下个时钟的下降沿释放对数据线的控制也就是让EN信号为低,让数据线作为输入。
然后再下个状态就是在上个时刻紧邻的高电平中间时刻去读SDA的电平状态,看是否有响应,这里就是之前说的我在子模块中IIC中取消了双向IO直接用一个输入和一个输出就行了,在上层模块例化它的时候再将输入和输出接在三态门上。
其实这里写的不完善,由于时间关系这里我写的是收到了应答信号就进入下一个状态,没有收到应答信号就一直在这里等待,实际中的若是要发出应答信号的设备不存在或者是正处于忙碌中的话程序就会在这里锁死,要完善的话可以在这里添加一个计数器,在一个限定时间内等待应答信号,如果超过这个时间就可以终止此次传输。 按照正常情况来,如果接收到了应答信号之后,下一个状态就是发送内部存储空间地址的高字节了,也是每等待一个低电平中间时刻发送一个Bit位的电平。
r_addr就是我用来存储地址的,即r_addr={3'b000,addr},,它的地址就是要寻址的13位地址,再在高三位拼接上三个0凑成两个字节。 发完高字节之后要进行的操作又跟前面一样了,还是释放数据线等待应答信号到来。
检测到应答信号后的下一个状态就是继续发送地址的低八位,也是每来一个低电平中间时刻输出一个Bit位。
然后又是释放数据线等待应答信号。
紧接着就是发送要写入的一个字节的数据了,虽然是发送要写入的数据,但操作其实跟前面发地址完全一样,都是发送数据。
发完之后便又是释放总线等待应答信号。
收到应答信号便是该发送停止位了,停止位的发送是在高电平的中间时刻将数据线的电平从低拉高也就是产生一个上升沿,根据前面状态分析,上一个状态是在高电平期间检测应答信号,所以须在紧邻的下一个低电平时刻拿回数据线的控制也就是使能输出,并在这个时刻提前将数据线拉低,这样就可以在下一个上升沿再将数据线拉高产生上升沿了。
然后下个状态就是等待上升沿到来将数据线拉高产生一个上升沿就是发送停止信号了。
至此整个字节写操作就算完成了,产生一个完成脉冲信号并且返回空闲态。 随机读的操作跟这个也差不多原理都一样,这个方式虽然看起来比较笨拙但是思路是比较清晰直接的,下面来对代码进行优化,深入状态分析。 对比两个读写时序,不难发现它们还是有很多共同点的,比如开始动作都是发送一个起始信号,然后紧接着又都是发送器件地址加写状态,然后又都是发送要寻址的空间地址,而且都有发送等待应答信号的状态,最后也都有发送停止信号的状态,意识是无论是读还是写这几个操作都是共同通用的,再看读操作时发送内部寻址地址后再次发送开始信号紧接着再发送的地址,此时最低位为1了,也就是真正的的读操作,这里我先不去区分发送器件地址的读写位,把读写位和器件地址捆在一起统称为发送器件地址状态,这样捆绑在一起之后细看不通用的状态就只有读数据状态和写数据状态以及一个额外的不应答状态了,下面就来画出状态转移图。 但这其中有一个最为麻烦的事情就是关于在应答信号后的状态转移,比如在发送完器件地址后,下一个状态便是进入等待应答信号状态了,那么应答响应后的下一个状态是什么,这里第一次进入应答状态下一个路径是清晰的,下一个状态当然是发送存储空间地址的高八位也就是 state <= 发送存储地址高八位,但是比如是第二次进入应答状态,在执行这条路径转移,那么整个状态转移就会在这里陷入死循环,所以还需提前知道下下个状态的转移。也就是说除了state本身,还需要定义一个next_state来提前保存下下个状态,然后在ACK状态里面直接让state <= next_state,这样程序就不会在等待应答信号的状态里迷路了。 状态转移图如上图所示。当读写都不使能时系统一直处于空闲状态,若二者有一个使能便开始运行,先进入发送起始信号状态,然后进入发送24LC器件的地址状态并提前标记下下个状态是发送内部存储空间地址高字节,然后在进入等待应答信号状态,然后再进入发送存储空间高字节状态并提前标记下下个状态是发送低字节,然后进入等待应答信号状态,然后再进入发送低字节状态,这里就需要根据本次使能状态是读使能还是写使能来提前标记下下个状态,这里就拿读使能举例,再发送存储空间低字节最后一位数据后判断使能标志是读使能然后标记下下个状态是发送起始信号并且这里需要做一个细节处理,也就是对24LC64器件地址重新赋值,因为发送完起始信号的下个状态是发送器地址而且读写位得置一表示读,然后再进入发送器件状态地址,并标记下下个状态时读数据状态,注意这里之前进来时也标记过下下个状态,前一次标记的下下个状态是发送器件的存储空间高字节,也就是说在这个状态里需要对标记下下个状态经行判定,并且这里的判定不能依据读写使能信号,因为即使是读,也要先去发送内部存储空间地址,这里的判定依据可以选择下下个状态的标记信号next_state,如果是第一次进来那么next_state是没被赋值过的,因为前面不需要标记下下次状态,如果是读路径的第二次进来,那么next_state的值此时应该是START,因为读路径进来到这个状态它的上次被标记的状态就是离开等待应答信号状态后进入发送起始信号状态,也就是如果 next_state == START 那么下下个状态就是 读数据状态,如果不等于那就说明是第一次进来,下下个状态便是发送内部存储空间地址高八位,紧接着进入等待应答信号状态,然后再进入读数据状态,接下来的路径都是单向的了,用不到下下个状态了,然后再进入不应答信号状态,最后再进入终止状态完成一次读数据操作,最后恢复空闲状态。 接下来就是这个状态机的设计实现了,这里我定义了九个状态,即空闲状态,发送起始信号状态,等待应答状态,不等待应答状态,发送器件地址状态,发送地址状态(这里我将发送高地址和地址合并为了一个状态),写数据状态和读数据状态,以及一个发送终止信号状态。
需要用到的相关信号变量的定义。
编码采用9位独热码的编码,这样可以使在进行状态转移时不跑飞(不发生竞争与冒险现象)。
这里复位和空闲状态做的事情差不多就一起贴出来,在空闲状态里不断去检测读写使能,如果有一个使能的话便进入发送起始信号状态,额外判断若果是写使能,就把要写入的值提前寄存起来,默人的读写控制位是写。
发送数据状态,如果在时钟的低电平中间时刻使能输出,并提前将数据线拉高,以方便高电平时刻到来将数据线拉低产生起始信号,这里如果是空闲状态进来数据线和时钟线都是高电平,直接将数据线拉低就行了,多了一步低电平时刻的判定是因为读操作会有第二次进入发送起始信号状态。
等待应答信号状态,如果检测到低电平就通过之前标记的状态进入下一个状态,如果没有检测到就会等待(这里还是跟之前的一样,没做容错处理,添加一个计数器防止进入死循环)
这是不响应状态,这里有点特殊,在后面读数据状态时一起说明。
这里便是发送器件地址状态了,在时钟的低电平时刻对SDA_OUT进行赋值输出,同时对发送的次数经行计数,在发送完八位数据后的下一个时钟的低电平的中间时刻释放IIC的数据线是双向IO变为输入状态以方便在下个等待等待应答信号状态直接去读取SDA的电平状态,并且还需要根据next_state的值判断是第一次进来还是第二次进来来确定下下个状态的转移。
发送寻址地址的状态因为考虑到实际分两次发送执行的操作都一样,我就将高低字节的发送结合在了一起,根据发送数据计数器的值在发送完高字节时跳出去一次,然后再回来继续发送低字节。 然后发送完低地址后的跳转就需要根据是读使能还是写使能来对next_state进行赋值,如果是读使能还需要更新一下器件地址,因为之前第一次发送器件地址已将寄存器内的值全部移出去了,而写下次发送的读写控制位还以该置1。
这里发送数据的跟前面发送器件地址的状态几乎一样,所以我觉得这里还可以优化一下,将发送器件地址和这里整合为一个状态,因为都是发送数据,只是发送的内容不一样。
读数据状态其实原理跟写数据都差不多,但读数据关注的是高电平的中间时刻而已,然后就是读八次SDA线的值,每次读的时候将值移位保存到一个寄存器变量中。这里不同的就在后面读完后的处理,也就是跟不应答信号有关,但是这里应答信号跟不应答信号有一个本质的区别,就是之前的应答信号全都是24LC64发给FPGA的,这里的不应答信号是FPGA发给24LC64的,所以在发送完数据的后一个低电平中间时刻就使能输出,并将数据线拉高表示不应答,然后再进入不应答信号状态,因为不应答信号状态是在下个高电平时刻24LC64去读取SDA的电平值,所以在不应答状态里什么都不需要做,只需等待那个高电平时刻到来然后进入下一个状态即可。
最后便是结束信号状态了,只需要提前在低电平时刻将数据线拉低,然后再在下一个高电平时刻将数据线拉高即可产生一个终止信号。 最后就是这个程序该如何去仿真,仿真有两个难点,一就是顶层例化的双向IO该如何去赋变量,二是整个通信器件涉及到发送应答信号,还有读数据的时候需要模拟EEPROM给SDA施加激励信号。
对于双向IO的仿真测试,在测试文件中定义一个wire型变量来表示双向IO,道理跟前面的一样了,也是模拟一个三态门,当测试文件中的tb_iic_en使能时iic_sda就输出,这个输出就相当于EEPROM的输出,对于FPGA来说就是输入状态,当测试文件中的en不使能时对于EEPROM来说就是高阻态也就相当于输入状态,对于FPGA来说就相当于输出状态。 其次对于应答信号就需要两个特殊的方法了,这里我采用的时wait()和@(negedge ......)来等待某个特殊时刻的到来。 本来都用wait()也可以但是因为我这里的低电平或高电平中间时刻只有一个时钟周期的高脉冲,对于这两个用wait()捕捉不到,于时便改用捕捉边沿的方式,wait()用来等待发送计数器到某个特定的值。
仿真测试时写入和读取的数据是1010_0101,地址是13'b0_0000_0000_0001。 写时序仿真图如下: 读时序仿真图如下: 更改程序,添加按键以及ISSP和signal Tap工具进行板上验证测试,程序中将读写地址默认设置为0_0000_0000_0001。 这是初始时的状态,下面修改源也就是write_data的值,给其赋值1010_0101。 按下写按键,通过Signal Tap观察实际时序。 可以看到实际波形与仿真相差无几,但是我的实验中每次应答信号最后那一点点会产生一个高脉冲,也就是图中黑圈标记的地方,不过还好是在时钟的低电平产生的,不会对通信产生影响,不知到是程序的原因,还是24LC64的问题。 在按下读按键,可以看到ISSP中探取的输入的数据值也发生了改变,跟写入的数据完全一致。 |
|
相关推荐
|
|
1396 浏览 1 评论
助力AIoT应用:在米尔FPGA开发板上实现Tiny YOLO V4
1051 浏览 0 评论
2470 浏览 1 评论
2173 浏览 0 评论
矩阵4x4个按键,如何把识别结果按编号01-16(十进制)显示在两个七段数码管上?
2439 浏览 0 评论
1906 浏览 50 评论
6019 浏览 113 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-24 08:38 , Processed in 0.645314 second(s), Total 41, Slave 30 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号