FPGA 片内 RAM 读写测试
实验原理
Xilinx 在 VIVADO 里为我们已经提供了 RAM 的 IP 核, 我们只需通过 IP 核例化一个 RAM, 根
据 RAM 的读写时序来写入和读取 RAM 中存储的数据。 实验中会通过 VIVADO 集成的在线逻辑
分析仪 ila,我们可以观察 RAM 的读写时序和从 RAM 中读取的数据。不停的写不停的读。
创建 Vivado 工程
在添加 RAM IP 之前先新建一个 ram_test 的工程, 然后在工程中添加 RAM IP,方法如下:
1) 点击下图中 IP Catalog,在右侧弹出的界面中搜索 ram,找到 Block Memory Generator,双击
打开
2) 将 Component Name 改为 ram_ip,在 Basic 栏目下,将 Memory Type 改为 Simple Dual Prot
RAM,也就是伪双口 RAM。一般来讲"Simple Dual Port RAM"是最常用的,因为它是两个端
口,输入和输出信号独立。
3) 切换到 Port A Options 栏目下,将 RAM 位宽 Port A Width 改为 16,也就是数据宽度。将
RAM 深度 Port A Depth 改为 512,深度指的是 RAM 里可以存放多少个数据。使能管脚
Enable Port Type 改为 Always Enable。
4) 切换到 Port B Options 栏目下, 将 RAM 位宽 Port B Width 改为 16,使能管脚 Enable Port
Type 改为 Always Enable,当然也可以 Use ENB Pin,相当于读使能信号。 而 Primitives
Output Register 取消勾选,其功能是在输出数据加上寄存器,可以有效改善时序,但读出
的数据会落后地址两个周期。很多情况下,不使能这项功能,保持数据落后地址一个周
期。
5) 在 Other Options 栏目中, 这里不像 ROM 那样需要初始化 RAM 的数据,我们可以在程序中
写入,所以配置默认即可,直接点击 OK。
6) 点击“Generate”生成 RAM IP。
代码部分
先看第一个代码:
`timescale 1ns / 1ps
module ram(
input i_sysclk,
input i_sysrst
);
reg [8:0] wr_addr;
reg wr_en;
reg [15:0] wr_data;
reg [8:0] rd_addr;
wire [15:0] rd_data;
//write ram
always @(posedge i_sysclk)begin
if(i_sysrst == 1’b1)begin
wr_en <= 1’b0;
end
else if(&wr_addr)begin
wr_en <= 1’b0;
end
else begin
wr_en <=1’b1;
end
end
always @(posedge i_sysclk)begin
if(i_sysrst == 1’b1)begin
wr_addr <= 9’d0;
wr_data <= 16’d0;
end
else if(wr_en == 1’b1)begin
wr_addr <= wr_addr + 1’b1;
wr_data <= wr_data + 1’b1;
end
end
//read ram
always @(posedge i_sysclk)begin
if(i_sysrst == 1’b1)begin
rd_addr <= 9’d0;
end
else if(|wr_addr)begin
rd_addr <= rd_addr + 1’b1;
end
else begin
rd_addr <= 9’d0;
end
end
ram_ip inst_ram_ip (
.clka (i_sysclk), // input wire clka
.wea (wr_en), // input wire [0 : 0] wea
.addra(wr_addr), // input wire [8 : 0] addra
.dina (wr_data), // input wire [15 : 0] dina
.clkb (i_sysclk), // input wire clkb
.addrb(rd_addr), // input wire [8 : 0] addrb
.doutb(rd_data) // output wire [15 : 0] doutb
);
//ILA DEBUG
ILA_RAM_DEBUG
ILA_RAM_DEBUG (
.clk(i_sysclk), // input wire clk
.probe0({
wr_addr,
wr_en,
wr_data,
rd_addr,
rd_data
}) // input wire [50:0] probe0
);
endmodule
这代码虽然完成了重复读写的工作,但是有个问题,请看调试结果:
上图放大后
看到没,每一个周期完成后,会读到上一个周期中0地址的数据。那是因为当我们正在写0地址的时候,这个时候又同时去读0地址的数据,而这时候的数据还保留的是上一个周期时的数据。
//read ram
always @(posedge i_sysclk)begin
if(i_sysrst == 1’b1)begin
rd_addr <= 9’d0;
end
else if(|wr_addr)begin
rd_addr <= rd_addr + 1’b1;
end
else begin
rd_addr <= 9’d0;
end
end
这段读代码的潜在意思就是当我地址写到511时,读地址就会在下一个时钟周期切换的到0地址,而下一个时钟周期才去写0地址的数据。而在写0地址这个时钟周期,读地址还是保持在读0地址,因此当下一个时钟到来的时,读地址还是会读0地址的数据,这个时候以及后面时钟周期读出来的数据就是正确的。
因此这里需要解决的是当写完一轮地址后,写到511时,读地址在下一个时钟周期保持当前读数的操作。也就是当我在写0地址的时候,你不要来读0地址的数据,而是保持你上一个周期的读地址。
所以读ram的程序这样改:
//read ram
always @(posedge i_sysclk)begin
if(i_sysrst == 1’b1)begin
rd_addr <= 9’d0;
end
else if(wr_addr == 9’d0)begin
rd_addr <= rd_addr;
end
else if(|wr_addr)begin
rd_addr <= rd_addr + 1’b1;
end
else begin
rd_addr <= 9’d0;
end
end
改版后的测试结果:
不难看出,读出的数据连续。