2021.6.6 更新:
在另一个项目中使用伪双口RAM
时,发现自己之前有部分内容理解错了。先总结如下:
- 如果端口选择始终使能,那么A端口有个
wea
信号,用来控制写入;而B端口没有web
信号,所以只要有地址就往外读取数据。 - 如果端口没有选择始终使能,那么两个端口分别会多一个
ena
和enb
信号,A端口通过wea
和ena
同时控制,相与结果为1
就写入;B端口只有一个enb
信号,为高读出,为低不读。
一、双口RAM介绍
双口RAM(dual port RAM)
在异构系统中应用广泛,通过双口RAM
,不同硬件架构的芯片可以实现数据的交互,从而实现通信。例如,一般情况下,ARM与DSP
之间的通信,可以利用双口RAM
实现,ARM
通过EBI
总线连接到双口RAM
的A
口,DSP
通过EMIF
总线(也可以是uPP
总线,取决于速度需求)连接到双口RAM
的B
口,两者对同一块存储区域进行操作,即可实现两者的数据交互。
但是,因为双口RAM
的A
口和B
口都可以对相同的内存地址进行操作,这就引出了一个问题—假如通信双方在两个端口对同一地址同时读写,就会引发冲突。要解决这个问题,办法有二。
- 一是通信双方在时序上保证不会同时读写同一地址,将
ARM和DSP
可写地址范围进行分区,无论任何一方写完数据后都通过IO
发送中断通知对方,对方进行数据读取(乒乓RAM
操作),这样是比较可靠的; - 另外一个办法就是在
FPGA
里设置写busy
信号,实现两端写同步。
在FPGA
中,构建双口RAM
可以通过两种方法,一种是利用distributed RAM
构建,另一种是利用Block RAM
构建。简而言之,Block RAM
是使用FPGA
中的整块双口RAM
资源,而distributed RAM
则是用FPGA
中的逻辑资源拼凑形成的。一般的原则是,较大的存储应用,建议用Block RAM
;零星的小RAM
,一般就用distributed RAM
。
二、RAM IP核的生成和配置
block RAM有三种:单口RAM、伪双口RAM和真双口RAM。
- 单口RAM只有一个端口(A端口),可以对A端口进行读写。
- 伪双口RAM有两个端口(A和B端口),但是A端口只能进行写入操作,不能进行读出操作,而B端口则只能进行读出操作,不能进行写入操作。
- 真双口RAM有两个端口(A和B端口),A和B端口都能进行读写操作。
我们上面选择的是伪双口RAM。
关于输入端口A的 Operating Mode选项,具体可以参考下面这篇文章:
https://blog.csdn.net/lyfwill/article/details/89355012
“WRITE_FIRST”
模式,写操作时,输出端口会将当前写的数据输出。“READ_FIRST”
模式,写操作时,输出端口会将当前写地址的原数据输出。“NO_CHANGE”
模式,写操作时,输出端口会保持原值不变。只有在读操作的过程中输出端口才会变化。
关于输出端口B的Primitives Output Registers选项:
如果这个选项不勾上,那么正常的情况下,当第一个时钟时送来地址,那么数据会在第二个时钟取好送出RAM。但是当勾上这个选项后,那么数据就会延迟两个时钟,在第三个时钟送出。这是取数据情况下的时序。当存数据时候,那么只需要地址和数据在同一个时钟下即可。
三、实例
以下实例实现的功能是:对于一个已经配置好的,数据位宽为16,深度为512的双口RAM先进行地址递增的写操作,然后紧接着进行地址递增的读操作。其中,读操作比写操作延迟一个时钟周期,也可以避免读写操作同时作用于同一个地址;AB两个口都设置为一直使能,但是需要注意的是输入口A还有一个额外的写使能wea。
1、RTL代码
`timescale 1ns / 1ps
//
module ram_test(
input clk, //50MHz时钟
input rst_n //复位信号,低电平有效
);
//-----------------------------------------------------------
reg [8:0] w_addr; //RAM PORTA写地址
reg [15:0] w_data; //RAM PORTA写数据
reg wea; //RAM PORTA使能
reg [8:0] r_addr; //RAM PORTB读地址
wire [15:0] r_data; //RAM PORTB读数据
//产生RAM PORTB读地址
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
r_addr <= 9'd0;
/*
这里是为了让读地址比写地址延迟一个周期
你可能会想,当w_addr=1不等于9'd0的时候,r_addr不是也加1变成1了吗?
怎么就实现了延迟一个时钟周期了呢?
那是因为r_addr是寄存器类型的变量,只有在下一个时钟周期才能采集到此时的值。
*/
else if (|w_addr) //w_addr位或,不等于0
r_addr <= r_addr+1'b1;
else
r_addr <= 9'd0;
end
//产生RAM PORTA写使能信号
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
wea <= 1'b0;
else
begin
if(&w_addr) //w_addr的bit位全为1,共写入512个数据,写入完成
wea <= 1'b0;
else
wea <= 1'b1; //ram写使能
end
end
//产生RAM PORTA写入的地址及数据
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
w_addr <= 9'd0;
w_data <= 16'd0;
end
else
begin
if(wea)//ram写使能有效
begin
//当写地址全为1的时候,说明已经写够512个数据,就可以不用写使能了。
if (&w_addr)
begin
w_addr <= w_addr ; //将地址和数据的值保持住,只写一次RAM
w_data <= w_data ;
end
else
begin
w_addr <= w_addr + 1'b1;
w_data <= w_data + 1'b1;
end
end
end
end
//-----------------------------------------------------------
//实例化RAM
ram_ip ram_ip_inst (
.clka (clk ), // input clka
.wea (wea ), // input [0 : 0] wea
.addra (w_addr ), // input [8 : 0] addra
.dina (w_data ), // input [15 : 0] dina
.clkb (clk ), // input clkb
.addrb (r_addr ), // input [8 : 0] addrb
.doutb (r_data ) // output [15 : 0] doutb
);
endmodule
2、仿真程序
`timescale 1ns / 1ps
module vtf_ram_tb;
// Inputs
reg clk;
reg rst_n;
// Instantiate the Unit Under Test (UUT)
ram_test uut (
.clk (clk),
.rst_n (rst_n)
);
initial
begin
// Initialize Inputs
clk = 0;
rst_n = 0;
// Wait 100 ns for global reset to finish
#100;
rst_n = 1;
end
always #10 clk = ~ clk; //20ns一个周期,产生50MHz时钟源
endmodule
3、仿真结果
可以看出,在读数据的时候,输出数据比输入的地址也延迟了一个时钟周期!!!