18. 基于i2c协议的adda实验

在上一章节我们学习了SPI通讯协议的基础知识,通过若干个实验,编写工程,运用SPI通讯协议实现了Flash芯片的擦除、数据读写等操作,在这一章节,我们同样要介绍一个较为重要的通讯协议 —— I2C通讯协议

本章节读者要学习掌握二线制 I2C通讯协议的基本知识和概念,熟悉 FPGA与I2C器件之间数据通信流程。运用所学知识设计一个可进行读写操作的I2C控制器,实现FPGA对EEPROM存储器的数据写入和数据读取操作,并上板验证。

18.1. 理论学习

I2C 通讯协议(Inter-Integrated Circuit)是由Philips公司开发的一种简单、双向二线制同步串行总线,只需要两根线即可在连接于总线上的器件之间传送信息。

I2C 通讯协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 I2C协议占用引脚特别少,硬件实现简单,可扩展型强,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。

下面我们分别对I2C协议的物理层及协议层进行讲解。

18.1.1. I2C物理层

I2C通讯设备之间的常用连接方式,具体见图 47‑1。

I2CEEP002

图 47‑1 I2C通讯设备连接图

它的物理层有如下特点:

  1. 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个I2C通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。

  2. 一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

  3. 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。

  4. 总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

  5. 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

  6. 具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。

  7. 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

18.1.2. i2c协议层

在本小节中,我们对I2C协议的整体时序图、读写时序以及I2C设备的器件地址和存储地址做一下详细介绍。

18.1.2.1. I2C整体时序图

I2C协议的整体时序图,具体见图 47‑2。

I2CEEP003

图 47‑2 I2C协议整体时序图

由图可知,I2C协议整体时序图分为4个部分,图中标注的①②③④表示I2C协议的4个状态,分别为“总线空闲状态”、“起始信号”、“数据读/写状态”和“停止信号”,针对这4个状态,我们来做一下详细介绍。

  1. 图中标注①表示“总线空闲状态”,在此状态下串口时钟信号SCL和串行数据信号SDA均保持高电平,此时无I2C设备工作。

  2. 图中标注②表示“起始信号”,在I2C总线处于“空闲状态”时,SCL依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有I2C设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。

  3. 图中标注③表示“数据读/写状态”,“数据读/写状态”时序图具体见图 47‑3。

I2CEEP004

图 47‑3 数据读写时序图

I2C通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分。

当主机向从机进行指令或数据的写入时,串行数据线SDA上的数据在串行时钟SCL为高电平时写入从机设备,每次只写入一位数据;串行数据线SDA中的数据在串行时钟SCL为低电平时进行数据更新,以保证在SCL为高电平时采集到SDA数据的稳定状态。

当一个完整字节的指令或数据传输完成,从机设备正确接收到指令或数据后,会通过拉低SDA为低电平,向主机设备发送单比特的应答信号,表示数据或指令写入成功。若从机正确应答,可以结束或开始下一字节数据或指令的传输,否则表明数据或指令写入失败,主机就可以决定是否放弃写入或者重新发起写入。

  1. 图中标注④表示“停止信号”,完成数据读写后,串口时钟SCL保持高电平,当串口数据信号SDA产生一个由低电平转为高电平的上升沿时,产生一个停止信号,I2C总线跳转回“总线空闲状态”。

18.1.3. 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时,表示主机要对从机进行数据读出操作。

EEPROM储存芯片控制命令格式示意图,具体见图 47‑4。

I2CEEP005

图 47‑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存储地址示意图,具体见图 47‑5。

I2CEEP006

图 47‑5 AT24C04、AT24C64存储地址

18.1.3.1. I2C读/写操作

对传入从机的控制命令最低位读写控制位写入不同数据值,主机可实现对从机的读/写操作,读写控制位为0时,表示主机要对从机进行数据写入操作;读写控制位为1时,表示主机要对从机进行数据读出操作。对于I2C协议的读/写操作,我们将其分为读操作和写操作两部分进行讲解。

首先讲解I2C写操作,由于一次写入数据量的不同,I2C的写操作可分为单字节写操作和页写操作,详细讲解如下。

I2C单字节写操作

I2C设备单字节写操作时序图,具体见图 47‑6、图 47‑7。

I2CEEP007

图 47‑6 单字节写操作时序图(单字节存储地址)

I2CEEP008

图 47‑7 单字节写操作时序图(2字节存储地址)

参照时序图,列出单字节写操作流程如下:

  1. 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;

  2. 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为2字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);

  3. 先向从机写入高8位地址,且高位在前低位在后;

  4. 待接收到从机回传的应答信号,再写入低8位地址,且高位在前低位在后,若为2字节地址,跳转到步骤(6);

  5. 按高位在前低位在后的顺序写入单字节存储地址;

  6. 地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;

  7. 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单字节数据写入完成。

I2C页写操作

单字节写操作中,主机一次向从机中写入单字节数据;页写操作中,主机一次可向从机写入多字节数据。连续写时序图,具体见图 47‑8、图 47‑9。

I2CEEP009

图 47‑8 页写操作时序图(单字节存储地址)

I2CEEP010

图 47‑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读时序部分的介绍。根据一次读操作读取数据量的多少,读操作可分为随机读操作和顺序读操作,详细讲解如下。

I2C随机读操作

I2C随机读操作可以理解为单字节数据的读取,操作时序图具体见图 47‑10、图 47‑11。

I2CEEP011

图 47‑10 随机读操作时序图(单字节存储地址)

I2CEEP012

图 47‑11 随机读操作时序图(2字节存储地址)

参照时序图,列出页写时序操作流程如下:

  1. 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;

  2. 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为2字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);

  3. 先向从机写入高8位地址,且高位在前低位在后;

  4. 待接收到从机回传的应答信号,再写入低8位地址,且高位在前低位在后,若为2字节地址,跳转到步骤(6);

  5. 按高位在前低位在后的顺序写入单字节存储地址;

  6. 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;

  7. 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;

  8. 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;

  9. 数据接收完成后,主机产生一个时钟的高电平无应答信号;

  10. 主机向从机发送停止信号,单字节读操作完成。

I2C顺序读操作

I2C顺序读操作就是对寄存器或存储单元数据的顺序读取。假如要读取n字节连续数据,只需写入要读取第一个字节数据的存储地址,就可以实现连续n字节数据的顺序读取。操作时序具体见图 47‑12、图 47‑13。

I2CEEP013

图 47‑12 顺序读操作时序图(单字节存储地址)

I2CEEP014

图 47‑13 顺序读操作时序图(2字节存储地址)

参照时序图,列出页写时序操作流程如下:

  1. 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;

  2. 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为2字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);

  3. 先向从机写入高8位地址,且高位在前低位在后;

  4. 待接收到从机回传的应答信号,再写入低8位地址,且高位在前低位在后,若为2字节地址,跳转到步骤(6);

  5. 按高位在前低位在后的顺序写入单字节存储地址;

  6. 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;

  7. 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;

  8. 主机接收到从机回传的应答信号后,开始接收从机传回的第一个单字节数据;

  9. 数据接收完成后,主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据接收完成,执行下一操作步骤;若数据接收未完成,在此执行步骤(9);

  10. 主机产生一个时钟的高电平无应答信号;

  11. 主机向从机发送停止信号,顺序读操作完成。

18.1.4. 实战演练

在本章节的“理论学习”小结,我们对I2C通讯协议的相关理论知识做了系统性的讲解,为了帮助读者更好的理解I2C的理论知识,我们结合理论知识设计一个使用I2C通讯协议的EEPROM读写控制器,通过实验使用按键控制实现EEPROM数据读写。

18.1.5. 实验目标

运用所学理论知识设计一个使用I2C通讯协议的EEPROM读写控制器,使用按键控制数据写入或读出EEPROM。使用写控制按键向EEPROM中写入数据1-10共10字节数据,使用读控制按键读出之前写入到EEPROM的数据,并将读出的数据在数码管上显示出来。

18.1.6. 硬件资源

征途Pro开发板使用的EEPROM型号为24C64存储容量为64 Kbit(8Kbyte),需要13位存储地址才可满足所有存储单元的寻址,存储地址为2字节。EEPROM实物图与原理图,如图 47‑14、图 47‑15所示。

I2CEEP015

图 47‑14 板载EEPROM实物图

I2CEEP016

图 47‑15 板载EEPROM部分原理图

由原理图可知,征途Pro板载EEPROM地址位A0、A1接高电平,A2接地;EEPROM地址为7’b1010_011。

18.2. 程序设计

18.2.1. 整体说明

由“实验目标”小节,我们知道们实验工程是要设计一个使用I2C通讯协议的EEPROM读写控制器,使用按键控制数据的写入或读出,并将读出数据显示在数码管上。结合实验目标,运用前面学到的设计方法和相关理论知识,我们开始实验工程的设计。

首先,实验目标要求要使用I2C通讯协议,那么工程中要包含一个I2C驱动控制模块;其次,使用按键控制数据读/写,并要求将读出数据显示到数码管上,我们可以直接调用前面章节的按键消抖模块和数码管动态显示模块;再次,我们需要设计一个数据收发模块控制数据的收发;最后,需要顶层模块将各子功能模块例化起来,连接个 功能模块对应信号。综上所述,实验工程整体框图,具体见图 47‑16;模块功能简介,具体见表格 47‑1。

I2CEEP017

图 47‑16 EEPROM字节读写整体框图

表格 47‑1 模块功能简介

模块名称

功能描述

key_filter

按键消抖模块,将物理按键传入的读/写触发信号作消抖处理

i2c_rw_data

