关于Xilinx 2020.1新建工程时配置MIG核的完整步骤,请参阅:https://blog.csdn.net/ZLK1214/article/details/111349678
MIG核里面有两个通道:命令通道和数据通道。这两个通道是相互独立的,互不影响。
命令通道:要发送的命令由ddr3_app_cmd指定(0号命令是写内存,1号命令是读内存),ddr3_app_en拉高就开始发送命令。
数据通道:要发送的数据由ddr3_app_wdf_data指定,ddr3_app_wdf_wren拉高就开始往FIFO里面送入数据。
MIG里面是严格一个时钟(ddr3_ui_clk)发送一个命令(或写一个数据)。ddr3_app_en如果一直拉高,那么就一直重复发送命令。拉高了多少个时钟周期,就发送多少条命令。同样,ddr3_app_wdf_wren如果一直拉高,就一直重复往FIFO里面写入数据。编写程序的时候要特别注意这一点。
如下图所示是MIG连续写DDR3内存时的时序。ILA的采样周期刚好就是ddr3_ui_clk(100MHz),横向时间单位为10ns。
从红竖线(8192时刻)开始写内存,发送写命令(ddr3_app_cmd=0且ddr3_app_en=1),写的内存地址是ddr3_app_addr=0,同时往FIFO里面送入要写的数据(ddr3_app_wdf_wren=1,wdf的全称为write data FIFO)。第8193时刻,读到ddr3_app_rdy为1说明写命令发送成功,读到ddr3_app_wdf_rdy=1说明数据成功送入FIFO。这两位同时为1表示写内存成功。
第8193时刻又发出了写8号地址的写命令,往FIFO里面塞入数据0x0008。然而,第8194时刻读到ddr3_app_rdy为0,说明写命令发送失败了!!同时读到ddr3_app_wdf_rdy为1,说明往FIFO里面送入数据成功了。这个时候就要注意了,写命令发送失败但FIFO送入数据成功,接下来写命令应该重新发送,但数据不能再往FIFO里面送了!!!也就是图中红框的部分,在第8194时刻必须把ddr3_app_wdf_wren拉低,停止送入数据,不然的话,这种情况下就会连续往FIFO里面送入三个0x0008,导致后面的地址0x0000010(16号地址),0x0000018(24号地址)都被写入0x0008这个数据!!!
如果不懂的话,是非常容易犯这个错误的。
写8号地址的命令发了两次都失败了,第三次终于发送成功了,ddr3_app_rdy为1。在8196时刻可以发送新的写命令了,向FIFO里面写入新的数据。
连续读内存也是类似的。如下图所示是一种错误的做法。只要ddr3_app_en为1,就一直在发命令,ddr3_app_addr一直为0就发了一大串读0号地址的命令。
那改成发一个读命令,等到数据读出来后(ddr3_app_rd_data_valid=1),再发下一个命令,这样就能读出正确的结果,但是抓了波形图一看,中间的等待时间很长,每读一个地址都要等待很久,非常浪费时间。
于是我们可以边发命令,边接收数据。一开始就从0地址开始一直往后发命令:0,8,16,24,36,48……
当ddr3_app_rd_data_valid=1时,我们已经发到0xb0(176)地址了,这个时候0号地址的数据才读出来,于是一个一个接收,如下图所示。
这样连续读数据就会比刚才快很多很多。值得注意的是,接收完最后一个数据后,ddr3_app_rd_data_valid会变为0,表明数据已接收完毕。
我们来看看最终的代码:
这里程序有一个失误,就是ddr3_ui_clk_sync_rst是一个同步复位信号,不应该是异步复位。代码里面写成了always @(posedge ddr3_ui_clk, posedge ddr3_ui_clk_sync_rst) begin,把ddr3_ui_clk_sync_rst用成了一个异步的复位信号,这是不对的。应该把always敏感列表里面的posedge ddr3_ui_clk_sync_rst去掉,写成always @(posedge ddr3_ui_clk) begin才对。不过笔者在实际运行时并没有发现有什么不对的地方。
module main(
input clock, // 50MHz外部晶振
// DDR3引脚
inout [15:0] ddr3_dq,
inout [1:0] ddr3_dqs_n,
inout [1:0] ddr3_dqs_p,
output [13: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 ddr3_ck_p,
output ddr3_ck_n,
output ddr3_cke,
output ddr3_cs_n,
output [1:0] ddr3_dm,
output ddr3_odt,
input [3:0] keys,
output [3:0] leds, // 4个LED灯
output uart_tx // 串口发送引脚
);
parameter SYSCLK = 50000000;
wire nrst;
Reset #(SYSCLK) reset(clock, !keys[0], nrst);
wire clock200;
wire locked;
clk_wiz_0 clk_wiz_0(
.reset(!nrst),
.clk_in1(clock), // 输入50MHz时钟
.clk_out1(clock200), // 输出200MHz时钟
.locked(locked) // 该信号表示输出时钟是否已稳定
);
reg [27:0] ddr3_app_addr;
reg [2:0] ddr3_app_cmd;
reg ddr3_app_en;
reg [127:0] ddr3_app_wdf_data; // 因为burst=8, data_width=16, 所以wdf_data的宽度为8*16=128
reg ddr3_app_wdf_end;
reg ddr3_app_wdf_wren;
wire [127:0] ddr3_app_rd_data; // 这个也是128位
wire ddr3_app_rd_data_end;
wire ddr3_app_rd_data_valid;
wire ddr3_app_rdy;
wire ddr3_app_wdf_rdy;
wire ddr3_app_sr_active;
wire ddr3_app_ref_ack;
wire ddr3_app_zq_ack;
wire ddr3_ui_clk;
wire ddr3_ui_clk_sync_rst;
wire ddr3_init_calib_complete;
wire [11:0] ddr3_device_temp;
mig_7series_0 mig_7series_0(
.ddr3_dq(ddr3_dq),
.ddr3_dqs_n(ddr3_dqs_n),
.ddr3_dqs_p(ddr3_dqs_p),
.ddr3_addr(ddr3_addr),
.ddr3_ba(ddr3_ba),
.ddr3_ras_n(ddr3_ras_n),
.ddr3_cas_n(ddr3_cas_n),
.ddr3_we_n(ddr3_we_n),
.ddr3_reset_n(ddr3_reset_n),
.ddr3_ck_p(ddr3_ck_p), // DDR3内存时钟输出: 400MHz
.ddr3_ck_n(ddr3_ck_n),
.ddr3_cke(ddr3_cke),
.ddr3_cs_n(ddr3_cs_n),
.ddr3_dm(ddr3_dm),
.ddr3_odt(ddr3_odt),
.sys_clk_i(clock200), // 系统时钟输入: 200MHz
.clk_ref_i(clock200), // 参考时钟输入: 200MHz
.app_addr(ddr3_app_addr),
.app_cmd(ddr3_app_cmd),
.app_en(ddr3_app_en),
.app_wdf_data(ddr3_app_wdf_data),
.app_wdf_end(ddr3_app_wdf_end),
.app_wdf_mask(16'h0), // 8突发*每个数据2字节=16字节, 所以mask有16位
.app_wdf_wren(ddr3_app_wdf_wren),
.app_rd_data(ddr3_app_rd_data),
.app_rd_data_end(ddr3_app_rd_data_end),
.app_rd_data_valid(ddr3_app_rd_data_valid),
.app_rdy(ddr3_app_rdy),
.app_wdf_rdy(ddr3_app_wdf_rdy),
.app_sr_req(1'b0),
.app_ref_req(1'b0),
.app_zq_req(1'b0),
.app_sr_active(ddr3_app_sr_active),
.app_ref_ack(ddr3_app_ref_ack),
.app_zq_ack(ddr3_app_zq_ack),
.ui_clk(ddr3_ui_clk), // 用户时钟输出: 因为选的是4:1, 所以ddr3_ck_p:ddr3_ui_clk=4:1, ddr3_ui_clk是100MHz
.ui_clk_sync_rst(ddr3_ui_clk_sync_rst), // 用户程序复位输出
.init_calib_complete(ddr3_init_calib_complete),
.device_temp(ddr3_device_temp),
.sys_rst(locked) // 复位输入: 当倍频器时钟未稳定时, 使MIG处于复位状态
);
// FPGA内部的分布式RAM
// Depth: 112, Data Width: 128, Memory Type: Single Port RAM
reg [6:0] dist_mem_a;
reg [127:0] dist_mem_d;
reg dist_mem_we; // 写使能
wire [127:0] dist_mem_spo;
dist_mem_gen_0 dist_mem_gen_0(
.clk(ddr3_ui_clk),
.a(dist_mem_a),
.d(dist_mem_d),
.spo(dist_mem_spo),
.we(dist_mem_we)
);
// 串口单字节发送
wire uart_tx_request;
wire [7:0] uart_tx_data;
wire uart_tx_ready;
wire uart_sent;
UARTTransmitter #(SYSCLK, 115200) uart_transmitter(clock, locked, uart_tx, uart_tx_request, uart_tx_data, uart_tx_ready, uart_sent);
// 串口多字节组合发送
reg uart_bytearray_tx_mode;
reg [159:0] uart_bytearray_tx_data;
reg uart_bytearray_tx_request;
reg [7:0] uart_bytearray_tx_size;
wire uart_bytearray_tx_ready;
wire uart_bytearray_sent;
ByteArrayTransmitter #(20) uart_bytearray_transmitter(clock, locked, uart_tx_request, uart_tx_data, uart_tx_ready, uart_sent,
uart_bytearray_tx_mode, uart_bytearray_tx_request, uart_bytearray_tx_data, uart_bytearray_tx_size, uart_bytearray_tx_ready, uart_bytearray_sent);
reg [3:0] i;
reg uart_state;
assign leds = i;
always @(posedge ddr3_ui_clk, posedge ddr3_ui_clk_sync_rst) begin
if (ddr3_ui_clk_sync_rst) begin
ddr3_app_en <= 0;
ddr3_app_wdf_wren <= 0;
uart_bytearray_tx_request <= 0;
i <= 0;
uart_state <= 0;
end
else if (!uart_state) begin
if (uart_bytearray_tx_ready) begin
// 串口数据未开始发送
if (!uart_bytearray_tx_request) begin
// 串口空闲
case (i)
0, 1: begin
// 写数据测试
if (ddr3_app_rdy && ddr3_app_wdf_rdy) begin
// 送入新数据
if (i == 0) begin
// [第1批数据]
// MIG配置选择了4:1关系, 所以wdf_end信号应该一直为高
// 且wdf_data的宽度为16字节 (一次性写8个地址)
ddr3_app_cmd <= 0; // WRITE
ddr3_app_addr <= 0; // 写的地址
ddr3_app_en <= 1; // 写命令使能
ddr3_app_wdf_data <= 128'h01234567_89abcdef_5555aaaa_bbbb0000; // 写的数据
ddr3_app_wdf_wren <= 1; // 数据送入FIFO使能
ddr3_app_wdf_end <= 1; // 是本突发最后一组数据
// 这是因为, ddr3_ui_clk的频率是100MHz, ddr3_ck_p的频率是400MHz, DDR内存是上升沿和下降沿都要写数据
// 一个ddr3_ui_clk周期内, ddr3_ck_p的上升沿和下降沿一共出现了8次
// 每出现一次, 就要往ddr3_dq[15:0]上送入2个字节的数据
// 因此, 每个ddr3_ui_clk时钟周期, 刚好要写8*2=16个字节的数据
i <= 1;
end
else if (ddr3_app_addr < 100 * 8) begin
// [第2~101批数据]
ddr3_app_addr <= ddr3_app_addr + 8;
ddr3_app_en <= 1;
ddr3_app_wdf_data[127:16] <= 112'h76543210_fedcba98_bbbb6666_cccc;
ddr3_app_wdf_data[15:0] <= ddr3_app_addr[15:0] + 8;
ddr3_app_wdf_wren <= 1;
end
else begin
// 没有数据要写了, 写数据结束
ddr3_app_en <= 0;
ddr3_app_wdf_wren <= 0;
ddr3_app_wdf_end <= 0;
i <= 2;
end
end
else if (!ddr3_app_rdy && ddr3_app_wdf_rdy) begin
// ddr3_app_rdy=0, 是写命令没有发送成功
// ddr3_app_wdf_rdy=1, 是数据已成功送入FIFO
// 数据已送入FIFO, 但写命令发送失败, 必须撤销数据送入FIFO的请求, 保留写命令请求
// 不能再往FIFO里面送入数据了, 否则多送入的数据会被写入到后续的地址上
ddr3_app_wdf_wren <= 0;
end
else if (ddr3_app_rdy && !ddr3_app_wdf_rdy)
ddr3_app_en <= 0; // 写命令发送成功了, 但数据没有成功送入FIFO, 则需要撤销写命令, 保留数据送入FIFO的请求
end
2, 3: begin
// 读数据测试
// 边发读命令, 边接收数据, 只要ddr3_app_rd_data_valid=1就可以接收数据, 这样读起来才快
// 而不是发一个命令, 收到数据后再发下一个命令
if (ddr3_app_rdy) begin
if (i == 2) begin
// 发送第1条读命令
ddr3_app_addr <= 0;
ddr3_app_cmd <= 1; // READ
ddr3_app_en <= 1;
i <= 3;
end
else if (ddr3_app_addr < 100 * 8)
ddr3_app_addr <= ddr3_app_addr + 8; // 发送第2~101条读命令
else
ddr3_app_en <= 0; // 读命令发送结束
// addr=0*8是第1条命令
// addr=1*8是第2条命令
// addr=100*8是第101条命令
end
if (ddr3_app_rd_data_valid) begin
// 数据读出来了, 将读到的16字节数据暂存到分布式RAM
if (!dist_mem_we)
dist_mem_a <= 0; // 第1个数据
else if (dist_mem_a < 100) begin
dist_mem_a <= dist_mem_a + 1'b1; // 第2~101个数据
if (dist_mem_a + 1'b1 == 100) // dist_mem_a的现态+1(即dist_mem_a的次态)==100
i <= 4; // 最后一个数据读完, 进入串口发送模式
end
dist_mem_d <= ddr3_app_rd_data;
dist_mem_we <= 1;
end
end
4: begin
// 将分布式RAM中暂存的数据通过串口发送出来
// 注意: 把地址赋给dist_mem_a后, 要下一个时钟周期才能从dist_mem_spo取出数据
if (dist_mem_we) begin
dist_mem_we <= 0;
dist_mem_a <= 0;
end
else if (dist_mem_a <= 100) begin
uart_bytearray_tx_request <= 1;
uart_bytearray_tx_mode <= 0;
uart_bytearray_tx_data <= {dist_mem_a, 3'b0, dist_mem_spo};
uart_bytearray_tx_size <= 20;
dist_mem_a <= dist_mem_a + 1'b1;
end
else
i <= 5;
end
endcase
end
end
else
uart_state <= 1; // 串口数据已开始发送
end
else begin
if (uart_bytearray_tx_ready && uart_bytearray_tx_request) begin
// 串口数据已发送
uart_bytearray_tx_request <= 0; // 关闭发送请求
uart_state <= 0;
end
end
end
ila_0 ila_0(
.clk(ddr3_ui_clk),
.probe0(ddr3_ui_clk_sync_rst),
.probe1(ddr3_app_addr), // [27:0]
.probe2(ddr3_app_cmd), // [2:0]
.probe3(ddr3_app_en),
.probe4(ddr3_app_wdf_data[15:0]), // [15:0]
.probe5(ddr3_app_wdf_end),
.probe6(ddr3_app_wdf_wren),
.probe7(ddr3_app_rd_data[15:0]), // [15:0]
.probe8(ddr3_app_rd_data_end),
.probe9(ddr3_app_rd_data_valid),
.probe10(ddr3_app_rdy),
.probe11(ddr3_app_wdf_rdy),
.probe12(ddr3_app_sr_active),
.probe13(ddr3_app_ref_ack),
.probe14(ddr3_app_zq_ack),
.probe15(ddr3_init_calib_complete),
.probe16(ddr3_device_temp) // [11:0]
);
endmodule
我们选用的DDR3内存型号为MT41K128M16,内存容量为256MB。FPGA是米联客XC7A35TFGG484-2的开发板,开发板上接的晶振是50MHz,由Clock Wizard倍频到200MHz后(clock200)用作MIG的系统时钟(sys_clk_i)和参考时钟(clk_ref_i),由MIG再次倍频到400MHz后(ddr3_ck_p和ddr3_ck_n)驱动DDR3内存,分频到100MHz后(ddr3_ui_clk)拿给程序使用,ddr3_ui_clk_sync_rst用作程序的复位信号。ILA抓取信号也是用的ddr3_ui_clk这个时钟。
内存的位宽为16位,一次读写2个字节,时钟的上升沿和下降沿都要读写数据。100MHz的每个时钟周期,DDR3内存的400MHz时钟共出现了4对上升/下降沿,写了8次数据,每次2个字节,所以每个100MHz的时钟周期(10ns),要写16字节。
每10ns写16字节的话,1μs要写1600字节,1ms要写1600000字节,1秒钟要写1600000000字节。也就是说,理论上这片内存的读写速率是1.49GB/s。
ddr3_app_wdf_data和ddr3_app_rd_data都是128位(16字节)。ILA抓信号的时候,为了节约BRAM,只抓data的末16位。
一次性要写8个地址,MIG核配置的突发数也是8,所以,写内存的时候ddr3_app_wdf_end一直为高,每次都是写的突发的最后一组数据。
程序运行后,串口的输出结果如下:
串口输出的是读出来的内存数据,可以看到DDR3内存读写是完全正确的。
引脚配置文件pins.xdc内容如下:
set_property PACKAGE_PIN V4 [get_ports clock]
set_property IOSTANDARD LVCMOS15 [get_ports clock]
set_property PACKAGE_PIN R17 [get_ports uart_tx]
set_property IOSTANDARD LVCMOS33 [get_ports uart_tx]
set_property PACKAGE_PIN D22 [get_ports {leds[3]}]
set_property PACKAGE_PIN E22 [get_ports {leds[2]}]
set_property PACKAGE_PIN D21 [get_ports {leds[1]}]
set_property PACKAGE_PIN E21 [get_ports {leds[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds[0]}]
set_property PACKAGE_PIN R14 [get_ports {keys[3]}]
set_property PACKAGE_PIN P14 [get_ports {keys[2]}]
set_property PACKAGE_PIN N14 [get_ports {keys[1]}]
set_property PACKAGE_PIN N13 [get_ports {keys[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {keys[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {keys[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {keys[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {keys[0]}]
DDR3内存的引脚已经在MIG的GUI配置界面配置了,所以我们不需要再在pins.xdc里面配置了。
配置DDR3内存引脚是单独一个xdc文件,是在MIG核的文件夹里面:sources_1\ip\mig_7series_0\mig_7series_0\user_design\constraints\mig_7series_0.xdc
Vivado工程下载地址:
(自己新建的,没有用MIG核的example工程)
https://pan.baidu.com/s/1KrmD7qbhHhRX7BFalfsn-A(提取码:b9x9)
实际上,写内存也可以边发命令边发数据。write data FIFO(WDF)里面最多可以放16个数据。用16个时钟周期把16个数据放进去,直到ddr3_app_wdf_rdy为0就说明把FIFO占满了,然后再来慢慢地发写命令,就可以达到最快的写速度。
MIG时序要求的是数据可以到的比写命令早,也可以滞后,但最晚只能比写命令晚一个周期。
接下来,我们可以用FPGA将DDR3模拟成一个W25Q256存储器,用杜邦线和STM32F103C8小开发板连接起来,做一个U盘,读写DDR3整个256MB的空间,进行全片测试:
https://blog.csdn.net/ZLK1214/article/details/111416038