手把手Verilog HDL同步Vaild-Ready握手FIFO机制

一级目录

二级目录

三级目录

1、V-R握手FIFO简介

V-R握手FIFO机制,即是两级时钟速率不同的模块之间传递信号的机制,上游往下游传递的数据暂时缓存在FIFO中,上游和FIFO、FIFO和下游之间通过握手传递信号。即在一个FIFO模块外层套了一层握手机制。如下图:
在这里插入图片描述如何用Verilog代码实现呢?我们可以这么来做,
1、先实现一个同步FIFO,
2、再实现一个单信号握手,
3、把握手机制套在FIFO外面。

2、先实现一个同步FIFO

2.1 FIFO简介

FIFO:Frist-in-first-out,先进先出,是一种数据缓存器,实现速率匹配。FIFO包括同步 FIFO 和异步 FIFO 两种,同步FIFO有一个时钟信号,读和写逻辑全部使用这一个时钟信号,异步FIFO有两个时钟信号,读和写逻辑用的各自的读写时钟。
FIFO 与普通存储器RAM 的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写入数据,顺序读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。 FIFO本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。

2.2 同步FIFO指标

FIFO的宽度:即 FIFO一次读写操作的数据位。
FIFO的深度:指的是 FIFO可以存储多少个 N位的数据(如果宽度为 N)。
满标志:FIFO 已满或将要满时由 FIFO 的状态送出的一个信号,以阻止 FIFO的写操作继续向 FIFO 中写数据而造成溢出(overflow)。
空标志:FIFO 已空或将要空时由 FIFO 的状态送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO中读出数据而造成无效数据的读出(underflow)。
读/写时钟:读/些操作所遵循的时钟,在每个时钟沿来临时读/写数据。

2.3 同步FIFO设计

FIFO 读写指针(读写地址)的工作原理:
写指针:总是指向下一个被写入的单元,复位时,指向第1个单元(编号为0); 
读指针:总是指向当前要被读出的数据,复位时,指向第1个单元(编号为0);


FIFO 的“空”/“满”检测 
FIFO 设计的关键:产生可靠的 FIFO 读写指针和生成 FIFO“空”/“满”标志。 
当读写指针相等时,表明FIFO为空,这种情况发生在复位操作时,或者当读指针读出 FIFO 中最后一个字后,追赶上了写指针时,如下图1所示:
当读写指针再次相等时,表明 FIFO为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around) 又追上了读指针,如下图2:

在这里插入图片描述 读写指针可以在读写使能有效时,每时钟周期+1,而如何产生可靠的“空”/“满”信号则成了同步FIFO设计的重点。下面有两种解决方法:计数器法和高位扩展法。 本次采用计数器法

2.4 计数器法实现同步FIFO

构建一个计数器fifo_cnt,该计数器用于指示当前 FIFO 中数据的个数:
复位时,该计数器为0,FIFO中的数据个数为0;
当读写使能信号均有效时,说明又读又写,计数器不变,FIFO中的数据个数无变化;
当写使能有效且 full =0,则 fifo_cnt +1;表示写操作且 FIFO 未满时候,FIFO 中的数据个数增加了 1; 
当读使能有效且 empty=0,则 fifo_cnt -1;;表示读操作且 FIFO 未空时候,FIFO 中的数据个数减少了 1 ;
fifo_cnt =0 的时候,表示 FIFO 空,需要设置 empty=1;
fifo_cnt = fifo的深度 的时候,表示 FIFO 现在已经满,需要设置 full=1。

2.5 同步FIFO代码

//计数器法实现同步FIFO
module	sync_fifo_cnt
#(
	parameter   DATA_WIDTH = 'd8  ,							//FIFO位宽
    parameter   DATA_DEPTH = 'd16 							//FIFO深度
)
(
	input									clk		,		//系统时钟
	input									rst_n	,       //低电平有效的复位信号
	input	[DATA_WIDTH-1:0]				data_in	,       //写入的数据
	input									rd_en	,       //读使能信号,高电平有效
	input									wr_en	,       //写使能信号,高电平有效
															
	output	reg	[DATA_WIDTH-1:0]			data_out,	    //输出的数据
	output									empty	,	    //空标志,高电平表示当前FIFO已被写满
	output									full	,       //满标志,高电平表示当前FIFO已被读空
	output	reg	[$clog2(DATA_DEPTH) : 0]	fifo_cnt		//$clog2是以2为底取对数	
);
 
