OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 1108|回复: 0

[国产FPGA] 《ATK-DFPGL22G 之FPGA开发指南》第三十章 EEPROM读写测试实验

[复制链接]

1117

主题

1128

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4666
金钱
4666
注册时间
2019-5-8
在线时间
1224 小时
发表于 2023-12-2 16:47:29 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-12-1 15:46 编辑

第三十章 EEPROM读写测试实验
1)实验平台:正点原子 ATK-DFPGL22G开发板

2) 章节摘自【正点原子】ATK-DFPGL22G之FPGA开发指南_V1.0

3)购买链接:https://detail.tmall.com/item_o.htm?id=692712955836

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz-PGL22G.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)FPGA技术交流QQ群:435699340

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

EEPROM是一种用于计算机系统的非易失性存储器,也常在嵌入式领域中作为数据的存储设备,在物联网及可穿戴设备等需要存储少量数据的场景中也有广泛应用。本章我们学习EEPROM的读写操作并进行EEPROM读写实验。
本章包括以下几个部分:
1.1EEPROM简介
1.2实验任务
1.3硬件设计
1.4程序设计
1.5下载验证

1.1 EEPROM简介
EEPROM (Electrically Erasable Progammable Read OnlyMemory,E2PROM)即电可擦除可编程只读存储器,是一种常用的非易失性存储器(掉电数据不丢失),EEPROM有多种类型的产品,我们ATK-DFPGL22G开发板上使用的是ATMEL公司生产的AT24C系列的AT24C64这一型号。AT24C64具有高可靠性,可对所存数据保存100年,并可多次擦写,擦写次数达一百万次。
一般而言,对于存储类型的芯片,我们比较关注其存储容量。我们这次实验所用的AT24C64存储容量为64Kbit,内部分成256页,每页32字节,共有8192个字节,且其读写操作都是以字节为基本单位。可以把AT24C64看作一本书,那么这本书有256页,每页有32行,每行有8个字,总共有256*32*8=65536个字,对应着AT24C64的64*1024=65536个bit。
知道了AT24C64的存储容量,就知道了读写的空间大小。那么我们该如何对AT24C64进行读写操作呢?
由于AT24C64采用两线串行接口的双向数据传输协议——I2C协议实现读写操作,所以我们有必要了解一下I2C协议。
I2C即Inter-IntegratedCircuit(集成电路总线),是由Philips半导体公司(现在的NXP半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。
I2C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据。在主控与被控IC之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,具体可查器件手册)识别。我们ATK-DFPGL22G开发板上的I2C总线物理拓扑结构如下图所示。                             
image002.png
图 30.1.1 I2C总线物理拓扑结构图
图中的I2C_SCL是串行时钟线,I2C_SDA是串行数据线,由于I2C器件一般采用开漏结构与总线相连,所以I2C_SCL和I2C_SDA均需接上拉电阻,也正因此,当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低,即各器件的SDA及SCL都是“线与”关系。
I2C总线支持多主和主从两种工作方式,通常工作在主从工作方式,我们的开发板就采用主从工作方式。在主从工作方式中,系统中只有一个主机,其它器件都是具有I2C总线的外围从机。在主从工作方式中,主机启动数据的发送(发出启动信号)并产生时钟信号,数据发送完成后,发出停止信号。
I2C总线结构虽然简单,使用两线传输,然而要实现器件间的通信,需要通过控制SCL和SDA的时序,使其满足I2C的总线传输协议,方可实现器件间的数据传输。那么I2C协议的时序是怎样的呢?
在I2C器件开始通信(传输数据)之前,串行时钟线SCL和串行数据线SDA线由于上拉的原因处于高电平状态,此时I2C总线处于空闲状态。如果主机(此处指FPGA)想开始传输数据,只需在SCL为高电平时将SDA线拉低,产生一个起始信号,从机检测到起始信号后,准备接收数据,当数据传输完成,主机只需产生一个停止信号,告诉从机数据传输结束,停止信号的产生是在SCL为高电平时,SDA从低电平跳变到高电平,从机检测到停止信号后,停止接收数据。I2C整体时序如下图。起始信号之前为空闲状态,起始信号之后到停止信号之前的这一段为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输由双向数据线(SDA)完成。停止信号产生后,总线再次处于空闲状态。
image003.png
图 30.1.2 I2C整体时序图
了解到了整体时序之后,我们可能有疑问,数据是以什么样的格式传输的呢?满足怎样的时序要求呢?是在任何时候改变都可以吗?怎么知道从机有没有接收到数据呢?带着这些疑问,我们继续学习I2C。
由于只有一根数据线进行数据的传输,如果不规定好传输规则肯定会导致信息错乱,如同在单条道路上驾驶,没有交通规则,再好的道路也会发生拥堵甚至更糟。采用两线结构的I2C虽然只有一根数据线,但由于还有一条时钟线,可以让数据线在时钟线的带领下有顺序的传送,就好像单条道路上的车辆在交警或信号指示灯的指示下有规则的通行。那么I2C遵循怎样的规则呢?
image005.png
图 30.1.3 I2C具体时序图
如果要想回答这些问题,我们得读懂图 30.1.3。由图 30.1.3可知,我们在起始信号之后,主机开始发送传输的数据;在串行时钟线SCL为低电平状态时,SDA允许改变传输的数据位(1为高电平,0为低电平),在SCL为高电平状态时,SDA要求保持稳定,相当于一个时钟周期传输1bit数据,经过8个时钟周期后,传输了8bit数据,即一个字节。第8个时钟周期末,主机释放SDA以使从机应答,在第9个时钟周期,从机将SDA拉低以应答;如果第9个时钟周期,SCL为高电平时,SDA未被检测到为低电平,视为非应答,表明此次数据传输失败。第9个时钟周期末,从机释放SDA以使主机继续传输数据,如果主机发送停止信号,此次传输结束。我们要注意的是数据以8bit即一个字节为单位串行发出,其最先发送的是字节的最高位。
I2C的时序部分已经基本介绍完了,但还有一个小问题,就是当多个I2C器件挂接在总线上时,怎样才能与我们想要传输数据的器件进行通信。这就涉及到了器件地址(也称从机地址,SLAVE ADDRESS)。
每个I2C器件都有一个器件地址,有些I2C器件的器件地址是固定的,而有些I2C器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到I2C总线上,例如EEPROM器件,为了增加系统的EEPROM容量,可能需要多个EEPROM。器件可编程地址位的数量由它可使用的管脚决定,比如EEPROM器件一般会留下3个管脚用于可编程地址位。但有些I2C器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟PCF8563的器件地址为固定的7’h51)。所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可。
对于AT24C64而言,其器件地址为1010加3位的可编程地址,3位可编程地址由器件上的3个管脚A2、A1、A0(见图 30.3.2)的硬件连接决定。当硬件电路上分别将这三个管脚连接到GND或VCC时,就可以设置不同的可编程地址。对于我们的开发板,这3个管脚连接到地。
进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0时表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。对于AT24C64来说,其传输器件地址格式如下图所示。
image007.png
图 30.1.4 器件地址格式示意图
发送完第一个字节(7位器件地址和一位读写控制位)并收到从机正确的应答后就开始发送字地址(Word Address)。一般而言,每个兼容I2C协议的器件,内部总会有可供读写的寄存器或存储器,对于我们本次实验用到的EEPROM存储器,内部就是一系列顺序编址的存储单元。所以,当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入内容。该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量(2^8=256)时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示,例如同是EEPROM存储器,AT24C02的存储单元容量为2Kbit=256Byte(一般bit缩写为b,Byte缩写为B),用一个字节地址即可寻址所有的存储单元,而AT24C64的存储单元容量为64Kb=8KB,需要13位(2^13=8KB)的地址位,而I2C又是以字节为单位进行传输的,所以需要用两个字节地址来寻址整个存储单元。图 30.1.5 和图 30.1.6分别为单字节字地址和双字节字地址器件的地址分布图,其中单字节字地址的器件是以存储容量为2Kb的EEPROM存储器AT24C02为例,双字节字地址的器件是以存储容量为64Kb的EEPROM存储器AT24C64为例,WA7即字地址WordAddress的第7位,以此类推,用WA是为了区别前面器件地址中的A。
image009.png
图 30.1.5 单字节字地址分布

