Tiny ZYNQ板 工程十四  基于PL端的 BLOCK RAM IP核的使用 v1.1

几乎所有的FPGA芯片内部都有存储单元BLOCK RAM,ZYNQ也不例外,本文将主要介绍BRAM IP核的使用

本文在 vivado2018.3版本上 演示, 其他版本请自行研究

备注 此文针对1.1及以上版本硬件,如果是v1.0的硬件请看对应的工程

单片机和STM32这样的微处理器内部有RAM用来缓存变量,FPGA也不例外,用FPGA的设计过程中我们也需要经常性的缓存一些数据(如中间变量,图像的临时文件,或者矩阵运算的模板等),那我们在FPGA的设计中又该如何实现数据的暂存呢,有很多方法。 我们可以将这些数据存储在芯片外部的DDR和SDRAM中,也可以存储在FPGA的芯片内部。

FPGA的芯片内部有两种方式来实现数据的缓存功能,一种是基于硬件的BLOCK RAM(可以理解为FPGA内部放了一个RAM芯片,相当于是物理存储模块),另一种是DRAM,(Distributed RAM)由fpga的逻辑资源拼接出来的

DRAM 和BRAM 各有优缺点,最明显的区别是 BRAM不占用FPGA逻辑资源,DRAM需要占用LUT及查找表,BRAM 使用起来复杂(需要时钟同步并且给一些控制信号),DRAM 只需要简单调用查找表就好(可以不用时钟,异步存储和读取,相当于组合逻辑), BRAM 可以存储比较多的数据, 而DRAM受FPGA逻辑资源闲置较大(通常仅少量数据如几个字节的时候推荐用DRAM,),当数据较多如缓存一行图像数据或者存储卷积模板的时候会用BRAM(几K 到几百K), 当存储非常大的数据 如整张图像(几M 甚至几十M)时 ,这时候推荐使用外部存储芯片(SDRAM 或者 DDR) 当然如何去使用方法不是固定的,只要项目的程序能跑通,哪怕外部挂个内存条都没事。

我们可以利用BRAM 生成多种不同形式的RAM,比方说 ROM FIFO 等。

项目创建

1)首先正常创建一个工程(可参考前面的例子),

2)添加PLL IP核。在Vivado软件的选中“IP Catalog”

在弹出的窗口中搜索 BRAM 并在搜索出的结果中双击Block Me。进入时钟模块设置向导

Xilinx 7系列FPGA 内部的BRAM 可以配置成 真双口RAM(True Dual-Port ram,能同时负责读写),简单双口RAM(Simple Dual-Port ram,一个只能负责读 一个只能负责写)单端口RAM(只有一个端口)以及ROM,如下图所示。

各种RAM使用起来大同小异, 我们这边选择一个简单双口RAM来进行演示。

接下来 设置 A端口 (这里我们创建一个8bit 位宽,16深度的一个RAM) 深度代表有多少个 8bit位宽的RAM单元, 这里用16 也是为了方便演示

关于Operating Mode选项 有三种模式,读优先,写优先,和保持模式, 因为我们用的simple dual port ram 属于 两个端口 一个负责写 一个负责读,所以这边 operating Mode选保持模式就好

接下来是B端口设置, 位宽和深度系统默认会和A端口匹配,这里不需要进行修改。这里有个输出寄存器选项 Primitives Output Register需要注意系统默认是打开的,这样BRAM 的B端口读出的数据会经过一个寄存器,在高速流水线的设计中,可以提高时序性能,让数据和输出时钟同步,但是这会使得BRAM输出的数据延迟一拍,所以需要特别注意(下面会提到),之后选择OK 生成IP 模块

在其他选项里 可以导入初始化文件 来给RAM赋初值, 这里我们用不到 所以留空

之后点选确认保存并生成bram资源

为了让BRAM 的调用结果更直观, 这里我们增加一个ILA模块(ILA模块相当于FPGA内部的逻辑分析仪功能),ILA的具体使用可以参考下一个工程(工程十一)http://www.hellofpga.com/index.php/2022/10/12/ila/

在IP菜单里添加ILA 模块

将探针的数量修改成6(这里我们要抓取6个信号)


