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


一、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为例,引脚说明如下:

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

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 SDRAM控制模块框图

在这里插入图片描述

  整个SDRAM控制模块,由五个子模块组成:SDRAM初始化模块、SDRAM自动刷新模块、SDRAM写模块、SDRAM读模块以及SDRAM仲裁模块。整个SDRAM的读写数据请求由外部模块给出,后续会给出,由于读写请求和自动刷新请求的时间不确定,因此仲裁模块的作用就是协调其他模块的操作不会产生冲突。

4.2 初始化模块

  在数据手册里找到初始化时序,如下图所示:
在这里插入图片描述
  由上图可知,在SDRAM上电后需要等待最少100us的时间才能开始初始化。第一步是对所有bank进行预充电;第二步是写入几次自动刷新命令;第三步是写入配置模式寄存器命令,然后初始化就完成了。

4.2.1 波形图

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

4.2.2 Verilog代码

`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工作时钟,时钟频率一致,相位偏移 -30°。这样可以确保SDRAM内部正确采样,仿真代码如下:

`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代码

`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 仿真代码

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

`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代码

`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((init_done==1'b1)&&(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 仿真代码

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

`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进行预充电,观看仿真波形如下:

在这里插入图片描述
  观测波形发现和所画的波形图一致。

4.5 读操作模块

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

在这里插入图片描述

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

4.5.1 波形图

在这里插入图片描述

  1. 收到仲裁模块给的rd_en时,状态机跳转到激活命令状态
  2. 给出激活命令后,等待激活命令周期时间后跳到读命令状态
  3. 给出读命令后等待CL周期,SDRAM将数据读出来
  4. 由于SDRAM工作时钟和用户时钟存在相位偏移,因此需要将读出的数据同步到用户时钟下
  5. 后面再依次给出预充电指令,对所有bank进行预充电

4.5.2 Verilog代码