image011.png
image013.png
图 30.1.6 双字节字地址分布
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为“0”即写命令,从机就处于接收数据的状态,此时,主机就开始写数据了。写数据分为单次写(对于EEPROM而言,称为字节写)和连续写(对于EEPROM而言,称为页写),那么这两者有什么区别呢?对比图 30.1.7和图 30.1.8可知,两者的区别在于发送完一字节数据后,是发送结束信号还是继续发送下一字节数据,如果发送的是结束信号,就称为单次写,如果继续发送下一字节数据,就称为连续写。图 30.1.7是AT24C64的单次写(字节写)时序,对于字地址为单字节的I2C器件而言,在发送完字地址(对应图 30.1.7的字地址高位),且从机应答后即可串行发送8bit数据。图 30.1.8是AT24C64连续写(页写)时序。要注意的是,对于AT24C64的页写,是不能发送超过一页的单元容量的数据的,而AT24C64的一页的单元容量为32Byte,当写完一页的最后一个单元时,地址指针指向该页的开头,如果再写入数据,就会覆盖该页的起始数据。
image015.png
图 30.1.7 单次写(字节写)时序

image017.png
图 30.1.8 连续写(页写)时序
如果读写控制位R/W位为“1”即读命令,主机就处于接收数据的状态,从机从该地址单元输出数据。读数据有三种方式:当前地址读、随机读和连续读。当前地址读是指在一次读或写操作后发起读操作。由于I2C器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一个字地址的数据。也就是说上次读或写操作的单元地址为02时,当前地址读的内容就是地址03处的单元数据,时序图如图 30.1.9所示。
image019.png
图 30.1.9 当前地址读时序
由于当前地址读极不方便读取任意的地址单元的数据,所以就有了随机读,随机读的时序有点奇怪,见图 30.1.10,发送完器件地址和字地址后,竟然又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为“0”,也就是写命令,第二次发送器件地址时后面的读写控制位为“1”,也就是读。为什么会有这样奇怪的操作呢?这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次Dummy Write也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置,等从机应答后,就可以以当前地址读的方式读数据了,如图 30.1.10所示,随机地址读是没有发送数据的单次写操作和当前地址读操作的结合体。
image021.png
图 30.1.10 随机地址读时序
至于连续读,对应的是当前地址读和随机读都是一次读取一个字节而言的,它是将当前地址读或随机读的主机非应答改成应答,表示继续读取数据,图 30.1.11是在当前地址读下的连续读。
image023.png
图 30.1.11 顺序读时序
至此,I2C协议就基本讲完了,本章我们主要采用单次写和随机读的方式进行EEPROM读写测试。

