FPGA经验分享——时序收敛之路

 

FPGA经验分享——时序收敛之路

  1132人阅读  评论(0)  收藏  举报
  分类:
FPGA研究(42)    FPGA之时序分析(2) 

首先感谢 coyoo 博主一直以来在 EDN 上分享他的经验,也感谢他这次慷慨拿出新作与我们分享。

 
非常希望能够拜读 coyoo 博主的大作,尤其希望对虚拟 JTAG 技术有一个深入的了解。
 
这里分享一下之前自己优化设计时序时的经验总结。相同的内容之前已经发在自己的 EDN 博客中了,不算违规吧?
 
内容分为五个部分(其实只有三个啦)。
 
  • PART1 引子
  • PART2 ISE 综合选项设置
  • PART3 代码风格
  • PART4 高速电路的设计方法
  • PART5 结语
 
PART1 引子
 
第一次做比较大型的设计,结果真的很悲剧。

布局布线以后,静态时序分析的结果和自己的预期相差很远,和综合后XST的估值也相差很远。时延里面,route时延占了绝大部分(logic占20%,route占80%)。

恶补了一些资料,给自己的设计总结了三个可能的问题:有些控制信号的扇出太大,没有做位置约束,不好的代码风格。

决定在这里记录自己通向时序收敛的过程。这不是一条平坦的路吧。

 
PART2 综合选项设置

 

这里想说一下我对综合选项的设置。设置的依据是ISE的帮助,网上的资料,以及自己的理解。请大家能为我指正设置得不合理的地方。没有提及的选项采用缺省设置。采用的EDA软件是ISE 13.2,综合器为XST。

【Synthesis Options】

Use Synthesis Constraints File & Synthesis Constraints File:

一般来说,会在implement之前,采用UCF文件对设计进行时序约束。实际上,在synthesis之前,可以先采用XCF文件对设计进行时序约束,以使XST针对时序约束进行synthesis,在synthesis时产生更好的网表。按照网上的说法,XCF中的时序约束应当要比实际需求更紧一些。

XCF文件的实际效果嘛。我谈一下自己使用XST的情况吧。XST完成综合(synthesis)后,会产生一个估计的最大工作频率。某一次综合后,我使用XCF文件将最大工作频率约束的比XST的估值大一些,再重新进行综合后,XST给出的估值确实增大了少许。大部分时候,在重新综合后是看不到效果的。至于对最终结果的影响,就不得而知了。

XCF文件的语法与UCF文件完全一致。与UCF不同,ISE没有为XCF提供编辑的工具,只能自己用文本编辑器编辑。

Keep Hierarchy:

这个选项是设置是否在synthesis与implement中打破设计的层次结构。选项【yes】和【no】很容易理解。选项【soft】的意思则是在综合时不打破层次结构,而在之后打破层次结构。

个人理解。打破层次结构后,有些信号就变了,不利于分析与约束。而打破层次结构,更有利于电路的优化。

【HDL Options】

FSM Encoding Algorithm:

有限状态机的编码方式。我采用了【One-Hot】(独热码)。其优缺点相信大家都非常清楚,不再赘述了。

Case Implementation Style:

case语句的实现方式。使用verilog时,缺省状态下XST不会把case语句综合成你想象的结构。这点,大家可以自己写一段简单的代码试试。

以一个独热码状态机为例:

 

以下是代码片段:
reg [2:0] sta;

