第2节 优秀的HDL代码风格
推荐给好友
打印
加入收藏
更新于2008-05-28 13:10:58

3.2.1 代码风格的含义

代码风格有两层含义:其一是Verilog的代码书写习惯;另一个则是对于一特定电路,用哪一种形式的语言描述,才能将电路描述得更准确,综合以后产生的电路更为合理。前者在2.5节已有涉及,本节主要介绍后者。

代码风格有通用风格和专用风格两大类,前者指不依赖于FPGA开发的EDA软件工具和FPGA芯片类型,仅仅是从HDL语言出发的代码风格;后者指和开发软件以及硬件芯片密切相关的代码风格,不仅需要关注EDA软件在语法细节上的差异,还要紧密依赖于固有的硬件结构。显然,前者具有较好的通用性,但性能未必最优。在使用时,如果有后续的进一步开发,建议使用通用风格;否则就可采用后者,以便极大地挖掘芯片潜力。
 
3.2.2 通用代码风格的介绍

1.逻辑复用与逻辑复制

在3.1.2节介绍过FPGA设计中面积和速度的转化关系,该理念始终贯穿HDL代码设计,二者典型的转换方式有逻辑复用和逻辑复制。前者通过速度换面积,后者通过面积换取速度,两者各有相应的应用范围。

  • 逻辑复用

逻辑复用是通过提高工作频率来节省面积的优化方法,经常用于存在多个资源可共享单元的设计中,是大规模FPGA设计的核心思想。为了便于理解,首先给出一个例子。

例3-6 幅度到功率转化模块中的逻辑复用。假设系统输入信号为I、Q两路,每路速率为5Mbps、位宽为16比特,分别给出普通实现和使用逻辑复用的实现代码。

功能分析:计算功率需要先将I、Q输入信号经过平方计算模块,然后再将2个平方和相加输出即可。

普通实现方式:

 

图3-11 功率统计模块的一般实现方式


逻辑复用方式:

 

图3-12 采用逻辑复用方式实现功率统计模块


经过比较可以发现,第1种实现方式需要两个16比特乘法器,第2种实现方式只需要1个乘法器,基本上将资源缩减到第1种方式的一半。目前,虽然有很多综合工具可以提供逻辑复用的选项,但不能因为此而放松对代码编程的要求。主要原因有两点:首先EDA工具的能力毕竟有限,很多情况下还是不能智能发现可以复用的逻辑;其次,不同综合工具的优化参数以及能力并不相同,所以综合结果对于设计者是不确定的。所以,逻辑复用主要还是通过代码体现。

  • 逻辑复制

逻辑复制是通过增加面积而改善设计时序的优化方法,经常用于调整信号的扇出。如果信号具有高的扇出(如:时钟和复位信号等),即要驱动很多后续电路,则要添加缓存器来增强驱动能力,但这会增大信号的时延。通过逻辑复制,使用多个相同的信号来分担驱动任务。这样,每路信号的扇出就会变低,就不需要额外的缓冲器来增强驱动,即可减少信号的路径延迟。例如用于产生控制信号的监控模块一般都有高的扇出,这时就往往需要考虑逻辑复制这一功能。图3-13(a)给出未采用逻辑复制的设计思路,其占用资源较少,但延迟大,容易出错;而采用逻辑复制的设计如图3-13(b)所示,延迟小,但占用的资源多。

 


(a) 未采用逻辑复制的设计模式 (b) 采用逻辑复制的设计模式

图3-13 逻辑复制实例示例


逻辑复用和逻辑复制是资源与速度的对立统一,目的都是为了提高设计性能、达到设计目标,但一个是为了节省面积来提高速度,另一个却是为了提高速度来占用额外的面积,两者之间存在转换和平衡的关系。在实际工程中,经常可以看到这两种方法的应用。在XST以及Synplify Pro综合工具中,用户可以设定最大扇出数,当某信号的扇出超过最大扇出值时,该信号会自动被综合工具复制,以降低扇出。

