超级版主
- 积分
- 4676
- 金钱
- 4676
- 注册时间
- 2019-5-8
- 在线时间
- 1224 小时
|
4) 对正点原子FPGA感兴趣的同学可以加群讨论:712557122 点击加入:5)关注正点原子公众号,获取最新资料更新:
第二十八章 EEPROM读写测试实验
EEPROM是一种用于计算机系统的非易失性存储器, 也常在嵌入式领域中作为数据的存储设备, 在物联网及可穿戴设备等需要存储少量数据的场景中也有广泛应用。本章我们学习EEPROM的读写操作并进行EEPROM读写实验。
本章包括以下几个部分:
28.1 EEPROM 简介
28.2 实验任务
28.3 硬件设计
28.4 程序设计
28.5 下载验证
28.1 EEPROM 简介
EEPROM (Electrically Erasable Progammable Read Only Memory, E2PROM)即电可擦除可编程只读存储器, 是一种常用的非易失性存储器(掉电数据不丢失), EEPROM有多种类型的产品,我们开拓者FPGA开发板上使用的是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-Integrated Circuit(集成电路总线),是由Philips半导体公司(现在的NXP半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。I2C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据。 在主控与被控IC之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR, 具体可查器件手册)识别。我们开拓者I2C总线物理拓扑结构如下图所示。
图 28.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)完成。停止信号产生后,总线再次处于空闲状态。
图 28.1.2 I2C整体时序图
了解到了整体时序之后,我们可能有疑问,数据是以什么样的格式传输的呢?满足怎样的时序要求呢?是在任何时候改变都可以吗?怎么知道从机有没有接收到数据呢?带着这些疑问,我们继续学习I2C。由于只有一根数据线进行数据的传输,如果不规定好传输规则肯定会导致信息错乱,如同在单条道路上驾驶,没有交通规则,再好的道路也会发生拥堵甚至更糟。采用两线结构的I2C虽然只有一根数据线,但由于还有一条时钟线,可以让数据线在时钟线的带领下有顺序的传送,就好像单条道路上的车辆在交警或信号指示灯的指示下有规则的通行。那么I2C遵循怎样的规则呢?
图 28.1.3 I2C具体时序图
如果要想回答这些问题,我们得读懂图 28.1.3。由图 28.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(见图 28.3.2)的硬件连接决定。 当硬件电路上分别将这三个管脚连接到GND
或VCC时,就可以设置不同的可编程地址。 对于我们的开发板,这3个管脚连接到地。进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0时表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。对于AT24C64来说,其传输器件地址格式如下图所示。
图 28.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又是以字节为单位进行传输的,所以需要用两个字节地址来寻址整个存储单元。 图 28.1.4 和图 28.1.5分别为单字节字地址和双字节字地址器件的地址分布图,其中单字节字地址的器件是以存储容量为2Kb的EEPROM存储器AT24C02为例,双字节字地址的器件是以存储容量为64Kb的EEPROM存储器AT24C64为例, WA7即字地址Word Address的第7位,以此类推,用WA是为了区别前面器件地址中的A。
图 28.1.4 单字节字地址分布
图 28.1.5 双字节字地址分布
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为“0”即写命令,从机就处于接收数据的状态,此时,主机就开始写数据了。写数据分为单次写(对于EEPROM而言,称为字节写)和连续写(对于EEPROM而言,称为页写),那么这两者有什么区别呢?对比图 28.1.6和图 28.1.7可知,两者的区别在于发送完一字节数据后,是发送结束信号还是继续发送下一字节数据, 如果发送的是结束信号,就称为单次写,如果继续发送下一字节数据,就称为连续写。
图 28.1.6是AT24C64的单次写(字节写)时序
对于字地址为单字节的I2C器件而言,在发送完字地址(对应图 28.1.6的字地址高位),且从机应答后即可串行发送8bit数据。 图 28.1.7是AT24C64连续写(页写)时序。要注意的是,对于AT24C64的页写,是不能发送超过一页的单元容量的数据的,而AT24C64的一页的单元容量为32Byte,当写完一页的最后一个单元时,地址指针指向该页的开头,如果再写入数据,就会覆盖该页的起始数据。
图 28.1.6 单次写(字节写)时序
图 28.1.7 连续写(页写)时序
如果读写控制位R/W位为“1”即读命令,主机就处于接收数据的状态,从机从该地址单元输出数据。读数据有三种方式:当前地址读、随机读和连续读。当前地址读是指在一次读或写操作后发起读操作。由于I2C器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一个字地址的数据。也就是说上次读或写操作的单元地址为02时,当前地址读的内容就是地址03处的单元数据,时序图如图 28.1.8所示。
图 28.1.8 当前地址读时序 由于当前地址读极不方便读取任意的地址单元的数据,所以就有了随机读,随机读的时序有点奇怪,见图 28.1.9,发送完器件地址和字地址后,竟然又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为“0”,也就是写命令,第二次发送器件地址时后面的读写控制位为“1”,也就是读。为什么会有这样奇怪的操作呢?这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次DummyWrite也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据, 而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置,等从机应答后,就可以以当前地址读的方式读数据了,如图 28.1.9所示,随机地址读是没有发送数据的单次写操作和当前地址读操作的结合体。
图 28.1.9 随机地址读时序 至于连续读,对应的是当前地址读和随机读都是一次读取一个字节而言的,它是将当前地址读或随机读的主机非应答改成应答,表示继续读取数据, 图 28.1.10是在当前地址读下的连续读。
图 28.1.10 顺序读时序 至此, I2C协议就基本讲完了,本章我们主要采用单次写和随机读方式进行EEPROM读写测试。
28.2 实验任务
本次实验任务是先通过FPGA从EEPROM(AT24C64)的存储器地址0至存储器地址255分别写入数据0~255;写完之后再开始读取存储器地址0~ 255中的数据, 若读取的值正确则LED灯常亮,否则LED灯闪烁。
28.3 硬件设计
AT24C64芯片的常用封装形式有直插(DIP8)式和贴片(SO-8)式两种,无论是直插式还是贴片式,其引脚功能与序号都一样,我们开发板上采用的是贴片式,实物图和引脚图分别如图 28.3.1和图 28.3.2所示。
图 28.3.1 开发板上的AT24C64实物图
图 28.3.2 AT12C64引脚图
AT24C64的引脚功能如下:
A2,A1,A0:可编程地址输入端。
GND:电源地引脚
SDA: SDA(Serial Data,串行数据) 是双向串行数据输入/输出端。
SCL: SCL(Serial clock,串行时钟)串行时钟输入端。
WP(写保护): AT24C64有一个写保护引脚用于提供数据保护,当写保护引脚连接至GND时,
芯片可以正常写,当写保护引脚连接至VCC时,使能写保护功能,此时禁止向芯片写入数据,只能进行读操作。
VCC:电源输入引脚
我们的开拓者FPGA开发板上EEPROM的原理图如图 28.3.3所示:
图 28.3.3 EEPROM原理图
由上图可知,我们开发板上的EEPROM可编程地址A2、 A1、 A0连接到地,所以AT24C64的器件地址为1010000,如下图所示:
图 28.3.4 AT24C64的器件地址
本实验中,系统时钟、按键复位以及EEPROM的SCL和SDA的管脚分配如下表所示:
表 28.3.1 串口通信实验管脚分配
28.4 程序设计
根据实验任务,我们可以大致规划出系统的控制流程: 首先FPGA向EEPROM写数据,写完之后从EEPROM读出所写入的数据,并判断读出的数据与写入的数据是否相同,如果相同则LED灯常亮,否则LED灯闪烁。由此画出系统的功能框图如下图所示:
图 28.4.1 EEPROM读写实验系统框图
由系统总体框图可知, FPGA部分包括四个模块, 顶层模块(e2prom_top)、 读写模块(e2prom_rw)、 I2C驱动模块(i2c_dri) 和LED灯显示模块(led_alarm) 。 其中在顶层模块
中完成对I2C驱动模块的例化。各模块端口及信号连接如图 28.4.2所示:
图 28.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模块读到的数据。 error_flag是错误标志,用来控制led的显示状态。
顶层模块的代码如下:
1 module e2prom_top(
2 //system clock
3 input sys_clk , // 系统时钟
4 input sys_rst_n , // 系统复位
5 //eeprom interface
6 output scl , // eeprom的时钟线scl
7 inout sda , // eeprom的数据线sda
8 //user interface
9 output [3:0] led // led显示
10 );
11
12 //parameter define
13 parameter SLAVE_ADDR = 7'b1010000 ; // 器件地址
14 parameter BIT_CTRL = 1'b1 ; // 字地址位控制参数(16b/8b)
15 parameter CLK_FREQ = 26'd50_000_000; // i2c_dri模块的驱动时钟频率
16 parameter I2C_FREQ = 18'd250_000 ; // I2C的SCL时钟频率
17 parameter L_TIME = 17'd125_000 ; // led闪烁时间参数
18
19 //reg define
20 reg [25:0] cnt ; // 计数
21 reg [ 1:0] flow_cnt ; // 状态流控制
22 reg [13:0] wait_cnt ; // 等待计数
23
24 //wire define
25 wire clk ; // I2C操作时钟
26 wire i2c_exec ; // i2c触发控制
27 wire [15:0] i2c_addr ; // i2c操作地址
28 wire [ 7:0] i2c_data_w; // i2c写入的数据
29 wire i2c_done ; // i2c操作结束标志
30 wire i2c_rh_wl ; // i2c读写控制
31 wire [ 7:0] i2c_data_r; // i2c读出的数据
32 wire error_flag; // 错误标志
33
34 //*****************************************************
35 //** main code
36 //*****************************************************
37
38 //例化e2prom读写模块
39 e2prom_rw u_e2prom_rw (
40 //global clock
41 .clk (clk ), // 时钟信号
42 .rst_n (sys_rst_n ), // 复位信号
43 //i2c interface
44 .i2c_exec (i2c_exec ), // I2C触发执行信号
45 .i2c_rh_wl (i2c_rh_wl ), // I2C读写控制信号
46 .i2c_addr (i2c_addr ), // I2C器件内地址
47 .i2c_data_w (i2c_data_w), // I2C要写的数据
48 .i2c_data_r (i2c_data_r), // I2C读出的数据
49 .i2c_done (i2c_done ), // I2C一次操作完成
50 //user interface
51 .error_flag (error_flag) // 错误标志
52 );
53
54 //例化i2c_dri
55 i2c_dri #(
56 .SLAVE_ADDR (SLAVE_ADDR), // slave address从机地址,放此处方便参数传递
57 .CLK_FREQ (CLK_FREQ ), // i2c_dri模块的驱动时钟频率(CLK_FREQ)
58 .I2C_FREQ (I2C_FREQ ) // I2C的SCL时钟频率
59 ) u_i2c_dri(
60 //global clock
61 .clk (sys_clk ), // i2c_dri模块的驱动时钟(CLK_FREQ)
62 .rst_n (sys_rst_n ), // 复位信号
63 //i2c interface
64 .i2c_exec (i2c_exec ), // I2C触发执行信号
65 .bit_ctrl (BIT_CTRL ), // 器件地址位控制(16b/8b)
66 .i2c_rh_wl (i2c_rh_wl ), // I2C读写控制信号
67 .i2c_addr (i2c_addr ), // I2C器件内地址
68 .i2c_data_w (i2c_data_w), // I2C要写的数据
69 .i2c_data_r (i2c_data_r), // I2C读出的数据
70 .i2c_done (i2c_done ), // I 2C一次操作完成
71 .scl (scl ), // I2C的SCL时钟信号
72 .sda (sda ), // I2C的SDA信号
73 //user interface
74 .dri_clk (clk ) // I2C操作时钟
75 );
76
77 //例化led_alarm模块
78 led_alarm #(.L_TIME(L_TIME ) // 控制led闪烁时间(此为500ms)
79 ) u_led_alarm(
80 //system clock
81 .clk (sys_clk ), // 时钟信号
82 .rst_n (sys_rst_n ), // 复位信号
83 //led interface
84 .led (led ), // LED灯
85 //user interface
86 .error_flag (error_flag) // 错误标志
87 );
88 endmodule
顶层模块中主要完成对其余模块的例化,需要注意的是程序第13行到第17行定义了五个参数, 在模块例化时会将这些变量传递到相应的模块。当程序用于读写不同器件地址的EEPROM时将SLAVE_ADDR修改为新的器件地址; 字地址位控制参数(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.5s, clk的频率为250KHz时,L_TIME=250000*0.5=125000。由前面的I2C读写操作时序图我们可以发现, I2C驱动模块非常适合采用状态机来编写。无论是字节写(图 28.1.6)还是随机读(图 28.1.9),都要先从空闲状态开始,先发送起始信
号,然后发送器件地址和读写命令(这里为了方便表示我们使用“控制命令”来表示器件地址和读写命令)。发送完控制命令并接收应答信号后发送字地址, 然后就可以进行读写数据的传输了。读写数据传输结束后接收应答信号, 最后发送停止信号,此时I2C读写操作结束, 再次进入空闲状态。状态机的状态跳转图如下所示,总共有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。
图 28.4.3 I2C驱动模块状态跳转图
在程序中我们采用三段式状态机。 由于代码较长,我们在这里将其中第二段的源代码粘贴
如下:
105 //组合逻辑判断状态转移条件
106 always @( * ) begin
107 next_state = st_idle;
108 case(cur_state)
109 st_idle: begin // 空闲状态
110 if(i2c_exec) begin
111 next_state = st_sladdr;
112 end
113 else
114 next_state = st_idle;
115 end
116 st_sladdr: begin
117 if(st_done) begin
118 if(bit_ctrl) // 判断是16位还是8位字地址
119 next_state = st_addr16;
120 else
121 next_state = st_addr8 ;
122 end
123 else
124 next_state = st_sladdr;
125 end
126 st_addr16: begin // 写16位字地址
127 if(st_done) begin
128 next_state = st_addr8;
129 end
130 else begin
131 next_state = st_addr16;
132 end
133 end
134 st_addr8: begin // 8位字地址
135 if(st_done) begin
136 if(wr_flag==1'b0) // 读写判断
137 next_state = st_data_wr;
138 else
139 next_state = st_addr_rd;
140 end
141 else begin
142 next_state = st_addr8;
143 end
144 end
145 st_data_wr: begin // 写数据(8 bit)
146 if(st_done)
147 next_state = st_stop;
148 else
149 next_state = st_data_wr;
150 end
151 st_addr_rd: begin // 写地址以进行读数据
152 if(st_done) begin
153 next_state = st_data_rd;
154 end
155 else begin
156 next_state = st_addr_rd;
157 end
158 end
159 st_data_rd: begin // 读取数据(8 bit)
160 if(st_done)
161 next_state = st_stop;
162 else
163 next_state = st_data_rd;
164 end
165 st_stop: begin // 结束I2C操作
166 if(st_done)
167 next_state = st_idle;
168 else
169 next_state = st_stop ;
170 end
171 default: next_state= st_idle;
172 endcase
173 end
我们可以对照着图 28.4.3来分析程序中各状态之间是如何跳转的。EEPROM读写模块主要实现对I2C读写过程的控制,包括给出字地址及需要写入该地址中的数据、 启动I2C读写操作、判断读写数据是否一致等。 EEPROM读写模块的代码如下:
1 module e2prom_rw(
2 //global clock
3 input clk , // 时钟信号
4 input rst_n , // 复位信号
5 6
//i2c interface
7 output i2c_rh_wl , // I2C读写控制信号
8 output reg i2c_exec , // I2C触发执行信号
9 output reg [15:0] i2c_addr , // I2C器件字地址
10 output reg [ 7:0] i2c_data_w , // I2C要写的数据
11 input [ 7:0] i2c_data_r , // I2C读出的数据
12 input i2c_done , // I2C一次操作完成
13
14 //user interface
15 output reg error_flag // 错误标志
16 );
17
18 //parameter define
19 parameter WAIT = 14'd5000 ; // 读写等待时间
20 parameter BYTE_N = 16'd255 ; // 读写的字节数
21
22 //reg define
23 reg addr_over ; // 地址结束标志
24 reg rom_w_done; // 字节全部写入e2prom的标志
25 reg [ 1:0] flow_cnt ; // 状态流控制
26 reg [13:0] wait_cnt ; // 等待计数
27
28 //*****************************************************
29 //** main code
30 //*****************************************************
31
32 //读写控制
33 assign i2c_rh_wl = addr_over & rom_w_done;
34
35 //eeprom字节地址配置
36 always @(posedge clk or negedge rst_n) begin
37 if(rst_n == 1'b0) begin
38 i2c_addr <= 16'd0;
39 addr_over<= 1'b0;
40 end
41 else if(i2c_done == 1'b1) begin
42 if(i2c_rh_wl == 1'b1) begin
43 if(i2c_addr < BYTE_N)
44 i2c_addr <= i2c_addr + 1'd1;
45 else
46 i2c_addr <= i2c_addr;
47 end
48 else begin
49 if(i2c_addr == BYTE_N) begin
50 i2c_addr <= 16'd0;
51 addr_over<= 1'b1; //写完指定地址标志
52 end
53 else
54 i2c_addr <= i2c_addr + 1'd1;
55 end
56 end
57 else
58 i2c_addr <= i2c_addr;
59 end
60
61 //读写eeprom
62 always @(posedge clk or negedge rst_n) begin
63 if(rst_n == 1'b0) begin
64 flow_cnt <= 2'b0;
65 wait_cnt <= 14'b0;
66 i2c_exec <= 1'b0;
67 i2c_data_w <= 8'd0;
68 rom_w_done <= 1'b0;
69 error_flag <= 1'b1;
70 end
71 else begin
72 i2c_exec <= 1'b0;
73 //从eeprom的第1页的第1个字节到第16页的第16个字节(共256字节)写入数据0~255
74 if(i2c_rh_wl == 1'b0) begin
75 case(flow_cnt)
76 2'd0: begin
77 rom_w_done <= 1'b0;
78 wait_cnt <= wait_cnt + 1'b1;
79 if(wait_cnt == 14'd100) begin
80 wait_cnt <= 14'd0;
81 flow_cnt <= flow_cnt + 1'b1;
82 end
83 end
84 2'd1: begin
85 i2c_exec <= 1'b1;
86 i2c_data_w <= i2c_addr[7:0];
87 flow_cnt <= flow_cnt + 1'b1;
88 end
89 2'd2: begin
90 if(i2c_done == 1'b1)
91 flow_cnt <= flow_cnt + 1'b1;
92 end
93 2'd3:begin
94 if(wait_cnt == WAIT) begin //写间隔控制
95 flow_cnt <= 2'b0;
96 wait_cnt <= 14'd0;
97 rom_w_done <= 1'b1;
98 end
99 else
100 wait_cnt <= wait_cnt + 1'b1;
101 end
102 endcase
103 end
104 //读取从eeprom的第1页的第1个字节开始的共256字节的值并判断值是否正确
105 else begin
106 case(flow_cnt)
107 2'd0: begin
108 wait_cnt <= wait_cnt + 1'b1;
109 if(wait_cnt == 14'd100) begin
110 flow_cnt <= flow_cnt + 1'b1;
111 wait_cnt <= 14'd0;
112 end
113 end
114 2'd1: begin
115 i2c_exec <= 1'b1;
116 flow_cnt <= flow_cnt + 1'b1;
117 end
118 2'd2: begin
119 if(i2c_done == 1'b1) begin // 判断I2C操作是否完成
120 if(i2c_addr[7:0] == i2c_data_r) begin // 判断读到的值正确与否
121 error_flag <= 1'b0; // 读到的值正确
122 flow_cnt <= 2'b0; // 返回状态0
123 end
124 else begin
125 error_flag <= 1'b1; // 读到的值错误
126 end
127 end
128 end
129 default: flow_cnt <= 2'b0;
130 endcase
131 end
132 end
133 end
134
135 endmodule
程序中第62行的always块是读写控制块,具体是读还是写由I2C读写控制信号i2c_rh_wl决定。 当该信号为低电平时,为写数据操作,从EEPROM的存储地址0开始, 每写入一个字节的数据,地址加1,直至写入指定的字节数(BYTE_N) 。当写最后一个存储地址结束后, 写EEPROM结束的标志信号rom_w_done拉高, 写数据过程结束。 由程序第33行可知,此时i2c_rh_wl为高电平,程序进入读数据过程。 由于写入每个存储单元的数据与该单元的地址相同,所以当读到的数据与该存储单元的地址相等时,表明读写一致, 错误标志信号error_flag为低电平;若两者不相等,则说明读写过程发生错误,此时将error_flag拉高,结束读操作。 图 28.4.4为写过程中SignalTap抓取的波形图:
图 28.4.4 写过程中SignalTap抓取的波形图
从该波形图中我们看到读写控制信号i2c_rh_wl为低电平, 表示处于写操作状态。 当I2C触发执行信号i2c_exec为高电平时开始执行I2C写操作,从上图中可以看到当前写的存储单元地址为0000h,写入的数据为00h。图 28.4.5为读过程中SignalTap抓取的波形图。
图 28.4.5 读过程中SignalTap抓取的波形图
从该波形图中我们看到读写控制信号i2c_rh_wl为高电平, 表示处于读操作状态,当I2C触发执行信号i2c_exec为高电平时开始执行I2C读操作,从上图中可以看到当前读的存储单元地址为0001h, 读到的数据为01h。led显示模块利用LED灯的显示状态来标识读写过程是否出错, 在模块中通过检测错误标志信号error_flag来改变LED灯的显示状态。LED显示模块led_alarm代码如下:
1 module led_alarm #(parameter L_TIME = 25'd25_000_000 // 控制led闪烁间隔时间
2 )( // 此处为500ms
3 //system clock
4 input clk , // 时钟信号
5 input rst_n , // 复位信号
6 7
//led interface
8 output [3:0] led , // LED 灯
9
10 //user interface
11 input error_flag // 错误标志
12 );
13
14 //reg define
15 reg led_t ; // 使用的led灯
16 reg [24:0] led_cnt; // led计数
17
18 //*****************************************************
19 //** main code
20 //*****************************************************
21
22 //led输出
23 assign led = {3'b000,led_t};
24
25 //错误标志为1时led闪烁,否则, LED0常亮
26 always @(posedge clk or negedge rst_n) begin
27 if(rst_n == 1'b0) begin
28 led_cnt <= 25'd0;
29 led_t <= 1'b0;
30 end
31 else begin
32 if(error_flag) begin // 读到的值错误
33 led_cnt <= led_cnt + 25'd1;
34 if(led_cnt == L_TIME) begin // 数据错误时LED灯每隔L_TIME时间闪烁一次
35 led_cnt <= 25'd0;
36 led_t <= ~led_t;
37 end
38 end
39 else begin // 读完且读到的值正确
40 led_cnt <= 25'd0;
41 led_t <= 1'b1; // led灯常亮
42 end
43 end
44 end
45
46 endmodule
程序第一行的参数L_TIME用于控制led闪烁间隔时间,在例化时重新指定参数值, 可以改变led闪烁的快慢。 程序中第32行判断error_flag的值,当error_flag为高电平时表明读写数据不一致,此时, led灯每隔L_TIME时间闪烁一次;当error_flag为低电平时,表明读写数据
一致, EEPROM读写正确, led灯常亮。
28.5 下载验证
首先我们打开工程, 在工程所在的路径下打开e2prom_top/par文件夹, 在里面找到“e2prom_top.qpf” 并双击打开。注意工程所在的路径名只能由字母、 数字以及下划线组成,不能出现中文、空格以及特殊字符等。
EEPROM读写工程打开后如图 28.5.1所示。
图 28.5.1 EEPROM读写工程
接下来我们下载程序,验证EEPROM读写功能。工程打开后通过点击工具栏中的“Programmer”图 标 打 开 下 载 界 面 , 通 过 “ Add File ” 按 钮 选 择 EEPROM 读 写 工 程 中e2prom_top/par/output_files 目录下的“e2prom_top.sof”文件。开发板电源打开后,在程序下载界面点击“Hardware Setup”,在弹出的对话框中选择当前的硬件连接为“USBBlaster[USB-0]”。然后点击“Start”将工程编译完成后得到的 sof 文件下载到开发板中,如图 28.5.2所示。
图 28.5.2 程序下载界面
接下来我们下载程序,验证通过I2C协议读写EEPROM功能。 下载完成后观察开发板的led显示如图 28.5.3所示, led灯常亮, 说明通过EEPROM读写程序下载验证正确。
图 28.5.3 实验现象
|
|