3. BRAM(块RAM)详解

各位同学,今天我们来聊聊FPGA里一个特别重要的资源——BRAM。说白了,BRAM就是FPGA内部集成的专用存储块。我刚开始接触FPGA时,总觉得用分布式RAM(也就是LUT搭出来的)也能凑合,直到有一次项目里需要缓存一帧图像数据,才真正体会到BRAM的威力。

3.1 BRAM的容量与基本结构

先说说容量。Xilinx 7系列里,每个BRAM是36Kb的。嗯,这里要注意,是Kb(千比特),不是KB(千字节)。换算一下,一个BRAM能存4KB的数据。为什么是36Kb而不是32Kb?多出来的4Kb其实是用来做ECC(纠错码)的,不过一般我们用不到。

我习惯把BRAM想象成一个仓库。仓库有货架(存储单元),有叉车(地址译码器),还有传送带(数据总线)。你告诉叉车去哪个货架,它就把东西搬出来或者放进去。

参数 数值 说明
总容量 36 Kb 其中32Kb为数据,4Kb为ECC
最大位宽 36 bit 可配置为×1, ×2, ×4, ×9, ×18, ×36
最大深度 32768 位宽为1时
最小深度 512 位宽为36时

你想想看,一个36Kb的BRAM,如果配成×1位宽,就能存32768个比特。如果配成×36位宽,就只能存1024个36比特的字。这就是容量和位宽之间的权衡。

3.2 端口模式详解

BRAM最灵活的地方在于它的端口模式。说白了,就是你能同时从几个口读写数据。一共有三种模式:单口、简单双口、真双口。

3.2.1 单口模式

单口模式最简单——只有一个读写口。读和写不能同时进行,得排队。我在项目中用过一次单口BRAM做配置寄存器,因为配置数据只在初始化时写入,后面只读,所以单口完全够用。

适用场景:数据流单向、读写不同时发生的场合。比如查表、系数存储。

3.2.2 简单双口模式

简单双口模式有两个口:一个只读,一个只写。读和写可以同时进行,但两个口不能同时写。这个模式我用的最多。比如做FIFO,本质上就是简单双口——写端口只管往里塞数据,读端口只管往外拿数据。

// 简单双口BRAM的Verilog例化示例
// 端口A:只写,端口B:只读
BRAM_SDP_MACRO #(
    .BRAM_SIZE("36Kb"),
    .DEVICE("7SERIES"),
    .WRITE_WIDTH(18),
    .READ_WIDTH(18)
) bram_inst (
    .CLK(clk),
    .RST(rst),
    .WE(we_a),          // 写使能
    .ADDRWR(addr_a),    // 写地址
    .DI(din_a),         // 写数据
    .ADDRRD(addr_b),    // 读地址
    .DO(dout_b)         // 读数据
);

这里要注意,简单双口模式下,读和写可以访问不同的地址,互不干扰。但如果你在写的同时去读同一个地址,读出来的数据可能是旧的,也可能是新的——这取决于具体的时序。我曾经在这个坑里摔过一次,后来老老实实加了握手信号。

3.2.3 真双口模式

真双口模式就厉害了——两个口都能读也能写。两个口完全独立,各有各的时钟、地址、数据总线。这就像两个叉车可以同时在仓库里干活,但得小心别撞车。

真双口模式最大的坑是什么?是写冲突。如果两个口同时往同一个地址写数据,结果是不确定的。Xilinx的BRAM有冲突检测机制,但不会帮你解决冲突,只会报个警告。我建议你在设计时就避免这种情况,或者用仲裁逻辑来保证同一时刻只有一个口写某个地址。

避坑指南:我曾经在一个多核处理器项目中,两个核通过真双口BRAM共享数据。结果发现偶尔数据会错乱。查了三天,最后发现是两个核同时写了同一个地址。解决方案是给每个地址加一个"写锁"标志位。

3.3 级联与位宽扩展

单个BRAM的容量和位宽有限,但我们可以把多个BRAM拼起来用。这就是级联和位宽扩展。

3.3.1 深度扩展(级联)