//reg define
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];	//用二维数组实现RAM	
reg [$clog2(DATA_DEPTH) - 1 : 0]	wr_addr;				//写地址
reg [$clog2(DATA_DEPTH) - 1 : 0]	rd_addr;				//读地址
 
//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		rd_addr <= 0;
	else if (!empty && rd_en)begin							//读使能有效且非空
		rd_addr <= rd_addr + 1'd1;
		data_out <= fifo_buffer[rd_addr];
	end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		wr_addr <= 0;
	else if (!full && wr_en)begin							//写使能有效且非满
		wr_addr <= wr_addr + 1'd1;
		fifo_buffer[wr_addr]<=data_in;
	end
end
//更新计数器
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		fifo_cnt <= 0;
	else begin
		case({wr_en,rd_en})									//拼接读写使能信号进行判断
			2'b00:fifo_cnt <= fifo_cnt;						//不读不写
			2'b01:	                               			//仅仅读
				if(fifo_cnt != 0)				   			//fifo没有被读空
					fifo_cnt <= fifo_cnt - 1'b1;   			//fifo个数-1
			2'b10:                                 			//仅仅写
				if(fifo_cnt != DATA_DEPTH)         			//fifo没有被写满
					fifo_cnt <= fifo_cnt + 1'b1;   			//fifo个数+1
			2'b11:fifo_cnt <= fifo_cnt;	           			//读写同时
			default:;                              	
		endcase
	end
end
//依据计数器状态更新指示信号
//依据不同阈值还可以设计半空、半满 、几乎空、几乎满
assign full  = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0;		//满信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;				//空信号
 
