RAM 的英文全称是 Random Access Memory,即随机存取存储器,它可以随时把数据写入任一指定地址的存储单元,也可以随时从任一指定地址中读出数据,其读写速度是由时钟频率决定的。RAM 主要用来存放程序及程序执行过程中产生的中间数据、运算结果等。
Xilinx 7 系列器件具有嵌入式存储器结构,它由一列列 BRAM(块 RAM)存储器模块组成,通过对这些 BRAM 存储器模块进行配置,可以实现各种存储器的功能,例如:RAM、移位寄存器、ROM 以及 FIFO 缓冲器。
Vivado 软件自带了 BMG IP 核(Block Memory Generator,块 RAM 生成器),可以配置成 RAM 或者ROM。这两者的区别是 RAM 是一种随机存取存储器,不仅仅可以存储数据,同时支持对存储的数据进行修改;而 ROM 是一种只读存储器,也就是说,在正常工作时只能读出数据,而不能写入数据。
BRAM 全部是真双端口 RAM(True Dual-Port ram,TDP),这两个端口都可以独立地对 BRAM 进行读/写。但也可以被配置成伪双端口 RAM(Simple Dual-Port ram,SDP)(有两个端口,但是其中一个只能读,另一个只能写)或单端口 RAM(只有一个端口,读/写只能通过这一个端口来进行)。单端口 RAM 只有一组数据总线、地址总线、时钟信号以及其他控制信号,而双端口 RAM 具有两 组数据总线、地址总线、时钟信号以及其他控制信号。
配置一个单端口的 RAM,然后对 RAM 进行读写操作,通过在 Vivado 自带的仿真器中观察波形是否正确,最后将设计下载到 Zynq 开发板中,并使用 ILA 对其进行在线调试观察。
|
|
|
/*
* 当计数范围在 0~31 之间时,向 ram 中写入数据;
* 当计数范围在 32~63 之间时,从 ram 中读出数据。
*/
module ram_rw(
input clk,
input rst_n,
output ram_wea,
output ram_en,
output reg [4:0] ram_addr,
output reg [7:0] ram_wr_data,
input [7:0] ram_rd_data
);
reg [5:0] cnt;
//计数器 范围(0~63)
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 6'd0;
else if(cnt == 6'd63)
cnt <= 6'd0;
else
cnt <= cnt + 1'b1;
end
//使能信号
assign ram_en = rst_n;
//读写信号
assign ram_wea = (cnt < 6'd32 && ram_en) ? 1'b1:1'b0;
//地址信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_addr <= 5'd0;
else if(ram_addr == 5'd31)
ram_addr <= 5'd0;
else
ram_addr <= ram_addr + 1'b1;
end
//写信号的数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_wr_data <= 8'd0;
else if(ram_addr == 6'd31)
ram_wr_data <= 8'd0;
else
ram_wr_data <= ram_wr_data + 1'b1;
end
endmodule
module top_singlep_ram_test(
input clk,
input rst_n
);
wire ram_wea;
wire ram_en;
wire [4:0] ram_addr;
wire [7:0] ram_wr_data;
wire [7:0] ram_rd_data;
// ram_rw 读写模块
ram_rw u_ram_rw(
.clk (clk),
.rst_n (rst_n),
.ram_wea (ram_wea),
.ram_en (ram_en),
.ram_addr (ram_addr),
.ram_wr_data (ram_wr_data),
.ram_rd_data (ram_rd_data)
);
// ip_ram 核
blk_mem_gen_0 u_blk_mem_gen_0 (
.clka (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 clk;
reg rst_n;
initial begin
clk = 1'b0;
rst_n = 1'b0;
#200
rst_n = 1'b1;
end
always #10 clk = ~clk;
top_singlep_ram_test u_top_singlep_ram_test(
.clk (clk),
.rst_n (rst_n)
);
endmodule
由上图可知,ram_wea 信号拉高,说明此时是对 ram 进行写操作。ram_wea 信号拉高之后,地址和数据都是从 0 开始累加,也就说当 ram 地址为 0 时,写入的数据也是 0;当 ram 地址为 1 时,写入的数据也是 1,我们总共向 ram 中写入 32 个数据。
由上图可知,ram_wea 信号拉低,说明此时是对 ram 进行读操作。ram_wea 信号拉低之后,ram_addr从 0 开始增加,也就是说从 ram 的地址 0 开始读数据;ram 中读出的数据 ram_rd_data 在延时一个时钟周期之后,开始输出数据,输出的数据为 0,1,2……,和我们写入的值是相等的。
添加 ILA IP 核,将 ram_en、ram_wea、ram_addr、ram_wr_data 和 ram_rd_data 信号添加至观察列表中。
|
|
ILA 的时钟和探针信号连接到顶层设计中,例化 ILA IP 核的代码如下:
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 [4:0] probe2
.probe3(ram_wr_data), // input wire [7:0] probe3
.probe4(ram_rd_data) // input wire [7:0] probe4
);
最后为工程添加 IO 管脚约束,对应的 XDC 约束语句如下所示:
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports clk]
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst_n]
生成比特流之后,直接点击“Program”,此时 Vivado 会自动打开 ILA 的调试窗口,如下图所示:
|
|
|
|
注意:在Port B Options 栏目下,Primitives Output Register 取消勾选,其功能是在输出数据加上寄存器,可以有效改善时序,但读出的数据会落后地址两个周期。很多情况下,不使能这项功能,保持数据落后地址一个周期。
完成配置后,点击“Generate”生成 RAM IP。
Simple Dual Port RAM 模块端口的说明如下:
信号名称 | 方向 | 说明 |
---|---|---|
clka | in | 端口A时钟输入 |
wea | in | 端口A使能 |
addra | in | 端口A地址输入 |
dina | in | 端口A数据输入 |
clkb | in | 端口B时钟输入 |
addrb | in | 端口B地址输入 |
doutb | in | 端口B数据输出 |
|
|
RAM 的数据写入和读出都是按时钟的上升沿操作的,端口 A 数据写入的时候需要置高wea 信号,同时提供地址和要写入的数据。
而端口 B 是不能写入数据的,只能从 RAM 中读出数据,只要提供地址就可以了,一般情况下可以在下一个周期采集到有效的数据。
/*
* 当计数范围在 0~511 之间时,向 ram 中写入数据;
* 当计数范围在 512~1023 之间时,从 ram 中读出数据。
*/
module ip_simple_port_ram_test(
input clk,
input rst_n
);
wire ram_wea;
reg [8:0] ram_wr_addr;
reg [15:0] ram_wr_data;
reg [8:0] ram_rd_addr;
wire [15:0] ram_rd_data;
reg [9:0] cnt;
//读写使能信号
assign ram_wea = (cnt < 10'd512) ? 1'b1:1'b0;
//计数器 范围(0~1023)
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 1'b0;
else if(cnt == 10'd1023)
cnt <= 1'b0;
else
cnt <= cnt + 1'b1;
end
//写信号地址及数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
ram_wr_addr <= 1'b0;
ram_wr_data <= 1'b0;
end
else if(ram_wr_addr == 9'd511 && ram_wr_data == 16'd511) begin
ram_wr_addr <= 1'b0;
ram_wr_data <= 1'b0;
end
else begin
ram_wr_addr <= ram_wr_addr + 1'b1;
ram_wr_data <= ram_wr_data + 1'b1;
end
end
//读信号地址
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
ram_rd_addr <= 1'b0;
else if (ram_rd_addr == 9'd511)
ram_rd_addr <= 1'b0;
else
ram_rd_addr <= ram_rd_addr + 1'b1;
end
//例化 ip ram核
ip_simple_ram u_ip_simple_ram (
.clka(clk), // input wire clka
.wea(ram_wea), // input wire [0 : 0] wea
.addra(ram_wr_addr), // input wire [8 : 0] addra
.dina(ram_wr_data), // input wire [15 : 0] dina
.clkb(clk), // input wire clkb
.addrb(ram_rd_addr), // input wire [8 : 0] addrb
.doutb(ram_rd_data) // output wire [15 : 0] doutb
);
endmodule
`timescale 1ns / 1ps
module tb_ip_simport_ram();
reg clk;
reg rst_n;
initial begin
clk = 1'b0;
rst_n = 1'b0;
#200
rst_n = 1'b1;
end
always #10 clk = ~clk;
ip_simple_port_ram_test u_ip_simple_port_ram_test(
.clk (clk),
.rst_n (rst_n)
);
endmodule
添加 ILA IP 核,将 ram_wea、ram_wr_addr、ram_wr_data 、ram_rd_addr和ram_rd_data 信号添加至观察列表中。
|
|
ILA 的时钟和探针信号连接到顶层设计中,例化 ILA IP 核的代码如下:
//例化 ILA IP核
ila_0 your_instance_name (
.clk(clk), // input wire clk
.probe0(ram_wea), // input wire [0:0] probe0
.probe1(ram_wr_addr), // input wire [8:0] probe1
.probe2(ram_wr_data), // input wire [15:0] probe2
.probe3(ram_rd_addr), // input wire [8:0] probe3
.probe4(ram_rd_data) // input wire [15:0] probe4
);
最后为工程添加 IO 管脚约束,对应的 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]
生成比特流之后,直接点击“Program”,此时 Vivado 会自动打开 ILA 的调试窗口,如下图所示: