第二章:验证语言基础——SystemVerilog简介、数据类型、操作符、过程语句与任务函数
各位同学好,今天我们正式进入SystemVerilog的世界。说实话,我刚从Verilog转到SV时,心里还有点抵触——觉得不就是加了些语法糖吗?后来真用上了,才发现自己当初太年轻了。SV不是简单的升级,它把验证从「描述电路」提升到了「描述行为」的层面。
2.1 SystemVerilog简介
SystemVerilog,说白了就是Verilog的超级增强版。它保留了Verilog所有的硬件描述能力,同时加入了面向对象编程、断言、随机化等验证专用特性。
我个人习惯把SV看作两部分的结合:
- 设计部分:和Verilog基本一致,用来描述RTL电路
- 验证部分:新增的类、约束、覆盖率等,用来搭建测试平台
你想想看,以前用Verilog写testbench,得手动生成所有激励,检查波形靠肉眼。有了SV之后,我们可以用随机约束自动产生测试向量,用断言自动检查协议——这才是现代验证该有的样子。
核心观点:SystemVerilog不是一门新语言,它是Verilog的延续。如果你已经会Verilog,那SV的语法你至少已经掌握了一半。
我在项目中遇到过一位同事,他坚持用纯Verilog写验证环境,结果一个AHB总线协议检查写了3000行。我用SV的断言,30行搞定。嗯,从那以后他也开始学SV了。
2.2 数据类型
SV的数据类型比Verilog丰富得多。这里我挑几个最常用的讲。
2.2.1 基本数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
| bit | 二值逻辑(0/1) | bit [7:0] data; |
| logic | 四值逻辑(0/1/X/Z) | logic [31:0] addr; |
| int | 32位有符号整数 | int count; |
| string | 字符串类型 | string name = "ahb"; |
这里有个坑,我刚开始用SV时踩过——logic和wire的区别。logic可以被过程赋值(always块里),也可以被连续赋值(assign)。但wire只能被连续赋值。如果你在always块里给wire赋值,编译器会报错。
注意:在验证环境中,我建议你尽量用logic代替wire。原因很简单——logic更灵活,而且验证环境不需要像RTL那样纠结于综合问题。
2.2.2 数组与队列
SV的数组比Verilog强大得多。我最喜欢的是动态数组和队列。
// 动态数组:运行时确定大小
int dyn_arr[];
dyn_arr = new[10]; // 分配10个元素
// 队列:类似C++的deque
int queue[$];
queue.push_back(5);
queue.push_front(3);
int val = queue.pop_back();
为什么要用队列?我在写AXI验证组件时,经常需要缓存多个未完成的交易。队列的push/pop操作比手动维护数组索引方便太多了。
2.2.3 结构体与联合体
结构体可以把相关的信号打包在一起,让代码更整洁。
typedef struct {
logic [31:0] addr;
logic [31:0] data;
logic valid;
} trans_t;
trans_t t;
t.addr = 32'h1000;
t.data = 32'hA5A5;
t.valid = 1'b1;
我个人习惯在验证环境中大量使用typedef定义的结构体。你想想看,如果每个函数都传一堆散乱的信号,代码维护起来多痛苦。
2.3 操作符
SV的操作符和C语言很像,但有几个特殊的需要留意。
2.3.1 赋值操作符
=:阻塞赋值,用在组合逻辑或验证代码中<=:非阻塞赋值,用在时序逻辑中
我曾经在验证环境里用非阻塞赋值给一个变量,结果仿真行为完全不对。查了半天才发现——验证环境里大部分赋值都应该用阻塞赋值,除非你在模拟寄存器行为。
2.3.2 逻辑与位操作
| 操作符 | 含义 | 示例 |
|---|---|---|
&& |
逻辑与 | if (a && b) |
& |
位与 | c = a & b; |
=== |
全等比较(包含X/Z) | if (a === 1'bx) |
这里要特别强调===和==的区别。普通==遇到X或Z会返回未知,而===会精确比较。我在调试总线协议时,经常用===来检查信号是否出现了不该有的X态。
2.4 过程语句
过程语句是SV中描述行为逻辑的核心。常用的有always、initial、always_comb、always_ff等。
2.4.1 always_comb 与 always_ff
SV引入了这两个专用过程块,比传统的always @(*)更明确。
// 组合逻辑
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和always_ff。它们不仅语义清晰,而且仿真器能帮你检查是否误用了锁存器。
2.4.2 initial块
initial块是验证环境的起点。测试平台的初始化、时钟生成、激励发送都在这里完成。
initial begin
// 初始化信号
rst_n = 0;
#100 rst_n = 1;
// 发送激励
send_transaction();
// 等待仿真结束
#1000 $finish;
end
嗯,这里要注意——initial块在硬件综合时会被忽略,所以它只用于仿真验证。
2.5 任务与函数
任务和函数是SV中组织代码的基本单元。它们很像C语言中的函数,但有一些关键区别。
2.5.1 函数(function)
- 必须返回一个值
- 不能包含时序控制(如#延迟、@事件)
- 默认是静态的,所有变量共享
function automatic int calc_parity(logic [7:0] data);
int parity = 0;
for (int i = 0; i < 8; i++)
parity ^= data[i];
return parity;
endfunction
为什么这里用了automatic?如果不加,函数默认是静态的,递归调用时会出问题。我曾经在写CRC计算函数时忘了加automatic,结果仿真结果时对时错,查了两天才发现是静态变量在作怪。
2.5.2 任务(task)
- 可以没有返回值
- 可以包含时序控制
- 适合描述总线操作等需要时间的行为
task automatic read_bus(input logic [31:0] addr,
output logic [31:0] data);
// 发起读请求
addr_valid = 1;
addr_out = addr;
@(posedge clk);
addr_valid = 0;
// 等待响应
@(posedge ready);
data = data_in;
endtask
你想想看,如果没有任务,每次读总线都要重复写一堆时序控制代码。有了任务,一行read_bus(addr, data);就搞定了。
经验之谈:在验证环境中,我通常用函数做纯计算(如CRC、校验),用任务做时序操作(如总线读写)。这样代码结构清晰,也方便复用。
2.6 本章知识体系
下面这张图展示了本章的核心知识结构,我把它画成了SVG,方便你对照复习。
这张图把本章内容分成了四大块:数据类型、操作符、过程语句、任务与函数。你可以用它做复习提纲,看看哪个部分还不熟。
可可精品资料库