Verilog设计实例(3)基于Verilog的单端口同步读写RAM设计
程序员文章站
2024-02-22 21:39:16
...
写在前面
为什么要写单端口同步读写RAM呢?
没有那么多为什么?就是因为简单、基础,能清晰说明单端口RAM的原理,顺手给出设计,也能说明你的设计基础,作为专题Verilog设计实例中的一员,单端口RAM必然上榜了!
曾经也写过很多类似的博文,在博客首页搜索即可,给出搜索链接:
好了,那我们开始正文吧。
正文
电路设计
设计代码:
`timescale 1ns / 1ps
////////////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: single_port_syn_ram
////////////////////////////////////////////////////////////
module single_port_syn_ram#(
parameter ADDR_WIDTH = 4,
parameter DATA_WIDTH = 16,
parameter DEPTH = 2**ADDR_WIDTH
)(
input i_clk,
input [ADDR_WIDTH - 1 : 0] addr,
inout [DATA_WIDTH - 1 : 0] data,
input cs,
input wr,
input oe
);
reg [DATA_WIDTH - 1 : 0] mem[0 : DEPTH - 1];
reg [DATA_WIDTH - 1 : 0] mid_data;
// write part
aaa@qq.com(posedge i_clk) begin
if(cs&wr) begin
mem[addr] <= data;
end
end
// read part
aaa@qq.com(posedge i_clk) begin
if(cs & !wr) begin
mid_data <= mem[addr];
end
end
assign data = (cs & oe & !wr)? mid_data: 'hz;
endmodule
代码简介
这段代码需要说明的就是这么一句:
reg [DATA_WIDTH - 1 : 0] mid_data;
//......
assign data = (cs & oe & !wr)? mid_data: 'hz;
由于RAM的数据总线是inout类型:
inout [DATA_WIDTH - 1 : 0] data,
由于才用的是同步读,因此读数据肯定是一个时序电路的行为,也即在always块内进行:
// read part
aaa@qq.com(posedge i_clk) begin
if(cs & !wr) begin
mid_data <= mem[addr];
end
end
inout 类型的端口,不能放入always块内,因此需要一个中间寄存器来过渡;当读使能oe有效时,读出数据放入中间寄存器mid_data中,之后在通过assign连接到inout端口上;如果读使能无效,则将高阻态赋值给inout端口,意味着这里不做处理(悬空),而在其他地方处理,例如作为输入数据写入RAM。
这是因为读使能无效时,应该处于写的状态,写的话需要将该端口总线上的数据data写入RAM:
// write part
aaa@qq.com(posedge i_clk) begin
if(cs&wr) begin
mem[addr] <= data;
end
end
此时这个端口作为输入使用的。
总之,这样的处理很关键!
行为仿真
测试文件可以写的很简单,也可以写的稍微复杂点,本文做简单测试:
Testbench设计:
`timescale 1ns / 1ps
////////////////////////////////////////////////////////////
// Engineer: Reborn Lee
// Module Name: ram_tb
////////////////////////////////////////////////////////////
module ram_tb(
);
parameter ADDR_WIDTH = 4;
parameter DATA_WIDTH = 16;
parameter DEPTH = 2**ADDR_WIDTH;
reg i_clk;
reg [ADDR_WIDTH - 1 : 0] addr;
wire [DATA_WIDTH - 1 : 0] data;
reg cs;
reg wr;
reg oe;
reg [DATA_WIDTH-1:0] tb_data;
//generate system clock
initial begin
i_clk = 0;
forever begin
# 5 i_clk = ~i_clk;
end
end
assign data = !oe ? tb_data : 'hz;
initial begin
{cs, wr, addr, tb_data, oe} = 0;
repeat (2) @ (posedge i_clk);
//write test
for (integer i = 0; i < 2**ADDR_WIDTH; i= i+1) begin
repeat (1) @(negedge i_clk) addr = i; wr = 1; cs =1; oe = 0; tb_data = $random;
end
//read test
repeat (2) @ (posedge i_clk);
for (integer i = 0; i < 2**ADDR_WIDTH; i= i+1) begin
repeat (1) @(posedge i_clk) addr = i; wr = 0; cs = 1; oe = 1;
end
#20 $finish;
end
single_port_syn_ram #(
.ADDR_WIDTH(ADDR_WIDTH),
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(DEPTH)
) inst_single_port_syn_ram (
.i_clk (i_clk),
.addr (addr),
.data (data),
.cs (cs),
.wr (wr),
.oe (oe)
);
endmodule
注意事项
- 第一:inout类型的端口,声明为wire;
wire [DATA_WIDTH - 1 : 0] data;
- 第二 :在写数据的时候,需要定义中间寄存器 tb_data;
reg [DATA_WIDTH-1:0] tb_data;
- 第三:对第二条的解释是,简单来说是因为initial是一个过程块,需要写的数据不可能直接赋值给wire类型的data,需要中间过渡:
assign data = !oe ? tb_data : 'hz;
- 第四:注意到下面这条语句了吗?在下降沿给数据以及给地址,这样可以保证数据在上升沿时刻写入RAM;且写入数据为随机数:
//write test
for (integer i = 0; i < 2**ADDR_WIDTH; i= i+1) begin
repeat (1) @(negedge i_clk) addr = i; wr = 1; cs =1; oe = 0; tb_data = $random;
end
- 第五:大家一定要学会这种测试文件的写法,严格控制写入多少个周期或数据,以及读出多少个数据。
仿真波形
写有效:
读数据:
交个朋友
个人微信公众号:FPGA LAB,二维码,左下角;
知乎:李锐博恩,二维码右下角。
上一篇: JS 比较常用的几个特性