2. SystemVerilog验证语言核心
验证平台与RTL设计最大的差别之一,是验证代码要频繁地“造数据、存数据、查数据、比数据”。SystemVerilog在Verilog基础上补齐了软件语言级别的数据结构能力,本节围绕验证环境中最常用的七类对象展开:二值/四值数据类型、定长与动态数组、队列、关联数组、结构体、枚举和字符串。理解它们的存储模型与访问代价,才能写出既正确又高效的验证代码。
一、数据类型:二值态与四值态的取舍
Verilog只有四值逻辑(0、1、X、Z),而SystemVerilog引入了二值类型:bit、byte、shortint、int、longint,以及对应的无符号版本。二值类型只保存0和1,仿真器可以用更紧凑的内存表示,访问更快,适合存放激励数据、计数值、循环变量等不需要表达未知态的场景。
四值类型包括logic、reg、wire、integer等,能表达X和Z,适合与DUT端口连接、采样可能未初始化的信号。一个常见的验证约定是:与DUT交互的接口信号用logic,平台内部的配置、计数、索引用int或bit。若把四值信号直接赋给二值变量,X/Z会被映射为0,可能掩盖真实问题,因此跨类型赋值时要显式判断。
logic [7:0] rdata; // 四值,接DUT输出
bit [7:0] exp_data; // 二值,存期望值
int err_cnt; // 二值,错误计数
if (rdata === 8'hxx) begin
$display("read unknown, skip compare");
end else begin
exp_data = rdata; // 已知态才转换
end
二、定长数组、动态数组与队列
定长数组在编译期确定大小,例如int fixed[4],适合元素数量固定的查找表、寄存器模型镜像。动态数组用int dyn[]声明,运行时通过new[n]分配,适合长度随激励变化的场景,如一次事务的payload。队列用int q[$]声明,两端可增删,天然适合做FIFO、待处理事务列表、scoreboard的期望队列。
三者的关键操作差异值得记住:动态数组用size()取长度、delete()清空;队列用push_back()、push_front()、pop_front()、pop_back(),并支持insert()、delete()按下标操作。队列在中间插入/删除的代价高于两端操作,若只需两端进出,优先用队列而非动态数组。
int payload[]; // 动态数组
int fifo[$]; // 队列
payload = new[8];
foreach (payload[i]) payload[i] = i;
fifo.push_back(32'hA5); // 入队
fifo.push_back(32'h5A);
int head = fifo.pop_front(); // 出队,head = A5
$display("fifo size = %0d", fifo.size()); // 1
三、关联数组:稀疏索引的利器
关联数组用int mem[string]或bit [31:0] regs[int]声明,索引可以是整数、字符串甚至类句柄。它只在实际写入时分配存储,因此非常适合地址空间巨大但实际访问稀疏的场景,例如寄存器模型、覆盖率bin、以字符串为键的配置表。
常用操作包括exists()判断键是否存在、delete()删除键、num()取元素个数,以及用foreach遍历。注意:关联数组遍历顺序不保证与插入顺序一致,若需要顺序语义,应改用队列或额外维护顺序表。
int regs[string];
regs["CTRL"] = 32'h0000_0001;
regs["STAT"] = 32'h0000_0000;
if (regs.exists("CTRL"))
$display("CTRL = %h", regs["CTRL"]);
$display("total regs = %0d", regs.num());
四、结构体与枚举:让数据自解释
结构体用typedef struct把相关字段打包成一个整体,例如把一次总线事务的地址、数据、读写方向、响应状态组合成bus_txn_t。这样在driver、monitor、scoreboard之间传递时,只需一个变量,字段含义清晰,也便于后续扩展。结构体可以嵌套,也可以包含动态数组或队列。
枚举用typedef enum定义一组具名常量,例如IDLE、READ、WRITE、RESP。枚举让状态机、事务类型、比较结果可读性大幅提升,并可用name()打印名字、first()/next()遍历。建议为枚举指定显式数值,避免默认编码变化影响覆盖率或日志分析。
typedef enum logic [1:0] {IDLE, READ, WRITE, RESP} state_e;
typedef struct {
bit [31:0] addr;
bit [31:0] data;
bit wr;
state_e st;
} bus_txn_t;
bus_txn_t txn;
txn = '{addr:32'h1000, data:32'hDEAD_BEEF, wr:1, st:WRITE};
$display("txn st = %s", txn.st.name());
五、字符串操作
SystemVerilog的string类型支持拼接、比较、查找、替换、大小写转换等操作。常用方法有len()、substr()、toupper()、tolower()、atoi()、itoa()、compare()。字符串在验证中常用于日志格式化、配置文件解析、覆盖率名称拼接。注意字符串是不可变值语义,赋值会复制内容,频繁拼接大字符串时要注意性能。
string s = "vcs";
string t = "systemverilog";
$display("%s len=%0d", t, t.len());
if (t.substr(0,5) == "system")
$display("prefix match");
$display("upper = %s", s.toupper()); // VCS
六、选型速查与本节图解
面对一个“存数据”的需求,可以按下面的判断路径选择容器:元素个数编译期固定→定长数组;运行期才知道长度且只整体访问→动态数组;需要频繁两端进出→队列;索引稀疏或键为字符串→关联数组;多个字段要绑定传递→结构体;取值来自有限集合→枚举;文本处理→字符串。下图把七类对象的存储模型、典型操作和适用场景放在同一张关系图中,便于对照记忆。
logic信号直接赋给bit变量,X/Z被静默转为0;用关联数组遍历顺序当作事务顺序;在循环中反复new[]动态数组而不复用,导致仿真变慢。遇到比对失败时,先确认容器类型与访问方式是否匹配语义。