基于vivado(语言Verilog)的FPGA学习(5)——跨时钟处理

基于vivado(语言Verilog)的FPGA学习(5)——跨时钟处理

1. 为什么要解决跨时钟处理问题

慢时钟到快时钟一般都不需要处理,关键需要解决从快时钟到慢时钟的问题,因为可能会漏信号或者失真,比如:
在这里插入图片描述

2.解决办法

第一种办法是开环解决方案,也就是人为设置目标信号脉宽大于1.5倍的周期。但是容易和设计要求冲突
所以第二个大方法是闭环解决方案,也就是从改善同步方式:最基础的是二级、三级寄存器。但是还是会在极端情况下出现失真,并且需要满足:
【1】级联的寄存器必须使用同一个采样时钟;
【2】发送端时钟域寄存器输出和接收端异步时钟域级联寄存器输入之间不能有任何其他组合逻辑;
【3】同步器中级联的寄存器中除了最后一个寄存器外所有的寄存器只能有一个扇出,即其只能驱动下一级寄存器的输入
于是就有了将控制信号当作使能信号进行传递(单比特的话,这个使能信号可以是信号本身):
在这里插入图片描述

我的理解就是弄一个使能信号当作两个时钟域的信使,来告诉对方是不是开始了读/写数据了。
为了进一步进行多比特信号的跨时钟处理,干脆就拿地址作为同步信号(下图中的wptr和rptr),用RAM作为数据的缓存区,用不同时钟域给的空/满作为数据输出/输入的标识。传输的过程需要格雷码和二进制的转换。
这一方法被称为FIFO结果处理多比特跨时钟域信号。
在这里插入图片描述

3.实现代码

