FPGA SDRAM读写测试教程分享-Xilinx-AMD社区-FPGA CPLD-ChipDebug

FPGA SDRAM读写测试教程分享

SDRAM是一种可以指定任意地址进行读写的存储器, 它具有存储容量大,读写速度快的特点,同时价格也相对低廉。 因此, SDRAM常作为缓存, 应用于数据存储量大,同时速度要求较高的场合,如复杂嵌入式设备的存储器等。

SDRAM简介

SDRAM( Synchronous Dynamic Random Access Memory) ,同步动态随机存储器。 同步是指内存工作需要同步时钟,内部的命令的发送与数据的传输都以它为基准;动态是指存储阵列需要不断的刷新来保证数据不丢失;随机是指数据不是线性依次存储,而是自由指定地址进行数据读写。

SDRAM具有空间存储量大、读写速度快、价格相对便宜等优点。 然而由于SDRAM内部利用电容来存储数据, 为保证数据不丢失,需要持续对各存储电容进行刷新操作;同时在读写过程中需要考虑行列管理、 各种操作延时等,由此导致了其控制逻辑复杂的特点。SDRAM的内部是一个存储阵列, 你可以把它想象成一张表格。 我们在向这个表格中写入数据的时候, 需要先指定一个行( Row),再指定一个列( Column), 就可以准确地找到所需要的“ 单元格” ,这就是SDRAM寻址的基本原理。如图 33.1.1所示:

图 33.1.1 FPGA SDRAM 寻址原理.png

图 31.1.1中的“ 单元格”就是SDRAM存储芯片中的存储单元, 而这个“ 表格”(存储阵列)我们称之为L-Bank。通常SDRAM的存储空间被划分为4个L-Bank, 在寻址时需要先指定其中一个L-Bank,然后在这个选定的L-Bank中选择相应的行与列进行寻址(寻址就是指定存储单元地址的过程) 。

对SDRAM的读写是针对存储单元进行的,对SDRAM来说一个存储单元的容量等于数据总线的位宽, 单位是bit。 那么SDRAM芯片的总存储容量我们就可以通过下面的公式计算出来:

SDRAM总存储容量 = L-Bank的数量×行数×列数×存储单元的容量SDRAM存储数据是利用了电容的充放电特性以及能够保持电荷的能力。一个大小为1bit的存储单元的结构如下图所示, 它主要由行列选通三极管,存储电容,刷新放大器组成。 行地址与列地址选通使得存储电容与数据线导通, 从而可进行放电(读取)与充电(写入) 操作。

图 33.1.2 FPGA SDRAM 存储单元结构示意图.png

图 33.1.3为SDRAM的功能框图, SDRAM内部有一个逻辑控制单元,并且有一个模式寄存器为其提供控制参数。 SDRAM接收外部输入的控制命令,并在逻辑控制单元的控制下进行寻址、读写、刷新、预充电等操作。

图 33.1.3 FPGA SDRAM 功能框图.png

在了解SDRAM的寻址原理及存储结构之后, 我们来看下如何实现SDRAM的读写。 首先, 在对SDRAM进行读写操作之前需要先对芯片进行初始化;其次, SDRAM读写是一个较为复杂的控制流程,其中包括行激活、列读写、 预充电、 刷新等一系列操作。大家需要熟练掌握每一个操作所对应的时序要求,才能够正确地对SDRAM进行读写操作。

1、芯片初始化

SDRAM芯片上电之后需要一个初始化的过程,以保证芯片能够按照预期方式正常工作,初始化流程如图 33.1.4所示:

图 33.1.4 FPGA SDRAM 初始化.png

SDRAM上电后要有200us的输入稳定期,在这个时间内不可以对SDRAM的接口做任何操作;200us结束以后给所有L-Bank预充电, 然后是连续8次刷新操作;最后设置模式寄存器。 初始化最关键的阶段就在于模式寄存器( MR, Mode Register)的设置,简称MRS( MR Set) 。

图 33.1.5 FPGA SDRAM 模式寄存器.png

如上图所示,用于配置模式寄存器的参数由地址线提供, 地址线不同的位分别用于表示不同的参数。SDRAM通过配置模式寄存器来确定芯片的工作方式,包括突发长度( Burst Length)、潜伏期( CAS Latency) 以及操作模式等。
需要注意的是, 在模式寄存器设置指令发出之后,需要等待一段时间才能够向SDRAM发送新的指令,这个时间我们称之为模式寄存器设置周期tRSC( Register Set Cycle) 。

2、 行激活

初始化完成后, 无论是读操作还是写操作, 都要先激活( Active) SDRAM中的一行,使之处于活动状态(又称行有效) 。在此之前还要进行SDRAM芯片的片选和L-Bank的定址,不过它们与行激活可以同时进行。

图 33.1.6 FPGA SDRAM 行激活时序图.png

从上图可以看出,在片选CS#( #表示低电平有效) 、 L-Bank定址的同时, RAS( Row Address Strobe,行地址选通脉冲)也处于有效状态。此时An地址线则发送具体的行地址。如图中是A0-A11,共有12个地址线,由于是二进制表示法,所以共有4096个行( 2^12=4096), A0-A11的不同数值就确定了具体的行地址。由于行激活的同时也是相应L-Bank有效,所以行激活也可称为L-Bank有效。

3、列读写

行地址激活之后,就要对列地址进行寻址了。 由于在SDRAM中,地址线是行列共用的,因此列寻址时地址线仍然是A0-A11。在寻址时,利用RAS( Row Address Strobe,行地址选通脉冲)与CAS( Column Address Strobe,列地址选通脉冲)来区分行寻址与列寻址,如图 33.1.7所示。

图 33.1.7 FPGA SDRAM 列选通与读操作时序图.png

图 33.1.7中“x16” 表示存储单元容量为16bit。一般来说, 在SDRAM中存储阵列 ( L-Bank)的列数小于行数, 即列地址位宽小于行地址,因此在列地址选通时地址线高位可能未用到,如下图中的A9、 A11。
另外, 列寻址信号与读写命令是同时发出的, 读/写命令是通过WE( Write Enable, 写使能) 信号来控制的, WE为低时是写命令, 为高时是读命令。

然而,在发送列读写命令时必须要与行激活命令有一个时间间隔,这个间隔被定义为tRCD,即RAS to CAS Delay( RAS至CAS延迟)。 这是因为在行激活命令发出之后, 芯片存储阵列电子元件响应需要一定的时间。 tRCD是SDRAM的一个重要时序参数, 广义的tRCD以时钟周期( tCK,Clock Time)数为单位,比如tRCD=3,就代表RAS至CAS延迟为三个时钟周期,如图 33.1.8所示。 具体到确切的时间,则要根据时钟频率而定。

图 33.1.8 FPGA SDRAM tRCD = 3时序图.png

4、 数据输出(读)

在选定列地址后,就已经确定了具体的存储单元,剩下的事情就是数据通过数据I/O通道( DQ)输出到内存总线上了。但是在CAS发出之后,仍要经过一定的时间才能有数据输出,从CAS与读取命令发出到第一笔数据输出的这段时间,被定义为CL( CAS Latency, CAS潜伏期)。CL时间越短,读数据时SDRAM响应就越快。 由于CL只在读取时出现,所以CL又被称为读取潜伏期( RL, Read Latency)。 CL的单位与tRCD一样,为时钟周期数,具体耗时由时钟频率决定。

图 33.1.9 FPGA SDRAM CL = 2 时序图.png

5、数据输入(写)

数据写入的操作也是在tRCD之后进行,但此时没有了CL (记住, CL只出现在读取操作中),行寻址与列寻址的时序图和上文一样,只是在列寻址时, WE#为有效状态。

图 33.1.10 FPGA SDRAM 数据写入的时序图.png

从上图中可见,数据与写指令同时发送。不过,数据并不是即时地写入存储单元,数据的真正写入需要一定的周期。为了保证数据的可靠写入,都会留出足够的写入/校正时间( tWR,Write Recovery Time),这个操作也被称作写回( Write Back)。 tWR至少占用一个时钟周期或再多一点(时钟频率越高, tWR占用周期越多)。

6、突发长度

突发( Burst)是指在同一行中相邻的存储单元连续进行数据传输的方式,连续传输所涉及到存储单元(列)的数量就是突发长度( Burst Lengths,简称BL)。
上文讲到的读/写操作,都是一次对一个存储单元进行寻址。然而在现实中很少只对SDRAM中的单个存储空间进行读写, 一般都需要完成连续存储空间中的数据传输。 在连续读/写操作时,为了对当前存储单元的下一个单元进行寻址, 需要不断的发送列地址与读/写命令(行地址不变,所以不用再对行寻址),如图 33.1.11所示:

图 33.1.11 FPGA SDRAM 非突发连续读操作.png

由上图可知, 虽然由于读延迟相同可以让数据的传输在I/O端是连续的,但它占用了大量的内存控制资源,在数据进行连续传输时无法输入新的命令,效率很低。为此,人们开发了突发传输技术,只要指定起始列地址与突发长度,内存就会依次地自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址。这样,除了第一笔数据的传输需要若干个周期(主要是之前的延迟,一般的是tRCD+CL)外,其后每个数据只需一个周期的延时即可获得。 如图 33.1.12所示:

图 33.1.12 FPGA SDRAM 突发连续读操作.png

至于BL的数值,也是不能随便设或在数据进行传输前临时决定。在上文讲到的初始化过程中的模式寄存器配置( MRS) 阶段就要对BL进行设置。 突发长度( BL) 可以为1、 2、 4、 8和“ 全页( Full Page) ” , 其中“ 全页” 是指突发传输一整行的数据量。另外,在MRS阶段除了要设定BL数值之外,还需要确定“ 读/写操作模式”以及“突发传输模式” 。读/写操作模式分为“ 突发读/突发写” 和“ 突发读/单一写” 。 突发读/突发写表示读和写操作都是突发传输的,每次读/写操作持续BL所设定的长度,这也是常规的设定。突发读/单一写表示读操作是突发传输,写操作则只是一个个单独进行。突发传输模式代表着突发周期内所涉及到的存储单元的传输顺序。顺序传输是指从起始单元开始顺序读取。假如BL=4,起始存储单元编号是n, 突发传输顺序就是n、 n+1、 n+2、 n+3。交错传输就是打乱正常的顺序进行数据传输(比如第一个进行传输的单元是n,而第二个进行传输的单元是n+2而不是n+1)。由于交错传输很少用到, 它的传输规则在这里就不详细介绍了,大家可以参考所选用的SDRAM芯片手册。

