FPGA之道(50)复位的设计

前言

复位设计在FPGA以及IC设计中,是一个十分重要的话题,关于这个话题,之前也作为重点总结过,但是今天我们要看的内容节选自《FPGA之道》,一起来看下作者对复位的见解。

复位的设计

为什么FPGA设计中要有复位

当FPGA芯片上电工作时,我们的FPGA设计到底处于一个什么样的状态呢?这个还真是说不准。有些厂商的FPGA芯片,其触发器、BLOCK RAM等记忆性单元,上电时默认为逻辑0;而另一些厂商的FPGA芯片,其触发器、BLOCK RAM等记忆性单元,上电时默认为逻辑1;甚至还有些厂商的FPGA芯片,其触发器、BLOCK RAM等记忆性单元,上电时默认的逻辑电平根本就不统一。试问,如果我们的FPGA设计不是一根筋通到底的话,那么从这样一个搞不清楚的初始状态开始工作,它的行为能靠谱么?即使我们在FPGA设计的HDL代码中对所有存储单元都进行了必要的初始化,但是这些初始化代码在实际中是否能够发挥和仿真时同样的作用,也得仰仗FPGA芯片的工艺。因此,为了确保系统能够从一个确定的状态开始工作,FPGA设计中必须要有一个复位信号。
除此以外,谁都不能保证自己的FPGA设计百分之百健壮,当外部输入信号出现不稳定或者非预期情况时,肯定无法保证FPGA设计行为的正确性。如果FPGA设计的行为出错是由于系统本身的不稳定或非预期情况导致的,这其实并不算是什么问题,因为如果系统本身有问题,那么我们为什么要苛求FPGA设计的行为不出问题呢?但是,如果系统已经恢复正常,可是FPGA设计的行为却永远都无法从问题中恢复,那这就是一个很严重的问题了。因此,为了确保系统经历了一次问题后,FPGA设计的行为也能够再次恢复正常,采用复位信号是最简单、直接且有效的方案。
综上所述,复位对于FPGA设计的重要性可见一斑,而事实上,几乎所有的FPGA设计中都引入了复位功能,因此我们有必要来好好的认识一下复位,有必要来深入了解一下如何设计复位。

复位方式的分类

FPGA设计的复位方式可以分为两大类,即同步复位与异步复位,它们的概念、区别及优缺点分别介绍如下:

同步复位

同步复位,是指若复位信号在时钟有效边沿到来时刻为有效,则执行一次复位操作,因此同步复位操作是瞬间的、离散的。
同步复位的优点如下:
首先,由于同步复位是离散的,因此其非常有利于仿真器的仿真;
其次,由于同步复位只有在时钟有效边沿到来时才生效,因此可以滤除高于时钟频率的毛刺,提高复位操作的可靠性;
第三,使用同步复位的系统可以被设计成为纯粹的同步时序电路,这样会大大有利于FPGA项目开发流程中的时序约束与时序分析环节的工作,而且综合出来的FPGA设计的性能一般也会较高。
同步复位的缺点如下:
首先,必须保证复位信号的有效持续时间长度大于一个时钟周期,否则该复位信号很可能被当做毛刺滤除掉,从而系统将漏掉一次应有的复位动作。
其次,由于同步复位信号与时钟的相关性很大,因此设计和实现时必须要考虑到各种延迟和时间参数,确保其能够被FPGA设计正确的接收并作出相应动作。
第三,由于大多数的寄存器都只有异步复位端口(参看【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元简介】小节),因此,如果采用同步复位,那么编译器势必会在寄存器的数据输入端口添加额外的组合逻辑,这样就会消耗较多的逻辑资源。

异步复位

