浅析Xilinx家DDR控制器MIG的使用

微笑、不失礼 提交于 2020-02-20 08:04:52

FPGA的片上存储资源bram简单好用,时序清晰,要不是总容量往往就几十Mb谁愿意用DDR呀······
害,言归正传,因为设计需要存储1477x1800x3 双精度浮点复数这样的大号矩阵,所以只能放到DDR上去进行读写。之前在网上找了好多资料,但发现都没有一个很完整的教程教你怎么使用DDR控制器IP核MIG(Memory Interface Generator),所以写了这篇文章主要希望能帮初学者快速上手MIG的使用以实现DDR读写。
介绍MIG之前,我觉得有必要先对DDR做一个介绍,DDR SDRAM(Double Data Rate Synchronous Dynamic Random Access Memory,实际上还分为DDR SDRAM,DDR2 SDRAM,DDR3 SDRAM,DDR4 SDRAM,主要是数据预取prefetch和工作频率的不同,感兴趣的大家可以自己查),搭眼一看,这玩意本质上不就是数字集成电路里学的DRAM嘛(电容存储,会漏电,时不时需要刷新blablabla······),而double data rate说的是他在clock的上升沿和下降沿都会进行数据读写,设想如果用户逻辑侧的时钟频率和DDR的工作频率之比为1:4的话,用户侧的一个clk, 那么DDR实际上进行了4*2(上下沿)=8次读写操作。
在这里插入图片描述
DDR3的内部是如上图的存储阵列组成,数据存储在单元格中,在检索你想要的数据时,先指定一个行(Row),再指定一个列(Column),我们就可以准确地找到所需要的单元格,这就是内存芯片寻址的基本原理。对于内存,这个单元格可称为存储单元,常见存储单元位宽有4bit,8bit,16bit,那么这个表格(存储阵列)就是逻辑 Bank(Logical Bank),而一颗DDR芯片上有若干个bank,一般内存芯片厂家在芯片上是标明容量的,我们可以看芯片上的标识知道,这个芯片有几个逻辑BANK,每个逻辑bank的位宽是多少,每个逻辑BANK内有多少单元格(CELL),eg.比如64MB和128MB内存条常用的**64Mbit的芯片(注意不是Mb,1byte=8bit,所以除以8才是Mb)**就有如下三种结构形式:
①16 M x 4 (4 M x 4 x 4 banks) [16M X 4]
②8 M x 8 (2 M x 8 x 4 banks) [8M X 8]
③4 M x 16 (1 M x 16 x 4 banks) [4M X 16]
即一颗DDR芯片的单元格数(逻辑bank数 X 每个逻辑bank的单元格数)X 每个单元格的位宽(bit)。
16bit位宽的芯片意味着你给内存一个地址,内存会给你一个16bit的数据到数据线上,但实际中我们的数据往往不止16bit,例如双精度浮点数(64bit),那么就需要用4块DDR芯片拼接成一个64位宽的DDR,只是这4块DDR共用一个地址线,每个取出的16bit数据再拼成一个64bit的数据,那么就相当于每个地址可以存一个64位宽的数据,而这样一个64位宽的数据集合就是一个物理bank,也经常叫Rank。大家可以参考这篇文章说的很详细:

