第 7 章 · 共 16 章
运算符与过程语句
快速过一遍 SystemVerilog 里和你已经熟悉的 if/case/循环相同的部分,重点讲清楚几个 SV 特有的新东西:repeat/forever 循环、unique/priority case、按位与逻辑运算符、4 态全等比较 ===/!==、自增自减运算符,以及流操作符。
如果你写过任何一门主流编程语言,if/else、case(switch)、循环这些控制流对你来说不会陌生——这一节不会从头讲一遍它们的基本用法,而是快速过一下 SystemVerilog 里和你已知的部分一致的地方,把篇幅留给几个真正需要注意的 SV 特有语法。
熟悉的部分:if/else 与循环
if/else if/else 和大多数语言写法一致;for/while/do...while 也是同样的语义。SystemVerilog 额外提供了两个循环,在硬件描述和验证代码里很常见:
// repeat:执行固定次数
initial begin
repeat (3) begin
$display("hello");
end
end
// forever:无限循环,验证代码里最常见的用法是产生时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 每 5 个时间单位翻转一次,产生周期为 10 的时钟
endrepeat(N) 就是"运行 N 次"的简写;forever 是无限循环,几乎不会单独出现在设计代码里,但在验证代码里产生时钟、驱动持续激励时非常常见——上面这个 forever #5 clk = ~clk; 是几乎每个 testbench 都会写的时钟产生模板。
case 语句:没有隐式穿透
case 语义上更接近 Python 的 match 或 Rust 的 match,而不是 C/Java 的 switch——每个分支执行完自动结束,不会像 C 的 switch 那样在没写 break 时继续往下"穿透"(fall through)到下一个分支:
case (cur_state)
IDLE: next_state = RUN;
RUN: next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase命中 RUN 分支后,只会执行 next_state = DONE;,然后直接结束整个 case,不需要(也没有)break 语句。
unique case 与 priority case:让工具帮你检查分支质量
普通 case 语句不会检查你的分支是否穷尽(覆盖了所有可能取值)或互斥(多个分支同时匹配时该怎么办)——分支没写全,最常见的后果就是无意间生成了一个锁存器(latch),这类 bug 往往很隐蔽。SystemVerilog 提供了两个修饰关键字,让仿真器和综合工具在编译期/仿真期就帮你检查:
unique case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcaseunique case:告诉工具"这些分支应该互斥,且应该穷尽所有取值"——如果仿真时sel命中了不止一个分支,或者一个分支都没命中,工具会报警告/错误,而不是像普通case那样默默地按第一个匹配项执行(或者干脆什么都不做)。priority case:告诉工具"这些分支按书写顺序有优先级,第一个匹配的生效",同时仍然要求至少有一个分支被命中,否则报警告。
这两个关键字不会改变仿真时的实际行为(该怎么匹配还是怎么匹配),但能帮你在开发阶段就发现"漏写了某个分支"这类问题,而不是等到综合出一个意料之外的锁存器才发现。
按位运算符与逻辑运算符:&/|/^/~ vs. &&/||/!
这两组运算符长得很像,含义却完全不同,是新手很容易踩的坑:
- 按位运算符(
&、|、^、~)对两个操作数逐位运算——如果操作数是多位向量,结果也是同样位宽的向量,每一位互不影响。 - 逻辑运算符(
&&、||、!)把整个操作数当成一个"真/假"值(非零即真),结果永远是 1 位的布尔值。
logic [3:0] a = 4'b1100;
logic [3:0] b = 4'b1010;
$display("a & b = %b", a & b); // 按位与,逐位计算:1000
$display("a && b = %b", a && b); // 逻辑与:a 非零且 b 非零,结果是 1a & b 是把 a、b 的每一位分别做与运算,结果还是一个 4 位向量;a && b 则是先分别判断 a、b 是否为非零(真),再对这两个布尔值做逻辑与,结果永远是 1 位的 0/1。写错一个符号(比如该用 && 时写成 &)不会报错,但会得到完全不同的结果,需要格外留意。
4 态全等比较:=== 与 !==
第 5 章讲过 4 态类型有 x(未知)、z(高阻)两种特殊状态——但普通的 ==/!= 遇到 x/z 时,行为和直觉不一样,是初学者必踩的坑:
logic [3:0] data = 4'bx01x;
if (data == 4'bx01x)
$display("won't print"); // == 只要任意一位是 x/z,结果就是 x,if() 把 x 当成假==/!=(逻辑相等)只要任意一个操作数含有 x/z,比较结果就是 x(既不是 0 也不是 1),而 if 会把 x 当成假处理——所以上面这行 $display 永远不会执行,data 里含 x 的那一刻,== 比较就悄悄失效了。
要按位逐位比较,把 x/z 也当成普通值精确匹配,需要用 ===/!==(全等/不全等,也叫 case equality):
if (data === 4'bx01x)
$display("prints: === 逐位精确比较,把 x 也当成普通值"); // 会执行==/!=:只要有一方含x/z,结果就是x,永远不是明确的真或假。===/!==:逐位精确比较,x/z也参与比较,结果永远是明确的0或1,绝不会是x。
验证代码里经常需要明确检查一个信号是不是 x(比如确认某个信号确实被驱动过),这种场景必须用 ===/!==,普通的 ==/!= 做不到:
if (some_signal === 1'bx)
$error("some_signal was never driven!");归约运算符(reduction operators)
在单个操作数前面加上 &、|、^(以及它们的取反形式 ~&、~|、~^),会把这个向量的所有位依次做该运算,归约(reduce)成一个 1 位结果:
logic [7:0] data = 8'b1011_0110;
$display("reduction AND = %b", &data); // 只有 8 位全是 1,结果才是 1
$display("reduction OR = %b", |data); // 只要有任意一位是 1,结果就是 1
$display("reduction XOR = %b", ^data); // 把 8 位依次异或,常用于奇偶校验归约异或 ^data 尤其常见——它统计的是"1 的个数是奇数还是偶数",是实现奇偶校验(parity)最简洁的写法(第 9 章的奇偶校验函数用的就是这个运算符)。
拼接与复制运算符:{} 与 {n{...}}
拼接(concatenation){a, b, c} 把几个信号首尾相连,拼成一个更宽的向量;复制(replication){n{expr}} 把 expr 重复 n 次再拼接起来:
logic [3:0] hi = 4'hA;
logic [3:0] lo = 4'hB;
logic [7:0] combined;
combined = {hi, lo}; // 拼接:8'hAB
combined = {8{1'b0}}; // 复制:8 个 0,即 8'h00
combined = {4{2'b10}}; // 复制:4 次 "10",拼成 8'b10101010拼接和复制都能直接写在赋值语句左边或右边,也可以嵌套组合(比如 {a, {4{1'b0}}, b}),是搭建/拆解总线数据时很常用的写法。
自增自减运算符:++ 和 --
Verilog 里没有 ++/--,只能写 i = i + 1;SystemVerilog 补上了这两个运算符,用法和大多数语言一样支持前置、后置:
int i = 0;
i++; // 等价于 i = i + 1,i 变为 1
++i; // 同样让 i 自增 1
i--; // i 自减 1有一点容易被误解,需要说清楚:++/-- 语法上确实可以出现在更大的表达式里(比如 arr[idx++] = x; 就是常见的合法写法),但如果同一个表达式里同一个变量被读写了不止一次(比如 y = i++ + i;),具体的求值顺序语言标准里没有规定,不同仿真器可能给出不一样的结果。为了避免这种潜在的歧义,本系列的约定是:++/-- 只当独立语句使用,不嵌入到其他表达式里——这样写出来的代码永远不会有求值顺序的问题:
i++; // ✓ 推荐:作为独立语句,没有歧义
y = i++; // ⚠ 避免:语法上合法,但把自增揉进赋值表达式不是本系列推荐的风格流操作符(streaming operators)
流操作符 { << {...} } 和 { >> {...} } 用来按指定的位宽分组,重新"打包"一段数据——最常见的用途是字节序转换(byte swap):
logic [31:0] data, swapped;
initial begin
data = 32'h1122_3344;
swapped = { << 8 {data} }; // 按 8 位(1 字节)分组,反转组的顺序
$display("swapped = %h", swapped); // 44332211
end{ << 8 {data} } 的意思是:把 data 按 8 位一组切开,再把这些组的顺序反转过来重新拼接——四个字节 11 22 33 44 就变成了 44 33 22 11,正是常见的大端/小端字节序转换。流操作符还常用于在"一整块数据"和"拆分成若干字段/数组元素"之间转换,避免手写循环去逐位/逐字节搬运。
小结
if/else、for/while/do...while和大多数编程语言一致;SystemVerilog 额外提供repeat(N)(固定次数循环)和forever(无限循环,常用于产生时钟)。case语句每个分支执行完自动结束,没有 C 语言那种隐式穿透,不需要break。unique case/priority case让工具在开发阶段就检查分支是否互斥、是否穷尽,有助于提前发现漏写分支导致的意外锁存器。- 按位运算符(
&/|/^/~)逐位运算,结果和操作数同宽;逻辑运算符(&&/||/!)把整个操作数当真假值,结果永远是 1 位——两者符号相似但含义不同,容易写错。 ==/!=只要任意一方含x/z,结果就是x(if会当成假);===/!==逐位精确比较、把x/z也当普通值匹配,结果永远是明确的0/1,是检查信号是否为x的正确方式。- 归约运算符(如
^data)把一个向量的所有位归约成 1 位结果,^(归约异或)常用于奇偶校验。 - 拼接
{a, b}把信号拼成更宽的向量,复制{n{expr}}把expr重复 n 次再拼接,常用于搭建/拆解总线数据。 ++/--是 SystemVerilog 新增的运算符;语法上可以出现在表达式里,但同一变量在同一表达式中被多次读写时求值顺序未定义,因此约定只当独立语句使用。- 流操作符
{ << {...} }/{ >> {...} }能按指定位宽分组并重排数据,常用于字节序转换等场景。
关于 SystemVerilog 的 case 语句,以下哪项描述正确?
在 case 关键字前加上哪个关键字,可以让工具检查各分支是否互斥且穷尽,命中零个或多个分支时报警告?(英文小写)
关于流操作符 { << 8 {data} },以下哪种描述最准确?
logic [3:0] a = 4'b1100; logic [3:0] b = 4'b1010; 之后,a & b 和 a && b 的区别是什么?
对一个多位向量取归约异或(比如 ^data),最常见的用途是实现什么?
data 是一个含有 x 的 4 态信号。if (data == 4'bx01x) 这行代码会发生什么?