1. 写在前面为什么你的信号在综合后“消失”了如果你写过Verilog或VHDL并且跑过综合Synthesis那你大概率遇到过这个让人抓狂的场景仿真Simulation里明明跑得好好的信号波形清晰可见逻辑功能完全正确但一旦丢给综合工具比如Vivado、Quartus、Design Compiler这些信号就像人间蒸发了一样在综合后的网表Netlist里怎么也找不到。你检查RTL代码反复确认代码逻辑清晰信号定义明确可工具就是“自作主张”地把它优化掉了。这不仅仅是新手会遇到的问题很多有经验的工程师在写一些中间状态、调试信号或者特定胶合逻辑时也常常会踩进这个坑。这种现象的根本原因在于仿真和综合是两个截然不同的世界。仿真是基于事件驱动的行为级模拟它的目标是尽可能忠实地执行你写的每一行代码模拟出信号的跳变和传播。而综合则是一个“翻译”和“优化”的过程它的目标是将你的高层次描述RTL转换成由实际门电路与门、或门、触发器组成的网表并且在这个过程中它会尽其所能地优化掉它认为“冗余”或“无用”的逻辑以追求更小的面积Area、更低的功耗Power和更高的性能Performance。综合工具就像一个极其精明且“抠门”的管家它会审视你的代码如果发现某个信号没有驱动任何后续逻辑输出悬空比如你定义了一个wire temp;但只给它赋值却没有任何其他信号或输出端口用到它。逻辑上被恒定为某个值比如一个信号经过一系列逻辑后其值在任何输入条件下都是固定的例如逻辑被优化成了1‘b0或1‘b1。是冗余逻辑的一部分比如两个完全相同的逻辑路径产生同一个信号工具可能会合并它们导致其中一条路径上的中间信号消失。被更高效的电路结构所替代工具可能会用查找表LUT、选择器MUX等更底层的元件来实现你的逻辑导致RTL层面的信号边界在网表中变得模糊。对于调试、观测、或一些特殊的电路结构如手动插入的延迟链、用于形式验证的断言信号来说这种优化往往是“过度”的我们需要告诉工具“这个信号很重要请保留它”。接下来我将结合多年在FPGA和ASIC前端设计中的实战经验系统性地总结那些真正实用、经过验证的防止信号被优化的方法。这些方法各有其适用场景和副作用理解其背后的原理比死记硬背命令更重要。2. 基础防御利用综合属性与编译指令这是最直接、最常用的一类方法通过在代码中嵌入特定的属性Attribute或编译指令Pragma直接与综合工具“对话”指导其行为。这种方法通常与工具链强相关但主流工具都支持类似功能。2.1keep属性最直接的“保留”命令keep属性的含义非常直白“请保留这个网络net或层次hierarchy不要优化掉它”。它在不同工具中的语法略有不同。2.1.1 在Verilog中使用keepXilinx Vivado / ISE:(* keep true *) wire debug_signal; // 或者使用Verilog-2001属性语法 wire debug_signal /* synthesis keep */;这行代码告诉Vivado综合器无论debug_signal看起来多么“无用”都必须将它保留在网表中。你可以在信号声明时添加也可以在模块实例化时添加以保留整个实例。Intel Quartus (Synopsys Synplify Pro也类似):(* preserve *) wire debug_signal; // 或者 wire debug_signal /* synthesis preserve */;Quartus中通常使用preserve其作用与keep等效。syn_keep也是Synplify中常见的属性。2.1.2 在VHDL中使用keepVHDL中使用属性需要先声明属性名有时工具已预定义然后进行属性描述。通用方法工具可能支持:signal debug_signal : std_logic; attribute keep : string; attribute keep of debug_signal : signal is true;Xilinx Vivado专用:signal debug_signal : std_logic; attribute keep : string; attribute keep of debug_signal : signal is true; -- 或者使用Xilinx的宏需包含相关库 -- attribute mark_debug : string; -- attribute mark_debug of debug_signal : signal is true;实战心得与陷阱keep属性非常强力但它有一个关键限制它主要作用于“网络”net而非“寄存器”register。这意味着如果你对一个reg型变量使用keep综合工具可能会保留承载这个寄存器输出的那根线但寄存器本身可能仍然被优化掉例如如果它的输入逻辑被优化导致其功能改变。对于需要保留的寄存器我们通常需要更强的约束。2.2noprune与preserve针对寄存器的守护神当你的目标是保留一个特定的触发器Flip-Flop时keep可能力有不逮。这时就需要noprune不要修剪或针对寄存器的preserve属性。noprune这个属性明确禁止工具移除一个寄存器即使这个寄存器的输出没有连接到任何其他逻辑即输出悬空。这对于插入观测点或调试寄存器至关重要。// Verilog (Quartus/Vivado通常都支持) (* noprune *) reg debug_reg;-- VHDL signal debug_reg : std_logic; attribute noprune : boolean; attribute noprune of debug_reg : signal is true;preserve(对寄存器)在一些工具中对寄存器使用preserve可以达到类似noprune的效果确保寄存器不被优化。// Verilog for Quartus (* preserve *) reg debug_reg;为什么需要区分网络和寄存器这源于综合工具的优化策略。工具优化寄存器逻辑的积极性远高于组合逻辑网络。一个没有负载的纯组合逻辑线被优化掉通常对功能影响不大除了你看不到它了。但一个没有负载的寄存器它仍然会消耗时钟资源和面积工具会认为这是一个明显的浪费从而更积极地尝试优化掉它例如将其与有负载的寄存器合并或者如果其逻辑可推导为常数则直接移除。noprune就是一道明确的“禁止令”。2.3full_case与parallel_case谨慎使用的指令这两个指令用于指导case语句的综合用错了反而会导致信号“意外消失”或功能错误。full_case告诉综合工具这个case语句的所有可能情况都已经在分支中列出不需要生成默认的锁存器Latch来保持未列出情况下的值。如果实际运行时出现了未列出的情况电路行为将是未定义的工具可能将其优化为任意常数导致相关信号消失或固定。// 假设sel是2bit但只列出了3种情况 (* full_case *) // 危险告诉工具已全覆盖实际未覆盖2‘b11 case(sel) 2b00: out a; 2b01: out b; 2b10: out c; // 2b11 情况缺失 endcase正确做法确保代码逻辑上就是full_case或者使用default分支。不要为了消除警告而滥用此指令。parallel_case告诉综合工具各个case分支是互斥的可以并行评估。这有助于生成更快的多路选择器。但如果分支实际上并不互斥例如有重叠的项使用此指令会导致只有一个分支生效其他分支的逻辑被优化掉信号丢失。// 假设a和b可能同时为1 reg [1:0] out; (* parallel_case *) // 危险分支不互斥 case(1‘b1) a: out 2‘b01; b: out 2‘b10; // 如果a和b同时为1此分支可能被优化掉 endcase核心建议除非你百分百确定case语句的逻辑满足full或parallel的条件否则不要使用这两个指令。让综合工具自己推断通常更安全。它们的主要用途是在你明确知道工具推断不理想且你有充分把握时进行性能优化而非防止信号丢失。3. 结构设计法从代码逻辑上杜绝优化通过改变代码的编写方式从逻辑上让综合工具“无法”优化掉目标信号。这种方法不依赖工具属性可移植性更好但可能会引入额外的逻辑开销。3.1 制造“虚假”负载既然工具优化掉信号是因为它没有驱动任何负载那么最直观的想法就是给它加一个负载。但这个负载不能影响原始功能。3.1.1 输出到一个未使用的模块端口这是最干净的方法之一。在你的模块中声明一个额外的输出端口专门用于引出需要观测的内部信号。module my_design ( input clk, input rst_n, input [7:0] data_in, output [7:0] data_out, output reg debug_signal // 新增的调试输出 ); // ... 内部逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin debug_signal 1‘b0; end else begin // debug_signal 是某个内部过程的产物 debug_signal some_internal_condition; end end // 顶层模块中这个端口可以悬空或不接 endmodule综合工具看到debug_signal连接到了模块的输出端口就无法将其优化掉。在顶层你可以选择不连接它作为No Connect或者连接到虚拟的调试模块。3.1.2 使用(* dont_touch *)属性更高级的keepdont_touch比keep更强大。keep是“尽量保留”但工具在极端优化下仍可能对其做转换如缓冲器插入、重命名。dont_touch则是“禁止触碰”要求工具保持该网络或实例的完整性几乎不做任何改变。(* dont_touch true *) wire critical_net; (* dont_touch true *) module_name instance_name (...);这个属性常用于保留关键路径、手动实例化的底层原语如时钟缓冲器、IOBUF或用于形式验证的断言逻辑。注意滥用dont_touch可能会妨碍工具进行必要的合法优化影响最终性能。3.2 阻止常数传播与逻辑折叠如果信号是因为逻辑被优化成常数而消失我们需要打破这种常数传播链。3.2.1 引入冗余或条件逻辑例如你有一个信号internal_flag它理论上在某些条件下应该为高但工具可能推导出它永远为低而优化掉。// 原始可能被优化的代码 reg flag; always (*) begin if (very_complex_condition) begin flag 1‘b1; end else begin flag 1‘b0; // 工具可能证明 very_complex_condition 永远不成立从而将 flag 优化为 0 end end // 修改后引入一个无法被静态分析的“扰动” reg flag; always (*) begin if (very_complex_condition) begin flag 1‘b1 ^ 1‘b0; // 异或0逻辑不变但增加了复杂度 end else begin flag 1‘b0 ^ 1‘b0; end end // 或者更优雅地使用一个来自顶层的、非常量的输入作为“保护” // input debug_en; // 顶层传入可固定为0但工具不知道它运行时是否为0 always (*) begin flag (very_complex_condition) ? 1‘b1 : 1‘b0; if (!debug_en) begin // 这个条件逻辑阻止了flag被直接优化为常数 flag flag; // 看似冗余但打断了优化链 end end3.2.2 使用(* equivalent_register_removal off *)这个属性专门用于禁止工具合并功能等效的寄存器。当你有多个寄存器其输入逻辑经过优化后被判定为相同工具会合并它们以节省面积。如果你需要分别观测它们就需要关闭这个优化。(* equivalent_register_removal off *) reg [3:0] state_reg;这个属性在需要精确控制寄存器数量的场景如某些低功耗状态机设计中非常有用。4. 工具链协同在约束与流程中锁定信号除了修改RTL代码我们还可以在综合工具的使用流程和约束文件中进行操作。4.1 使用SDC或XDC约束文件在ASIC设计或高级FPGA流程中使用Synopsys Design Constraints (SDC) 或 Xilinx Design Constraints (XDC) 文件是标准做法。里面也可以包含保留信号的命令。在Vivado的XDC中:# 防止优化特定的网络 set_property DONT_TOUCH true [get_nets {design_hierarchy/debug_signal}] # 防止优化特定的单元寄存器、LUT等 set_property DONT_TOUCH true [get_cells {design_hierarchy/instance_name/debug_reg}] # 标记用于调试的信号 set_property MARK_DEBUG true [get_nets {design_hierarchy/debug_signal}]MARK_DEBUG是Vivado中一个非常强大的属性。它不仅会保留信号还会在布局布线后将这些信号连接到FPGA的调试核心如ILA方便通过ChipScope或Vivado Logic Analyzer进行抓取。在Quartus的QSF中:# 保留信号 set_instance_assignment -name PRESERVE_REGISTER ON -to debug_reg set_instance_assignment -name PRESERVE_FANOUT_FREE_NODE ON -to debug_net工作流建议将调试信号的约束单独放在一个约束文件如debug.xdc中。在项目早期这个文件可以为空或包含少量信号。随着调试深入逐步添加需要观测的信号。在最终生成产品比特流时可以轻松地排除或注释掉这个文件避免调试逻辑占用额外资源。4.2 分步综合与增量综合对于大型设计一次性综合所有模块调试信号可能被深埋在层次化结构中。可以采用分步策略黑盒化Black Boxing将不需要修改或已验证的模块设置为黑盒。综合工具只看到其接口不优化其内部自然也保留了其输出信号。在Vivado中可以在RTL属性或XDC中设置DONT_TOUCH为true于模块实例或者使用set_property BLACKBOX true [get_files submodule.v]。增量综合Incremental Synthesis只对修改过的模块及其上级模块进行重新综合未修改模块使用之前综合好的网表。这可以保留之前已设置好的调试信号和约束。Vivado和Quartus都支持增量编译/综合流程。层次化保持Keep Hierarchy禁止工具打平Flatten设计层次。保持层次结构使得在网表中定位和保留特定模块内的信号变得更加容易。// 在模块声明时添加属性 (* keep_hierarchy yes *) module my_submodule (...);或者在约束文件中设置# Vivado XDC set_property KEEP_HIERARCHY true [get_cells design_hierarchy/my_submodule_inst]5. 高级场景与疑难杂症处理5.1 保留用于形式验证的断言与覆盖点形式验证工具如JasperGold、VC Formal会依赖RTL中的断言assert和覆盖点cover进行验证。但这些语句通常被综合工具视为无负载的代码而优化掉。使用(* assert “true” *)或(* coverage “true” *)一些综合工具支持这些属性来保留验证语句。使用宏定义包裹最通用的方法是使用ifdef在综合时排除这些语句。ifdef FORMAL_VERIFICATION assert property ((posedge clk) disable iff (!rst_n) (req |- ##[1:2] gnt)) else $error(Grant not asserted in time!); cover property ((posedge clk) (state IDLE)); endif在运行综合时不定义FORMAL_VERIFICATION宏在运行形式验证时定义该宏。这样既保证了综合的干净又保留了验证逻辑。5.2 防止跨时钟域CDC同步器被优化跨时钟域同步器如两级触发器是CDC处理的关键。工具可能会将两个触发器识别为冗余如果它们之间没有组合逻辑并尝试合并它们这将彻底破坏同步功能。使用(* ASYNC_REG “TRUE” *)这是Xilinx推荐的用于标记异步寄存器对的属性。它告诉工具这两个寄存器是用于CDC同步的工具不仅会保留它们还会将它们放置在同一个SLICE中尽可能靠近的位置以降低亚稳态概率并避免被优化。(* ASYNC_REG TRUE *) reg sync_stage0, sync_stage1; always (posedge clk_b) begin sync_stage0 async_signal; sync_stage1 sync_stage0; end使用set_false_path约束除了保留寄存器还需要告诉时序分析工具不要检查这两个寄存器之间的路径因为亚稳态的存在使得常规时序分析没有意义。这通常在XDC/SDC中完成。set_false_path -from [get_cells sync_stage0] -to [get_cells sync_stage1]5.3 调试信号本身被优化了怎么办有时候你明明已经对目标信号internal_sig使用了keep但在网表查看器中还是找不到。这可能是因为internal_sig的**驱动源Driver**被优化了。例如wire internal_sig some_condition another_condition; (* keep true *) wire kept_signal internal_sig; // 保留了kept_signal如果some_condition和another_condition被优化为常数导致internal_sig恒为0那么kept_signal虽然被保留但它只是一根接地的线GND。你需要回溯对产生internal_sig的组合逻辑的输入施加约束或者使用dont_touch保护整个逻辑锥Logic Cone。5.4 综合后仿真Post-Synthesis Simulation的信号匹配进行综合后仿真是验证综合结果是否正确的重要手段。但综合后网表中的信号名可能已经改变被重命名、优化。为了在仿真波形中方便地找到对应的信号可以使用keep/preserve这是最有效的方法能最大程度保持信号名不变。查看综合报告工具会生成报告列出被移除的寄存器、被合并的逻辑等。仔细阅读报告可以知道哪些信号被处理了。在网表中查找使用工具的网表查看器搜索关键字符。工具重命名通常有规律可循比如在原名后加_reg、_dup等。6. 方法总结与选型指南面对“信号被优化”这个问题不要盲目尝试所有方法。根据你的场景按以下流程选择最合适的方法明确目标调试观测需要将内部信号引出到顶层或调试工具如ILA。首选方法是**MARK_DEBUG属性Vivado或输出到未使用的端口**。keep/preserve是基础保障。保留关键寄存器如状态机状态、计数器、CDC同步器。首选**noprune或ASYNC_REG**。保留完整逻辑链如手动插入的延迟单元、特定的门级电路。首选**dont_touch**。形式验证使用**ifdef宏**隔离断言和覆盖点。保持设计层次使用**keep_hierarchy**。评估影响keep/preserve影响较小主要用于保留网络或防止寄存器合并。dont_touch影响较大会阻止工具对该对象进行任何优化可能影响时序和面积谨慎使用。添加冗余逻辑/端口会引入额外的资源和布线复杂度。操作位置RTL代码内使用属性(* *)或/* synthesis */。优点是直接、与代码共存。缺点是可能降低代码可读性且与工具链绑定。约束文件使用SDC/XDC/QSF命令。优点是将物理约束与功能代码分离便于管理。缺点是需要维护额外的文件。一个通用的最佳实践是在项目初期就规划好调试方案。为关键的内部信号、状态寄存器、跨模块接口预留调试输出端口或者在顶层设计一个调试总线Debug Bus来收集这些信号。在RTL代码中对于确定需要保留的信号及时添加keep或preserve属性。对于CDC路径严格使用ASYNC_REG。将所有的调试约束集中管理。这样当问题出现时你能快速定位和观测而不是在问题发生后手忙脚乱地回溯代码、添加属性还可能因为工具优化策略的复杂性而陷入新的困惑。最后记住综合工具的优化是“好心办坏事”。我们的目标不是禁止所有优化而是精准地告诉工具“这里请保持原样”。理解工具的行为并学会与之有效沟通是数字电路设计工程师的一项核心技能。
数字电路设计:防止综合工具优化关键信号的实用方法
1. 写在前面为什么你的信号在综合后“消失”了如果你写过Verilog或VHDL并且跑过综合Synthesis那你大概率遇到过这个让人抓狂的场景仿真Simulation里明明跑得好好的信号波形清晰可见逻辑功能完全正确但一旦丢给综合工具比如Vivado、Quartus、Design Compiler这些信号就像人间蒸发了一样在综合后的网表Netlist里怎么也找不到。你检查RTL代码反复确认代码逻辑清晰信号定义明确可工具就是“自作主张”地把它优化掉了。这不仅仅是新手会遇到的问题很多有经验的工程师在写一些中间状态、调试信号或者特定胶合逻辑时也常常会踩进这个坑。这种现象的根本原因在于仿真和综合是两个截然不同的世界。仿真是基于事件驱动的行为级模拟它的目标是尽可能忠实地执行你写的每一行代码模拟出信号的跳变和传播。而综合则是一个“翻译”和“优化”的过程它的目标是将你的高层次描述RTL转换成由实际门电路与门、或门、触发器组成的网表并且在这个过程中它会尽其所能地优化掉它认为“冗余”或“无用”的逻辑以追求更小的面积Area、更低的功耗Power和更高的性能Performance。综合工具就像一个极其精明且“抠门”的管家它会审视你的代码如果发现某个信号没有驱动任何后续逻辑输出悬空比如你定义了一个wire temp;但只给它赋值却没有任何其他信号或输出端口用到它。逻辑上被恒定为某个值比如一个信号经过一系列逻辑后其值在任何输入条件下都是固定的例如逻辑被优化成了1‘b0或1‘b1。是冗余逻辑的一部分比如两个完全相同的逻辑路径产生同一个信号工具可能会合并它们导致其中一条路径上的中间信号消失。被更高效的电路结构所替代工具可能会用查找表LUT、选择器MUX等更底层的元件来实现你的逻辑导致RTL层面的信号边界在网表中变得模糊。对于调试、观测、或一些特殊的电路结构如手动插入的延迟链、用于形式验证的断言信号来说这种优化往往是“过度”的我们需要告诉工具“这个信号很重要请保留它”。接下来我将结合多年在FPGA和ASIC前端设计中的实战经验系统性地总结那些真正实用、经过验证的防止信号被优化的方法。这些方法各有其适用场景和副作用理解其背后的原理比死记硬背命令更重要。2. 基础防御利用综合属性与编译指令这是最直接、最常用的一类方法通过在代码中嵌入特定的属性Attribute或编译指令Pragma直接与综合工具“对话”指导其行为。这种方法通常与工具链强相关但主流工具都支持类似功能。2.1keep属性最直接的“保留”命令keep属性的含义非常直白“请保留这个网络net或层次hierarchy不要优化掉它”。它在不同工具中的语法略有不同。2.1.1 在Verilog中使用keepXilinx Vivado / ISE:(* keep true *) wire debug_signal; // 或者使用Verilog-2001属性语法 wire debug_signal /* synthesis keep */;这行代码告诉Vivado综合器无论debug_signal看起来多么“无用”都必须将它保留在网表中。你可以在信号声明时添加也可以在模块实例化时添加以保留整个实例。Intel Quartus (Synopsys Synplify Pro也类似):(* preserve *) wire debug_signal; // 或者 wire debug_signal /* synthesis preserve */;Quartus中通常使用preserve其作用与keep等效。syn_keep也是Synplify中常见的属性。2.1.2 在VHDL中使用keepVHDL中使用属性需要先声明属性名有时工具已预定义然后进行属性描述。通用方法工具可能支持:signal debug_signal : std_logic; attribute keep : string; attribute keep of debug_signal : signal is true;Xilinx Vivado专用:signal debug_signal : std_logic; attribute keep : string; attribute keep of debug_signal : signal is true; -- 或者使用Xilinx的宏需包含相关库 -- attribute mark_debug : string; -- attribute mark_debug of debug_signal : signal is true;实战心得与陷阱keep属性非常强力但它有一个关键限制它主要作用于“网络”net而非“寄存器”register。这意味着如果你对一个reg型变量使用keep综合工具可能会保留承载这个寄存器输出的那根线但寄存器本身可能仍然被优化掉例如如果它的输入逻辑被优化导致其功能改变。对于需要保留的寄存器我们通常需要更强的约束。2.2noprune与preserve针对寄存器的守护神当你的目标是保留一个特定的触发器Flip-Flop时keep可能力有不逮。这时就需要noprune不要修剪或针对寄存器的preserve属性。noprune这个属性明确禁止工具移除一个寄存器即使这个寄存器的输出没有连接到任何其他逻辑即输出悬空。这对于插入观测点或调试寄存器至关重要。// Verilog (Quartus/Vivado通常都支持) (* noprune *) reg debug_reg;-- VHDL signal debug_reg : std_logic; attribute noprune : boolean; attribute noprune of debug_reg : signal is true;preserve(对寄存器)在一些工具中对寄存器使用preserve可以达到类似noprune的效果确保寄存器不被优化。// Verilog for Quartus (* preserve *) reg debug_reg;为什么需要区分网络和寄存器这源于综合工具的优化策略。工具优化寄存器逻辑的积极性远高于组合逻辑网络。一个没有负载的纯组合逻辑线被优化掉通常对功能影响不大除了你看不到它了。但一个没有负载的寄存器它仍然会消耗时钟资源和面积工具会认为这是一个明显的浪费从而更积极地尝试优化掉它例如将其与有负载的寄存器合并或者如果其逻辑可推导为常数则直接移除。noprune就是一道明确的“禁止令”。2.3full_case与parallel_case谨慎使用的指令这两个指令用于指导case语句的综合用错了反而会导致信号“意外消失”或功能错误。full_case告诉综合工具这个case语句的所有可能情况都已经在分支中列出不需要生成默认的锁存器Latch来保持未列出情况下的值。如果实际运行时出现了未列出的情况电路行为将是未定义的工具可能将其优化为任意常数导致相关信号消失或固定。// 假设sel是2bit但只列出了3种情况 (* full_case *) // 危险告诉工具已全覆盖实际未覆盖2‘b11 case(sel) 2b00: out a; 2b01: out b; 2b10: out c; // 2b11 情况缺失 endcase正确做法确保代码逻辑上就是full_case或者使用default分支。不要为了消除警告而滥用此指令。parallel_case告诉综合工具各个case分支是互斥的可以并行评估。这有助于生成更快的多路选择器。但如果分支实际上并不互斥例如有重叠的项使用此指令会导致只有一个分支生效其他分支的逻辑被优化掉信号丢失。// 假设a和b可能同时为1 reg [1:0] out; (* parallel_case *) // 危险分支不互斥 case(1‘b1) a: out 2‘b01; b: out 2‘b10; // 如果a和b同时为1此分支可能被优化掉 endcase核心建议除非你百分百确定case语句的逻辑满足full或parallel的条件否则不要使用这两个指令。让综合工具自己推断通常更安全。它们的主要用途是在你明确知道工具推断不理想且你有充分把握时进行性能优化而非防止信号丢失。3. 结构设计法从代码逻辑上杜绝优化通过改变代码的编写方式从逻辑上让综合工具“无法”优化掉目标信号。这种方法不依赖工具属性可移植性更好但可能会引入额外的逻辑开销。3.1 制造“虚假”负载既然工具优化掉信号是因为它没有驱动任何负载那么最直观的想法就是给它加一个负载。但这个负载不能影响原始功能。3.1.1 输出到一个未使用的模块端口这是最干净的方法之一。在你的模块中声明一个额外的输出端口专门用于引出需要观测的内部信号。module my_design ( input clk, input rst_n, input [7:0] data_in, output [7:0] data_out, output reg debug_signal // 新增的调试输出 ); // ... 内部逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin debug_signal 1‘b0; end else begin // debug_signal 是某个内部过程的产物 debug_signal some_internal_condition; end end // 顶层模块中这个端口可以悬空或不接 endmodule综合工具看到debug_signal连接到了模块的输出端口就无法将其优化掉。在顶层你可以选择不连接它作为No Connect或者连接到虚拟的调试模块。3.1.2 使用(* dont_touch *)属性更高级的keepdont_touch比keep更强大。keep是“尽量保留”但工具在极端优化下仍可能对其做转换如缓冲器插入、重命名。dont_touch则是“禁止触碰”要求工具保持该网络或实例的完整性几乎不做任何改变。(* dont_touch true *) wire critical_net; (* dont_touch true *) module_name instance_name (...);这个属性常用于保留关键路径、手动实例化的底层原语如时钟缓冲器、IOBUF或用于形式验证的断言逻辑。注意滥用dont_touch可能会妨碍工具进行必要的合法优化影响最终性能。3.2 阻止常数传播与逻辑折叠如果信号是因为逻辑被优化成常数而消失我们需要打破这种常数传播链。3.2.1 引入冗余或条件逻辑例如你有一个信号internal_flag它理论上在某些条件下应该为高但工具可能推导出它永远为低而优化掉。// 原始可能被优化的代码 reg flag; always (*) begin if (very_complex_condition) begin flag 1‘b1; end else begin flag 1‘b0; // 工具可能证明 very_complex_condition 永远不成立从而将 flag 优化为 0 end end // 修改后引入一个无法被静态分析的“扰动” reg flag; always (*) begin if (very_complex_condition) begin flag 1‘b1 ^ 1‘b0; // 异或0逻辑不变但增加了复杂度 end else begin flag 1‘b0 ^ 1‘b0; end end // 或者更优雅地使用一个来自顶层的、非常量的输入作为“保护” // input debug_en; // 顶层传入可固定为0但工具不知道它运行时是否为0 always (*) begin flag (very_complex_condition) ? 1‘b1 : 1‘b0; if (!debug_en) begin // 这个条件逻辑阻止了flag被直接优化为常数 flag flag; // 看似冗余但打断了优化链 end end3.2.2 使用(* equivalent_register_removal off *)这个属性专门用于禁止工具合并功能等效的寄存器。当你有多个寄存器其输入逻辑经过优化后被判定为相同工具会合并它们以节省面积。如果你需要分别观测它们就需要关闭这个优化。(* equivalent_register_removal off *) reg [3:0] state_reg;这个属性在需要精确控制寄存器数量的场景如某些低功耗状态机设计中非常有用。4. 工具链协同在约束与流程中锁定信号除了修改RTL代码我们还可以在综合工具的使用流程和约束文件中进行操作。4.1 使用SDC或XDC约束文件在ASIC设计或高级FPGA流程中使用Synopsys Design Constraints (SDC) 或 Xilinx Design Constraints (XDC) 文件是标准做法。里面也可以包含保留信号的命令。在Vivado的XDC中:# 防止优化特定的网络 set_property DONT_TOUCH true [get_nets {design_hierarchy/debug_signal}] # 防止优化特定的单元寄存器、LUT等 set_property DONT_TOUCH true [get_cells {design_hierarchy/instance_name/debug_reg}] # 标记用于调试的信号 set_property MARK_DEBUG true [get_nets {design_hierarchy/debug_signal}]MARK_DEBUG是Vivado中一个非常强大的属性。它不仅会保留信号还会在布局布线后将这些信号连接到FPGA的调试核心如ILA方便通过ChipScope或Vivado Logic Analyzer进行抓取。在Quartus的QSF中:# 保留信号 set_instance_assignment -name PRESERVE_REGISTER ON -to debug_reg set_instance_assignment -name PRESERVE_FANOUT_FREE_NODE ON -to debug_net工作流建议将调试信号的约束单独放在一个约束文件如debug.xdc中。在项目早期这个文件可以为空或包含少量信号。随着调试深入逐步添加需要观测的信号。在最终生成产品比特流时可以轻松地排除或注释掉这个文件避免调试逻辑占用额外资源。4.2 分步综合与增量综合对于大型设计一次性综合所有模块调试信号可能被深埋在层次化结构中。可以采用分步策略黑盒化Black Boxing将不需要修改或已验证的模块设置为黑盒。综合工具只看到其接口不优化其内部自然也保留了其输出信号。在Vivado中可以在RTL属性或XDC中设置DONT_TOUCH为true于模块实例或者使用set_property BLACKBOX true [get_files submodule.v]。增量综合Incremental Synthesis只对修改过的模块及其上级模块进行重新综合未修改模块使用之前综合好的网表。这可以保留之前已设置好的调试信号和约束。Vivado和Quartus都支持增量编译/综合流程。层次化保持Keep Hierarchy禁止工具打平Flatten设计层次。保持层次结构使得在网表中定位和保留特定模块内的信号变得更加容易。// 在模块声明时添加属性 (* keep_hierarchy yes *) module my_submodule (...);或者在约束文件中设置# Vivado XDC set_property KEEP_HIERARCHY true [get_cells design_hierarchy/my_submodule_inst]5. 高级场景与疑难杂症处理5.1 保留用于形式验证的断言与覆盖点形式验证工具如JasperGold、VC Formal会依赖RTL中的断言assert和覆盖点cover进行验证。但这些语句通常被综合工具视为无负载的代码而优化掉。使用(* assert “true” *)或(* coverage “true” *)一些综合工具支持这些属性来保留验证语句。使用宏定义包裹最通用的方法是使用ifdef在综合时排除这些语句。ifdef FORMAL_VERIFICATION assert property ((posedge clk) disable iff (!rst_n) (req |- ##[1:2] gnt)) else $error(Grant not asserted in time!); cover property ((posedge clk) (state IDLE)); endif在运行综合时不定义FORMAL_VERIFICATION宏在运行形式验证时定义该宏。这样既保证了综合的干净又保留了验证逻辑。5.2 防止跨时钟域CDC同步器被优化跨时钟域同步器如两级触发器是CDC处理的关键。工具可能会将两个触发器识别为冗余如果它们之间没有组合逻辑并尝试合并它们这将彻底破坏同步功能。使用(* ASYNC_REG “TRUE” *)这是Xilinx推荐的用于标记异步寄存器对的属性。它告诉工具这两个寄存器是用于CDC同步的工具不仅会保留它们还会将它们放置在同一个SLICE中尽可能靠近的位置以降低亚稳态概率并避免被优化。(* ASYNC_REG TRUE *) reg sync_stage0, sync_stage1; always (posedge clk_b) begin sync_stage0 async_signal; sync_stage1 sync_stage0; end使用set_false_path约束除了保留寄存器还需要告诉时序分析工具不要检查这两个寄存器之间的路径因为亚稳态的存在使得常规时序分析没有意义。这通常在XDC/SDC中完成。set_false_path -from [get_cells sync_stage0] -to [get_cells sync_stage1]5.3 调试信号本身被优化了怎么办有时候你明明已经对目标信号internal_sig使用了keep但在网表查看器中还是找不到。这可能是因为internal_sig的**驱动源Driver**被优化了。例如wire internal_sig some_condition another_condition; (* keep true *) wire kept_signal internal_sig; // 保留了kept_signal如果some_condition和another_condition被优化为常数导致internal_sig恒为0那么kept_signal虽然被保留但它只是一根接地的线GND。你需要回溯对产生internal_sig的组合逻辑的输入施加约束或者使用dont_touch保护整个逻辑锥Logic Cone。5.4 综合后仿真Post-Synthesis Simulation的信号匹配进行综合后仿真是验证综合结果是否正确的重要手段。但综合后网表中的信号名可能已经改变被重命名、优化。为了在仿真波形中方便地找到对应的信号可以使用keep/preserve这是最有效的方法能最大程度保持信号名不变。查看综合报告工具会生成报告列出被移除的寄存器、被合并的逻辑等。仔细阅读报告可以知道哪些信号被处理了。在网表中查找使用工具的网表查看器搜索关键字符。工具重命名通常有规律可循比如在原名后加_reg、_dup等。6. 方法总结与选型指南面对“信号被优化”这个问题不要盲目尝试所有方法。根据你的场景按以下流程选择最合适的方法明确目标调试观测需要将内部信号引出到顶层或调试工具如ILA。首选方法是**MARK_DEBUG属性Vivado或输出到未使用的端口**。keep/preserve是基础保障。保留关键寄存器如状态机状态、计数器、CDC同步器。首选**noprune或ASYNC_REG**。保留完整逻辑链如手动插入的延迟单元、特定的门级电路。首选**dont_touch**。形式验证使用**ifdef宏**隔离断言和覆盖点。保持设计层次使用**keep_hierarchy**。评估影响keep/preserve影响较小主要用于保留网络或防止寄存器合并。dont_touch影响较大会阻止工具对该对象进行任何优化可能影响时序和面积谨慎使用。添加冗余逻辑/端口会引入额外的资源和布线复杂度。操作位置RTL代码内使用属性(* *)或/* synthesis */。优点是直接、与代码共存。缺点是可能降低代码可读性且与工具链绑定。约束文件使用SDC/XDC/QSF命令。优点是将物理约束与功能代码分离便于管理。缺点是需要维护额外的文件。一个通用的最佳实践是在项目初期就规划好调试方案。为关键的内部信号、状态寄存器、跨模块接口预留调试输出端口或者在顶层设计一个调试总线Debug Bus来收集这些信号。在RTL代码中对于确定需要保留的信号及时添加keep或preserve属性。对于CDC路径严格使用ASYNC_REG。将所有的调试约束集中管理。这样当问题出现时你能快速定位和观测而不是在问题发生后手忙脚乱地回溯代码、添加属性还可能因为工具优化策略的复杂性而陷入新的困惑。最后记住综合工具的优化是“好心办坏事”。我们的目标不是禁止所有优化而是精准地告诉工具“这里请保持原样”。理解工具的行为并学会与之有效沟通是数字电路设计工程师的一项核心技能。