關於DRAM(distributed ram)和BRAM(block ram)的區別與使用


開發軟件:VIVADO2019.1.3

FPGA型號:xc7a35tcsg325-2

看完這篇文章你將收獲以下內容:

  • 理解SLICEL,SLICEM最本質的區別。
  • 理解什么是單端口DRAM,雙端口DRAM,簡單雙端口DRAM,以及四端口DRAM,SRL。
  • 通過對比調用DRAM 原語/IP產生DRAM的結果與直接運用Verilog來產生RAM的結果來加深DRAM的認識。
  • 通過對比調用SRL原語/IP產生DRAM的結果與直接運用Verilog reg來產生RAM的結果來加深DRAM的認識。

好的進入正題,今天我來帶大家認識LUT的另一種形態,DRAM(Distributed RAM,翻譯為分布式隨機存儲器,而不是我們平時看到的動態隨機存儲器)。在上一節課中,我們講到LUT6可以用作64bit的ROM(Read Only Memory),ROM里面的內容是只讀的,因此它里面的內容在我們將bit文件燒錄到FPGA中就已經確定好的了,無法修改。

有沒有方法將LUT6里面的內容修改呢,有在XILINX 7系中,有兩種SLICE。分別為SLICEL,和SLICEM。SLICEM 除了具備完整的SLICEL功能之外,它還具有寫存儲數據功能。結構決定功能,那么SLICEM與SLICEL是結構上存在哪些不同,我們通過圖1進行對比一下。我們可以看到,兩者只有紅框里的LUT6是不同的,其他結構完全一樣。

圖1:左邊為SLICEM,右邊為SLICEL

接下來我們分別將兩者的LUT6結構(圖2)放大進行進一步對比。兩者的LUT6相同點和不同點如下:

相同點:都具有地址輸入線(A1-A6),兩個輸出口(O5-O6)。

不同點:SLICEM的LUT6具有寫地址輸入線(WA1-WA8),寫數據端(DI1 DI2),寫使能端(WE),而SLICEL的LUT6沒有。

因此SLICEM的LUT6可以讀寫存儲數據,而SLICEL的LUT6只能讀數據不能寫。這就是為什么SLICEM的LUT6可以做RAM,而SLICEL的LUT6只能做ROM。

圖2:左為SLICEM的LUT6,右為SLICEL的LUT6

從圖1 中我們可以看到,1個SLICEM中有4個LUT6,它們可以單獨使用或者組合使用來形成多種形態的DRAM。如:

  • 32 x1 或 64x1的單端口DRAM (占用1個LUT6)
  • 32 x1 或 64x1的雙端口DRAM (占用2個LUT6 )
  • 32 x6 或 64x3的簡單雙端口DRAM(占用4個LUT6)
  • 32 x2或 64x1的四端口DRAM (占用4個LUT6)

除此之外這些LUT6還可以配合MUX來使用組成更大深度的DRAM如:

  • 128 x 1 的單端口DRAM(占用2個LUT6+1個MUX)
  • 128 x 1 的雙端口DRAM (占用4個LUT6+2個MUX)
  • 256 x 1 的單端口DRAM (占用4個LUT6+3個MUX)

以下是單端口DRAM、雙端口DRAM、簡單雙端口DRAM、四端口DRAM的讀寫特性(下面圖中,所有的的寄存器都是SLICE中四大件(LUT ,MUX,進位鏈,寄存器)的寄存器,不是DRAM內部結構,有關寄存器的內容在后面章節會講) :

  • 單端口DRAM:同步讀,同步寫(均以A[5:0]為輸入地址),結構如圖3所示。D為數據輸入,O為數據輸出,WCLK為同步時鍾,WE為寫使能(當要寫數據到DRAM時置高,當要從DRAM中讀數據時置低)
