27 浅谈XILINX BRAM的基本使用

发布时间 2023-12-29 17:33:32作者: 米联客(milianke)

软件版本:VIVADO2021.1

操作系统:WIN10 64bit

硬件平台:适用XILINX A7/K7/Z7/ZU/KU系列FPGA

登录米联客(MiLianKe)FPGA社区-www.uisrc.com观看免费视频课程、在线答疑解惑!

1 概述

对于BRAM 详细的说明在XILINX 官方文档,pg058中有说明,我们这里仅对课程涉及的内容讲解。

Xlinx系列FPGA,包含两种RAM:Block RAM和分布式RAM(Distributed RAM),他们的区别在于,Block RAM是内嵌专用的RAM,而Distributed RAM需要消耗珍贵的逻辑资源组成。前者具有更高的时序性能,而后者由于分布在不通的位置,延迟较大。

2 BRAM RAM的应用形式

2.1 单口ROM (Single-Port ROM)

单口ROM,就是数据只读的,需要在IP初始化的时候,对ROM进行初始化,而且只有一个读接口。

2.2 双口ROM(Dual-port ROM)

端口A和端口B可以同时访问ROM

2.3 单口RAM(Single-port RAM)

2.4 简单双口RAM(Simple Dual-port RAM)

A端口写,B端口读

2.5 真双口RAM(True Dual-port RAM)

A 端口和B端口都可以读或者写

3 BLOCK RAM的读写模式

支持3种模式,分别是Write First Mode, Read First Mode, No Change Mode

3.1 先写模式(Write First Mode)

这种模式下:

1)写操作:设置WEA为1写入当前地址的数据,在下一个时钟DOUTA会输出这个地址新写入的数据

2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据

3.2 先读模式(Read First Mode)

这种模式下:

1)写操作:设置WEA为1写入当前地址的数据,而且在下一个时钟DOUTA会输出这个地址的原先的数据

2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据

 

3.3 不变模式(No Change Mode)

这种模式下:

1)写操作:设置WEA为1写入当前地址的数据,和前面两种方式不一样,DOUT保存不变

2)读操作:设置WEA为0读出当前地址的数据,在下一个时钟DOUTA会输出这个地址的数据

4 支持字节写入(BYTE Write)

另外,BRAM还具备BYTE Write功能,这样可以只对某一个字节进行修改,从下图时序图可以看出,只要控制WEA就可以控制对具体哪一个BYTE进行写控制。

5 访问冲突 (Collision Behavior)

BRAM 很好用,但是需要注意冲突的问题,就是对于同一个地址写或者读的时候需要注意。

5.1 异步时钟处理原则

使用异步时钟,当一个端口将数据写入存储位置时,另一端口在指定的时间内不得读取或写入该位置。 器件数据手册中定义了该时钟到时钟的建立时间,以及其他Block RAM切换特性。这里说到的" 时钟到时钟的建立时间"我还没注意到哪一个文档有说明。所以异步时钟可以通过长期的稳定性测试获取到这个时间间隔。

5.2 同步时钟处理原则

同步写冲突:如果两个端口都试图写到内存中的同一位置,则会发生写写冲突。 内存位置的结果内容未知。 请注意,Write-Write冲突会影响内存内容,而Write-Read冲突只会影响数据输出

使用字节写入:使用字节写入时,在同一数据字中写入单独的字节时,存储器内容不会损坏。 仅当两个端口都试图写入同一字节时,RAM内容才会损坏。 下图说明了这种情况。 假设addra = addrb = 0

同步读写冲突:如果一个端口尝试写入内存位置而另一个端口读取相同的位置,则可能发生同步读写冲突。 虽然在写-读冲突中存储器的内容没有损坏,但是输出数据的有效性取决于写端口的工作模式。

a: 如果写入端口处于READ_FIRST模式,则另一个端口可以可靠地读取旧的存储器内容。

b: 如果写入端口处于WRITE_FIRST或NO_CHANGE模式,则读取端口的输出上的数据无效。

c: 如果是字节写入,则只有更新的字节在读取端口输出上无效,但是RAM中的内容是对的

