可综合的async_fifo设计(一)

异步FIFO设计

    • 一、基本概念
    • 二、设计思路
      • 2.1 设计前准备工作
        • 2.1.1 系统框图
        • 2.1.2 格雷码基础
        • 2.1.3 异步fifo工作流程举例
        • 2.1.4 异步fifo空满标志产生的算法设计
      • 2.2 RTL建模
        • 2.2.1 DPRAM建模
        • 2.2.2 WR_LOGIC建模
        • 2.2.3 RD_LOGIC建模
        • 2.2.4 PIPE模块建模
        • 2.2.5 格雷码处理模块建模

一、基本概念

1.异步fifo定义:跨读写时钟域的dpram(双口ram),fifo的读写时钟是相互独立的两个时钟,在各自时钟下实现fifo数据的读写功能。

2.异步fifo用途:

【1】跨时钟域的多bit数据传输;

【2】数据的缓存;

二、设计思路

2.1 设计前准备工作

2.1.1 系统框图

主要包括三部分:Dpram、rd_logic和wr_logic。构成如下图2-1.1的系统框图。
可综合的async_fifo设计(一)_第1张图片

图2-1.1 本设计的异步fifo系统框图

具体设计之前,定义各个模块的基本功能。Dpram本质上是一个读写ram,在wr_en(写使能)和rd_en(读使能)的作用下,在各自时钟沿完成数据的写入和读出。

Wr_logic为写时钟域逻辑模块,主要实现:
[1] 在wr_en下wr_addr增加,并将该地址作格雷码处理待输出给rd_logic;
[2] 写满标志wr_full判断及产生;
[3] 接收rd_loic传来的rd_addr_gray,打2-3拍完成地址同步;
rd_logic为读时钟域逻辑模块,主要实现:
[1] 在rd_en下rd_addr增加,并将该地址作格雷码处理待输出给wr_logic;
[2] 读空标志rd_empty判断及产生;
[3] 接收wr_loic传来的wr_addr_gray,打2-3拍完成地址同步;

2.1.2 格雷码基础

作为一种计算机编码方式,格雷码具有如下特点:循环码;相邻码或对称项编码仅有1bit不相同。举例,如下2-1.2列出了0-15的4bit格雷码,可见,十进制码0和1、1和2、14和15(相邻码)对应的Gray4仅有1bit不同,其余位全相同;再观察0和15、1和14、7和8(对称项码)也仅有1bit不同,其余位全相同;再观察,gray4的第0bit,以0110作为循环节,次低位(第1bit)以00111100位循环节,第2bit按照0000_1111_1111­_0000为循环节,且循环节中0和1的个数是相等的。
可综合的async_fifo设计(一)_第2张图片

图2-1.2 十进制数0-15的4bit格雷码对照图

格雷码具有相邻码仅有1bit相异的特点,故在跨时钟域处理时,用作地址传输时,可以有效减少重汇聚(re-coverage,即多bit数据跨时钟域传输时,若时钟采样刚好发生在数据变化时刻,采样到的各bit数据就会不确定,有可能是变化前,也有可能是变化后的)现象发生的概率。这就是异部fifo采用格雷码地址传输的原因。

2.1.3 异步fifo工作流程举例

在RTL建模之前,先举例了解async_fifo的工作机制。如下图2-1.3,为一个深度为8的async_fifo。

可综合的async_fifo设计(一)_第3张图片

图2-1.3 深度为8的fifo

NOTE1:规定写指针总是指向下一个将要写入的地址,读地址指向当前读出的地址;

NOTE2:写满后不能继续写,有数据保护,读空后不能再读,避免错读;

①开始,fifo为空,wr_ptr(蓝色为写指针)和rd_ptr(橘黄色为读指针)均指向0地址。向fifo中写入3个数据后,蓝色指针指向addr3,此时未写满。接下来有两种操作,1继续写;2不写,读数据;若选择操作1,蓝色指针向上指直到写满并产生wr_full标志,则不能再写入数据,直到进行读操作,若选择操作2,橘黄色指针递增,当橘黄色指针指向addr3的前一个地址即addr2时,fifo被读空,产生rd_empty标志,此后不能再读。(先写到0010(实际上写地址指向0110,即下一个地址)再读到0010,将写地址à读时钟域,读空标志产生)