在ILA的第二页设置栏,分别设置每一个探针的位宽

之后点选OK 保存并生成ILA资源

程序设计

接下来是本次试验的程序设计部分

本次程序设计 共分3个阶段

  • 第一个阶段(mode=0)对系统中的寄存器(包括bram 调用的地址,读写使能等寄存器)进行初始化(mode 0)
  • 第二个阶段(mode=1)(port a口的地址从0-15进行自增,并且将对应的0-15存储到bram内,存储完16个数字后,对port a的读写功能切换成读,即不再写入数据)
  • 第三阶段 (mode=2)(port b口的地址从0-15进行自增,并且读取port b 的输出数据,当自增到15时,将mode的值赋值为3)
  • 第四阶段 (mode=3)不做任何操作 给mode赋值0 让四个阶段循环起来(为什么这里要循环,因为如果不增加循环,上电瞬间你还没用ila开始抓波形,人家FPGA已经完成了全部的读写操作,你就看不到完整的过程了,所以这里需要对整个过程进行循环可以方便观察)

以下是完整程序

`timescale 1ns / 1ps
module BRAM_TEST(
    input CLK
);
     
reg [3:0]addr_a;
reg [3:0]addr_b;
reg  wr_en_a;
reg  [1:0]mode=2'd0;
reg  [7:0]din_a;
wire  [7:0]dout_b;

always@(posedge CLK)begin
    if(mode==0)begin
        addr_a<=4'd0;
        din_a<=8'd0;
        addr_b<=4'd0;
        wr_en_a<=1'b1;
        mode<=2'd1;
    end
    else if(mode==1)begin
        if(addr_a==4'd15)begin
            mode<=2'd2;
            wr_en_a<=1'b0;
        end
        else begin
            addr_a<=addr_a+1'b1;
            din_a<=din_a+1'b1;
        end
    end
    else if(mode==2)begin
        if(addr_b==4'd15)begin
            mode<=2'd3;
        end
        else addr_b<=addr_b+1'b1;
    end
    else mode<=2'd0;
end


blk_mem_gen_0 u_bram (
  .clka(CLK),   
  .ena(1'b1),    
  .wea(wr_en_a),    
  .addra(addr_a),
  .dina(din_a),  
  .clkb(CLK),   
  .enb(1'b1),  
  .addrb(addr_b), 
  .doutb(dout_b) 
);  

ila_0 ila_u (
	.clk(CLK),
	.probe0(mode),
	.probe1(wr_en_a),
	.probe2(addr_a), 
	.probe3(din_a), 
	.probe4(addr_b), 
	.probe5(dout_b) 
);

endmodule

本程序中一共只有一个always块,根据mode寄存器值的不同分别执行上面提到的4个阶段的功能,其中 din_a和addr_a的值一直相同; 代表每个地址存入的内容即该地址的地址号,也即是对应(0-15)之间的数字

添加约束文件

set_property PACKAGE_PIN K17 [get_ports CLK]
set_property IOSTANDARD LVCMOS33 [get_ports CLK]

之后进行编译综合

在线ILA 观察波形看试验结果

将刚才编译综合好的程序 对FPGA进行PROGRAM

保持默认选项,点选program

之后系统会自动增加一个ILA窗口(窗口中包含着我们之前程序中用探针观察的所有信号)

点选箭头,开始抓取信号

之后我们要观察的信号就被抓取出来了

如果数据是十六进制的 ,可以将观察的信号全部选中,切换数据进制

下图为抓出来的波形结果,这里有几个地方备注下,输出输出是跟地址有两个时钟节拍的延迟(ram块读入地址,对地址数据进行输出这个过程是需要时钟同步的,这里有一个时钟,而输出的数据需要经过ram口的寄存器,这里就是第二个时钟) 第二个时钟延迟可以在之前设置bram的界面把输出寄存器关闭来取消,但是在高速访问的情况下不利于系统的稳定性。

程序比较简单,在实际调用BRAM模块的时候,除了用ILA来验证,也可以用仿真的方式来验证。 另外BRAM模块在使用的过程中对输出延时需要格外注意(这里在实际项目中很容易忽视输出数据的延时,而造成实际的bug)

以下是本次实验的完整工程:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注