DDR控制器MIG核的使用

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章