endmodule
`timescale 1ns/1ns	//时间单位/精度
 
//------------<模块及端口声明>----------------------------------------
module tb_sync_fifo_cnt();
 
parameter   DATA_WIDTH = 8  ;			//FIFO位宽
parameter   DATA_DEPTH = 8 ;			//FIFO深度
 
reg									clk		;
reg									rst_n	;
reg		[DATA_WIDTH-1:0]			data_in	;
reg									rd_en	;
reg									wr_en	;
						
wire	[DATA_WIDTH-1:0]			data_out;	
wire								empty	;	
wire								full	;
wire	[$clog2(DATA_DEPTH) : 0]	fifo_cnt;
 
 
//------------<例化被测试模块>----------------------------------------
sync_fifo_cnt
#(
	.DATA_WIDTH	(DATA_WIDTH),			//FIFO位宽
    .DATA_DEPTH	(DATA_DEPTH)			//FIFO深度
)
sync_fifo_cnt_inst(
	.clk		(clk		),
	.rst_n		(rst_n		),
	.data_in	(data_in	),
	.rd_en		(rd_en		),
	.wr_en		(wr_en		),
                 
	.data_out	(data_out	),	
	.empty		(empty		),	
	.full		(full		),
	.fifo_cnt	(fifo_cnt	)			
);
 
//------------<设置初始测试条件>----------------------------------------
initial begin
	clk = 1'b0;							//初始时钟为0
	rst_n <= 1'b0;						//初始复位
	data_in <= 'd0;		
	wr_en <= 1'b0;		
	rd_en <= 1'b0;
//重复8次写操作,让FIFO写满 	
	repeat(8) begin		
		@(negedge clk)begin		
			rst_n <= 1'b1;				
			wr_en <= 1'b1;		
			data_in <= $random;			//生成8位随机数
		end
	end
//重复8次读操作,让FIFO读空	
	repeat(8) begin
		@(negedge clk)begin		
			wr_en <= 1'b0;
			rd_en <= 1'd1;
		end
	end
//重复4次写操作,写入4个随机数据	
	repeat(4) begin
		@(negedge clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
			rd_en <= 1'b0;
		end
	end
//持续同时对FIFO读写,写入数据为随机数据	
	forever begin
		@(negedge clk)begin		
			wr_en <= 1'b1;
			data_in <= $random;	//生成8位随机数
			rd_en <= 1'b1;
		end
	end
end
 
//------------<设置时钟>----------------------------------------------
always #10 clk = ~clk;			//系统时钟周期20ns
 
endmodule

3、再实现一个同步握手

3.1 Valid-Ready简介

设计中经常会用到几个模块之间的数据准确无误的传输,那么我们就需要加上握手信号来控制信号的传输。通常它有两组接口:一个接上游、一个传下游。

3.2 Valid-Ready原理

datain_val用来指示上游数据输入data_in的有效性;
datain_rdy用来指示本模块是否准备好接收上游数据;
dataout_val用来指示往下游数据输出data_out的有效性;
dataout_rdy表示下游是否准备好接收本模块的输出数据;
clk是时钟信号;rst_n是异步复位信号。
其中,datain_ready,由dataout_rdy和data_val组合得到。

在这里插入图片描述

3.3 Valid-Ready三种情况

上下游各自的valid和ready信号都有三种情况,即Ready-Before-Valid,Valid-Before-Ready,Ready-With-Valid。这三种各自对应如下:

3.3.1 Ready-Before-Valid(输入)

Ready-Before-Valid是Ready信号在Valid信号之前有效。如下时序图:
在这里插入图片描述

先准备好,再来数据,输入端口适合采取这种情况。这种设计使得在上游数据来临之前,通道已经准备好接收数据了。

3.3.2 Valid-Before-Ready(输出)

Valid-Before-Ready是Valid信号在Ready信号之前有效。如下时序图:
在这里插入图片描述

先有数据,再看是否准备好往下传,输出端口采取这种情况。提前把数据给准备等待下游来取。这样可以避免数据已经好了但没人来取的情况。

3.3.3 Valid-With-Ready

Valid-With-Ready是Valid信号和Ready信号同时有效,同时到达就很简单,数据来了就被取走。

3.3.4 Stalemate(僵局)

输出端用Ready-Before-Valid,  输出端等待接受端给出的Ready来输出数据
接受端用Valid-before-Ready,  接收端也在等待输出端给出Valid信号来接受数据
两者都在等待却没有一方先给,  通道无效,被“锁住”了。

握手需要特别注意三件事:

  1. valid与ready不可过度依赖,他俩彼此可以互相独立发出请求拉高;
  2. 输如输出各自的valid与数据同步;
  3. 数据与valid一起拉高后,若无新的数据,valid要及时置0。

3.4 代码


//annotation one 
//datain_rdy
//何时准备好接收上游的输入数据?即 datain_rdy == 1?
//当输出下游的数据无效,即没有数据往下游传送,此时“管道为空”,等待数据来。即dataout_val=0;
//当输出下游的数据准备好时,即接下来一边往下游送数据,一边等待上游有效数据来。即dataout_rdy=1。

//annotation two 
//rst_n
//复位信号来时,输出数据和输出数据有效置0,输入数据准备不置0。

//annotation three
//dataout_val
//输出数据有效,当输入数据准备好后(即datain_rdy=1),输入数据有效即为输出数据有效。

//annotation three
//dataout
//输出数据,当输入数据准备好且有效后(即datain_rdy 、datain_val=1),输入数据即为输出数据。


module Valid_Ready #(parameter wd = 4)(
	input                   clk, 
	input                   rst_n,
	input      [wd-1:0]     datain,
	input                   datain_val,
    input                   dataout_rdy, 

	output                  datain_rdy, 
	output reg              dataout_val,
	output reg [wd-1:0]     dataout 
);


//annotation one
assign datain_rdy = dataout_rdy || !dataout_val;        
	
always @(posedge clk or negedge rst_n) 
begin
	if (!rst_n) 
		begin
			dataout     <= 0;                //annotation two                  
			dataout_val <= 0;
		end
	else 
		begin
			if (datain_rdy)
			dataout_val <= datain_val;      // annotation three
			else
			dataout_val <=dataout_val;

            if (datain_rdy && datain_val)
			dataout     <= datain;          // annotation three
			else
			dataout     <= dataout;
		end
end
endmodule

testbench如下:

`timescale 1ns/1ns
module Valid_Ready_tb #(parameter wd = 4);
	reg               clk;
	reg               rst_n;
	reg   [wd-1:0]    datain;
	reg               datain_val;
    reg               dataout_rdy; 

	wire              datain_rdy; 
	wire              dataout_val;
	wire  [wd-1:0]    dataout; 
	
	Valid_Ready r2 (.clk(clk), 
		    .rst_n(rst_n), 
		    .datain(datain), 
			.datain_val(datain_val), 
			.dataout_rdy(dataout_rdy), 
			.datain_rdy(datain_rdy), 
			.dataout_val(dataout_val),
			.dataout(dataout));			
	
always 
begin
	#2 clk = ~clk;