下图说明了读写冲突和字节写入的影响。 当端口A处于WRITE_FIRST模式和READ_FIRST模式时,显示doutb。 假定addra = addrb = 0,端口B始终处于读取状态,并且所有内存位置均初始化为0。RAM的内容在读写冲突中不会被破坏。

5.3 简单的双端口RAM冲突

对于简单双端口RAM,无论时钟如何,都可以使用READ_FIRST,WRITE_FIRST和NO_CHANGE工作模式。

简单双端口RAM就像真正的双端口RAM,其中仅连接了A端口的Write接口和B端口的Read接口。 工作模式定义了A或B端口的读写关系,并且仅在地址冲突期间影响A和B端口之间的关系。

对于同步时钟和冲突期间,可以配置端口A的写模式,以便对端口B的读操作可以产生数据(作用类似于READ_FIRST),也可以产生未定义的数据(Xs)。 因此,始终建议在配置为简单双端口RAM时使用READ_FIRST。 对于异步时钟,Xilinx建议将端口A的写入模式设置为WRITE_FIRST以确保碰撞安全。 有关此行为的详细信息,请参阅pg058第51页的冲突行为。

对于7系列设备,当RAM_MODE设置为ture dual port时,选定的操作模式将传递到Block RAM。 对于将RAM_MODE设置为simple dual port的原语,写模式为READ_FIRST用于同步时钟,而WRITE_FIRST用于异步时钟。

对于基于UltraScale架构的设备,没有限制,并且无论时钟如何,所选的操作模式总是传递给Block RAM原语。这一段说明,这种高级模式我们暂时不涉及。

其他内存冲突限制:地址空间重叠

7系列FPGA Block RAM存储器在以下配置中具有附加的冲突限制:

•当配置为真双端口(ture dual port)

•当CLKA(端口A)和CLKB(端口B)异步时

•在同时执行读写操作的应用程序中

•使用配置为READ_FIRST的写入模式配置端口A,端口B或两个端口时

上面文字描述中很多都在讲解冲突,其实对于我们的具体应用而言,更多时候我们BRAM是做乒乓使用的,也就是读地址和写地址,都是不会同时发生,而且时钟是同步的,这样就不容易发生冲突导致的数据破坏,和不正确。

6 输出寄存器

BRAM 可以设置有寄存器输出和无寄存器输出,下图是BRAM的框图结构

下图是有寄存器和无寄存器输出,可以达到的最高时钟频率的数据表,所以增加寄存器输出可以提高速度。我们例子中由于用到的演示时钟并没有很高所以不需要增加寄存器输出。

6.1 通过无输出寄存器输出方式读取

6.2 通过Primitives Output Register读取数据并实现rEad使能延迟

6.3 使用两个流水线阶段读取数据的延迟

7 添加BRAM IP

设置简单双口RAM

设置BRAM的端口A的宽度和深度

设置BRAM的端口B的宽度和深度,并且没有寄存器输出

这一页默认

单击OK

8 读写BRAM代码

本代码的设计和FIFO使用非常类似

1)、写操作:写操作不断进行,每次写入1024个数据

2)、读操作:读操作是在每次写入达到512个数据开始的,当然实际上读操作完全可以和写操作同时进行,错开512个数据是为了方便观察现象。

/*************BRAM IP 测试********************************************

--BLOCK RAM 是FPGA内部自带的资源,BLOCK RAM分为硬核BLOCK RAM 和分布式BLOCK RAM(逻辑实现)

--BLCOK RAM 属于FPGA稀有资源,具有非常高效的访问速度和效率,比如FIFO实际也是用过BLOCK RAM实现

--本实验实现对 BLOCK RAM IP的仿真测试

*********************************************************************/

