第二章 SystemVerilog基础回顾:面向对象编程、随机化与约束、功能覆盖率、断言基础
各位同学,咱们直接进入正题。这一章我把它叫做“热身运动”。
你想想看,UVM 说白了就是 SystemVerilog 的一个应用框架。如果 SV 基础不牢,后面学 UVM 会非常痛苦。我自己带过不少新人,很多人一上来就啃 UVM 源码,结果被 uvm_component_utils 这些宏搞晕了。其实,底层全是面向对象那套东西。
所以,咱们花点时间,把 SV 里跟 UVM 最相关的四个核心点过一遍。嗯,这章内容不少,但都是干货。
2.1 面向对象编程(OOP)—— UVM 的骨架
UVM 整个框架就是建立在 OOP 之上的。你写的每一个 uvm_driver、uvm_monitor,本质上都是类的实例化。
2.1.1 类与对象
类就是模板,对象是实例。这个不难理解。我习惯把类比作“设计图纸”,对象就是“造出来的房子”。
class packet;
rand bit [7:0] addr;
rand bit [7:0] data;
function void display();
$display("addr=%0h, data=%0h", addr, data);
endfunction
endclass
packet pkt; // 声明句柄
pkt = new(); // 创建对象
个人习惯: 我每次写 new() 的时候,都会检查一下构造函数里有没有初始化所有成员变量。曾经有一次我忘了初始化一个队列,结果仿真跑了两小时才发现数据全是 X,血的教训。
2.1.2 继承与多态
继承是 UVM 的核心思想。你写的 my_driver extends uvm_driver,就是把父类的所有属性和方法拿过来,再添加自己的东西。
多态呢?说白了就是“同样的函数名,不同的行为”。UVM 里到处都是 build_phase、connect_phase,你重写它们,框架自动调用你的版本。
class base_test;
virtual function void build();
$display("base_test::build");
endfunction
endclass
class my_test extends base_test;
function void build();
super.build(); // 调用父类
$display("my_test::build");
endfunction
endclass
避坑指南: 我曾经在重写 build_phase 时忘了调用 super.build(),结果父类里创建的子组件全没了。嗯,从那以后我每次重写都会先写 super.build(),养成肌肉记忆。
2.1.3 句柄与对象
这是新人最容易搞混的地方。句柄不是对象,它只是指向对象的“遥控器”。
packet pkt1, pkt2;
pkt1 = new();
pkt2 = pkt1; // pkt2 和 pkt1 指向同一个对象
pkt1 = null; // 对象还在,pkt2 还能用
2.2 随机化与约束——验证的精髓
验证的本质是什么?就是产生各种合法的、非法的输入,看 DUT 怎么反应。随机化就是干这个的。
2.2.1 随机化基础
SV 里用 rand 和 randc 关键字声明随机变量。rand 每次独立随机,randc 是循环随机,所有值轮完一遍才重复。
class packet;
rand bit [7:0] addr;
randc bit [3:0] id; // 0~15 轮一遍
endclass
packet pkt = new();
repeat(10) begin
assert(pkt.randomize());
pkt.display();
end
注意: randomize() 返回 1 表示成功,0 表示失败。我建议每次都加 assert 检查,否则随机化失败了你都不知道,仿真结果全是错的。
2.2.2 约束的写法
约束就是给随机化加“规矩”。UVM 里最常用的就是 constraint 块。
class packet;
rand bit [7:0] addr;
rand bit [7:0] data;
constraint addr_range { addr inside {[0:100], [200:255]}; }
constraint data_valid { data != 8'h00; }
constraint addr_data_corr { addr % 2 == 0 -> data[0] == 1'b1; }
endclass
我个人习惯把约束分成三类:
| 约束类型 | 说明 | 示例 |
|---|---|---|
| 范围约束 | 限制取值范围 | addr inside {[0:100]} |
| 关系约束 | 变量间的关系 | addr % 2 == 0 -> data[0] == 1 |
| 分布约束 | 控制概率分布 | dist {0:=1, [1:10]:/9} |
避坑指南: 我曾经写过一个约束 addr > 10 && addr < 5,结果随机化永远失败。仿真器不会告诉你约束矛盾,你得自己排查。建议用 solve...before 控制求解顺序,避免约束冲突。
2.3 功能覆盖率——你到底测了多少?
功能覆盖率回答一个问题:“你的测试用例覆盖了所有功能点吗?”
2.3.1 覆盖组与覆盖点
covergroup 是 SV 里定义覆盖率的结构。一个覆盖组可以包含多个 coverpoint。
covergroup packet_cg @(posedge clk);
addr_cp: coverpoint pkt.addr {
bins low = {[0:50]};
bins mid = {[51:150]};
bins high = {[151:255]};
bins zero = {0};
}
data_cp: coverpoint pkt.data;
cross_cp: cross addr_cp, data_cp;
endgroup
packet_cg cg = new();
// 每次采样时调用 cg.sample();
为什么需要交叉覆盖率?因为单独看 addr 和 data 可能都覆盖了,但 addr=0 且 data=0 这个组合可能从来没出现过。交叉覆盖率就是抓这种“组合漏洞”的。
2.3.2 覆盖率驱动的验证
我习惯在仿真结束后看覆盖率报告。如果某个 bin 是 0%,说明这个场景没测到。这时候就要调整约束,或者写定向用例去覆盖。
个人经验: 覆盖率不是越高越好。我曾经为了把交叉覆盖率跑到 100%,写了 200 个定向用例,结果发现有些组合根本不可能出现(因为约束限制了)。所以,先看“合法组合”,再看覆盖率。
2.4 断言基础——让 DUT 自己说话
断言就是“自动检查器”。你告诉它“应该这样”,它帮你盯着,出错了就报。
2.4.1 立即断言与并发断言
立即断言用 assert,在过程块里用,检查当前时刻的值。
always_ff @(posedge clk) begin
assert (data !== 8'hxx) else $error("data is X at time %0t", $time);
end
并发断言用 assert property,可以检查时序关系。
property req_ack;
@(posedge clk) req |=> ##[1:3] ack;
endproperty
assert property (req_ack);
这个断言的意思是:如果 req 拉高,那么 1~3 个时钟周期后 ack 必须拉高。这就是典型的握手协议检查。
2.4.2 断言在 UVM 中的使用
UVM 里断言通常放在 uvm_monitor 或者 uvm_scoreboard 里。我习惯在 monitor 里写断言检查协议,在 scoreboard 里写断言检查数据一致性。
注意: 断言不要写得太复杂。我曾经见过一个断言写了 20 行,结果仿真速度慢了 30%。而且复杂断言 debug 起来非常痛苦。建议:一个断言只检查一件事。
知识体系总览
下面这张图是我自己整理的,把本章四个知识点的关系画出来了。你仔细看看,OOP 是基础,随机化产生激励,覆盖率衡量进度,断言保证正确性。四者缺一不可。
好了,这一章的内容就到这里。这四个知识点是 UVM 的基石,你花时间把它们吃透,后面学 UVM 会轻松很多。记住,验证不是写代码,而是设计场景。随机化给你无限场景,覆盖率告诉你场景够不够,断言帮你盯着场景对不对。
可可精品资料库