開發軟件: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是不同的,其他結構完全一樣。

接下來我們分別將兩者的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。

從圖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中讀數據時置低)

- 雙端口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]。

- 簡單雙端口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]並行輸出。


- 四端口DRAM(圖7): 一個端口(ADDRD為地址輸入)可同步寫,異步讀。另外三個端口(ADDRA,ADDRB,ADDRC為輸入地址)只能異步讀。結構與雙端口DRAM相似,4個LUT所存放着着相同的數據,只不過每個端口都可以單獨讀不同地址的內容。

- 128x1單端口DRAM(圖8),它由2個64x1單端口DRAM+1個MUX組成,結構與上一章2個LUT5組成1個LUT6類似,不同之處就是這里只有一個輸出口O。要注意:這里的MUX是SLICE中四大件(LUT ,MUX,進位鏈,寄存器)中的MUX,不是LUT6中的MUX。

- 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個級聯的觸發器來做的,而是將上一級的電荷轉移到下一級,如果第一級發生出現亞穩態,它將會一直傳遞到最后一級。

- SRL32E級聯為64位移位寄存器(圖10) ,上一級的SHIFTOUT輸出連到下一級的SHIFTIN輸入即可。如果要實現它64x1單端口DRAM功能,需要額外增加一個MUX。可以參考128x1單端口DRAM(圖8)的情況,這里不加以累贅。

下面我將通過實例來講解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


- 例(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


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

- 在memory config ,先給這個IP起個帥氣,讓你印象深刻的,有代表意義的名字,如 SDP。然后再設置深度為64,寬度16。再將Memory Type設置為Simple dual port。如果有需要添加輸入輸出寄存器的話在Port config中設置,如果要初始化DRAM的值我們在RST&Initialization中設置,這里我們暫時沒這需要,直接點OK就行(圖16)

- 期間有OK點OK,然后選擇Out of context per IP(Global和Out of context per IP的區別就是一個隨整個工程一起編譯,另一個是作為一個模塊單獨編譯),點擊Generate等待IP編譯完成。(圖17)

- 最后在IP SOURCE 中點擊SDP->Instantiation Template-> SDP.veo(veo 為verilog,vho為vhdl)打開SDP這個IP的模板,並復制到自己的工程,根據需求自己做相應的修改。(圖18)

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