【牛客网verilog刷题】跨时钟域传输,VL46同步FIFO(详细讲解了同步fifo)

1.序言

(1)什么是FIFO

FIFO(First in,First out),先入先出缓冲器,对数据起缓冲作用,常用在数据的跨时钟域传输。

(2)FIFO分类

FIFO由数据存取电路外围的控制电路构成,通过外围控制电路将需要写入的数据写入存储器件,并在需要时将数据读出。
写入数据操作读出数据操作分别在写数据时钟读数据时钟的控制下进行。当这两个时钟为同一个时,我们称之为同步FIFO;当这两个时钟不是同一个时,我们称之为异步FIFO。(异步fifo涉及到跨时钟域数据传输问题,设计更为复杂)
在这里插入图片描述

2.同步FIFO

(1)接口说明(同步fifo同用一个时钟和复位)

端口名I/O描述
端口名I/O描述
clkinput系统时钟,读写操作共用
rst_ninput系统复位,读写操作共用
wdatainput写入的数据
wincinput写入数据使能信号
wfulloutput存储空间写满反馈信号
rdataoutput读出的数据
rincinput读出数据使能信号
wemptyoutput存储空间读空反馈信号

在这里插入图片描述

(2)代码实现

A)序言
FIFO由数据存取电路外围的控制电路构成。数据存取电路由存储空间配置读写操作电路实现,外围控制电路由读写使能信号产生,读写操作地址产生写满/读空信号产生两部分实现。(本文采用高位扩展法实现写满/读空信号产生,还有一种计数器的方法本文没有说明)
B)存储空间配置
作用:根据需要存取数据的位宽和深度分配一个存储空间。用来存储写入的数据。
注意:为了适应不同的数据位宽存储深度,我们通常将位宽和深度参数化,使其可配置。即在调用此fifo函数时,通过传参实现不同数据位宽存储深度的fifo。需要注意的是,fifo存储深度通常给的参数是10进制,就需要根据10进制数计算所需要的二进制存储空间,使用了系统函数**$clog2**,比如当存储深度为16时,我们所需要的数据地址位宽为log216-1,所以定义数据位宽:

	input [$clog2(DEPTH)-1:0] waddr;

实现:

	reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];

C)读写操作电路
作用:当读写使能信号有效时,根据当前需要读出或写入数据的地址,进行读写数据操作。
实现:

always @(posedge wclk) begin
	if(wenc)
		RAM_MEM[waddr] <= wdata;
end 

always @(posedge rclk) begin
	if(renc)
		rdata <= RAM_MEM[raddr];
end 

D)读写使能信号产生
作用:提供给读写操作电路读写数据操作有效信号(wenc,renc),
写入数据操作时:数据能够写入需要满足两个条件,第一个是写入数据有效(即写入数据使能信号winc为高);第二个是当前存储空间还有空位置,即存储空间没有被写满(full为低)。
读出数据操作时,数据能够读出需要满足两个条件:第一个是读出数据使能信号rinc为高;第二个是当前存储空间还有没有被读出的数据,即存储空间没有被读空(empty为低)。
实现:

	//读写操作有效信号
	wire wenc;
	wire renc;
	assign wenc = (winc && (!wfull))?1'b1:1'b0;
	assign renc = (rinc && (!rempty))?1'b1:1'b0;

E)读写操作地址
作用:提供读写操作数据的读写地址;
注意:a)当读写操作有效信号有效,就会按照当前地址读写数据,说明地址是提前准备好的,所以读写一个数据的同时,需要准备好下一时刻的数据地址;
时序图:(以写数据操作为例)
在这里插入图片描述
b)为了方便产生写满信号读空信号,将读/写数据地址扩展了一位(高位扩展法)构成地址指针,高位扩展法在下一步详细说明;
c)所以实际的数据地址应该为地址指针除去最高位。
实现:

	//读写操作地址指针
	reg [$clog2(DEPTH):0] waddr_ptr;
	reg [$clog2(DEPTH):0] raddr_ptr;	
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			waddr_ptr <= 1'b0;
		else if(wenc)
			waddr_ptr <= waddr_ptr +1'b1;
		else
			waddr_ptr <= waddr_ptr;
	end
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			raddr_ptr <= 1'b0;
		else if(renc)
			raddr_ptr <= raddr_ptr +1'b1;
		else
			raddr_ptr <= raddr_ptr;
	end
	//实际的地址为地址指针除去最高位;
	assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
	assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];