7、预充电

在对SDRAM某一存储地址进行读写操作结束后,如果要对同一L-Bank的另一行进行寻址,就要将原来有效(工作)的行关闭,重新发送行/列地址。 L-Bank关闭现有工作行,准备打开新行的操作就是预充电( Precharge)。 在读写过程中,工作行内的存储体由于“ 行激活” 而使存储电容受到干扰, 因此在关闭工作行前需要对本行所有存储体进行重写。预充电实际上就是对工作行中所有存储体进行数据重写,并对行地址进行复位,以准备新行工作的过程。

预充电可以通过命令控制,也可以通过辅助设定让芯片在每次读写操作之后自动进行预充电。现在我们再回过头看看读写操作时的命令时序图( 图 33.1.7) ,从中可以发现地址线A10控制着是否进行在读写之后对当前L-Bank自动进行预充电,这就是上文所说的“辅助设定”。而在单独的预充电命令中, A10则控制着是对指定的L-Bank还是所有的L-Bank (当有多个L-Bank处于有效/活动状态时)进行预充电,前者需要提供L-Bank的地址,后者只需将A10信号置于高电平。

在发出预充电命令之后,要经过一段时间才能发送行激活命令打开新的工作行,这个间隔被称为tRP( Precharge command Period,预充电有效周期) , 如图 33.1.13所示。和tRCD、CL一样, tRP的单位也是时钟周期数,具体值视时钟频率而定。

图 33.1.13 FPGA SDRAM 读取时预充电时序图( CL=2、 BL=4、 tRP=2).png

自动预充电的开始时间与上图一样,只是没有了单独的预充电命令,并在发出读取命令时,A10地址线要设为高电平(允许自动预充电)。可见控制好预充电启动时间很重要,它可以在读取操作结束后立刻进入新行的寻址,保证运行效率。
写操作时,由于每笔数据的真正写入则需要一个足够的周期来保证,这段时间就是写回周期( tWR)。所以预充电不能与写操作同时进行,必须要在tWR之后才能发出预充电命令,以确保数据的可靠写入,否则重写的数据可能出错, 如图 33.1.14所示。

图 33.1.14 FPGA SDRAM 写入时预充电时序图( BL=4、 tWR=1、 tRP=2).png

8、刷新

SDRAM之所以称为同步“动态”随机存储器,就是因为它要不断进行刷新( Refresh)才能保留住数据,因此刷新是SDRAM最重要的操作。

刷新操作与预充电类似,都是重写存储体中的数据。但为什么有预充电操作还要进行刷新呢?因为预充电是对一个或所有L-Bank中的工作行(处于激活状态的行) 操作,并且是不定期的; 而刷新则是有固定的周期, 并依次对所有行进行操作,以保留那些久久没经历重写的存储体中的数据。但与所有L-Bank预充电不同的是,这里的行是指所有L-Bank中地址相同的行,而预充电中各L-Bank中的工作行地址并不是一定是相同的。

那么要隔多长时间重复一次刷新呢?目前公认的标准是,存储体中电容的数据有效保存期上限是64ms,也就是说每一行刷新的循环周期是64ms。我们在看SDRAM芯片参数时,经常会看到4096 Refresh Cycles/64ms或8192 Refresh Cycles/64ms的标识,这里的4096与8192就代表这个芯片中每个L-Bank的行数。 刷新命令一次仅对一行有效, 也就是说在64ms内这两种规格的芯片分别需要完成4096次和8192次刷新操作。 因此, L-Bank为4096行时刷新命令的发送间隔为15.625μs( 64ms/4096), 8192行时为7.8125μs( 64ms/8192)。刷新操作分为两种:自动刷新( Auto Refresh,简称AR)与自刷新( Self Refresh,简称SR)。不论是何种刷新方式,都不需要外部提供行地址信息,因为这是一个内部的自动操作。

对于自动刷新( AR) , SDRAM内部有一个行地址生成器(也称刷新计数器)用来自动生成行地址。由于刷新是针对一行中的所有存储体进行,所以无需列寻址,或者说CAS在RAS之前有效。所以, AR又称CBR( CAS Before RAS,列提前于行定位)式刷新。在自动刷新过程中,所有L-Bank都停止工作。 每次刷新操作所需要的时间为自动刷新周期( tRC) , 在自动刷新指令发出后需要等待tRC才能发送其他指令。 64ms之后再次对同一行进行刷新操作,如此周而复始进行循环刷新。显然,刷新操作肯定会对SDRAM的性能造成影响,但这是没办法的事情,也是DRAM相对于SRAM(静态内存,无需刷新仍能保留数据)取得成本优势的同时所付出的代价。

自刷新( SR) 主要用于休眠模式低功耗状态下的数据保存。在发出AR命令时,将CKE置于无效状态,就进入了SR模式,此时不再依靠系统时钟工作,而是根据内部的时钟进行刷新操作。

在SR期间除了CKE之外的所有外部信号都是无效的(无需外部提供刷新指令),只有重新使CKE有效才能退出自刷新模式并进入正常工作状态。

9、数据掩码

在讲述读/写操作时,我们谈到了突发长度。如果BL=4,那么也就是说一次就传送4笔数据。但是,如果其中的第二笔数据是不需要的,怎么办?还要传输吗?为了屏蔽不需要的数据,人们采用了数据掩码( Data I/O Mask,简称DQM)技术。通过DQM,内存可以控制I/O端口取消哪些输出或输入的数据。为了精确屏蔽一个数据总线位宽中的每个字节,每个DQM信号线对应一个字节( 8bit) 。 因此, 对于数据总线为16bit的SDRAM芯片, 就需要两个DQM引脚。SDRAM官方规定,在读取时DQM发出两个时钟周期后生效, 如图 33.1.15所示。而在写入时,
DQM与写入命令一样是立即成效, 如图 33.1.16所示。

图 33.1.15 FPGA SDRAM 读取时DQM信号时序图.png

图 33.1.16 FPGA SDRAM 写入时DQM信号时序图.png

测试方法

向SDRAM中写入1024个数据, 从SDRAM存储空间的起始地址写起,写完后再将数据读出, 并验证读出数据是否正确。

硬件设计

图 33.3.1 FPGA SDRAM电路原理图.png

开发板上的FPGA芯片是EP4CE10F17C8,开发板上的SDRAM芯片型号为W9825G6DH-6,内部分为4个L-Bank, 行地址为13位,列地址为9位, 数据总线位宽为16bit。 故该SDRAM总的存储空间为4×(2^13)×(2^9)×16 bit = 256Mbit, 即32MB。
W9825G6DH-6工作时钟频率最高可达166MHz, 潜伏期( CAS Latency) 可选为2或3,突发长度支持1、 2、 4、 8或全页, 64ms内需要完成8K次刷新操作。其他时序参数请大家参考该芯片的数据手册

各端口信号的管脚分配如下表所示:

信号名 方向 管脚 端口说明
sys_clk input   E1 系统时钟, 50M
sys_rst_n input   M1 系统复位, 低有效
Led output F9 LED灯
sdram_clk output B14 SDRAM芯片时钟
sdram_cke output F16 SDRAM时钟有效
sdram_cs_n output K10 SDRAM片选
sdram_ras_n output K11 SDRAM行有效
sdram_cas_n output J12 SDRAM列有效
sdram_we_n output J13 SDRAM写有效
sdram_ba[1] output F13 SDRAMBank地址
sdram_ba[0] output G11 SDRAMBank地址
sdram_addr[12] output F15 SDRAM行/列地址
sdram_addr[11] output D16 SDRAM行/列地址
sdram_addr[10] output F14 SDRAM行/列地址
sdram_addr[9] output D15 SDRAM行/列地址
sdram_addr[8] output C16 SDRAM行/列地址
sdram_addr[7] output C15 SDRAM行/列地址
sdram_addr[6] output B16 SDRAM行/列地址
sdram_addr[5] output A15 SDRAM行/列地址
sdram_addr[4] output A14 SDRAM行/列地址
sdram_addr[3] output C14 SDRAM行/列地址
sdram_addr[2] output D14 SDRAM行/列地址
sdram_addr[1] output E11 SDRAM行/列地址
sdram_addr[0] output F11 SDRAM行/列地址
sdram_data[15] inout L15 SDRAM数据
sdram_data[14] inout L16 SDRAM数据
sdram_data[13] inout K15 SDRAM数据
sdram_data[12] inout K16 SDRAM数据
sdram_data[11] inout J15 SDRAM数据
sdram_data[10] inout J16 SDRAM数据
sdram_data[9] inout J11 SDRAM数据
sdram_data[8] inout G16 SDRAM数据
sdram_data[7] inout K12 SDRAM数据
sdram_data[6] inout L11 SDRAM数据
sdram_data[5] inout L14 SDRAM数据
sdram_data[4] inout L13 SDRAM数据
sdram_data[3] inout L12 SDRAM数据
sdram_data[2] inout N14 SDRAM数据
sdram_data[1] inout M12 SDRAM数据
sdram_data[0] inout P14 SDRAM数据
sdram_dqm[1] output G15 SDRAM数据掩码
sdram_dqm[0] output J14 SDRAM数据掩码

代码设计

由于SDRAM的控制时序较为复杂,为方便用户调用,我们将SDRAM控制器封
装成FIFO接口, 这样我们操作SDRAM就像读写FIFO一样简单。整个系统的功能框图如图 33.4.1
所示:

图 33.4.1 FPGA SDRAM读写测试系统框图.png

PLL时钟模块: SDRAM读写测试及LED显示模块输入时钟均为50MHz, 而SDRAM控制
器工作在100MHz时钟频率下, 另外还需要一个输出给SDRAM芯片的100MHz时钟。因此需要一个
PLL时钟模块用于产生系统各个模块所需的时钟。

