本章节将演示如何通过FPGA的MIG模块实现DDR3内存的读写,从而实现FPGA端的大规模数据缓存功能。
- 此章节内容适用于Smart Artix 的主板 ,如是本站其他板子请看对应板子目录
- 本文在 vivado2018.3版本上演示
备注:本文中的部分内容及代码使用和参考了黑金FPGA的教程(包括mem_burst.v 和 mem_test.v部分的代码及对应的说明部分),也在此说明。
在做FPGA项目的时候,我们经常会遇到需要将大量的临时数据进行暂存的情况,数据量较少的时候,我们可以使用片内BRAM的资源来进行暂存,但是片内BRAM资源一般都非常有限,这个时候我们就需要考虑将数据暂存到外部ram,sdram,或者ddr上了,ram和sdram的存储容量通常有限,如果要存储的数据比较多,就该ddr上场了。
一、实验内容:
本实验旨在通过FPGA的MIG模块实现DDR3内存的读写操作,从而实现FPGA端的大规模数据缓存功能。实验内容包括配置MIG模块与DDR3内存的连接,进行数据的读取与写入操作,并通过实验验证DDR3在FPGA系统中的数据存取效率与稳定性。(本节的侧重点放在如何通过MIG模块去调用DDR3芯片,至于更深的原理和细节大家有兴趣可以自行研究)
二、硬件介绍:
我们的Smart Artix 最小系统板上使用了一片Micron 的DDR3颗粒 MT41K256M16TW,DDR容量是256Mx16bit 即512MB。 位宽是16位的,与FPGA的BANK34相连。电路设计上已严格按照DDR3的设计规范做了等长控制。
三、工程创建:
工程创建的过程可以参考实验一中的内容,这里不详细描述了。基于Smart Artix 的FPGA实验一 用FPGA资源点亮一个LED(完整图文) (芯片型号选XC7A50TFGG484-2)
四、添加MIG模块
MIG IP 控制器是 Xilinx 为用户提供的一个 DDR 控制的 IP, 这样用户即使不了解 DDR 的控制和
读写时序也能通过 DDR 控制器方便的读写 DDR 存储器。7 系列的 DDR 控制器的解决方案如下所示:

- DDR3 控制器包含 3 部分:
- 用户接口模块(User interface Block),
- 存储器控制模块(MemoryController)
- DDR3 的物理接口(Physical Layer)
我们只需要学会调度使用MIG就能控制DDR3的数据读写了, 关于MIG的详细内容可以查看xilinx官方的文档 UG586。
1) 第一步,点击IP Catalog 打开模块选择器, 在里面的搜索栏输入MIG ,系统会自动跳出符合的 Memory Interface Generator (MIG 7 Series)选项,双击它

2) 这一页会显示工程的一些信息,我们直接点NEXT

3)选择Create Design 你也可以在这个页面改变名称,这里我保留默认的元件名

4) 第四步选择兼容芯片, 这里根据实际的需求来,这里我们默认不选,直接NEXT

5) 芯片类型 选择DDR3 然后直接下一步NEXT

6) Smart Artix 的Memory Part 选择我们的DDR3型号”MT41K256M16XX-107″, Data Width数据宽度选择16位。速度范围我们可以保留默认,也可以根据给出的范围进行调整, 这里我们改成2500ps 对应400MHz

7) 选择PLL输入时钟的频率为200Mhz, Controller Chip Select Pin 改成Disable (硬件电路上已经上拉使能了),其他设置保持默认不变。

8) System Clock 选择差分”No Buffer”,(因为sys_clk使用内部时钟所以使用no buffer)Reference Clock 因为开发板上没有提供单独的 DDR 参考时钟,这里Use System Clock。System Reset Polarity 选择 ACTIVE LOW 低电平复位。 这里一定要 开启芯片内部的Vref

9)BANK 的内部端接阻抗,这里为50 ohms,不用修改。

10) 选择Fixed Pin Out

11)在这个界面里设置DDR3的数据、地址和控制信号的FPGA管脚分配和IO电平。我们可以选择手动分配,也可以使用点击Read XDC/UCF 按键直接导入我们预先分配好的管脚。(DDR管脚对应文件:Smart_Artix_DDRPIN,记得要解压缩)

12)按下 Validate 对管脚设置进行检查, 如果没有错误的话 会显示Current Pinout is valid ,点OK 并点NEXT 继续

