Verilog设计实例(3)基于Verilog的单端口同步读写RAM设计

写在前面


为什么要写单端口同步读写RAM呢?

没有那么多为什么?就是因为简单、基础,能清晰说明单端口RAM的原理,顺手给出设计,也能说明你的设计基础,作为专题Verilog设计实例中的一员,单端口RAM必然上榜了!

曾经也写过很多类似的博文,在博客首页搜索即可,给出搜索链接:

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
	always@(posedge i_clk) begin
		if(cs&wr) begin
			mem[addr] <= data;
		end
	end

	// read part
	always@(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
	always@(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
	always@(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,二维码,左下角;
知乎:李锐博恩,二维码右下角。
在这里插入图片描述

FPGA/IC技术交流2020

猜你喜欢

转载自blog.csdn.net/Reborn_Lee/article/details/106555619