2.逻辑结构
逻辑结构主要分为链状结构(Chain Architecture)和树状结构(Tree Architecture)。一般来讲,链状结构具有较大的时延,后者具有较小的时延。所谓的链状结构主要指程序是串行执行的,树状结构是串并结合的模式,具体如例3-7所示。

例3-7 表3-3给出具有链状结构和树状结构的4输入加法器的实现实例。

 
        表3-3 4输入加法器的实现

 


从上例可以明显看出树状结构的优势,它能够在同等资源的情况下,缩减运算时延,从而提高电路吞吐量以节省面积。

3.if和case语句的使用原则

  • if和case语句的区别

if语句指定了一个有优先级的编码逻辑,而case语句生成的逻辑是并行的,不具有优先级。if语句可以包含一系列不同的表达式,而case语句比较的是一个公共的控制表达式。通常if-else结构速度较慢,但占用的面积小,如果对速度没有特殊要求而对面积有较高要求,则可用if -else语句完成编解码。case结构速度较快,但占用面积较大,所以用case语句实现对速度要求较高的编解码电路。嵌套的if语句如果使用不当,就会导致设计的更大延时,为了避免较大的路径延时,最好不要使用特别长的嵌套if结构。如想利用if语句来实现那些对延时要求苛刻的路径时,应将最高优先级给最迟到达的关键信号。有时为了兼顾面积和速度,可以将if和case语句合用。

  • if和case实例

下面给出两个分别使用if和case语句的实例,希望读者从中体会到二者的不同。

例3-8 使用if语句实现一个4选1的数据通路选择器。

