本帖最后由 yyliu 于 2023-2-19 15:34 编辑
声明:1.本帖如有对引用其他网站资源,均附上了网址,针对本帖中可能出现的侵权行为,请及时联系本人修改或删除。
2. 未经本人允许,请勿转载。若本帖存在错误或不足之处,烦请指正,本人会及时修改。
3.本帖代码根据正点原子代码修改,并给出了源码,若对正点原子构成侵权,请及时联系本人删除。
0.说明
DDR在高速电路中较为常见,面向服务器、云计算、网络、笔记本电脑、台式机和消费类应用。本帖分为两个部分,第一部分是DDR的硬件设计经验,第二部分是学习正点原子的FPGA开发板DDR读写实验。
学习本帖需要掌握DDR的基础知识,包括DDR读写原理、DDR内存容量的计算、DDR的操作时序、DDR各信号的作用、DDR3和DDR4的区别等。这部分基础知识网上有很多资源,本帖不再一一介绍,默认大家有相关基础。
1.DDR硬件设计
DDR的硬件设计笼统的可以分为DDR DIMM设计和MEMORY DOWN设计,DDR DIMM就是我们常在计算机、服务器中见到的内存条,内存条容量易扩展、便于更换和维护,内存条是将内存颗粒焊接在内存条这一PCB上,金手指和信号都必须符合内存条的设计规范,内存条根据使用场合不同还分为UDIMM、RDIMM、LRDIMM等等;而MEMORY DOWN设计不同于内存条,它是将内存颗粒焊接在主板上,能够节约成本,容量也可以选配,但是灵活性不如内存条。一般MEMORY DOWN设计多用于对内存容量需求较小,CPU运算量有限的场合,如消费电子等领域。
正点原子的国产紫光同创FPGA开发板使用的就是MEMORY DOWN的设计方法。下面分别对两种设计方法加以介绍。
1.1 DDR DIMM设计
我们首先以镁光的内存条DDR4 SDRAM SODIMM MTA18ASF2G72HZ-16GB的datasheet为例,简单介绍下信号定义及作用:
由于信号较多,可以按照功能对上述信号进行分组:
1.数据组:MDQS(8:0),DQS(8:0),MDM(8:0),MDQ(63:0),MECC(7:0)
2.地址/命令组:MBA(2:0),MA(15:0),RAS,MCAS,MWE
3.控制组:CS(3:0),MCKE(3:0),MODT(3:0)
4.时钟组:MCK(5:0),CK(5:0)
CPU与DDR DIMM信号连接规则:
1.数据组:CPU MDQ数据线与DDR DIMM数据线相连,MDQS数据选取脉冲,一个DQS对应一组数据线MDQ,为了走线方便,数据线组内可以相互交换;但是组间不可以交换;
MECC是数据校验位,一个MDQS对应8bit数据校验;
2.地址/命令组:MBA是选择内存颗粒的BANKS信号,内存颗粒可能有4或8个BANKS信号,对于DDR4来说,还出现了BANK GROUPS。如果我们希望对内存进行读写操作,首先需要选中内存颗粒的BANK GROUPS,再选中这个BANK GROUPS的某一个BANK,再根据行列地址定位到内存单元。
3.控制组:CS信号用于选择某个内存条或者内存条的某几个内存颗粒,只有在CS有效,对内存的操作才能起作用。MCKE是时钟使能信号,对于内存条的设计来说,还有个RANK的概念,对于64bit的计算机来说,如果我们的内存条能够组成多个64bit位宽与CPU交互,则成为多RANK设计。每个RANK单独使用一路时钟使能信号MCKE和MODT信号。
4.时钟组:个RANK单独使用一路时钟使能信号MCK。
以上是对DDR信号及互联的介绍,下图给出了Wistron Corporation公司某原理图的DDR DIMM设计案例(侵删):
该设计的HOST是INTEL CPU,内存条为DDR3。
1.数据组:该设计为了走线方便,数据线大量采用了组内交换和整组交换。从DQS信号有8组来看,此内存条不支持ECC校验。
2.地址命令组:地址线共15根,A10和A16可以复用;内存颗粒有4个BANKS,因此采用两个BANK信号与CPU相连;
3.控制组:时钟组使能共有两组,因此该内存条为双RANK设计。
4.时钟组:时钟组共有两组,时钟源来自CPU。
1.2 MEMORY DOWN设计
正点原子提供的原理图就是采用MEMORY DOWN设计方案,直接参考对应内存颗粒和CPU的datasheet即可。内存颗粒和内存条相比,信号数量更少,共有的信号作用都相同,因此设计难度更低一些。
下图是正点原子的内存颗粒原理图(侵删),具体信号和互联不再分析了,参考DDR DIMM的设计:
对于另一种场景,出于速率和稳定性、成本、布线难度等因素,我们使用可能不止一颗内存颗粒,而是采用多颗内存颗粒组成和DDR DIMM相同的容量。这种场景就需要我们能够组合不同的数量和容量内存颗粒达到项目对内存容量的需求。我们在设计评估时要能列出表格。下图出自英特尔,给出了MEMORY DOWN某设计框图。
2. DDR读写实验(出自正点原子)
PGL22系列FPGA自带了DDR3控制器的硬核,可以直接借助IP核来实现对DDR3的读写操作,本次实验将使用紫光公司的LogosIMIC HIP核来实现DDR3读写测试。
HMICHIP是深圳市紫光同创电子有限公司FPGA产品中用于实现对SDRAM读写而设计的IP,通过紫光同创公司Pango Design Suite套件(后文简称PDS)中IPCompiler工具(后文简称IPC)例化生成IP模块。HMICHIP系统框图如下:
2.1 实验任务
PGA调用ddr3测试数据模块向ddr3控制模块写入数据,写完之后ddr测试数据模块从ddr3控制模块读出所写入的数据,并判断读出的数据与写入数据是否相同,如果相同则LED1灯常亮,否则LED1灯闪烁。
ddr3控制模块产生读写DDR3IP核用户接口的时序,实现与DDR3IP核的数据及信号交互。ddr3控制模块一方面负责与用户FPGA进行数据交互,另一方面还产生控制DDR3读写的各种时序,并实现对DDR芯片的读写操作。
ddr测试数据模块的作用是写入和读出ddr3控制器的数据并且将读写数据进行比较。
led显示模块是根据读写错误信号的高低来判断是否翻转LED灯的电平,以及显示ddr3初始化完成情况。
2.2 程序运行逻辑
module ddr3_rw_top(
input sys_clk , //系统时钟50M
input sys_rst_n , //系统复位
output led_error , //读写错误led灯
output led_ddr_init_done, //ddr3初始化完成led灯
//DDR3接口
input pad_loop_in , //低位温度补偿输入
input pad_loop_in_h , //高位温度补偿输入
output pad_rstn_ch0 , //Memory复位
output pad_ddr_clk_w , //Memory差分时钟正端
output pad_ddr_clkn_w , //Memory差分时钟负端
output pad_csn_ch0 , //Memory片选
output [15:0] pad_addr_ch0 , //Memory地址总线
inout [16-1:0] pad_dq_ch0 , //数据总线
inout [16/8-1:0] pad_dqs_ch0 , //数据时钟正端
inout [16/8-1:0] pad_dqsn_ch0 , //数据时钟负端
output [16/8-1:0] pad_dm_rdqs_ch0 , //数据Mask
output pad_cke_ch0 , //Memory差分时钟使能
output pad_odt_ch0 , //On Die Termination
output pad_rasn_ch0 , //行地址strobe
output pad_casn_ch0 , //列地址strobe
output pad_wen_ch0 , //写使能
output [2:0] pad_ba_ch0 , //Bank地址总线
output pad_loop_out , //低位温度补偿输出
output pad_loop_out_h //高位温度补偿输出
);
//parameter define
parameter APP_ADDR_MIN = 28'd0 ; //ddr3读写起始地址,以一个16bit的数据为一个单位
//APP_ADDR_MAX = BURST_LENGTH * 8 * (n+1)(n表示突发次数)
parameter APP_ADDR_MAX = 28'd5120 ; //ddr3读写结束地址,以一个16bit的数据为一个单位
parameter BURST_LENGTH = 8'd64 ; //ddr3读写突发长度,64个128bit的数据
parameter DATA_MAX = APP_ADDR_MAX - APP_ADDR_MIN; //读写ddr3的最大数据量
//wire define
wire [15:0] wr_data ; //DDR3控制器模块写数据
wire [15:0] rd_data ; //DDR3控制器模块读数据
wire wr_en ; //DDR3控制器模块写使能
wire rd_en ; //DDR3控制器模块读使能
wire ddr_init_done ; //ddr3初始化完成信号
wire error_flag ; //ddr3读写错误标志
////*****************************************************
////** main code
////*****************************************************
//ddr3控制器顶层模块
ddr3_top u_ddr3_top(
.refclk_in (sys_clk ),
.rst_n (sys_rst_n ),
.app_addr_rd_min (APP_ADDR_MIN ),
.app_addr_rd_max (APP_ADDR_MAX ),
.rd_bust_len (BURST_LENGTH ),
.app_addr_wr_min (APP_ADDR_MIN ),
.app_addr_wr_max (APP_ADDR_MAX ),
.wr_bust_len (BURST_LENGTH ),
.wr_clk (sys_clk ),
.rd_clk (sys_clk ),
.datain_valid (wr_en ),
.datain (wr_data ),
.rdata_req (rd_en ),
.dataout (rd_data ),
.ddr_init_done (ddr_init_done ),
//DDR3接口
.pad_loop_in (pad_loop_in ),
.pad_loop_in_h (pad_loop_in_h ),
.pad_rstn_ch0 (pad_rstn_ch0 ),
.pad_ddr_clk_w (pad_ddr_clk_w ),
.pad_ddr_clkn_w (pad_ddr_clkn_w ),
.pad_csn_ch0 (pad_csn_ch0 ),
.pad_addr_ch0 (pad_addr_ch0 ),
.pad_dq_ch0 (pad_dq_ch0 ),
.pad_dqs_ch0 (pad_dqs_ch0 ),
.pad_dqsn_ch0 (pad_dqsn_ch0 ),
.pad_dm_rdqs_ch0 (pad_dm_rdqs_ch0 ),
.pad_cke_ch0 (pad_cke_ch0 ),
.pad_odt_ch0 (pad_odt_ch0 ),
.pad_rasn_ch0 (pad_rasn_ch0 ),
.pad_casn_ch0 (pad_casn_ch0 ),
.pad_wen_ch0 (pad_wen_ch0 ),
.pad_ba_ch0 (pad_ba_ch0 ),
.pad_loop_out (pad_loop_out ),
.pad_loop_out_h (pad_loop_out_h )
);
//ddr3测试数据模块
ddr_test u_ddr_test(
.clk_50m (sys_clk ), //时钟
.rst_n (sys_rst_n ), //复位,低有效
.wr_en (wr_en ), //写使能
.wr_data (wr_data ), //写数据
.rd_en (rd_en ), //读使能
.rd_data (rd_data ), //读数据
.data_max (DATA_MAX ), //读写ddr的最大数据量
.ddr3_init_done(ddr_init_done ), //ddr3初始化完成信号
.error_flag (error_flag ) //ddr3读写错误
);
//利用LED灯指示ddr3读写测试的结果及ddr3是否初始化完成
led_disp u_led_disp(
.clk_50m (sys_clk ),
.rst_n (sys_rst_n ),
.ddr3_init_done (ddr_init_done ),
.error_flag (error_flag ),
.led_error (led_error ),
.led_ddr_init_done (led_ddr_init_done)
);
endmodule
DDR读写实验的顶层模块代码如上所述,u_ddr3_top可以理解为两个部分构成:
1.DDR3接口。这部分是DDR3 PHY接口,物理上直接连接到FPGA的IO并与DDR内存颗粒连接。
2.ddr3控制器顶层模块。这部分信号可以与其他模块交互,如ddr3测试数据模块u_ddr_test,通过这部分交互信号,ddr3测试数据模块能够对DDR3进行读写操作。
3.LED指示灯模块u_led_disp十分简单,就是将ddr3测试数据模块成功或者失败的标志位传出与LED指示灯模块相连,作为LED指示灯模块的控制信号。
DDR3测试数据模块主要就是一些业务逻辑,通过DDR3顶层模块预留出来的接口,对其进行读写操作即可,总的来说难度不大。
比较复杂的是DDR3顶层模块ddr3_top,其包含了DDR IP核的调用,要想运用好该IP核还需要理解DDR的读写时序。下面给出了DDR3顶层模块代码。
module ddr3_top(
input refclk_in ,//外部参考时钟输入
input rst_n ,//外部复位输入
input [27:0] app_addr_rd_min ,//读ddr3的起始地址
input [27:0] app_addr_rd_max ,//读ddr3的结束地址
input [7:0] rd_bust_len ,//从ddr3中读数据时的突发长度
input [27:0] app_addr_wr_min ,//读ddr3的起始地址
input [27:0] app_addr_wr_max ,//读ddr3的结束地址
input [7:0] wr_bust_len ,//从ddr3中读数据时的突发长度
//用户
input wr_clk ,//wfifo写时钟
input rd_clk ,//rfifo读时钟
input datain_valid ,//数据有效使能信号
input [15:0] datain ,//有效数据
input rdata_req ,//请求数据输入
output [15:0] dataout ,//rfifo输出数据
output ddr_init_done ,//DDR初始化完成
input pad_loop_in ,
input pad_loop_in_h ,
output pad_rstn_ch0 ,
output pad_ddr_clk_w ,
output pad_ddr_clkn_w ,
output pad_csn_ch0 ,
output [15:0] pad_addr_ch0 ,
inout [16-1:0] pad_dq_ch0 ,
inout [16/8-1:0] pad_dqs_ch0 ,
inout [16/8-1:0] pad_dqsn_ch0 ,
output [16/8-1:0] pad_dm_rdqs_ch0 ,
output pad_cke_ch0 ,
output pad_odt_ch0 ,
output pad_rasn_ch0 ,
output pad_casn_ch0 ,
output pad_wen_ch0 ,
output [2:0] pad_ba_ch0 ,
output pad_loop_out ,
output pad_loop_out_h
);
//wire define
wire [32-1:0] axi_awaddr ;
wire [7:0] axi_awlen ;
wire [2:0] axi_awsize ;
wire [1:0] axi_awburst ;
wire axi_awlock ;
wire axi_awready ;
wire axi_awvalid ;
wire axi_awurgent ;
wire axi_awpoison ;
wire [128-1:0] axi_wdata ;
wire [16-1:0] axi_wstrb ;
wire axi_wvalid ;
wire axi_wready ;
wire axi_wlast ;
wire axi_bready ;
wire [32-1:0] axi_araddr ;
wire [7:0] axi_arlen ;
wire [2:0] axi_arsize ;
wire [1:0] axi_arburst ;
wire axi_arlock ;
wire axi_arpoison ;
wire axi_arurgent ;
wire axi_arready ;
wire axi_arvalid ;
wire [128-1:0] axi_rdata ;
wire axi_rlast ;
wire axi_rvalid ;
wire axi_rready ;
wire axi_clk ;
wire [10:0] wfifo_rcount ;//rfifo剩余数据计数
wire [10:0] rfifo_wcount ;//wfifo写进数据计数
wire wrfifo_en_ctrl ;//写FIFO数据读使能控制位
wire wfifo_rden ;//写FIFO数据读使能
wire pre_wfifo_rden ;//写FIFO数据预读使能
//*****************************************************
//** main code
//*****************************************************
//因为预读了一个数据所以读使能wfifo_rden要少一个周期通过wrfifo_en_ctrl控制
assign wfifo_rden = axi_wvalid && axi_wready && (~wrfifo_en_ctrl) ;
assign pre_wfifo_rden = axi_awvalid && axi_awready ;
//ddr3读写控制器模块
rw_ctrl_128bit u_rw_ctrl_128bit
(
.clk (axi_clk ),
.rst_n (rst_n ),
.ddr_init_done (ddr_init_done ),
.axi_awaddr (axi_awaddr ),
.axi_awlen (axi_awlen ),
.axi_awsize (axi_awsize ),
.axi_awburst (axi_awburst ),
.axi_awlock (axi_awlock ),
.axi_awready (axi_awready ),
.axi_awvalid (axi_awvalid ),
.axi_awurgent (axi_awurgent ),
.axi_awpoison (axi_awpoison ),
.axi_wstrb (axi_wstrb ),
.axi_wvalid (axi_wvalid ),
.axi_wready (axi_wready ),
.axi_wlast (axi_wlast ),
.axi_bready (axi_bready ),
.wrfifo_en_ctrl (wrfifo_en_ctrl ),
.axi_araddr (axi_araddr ),
.axi_arlen (axi_arlen ),
.axi_arsize (axi_arsize ),
.axi_arburst (axi_arburst ),
.axi_arlock (axi_arlock ),
.axi_arpoison (axi_arpoison ),
.axi_arurgent (axi_arurgent ),
.axi_arready (axi_arready ),
.axi_arvalid (axi_arvalid ),
.axi_rlast (axi_rlast ),
.axi_rvalid (axi_rvalid ),
.axi_rready (axi_rready ),
.wfifo_rcount (wfifo_rcount ),
.rfifo_wcount (rfifo_wcount ),
.app_addr_rd_min (app_addr_rd_min ),
.app_addr_rd_max (app_addr_rd_max ),
.rd_bust_len (rd_bust_len ),
.app_addr_wr_min (app_addr_wr_min ),
.app_addr_wr_max (app_addr_wr_max ),
.wr_bust_len (wr_bust_len )
);
//ddr3IP核模块
ddr3_ip u_ddr3_ip (
.pll_refclk_in (refclk_in ), // input
.top_rst_n (rst_n ), // input
.ddrc_rst (0 ), // input
.csysreq_ddrc (1'b1 ), // input
.csysack_ddrc ( ), // output
.cactive_ddrc ( ), // output
.pll_lock ( ), // output
.pll_aclk_0 (axi_clk ), // output
.pll_aclk_1 ( ), // output
.pll_aclk_2 ( ), // output
.ddrphy_rst_done ( ), // output
.ddrc_init_done (ddr_init_done ), // output
.pad_loop_in (pad_loop_in ), // input
.pad_loop_in_h (pad_loop_in_h ), // input
.pad_rstn_ch0 (pad_rstn_ch0 ), // output
.pad_ddr_clk_w (pad_ddr_clk_w ), // output
.pad_ddr_clkn_w (pad_ddr_clkn_w ), // output
.pad_csn_ch0 (pad_csn_ch0 ), // output
.pad_addr_ch0 (pad_addr_ch0 ), // output [15:0]
.pad_dq_ch0 (pad_dq_ch0 ), // inout [15:0]
.pad_dqs_ch0 (pad_dqs_ch0 ), // inout [1:0]
.pad_dqsn_ch0 (pad_dqsn_ch0 ), // inout [1:0]
.pad_dm_rdqs_ch0 (pad_dm_rdqs_ch0), // output [1:0]
.pad_cke_ch0 (pad_cke_ch0 ), // output
.pad_odt_ch0 (pad_odt_ch0 ), // output
.pad_rasn_ch0 (pad_rasn_ch0 ), // output
.pad_casn_ch0 (pad_casn_ch0 ), // output
.pad_wen_ch0 (pad_wen_ch0 ), // output
.pad_ba_ch0 (pad_ba_ch0 ), // output [2:0]
.pad_loop_out (pad_loop_out ), // output
.pad_loop_out_h (pad_loop_out_h ), // output
.areset_0 (0 ), // input
.aclk_0 (axi_clk ), // input
.awid_0 (0 ), // input [7:0]
.awaddr_0 (axi_awaddr ), // input [31:0]
.awlen_0 (axi_awlen ), // input [7:0]
.awsize_0 (axi_awsize ), // input [2:0]
.awburst_0 (axi_awburst ), // input [1:0]
.awlock_0 (axi_awlock ), // input
.awvalid_0 (axi_awvalid ), // input
.awready_0 (axi_awready ), // output
.awurgent_0 (axi_awurgent ), // input
.awpoison_0 (axi_awpoison ), // input
.wdata_0 (axi_wdata ), // input [127:0]
.wstrb_0 (axi_wstrb ), // input [15:0]
.wlast_0 (axi_wlast ), // input
.wvalid_0 (axi_wvalid ), // input
.wready_0 (axi_wready ), // output
.bid_0 ( ), // output [7:0]
.bresp_0 ( ), // output [1:0]
.bvalid_0 ( ), // output
.bready_0 (axi_bready ), // input
.arid_0 (0 ), // input [7:0]
.araddr_0 (axi_araddr ), // input [31:0]
.arlen_0 (axi_arlen ), // input [7:0]
.arsize_0 (axi_arsize ), // input [2:0]
.arburst_0 (axi_arburst ), // input [1:0]
.arlock_0 (axi_arlock ), // input
.arvalid_0 (axi_arvalid ), // input
.arready_0 (axi_arready ), // output
.arpoison_0 (axi_arpoison ), // input
.rid_0 ( ), // output [7:0]
.rdata_0 (axi_rdata ), // output [127:0]
.rresp_0 ( ), // output [1:0]
.rlast_0 (axi_rlast ), // output
.rvalid_0 (axi_rvalid ), // output
.rready_0 (axi_rready ), // input
.arurgent_0 (axi_arurgent ), // input
.csysreq_0 (1'b1 ), // input
.csysack_0 ( ), // output
.cactive_0 ( ) // output
);
//ddr3控制器fifo控制模块
ddr3_fifo_ctrl u_ddr3_fifo_ctrl (
.rst_n (rst_n && ddr_init_done ) , //复位
//输入源接口
.wr_clk (wr_clk ) , //写时钟
.rd_clk (rd_clk ) , //读时钟
.clk_100 (axi_clk ) , //用户时钟
.datain_valid (datain_valid ) , //数据有效使能信号
.datain (datain ) , //有效数据
.rfifo_din (axi_rdata ) , //用户读数据
.rdata_req (rdata_req ) , //请求像素点颜色数据输入
.rfifo_wren (axi_rvalid ) , //ddr3读出数据的有效使能
.wfifo_rden (wfifo_rden||pre_wfifo_rden) , //ddr3 写使能
//用户接口
.wfifo_rcount (wfifo_rcount ) , //rfifo剩余数据计数
.rfifo_wcount (rfifo_wcount ) , //wfifo写进数据计数
.wfifo_dout (axi_wdata ) , //用户写数据
.pic_data (dataout ) //rfifo输出数据
);
endmodule
ddr3控制器顶层模块主要完成ddr3读写控制器模块、FIFO控制模块和ddr3IP核的例化。
ddr3IP核模块一边与用户端进行交互,另一边对芯片进行操作,以实现数据的存储。
FIFO控制模块负责对输入和输出的数据进行时钟域的切换和位宽的转换。
ddr3读写控制器模块负责与ddr3IP核模块的命令和地址的交互,根据FIFO控制模块中fifo的剩余数据量来切换DDR3的读写命令和地址。这部分的理解大家可以参考DDR3规范。
下图是DDR3命令真值表:
DDR3各个命令都是由时序要求的,如下图的写时序要求:
大家对时序或者程序中的参数有不理解的,可以参考DDR3规范和本案例的DDR3颗粒规格数。
对于程序细节的理解,有一部分我也需要好好消化下,先把方法交给大家。
3.总结
本帖前半部分结合我的经验给出了DDR硬件设计方法,还有很多原理图的设计细节以及PCB布局布线要求,如FLY-BY走线等没法一一详尽;后面结合正点原子的代码,梳理了程序框架以及理解的方法,大家可以自行深入学习。工作闲暇我也需要对这部分内容的一些细节好好消化下。
针对实物仿真,大家手头如果有开发板,可以先从修改读写测试模块开始并观察现象,从能够对DDR用起来,再到细节的深入理解,这需要一个过程,毕竟DDR的时序和状态机跳转都有一定难度。
|