Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

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送入数据成功,接下来应该重新执行写命令,数据不能再往里面送了!!!也就是图中红框的部分,在第8194时刻必须把ddr3_app_wdf_wren拉低,停止送入数据,不然的话,这种情况下就会连续往FIFO里面送入三个0x0008,导致后面的地址0x0000010(16号地址),0x0000018(24号地址)都被写入0x0008这个数据!!!
如果不懂的话,是非常容易犯这个错误的。
写8号地址的命令发了两次都失败了,第三次终于发送成功了,ddr3_app_wdf_rdy为1。在8196时刻可以发送新的写命令了,向FIFO里面写入新的数据。
Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

连续读内存也是类似的。如下图所示是一种错误的做法。只要ddr3_app_en为1,就一直在发命令,ddr3_app_addr一直为0就发了一大串读0号地址的命令。

Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

那改成发一个读命令,等到数据读出来后(ddr3_app_rd_data_valid=1),再发下一个命令,这样就能读出正确的结果,但是抓了波形图一看,中间的等待时间很长,每读一个地址都要等待很久,非常浪费时间。

Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

于是我们可以边发命令,边接收数据。一开始就从0地址开始一直往后发命令:0,8,16,24,36,48……
当ddr3_app_rd_data_valid=1时,我们已经发到0xb0(176)地址了,这个时候0号地址的数据才读出来,于是一个一个接收,如下图所示。

Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

这样连续读数据就会比刚才快很多很多。值得注意的是,接收完最后一个数据后,ddr3_app_rd_data_valid会变为0,表明数据已接收完毕。

Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

我们来看看最终的代码:

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一直为高,每次都是写的突发的最后一组数据。

程序运行后,串口的输出结果如下:

Xilinx MIG核读写DDR3内存,连续读写内存的正确方法(时序)及代码

串口输出的是读出来的内存数据,可以看到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)

上一篇:虚拟机环境docker下简单搭建单节点elasticsearch+kibana


下一篇:DDR3自学笔记