欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-02-22 21:39:16
...

写在前面


为什么要写单端口同步读写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
	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
  • 第五:大家一定要学会这种测试文件的写法,严格控制写入多少个周期或数据,以及读出多少个数据。

仿真波形

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

读数据:

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

交个朋友

个人微信公众号:FPGA LAB,二维码,左下角;
知乎:李锐博恩,二维码右下角。
Verilog设计实例(3)基于Verilog的单端口同步读写RAM设计

FPGA/IC技术交流2020