数据收发模块,生成eeprom待写入数据,暂存eeprom读出数据

i2c_ctrl

I2C驱动模块,按照I2C协议对I2C设备进行数据读写操作

seg_595_dynamic

数码管动态显示模块,显示读出eeprom的数据

eeprom_byte_rd_wr

顶层模块,实例化各子功能模块,连接各模块对应信号

结合图表,简述一下本实验工程的具体流程。

按下数据写操作按键,写触发信号传入按键消抖模块(key_filter),经消抖处理后的写触发信号传入数据收发模块(i2c_rw_data),模块接收到有效的写触发信号后,生成写使能信号、待写入数据、数据地址传入I2C驱动模块(i2c_ctrl),I2C驱动模块按照I2C协议将数据写入EEPROM存储 芯片;

数据写入完成后,按下数据读操作按键,读触发信号传入按键消抖模块(key_filter),经消抖处理后的读触发信号传入数据收发模块(i2c_rw_data),模块接收到有效的读触发信号后,生成读使能信号、数据地址传入I2C驱动模块(i2c_ctrl),I2C驱动模块自EEPROM存储芯片读取数据,将读 取到的数据回传给数据收发模块(i2c_rw_data),数据收发模块将数据暂存,待所有数据均读取完成后,将数据传至数码管动态显示模块(seg_595_dynamic),自EEPROM中读取的数据在数码管显示出来。

经过本小节的讲解,相信读者对本实验工程的整体框架有了简单了解,接下来我们对实验工程的各子功能模块分别进行详细讲解,帮助更加深入理解实验工程。

18.2.1.1. I2C驱动模块

模块框图

I2C驱动模块的主要功能是按照I2C协议对EERPROM存储芯片执行数据读写操作。I2C驱动模块框图和输入输出端口简介,具体见图 47‑17、表格 47‑2。

I2CEEP018

图 47‑17 I2C驱动模块框图

表格 47‑2 I2C驱动模块输入输出信号简介

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

wr_en

1Bit

Input

写使能信号

rd_en

1Bit

Input

读使能信号

i2c_start

1Bit

Input

单字节数据读/写开始信号

addr_num

1Bit

Input

数据存储地址字节数标志信号

byte_addr

16Bit

Input

数据存储地址

wr_data

8Bit

Input

待写入EEPROM字节数据

i2c_clk

1Bit

Output

工作时钟

i2c_end

1Bit

Output

单字节数据读/写结束信号

rd_data

8Bit

Output

自EEPROM中读出的单字节数据

i2c_scl

1Bit

Output

I2C串行时钟信号SCL

i2c_sda

1Bit

Output

I2C串行数据信号SDA

由图表可知,I2C驱动模块包括13路输入输出信号,其中输入信号8路、输出信号5路。输入信号中,sys_clk、sys_rst_n是必不可少的系统时钟和复位信号;wr_en、rd_en为写使能信号,由数据收发模块生成并传入,高电平有效;i2c_start信号为单字节数据读/写开始信号;与i2c_sta rt信号同时传入的还有数据存储地址byte_addr和待写入字节数据wr_data;当写使能wr_en和i2c_start信号同时有效,模块执行单字节数据写操作,按照数据存储地址byte_addr,向EEPROM对应地址写入数据wr_data;当读使能信号rd_en和i2c_start信号同时有效, 模块执行单字节数据读操作,按照数据存储地址byte_addr读取EEPROM对应地址中的数据;前文中我们提到, I2C设备存储地址有单字节和2字节两种,为了应对这一情况,我们向模块输入addr_num信号,当信号为低电平时,表示I2C设备存储地址为单字节,在进行数据读写操作时只写入数据存储地址byt e_addr的低8位;当信号为高电平时,表示I2C设备存储地址为2字节,在进行数据读写操作时要写入数据存储地址byte_addr的全部16位。

输出信号中,i2c_clk是本模块的工作时钟,由系统时钟sys_clk分频而来,它的时钟频率为串行时钟i2c_scl频率的4倍,时钟信号i2c_clk要传入数据收发模块(i2c_rw_data)作为模块的工作时钟;输出给数据收发模块(i2c_rw_data)的单字节数据读/写结束信号i2c_end, 高电平有效,表示一次单字节数据读/写操作完成;rd_data信号表示自EEPROM读出的单字节单字节数据,输出至数据收发模块(i2c_rw_data);i2c_scl、i2c_sda分别是串行时钟信号和串行数据信号,由模块产生传入EEPROM存储芯片。

注:对EERPROM的数据读写操作均使用单字节读/写操作,即每次操作只读/写单字节数据;若想要实现数据的连续读/写,可持续拉高读/写使能rd_en/wr_en,并输入有效的单字节数据读/写开始信号i2c_start即可。

波形图绘制

在“模块框图”小节,我们结合图表对I2C驱动模块的具体功能和输入输出端口做了说明。那么如何利用输入信号实现模块功能,并输出正确信号呢?在本小节,我们会通过绘制模块波形图,对模块功能以及各信号波形的设计与实现作出详细讲解。

在绘制波形图之前我们回想一下前面讲到的I2C设备单字节写操作和随机读操作的操作流程,结合前面学到的知识,我们发现使用状态机来实现I2C设备的读/写操作是十分方便的。参照I2C设备单字节写操作和随机读操作的操作流程,我们绘制I2C读/写操作状态转移图如下。

I2CEEP019

图 47‑18 I2C读/写操作状态转移图

有图可知,状态机中共包含16个状态,将单字节写操作和随机读操作相结合,可以实现I2C设备单字节写操作和随机读操作的状态跳转。

系统上电后,状态机处于IDLE(初始状态),接收到有效的单字节数据读/写开始信号i2c_start后,状态机跳转到START_1(起始状态);FPGA向EEPROM存储芯片发送起始信号;随后状态机跳转到SEND_D_ADDR(发送器件地址状态),在此状态下向EEPROM存储芯片写入控制指令,控制指令 高7位为器件地址,最低位为读写控制字,写入“0”,表示执行写操作;控制指令写入完毕后,状态机跳转到ACK_1(应答状态)。

在ACK_1(应答状态)状态下,要根据存储地址字节数进行不同状态的跳转。当FPGA接收到EEPROM回传的应答信号且存储地址字节为2字节,状态机跳转到SEND_B_ADDR_H(发送高字节地址状态),将存储地址的高8位写入EEPROM,写入完成后,状态机跳转到ACK_2(应答状态);FPGA接收到应 答信号后,状态机跳转到SEND_B_ADDR_L(发送低字节地址状态);当FPGA接收到EEPROM回传的应答信号且存储地址字节为单字节,状态机状态机直接跳转到SEND_B_ADDR_L(发送低字节地址状态);在此状态低8位存储地址或单字节存储地址写入完成后,状态机跳转到ACK_3(应答状态)。

在ACK_3(应答状态)状态下,要根据读/写使能信号做不同的状态跳转。当FPGA接收到应答信号且写使能信号有效,状态机跳转到WR_DATA(写数据状态);在写数据状态,向EEPROM写入单字节数据后,状态机跳转到ACK_4(应答状态);待FPGA接收到有效应答信号后,状态机跳转到STOP(停止状态) ;当FPGA接收到应答信号且读使能信号有效,状态机跳转到START_2(起始状态);再次向EEPROM写入起始信号,状态跳转到SEND_RD_ADDR(发送读控制状态);再次向EEPROM写入控制字节,高7位器件地址不变,读写控制位写入“1”,表示进行读操作,控制字节写入完毕后,状态机跳转到ACK_ 5(应答状态);待FPGA接收到有效应答信号后,状态机跳转到RD_DATA(读数据状态);在RD_DATA(读数据状态)状态,EEPROM向FPGA发送存储地址对应存储单元下的单字节数据,待数据读取完成户,状态机跳转到N_ACK(无应答状态),在此状态下向EEPROM写入一个时钟的高电平,表示数据读 取完成,随后状态机跳转到STOP(停止状态)。

在STOP(停止状态)状态,FPGA向EEPROM发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回IDLE(初始状态),等待下一次单字节数据读/写开始信号i2c_start。

使用状态机实现I2C驱动模块功能是模块的大体思路,结合前面讲解的I2C通讯协议的相关知识和相关设计方法,我们开始模块波形图的绘制。I2C驱动模块整体波形图,具体见图 47‑19、图 47‑20、图 47‑21、图 47‑22、图 47‑23。

I2CEEP020

图 47‑19 单字节写操作局部波形图(一)

I2CEEP021

图 47‑20 单字节写操作局部波形图(二)

I2CEEP022

图 47‑21 随机读操作局部波形图(一)

I2CEEP023

图 47‑22 随机读操作局部波形图(二)

I2CEEP024

图 47‑23 随机读操作局部波形图(三)

由于篇幅原因,我们将波形图分开展示,为了便于理解,我们将单字节写操作和随机读操作读操作分开讲解,对于各信号波形的设计与实现进行详细说明。

首先,先来看一下单字节写操作。

第一部分:输入信号说明

本模块的输入信号有8路,其中7路信号与单字节写操作有关。系统时钟信号sys_clk和复位信号sys_rst_n不必多说,这是模块正常工作必不可少的;写使能信号wr_en、 单字节数据读/写开始信号i2c_start,只有在两信号同时有效时,模块才会执行单字节数据写操作,若wr_en有效时,i2c_s tart信号n次有效输入,可以实现n个字节的连续写操作;addr_num信号为存储地址字节数标志信号,赋值为0时,表示I2C设备存储地址为单字节,赋值为1时,表示2C设备存储地址为2字节,本实验使用的EEPROM存储芯片的存储地址位2字节,此信号恒为高电平;信号byte_addr为存储地址;wr_d ata表示要写入该地址的单字节数据。