64bit数据存储形式
接下来说说MIG核是干什么的,放一张DDR工作的状态图:
在这里插入图片描述
DDR上电后需要控制其初始化,ZQ校准,激活bank,而后读写等一系列过程,这其中还要配置模式寄存器,控制DDR refresh、precharge,考虑怎么样将8/16bit位宽的DDR芯片拼接成更大位宽等等问题。这对于用户侧是十分不友好的。所以就需要DDR控制器提供一个用户友好的接口,而MIG核做的就是这件事情
在这里插入图片描述
左边是用户侧读写逻辑,经由MIG核生成DDR需要的信号控制读写。所以,我们只需要把关注点放在MIG核的用户侧接口信号,从官方文档里可以找到接口说明。
在这里插入图片描述
在这里插入图片描述
需要重点关注的信号用红框标注出来了,根据英文也能猜出大概是干嘛的,接下来就可以生成IP核了,这里不得不说一下大家FPGA最好选用Xilinx的亲儿子,之前用的一款adm-pcie 7v3,参考资料少,问题多,而且DDR的管脚还要自己去配,属实折腾人······这次用的是ultrascale+系列的亲儿子kcu116,简直不要太舒服哈哈。在这里插入图片描述
在这里插入图片描述
这里说一下系统时钟,因为我的板子时钟源有一个300MHz的差分时钟,所以就刚好用它作为MIG的输入时钟,实际上MIG会利用分频倍频将这个差分时钟变成我们想要的DDR工作频率,第二张图里的memory interface speed指的就是DDR的工作频率,而frequency ratio指的就是DDR工作频率:用户逻辑频率,再加上上下沿传输,意味着用户侧一个clk可以给DDR读写8个数据。reference input就是刚才所说的外接时钟源。底下的data_width指的是我们要存的数据位宽,例如双精度浮点数64,这时候我们可以发现无论是app_rd_data还是app_wdf_data都变成了512位,,刚好和64是8:1的关系,也就是我们所说的用户侧一个clk可以读写8个数据,所以我们在写自己的读写逻辑时,如果是连续地址读写,那么每次的地址数据应该+8。CAS latency指的是DDR在读取时,列地址选中后数据传输放大到I/O口这个过程本身就有的一个延时,相当于我们给了读地址后到得到读数据的延时,这涉及到DDR的工作原理这里不作深究我们可以随便设个值。
在这里插入图片描述
在这里插入图片描述
这里说一下memory address map,大家注意到有row-bank-colum,row-colum-bank,bank-row-column几种数据地址映射机制,就是我们在进行存储时,是按照先填写column,第一行的column写满后跳到下一个bank的第一行去存储,8个bank的第一行都填满后,再跳转到下一行存储,其它几种同理。进一步深究MIG核的代码发现一个有趣的事情,例如row-column-bank的地址映射机制,在这里插入图片描述 这里的addr并不是单纯的row-column-bank的排列顺序,不由得想到一个问题,我们在设计自己的逻辑时,一个app_rd_data或者一个app_wdf_data要占用8个地址所以每次地址都是+8,即+4’b1000,相当于刚好给bank+1从而实现了bank 的跳转,因此Xilinx官方已经帮我们考虑了这些问题我们可以只管给addr+4’b1000其他的不用操心。
处理完这些就可以点击完成生成IP核。等IP核综合完成,这个时候推荐大家直接右键生成example design,因为你只是生成例化了控制器,但还没有和DDR芯片连接,生成example design便可以直接开始设计自己的读写逻辑。打开example design之后可以看到代码结构,只需要在u_example_tb.v里修改即可。在这里插入图片描述

接下来就是读写时序的设计了。

写命令与写地址
在这里插入图片描述
如上图所示①,②,③情况,只有在③时刻app_en和app_rdy同时为高电平app_cmd(命令)和(app_addr)地址才有效,所以当需要app_cmd,app_addr有效时app_en必须保持到app_rdy为高电平才有效。
写时序
在这里插入图片描述
如上图所示①,②,③种情况,写命令和写数据直接存在三种逻辑关系。
1、①表示写命令(app_cmd),写当前地址(app_addr)和写数据(app_wdf_data)以及写控制信号(app_en,app_rdy,app_wdf_rdy,app_wdf_wren,app_wdf_end)同时有效。
2、②表示写数据(app_wdf_data)和写控制信号(app_wdf_wren,app_wdf_end)先于写命令(app_cmd)和写当前地址(app_addr)以及其他写控制信号(app_en,app_rdy,app_wdf_rdy)一个用户时钟(ui_clk)。
3、③表示写数据(app_wdf_data)和写控制信号(app_wdf_wren,app_wdf_end)迟于写命令(app_cmd)和写当前地址(app_addr)以及其他写控制信号(app_en,app_rdy,app_wdf_rdy)。最多两个用户时钟(ui_clk)。
读时序
在这里插入图片描述
如上图所示,当读命令(app_cmd)和当前读地址(app_addr)以及读控制信号(app_en,app_rdy)同时有效时,等待读数据有效信号(app_rd_data_valid)有效时读数据(app_rd_data)有效。
重点说一下app_rdy,因为它的作用是指示你当前给MIG的指令是否被接受。只有它拉高时才会接受指令,否则即使让app_en拉高app_rdy为0也必须重新发出当前请求命令(实际上我们可以设置一个FIFO,将app_rdy作为FIFO的使能信号即可)。而导致app_rdy信号为0的原因可能有:

  1. PHY /内存初始化尚未完成;
  2. 所有bank都被占用;
  3. 请求读取并且读取缓冲区已满;
  4. 请求写入,没有可用的写缓冲区指针(也就是地址信号不可用);
  5. 正在插入定期读取;

