1、实验目的
了解Compact系列的RAM IP的使用以及配置的方法。
2、实验原理
2.1、RAM介绍
RAM即随机存取存储器。它可以在运行过程中把数据写进任意地址,也可以把数据从任意地址中读出。其作用可以拿来做数据缓存,也可以跨时钟,也可以存放算法中间的运算结果等。
注意:PDS的IP配置工具中提供两种不同的RAM,一种是Distributed RAM(分布式RAM)另一种是DRM Based RAM,分布式RAM用的是LUT(查找表)资源去构成的RAM,这种RAM会消耗大量LUT资源,因此通常在一些比较小的存储才会用到这种RAM,以节省DRM资源。而DRM Based RAM是利用片内的DRM资源去构成的RAM,不占用逻辑资源,而且速度快,通常设计中均使用DRM Based RAM。
RAM分为三种,如下表所示:
注意:当使用真双端口时,要避免出现同时读写同个地址,这会造成写入失败,在逻辑设计上需要避开这个情况。
2.2、IP配置
以下给出比较常用的RAM的配置作为介绍,通常我们比较常用伪双端口RAM来设计。
首先点击快捷工具栏的 IP 图标,进入IP例化设置
然后在IP目录处选择DRM Based Simple Dual Port RAM,在Instance name处为本次实例化的IP取一个名字,接着点击Customise进入IP配置页面。操作示意图如下:
接下来是IP配置页面,本实验只需要在这个界面简单的设置读写数据深度 5 和数据位宽 8 ,其他设置保持默认即可。
此页面选项卡功能在ip手册有详细说明,具体如下图:
如设计需要用到其他功能可参考用户手册进行配置。
注意:如果勾选Enable Output Register(输出寄存),输出数据会延迟一个时钟周期。具体每个端口的含义这里参考官方手册,大家也可以自行查看IP手册,如下图所示:
2.3、RAM的读写时序
配置成不同模式的时候,RAM的读写时序是不一样的,真双端口和单端口的RAM配置均有三种模式,而伪双端口只有一种。由于真双端口和单端口的配置是一样的,这里以真双端口为例子。分为NORMAL_WRITE(正常模式)、TRANSPARENT_WRITE(直写)、READ_BEFORE_WRITE(读优先模式)三种模式。
而伪双端口不属于上面三种模式,有它独特的模式。这几种模式的差异就在于读写时序的不同,接下来,我们来分析读写时序。
以下时序图均来自官方IP手册,并且均未使能输出寄存。注意wr_en为1时表示写数据,为0表示读数据。
(1)NORMAL_WRITE
在NORMAL_WRITE这种模式下,可以看到,当时钟的上升沿到来,且clk_en和wr_en均为高电平时,就会把数据写到对应的地址里面,如图中的1时刻。然后看读数据端口,当wr_en不为0的时候,a_rd_data一直为Don’t Care状态,而当时钟上升沿到来,且clk_en为高电平,wr_en为低电平时,a_rd_data输出当前a_addr里的数据,即Mem(ADDR1)和ADDR0里的D0。
(2)READ_BEFORE_WRITE
在READ_BEFORE_WRITE这种模式下,可以看到在1的时刻,时钟上升沿到来,且clk_en和wr_en均为高电平,D0写进了ADDR0里面,但是注意看此时的a_rd_data和a_addr,可以发现,此时a_wr_en并不为0,可a_rd_data还是输出了上一刻ADDR0的数据(因为不是输出D0)。之后,a_wr_en拉低,此时才是读数据,在3时刻,把ADDR0的数据读出来,a_rd_data才输出了D0。
所以总结一下,这个模式其实就是进行写操作时,读端口会把当前写的地址的原始数据输出,因此叫读优先模式很合情合理对吧,顾名思义,就是我优先把原来的数据读出来。
(3)Transparent_Write
在Transparent_Write这种模式下,可以看到在1的时刻,时钟上升沿到来,且clk_en和wr_en均为高电平,D0写进了ADDR0里面,但是注意看此时的a_rd_data和a_addr,可以发现,此时a_wr_en并不为0,可a_rd_data居然直接输出了D0,之后a_wr_en拉低,进入读状态,在2时刻,再一次把ADDR0的数据读出来,输出了D0。
分析总结一下,根据1时刻的情况,我们可以得出结论,在这种模式下,当我们进行写操作时,读端口会马上输出我们写入的数据。所以叫直写模式。
伪双端口的读写时序:
注意:wr_en为1时是写操作,为0是读操作。
伪双端口的读写时序与上面三种都不同,我们看图时序来分析:
注意:看1时刻,此时wr_en和wr_clk_en均为高电平,所以是写操作,所以1时刻就是往地址ADDR0里写入D0,注意此时的rd_addr和rd_data,可以看到这一时刻rd_addr是ADDR2,然后进行写操作时,rd_data同样输出了ADDR2里的数据,而此时wr_en还是高电平。接下来看2和3时刻,此时wr_en为0,rd_clk_en是高电平,所以是读操作,此时分别读出ADDR1和ADDR0里的数据,之后rd_clk_en变成低电平,读时钟无效,可以看到rd_data保持D0输出。
分析总结一下,主要是1时刻,大家可以看到1时刻往ADDR0写入了D0,读端口却输出了ADDR2中的数据。仔细观察可以得出结论:伪双端口RAM在进行写操作的时候,会把当前读端口指向的地址的数据输出。是不是有点像直写?只不过直写是输出写入的数据,而伪双端口是输出读端口指向的地址的数据。
3、代码设计
模块顶层端口列表如下:
模块顶层代码如下:
module ram_test_top
(
input wire wr_clk ,//写时钟
input wire rd_clk ,//读时钟
input wire rst_n ,//复位
input wire rw_en ,//读写使能信号
input wire [4:0] wr_addr ,//写地址
input wire [4:0] rd_addr ,//读地址
input wire [7:0] wr_data ,//写数据
output wire [7:0] rd_data //读数据
);
ram_test ram_test_inst (
.wr_data(wr_data), // input [7:0]
.wr_addr(wr_addr), // input [4:0]
.wr_en(rw_en), // input
.wr_clk(wr_clk), // input
.wr_rst(~rst_n), // input
.rd_addr(rd_addr), // input [4:0]
.rd_data(rd_data), // output [7:0]
.rd_clk(rd_clk), // input
.rd_rst(~rst_n) // input
);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
该模块功能是例化ram ip并将相关信号在顶层引出。
测试代码如下:
`timescale 1ns/1ns
module ram_test_tb();
reg sys_clk;
reg rd_clk ;
reg rst_n;
reg rw_en; //读写使能信号
reg [7:0] wr_data;
reg [4:0] wr_addr;
reg [4:0] rd_addr;
wire [7:0] rd_data;
reg [1:0] state;
initial
begin
rst_n <= 1'd0;
sys_clk <= 1'd0;
rd_clk <= 1'd0;
#20
rst_n <= 1'd1;
end
//读写控制
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
begin
state <= 2'd0;
wr_data <= 8'd0;
rw_en <= 1'd0;
wr_addr <= 8'd0;
rd_addr <= 8'd1;
end
else
begin
case(state)
2'd0:begin
rw_en <= 1'd1;
state <= 2'd1;
end
2'd1:begin
if(wr_addr == 5'd31) //32个数据
begin
rw_en <= #2 1'd0;
state <= #2 2'd2;
wr_data <= #2 8'd0;
wr_addr <= #2 5'd0;
rd_addr <= #2 5'd0;
end
else
begin
state <= #2 2'd1;
wr_data <= #2 wr_data+1'b1;
//rd_addr <= #2 rd_addr+1'b1;
wr_addr <= #2 wr_addr+1'b1;
end
end
2'd2:begin
if(rd_addr == 5'd31)//读出32个
begin
state <= #2 2'd3;
rd_addr <= #2 5'd0;
end
else
begin
state <= #2 2'd2;
rd_addr <= #2 rd_addr+1'b1;
end
end
2'd3:begin
state <= 2'd0;
end
default: state <= 2'd0;
endcase
end
end
//50MHZ
always#10 sys_clk = ~sys_clk;
//
GTP_GRS GRS_INST(
.GRS_N(1'b1)
) ;
ram_test_top u_ram_test_top(
.wr_clk ( sys_clk ),
.rd_clk ( sys_clk ),
.rst_n ( rst_n ),
.rw_en ( rw_en ),
.wr_addr ( wr_addr ),
.rd_addr ( rd_addr ),
.wr_data ( wr_data ),
.rd_data ( rd_data )
);
//test posedge
reg flag;
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
flag <= 1'd0;
else
flag <= 1'd1;
end
reg test;
always@(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
test <= 1'd0;
else if(flag)
test <= 1'b1;
end
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
从代码的27行到80行是ram的读写控制状态机。主要用来控制读写地址的生成和使能以及写入的数据。这里只讲解主要实现的功能。
首先代码的39-42行,也就是state=0的时候,拉高rw_en,并跳转到状态1,此时进入写操作(没有使能clk_en,可以不管),下个时钟周期开始写入数据(注意是时序逻辑,边沿采样,所以是下个时钟周期才开始写数据),即state=1的时候是一直在往ram里面写数据,在代码的44到60行就是写操作了,可以看到,当wr_addr不等于31的时候,wr_data和wr_addr不断加1(rd_addr这里+1,主要为了验证伪双端口的时序),当wr_addr等于31的时候,在下个时钟周期把数据清0,状态跳转,在当前时钟周期下还会再往地址31里面写入数据,所以在该时钟周期,一共写入了32个数据(从地址0写到地址31)。即状态1完成写入32个数据后跳转到state=2的逻辑。
代码的45-61行,也就是state=2的时候,在每个周期的上升沿让rd_addr不断累加,直到rd_addr=31的时候,在下个时钟周期清空地址并让状态跳转的操作,而在当前时钟周期会继续把地址31的数据读出来,完成读取地址0-31的数据,一共32个数据,所以该状态主要完成读取32个数据,然后在下个时钟周期就跳转到state=3。state=3可以看到其主要作用就是等待一个时钟周期,然后跳转回去state=0下,起到一个延时作用。
可以总结出一句话就是时序逻辑的赋值总在下一个时钟周期才生效。所以在rd_addr=31时执行的操作要在下一个时钟周期才会被采样生效。所以当前时钟还是会再从RAM读出一个数据。
4、实验现象
右键仿真的文件,选择Run Behavior Simulation开始行为仿真。启动Modelsim进行仿真:
我们截取一部分波形来查看ram读写时序:
当state为0时,将读写使能rw_en拉高,进行数据的写入操作,结合波形我们可以看到rw_en拉高同时state变为1状态,对ram写入0~31共32个数据。
当写入第32个数据之后rw_en拉低,同时state跳转到2状态,对ram中的数据进行读取。当读取完32个数据之后state跳转到3状态再跳转到0状态(因为数据是在下一个时钟周期才会读出,所以这里用state跳转的形式等待一个时钟周期便于观察),如此往复。