详解SDRAM基本原理以及FPGA实现读写控制

文章目录

  • 一、SDRAM简介
  • 二、SDRAM存取结构以及原理
    • [2.1 BANK以及存储单元结构](#2.1 BANK以及存储单元结构)
    • [2.2 功能框图](#2.2 功能框图)
    • [2.3 SDRAM速度等级以及容量计算](#2.3 SDRAM速度等级以及容量计算)
  • 三、SDRAM操作命令
    • [3.1 禁止命令: 4'b1xxx](#3.1 禁止命令: 4'b1xxx)
    • [3.2 空操作命令:4'b0111](#3.2 空操作命令:4'b0111)
    • [3.3 激活命令:4'b0011](#3.3 激活命令:4'b0011)
    • [3.4 读命令:4'b0101](#3.4 读命令:4'b0101)
    • [3.5 写命令 :4'b0100](#3.5 写命令 :4'b0100)
    • [3.6 突发中止命令 :4'b0110](#3.6 突发中止命令 :4'b0110)
    • [3.7 预充电命令 :4'b0010](#3.7 预充电命令 :4'b0010)
    • [3.8 刷新命令:4'b0001](#3.8 刷新命令:4'b0001)
    • [3.9 配置模式寄存器命令:4'b0000](#3.9 配置模式寄存器命令:4'b0000)
  • 四、FPGA实现SDRAM读写操作
    • [4.1 系统框图](#4.1 系统框图)
    • [4.2 初始化模块](#4.2 初始化模块)
      • [4.2.1 波形图](#4.2.1 波形图)
      • [4.2.2 Verilog代码](#4.2.2 Verilog代码)
      • [4.2.3 仿真代码](#4.2.3 仿真代码)
      • [4.2.4 仿真结果观测](#4.2.4 仿真结果观测)
    • [4.3 自动刷新模块](#4.3 自动刷新模块)
      • [4.3.1 波形图](#4.3.1 波形图)
      • [4.3.2 Verilog代码](#4.3.2 Verilog代码)
      • [4.3.3 仿真代码](#4.3.3 仿真代码)
      • [4.3.4 仿真结果观测](#4.3.4 仿真结果观测)
    • [4.4 写操作模块](#4.4 写操作模块)
      • [4.4.1 波形图](#4.4.1 波形图)
      • [4.4.2 Verilog代码](#4.4.2 Verilog代码)
      • [4.4.3 仿真代码](#4.4.3 仿真代码)
      • 4.4.4仿真结果观测

一、SDRAM简介

SDRAM是"Synchronous Dynamic Random Access Memory"的缩写,也叫同步动态随机存取器。因为其单位存储量大、高数据带宽、读写速度快、价格相对便宜等优点被广泛使用在各行各业。同时,其升级版的DDR作为电脑内存也被广泛使用。

  • 同步:是指SDRAM工作需要同步时钟,内部的命令的发送与数据的传输都以此时钟为基准,同步时钟是由控制器(CPU/FPGA)发出;
  • 动态:是指SDRAM需要不断的刷新来保证数据不丢失;
  • 随机:是指数据不是线性依次存储,而是自由指定地址进行数据读写。

二、SDRAM存取结构以及原理

2.1 BANK以及存储单元结构

SDRAM存取数据结构不同于FIFO和RAM,可以把SDRAM存取结构类比成表格结构,如下图所示。想要对一个存储单元进行读写操作,可以通过行地址列地址来定位到所想要操作的存储单元。

把这种N行N列的"表格"称为一个逻辑BANK也叫 L-bank。通常SDRAM里面存在多个逻辑BANK,因此想要读写操作需要先指定一个逻辑BANK然后通过行地址和列地址定位到想要读写的存储单元。"表格"中的一个单元格是SDRAM的一个存储单元,一个存储单元里可以存放一个或者多个bit的数据,其中存放一个bit的存储单元结构入下图所示:

由上图可以看出,一个存储单元是由两个选通三极管以及电容组成。当电容的电荷高于设定阈值时,表示存储的为高电平,否者为低电平。由于电容不是理想的电容,因此电容的电荷量会随着时间的移动慢慢减少。所以需要定时不断地给电容充电来保持数据的稳定,这就体现了SDRAM的动态特性。

2.2 功能框图

打开芯片数据手册,我们可以看到SDRAM功能框图如下:

由功能框图可以看出,SDRAM结构包含一个逻辑控制单元,其中包含了(命令解码单元和模式寄存器)、包含了一个地址寄存器(行列地址复用)、刷新计数器、BANK控制器、四个BANK、以及数据输出寄存器等等。外部通过 CS_N、RAC_N、CAS_N、WE_N 以及地址总线向逻辑控制单元输入命令,命令经过命令解码器进行译码后,将控制参数保存到模式寄存器中,逻辑控制单元进而控制逻辑运行。SDRAM器件引脚图如下所示:

# 符号表示该信号为低电平有效。破折号 (-) 表示 x8 和 x4 引脚功能与 x16 引脚功能相同。以镁光生产的4 Meg x 16 x 4 banks的SDRAM为例,引脚说明如下:

引脚名称 位宽 类型 功能描述
CLK 1bit input SDRAM工作所需的系统时钟:所有信号都在CLK上升沿采样
CKE 1bit input 时钟使能信号:高电平时才能操作SDRAM
CS# 1bit input 片选信号:低电平时有效,高电平时屏蔽所有输入输出端口,CLK,CKE,DQM除外
CAS# 1bit input 列选通信号:低电平时A[8:0]为输入的列地址
RAS# 1bit input 行选通信号:低电平时A[12:0]为输入的行地址
WE# 1bit input 写使能信号:低电平时有效,{CS#,CAS#,RAS#,WE#}构成SDRAM操作命令
DQM[1:0] 2bit input 数据掩码信号:高位掩码高字节数据,低位掩码低字节数据
BA[1:0] 2bit input BANK地址信号
A[12:0] 13bit input 地址总线,不同命令下有不同含义
DQ[15:0] 16bit inout 输入和输入数据复用的数据总线

2.3 SDRAM速度等级以及容量计算

打开芯片数据手册,最开始位置我们可以看到本数据手册支持这三种SDRAM。

  • MT48LC16M16A2:代表镁光公司生产的SDRAM型号,不同厂商生产的SDRAM命名各不相同。
  • 4 Meg x 16 x 4 banks:4 Meg表示一个BANK的存储单元数量,16表示一个存储单元所占的bit数,4bank表示一共有四个bank

SDRAM容量数=一个bank存储单元数量✖一个存储单元的位宽✖bank数量。例如:4 Meg x 16 x 4 banks的SDRAM容量= 8192✖4096✖16✖4 = 2147483648 bit = 268435456 Byte=256MB

由上图可以看出,不同速度等级对应的最大系统时钟频率不同。CL为列选通潜伏期, t R C D ^tRCD tRCD, t R P ^tRP tRP分别为自激活等待时间和预充电等待时间。

三、SDRAM操作命令

对SDRAM的操作都是通过由{CS_N、RAS_N、CAS_N、WE_N} 四路控制信号构成指令来的, 数据手册也提供了SDRAM的指令集,如图所示:

3.1 禁止命令: 4'b1xxx

无论CKE是否有效,COMMAND INHIBIT (禁止命令)都会阻止器件执行新命令。该设备被取消选择,已经进行的操作命令不受影响。

3.2 空操作命令:4'b0111

该命令给被选中的 SDRAM 芯片传递一个空操作信息,目的是为了防止 SDRAM 处于空闲或等待状态时,SDRAM被其他命令写入,已经进行的操作命令不受影响。

3.3 激活命令:4'b0011

该命令用于激活指定bank中的某一行。{BA0,BA1}的值用于选择哪个bank,地址总线A[12:0]用于选择哪一行。激活该行后,该行一直保持激活状态,并可以进行后续读/写操作,操作完成后,只有执行一次预充电命令(Precharge)后,被激活的特定行被关闭。每次激活只能激活一个Bank,同一个Bank 中每次只能激活一行,当需要对同一 L-Bank 中其他行进行操作时, 必须先执行一个预充电命令关闭当前行,再激活另一行进行操作。激活命令示意图如下:

3.4 读命令:4'b0101

读命令用于对已经激活的bank和行进行读操作。A10的值决定是否在读操作完成后对该行进行预充电来关闭该行。如果为低电平,则继续保持该行的激活状态,后续也能继续进行读写操作。{BA1,BA0}选择想要读取的bank,A0-A9选择哪一行,读命令操作示意图如下:

3.5 写命令 :4'b0100

写命令用于对已经激活的bank和行进行写操作。A10的值决定是否在读操作完成后对该行进行预充电来关闭该行。如果为低电平,则继续保持该行的激活状态,后续也能继续进行读写操作。{BA1,BA0}选择想要写入的bank,A0-A9选择哪一行,写命令操作示意图如下:

3.6 突发中止命令 :4'b0110

SDRAM 处于读/写操作过程中可被写入,突发停止操作被用来截断固定长度或者整页长度的突发,执行突发停止命令后,最近执行的数据读写操作被终止。

3.7 预充电命令 :4'b0010

该命令用于关闭指定bank中打开行或所有bank中打开的行。在发出预充电命令后的指定时间 ( t R P ^tRP tRP)后,相对应的bank才能用于后续的行访问。输入 A10 确定是对指定bank还是所有bank进行预充电,如果仅对指定bank进行预充电,则输入 BA0 和 BA1 选择该bank。Bank 预充电后,它处于空闲状态,必须在向该 Bank 发出任何 READ 或 WRITE 命令之前重新激活它。

3.8 刷新命令:4'b0001

前面提到,由于电容会产生漏电流,因此必须在电容电荷量泄露完成之前对电容进行充电,这就叫刷新。目前国际公认的标准是,存储体中电容的数据有效保存期上限是 64ms,也就是说每一行刷新的循环周期最大为 64ms,那么刷新速度就是:行数/64ms。例如8192行的SDRAM刷新周期就为7.8125us。

刷新命令分为自动刷新和自刷新。在CKE为高电平时,执行自动刷新前必须执行预充电命令,来关闭所有bank。每次刷新后,需要等待相应周期后才能进行读写操作。在CKE为低电平时,执行自刷新,主要用于休眠状态下,对数据的保存。

3.9 配置模式寄存器命令:4'b0000

该命令只有所有bank 均处于空闲状态时才可被写入,否则配置出错,而且在执行此命令后,SDRAM 必须等待相应的响应时间 t R S C ^tRSC tRSC(Register Set Cycle)后,才可写入新的命令。在配置模式寄存器时,需要使用地址总线来辅助配置,如下图所示:

  1. A0,A1,A2 控制数据突发长度。突发长度可设置为1、2、4、8 和整页,单位为字节,整页表示一次突发传输一整行的数据量。若在数据读写操作时不使用突发传输,此时可等效为突发长度为 1 字节,每次读写数据时,都要对存储单元进行寻址,如果要实现连续的读写操作,就要不断地发送列地址和读/写命令。下图是突发长度设置为4的读操作时序图:

由图可以看出,若使用突发传输,只要指定起始列地址和突发长度,内存就会依次地自动对后面相应数量的存储单元进行读写操作,这样,除了第一笔数据传输需要若干个(CL)周期外,其后的每个数据只要一个周期即可获得。下图是突发长度不设置突发长度的读操作时序图:

  1. A3设置突发类型,0为顺序突发,1为隔行突发。具体突发顺序如下图所示:

一般情况下都设置为顺序突发。

  1. A4,A5,A6设置列选通潜伏期,是指从读命令被寄存到数据总线上到出现第一个有效数据之间的时钟周期间隔,列选通潜伏期可被设置为 2 个或 3 个时钟周期,如下图所示。

  2. A7,A8设置操作模式,SDRAM 存在标准模式、测试模式等多种模式,但对于普通用户,只开放了标准模式,在使用 SDRAM 时只需将 A7,A8 设置为低电平进入标准模式即可。

  3. A9设置写突发模式:当 A9 = 0 时,通过 A[2:0] 编程的突发长度适用于读和写突发;当M9 = 1时,编程的突发长度适用于读突发,但是写操作不是突发,一次只能写一个数据

  4. A12,A11,A10 保留。

四、FPGA实现SDRAM读写操作

实现uart接收数据到SDRAM中,然后从SDRAM读出来再通过uart发送出去。

4.1 系统框图

(未完待续)

4.2 初始化模块

在数据手册里找到初始化时序,如下图所示:

由上图可知,在SDRAM上电后需要等待最少100us的时间才能开始初始化。第一步是对所有bank进行预充电;第二步是写入几次自动刷新命令;第三步是写入配置模式寄存器命令,然后初始化就完成了。

4.2.1 波形图

整个初始化模块的波形图如下所示,不同芯片的手册上电等待时间不同,这里设置等待200us可以兼容更多的器件,手册建议自动刷新次数大于2次,代码里设置的8次,也是兼容更多的器件。

4.2.2 Verilog代码

c 复制代码
`timescale 1ns / 1ps
module sdram_init(
    input                                               clk ,
    input                                               rst_n   ,
    output  reg     [3:0]                               init_cmd    ,
    output  reg     [1:0]                               init_ba ,
    output  reg     [12:0]                              init_addr   ,
    output  wire                                        init_done   
);

parameter   INIT_IDLE           =   8'b0000_0001,   //初始状态
            INIT_PRE            =   8'b0000_0010,   //预充电指令状态
            INIT_TRP            =   8'b0000_0100,   //预充电等待时间(tRP)状态
            INIT_AR             =   8'b0000_1000,   //自动刷新指令状态
            INIT_TRF            =   8'b0001_0000,   //自动刷新等待时间(tRFC)状态
            INIT_MRS            =   8'b0010_0000,   //配置模式寄存器指令状态
            INIT_TMRD           =   8'b0100_0000,   //配置模式寄存器等待时间(tMRD)状态
            INIT_END            =   8'b1000_0000;   //初始化完成状态

parameter   CNT_200US_MAX       =   15'd20_000  ;   //100M系统时钟计数200us所需的计数值 

parameter   TRP_CLK             =   2'd2,           //预充电等待周期
            TPFC_CLK            =   3'd7,           //自动刷新等待周期
            TMRD_CLK            =   2'd2;           //配置模式寄存器等待周期

parameter   P_CHARGE            =   4'b0010,        //预充电命令
            NOP                 =   4'b0111,        //空操作命令
            AUTO_REF            =   4'b0001,        //自动刷新命令
            M_REG_SET           =   4'b0000;        //配置模式寄存器命令

reg             [14:0]                              cnt_200us   ;   //200us计数器
reg             [7:0]                               init_state  ;   //状态机
reg             [3:0]                               cnt_clk     ;   //时钟周期计数器
reg             [3:0]                               ar_cnt      ;   //自动刷新计数器
reg                                                 cnt_rst     ;   //时钟周期计数器复位信号
wire                                                wait_end    ;   //等待结束信号

//上电后等待200us完成信号
assign  wait_end    =(cnt_200us == CNT_200US_MAX - 1)? 1'b1 : 1'b0;
//整个初始化完成信号
assign  init_done   =(init_state == INIT_END)? 1'b1 : 1'b0;

//200us计数器
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cnt_200us <= 'd0;
    else if(cnt_200us == CNT_200US_MAX)
        cnt_200us <= CNT_200US_MAX;
    else
        cnt_200us <= cnt_200us + 1'b1;  
end

//时钟周期计数器
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cnt_clk <= 'd0;
    else if(cnt_rst == 1'b1)
        cnt_clk <= 'd0;
    else
        cnt_clk <= cnt_clk + 1'b1;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        init_state <= INIT_IDLE;
    else 
        case (init_state)
            INIT_IDLE: 
                if(wait_end == 1'b1)                //上电等待200us完成后进入预充电状态
                    init_state <= INIT_PRE;
                else
                    init_state <= INIT_IDLE;
            INIT_PRE:                               //预充电状态给出预充电命令,然后跳转预充电等待状态
                init_state <= INIT_TRP;
            INIT_TRP:
                if(cnt_clk == TRP_CLK)              //等待预充电周期后跳转自动刷新状态
                    init_state <= INIT_AR;
                else
                    init_state <= INIT_TRP;
            INIT_AR:
                init_state <= INIT_TRF;             //给出自动刷新命令后,跳转到自动刷新等待状态
            INIT_TRF:
                if((cnt_clk == TPFC_CLK)&&(ar_cnt == 'd8))  //当自动刷新完成8次后跳转配置模式寄存器状态
                    init_state <= INIT_MRS;
                else if(cnt_clk == TPFC_CLK)
                    init_state <= INIT_AR;
                else
                    init_state <= INIT_TRF;
            INIT_MRS:
                init_state <= INIT_TMRD;            //给出配置模式寄存器命令后。跳转到等待状态
            INIT_TMRD:                              //等待完成后,跳转到结束状态
                if(cnt_clk ==TMRD_CLK)
                    init_state <= INIT_END;
                else
                    init_state <= INIT_TMRD;
            INIT_END:                               
                init_state <= INIT_END;
            default: init_state <= INIT_IDLE;
        endcase
end

//在每个状态等待的周期完成后清空时钟周期计数器
always @(*) begin
    case (init_state)
        INIT_IDLE: 
            cnt_rst <= 1'b1;
        INIT_TRP: 
            cnt_rst <= (cnt_clk == TRP_CLK)? 1'b1 : 1'b0;
        INIT_TRF:
            cnt_rst <= (cnt_clk == TPFC_CLK)? 1'b1 : 1'b0;
        INIT_TMRD:
            cnt_rst <= (cnt_clk == TMRD_CLK)? 1'b1 : 1'b0;
        INIT_END:
            cnt_rst <= 1'b1;
        default: cnt_rst <= 1'b0;
    endcase
end

//每次进入自动刷新状态,自动刷新计数器+1
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        ar_cnt <= 'd0;
    else if(init_state == INIT_IDLE)
        ar_cnt <= 'd0;
    else if(init_state == INIT_AR)
        ar_cnt <= ar_cnt + 1'b1;
    else
        ar_cnt <= ar_cnt;
end

//每个状态给出的不同命令
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        init_cmd    <=  NOP;
        init_ba     <=  2'b11;
        init_addr   <=  13'h1FFF;
    end
    else    
        case (init_state)
            INIT_IDLE: 
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
             INIT_PRE: 
                begin
                    init_cmd    <=  P_CHARGE;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            INIT_TRP: 
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            INIT_AR: 
                begin
                    init_cmd    <=  AUTO_REF;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            INIT_TRF: 
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            INIT_MRS: 
                begin
                    init_cmd    <=  M_REG_SET;
                    init_ba     <=  2'b00;
                    init_addr   <=  
                    {    //地址辅助配置模式寄存器,参数不同,配置的模式不同
                        3'b000,     //A12-A10:预留
                        1'b0,       //A9=0:读写方式,0:突发读&突发写,1:突发读&单写
                        2'b00,      //{A8,A7}=00:标准模式,默认
                        3'b011,     //CAS潜伏期,{A6,A5,A4}=011:3,010:2,001:1,其他:保留
                        1'b0,       //A3=0:突发传输方式,0:顺序,1:隔行
                        3'b111      //{A2,A1,A0}=111:突发长度,000:单字节,001:2字节
                                    //010:4字节,011:8字节,111:整页,其他:保留
                    };
                end  
            INIT_TMRD: 
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            INIT_END: 
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
            default:  
                begin
                    init_cmd    <=  NOP;
                    init_ba     <=  2'b11;
                    init_addr   <=  13'h1FFF;
                end
        endcase  
end
endmodule

4.2.3 仿真代码

从网上下载一个SDRAM仿真模型,然后例化到仿真文件里面,因为SDRAM内部是上升沿采样,因此通过PLL设置一个用户工作时钟和SDRAM工作时钟,时钟频率一致,相位相差180°。这样可以确保SDRAM内部正确采样,仿真代码如下:

c 复制代码
`timescale 1ns/1ns
module tb_sdram_init();

    reg                                                 clk ;
    reg                                                 rst_n   ;
    wire                                                sdram_clk   ;
    wire                                                sdram_clk_shift ;
    wire                                                locked  ;
    wire                                                sys_rst_n   ;

    wire            [3:0]                               init_cmd    ;
    wire            [1:0]                               init_ba ;
    wire            [12:0]                              init_addr   ;
    wire                                                init_done   ;  

initial begin
    clk = 0;
    rst_n = 0;
    #200;
    rst_n = 1;
end

always #10 clk = ~clk;
assign sys_rst_n = rst_n & locked;

defparam    u_sdram_model_plus.addr_bits  = 13;
defparam    u_sdram_model_plus.data_bits  = 16;
defparam    u_sdram_model_plus.col_bits  = 9;
defparam    u_sdram_model_plus.mem_sizes  = 2*1024*1024;

sdram_clk u_sdram_clk
   (
    .clk_out1(sdram_clk),     
    .clk_out2(sdram_clk_shift),     
    .resetn(rst_n), 
    .locked(locked),       
    .clk_in1(clk));      

sdram_init u_sdram_init(
    .clk        ( sdram_clk        ),
    .rst_n      ( sys_rst_n      ),
    .init_cmd   ( init_cmd   ),
    .init_ba    ( init_ba    ),
    .init_addr  ( init_addr  ),
    .init_done  ( init_done  )
);

sdram_model_plus u_sdram_model_plus(
    .Dq         (), 
    .Addr       (init_addr),
    .Ba         (init_ba),
    .Clk        (sdram_clk_shift),
    .Cke        (1'b1), 
    .Cs_n       (init_cmd[3]), 
    .Ras_n      (init_cmd[2]), 
    .Cas_n      (init_cmd[1]), 
    .We_n       (init_cmd[0]), 
    .Dqm        (2'b00),
    .Debug      (1'b1)
);

endmodule

4.2.4 仿真结果观测

执行仿真后查看打印信息:先是对所有bank进行预充电,然后自动刷新8次,最后配置模式寄存器为,cas=3,整夜突发,顺序突发,和程序一致,接下来看仿真波形:

和设置的波形图一致,仿真完成。

4.3 自动刷新模块

由上面SDRAM原理可知,SDRAM依靠电容充放电来实现数据的保存,因此需要不断地对电容进行充电操作以确保数据的可靠性,这就是刷新操作。国际上公认最迟64ms就需要对每一行进行充电,因此例如一个sdram的地址位宽位13,则由2^13=8192行。所以每次间隔64ms/8192 =7810ns就要对一行进行刷新操作。刷新又分为自动刷新和自刷新。自动刷新是指CKE有效时对SDRAM进行刷新操作。自刷新是指CKE无效时,SDRAM待机状态时候内部进行自动刷新的操作。

打开数据手册,找到自动刷新操作时序图如下:

由操作时序图可以看出,自动刷新步骤为:

  1. 对所有bank进行预充电
  2. 等待预充电周期
  3. 写入自动刷新指令
  4. 等待自动刷新周期
  5. 再写入自动刷新指令
  6. 再等待自动刷新指令
  7. 后续操作

4.3.1 波形图

  1. 在SDRAM初始化完成后,通过cnt_ref开始计数7800ns
  2. 每次计数完成就拉高aref_req信号,请求自动刷新
  3. 仲裁模块接收到aref_req信号后,判断当前SDRAM是否空闲,若空闲,则发出aref_en信号给本模块
  4. 本模块收到aref_en信号后就拉低aref_req信号,并且状态机开始跳转到预充电状态
  5. 预充电等待结束后跳转到自动刷新状态
  6. 自动刷新两次后,跳转到结束状态
  7. 结束状态拉高aref_done信号,然后跳转到空闲状态等待新一轮自动刷新操作

4.3.2 Verilog代码

c 复制代码
`timescale 1ns / 1ps
module sdram_auto_ref(
    input                                               clk ,
    input                                               rst_n   ,
    input                                               init_done   ,
    input                                               aref_en ,
    output  reg                                         aref_req    ,
    output                                              aref_done   ,
    output  reg     [1:0]                               aref_ba ,
    output  reg     [3:0]                               aref_cmd    ,
    output  reg     [12:0]                              aref_addr   
    );

parameter   CNT_REF_MAX = 10'd750;       //SDRAM地址位宽13位,一共2^13=8192行。64ms/8192=7812ns  提前一点做预留时间就是7500ns,时钟频率100m

parameter   P_CHARGE            =   4'b0010,        //预充电命令
            NOP                 =   4'b0111,        //空操作命令
            AUTO_REF            =   4'b0001;        //自动刷新命令
            
parameter   AREF_IDLE           =   6'b00_0001,   //空闲状态
            AREF_PRE            =   6'b00_0010,   //预充电指令状态
            AREF_TRP            =   6'b00_0100,   //预充电等待时间(tRP)状态
            AREF_AR             =   6'b00_1000,   //自动刷新指令状态
            AREF_TRF            =   6'b01_0000,   //自动刷新等待时间(tRFC)状态
            AREF_END            =   6'b10_0000;   //自动刷新完成状态

parameter   TRP_CLK             =   2'd2,           //预充电等待周期
            TPFC_CLK            =   3'd7;           //自动刷新等待周期

reg             [9:0]                               cnt_ref ;
reg             [5:0]                               aref_state  ;
reg             [2:0]                               cnt_clk ;
reg                                                 cnt_rst ;
reg             [2:0]                               ar_cnt  ;

assign aref_done = (aref_state == AREF_END)? 1'b1 : 1'b0;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cnt_ref <= 'd0;
    else if(cnt_ref == CNT_REF_MAX - 1)
        cnt_ref <= 'd0; 
    else if(init_done == 1'b1)
        cnt_ref <= cnt_ref + 1'b1;
    else
        cnt_ref <= cnt_ref;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        aref_req <= 1'b0;
    else if(cnt_ref == CNT_REF_MAX - 1)
        aref_req <= 1'b1;
    else if(aref_en == 1'b1)
        aref_req <= 1'b0;
    else
        aref_req <= aref_req;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        aref_state <= AREF_IDLE;
    else
        case (aref_state)
            AREF_IDLE: 
                if((init_done == 1'b1)&&(aref_en == 1'b1))      //SDRAM初始化完成并且仲裁模块允许自动刷新信号来临时跳转
                    aref_state <= AREF_PRE;
                else
                    aref_state <= AREF_IDLE;
            AREF_PRE:
                aref_state <= AREF_TRP;
            AREF_TRP:
                if(cnt_clk == TRP_CLK)
                    aref_state <= AREF_AR;
                else
                    aref_state <= AREF_TRP;
            AREF_AR:
                aref_state <= AREF_TRF;
            AREF_TRF:
                if((cnt_clk == TPFC_CLK)&&(ar_cnt == 'd2))
                    aref_state <= AREF_END;
                else if(cnt_clk == TPFC_CLK)
                    aref_state <= AREF_AR;
                else
                    aref_state <= AREF_TRF;
            AREF_END:
                aref_state <= AREF_IDLE;
            default: aref_state <= AREF_IDLE;
        endcase
end

//每次进入自动刷新状态,自动刷新计数器+1
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        ar_cnt <= 'd0;
    else if(aref_state == AREF_IDLE)
        ar_cnt <= 'd0;
    else if(aref_state == AREF_AR)
        ar_cnt <= ar_cnt + 1'b1;
    else
        ar_cnt <= ar_cnt;
end

//时钟周期计数器
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cnt_clk <= 'd0;
    else if(cnt_rst == 1'b1)
        cnt_clk <= 'd0;
    else
        cnt_clk <= cnt_clk + 1'b1;
end

always @(*) begin
    case (aref_state)
        AREF_IDLE: 
            cnt_rst <= 1'b1;
        AREF_TRP: 
            cnt_rst <= (cnt_clk == TRP_CLK)? 1'b1 : 1'b0;
        AREF_TRF:
            cnt_rst <= (cnt_clk == TPFC_CLK)? 1'b1 : 1'b0;
        AREF_END:
            cnt_rst <= 1'b1;
        default: cnt_rst <= 1'b0;
    endcase
end

//SDRAM操作指令控制
always@(posedge clk or negedge rst_n)
    if(rst_n == 1'b0)
        begin
            aref_cmd    <=  NOP;
            aref_ba     <=  2'b11;
            aref_addr   <=  13'h1fff;
        end
    else
        case(aref_state)
            AREF_IDLE,AREF_TRP,AREF_TRF:    //执行空操作指令
                begin
                    aref_cmd    <=  NOP;
                    aref_ba     <=  2'b11;
                    aref_addr   <=  13'h1fff;
                end
            AREF_PRE:  //预充电指令
                begin
                    aref_cmd    <=  P_CHARGE;
                    aref_ba     <=  2'b11;
                    aref_addr   <=  13'h1fff;
                end 
            AREF_AR:   //自动刷新指令
                begin
                    aref_cmd    <=  AUTO_REF;
                    aref_ba     <=  2'b11;
                    aref_addr   <=  13'h1fff;
                end
            AREF_END:   //一次自动刷新完成
                begin
                    aref_cmd    <=  NOP;
                    aref_ba     <=  2'b11;
                    aref_addr   <=  13'h1fff;
                end    
            default:
                begin
                    aref_cmd    <=  NOP;
                    aref_ba     <=  2'b11;
                    aref_addr   <=  13'h1fff;
                end    
        endcase
endmodule

4.3.3 仿真代码

仿真代码和初始化模块几乎一致

c 复制代码
`timescale 1ns/1ns
module tb_sdram_auto_ref();

    reg                                                 clk ;
    reg                                                 rst_n   ;
    wire                                                sdram_clk   ;
    wire                                                sdram_clk_shift ;
    wire                                                locked  ;
    wire                                                sys_rst_n   ;

    wire            [3:0]                               init_cmd    ;
    wire            [1:0]                               init_ba ;
    wire            [12:0]                              init_addr   ;
    wire                                                init_done   ;
    wire                                                aref_req    ;  
    wire                                                aref_done   ;
    wire            [1:0]                               aref_ba ;
    wire            [3:0]                               aref_cmd    ;
    wire            [12:0]                              aref_addr ;  
    reg                                                 aref_en ;

    wire            [1:0]                               sdram_ba ;
    wire            [3:0]                               sdram_cmd    ;
    wire            [12:0]                              sdram_addr ;  

defparam    u_sdram_model_plus.addr_bits  = 13;
defparam    u_sdram_model_plus.data_bits  = 16;
defparam    u_sdram_model_plus.col_bits  = 9;
defparam    u_sdram_model_plus.mem_sizes  = 2*1024*1024;


initial begin
    clk = 0;
    rst_n = 0;
    #200;
    rst_n = 1;
end


assign sys_rst_n = rst_n & locked;

//根据是否初始化完成来判断当前给初始化命令还是自动刷新命令
assign sdram_ba     =(init_done == 1'b1)? aref_ba    : init_ba ;
assign sdram_cmd    =(init_done == 1'b1)? aref_cmd   : init_cmd ;
assign sdram_addr   =(init_done == 1'b1)? aref_addr     : init_addr;

//模拟仲裁模块给出aref_en信号
always @(posedge sdram_clk or negedge sys_rst_n) begin
    if(sys_rst_n == 1'b0)
        aref_en <= 1'b0;
    else if((init_done == 1'b1)&&(aref_req == 1'b1))
        aref_en <= 1'b1;
    else if(aref_done == 1'b1)
        aref_en <= 1'b0;
    else
        aref_en <= aref_en;
end

always #10 clk = ~clk;

sdram_clk u_sdram_clk
   (
    .clk_out1(sdram_clk),     
    .clk_out2(sdram_clk_shift),     
    .resetn(rst_n), 
    .locked(locked),       
    .clk_in1(clk));      

sdram_init u_sdram_init(
    .clk        ( sdram_clk  ),
    .rst_n      ( sys_rst_n  ),
    .init_cmd   ( init_cmd   ),
    .init_ba    ( init_ba    ),
    .init_addr  ( init_addr  ),
    .init_done  ( init_done  )
);

sdram_auto_ref u_sdram_auto_ref(
    .clk        ( sdram_clk  ),
    .rst_n      ( rst_n      ),
    .init_done  ( init_done  ),
    .aref_en    ( aref_en    ),
    .aref_req   ( aref_req   ),
    .aref_done  ( aref_done  ),
    .aref_ba    ( aref_ba    ),
    .aref_cmd   ( aref_cmd   ),
    .aref_addr  ( aref_addr  )
);

sdram_model_plus u_sdram_model_plus(
    .Dq         (), 
    .Addr       (sdram_addr),
    .Ba         (sdram_ba),
    .Clk        (sdram_clk_shift),
    .Cke        (1'b1), 
    .Cs_n       (sdram_cmd[3]), 
    .Ras_n      (sdram_cmd[2]), 
    .Cas_n      (sdram_cmd[1]), 
    .We_n       (sdram_cmd[0]), 
    .Dqm        (2'b00),
    .Debug      (1'b1)
);

endmodule

4.3.4 仿真结果观测

查看打印信息,SDRAM先是经过了初始化,然后再执行了自动刷新,接下来观看仿真波形:

和设置的波形图一致,仿真完成。

4.4 写操作模块

打开数据手册,我们可以看到很多写操作模式,这里我们选择使用整页突发写操作,不自动预充电模式;时序图如下:

  1. 首先写入激活指令,对所选择的bank和行数进行激活操作
  2. 然后等待激活指令时间
  3. 再写入写指令以及对应的bank和列地址
  4. 持续写入数据
  5. 写入突发中止指令
  6. 写入预充电命令,对所有bank进行预充电

4.4.1 波形图

整体写模块也由状态机完成,当收到仲裁模块给的wr_en时,状态机按照数据手册的时序图跳转。在写满突发长度的数据后,给出突发中止指令,然后对所有bank进行预充电。

4.4.2 Verilog代码

c 复制代码
`timescale 1ns / 1ps
module sdram_write(
    input                                               clk ,
    input                                               rst_n   ,
    input                                               init_done   ,
    input                                               wr_en   ,       //写使能
    input           [23:0]                              wr_addr ,       //高两位为bank地址,其次13位为行地址,最后9位位列地址
    input           [15:0]                              wr_data ,       //写数据
    input           [9:0]                               wr_burst_len    , //一次写突发长度

    output  reg     [3:0]                               wr_cmd  ,
    output  reg     [1:0]                               wr_ba  ,
    output  reg     [12:0]                              wr_sdram_addr ,
    output  reg                                         wr_sdram_en ,
    output          [15:0]                              wr_sdram_data   ,
    output                                              wr_done ,
    output  reg                                         wr_fifo_req 
    );

parameter   WR_IDLE             =   8'b0000_0001,   //写空闲状态
            WR_ACTIVE           =   8'b0000_0010,   //激活指令状态
            WR_TRCD             =   8'b0000_0100,   //激活指令等待时间(tRCD)状态
            WR_WRITE            =   8'b0000_1000,   //写指令状态
            WR_DATA             =   8'b0001_0000,   //写数据状态
            WR_PRE              =   8'b0010_0000,   //预充电状态
            WR_TRP              =   8'b0100_0000,   //预充电等待状态
            WR_END              =   8'b1000_0000;   //写完成

parameter   NOP                 =   4'b0111 ,   //空操作指令
            ACTIVE              =   4'b0011 ,   //激活指令
            WRITE               =   4'b0100 ,   //数据写指令
            B_STOP              =   4'b0110 ,   //突发停止指令
            P_CHARGE            =   4'b0010 ;   //预充电指令

parameter   TRCD_CLK            =   10'd2   ,   //激活周期
            TRP_CLK             =   10'd2   ;   //预充电周期

reg             [7:0]                               wr_state    ;
reg             [9:0]                               cnt_clk ;
reg                                                 cnt_rst ;

assign wr_done =(wr_state == WR_END)? 1'b1:1'b0;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        wr_state <= WR_IDLE;
    else
        case (wr_state)
            WR_IDLE: 
                if(wr_en == 1'b1)
                    wr_state <= WR_ACTIVE;
                else
                    wr_state <= WR_IDLE;
            WR_ACTIVE:
                wr_state <= WR_TRCD;
            WR_TRCD:
                if(cnt_clk == TRCD_CLK)
                    wr_state <= WR_WRITE;
                else
                    wr_state <= WR_TRCD;
            WR_WRITE:
                wr_state <= WR_DATA;
            WR_DATA:
                if(cnt_clk == wr_burst_len)
                    wr_state <= WR_PRE;
                else
                    wr_state <= WR_DATA;
            WR_PRE:
                wr_state <= WR_TRP;
            WR_TRP:
                if(cnt_clk == TRP_CLK)
                    wr_state <= WR_END;
                else
                    wr_state <= WR_TRP;
            WR_END:
                wr_state <= WR_IDLE;
            default: wr_state <= WR_IDLE;
        endcase
end

//时钟周期计数器
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cnt_clk <= 'd0;
    else if(cnt_rst == 1'b1)
        cnt_clk <= 'd0;
    else
        cnt_clk <= cnt_clk + 1'b1;
end

always @(*) begin
    case (wr_state)
        WR_IDLE: 
            cnt_rst <= 1'b1;
        WR_TRCD: 
            cnt_rst <= (cnt_clk == TRCD_CLK)? 1'b1 : 1'b0;
        WR_DATA:
            cnt_rst <= (cnt_clk == wr_burst_len)? 1'b1 : 1'b0;
        WR_TRP:
            cnt_rst <= (cnt_clk == TRP_CLK)? 1'b1 : 1'b0;
        WR_END:
            cnt_rst <= 1'b1;
        default: cnt_rst <= 1'b0;
    endcase
end

always @(*) begin
    if(rst_n == 1'b0)
        wr_fifo_req <= 1'b0;
    else if(wr_state == WR_WRITE)
        wr_fifo_req <= 1'b1;
    else if((wr_state == WR_DATA)&&(cnt_clk == wr_burst_len))
        wr_fifo_req <= 1'b0;
    else
        wr_fifo_req <= wr_fifo_req;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        wr_sdram_en <= 1'b0;
    else
        wr_sdram_en <= wr_fifo_req;
end

assign wr_sdram_data = (wr_sdram_en == 1'b1)? wr_data : 16'd0;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        wr_cmd <= NOP;
        wr_ba <= 2'b11;
        wr_sdram_addr <= 13'h1fff;
    end
    else
        case (wr_state)
            WR_IDLE: begin
                wr_cmd <= NOP;
                wr_ba <= 2'b11;
                wr_sdram_addr <= 13'h1fff;
            end
            WR_ACTIVE: begin
                wr_cmd <= ACTIVE;                   //给出激活指令和bank地址以及行地址
                wr_ba <= wr_addr[23:22];
                wr_sdram_addr <= wr_addr[21:9];
            end
            WR_TRCD: begin
                wr_cmd <= NOP;
                wr_ba <= 2'b11;
                wr_sdram_addr <= 13'h1fff;
            end
            WR_WRITE: begin
                wr_cmd <= WRITE;                    //给出写指令和bank地址以及列地址
                wr_ba <= wr_addr[23:22];
                wr_sdram_addr <= {4'b0000,wr_addr[8:0]};
            end
            WR_DATA: begin
                if(cnt_clk == wr_burst_len)
                    wr_cmd <= B_STOP;
                else begin
                    wr_cmd <= NOP;
                    wr_ba <= 2'b11;
                    wr_sdram_addr <= 13'h1fff;
                end
            end
            WR_PRE: begin
                wr_cmd <= P_CHARGE;                 //给出预充电指令,A10位1时选择所有bank
                wr_ba <= wr_addr[23:22];
                wr_sdram_addr <= 13'h0400;
            end
            WR_TRP: begin
                wr_cmd <= NOP;
                wr_ba <= 2'b11;
                wr_sdram_addr <= 13'h1fff;
            end
            WR_END: begin
                wr_cmd <= NOP;
                wr_ba <= 2'b11;
                wr_sdram_addr <= 13'h1fff;
            end
            default: begin
                wr_cmd <= NOP;
                wr_ba <= 2'b11;
                wr_sdram_addr <= 13'h1fff;
            end
        endcase
end

endmodule

4.4.3 仿真代码

在初始化模块仿真代码中添加写模块:

c 复制代码
`timescale 1ns/1ns
module tb_sdram_write();

    reg                                                 clk ;
    reg                                                 rst_n   ;
    wire                                                sdram_clk   ;
    wire                                                sdram_clk_shift ;
    wire                                                locked  ;
    wire                                                sys_rst_n   ;

    wire            [3:0]                               init_cmd    ;
    wire            [1:0]                               init_ba ;
    wire            [12:0]                              init_addr   ;
    wire                                                init_done   ;  
    reg                                                 wr_en   ;
    wire                                                wr_done ;
    reg             [15:0]                              wr_data ;
    wire                                                wr_fifo_req ;
    wire            [15:0]                              sdram_dq;  
    wire                                                wr_sdram_en ;
    wire            [15:0]                              wr_sdram_data;  

    wire            [1:0]                               sdram_ba ;
    wire            [3:0]                               sdram_cmd    ;
    wire            [12:0]                              sdram_addr ;  
    wire            [1:0]                               wr_ba ;
    wire            [3:0]                               wr_cmd    ;
    wire            [12:0]                              wr_sdram_addr ; 

defparam    u_sdram_model_plus.addr_bits  = 13;
defparam    u_sdram_model_plus.data_bits  = 16;
defparam    u_sdram_model_plus.col_bits  = 9;
defparam    u_sdram_model_plus.mem_sizes  = 2*1024*1024;

initial begin
    clk = 0;
    rst_n = 0;
    #200;
    rst_n = 1;
end

always #10 clk = ~clk;
assign sys_rst_n = rst_n & locked;

always @(posedge sdram_clk or negedge sys_rst_n) begin  //当初始化完成后打开写使能,当写操作完成后拉低写使能
    if(sys_rst_n == 1'b0)
        wr_en <= 1'b0;
    else if(wr_done == 1'b1)
        wr_en <= 1'b0;
    else if(init_done == 1'b1)
        wr_en <= 1'b1;
    else
        wr_en <= wr_en;
end

always @(posedge sdram_clk or negedge sys_rst_n) begin //当收到写模块的读数据请求后,数据累加
    if(sys_rst_n == 1'b0)
        wr_data <= 'd0;
    else if(wr_data == 'd10)
        wr_data <= 'd0;
    else if(wr_fifo_req == 1'b1)
       wr_data <= wr_data + 1'b1;
    else
        wr_data <= wr_data;
end

assign sdram_ba     =(init_done == 1'b1)? wr_ba    : init_ba ;
assign sdram_cmd    =(init_done == 1'b1)? wr_cmd   : init_cmd ;
assign sdram_addr   =(init_done == 1'b1)? wr_sdram_addr     : init_addr;

assign sdram_dq = (wr_sdram_en == 1'b1)?wr_sdram_data : 16'hz;  //在写操作时控制dq总线,写完成后释放

sdram_clk u_sdram_clk
   (
    .clk_out1(sdram_clk),     
    .clk_out2(sdram_clk_shift),     
    .resetn(rst_n), 
    .locked(locked),       
    .clk_in1(clk));      

sdram_init u_sdram_init(
    .clk        ( sdram_clk        ),
    .rst_n      ( sys_rst_n      ),
    .init_cmd   ( init_cmd   ),
    .init_ba    ( init_ba    ),
    .init_addr  ( init_addr  ),
    .init_done  ( init_done  )
);

sdram_write u_sdram_write(
    .clk            ( sdram_clk      ),
    .rst_n          ( sys_rst_n      ),
    .init_done      ( init_done      ),
    .wr_en          ( wr_en          ),
    .wr_addr        ( 24'd0        	 ),
    .wr_data        ( wr_data        ),
    .wr_burst_len   ( 10'd10   		 ),
    .wr_cmd         ( wr_cmd         ),
    .wr_ba          ( wr_ba          ),
    .wr_sdram_addr  ( wr_sdram_addr  ),
    .wr_sdram_en    ( wr_sdram_en    ),
    .wr_sdram_data  ( wr_sdram_data  ),
    .wr_done        ( wr_done        ),
    .wr_fifo_req    ( wr_fifo_req    )
);


sdram_model_plus u_sdram_model_plus(
    .Dq         (sdram_dq			), 
    .Addr       (sdram_addr			),
    .Ba         (sdram_ba	 		),
    .Clk        (sdram_clk_shift	),
    .Cke        (1'b1		 		), 
    .Cs_n       (sdram_cmd[3]		), 
    .Ras_n      (sdram_cmd[2]		), 
    .Cas_n      (sdram_cmd[1]		), 
    .We_n       (sdram_cmd[0]		), 
    .Dqm        (2'b00		 		),
    .Debug      (1'b1		 		)
);

endmodule

4.4.4仿真结果观测

查看打印信息发现,首先对sdram进行初始化操作;初始化完成后先是对所选择的bank和行地址进行激活,然后执行写命令,写突发完成后,对所有bank进行预充电,观看仿真波形如下:

观测波形发现和所画的波形图一直。

相关推荐
fei_sun19 小时前
【Verilog】第一章作业
fpga开发·verilog
深圳市雷龙发展有限公司longsto19 小时前
基于FPGA(现场可编程门阵列)的SD NAND图片显示系统是一个复杂的项目,它涉及硬件设计、FPGA编程、SD卡接口、NAND闪存控制以及图像显示等多个方面
fpga开发
9527华安1 天前
FPGA实现PCIE3.0视频采集转10G万兆UDP网络输出,基于XDMA+GTH架构,提供工程源码和技术支持
网络·fpga开发·udp·音视频·xdma·pcie3.0·万兆网
able陈1 天前
为什么verilog中递归函数需要定义为automatic?
fpga开发
fei_sun1 天前
【Verilog】第二章作业
fpga开发·verilog
碎碎思1 天前
如何使用 Vivado 从源码构建 Infinite-ISP FPGA 项目
fpga开发·接口隔离原则
江山如画,佳人北望1 天前
fpga-状态机的设计及应用
fpga开发
晓晓暮雨潇潇1 天前
Xilinx IP核(3)XADC IP核
fpga开发·vivado·xadc·ip核
CWNULT1 天前
AMD(Xilinx) FPGA配置Flash大小选择
fpga开发
碎碎思2 天前
很能体现FPGA硬件思维的一道面试题
fpga开发