深度扩展就是把多个BRAM串起来,增加存储深度。比如两个36Kb的BRAM级联,就能得到72Kb的存储空间。怎么做?把地址的高位作为片选信号,低位作为片内地址。

举个例子:我需要一个64Kb的RAM,位宽18位。一个BRAM只有36Kb,不够。那就用两个BRAM,一个存低32Kb,一个存高32Kb。地址的最高位用来选片,剩下的地址位用来选片内地址。

3.4.2 位宽扩展

位宽扩展就是把多个BRAM并联,增加数据位宽。比如我需要一个72位宽的RAM,一个BRAM最大只有36位。那就用两个BRAM,一个存低36位,一个存高36位。两个BRAM的地址完全一样,数据拼在一起。

小技巧:Xilinx的Vivado工具会自动帮你做位宽扩展。你只需要在IP Catalog里配置好位宽和深度,工具会自动计算需要多少个BRAM。但深度扩展需要手动处理片选逻辑。

3.4 FIFO生成器使用

FIFO(先进先出)是BRAM最经典的应用之一。说白了,FIFO就是一个数据缓冲队列。数据从一头进去,从另一头出来,先进去的先出来。

Xilinx提供了FIFO Generator IP核,用起来很方便。但我建议你了解一下它的内部原理,这样出了问题才知道怎么排查。

3.4.1 FIFO的基本参数

  • 深度:FIFO能存多少个数据。一般用2的幂次,比如16、32、64、512。
  • 位宽:每个数据有多少位。
  • 读写时钟:同步FIFO用同一个时钟,异步FIFO用两个不同的时钟。
  • 满/空标志:告诉外部电路FIFO满了还是空了。

3.4.2 异步FIFO的注意事项

异步FIFO在跨时钟域传输中特别有用。但这里有个大坑——空满标志的判断。因为读写时钟不同步,空满标志的生成需要用到格雷码和同步器。我曾经在一个项目中,异步FIFO的空标志偶尔会误判,导致读数据时读到无效值。后来发现是格雷码转换时出了时序问题。

// 异步FIFO的格雷码指针同步
// 写指针同步到读时钟域
reg [ADDR_WIDTH:0] wr_ptr_gray_sync1;
reg [ADDR_WIDTH:0] wr_ptr_gray_sync2;

always @(posedge rd_clk or posedge rd_rst) begin
    if (rd_rst) begin
        wr_ptr_gray_sync1 <= 0;
        wr_ptr_gray_sync2 <= 0;
    end else begin
        wr_ptr_gray_sync1 <= wr_ptr_gray;
        wr_ptr_gray_sync2 <= wr_ptr_gray_sync1;
    end
end

嗯,这里要注意,格雷码同步需要两级触发器来消除亚稳态。两级是最低要求,如果时钟频率很高,我建议用三级。

3.4.3 FIFO生成器配置建议

参数 建议值 说明
FIFO类型 独立时钟块RAM 异步FIFO选这个
读模式 标准模式 First Word Fall Through模式慎用
满标志 几乎满 留一点余量,防止溢出
空标志 几乎空 留一点余量,防止下溢

个人经验:我习惯把FIFO的深度配成实际需要的1.5倍。比如我需要存100个数据,就配成128的深度。多出来的空间可以吸收突发写入的峰值,避免数据丢失。

3.5 本章小结

BRAM是FPGA里最宝贵的资源之一。用好了,事半功倍;用不好,资源浪费还容易出bug。我建议你在设计初期就规划好BRAM的使用方案——用哪种端口模式?需不需要级联?FIFO的深度和位宽怎么配?这些都想清楚了,后面写代码就顺了。

最后说一句,别光看理论。找个开发板,实际跑一下BRAM的例程,看看资源报告里BRAM的使用情况。实践出真知,这话一点不假。

BRAM知识体系结构图 BRAM(块RAM) 容量:36Kb 端口模式 级联与扩展 位宽配置 深度配置 单口 简单双口 真双口 深度扩展(级联) 位宽扩展(并联) 典型应用:FIFO生成器 同步FIFO 异步FIFO 空满标志 图:BRAM知识体系结构图

专注资料整理