1.双口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中的逻辑资源拼凑形成的。一般的原则是,较大的存储应用,建议用bram;零星的小ram,一般就用dram。
在Vivado中,RAM IP核在Memories & Strorage ElementsRAM & ROMs和RAM & ROMs & BRAM文件夹下,如图所示,下面简要介绍一下Vivado的双口RAM IP核。
(图1.1)
2.Vivado 双口RAM IP核
2.1 Block Memory Generator概述
点击图1.1的Block Memory Generator项,利用BRAM来构建双口RAM。Block Memory Generator窗口如图2.1所示。
图中,第1部分,在IP symbol选项卡,点击"+"号可以展开端口具体信号,如图2.2所示。第2部分,Component Name可以设置IP核的名字。第3部分,Basic选项卡,在Memory Type下拉列表中,可以设置内存的类型,如图2.3所示。Block Memory Gnerator一共可以产生5种不同类型的内存空间,其中block RAM有三种:单口RAM、简化双口RAM和真双口RAM[]。单口RAM只有一个端口(A端口),可以对A端口进行读写。简化双口RAM有两个端口(A和B端口),但是A端口只能进行写入操作,不能进行读出操作,而B端口则只能进行读出操作,不能进行写入操作。真双口RAM有两个端口(A和B端口),A和B端口都能进行读写操作[]。
(图2.1)
(图2.2)
(图2.3)
2.2 真双口RAM的设置
2.2.1 Basic设置
在Basic选项卡的Memory type选项中选择真双口RAM,IP Symbol如图2.4所示。ECC Options为默认设置,Write Enable中也选择默认设置,不使能字节写,Algorithm Options选择默认设置。
(图2.4)
2.2.2 Port设置
点击Port A Options选项卡,对A端口进行设置, 设置Write Width为16(即RAM单元为16位),Write Width为1024(即内存深度为1024,该端口可读写的RAM单元有1024个),Operating Mode(操作模式)一共有三种:Write First,Read First,No Change。在Write First模式中,在一个时钟周期里,写入内存单元的数据被同步输出到输出数据总线上;在Read First模式中,在一个时钟周期里,写入到内存单元的数据是当前输入数据总线上的数据,而输出到输出数据总线上的数据则是上一个时钟周期存储在内存单元中的数据。细节可参考PG058的49到50页4。Enable Port Type设置为Always Enabled,一直使能端口A。其它设置使用默认设置。如图2.5所示。
(图2.5)
端口B设置为与A一致。在Other Options选项卡中,保留默认设置。Load Init File设置是否用Coe文件对内存区域初始化,这个在初始化ROM的时候会用到,这里不勾选,保持默认。最后,在Summary选项卡会显示消耗的资源。
3.双口RAM例程
例程1,该例程是Altera官方例程[],采用寄存器构建双口RAM,代码如下:
moduletrue_dpram_sclk
(
input [7:0] data_a, data_b,
input [5:0] addr_a, addr_b,
input we_a, we_b, clk,
outputreg [7:0] q_a, q_b
);
// Declare the RAM variable
reg [7:0] ram[63:0];
// Port A
always @ (posedge clk)
begin
if (we_a)
begin
ram[addr_a] <= data_a;
q_a <= data_a;
end
else
begin
q_a <= ram[addr_a];
end
end
// Port B
always @ (posedge clk)
begin
if (we_b)
begin
ram[addr_b] <= data_b;
q_b <= data_b;
end
else
begin
q_b <= ram[addr_b];
end
end
endmodule
例程2,该例程是Xilinx官方例程[],采用寄存器构建真双口RAM,代码如下:
// Dual-Port Block RAM with Two Write Ports
// File: rams_16.v
modulev_rams_16 (clka,clkb,ena,enb,wea,web,addra,addrb,dia,dib,doa,dob);
input clka,clkb,ena,enb,wea,web;
input [9:0] addra,addrb;
input [15:0] dia,dib;
output [15:0] doa,dob;
reg[15:0] ram [1023:0];
reg[15:0] doa,dob;
always @(posedge clka) beginif (ena)
begin
if (wea)
ram[addra] <= dia;
doa <= ram[addra];
end
end
always @(posedge clkb) beginif (enb)
begin
if (web)
ram[addrb] <= dib;
dob <= ram[addrb];
end
end
endmodule
例程3,该例程是网友博客中的例程[],代码如下:
moduleTOP(
input USER_CLK
);
`define DLY #1
reg FPGA_Enable=0;
reg[3:0] FPGA_Write_Enable=4'h0;
reg[31:0] FPGA_Address=0;
reg[31:0] FPGA_Write_Data=0;
reg[31:0] FPGA_Read_Data_reg=0;
wire[31:0] FPGA_Read_Data;
reg[10:0] count=0;
always @ (posedge USER_CLK)
begin
count <= count +1;
if(count<=100)
begin
FPGA_Enable <=0;
FPGA_Write_Enable <=4'h0;
end
elseif((count <=105)&&(count >100))
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=4'hf;
FPGA_Address <= FPGA_Address +4;
FPGA_Write_Data <= FPGA_Write_Data +1;
end
elseif((count <=110)&&(count >105))
begin
FPGA_Enable <=0;
FPGA_Write_Enable <=4'h0;
FPGA_Address <=0;
FPGA_Write_Data <=0;
end
elseif((count <=117)&&(count >110))
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=4'h0;
FPGA_Read_Data_reg <= FPGA_Read_Data;
FPGA_Address <= FPGA_Address +4;
end
elseif(count ==118)
begin
FPGA_Enable <=0;
count <= count;
end
end
BBBByour_instance_name (
.clka(USER_CLK), // input clka
.ena(FPGA_Enable), // input ena
.wea(FPGA_Write_Enable), // input [3 : 0] wea
.addra(FPGA_Address), // input [31 : 0] addra
.dina(FPGA_Write_Data), // input [31 : 0] dina
.douta(FPGA_Read_Data), // output [31 : 0] douta
.clkb(clkb), // input clkb
.enb(enb), // input enb
.web(web), // input [3 : 0] web
.addrb(addrb), // input [31 : 0] addrb
.dinb(dinb), // input [31 : 0] dinb
.doutb(doutb) // output [31 : 0] doutb
);
endmodule
该例程中,在count为101(>100)后开始往地址4到20写入1-5,然后在count为111(>110)的时候读出写入的数据。
4.仿真
下面利用Modelsim和Vivado进行联合仿真,关于vivado如何与modelsim进行联合仿真可以参考这篇文章:
vivado与modelsim的关联以及器件库编译
有一点要注意的是,我用的是Vivado2017.1版本,这个版本只支持Modelsim10.5及以上的版本,如果是低版本的Modelsim,在用Vivado2017.1编译Modelsim的仿真库时,会出错。Modelsim10.5版本可以在这里下载:
modelsim 10.5 适用vivado 2017.1
用Modelsim仿真时,会在sim_1/behav文件夹下产生3个.do文件,分别是xx_compile.do,xx_simulate.do,xx _wave.do文件。在设计的verilog文件修改之后,如果在Modelsim中直接restart,仿真的其实还是没有修改前的文件,要使修改的.v文件在Modelsim中生效,可以在Modelsim的命令窗口输入do xx_compile.do文件,对仿真的库文件以及设计文件(.v文件)重新编译,然后在输入do xx_simulate.do文件,才能仿真修改后的文件。输入do xx_compile.do命令对设计文件重新编译的时候,Modelsim会强制退出,这时由最后一句force quit命令引起的,只要把它删掉就行了。如果要保存波形文件,可以save format,另存为xx_wave.do文件。
参考上面双口RAM的例程3进行功能仿真,RAM IP使用Write First模式,设计文件代码如下:
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2017/12/09 22:36:48
// Design Name:
// Module Name: dual_port_ram_demo
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
moduledual_port_ram_demo(
input USER_CLK
);
`define DLY #1
//Port A declaration
reg FPGA_Enable=0;
reg FPGA_Write_Enable=0;
reg[31:0] FPGA_Address=0;
reg[31:0] FPGA_Write_Data=0;
reg[31:0] FPGA_Read_Data_reg=0;
wire[31:0] FPGA_Read_Data;
//Port B declaration
reg enb=0;
reg[3:0] web=4'h0;
reg[31:0] addrb=0;
reg[31:0] dinb=0;
reg[31:0] doutb_reg=0;
wire[31:0] doutb=0;
reg[10:0] count=0;
always @ (posedge USER_CLK)
begin
count <= count +1;
if(count<=100)
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=0;
end
elseif((count <=105)&&(count >100))
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=1;
FPGA_Address <= FPGA_Address +4;
FPGA_Write_Data <= FPGA_Write_Data +1;
end
elseif((count <=110)&&(count >105))
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=0;
FPGA_Address <=0;
FPGA_Write_Data <=0;
end
elseif((count <=117)&&(count >110))
begin
FPGA_Enable <=1;
FPGA_Write_Enable <=1;
FPGA_Read_Data_reg <= FPGA_Read_Data;
FPGA_Address <= FPGA_Address +4;
end
elseif(count ==118)
begin
FPGA_Enable <=0;
count <= count;
end
end
dpRAMu1 (
.clka(USER_CLK), // input clka
.ena(FPGA_Enable), // input ena
.wea(FPGA_Write_Enable), // input [3 : 0] wea
.addra(FPGA_Address), // input [31 : 0] addra
.dina(FPGA_Write_Data), // input [31 : 0] dina
.douta(FPGA_Read_Data), // output [31 : 0] douta
.clkb(USER_CLK), // input clkb
.enb(enb), // input enb
.web(web), // input [3 : 0] web
.addrb(addrb), // input [31 : 0] addrb
.dinb(dinb), // input [31 : 0] dinb
.doutb(doutb) // output [31 : 0] doutb
);
endmodule
testbench文件如下:
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2017/12/09 22:47:26
// Design Name:
// Module Name: simu
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
modulesimu(
);
//testbench 时钟信号
reg clk =0;
always # 10 clk <=~clk;
//调用dual_port_ram_demo模块
dual_port_ram_demodemo1(clk);
endmodule
仿真结果如下:
(图4.1)
程序在1时刻准备好地址和要写入RAM的数据,在2时刻写入RAM中,在3时刻端口才会输出2时刻写入RAM的数据,注意与PG058的图稍有不同。
(图4.2)
4.后记
关于BRAM,推荐一个youtube视频,里面讲的非常清晰易懂。
What is a Block RAM in an FPGA?
5.参考文献
--------------------- 本文来自 UCASers 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/zengaliang/article/details/78765159?utm_source=copy
Vivado综合可以理解多种多样的RAM编写方式,将其映射到
分布式RAM或
块RAM中。两种实现方法在向RAM写入数据时都是采取同步方式,区别在于从RAM读取数据时,分布式RAM采用异步方式,块RAM采用同步方式。使用RAM_STYLE属性可以强制规定使用哪种方法实现RAM。
Xilinx FPGA的内存接口具有如下特性:
- 支持任意大小的深度和数据宽度(综合时会使用一个或多个RAM原语实现);
- 单端、伪双端、真双端三种模式;
- 最多可以使用两个写端口;
- 可以存在多个读端口;
- 支持写使能信号
- 块RAM支持RAM使能、数据输出复位、可选的输出寄存器和字节写使能;
- 每个RAM端口可以由独立的时钟、端口使能、写使能和数据输出复位控制;
- 按设定的值初始化RAM内容;
- Vivado综合可以将校验位作为常规数据位,以适应描述的数据宽度。
另外目前最新的UltraScale架构的FPGA中还有专用的UltraRAM资源,这里不做过多介绍。下面给出几个各种实现方式的Verilog示例代码。
分布式RAM
下面给出一个异步读模式的双口分布式RAM的示例:
// 异步读模式的双口分布式RAM
module rams_dist (
input clk, we,
input [5:0]a, dpra,
input [15:0]di,
output [15:0]spo, dpo
);
reg [15:0] ram [63:0];
always @(posedge clk)
if (we) ram[a] <= di;
assign spo = ram[a];
assign dpo = ram[dpra];
endmodule
单端块RAM
块RAM支持3种不同的读写同步模式,解决同时读写同一地址的情况,每一个读、写端口都可以配置为:
- Write-First模式:新内容载入时可以马上被读取;
- Read-First模式:新内容载入时,先读取旧的内容;
- No-Change模式:新内容载入时,不读取该地址的内容(即维持之前的值不变);
下面给出三种模式的单端块RAM Verilog示例代码:
// 数据输出可复位的单端块RAM,Read_first
module rams_sp_rf_rst (
input clk, en, we, rst,
input [9:0]addr,
input [15:0]di,
output reg [15:0]dout
);
reg [15:0] ram [1023:0];
always @(posedge clk)
if (en) begin //块RAM使能
if (we) ram[addr] <= di; //写使能
if (rst) dout <= 0; //输出复位
else dout <= ram[addr];
end
endmodule
// 写优先模式的单端块RAM,Wrist_first
module rams_sp_wf (
input clk, en, we,
input [9:0]addr,
input [15:0]di,
output reg [15:0]dout
);
reg [15:0] ram [1023:0];
always @(posedge clk)
if (en) begin
if (we) begin
RAM[addr] <= di;
dout <= di;
end
else dout <= RAM[addr];
end
endmodule
// No-Change模式的单端块RAM
module rams_sp_wf (
input clk, en, we,
input [9:0]addr,
input [15:0]di,
output reg [15:0]dout
);
reg [15:0] ram [1023:0];
always @(posedge clk)
if (en) begin
if (we) RAM[addr] <= di;
else dout <= RAM[addr];
end
endmodule
伪双端块RAM
下面给出伪双端块RAM的Verilog 示例代码:
// 单时钟控制,伪双端块RAM
module simple_dual_one_clock (
input clk,ena,enb,wea,
input [9:0]addra,addrb,
input [15:0]dia,
output reg [15:0] dob
);
reg [15:0] ram [1023:0];
always @(posedge clk) //写
if (ena)
if (wea) ram[addra] <= dia;
always @(posedge clk)
if (enb) dob <= ram[addrb]; //读
endmodule
// 双时钟控制,伪双端块RAM
module simple_dual_two_clocks (
input clka,clkb,ena,enb,wea,
input [9:0] addra,addrb,
input [15:0]dia,
output reg [15:0]dob
);
reg [15:0] ram [1023:0];
always @(posedge clka) //写
if (ena)
if (wea) ram[addra] <= dia;
always @(posedge clkb)
if (enb) dob <= ram[addrb]; //读
endmodule
真双端块RAM
下面给出两个真双端块RAM的Verilog 示例代码:
初始化RAM内容
除了上面介绍的一些基本模式外,Xilinx的RAM还包括字节使能模式、非对称RAM(即读写数据的位宽不同)、3D RAM,等后面用到时再做补充。下面介绍一下如何初始化RAM的内容。初始化工作可以在HDL源代码中进行,也可以利用外部数据文件设置。
比如Verilog中可以使用下面代码将RAM全部初始化为一个值:
reg [DATA_WIDTH-1:0] ram [DEPTH-1:0];
integer i;
initial for (i=0; i < DEPTH; i=i+1) ram[i] = 0;
或者使用Verilog中的文件读取函数从外部数据文件中获取RAM初始化数据。数据文件必须是ASCII文本文件,每一行表示RAM中的一个地址,因此文件的行数要与RAM的深度对应。文件中数据应用2进制或16进制表示,不能混合使用。除了数据外不能有其它任何内容(包括注释)。一个文件示例如下:
00001110110000011001111011000110
00101011001011010101001000100011
01110100010100011000011100001111
01000001010000100101001110010100
对应一个4*32bit的RAM初始化内容。通常将这种格式称之为
bit向量格式,与整数格式或hex格式作区别。Verilog代码中使用如下示例调用该文件:
reg [31:0] ram [0:3];
initial begin
$readmemb("ram.data", ram, 0, 3);
end
如果文件用16进制定义数据则应使用
$readmemh系统任务。下面给出一个直接在HDL中赋值初始化块RAM的完整Verilog示例:
module rams_sp_rom (
input clk, we,
input [5:0]addr,
input [19:0]di,
output reg [19:0] dout
);
reg [19:0] ram [63:0];
initial begin
ram[63] = 20'h0200A; ram[62] = 20'h00300; ram[61] = 20'h08101;
ram[60] = 20'h04000; ram[59] = 20'h08601; ram[58] = 20'h0233A;
ram[57] = 20'h00300; ram[56] = 20'h08602; ram[55] = 20'h02310;
ram[54] = 20'h0203B; ram[53] = 20'h08300; ram[52] = 20'h04002;
ram[51] = 20'h08201; ram[50] = 20'h00500; ram[49] = 20'h04001;
ram[48] = 20'h02500; ram[47] = 20'h00340; ram[46] = 20'h00241;
ram[45] = 20'h04002; ram[44] = 20'h08300; ram[43] = 20'h08201;
ram[42] = 20'h00500; ram[41] = 20'h08101; ram[40] = 20'h00602;
ram[39] = 20'h04003; ram[38] = 20'h0241E; ram[37] = 20'h00301;
ram[36] = 20'h00102; ram[35] = 20'h02122; ram[34] = 20'h02021;
ram[33] = 20'h00301; ram[32] = 20'h00102; ram[31] = 20'h02222;
ram[30] = 20'h04001; ram[29] = 20'h00342; ram[28] = 20'h0232B;
ram[27] = 20'h00900; ram[26] = 20'h00302; ram[25] = 20'h00102;
ram[24] = 20'h04002; ram[23] = 20'h00900; ram[22] = 20'h08201;
ram[21] = 20'h02023; ram[20] = 20'h00303; ram[19] = 20'h02433;
ram[18] = 20'h00301; ram[17] = 20'h04004; ram[16] = 20'h00301;
ram[15] = 20'h00102; ram[14] = 20'h02137; ram[13] = 20'h02036;
ram[12] = 20'h00301; ram[11] = 20'h00102; ram[10] = 20'h02237;
ram[9] = 20'h04004; ram[8] = 20'h00304; ram[7] = 20'h04040;
ram[6] = 20'h02500; ram[5] = 20'h02500; ram[4] = 20'h02500;
ram[3] = 20'h0030D; ram[2] = 20'h02341; ram[1] = 20'h08201;
ram[0] = 20'h0400D;
end
always @(posedge clk) begin
if (we)
ram[addr] <= di;
dout <= ram[addr];
end
endmodule
再给出一个用外部文件初始化块RAM的完整Verilog示例:
module rams_init_file (
input clk, we,
input [5:0]addr,
input [31:0]din,
output reg [31:0]dout
);
reg [31:0] ram [0:63];
initial begin
$readmemb("rams_init_file.data",ram);
end
always @(posedge clk) begin
if (we)
ram[addr] <= din;
dout <= ram[addr];
end
endmodule