13)这里会提示一些其他信号的设置,这里不管,直接点击Next。

14)显示ddr3 IP配置的概况,可大致看一下,没有问题就点击Next

15)选择Accept, 点击Next。

16) 选择NEXT

17) 选择Generate 生成我们的 DDR控制模块

18)之后我们的工程里就多了一个 DDR控制器的IP

五、添加时钟模块
因为我们的DDR MIG模块需要一个200M的时钟来驱动,而我们板子上焊接的有源晶体是50MHZ,这里就需要用时钟管理模块MMCM来生成我们要的200Mhz频率。
1)第一步,点击IP Catalog 打开模块选择器, 在里面的搜索栏输入 CLOCKING ,系统会自动跳出符合的 Clocking Wizard选项,双击它

2)在弹出的窗口中我们将input Frequence 输入频率修改为板子上焊接的50M时钟, 右边改为单端输入

3)在output Clocks选项中 将clk_out1改成200m

4)将界面托到最下面,因为我们这里的要求并不高,所以把locked选项去除,这里保留reset功能,但是将reset 功能改成低电平复位(Active Low)最后点击ok生成模块

六 、添加DDR burst 模块
虽说我们在vivado中配置并生成好了mig模块,但是mig模块里留给用户部分的接口仍然过于复杂,读写数据时用的接口信号也比较多。这里我们我们增加一个men_burst模块来封装我们的ddr功能,让用户接口更为通用和简单。
通过mem_burst程序, 用户读写DDR3数据就变得简单。写入数据的时候,只要控制写请求信号wr_burst_req, 写长度wr_burst_len,写地址wr_burst_add和写数据wr_burst_data。同样,读数据的时候,只要控制读请求信号rd_burst_req, 读长度rd_burst_len,读地址rd_burst_add,读数据有效rd_burst_data_valid和读数据rd_burst_data。
下列mem_burst.v 出处 :黑金FPGA教程
module mem_burst
#(
parameter MEM_DATA_BITS = 64,
parameter ADDR_BITS = 24
)
(
input rst,
input mem_clk,
input rd_burst_req,
input wr_burst_req,
input[9:0] rd_burst_len,
input[9:0] wr_burst_len,
input[ADDR_BITS - 1:0] rd_burst_addr,
input[ADDR_BITS - 1:0] wr_burst_addr,
output rd_burst_data_valid,
output wr_burst_data_req,
output[MEM_DATA_BITS - 1:0] rd_burst_data,
input[MEM_DATA_BITS - 1:0] wr_burst_data,
output rd_burst_finish,
output wr_burst_finish,
output burst_finish,
///////////////////
output[ADDR_BITS-1:0] app_addr,
output[2:0] app_cmd,
output app_en,
output [MEM_DATA_BITS-1:0] app_wdf_data,
output app_wdf_end,
output [MEM_DATA_BITS/8-1:0] app_wdf_mask,
output app_wdf_wren,
input [MEM_DATA_BITS-1:0] app_rd_data,
input app_rd_data_end,
input app_rd_data_valid,
input app_rdy,
input app_wdf_rdy,
input ui_clk_sync_rst,
input init_calib_complete
);
assign app_wdf_mask = {MEM_DATA_BITS/8{1'b0}};
localparam IDLE = 3'd0;
localparam MEM_READ = 3'd1;
localparam MEM_READ_WAIT = 3'd2;
localparam MEM_WRITE = 3'd3;
localparam MEM_WRITE_WAIT = 3'd4;
localparam READ_END = 3'd5;
localparam WRITE_END = 3'd6;
localparam MEM_WRITE_FIRST_READ = 3'd7;
reg[2:0] state;
reg[9:0] rd_addr_cnt;
reg[9:0] rd_data_cnt;
reg[9:0] wr_addr_cnt;
reg[9:0] wr_data_cnt;
reg[2:0] app_cmd_r;
reg[ADDR_BITS-1:0] app_addr_r;
reg app_en_r;
reg app_wdf_end_r;
reg app_wdf_wren_r;
assign app_cmd = app_cmd_r;
assign app_addr = app_addr_r;
assign app_en = app_en_r;
assign app_wdf_end = app_wdf_end_r;
assign app_wdf_data = wr_burst_data;
assign app_wdf_wren = app_wdf_wren_r & app_wdf_rdy;
assign rd_burst_finish = (state == READ_END);
assign wr_burst_finish = (state == WRITE_END);
assign burst_finish = rd_burst_finish | wr_burst_finish;
assign rd_burst_data = app_rd_data;
assign rd_burst_data_valid = app_rd_data_valid;
assign wr_burst_data_req = (state == MEM_WRITE) & app_wdf_rdy ;
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
app_wdf_wren_r <= 1'b0;
end
else if(app_wdf_rdy)
app_wdf_wren_r <= wr_burst_data_req;
end
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
state <= IDLE;
app_cmd_r <= 3'b000;
app_addr_r <= 0;
app_en_r <= 1'b0;
rd_addr_cnt <= 0;
rd_data_cnt <= 0;
wr_addr_cnt <= 0;
wr_data_cnt <= 0;
app_wdf_end_r <= 1'b0;
end
else if(init_calib_complete === 1'b1)
begin
case(state)
IDLE:
begin
if(rd_burst_req)
begin
state <= MEM_READ;
app_cmd_r <= 3'b001;
app_addr_r <= {rd_burst_addr,3'd0};
app_en_r <= 1'b1;
end
else if(wr_burst_req)
begin
state <= MEM_WRITE;
app_cmd_r <= 3'b000;
app_addr_r <= {wr_burst_addr,3'd0};
app_en_r <= 1'b1;
wr_addr_cnt <= 0;
app_wdf_end_r <= 1'b1;
wr_data_cnt <= 0;
end
end
MEM_READ:
begin
if(app_rdy)
begin
app_addr_r <= app_addr_r + 8;
if(rd_addr_cnt == rd_burst_len - 1)
begin
state <= MEM_READ_WAIT;
rd_addr_cnt <= 0;
app_en_r <= 1'b0;
end
else
rd_addr_cnt <= rd_addr_cnt + 1;
end
if(app_rd_data_valid)
begin
if(rd_data_cnt == rd_burst_len - 1)
begin
rd_data_cnt <= 0;
state <= READ_END;
end
else
begin
rd_data_cnt <= rd_data_cnt + 1;
end
end
end
MEM_READ_WAIT:
begin
if(app_rd_data_valid)
begin
if(rd_data_cnt == rd_burst_len - 1)
begin
rd_data_cnt <= 0;
state <= READ_END;
end
else
begin
rd_data_cnt <= rd_data_cnt + 1;
end
end
end
MEM_WRITE_FIRST_READ:
begin
app_en_r <= 1'b1;
state <= MEM_WRITE;
wr_addr_cnt <= 0;
end
MEM_WRITE:
begin
if(app_rdy)
begin
app_addr_r <= app_addr_r + 'b1000;
if(wr_addr_cnt == wr_burst_len - 1)
begin
app_wdf_end_r <= 1'b0;
app_en_r <= 1'b0;
end
else
begin
wr_addr_cnt <= wr_addr_cnt + 1;
end
end
if(wr_burst_data_req)
begin
if(wr_data_cnt == wr_burst_len - 1)
begin
state <= MEM_WRITE_WAIT;
end
else
begin
wr_data_cnt <= wr_data_cnt + 1;
end
end
end
READ_END:
state <= IDLE;
MEM_WRITE_WAIT:
begin
if(app_rdy)
begin
app_addr_r <= app_addr_r + 'b1000;
if(wr_addr_cnt == wr_burst_len - 1)
begin
app_wdf_end_r <= 1'b0;
app_en_r <= 1'b0;
if(app_wdf_rdy)
state <= WRITE_END;
end
else
begin
wr_addr_cnt <= wr_addr_cnt + 1;
end
end
else if(~app_en_r & app_wdf_rdy)
state <= WRITE_END;
end
WRITE_END:
state <= IDLE;
default:
state <= IDLE;
endcase
end
end
endmodule
mem_burst.v程序的功能就是把外部的burst读请求和写请求转化成DDR3 IP用户接口的所需的信号和时序。下面分别是读和写的流程:
DDR Burst读:
当程序在IDLE状态接收到读请求(rd_burst_req为高)时, 会向DDR3 IP的用户接口发送第一个数据读命令(读命令、地址、命令有效)信号。并会进入MEM_READ状态, 在MEM_READ状态里,如果判断DDR3 IP的用户接口空闲的话,会发送剩余的数据读命令(地址增加),发送完成转到MEM_READ_WAIT状态。另外在这个MEM_READ状态里,还需要判断DDR3 IP从DDR3里读出的数据是否有效来统计读出的数据是否为读burst长度。在MEM_READ_WAIT状态里读取读burst长度的DDR3的数据。数据全部读出完成后进入READ_END状态,再返回IDLE状态。
DDR Burst写:
当程序在IDLE状态接收到写请求(wr_burst_req为高)时, 会向DDR3 IP的用户接口发送第一个数据写命令(写命令、地址、命令有效)信号。并会进入MEM_WRITE状态, 在MEM_WRITE状态里,如果判断DDR3 IP的用户接口空闲的话,会发送剩余的数据写命令(地址增加),同时在这个MEM_WRITE状态里,还会向DDR3 IP的数据FIFO里写入要写到DDR的数据。当写入到DDR3 IP的FIFO的数据长度长度为写burst长度时转到MEM_WRITE_WAIT状态。在MEM_WRITE_WAIT状态,判断DDR数据写命令是否发送完成,发送完成就进入WRITE_END状态,再返回IDLE状态。
这里需要注意的是,向DDR3 IP的用户接口发送写命令和写入DDR的数据是独立的,因为写入到DDR的数据是需要先存储到DDR3 IP的FIFO中。只要app_wdf_rdy有效,就可以往DDR3 IP里写入数据。写入到DDR3的数据需要提前准备好,这里有一个wr_burst_data_req信号来请求用户提前准备要写入的DDR3的数据。
七、添加 DDR3的测试程序
下列mem_test.v 出处 :黑金FPGA教程
module mem_test
#(
parameter MEM_DATA_BITS = 64,
parameter ADDR_BITS = 24
)
(
input rst,
input mem_clk,
output reg rd_burst_req,
output reg wr_burst_req,
output reg[9:0] rd_burst_len,
output reg[9:0] wr_burst_len,
output reg[ADDR_BITS - 1:0] rd_burst_addr,
output reg[ADDR_BITS - 1:0] wr_burst_addr,
input rd_burst_data_valid,
input wr_burst_data_req,
input[MEM_DATA_BITS - 1:0] rd_burst_data,
output[MEM_DATA_BITS - 1:0] wr_burst_data,
input rd_burst_finish,
input wr_burst_finish,
output reg error
);
localparam IDLE = 3'd0;
localparam MEM_READ = 3'd1;
localparam MEM_WRITE = 3'd2;
reg[2:0] state;
reg[7:0] wr_cnt;
reg[MEM_DATA_BITS - 1:0] wr_burst_data_reg;
assign wr_burst_data = wr_burst_data_reg;
reg[7:0] rd_cnt;
always@(posedge mem_clk or posedge rst)
begin
if(rst)
error <= 1'b0;
else
if(error==0)begin
error <= (state == MEM_READ) && rd_burst_data_valid && (rd_burst_data != {(MEM_DATA_BITS/8){rd_cnt}});
end
end
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
wr_burst_data_reg <= {MEM_DATA_BITS{1'b0}};
wr_cnt <= 8'd0;
end
else if(state == MEM_WRITE)
begin
if(wr_burst_data_req)
begin
wr_burst_data_reg <= {(MEM_DATA_BITS/8){wr_cnt}};
wr_cnt <= wr_cnt + 8'd1;
end
else if(wr_burst_finish)
wr_cnt <= 8'd0;
end
end
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
rd_cnt <= 8'd0;
end
else if(state == MEM_READ)
begin
if(rd_burst_data_valid)
begin
rd_cnt <= rd_cnt + 8'd1;
end
else if(rd_burst_finish)
rd_cnt <= 8'd0;
end
else
rd_cnt <= 8'd0;
end
always@(posedge mem_clk or posedge rst)
begin
if(rst)
begin
state <= IDLE;
wr_burst_req <= 1'b0;
rd_burst_req <= 1'b0;
rd_burst_len <= 10'd128;
wr_burst_len <= 10'd128;
rd_burst_addr <= 0;
wr_burst_addr <= 0;
end
else
begin
case(state)
IDLE:
begin
state <= MEM_WRITE;
wr_burst_req <= 1'b1;
wr_burst_len <= 10'd128;
end
MEM_WRITE:
begin
if(wr_burst_finish)
begin
state <= MEM_READ;
wr_burst_req <= 1'b0;
rd_burst_req <= 1'b1;
rd_burst_len <= 10'd128;
rd_burst_addr <= wr_burst_addr;
end
end
MEM_READ:
begin
if(rd_burst_finish)
begin
state <= MEM_WRITE;
wr_burst_req <= 1'b1;
wr_burst_len <= 10'd128;
rd_burst_req <= 1'b0;
wr_burst_addr <= wr_burst_addr + 128;
end
end
default:
state <= IDLE;
endcase
end
end
endmodule
mem_test.v测试程序里主要实现ddr的burst读和burst写的功能, 程序里产生读写请求信号, 地址和测试数据并校验读和写的数据是否正确, 这里burst的数据长度是128。如果DDR的读写数据错误(写入的数据和读出的数据不一致),error信号会输出高电平。
八、添加顶层模块top.v
1) 添加top.v顶层模块
下列top.v 出处 :黑金FPGA教程, 修改其中的地址位从28位修改成29位,以支持256Mx16bit(512MB)的DDR
`timescale 1ps/1ps
module top
(
// Inouts
inout [15:0] ddr3_dq,
inout [1:0] ddr3_dqs_n,
inout [1:0] ddr3_dqs_p,
// Outputs
output [14: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 [0:0] ddr3_ck_p,
output [0:0] ddr3_ck_n,
output [0:0] ddr3_cke,
output [1:0] ddr3_dm,
output [0:0] ddr3_odt,
//system clock 50Mhz on board
input sys_clk,
output init_calib_complete,
output error,
input rst_n
);
localparam nCK_PER_CLK = 4;
localparam DQ_WIDTH = 16;
localparam ADDR_WIDTH = 29;
localparam DATA_WIDTH = 16;
localparam PAYLOAD_WIDTH = 16;
localparam APP_DATA_WIDTH = 2 * nCK_PER_CLK * PAYLOAD_WIDTH;
localparam APP_MASK_WIDTH = APP_DATA_WIDTH / 8;
// Wire declarations
wire [ADDR_WIDTH-1:0] app_addr;
wire [2:0] app_cmd;
wire app_en;
wire app_rdy;
wire [APP_DATA_WIDTH-1:0] app_rd_data;
wire app_rd_data_end;
wire app_rd_data_valid;
wire [APP_DATA_WIDTH-1:0] app_wdf_data;
wire app_wdf_end;
wire [APP_MASK_WIDTH-1:0] app_wdf_mask;
wire app_wdf_rdy;
wire app_sr_active;
wire app_ref_ack;
wire app_zq_ack;
wire app_wdf_wren;
wire clk;
wire rst;
wire clk_200Mhz;
clk_wiz_0 clk_refm0
(
// Clock out ports
.clk_out1(clk_200Mhz), // output clk_out1
// Status and control signals
.resetn(rst_n), // input reset
// Clock in ports
.clk_in1(sys_clk) // input clk_in1
);
mig_7series_0 u_ddr3
(
// Memory interface ports
.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_we_n (ddr3_we_n),
.ddr3_dq (ddr3_dq),
.ddr3_dqs_n (ddr3_dqs_n),
.ddr3_dqs_p (ddr3_dqs_p),
.ddr3_reset_n (ddr3_reset_n),
.init_calib_complete (init_calib_complete),
.ddr3_dm (ddr3_dm),
.ddr3_odt (ddr3_odt),
// Application interface ports
.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_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_sr_active),
.app_ref_ack (app_ref_ack),
.app_zq_ack (app_zq_ack),
.ui_clk (clk),
.ui_clk_sync_rst (rst),
.app_wdf_mask (app_wdf_mask),
// System Clock Ports
.sys_clk_i (clk_200Mhz),
.sys_rst (rst_n)
);
// End of User Design top instance
wire wr_burst_data_req;
wire wr_burst_finish;
wire rd_burst_finish;
wire rd_burst_req;
wire wr_burst_req;
wire[9:0] rd_burst_len;
wire[9:0] wr_burst_len;
wire[28:0] rd_burst_addr;
wire[28:0] wr_burst_addr;
wire rd_burst_data_valid;
wire[48* 8 - 1 : 0] rd_burst_data;
wire[48* 8 - 1 : 0] wr_burst_data;
mem_burst
#(
.MEM_DATA_BITS(APP_DATA_WIDTH),
.ADDR_BITS(ADDR_WIDTH)
)
mem_burst_m0
(
.rst(rst),
.mem_clk(clk),
.rd_burst_req(rd_burst_req),
.wr_burst_req(wr_burst_req),
.rd_burst_len(rd_burst_len),
.wr_burst_len(wr_burst_len),
.rd_burst_addr(rd_burst_addr),
.wr_burst_addr(wr_burst_addr),
.rd_burst_data_valid(rd_burst_data_valid),
.wr_burst_data_req(wr_burst_data_req),
.rd_burst_data(rd_burst_data),
.wr_burst_data(wr_burst_data),
.rd_burst_finish(rd_burst_finish),
.wr_burst_finish(wr_burst_finish),
.burst_finish(),
///////////////////
.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_mask(app_wdf_mask),
.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_rdy(app_rdy),
.app_wdf_rdy(app_wdf_rdy),
.ui_clk_sync_rst(),
.init_calib_complete(init_calib_complete)
);
mem_test
#(
.MEM_DATA_BITS(APP_DATA_WIDTH),
.ADDR_BITS(ADDR_WIDTH)
)
mem_test_m0
(
.rst(rst),
.mem_clk(clk),
.rd_burst_req(rd_burst_req),
.wr_burst_req(wr_burst_req),
.rd_burst_len(rd_burst_len),
.wr_burst_len(wr_burst_len),
.rd_burst_addr(rd_burst_addr),
.wr_burst_addr(wr_burst_addr),
.rd_burst_data_valid(rd_burst_data_valid),
.wr_burst_data_req(wr_burst_data_req),
.rd_burst_data(rd_burst_data),
.wr_burst_data(wr_burst_data),
.rd_burst_finish(rd_burst_finish),
.wr_burst_finish(wr_burst_finish),
.error(error)
);
wire probe0;
wire probe1;
wire probe2;
wire probe3;
wire probe4;
wire probe5;
wire probe6;
wire probe7;
wire [127 : 0] probe8;
wire [127 : 0] probe9;
wire [27 : 0] probe10;
ila_0 u_ila_0(
.clk(clk),
.probe0(probe0),
.probe1(probe1),
.probe2(probe2),
.probe3(probe3),
.probe4(probe4),
.probe5(probe5),
.probe6(probe6),
.probe7(probe7),
.probe8(probe8),
.probe9(probe9),
.probe10(probe10)
);
assign probe0 = rd_burst_req;
assign probe1 = wr_burst_req;
assign probe2 = rd_burst_data_valid;
assign probe3 = wr_burst_data_req;
assign probe4 = rd_burst_finish;
assign probe5 = wr_burst_finish;
assign probe6 = error;
assign probe7 = init_calib_complete;
assign probe8 = wr_burst_data[127:0];
assign probe9 = rd_burst_data[127:0];
assign probe10 = app_addr[27:0];
endmodule
top.v程序里例化mem_burst模块、mem_test模块和DDR3 IP模块, 时钟模块,另外实例化了一个ila_0 IP,用来观察DDR Burst读和Burst写的数据、地址和控制信号。
2. 为了方便观察结果,所以我们还需要向工程中再添加一个ila的IP。
在vivado软件中,打开IP核目录(IP Catalog),搜索ILA,如下图所示
Probe的数量为11,就是11个采样通道,采样的数据深度为4096, 采样深度越深,采样的数据量越大,但会消耗更多的FPGA逻辑资源。

再对前面8个通道设置数据宽度,这里因为我们每个通道只是采样一个bit的信号,所以数据宽度都为1。

再设置后面8~10通道的数据宽度,8通道和9通道设置成128数据宽度,用来采样DDR 读写数据, 10通道设置成28,用来采样DDR的地址。点击OK完成。

3)添加XDC管脚约束
在约束中把error信号连接到开发板的LED1上,如果error为高电平,说明数据出错,如果error为低电平,LED1熄灭,说明DDR3读写正常。另外设置系统的复位信号rst_n和FPGA时钟输入sys_clk,详细约束如下:
# Clock Constraints
set_property PACKAGE_PIN Y18 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_clk]
# Define the clock period for 50 MHz
create_clock -period 20.000 -name sys_clk -waveform {0.000 10.000} [get_ports sys_clk]
# Reset Constraints (active-low reset)
set_property PACKAGE_PIN T20 [get_ports rst_n]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
# LED Constraints
set_property PACKAGE_PIN R17 [get_ports error]
set_property IOSTANDARD LVCMOS33 [get_ports error]
set_property PACKAGE_PIN P16 [get_ports init_calib_complete]
set_property IOSTANDARD LVCMOS33 [get_ports init_calib_complete]
九、下载和测试
最后我们编译程序生成和综合工程,然后下载bit文件到FPGA,观察结果
下载后vivado会自动打开ila的波形调试界面,点击“Run trigger for ILA core” 按钮运行ila采集数据。可以从中看到write 和read 的结果

- 另外我们也可以检查开发板上的LED1和LED2是否点亮,来判断系统的工作状态:
- LED1亮说明error信号为高证明读写出问题了,如果LED1熄灭说明DDR3读写数据正确。
- LED2亮说明DDR初始化成功
- 如果工作都是正常的 那么LED1应该是一直熄灭的状态,LED2是点亮的状态
这里简单说明一下几个黑金教程中没有提到的点:
1)DDR数据位宽与MIG的传输关系
- 我们硬件的DDR数据位宽是16-bit,但由于MIG(的工作方式,实际每个时钟周期传输的数据是:
nCK_PER_CLK = 4(每时钟周期传输4次数据)- 双边沿采样:每个时钟周期两次采样(上升沿和下降沿)
- 16-bit实际位宽(DDR接口的硬件宽度)
- 所以,最终每个时钟周期实际传输的数据位宽是:
4 (nCK_PER_CLK) * 2 (双边沿) * 16 bits = 128 bits(即16个字节)。
2)地址的突发起始地址要求
- 地址步长为8字节:
- 因为每个时钟周期传输16个字节,所以突发读写操作的地址最好是8字节对齐的。
- 如果地址不是8字节对齐,会导致:
- 读操作需要做额外的首字节处理。
- 写操作需要做字节掩码处理。
- 设计上的要求:
- 确保突发地址起始为8字节的倍数,且每次地址递增8字节。
- 黑金代码中的底层burst地址设计即按照8字节对齐,即每次加8字节。
3)mem_test模块的读写测试数据
- mem_test模块中,每个时钟周期里16个字节传输的内容都是相同的,即 从计数器的00-01到7e,刚好128个数据,对应的数据如下:
- 00000000000000000000000000000000
- 01010101010101010101010101010101
- ………
- 7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e
4)数据写入与错误检测
- 每次写入128字节数据后,mem_test模块会立即将这些数据读回并进行验证。
- 如果发现数据错误,
error信号会被拉高。- 注意:一旦
error信号被拉高,后续的任何数据都会被认为有问题,且error信号会保持高电平,这和黑金原代码有所不同。
- 注意:一旦
5)突发地址递增与循环访问内存空间
- 每完成一次128字节的写读校验后,mem_test模块会将突发的起始地址增加128字节,然后开始下一轮的读写校验。
- 这样不断遍历整个DDR的内存地址空间,实现对DDR芯片的全范围读写测试。
6)mem_test中的地址与MIG地址的关系
- 在mem_test中使用的地址与最终送给MIG的地址并不相同。具体来说:
- 在这个代码中MIG地址的定义是:
app_addr_r <= {rd_burst_addr, 3'd0}; - 这里的
3'd0表示3个最低有效位为0。这个偏移量是为了对齐8字节数据传输,确保每次数据操作都以8为步长。 - 这和我们提到的128字节传输是相关的:每次操作的地址按8对齐,保证数据在MIG传输时能够正确对接。(8×16=128)
- 在这个代码中MIG地址的定义是:
十、VIVADO官方的DDR demo
其实VIVADO 官方有准备一个DDR EXAMPLE demo, 这个demo会比本工程更复杂一些,大家如果感兴趣可以自行打开查看,
在我们工程中右键mig的IP模块,在弹出的下拉菜单里选择Open IP Example Design即可打开新的参考工程,软件已经自动编写了ddr的测试程序、管脚定义文件xdc文件和仿真程序。大家感兴趣的可以自行研究。
下列是本次实验的完整工程:
- 本文的完整工程下载:16_DDR3_TEST Smart Artix 50T)
- VIVADO的版本:2018.3
- 工程创建目录:E:\Smart_Artix\4_Code\16_DDR3_TEST
