原创 3

2014-1-12 21:45 1037 13 13
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设计中,有许多模块确实不需要复位。在设计时,认真考虑一下这一点,去掉那些无用的复位信号。
 
下面来谈谈异步复位的问题。
 
异步复位的一个常见问题,是亚稳态问题。这一问题在很多资料中均有阐述。简单来说,就是异步复位信号的释放可能正好发生在寄存器的保持/建立时间内。这样,复位是否被释放是一种随机的状态:有些寄存器处于复位状态,而有些寄存器的异步复位信号则已经释放,导致不可重复的随机错误。这种错误如下图所示。
 
image1.jpg
 
另外,异步复位将阻止综合器对电路进行优化。前文提到过,当有些电路具有复位信号后,综合器就无法对其进行一些优化。而另一种更常见的情况是,综合器能够针对同步复位的电路进行一些优化,而对异步复位的电路,则无法进行这些优化。在Xilinx的白皮书WP231《HDL Coding Practices to Accelerate Design Performance》中,举了一些这类的例子。比如:
  • 同步复位电路能够被综合为性能更好的基于硬核(块RAM、DSP等)的电路,异步复位电路则不能。
  • 综合器能够将同步复位信号与其它信号放在一起优化,而异步复位信号只是复位信号。
综上所述,同步复位要优于异步复位。采用同步复位时出现亚稳态的可能性很小,而且电路可得到更好的优化。
 
一些资料中提到,与异步复位相比,同步复位需要更多的寄存器(如下图所示)。不过,实际上不用担心这点:较新的Xilinx器件中的寄存器都具有专门的同步复位端口,不需要额外的寄存器实现同步复位。
 
image2.jpg
 
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为并行结构)。注意,只是示意图,实际的电路与之有较大差别。
 
image3.jpg
 
可以看到,在第一段代码中,由于sta[0]、sta[1]、sta[2]、sta[3]的优先级依次递减,综合而成的优先级电路中有很长的组合逻辑链路,严重影响电路的速度;而第二段代码综合出的是并行电路,其关键路径要短不少。我对8状态的状态机进行了测试,并行电路的关键路径仅有优先级电路的一半。
 
为什么综合器没有把功能相同的电路优化为相同的结构?答案是两者的逻辑其实并不相同。考虑sta为4'b0101的情况,优先级电路的输出与sta为4'b0100时的输出一致;而并行电路的输出则未知(这里指电路是并行结构;如果代码是并行的,但未开启之前所述的【Full-Parallel】选项,也将被综合为优先级电路)。
 
所以说,综合器并没有问题。如果可能出现4'b0101,确实需要优先级电路,否则电路将出现异常。但是,在独热码状态机中,不会出现4'b0101的情况,使用优先级电路,是一种浪费。
 
在实际设计时,有时候无法避免多层嵌套的逻辑。并不是什么时候都有替代的并行电路,该嵌套的时候还是得嵌套,毕竟实现功能是首要的目的。如果关键路径确实无法满足需求,就需要考虑进行一些电路结构上的变换了。这部分内容,将在 PART4 中阐述。
 
3. 模块的输入输出寄存器
 
通常需要用寄存器对模块的输入与输出进行缓存。
 
对于连接至其它异步电路的输入输出,缓存是必须的。否则,可能导致错误。此时,用寄存器缓存输入,能够减小亚稳态发生的概率;用寄存器缓存输出,能够消除组合逻辑竞争与冒险带来的毛刺。
 
如果输入输出连接的是同步电路呢?这时候,不缓存输入输出不会引发错误,但缓存仍有一定的必要性。
 
一方面,是基于寄存器复制的需求来考虑的。如 PART2 中所述,综合器无法进行跨模块的寄存器复制。未经缓存的输入输出,即使扇出很大,也不会被复制。
 
另一方面,不缓存输入输出,容易在模块连接处形成关键路径。与缓存输入输出的模块相比,不缓存的模块的输入输出门延时要大不少。虽然也会去注意控制输入输出门延时的大小,但是毕竟外部的情况是未知的,不确定的因素太多。与其如此,倒不如为输入输出添加缓存,将输入输出门延时降到最低。
 
当然,也不是所有模块都必须缓存输入输出。比如,只完成子模块连接的顶层模块,就不该缓存输入输出(其实都在子模块内部完成缓存了)。
 
另外,电路内部的一些同步子模块,如果能够确保扇出不大,并且模块之间不形成关键路径的话,也可以考虑不缓存输入输出。不管怎么说,缓存需要占用资源,还带来一个时钟周期的延迟,并非免费的午餐。
 
总结
  • 能不用复位就不用复位;不能的话,采用同步复位。
  • 尽量不要使用多层嵌套的条件判断语句。
  • 一般情况下,需要用寄存器对模块的输入与输出进行缓存。

 

 

文章评论0条评论)

登录后参与讨论
EE直播间
更多
我要评论
0
13
关闭 站长推荐上一条 /3 下一条