`timescale 1ns / 1ns

module sdram_read(
    input                                               clk ,
    input                                               rst_n   ,
    input                                               init_done   ,
    input                                               rd_en   ,           //读使能信号
    input           [23:0]                              rd_addr ,           //本次读开始地址
    input           [9:0]                               rd_burst_len    ,   //读突发长度
    input           [15:0]                              rd_data ,           //从SDRAM读出的数据
    output                                              rd_done ,           //本次读操作完成信号
    output  reg                                         rd_data_valid   ,   //读数据有效信号
    output          [15:0]                              rd_sdram_data   ,   //读出的数据
    output  reg     [3:0]                               rd_cmd  ,
    output  reg     [1:0]                               rd_ba   ,
    output  reg     [12:0]                              rd_sdram_addr 
);


parameter   RD_IDLE             =   9'b0_0000_0001,   //读空闲状态
            RD_ACTIVE           =   9'b0_0000_0010,   //激活指令状态
            RD_TRCD             =   9'b0_0000_0100,   //激活指令等待时间(tRCD)状态
            RD_READ             =   9'b0_0000_1000,   //读指令状态
            RD_CL               =   9'b0_0001_0000,   //等CL周期状态
            RD_DATA             =   9'b0_0010_0000,   //读数据状态
            RD_PRE              =   9'b0_0100_0000,   //预充电指令状态
            RD_TRP              =   9'b0_1000_0000,   //预充电等待周期状态
            RD_END              =   9'b1_0000_0000;   //读完成

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

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


reg             [8:0]                               rd_state    ;
reg             [9:0]                               cnt_clk ;
reg                                                 cnt_rst ;
reg             [15:0]                              rd_data_reg ;

assign rd_done = (rd_state == RD_END)? 1'b1:1'b0;

//因为SDRAM工作时钟和用户时钟存在180°相位差,因此需要同步到用户时钟域
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rd_data_reg <= 16'd0;
    else
        rd_data_reg <= rd_data;
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rd_state <= RD_IDLE;
    else
        case (rd_state)
            RD_IDLE: 
                if((init_done == 1'b1)&&(rd_en == 1'b1))            //当初始化完成并且收到读使能信号后挑战
                    rd_state <= RD_ACTIVE;
                else
                    rd_state <= RD_IDLE;
            RD_ACTIVE:                                              //选择bank和行地址激活
                rd_state <= RD_TRCD;
            RD_TRCD:
                if(cnt_clk == TRCD_CLK)
                    rd_state <= RD_READ;
                else
                    rd_state <= RD_TRCD;
            RD_READ:                                                //选择bank和列地址写入读指令
                rd_state <= RD_CL;
            RD_CL:                                                  //等待CAS潜伏期
                if(cnt_clk == TCL_CLK)
                    rd_state <= RD_DATA;
                else
                    rd_state <= RD_CL;
            RD_DATA:
                if(cnt_clk == TCL_CLK + rd_burst_len -1'b1)
                    rd_state <= RD_PRE;
                else
                    rd_state <= RD_DATA;
            RD_PRE:                                                 //选择所有bank和行地址进行预充电
                rd_state <= RD_TRP;
            RD_TRP:
                if(cnt_clk == TRP_CLK)
                    rd_state <= RD_END;
                else
                    rd_state <= RD_TRP;
            RD_END:
                rd_state <= RD_IDLE;
            default: rd_state <= RD_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 (rd_state)
        RD_IDLE: 
            cnt_rst <= 1'b1;
        RD_TRCD: 
            cnt_rst <= (cnt_clk == TRCD_CLK)? 1'b1 : 1'b0;
        RD_CL:
            cnt_rst <= (cnt_clk == TCL_CLK)? 1'b1 : 1'b0;
        RD_DATA:
            cnt_rst <= (cnt_clk == TCL_CLK + rd_burst_len -1'b1)? 1'b1 : 1'b0;
        RD_TRP:
            cnt_rst <= (cnt_clk == TRP_CLK)? 1'b1 : 1'b0;
        RD_END:
            cnt_rst <= 1'b1;
        default: cnt_rst <= 1'b0;
    endcase
end

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rd_data_valid <= 1'b0;
    else if((rd_state == RD_DATA)&&(cnt_clk < rd_burst_len))
        rd_data_valid <= 1'b1;
    else
        rd_data_valid <= 1'b0;
end

assign  rd_sdram_data = (rd_data_valid == 1'b1) ? rd_data_reg : 16'd0;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        rd_cmd <= NOP;
        rd_ba <= 2'b11;
        rd_sdram_addr <= 13'h1fff;
    end
    else
        case (rd_state)
            RD_IDLE: begin
                rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
            RD_ACTIVE: begin
                rd_cmd <= ACTIVE;                   //给出激活指令和bank地址以及行地址
                rd_ba <= rd_addr[23:22];
                rd_sdram_addr <= rd_addr[21:9];
            end
            RD_TRCD: begin
                rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
            RD_READ: begin
                rd_cmd <= READ;                    //给出读指令和bank地址以及列地址
                rd_ba <= rd_addr[23:22];
                rd_sdram_addr <= {4'b0000,rd_addr[8:0]};
            end
            RD_CL:begin
                rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
            RD_DATA: begin
                if(cnt_clk == rd_burst_len - TCL_CLK -1'b1)
                    rd_cmd <= B_STOP;
                else begin
                    rd_cmd <= NOP;
                    rd_ba <= 2'b11;
                    rd_sdram_addr <= 13'h1fff;
                end
            end
            RD_PRE: begin
                rd_cmd <= P_CHARGE;                 //给出预充电指令,A10位1时选择所有bank
                rd_ba <= rd_addr[23:22];
                rd_sdram_addr <= 13'h0400;
            end
            RD_TRP: begin
                rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
            RD_END: begin
                rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
            default: begin
                 rd_cmd <= NOP;
                rd_ba <= 2'b11;
                rd_sdram_addr <= 13'h1fff;
            end
        endcase
end
endmodule

4.5.3 仿真代码

   在写模块的仿真代码中添加读模块:

`timescale 1ns/1ns
module tb_sdram_read();

    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                                                wr_sdram_en ;
    wire            [15:0]                              wr_sdram_data;  
    wire            [1:0]                               wr_ba ;
    wire            [3:0]                               wr_cmd    ;
    wire            [12:0]                              wr_sdram_addr ; 

