(九)零基础学懂 IIC 协议——EEPROM 读写测试
0 致读者
此篇为专栏 《Ryan的FPGA学习笔记》 的第九篇,记录我的学习 FPGA 的一些开发过程和心得感悟,刚接触 FPGA 的朋友们可以先去此专栏置顶 《FPGA零基础入门学习路线》来做最基础的扫盲。
本篇内容基于笔者实际开发过程和正点原子资料撰写,将会详细讲解此 FPGA 实验的全流程,诚挚地欢迎各位读者在评论区或者私信我交流!
EEPROM 是一种用于计算机系统的非易失性存储器,也常在嵌入式领域中作为数据的存储设备,在物联网及可穿戴设备等需要存储少量数据的场景中也有广泛应用。本章我们学习设计一个 IIC 读写控制器,实现 FPGA 对 EEPROM 存储器的读写控制实验。
本文的工程文件开源地址如下(基于ZYNQ7020,大家 clone 到本地就可以直接跑仿真,如果要上板请根据自己的开发板更改约束即可):
https://github.com/ChinaRyan666/FPGA-IIC-EEPROM
1 实验任务
本节的实验任务是先向 E2PROM 的存储器地址 0 至 255 分别写入数据 0~ 255;写完之后再读取存储器地址 0~ 255 中的数据,若读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁。
2 EEPROM 简介
EEPROM
(Electrically Erasable Progammable Read Only Memory) 是指带电可擦可编程只读存储器,是一种常用的非易失性存储器(掉电数据不丢失)。
E2PROM 有多种类型的产品,我使用的 ZYNQ 开发板上搭载的是 ATMEL 公司生产的 AT24C 系列的 AT24C64 这一型号,采用两线串行接口的双向数据传输协议—— I2C 协议实现读写操作。AT24C64 具有高可靠性,可对所存数据保存 100 年,并可多次擦写,擦写次数达一百万次。(其他的型号也类似,本文主要学习如何操作,大家根据自己开发板上的型号进行引脚调整即可)
一般而言,对于存储类型的芯片,我们比较关注其存储容量。以我使用的 AT24C64 芯片为例,存储容量为 64Kbit,内部分成 256 页,每页 32 字节,共有 8192 个字节,且其读写操作都是以字节为基本单位。可以把 AT24C64 看作一本书,那么这本书有 256 页,每页有 32 行,每行有 8 个字,总共有 256328=65536 个字,对应着 AT24C64 的 64*1024=65536 个 bit。
知道了 AT24C64 的存储容量,就知道了读写的空间大小,接下来就是通过 I2C 协议来对其进行读写操作。
3 IIC 协议简介
3.1 物理层
IIC
即 Inter-Integrated Circuit(集成电路总线),是由 Philips 半导体公司(现在的 NXP 半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。
IIC 总线由 数据线 SDA
和 时钟线 SCL
构成通信线路,既可用于发送数据,也可接收数据。在主控与被控 IC 之间可进行双向数据传送,数据的传输速率在标准模式下可达 100kbit/s,在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s,但目前大多 IIC 设备尚不支持高速模式。各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,具体可查器件手册)识别。我使用的 ZYNQ 的 IIC 总线物理拓扑结构如下图所示:
图中的 I2C_SCL
是串行时钟线,I2C_SDA
是串行数据线,由于 I2C
器件一般采用开漏结构与总线相连,所以 I2C_SCL
和 I2C_SDA
均需接上拉电阻,也正因此,当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低,即各器件的 SDA
及 SCL
都是“线与”关系。
I2C 总线支持多主和主从两种工作方式,通常工作在主从工作方式。在主从工作方式中,系统中只有一个主机,其它器件都是具有 I2C 总线的外围从机。在主从工作方式中,主机启动数据的发送(发出启动信号)并产生时钟信号,数据发送完成后,发出停止信号。
I2C 总线 结构虽然简单,使用两线传输,然而要实现器件间的通信,需要通过控制 SCL 和 SDA 的时序,使其满足 I2C 的总线传输协议,方可实现器件间的数据传输。那么 I2C 协议的时序是怎样的呢?
3.2 协议层
IIC 协议的整体时序图如下图所示:
仔细观察上图可知 IIC 协议的整体时序由四个部分构成,上图中分别使用①、②、③、④进行区分。
图中①: 总线空闲状态, 在 I2C 器件开始通信(传输数据)之前,串行时钟线 SCL 和串行数据线 SDA线由于上拉的原因处于高电平状态,此时 I2C 总线处于空闲状态。
图中②: 起始信号, 如果主机(此处指 FPGA)想开始传输数据,只需在 SCL 为高电平时将 SDA 线拉低,产生一个起始信号。
图中③: 数据传输状态, 主机可以向从机写数据,也可以读取从机输出的数据,数据的传输由双向数据线(SDA)完成。
图中④: 停止状态, 当数据传输完成,主机只需产生一个停止信号,告诉从机数据传输结束,停止信号的产生是在 SCL 为高电平时, SDA 从低电平跳变到高电平,从机检测到停止信号后,停止接收数据,并且 I2C 总线跳转回总线空闲状态。
了解到了整体时序之后,我们可能有疑问,数据是以什么样的格式传输的呢?满足怎样的时序要求呢?是在任何时候改变都可以吗?怎么知道从机有没有接收到数据呢?带着这些疑问,我们继续学习 I2C。
由于只有一根数据线进行数据的传输,如果不规定好传输规则肯定会导致信息错乱,如同在单条道路上驾驶,没有交通规则,再好的道路也会发生拥堵甚至更糟。采用两线结构的 I2C 虽然只有一根数据线,但由于还有一条时钟线,可以让数据线在时钟线的带领下有顺序的传送,就好像单条道路上的车辆在交警或信号指示灯的指示下有规则的通行。那么 I2C 遵循怎样的规则呢?
由上图可知,我们在起始信号之后,主机开始发送传输的数据;在串行时钟线 SCL 为低电平状态时,SDA 允许改变传输的数据位(1 为高电平,0 为低电平),在 SCL 为高电平状态时, SDA 要求保持稳定,相当于一个时钟周期传输 1bit 数据,经过 8 个时钟周期后,传输了 8bit 数据,即一个字节。
第 8 个时钟周期末,主机释放 SDA 以使从机应答,在第 9 个时钟周期,从机将 SDA 拉低以应答;如果第 9 个时钟周期, SCL 为高电平时, SDA 未被检测到为低电平,视为非应答,表明此次数据传输失败。第 9 个时钟周期末,从机释放 SDA 以使主机继续传输数据,如果主机发送停止信号,此次传输结束。我们要注意的是数据以 8bit 即一个字节为单位串行发出,其最先发送的是字节的最高位。
I2C 的时序部分已经基本介绍完了,但还有一个小问题,就是当多个 I2C 器件挂接在总线上时,怎样才能与我们想要传输数据的器件进行通信。这就涉及到了器件地址(也称从机地址,SLAVE ADDRESS)。
3.3 IIC 器件地址
每个 I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C 总线上,例如 E2PROM 器件,为了增加系统的 E2PROM 容量,可能需要多个 E2PROM。
器件可编程地址位的数量由它可使用的管脚决定,比如 E2PROM 器件一般会留下 3 个管脚用于可编程地址位。但有些 I2C 器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟 PCF8563 的器件地址为固定的 7’h51)。所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可。
对于本文提到的 AT24C64 而言,其器件地址为 1010 加 3 位的可编程地址, 3 位可编程地址由器件上的 3 个管脚 A2、 A1、 A0 的硬件连接决定。当硬件电路上分别将这三个管脚连接到 GND 或 VCC 时,就可以设置不同的可编程地址。
进行数据传输时,主机首先向总线上发出开始信号,对应开始位 S,然后按照从高到低的位序发送器件地址,一般为 7bit,第 8bit 位为读写控制位 R/W,该位为 0 时表示主机对从机进行写操作,当该位为 1 时表示主机对从机进行读操作,然后接收从机响应。对于 AT24C64 来说,其传输器件地址格式如下图所示。
3.4 存储地址
发送完第一个字节(7 位器件地址和1 位读写控制位)并收到从机正确的应答后就开始发送字地址(Word Address)。一般而言,每个兼容 I2C 协议的器件,内部总会有可供读写的寄存器或存储器,对于我们本次实验用到的 E2PROM 存储器,内部就是一系列顺序编址的存储单元。所以,当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入内容。
该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量 (2^ 8=256) 时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示,例如同是 E2PROM 存储器, AT24C02 的存储单元容量为 2Kbit=256Byte(一般 bit 缩写为 b, Byte缩写为 B),用一个字节地址即可寻址所有的存储单元,而 AT24C64 的存储单元容量为 64Kb=8KB,需要13 位(2^13=8KB)的地址位,而 I2C 又是以字节为单位进行传输的,所以需要用两个字节地址来寻址整个存储单元。
下面两张图分别为单字节字地址和双字节字地址器件的地址分布图,其中单字节字地址的器件是以存储容量为 2Kb 的 E2PROM 存储器 AT24C02 为例,双字节字地址的器件是以存储容量为 64Kb 的 E2PROM 存储器 AT24C64 为例, WA7 即字地址 Word Address 的第 7 位,以此类推,用 WA 是为了区别前面器件地址中的 A。
3.5 IIC 写时序
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位 R/W 位为 “0” 即写命令,从机就处于接收数据的状态,此时,主机就开始写数据了,写数据可分为单次写(对于 E2PROM 而言,称为字节写)和连续写(对于 E2PROM 而言,称为页写)。下面我们先分别介绍 E2PROM 的单次写与连续写。
E2PROM 单次写(字节写) 的时序如下图所示:
参照时序图,列出单次写(字节写) 操作流程如下:
(1) 主机产生并发送起始信号到从机, 并且将写控制命令发送给从机设备,读写控制位设置为低电平,表示对从机进行写数据操作。注意,写控制命令的发送是高位在前低位在后;
(2) 从机接收到写控制指令后,回传应答信号, 如果从机没有应答则会输出 I2C 通信错误信号,如果主机接收到应答信号,就开始字地址的写入。根据器件类型,我们需要先判断使用的器件是单字节地址还是双字节地址, 若为双字节地址,先向从机写入高 8 位地址,且高位在前低位在后;待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4);若为单字节地址跳转到步骤(3);
(3) 按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4) ;
(4)字地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;
(5) 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号, 单次写(字节写) 完成。
E2PROM 单次写(字节写)的时序介绍完成,接下来我们开始介绍 E2PROM 的连续写(页写)时序,E2PROM 连续写(页写) 的时序如下图所示:
参照时序图, 连续写(页写) 操作流程如下:
(1) 主机产生并发送起始信号到从机, 并且将写控制命令发送给从机设备,读写控制位设置为低电平,表示对从机进行写数据操作。注意,写控制命令的发送是高位在前低位在后;
(2) 从机接收到写控制指令后,回传应答信号, 如果从机非应答,则会输出 I2C 通信错误信号,如果主机接收到从机应答信号,就开始字地址的写入。 根据器件类型,我们需要先判断使用的器件是单字节地址还是双字节地址, 若为双字节地址,先向从机写入高 8 位地址,且高位在前低位在后;待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4);若为单字节地址跳转到步骤(3);
(3) 按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4) ;
(4)地址写入完成,主机接收到从机回传的应答信号后,开始第一个单字节数据的写入;
(5)数据写入完成,主机接收到从机回传应答信号后,开始下一个单字节数据的写入;
(6)直到所有数据写入完成,主机接收到从机回传应答信号后, 执行步骤(7) 。若数据未完成写入,跳回到步骤(5);
(7)主机向从机发送停止信号, 连续写(页写) 操作完成。
上面关于 E2PROM 的连续写(页写)时序也介绍完成,接下来我们总结下 E2PROM 单次写(字节写) 与 E2PROM 连续写(页写) 的区别:
对比上面两张时序图可知,两者的区别在于发送完一字节数据后,是发送结束信号还是继续发送下一字节数据,如果发送的是结束信号,就称为单次写,如果继续发送下一字节数据,就称为连续写。
AT24C64 的单次写(字节写)时序,对于字地址为单字节的 I2C 器件而言,在发送完字地址(对应上图的字地址高位),且从机应答后即可串行发送 8bit 数据。要注意的是, 所有 I2C 设备均支持单字节数据写入操作,但只有部分 I2C 设备支持页写操作,对于 AT24C64 的页写,是不能发送超过一页的单元容量的数据的,而 AT24C64 的一页的单元容量为 32Byte,当写完一页的最后一个单元时,地址指针指向该页的开头,如果再写入数据,就会覆盖该页的起始数据。
I2C 写时序介绍完毕后,接下来我们开始 I2C 读时序部分的介绍。
3.6 IIC 读时序
根据一次读操作读取数据量的多少,读操作可分为随机读操作和顺序读操作,详细讲解如下。
在发送控制命令时,如果读写控制位 R/W 位为 “1” 即读命令,主机就处于接收数据的状态,从机从该地址单元输出数据。读数据有三种方式:当前地址读、随机读和连续读。当前地址读是指在一次读或写操作后发起读操作。由于 I2C 器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一个字地址的数据。也就是说上次读或写操作的单元地址为 02 时,当前地址读的内容就是地址 03 处的单元数据,时序图如下图所示。
参照时序图, 当前地址读操作流程如下:
(1) 主机产生并发送起始信号到从机, 并且将读控制命令发送给从机设备,读写控制位设置为高电平,表示对从机进行读数据操作。注意,读控制命令的发送是高位在前低位在后;
(2) 从机接收到读控制指令后, 如果回传非应答信号,则会输出 I2C 通信错误信号,如果回传应答信号, 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(3) 数据接收完成后,主机产生一个时钟的高电平无应答信号;
(4) 主机向从机发送停止信号, 当前地址读操作完成。由于当前地址读极不方便读取任意的地址单元的数据,所以就有了随机读,随机读的时序有点复杂,见下图。
参照时序图, 随机地址读操作流程如下:
(1) 主机产生并发送起始信号到从机, 并且将写控制命令发送给从机设备,读写控制位设置为低电平,表示对从机进行写数据操作。注意,写控制命令的发送是高位在前低位在后;
(2) 从机接收到读控制指令后, 如果回传非应答信号,则会输出 I2C 通信错误信号,如果回传应答信号,主机接收到应答信号后开始字地址的写入。 根据器件类型,我们需要先判断使用的器件是单字节地址还是双字节地址, 若为双字节地址,先向从机写入高 8 位地址,且高位在前低位在后;待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4);若为单字节地址跳转到步骤(3);
(3) 按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4);
(4)字地址写入完成,主机接收到从机回传的应答信号后, 主机再次向从机发送一个起始信号;
(5) 主机向从机发送读控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(6) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(7) 数据接收完成后,主机产生一个时钟的高电平无应答信号;
(8) 主机向从机发送停止信号,单字节读操作完成。
至此随机地址读时序的操作步骤介绍完成,需要注意理解的是随机地址读在发送完器件地址和字地址后,竟然又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为 “0”,也就是写命令,第二次发送器件地址时后面的读写控制位为 “1”,也就是读。
为什么会有这样奇怪的操作呢?这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次 Dummy Write 也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置,等从机应答后,就可以以当前地址读的方式读数据了,如上图所示,随机地址读是没有发送数据的单次写操作和当前地址读操作的结合体。
随机地址读时序介绍完成,接下来我们我们开始介绍连续读。当前地址读和随机读都是一次读取一个字节,连续读是将当前地址读或随机读的主机非应答改成应答,表示继续读取数据,下图是在当前地址读下的连续读。
(1) 主机产生并发送起始信号到从机, 并且将读控制命令发送给从机设备,读写控制位设置为高电平,表示对从机进行读数据操作。注意,读控制命令的发送是高位在前低位在后;
(2) 从机接收到读控制指令后, 如果回传非应答信号,则会输出 I2C 通信错误信号,如果回传应答信号, 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(3) 数据接收完成后, 主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据传输完成,主机产生非应答信号回传给从机, 执行下一操作步骤;若数据传输未完成, 再次执行步骤(3);
(4) 主机向从机发送停止信号, 当前地址连续读时序操作完成。有当前地址连续读,当然也有随机地址连续读,随机地址连续读是在随机地址读时序的基础上,主机非应答改成应答,表示继续读取数据,下图是随机地址连续读。
参照时序图, 随机地址连续读操作流程如下:
(1) 主机产生并发送起始信号到从机, 并且将写控制命令发送给从机设备,读写控制位设置为低电平,表示对从机进行写数据操作。注意,写控制命令的发送是高位在前低位在后;
(2) 从机接收到读控制指令后, 如果回传非应答信号,则会输出 I2C 通信错误信号,如果回传应答信号,主机接收到应答信号后开始字地址的写入。 根据器件类型,我们需要先判断使用的器件是单字节地址还是双字节地址, 若为双字节地址,先向从机写入高 8 位地址,且高位在前低位在后;待接收到从机回传的应答信号, 再写入低 8 位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4) ;若为单字节地址跳转到步骤(3) ;
(3) 按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4) ;
(4)字地址写入完成,主机接收到从机回传的应答信号后, 主机再次向从机发送一个起始信号;
(5) 主机向从机发送读控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(6) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(7) 数据接收完成后, 主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据传输完成,主机产生非应答信号回传给从机, 执行下一操作步骤;若数据传输未完成, 再次执行步骤(7) ;
(8) 主机向从机发送停止信号, 随机地址连续读时序操作完成。
上面几种 IIC 读写方法简单总结如下图所示:
至此, I2C 协议就基本讲完了,本文我们主要采用单次写和随机读的方式进行 E2PROM 读写测试。
4 程序设计
4.1 整体模块设计
由实验任务可知,我们需要实现的功能是对开发板板载的 E2PROM 进行读写测试,并将读写测试的结果使用 LED 灯显示出来。对板载的 E2PROM 进行读写测试,我们是以 FPGA 为主机,板载的 E2PROM 为从机,FPGA 通过 IIC 协议对板载的 E2PROM 进行读写控制,所以在模块划分时我们需要一个 IIC 驱动模块和一个 E2PROM 读写模块,两个模块分别命名为 i2c_dri 与 e2prom_rw。
对于读写测试的结果是使用 LED 显示结果表示的,既读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁,所以我们还有要一个 E2PROM 读写测试结果显示模块,该模块我们命名为 rw_result_led。
最后,需要顶层模块将各子功能模块例化起来,连接个功能模块对应信号。本次实验需要使用系统时钟,系统复位在 FPGA 系统中也是必不可少的,当程序出现跑飞等异常情况时,可以使程序恢复至默认状态;由以上分析可知,本次实验需要 2 个输入的端口,分别为系统时钟、系统复位,输出为 LED 灯端口、 IIC 串行时钟信号 SCL 与 IIC 串行数据信号 SDA。 综上所述,实验工程整体框图, 如下图所示;
模块功能简介如下图所示:
为了让大家更容易理解,我简述一下本实验工程的具体流程。
e2prom 数据读写模块,生成 I2C 要写的数据、 I2C 器件字地址、 I2C 触发执行信号以及 I2C 读写控制信号传入 I2C 驱动模块(i2c_dri),I2C 驱动模块按照 I2C 协议的单次写操作,将数据写入 E2PROM 存储芯片。
数据全部写入完成后, I2C 驱动模块自 E2PROM 存储芯片读取数据,将读取到的数据回传给 e2prom 数据读写模块(e2prom_rw), e2prom 数据读写模块将读出的值与写入的值进行对比,然后将 I2C 读写操作完成信号与输出对比的结果信号传输给读写结果显示模块(rw_result_led),读写结果显示模块根据接收的信号来控制 LED 灯进行显示,如果 I2C 读写操作完成时,e2prom 数据读写模块的读出的值与写入的值对比结果不一致, 读写测试失败, 读写结果显示模块将控制 LED 灯闪烁,反之,对比结果一致, 读写测试成功,读写结果显示模块将控制 LED 灯保持常亮。
经过以上讲解,相信您对本实验工程的整体框架有了简单了解,接下来我对实验工程的各子功能模块分别进行详细讲解,帮助您更加深入理解实验工程。 四个模块,分别是 E2PROM 读写测试顶层模块(top_e2prom)、IIC 驱动模块(i2c_dri)、E2PROM 读写模块(e2prom_rw)和读写测试结果显示模块(rw_result_led)。接下来我将分别介绍各个模块的详细设计。
4.2 IIC 驱动模块设计
4.2.1 功能讲解
首先介绍 IIC 驱动模块的设计, I2C 驱动模块的主要功能是按照 I2C 协议对 E2PROM 存储芯片执行数据读写操作。I2C 驱动模块框图如下图所示:
IIC 驱动模块端口与功能描述如下图所示:
由图表可知,I2C 驱动模块包括 13 路输入输出信号,其中 7 路输入信号、 5 路输出信号,还有一路 sda 既可以做输出,也可以做输入。
输入信号中, clk、rst_n 是从顶层例化到 I2C 驱动模块的系统时钟和复位信号;i2c_exec 是 I2C 触发执行信号,由 e2prom 读写模块生成并传入,高电平有效;i2c_rh_wl 是 I2C 读写控制信号,i2c_rh_wl 为 1 表示进行读操作, i2c_rh_wl 为 0 表示进行写操作;与 i2c_exec 信号同时传入的还有字地址 i2c_addr[15:0] 和待写入字节数据 i2c_data_w[7:0];当 I2C 触发执行信号有效, 并且 i2c_rh_wl 信号为 0, 模块执行单次写操作,按照 I2C 器件字地址 i2c_addr,向 E2PROM 对应地址写入数据 i2c_data_w;当 i2c_rh_wl 为 1,模块执行随机数据读操作,按照 I2C 器件字地址 i2c_addr 读取 E2PROM 对应地址中的数据。
前文中我们提到, I2C 设备字地址有单字节和双字节两种,为了应对这一情况,我们向模块输入 bit_ctrl 信号, bit_ctrl 信号为字地址位控制信号,是顶层模块定义的参数通过例化传入的 I2C 驱动模块, bit_ctrl 为 1 时表示是双字节字地址, 在进行数据读写操作时要写入数据字地址 i2c_addr 的全部 16 位, bit_ctrl 为 0 时表示是单节字地址, 在进行数据读写操作时只写入数据字地址 i2c_addr 的低 8 位。
输出信号中, dri_clk 是本模块的工作时钟,由系统时钟 sys_clk 分频而来,它的时钟频率为串行时钟 scl 频率的 4 倍。I2C 起始信号是在 scl 为高电平时拉低 sda 信号产生的,I2C 停止信号是在 scl 为高电平时,sda 从低电平跳变到高电平产生的,使用 dri_clk 检测该起始信号与结束信号的波形如下图所示:
从上图我们可以得知,只有当 dri_clk 是 scl 的 4 倍以上的频率时,才能正确产生 I2C 起始信号和停止信号。
时钟信号 dri_clk 要传入 e2prom 读写模块(e2prom_rw) 作为模块的工作时钟;输出给 e2prom 读写模块(e2prom_rw) 的 I2C 一次操作完成信号 i2c_done,高电平有效,表示 I2C 一次操作完成;i2c_data_r[7:0] 信号表示自 E2PROM 读出的单字节数据,输出至 e2prom 读写模块(e2prom_rw);scl、sda 分别是串行时钟信号和串行数据信号,由模块产生传入 E2PROM 存储芯片。
4.2.2 波形图绘制
上面我们结合图表对 I2C 驱动模块的具体功能和输入输出端口做了说明。那么如何利用输入信号实现模块功能,并输出正确信号呢?
在本小节,我会通过绘制模块波形图,对模块功能以及各信号波形的设计与实现作出详细讲解。在绘制波形图之前我们回想一下前面讲到的 I2C 设备单次写操作和随机读操作的操作流程,我们发现使用状态机来实现 I2C 设备的读/写操作是十分方便的。参照 I2C 设备单次写操作和随机读操作的操作流程,我们绘制 I2C 读/写操作状态转移图如下。
(1) I2C 驱动模块接收到 I2C 触发执行信号(i2c_exec)后,IIC 控制总线从空闲状态跳转到写控制命令状态(st_sladdr),FPGA 开始向 E2PROM 发送写控制命令(从机地址+写控制);
(2) 写控制命令发送完成后,FPGA 开始向 E2PROM 发送字地址(i2c_addr[15:0])数据,I2C 驱动模块根据接收到的字地址位控制信号(bit_ctrl)进行判断,如果 bit_ctrl 是 0,写单字节的字地址(st_addr8),如果 bit_ctrl 是 1,写双字节的字地址(st_addr16),因为我们使用 E2PROM 是 E2PROM(AT24C64),所以 I2C 驱动模块接收到的字地址位控制信号是 1,我们从写控制命令状态(st_sladdr)跳转到写双字节的字地址(st_addr16)状态;
(3) 我们定义写双字节的字地址(st_addr16)状态是写双字节字地址的高八位状态,因位写双字节字地址的低八位状态与写单字节字地址状态是一样的,所以 st_addr8 既是写双字节字地址的低八位状态,也是写单字节字地址状态。 在发送完双字节字地址的高八位后下一个状态跳转到发送双字节字地址的低八位状态;
(4) 字地址发送完成后,I2C 驱动模块写标志信号(wr_flag,由 i2c_rh_wl 在 i2c_exec 为高时寄存得到)来判断下一步是进行读操作还是写操作。如果 i2c_rh_wl 信号为0,表示即将进入写数据状态(st_data_wr),顺序执行第(5)步,如果 i2c_rh_wl 信号为 1,跳转到第(6)步发送读控制命令状态 st_addr_rd((1)、(2)与(3)是随机读之前虚写);
(5) 进入写数据状态(st_data_wr),I2C 驱动模块将接收的要写的数据(i2c_data_w[7:0])写入 E2PROM 存储芯片, 写操作完成后直接跳到第(8)步停止状态;
(6) st_addr_rd 是 FPGA 开始向 E2PROM 发送读控制命令(重新开始+器件地址+读控制),读控制命令发送完成后,开始下一步的读数据操作(st_data_rd);
(7) 读数据操作(st_data_rd)是I2C驱动模块将发送的要读的数据(i2c_data_r[7:0])从 E2PROM 存储芯片中读出,读操作完成后顺序执行下一步;
(8) 进入停止状态后(st_stop),FPGA 向 E2PROM 发送 I2C 操作完成信号(st_done),一次 I2C 操作完成,随后状态机跳回 st_idle (初始状态),等待下一次 I2C 触发执行信号(i2c_exec) 。
单次写操作的波形图如下图所示:
随机读操作的波形图如下图所示:
使用状态机实现 I2C 驱动模块功能是模块的大体思路,结合前面讲解的 I2C 通讯协议的相关知识和相关设计方法,我们开始继续模块波形图的绘制。
I2C 驱动模块主要内容是单次写操作和随机读操作。但是这两个操作的时序波形都比较繁复,所以我们不仅将单次写操作和随机读操作读操作分开讲解, 我们还将读写操作的波形图按照I2C驱动模块状态跳转图的状态分开进行展示,对于各信号波形的设计与实现进行详细说明。
我们这里分主要分三部分,详细说明一下 I2C 单次写操作的相关信号、驱动时钟的产生与单次写操作的状态跳转流程。
第一部分: I2C单次写的输入信号说明
I2C 驱动模块的输入信号有 7 路,并且 7 路信号都与单次写操作有关。系统时钟信号 sys_clk(50MHz) 和复位信号 sys_rst_n 不必多说,这是模块正常工作必不可少的;I2C 触发执行信号 i2c_exec,只有该信号被触发,I2C 操作才会进行;I2C 操作被触发后,I2C 读写控制信号 i2c_rh_wl 为 0 时模块才会执行单次写操作;bit_ctrl 信号为字地址位控制信号,赋值为 0 时,表示 I2C 设备字地址为单字节,赋值为 1 时,表示 I2C 设备字地址为双字节,本实验使用的 E2PROM 存储芯片的存字地址是双字节,所以此信号恒为高电平;信号 i2c_addr[15:0] 为字地址;i2c_data_w[7:0] 表示要 I2C 要写入的数据。
第二部分:时钟信号计数器clk_cnt和输出信号i2c_clk的设计与实现
本实验对 E2PROM 读写操作的串行时钟 scl 的频率为 250KHz ,且只在数据读写操作时时钟信号才有效,其他时刻 scl 始终保持高电平。若直接使用系统时钟生成串行时钟 scl ,计数器要设置较大的位宽,较为麻烦,我们这里先将系统时钟分频为频率较小的时钟, 再使用新分频的时钟来生成串行时钟 scl 。所以,在这里声明一个新的计数器 clk_cnt 对系统时钟 sys_clk 进行计数,利用计数器 clk_cnt 生成新的时钟 dri_clk 。
串行时钟 scl 的时钟频率为 250KHz ,我们要生成的新时钟 dri_clk 的频率要是 scl 的 4 倍,之所以这样是为了后面更好的生成 scl 和 sda ,所以 dri_clk 的时钟频率为 1MHz 。经计算, clk_cnt 要在 0-24 内循环计数,每个系统时钟周期自加 1 ; clk_cnt 每计完一个周期, dri_clk 进行一次取反,最后得到 dri_clk 为频率 1MHz 的时钟,本模块中其他信号的生成都以此信号为同步时钟。信号波形图如下。
在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 scl 串行时钟的配合下,在 sda 上逐位地串行传送每一位数据。数据位的传输是边沿触发,即在 scl 的上升沿采集数据,在 scl 为高电平时数据保持,sda 可以在 scl 为低电平时进行改变,scl 低电平的中间是 i2c 驱动模块时钟(dri_clk)的上升沿,所以主机在写数据的时候在 scl 低电平的中间更新数据,读数据的时候可以在 scl 为高电平的时候寄存数据。 I2C 数据传输的波形如下图所示:
第三部分: 单次写状态机相关信号波形的设计与实现
前文提到输出至 E2PROM 的串行时钟 scl 与串行数据 sda 只有在进行数据读写操作时有效,其他时刻始终保持高电平。我们使用 50MHz 系统时钟生成了 1MHz 时钟 i2c_clk,但输出至 E2PROM 的串行时钟 scl 的时钟频率为 250KHz, 为此我们定义了一个 I2C 驱动时钟 i2c_clk 的时钟个数计数器 cnt,对时钟 i2c_clk 时钟信号进行计数。单次写操作的每个状态初始时,cnt 的值为 0 ,每计数一个周期的 i2c_clk 时钟,cnt 自加 1 ,根据 I2C 单次写时序,随着 cnt 计数,依次对串行时钟与串行数据赋值,既传输的指令、地址以及数据,位宽为固定的8位数据。并且申明一个该状态完成信号 st_done,st_done 高有效,作为状态机状态跳转的触发信号。
输出的串行数据 sda 作为一个双向端口,主机通过它向从机发送控制指令、地址以及数据,接收从机回传的应答信号和读取数据。回传给主机的应答信号是校验主机发送数据是否成功的条件之一。声明信号 sda_in 作为串行数据 sda 缓存,声明 i2c_ack 信号作为应答信号, 如果在主机发送完成后,从机发送非应答信号(sda_in =1),此时给 i2c_ack 信号赋值一个高电平,代表数据传输错误,会使 led 灯闪烁,提示 I2C 通信错误,接下来清空 cnt 计数跳转到下一个状态开始工作。
i2c_ack 信号只在状态机处于各应答状态时根据 sda_in 信号进行赋值,此时为从机回传的应答信号,其他状态时钟保持低电平。
状态机状态跳转的各约束条件均已介绍完毕, 首先声明状态变量 cur_state,结合各约束信号,单次写操作状态机跳转流程如下:
(1) 系统上电后, clk_cnt 计数器开始计数,产生 I2C 驱动时钟 dri_clk,并且状态机处于 st_idle (空闲状态),接收到 I2C 触发执行信号 i2c_exec 后,状态机跳转到 st_sladdr (发送写控制命令状态),同时 cnt 计数器开始计数 dri_clk 时钟个数;
(2) 在 st_sladdr (发送写控制命令状态)状态, 保持一个串行时钟周期,期间 FPGA 向 E2PROM 存储芯片发送起始信号,既在 scl 为高电平时拉低 sda 信号, 开始 I2C 操作,同时开始传输 7 位器件地址 + 写控制位,写控制命令传输完成会产生一个传输完成信号 st_done,该传输完成信号高有效,并且判断接收到字地址控制位信号 bit_ctrl,bit_ctrl 为 1(我们 E2PROM 器件地址为双字节),状态机跳转到传输写双字节高 8 位字地址状态(st_addr16);
(3) 在 st_addr16 状态双字节高 8 位字地址传输完成后,输出高有效的传输完成信号 st_done 后,状态机跳转到传输写双字节低 8 位字地址状态(st_addr8);
(4) 在 st_addr8 状态双字节低 8 位字地址传输完成后,输出高有效的传输完成信号 st_done 后,判断接收到的写标志信号 wr_flag,wr_flag 为 0 时,状态机跳转到传输写数据状态(st_data_wr);
(5) 在写数据状态(st_data_wr)传输 8 位写数据后输出高有效的传输完成信号 st_done,此时状态机会跳转到 I2C 操作结束状态,输出一个 I2C 单次写操作完成信号 i2c_done,i2c_done 高有效后状态机再跳转回 st_idle(空闲状态)。
这里我画了单次写的各个状态的波形图,可见下文。
首先我们讲解 I2C 发送写控制命令,在空闲状态时,我们给 I2C 数据方向控制(sda_dir)信号赋值高电平,将 sda 总线的方向设置为主机 FPGA 输出数据。至于我们为什么要定义一个 I2C 数据方向控制信号,是因为数据线 SDA 是双向的, 如下图所示。
为了避免主机、从机同时操作数据线,可以在 FPGA 内部可以使用三态门结构避免此事件发生。sda_dir 表示 I2C 数据方向,为 1 时表示主机(FPGA)输出信号,为 0 时 FPGA 输出高阻态,表示释放控制权。 如下图所示。
所以在 I2C 单次写操作,即每次 FPGA 输出数据时,在进行数据传输之前都需要先将 sda_dir 信号拉高,在数据传输完成后再将 sda_dir 信号拉低,将 SDA 总线的控制权交给从机发送响应数据。
在空闲状态,接收到 I2C 触发执行信号后进行执行 I2C 操作,并且接收的读写控制信号 i2c_rh_wl 也为低电平时,状态机从空闲状态跳转到发送写命令状态(st_sladdr),并且将接收的 I2C 读写控制信号(i2c_rh_wl)赋值给写标志 wr_flag,将接收的 I2C 字地址寄存为 addr_t,将接收的 i2c 将写数据寄存为 data_wr_t, I2C 应答信号 i2c_ack 一直处于应答状态。
在 st_sladdr 状态,cnt 从 0 开始计数, scl 与 sda 保持默认高电平,cnt 计数为 1 时, scl 为保持高电平,此时将 sda 拉低,代表开始 I2C 操作,cnt 计数加 1,cnt 计数值为 2 时,以连续 4 个 cnt 计数 dri_clk 时钟为一个周期产生串行时钟 scl,用来传输串行数据 sda。在单次写的 st_sladdr 状态,传输的数据主要是器件地址与写控制位,即 “10100000” ;8bit 数据传输完成,拉高一个周期的数据该次操作完成信号 i2c_done,为下一个状态跳转的标志信号。
接下来主机释放 SDA 以使从机应答,即 sda_dir 拉低,sda_out 拉高,接下来从机开始应答,因为我们设计从机一直处于应答状态,只有传输发生错误是从机回发出一个非应答信号,提示数据传输错误,数据重新传输;应答完成后开始切换状态机的状态,所以之后滞后一个周期,状态由上一个状态切换到当前状态,计数器 cnt 清零,开始下一数据传输状态。
由上面的状态机跳转图可知,写命令传输完成后,根据接收到的字地址控制命令 bit_ctrl 可知,我们下一个进入状态是传输双字节高 8 位字地址状态(st_addr16)。在传输双字节高 8 位字地址的状态(st_addr16),cnt 从 0 开始计数,进入状态后,第一步拉高 sda_dir 信号,切换 SDA 数据方向为 FPGA 输出,然后开始传输 8bit 字地址,因为第一个传入的字地址为 “16’b0000_0000_0000_0000”,所以 st_addr16 状态发送的高 8 位字地址位 “8’b0000_0000” ,通过 sda_out 一个 bit 一个 bit 的传输出去。
8bit 数据传输完成,拉高一个周期的数据该次操作完成信号 i2c_done,为下一个状态跳转的标志信号。接下来主机释放 SDA 以使从机应答,即 sda_dir 拉低,sda_out 拉高,接下来从机开始应答,因为我们设计从机一直处于应答状态,只有传输发生错误是从机回发出一个非应答信号,提示数据传输错误,数据重新传输。
应答完成后开始切换状态机的状态,所以之后滞后一个周期,状态的状态由上一个状态切换到当前状态,计数器 cnt 清零,开始下一数据传输状态,该状态的波形图如下图所示:
由上面的 2 个状态机跳转图可知, 在第 9 个 scl 时钟周期的上升沿,从机开始应答,将 sda 信号拉低,在其下降沿到来后,从机释放了总线,此时的 sda 由外部的上拉电路将其电平拉成高电平。
双字节高 8 位字地址传输完成后,我们下一个进入状态是传输双字节低 8 位字地址状态(st_addr8)。传输双字节低 8 位字地址状态(st_addr8)操作与传输双字节高 8 位字地址的状态(st_addr16)操作基本一致,只是传输的数据内容是 addr_t 的低 8 位数据,即 “8’b0000_0000” 通过 sda_out 传输出去,该状态的波形图如下图所示:
接下来是进入传输写数据状态(st_data_wr), 写数据状态(st_data_wr) 操作与传输双字节高 8 位字地址的状态(st_addr16)操作也是基本一致,只是传输的数据内容是 data_wr_t 的低 8 位数据,即 8’b0000_0000 通过 sda_out 传输出去。该状态数据传输的波形图如下所示:
接下来是进入停止发送状态即 I2C 操作完成状态(st_stop),该状态数据传输的波形图如下所示:
在 I2C 操作完成状态(st_stop) ,cnt 从 0 开始计数,首先拉高 sda_dir 信号切换 sda 数据方向位 FPGA 主机输出,接下来主机在 scl 为高电平时拉低 sda_out, 结束本次 I2C 单次写操作,scl 与 sda 都被拉高,即将进入空闲状态,在 I2C 操作完成状态(st_stop) 最后一个周期内输出一个单次写完成信号 i2c_done 并且给 cnt 计数器清零后彻底结束本次单次写操作,sda 总线恢复空闲状态。开始下一个字节的写入,直至 256 个数据全部写入完成,拉高读写控制信号为读数据状态,重新触发 I2C,再通过随机读将 256 个数据从 E2PROM 中读出。
由上面的波形图可知,我们在主机发送停止信号后没有马上拉高单次写完成信号 i2c_done,是因为 I2C 读写之间需要一点儿间隔时间,这个间隔时间由各个器件的类型决定。
因为此实验设计采用的是随机读,所以在发起读命令之前,我们需要先进行虚写。接下来我画了随机读的波形图如下文所示。
虚写命令发送的数据传输与上文中的图( I2C 发送写控制命令)完全一致,只是此时的数据读写控制信号(i2c_rh_wl)在 I2C 被触发时同时也被拉高了,I2C 操作从空闲状态跳转到虚写状态,同时写标志信号(wr_flag)也被拉高,开始随机读操作。数据传输过程与单次写命令一致,传输的数据也是 7 位器件地址与写控制位,即 “8’b10100000” 。
虚写 I2C 发送双字节高 8 位字地址的数据传输波形也是与单次写 I2C 发送双字节高 8 位字地址一致,区别是此时读写标志为高的读状态,第一次随机读,传输的双字节高 8 位字地址是 “8’b0000_0000” 。
虚写 I2C 发送双字节低 8 位字地址的数据传输波形与单次写 I2C 发送双字节低 8 位字地址一致,区别是此时读写标志为高的读状态,第一次随机读,传输的双字节低 8 位字地址是 “8’b0000_0000” 。
至此,I2C 的虚写操作已经全部完成,接下来状态机会跳转到发送读命令状态。
I2C 发送读控制命令的数据传输与图( I2C 发送写控制命令)非常相似,只是此时的写标志信号(wr_flag)是被拉高的。I2C 发送读控制命令数据传输过程与 I2C 发送写命令一致,只是传输的数据内容有差异,I2C 发送读控制命令传输的数据是 7 位器件地址与读控制位,即 “8’b10100001” 。
由上面两张图可知,在 iic 总线发送重新开始时序后的第 9 个 scl 的上升沿时钟周期,从机响应主机,拉低 sda 的电平,因为后面是读操作,所以在第 9 个 scl 的下降沿从机没有释放总线,故在第 9 个 scl 的下降沿 sda 还是低电平。
进入 I2C 读数据时,此时的写标志信号(wr_flag)是被拉高的。cnt 计数器从 0 开始计数,sda 数据方向控制信号 sda_dir 信号为低电平(上个状态结尾切换了从机应答),此时 sda 与 sda_out 信号间为高阻状态,FPGA 开始读取 E2PROM 里面存储的数据,通过 sda_in 信号来获取 sda 信号线上的输入数据。
第一次读的数据是 “8’b0000_0000”,sda_in 将从 sda 读取的数据逐 bit 赋值给 data_r,然后将最终读取的值赋值给 i2c_data_r 输出模块。
成功读取一个字节数据后,拉高一个周期本次读数据完成信号 st_done,下一步进入一次随机读操作的结束状态,如下图所示:
在 I2C 随机读操作完成状态(st_stop),cnt 从 0 开始计数,首先拉高 sda_dir 信号切换 sda 数据方向位 FPGA 主机输出,接下来主机在scl 为高电平时拉低 sda_out, 结束本次 I2C 单次写操作, scl 与 sda 都被拉高,即将进入空闲状态,在 I2C 操作完成状态(st_stop) 最后一个周期内输出一个单次写完成信号 i2c_done 并且给 cnt 计数器清零后彻底结束本次随机读操作,sda 总线恢复空闲状态。开始下一个字节的读出,直至 256 个数据全部读完,本次 E2PROM 读写操作完成。
这一通操作下来还是比较复杂的,建议收藏好好品一品,有问题可以私信我~~
4.2.3 编写代码
从上面波形的绘制与讲解我们可以看出,本模块代码主要分为驱动时钟产生模块、I2C 读写模块(使用三段式状态机完成)以及 sda 数据方向控制模块等。接下来我们就按照上面的功能划分与波形分析,编写对应的功能代码,详细代码如下所示(因为代码比较长,将分块展示并讲解):
因为 I2C 驱动模块的代码的输入输出以及中间信号比较多,所以在编写功能代码之前,我们先详细介绍下该模块的信号定义,I2C 驱动模块我们命名为 i2c_dri。
第 3 行代码定义的是 E2PROM 的器件地址 7’b101_0000,第 4 行代码定义的是系统时钟的频率是 50MHz,第 5 行代码定义的是 I2C 串行时钟频率 250KHz。该参数主要是用来产生 I2C 驱动模块的驱动时钟 dri_clk 的。
第 8 行到第 24 行代码的输入、输出定义可以详细阅读上文中的图(IIC驱动模块端口与功能描述)。
第 29 行到第 35 行代码是定义了 I2C 读写操作的 8 个状态,都是 I2C 读写模块三段式状态机里面需要使用的,可以查阅上文中的图(I2C 驱动模块状态跳转图)。
第 38 行代码到第 52 行代码主要是定义了 I2C 读写操作使用的中间变量,可直接查阅下图进行阅读:
介绍完 I2C 驱动模块(i2c_dri.v)的各种信号定义后,根据上文中图(SDA 双向数据线)与图(sda_dir 控制 I2C 数据方向)的详细介绍,我们编写 sda 数据方向控制模块的代码如下图所示:
在整个 IIC 传输过程中,SCL 信号是由主模块传输给从模块的时钟信号,SDA 既可以主机发送从机接收,也可以是主机接收从机发送的信号,因而 SDA 线是双向的信号,在 RTL 中用 inout 接口类型来表示该双向信号,在这里即涉及到一个三态门的实现, 即当 IIC 进行主机发送从机接收传输时,sda_dir 信号置高,sda 线上信号即为主机发送信号,当从机发送主机接收时,sda_dir 信号拉低,此时 sda 与 sda_out 信号间为高阻状态,视为断开,此时通过 sda_in 信号来获取 sda 信号线上的输入数据。
sda 数据方向控制模块编写完成,接下来我们开始编写生成 I2C 的 SCL 的四倍频率的驱动时钟,用于驱动 i2c 的操作的代码。系统时钟是 50Mhz,I2C 的 SCL 时钟是 250KHz,那么系统时钟通过分频得到 SCL 时钟的分频系数是 50MHz/250KHz=200。
由计算可知直接从系统时钟分频到 SCL 时钟分频系数过大,所以我们可以先从系统时钟 50MHz 通过分频得到 I2C 驱动时钟 dri_clk,再通过计数 dri_clk 的时钟周期得到 SCL 时钟。I2C 驱动时钟 dri_clk 是 SCL 的 4 倍即 250KHz*4=1MHz,系统时钟通过分频得到 dri_clk 时钟的分频系数是 clk_divide=50MHz/1MHz=50。
通过前面时钟分频的学习可知,我们定义一个系统时钟计数器 clk_cnt,从 0 开始计数,每计数到 24,翻转一次 dri_clk 信号的值,然后 clk_cnt 清零,从新开始计数,如此循环往复,就可以产生系统时钟 50 分频得到的 dri_clk 时钟,该分频时钟的波形可以查阅图(dri_clk 时钟波形图)。
第 1 行代码是计算系统时钟通过分频得到 dri_clk 时钟的分频系数 clk_divide。CLK_FREQ/I2C_FREQ=200,计算的是系统时钟通过分频得到 SCL 时钟的分频系数,dri_clk 时钟是 SCL 时钟的四倍,所以我们通过移位运算符 “>>”,将 SCL 时钟的分频系数右移两位得到 dri_clk 时钟的分频系数 clk_divide=200/4=50=9’b0_0011_0010。“>>” 为右移运算符,每次右移一位,数据的高位补 0,相当于将数据除以 2,右移两位即将数据除以 4。
第 5 行代码到第 15 行代码就是将系统时钟进行 50 分频。“clk_divide[8:1]” 的作用与右移运算符的实际功能一致,“clk_divide[8:1]” 是直接丢弃最低位,数据高位补 0,即 clk_divide[8:1]= 9’b0_0001_1001=25,实现的运算是将 clk_divide 的值除以 2。
I2C 驱动模块的驱动时钟产生后,我们就可以利用三段式状态机,按照 I2C 协议来描述 I2C 的读写过程了。
首先我们复习一下三段式状态机的基本格式:
第一个 always 语句实现同步状态跳转;
第二个 always 语句采用组合逻辑判断状态转移条件;
第三个 always 语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
实现同步状态跳转的代码如下所示
上面代码主要实现同步状态跳转,在系统上电后,状态机的状态(cur_state)处于空闲状态(st_idle),否则将下一个状态赋值给当前状态。
接下来编写的代码是 always 采用组合逻辑判断状态转移条件,该状态机的跳转可以查阅图 I2C 驱动模块状态跳转图,代码如下所示:
上面代码中的各个状态之间的跳转还有判断条件可以参照图 I2C 驱动模块状态跳转图的说明进行理解。
接下来就是编写三段式状态机的第三段的代码,使用时序逻辑描述状态的输出。代码如下所示:
该部分代码比较长,并且数据传输机制一致,由于篇幅原因,在此我们拿出其中一个状态的输出进行讲述,完整代码见文首的Github开源地址,这里主要简述的是主机发送写命令状态的代码,如下所示:
上面代码对应的波形是图(I2C 发送写控制命令),根据此图片来理解上述代码的编写思路。
I2C 读写状态的输出的部分代码在上文中都有绘制对应的波形图,方便按照波形进行编写代码。
至此, I2C 驱动模块的代码已经编写完成,接下来我们开始编写 E2PROM 模块的代码。
4.3 EEPROM 读写模块
4.3.1 功能讲解
E2PROM 读写模块主要实现对 I2C 读写过程的控制,包括给出字地址及需要写入该地址中的数据、启动 I2C 读写操作、判断读写数据是否一致等。E2PROM 读写模块框图如下所示。
E2PROM 读写模块端口与功能描述如下图所示:
由上图可知,E2PROM 读写模块包括 11 路输入输出信号,其中 5 路输入信号、 6 路输出信号。输入信号中,clk、rst_n 是从顶层例化到 E2PROM 读写模块的 I2C 驱动时钟 dri_clk 和系统复位信号; i2c_data_r 是从 E2PROM 读出的数据, i2c_done 是一次 I2C 操作完成信号, i2c_ack 是 I2C 应答标志。这三个信号都是由 I2C 驱动模块(i2c_dri.v)输入进来。
i2c_rh_wl 是 I2C 读写控制信号,初始值为 0,表示在进行单次写,在写完 256 个数据后,拉高该信号,I2C 开始随机读操作; i2c_exec 是 I2C 触发执行信号,i2c_exe 信号拉高一个一个周期触发一次 I2C 操作;i2c_addr 是 I2C 器件字地址,i2c_data_w 是 I2C 要写的数据,初始值都为 0,随着每次单次写操作完成信号 i2c_done 逐次加 1。
rw_done 是 E2PROM 读写测试完成信号,在 I2C 读写完成拉高一个周期;rw_result 是 E2PROM 读写测试结果,将读取的数据与写入的数据进行对比,两者一致说明 E2PROM 读写测试成功,将 rw_result 拉为高电平;rw_done 与 rw_result 会传入读写测试结果显示模块(rw_result_led.v)。
4.3.2 波形图绘制
e2prom 数据读写模块除了上面描述的输入输出信号,还需要定义一个写延时计数器 wait_cnt,用来计数 5ms 的写延迟时间。因为 AT24C64 官方手册规定了数据写入芯片的完成时间最大不超过 10ms,所以为了保证数据能够正确写入,单次写入数据操作完成后,最好延时 10ms 的时间。本次实验为了节省数据写入的时间,WR_WAIT_TIME 的值设置为 5000,即 5ms(输入时钟的周期为1us,1us*5000=5ms),实测延时 5ms 也可以正确写入。
这里不建议大家将写入的间隔设置的过于短,否则会导致数据写入失败。另外,E2PROM 只有对写操作有时间间隔要求,对读操作没有间隔要求,因此读写测试模块仅对写操作增加时间间隔。
另外我们还定义一个状态流控制 flow_cnt,用来切换读写控制与生成 I2C 的将写数据。系统上电后,进入 flow_cnt=2’d0 状态,读写控制信号(i2c_rh_wl)为低电平表示可以进行写操作,wait_cnt 计数器从 0 开始计数 ,计数到 5ms 后拉高一个周期 I2C 触发信号( i2c_exec ),触发一次 I2C 操作,将 i2c_addr=16’b0000_0000_0000_0000 与 i2c_data_w=8’b0000_0000 的数据传入 I2C 驱动模块进行一次单次写操作,I2C 驱动模块单次写完成后输出一个周期的 i2c_done 高电平,此时控制状态流控制信号(flow_cnt)加 1 进入 2’d1 状态。
在 2'd1
状态,i2c_addr 与 i2c_data_w 数据分别加 1 后又进入 2’d0 状态,wait_cnt 计数器又从 0 开始计数,计数到 5ms 后拉高 I2C 触发信号(i2c_ack),触发一次 I2C 操作,再次将现在的 i2c_addr 与 i2c_data_w数据传入 I2C 驱动模块进行一次单次写操作,如此循环操作,直至传输完成 256 各将写入的数据后,拉高读写控制信号(i2c_rh_wl),表示可以进行读操作,并且控制状态流控制信号(flow_cnt)进入 2’d2 状态。
在 2'd2
状态,收到 I2C 触发信号(i2c_exec)后,开始 I2C 随机读操作,并且控制状态流控制信号(flow_cnt)加 1 进入 2’d3 状态。
在 2'd3
状态,在随机读完成以后,将接收的随机读到数据( i2c_data_r)与写入的数据进行对比,如果两者不一致或者在 I2C 读写操作中从机非应答,则说明虽然 I2C 读写操作完成了,但是 I2C 读写操作测试失败,此时输出一个周期高电平的 E2PROM 读写测试完成信号(rw_done),此时表示 E2PROM 读写测试结果信号(rw_result)处于低电平表示测试失败。如果随机读到数据(i2c_data_r)与写入的数据对比一致,则输出一个周期高电平的 E2PROM 读写测试完成信号(rw_done)并且拉高 E2PROM 读写测试结果信号(rw_result)表示 E2PROM 读写测试成功。
e2prom 数据读写模块的波形图如下图所示:
4.3.3 编写代码
根据上面的波形图的设计,我们编写 E2PROM 读写模块的代码如下:
module e2prom_rw(
input clk , //时钟信号
input rst_n , //复位信号
//i2c interface
output reg i2c_rh_wl , //I2C读写控制信号
output reg i2c_exec , //I2C触发执行信号
output reg [15:0] i2c_addr , //I2C器件内地址
output reg [ 7:0] i2c_data_w , //I2C要写的数据
input [ 7:0] i2c_data_r , //I2C读出的数据
input i2c_done , //I2C一次操作完成
input i2c_ack , //I2C应答标志
//user interface
output reg rw_done , //E2PROM读写测试完成
output reg rw_result //E2PROM读写测试结果 0:失败 1:成功
);
//parameter define
//EEPROM写数据需要添加间隔时间,读数据则不需要
parameter WR_WAIT_TIME = 14'd5000; //写入间隔时间
parameter MAX_BYTE = 16'd256 ; //读写测试的字节个数
//reg define
reg [1:0] flow_cnt ; //状态流控制
reg [13:0] wait_cnt ; //延时计数器
//*****************************************************
//** main code
//*****************************************************
//EEPROM读写测试,先写后读,并比较读出的值与写入的值是否一致
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
flow_cnt <= 2'b0;
i2c_rh_wl <= 1'b0;
i2c_exec <= 1'b0;
i2c_addr <= 16'b0;
i2c_data_w <= 8'b0;
wait_cnt <= 14'b0;
rw_done <= 1'b0;
rw_result <= 1'b0;
end
else begin
i2c_exec <= 1'b0;
rw_done <= 1'b0;
case(flow_cnt)
2'd0 : begin
wait_cnt <= wait_cnt + 14'b1; //延时计数
if(wait_cnt == (WR_WAIT_TIME - 14'b1)) begin //EEPROM写操作延时完成
wait_cnt <= 14'b0;
if(i2c_addr == MAX_BYTE) begin //256个字节写入完成
i2c_addr <= 16'b0;
i2c_rh_wl <= 1'b1;
flow_cnt <= 2'd2;
end
else begin
flow_cnt <= flow_cnt + 2'b1;
i2c_exec <= 1'b1;
end
end
end
2'd1 : begin
if(i2c_done == 1'b1) begin //EEPROM单次写入完成
flow_cnt <= 2'd0;
i2c_addr <= i2c_addr + 16'b1; //地址0~255分别写入
i2c_data_w <= i2c_data_w + 8'b1; //数据0~255
end
end
2'd2 : begin
flow_cnt <= flow_cnt + 2'b1;
i2c_exec <= 1'b1;
end
2'd3 : begin
if(i2c_done == 1'b1) begin //EEPROM单次读出完成
//读出的值错误或者I2C未应答,读写测试失败
if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
rw_done <= 1'b1;
rw_result <= 1'b0;
end
else if(i2c_addr == (MAX_BYTE - 16'b1))begin //读写测试成功
rw_done <= 1'b1;
rw_result <= 1'b1;
end
else begin
flow_cnt <= 2'd2;
i2c_addr <= i2c_addr + 16'b1;
end
end
end
default : ;
endcase
end
end
endmodule
程序中第 21 行和第 22 行定义了两个参数,WR_WAIT_TIME 是写入间隔时间,MAX_BYTE 是读写测试的字节个数。
程序中第 32 至 94 行代码先对 I2C 驱动模块发起写操作,即拉高 i2c_exec,拉低 i2c_rh_wl(低电平表示写),然后分别向 E2PROM 的地址 0 至地址 255 写入数据 0 至 255,并且在每次写操作之间增加 5ms 的延时。数据全部写完后,发起读操作,即拉高 i2c_exec,拉高 i2c_rh_wl(高电平表示读),然后分别从 E2PROM 的地址 0 至地址 255 读出数据,并判断读出的值与写入的值是否一致,如果数据一致并且每次操作 IIC 都有应答信号产生(i2c_ack),E2PROM 的读写测试才正确,否则读写测试失败。
读写测试完成后,输出 rw_done 信号和 rw_result 信号,rw_done 为 E2PROM 读写测试完成信号,rw_result 为读写测试的结果, 0 表示读写失败, 1 表示读写正确。
至此 E2PROM 读写模块的代码已经讲解完成了,读写测试结果也已经出来,但是为了使这个测试结果更加直观的显示出来,接下来我们将设计通过 led 的状态来展示 E2PROM 读写测试的结果。
4.4 EEPROM 读写测试结果显示模块
4.4.1 功能讲解
在 E2PROM 读写测试结果显示模块,我们通过 LED 灯的状态来展示 E2PROM 读写测试的结果,led 灯保持常亮表示 E2PROM 读写测试成功,led 灯以 0.25s 间隔闪烁表示 E2PROM 读写测试成功失败。
E2PROM 读写测试结果显示模块框图如下图所示。
E2PROM 读写测试结果显示模块端口与功能描述如下图所示:
4.4.2 波形图绘制
因为需要使用 led 灯闪烁来表示 E2PROM 读写测试结果失败,所以我们需要定义一个计数 led 灯闪烁频率的计数器 led_cnt,E2PROM 读写测试结果显示模块的波形图如下所示:
4.4.3 编写代码
根据上面的波形图编写的 E2PROM 读写测试结果显示模块的代码如下:
module rw_result_led
#(parameter L_TIME = 17'd125_000
)
(
input clk , //时钟信号
input rst_n , //复位信号
input rw_done , //错误标志
input rw_result , //E2PROM读写测试完成
output reg led //E2PROM读写测试结果 0:失败 1:成功
);
//reg define
reg rw_done_flag; //读写测试完成标志
reg [16:0] led_cnt ; //led计数
//*****************************************************
//** main code
//*****************************************************
//读写测试完成标志
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rw_done_flag <= 1'b0;
else if(rw_done)
rw_done_flag <= 1'b1;
end
//错误标志为1时PL_LED0闪烁,否则PL_LED0常亮
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
led_cnt <= 17'd0;
led <= 1'b0;
end
else begin
if(rw_done_flag) begin
if(rw_result) //读写测试正确
led <= 1'b1; //led灯常亮
else begin //读写测试错误
led_cnt <= led_cnt + 17'd1;
if(led_cnt == (L_TIME - 17'b1)) begin
led_cnt <= 17'd0;
led <= ~led; //led灯闪烁
end
else
led <= led;
end
end
else
led <= 1'b0; //读写测试完成之前,led灯熄灭
end
end
endmodule
led 显示模块利用 LED 灯的显示状态来标识读写过程是否出错。程序中第 22 行至 27 行代码寄存输入的 rw_done 信号,采集到 rw_done 信号之后,拉高 rw_done_flag 信号。
程序中第 22 至 51 行代码根据 rw_done_flag 和 rw_result 信号控制 LED 灯的状态。在 E2PROM 读写测试完成之前,LED 灯处于熄灭状态;如果 E2PROM 读写测试成功, LED 灯处于常亮状态;如果 E2PROM 读写测试失败,LED 灯会不停的闪烁。
至此 E2PROM 读写测试结果显示模块的代码也讲解完成了,接下来我们将 I2C 驱动模块、E2PROM 读写模块与 E2PROM 读写测试结果显示模块通过例化添加到顶层模块。
4.5 顶层模块设计
4.5.1 功能讲解
E2PROM 读写测试实验的顶层模块框图设计如下:
E2PROM 读写测试实验顶层模块端口与功能描述如下图所示:
4.5.2 编写代码
从上面的描述可知,顶层模块的代码比较简单,不需要绘制波形图进行讲解了,E2PROM 读写测试实验顶层模块的详细代码如下图所示:
module top_e2prom(
input sys_clk , //系统时钟
input sys_rst_n , //系统复位
//eeprom interface
output iic_scl , //eeprom的时钟线scl
inout iic_sda , //eeprom的数据线sda
//user interface
output led //led显示eeprom读写测试结果
);
//parameter define
parameter SLAVE_ADDR = 7'b1010000 ; //器件地址(SLAVE_ADDR)
parameter BIT_CTRL = 1'b1 ; //字地址位控制参数(16b/8b)
parameter CLK_FREQ = 26'd50_000_000 ; //i2c_dri模块的驱动时钟频率(CLK_FREQ)
parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率
parameter L_TIME = 17'd125_000 ; //led闪烁时间参数
parameter MAX_BYTE = 16'd256 ; //读写测试的字节个数
//wire define
wire dri_clk ; //I2C操作时钟
wire i2c_exec ; //I2C触发控制
wire [15:0] i2c_addr ; //I2C操作地址
wire [ 7:0] i2c_data_w; //I2C写入的数据
wire i2c_done ; //I2C操作结束标志
wire i2c_ack ; //I2C应答标志 0:应答 1:未应答
wire i2c_rh_wl ; //I2C读写控制
wire [ 7:0] i2c_data_r; //I2C读出的数据
wire rw_done ; //E2PROM读写测试完成
wire rw_result ; //E2PROM读写测试结果 0:失败 1:成功
//*****************************************************
//** main code
//*****************************************************
//e2prom读写测试模块
e2prom_rw #(
.MAX_BYTE (MAX_BYTE ) //读写测试的字节个数
) u_e2prom_rw(
.clk (dri_clk ), //时钟信号
.rst_n (sys_rst_n ), //复位信号
//i2c interface
.i2c_exec (i2c_exec ), //I2C触发执行信号
.i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
.i2c_addr (i2c_addr ), //I2C器件内地址
.i2c_data_w (i2c_data_w), //I2C要写的数据
.i2c_data_r (i2c_data_r), //I2C读出的数据
.i2c_done (i2c_done ), //I2C一次操作完成
.i2c_ack (i2c_ack ), //I2C应答标志
//user interface
.rw_done (rw_done ), //E2PROM读写测试完成
.rw_result (rw_result ) //E2PROM读写测试结果 0:失败 1:成功
);
//i2c驱动模块
i2c_dri #(
.SLAVE_ADDR (SLAVE_ADDR), //EEPROM从机地址
.CLK_FREQ (CLK_FREQ ), //模块输入的时钟频率
.I2C_FREQ (I2C_FREQ ) //IIC_SCL的时钟频率
) u_i2c_dri(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
//i2c interface
.i2c_exec (i2c_exec ), //I2C触发执行信号
.bit_ctrl (BIT_CTRL ), //器件地址位控制(16b/8b)
.i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
.i2c_addr (i2c_addr ), //I2C器件内地址
.i2c_data_w (i2c_data_w), //I2C要写的数据
.i2c_data_r (i2c_data_r), //I2C读出的数据
.i2c_done (i2c_done ), //I2C一次操作完成
.i2c_ack (i2c_ack ), //I2C应答标志
.scl (iic_scl ), //I2C的SCL时钟信号
.sda (iic_sda ), //I2C的SDA信号
//user interface
.dri_clk (dri_clk ) //I2C操作时钟
);
//led指示模块
rw_result_led #(.L_TIME(L_TIME ) //控制led闪烁时间
) u_rw_result_led(
.clk (dri_clk ),
.rst_n (sys_rst_n ),
.rw_done (rw_done ),
.rw_result (rw_result ),
.led (led )
);
endmodule
顶层模块中主要完成对其余模块的例化,需要注意的是程序第 12 行到第 17 行定义了六个参数,在模块例化时会将这些变量传递到相应的模块。
SLAVE_ADDR 定义了 E2PROM 的器件地址;字地址位控制参数 (16b/8b)BIT_CTRL 是用来控制不同字地址的 I2C 器件读写时序中字地址的位数,当 I2C 器件的字地址为 16 位时,参数 BIT_CTRL 设置为 “1”,当 I2C 器件的字地址为 8 位时,参数 BIT_CTRL 设置为 “0”;
i2c_dri 模块的驱动时钟频率 CLK_FREQ 是指在例化 I2C 驱动模块 i2c_dri 时,驱动 i2c_dri 模块的时钟频率;
I2C 的 SCL 时钟频率参数I2C_FREQ 是用来控制 I2C 协议中的 SCL 的频率,一般不超过 400KHz;
led 闪烁时间参数 L_TIME 用来控制 led 的闪烁间隔时间,参数值与驱动该模块的 clk 时钟频率有关。例如,控制 led 闪烁的间隔时间为 0.25s,clk 的频率为 1MHz 时,0.25s/1us=250000,由于代码中当计数器计数到 L_TIME 的值时,LED 的状态改变一次,LED 高电平加上低电平的时间才是一次闪烁的时间,所以 L_TIME 的值应定义成 125000。MAX_BYTE 定义了读写测试的字节个数为 256 个字节。
至此 E2PROM 读写测试的代码就讲解完成了,接下来我们可以通过仿真验证代码功能是否正常。
5 仿真验证
5.1 编写 TestBench
接下来我开始讲解 E2PROM 读写测试实验的仿真代码,因为该部分代码较长,为了使代码与下面的讲解看着比较连贯,我将 TB 文件总体分为 3 部分讲解。
这第一部分主要是 E2PROM 读写测试实验顶层模块的信号定义,前面都有介绍,这里做了一点儿修改,第 11 行代码定义的 led 灯闪烁间隔时间从比较长的时间 0.25s 修改为 1us,第 12 行代码将读写的字节个数从 256 个修改为读写 3 个字节,节约仿真时间,提高仿真效率。
第 1 行到第 6 行代码是给输入信号进行初始化赋值,第 9 行代码是生成一个 50Mhz 的时钟,即系统时钟 sys_clk。
第 12 行代码是将 SDA 数据线为高阻态时上拉为高电平,板子上是通过连接一个上拉电阻实现的上拉效果。
第 1 行到 11 行代码是例化例化 e2prom_top 模块;第 14 行到 17 行代码是例化了一个 E2PROM 的仿真模型。因为 I2C 传输数据时需要从机应答,所以在仿真时需要一个 E2PROM 的模型配合才能进行仿真,该模型(E2PROM_AT24C64.v)需要与我们使用开发板板载的器件同型号,可以从官网获取,如果需要移植工程需根据自己的使用的开发板来配置。由于篇幅原因,这里就不贴本实验涉及的模型的代码了,本实验涉及的模型的存放路径如下图所示:
I2C 驱动模块激励文件编写完成,E2PROM 的仿真模型添加成功,此时就可以打开 Modelsim 软件对代码进行仿真啦。
5.2 代码仿真
接下来打开 Modelsim 软件对代码进行仿真,首先我们查看 E2PROM 读写测试顶层模块在运行仿真 30ms 后,仿真的波形如下图所示:
通过观察上图,读出的数据与写入的数据一致,读写结束后输出读写结束信号 rw_done,并且输出读写对比结果 rw_result 信号, led 灯保持常亮。
从该波形图中我们看到读写控制信号 i2c_rh_wl 为低电平,表示处于写操作状态。当 I2C 触发执行信号 i2c_exec 为高电平时开始执行 I2C 写操作。在 IIC 操作结束后,拉高 i2c_done 信号。另外,在 IIC 写操作期间, i2c_ack(IIC 应答标志)一直处于低电平,说明 E2PROM 响应了主机并应答。
E2PROM 读操作的 ILA 波形图如下图所示:
从该波形图中我们看到读写控制信号 i2c_rh_wl 为高电平,表示处于读操作状态。当 I2C 触发执行信号 i2c_exec 为高电平时开始执行 I2C 读操作。在 IIC 操作结束后,拉高 i2c_done 信号。另外,在 IIC 读操作期间, i2c_ack(IIC 应答标志)一直处于低电平,说明 E2PROM 响应了主机并应答。仿真结果与我们绘制的波形显示一致,仿真验证成功,仿真至此结束。
6 下载验证
在仿真验证完成后,接下来创建工程,对引脚进行分配并上板验证。 本实验中使用的端口有系统时钟、按键复位、iic 串行时钟、iic 串行数据以及一个 LED 灯,根据自己开发板进行管脚分配即可。
程序下载完成后,LED 在短暂延时之后,开始处于常亮的状态,代表我们实验成功!
7 总结
本文内容比较长,需要花时间好好消化,我认为需要重点掌握的知识点有:
-
E2PROM
器件的器件特性:熟读 AT24C64_datasheet.pdf(根据自己开发板上的芯片来) 手册,明白该器件的存储容量、字地址长度,结合原理图设计会查看器件地址; -
I2C 协议
:掌握 I2C 协议的各种读写操作的时序原理,这也是本文的重点; -
FPGA
开发中状态机
的使用: 因为 FPGA 的特点是并行执行, 所以如果需要处理一些具有前后顺序的事件,就需要使用状态机。 此时利用状态机进行开发, 可以将一个复杂的控制流程分解成多个互相独立的状态,从而简化设计过程并提高了系统的可靠性和性能。这是一个入门 FPGA 工程师必须掌握的。
希望以上的内容对您有所帮助,诚挚地欢迎各位读者在评论区或者私信我交流!
微博:沂舟Ryan (@沂舟Ryan 的个人主页 - 微博 )
GitHub:ChinaRyan666
微信公众号:沂舟无限进步(内含精品资料及详细教程)
如果对您有帮助的话请点赞支持下吧!文章来源:https://www.toymoban.com/news/detail-786613.html
集中一点,登峰造极。文章来源地址https://www.toymoban.com/news/detail-786613.html
到了这里,关于(九)零基础学懂 IIC 协议——EEPROM 读写测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!