ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Verilog综合指令与属性详解:从仿真到可综合代码的工程实践

Verilog综合指令与属性详解:从仿真到可综合代码的工程实践 写Verilog也有几年了第无数次在群里看到有人问类似的问题“为什么我timescale 1ns/1ps写在模块里综合就报错”“我用#10延时写的计数器上板怎么完全不工作”“仿真好好的综合之后功能就变了是不是我综合指令写错了”这些问题的根源其实都指向同一个地方——很多同学分不清仿真代码和可综合代码更没搞懂Verilog里那些“综合指令”到底是给谁看的、怎么用才是对的。我打算把这几年积累的关于Verilog综合指令的经验完整梳理一遍。本文会覆盖最常用的编译指令、综合属性、宏定义在真实项目中的用法以及各类工具Vivado、Quartus、Synopsys DC等对综合指令的兼容性差异最后再讲几个我踩过的比较深的坑。无论你现在在写UART、I2C、FIFO、状态机还是滑动窗口滤波这篇文章涉及的场景应该都能覆盖到。1. 别把仿真当综合理解综合指令前必须先搞清的两件事1.1 仿真和综合是两套完全不同的“运行逻辑”很多初学者在写Verilog时脑子里其实只有一个模型“我写的代码会被执行”。这个理解在仿真阶段勉强说得通因为你确实是在用initial块、#10延时、$display这些东西驱动模拟器去“跑”一段行为。但是综合根本不是“执行”的概念。综合的本质是把你写的行为级描述映射到目标FPGA芯片上的查找表LUT、触发器FF、块内存BRAM、DSP运算单元等实际物理资源上然后布线形成电路。打个比方仿真像是在写剧本然后排练你怎么描述、加多少内心独白都可以综合像是把剧本直接变成表演现场那些“内心独白”和舞台指示没法变成演员的实体动作——你必须把它变成真正的台词和动作。这就是为什么initial块、#10延时、$display这些在仿真里面非常常用的语法到了综合阶段会被工具忽略甚至直接报错。它们不是给电路看的是给仿真器看的。1.2 综合指令在这个流程里扮演什么角色那么综合指令是干嘛的它其实就是你写给综合工具看的“批注”和“配置项”用来告诉编译器这段代码在综合时怎么处理某个模块或者信号要保持还是可以被优化掉一个case语句是完整的还是不完整的某些仿真专用的代码块在综合时直接跳过某些全局参数在综合时采用哪个值。举个例子translate_off和translate_on是一对注释式的综合指令它们之间包裹的代码在仿真时会被执行但在综合时会被完全忽略。这是最常见的综合指令之一也是很多工程里仿真模型和实际电路共存的基石。所以当你了解了综合指令的定位以后再看到那些奇怪的/* synthesis */注释或者ifdef宏包裹的代码块就不会一头雾水了。它们是“工程管理”和“工具引导”的手段不是可综合逻辑本身。2. 最常用的Verilog综合指令逐条拆解2.1 编译器指令宏定义与条件编译先说反引号开头的编译器指令。这类指令由预处理器处理控制的都是编译之前的行为。指令作用典型场景define定义宏类似C语言的#define定义常量、参数开关ifdef条件编译判断宏是否已定义区分仿真/综合代码段、多版本配置include包含文件类似C语言的#include统一管理头文件、共享参数定义timescale设置时间单位和时间精度仿真时使用综合时会体现延迟信息default_nettype设置未声明网络的默认类型建议设成wire或none便于查错先说ifdef。这是我在多字节收发和UART工程里用得最多的指令。比如一个串口模块支持1位停止位和2位停止位两种配置用ifdef可以把两份逻辑放在同一份代码里通过综合工具里的宏定义开关选择ifdef TWO_STOP parameter IDLE_TO_START 4d15; else parameter IDLE_TO_START 4d14; endif在Vivado里你可以在Settings - Verilog Options 里加宏定义在Quartus里则是Assignments - Settings - Verilog HDL Input 里设置。做仿真时也用同样的宏定义来控制Testbench保证仿真和综合用的是同一套条件分支。timescale是另一个很关键的指令。它的格式是timescale时间单位 / 时间精度比如timescale 1ns / 1ps。我的建议是每个模块文件开头都显式写上timescale而不是依赖全局设置。如果全工程不统一不同文件的时间精度互相干扰特别容易在静态时序分析和门级仿真中出现奇怪的偏差。综合指令这块虽然timescale对综合结果本身影响不是决定性的但它影响综合后仿真模型的时间参数该写还是得写。2.2 注释式综合指令告诉综合工具“这段怎么处理”接下来是注释式综合指令。Verilog标准里有些工具指令是以注释的形式存在的最常见的三个指令格式作用translate_off/* synthesis translate_off */在综合时跳过这段代码translate_on/* synthesis translate_on */重新开始综合这段代码synthesis full_case/* synthesis full_case */声明case语句是完整的synthesis parallel_case/* synthesis parallel_case */声明case分支是互斥的在Xilinx工具链中也支持pragma风格的指令例如pragma translate_off pragma translate_onVivado同时支持注释式和pragma式两种写法Altera Quartus也类似。但Synopsys Design CompilerDC只认注释式写法所以做ASIC流程的同学尽量统一用/* synthesis ... */注释式这样换工具链时不需要大改代码。这些指令的作用在综合工具里是实打实的。比如translate_off之间的代码你写什么工具都会忽略——$display、initial、#10延时、仿真用的存储器初始化模型都可以包在里面。而full_case和parallel_case影响的是综合工具对case语句推断出的硬件结构。2.3 关于full_case和parallel_case我的观点是有条件才用full_case告诉综合工具这个case语句的所有可能分支都已经列出了不需要额外的默认逻辑。parallel_case告诉综合工具这些分支条件互斥不会有两个分支同时命中因此可以把优先级逻辑简化为并行多路选择器。但是这里有一个非常关键的坑如果代码本身确实写全了所有分支综合工具自己会识别完全不需要full_case。如果没写全你加了full_case相当于让综合工具在缺少分支条件时生成一个锁存器还是不确定值的信号之间做选择——很多情况下它生成的电路行为会和你仿真的行为不一致。parallel_case也是类似。如果你的case分支实际上有重叠比如case ({a, b}) 2b1?: x 1; 2b?1: x 2; default: x 0; endcase当a1, b1时两个分支条件都为真仿真时由于优先级会命中第一个分支。但你加了parallel_case综合时就认为它们互斥生成并行选择逻辑结果硬件行为和仿真对不上。这种问题特别隐蔽门级仿真时才会暴露排查起来极其痛苦。所以我的原则是依赖代码本身的完整性少依赖综合指令去修正代码问题。代码写全了工具自然能推断出正确结果。3. 综合属性的进阶玩法从寄存器保留到存储推断3.1 综合属性是“贴在代码上的说明标签”如果说综合指令是给整个编译过程下命令那么综合属性就是贴在某个信号、模块或实例上的一张说明标签。它们通常以(* ... *)的形式出现在声明前语法类似(* keep true *) reg [7:0] data_buffer; (* dont_touch true *) my_module u_my_module ( .clk(clk), .din(din), .dout(dout) );综合属性最大的价值在于它能在不改变逻辑功能的前提下微妙而强力地影响综合工具的优化决策。很多情况下你不需要去改代码逻辑只需要加一行属性综合结果就完全不同。3.2 常用综合属性一览keep、dont_touch、ramstyle、fsm_encoding在实际项目中这些属性是我最常用的(* keep true *)保留信号这个属性的意思是告诉综合工具这个信号不要被优化掉。在调试时特别有用比如你想在ILA集成逻辑分析仪里观测一个中间信号但它因为逻辑太简单被综合工具优化合并了这时候加keep就能保住信号不被抹除。我在用Vivado调试滑动窗口滤波算法时经常遇到这种情况明明代码里写了中间寄存器综合后却发现UCF或XDC里找不到这个信号就是因为综合工具把它优化成一根线了。加(* keep true *)后信号就能保留下来方便抓波形分析。(* dont_touch true *)禁止优化dont_touch和keep类似但作用范围更广适用于模块实例或更底层的单元。它的意思是这部分电路保持原样不做任何优化、不合并、不重组。ASIC流程中dont_touch常用于某些需要固定位置的模拟IP、PLL、特殊IO接口模块避免DC优化把它们重排掉。FPGA流程里也常用来保护跨时钟域的同步寄存器防止综合工具把两级同步器优化成一级。(* ramstyle block *)或(* ram_style auto | block | distributed *)存储推断控制这是FPGA项目里的高频属性。你在写FIFO或寄存器组时综合工具会自动推断用BRAM还是分布式RAM。默认auto模式下工具通常基于容量和逻辑使用率做选择但你不一定满意。例如在I2C读写EEPROM的工程里我经常需要把缓冲小数据块指定为分布式RAM减少BRAM占用而大数据FIFO则指定为BRAM利用其容量优势。(* ram_style block *) reg [7:0] fifo_mem [0:1023];不同工具的属性名略有差异Vivado用ram_styleQuartus用的是ramstyle或ram_init_fileSynopsys DC则不直接对应。你需要在做平台迁移时注意这点。(* fsm_encoding one_hot | gray | sequential *)状态机编码控制三段式状态机是热词里出现最多的场景。状态机的编码方式直接决定资源消耗和时序性能one_hot独热码每个状态一个寄存器译码逻辑最简单速度最快适合状态数不多但要求高频率的场景gray格雷码相邻状态之间只有一位变化适合顺序跳转为主的状态机能显著减少翻转功耗和毛刺sequential二进制状态寄存器数量最少适合状态非常多但跳转复杂的场景。不同工具对FSM编码的推断能力不同手动指定编码属性可以在Vivado和Quartus中直接影响优化结果。我在UART状态机里用one_hot在计数器型状态机里用gray综合后的时序余量和资源使用差异比较明显。(* max_fanout N *)扇出限制高扇出信号比如全局复位、使能信号在综合优化后可能导致布线拥塞和时序恶化。限制扇出可以让综合工具在超过N时自动复制寄存器来分担负载。这个属性在全局信号上比较有用但要注意不要滥用。过多的寄存器复制会消耗大量FF资源所以N的选择通常需要配合时序报告来反复调整。(* max_delay N *)路径延迟限制这个属性是直接写给综合工具的告诉它某条路径上的组合逻辑延迟不能超N ns。在DC里可以用set_max_delay在Vivado里可以配合XDC约束但在RTL内以属性的方式写最大延迟约束在FPGA工具链中支持度不一我是建议优先用约束文件而不是把延迟属性写死在RTL里——毕竟布局布线平台变了约束也要跟着变。(* shreg_extract yes | no *)移位寄存器推断控制在处理滑动窗口滤波时你要做一串移位寄存器数组。Vivado默认会把长的移位寄存器推断到SRL16或SRL32资源里这在某些时候是好事情但有时候移位寄存器中间需要同时抽出多级信号作为滤波器窗口SRL链会限制中间抽头的实现。此时可以设置shreg_extract no强制工具使用普通触发器实现每个窗口级都能独立访问。3.3 综合属性写在哪儿作用范围有多大综合属性要写在声明之前或实例之前注意位置有讲究。对于模块级属性写在module关键字之前对于端口写在端口声明之前对于信号写在reg/wire声明之前对于实例写在实例名之前。(* keep true, dont_touch true *) reg [31:0] debug_counter;多个属性可以用逗号合在一起写。还需要注意每个工具的属性和指令都不完全一样。Vivado支持keep、dont_touch、ram_style、shreg_extract、fsm_encodingQuartus的keep和dont_touch也基本支持但属性名在部分场景下要写成(* preserve *)或(* noprune *)。DC里则是通过set_keep_value、set_dont_touch等命令组合来处理不一定通过RTL属性。如果代码要在多工具之间复用最好把属性集中抽到一个头文件或者宏定义里方便切平台时统一修改。4. 综合指令在多字节收发、滑动窗口滤波和状态机里的实战位置4.1 多字节收发场景用宏定义做可配置功能开关多字节收发是很多通信接口的核心需求。我在设计UART或者I2C控制器的时候数据位宽经常要在一份代码里支持8位、16位甚至32位模式。直接用参数parameter是一种方法但如果你需要区分某些组合逻辑甚至硬件结构层面的差异参数化可能不够直观这时候宏定义就派上用场了。ifdef BYTE_16 localparam DATA_WIDTH 16; reg [DATA_WIDTH-1:0] rx_buffer; else localparam DATA_WIDTH 8; reg [DATA_WIDTH-1:0] rx_buffer; endif综合时在工具里定义BYTE_16工具就会把DATA_WIDTH设置成16综合出16位的数据通路不定义就默认8位。这样一套代码维护多个版本后续要改成32位也只是加一个宏的事。但是宏定义过多会导致代码可读性下降。一般来说能用parameter尽量用parameter只有那些确实影响硬件结构、分支差异非常大的地方才用宏。参数是运行期的常量综合工具也能优化灵活性更好。4.2 滑动窗口滤波场景确保中间寄存器不被优化掉滑动窗口滤波本质就是对一段连续数据做移位和加权平均。以3点滑动平均为例(* shreg_extract no *) reg [15:0] window_reg [0:2]; always (posedge clk) begin window_reg[0] din; window_reg[1] window_reg[0]; window_reg[2] window_reg[1]; end assign dout (window_reg[0] window_reg[1] window_reg[2]) / 3;这里有个很容易踩的坑如果你把window_reg声明成普通的数组综合工具可能直接把它推断成移位寄存器整合进SRL资源。SRL资源本身并没有问题但如果后续你需要在窗口中间插入使能控制、复位逻辑或者用ILA观测中间级数据SRL就有诸多不便。这时候用shreg_extract no强制使用FF实现会比较稳妥。另外如果中间级的信号在综合报告里被优化掉了加(* keep true *)可以保留它们。特别是在调试滑动窗口滤波系数是否生效、数据对齐是否正确时这个属性救过我很多次。4.3 三段式状态机场景用好FSM编码属性但别乱用full_case三段式状态机的标准写法是第一段状态转移、第二段次态逻辑、第三段输出寄存。这种写法在综合时具有天然优势——输出寄存器可以有效避免组合毛刺时序性能也更稳定。在状态机场景下我建议显式指定编码方式(* fsm_encoding one_hot *) reg [3:0] state; localparam IDLE 4b0001, START 4b0010, DATA 4b0100, STOP 4b1000;这样综合工具就不会自作主张选择二进制或格雷码。对于状态数不超过16个的典型通信协议解析器用独热码一般能跑出不错的Fmax。但要注意添加fsm_encoding属性之后综合工具可能不会按照源码里的状态值来判断case分支而是按工具重新编码后的值来做逻辑综合。这种RTL综合前仿真和门级综合后仿真状态值不一致的问题需要你在验证时留意。我的经验是在RTL仿真阶段状态机变量的值确实按源码localparam走综合后工具会替换成自己选定的编码。因此如果你在代码里通过$display或ILA查看状态值前后可能不一样这是正常的工具行为。至于full_case和parallel_case在状态机场景我再强调一次case分支一般天然是互斥的parallel_case多数时候是多余的如果状态转移没有覆盖所有状态组合full_case反而会掩盖未定义状态的行为造成综合后跳转到非法状态。正确做法是补一个default: state IDLE;让FSM可以自恢复而不是用full_case去骗综合工具。4.4 计数器、分频器、CRC、FIFO里的综合指令使用思路计数器/分频器的核心是组合逻辑寄存器一般不需要特殊综合指令。但有一种情况分频器如果你是用always (posedge clk)加#10这种写法仿真能过综合必挂。这就是最基础的综合指令边界认知——#10是仿真延时不是硬件行为。CRC生成器本质上是一组线性反馈移位寄存器它的特性和移位寄存器类似如果CRC长度很长工具可能把部分寄存器组优化合并。这时用dont_touch或者keep保护CRC寄存器链可以避免综合工具做过度优化导致硬件和仿真不匹配。FIFO的实现里我最常关注存储体的推断。异步FIFO一般会例化专用的FIFO IP或者手写双口RAM。手写双口RAM时ram_style属性就很重要如果要高性能高容量用BRAM如果FIFO比较小且需要低延迟用分布式RAM。在Vivado里不指定时工具根据容量选但在Quartus里行为可能不同所以最好显式指定。5. 综合指令的坑版本差异、可综合性、后仿真验证5.1 不同工具的“方言”差异综合指令和综合属性最大的坑就是工具方言问题。同一个功能四家工具写法可能完全不同功能VivadoQuartusSynopsys DC保留信号(* keep true *)(* keep *)set_keep_value禁止优化(* dont_touch true *)(* preserve *)set_dont_touchRAM实现(* ram_style block *)(* ramstyle M9K *)RAM inference 指导跳过仿真代码/* synthesis translate_off */同左同左状态机编码(* fsm_encoding one_hot *)(* fsm_encoding one-hot *)set_fsm_encoding我在多平台迁移时吃过亏原来代码里写了(* ramstyle M9K *)在Quartus上工作正常换到Vivado后工具直接忽略这个属性FIFO被推断成了分布式RAMBRAM占用量为0导致时序不满足。排查过程花了大半天最后才发现是属性名不兼容。所以如果你打算在多个工具之间复用代码最稳妥的方式是在头文件里集中定义宏按工具平台选择不同的宏分支。比如ifdef VIVADO define KEEP_ATTR (* keep true *) elsif QUARTUS define KEEP_ATTR (* keep *) else define KEEP_ATTR endif然后代码里统一写KEEP_ATTR换平台时只需要改预处理器的宏定义即可代码主体不用动。这种思路在大型工程里非常实用。5.2 Icarus Verilog和综合工具之间的差别很多初学者用Icarus Verilog做仿真然后问我“为什么我的代码在Icarus上能跑到Vivado综合就报错”要明确一点Icarus Verilog是仿真器不是综合器。它只负责验证功能正确性不关心代码是否可综合。这意味着你可以在Icarus里用#10延时、initial、$readmemh等仿真语法但这些语法到了综合工具里往往就是语法错误。正确流程是用Icarus或VCS等仿真器做RTL仿真验证功能确保逻辑行为正确后再把代码拿到综合工具里做综合和实现。两个步骤的工具不同目的不同注意事项也不同。我见过最典型的例子有人在Testbench里用#10产生时钟然后直接把Testbench当设计文件拿去综合结果综合工具报上百个错误。这种问题不是综合指令能解决的而是设计流程概念没理清。5.3 translate_off的“仿真综合不一致”经典问题translate_off和translate_on用起来非常爽但也是造成仿真和综合不一致的常见元凶。假设你有这样一段代码always (posedge clk) begin if (rst) begin data 0; end else begin data din; end /* synthesis translate_off */ if (debug_en) $display(data %d, data); /* synthesis translate_on */ end仿真时$display会正常打印综合时那段代码被忽略。这是预期的行为。但如果你把translate_off包住的代码里包含了某些对输出有实际影响的语句比如/* synthesis translate_off */ data din 1; /* synthesis translate_on */那么仿真时data的值会比综合后多1功能立刻不一致。这类错误往往在人疲惫时无意引入而且不会在综合时报错只会在上板测试时表现为数据不对排查难度极高。我的建议是translate_off和translate_on之间只写纯仿真的监控代码、断言、打印信息绝对不要在里面给任何设计信号赋值。如果一定要条件性赋值用defparam、initial或者宏定义单独隔离仿真模型而不是用translate_off硬切。5.4 综合报告和门级仿真是检验综合指令是否生效的唯一标准写完综合指令和属性怎么确认它们真的生效了两个字报告。看综合报告里的资源利用如果指定了BRAM却显示使用0说明属性没生效或写错了看RAM/FF/DSP推断报告很多工具会详细列出哪些信号被推断成了什么资源看状态机报告Vivado的综合日志里有FSM编码信息会显示每个状态机的编码方式跑门级功能仿真用综合后的网表做仿真对比RTL仿真的波形一旦有差异优先检查你写的综合指令是否影响了设计语义。我记得有一次在滑动窗口滤波模块里我加了(* keep true *)保护中间级信号但综合报告显示某个窗口级还是被优化成了常量。后来仔细查代码才发现问题不在属性而是该信号pad上的组合逻辑提前被常量传播了——keep只能防止信号被优化掉不能防止输入恒定时工具做常量优化。这类问题时需要回到RTL逻辑本身而不是继续堆综合指令。5.5 综合指令不是越多越好最后说一点经验综合指令和综合属性是解决特定问题的工具不是用来堆砌的装饰。一个设计里塞满dont_touch、keep、各种编码属性会让综合工具失去优化自由度最终资源变多、时序变差而且代码可维护性也急剧下降。真正合理的用法是默认状态下尽量写标准可综合Verilog让工具自由优化只在工具的默认行为不符合设计意图时才加属性做局部干预每条指令和属性都要能在综合报告里找到对应的证据换工具、换平台时重新审视这些指令是否还有必要。我自己现在的习惯是新代码先不加任何综合指令综合一遍看资源时序报告再按需补属性。只有在调试过程中确实遇到“信号被优化找不到”、“RAM推断不符合预期”、“状态机编码导致时序差”这些具体问题时才针对性加属性。这样做的好处是绝大多数代码保持干净简洁综合指令的性能影响被限制在最小范围。写在最后的一次实战排查经历好理论讲完了最后分享一个非常典型的实战排查过程。此前我在调试一个UART多字节收发模块时遇到的问题是RTL仿真完全正确上板后接收到的数据偶发错位。一开始我怀疑是跨时钟域问题后来检查发现收发两端时钟同源排除。又怀疑是FIFO读写时序问题把FIFO的读写信号用ILA抓出来波形上看完全正常。最后我只能对比综合报告。结果发现FIFO的存储体被综合工具推断成了LUTRAM分布式RAM而我的设计预期是BRAM。因为代码里虽然写了(* ram_style block *)但属性名在Quartus工程中被宏定义重写成了旧写法实际没有生效。加上这个属性写对后BRAM推断生效偶发错位问题消失。事后复盘真正的原因是LUTRAM在FPGA里由LUT搭建容易出现读写时序边界问题而BRAM有更成熟的时序控制机制。这个案例说明三件事综合报告必须看、综合指令要确认生效、工具方言差异要认真对待。如果当初写代码时就把存储体属性用宏封装好按工具平台统一管理可能就不会浪费那整整一天的排查时间了。综合指令这个主题往深了说可以写一本书但日常工程里高频使用的其实就是本文梳理的这些。掌握了编译器指令、注释式指令、综合属性三种类型再配合报告验证你在Verilog综合这条路上会少踩很多坑。
返回列表