SDRAM测试模块:产生测试数据及读写使能,写使能将1024个数据( 1~1024) 写入SDRAM,写操作完成后读使能拉高, 持续进行读操作, 并检测读出的数据是否正确。
FIFO控制模块: 作为SDRAM控制器与用户的交互接口, 该模块在写FIFO中的数据量到达用户指定的突发长度后将数据自动写入SDRAM;并在读FIFO中的数据量小于突发长度时将SDRAM中的数据读出。
SDRAM控制器:负责完成外部SDRAM存储芯片的初始化、读写及刷新等一系列操作。
Led显示模块:通过控制LED灯的显示状态来指示SDRAM读写测试结果。
由系统框图可知, FPGA顶层例化了以下四个模块: PLL时钟模块( pll_clk) 、 SDRAM测试模块( sdram_test) 、 LED灯指示模块( led_disp) 以及SDRAM控制器顶层模块( sdram_top) 。
各模块端口及信号连接如图 33.4.2所示:

图 33.4.2 FPGA SDRAM 顶层模块原理图.png

SDRAM测试模块( sdram_test) 输出读写使能信号及写数据,通过SDRAM控制器将数据写入
SDARM中地址为0~1023的存储空间中。在写过程结束后进行读操作,检测读出的数据是否与写
入数据一致,检测结果由标志信号error_flag指示。 LED显示模块根据error_flag的值驱动LED
以不同的状态显示。当SDRAM读写测试正确时, LED灯常亮;读写测试结果不正确时, LED灯闪
烁。
顶层模块的代码如下:

module sdram_rw_test(
    input         clk,                      //FPGA外部时钟,50M
    input         rst_n,                    //按键复位,低电平有效
    //SDRAM 芯片接口
    output        sdram_clk,                //SDRAM 芯片时钟
    output        sdram_cke,                //SDRAM 时钟有效
    output        sdram_cs_n,               //SDRAM 片选
    output        sdram_ras_n,              //SDRAM 行有效
    output        sdram_cas_n,              //SDRAM 列有效
    output        sdram_we_n,               //SDRAM 写有效
    output [ 1:0] sdram_ba,                 //SDRAM Bank地址
    output [12:0] sdram_addr,               //SDRAM 行/列地址
    inout  [15:0] sdram_data,               //SDRAM 数据
    output [ 1:0] sdram_dqm,                //SDRAM 数据掩码
    //LED
    output        led                       //状态指示灯
    );

//wire define
wire        clk_50m;                        //SDRAM 读写测试时钟
wire        clk_100m;                       //SDRAM 控制器时钟
wire        clk_100m_shift;                 //相位偏移时钟

wire        wr_en;                          //SDRAM 写端口:写使能
wire [15:0] wr_data;                        //SDRAM 写端口:写入的数据
wire        rd_en;                          //SDRAM 读端口:读使能
wire [15:0] rd_data;                        //SDRAM 读端口:读出的数据
wire        sdram_init_done;                //SDRAM 初始化完成信号

wire        locked;                         //PLL输出有效标志
wire        sys_rst_n;                      //系统复位信号
wire        error_flag;                     //读写测试错误标志

//*****************************************************
//**                    main code
//***************************************************** 

//待PLL输出稳定之后,停止系统复位
assign sys_rst_n = rst_n & locked;

//例化PLL, 产生各模块所需要的时钟
pll_clk u_pll_clk(
    .inclk0             (clk),
    .areset             (~rst_n),

    .c0                 (clk_50m),
    .c1                 (clk_100m),
    .c2                 (clk_100m_shift),
    .locked             (locked)
    );

//SDRAM测试模块,对SDRAM进行读写测试
sdram_test u_sdram_test(
    .clk_50m            (clk_50m),
    .rst_n              (sys_rst_n),

    .wr_en              (wr_en),
    .wr_data            (wr_data),
    .rd_en              (rd_en),
    .rd_data            (rd_data),   

    .sdram_init_done    (sdram_init_done),    
    .error_flag         (error_flag)
    );

//利用LED灯指示SDRAM读写测试的结果
led_disp u_led_disp(
    .clk_50m            (clk_50m),
    .rst_n              (sys_rst_n),

    .error_flag         (error_flag),
    .led                (led)             
    );

