101.【SV】SystemVerilog Interview Questions Set 2
📘 SystemVerilog 面试题集 2 —— 验证工程师的“知识加油站”
在芯片验证的面试中,除了基础语法,面试官更关注你对高级特性和验证方法学的理解。今天,我们继续解析第二组常见的面试题,涵盖对象复制、约束控制、覆盖率、数据类型、验证架构等多个方面。让我们逐一攻克!
1. 深拷贝与浅拷贝的区别是什么?
通俗解释:
浅拷贝就像复印一份简历,但简历里提到“我的证书放在家里那个抽屉里”,结果复印件也指向同一个抽屉。如果后来你在抽屉里加了新证书,原件和复印件都能看到。
深拷贝则会把抽屉里的证书也复印一份,放到另一个新抽屉里,从此两者独立。
在 SystemVerilog 中,当复制一个对象时:
- 浅拷贝:只复制对象本身的成员变量,但如果成员变量是句柄(指向另一个对象),则只复制句柄值(即指向同一个对象),而不复制句柄指向的对象。
- 深拷贝:不仅复制对象本身,还递归复制其所有引用的对象,得到完全独立的副本。
代码示例:
class Inner;
int val;
endclass
class Outer;
int id;
Inner in;
function new();
in = new();
endfunction
endclass
initial begin
Outer o1 = new();
o1.id = 1;
o1.in.val = 10;
// 浅拷贝:直接赋值句柄
Outer o2 = o1; // o2 与 o1 共享 Inner 对象
o2.id = 2;
o2.in.val = 20; // 同时修改了 o1.in.val
$display(o1.in.val); // 输出 20
// 深拷贝:自己实现复制所有内容
Outer o3 = new();
o3.id = o1.id;
o3.in = new();
o3.in.val = o1.in.val; // 复制 Inner 对象
o3.in.val = 30; // 不影响 o1
$display(o1.in.val); // 仍然是 20
end
关键点:
- 直接赋值(
o2 = o1)是浅拷贝。 - 实现深拷贝需要自定义方法(如
copy()),手动复制所有嵌套对象。 - 在 UVM 中,
copy()方法默认是浅拷贝,通常需要重写以实现深拷贝。
2. 如何禁用约束?
通俗解释:
约束就像给随机变量设定的“规则”。有时你想临时忽略某些规则,让变量完全随机。这时就可以“禁用”约束。
在 SystemVerilog 中,每个约束都有一个约束模式,默认启用。可以通过 constraint_mode() 方法来启用或禁用约束。
代码示例:
class Packet;
rand bit [3:0] length;
constraint c_len { length > 5; } // 默认启用
endclass
initial begin
Packet p = new();
// 禁用约束
p.c_len.constraint_mode(0); // 0 表示禁用
repeat(5) begin
p.randomize();
$display("length = %0d", p.length); // 可能输出 0~15 任意值
end
// 重新启用
p.c_len.constraint_mode(1);
p.randomize();
$display("constrained length = %0d", p.length); // 输出 6~15
end
关键点:
constraint_mode(0)禁用,constraint_mode(1)启用。- 可以针对单个约束,也可以针对所有约束(通过
constraint_mode(0)不加参数?实际上constraint_mode是约束名的方法)。 - 禁用后,随机化时该约束不被考虑。
3. 代码覆盖率与功能覆盖率的区别
通俗解释:
- 代码覆盖率:检查“代码有没有跑过”。比如你写了一个 if-else,代码覆盖率会告诉你 if 分支和 else 分支是否都执行过。它关注的是设计实现的代码行、分支、条件等。
- 功能覆盖率:检查“功能点有没有测到”。比如你的设计要求支持“读操作”和“写操作”,功能覆盖率会告诉你这两种操作是否都发生过。它关注的是设计规范中的功能点。
区别总结:
| 方面 | 代码覆盖率 | 功能覆盖率 |
|---|---|---|
| 目的 | 衡量测试对设计代码的执行程度 | 衡量测试对设计功能点的覆盖程度 |
| 测量对象 | 代码行、分支、条件、状态机等 | 自定义的覆盖点(coverpoint)、交叉覆盖等 |
| 是否需要额外定义 | 不需要,工具自动收集 | 需要用户手动定义覆盖组 |
| 能否达到100% | 可能,但 100% 不代表功能完备 | 100% 表示所有定义的功能点都被覆盖 |
示例:
// 代码覆盖率由工具自动收集,无需写代码。
// 功能覆盖率需要手动定义:
covergroup cg;
coverpoint opcode {
bins read = {READ};
bins write = {WRITE};
}
endgroup
关键点:
- 代码覆盖率是“实现了多少”,功能覆盖率是“验证了多少”。
- 验证目标通常是功能覆盖率 100%,同时代码覆盖率作为辅助指标。
4. 什么是 ignore_bins?
通俗解释:
在功能覆盖率中,有时候某些值(比如保留值、非法值)你根本不想测,或者根本测不到。如果把它们也算进覆盖率目标,分母会变大,导致覆盖率数字偏低。ignore_bins 就是用来把这些“不想关心”的值排除在覆盖率统计之外。
代码示例:
covergroup cg;
coverpoint addr {
bins low = {[0:100]}; // 关心 0~100
ignore_bins high = {[101:255]}; // 忽略 101~255
}
endgroup
这样,覆盖率计算时只考虑 0~100 的值,101~255 的出现与否不影响覆盖率。
关键点:
ignore_bins告诉工具:这些 bin 即使出现也不计入覆盖率,未出现也不影响。- 常用于排除无效值、保留值,或已知无法产生的值。
- 与
illegal_bins不同,illegal_bins是若出现则报错。
5. 两状态变量和四状态变量是什么?举例说明。
通俗解释:
数字电路中有四种逻辑值:0、1、X(未知)、Z(高阻)。
- 四状态变量可以表示这四种值,模拟真实硬件行为,如
reg、logic、wire。 - 两状态变量只能表示 0 和 1,仿真效率更高,常用于纯验证环境(如事务级建模),如
bit、byte、int。
示例:
bit two_state_bit; // 两状态:0,1
byte two_state_byte; // 两状态:0~255
int two_state_int; // 两状态:32位整数
logic four_state_logic; // 四状态:0,1,X,Z
reg four_state_reg; // 四状态
wire four_state_wire; // 四状态
关键点:
- 四状态变量仿真时占用更多内存,且 X/Z 传播可能导致仿真速度变慢。
- 验证环境中通常用两状态变量处理数据,用四状态变量连接 DUT 接口。
- 注意:两状态变量无法检测 X/Z,可能导致 RTL 中的 X 被误当作 0 或 1。
6. 如何确保地址范围 0x2000 到 0x9000 在仿真中被覆盖?
通俗解释:
要确保某个地址范围被测试到,可以定义一个覆盖组,将这个范围分成几个仓(bins)。然后运行仿真,检查这些仓是否都被命中。
代码示例:
covergroup address_cg @(posedge clk);
coverpoint addr {
bins low_bound = {16'h2000}; // 边界值
bins middle = {[16'h2001:16'h8FFF]}; // 中间区域
bins high_bound = {16'h9000}; // 边界值
}
endgroup
也可以更细粒度地划分,比如每 1KB 一个仓,确保每个子区域都被覆盖。
关键点:
- 边界值(如 0x2000 和 0x9000)应单独列仓,因为边界常是 bug 多发区。
- 覆盖率收集完成后,查看哪些仓未被命中,补充相应测试。
7. 验证中的分层架构是什么?
通俗解释:
分层架构就是把验证环境按照功能抽象程度分成多层,下层为上层提供服务。就像盖楼:地基(底层模块) → 框架(VIP) → 装修(测试用例)。每一层只关心自己的职责,下层对上层隐藏实现细节。
典型的 UVM 验证环境分层(从底向上):
- 信号层:直接与 DUT 引脚相连的接口、驱动、监视器。
- 命令层:序列、驱动器,将事务(transaction)转换为信号层时序。
- 功能层:代理(agent)、记分板、参考模型,处理协议级的事务。
- 场景层:虚拟序列、测试用例,组合各种功能场景。
- 测试层:顶层测试控制。
好处:
- 复用性:下层 VIP 可跨项目复用。
- 可维护性:修改某层不影响其他层。
- 可扩展性:添加新测试只需在上层增加场景。
示例(UVM 环境示意图):
Test
└── Virtual Sequencer
└── Sequencer
└── Driver → Interface → DUT
Monitor → Scoreboard
关键点:
- 分层架构是 UVM 的核心思想,让验证环境更清晰、高效。
8. 解释验证的周期及其收敛。
通俗解释:
验证周期就是不断“设计 → 验证 → 发现问题 → 修改设计 → 再验证”的循环,直到所有问题都解决,达到验证目标(收敛)。就像打磨一块玉石,反复检查、修整,直到完美。
验证周期主要阶段:
- 制定验证计划:根据设计规范,确定功能点、覆盖率目标、测试策略。
- 搭建验证环境:开发测试平台、BFM、参考模型、检查器等。
- 编写测试用例:生成各种激励,覆盖功能点。
- 运行仿真:执行测试,收集覆盖率。
- 调试与修复:发现 bug 反馈给设计人员,修复后重新验证。
- 分析覆盖率:检查功能覆盖率、代码覆盖率,补充缺失的场景。
- 回归测试:确保新修改不影响已有功能。
- 验证收敛:当所有功能点都被覆盖,所有 bug 已修复,且达到质量目标(如覆盖率 100%),验证结束。
关键点:
- 收敛不是一次完成,而是迭代逼近。
- 常用指标:功能覆盖率、代码覆盖率、断言覆盖率、bug 曲线等。
9. 动态数组与队列的区别
通俗解释:
- 动态数组:就像一排连续的储物柜,编号从 0 开始。你可以随时改变柜子数量(重新分配),但只能在末尾增加/减少。适合需要随机访问且大小可变的场合。
- 队列:就像排队买票的队伍,只能在队尾加入(push_back),队首离开(pop_front)。也可以从两端操作,适合实现 FIFO、缓冲等。
区别总结:
| 特性 | 动态数组 | 队列 |
|---|---|---|
| 声明 | int dyn[]; |
int queue[$]; |
| 大小变化 | 通过 new[] 重新分配,或使用 {} 构造 |
自动增长,使用 push_back()、push_front() |
| 访问方式 | 索引访问 dyn[i] |
索引访问 queue[i],也支持 $ 表示末尾 |
| 常用方法 | size()、delete() |
push_back()、pop_front()、insert() 等 |
| 内存管理 | 连续内存,重新分配可能涉及复制 | 通常实现为链表或循环缓冲区,插入删除高效 |
| 典型用途 | 需要随机访问且大小动态变化的数据集合 | FIFO 缓冲、待处理队列、任务调度 |
示例:
// 动态数组
int dyn[];
dyn = new[5]; // 分配 5 个元素
dyn[0] = 10;
dyn = new[10](dyn); // 扩展为 10,复制前 5 个元素
// 队列
int q[$];
q.push_back(1); // {1}
q.push_back(2); // {1,2}
int x = q.pop_front(); // x=1, q={2}
q.push_front(0); // {0,2}
10. 结构体与类的区别
通俗解释:
- 结构体(struct):就像一张表格,只记录数据,没有行为(方法)。它是“被动”的。
- 类(class):就像一个人,不仅有属性(身高、体重),还有行为(走路、说话)。它是“主动”的。
详细区别:
| 特性 | 结构体 (struct) | 类 (class) |
|---|---|---|
| 数据成员 | 可以有,默认是公共的 | 可以有,可通过访问修饰符控制(public/protected/local) |
| 方法 | 不能有方法(Verilog 结构体无方法,SV 结构体可以有方法?实际上 SV 的 struct 不能定义方法,只能定义数据。但有些资料说 SV 的 struct 可以包含方法?查阅标准:SystemVerilog struct 可以包含 typedef、但不可以包含 task/function。所以一般说 struct 没有方法) | 可以有函数和任务,操作自己的数据 |
| 继承 | 不支持 | 支持继承和多态 |
| 构造/析构 | 无构造函数和析构函数,需手动初始化 | 有 new() 构造函数,可自动初始化 |
| 存储方式 | 一般是存放在栈上(作为局部变量)或静态区 | 对象分配在堆上,通过句柄引用 |
| 默认复制 | 赋值时复制所有成员(深拷贝) | 赋值时复制句柄(浅拷贝) |
| 使用场景 | 简单的数据打包,如描述一个数据包的结构 | 复杂的验证组件,如 UVM 中的 driver、monitor 等 |
代码示例:
// 结构体
typedef struct {
bit [7:0] addr;
bit [31:0] data;
} packet_t;
packet_t pkt;
pkt.addr = 8'h10;
pkt.data = 32'hdeadbeef;
// 类
class Packet;
rand bit [7:0] addr;
rand bit [31:0] data;
function void display();
$display("addr=%h, data=%h", addr, data);
endfunction
endclass
Packet p = new();
p.randomize();
p.display();
关键点:
- 结构体适合简单数据集合,类适合复杂对象和行为。
- 验证环境中,大部分验证组件都是类(如 UVM 的
uvm_agent、uvm_driver)。 - 结构体常用于定义事务(transaction)的数据部分,但也可以被类包裹。
希望这份解析能帮你理清这些面试题背后的概念。记住:面试官不仅看你知道多少,更看你能否用通俗语言把复杂问题讲清楚。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)