RAM 的英文全称是 Random Access Memory,即随机存取存储器,它可以随时把数据写入任一指定地址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度是由时钟频率决定的。RAM 主要用来存放程序及程序执行过程中产生的中间数据、运算结果等。
1、简介
Xilinx 7 系列器件具有嵌入式存储器结构,满足了设计对片上存储器的需求。嵌入式存储器结构由一列列 BRAM(块 RAM)存储器模块组成,通过对这些 BRAM 存储器模块进行配置,可以实现各种存储器的功能,例如:RAM、移位寄存器、ROM 以及 FIFO 缓冲器。
Vivado 软件自带了 BMG IP 核(Block Memory Generator,块 RAM 生成器),可以配置成 RAM 或者ROM。这两者的区别是 RAM 是一种随机存取存储器,不仅仅可以存储数据,同时支持对存储的数据进行修改;而 ROM 是一种只读存储器,也就是说,在正常工作时只能读出数据,而不能写入数据。需要注意的是,配置成 RAM 或者 ROM 使用的资源都是 FPGA 内部的 BRAM,只不过配置成 ROM 时只用到了嵌入式BRAM 的读数据端口。
Xilinx 7 系列器件内部的 BRAM 全部是真双端口 RAM(True Dual-Port ram,TDP),这两个端口都可以独立地对 BRAM 进行读/写。但也可以被配置成伪双端口 RAM(Simple Dual-Port ram,SDP)(有两个端口,但是其中一个只能读,另一个只能写)或单端口 RAM(只有一个端口,读/写只能通过这一个端口来进行)。单端口 RAM 只有一组数据总线、地址总线、时钟信号以及其他控制信号,而双端口 RAM 具有两组数据总线、地址总线、时钟信号以及其他控制信号。有关 BRAM 的更详细的介绍,请读者参阅 Xilinx 官方的手册文档“UG473,7 Series FPGAs Memory Resources User Guide”。
单端口 RAM 类型和双端口 RAM 类型在操作上都是一样的,我们只要学会了单端口 RAM 的使用,那么学习双端口 RAM 的读写操作也是非常容易的。本章我们以配置成单端口 RAM 为例进行讲解。
BMG IP 核配置成单端口 RAM 的框图如下图所示。
各个端口的功能描述如下:
DINA:RAM 端口 A 写数据信号。
ADDRA:RAM 端口 A 读写地址信号,对于单端口 RAM 来说,读地址和写地址共用同该地址线。
WEA:RAM 端口 A 写使能信号,高电平表示向 RAM 中写入数据,低电平表示从 RAM 中读出数据。
ENA:端口 A 的使能信号,高电平表示使能端口 A,低电平表示端口 A 被禁止,禁止后端口 A 上的读写操作都会变成无效。另外 ENA 信号是可选的,当取消该使能信号后,RAM 会一直处于有效状态。
RSTA:RAM 端口 A 复位信号,可配置成高电平或者低电平复位,该复位信号是一个可选信号。
REGCEA:RAM 端口 A 输出寄存器使能信号,当 REGCEA 为高电平时,DOUTA 保持最后一次输出的数据,REGCEA 同样是一个可选信号。
CLKA:RAM 端口 A 的时钟信号。
DOUTA:RAM 端口 A 读出的数据。
2、程序设计
首先在 Vivado 软件中创建一个名为 ip_ram 的工程,工程创建完成后,在 Vivado 软件的左侧“Flow Navigator”栏中单击“IP Catalog”,如下图所示。
在“IP Catalog”窗口的搜索框中输入“Block Memory”,出现唯一匹配的“Block Memory Generator”,如下图所示(图中出现的两个 IP 核为同一个 BMG IP 核)。
双击“Block Memory Generator”后弹出 IP 核的配置界面,接下来对 BMG IP 核进行配置,“Basic”选项页配置界面如下图所示。
Component Name:设置该 IP 核的名称,这里保持默认即可。
Interface Type:RAM 接口总线。这里保持默认,选择 Native 接口类型(标准 RAM 接口总线);
Memory Type:存储器类型。可配置成 Single Port RAM(单端口 RAM)、Simple Dual Port RAM(伪双端口 RAM)、True Dual Port RAM(真双端口 RAM)、Single Port ROM(单端口 ROM)和 Dual Port ROM(双端口 ROM),这里选择 Single Port RAM,即配置成单端口 RAM。
ECC Options:Error Correction Capability,纠错能力选项,单端口 RAM 不支持 ECC。
Write Enable:字节写使能选项,勾中后可以单独将数据的某个字节写入 RAM 中,这里不使能。
Algorithm Options:算法选项。可选择 Minimum Area(最小面积)、Low Power(低功耗)和 Fixed Primitives(固定的原语),这里选择默认的 Minimum Area。
接下来切换至“Port A”选项页,设置端口 A 的参数,该页面配置如下:
Write Width:端口 A 写数据位宽,单位 Bit,这里设置成 8。
Read Width:端口 A 读数据位宽,一般和写数据位宽保持一致,设置成 8。
Write Depth:写深度,这里设置成 32,即 RAM 所能访问的地址范围为 0-31。
Read Depth:读深度,默认和写深度保持一致。
Operating Mode:RAM 读写操作模式。共分为三种模式,分别是 Write First(写优先模式)、Read First(读优先模式)和 No Change(不变模式)。写优先模式指数据先写入 RAM 中,然后在下一个时钟输出该数据;读优先模式指数据先写入 RAM 中,同时输出 RAM 中同地址的上一次数据;不变模式指读写分开操作,不能同时进行读写,这里选择 No Change 模式。
Enable Port Type:使能端口类型。Use ENA pin(添加使能端口 A 信号);Always Enabled(取消使能信号,端口 A 一直处于使能状态),这里选择默认的 Use ENA pin。
Port A Optional Output Register:端口 A 输出寄存器选项。其中“Primitives Output Register”默认是选中状态,作用是打开 BRAM 内部位于输出数据总线之后的输出流水线寄存器,虽然在一般设计中为了改善时序性能会保持此选项的默认勾选状态,但是这会使得 BRAM 输出的数据延迟一拍,这不利于我们在 Vivado的ILA调试窗口中直观清晰地观察信号;而且在本实验中我们仅仅是把BRAM的数据输出总线连接到了ILA的探针端口上来进行观察,除此之外数据输出总线没有别的负载,不会带来难以满足的时序路径,因此这里取消勾选。
Port A Output Reset Options:RAM 复位信号选项,这里不添加复位信号,保持默认即可。
接下来的“Other Options”选项页用于设置 RAM 的初始值等,本次实验不需要设置,直接保持默认即可。
最后一个是“Summary”选项页,该页面显示了存储器的类型,消耗的 BRAM 资源等,我们直接点击“OK”按钮完成 BMG IP 核的配置,如下图所示:
接下来会弹出询问是否在工程目录下创建存放 IP 核的文件,我们点击“OK”按钮即可。
紧接着会弹出“Genarate Output Products”窗口,我们直接点击“Generate”,如下图所示。
之后我们就可以在“Design Run”窗口的“Out-of-Context Module Runs”一栏中出现了该 IP 核对应的run“blk_mem_gen_0_synth_1”,其综合过程独立于顶层设计的综合,所以在我们可以看到其正在综合,如下图所示。
在其 Out-of-Context 综合的过程中,我们就可以进行 RTL 编码了。首先打开 IP 核的例化模板,在“Source”窗口中的“IP Sources”选项卡中,依次用鼠标单击展开“IP”-“blk_mem_gen_0”-“Instantitation Template”,我们可以看到“blk_mem_gen_0.veo”文件,它是由 IP 核自动生成的只读的 verilog 例化模板文件,双击就可以打开它,如下图所示。
接下来我们创建一个新的设计文件,命名为 ram_rw.v,代码如下:
module ram_rw(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
output ram_en , //ram使能信号
output ram_wea , //ram读写选择
output reg [4:0] ram_addr , //ram读写地址
output reg [7:0] ram_wr_data, //ram写数据
input [7:0] ram_rd_data //ram读数据
);
//reg define
reg [5:0] rw_cnt ; //读写控制计数器
//*****************************************************
//** main code
//*****************************************************
//控制RAM使能信号
assign ram_en = rst_n;
//rw_cnt计数范围在0~31,写入数据;32~63时,读出数据
assign ram_wea = (rw_cnt <= 6'd31 && ram_en == 1'b1) ? 1'b1 : 1'b0;
//读写控制计数器,计数器范围0~63
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rw_cnt <= 1'b0;
else if(rw_cnt == 6'd63)
rw_cnt <= 1'b0;
else
rw_cnt <= rw_cnt + 1'b1;
end
//产生RAM写数据
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_wr_data <= 1'b0;
else if(rw_cnt <= 6'd31) //在计数器的0-31范围内,RAM写地址累加
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 1'b0 ;
end
//读写地址信号 范围:0~31
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_addr <= 1'b0;
else if(ram_addr == 5'd31)
ram_addr <= 1'b0;
else
ram_addr <= ram_addr + 1'b1;
end
ila_0 your_instance_name (
.clk(clk), // input wire clk
.probe0(ram_en), // input wire [0:0] probe0
.probe1(ram_wea), // input wire [0:0] probe1
.probe2(ram_addr), // input wire [0:0] probe2
.probe3(ram_wr_data),// input wire [4:0] probe3
.probe4(ram_rd_data) // input wire [7:0] probe4
);
endmodule
模块中定义了一个读写控制计数器(rw_cnt),当计数范围在 0~31 之间时,向 ram 中写入数据;当计数范围在 32~63 之间时,从 ram 中读出数据。
接下来我们设计一个 verilog 文件来实例化创建的 RAM IP 核以及 ram_rw 模块,文件名为 ip_ram.v,编写的 verilog 代码如下。
module ip_ram(
input sys_clk //系统时钟
//input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_en ; //RAM使能
wire ram_wea ; //ram读写使能信号,高电平写入,低电平读出
wire [4:0] ram_addr ; //ram读写地址
wire [7:0] ram_wr_data ; //ram写数据
wire [7:0] ram_rd_data ; //ram读数据
//*****************************************************
//** main code
//*****************************************************
reg [9:0]iReset_cnt;
reg sys_rst_n;
//reset
always @(posedge sys_clk) begin
if(iReset_cnt<=100)begin
sys_rst_n<=1'b0;
iReset_cnt<=iReset_cnt+1'b1;
end
else if(iReset_cnt>100)begin
sys_rst_n<=1'b1;
end
else begin
iReset_cnt<=1'b0;
end
end
//ram读写模块
ram_rw u_ram_rw(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
//RAM
.ram_en (ram_en ),
.ram_wea (ram_wea ),
.ram_addr (ram_addr ),
.ram_wr_data (ram_wr_data ),
.ram_rd_data (ram_rd_data )
);
//ram ip核
blk_mem_gen_0 blk_mem_gen_0 (
.clka (sys_clk ), // input wire clka
.ena (ram_en ), // input wire ena
.wea (ram_wea ), // input wire [0 : 0] wea
.addra (ram_addr ), // input wire [4 : 0] addra
.dina (ram_wr_data ), // input wire [7 : 0] dina
.douta (ram_rd_data ) // output wire [7 : 0] douta
);
endmodule
程序中例化了 ram_rw 模块和 ram IP 核 blk_mem_gen_0,其中 ram_rw 模块负责产生对 ram IP 核读/写所需的所有数据、地址以和读写使能信号,同时从 ram IP 读出的数据也连接至 ram_rw 模块。
接下来对 RAM IP 核进行仿真,来验证对 RAM 的读写操作是否正确。tb_ip_ram 仿真文件源代码如下:
module ram_tb();
reg sys_clk;
//reg sys_rst_n;
always #10 sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b0;
//sys_rst_n = 1'b0;
//#200
//sys_rst_n = 1'b1;
end
ip_ram u_ip_ram(
.sys_clk (sys_clk )
//.sys_rst_n (sys_rst_n )
);
endmodule
接下来就可以开始仿真了,仿真过程这里不再赘述,仿真波形图如下图所示。
由上图可知,由于PL端没有系统复位,这里写了一个复位信号,iReset_cnt计数到100后进行复位,之后ram_wea 信号拉高,说明此时是对 ram 进行写操作。ram_wea 信号拉高之后,地址和数据都是从 0 开始累加,也就说当 ram 地址为 0 时,写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是 1,我们总共向 ram 中写入 32 个数据。
RAM 读操作仿真波形图如下图所示:
由上图可知,ram_wea 信号拉低,说明此时是对 ram 进行读操作。ram_wea 信号拉低之后,ram_addr从 0 开始增加,也就是说从 ram 的地址 0 开始读数据;ram 中读出的数据 ram_rd_data 在延时一个时钟周期之后(这里之所以有一个时钟周期的延迟,是因为输入端存在一个寄存器,输入地址寄存器,所以从你外部输入读地址到RAM内部锁存该地址需要一个时钟周期。在生成IP核的时候有显示),开始输出数据,输出的数据为 0,1,2……,和我们写入的值是相等的, 也就是说,我们创建的 RAM IP 核从仿真结果上来看是正确的。
RAM 写操作在 ILA 中观察的波形如下图所示:
ram_wea 信号拉高之后,地址和数据都是从 0 开始累加,也就说当 ram 地址为 0 时,写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是 1。我们可以发现,上图中的数据变化和在 Vivado 仿真的波形是一致。
RAM 读操作在 ILA 中观察的波形如下图所示:
ram_wea(读使能)信号拉低之后,ram_addr 从 0 开始增加,也就是说从 ram 的地址 0 开始读数据;ram中读出的数据 ram_rd_data 在延时一个时钟周期之后,开始输出数据,输出的数据为 0,1,2……,和我们写入的值是相等的。我们可以发现,上图中的数据变化同样和 Vivado 仿真的波形是一致的。本次实验的 IP核之 RAM 读写实验验证成功。