//SDRAM 控制器顶层模块,封装成FIFO接口
//SDRAM 控制器地址组成: {bank_addr[1:0],row_addr[12:0],col_addr[8:0]}
sdram_top u_sdram_top(
    .ref_clk            (clk_100m),            //sdram    控制器参考时钟
    .out_clk            (clk_100m_shift),    //用于输出的相位偏移时钟
    .rst_n                (sys_rst_n),        //系统复位

    //用户写端口
    .wr_clk             (clk_50m),            //写端口FIFO: 写时钟
    .wr_en                (wr_en),            //写端口FIFO: 写使能
    .wr_data            (wr_data),            //写端口FIFO: 写数据
    .wr_min_addr        (24'd0),            //写SDRAM的起始地址
    .wr_max_addr        (24'd1024),            //写SDRAM的结束地址
    .wr_len                (10'd512),            //写SDRAM时的数据突发长度
    .wr_load            (~sys_rst_n),        //写端口复位: 复位写地址,清空写FIFO

    //用户读端口
    .rd_clk             (clk_50m),            //读端口FIFO: 读时钟
    .rd_en                (rd_en),            //读端口FIFO: 读使能
    .rd_data            (rd_data),            //读端口FIFO: 读数据
    .rd_min_addr        (24'd0),            //读SDRAM的起始地址
    .rd_max_addr        (24'd1024),            //读SDRAM的结束地址
    .rd_len             (10'd512),            //从SDRAM中读数据时的突发长度
    .rd_load            (~sys_rst_n),        //读端口复位: 复位读地址,清空读FIFO

     //用户控制端口  
    .sdram_read_valid    (1'b1),             //SDRAM 读使能
    .sdram_init_done    (sdram_init_done),    //SDRAM 初始化完成标志

    //SDRAM 芯片接口
    .sdram_clk            (sdram_clk),        //SDRAM 芯片时钟
    .sdram_cke            (sdram_cke),        //SDRAM 时钟有效
    .sdram_cs_n            (sdram_cs_n),       //SDRAM 片选
    .sdram_ras_n        (sdram_ras_n),      //SDRAM 行有效
    .sdram_cas_n        (sdram_cas_n),      //SDRAM 列有效
    .sdram_we_n            (sdram_we_n),       //SDRAM 写有效
    .sdram_ba            (sdram_ba),         //SDRAM Bank地址
    .sdram_addr            (sdram_addr),       //SDRAM 行/列地址
    .sdram_data            (sdram_data),       //SDRAM 数据
    .sdram_dqm            (sdram_dqm)         //SDRAM 数据掩码
    );

endmodule

顶层模块中主要完成对其余模块的例化, 需要注意的是由于SDRAM工作时钟频率较高,且对时序要求比较严格,考虑到FPGA内部以及开发板上的走线延时, 为保证SDRAM能够准确的读
写数据,我们输出给SDRAM芯片的100MHz时钟相对于SDRAM控制器时钟有一个相位偏移。程序中的相位偏移时钟为clk_100m_shift(第48行) , 相位偏移量在这里设置为-75deg。
由于SDRAM控制器被封装成FIFO接口,在使用时只需要像读写FIFO那样给出读/写使能即可,如代码82~98行所示。同时控制器将SDRAM的阵列地址映射为线性地址,在调用时将其当作连续
存储空间进行读写。因此读写过程不需要指定Bank地址及行列地址,只需要给出起始地址和结束地址即可,数据在该地址空间中连续读写。线性地址的位宽为SDRAM的Bank地址、行地址和
列地址位宽的总和, 也可以理解成线性地址的组成结构为{ bank_addr[1:0], row_addr[12:0],col_addr[8:0]}。
程序第88行及第92行指定SDRAM控制器的数据突发长度, 由于W9825G6DH-6的全页突发长度为512, 因此控制器的突发长度不能大于512。
SDRAM读写测试模块的代码如下所示:

module sdram_test(
    input             clk_50m,          //时钟
    input             rst_n,            //复位,低有效

    output reg        wr_en,            //SDRAM 写使能
    output reg [15:0] wr_data,          //SDRAM 写入的数据
    output reg        rd_en,            //SDRAM 读使能
    input      [15:0] rd_data,          //SDRAM 读出的数据

    input             sdram_init_done,  //SDRAM 初始化完成标志
    output reg        error_flag        //SDRAM 读写测试错误标志
    );

//reg define
reg        init_done_d0;                //寄存SDRAM初始化完成信号
reg        init_done_d1;                //寄存SDRAM初始化完成信号
reg [10:0] wr_cnt;                      //写操作计数器
reg [10:0] rd_cnt;                      //读操作计数器
reg        rd_valid;                    //读数据有效标志

//*****************************************************
//**                    main code
//***************************************************** 

//同步SDRAM初始化完成信号
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) begin
        init_done_d0 <= 1'b0;
        init_done_d1 <= 1'b0;
    end
    else begin
        init_done_d0 <= sdram_init_done;
        init_done_d1 <= init_done_d0;
    end
end            

//SDRAM初始化完成之后,写操作计数器开始计数
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) 
        wr_cnt <= 11'd0;  
    else if(init_done_d1 && (wr_cnt <= 11'd1024))
        wr_cnt <= wr_cnt + 1'b1;
    else
        wr_cnt <= wr_cnt;
end    

//SDRAM写端口FIFO的写使能、写数据(1~1024)
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) begin      
        wr_en   <= 1'b0;
        wr_data <= 16'd0;
    end
    else if(wr_cnt >= 11'd1 && (wr_cnt <= 11'd1024)) begin
            wr_en   <= 1'b1;            //写使能拉高
            wr_data <= wr_cnt;          //写入数据1~1024
        end    
    else begin
            wr_en   <= 1'b0;
            wr_data <= 16'd0;
        end                
end        

//写入数据完成后,开始读操作    
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) 
        rd_en <= 1'b0;
    else if(wr_cnt > 11'd1024)          //写数据完成
        rd_en <= 1'b1;                  //读使能拉高
end

//对读操作计数     
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) 
        rd_cnt <= 11'd0;
    else if(rd_en) begin
        if(rd_cnt < 11'd1024)
            rd_cnt <= rd_cnt + 1'b1;
        else
            rd_cnt <= 11'd1;
    end
end

//第一次读取的数据无效,后续读操作所读取的数据才有效
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n) 
        rd_valid <= 1'b0;
    else if(rd_cnt == 11'd1024)         //等待第一次读操作结束
        rd_valid <= 1'b1;               //后续读取的数据有效
    else
        rd_valid <= rd_valid;
end            

//读数据有效时,若读取数据错误,给出标志信号
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n)
        error_flag <= 1'b0; 
    else if(rd_valid && (rd_data != rd_cnt))
        error_flag <= 1'b1;             //若读取的数据错误,将错误标志位拉高 
    else
        error_flag <= error_flag;
end

endmodule

SDRAM读写测试模块从写起始地址开始,连续向1024个存储空间中写入数据1~1024。写完成后一直进行读操作,持续将该存储空间的数据读出。需要注意的是程序中第97行通过变量rd_valid将第一次读出的1024个数据排除,并未参与读写测试。这是由于SDRAM控制器为了保证读FIFO时刻有数据,在读使能拉高之前就已经将SDRAM中的数据“预读”一部分(突发读长度)到读FIFO中;而此时写SDRAM尚未完成,因此第一次从FIFO中读出的512个数据是无效的。
第一次读操作结束后,读FIFO中的无效数据被读出并丢弃,后续读SDRAM得到的数据才用于验证读写过程是否正确。
LED显示模块的代码如下:

module led_disp(
    input      clk_50m,     //系统时钟
    input      rst_n,       //系统复位

    input      error_flag,  //错误标志信号
    output reg led          //LED灯             
    );

//reg define
reg [24:0] led_cnt;         //控制LED闪烁周期的计数器

//*****************************************************
//**                    main code
//***************************************************** 

//计数器对50MHz时钟计数,计数周期为0.5s
always @(posedge clk_50m or negedge rst_n) begin
    if(!rst_n)
        led_cnt <= 25'd0;
    else if(led_cnt < 25'd25000000) 
        led_cnt <= led_cnt + 25'd1;
    else
        led_cnt <= 25'd0;
end

//利用LED灯不同的显示状态指示错误标志的高低
always @(posedge clk_50m or negedge rst_n) begin
    if(rst_n == 1'b0)
        led <= 1'b0;
    else if(error_flag) begin
        if(led_cnt == 25'd25000000) 
            led <= ~led;    //错误标志为高时,LED灯每隔0.5s闪烁一次
        else
            led <= led;
    end    
    else
        led <= 1'b1;        //错误标志为低时,LED灯常亮
end

endmodule

LED显示模块用LED不同的显示状态指示SDRAM读写测试的结果:若读写测试正确无误,则
LED常亮;若出现错误(读出的数据与写入的数据不一致),则LED灯以0.5s为周期闪烁。
SDRAM控制器顶层模块如下:

module    sdram_top(
    input         ref_clk,                  //sdram 控制器参考时钟
    input         out_clk,                  //用于输出的相位偏移时钟
    input         rst_n,                    //系统复位

    //用户写端口            
    input         wr_clk,                   //写端口FIFO: 写时钟
    input         wr_en,                    //写端口FIFO: 写使能
    input  [15:0] wr_data,                  //写端口FIFO: 写数据
    input  [23:0] wr_min_addr,              //写SDRAM的起始地址
    input  [23:0] wr_max_addr,              //写SDRAM的结束地址
    input  [ 9:0] wr_len,                   //写SDRAM时的数据突发长度
    input         wr_load,                  //写端口复位: 复位写地址,清空写FIFO

    //用户读端口
    input         rd_clk,                   //读端口FIFO: 读时钟
    input         rd_en,                    //读端口FIFO: 读使能
    output [15:0] rd_data,                  //读端口FIFO: 读数据
    input  [23:0] rd_min_addr,              //读SDRAM的起始地址
    input  [23:0] rd_max_addr,              //读SDRAM的结束地址
    input  [ 9:0] rd_len,                   //从SDRAM中读数据时的突发长度
    input         rd_load,                  //读端口复位: 复位读地址,清空读FIFO

    //用户控制端口  
    input         sdram_read_valid,         //SDRAM 读使能
    output        sdram_init_done,          //SDRAM 初始化完成标志

    //SDRAM 芯片接口
    output        sdram_clk,                //SDRAM 芯片时钟
    output        sdram_cke,                //SDRAM 时钟有效
    output        sdram_cs_n,               //SDRAM 片选
    output        sdram_ras_n,              //SDRAM 行有效
    output        sdram_cas_n,              //SDRAM 列有效
    output        sdram_we_n,               //SDRAM 写有效
    output [ 1:0] sdram_ba,                 //SDRAM Bank地址
    output [12:0] sdram_addr,               //SDRAM 行/列地址
    inout  [15:0] sdram_data,               //SDRAM 数据
    output [ 1:0] sdram_dqm                 //SDRAM 数据掩码
    );

//wire define
wire        sdram_wr_req;                   //sdram 写请求
wire        sdram_wr_ack;                   //sdram 写响应
wire [23:0]    sdram_wr_addr;                  //sdram 写地址
wire [15:0]    sdram_din;                      //写入sdram中的数据

wire        sdram_rd_req;                   //sdram 读请求
wire        sdram_rd_ack;                   //sdram 读响应
wire [23:0]    sdram_rd_addr;                   //sdram 读地址
wire [15:0]    sdram_dout;                     //从sdram中读出的数据

//*****************************************************
//**                    main code
//***************************************************** 
assign    sdram_clk = out_clk;                //将相位偏移时钟输出给sdram芯片
assign    sdram_dqm = 2'b00;                  //读写过程中均不屏蔽数据线

//SDRAM 读写端口FIFO控制模块
sdram_fifo_ctrl u_sdram_fifo_ctrl(
    .clk_ref            (ref_clk),            //SDRAM控制器时钟
    .rst_n                (rst_n),            //系统复位

    //用户写端口
    .clk_write             (wr_clk),            //写端口FIFO: 写时钟
    .wrf_wrreq            (wr_en),            //写端口FIFO: 写请求
    .wrf_din            (wr_data),            //写端口FIFO: 写数据    
    .wr_min_addr        (wr_min_addr),        //写SDRAM的起始地址
    .wr_max_addr        (wr_max_addr),        //写SDRAM的结束地址
    .wr_length            (wr_len),            //写SDRAM时的数据突发长度
    .wr_load            (wr_load),            //写端口复位: 复位写地址,清空写FIFO    

    //用户读端口
    .clk_read            (rd_clk),             //读端口FIFO: 读时钟
    .rdf_rdreq            (rd_en),            //读端口FIFO: 读请求
    .rdf_dout            (rd_data),            //读端口FIFO: 读数据
    .rd_min_addr        (rd_min_addr),        //读SDRAM的起始地址
    .rd_max_addr        (rd_max_addr),        //读SDRAM的结束地址
    .rd_length            (rd_len),            //从SDRAM中读数据时的突发长度
    .rd_load            (rd_load),            //读端口复位: 复位读地址,清空读FIFO

    //用户控制端口    
    .sdram_read_valid    (sdram_read_valid), //sdram 读使能
    .sdram_init_done    (sdram_init_done),    //sdram 初始化完成标志

    //SDRAM 控制器写端口
    .sdram_wr_req        (sdram_wr_req),        //sdram 写请求
    .sdram_wr_ack        (sdram_wr_ack),        //sdram 写响应
    .sdram_wr_addr        (sdram_wr_addr),    //sdram 写地址
    .sdram_din            (sdram_din),        //写入sdram中的数据

    //SDRAM 控制器读端口
    .sdram_rd_req        (sdram_rd_req),        //sdram 读请求
    .sdram_rd_ack        (sdram_rd_ack),        //sdram 读响应
    .sdram_rd_addr        (sdram_rd_addr),    //sdram 读地址
    .sdram_dout            (sdram_dout)        //从sdram中读出的数据
    );

//SDRAM控制器
sdram_controller u_sdram_controller(
    .clk                (ref_clk),            //sdram 控制器时钟
    .rst_n                (rst_n),            //系统复位

    //SDRAM 控制器写端口    
    .sdram_wr_req        (sdram_wr_req),     //sdram 写请求
    .sdram_wr_ack        (sdram_wr_ack),     //sdram 写响应
    .sdram_wr_addr        (sdram_wr_addr),     //sdram 写地址
    .sdram_wr_burst        (wr_len),            //写sdram时数据突发长度
    .sdram_din          (sdram_din),        //写入sdram中的数据

    //SDRAM 控制器读端口
    .sdram_rd_req        (sdram_rd_req),     //sdram 读请求
    .sdram_rd_ack        (sdram_rd_ack),        //sdram 读响应
    .sdram_rd_addr        (sdram_rd_addr),     //sdram 读地址
    .sdram_rd_burst        (rd_len),            //读sdram时数据突发长度
    .sdram_dout            (sdram_dout),       //从sdram中读出的数据

    .sdram_init_done    (sdram_init_done),    //sdram 初始化完成标志

    //SDRAM 芯片接口
    .sdram_cke            (sdram_cke),        //SDRAM 时钟有效
    .sdram_cs_n            (sdram_cs_n),        //SDRAM 片选
    .sdram_ras_n        (sdram_ras_n),        //SDRAM 行有效    
    .sdram_cas_n        (sdram_cas_n),        //SDRAM 列有效
    .sdram_we_n            (sdram_we_n),        //SDRAM 写有效
    .sdram_ba            (sdram_ba),            //SDRAM Bank地址
    .sdram_addr            (sdram_addr),        //SDRAM 行/列地址
    .sdram_data            (sdram_data)        //SDRAM 数据    
    );

endmodule

SDRAM读写FIFO控制模块在SDRAM控制器的使用过程中起到非常重要的作用,它一方面通过用户接口处理读写请求,另一方面通过控制器接口完成SDRAM控制器的操作。它的存在为用户屏蔽了相对复杂的SDRAM控制器接口, 使我们可以像读写FIFO一样操作SDRAM控制器。
如程序中第162~188行所示, FIFO控制模块优先处理SDRAM写请求,以免写FIFO溢出时,用于写入SDRAM的数据丢失。当写FIFO中的数据量大于写突发长度时,执行写SDRAM操作;当读FIFO中的数据量小于读突发长度时,执行读SDRAM操作。
SDRAM控制器代码如下:

module sdram_fifo_ctrl(
    input             clk_ref,             //SDRAM控制器时钟
    input             rst_n,             //系统复位 

    //用户写端口                         
    input             clk_write,         //写端口FIFO: 写时钟 
    input             wrf_wrreq,         //写端口FIFO: 写请求 
    input      [15:0] wrf_din,             //写端口FIFO: 写数据    
    input      [23:0] wr_min_addr,         //写SDRAM的起始地址
    input      [23:0] wr_max_addr,         //写SDRAM的结束地址
     input      [ 9:0] wr_length,         //写SDRAM时的数据突发长度 
    input             wr_load,             //写端口复位: 复位写地址,清空写FIFO 

    //用户读端口                         
    input             clk_read,             //读端口FIFO: 读时钟
    input             rdf_rdreq,         //读端口FIFO: 读请求 
    output     [15:0] rdf_dout,             //读端口FIFO: 读数据
    input      [23:0] rd_min_addr,         //读SDRAM的起始地址
    input      [23:0] rd_max_addr,         //读SDRAM的结束地址
    input      [ 9:0] rd_length,         //从SDRAM中读数据时的突发长度 
    input             rd_load,             //读端口复位: 复位读地址,清空读FIFO

    //用户控制端口                         
    input             sdram_read_valid,  //SDRAM 读使能
    input             sdram_init_done,   //SDRAM 初始化完成标志

    //SDRAM 控制器写端口                 
    output reg          sdram_wr_req,         //sdram 写请求
    input             sdram_wr_ack,         //sdram 写响应
    output reg [23:0] sdram_wr_addr,     //sdram 写地址
    output       [15:0] sdram_din,         //写入SDRAM中的数据 

    //SDRAM 控制器读端口                 
    output reg        sdram_rd_req,         //sdram 读请求
    input             sdram_rd_ack,         //sdram 读响应
    output reg [23:0] sdram_rd_addr,         //sdram 读地址 
    input      [15:0] sdram_dout          //从SDRAM中读出的数据 
    );

//reg define
reg           wr_ack_r1;                    //sdram写响应寄存器      
reg           wr_ack_r2;                    
reg        rd_ack_r1;                    //sdram读响应寄存器      
reg           rd_ack_r2;                    
reg           wr_load_r1;                   //写端口复位寄存器      
reg        wr_load_r2;                   
reg           rd_load_r1;                   //读端口复位寄存器      
reg        rd_load_r2;                   
reg        read_valid_r1;                //sdram读使能寄存器      
reg        read_valid_r2;                

//wire define                            
wire       write_done_flag;              //sdram_wr_ack 下降沿标志位      
wire       read_done_flag;               //sdram_rd_ack 下降沿标志位      
wire       wr_load_flag;                 //wr_load      上升沿标志位      
wire       rd_load_flag;                 //rd_load      上升沿标志位      
wire [9:0] wrf_use;                      //写端口FIFO中的数据量
wire [9:0] rdf_use;                      //读端口FIFO中的数据量

//*****************************************************
//**                    main code
//***************************************************** 

//检测下降沿
assign write_done_flag = wr_ack_r2   & ~wr_ack_r1;    
assign read_done_flag  = rd_ack_r2   & ~rd_ack_r1;

//检测上升沿
assign wr_load_flag    = ~wr_load_r2 & wr_load_r1;
assign rd_load_flag    = ~rd_load_r2 & rd_load_r1;

//寄存sdram写响应信号,用于捕获sdram_wr_ack下降沿
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        wr_ack_r1 <= 1'b0;
        wr_ack_r2 <= 1'b0;
    end
    else begin
        wr_ack_r1 <= sdram_wr_ack;
        wr_ack_r2 <= wr_ack_r1;        
    end
end    

//寄存sdram读响应信号,用于捕获sdram_rd_ack下降沿
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        rd_ack_r1 <= 1'b0;
        rd_ack_r2 <= 1'b0;
    end
    else begin
        rd_ack_r1 <= sdram_rd_ack;
        rd_ack_r2 <= rd_ack_r1;
    end
end    

//同步写端口复位信号,用于捕获wr_load上升沿
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        wr_load_r1 <= 1'b0;
        wr_load_r2 <= 1'b0;
    end
    else begin
        wr_load_r1 <= wr_load;
        wr_load_r2 <= wr_load_r1;
    end
end

//同步读端口复位信号,同时用于捕获rd_load上升沿
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        rd_load_r1 <= 1'b0;
        rd_load_r2 <= 1'b0;
    end
    else begin
        rd_load_r1 <= rd_load;
        rd_load_r2 <= rd_load_r1;
    end
end

//同步sdram读使能信号
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        read_valid_r1 <= 1'b0;
        read_valid_r2 <= 1'b0;
    end
    else begin
        read_valid_r1 <= sdram_read_valid;
        read_valid_r2 <= read_valid_r1;
    end
end

//sdram写地址产生模块
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n)
        sdram_wr_addr <= 24'd0;    
    else if(wr_load_flag)                //检测到写端口复位信号时,写地址复位
        sdram_wr_addr <= wr_min_addr;    
    else if(write_done_flag) begin         //若突发写SDRAM结束,更改写地址
                                         //若未到达写SDRAM的结束地址,则写地址累加
        if(sdram_wr_addr < wr_max_addr - wr_length)
            sdram_wr_addr <= sdram_wr_addr + wr_length;
            else                         //若已到达写SDRAM的结束地址,则回到写起始地址
            sdram_wr_addr <= wr_min_addr;
    end
end

//sdram读地址产生模块
always @(posedge clk_ref or negedge rst_n) begin
    if(!rst_n)
        sdram_rd_addr <= 24'd0;
    else if(rd_load_flag)                 //检测到读端口复位信号时,读地址复位
        sdram_rd_addr <= rd_min_addr;
    else if(read_done_flag) begin        //突发读SDRAM结束,更改读地址
                                         //若未到达读SDRAM的结束地址,则读地址累加
        if(sdram_rd_addr < rd_max_addr - rd_length)
            sdram_rd_addr <= sdram_rd_addr + rd_length;
        else                             //若已到达读SDRAM的结束地址,则回到读起始地址
            sdram_rd_addr <= rd_min_addr;
    end
end

//sdram 读写请求信号产生模块
always@(posedge clk_ref or negedge rst_n) begin
    if(!rst_n) begin
        sdram_wr_req <= 0;
        sdram_rd_req <= 0;
    end
    else if(sdram_init_done) begin       //SDRAM初始化完成后才能响应读写请求
                                         //优先执行写操作,防止写入SDRAM中的数据丢失
        if(wrf_use >= wr_length) begin   //若写端口FIFO中的数据量达到了写突发长度
            sdram_wr_req <= 1;             //发出写sdarm请求
            sdram_rd_req <= 0;             
        end
        else if((rdf_use < rd_length)    //若读端口FIFO中的数据量小于读突发长度,
                 && read_valid_r2) begin //同时sdram读使能信号为高
            sdram_wr_req <= 0;             
            sdram_rd_req <= 1;             //发出读sdarm请求
        end
        else begin
            sdram_wr_req <= 0;
            sdram_rd_req <= 0;
        end
    end
    else begin
        sdram_wr_req <= 0;
        sdram_rd_req <= 0;
    end
end

//例化写端口FIFO
wrfifo    u_wrfifo(
    //用户接口
    .wrclk        (clk_write),             //写时钟
    .wrreq        (wrf_wrreq),             //写请求
    .data        (wrf_din),                 //写数据

    //sdram接口
    .rdclk        (clk_ref),                 //读时钟
    .rdreq        (sdram_wr_ack),             //读请求
    .q            (sdram_din),             //读数据

    .rdusedw    (wrf_use),                 //FIFO中的数据量
    .aclr        (~rst_n | wr_load_flag)  //异步清零信号
    );    

//例化读端口FIFO
rdfifo    u_rdfifo(
    //sdram接口
    .wrclk        (clk_ref),                //写时钟
    .wrreq        (sdram_rd_ack),           //写请求
    .data        (sdram_dout),               //写数据

    //用户接口
    .rdclk        (clk_read),              //读时钟
    .rdreq        (rdf_rdreq),              //读请求
    .q            (rdf_dout),                 //读数据

    .wrusedw    (rdf_use),                 //FIFO中的数据量
    .aclr        (~rst_n | rd_load_flag)  //异步清零信号   
    );

endmodule

SDRAM控制器主要例化了三个模块: SDRAM状态控制模块、 SDRAM命令控制模块、 SDRAM数据读写模块。下图为SDRAM控制器的功能框图:

图 33.4.3 FPGA SDRAM控制器功能框图.png

SDRAM状态控制模块根据SDRAM内部及外部操作指令控制初始化状态机和工作状态机;SDRAM命令控制模块根据两个状态机的状态给SDRAM输出相应的控制命令;而SDRAM数据读写模块则负责根据工作状态机控制SDRAM数据线的输入输出。
SDRAM状态控制模块代码如下所示:

module sdram_ctrl(
    input            clk,                //系统时钟
    input            rst_n,                //复位信号,低电平有效

    input            sdram_wr_req,        //写SDRAM请求信号
    input            sdram_rd_req,        //读SDRAM请求信号
    output           sdram_wr_ack,        //写SDRAM响应信号
    output           sdram_rd_ack,        //读SDRAM响应信号
    input      [9:0] sdram_wr_burst,    //突发写SDRAM字节数(1-512个)
    input      [9:0] sdram_rd_burst,    //突发读SDRAM字节数(1-256个)    
    output           sdram_init_done,   //SDRAM系统初始化完毕信号

    output reg [4:0] init_state,        //SDRAM初始化状态
    output reg [3:0] work_state,        //SDRAM工作状态
    output reg [9:0] cnt_clk,            //时钟计数器
    output reg       sdram_rd_wr         //SDRAM读/写控制信号,低电平为写,高电平为读
    );

`include "sdram_para.v"                    //包含SDRAM参数定义模块

//parameter define                      
parameter  TRP_CLK      = 10'd4;            //预充电有效周期
parameter  TRC_CLK      = 10'd6;            //自动刷新周期
parameter  TRSC_CLK      = 10'd6;            //模式寄存器设置时钟周期
parameter  TRCD_CLK      = 10'd2;            //行选通周期
parameter  TCL_CLK      = 10'd3;            //列潜伏期
parameter  TWR_CLK      = 10'd2;            //写入校正

//reg define                            
reg [14:0] cnt_200us;                   //SDRAM 上电稳定期200us计数器
reg [10:0] cnt_refresh;                    //刷新计数寄存器
reg        sdram_ref_req;                //SDRAM 自动刷新请求信号
reg        cnt_rst_n;                    //延时计数器复位信号,低有效    
reg [ 3:0] init_ar_cnt;                 //初始化过程自动刷新计数器

//wire define                           
wire       done_200us;                    //上电后200us输入稳定期结束标志位
wire       sdram_ref_ack;                //SDRAM自动刷新请求应答信号    

//*****************************************************
//**                    main code
//***************************************************** 

//SDRAM上电后200us稳定期结束后,将标志信号拉高
assign done_200us = (cnt_200us == 15'd20_000);

//SDRAM初始化完成标志 
assign sdram_init_done = (init_state == `I_DONE);

//SDRAM 自动刷新应答信号
assign sdram_ref_ack = (work_state == `W_AR);

//写SDRAM响应信号
assign sdram_wr_ack = ((work_state == `W_TRCD) & ~sdram_rd_wr) | 
                      ( work_state == `W_WRITE)|
                      ((work_state == `W_WD) & (cnt_clk < sdram_wr_burst - 2'd2));

//读SDRAM响应信号
assign sdram_rd_ack = (work_state == `W_RD) & 
                      (cnt_clk >= 10'd1) & (cnt_clk < sdram_rd_burst + 2'd1);

//上电后计时200us,等待SDRAM状态稳定
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) 
        cnt_200us <= 15'd0;
    else if(cnt_200us < 15'd20_000) 
        cnt_200us <= cnt_200us + 1'b1;
    else
        cnt_200us <= cnt_200us;
end

//刷新计数器循环计数7812ns (60ms内完成全部8192行刷新操作)
always @ (posedge clk or negedge rst_n)
    if(!rst_n) 
        cnt_refresh <= 11'd0;
    else if(cnt_refresh < 11'd781)      // 64ms/8192 =7812ns
        cnt_refresh <= cnt_refresh + 1'b1;    
    else 
        cnt_refresh <= 11'd0;    

//SDRAM 刷新请求
always @ (posedge clk or negedge rst_n)
    if(!rst_n) 
        sdram_ref_req <= 1'b0;
    else if(cnt_refresh == 11'd780) 
        sdram_ref_req <= 1'b1;            //刷新计数器计时达7812ns时产生刷新请求
    else if(sdram_ref_ack) 
        sdram_ref_req <= 1'b0;            //收到刷新请求响应信号后取消刷新请求 

//延时计数器对时钟计数
always @ (posedge clk or negedge rst_n) 
    if(!rst_n) 
        cnt_clk <= 10'd0;
    else if(!cnt_rst_n)                 //在cnt_rst_n为低电平时延时计数器清零
        cnt_clk <= 10'd0;
    else 
        cnt_clk <= cnt_clk + 1'b1;

//初始化过程中对自动刷新操作计数
always @ (posedge clk or negedge rst_n) 
    if(!rst_n) 
        init_ar_cnt <= 4'd0;
    else if(init_state == `I_NOP) 
        init_ar_cnt <= 4'd0;
    else if(init_state == `I_AR)
        init_ar_cnt <= init_ar_cnt + 1'b1;
    else
        init_ar_cnt <= init_ar_cnt;

//SDRAM的初始化状态机
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) 
        init_state <= `I_NOP;
    else 
        case (init_state)
                                        //上电复位后200us结束则进入下一状态
            `I_NOP:  init_state <= done_200us  ? `I_PRE : `I_NOP;
                                        //预充电状态
            `I_PRE:  init_state <= `I_TRP;
                                        //预充电等待,TRP_CLK个时钟周期
            `I_TRP:  init_state <= (`end_trp)  ? `I_AR  : `I_TRP;
                                        //自动刷新
            `I_AR :  init_state <= `I_TRF;    
                                        //等待自动刷新结束,TRC_CLK个时钟周期
            `I_TRF:  init_state <= (`end_trfc) ? 
                                        //连续8次自动刷新操作
                                   ((init_ar_cnt == 4'd8) ? `I_MRS : `I_AR) : `I_TRF;
                                        //模式寄存器设置
            `I_MRS:     init_state <= `I_TRSC;    
                                        //等待模式寄存器设置完成,TRSC_CLK个时钟周期
            `I_TRSC: init_state <= (`end_trsc) ? `I_DONE : `I_TRSC;
                                        //SDRAM的初始化设置完成标志
            `I_DONE: init_state <= `I_DONE;
            default: init_state <= `I_NOP;
        endcase
end

//SDRAM的工作状态机,工作包括读、写以及自动刷新操作
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) 
        work_state <= `W_IDLE;          //空闲状态
    else
        case(work_state)
                                        //定时自动刷新请求,跳转到自动刷新状态
            `W_IDLE: if(sdram_ref_req & sdram_init_done) begin
                         work_state <= `W_AR;         
                         sdram_rd_wr <= 1'b1;
                     end                 
                                        //写SDRAM请求,跳转到行有效状态
                     else if(sdram_wr_req & sdram_init_done) begin
                         work_state <= `W_ACTIVE;
                         sdram_rd_wr <= 1'b0;    
                     end                
                                        //读SDRAM请求,跳转到行有效状态
                     else if(sdram_rd_req && sdram_init_done) begin
                         work_state <= `W_ACTIVE;
                         sdram_rd_wr <= 1'b1;    
                     end                
                                        //无操作请求,保持空闲状态
                     else begin 
                         work_state <= `W_IDLE;
                         sdram_rd_wr <= 1'b1;
                     end

            `W_ACTIVE:                  //行有效,跳转到行有效等待状态
                         work_state <= `W_TRCD;
            `W_TRCD: if(`end_trcd)      //行有效等待结束,判断当前是读还是写
                         if(sdram_rd_wr)//读:进入读操作状态
                             work_state <= `W_READ;
                         else           //写:进入写操作状态
                             work_state <= `W_WRITE;
                     else 
                         work_state <= `W_TRCD;

            `W_READ:                    //读操作,跳转到潜伏期
                         work_state <= `W_CL;    
            `W_CL:                        //潜伏期:等待潜伏期结束,跳转到读数据状态
                         work_state <= (`end_tcl) ? `W_RD:`W_CL;                                            
            `W_RD:                        //读数据:等待读数据结束,跳转到预充电状态
                         work_state <= (`end_tread) ? `W_PRE:`W_RD;

            `W_WRITE:                    //写操作:跳转到写数据状态
                         work_state <= `W_WD;
            `W_WD:                        //写数据:等待写数据结束,跳转到写回周期状态
                         work_state <= (`end_twrite) ? `W_TWR:`W_WD;                         
            `W_TWR:                        //写回周期:写回周期结束,跳转到预充电状态
                         work_state <= (`end_twr) ? `W_PRE:`W_TWR;

            `W_PRE:                        //预充电:跳转到预充电等待状态
                         work_state <= `W_TRP;
            `W_TRP:                    //预充电等待:预充电等待结束,进入空闲状态
                         work_state <= (`end_trp) ? `W_IDLE:`W_TRP;

            `W_AR:                        //自动刷新操作,跳转到自动刷新等待
                         work_state <= `W_TRFC;             
            `W_TRFC:                    //自动刷新等待:自动刷新等待结束,进入空闲状态
                         work_state <= (`end_trfc) ? `W_IDLE:`W_TRFC;
            default:      work_state <= `W_IDLE;
        endcase
end

//计数器控制逻辑
always @ (*) begin
    case (init_state)
        `I_NOP:     cnt_rst_n <= 1'b0;     //延时计数器清零(cnt_rst_n低电平复位)

        `I_PRE:     cnt_rst_n <= 1'b1;     //预充电:延时计数器启动(cnt_rst_n高电平启动)
                                        //等待预充电延时计数结束后,清零计数器
        `I_TRP:     cnt_rst_n <= (`end_trp) ? 1'b0 : 1'b1;
                                        //自动刷新:延时计数器启动
        `I_AR:
                 cnt_rst_n <= 1'b1;
                                        //等待自动刷新延时计数结束后,清零计数器
        `I_TRF:
                 cnt_rst_n <= (`end_trfc) ? 1'b0 : 1'b1;    

        `I_MRS:  cnt_rst_n <= 1'b1;        //模式寄存器设置:延时计数器启动
                                        //等待模式寄存器设置延时计数结束后,清零计数器
        `I_TRSC: cnt_rst_n <= (`end_trsc) ? 1'b0:1'b1;

        `I_DONE: begin                  //初始化完成后,判断工作状态
            case (work_state)
                `W_IDLE:    cnt_rst_n <= 1'b0;
                                        //行有效:延时计数器启动
                `W_ACTIVE:     cnt_rst_n <= 1'b1;
                                        //行有效延时计数结束后,清零计数器
                `W_TRCD:    cnt_rst_n <= (`end_trcd)   ? 1'b0 : 1'b1;
                                        //潜伏期延时计数结束后,清零计数器
                `W_CL:        cnt_rst_n <= (`end_tcl)    ? 1'b0 : 1'b1;
                                        //读数据延时计数结束后,清零计数器
                `W_RD:        cnt_rst_n <= (`end_tread)  ? 1'b0 : 1'b1;
                                        //写数据延时计数结束后,清零计数器
                `W_WD:        cnt_rst_n <= (`end_twrite) ? 1'b0 : 1'b1;
                                        //写回周期延时计数结束后,清零计数器
                `W_TWR:        cnt_rst_n <= (`end_twr)    ? 1'b0 : 1'b1;
                                        //预充电等待延时计数结束后,清零计数器
                `W_TRP:    cnt_rst_n <= (`end_trp) ? 1'b0 : 1'b1;
                                        //自动刷新等待延时计数结束后,清零计数器
                `W_TRFC:    cnt_rst_n <= (`end_trfc)   ? 1'b0 : 1'b1;
                default:    cnt_rst_n <= 1'b0;
            endcase
        end
        default: cnt_rst_n <= 1'b0;
    endcase
