3. SystemVerilog基础回顾(一):数据类型、数组、结构体、枚举、字符串
各位同学,咱们今天聊聊SystemVerilog的基础数据类型。说实话,很多验证工程师写了几年代码,对这些基础概念还是一知半解。我刚开始带项目时也踩过不少坑,今天就把这些经验掰开揉碎了讲给你们听。
3.1 数据类型:从Verilog到SystemVerilog的进化
Verilog时代,我们只有reg和wire。但到了SystemVerilog,数据类型丰富了很多。我个人习惯把数据类型分成两大类:二值逻辑和四值逻辑。
核心区别:
- 二值逻辑:
bit、byte、int、shortint、longint——只有0和1 - 四值逻辑:
logic、reg、integer、time——有0、1、X、Z
为什么要有这个区分?说白了,仿真效率。二值逻辑在仿真时占用内存更少,跑得更快。我在项目中做过对比,同样的测试用例,用bit比用logic快大概15%。但要注意,如果你需要模拟X态或高阻态,那就必须用四值逻辑。
我的建议:验证环境中的控制信号、状态机变量,尽量用bit或int。只有需要和DUT接口交互的信号,才用logic。这样既保证效率,又不失准确性。
3.2 数组:定宽数组、动态数组、关联数组
数组这块,很多新手容易搞混。我给你们画个图,一目了然。
三种数组各有各的用途。我给你们总结一下:
| 数组类型 | 特点 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 定宽数组 | 大小固定,编译时确定 | 配置寄存器、固定大小的FIFO | 曾经在循环中越界访问,仿真没报错但结果全错 |
| 动态数组 | 运行时分配,可调整大小 | 数据包队列、不确定数量的激励 | 忘记调用new[]就访问,直接崩溃 |
| 关联数组 | 稀疏存储,按索引访问 | 地址映射、配置表、查找表 | 用字符串做索引时大小写搞混,查了半天 |
注意:动态数组用new[]分配后,记得用delete()释放。我曾经在一个跑24小时的回归测试中,因为动态数组没释放,内存涨到32GB,服务器直接挂了。嗯,从那以后我每次用完都检查。
3.3 结构体:把相关数据打包
结构体这东西,说白了就是把一堆相关的信号打包在一起。比如一个AXI总线的地址、数据、控制信号,你想想看,如果每个信号都单独声明,代码得多乱?
// 不好的做法
logic [31:0] addr;
logic [31:0] wdata;
logic [31:0] rdata;
logic valid;
logic ready;
// 好的做法
typedef struct packed {
logic [31:0] addr;
logic [31:0] wdata;
logic [31:0] rdata;
logic valid;
logic ready;
} axi_bus_t;
axi_bus_t bus; // 一个变量搞定
我个人习惯用typedef定义结构体类型,这样复用起来方便。而且packed关键字可以让结构体在内存中连续存储,方便做位操作。
小技巧:结构体可以嵌套。比如你定义一个packet_t,里面包含header_t和payload_t。这样层次清晰,代码可读性高。我在做网络协议验证时,经常这么用。
3.4 枚举:让状态机更优雅
枚举类型,我觉得是SystemVerilog最被低估的特性之一。很多工程师还在用parameter定义状态,其实枚举好得多。
// 老式做法
parameter IDLE = 2'b00;
parameter READ = 2'b01;
parameter WRITE = 2'b10;
parameter DONE = 2'b11;
reg [1:0] state;
// 枚举做法
typedef enum logic [1:0] {
IDLE,
READ,
WRITE,
DONE
} state_t;
state_t state;
为什么我推荐枚举?三个理由:
- 自动编码:不用手动分配数值,编译器自动处理
- 类型安全:不能随便给枚举变量赋非法值
- 调试方便:波形里显示的是状态名,不是二进制数
避坑指南:我曾经在项目中用枚举定义状态机,但忘了加logic [1:0]的位宽声明。结果默认是int类型,仿真器给每个状态分配了32位,波形里看状态值全是0和1以外的数,排查了好久才发现。所以记得显式指定位宽。
3.5 字符串:不只是文本
字符串在验证中太常用了。打印日志、拼接文件名、处理命令行参数,哪哪都离不开它。
string name;
string prefix = "test_";
string suffix = ".log";
name = {prefix, "case_001", suffix}; // 拼接
$display("Running: %s", name);
// 常用方法
int len = name.len(); // 长度
int pos = name.find("case"); // 查找
string sub = name.substr(0,4); // 子串
name.tolower(); // 转小写
字符串拼接用{}操作符,这个和Verilog的位拼接一样,但注意字符串拼接不能混用其他类型。我刚开始用的时候,试过把int直接拼到字符串里,结果报错。正确的做法是用$sformatf格式化。
实用技巧:用$sformatf生成动态文件名:
string filename;
int test_id = 42;
filename = $sformatf("result_%0d.log", test_id);
好了,这一章的基础知识就讲到这里。数据类型、数组、结构体、枚举、字符串,这些都是验证环境的砖瓦。你想想看,如果连砖瓦都搞不清楚,怎么盖得起UVM这座大厦?下一章我们继续深入,聊聊接口和时钟块。记住,基础不牢,地动山摇。