目录
Vivado 下 IP 核之单端口 RAM 读写
1、RAM 简介
2、实验任务
3、程序设计
3.1、RAM IP 核配置
3.2、时序图讲解
1、写优先模式的时序图如下所示:
2、读优先模式的时序图如下所示:
3、不变模式的时序图如下所示:
3.3、顶层模块设计
4、代码编写
4.1、顶层模块 ip_1port_ram
4.2、RAM 读写模块设计
4.3、仿真验证
5、下载验证
5.1、引脚约束
5.2、添加 ILA IP 核进行在线调试
5.3、上板验证
6、总结
RAM 的英文全称是 Random Access Memory,即随机存取存储器,简称随机存储器,它可以随时把数据 写入任一指定地址的存储单元,也可以随时从任一指定地址的存储单元中读出数据,其读写速度是由时钟频率决定的。本章将向大家介绍 Xilinx RAM IP 核的使用方法并对 RAM IP 核进行简单的读写测试。
由上图可知,存储器包括随机存储器和只读存储器,随机存储器包括静态 RAM 和动态 RAM。静态 RAM 只要有供电,它保存的数据就不会丢失;而动态 RAM 在供电的情况下,还需要根据其要求的时间来对存储的数据进行刷新,才能保持存储的数据不会丢失。
静态 RAM 一般包括单端口 RAM(Single Port RAM,缩写为 SP RAM)、简单双端口 RAM(Simple Dual Port RAM,缩写为 SDP RAM,也叫伪双端口 RAM)和 真双端口 RAM(True Dual Port RAM,缩写为 TDP RAM)。静态 RAM 的特点是存储容量相对不是很大,但是读写速度非常高,其在 FPGA 或者 ASIC 设计中 都属于非常重要的器件,可以说查找表、寄存器、组合逻辑和静态 RAM 构成了整个数字电路体系,足见静 态 RAM 的重要性。
动态 RAM 一般包括 SDRAM 和 DDR SDRAM。目前 DDR SDRAM 已经从 DDR1 代发展到 DDR5 代 了,DDR3 和 DDR4 SDRAM 是目前非常主流的存储器,大量使用在电脑、嵌入式和 FPGA 板卡上面,其特 点是存储容量非常大、但是读写速度相比于静态 RAM 会稍低一些,这一点在数据量较少的情况下尤为明显。
只读存储器一般包括 PROM、EPROM 和 EEPROM 等,是非易失性的存储器。目前使用率较高的是EEPROM,其特点是容量相对较小,存储的一般是器件的配置参数信息,例如 USB 2.0 芯片一般会配有一个 EEPROM 来存储相关的固件信息。
从上表可以看出,单端口 RAM 只有一个端口进行读写,即读/写只能通过这一个端口来进行。对于伪双端口 RAM 而言,其也有两个端口可以用于读写,但是其中一个端口只能读不能写,另一个端口只能写不能读;对于真双端口 RAM 而言,其有两个端口可以用于读写,且两个端口都可以进行读或写;
不同的特性决定不同的应用场景,在 RAM 的实际应用中,我们一般根据功能需求和带宽需求来选择合适的 RAM 类型,例如:
通过上述的学习,想必大家已经对几种 RAM 的区别有了一定的了解,那么现在我们就回到实际应用中来。在 FPGA 设计中 RAM 主要用于存放程序和数据,在 RAM 的实际使用过程中,我们一般将其作为关键数据的临时存储。接下来我们就介绍一下 Vivado 中的 RAM IP 核。
Vivado 软件自带的 Block Memory Generator IP 核(缩写为 BMG,中文名为块 RAM 生成器),可以用来配置生成 RAM 或者 ROM。RAM 是一种随机存取存储器,不仅可以读出存储的数据,同时还支持对存储的数据进行修改,而 ROM 是一种只读存储器,也就是说,在工作时只能读出数据,而不能写入数据。需要注意的是,配置生成的 RAM 或者 ROM 使用的都是 FPGA 内部的 BRAM 资源(Block RAM,即块随机存储器,是 FPGA 厂商在逻辑资源之外,给 FPGA 加入的专用 RAM 块资源),只不过配置成 ROM 时只用到了嵌入式 BRAM 的读数据端口。本章我们主要介绍如何将 BMG IP 核配置成 RAM。
这里有个地方需要大家注意一下,Xilinx 7 系列器件内部的 BRAM 全部是真双端口 RAM,但是通过 BMG IP 核,我们还可以将其配置为伪双端口 RAM 或者单端口 RAM。
有关双端口 RAM 的内容我们将在下一章进行讲解,本章我们围绕单端口 RAM 进行讲解。
BMG IP 核配置成单端口 RAM 的框图如下图所示。
本章实验任务是将 Xilinx BMG IP 核配置成一个单端口的 RAM 并对其进行读写操作,然后通过仿真观察波形是否正确,最后将设计下载到 FPGA 开发板中,并通过在线调试工具对实验结果进行验证。本实验只需要用到时钟信号和按键复位信号。
首先我们创建一个名为“ip_1port_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 元件的名称,这里我们保持默认命名,当然也可以命名为其它方便自己一眼看出其功能的名称。接着目光回到“Basic”选项卡上,该选项卡下各参数(我们重点关注(1)和(3)中的内容)含义如下:
(1)、“lnterface Type(接口模式)”:有两种接口模式可选,分别为 Native(常规)接口和 AXI4 接口。AXI4 模式一般是在处理器中的数据需要和 BRAM 交互(如 ZYNQ 中的 PS 和 PL 交互)时才会使用,当不需要与处理器数据进行交互时,我们一般采用 Native 模式,所以本次实验我们选择 Native 模式。
(2)、“Generate address interface with 32 bits”选项为是否生成位宽为 32 的地址接口,勾选后内存的数据位宽必须设置成 32 的整数倍,在实际应用中,我们一般不做勾选。
(3)、“Memory Type(存储类型)”:有五种类型可选,分别为 “Single Port RAM(单端口 RAM)”、 “Simple Dual Port RAM(伪双端口 RAM)”、“True Dual Port RAM(真双端口 RAM)”、“Single Port ROM(单端口 ROM)” 和 “Dual Port ROM(双端口 ROM)”,因为本章的试验任务是学习单端口 RAM, 所以我们选择“Single Port RAM”。
(4)、“Common Clock” 选项为是否启用同步时钟,在单端口模式下不需要考虑这个问题,所以该选 项也无法进行设置。
(5)、“ECC(全称 Error Correcting Code,即纠错码) Options”,纠错码选项只有在伪双端口 RAM 类型下才可以进行配置。
(6)、“Write Enable(写使能)”:可以选择是否使用字节写使能功能,启用后可设置字节大小为 8 位(无奇偶校验)或 9 位(包括奇偶校验),需要注意的是启用后内存的数据位宽必须设置为所选字节大小 的整数倍。这里我们不启用字节写使能功能。
(7)、“Algorithm Options(算法选项)”:算法选项主要用于决定 BRAM 的拼接的方式,一般在 BRAM 深度、宽度较大的时候起作用,有三种算法可选,分别为 “Minimum Area(最小面积算法)”、“Low Power(低功耗算法)” 和 “Fixed Primitives(固定单元算法)”。这里我们保持默认即可。
接下来我们对“Port A Options”选项卡进行配置,如下图所示:
“Other Options” 选项卡下各参数含义如下:
(1)、“Pipeline Stages within Mux”输出端 Mux 选择器的流水线级数。在大位宽、大深度的 BRAM 拼接场景中,我们会用 MUX 来选择输出地址所对应的数据,勾选该选项后可以使输出数据具有更好的时序。
(2)、“Memory Initialization(初始化文件)”,选择是否使用本地初始化文件(.coe 文件)来对存储空间进行初始化。
(3)、“Structural/UniSim Simulation Model Options”,用于选择结构仿真模型发生碰撞时生成的警告消息和输出的类型。
(4)、“Behavioral Simulation Model Options”,用于关闭仿真时的冲突告警和超出范围告警。最后一个是“Summary”选项卡,该界面显示了我们配置的存储器的类型,消耗的 BRAM 资源等信息,我们直接点击“OK”按钮完成 BMG IP 核的配置,如下图所示
接着就弹出了“Generate Output Products”窗口,我们直接点击“Generate”即可,如下图所示:
之后我们就可以在“Design Run”窗口的“Out-of-Context Module Runs”一栏中看到该 IP 核对应的 run“blk_mem_gen_0_synth_1”,其综合过程独立于顶层设计的综合,所以我们可以看到其正在综合,如下图所示:
接下来我们看下 IP 核的接口时序。
Xilinx BMG IP 核提供了三种 RAM 读写操作模式,分别为写优先模式、读优先模式和不变模式,针对不同的模式,Xilinx 也给出了不同的时序图。我们想要对 BMG IP 核进行读写操作,首先就需要了解其时序,如果读写操作不满足其时序,那么设计的代码就不能达到我们预期的效果,所以本小节就带着大家一起学习在不同 RAM 读写操作模式下的读写时序。
在写优先模式下,当 ENA(使能信号)为高后,第一个时钟上升沿 WEA(读/写使能信号)为低电平,表示读数据,此时的地址为 aa,所以读出的就是 aa 内的数据;第二个时钟上升沿 WEA 为高电平,表示写数据,此时的地址为 bb,即先将数据(DAIN)“1111”写入地址 bb 后再读出,所以读出的数据也是“1111”;第三个时钟上升沿同理,将 DINA 数据写入存储器后再将更新后的数据送到 DOUTA 上进行输出;第四个时钟上升沿 WEA 为低电平,表示读数据,读取的就是地址 dd 内的数据。
在读优先模式下,当 ENA(使能信号)为高后,第一个时钟上升沿 WEA 为低电平,表示读数据,此时的地址为 aa,所以读出的就是 aa 内的数据;第二个时钟上升沿 WEA 为高电平,表示写数据,此时的地址为 bb,即先读出地址 bb 中的旧数据,然后再将数据(DAIN)“1111”写入地址 bb;第三个时钟上升沿同理,先将当前存储器中的旧数据送到 DOUTA 上进行输出,然后再将 DINA 数据写入当前存储地址;第四个时钟上升沿 WEA 为低电平,表示读数据,读取的就是地址 dd 内的数据。
在不变模式下,当 ENA 和 WEA 同时为高时,只进行写操作,读数据保持不变,也就是说 DOUTA 保持前一拍数据,直到 WEA 为低。
总结一下就是在进行写操作时,读数据线(DOUT)上的数据在写优先模式和读优先模式下会随着地址的变化而输出相应的数据,在不变模式下则会保持最后一次读操作输出的数据。
本次实验的目的是为了将 Xilinx BMG IP 核配置成一个单端口的 RAM 并对其进行读写操作,因此可以给模块命名为 ip_1port_ram。由于单端口 RAM 的读写是共用一组信号总线的,所以这里我们将读写操作合成一个模块,可以将其命名为 ram_rw;系统时钟和系统复位是一个完整的工程中必不可少的输入端口信号,这里就不再多讲了;因为 RAM 主要用于存储程序中的数据,并不需要输出到 IO 引脚上,所以本实验不需要输出端口。由上述分析我们可以画出一个大致的模块框图,如下图所示:
因为本次实验除了调用 BMG IP 核外还需要例化一个读写模块(这里我们将其命名为 ram_rw),所以我们需要创建一个顶层模块来例化 IP 核与读写模块,这里我们可以将顶层模块命名为 ip_1port_ram,代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/01 17:52:37
// Design Name:
// Module Name: ip_1port_ram
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//实验任务
//本章实验任务是将 Xilinx BMG IP 核配置成一个单端口的 RAM 并对其进行读写操作,
//然后通过仿真观察波形是否正确,最后将设计下载到 FPGA 开发板中,
//并通过在线调试工具对实验结果进行验证。
module ip_1port_ram(
input sys_clk , //系统时钟 //50MHz系统时钟(一个周期是20ns:1/50MHz=0.02us=20ns)
input sys_rst_n //系统复位,低电平有效
);
//wire define
wire ram_en; //RAM 使能
wire ram_we; //ram 读写使能信号,高电平写入,低电平读出
wire [4:0] ram_addr; //ram 读写地址
wire [7:0] ram_wr_data; //ram 写数据
wire [7:0] ram_rd_data; //ram 读数据
//*****************************************************
//** main code
//*****************************************************
//ram 读写模块
ram_rw u_ram_rw(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
//RAM
.ram_en (ram_en ),
.ram_we (ram_we ),
.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_we ), // 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
//可以看出 ip_1port_ram 顶层模块只是例化了 IP 核(blk_mem_gen_0)和读写模块(ram_rw),
//其中读写模块负责产生对 RAM IP 核读/写所需的所有数据、地址和读写使能信号,
//同时将 RAM IP 读出的数据也连接至 ram_rw 模块。
首先介绍下 RAM 读写模块的设计,在 RAM 读写模块中,我们的输入信号主要有系统时钟信号、系统 复位信号和 RAM IP 核输出的读数据(ram_rd_data),输出有控制 RAM 所需的 ram_en(RAM 端口使能)、 ram_we(读写使能)、ram_addr(读写地址)和 ram_wr_data(写数据)这四个信号。由上述分析绘制出如下图所示的模块框图:
绘制波形图
在编写代码之前,我们可以先绘制一个大致的模块端口信号的波形图,其中时钟和复位信号是必不可少的,ram_en(RAM 端口使能信号)、ram_we(读写使能信号)、ram_addr(读写地址)、ram_wr_data(写数据)信号是我们使用 RAM 时要控制的信号,ram_rd_data 是 RAM 输出的数据,rw_cnt(读写计数器)是用来改变读写使能状态的,以读写 32 个数据为例,绘制出如下波形图:
当系统复位结束后,我们便一直对 RAM 进行读写操作,所以 ram_en 在复位结束后一直处于使能状态。 因为是读写 32 个数据,所以地址一直在 0~31 之间循环计数。接着我们用一个计数器(rw_cnt)循环计数0~63,以此来对读写使能信号(ram_we)进行切换。当计数器值在 0~31 时进行写操作,写操作期间我们需要将 ram_we 置为高;当计数器值在 32~63 时进行读操作,读操作期间我们需要将 ram_we 置为低。
在写操作期间,写数据(ram_wr_data)由 0 开始累加(步进为 1),直到累加值等于 31(即写入 32 个数据)时清零。而读数据只需要我们给出地址即可,需要注意的是读数据的输出会比地址晚一个时钟周期,当我们由读状态切换至写状态时,读数据会保持最后一次读出的数据。
代码编写
ram_rw 模块用于产生 RAM 读/写操作所需的信号,其代码如下所示:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/06/01 18:02:59
// Design Name:
// Module Name: ram_rw
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//ram_rw 模块用于产生 RAM 读/写操作所需的信号
module ram_rw(
//输入
input clk, //时钟信号 //50MHz系统时钟(一个周期是20ns:1/50MHz=0.02us=20ns)
input rst_n, //复位信号,低电平有效
//输出
output reg ram_en, //ram 使能信号
output ram_we, //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
//*****************************************************
//rw_cnt 计数范围在 0~31,写入数据;32~63 时,读出数据
assign ram_we = (rw_cnt <= 6'd31 && ram_en == 1'b1) ? 1'b1 : 1'b0;
//控制 RAM 使能信号
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_en <= 1'b0;
else
ram_en <= 1'b1;
end
//读写控制计数器,计数器范围 0~63
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rw_cnt <= 6'b0;
else if(rw_cnt == 6'd63 && ram_en)
rw_cnt <= 6'b0;
else if(ram_en)
rw_cnt <= rw_cnt + 1'b1;
else
rw_cnt <= 6'b0;
end
//读写地址信号 范围:0~31
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_addr <= 5'b0;
else if(ram_addr == 5'd31 && ram_en)
ram_addr <= 5'b0;
else if (ram_en)
ram_addr <= ram_addr + 1'b1;
else
ram_addr <= 5'b0;
end
//在 WE 拉高期间产生 RAM 写数据,变化范围是 0~31
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_wr_data <= 8'b0;
else if(ram_wr_data < 8'd31 && ram_we)
ram_wr_data <= ram_wr_data + 1'b1;
else
ram_wr_data <= 8'b0 ;
end
//添加 ila IP 核进行在线调试
//ILA实例化代码如下:
ila_0 u_ila_0 (
.clk (clk ), // input wire clk
.probe0 (ram_en ), // input wire [0:0] probe0
.probe1 (ram_we ), // input wire [0:0] probe1
.probe2 (ram_addr ), // input wire [4:0] probe2
.probe3 (ram_wr_data ), // input wire [7:0] probe3
.probe4 (ram_rd_data ) // input wire [7:0] probe4
);
endmodule
//模块中定义了一个读写控制计数器(rw_cnt),
//当计数范围在 0~31 之间时,向 ram 的 0~31 地址写入数据(0~31);
//当计数范围在 32~63 之间时,从 ram 的 0~31 地址中读出数据。
编写 TB 文件
我们接下来先对代码进行仿真,因为本章实验我们只有系统时钟和系统复位这两个输入信号,所以仿真文件也只需要编写这两个信号的激励即可,TestBench 代码如下:
`timescale 1ns / 1ps //仿真单位/仿真精度
//
// Company:
// Engineer:
//
// Create Date: 2023/06/09 11:43:05
// Design Name:
// Module Name: tb_ip_1port_ram
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
//编写 TB 文件
//我们接下来先对代码进行仿真,因为本章实验我们只有系统时钟和系统复位这两个输入信号,
//所以仿真文件也只需要编写这两个信号的激励即可,
//TestBench 代码如下:
module tb_ip_1port_ram();
//parameter define
parameter CLK_PERIOD = 20; //50MHz系统时钟(一个周期是20ns:1/50MHz=0.02us=20ns)
//200MHz系统时钟(一个周期是5ns:1/200MHz=0.005us=5ns)
//reg define
reg sys_clk;
reg sys_rst_n;
//信号初始化
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
end
//产生时钟
always #(CLK_PERIOD/2) sys_clk = ~sys_clk;
ip_1port_ram u_ip_1port_ram(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n )
);
endmodule
//可以看出我们的波形是满足我们所选择的不变模式的,即读写不同时进行。
写操作波形图如下所示:
//由上图可知,ram_wea 信号拉高,说明此时是对 ram 进行写操作。
//ram_wea 信号拉高之后,地址和数据都是从 0 开始累加,也就说当 ram 地址为 0 时,
//写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是1,
//我们总共向 ram 中写入 32 个(即 0~31)数据。
读操作波形图如下所示:
//由上图可知,ram_wea 信号拉低,说明此时是对 ram 进行读操作。
//ram_wea 信号拉低之后,ram_addr 从0 开始增加,即从 ram 的地址 0 开始读数据;
//ram_rd_data 在给出地址的下一个时钟周期后,开始输出数据,
//输出的数据为 0、1、2……30、31,和我们写入的值是相等的,
//也就是说,我们创建的 RAM IP 核从仿真结果上来看是正确的。
在仿真验证完成后,接下来对引脚进行分配,并上板验证。本实验中,系统时钟、按键复位的管脚分配如下表所示:
在仿真验证完成后,接下来对引脚进行分配,并上板验证。本实验中,系统时钟、按键复位的管脚分配如下表所示
引脚约束文件 ip_1port_ram.xdc 内容如下:
############## clock define 时钟引脚、电平信号约束#####Pro-FPGA##################
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports sys_clk]
############## reset key define##########################
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports sys_rst_n]
接下来添加 ILA IP 核,将 ram_en(1 位)、ram_wea(1 位)、ram_addr(5 位)、ram_wr_data(8 位) 和 ram_rd_data(8 位)信号添加至观察列表中,添加 ILA IP 核的方法如下:(注意:在调试工作完毕之后,还需要在 HDL 源代码中删除 ILA IP 核,然后重新综合以生成最终的比特流。)
我们在搜索栏中输入“ILA” 搜索,这时 Vivado 会自定根据关键词搜索出相应的结果,我们双击 “ILA (Integrated Logic Analyzer)”,如下图所示:
需要被观察的信号有 5 个,分别为 ram_en(1 位)、ram_wea(1 位)、ram_addr(5 位)、ram_wr_data(8 位) 和 ram_rd_data(8 位),则需要把 “Number of Probes” 设置为 5 ;“Sample Data Depth” 用于设置采样深度,在每个采样时钟下,ILA 都会将捕获到的探针信号的值送入 RAM 中,由于 RAM 的存 储空间是有限的,所以此选项就用于设置 RAM 最大存储多少个探针信号的值,我们保持其默认 1024,其数值越大,消耗的 RAM 资源也越多。其它选项保持默认即可,如下图所示:
点击 “ OK ”,出来如下界面,点击 “Generate” 即可。如下图:
本例程是将 ILA 例化在了 ram_rw 模块中,例化代码如下所示:
//添加 ila IP 核进行在线调试
//ILA实例化代码如下:
ila_0 u_ila_0 (
.clk (clk ), // input wire clk
.probe0 (ram_en ), // input wire [0:0] probe0
.probe1 (ram_we ), // input wire [0:0] probe1
.probe2 (ram_addr ), // input wire [4:0] probe2
.probe3 (ram_wr_data ), // input wire [7:0] probe3
.probe4 (ram_rd_data ) // input wire [7:0] probe4
);
编译工程并生成比特流文件后,将下载器一端连接电脑,另一端与开发板上的 JTAG 下载口连接,连接电源线,并打开开发板的电源开关。
点击 Vivado 左侧“Flow Navigator”窗口最下面的“Open Hardware Manager”,如果此时 Vivado 软件识别到了下载器,则点击“Hardware”窗口中“Progam Device”,在弹出的界面中选择“Program”下载程序。
RAM 写操作在 ILA 中观察到的波形如下图所示:
由上图可知,ram_wea 信号拉高之后,地址和数据都从 0 开始累加,也就说当 ram 地址为 0 时,写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是 1。可以发现,写操作的验证结果与我们的仿真波形是一致。
接着我们再来看一下 RAM 读操作在 ILA 中观察到的波形,如下图所示:
由上图可知,ram_wea信号拉低之后,ram_addr从0开始增加,即从ram的地址0开始读数据;ram_rd_data 在给出地址的下一个时钟周期后,开始输出数据,输出的数据为 0,1,2……30,31,和我们写入的值是相等的。我们可以发现,读操作的验证结果和仿真的波形也是一致的,至此说明了 IP 核之单端口 RAM 实验验证成功。
本章节我们主要介绍了存储器的分类、静态 RAM 的种类和特性,重点讲解了 Vivado 软件中如何将BGM IP 核配置成单端口 RAM 的方法,并调用其进行读写操作,读者要牢记于心,这对后面章节的学习至关重要,能大大提高大家的开发效率。IP 核调用(例化)格式如下所示:
//ram ip 核
blk_mem_gen_0 blk_mem_gen_0 (
.clka (sys_clk ), // input wire clka
.ena (ram_en ), // input wire ena
.wea (ram_we ), // 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
);