`timescale 1ns / 1ns //仿真时间刻度/精度

 

module bram_test(

input I_rstn, //系统复位输入

input I_sysclk //系统时钟输入

);

 

reg [9:0]addra;    //通道A 地址

reg [7:0]wr_frame; //帧计数器

reg [1:0]WR_S;     //写状态机

reg ena;           //通道A使能

reg wea;           //通道A写使能

 

reg [9:0]addrb;   //读通道B地址

reg [1:0]RD_S;    //读状态机

reg enb;          //通道B使能

 

wire [31:0] dina;    //bram 数据输入

wire [31:0] doutb;   //bram 数据输出

 

assign dina = {wr_frame,wr_frame,addra[7:0],addra[7:0]};  //输入的数据包{帧信号,帧信号,通道A地址,通道A地址}

 

always @(posedge I_sysclk)begin

    if(!I_rstn)begin //复位重置相关寄存器

       wr_frame <= 8'd0;

       addra    <= 9'd0;

       ena      <= 1'b1;

       wea      <= 1'b0;

       WR_S     <= 2'd0;

    end

    else begin

        case(WR_S)

        0:begin

             addra  <= 10'd0; //设置地址从0开始

             ena    <= 1'd1;  //设置通道A使能

             wea    <= 1'b1;  //设置写使能

             WR_S   <= 2'd1;  //下一个状态

        end

        1:begin

            if(addra != 10'd1023)begin //如果写地址不等于1023,

               wea   <= 1'b1; //设置写使能

               ena   <= 1'b1; //设置通道A使能

               addra <= addra + 1'b1;//那么通道A地址,每个时钟地址增加1

            end

            else begin //否则代表完成了1帧数据写入到BRAM

               wea   <= 1'b0; //设置写使能为0,停止写

               ena   <= 1'b0; //设置通道A使能为0

               wr_frame <= wr_frame +1'b1;//帧计数器

               WR_S   <= 2'd2;//下一个状态

            end

        end

        2:begin

            if(RD_S == 2'd2) //如果读操作完成

               WR_S   <= 2'd0; //回到状态0重新开始

        end

        default:WR_S   <= 2'd0;

        endcase

     end

end

 

always @(posedge I_sysclk)begin

    if(!I_rstn)begin //复位重置相关寄存器

       addrb    <= 9'd0;

       enb      <= 1'b0;

       RD_S     <= 2'd0;

    end

    else begin

        case(RD_S)

        0:begin

            enb     <= 1'b0; //设置读使能0

            addrb   <= 10'd0; //设置读地址从0开始

             if(addra == 10'd512)begin//读数据在写数据的第512个地址开始

                enb   <= 1'b1; //使能读通道

                RD_S  <= 2'd1; //下一状态

             end  

        end

        1:begin

            enb    <= 1'b1;//设置读使能1

            if(addrb != 10'd1023)////如果读地址不等于1023,

               addrb  <= addrb + 1'b1;//那么通道B地址,每个时钟地址增加1

            else

               RD_S   <= 2'd2;//下一状态

        end

        2:begin

            RD_S   <= 2'd0;//下一状态

        end

        default:RD_S   <= 2'd0;

        endcase

     end

end  

 

//例化BRAM IP,简单双口RAM

blk_mem_gen_0 bram_inst (

      .clka(I_sysclk),    //通道A时钟输入

      .ena(ena),          //通道A使能

      .wea(wea),          //写使能

      .addra(addra),      //通道A地址

      .dina(dina),        //通道A数据输入

      .clkb(I_sysclk),    //通道B时钟输入

      .enb(enb),          //通道B使能

      .addrb(addrb),      //通道B地址

      .doutb(doutb)       //通道B数据输出

    );

     

endmodule

9 仿真文件

/*********************仿真文件****************************************

*********************************************************************/

 

`timescale 1ns / 1ns//仿真时间刻度/精度

 

module tb_bram_test;

localparam  SYS_TIME = 20 ;//定义时钟周期 单位ns

 

reg   I_sysclk; //系统时钟

reg   I_rstn;   //系统复位

 

//例化bram_test

bram_test bram_test_inst

(

.I_sysclk(I_sysclk),

.I_rstn(I_rstn)

);

    

//初始化

initial begin

    I_sysclk  = 1'b0;

    I_rstn = 1'b0;

    #100;//产生100ns的系统复位

    I_rstn = 1'b1;//复位完成

end

//产生仿真时钟

always #(SYS_TIME/2) I_sysclk= ~I_sysclk;

                                                    

endmodule

10 仿真结果

箭头1 写开始

箭头2 写完1024个数据

箭头3 当写数据达到512个后开始读

箭头4 读完1024个数据

上图中,数据采用Primitives Output Register 方式数据延迟了3个时钟,这一点需要注意