1.2 实验任务
本节的实验任务是先向EEPROM(AT24C64)的存储器地址0至255分别写入数据0~255;写完之后再读取存储器地址0~255中的数据,若读取的值全部正确则LED灯常亮,否则LED灯闪烁。

1.3 硬件设计
AT24C64芯片的常用封装形式有直插(DIP8)式和贴片(SO-8)式两种,无论是直插式还是贴片式,其引脚功能与序号都一样,我们开发板上采用的是贴片式,实物图和引脚图分别如图 30.3.1和图 30.3.2所示。
image025.jpg
图 30.3.1 开发板上的AT24C64实物图

image027.png
图 30.3.2 AT24C64引脚图
AT24C64的引脚功能如下:
A2,A1,A0:可编程地址输入端。
GND:电源地引脚
SDA:SDA(Serial Data,串行数据)是双向串行数据输入/输出端。
SCL:SCL(Serial clock,串行时钟)串行时钟输入端。
WP(写保护):AT24C64有一个写保护引脚用于提供数据保护,当写保护引脚连接至GND时,芯片可以正常写,当写保护引脚连接至VCC时,使能写保护功能,此时禁止向芯片写入数据,只能进行读操作。
VCC:电源输入引脚
我们的ATK-DFPGL22G开发板上EEPROM的原理图如图 30.3.3所示:
image029.png
图 30.3.3 EEPROM原理图
由上图可知,我们开发板上的EEPROM可编程地址A2、A1、A0连接到地,所以AT24C64的器件地址为1010000,如下图所示:
image031.png
图 30.3.4 AT24C64的器件地址
本实验中,系统时钟、按键复位以及EEPROM的SCL和SDA的管脚分配如下表所示:
QQ截图20231201153541.png
30.3.1 EEPROM读写测试实验管脚分配

