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的使用情况。实践出真知,这话一点不假。