写 Verilog 的这些年遇到不少刚转过来的朋友第一反应都是运算符不就是加减乘除、与或非、比较大小嘛跟 C 语言能差到哪去可一旦真正开始写 RTL、做仿真、跑综合就会被位宽、有符号无符号、表达式到底扩展成多少位这些事折腾到怀疑人生。我印象很深的一次同事写的计数器判断逻辑仿真波形完全正确一到综合就出问题最后翻出来是 3 位移位和一个比较运算的位宽补齐方式不一致导致的。这种坑我在项目里踩过无数次也帮别人排查过无数次。所以这篇文章不打算泛泛地罗列运算符表而是想把 Verilog 运算符背后的规则、容易踩的雷、以及实际工程里的使用习惯一次讲透。适合刚入门 FPGA 和数字 IC 的同学也适合被位宽和符号问题反复折磨的工程师。1. 先把运算符地图摊开Verilog 的运算符按功能可以分为几大类算术运算符、关系运算符、逻辑运算符、位运算符、归约运算符、移位运算符、拼接运算符、条件运算符还有赋值运算符。很多教材会列一张大表这里我也给一张精简版方便随时查。类别运算符典型用途能否综合算术 - * / %加减乘除、取模加减乘基本可综合除法取模慎用关系 !比较大小、相等判断可综合为比较器相等 !含 x/z 的相等比较不可综合仅供仿真逻辑 || !逻辑与、或、非可综合位运算 | ^ ~ ^~按位与、或、异或、同或可综合归约 | ^ ~ ~| ^~总线归约成 1 位可综合移位 逻辑/算术左移右移可综合但注意面积拼接{ } {{ }}位拼接、重复拼接可综合条件? :二选一多路器可综合幂运算**指数运算大多综合工具支持差初看这张表和 C 语言很像但是有两个最大的不同点。第一个不同是 Verilog 里很多运算符既可以当二元运算符也可以当一元运算符。比如写在两个信号之间是“按位与”写在单个变量前面就成了“归约与”把变量里的所有比特挨个做与最后得到一个 1bit 结果。|和^也一样写在不同位置含义完全不同初学很容易搞混。我见过有同学想在 8bit 数据中检查是否存在高电平写了assign any_high |data;才对但他写成了assign any_high data | 1b0;结果比较了半天发现结果跟预想不一样因为后者是位运算宽度被扩展成 8bit。第二个不同是 Verilog 运算符的仿真语义和硬件映射非常直接。加法器、比较器、多路选择器几乎和运算符是一一对应的。理解这种映射对后续判断“这个写法能不能综合、面积大不大”很有帮助。后面单独拿一个章节展开。2. 位宽规则决定你八成以上的 Bug讲运算符之前必须先把位宽规则讲清楚因为 Verilog 里几乎所有的“看似正确但结果不对”最后都会追溯到位宽扩展和符号处理上。2.1 表达式位宽上下文决定与自决定C 语言里整数运算一般按 int 处理大家习惯了不用操心宽度。Verilog 不一样每个信号都有自己的精确位宽运算结果到底取多少位有一套严格的规则。Verilog 把表达式分成两类自决定表达式和上下文决定表达式。自决定表达式的位宽只看它自己的操作数不往外扩展上下文决定表达式的位宽则由它所在的环境一起决定。最经典的上下文决定场景是赋值语句右端的算术表达式。举个例子wire [7:0] a; wire [7:0] b; wire [8:0] sum; assign sum a b;这里a b的上下文宽度取 LHS 宽度和操作数宽度中的较大值也就是 max(9, 8) 9 位。所以 a 和 b 都会被扩展到 9 位再相加sum 能正确保存 8bit 加法的进位。如果 sum 写成 8bit上下文宽度就是 8 位结果自动截断加法溢出就成了静默发生的设计意图之外的行为。真正容易出错的是复合运算表达式。比如assign y a b c;这个式子实际是先算(a b)再算(...) c。第一步的结果位宽由随后使用它的第二个加法决定而第二个加法又由 LHS 和所有操作数决定。规则本身不复杂麻烦的是当 a、b、c 宽度都不一样而中间某个量被截断时问题就来了。我自己写代码时只要表达式超过两层就基本不用长表达式一步到位而是拆成中间变量。写清楚每步的位宽比事后排查要省很多时间。移位运算符是特殊情况。它的结果位宽由左操作数决定右移位数本身是自决定的不参与上下文扩展。也就是说data n结果和 data 一样宽n 只控制移多少位不改变结果宽度。这跟加法不一样别搞混。2.2 有符号与无符号混着用等于埋雷位宽之外符号性是最容易被忽视的。Verilog 里一个很硬的规定是只要表达式里混入了无符号操作数整个表达式就按无符号处理。哪怕你特意声明了reg signed只要另一个操作数是普通wire [7:0]结果就变成无符号语义。举一个高频踩坑场景reg [7:0] a; reg [7:0] b; reg signed [15:0] diff; assign diff a - b;如果 a 是 8h10b 是 8h20数学上 diff 应该是 -16但这段代码仿真出来 diff 会是 16h00F0也就是 240。原因是 a 和 b 都没有声明成 signed减法按无符号做得到 8hF0再零扩展成 16 位结果是 240。要得到正确的 -16必须先把操作数转成有符号assign diff $signed(a) - $signed(b);$signed()的作用是把原数据按相同位宽解释为有符号数之后减法表达式里所有操作数都是 signed结果再做符号扩展才是正确的 16hFFF0。比较运算也一样。两个 8bit 数如果都想按有符号比较必须两边都是 signed。最常见的陷阱是一个 signed 变量和一个无符号常数比较。比如reg signed [7:0] val; if (val 8sh7F) ...如果写成if (val 8h7F)8h7F 是无符号数表达式自动变成无符号比较val 里的 bit7 会被当成数值位而不是符号位比较结果完全错乱。这种 bug 尤其恶心因为单看代码很容易忽略。2.3 扩展还是截断取决于目标当一个表达式结果要赋给不同宽度的 LHS 时综合器和仿真器会做扩展或截断。无符号数高位补 0有符号数高位补符号位。比如一个 8bit 有符号数 -1也就是 8hFF赋值给 16bit 有符号变量应扩展成 16hFFFF但如果是无符号 8hFF 赋值给 16bit 变量就扩展成 16h00FF。这个差异在运算中体现得非常明显而且常发生在你不注意的地方。我建议所有做数字设计的人在写算术运算之前先在草稿纸上标清楚三件事每个输入信号的位宽、每个输入信号的符号性、输出 LHS 的位宽和符号性。不是小题大做因为位宽错误在仿真里很多时候不报错只是结果悄悄变错等综合后时序和功能对不上才暴露代价就大了。2.4 位宽问题常见速查场景错误示范正确做法两数相加需要进位assign sum a b;且 sum 与 a/b 同宽把 sum 扩展一位避免最高位进位被截掉计数器计满回卷count count 1b1;且 count 只有 4bit 又要计到 31将 count 位宽扩到 5bit 或更大乘法结果截断assign y a * b;y 和 a/b 同宽先把乘法输出定义成 a 宽 b 宽再截取所需位常数默认宽度过大count 1中1默认是 32bit写成count 1b1尽显式宽度无符号减法想得负数assign diff a - b;diff 比 a/b 宽使用$signed(a) - $signed(b)3. 逐个拆解最容易被坑的运算符3.1 相等比较与天差地别比较的是逻辑值如果两个操作数里出现 x 或 z结果直接是 x而不是 0 或 1。则是按位严格比较连 x/z 状态也算一种“值”x 等于 xz 等于 z结果一定是 1 或 0。实际工程里更多用于仿真环境的断言和测试不用于可综合逻辑。因为你不太可能想在硬件里判断“这个信号是不是 x”。如果确实要在写 Testbench 时检测信号是否处于未初始化状态if (sig 1bx)是常见写法。要注意的是综合工具看到通常会报错或忽略不要在 RTL 代码里碰它。3.2 条件运算符?:和多路选择器?:的硬件语义非常明确二选一多路器。嵌套使用时就是多级 MUX综合工具会尽量优化成更合理的结构。常见坑是优先级问题。assign y a ? (b ? 2b10 : 2b01) : 2b00;这个写法外层条件不满足时y 固定为 00内层条件只在 a 为真时起作用。如果逻辑上需要“先判断 b 再判断 a”那就是另一套写法。嵌套层级多了以后人很难一眼看出优先级对应哪些条件组合。我更喜欢把复杂条件拆成多个always块或临时变量保证每个?:只做一层判断。另外综合器对?:生成 MUX 比较高效有时候比用if-else生成优先级逻辑更可控。比如多个互斥条件的输出用?:写通常最后的面积和时序都比一层层if-else要好。不是绝对的但可以作为参考。3.3 移位运算符与“算术右移”的玄机是逻辑右移高位补 0是算术右移高位由左操作数是否为 signed 决定。如果是 signed高位补符号位如果是 unsigned你的写出来和高位补 0 没有区别。reg signed [7:0] val; wire [7:0] out1; wire [7:0] out2; assign out1 val 2; // 算术右移补符号位 assign out2 val 2; // 逻辑右移补 0val 是 8sh80 时out1得到 8hE0而out2得到 8h20。差异非常明显。还有一个工程细节当移位次数是变量时综合出来的电路是桶式移位器本质是一堆 MUX 级联位宽大一点面积就涨得很快。比如 32bit 数据的可变右移大概会消耗几百个 LUT。如果项目中并不需要“任意次数移位”能用固定移位就用固定移位或者把变量限制在一定范围内可以省不少资源。3.4 拼接{}和复制{{}}拼接是 Verilog 里非常有含金量、用着最舒服的运算符。重组字节序、提取位域、插入常数值一个{}全搞定。wire [7:0] high data[15:8]; wire [7:0] low data[7:0]; assign swapped {low, high};拼接结果的总位宽是各成员宽度之和成员本身不会被上下文扩展。这一点和算术表达式不一样。比如你写assign y {a, b};如果 y 位宽大于{a,b}赋值时会对拼接结果做高位补零如果 y 位宽小于拼接结果高位部分直接扔掉。复制操作符写作{N{信号}}适合生成重复的 bit pattern。比如把一个 8bit 数复制 4 次变成 32bitwire [31:0] rep {4{data}};注意复制的大括号外套一层大括号。老手也偶尔会漏掉外层大括号一旦漏了语法直接报错。3.5 归约运算符一言不合就检测全 0 全 1归约运算符是一元运算符作用是把一个多位总线归约成 1bit。data是“所有位都为 1 才输出 1”|data是“任意位为 1 输出 1”^data是“奇数个 1 输出 1”。这几个运算符特别适合做引脚/总线状态判断。比如检查 8bit 配置寄存器是否是 0wire cfg_zero ~|cfg_reg; // cfg_reg 全 0 时为 1 wire cfg_full cfg_reg; // cfg_reg 全 1 时为 1有时候写成cfg_reg 8h00也能综合但用归约运算符更简洁而且不会引入“常数位宽”这种额外考虑。实现奇偶校验也常用^data比如 UART 的偶校验位就可以直接这样产生。3.6 自增减运算符和幂运算仿真爽但综合小心i、i--、i 1这些写法在 Testbench 里非常方便。可惜在可综合 RTL 里非常不推荐。和--在 Verilog 里是否可以被工具接受都是问题很多综合工具要么报错要么转换成行为不明的方式。RTL 时钟边沿里写计数老老实实count count 1b1;就好。**幂运算符在仿真里可以用综合支持则看工具和工艺库。即使支持一个非 2 次幂的幂运算大概率产生很大组合逻辑时序很难收敛。做算法设计时尽量把幂运算转成移位或者放到 CPU/MCU 里算再或者用查表。4. 从综合视角看运算符它到底变成了什么电路4.1 加减乘与比较器的硬件语义加法器是数字电路的基本单元综合工具对的优化已经做得很好甚至能推断出超前进位链或进位保存加法器。减法器本质是加法器加补码变换。乘法器在有 DSP 资源的 FPGA 里会被吸收到 DSP 块中综合工具一般能优化出不错的结果。比较器也是基础单元。两个 8bit 数比较大小综合结果是一棵比较树位宽越大延迟越长。如果比较器出现在关键路径上可以考虑用流水或提前计算来改善时序。算术运算符在 RTL 里基本可以放心用但要留意上下文位宽和符号规则。综合工具是按 Verilog 规则推断位宽不是按人的直觉推断。4.2 除法、取模能用但别乱用/和%是可综合的但直接综合a / b往往意味着生成一个庞大的组合除法器面积和延迟都很惊人。尤其在 FPGA 上很多综合器对除法会生成一连串 LUT 阵列稍微宽一点就严重拖慢时序。工程上有几个替代思路。除数是 2 的幂直接用移位除数是常数但不是 2 的幂可以手工转成乘法和移位组合近似很多场景都够用除数完全可变推荐用厂商提供的除法器 IP或者自己写基于恢复余数法/非恢复余数法的迭代除法器用状态机分拍算。%取模也是一样。模 2 的幂可以直接data (2^n - 1)效率极高。模任意数除非是用在 Testbench 或仿真模型否则不建议综合。4.3 不可综合的运算符与写法下面这些写法在仿真里运行良好但综合工具要么直接报错要么忽略写法原因改用#5 clk ~clk;延迟控制不可综合用时钟生成器或 Testbench 中单独写initial块仿真初始化不可综合用复位逻辑上电初始化/!含 x/z 严格比较仅 Testbench 使用casexx 态通配容易误导综合用casez配合?通配i/i--综合支持差写成i i 1b1;**综合面积巨大或不可综合移位、IP、查表一定要记住可综合代码不等于 “语法没错的代码”。语法正确但工具不理解最终只会得到错误网表或综合报告里的一堆 warning。我的习惯是每个模块写完都立刻跑一遍 lint 和空综合哪怕只是看看有没有不可综合语句也比等后端才发现问题强。4.4 工程实例计数器 比较器 边沿检测的组合用一个实际小模块串一遍这些运算符。需求产生一个 PWM 波形周期 255占空比由duty控制同时检测输入信号sig_in的上升沿。module pwm_edge_detect ( input wire clk, input wire rst_n, input wire [7:0] duty, input wire sig_in, output reg pwm_out, output wire rising_edge ); reg [7:0] cnt; reg sig_d; // 8bit 计数到 8hFF 回卷 always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 8h00; else cnt cnt 1b1; end // 占空比比较输出 always (posedge clk or negedge rst_n) begin if (!rst_n) pwm_out 1b0; else pwm_out (cnt duty) ? 1b1 : 1b0; end // 上升沿检测用一级打拍 位运算 always (posedge clk or negedge rst_n) begin if (!rst_n) sig_d 1b0; else sig_d sig_in; end assign rising_edge sig_in ~sig_d; endmodule这个例子里用到了、、?:、、~。注意rising_edge用的不是运算符和~而是位运算把当前值和上一拍值做组合得到“上一拍为 0 且当前拍为 1”的标志。这是非常经典的 1bit 边沿检测写法很多新手会写成sig_in ~sig_d逻辑上结果一样但用位运算的语义更贴近硬件也更适合综合成组合逻辑。5. 常见问题与排查技巧实录5.1 一张表格收拢最常踩的坑现象可能原因排查思路仿真结果比预期多 1 或回卷LHS 位宽不够进位被截断打印输出信号和中间信号位宽检查 LHS 是否需要扩位负数算出来变成很大的正数无符号减法被零扩展检查操作数是否有 signed必要时$signed转换同一个表达式仿真和综合结果不一致存在符号性混用或位宽警告先跑 lint 工具看位宽警告再打印每个子表达式的位宽比较器始终不满足条件常数的默认符号性和位宽把比较带偏了显式写成8sh80这类带符号常量综合报告面积异常大存在可变移位、除法、取模定位是哪个运算符导致改用固定移位或 IP代码里出现#延迟但综合像没看到延迟控制不可综合RTL 中删掉所有#只放 Testbenchcase 语句功能在综合后丢失部分分支casex或分支不完整导致锁存器用casez并补全默认分支5.2 排查流程参考碰到运算符相关的问题我的排查顺序基本固定。先看 lint 和综合日志里的位宽警告。很多人无视 warning但“result of N-bit expression is truncated to fit in M-bit destination”这种话基本就是通向 bug 的路标。再不行就在 Testbench 里加$display把参与运算的变量在运算前打印出来人工手算一遍。很多位宽问题不是逻辑复杂而是小数点没对齐。最后用综合工具的 schematic 或资源报告看关键运算符究竟被映射成了多大电路。5.3 个人习惯总结写可综合 RTL 时我给自己定了几条死规矩。第一任何算术表达式的常数都写显式位宽和进制比如1b1、8shFF不写裸的1或255。第二算术运算结果先赋值给足够宽的中间信号宁可多占 reg 也不要让进位悄悄丢。第三所有信号命名尽量带上符号暗示无符号默认不加有符号加_sgn后缀。第四比较运算里如果涉及 signed保证比较两边都是 signed。第五一个表达式超过三个操作数就拆行拆变量。这些习惯不能让你写出更“聪明”的代码但能极大降低调试成本。Verilog 运算符本身不难难的是时刻记得硬件世界里位宽和符号性无处不在。把这两条底线守住运算符的坑你基本就趟平了一大半。最后再分享一个小经验如果你不确定某个运算符的位宽或符号行为最有效的验证方法不是翻标准文档而是直接在仿真器里写一个 20 行的最小例子用$display打印每个中间结果。跑一遍观察值和位宽比看十遍文档都印象深刻。这个习惯让我少走了很多弯路也推荐给你。