第二部分:时钟信号计数器cnt_clk和输出信号i2c_clk的设计与实现

本实验对EEPROM读写操作的串行时钟scl的频率为250KHz,且只在数据读写操作时时钟信号才有效,其他时刻scl始终保持高电平。若直接使用系统时钟生成串行时钟scl,计数器要设置较大的位宽,较为麻烦,我们这里先将系统时钟分频为频率较小的时钟,在使用新分频的时钟来生成串行时钟scl。

所以,在这里声明一个新的计数器cnt_clk对系统时钟sys_clk进行计数,利用计数器cnt_clk生成新的时钟i2c_clk。

串行时钟scl的时钟频率为250KHz,我们要生成的新时钟i2c_clk的频率要是scl的4倍,之所以这样是为了后面更好的生成scl和sda,所以i2c_clk的时钟频率为1MHz。经计算,cnt_clk要在0-24内循环计数,每个系统时钟周期自加1;cnt_clk每计完一个周期,i2c_clk进行 一次取反,最后得到i2c_clk为频率1MHz的时钟,本模块中其他信号的生成都以此信号为同步时钟。两信号波形图如下。

I2CEEP025

图 47‑24 cnt_clk、i2c_clk 信号波形图

注:由于系统时钟sys_clk与时钟i2c_clk时钟频率相差较大,sys_clk信号用虚线表示。

第三部分:状态机相关信号波形的设计与实现

前文理论部分提到,输出至EEPROM的串行时钟scl与串行数据sda只有在进行数据读写操作时有效,其他时刻始终保持高电平。由前文状态机相关讲解可知,除IDLE(初始状态)状态之外的其他状态均属于数据读写操作的有效部分,所以声明一个使能信号cnt_i2c_clk_en,在除IDLE(初始状态)状态之外 的其他状态保持有效高电平,作为I2C数据读写操作使能信号。

我们使用50MHz系统时钟生成了1MHz时钟i2c_clk,但输出至EEPROM的串行时钟scl的时钟频率为250KHz,我们声明时钟信号计数器cnt_i2c_clk,作为分频计数器,对时钟i2c_clk时钟信号进行计数,初值为0,计数范围为0-3,计数时钟为i2c_clk时钟,每个时钟周期自加1, 实现时钟i2c_clk信号的4分频,生成串行时钟scl。同时计数器cnt_i2c_clk也可作为生成串行数据sda的约束条件,以及状态机跳转条件。

计数器cnt_i2c_clk循环计数一个周期,对应串行时钟scl的1个时钟周期以及串行数据sda的1位数据保持时间,进行数据读写操作时,传输的指令、地址以及数据,位宽为固定的8位数据,我们声明一个比特计数器cnt_bit,对计数器cnt_i2c_clk的计数周期进行计数,可以辅助串行数据sda的生成 ,同时作为状态机状态跳转的约束条件。

输出的串行数据sda作为一个双向端口,主机通过它向从机发送控制指令、地址以及数据,接收从机回传的应答信号和读取数据。回传给主机的应答信号是实现状态机跳转的条件之一。声明信号sda_in作为串行数据sda 缓存,声明ack信号作为应答信号,ack信号只在状态机处于各应答状态时由sda_in信号赋值,此时为从机回传的应答信号,其他状态时钟保持高电平。

状态机状态跳转的各约束条件均已介绍完毕,声明状态变量state,结合各约束信号,单字节写操作状态机跳转流程如下:

系统上电后,状态机处于IDLE(初始状态),接收到有效的单字节数据读/写开始信号i2c_start后,状态机跳转到START_1(起始状态),同时使能信号cnt_i2c_clk_en拉高、计数器cnt_i2c_clk、cnt_bit开始计数,开始数据读写操作;

在START_1(起始状态)状态保持一个串行时钟周期,期间FPGA向EEPROM存储芯片发送起始信号,一个时钟周期过后,计数器cnt_ i2c_clk完成一个周期计数,计数器cnt_ i2c_clk计数到最大值3,状态机跳转到SEND_D_ADDR(发送器件地址状态);

计数器cnt_i2c_clk、cnt_bit同时归0,重新计数,计数器cnt_i2c_clk每计完一个周期,cnt_bit自加1,当计数器cnt_i2c_clk完成8个计数周期后,cnt_bit计数到7,实现8个比特计数,器件FPGA按照时序向EEPROM存储芯片写入控制指令,控制指令高7位为器件地 址,最低位为读写控制字,写入“0”,表示执行写操作。当计数器cnt_ i2c_clk计数到最大值3、cnt_bit计数到7,两计数器同时归0,状态机跳转到转到ACK_1(应答状态);

在ACK_1(应答状态)状态下,计数器cnt_i2c_clk、cnt_bit重新计数,当计数器cnt_ i2c_clk计数到最大值3,且应答信号ack为有效的低电平,状态机跳转到SEND_B_ADDR_H(发送高字节地址状态),两计数器清0;

此状态下,FPGA将存储地址的高8位按时序写入EEPROM,当计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,状态机跳转到ACK_2(应答状态), 两计数器清0;

ACK_2状态下,当计数器cnt_ i2c_clk计数到3,且应答信号ack为有效的低电平,状态机跳转到SEND_B_ADDR_L(发送低字节地址状态) ,两计数器清0;

在此状态下,低8位存储地址按时序写入EEPROM,计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,状态机跳转到ACK_3(应答状态);

在ACK_3(应答状态)状态下,当cnt_ i2c_clk计数3、应答信号ack有效,且写使能信号wr_en有效,状态机跳转到WR_DATA(写数据状态);

在写数据状态,按时序向EEPROM写入单字节数据,计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,状态机跳转到ACK_4(应答状态);

在ACK_4(应答状态)状态下,当cnt_ i2c_clk计数3、应答信号ack有效,状态机跳转到STOP(停止状态)状态;

在STOP(停止状态)状态,FPGA向EEPROM发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回IDLE(初始状态),等待下一次单字节数据读/写开始信号i2c_start。

状态机相关信号波形如下。

I2CEEP026

图 47‑25 状态机相关信号波形图(一)

I2CEEP027

图 47‑26 状态机相关信号波形图(二)

第四部分:输出串行时钟i2c_scl、串行数据信号i2c_sda及相关信号的波形设计与实现

串口数据sda端口作为一个双向端口,在单字节读取操作中,主机只在除应答状态之外的其他状态拥有它的控制权,在应答状态下主机只能接收由从机通过sda传入的应答信号。声明使能信号sda_en,只在除应答状态之外的其他状态赋值为有效的高电平,sda_en有效时,主机拥有对sda的控制权。

声明i2c_sda_reg作为输出i2c_sda信号的数据缓存,在sda_en有效时,将i2c_sda_reg的值赋值给输出串口数据i2c_sda,sda_en无效时,输出串口数据i2c_sda为高阻态,主机放弃其控制权,接收其传入的应答信号。

i2c_sda_reg在使能信号sda_en无效时始终保持高电平,在使能sda_en有效时,在状态机对应状态下,以计数器cnt_ i2c_clk、cnt_bit为约束条件,对应写入起始信号、控制指令、存储地址、写入数据、停止信号。

对于输出的串行时钟i2c_clk,由I2C通讯协议可知,I2C设备只在串行时钟为高电平时进行数据采集,在串行时钟低电平时实现串行数据更新。我们使用计数器cnt_ i2c_clk、cnt_bit以及状态变量state为约束条件,结合I2C通讯协议,生成满足时序要求的输出串行时钟i2c_clk。

输出串行时钟i2c_scl、串行数据信号i2c_sda及相关信号的波形图如下。

I2CEEP028

图 47‑27 i2c_scl、i2c_sda及相关信号波形图(一)

I2CEEP029

图 47‑28 i2c_scl、i2c_sda及相关信号波形图(二)

单字节写操作部分涉及的各信号波形的设计与实现讲解完毕,下面开始随机读操作部分的讲解。单字节写操作和随机读操作所涉及的各信号大体相同,在随机读操作,我们只讲解差别较大之处,两操作相同或相似之处不再说明,读者可回顾单字节写操作部分的介绍。

第一部分:输入信号说明

本模块的输入信号有8路,其中6路信号与随机读操作有关。系统时钟信号sys_clk和复位信号sys_rst_n不必多说,这是模块正常工作必不可少的;读使能信号rd_en、 单字节数据读/写开始信号i2c_start,只有在两信号同时有效时,模块才会执行随机读操作,若rd_en有效时,i2c_start 信号n次有效输入,可以实现n个字节的连续读操作;addr_num信号为存储地址字节数标志信号,赋值为0时,表示I2C设备存储地址为单字节,赋值为1时,表示2C设备存储地址为2字节,本实验使用的EEPROM存储芯片的存储地址位2字节,此信号恒为高电平;信号byte_addr为存储地址。

第二部分:状态机相关信号波形的设计与实现

状态机状态跳转的各约束条件,读者可回顾单字节写操作部分介绍。声明状态变量state,结合各约束信号,单字节写操作状态机跳转流程如下:

系统上电后,状态机处于IDLE(初始状态),接收到有效的单字节数据读/写开始信号i2c_start后,状态机跳转到START_1(起始状态),同时使能信号cnt_i2c_clk_en拉高、计数器cnt_i2c_clk、cnt_bit开始计数,开始数据读写操作;

在START_1(起始状态)状态保持一个串行时钟周期,期间FPGA向EEPROM存储芯片发送起始信号,一个时钟周期过后,计数器cnt_ i2c_clk完成一个周期计数,计数器cnt_ i2c_clk计数到最大值3,状态机跳转到SEND_D_ADDR(发送器件地址状态);