end
//  assign datain_rdy = dataout_rdy || !dataout_val;  
	
	initial begin
		clk          = 0;
		rst_n        = 1;
		datain       = 0;
		datain_val   = 0;                   // 输入数据无效
		dataout_rdy  = 0;                   // 准备输出置0。
	
		#5 rst_n = 0; datain = 5;
		#5 rst_n = 1;                         //此时复位,“管道为空”,dataout_val=0,则datain_rdy为1,准备接收上游数据
		#5 datain_val = 1;                  //输入数据有效,此时datain_rdy && datain_val都为1,数据存入dataout了
		                                    //握手已经完成
		#5 datain_val = 0;                  //由于datain寄存于dataout,发送端认为捕获端已获取数据,datain_val端要置0
		#5 dataout_rdy = 1;                 //slave端可以接受数据,将pipe中数据取走,此时pipe可以寄存下一个数据dataout_val=0,但为了避免bubble,dataout保持不变
		#5 dataout_rdy = 0;                 //slave端接收数据后,停止接收数据
		
		#5 datain = 15; datain_val = 1;     //master端发送数据,因为此时pipe可以寄存数据,由pipe寄存,dataout_val变为1
		#5 datain_val = 0;                  //后续分析过程与上类似
		#5 dataout_rdy = 1; 
		#5 dataout_rdy = 0; 
		
		#10 $stop;
	end
endmodule

4、最后整合为同步Vaild-Ready握手FIFO

4.1 思路原理

如何把valid-ready和握手联系起来?
模块外部是握手,内部是FIFO。
故模块整体的输入输出端口应和valid-read保持一致,在内部声明一个FIFO。如下:

在这里插入图片描述

4.2 具体操作

如何把模块外部变量和FIFO定义的变量联系起来?

1、读写使能:
当前reg准备好接收且输入数据有效,即可 使能 写信号;
当后reg准备好接收且当前reg输出数据有效,即可 使能 读信号;

2、FIFO自己写数据与写地址、读数据与读数据、计数器自增自减、空满标志判断不变;

   
3、把空满标志传出去:
没有空(有数据)则可以继续给下面传;     assign out_val = (!empty);
没有满(有地方可以接着写)则可以继续接受。assign in_rdy  = (!full); 

这样,就把一个Valid-Ready的外衣,套在了FIFO上。具体代码如下:

4.3 代码

模块代码:

module VR_FIFO (//外部Valid_Ready输入输出端口
    input                   clk, 
	input                   rst_n,
	input      [7:0]        data_in,
	input                   in_val,
    input                   out_rdy, 

	output                  in_rdy, 
	output                  out_val,
	output reg [7:0]        data_out 
);

//内部FIFO的输入输出端口,以下三组变量是FIFP的输入输出端口,故而要在内部定义。
reg          wr_en     ;                //FIFO 写使能
reg          rd_en     ;                //FIFO 读使能
wire         empty     ;                //FIFO 空信号
wire         full      ;                //FIFO 满信号
reg  [4:0]   fifo_cnt  ;                //FIFO 计数器

//FIFO内部自带的变量定义
reg [7:0]   fifo_buffer[15 : 0];	//用二维数组实现RAM	
reg [3:0]	wr_addr;				//写地址
reg [3:0]	rd_addr;				//读地址


//写使能,当前reg准备好接收且输入数据有效,即可 使能 写信号
always@(posedge clk or negedge rst_n)
begin
    if(!rst_n)
        wr_en <= 1'b0;
    else if(in_val && in_rdy)
        wr_en <= 1'b1;
    else
        wr_en <= 1'b0;
end 

//读使能,当后reg准备好接收且当前reg输出数据有效,即可 使能 读信号
always@(posedge clk or negedge rst_n)
begin
    if(!rst_n)
        rd_en <= 1'b0;
    else if(out_val && out_rdy)
        rd_en <= 1'b1;
    else
        rd_en <= 1'b0;
end 

//向FIFO写数据操作,并且更新写地址
always @ (posedge clk or negedge rst_n) 
begin
	if (!rst_n)
		wr_addr              <= 0;
	else if (!full && wr_en)
    begin							//写使能有效且没有写满
		wr_addr              <= wr_addr + 1'd1;
		fifo_buffer[wr_addr] <= data_in;
	end
end

//从FIFO读数据操作,并且更新读地址
always @ (posedge clk or negedge rst_n) 
begin
	if (!rst_n)
		rd_addr  <= 0;
	else if (!empty && rd_en)begin	//读使能有效且没有读空
		rd_addr  <= rd_addr + 1'd1;
		data_out <= fifo_buffer[rd_addr];
	end
end