end

endmodule

由于SDRAM控制器参数较多,我们将常用的参数放在了一个单独的文件( sdram_para.v) ,并在相应的模块中引用该文件,如代码中第19行所示。
SDRAM状态控制模块的任务可以划分为三部分: SDRAM的初始化、 SDRAM的自动刷新、以及SDRAM的读写。在本模块中我们使用两个状态机来完成上述任务,其中“初始化状态机”负责SDRAM的初始化过程;而“工作状态机”则用于处理自动刷新以及外部的读写请求。

简介部分对SDRAM的初始化流程(图 33.4.4)作了简单介绍,由此我们可以画出初始
化状态机的状态转换图如下所示:

图 33.4.4 FPGA SDRAM 初始化状态机——状态转换图.png

如上图所示, SDRAM在上电后要有200us的输入稳定期。200us结束后对所有L-Bank预充电,然后等待预充电有效周期( tRP) 结束后连续进行8次自动刷新操作,每次刷新操作都要等待自动刷新周期( tRC)。 最后对SDRAM的模式寄存器进行设置, 并等待模式寄存器设置周期( tRSC)结束。到这里SDRAM的初始化也就完成了,接下来SDRAM进入正常的工作状态。
由于SDRAM需要定时进行刷新操作以保存存储体中的数据,所以工作状态机不仅要根据外部的读写请求来进行读写操作,还要处理模块内部产生的刷新请求。那么当多个请求信号同时到达时,工作状态机该如何进行仲裁呢?
首先,为了保存SDRAM中的数据,刷新请求的优先级最高;写请求次之,这是为了避免准备写入SDRAM中的数据丢失;而读请求的优先级最低。因此,当刷新请求与读写请求同时产生时,优先执行刷新操作;而读请求与写请求同时产生时,优先执行写操作。
另外,由于刷新操作需要等待刷新周期( tRC)结束,而读写操作同样需要一定的时间(特别是突发模式下需要等待所有数据突发传输结束)。因此在上一个请求操作执行的过程中接收
到新的请求信号是很有可能的,这种情况下,新的请求信号必须等待当前执行过程结束才能得到工作状态机的响应。
工作状态机的状态转换图如下所示:

