使用Verilog实现RAM的构造并读写数据
目的
1模拟实现一个宽度为32,深度为256的内存空间,先向内存空间写一批数据,再读出这批数据,并比较数据是否正确;
2完成ram的实现代码和tb_ram的仿真测试代码;
说明
提供输入: clk_i 时钟信号,100MHz;
rst_i 复位信号,高电平复位;
wr_en_i 写使能,表示data_io的数据有效;
rd_en_i 读使能,表示要求读出内存中的数据;
addr_i 地址信号表示要访问的数据所在地址;
要求 输出: data_io 当wr_en_i有效时,表示为写入到内存的数据;当rd_en_i有效时,表示要求读出在addr_i的内存数据;
实现
1 top模块
只需要定义好两个子模块之间的连线即可,不做过多描述。Verilog描述如下:
//tb_top
module tb_top;
wire clk;
wire rst;
wire wr_en;
wire rd_en;
wire[7:0] addr;
wire[31:0] data;
ram inst_ram(
.clk_i(clk),
.rst_i(rst),
.wr_en_i(wr_en),
.rd_en_i(rd_en),
.addr_i(addr),
.data_io(data)
);
tb_ram inst_tb_ram(
.clk_o(clk),
.rst_o(rst),
.wr_en_o(wr_en),
.rd_en_o(rd_en),
.addr_o(addr),
.data_io(data)
);
Endmodule
2 ram模块
使用双向口的话需要利用三态门进行输入输出的控制。使用条件操作符实现三态门的构造。在时钟上升沿,若写信号有效,则将当前地址线对应存储器的空间存入当前data_io上的数据;若读信号有效,则将地址线对应存储器空间的数据输出至data_io,读写无效时为高阻态。编写代码如下,部分说明包含在注释中:
//ram.v
module ram(
input clk_i,
input rst_i,
input wr_en_i,
input rd_en_i,
input [7:0] addr_i,
inout [31:0] data_io
);
reg [31:0] bram[255:0];
integer i;
reg [31:0] data;
//add implementation code here
always @(posedge clk_i or posedge rst_i)
begin
if (rst_i)
begin
for(i=0;i<=255;i=i+1) //reset, 按字操作
bram[i] <= 32'b0;
end
else if (wr_en_i) begin
bram[addr_i] <= data_io;
end
else if (rd_en_i) begin
data <= bram[addr_i];
end
else begin
data <= 32'bz; //读写均无效时,为高阻态。若不加此句,时序会出现问题
end
end
assign data_io = rd_en_i? data : 32'bz; //三态门实现
endmodule
3 tb_ram模块
根据设计的ram,需要激励大致要遵循以下几个原则:
(1)高电平复位清空数据
(2)先给地址和数据,再给读写信号
(3)同样采用三态门结构对应与ram相连的inout线(inout端口不能单独存在)
(4)采用task简化激励组合
Verilog描述如下:
`timescale 1ns / 1ps //1 period of clk is 10ns
module tb_ram (
output reg clk_o,
output reg rst_o,
output reg wr_en_o,
output reg rd_en_o,
output reg [7:0] addr_o,
inout [31:0] data_io
);
reg[31:0] WriteRAM;
//三态门的需要不能直接输出至data_io,使用WriteRAM实现缓存
initial begin
$monitor($time,,, //监视data_io数据线
"Data = %d", data_io);
WriteRAM= 0;
clk_o = 0;
wr_en_o = 0;
rd_en_o = 0;
addr_o = 0;
rst_o = 1;
#20 rst_o = 0;
//为方便查看,使写入的数据为地址的5倍
write(50,250);
write(100,500); //地址100,写入500,下同
write(200,1000);
write (250,1250);
//读取数据
read(50);
read(100);
read(200);
read(250);
end
assign data_io = wr_en_o ? WriteRAM : 32'bz;
always begin
#5 clk_o = ~clk_o; //提供100MHz时钟
end
//0 < addr <255, 0 < WriteRAM <2^32 - 1
task write(
input[7:0] x_i,
input[31:0] y_i
);
begin
#100; //拉开每次读写之间的间隔,简化initial内容
@(posedge clk_o); //时钟控制
addr_o = x_i;
WriteRAM = y_i;
#1 //先给出地址和数据,再使能
wr_en_o = 1'b1;
@(posedge clk_o);
#50 //拉长有效时间便于波形显示
wr_en_o = 1'b0;
end
endtask
task read( //读操作只需给出地址参数
input[7:0] x_i
);
begin
#100;
@(posedge clk_o);
addr_o = x_i;
#1
rd_en_o = 1'b1;
@(posedge clk_o);
#50
rd_en_o = 1'b0;
end
endtask
endmodule
仿真验证
写好三个文件之后,打开modelsim将它们添加到一个工程中,全部编译,然后选择tb_top文件进行仿真(去掉Optimization)
再右键最顶层的选项添加到波形,仿真时间2us,得到图像如下:
图像从上至下依次为clk时钟,rst复位信号,wr_en写使能,rd_en读使能,addr地址线,data双向数据线。
下面开始具体分析每一次读写。总共四次写入,四次读取。
为了检验方便,使输入的数据为对应地址的5倍。
写入数据的操作如下图所示:
读取数据的部分:
读取数据与输入数据吻合,但输出要比读信号延后一点。
接下来说明延后的原因:
由于采用三态门形式输出,所以需要data作为中继缓冲。造成了总线输出要在读信号给出后一个时钟周期后才响应。
读信号到来时,data更新为将要输出的值(即对应地址的数据),在下一个clk上升沿到来之前,还不能输出更新的值。这段时间总线的输出由读信号到来时刻前data的值决定。由于代码
else begin
data <= 32'bz; //读写均无效时,为高阻态。
所以期间总线的输出仍未高阻态。
监视得到双向数据线上的数据变化如图,对应波形,四次输入,四次输出,数据相符。(时间单位为微秒)
可见RAM功能实现时序波形正确。
注:
- 因为地址空间较大,最好用task来实现testbench的编写,可使仿真激励清晰明了。
- 在testbench中加入打印输出,可用display,这样就可以直接看到输出。
- 为方便观察结果,我让写入某地址的数据内容与地址addr存在一定关系。
- 读写均无效时输出为高阻态,应该使用三态门。
5.代码中注释,工程目录不能含有中文。
6.使用modelsim仿真时应选择最顶层的文件进行仿真。博主曾经因为仿真错了文件浪费了好几天时间……
7..inout接口使用三态门结构实现,当不向外输出时置为高阻态,此时inout的值由外界输入决定。需要输出时连接内部输出缓存,以实现inout功能。
上一篇: Filebeat 模块与配置
推荐阅读
-
Excel使用条件定位实现复制筛选数据中的可见单元格并粘贴
-
ES6使用Set数据结构实现数组的交集、并集、差集功能示例
-
使用jquery中封装的$.Ajax()进行异步通信并使用json方式实现数据传输
-
使用Verilog实现RAM的构造并读写数据
-
android硬编码h264数据,并使用rtp推送数据流,实现一个简单的直播-MediaCodec(二)
-
ES6使用Set数据结构实现数组的交集、并集、差集功能示例
-
Excel使用条件定位实现复制筛选数据中的可见单元格并粘贴
-
android硬编码h264数据,并使用rtp推送数据流,实现一个简单的直播-MediaCodec
-
使用logical standby技术实现Oracle数据库的读写分离
-
ES6使用Set数据结构实现数组的交集、并集、差集功能示例