//读模块
    reg                                                 rd_en   ;
    wire            [15:0]                              rd_sdram_data ;
    wire                                                rd_done ;
    wire                                                rd_data_valid   ;
    wire            [1:0]                               rd_ba ;
    wire            [3:0]                               rd_cmd    ;
    wire            [12:0]                              rd_sdram_addr ; 
//SDRAM模块
    wire            [1:0]                               sdram_ba ;
    wire            [3:0]                               sdram_cmd    ;
    wire            [12:0]                              sdram_addr ;  
    wire            [15:0]                              sdram_dq;  

 //中间变量
    wire            [1:0]                               wr_rd_ba ;
    wire            [3:0]                               wr_rd_cmd    ;
    wire            [12:0]                              wr_rd_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'b1;
    else if(wr_done == 1'b1)
        wr_en <= 1'b0;
    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

//读操作
always @(posedge sdram_clk or negedge sys_rst_n) begin  //当写操作完成后打开读使能
    if(sys_rst_n == 1'b0)
        rd_en <= 1'b0;
    else if(rd_done == 1'b1)
        rd_en <= 1'b0;
    else if(wr_en == 1'b0)
        rd_en <= 1'b1;
    else
        rd_en <= rd_en;
end

assign sdram_ba     =(init_done == 1'b1)? wr_rd_ba    : init_ba ;
assign sdram_cmd    =(init_done == 1'b1)? wr_rd_cmd   : init_cmd ;
assign sdram_addr   =(init_done == 1'b1)? wr_rd_sdram_addr     : init_addr;

assign wr_rd_ba     =(wr_en == 1'b1)? wr_ba    : rd_ba ;
assign wr_rd_cmd    =(wr_en == 1'b1)? wr_cmd   : rd_cmd ;
assign wr_rd_sdram_addr   =(wr_en == 1'b1)? wr_sdram_addr     : rd_sdram_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_read u_sdram_read(
    .clk            ( sdram_clk      ),
    .rst_n          ( sys_rst_n      ),
    .init_done      ( init_done      ),
    .rd_en          ( rd_en          ),
    .rd_addr        ( 24'd0          ),
    .rd_burst_len   ( 10'd10         ),
    .rd_data        ( sdram_dq       ),
    .rd_done        ( rd_done        ),
    .rd_data_valid  ( rd_data_valid  ),
    .rd_sdram_data  ( rd_sdram_data  ),
    .rd_cmd         ( rd_cmd         ),
    .rd_ba          ( rd_ba          ),
    .rd_sdram_addr  ( rd_sdram_addr  )
);


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.5.4 仿真结果观测

在这里插入图片描述
  查看打印信息发现:

  1. 首先对sdram进行初始化操作;
  2. 初始化完成后先是对所选择的bank和行地址进行激活,
  3. 然后执行写命令;
  4. 写突发完成后,对所有bank进行预充电;
  5. 然后对所选择的bank和行地址进行激活,然后执行读命令;
  6. 读突发完成后对所有bank进行预充电;

仿真波形如下:
在这里插入图片描述
  观测波形发现和所画的波形图一致。

4.6 仲裁模块

4.6.1 状态机跳转图

  至此,我们已经完成了SDRAM的初始化模块、自动刷新模块以及读写模块,不同模块请求的指令以及请求操作的时间都是不确定的,因此需要一个仲裁模块,来判断各个模块的优先级,确保不会发生操作冲突,操作优先级设置为 自动刷新>数据写操作>数据读操作,状态机跳转图如下:

在这里插入图片描述
  当上电开始时,状态机停留在IDLE状态,等待初始化模块完成后,跳转到仲裁判断状态,然后根据不同的模块请求操作,来跳转到对应的操作状态,等当前操作完成后,再跳回仲裁判断模块以此循环。

4.6.2 Verilog代码

`timescale 1ns / 1ps
module sdram_arbit(
    input                                               clk         ,
    input                                               rst_n       ,
    //sdram_init
    input           [3:0]                               init_cmd    ,
    input           [1:0]                               init_ba     ,
    input           [12:0]                              init_addr   ,
    input                                               init_done   ,
    //sdram_auto_ref
    input                                               aref_req    ,
    input           [3:0]                               aref_cmd    ,
    input           [1:0]                               aref_ba     ,
    input           [12:0]                              aref_addr   ,
    input                                               aref_done   ,
    //sdram_write
    input                                               wr_req    ,
    input           [3:0]                               wr_cmd    ,
    input           [1:0]                               wr_ba     ,
    input           [12:0]                              wr_addr   ,
    input                                               wr_done   ,
    input                                               wr_sdram_en ,
    input           [15:0]                              wr_sdram_data   ,
    //sdram_read
    input                                               rd_req    ,
    input           [3:0]                               rd_cmd    ,
    input           [1:0]                               rd_ba     ,
    input           [12:0]                              rd_addr   ,
    input                                               rd_done   ,

    output  reg                                         aref_en ,
    output  reg                                         wr_en   ,
    output  reg                                         rd_en   ,
    //sdram_interface
    output  wire                                        sdram_cke   ,   //SDRAM时钟使能
    output  wire                                        sdram_cs_n  ,   //SDRAM片选信号
    output  wire                                        sdram_ras_n ,   //SDRAM行地址选通
    output  wire                                        sdram_cas_n ,   //SDRAM列地址选通
    output  wire                                        sdram_we_n  ,   //SDRAM写使能
    output  reg     [1:0]                               sdram_ba    ,   //SDRAM Bank地址
    output  reg     [12:0]                              sdram_addr  ,   //SDRAM地址总线
    inout   wire    [15:0]                              sdram_dq    //SDRAM数据总线
    );

parameter   IDLE        =   5'b0_0001   ,   //初始状态
            ARBIT       =   5'b0_0010   ,   //仲裁状态
            AREF        =   5'b0_0100   ,   //自动刷新状态
            WRITE       =   5'b0_1000   ,   //写状态
            READ        =   5'b1_0000   ;   //读状态
parameter   CMD_NOP     =   4'b0111     ;   //空操作指令

reg             [3:0]                               sdram_cmd   ;
reg             [4:0]                               state   ;

always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        state <= IDLE;
    else
        case (state)
            IDLE: 
                if(init_done == 1'b1)
                    state <= ARBIT;    
                else
                    state <= IDLE;
            ARBIT:
                if(aref_req == 1'b1)
                    state <= AREF;  
                else if(wr_req == 1'b1)
                    state <= WRITE; 
                else if(rd_req == 1'b1) 
                    state <= READ;
                else
                    state <= ARBIT;  
            AREF:
                if(aref_done == 1'b1)
                    state <= ARBIT;  
                else
                    state <= AREF; 
            WRITE:
                if(wr_done == 1'b1)
                    state <= ARBIT;  
                else
                    state <= WRITE; 
            READ:
                if(rd_done == 1'b1)
                    state <= ARBIT;  
                else
                    state <= READ; 
            default: state <= IDLE;
        endcase
end

//如果自动刷新请求信号拉高
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        aref_en <= 1'b0;
    else if((state == ARBIT)&&(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 @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        wr_en <= 1'b0;
    else if((state == ARBIT)&&(aref_req == 1'b0)&&(wr_req == 1'b1))
        wr_en <= 1'b1;
    else if(wr_done == 1'b1)
        wr_en <= 1'b0;
    else
        wr_en <= wr_en;
end

//如果自动刷新没有请求,写请求信号也没拉高,并且读请求信号拉高
always @(posedge clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rd_en <= 1'b0;
    else if((state == ARBIT)&&(aref_req == 1'b0)&&(wr_req == 1'b0)&&(rd_req == 1'b1))
        rd_en <= 1'b1;
    else if(rd_done == 1'b1)
        rd_en <= 1'b0;
    else
        rd_en <= rd_en;
end

always @(*) begin
    case(state) 
        IDLE: begin
            sdram_cmd   <=  init_cmd;
            sdram_ba    <=  init_ba;
            sdram_addr  <=  init_addr;
        end
        AREF: begin
            sdram_cmd   <=  aref_cmd;
            sdram_ba    <=  aref_ba;
            sdram_addr  <=  aref_addr;
        end
        WRITE: begin
            sdram_cmd   <=  wr_cmd;
            sdram_ba    <=  wr_ba;
            sdram_addr  <=  wr_addr;
        end
        READ: begin
            sdram_cmd   <=  rd_cmd;
            sdram_ba    <=  rd_ba;
            sdram_addr  <=  rd_addr;
        end
        default: begin
            sdram_cmd   <=  CMD_NOP;
            sdram_ba    <=  2'b11;
            sdram_addr  <=  13'h1fff;
        end
    endcase
end

//SDRAM时钟使能
assign  sdram_cke = 1'b1;
//SDRAM数据总线
assign  sdram_dq = (wr_sdram_en == 1'b1) ? wr_sdram_data : 16'bz;
//片选信号,行地址选通信号,列地址选通信号,写使能信号
assign  {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} = sdram_cmd;

endmodule

4.7 控制模块

在这里插入图片描述
  把SDRAM初始化模块、SDRAM自动刷新模块、SDRAM写模块、SDRAM读模块、SDRAM仲裁模块全都用 SDRAM_CTRL顶层调用起来作为一个整体模块,和4.1章画的控制模块框图一致。

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
单片机(微控制器)PWM(脉宽调制)控制是一种通过调整信号的脉冲宽度来控制电路元件的工作的方法。下面是对单片机PWM控制的详细解释: 单片机通过产生一个特定频率的方波信号,并通过改变该信号的脉冲宽度来控制输出电平的高低。这就是PWM控制。 首先,利用单片机的定时器,设置工作时钟的频率。定时器的计数器会根据此频率不断累加,当计数器达到某个设定值时,会触发一个中断或产生一个特定信号,标志着一个周期的结束。 然后,设置一个占空比(Duty Cycle)值,代表高电平的持续时间占整个周期的比例。占空比的大小决定了输出电平的高低。 在每个周期内,当计数器的值小于占空比设定值时,输出为高电平;当计数器的值大于占空比设定值时,输出为低电平。以此类推,通过改变占空比的大小,可以控制输出电平的高低。 最后,将PWM信号输出到需要控制的电路元件上。通过PWM信号的高低电平交替变换的特性,能够控制电路元件工作的强度、速度等。 通过改变频率和占空比的数值,可以实现不同种类的PWM控制,如调光、调速、控制电机转速等。 需要注意的是,单片机的PWM控制原理是通过软件编程实现的,而具体的实现方式会因不同的单片机型号和开发环境而有所差异。在编程过程中,需要根据具体要求来设定定时器频率、占空比等参数,并合理处理定时器中断和输出引脚的操作。 总结一下,单片机PWM控制基本原理是通过定时器产生特定频率的方波信号,根据设定的占空比来控制输出电平的高低,从而实现对电路元件的控制

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱奔跑的虎子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值