case(1'b1)

sta[2]: ...;

sta[1]: ...;

sta[0]: ...;

endcase
 

 

首先,综合出来的电路不止判断一个比特。XST不知道sta只会出现3'b100、3'b010、3'b001三种可能,它会把诸如3'b101这样的状态也考虑在内,大概把电路综合成如下的样子:

 

以下是代码片段:
reg [2:0] sta;

case (sta)

3'b001: ...;

3'b010: ...;

3'b100: ...;

default: ...;

endcase
 

这样,采用独热码似乎没有什么意义。而且因为采用独热码时,sta的比特数比采用格雷码时更多,复杂度反而还增加了。

另外,XST也可能没有把case语句转换为并行结构,而是有优先级的结构。

【Case Implementation Style】中有三个选项:【Full】、【Parallel】与【Full-Parallel】。其中,【Full】针对上述的第一点,向XST说明有些状态是不可能出现的,让XST不要考虑太多;【Parallel】让XST将case语句综合为并行的电路结构;【Full-Parallel】则是两者的结合。

对于状态机,这项设置的影响很大。器件为xc6vlx240t-1ff1156时,同样代码的8状态独热码状态机,缺省设置时占用11个寄存器、6个查找表,只能工作在575 MHz时钟频率下;改为【Full-Parallel】设置后,占用11个寄存器、3个查找表,可以工作在900 MHz时钟频率下。

【Xilinx Sepcific Options】

Max Fanout & Register Duplication:
 
寄存器的最大扇出。扇出是一个门需要驱动的门数目。如果一个门的扇出很大,那么它的输出布线将非常拥塞,布线的时延可能就会很长。在时序报告中,如果看到逻辑一条路径的逻辑时延很短,但布线时延很长,很可能的原因就是信号的扇出太大。这样,即使设计时保证了此处的组合逻辑很简单,也无法减少时延。
 
一个有效的方法是寄存器复制。也就是说,像下图一样,为大扇出的门加入一些副本,让这些完全相同的门来分担扇出。寄存器复制可以通过代码来实现,不过显然,这样做是非常麻烦的。而【Max Fanout】这个选项,能够使XST自动实现寄存器复制。在综合时,通过这个选项为所有寄存器都加上扇出的限制,一旦寄存器的扇出大于设置的值,XST会自动进行寄存器复制。在我的设计中,将最大输出设为20。
 
不过这么做没法解决所有的问题。一方面,XST只能做寄存器复制,对于扇出大的组合逻辑,就没有办法了;另一方面,XST也无法跨越模块进行优化。对于第一个问题,可以对大扇出的组合逻辑一级寄存器缓冲一下,这样就可以复制了。对于第二个问题,采用扁平化的设计是一种方法,不过这样会为设计带来很大的困难。 另一种方法,是在模块的边界处加入寄存器,即对模块的输入输出都进行缓存。这是比较推荐的做法。
 
顺便提一句,在【Map Properties】中,同样有【Register Duplication】的选项。该选项是根据时序约束(而非对扇出的限制)来进行寄存器复制。
 
Equivalent Register Removal:
 
把设计中等效的寄存器去掉,合并为一个。显然,这个选项让人感觉与【Register Duplication】是互斥的。但是,在设置时,这两个选项是能够同时勾选的。因为不知道同时勾选的效果会是怎样的,我把这个选项前面的复选框去掉了。
 
要点总结
  • case 语句未必被综合为并行电路。需要并行电路的话,需要设置【HDL Options】中的【Case Implementation Style】。
  • 配合【Max Fanout】与【Register Duplication】来进行全局的寄存器复制,以减小扇出。
 
PART3 代码风格
 
引子中提到的那个设计,已经基本完成了。优化后,电路工作的时钟频率提升了一倍左右,还是有明显效果的。
 
在通向时序收敛的路途中,Xilinx的一些白皮书、《高级FPGA设计-结构、实现和优化》、特权与rickysu等前辈的博文,都对我有很大的帮助。不过,在这一过程中,我还是强烈地感觉到相关的资料太少、太简略。
 
现在回头想想,能够理解这种现象的原因了。毕竟是进行FPGA设计,完成设计后,大量的工作还是要交给EDA工具。这之中,能够人为干涉的内容有限,干涉的效果也未必好。 改进时序的过程中,主要的工作还是在静态时序分析的基础上优化代码。
 
因此,我们要做的其实还是:在设计时写出那些能够被EDA工具高效综合、便于EDA工具优化的具有良好代码风格的HDL程序。在FPGA开发的过程中,我深深地体会到HDL是非常容易被误用的。尤其是在初学的时候,因为对电路的理解不够深,常常写出一些综合效率很低的代码。或许,针对HDL的也需要一本《Effective C++》吧!
 
在这个部分中,主要介绍一些有利于优化电路时序的代码风格。 这些内容主要来源于我自己对于专著与Xilinx的白皮书的理解,如果有理解错误的地方,还请指正。
 
1. 复位
 
能不用复位就不用复位;不能的话,采用同步复位。
 
从学习FPGA开始,大量的示例代码中,模块都带有一个异步复位端口。这本身没有什么问题:有些电路,比如计数器、状态机,寄存器没有初值的结果是灾难性的;而FPGA中的寄存器单元又具有异步复位的引脚,何乐而不为?
 
然而,在大型设计中,这却真的会带来一些问题。
 
首先,如果所有模块都带有复位端口,复位信号就会具有很大的扇出,导致布线拥塞,影响电路的速度。而且大扇出信号到达不同模块的时间,会有很大的差距。
 
XST本身会对这种扇出极大的信号(时钟、复位等)进行处理:将大扇出的信号接到BUFG上,以缓解大扇出信号的各种问题。不过,ISE映射(MAP)时对连接BUFG的管脚的类型有要求(必须为时钟管脚),否则将报错。如果确实无法采用时钟管脚作为复位管脚,可在UCF文件中添加语句放宽这一限制(具体的语句忘了,如果真出现了这一错误,在ISE的错误报告中会告知解决方案)。
 
复位的另一个问题,是阻止了综合器可能进行的一些优化,从而同时影响电路的速度与面积。这其中一个典型的例子,就是当移位寄存器具有复位信号时,便无法采用SRL单元实现。其它的这一类问题主要是因为异步复位造成的,将在之后介绍异步复位的内容中介绍。
 
其实,在FPGA设计中,有许多模块确实不需要复位。 在设计时,认真考虑一下这一点,去掉那些无用的复位信号。
 
下面来谈谈异步复位的问题。
 
异步复位的一个常见问题,是亚稳态问题。这一问题在很多资料中均有阐述。简单来说,就是异步复位信号的释放可能正好发生在寄存器的保持/建立时间内。这样,复位是否被释放是一种随机的状态:有些寄存器处于复位状态,而有些寄存器的异步复位信号则已经释放,导致不可重复的随机错误。这种错误如下图所示。
 
 
另外,异步复位将阻止综合器对电路进行优化。前文提到过,当有些电路具有复位信号后,综合器就无法对其进行一些优化。而另一种更常见的情况是,综合器能够针对同步复位的电路进行一些优化,而对异步复位的电路,则无法进行这些优化。在Xilinx的白皮书WP231《HDL Coding Practices to Accelerate Design Performance》中,举了一些这类的例子。比如:
  • 同步复位电路能够被综合为性能更好的基于硬核(块RAM、DSP等)的电路,异步复位电路则不能。
  • 综合器能够将同步复位信号与其它信号放在一起优化,而异步复位信号只是复位信号。
综上所述,同步复位要优于异步复位。采用同步复位时出现亚稳态的可能性很小,而且电路可得到更好的优化。
 
一些资料中提到,与异步复位相比,同步复位需要更多的寄存器(如下图所示)。不过,实际上不用担心这点: 较新的Xilinx器件中的寄存器都具有专门的同步复位端口,不需要额外的寄存器实现同步复位。
 
 
2. 条件判断语句
 
在HDL代码中,尽量不要使用多层嵌套的条件判断语句。
 
这么做的原因是多层嵌套的代码将被综合为具有优先级的电路。其中,最典型的例子是状态机。
 
以一个4状态的独热码状态机为例。
 
以下是代码片段:
     reg [0:3] sta;

     always@(posedge clk)
     if (sta[0]) ...;
     end else if (sta[1]) ...;
     end else if (sta[2]) ...;
     end else if (sta[3]) ...;
如果使用上述代码实现状态机,综合出的电路是具有优先级的电路。而如果采用下面的代码实现,并如 PART2 中所述,将【HDL Options】中的【Case Implementation Style】设为【Full-Parallel】,则综合出的电路为并行结构,关键路径明显减小。
 
以下是代码片段:
     reg [0:3] sta;

     always@(posedge clk)
     case(1'b1)
     sta[0]: ...;
     sta[1]: ...;
     sta[2]: ...;
     sta[3]: ...;
     endcase
两者的差别可以参考下面的示意图(A为优先级结构,B为并行结构)。 注意,只是示意图,实际的电路与之有较大差别。
 
可以看到,在第一段代码中,由于sta[0]、sta[1]、sta[2]、sta[3]的优先级依次递减,综合而成的优先级电路中有很长的组合逻辑链路,严重影响电路的速度;而第二段代码综合出的是并行电路,其关键路径要短不少。我对8状态的状态机进行了测试,并行电路的关键路径仅有优先级电路的一半。
 
为什么综合器没有把功能相同的电路优化为相同的结构? 答案是两者的逻辑其实并不相同。考虑sta为4'b0101的情况,优先级电路的输出与sta为4'b0100时的输出一致;而并行电路的输出则未知(这里指电路是并行结构;如果代码是并行的,但未开启之前所述的【Full-Parallel】选项,也将被综合为优先级电路)。
 
所以说,综合器并没有问题。如果可能出现4'b0101,确实需要优先级电路,否则电路将出现异常。但是,在独热码状态机中,不会出现4'b0101的情况,使用优先级电路,是一种浪费。
 
在实际设计时,有时候无法避免多层嵌套的逻辑。 并不是什么时候都有替代的并行电路,该嵌套的时候还是得嵌套,毕竟实现功能是首要的目的。如果关键路径确实无法满足需求,就需要考虑进行一些电路结构上的变换了。这部分内容,将在 PART4 中阐述。
 
3. 模块的输入输出寄存器
 
通常需要用寄存器对模块的输入与输出进行缓存。
 
对于连接至其它异步电路的输入输出,缓存是必须的。否则,可能导致错误。此时,用寄存器缓存输入,能够减小亚稳态发生的概率;用寄存器缓存输出,能够消除组合逻辑竞争与冒险带来的毛刺。
 
如果输入输出连接的是同步电路呢?这时候,不缓存输入输出不会引发错误,但缓存仍有一定的必要性。
 
一方面,是基于寄存器复制的需求来考虑的。如 PART2 中所述,综合器无法进行跨模块的寄存器复制。未经缓存的输入输出,即使扇出很大,也不会被复制。
 
另一方面,不缓存输入输出,容易在模块连接处形成关键路径。与缓存输入输出的模块相比,不缓存的模块的输入输出门延时要大不少。虽然也会去注意控制输入输出门延时的大小,但是毕竟外部的情况是未知的,不确定的因素太多。与其如此,倒不如为输入输出添加缓存,将输入输出门延时降到最低。
 
当然,也不是所有模块都必须缓存输入输出。比如,只完成子模块连接的顶层模块,就不该缓存输入输出(其实都在子模块内部完成缓存了)。
 
另外,电路内部的一些同步子模块,如果能够确保扇出不大,并且模块之间不形成关键路径的话,也可以考虑不缓存输入输出。不管怎么说,缓存需要占用资源,还带来一个时钟周期的延迟,并非免费的午餐。
 
总结
  • 能不用复位就不用复位;不能的话,采用同步复位。
  • 尽量不要使用多层嵌套的条件判断语句。
  • 一般情况下,需要用寄存器对模块的输入与输出进行缓存。
  • PART4 高速电路的设计方法
     
    在上一个部分中,说明了几种自己总结的利于设计出高速电路的代码风格。这里其实暗含着下面这样的场景:电路已经设计好了,而且已经很棒,只是需要用风格良好的 HDL 代码进行输入,以保证综合出的电路和设计的一样好。
     
    而在这一部分中,想讨论一下怎样设计高速电路。可以说,是进行 HDL 输入前的工作。
     
    最初的时候,是没有这个部分的。原因是这部分并非是自己总结的内容,更多还是从前辈们那儿吸取的经验。不过,少了这个部分,感觉有点缺乏完整性。毕竟 PART3 和 PART4 的内容,只是一些小技巧。而且我想了一下,将获取的知识再整理整理,也没有什么不好的。
     
    因此,这部分内容是我对 Steve Kilts 的《高级 FPGA 设计:结构、实现和优化》第 1 章 1.3 节(4 至 13 页)的总结。读过这本书的朋友可以跳过这个部分了。其中的例子大都是我自己归纳后想出的简单例子,可能还有些不妥之处,还请各位朋友指出。
     
    这里强烈推荐这本书。一般的 FPGA 书中,讲流程、接口的多,讲电路结构的感觉比较少,而这本书涉及的还挺多的;而比起 Parhi 的《VLSI 数字信号处理系统:设计与实现》这种比较纯粹的 IC 设计的专著(当然这本也很好),这本书又和 FPGA 有着非常紧密的联系。
     
    1 增加寄存器层次
     
    通过在组合逻辑中插入一级寄存器来优化关键路径。
     
    考虑下图所示的三输入加法器。此时电路的关键路径为两个加法器的门延时。
     
     
     
    以下是代码片段:
    always @ (posedge clk) begin
         // 第一层
         a_d <= a;
         b_d <= b;
         c_d <= c;
         // 第二层
         s <= m+c_d;
    end

    assign m = a_d+b_d;
    通过在两个加法器之间引入一级寄存器,能够将关键路径缩短为一个加法器的门延时。 当然,这会带来一个周期的时延,同时也引入了多余的寄存器,增加了电路面积。
     
     
    以下是代码片段:
    always @ (posedge clk) begin
              // 第一层
         a_d <= a;
         b_d <= b;
         c_d <= c;
         // 第二层
         c_2d <= c_d;
         m <= a_d+c_d;
         // 第三层
         s <= m+c_2d;
    end
     
    2 并行结构
     
    与通常所说的通过增加并行路数从而提高吞吐量的并行架构不同,这里指的是通过重新组织组合逻辑的结构,来优化关键路径。
     
    考虑一个链状的四输入加法器电路。其关键路径为三个加法器的门延时。
     
     
    以下是代码片段:
    assign m1 = a+b;
    assign m2 = c+m1;
    assign s  = d+m2;
    如果采用树状结构,则关键路径仅为两个加法器的门延时。
     
     
    以下是代码片段:
    assign m1 = a+b;
    assign m2 = c+d;
    assign s  = m1+m2;
     
    3 去除电路中的优先级编码
     
    这点就是 PART3 中所说的“尽量不要使用多层嵌套的条件判断语句”。
     
    考虑一个四状态的独热码状态机,采用优先级编码的电路有四层组合逻辑。
     
     
    以下是代码片段:
         reg [0:3] sta;

         always@(posedge clk)
         if (sta[0]) ...;
         end else if (sta[1]) ...;
         end else if (sta[2]) ...;
         end else if (sta[3]) ...;
    而如果使用 case 语句,并在【综合选项】中设置好对应的项目(见 PART2),得到的非优先级编码电路则是并行结构,速度更快。
     
     
    以下是代码片段:
         reg [0:3] sta;

         always@(posedge clk)
         case(1'b1)
         sta[0]: ...;
         sta[1]: ...;
         sta[2]: ...;
         sta[3]: ...;
         endcase
    这里需要强调的是,如果电路的逻辑确实需要优先级,还应该使用嵌套的条件判断语句,而不该盲目采用无优先级的电路。
     
    4 寄存器平衡(重定时)
     
    电路的速度,是由关键路径决定的。因此在设计电路时,如果有一条路径的门延时特别长,其它路径的门延时再短,该电路的速度也仍然很慢。因此,设计电路时,可以通过改变寄存器的位置,使得各路径的门延时变得平均,以达到提高电路速度的目的。
     
    考虑下面的五输入加法器。可以看到,第一级寄存器和第二级寄存器之间的路径长度是一个加法器的门延时,而第二级寄存器和第三级寄存器之间的关键路径长度是三个加法器的门延时。因此,整个电路的关键路径长度是三个加法器的门延时。
     
     
    以下是代码片段:
    always @ (posedge clk) begin
         // 第一层
         a_d <= a;
         b_d <= b;
         c_d <= c;
         d_d <= d;
         e_d <= e;
         // 第二层
         m1 <= a_d+b_d;
         c_2d <= c_d;
         d_2d <= d_d;
         e_2d <= e_d;
         // 第三层
         s <= m4;
    end

    assign m2 = m1+c_2d;
    assign m3 = m2+d_2d;
    assign m4 = m3+e_2d;
    经过寄存器平衡后,输入输出之间的逻辑关系没有改变,但两条路径的长度都变为两个加法器的时延,整个电路的关键路径长度缩短为两个加法器的时延。
     
     
    以下是代码片段:
    always @ (posedge clk) begin
         // 第一层
         a_d <= a;
         b_d <= b;
         c_d <= c;
         d_d <= d;
         e_d <= e;
         // 第二层
         m2 <= m1+c_d;
         d_2d <= d_d;
         e_2d <= e_d;
         // 第三层
         s <= m4;
    end

    assign m1 = a_d+b_d;
    assign m3 = m2+d_2d;
    assign m4 = m3+e_2d;
    在《高级 FPGA 设计:结构、实现和优化》中,还有一种“重新安排路径”的策略。对于这点,可能我的理解还不够深,感觉这一策略和上面第二种策略类似:都是通过重新组织组合逻辑的结构,以提升电路速度。此处就不对这种策略进行详细的说明了。
     
    总结
     
    • 最基本的策略:在组合逻辑中插入寄存器,通过引入时延与增大电路面积来提升电路速度。
    • 组合电路设计策略:提升组合逻辑的并行度,缩短门延时。
    • 条件判断电路设计策略:不需要优先级编码的话,采用无优先级的电路。
    • 整体设计策略:移动寄存器位置,平衡各组合逻辑路径的门延时。
     
    PART5 结语
     
    关于前一段时间优化电路速度工作的总结到这里就结束了。
     
    其实之前还打算通过使用位置约束,将各个模块约束在较小的范围内,以使 EDA 软件实现出更优化的电路。不过因为经验不足,实际操作时位置约束后性能反而更差。关于这点,还需要进一步的研究,也请朋友们指导一二。
     

你可能感兴趣的:(FPGA经验分享——时序收敛之路)