快速上手DDR读写例程——DDR接口专栏(三)

本文转载自: FpgaHome微信公众号

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,共128bit
parameter 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,其他时候命令值为0
assign 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) begin
if(~rst_n)
cur_state <= IDLE;
else
cur_state <= next_state;
end

always @(*) begin
if(~rst_n)
next_state = IDLE;
else
case(cur_state)
IDLE:
if(init_calib_complete) //MIG IP核初始化完成
next_state = WRITE;
else
next_state = IDLE;
WRITE:
if(wr_last) //写入最后一个数据
next_state = WAIT;
else
next_state = WRITE;
WAIT:
next_state = READ;
READ:
if(rd_addr_last) //写入最后一个读地址,数据读出需要时间
next_state = IDLE;
else
next_state = READ;
default:;
endcase
end

always @(posedge ui_clk or negedge rst_n) begin
if(~rst_n) begin
app_wdf_data <= 0;
wr_addr_cnt <= 0;
rd_addr_cnt <= 0;
app_addr <= 0;
end
else
case(cur_state)
IDLE:begin
app_wdf_data <= 0;
wr_addr_cnt <= 0;
rd_addr_cnt <= 0;
app_addr <= 0;
end
WRITE:begin
if(wr_proc)begin //写条件满足
app_wdf_data <= app_wdf_data + 1; //写数据自加
wr_addr_cnt <= wr_addr_cnt + 1; //写地址自加
app_addr <= app_addr + 8; //DDR3 地址加8
end
else begin //写条件不满足,保持当前值
app_wdf_data <= app_wdf_data;
wr_addr_cnt <= wr_addr_cnt;
app_addr <= app_addr;
end
end
WAIT:begin
rd_addr_cnt <= 0; //读地址复位
app_addr <= 0; //DDR3读从地址0开始
end
READ:begin //读到设定的地址长度
if(app_rdy)begin //若MIG已经准备好,则开始读
rd_addr_cnt <= rd_addr_cnt + 1'd1;//用户地址每次加一
app_addr <= app_addr + 8; //DDR3地址加8
end
else begin //若MIG没准备好,则保持原值
rd_addr_cnt <= rd_addr_cnt;
app_addr <= app_addr;
end
end
default:begin
app_wdf_data <= 0;
wr_addr_cnt <= 0;
rd_addr_cnt <= 0;
app_addr <= 0;
end
endcase
end
//==========================================================================
//== 其他
//==========================================================================
//读信号有效,且读出的数不是写入的数时,将错误标志位拉高
assign error = (app_rd_data_valid && (rd_cnt!=app_rd_data));

//寄存状态标志位
always @(posedge ui_clk or negedge rst_n) begin
if(~rst_n)
error_flag <= 0;
else if(error)
error_flag <= 1;
end

//对DDR3实际读数据个数编号计数
always @(posedge ui_clk or negedge rst_n) begin
if(~rst_n)
rd_cnt <= 0;
//若计数到读写长度,且读有效,地址计数器则置0
else if(app_rd_data_valid && rd_cnt == WR_LEN - 1)
rd_cnt <= 0;
else if (app_rd_data_valid ) //读有效情况下每个时钟+1
rd_cnt <= rd_cnt + 1;
end

endmodule

5. 下期预告

本文介绍了如何使用DDR IP核的Native接口对DDR进行数据读写。在DDR专栏的下期文章里,我们将介绍如何使用DDR IP核的AXI4接口,并给出一种更加简便方便的读写DDR方法。如果觉得我们原创或引用的文章写的还不错,帮忙点赞和推荐吧,谢谢您的关注。

---------------------------------------------
参考文献:
[1] Xilinx, NavDoc, Zynq-7000 SoC and 7 Series Devices Memory Interface Solutions v4.2.
[2] FPGA技术实战,CSDN博客,《DDR IP核详解及读写测试》https://blog.csdn.net/gslscyx/article/details/130694959
[3] 孤独的单刀,CSDN博客,《MIG IP核的官方例程与读写测试模块(Native接口)》https://blog.csdn.net/wuzhikaidetb/article/details/121646652

最新文章

最新文章