1、BRAM配置测试
我们知道Vivado中BRAM大小分为18K和36K两种,这两种BRAM在何种配置下会如何分配资源,需要进行一定的考量。由于Vivado可以配置生成任意bit数的IO位宽,所以我对BRAM配置进行了简单的实验,结果如下所示。
可以看到,18bit位宽,1K深度可以正常使用18Kb的小BRAM。
如果使用16bit访存位宽,1152深度,依然是18Kb大小来生成BRAM,会导致资源无法映射到18Kb的BRAM中,而是使用了36Kb大小的BRAM,这导致了一半的BRAM被浪费。
上面两张图可以看到,如果使用较低的8bit位宽实现18Kb大小的RAM,也无法调用18K BRAM。但是如果将深度缩减为2K(经过测试2049深度也是调用了36K BRAM,所以深度必须是2K以下),即RAM大小减小为16Kb,则可以调用18K BRAM,减少资源浪费,这就很有意思了。
根据上述思路,我又对16bit位宽进行测试,设定深度为1K,则调用了18K BRAM,并且深度也是必须小于等于1K才会调用,否则就是36K BRAM,即RAM大小必须小于等于16Kb。依此类推,4bit位宽如果想使用18K BRAM,则必须深度小于等于4K,即RAM小于等于16Kb。
从官方文档中找到了对上述内容的解释,其实BRAM有16K×1、8K×2、4K×4、2K×9、1K×18、512×36等6种原型方案,所有的BRAM配置方案均在这些原型的基础上进行叠加拼接得到。所以说如果使用了1152×16的配置方案,则需要至少2块1K×18串联来满足深度要求,或者两块2K×9并联(4个4K×4并联等其他方案也可,但太浪费BRAM)来满足位宽要求,所以必须要占用36K BRAM。并且如果有更大的深度或者位宽出现时,可能会有很多种不同的解决方案,Vivado中也提供了相应的三种方案,Minimum Area Algorithm、Low Power Algorithm、Fixed Primitive Algorithm,帮助生成最适合项目需求的RAM形式。详见文档pg058-blk-mem-gen,42~45页。
2、BRAM读写时序
在本文中主要使用了写优先模式,以保证读出数据为最新。可以从时序图中看到,输入数据、数据地址、Enable信号等需要在时钟上升沿之前就到达接口位置。
3、ARM Memory Compiler SRAM时序
与ASIC实现中使用Memory Compiler生成的SRAM时序进行对比,可以发现该SRAM中控制信号和数据写入基本与Xilinx的BRAM是一致的,均需要在时钟上升沿之前到来地址和数据。但是MC SRAM数据读出速度要更快,在一定的时间延迟后即可得到有效输出数据吗,而BRAM则至少有1~3个周期的延迟才能获得读出数据。
4、读写冲突
BRAM读写时对端口位宽大小的单元进行操作,例如dout位宽为32bit,则每次读写均为32bit,BRAM深度大小就是有几个32bit的数据。
因此读写冲突的可能仅存在于同时读写同一个32bit数据,即同时读写一个地址的数据。所以我们在进行OBUF实现时,应该不会有读写冲突的出现。下图中所示为三种端口模式下的读写冲突情况,其中写优先模式的读写冲突,是由于前一周期B端口在读,本周期A端口在写,导致本周期B端口应当读到的旧数据被覆盖。从数据读逻辑上来说,只要有一个端口在本周期写数据,本周期读到的数据必定是亚稳态,如果是字节写模式(有写Mask),则写入部分地址的数据输出为亚稳态。如果是本周期A端口写入、B端口读出,则可以在下周期读出最新写入的数据。(本周期读的数据在下周期获得是在BRAM配置时设定的输出寄存器个数决定的,延迟周期数可以为1~3)
各种RAM(双端RAM、DRAM)HDL写法详见Vivado使用技巧(27):RAM编写技巧。
5、双端RAM Ping-Pong Buffer读写控制
转自FPGA基础设计(7)双口RAM乒乓操作。
这里的双端RAM两个口接的时钟频率不一样,写端口CLKA为20MHz,读端口CLKB为100MHz,也就是说读速度为写速度的5倍。
`timescale 1ns / 1ps
module DualRAM
(
input clk_wr, //写时钟速率20Mhz
input clk_rd, //读时钟速率100Mhz
input rst_n,
input [7:0] din,
output reg out_valid,
output reg [7:0] dout
);
reg [9:0] addr_wr, addr_rd;
reg en_wr1, en_wr2, we_wr1, we_wr2, en_rd1, en_rd2;
wire [7:0] dout1, dout2;
dual_port_ram u1 (
.clka(clk_wr), //写端口
.ena(en_wr1),
.wea(we_wr1),
.addra(addr_wr),
.dina(din),
.douta(),
.clkb(clk_rd), //读端口
.enb(en_rd1),
.web(1'b0),
.addrb(addr_rd),
.dinb(8'd0),
.doutb(dout1)
);
dual_port_ram u2 (
.clka(clk_wr), //写端口
.ena(en_wr2),
.wea(we_wr2),
.addra(addr_wr),
.dina(din),
.douta(),
.clkb(clk_rd), //读端口
.enb(en_rd2),
.web(1'b0),
.addrb(addr_rd),
.dinb(8'd0),
.doutb(dout2)
);
//写端口乒乓操作
always @ (posedge clk_wr) //写地址信号控制0~1023
if (!rst_n) addr_wr <= 1023;
else addr_wr <= addr_wr + 1'b1;
always @ (posedge clk_wr) //轮流写RAM1与RAM2
if (!rst_n) begin we_wr1 <= 1'b1; we_wr2 <= 1'b0;
en_wr1 <= 1'b1; en_wr2 <= 1'b0; end
else if (addr_wr == 1023) begin
we_wr1 <= ~we_wr1; we_wr2 <= ~we_wr2;
en_wr1 <= ~en_wr1; en_wr2 <= ~en_wr2;
end
//读端口乒乓操作
always @ (posedge clk_rd) //读地址信号控制0~1023
if (!rst_n) addr_rd <= 1021; //匹配延迟
else addr_rd <= addr_rd + 1'b1;
reg [15:0] cnt;
always @ (posedge clk_rd) //读时钟为写时钟的5倍
if (!rst_n) cnt <= 16'hFFFE; //匹配延迟
else if (cnt == 5119) cnt <= 0;
else cnt <= cnt + 1'b1;
reg flag1, flag2;
always @ (posedge clk_rd) //读RAM标志,RAM1或RAM2
if (!rst_n) begin flag1 <= 1'b1; flag2 <= 1'b0; end
else if (cnt == 5119) begin flag1 = ~flag1; flag2 = ~flag2; end
else begin flag1 <= flag1; flag2 <= flag2; end
always @ (posedge clk_rd) //读RAM使能,选择cnt的前1/5时间读取
if (!rst_n) begin en_rd1 <= 1'b1; en_rd2 <= 1'b0; end
else if (cnt < 1024) begin en_rd1 <= flag1; en_rd2 <= flag2; end
else begin en_rd1 <= 1'b0; en_rd2 <= 1'b0; end
reg en_rd1_reg, en_rd2_reg;
always @ (posedge clk_rd) //延迟一级,匹配时序
if (!rst_n) begin en_rd1_reg <= 0; en_rd1_reg <= en_rd1_reg; end
else begin en_rd1_reg <= en_rd1; en_rd2_reg <= en_rd2; end
always @ (posedge clk_rd) //输出选择,RAM1或RAM2;控制输出使能信号
if (!rst_n) begin dout <= 0; out_valid <= 0; end
else if (en_rd1_reg) begin dout <= dout1; out_valid <= 1; end
else if (en_rd2_reg) begin dout <= dout2; out_valid <= 1; end
else begin dout <= 0; out_valid <= 0; end
endmodule
上面的代码中有例化BRAM模块,不过这些端口不一定全部需要,根据本项目特点,使用Simple dual-port BRAM就可以,因此A口写,B口读,A口没有douta信号,B口没有dinb和web信号,例化的时候要注意,如果不清楚可以到Vivado中打开diagram看一下。
6、对比DNN Weaver RAM与BRAM数据接口与时序差别
下面是DNN Weaver RAM模块代码,该RAM使用在IBUF和BBUF中。与BRAM模块接口对比,该RAM的读写使能信号分开,并且读通道与写通道分开,可以同时读写,但没有解决读写冲突问题,说明IBUF和BBUF不会出现该问题,并且输出均有1个寄存器的延迟(例化模块时设定OUTPUT_REG=1)。总之使用BRAM对该模块可以进行很好的代替,因为功能上来说该RAM是BRAM的子集。
除了IBUF和BBUF,DNN Weaver中还有个OBUF。由于OBUF需要大量的读写,OBUF设计比IBUF等逻辑复杂很多,并且有两套读写接口,模块名称为banked_ram。学姐当时建议使用DRAM(Distributed RAM)实现OBUF,不知道会不会在综合的时候RAM逻辑过大,导致片上资源不足,或者导致综合时间过长的问题。
`timescale 1ns/1ps
module ram
#(
parameter integer DATA_WIDTH = 10,
parameter integer ADDR_WIDTH = 12,
parameter integer OUTPUT_REG = 0
)
(
input wire clk,
input wire reset,
input wire s_read_req,
input wire [ ADDR_WIDTH -1 : 0 ] s_read_addr,
output wire [ DATA_WIDTH -1 : 0 ] s_read_data,
input wire s_write_req,
input wire [ ADDR_WIDTH -1 : 0 ] s_write_addr,
input wire [ DATA_WIDTH -1 : 0 ] s_write_data
);
reg [ DATA_WIDTH -1 : 0 ] mem [ 0 : 1<<ADDR_WIDTH ];
always @(posedge clk)
begin: RAM_WRITE
if (s_write_req)
mem[s_write_addr] <= s_write_data;
end
generate
if (OUTPUT_REG == 0)
assign s_read_data = mem[s_read_addr];
else begin
reg [DATA_WIDTH-1:0] _s_read_data;
always @(posedge clk)
begin
if (reset)
_s_read_data <= 0;
else if (s_read_req)
_s_read_data <= mem[s_read_addr];
end
assign s_read_data = _s_read_data;
end
endgenerate
endmodule