『FPGA通信接口』DDR(4)DDR3内存条SODIMMs读写测试

文章目录

前言

不论是DDR3颗粒还是DDR3内存条,xilinx都是通过MIG IP核实现FPGA与DDR的读写。本文区别于DDR颗粒,记录几个与颗粒配置不同的地方。关于DDR的原理与MIG IP的简介,请查看前面文章,链接在文末。本文提供了配套的工程源码,链接在文末,本文用的内存条为MT16KTF1G64HZ-1G6,FPGA芯片为xc7k325tffg900 -2。请按照顺序循序渐进阅读本系列的文章。

1.MIG IP核配置

  • 1.如无特殊说明,配置保持与前述文章MIG IP核配置一致。第五页如图所示配置。其中①指DDR3颗粒的物理时钟,例如一颗16bit位宽的DDR设置为400MHz,则它的传输速度为每一个800MHz(DDR双沿传输的原因)周期传输16bit。这里时钟的范围受到FPGA芯片速度等级和型号的制约,以及与内存条的支持速度也有关。如我用的内存条MT16KTF1G64HZ-1G6,速度范围在1500ps-3000ps之间。②指的是用户时钟,4:1的4指的是①设置的物理时钟。第③部分指的是类型选择,内存条选择SODIMMs。④处是DDR3内存条的型号,如果不在列表需要根据速度参数,位宽大小找一个兼容的型号。⑤处勾选上mask后,如果相应的管脚不连接,会造成DDR3初始化失败。其余配置保持默认即可。
  • 2.第六页 此处①代表参考时钟,选择200MHz为固定大小。②处如果是内存条就选择RZQ/4,如果是颗粒就选择RZQ/6。
  • 3.DDR3颗粒要勾选DCI Cascade,内存条不用勾选。后面就是选择引脚,其他的都保持默认,即可。

2.测试程序

如果需要修改这段程序,需要注意MIG的接口各信号的位宽应该保持一致,另外程序中设计了两个LED灯,读写测试正确的时候,指示灯led1常亮,反之则闪烁。LED2只是容量,当测试到所设置的容量的时候常量。代码中TEST_LENGTH指示的含义是突发次数,也可以说是容量。每一次突发是512bit数据,使用内存的容量除以512bit,即为最大的突发次数。当然使用的芯片的物理位宽不同,例如只有一片16bit位宽的DDR颗粒,那一次突发的数据为8*16bit=128bit,那最大的突发次数就要用量除以128bit了。

cpp 复制代码
  module dimm_top(
    input              sys_clk_p,
    input              sys_clk_n,       
  inout  [63:0]      ddr3_dq			, 
  inout  [7:0]       ddr3_dqs_n		, 
  inout  [7:0]       ddr3_dqs_p		,   
  output [15:0]      ddr3_addr		, 
  output [2:0]       ddr3_ba			, 
  output             ddr3_ras_n		, 
  output             ddr3_cas_n		, 
  output             ddr3_we_n		, 
  output             ddr3_reset_n	, 
  output [1:0]       ddr3_ck_p		, 
  output [1:0]       ddr3_ck_n		, 
  output [1:0]       ddr3_cke		    , 
  output [1:0]       ddr3_cs_n		, 
  output [7:0]       ddr3_dm			, 
  output [1:0]       ddr3_odt         , 
    output reg         led1,              
    output reg         led2        
    );         
    wire                clk_rst;                            
    wire                clk_200;
    reg     [29:0]      app_addr_begin=0;
    wire                app_en;              //写命令使能
    wire    [2:0]       app_cmd;             //用户读写命令
    wire                app_wdf_wren;        //DDR3写使能
    wire                app_wdf_end;         //突发写最后一个数标识
    wire    [29:0]      app_addr;            //用户平面地址
    wire                app_rdy;             //设备接收准备就绪   
    wire                app_wdf_rdy;         //写响应
    wire    [511:0]     app_rd_data;         //用户读数据
    wire                app_rd_data_end;     //突发读当前时钟最后一个数据
    wire                app_rd_data_valid;   //读数据有效
    wire    [511:0]     app_wdf_data;        //用户写数据
    wire                app_sr_active;       //保留
    wire                app_ref_ack;         //刷新请求
    wire                app_zq_ack;          //ZQ 校准请求
    wire        		init_calib_complete; //校准完成信号
    wire                ui_clk ;             //用户时钟
    wire                ui_clk_sync_rst;              
    clk_wiz_0 u_clk_wiz_0(.clk_out1(clk_200), .reset(1'b0), .locked(clk_rst), .clk_in1_p(sys_clk_p),.clk_in1_n(sys_clk_n));
  mig_7series_0 mig_JC (
    // Memory interface ports
    .ddr3_addr                      (ddr3_addr), 		// output [15:0]	
    .ddr3_ba                        (ddr3_ba),  		// output [2:0]
    .ddr3_cas_n                     (ddr3_cas_n), 		// output			
    .ddr3_ck_n                      (ddr3_ck_n),  		// output [1:0]		
    .ddr3_ck_p                      (ddr3_ck_p),  		// output [1:0]		
    .ddr3_cke                       (ddr3_cke),  		// output [1:0]		
    .ddr3_ras_n                     (ddr3_ras_n),  		// output		
    .ddr3_reset_n                   (ddr3_reset_n),  	// output			
    .ddr3_we_n                      (ddr3_we_n),  		// output		
    .ddr3_dq                        (ddr3_dq),  		// inout [63:0]	
    .ddr3_dqs_n                     (ddr3_dqs_n),  		// inout [7:0]		
    .ddr3_dqs_p                     (ddr3_dqs_p),  		// inout [7:0]		
    .init_calib_complete            (init_calib_complete),  // output		
  .ddr3_cs_n                      (ddr3_cs_n),  		// output [1:0]		
    .ddr3_dm                        (ddr3_dm),  		// output [7:0]	
    .ddr3_odt                       (ddr3_odt),  		// output [1:0]		
    // Application interface ports
    .app_addr                       (app_addr), 		 // input [29:0]		
    .app_cmd                        (app_cmd),  		 // input [2:0]		
    .app_en                         (app_en),  			 // input	
    .app_wdf_data                   (app_wdf_data), 	 // input [511:0]	
    .app_wdf_end                    (app_wdf_end), 		 // input			
    .app_wdf_wren                   (app_wdf_wren),  	 // input				
    .app_rd_data                    (app_rd_data),  	 // output [511:0]		
    .app_rd_data_end                (app_rd_data_end),   // output		
    .app_rd_data_valid              (app_rd_data_valid), // output	
    .app_rdy                        (app_rdy),  		 // output		
    .app_wdf_rdy                    (app_wdf_rdy),  	 // output			
    .app_sr_req                     (1'b0),  			 // input			
    .app_ref_req                    (1'b0),  			 // input		
    .app_zq_req                     (1'b0),              // input			
    .app_sr_active                  (app_sr_active),     // output			
    .app_ref_ack                    (app_ref_ack), 		 // output		
    .app_zq_ack                     (app_zq_ack),  	   	 // output		
    .ui_clk                         (ui_clk),  			 // output用户时钟输出,其实是通过IP配置自己配出来的 	 
    .ui_clk_sync_rst                (ui_clk_sync_rst),   // output		
    .app_wdf_mask                   (64'b0),  			 // input [63:0] //写数据屏蔽
    .sys_clk_i                      (clk_200),//输入IP的时钟
    // Reference Clock Ports
    .clk_ref_i                      (clk_200),//参考时钟		
    .sys_rst                        (clk_rst) // input sys_rst
    );
  parameter  TEST_LENGTH = 27'd134200000; //每一次突发是512bit 8GB可以支持134217728次突发 99.98%
  // parameter  TEST_LENGTH = 32'd60000000;
  //**************1.先写后读状态机state machine
  parameter  IDLE  = 2'd0;           
  parameter  WRITE = 2'd1;          
  parameter  WAIT  = 2'd2;           
  parameter  READ  = 2'd3;   
  reg [511:0]my_512_data;
  reg [26:0] wr_addr_cnt;
  reg [26:0] rd_addr_cnt;
  reg [1:0]  state;
  always @(posedge ui_clk or negedge rst_n) begin
     if((~rst_n)||(error_flag)) begin 
         state    <= IDLE;          
         my_512_data <= 512'd0;     
         wr_addr_cnt  <= 27'd0;      
         rd_addr_cnt  <= 27'd0;       
         app_addr_begin<= 30'd0;         
     end
     else if(init_calib_complete)begin               //MIG IP核初始化完成
         case(state)
            IDLE:begin
                state    <= WRITE;
                my_512_data <= 512'd0;   
                wr_addr_cnt  <= 27'd0;     
                rd_addr_cnt  <= 27'd0;       
                app_addr_begin     <= 30'd0; 
            end
            WRITE:begin
                if((wr_addr_cnt == TEST_LENGTH-1) &&(app_rdy && app_wdf_rdy))
                    state    <= WAIT;                  //写到设定的长度跳到等待状态
                else if(app_rdy && app_wdf_rdy)begin   //写条件满足
                    my_512_data <= my_512_data + 1;  //写数据自增
                    wr_addr_cnt  <= wr_addr_cnt + 1;   //写计数自增
                    app_addr_begin<= app_addr_begin + 8;      //DDR3 地址自增
                end else begin          //写条件不满足,保持当前状态
                     my_512_data <= my_512_data;      
                     wr_addr_cnt  <= wr_addr_cnt;
                     app_addr_begin<= app_addr_begin; 
                end
            end
            WAIT:begin                                                 
                state   <= READ;                     //下一个时钟,跳到读状态
                rd_addr_cnt <= 27'd0;                //读地址复位
                app_addr_begin<= 30'd0;                //DDR3读从地址0
            end
            READ:begin                               //读到设定的地址长度    
                if((rd_addr_cnt == TEST_LENGTH -1 ) && app_rdy)
                    state   <= IDLE;                   //则跳到空闲状态 
                else if(app_rdy)begin                  //若MIG已经准备就绪,则开始读
                    rd_addr_cnt <= rd_addr_cnt + 1'd1; //用户地址每次加一
                    app_addr_begin    <= app_addr_begin + 8;       //DDR3地址加8
                end else begin   //若MIG没准备好,则保持原
                    rd_addr_cnt <= rd_addr_cnt;
                    app_addr_begin    <= app_addr_begin; 
                end
            end
            default:begin
                state    <= IDLE;
                my_512_data  <= 512'd0;
                wr_addr_cnt  <= 27'd0;
                rd_addr_cnt  <= 27'd0;
                app_addr_begin <= 30'd0;
            end
         endcase
     end
  end   
  //**************2.根据状态机与MIG指示信号为app信号赋值
  assign app_en  =((state == WRITE && (app_rdy && app_wdf_rdy))||(state == READ && app_rdy)) ? 1'b1:1'b0;             
  assign app_cmd =(state == READ) ? 3'd1 :3'd0;  
  assign app_wdf_wren=(state == WRITE && (app_rdy && app_wdf_rdy)) ? 1'b1:1'b0;
  assign app_wdf_end =app_wdf_wren; 
  assign app_addr    =app_addr_begin;
  assign app_wdf_data=my_512_data;   
  //*******************3.用户判错逻辑
  reg     [26:0]   rd_cnt;
  wire             rst_n;     //复位,低有效
  reg              error_flag;
  parameter  L_TIME = 28'd200_000_000;
  reg     [27:0]   led_cnt;    //led计数
  wire             error;     //读写错误标记
  assign rst_n = ~ui_clk_sync_rst;//&&myrst
  always @(posedge ui_clk or negedge rst_n) begin
     if(~rst_n) 
        rd_cnt  <= 0;              //若计数到读写长度,且读有效,地址计数器则�?0                                    
     else if(app_rd_data_valid&&(rd_cnt == TEST_LENGTH - 1))
        rd_cnt <= 0;              //其他条件只要读有效,每个时钟自增1
     else if (app_rd_data_valid)
        rd_cnt <= rd_cnt + 1;
  end
  //判断错误,读出数据应为计数递增数据
  assign error = (app_rd_data_valid && (rd_cnt!=app_rd_data));
  always @(posedge ui_clk or negedge rst_n) begin
    if(~rst_n)
        led2<=0;
    else if(rd_cnt==32'd134200000-1)
        led2<=1;
  end 
  always @(posedge ui_clk or negedge rst_n) begin
     if(~rst_n) 
         error_flag <= 0;
     else if(error)
         error_flag <= 1;
  end
  //读写测试正确,指示灯led1常亮,反之则闪烁
  always @(posedge ui_clk or negedge rst_n) begin
      if((~rst_n) || (~init_calib_complete )) begin
         led_cnt <= 28'd0;
         led1 <= 1'b0;
     end
     else begin
         if(~error_flag)   //常亮代表正常,闪烁代表故障                            
             led1 <= 1'b1;                     
         else begin                            
             led_cnt <= led_cnt + 28'd1;
             if(led_cnt == L_TIME - 1'b1) begin
             led_cnt <= 25'd0;
             led1 <= ~led1;                     
             end                    
        end
    end
  end
  endmodule

3.DDR应用

DDR在FPGA系统中的作用主要是作为存储器使用,用于存储数据和程序。DDR存储器通常被用作FPGA系统中的主存储器,用于存储采集数据和中间结果。DDR3作为高速缓存与FPGA相连,在不同领域均发挥着重要作用。在高性能计算领域,DDR用于存储大规模数据集、模型参数、数据计算结果,从而充分发挥FPGA并行计算的能力,完成计算任务;在图像处理领域,用于匹配图像采集接口与传输接口之间的速度,完成图像采集;在通信领域,DDR用于存储大量数据包,实现数据的缓存和处理,提高数据传输速度和处理效率。总之,DDR在不同领域扮演着重要角色。以采集摄像头数据为例,采用乒乓操作的思想是在DDR中开辟两块大小为1帧图像的缓冲区,如果读取速度大于写入速度的时候,需要采用乒乓操作的方式发挥DDR弹性缓冲的作用。往缓冲区写的时候,1号缓冲区写满之后,切换到2号缓冲区写,2号写满之后,在往1号去写,如此往复 。由于读取速度大于写入速度,因此,读一定是在与当前写不同的另一块缓冲区去读,在底层,可能会对同一块缓冲区的数据读取很多次,但是这并不影响人在视觉上对于画面流畅的影响。这就是通过乒乓操作实现了数据缓冲,匹配了读写两端的速度。如下图所示,

说明:1.每一个bank存储一帧图像数据,bank中每一行为图像的一行数据(即每一次读写的突发长度是一行像素数据,这个突发长度可以自己定义,并不必须要是一行数据),读和写彼此独立进行;2.bank之间的切换由状态机实现,由于读速度大于写速度,则每写完一个bank切换另一个bank去写;每读完一个bank,判断当前写bank,选择不同于写bank的bank进行读。3.由于读速度大于写速度,因此永远不会发生冲突,只是可能某一帧会被重复播放,但在视频应用中,这对用户的视觉不产生任何影响。同理,如果是读的速度小于写的速度,那让写操作刷新缓冲区即选择与读相排斥的缓冲区写,让读操作按照顺序读即可。上面描述的只是一种乒乓缓冲的思想,实际操作中,可以通过设置三缓存,四缓存的方式让图像更为平滑。另外可以考虑基于这种思想为DDR写一个消息队列,让DDR仲裁控制器的通用性和适配性更强。

4.传送门

|-----|
| END |

🔈文章原创,首发于CSDN论坛。

🔈欢迎点赞❤❤收藏⭐⭐打赏💴💴!

🔈欢迎评论区或私信指出错误❌,提出宝贵意见或疑问❓。

相关推荐
内有小猪卖3 分钟前
时序约束 记录
fpga开发
Cao1234567893213 小时前
FPGA时钟设计
fpga开发
JNTeresa6 小时前
锁存器知识点详解
fpga开发
Cao1234567893219 小时前
FPGA基础之基础语法
fpga开发
一大Cpp10 小时前
通过Quartus II实现Nios II编程
fpga开发
7yewh10 小时前
Verilog 语法 (二)
fpga开发
边缘计算社区1 天前
FPGA与边缘AI:计算革命的前沿力量
人工智能·fpga开发
S&Z34631 天前
[官方IP] Shift RAM
网络协议·tcp/ip·fpga开发
S&Z34631 天前
[FPGA Video IP] Video Processing Subsystem
网络协议·tcp/ip·fpga开发·video
FPGA_Linuxer1 天前
FPGA 100G UDP纯逻辑协议栈
网络协议·fpga开发·udp