module sdata_if(clk, reset, x, s, y);
       input clk; 
       input reset;
       input [3:0] x; 
       input [1:0] s; 
       output y;

       reg y;
       always @(posedge clk) begin
            if(!reset) begin
                    y <= 0 ;
           end
           else begin
                   if(s == 2'b00)
                          y <= x[0];
                   else if (s == 2'b01)
                         y <= x[1];
                   else if (s == 2'b10)
                         y <= x[2]; 
                   else
                         y <= x[3];
          end
end

endmodule

上述程序经过综合后,其中数据选择部分的RTL级结构如图3-14所示。从中可以看出状态变量s[1:0]通过y13、y14、y15以及y16是串行输入到复用器中的,且具有严格的逻辑顺序,其逻辑级数为4。



图3-14 用if语句实现的4选1选择器RTL级结构图


例3-9 使用case语句实现一个4选1的8位数据选择器。

module sdata_case(clk, reset, x, s, y);
         input clk; 
         input reset;
         input [3:0] x;
         input [1:0] s; 
         output y;

         reg y;
         always @(posedge clk) begin
               if(!reset) begin
                     y <= 0 ;
              end 
              else begin
                       case(s)
                             2'b00: y <= x[0];
                             2'b01: y <= x[1];
                             2'b10: y <= x[2];
                             2'b11: y <= x[3];
                     endcase 
             end
end

endmodule

上述程序经过综合后,其中数据选择部分的RTL级结构如图3-15所示。从中可以看出状态变量s[1:0]通过y13、y14、y15以及y16是并行输入到复用器中的,因此逻辑级数只有1级。

 

图3-15 用case语句实现的4选1选择器RTL级结构图


4.关键路径信号处理原则

在Verilog设计中,经常会遇到由于信号路径过长或信号来的比较晚,从而造成建立时间不够的情况[1]。这种引起电路建立时间不够的信号路径就称为关键路径。在复杂电路设计中必须有效地处理关键信号,尽量减少其时延,提高电路的工作频率。

  • 简单组合电路关键路径的提取方法

简单组合电路的关键路径提取方法就是拆分逻辑,将复杂逻辑变成多个简单组合电路的进一步组合,缩减关键信号的逻辑级数,如例3-10所示。

例3-10 简单关键电路的提取实例。对于语句:

assign y = a & b & c | d & e & b;

        从中可以看出,信号b为关键信号。现将其简单路径计算,再经过关键路径逻辑。

assign temp = a & b & c & d;
assign y = b & temp;

        通过关键路径提取,将信号b的路径由2级变成1级。拆分逻辑的方法就是布尔逻辑扩展,也被称为香农扩展,其原理如下式所示:

         可以看出,拆分逻辑可通过复制逻辑,缩短那些组合路径长的关键信号的路径延迟,从而提高工作频率。

  • 复杂always块中关键路径的提取方法

对于always模块中时间要求非常紧的信号,需要通过分布提取方法,让关键路径先行,保证改写后的描述与原always块逻辑等效。例3-11给出了提取并改善always模块中关键信号的实例。

        例3-11 always块中关键路径的提取和优化实例。

always@(w or x or y or z or in1 or in2) begin
   if(!w) begin
           if(x&&(! (y&&z)))
                  out = in1;
            else
                        out = in2;
   else if(y&&z)
                 out =in1;
          end
          else begin 
                 out = out;
          end
end

其中,若z=0,则原代码等效于if (!w) out = in1; else out = out;。若z=1,则源代码等效于if (!w && x && !y ) out = in1; else if(!w && x && y) out = in2; else out = out;。对于信号y也有类似的分析结果,因此y、z都是关键信号。因此通过优先首先计算关键路径,改写为:

always@(w or x or y or z or in1 or in2) begin
  temp = y && z; 
      if(!temp) begin
   if(x&&!w)
           out = in1;
   else 
                  out = in2;
   else if(temp)
                  out =in1;
          end
          else begin
                  out = out;
          end
end

5.避免出现意外锁存器

锁存器是电平触发的存储器,触发器是边沿触发的存储器,在同步电路设计中要尽量避免出现锁存器。在Verilog HDL设计中,很容易由于条件判断语句表述的不完整,而造成设计中会出现意想不到的锁存器。本质上,锁存器和D触发器的逻辑功能是基本相同的,都可存储数据,且锁存器的资源更少,具备更高的集成度。但锁存器对毛刺敏感,不能异步复位,因此在上电后处于不确定的状态。此外,锁存器还会使静态时序分析变得非常复杂,不具备可重用性。在FPGA芯片中,基本的单元是由查找表和触发器组成的,若生成锁存器反而需要更多的资源。因此,在设计中需要避免产生锁存器。这一事件具体可分为两种情况:其一是在if语句中,另一种是在case语句中。下面将对if和case语句造成的锁存器分别进行分析。

  •  由if语句造成的锁存器

        例3-12给出了在always块中使用if语句,但缺乏else分支而造成锁存器的情况。
        例3-12 表3-4给出由于if语句不完整而生成意外的锁存器示例


表3-4  if语句不完整的情况


        其经过Synplify Pro综合后,得到的RTL图分别如图3-16(a)以及(b)所示。



(a) 存在锁存器的RTL结构



(b) 不存在锁存器的RTL结构

图3-16 if语句生成锁存器的RTL示意图

可以看出,左边的语句块只有在a的值为1的情况下,data_in的值才能传递给data_out,但没有指定在a的值为0的情况下data_out的取值。这样在always语句块中,如果没有改变变量的赋值,变量值将保持不变,生成锁存器。如果希望在a=0时,data_out值为0,那么程序就如右例所示。本例说明利用else分支就不会生成锁存器。

  • 由case语句造成的锁存器

        例3-13给出了在always块中使用case语句,由于缺乏default分支而造成锁存器的情况。
        例3-13 表3-5给出由于case语句不完整而生成意外的锁存器示例

        表3-5 case语句不完整的情况

左边的例子中,当a[1:0]的值为2'00、2'01时,分别将data_in1或data_in2赋给data_out,在a为其余值的时候就生成了锁存器,data_out保持上一次的赋值保持不变;右边的例子比较明确,在a的值不等于2'00、2'01时,data_out的值为0,不会生成锁存器。

以上两个例子给出了如何避免生成意外锁存器。因此,如果用到if语句,最好有else分支;如果用到else语句,最好有default语句。即使需要锁存器,也通过else分支或default分支来显示说明。按照上面的建议,可以避免意想不到的错误,提高程序的稳健性和可读性。

6.流水线技术的使用

  • 流水线技术的工作原理

流水线能动态地提升器件性能,它的基本思想是对经过多级逻辑的长数据通路进行重新构造,把原来必须在一个时钟周期内完成的操作分成在多个周期内完成。这种方法允许更高的工作频率,因而提高了数据吞吐量。因为FPGA的寄存器资源非常丰富,所以对FPGA设计而言,流水线是一种先进的而又不耗费过多器件资源的结构。但是采用流水线后,数据通道将会变成多时钟周期,所以要特别考虑设计的其余部分,解决增加通路带来的延迟。

流水线设计的结构示意图如图3-17所示。

图3-17(b) 流水线的结构示意图

流水线的基本结构是将适当划分的N个操作步骤串连起来。流水线操作的最大特点是数据流在各个步骤的处理,从时间上看是连续的;其操作的关键在于时序设计的合理安排、前后级接口间数据的匹配。如果前级操作的时间等于后级操作的时间,直接输入即可;如果前级操作时间小于后级操作时间,则需要对前级数据进行缓存,才能输入到后级;如果前级操作时间大于后者,则需要串并转换等方法进行数据分流,然后再输入到下一级。

  • 流水线技术实例

例3-14 使用Verilog实现具有4级流水线结构的8位加法器。

module adder8_4(cout ,sum ,clk ,cina ,cinb ,cin);
        input [7 :0]cina ,cinb;
        input clk ,cin;
        output [7 :0] sum;
        output cout;
        reg cout ;
        reg cout1, cout2, cout3; 
        reg [2 :0]sum1;
        reg [4:0] sum2;
        reg [6:0] sum3; 
        reg [7 :0]sum;

        always @(posedge clk) begin // 低二位相加
        {cout1 ,sum1} = cina [ 1 :0 ] + cinb [ 1 :0 ] + cin; 
        end

        always @(posedge clk) begin //相加,并且将低4 位拼接起来;
                {cout2 ,sum2} = {{cina[3],cina[3 :2 ]} + {cinb[3],cinb[3 :2 ]} + cout1 ,sum1};
        end 

        always @(posedge clk) begin //相加,并且将低六位拼接起来;
               {cout3 , sum3} = {{cina[5],cina [ 5 : 4 ]} + {cinb[5],cinb [ 5 : 4 ]} + cout2 ,sum2};
         end 

         always @(posedge clk) begin //高2位相加,并且将8位拼接起来;
               {cout ,sum} = {{cina[7],cina [7 :6 ]} + {cinb[7],cinb[7 :6 ]} + cout3 ,sum3};
         end

endmodule

图3-18给出了Synplify综合后的RTL结构图,其中有4个2位加法器,添加了较多的辅助逻辑,使加法的运算时延降低到对2个比特的处理,从而可以应用在高速设计中。



图3-18 具有4级流水线结构加法器的RTL图


3.2.3 专用代码风格的简要说明

专用代码风格是指从FPGA器件特征角度考虑,尽可能利用芯片结构以及内嵌的底层宏单元,以取得最佳的综合和实现效果。对于同一个设计,使用适合于FPGA体系结构特点的优化设计方法,可以大大提高芯片利用率和设计实现速度。

1.Xilinx FPGA的体系结构特点

Xilinx FPGA芯片的三种可构造单元是:(1)可编程输入、输出块IOB,主要为逻辑阵列与外部芯片引脚之间提供一个可编程接口;(2)可编程逻辑块CLB,CLB主要由一个组合逻辑、几个触发器、若干个多选一电路和控制单元组成,若干个CLB有规则地组成FPGA逻辑单元阵列结构,以完成用户指定的逻辑功能;(3)各种连线资源,包括可编程的开关矩阵,内部连接点和金属线。它们位于芯片内部的逻辑块之间,经编程后形成连线网络,以连接芯片内的逻辑块及传递逻辑信息。

组合逻辑功能通过用户可编程的查找表实现。查找表是由静态存储器构成函数发生器,在此基础上再增加触发器来形成的,它是即可实现组合逻辑功能又可实现时序逻辑功能的基本逻辑单元电路。SRL16是Xilinx器件中独有的一种移位寄存器查找表,有4个输入用来选择输出序列的长度,能够以极少的硬件资源实现数据缓存和组合逻辑。

每个CLB中包含两个触发器,CLB的组合逻辑功能较少,触发器资源十分丰富。每个CLB中包含一个高速进位逻辑。专用进位电路速度远远大于采用传统的加速方法所能增加的速度。算术进位逻辑为有关算术运算中许多新的应用问题提供了有效的解决途径。

同时,Xilinx提供了片上RAM,特别是大量块RAM,可以配置成双口RAM或ROM,存储量大、速度快,且不占用逻辑资源,在设计实现中有着广泛的应用。

内嵌的宏单元包括硬核乘加器、硬核处理器、数字时钟处理模块以及高速串行接口,处理能力强,处理能力为片上最高,且不存在时序问题。如果合理地利用宏单元,可达到事半功倍的效果。


2.Xilinx FPGA 芯片专用代码风格

  • 时钟信号的分配策略

时钟分配网络是FPGA芯片中的特殊布线资源,由特定的引脚和特定的驱动器驱动,只能驱动芯片上触发器的时钟输入端或除了时钟输入端外有限的一些负载,其目的是为设计提供小延迟偏差和扭曲可忽略的时钟信号。

首先,使用全局时钟,可为信号提供最短的延时和可忽略的扭曲。全局网线由全局缓冲器BUFG来驱动,使用BUFG时,时钟信号经BUFG驱动后通过长线同时接到每个触发器的时钟端,减少传输延迟。如不使用BUFG,时钟信号按一般布线连接到不同CLB,时钟信号到达各触发器的延迟不一致,使同步时序电路出现不同步的现象。

其次,FPGA特别适合于同步电路设计,尽可能减少使用的时钟信号种类。如TTL电路设计中,经常采用的由组合逻辑生成多个时钟分别驱动多个触发器的设计方法对FPGA的设计不适用。因为这样做使得时钟种类很多,不能利用专用的时钟驱动器和专用的时钟布线资源,时钟信号只能由通用的布线资源拼凑而成,各个负载点上的时钟延迟偏差很大,会引起数据保持时间问题,降低工作速度。

第三,减小时钟摆率的另一种更有效方法是使用一个时钟信号,生成多个时钟使能信号,分别驱动触发器的时钟使能端,所有触发器的数据装入都由同一个时钟控制,但只有时钟使能信号有效的触发器才会装入数据,时钟使能信号无效的触发器则保持数据。这种方法充分发挥了FPGA器件体系结构的优势。

图3-19所示电路为分频器的一般设计和优化设计的对比。两种设计方法相比较,图(a)电路使用两个全局缓冲,实现两个触发器的异步控制。图(b)电路将异步控制转化为同步控制,只占用一个全局缓冲,利用时钟使能信号CE控制触发器的动作。因此图b电路占用更少的全局时钟资源,而且使用一种时钟信号更利用同步控制和减小时钟摆率。



(a) 分频器的普通设计




(b) 分频器的优化设计        
图3-19 分频器两种设计电路的比较

第四,要避免时钟信号毛刺。由图3-20所示电路可知,当二进制计数器从0111向1000变化时,必然会出现一个1111的过渡过程,D触发器的时钟就会产生毛刺,毛刺出现的时间很短,但对于高速处理来讲,足以使触发器误动作。图b所示电路就是使用同步时钟,并使用时钟使能CE端避免时钟信号毛刺的设计电路。


(a) 会产生毛刺的传统设计 (b) 能消除毛刺的优化电路

图3-20 两种设计电路的比较

  • SRL16结构的使用

SRL16是一种基于查找表(LUT)的移位寄存器,可用于构建高密度DSP结构,如滤波器,能够大幅消减硬件资源。在Virtex-5芯片中为6输入的查找表,在其余系列芯片中都为4输入查找表。其位宽(B)和深度(D)可以任意配置,最大的特点就是占用Slice资源特别少。其占用Slice资源M的计算公式为:

        其中,R[]函数的意义是取整。从上式可以看出,深度为1的移位寄存器和深度为16的移位寄存器所占用的Slice资源是一样的。 
       
        例3-15 使用SRL16生成位宽为8,深度为10的移位寄存器。

module lut_ram(clk, d, q);

input  clk;
input [7 : 0] d;
output [7 : 0] q;

srl16_based_ram srl16_based_ram(
.clk(clk),
.d(d),
.q(q)
);

endmodule

其中srl16_based_ram例化了Xilinx提供的RAM_Based_Shiftreg的IP Core,使用SRL16结构完成了移位寄存器。上述程序经过Synplify Pro综合后,得到的RTL结构如图3-21所示。



图3-21 SRL16结构移位寄存器的RTL结构示意图

在ModelSim 6.2b中完成仿真,其结果如图3-22所示



图3-22 SRL16结构移位寄存器的仿真结果示意图

  • 触发器资源的分配技术

由于FPGA是一种触发器密集型可编程器件,因此系统的逻辑设计就应该充分利用触发器资源,尽可能降低每个组合逻辑操作的复杂度。

首先,应尽量使用库中的触发器资源。因为FPGA触发器资源丰富,而且开发系统在划分逻辑块时,对D触发器等元件直接利用CLB中的触发器,而对自建触发器则认为是组合电路,需要使用CLB中的组合逻辑电路构成,这样既占用更多的CLB,又浪费了CLB的触发器资源。将两种方法进行比较,每使用一个自建D触发器比使用库中D触发器的电路多占用二至三个CLB。

其次,在设计状态机时,应该尽量使用ONE-HOT状态编码方案,不用2进制状态编码方案。ONE-HOT状态编码方案是表示每个状态由1位触发器来表示,而2 进制状态编码方案是用LgN/Lg2位触发器来表示N个状态。由于2进制状态编码的稳定度较低,ONE - HOT状态编码方案对于触发器资源丰富的FPGA 芯片十分适用。

  • 信号反相的处理策略

在处理反相信号时,在设计时应尽可能地遵从分散反相原则。即应使用多个反相器分别反相,每个反相器驱动一个负载,这个原则无论对时钟信号还是对其它信号都是适用的。因为在FPGA设计中,反相是被吸收到CLB或IOB中的,使用多个反相器并不占用更多的资源,而使用一个反相器将信号反相后驱动多个负载却往往会多占资源,而且延迟也增加了。

首先,如果输入信号需要反相,则应尽可能地调用输入带反相功能的符号,而不是用分离的反相器对输入信号进行反相。例如,如图3-21所示电路是对 的两种设计电路,就应该用如图(b)所示电路直接调用AND2B1,而不要用如图a所示电路用分离的非门对输入信号C 反相后,再连接到AND3的输入。因为在前一种作法中,由于函数发生器用查表方式实现逻辑,C的反相操作是不占资源的,也没有额外延迟;而后一种作法中,C的反相操作与AND3操作可能会被分割到不同的逻辑单元中实现,从而消耗额外的资源,增加额外的延迟。



(a) 非优化反相设计 (b) 优化反相设计
 图3-23 两种反相设计电路比较

 
其次,如果一个信号反相后驱动了多个负载,则应将反相功能分散到各个负载中实现,如图3-22(b)图所示。而不能采用传统TTL电路设计,采用集中反相驱动多个负载来减少所用的器件的数量,如图3-22(a)图所示。因为在FPGA设计中,集中反相驱动多个负载往往会多占一个逻辑块或半个逻辑块,,而且延迟也增加了。分散信号的反相往往可以与其它逻辑在同一单元内完成而不消耗额外的逻辑资源。

 

(a) 非优化集中反相 (b) 优化分散反相  。
图3-24 两种反相驱动电路比较 

 

<<上一节    下一节>>




 
关于我们 | 诚邀加盟 | 客户服务 | 相关法律 | 网站地图 | 友情链接 | 服务信箱:service@eefocus.com
© 2006 与非门科技信息咨询(北京)有限公司 All Rights Reserved.