`timescale 1ns / 1ns
  module ASFIFO#
	(
		parameter WIDTH = 16,		// FIFO数据总线位宽
		parameter PTR   = 4			// FIFO存储深度(bit数,深度只能是2^n个)
	)
	(
		// write interface
		input					wrclk	,		// 写时钟
		input					wr_rst_n,		// 写指针复位
		input	[WIDTH-1:0]		wr_data	,		// 写数据总线
		input					wr_en	,		// 写使能
		output  reg				wr_full	,		// 写满标志
 
		//read interface
		input					rdclk	,		// 读时钟
		input					rd_rst_n,		// 读指针复位
		input					rd_en	,		// 读使能
		output	[WIDTH-1:0]		rd_data	,		// 读数据输出
		output	reg				rd_empty		// 读空标志
    );
    
    
 
 
	// 写时钟域信号定义
	reg [PTR:0] wr_bin		;					// 二进制写地址
	reg [PTR:0] wr_gray		;					// 格雷码写地址
	reg [PTR:0] rd_gray_ff1 ;					// 格雷码读地址同步寄存器1
	reg [PTR:0] rd_gray_ff2 ;					// 格雷码读地址同步寄存器2
	reg [PTR:0] rd_bin_wr	;					// 同步到写时钟域的二进制读地址
 
 
	// 读时钟域信号定义
	reg [PTR:0] rd_bin		;					// 二进制读地址
	reg [PTR:0] rd_gray		;					// 格雷码读地址
	reg [PTR:0] wr_gray_ff1 ;					// 格雷码写地址同步寄存器1
	reg [PTR:0] wr_gray_ff2 ;					// 格雷码写地址同步寄存器2
	reg [PTR:0] wr_bin_rd	;					// 同步到读时钟域的二进制写地址
 
 
	// 解格雷码电路循环变量
	integer i ;
	integer j ;
 
 
	// DPRAM控制信号
	wire  				dpram_wr_en		 ;		// DPRAM写使能
	wire [PTR-1:0]		dpram_wr_addr    ;		// DPRAM写地址
	wire [WIDTH-1:0] 	dpram_wr_data	 ;		// DPRAM写数据
	wire  				dpram_rd_en		 ;		// DPRAM读使能
	wire [PTR-1:0]		dpram_rd_addr    ;		// DPRAM读地址
	wire [WIDTH-1:0] 	dpram_rd_data	 ;		// DPRAM读数据
 
 
 
	// ******************************** 写时钟域 ******************************** //
	// 二进制写地址递增
	always @(posedge wrclk or posedge wr_rst_n) begin
		if (!wr_rst_n) begin
			wr_bin <= 'b0;
		end
		else if ( wr_en == 1'b1 && wr_full == 1'b0 ) begin
			wr_bin <= wr_bin + 1'b1;
		end
		else begin
			wr_bin <= wr_bin;
		end
	end
 
 
	// 写地址:二进制转格雷码
	always @(posedge wrclk or posedge wr_rst_n) begin
		if (!wr_rst_n) begin
			wr_gray <= 'b0;
		end
		else begin
			wr_gray <= { wr_bin[PTR], wr_bin[PTR:1] ^ wr_bin[PTR-1:0] };
		end
	end
	// 格雷码读地址同步至写时钟域
	always @(posedge wrclk or posedge wr_rst_n) begin
		if(!wr_rst_n) begin
			rd_gray_ff1 <= 'b0;
			rd_gray_ff2 <= 'b0;
		end
		else begin
			rd_gray_ff1 <= rd_gray;
			rd_gray_ff2 <= rd_gray_ff1;
		end
	end
	// 同步后的读地址解格雷
	always @(*) begin
		rd_bin_wr[PTR] = rd_gray_ff2[PTR];
		for ( i=PTR-1; i>=0; i=i-1 )
			rd_bin_wr[i] = rd_bin_wr[i+1] ^ rd_gray_ff2[i];
	end
	// 写时钟域产生写满标志
	always @(*) begin
		if( (wr_bin[PTR] != rd_bin_wr[PTR]) && (wr_bin[PTR-1:0] == rd_bin_wr[PTR-1:0]) ) begin
			wr_full = 1'b1;
		end
		else begin
			wr_full = 1'b0;
		end
	end
	// ******************************** 读时钟域 ******************************** //
	always @(posedge rdclk or posedge rd_rst_n) begin
		if (!rd_rst_n) begin
			rd_bin <= 'b0;
		end
		else if ( rd_en == 1'b1 && rd_empty == 1'b0 ) begin
			rd_bin <= rd_bin + 1'b1;
		end
		else begin
			rd_bin <= rd_bin;
		end
	end
	// 读地址:二进制转格雷码
	always @(posedge rdclk or posedge rd_rst_n) begin
		if (!rd_rst_n) begin
			rd_gray <= 'b0;
		end
		else begin
			rd_gray <= { rd_bin[PTR], rd_bin[PTR:1] ^ rd_bin[PTR-1:0] };
		end
	end
 
 
	// 格雷码写地址同步至读时钟域
	always @(posedge rdclk or posedge rd_rst_n) begin
		if(!rd_rst_n) begin
			wr_gray_ff1 <= 'b0;
			wr_gray_ff2 <= 'b0;
		end
		else begin
			wr_gray_ff1 <= wr_gray;
			wr_gray_ff2 <= wr_gray_ff1;
		end
	end
 
 
	// 同步后的写地址解格雷
	always @(*) begin
		wr_bin_rd[PTR] = wr_gray_ff2[PTR];
		for ( j=PTR-1; j>=0; j=j-1 )
			wr_bin_rd[j] = wr_bin_rd[j+1] ^ wr_gray_ff2[j];
	end
 
 
	// 读时钟域产生读空标志
	always @(*) begin
		if( wr_bin_rd == rd_bin )
			rd_empty = 1'b1;
		else
			rd_empty = 1'b0;
	end
 
 
	// RTL双口RAM例化
	DPRAM
		# ( .WIDTH(16), .DEPTH(16), .ADDR(4) )
		U_DPRAM
		(
			.wr_clk		(wrclk		 	),
			.rd_clk		(rdclk			),
			.rd_rst_n   (rd_rst_n		),
			.wr_rst_n   (wr_rst_n       ),
			.wr_en 		(dpram_wr_en	),
			.rd_en 		(dpram_rd_en	),
			.wr_data 	(dpram_wr_data	),
			.rd_data 	(dpram_rd_data	),  //唯一输出output
			.wr_addr 	(dpram_wr_addr	),
			.rd_addr 	(dpram_rd_addr	)
		);
 
 
	// 产生DPRAM读写控制信号
	assign dpram_wr_en   = ( wr_en == 1'b1 && wr_full == 1'b0 )? 1'b1 : 1'b0;
	assign dpram_wr_data = wr_data;
	assign dpram_wr_addr = wr_bin[PTR-1:0];
 
	assign dpram_rd_en   = ( rd_en == 1'b1 && rd_empty == 1'b0 )? 1'b1 : 1'b0;
	assign rd_data = dpram_rd_data;
	assign dpram_rd_addr = rd_bin[PTR-1:0];

endmodule

其中的DPRAM就是一个数据缓存区,根据wr_en&~wr_full来作为写操作使能,控制数据写入RAM中。RAM模块定义如下:

`timescale 1ns / 1ns

module DPRAM#(parameter WIDTH = 8 ,parameter DEPTH = 16,parameter ADDR = 4)(
	input wr_clk,
	input rd_clk,
    input rd_rst_n,
    input wr_rst_n,
	input wr_en,
	input rd_en,
	input [WIDTH-1:0]wr_data,
	output reg [WIDTH-1:0]rd_data,
	input [ADDR-1:0]wr_addr,
	input[ADDR-1:0]rd_addr
);
	reg [WIDTH-1:0] memory[DEPTH-1:0]; 
	
	//写
	always@(posedge wr_clk)begin
	if (!wr_rst_n) begin
	   memory[wr_addr] <= 0;
	   end
	else if(wr_en) begin
		memory[wr_addr] <= wr_data;
		end
	else   begin
		memory[wr_addr] <= memory[wr_addr];
		end
	end

	//读
	always@(posedge rd_clk)begin
	if(! rd_rst_n) begin
	   rd_data <= 0;
	   end
	if(rd_en)  begin
		rd_data <= memory[rd_addr];
		end
	else   begin
		rd_data <= rd_data;
		end
	end