异步复位,是指无论时钟信号的有效边沿沿是否到来,只要复位信号有效,就执行复位操作,直到复位信号变为无效才停止复位,因此异步复位操作是持续的、连续的。
异步复位的优点如下:
首先,由于大多数的寄存器都具有异步复位端口,因此采用异步复位可以节省资源。
其次,由于异步复位根本不关心复位信号与时钟信号及其他信号之间的关系,因此设计起来相对较简单。
异步复位的缺点如下:
首先,在异步复位信号释放的时候非常容易出现问题。原因有两点:第一、当异步复位信号释放的时刻和时钟有效沿比较接近时,就很容易导致寄存器的输出出现亚稳态,而亚稳态就是电平信号的电压较弱的状态,其危害可以参考【本篇->编程思路->时钟及时钟域->跨时钟域问题->两级采样法】小节中的分析;第二、由于复位信号所管辖的寄存器非常之多,而这些寄存器又位于FPGA芯片的不同地方,因此复位信号到达各个寄存器的路径延迟肯定是参差不齐的,所以,如果异步复位信号释放的时刻和时钟信号有效沿比较接近时,很可能导致一部分寄存器在该时钟有效沿之前完成复位,而另一部分寄存器在该时钟有效沿之后才完成复位,那么由于这一时钟周期的偏差,就很可能会导致后续的逻辑功能出现全面的混乱。
其次,异步复位信号非常容易受到毛刺等干扰的影响。

复位的设计方法

由于纯粹的异步复位会导致系统的功能紊乱,所以强烈建议不要使用,因此一般情况下还是推荐使用同步复位的。不过同步复位会额外消耗不少资源,因此当资源比较紧张的时候,采用低电平有效的(参看【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元简介】小节,可以发现触发器的复位大部分为低电平有效)异步复位、同步释放机制,是业界普遍推荐的能够很好的将这两者的优点集中到一起的方法。接下来的章节,将详细为大家阐述各种情况下复位的设计方法。

同步信号同步复位

如果复位信号是属于当前时钟域的,那么该复位信号就是一个同步信号,如果资源不是特别紧张的话,我们就可以放心采用同步复位的方式来直接使用该复位信号,代码示例如下:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		if(rst =1)then
			<statements>;
		else
			<statements>;
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	if(rst)
	begin
		<statements>;
	end
	else
	begin
		<statements>;
	end
end

注:这个范例还不是很完善,具体请参考【复位高扇出的解决方案】小节。

同步信号异步复位

如果复位信号是一个同步信号,我们也可以采用异步复位的方式来直接使用该复位信号,因为使用同步信号可以规避异步复位方式的缺点,并且还更节省资源。需要注意的是,鉴于大部分触发器都具有低电平有效的复位引脚,因此一般来说,复位信号设计为低电平有效的话会更加的节省资源。代码示例如下:

-- VHDL example 
process(clk, rst)
begin
	if(rst = '0')then
		<statements>;
	else
		if(clk'event and clk = '1')then
			<statements>;
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk, negedge rst)
begin
	if(!rst)
	begin
		<statements>;
	end
	else
	begin
		<statements>;
	end
end
注:这个范例还不是很完善,具体请参考【复位高扇出的解决方案】小节。

异步信号同步复位

如果复位信号并不属于当前模块的时钟域,那么该复位信号就是一个异步信号,此时无论是采用同步复位方式或者异步复位方式,都有可能造成系统行为的出错。这是跨时钟域问题所导致的必然结果,因此,我们可以参考【本篇->编程思路->时钟及时钟域->跨时钟域问题->两级采样法】小节中的分析,对该异步复位信号做相应的处理,先将其同步化,然后再用来复位系统即可。同样,如果资源不是特别紧张的话,我们就可以采用同步复位的方式来使用同步化后的复位信号,代码示例如下:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		tmp <= aRst;
		rst <= tmp;
	end if;
end process;