②随后继续写,则读空标志消失,比如写到addr6(0101),随后可进行读操作,读到addr6则fifo再次读空并产生rd_empty标志;若写到addr6后继续写,写到addr7后达到fifo_deepth,再执行写操作,写指针会循环一圈,重新指向addr0(此时写地址记成wr+1:addrx),直到再次写到wr+1:addr3,此时fifo倍写满,由于写满保护,则后续只能进行读操作。若一直进行读操作,读指针递增至rd:addr7,之后读指针也循环一圈,直到再次指向addr3(记作rd+1:addr3),再一次产生rd_empty标志。

③至此,读写指针都循环一圈,重新指向addr3回到新的同一个起点,此记作一个round,而后的操作重复执行①、②。

将上述读写流程用流程图描述,见图2-1.4.

可综合的async_fifo设计(一)_第4张图片

图2-1.4 深度为8的fifo读写流程示意图(1个循环)

2.1.4 异步fifo空满标志产生的算法设计

按照图2-1.4所述读写流程,发现规律:

1、在读写流程中,当读写指针都在同一个循环之内(读写指针轮回的次数相同)时,

wr_addr≥rd_addr

此时读写指针的状态就是读指针滞后于写指针(即rd_addr总是追赶wr_addr),当rd_addr追赶上wr_addr时(认为相等),即把写的内容全部读完,则rd_empty标志产生。

2、写满、读空保护机制的存在,在逻辑上总是“先写后读”,这就使得写指针总是先于读指针完成一次轮回,当写指针来到新的一圈轮回时,此时,

Case1:rd_addr≥wr_addr

意思是,写指针已经写完一圈了,即将和读指针相遇,直到再次追上rd_addr(认为相等),则把所有的空余空间全部写完,则wr_full标志产生。

若读使能有效,则rd_addr也会递增,在wr_addr轮回一圈之后rd_addr也会轮回新的一圈,记此时两者仍在同一等级(至少是在同一圈),此时,

Case2:wr_addr≥rd_addr

很不幸,rd_addr再次踏上追逐wr_addr的道路,当两者再次相等时,rd_empty标志产生,再次读完。此后循环1、2步骤。

基于上,需要记录读写指针所在圈数轮回的不同,用 奇偶数圈标志(0、1、2、3…….),转两圈就回到偶数圈,这样可以在原有指针地址的基础上, 扩展1bit记录奇偶圈;新的一圈时,地址表示见下图2-1.5。

可综合的async_fifo设计(一)_第5张图片

图2-1.5 深度为8的fifo读写地址轮回一圈的表示方法

NOTE1:上述圆圈转一圈回到起点,该圈成为映射圈。

NOTE2:十进制地址0-7之后就会变成0,这是fifo的一次轮回(因为fifo物理上不是一个圆圈),当十进制地址新的一圈轮回结束后,再次回到起点,这称为映射圈(格雷码映射圈)的一个轮回。

深度为8的fifo,用3bit(8个地址)+1bit(奇偶圈标志)表示,上图所示,红色线表示圈的起点,绿色线标志圈的终点(0-7为第0圈,扩展位为0,15-8为轮回一圈,扩展位为1),当从0-7再从15-8时,地址再次变成0(0000)回到偶数圈,新的一个映射圈轮回开始。

观察知,上述映射圈实际上被16个地址平均分配,十进制数对应0-15,格雷码对应0000-1000,因为格雷码循环码的特征,正好可以用来记录fifo的奇偶圈,又因为格雷码具有对称位置相差1bit的特征,即十进制0-15的gray4只有最高位相反。所以,十进制地址计数器只会计数到7,只不过不同的两圈用不同的格雷码映射,两者结合可以完成空满标志的判断。

综上分析,格雷码映射圈顺时针循环,从十进制0-7,再从15-8,而体现在格雷码上,除去最高位(bit3),低3位是循环的,这正好符合设计初衷。至此,完成空满标志产生的算法设计。

2.2 RTL建模

2.2.1 DPRAM建模

如图2-2.1,为DPRAM的接口示意图。该模块功能简单,只需要在rd_clk和wr_clk下,当使能信号有效,分别向ram中写入和读取数据即可。

可综合的async_fifo设计(一)_第6张图片

图2-2.1 深度为8的dpram的接口示意图

RTL代码如下:

module dpram # (
	parameter WR_WID = 8 , 
	parameter RD_WID = 8 ,
	parameter ADDR_WID = 11
)(	
	input rst_n ,
	
	input [WR_WID - 1 : 0] wr_data ,
	input wr_en ,
	input [ADDR_WID - 1 : 0] wr_addr ,
	input wr_clk ,
	input wr_full ,

	input rd_en ,
	input [ADDR_WID - 1 : 0] rd_addr ,
	input rd_clk ,
	input rd_empty ,
	
	output reg [RD_WID - 1 : 0] rd_data 
);

