每个运算符都是一个电路:FPGA 开发者的 Verilog 运算符实战手册
💡 你写了一行 assign result = data / 8;,综合工具报了一个 Warning,或者综合出了一个占用几百个 LUT 的庞大电路。而你的同事只改了一个符号——assign result = data >> 3;——资源消耗变成了零。
这就是 Verilog 运算符和 C 语言运算符的本质区别:Verilog 的每一个运算符都对应着一段真实的硬件电路。+ 号背后是加法器,* 号背后是 DSP Slice,>> 号背后只是重新接了几根线。写 C 语言时你不用关心加法和移位的性能差异,但写 Verilog 时,选错运算符可能意味着芯片放不下你的设计。
这篇文章不是运算符语法的枯燥罗列,而是帮你建立**“运算符 → 硬件电路”的映射思维**——这是 FPGA 工程师的核心能力之一。
📌 系列衔接:本文承接上一篇 Verilog 模块与信号系统。
目录
- 1. 运算符优先级总览
- 2. 算术运算符
- 3. 位运算符
- 4. 归约运算符
- 5. 逻辑运算符与关系运算符
- 6. 移位运算符
- 7. 拼接与复制运算符
- 8. 条件运算符
- 9. 等式运算符
- 10. 总结
- 常见问题
- 参考资料
1. 运算符优先级总览
先给一张总表,遇到优先级拿不准时回来查:
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 最高 | ! ~ | 逻辑非、按位取反 |
* / % | 乘、除、取模 | |
+ - | 加、减 | |
<< >> | 左移、右移 | |
< <= > >= | 关系比较 | |
== != === !== | 等式比较 | |
& | 按位与 | |
^ ~^ | 按位异或、同或 | |
| | 按位或 | |
&& | 逻辑与 | |
|| | 逻辑或 | |
| 最低 | ?: | 条件运算符 |
💡 工程师手记:虽然有优先级表,但我的建议是——拿不准就加括号。RTL 代码不是炫技的地方,可读性永远比简洁性重要。加括号还能避免不同综合工具对优先级理解不一致的问题。
2. 算术运算符
| 运算符 | 功能 | 硬件对应 |
|---|---|---|
+ | 加法 | 加法器 |
- | 减法 | 加法器(补码) |
* | 乘法 | 乘法器(DSP Slice) |
/ | 除法 | ⚠️ 通常不可综合 |
% | 取模 | ⚠️ 通常不可综合 |
wire [8:0] sum = a + b; // 8位加法,注意进位需要9位
wire [15:0] prod = a * b; // 8位乘8位,结果16位
FPGA 开发者须知
- 加法和减法是最基础的运算,综合后占用 LUT 资源,开销不大。
- 乘法在 Xilinx FPGA 中会映射到专用的 DSP48 Slice,效率很高。但 DSP Slice 数量有限,大量乘法可能耗尽。
- 除法和取模没有简单的硬件对应,综合工具通常报错或综合出极其庞大的电路。如果确实需要除法,考虑用移位代替(除以 2 的幂次)或使用专用 IP 核。
- 当操作数中有
x(未知值)时,整个运算结果都是x。
// ✅ 推荐:用移位代替除以2的幂次
assign result = data >> 3; // 等效于 data / 8
// ❌ 避免:直接使用除法
assign result = data / 8; // 综合结果不可预测
3. 位运算符
位运算符是 FPGA 开发中使用频率最高的运算符——因为数字电路的本质就是对每一位信号做逻辑运算。
| 运算符 | 功能 | 硬件对应 |
|---|---|---|
& | 按位与 | AND 门 |
| | 按位或 | OR 门 |
~ | 按位取反 | NOT 门(反相器) |
^ | 按位异或 | XOR 门 |
~^ | 按位同或 | XNOR 门 |
wire [3:0] a = 4'b1100;
wire [3:0] b = 4'b1010;
wire [3:0] and_result = a & b; // 1000
wire [3:0] or_result = a | b; // 1110
wire [3:0] xor_result = a ^ b; // 0110
wire [3:0] not_result = ~a; // 0011
位宽不等时的处理
当两个操作数位宽不同时,Verilog 会自动将较短的操作数高位补零,然后按位运算。这是一个隐蔽的坑——如果你的信号是有符号数,高位补零可能导致结果错误。
wire [7:0] a = 8'hFF; // 8位
wire [3:0] b = 4'hF; // 4位
// b 会被扩展为 8'h0F,然后做按位与
wire [7:0] result = a & b; // 结果是 8'h0F,而非 8'hFF
4. 归约运算符
归约运算符和位运算符长得一模一样,但含义完全不同。位运算是两个操作数之间逐位运算,归约运算是一个操作数内部所有位之间运算。
| 运算符 | 功能 | 结果位宽 |
|---|---|---|
& | 所有位做与 | 1 bit |
| | 所有位做或 | 1 bit |
^ | 所有位做异或 | 1 bit |
wire [3:0] data = 4'b1010;
wire all_one = &data; // 1&0&1&0 = 0(是否全1)
wire any_one = |data; // 1|0|1|0 = 1(是否有1)
wire parity = ^data; // 1^0^1^0 = 0(奇偶校验)
归约运算符在 FPGA 开发中非常实用:
&data:判断信号是否全为 1|data:判断信号是否全为 0(取反:~|data)^data:奇偶校验位生成
💬 你可能会问:怎么区分
&是位运算还是归约运算?看操作数个数。
a & b(两个操作数)是位运算,&a(一个操作数)是归约运算。编译器根据上下文自动判断。
5. 逻辑运算符与关系运算符
逻辑运算符
| 运算符 | 功能 | 结果 |
|---|---|---|
&& | 逻辑与 | 1 bit(真/假) |
|| | 逻辑或 | 1 bit(真/假) |
! | 逻辑非 | 1 bit(真/假) |
逻辑运算符将整个操作数视为一个整体——非零为真(1),零为假(0)。
wire [3:0] a = 4'b1010; // 非零,逻辑上为"真"
wire [3:0] b = 4'b0000; // 零,逻辑上为"假"
wire result = a && b; // 1 && 0 = 0
! vs ~ 的区别:! 是逻辑非(结果是 1 bit),~ 是按位取反(结果与操作数同位宽)。
wire [3:0] a = 4'b1010;
wire logic_not = !a; // 结果:1'b0(a非零,取反为假)
wire [3:0] bit_not = ~a; // 结果:4'b0101(逐位取反)
关系运算符
>、<、>=、<= 用于比较大小,结果为 1 bit。在 FPGA 中综合为比较器电路。
6. 移位运算符
| 运算符 | 功能 | 补位 |
|---|---|---|
<< | 逻辑左移 | 低位补 0 |
>> | 逻辑右移 | 高位补 0 |
wire [7:0] a = 8'b1100_0011;
wire [7:0] left = a << 2; // 0000_1100(左移2位)
wire [7:0] right = a >> 2; // 0011_0000(右移2位)
移位的硬件本质——零资源消耗的”免费运算”
这是本文最值得记住的知识点:移位操作在硬件中不消耗任何逻辑资源——它只是重新连线。左移 N 位就是把信号线向高位平移 N 位,空出来的低位接地(0)。这也是为什么用移位代替乘/除 2 的幂次是 FPGA 设计的常见优化。
// 这三行综合结果完全相同,但移位的意图更清晰
assign y = a << 3; // 乘以8
assign y = a * 8; // 乘以8(综合工具通常也会优化为移位)
assign y = a * 4'd8; // 乘以8
7. 拼接与复制运算符
拼接运算符 {} 是 Verilog 的”瑞士军刀”——把多个信号首尾相连,组成更宽的信号。
拼接
wire [3:0] high = 4'hA;
wire [3:0] low = 4'h5;
wire [7:0] byte_data = {high, low}; // 8'hA5
复制
用 {N{signal}} 将信号复制 N 份后拼接:
wire [7:0] all_one = {8{1'b1}}; // 8'hFF
wire [7:0] extended = {{4{data[3]}}, data[3:0]}; // 符号扩展
常见应用场景
// 1. 符号扩展:将4位有符号数扩展为8位
wire [3:0] signed_4bit = 4'b1010; // -6(补码)
wire [7:0] signed_8bit = {{4{signed_4bit[3]}}, signed_4bit}; // 8'b1111_1010
// 2. 清零高位
wire [7:0] masked = {4'b0, data[3:0]}; // 只保留低4位
// 3. 位域拼接
wire [15:0] packet = {start_bit, addr, data, parity};
8. 条件运算符
? : 是 Verilog 中的三目运算符,功能等同于 2 选 1 MUX(多路选择器)。
assign out = sel ? data_1 : data_0;
综合后就是一个 MUX:当 sel=1 时输出 data_1,否则输出 data_0。
可以嵌套使用,实现多路选择:
// 4选1 MUX
assign out = (sel == 2'b00) ? d0 :
(sel == 2'b01) ? d1 :
(sel == 2'b10) ? d2 :
d3 ;
💡 工程师手记:条件运算符嵌套超过 3 层后,可读性会急剧下降。这时候我通常会改用
always @(*)块配合case语句,虽然代码长了一点,但看起来清爽多了。(建议替换为你自己的真实经历,读者会更有共鸣)
9. 等式运算符
Verilog 有两组等式运算符:
| 运算符 | 名称 | 对 x/z 的处理 |
|---|---|---|
== != | 逻辑等/不等 | 含 x 或 z 时结果为 x |
=== !== | 全等/不全等 | x 和 z 也参与精确比较 |
wire a = 1'bx;
wire b = 1'bx;
wire eq1 = (a == b); // 结果:x(不确定)
wire eq2 = (a === b); // 结果:1(两个x完全相同)
FPGA 开发实践:
- RTL 设计(可综合代码)中只用
==和!= ===和!==只用于仿真和 Testbench,综合工具不支持===常用于casex/casez的行为模拟
10. 总结
| 运算符类型 | 代表 | 硬件对应 | FPGA 开发关注点 |
|---|---|---|---|
| 算术 | + - * | 加法器、乘法器 | 除法/取模尽量避免 |
| 位运算 | & | ^ ~ | 逻辑门 | 使用频率最高 |
| 归约 | &a |a ^a | 级联逻辑门 | 全1/全0判断、奇偶校验 |
| 移位 | << >> | 重新连线 | 零资源消耗,替代乘除 |
| 拼接 | {} | 重新连线 | 信号拼接、符号扩展 |
| 条件 | ?: | MUX | 嵌套不超过3层 |
| 等式 | == === | 比较器 | === 仅用于仿真 |
核心思维:每写一个运算符,想一想它对应什么硬件。加法器多大?乘法器要不要用 DSP?移位是不是零开销?这种思维习惯越早养成,你的 RTL 代码质量就越高。
常见问题
Q1:& 到底是位运算还是归约运算?编译器怎么区分?
看操作数个数。
a & b(双目)是按位与,&a(单目)是归约与。编译器根据语法上下文自动判断,不会混淆。
Q2:乘法一定会用 DSP Slice 吗?
不一定。如果操作数是常数且为 2 的幂次,综合工具会优化为移位。如果操作数很小(如 3 位乘 3 位),工具可能用 LUT 实现。你可以通过综合报告查看具体的映射结果,也可以用属性
(* use_dsp = "yes" *)强制使用 DSP。
Q3:Verilog 有没有算术右移(保留符号位)?
Verilog-2001 引入了
>>>算术右移和<<<算术左移。但要注意,信号必须声明为signed类型才能生效:wire signed [7:0] a = -8'd4; wire signed [7:0] b = a >>> 1; // 算术右移,高位补符号位
参考资料
- IEEE Std 1364-2005: IEEE Standard for Verilog Hardware Description Language
- Xilinx/AMD,UG901: Vivado Design Suite User Guide - Synthesis(第 4 章:HDL Coding Techniques)
- Pong P. Chu,FPGA Prototyping by Verilog Examples(第 2 章)
系列导航:本文是「FPGA 入门系列」第 8 篇。
- 上一篇:Verilog 模块与信号系统
- 下一篇:Verilog 赋值机制与三种描述方式
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区分享你踩过的运算符相关的坑。