F)写满/读空信号产生
作用:
指示当前存储空间的状态,是否被写满或读空;
说明:#############关键知识点###############
实际的读写数据地址位宽为[WIDTH-1:0],但是上一步我们给的数据地址指针(raddr_ptr,waddr_ptr)位宽为[WIDTH:0],拓展了一位(后面称为拓展位),这一位就是为了方便产生写满和读空信号。如下图所示,数据进行写入和读出的数据地址从小到大递增,当操作到最后一个地址后,又会再一次跳到0000。当读和写操作地址指针指向同一个存储单元,其实有两种可能性,第一种是所有被写入的数据均已经被读出去了,即读空状态;第二种是写数据操作已经进行了一周,即写数据地址已经超过读数据地址一周了,即写满状态。所以地址指针通过增加拓展位,来区分读空和写满状态。
读空写满信号判断依据
读空信号:读写地址指针完全一致;
写满信号:读写地址指针,只有拓展位不同。
在这里插入图片描述
代码:

	//读空写满信号产生模块
	assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
	assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[[$clog2(DEPTH)-1:0]])))?1'b1:1'b0;

3.同步完整代码

(1)完整代码(前面说明的代码也是基于此的)

`timescale 1ns/1ns
/**********************************RAM************************************/
module dual_port_RAM #(parameter DEPTH = 16,
					   parameter WIDTH = 8)(
	 input wclk
	,input wenc
	,input [$clog2(DEPTH)-1:0] waddr  //深度对2取对数,得到地址的位宽。
	,input [WIDTH-1:0] wdata      	//数据写入
	,input rclk
	,input renc
	,input [$clog2(DEPTH)-1:0] raddr  //深度对2取对数,得到地址的位宽。
	,output reg [WIDTH-1:0] rdata 		//数据输出
);
//存储空间配置
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];

always @(posedge wclk) begin
	if(wenc)
		RAM_MEM[waddr] <= wdata;
end 

always @(posedge rclk) begin
	if(renc)
		rdata <= RAM_MEM[raddr];
end 

endmodule  

/**********************************SFIFO************************************/
module sfifo#(
	parameter	WIDTH = 8,
	parameter 	DEPTH = 16
)(
	input 					clk		, 
	input 					rst_n	,
	input 					winc	,
	input 			 		rinc	,
	input 		[WIDTH-1:0]	wdata	,

	// output reg				wfull	,
	// output reg				rempty	,
	output 				wfull	,
	output 				rempty	,
	output wire [WIDTH-1:0]	rdata
);
	//读写操作有效信号
	wire wenc;
	wire renc;
	assign wenc = (winc && (!wfull))?1'b1:1'b0;
	assign renc = (rinc && (!rempty))?1'b1:1'b0;
	//读写操作地址指针
	reg [$clog2(DEPTH):0] waddr_ptr;
	reg [$clog2(DEPTH):0] raddr_ptr;	
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			waddr_ptr <= 1'b0;
		else if(wenc)
			waddr_ptr <= waddr_ptr +1'b1;
		else
			waddr_ptr <= waddr_ptr;
	end
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			raddr_ptr <= 1'b0;
		else if(renc)
			raddr_ptr <= raddr_ptr +1'b1;
		else
			raddr_ptr <= raddr_ptr;
	end
	// //读空写满信号产生模块
	// always@(posedge clk or negedge rst_n)begin
	// 	if(!rst_n)
	// 		rempty <= 1'b0;
	// 	else if(waddr_ptr == raddr_ptr)
	// 		rempty <= 1'b1;
	// 	else
	// 		rempty <= 1'b0;
	// end
	// always@(posedge clk or negedge rst_n)begin
	// 	if(!rst_n)
	// 		wfull <= 1'b0;
	// 	else if((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))
	// 		wfull <= 1'b1;
	// 	else
	// 		wfull <= 1'b0;
	// end
	assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
	assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))?1'b1:1'b0;
	//实际的数据地址
	wire [3:0]	waddr;
	wire [3:0]	raddr;
	assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
	assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];
	
	//模块例化
	dual_port_RAM #(
		.DEPTH(DEPTH),
		.WIDTH(WIDTH)
	)
	u_dual_port_RAM
	(
		.wclk	(clk	),
		.wenc	(wenc	),
		.waddr  (waddr	),
		.wdata	(wdata	),
		.rclk	(clk	),
		.renc	(renc	),
		.raddr  (raddr	),
		.rdata 	(rdata)	
);
endmodule

(2)牛客网提交通过的代码(为提交通过做了修改)

说明:
牛客网的提交通过的代码存在bug,即存在和实际情况不符合的地方:
(1)牛客网的读空和写满信号是reg类型,且相较于正常情况延迟了一拍,当读写地址指一致的时候(读空),读出使能信号先到,会误读一位数据;
(2)牛客网的读空和写满信号是reg类型,且读空信号复位为0才能通过,但是实际情况是系统复位清空,存储空间没有数据,所以实际读空信号应该复位为1。
牛客网完整代码:

`timescale 1ns/1ns
/**********************************RAM************************************/
module dual_port_RAM #(parameter DEPTH = 16,
					   parameter WIDTH = 8)(
	 input wclk
	,input wenc
	,input [$clog2(DEPTH)-1:0] waddr  //深度对2取对数,得到地址的位宽。
	,input [WIDTH-1:0] wdata      	//数据写入
	,input rclk
	,input renc
	,input [$clog2(DEPTH)-1:0] raddr  //深度对2取对数,得到地址的位宽。
	,output reg [WIDTH-1:0] rdata 		//数据输出
);
//存储空间配置
reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];