1.4 程序设计
根据实验任务,我们可以大致规划出系统的控制流程:首先FPGA向EEPROM写数据,写完之后从EEPROM读出所写入的数据,并判断读出的数据与写入的数据是否相同,如果相同则LED灯常亮,否则LED灯闪烁。由此画出系统的功能框图如下图所示:
image033.png
图 30.4.1 EEPROM读写实验系统框图
由系统总体框图可知,FPGA部分包括四个模块,顶层模块(e2prom_top)、读写模块(e2prom_rw)、I2C驱动模块(i2c_dri)和LED灯显示模块(led_alarm)。其中在顶层模块中完成对其余模块的例化。
各模块端口及信号连接如下图所示:
image034.png
图 30.4.2 顶层模块原理图
i2c_dri为I2C驱动模块,用来驱动I2C的读写操作。当FPGA通过EEPROM读写模块e2prom_rw向EEPROM读写数据时,拉高i2c触发控制信号i2c_exec以使能I2C驱动模块,并使用读写控制信号i2c_rh_wl控制读写操作,当i2c_rh_wl为低电平时,I2C驱动模块i2c_dri执行写操作,当i2c_rh_wl为高电平时,I2C驱动模块i2c_dri执行读操作。此外,e2prom_rw模块通过i2c_addr接口向i2c_dri模块输入器件字地址,通过i2c_data_w接口向i2c_dri模块输入写的数据,并通过i2c_data_r接口读取i2c_dri模块读到的数据。rw_done信号是读写测试完成的标志,rw_result是读写测试的结果。
顶层模块的代码如下:
  1. 1  module e2prom_top(
  2. 2      input               sys_clk    ,      //系统时钟
  3. 3      input               sys_rst_n  ,      //系统复位
  4. 4      //eeprom interface
  5. 5      output              iic_scl    ,      //eeprom的时钟线scl
  6. 6      inout               iic_sda    ,      //eeprom的数据线sda
  7. 7      //user interface
  8. 8      output              led               //led显示
  9. 9  );
  10. 10
  11. 11 //parameter define
  12. 12 parameter    SLAVE_ADDR = 7'b1010000    ; //器件地址(SLAVE_ADDR)
  13. 13 parameter    BIT_CTRL  = 1'b1          ; //字地址位控制参数(16b/8b)
  14. 14 parameter    CLK_FREQ  = 26'd50_000_000; //i2c_dri模块的驱动时钟频率(CLK_FREQ)
  15. 15 parameter    I2C_FREQ  = 18'd250_000   ; //I2C的SCL时钟频率
  16. 16 parameter    L_TIME    = 17'd125_000   ; //led闪烁时间参数
  17. 17
  18. 18 //wire define
  19. 19 wire           dri_clk   ; //I2C操作时钟
  20. 20 wire           i2c_exec  ; //I2C触发控制
  21. 21 wire   [15:0]  i2c_addr ; //I2C操作地址
  22. 22 wire   [ 7:0]  i2c_data_w; //I2C写入的数据
  23. 23 wire           i2c_done  ; //I2C操作结束标志
  24. 24 wire           i2c_ack   ; //I2C应答标志
  25. 25 wire           i2c_rh_wl ; //I2C读写控制
  26. 26 wire   [ 7:0]  i2c_data_r; //I2C读出的数据
  27. 27 wire           rw_done   ; //E2PROM读写测试完成
  28. 28 wire           rw_result ; //E2PROM读写测试结果 0:失败 1:成功
  29. 29
  30. 30 //*****************************************************
  31. 31 //**                    main code
  32. 32 //*****************************************************
  33. 33
  34. 34 //e2prom读写测试模块
  35. 35 e2prom_rwu_e2prom_rw(
  36. 36     .clk         (dri_clk   ),  //时钟信号
  37. 37     .rst_n       (sys_rst_n ),  //复位信号
  38. 38     //i2c interface
  39. 39     .i2c_exec    (i2c_exec  ),  //I2C触发执行信号
  40. 40     .i2c_rh_wl   (i2c_rh_wl ),  //I2C读写控制信号
  41. 41     .i2c_addr    (i2c_addr  ),  //I2C器件内地址
  42. 42     .i2c_data_w  (i2c_data_w),  //I2C要写的数据
  43. 43     .i2c_data_r  (i2c_data_r),  //I2C读出的数据
  44. 44     .i2c_done    (i2c_done  ),  //I2C一次操作完成
  45. 45     .i2c_ack     (i2c_ack   ),  //I2C应答标志0:应答 1:未应答
  46. 46     //user interface
  47. 47     .rw_done     (rw_done   ),  //E2PROM读写测试完成
  48. 48     .rw_result   (rw_result )   //E2PROM读写测试结果 0:失败 1:成功
  49. 49 );
  50. 50
  51. 51 //i2c驱动模块
  52. 52 i2c_dri #(
  53. 53     .SLAVE_ADDR  (SLAVE_ADDR),  //EEPROM从机地址
  54. 54     .CLK_FREQ    (CLK_FREQ  ),  //模块输入的时钟频率
  55. 55     .I2C_FREQ    (I2C_FREQ  )   //IIC_SCL的时钟频率
  56. 56 ) u_i2c_dri(
  57. 57     .clk         (sys_clk   ),  
  58. 58     .rst_n       (sys_rst_n ),  
  59. 59     //i2c interface
  60. 60     .i2c_exec    (i2c_exec  ),  //I2C触发执行信号
  61. 61     .bit_ctrl    (BIT_CTRL  ),  //器件地址位控制(16b/8b)
  62. 62     .i2c_rh_wl   (i2c_rh_wl ),  //I2C读写控制信号
  63. 63     .i2c_addr    (i2c_addr  ),  //I2C器件内地址
  64. 64     .i2c_data_w  (i2c_data_w),  //I2C要写的数据
  65. 65     .i2c_data_r  (i2c_data_r),  //I2C读出的数据
  66. 66     .i2c_done    (i2c_done  ),  //I2C一次操作完成
  67. 67     .i2c_ack     (i2c_ack   ),  //I2C应答标志
  68. 68     .scl         (iic_scl   ),  //I2C的SCL时钟信号
  69. 69     .sda         (iic_sda   ),  //I2C的SDA信号
  70. 70     //user interface
  71. 71     .dri_clk     (dri_clk   )   //I2C操作时钟
  72. 72 );
  73. 73
  74. 74 //led指示模块
  75. 75 led_alarm #(.L_TIME(L_TIME  )   //控制led闪烁时间
  76. 76 ) u_led_alarm(
  77. 77     .clk         (dri_clk   ),  
  78. 78     .rst_n       (sys_rst_n ),
  79. 79     
  80. 80     .led         (led       ),
  81. 81     .rw_done     (rw_done
  82.    ),  
  83. 82     .rw_result   (rw_result )
  84. 83 );
  85. 84
  86. 85 endmodule