图 33.4.5 FPGA SDRAM 工作状态机——状态转换图.png

工作状态机在空闲状态时接收自动刷新请求和读写请求,并根据相应的操作时序在各个状态之间跳转。例如,在接收到自动刷新请求后,跳转到自动刷新状态(此时SDRAM命令控制模块sdram_cmd会向SDRAM芯片发送自动刷新命令),随即进入等待过程,等自动刷新周期( tRC)

结束后刷新操作完成,工作状态机回到空闲状态。

由介部分可知,无论读操作还是写操作首先都要进行“行激活”,因此工作状态机
在空闲状态时接收到读请求或写请求都会跳转到行激活状态,然后等待行选通周期( tRCD) 结束。接下来判断当前执行的是读操作还是写操作,如果是读操作,需要在等待读潜伏期结束后
连续读取数据线上的数据,数据量由读突发长度指定;如果是写操作,则不存在潜伏期,直接将要写入SDRAM中的数据放到数据线上,但是在最后一个数据放到数据线上之后,需要等待写入周期( tWR) 结束。
需要注意的是,由于W9825G6DH-6在页突发模式下不支持自动预充电,上述读写操作过程中都选择了禁止自动预充电(地址线A10为低电平)。因此在读写操作结束后,都要对SDRAM进行预充电操作,并等待预充电周期结束才回到空闲状态。
由于SDRAM的操作时序涉及到大量的延时、等待周期,程序中设置了延时计数器对时钟进行计数,如程序第90至97行所示。而初始化状态机和工作状态机不同状态下延时或等待时间不同,程序中第202至245行利用延时计数器复位信号cnt_rst_n来实现对延时计数器的控制。
为了使程序的简洁易懂, SDRAM状态控制模块中状态机的跳转及延时参数的控制条件使用了变量声明的方式。为了方便大家对程序的理解,我们将sdram_para.v中的内容也列在这里,大家可以对照其中的“延时参数”重新回顾SDRAM状态控制模块相应部分的代码:


