ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vivado综合阶段HDL与XDC属性设置完整指南:从KEEP到时序约束

Vivado综合阶段HDL与XDC属性设置完整指南:从KEEP到时序约束 有不少人拿到一个Vivado工程第一件事就是先跑一遍综合看到时序报告没有大红大紫就以为万事大吉。我自己刚开始做FPGA的时候也是这样结果在一个项目里被一个“找不到的寄存器”坑了两天综合报告里清清楚楚显示这个信号还在打开网表一搜它早被优化得干干净净。从那以后我养成一个习惯在写RTL的时候就顺手把HDL属性声明加上等需要做详细外部时序约束时再在XDC里补齐对应的属性设置。这个系列的第二篇我们就围绕HDL和XDC属性设置清单把综合阶段最值得用的属性和约束从原理到实操完整捋一遍内容包括每个属性的作用对象、生效时机、典型写法以及我实际调试中踩过的坑。不管你是刚接触Vivado的新手还是已经被时序收敛逼到头秃的老工程师这份清单应该都能帮上忙。1. 属性能干什么先画清HDL属性和XDC属性的职责边界1.1 两类属性的生效时机和作用域不一样乍一看HDL属性和XDC属性都是“告诉工具怎么做”但它们在时间轴上的位置完全不同。HDL属性写在RTL的源文件里是给综合器看的。你写一行 (* keep true *) wire cnt_en; 综合器在解析这个wire时就会读到这条声明接下来做逻辑优化、资源推断、寄存器合并时都会把它纳入考虑。所以HDL属性可以影响综合器在RTL层面的每一次结构选择比如把这段代码推断成BRAM还是LUTRAM把状态机编码成one-hot还是gray。XDC属性则不一样。XDC是基于Tcl语法的约束文件综合阶段和实现阶段都会读取但不同类别的约束在不同的阶段起作用。像create_clock、set_input_delay这类的时序约束综合阶段就会读因为综合器要估算路径延迟来指导优化而set_property PACKAGE_PIN这种管脚位置约束虽然综合阶段也能接收但真正落地发生在实现阶段。还有一些属性比如DONT_TOUCH、MARK_DEBUG综合前读和综合后读的效果还不一样综合前读能影响综合优化综合后读则更多是保护已经生成的网表。这个区别非常关键。我见过很多工程师把DONT_TOUCH写进XDC但XDC文件被设置成只参与实现阶段结果综合阶段根本读不到这条约束该优化的地方照样优化到了布线阶段DONT_TOUCH才生效网表已经变样。所以理解属性的生效时机比背一百条语法都重要。1.2 为什么要在综合阶段就把关键属性写到位一个很常见的想法是反正综合完还能改属性晚点加也没事。这个想法在部分场景下成立但在很多关键场景会吃亏。综合器的优化动作是不可逆的。如果综合器已经把一段乘加逻辑推断成LUT阵列等你在综合后的网表里再设USE_DSP往往需要重新综合才能生效或者只能通过属性重映射原语复杂度成倍增加。更实际的原因是RTL属性是跟着源码走的。你写了 (* ram_style block *) 放在数组声明前这段代码就算从这个工程复制到别的工程属性也不会丢。而XDC文件经常因为工程结构变化、约束文件被排除等原因失去作用属性写在RTL里相当于多了一层保险。还有一种情况是做跨模块优化。综合器默认可以在不同模块之间做常量传播、寄存器吸收、等价逻辑合并这是好事但也可能把你在子模块里精心设计的结构打散。如果这是你不希望发生的就应该在综合前用KEEP_HIERARCHY或DONT_TOUCH把边界保护起来。等综合完再处理原始的结构信息已经部分丢失了。1.3 一句口诀HDL属性管“优化方向”XDC属性管“约束条件”我用一句话总结自己的经验HDL属性是给综合器的优化方向说明书XDC属性是给布局布线工具的行为约束。前者回答“这段逻辑我期望它长成什么样”后者回答“这个端口、这个时钟、这条路径必须满足什么条件”。在实际操作中我的习惯是凡是跟结构选择、资源类型、防优化相关的属性尽量写进RTL比如KEEP、DONT_TOUCH、RAM_STYLE、USE_DSP、FSM_ENCODING凡是跟外部接口、时序参数、物理位置相关的约束写在XDC里比如IOSTANDARD、PACKAGE_PIN、时钟周期、输入输出延时。遇到两边都能写的比如ASYNC_REG和MARK_DEBUG就两个地方都写上保证在不同阶段的一致性。这个习惯帮我减少了很多“属性失效”问题。2. 综合阶段最常用HDL属性解析2.1 KEEP、PRESERVE和DONT_TOUCH三种“保留”到底有什么区别很多朋友分不清KEEP、PRESERVE和DONT_TOUCH之间的关系我理解它们其实分两个维度KEEP和PRESERVE偏向“防止综合优化把它删掉”DONT_TOUCH是“把整个对象保护起来综合和实现阶段都不准动”。KEEP最常用在wire和reg这类信号上。当信号在代码里生成但最终没有扇出或者只是作为调试信号时综合器很容易把它优化掉。你如果后续还要用这个信号做约束、做ILA调试或者在网表里手工分析就需要声明 (* keep true *) wire sig; 保住它。注意KEEP只保证综合阶段网络被保留到了布局布线阶段它不一定能挡住优化所以如果是要跨实现阶段保护还得用DONT_TOUCH。PRESERVE更偏向保留寄存器或某些逻辑单元不被合并。比如两个寄存器因为功能等价被综合器合并这通常能省面积但如果你明确希望保留独立的寄存器可以用 (* preserve true *) 声明。Vivado综合里PRESERVE对寄存器、状态单元和某些组合逻辑的保留效果比KEEP更具体。DONT_TOUCH则是大杀器它可以放在wire、cell、instance、module上告诉综合器和实现器这个对象不能优化、不能合并、不能删除。典型写法是 (* dont_touch true *) my_module u_inst (...); 或者加在wire上。这个属性在保护特定模块、防止跨层次优化时非常好用但千万别随手给整个顶层模块加dont_touch那样会关闭几乎所有优化空间面积和时序都会变得非常难看。(* keep true *) wire cnt_en; (* preserve true *) reg [3:0] sync_cnt; (* dont_touch true *) my_module u_inst (...);我自己的经验是KEEP和PRESERVE能解决的问题优先不要动用DONT_TOUCH。前者优化的范围小对面积和时序的副作用也小DONT_TOUCH更像最后一道防线用它的时候心里要想清楚如果这段逻辑完全不做优化后果我能不能接受2.2 RAM、ROM和DSP资源推断不是你猜出来的是声明出来的Vivado综合器有很强的RAM/ROM/DSP推断能力。但“能推断”不代表“推断得对”。它判断的标准往往跟你预期的资源类型不完全一致。比如一段简单的双端口RAM综合器可能因为写读地址风格、使能信号组合等问题最终推断成LUTRAM而不是BRAM。这时候你不需要把代码重写一遍直接用属性声明期望的资源类型即可。RAM_STYLE属性是最常用的。(* ram_style block) 强制使用BRAM(ram_style distributed) 强制使用LUTRAM在UltraScale系列里还可以用 (ram_style ultra) 强制使用URAM。ROM也有类似属性(rom_style block) 或 (rom_style distributed *)。DSP也类似。乘加运算默认由综合器根据资源和性能做分配可以用 (* use_dsp yes) 把乘加推向DSP48E用 (use_dsp no) 让它回到LUT逻辑(use_dsp max *) 则是尽可能多用DSP。(* ram_style block *) reg [15:0] buff [0:1023]; (* rom_style distributed *) reg [7:0] lut_data [0:255]; (* use_dsp yes *) wire [31:0] mul_res;用这类属性时要留意一个前提它只对“综合器能识别为RAM/ROM/DSP”的结构有效。如果你的代码风格比较随意地址倒换、读写端口很乱哪怕声明了block综合器也会报warning然后Fallback到其他实现。所以遇到资源用不上先去看warning再看看代码是不是能优化成标准模板这比盲目加属性更有效。2.3 FSM编码属性状态机面积与速度的权衡状态机是FPGA逻辑里最常见的结构默认情况下Vivado综合器会根据状态数量和目标频率自动选择编码方式FSM_ENCODING AUTO。这个自动选择通常不差但当你对时序、面积有明确预期时手动指定编码属性能带来更稳定的结果。常用的FSM_ENCODING取值包括one_hot一位状态一比特状态寄存器数量多但组合逻辑简单、路径短适合速度敏感和状态数较少的设计。gray相邻状态变化时只有一位翻转跳转时间短状态寄存器少适合连续跳转的状态机。sequential状态编码按顺序递增面积最省但组合逻辑可能更复杂适合面积优先的设计。johnson折中方案解码逻辑比较简单。代码写法是把属性加在状态寄存器上(* fsm_encoding one_hot *) reg [7:0] state;如果你是沿用Verilog的parameter写状态而没用enum类型Vivado也能识别只要状态寄存器的赋值满足状态机的典型写法。我的建议是只有当综合报告明显偏离你的预期、或者某个状态机的组合逻辑成为了关键路径才考虑手动指定。不要每个状态机都去手动指定尤其是灰色编码和one-hot选错反而会让面积和时序一起变差。2.4 同步器和CDC打拍ASYNC_REG属性的正确用法跨时钟域处理最基础的方案就是两级甚至三级同步打拍。但很多人的同步器只是“代码上写了两级寄存器”综合器可不会自动把它们当成同步器。如果这两个寄存器之间插入了组合逻辑或者它们被当作普通寄存器参与优化同步效果就可能被破坏。处理CDC信号的正确做法是在RTL里给同步器的寄存器加ASYNC_REG属性。Vivado会据此把这个寄存器标记为异步寄存器不参与普通寄存器的等价合并、重定时等优化而且在时序分析时也作特殊处理。常用写法如下(* async_reg true *) reg sync_a, sync_b; // 或对向量 (* async_reg true *) reg [1:0] sync_vec;需要注意RTL里加了ASYNC_REG属性只是第一层XDC里最好也同步设置set_property ASYNC_REG TRUE并在CDC路径上添加合理的set_false_path或者set_clock_groups否则即使寄存器保住了时序分析报告也会对这一串寄存器报出异常路径影响其他路径的收敛判断。这是很多新手容易漏的第二步。2.5 还有一些虽不常用但很救命的HDL属性除了前面几个大块头综合阶段还有几个属性值得收藏MAX_FANOUT限制信号最大扇出通常配合高扇出复位、使能信号使用。写法是 (* max_fanout 64 *) reg rst_n; 综合器会根据条件复制寄存器。注意扇出限制不是越小越好复制太多寄存器反而会消耗面积也会影响时钟树和布局。SHREG_EXTRACT / SRL_STYLE控制移位寄存器是推断成SRL16/SRLC32还是普通寄存器链。在有些工艺或时序要求下强制SRL风格可能更省LUT但后续无法用FF资源如果代码里还有异步复位需求可以用 (* shreg_extract no *) 关掉SRL推断。KEEP_HIERARCHY写在子模块上(* keep_hierarchy yes *) 可以防止综合器跨模块吸收逻辑。需要保持模块边界做后续调试和复用的时候很有用。EQUIVALENT_REGISTER_REMOVAL默认等价寄存器会被合并如果你明确希望保留可以用 (* equivalent_register_removal no *) 关闭。不过大多数情况下默认就好别乱关。DONT_RETIME防止综合器对这个寄存器做重定时在处理一些对延迟敏感的逻辑时能派上用场。REGISTER_BALANCING控制寄存器平衡优化对流水线结构调整很有用但需要搭配时序分析一起看。这些属性不常用但遇到具体问题时比绕道改代码高效得多。我会在后面的速查清单里把它们补全。3. XDC属性设置的实操要点3.1 XDC里怎么写KEEP、DONT_TOUCH和MARK_DEBUGXDC文件是Tcl语法属性设置用set_property。单个对象最简单的方式set_property KEEP TRUE [get_nets {u_top/u_inst/enable}] set_property DONT_TOUCH TRUE [get_cells {u_top/u_inst/u_ram_ctrl}] set_property MARK_DEBUG TRUE [get_nets {u_top/debug_counter[7:0]}]KEEP在XDC里也能设但我更建议KEEP放在RTL里声明因为RTL信号名是源码层面的名字写起来最直观而且跟着代码走不会丢。DONT_TOUCH则经常在综合后设置因为你可以先打开综合网表看看哪些cell被保留下来了再决定对哪个具体实例加保护。如果是综合前XDC里设置DONT_TOUCH则要确保对象路径在RTL里就能被get_cells对应上否则会静默失败。MARK_DEBUG是调试时非常常用的属性。当你需要在Vivado里把某个内部信号接到ILA核上时可以先用set_property MARK_DEBUG TRUE标记这个信号然后在硬件管理器里自动生成ILA。MARK_DEBUG的前提是信号在网络中存在如果它在综合时已经被优化掉设置也不会生效。所以怀疑信号被优化时先回到RTL给它加KEEP再用MARK_DEBUG。还有一个容易让人困惑的属性是CLOCK_DEDICATED_ROUTE。当某个时钟信号没有满足专用的时钟路由规则Vivado会在实现阶段报错或警告。如果确认你的时钟来源没有问题只是绕了某一小段路径可以用set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk_net] 放宽检查。但这属于“下不为例”型操作每次放宽都应当在代码或注释里写清楚为什么这么做否则风险很大。3.2 IO约束不只是管脚位置IO约束是XDC里最基础的部分很多新手只写了PACKAGE_PIN和IOSTANDARD就算完事。但真实项目中IO标准、驱动能力、翻转速率、上下拉这些属性同样会影响到板级信号的完整性和时序。举个例子set_property PACKAGE_PIN U18 [get_ports clk_200m_p] set_property IOSTANDARD LVDS [get_ports clk_200m_p] set_property PACKAGE_PIN U19 [get_ports clk_200m_n] set_property IOSTANDARD LVDS [get_ports clk_200m_n] set_property PACKAGE_PIN W20 [get_ports data_out[0]] set_property IOSTANDARD LVCMOS18 [get_ports data_out[0]] set_property SLEW FAST [get_ports data_out[0]] set_property DRIVE 12 [get_ports data_out[0]] set_property PULLUP TRUE [get_ports key_in]SLEW控制信号翻转速率FAST会给更陡峭的沿适合高速输出但太陡又会带来反射板上走线不长时没必要强行FAST。DRIVE是输出驱动强度一般根据负载选择。PULLUP用在按键输入、拨码开关等上拉需求场景用一个外部电阻往往更稳FPGA内部的弱上拉只是辅助。很多人不知道set_property还支持-dict参数可以把多个属性一次性设置set_property -dict {PACKAGE_PIN W20 IOSTANDARD LVCMOS18 SLEW FAST DRIVE 12} [get_ports data_out[0]]这在工程中大量IO约束时能省不少建模时间。IO约束还会牵涉到IO Bank电压不同电平标准要求不同的VCCO如果XDC里设置了LVCMOS33但Bank实际供的1.8V那大概率会烧坏IO或者直接崩塌。所以IO约束不仅是“写上去”还要和原理图对照。3.3 时序例外约束从综合阶段就要考虑时序约束是XDC里的重头戏但很多人习惯在综合完之后再写时序约束。我建议反过来在做综合之前就把关键时序约束写好。因为综合器需要时钟周期、输入输出延迟、例外路径来计算路径延迟从而决定逻辑结构和优化力度。最基本的时钟约束create_clock -period 5.000 -name clk200 [get_ports clk_200m] create_clock -period 8.000 -name clk125 [get_ports clk_125m]然后是输入输出延时set_input_delay -clock clk200 -max 2.5 [get_ports data_in] set_output_delay -clock clk200 -min 0.5 [get_ports data_out]跨时钟域路径如果确实不需要做时序收敛用set_false_path或set_clock_groups显式告诉工具set_false_path -from [get_clocks clk200] -to [get_clocks clk125] set_clock_groups -asynchronous -group [get_clocks clk200] -group [get_clocks clk125]对于某些路径数据不是每个时钟周期都有效可以用set_multicycle_path扩展建立和保持关系。比如一个只在每两个周期采一次数据的寄存器set_multicycle_path -setup 2 -from [get_pins u_top/reg_a_reg/C] -to [get_pins u_top/reg_b_reg/D]还有一个容易被忽视的命令是set_case_analysis。它告诉工具某个信号在正常运行时恒定为0或1让工具把相关逻辑门剪掉以利于优化set_case_analysis 0 [get_pins u_inst/mode_sel_reg/Q]不过这个约束一定要谨慎信号真不是恒定的情况下综合结果会完全不正确。3.4 控制XDC文件参与综合和仿真的范围Vivado里每个XDC文件都有一个参与阶段的属性。默认情况下XDC文件会同时参与综合和实现。但某些XDC可能只想在实现阶段生效或者只想在仿真里用这时就可以用File Properties里的USED_IN_SYNTHESIS / USED_IN_SIMULATION来控制。set_property used_in_synthesis false [get_files io_only.xdc] set_property used_in_simulation false [get_files timing_only.xdc]这对工程管理很有价值。比如你有一份只做IO管脚位置约束的文件它不影响综合逻辑但又不想被综合器反复读取增加编译时间就可以把它设为只在实现阶段使用。仿真专用约束文件更是可以通过设置让综合阶段完全不加载。在源文件较多的工程里当我怀疑某个属性没生效时第一件事就是检查它的XDC是不是被排除了或者是不是因为文件优先级问题被其他XDC里的同名属性覆盖了。这个问题在后面还会专门展开。4. 属性速查清单与排查技巧4.1 HDL属性速查清单我整理了一份自己在项目里常用的HDL属性表按作用对象排列属性作用对象典型取值主要功能keepwire / regtrue防止信号被优化删除preservereg / celltrue保留寄存器或单元不被合并dont_touchwire / instance / moduletrue综合和实现阶段都禁止优化ram_stylememory变量block / distributed / ultra指定RAM实现资源rom_stylememory变量block / distributed指定ROM实现资源use_dsp乘加运算yes / no / max控制乘加是否映射到DSPfsm_encoding状态寄存器auto / one_hot / gray / sequential / johnson状态机编码方式async_reg同步器寄存器true标记异步同步寄存器shreg_extract移位寄存器yes / no是否推断SRLsrl_style移位寄存器reg / srl / srl_reg等SRL和FF的组合方式max_fanoutreg / instance整数限制最高扇出keep_hierarchymoduleyes / no保持模块层次equivalent_register_removalregno禁止等价寄存器合并dont_retimeregtrue禁止重定时优化这份表看起来条目不少实际项目里高频使用的也就前八行。有需要时再把后面的翻出来。4.2 XDC属性速查清单XDC常用属性和命令整理如下约束 / 属性作用对象典型值主要功能set_property KEEPnetTRUE保留网络不被综合删除set_property DONT_TOUCHcell / netTRUE综合和实现阶段保留对象set_property MARK_DEBUGnetTRUE标记信号用于ILA调试set_property MAX_FANOUTcell32 / 64限制扇出set_property ASYNC_REGcellTRUE标记异步同步寄存器set_property PACKAGE_PINport如 U18管脚位置set_property IOSTANDARDportLVCMOS33等IO电平标准set_property SLEWportFAST / LOW翻转速率set_property DRIVEport4 / 8 / 12 / 16输出驱动强度set_property PULLUPportTRUE内部上拉set_property CLOCK_DEDICATED_ROUTEnetFALSE放宽时钟专用路由检查create_clock时钟Port-period / -name创建时钟set_input_delayInput port-max / -min输入延迟约束set_output_delayOutput port-max / -min输出延迟约束set_false_pathPathfrom / to定义伪路径set_clock_groupsClock group-asynchronous定义异步时钟组set_multicycle_pathPath-setup / -hold多周期路径set_case_analysisPin / Port0 / 1静态逻辑约束set_property USED_IN_SYNTHESISXDC filefalse排除综合阶段set_property USED_IN_SIMULATIONXDC filefalse排除仿真阶段这个清单不是让你全背下来而是遇到问题的时候知道去哪里查。我电脑里就长期放着一份这样的表每次新项目开始前过一遍。4.3 属性写了却没生效怎么办排查思路我在技术答疑群见过最多的问题是“我明明加了KEEP/DONT_TOUCH/RAM_STYLE为什么没有效果”排查这类问题是有套路的按顺序做基本都能定位第一步先确认属性的作用对象对不对。KEEP必须作用在net或wire上你写在module上大概率不生效DONT_TOUCH作用在instance或cell上你写在reg变量上就不是同一个概念。打开综合报告搜索“property”或“attribute”关键词看工具是否识别了这条属性。第二步确认路径是否正确。综合后的网表对象路径和RTL里的信号路径不一定完全一致。在Vivado的Tcl Console里执行get_nets和get_cells验证对象是否存在。比如你想给 u_inst 下的某根线加KEEP先执行get_nets -hierarchical {*u_inst*enable*}如果返回为空说明信号名已经被综合器改造成了别的名字需要根据网表去查名字再重新设置属性。第三步检查综合日志有没有warning。比如RAM_STYLE指定block但综合器发现代码不能被推断为BRAM会打印类似“The RAM will be implemented in distributed”的信息原因都会写在警告里。第四步检查XDC是否真的参与了当前阶段。在Vivado里选中XDC文件查看它的USED_IN_SYNTHESIS / USED_IN_SIMULATION属性。如果文件被设置成只参与实现那综合阶段读取不到它自然不生效。第五步检查是否有其他约束覆盖了它。同一个对象被两个XDC设置了相同属性但值不同Vivado默认后读入的会覆盖先读入的。这时候需要理清文件的读取顺序或者把冲突的属性归并到一个文件里。这个排查流程几乎能覆盖九成以上的“属性没生效”问题。剩下的特殊情况往往都是还没理清生效时机造成的。4.4 我踩过的几个真实的坑第一个坑是乱加KEEP导致网表膨胀。当时为了调试我在RTL里给十几个内部信号全加了 (* keep true *)结果综合完发现网表体积比预期大了一半布局布线时间也明显拉长。原因是这些信号原本是会被优化的中间节点KEEP一加所有关联逻辑都失去了简化机会。后来我只保留了真正需要观察的几个信号网表立刻瘦身成功。所以KEEP是按需使用不是多多益善。第二个坑是给整个顶层模块加DONT_TOUCH。那是个已经验证过的IP核我怕综合器动它的结构就加上了DONT_TOUCH。结果模块内部的常量传播彻底失效很多简单逻辑都保留成电阻网时序收敛变得极差折腾了一整天才发现问题。DONT_TOUCH的作用范围尽量缩小到具体实例或关键网络不要图省事直接包住大模块。第三个坑是使用RAM_STYLE强制block但代码里的RAM实际上是一个单端口异步读的复杂模式综合器根本不能推断成BRAM。我盯着warning看了很久才明白属性只能影响推断的倾向不能改变代码的结构。后来把读端口改成同步读BRAM才真正生效。这说明在加属性之前先把代码往标准模板上靠是更高优先级的动作。还要提醒一句很多属性在综合前和综合后设置效果完全不同。比如MAX_FANOUT在综合前设置可以让综合器帮你复制寄存器而在综合后设置更多是指导布局器处理已有寄存器。什么时候用哪种方式取决于你想让工具在哪一步做结构调整想清楚了再写。最后再分享一个我觉得非常值的小技巧在Vivado里做综合功能属性设置时我很喜欢用set_property -dict把同一对象的所有属性一次性写完比如说对某个时钟端口同时设置位置和电平标准。这个习惯能大幅减少Tcl的执行时间也让约束文件更干净。另外综合完以后别急着关掉IDE打开Schematic视图搜索你加了KEEP的信号如果还能在原理图里找到它说明属性确实生效了这比盯着日志看半天都直观。希望这份清单能帮你在综合阶段少走一点弯路。
返回列表