# 基于FPGA和IS42S16320D的SDRAM控制器设计 **Repository Path**: huifeng28/sdram_controller ## Basic Information - **Project Name**: 基于FPGA和IS42S16320D的SDRAM控制器设计 - **Description**: 记录一下自己的毕设项目,当时第一次接触FPGA,踩了很多坑,希望此贴可以帮助大家设计SDRAM控制器。 项目提出了一种基于FPGA和IS42S16320D的SDRAM控制器的设计方法。根据SDRAM的操作时序,使用Verilog语言采用自顶向下的设计方法,并对该控制器进行RTL级仿真和板级验证。在此基础上,利用DE10-Lite开发板的组件提供一种便捷的验证该SDRAM控制器的方法。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 2 - **Created**: 2023-11-03 - **Last Updated**: 2025-08-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 基于FPGA和IS42S16320D的SDRAM控制器设计 --- #### 介绍 记录一下自己的毕设项目,当时第一次接触FPGA,踩了很多坑,希望此贴可以帮助大家设计SDRAM控制器。 项目提出了一种基于FPGA和IS42S16320D的SDRAM控制器的设计方法。根据SDRAM的操作时序,使用Verilog语言采用自顶向下的设计方法,并对该控制器进行RTL级仿真和板级验证。在此基础上,利用DE10-Lite开发板的组件提供一种便捷的验证该SDRAM控制器的方法。 --- # 一、硬件环境 SDRAM芯片是ISSI生产的IS42S16320D。该芯片使用管道架构实现高速的数据传输,每64ms需要刷新8k次。 常用的**命令真值**表,如下表所示。 ![输入图片说明](png/%E5%9B%BE%E7%89%871.png) 芯片IS42S16320D的**引脚的定义与功能**描述如下表所示。 ![输入图片说明](png/%E5%9B%BE%E7%89%872.png) IS4216320D的主要**时钟**参数如下表所示。 ![输入图片说明](png/%E5%9B%BE%E7%89%8720.png) # 二、模块设计 该SDRAM控制器使用Verilog HDL(Verilog)语言设计。 SDRAM控制器的模块划分采用自顶向下的设计方法。为了提高SDRAM控制器代码的可读性,模块的设计采用**多段式有限状态机(Finite State Machine,FSM)**描述。具体方法如下:通过使用always块,一个always块利用同步时序逻辑描述状态转移;一个always块利用同步时序逻辑描述状态输出(有些模块会再使用一个always块利用组合逻辑描述状态转移条件)。 ## SDRAM状态转换 根据SDRAM的时序,我们可以将SDRAM的操作流程用状态转换图表示出来 ![输入图片说明](png/%E5%9B%BE%E7%89%873.png) 该SDRAM控制器整体框架如图所示。 ![输入图片说明](png/%E5%9B%BE%E7%89%874.png) ### 顶层模块 #### 状态转换 ![输入图片说明](png/%E5%9B%BE%E7%89%875.png) #### 状态机分析 SDRAM控制器开始运行时,需要初始一个上电复位信号。上电复位是十分重要的,如果没有复位,会导致一些寄存器的初始值变得未知,如果此时SDRAM控制器就开始工作的话,极易出现错误。复位完成后进入初始化状态Init_state,初始化内部的状态将在初始化模块进行介绍。当初始化完成后(init_end_flag为高电位),进入空闲状态idle_state。 SDRAM大多数时间都会处在空闲状态,它相当于一个顶层模块,用来判断SDRAM下一步进行读,写或刷新操作。优先级顺序为刷新 > 读 > 写。 刷新操作,由刷新信号refresh_flag来进行判断,如果refresh_flag为高电位,则进入刷新状态Auto_Refresh_state。刷新信号怎么获得呢?该SDRAM每64ms需要刷新8192次,所以我们需要对64ms进行计数,由于该SDRAM为100MHz,相当于1个周期10ns,所以需要计数6400000个周期。但是由于一次刷新8192次,所以需要预留一定的时间进行刷新。这里设置在63.4ms处开始刷新,将刷新信号拉高,等到进入刷新状态后再将刷新信号拉低,计数器需要每隔64ms重新清零。 然后是判断读写操作,由读使能信号RE和写使能信号WR进行判断,当两者任何一个为高电位且refresh_flag为低电位的时候,进入激活状态activate_state。 激活状态需要tRCD(15ns)个时间来进入读/写操作。对于短时间的延迟,我们直接以状态替代延时,比如15ns需要执行2个时钟周期,就可以在activate_state后设置1个activate_NOP_state状态来满足要求。然后分别根据读写使能信号来进入读状态read_state或写状态write_state。读写操作完成后仍回到空闲状态。 #### 状态机操作 首先是对命令进行操作。在Init_Load_mode_register_state、Init_PALL_state和Init_ Auto_Refresh_state分别赋值模式寄存器配置、预充电命令和自动刷新命令,其余时刻执行NOP命令。 然后是对Bank和A0-A12地址进行操作。在Init_PALL_state将init_ba和init_addr赋值全为1;在Init_Load_mode_register_state需要设置模式寄存器,其余时刻都为0。关于模式寄存器设置,由于本次实验是对小数据进行单次测试,这里我们设置突发长度为1,顺序突发,列选潜伏期为2。 ### 初始化模块 #### 状态转换 ![输入图片说明](png/%E5%9B%BE%E7%89%876.png) #### 状态机分析 首先需要1个100us的上电延迟。这里我们设置1个计数器,由于该SDRAM为100MHz,相当于1个周期10ns,所以需要计数10000个周期,在计数期间,令其一直处在Init_NOP_state。在计数结束时将标志拉高powerup_complete_flag,并进入初始化预充电状态Init_PALL_state。初始化预充电需要tRP(15ns)个时间,所以还需要设置Init_PALL_state_NOP1_state。 预充电后进入初始化的刷新状态Init_Auto_Refresh_state。初始化的刷新至少需要2次,每次需要tRC(60ns)个时间。这里我们还需要设置6个时钟周期,以预防一些意外情况发生。(在实际设计中,最初加上Init_Auto_Refresh_state只设计了6个时钟,但是在仿真过程中出现了tRC时序冲突)所以设置Init_Auto_Refresh_NOP(1-6)_state。每次自动刷新都会使计数器count_auto_ refresh_init加1。当到达状态6时,判断是否完成2次刷新,如果只完成一次,则返回Init_Auto_Refresh_state。完成2次刷新后进入模式寄存器配置状态Init_Load_mode_register_state并将计数器清零。 模式寄存器配置状态需要tMRD(14ns)个时间,所以还需要额外设置1个时钟周期Init_Load_mode_register_NOP_ state。结束后,完成SDRAM的初始化,将init_end_flag拉高。 #### 状态机操作 首先是对命令进行操作。在Init_Load_mode_register_state、Init_PALL_state和Init_ Auto_Refresh_state分别赋值模式寄存器配置、预充电命令和自动刷新命令,其余时刻执行NOP命令。 然后是对Bank和A0-A12地址进行操作。在Init_PALL_state将init_ba和init_addr赋值全为1;在Init_Load_mode_register_state需要设置模式寄存器,其余时刻都为0。关于模式寄存器设置,由于本次实验是对小数据进行单次测试,这里我们设置突发长度为1,顺序突发,列选潜伏期为2。 ### 刷新模块 #### 状态转换 ![输入图片说明](png/%E5%9B%BE%E7%89%877.png) #### 状态机分析 每次自动刷新需要执行8192次,每次需要tRC(60ns)个时间,其步骤与初始化刷新类似。在自动刷新状态Auto_Refresh_64ms_state,其后设置6个状态Auto_Refresh_NOP(1-6)_state的延时来满足tRC,在状态6判断计数器是否完成8192次。如果没有完成,则返回Auto_Refresh_64ms_state,如果全部完成则将刷新结束信号refresh_end_flag拉高,计数器清零,刷新操作,结束进入空闲状态idle_state。 在空闲状态时,当刷新使能信号refresh_flag为高时,才会进入自动刷新状态。 #### 状态机操作 此模块只需要对状态命令进行设置,在Init_Auto_Refresh_state赋值自动刷新命令,其余时刻执行NOP命令。 ### 读模块 #### 状态转换 ![输入图片说明](png/%E5%9B%BE%E7%89%878.png) #### 状态机分析 进入读状态read_state,在CAS latency为2的时候,需要持续四个时钟周期,所以这里还需设置NOP_read_state1、NOP_read_state2和NOP_read_state3。完成后,进入预充电状态Precharge_state。 预充电状态到下一次操作前需要tRP(15ns)个时间,需要两个时钟周期,所以还需设置Precharge_NOP_state。预充电完成后返回空闲状态idle_state。 在空闲状态,对刷新使能refresh_flag和读使能re进行判断。如果re为高电位且refresh_flag为低电位才能进入读状态,其他时候则保持空闲状态。 #### 状态机操作 首先是对命令进行操作。在read_state和Precharge_state赋值读命令和预充电命令,其余时刻执行NOP命令。 然后是对Bank和A0-A12地址进行操作。在read_state时,将用户选择的Bank和列地址addr赋值到read_ba和read_addr;在Precharge_state将所有Bank设置为1全部预充电,其余时刻为0。 在读取的任何时刻,数据掩码dqm都需要拉低,其余时刻将其拉高。数据的读出,在clk_3ns时钟下,只有当读取的最后一个状态,才能读取数据,所以在NOP_read_state3时,将SDRAM数据赋值给data。 ### 写模块 状态转换 ![输入图片说明](png/%E5%9B%BE%E7%89%879.png) #### 状态机分析 进入写状态write_state,因为写操作没有列选潜伏期,所以根据3.2.4的时序图只需要持续3个时钟周期,所以这里还需设置NOP_write_state1和NOP_write_state2。完成后,进入预充电状态Precharge_state。 预充电状态到下一次操作前需要tRP(15ns)个时间,需要两个时钟周期,所以还需设置Precharge_NOP_state。预充电完成后返回空闲状态idle_state。 在空闲状态,对刷新使能refresh_flag和写使能wr进行判断。如果wr为高电位且refresh_flag为低电位才能进入写状态,其他时候则保持空闲状态。 #### 状态机操作 首先是对命令进行操作。在write_state和Precharge_state赋值写命令和预充电命令,其余时刻执行NOP命令。 然后是对Bank和A0-A12地址进行操作。在write_state时,将用户选择的Bank和列地址addr赋值到write_ba和write_addr;在Precharge_state将所有Bank全部预充电,其余时刻为0。 在写入操作下,只有在write_state时才能进行数据写入,所以在write_state时将数据掩码dqm拉低,其余时刻将其拉高。数据的写入,write_state时,将data数据写入SDRAM中。 ## 模块优化 ### 连续读和连续写 上文中所提及的读写操作采用的是单个读写操作,每次读写完成后,回到预充电状态,然后再回到空闲状态。如果进行下一次读写操作,需要重新进行激活,然后再进行读写操作。如果是采用连续读和连续写操作,每次读写操作完成后,只需要读写使能信号(RE / WR)依旧为高电位且刷新信号为低电位时,便可以直接进行下一次读写操作,从而跳过了预充电和激活状态。大大提高SDRAM控制器的读写效率。 具体操作是在读写操作的最后一个状态,预充电的前一个状态添加判断条件,如果读写使能信号依旧为高电位,刷新信号为低电位的时候,继续执行读/写操作,否则跳到预充电状态。 ### 反馈机制 SDRAM控制器可以看成是一个综合的内部系统。当用户需要写入数据后,向SDRAM控制器输入写使能信号WR,然后SDRAM控制器控制SDRAM进行写入。但是用户不知道系统有没有完成这次写入。因此,需要相应的响应信号,来帮助SDRAM控制器系统对用户进行反馈。 在sdram_controller模块添加2个引脚,RD_REQUEST_APPROVED和WR_REQUEST_APPROVED为输出信号。在每次读写操作开始的时候将其拉高,在读写操作完成后将其拉低。 ### 刷新模式优化 市面上大多数SDRAM控制器的刷新模式都是采用间隔一段时间刷新一次。以该SDRAM为例,每64ms刷新8192次,64ms / 8192=7.812us,也就是说需要每7.812us刷新一次。但是,如果当时状态是处于读写操作,因为读写操作与刷新操作是同级的,所以刷新操作需要预留出一定时间。这个时间是由突发长度决定的,以该SDRAM控制器为例,读写操作至少为40ns,预充电操作至少为20ns,每个操作前后预留一个周期的时间,这里设置一共预留100ns。此外,从空闲状态到刷新状态需要tRP(15ns),从刷新状态跳出需要tRC(15ns),所以至少4个周期40ns。这些时间看起来并不大,但是每64ms乘以8192次就是一个比较大的数字。所以本文设计的控制器系统将其一次刷新完,会大大提高SDRAM控制器的存储和写入效率。 # 三、仿真验证 SDRAM控制器程序的编译与仿真是基于ModelSim SE 10.4。 ## TestBench 仿真测试(TestBench)相当于计算机替代用户对SDRAM控制器进行具体的操作,目的是为了对电路进行仿真验证,测试设计电路的功能、性能与设计的预期是否相符。 仿真的TestBench具体操作描述如下:SDRAM控制器系统运行后,间隔100ns进行复位。(复位条件是必要的,如果没有复位,会导致寄存器的初始值变得未知,如果此时SDRAM控制器就开始工作的话,极易导致错误)复位完成后,延时101us,测试初始化模块。 测试连续写模块:延时10ns(SDRAM控制器1个周期为10ns),将WR信号拉高;延时10ns,地址位输入0,写入数据16’d8;延时20ns,因为写操作至少需要3个周期。延时10ns,令地址位为1,写入数据16’d1;延时20ns,写操作延时;延时10ns,将WR信号拉低。 测试连续读模块:接上述操作后,延时200ns,将RE信号拉高,地址位输入0;延时40ns,读操作至少持续4个周期;延时10ns,令地址位为1;延时40ns,读操作延时;延时10ns,将RE信号拉低。 编写完成后,例化一个虚拟SDRAM的仿真模型sdram_model_plus。 运行如下图所示。 ![输入图片说明](png/%E5%9B%BE%E7%89%8710.png) ![输入图片说明](png/%E5%9B%BE%E7%89%8711.png) ![输入图片说明](png/%E5%9B%BE%E7%89%8712.png) ## 仿真波形 ### 初始化模块仿真波形 ![输入图片说明](png/%E5%9B%BE%E7%89%8713.png) ### 写模块仿真波形 ![输入图片说明](png/%E5%9B%BE%E7%89%8714.png) ### 读模块仿真波形 ![输入图片说明](png/%E5%9B%BE%E7%89%8715.png) ### 刷新模块仿真波形 ![输入图片说明](png/%E5%9B%BE%E7%89%8716.png) # 四、板级验证 本文板级验证选取的开发板为DE10-Lite。 软件使用Altera公司的Quartus II。 ## 搭建环境 对于DE10-Lite用户,Intel提供System Builder程序来快速创建De10-Lite所需要的环境工程。 ![输入图片说明](png/%E5%9B%BE%E7%89%8717.png) 本次板级验证工程需要用到的引脚有CLOCK、LED、Button和Switch。 ## 模块设计 为了简化用户操作,使用DE10-Lite的组件进行操控:SW开关控制SDRAM的写入,LED灯表示SDRAM的读取,Button按钮控制读取和写入操作的选择。 ![输入图片说明](png/%E5%9B%BE%E7%89%8718.png) ### 按键消抖模块 由于机械按键的物理特性,按键被按下的过程中,存在一段时间的抖动,同时,在按键释放的过程中也存在抖动,这就导致在识别的时候可能检测为多次的按键按下。通常检测到一次按键输入信号的状态为低电平,按键释放信号的状态为高电平,系统就会认为按键被按下。所以在使用按键时往往需要消抖,以确保按键每被按下一次只检测到一次低电平,释放的时候只检测到一次高电平。 ### 锁相环模块 PLL(Phase Locked Loop):为锁相回路或锁相环,是一种反馈控制电路,其特点是利用外部输入的参考信号控制环路内部振荡信号的频率和相位。 ### 顶层模块 例化三个子模块,并写入一个测试程序。 首先,在2s后,对整个系统进行上电复位。上电复位是十分重要的,如果没有复位,会导致一些寄存器的初始值变得未知,如果此时SDRAM控制器就开始工作的话,极易出现错误。 复位完成后,可进行读写操作的选择。按下KEY0,进行读操作。按下KEY1,进行写操作。 写操作,该系统使用De-10Lite的10个SW开关进行写入数据,写入的数据存储在Bank为0和地址为0的位置。注意,每个SW开关代表二进制,但是DQ为16位数据,所以写入的数据是将2进制转化为16进制。 读操作,该系统使用De10-Lite的10个LED灯进行读取数据,每个LED灯对应相应的SW开关。读取数据位置是在Bank为0和地址为0。 ## 验证 Intel对于De10-Lite用户提供了一个便于测试和验证的Control Panel程序。 ![输入图片说明](png/%E5%9B%BE%E7%89%8719.png) ### 验证方法 #### 开发板验证 首先,整个板级工程在Quartus编译完成后,下载到De10-Lite开发板上。待其上电稳定后,通过板上的KEY1和SW开关,将数据写入到SDRAM中。然后按下KEY0,观察LED亮灯的位置与SW开关是否一一对应。 #### Control Panel验证 按照上述操作将数据输入进SDRAM后,将Control Panel与De10-Lite进行连接,并读取地址为0的数据。将读取的十六进制数据转化为2进制,并与De10-Lite上的SW开关进行比较。