3. SystemVerilog 验证基础(上):数据类型、数组、结构体、枚举、字符串、接口
各位好,欢迎来到验证基础的第一部分。说实话,很多刚入行的朋友觉得 SystemVerilog 就是 Verilog 加了个“System”前缀,没什么大不了的。但我在项目中吃过不少亏,才明白这些基础类型用不好,后期 debug 能让你怀疑人生。今天咱们就把这些地基打牢。
3.1 数据类型:从 wire/reg 到 logic 的进化
Verilog 时代,wire 和 reg 的区分让无数人头疼。你写个 reg 信号,综合出来可能连个寄存器都没有。SystemVerilog 引入了 logic 类型,彻底解决了这个尴尬。
核心原则:在验证环境中,90% 的场景用 logic 就够了。它既能当线网,也能当变量,综合工具会自动判断。
// 传统 Verilog 写法
wire clk;
reg [7:0] data;
reg valid;
// SystemVerilog 推荐写法
logic clk;
logic [7:0] data;
logic valid;
我个人习惯在顶层模块的端口声明里仍然用 wire,但在验证组件(driver、monitor)内部全部用 logic。这样代码更干净,也少了很多类型转换的麻烦。
注意:logic 不能有多个驱动源。如果你需要三态总线或多驱动场景,还是得用 wire。我曾经有个项目,实习生把双向总线全改成 logic,仿真直接 X 态满天飞。
3.2 数组:定宽、动态、队列与关联数组
SystemVerilog 的数组家族比 Verilog 丰富太多了。咱们一个一个说。
3.2.1 定宽数组与动态数组
定宽数组就是 Verilog 的老朋友,编译时大小就固定了。动态数组则可以在运行时调整大小,用 new[] 分配空间。
// 定宽数组
logic [7:0] fixed_arr [0:15]; // 16个元素,编译时确定
// 动态数组
logic [7:0] dyn_arr[]; // 声明时未分配空间
dyn_arr = new[16]; // 运行时分配16个元素
dyn_arr = new[32](dyn_arr); // 重新分配为32个,保留前16个值
动态数组在验证环境里特别有用。比如你要模拟一个可变深度的 FIFO,用动态数组就比定宽数组灵活得多。
3.2.2 队列:我最喜欢的数据结构
队列(queue)结合了链表和数组的优点。它支持在头部和尾部快速插入/删除,而且可以像数组一样随机访问。
int q[$]; // 声明一个整数队列
q.push_back(10); // 尾部插入
q.push_front(5); // 头部插入
int val = q.pop_back(); // 尾部取出
int val = q.pop_front(); // 头部取出
我的经验:在 scoreboard 里做数据比对时,队列几乎是标配。把预期数据压入队列,DUT 输出一个就 pop 一个比对,逻辑清晰又高效。
3.2.3 关联数组
关联数组用索引来访问,索引可以是任意数据类型。说白了就是个哈希表。
// 以字符串为索引的关联数组
int score[string];
score["Alice"] = 95;
score["Bob"] = 87;
// 检查是否存在
if (score.exists("Charlie"))
$display("Found Charlie");
我在做寄存器模型验证时,经常用关联数组来存储所有寄存器的地址和期望值。这样查找起来比遍历数组快得多。
3.3 结构体与联合体
结构体(struct)让你能把相关的信号打包在一起。联合体(union)则让同一块内存有不同的解读方式。
// 结构体定义
typedef struct packed {
logic valid;
logic [7:0] addr;
logic [31:0] data;
} packet_t;
packet_t pkt;
pkt.valid = 1'b1;
pkt.addr = 8'hA0;
pkt.data = 32'hDEAD_BEEF;
打包 vs 非打包:打包结构体(packed)在内存中是连续的,可以整体做位操作。非打包结构体每个成员独立存储,适合描述不同宽度的信号组合。
联合体我用的相对少,但在解析协议头时特别好用。比如一个 32 位的指令,你可以用联合体同时定义它的位域视图和原始值视图。
3.4 枚举:让状态机不再神秘
枚举类型给常量取个有意义的名字,代码可读性直接拉满。
typedef enum logic [1:0] {
IDLE = 2'b00,
READ = 2'b01,
WRITE = 2'b10,
ERROR = 2'b11
} state_t;
state_t current_state, next_state;
枚举的好处不止是好看。仿真器会检查枚举变量的赋值是否合法,防止你写出 current_state = 2'b11 这种硬编码。我曾经在 debug 一个状态机死锁问题时,就是靠枚举的合法性检查找到了一个非法状态跳转。
避坑指南:枚举默认是 int 类型,宽度 32 位。如果你要综合,记得显式指定宽度,比如 enum logic [1:0]。否则综合工具会报一堆警告。
3.5 字符串:不只是文本
SystemVerilog 的字符串类型比 Verilog 的 reg [8*N:1] 好用太多了。它支持动态长度、内建方法,还能直接比较。
string s1 = "Hello";
string s2 = "World";
// 拼接
string s3 = {s1, " ", s2}; // "Hello World"
// 方法
int len = s1.len(); // 5
string upper = s1.toupper(); // "HELLO"
string sub = s1.substr(1,3); // "ell"
// 比较
if (s1 == "Hello")
$display("Match!");
我在写 UVM 环境的报告机制时,大量使用字符串来构建格式化的日志信息。配合 $sformatf 函数,可以像 C 语言一样格式化输出。
3.6 接口(Interface):模块间通信的利器
接口是 SystemVerilog 对 Verilog 最革命性的改进之一。它把一组相关的信号封装在一起,让模块间的连接变得清晰且可维护。
interface bus_if(input logic clk, rst_n);
logic [31:0] addr;
logic [31:0] wdata;
logic [31:0] rdata;
logic we;
logic re;
logic ready;
// 时钟块,控制时序
modport master (
input clk, rst_n,
output addr, wdata, we, re,
input rdata, ready
);
modport slave (
input clk, rst_n,
input addr, wdata, we, re,
output rdata, ready
);
endinterface
接口的核心价值:当你需要修改总线协议时,只需要改接口定义,所有使用该接口的模块自动更新。这在大型项目中能节省数天的联调时间。
我个人习惯在接口里加上断言(assertion)来检查协议时序。比如地址有效时 valid 信号必须拉高,这种检查放在接口里比分散在各个模块里要优雅得多。
3.7 知识体系总览
下面这张图把本章的核心知识点串了起来,方便你建立整体认知。
3.8 避坑总结
最后,我把这些年踩过的坑整理成一张表,你写代码时对照着检查,能省不少 debug 时间。
| 知识点 | 常见错误 | 正确做法 |
|---|---|---|
| logic 类型 | 多驱动源使用 logic | 三态总线用 wire |
| 动态数组 | 未分配空间就访问 | 先 new[] 再使用 |
| 队列 | 用索引访问空队列 | 先检查 size() 或使用 exists() |
| 枚举 | 未指定宽度导致综合警告 | 显式声明 enum logic [W-1:0] |
| 接口 | modport 方向定义错误 | 从 master/slave 视角分别定义 |