2、SystemVerilog基础回顾:数据类型、数组、结构体、枚举、字符串
各位同学,咱们今天聊聊SystemVerilog的基础数据类型。说实话,这部分内容看起来简单,但我在项目里见过太多人在这上面栽跟头了。你想想看,如果连数据类型都搞不清楚,后面写验证环境的时候,那真是寸步难行。
2.1 数据类型:从Verilog到SystemVerilog的进化
老Verilog用户应该都记得,以前我们只有reg和wire。但SystemVerilog引入了logic类型,我个人习惯是:能用logic的地方绝不用reg。为什么?因为logic既能当寄存器用,也能当线网用,省心。
核心要点:
logic:四值逻辑(0,1,X,Z),默认是wire行为bit:二值逻辑(0,1),适合做验证用的临时变量int、byte、shortint、longint:有符号整型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'b001、3'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验证的另一个核心。到时候你会发现,今天学的这些数据类型,在类里面都会用到。所以,打好基础很重要。