process(clk)
begin
	if(clk'event and clk = '1')then
		if(rst =1)then
			<statements>;
		else
			<statements>;
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	tmp <= aRst;
	rst <= tmp;
end

always@(posedge clk)
begin
	if(rst)
	begin
		<statements>;
	end
	else
	begin
		<statements>;
	end
end; 
注:这个范例还不是很完善,具体请参考【复位高扇出的解决方案】小节。

异步信号异步复位

与异步信号的同步复位一样,要想保证系统行为的正确性,异步信号在按照异步复位的方式对系统进行复位前,也必须先同步化,然后再应用一个同步信号异步复位的设计方法即可,代码示例如下:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		tmp <= aRst;
		rst <= tmp;
	end if;
end process;

process(clk, rst)
begin
	if(rst = '0')then
		<statements>;
	else
		if(clk'event and clk = '1')then
			<statements>;
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	tmp <= aRst;
	rst <= tmp;
end

always@(posedge clk, negedge rst)
begin
	if(!rst)
	begin
		<statements>;
	end
	else
	begin
		<statements>;
	end
end
注:这个范例还不是很完善,具体请参考【复位高扇出的解决方案】小节。

复位高扇出的解决方案

在FPGA设计中,复位信号的扇出可以说和时钟信号相当了,因为复位信号几乎是作用于设计中的所有寄存器,那么,在FPGA设计中,我们该怎么满足复位信号如此高的扇出要求呢?在本小节,我们就将给出两种解决思路,并请大家根据这类思路来修改之前小节的示例代码。

寄存器的复制

寄存器的复制是一种比较省事的一种方法,因为作为FPGA的开发者,我们无需改动太多,主要的工作都是由编译器帮助我们搞定。
通过寄存器的复制来解决复位信号大扇出的原理大致如此:假设FPGA设计中有1000个寄存器需要复位,而复位信号是由一个叫R的寄存器的输出驱动的,那么此时的复位信号的扇出即为1000。此时,如果我们能够让多个寄存器的输出都跟R保持一致,那么其实这些寄存器的输出也可用来对这1000个寄存器进行复位的,那么如果类似R的寄存器共有10个,并且我们均匀分配每一个类R寄存器所需要复位的寄存器的数量,再加上让这些类R寄存器在FPGA芯片中均匀排列,那么每一个类R寄存器都可以就近去复位其周围的100个寄存器,这样不但扇出减少了一个数量级,而且布线延迟也减小了许多。
按照上述原理,我们只要让复位信号在FPGA中被一个寄存器驱动,至于需要创建多少个类R寄存器以及它们该分布在什么位置?怎么布局布线?这些都交给编译器就好了,我们只需要为编译器提供正确的时序约束即可。下面,具体介绍一下我们该做的工作:
如果是同步信号作为复位信号,那么需要添加如下代码:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		rst <= reset;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	rst <= reset;
end

这样上例中的rst便成了R寄存器,编译器可以利用它来复制出若干个类R寄存器,当然了,在我们的代码中,还是直接使用rst来作为复位信号的。
如果是异步信号作为复位信号,那么需要修改异步信号同步化的那部分代码,将原有的两级采样扩展为三级采样,修改后的代码如下:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		tmp0 <= aRst;
		tmp1 <= tmp0;
		rst <= tmp1;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	tmp0 <= aRst;
	tmp1 <= tmp0;
	rst <= tmp1;
end

这样上例中的tmp0和tmp1便组成了两级采样来完成跨时钟域的正确传递,而rst便成了R寄存器,编译器可以利用它来复制出若干个类R寄存器,同样,在我们的代码中,还是直接使用rst来作为复位信号的。
综上所述,可见寄存器复制的方法应用起来比较简单,但是会多消耗一些寄存器资源罢了,是比较推荐的复位高扇出解决方案。

正确的利用全局时钟树

在【本篇->编程思路->时钟及时钟域->时钟域】章节中,我们介绍了时钟树的概念,FPGA中的时钟信号正是利用这样一个特殊的资源来实现时钟信号的大扇出的。那么,我们是否也可以利用全局时钟树资源来解决复位信号的高扇出问题呢?答案是肯定的,但是需要注意使用正确的方法。
再次重申——全局时钟树是保证信号到达整个FPGA芯片内不同触发器的时间差尽可能小的资源,而不是保证时钟信号到达触发器所消耗时间最短的资源。事实上,全局时钟树带给通过其上信号的群延迟还是比较大的,因此,为了抵消复位信号使用全局时钟树所带来的群延迟,我们必须为时钟信号也引入一个同样的延迟,从而保证系统能够在一个比较高速的时钟信号下面工作。
如果是同步信号作为复位信号,以Xilinx的FPGA为例,需要添加如下代码:

-- VHDL example 	
resetOnTree: BUFG port map(O => rst, I => reset);
clockOnTree: BUFG port map(O => clk, I => clock);	

// Verilog example
BUFG resetOnTree(.O(rst), .I(reset));
BUFG clockOnTree(.O(clk), .I(clock));
这样在FPGA设计其他部分的代码即可使用rst和clk来完成代码描述。
如果是异步信号作为复位信号,仍以Xilinx的FPGA为例,需要添加如下代码:
-- VHDL example 
process(clock)
begin
	if(clock'event and clock = '1')then
		tmp <= aRst;
		reset <= tmp;
	end if;
end process;
resetOnTree: BUFG port map(O => rst, I => reset);
clockOnTree: BUFG port map(O => clk, I => clock);

// Verilog example
always@(posedge clock)
begin
	tmp <= aRst;
	reset <= tmp;
end
BUFG resetOnTree(.O(rst), .I(reset));
BUFG clockOnTree(.O(clk), .I(clock));

注意,使用以上代码的先决条件是clock信号之前并没有走全局时钟树,否则再一次的上树过程可能会被优化掉。如果由于某些原因,clock信号已经是全局时钟树上的信号,那么此时可能就需要采用一些特殊手段来抵消全局时钟树为复位信号引入的延迟,例如通过使用具有不带BUFG的反馈回路的PLL、DCM等等。
综上所述,利用全局时钟树的方法来解决复位信号高扇出的问题,可以让我们更多的将命运掌握在自己的手中,但是它需要额外多消耗两个全局时钟树资源,并且也许仅仅这样还不够!因此实现起来难度较大。

减少不必要的复位扇出

复位信号是一个高扇出信号,并且同步复位每多一个扇出,就会增加一些逻辑资源;异步复位每多一个扇出,虽然几乎不消耗逻辑资源,但是肯定是要消耗布线资源的。再加上随着扇出的增多,需要复制的寄存器个数就会增多,而且时序约束就更难收敛,因此,如果可能的话,请尽量减少复位扇出的个数。
事实上,并不是所有的寄存器都需要复位信号的,只有那些初始状态会决定FPGA设计后续功能走向的寄存器等存储单元才是需要在复位时给于一个确定的初态值的,而那些位于数据流中间的寄存器往往是不需要确定的初态值的。请先看如下代码:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		if(rst =1)then
			enTmp <= '0';
			enOut <= '0';	
			addAB <= (others => '0');
			addCD <= (others => '0');
			sum <= (others => '0');			
		else
			enTmp <= enIn;
			enOut <= enTmp;	
			addAB <= A + B;
			addCD <= C + D;
			sum <= addAB + addCD;	
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	if(rst)
	begin
		enTmp <= '0';
		enOut <= '0';	
		addAB <= (others => '0');
		addCD <= (others => '0');
		sum <= (others => '0');	
	end
	else
	begin
		enTmp <= enIn;
		enOut <= enTmp;	
		addAB <= A + B;
		addCD <= C + D;
		sum <= addAB + addCD;	
	end
end

上述代码采用流水线(更多关于流水线的介绍请参考【本篇->编程思路->时空变换之空间换时间->流水线】章节)的形式实现了一个4输入的加法,相信大部分人在编写FPGA代码的时候,都采用类似上例的方式,将所有会被综合为寄存器的信号都列入了复位条件语句中。首先,这样做没有任何错误,因为这样可以100%保证FPGA设计在每次复位后都能进入一个固定的初态并开始工作。但是,这样做有些盲目,因为这种做法仅仅是充分的,却不是必要的,并且还会为系统增加额外的资源和编译负担。通过仔细分析上例代码,我们可以发现addAB、addCD、sum三个变量在复位的时候都是无需被重置的,因为后续模块只在使能信号enOut有效时才会读取sum的值,因此我们无需关心也无需确保enOut无效时的情况。所以上述代码可以修改为更为合理的代码如下:

-- VHDL example 
process(clk)
begin
	if(clk'event and clk = '1')then
		if(rst =1)then
			enTmp <= '0';
			enOut <= '0';			
		else
			enTmp <= enIn;
			enOut <= enTmp;	
			addAB <= A + B;
			addCD <= C + D;
			sum <= addAB + addCD;	
		end if;
	end if;
end process;

// Verilog example
always@(posedge clk)
begin
	if(rst)
	begin
		enTmp <= '0';
		enOut <= '0';	
	end
	else
	begin
		enTmp <= enIn;
		enOut <= enTmp;	
		addAB <= A + B;
		addCD <= C + D;
		sum <= addAB + addCD;	
	end
end

可见相比之前的例子,这段代码极大的减少了为设计引入复位机制所增加的开销。

全局复位与局部复位

全局复位指的是当复位信号有效时,整个FPGA芯片将会被重置为一个预先设定好的初始状态。可事实上,有些时候我们并不需要也并不希望重置整个FPGA芯片的状态,这就好比此时你正坐在电脑前上网,突然发现网络通信出现问题,导致网页打开错误,我想应该没有人会立即为此重启电脑吧?所以,在FPGA设计中还可以存在一种仅使部分FPGA芯片重置的复位信号,相应的,我们称其为局部复位。除了功能上的原因外,不同的时钟域也不可能共用同一个复位信号(即使我们希望使用一个复位信号来复位所有的时钟域,那么该复位信号被各个时钟域同步化后,也已经变成了多个局部复位信号),因此局部复位的情况是广泛存在的。
针对每一个FPGA设计,我们都需要根据其特点来确定该如何规划全局复位和局部复位,而最终确定的就叫做当前FPGA设计的复位体系。复位体系可以很简单,例如整个FPGA设计就只有一个全局复位信号;复位体系也可以很复杂,例如整个FPGA设计有一个全局复位信号和若干个局部复位信号,每一个局部复位信号内部又可划分为若干个子局部复位信号等等。不过在这里,本书推荐一种比较常用的复位体系规划模板,即“两级复位体系”,大体思路为:整个FPGA芯片必须有一个全局复位信号,如果FPGA设计包含不止一个时钟域的话,该全局复位信号经过不同的时钟域同步化后,转化为每个时钟域的全域复位信号,与此同时,根据实际情况将每个时钟域划分为若干个局部子域,并为每个子域提供一个局部复位信号。其中,全局复位信号或全域复位信号采用异步复位的方式,而各个局部复位信号则采用同步复位的方式。以单时钟域FPGA设计,复位信号均为同步信号为例,参考代码如下:

-- VHDL example 
process(clk, globalRst) -- area 1
begin
	if(globalRst = '0')then
		<statements>;
	else
		if(clk'event and clk = '1')then
			if(localRst1 = '1')then
				<statements>;
			else
				<statements>;
			end if;
		end if;
	end if;
end process;

process(clk, globalRst) -- area 2
begin
	if(globalRst = '0')then
		<statements>;
	else
		if(clk'event and clk = '1')then
			if(localRst2 = '1')then
				<statements>;
			else
				<statements>;
			end if;
		end if;
	end if;
end process;

...... -- other areas

// Verilog example
always@(posedge clk, negedge globalRst) // area 1
begin
	if(!globalRst)
	begin
		<statements>;
	end
	else
	begin
		if(localRst1)
		begin
			<statements>;
		end
		else
		begin
			<statements>;
		end
	end
end

always@(posedge clk, negedge globalRst) // area 2
begin
	if(!globalRst)
	begin
		<statements>;
	end
	else
	begin
		if(localRst2)
		begin
			<statements>;
		end
		else
		begin
			<statements>;
		end
	end
end

...... // other areas	

一般来说,各个不同的复位信号不太可能都是同步信号,因此大部分时候都需要做同步化以及高扇出处理,因此,建议在FPGA设计中单独使用一个模块,集中对所有复位信号进行同步化以及高扇出等的代码描述。

发布了806 篇原创文章 · 获赞 1541 · 访问量 151万+

猜你喜欢

转载自blog.csdn.net/Reborn_Lee/article/details/104351693