SystemVerilog 基础

第 7 章 · 共 16 章

运算符与过程语句

快速过一遍 SystemVerilog 里和你已经熟悉的 if/case/循环相同的部分,重点讲清楚几个 SV 特有的新东西:repeat/forever 循环、unique/priority case、按位与逻辑运算符、4 态全等比较 ===/!==、自增自减运算符,以及流操作符。

如果你写过任何一门主流编程语言,if/elsecase(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 的时钟
end

repeat(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 casepriority case:让工具帮你检查分支质量

普通 case 语句不会检查你的分支是否穷尽(覆盖了所有可能取值)或互斥(多个分支同时匹配时该怎么办)——分支没写全,最常见的后果就是无意间生成了一个锁存器(latch),这类 bug 往往很隐蔽。SystemVerilog 提供了两个修饰关键字,让仿真器和综合工具在编译期/仿真期就帮你检查:

unique case (sel)
  2'b00: y = a;
  2'b01: y = b;
  2'b10: y = c;
  2'b11: y = d;
endcase
  • unique 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 非零,结果是 1

a & b 是把 ab 的每一位分别做与运算,结果还是一个 4 位向量;a && b 则是先分别判断 ab 是否为非零(真),再对这两个布尔值做逻辑与,结果永远是 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 也参与比较,结果永远是明确的 01,绝不会是 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/elsefor/while/do...while 和大多数编程语言一致;SystemVerilog 额外提供 repeat(N)(固定次数循环)和 forever(无限循环,常用于产生时钟)。
  • case 语句每个分支执行完自动结束,没有 C 语言那种隐式穿透,不需要 break
  • unique case/priority case 让工具在开发阶段就检查分支是否互斥、是否穷尽,有助于提前发现漏写分支导致的意外锁存器。
  • 按位运算符(&/|/^/~)逐位运算,结果和操作数同宽;逻辑运算符(&&/||/!)把整个操作数当真假值,结果永远是 1 位——两者符号相似但含义不同,容易写错。
  • ==/!= 只要任意一方含 x/z,结果就是 xif 会当成假);===/!== 逐位精确比较、把 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) 这行代码会发生什么?