第5章 CHAPTER 5 数字系统的高级 设计与综合 从基于原理图的设计转到硬件描述语言设计是电子设计的一次变革,它允许一个设计者从理论上以工艺无关的行为方式来描述所设计的数字系统模型。随着设计要求不断提高,复杂性不断增加,用硬件描述语言的数字电路设计在很多方面已经变成单调和费时的事情,设计者迫切需要更高层次抽象的设计与综合技术,为了适应技术的发展,大量高层次的设计技术与综合工具可提供给设计工程师使用。 对于结构比较清晰的数字系统,可以利用硬件描述语言(HDL)直接在寄存器传输级(即RTL级)对设计的系统进行描述,这种描述是对系统行为的描述,然后由综合工具进行综合,利用硬件来实现数字系统。 在第3章介绍Verilog HDL的基础上,第4章结合Vivado设计软件介绍了如何将描述系统行为的设计程序进行硬件实现,并加载到目标器件进行调试和验证。由于设计者的程序的编码风格和采用的设计技术直接影响系统模型的建立和综合的结果,本章将讨论如何使编写的程序能够建立正确的系统模型,并被软件综合成设计者设想的结构,包括编码风格的影响、综合工具优化的使用,以及同步设计技术的概念和措施。最后,按照数字系统的层次结构简要介绍综合可能采用的一些方法,以便读者理解从语言描述到硬件实现的过程。 5.1Verilog编程风格 对于使用HDL编程的抽象级描述,综合优化技术仅能协助设计者满足设计要求。综合工具遵循编码构造和按照RTL中展开的结构在最基础的层次上映射逻辑。如果没有类似FSM和RAM等十分规则的结构,综合工具可以从代码中提取功能,识别可替代的结构,并相应地实现。 除了优化之外,为综合编码时的基本指导原则是不减少功能而使所写的结构和伪指令最小化,但是这样可能在仿真和综合之间产生不一致的结果。一个好的编码风格一般要保证RTL仿真与可综合的网表具有相同的性能。一类偏差是厂商支持的伪指令,它可以按照专门注释的形式(不考虑仿真工具)加入RTL代码,并引起综合工具按RTL代码本身不明显的方式推演一个逻辑结构。 由于综合工具只能对可综合的语句产生最终的硬件实现,如果设计者对语言规则和电路行为的理解不同,则可能使设计描述的编码风格直接影响EDA软件工具的综合结果。例如,描述同一功能的两段RTL程序可能产生出时序和面积上完全不同的电路。好的描述方式就是综合器容易识别并可以综合出所期望的电路,而电路的质量取决于工程师使用的描述风格和综合工具的能力。 5.1.1逻辑推理 1. ifelse和case结构——特权与并行性 在FPGA设计的范围内,把一系列用来决定逻辑应该采取什么动作的条件称作一个判决树。通常,可以分类成ifelse和case结构。考虑一个十分简单的寄存器写入的示例(例51)。 例51 module regwrite( outputreg rout, input clk, input[3:0]in, input[3:0]sel); always @(posedge clk) if(sel[0])rout <=in[0]; else if (sel[1])rout <=in[1]; else if (sel[2])rout <=in[2]; else if (sel[3])rout <=in[3]; endmodule 这类ifelse的结构可以推理成如图51所示的多路选择器的结构。 这类判决结构可以按许多不同的方式来实现,取决于速度/面积的权衡和要求的特权。下面介绍如何针对不同的综合结构对各种判决树进行编码和约束。 ifelse结构固有的性质是特权的概念。出现在ifelse语句的条件所给予的特权超过判决树中的其他条件。所以,上述结构中更高的特权将对应靠近链的末尾和更接近寄存器的多路选择器。 在图51中,如果选择字的位0被设置,则不管选择字的其他位的状态,in0将被寄存; 如果选择字的位0没有被设置,则利用其他位的状态来决定通过寄存器的信号。通常,只有当某一位(在此情况是最低位LSB)前面的所有位均没有被设置时,则利用该位来选择输出。这个特权多路选择器的真正实现如图52所示。 图51串行多路选择器结构的简单特权 图52特权多路选择器 无论ifelse结构最后如何实现,将赋予出现在任何给定的条件之前的条件语句更高的特权。所以,当判决树有特权编码时应该利用ifelse结构。 另外,case结构通常(不总是)用于所有条件互不相容的情况。换言之,可以在任何时刻只有一个条件成立的情况下优化判决树。例如,根据其他多位网线或寄存器(例如加法器的译码器)进行判决时,在一个时刻只有一个条件成立。这与上述用ifelse结构实现的译码操作是一样的。为了在Verilog HDL中实现完全相同的功能,可以采用case语句,见例52。 例52 case(l) sel[0]:rout <=in[0]; sel[1]:rout <=in[1]; sel[2]:rout <=in[2]; sel[3]:rout <=in[3]; endcase 由于case语句是ifelse结构的一种有效替代,许多初学者以为这是自动地无特权判决树的实现。对于更严格的VHDL,该想法恰巧是正确的; 但是对于Verilog语言,却不是这种情况,可以从图53中case语句的实现看出。 图53特权译码逻辑 如图53所示,缺省的部分是通过特权译码来设置多路选择器上相应的使能引脚,这导致许多设计者落入陷阱。如果综合工具报告case结构不是并行的,则RTL必须把它改变为并行的。如果特权条件是成立的,在相应的位置应该采用ifelse结构。 2. 完全条件 目前为止检查的判决树中,如果case语句的条件没有一个是成立的,则综合工具将寄存器的输出返回到判决树作为一个默认条件(这个行为取决于综合工具默认的实现方式,但是本节假设它是成立的)。这个假设是如果没有条件满足,数值不改变。 建议设计者添加默认条件。这个默认值可以是也可以不是当前的数值,但是,为每个case条件分配输出一个数值,可避免工具自动地锁存当前值。用这个默认条件消除寄存器使能,如例53中修改后的case语句。 例53 //不安全的case语句 module regwrite( outputreg rout, input clk, input[3:0]in, input[3:0]sel); always @(posedge clk) case(l) sel[0]:rout <=in[0]; sel[1]:rout <=in[1]; sel[2]:rout <=in[2]; sel[3]:rout <=in[3]; default:rout <=0; endcase endmodule 如图54所示,默认条件现在是明确的,作为多路选择器一个可供选择的输入实现。虽然触发器不再要求一个使能信号,总的逻辑资源不一定减少。同时应注意到,如果每个条件不对寄存器定义一个输出(通常发生在单个case语句分配多个输出时),默认条件和任何综合的特征位都不能防止产生一个锁存器。为了保证总有一个数值分配到寄存器,可以在case语句之前利用初始赋值分配给寄存器一个数值,如例54所示。 图54为默认条件编码 例54 module regwrite ( output regrout, input clk, input[3:0]in, input[3:0]sel); always @(posedge clk) rout <= 0; case(l) sel[0]:rout <=in[0]; sel[1]:rout <=in[1]; sel[2]:rout <=in[2]; sel[3]:rout <=in[3]; endcase endmodule 这类编码风格消除了对默认情况的需要,也保证了如果没有其他赋值定义时,寄存器会分配到这个默认值。 完全条件可以用正确的编码方式来设计,推荐的方法是避免该约束仅由设计保证完全的覆盖,如前所述,即在case语句之前利用默认条件和设置默认数值。这将使代码具有更高的可移植性,减少不希望的失配可能性。设置FPGA综合选项时最大的风险之一是仅允许一个默认设置,因此所有的case语句自动地假设是parallel_case、full_case或二者兼之。厂商明确地提供了该选项。实际上,应该尽量避免使用这个选项,因为它会产生隐含的风险,以不正确的形式进行代码综合,并且用基本的系统内测试无法发现,在仿真中会产生不确定性。所以,parallel_case和full_case可以引起仿真和综合的失配。 3. 多控制分支 设计者通常犯的错误(在较差的编码风格中)是对单个寄存器不连接控制分支。在例55中,oDat被唯一的判决树中两个不同的数值赋值。 例55 //不好的编程风格 module separated( output regoDat, inputiclk, input iDat1, iDat2, iCtrl1, iCtrl2); always @ (posedge iclk)begin if (iCtrl2) oDat <=iDat2; if (iCtrl1) oDat <=iDat1; end endmodule 因为无法说明iCtrl1和iCtrl2是否是互不相容的,因此该编码是模糊的,综合工具必须为实现做一定的假设。特别地,当二者的条件同时成立时,没有明显的方式管理特权。因此, 图55带明显特权的实现 综合工具必须基于这些条件发生的顺序赋予特权。在这种情况下,如果条件最后出现,它将获得优于第一个条件的特权。 基于图55,iCtrl1有优于iCtrl2的特权,如果交换它们的次序,特权同样也会交换。这与ifelse结构的行为相反,ifelse结构通常把特权给予第一个条件。 所以,好的设计实践是把所有的寄存器赋值保持在单个控制结构内。 5.1.2陷阱 在描述与可综合RTL结构特性有关的功能方面,HDL具有十分灵活的能力,自然地会产生大量的陷阱,当设计者不理解综合工具如何解释各种结构时可能会落入陷阱。本节识别大量的陷阱,并且讨论避免这些陷阱的设计方法。 1. 阻塞与非阻塞 在软件设计领域,按照预定的顺序执行规定的操作来产生功能。在HDL设计领域,这类执行可以想象为阻塞。这意味着当前的操作完成之后,进一步的操作才不被阻塞(之前它们没有被执行)。所有进一步的操作是在所有前面的操作已经完成和存储器中的所有变量已被更新的假设之下执行的。非阻塞的操作执行时与次序无关,更新是被专门的事件所触发的,当触发的事件发生时所有的更新同时发生。 Verilog HDL和VHDL等硬件描述语言提供为阻塞和非阻塞赋值的结构。如果不能理解何处和如何利用阻塞和非阻塞可能导致不期望的行为,还会使仿真和综合之间失配。例如,例56中的代码。 例56 非阻塞模块 module blockingnonblocking( (output regout, input clk, regin1, in2,in3); reglogicfun; always @(posedge clk) begin logicfun <=in1 & in2; out<=logicfun | in3; end endmodule 该逻辑按照逻辑设计者预期的实现如图56所示。 图56用非阻塞赋值的简单逻辑 在图56所示的非阻塞赋值实现中,信号logicfun和out是触发器输出,在in1和in2上的任何变化将花费两个时钟周期传播到out。对于阻塞赋值,仅需做微小修改,见例57。 例57 //不好的编程风格 logicfun=in1 & in2; out=logicfun | in3; 在例57的修改中,非阻塞语句已经被改变为阻塞语句。这意味着out在logicfun更新之后才被更新,二者的更新必须在同一个时钟周期内发生。 从图57可以看出,把赋值改变到阻塞方式,已经有效地消除了logicfun的寄存器,并改变了整个设计的时序。但是,并不是说与例56相同的功能不可以用阻塞赋值来完成,参见例58的修改。 图57采用阻塞赋值的不正确的实现 例58 //不好的编程风格 out=logicfun | in3; logicfun=in1 & in2; 在例58的修改中,强迫out寄存器在logicfun之前更新,它迫使输入in1和in2经两个时钟周期的延时传播到out。这将得到预期的逻辑实现,但是很少有直接的方法。事实上,对于具有相当复杂性的许多逻辑结构,这不是清晰的或者可行的方法。一个诱人的方法是为每个赋值使用独立的always语句,参见例59的修改。 例59 //不好的编程风格 always @(posedge clk) logicfun=in1 & in2; always @(posedge clk) out=logicfun | in3; 尽管这些赋值被分解成看上去像并行的模块,但它们却不是这样仿真的。应该避免这类编码风格。 阻塞赋值常常出现在要求相对大量默认条件的操作的情况。在例510使用非阻塞赋值的代码中,控制信号ctrl定义哪个输入被赋值为相应的输出,其余的输出被赋值为零。 例510 //不良的编程风格 module blockingnonblocking( outputreg[3:0]out; inputclk; input[3:0]ctrl, in); always @(posedge clk) if(ctrl[0]) begin out[0]<=in[0]; out[3:1]<=0; end else if(ctrl[1]) begin out[1]<=in[1]; out[3:2]<=0; out[0]<=0; end else if(ctrl[2]) begin out[2]<=in[2]; out[3]<=0; out[1:0]<=0; end else if(ctrl[1]) begin out[3]<=in[3]; out[2:0]<=0; end else out<=0; endmodule 在例510实现的每个判决分支中,不赋值的所有输出必须设置为零,每个分支包含单个输出(赋值为一个输入)以及三个零赋值语句。为了简化代码,阻塞语句有时被用于初始赋值,如例511所示。 例511 //不好的编程风格 module blockingnonblocking( output reg[3:0]out, inputclk; input[3:0]ctrl, in); always @(posedge clk) begin Out=0; if(ctrl[0]) out[0]<=in[0]; else if(ctrl[1]) out[1]<=in[1]; else if(ctrl[2]) out[2]<=in[2]; else if(ctrl[3]) out[3]<=in[3]; end endmodule 在例511中,最后的赋值是“黏合”(stick)的赋值,因为设置了一个对所有输出位的初始值,需要时只改变一个输出。虽然这个代码将综合到与更复杂的非阻塞结构相同的逻辑结构,但仿真中可能出现竞争条件。非阻塞赋值可以完成相同的功能,如例512所示的类似的编码风格。 例512 module blockingnonblocking( output reg[3:0]out, inputclk; input[3:0]ctrl, in); always @(posedge clk) begin out<=0; if(ctrl[0]) out[0]<=in[0]; else if(ctrl[1]) out[1]<=in[1]; else if(ctrl[2]) out[2]<=in[2]; else if(ctrl[3]) out[3]<=in[3]; end endmodule 这类编码风格是普遍使用的,因为已经用非阻塞赋值消除竞争条件。 违反这些准则将导致仿真与综合的失配,程序可读性差,同时会降低仿真性能,较难诊断出硬件的错误。 2. forloop环路 类似于C语言的环路结构,forloop可能对有软件设计背景的设计者存在陷阱。与C语言不同,这些环路一般不可以在可综合的代码中被算法迭代利用。然而,为了以最少的操作为大阵列赋值,HDL设计者一般会使用这些环路结构。例如,软件设计者可能利用forloop获得X的N次幂,如以下代码片段所示。 PowerX=1; for (i=0; i= 0; I=I-1) xor_reduce_func=XOR_reduce_func ^ data[I]; 例519中的循环语句可综合成如图58所示的链状结构,对于长的组合链路,设计者应该在代码编写阶段就注意描述成树状结构,如例520所示,综合得到如图59所示的结果。 图58链状结构 图59树状结构 例520 i_data=data; LEN=i_data`LENGTH; if LEN=1 then result=i_data(i_data`LEFT); elsif LEN=2 then result=i_data(i_data`LEFT) XOR i_data(i_data`RIGHT); else MID=(LEN+1)/2+i_data`RIGHT; UPPER_TREE=XOR_tree_func(i_data(i_data`LEFT downto MID)); LOWER_TREE=XOR_tree_func(i_data(MID-1 downto i_data`RIGHT)); result=UPPER_TREE XOR LOWER_TREE; end if; 3. 组合环路 组合环路是包含反馈的逻辑结构,其中没有任何的同步元件。从图510可以看出,当一组组合逻辑的输出不带中间寄存器反馈回自身时会出现组合环路。这类行为很少遇到,一般表示为设计和实现中的一个错误,这里要讨论可能产生这样一个结构的陷阱和如何避免它们。参见例521所示的代码段。 图510组合与时序环路 例521 //不好的编程风格 Module combfeedback( output out, input a); reg b; //不好的编程风格:会将b反馈回b assign out = b; //不好的编程风格: 不完整的敏感信号列表 always @(a) b = out ^ a; endmodule 例521的模块表示一个行为描述,在仿真中它可能表现为: 当导线a改变时,输出被赋值为当前输出和a异或的结果。输出只在a改变时改变,不呈现任何反馈或振荡的行为。但是,在FPGA综合中,一个always结构描述了寄存器或组合逻辑的行为。在这种情况下,综合工具将扩展敏感清单(当前只包含a),使敏感清单包含假设结构为组合的所有输入,当这些发生时,反馈环路关闭,将通过一个反馈到自身的异或门来实现,如图511所示。 图511偶然的组合反馈 这类结构是有很大问题的,因为输入a为逻辑1的任何时刻它将振荡。例521列出的Verilog HDL语句描述了一个编码风格很差的电路,设计者显然没有硬件的概念,在仿真和综合时将看到惊人的失配。作为好的编码实践,所有的组合结构应该编码,使得在always模块内的表达式中所包含的全部输入都列在敏感清单中。如果做到这点,在综合之前就能检测出问题。 4. 寄存器与锁存器 寄存器与锁存器都是用来暂存数据的器件。 寄存器是时钟沿触发的,输出端通常不随输入端的变化而变化,只在时钟上升沿(或下降沿)才将数据打入寄存器,输入端的数据送至输出端; 锁存器是电平敏感的,只要使能信号有效,输出端总随输入端的变化而变化; 相反地,使能信号无效时,输出保持已有的数值。 锁存器比寄存器快,有些场合适合使用锁存器,但是一定要保证输入信号的质量。锁存器的缺点是对输入信号的毛刺敏感,锁存器在ASIC设计中比寄存器简单,但是在FPGA的资源中,很多器件需要用查找表和寄存器来组成锁存器,浪费逻辑资源。锁存器在时序分析中也比较困难。程序中使用不完整的if语句结构或case语句结构,将导致综合软件综合出锁存器。所以要避免不必要的锁存器推论。 专门类型的组合反馈实际上可以推论出时序元件。例522以典型的方式模拟一个锁存器模块。 例522 //锁存器接口 module latch ( inputiClk, iDat, outputregoDat); always @* if(iClk) oDat <=iDat; endmodule 每当控制插入时,输入直接传递到输出; 当控制释放时,锁存器被禁止。一个很常见的编码错误是产生组合的ifelse树,忽略了对每个条件定义输出。实现时会包含一个锁存器,通常将指示一个编码错误。 对于FPGA设计,一般不推荐使用锁存器,但可以使用这些器件设计和执行时序分析。注意,也有其他方式可偶然地推论出锁存器,更多的情况是无意的。在例523的赋值中,默认条件是信号本身。 例523 //不好的编程风格 assign O=C ? I: O; 一些综合工具将推论出一个锁存器,而不是推论一个反馈到其输入之一的多路选择器(它不可以任何方式预测)。附带的问题是一个时序的终点(锁存器)被插入进一个路径,其中可能会有设计中介的时序元件。这类锁存器推论一般表示HDL描述中的一个错误。 对于FPGA设计,一般不推荐的锁存器可以十分容易地变成不正确的实现或完全不实现。例如通过使用函数调用实现,考虑锁存器封装进一个函数的典型例示,如例524所示。 例524 //不好的编程风格 module latch ( input iClk, iDat, outputreg oDat); always @* oDat <=MyLatch(iDat, iClk); function D, G; if (G) MyLatch=D; endfunction endmodule 在这个示例中,输入输出的条件赋值被放进一个函数,尽管锁存器的表示似乎精确,但是函数将总是判定组合逻辑,会把输入直接传递到输出。 敏感列表只对前面的仿真起作用,对综合器不起作用,而综合后生成的仿真模型是保证不遗漏敏感信号的,因此设计者不小心造成的敏感信号表的遗漏往往会导致前、后仿真的不一致。 引起硬件动作的信号应该都放在敏感信号列表中,包括: ①组合电路描述中,所有被读取的信号; ②时序电路中的时钟信号、异步控制信号。不完整的敏感信号列表代码见例525。 例525 //不完整的敏感信号列表 always @(d or clr) if (clr) q=1`b0 else if (e) q=d; 例526 //完整的敏感信号列表 always @(d or clr or e) if (clr) q=1`b0 else if (e) q=d; 概括上面的分析,对于编程风格,列出以下注意要点或需要遵循的准则: (1) 对希望形成组合逻辑的ifelse和case语句,要完整地描述其各个分支,避免形成锁存器,一个可行的办法是在语句前为所有被赋值信号赋一个初始值。 (2) 有大量关于阻塞和非阻塞赋值为综合编码时广泛接受的准则: ①利用阻塞赋值设计组合逻辑模型; ②利用非阻塞赋值设计时序逻辑或混合逻辑模型。 (3) 从不把阻塞和非阻塞赋值混合在一个always模块中。 (4) 尽量使用简单的逻辑、简单的数学运算符。 (5) 进程的敏感列表应该列举完全,否则可能产生综合前后的仿真结果不同和引入锁存器的现象,可用*替代来自动识别全部敏感变量。 (6) 在循环中不要放置不随循环变化的表达式。 (7) 对于复杂的数学运算要充分进行资源共享,如采用if块等。 (8) 对于长的组合链路应该在代码编写阶段就注意描述成树状结构。 (9) 时序逻辑尽可能采用同步设计。 (10) 对具有不同的时序或面积限制的设计,应尽可能采用不同的代码描述以达到不同的要求(一般而言,面积与延时是相互冲突的)。 (11) 对于复杂系统设计,应尽量采用已有的算法和模块实现。 5.1.3设计组织 所有与工程师队伍一起工作设计过大型FPGA的人都理解把设计组织成有用的功能约束,以及为重用和扩展做设计的重要性。在顶层上组织一个设计的目标是产生一个容易在模块基础上管理的设计,产生可读和可重用的代码,产生一个允许设计缩放的基础。本节讨论一些影响可读性、可重用性和综合效率的结构设计要考虑的内容。 1. 分割 分割是指依据模块、层次和其他功能约束组织设计。一个设计的分割应该预先考虑,因为设计组织的主要变化在设计进展时将变得更困难和更昂贵。设计者可以方便地围绕一部分功能交换他们的想法,允许他们以有效的方式设计、仿真和诊断自己的工作。 一般情况下,一个设计应按功能分割成较小的功能单元,每个功能单元都有一个公共的时钟域,并能独立进行验证。设计层次应能将时钟域分割开,以说明多个时钟之间的相互作用和对同步电路的需求,每个时钟域的逻辑在系统整合之前分别进行验证。 1) 数据通道与控制 从第1章中对数字系统结构的分析可知,数字系统的许多结构可以分割成数据通道和控制结构的形式。数据通道一般是一个把数据从设计输入端运送到输出端并对数据执行必要操作的“管道”。控制结构通常不对设计数据处理或运送,但是为各种操作配置数据通道。按照数据通道和控制结构之间的逻辑分割,可以逻辑地将数据通道和控制结构设置在不同的模块,清楚地对各个设计者定义接口。对不同的逻辑设计者,这样不仅方便地划分设计活动,而且也可以优化可能要求的下游活动。 所以,数据通道和控制结构应该分割成不同的模块。 因为数据通道常常是设计的关键路径(设计的流量是与流水线的时序有关的),可能要求为这个通道达到最大性能设计布图。另外,通常将较慢的时序要求安排给控制逻辑,因为它不是主要的数据通道的一部分。 例如,通常用简单的SPI(串行外设接口)或I2C(IntelIC)类型总线来设置设计中的控制寄存器。如果流水线运行在几百兆赫兹,在两个时序要求之间将确实有大的偏差。因此,如果布图是应对数据通道要求的,控制逻辑的布图则可以保持空间无约束和散布地围绕流水线的合适的位置,像自动布局布线工具所实现的那样。 2) 时钟与复位结构 好的设计实践表明,任何给定的模块只有一个类型的时钟和一个类型的复位。例如在许多设计案例中,当有多个时钟区域和(或)复位结构时,重要的是分割层次使得它们被不同的模块分开。由于混合时钟和程序描述复位类型会带来设计的风险,但是如果任意给定模块只有一个时钟和复位,这些问题几乎很少出现。 所以,在每个模块中只利用一个时钟和一个类型的复位是好的设计实践。 3) 多个例示 如果有些逻辑操作在特定的模块中出现不止一次(或者横跨多个模块),则应自然地分割设计,把整块分成分开的模块,并放进多个例示的层次中。 图512模块设计 图512描述的分割有许多优点。首先,整块的功能分配给相互独立的设计者,使设计更方便。一个设计者可以集中在顶层设计、组织和仿真,而另一个设计者可以集中于子模块的功能性技术要求。如果接口定义明确,这类分组设计可以获得更好的效果。但是,两个设计者在相同的模块内开发,可能发生更大的混淆和困难。此外,子模块可以在设计的其他区域重用,或者完全在不同的设计中。一般重新例示一个存在的模块更方便,而不是切割和连接更大的模块,并重新设计接口。 采用这样的策略可能引起的一个问题是各个模块的数据宽度、迭代次数等有细微的变化。这些案例强调参数化的设计方法,类型相似的模块可以共享公共的代码基,即在例示基础上参数化。下一节将更详细地讨论。 2. 参数化 在FPGA设计的范围内,参数是一个模块的特性,它可以在全局的意义上或者在每个例示的基础上改变,同时保持模块的基本功能。本节描述参数的形式,以及介绍它们如何加强综合的有效编码。 1) 定义 参数和定义是类似的,在许多情况下可以相互交换使用。但是,在许多情况下指有效的、可读的和模块化设计。定义一般用于规定横跨所有模块的恒定的全局数值,或者为相容或不相容的部分代码提供编译时间的伪指令。在Verilog中,定义使用`define语句,编译时间控制用一系列的`ifdef语句。全局定义可定义全部设计的常数,如例527所示。 例527 `define CHIPID 8`hC9//全局芯片ID `define onems90000  //使用11ns时钟近似的1ms `define ulimit1665535  //一个16位无符号字的上限值 上面列出的定义是全局的“明确事情”的例子,从一个子模块到另一个子模块将不会发生改变。全局定义的另一个用途是为代码选择规定的编译时间伪指令。一个广泛的应用是FPGA中ASIC样机的使用。ASIC和FPGA之间的不同常常需要对设计进行细微的修改(特别是I/O和全局结构),例如,考虑例528中的定义。 例528 `define FPGA //`define ASIC 在顶层模块中,可能有例529的输入。 例529 `ifdef ASIC input TESTMODE; output TESTOUT; `endif `ifdef FPGA output DEBUGOUT; `endif 在上面的代码例子中,为插入ASIC测试必须包含测试引脚,但是在FPGA实现中这样做并没有意义。因此,设计者只会包含那些在ASIC综合中的位置支架。类似地,设计者可能需要为FPGA样机诊断使用某个输出,但是却不包含在最后的ASIC实现中。全局定义允许设计者用行中包含的变化保持单个代码载体。所以,ifdef伪指令应该为全局定义使用。 为了保证在全局意义上应用定义,并且不与另一个全局定义冲突,推荐编写一个可以包括所有设计模块的全局定义文件。因此,任何全局参数都可以在集中的位置修改使其改变。 2) 参数 与全局定义不同,参数一般位于专门的模块,从一个例示到另一个例示可以改变,一个广泛应用的参数是尺寸或总线宽度,如例530的寄存器的例子中所示。 例530 module paramreg #(parameter WIDTH =8) ( outputreg [WIDTH-1:0]rout; inputclk, input[WIDTH-1:0]rst); always @(posedge clk) if (!rst) rout <=0; else rout <=rin; endmodule 例530描述了一个具有可变宽度的简单参数化的寄存器。虽然参数的默认值是8,但是可以只为这个例示修改宽度。例如,在更高层次中的模块可以例示例531所示的2位寄存器。 例531 //正确,但是过时的参数传递 paramreg #(2)r1(.clk(clk), .rin(rin), .rst(rst), rout(rout)); 或者以下22位的寄存器: //正确,但是过时的参数传递 paramreg #(22)r2(.clk(clk), .rin(rin), .rst(rst), rout(rout)); 从上面的例示可以看出,对于paramreg,相同代码的载体可用来例示两个有不同特性的寄存器,同时注意到模块的基本功能在例示(寄存器)之间没有改变,只改变功能的专门特性(尺寸)。 所以,参数应该被局部定义使用,从一个模块到另一个模块才改变。 当要求类似功能的不同模块有细微的不同特性时,像这样的参数代码是十分有用的。如果没有参数化,设计者可能需要为相同模块编写冗长的代码载体,并且修改差异特性时容易出错。 上述参数定义可在Verilog中利用defparam命令替代,允许设计者在设计层次中规定任何参数。容易出现的问题是因为一般的参数是在专门的模块例示中使用,在特定例子的外部是看不到的(类似软件设计中的局部变量),容易混淆综合工具,产生仿真失配。所以,如果使用defparam,应该将其包含在与定义的参数对应的模块例示中。 3) Verilog2001中的参数 参数改进的方法在Verilog2001中引入。在Verilog的旧版本中,参数值的传递是含义模糊的,或者很难通过位置参数传递来读出,使用defparam会引起前述讨论的一些风险。理想情况下,设计者应该按照与模块的I/O之间传递信号相似的方式将一个参数值清单传递到模块。在Verilog2001中,参数可以通过模块外部的名称识别,从而可消除可读性的问题以及使用defparam产生的风险。例如,使用paramreg的例示将包含参数的名称修改。 paramreg #(.WIDTH(22))r2(.clk(clk), .rin(rin), .rst(rst), routy(rout)); 这样可以不锁定位置要求,增加代码的可读性,减少人为错误的概率。这类参数化的命名是极力推荐的。所以,命名参数传递优于位置参数传递或defparam语句。 在Verilog2001中,参数化另一个主要的改进是localparam。localparam是局部变量的Verilog参数版本。localparam可以通过其他参数的表达式推导出,被处在其内的模块的实际例示所约束。例如,考虑例532中参数化的乘法器。 例532 //混合方式参数 module multiparam #( parameter WTDTH1=8, parameter WTDTH2=8) localparamWIDTHOUT = WIDTH1 + WIDTH2; output[WIDTH-1:0]oDat; input[WIDTH-1:0]iDat1; input[WIDTH-1:0]iDat2; always oDat=iDat1+iDat2; endmodule 在上面的例子中,需要外部定义的参数只是两个输入的宽度。因为设计者假设输出的宽度是输入宽度之和,因此这个参数可以从输入参数推导出来,没有冗余的外部计算。这使得设计者的工作更方便,消除了输出尺寸与输入尺寸之和不匹配的可能性。 通常,在模块的头部不支持localparam,如果在I/O清单中使用localparam,端口清单必须额外说明(Verilog1995方式)。只要localparam可以从其他输入参数推导出来,就推荐使用,因为它将进一步减小人为错误的可能性。 5.1.4针对Xilinx FPGA的HDL编码 由于没有一个完美的方式来产生设计,所以推荐使用编码技术的设计准则。对于Xilinx FPGA的架构,可以减少器件的使用,改善设计性能,使实现工具获得更好的布局和布线结果,进而得到更好的系统速度。 作为满足时序的设计策略,建议尽可能地使用专用资源,例如由LUT实现的移位寄存器SRL、大量的DSP和BRAM等,达到提高速度、降低功耗和改善器件利用率的目的。 在可能的情况下,可以利用硬模块映射大的寄存器阵列的优点,SRL、BRAM和DSP Slice可以有效地用于节省逻辑的Slice资源并改善性能。例如,可以利用BRAM实现有限状态机等。 7系列FPGA的查找表LUT是6输入的,因此与4输入LUT相比,可以封装更多的逻辑。特别是当设计需要从4输入LUT架构优化的代码移植到7系列器件时,可能需要对设计进行重新编码,才能减少设计中流水线的级数,从而充分利用有效的资源。 当设计中FPGA占用比较满时,必须尝试不同的设计策略。如果没有将设计封装进器件,时序又无关,为减少设计中CLB资源的数量,可以使用更多的DSP和BRAM资源替代CLB资源。Xilinx建议关闭Logic Replication的综合选项,可以帮助减少设计尺寸。 在某些情况下,优化面积设计可以改善性能,特别是在RTL级优化时,可以降低功耗。 1. 控制集 在2.4节中,图219 SliceL电路图中的圈3和圈5是八个触发器,每个触发器具有时钟CK、时钟使能CE和同步/异步的置位/复位SR等三种控制端口,八个触发器共享相同的时钟和控制信号,而时钟使能是四个触发器共享的高电平有效的信号。 驱动一个寄存器的这些控制信号被称为“控制集”。由于设计的编程风格和综合工具不同,设计可能有许多控制集。设计软件会以最优的性能智能地封装逻辑。 通常,Xilinx FPGA对于每个设计提供足够的寄存器,但是在用户的设计中,某些时候寄存器不足,一些设计实践可能限制寄存器的有效利用。由于控制信号的限制,对于分组的Slice资源,在CLB模块中的资源对布局也有一定影响。FPGA不推荐使用低电平有效的控制信号,因为低电平的控制信号不节省功耗,所以设计中低电平有效的控制信号的不合理使用也会造成资源的不足。 带有不同控制集的触发器不可以封装进相同的Slice中,但是通过把控制信号映射到LUT资源中,软件可以按指令减少控制集的数目,如图513所示,设计中不同的三个控制集,原来要映射到3个Slice中实现,但是使用同步的置位和复位,可以利用LUT将三个不同控制集变为一个相同的控制集,其中同步置位Sset与输入信号进行或运算,同步复位SReset与输入信号进行与运算,控制信号仅保留时钟信号CK,这种映射增加了LUT的利用率,减少了Slice的利用,节省了资源。 图513三个不同控制集变为一个相同的控制集 2. 控制信号设计技巧 控制端口的使用规则是时钟和异步的置位/复位信号总是占用触发器的控制端口,不可采用LUT的等效逻辑将它们移到数据通道; 时钟使能和同步置位/复位信号与Slice中的大多数触发器共享相同的控制集时,它们将连接到触发器的控制端口,但是可以移到LUT输入端的数据通道。所以异步的置位/复位信号比同步的置位/复位信号有更高的特权来获取触发器的控制端口。 Xilinx建议设计中要尽可能地利用同步置位和复位,也建议使用高电平有效的CE和置位及复位,并且尽量构造具有尽可能少的控制信号的设计。 3. 其他设计技巧 1) I/O寄存器用法 IOB模块的寄存器提供固定的建立时间和时钟至输出的时间TCO,这是捕获器件外的输入数据和时钟数据的最快的方式,但是,由于采用了IOB寄存器,内部的时序通道可能变得更长,导致内部逻辑的布线延时加长。 Xilinx建议只在必须满足I/O时序时使用IOB寄存器。 可选择性地利用XDC属性或TCL指令把寄存器移进IOB。 set property IOB TRUE [get_cells b1_reg[*]] set property ILOGIC_X0Y341 [get_cells b1_reg[0]] 每个寄存器例示的名称可以从综合工具的原理图视图或时序报告中获得。 2) Block RAM的用法 为了得到Block RAM的最佳性能,可利用IP库工具的帮助例示存储器,避免使用Block RAM存储器的“写之前读”(read before write)模式, Synplify和其他第三方综合工具可以插入旁路逻辑防止RTL和硬件行为之间可能存在的失配错误。当读和写操作发生在相同的存储器地址时,强制Block RAM输出一个已知的数值。当确认不会出现失配错识时,可以不添加这个逻辑以免损害相关属性的性能。 attribute syn_ramstayle of mem: signal "no_rw_check" 3) 时钟使能 利用HDL代码对时钟使能的控制,只有当需要时才使用。如果需要低扇出的CE,可利用单独网线的综合属性来管制信号或模块级的控制信号。 例533 always @ (posedge CLK) If (CE=`1`) Q <=A; 例533的程序中,设计软件会将控制信号CE映射到相应的控制端口,但是对于低扇出的时钟使能信号,可以利用替代的编码方法,将控制信号CE映射到LUT的输入端,而不占用触发器的控制端口,如例534所示。 例534 always @ (posedge CLK) Q <=(~CE & A)| (CE & Q); Xilinx不建议设计异步或不带CE的程序,应合理地采用低扇出CE的设计。 为了降低整个门控时钟域的功耗,利用时钟使能全局缓冲资源BUFGCE和BUFHCE,对于暂时的小面积上的时钟的应用,可利用寄存器的时钟使能引脚,从而节省布线资源。 5.2综合优化 大多数为FPGA综合的实现工具都为设计者提供了很多优化选项,但是大多数设计者在运用中往往不清楚这些选项的准确功能,以及如何利用这些优化选项来优化一个设计。许多设计者没有完全理解这些优化选项,通常会花费几小时、几天甚至几周时间来进行无尽头的组合,直到似乎找到一个最好的结果。只有少数设计者能恰好达到优化目的,甚至超过已有的方案。因此,对于许多设计者来说,由于缺少基本的理解,大多数优化选项变得无用,也很难开发一个完全的试探法的算法库。 本节介绍综合优化最重要的方面,基于尝试而成功的真实经验,为读者在实现层次提供实际的探索。 5.2.1速度与面积 大多数综合工具都允许设计者在速度与面积优化之间转换。这似乎是轻而易举的: 想要使设计运行得更快,就选择优化速度; 想要使设计更小,就选择优化面积。实际上却并不简单,因为一些算法可能产生适得其反的结果,即在要求运行更快之后设计却变得更慢。所以,必须理解速度和面积优化在实际设计中是如何实现的。 在综合层次,速度和面积优化决定了实现RTL将要采用的逻辑拓扑。在抽象层次,很难知道FPGA有关的物理特性。目前的讨论与布局和布线的互连延时有关。综合工具采用所谓的导线负载模型,基于各种设计准则统计地估计互连延时。在ASIC中,设计者是可以理解这点的,但是使用FPGA设计时这些是隐藏在幕后的。综合工具最终得到的估计值,常常与实际的结果有显著的不同。由于缺少后端的知识,综合工具主要执行门级优化。在高级FPGA设计工具中,基于布局的综合流程会帮助关闭这个环路。 基于综合的门级优化包括状态机编码、并行与交错多路选择、逻辑复制等。作为一般的准则(虽然不总是一定成立),更快的电路要求更高的并行性,相当于使用面积更大的电路,即速度与面积权衡的基本概念。但是,由于存在FPGA布局配置的二阶效应,并不总是能获得预期的效果。 直到布局布线完成后,工具才真正知道器件在布局布线过程的困难。但是,实际的逻辑拓扑已经被综合工具提交,因此,如果在综合级致力于优化速度,后端工具将发现器件过于拥挤。当器件过于拥挤,工具将别无选择,而将元件布局到适合的地方,而次优路径会引入更长的延时。实际上设计者常常出于经济的考虑使用尽可能小的FPGA,导致一般的直观推断: 当资源利用率接近100%时,在综合级进行速度优化不一定产生更快的设计。事实上,面积优化可以获得更快速的设计。 过分约束设计也会出现问题,如果目标频率设置太高(比最后的速度大15%~20%),设计可以按次优的方式实现,实际获得较低的最大速度。在实现初期,综合工具基于时序要求产生逻辑结构。如果在初始时序分析阶段,确定设计离达到的时序太远,工具可能提早放弃。但是,如果将约束设置到正确的目标,不高于最后的频率20%(假设没有对时序进行初始化),逻辑将用达到规定时序的最小面积来实现,在时序收敛期间有更多的灵活性。同时需要注意,由于FPGA实现的二阶效应,更小的设计能否改善时序与实际的环境有关。 在FPGA设计中,面积优化实际上是对FPGA的资源利用率的优化,设计要在各种资源利用之间达到一种平衡,最大限度地发挥器件的性能。尽量使用器件中的专用硬件模块(如RAM、硬件乘法器),这些硬件资源能够提高设计性能,并且它们已经存在于FPGA中,不用就浪费了。如果专用硬件模块不够用,而逻辑单元资源丰富,则可以用逻辑资源去实现这些硬件模块。DSP、I/O单元中都有专用的触发器资源,应尽量使用,可以提升设计性能,减少可编程逻辑资源的消耗。表51给出了Xilinx FPGA的设计中从设计代码优化和软件设置选择等方面进行速度和面积优化的一些方法。优化设计代码要求深入理解整个设计,每个设计都有独特的地方,对设计进行优化时,需要充分理解设计的特点和需求(例如性能、成本、开发周期等)。而通过软件对设计进行约束和设置,目的是进行编译之后,可以根据编译结果对设计进行分析,找到设计中的真正瓶颈,从而有效地引导后续的优化过程。 表51速度和面积优化的方法 速 度 优 化面积(资源)优化 设计 代码 优化对最高工作频率进行优化,最根本、最有效的方法是对设计代码的优化: (1) 编码风格直接影响组合逻辑的级数; (2) 按目标FPGA器件的结构特点编写代码 几种速度优化方法: (1) 增加流水线级数; (2) 组合逻辑分割和平衡; (3) 复制高扇出的结点; (4) 状态机仅完成控制逻辑的功能; (5) 模块边界用寄存器 设计代码优化: 设计中最根本、最有效的优化方法是对设计输入(如HDL代码)的优化,与编码风格有关,常用的面积优化方法: (1) 模块的时分复用; (2) 改变状态机的编码方式; (3) 改变模块的实现方式 软件 设置 选择综合软件中,进行“速度优化”设置; 状态机编码方式,使用OneHot; 使用全局信号,全局网络具有高扇出、低抖动等优点,而且可节省布线资源; 展平层次结构,可以使模块边界的时序路径充分优化; 不同的随机数种子,导致编译结果的小幅度变动; 使用逻辑锁定进行局部优化; 利用位置约束、手动布局和反标注来优化设计 逻辑综合对设计实现的结果影响很大,选择合适的综合约束条件和编译选项很重要; 可以设置“资源优化”或“面积优化”相关选项: (1) 较少复制逻辑; (2) 设置资源共享; (3) 状态机编码方式设置; (4) 展平设计的层次结构; (5) 用专用硬件资源(如DSP块)代替逻辑单元功能模块; (6) 网表面积优化; (7) 寄存器打包 最高时钟频率优化: 当一个设计的I/O时序满足要求后,就可以优化设计的运行速度,即内部的最高工作频率; 这个最大的时钟频率由关键路径决定资源重新分配: 专用硬件模块与可编程逻辑资源的平衡; 专用硬件资源间的平衡(如Xilinx FPGA中有分布式和Block RAM,容量不同,适合不同应用目标) 5.2.2资源共享 在编码风格上,只有在同一个条件语句(ifelse和case)的不同分支中的算术操作才能资源共享。结构性资源共享可以采用不同功能模块的部分设计通过调整逻辑被重用。在高层次,这类结构可以显著地减少整个面积,如果操作不是互不相容的,可能包含流量的损失。在综合优化层次上,资源共享一般指在寄存器级之间对逻辑进行分组操作。这个较简单的结构可以归结为简单逻辑和经常的算术运算。 支持资源共享的综合引擎将识别互不相容的类似的算术运算,通过调整逻辑来组合这些运算。例如,考虑例535中的例子。 例535 module addshare ( output oDat, inputiDat1, iDat2, iDat3, inputisel); assignoDat=isel ? iDat1+iDat2; iDat1+iDat3; endmodule 在上面的例子中,输出oDat要么被前两个输入之和赋值,要么被第一个和第三个输入之和赋值,这取决于选择位。这个逻辑的直接实现如图514所示。 在图514中,两个和是独立计算的,基于输入iSel来选择。这是从代码直接映射的,但不是最有效的方法。有经验的设计者将识别出输入iDat1是在两个加法运算中使用,用输入端多路选择输入iDat2和iDat3可以采用单个加法器。 这个结果也可以通过综合提供的资源共享选项获得。资源共享将两个加法操作识别为两个互不相容的事件。取决于选择位的状态(或其他条件运算符),不是这个加法器被更新,就是另一个被更新。综合工具则能够组合这些加法器并实现多路选择器控制输入,如图515所示。 图514两个加法器的直接实现 图515组合加法器资源 虽然上述实现的最大延时不受资源共享优化的影响,但有些情况资源共享要求在各个路径附加多路选择,可参见例536。 例536 module addshare( output oDat, inputiDat1, iDat2, iDat3, input[1:0]isel); assignoDat=(isel==0) ? iDat1+iDat2; (isel==1) ? iDat1+iDat3; iDat2+iDat3; endmodule 图516直接映射三个加法器 直接的映射将产生如图516所示的结构。这个实现通过并行结构来产生所有的加法器和选择逻辑。最坏条件的延时是通过一个加法器和一个多路选择的路径。由于资源共享使能,加法器输入如图517所示被组合。在这个实现中,全部加法器已经减少到带多路选择输入的单个加法器。但是,应注意到关键路径被扩展成三层逻辑,会影响这个路径的时序,使时序不仅与被实现的逻辑规定有关,同时也与FPGA的可用资源有关。 图517加法器共享时额外的逻辑层次 当某路径不是关键路径时,智能的综合工具一般将利用资源共享,也就是运算中触发器到触发器的时序路径不是最坏情况。如果综合工具有这个能力,则总会采用资源共享; 如果不是,设计者必须分析关键路径,了解该优化是否会增加附加的延时。 如果资源共享被激活,则要验证没有添加延时到关键路径。 5.2.3流水线、重新定时和寄存器平衡 在速度优化的设计中,流水线是一种方法,它在成组的逻辑之间添加寄存器级数,用于增加流量和触发器到触发器的时序。一个好的设计模块通常可以增加附加的寄存器级数流水,只影响总的时滞和小的面积损失。综合对相同结构的流水线、重新定时和寄存器平衡的操作进行选择,但不添加或移去寄存器本身。相反,围绕逻辑移动寄存器的优化可平衡任何两个寄存器级之间延时,从而使最坏条件下的延时最小化。流水线、重新定时和寄存器平衡在手段上是十分类似的,不同的厂商之间也只有细微的改变。图518从概念上进行了说明。 一般认为流水线起源于最广泛采用的负载平衡的方法,只要流水存储器或乘法器等规则结构可以被综合工具识别,就可以用重新分布逻辑来重新构造。在这个情况下,流水线要求规则的流水存在,并且该流水容易被工具重新组织。例如,例537中的代码定义一个可参数化的流水线乘法器。 例537 module multpipe #(parameter width = 8; parameter depth = 3) ( output [2*width-1: 0] oProd, input[width-1: 0]iIn1, iIn2, inputiClk); reg[2*width-1: 0] ProdReg[depth-1: 0], integerI; assign oProd=ProdReg [depth-1: 0]; always @ (posedge iClk) begin ProdRey[0]<= iIn1*iIn2; for (i=1;I