2、SystemVerilog基础回顾:数据类型、数组、结构体、枚举、字符串

各位同学,咱们今天聊聊SystemVerilog的基础数据类型。说实话,这部分内容看起来简单,但我在项目里见过太多人在这上面栽跟头了。你想想看,如果连数据类型都搞不清楚,后面写验证环境的时候,那真是寸步难行。

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

老Verilog用户应该都记得,以前我们只有regwire。但SystemVerilog引入了logic类型,我个人习惯是:能用logic的地方绝不用reg。为什么?因为logic既能当寄存器用,也能当线网用,省心。

核心要点:

  • logic:四值逻辑(0,1,X,Z),默认是wire行为
  • bit:二值逻辑(0,1),适合做验证用的临时变量
  • intbyteshortintlongint:有符号整型
  • real:浮点数,慎用,综合不支持
// 我常用的声明方式
logic [31:0] data_bus;      // 四值逻辑,适合总线
bit [7:0]    temp_val;      // 二值逻辑,验证用
int          counter;       // 有符号32位
byte         char_data;     // 有符号8位

我的小技巧:在写testbench时,我习惯把所有的信号都声明成logic。除非明确知道这个信号只会在验证环境中用,我才用bit。这样万一要跟DUT对接,不会出现类型不匹配的问题。

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

数组这块,我建议你分清楚三种场景。定宽数组适合固定大小的存储,动态数组适合运行时才知道大小,关联数组适合稀疏存储。我曾经在一个项目中,用定宽数组存了1000个元素,结果后来需求变了要存2000个,改得我头大。从那以后,只要不是确定不变的大小,我都用动态数组。

2.2.1 定宽数组

// 声明方式
int array_fixed [10];          // 10个元素的数组
int array_2d [3][4];           // 3行4列的二维数组

// 初始化
array_fixed = '{0,1,2,3,4,5,6,7,8,9};

2.2.2 动态数组

// 声明和分配
int dyn_array[];
dyn_array = new[20];           // 分配20个元素

// 重新分配
dyn_array = new[50] (dyn_array); // 保留原内容,扩展到50个

// 常用方法
dyn_array.size();              // 返回数组大小
dyn_array.delete();            // 清空数组

注意:动态数组用new[]分配后,记得检查返回值。我遇到过因为内存不足导致分配失败,仿真直接崩溃的情况。虽然概率低,但养成好习惯总没错。

2.2.3 关联数组

关联数组说白了就是用索引来查找数据,索引可以是任意类型。我在做地址映射表时特别喜欢用这个,因为地址空间很大,但实际用到的地址很少,用定宽数组太浪费了。

// 声明
int associative_array [string];  // 用字符串做索引
bit [63:0] addr_map [int];      // 用整数做索引

// 使用
associative_array["key1"] = 100;
associative_array["key2"] = 200;

// 遍历
foreach (associative_array[i]) begin
  $display("key: %s, value: %0d", i, associative_array[i]);
end

2.3 结构体:把相关数据打包在一起

结构体这东西,说白了就是把一堆相关的信号捆在一起。我在做AXI总线验证时,把地址、数据、控制信号都放在一个结构体里,代码看起来清爽多了。

// 定义结构体
typedef struct {
  logic [31:0] address;
  logic [31:0] data;
  logic        valid;
  logic        ready;
} axi_channel_t;

// 声明和使用
axi_channel_t aw_channel;
aw_channel.address = 32'h1000;
aw_channel.data    = 32'hA5A5A5A5;
aw_channel.valid   = 1'b1;

经验之谈:我建议你给结构体加上typedef,这样后面用起来方便。另外,结构体可以嵌套,比如把多个通道的结构体再打包成一个更大的结构体,这在复杂协议验证中非常实用。

2.4 枚举:让代码更可读

枚举类型,说白了就是给数字起个名字。你想想看,代码里到处都是3'b0013'b010,过两天你自己都看不懂了。用枚举,代码自文档化,多好。

// 定义枚举
typedef enum logic [2:0] {
  IDLE    = 3'b000,
  READ    = 3'b001,
  WRITE   = 3'b010,
  WAIT    = 3'b011,
  DONE    = 3'b100
} state_t;

// 使用
state_t current_state, next_state;

// 枚举方法
current_state.first();    // 返回第一个枚举值
current_state.last();     // 返回最后一个枚举值
current_state.next();     // 返回下一个枚举值

我的习惯:枚举值一定要显式赋值,不要依赖默认值。为什么?因为不同编译器对默认值的处理可能不一样。我曾经吃过这个亏,代码在VCS上跑得好好的,换到Questa就出问题了。从那以后,我每个枚举都老老实实赋值。

2.5 字符串:不只是文本

SystemVerilog的字符串处理能力比Verilog强太多了。我记得以前用Verilog做字符串拼接,那叫一个痛苦。现在好了,有string类型,各种方法随便用。

// 声明和赋值
string str1 = "Hello";
string str2 = "World";
string str3;

// 拼接
str3 = {str1, " ", str2};  // "Hello World"

// 常用方法
str3.len();                 // 返回长度
str3.tolower();             // 转小写
str3.toupper();             // 转大写
str3.substr(0, 4);          // 取子串 "Hello"
str3.compare("Hello");      // 比较,相等返回0

// 格式化
$sformat(str3, "Value = %0d", 42);

注意:字符串比较用==或者compare()方法都可以。但如果你要忽略大小写比较,记得先用tolower()toupper()统一格式。我见过有人直接比较"Hello"和"hello",结果死活对不上,查了半天才发现是大小写的问题。

2.6 类型转换:小心数据丢失

类型转换这块,我建议你记住一个原则:显式转换比隐式转换安全。虽然SystemVerilog会自动做类型转换,但有时候结果可能不是你想要的。

// 显式转换
int a = 42;
bit [7:0] b = bit' (a);      // 显式转换为8位

// 四值转二值
logic [3:0] c = 4'b01xz;
bit [3:0] d = bit' (c);      // X和Z会变成0

// 有符号无符号转换
int signed_val = -5;
bit [31:0] unsigned_val = unsigned' (signed_val);

避坑指南:我曾经在项目中用$cast()做类型转换,结果因为目标类型范围不够大,数据被截断了。从那以后,我每次做类型转换前都会先检查一下范围,或者用$cast()配合错误处理。

2.7 小结

好了,这一章的内容就这些。数据类型、数组、结构体、枚举、字符串,这些都是SystemVerilog验证的基础。你想想看,如果连这些都不熟练,后面写UVM环境的时候,光是数据类型匹配就能把你搞疯。

我个人建议你多写多练,尤其是动态数组和关联数组,这两个在实际项目中用得最多。结构体和枚举,能帮你写出可读性更好的代码。字符串处理,在写自动化脚本和日志分析时特别有用。

下一章我们会聊到面向对象编程,那是SystemVerilog验证的另一个核心。到时候你会发现,今天学的这些数据类型,在类里面都会用到。所以,打好基础很重要。