endmodule

testbench如下:

`timescale 1ns / 1ns
module ASFIFO_tb;
	parameter WIDTH = 16;
	parameter PTR   = 4;
	// 写时钟域tb信号定义
	reg					wrclk		;
	reg					wr_rst_n	;
	reg	[WIDTH-1:0]		wr_data		;
	reg 				wr_en		;
	wire				wr_full		;
	// 读时钟域tb信号定义
	reg					rdclk		;
	reg					rd_rst_n	;
	wire [WIDTH-1:0]	rd_data		;
	reg					rd_en		;
	wire				rd_empty	;
	// testbench自定义信号
	reg					init_done	;		// testbench初始化结束
	// FIFO初始化
	initial	begin
		// 输入信号初始化
		wr_rst_n  = 1	;
		rd_rst_n  = 1	;
		wrclk 	  = 0	;
		rdclk 	  = 0	;
		wr_en 	  = 1	;
		rd_en 	  = 1	;
		wr_data   = 'b0 ;
		init_done = 0	;
 
		// FIFO复位
		#30 wr_rst_n = 0;
			rd_rst_n = 0;
		#30 wr_rst_n = 1;
			rd_rst_n = 1;
 
		// 初始化完毕
		#30 init_done = 1;
	end
  
	// 写时钟
	always
		#2 wrclk = ~wrclk;
 
	// 读时钟
	always
		#4 rdclk = ~rdclk;
  
	// 写入数据自增
	always @(posedge wrclk) begin
		if(init_done) begin
			if( wr_full == 1'b0 )
				wr_data <= wr_data + 1;
			else
				wr_data <= wr_data;
		end
		else begin
			wr_data <= 'b0;
		end
	end
  
 	// 异步fifo例化
	ASFIFO
		# ( .WIDTH(16), .PTR(4) )
		U_ASFIFO
		(
			.wrclk	 	(wrclk		),
			.wr_rst_n	(wr_rst_n	),
			.wr_data	(wr_data	),
			.wr_en		(wr_en		),
			.wr_full	(wr_full	),
 
			.rdclk		(rdclk		),
			.rd_rst_n	(rd_rst_n	),
			.rd_data	(rd_data	),
			.rd_en		(rd_en		),
			.rd_empty	(rd_empty	)
		);
 
 
endmodule

对应的框架图自己重新画了一遍,思路清晰很多。
在这里插入图片描述
看时序图的时候,可以将RAM模块的端口也画出来,方便看地址变化:
在这里插入图片描述
时序图:
在这里插入图片描述
上图中,上下两个读写使能wr_en和rd_en分别表示DPRAM例划前后的:

assign dpram_wr_en   = ( wr_en == 1'b1 && wr_full == 1'b0 )? 1'b1 : 1'b0;
assign dpram_rd_en   = ( rd_en == 1'b1 && rd_empty == 1'b0 )? 1'b1 : 1'b0;

3.1 细节一:写地址和同步读地址的比较

通过时序图,可以看出写地址是与两帧(相对于wr_clk时钟)前的同步读地址相比较。
在这里插入图片描述
上图可以看出当写地址为6时,读地址的前两帧才是6,因为为了仿真亚稳态出现,读地址过来对比是经过了两级触发器。
在这里插入图片描述

3.2 RAM的具体数据情况

指针所指的时刻为上时序图中黄线时刻,也就是wr_full第一次变为1时。
从代码中可以看出RAM例划前地址为5位,例划后只取4位,现在明白了原因:
第一位用来判断是写指针超过读指针一圈了(满标识:第一位地址相反,其余相同),还是写指针和读指针在一起(空标识:5位地址全部相反)。
在这里插入图片描述

3.3 实时性的要求

在testbench中,有这么一段,意思就是如果该RAM已满时,就不自增数据(我的理解就是不添加新的数据了):

	// 写入数据自增
	always @(posedge wrclk) begin
		if(init_done) begin
			if( wr_full == 1'b0 )
				wr_data <= wr_data + 1;
			else
				wr_data <= wr_data;
		end
		else begin
			wr_data <= 'b0;
		end
	end

但实际情况很有可能是实时处理,数据是源源不断传来,所以还是在满足快时钟同步至慢时钟的不漏报情况下,就需要衡量最长持续数据传输长度和RAM容积大小。当持续传输数据有n个时,就需要至少m*n的RAM。m=快时钟频率/慢时钟频率

参考:
https://www.codenong.com/cs105834073/
https://blog.csdn.net/qq_40807206/article/details/109555162
转格雷码https://blog.csdn.net/jingfengvae/article/details/51691124

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值