圖3:單端口DRAM結構
  • 雙端口DRAM : 一個端口(A[5:0]為地址輸入)可同步寫,異步讀。另一個端口(DPRA[5:0]為輸入地址)只能異步讀,其結構如圖4所示。兩個LUT6中存放着相同的數據,其實上面的LUT6就是一個單端DRAM,它的輸出(SPO)取決於輸入地址A[5:0]。下面的LUT6的不同之處就是它的輸入端口A[6:1]連的是DRPA[5:0],因此它的輸出取決於地址DPRA[5:0]。
圖4:雙端口DRAM結構
  • 簡單雙端口DRAM:一個端口(WADDR為地址輸入地址)只可同步寫,另一端口(RADDR為地址輸入)只能異步讀。在64x3簡單雙端口DRAM(圖5)中,3個數據輸入口DATA[3:1]並行輸入,3個數據輸出口O[3:1]並行輸出。在32x6簡單雙端口DRAM(圖6)中,6個數據輸入口DATA[6:1]並行輸入,6個數據輸出口O[6:1]並行輸出。
圖5:64x3簡單雙端口DRAM 圖6:32x6簡單雙端口DRAM
  • 四端口DRAM(圖7): 一個端口(ADDRD為地址輸入)可同步寫,異步讀。另外三個端口(ADDRA,ADDRB,ADDRC為輸入地址)只能異步讀。結構與雙端口DRAM相似,4個LUT所存放着着相同的數據,只不過每個端口都可以單獨讀不同地址的內容。
圖7:四端口DRAM
  • 128x1單端口DRAM(圖8),它由2個64x1單端口DRAM+1個MUX組成,結構與上一章2個LUT5組成1個LUT6類似,不同之處就是這里只有一個輸出口O。要注意:這里的MUX是SLICE中四大件(LUT ,MUX,進位鏈,寄存器)中的MUX,不是LUT6中的MUX。
