
1. 前言
本文将向大家介绍如何使用DDR IP核的Native接口来对DDR进行读写操作。
2. DDR IP核接口介绍
要想把DDR3 IP核使用起来,必先需要了解下该IP核有哪些接口。DDR3 IP核接口图如下所示。注:图中展示的为DDR IP的Native接口,除了Native接口,该IP核还支持AXI4接口。

图中黄色的区域为“用户接口(UI)”,是DDR IP核对外的读写接口。绿色的框是用户通过代码写的逻辑电路,用户逻辑直接操作“用户接口”,实现对DDR数据的读写。右边蓝色的框是FPGA与DDR颗粒之间的物理接口。
下表列出了用户接口的定义清单,其中被黄色标注出来的几个信号是用户需要重点操作的信号。


-
app_addr:当前读写请求地址;
-
app_cmd:当前读写请求命令,读命令为3’b001,写命令为3’b000;
-
app_en:高有效,使能app_cmd、app_addr、app_sz和app_hi_pri信号;
-
app_rdy:该信号指示用户接口(UI)是否可以接收命令。当app_en使能时,如果该信号无效,则app_cmd、app_addr必须保持,等待app_rdy信号有效时再释放;
-
app_rd_data:用户接口读回数据;
-
app_rd_data_end:高有效,指示当前app_rd_data为最后一个数据,该信号仅当app_rd_data_valid为高时有效;
-
app_rd_data_valid:高有效,指示app_rd_data数据有效;
-
app_wdf_end:高有效,指示当前app_wdf_data为最后一个数据;
-
app_wdf_mask:提供app_wdf_data屏蔽码;
-
app_wdf_rdy:指示UI可以接收写数据写入;
-
app_wdf_wren:高有效,指示app_wdf_data数据有效;
-
app_wdf_data:用户待写入DDR的数据。
以上接口功能描述的翻译仅供参考,详细的还是参考上面两张官方的表格。
3. 用户接口的读写时序
操作DDR的UI接口时,要特别注意app_rdy信号的状态。如下图所示,app_cmd、app_addr和app_en分别给了3次写addr0地址的指令。但指令只有在app_rdy为高时才被接受,即图中只有前两次写指令写给了DDR IP核。

3.1 写数据至DDR
在本文讲述读写DDR时序时,均采用的是4:1模式,即FPGA的用户逻辑采用时钟频率为DDR工作频率的四分之一,该设置需要在建立DDR IP时进行设置,如不了解,可以参考上一篇文章《MIG IP核的使用——DDR接口专栏(二)》。
往DDR UI接口写数据时,时序如下图所示:

图中上半部给出了写控制指令的操作时序图,下半部分别有3个虚线框,给出了3种写数据总线的操作时序图。方案1为作者推荐的方法,即写数据操作和写指令操作对齐。方案2意思为写数据操作可以提前写指令操作一个时钟周期。方案3表明,写数据操作最多可以落后写指令操作两个时钟周期。

上图给了一个实际的写数据例子。红色框为写指令的操作,总共向8个地址(0x0a00~0x0a38)进行写操作。由于第1个时刻到第6个时刻app_rdy一直为高,所以往地址0x0a00~0x0a28写指令立即写入了DDR IP核的FIFO中。但由于第7个时刻app_rdy突然拉低,往地址0x0a30写数据的指令没有成功写入DDR IP核中,因此必须等待。此时app_addr、app_en、app_cmd这些信号都必须保持,直到app_rdy再度拉高,该指令才会被成功写入给DDR。
蓝色框为写具体的DDR数据操作,由于app_wdf_rdy一直为高,因此UI接口上一次性将8个数据都写入到DDR IP核的数据缓存FIFO中。
显然上面实例是采用方案3的写数据方式,但作者还是推荐初学者采用方案1的方式写数据。即判断app_rdy和app_wdf_rdy都为高时,再同时写入指令和数据。
3.2 读数据
从DDR UI接口读数据时,时序如下图所示:

图中上半部给出了读控制指令的操作时序图,下半部分为读出的数据结果。从读指令被UI接口接收后到数据被读出来的延时时间是随机的,没有具体对应关系。
4. 读写DDR例程代码
话不多说,上读写DDR例程代码吧。本文引用了CSDN博主“孤独的单刀”编写的代码。这段代码非常好的向大家展示了UI接口的使用方法。
代码功能描述:(1)等待DDR初始化成功;(2)往DDR的地址连续写入了1024个数据;(3)从DDR中读出刚写入相同地址段的数据,并进行比对。
//**************************************************************************// *** 名称 : ddr3_rw// *** 作者 : 孤独的单刀// *** 博客 : https://blog.csdn.net/wuzhikaidetb// *** 日期 : 2021.12// *** 描述 : 对DDR3进行循环读写//**************************************************************************//============================< 端口 >======================================module ddr3_rw #(parameter integer WR_LEN = 1024 , //读、写长度parameter integer DATA_WIDTH = 128 , //数据位宽,突发长度为8,16bit,共128bitparameter integer ADDR_WIDTH = 28 //根据MIG例化而来)(//DDR3相关 ------------------------------------------------------input ui_clk , //用户时钟input ui_clk_sync_rst , //复位,高有效input init_calib_complete , //DDR3初始化完成//DDR3相关 ------------------------------------------------------input app_rdy , //MIG 命令接收准备好标致input app_wdf_rdy , //MIG数据接收准备好input app_rd_data_valid , //读数据有效input [DATA_WIDTH - 1:0] app_rd_data , //用户读数据output reg [ADDR_WIDTH - 1:0] app_addr , //DDR3地址output app_en , //MIG IP发送命令使能output app_wdf_wren , //用户写数据使能output app_wdf_end , //突发写当前时钟最后一个数据output [2:0] app_cmd , //MIG IP核操作命令,读或者写output reg [DATA_WIDTH - 1:0] app_wdf_data , //用户写数据//指示 ----------------------------------------------------------output reg error_flag //读写错误标志);//============================< 信号定义 >======================================//测试状态机-----------------------------------------localparam IDLE = 4'b0001 ; //空闲状态localparam WRITE = 4'b0010 ; //写状态localparam WAIT = 4'b0100 ; //读到写过度等待localparam READ = 4'b1000 ; //读状态//reg define ----------------------------------------reg [3:0] cur_state ; //三段式状态机现态reg [3:0] next_state ; //三段式状态机次态reg [ADDR_WIDTH - 1:0] rd_addr_cnt ; //用户读地址计数reg [ADDR_WIDTH - 1:0] wr_addr_cnt ; //用户写地址计数reg [ADDR_WIDTH - 1:0] rd_cnt ; //实际读地址标记//wire define ---------------------------------------wire error ; //读写错误标记wire rst_n ; //复位,低有效wire wr_proc ; //拉高表示写过程进行wire wr_last ; //拉高表示写入最后一个数据wire rd_addr_last ; //拉高表示是最后一个读地址//*********************************************************************************************//** main code//**********************************************************************************************//==========================================================================//== 信号赋值//==========================================================================assign rst_n = ~ui_clk_sync_rst;//当MIG准备好后,用户同步准备好assign app_en = app_rdy && ((cur_state == WRITE && app_wdf_rdy) || cur_state == READ);//写指令,命令接收和数据接收都准备好,此时拉高写使能assign app_wdf_wren = (cur_state == WRITE) && wr_proc;//由于DDR3芯片时钟和用户时钟的分频选择4:1,突发长度为8,故两个信号相同assign app_wdf_end = app_wdf_wren;assign app_cmd = (cur_state == READ) ? 3'd1 :3'd0; //处于读的时候命令值为1,其他时候命令值为0assign wr_proc = ~app_cmd && app_rdy && app_wdf_rdy; //拉高表示写过程进行//处于写使能且是最后一个数据assign wr_last = app_wdf_wren && (wr_addr_cnt == WR_LEN - 1) ;//处于读指令、读有效且是最后一个数据assign rd_addr_last = (rd_addr_cnt == WR_LEN - 1) && app_rdy && app_cmd;//==========================================================================//== 状态机//==========================================================================always @(posedge ui_clk or negedge rst_n) beginif(~rst_n)cur_state <= IDLE;elsecur_state <= next_state;endalways @(*) beginif(~rst_n)next_state = IDLE;elsecase(cur_state)IDLE:if(init_calib_complete) //MIG IP核初始化完成next_state = WRITE;elsenext_state = IDLE;WRITE:if(wr_last) //写入最后一个数据next_state = WAIT;elsenext_state = WRITE;WAIT:next_state = READ;READ:if(rd_addr_last) //写入最后一个读地址,数据读出需要时间next_state = IDLE;elsenext_state = READ;default:;endcaseendalways @(posedge ui_clk or negedge rst_n) beginif(~rst_n) beginapp_wdf_data <= 0;wr_addr_cnt <= 0;rd_addr_cnt <= 0;app_addr <= 0;endelsecase(cur_state)IDLE:beginapp_wdf_data <= 0;wr_addr_cnt <= 0;rd_addr_cnt <= 0;app_addr <= 0;endWRITE:beginif(wr_proc)begin //写条件满足app_wdf_data <= app_wdf_data + 1; //写数据自加wr_addr_cnt <= wr_addr_cnt + 1; //写地址自加app_addr <= app_addr + 8; //DDR3 地址加8endelse begin //写条件不满足,保持当前值app_wdf_data <= app_wdf_data;wr_addr_cnt <= wr_addr_cnt;app_addr <= app_addr;endendWAIT:beginrd_addr_cnt <= 0; //读地址复位app_addr <= 0; //DDR3读从地址0开始endREAD:begin //读到设定的地址长度if(app_rdy)begin //若MIG已经准备好,则开始读rd_addr_cnt <= rd_addr_cnt + 1'd1;//用户地址每次加一app_addr <= app_addr + 8; //DDR3地址加8endelse begin //若MIG没准备好,则保持原值rd_addr_cnt <= rd_addr_cnt;app_addr <= app_addr;endenddefault:beginapp_wdf_data <= 0;wr_addr_cnt <= 0;rd_addr_cnt <= 0;app_addr <= 0;endendcaseend//==========================================================================//== 其他//==========================================================================//读信号有效,且读出的数不是写入的数时,将错误标志位拉高assign error = (app_rd_data_valid && (rd_cnt!=app_rd_data));//寄存状态标志位always @(posedge ui_clk or negedge rst_n) beginif(~rst_n)error_flag <= 0;else if(error)error_flag <= 1;end//对DDR3实际读数据个数编号计数always @(posedge ui_clk or negedge rst_n) beginif(~rst_n)rd_cnt <= 0;//若计数到读写长度,且读有效,地址计数器则置0else if(app_rd_data_valid && rd_cnt == WR_LEN - 1)rd_cnt <= 0;else if (app_rd_data_valid ) //读有效情况下每个时钟+1rd_cnt <= rd_cnt + 1;endendmodule