单端口RAM的Verilog设计(同步读、同步写)
在学习最近的过程中,需要用到单端口RAM来存储我们设计过程中产生的数据,我们可以调用vivado内部的IP核,也可以在自己用Verilog写一个单端口RAM。原理在这里不进行讲解。
一、例程一
1.1 顶层代码
module single_port_ram
(
input [7:0] data,
input [5:0] addr,
input we, clk,
output [7:0] q
);
// Declare the RAM variable
reg [7:0] ram[63:0];
// Variable to hold the registered read address
reg [5:0] addr_reg;
always @ (posedge clk)
begin
// Write
if (we)
ram[addr] <= data;
addr_reg <= addr;
end
// Continuous assignment implies read returns NEW data.
// This is the natural behavior of the TriMatrix memory
// blocks in Single Port mode.
assign q = ram[addr_reg];
endmodule
1.2 TestBench
这里简单写个TestBench对其进行仿真。
`timescale 1ns / 1ps
module tb_single_port_ram(
);
reg clk;
reg [7 : 0] address;
reg we;
reg [7 : 0] data_in;
wire [7 : 0] data_out;
initial begin
clk = 0;
forever
#10 clk = ~clk;
end
initial begin
we <= 1'b0;
address <= 8'd0;
data_in <= 8'h00;
#10
#20
we <= 1'b1;
address <= 8'd32;
data_in <= 8'd12;
#60
we <= 1'b1;
address <= 8'd39;
data_in <= 8'd77;
#40
we <= 1'b0;
address <= 8'd32;
#80
we <= 1'b0;
address <= 8'd39;
#20
we <= 1'b0;
address <= 8'd30;
end
single_port_ram single_port_ram_inst
(
.data(data_in),
.addr(address),
.we (we),
.clk (clk),
.q (data_out)
);
endmodule
1.3 仿真结果
仿真结果如下:
二、例程二
1.1 顶层代码
参考二:CSDN博主给出的《单端口RAM的设计(同步读、同步写)》
其顶层代码如下:
`timescale 1ns / 1ps
module ram_sp_sr_sw (
clk , // Clock Input
address , // Address Input
data , // Data bi-directional
cs , // Chip Select
we , // Write Enable/Read Enable
oe // Output Enable
);
parameter DATA_WIDTH = 8 ;
parameter ADDR_WIDTH = 8 ;
parameter RAM_DEPTH = 1 << ADDR_WIDTH;
//--------------Input Ports-----------------------
input clk ;
input [ADDR_WIDTH-1:0] address ;
input cs ;
input we ;
input oe ;
//--------------Inout Ports-----------------------
inout [DATA_WIDTH-1:0] data ;
//--------------Internal variables----------------
reg [DATA_WIDTH-1:0] data_out ;
reg [DATA_WIDTH-1:0] mem [0:RAM_DEPTH-1];
//initialization
// synopsys_translate_off
integer i;
initial begin
for(i=0; i < RAM_DEPTH; i = i + 1) begin
mem[i] = 8'h00;
end
end
// synopsys_translate_on
//--------------Code Starts Here------------------
// Tri-State Buffer control
// output : When we = 0, oe = 1, cs = 1
assign data = (cs && oe && !we) ? data_out : 8'bz;
// Memory Write Block
// Write Operation : When we = 1, cs = 1
always @ (posedge clk)
begin : MEM_WRITE
if ( cs && we ) begin
mem[address] <= data;
end
end
// Memory Read Block
// Read Operation : When we = 0, oe = 1, cs = 1
always @ (posedge clk)
begin : MEM_READ
if (cs && !we && oe) begin
data_out <= mem[address];
end
end
endmodule
1.2 TestBench
`timescale 1ns / 1ps
module tb_single_port_ram(
);
reg clk; // Clock Input
reg [7 : 0] address; // address Input
wire [7 : 0] data; // Data bi-directional
reg cs; // Chip Select
reg we; // Write Enable/Read Enable
reg oe; // Output Enable
reg [7 : 0] data_in;
assign data = (cs && we && !oe) ? data_in : 'dz;
integer i;
initial begin
clk = 0;
forever
#10 clk = ~clk;
end
initial begin
cs <= 1'b0;
we <= 1'b0;
oe <= 1'b0;
address <= 8'd0;
data_in <= 8'h00;
#10
#20
cs <= 1'b1;
we <= 1'b1;
oe <= 1'b0;
address <= 8'd32;
data_in <= 8'd12;
#60
cs <= 1'b1;
we <= 1'b1;
oe <= 1'b0;
address <= 8'd39;
data_in <= 8'd77;
#40
cs <= 1'b0;
#40
cs <= 1'b1;
we <= 1'b0;
oe <= 1'b1;
address <= 8'd32;
#20
oe <= 1'b0;
cs <= 1'b0;
#80
cs <= 1'b0;
oe <= 1'b0;
#80
cs <= 1'b1;
we <= 1'b0;
oe <= 1'b1;
address <= 8'd39;
#20
cs <= 1'b1;
we <= 1'b0;
oe <= 1'b1;
address <= 8'd30;
end
single_port_ram u_ram(
.clk(clk) , // Clock Input
.address(address) , // address Input
.data(data) , // Data bi-directional
.cs(cs) , // Chip Select
.we(we) , // Write Enable/Read Enable
.oe(oe) // Output Enable
);
1.3 仿真结果
这里仿真后结果如下,写数据是正常的,但是读数据会出现读出数据是高阻态的情况。
分析过后,发现原因是由于读数据是由oe进行控制的,data_out始终晚于读数据控制信号oe一拍,但是oe又作为data的输出条件,所以这时取值必然导致读出的值不是我们想要的值,所以这里有两个办法解决上诉问题:
1.4 修改版
1.4.1 方法一
修改读使能信号oe、读地址address、片选信号cs、写使能信号we均为阻塞赋值,且这四个的输入时间完全一致,即将testbench修改如下:
`timescale 1ns / 1ps
module tb_single_port_ram(
);
reg clk; // Clock Input
reg [7 : 0] address; // address Input
wire [7 : 0] data; // Data bi-directional
reg cs; // Chip Select
reg we; // Write Enable/Read Enable
reg oe; // Output Enable
reg [7 : 0] data_in;
assign data = (cs && we && !oe) ? data_in : 'dz;
integer i;
initial begin
clk = 0;
forever
#10 clk = ~clk;
end
initial begin
cs = 1'b0;
we = 1'b0;
oe = 1'b0;
address = 8'd0;
data_in = 8'h00;
#10
#20
cs = 1'b1;
we = 1'b1;
oe = 1'b0;
address = 8'd32;
data_in = 8'd12;
#60
cs = 1'b1;
we = 1'b1;
oe = 1'b0;
address = 8'd39;
data_in = 8'd77;
#40
cs = 1'b0;
#40
cs = 1'b1;
we = 1'b0;
oe = 1'b1;
address = 8'd32;
#20
oe = 1'b0;
cs = 1'b0;
#80
cs = 1'b0;
oe = 1'b0;
#80
cs = 1'b1;
we = 1'b0;
oe = 1'b1;
address = 8'd39;
#20
cs = 1'b1;
we = 1'b0;
oe = 1'b1;
address = 8'd30;
end
single_port_ram u_ram(
.clk(clk) , // Clock Input
.address(address) , // address Input
.data(data) , // Data bi-directional
.cs(cs) , // Chip Select
.we(we) , // Write Enable/Read Enable
.oe(oe) // Output Enable
);
仿真结果如下,可见读出数据的时间与读地址输入的时间是一致的。
1.4.2 方法二
在RAM的顶层代码中,将读使能信号oe与片选信号cs打一拍,用于RAM内部data的取值,修改后的RAM顶层代码如下:
`timescale 1ns / 1ps
//
// Engineer: Shaosheng Wei
// Create Date: 2022/07/25 15:46:55
// Design Name: single_port_ram
// Module Name: single_port_ram
// Function : Synchronous read write RAM
// Revision 0.01 - File Created
// Additional Comments:
//
module single_port_ram (
clk , // Clock Input
address , // Address Input
data , // Data bi-directional
cs , // Chip Select
we , // Write Enable/Read Enable
oe // Output Enable
);
parameter DATA_WIDTH = 8 ;
parameter ADDR_WIDTH = 8 ;
parameter RAM_DEPTH = 1 << ADDR_WIDTH;
//--------------Input Ports-----------------------
input clk ;
input [ADDR_WIDTH-1:0] address ;
input cs ;
input we ;
input oe ;
//--------------Inout Ports-----------------------
inout [DATA_WIDTH-1:0] data ;
//--------------Internal variables----------------
reg [DATA_WIDTH-1:0] data_out ;
reg [DATA_WIDTH-1:0] mem [0:RAM_DEPTH-1];
reg cs_reg,we_reg,oe_reg;
always @(posedge clk)
begin
oe_reg <= oe;
cs_reg <= cs;
we_reg <= we;
end
//initialization
// synopsys_translate_off
integer i;
initial begin
for(i=0; i < RAM_DEPTH; i = i + 1) begin
mem[i] = 8'h00;
end
end
// synopsys_translate_on
//--------------Code Starts Here------------------
// Tri-State Buffer control
// output : When we = 0, oe = 1, cs = 1
assign data = (cs_reg && oe_reg && !we_reg) ? data_out : 'hz;
// Memory Write Block
// Write Operation : When we = 1, cs = 1
always @ (posedge clk)
begin : MEM_WRITE
if ( cs && we ) begin
mem[address] <= data;
end
end
// Memory Read Block
// Read Operation : When we = 0, oe = 1, cs = 1
always @ (posedge clk)
begin : MEM_READ
if (cs && !we && oe) begin
data_out <= mem[address];
end
end
endmodule // End of Module ram_sp_sr_sw
仿真结果如下:
相比之下,修改版中的方法二更适合于我们日常FPGA设计中直接调用,因为在很多情况下,读使能信号oe和读地址address都是由其他模块产生的,我们无法保证其输入符合方法一中的情况,即使通过打一拍的方式,依然无法保证其输入的读使能信号oe和读地址address在上升沿处进行输入,而不是在上升沿之后才进行输入。
Tip:对变量进行阻塞赋值,变量是在边沿之后才发生变化;而对变量进行阻塞赋值,变量是在边沿处就发生变化。
总结
这里参考一和参考二的区别在于:
(1)参考一只有一个写使能信号we,而参考二除了写使能信号we,还有片选信号cs(控制是否进行读写)和读使能信号oe;
(2)参考一有分开的两个数据写入端口和数据读出端口;而参考二只有一个inout类型数据端口,支持数据的写入和读出;
(3)参考一写数据是由we进行控制的,而读数据则不由任何控制信号进行控制,输入地址,则立即输出对应地址下的数据,在写数据的同时,也在读出该地址对应数据;而参考二写数据是由写使能信号we进行控制的,读数据由读使能信号oe进行控制的,而且需要注意的是:在读数据时不进行写数据,写数据时则不进行读数据,即we=!oe;
(4)参考一读数据输出时间与读地址输入时间同步,参考二(修改版:方法二)读数据输出时间晚于读地址输入时间一个时钟周期;