以下这篇内容,本来是某个深夜我在论坛上回复一位陌生工程师的帖子,越写越长,干脆整理成了一篇完整的文章。里面的场景、坑和结论,都是这几年做FPGA项目时被真实教训过的,希望能帮到用Vivado但一直没有认真翻过UG901的你。
先说一个我遇到过的典型场景。同事把新写的AXI寄存器模块接到了顶层,RTL仿真全部通过,点击Generate Bitstream,整个编译流程绿灯亮到底。结果打开实现后的原理图,新模块像蒸发了一样,怎么找都找不到。他把综合选项翻了个底朝天,怀疑人生。最后我在综合日志里搜到两行不显眼的消息:"Removing unused instance ...",原因才浮出水面:新模块的端口确实连了,但所有输出信号在顶层根本没有被使用,综合器按照优化规则直接把它当垃圾清掉了。
这件事让我意识到一个很普遍的问题:很多人用Vivado很多年,但对综合器的理解依然停留在"点Run Synthesis,等绿灯"的层面。遇到综合结果不符合预期、网表长得很奇怪、模块消失这类问题,只能凭感觉瞎试,而Xilinx官方有一本文档,恰恰就是专门解释综合器所有行为的,那就是UG901《Vivado Design Suite User Guide: Synthesis》。这篇文章我想从一个常年被综合器坑过的人的角度,把UG901里真正影响项目成败的细节,结合实战踩坑,一次性讲透。
1. 为什么我劝你别再傻点 Run Synthesis:UG901 在讲什么
很多工程师把UG901当成一本"参数手册":用到某个属性时翻一下,查完就关。这很可惜,因为UG901本质上是一份"综合器行为说明书",它回答的问题不是"这个按钮在哪",而是"综合器为什么把我的RTL变成现在这个网表"。
UG901的核心内容大致可以分成四大块。第一块是综合基础概念,包括RTL到网表的流程、综合与实现的边界、synth_design命令的各选项含义。第二块是HDL编码风格,讲的是什么样的Verilog/VHDL代码会综合出什么样的硬件结构,寄存器、锁存器、状态机、RAM、DSP的推断规则都在这部分。第三块是综合属性,比如ram_style、use_dsp、dont_touch、shreg_extract这些"魔法属性"的适用场景和生效条件。第四块是综合报告解读,教你怎么从资源利用报告、时序报告中判断设计质量。
这里必须先划清一个边界,因为太多人把综合和实现搅在一起。综合(Synthesis)做的事情是:把你写的RTL翻译成由逻辑原语(LUT、FF、BRAM、DSP48等)组成的网表,并在这个阶段做一轮逻辑优化,比如常量传播、资源共享、状态机编码、寄存器重定时。它不做布局、不做布线,也不做真正的时序收敛。布局布线是实现的活。所以综合报告里的时序数字,全部是基于估算模型算出来的,不是真实延迟。
理解了这条边界,很多困惑就能解开。比如有人问"为什么综合报告WNS为正,Implementation后却红了",原因是综合器的延迟估算过于乐观,没有考虑布线拥塞、时钟偏斜、扇出带来的实际延迟。反过来,也有人问"综合报告时序很烂,是不是没救了",答案是:只要结构上存在过大组合延迟,布线阶段再努力也救不回来。搞清楚哪些问题该在哪一阶段解决,是读UG901的第一课。
读UG901的正确姿势,我建议不要按顺序从头读到尾,而是先建立一张"行为模型"地图:综合器看你的代码时,脑子里其实是分类处理的——这个always块是时序逻辑还是组合逻辑?这个数组是大容量存储还是小寄存器组?这个乘加运算适合放DSP48还是LUT?不同的识别结果导向完全不同的优化行为。建立这张地图之后,你再看具体章节,会发现文档讲的不是抽象规则,而是每一类硬件结构的"挑选条件"。
2. 综合器眼里的"真硬件":寄存器、锁存器与组合逻辑的推断规则
综合器的本职工作之一,是把HDL语句翻译成硬件原语。同一段语法,因为写法不同,最后可能映射到完全不同的物理结构。UG901里最著名的部分就是HDL Coding Techniques,其中寄存器推断、锁存器推断、组合逻辑推断,是三个最常出问题的地方。
2.1 触发器推断:时钟边沿写法与复位映射
时序逻辑的标志是always块敏感列表包含时钟边沿。比如下面这段同步复位寄存器,是VERY标准的写法:
module sync_reset_ff ( input clk, input rst_n, input d, output reg q ); always @(posedge clk) begin if (!rst_n) q <= 1'b0; else q <= d; end endmodule综合器看到敏感列表里有posedge clk,就知道这是一个D触发器。复位条件的写法决定复位信号怎么映射:如果复位条件在时钟边沿分支内,被综合器识别为同步复位,很多情况下并不是映射到FF原语专门的复位引脚,而是需要额外的逻辑参与。如果写成异步复位模板:
always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; else q <= d; end综合器会把rst_n映射到触发器的异步复位端口(比如FDRE的CLR引脚)。
这里有个非常重要的实战经验:不要为了"用上异步复位引脚"就在所有寄存器上无脑加异步复位。UG901在复位策略上反复强调,Xilinx FPGA里的BRAM、DSP内部寄存器并没有全局复位能力,你给它们加复位,实际综合结果可能跟预期完全不同。更常见的问题是,一个设计里80%的寄存器根本不需要复位,复位信号的高扇出反而成为布局布线的负担。我见过一个项目因为全局复位扇出过高,时序收敛困难,后来把复位范围缩小到实际需要复位的模块,WNS直接提升了将近1ns。UART、复位同步器、关键状态机才需要复位,数据通路的流水线寄存器通常不需要。
2.2 意外锁存器:case 缺 default 和 if 缺 else
锁存器(Latch)是综合器推断规则里最典型的"你不想看到但经常出现"的结构。一个组合逻辑always块,如果存在某个分支没有对所有输出赋值,综合器为了保证功能完整,会推断出锁存器。
最常见的两个写法,一个是if没有else:
always @(*) begin if (sel) q = d; // 缺了 else 分支 end另一个是case语句没有default:
always @(*) begin case (sel) 2'b00: q = d0; 2'b01: q = d1; // 缺省 2'b10 和 2'b11 endcase end综合器识别到这种结构后,会生成锁存器,并且在综合日志里留下"Inferred Latch"一类的警告。很多工程师看到warning没事就过去了,但锁存器在FPGA里不是免费的原语,它通常会用LUT加上反馈回路实现,时序分析复杂、可测试性差,还容易在布局布线时引发奇怪的路径问题。
我的建议是:组合逻辑always块里,要么在开头给所有输出赋默认值,要么确保if/else和case/default完全覆盖。如果确实需要锁存行为,那就显式地用锁存器描述,并想清楚为什么不用时钟同步设计替代。
2.3 组合逻辑优先级:if-else 链和 case 的差别
同样是多路选择,if-else链和case在综合结果上有本质区别。if-else链是优先级结构,综合器会按优先顺序级联出选择逻辑;case则通常被综合成并行的多路选择器,没有优先级。逻辑上它们可能是等价的,但物理网表差别很大。
对短链来说差别不大,一旦分支数量增加,冗长的if-else链会让关键路径明显变长,因为优先级逻辑是串联结构,信号每通过一级选择器都有额外延迟。这也是UG901里反复提到的一个编码风格问题。我一般给团队定的规矩是:四个分支以上的多路选择,优先用case;带优先级的中断、仲裁类逻辑,用if-else;两者混合时,把case嵌在if-else里,而不是反过来。
3. 存储与算术资源:BRAM、分布式 RAM 和 DSP48 的推断边界
FPGA内部除了通用逻辑,还有大量专用硬件资源。综合器如何把你的数组和乘法运算映射到这些专用资源上,决定了面积和时序,而这部分恰恰是UG901最实用的章节之一。
3.1 BRAM vs 分布式 RAM:阈值、读写风格与 ram_style
对于寄存器数组(memory),综合器会判断是映射到分布式RAM(用LUT实现)还是块RAM(BRAM)。判断依据主要包括数组容量、数据位宽、读写端口数量、读写时钟是否相同,以及是否包含复位和异步读特性。
一般来说,容量很小的RAM基本落在分布式RAM;容量达到几十Kbit甚至更大时,综合器会优先考虑BRAM。这个阈值不是死的,和器件类型以及RAM配置模式都有关系。比如60x8这种小数组,几乎肯定是分布式RAM;4096x32这种大容量,如果不加任何限制,综合器基本会往BRAM上靠。
如果你有明确偏好,可以用ram_style属性强制约束:
(* ram_style = "block" *) reg [7:0] mem [0:1023]; always @(posedge clk) begin if (we) mem[addr] <= din; dout <= mem[addr]; end需要注意,综合器能不能按你的期望推断出BRAM,不仅看数组大小,还看你的读写风格。UG901里有很明确的描述:BRAM推断要求RAM的读取操作是同步读取。如果你的数组既有异步地址变化又直接读输出,综合器往往无法推断BRAM,只能退化成分布式RAM甚至寄存器堆。所以写RAM的标准动作是"写同步、读也同步",让输出经过一级寄存器。这也是为什么很多模板里dout都会先寄存一拍。
另外要特别注意:不要对RAM内容做异步复位。BRAM内部没有全局复位,如果代码里对RAM数组做初始化或复位,综合器就放弃BRAM推断,一个BRAM大小的设计瞬时变成成百上千个LUT,面积爆炸。
3.2 DSP48 自动推断:乘加树、乘法器与 use_dsp
Xilinx FPGA里的DSP48是专门为乘加运算设计的硬核单元,一个DSP48就能完成一个乘法加一个加法。综合器检测到乘法和乘加结构时,会有自动推断机制。
(* use_dsp = "yes" *) reg [15:0] acc; always @(posedge clk) begin acc <= acc + a * b; end上面这种累加乘法结构,是DSP48最喜欢的形态,综合器大概率会映射到DSP48的MACC模式。use_dsp属性有三个常用值:yes表示强制推断到DSP48;no表示不要使用DSP48;auto表示让综合器权衡。默认auto。
很多人不知道的是,use_dsp=yes不是万能灵药。DSP48是有限资源,当设计中的乘法器数量超过器件DSP48数量时,强制全部使用DSP48反而会让实现阶段资源紧张,导致布局困难。另外在某些低速小位宽场景,DSP48的固定结构可能不如LUT实现灵活。所以auto模式其实在多数时候是合理的,综合器会根据资源比例和时序压力做权衡。
真正需要注意的是运算的流水化。同样的乘加逻辑,如果组合路径里从输入直接到输出,中间没有寄存器,即使映射到DSP48,延迟依然很高,因为DSP48内部的寄存器级没有被利用。综合器不会主动帮你插入流水线,它只会在你已经写出寄存器结构的前提下,把寄存器吸收进DSP48。换句话说:先自己打流水,再谈DSP映射。
3.3 为什么"看起来聪明"的代码反而综合不出好网表
我在很多代码 review 里看到一种倾向:为了"减少逻辑",有人会写非常复杂的表达式,比如把所有条件判断压缩进一个三元运算符嵌套、在一个always块里写多个累计操作。这种代码在EDA工具看来恰恰是最难优化的——它限制了综合器的识别范围。
UG901的编码建议总结成一句话就是:写得直白,让工具一眼看出结构。乘加就写乘加,多路选择就写case,RAM读取就写同步读。刻意追求"代码短"往往会得到更差的综合结果,因为综合器需要花更多功夫去反推结构,而结构一旦反推失败,就只能用通用逻辑搭出又大又慢的电路。我在实际项目里的体会是,RTL代码首先是为综合器和后续维护工程师服务的,它应该最大程度暴露设计者的意图,而不是展示语言技巧。
4. 综合策略与选项:看懂 synth_design 的每一个关键开关
Synth_design是Vivado综合引擎的总入口。很多人跑综合只双击一个按钮,但脚本自动化、性能调优时,这些选项能显著改变结果。UG901里有一个完整的选项表,我挑几个影响最大的讲。
4.1 mode 参数:default 和 out_of_context 的选择
synth_design的-mode参数主要影响综合的"上下文"。最常见的是default,整个设计作为完整上下文综合,端口、约束、时钟全部参与优化。另一种是out_of_context,简称OOC,它把某个模块隔离出来单独综合,不依赖顶层时钟和约束,生成一个可以被"看成一个黑盒"的网表。
IP核默认使用OOC模式,所以你在顶层综合后的原理图里看到IP只是一个黑盒符号,这是正常的,不是模块丢了。如果想让某个IP随顶层一起综合,可以把它改成Global模式,但代价是每次顶层综合都要重新处理IP内部逻辑,编译时间明显上升,而且大型IP的时序优化在顶层上下文中未必更好。工程里我基本遵循官方建议:IP保持OOC,自己的子模块做层次化设计时也可以对部分模块用OOC,这样可以避免每次综合整个工程,加快迭代速度。
4.2 flatten_hierarchy:三档选择背后的隐藏成本
flatten_hierarchy控制综合器如何处理设计层次,可选值主要有none、rebuilt和full。none表示保持各模块层次独立,综合器不会跨模块边界优化;full表示把整个设计展平成一个平面网表,逻辑优化范围最大;rebuilt则是折中方案,也是现代Vivado的默认行为——优化时允许跨层次合并,但综合完成后会尽量重建层次结构。
初学的人往往觉得full最聪明,其实不一定。展平后综合器确实能做更激进的优化,但完全失去层次意味着后续实现阶段无法利用层次分区的物理约束,布局器可用的信息也变少。对于大设计,full会明显拉长编译时间,并且调试时网表里找不到"这是哪个模块的逻辑"的线索。我的建议是:小型设计或性能压力大的设计可以试full对比一下;中大型设计保持默认rebuilt;调试阶段用none,因为Schematic视图里的层级关系最清晰。
4.3 状态机编码:one_hot 不是万能的
状态机的编码方式直接影响FF资源和组合逻辑复杂度。fsm_encoding选项可以选auto、one_hot、sequential、gray、johnson。Vivado默认auto,让它自己判断。
one_hot编码每位状态一个寄存器,转台逻辑简单、容易跑到更高频率,但FF开销大;gray和sequential编码位数少,但状态译码组合逻辑更复杂。对于状态数多的FSM,one_hot的FF数量会线性增长,有时反而不划算。UG901的建议方向是:小型FSM用sequential/gray省资源,大型且时序敏感FSM用one_hot。我在做高速接口控制器时通常会把状态机明确设成one_hot,因为它能降低组合路径深度;做低速控制逻辑时则不干预,让auto去选。
其实比编码更重要的,是状态机本身不要让综合器"心想事成"。如果你写了不可达状态、不完整的状态转移,综合器在优化时会按它的理解做冗余状态合并,结果和你仿真时的预期行为可能不一致。所以FSM代码务必要有完整的default处理和复位初始化。
4.4 retiming、shreg_min_size 等会改变结构的开关
retiming是综合器的一项重定时优化:它可以在组合逻辑两侧移动寄存器的边界,本质上是把流水级重新分配,让关键路径减短,前提是不改变模块输入输出端的寄存器数量。听起来很美好,但实际使用时需要注意,retiming会移动你写在代码里的寄存器位置,综合后的网表里寄存器名字和位置可能和你源码完全对不上,调试可能比较困难。在性能危机的场合我会打开,日常不用。
shreg_min_size影响移位寄存器的推断方式。位移寄存器如果长度超过阈值,综合器会把它映射到SRL16/SRLC32这种专用移位原语,而不是用普通FF链。这样做节省FF资源,但SRL结构不支持随机访问,也不适合需要中间级输出的移位逻辑。写移位寄存器之前先想清楚:这个移位寄存器只是延时,还是需要抽头输出?需要抽头就不要让综合器映射成SRL,否则会因为访问冲突导致网表功能和仿真预期不一致。
resource_sharing的默认状态是auto,综合器会尝试让多个算术操作共享同一个加法器或乘法器,节省面积,但会引入额外的多路选择器。如果你追求性能、不在乎面积,可以在综合时关闭资源共享;如果面积紧张,开auto通常是合理选择。
还有一个经常被忽略的directive参数:default、RuntimeOptimized、AreaOptimized等。RuntimeOptimized会在不影响质量的前提下减少编译时间;AreaOptimized倾向减小面积;AlternateRoutability某些场景下能改善布线困难。它们不改变设计功能,只影响综合器的优化"口味"。日常开发用default,大规模编译赶时间用RuntimeOptimized,高资源占用且布局布线困难的项目可以试AlternateRoutability。
5. 约束在综合器里到底有什么用:把时序意图提前给优化器
很多工程师对约束的理解是"给Implementation用的",于是综合阶段不写任何XDC,等到实现阶段才把约束加进去。这种做法非常危险。UG901明确说明,综合器在优化过程中会读取SDC/XDC约束,并利用这些约束指导逻辑优化方向。
5.1 综合器如何消费时钟约束
综合器需要知道每个时钟周期是多少,才能判断哪条路径是紧的,哪条路径是松的。如果你不给create_clock,综合器默认所有信号都是不受时序约束的,它会做大量"无意义优化"——它不知道该朝哪个方向努力,结果往往是把面积优化得很小,却把关键路径逻辑堆积在一起。等到实现阶段加上约束,布线器发现到处都是长路径,悔之晚矣。
对于多时钟设计,时钟约束还会影响跨时钟域路径的处理。如果你明确声明set_false_path,综合器就知道这条路径不需要时序优化,它可以把优化资源集中到真正关键的路径上。反之,如果没有false path约束,综合器会尝试优化两个无关时钟域之间的路径,浪费时间不说,还可能为了满足这些虚假路径的时序要求而牺牲正常路径的质量。
5.2 set_false_path 和 set_max_delay 的实战位置
我习惯在综合前就把完整的时序约束写好,至少包括三类。第一类是create_clock和create_generated_clock,把时钟整清楚。第二类是输入输出延迟,用set_input_delay和set_output_delay约束端口路径。第三类是跨时钟域的set_false_path或set_max_delay。比如异步FIFO两侧时钟没关系,直接false path掉:
set_false_path -from [get_clocks clk_axi] -to [get_clocks clk_gmac]有些跨时钟域路径虽然不需要严格时序收敛,但完全false掉可能过于激进,比如握手信号需要脉冲宽度足够,可以考虑set_max_delay限定一个范围。这类约束需要根据实际CDC设计去取舍,没有统一答案。
注意把约束放在综合前还有一个好处:如果在综合阶段就能看到这些约束对WNS的影响,等于提前验证约束本身是否合理。比如某个set_input_delay设得太苛刻,综合报告直接可以看出逻辑路径宽的离谱;如果你等到实现阶段才发现,一个半小时的编译时间已经搭进去了。
5.3 综合报告的时序数字:WNS/TNS 该怎么看
综合后的时序报告本质是"预布线估算",不要把它当成真实时序。但它是很好的早期预警:如果综合报告中某条路径的WNS已经很快或者很差,说明结构上松紧已经定了型。布局布线阶段能优化的通常是5%到15%的量级,不可能把一条组合逻辑延迟10ns的路径缩短到3ns。
我记得有个项目,综合后WNS已经是负数,但当时觉得"反正综合是估算,等布线再看"。结果布线后WNS比综合报告还要负几百皮秒,白白多花了两天做floorplan和pipeline调整。现在我的做法是:综合阶段WNS无论如何都要先做到正,哪怕只是勉强为正,否则大概率进入实现阶段后是红的。这不是UG901里的硬性要求,但是一个非常有效的经验阈值。
5.4 别忽视综合对常量传播的处理
综合器有一个极其重要的行为:常量传播。如果某个端口被固定为常量,或者某个使能信号恒为有效,综合器会顺藤摸瓜把相关逻辑全部简化。这在设计中是双刃剑:简化掉无用的逻辑是好事;但如果你的某个信号只是暂时绑死,以后要复用,注释和约束一旦没跟上,逻辑就被优化得面目全非。所以,涉及测试模式、启动模式、版本选择这类端口,一定要用参数或信号区分清楚,不要直接硬编码常量。这也能解释一种常见现象:同一个模块在testbench里工作正常,综合到顶层后被裁剪了,因为某个输入条件在你的实际连接中不可能发生,综合器按照你的连接关系把它定义为死逻辑。
6. 综合报告与原理图视图:把推断结果"验尸"出来
综合完成后,第一步不是直接点Next,而是打开报告和Schematic视图,看看综合器到底"理解"了你的设计没有。这一步很多时候能救回一整天的DEBUG时间。
6.1 综合报告的核心数字:FF/LUT/BRAM/DSP 的解读逻辑
综合报告的资源利用章节,会列出FF、LUT、LUTRAM、BRAM、DSP、URAM、IO等资源的用量。看这个表不能只看总量,要结合你本次改动去判断增量。比如你明明加了一组256x32的RAM,报告里BRAM却只多了0.5块,这时候就值得怀疑:你的数组真的推断成BRAM了吗?还是变成了LUTRAM?
比较两个版本的综合报告也是好习惯。我通常会导出两份utilization CSV做diff,新增模块对应的资源变化一目了然。如果期望的资源没有出现,大概率是以下三种情况之一:代码被打平到其他逻辑里了、被常量传播裁剪了、被推断成了非预期结构(该用BRAM的用成了分布式RAM,该用DSP的用成了LUT)。
6.2 用 Schematic 验证 RAM/DSP 是否被正确推断
综合完成后,在Vivado里点击Open Synthesized Design,打开Schematic视图,你能直接看到逻辑连接。验证RAM推断是否成功,最直接的办法是找到对应的memory实例,看它是一个BRAM原语(RAMB18E1、RAMB36E1)还是由一堆LUT和FDPE组成的分布式实现。验证DSP同理,看到DSP48E1/DSP48E2原语,说明推断成功;看到一堆LUT加进位链,说明推断失败或你主动禁用了DSP。
Schematic视图还有一个用途:追踪关键路径。在Schematic里选中一条高亮路径,能看出组合逻辑经过了哪几级,中间哪个节点跨了层次。我曾经通过Schematic发现某条关键路径被不必要地穿了一高一低两个模块,最后通过调整模块层级和寄存器位置,路径长度直接减少了30%。
6.3 顶层加了新模块,写Bitstream后原理图却找不到:完整排查链
现在专门回到开头那个问题。顶层新加模块,综合后原理图里没有它,我建议按下面顺序排查,每一步都对应UG901里的具体机制。
第一步,看Sources窗口。新模块如果在Design Sources里显示灰色,说明它没有被任何上层模块例化,或者说例化关系断了。这时候回到RTL里搜索模块名,看例化语句是否真的存在。
第二步,检查RTL Elaboration。综合之前,先点击Run Synthesis旁边的"Elaborated Design",打开原理图。如果这里能看到新模块,说明RTL连接正常,问题出在综合优化环节。如果这里就看不到,问题在RTL连接本身,和综合器无关。
第三步,如果Elaboration能看到但综合后消失,去综合日志里搜"Removing"或者"Unused"。综合器删除一个未使用实例时,会打出Remove消息。新模块所有输出悬空、没有扇出,在综合器眼里就是死逻辑。此时有两种选择:要么接受综合器优化,把悬空引脚接上或去掉这个例化;要么明确告诉综合器保留它,加上dont_touch属性:
(* dont_touch = "true" *) module debug_probe ( input clk, input trig, output reg [31:0] cnt );加了dont_touch之后,综合和实现阶段都会保留该实例,不会因为"没有扇出"被移除。但注意,dont_touch是一把重剑,它同时阻止了后续优化,能不用就不用。
第四步,确认是不是用了Incremental Compile或Reuse。Vivado的综合/实现支持增量复用,如果综合结果被判定为可以复用,新的模块可能不会进入当前阶段的网表。这种情况在工程设置里能看到相关选项,必要时Reset Run后重新编译。
第五步,检查IP的OOC模式。如果新模块是一个IP核,而该IP被配置成OOC综合,顶层综合后的原理图里只会以一个黑盒符号出现,看起来就像"没有模块"。双击黑盒或打开IP自己的综合设计,内部逻辑其实都在。这个问题不属于故障,是综合策略的正常形态。
6.4 增量综合是双刃剑
增量综合(Incremental Synthesis)的思路是复用上次综合中没变的部分,只重新综合有变化的部分,从而加快迭代。听起来很香,但我在实践中吃过亏:有时RTL已经修改了,综合器因为某种原因判断可以复用旧结果,导致原理图里的结构和预期不一致。UG901对incremental compile的限制条件其实写得很细,包括RTL结构指纹、综合选项一致性、约束文件变更判断等,一旦某个触发器没触发,结果就可能是你想看到的新模块没有出现。建议在关键节点用干净的Full Rebuild验证一遍,再决定是否长期开启增量复用。
7. 综合器"脾气"实录:五个差点让我怀疑人生的设计细节
最后这一节,我把自己在真实项目中踩过、也在UG901中找到解释的五个教训列出来,每条都可以当独立检查项用。
第一个是复位模板的纪律问题。曾经一个项目里,两个工程师分别写了两种复位风格:一部分模块用异步复位,一部分用同步复位,接口之间没有同步,综合器自然按照各自代码模板映射,结果复位释放时的亚稳态问题在板级测试中暴露,定位花了好几天。现在的统一约定是:异步复位只用在复位同步器、需要立即失效的关键控制路径;其余统一走同步复位,并且所有异步复位释放前必须经过同步器。综合器能识别代码模板,但它不负责帮你判断复位策略的合理性。
第二个是阻塞赋值和非阻塞赋值混用。时序逻辑always块里用阻塞赋值,综合器通常能推断出寄存器,但仿真行为和门级网表行为很容易不一致;组合逻辑always块里用非阻塞赋值,仿真和综合结果的差异更隐蔽。我见过一个案例:组合逻辑里写了q <= d;,综合器最终推断出了寄存器,但仿真波形里输出总是慢一拍,跑到板子上干脆错乱。现在代码检查阶段就强制:时序块只用<=,组合块只用=,分开写清楚,不要混在一个always块里。
第三个是组合逻辑里放大乘法器。有人为了"省一级流水线",把大位宽乘法直接放在组合逻辑里,综合器可能把它推断到DSP48,但DSP的寄存器级没有被利用,输入到输出一条组合长路径贯穿整个乘法器,布线后的时序惨不忍睹。正确做法是把乘法拆开打流水,比如16x16乘法加累加,插入一两级寄存器后,综合器和实现器才能发挥DSP48内部的流水优势。
reg [31:0] mul_reg; reg [31:0] acc; always @(posedge clk) begin mul_reg <= a * b; end always @(posedge clk) begin acc <= acc + mul_reg; end第四个是状态机没有default分支。这个坑和锁存器问题同源。FSM的case语句如果不写default,综合器可能会生成"保持当前状态"的锁存反馈逻辑,在某些器件上表现为偶发功能错误。高可靠设计里,状态机一定要有default分支并回到复位态或安全态,不要再把"不会出现"当理由,综合器不会替你做这种判断。
第五个是过度依赖综合报告的资源表而忽略网表结构。资源表只能告诉你数量和比例,不能告诉你是谁占用了DSP、哪块BRAM由哪个模块推断。要回答这个问题,必须回到Schematic视图里选中实例,查看它所属的层级和源码。UG901的整个报告体系设计思路就在这里:报告给你量化视图,schematic给你结构视图,两者配合才是完整的综合体检。
聊到最后,分享一个我养成很久的习惯:每次跑完综合,先不急着看时序,而是先看两样东西——综合日志里的"Inferring RAM/DSP/Latch"一类消息,以及Utilization报告里的增量。这两样东西能最快暴露RTL与网表之间的理解偏差。UG901不是一本用来从头读到尾的教材,它是你遇到具体困惑时用来对号入座的字典。搞懂了综合器的脾气,很多看起来玄学的Vivado问题,其实都能在文档里找到明确答案。