做 FPGA 和 ASIC 的时间长了你会发现真正决定一块板子能不能稳定跑起来的往往不是 RTL 本身写得有多漂亮而是 RTL 里那几行容易被忽略的综合指令。我刚开始用 Verilog 的时候一度以为综合指令就是 C 语言的预处理宏后来被工具优化掉好几个关键信号、被迫重新综合了几次才彻底搞明白综合指令是写给综合工具看的“耳语”它不改变功能却能决定寄存器保不保留、RAM 用 Block RAM 还是分布式 RAM、case 语句生成优先级逻辑还是并行逻辑。这篇东西就是把这些年我用 Vivado、Quartus、Design Compiler 时踩过的坑和沉淀下来的写法一次性倒给你。1. 先搞清楚综合指令到底是什么1.1 从一次“信号被优化掉”的排查说起早期我做过一个串口透传的小模块收发 FIFO 的逻辑都写好了功能仿真一切正常下载到板子上却发现调试信号完全观察不到。我想用逻辑分析仪抓fifo_wr_ptr和fifo_rd_ptr的变化结果网表里这两个信号根本不存在。查综合报告里面写着“signal has been optimized away”我才意识到是综合工具认为这两个中间信号没有扇出直接给优化了。从那以后我开始认真对待综合指令。这类问题几乎每个 RTL 工程师都会碰见尤其是写了一些纯中间变量、只用于仿真或调试的看位信号。综合工具的目标是“在满足约束的前提下把逻辑做小、做快”它不会关心你调试时想不想看到这个信号。如果你不主动告诉它“这个信号留着别删”它会毫不犹豫地帮你去掉。Verilog 综合指令就是我们用来跟工具沟通“哪些东西不准动、哪些资源必须用、哪段代码综合时不用管”的渠道。1.2 综合指令的三类常见写法Verilog 里的综合指令从写法上大致分三类。第一类是属性语法也就是 Verilog-2001 标准里的 attribute典型形式是(* keep true *)放在信号声明、模块声明或例化之前。标准里允许任何工具解析并选择忽略但因为它是语言标准的一部分仿真器通常也会接受只是不会报错。第二种是注释型 pragma典型形式是// synthesis translate_off、/* synopsys full_case */它把指令藏在注释里仿真器完全看不见只有综合工具在解析源码时会去识别这些特殊注释。第三种是宏定义法典型形式是\ifdef SYNTHESIS通过工具内置或工程定义的宏把仿真专用代码和综合专用代码在预处理阶段分离。为什么会有这么多种写法因为 Verilog 标准本身对“综合”的控制能力很弱各厂商只能各显神通。Xilinx 的 Vivado 偏向前属性法Intel Quartus 早期大量使用注释型 pragmaSynopsys Design Compiler 则把// synopsys ...写成了行业惯例。做工程时不能只背一种语法否则换工具就要重写一遍代码。1.3 为什么综合指令不是仿真指令很多人会把综合指令和仿真指令混在一起我自己早期也犯过这种错。仿真指令比如$display、$finish、initial里的#10延时是仿真器用来模拟时序的综合指令则是综合工具用来控制实现过程的两者作用阶段完全不同。仿真器的目标是把逻辑跑出波形让设计者检查行为综合工具的目标是把行为映射成真实的 LUT、触发器、BRAM、DSP还要考虑时序收敛。这就带来一个关键认知在 RTL 里写了initial $display(hello)Questa 能看到输出但综合工具根本不认识$display综合时要么忽略要么报错。反过来(* keep true *)写在信号声明上仿真器大概率什么都不做综合工具却会把信号保留下来。所谓“综合指令不是仿真指令”意思是它们面向的对象完全不同千万别指望一段代码既能在仿真里打印信息又能在综合时控制硬件结构。如果你确实需要两者兼顾就得用\ifdef SYNTHESIS 或者 translate_off/on 把区域隔开这个下面细说。2. 高频综合指令逐条拆解2.1 区域开关translate_off / translate_on如果要给综合指令排个使用频率translate_off和translate_on肯定是第一梯队。它们是成对出现的注释型 pragma告诉综合工具从// synthesis translate_off开始到// synthesis translate_on结束这段代码综合时不用管但仿真时保留。最常见的用途是隔离仿真专用代码。比如你想在仿真文件里加一个时钟初始化reg clk_r 1b0; // synthesis translate_off initial begin clk_r 1b0; #10; forever #10 clk_r ~clk_r; end // synthesis translate_on综合时这段initial不会进网表因为综合工具遇到 translate_off 会直接跳过不像普通仿真代码那样报“只支持 initial 常量化”之类的错。注意不同厂商的关键字有差异Xilinx 老工具和 Vivado 普遍识别// synthesis translate_offSynopsys 系传统是// synopsys translate_offCadence 系则可能是// pragma translate_off。如果你的工程要跨工具可以把这些注释都写上但顺序不能乱尤其不能交叉嵌套。这条指令最大的坑是必须严格配对而且 off 和 on 的放置位置有讲究。我见过有人把 off 放在模块注释中间结果整个模块被综合工具静默跳过最后网表上空空荡荡。所以每次写完 translate_off我第一时间就会确认 on 写在了哪一行最好是紧跟着被隔离代码块的结尾。2.2 防止信号消失keep / preserve / dont_touch / keep_hierarchy我们做在线调试时最常用的就是保留信号类指令。(* keep true *)在 Vivado 里用来防止信号被优化适合加在 wire 或 reg 上(* preserve true *)通常用于保留寄存器避免重定时、复制或合并寄存器(* dont_touch true *)作用更强不仅保留信号还禁止跨层级优化(* keep_hierarchy yes *)一般加在模块例化处防止工具打平层级方便后仿真和调试定位。举个最简单的例子我们想观察 FIFO 指针在综合后的网表里是否存在(* keep true *) wire [3:0] fifo_wr_ptr_w; (* keep true *) reg [3:0] fifo_rd_ptr_r;加了属性之后在 Vivado 里打开综合后网表用get_nets -hierarchical *fifo*就能查到这两个名字。注意 keep 属性对寄存器的保留效果不是绝对的如果寄存器被优化成常量或者被跨层级合并工具大概率还是会把它处理掉这时候就得升级成dont_touch。Quartus 里写法不太一样常见的是(* keep *) wire [3:0] debug_bus; /* synthesis noprune */ reg [3:0] hold_flag;noprune是 Quartus 独有的告诉工具不要裁掉这个寄存器preserve则是保留寄存器防止被合并复制。做 Altera/Intel 工程时这些指令比 Vivado 的 keep 更常用我第一次从 Xilinx 转到 Quartus 时不知道 noprune被优化掉一堆计数器后来才把两个平台的指令对应关系理清楚。2.3 资源映射ram_style / use_dsp48 / shreg_extract除了保留信号综合指令还经常用来干预资源映射。最典型的是 RAM 风格控制。FPGA 里的存储资源五花八门Vivado 里有 Block RAM、分布式 RAM、UltraRAMQuartus 里有 M9K、M10K、MLAB。工具自动推断通常没问题但当你对时序、功耗或面积有明确要求时就得用手指头戳一下方向。Vivado 下的写法(* ram_style block *) reg [7:0] mem_data [0:1023];Quartus 下的写法(* ramstyle M10K *) reg [7:0] mem_data [0:1023];注意属性名一个是ram_style一个是ramstyle一模一样的外表换工具就容易翻车。如果综合工具觉得这个 RAM 太大或太小或者电路结构不符合 RAM 推断条件它会给出 warning然后退回默认行为。所以写完资源属性一定要去综合报告里看 RAM 到底用了几块、什么类型不要想当然。类似的还有 DSP 推断。Vivado 里可以对乘法器或累加器加(* use_dsp48 yes *)Quartus 里则常用(* altera_attribute -name DSP_BALANCING ... *)之类的属性去控制但这块相对复杂。更通用的移位寄存器也可以用(* shreg_extract no *)强制不要做移位寄存器抽取这样可以在更早阶段看到寄存器的完整展开方便调试。2.4 case与状态机full_case / parallel_casefull_case和parallel_case是综合圈里最有争议的指令没有之一。它们最早流行于 Synopsys Design Compiler 时代很多人用它们来压缩状态机和 case 的生成逻辑。full_case的意思是告诉综合工具“case 里的分支已经列全了没列出来的就是 dont care”这样工具不会为没列出的条件生成默认逻辑。parallel_case的意思是“分支之间互斥最多只有一个匹配”这样工具不会生成优先级逻辑。写法通常是case (sel) 2b00: data_o data_a; 2b01: data_o data_b; 2b10: data_o data_c; 2b11: data_o data_d; endcase // synthesis full_case parallel_case问题在于仿真器完全不看这两个注释。仿真器里的 case 永远是“从第一个分支开始匹配匹配上就执行否则继续往下”这在数学上是带优先级的。如果你写了一个重叠分支仿真结果是优先级抢占综合结果却因为 parallel_case 变成了并行逻辑两者就开始分道扬镳了。等到板子上电某个不该出现的分支被触发你查 bug 会查到怀疑人生。现在很多公司的代码规范里明确禁止使用 full_case 和 parallel_case我更推荐用显式的 default 分支和if else来表明意图。只有在极端情况下比如状态机已经明显吃到面积、综合报告显示生成了不必要的优先级链才在评审过的基础上谨慎使用且必须在注释里标清楚为什么加。2.5 常用属性速查表这里把我平时用得比较多的指令整理一下方便大家快速对照。工具版本不同部分属性名会有差异但大方向一致。指令/属性作用Vivado 示例Quartus 示例区域注释跳过综合// synthesis translate_off// synthesis translate_off保留信号防止优化(* keep true *)(* keep *)保留寄存器防合并复制(* preserve true *)/* synthesis preserve */完全保留禁止跨层优化(* dont_touch true *)(* altera_attribute -name DONT_TOUCH ON *)层级保留禁止打平(* keep_hierarchy yes *)(* preserve *)或属性RAM 风格指定 RAM 类型(* ram_style block *)(* ramstyle M10K *)DSP 推断使用 DSP 块(* use_dsp48 yes *)工具属性移位寄存器禁止抽取(* shreg_extract no *)工具属性case 穷举省略默认逻辑// synthesis full_case// synthesis full_casecase 并行省略优先级逻辑// synthesis parallel_case// synthesis parallel_case这张表不是金科玉律但能让你在遇到“信号没了”“RAM 没用上”这类问题时第一反应知道该去哪里查、该用哪个指令。3. 在 Vivado、Quartus、DC 里正确落地的实操步骤3.1 Verilog-2001属性语法和兼容性前面提到属性语法是 Verilog-2001 标准的一部分所以在跨工具场景下我推荐优先使用(* ... *)。比如(* keep true *)、(* dont_touch true *)这些在 Vivado 和 Quartus 里都有较好的支持而且仿真器不会因为见到这类属性就报错。属性语法可以放在多种位置信号声明前、模块端口前、模块例化前、语句块前。位置不同作用对象也不同。放在 wire 声明前作用于这条网络放在 reg 声明前作用于这个寄存器变量放在模块例化前作用于这个实例放在 module 关键字前作用于整个模块。我踩过一个坑把(* keep true *)放在了 assign 语句前想着保护输出网线实际工具没有识别因为这条属性位置不属于标准规定的任何属性对象综合器直接忽略。正确的做法是声明wire keep_me;时就把属性加上或者如果输出是模块输出端口可以加在端口声明里。另外属性值在某些工具里必须加引号比如(* keep true *)在另外一些工具里可以省略引号写成(* keep true *)。为了保险我统一加引号并且属性名小写减少工具之间的解析差异。3.2 工具厂商pragma的写法差异各厂商的 pragma 写法不能通用这是最需要管理的风险点。如果代码要在 Vivado 和 Quartus 之间来回切建议做一层“宏封装”或者直接押注标准属性法不要混用注释型 pragma。Synopsys Design Compiler 里传统写法是// synopsys translate_off、// synopsys full_case、// synopsys parallel_case这些在它的语法手册里叫“synopsys pragma”。Vivado 的一般综合工具也兼容一部分 Synopsys pragma但兼容程度有限尤其是full_case和parallel_caseVivado 会识别并应用但 Quartus 不一定。Intel Quartus 官方推荐的是// synthesis ...系列比如// synthesis translate_off、// synthesis keep、// synthesis noprune、// synthesis preserve。所以当你拿到一份别人写的 RTL第一件事不是急着综合而是先 grep 一下代码里出现了哪些 pragma确认当前工具认不认。我自己的习惯是核心代码尽量只用标准属性法注释型 pragma 只在特定工具的一次性脚本或 IP 生成代码里保留。这样换工具时不会满屏 warning也不会因为某个厂商不兼容导致网表结构变得不可控。3.3 用Tcl和综合报告验证指令生效写指令是一回事验证指令有没有生效是另一回事。我见过太多人加了keep就认为万事大吉结果综合报告里信号还是消失因为属性拼错了、对象选错了或者被优化得太彻底。正确的流程是综合完之后开网表用 Tcl 命令查询目标信号。在 Vivado 里综合后执行get_nets -hierarchical *fifo_wr_ptr* get_cells -hierarchical *fifo_wr_ptr* get_pins -hierarchical *fifo_wr_ptr*如果get_nets能查到说明 net 还在如果只有get_cells能查到说明寄存器还在但网络名被改写了如果三个都查不到那大概率被优化掉了。此时看综合日志WARNING: [Synth 8-3301] register fifo_wr_ptr_r has been optimized away这个警告出现时再去确认 keep 属性是否真的挂到了寄存器上。也可以在 synthesis 完成后用report_utilization看 RAM 类型用report_design_analysis看 DSP 数量。Quartus 里对应的流程是打开 Compilation Report在 Analysis Synthesis 部分找 Warnings搜索 “removed” 或 “preserve”能直接看到哪些信号被移除、哪些寄存器因 preserve 被保留。做 ASIC 时Design Compiler 里可以用report_net、report_registers和check_design来验证 keep_signal、dont_touch 是否生效这个基本功一定要扎实。3.4 与SDC/XDC约束的分工综合指令和约束文件经常被混在一起说但它们的职责边界其实很清楚。综合指令写在 RTL 里面向逻辑结构和资源映射比如“这个寄存器必须保留”“这片 RAM 必须用 block”SDC/XDC 约束文件面向时序、时钟和 IO比如“这个时钟周期 10ns”“这条路径最大延迟 5ns”。两者也会有交集。例如 max_fanoutVivado 既支持 RTL 里写(* max_fanout 16 *)也支持在 XDC 里用set_property MAX_FANOUT 16 [get_nets net_name]。我的经验是如果这个要求属于“设计固有属性”比如某个复位信号必须限制扇出那就写在 RTL 里如果属于“针对某个实现版本的要求”比如为了时序收敛临时调整就放在约束文件里避免反复改 RTL。综合指令还会影响综合工具对时序的估算。比如你用了keep_hierarchy模块边界不会被优化打平路径上多出的层级可能导致时序更难收敛用了dont_touch后某些优化无法跨边界也会变相增加延迟。所以加这类指令时一定要配套做一次时序评估不要只看了面积就收工。4. 我被综合指令坑过的地方典型误用与排查4.1 translate_off没配对综合整个文件错位有一次我在一个工程里做仿真时钟初始化写了 translate_off 之后忘了写 translate_on结果从那个位置开始到文件末尾所有逻辑都被综合工具跳过了。综合没有报错但生成的电平文件里一个触发器都没有下载到板子上自然是全黑。那次排查花了一个多小时最后 grep 注释才发现 off 和 on 的数量不相等。这个坑提醒我两点第一translate_off 和 translate_on 之间只能放仿真专用内容不要把功能逻辑夹进去第二写完 off 立刻写 on再把内容填到中间不要写完一大段再回头补 on。如果真的找不到 on可以用grep -n translate *.v先看配对再结合综合报告里模块是否为空来判断。另外在多文件工程里translate_off 是文件级扫描的不能跨文件。有人试图在一个文件里 off然后在另一个文件里 on这是完全无效的因为综合工具是逐个文件解析的。4.2 keep加错对象关键信号照样没了keep 属性加到信号上确实能防止优化但有个前提这个信号要真的存在合理电路连接。如果你加 keep 的 wire 只是悬浮连接没有任何驱动端工具照样会删如果你加 keep 的寄存器在综合前就被常量传播掉了比如一个寄存器永远等于固定值工具也会认为它没有存在必要。比较典型的场景是我想调试某个中间计算信号但它只被用在一个assign的等号右边而且这个 assign 的结果最后被综合成常量那么这个中间信号就没了。此时更可靠的办法是在信号声明处加(* dont_touch true *)同时在模块例化处也考虑是否需要keep_hierarchy防止工具跨边界把信号吞掉。还有一点要注意Vivado 的(* keep true *)和(* MARK_DEBUG true *)是不同的。MARK_DEBUG 是给 ILA 调试用的会让信号在布局布线后保留并且可以被 debug core 采集keep 只是保留网络或寄存器。如果需要逻辑分析仪观察两者最好一起加。4.3 full_case/parallel_case让仿真相差十万八千里之前接手过一个同事留下的状态机里面写了不少// synthesis full_case parallel_case。功能仿真时状态机表现正常但上板之后时不时跑到诡异状态最后抓波形发现仿真里一个分支的优先级行为在综合后的硬件里变成了并行选择。因为 case 条件存在重叠综合工具按照全并行逻辑生成电路仿真器却按照优先级执行两边从一开始就不是同一个逻辑。吃了几次亏之后我在项目组定了个规矩新代码里禁止使用 full_case 和 parallel_case老代码里的也要逐步清掉。默认分支不写全就用default: next_state IDLE;补上。分支重叠就用if else if显式表达优先级。状态机少一点“聪明”指令多一点直白逻辑后续维护和排错都会省很多事。4.4 资源属性写了但没生效RAM还是LUT写(* ram_style block *)是很多人的习惯但很多情况下工具并不会理你。有一次我想把一块 256x8 的小存储放进 Block RAM深度只有 256Vivado 直接忽略属性最后还是用 LUT 实现了。看报告才发现分布式 RAM 的面积比 BRAM 小很多而且 BRAM 的最小容量用不满工具认为不划算。这类资源属性本质上是“建议”不是“命令”。如果你确实需要强制使用某种资源不能只靠 RTL 属性还要结合综合选项或者在 XDC 里对相关单元做更严格的绑定。更常见的问题是把ram_style写成了ramstyleVivado 不识别Quartus 也不识别工具只能默默忽略。所以我每次写完资源属性都会去综合报告的 memory 部分看看到底是 block 还是 distributed用报告说话而不是用代码说话。4.5 常见问题速查表症状大概率原因处理方式综合后信号找不到无扇出被优化加 keep / dont_touch仿真有信号综合后无逻辑translate_off 把功能代码夹住了检查 off/on 配对状态机行为上板与仿真不一致full_case / parallel_case 滥用改为显式 default 和 if elseRAM 没有按属性实现资源大小不匹配或属性名写错查综合报告 memory 类型修正属性名加了 keep 仍被优化常量传播或跨层级合并升级 dont_touch加 keep_hierarchy用 ILA 抓不到信号忘了加 MARK_DEBUGkeep 和 MARK_DEBUG 一起加综合时间突然变长过多 dont_touch / keep_hierarchy 限制优化逐个验证必要性尽量减少这张表是我平时定位综合问题的第一张地图多数情况信号消失、行为不对都能从里面找到对应的方向。5. 关于综合指令的一些个人体会5.1 综合指令宁可少用也不要用玄学综合指令确实能解决很多问题但它不应该成为“代码写不清楚时的遮羞布”。我见过一些代码到处撒 keep为了保留一个没必要的中间信号导致综合面积膨胀时序更难收敛也见过有人用 full_case 压缩逻辑结果功能仿真和硬件行为分道扬镳。综合指令加得越多工具优化空间越小网表就越臃肿。更可怕的是很多指令一旦写上去就没人敢删因为“删了怕出事”。最后整个 RTL 到处都是dont_touch、keep_hierarchy无法维护。我的做法是每条综合指令必须带注释说明为什么加、什么时候加的、验证过什么没有注释的指令在 code review 时直接打回去。要让综合指令成为设计意图的一部分而不是不可解释的玄学。5.2 让指令可配置、可追溯对于可能跨工具复用的模块我通常会把综合指令抽到宏定义或者参数里。比如保留信号这种需求可以写成一个宏ifdef VIVADO_SYNTH define KEEP_NET(x) (* keep true *) x else define KEEP_NET(x) x endif但宏不是万能药尤其是属性语法宏展开位置必须和属性语法要求完全一致否则会引入奇怪的解析错误。大多数情况下我更倾向于让综合指令直接写在 RTL 里但通过集中的注释和文档管理起来谁加的、为什么加、影响什么一目了然。追溯性方面我习惯在综合跑完后导出一份“综合指令清单”用脚本扫描源码里所有(*、synthesis、synopsys关键字生成表格对比综合报告。这样能快速发现哪些指令没生效哪些指令被工具忽略不会等到板子调不通才回头翻代码。5.3 最后再分享一个检查信号是否保留的小经验很多初学者加完 keep 之后只会看综合日志里有没有 warning但 warning 往往很多压根看不过来。我的做法是综合完成后直接用 Tcl 在 Vivado 里跑一句report_net -all -name only_my_keep或者用get_nets -hierarchical *关键名字*把结果导出成一个文本再和 RTL 里加了 keep 的清单做比对。Quartus 里则可以在 Analysis Synthesis 后打开 Netlist Viewers直接搜信号名。这个方法看起来很笨但确实是最稳的。综合指令这种东西写对不难难的是确认它真的在网表里起作用。我每次给关键信号加保留指令都会老老实实查一遍网表吃过太多“以为有效”的亏。如果你刚开始接触综合指令建议从 keep、translate_off/on 这两个最常用的练手先在一小块代码上验证效果再逐步扩展到 RAM 和 DSP 的资源控制。综合指令不复杂但它和工具版本、代码上下文、资源类型都绑得很紧只有亲手在报告里看到信号保留、RAM 类型切换成功你才算是真正把它用熟了。