计数器cnt_i2c_clk、cnt_bit同时归0,重新计数,计数器cnt_i2c_clk每计完一个周期,cnt_bit自加1,当计数器cnt_i2c_clk完成8个计数周期后,cnt_bit计数到7,实现8个比特计数,器件FPGA按照时序向EEPROM存储芯片写入控制指令,控制指令高7位为器件地 址,最低位为读写控制字,写入“0”,表示执行写操作。当计数器cnt_ i2c_clk计数到最大值3、cnt_bit计数到7,两计数器同时归0,状态机跳转到转到ACK_1(应答状态);

在ACK_1(应答状态)状态下,计数器cnt_i2c_clk、cnt_bit重新计数,当计数器cnt_ i2c_clk计数到最大值3,且应答信号ack为有效的低电平,状态机跳转到SEND_B_ADDR_H(发送高字节地址状态),两计数器清0;

此状态下,FPGA将存储地址的高8位按时序写入EEPROM,当计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,状态机跳转到ACK_2(应答状态), 两计数器清0;

ACK_2状态下,当计数器cnt_ i2c_clk计数到3,且应答信号ack为有效的低电平,状态机跳转到SEND_B_ADDR_L(发送低字节地址状态) ,两计数器清0;

在此状态下,低8位存储地址按时序写入EEPROM,计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,状态机跳转到ACK_3(应答状态);

在ACK_3(应答状态)状态下,当cnt_ i2c_clk计数3、应答信号ack有效,且读使能信号rd_en有效,状态机跳转到START_2(起始状态);

在START_2(起始状态)状态保持一个串行时钟周期,期间FPGA再次向EEPROM存储芯片发送起始信号,一个时钟周期过后,计数器cnt_ i2c_clk完成一个周期计数,计数器cnt_ i2c_clk计数到3,状态机跳转到SEND_RD_ADDR(发送读控制状态);

在此状态下,按时序向EEPROM写入控制指令,控制指令高7位为器件地址,最低位为读写控制字,写入“1”,表示执行读操作。当计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,两计数器同时归0,状态机跳转到ACK_5(应答状态);

在ACK_5(应答状态)状态下,当cnt_ i2c_clk计数3、应答信号ack有效,状态机跳转到RD_DATA(读数据状态);读数据状态下,主机读取从机发送的单字节数据,当计数器cnt_ i2c_clk计数到3、cnt_bit计数到7,数据读取完成,计数器清0,状态机跳转到N_ACK(非应答状态);在非应答状态下,向EEPROM写入一个时钟的高电平,当cnt_ i2c_clk计数3,状态机跳转到STOP(停止状态)。

在STOP(停止状态)状态,FPGA向EEPROM发送停止信号,一次随机数据读操作完成,随后状态机跳回IDLE(初始状态),等待下一次单字节数据读/写开始信号i2c_start。

状态机相关信号波形如下。

I2CEEP030

图 47‑29 状态机相关信号波形图(一)

I2CEEP031

图 47‑30 状态机相关信号波形图(二)

I2CEEP032

图 47‑31 状态机相关信号波形图(三)

第三部分:输出串行时钟i2c_scl、串行数据信号i2c_sda、读出数据rd_data及相关信号的波形设计与实现

串口数据sda端口作为一个双向端口,在随机读操作中,主机只在除应答状态、读数据状态之外的其他状态拥有它的控制权,在应答状态下主机接收由从机通过sda传入的应答信号,在读数据状态下主机接收由从机传入的单字节数据。声明使能信号sda_en,只在除应答状态、读数据状态之外的其他状态赋值为有效的高电平,sd a_en有效时,主机拥有对sda的控制权。

声明i2c_sda_reg作为输出i2c_sda信号的数据缓存;声明rd_data_reg作为EEPROM读出数据缓存。

i2c_sda_reg在使能信号sda_en无效时始终保持高电平,在使能sda_en有效时,在状态机对应状态下,以计数器cnt_ i2c_clk、cnt_bit为约束条件,对应写入起始信号、控制指令、存储地址、写入数据、停止信号;在状态机处于读数据状态时,变量rd_data_reg由输入信号sda_in赋值,暂存EEPROM读取数据。

当sda_en有效时,将i2c_sda_reg赋值给i2c_sda;当sda_en无效时,i2c_sda保持高阻态。主机放弃对sda端口的控制;在状态机处于读数据状态时,变量rd_data_reg暂存EEPROM读取数据,读数据状态结束后,将暂存数据赋值给输出信号rd_data。

对于输出的串行时钟i2c_clk,由I2C通讯协议可知,I2C设备只在串行时钟为高电平时进行数据采集,在串行时钟低电平时实现串行数据更新。我们使用计数器cnt_ i2c_clk、cnt_bit以及状态变量state为约束条件,结合I2C通讯协议,生成满足时序要求的输出串行时钟i2c_clk。

输出串行时钟i2c_scl、串行数据信号i2c_sda、读出数据rd_data及相关信号的波形图如下。

I2CEEP033

图 47‑32 i2c_scl、i2c_sda、rd_data及相关信号波形图(一)

I2CEEP034

图 47‑33 i2c_scl、i2c_sda、rd_data及相关信号波形图(二)

I2CEEP035

图 47‑34 i2c_scl、i2c_sda、rd_data及相关信号波形图(三)

代码编写

参考波形图绘制完毕,参照参考波形图进行代码编写,I2C驱动模块参考代码,具体见代码清单 47‑1。