这里给大家提供一段简单的读写代码,为了方便是直接在example design的u_example_tb基础上改的,所以比较粗糙可读性差点,大家可以把波形跑出来重点看时序,连续地址写入50个512bit数据再把它读出来,然后反复这个操作,主要是希望帮助大家熟悉读写时序,然后再根据你们自己的需要去设计自己的读写逻辑。注意app_rdy信号拉高后才可以正常读写操作所以应该用app_rdy信号作为指令继续进行的先决条件,还有就是大家会发现读数据时,第一个数据需要隔很长一段时间才能读出来,但是后续的数据会连续读出,所以在进行自己的设计时,最好一次性读出来一部分数据做处理(这时就体现FIFO的重要性了),不要读一个写一个否则效率太低。还有就是如果你的读写地址是随机的不连续的往往也会导致效率低下(app_rdy信号时常拉低),这是由DDR的工作机制决定的,因为从DDR读数据时,它会先充电激活一个row,如果下一个地址不在这一row,则需要关闭当前操作行(row)再打开新的行,这一过程叫precharge,具体细节感兴趣的同学可以去深究DDR工作机制。因为我的水平也一般般所以欢迎大家给我指出文章中的漏洞,也欢迎讨论~

module example_tb #(
  parameter SIMULATION       = "FALSE",   // This parameter must be
                                          // TRUE for simulations and 
                                          // FALSE for implementation.
                                          //
  parameter APP_DATA_WIDTH   = 512,        // Application side data bus width.
                                          // It is 8 times the DQ_WIDTH.
                                          //
  parameter APP_ADDR_WIDTH   = 29,        // Application side Address bus width.
                                          // It is sum of COL, ROW and BANK address
                                          // for DDR3. It is sum of COL, ROW, 
                                          // Bank Group and BANK address for DDR4.
                                          //
  parameter nCK_PER_CLK      = 4,         // Fabric to PHY ratio
                                          //
  parameter MEM_ADDR_ORDER   = "ROW_COLUMN_BANK" // Application address order.
                                                 // "ROW_COLUMN_BANK" is the default
                                                 // address order. Refer to product guide
                                                 // for other address order options.
 )

(
  // ********* ALL SIGNALS AT THIS INTERFACE ARE ACTIVE HIGH SIGNALS ********/
  input clk,                 // MC UI clock.
                             //
  input rst,                 // MC UI reset signal.
                             //
  input init_calib_complete, // MC calibration done signal coming from MC UI.
                             //
  input app_rdy,             // cmd fifo ready signal coming from MC UI.
                             //
  input app_wdf_rdy,         // write data fifo ready signal coming from MC UI.
                             //
  input app_rd_data_valid,   // read data valid signal coming from MC UI
                             //
  input [APP_DATA_WIDTH-1 : 0]  app_rd_data, // read data bus coming from MC UI
                                             //
  output [2 : 0]                app_cmd,     // command bus to the MC UI
                                             //
  output [APP_ADDR_WIDTH-1 : 0] app_addr,    // address bus to the MC UI
                                             //
  output                        app_en,      // command enable signal to MC UI.
                                             //
  output [(APP_DATA_WIDTH/8)-1 : 0] app_wdf_mask, // write data mask signal which
                                                  // is tied to 0 in this example
                                                  // 
  output [APP_DATA_WIDTH-1: 0]  app_wdf_data, // write data bus to MC UI.
                                              //
  output                        app_wdf_end,  // write burst end signal to MC UI
                                              //
  output                        app_wdf_wren // write enable signal to MC UI
                                   
                                              
  );