//更新计数器,来判断空满信号
always @ (posedge clk or negedge rst_n) 
begin
	if (!rst_n)
		fifo_cnt <= 0;
	else begin
		case({wr_en,rd_en})									
			2'b00:  fifo_cnt <= fifo_cnt;						//不读不写
			2'b01:
                begin	                               			//仅仅读
				if(fifo_cnt != 0)				   			    //fifo没有被读空
					fifo_cnt <= fifo_cnt - 1'b1;                //fifo个数-1
                end   			
			2'b10:                                              //仅仅写
                begin                                 			
				if(fifo_cnt != 5'd16)         			        //fifo没有被写满
					fifo_cnt <= fifo_cnt + 1'b1;   			    //fifo个数+1
                end
            2'b11:  fifo_cnt <= fifo_cnt;	           			//读写同时
			default:;                              	
		endcase
	end
end

//依据计数器状态更新指示信号
//依据不同阈值还可以设计半空、半满 、几乎空、几乎满
assign full  = (fifo_cnt == 5'd16) ? 1'b1 : 1'b0;		    //满信号
assign empty = (fifo_cnt == 0)     ? 1'b1 : 1'b0;		    //空信号

assign in_rdy  = (!full);    
assign out_val = (!empty);
endmodule

testbench:

`timescale 1ns/1ns

module tb_VR_FIFO;

    reg                   clk;
	reg                   rst_n;
	reg      [7:0]        data_in;
	reg                   in_val;
    reg                   out_rdy; 
	wire                  in_rdy;
	wire                  out_val;
	wire     [7:0]        data_out; 

VR_FIFO VR_FIFO_u0 (
					.clk(clk), 
					.rst_n(rst_n),
					.data_in(data_in),
					.in_val(in_val),
					.out_rdy(out_rdy), 
					.in_rdy(in_rdy), 
					.out_val(out_val),
					.data_out(data_out));

initial begin
        clk      = 1'b0;
        rst_n    = 1'b0;
        data_in  = 8'd0;
        in_val   = 1'd0;
        out_rdy   = 1'b0;

#10     rst_n    = 1'b1;

#20     repeat(50)
    begin
    

/*
	//正常写一次读一次
    #20   w_data;
	#20   r_data;

	//只写不读,会满
    #20   w_data;

	//写多读少
    #20   w_data;
	#20   w_data
	#20   r_data;

	//写少读多
    #20   w_data;
	#20   r_data;
	#20   r_data;
*/
    end


#20000  $stop;
end

task w_data;
begin
	    data_in = $random;
	    in_val  = 1'b1;
#20     in_val  = 1'b0;

end
endtask

task r_data;
begin
	    out_rdy  = 1'b1; 
#20     out_rdy  = 1'b0;
end
endtask

always #10 clk =~ clk;
endmodule

至此就完了。
第一次写文章,有的地方写的很粗糙,有问题提出,欢迎大家讨论。
最后贴上邮箱:boyue8817505@163.com

  • 13
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Valid-Ready握手协议在Verilog设计中用于数据传输的同步,确保数据的有效性和可靠性。它通常用于两个模块之间的数据交换,并且在发送数据之前,接收方必须准备好接收数据。 下面是一个简单的Verilog代码示例,演示了Valid-Ready握手协议的基本原理: ```verilog module ValidReadyHandshake ( input wire clk, input wire reset, input wire data_valid, output wire data_ready, input wire [DATA_WIDTH-1:0] data_in, output wire [DATA_WIDTH-1:0] data_out ); // 内部状态定义 reg [DATA_WIDTH-1:0] internal_data; reg internal_valid; reg internal_ready; // 同步时钟 always @(posedge clk) begin if (reset) begin internal_ready <= 1'b0; internal_data <= {DATA_WIDTH{1'b0}}; internal_valid <= 1'b0; end else begin internal_ready <= data_ready; internal_data <= data_in; internal_valid <= data_valid; end end // 数据输出逻辑 assign data_out = internal_data; // Ready信号逻辑 assign data_ready = internal_ready && !internal_valid; endmodule ``` 在这个示例中,Valid-Ready握手协议的发送方将数据放入`data_in`端口,并设置`data_valid`信号为高电平。接收方通过`data_ready`信号表示它已经准备好接收数据。当接收方准备好时,它将`data_ready`信号设置为高电平,发送方将通过`data_ready`信号的状态来判断是否可以发送数据。 值得注意的是,`clk`和`reset`信号是必需的,用于同步时钟和复位。 这只是一个基本示例,你可以根据具体的设计需求对握手协议进行扩展和修改。希望这个示例对你有所帮助!如果你有更多问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值