代码清单 47‑1 I2C驱动模块参考代码(i2c_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
module i2c_ctrl
#(
parameter DEVICE_ADDR = 7'b1010_000 , //i2c设备地址
parameter SYS_CLK_FREQ = 26'd50_000_000 , //输入系统时钟频率
parameter SCL_FREQ = 18'd250_000 //i2c设备scl时钟频率
)
(
input wire sys_clk , //输入系统时钟,50MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire wr_en , //输入写使能信号
input wire rd_en , //输入读使能信号
input wire i2c_start , //输入i2c触发信号
input wire addr_num , //输入i2c字节地址字节数
input wire [15:0] byte_addr , //输入i2c字节地址
input wire [7:0] wr_data , //输入i2c设备数据
output reg i2c_clk , //i2c驱动时钟
output reg i2c_end , //i2c一次读/写操作完成
output reg [7:0] rd_data , //输出i2c设备读取数据
output reg i2c_scl , //输出至i2c设备的串行时钟信号scl
inout wire i2c_sda //输出至i2c设备的串行数据信号sda
);
////
//\* Parameter and Internal Signal \//
////
// parameter define
parameter CNT_CLK_MAX = (SYS_CLK_FREQ/SCL_FREQ) >> 2'd3 ;
//cnt_clk计数器计数最大值
parameter CNT_START_MAX = 8'd100; //cnt_start计数器计数最大值
parameter IDLE = 4'd00, //初始状态
START_1 = 4'd01, //开始状态1
SEND_D_ADDR = 4'd02, //设备地址写入状态 + 控制写
ACK_1 = 4'd03, //应答状态1
SEND_B_ADDR_H = 4'd04, //字节地址高八位写入状态
ACK_2 = 4'd05, //应答状态2
SEND_B_ADDR_L = 4'd06, //字节地址低八位写入状态
ACK_3 = 4'd07, //应答状态3
WR_DATA = 4'd08, //写数据状态
ACK_4 = 4'd09, //应答状态4
START_2 = 4'd10, //开始状态2
SEND_RD_ADDR = 4'd11, //设备地址写入状态 + 控制读
ACK_5 = 4'd12, //应答状态5
RD_DATA = 4'd13, //读数据状态
N_ACK = 4'd14, //非应答状态
STOP = 4'd15; //结束状态
// wire define
wire sda_in ; //sda输入数据寄存
wire sda_en ; //sda数据写入使能信号
// reg define
reg [7:0] cnt_clk ; //系统时钟计数器,控制生成clk_i2c时钟信号
reg [3:0] state ; //状态机状态
reg cnt_i2c_clk_en ; //cnt_i2c_clk计数器使能信号
reg [1:0] cnt_i2c_clk ; //clk_i2c时钟计数器,控制生成cnt_bit信号
reg [2:0] cnt_bit ; //sda比特计数器
reg ack ; //应答信号
reg i2c_sda_reg ; //sda数据缓存
reg [7:0] rd_data_reg ; //自i2c设备读出数据
////
//\* Main Code \//
////
// cnt_clk:系统时钟计数器,控制生成clk_i2c时钟信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 8'd0;
else if(cnt_clk == CNT_CLK_MAX - 1'b1)
cnt_clk <= 8'd0;
else
cnt_clk <= cnt_clk + 1'b1;
// i2c_clk:i2c驱动时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
i2c_clk <= 1'b1;
else if(cnt_clk == CNT_CLK_MAX - 1'b1)
i2c_clk <= ~i2c_clk;
// cnt_i2c_clk_en:cnt_i2c_clk计数器使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_i2c_clk_en <= 1'b0;
else if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
cnt_i2c_clk_en <= 1'b0;
else if(i2c_start == 1'b1)
cnt_i2c_clk_en <= 1'b1;
// cnt_i2c_clk:i2c_clk时钟计数器,控制生成cnt_bit信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_i2c_clk <= 2'd0;
else if(cnt_i2c_clk_en == 1'b1)
cnt_i2c_clk <= cnt_i2c_clk + 1'b1;
// cnt_bit:sda比特计数器
always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_bit <= 3'd0;
 else if((state == IDLE) \|\| (state == START_1) \|\| (state == START_2)
 \|\| (state == ACK_1) \|\| (state == ACK_2) \|\| (state == ACK_3)
 \|\| (state == ACK_4) \|\| (state == ACK_5) \|\| (state == N_ACK))
 cnt_bit <= 3'd0;
 else if((cnt_bit == 3'd7) && (cnt_i2c_clk == 2'd3))
 cnt_bit <= 3'd0;
 else if((cnt_i2c_clk == 2'd3) && (state != IDLE))
 cnt_bit <= cnt_bit + 1'b1;

 // state:状态机状态跳转
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 state <= IDLE;
 else case(state)
 IDLE:
 if(i2c_start == 1'b1)
 state <= START_1;
 else
 state <= state;
 START_1:
 if(cnt_i2c_clk == 3)
 state <= SEND_D_ADDR;
 else
 state <= state;
 SEND_D_ADDR:
 if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
 state <= ACK_1;
 else
 state <= state;
 ACK_1:
 if((cnt_i2c_clk == 3) && (ack == 1'b0))
 begin
 if(addr_num == 1'b1)
 state <= SEND_B_ADDR_H;
 else
 state <= SEND_B_ADDR_L;
 end
 else
 state <= state;
 SEND_B_ADDR_H:
 if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
 state <= ACK_2;
 else
 state <= state;
 ACK_2:
 if((cnt_i2c_clk == 3) && (ack == 1'b0))
 state <= SEND_B_ADDR_L;
 else
 state <= state;
 SEND_B_ADDR_L:
 if((cnt_bit == 3'd7) && (cnt_i2c_clk == 3))
 state <= ACK_3;
 else
 state <= state;
 ACK_3:
 if((cnt_i2c_clk == 3) && (ack == 1'b0))
 begin
 if(wr_en == 1'b1)
 state <= WR_DATA;
 else if(rd_en == 1'b1)
 state <= START_2;
 else
 state <= state;
 end
 else
 state <= state;
 WR_DATA:
 if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
 state <= ACK_4;
 else
 state <= state;
 ACK_4:
 if((cnt_i2c_clk == 3) && (ack == 1'b0))
 state <= STOP;
 else
 state <= state;
 START_2:
 if(cnt_i2c_clk == 3)
 state <= SEND_RD_ADDR;
 else
 state <= state;
 SEND_RD_ADDR:
 if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
 state <= ACK_5;
 else
 state <= state;
 ACK_5:
 if((cnt_i2c_clk == 3) && (ack == 1'b0))
 state <= RD_DATA;
 else
 state <= state;
 RD_DATA:
 if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
 state <= N_ACK;
 else
 state <= state;
 N_ACK:
 if(cnt_i2c_clk == 3)
 state <= STOP;
 else
 state <= state;
 STOP:
 if((cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
 state <= IDLE;
 else
 state <= state;
 default: state <= IDLE;
 endcase

 // ack:应答信号
 always@(*)
 case (state)
 IDLE,START_1,SEND_D_ADDR,SEND_B_ADDR_H,SEND_B_ADDR_L,
 WR_DATA,START_2,SEND_RD_ADDR,RD_DATA,N_ACK:
 ack <= 1'b1;
 ACK_1,ACK_2,ACK_3,ACK_4,ACK_5:
 if(cnt_i2c_clk == 2'd0)
 ack <= sda_in;
 else
 ack <= ack;
 default: ack <= 1'b1;
 endcase

 // i2c_scl:输出至i2c设备的串行时钟信号scl
 always@(*)
 case (state)
 IDLE:
 i2c_scl <= 1'b1;
 START_1:
 if(cnt_i2c_clk == 2'd3)
 i2c_scl <= 1'b0;
 else
 i2c_scl <= 1'b1;
 SEND_D_ADDR,ACK_1,SEND_B_ADDR_H,ACK_2,SEND_B_ADDR_L,
 ACK_3,WR_DATA,ACK_4,START_2,SEND_RD_ADDR,ACK_5,RD_DATA,N_ACK:
 if((cnt_i2c_clk == 2'd1) \|\| (cnt_i2c_clk == 2'd2))
 i2c_scl <= 1'b1;
 else
 i2c_scl <= 1'b0;
 STOP:
 if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0))
 i2c_scl <= 1'b0;
 else
 i2c_scl <= 1'b1;
 default: i2c_scl <= 1'b1;
 endcase

 // i2c_sda_reg:sda数据缓存
 always@(*)
 case (state)
 IDLE:
 begin
 i2c_sda_reg <= 1'b1;
 rd_data_reg <= 8'd0;
 end
 START_1:
 if(cnt_i2c_clk <= 2'd0)
 i2c_sda_reg <= 1'b1;
 else
 i2c_sda_reg <= 1'b0;
 SEND_D_ADDR:
 if(cnt_bit <= 3'd6)
 i2c_sda_reg <= DEVICE_ADDR[6 - cnt_bit];
 else
 i2c_sda_reg <= 1'b0;
 ACK_1:
 i2c_sda_reg <= 1'b1;
 SEND_B_ADDR_H:
 i2c_sda_reg <= byte_addr[15 - cnt_bit];
 ACK_2:
 i2c_sda_reg <= 1'b1;
 SEND_B_ADDR_L:
 i2c_sda_reg <= byte_addr[7 - cnt_bit];
 ACK_3:
 i2c_sda_reg <= 1'b1;
 WR_DATA:
 i2c_sda_reg <= wr_data[7 - cnt_bit];
 ACK_4:
 i2c_sda_reg <= 1'b1;
 START_2:
 if(cnt_i2c_clk <= 2'd1)
 i2c_sda_reg <= 1'b1;
 else
 i2c_sda_reg <= 1'b0;
 SEND_RD_ADDR:
 if(cnt_bit <= 3'd6)
 i2c_sda_reg <= DEVICE_ADDR[6 - cnt_bit];
 else
 i2c_sda_reg <= 1'b1;
 ACK_5:
 i2c_sda_reg <= 1'b1;
 RD_DATA:
 if(cnt_i2c_clk == 2'd2)
 rd_data_reg[7 - cnt_bit] <= sda_in;
 else
 rd_data_reg <= rd_data_reg;
 N_ACK:
 i2c_sda_reg <= 1'b1;
 STOP:
 if((cnt_bit == 3'd0) && (cnt_i2c_clk < 2'd3))
 i2c_sda_reg <= 1'b0;
 else
 i2c_sda_reg <= 1'b1;
 default:
 begin
 i2c_sda_reg <= 1'b1;
 rd_data_reg <= rd_data_reg;
 end
 endcase

 // rd_data:自i2c设备读出数据
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_data <= 8'd0;
 else if((state == RD_DATA)&&(cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
 rd_data <= rd_data_reg;

 // i2c_end:一次读/写结束信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 i2c_end <= 1'b0;
 else if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
 i2c_end <= 1'b1;
 else
 i2c_end <= 1'b0;

 // sda_in:sda输入数据寄存
 assign sda_in = i2c_sda;
 // sda_en:sda数据写入使能信号
 assign sda_en = ((state == RD_DATA)||(state == ACK_1)||(state == ACK_2)
 \|\| (state == ACK_3)||(state == ACK_4)||(state == ACK_5))
 ? 1'b0 : 1'b1;
 // i2c_sda:输出至i2c设备的串行数据信号sda
 assign i2c_sda = (sda_en == 1'b1) ? i2c_sda_reg : 1'bz;

 endmodule

参考代码编写完成,代码是参照上一小节绘制波形图编写而成,其中各信号已在波形图绘制小节做了详细说明,此处不再赘述。

代码中有一处,读者要注意,代码第28行。由前文可知,我们要使用50MHz系统时钟生成1MHz的i2c_clk时钟信号,输出的串行时钟i2c_scl时钟频率为250KHz,为了便于串行时钟i2c_scl的生成和串行数据i2c_sda的写入,生成的i2c_clk时钟信号要与串行时钟i2c_scl时钟频率 保持4倍关系。经计算,生成i2c_clk时钟信号的计数器cnt_clk一个循环周期计数25次满足要求,但此处计数器cnt_clk计数最大值CNT_CLK_MAX并未直接赋值,而是使用公式赋值。

28 parameter CNT_CLK_MAX = (SYS_CLK_FREQ/SCL_FREQ) >> 2’d3 ;

这是为了提高I2C驱动模块的复用性,参数SYS_CLK_FREQ表示系统时钟sys_clk时钟频率,参数SCL_FREQ表示输出串行时钟i2c_scl时钟频率;两参数做除法运算,结果右移一位结表示除2,得到的结果用于分频计数器计数最大值,可直接由系统时钟分频产生串行时钟i2c_scl时钟信号;结果继 续右移两位表示除4,作为分频计数器计数最大值,可产生时钟信号i2c_clk,时钟频率为串行时钟i2c_scl时钟频率的4倍。

这样一来,只要设置好系统时钟与串行时钟的时钟频率,本模块即可在多种时钟频率下使用,复用性大大提高。

绘制完成波形图,代码编写完毕,对于模块的仿真验证我们等到其他模块介绍完毕后,对整个实验工程进行整体仿真,到时候再对模块仿真波形进行具体分析。

18.2.1.2. 数据收发模块

模块框图

I2C驱动模块介绍完毕,我们开始数据收发模块的讲解。

数据收发模块的主要功能是:为I2C驱动模块提供读/写数据存储地址、待写入数据以及作为EEPROM读出数据缓存,待数据读取完成后将读出数据发送给数码管显示模块进行数据显示。数据收发模块框图及模块输入输出端口简介,具体见图 47‑35、表格 47‑3。

I2CEEP036

图 47‑35 数据收发模块框图

表格 47‑3 I2C数据收发模块输入输出信号简介

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

系统时钟50MHz

i2c_clk

1Bit

Input

模块工作时钟,频率1MHz

sys_rst_n

1Bit

Input

复位信号,低有效

write

1Bit

Input

写触发信号

read

1Bit

Input

读触发信号

i2c_end

1Bit

Input

单字节数据读/写结束信号

rd_data

8Bit

Input

EEPROM读出数据

wr_en

1Bit

Output

写使能信号

rd_en

1Bit

Output

读使能信号

i2c_start

1Bit

Output

单字节数据读/写开始信号

byte_addr

16Bit

Output

读/写数据存储地址

wr_data

8Bit

Output

待写入EEPROM数据

fifo_rd_data

8Bit

Output

数码管待显示数据

由图表可知,I2C驱动模块包括13路输入输出信号,其中输入信号7路、输出信号6路。

输入信号中,有2路时钟信号和1路复位信号,sys_clk为系统时钟信号,在数据收发模块中用于采集读/写触发信号read和write,2路触发信号均由外部按键输出,经消抖处理后传入本模块,消抖模块使用的时钟信号为与sys_clk相同的系统时钟,所以读/写触发信号的采集要使用系统时钟;i2c_clk为模 块工作时钟,由I2C驱动模块生成并传入,是存储地址、读/写数据以及使能信号的同步时钟,因为I2C模块的工作时钟为i2c_clk时钟信号,两模块工作时钟相同,不会出现时钟不同引起时序问题;复位信号sys_rst_n,低电平有效,不必多说;i2c_end为单字节数据读/写接数信号,由I2C驱动模块产生并 传入,告知数据生成模块单字节数据读/写操作完成。若连续读/写多字节数据,此信号可作为存储地址、写数据的更新标志;rd_data为I2C驱动模块传入的数据信号,表示由EEPROM读出的字节数据。

输出信号中, rd_en、wr_en分别为读写使能信号,生成后传入I2C驱动模块,作为I2C驱动模块读/写操作的判断标志;i2c_start是单字节数据读/写开始信号,作为I2C驱动模块单字节读/写操作开始的标志信号;byte_addr为读写数据存储地址;wr_data为待写入EEPROM的字节数据 ;fifo_rd_data为自EEPROM读出的字节数据,要发送到数码换显示模块在数码管显示出来。

注:数据收发模块内部实例化一个FIFO,将读出EEPROM的字节数据做暂存,待所有数据读取完成后,开始向数码管发送数据。例如本实验向EEPROM连续写入10个字节数据,随后将写入数据读出并在数码管显示,数据收发模块只有接收到读出的10个字节数据后,才会开始向数码管显示模块发送数据。

波形图绘制

在模块框图小节,对模块框图以及各输入输出信号做了简单介绍,接下来我们通过波形图的绘制,对模块各信号波形的设计与实现做详细说明。使用已知输入信号实现模块功能,并输出有效信号。数据收发模块整体波形图,具体见图 47‑36、图 47‑37、图 47‑38。

I2CEEP037

图 47‑36 数据收发模块写数据操作波形图

I2CEEP038

图 47‑37 数据收发模块读数据操作波形图

I2CEEP039

图 47‑38 数据收发模块显示数据发送波形图

由于篇幅原因,我们将数据收发模块波形图分3部分展示,分别为写数据操作部分、读数据操作部分和显示数据发送部分。接下来,我们分部分对各信号的设计与实现进行详细讲解。

首先,先来看一下写数据操作部分。对于模块的的输入信号,在模块框图小节已经做了详细说明,此处不再赘述。

第一部分:输出写使能信号wr_en及其相关信号波形的设计与实现

外部按键传入的写触发信号经消抖处理后传入本模块,该信号只保持一个有效时钟,且同步时钟为系统时钟sys_clk,模块工作时钟i2c_clk很难采集到该触发信号。我们需要延长该写使能触发信号的有效时间,使模块工作时钟i2c_clk可以采集到该触发信号。

声明计数器cnt_wr和写有效信号wr_valid两信号的同步时钟均为系统时钟sys_clk,当外部传入有效的写触发信号write,写有效信号wr_valid拉高,计数器cnt_wr来时计数,计数器计数到设定值(200)后归0,写有效信号拉低。计数器cnt_wr的计数设定值可自主设定,只要能使wr_ valid信号保持一个工作时钟周期高电平即可。计数器cnt_wr和写有效信号wr_valid波形图如下。

I2CEEP040

图 47‑39 cnt_wr、 wr_valid信号波形图

写有效信号wr_valid拉高后,工作时钟i2c_clk上升沿采集到wr_valid高电平,拉高写使能信号wr_en,告知I2C驱动模块接下来要进行数据写操作。在此次实验我们要连续写入10字节数据,所以写使能信号wr_en要保持10次数据写操作的有效时间,在这一时间段我们要输出10次有效的i2c_s tart信号,在接收到第10次i2c_end信号后,表示10字节数据均已写入完成,将写使能信号rw_en拉低,完成10字节数据的连续写入。

要实现这一操作我们需要声明2个变量,声明字节计数器wr_i2c_data_num对已写入字节进行计数;由数据手册可知,两次相邻的读/写操作之间需要一定的时间间隔,以保证上一次读/写操作完成,所以声明计数器cnt_start,对相邻读/写操作时间间隔进行计数。

采集到写有效信号wr_valid为高电平,拉高写使能信号wr_en,计数器cnt_wait、wr_i2c_data_num均由0开始计数,每一个工作时钟周期cnt_wait自加1,计数到最大值1499,i2c_start保持一个工作时钟的高电平,同时cnt_wait归0,重新开始计数;I2C驱动模块 接收到有效的i2c_start信号后,向EEPROM写入单字节数据,传回i2c_end信号,表示一次单字节写操作完毕,计数器wr_i2c_data_num加1;计数器cnt_start完成10次循环计数,i2c_start拉高10次,在接收到第10次有效的i2c_end信号后,表示连续10字节数据写 入完毕,将写使能信号wr_en拉低,写操作完毕。相关信号波形如下。

I2CEEP041

图 47‑40 wr_en及相关信号波形图

第二部分:输出存储地址byte_addr、写数据wr_data信号波形的设计与实现

既然是对EEPROM中兴写数据操作,存储地址和写数据必不可少,在本从实验中,向EEPROM中10个连续存储存储单元写入10字节数据。对输出存储地址byte_addr,赋值初始存储地址,当i2c_end信号有效时,地址加1,待10字节数据均写入完毕,再次赋值初始从从地址;对于写数据wr_data处理方 式相同,先赋值写数据初值,当i2c_end信号有效时,写数据加1 ,待10字节数据均写入完毕,在此赋值写数据初值。两输出信号波形如下。

I2CEEP042

图 47‑41 byte_addr、wr_data信号波形图

数据收发模块写操作部分介绍完毕,接下来介绍一下读操作部分各信号波形。

与写操作部分相同,外部按键传入的读触发信号经消抖处理后传入本模块,该信号只保持一个有效时钟,且同步时钟为系统时钟sys_clk,模块工作时钟i2c_clk很难采集到该触发信号。我们需要延长该读使能触发信号的有效时间,使模块工作时钟i2c_clk可以采集到该触发信号。处理方式和写操作方式相同,声明计数 器cnt_rd和读有效信号rd_valid两信号,延长读触发信号read有效时间,使i2c_clk时钟能采集到该读触发信号。具体方法参照写操作部分相关介绍,计数器cnt_rd和读有效信号rd_valid波形图如下。

I2CEEP043

图 47‑42 cnt_rd、 rd_valid信号波形图

对于读使能信号的处理方式也与写操作方式相同,工作时钟i2c_clk上升沿采集到有效rd_valid信号,拉高读使能信号rd_en,告知I2C驱动模块接下来要进行数据读操作。

声明字节计数器rd_i2c_data_num对已读出字节进行计数;使用之前声明的计数器cnt_start,对相邻读/写操作时间间隔进行计数。

采集到读有效信号rd_valid为高电平,拉高读使能信号rd_en,计数器cnt_wait、rd_i2c_data_num均由0开始计数,每一个工作时钟周期cnt_wait自加1,计数到最大值1499,i2c_start保持一个工作时钟的高电平,同时cnt_wait归0,重新开始计数;I2C驱动模块 接收到有效的i2c_start信号后,自EEPROM读出单字节数据,传回i2c_end信号,表示一次单字节写操作完毕,计数器rd_i2c_data_num加1;计数器cnt_start完成10次循环计数,i2c_start拉高10次,在接收到第10次有效的i2c_end信号后,表示连续10字节数据写 入完毕,将读使能信号rd_en拉低,读操作完毕。相关信号波形如下。

I2CEEP044

图 47‑43 rd_en及相关信号波形图

既然是数据读操作,自然有读出数据传入本模块,一次读操作连续读出10字节数据,先将读取的10字节数据暂存到内部例化的FIFO中,以传回的i2c_end结束信号为写使能,在i2c_clk时钟同步下将读出数据写入FIFO中。同时我们将FIFO的数据计数器引出,方便后续数据发送阶段的操作。相关信号波形图如下 。

I2CEEP045

图 47‑44 FIFO数据写入相关信号波形

对于存储地址信号byte_addr的讲解,读者参阅写操作部分相关介绍,此处不再赘述,接下来开始数据发送部分各信号波形的讲解。

等到读取的10字节均写入FIFO中,FIFO数据计数器data_num显示为10,表示FIFO中存有10字节读出数据。此时拉高FIFO读有效信号fifo_rd_valid,只有信号fifo_rd_valid为有效高电平,对FIFO的读操作才有效;fifo_rd_valid有效时,计数器cnt_wai t开始循环计数,声明此计数器的目的是计数字节数据读出时间间隔,间隔越长,每字节数据在数码管显示时间越长,方面现象观察;当计数器cnt_wait计数到最大值时,归0重新计数,FIFO读使能信号信号fifo_rd_en拉高一个时钟周期,自FIFO读出一个字节数据,由fifo_rd_data将数据传出给数 码管显示模块,读出字节计数器rd_data_num加1;等到10字节数据均读取并传出后,fifo_rd_valid信号拉低,数据发送操作完成。相关信号波形如下。

I2CEEP046

图 47‑45 数据发送操作相关信号波形图

代码编写

数据收发模块个信号波形介绍完毕,波形图绘制完成,参照波形图开始参考代码的编写。模块参考代码,具体见代码清单 47‑2。

代码清单 47‑2 数据收发模块参考代码(i2c_rd_data.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
module i2c_rw_data
(
input wire sys_clk , //输入系统时钟,频率50MHz
input wire i2c_clk , //输入i2c驱动时钟,频率1MHz
input wire sys_rst_n , //输入复位信号,低有效
input wire write , //输入写触发信号
input wire read , //输入读触发信号
input wire i2c_end , //一次i2c读/写结束信号
input wire [7:0] rd_data , //输入自i2c设备读出的数据

output reg wr_en , //输出写使能信号
output reg rd_en , //输出读使能信号
output reg i2c_start , //输出i2c读/写触发信号
output reg [15:0] byte_addr , //输出i2c设备读/写地址
output reg [7:0] wr_data , //输出写入i2c设备的数据
output wire [7:0] fifo_rd_data //输出自fifo中读出的数据
);
////
//\* Parameter and Internal Signal \//
////
// parameter define
parameter DATA_NUM = 8'd10 ,//读/写操作读出或写入的数据个数
CNT_START_MAX = 11'd1500 ,//cnt_start计数器计数最大值
CNT_WR_RD_MAX = 8'd200 ,//cnt_wr/cnt_rd计数器计数最大值
CNT_WAIT_MAX = 28'd500_000 ;//cnt_wait计数器计数最大值
// wire define
wire [7:0] data_num ; //fifo中数据个数
// reg define
reg [7:0] cnt_wr ; //写触发有效信号保持时间计数器
reg write_valid ; //写触发有效信号
reg [7:0] cnt_rd ; //读触发有效信号保持时间计数器
reg read_valid ; //读触发有效信号
reg [10:0] cnt_start ; //单字节数据读/写时间间隔计数
reg [7:0] wr_i2c_data_num ; //写入i2c设备的数据个数
reg [7:0] rd_i2c_data_num ; //读出i2c设备的数据个数
reg fifo_rd_valid ; //fifo读有效信号
reg [27:0] cnt_wait ; //fifo读使能信号间时间间隔计数
reg fifo_rd_en ; //fifo读使能信号
reg [7:0] rd_data_num ; //读出fifo数据个数
////
//\* Main Code \//
////
//cnt_wr:写触发有效信号保持时间计数器,计数写触发有效信号保持时钟周期数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wr <= 8'd0;
else if(write_valid == 1'b0)
cnt_wr <= 8'd0;
else if(write_valid == 1'b1)
cnt_wr <= cnt_wr + 1'b1;
//write_valid:写触发有效信号
//由于写触发信号保持时间为一个系统时钟周期(20ns),
//不能被i2c驱动时钟i2c_scl正确采集,延长写触发信号生成写触发有效信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
write_valid <= 1'b0;
else if(cnt_wr == (CNT_WR_RD_MAX - 1'b1))
write_valid <= 1'b0;
else if(write == 1'b1)
write_valid <= 1'b1;
//cnt_rd:读触发有效信号保持时间计数器,计数读触发有效信号保持时钟周期数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_rd <= 8'd0;
else if(read_valid == 1'b0)
cnt_rd <= 8'd0;
else if(read_valid == 1'b1)
cnt_rd <= cnt_rd + 1'b1;
//read_valid:读触发有效信号
//由于读触发信号保持时间为一个系统时钟周期(20ns),
//不能被i2c驱动时钟i2c_scl正确采集,延长读触发信号生成读触发有效信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
read_valid <= 1'b0;
else if(cnt_rd == (CNT_WR_RD_MAX - 1'b1))
read_valid <= 1'b0;
else if(read == 1'b1)
read_valid <= 1'b1;
//cnt_start:单字节数据读/写操作时间间隔计数
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_start <= 11'd0;
else if((wr_en == 1'b0) && (rd_en == 1'b0))
cnt_start <= 11'd0;
else if(cnt_start == (CNT_START_MAX - 1'b1))
cnt_start <= 11'd0;
else if((wr_en == 1'b1) \|\| (rd_en == 1'b1))
cnt_start <= cnt_start + 1'b1;
//i2c_start:i2c读/写触发信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
 i2c_start <= 1'b0;
 else if((cnt_start == (CNT_START_MAX - 1'b1)))
 i2c_start <= 1'b1;
 else
 i2c_start <= 1'b0;

 //wr_en:输出写使能信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_en <= 1'b0;
 else if((wr_i2c_data_num == DATA_NUM - 1)
 && (i2c_end == 1'b1) && (wr_en == 1'b1))
 wr_en <= 1'b0;
 else if(write_valid == 1'b1)
 wr_en <= 1'b1;

 //wr_i2c_data_num:写入i2c设备的数据个数
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_i2c_data_num <= 8'd0;
 else if(wr_en == 1'b0)
 wr_i2c_data_num <= 8'd0;
 else if((wr_en == 1'b1) && (i2c_end == 1'b1))
 wr_i2c_data_num <= wr_i2c_data_num + 1'b1;

 //rd_en:输出读使能信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_en <= 1'b0;
 else if((rd_i2c_data_num == DATA_NUM - 1)
 && (i2c_end == 1'b1) && (rd_en == 1'b1))
 rd_en <= 1'b0;
 else if(read_valid == 1'b1)
 rd_en <= 1'b1;

 //rd_i2c_data_num:写入i2c设备的数据个数
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_i2c_data_num <= 8'd0;
 else if(rd_en == 1'b0)
 rd_i2c_data_num <= 8'd0;
 else if((rd_en == 1'b1) && (i2c_end == 1'b1))
 rd_i2c_data_num <= rd_i2c_data_num + 1'b1;

 //byte_addr:输出读/写地址
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 byte_addr <= 16'h00_5A;
 else if((wr_en == 1'b0) && (rd_en == 1'b0))
 byte_addr <= 16'h00_5A;
 else if(((wr_en == 1'b1) \|\| (rd_en == 1'b1)) && (i2c_end == 1'b1))
 byte_addr <= byte_addr + 1'b1;

 //wr_data:输出待写入i2c设备数据
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_data <= 8'hA5;
 else if(wr_en == 1'b0)
 wr_data <= 8'hA5;
 else if((wr_en == 1'b1) && (i2c_end == 1'b1))
 wr_data <= wr_data + 1'b1;

 //fifo_rd_valid:fifo读有效信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 fifo_rd_valid <= 1'b0;
 else if((rd_data_num == DATA_NUM)
 && (cnt_wait == (CNT_WAIT_MAX - 1'b1)))
 fifo_rd_valid <= 1'b0;
 else if(data_num == DATA_NUM)
 fifo_rd_valid <= 1'b1;

 //cnt_wait:fifo读使能信号间时间间隔计数,计数两fifo读使能间的时间间隔
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_wait <= 28'd0;
 else if(fifo_rd_valid == 1'b0)
 cnt_wait <= 28'd0;
 else if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
 cnt_wait <= 28'd0;
 else if(fifo_rd_valid == 1'b1)
 cnt_wait <= cnt_wait + 1'b1;

 //fifo_rd_en:fifo读使能信号
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 fifo_rd_en <= 1'b0;
 else if((cnt_wait == (CNT_WAIT_MAX - 1'b1))
 && (rd_data_num < DATA_NUM))
 fifo_rd_en <= 1'b1;
 else
 fifo_rd_en <= 1'b0;

 //rd_data_num:自fifo中读出数据个数计数
 always@(posedge i2c_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_data_num <= 8'd0;
 else if(fifo_rd_valid == 1'b0)
 rd_data_num <= 8'd0;
 else if(fifo_rd_en == 1'b1)
 rd_data_num <= rd_data_num + 1'b1;

 ////
 //\* Instantiation \//
 ////
 //------------- fifo_read_inst -------------
 fifo_data fifo_read_inst
 (
 .clock (i2c_clk ), //输入时钟信号,频率1MHz,1bit
 .data (rd_data ), //输入写入数据,1bit
 .rdreq (fifo_rd_en ), //输入数据读请求,1bit
 .wrreq (i2c_end && rd_en ), //输入数据写请求,1bit

 .q (fifo_rd_data ), //输出读出数据,1bit
 .usedw (data_num ) //输出fifo内数据个数,1bit
 );

 endmodule

参考代码编写完毕,对于模块的仿真验证我们等到其他模块介绍完毕后,对整个实验工程进行整体仿真,到时候再对模块仿真波形进行具体分析。

18.2.1.3. 顶层模块

I2C驱动模块、数据收发模块的相关内容介绍完毕,对于其他消抖模块、数码管显示模块等子功能模块的介绍在前面章节有详细说明,此处不再赘述。

顶层模块内部实例化实验工程的个子功能模块,连接个模块对应信号;对外接收外部传入的时钟、复位信号以及读/写操作触发信号,发送串行时钟scl和串行数据sda给EEPROM存储芯片,发送片选和段选给数码管。顶层模块框图如下。

I2CEEP047

图 47‑46 顶层模块框图

顶层模块内容并不复杂,就是个子功能模块实例化,无需过多解释,无需波形图绘制,直接编写参考代码。

模块代码

顶层模块参考代码如下代码清单 47‑3。

代码清单 47‑3 顶层模块参考代码(eeprom_byte_rd_wr.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
module eeprom_byte_rd_wr
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire key_wr , //按键写
input wire key_rd , //按键读

inout wire sda , //串行数据
output wire scl , //串行时钟
output wire stcp , //输出数据存储寄时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
output wire oe //使能信号
);
////
//\* Parameter and Internal Signal \//
////
//wire define
wire read ; //读数据
wire write ; //写数据
wire [7:0] po_data ; //fifo输出数据
wire [7:0] rd_data ; //eeprom读出数据
wire wr_en ;
wire rd_en ;
wire i2c_end ;
wire i2c_start ;
wire [7:0] wr_data ;
wire [15:0] byte_addr ;
wire i2c_clk ;
////
//\* Instantiation \//
////
//------------- key_wr_inst -------------
key_filter key_wr_inst
(
.sys_clk (sys_clk ), //系统时钟50MHz
.sys_rst_n (sys_rst_n ), //全局复位
.key_in (key_wr ), //按键输入信号
.key_flag (write ) //key_flag为1时表示按键有效,0表示按键无效
);
//------------- key_rd_inst -------------
key_filter key_rd_inst
(
.sys_clk (sys_clk ), //系统时钟50MHz
.sys_rst_n (sys_rst_n ), //全局复位
.key_in (key_rd ), //按键输入信号
.key_flag (read ) //key_flag为1时表示按键有效,0表示按键无效
);
//------------- i2c_rw_data_inst -------------
i2c_rw_data i2c_rw_data_inst
(
.sys_clk (sys_clk ), //输入系统时钟,频率50MHz
.i2c_clk (i2c_clk ), //输入i2c驱动时钟,频率1MHz
.sys_rst_n (sys_rst_n ), //输入复位信号,低有效
.write (write ), //输入写触发信号
.read (read ), //输入读触发信号
.i2c_end (i2c_end ), //一次i2c读/写结束信号
.rd_data (rd_data ), //输入自i2c设备读出的数据
.wr_en (wr_en ), //输出写使能信号
.rd_en (rd_en ), //输出读使能信号
.i2c_start (i2c_start ), //输出i2c读/写触发信号
.byte_addr (byte_addr ), //输出i2c设备读/写地址
.wr_data (wr_data ), //输出写入i2c设备的数据
.fifo_rd_data(po_data ) //输出自fifo中读出的数据
);
//------------- i2c_ctrl_inst -------------
i2c_ctrl
#(
.DEVICE_ADDR (7'b1010_011 ), //i2c设备器件地址
.SYS_CLK_FREQ (26'd50_000_000 ), //i2c_ctrl模块系统时钟频率
.SCL_FREQ (18'd250_000 ) //i2c的SCL时钟频率
)
i2c_ctrl_inst
(
.sys_clk (sys_clk ), //输入系统时钟,50MHz
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效
.wr_en (wr_en ), //输入写使能信号
.rd_en (rd_en ), //输入读使能信号
.i2c_start (i2c_start ), //输入i2c触发信号
.addr_num (1'b1 ), //输入i2c字节地址字节数
.byte_addr (byte_addr ), //输入i2c字节地址
.wr_data (wr_data ), //输入i2c设备数据
.rd_data (rd_data ), //输出i2c设备读取数据
.i2c_end (i2c_end ), //i2c一次读/写操作完成
.i2c_clk (i2c_clk ), //i2c驱动时钟
.i2c_scl (scl ), //输出至i2c设备的串行时钟信号scl
.i2c_sda (sda ) //输出至i2c设备的串行数据信号sda
);
 //------------- seg7_dynamic_inst -------------
 seg_595_dynamic seg_595_dynamic_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .data (po_data ), //数码管要显示的值
 .point ( ), //小数点显示,高电平有效
 .seg_en (1'b1 ), //数码管使能信号,高电平有效
 .sign ( ), //符号位,高电平显示负号

 .stcp (stcp ), //数据存储器时钟
 .shcp (shcp ), //移位寄存器时钟
 .ds (ds ), //串行数据输入
 .oe (oe ) //使能信号
 );

 endmodule

RTL视图

至此实验工程基本完成,在Quartus中对代码进行编译,编译若有错误,请读者根据错误提示信息作出更改,直至编译通过,编译通过后查看RTL视图,与顶层模块框图对比,两者一致,各信号连接正确。RTL视图,具体见图 47‑47。

I2CEEP048

图 47‑47 RTL视图

18.2.1.4. 仿真验证

仿真代码编写

实验工程个模块均已讲解完毕,对本工程进行整体仿真,编写仿真参考代码如下代码清单 47‑4。

代码清单 47‑4 仿真参考代码(tb_eeprom_byte_rd_wr.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
module tb_eeprom_byte_rd_wr();
//wire define
wire scl ;
wire sda ;
wire stcp;
wire shcp;
wire ds ;
wire oe ;

 //reg define
 reg clk ;
 reg rst_n ;
 reg key_wr;
 reg key_rd;

 //时钟、复位信号
 initial
 begin
 clk = 1'b1 ;
 rst_n <= 1'b0 ;
 key_wr <= 1'b1 ;
 key_rd <= 1'b1 ;
 #200
 rst_n <= 1'b1 ;
 #1000
 key_wr <= 1'b0 ;
 key_rd <= 1'b1 ;
 #400
 key_wr <= 1'b1 ;
 key_rd <= 1'b1 ;
 #20000000
 key_wr <= 1'b1 ;
 key_rd <= 1'b0 ;
 #400
 key_wr <= 1'b1 ;
 key_rd <= 1'b1 ;
 #40000000
 $stop;
 end

 always #10 clk = ~clk;

 defparam eeprom_byte_rd_wr_inst.key_wr_inst.MAX_20MS = 5;
 defparam eeprom_byte_rd_wr_inst.key_rd_inst.MAX_20MS = 5;
 defparam eeprom_byte_rd_wr_inst.i2c_rw_data_inst.CNT_WAIT_MAX = 1000;

 //-------------eeprom_byte_rd_wr_inst-------------
 eeprom_byte_rd_wr eeprom_byte_rd_wr_inst
 (
 .sys_clk (clk ), //输入工作时钟,频率50MHz
 .sys_rst_n (rst_n ), //输入复位信号,低电平有效
 .key_wr (key_wr ), //按键写
 .key_rd (key_rd ), //按键读

 .sda (sda ), //串行数据
 .scl (scl ), //串行时钟
 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe )

 );

 //-------------eeprom_inst-------------
 M24LC64 M24lc64_inst
 (
 .A0 (1'b0 ), //器件地址
 .A1 (1'b0 ), //器件地址
 .A2 (1'b0 ), //器件地址
 .WP (1'b0 ), //写保护信号,高电平有效
 .RESET (~rst_n ), //复位信号,高电平有效

 .SDA (sda ), //串行数据
 .SCL (scl ) //串行时钟
 );

 endmodule

仿真波形分析

使用ModelSim对工程进行整体仿真,对实验工程各模块仿真波形我们只查看I2C驱动模块和数据收发模块仿真波形。

I2CEEP049

图 47‑48 I2C驱动模块写操作部分仿真波形图

I2CEEP050

图 47‑49 I2C驱动模块读操作部分仿真波形图

I2CEEP051

图 47‑50 I2C驱动模块局部波形图

I2CEEP052

图 47‑51 数据收发模块写数据部分波形图

I2CEEP053

图 47‑52 数据收发模块读数据部分波形图

I2CEEP054

图 47‑53 数据收发模块数据发送部分波形图

注:图中高阻态部分视为高电平即可。

上述仿真波形图只展示了整体部分,读者可自行对工程进行仿真,查看各信号波形变化情况。

18.3. 上板验证

18.3.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 43‑2所示。

表格 47‑4 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

输入系统时钟

sys_rst_n

Input

M15

复位信号

key_rd

Input

M1

数据读按键

key_wr

Input

M2

数据写按键

scl

Output

P15

i2c串行时钟

sda

Inout

N14

i2c数据信号

shcp

Output

B1

移位寄存器时钟

stcp

Output

K9

数据存储器时钟

ds

Output

R1

串行数据输入

oe

Output

L11

使能信号

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 47‑54所示。

I2CEEP055

图 47‑54 管脚分配

18.3.1.1. 结果验证

如图 47‑55所示,开发板连接12V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

I2CEEP056

图 47‑55 程序下载连线图

如图 47‑56所示,使用“Programmer”为开发板下载程序。

I2CEEP057

图 47‑56 程序下载图

程序下载完成后,按下数据读按键,数码管会显示出EEPROM事先写入的10字节数据;按下数据写按键;随后按下数据读按键,数码管会依次显示1-10共10字节数据。

18.4. 章末总结

本章节,我们通过EEPROM的数据读写实验为为读者详细讲解了I2C协议的相关内容,在电子设计领域,使用I2C通讯协议的芯片或设备还有很多,请读者务必理解掌握I2C通讯协议的相关知识。

18.5. 拓展训练

将串口RS232与EEPROM读写工程结合起来,使用PC机通过串口发送指令和和数据实现EEPROM数据读写操作。