第二章:验证语言基础——SystemVerilog简介、数据类型、过程语句、接口与模块

各位同学,欢迎来到验证语言基础这一章。说实话,SystemVerilog 这门语言,我用了十几年了。它不是什么新语言,但绝对是目前验证领域最趁手的工具。你想想看,从早期的 Verilog 到现在的 UVM,底层全是 SystemVerilog 在撑着。

这一章,我会带你快速过一遍核心基础。别担心,我们不搞教科书式的罗列。我会挑那些你在实际项目中天天要用的东西来讲。

2.1 SystemVerilog 到底是什么?

简单说,SystemVerilog 是 Verilog 的超集。它把硬件描述和验证整合在了一起。Verilog 能干的,它都能干。Verilog 干不了的,比如面向对象编程、断言、随机化,它也能干。

我个人习惯把 SystemVerilog 分成两大部分:

  • 设计部分:写 RTL 代码,跟 Verilog 几乎一样,但语法更严谨。
  • 验证部分:写 testbench、写 sequence、写 scoreboard,这才是它的精髓。

核心观点:如果你只会用 Verilog 写 testbench,那你还在石器时代。SystemVerilog 让你能用软件工程的思维来做硬件验证。

2.2 数据类型——别再用 reg 和 wire 了

很多从 Verilog 转过来的工程师,上来就写 reg [7:0] data;。我建议你改掉这个习惯。SystemVerilog 引入了 logic 类型,它基本可以替代 reg 和 wire 的大部分场景。

// 老式 Verilog 写法
reg [31:0] counter;
wire       clk;

// SystemVerilog 推荐写法
logic [31:0] counter;
logic        clk;

为什么推荐用 logic?因为它会自动判断是寄存器还是线网。你在 always 块里赋值,它就是 reg 行为;你用 assign 赋值,它就是 wire 行为。省心。

当然,也有例外。多驱动的情况(比如双向总线),你还是得用 wire。我在项目中遇到过有人全程用 logic,结果综合时报了好多警告。嗯,这里要注意。

2.2.1 两态与四态类型

Verilog 里默认是四态:0、1、X、Z。SystemVerilog 引入了两态类型:bitintbyte 等。两态类型没有 X 和 Z,仿真速度更快,内存占用更少。

类型 状态数 典型用途
logic 四态 RTL 设计、需要 X 传播检查
bit 两态 验证环境中的临时变量
int 两态 循环变量、计数器
byte 两态 数据包中的小字段

我的建议:写 testbench 时,能用两态就用两态。仿真速度快不少。但设计代码里,一定要用四态,因为 X 传播检查能帮你抓到很多 bug。

2.2.2 数组与队列

SystemVerilog 的数组比 Verilog 灵活太多了。我最常用的是动态数组和队列。

// 动态数组:大小可变
int dyn_arr[];
dyn_arr = new[10];  // 分配10个元素
dyn_arr = new[20](dyn_arr);  // 重新分配,保留原数据

// 队列:类似 C++ 的 deque
int queue[$];
queue.push_back(5);
queue.push_front(3);
int item = queue.pop_back();

队列在验证环境里特别实用。比如你要模拟一个 FIFO,直接用队列就行。我曾经用队列实现过一个 scoreboard,代码量比用数组少了至少一半。

2.3 过程语句——always_ff 和 always_comb

Verilog 里只有一个 always,靠敏感列表区分组合逻辑和时序逻辑。SystemVerilog 把它细化了:

  • always_comb:组合逻辑。工具会自动推导敏感列表。
  • always_ff:时序逻辑。明确指定时钟和复位。
  • always_latch:锁存器。说实话,我很少用,因为 latch 一般是要避免的。
// 组合逻辑
always_comb begin
  if (sel)
    out = a;
  else
    out = b;
end

// 时序逻辑
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    q <= '0;
  else
    q <= d;
end

避坑指南:我曾经在代码里漏写了 always_comb 的完整赋值,导致综合出来一堆 latch。用 always_comb 时,一定要确保所有输入都被覆盖到。不然工具会给你“惊喜”。

2.4 接口——把连线打包起来

接口(interface)是 SystemVerilog 里我最喜欢的功能之一。它把一组相关的信号打包在一起,让代码更整洁,也更容易复用。

你想想看,一个 AXI 总线有多少根线?地址、数据、控制、握手...几十根。如果每个模块都把这些线拉出来,代码会变得又臭又长。

// 定义一个简单的接口
interface bus_if(input logic clk);
  logic [31:0] addr;
  logic [31:0] data;
  logic        wr;
  logic        rd;
  logic        ready;

  // 定义时钟块
  modport master(input clk, output addr, data, wr, rd, input ready);
  modport slave(input clk, input addr, data, wr, rd, output ready);
endinterface

// 在模块中使用
module top;
  logic clk;
  bus_if bus(clk);

  master u_master(.bus(bus.master));
  slave  u_slave (.bus(bus.slave));
endmodule

接口的好处很明显:

  • 减少连线:一组信号一个接口搞定。
  • 便于修改:加一根信号,只改接口定义,不用改所有模块。
  • 支持 modport:明确方向,避免接错。

个人经验:我在做 PCIe 验证时,把整个数据链路层的接口都封装成了 interface。后来换协议版本,只改了接口定义,验证环境几乎没动。这就是接口的魅力。

2.5 模块——验证环境的基本单元

模块(module)是 Verilog 就有的概念。SystemVerilog 里,模块依然是基本单元,但用法更灵活了。

在验证环境中,模块通常用来做:

  • DUT 的顶层封装
  • 简单的 driver 或 monitor
  • 时钟和复位生成

不过,对于复杂的验证组件(比如 scoreboard、coverage collector),我建议用 class 而不是 module。为什么?因为 class 支持继承、多态,更适合做复杂的验证逻辑。

// 一个简单的时钟生成模块
module clk_gen(
  output logic clk
);
  initial begin
    clk = 0;
    forever #5 clk = ~clk;
  end
endmodule

// 一个简单的 driver 模块
module driver(
  interface bus_if
);
  initial begin
    // 驱动总线
    bus_if.addr = 32'h0;
    bus_if.data = 32'h0;
    bus_if.wr   = 0;
    #10;
    bus_if.wr   = 1;
    bus_if.addr = 32'h1000;
    bus_if.data = 32'hA5A5A5A5;
    #10;
    bus_if.wr   = 0;
  end
endmodule

小技巧:模块里的 initial 块很适合做初始化。但记得,initial 块在仿真中只执行一次。如果你需要反复执行的操作,用 always 或 forever。

2.6 本章小结

这一章我们聊了 SystemVerilog 的基础:数据类型、过程语句、接口和模块。说白了,这些都是你写验证环境时天天要用的东西。

我个人觉得,学 SystemVerilog 最好的方法就是动手写。别光看书。找个简单的设计,比如一个 FIFO 或者一个 SPI 接口,用 SystemVerilog 写个 testbench 跑一跑。遇到问题再回来翻书,印象会深得多。

下一章,我们会深入面向对象编程。到时候你会看到,class 才是 SystemVerilog 验证的核武器。