FPGA学习历程(六):SDRAM 控制器(读)


以下内容学习自开源骚客教程: SDRAM那些事儿第一季。

一、模块时序图

  时间参数和写模块是一样的:
在这里插入图片描述

二、模块源码

2.1 sdram_read.v

  可以偷个懒直接修改 sdram_write.v 的副本。

module sdram_read
(
    // system signals
    input 				    sclk	    ,       // 板载系统时钟 50MHz
	input                   s_rst_n     ,       // 复位信号,低电平有效
    
    // communicate with ARBIT
    input                   rd_en       ,
    output  wire            rd_req      ,
    output  reg             flag_rd_end ,
    
    // others
    input                   ref_req     ,
    input                   rd_trig     ,
    output  reg [3:0]       rd_cmd      ,
    output  reg [12:0]      rd_addr     ,
    output  wire [1:0]      bank_addr
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// 状态定义
localparam      S_IDLE  =   5'b0_0001       ;
localparam      S_REQ   =   5'b0_0010       ;
localparam      S_ACT   =   5'b0_0100       ;
localparam      S_RD    =   5'b0_1000       ;
localparam      S_PRE   =   5'b1_0000       ;

// SDRAM Command
localparam                  CMD_NOP         =   4'b0111 ;
localparam                  CMD_PRE         =   4'b0010 ;
localparam                  CMD_AREF        =   4'b0001 ;
localparam                  CMD_ACT         =   4'b0011 ;
localparam                  CMD_RD          =   4'b0101 ;

reg                         flag_rd         ;
reg     [4:0]               state           ;

// 大框架信号定义
reg                         flag_act_end    ;
reg                         flag_pre_end    ;
reg                         sd_row_end      ;
reg     [1:0]               burst_cnt       ;
reg     [1:0]               burst_cnt_t     ;
reg                         rd_data_end     ;

// 读命令产生信号定义
reg     [3:0]               act_cnt         ;
reg     [3:0]               break_cnt       ;
reg     [6:0]               col_cnt         ;
reg     [12:0]              row_addr        ;
wire    [8:0]               col_addr        ;

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
// flag_rd:外部提供读触发,且本身并不在模块读状态时进入模块读状态,数据读完时退出模块读状态
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        flag_rd <= 1'b0;
    else if(rd_trig == 1'b1 && flag_rd == 1'b0)
        flag_rd <= 1'b1;
    else if(rd_data_end == 1'b1)
        flag_rd <= 1'b0;
end

// burst_cnt:状态机读状态下才进行自加,2 位位宽(一个 CLK 对应一个数据)
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        burst_cnt <= 'd0;
    else if(state == S_RD)
        burst_cnt <= burst_cnt + 1'b1;
    else
        burst_cnt <= 'd0;
end

// burst_cnt_t
always  @(posedge sclk)
begin
    burst_cnt_t <= burst_cnt;
end

always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        state <= S_IDLE;
    else
        case(state)
            S_IDLE:
                if(rd_trig == 1'b1)
                    state <= S_REQ;
                else
                    state <= S_IDLE;
            S_REQ:
                if(rd_en == 1'b1)
                    state <= S_ACT;
                else
                    state <= S_REQ;
            S_ACT:
                if(flag_act_end == 1'b1)
                    state <= S_RD;
                else
                    state <= S_ACT;
            S_RD:
                if(rd_data_end == 1'b1)     // 数据写完了
                    state <= S_PRE;
                else if(ref_req == 1'b1 && burst_cnt == 'd3 && flag_rd == 1'b1)   // 模块写状态下需要进行刷新,且已经写完了当前批次的 burst
                    state <= S_PRE;
                else if(sd_row_end == 1'b1 && flag_rd == 1'b1)      // 写完一行了但是还在模块写状态(数据没写完)
                    state <= S_PRE;
            S_PRE:
                if(ref_req == 1'b1 && flag_rd == 1'b1)     // 这里可以继续使用 ref_req 信号来判断的原因是顶层仲裁模块的状态还没有进行切换,刷新请求就会一直保持
                    state <= S_REQ;
                    /* 这个位置的跳转条件不够严谨,且此处状态机设计与写模块内部时序图设计实际上存在相悖之处。
                    ** 状态机下从 S_PRE 跳转到 S_ACT 的条件是 sd_row_end 信号,而在时序图中 sd_row_end 信
                    ** 号在从 S_RD 跳出的时刻就动作了,所以不能作为这里的直接判断依据。而 flag_pre_end 信号
                    ** 只是 S_PRE 结束的标志信号而已,并非是 “换行退出状态机写状态” 的唯一标志。
                    */
                else if(flag_pre_end == 1'b1 && flag_rd == 1'b1)
                    state <= S_ACT;
                else if(rd_data_end == 1'b1)
                    state <= S_IDLE;
            default:
                state <= S_IDLE;
        endcase
end

// rd_cmd:只在各状态的一开始才给命令
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        rd_cmd <= CMD_NOP;
    else
        case(state)
            S_ACT:
                if(act_cnt == 'd0)
                    rd_cmd <= CMD_ACT;
                else
                    rd_cmd <= CMD_NOP;
            S_RD:
                if(burst_cnt == 'd0)
                    rd_cmd <= CMD_RD;
                else
                    rd_cmd <= CMD_NOP;
            S_PRE:
                if(break_cnt == 'd0)
                    rd_cmd <= CMD_PRE;
                else
                    rd_cmd <= CMD_NOP;
            default:
                rd_cmd <= CMD_NOP;
        endcase
end

// rd_addr:S_ACT 状态给行地址,S_RD 状态给列地址
always  @(*)
begin
    case(state)
        S_ACT:
            if(act_cnt == 'd0)
                rd_addr <= row_addr;
        S_RD:
            rd_addr <= {4'b0000,col_addr};
        S_PRE:
            if(break_cnt == 'd0)
                rd_addr <= 13'b0_0100_0000_0000;
    endcase
end

// flag_act_end:在 S_ACT 状态计时 3 个 CLK
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        flag_act_end <= 1'b0;
    else if(act_cnt == 'd3)
        flag_act_end <= 1'b1;
    else
        flag_act_end <= 1'b0;
end

// act_cnt
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        act_cnt <= 'd0;
    else if(state == S_ACT)
        act_cnt <= act_cnt + 1'b1;
    else
        act_cnt <= 'd0;
end

// flag_pre_end:在 S_PRE 状态计时 3 个 CLK
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        flag_pre_end <= 1'b0;
    else if(break_cnt == 'd3)
        flag_pre_end <= 1'b1;
    else
        flag_pre_end <= 1'b0;
end

// flag_rd_end
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        flag_rd_end <= 1'b0;
    else if((state == S_PRE && ref_req == 1'b1) ||      // 仿真调试后的判断条件
            (state == S_PRE && rd_data_end == 1'b1))      
        flag_rd_end <= 1'b1;
    else
        flag_rd_end <= 1'b0;
end

// break_cnt
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        break_cnt <= 'd0;
    else if(state == S_PRE)
        break_cnt <= break_cnt + 1'b1;
    else
        break_cnt <= 'd0;
end

// rd_data_end
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        rd_data_end <= 1'b0;
    else if(row_addr == 'd2 && col_addr == 'd511)       // 写两行数据特有标志(第二行也写完了才算数据写完)
        rd_data_end <= 1'b1;
    else
        rd_data_end <= 1'b0;
end

// col_cnt:实际上被视为一个四进制计数器的高位(低位是 burst_cnt_t 的 0~3)
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        col_cnt <= 'd0;
    else if(col_addr == 'd511)
        col_cnt <= 'd0;
    else if(burst_cnt_t == 'd3)
        col_cnt <= col_cnt + 1'b1;
end

// sd_row_end
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        sd_row_end <= 1'b0;
    else if(col_addr == 'd509)  // 这里提前是因为 col_addr 使用的是 burst_cnt 延拍
        sd_row_end <= 1'b1;
    else
        sd_row_end <= 1'b0;
end

// row_addr
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        row_addr <= 'd0;
    else if(sd_row_end == 1'b1)
        row_addr <= row_addr + 1'b1;
end

assign col_addr     =   {col_cnt,burst_cnt_t};
assign bank_addr    =   2'b00;
assign rd_req       =   state[1];

endmodule 

2.2 sdram_top.v

module sdram_top
(
    // system signals
    input 				    sclk	    ,       // 板载系统时钟 50MHz
	input                   s_rst_n     ,       // 复位信号,低电平有效
    
    // SDRAM Interfaces
    output  wire            sdram_clk   ,
    output  wire            sdram_cke   ,
    output  wire            sdram_cs_n  ,
    output  wire            sdram_cas_n ,
    output  wire            sdram_ras_n ,
    output  wire            sdram_we_n  ,
    output  wire [1:0]      sdram_bank  ,
    output  reg  [12:0]     sdram_addr  ,
    output  wire [1:0]      sdram_dqm   ,
    inout        [15:0]     sdram_dq    ,
    
    //others
    input                   wr_trig     ,       // 仿真测试用
    input                   rd_trig
);

/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
localparam          IDLE    =   5'b0_0001   ;   // 空闲状态
localparam          ARBIT   =   5'b0_0010   ;   // 仲裁状态
localparam          AREF    =   5'b0_0100   ;   // 刷新状态
localparam          WRITE   =   5'b0_1000   ;   // 写状态
localparam          READ    =   5'b1_0000   ;   // 读状态

reg     [3:0]               sd_cmd          ;

// init module
wire                        flag_init_end   ;
wire    [3:0]               init_cmd        ;
wire    [12:0]              init_addr       ;

// 仲裁模块
reg     [4:0]               state           ;

// refresh module
wire                        ref_req         ;   // 刷新请求(刷新模块产生)
wire                        flag_ref_end    ;   // 刷新结束标志(刷新模块产生)
reg                         ref_en          ;   // 刷新使能(仲裁模块产生)
wire    [3:0]               ref_cmd         ;
wire    [12:0]              ref_addr        ;

// write module
reg                         wr_en           ;
wire                        wr_req          ;
wire                        flag_wr_end     ;
wire    [3:0]               wr_cmd          ;
wire    [12:0]              wr_addr         ;
wire    [1:0]               wr_bank_addr    ;
wire    [15:0]              wr_data         ;

// read module
reg                         rd_en           ;
wire                        rd_req          ;
wire                        flag_rd_end     ;
wire    [3:0]               rd_cmd          ;
wire    [12:0]              rd_addr         ;
wire    [1:0]               rd_bank_addr    ;

/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        state <= IDLE;
    else 
        case(state)
            IDLE:
                if(flag_init_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= IDLE;
            ARBIT:
                if(ref_en == 1'b1)          // 优先刷新
                    state <= AREF;
                else if(wr_en == 1'b1)
                    state <= WRITE;
                else if(rd_en == 1'b1)      // 写优先级高于读
                    state <= READ;
                else
                    state <= ARBIT;
            AREF:
                if(flag_ref_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= AREF;
            WRITE:
                if(flag_wr_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= WRITE;
            READ:
                if(flag_rd_end == 1'b1)
                    state <= ARBIT;
                else
                    state <= READ;
            default:
                state <= IDLE;
        endcase
end

// ref_en
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        ref_en <= 1'b0;
    else if(state == ARBIT && ref_req == 1'b1)
        ref_en <= 1'b1;
    else
        ref_en <= 1'b0;
end

// wr_en
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        wr_en <= 1'b0;
        else if(state == ARBIT && ref_req == 1'b0 && wr_req == 1'b1)    // 只有在不和刷新操作冲突的时候才允许进行写操作
        wr_en <= 1'b1;
    else
        wr_en <= 1'b0;
end

// rd_en
always  @(posedge sclk or negedge s_rst_n)
begin
    if(s_rst_n == 1'b0)
        rd_en <= 1'b0;
    else if(state == ARBIT && ref_req == 1'b0 && wr_req == 1'b0 && rd_req == 1'b1)    // 只有在不和刷新操作、写操作冲突的时候才允许进行写操作
        rd_en <= 1'b1;
    else
        rd_en <= 1'b0;
end

// sd_cmd
always  @(*)
begin
    case(state)
        IDLE:
        begin
            sd_cmd <= init_cmd;
            sdram_addr <=   init_addr;
        end
        AREF:
        begin
            sd_cmd <= ref_cmd;
            sdram_addr <=   ref_addr;
        end
        WRITE:
        begin
            sd_cmd  <=  wr_cmd;
            sdram_addr  <=  wr_addr;
        end
        READ:
        begin
            sd_cmd  <=  rd_cmd;
            sdram_addr  <=  rd_addr;
        end
        default:
        begin
            sd_cmd  <= 4'b0111;         // NOP
            sdram_addr  <=  'd0;
        end
    endcase
end

assign  sdram_cke       =   1'b1;
assign  {sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n} =    sd_cmd;
assign  sdram_dqm       =   2'b00;
assign  sdram_clk       =   ~sclk;      // sdram 命令生成时钟与 sdram 命令采集时钟反向,保证命令采集时命令已经稳定生成
assign  sdram_dq        =   (state == WRITE) ? wr_data : {16{1'bz}};
assign  sdram_bank      =   (state == WRITE) ? wr_bank_addr : rd_bank_addr;

sdram_init sdram_init_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // others
    .cmd_reg        (init_cmd),         // 输出的命令(即 CS、RAS、CAS、WE 这四位)
    .sdram_addr     (init_addr),        // SDRAM 地址
    .flag_init_end  (flag_init_end)     // 初始化结束标志
);

sdram_aref sdram_aref_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // communicate with ARBIT
    .ref_en         (ref_en),
    .ref_req        (ref_req),
    .flag_ref_end   (flag_ref_end),     // 刷新结束标志
    
    // others
    .aref_cmd       (ref_cmd),          // 刷新命令
    .sdram_addr     (ref_addr),
    .flag_init_end  (flag_init_end)
);

sdram_write sdram_write_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // communicate with ARBIT
    .wr_en          (wr_en),
    .wr_req         (wr_req),
    .flag_wr_end    (flag_wr_end),
    
    // others
    .ref_req        (ref_req),
    .wr_trig        (wr_trig),
    .wr_cmd         (wr_cmd),
    .wr_addr        (wr_addr),
    .bank_addr      (wr_bank_addr),
    .wr_data        (wr_data)
);

sdram_read sdram_read_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // communicate with ARBIT
    .rd_en          (rd_en),
    .rd_req         (rd_req),
    .flag_rd_end    (flag_rd_end),
    
    // others
    .ref_req        (ref_req),
    .rd_trig        (rd_trig),
    .rd_cmd         (rd_cmd),
    .rd_addr        (rd_addr),
    .bank_addr      (rd_bank_addr)
);
endmodule 

2.3 tb_sdram_top.v

`timescale 1ns/1ns

module tb_sdram_top;

reg             sclk        ;
reg             s_rst_n     ;

wire            sdram_clk   ;
wire            sdram_cke   ;
wire            sdram_cs_n  ;
wire            sdram_cas_n ;
wire            sdram_ras_n ;
wire            sdram_we_n  ;
wire [1:0]      sdram_bank  ;
wire [12:0]     sdram_addr  ;
wire [1:0]      sdram_dqm   ;
wire [15:0]     sdram_dq    ;

reg             wr_trig     ;
reg             rd_trig     ;

initial
begin
    wr_trig <=  0;
    rd_trig <=  0;
    #205000
    wr_trig <=  1;
    #20
    wr_trig <=  0;
    #25000
    rd_trig <=  1;
    #20
    rd_trig <=  0;
end

initial 
begin
    sclk    =   1;
    s_rst_n <=  0;
    #100
    s_rst_n <=  1;
end

always  #10     sclk    =   ~sclk;

sdram_top sdram_top_inst
(
    // system signals
    .sclk	        (sclk),             // 板载系统时钟 50MHz
	.s_rst_n        (s_rst_n),          // 复位信号,低电平有效
    
    // SDRAM Interfaces
    .sdram_clk      (sdram_clk),
    .sdram_cke      (sdram_cke),
    .sdram_cs_n     (sdram_cs_n),
    .sdram_cas_n    (sdram_cas_n),
    .sdram_ras_n    (sdram_ras_n),
    .sdram_we_n     (sdram_we_n),
    .sdram_bank     (sdram_bank),
    .sdram_addr     (sdram_addr),
    .sdram_dqm      (sdram_dqm),
    .sdram_dq       (sdram_dq),
    
    //ohters
    .wr_trig        (wr_trig),
    .rd_trig        (rd_trig)
);

defparam	sdram_model_plus_inst.addr_bits = 13;					// 13
defparam    sdram_model_plus_inst.data_bits = 16;
defparam	sdram_model_plus_inst.col_bits	= 9;
defparam    sdram_model_plus_inst.mem_sizes = 2*1024*1024;		    // 2M

sdram_model_plus sdram_model_plus_inst
(
    .Dq              (sdram_dq), 
    .Addr            (sdram_addr), 
    .Ba              (sdram_bank), 
    .Clk             (sdram_clk), 
    .Cke             (sdram_cke), 
    .Cs_n            (sdram_cs_n), 
    .Ras_n           (sdram_ras_n), 
    .Cas_n           (sdram_cas_n), 
    .We_n            (sdram_we_n), 
    .Dqm             (sdram_dqm),
    .Debug           (1'b1)
);

endmodule 

三、Modelsim仿真

3.1 run.do脚本

  同样是在写模块的基础上做些小修改:

// run.do 源码
# 在当前路径下创建 work 文件夹
vlib work

# 编译下列文件
vlog	"./tb_sdram_top.v"
vlog	"./sdram_model_plus.v"
vlog	"../../rtl/sdram/*.v"

# 优化部分参数(-voptargs=+acc),链接到默认的 work 库,启动仿真顶层测试逻辑库(work)里面的 tb_sdram_top 文件
vsim	-voptargs=+acc work.tb_sdram_top

# Set the window types
view wave
view structure
view signals

# 添加分割线,在不同的信号之间进行分割,语法格式是:add wave -divider {分割线的名字}
add wave -divider {sdram_top}
# 添加指定例化模块下的所有信号
add wave tb_sdram_top/sdram_top_inst/*
add wave -divider {sdram_write}
add wave tb_sdram_top/sdram_top_inst/sdram_write_inst/*
add wave -divider {sdram_read}
add wave tb_sdram_top/sdram_top_inst/sdram_read_inst/*
add wave -divider {sdram_model}
add wave tb_sdram_top/sdram_model_plus_inst/*
# 清屏
.main clear

run 205us

3.2 补充

  仿真波形就不贴了,这里只简单提一下在读数据时出现的特有现象,即从读状态退出到仲裁状态时,在给了 Precharge 命令后还是出来了两个数据,这属于正常现象(因为有潜伏期,即手册中的参数 CL):
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值