4. SystemVerilog基础(一):数据类型、操作符、过程块、赋值语句

各位同学,咱们今天开始啃SystemVerilog这块硬骨头。说实话,很多刚入行的朋友觉得SV就是Verilog加了个“System”前缀,没啥大不了的。嗯,我当年也这么想,直到第一次在项目中因为数据类型用错,导致仿真结果和实际行为对不上,debug了整整两天……从那以后,我再也不敢小看这些基础概念了。

这一章咱们重点聊四个东西:数据类型、操作符、过程块、赋值语句。别觉得基础,这些是你写验证代码的“砖瓦”,砖瓦不牢,房子迟早要塌。

4.1 数据类型:从reg到logic,再到更多

先说说数据类型。Verilog时代,我们只有reg和wire。reg用来存值,wire用来连线。但问题来了——reg就一定是寄存器吗?不是的。reg在仿真中只是一个变量,综合工具可以把它变成寄存器,也可以变成组合逻辑。这个命名误导了很多人。

SystemVerilog引入了logic类型,说白了就是统一了reg和wire。你写logic [7:0] data;,它既可以当变量用,也可以当连线用。我个人习惯,现在写验证代码几乎只用logic,除非是双向总线或者多驱动场景,那才用wire。

核心数据类型一览:

  • logic:四值逻辑(0,1,X,Z),最常用
  • bit:二值逻辑(0,1),仿真速度快,适合纯数据通路
  • int/byte/shortint/longint:有符号整型,类似C语言
  • real:浮点数,慎用,综合不支持
  • string:字符串,调试利器

举个例子,我在项目中写一个AXI总线监视器,地址信号用logic,因为要处理X态检查;数据信号用bit,因为仿真速度更快。你想想看,一个设计里几千个信号,每个省一点仿真时间,累积起来就很可观了。

4.2 操作符:别小看这些符号

操作符这块,大家基本都会。但有几个坑,我必须要讲。

算术操作符: +、-、*、/、% 这些和C语言一样。但注意,除法在硬件中很贵,能不用就不用。我曾经接手过一个项目,设计里用了大量除法,综合后面积爆炸,最后全部改成移位和查表。

逻辑操作符: &&、||、! 返回的是1bit的布尔值。而按位操作符 &、|、~ 是按位运算。这两个别搞混了。我见过一个同事在if条件里写了 if (a & b) 而不是 if (a && b),结果a和b都是多位宽,按位与之后可能是一个非零值,但if判断永远为真……debug了一下午。

关系操作符: >、<、>=、<= 这些返回也是布尔值。注意,如果操作数里有X或Z,结果会是X。所以写断言的时候,一定要先确保信号没有X态。

避坑指南:我曾经在验证一个FIFO满信号时,用了 if (wr_ptr == rd_ptr + DEPTH),结果因为指针是logic类型,仿真中出现了X态,整个比较结果变成X,满信号永远不拉高。后来我改成用 if (wr_ptr - rd_ptr >= DEPTH),并且先做X态检查,问题才解决。

4.3 过程块:always和initial

过程块是硬件描述语言的核心。SystemVerilog里主要有两种:alwaysinitial

initial块: 只在仿真开始时执行一次。主要用于初始化、生成时钟、驱动激励。注意,initial块不可综合,只能用于仿真和验证。

initial begin
  clk = 0;
  rst_n = 0;
  #100 rst_n = 1;
  #50  reset_done = 1;
end

always块: 这个就复杂了。SystemVerilog把always分成了三种:

  • always_comb:组合逻辑,敏感列表自动推导
  • always_ff:时序逻辑,带时钟沿触发
  • always_latch:锁存器,慎用

我个人强烈建议:写验证环境时,能用always_comb就别用always @(*)。为什么?因为always_comb会在仿真0时刻自动执行一次,而且敏感列表是自动推导的,不会漏信号。我见过太多因为always @(a or b)漏写了c,导致仿真结果不对的案例了。

小技巧:写驱动模型时,我习惯用 always_ff @(posedge clk or negedge rst_n) 来模拟寄存器行为。这样代码可读性高,而且综合工具也能正确识别。

4.4 赋值语句:阻塞 vs 非阻塞

这个知识点,可以说是新手最容易翻车的地方。也是面试必考题。

阻塞赋值(=): 顺序执行,前面的赋值完成才执行后面的。用在组合逻辑中。

非阻塞赋值(<=): 并行执行,所有赋值在同一个时间步结束时同时更新。用时序逻辑中。

为什么会有这种区别?你想想看,硬件里寄存器是边沿触发的,所有寄存器在同一时钟沿同时采样。非阻塞赋值正好模拟了这个行为。而组合逻辑是连续赋值的,阻塞赋值模拟的就是这种“立即生效”的特性。

特性 阻塞赋值 = 非阻塞赋值 <=
执行顺序 顺序执行 并行执行
适用场景 组合逻辑、initial块 时序逻辑、always_ff
仿真行为 立即更新 时间步结束时更新
综合结果 组合逻辑或锁存器 寄存器

我给大家看一个经典的反例:

// 错误写法:在时序逻辑中用阻塞赋值
always_ff @(posedge clk) begin
  a = b;
  c = a;  // 这里a已经是新值了,不是硬件行为!
end

// 正确写法:时序逻辑用非阻塞赋值
always_ff @(posedge clk) begin
  a <= b;
  c <= a;  // 这里a还是旧值,符合硬件行为
end

我曾经在项目中review代码,发现一个同事在always_ff块里混用了阻塞和非阻塞赋值。仿真结果看起来是对的,但综合后的网表行为完全不一样。最后流片回来,那个模块的功能就是不对。嗯,从那以后,我们团队定了一条铁律:时序逻辑只用非阻塞,组合逻辑只用阻塞,混用一律打回重写。

记住这个口诀:

组合逻辑用阻塞,时序逻辑用非阻塞。

initial块里用阻塞,always_ff里用非阻塞。

同一个always块里,不要混用两种赋值。

4.5 实战建议

学完这些基础,我建议你马上动手写一个小模块。比如一个简单的计数器,或者一个FIFO控制器。写的时候刻意练习:

  • 用logic代替reg和wire
  • 用always_comb和always_ff代替always
  • 严格区分阻塞和非阻塞赋值

只有亲手写过、踩过坑,这些知识才能真正变成你的。下一章咱们会聊到数组、结构体、枚举这些更高级的数据类型,到时候你会发现,今天的基础打得越牢,后面的学习就越轻松。

好了,这一章就到这里。有什么问题,咱们课上讨论。