SDRAM是一种可以指定任意地址进行读写的存储器, 它具有存储容量大,读写速度快的特点,同时价格也相对低廉。 因此, SDRAM常作为缓存, 应用于数据存储量大,同时速度要求较高的场合,如复杂嵌入式设备的存储器等。
SDRAM简介
SDRAM( Synchronous Dynamic Random Access Memory) ,同步动态随机存储器。 同步是指内存工作需要同步时钟,内部的命令的发送与数据的传输都以它为基准;动态是指存储阵列需要不断的刷新来保证数据不丢失;随机是指数据不是线性依次存储,而是自由指定地址进行数据读写。
SDRAM具有空间存储量大、读写速度快、价格相对便宜等优点。 然而由于SDRAM内部利用电容来存储数据, 为保证数据不丢失,需要持续对各存储电容进行刷新操作;同时在读写过程中需要考虑行列管理、 各种操作延时等,由此导致了其控制逻辑复杂的特点。SDRAM的内部是一个存储阵列, 你可以把它想象成一张表格。 我们在向这个表格中写入数据的时候, 需要先指定一个行( Row),再指定一个列( Column), 就可以准确地找到所需要的“ 单元格” ,这就是SDRAM寻址的基本原理。如图 33.1.1所示:
图 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.3为SDRAM的功能框图, SDRAM内部有一个逻辑控制单元,并且有一个模式寄存器为其提供控制参数。 SDRAM接收外部输入的控制命令,并在逻辑控制单元的控制下进行寻址、读写、刷新、预充电等操作。
在了解SDRAM的寻址原理及存储结构之后, 我们来看下如何实现SDRAM的读写。 首先, 在对SDRAM进行读写操作之前需要先对芯片进行初始化;其次, SDRAM读写是一个较为复杂的控制流程,其中包括行激活、列读写、 预充电、 刷新等一系列操作。大家需要熟练掌握每一个操作所对应的时序要求,才能够正确地对SDRAM进行读写操作。
1、芯片初始化
SDRAM芯片上电之后需要一个初始化的过程,以保证芯片能够按照预期方式正常工作,初始化流程如图 33.1.4所示:
SDRAM上电后要有200us的输入稳定期,在这个时间内不可以对SDRAM的接口做任何操作;200us结束以后给所有L-Bank预充电, 然后是连续8次刷新操作;最后设置模式寄存器。 初始化最关键的阶段就在于模式寄存器( MR, Mode Register)的设置,简称MRS( MR Set) 。
如上图所示,用于配置模式寄存器的参数由地址线提供, 地址线不同的位分别用于表示不同的参数。SDRAM通过配置模式寄存器来确定芯片的工作方式,包括突发长度( Burst Length)、潜伏期( CAS Latency) 以及操作模式等。
需要注意的是, 在模式寄存器设置指令发出之后,需要等待一段时间才能够向SDRAM发送新的指令,这个时间我们称之为模式寄存器设置周期tRSC( Register Set Cycle) 。
2、 行激活
初始化完成后, 无论是读操作还是写操作, 都要先激活( Active) SDRAM中的一行,使之处于活动状态(又称行有效) 。在此之前还要进行SDRAM芯片的片选和L-Bank的定址,不过它们与行激活可以同时进行。
从上图可以看出,在片选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中“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所示。 具体到确切的时间,则要根据时钟频率而定。
4、 数据输出(读)
在选定列地址后,就已经确定了具体的存储单元,剩下的事情就是数据通过数据I/O通道( DQ)输出到内存总线上了。但是在CAS发出之后,仍要经过一定的时间才能有数据输出,从CAS与读取命令发出到第一笔数据输出的这段时间,被定义为CL( CAS Latency, CAS潜伏期)。CL时间越短,读数据时SDRAM响应就越快。 由于CL只在读取时出现,所以CL又被称为读取潜伏期( RL, Read Latency)。 CL的单位与tRCD一样,为时钟周期数,具体耗时由时钟频率决定。
5、数据输入(写)
数据写入的操作也是在tRCD之后进行,但此时没有了CL (记住, CL只出现在读取操作中),行寻址与列寻址的时序图和上文一样,只是在列寻址时, WE#为有效状态。
从上图中可见,数据与写指令同时发送。不过,数据并不是即时地写入存储单元,数据的真正写入需要一定的周期。为了保证数据的可靠写入,都会留出足够的写入/校正时间( tWR,Write Recovery Time),这个操作也被称作写回( Write Back)。 tWR至少占用一个时钟周期或再多一点(时钟频率越高, tWR占用周期越多)。
6、突发长度
突发( Burst)是指在同一行中相邻的存储单元连续进行数据传输的方式,连续传输所涉及到存储单元(列)的数量就是突发长度( Burst Lengths,简称BL)。
上文讲到的读/写操作,都是一次对一个存储单元进行寻址。然而在现实中很少只对SDRAM中的单个存储空间进行读写, 一般都需要完成连续存储空间中的数据传输。 在连续读/写操作时,为了对当前存储单元的下一个单元进行寻址, 需要不断的发送列地址与读/写命令(行地址不变,所以不用再对行寻址),如图 33.1.11所示:
由上图可知, 虽然由于读延迟相同可以让数据的传输在I/O端是连续的,但它占用了大量的内存控制资源,在数据进行连续传输时无法输入新的命令,效率很低。为此,人们开发了突发传输技术,只要指定起始列地址与突发长度,内存就会依次地自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址。这样,除了第一笔数据的传输需要若干个周期(主要是之前的延迟,一般的是tRCD+CL)外,其后每个数据只需一个周期的延时即可获得。 如图 33.1.12所示:
至于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的单位也是时钟周期数,具体值视时钟频率而定。
自动预充电的开始时间与上图一样,只是没有了单独的预充电命令,并在发出读取命令时,A10地址线要设为高电平(允许自动预充电)。可见控制好预充电启动时间很重要,它可以在读取操作结束后立刻进入新行的寻址,保证运行效率。
写操作时,由于每笔数据的真正写入则需要一个足够的周期来保证,这段时间就是写回周期( tWR)。所以预充电不能与写操作同时进行,必须要在tWR之后才能发出预充电命令,以确保数据的可靠写入,否则重写的数据可能出错, 如图 33.1.14所示。
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所示。
测试方法
向SDRAM中写入1024个数据, 从SDRAM存储空间的起始地址写起,写完后再将数据读出, 并验证读出数据是否正确。
硬件设计
开发板上的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
所示:
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所示:
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控制器的功能框图:
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)作了简单介绍,由此我们可以画出初始
化状态机的状态转换图如下所示:
如上图所示, SDRAM在上电后要有200us的输入稳定期。200us结束后对所有L-Bank预充电,然后等待预充电有效周期( tRP) 结束后连续进行8次自动刷新操作,每次刷新操作都要等待自动刷新周期( tRC)。 最后对SDRAM的模式寄存器进行设置, 并等待模式寄存器设置周期( tRSC)结束。到这里SDRAM的初始化也就完成了,接下来SDRAM进入正常的工作状态。
由于SDRAM需要定时进行刷新操作以保存存储体中的数据,所以工作状态机不仅要根据外部的读写请求来进行读写操作,还要处理模块内部产生的刷新请求。那么当多个请求信号同时到达时,工作状态机该如何进行仲裁呢?
首先,为了保存SDRAM中的数据,刷新请求的优先级最高;写请求次之,这是为了避免准备写入SDRAM中的数据丢失;而读请求的优先级最低。因此,当刷新请求与读写请求同时产生时,优先执行刷新操作;而读请求与写请求同时产生时,优先执行写操作。
另外,由于刷新操作需要等待刷新周期( tRC)结束,而读写操作同样需要一定的时间(特别是突发模式下需要等待所有数据突发传输结束)。因此在上一个请求操作执行的过程中接收
到新的请求信号是很有可能的,这种情况下,新的请求信号必须等待当前执行过程结束才能得到工作状态机的响应。
工作状态机的状态转换图如下所示:
工作状态机在空闲状态时接收自动刷新请求和读写请求,并根据相应的操作时序在各个状态之间跳转。例如,在接收到自动刷新请求后,跳转到自动刷新状态(此时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代表不关心):
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
也贴一个我当年的做的FPGA+SDRAM设计吧。当年这个设计还有一点经济价值,现在应该没有了,代码写得太烂,有需要的可以私信我。
先看看两张效果图吧,手机拍摄效果差于实际效果。
看看NES游戏的《坦克世界》,^_^
项目背景及开发目的
目前市面上各种TFT显示专用控制器价格相对较为高昂,且存在以下缺点:
由于以上这些原因,为了满足我司TFT相关产品的不同需求,工程师选取了多种TFT控制器方案,这不利于技术复用和维护,直接增加了开发工程师和测试工程师的工作量,同时还在一定程度上增加了项目的开发风险。
本设计的目的就在于实现成本控制和技术指标设计两方面的灵活性,增强技术的复用,降低开发难度,简化设计和维护成本,降低开发工程师和测试工程师的工作量,同时减少项目风险。
项目需求及设计指标
项目方案选型(参考了ZLG的相关文档)
1. 基于CPLD + SRAM的方案。
该方案采用性价比较高的CPLD(如EMP240、A3P030 nano)和SRAM来实现,不仅成本低,而且灵活性非常大,实现的功能丰富,由于采用性价比较低的SRAM,容量有限,适合于低分辨率的TFT屏显示。
功能特点:
缺点:由于SRAM成本较高,在高分辨率时整体硬件成本会明显增加
2. 基于FPGA + SDRAM的方案
该方案采用Lattice LCMXO2-1200(或Microsemi的A3P060)和SDRAM的方式实现,同样成本低,由于Microsemi资源较为丰富,并且内部带有PLL,所以可以实现高分率的TFT显示,最高可达1024×768。
功能特点:
缺点:SDRAM存储器由于其自身特性时序相对较为复制,开发技术难度大。
控制器总体设计
TFT控制器接口设计及PIN定义说明
TFT控制器的电气符号如上图,该接口各个PIN定义如下:
clk —— 输入,控制器时钟,频率10M
nRst —— 输入,控制器外部复位,低电平有效
dataUsr[15..0] —— 三态双向IO,8080总线数据线
cs —— 输入,8080总线片选,低电平有效
rs —— 输入,8080总线寄存器地址与寄存器数据选择,rs为低表示数据线上呈现的是寄存器地址,rs为高表示数据线上呈现的是寄存器数据
wr —— 输入,8080总线写使能,低电平有效
rd —— 输入, 8080总线读使能,低电平有效
clkTft —— 输出,TFT模组的时钟
vs —— 输出,TFT模组的场信号
hs —— 输出,TFT模组的行信号
de —— 输出,TFT模组的时钟信号
rDataTft[4..0] —— 输出,TFT模组的R通道信号,5位
gDataTft[5..0] —— 输出, TFT模组的G通信信号,6位
bDataTft[4..0] —— 输出, TFT模组的B通信信号,5位
lrTft —— 输出,TFT模组水平扫描方向信号
udTft —— 输出,TFT模组垂直扫描方向信号
modeTft —— 输出,TFT模组行场模式、DE模式选择信号
ledTft —— 输出,TFT模组背光PWM控制信号
clkSdr —— 输出,SDRAM时钟信号
ckeSdr —— 输出, SDRAM时钟使能信号
nCsSdr —— 输出,SDRAM片选信号
nCasSdr —— 输出,SDRAM列地址有效信号
nRasSdr —— 输出, SDRAM行地址有效信号
dqmSdr[1..0] —— 输出,SDRAM掩码信号
baSdr[1..0] —— 输出,SDRAM的BANK地址信号
addrSdr[12..00] —— 输出,SDRAM的地址线
dataSdr[15..0] —— 三态双向IO,SDRAM的数据线
###控制器总体设计框图
整体设计框图如下,图中各模块功能分别如为:
控制器模块详细设计说明
MCU_FPGA总线接口模块
SDRAM控制器(参考了ALTERA《SDR SDRAM Controller》白皮书)
SDRAM架构框图及原理
关于SDRAM原理详情,请参见《高手进阶,终极内存技术指南》一文。
SDRAM时序
在SDRAM初始化阶段,主要用于配置SDRAM的突发模式,突发顺序,潜伏期等参数。
在掉电模式下SDRAM将进入低功耗状态,但处在该模式下的时长不能超过64ms的刷新周期,否则数据将丢失。
在时钟挂起模式下,将会关闭SDRAM内部的时步逻辑,SDRAM不再响应任何组合命令,SDRAM数据线上的数据也将被屏蔽。
因为SDRAM内部的BANK是电容式的,最大只能保持64ms,因此每行存储单元必须在64ms内执行一次自动刷新。
Self Refresh。这种mode的功耗在1mA以下。在这种状态下,SDRAM芯片自己完成refresh的操作,不需要SDRAM controller的干涉(Auto refresh需要)。既然有刷新,SDRAM中的数据是自然可以保持住的。SDRAM进入self refresh后,SDRAM controller也会disable输出到SDRAM的clock,从而整体的功耗都降低下来。
上图是需要由外部控制器发送预充电命令的突发读操作。
上图的读操作,不需要外部的SDRAM控制器发送预充电命令,在每个读操作结束后SDRAM内部会自动产生一个预充电操作。
上图是需要由外部控制器发送预充电命令的Single读操作。
上图的Single读操作,不需要外部的SDRAM控制器发送预充电命令,在每个读操作结束后SDRAM内部会自动产生一个预充电操作。
上图是改变BANK的读操作时序
上图是SDRAM在进行读操作时的DQM信号的时序图
上图是SDRAM进行页突发读的操作时序,突发长度为一页。
上图是需要由外部控制器发送预充电命令的突发写操作。
上图的写操作,不需要外部的SDRAM控制器发送预充电命令,在每个写操作结束后SDRAM内部会自动产生一个预充电操作。
上图是需要由外部控制器发送预充电命令的Single写操作。
上图的Single写操作,不需要外部的SDRAM控制器发送预充电命令,在每个读操作结束后SDRAM内部会自动产生一个预充电操作。
上图是改变BANK的写操作时序
上图是SDRAM在进行写操作时的DQM信号的时序图
上图是SDRAM进行页突发写的操作时序,突发长度为一页。
SDRAM控制器接口定义
SDRAM控制器提供如下接口
其接口PIN定义如下
SDRAM控制器设计
SDRAM控制器框图如上图,主要由以下三个模块构成:
控制接口模块对主机发出的命令解码并寄存,传送已经解码的NOP, WRITEA, READA,
REFRESH, PRECHARGE 和 LOAD_MODE 命令和 ADDR给命令模块, LOAD_REG1 和
LOAD_REG2 命令解码后, 同ADDR一起装入内部的REG1和REG2寄存器。下图给出了控制接口
模块的框图。
控制接口模块也含有一个16位的减法计数器, 和用于给命令模块产生周期刷新命令的电路,
16位的减法计数器装入REG2中的数值, 并递减到0, 但计数器为0时, 执行REFRESH_REQ输出
给命令模块,该命令一致输出直到命令模块响应该刷新请求。收到命令模块的刷新请求后,减
法计数器重新装入REG2中的数值,重复以上过程。 REG2是一个表示SDR SDRAM控制其发出
的REFRESH(刷新)命令之间的时间间隔周期,数值等于refresh_period/clock_period 的整数。
“命令模块” 接收“控制接口模块” 输出的已经解码的命令,和周期性输出的刷新请求,
并产生合适的命令给SDRAM器件, 模块含有一个简易的仲裁电路用于仲裁主机的命令和刷新控
制逻辑所产生的刷新请求。从刷新控制逻辑电路发出的刷新请求比主机接口的命令的优先级别
高。如果主机命令和隐含的刷新操作同时出现,仲裁电路在刷新操作完成之前就不发出
CMDACK应答。 如果主机操作在进行中, 收到了刷新命令, 刷新操作将延时到主机操作完成后
执行,下图给出了命令模块的框图。
在仲裁电路已经接受主机命令后, 命令被送到模块的命令发生器部分, 命令模块使用3个移
位寄存器来产生命令之间的时序,一个移位寄存器用于控制ACTIVATE命令;第二个用于控制
READA or WRITEA命令发出的时间;第三个用于对命令的持续时间定时,这样仲裁其就可以
判断最近请求的操作是否已经完成。
命令模块也实现SDRAM的地址复用,地址的行部分在ACTIVATE(RAS) 命令时复用到
SDRAM输出的A[11:0], 地址的列部分在READA(CAS) 或 WRITEA命令时复用到SDRAM地址
线上,
控制模块所产生的输出信号OE用于控制数据通路模块的DATAIN 通路的三态缓冲。
数据通路模块提供了SDRAM和主机之间的数据接口, 主机在 WRITEA 时从 DATAIN 上输入
数据,在 READA 命令时 从 DATAOUT 上取出数据。图10给出了数据通路模块的方块图,
DATAIN通路由 2段通路组成,以和对应的 CMDACK和送往SDRAM的命令的时序对齐,
DATAOUT也由2段通路构成,用于在READA 命令期间寄存SDRAM的输出数据, DATAOUT
的通路延时能够减少到1次或者不寄存,唯一的影响的是DATAIUT和CMKACK的关系会改变。
注:在本项目设计中由于时序上的原因,该模块已被裁剪掉,但为了更好地完整说明SDRAM控制器的设计,所以仍在文档中保留了该模块的描述
TFT_CTRL控制核心模块
核心控制器模块的主要组成部分及功能设计说明:
用户显存坐标=》SDRAM地址转换 ——完成用户像素逻辑坐标到SDRAM地址的映射,省去了用户在上位机由坐标计算SDRAM地址的麻烦。同时该功能块还用于用户写显存时X、Y坐标在指定矩形操作区内的的自动增长维护。
SDRAM带宽仲裁 ——由于SDRAM的接口并不是并行,在TFT模块从SDRAM读取数据刷新屏模式,用户的不能SDRAM进行读写操作,因此需要一个SDRAM带宽仲裁模块来对SDRAM的操作带宽进行合理分配。本设计中包含有三个FIFO模块,其中FIFO_RD_TFT是用于缓存TFT从SDRAM读取的数据,FIFO_WR_USR用于缓存用户写写和数据,FIFO_RD_USR用于缓存用户从SDRAM读取的数据。在TFT读取SDRAM数据时,若有用户往控制器写入数据,将会被缓存在FIFO_WR_USR中,在本设计中由于仲裁器留给用户的SDRAM平均写入带宽远大于用户通过8080接口写入的带宽,因此FIFO不会溢出,用户也不会感觉到数据被缓存;当用户往控制器发送读数据命令时,若此时SDRAM处于TFT读操作中,该命令将会暂缓执行,直至TFT读取SDRAM数据操作结束,在本设中于TFT读取SDRAM操作的周期很短,仲裁器留给SDRAM的用户平均读取操作远大于用户通过8080接口读取的带宽,因此用户不会感觉到数据的平连续。
TFT_CTRL控制状态机 —— 这是TFT控制核心模块的核心,该状态机用于控制TFT_CTRL模块内部的状态切换,实现一种类似CPU的流程化控制。
TFT像素坐标=》SDRAM地址转换 —— 实现TFT像素坐标到SDRAM地址的转换,同时控制TFT刷新时X、Y坐标的自动增长控制。
TFT_CTRL控制核心状态机
状态机各状态含义:
WAITE_SDR_INIT ——等待SDRAM初始化完成
AUTO_REF_SDR —— 执行SDRAM自动刷新
RD_SDR_TFT —— 执行从SDRAM读取TFT刷屏数据
RD_SDR_USR —— 执行用户读显存操作
WR_SDR_USR —— 执行用户写显存操作
NONE_OP_TFT —— TFT无数据请求
NONE_OP_USR —— 无外部用户请求
在控制器上电后,首先控制器内部的多路时钟生成与系统复位控制模块会延时一段时间,以等待FPGA稳定,之后内部PLL产生一个CLK_LOCK信号,这个CLK_LOCK信号表明PLL已能稳定输出时钟,此时产生一个复位信号用于作为整个控制器的内部复位信号。内部复位完成之后,开始进行SDRAM的初始化工作,经过一系统的SDRAM控制命令之后,SDRAM的内部MODE寄存器成功加载数据,此时即完成了SDRAM的初始化,在这之前状态机始终处理WAITE_SDR_INIT,TFT时序发生器一直处于复位状态。WAITE_SDR_INIT结束后,进入到AUTO_REF_SDR状态,进行SDRAM非初始化阶段的首次自动刷新操作。AUTO_REF_SDR状态同时会结束TFT时序发生器的复位。直至SDRAM自动刷新命令成功完成,状态机始终处理AUTO_REF_SDR。自动刷新命令完成后,若此时TFT模块请求读取SDRAM那么将会进入RD_SDR_TFT状态,反之则进入NONE_OP_TFT状态。在进入RD_SDR_TFT状态后,状态机将始终处于RD_SDR_TFT状态直至数据读取完成。在RD_SDR_TFT或NONE_OP_TFT状态结束之后,若有用户操作请求则进入WR_SDR_USR或RD_SDR_USR状态或NONE_OP_USR状态。若FINISH_OP_SDR状态之后wrUsrFlag标志置位则进入RD_SDR_USR状态,若rdUsrFlag标志置位则进入WR_SDR_USR状态,若二者都未置位则进入NONE_OP_USR状态。进入WR_SDR_USR状态后,直至完成用户数据向SDRAM写入状态机都将始终处于WR_SDR_USR状态。结束WR_SDR_USR状态后,若autoRefFlag置位则进入到AUTO_REF_SDR状态。RD_SDR_USR、NONE_OP_USR状态的跳转逻辑与WR_SDR_USR相同。至此,整个状态机周而复始重复以上过程。
TFT时序控制器
TFT行场模式时序
在TFT行场模式下,HS行信号在VS场信号的t_W时段有效,DCLK像素时钟信号在HS行信号的t_HV段内有效,在t_HV时段内,每个DCLK下降沿TFT采样一个数据。在t_HV时段内,TFT完成一行图像的刷新,在t_W时段内TFT完成一幅图像的刷新。
TFT DE模式时序
在DE模式下,在DE有效的情况下,每个DCLK下降沿TFT采样一个数据,直至完成一幅图像的刷新。
TFT时序控制器设计
行同步计数器 —— 计数DCLK,用于产生行同步信号
场同步计数器 —— 计数行同步计数器的翻转次数,用于产生场同步信号
行信号生成逻辑 —— 产生行同步脉冲时序
场信号生成逻辑 —— 产生场同步脉冲时序
DE信号生成逻辑 —— 产生DE同步脉冲时序
数据请求生成逻辑 —— 产生数据请求信号,请求TFT_CTRL模块从SDRAM读取显存数据
###TFT背光PWM控制模块
背光寄存器 —— 用于寄存用户写入的背光值,该值由TFT_CTRL输出
PWM计数比较器 —— 计数背光模块时钟,并与背光寄存器中的值进行比较,输出PWM波
###多路时钟生成,与系统复位控制模块
PLL/DLL锁相环 —— 器件内置的PLL或DLL硬核IP,用于倍频和分频产生系统内部所需的25M,100M,与源时钟有一定相移的100M时钟。
内部系统延时复位模块 —— 用于等待上电后外部时钟,同步控制器外部的复位信号,同时等待内部PLL时钟锁定,综合以上信号生成用于系统内部的最终复位信号。