localparam FIFO_DEEPTH = 1024;
reg [WR_WID - 1 : 0] fifo_arr [FIFO_DEEPTH - 1 : 0];

//------------------------------------------
// 写时钟域,写入数据
//------------------------------------------
always @ (posedge wr_clk) begin 		// fifo_arr不进行初始化,若需要,在tb中初始化
	if (wr_en && ~wr_full) 				// wr_en有效 
		fifo_arr[wr_addr] <= wr_data;
end

//------------------------------------------
// 读时钟域,读出数据
//------------------------------------------
always @ (posedge rd_clk, negedge rst_n) begin
	if (!rst_n)
		rd_data <= {RD_WID{1'b0}};
	else if (rd_en && ~rd_empty ) begin //rd_en使能,且fifo不是空的 										
		rd_data <= fifo_arr[rd_addr];
	end
end

endmodule

2.2.2 WR_LOGIC建模

Wr_logic模块的逻辑接口如下图2-2.2所示,基本逻辑功能在2.1.1中描述。

可综合的async_fifo设计(一)_第7张图片

图2-2.2 Wr_logic模块的接口示意图

核心RTL代码如下:

【1】归一化格雷码输出

//---------------------------------------
// wr_addr_out_gray的归一化处理
// 主要实现奇数圈时,格雷码最高位的取反,而保持其他bit不变
//---------------------------------------
always @ (posedge wr_clk, negedge rst_n) begin
	if (!rst_n)
		wr_addr_out_gray_normal <= {ADDR_WID{1'b0}};
	else if (circle_high)
		wr_addr_out_gray_normal <= {~wr_addr_out_gray[ADDR_WID - 1], wr_addr_out_gray[ADDR_WID - 2 : 0]};
	else if (!circle_high)
		wr_addr_out_gray_normal <= wr_addr_out_gray;
end

其中,circle_high为wr_logic模块的奇偶圈标志,为1表示十进制写指针在奇数圈,初始值为0。如前所述,当写指针1个轮回时,其格雷码地址除最高位相反,其余位与在前一圈是相同的。否则,在偶数轮回时,写指针格雷码最高位0.

【2】写指针增加

//-------------------------------------
// 写时钟域地址回环逻辑,在wr_en有效时,
// 地址按照FIFO_DEEPTH(ADDR_NUM)自增,当每一次增加到等于FIFO_DEEPTH时,
// 二进制地址清零,但在格雷码地址上进行如下操作:
// 将最高位取反,其他位数值保持不变
//-------------------------------------
assign circle_high = wr_circle_flag ;		// circle_high有效区间应该和二进制地址同步(0-1023之间为1或者0)

always @ (posedge wr_clk, negedge rst_n) begin
	if (!rst_n) begin
		wr_addr <= {ADDR_WID{1'b0}};
		wr_circle_flag <= 1'b0;
	end else if (wr_en && wr_addr == ADDR_NUM) begin
		wr_addr <= {ADDR_WID{1'b0}};
		wr_circle_flag <= ~wr_circle_flag;
	end else if (wr_en && ~wr_full ) begin
		if ((rd_circle_high_sync == ~circle_high && rd_addr_sync_gray_bin > wr_addr) || rd_circle_high_sync == circle_high)
			wr_addr <= wr_addr + 1'b1;
	end
end

其中,(rd_circle_high_sync == ~circle_high && rd_addr_sync_gray_bin > wr_addr) || rd_circle_high_sync == circle_high)是关键,前一半实际上做了写保护,即写满之后,写指针不再增加(最多增加到与rd_addr_sync_gray_bin相等),同时又限制了写指针的多余增加(因为wr_full是有延迟的,在这段延迟的时间里若wr_en有效,实际已经写满但是写指针还在增加,就会导致逻辑上的错误)。

所以,后半句语句是什么意思呢?这里给大家留一个思考问题。

NOTE:读写分开进行时,经过同步后的rd_clk中的格雷码地址rd_addr_sync_gray_bin转换成十进制地址后,虽然存在延迟,但在很长一段区间内,rd_addr是不变的(读完后读指针不变,写指针在变化),所以对多地址的读写,不会影响wr_addr的增加。

【3】写满标志wr_full产生

//---------------------------------------
// 写满判断及满标志产生
// 采用格雷码(归一化之后)判断
//---------------------------------------
always @ (posedge wr_clk, negedge rst_n) begin
	if (!rst_n)
		wr_full <= 1'b0;
	else if (wr_addr_out_gray_normal[ADDR_WID - 1] == rd_addr_sync_gray[ADDR_WID - 1]) // 这个状态实际上不做判断,因为同等情况已经在读时钟域上进行判断
		wr_full <= 1'b0;															   // 且此时只影响读空标志的产生
	else if ((wr_en_latch | wr_en) && wr_addr_out_gray_normal[ADDR_WID - 1] == ~rd_addr_sync_gray[ADDR_WID - 1]) begin // wr_addr_out_gray_normal在wr_en之外,需要锁存wr_en
		if (wr_addr_out_gray_normal[ADDR_WID - 2 : 0] == rd_addr_sync_gray[ADDR_WID - 2 : 0])
			wr_full <= 1'b1;
		else
			wr_full <= 1'b0;
	end else if (rd_en_sync)
		wr_full <= 1'b0;
end

写满标志wr_full的逻辑是本设计的关键之一,首先明确,wr_full是在写操作的时候产生的(即写使能有效),且判满标志采用格雷码判断。因为wr_en下,归一化的格雷码地址wr_addr_out_gray_normal是延时了1clk,所以得到wr_en_latch,使得wr_addr_out_gray_normal包络在写使能区间内。此外,写指针要比读指针多一个轮回(即写指针已经充满fifo深度从头开始追赶读指针)。

然,wr_full持续多长时间呢?(即何时清零)

有思路是通过if (wr_addr_out_gray_normal[ADDR_WID - 2 : 0] == rd_addr_sync_gray[ADDR_WID - 2 : 0])中的else语句来判断,即除去最高位外格雷码相等,但实际上,else中的条件在整个else if ((wr_en_latch | wr_en) && wr_addr_out_gray_normal[ADDR_WID - 1] == ~rd_addr_sync_gray[ADDR_WID - 1])大前提都不满足(此时写使能已经失效),故这种判断有待改进。本设计采用另外一种思路:当写满标志产生后,不能再写入(写满保护),直到再次读的时候,将wr_full清零。

2.2.3 RD_LOGIC建模

RD_LOGIC模块类似Wr_logic模块,基本功能需求2.1.1中描述,以下对核心部分进行阐述。

接口模块如下图2-2.3所示。

可综合的async_fifo设计(一)_第8张图片

图2-2.3 Rd_logic模块的接口示意图

【1】归一化格雷码地址产生

此部分逻辑与写时钟域的处理相同。

【2】读地址增加

//-----------------------------------
// rd_en使能,读地址增加
//-----------------------------------
reg rd_circle_flag; 													// 写指针循环圈数标志位,为1表示表示奇数圈,即映射格雷码,为0表示偶数圈,即正常格雷码;
assign circle_high = rd_circle_flag;

always @ (posedge rd_clk, negedge rst_n) begin
	if (!rst_n) begin
		rd_addr_in_bin <= {ADDR_WID{1'b0}};
		rd_circle_flag <= 1'b0;
	end else if (rd_en && rd_addr_in_bin == ADDR_NUM) begin
		rd_addr_in_bin <= {ADDR_WID{1'b0}};
		rd_circle_flag <= ~rd_circle_flag;
	end else if (rd_en && ~rd_empty && ( (rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) 	 // 同圈数,读小于写 ,不同圈,读地址大于写
									||   (											circle_high == ~wr_circle_high_sync)))   // 读地址增加的条件(重要)
		rd_addr_in_bin <= rd_addr_in_bin + 1'b1;
end

关键代码(rd_en && ~rd_empty && ((rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) || ( circle_high == ~wr_circle_high_sync)),仔细观察,实际上和写时钟域构成了互补逻辑因为只有当读写指针在同奇偶圈轮回时才进行rd_empty的判断((rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) || ( circle_high == ~wr_circle_high_sync)的前半句是为了限制rd_addr_in_bin超过写指针从而实现读空保护。后半句是说,不同圈时,读地址可以一直递增到fifo_deepth。后半句语句不可少,否则地址增加条件会缺少。

NOTE:这里的读空保护,因为fifo被写过的地址对应的数据依然还是存在的,所以读空标志产生后,如果读使能有效且rd_empty无效,则是可以读出数据的,所以需要在检测到empty有效之后再次使能读信号。这里的保护应该理解为不多读。

【3】读空标志产生

//-----------------------------------
// rd_empty产生逻辑
//-----------------------------------
always @ (posedge rd_clk, negedge rst_n) begin
	if (!rst_n)
		rd_empty <= 1'b0;
	else if (rd_addr_out_gray_normal[ADDR_WID - 1] == ~wr_addr_sync_gray[ADDR_WID - 1]) // 这个状态实际上不做判断,因为同等情况已经在读时钟域上进行判断
		rd_empty <= 1'b0;
	else if ((rd_en | rd_en_latch) && rd_addr_out_gray_normal[ADDR_WID - 1] == wr_addr_sync_gray[ADDR_WID - 1] ) begin 	// 格雷码地址完全相等,则读空标志产生
		if ( rd_addr_out_gray_normal[ADDR_WID - 2 : 0] == wr_addr_sync_gray[ADDR_WID - 2 : 0])
			rd_empty <= 1'b1;
		else
			rd_empty <= 1'b0; 			// 该条件实际达不到,故清零rd_empty不可靠(rd_en此时已经等于零)
	end else if (wr_en_sync)			// 当再次写入时,则非空
		rd_empty <= 1'b0;

end

读空标志rd_empty产生于,读使能区间内读写指针格雷码相同的情况,类似地,rd_empty的持续时间也是由下一个有效的wr_en信号确定。

2.2.4 PIPE模块建模

模块主要实现数据的打拍寄存功能,用于设计中的异步时钟域数据的同步处理以及latch信号的产生。逻辑接口如下:

可综合的async_fifo设计(一)_第9张图片

图2-2.4 PIPE模块的接口示意图

RLT代码如下:

always @ (*) begin
	addr_ff[0 +: ADDR_WID] = addr_in;
	vld_ff[0] = vld_in;
end

genvar j;

generate for (j = 1; j < PIPE_NUM; j = j + 1) begin : U_ADDR_SYNC

	always @ (posedge clk, negedge rst_n) begin
		if (!rst_n)
			addr_ff[j * ADDR_WID +: ADDR_WID] <= {ADDR_WID{1'b0}};
		else // if (vld_ff[j - 1])
			addr_ff[j * ADDR_WID +: ADDR_WID] <= addr_ff[(j - 1) * ADDR_WID +: ADDR_WID];
	
	end
	
	always @ (posedge clk, negedge rst_n) begin
		if (!rst_n)
			vld_ff[j] <= 1'b0;
		else 
			vld_ff[j] <= vld_ff[j - 1];
	end
	
end
endgenerate

//取出打拍后的地址数据
assign addr_sync = addr_ff[(PIPE_NUM - 1) * ADDR_WID +: ADDR_WID];
assign vld_out = vld_ff[PIPE_NUM - 1];

主要通过generate for实现。其相关语法规则可自行百度或者参阅----2001 V:IEEE1364-2001 Verilog lrm----Verilog_1364-2001标准链接

2.2.5 格雷码处理模块建模

主要包括十进制地址和格雷码地址的相互转换。De_2_gray主要用于异步时钟域的传输,Gray_2_de用于转换到异步域之后,限制该域的指针的增加,目的是为了作空满保护。两个模块为啥是这样,各位可以举例试一试便知,这里不做详细阐述。

【1】bin_2_gray的RTL建模

// reg [DAT_WID - 1 : 0] shift_rgt_reg;

// always @ (posedge clk, negedge rst_n) begin
	// if (!rst_n)
		// shift_rgt_reg <= {DAT_WID{1'b0}};
	// else
		// shift_rgt_reg <= data_in_bin;
// end

assign data_out_gray = data_in_bin ^ data_in_bin >> 1;

注意,使用非阻塞赋值会导致gray的产生滞后一个clk。

【2】gray_2_bin的RTL建模

genvar i;

generate for (i = 0; i < ADDR_WID; i = i + 1) begin : GRAY_2_BINA
	
	always @ (*) begin
		addr_bin_out[i] = ^(addr_gray_in >> i);
	end
	
end
endgenerate

注意,考虑代码的可综合性,使用generate for语句实现循环,不推荐使用在begin end模块中直接使用for循环(不一定可综合)作上述操作。

至此,各基本模块建模完成。
顶层RTL视图如下:
可综合的async_fifo设计(一)_第10张图片

图2-2.5 异步FIFO的RTL视图

剩余部分将于下节讨论。

你可能感兴趣的:(FPGA设计开发,Verilog,数字电路设计)