// SDRAM 初始化过程各个状态
`define        I_NOP            5'd0                            //等待上电200us稳定期结束
`define        I_PRE             5'd1                            //预充电状态
`define        I_TRP             5'd2                            //等待预充电完成          tRP
`define        I_AR             5'd3                            //自动刷新            
`define        I_TRF            5'd4                            //等待自动刷新结束      tRC
`define        I_MRS            5'd5                            //模式寄存器设置
`define        I_TRSC            5'd6                            //等待模式寄存器设置完成 tRSC
`define        I_DONE            5'd7                            //初始化完成

// SDRAM 工作过程各个状态
`define        W_IDLE            4'd0                            //空闲
`define        W_ACTIVE        4'd1                            //行有效
`define        W_TRCD            4'd2                            //行有效等待
`define        W_READ            4'd3                            //读操作
`define        W_CL            4'd4                            //潜伏期
`define        W_RD            4'd5                            //读数据
`define        W_WRITE            4'd6                            //写操作
`define        W_WD            4'd7                            //写数据
`define        W_TWR            4'd8                            //写回
`define        W_PRE            4'd9                            //预充电
`define        W_TRP            4'd10                           //预充电等待
`define        W_AR            4'd11                           //自动刷新
`define        W_TRFC            4'd12                           //自动刷新等待

//延时参数
`define        end_trp            cnt_clk    == TRP_CLK              //预充电有效周期结束
`define        end_trfc        cnt_clk    == TRC_CLK              //自动刷新周期结束
`define        end_trsc        cnt_clk    == TRSC_CLK             //模式寄存器设置时钟周期结束
`define        end_trcd        cnt_clk    == TRCD_CLK-1           //行选通周期结束
`define     end_tcl            cnt_clk == TCL_CLK-1            //潜伏期结束
`define     end_rdburst        cnt_clk == sdram_rd_burst-4     //读突发终止
`define        end_tread        cnt_clk    == sdram_rd_burst+2     //突发读结束     
`define     end_wrburst        cnt_clk == sdram_wr_burst-1     //写突发终止
`define        end_twrite        cnt_clk    == sdram_wr_burst-1     //突发写结束
`define        end_twr            cnt_clk    == TWR_CLK                //写回周期结束

//SDRAM控制信号命令
`define        CMD_INIT         5'b01111                        // INITIATE
`define        CMD_NOP            5'b10111                        // NOP COMMAND
`define        CMD_ACTIVE        5'b10011                        // ACTIVE COMMAND
`define        CMD_READ        5'b10101                        // READ COMMADN
`define        CMD_WRITE        5'b10100                        // WRITE COMMAND
`define        CMD_B_STOP        5'b10110                        // BURST STOP
`define        CMD_PRGE        5'b10010                        // PRECHARGE
`define        CMD_A_REF        5'b10001                        // AOTO REFRESH
`define        CMD_LMR            5'b10000                        // LODE MODE REGISTER

SDRAM命令控制模块的代码如下所示:

module sdram_cmd(
    input             clk,                //系统时钟
    input             rst_n,            //低电平复位信号

    input      [23:0] sys_wraddr,        //写SDRAM时地址
    input      [23:0] sys_rdaddr,        //读SDRAM时地址
    input      [ 9:0] sdram_wr_burst,    //突发写SDRAM字节数
    input      [ 9:0] sdram_rd_burst,    //突发读SDRAM字节数

    input      [ 4:0] init_state,        //SDRAM初始化状态
    input      [ 3:0] work_state,         //SDRAM工作状态
    input      [ 9:0] cnt_clk,            //延时计数器    
    input             sdram_rd_wr,        //SDRAM读/写控制信号,低电平为写

    output            sdram_cke,        //SDRAM时钟有效信号
    output            sdram_cs_n,        //SDRAM片选信号
    output            sdram_ras_n,        //SDRAM行地址选通脉冲
    output            sdram_cas_n,        //SDRAM列地址选通脉冲
    output            sdram_we_n,        //SDRAM写允许位
    output reg [ 1:0] sdram_ba,            //SDRAM的L-Bank地址线
    output reg [12:0] sdram_addr        //SDRAM地址总线
    );

`include "sdram_para.v"                    //包含SDRAM参数定义模块

//reg define
reg  [ 4:0] sdram_cmd_r;                //SDRAM操作指令

//wire define
wire [23:0] sys_addr;                    //SDRAM读写地址    

//*****************************************************
//**                    main code
//***************************************************** 

//SDRAM 控制信号线赋值
assign {sdram_cke,sdram_cs_n,sdram_ras_n,sdram_cas_n,sdram_we_n} = sdram_cmd_r;

//SDRAM 读/写地址总线控制
assign sys_addr = sdram_rd_wr ? sys_rdaddr : sys_wraddr;

//SDRAM 操作指令控制
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) begin
            sdram_cmd_r <= `CMD_INIT;
            sdram_ba    <= 2'b11;
            sdram_addr  <= 13'h1fff;
    end
    else
        case(init_state)
                                        //初始化过程中,以下状态不执行任何指令
            `I_NOP,`I_TRP,`I_TRF,`I_TRSC: begin
                    sdram_cmd_r <= `CMD_NOP;
                    sdram_ba    <= 2'b11;
                    sdram_addr  <= 13'h1fff;    
                end
            `I_PRE: begin               //预充电指令
                    sdram_cmd_r <= `CMD_PRGE;
                    sdram_ba    <= 2'b11;
                    sdram_addr  <= 13'h1fff;
                end 
            `I_AR: begin
                                        //自动刷新指令
                    sdram_cmd_r <= `CMD_A_REF;
                    sdram_ba    <= 2'b11;
                    sdram_addr  <= 13'h1fff;                        
                end                  
            `I_MRS: begin                //模式寄存器设置指令
                    sdram_cmd_r <= `CMD_LMR;
                    sdram_ba    <= 2'b00;
                    sdram_addr  <= {    //利用地址线设置模式寄存器,可根据实际需要进行修改
                        3'b000,            //预留
                        1'b0,            //读写方式 A9=0,突发读&突发写
                        2'b00,            //默认,{A8,A7}=00
                        3'b011,            //CAS潜伏期设置,这里设置为3,{A6,A5,A4}=011
                        1'b0,            //突发传输方式,这里设置为顺序,A3=0
                        3'b111            //突发长度,这里设置为页突发,{A2,A1,A0}=011
                    };
                end    
            `I_DONE:                    //SDRAM初始化完成
                    case(work_state)    //以下工作状态不执行任何指令
                        `W_IDLE,`W_TRCD,`W_CL,`W_TWR,`W_TRP,`W_TRFC: begin
                                sdram_cmd_r <= `CMD_NOP;
                                sdram_ba    <= 2'b11;
                                sdram_addr  <= 13'h1fff;
                            end
                        `W_ACTIVE: begin//行有效指令
                                sdram_cmd_r <= `CMD_ACTIVE;
                                sdram_ba    <= sys_addr[23:22];
                                sdram_addr  <= sys_addr[21:9];
                            end
                        `W_READ: begin  //读操作指令
                                sdram_cmd_r <= `CMD_READ;
                                sdram_ba    <= sys_addr[23:22];
                                sdram_addr  <= {4'b0000,sys_addr[8:0]};
                            end
                        `W_RD: begin    //突发传输终止指令
                                if(`end_rdburst) 
                                    sdram_cmd_r <= `CMD_B_STOP;
                                else begin
                                    sdram_cmd_r <= `CMD_NOP;
                                    sdram_ba    <= 2'b11;
                                    sdram_addr  <= 13'h1fff;
                                end
                            end                                
                        `W_WRITE: begin //写操作指令
                                sdram_cmd_r <= `CMD_WRITE;
                                sdram_ba    <= sys_addr[23:22];
                                sdram_addr  <= {4'b0000,sys_addr[8:0]};
                            end        
                        `W_WD: begin    //突发传输终止指令
                                if(`end_wrburst) 
                                    sdram_cmd_r <= `CMD_B_STOP;
                                else begin
                                    sdram_cmd_r <= `CMD_NOP;
                                    sdram_ba    <= 2'b11;
                                    sdram_addr  <= 13'h1fff;
                                end
                            end
                        `W_PRE:begin    //预充电指令
                                sdram_cmd_r <= `CMD_PRGE;
                                sdram_ba    <= sys_addr[23:22];
                                sdram_addr  <= 13'h0000;
                            end                
                        `W_AR: begin    //自动刷新指令
                                sdram_cmd_r <= `CMD_A_REF;
                                sdram_ba    <= 2'b11;
                                sdram_addr  <= 13'h1fff;
                            end
                        default: begin
                                sdram_cmd_r <= `CMD_NOP;
                                sdram_ba    <= 2'b11;
                                sdram_addr  <= 13'h1fff;
                            end
                    endcase
            default: begin
                    sdram_cmd_r <= `CMD_NOP;
                    sdram_ba    <= 2'b11;
                    sdram_addr  <= 13'h1fff;
                end
        endcase
end

endmodule

SDRAM命令控制模块根据状态控制模块里初始化状态机和工作状态机的状态对SDRAM的控制信号线及地址线进行赋值,发送相应的操作命令。SDRAM的操作命令是sdram_cke、sdram_cs_n、sdram_ras_n、 sdram_cas_n、 sdram_we_n等控制信号的组合,不同的数值代表不同的指令。
W9825G6DH-6不同的操作命令与其对应的各信号的数值如下图所示(其中字母H代表高电平, L代表低电平, v代表有效, x代表不关心):

图 33.4.6 FPGA SDRAM操作指令.png

SDRAM数据读写模块代码如下:

module sdram_data(
    input             clk,                //系统时钟
    input             rst_n,            //低电平复位信号

    input   [15:0]    sdram_data_in,    //写入SDRAM中的数据
    output  [15:0]    sdram_data_out,   //从SDRAM中读取的数据
    input   [ 3:0]    work_state,        //SDRAM工作状态寄存器
    input   [ 9:0]    cnt_clk,             //时钟计数

    inout   [15:0]    sdram_data        //SDRAM数据总线
    );

`include "sdram_para.v"                    //包含SDRAM参数定义模块

//reg define
reg        sdram_out_en;                //SDRAM数据总线输出使能
reg [15:0] sdram_din_r;                    //寄存写入SDRAM中的数据
reg [15:0] sdram_dout_r;                //寄存从SDRAM中读取的数据

//*****************************************************
//**                    main code
//***************************************************** 

//SDRAM 双向数据线作为输入时保持高阻态
assign sdram_data = sdram_out_en ? sdram_din_r : 16'hzzzz;

//输出SDRAM中读取的数据
assign sdram_data_out = sdram_dout_r;

//SDRAM 数据总线输出使能
always @ (posedge clk or negedge rst_n) begin 
    if(!rst_n) 
       sdram_out_en <= 1'b0;
   else if((work_state == `W_WRITE) | (work_state == `W_WD)) 
       sdram_out_en <= 1'b1;            //向SDRAM中写数据时,输出使能拉高
   else 
       sdram_out_en <= 1'b0;
end

//将待写入数据送到SDRAM数据总线上
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) 
        sdram_din_r <= 16'd0;
    else if((work_state == `W_WRITE) | (work_state == `W_WD))
        sdram_din_r <= sdram_data_in;    //寄存写入SDRAM中的数据
end

//读数据时,寄存SDRAM数据线上的数据
always @ (posedge clk or negedge rst_n) begin
    if(!rst_n) 
        sdram_dout_r <= 16'd0;
    else if(work_state == `W_RD) 
        sdram_dout_r <= sdram_data;        //寄存从SDRAM中读取的数据
end

endmodule

SDRAM数据读写模块通过数据总线输出使能信号sdram_out_en控制SDRAM双向数据总线的输入输出,如程序第25行所示。同时根据工作状态机的状态,在写数据时将写入SDRAM中的数据送到SDRAM数据总线上,在读数据时寄存SDRAM数据总线上的数据。
图 33.4.7为SDRAM读写测试程序运行时SignalTap抓取的波形图, 图中包含了一个完整的读周期,其中rd_valid为低时读数据无效, rd_valid为高error_flag一直保持低电平,说明数据读写测试正确。

完成SDRAM初始化后可对其进行仿真验证,利用SDRAM仿真模型和设计testbench文件可对设 计 的 SDRAM 初 始 化 模 块 进 行 验 证 正 确 性 。 仿 真 需 要 用 到 是 sim 文 件 夹 中 的 sdr.v 和sdr_parameters.h两个文件, sdr_parameters.h文件主要是包含SDRAM模型的一些全局化参数
和宏定义。testbench仿真文件代码如下:

`timescale 1ns/1ns

module sdram_tb;

//reg define
reg         clock_50m;                    //50Mhz????
reg         rst_n;                        //????????

//wire define                             
wire        sdram_clk;                    //SDRAM ????    
wire        sdram_cke;                    //SDRAM ????    
wire        sdram_cs_n;                   //SDRAM ??    
wire        sdram_ras_n;                  //SDRAM ???    
wire        sdram_cas_n;                  //SDRAM ???    
wire        sdram_we_n;                   //SDRAM ???    
wire [ 1:0] sdram_ba;                     //SDRAM Bank??    
wire [12:0] sdram_addr;                   //SDRAM ?/???    
wire [15:0] sdram_data;                   //SDRAM ??    
wire [ 1:0] sdram_dqm;                    //SDRAM ????    

wire        led;                          //led???

//*****************************************************
//**                    main code
//***************************************************** 

//初始化
initial begin
  clock_50m = 0;
  rst_n     = 0;                      
  #100                                    //????100ns
  rst_n     = 1;
end

 //产生50Mhz时钟
always #10 clock_50m = ~clock_50m; 

//例化SDRAM读写测试模块
sdram_rw_test u_sdram_rw_test(
    .clk            (clock_50m),          //FPGA???50M
    .rst_n          (rst_n),              //??????????

    .sdram_clk      (sdram_clk),          //SDRAM ????
    .sdram_cke      (sdram_cke),          //SDRAM ????
    .sdram_cs_n     (sdram_cs_n),         //SDRAM ??
    .sdram_ras_n    (sdram_ras_n),        //SDRAM ???
    .sdram_cas_n    (sdram_cas_n),        //SDRAM ???
    .sdram_we_n     (sdram_we_n),         //SDRAM ???
    .sdram_ba       (sdram_ba),           //SDRAM Bank??
    .sdram_addr     (sdram_addr),         //SDRAM ?/???
    .sdram_data     (sdram_data),         //SDRAM ??
    .sdram_dqm      (sdram_dqm),          //SDRAM ????

    .led            (led)                 //?????
    );  

//例化SDRAM仿真模型   
sdr u_sdram(    
    .Clk            (sdram_clk),          //SDRAM ????
    .Cke            (sdram_cke),          //SDRAM ????
    .Cs_n           (sdram_cs_n),         //SDRAM ??
    .Ras_n          (sdram_ras_n),        //SDRAM ???
    .Cas_n          (sdram_cas_n),        //SDRAM ???
    .We_n           (sdram_we_n),         //SDRAM ???
    .Ba             (sdram_ba),           //SDRAM Bank??
    .Addr           (sdram_addr),         //SDRAM ?/???
    .Dq             (sdram_data),         //SDRAM ??
    .Dqm            (sdram_dqm)           //SDRAM ????
    );

endmodule
请登录后发表评论