【DNN Weaver FPGA实现】Vivado BRAM资源使用

1、BRAM配置测试

我们知道Vivado中BRAM大小分为18K和36K两种,这两种BRAM在何种配置下会如何分配资源,需要进行一定的考量。由于Vivado可以配置生成任意bit数的IO位宽,所以我对BRAM配置进行了简单的实验,结果如下所示。


18bit位宽,1K深度,共18Kb

可以看到,18bit位宽,1K深度可以正常使用18Kb的小BRAM。


16bit位宽,1152深度,共18Kb

如果使用16bit访存位宽,1152深度,依然是18Kb大小来生成BRAM,会导致资源无法映射到18Kb的BRAM中,而是使用了36Kb大小的BRAM,这导致了一半的BRAM被浪费。
8bit位宽,2304深度,共18Kb

8bit位宽,2048深度,共16Kb

上面两张图可以看到,如果使用较低的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原型分配

从官方文档中找到了对上述内容的解释,其实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页。

Examples of the Minimum Area Algorithm

Examples of the Low Power Algorithm

Examples of the Fixed Primitive Algorithm

2、BRAM读写时序

写优先

在本文中主要使用了写优先模式,以保证读出数据为最新。可以从时序图中看到,输入数据、数据地址、Enable信号等需要在时钟上升沿之前就到达接口位置。

3、ARM Memory Compiler SRAM时序

Memory Compiler SRAM时序

与ASIC实现中使用Memory Compiler生成的SRAM时序进行对比,可以发现该SRAM中控制信号和数据写入基本与Xilinx的BRAM是一致的,均需要在时钟上升沿之前到来地址和数据。但是MC SRAM数据读出速度要更快,在一定的时间延迟后即可得到有效输出数据吗,而BRAM则至少有1~3个周期的延迟才能获得读出数据。

4、读写冲突

转自Xilinx之RAM使用指南

BRAM读写时对端口位宽大小的单元进行操作,例如dout位宽为32bit,则每次读写均为32bit,BRAM深度大小就是有几个32bit的数据。

因此读写冲突的可能仅存在于同时读写同一个32bit数据,即同时读写一个地址的数据。所以我们在进行OBUF实现时,应该不会有读写冲突的出现。下图中所示为三种端口模式下的读写冲突情况,其中写优先模式的读写冲突,是由于前一周期B端口在读,本周期A端口在写,导致本周期B端口应当读到的旧数据被覆盖。从数据读逻辑上来说,只要有一个端口在本周期写数据,本周期读到的数据必定是亚稳态,如果是字节写模式(有写Mask),则写入部分地址的数据输出为亚稳态。如果是本周期A端口写入、B端口读出,则可以在下周期读出最新写入的数据。(本周期读的数据在下周期获得是在BRAM配置时设定的输出寄存器个数决定的,延迟周期数可以为1~3)


BRAM读写冲突

各种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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。