3. SystemVerilog基础回顾(一):数据类型、数组、结构体、枚举、字符串

各位同学,咱们今天聊聊SystemVerilog的基础数据类型。说实话,很多验证工程师写了几年代码,对这些基础概念还是一知半解。我刚开始带项目时也踩过不少坑,今天就把这些经验掰开揉碎了讲给你们听。

3.1 数据类型:从Verilog到SystemVerilog的进化

Verilog时代,我们只有regwire。但到了SystemVerilog,数据类型丰富了很多。我个人习惯把数据类型分成两大类:二值逻辑四值逻辑。

核心区别:

  • 二值逻辑bitbyteintshortintlongint——只有0和1
  • 四值逻辑logicregintegertime——有0、1、X、Z

为什么要有这个区分?说白了,仿真效率。二值逻辑在仿真时占用内存更少,跑得更快。我在项目中做过对比,同样的测试用例,用bit比用logic快大概15%。但要注意,如果你需要模拟X态或高阻态,那就必须用四值逻辑。

我的建议:验证环境中的控制信号、状态机变量,尽量用bitint。只有需要和DUT接口交互的信号,才用logic。这样既保证效率,又不失准确性。

3.2 数组:定宽数组、动态数组、关联数组

数组这块,很多新手容易搞混。我给你们画个图,一目了然。

SystemVerilog 数组分类 定宽数组 编译时确定大小 动态数组 运行时分配大小 关联数组 稀疏存储,按索引 int arr[10]; // 定宽 int dyn[]; // 动态 dyn = new[20]; // 分配 int assoc[string]; // 关联 assoc["key1"] = 42; // 赋值

三种数组各有各的用途。我给你们总结一下:

数组类型 特点 适用场景 我踩过的坑
定宽数组 大小固定,编译时确定 配置寄存器、固定大小的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_tpayload_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这座大厦?下一章我们继续深入,聊聊接口和时钟块。记住,基础不牢,地动山摇。

专注资料整理