localparam BEGIN_ADDRESS = 32'h00000000 ; // This is the starting address from
                                     // which the transaction are addressed to
localparam NUM_TRANSACT  = 100 ; // Total number of transactions
localparam NUM_WRITES = 50 ;// Total Number of WRITE transactions
localparam NUM_READS  = 50 ;// Total Number of READ transactions
localparam TCQ  = 100; // To model the clock to out delay
localparam RD_INSTR = 3'b001; // Read command
localparam WR_INSTR = 3'b000; // Write command

reg  [2 :0]                     cmd;               // Command instruction 
reg  [APP_ADDR_WIDTH-1:0]       cmd_addr;          // Command address
reg  [9 :0]                     cmd_cnt ;          // Command count
reg                             cmd_en;            // Command enable 
reg                             init_calib_complete_r; // Registered version of init_calib_complete 
reg  [APP_DATA_WIDTH-1: 0]      wr_data;       // Write data internal signal
reg                             wr_en;         // Write enable signal

always @ (posedge clk)  
  init_calib_complete_r <= #TCQ init_calib_complete;
assign app_en    = cmd_en & (app_rdy) ;
assign app_cmd       = cmd;
assign app_addr  = cmd_addr;

always @(posedge clk)
begin
  if(rst)
    cmd_addr <= #TCQ BEGIN_ADDRESS;
  else if (cmd_en & app_rdy)
    if (cmd_addr < ((NUM_WRITES-1)*8))
      cmd_addr <= #TCQ cmd_addr + 4'b1000;
    else if (cmd_cnt == (NUM_WRITES-1) || cmd_cnt == NUM_TRANSACT-1)
      cmd_addr <= #TCQ BEGIN_ADDRESS;
    else begin
      cmd_addr <= cmd_addr;
    end
end

assign app_wdf_wren     = wr_en ;
assign app_wdf_end    = wr_en ;
assign app_wdf_data   = {483'b0,cmd_addr};
assign app_wdf_mask   = 64'b0 ;

always @(posedge clk)
begin
  if(rst | ~init_calib_complete_r) begin
    cmd_en <= 1'b0;
    cmd <= #TCQ WR_INSTR;
  end
  else if( cmd_cnt == NUM_TRANSACT-1) begin
//    cmd_en           <= #TCQ 1'b0;
    cmd              <= #TCQ WR_INSTR;
  // Generate 100 write commands till the cmd_cnt reaches a value of 99
  end else if (cmd_cnt < (NUM_WRITES-1)) begin
    cmd             <= #TCQ WR_INSTR;
    cmd_en          <= #TCQ app_rdy;
  // Generate 100 read commands after cmd_cnt reaches 99
  end else if (cmd_cnt == (NUM_WRITES-1) & cmd_en & app_rdy) begin
    cmd             <= #TCQ RD_INSTR;
    cmd_en          <= #TCQ app_rdy;
  end else if (cmd_cnt == (NUM_TRANSACT-1) & cmd_en & app_rdy) begin
    cmd_en          <= #TCQ app_rdy;
  end
end

always @(posedge clk)
begin
  if(rst | ~init_calib_complete_r) begin
    wr_en            <= #TCQ 1'b0;
  // Generate 100 write commands till the cmd_cnt reaches a value of 99
  end else if (cmd_cnt < (NUM_WRITES-1)) begin
    wr_en           <= #TCQ app_wdf_rdy;
  end else if (cmd_cnt == (NUM_WRITES-1) & app_wdf_rdy) begin
    wr_en           <= #TCQ 1'b0;
  end else if (cmd_cnt == (NUM_TRANSACT-1) & app_wdf_rdy) begin
    wr_en           <= #TCQ 1'b1;
  end
end

always @(posedge clk )
begin
  if(rst)
    cmd_cnt <= #TCQ 'b0;
  else if (cmd_en && app_rdy && cmd_cnt < NUM_TRANSACT-1) begin
      cmd_cnt <= #TCQ cmd_cnt + 'b1;
  end
  else if(cmd_cnt == NUM_TRANSACT-1) begin
      cmd_cnt <= #TCQ 10'b0;
  end
  else begin
      cmd_cnt <= cmd_cnt;
  end
end

endmodule
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!