接上一篇文章(配置MIG IP过程):快速上手Xilinx DDR3 IP核(1)----MIG IP核的介绍及配置(Native接口)
在我心中,Xilinx是一家完美的公司(自动忽略vivado编译太慢),技术生态支持实在是做的太好了。Xilinx也知道我们不会用DDR3,所以提供了一个example design给你学习,怎么样?惊不惊喜?意不意外?(实际上,绝大多数的IP Xilinx都提供了example design~~)
那么接下来就一起学习下官方提供的参考设计呗。
右击生成的IP核(默认你已经生成了MIG IP核),选择open IP example design,选择好路径后就会生成一个新的工程mig_7series_0_ex。
打开工程mig_7series_0_ex,看下整个工程的结构----2个主要部分:1、MIG IP核;2、读写测试的数据生成模块
读写测试模块我们不展开讲(太多了,一时半会讲不完),我们直接通过波形来学习。
点击RUN SIMULATION,直接执行仿真(这里可能等几分钟(吃电脑配置)----强烈建议使用Modelsim仿真,速度快的不是一星半点)
仿真结果出来后,只保留一些比较有用的信号,并进行分组,如下:
找init_calib_complete拉高后(DDR3初始化完成)的一些局部时序图,如下图:
蓝线、和黄线处,app_cmd为0,app_en、app_rdy均为高,代表在执行写命令操作;同时app_wdf_data同地址一致,app_wdf_en、app_wdf_rdy均为高,代表在进行写数据操作
如下图:
很多个周期的app_rdy拉低,代表MIG没有准备好进行写数据命令,但此时app_wdf_data同地址一致,app_wdf_en、app_wdf_rdy均为高,代表在进行写数据操作。这正是前面章节提到了写数据提前与写命令,因为MIG种有一个FIFO,将要写入的数据存入FIFO,等到写命令有效时,再对FIFO进行读取。这样做的好处是可以很大程度上做到写命令和写数据分离。
如下图:
蓝线处,app_cmd为1,app_en、app_rdy均为高,地址app_addr为0x0000200,代表在执行读命令操作。经过若干个周期后,黄线处读取数据有效标志信号app_rd_data_valid拉高,代表成功读取到了一个数据,数据为0x0000200000020000002000000200,与前面写入的一致
通过上节对官方例程的学习,已经初步了解了DDR3的接口时序,接下来就自己编写一个简单的读写测试模块来对DDR3操作一番。(不当云玩家)
读写测试模块预期要实现的功能:
不妨先来回顾下DDR3提供的Native接口的时序。
DDR3 的读或者写都包含写命令操作,其中写操作命令(app_cmd)的值等于 0,读操作 app_cmd 的值 等于 1。首先来看写命令时序,如下图所示。首先检查 app_rdy,为高则表明此时 IP 核命令接收处于准备好 状态,可以接收用户命令,在当前时钟拉高 app_en,同时发送命令(app_cmd)和地址(app_addr),此时命令和地址被写入。
写数据的时序,如下图所示:
写数据的情况有3种:
结合上图,写时序总结如下:首先需要检查 app_wdf_rdy,该信号为高表明此时 IP 核数据接收处于准备完成状态,可以接收用户发过来的数据,在当前时钟拉高写使能(app_wdf_wren),给出写数据 (app_wdf_data)。这样加上发起的写命令操作就可以成功向 IP 核写数据。
接着来看读数据,如下图所示:
读时序比较简单,发出读命令后,用户只需等待数据有效信号(app_rd_data_valid)拉高,为高表明此 时数据总线上的数据是有效的返回数据。需要注意的是,在发出读命令后,有效读数据要晚若干周期才出现在数据总线上 (延迟的时间不定)。
本来仿真是不需要PLL模块的(直接写时钟激励就行),但是考虑到等下还需要上板测试,而我手头的开发板主频50M,MIG的工作时钟却是200M,所以需要PLL模块生成工作时钟。具体配置略。
在生成的example design中提供了DDR3的仿真模型,共两个文件:
1、DDR3仿真模型的头文件
2、DDR3仿真模型
找到这两个文件,把它们添加进我们自己的测试工程里。
读写测试模块生成对MIG IP核的控制时序,并使用一个状态机来实现循环写、读的过程。状态机如下:
前面说到,DDR3写数据的时候有3种模式,我们在测试的时候,固定使用“写命令与写数据发生在同一时钟周期”这一模式。这样一来,代码编写会简单很多,但相应的会牺牲一点点效率(不会造成多大的影响)。
完整的读写模块测试代码如下:
//**************************************************************************
// *** 名称 : 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
注释还是比较详细的,就不讲解了,只说几个需要注意的点。
顶层模块的设计十分简单:只要例化上述3个模块即可。如下:
//**********************************************************************************
// *** 名称 : ddr3_rw_top
// *** 作者 : 孤独的单刀
// *** 博客 : https://blog.csdn.net/wuzhikaidetb
// *** 日期 : 2021.12
// *** 描述 : 顶层模块----例化DDR3仿真模型与DDR3读写测试模块,对DDR3进行循环读写测试
//**********************************************************************************
//============================< 端口 >======================================
module ddr3_rw_top(
//时钟和复位 ---------------------------
input sys_clk , //系统时钟
input sys_rst_n , //复位,低有效
//DDR3相关 -----------------------------
inout [15:0] ddr3_dq , //DDR3 数据
inout [1:0] ddr3_dqs_n , //DDR3 dqs负
inout [1:0] ddr3_dqs_p , //DDR3 dqs正
output [13:0] ddr3_addr , //DDR3 地址
output [2:0] ddr3_ba , //DDR3 banck 选择
output ddr3_ras_n , //DDR3 行选择
output ddr3_cas_n , //DDR3 列选择
output ddr3_we_n , //DDR3 读写选择
output ddr3_reset_n , //DDR3 复位
output [0:0] ddr3_ck_p , //DDR3 时钟正
output [0:0] ddr3_ck_n , //DDR3 时钟负
output [0:0] ddr3_cke , //DDR3 时钟使能
output [0:0] ddr3_cs_n , //DDR3 片选
output [1:0] ddr3_dm , //DDR3_dm
output [0:0] ddr3_odt , //DDR3_odt
//标志相关------------------------------
output error_flag //错误指示信号
);
//============================< 信号定义 >======================================
//parameter define
parameter integer WR_LEN = 512 ; //读、写长度
parameter integer DATA_WIDTH = 128 ; //数据位宽,突发长度为8,16bit,共128bit
parameter integer ADDR_WIDTH = 28 ; //根据MIG例化而来
//wire define
wire ui_clk ; //用户时钟
wire [ADDR_WIDTH - 1:0] app_addr ; //DDR3 地址
wire [2:0] app_cmd ; //用户读写命令
wire app_en ; //MIG IP核使能
wire app_rdy ; //MIG IP核空闲
wire [DATA_WIDTH - 1:0] app_rd_data ; //用户读数据
wire app_rd_data_end ; //突发读当前时钟最后一个数据
wire app_rd_data_valid ; //读数据有效
wire [DATA_WIDTH - 1:0] app_wdf_data ; //用户写数据
wire app_wdf_end ; //突发写当前时钟最后一个数据
wire [15:0] app_wdf_mask ; //写数据屏蔽
wire app_wdf_rdy ; //写空闲
wire app_wdf_wren ; //DDR3 写使能
wire locked ; //锁相环频率稳定标志
wire clk_ref_i ; //DDR3参考时钟
wire sys_clk_i ; //MIG IP核输入时钟
wire clk_200 ; //200M时钟
wire ui_clk_sync_rst ; //用户复位信号
wire init_calib_complete ; //校准完成信号
//*****************************************************************************************
//** main code
//*****************************************************************************************
//============================< 例化DDR3读写测试模块 >======================================
ddr3_rw #(
.WR_LEN (WR_LEN ),
.DATA_WIDTH (DATA_WIDTH ),
.ADDR_WIDTH (ADDR_WIDTH )
)
u_ddr3_rw(
.ui_clk (ui_clk ),
.ui_clk_sync_rst (ui_clk_sync_rst ),
.init_calib_complete (init_calib_complete ),
.app_rdy (app_rdy ),
.app_wdf_rdy (app_wdf_rdy ),
.app_rd_data_valid (app_rd_data_valid ),
.app_rd_data (app_rd_data ),
.app_addr (app_addr ),
.app_en (app_en ),
.app_wdf_wren (app_wdf_wren ),
.app_wdf_end (app_wdf_end ),
.app_cmd (app_cmd ),
.app_wdf_data (app_wdf_data ),
.error_flag (error_flag )
);
//============================< 例化MIG IP核 >===============================================
mig_7series_0 u_mig_7series_0 (
//DDR3接口-------------------------------------------------
.ddr3_addr (ddr3_addr ),
.ddr3_ba (ddr3_ba ),
.ddr3_cas_n (ddr3_cas_n ),
.ddr3_ck_n (ddr3_ck_n ),
.ddr3_ck_p (ddr3_ck_p ),
.ddr3_cke (ddr3_cke ),
.ddr3_ras_n (ddr3_ras_n ),
.ddr3_reset_n (ddr3_reset_n ),
.ddr3_we_n (ddr3_we_n ),
.ddr3_dq (ddr3_dq ),
.ddr3_dqs_n (ddr3_dqs_n ),
.ddr3_dqs_p (ddr3_dqs_p ),
.ddr3_cs_n (ddr3_cs_n ),
.ddr3_dm (ddr3_dm ),
.ddr3_odt (ddr3_odt ),
//用户接口-------------------------------------------------
.app_addr (app_addr ),
.app_cmd (app_cmd ),
.app_en (app_en ),
.app_wdf_data (app_wdf_data ),
.app_wdf_end (app_wdf_end ),
.app_wdf_wren (app_wdf_wren ),
.app_rd_data (app_rd_data ),
.app_rd_data_end (app_rd_data_end ),
.app_rd_data_valid (app_rd_data_valid ),
.app_wdf_mask (31'b0 ),
.app_rdy (app_rdy ),
.app_wdf_rdy (app_wdf_rdy ),
.app_sr_req (1'b0 ),
.app_ref_req (1'b0 ),
.app_zq_req (1'b0 ),
.app_sr_active ( ),
.app_ref_ack ( ),
.app_zq_ack ( ),
//全局信号-------------------------------------------------
.ui_clk (ui_clk ),
.ui_clk_sync_rst (ui_clk_sync_rst ),
.init_calib_complete (init_calib_complete),
.sys_clk_i (clk_200 ),
.clk_ref_i (clk_200 ),
.sys_rst (sys_rst_n )
);
//============================< 例化PLL模块 >===============================================
clk_wiz_0 u_clk_wiz_0
(
.clk_out1 (clk_200 ), // output clk_out1
.reset (1'b0 ), // input resetn
.locked (locked ), // output locked
.clk_in1 (sys_clk ) // input clk_in1
);
endmodule
好的,Verilog代码写完了,接下来就跑跑仿真验证以下。
Testbench也很简单,只需要提供时钟、复位激励;例化读写测试模块及DDR3仿真模型就好了,如下:
//**************************************************************************
// *** 名称 : tb_ddr3_rw_top
// *** 作者 : 孤独的单刀
// *** 博客 : https://blog.csdn.net/wuzhikaidetb
// *** 日期 : 2021.12
// *** 描述 : 对DDR3进行循环读写测试
//**************************************************************************
`timescale 1ns/100ps
module tb_ddr3_rw_top();
//============================< 信号 >======================================
//时钟和复位 --------------------
reg sys_clk ; //系统时钟
reg sys_rst_n ; //系统复位
//DDR3相关 ----------------------
wire [15:0] ddr3_dq ; //DDR3 数据
wire [1:0] ddr3_dqs_n ; //DDR3 dqs负
wire [1:0] ddr3_dqs_p ; //DDR3 dqs正
wire [13:0] ddr3_addr ; //DDR3 地址
wire [2:0] ddr3_ba ; //DDR3 banck 选择
wire ddr3_ras_n ; //DDR3 行选择
wire ddr3_cas_n ; //DDR3 列选择
wire ddr3_we_n ; //DDR3 读写选择
wire ddr3_reset_n ; //DDR3 复位
wire [0:0] ddr3_ck_p ; //DDR3 时钟正
wire [0:0] ddr3_ck_n ; //DDR3 时钟负
wire [0:0] ddr3_cke ; //DDR3 时钟使能
wire [0:0] ddr3_cs_n ; //DDR3 片选
wire [1:0] ddr3_dm ; //DDR3_dm
wire [0:0] ddr3_odt ; //DDR3_odt
//标志相关-----------------------
wire error_flag ; //读、写错误标志
//============================< 测试条件设置 >===============================
//设置初始测试条件--------------------------------------------------------
initial begin
sys_clk = 1'b0 ; //初始时钟为0
sys_rst_n <= 1'b0 ; //初始复位
#50 //5个时钟周期后
sys_rst_n <= 1'b1 ; //拉高复位,系统进入工作状态
end
//设置时钟----------------------------------------------------------------
always #10 sys_clk = ~sys_clk; //系统时钟周期20ns
//============================< 被测试模块例化 >===============================
//例化DDR3读写测试-------------------
ddr3_rw_top ddr3_rw_top_inst(
.sys_clk (sys_clk ), //系统时钟
.sys_rst_n (sys_rst_n ), //复位,低有效
.ddr3_dq (ddr3_dq ), //DDR3 数据
.ddr3_dqs_n (ddr3_dqs_n ), //DDR3 dqs负
.ddr3_dqs_p (ddr3_dqs_p ), //DDR3 dqs正
.ddr3_addr (ddr3_addr ), //DDR3 地址
.ddr3_ba (ddr3_ba ), //DDR3 banck 选择
.ddr3_ras_n (ddr3_ras_n ), //DDR3 行选择
.ddr3_cas_n (ddr3_cas_n ), //DDR3 列选择
.ddr3_we_n (ddr3_we_n ), //DDR3 读写选择
.ddr3_reset_n (ddr3_reset_n ), //DDR3 复位
.ddr3_ck_p (ddr3_ck_p ), //DDR3 时钟正
.ddr3_ck_n (ddr3_ck_n ), //DDR3 时钟负
.ddr3_cke (ddr3_cke ), //DDR3 时钟使能
.ddr3_cs_n (ddr3_cs_n ), //DDR3 片选
.ddr3_dm (ddr3_dm ), //DDR3_dm
.ddr3_odt (ddr3_odt ), //DDR3_odt
.error_flag (error_flag ) //错误标志
);
//例化DDR3模型-----------------------
ddr3_model ddr3_model_inst
(
.rst_n (sys_rst_n ),
.ck (ddr3_ck_p ),
.ck_n (ddr3_ck_n ),
.cke (ddr3_cke ),
.cs_n (ddr3_cs_n ),
.ras_n (ddr3_ras_n ),
.cas_n (ddr3_cas_n ),
.we_n (ddr3_we_n ),
.dm_tdqs (ddr3_dm ),
.ba (ddr3_ba ),
.addr (ddr3_addr ),
.dq (ddr3_dq ),
.dqs (ddr3_dqs_p ),
.dqs_n (ddr3_dqs_n ),
.tdqs_n ( ), //NULL
.odt (ddr3_odt )
);
endmodule
仿真我们不用Vivado跑了(不是说慢,是真慢),改用Modelsim跑。Modelsim跑仿真速度快,来来回回的修改调试也方便。
废话少说,仿真结果如下:
上图中:MIG初始化完成信号init_calib_complete拉高,表示MIG控制器初始化完成。然后,进入开始写数据,再读数据。如此不停重复。
上图是:写数据过程的开头部分:
写入地址从0开始累加,每次累加8,因为突发长度为8;
写入数据从0开始累加,每次累加1;
当命令有效、写命令有效,且写指令为低(代表写)时,数据被写入。
上图是:写数据过程的结束部分:
写入地址从0开始累加,每次累加8,因为突发长度为8,到4088截止;
写入数据从0开始累加,每次累加1,到511截止,共写入512个数据;
当命令有效、写命令有效,且写指令为低(代表写)时,数据被写入。
上图是:读数据过程的开始部分:
读取地址从0开始累加,每次累加8,因为突发长度为8;
从发出读取指令到真正读到数据需要一定的时间;
当命令有效,且指令为高(代表读)时,数据被读出;
读出的数据与写入的数据一致(0-1-2-······)。
上图是:读数据过程的结束部分:
读取地址从0开始累加,每次累加8,因为突发长度为8,到4088;
从发出读取指令到真正读到数据需要一定的时间;
当命令有效,且指令为高(代表读)时,数据被读出;
读出的数据与写入的数据一致(0-1-2-······511)。
FPGA:XC7A35T FGG484-2
DDR3:NT5CB128M16CP-DI
Vivado:Vivado 2019.2
Modelsim:Modelsim SE-64 2020.4
文件:V1.0
编号:71
结果基本与仿真结果一致,写、读验证无误,读取数据一致。截几张图贴出来
初始化完成后进行的写操作:
读操作:
至此,读写模块验证成功。
使用modelsim仿真xilinx的IP核,主要需要解决仿真库的问题。不得不说modelsim是真的好用。
供参考:vivado与modelsim的联合仿真
没找到什么好办法,重装了新版本的modelsim(2020.4版本)。重装以后问题解决。
基本上是语法错误,请仔细检查。
供参考:Xilinx VIVADO 仿真时无法调用 ModelSim 失败的解决办法
下面三个保留的信号全接到0后解决(Xilinx都在手册告诉你这么做了,你又不听,哎······)