always @(posedge wclk) begin
	if(wenc)
		RAM_MEM[waddr] <= wdata;
end 

always @(posedge rclk) begin
	if(renc)
		rdata <= RAM_MEM[raddr];
end 

endmodule  

/**********************************SFIFO************************************/
module sfifo#(
	parameter	WIDTH = 8,
	parameter 	DEPTH = 16
)(
	input 					clk		, 
	input 					rst_n	,
	input 					winc	,
	input 			 		rinc	,
	input 		[WIDTH-1:0]	wdata	,

	output reg				wfull	,
	output reg				rempty	,
	// output 				wfull	,
	// output 				rempty	,
	output wire [WIDTH-1:0]	rdata
);
	//读写操作有效信号
	wire wenc;
	wire renc;
	assign wenc = (winc && (!wfull))?1'b1:1'b0;
	assign renc = (rinc && (!rempty))?1'b1:1'b0;
	//读写操作地址指针
	reg [4:0] waddr_ptr;
	reg [4:0] raddr_ptr;	
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			waddr_ptr <= 1'b0;
		else if(wenc)
			waddr_ptr <= waddr_ptr +1'b1;
		else
			waddr_ptr <= waddr_ptr;
	end
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			raddr_ptr <= 1'b0;
		else if(renc)
			raddr_ptr <= raddr_ptr +1'b1;
		else
			raddr_ptr <= raddr_ptr;
	end
	//读空写满信号产生模块
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			rempty <= 1'b0;
		else if(waddr_ptr == raddr_ptr)
			rempty <= 1'b1;
		else
			rempty <= 1'b0;
	end
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)
			wfull <= 1'b0;
		else if((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))
			wfull <= 1'b1;
		else
			wfull <= 1'b0;
	end
	// assign rempty = (waddr_ptr == raddr_ptr)?1'b1:1'b0;
	// assign wfull = ((waddr_ptr[$clog2(DEPTH)] != raddr_ptr[$clog2(DEPTH)])&&((waddr_ptr[$clog2(DEPTH)-1:0] == raddr_ptr[$clog2(DEPTH)-1:0])))?1'b1:1'b0;
	//实际的数据地址
	wire [3:0]	waddr;
	wire [3:0]	raddr;
	assign waddr = waddr_ptr[$clog2(DEPTH)-1:0];
	assign raddr = raddr_ptr[$clog2(DEPTH)-1:0];
	
	//模块例化
	dual_port_RAM #(
		.DEPTH(DEPTH),
		.WIDTH(WIDTH)
	)
	u_dual_port_RAM
	(
		.wclk	(clk	),
		.wenc	(wenc	),
		.waddr  (waddr	),
		.wdata	(wdata	),
		.rclk	(clk	),
		.renc	(renc	),
		.raddr  (raddr	),
		.rdata 	(rdata)	
);
endmodule
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 时钟同步是指将两个时钟之间的信号进行同步,以避免时序错误。在Verilog中,可以通过使用双边沿触发器(double-edge flip-flop)来实现时钟同步。 以下是一个简单的Verilog代码示例,演示了如何实现时钟同步: ``` module sync_signal( input wire clk1, // 第一个时钟时钟信号 input wire clk2, // 第二个时钟时钟信号 input wire async_signal, // 时钟的异步信号 output reg sync_signal // 同步信号 ); reg async_signal_synced; // 用于同步异步信号的寄存器 always @(posedge clk1 or negedge clk1) begin async_signal_synced <= async_signal; // 异步信号同步到第一个时钟 end always @(posedge clk2 or negedge clk2) begin sync_signal <= async_signal_synced; // 同步异步信号到第二个时钟 end endmodule ``` 在上述代码中,我们使用了两个always块来处理不同的时钟。第一个always块根据第一个时钟时钟信号clk1,将异步信号async_signal同步到第一个时钟。第二个always块根据第二个时钟时钟信号clk2,将异步信号同步到第二个时钟,并输出同步信号sync_signal。这样,我们就成功地实现了时钟同步。 ### 回答2: 时钟同步意味着在不同时钟信号的驱动下实现数据的同步Verilog是一种硬件描述语言,可以用来设计和描述数字电路。 在时钟同步中,通常使用两个时钟信号,一个作为输入时钟信号(Input Clock),另一个作为输出时钟信号(Output Clock)。需要在两个时钟之间同步数据时,必须采取一些措施来确保数据的正确传输。 以下是一个使用Verilog编写的时钟同步的简单示例: ```verilog module cross_clock_sync ( input wire input_data, input wire input_clock, input wire output_clock, output wire output_data ); reg input_data_sync; reg output_data_sync; always @(posedge input_clock) input_data_sync <= input_data; always @(posedge output_clock) output_data_sync <= input_data_sync; assign output_data = output_data_sync; endmodule ``` 在这个例子中,我们定义了一个名为"cross_clock_sync"的Verilog模块,它拥有一个输入数据信号(input_data)、输入时钟信号(input_clock)、输出时钟信号(output_clock)和一个输出数据信号(output_data)。 首先,我们使用触发器(Flip-Flop)通过输入时钟信号实现输入数据的同步,使用"always @(posedge input_clock)"来表示在输入时钟信号的上升沿时执行操作。然后,我们使用另一个触发器通过输出时钟信号实现从输入时钟到输出时钟同步,这是通过"always @(posedge output_clock)"来实现的。 最后,我们使用assign关键字将同步后的数据(output_data_sync)与输出数据信号(output_data)相连,使其在输出时钟中使用。 这是一个简单的时钟同步Verilog示例,当然,实际情况中可能会有更多的细节和考虑,但这个例子可以帮助你了解如何用Verilog实现时钟同步。 ### 回答3: 在Verilog中实现时钟同步可以通过以下步骤完成: 1. 设计两个时钟的输入信号,例如一个时钟时钟A,另一个时钟时钟B。 2. 在时钟A的触发器模块中定义一个时钟A的输入信号(clkA),并将该输入信号连接到时钟信号A。 3. 在时钟B的触发器模块中定义一个时钟B的输入信号(clkB),并将该输入信号连接到时钟信号B。 4. 在时钟A的触发器模块中再定义一个输出信号(out_A),用于将时钟A的输入信号(clkA)同步时钟B的时钟。 5. 在时钟B的触发器模块中再定义一个输出信号(out_B),用于将时钟B的输入信号(clkB)同步时钟A的时钟。 6. 在两个触发器模块中使用时钟切换的同步流片构造,例如使用时钟切换器(Cross-domain Clock Converter)或双边沿触发器(Double Edge Flip-flop)等来实现时钟同步。 7. 根据需要,可以进一步添加数据输入信号(data_in_A和data_in_B),并在相应的模块中进行时钟同步。 例如,以下是一个简化的Verilog代码示例,展示了如何实现时钟A到时钟B的时钟同步: ```verilog module cross_domain_sync(clkA, clkB, out_A, out_B); input clkA; input clkB; output reg out_A; output reg out_B; reg temp_A; reg temp_B; always @(posedge clkA) begin temp_A <= clkA; // 同步前的数据暂存在一个寄存器中 temp_B <= 0; // 初始化时钟B的输出信号 end always @(posedge clkB) begin temp_B <= clkB; // 同步前的数据暂存在一个寄存器中 out_A <= temp_A; //将来自时钟A的数据同步时钟B的时钟中 end always @(posedge clkA) begin out_B <= temp_B; //将来自时钟B的数据同步时钟A的时钟中 end endmodule ``` 这样,我们就利用Verilog实现了一个简单的时钟同步模块。通过合适的连接和时钟切换技术,可以确保在不同时钟的信号同步正确,并避免时钟边沿不同步带来的问
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值