硬件平台:征战Pro开发板
软件平台:Vivado2018.3
仿真软件:Modelsim10.6d
文本编译器:Notepad++
征战Pro开发板资料
链接:https://pan.baidu.com/s/1AIcnaGBpNLgFT8GG1yC-cA?pwd=x3u8
提取码:x3u8
1知识背景
I2C 通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线, 只需要两根线即可在连接于总线上的器件之间传送信息。
I2C 通讯协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 I2C 协议占用引脚特别少,硬件实现简单,可扩展型强,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
下面我们分别对 I2C 协议的物理层及协议层进行讲解。
1.1 I2C 物理层
I2C 通讯设备之间的常用连接方式,具体见图6-8-1。
图6-8- 1通讯设备连接图
它的物理层有如下特点:
(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4) 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6) 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
(7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
1.2 I2C协议层
在本小节中,我们对 I2C 协议的整体时序图、读写时序以及 I2C 设备的器件地址和存储地址做一下详细介绍。
1.2.1I2C 整体时序图
I2C 协议的整体时序图,如下图所示。
图6-8- 2 I2C 协议整体时序图
由图可知, I2C 协议整体时序图分为 4 个部分,图中标注的①②③④表示 I2C 协议的 4
个状态,分别为“总线空闲状态”、“起始状态”、“数据读/写状态”和“停止状态”,针对这 4 个状态,我们来做一下详细介绍。
(1) 图中标注①表示“总线空闲状态”,在此状态下串口时钟信号 SCL 和串行数据信号 SDA 均保持高电平,此时无 I2C 设备工作。
(2) 图中标注②表示“起始状态”,在 I2C 总线处于“空闲状态”时, SCL 依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有 I2C 设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。此处记住一个关键点,就是SCL保持高电平时,SDA出现了下降沿。
(3) 图中标注③表示“数据读 / 写状态” , “数据读 / 写状态” 时序图具体见图6-8-3。
图6-8- 3数据读写时序图
I2C 通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分。
当主机向从机进行指令或数据的写入时,在图 6-8-3 标号①我们可以看到,串行数据线 SDA 上的数据在串行时钟 SCL为高电平时写入从机设备,每次只写入一位数据;在图 6-8-3 标号②我们可以看到,串行数据线 SDA 中的数据在串行时钟SCL 为低电平时进行数据更新,以保证在 SCL 为高电平时采集到 SDA 数据的稳定状态。总结一下,图 6-8-3 标号①为数据稳定区,图 6-8-3 标号②为数据更新区。
当一个完整字节的指令或数据传输完成,从机设备正确接收到指令或数据后,会通过拉低 SDA 为低电平,向主机设备发送单比特的应答信号(ACK),表示数据或指令写入成功。 若从机正确应答,可以结束或开始下一字节数据或指令的传输,否则表明数据或指令写入失败,主机就可以决定是否放弃写入或者重新发起写入。当ACK等于 0 表示应答成功;当ACK等于 1 表示应答失败。
(4) 图6-8-2中标注④表示“停止信号” ,完成数据读写后,串口时钟 SCL 保持高电平,当串口数据信号 SDA 产生一个由低电平转为高电平的上升沿时,产生一个停止信号,I2C 总线跳转回“总线空闲状态”。
1.2.2 I2C 设备器件地址与存储地址
每个 I2C 设备在出厂前都被设置了器件地址,用户不可自主更改;器件地址一般位宽为 7 位,有的 I2C 设备的器件地址设置了全部位宽,例如后面章节要讲解的 OV7725、OV5640 摄像头;有的 I2C 设备的器件地址设置了部分位宽,例如本章节要使用的EEPROM 存储芯片,它的器件地址只设置了高 4 位,剩下的低 3 位由用户在设计硬件时自主设置。
FPGA 开发板使用的是 ATMEL 公司生产的 AT24C 系列中的型号为 AT24C64 的EEPROM 存储芯片。 AT24C64 存储容量为 64Kbit,内部分成 256 页,每页 32 字节, 共有8192 个字节,且其读写操作都是以字节为基本单位。 AT24C64 EEPROM 存储芯片的器件地址包括厂商设置的高 4 位 1010 和用户需自主设置的低 3 位 A0、 A1、 A2 。在硬件设计时,通过将芯片的 A0、 A1、 A2 这 3 个引脚分别连接到 VCC 或 GND 来实现器件地址低 3位的设置,若 3 个引脚均连接到 VCC,则设置后的器件地址为 1010_111; 若 3 个引脚均连接到 GND,则设置后的器件地址为 1010_000。由于 A0、 A1、 A2 这 3 位只能组合出 8 种情况,所以一个主机最多只能连接 8 个 AT24C64 存储芯片。
在 I2C 主从设备通讯时,主机在发送了起始信号后,接着会向从机发送控制命令。控制命令长度为 1 个字节,它的高 7 位为上文讲解的 I2C 设备的器件地址,最低位为读写控制位。读写控制位为 0 时,表示主机要对从机进行数据写入操作;读写控制位为 1 时,表示主机要对从机进行数据读出操作。对于 AT24C64 来说,其传输器件地址格式如下图所示。
图6-8- 4地址格式示意图
通常情况下,主机在与从机建立通讯时,并不是直接向想要通讯的从机发送控制命令
(器件地址 + 读/写控制位)以建立通讯,而是主机会将控制命令直接发送到串行数据线 SDA上,与主机硬件相连的从机设备都会接收到主机发送的控制命令。所有从机设备在接收到主机发送的控制命令后会与自身器件地址做对比;若两者地址相同,该从机设备会回应一个应答信号告知主机设备,主机设备接收到应答信号后,主从设备建立通讯连接,两者可进行数据通讯。
到这里 I2C 设备器件地址的相关内容已讲解完毕,我们开始 I2C 设备存储地址相关内容的介绍。
每一个支持 I2C 通讯协议的设备器件,内部都会包含一些可进行读/写操作的寄存器或存储器。例如后面章节将会讲到的 OV7725、 OV5640 摄像头(它们使用的是与 I2C 协议极为相似的 SCCB 协议,后面章节会进行讲解,他们内部包含一些需要进行读/写配置的寄存器,只有向对应寄存器写入正确参数,摄像头才能被正确使用;同样,本章节要使用的EEPROM 存储芯片内部则包含许多存储单元,需要存储的数据按照地址被写入对应存储单元。
由于 I2C 设备要配置寄存器的多少或存储容量的大小的不同,存储地址根据位宽分为单字节和 2 字节两种。例如后文要提到的 OV7725、 OV5640 摄像头,两者的寄存器数量不同, OV7725 摄像头需要配置寄存器较少,单个字节能够实现所有寄存器的寻址,所以他的存储地址位宽为 8 位;而 OV5640 摄像头需要配置寄存器较多,单个字节不能够实现所有寄存器的寻址,所以他的存储地址位宽为 16 位, 2 个字节。
以 EEPROM 存储芯片为例,在 ATMEL 公司生产的 AT24C 系列 EEPROM 存储芯片中选取两款存储芯片 AT24C04 和 AT24C64。 AT24C04 的存储容量为 1Kbit(128byte), 7 位存储地址即可满足所有存储单元的寻址,存储地址为单字节即可;而 AT24C64 的存储空间为64 Kbit(8Kbyte),需要 13 位存储地址才可满足所有存储单元的寻址,存储地址为 2 字节。AT24C04、 AT24C64 存储地址示意图,具体见图 6-8-5。
图6-8- 5 AT24C04、 AT24C64 存储地址
1.2.3 I2C 读/写操作
对传入从机的控制命令最低位读写控制位写入不同数据值,主机可实现对从机的读/写操作,读写控制位为 0 时,表示主机要对从机进行数据写入操作;读写控制位为 1 时,表示主机要对从机进行数据读出操作。对于 I2C 协议的读/写操作,我们将其分为读操作和写操作两部分进行讲解。
首先讲解 I2C 写操作,由于一次写入数据量的不同, I2C 的写操作可分为单字节写操作和页写(连续写)操作,详细讲解如下。
1.2.3.1 I2C单字节写操作
I2C 设备单字节写操作时序图,具体见图 6-8-6、 图 6-8-7。
图6-8- 6单字节写操作时序图(单字节存储地址)
图6-8- 7单字节写操作时序图(2 字节存储地址)
参照时序图,列出单字节写操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低
电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写
入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2
字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;
(7) 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单字节数据写入完成。
1.2.3.2 I2C 页写(连续写)操作
单字节写操作中,主机一次向从机中写入单字节数据;页写操作中,主机一次可向从机写入多字节数据。连续写时序图,具体见图 6-8-8、 图 6-8-9。
图6-8- 8页写操作时序图(单字节存储地址)
图6-8- 9页写操作时序图(2字节存储地址)
参照时序图,列出页写操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后, 若为 2
字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始第一个单字节数据的写入;
(7) 数据写入完成,主机接收到应答信号后,开始下一个单字节数据的写入;
(8) 数据写入完成,主机接收到应答信号。若所有数据均写入完成,顺序执行操作流程;若数据尚未完成写入,跳回到步骤(7);
(9) 主机向从机发送停止信号,页写操作完成。
讲到这里, I2C 设备的单字节数据写入和页写操作的流程已经讲解完毕,读者需要注意的是,所有 I2C 设备均支持单字节数据写入操作,但只有部分 I2C 设备支持页写操作;且支持页写操作的设备,一次页写操作写入的字节数不能超过设备单页包含的存储单元数。本章节使用的 AT24CXX 系列的 EEPROM 存储芯片,单页存储单元个数为 32 个,一次页写操作只能写入 32 字节数据。
I2C 写时序介绍完毕后,接下来我们开始 I2C 读时序部分的介绍。根据一次读操作读取数据量的多少,读操作可分为随机读操作和顺序读操作,详细讲解如下。
1.2.3.3 I2C 随机读操作
I2C 随机读操作可以理解为单字节数据的读取,操作时序图具体见图 6-8-10、 图6-8-11。
图6-8- 10随机读操作时序图(单字节存储地址)
图6-8- 11随机读操作时序图(2 字节存储地址)
参照时序图,列出随机读时序操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;
(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(8) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(9) 数据接收完成后,主机产生一个时钟的高电平无应答信号;
(10) 主机向从机发送停止信号,单字节读操作完成。
注意:时序图中有一个空写(Dummy Write)操作,主要目的就是指定要读的寄存器地址。
1.2.3.4 I2C 顺序读操作
I2C 顺序读操作就是对寄存器或存储单元数据的顺序读取。假如要读取 n 字节连续数据,只需写入要读取第一个字节数据的存储地址,就可以实现连续 n 字节数据的顺序读取。操作时序具体见图 6-8-12、 图 6-8-13。
图6-8- 12顺序读操作时序图(单字节存储地址)
图6-8- 13顺序读操作时序图(2 字节存储地址)
参照时序图,列出顺序读时序操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;
(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(8) 主机接收到从机回传的应答信号后,开始接收从机传回的第一个单字节数据;
(9) 数据接收完成后, 主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据接收完成,执行下一操作步骤;若数据接收未完成,在此执行步骤(9);
(10) 主机产生一个时钟的高电平无应答信号;
(11) 主机向从机发送停止信号,顺序读操作完成。
知识背景这一节在AT24C64数据手册中都能找到相关的说明,大家可以仔细看一下数据手册。
2 实验任务
使用按键控制数据向 EEPROM 中写入 100个数字,分别是1,3,5,7,9,11,13,15,17,19…,使用读控制按键读出之前写入到 EEPROM 的数据,读按键按1次读一个数据出来,并将读出的数据在数码管上显示出来。
3 硬件设计
3.1 原理图分析
征战 Pro 开发板使用的 EEPROM 型号为 24C64 存储容量为 64 Kbit(8Kbyte),需要 13位存储地址才可满足所有存储单元的寻址,存储地址为 2 字节。 电阻R177默认不焊接, EEPROM原理图如图6-8- 14所示。
图6-8- 14板载 EEPROM 原理图
由原理图可知,征战 Pro 板载 EEPROM 地址位 A0、 A1 、A2 接地;EEPROM 地址为 7’b1010_000。AT24C64 有一个写保护引脚(第 6 脚 WP)用于提供数据保护,当写保护引脚连接至 GND 时,芯片可以正常写,当写保护引脚连接至 VCC 时,使能写保护功能,此时禁止向芯片写入数据,只能进行读操作。
3.2 管脚映射表
表6-8- 1 管脚映射表
3.3 编写XDC约束文件
4 程序设计
为了让我们的代码能更好的重复利用,我们尽可能将代码按功能模块来进行划分,比如我们现在这个实验,可以划分成五个部份。
1. 顶层模块(eeprom_rd_wr_top),用于例化各个功能模块
2. 按键消抖模块(key_xd),用于实现按键消抖(该模块我们就可以直接应用于其它涉及到按键的工程,不用再去编写,直接调用即可)
3. 数码管动态扫描相关的模块(直接调用前面章节数码管动态扫描部份的代码即可)
4. I2C驱动模块(i2c_driver),主要功能是按照 I2C 协议对 EERPROM 存储芯片执行数据读写操作。
5. I2C数据控制模块(i2c_data_ctrl),主要功能为 I2C 驱动模块提供读/写数据存储地址、待写入数据。
4.1 顶层模块
4.1.1 模块框图
图6-8- 15 顶层模块框图
结合顶层模块框图,简述一下本实验工程的具体流程。按下数据写操作按键,写触发信号传入按键消抖模块(key_xd),经消抖处理后的写触发信号传入数据控制模块(i2c_data_ctrl),模块接收到有效的写触发信号后,生成写使能信号、待写入数据、字节地址传入 I2C 驱动模块(i2c_driver), I2C 驱动模块按照 I2C 协议将数据写入 EEPROM 存储芯片;
按下数据读操作按键,读触发信号传入按键消抖模块(key_xd),经消抖处理后的读触发信号传入数据控制模块(i2c_data_ctrl),模块接收到有效的读触发信号后,生成读使能信号、数据地址传入 I2C 驱动模块(i2c_driver), 每来一次读触发信号,字节地址进行加1操作,I2C 驱动模块从 EEPROM存储芯片读取数据,将读取到的数据传 至数码管动态显示模块。
4.1.2 设计思路
在本实验中,顶层模块只用于例化各个功能模块,比较简单,此处不做讲解。
4.1.3 代码编写
限于篇幅,仅贴出部份代码(详见 Source 文件夹下 eeprom_rd_wr_top.v 文件)定义模块端口,代码如下所示:
4.2 按键消抖模块(key_xd)
前面章节已经做了详细讲解,此处不再赘述。
4.3 数码管动态扫描相关的模块
前面章节已经做了详细讲解,此处不再赘述。
4.4 I2C驱动模块(i2c_driver)
4.4.1 模块框图
图6-8- 16 I2C驱动模块框图
输入端口:
clk:时钟,50Mhz,来源于板载晶振
rst_n:复位,低电平有效,来源于按键
byte_addr:I2C读写地址,来源I2C数据控制模块(i2c_data_ctrl)
byte_addr_num:I2C读写地址位宽,1表示地址位宽为8,2表示地址位宽为16,由于我们的EEPROM是16位地址,所以固定赋值2
i2c_start:I2C开始触发信号,高脉冲有效,来源I2C数据控制模块(i2c_data_ctrl)
rd_en:I2C读使能,高电平有效,读开始时拉高,读结束后将其拉低,来源I2C数据控制模块(i2c_data_ctrl)
wr_en:I2C写使能,高电平有效,写开始时拉高,写结束后将其拉低,来源I2C数据控制模块(i2c_data_ctrl)
wr_data:I2C写数据,来源I2C数据控制模块(i2c_data_ctrl)
输出端口:
i2c_clk:I2C驱动时钟,频率1Mhz,传给I2C数据控制模块(i2c_data_ctrl)
i2c_fini:I2C读写操作完成标志,高脉冲有效,传给I2C数据控制模块(i2c_data_ctrl)
i2c_scl:I2C时钟,250Khz,传给EEPROM芯片
i2c_sda:I2C数据,传给EEPROM芯片
rd_data:I2C读出的数据,传给数码管显示模块
4.4.2 设计思路
该模块主要是生成I2C读写时序,写操作包括写地址,写数据,再定义一个表示写地址写数据有效标志,当该标志为高电平时,进行写时序操作,所以当我们要进行写操作时,只用给该模块灌入这 3 个信号即可,当写操作完成后,再输出一个写操作完成标志信号。读操作一样包括了读地址,读地址有效标志,当我们要进行读操作时,只用给该模块灌入这两个信号,当读操作完成后,输出一个读操作完成标志以及读出的数据。
具体的读写时序可以参考前面的讲解,接下来我们开始编写代码,一边理解I2C时序,一边编写代码。
4.4.3 代码编写
限于篇幅,仅贴出部份代码(详见 Source 文件夹下 i2c_driver.v 文件)
定义模块端口,代码如下所示:
本实验对 EEPROM 读写操作的串行时钟 scl 的频率为 250KHz,且只在数据读写操作时时钟信号才有效,其他时刻 scl 始终保持高电平。若直接使用系统时钟生成串行时钟scl,计数器要设置较大的位宽,较为麻烦,我们这里先将系统时钟分频为频率较小的时钟,在使用新分频的时钟来生成串行时钟 scl。
所以,在这里声明一个新的计数器 clk_cnt 对系统时钟 clk 进行计数,利用计数器clk_cnt 生成新的时钟 i2c_clk。
串行时钟 scl 的时钟频率为 250KHz,我们要生成的新时钟 i2c_clk 的频率要是 scl 的 4倍,之所以这样是为了后面更好的生成 scl 和 sda,本模块中其他信号的生成都以i2c_clk为同步时钟
首先通过分频生成i2c_clk时钟,分频系数通过parameter定义,代码如下所示:
I2C_CLK_DIV_NUM等于CLK_FREQ除以SCL_FREQ,再将得到的商右移3位,数字电路中,右移表示除,右移3位,表示除以8,所以 I2C_CLK_DIV_NUM = CLK_FREQ / SCL_FREQ / 8,就等于25。再根据这个分频系统,对系统时钟(50Mhz)进行分频,计数器每计到I2C_CLK_DIV_NUM - 1,i2c_clk就进行翻转,这样就产生了一个1Mhz的时钟。我们想一下,为什么是1Mhz?因为i2c_clk高电平包括25个系统时钟,i2c_clk低电平包括25个系统时钟,所以一个i2c_clk周期包括50个系统时钟,一个系统时钟周期20ns,所以一个i2c_clk的周期为 50 x 20ns = 1000ns,即频率为1Mhz。为什么但此处计数器 clk_cnt 计数最大值 I2C_CLK_DIV_NUM 并未直接赋值呢,而是使用公式赋值。这是为了提高 I2C 驱动模块的复用性,这样一来,只要设置好系统时钟与串行时钟的时钟频率,本模块即可在多种时钟频率下使用,复用性大大提高。代码如下所示:
前文理论部分提到,输出至 EEPROM 的串行时钟 scl 与串行数据 sda 只有在进行数据读写操作时有效,其他时刻始终保持高电平。由前文状态机相关讲解可知,除 IDLE(初始状态)状态之外的其他状态均属于数据读写操作的有效部分,所以声明一个使能信号i2c_clk_en,在除 IDLE(初始状态)状态之外的其他状态保持有效高电平,作为 I2C 数据读写操作使能信号。代码如下所示:
我们使用 50MHz 系统时钟生成了 1MHz 时钟 i2c_clk,但输出至 EEPROM 的串行时钟scl 的时钟频率为 250KHz,我们声明时钟信号计数器 i2c_clk_cnt,作为分频计数器,对时钟 i2c_clk 时钟信号进行计数,初值为 0,由于位宽是2,所以计数范围为 0 - 3,计数时钟为 i2c_clk 时钟,每个时钟周期自加 1,实现时钟 i2c_clk 信号的 4 分频,生成串行时钟 scl。同时计数器i2c_clk_cnt 也可作为生成串行数据 sda 的约束条件,以及状态机跳转条件。代码如下所示:
计数器 i2c_clk_cnt 循环计数一个周期,对应串行时钟 scl 的 1 个时钟周期以及串行数据 sda 的 1 位数据保持时间,进行数据读写操作时,传输的指令、地址以及数据,位宽为固定的 8 位数据,我们声明一个比特计数器 bit_cnt,对计数器 i2c_clk_cnt 的计数周期进行计数,可以辅助串行数据 sda 的生成,同时作为状态机状态跳转的约束条件。代码如下所示:
输出的串行数据 sda 作为一个双向端口,主机(FPGA)通过它向从机发送控制指令、地址以及数据,主机(FPGA)也可能sda接收从机回传的应答信号和读取数据。回传给主机的应答信号是实现状态机跳转的条件之一。声明信号 sda_in 作为串行数据 sda 缓存,直接通过assign语句将i2c_sda赋值给sda_in,代码如下所示:
声明 ack 信号作为应答信号, ack 信号只在状态机处于各应答状态时由 sda_in 信号赋值,此时为从机回传的应答信号,其他状态时钟保持高电平,代码如下所示:
状态机状态跳转的各种约束条件讲解完毕,声明状态变量 curr_st。
在本实验中对 EERPROM 的数据读写操作均使用单字节读/写操作,即每次操作只读/写单字节数据。我们回想一下前面讲到的 I2C 设备单字节写操作和随机读操作的操作流程,结合前面学到的知识,我们发现使用状态机来实现 I2C 设备的读/写操作是十分方便的。
我们再来看看单字节写操作时序,如下图所示:
图6-8- 17单字节写操作时序
根据时序图,我们定义以下状态机及跳转关系,如下图所示:
图6-8- 18 单字节写操作状态机图
经过11个状态就完成了一次单字节写操作,那状态机跳转的条件是怎样的呢?比如什么条件下从IDLE跳转到WR_START状态,现在我们来仔细看一下状态机的工作流程。
系统上电后,状态机处于 IDLE(空闲状态),接收到有效的单字节数据读/写开始信号
i2c_start 后,状态机跳转到 WR_START(起始状态);FPGA 向 EEPROM 存储芯片发送起始信
号;随后状态机跳转到 WR_DEV_ADDR(发送器件地址状态),在此状态下向 EEPROM 存储芯片写入控制指令,控制指令高 7 位为器件地址,最低位为读写控制字,写入“0”,表示执行写操作;控制指令写入完毕后,状态机跳转到 ACK_1(应答状态)。
在 ACK_1(应答状态)状态下,要根据存储地址字节数进行不同状态的跳转。当 FPGA接 收 到 EEPROM 回 传 的 应 答 信 号 且 存 储 地 址 字 节 为 2 字 节 , 状 态 机 跳 转到WR_BYTE_ADDR_H(发送高字节地址状态),将存储地址的高 8 位写入 EEPROM,写入完成后,状态机跳转到 ACK_2(应答状态 ); FPGA 接收到应答信号后,状态机跳转到WR_BYTE_ADDR_L(发送低字节地址状态);当 FPGA 接收到 EEPROM 回传的应答信号且存储地址字节为单字节,状态机状态机直接跳转到 SEND_BYTE_ADDR_L(发送低字节地址状态);在此状态低 8 位存储地址或单字节存储地址写入完成后,状态机跳转到 ACK_3(应答状态)。
在 ACK_3(应答状态)状态下,状态机跳转到 WR_DATA(写数据状态);在写数据状态,向 EEPROM 写入单字节数据后,状态机跳转到 ACK_4(应答状态);待 FPGA 接收到有效应答信号后,状态机跳转到 STOP(停止状态);再从STOP跳转到IDLE,以上就是写操作的状态机。我们再来看看随机读的状态,还是回顾一下随机读的时序图,如下图所示:
图6-8- 19随机读的时序图
根据时序图,我们依然可以画出状态机图,如下所示:
图6-8- 20 随机读状态机图
我们将写操作的状态机和读操作的状态机放在一起,会发现虚线框里面的状态其实是一样的,如下图所示:
图6-8- 21 读写操作状态机对比图
所以,在ACK_3状态之前,写和读的状态机是一样的,但是在ACK_3之后我们就要做区分了,到底是写数据,还是读数据。要根据读/写使能信号做不同的状态跳转。当 FPGA 接收到应答信号且写使能信号有效,状态机跳转到 WR_DATA(写数据状态);在写数据状态,向 EEPROM 写入单字节数据后,状态机跳转到 ACK_4(应答状态);待 FPGA 接收到有效应答信号后,状态机跳转到 STOP(停止状态);当 FPGA 接收到应答信号且读使能信号有效,状态机跳转到 RD_START (起始状态);再次向 EEPROM 写入起始信号,状态跳转到WR_DEV_ADDR_R(发送读控制状态);再次向 EEPROM 写入控制字节,高 7 位器件地址不变,读写控制位写入“1”,表示进行读操作,控制字节写入完毕后,状态机跳转到ACK_5(应答状态);待 FPGA 接收到有效应答信号后,状态机跳转到 RD_DATA(读数据状态);在 RD_DATA(读数据状态)状态, EEPROM 向 FPGA 发送存储地址对应存储单元下的单字节数据,待数据读取完成户,状态机跳转到 N_ACK(无应答状态),在此状态下向EEPROM 写入一个时钟的高电平,表示数据读取完成,随后状态机跳转到 STOP(停止状态)。
在 STOP(停止状态)状态, FPGA 向 EEPROM 发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回 IDLE(初始状态),等待下一次单字节数据读 /写开始信号i2c_start。
使用状态机实现 I2C 驱动模块功能是模块的大体思路,结合前面讲解的 I2C 通讯协议的相关知识和相关设计方法,我们就可以编写状态机部份的代码了。由于代码量比较大,此处仅贴出部份代码,详见Source文件夹i2c_driver模块,代码如下所示:
串口数据 sda 端口作为一个双向端口,在单字节读取操作中,主机只在除应答状态之外的其他状态拥有它的控制权,在应答状态下主机只能接收由从机通过 sda 传入的应答信号。 所以定义一个使能信号 sda_en,在除应答状态之外的其他状态赋值为有效的高电平, sda_en为高电平时,主机拥有对 sda 的控制权。总结一下,当sda_en = 1 时,sda的方向为FPGA–>EEPROM;当sda_en = 0 时,sda的方向为EEPROM–> FPGA。声明 i2c_sda_buf 作为输出 i2c_sda 信号的数据缓存,在 sda_en 有效时,将 i2c_sda_buf的值赋值给输出串口数据 i2c_sda, sda_en 无效时,输出串口数据 i2c_sda 为高阻态,主机放弃其控制权,接收其传入的应答信号。代码如下所示:
i2c_sda_buf 在使能信号 sda_en 无效时始终保持高电平,在使能 sda_en 有效时,在状态机对应状态下,以计数器 cnt_ i2c_clk、 bit_cnt 为约束条件,对应写入起始信号、控制指令、存储地址、 写入数据、停止信号,声明 rd_data_buf 作为 EEPROM 读出数据缓存,在状态机处于读数据状态(RD_DATA)时,变量 rd_data_buf 由输入信号 sda_in 赋值,暂存 EEPROM 读取数据,代码如下所示:
对于输出的串行时钟 i2c_scl,由 I2C 通讯协议可知, I2C 设备只在串行时钟为高电平时进行数据采集,在串行时钟低电平时实现串行数据更新。我们使用计数器 cnt_ i2c_clk、bit_cnt 以及状态变量 curr_st 为约束条件,结合 I2C 通讯协议,生成满足时序要求的输出串行
时钟 i2c_scl,代码如下所示:
单字节写操作部分涉及的各信号代码设计与实现讲解完毕,下面开始随机读操作部分的代码编写与讲解。通过之前的状态机图,我们知道单字节写操作和随机读操作所涉及的各信号大体相同,在随机读操作,我们只讲解差别较大之处,操作相同或相似之处不再说明,读者可回顾单字节写操作部分的介绍。
通过读写状态机的对比,我们发现在状态K_3处需要做跳转区分,当写使能(wr_en=1)时,状态机跳转到WR_DATA状态,当读使能(rd_en=1)时,状态机跳转到RD_START 状态,代码如下所示:
串口数据 sda 端口作为一个双向端口,在随机读操作中,主机只在除应答状态、读数据状态之外的其他状态拥有它的控制权,在应答状态下主机接收由从机通过 sda 传入的应答信号,在读数据状态下主机接收由从机传入的单字节数据。 声明使能信号 sda_en,只在除应答状态、读数据状态之外的其他状态赋值为有效的高电平, sda_en 有效时,主机拥有对 sda 的控制权。代码如下所示:
由于rd_data_buf在读状态过程中,数据是一直在变化的,所以我们在读数据状态结束后,将暂存数据rd_data_buf赋值给输出信号 rd_data,代码如下所示:
声明定义一个读写结束标志信号(i2c_fini),每当完成一次写操作或者读操作,将i2c_fini拉高,维持一个时钟周期(i2c_clk)即可,代码如下所示:
4.5 I2C数据控制模块(i2c_data_ctrl)
4.5.1 模块框图
图6-8- 22 I2C数据控制模块框图
输入端口:
clk:时钟,50Mhz,来源于板载晶振
rst_n:复位,低电平有效,来源于按键
i2c_clk:I2C驱动时钟,频率1Mhz,来源I2C驱动模块(i2c_driver)
i2c_end:I2C读写操作完成标志,高脉冲有效,来源I2C驱动模块(i2c_driver)
wr_trig:I2C写触发标志,高脉冲有效,来源按键消抖模块(key_xd)
rd_trig:I2C读触发标志,高脉冲有效,来源按键消抖模块(key_xd)
输出端口:
byte_addr:I2C读写地址,传给I2C驱动模块(i2c_driver)
i2c_start:I2C开始触发信号,高脉冲有效,传给I2C驱动模块(i2c_driver)
rd_en:I2C读使能,高电平有效,读开始时拉高,读结束后将其拉低,传给I2C驱动模块(i2c_driver)
wr_en:I2C写使能,高电平有效,写开始时拉高,写结束后将其拉低,传给I2C驱动模块(i2c_driver)
wr_data:I2C写数据,传给I2C驱动模块(i2c_driver)
4.5.2 设计思路
该模块主要根据读写触发信号给I2C驱动模块发送开始信号,写地址 ,写数据,写使能以及读地址,读使能信号。当收到写触发信号时,向I2C驱动模块依次发送10个地址,与地址同步的10个数字以及写使能信号;当收到读触发信号时,向I2C驱动模块发送当前地址以及读使能信号,每收到一次读触发信号,读地址加1。
4.5.3 代码编写
限于篇幅,仅贴出部份代码(详见 Source 文件夹下 i2c_data_ctrl.v 文件)
定义模块端口,代码如下所示:
定义一个 wr_trig 有效信号(wr_trig_vld):由于 wr_trig 是在系统时钟(clk)50Mhz 时钟域下,高脉冲的时间为 20ns,但是 I2C 驱动模块的时钟是经过分频后,I2C 时钟(i2c_clk)频率为 1Mhz,所以在 wr_trig 等于 1 时将 wr_trig_vld 拉高,一直维持至少 50 个系统时钟,再拉低,这样 wr_trig_vld 的高电平才能在 i2c_clk 时钟域下正确捕捉,代码如下所示:
根据需求,我们需要写入 10 个字节,所以定义一个写使能(wr_en),当检测 wr_trig_vld 为高电平时,wr_en 拉高,直到到写入的字节等于 10 时,再将 wr_en 拉低。代码如下所示:
定义写地址(wr_byte_addr)和写数据(wr_data),当写使能(wr_en)等于1时,开始控制写地址和写数据,写地址从 0 开始,写数据从 1 开始,当一个字节写结束标志(i2c_fini)到来时,写地址加 1 ,写数据加 2 ,直到写使能(wr_en)等于0。在前面我们已经提到当写入 10个字节时,wr_en 拉低。所以这个写地址我们也只写入 10 次。代码如下所示:
I2C 写操作部份代码编写好了,再来看看读操作的。同样需要定义一个 rd_trig 有效信号(rd_trig_vld),原因大家已经知道了,就是因为时钟域的原因。根据需求,由于读操作不是连续读 10 个数据,而是按键按 1 次读 1 次,所以读使能(rd_en)拉高后,检测到本次读操作完成标志(i2c_fini)等于 1,就将 rd_en 拉低,此处和读使能有区别,读者朋友可以好好理解一下。代码如下所示:
定义一个读地址寄存器(rd_byte_addr),检测到 rd_trig_vld 为高时,进行加 1 操作,该地址会一直累加,所以我们的实验现象是在读按键按下 10 次后,读出的数据就是8’hff,读者朋友可以自行将代码修改一下,改成读 10 次后,又从 0 地址开始读。代码如下所示:
5 仿真验证
5.1 顶层模块(eeprom_rd_wr_top)仿真验证
由于顶层文件例化了其它功能模块,我们只用给顶层文件的输入信号灌入激励信号,其它功能模块也都有了激励信号,在本实验中我们仅用写顶层激励文件即可。本工程的sda是输入输出端口,i2c_driver的状态跳转以及读出的数据都与sda信号的输入有关,所以,最好有一个EEPROM的仿真模型,这样我们发出的scl和sda信号给仿真模型,仿真模型就会按读写时序通过sda给FPGA回传相应的信号,这为我们仿真带来极大的方便。EEPROM的仿真模型非常复杂,我们就不自己写了,可以在网上或芯片官网找到现成的仿真模型,我们直接调用就行。在仿真过程中,我发现这个仿真模型有点问题,就是在读数据时,本来读出的数据应该为高电平,但是表现出来的却是高阻态,读者朋友知道有这回事就行,这个不影响我们验证代码的逻辑功能。
5.1.1仿真激励代码编写
限于篇幅,仅贴出部份代码,详见( Sim 文件夹 tb_eeprom_rd_wr_top.v 文件)
5.1.2批处理仿真
仿真代码编写好,就可以使用批处理仿真了,在该章节我们可以不用再更改modelsim.bat文件。sim.do 文件也仅仅只用修改一处地方,如下图所示:
此处改为我们当前的仿真代码模块名:tb_eeprom_rd_wr_top,改好以后,保存。
5.1.3仿真波形分析
双击 modelsim.bat,我们就不管了,先喝茶,等软件自已跑(具体步骤参考前一章节)
我们将 i2c_data_ctrl 模块的信号加入 modelsim 波形窗口观察,仿真波形如下图所示:
图6-8- 23 仿真波形1
从上图的波形中可以看到:
wr_trig 来了一个高脉冲,写使能 wr_en 就持续拉高,直到写入数据个数 wr_i2c_data_num 等于 2,同时写完成标志( i2c_fini )为高时,wr_en 拉低,在 wr_en 有效期间,byte_addr 从 0 开始到 2 结束,写数据(wr_data)为 1,3,5,与设计相符。
rd_trig 来了一个高脉冲,读使能 rd_en 持续拉高,同时 byte_addr 等于 0,当收到读完成标志(i2c_fini )时,rd_en 拉低,当再次收到 rd_trig 时,读使能 rd_en 再次持续拉高,同时 byte_addr 等于 1,当收到读完成标志(i2c_fini )时,rd_en 拉低,与设计相符。
我们将 i2c_driver 模块的信号加入modelsim波形窗口观察,整体仿真波形如下图所示:
图6-8- 24 仿真波形2
接下来,我们将波形放大,主要看一下各个状态 scl 和 sda 的信号是否正确,我们以第二次写入为例。
图6-8- 25 仿真波形3
从上图中可以看到:
在 curr_st = 1 时,即 WR_START 状态,在 i2c_scl 为高电平时,i2c_sda 有一个下降沿,这与我们之前提到的时序(起始位)一致。
在 curr_s t= 2 时,即 WR_DEV_ADDR_W 状态,在 i2c_scl 为高电平时,i2c_sda 没有跳变沿处于稳定状态,依次写入的数据为:8’b1010_0000,高 7 位为设备地址 7’b1010_000,最低位为读写标志,低电平表示写,高电平表示读,此处为 0,即表示写,与设计相符。
在 curr_st = 3 时,即 ACK_1 状态,该状态为从机响应状态,i2c_sda 为低电平表示从机正确响应,可进入下一状态。
图6-8- 26 仿真波形4
从上图可以看到:
在 curr_st = 4 时,即 WR_BYTE_ADDR_H 状态,即写地址的高 8 位,在 i2c_scl 为高电平时,i2c_sda 全部等于 0,即高 8 位等于 0 ,与设计相符
在 curr_st = 5 时,即 ACK_2 状态,该状态为从机响应状态,i2c_sda 为低电平表示从机正确响应,可进入下一状态。
在 curr_st = 6 时,即 WR_BYTE_ADDR_L 状态,即写地址的低 8 位。低 8 位等于 8’h01,i2c_sda 红色虚线标注的为高电平,其余都为低电平,即 8’h01,与设计相符。
在 curr_st = 7 时,即 ACK_3 状态,该状态为从机响应状态,i2c_sda 为低电平表示从机正确响应,可进入下一状态。
在 curr_st = 8 时,即 WR_DATA 状态。由于我们此处的波形是第二次写入数据的波形,第二次写入的地址为 16’h0001,写入的数据为 8’h03,i2c_sda 红色虚线标注的为高电平,其余都为低电平,即写入的数据为 8’h03,与设计相符。
在 curr_st = 9 时,即 ACK_4 状态,该状态为从机响应状态,i2c_sda 为低电平表示从机正确响应,可进入下一状态。
在 curr_st = 4’hf 时,即 STOP 状态,在 i2c_scl 为高电平时,i2c_sda 有一个上升沿,这与我们之前提到的时序(停止位)一致。
写操作波形讲解完了,我们接着看一下读操作的波形。前面我们提到写操作和读操作的时序有很多相似的地方,从curr_st = 7 (ACK_3)状态开始,就不一样了,此处主要分析一下不同的地方,以第二次读操作波形为例,如下图所示:
图6-8- 27 仿真波形5
从上图可以看到:
在 curr_st = 10 时,即 RD_START 状态,在 i2c_scl 为高电平时,i2c_sda 有一个下降沿,这与我们之前提到的时序(起始位)一致。
在 curr_st = 11 时,即 WR_DEV_ADDR_R 状态,在 i2c_scl 为高电平时,i2c_sda 没有跳变沿处于稳定状态,依次写入的数据为:8’b1010_0001,高 7 位为设备地址 7’b1010_000,最低位为读写标志,低电平表示写,高电平表示读,此处为 1,即表示读,与设计相符。
在 curr_st = 12 时,即 ACK_5 状态,该状态为从机响应状态,i2c_sda 为低电平表示从机正确响应,可进入下一状态。
在 curr_st = 13 时,即 RD_DATA 状态。i2c_sda 红色虚线标注的为高阻态(由于仿真模型的问题,高阻态我们认为是高电平),其余都为低电平,即读出的数据为8’h03,与设计相符。
在 curr_st = 14 时,即 NACK 状态,数据接收完成后,主机产生一个时钟的高电平无应答信号,与设计相符。
6 综合编译
在前面我们已经将Source里面的源码(xx.v)和约束文件(pin.xdc)通过notepad++ 软件编辑好了,并且通过Modelsim进行了功能仿真,接下来我们新建Vivado工程并生成bit文件。具体步骤见《6-1 LED灯闪烁实验》,此处不再赘述。
7 下载验证
程序下载好以后,先按写按键,再按读按键,按1次读一个数据出来,并将读出的数据显示在数码管上了,如下图所示:
图6-8- 28 实验现象