第二章:验证语言基础——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 引入了两态类型:bit、int、byte 等。两态类型没有 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 验证的核武器。