复制代码
顶层模块中主要完成对其余模块的例化,需要注意的是程序第11行到第16行定义了五个参数,在模块例化时会将这些变量传递到相应的模块。
SLAVE_ADDR定义了EEPROM的器件地址;字地址位控制参数(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。
由前面的I2C读写操作时序图我们可以发现,I2C驱动模块非常适合采用状态机来编写。无论是字节写还是随机读,都要先从空闲状态开始,先发送起始信号,然后发送器件地址和读写命令(这里为了方便我们使用“控制命令”来表示器件地址和读写命令)。发送完控制命令并接收应答信号后发送字地址,然后就可以进行读写数据的传输了。读写数据传输结束后接收应答信号,最后发送停止信号,此时I2C读写操作结束,再次进入空闲状态。
状态机的状态跳转图如图 30.4.3所示,总共有8个状态,一开始状态机处于空闲状态st_idle,当I2C触发执行信号触发(i2c_exec=1)时,状态机进入发送控制命令状态st_sladdr;发送完控制命令后就发送字地址,这里出于简单考虑,不对从机EEPROM的应答信号进行判断。由于字地址存在单字节和双字节的区别,我们通过bit_ctrl信号判断是单字节还是双字节字地址。对于双字节的字地址我们先发送高8位即第一个字节,发送完高8位后进入发送8位字地址状态st_addr8,也就是发送双字节地地址的低8位;对于单字节的字地址我们直接进入发送8位字地址状态st_addr8。发送完字地址后,根据读写判断标志来判断是读操作还是写操作。如果是写(wr_flag=0)就进入写数据状态st_data_wr,开始向EEPROM发送数据;如果是读(wr_flag=1)就进入发送器件地址读状态st_addr_rd发送器件地址,此状态结束后就进入读数据状态st_data_rd接收EEPROM输出的数据。读或写数据结束后就进入结束I2C操作状态st_done并发送结束信号,此时,I2C总线再次进入空闲状态st_idle。
QQ截图20231201153527.png
图 30.4.3 I2C驱动模块状态跳转图
在程序中我们采用三段式状态机。由于代码较长,我们在这里将其中第二段的源代码粘贴如下:
  1. 85  //组合逻辑判断状态转移条件
  2. 86  always @(*) begin
  3. 87      next_state =st_idle;
  4. 88      case(cur_state)
  5. 89          st_idle:begin                         //空闲状态
  6. 90             if(i2c_exec) begin
  7. 91                next_state = st_sladdr;
  8. 92             end
  9. 93             else
  10. 94                next_state = st_idle;
  11. 95          end
  12. 96          st_sladdr:begin
  13. 97              if(st_done) begin
  14. 98                  if(bit_ctrl)                    //判断是16位还是8位字地址
  15. 99                    next_state = st_addr16;
  16. 100                 else
  17. 101                   next_state = st_addr8 ;
  18. 102             end
  19. 103             else
  20. 104                next_state = st_sladdr;
  21. 105         end
  22. 106         st_addr16:begin                       //写16位字地址
  23. 107             if(st_done) begin
  24. 108                next_state = st_addr8;
  25. 109             end
  26. 110             else begin
  27. 111                next_state = st_addr16;
  28. 112             end
  29. 113         end
  30. 114         st_addr8:begin                        //8位字地址
  31. 115             if(st_done) begin
  32. 116                 if(wr_flag==1'b0)               //读写判断
  33. 117                    next_state = st_data_wr;
  34. 118                 else
  35. 119                    next_state = st_addr_rd;
  36. 120             end
  37. 121             else begin
  38. 122                next_state = st_addr8;
  39. 123             end
  40. 124         end
  41. 125         st_data_wr:begin                      //写数据(8bit)
  42. 126             if(st_done)
  43. 127                next_state = st_stop;
  44. 128             else
  45. 129                 next_state= st_data_wr;
  46. 130         end
  47. 131         st_addr_rd:begin                      //写地址以进行读数据
  48. 132             if(st_done) begin
  49. 133                next_state = st_data_rd;
  50. 134             end
  51. 135             else begin
  52. 136                next_state = st_addr_rd;
  53. 137             end
  54. 138         end
  55. 139         st_data_rd:begin                      //读取数据(8bit)
  56. 140             if(st_done)
  57. 141                next_state = st_stop;
  58. 142             else
  59. 143                next_state = st_data_rd;
  60. 144         end
  61. 145         st_stop:begin                         //结束I2C操作
  62. 146             if(st_done)
  63. 147                next_state = st_idle;
  64. 148             else
  65. 149                next_state = st_stop ;
  66. 150         end
  67. 151         default: next_state= st_idle;
  68. 152     endcase
  69. 153 end
复制代码
我们可以对照着图 30.4.3来分析程序中各状态之间是如何跳转的。
EEPROM读写模块主要实现对I2C读写过程的控制,包括给出字地址及需要写入该地址中的数据、启动I2C读写操作、判断读写数据是否一致等。
EEPROM读写模块的代码如下:
  1. 1  module e2prom_rw(
  2. 2      input                 clk        , //时钟信号
  3. 3      input                 rst_n      , //复位信号
  4. 4  
  5. 5      //i2c interface
  6. 6      output   reg          i2c_rh_wl  , //I2C读写控制信号
  7. 7      output   reg          i2c_exec   , //I2C触发执行信号
  8. 8      output   reg  [15:0]  i2c_addr  , //I2C器件内地址
  9. 9      output   reg  [ 7:0]  i2c_data_w , //I2C要写的数据
  10. 10     input         [ 7:0]  i2c_data_r , //I2C读出的数据
  11. 11     input                 i2c_done   , //I2C一次操作完成
  12. 12     input                 i2c_ack    , //I2C应答标志
  13. 13
  14. 14     //user interface
  15. 15     output   reg          rw_done    , //E2PROM读写测试完成
  16. 16     output   reg          rw_result    //E2PROM读写测试结果 0:失败 1:成功
  17. 17 );
  18. 18
  19. 19 //parameter define
  20. 20 //EEPROM写数据需要添加间隔时间,读数据则不需要
  21. 21 parameter      WR_WAIT_TIME = 14'd5000; //写入间隔时间
  22. 22 parameter      MAX_BYTE     = 16'd256 ; //读写测试的字节个数
  23. 23
  24. 24 //reg define
  25. 25 reg   [1:0]    flow_cnt ; //状态流控制
  26. 26 reg   [13:0]   wait_cnt ; //延时计数器
  27. 27
  28. 28 //*****************************************************
  29. 29 //**                    main code
  30. 30 //*****************************************************
  31. 31
  32. 32 //EEPROM读写测试,先写后读,并比较读出的值与写入的值是否一致
  33. 33 always @(posedge clk or negedge rst_n) begin
  34. 34     if(!rst_n) begin
  35. 35         flow_cnt <= 2'b0;
  36. 36         i2c_rh_wl <= 1'b0;
  37. 37         i2c_exec <= 1'b0;
  38. 38         i2c_addr <= 16'b0;
  39. 39         i2c_data_w <= 8'b0;
  40. 40         wait_cnt <= 14'b0;
  41. 41         rw_done <= 1'b0;
  42. 42         rw_result <= 1'b0;        
  43. 43     end
  44. 44     else begin
  45. 45         i2c_exec <= 1'b0;
  46. 46         rw_done <= 1'b0;
  47. 47         case(flow_cnt)
  48. 48             2'd0 : begin                                 
  49. 49                 wait_cnt <= wait_cnt + 1'b1;               //延时计数
  50. 50                 if(wait_cnt == WR_WAIT_TIME - 1'b1) begin  //EEPROM写操作延时完成
  51. 51                     wait_cnt <= 1'b0;
  52. 52                     if(i2c_addr == MAX_BYTE) begin         //256个字节写入完成
  53. 53                         i2c_addr <= 1'b0;
  54. 54                         i2c_rh_wl <= 1'b1;
  55. 55                         flow_cnt <= 2'd2;
  56. 56                     end
  57. 57                     else begin
  58. 58                         flow_cnt <= flow_cnt + 1'b1;
  59. 59                         i2c_exec <= 1'b1;
  60. 60                     end
  61. 61                 end
  62. 62             end
  63. 63             2'd1 : begin
  64. 64                 if(i2c_done == 1'b1) begin                 //EEPROM单次写入完成
  65. 65                     flow_cnt <= 2'd0;
  66. 66                     i2c_addr <= i2c_addr + 1'b1;           //地址0~255分别写入
  67. 67                     i2c_data_w <= i2c_data_w + 1'b1;       //数据0~255
  68. 68                 end   
  69. 69             end
  70. 70             2'd2 : begin                                   
  71. 71                 flow_cnt <= flow_cnt + 1'b1;
  72. 72                 i2c_exec <= 1'b1;
  73. 73             end   
  74. 74             2'd3 : begin
  75. 75                 if(i2c_done == 1'b1) begin                 //EEPROM单次读出完成
  76. 76                     //读出的值错误或者I2C未应答,读写测试失败
  77. 77                     if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
  78. 78                         rw_done <= 1'b1;
  79. 79                         rw_result <= 1'b0;
  80. 80                     end
  81. 81                     else if(i2c_addr == MAX_BYTE - 1'b1) begin //读写测试成功
  82. 82                         rw_done <= 1'b1;
  83. 83                         rw_result <= 1'b1;
  84. 84                     end   
  85. 85                     else begin
  86. 86                         flow_cnt <= 2'd2;
  87. 87                         i2c_addr <= i2c_addr + 1'b1;
  88. 88                     end
  89. 89                 end                 
  90. 90             end
  91. 91             default : ;
  92. 92         endcase   
  93. 93     end
  94. 94 end   
  95. 95
  96. 96 endmodule
复制代码
程序中第21行和第22行定义了两个参数,WR_WAIT_TIME(写入间隔时间)和MAX_BYTE(读写测试的字节个数)。AT24C64官方手册对写入数据后,数据写入完成的时间最大不超过10ms,所以为了保证数据能够正确写入,单次写入数据操作完成后,最好延时10ms的时间。本次实验为了节省数据写入的时间,WR_WAIT_TIME的值设置为5000,即5ms(输入时钟的周期为1us,1us*5000=5ms),实测延时5ms也可以正确写入。这里不建议大家将写入的间隔设置的过于短,否则会导致数据写入失败。另外,EEPROM只有对写操作有时间间隔要求,对读操作没有间隔要求,因此读写测试模块仅对写操作增加时间间隔。
程序中第32至94行代码先对I2C驱动模块发起写操作,即拉高i2c_exec,拉低i2c_rh_wl(低电平表示写),然后分别向EEPROM的地址0至地址255写入数据0至255,并且在每次写操作之间增加5ms的延时。数据全部写完后,发起读操作,即拉高i2c_exec,拉高i2c_rh_wl(高电平表示读),然后分别从EEPROM的地址0至地址255读出数据,并判断读出的值与写入的值是否一致,如果数据一致并且每次操作IIC都有应答信号产生(i2c_ack),EEPROM的读写测试才正确,否则读写测试失败。
读写测试完成后,输出rw_done信号和rw_result信号,rw_done为EEPROM读写测试完成信号,rw_result为读写测试的结果,0表示读写失败,1表示读写正确。
I2C驱动模块仿真代码如下:
  1. 1  `timescale  1ns/1ns                         //定义仿真时间单位1ns和仿真时间精度为1ns
  2. 2  
  3. 3  module  tb_i2c_dri;              
  4. 4  
  5. 5  //parameter  define
  6. 6  parameter  T = 20;                          //时钟周期为20ns
  7. 7  parameter  IIC_WR_CYCYLE = 10_000;
  8. 8  parameter  SLAVE_ADDR    = 7'b1010000 ;    //EEPROM从机地址
  9. 9  parameter  CLK_FREQ      = 26'd50_000_000; //模块输入的时钟频率
  10. 10 parameter  I2C_FREQ      = 18'd250_000;    //IIC_SCL的时钟频率
  11. 11 //reg define
  12. 12 reg          sys_clk;                           //时钟信号
  13. 13 reg          sys_rst_n;                        //复位信号
  14. 14      
  15. 15 reg          i2c_exec  ;
  16. 16 reg          bit_ctrl  ;
  17. 17 reg          i2c_rh_wl ;
  18. 18 reg   [15:0 i2c_addr  ;
  19. 19 reg   [7:0  i2c_data_w;
  20. 20 reg   [3:0  flow_cnt  ;
  21. 21 reg   [13:0 delay_cnt ;
  22. 22
  23. 23 //wire define
  24. 24 wire  [7:0  i2c_data_r;
  25. 25 wire         i2c_done  ;
  26. 26 wire         i2c_ack   ;
  27. 27 wire         scl       ;
  28. 28 wire         sda       ;
  29. 29 wire         dri_clk   ;
  30. 30
  31. 31 //*****************************************************
  32. 32 //**                    main code
  33. 33 //*****************************************************
  34. 34
  35. 35 //给输入信号初始值
  36. 36 initial begin
  37. 37      sys_clk            = 1'b0;
  38. 38      sys_rst_n          = 1'b0;     //复位
  39. 39      #(T+1)  sys_rst_n = 1'b1;     //在第21ns的时候复位信号信号拉高
  40. 40 end
  41. 41
  42. 42 //50Mhz的时钟,周期则为1/50Mhz=20ns,所以每10ns,电平取反一次
  43. 43 always #(T/2) sys_clk = ~sys_clk;
  44. 44
  45. 45 always @(posedge dri_clk or negedge sys_rst_n) begin
  46. 46      if(!sys_rst_n) begin
  47. 47          i2c_exec <= 1'b0;
  48. 48          bit_ctrl <= 1'b0;
  49. 49          i2c_rh_wl <= 1'b0;
  50. 50          i2c_addr <= 1'b0;
  51. 51          i2c_data_w <= 1'b0;
  52. 52          flow_cnt <= 1'b0;
  53. 53          delay_cnt <= 1'b0;
  54. 54      end
  55. 55      else begin
  56. 56          case(flow_cnt)
  57. 57              'd0 : flow_cnt <= flow_cnt + 1'b1;
  58. 58              'd1 : begin
  59. 59                  i2c_exec <= 1'b1;                //拉高触发信号
  60. 60                  bit_ctrl <= 1'b1;                //地址位选择信号   1: 16位               
  61. 61                  i2c_rh_wl <= 1'b0;               //写操作               
  62. 62                  i2c_addr <= 16'h0555;            //写地址
  63. 63                  i2c_data_w <= 8'hAA;             //写数据               
  64. 64                  flow_cnt <= flow_cnt + 1'b1;
  65. 65              end
  66. 66              'd2 : begin
  67. 67                  i2c_exec <= 1'b0;
  68. 68                  flow_cnt <= flow_cnt + 1'b1;
  69. 69              end   
  70. 70              'd3 : begin
  71. 71                  if(i2c_done)
  72. 72                      flow_cnt <= flow_cnt + 1'b1;
  73. 73              end
  74. 74              'd4 : begin
  75. 75                  delay_cnt <= delay_cnt + 1'b1;
  76. 76                  if(delay_cnt == IIC_WR_CYCYLE - 1'b1)
  77. 77                      flow_cnt <= flow_cnt + 1'b1;
  78. 78              end
  79. 79              'd5 : begin
  80. 80                      i2c_exec <= 1'b1;
  81. 81                      bit_ctrl <= 1'b1;                  
  82. 82                      i2c_rh_wl <= 1'b1;           //读操作   
  83. 83                      i2c_addr <= 16'h0555;
  84. 84                      i2c_data_w <= 8'hAA;        
  85. 85                      flow_cnt <= flow_cnt + 1'b1;                  
  86. 86              end
  87. 87              'd6 : begin
  88. 88                  i2c_exec <= 1'b0;
  89. 89                  flow_cnt <= flow_cnt + 1'b1;
  90. 90              end
  91. 91              'd7 : begin
  92. 92                  if(i2c_done)
  93. 93                      flow_cnt <= flow_cnt + 1'b1;
  94. 94              end
  95. 95              default:;
  96. 96          endcase   
  97. 97      end
  98. 98 end
  99. 99
  100. 100 pullup(sda);
  101. 101
  102. 102 //例化led模块
  103. 103 i2c_dri #(
  104. 104     .SLAVE_ADDR  (SLAVE_ADDR),  //EEPROM从机地址
  105. 105     .CLK_FREQ    (CLK_FREQ  ),  //模块输入的时钟频率   
  106. 106     .I2C_FREQ    (I2C_FREQ  )   //IIC_SCL的时钟频率
  107. 107 )
  108. 108 u_i2c_dri
  109. 109 (
  110. 110     .clk          (sys_clk),
  111. 111     .rst_n        (sys_rst_n),
  112. 112
  113. 113     .i2c_exec     (i2c_exec  ),
  114. 114     .bit_ctrl     (bit_ctrl  ),
  115. 115     .i2c_rh_wl    (i2c_rh_wl ),
  116. 116     .i2c_addr     (i2c_addr  ),
  117. 117     .i2c_data_w   (i2c_data_w),
  118. 118     .i2c_data_r   (i2c_data_r),
  119. 119     .i2c_done     (i2c_done  ),
  120. 120     .i2c_ack      (i2c_ack   ),
  121. 121     .scl          (scl       ),
  122. 122     .sda          (sda       ),
  123. 123     .dri_clk      (dri_clk   )
  124. 124 );
  125. 125
  126. 126 EEPROM_AT24C64 u_EEPROM_AT24C64(
  127. 127     .scl         (scl),
  128. 128     .sda         (sda)
  129. 129     );
  130. 130
  131. 131 endmodule
复制代码
从I2C驱动模块仿真代码中,代码第45行到98行我们使用了一个状态机来对I2C驱动进行读写的仿真验证,当flow_cnt等于1时进入写操作状态,当flow_cnt等于5时进入读操作状态。第100行代码表示,当sda为z时,pullup只会对当前无驱动的sda信号进行一个拉高操作,说明此时sda的IO切换成输入模式。第126行例化的EEPROM_AT24C64模块为官方提供的EEPROM仿真模型,用于模拟EEPROM器件得接口程序,代码的详细路径在该例程的sim文件夹内,大家如果有需要可以自行查找。
I2C写操作的仿真波形图如下图所示:
image038.png
图 30.4.4 I2C写操作仿真波形图
从该波形图中我们看到读写控制信号i2c_rh_wl为低电平,表示处于写操作状态。当I2C触发执行信号i2c_exec为高电平时开始执行I2C写操作。在提供的仿真文件中,写入的数据为8'hAA,在IIC操作结束后,拉高i2c_done信号。另外,在IIC写操作期间,i2c_ack(IIC应答标志)一直处于低电平,说明EEPROM响应了主机并应答。
I2C读操作的仿真波形图如下图所示:
image040.png
图 30.4.5 I2C读操作仿真波形图
从该波形图中我们看到读写控制信号i2c_rh_wl为高电平,表示处于读操作状态。当I2C触发执行信号i2c_exec为高电平时开始执行I2C读操作,读出得数据为8'hAA,说明读写得结果是一致的。在仿真在IIC操作结束后,拉高i2c_done信号。另外,在IIC读操作期间,i2c_ack(IIC应答标志)一直处于低电平,说明EEPROM响应了主机并应答。
LED显示模块的代码如下:
  1. 1 module led_alarm
  2. 2 #(
  3. 3  parameter L_TIME = 25'd25_000_000
  4. 4 )
  5. 5  (
  6. 6  input        clk       ,  //时钟信号
  7. 7  input        rst_n     ,  //复位信号
  8. 8               
  9. 9  input        rw_done   ,  //错误标志
  10. 10 input        rw_result ,  //E2PROM读写测试完成
  11. 11 output  reg  led         //E2PROM读写测试结果 0:失败 1:成功
  12. 12 );
  13. 13
  14. 14 //reg define
  15. 15 reg         rw_done_flag;    //读写测试完成标志
  16. 16 reg  [24:0  led_cnt     ;    //led计数
  17. 17
  18. 18 //*****************************************************
  19. 19 //**                   main code
  20. 20 //*****************************************************
  21. 21
  22. 22 //读写测试完成标志
  23. 23 always @(posedge clk or negedge rst_n) begin
  24. 24     if(!rst_n)
  25. 25         rw_done_flag <= 1'b0;
  26. 26     else if(rw_done)
  27. 27         rw_done_flag <= 1'b1;
  28. 28 end        
  29. 29
  30. 30 //错误标志为1时LED0闪烁,否则LED0常亮
  31. 31 always @(posedge clk or negedge rst_n) begin
  32. 32     if(!rst_n) begin
  33. 33         led_cnt <= 25'd0;
  34. 34         led <= 1'b0;
  35. 35     end
  36. 36     else begin
  37. 37         if(rw_done_flag) begin
  38. 38             if(rw_result)                          //读写测试正确
  39. 39                 led <= 1'b1;                       //led灯常亮
  40. 40             else begin                             //读写测试错误
  41. 41                 led_cnt <= led_cnt + 25'd1;
  42. 42                 if(led_cnt == L_TIME - 1'b1) begin
  43. 43                     led_cnt <= 25'd0;
  44. 44                     led <= ~led;                   //led灯闪烁
  45. 45                 end                  
  46. 46             end
  47. 47         end
  48. 48         else
  49. 49             led <= 1'b0;                           //读写测试完成之前,led灯熄灭
  50. 50     end   
  51. 51 end
  52. 52
  53. 53 endmodule
复制代码
led显示模块利用LED灯的显示状态来标识读写过程是否出错。程序中第21行至27行代码寄存输入的rw_done信号,采集到rw_done信号之后,拉高rw_done_flag信号。
程序中第21至50行代码根据rw_done_flag和rw_result信号控制LED灯的状态。在EEPROM读写测试完成之前,LED灯处于熄灭状态;如果EEPROM读写测试成功,LED灯处于常亮状态;如果EEPROM读写测试失败,LED灯会不停的闪烁。

1.5 下载验证
将下载器一端连接电脑,另一端与开发板上的JTAG下载口连接,连接电源线,并打开开发板的电源开关。
程序下载完成后,LED0在短暂延时之后,开始处于常亮的状态,如下图所示:
image042.png
图 30.5.1 实验现象
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-11-22 17:41

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表