本系列文章从秋招高频问题入手,深度剖析 SV 和 C++,如有错误之处,欢迎批评指正!
联系邮箱:zhangshaopu@ufl.edu
必考题 1:什么是 OOP?三个特性?/ 面向对象编程的优势?
OOP 的全称是 Object-Oriented Programming,也就是面向对象编程;三大特性分别是:封装、继承 和 多态
封装指的是:将数据和函数封装在一个 class 中,对外部隐藏细节,仅仅公布一些接口,类似一个黑盒子
- 好处:提高安全性和模块化程度
- 对应的语法:public、protected、local 等关键词
- local:只有该类 可以访问成员,子类和外部无法访问
- protected:该类和子类 可以访问成员,外部无法访问
继承指的是:一个类能够从另一个类继承属性和方法
- 好处:提高代码的复用性
- 对应的语法:extends 等关键词
多态指的是:允许【基类句柄】指向【派生类对象】,并根据【实际指向的对象类型】来调用相应的方法
- 好处:提高灵活性和扩展性
- 对应的语法:virtual 等关键词
必考题 2:OOP 为什么要用 virtual?什么时候用 virtual?
virtual 关键字是实现 多态 这一特性的关键,在 SV 中很多地方都需要使用 virtual
首先就是 virtual class,也就是虚类
它常用于定义 类的模板,往往作为 基类 存在。因此,虚类 不能被实例化,只能在 被重载后 实例化
uvm 中有一些显著的虚类,例如 uvm_void 和 uvm_reg
其次是 virtual interface
virtual interface 是一个 句柄,指向真实的 interface
在 uvm 的设计模式中,一般在 top 定义 interface 的实例;在 driver 和 monitor 里面定义 virtual interface,然后将 top 的 interface 通过 config_db 机制和 virtual interface 联系起来
最后是 virtual task/function
如果子类重写了父类的虚方法,使用父类指针或引用调用该方法时,实际调用的是子类中重写后的版本。这种机制极大地提高了代码的灵活性和扩展性
如果某一 class 会被继承,则用户定义的 task/function 都应该加上 virtual 关键字,以备后续扩展
必考题 3:OOP 的虚函数和纯虚函数的区别?
虚函数 更多强调的是 OOP 的 多态性:通过在父类中实现一个虚函数,并在子类中完成覆盖,则通过父类指针调用该方法时,呈现的是子类的实现;使用的关键词是:virtual
纯虚函数 更多强调的是 OOP 的 继承性:纯虚函数 不会被实现,只是起一个 规范 的作用,表示:继承这个类的时候必须实现这个函数;使用的关键词是:pure virtual
必考题 4:OOP 中的深复制(Deep Copy)和浅复制(Shallow Copy)
深复制 和 浅复制 都指的是:复制一个对象,而不是句柄;它们的区别在于:
- 深复制会复制原有对象中的 所有成员变量,包括【原有对象中的句柄】指向的内容;深复制的函数往往 需要自己定义
- 在 UVM 中,可以使用 UVM 提供的 clone 和 copy 实现深复制
-
// copy item item_new = new(); item_new.copy(item_old); // clone item item_new; $cast(item_new, item_old.clone());
- 浅复制同样复制原有对象中的 所有成员变量,但 不包括【原有对象中的句柄】指向的内容
- 浅复制通过 new 即可实现
-
item item_new; item_new = new item_old;
必考题 5:SV 中的四大数据结构?
定宽数组:声明的时候需要 确定数组大小,编译后分配的 内存空间固定,不可改变
定宽数组可以分为 合并数组 和 非合并数组:
- 合并数组 在内存中 连续存储,适用于【逐位 / 逐字节访问】数据的场景
- 非合并数组 在内存中 按元素存储(每个元素可以是一个复杂的数据类型),适用于【按元素访问】数据的场景
// 一个 8 位宽、16 个元素的定宽数组
logic [7:0] fixed_array [0:15];
// 合并数组特点:在内存中连续存储,适用于需要逐位或逐字节访问数据的场景
logic [7:0] packed_array;
logic [11:0][7:0] packed_array;
// 非合并数组特点:在内存中按元素存储,每个元素可以是一个更复杂的数据类型,适用于需要按元素访问而不是逐位访问的场景
logic unpacked_array[0:7];
logic [11:0] unpacked_array[0:7];
动态数组:声明的时候 不需要确定数组大小,而是在仿真的时候 通过 new 进行空间分配
int dynamic_array[]; // 声明一个动态数组
dynamic_array = new[10]; // 分配 10 个元素的大小
关联数组:使用 索引访问 元素,索引可以是 任何的标量类型
它的结构类似 hash,也是一种 键值对 的数据结构
int associative_array[string]; // 声明一个字符串键的关联数组
associative_array["key1"] = 1;
associative_array["key2"] = 2;
队列:是一种 FIFO 结构,支持 动态增长和缩小
SV 中的队列结合了 链表 的优点,可以 在一个队列的任何位置进行增删
int queue[$];
必考题 6:上述四种数据结构的应用场景?
定宽数组:针对 信号位宽固定 的数据进行建模,例如 AMBA 总线中的控制信号和数据信号、以太网包的非 payload 字段 等等
动态数组:在 不确定数据大小 的情况使用,例如 以太网包的 vlan_id 字段 和 payload 字段
关联数组:适用于 非连续 / 稀疏 访问数据的场景
队列:适用于需要 频繁插入和删除 的情况
必考题 7:SV 中的 function 和 task 的区别?
区别 | function | task |
输入 | function 中 至少有一个输入参数 | task 对输入参数没有限制 |
输出 | function 只能通过 return 返回一个值 | task 没有返回值,通过定义参数列表中的 output 或 inout 返回 |
时序 | function 中不能有任何时序控制语句 它在 仿真 0 时刻 开始执行 | task 可以包含时序控制语句 它可以在 非 0 时刻 执行 |
调用 | function 只能调用 function | task 都可以调用 |
必考题 8:从可综合的角度谈谈 function 和 task?
只要 function 和 task 内部的代码可综合,并且 function 和 task 满足语法范式的要求,就可以综合
可以把 function 看作 组合逻辑的子 module,task 看作 时序逻辑的子 module
但是我一般不在 RTL 中使用 function 和 task,因为不够直观,debug 也很复杂