RAM 的英文全称是 Random Access Memory,即随机存取存储器,它可以随时把数据写入任一指定地址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度是由时钟频率决定的。RAM 主要用来存放程序及程序执行过程中产生的中间数据、运算结果等。本章我们将对 Vivado 软件生成的 RAM IP 核进行读写测试,并向大家介绍 Xilinx RAM IP 核的使用方法。
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 的读数据端口。本章我们主要介绍通过 BRAM IP 核配置成 RAM 的使用方法。
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 来说,读地址和写地址共用同该地址线。
本节实验任务是使用 Xilinx BMG IP 核,配置成一个单端口的 RAM,然后对 RAM 进行读写操作,通 过在 Vivado 自带的仿真器中观察波形是否正确,最后将设计下载到领航者 Zynq 开发板中,并使用 ILA 对其进行在线调试观察。
本实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设,各端口信号的管脚分配如下表所示:
对应的 XDC 约束语句如下所示:
- set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
- set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
Component Name:设置该 IP 核的名称,这里保持默认即可。
Write Width:端口 A 写数据位宽,单位 Bit,这里设置成 8。
之后我们就可以在“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
- //*****************************************************
-
- //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
- `timescale 1ns / 1ps
- module tb_ip_ram();
-
- 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
接下来就可以开始仿真了,仿真过程这里不再赘述,仿真波形图如下图所示。
上图为 RAM 的写操作仿真波形图,由上图可知,ram_wea 信号拉高,说明此时是对 ram 进行写操作。ram_wea 信号拉高之后,地址和数据都是从 0 开始累加,也就说当 ram 地址为 0 时,写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是 1,我们总共向 ram 中写入 32 个数据。
RAM 读操作仿真波形图如下图所示:
- set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
- set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
ip_ram