圖8:128x1單端口DRAM
  • 32位移位寄存器,SRL32E(圖9)。它支持最高32位的移位輸出,可以選擇普通數據(D)輸入,也可以選擇由上一級SRLC32E的SHIFTOUT作為輸入。如果想用SRLC32E做12位移位輸出,只需要將A[4:0]設置為5'd12,移位輸出結果在SHIFTOUT口輸出,與此同時O6也將會輸出地址5'd12的結果,相當於一個32x1單端口DRAM(這地址是對於SRLC32E外部接口A[4:0]來說的,對於SRL32內部來說它是將高5位地址A[6:2]設為5'd12)。特別注意:SRLC32E只能在同一時鍾域使用,不能做跨時鍾域打拍使用,因為它內部移位操作不是用32個級聯的觸發器來做的,而是將上一級的電荷轉移到下一級,如果第一級發生出現亞穩態,它將會一直傳遞到最后一級。
圖9:SRLC32E
  • SRL32E級聯為64位移位寄存器(圖10) ,上一級的SHIFTOUT輸出連到下一級的SHIFTIN輸入即可。如果要實現它64x1單端口DRAM功能,需要額外增加一個MUX。可以參考128x1單端口DRAM(圖8)的情況,這里不加以累贅。
圖10:SRL32級聯組成64位移位寄存器

下面我將通過實例來講解SLICE中的DRAM和 移位寄存器。

  • 單端口DRAM,使用verilog 中的reg來寫(例1),綜合后一共耗費了54個LUT6,64個寄存器,8個MUX_F7,4個MUX_F4,肯用那么多資源去寫一個64x1的DRAM十有八九是土豪。當然造成資源使用過多有可能是我寫法比較隨意,vivado優化不了,在這里不細糾。
//以下是例1
module dram64_1(
    input clk,  //input clk
    input [5:0]a, //input address
    input d,    //input data
    input wr_en,//input write enable
    output o//output 
);


//這種寫法結果和使用DRAM原語一樣,但是會耗費很多資源,當然EDA軟件機智點也是可以優化成只用一個LUT的
reg dram64x1 [63:0] ;

always@(clk)
begin
    if(wr_en)
    begin
        dram64x1[a]<=d; 
    end      
end

assign o=dram64x1[a];

endmodule
  • 單端口DRAM,使用verilog 中的原語來寫(例2),綜合后一共使用了1個LUT6,資源利用率十分親民。
//以下是例2
module dram64_1(
    input clk,  //input clk
    input [5:0]a, //input address
    input d,    //input data
    input wr_en,//input write enable
    output o//output 
);

   RAM64X1S #(
      .INIT(64'h0000000000000000) // Initial contents of RAM
   ) RAM64X1S_inst (
      .O(o),        // 1-bit data output
      .A0(a[0]),    // Address[0] input bit
      .A1(a[1]),   // Address[1] input bit
      .A2(a[2]),      // Address[2] input bit
      .A3(a[3]),      // Address[3] input bit
      .A4(a[4]),      // Address[4] input bit
      .A5(a[5]),      // Address[5] input bit
      .D(d),        // 1-bit data input
      .WCLK(clk),  // Write clock input
      .WE(wren)       // Write enable input
   );

endmodule
  • 接下來我將對例(1),例(2)進行仿真,由於我們的例(1),例(2)的頂層的輸入輸出是一樣的,所以兩者的testbench一模一樣,我們往地址0~15依次寫入1111_0000_0000_1111。圖11為例(1)仿真結果,兩個紅色框中輸入d的內容和輸出o的內容是一樣的。至於藍色框出現不定態x,是因為我沒有將例(1)中的RAM初始化。圖12為例(2)的仿真結果,結果與例(1)類似,這里寫入時,o的輸出為我們預先寫入的初始值0。
//以下是例(1),例(2)的仿真測試用例
`timescale 1ns / 1ps
module dram64_1_tb(
    );
reg clk;
reg [5:0]a;
reg d;
reg wr_en;
reg [5:0]i;
wire o;

dram64_1 dram64_1_inst(.clk(clk),.a(a),.d(d),.wr_en(wr_en),.o(o));

initial begin
    clk=1'b1;
    a=6'h0;
    d=1'b1;
    wr_en=1'b1;
    i=6'h0;
    
end

always  begin
    #10 clk=~clk;
end

always@(posedge clk)begin
    i<=i+6'b1;
end

always@(posedge clk)begin
            if(i<16)begin
               wr_en<=1'b1;
               a<=i;
               case(i)
                   0,1,2,3:begin  d<=1'b1;end
                   4,5,6,7:begin  d<=1'b0;end
                   8,9,10,11:begin  d<=1'b0;end
                   11,12,13,14,15:begin  d<=1'b1;end 
               endcase
             end
             else  begin
                wr_en<=1'b0;
                a<=i-16;
             end
 end 

endmodule

圖11:例(1)的仿真結果 圖12:例(1)的仿真結果
  • 例(3)是用SLICEM中的SRLC32E來做成32位移位寄存器,總共使用了1個LUT。它所謂的移位就是將上一級的電荷轉移到下一級。如果電荷只有二分之一的高電平,它輸出也是二分之一的高電平,這樣很容易將邏輯錯誤傳遞下去,具體內容我會在有關亞穩態的章節講。
​
module shift(
    input clk,
    input ce,
    input shift_in,
    input [4:0] a,
    output Q,
    output shift_out
    );

   SRLC32E #(
      .INIT(32'h00000000) // Initial Value of Shift Register
   ) SRLC32E_inst (
      .Q(Q),     // SRL data output
      .Q31(shift_out), // SRL cascade output pin
      .A(a),     // 5-bit shift depth select input
      .CE(ce),   // Clock enable input
      .CLK(clk), // Clock input
      .D(shift_in)      // SRL data input
   );
endmodule

​
  • 例(4)是用SLICE中的reg來做32位移位寄存器(為避免綜合成SRLC32E,要在reg前使用(* SHREG_EXTRACT = “no” *)語句,否則會被VIVADO優化成用SRLC32E), 它總共使用了9個LUT ,32個移位寄存器,4個MUX_F7。
module shift(
    input clk,
    input ce,
    input shift_in,
    input [4:0] a,
    output Q,
    output shift_out
    );
(* SHREG_EXTRACT = "no" *)reg [31:0] dff;

always@(posedge clk) begin
    if(ce) begin
       dff[31:0]<={dff[30:0],shift_in}; 
    end    
end

assign shift_out=dff[31];
assign Q=dff[a];
endmodule
  • 接下倆我們對例(3),例(4)進行仿真。我們在測試用例中每個時鍾(周期10ns)上升沿依次寫入10101010。。。(記住,第一個數是1)。忽略寄存器未初始化所帶來的不定態,正常來說我們會在第32拍得到移位后的結果10101010。。。我們可以看到例(3)仿真結果(圖13)在310ns時輸出第一個1,但是例(4)仿真結果(圖14)在330ns時才輸出第1個1,那么哪一個才是對的呢?對此我個人理解例(3)的仿真結果是正確的,在0ns的時候我們可以看到shift_in是0,而不是我們的初始值1,因此寄存器已經在0ns時打了1拍,在310ns輸出1,剛好打了32拍。例(4)與預期不符的原因應該是仿真工具在0ns中沒有打拍,而是在10ns時打第一拍(初學者應注意:10ns時的輸入應該10ns前已經穩定好的那個數值,不是10ns下面所對應的數值),此時輸入時0,所以在打第32拍(320ns)時輸出是0。
`timescale 1ns / 1ps
module top_tb(

    );
    
    reg clk;
    reg ce;
    reg shift_in;
    reg [4:0] a;
    wire Q;
    wire shift_out;

shift shift_inst(.clk(clk),.ce(ce),.shift_in(shift_in),.a(a),.Q(Q),.shift_out(shift_out));
initial
begin
    clk=1;ce=1;shift_in=1;a=5'd31;
end

always
begin
    #5 clk=~clk;
end

always@(posedge clk)
    shift_in<=~shift_in;
    
endmodule
圖13:例(3)的仿真輸出結果 圖14:例4的仿真結果

在實際的應用過程中,我們不可能只用到寬度只有幾位的輸入,我們不可能例化多個原語來將多個64x1DRAM單元拼在一起,也不太推薦直接用verilog語句定義寄存器的方式去寫。此時我們應該學會使用與DRAM有關的IP,來自定義不同深度不同位寬的DRAM。下面我簡單介紹一下VIVADO 中 DRAM IP的使用(生成 64x16的簡單雙端口RAM)。

  • 在VIVADO工程下點擊打開IP Catalog->搜索RAM->找到Distributed Memory Generator 並點擊打開。(圖15)
圖15:DRAM IP 生成步驟1
  • 在memory config ,先給這個IP起個帥氣,讓你印象深刻的,有代表意義的名字,如 SDP。然后再設置深度為64,寬度16。再將Memory Type設置為Simple dual port。如果有需要添加輸入輸出寄存器的話在Port config中設置,如果要初始化DRAM的值我們在RST&Initialization中設置,這里我們暫時沒這需要,直接點OK就行(圖16)
圖16:DRAM IP生成步驟2
  • 期間有OK點OK,然后選擇Out of context per IP(Global和Out of context per IP的區別就是一個隨整個工程一起編譯,另一個是作為一個模塊單獨編譯),點擊Generate等待IP編譯完成。(圖17)
圖17:DRAM IP生成步驟3
  • 最后在IP SOURCE 中點擊SDP->Instantiation Template-> SDP.veo(veo 為verilog,vho為vhdl)打開SDP這個IP的模板,並復制到自己的工程,根據需求自己做相應的修改。(圖18)
圖18:生成DRAM IP步驟4

好的本章節的內容就到這里。記住本章節的DRAM 是 Distributed RAM 不是Dynamic RAM !!!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM