You are on page 1of 95

欢迎关注公众号 摸鱼范式

Digital Logic Design

Number Systems, Arithmetic and Codes

[1] 将下列十进制数转换为有符号二进制,八进制和十六进制,使用尽可能少的比特位

a) 17

b)-17

a: 对17辗转相除,得到其二进制为010001,八进制为21,十六进制为0x11。注意二进制必须是010001,而不是
10001,不能缺少符号位

b: 根据17的二进制,-17的二进制数原码位110001,取反加一得到补码为101111,因此八进制为57,十六进制为0x2f

二进制转八进制,取三合一,二进制转十六进制,取四合一

[2] 0x3A的十进制数是多少?

ans=3*16^1+10*16^0=58

[3] 什么是格雷码,格雷码相对于普通二进制码有什么好处?

格雷码是一种二值编码,相邻的编码之间只有一位的区别。因此与普通二进制码相比,在递增时出错概率更加小。下
表是3bit的格雷码编码:

十进制 二进制 格雷码

0 000 000

1 001 001

2 010 011

3 011 010

4 100 110

5 101 111

6 110 101

7 111 100

此外,由于格雷码中比特位变化比较少,与二进制码相比,使用格雷码的功耗更加低
[4] 什么是奇偶校验位,如何计算?

奇偶校验位是在一串二进制码的最后添加的一位,它使得整个二进制串的1的个数为奇数或者偶数。因此奇偶校验分为
两种,奇校验和偶校验。

计算校验位需要对二进制码中的1进行计数。如果1的数量为奇数,并且使用偶校验,则校验位为1,使得整体1的个数
为偶数。如果1的数量为偶数,并且使用偶校验,则校验位为0,使得整体1的个数为偶数。奇校验类似。奇偶校验位可以
通过对所有的比特位进行异或得到。

[5] 计算二进制数111001的奇校验位

111001中1的个数为4个,所以校验位为1,带上校验位以后为1110011,1的个数为5,为奇数

[6] 什么是BCD码,他和二进制码有什么区别?十进制27的二进制码和BCD码是什么?

BCD码也称二进码十进数,BCD码可分为有权码和无权码两类。其中,常见的有权BCD码有8421码、2421码、5421
码,无权BCD码有余3码、余3循环码、格雷码。8421BCD码是最基本和最常用的BCD码,它和四位自然二进制码相似,各
位的权值为8、4、2、1,故称为有权BCD码。

2的二进制位0010,7的二进制位0111,十进制27的8421BCD码为,00100111,二进制码为11011

Basic Gates

[7] 以下哪个是通用门?为什么?

AND
NAND
OR
NOR
XOR

通用门是可以实现任何布尔函数而无需使用任何其他门类型的门。 与非门或非门是通用门。

[8] 如何使用两个两输入与非门实现,两输入与门,两输入或门,非门?

与门:

或门:

非门:

[9] 如何使用两个两输入或非门实现,两输入与门,两输入或门,非门?
与门:

或门:

非门:

[10] 用一个2:1MUX构成下面的门

非门
两输入与门
两输入或门
两输入或非门
两输入与非门
两输入异或门

非门:

与门:

或门:

或非门:用或门和非门组成
与非门:用与门和非门组成

异或门:

[11] 异或门在数据通信中的典型应用是什么?

通常被用于错误检测,例如,奇偶校验,CRC校验,ECC。异或门也可以用于伪随机数生成。

[12] 三输入与非门的输出何时为0?

所有输入都为1

[13] 如何使用异或门实现一个非门

Combinational Logic Circuits

[14] 请用2:1选择器实现4:1选择器

[15] 什么是环形振荡器?如果每个门的延迟是2ps,使用三个非门的环形振荡器的频率是什么?

环形振荡器可以由奇数个非门组成,非门或者反相器连接成链后,最后一个输出反馈回第一个反相器。

三个反相器,信号需要经过两次反馈,即2*3个反相器,振荡频率为1/(6*2ps) = 1000/12 GHz = 83.33 GHz

Sequential Circuits and State Machines

[16] 同步电路和异步电路有什么不同?

时序电路分为两种,同步时序电路和异步时序电路

同步时序电路在适中的上升沿或者下降沿改变状态和输出值。常见的例子是flip-flop,在时钟边沿根据输入改变输出。

异步时序电路的状态和输出值是根据使能信号进行控制,这更加类似于一个带有反馈的组合逻辑。
[17] 阐述建立时间和保持时间

建立时间是在时钟进行有效转换前数据信号应该保持稳定的最短时间。
保持时间是在时钟进行有效转换后数据信号应该保持稳定的最短时间。

[18] 解释什么是clock skew

时钟信号到达两个FF的时间差称之为clock skew(时钟偏斜)

例如图中两个FF的时钟,虽然是同一个时钟源,但是由于走线的延迟,导致a的时钟比b的快。

[19] 下图output delay为10ns,setup time为5ns,hold time为2ns,组合逻辑delay为10ns,请


计算该电路的最大工作频率

建立时间约束为 ,即 ,最大工作频率为40Mhz

[20] 触发器和锁存器的区别什么?

触发器和锁存器都是存储信息的基本单元。一个触发器或者锁存器能够存储一bit的信息。两者的主要不同点是,触发
器只在时钟上升沿或者下降沿根据采样改变输出,而锁存器在enable信号拉高期间都会跟随输入。

[21] 什么是竞争?什么时候会出现?如何避免?

当输出取决于不同信号的顺序或者时序时,被称为竞争。竞争可以分为两种

1. 实际的硬件中的竞争
2. 仿真行为中的竞争

实际硬件中的竞争:以SR锁存器为例,当SR都是1的时候,输出为1,此时如果SR同时变成0,那么Q和Q'就会进入竞
争的情况。可以通过添加合适的逻辑避免。
仿真行为中的竞争:例如下面的代码

1 always @(posedge clk or posedge reset)


2 if (reset) X1 = 0; // reset
3 else X1 = X2;
4 always @(posedge clk or posedge reset)
5 if (reset) X2 = 1; // reset
6 else X2 = X1;

由于使用了阻塞赋值,便会发生竞争的情况,通过改为非阻塞赋值可以解决

[22] 用2:1mux实现D触发器

[23] 用D触发器实现T触发器

T触发器,T为0时输出不变,1时翻转。写出真值表就能看出来,将输入和Q异或再输入到D端。

[24] 用JK触发器实现D触发器

J=D,K=D'

[25] 行波进位加法器和超前进位加法器的区别是?

行波进位加法器:
结构类似于我们拿笔在纸上做加法的方法。从最低位开始做加法,将进位结果送到下一级做和。由于本级的求和需要
等待前一级的进位结果才可以得到,所以对于两个N-bit的求和。即使有N个一位的全加器,也需要N个延迟。
超前进位加法器:

事实上,在以下两种情况中,Ci=1:

1. Ai和Bi都为1
2. Ai和Bi有一个为1,且Ci-1为1

其对应的表达式为

递归后

可以看出每一级的进位信号可以不通过上一级的结果产生,只与输入有关系。因此减少了时间。

[26] 实现一个32bit寄存器需要几个flip-flop?

一个FF存储一bit信息,因此需要32个FF。

[27] mealy型FSM和moore型FSM有什么区别?

mealy型FSM的输出和当前的状态以及当前的输入有关系。

moore型FSM的输出只和当前的状态有关系。

[28] 九个状态的记录最少需要几个FF?

2^3 < 9 < 2^4,因此是4个

[29] 使用尽可能少的DFF实现二分频和四分频

二分频:
四分频:

Computer Architecture

[30] RISC和CISC的区别是什么?

RSIC:精简指令集。CISC:复杂指令集
RISC结构具有比较少的指令,这些指令是简单的指令(固定长度的指令和较少的寻址模式)。CISC结构具有更
多的指令,更加复杂,可变长度指令和更加多的寻址方式
RISC具有较小的指令,硬件上相对没有那么复杂。CISC使用更加复杂的硬件来解码和分解复杂的指令。因此
RISC更加侧重软件,CSIC更加侧重硬件
CISC具有更加复杂的指令,因此可以使用更加少的RAM来存储编程指令。相对的,RISC需要更加多的RAM来存
储指令
RISC的指令通常需要一个周期完成,而CISC可以能需要多个周期才能完成,具体取决于指令的复杂性。因此在
RISC中可以进行流水线化。
RISC通过减少每条指令的周期数来提高性能,而CISC通过减少每条程序的指令数来提高细嫩那个。CISC支持单
条指令完成从内存读取,执行操作,写回内存的操作(mem2mem操作)。而RISC完成则需要三个指令。

[31] 冯诺依曼结构和哈佛结构有什么区别?

冯诺依曼结构中,指令和数据存储在同一个存储器中。CPU读取数据和指令使用同一条总线,具有存储数据和
指令的统一缓存。
哈佛结构中,数据和指令是分开存储的,可以使用两条不同的总线同时访问数据和指令,指令和数据都具有单
独的缓存。

[32] 解释内存存储中的Little Endian 和 Big Endian的概念

字节顺序是指字节在内存中的存储顺序。数据在内存中通常是按照字节寻址的,但是大多数计算即通常采用32位(4
个字节)存储操作数的。因此,为了将一个字节存在存储器中,由两种方式:

1. 高字节存在低地址中,这称之为Big Endian
2. 低字节存在低地址中,这称之为Little Endian

例如,当cpu向地址0x1000中写入0xDDCCBBAA时,两种方式就会出现差异
[33] SRAM和DRAM的区别是什么?

DRAM:Dynamic Random Access Memory。动态随机存储,数据以电荷的形式存储,每个存储单元是由晶体管和电


容组成的,数据存在电容中。DRAM是易失性设备,电容会因为漏电而流失电荷,所以需要定时刷新。

SRAM:Static Random Access Memory。静态随机存储,不需要属性,只需要电源即可,SRAM的存储单元由六个晶


体管组成,因此与DRAM相比,占用面积更加多。

SRAM速度快,成本搞,常用于高速缓存。DRAM密度小,速度慢,常用于主存储器

[34] 对于256KB的内存,如果按字节寻址,需要多少位的地址线?

256KB=2^8*2^10Bytes,需要18位地址线

[35] CPU中不同类型的寄存器有什么区别?

Program Counter (PC):保存当前正在执行的指令的地址


Instruction Register (IR):保存当前正在执行的指令的寄存器。(PC所指向的地址上的值)
General Purpose Registers:通用寄存器是用于存储程序所需要的数据,通用寄存器的数量由体系结构定义,并
可以由软件用于运行期间零是存储数据。
Stack Pointer Register (SP):堆栈指针寄存器用于存储压入队长的最新数据的地址。常见的用途是存储子函数调
用时的返回地址。

[36] 解释计算机架构中的流水线

流水线技术是在单个处理器中实现指令集并行的技术。将基本的指令周期拆分为多个阶段,无需等待每条指令完成,
并行执行不同的步骤,在一条指令结束之前开始下一条指令。流水线能投提高指令的吞吐率,但是并不能减小指令的指令
时间。

[37] 什么是pipeline hazard?处理器中有几种pipeline hazard?

pipeline hazard是指由于某些原因下一条指令无法执行的情况。有三种类型的hazard:

1. Structural Hazards:由于硬件资源不足产生的。比如如果设计结构中只有一个浮点执行单元,每次执行都需要
两个周期的时间,那么当程序中出现背靠背的浮点指令时会导致流水线停止。除此之外内存和缓存的访问也会
导致Structural Hazards
2. Data Hazards:由于前后指令数据的依赖性而造成的hazard

A、RAW:先读后写register,ture data dependence


B、WAW:先写后写register,output data dependence
C、WAR:先写后读register, anti-data dependence

3. Control Hazards:当碰到跳转指令时,processor会stall一个cycle。
因为processor在处理指令时会分两个stage:取指令和解码指令。
当一条指令进入到解码阶段时,才会被发现需要跳转,所以在取指令阶段
的那条指令会被废掉,故浪费掉一个cycle。

[38] 如何避免三种pipeline hazard?

1. Structural Hazards:将一个function unit切分成更小的stage或对设计相同功能的硬件等,总之,就是让硬件资


源够用。
2. Data Hazards:A情况是真正的数据依赖,会产生hazard,可以用forwarding技术来减少或消除它;而B和C是在
当指令顺序被compiler或者是硬件调整后才会出现的数据依赖。如果出现了B和C的情况,可以有一种技术来消
除它,叫做register renaming。
3. Control Hazards:可以在program中加入likely()or unlikely()来帮助compiler预测taken or not taken的可
能性。另外,compiler可以通过delayed-branch的技术来消除branch hazard,但是该技术很少用在很长的
pipeline中。最后,可以通过硬件的技术消除Brach hazard。以上的技术都是compiler或hardware的技术,
programmer可以不关心,但好的if语句应该
如下:
1 if(unlikely(condition))
2 {
3 几率小
4 }
5 else
6 {
7 几率大
8 }
9 注意:将几率大的语句放在not taken下面,将几率小的语句放在taken下,这样会节省4个
10 cycle左右(由pipeline的深度决定)。另外,还有一种写法就是将unlikely改为likely,将几率大的和几率小的对调,
这种方法比第一种方法要慢或持平(至于原因有compiler的原因,有pipeline的原因,所以它依赖于compiler和chip的
设计)。

[39] 如果一个流水线由十个阶段,每个阶段需要1ns执行。假设没有hazards,那么处理100个数
据需要多久?

第一个数据需要10ns完成,此后1ns完成一个数据的处理,因此总时间位10+99=109ns

[40] 指令有多少种寻址方式?

1. 立即数寻址,操作数作为指令的一部分

1 add r0 r1 0x12 将r1+0x12的结果存在r1

2. 直接寻址,操作数的地址直接出现在指令中

1 load r0 0x10000 将地址0x10000的数据存到r0中

3. 寄存器寻址,操作数被存在寄存器中,寄存器的名字出现在指令中

1 mul r0, r1 , r2 将r1*r2的结果存在r0中

4. 偏移量寻址,操作数的地址由一个寄存器的数据加上一个立即数的偏移量得到

1 load r0 r1 offset r1包含了及地址,r1+offset才是真实的地址

[41] 什么是时间局域性和空间局域性?

局域性原理:程序常常重复使用它们最近用过的数据和指令。一条广泛适用的经验规律是:一个程序90%的执行时间
花费在仅10%的代码中。局域性意味着我们可以根据一个程序最近访问的指令和数据,比较准确地预测它近期会使用哪些
内容。局域性的原理也适应于数据访问,不过不像代码访问那样明显。

时间局域性是指最近访问过的内容很可能会在短期内被再次访问。
空间局域性是指地址相互临近的项目很可能会在短时间内都被用到。

[42] 计算机系统中有哪些存储?

1. Register
2. Cache
3. Main Memory/Primary Memory
4. Secondary Memory (Magnetic/Optical)

[43] 什么是cache?

cache是小型的快速存储。通常在Main Memory和CPU之间,有时放置于CPU内。
[44] 概述cache操作,cache功能的原理是?

每当CPU请求存储位置的内容时,都会首先检查cache中是否有此数据。如果cache中存在数据,则CPU直接从cache中
获取数据。 由于不需要CPU进入该数据的Main Memory,因此速度更快。 如果缓存中不存在数据,则将一块数据从Main
Memory读取到cache中,然后以字块的形式从cache中传输到CPU。

[45] 什么是cache的miss和hit?

在cache中查找地址时,若缓存中包含该内存位置,称之为cache hit。如果在cache找不到,则称之为cache miss。

[46] 如果一台机器存在cache,那么在链表和向量中搜索一个值的性能表现有什么差异?

链表是一种将其元素存储在非连续存储位置中的数据结构,而向量是一种将元素存储在连续位置中的数据结构。 对于
具有cache的设计:如果cache中存在一块数据,则很可能在cache中也存在后续的连续数据,因为通常是从主存储器到高
速缓冲存储器的任何取指令 根据缓存行(通常为64或128字节)获取。 因此,在拥有cache的机器上,通过向量进行搜索
将比链表进行搜索更快。

[47] 将内存映射到cache有哪些机制?请比较他们的优缺点

一共有三种主要的映射方法。这三种映射中,主存储器和cache都被划分为存储块(blocks of memory,也称cache
line,每个64bytes),这是映射时的最小单位。

直接映射(Direct Mapping):直接映射中,主内存和cache中始终存在一对一的映射,一组只有一个数据块。例
如:在下图中,cache大小为128个块,而主内存中有4096个块。设计实现是基于模数计算得到的,i为cache的
块序号,j是主内存的块编号,m是cache的行数,i = j mod m。如果块的大小为64B而地址为32为,则地址[5:
0]称为字节偏移量,它说明字节在块中的位置,地址[12:6]称为组位,说明地址地址映射到哪一组,剩下的地
址位作为标志位,标志说明cache映射了哪些内存中的地址。这是最简单的映射,并且可以通过内存地址可以轻
松计算cache中的数据在内存中的位置,并且只需要一个标记为进行比较就能知道是否命中。这种映射的缺点是
命中率低, Cache的存储空间利用率低。
全局关联映射(Fully Associate Mapping):任何的内存块都能映射到cache的任何块中,使用和上面的图一样的例
子,地址[5:0],作为块内部的索引,剩下的所有位都用于和cache中的所有标记为进行比较,这需要很大的比较
器。虽然这种方式命中率较高,Cache的存储空间利用率高,但是线路复杂,成本高,速度低
组关联映射(Set Associate Mapping):将cache分成u组,每组v行,主存块存放到哪个组是固定的,至于存到该组
哪一行是灵活的,即有如下函数关系:cache总行数m=u×v,组号q=j mod u组间采用直接映射,组内为全相
联。例如,下图显示了128个块的相同高速缓存,这些高速缓存组织为64个集合,每个集合具有2个块。硬件较
简单,速度较快,命中率较高,但是与分组有关系。

[48] 更高关联性的缓存有什么缺点?

更高关联性的cache意味着需要更加大的比较器,用于将传入的地址和标签进行对比,会导致更加大的硬件需求和功
耗。

[49] 一个按字节寻址的CPU的cache具有以下特征:a)与大小为1byte的块直接映射 b)块的cache


索引位4位。那么cache中包含多少个块,作为cache的一部分,需要存储多少个标记为?

cache的索引位有4位,因此cache包含2^4=16个块,每个块只有1byte所以不需要块内索引,剩下的16-4=12位全部作
为标记位

[50] 一个四路组关联的cache总大小为256KB,如果每个cache line的大小为64byte,那么cache


中有多少组?标记位需要多少位?假设地址位宽位32位。

cache的块的数量为256K/64=4096,而cache为四路关联,则组数量为4096/4=1024。64byte的cache line 需要6位进


行块内索引,10位进行集合索引,剩下的32-6-10=16位作为标记为

[51] 直写式缓存和回写式缓存有什么区别?优缺点是什么?

直写式缓存方式: 当CPU要将数据写入内存时,除了更新缓冲内存上的数据外,也将数据写在SDRAM中以维持主存
与缓冲内存的一致性,当要写入内存的数据多起来的话,速度自然就慢了下来.
回写式缓存方式: 当CPU要将数据写入内存时,只会先更新缓冲内存上的数据,随后再让缓冲内存在总线不塞车的时
候才把数据写回SDRAM,所以速度自然快得多

回写缓存在内存带宽利用方面更好,因为仅在需要时才回写数据。 如果系统中存在多个可以缓存同一地址的cache,
则维护数据一致性非常复杂,因为内存可能并不总是具有最新数据。

[52] inclusive 和 exclusive cache之间有什么区别?

cache的inclusive和exclusive属性适用于具备多级缓存的设计。

当CPU试图从某地址load数据时,首先从L1 cache中查询是否命中,如果命中则把数据返回给CPU。如果L1
cache缺失,则继续从L2 cache中查找。当L2 cache命中时,数据会返回给L1 cache以及CPU。如果L2 cache也缺
失,很不幸,我们需要从主存中load数据,将数据返回给L2 cache、L1 cache及CPU。这种多级cache的工作方
式称之为inclusive cache。某一地址的数据可能存在多级缓存中。
与inclusive cache对应的是exclusive cache,这种cache保证某一地址的数据缓存只会存在于多级cache其中一
级。也就是说,任意地址的数据不可能同时在L1和L2 cache中缓存。
exclusive cache的优点之一是多级cache可以一起存储更多的数据。一般使用inclusive cache类型,每次cache miss,可
以从下一级的cache中寻找,load。而exclusive cache,每次cache miss,只能去main memory中load。但是exclusive
cache比较节省cache size。

[53] 组关联映射中用于替换cache line的算法有什么不同?

以下是一些可用于替换cache line的算法

LRU(Least Recently Used):将最近最少使用的内容替换出cache


MRU (Most Recently Used):与MRU,将最近经常的内容提出cache
PLRU (Pseudo LRU):关联性很大的时候,LRU的实现成本很高。如果实际情况在丢弃任一个最近最少使用的数据
就能满足,那么伪LRU算法就派上用场了,它为每一个缓存数据设立一个标志位就可以工作。
LFU (Least Frequently Used):这个缓存算法使用一个计数器来记录条目被访问的频率。通过使用LFU缓存算法,
最低访问数的条目首先被移除。这个方法并不经常使用,因为它无法对一个拥有最初高访问率之后长时间没有
被访问的条目缓存负责。
Random replacement:在该算法中,不存储任何信息,并且在需要替换时选择一条随机行。

[54] 缓存一致性的问题是什么?

在多个处理器拥有自己的cache的共享多处理器系统中,相同数据(相同地址)的多个副本可能会同时存在于不同的
cache中。 如果允许每个处理器自由更新cache,则可能导致内存视图不一致。 这称为高速缓存一致性问题。 例如:如果
允许两个处理器将值写入同一地址,则在不同处理器上读取同一地址可能会看到不同的值。

[55] 基于监听和基于目录的缓存一致性协议之间有什么区别?

Snoop based Coherence Protocol:来自处理器的数据请求将发送到共享系统一部分的所有其他处理器。 其他所


有处理器都监听此请求,并查看它们是否具有数据副本并做出相应响应。 因此,每个处理器都需要维护存储器
的一致性视图。
Directory based Coherence Protocol:目录用于跟踪哪些处理器正在访问和缓存哪些地址。 发出新请求的任何处
理器都将检查此目录,以了解其他代理是否有副本,然后可以向该代理发送点对点请求以获取最新的数据副
本。

Snoop based Coherence Directory based Coherence Protocol

对于小的系统来说,如果带宽足够,基于监听的协议速度会更 基于目录的协议需要使用查找表,这将会导致
加快 较长的时延

基于监听的协议不适合大型系统,因为它需要将将每一个请求 由于不需要广播,基于目录的协议更加适合大
信息进行广播 型系统

[56] 什么是MESI协议?

MESI协议时多处理器系统中最常用的cache一致性协议。MESI 是指4中状态的首字母。每个Cache line有4个状态,可


用2个bit表示,它们分别是:

状态 描述 监听任务

该Cache line有效,数据被修改 缓存行必须时刻监听所有试图读该缓存行相对就主存的操


M 修改
了,和内存中的数据不一致,数 作,这种操作必须在缓存将该缓存行写回主存并将状态变成
(Modified)
据只存在于本Cache中。 S(共享)状态之前被延迟执行。

E 独享、 该Cache line有效,数据和内存中


缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦
互斥 的数据一致,数据只存在于本
有这种操作,该缓存行需要变成S(共享)状态。
(Exclusive) Cache中。

该Cache line有效,数据和内存中
S 共享 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存
的数据一致,数据存在于很多
(Shared) 行的请求,并将该缓存行变成无效(Invalid)。
Cache中。

I 无效
该Cache line无效。 无
(Invalid)

[57] 什么是MESIF和MOESIF协议?
是对于MESI协议的扩展,引入了两个新状态“F”“O”:

F (Forward): 在MOESI协议中,S状态的定义发生了细微的变化。当一个Cache行状态为S时,其包含的数据并不
一定与存储器一致。如果在其他CPU的Cache中不存在状态为O的副本时,该Cache行中的数据与存储器一致;
如果在其他CPU的Cache中存在状态为O的副本时,Cache行中的数据与存储器不一致。
O (Owned): O位为1表示在当前Cache 行中包含的数据是当前处理器系统最新的数据拷贝,而且在其他CPU中一
定具有该Cache行的副本,其他CPU的Cache行状态为S。如果主存储器的数据在多个CPU的Cache中都具有副本
时,有且仅有一个CPU的Cache行状态为O,其他CPU的Cache行状态只能为S。与MESI协议中的S状态不同,状
态为O的Cache行中的数据与存储器中的数据并不一致。

[57] 什么是虚拟内存?

虚拟内存是一致内存管理技术,及时实际的物理内存很小,虚拟处理器也允许处理器查看地址的虚拟连续空间。操作
系统管理虚拟地址空间以及从辅助设备(如磁盘)到物理主内存的内存分配。CPU中的地址转换硬件(memory
management unit,MMU)将虚拟地址转换为物理地址。 此地址转换使用分页的概念,其中将连续的内存地址块(称为
页)用于虚拟内存和实际物理内存之间的映射。

[58] 虚拟内存地址和物理内存地址的区别是什么?

软件程序或进程用来访问其地址空间中存储位置的地址称为虚拟地址。 然后,操作系统连同硬件将其转换为另一个地
址,该地址可用于实际访问DRAM上的主内存位置,该地址称为物理地址。 地址转换是使用分页的概念完成的,如果主内
存或DRAM没有此位置,则在OS的协助下,数据将从辅助内存(如磁盘)移至主内存。

[59] 什么是页的概念?

所有虚拟内存都将虚拟地址空间划分为页,页内的虚拟内存地址是连续的。页是内存从辅助存储移动到物理内存以管
理虚拟内存的最小单位。大多数计算机系统的页至少为4KB.当需要更大的实际内存是,某些结构还支持更大的页。页表用
于将应用程序看到的虚拟地址转换为物理地址,是一种数据结构,用于多页情况下,在内存中虚拟地址到物理地址的映
射。

[60] 什么是TLB?

TLB: Translation Lookaside Buffer。根据功能可以译为快表,直译可以翻译为旁路转换缓冲,也可以把它理解成页表


缓冲。里面存放的是一些页表文件(虚拟地址到物理地址的转换表)。当处理器要在主内存寻址时,不是直接在内存的物
理地址里查找的,而是通过一组虚拟地址转换到主内存的物理地址,TLB就是负责将虚拟内存地址翻译成实际的物理内存
地址,而CPU寻址时会优先在TLB中进行寻址。处理器的性能就和寻址的命中率有很大的关系。
[61] 什么是页面错误(page fault)?

当程序访问映射到虚拟地址空间但未加载到主存储器中的内存页时,计算机硬件[内存管理单元(MMU)]会发起中
断。 此中断称为页面错误。

[62] 如果CPU正在执行一个任务,如何停止他并运行另一个任务?

可以使用外部中断源来中断CPU上的程序执行。

[63] 什么是中断和异常,它们有何不同?

中断是一个异步事件

通常由外部硬件(I / O设备或其他外围设备)生成,并且不会与指令执行边界同步。例如:键盘,存储设备或USB端
口可能会发生中断。当前指令执行结束后,总是对中断进行服务,并且CPU跳转到执行中断服务程序。

异常是一个同步事件

异常当处理器在执行指令时检测到任何预定义条件时生成的同步事件。例如:当程序遇到被零除或未定义的指令时,
它将生成异常。异常又分为三种类型,程序流的改变方式取决于类型:

1. 故障:在发生故障的指令之前由处理器检测到故障并对其进行维修
2. 陷阱:在导致陷阱的指令之后对陷阱进行维修。最常见的陷阱是用于调试的用户定义中断。
3. 中止:中止仅用于在执行不再继续的情况下发出严重的系统问题信号

[64] 什么是向量中断?

向量中断是一种中断,终端设备使用该中断特有的代码将处理器定向到正确的中断服务程序中,该代码由中断设备与
该中断一起发送给处理器。

而非向量中断,中断服务程序需要读取中断寄存器,解码出导致中断的中断源,然后执行特定的中断服务程序

[65] 有哪些技术可以提高从内存提取指令的性能?

指令缓存和预取

指令缓存和预取算法将在实际的指令解码和执行阶段之前继续提取指令,这可以较小设计中指令提取阶段的存储等待
时间延迟。

分支预测和分支目标预测

分支预测基于历史指令预测是否将发生条件分支,而分支目标预测将有助于在处理器计算之前预测目标。 这能最大程
度地减少指令提取停顿,因为提取算法可以根据预测保持提取指令。

[66] 内存映射I/O(memory mapped I/O,MMIO)是什么意思?

Memory Mapped I/O (MMIO)是一种在CPU与I/O或外围设备之间执行输入/输出(I/O)的方法.CPU使用相同的地址总


线来访问内存和I/O设备(包括I/O设备内部的寄存器或内部的任何内存)。在系统地址映射中,为I/ O设备保留了一些内存
区域,并且当CPU访问该地址时,响应访问并监视该地址总线的相应I/ O设备。例如:如果CPU具有32位地址总线:它可以
访问0到2\^32之间的地址,并且在该区域中,我们可以为一个或多个I/O设备保留地址(例如0到2^10)。

[67] 独热码在设计中有什么好处?

独热码中,状态转换时,会有两位改变,一位清零,一位置一。它的优点时,不需要进行解码就能知道当前的状态。
独热码会使用更多的触发器,但是更加少的组合逻辑,在时序电路中不需要用解码逻辑进行区分状态。
Programming Basics

Basic Programming Concepts

[68] 在任何一种编程语言中,静态(static)变量和自动(automatic)变量,局部(local)变量和全局
(global)变量之间有什么区别?

区分这些名词需要两个概念,作用域(scope)和存储持续时间(storage duration),前者定义了在何处可以访问变量,后
者定义了在何时可以访问变量。

按照变量的作用域可以区分局部(local)和全局(global)变量。局部变量的作用范围有限,尽在声明它们的代码块
中可见。而全局变量在声明后在程序的任何位置都可见。
存储持续时间可以区分自动(automatic)变量和静态(static)变量。静态变量的生命周其一直持续到程序结束,因
此可以始终访问。自动变量具有有限的生命周期,只能持续到程序离开定义的块或者作用域为止。

例如:在以下的systemverilog代码中,global_int被声明为类成员,并且在整个类中具有全局作用域,而当取消引用该
类的对象时,其生命期结束。global_static变量被声明为静态变量并具有全局作用域整个类以及整个程序的生命周期,即使
取消引用类的对象也存在。sum变量对于函数compute()是局部的,并且仅在函数内部可见,并且仅在compute执行时存
在, count变量在函数compute()中是局部变量,仅在函数范围内可见,但由于它是静态的,因此即使在多次执行函数
compute()之后,它也只有单个副本并保留该值.

1 class test_class;
2 int global_int; //automatic by default
3 static global_static; //global static variable
4
5 void function compute()
6 begin
7 static int count; //local static variable
8 local int sum; //local automatic variable
9 sum = sum +1;
10 count = count +sum;
11 end
12 endfunction
13
14 endclass

[69] 什么是内联(inline)函数?

内联函数时调用时会进行展开内联的函数,即编译器会将函数调用替换为相应函数代码。如果函数非常小并且在多个
地方使用,使用内联函数会更有优势。这么做会提高运行速度,没有调用函数和从函数返回的开销。

例如:在C语言中,定义一个名为max的内联函数,在main内部的每次调用都会通过替换代码实现,而不是函数调用
实现。

1 inline int max(int a, int b) {


2 return a > b ? a : b;
3 }
4
5 main () {
6 int a1,a2,a3,b1,b2,b3;
7 int c1,c2,c3;
8 c1 = max(a1,b1);
9 c2 = max(a2,b2);
10 c3 = max(a3,b3);
11 }

[70] 什么是正则表达式?

正则表达式是特殊的字符序列,可以使用特殊的语法帮助用户匹配或查找其他字符串(或字符串集)。 它是用于字符
串内模式匹配的最强大的概念之一,广泛用于Perl,Python,Tcl等语言。

[71] 堆和栈的区别是什么?
栈是内存的一块特殊区域,用于存储由函数创建的临时变量。每次函数声明一个新的自动变量时,它将被压入栈,并
且每次函数退出时,会删除压入栈的所有变量。所有局部变量都使用栈进行存储,并且时自动管理的,也有大小限制。如
果栈的空间不足,则会出现栈溢出错误。

堆则是需要管理的内存区域,程序员需要分配和释放内存,某些语言中是自动完成的。堆通常用于存储静态变量和对
象。与栈相比,堆略慢,并且是通过指针应用的,并且可以在任何位置应用堆的变量。堆的大小也是可以更改的,当可用
内存是不连续的块时,堆可能会出现碎片问题。

[72] a++和++a的区别是?

++a首先“a”自增,然后返回引用“a”的值。 因此,如果将“++ a”分配给变量,则将使用递增值“a”。

a++首先返回值“a”(当前值为“a”),然后“a”自增。因此,如果将“a ++”分配给变量,则将在分配中使用旧值“a”。

[73] 什么是内存泄漏?

当我们动态分配内存但以某种方式失去到达该内存的方式时,这称为内存泄漏。 在某些编程语言(如C ++)中,应释


放(通过调用析构函数)完成的每个内存分配(例如,创建对象),否则,这些内存将泄漏且不再可用。 在某些其他语言
(例如SystemVerilog,Java等)中,语言内部机制负责清理内存,并且内存泄漏的可能性较小。

[74] 编译器和解释器的区别是什么?

机器(例如计算机)理解代码是通过二进制的,机器可以理解的二进制代码称之为“机器码”。程序员通常使用高级编
程语言(C,C++,Perl,Python)变写计算机程序或者代码。编译器和解释器就是将这些源代码转换为机器代码的程序。

编译器 解释器

扫描整个程序并将整个源代码转换为机器代码 一次扫描并转化一行源代码

需要大量的时间去分析源代码 只需要少量时间用于分析源代码

输出机器专用的二进制码 输出代码是某种中间代码,由另一个程序解释

执行速度更快(计算机硬件运行) 执行更慢(由另一个程序执行)

扫描整个程序后报告错误 一直运行,直到遇到第一个错误,并且停止程序

[75] 静态语言和动态语言的区别是什么?

静态语言:静态语言是一种在编译时固定类型的语言。 这意味着您需要在使用它们之前声明所有变量及其数据类型。
例如:Java,C和SystemVerilog是静态类型的语言。

动态语言:动态语言是一种在执行时确定类型的语言。这与静态类型的语言相反。 例如:VBScript和Python是动态类
型的,因此在使用之前不需要声明所有变量及其数据类型。 他们会在首次为变量分配值时弄清楚变量的类型。

[76] 下面关于栈的观点哪个是错的?

1. 栈只能push或者pop
2. 可以使用栈实现FIFO
3. 栈对于嵌套循环,子程序调用很有用
4. 队长堆算术表达式计算有用

选项2是错的,栈是LIFO而非FIFO,先入后出。

[77] Perl中“use”和“require”的主要区别是?

use在编译时起作用,require在运行时起作用
use隐式调用了将要加载的模块,require则没有
use引入的名称不需要后缀名,而require需要
use引入模块的同时,也引入了模块的子模块。而require则不能引入,要再重新声明

[78] 静态内存分配和动态内存分配有什么区别?
静态内存分配 动态内存分配

内存在编译时分配 内存在运行时分配

内存被分配到栈上或者程序的其他部分 内存分配到堆上

不需要释放内存,静态变量的生命周期就是程序的生命周期 需要及时释放内存

固定大小,一旦分配以后内存大小就不能改变 能够改变大小

执行更加快 执行更加慢

[79] 什么是编译预处理命令?

在代码中,预处理器指令是以#开头的行。它们充当预处理程序的指令,预处理程序在代码编译开始之前检查代码。
其结果就是替换了源代码中的某些代码。例如:预处理程序指令的常规语法为:#define标识符值每当预处理程序在源代码
中遇到“标识符”时,它将用“值”替换,并在编译之前生成新的源代码。

[80] C++代码中"using namespace std"的功能是什么?

namespace是指标识符的各种可见范围。命名空间用关键字namespace 来定义。命名空间是C++的一种机制,用来把
单个标识符下的大量有逻辑联系的程序实体(例如类、对象和函数)组合到一起。"std"是"standard"一词的缩写。 standard
namespace (std namespace)是一种特殊类型的名称空间,其中保留了所有内置的C ++库(例如字符串,cin,cout,
vector等)。 因此,"using namespace std"告诉C ++编译器使用标准C ++库。

[81] 以下两种初始化的方式有什么区别:“int a;” and “const int a;”?

const关键字告诉编译器,该变量或对象一旦进行初始化便不可更改。所以,int a 声明后,后续可以对变量a进行更
改,而const int a,后续不可更改

[82] C语言中的关键词volatile是什么意思?

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,
都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,
如果这个变量由别的程序更新了的话,将出现不一致的现象。volatile关键字主要在与内存映射的输入输出(硬件)接口时
使用。 变量声明为volatile之后,编译器将无法执行任何优化,例如:删除内存分配,将变量缓存在寄存器中或更改分配的
执行顺序。

[83] 解释指针的概念

指针是一个变量,其值是另一个变量的地址。星号*表示指针,int * p 告诉编译器变量“p”是一个指针,其值是存储整
数变量的存储位置的地址。 同样,float * f; 告诉编译器变量“ f”是一个指针,其值是存储浮点变量的存储位置的地址。以下
列代码为例

1 int a = 10;
2 int *b;
3 int c;
4 b = &a;
5 c = *b;
6 printf(“b=%d and c=%d\n”,b,c);

其中a是一个变量,他的值是10,b是一个指针,通过语句 b = &a 将a的地址传给了指针b。而通过c = *b 将指针b内地


址所指向的值,即a的值赋予c。
[84] 解释C语言中的“值传递”、“地址传递”和“引用传递”的区别

值传递:在这种情况下,函数会用一块新的内存去存储变量,将参数的值复制进来,并且函数内部对参数的修
改,不会影响到外部。下例中,在调用Exchg1(a,b)时最开始做的两个隐含动作是:int x=a;int y=b; 及 x=a;y=b;
原来函数在调用时是隐含地把参数a,b的值分别赋值给了x,y。之后在函数体内一直是对形参x,y进行操作。并没有
对a,b进行任何操作。函数只是把a,b的值通过赋值传递将值传递给了x,y。函数里操作的只是x,y的值,并不是a,b
的值。这就是所谓的值传递

1 void Exchg1(int x, int y)


2 {
3 int tmp;
4 tmp = x;
5 x = y;
6 y = tmp;
7 printf("x = %d, y = %d\n", x, y);
8 }
9 main()
10 {
11 int a = 4,b = 6;
12 Exchg1(a, b);
13 printf("a = %d, b = %d\n", a, b);
14 return(0);
15 }

地址传递:地址传递的参数为指针,函数内部实际上是通过指针实现的,通过指针的方式寻址,这种修改会对
外部的值产生影响。下例中:在调用Exchg2(&a,&b)时最开始做的两个隐含动作是:int *px=&a;int *py=&b;.及
px=&a;py=&b; 原来函数在调用时是隐含地把参数a,b的地址分别传递给了指针px,py。之后在函数体内一直是对
指针px,py进行操作。也就是对a,b的地址进行的操作。

1 void Exchg2(int *px, int *py)


2 {
3 int tmp = *px;
4 *px = *py;
5 *py = tmp;
6 printf("*px = %d, *py = %d.\n",*px, *py);
7 }
8 main()
9 {
10 int a = 4,b = 6;
11 Exchg2(&a, &b);
12 printf("a = %d, b = %d.\n", a,b);
13 return(0);
14 }

引用传递:这种情况下会将参数的地址复制进来,函数内对参数的修改会反映到外部。通常通过这种方式减小
对内存的消耗,例如数组的传递,使用引用穿的可以减小内存消耗。下例中:与值传递相比,代码上只有只有
一处不同,即函数定义处:void Exchg3(int &x, int &y) Exchg3函数的定义处Exchg3(int&x, int &y)。调用时我们
可以像值传递(如: Exchg1(a, b); )一样调用函数(如: Exchg3(a,b);)。但是x、y前都有一个取地址符号
“&”。有了这个,调用Exchg3时函数会将a、b 分别代替了x、y了,我们称:x、y分别引用了a、b变量。这样函
数里操作的其实就是实参a、b本身了,因此函数的值可在函数里被修改

1 void Exchg3(int &x, int &y)


2 {
3 int tmp = x;
4 x = y;
5 y = tmp;
6 printf("x= %d,y = %d\n", x, y);
7 }
8 main()
9 {
10 int a = 4,b =6;
11 Exchg3(a, b);
12 printf("a= %d, b = %d\n", a, b);
13 return(0);
14 }

[85] NULL指针的值和大小是多少?

NULL指针可以定义为:int * a = NULL; NULL指针的值为0。指针是一个变量,其值是另一个变量的地址。 由于指针的


值是地址,所以指针的大小会因机器而异。 如果是32=4*8位计算机,则指针大小为4个字节,如果计算机大小为64=8*8
位,则指针大小为8个字节。

[86] 什么是链表?一共有几种类型的链表?

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存
储数据元素的数据域,另一个是存储下一个结点地址的指针域。

一共有三种不同类型的链表:

1. 单向链表
2. 双向链表
3. 循环链表

[87] 以下算法的“最坏情况”时间复杂度是多少?

1. 线性搜索
2. 二进制搜索
3. 插入排序
4. 合并排序
5. 桶排序

算法的时间复杂度代表了算法的运行时间,n代表输入算法的参数数量。通常使用big O算法进行评估,例如某算法隐
形时间为5n^4 + 6n^2 + 1,取最高阶为n^4,那么其算法复杂度为O(n^4)。所以以上算法的算法复杂度为:

1. O(N)
2. O(log(N))
3. O(N2)
4. O(N*log(N))
5. O(N)

[88] 以下算法的空间复杂度是多少?

1. 线性搜索
2. 二进制搜索
3. 插入排序
4. 合并排序
5. 桶排序

空间复杂度的概念类似于时间复杂度,但是衡量的值是算法运行时所需要的内存空间。以上算法的空间复杂度为:

1. O(1)
2. O(1)
3. O(N)
4. O(N)
5. O(N)
[89] C/C++中,"&"和"&&"有什么区别?

&是按位与运算符,而&&是逻辑与运算符。 逻辑运算符使用布尔值-真(1)和假(0),并返回布尔值。 按位运算符对每


个位执行位操作并返回位值。

按位运算符:如果a = 10而b = 6,则a&b将返回2(4'b1010&4'b0110 = 4'b0010)

逻辑运算符:如果a = 10而b = 6,则以下表达式将返回true,因为对两个布尔值进行操作,则为true c =(a == 10)&&


(b == 6);

[90] “Struct” 和 “Union” 在 C/C++ 中,内存分配上有什么不同?

Struct分配足够的空间来存储结构中的所有字段/成员。 第一个存储在Struct的开头,第二个存储在Struct的开头,依此
类推。

Union仅分配足够的空间来存储列出的最大字段,并且所有字段都存储在同一空间中。 这是因为在Union中,一次只
能使用一种类型的封闭变量,而不是可以引用所有封闭变量的struct。

[91] 下面这个结构体需要多大的内存进行存储?

1 struct ID {
2 int IntID;
3 char CharID[8];
4 };

需要12个字节,int需要4个字节,char数组需要8个字节。

[92] 下面这个联合体需要多大的内存进行存储?

1 union ID {
2 int IntID;
3 char CharID[8];
4 };

需要8个字节,数组CharID需要8个字节。

[93] 什么是内核(kernel)?

内核是一种计算机程序,它用于管理来自软件的输入/输出请求,并将这些请求转换为CPU指令或其他指令。

[94] perl代表什么意思?

Practical Extraction and Reporting Language。

[95] perl中有多少种不同类型的变量?

标量(scalars):标量用$定义,标量是perl中最简单的变量。 标量可以是数字,也可以是字符串或引用。
数组(arrays):数组用@定义,数组是标量的有序列表,数组的索引是从0开始的。
哈希(hashes):哈希用%定义,哈希是键/值对的无序集合,可以将键用作下标来访问。

[96] 什么是Cron Job?如何使用Cron Job?

Cron Job是操作系统中基于时间的作业调度程序。 它允许在指定的时间,日期,间隔等自动定期运行作业。例如:假


设用户具有Shell或Perl脚本,该脚本计算UNIX / Linux中磁盘的人均磁盘空间使用情况。 在UNIX / Linux中为此脚本以指定
的频率(或时间)设置Cron Job将确保该脚本在计划的时间(或频率)下自动运行,而用户无需每次都手动运行它。

[97] 在UNIX / Linux中,“ rsync”命令的用途是什么?


“ rsync”代表“Remote Sync(远程同步)”,它是在磁盘,网络,服务器和机器之间复制或同步文件/目录的常用命令。
rsync仅移动文件中已更改的那些部分,因此可以将需要复制的数据量减至最少。 “ rsync”在发送和接收数据时使用某些压
缩和解压缩方法,进步减小带宽消耗。 “ rsync”命令最常见的用途之一是在两台计算机之间执行数据备份和镜像磁盘等操
作。

[98] C/C++中"\0"字符的用途是什么?

字符串总是以'\0'作为串的结束符。因此当把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字
符串是否结束的标志。

[99] 什么是二叉树?

二叉树是链表概念的扩展。 一个二叉树的节点有两个指针:“一个左指针”和“一个右指针”。 每一个节点可以进一步分


支以形成另外的节点,每个节点也具有两个指针。

[100] 什么是正则表达式中的特殊字符、量词和锚点?

特殊字符是为正则表达式用于搜索的,具备特殊含义的元字符。 示例:\,^,$,(),[],|,&
量词用于指定匹配前面的正则表达式的“频率”。 示例:*, +, ?, {}
锚点指正则匹配时的匹配的位置。锚点允许用户指定文本搜索的位置。示例:^, $, <, >

Object Oriented Programming Concepts

[101] 类和对象有什么区别?

类是可以组合在一起的一组属性和相关行为。 对象是类的实例,表示具有属性和行为的真实实体。 可以使用类数据成


员来表示属性,而可以使用方法来表示行为。 例如:可以将动物表示为一类,而不同的动物(如狗,猫等)可以是该动物
的对象。

[102] C++的类和结构体有什么区别?

最初,在C中定义了一个“结构体”,以将不同的数据类型组合在一起以执行某些已定义的功能。 但是,在C++中,这
种结构体也扩展为包括函数的结构。 “类”也是一种数据类型,可以将不同的数据类型和其对应的方法进行分类。 C++中两
者的区别之一是,类的所有成员默认情况下都是私有的,而结构的所有成员默认情况下都是公共的。

[103] Systemverilog中的类和结构体有什么区别?

在SystemVerilog中,基于要执行的某些功能,类和结构都用于定义一堆数据类型。 但是,结构是整体式的类型,在声
明结构时会分配必要的内存。 类是动态类型,一旦声明了一个类,就只能将一个类句柄引用为null。 内存分配仅在创建该
类的实际对象时发生。

[104] 什么是public, private 和 protected 成员?

这三者是类成员的不同访问属性

类的private成员只能从该类内部访问。 这些数据成员在派生类中将不可见。
public成员可以从该类内部也可以在类外部访问。
protected数据成员与private成员类似,因为它们只能在该类中访问。 但是,与private成员不同,这些成员在派
生类中也可见。

[105] 什么是多态

多态性是指具有多种形式的能力。 在OOP上下文中,这是指实体在运行时引用各种类的对象的能力。 这可以通过


SystemVerilog中的继承和虚函数的概念(以及C++中存在的函数和运算符重载的概念)来实现。根据对象的类型,将从相
应的类中调用适当的方法。

[106] 什么是Method Overriding和Method Overloading? 两者有什么区别?

Method Overriding:重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改


变。即外壳不变,核心重写。
Method Overloading:重载是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

[107] 什么是运算符重载?

在面向对象的编程中,运算符重载是多态的一种特殊情况,可以重新定义或重载可用的不同内置运算符。 因此,程序
员也可以将运算符与用户定义的类型一起使用。 C++支持此功能,而SystemVerilog不支持此功能。 以下示例显示了一个
Testclass,其中运算符+被重载,从而可以把两个类型为“Testclass”的类对象相加。 然后,实现将来自两个对象的数据成员
相加,并将其分配给结果类的数据成员。

1 #include <iostream>
2 class Testclass{
3 public:
4 int a;
5 int b;
6 Testclass operator+(const Testclass& obj);
7 }
8
9 Testclass Testclass::operator+(const Testclass& obj2){
10 Testclass tmp_obj = *this;
11 tmp_obj.a = tmp_obj.a + obj2.a;
12 tmp_obj.b = tmp_obj.b + obj2.b;
13 return tmp_obj;
14 }
15
16 int main(void){
17 Testclass obj1, obj2, obj3;
18 obj1.a = 1;
19 obj1.b = 1;
20 obj2.a = 2;
21 obj2.b = 2;
22 obj3.a = 0;
23 obj3.b = 0;
24 obj3 = obj1 + obj2;
25 std::cout<<obj3.a<<" "<<obj3.b<<"\n";
26 return 0;
27 }

[108] 什么是构造函数?

构造函数是类的特殊成员函数,每当创建该类的实例时,构造函数就会自动调用。 在C++中,它与类具有相同的名
称。 在SystemVerilog中,它作为new()函数实现。

[109] 什么是析构函数?

与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。 在C++中,
它与类具有相同的名称,并带有波浪号字符前缀,而在SystemVerilog中,由于该语言支持自动垃圾收集,因此没有析构函
数。

[110] OOP中的组合(composition)和继承(inheritance)之间有什么区别?

组合使两个类之间具有“has - a”关系。 当一个类实例化另一个类的对象时,该关系为“ has-a”,并且此属性称为


composition。

继承使两个类之间具有“is - a”关系。 当一个类从另一个类派生时,该关系为“ is-a”,并且此属性称为继承。

下图说明了这一点。 基类汽车中派生出福特类,则该关系为“is-a”,这意味着福特类为汽车类。 如果福特类内部具有


引擎类的对象,则关系为“has - a”,如图所示。
[111] OOP的浅拷贝和深拷贝有什么区别?

在浅拷贝中,将创建一个新对象,该对象具有与原始对象中的值完全相同的副本。 如果对象的任何字段是对其他对象
的引用,则仅复制引用地址(句柄)。在深拷贝中,将创建一个新对象,该对象具有与原始对象相同的值的精确副本。 如
果任何对象都引用了其他对象,则还将复制属于该对象的所有值的副本,而不仅仅是内存地址或句柄。因此,称为深拷
贝。

例如,对比如下两个类。

1 class A;
2 int a;
3 int b;
4 endclass
5
6 class B;
7 int c;
8 A objA;
9 endclass

如果在类B中实现了浅拷贝方法,则当我们将B复制到新对象时,仅复制“ objA”的内存句柄。 在深度复制的情况下,还


将复制A的所有值(即其数据成员a和b),而不是“objA”的内存句柄。

[112] 什么是OOP的虚方法?

虚方法是在基类中声明的成员方法,并且可以由派生类重新定义。 要创建虚方法,在基类中的方法声明之前要加上关
键字virtual。 在派生类中重新定义基类方法的这种方式也称为方法重写。使得调用方法时,是根据对象类型而不是句柄类
型调用函数。

[113] 什么是多重继承?

多重继承是某些面向对象的计算机编程语言的功能,其中对象或类可以从多个父对象或父类继承特征和功能。 它不同
于单一继承,在单一继承中,一个对象或类只能从一个特定的对象或类继承。注意:C++支持多重继承,而SystemVerilog
语言则不支持。

[114] 什么是抽象类?

抽象类是包含一个或多个抽象方法的类。 抽象方法是已声明但不包含任何实现的方法。 抽象类可能无法实例化,并且


需要子类为抽象方法提供实现。 在SystemVerilog中,类名前面带有虚拟关键字,以使其成为抽象类。 以下是如何使用函
数定义为virtual定义抽象类的示例。 然后派生的类可以实现此功能。相当于一个模板类。

[115] 什么是类的静态方法?

静态方法是使用static关键字在类内部定义的方法。 可以在不创建类对象的情况下使用它们。 同样,如果有多个此类


创建的对象,则仍然只有一个静态方法成为所有对象的一部分。

[116] 类的this指针是什么意思?

该指针是一个特殊的指针,可用于在类范围内引用该类的当前对象。
[117] type conversion 和 type casting的区别是?

type conversion 和 type casting的最大区别就是,type conversion有编译器自动(隐式)转换的,而type casting是显式


完成的。



type casting type conversion

意 一个数据类型由用户分配给另一个数据类型,使用强制 编译器自动将一种数据类型转换为另一种数据
义 转换运算符,称为"type casting"。 类型称为"type conversion"。

应 仅当两个数据类型"兼容"时,才能实现类型转
类型强制转换也可以应用于两个"不兼容"的数据类型。
用 换。

算 要将数据类型强制转换到另一个数据类型,需要强制转
无需操作符。
子 换运算符"()"。


它在程序设计过程中完成。 它在编译时显式完成。

下面第一个例子是type casting,第二个是type conversion

1 int a;
2 byte b;
3 ...
4 ...
5 b= (byte) a;
6 ////////////////////////////////////////
7 int a=3;
8 float b;
9 b=a; // value in b=3.000.

UNIX/Linux

[118] 如何找到有关UNIX/Linux命令做什么的详细信息?

通过man <command-name> ,例如man grep

[119] 编写UNIX/Linux命令完成以下任务,假设文件名为file.txt

1. 显示文件的前10行
2. 显示文件的第10行
3. 从文件中删除第13行
4. 从文件中删除最后一行
5. 反转字符串(例如:“ Hello” )
6. 检查上一条命令是否成功
7. 查找文件中的行数
8. 查找文件中的字符数
9. 查找文件中第17行的字符数
10. 获取第三个单词文件中第17行的内容
11. 将所有用户的文件权限更改为“读取”和“可执行”。
12. 将文件的组访问权限更改为组。(假设新的组名称为“ new_group”)
13. 将两个文件(file1.txt和file2.txt)的内容移动到一个文件(file.txt)
14. 显示本账号下的所有进程
15. uniquely排序文件(file1.txt)的内容并将其复制到另一个文件(file2.txt)
16. 检查用户名
17. 登录到远程主机(例如“远程服务器”)

1 任意一种:
2 a) head -10 file.txt
3 b) cat file.txt | head -10
4 c) sed “11,$ d” file.txt
5 head -10 file.txt | tail -1
6 sed -i “13 d” file.txt
7 sed -i “$ d” file.txt
8 echo “Hello” | rev
9 echo $?
10 cat file.txt | wc -l
11 cat file.txt | wc -c
12 head -17 file.txt | tail -1 | wc -c
13 head -17 file.txt | tail -1 | cut -f3 -d’ ‘
14 chmod 555 file.txt
15 chgrp new_group file.txt
16 cat file1.txt file2.txt > file.txt
17 ps -aef
18 sort -u file1.txt > file2.txt
19 whoami
20 ssh username@remote-server

[120] 编写UNIX/Linux命令,按照要求显示文件内容,假设文件名为file.txt

1. 所有匹配“cat”的行
2. 所有单词“ cat”的行
3. 所有不包含“cat”的行
4. 所有包含单词“ cat”的行(不区分大小写)
5. 所有以“cat”开头的行
6. 所有以“ cat”结尾的行
7. 所有包含“cat”和“123”的行(“cat”出现在“123”之前)

1 grep “cat” file.txt


2 grep -w “cat” file.txt
3 grep -v -w “cat” file.txt
4 grep -i “cat” file.txt
5 grep “^cat” file.txt
6 grep “cat$” file.txt
7 grep “cat.*123” file.txt

[121] 编写UNIX/Linux命令以列出目录中所有文件的名称(例如/usr/bin/dir/)(及其子目
录),文件应该包含不区分大小写的“I am preparing for Interview”。

1 grep -ilr “I am preparing for Interview” /usr/bin/dir/*

[122] 有一个文件(例如/usr/home/file.txt)包含目录列表。 编写一组UNIX/Linux命令,以查


看该文件的内容,进入每个目录并运行一个进程(例如script.pl)。 假设文件
(/usr/home/file.txt)的每一行仅包含一个目录的路径。

1 foreach x (`cat /usr/home/file.txt`)


2 foreach> cd $x
3 foreach> script.pl
4 foreach> end

[123] 编写UNIX/Linux命令,该命令将所有非空白行从文件(file1.txt)移至另一个文件
(file2.txt)

1 grep -v “^$” file1.txt > file2.txt

[124] 编写一个UNIX/Linux命令(假设filename = file.txt):

1. 查找当前目录或其子目录中是否存在某个文件
2. 查找某个文件是否在目录“/usr/bin/DIR”或其子目录中
3. 查找某个文件是否仅存在于当前目录中
4. 查找当前目录或其子目录中是否包含名称中包含特定单词“dummy”的文件
5. 查找当前目录或其子目录中是否存在不区分大小写的文件“file”
6. 查找所有名称不是“file.txt”且存在于当前目录或其子目录中的文件
7. 重新运行以前执行的find命令

1 find . -name “file.txt” OR find -name “file.txt”


2 find /usr/bin/DIR -name “file.txt”
3 find -maxdepth 1 -name “file.txt”
4 find . -name “*dummy*”
5 find . -iname “file”
6 find -not -name “file.txt”
7 ! find

[125] 编写一个UNIX/Linux命令:

1. 列出在计算机上以你的名字设置的所有Cron Jobs
2. 列出用户在计算机上设置的所有Cron Jobs
3. 删除计算机上以你的名字设置的所有Cron Jobs
4. 删除用户在计算机上的所有Cron Jobs(如果你有权这样做)
5. 在计算机上以您的名字编辑Cron Job。
6. 设置每天下午6:30运行的Cron Jobs
7. 设置每分钟运行一次的Cron Jobs。
8. 设置一个Cron Jobs,该作业在每个月的前20天上午6:30运行
9. 设置仅在每月的星期五的6:30 AM和6:30 PM运行的Cron Jobs

1 crontab -l
2 crontab -u <user_name> -l
3 crontab -r
4 crontab -u <user_name> -r
5 crontab -e
6 30 18 * * * <command_to_invoke_your_process>
7 * * * * * <command_to_invoke_your_process>
8 30 6 1-20 * * <command_to_invoke_your_process>
9 30 6 18 * * 6 <command_to_invoke_your_process> (assuming Sunday is represented by 0)

[126] 列出下列shell中的快捷键

1. 杀死进程
2. 将在终端上运行的进程移至后台
3. 将光标移至Shell上命令的开头
4. 将光标移至Shell上命令的结尾
5. Ctrl + c
6. Ctrl + z
7. Ctrl + a
8. Ctrl + e

Programming in C/C++

[127] 编写C代码以检测计算机中的架构是little Endian 还是 big Endian

什么是大小端请参考问题[32]
1 #include <stdio.h>
2 int main() {
3 unsigned int i = 1;
4 char *c = (char*)&i;
5 if (*c)
6 printf("Little Endian \n");
7 else
8 printf("Big Endian \n");
9 return 0;
10 }

[128] 经过下列代码后,b和c的值是多少?

1 a = 10;
2 b = a++;
3 c = ++a;

b等于10,而c等于12。后置自增运算符仅在赋值后才进行自增,因此b得到的是自增前的值。 前置增量运算符将首先
进行自增,因此a将从11(在b = a++后变为11)增加到12

[129] 下列代码的输出是什么?

1 #include<stdio.h>
2 int xyz=10;
3 int main() {
4 int xyz=20;
5 printf("%d",xyz);
6 return 0;
7 }

变量xyz定义了全局变量和局部变量,而在函数中,优先调用的是局部变量,所以将为打印出20.

[130] 下列代码中,y的值是多少?

1 int main() {
2 int x=4;
3 float y = * (float *) &x;
4 return 0;
5 }

一个很小的值。 一些编译器可能会将答案显示为0。“(float *)&x”,告诉编译器指针指向存储在内存位置的浮点数。


浮点数的存储方式不同于整数(对于浮点数,位[31]表示带符号的位,位[30:23]表示指数,位[22:0]表示分数)。 因此,
当解释为浮点数(00000000000000000000000000000100)时,值将为非常小。

[131] 下列C程序的输出是什么?

1 #include<stdio.h>
2 int main() {
3 int i=0;
4 for(i=0;i<20;i++) {
5 switch(i) {
6 case 0:i+=5;
7 case 1:i+=2;
8 case 5:i+=5;
9 default: i+=4;
10 break;
11 }
12 printf("%d\n",i);
13 }
14 return 0;
15 }
输出是16,21。

注意两点,i在循环内进行了修改,case后没有跟着break。第一次进入循环,i将一次加5 2 5 4,然后打印输出16,最
后再加1。第二次直接进入default,加4,然后输出21。

[132] 编写一个递归函数求n的阶乘,n为正整数

1 int factorial (int x) {


2 if ( (x==0) || (x==1) )
3 return 1;
4 else
5 return (x*factorial(x-1));
6 }

[133] 编写一个递归函数求斐波纳契数列

1 int fibonacci (int num){


2 if( (num==0) || (num==1) )
3 return num;
4 else
5 return (fibonacci(num-1) + fibonacci(num-2));
6 }

[134] 下列代码在64位机上的输出是什么?

1 #include <stdio.h>
2 int main() {
3 int x = 10000;
4 double y = 56;
5 int *p = &x;
6 double *q = &y;
7 printf("p and q are %d and %d", sizeof(p), sizeof(q));
8 return 0;
9 }

输出是p and q are 8 and 8 。

由于“p”和“q”是指针,因此它们只不过是64位计算机中的地址。 无论它们指向整数还是双精度数据类型,两者的大小
均为64位(8字节)。

[135] 什么是链表?何时使用链表?

链表是由一组节点组成的数据结构,这些节点一起代表一个序列。链表是由一组节点组成的数据结构,这些节点一起
代表一个序列。如果我们不知道要存储的数据量,则首选链表。 例如:我们可以在员工管理系统中使用链接列表,在这里
我们可以轻松地添加新员工的记录(添加新节点-动态内存分配),删除旧员工的记录(删除节点),编辑 员工记录(在
节点中编辑数据)。

在[136]-[140]中,使用下列变量和定义:
1 struct node;
2 typedef struct node NODE;
3 typedef int Element;
4
5 // A pointer to a node structure
6 typedef NODE *LINK;
7
8 // A node defined as having an element of data
9 // and a pointer to another node
10 struct node { Element elem; LINK next; };
11
12 // The Head or start of the List
13 typedef struct { int size; LINK start; } ListHead;

[136] 编写一个C程序用于创建单链表

要创建单链表,我们需要:

1. 创建链表的HEAD(h)
2. 初始化链表的大小(为零)
3. 将起始指针指向NULL(在创建时为空)。

请参考以下函数来创建单链表:

1 ListHead createList() {
2 ListHead h;
3 h.size = 0;
4 h.start = NULL;
5 return h;
6 }

[137] 编写一个C程序用于在单链表的头部插入一个元素

在链表(h)的头部插入元素(e)时,我们需要:

1. 为新节点动态分配内存。
2. 为新节点中的元素分配值。
3. 将新节点中的“next”指针指向HEAD先前指向的节点。
4. 在链接列表HEAD中,增大“size”变量(随着添加了新节点),然后将“start”指针指向新节点。

1 ListHead InsertElementAtHead(Element e, ListHead h) {


2 LINK nl= (LINK) malloc (sizeof(NODE));
3 nl->elem = e;
4 nl->next = h.start;
5 h.start= nl;
6 h.size++;
7 return h;
8 }

[138] 编写一个C程序用于在单链表的尾部插入一个元素

在链接列表(h)的末尾插入元素(e)时,我们需要:

1. 为新节点动态分配内存。
2. 为新节点中的元素分配值。
3. 将新节点中的“next”指针指向NULL(因为新节点代表链表的尾部)。
4. 如果链表最初为空,则将HEAD中的“start”指针指向新节点,否则遍历链接列表以找出链接列表中的最后一个节
点,并将最后一个节点中的“next”指针指向新节点。
5. 在链表HEAD中增大“size”变量(随着添加了新节点)。

1 ListHead InsertElementAtTail(Element e, ListHead h) {


2 LINK temp;
3 LINK nl;
4 nl=(LINK) malloc (sizeof(NODE));
5 nl->elem=e; nl->next=NULL;
6 if(h.start==NULL)
7 h.start=nl;
8 else {
9 temp=h.start;
10 while(temp->next!=NULL)
11 temp=temp->next;
12 temp->next=nl;
13 }
14 h.size++;
15 return h;
16 }

[139] 编写一个C程序用于在单链表的pos处插入一个元素

在链表(h)中的pos处插入元素(e)时,我们需要:

1. 为新节点动态分配内存,
2. 为新节点中的元素分配值。
3. 如果“pos”大于链表的大小,则返回错误消息(因为这是不可能的)。 否则,如果“ pos”为“ 0”,则将元素插入头
部(如上所示)。 否则,将链表遍历到“ pos”之前的节点。 将新节点中的“next”指针指向“pos-1”处的节点所指
向的节点,并将节点中“pos-1”处的“next”指针指向新节点。
4. 在链表HEAD中增大“size”变量(随着添加了新节点)。

1 ListHead InsertAtPos(Element e, ListHead h, int pos) {


2 LINK temp;
3 LINK nl;
4 nl=(LINK)malloc(sizeof(NODE));
5 nl->elem=e;
6 int count=0;
7 if(pos>h.size) {
8 printf("Error: Wrong position \n");
9 return h;
10 }
11 if(pos==0) {
12 nl->next=h.start;
13 h.start=nl;
14 } else {
15 for (temp = h.start; count<(pos-2); temp = temp->next, count++) ;
16 nl->next=temp->next;
17 temp->next=nl;
18 }
19 h.size++;
20 return h;
21 }

[140] 编写一个C程序用于删除单链表的一个元素

从链表(h)中删除元素(e)时,我们需要:

1.检查链表是否为空。 如果为空,则无需删除任何内容。

2.如果链表不为空,则需要遍历链表以找到包含元素(e)的节点。 找到节点之后,我们需要在要删除的节点之前更
改节点中的“next”指针,以指向要删除的节点的“next”指针中存的值。

3.减小链表HEAD中的“size”变量(因为删除了节点)。

1 ListHead DeleteElement(Element e, ListHead h) {


2 LINK cur, prev;
3 cur=h.start;
4 if(cur==NULL) {
5 printf ("Empty List \n");
6 return h;
7 }
8 while(cur!=NULL) {
9 if(cur->elem==e) {
10 if(cur==h.start)
11 h.start=cur->next;
12 else
13 prev->next=cur->next;
14 free(cur);
15 h.size--;
16 break;
17 }
18 prev=cur;
19 cur=cur->next;
20 }
21 return h;
22 }

Programming in PERL

[141] 下列Perl代码的输出是什么?

1 my @value_array = ("Index0","Index1");
2 my $value;
3 foreach $value (@value_array){
4 $value =~ s/Index//;
5 }
6 print "@value_array\n";

结果:0 1

在foreach中使用$value索引,将会改变数组的值

[142] 下列Perl代码的输出是什么?

1 my @value_array = ("Index0","Index1");
2 my $value;
3 for(my $i=0; $i<@value_array; $i++) {
4 $value = $value_array[$i];
5 $value =~ s/Index//;
6 }
7 print "@value_array\n"

$value对于for循环来说是局部的,不会影响数组内容

[143] 在Perl中的“-w”和“use strict”的作用是?

-w是用于标记warning,对潜在的歧义代码进行警告。

use strict是Perl中编译指令,是提供给Perl编译器的指令,告诉编译器,如果perl代码中有不好的编码风格,那么提示
编译失败。也就是说,加上use strict后,我们的Perl代码的编写必须遵循一些规范,否则编译器会报错。

[144] 下列Perl代码的输出是什么?

1 my $line_in_a_file = "I am preparing for an Interview";


2 my $line_in_a_file =~ s/a/A/;
3 print "$line_in_a_file\n";

匹配第一个a,替换为A。因此输出为“I Am preparing for an Interview”

[145] 下列Perl代码的输出是什么?

1 my $line_in_a_file = "I am preparing for an Interview";


2 my $line_in_a_file =~ s/a/A/g;
3 print "$line_in_a_file\n";
g代表global,进行全局匹配,将所有的a都替换为A。因此输出为“I Am prepAring for An Interview”

[146] 在Perl中如何将两个字符串进行拼接?在空白处填充

1 my $string1 = "I am preparing ";


2 my $string2 = "for an Interview";
3 my $string = __?__

Perl使用“.”进行填充。因此空白处应该填写$string1.$string2

[147] 下面程序的输出是什么?

1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 my $scalar =0;
5 my @array = ("A","B","C","D");
6 $scalar = @array;
7 print "Scalar is $scalar\n";

标量会存储数组的元素数量,因此打印出来的值是4

[148] 正则匹配的特殊字符有哪些?解释他们的作用

1. \转义字符。 使元字符成为文字
2. ^匹配字符串/行开头的字符
3. $匹配字符串/行结尾的字符
4. .匹配除换行符以外的所有字符
5. *匹配0或更多次
6. +匹配1或更多次
7. -表示字符类中的范围(如a-z)
8. &匹配到的字符,通过$&引用
9. ()分组字符
10. []字符类以匹配单个字符
11. {}指定量词范围
12. <>指定单词锚点
13. ?匹配0或1次
14. |从多个模式中选择一个

[149] 正则表达式中的量词有哪些?他们的用法是?

1. *匹配0或更多次
2. +匹配1或更多次
3. ?匹配0或1次
4. {N}匹配N次
5. {N,}匹配至少N次
6. {N,M}匹配至少N次之多M次

[150] 正则表达式的锚位有哪些?他们的用法是?

1. ^匹配字符串开头
2. $匹配字符串结尾
3. <单词开头
4. >单词结尾
5. \b单词与非单词的边界
6. \B匹配\b所不能匹配的位置

在[151]-[155]中使用如下代码,针对问题再下面的空白处填空

1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 my $input_file = "input_file.txt";
5 my $output_file = "output_file.txt";
6 my @input_array;
7
8 open(OUTPUT_FILE,'>',$output_file) or die "Cannot Open $output_file file for writing\n$!\n";
open(INPUT_FILE,'<',$input_file) or die "Cannot Open $input_file for reading\n$!\n";
9
10 while(<INPUT_FILE>){
11 if($_ =~ /__?__/){
12 print OUTPUT_FILE $_;
13 }
14 }
15
16 close INPUT_FILE;
17 close OUTPUT_FILE;

[151] 将input_file.txt中仅包含小写字母(a到z)的所有行复制到output_file.txt

^([a-z]+)$

[152] 将input_file.txt中仅包含字母(a到z或者A-Z)的所有行复制到output_file.txt

^([a-zA-Z]+)$

[153] 将input_file.txt中仅包含字母或数字(a到z或者A-Z或者0-9)的所有行复制到
output_file.txt

^([a-zA-Z0-9]+)$

[154] 将input_file.txt中的所有行复制到output_file.txt

\$

[155] 将input_file.txt中的仅包含“\”或者“$”的所有行复制到output_file.txt

^([\\\$]+)$

[156] Perl中chop和chomp的作用是什么?

chop:删除字符串的最后一个字符,并返回该字符

chomp:删除字符串结尾的换行符,并返回删除的字符数

[157] 下列Perl代码的输出是什么?

1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 my $example_1 = "chop_example";
5 my $example_2 = "chop_example";
6 chop($example_1);
7 my $b = chop($example_2);
8 print "$example_1 AND $b\n";

chop_exampl AND e

参考[157]

[158] 下列Perl代码的输出是什么?
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 my $example_1 = "chomp_example\n";
5 my $example_2 = "chomp_example\n";
6 chomp($example_1);
7 my $b = chomp($example_2);
8 print "$example_1 AND $b\n";

chomp_example AND 1

参考[157]

Hardware Description Languages

Verilog

[159] verilog中的阻塞赋值和非阻塞赋值有什么区别?

verilog支持阻塞与非阻塞两种赋值方式。使用阻塞赋值时,计算和赋值会立刻执行。因此但是顺序执行块中由多个阻
塞赋值,他们会按照阻塞的方式顺序执行。如下所示。

1 always @(posedge clk)


2 begin
3 x = a|b;
4 y = a&b;
5 z = x|y;
6 end

在这个例子中,所有赋值都采用的时阻塞赋值,执行时会立刻进行计算,然后完成赋值操作。因此在第三条语句中,
会将前两条语句中更新后的x和y用于第三条语句的计算并赋值。

在非阻塞赋值中,所有的赋值都将在当前的仿真时刻的最后执行。首先对右侧的表达式进行计算,然后才对左侧进行
赋值,因此在下面的例子中,三条语句都先进行右侧语句的计算,然后再一起对左侧进行赋值操作。结果就是,第三条语
句所采用的x和y是一个旧值,并不是前两条语句在这个仿真时刻改变后的值。

1 always @(posedge clk)


2 begin
3 x <= a|b;
4 y <= a&b;
5 z <= x|y;
6 end

[160] 下面的两个例子综合时需要多少个Flip-Flop?

1 1)
2 always @(posedge clk)
3 begin
4 B = A;
5 C = B;
6 end
7 2)
8 always @(posedge clk)
9 begin
10 B <= A;
11 C <= B;
12 end

1)一个;2)两个

第一种情况下,A的值赋予B,B更新后的值在同一个周期将赋予C,因此只需要一个触发器即可
第二种情况下,B更新后的值,在下一个周期才能赋予C,需要两个触发器实现

[161] 下列代码的输出是什么?

1 always @(posedge clk)


2 begin
3 a = 0;
4 a <=1;
5 $display("a=%0b", a);
6 end

由于非阻塞赋值只能在周期结束生效,而display语句打印的是当前值,所以结果是a=0。

[162] 编写verilog代码,交换两个寄存器的值,并且不使用中间寄存器

1 always @(posedge clk)


2 begin
3 A<=B;
4 B<=A;
5 end

[163] 下列代码的输出是?

1 module test;
2 int alpha,beta;
3 initial begin
4 alpha = 4;
5 beta = 3;
6 beta <= beta + alpha;
7 alpha <= alpha + beta;
8 alpha = alpha - 1;
9 $display("Alpha=%0d Beta=%0d", alpha,beta);
10 end
11 endmodule

这些赋值在一个没有任何时钟的initial块中,所以非阻塞赋值是不起作用的,因此只关心阻塞赋值。结果是:Alpha=3
Beta=3

[164] 下列两种情况下c的值会是多少(五个时间单位后)?

1 1)
2 initial begin
3 a=0;
4 b=1;
5 c = #5 a+b;
6 end
7 2)
8 initial begin
9 a=0;
10 b=1;
11 #5 c = a+b;
12 end
13 initial begin
14 a=0;
15 #3 a=1;
16 end

1. c=1
2. c=2

第一种情况下,c=a+b,但是需要五个时间单位的延迟以后才完成赋值
第二种情况下,在c=a+b赋值完成之前,另一个initial块中,第三个时间单位时,修改了a的值,所以在计算a+b时,
a=1,因此最终结果为2

[165] 分析下面的代码,找出代码的错误

1 bit a, b, c, d, e;
2 always @(a, b, c) begin
3 e = a & b & c & d;
4 end

敏感列表缺失了d,这会导致仿真结果错误,而综合结果可能是正确的

[166] 编写verilog模块,使用“?:”运算符实现3:1mux

1 module mux31_2(inp0,inp1,inp2,sel0,sel1, outres);


2 input inp0, inp1, inp2, sel0, sel1;
3 output outres;
4 assign outres = sel1 ? inp2 : (sel0 ? inp1 : inp0);
5 endmodule

[167] 用下列两段代码进行建模,这两种代码风格有什么问题?

1 always @(posedge clk or posedge reset)


2 if (reset)
3 X1 = 0; // reset
4 else
5 X1 = X2;
6
7 always @(posedge clk or posedge reset)
8 if (reset)
9 X2 = 1;// set
10 else
11 X2 = X1;

verilog仿真器并不能保证always块的执行顺序,在上面的代码中,由于使用了阻塞赋值,因此会导致竞争现象。如果
我们使用不同的仿真器,always块的执行顺序不同可能会导致不同的结果。推荐使用非阻塞赋值。

[168] 同步复位和异步复位之间有什么区别?如何使用verilog进行同步复位和异步复位建模?

上电以后,使用复位进行状态设定为一个确定状态。如果对复位在时钟的边沿进行采样,那么就是同步复位。如果不
依赖于时钟边沿进行复位采用,则为异步复位。

下面的代码为同步复位

1 always @ (posedge clk) begin


2 if (reset) begin
3 ABC <=0;
4 end
5 end

下面的代码为异步复位

1 always @ (posedge clk or posedge reset ) begin


2 if (reset) begin
3 ABC <=0;
4 end
5 end

[169] “==”和“===”有什么区别?
两者都是相等或比较运算符。“==”测检查二值逻辑相等,而“===”运算符测试四值逻辑相等。

使用“==”比较四值逻辑,如果出现X或者Z,则结果为X。

使用“===”比较四值逻辑,如果出现X或Z,则结果为0或1,能够正确的进行比较。

[170] 如果A和B是两个3bit的变量:A = 3'b1x0 B = 3'b1x0 ,那么1)A==B 2)A===B的结果分别


是?

1. A==B只能判断非X/Z,出现X或Z,最后的结果为X
2. A===B能够判断X/Z,结果为1

[171] 用verilog建模Latch和Flip-Flop,并解释他们的不同

Flip-Flop,在时钟上下沿进行采样。Latch,在使能时,一直进行采样,输出跟随输入

D触发器

1 always @ (posedge clk) begin


2 if(reset) begin
3 Q <= 0;
4 Qbar <= 1;
5 end else begin
6 Q <= D;
7 Qbar <= ~D;
8 end
9 end

锁存器

1 always @ (D or Enable) begin


2 if(Enable) begin
3 Q <= D;
4 Qbar <= ~D;
5 end
6 end

[172] 编写verilog代码,检测序列10110

首先设计状态机。

1. 没有检测到序列
2. 检测到1
3. 检测到10
4. 检测到101
5. 检测到1011

状态机如下

1 module seq_detector(z,x,clock,reset);
2 output z;
3 input x,clock;
4 input reset; //active high
5 reg [2:0] state,nextstate;
6 parameter s0=3'b000,s1=3'b001,s2=3'b010,s3=3'b011,s4=3'b100;
7
8 always @ (posedge clock) begin
9 if(reset) begin
10 state <=s0;
11 nextstate<=s0;
12 end else begin
13 state<=nextstate;
14 end
15 end
16
17 always @ (x or state)
18 case(state)
19 s0: if(x) nextstate=s1; else nextstate=s0;
20 s1: if(x) nextstate=s1; else nextstate=s2;
21 s2: if(x) nextstate=s3; else nextstate=s0;
22 s3: if(x) nextstate=s4; else nextstate=s2;
23 s4: if(x) nextstate=s1; else nextstate=s2;
24 endcase
25
26 always @ (x or state)
27 case(state)
28 s4: if(x) z=1'b0; else z=1'b1;
29 s0,s1,s2,s3: z=1'b0;
30 endcase
31 endmodule

[173] 写一段verilog代码,根据输入的n计算斐波那契数列

斐波那契数列是一种数列,每一项是通过将前两项相加而得到的。 从0和1开始,顺序为0、1、1、2、3、5、8、13、
21、34,依此类推。 通常,表达式为xn = xn-1 + xn-2。 假设最大值n = 256,以下代码将生成第n个斐波那契数。 值“n”
作为输入传递给模块(nth_number)

1 module fibonacci(input clock, reset, input [7:0] nth_number, output [19:0] fibonacci_number,
output number_ready);
2 reg [19:0] previous_value, current_value;
3 reg [7:0] internal_counter;
4 reg number_ready_r;
5
6 always @(posedge clock or posedge reset) begin
7 if(reset) begin
8 previous_value <='d0; //1st Fibonacci Number
9 current_value <='d1; //2nd Fibonacci Number
10 internal_counter <='d1;
11 number_ready_r <= 0;
12 end else begin
13 if (internal_counter == (nth_number-2)) begin
14 number_ready_r <= 1;
15 end else begin
16 internal_counter <= internal_counter + 1;
17 current_value <= current_value + previous_value;
18 previous_value <= current_value;
19 number_ready_r <= 0;
20 end
21 end
22 end
23
24 assign fibonacci_number = current_value;
25 assign number_ready = number_ready_r
26
27 endmodule

[174] 写一段verilog代码,用半加器组成全加器

1 module half_adder(input_0, input_1, sum, carry);


2 input input_0, input_1;
3 output sum, carry;
4 assign sum = (input_0)^(input_1);
5 assign carry = (input_0)&(input_1);
6 endmodule
7
8 module full_adder(input_0, input_1, input_2, sum, carry);
9 input input_0, input_1, input_2;
10 output sum, carry;
11 reg sum_intermediate, carry_intermediate_0, carry_intermediate_1;
12 half_adder ha1(input0,input1,sum_intermediate,carry_intermediate_0);
13 half_adder ha2(sum_intermediate,input2,sum,carry_intermediate_1);
14 assign carry = (carry_intermediate_0)|(carry_intermediate_1);
15 endmodule

[175] verilog中的task和function有什么区别?

1. function不能使用任何延时语句,task可以
2. 由于1中的原因,function可以调用function,但是不能调用task。task可以调用task和funtion
3. funtion可以综合,task不可以
4. function需要有通过返回参数作为输出,但是有多个输入输出参数。task不能返回任何值,但是具有多个输入或
者输出参数。

SystemVerilog

[176] systemverilog中的reg,wire和logic有什么区别?

reg和wire是Verilog中就存在的两种数据类型,而logic是SystemVerilog中引入的新数据类型。

1. wire是一种数据类型,可以对物理导线进行建模以连接两个元素。 导线只能由连续赋值语句驱动,如果不驱
动,则无法保持值。 因此,wire只能用于对组合逻辑进行建模。
2. reg是可以为存储数据或状态建模的数据类型。 它们需要由always块驱动,而不能由连续赋值语句驱动。 reg可
用于建模顺序逻辑和组合逻辑。
3. logic是SystemVerilog中的一种新数据类型,可用于wire和reg建模,也是四值逻辑,可以被用作reg也可以
wire。

[177] bit和logic有什么区别?

bit是只能存储0和1的二值逻辑,而logic能够储存0、1、X和Z的四值逻辑。

二值逻辑能够加速仿真速度,而如果用二值逻辑用于驱动或者采样来自RTL的信号,会导致错误采样X和Z

[178] logic[7:0] 和 byte 有什么区别?

byte是有符号类型,最大为127,而logic可以被声明为无符号,最大可以达到255.

[179] 动态数组和关联数组,哪个更加适合模拟大型数组?例如32KB的巨大内存数组

关联数组更加适合用于大型数组的建模,因为只有在讲元素写入数组时才分配内存。而动态数组需要在使用之前分配
和初始化内存。例如:如果要使用动态数组对32KB的内存数组进行建模,则首先需要分配32K的空间用于读/写。但是由于
关联数组内部使用哈希进行元素的搜索,所以速度也是最慢的。

[180] 有一个动态数组通过下列代码进行初始化,写一段代码找出数组中所有大于3的元素

1 int myvalues [] = '{9,1,8,3,2,4,6},


2 int match_q[$];
3 match_q = myvalues.find with (item > 3);

[181] systemverilog中的union和struct有什么区别?

struct表示不同数据类型的集合。 例如:在下面的示例中,我们定义了一个名为instruction_s的struct,该struct由24位
地址和8位操作码构成。
1 typedef struct {
2 bit [7:0] opcode;
3 bit [23:0] addr;
4 } instruction_s;
5 instruction_s current_instruction;
6 current_instruction.addr='h100;

可以直接引用instruction_s,也可以单独引用成员。存储struct所需的内存空间为成员之和,例如instruction_s需要
32bit的空间。

union是一种数据类型,可以使用有且只有一种命名成员数据类型来访问它。 与struct不同,不能访问所有成员数据类
型。 分配给union的内存将是成员数据类型所需的最大内存。 如果要对诸如寄存器之类的硬件资源建模,该寄存器可以存
储不同类型的值,则union很有用。 例如:如果寄存器可以存储整数和实数值,则可以按以下方式定义union:

1 typedef union {
2 int data;
3 real f_data;
4 }state_u;
5 state_u reg_state;
6 reg_state.f_data = 'hFFFF_FFFF_FFFF_FFFF;
7 $display(" int_data =%h", reg_state.data);

在此示例中,state_u可以保存32位整数数据,也可以保存64位实数数据。 因此,为reg_state分配的内存将为
64bit(两种数据类型中的较大者)。 由于所有成员数据类型都有共享内存,因此在上面的示例中,如果我们将64位值分
配给reg_state.f_data,则我们也可以使用其他数据类型引用相同的32位。

[182] systemverilog的function和task中“ref”和“const ref”是什么意思?

ref关键字用于通过引用而不是值的方式传递参数。 子例程/函数与调用者共享句柄以访问值。 这是传递诸如类对象或


对象数组之类的参数的有效方法,否则创建副本将消耗更多内存。 同样,由于调用方和function/task共享相同的引用,因
此使用ref在函数内部完成的任何更改也对调用方可见。

例如:这是一个CRC函数的示例,该函数需要一个大数据包作为参数来计算CRC。 通过作为参考传递,每次调用CRC
函数都不需要在存储器上创建数据包的副本。

1 function automatic int crc(ref byte packet [1000:1] );


2 for( int j= 1; j <= 1000; j++ ) begin
3 crc ^= packet[j];
4 end
5 endfunction

const关键字用于禁止函数修改传递的参数。例如:在同一个CRC函数中,可以将参数声明为“const ref”参数,如下所
示,以确保原始数据包内容不会被CRC函数意外修改。

1 function automatic int crc(const ref byte packet [1000:1] );


2 for( int j= 1; j <= 1000; j++ ) begin
3 crc ^= packet[j];
4 end
5 endfunction

[183] 下面代码中参数a和b的方向是什么?

1 task sticky(ref int array[50], int a, b);

function和task的每一个参数都有他的方向,input,ouput,inout或者ref。如果没有显式声明,则默认与前面的参数
保持一致,如果前面没有参数,则默认为输入。上述代码中,第一个参数array的方向为ref,而后续a和b没有显式声明,所
以将延续前者的方向,即为ref。

[184] 压缩数组和非压缩数组的区别是?
压缩数组表示一组连续的bit,而非压缩数组不是。在声明上,有下面的区别

1 bit [7:0] data ; // packed array of scalar bit types


2 real latency [7:0]; // unpacked array of real types

压缩数组只能由一位数据类型(bit,logic,reg)或枚举类型组成。非压缩数组能够由任意类型组成

1 logic[31:0] addr; //packed array of logic type


2 class record_c;
3 record_c table[7:0]; //unpacked array of record objects

[185] packed struct和unpacked struct的区别是什么?

压缩结构体是一种可以将压缩位向量作为结构成员进行访问的方法。 换句话说,如果struct的所有成员仅由位字段组
成并且可以无间隙地打包在内存中,则它可以是压缩结构。 例如:在下面的struct定义中,所有成员都可以表示为位向量
(int等于32位,short int到16位,byte到8位),并且一个struct可以打包到单个连续内存56bit。

1 struct packed {
2 int a;
3 short int b;
4 byte c;
5 } pack1_s;

非压缩结构体不需要打包到连续的bit位中,因此在不同成员之间可以存在空隙。下面是一个无法压缩的结构体。

1 struct {
2 string name;
3 int age;
4 string parent;
5 } record_s

[186] 下面哪个是对的?

1. function不能消耗仿真时间
2. task不能消耗仿真时间
3. 正确
4. 错误

[187] 现有一个动态数组的大小为100,如何把他的大小定义为200,并且前100个元素例子原来
的数组?

动态数组使用new[]进行保留元素的内存分配。

1 integer addr[]; // Declare the dynamic array.


2 addr = new[100]; // Create a 100-element array.
3 ………
4 // Double the array size, preserving previous values.
5 addr = new[200](addr);

[188] systemverilog中case,casex和casez的区别是?

case语句是选择语句,匹配表达式并执行对应的语句。下面是一个3:1MUX
1 case (select[1:0])
2 2'b00: out_sig = in0;
3 2'b01: out_sig = in1;
4 2'b10: out_sig = in2;
5 default: out_sig = 'x;
6 endcase

上面的例子中,如果表达式与指定的内容完全匹配,则执行后续语句,如果出现x或者z,将执行默认语句。

casez是case的一个特殊版本,通常在处理较少位的译码器中使用。下面的示例中,将3位中断申请队列解码位三个独
立的中断引脚,只关心对应位,而其他位无关紧要。带有优先级。

1 casez (irq)
2 3'b1?? : int2 = 1'b1;
3 3'b?1? : int1 = 1'b1;
4 3'b??1 : int0 = 1'b1;
5 endcase

“ casex”是另一个特殊版本,除了无关项,在比较中它也忽略X和Z值。

[189] 在case、casez、casex中使用的是==还是===?

三者使用的都是===

[190] systemverilog中的$display,$write, $monitor 和 $strobe用什么区别?

1. $display:执行时立刻打印内容
2. $strobe:在当前的timestep结束时打印内容
3. $monitor:在timestep结束时,如果内容改变了,则进行打印。如果多次调用,则新的覆盖旧的。
4. $write:和$display一样,但是不会在结尾打印换行符

[191] 下面的systemverilog代码中有什么错误?

1 task wait_packet;
2 Packet packet;
3 event packet_received;
4 @packet_received;
5 packet = new();
6 endtask
7 function void do_print();
8 wait_packet();
9 $display("packet received");
10 endfunction

function中不能使用任何延时语句。上面的例子中,function调用了一个耗时的task,这是非法的。

[192] systemverilog中new()和new[]有什么区别?

new()时systemverilog中类的构造函数。他在类中定义,并初始化对象。

new[]用于动态数组的内存分配。

[193] 什么是systemverilog中的前置声明?

有时候,一个类有可能引用另一个尚未编译的类,这会导致编译错误。例如,如果按照下面的顺序编译Statistics和
Packet,由于在编译Statistics时,Packet尚未定义,编译器将为报错。
1 class Statistics;
2 Packet p1;
3 endclass
4
5 class Packet; //full definition here
6 endclass

为了避免这个问题,类在完全定义之前,可以先进行前置声明。

1 typedef Packet; //forward declaration


2
3 class Statistics;
4 Packet p1;
5 endclass
6
7 class Packet; //full definition here
8 endclass

[194] 下面代码有什么问题?

1 task gen_packet(Packet pkt);


2 pkt = new();
3 pkt.dest = 0xABCD;
4 endtask
5
6 Packet pkt;
7 initial begin
8 gen_packet(pkt);
9 $display(pkt.dest);
10 end

这段代码在运行时,会产生空指针错误。task传入pkt句柄,而在内部为句柄创建了一个对象。在initial块中,调用了
gen_packet,并修改了pkt.dest,但是对于task来说这些都是局部的。task的默认方向是input,在内部的修改句柄的指向并
不能影响外部,尽管在task内部进行了对象例化并且修改了值,而实际上外部的pkt始终是空句柄。

[195] systemverilog中,类成员的private、public和protect属性是什么意思?

1. private成员只允许在class内部进行访问。这些成员在派生类中也是不可见的。
2. public成员在class内外都可以进行访问。这些成员在派生类中是可见的。
3. protect成员只允许在class内部进行访问。这些成员在派生类中是可见的。

[196] systemverilog的类中,成员变量默认是public还是private?

与C++和Java中不一样,systemverilog的类成员默认为public。

[197] 什么是嵌套类?何时使用他?

当一个类的定义包含另一个类的定义,则该类为嵌套类。例如,下面代码中,StringList类定义包含另一个类Node的定
义。

1 class StringList;
2 class Node; // Nested class for a node in a linked list.
3 string name;
4 Node link;
5 endclass
6 endclass

嵌套允许隐藏本地名称和资源的本地分配。 当需要新的类作为类实现的一部分时很有用。

[198] systemverilog中的interface是什么?
systemverilog中的接口用于将多个线网进行捆绑,有助于封装设计块之间的接口通信。接口可以在设计中实例化,并
且可以使用单个名称进行连接,不需要显式声明所有的端口和连接。除了用于连接,接口内部还可以定义function,可以
通过实例化接口调用function。接口还支持过程块和assign,这些对于协议检查和断言非常有用。下例是一个接口的简单示
例:

1 interface simple_bus; // Define the interface


2 logic req, gnt;
3 logic [7:0] addr, data;
4 logic [1:0] mode;
5 logic start, rdy;
6 endinterface: simple_bus

[199] 什么是modport?

modport(模块端口的缩写)是接口中的一种构造,可用于分组信号并指定方向。 以下是如何使用Modport将接口进
一步分组以连接到不同组件的示例。

1 interface arb_if(input bit clk);


2 logic [1:0] grant, request;
3 logic reset;
4 modport TEST (output request, reset, input grant, clk);
5 modport DUT (input request, reset, clk, output grant);
6 modport MONITOR (input request, grant, reset, clk);
7 endinterface

上面的示例中,相同的信号在不同的modport中具有不同的方向。

[200] interface是可综合的吗?

是可综合的。

[201] 什么是时钟块?在interface中使用时钟块有什么好处?

时钟块类似于modport,除了具备modport的信号方向指定,还能够建模信号的时序行为。下面是一个时钟块的例
子。

1 clocking sample_cb @(posedge clk);


2 default input #2ns output #3ns;
3 input a1, a2;
4 output b1;
5 endclocking

在上面的示例中,定义了一个名为sample_cb的时钟块,关联的时钟为clk。default关键字定义默认的时钟偏斜,输入
为2ns,输出为3ns。输入偏斜定义了时钟采样在时钟边沿前多少个时间单位。 输出偏斜定义了时钟驱动在时钟边沿后的多
少个时间单位。

时钟块只能在module或者interface中定义
[202] 下面两种定义时钟偏斜的方式有什么不同?

1 1) input #1 step req1;


2 2) input #1ns req1;

定义时钟偏斜有两种方式

1. 以time step为单位,这是由全局的时间精度定义的,即`timescale
2. 之间指明具体的时间长度

[203] systemveirlog仿真里在一个timestep中有哪些主要的阶段?

systemverilog仿真器是事件驱动的仿真器,明确定义了不同的阶段来计划和执行所有事件。在仿真中,所有的事件都
以timeslot为单位进行。timeslot被划分为一组有序阶段,提供设计和测试代码之间的可预测交互。如下图所示,一个
timeslot可以划分为五个主要阶段,每个阶段都可以进一步的被划分为细分的子阶段。

1. Prepone:这是timeslot最先执行的阶段,并且只执行一次。来自测试平台对设计信号的采样发生在这个阶段。
2. Active:Active阶段包括三个子阶段:Active,Inactive和NBA(Nonblocking assignment)阶段.RTL代码和行为代码
在Active阶段执行。阻塞赋值在Active阶段执行。非阻塞赋值,RHS的计算在Active阶段执行,赋值操作在NBA
阶段执行。如果由#0的赋值行为,则在inactive阶段执行。
3. Observed:Observed用于计算property(并发断言中)表达式的触发行为。在property计算期间,通过或者失败的
调度会在当前timeslot的Reactive阶段进行。
4. Reactive:Reactive阶段包括三个子阶段:Re-Active,Re-Inactive和Re-NBA(Re-Nonblocking assignment)阶
段。用于执行systemverilog中program块的阻塞赋值,#0阻塞赋值,非阻塞赋值。这个独立的响应阶段确保在
测试代码执行之前,设计代码的状态已经稳定。使用OVM/UVM不需要程序块,因此Reactive阶段可能使用得不
多。
5. Postponed:Postponed阶段是当前timeslot的最后一个阶段。$monitor,$strobe和其他类似的事件在此区域执
行。而$dislpay事件在Active 和 Reactive(如果在program块中调用)阶段执行

[204] 根据下面的约束,哪一个选项是错误的?

1 rand logic [15:0] a, b, c;


2 constraint c_abc {
3 a < c;
4 b == a;
5 c < 30;
6 b > 25;
7 }
1. b可以取26-29之间的任意值
2. c可以取0-29之间的任意值
3. c可以取26-29之间的任意值

将约束内容取交集,c可以取27-30之间的任意值。

[205] 什么是systemverilog中的unique约束?

unique约束会令一组成员两两各不相同。下面是一个示例。

1 class Test;
2 rand byte a[5];
3 rand byte b;
4 constraint ab_cons { unique {b, a[0:5]}; }
5 endclass

[206] 如何选择性的激活或者关闭一个类中的约束?

1 //<object>.constraint_mode(0) :: 关闭对象中的所有约束
2 //<constraint>.constraint_mode(0) :: 选择性关闭某一个约束
3 class ABC;
4 rand int length;
5 rand byte SA;
6 constraint c_length { length inside [1:64];}
7 constraint c_sa {SA inside [1:16];}
8 endclass ABC abc = new();
9 abc.constraint_mode(0) // 关闭所有约束
10 abc.c_length.constraint_mode(0) // 只关闭约束c_length

[207] 现有下面的一个类,如何生成addr大于200的Packet对象?

1 class Packet;
2 rand bit[31:0] addr;
3 constraint c_addr { addr inside [0:100];}
4 endclass

大于200与内置约束冲突,所以需要先关闭内置约束,然后通过内联约束进行随机化

1 Packet p = new();
2 p.c_addr.constraint_mode(0);
3 p.randomize with {addr > 200;};

[208] 什么是pre_randomize()和post_randomize()函数?

这显示systemverilog内建的回调函数。在调用randomize之前会自动调用pre_randomize函数,之后会自动调用
post_randomize函数。可以通过定义这两个函数,完成在随机化之前或者之后进行某些操作。

[209] 编写一个约束,为下面对象中的动态数组生成约束,使得每个元素都小于10,数组大小也
小于10

1 class dynamic_array;
2 rand unsigned int abc[];
3 endclass
1 constraint c_abc_len {
2 abc.size() < 10;
3 foreach (abc[i])
4 abc[i] < 10;
5 }

[210] 编写约束,创建随机整数数组,使数组大小在10-16之间,并且数组按照降序排列

1 class array_abc;
2 rand unsigned int myarray[];
3 endclass

1 constraint c_abc_val {
2 myarray.size inside { [10:16] };
3 foreach (myarray[i])
4 if (i>0) myarray[i] < myarray[i-1];
5 }

[211] 如何对一个生成一个随机动态数组,并且元素两两各不相同?参考下面的代码

1 class TestClass;
2 rand bit[3:0] my_array[];//dynamic array of bit[3:0]
3 endclass

1. 使用unique进行约束

1 constraint c_rand_array_uniq {
2 my_array.size == 6; //or any size constraint
3 unique {my_array}; //unique array values
4 }

2. 不适用unique进行约束,使用post_randomize回调函数完成

1 constraint c_rand_array_inc {
2 my_array.size == 6 ;// or any size constraint
3 foreach (my_array[i])
4 if(i >0)
5 my_array[i] > my_array[i-1];
6 }
7 function post_randomize();
8 my_array.shuffle();
9 endfunction

[212] “fork - join”, “fork - join_any” 和“fork - join_none”之间有什么区别?

systemverilog支持三种类型的动态进程,可以在运行时创建,并作为独立线程执行。

1. fork-join:使用“ fork .. join”创建的进程作为单独的线程运行,但是父进程停滞不前,直到所有进程全部执行


完。 如果我们看下面的示例:共有三个进程task1,task2和task3,它们将并行运行,并且只有在这三个进程全
部完成之后,join语句之后的$display()才会执行。

1 initial begin
2 fork
3 task1; // Process 1
4 task2; // Process 2
5 task3; // Process 3
6 join $display(“All tasks finished”);
7 end
2. fork-join_any:使用“ fork…join_any”创建的进程作为单独的进程运行,但是父进程会在任何一个子进程完成后
继续。其余进程和父进程可以并行运行。 如果我们看下面的示例:有三个进程-task1,task2和task3将并行运
行。 当task1 / task2 / task3之一完成时,join_any将完成,并在其他线程可能仍在运行时执行$ display()。

1 initial begin
2 fork
3 task1; // Process 1
4 task2; // Process 2
5 task3; // Process 3
6 join_any
7 $display(“Any one of task1/2/3 finished”);
8 end

3. fork-join_none:使用“ fork…join_none”创建的进程将作为单独的进程运行,但是父进程不会停滞并且也将并行
进行。 参考以下示例,其中有三个进程-task1,task2和task3,它们将与父进程并行运行。

1 initial begin
2 fork
3 task1; // Process 1
4 task2; // Process 2
5 task3; // Process 3
6 join_none
7 $display(“All tasks launched and running”);
8 end

[213] “wait fork”和“disable fork”的作用是什么?

在使用“ fork..join_none”或“ fork..join_any”时,有时候需要父进程和子进程进行同步,这可以通过wait fork完成。

1 initial begin
2 fork task1; // Process 1
3 task2; // Process 2
4 join_none
5 $display(“All tasks launched and running”);
6 wait fork;
7 $display(“All sub-tasks finished now”);
8 end

类似的,disable fork可以提前将子进程停止。

1 initial begin
2 fork
3 task1; // Process 1
4 task2; // Process 2
5 join_any
6 $display(“One of task1/2 completed ”);
7 disable fork;
8 $display(“All other tasks disable now”);
9 end

[214] 硬约束和软约束有什么区别?

systemverilog中编写的常规约束为硬约束,对成员的随机化必须始终满足约束,如果约束无解,则会导致错误。如果
将约束定义为软约束,在没有外部约束的条件下,和硬约束一样,外部约束的优先级比软约束高。软约束通常用于指定随
机变量的默认值和分布,并且可以被外部特定的约束覆盖。
1 class Packet;
2 rand int length;
3 constraint length_default_c {
4 soft length inside {32,1024};
5 }
6 endclass
7
8 Packet p = new();
9 p.randomize() with { length == 1512; }

上例中,如果约束没有定义为软约束,则随机化会失败。

[215] 下面每个线程的输出是什么?

1 initial begin
2 for (int j=0; j<3; j++) begin
3 fork
4 automatic int result;
5 begin
6 result= j*j;
7 $display("Thread=%0d value=%0d", j, value);
8 end
9 join_none
10 wait
11 fork;
12 end
13 end

由于“ j”不是每个线程的动态变量,因此在生成每个线程后它会不断递增,并且当所有线程开始执行时,每个线程将看
到的j都是3。因此,每个线程都将输出9。 如果每个线程打算使用不同的值,则应将“ j”的值复制到一个自动变量,如下所
示:

1 automatic int k = j;
2 begin
3 result = k*k;
4 end

[216] 下面的代码会产生多少个并行的线程?

1 fork
2 for (int i=0; i < 10; i++ ) begin
3 ABC();
4 end
5 join

for循环在fork join内,所以只有一个线程。

[217] 下面的约束有什么错误?

1 class packet;
2 rand bit [15:0] a, b, c;
3 constraint pkt_c { 0 < a < b < c; }
4 endclass

约束表达式中,至多只有一个关系运算符(<,<=,==,>=或>)。如果要实现变量的顺序约束,需要多个表达式。

1 constraint pkt_c { 0 < a; a < b ; b < c; }


[218] systemverilog中的虚方法和纯虚方法的区别是?

在类中将方法定义为虚方法,则在派生类中可以重写这个方法。基类可以定义具有实现或不实现的虚函数,在派生类
中也可以选择覆盖或不覆盖。而纯虚函数只具备函数声明,没有具体实现,在派生类中必须有具体实现。纯虚函数常用在
抽象类中使用,下面是一个示例。

1 virtual class BasePacket; // No implementation


2 pure virtual function integer send(bit[31:0] data);
3 endclass
4
5 class EtherPacket extends BasePacket;
6 virtual function integer send(bit[31:0] data);
7 // body of the function
8 // that implements the send
9 ….…
10 endfunction
11 endclass

[219] 什么是Semaphores?何时使用?

Semaphores是用于控制对公用资源的机制。Semaphores可以视为在创建时具有多个钥匙的存储池,使用Semaphores
访问资源时,首先要取得要是,然后才能够继续执行。通过这种机制,可以确保没有要是的进程一直等待到获取钥匙。
Semaphores通常用于相互排斥,对公用资源进行访问控制以及简单同步。下面是简单的Semaphores的创建方法。

1 semaphore smTx;
2 smTx = new(1); //create the semaphore with 1 keys.

get()和try_get()分别是阻塞和非阻塞的获取钥匙的方法,put()用于返还钥匙。

[220] 下面两个约束有什么不同?

1 1)
2 class ABSolveBefore;
3 rand bit A;
4 rand bit [1:0] B;
5 constraint c_ab { (A==0) -> B==0; solve A before B; }
6 endclass
7 2)
8 class ABSolveBefore;
9 rand bit A;
10 rand bit [1:0] B;
11 constraint c_ab { (A==0) -> B==0; solve B before A; }
12 endclass

两种情况下,A都能取到0和1,B能取到0123,并且A为0时B都为0。但是求解顺序会导致两者的分布会不一致。

如果先求解A再求解B,那么概率分布表为
A B 概率

0 0 0.5

0 1 0

0 2 0

0 3 0

1 0 0.5*0.25

1 1 0.5*0.25

1 2 0.5*0.25

1 3 0.5*0.25

如果先求解B再求解A,那么概率分布表为

A B 概率

0 0 0.5*0.25

0 1 0

0 2 0

0 3 0

1 0 0.5*0.25

1 1 0.25

1 2 0.25

1 3 0.25

[221] 什么是mailbox?如何使用mailbox?

mailbox是一种通信机制,用于线程之间的数据交换。数据在一个线程存入mailbox中,在另一个线程中检索。下面是
mailbox的声明与创建的示例:

1 mailbox mbxRcv;
2 mbxRcv = new();

将数据存入mailbox中可以使用put(阻塞)和peek(非阻塞)实现,从mailbox中取出数据可以使用get(阻塞)和try_get(非阻
塞)方法,查询mailbox中的数据数量可以使用num()方法。

[222] 有限容量和无限容量的mailbox有什么区别?如何创建?

有限容量的mailbox是指邮箱在创建时就指定容量。

1 mailbox mbxRcv;
2 mbxRcv = new(10); //size bounded to 10

无限容量的mailbox是指邮箱再创建时不指定容量。

1 mailbox mbxRcv;
2 mbxRcv = new(); //size is unbounded or infinite

有限容量的mailbox如果存满,线程将无法继续存入数据,只到mailbox有空间。
[223] 什么是systemverilog中的event?如何触发event?

event类型的变量不用于存储数据,用于线程同步。可以使用"->"显式触发事件,而线程可以通过"@"来等待事件的触
发,阻断线程只到事件被触发。event为两个或者多个同时运行的进程的同步提供强大而有效的手段。

下面的示例中,两个线程通过event进行同步。一旦发送请求,send_req()将会触发一个事件,而receive_response()检
测到事件被触发以后就会继续执行任务。

1 module test;
2 event req_send;
3
4 initial begin
5 fork
6 send_req();
7 receive_response);
8 join
9 end
10
11 task send_req(); //create and send a req
12 -> req_send; //trigger event
13 endtask
14
15 task receive_response();
16 @req_send; //wait until a send event is triggered
17 //collect response
18 endtask
19 endmodule

[224] 如何合并两个event?

可以指将将一个event变量赋值给另一个event变量,此时两个event变量都指向同一个同步对象,可以认为两者合并。

[225] 什么是systemverilog中的std::randomize()方法?何时使用它?

std::randomize()是作用域随机化函数,无需定义类或者实例化类对象仅能对当前作用域中的数据进行随机化。如果某
些需要随机化的变量不是类的成员,则需要使用std::randomize()。下面是一个示例。

1 module stim;
2 bit [15:0] addr;
3 bit [31:0] data;
4 function bit gen_stim();
5 bit success, rd_wr;
6 success = std::randomize(addr, data, rd_wr);
7 return rd_wr ;
8 endfunction
9 …
10 endmodule

std::randomize()和类的randomize()具有相似的功能,也可以使用约束,下面是一个通过with添加约束的示例。

1 success = std::randomize( addr, data, rd_wr ) with {rd_wr -> addr > 'hFF00;};

[226] 在派生类中可以覆盖基类中的约束嘛?如果可以,如何实现?

可以通过使用相同的约束名称在派生类中重写基类定义的约束。下面是一个示例
1 class Base;
2 rand int a ;
3 rand int b;
4 constraint c_a_b_const {a < b;}
5 endclass
6
7 class Derived extends Base;
8 constraint c_a_b_const {a > b;}
9 endclass

[227] 下面的systemverilog代码的调用有什么问题?

1 function int count_ones ( ref bit [9:0] vec );


2 for( count_ones = 0; vec != 0; vec = vec >> 1 )
3 begin
4 count_ones += vec & 1'b1;
5 end
6 endfunction
7
8 constraint C1 { length == count_ones( myvec ) ; }

在约束中,不允许调用方向为ref的函数,除非使用“const ref”,这保证函数在内部不会修改参数。

[228] 下面两个派生类有什么不同?

1 class Base;
2 virtual function printA();
3 endfunction
4 endclass
5 1)
6 class Derived extends Base;
7 function printA();
8 //new print implementation
9 endfunction
10 endclass
11 2)
12 class Derived extends Base;
13 virtual function printA();
14 //new print implementation
15 endfunction
16 endclass

两者并没有区别,在基类中如果定义了virtual关键字,那么派生类也会继承该属性,无论有没有显式的二次声明。

[229] 找出下面代码中的问题(如果有的话)

1 class Packet;
2 bit [31:0] addr;
3 endclass
4
5 class ErrPacket extends Packet;
6 bit err;
7 endclass
8
9 module Test;
10 initial begin
11 Packet p;
12 ErrPacket ep;
13 ep = new();
14 p = ep;
15 $display("packet addr=%h err=%b", p.addr, p.err);
16 end
17 endmodule
没有问题,基类句柄可以指向派生类对象,但是反过来是不允许的。

[230] 现有下面两个类,请问在示例代码中compute_crc函数的调用顺序是?

1 class Packet; //Base Class


2 rand bit [31:0] src, dst, data[8]; // Variables
3 bit [31:0] crc;
4 virtual function void compute_crc;
5 crc = src ^ dst ^ data.xor;
6 endfunction
7 endclass : Packet
8
9 class BadPacket extends Packet; //Derived class
10 rand bit bad_crc;
11 virtual function void compute_crc; //overriding definition
12 super.compute_crc(); // Compute good CRC
13 if (bad_crc) crc = ~crc; // Corrupt the CRC bits
14 endfunction
15 endclass : BadPacket

示例

1 Packet pkt;
2 BadPacket badPkt;
3 initial begin
4 pkt = new;
5 pkt.compute_crc; // 1) Which of compute_crc() gets called?
6 badPkt = new;
7 badPkt.compute_crc; // 2) Which of compute_crc() gets called?
8 pkt = badPkt; // Base handle points to ext obj
9 pkt.compute_crc; // 3) Which of compute_crc() gets called ?
10 end

1. 调用了基类的compute_crc
2. 调用了派生类的compute_crc
3. 调用了派生类的compute_crc,虽然使用的是基类的句柄,但是方法定义为虚方法,所以要根据对象的类型进行
调用

[231] 下面两种代码风格哪种更加好?为什么?

1 1)
2 for (i=0; i < length*count; i++) begin
3 a[i] = b[i];
4 end
5 2)
6 l_end = length * count;
7 for (i=0; i < l_end; i++) begin
8 a[i] = b[i]
9 end

2比1更好,在1中,每次迭代都需要计算length*count,2中只需要计算一次

[232] 下面的代码有什么错误?

1 class ABC;
2 local int var;
3 endclass
4
5 class DEF extends ABC;
6 function new();
7 var = 10;
8 endfunction
9 endclass
变量var具备local关键字,在派生类中是不可用的。

[233] 什么是虚接口,何时使用它?

虚接口是指向实际结构的变量。他在类中用于提供接口的连接点,通过虚接口可以访问接口中的信号。在下面的示例
中,接口bus_if将多个信号整合起来。然后,BusTransactor类中定义了这一接口类型的虚接口,这个虚接口用于访问来自
this.b_if的所有驱动或检测。实例化物理接口以后,通过构造函数将句柄传递给BusTransactor类。

1 interface bus_if; // A bus interface


2 logic req, grant;
3 logic [7:0] addr, data;
4 endinterface
5
6 class BusTransactor; // Bus transactor class
7 virtual bus_if bus; // virtual interface of type bus_if
8 function new( virtual bus_if b_if );
9 bus = b_if; // initialize the virtual interface
10 endfunction
11
12 task request(); // request the bus
13 bus.req <= 1'b1;
14 endtask
15
16 task wait_for_bus(); // wait for the bus to be granted
17 @(posedge bus.grant);
18 endtask
19 endclass
20
21 module top;
22 bus_if bif(); // instantiate interfaces, connect signals etc
23 initial begin
24 BusTransactor xactor;
25 xactor = new(bif); //pass interface to constructor
26 end
27 endmodule

[234] 工厂和工厂模式的意思是?

在面向对象编程中,工厂是用于创建原型或类的不同对象的方法或函数。不同的类在工厂中注册后,工厂方法可以通
过调用相应的构造函数来创建任何已注册类类型的对象。创建对象不直接调用构造函数的模式称为工厂模式。使用基于工
厂的对象创建而不是直接调用构造函数,允许在对象创建中使用多态性。这个概念是在UVM (Univers)中实现的。

[235] 回调函数(callback)的意义是什么?

“回调”是由另一个函数调用的任何函数,它以第一个函数为参数。大多数情况下,当某个“事件”发生时,会调用回调函
数。在验证平台中,回调函数很多优点:

1. 注入从驱动程序发送的事务错误
2. 当一个模拟阶段准备结束时,调用一个函数来关闭所有序列/驱动程序中所有挂起的事务。
3. 在一个特定的事件上调用一个覆盖率采样函数。

大多数情况下,回调函数是通过将它们注册到一个组件/对象中来实现的,该组件/对象会在某些定义的条件下回调。
UVM中的phase_ready_to_end()就是回调函数,它在基类中实现,并注册到UVM_component类中。当当前仿真阶段准备结
束时,将调用该函数。因此,用户可以通过覆盖此函数定义来实现需要在仿真阶段结束时执行的任何功能。

[236] 什么是DPI调用?

DPI是直接编程接口的缩写,它是SystemVerilog和C/C++等外语编程语言之间的接口。DPI允许在接口两边的语言之间
直接进行跨语言函数调用。在C语言中实现的函数可以在SystemVerilog中调用(import),在SystemVerilog中实现的函数可
以使用DPI层在C语言中调用(export)。DPI支持跨语言边界的function(零时间执行)和task(耗时执行)。SystemVerilog数据类
型是惟一能够在任何方向上跨越SystemVerilog和外部语言之间的边界的数据类型。

[237] “DPI import” 和“DPI export”有什么区别?

import的DPI函数是用C语言实现并在SystemVerilog代码中调用的函数。
export的DPI函数是用SystemVerilog语言实现并导出到C语言的函数,这样就可以从C语言调用它。

函数和任务都可以导入或导出。

[238] 什么是系统函数?举例说明他们的作用

SystemVerilog语言支持许多不同的内置系统任务和函数,通常在任务/函数名称前加上“$”前缀。此外,语言还支持添
加用户定义的系统任务和功能。下面是一些系统任务和功能的例子(根据功能分类)。对于完整的列表,可以参考LRM。

1. 仿真控制函数 - $finish, $stop, $exit


2. 转换函数 - $bitstoreal, $itor, $cast
3. bit向量系统函数 - $countones, $onehot, $isunknown
4. 安全系统函数 - $error, $fatal, $warning
5. 采样值控制函数 - $rose, $fell, $changed
6. 断言控制函数 - $asserton, $assertoff

Fundamentals of Verification

[239] 定向测试和受约束的随机测试有什么区别?两者有什么优缺点?

定向测试是一种编写定向测试来验证设计中的每个特性的方法。约束随机测试是一种使用约束随机生成器自动生成激
励的方法,该生成器根据设计规范生成激励。下表比较了两者的优缺点。推荐的方法是混合使用这两种方法——约束随机
覆盖大部分验证空间,然后指导测试覆盖难以到达的边界条件。

定向测试 约束随机测试

针对每个功能点需要编写一个或者多个
使用激励发生器根据功能点,自动生成符合功能规范的测试向量
测试向量

每次测试都能很简单的进行追踪,具有 测试是自动生成的,因此只能通过收集覆盖率,并观察覆盖率确保功
很好的可视化和可预测性 能的验证

当设计特征被充分了解后,定向测试的 开发约束随机测试平台更加复杂,也更加需要经验。需要更多的事件
编写会更加简单 来设计验证平台。

对于复杂的设计,定向测试的编写会变
与大型测试套件相比,约束随机生成器在开发后更容易维护
得非常困难并且事件消耗会很大

定向测试编写仅限于通过理解设计规范 约束随机生成器可以结合随机配置来覆盖更多的场景和特性,从而更
确定的场景 好地强调设计,并覆盖手动识别可能遗漏的一些场景

[240] 什么是自检测试(self-checking tests)?

自检测试是指在测试结束时通过某种方式来检测测试结果的测试。在测试中,可以通过计算某些内存操作的结果或从
DUT(如状态寄存器或任何其他信息)收集结果来预测结果。

[241] 什么是覆盖率驱动的验证?

在覆盖率驱动的验证方法中,验证计划是通过将每个特性或场景映射到一个覆盖率监视器来实现的,该监视器在仿真
期间收集覆盖率信息。

1. 覆盖率可以是基于样本的覆盖组和基于属性的覆盖的组合。
2. 在基于覆盖率的验证中,测试通常使用约束的随机激励生成器生成,测试正确性由功能检查器确保,并为实现
的所有监视器收集覆盖率。
3. 通常在设计上对随机生成器的多个测试或多个种子进行回归,并合并从每个测试中收集到的单个覆盖率,从而
获得累积覆盖率。有时,在设计的一个角边界情况可能不容易被覆盖,使用约束随机激励和可能更好地完成使
用一个有指导的测试。
4. 覆盖率信息还为测试的质量和生成器中的约束提供反馈,并帮助对约束进行微调,从而有效地随机生成刺激
励。
5. 由于在这种方法中,覆盖率定义是跟踪验证执行以获得进展和完成的关键步骤,因此确保根据验证计划和设计
规范审查覆盖率定义和实现的完整性和正确性是很重要的。

[243] 功能验证中的测试分级是什么概念?
设计的功能验证是通过创建定向测试以及对激励进行不同控制的约束随机激励生成器来完成的。
通过设计验证项目,开发一组测试,该测试套件用于验证设计正确性、发现设计中的bug和收集覆盖率等。

测试分级是一个过程,在这个过程中,单个测试根据不同的标准(如功能覆盖率、发现的bug、仿真运行时、维护的容
易程度等)对质量进行分级。

这个过程有助于从测试套件中识别出有效的测试,从而为设计验证开发出最有效的测试套件。

[244] 什么是基于断言的验证方法?

基于断言的验证(ABV)是一种用于捕获特定设计意图的方法。这些断言用于仿真、形式验证,以验证设计实现是否正
确。ABV方法可以通过断言的优点来补充其他功能验证方法,从而实现有效的验证。

断言的一些好处如下:

1. 断言从源头上检测设计错误,从而提高可观察性和减少调试时间。
2. 相同的断言可以用于仿真和形式分析,甚至可以用于仿真。
3. 在断言库中有很多通用设计的断言,可以很容易地移植到任何验证环境中。
4. 作为属性编写的SystemVerilog断言也可以用于覆盖率(使用覆盖属性),因此有助于基于覆盖率的验证方法。

[245] 2*2的分组交换器的spec如下,你将如何验证设计?如何设计激励和检查器?哪些是你需要验
证的case?

SPEC:有两个输入和输出端口A和B,如上所示。每个端口可以接收大小在64到1518字节之间的可变数据包。每个包将
有一个4字节的源地址(Source Address)和4字节的目标地址(Destination Address),以及跨包计算的数据和一个4字节的
CRC,如下所示。数据包将根据目标地址(Destination Address)被切换到一个输出端口。

对于这类有设计说明的问题,第一步是理解设计说明,并向面试官阐述问题。下一步是确定要验证的场景,并提出验
证计划和策略文档。这应该列出要验证的特性/场景,可以使用什么方法来验证(定向/约束随机、覆盖、断言,等等),如何
检查正确性等等。此外,还应详细说明如何产生激励以及如何进行检查。

另一个方面是考虑所有的设计特性,并确定需要验证的关键case。现在,让我们试着列出如何验证这个简单的路由器
设计

1. 以下是一些需要验证的场景:
1. 根据目的地地址测试数据包从a端口到两个输出端口的正确切换。
2. 测试不同的数据包大小-最小尺寸,最大尺寸和之间的随机尺寸。
3. 测试源地址和目标地址的所有可能值。
4. 测试不同的数据模式。
5. 流包测试(背对背无延迟、周期延迟少、周期延迟大)、大小相同的包流或大小不同的包流。
6. 测试CRC功能。
7. SA/DA或数据甚至CRC的某些位被损坏的测试。
8. 你还能想到什么
2. 现在,为了验证上述场景,我们需要设计一个约束随机数据包生成器,我们还需要一个计分板/检查器来检查数
据包的正确性和正确的交换行为。如果测试是随机的,我们还需要编写一些覆盖率监视器,以确保上面提到的
所有重要场景都得到了覆盖。

3. 如果面试官想对你进行更多的测试,那么他也可以继续问你一些问题,要求你写一个SystemVerilog数据包生成
器代码或一个检查程序或驱动程序等。

[246] 对于一个单端口读写RAM,有哪些场景需要进行测试?

单端口RAM只有一个读和写端口。因此,它只能在任何给定的时间点进行读或写操作。其他需要考虑验证的设计规范
包括RAM大小、地址和数据总线的宽度。基于此,以下是一些需要验证的场景:

1. 正确读写行为
2. 背靠背读取或写入相同的地址和不同的地址
3. 背靠背先读后写同一地址
4. 背靠背先写后读同一地址
5. 验证内存大小的边界——读和写
6. 验证写入数据的所有可能,全0,全1,01交替等。

如果您被进一步要求定义一个验证环境,您可以考虑像上面这样的场景,并定义一个有向或有约束的随机环境是否会
更好地工作,以及如何设计激励生成器和检查器。

[247] 单端口和双端口RAM有什么区别?

单个端口RAM只有一个读和写端口。所以它只能在任何给定的时间点进行读或写操作。一个双端口RAM有2个读/写端
口,因此允许同时读写。

[248] 一个简单的带有如下所示的方框图的ALU支持两个4位操作数、一个4位结果总线和进位溢
出。ALU支持最多8条指令,使用3位操作码或选择行(S2、S1、S0),解码如下。

解释所有需要验证的场景,以确保ALU按照下面的SPEC工作:
以下是需要对这个给定的ALU设计进行验证的场景:

1. 通过驱动两个操作数A和B,以及驱动每个操作的选择行,来验证所有单独的操作是否工作(Add、Sub、
Increment、AND和OR)。
2. 验证如果select行在110-111之间,不发生操作。
3. 对于以上每个指令,选择A和B的最小值和最大值以及组合。假设A和B是4位,最大值可能是4 ' b1111
4. 验证加法和减法的溢出和下溢情况。如果A和B都是4'b1111,则A加法发生溢出,而如果B的值大于A,则减法发
生下溢。
5. 验证自增指令的溢出。如果A= 4'b1111,增量应该产生一个0值。
6. 一旦验证了各个场景,创建随机的操作码序列,验证一个操作的效果不会影响到下面的操作。检查相同的操作
码重复超过一次或不同的操作码以不同的模式重复的序列。
7. 为了创建激励,你可以设计一个随机的操作码和操作数生成器以及一个简单的驱动程序。为了检查结果,可以
编写一个简单的模型或ALU,并与相同的结果进行比较。

[249] 事件驱动和循环驱动的仿真器有何不同?

事件驱动仿真器对每个事件进行设计评估,采用每个事件并通过设计传播变化,直到达到稳定状态。事件是设计元素
的任何输入激励的更改。由于输入和下游设计的信号反馈的到达时间不同,一个设计可能在一个周期内被评估多次。

例如:考虑在时钟上运行的两个触发器之间的逻辑路径。组合逻辑路径可以有多个门和反馈路径。在时钟变化时,当第
一个触发器的输出发生变化时,它将应用于组合逻辑的输入,并进一步应用于组合逻辑中不同阶段输入的任何变化,这会
触发要评估的特定设计。在这个值稳定下来并且不再在那个时钟周期中变化之前,可能需要进行几次评估。大多数业界广
泛使用的模拟器都是事件驱动的,比如:来自Mentor的Questa、来自Synopsys的VCS或来自Cadence的Incisive模拟器。这是
因为事件驱动模拟器提供了准确的模拟环境。

基于循环的模拟器没有时钟周期内的时间概念。它们一次性评估状态元素或端口之间的逻辑。这有助于显著提高仿真
速度,因为每个逻辑元素在每个周期中只计算一次。缺点是它不能真正地检测信号中的任何小故障,而且它只在完全同步
的逻辑设计上表现正常。由于在仿真期间没有考虑设计的时间安排,因此需要再所有的静态时序分析工具对时序进行单独
的验证。基于循环的模拟器在一般设计中不太受欢迎,但在一些开发大型设计(如微处理器)的公司中可以定制和使用。
[250] 什么是事务(transaction?)?基于事务的验证有什么有点?

事务是一组低层信息(如一组信号)的高级抽象。当设计在信号级信息上运行时,testbench需要在信号级与设计接口驱
动程序和监视器,而testbench的所有其他方面都可以抽象为事务级。在基于事务的验证方法中,testbench以分层的方式
进行架构,其中只有较低层的组件在信号级进行操作,而所有其他组件都基于事务进行操作和通信,如下所示。

1. 基于事务的验证的主要优点是在一个项目内或跨不同项目的不同验证环境中重复使用事务性接口开发的组件。
例如:参考上面的图表,只有driver, monitor 和 responder需要信号级接口。一旦这些组件将信号级信息分组到一
个事务中,其他组件(如stimulus generators, slave models 和 scoreboards)都可以对事务进行操作。
2. 由于事务性组件需要由模拟器在事务性边界上进行评估,而不是在每个信号变化上进行评估,因此模拟可能会
快一些。
3. 如果一个设计改变了接口时序,那么只有驱动和监控组件需要改变,其他组件不受影响。

[251] 你使用或者熟悉的仿真调试工具是什么?

这是测试你对不同工具的意识的一个普遍问题。 根据你对各种工具的回答和经验,还可能会询问你在使用这些工具时
可能遇到的难易程度/局限性方面的观点。 没有固定的答案,但是常用的模拟器是Mentor Graphics的Questa,Synopsys的
VCS和Cadence的Incisive模拟器。 Synopsys的Verdi还是与DVE一起调试的常用工具。 正式工具包括来自Cadence的Jasper
和来自Mentor graphics的QuestaFormal。

[252] 我们什么时候需要参考模型来验证RTL设计? 使用参考模型的优点是什么?

参考模型通常是符合spec的不可综合模型,通常使用高级编程语言(例如C / SystemVerilog)编写。 有时会实现参考


模型,以便以周期级别的精度或更高级别的边界匹配设计规范。 例如:CPU /微处理器的参考模型应该准确地对指令边界
处的状态进行建模,而AMBA总线协议的参考模型应该根据该协议具有精确的周期。 参考模型通常用于检查器/记分板中,
以生成给定激励的预期响应,以便可以将其与实际结果或从设计获得的输出进行比较。

[253] 什么是总线功能模型?

传统上,总线功能模型(BFM)是用高级编程语言(如C / SystemVerilog)编写的不可综合模型,该模型可对总线接
口的功能进行建模,并可连接到用于仿真设计的设计接口。 在BFM的一侧,将是一个在信号级别上实现总线协议的接口,
另一侧将具有一个接口,以支持发送或接收事务。 随着时间的流逝,这个定义已经演变,在诸如UVM之类的方法中,没有
像BFM这样的实际组件,他的功能是由一系列组件(如驱动程序,监视器和接收器)实现的。

[254] 如何跟踪验证项目的进度? 使用什么指标?

有很多指标用于跟踪针对计划的验证进度。 验证计划根据定向测试或针对详细方案和特殊情况的功能覆盖率监视器,
捕获要验证的功能。 该计划还包括了有关验证环境开发的详细信息,其中包括激励产生和检查方法。 通过跟踪环境开发
(激励发生器,检查器,监视器等),测试开发和功能覆盖率监视器开发的完整性,可以在项目的早期阶段跟踪进度。 一
旦开发了大多数测试和受约束的随机数发生器,通常就可以在服务器场中以回归方式运行测试,然后根据回归通过率,错
误率和功能覆盖率来监视进度。
[255] 如何衡量验证的完整性,或者说何时/如何验证已完成?

当设计的表现与设计规范相匹配而没有任何错误时,可以将功能验证称为完成。为此,我们需要对设计施加激励,以
涵盖所有可能的输入可能,并验证设计是否符合规格要求,并且没有任何错误。但是,随着设计复杂性的不断提高,实际
上不可能定义所有可能的输入激励方案。此外,资源和时间的限制也使得这种完整的理想定义不切实际。

因此,在大多数项目中,验证的完整性与通过一组度量和过程获得的信心有关,该度量和过程使设计缺陷的风险降至
最低。以下是为实现对验证完整性的高度信任而遵循的一些度量和过程:

1. 审查验证计划和设计规范,以确保理解并捕获所有细节。
2. 确保环境开发方面的适当完整性,测试开发,功能覆盖率根据已有的计划开发。
3. 审查测试平台刺激生成器和约束,检查器,断言和覆盖率监视器的实现。
4. 确保以回归模式启用所有测试,并且在数周内始终没有失败,所有覆盖率指标均得到满足和理解。
5. 确保错误率和未解决的错误为零或者能够追溯,对设计没有影响。
6. 重要场景的波形审查。
7. 确保进行形式验证(尽可能)。
8. 将传入的漏洞和漏洞趋势与过去复杂程度类似的成功项目进行比较。

[256] 什么是GLS?为什么GLS很重要?

GLS是“门级仿真(Gate Level Simulation)”的首字母缩写。 在将RTL代码综合到门级网表之后,运行门级仿真。 GLS构成


了验证生命周期的重要组成部分。 除了STA(静态时序分析,Static Timing Analysis)和LEC(逻辑等效性检查,Logical
Equivalence Checking)之类的静态验证工具外,它也是必需的,因为STA和LEC不能涵盖/报告所有问题。 GLS主要用于:

1. 验证DFT扫描链。
2. 验证异步设计中的关键时序路径(STA未完成)。
3. 验证复位和上电流程。
4. 分析RTL中的X-Sate乐观度。
5. 收集开关活动以进行功率估算。

[257] 什么是功率和性能的权衡?

功率和性能是成功产品的两个重要设计要点。 尽管大多数设计在理想情况下都希望以尽可能低的功耗获得尽可能高的
性能,但实际上并非总是如此。 动态功耗与CV^2f成正比,其中f为频率,V为电压,C为电容。 因此,通常:

1. 降低电压会降低功耗,但会降低性能(随着延迟的增加)
2. 降低频率会降低功耗,但会降低性能(时钟变慢)

因此,为了获得最佳性能和功率目标,设计需要选择正确的电压和频率值。
注意:下一章(验证方法)的“电源和时钟”部分(6.3)中提供了有关电源和时钟的更多问题。

Verification Methodologies

UVM (Universal Verification Methodology)

[258] UVM的优点有哪些?

UVM是一种标准验证方法,已被收录为IEEE 1800.12标准。 UVM包括在设计测试平台和测试用例方面定义的方法,并


且还附带有一个类库,可帮助轻松轻松地构建有效的受约束的随机测试平台。该方法的一些优点和重点包括:

1. 模块化和可重用性:该方法被设计为模块化组件(Driver, Sequencer, Agents, Env等),这使跨模块级别的组件


重用为多单元或芯片级验证以及跨项目。
2. 将测试与测试平台分开:stimulus/sequencers方面的测试与实际的测试平台层次保持分开,因此可以在不同的
模块或项目之间重复使用激励。
3. 独立于模拟器:所有模拟器都支持基类库和方法,因此不依赖于任何特定的模拟器。
4. Sequence方法可以很好地控制刺激的产生:有几种开发序列的方法:randomization, layered sequences, virtual
sequences等。这提供了良好的控制和丰富的激励生成能力。
5. Config机制简化了具有深层次结构的对象的配置:Config机制有助于基于使用它的验证环境轻松地配置不同的
测试台组件,而无需担心测试台层次结构中任何组件的深度。
6. 工厂机制可轻松简化组件的修改:使用工厂创建每个组件可以使它们在不同的测试或环境中被覆盖,而无需更
改基础代码库。

[259] UVM的缺点有哪些?
显然,随着UVM方法在验证行业中的采用越来越广泛,UVM的优点会盖过缺点。

1. 对于任何不熟悉该方法的人,要了解所有细节的学习曲线就非常陡峭。
2. 方法论仍在发展中,并且有很多开销,有时可能会导致仿真缓慢或出现一些错误。

[260] 事务级建模的概念是什么?

事务级别建模(TLM,Transaction level Modelling)是一种在较高抽象级别上对任何系统或设计进行建模的方法。 在


TLM中,使用事务对不同模块之间的通信进行建模,从而抽象出所有底层实现细节。 这是验证方法中用于提高模块化和重
用性的主要概念之一。 即使DUT的实际接口由信号级别的活动表示,但大多数验证任务(如生成激励,功能检查,收集覆
盖率数据等)都可以在事务级别上更好地完成,只要使它们独立于实际信号级别的细节即可 。 这有助于在项目内部和项目
之间重用和更好地维护这些组件。

[261] 什么是TLM port和export?

在事务级建模中,不同的组件或模块使用事务对象进行通信。TLM port 定义了一组用于特定连接的方法(API),这些方


法的实际实现称为TLM export。TLM port和export之间的连接建立了两个组件之间的通信机制。

上面是一个简单的示例,演示了生产者如何使用一个简单的TLM port与消费者通信。生产者可以创建一个事务并将其
“put”到TLM port,而“put”方法(也称为TLM export)的实现将在读取生产者创建的事务的使用者中进行,从而建立一个通信
通道。

[262] 什么是TLM FIFO?

如果生产组件和消费组件都需要独立操作,则使用TLM FIFO进行事务性通信。在这种情况下(如下所示),生产组件生
成事务并将其“放入”FIFO,而消费组件每次从FIFO获取一个事务并对其进行处理。

[263] TLM FIFO中的get方法和peek方法有什么区别?

get()操作将从TLM FIFO中返回一个事务(如果可用),并从FIFO中删除该事务。如果FIFO中没有可用的事务,它将阻塞
并等待,直到FIFO至少有一个事务。peek()操作将从TLM FIFO中返回一个事务(如果可用),而不会实际从FIFO中删除该项
目。它也是一个阻塞调用,如果FIFO没有可用的条目,它将等待。

[264] TLM FIFO中的get方法和try_get方法有什么区别?

get()是一个从TLM FIFO获取事务的阻塞调用。因为它是阻塞的,所以如果FIFO中没有事务,任务get()将等待。
try_get()是一个非阻塞调用,即使FIFO中没有可用的事务,它也会立即返回。try_get()的返回值指示是否返回成功。下面是
使用get()和try_get()的两个等效实现

阻塞方法,get

1 class consumer extends uvm_component;


2 uvm_get_port #(simple_trans) get_port;
3 task run;
4 for(int i=0; i<10; i++)
5 begin
6 t = get(); //blocks until a transaction is returned //Do something with it.
7 end
8 endtask
9 endclass

非阻塞方法,try_get
1 class consumer extends uvm_component;
2 uvm_get_port #(simple_trans) get_port;
3 task run;
4 for(int i=0; i<10; i++)
5 begin
6 //Try get is nonblocking. So keep attempting
7 //on every cycle until you get something
8 //when it returns true
9 while(!get_port.try_get(t))
10 begin
11 wait_cycle(1); //Task that waits one clock cycle
12 end //Do something with it
13 end
14 endtask
15 endclass

[265] analysis port和TLM port有什么区别?analysis FIFO和TLM FIFO的区别是什么?何时使用


analysis FIFO 和 TLM FIFO?

TLM port/FIFOs用于具有使用put/get方法建立的通信通道的两个组件之间的事务级通信。

analysis port/FIFOs是另一种事务性通信通道,用于组件将事务广播给多个组件。

TLM port/FIFO用于driver和sequencer之间的连接,而analysis port/FIFOs用于monitor广播事务,这些事务可由


scoreboard或覆盖率收集组件接收。

[266] sequence和sequence item有什么区别?

sequence item是对在两个组件之间传输的信息进行建模的对象(有时它也可以称为事务,transaction)。例如:考虑从
CPU到主内存的内存访问,在主内存中CPU可以执行内存读或内存写,每个事务都有一些信息,比如地址、数据和读/写类
型。sequence可以被认为是一个已定义的sequence item模式,它可以被发送到驱动程序以注入到设计中。sequence item
的模式是由如何按顺序实现body()方法定义的。例如:扩展上面的例子,我们可以定义一个从10个事务中读取增量内存地址
的sequence。在这种情况下,body()方法将被实现来生成10次sequence item,并将它们发送到驱动程序,同时在下一个项
之前递增或随机地址。

[267] uvm_transaction和uvm_sequence_item的区别是什么?

uvm_transaction 派生自uvm_object。uvm_sequence_item不仅是一个事务,还组织了一些信息在一起,增加了一些其他
信息,如:sequence id,和transaction id等等。建议使用uvm_sequence_item实现基于sequence的激励。

[268] component类中的copy,clone和create方法有什么区别?

1. create()方法用于构造对象。
2. copy()方法用于将一个对象复制到另一个对象。
3. clone()方法是一个一步的命令来创建和复制一个现有的对象到一个新的对象句柄。它首先通过调用create()方法
创建一个对象,然后调用copy()方法将现有对象复制到新句柄。

[269] 解释UVM方法学中的agent的概念

UVM agent是一个组件,它将一组其他uvm_components集中在DUT的特定pin级接口上。大多数dut具有多个逻辑接
口,并且使用一个agent来对所有组件进行分组:driver, sequencer, monitor, 和其他组件,这些组件在特定的接口上进行操
作。使用这种层次结构组织组件有助于跨具有相同接口的不同验证环境和项目重用“agent”。

下图是一个典型的agent组织方式。
[270] UVM agent中有多少种不同的组件?

如前所述,agent是基于DUT的逻辑接口进行分组的组件集合。一个agent通常有一个driver和一个sequencer来驱动它
所操作的接口上的被DUT的激励。它还具有一个monitor和一个分析组件(如记分牌或覆盖率收集器)来分析该接口上的活
动。此外,它还可以拥有配置agent及其组件的配置对象。

[271] 在uvm_object类中,get_name和get_full_name方法有什么区别?

get_name返回对象的名称,由new构造函数或set_name()方法中的名称参数提供。get_full_name返回对象的完整层次
结构名称。对于uvm_components,在打印语句中使用时非常有用,因为它显示了组件的完整层次结构。对于没有层次结
构的sequence或config对象,它将输出与get_name相同的值

[272] ACTIVE agent和PASSIVE agent有什么区别?

ACTIVE agent是能够在其操作的pin级接口上生成激励的代理。这意味着,像driver和sequencer这样的组件将被连接,
并且会有一个sequence在其上运行以生成激励。

PASSIVE agent是不生成任何激励,但能监视接口上发生的活动的代理。这意味着,在被动代理中不会创建driver和
sequencer。

在需要生成激励的模块级验证环境中,agent通常被配置为ACTIVE 。当我们从模块级转移到芯片级验证环境时,可以
配置相同的agent,在这种环境中不需要生成激励,但是我们仍然可以在调试或覆盖率方面使用相同的agent来监视信号。

[273] 如何将agent配置为ACTIVE或者PASSIVE?

UVM agent有一个类型为UVM_ACTIVE_PASSIVE_e的变量,该变量定义了agent是ACTIVE的(UVM_ACTIVE),它构造了
sequencer和driver,还是PASSIVE的(UVM_PASSIVE),它既不构造driver,也不构造sequencer。这个参数被称为active,默
认情况下它被设置为UVM_ACTIVE。

当在environment类中创建agent时,可以使用set_config_int()更改这一点。然后,agent的build阶段应该具有如下代
码,以有选择地构造driver和sequencer。
1 function void build_phase(uvm_phase phase);
2 if(m_cfg.active == UVM_ACTIVE)
3 begin //create driver, sequencer
4 end
5 endfunction

[274] 什么是sequencer和driver?为什么需要他们?

driver是将transaction或sequence item转换为一组基于信号接口协议的pin级别切换的组件。

sequencer是一个组件,它将sequence item从sequencer路由到driver,并将响应从driver路由回sequence。还负责多
个sequence(如果存在)之间的仲裁。

在像UVM这样的TLM方法中,这些组件是必需的,激励产生是根据事务进行抽象的,而sequencer和driver是路由它们
并将它们转换为实际的信号行为的组件。

[275] UVM中的monitor和scoreboard有什么区别?

monitor是观察pin级活动并将其观察结果转换为事务或sequence_items的组件。它还通过analysis port将这些事务发送
到分析组件。

scoreboard是一个分析组件,用来检查DUT是否正常工作。UVM scoreboard使用来自agent内部实现的monitor来分析
事务。

[276] 如何启动UVM?

run_test方法(一个静态方法)可以激活UVM测试平台。它通常在顶级测试模块的“initial begin…end”块中调用,它接受
要运行的测试类的参数。然后它触发测试类的构造,build_phase()将在层次结构中执行并进一步构造
Env/Agent/Driver/Sequencer 对象。

[277] 运行一个sequence需要哪些步骤?

运行一个sequence需要三个步骤:

1. 创建一个sequence。使用factory create方法创建一个序列,如下所示:

1 my_sequence_c seq;
2 seq = my_sequence_c:: type_id::create(“my_seq”)

2. 配置或随机化一个sequence。一个sequence可能有几个需要配置或随机化的数据成员。因此,要么配置值,要
么调用seq.randomize()
3. 开始一个sequence。sequence使用sequence.start()方法启动。start方法接受一个参数,该参数是指向sequencer
的句柄。一旦sequence启动,sequence中的body()方法就会被执行,它定义了序列的操作方式。start()方法是阻
塞的,只在sequence执行完成后返回。

[278] 解释sequencer和driver中的握手协议

UVM sequence-driver API主要在sequence和driver端使用阻塞方法,如下所述,用于将sequence item从sequencer传


输到driver并从driver收集响应。

在sequence端,有两种方法:

1. start_item(<item>):请求sequencer访问sequence item的driver,并在sequencer授予访问权时返回。
2. finish_item(<item>):该方法使driver接收sequencer item,是一个阻塞方法,仅在driver调用item_done()方法后
返回。

在driver侧

1. get_next_item(req):这是一个在driver中的阻塞方法,它阻塞直到一个sequence item在连接到sequencer的端口
上被接收。该方法返回sequence item,sequence item可由driver转换为pin级协议。
2. item_done(req):driver使用这个非阻塞方法向sequencer发出信号,表明它可以解除对sequence中finish_item()方
法的阻塞,无论是在driver接受sequence请求时,还是在它已经执行sequence请求时。

下图说明了sequencer和driver之间的协议握手,这是在sequencer和driver之间传输请求和响应时最常用的握手。
还存在一些其他替代方法:在driver中使用get()方法,它相当于调用get_next_item()和item_done()。
有时,如果从driver到sequence的响应包含比请求类中封装的信息更多的信息,那么还需要一个单独的响应端口。
在这种情况下,sequencer将使用get_response()阻塞方法,当driver使用put()方法在该端口上发送单独的响应时,该方法
将被解除阻塞。如下图所示。

[279] 什么是sequence中的pre_body()和post_body()函数?他们通常会被调用吗?

pre_body()是sequence类中的一个方法,它在调用sequence的body()方法之前被调用。在调用body()方法之后,依次
调用post_body()方法。并不总是调用pre_body()和post_body()方法。uvm_sequence::start()有一个可选参数,如果将其设
置为0,将导致这些方法不被调用。下面是一个sequence中的start()方法的形式参数。
1 virtual task start (
2 uvm_sequencer_base sequencer, // Pointer to sequencer
3 uvm_sequence_base parent_sequencer = null, // parent sequencer
4 integer this_priority = 100, // Priority on the sequencer
5 bit call_pre_post = 1); // pre_body and post_body called

[280] sequence中的start方法使阻塞还是非阻塞方法?

start方法是一个阻塞方法,阻塞进程直到sequence的body方法执行完毕。

[281] sequencer有哪些仲裁机制?

多个sequence可以与同一个接口的driver并发交互。sequencer支持多种仲裁机制,以确保在任何时间点只有一个
sequence可以访问driver。哪个sequence可以发送sequence_item取决于用户选择的仲裁机制。在UVM中实现了五种内置
的仲裁机制。还有一个附加的回调函数可以实现用户定义的算法。sequencer具有一种称为set_arbitration()的方法,可以调
用该方法来选择sequencer应使用哪种算法进行仲裁。可以选择的六种算法如下:

1. SEQ_ARB_FIFO(默认值)。如果指定了此仲裁模式,那么sequencer将使用一个FIFO选择sequence。例如:如
果seq1,seq2和seq3在sequencer上运行,它将首先从seq1中选择一个item,然后从seq2中选择一个item,然
后从seq3中选择一个item(如果可用),然后继续。
2. SEQ_ARB_WEIGHTED:如果选择此仲裁模式,则始终首先选择优先级最高的sequence,直到没有可用为止,然
后再选择下一个优先级sequence,依此类推。如果两个sequence具有相同的优先级,则以随机顺序从中选择它
们。
3. SEQ_ARB_RANDOM:如果选择此仲裁模式,忽略所有优先级以随机顺序选择sequence。
4. SEQ_ARB_STRICT_FIFO:与SEQ_ARB_WEIGHTED相似,不同之处在于,如果两个sequence具有相同的优先级,
则来自这些sequence的项顺序是按FIFO顺序而不是随机顺序选择的。
5. SEQ_ARB_STRICT_RANDOM:与SEQ_ARB_RANDOM相似,只是不忽略优先级。首先从优先级最高的序列中随
机选择sequence,然后依次选择下一个和最高顺序。
6. SEQ_ARB_USER:此算法允许用户定义用于sequence之间仲裁的自定义算法。这是通过扩展uvm_sequencer类并
覆盖user_priority_arbitration()方法来完成的。

[282] 在sequencer上启动sequence时,如何指定其优先级?

通过将参数传递给序列的start()方法来指定优先级。 根据为权重来确定优先级。 例如:第三个参数指定sequence的优


先级。

1 seq_1.start(m_sequencer, this, 500); //Highest priority


2 seq_2.start(m_sequencer, this, 300); //Next Highest priority
3 seq_3.start(m_sequencer, this, 100); //Lowest priority among three sequences

[283] sequence如何才能独占sequencer的访问权限?

当在sequencer上运行多个sequence时,sequencer进行仲裁并授予sequence的访问权限。 有时,一个sequence可能
希望独占sequencer的访问权限,直到将其所有sequence_item送到driver为止(例如:如果您希望在不中断的情况下激发
确定性模式)。 有两种机制允许sequence获得对sequencer的独占访问权。

1. 使用lock()和unlock():sequence可以调用在其上运行的sequencer的lock方法。当sequence通过sequencer仲裁
机制获得下一个访问权限时,将授予该sequence对driver的独占访问权限。如果还有其他标记为更高优先级的
sequence,则此序列需要等待,直到获得授权。权限锁定后,其他sequence将无法访问driver,直到该序列在
sequencer上调用unlock()后,权限将被释放。 lock方法是一个阻塞方法,直到授予锁后才返回。
2. 使用grab()和ungrab():grab方法类似于lock方法,可以在需要独占访问时由sequence调用。grab和lock之间的
区别在于,调用grab()时,它将立即生效,并且该sequence将100%获得下一个sequencer的授权机会,从而高于
所有当前的sequence优先级。阻止sequence通过grab独占sequencer的唯一方法是sequencer上已经存在的
lock()或grab()。

[284] sequencer上的lock和grab有什么区别?

在sequencer上运行的sequence使用sequencer的grab()和lock()方法来获得对该序列器的独占访问权,直到调用了相应
的unlock()或ungrab()。 grab和lock之间的区别在于,当调用sequencer上的某个grab()时,它将立即生效,并且该
sequence将获得下一个sequencer的授权,从而高于所有当前的sequence优先级。 但是,对lock()的调用将需要等待,直到
sequence根据设置的优先级和仲裁机制获得其下一个授权为止。 在用法方面,可以使用lock来对优先级中断进行建模,并
使用grab来对不可屏蔽的中断进行建模,在别的建模场景中也很有用。
[285] 流水线和非流水线sequencer-driver模型有什么区别?

根据需要如何通过interface发送激励,在UVM driver类中可以实现两种模式。

1. 非流水线模型:如果driver一次仅对一个事务进行建模,则称为非流水线模型。 在这种情况下,sequence可以
将一个事务发送给driver,并且driver可能需要几个周期(基于接口协议)才能完成驱动该事务。 只有在那之
后,驱动程序才会接受sequencer的新事务.

1 class nonpipe_driver extends uvm_driver #(req_c);


2 task run_phase(uvm_phase phase);
3 req_c req;
4 forever begin
5 get_next_item(req); // Item from sequence via sequencer
6 // drive request to DUT which can take more clocks
7 // Sequence is blocked to send new items til then
8 item_done(); // ** Unblocks finish_item() in sequence
9 end
10 endtask: run_phase
11 endclass: nonpipe_driver

2. 流水线模型:如果driver一次建模多个活动事务,则称为流水线模型。 在这种情况下,sequence可以继续向
driver发送新事务,而无需等待driver完成之前的事务。 在这种情况下,对于从该sequence发送的每个事务,
driver都会派生一个单独的进程来基于该事务驱动接口信号,不会等到它完成后再接受新事务。 如果我们要在接
口上回送请求而不等待设计的响应,应该采用这种建模。

1 class pipeline_driver extends uvm_driver #(req_c);


2 task run_phase(uvm_phase phase);
3 req_c req;
4 forever begin
5 get_next_item(req); // Item from sequence via sequencer
6 fork begin //drive request to DUT which can take more clocks
7 //separate thread that doesn't block sequence
8 //driver can accept more items without waiting
9 end
10 join_none
11 item_done(); // ** Unblocks finish_item() in sequence
12 end
13 endtask: run_phase
14 endclass: pipeline_driver

[286] 如何确保在sequencer-driver上运行多个sequnce时,响应会从driver发送回正确的
sequence?

如果从driver返回了几个序列之一的响应,则sequencer将sequence中的sequenceID字段用于将响应路由回正确的
sequence。 driver中的响应处理代码应调用set_id_info(),以确保任何响应项都具有与其原始请求相同的sequenceID。 下
面是driver中的一个示例代码,该代码获取sequencer_item的id并发送回响应(请注意,这是用于说明的参考伪代码,并且
假定某些功能在其他地方进行了编码)

1 class my_driver extends uvm_driver;


2 //function that gets item from sequence port and
3 //drives response back
4 function drive_and_send_response();
5 forever begin
6 seq_item_port.get(req_item);
7 //function that takes req_item and drives pins
8 drive_req(req_item);
9 //create a new response
10 itemrsp_item = new();
11 //some function that monitors response signals from dut
12 rsp_item.data = m_vif.get_data();
13 //copy id from req back to response
14 rsp.set_id_info(req_item);
15 //write response on rsp port
16 rsp_port.write(rsp_item);
17 end
18 endfunction
19 endclass
[287] 什么是m_sequencer句柄?

启动sequence时,它始终与启动sequencer相关联。 m_sequencer句柄指向了当前sequence挂载的sequencer。 使用
此句柄,序列可以访问UVM组件层次结构中的任何信息和其他资源句柄。

[288] m_sequencer和p_sequencer有什么区别?

UVM sequence是寿命有限的对象,与sequencer,driver或monitor不同,后者是UVM的组件,并且在整个仿真时间内
都存在。 因此,如果需要从测试平台层次结构(组件层次结构)访问任何成员或句柄,sequence需要sequencer的句柄。
m_sequencer是uvm_sequencer_base类型的句柄,默认情况下在uvm_sequence中可用。 但是,要访问在其上运行
sequence的真实sequencer,我们需要将m_sequencer转换为真实sequencer,通常称为p_sequencer(可以自定义为任何
名字,不仅仅是p_sequencer)。 下面是一个简单的示例,其中sequence要访问clock monitor组件的句柄,该组件可在
sequencer中用作句柄。

1 //clock monitor component which is available as a handle in the sequencer.


2
3 class test_sequence_c extends uvm_sequence;
4 test_sequencer_c p_sequencer;
5 clock_monitor_c my_clock_monitor;
6 task pre_body();
7 if(!$cast(p_sequencer, m_sequencer))begin
8 `uvm_fatal("Sequencer Type Mismatch:", " Wrong Sequencer");
9 end
10 my_clock_monitor = p_sequencer.clk_monitor;
11 endtask
12 endclass
13
14 class test_Sequencer_c extends uvm_sequencer;
15 clock_monitor_c clk_monitor;
16 endclass

[289] 生成sequence时,早期随机化和后期随机化有什么区别?

在早期随机化中,首先使用randomize()对sequence进行随机化,然后使用start_item()来请求对sequencer的访问,这
是一个阻塞调用,根据sequencer的繁忙程度可能会花费一些时间。 下面的示例显示一个对象(req)首先被随机化,然后
sequence等待仲裁。

1 task body()
2 assert(req.randomize());
3 start_item(req); //Can consume time based on sequencer arbitration
4 finish_item(req);
5 endtask

在后期随机化中,sequence首先调用start_item(),等待直到sequencer批准仲裁,然后在将事务发送给
sequencer/driver之前,调用randomize。 这样做的好处是,可以将item及时地随机化,并且可以在将item发送给driver之
前使用来自设计或其他组件的任何反馈。 以下代码中使用的就是后期随机化(req)

1 task body()
2 start_item(req); //Can consume time based on sequencer arbitration
3 assert(req.randomize());
4 finish_item(req);
5 endtask

[290] 什么是subsequence?

subsequence是从另一个sequence开始的sequence。 从sequence的body()中,如果调用了另一个sequence的start(),
则通常将其称为subsequence。

[291] UVM driver中的get_next_item()和try_next_item()有什么不同?


get_next_item是一个阻塞方法(driver-sequencer API的一部分),阻塞直到sequence_item可供driver处理为止,并返
回指向sequence_item的指针。 try_next_item是非阻塞版本,如果没有sequence_item可用于driver处理,则它将返回空指
针。

[292] UVM driver中的get_next_item()和get()有什么不同?

get_next_item是一个阻塞调用,用于从sequencer FIFO获取sequence_item以供driver处理。 driver处理完


sequence_item后,需要先使用item_done完成握手,然后再使用get_next_item()请求新的sequence_item。

get()也是一个阻塞调用,它从sequencer FIFO获取sequence_item以供driver处理。 但是,在使用get()时,由于get()


方法隐式完成了握手,因此无需显式调用item_done()。

[294] UVM driver中的peek()和get()有什么不同?

driver的get()方法是一个阻塞调用,从sequencer FIFO获取sequence_item以供driver处理。 一旦有sequence_item可


用,它就会解锁,并与sequencer完成握手。 peek()方法类似于get(),并阻塞直到sequence_item可用为止。 但是,它不会
从sequencer FIFO中删除sequence。 因此,多次调用peek()将在driver中返回相同的sequence_item。

[295] 带有和不带有参数调用时,driver-sequencer API的item_done()方法有什么区别?

item_done()方法是driver中的一种非阻塞方法,用于在get_next_item()或try_next_item()成功之后与sequencer完成握
手。 如果不需要发回响应,则不带参数调用item_done(),它将完成握手,而无需在sequencer响应FIFO中放置任何内容。
如果希望将响应发送回去,则将item_done()与指向响应sequence_item的句柄作为参数一起传递。 该响应句柄将放置在
sequencer响应FIFO中,sequence通过这种方式进行响应。

[296] 下面哪些方法是阻塞的,哪些是非阻塞的?

1. get()
2. get_next_item()
3. item_done()
4. put()
5. try_next_item()
6. peek()

get(), get_next_item(), peek() 是阻塞的

try_next_item(), item_done(), and put() 是非阻塞的

[297] UVM driver中,下面哪些代码是错误的?

1 1)
2 function get_drive_req();
3 forever begin
4 req = get();
5 req = get();
6 end
7 endfunction
8 2)
9 function get_drive_req();
10 forever begin
11 req = get_next_item();
12 req = get_next_item();
13 item_done();
14 end
15 endfunction
16 3)
17 function get_drive_req();
18 forever begin
19 req = peek();
20 req = peek();
21 item_done();
22 req = get();
23 end
24 endfunction
2是错的,因为不能在调用item_done之前两次调用get_next_item,它无法完成与sequencer的握手。

[298] 如何停止sequencer上的所有sequence?

sequencer具有stop_sequences()方法,可用于停止所有sequence。 但是,此方法不检查driver当前是否正在处理任何
sequence_items。 因此,如果driver调用item_done()或put(),则可能会出现致命错误,因为sequence指针可能无效。 因
此,用户需要注意,一旦调用stop_sequence(),就禁用了sequencer线程(如果在fork中启动)。

[299] 用户调用sequence.print()方法时,将调用sequence中的哪个方法?

convert2string():建议实现此函数,该函数返回对象的字符串表示形式(其数据成员的值)。 这对于将调试信息打印
到模拟器脚本或日志文件很有用。

[300] 找出UVM sequence的以下代码部分中的所有潜在问题

1 task body();
2 seq_item_c req;
3 start_item(req);
4 #10 ns;
5 assert(req.randomize());
6 finish_item(req);
7 endtask

应该避免在start_item和finish_item之间添加延迟。 start_item返回后,该sequence将赢得仲裁并可以访问driver-
sequencer。 从那时起直到finish_item的任何延迟都将阻塞driver-sequencer,并且使得任何其他sequence都不能访问
driver-sequencer。 如果在一个接口上运行多个sequence,并且延迟很大,设计接口上有很多空闲时,这会造成很大的麻
烦。

[301] 什么是virtual sequence?如何使用virtual sequence?为什么要使用virtual sequence?

virtual sequence是控制多个sequencer中激励生成的序列。 由于sequence,sequencer和driver集中在单个接口上,因


此几乎所有测试平台都需要virtual sequence来协调不同接口之间的激励和交互。 virtual sequence在子系统或系统级别的
测试台上也很有用,可以使单元级别的sequence以协调的方式运行。 下图从概念上展示了这一点,其中virtual sequence
具有三个sequencer的句柄,这些sequencer连接到driver,以连接到DUT的三个独立接口。 然后,virtual sequence可以在
每个接口上生成subsequence,并在相应的subsequencer上运行它们。
[302] 如下所示,给定一个简单的单端口RAM,它可以读取或写入,请shiyong1UVM编写一个
sequence和driver以测试他的读取和写入。 假设read_enable = 1表示读取,而write_enable = 1表
示写入。 读或写操作只能在一个周期内发生。

要实现UVM driver和sequence,我们需要先定义sequence item类,然后sequence和driver类才能将其用作事务进行通


信。 以下是sequence item,sequence和driver类的示例代码。 可以使用类似的方法使用UVM处理任何编程代码。

sequence item是用于sequence和driver之间通信的事务。 应该抽象出最终需要在DUT上进行信号驱动的所有信


息。

1 class rw_txn extends uvm_sequence_item;


2
3 rand bit[7:0] addr; //address of transaction
4 typedef enum {READ, WRITE} kind_e; //read or write type
5 rand kind_e sram_cmd;
6 rand bit[7:0] datain; //data
7
8 //Register with factory for dynamic creation
9 `uvm_object_utils(rw_txn)
10
11 //constructor
12 function new (string name = "rw_txn");
13 super.new(name);
14 endfunction
15
16 //Print utility
17 function string convert2string();
18 return $psprintf("sram_cmd=%s addr=%0h datain=%0h,sram_cmd.name(),addr,datain);
19 endfunction
20
21 endclass

生成上述类型的10个事务并发送给driver的sequence:

1 class sram_sequence extends uvm_sequence#(rw_txn) ; //Register with factory


2
3 `uvm_object_utils(sram_sequence)
4
5 function new(string name ="sram_sequence");
6 super.new(name);
7 endfunction
8
9 //Main Body method that gets executed once sequence is started
10 task body();
11 rw_txn rw_trans; //Create 10 random SRAM read/write transaction and send to driver
12 repeat(10) begin
13 rw_trans = rw_txn::type_id::create(.name("rw_trans"),.contxt(get_full_name()));
14 start_item(rw_trans); //start arbitration to sequence
15 assert (rw_trans.randomize());//randomize item
16 finish_item(rw_trans); //send to driver
17 end
18 endtask
19
20 endclass

driver代码,从sequence中接收上述事务并根据SRAM协议进行驱动。

1 class sram_driver extends uvm_driver#(rw_txn);


2 `uvm_component_utils(sram_driver)
3 virtual sram_if vif; //Interface that groups dut signals
4 function new(string name,uvm_component parent = null);
5 super.new(name,parent);
6 endfunction
7
8 //Build Phase
9
10 //Get the virtual interface handle from config_db
11 function void build_phase(uvm_phase phase);
12 super.build_phase(phase);
13 if (!uvm_config_db#(virtual sram_if)::get(this, "", "sram_if", vif))
14 begin
15 `uvm_fatal("SRAM/DRV", "No virtual interface specified")
16 end
17 endfunction
18
19 //Run Phase
20
21 //Implement the Driver-Sequencer API to get an item
22 //Based on if it is Read/Write - drive on SRAM interface the corresponding pins virtual
23 task run_phase(uvm_phase phase);
24 super.run_phase(phase);
25 this.vif.read_enable <= '0;
26 this.vif.write_enable <= '0;
27 forever begin
28 rw_txn tr;
29 @ (this.vif.master_cb);
30 //First get an item from sequencer
31 seq_item_port.get_next_item(tr);
32 @ (this.vif.master_cb); //wait for a clock edge
33 uvm_report_info("SRAM_DRIVER ", $psprintf("Got Transaction%s",tr.convert2string()));
34 //Decode the SRAM Command and call either the read/write function
35 case (tr.sram_cmd)
36 rw_txn::READ: drive_read(tr.addr, tr.dataout);
37 rw_txn::WRITE: drive_write(tr.addr, tr.datain);
38 endcase
39 //Handshake DONE back to sequencer
40 seq_item_port.item_done();
41 end
42 endtask: run_phase
43
44 //Drive the SRAM signals needed for a Read
45 virtual protected task drive_read(input bit [31:0] addr, output logic [31:0] data);
46 this.vif.master_cb.addr <= addr;
47 this.vif.master_cb.write_enable <= '0;
48 this.vif.master_cb.read_enable <= '1;
49 @ (this.vif.master_cb);
50 this.vif.master_cb.read_enable <= '0;
51 data = this.vif.master_cb.dataout;
52 endtask: drive_read
53
54 //Drive the SRAM signals needed for a Write
55 virtual protected task drive_write(input bit [31:0] addr, input bit [31:0] data);
56 this.vif.master_cb.addr <= addr;
57 this.vif.master_cb.write_enable <= '1;
58 this.vif.master_cb.read_enable <= '0;
59 @ (this.vif.master_cb);
60 this.vif.master_cb.write_enable <= '0;
61 endtask: drive_write
62 endclass

[303] 什么是工厂?
UVM中的“工厂”是一个特殊的查找表,其中记录了所有UVM组件和事务。 在UVM中创建组件和事务对象的推荐方法
是使用工厂方法create()。 使用工厂创建对象可以很方便地实现类型覆盖,而不必更改验证环境的结构或修改验证环境的
代码。

[304] 使用new()和create()创建对象有什么区别?

UVM中用于创建组件或事务对象的推荐方法是使用内置方法::type_id::create(),而不是直接调用构造函数new()。

create方法在内部调用工厂,查找所请求的类型,然后调用构造函数new()来创建对象。 这样可以轻松地重写类型,可
以指定类的类型(基类,一个或派生类),并且所有其他测试平台组件将能够创建该类类型的对象而无需任何代码更改。

new()构造函数将仅创建给定类型的对象,因此使用new()将不允许在运行时更改类类型。 因此,使用new()意味着测试
平台代码将需要根据要使用的不同类型进行更改。

[305] 如何在工厂中注册uvm_component类和uvm_sequence类?

使用uvm_object_utils()宏注册uvm_sequence类,uvm_component_utils()宏注册uvm_component类,下面是示例代码

1 class test_seq_c extends uvm_sequence;


2 `uvm_object_utils(test_seq_c)
3
4 class test_driver_c extends uvm_component;
5 `uvm_component_utils(test_driver_c)

[306] 为什么要将类注册到工厂?

工厂是UVM中使用的一种特殊查找表,用于创建组件或事务类型的对象。 使用工厂创建对象的好处是,测试平台构建
可以在运行时决定创建哪种类型的对象。 因此,一个类可以用另一个派生类替换,而无需任何实际代码更改。 为确保此功
能,建议所有类都在工厂注册。 如果不注册到工厂,则将无法使用工厂方法::type_id::create()构造对象。

[307] 工厂覆盖(override)的意思是?

UVM工厂允许在构造时将一个类替换为另一个派生类。 通过将一个类替换为另一个类而不需要编辑或重新编译测试平
台代码,这对于控制测试平台的行为很有用。

[308] 工厂的实例覆盖(instance override)和类型覆盖(type override)有什么区别?

类型覆盖意味着每次在测试平台层次结构中创建组件类类型时,都会在其位置创建替代类型。 这适用于该组件类型的
所有实例。

另一方面,实例覆盖意味着仅覆盖组件类的特定实例。 组件的特定实例由该组件在UVM组件层次结构中的位置进行索
引。

由于只有UVM组件类可以在UVM测试平台中具有层次结构,因此实例覆盖只能作用于组件类,而sequence(或者说
object)只能覆盖类型。

[309] 实例覆盖和类型覆盖都可以作用于UVM_component和transaction吗?

不,只有UVM_component类是UVM测试平台层次结构的一部分,从而可以使用实例覆盖。 sequence_item或
sequence不是UVM测试平台层次结构的一部分,因此只能使用类型覆盖来覆盖,类型覆盖将覆盖该类型的所有对象。

[310] uvm_obejction是什么?在哪里使用它们?

uvm_objection类提供了一种在多个组件和sequence之间共享计数器的方法。 每个组件/sequence可以异步
地"raise"和"drop" objections,这会增加或减少计数器值。 当计数器达到零(从非零值开始)时,将发生"all dropped"情
况。

objection机制最常用于UVM phase机制中,以协调每个run_time phase的结束。 在phase中启动的用户进程会首先


raise objections,并在进程完成后drop objections。 当一个phase中的所有进程都放下objections时,该phase的objections
计数器清零。 这种“all dropped”的情况说明每个进程都同意结束该phase。

下面是一个示例,说明如何在sequencer(my_sequencer)上启动sequence(my_test_sequence)并在sequence执行后drop
objections
1 task run_phase( uvm_phase phase);
2 phase.raise_objection( this );
3 my_test_sequence.start(my_sequencer);
4 phase.drop_objection( this );
5 endtask

[311] 如何在UVM中实现仿真超时机制?

如果由于超出最大时间的某些错误而导致测试无法进行,那么仿真超时机制有助于停止仿真。 在UVM中,
set_global_timeout(timeout)是一个便捷函数,用于将uvm_top.phase_timeout变量设置为超时值。 如果run()阶段在该这个
时间内之前没有结束,则仿真将停止并报告错误。

在顶层模块中调用此函数,该模块会按以下方式启动测试

1 module test;
2 initial begin
3 set_global_timeout(1000ns);
4 end
5
6 initial begin
7 run_test();
8 end
9 endmodule

[312] uvm中的phase机制是什么意思?

与基于module的测试平台(所有module静态地存在于层次结构中)不同,基于类的测试平台需要管理不同对象的创
建以及这些对象中各种task和function的执行。 phase是基于类的测试平台中重要的概念,它具有一致的测试平台执行流
程。 从概念上讲,测试执行可以分为以下阶段-配置,创建测试平台组件,运行时激励和测试结束。 UVM为每一个阶段中
定义了标准phase。

[313] uvm_component有哪些phase?UVM的run_phase有哪些子phase?

UVM使用标准phase来排序仿真过程中发生的主要步骤。 有三组阶段,按以下顺序执行。

Build phases-在Build phases; 测试平台将会被配置和构建。 它具有以下子phase,这些子phase都在


uvm_component基类中作为虚方法实现。
build_phase()
connect_phase()
end_of_elaboration()
Run time phases-这些phase会消耗时间,这是大多数测试执行的地方。

start_of_simulation()

run_phase(),run_phase()进一步分为以下12个子phase:

pre_reset
reset
post_reset
pre_configure
configure
post_configure
pre_main
main
post_main
pre_shutdown
shutdown
post_shutdown
Clean up phase -此phase 在测试结束后执行,用于收集并报告测试的结果和统计信息。 包括以下子phase :

extract()
check()
report()
final()
[314] 为什么build_phase是自顶向下执行的?

在UVM中,所有组件(例如test,Env,Agent,Driver,Sequencer)都基于uvm_component类,并且组件始终具有
层次结构。 build_phase()方法是uvm_component类的一部分,用于从父组件构造所有子组件。 因此,要构建测试平台层
次结构,始终需要先拥有一个父对象,然后才能构造其子对象,并可以使用build_phase进一步构造其子对象。 因此,
build_phase()总是自顶向下执行。

例如:uvm_test类调用build_phase,构造该test的所有uvm_env组件,而每个uvm_env类的build_phase()应该构造属于
该uvm_env的所有uvm_agent组件,然后继续。 对于所有其他phase,调用顺序实际上并不重要。 所有组件的run_phase()
是并行运行的。

[315] uvm_component中的phase_ready_to_end()的作用是?

phase_ready_to_end(uvm_phase phase)是组件类的回调方法,当相应phase的所有objection均被放下并且该phase
将要结束时,会调用该方法。 组件类可以使用此回调方法来定义phase即将结束时需要执行的任何功能。

例如,如果某个组件希望将phase结束延迟到某个条件,甚至在所有objections均被放下之后,也可以使用此回调方法
来完成。

再比如,如果一个激励或应答sequence正在运行,在主sequence结束之前,则可以使用main_phase()中的
phase_ready_to_end()回调方法来停止那些激励或应答sequence。

[316] 什么是uvm_config_db?它的作用是?

uvm_config_db机制支持在不同的测试平台组件之间共享配置和参数。 用名为uvm_config_db的配置数据库启用该功
能。 任何测试台组件都可以使用变量,参数,对象句柄等填充配置数据库。

其他测试平台组件可以从配置数据库访问这些变量,参数,对象句柄,而无需真正知道其在层次结构中的位置。

例如,顶层测试平台模块可以通过uvm_config_db储存虚接口句柄。 然后,任何uvm_driver或uvm_monitor组件都可
以查询uvm_config_db获取此虚接口的句柄,并将其用于通过接口实际访问信号。

[317] 如何使用uvm_config_db的get()和set()方法?

get()和set()是用于从uvm_config_db存储或检索信息的主要方法。任何验证组件都可以使用set()方法为config_db存储
一些配置信息,还可以控制哪些其他组件对相同信息具有可见性。可以将其设置为具有全局可见性,或者仅对一个或多个
特定测试平台组件可见。 get()函数从数据库中检查与参数匹配的共享配置。

get()和set()方法的语法如下:

1 uvm_config_db#(<type>)::set(uvm_component context, string inst_name, string field_name,<type>


value)
2 uvm_config_db#(<type>)::get(uvm_component context, string inst_name, string field_name, ref value)

context指定从中调用get / set的当前类或组件。 inst_name是从中调用get / set的组件实例的名称。 field_name是在


config_db中设置/获取的对象或参数或变量的名称。 <type>标识config_db中设置/获取的配置信息的类型。对于对象句
柄,type是类名,而对于其他变量,type是数据类型名,代表了该变量的类型。

[318] 在验证平台层次结构中较低的组件是否可以使用get / set config方法将句柄传递给较高层


次结构中的组件?

建议不要在UVM中这么做。 通常,较高级别的组件使用句柄设置配置数据库,而较低级别的组件则使用get / set方法


获取它们。

[319] 在UVM中,将虚接口分配给不同组件的最佳方法是什么?

实例化DUT和接口的顶级testbench模块在uvm_config_db中例化虚接口。 然后,测试类或UVM组件层次结构中的任何
其他组件可以使用get()方法查询uvm_config_db,获得此虚接口的句柄并将其用于访问信号。

下面栈是了如何进行此操作。 为APB总线master实例化了DUT和物理接口,然后,将虚接口句柄设置到
uvm_config_db。

1 module test;
2 logic pclk;
3 logic [31:0] paddr;
4
5 //Instantiate an APB bus master DUT
6 apb_master apb_master(.pclk(pclk),*);
7
8 //Instantiate a physical interface for APB interface
9 apb_if apb_if(.pclk(pclk), *);
10
11 initial begin //Pass this physical interface to test class top //which will further pass it
down to env->agent->drv/sqr/mon
12 uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top", "vif",apb_if);
13 end
14
15 endmodule

下面是APB Env类,该类使用uvm_config_db中的get()方法检索在顶层测试模块中设置的虚接口。

1 class apb_env extends uvm_env;


2 `uvm_component_utils(apb_env);
3
4 //ENV class will have agent as its sub component
5 apb_agent agt;
6
7 //virtual interface for APB interface
8
9 virtual apb_if vif; //Build phase - Construct agent and get virtual interface handle fromtest
and pass it down to agent
10
11 function void build_phase(uvm_phase phase);
12 agt = apb_agent::type_id::create("agt", this);
13 if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))begin
14 `uvm_fatal("config_db_err", "No virtual interface specified forthis env instance")
15 end
16 uvm_config_db#(virtual apb_if)::set( this, "agt", "vif", vif);
17 endfunction: build_phase
18
19 endclass : apb_env

[320] 在UVM中,如何结束仿真?

UVM具有phase机制,由一组构建阶段,运行阶段和检查阶段组成。 在run()阶段进行实际的测试仿真,并且在此
phase中,每个组件都可以在开始时提出raise_objection和drop_objection。 一旦所有组件都drop_objection,则run_phase
完成,然后所有组件的check_phase执行,然后测试结束。

这是正常仿真结束的方式,但是如果某些组件由于设计或测试平台中的错误而挂起,则仿真超时也可以终止
run_phase。 当run_phase启动时,并行超时计时器也会启动。 如果在run_phase完成之前超时计时器达到指定的超时限
制,则将发出一条错误消息,然后将执行run_phase之后的所有phase,最后测试结束。

[321] 什么是UVM RAL(UVM Register Abstraction Layer)?

UVM RAL(UVM Register Abstraction Layer)是UVM所支持的功能,有助于使用抽象寄存器模型来验证设计中的寄存


器以及DUT的配置。 UVM寄存器模型提供了一种跟踪DUT寄存器内容的方法,以及一个用于访问DUT中寄存器和存储器的
层次结构。 寄存器模型反映了寄存器spec的结构,能够作为硬件和软件工程师的共同参考。 RAL还具备其他功能,包括寄
存器的前门和后门初始化以及内置的功能覆盖率支持。

[322] 什么是UVM Callback?

uvm_callback类是用于实现回调的基类,这些回调通常用于在不更改组件类的情况下修改或增强组件的行为。 通常,
组件开发人员会定义一个专用于应用程序的回调类,该类扩展并定义一个或多个虚方法,称为回调接口。 这些方法用于实
现组件类行为的重写。

一种常见用法是在driver将错误发送到DUT之前将错误注入到生成的数据包中。 以下伪代码展示了如何实现。

1. 定义一个具有错误位的数据包类
2. 定义一个从sequence中提取数据包,输入到DUT的driver类
3. 定义从基类uvm_callback派生的driver回调类,并添加一个虚方法,该方法可用于注入错误或翻转数据包中的某
个位。
4. 用`uvm_register_cb()宏注册回调类
5. 在接收和发送数据包到DUT的driver的run_phase()方法中,基于概率执行回调以导致数据包损坏

1 class Packet_c;
2 byte[4] src_addr, dst_addr;
3 byte[] data;
4 byte[4] crc;
5 endclass
6 //User defined callback class extended from base class
7 class PktDriver_Cb extends uvm_callback;
8 function new (string name = "PktDriver_Cb");
9 super.new(name);
10 endfunction
11
12 virtual task corrupt_packet (Packet_c pkt);
13 //Implement how to corrupt packet
14 //example - flip one bit of byte 0 in CRC
15 pkt.crc[0][0] = ~pkt.crc[0][0]
16 endtask
17 endclass : PktDriver_Cb
18
19 //Main Driver Class
20 class PktDriver extends uvm_component;
21 `uvm_component_utils(PktDriver)
22
23 //Register callback class with driver
24 `uvm_register_cb(PktDriver,PktDriver_Cb)
25
26 function new (string name, uvm_component parent=null);
27 super.new(name,parent);
28 endfunction
29
30 virtual task run();
31 forever begin
32 seq_item_port.get_next_item(pkt);
33 `uvm_do_callbacks(PktDriver,PktDriver_Cb, corrupt_packet(pkt))
34 //other code to derive to DUT etc
35 end
36 endtask
37 endclass

[323] 什么是uvm_root类?

uvm_root类充当所有UVM组件的隐式顶级和phase控制器。 用户不直接实例化uvm_root。 UVM会自动创建一个


uvm_root实例,用户可以通过全局(uvm_pkg-scope)变量uvm_top访问该实例。

[324] uvm_test的父级类是什么?

uvm_test类是用户可以实现的顶级类,并且没有显式父类。 但是,UVM有一个称为uvm_top的特殊组件,它被指定为
测试类的父级。

形式验证

[325] 什么是形式验证?

形式验证是使用数学建模来验证设计实现是否符合spec的方法。 形式验证使用数学推理和算法来证明设计符合spec。
在形式验证中,该工具隐式地涵盖了所有情况(输入和状态),而无需开发任何激励生成器或预期输出。 该工具需要以
property或更高级别的模型形式对spec进行形式描述,以详尽地涵盖所有输入组合,证明功能的正确性。 SystemVerilog的
property也可用于形式化描述spec。

[326] 形式验证是静态仿真还是动态仿真?

是静态仿真

[327] 形式验证有哪些方法?
1. 模型检查
2. 形式等效

[328] 解释模型检查

在“模型检查”方法中,将要验证的模型描述为从设计规范中提取的一组property。 因此要详尽搜索设计的状态空间,
检查所有property是否在所有状态下均成立。 如果在任何状态下违反了property,则会引发错误。 下图是一个示意图:

[329] 什么是形式等效

形式等效用于验证两个具有相同或不同抽象的模型在功能上是否一致的方法。 此方法无法确定模型在功能上是否正
确,但是可以确定两个模型在功能上是否相同。 常用于比较RTL设计和综合网表的功能。 它也可以用来检查两个RTL模型
或两个门级模型的一致性。 下图是一个示意图:

[330] 列出一些可以使用形式等效的场景

1. RTL设计与综合网表
2. RTL设计与参考模型
3. 两个RTL设计
4. 两个门级模型
5. 两个参考模型

[331] 与动态仿真相比,形式验证有什么优势?

1. 动态仿真不可能进完全的验证,因为输入激励是使用生成器或testcase来实现的。 但是,形式验证会覆盖所有的
状态空间,因为该工具会自动生成激励来验证所有的spec。
2. 由于工具会自动生成完备的激励,因此无需自行生成激励。 用户可以专注于使用属性来映射形式spec。
3. 无需生成预期的输出序列,并且在数学上保证了设计的正确性。

[332] 形式验证有什么局限性?

1. 可拓展性是形式验证的最大限制之一。 形式验证仅限于较小的设计,因为即使添加一个触发器也会将设计状态
空间增加2倍(这意味着每个触发器的输入场景都会加倍)。
2. 它可以确保设计相对于spec的正确性。 它不能保证设计是否正常工作(例如spec本身是否有错误)。
3. 对于模型检查,spec需要使用property来描述、编码

[333] 如果设计中的某个模块经过形式验证可以正常工作,我们是否还需要收集该模块的覆盖
率?

不,我们不需要通过了形式验证的模块的覆盖率。 因为形式验证在数学上保证了可以在所有可能的输入条件下都符合
spec。

功耗和时钟

[334] CMOS电路功耗由哪些部分组成?

1. 动态功耗:这部分是由晶体管电容充放电产生的
2. 静态功耗:这部分是由开关的漏电流产生的

[335] 什么是动态功耗?它与哪些参数有关?

动态功耗( )是容性负载功耗( )与瞬态功耗( )的总和。 动态功耗( )与 成正比。

其中,

活 性 因 子 进 行 状 态 转 换 的 开 关 比 例

内 部 电 容

负 载 电 容

电 源 电 压

工 作 频 率

[336] 什么是静态功耗?它与哪些参数有关?

静态功耗是电路中没有开关活动时消耗的功耗。 它是漏电功耗。 例如:打开电路时,由于电流流动,电池开始漏电。

其中, 是电源电压, 是流过器件的总电流。

[337] 什么是多电压域?为什么要使用它?

多电压域是一种低功耗技术,它在设计中使用多个电压域。 在此,它的目标是针对所需性能优化功耗。 电压越高,电


路速度越快(性能越高),但功耗也越高(因为动态功耗( )与 成正比)。 在某些设计中,设计的只有几个部分可
能需要以较高的频率运行,而其他部分可以较低的频率运行。 在这种情况下,给低频率部分提供低电压,从而减小功耗。

[338] 什么是“动态电压频率调节”(DVFS)?何时使用?
动态电压频率调节是一种低功耗设计技术,通过动态调整频率降低功耗。 在DVFS中,工作频率或电压以某种方式进
行调节,使得设计在正常运行的同时使用最小的频率或电压。 这个过程发生在设计的运行过程中,因此称为“动态”。 传统
的低功耗方法中,设计以工作频率运行,然后在空闲时关闭电源来进行定期调度。DVFS技术利用了CMOS芯片的特性:
CMOS芯片的能量消耗正比于电压的平方和时钟频率。DVFS技术是以延长任务执行时间为代价来达到减少系统能量消耗的
目的,体现了功耗与性能之间的权衡。可以通过减少时钟频率来降低通用处理器功耗的。然而,仅仅降低时钟频率并不节
约能量,因为性能的降低会带来任务执行时间的增加。调节电压需要以相同的比例调节频率以满足信号传播延迟要求。然
而不管是电压调节还是频率调节,都会造成系统性能的损失,并增加系统的响应延迟。

为了尽量减少可感知的系统性能负面影响同时又能最大程度地降低系统能耗,策略必须估计未来的工作负载并选择最
合适的频率。准确地预测未来的工作负载对广泛使用的策略是至关重要的。预测错误可能会导致设置的频率太高降低节省
能耗,或设置频率过低造成系统响应延迟过高。所以,要想降低功耗,需要选择合适的供电电压和时钟频率。

所以安全的调节机制是:

当需要提升功率时,应先提升供电电压,然后提升时钟频率。
当需要降低功率时,应先降低时钟频率,再降低供电电压。

制定调整策略前,需要先找出系统中的耗电大的部件,如CPU、GPU、DSP等硬件算法加速模块(结合逻辑规模);
然后统计出这些模块的负载情况,基本的策略当然是工作负载增加则升频升压,工作负载降低则降频降压。工作负载的粗
略模型是在一个时间窗口内,统计模块工作的时间长度,设定不同阈值,高阈值对应高电压高频率,低阈值对应低电压低
频率。每次统计值穿过阈值边界,触发DVFS转换。

[339] 什么是UPF?

其主要是由Synopsys推出的专门用于描述电路电源功耗意图的一种语言标准,它是Tcl语言的扩展,并且现在已经成为
IEEE-1801标准且被三大EDA厂商(Synopsys、Cadence、Mentor)支持。 传统的数字芯片设计均是采用Verilog或者VHDL
语言对电路进行描述,但是这种方式描述出的电路并没有包含任何的芯片的供电网络信息,这会导致后续的流程如功耗验
证和后端实现很难处理或者极易出错。UPF标准正好可以很好的解决这个问题,因为UPF标准本身包含了大量的用于描述电
源网络的Tcl命令,直接使用这些命令可以很方便的创建电源域和功耗控制的特殊单元等,用UPF编写的统一功耗格式文件
不仅可以在RTL级,同时还可以被后端工具使用,这在一定意义上保证了整个芯片设计过程中功耗流程的一致性,在后端工
具进行处理之后也会生成相应的UPF文件,此时前端工具可以使用该UPF文件对网表进行Power仿真分析

[340] 什么是Power Aware Simulation,它的重要性体现在什么地方?

Power Aware Simulation意味着在RTL或GLS级别上对Power Down和Power Up行为进行建模。Power Aware Simulation


的重要性体现在:

1. 在设计周期的早期就必须找到与电源相关的RTL / Spec的错误。 与电源有关的严重错误可能导致芯片无法正常


工作。
2. 电源管理至关重要,现代的ASIC / SoC设计都具有为电源管理而专门的重要逻辑。##

[341] 电源域的意思是?

电源域是共享一个主要电源的设计元素的集合,根据通用电源策略(例如工作电压,电源网络,上电/断电条件等)进
行分组。

[342] 什么是亚稳态?亚稳态是如何产生的?它有什么影响?

亚稳态是一种电路状态,在电路正常工作所需的时间内,电路无法稳定在的“ 0”或“ 1”逻辑电平的状态。 通常在建立时


间和保持时间违例时发生。

亚稳态可能会导致:

1. 不可预测的系统行为。
2. 不同的扇出可能得到不同的信号值,从而导致设计进入未知状态。
3. 如果不稳定的数据(“ 0”或“ 1”)传播到设计中的不同部分,则可能导致高电流并最终芯片烧坏。

[343] 如何避免亚稳态?

通过在设计中使用同步器,可以避免跨时钟域时的亚稳态。同步器让信号有足够的时间从不稳定的振荡(“ 0”和“ 1”)


稳定下来,从而获得稳定的输出。对于跨时钟域时可能出现的亚稳态,还可以使用包括握手机制、异步FIFO等方法。对于
同步电路中,要进行合理的设计与设计约束,避免建立时间和保持时间违例。

[344] 同步器的构成是怎样的?
以下是一个同步器电路的例子。 这是一个两个触发器同步器,第一个触发器等待一个时钟周期,使输入端的亚稳态稳
定下来/逐渐消失,然后第二个触发器在输出端提供稳定的信号。

值得注意的是,在信号输入第二级时,第一触发器的输出仍然可能不稳定(并导致第二级输出信号变为亚稳态)。 在
这种情况下,我们可以使用三个触发器同步器电路。 但是,通常两个触发器同步器电路足以消除亚稳态,使用三个触发器
的情况比较少。

[345] 什么是时钟门控?

时钟门控是一种低功耗技术,通过关闭了设计中某些部分的时钟达到降低功耗的目的。 它是一种被用于控制时钟网络
耗散功率的技术,通过避免不必要的开关活动,减少设计消耗的动态功耗。

[346] 什么是电源门控,为什么要使用它?

电源门控是一种低功耗,可以关闭设计中不工作的部分。 当不工作时,电源门控可关闭电源,减少漏电功耗,从而降
低了功耗。 时钟门控有助于降低动态功耗,而时钟门控有助于降低静态功耗。

[347] 多时钟域设计会遇到哪些问题?

亚稳性导致的同步失败:时钟在不同的时钟域中以不同的频率运行,并且在一个时钟域中生成的信号在非常接
近第二个时钟域中时钟有效沿的位置采样时,输出可能进入亚稳态状态,在设计中出现同步失败。
数据不一致:如果设计不合理,目标时钟域可能会接收错误的数据。 例如:如果多个信号从一个时钟域传输到
另一个时钟域,所有这些信号同时变化,并且源和目标时钟沿彼此接近,那么这些信号中的某些可能会在一个
时钟中捕获,而有一些信号可能在另一个时钟周期中被捕获,从而导致数据不一致。 注意:这只是数据不一致
的一个例子, 数据不一致的产生还有很多原因。
数据丢失:如果设计不合理,则数据可能会在CDC边界丢失。 例如:如果信号从较快的时钟域送到较慢的时钟
域,并且该信号的宽度仅等于一个时钟周期(较快的时钟),则可能会丢失以下信息: 信号在较慢的时钟域中
的采样沿前就变化了。 注意:这只是数据丢失的一个例子, 数据丢失的产生还有很多原因。

[348] 如何处理跨时钟域信号?

跨时钟域处理有很多方法,具体取决于我们需要在不同的时钟域之间传递1位还是多位。 假设以下情况:多个信号从
一个时钟域传输到另一时钟域,所有信号同时变化,并且源和目标活动时钟沿彼此接近。 在这种情况下,这些信号中的某
些信号可能在目标时钟域的一个时钟周期中被捕获,而另一些信号在目标时钟域中的另一个时钟周期中被捕获,从而导致
数据不一致性。 可以使用下面方法在两个时钟域之间同步信号。

对于单bit跨时钟域:

两级或者三级同步器
使用握手信号进行同步

对于多bit跨时钟域:

使用多周期路径的方法进行同步,将未经同步的信号和同步控制信号一起发射到目标时钟域
对信号进行格雷码编码,由于相邻的格雷码计数只会变化1bit,亚稳态的发生会大大减小
使用异步FIFO
将多比特信号合并成1bit,然后再通过多级同步器进行传输

[349] 举例信号从快时钟域到慢时钟域可能发生的问题

信号只持续一个时钟周期(快时钟域),可能导致慢时钟域漏采样。
[350] 异步复位的优缺点有哪些?

优点:

异步复位具有最高优先级。
保证数据路径干净。
在有或没有时钟信号的情况下都能生效。

缺点:

如果在时钟的有效沿(或附近)撤销异步复位,则触发器的输出可能进入亚稳态。
它对毛刺很敏感,可能导致虚假的复位。

[351] 同步复位的优缺点有哪些?

优点:

整个电路都是同步的
更加容易仿真
综合以后可能会更加节省面积

缺点

需要脉冲扩展,让复位脉冲足够长,保证能够正确地被采样
会添加额外的组合逻辑
同步复位需要时钟才能复位。 如果电路具有内部三态总线,则需要单独的异步复位,以防止内部三态总线上的
总线冲突。

[352] 什么是Reset Recovery Time? 它和复位有什么关系?

复位恢复时间(Reset Recovery Time)是复位解除和时钟信号有效沿之间的时间。 如果发生复位解除,并且在非常小的


时间窗口内,如果时钟信号边沿来临,则可能导致亚稳态。 这是因为复位解除置位后所有信号将不满足下一个触发器输入
的时序条件。

[353] 什么是频率合成器? 举一个频率合成器的例子?

频率合成器是一种可以从单个稳定参考频率生成新频率的电路。 例如:为了从参考100 MHz时钟信号生成200MHz时


钟信号,PLL通常用作频率合成器。

[354] 什么是PLL?

PLL全称是“Phase Locked Loop,锁相环”。 简而言之,它是一种反馈电路(准确地说是控制系统),用于生成输出信


号,该输出信号的相位与输入信号的相位有关。 它用于相位/频率调制和解调,还可以用作频率合成器。 PLL由三个功能块
组成:

鉴相器
环路滤波器
压控振荡器

[355] 画出PLL的框图

这里 是参考频率, 是输出频率,这样 ,这意味着


覆盖率

[356] 代码覆盖率与功能覆盖率的区别是什么?

代码覆盖率:代码覆盖率是一种度量,用于度量给定测试case对设计代码(HDL模型)进行测试的程度。 启用
后,模仿真器会自动提取代码覆盖率。
功能覆盖率:功能覆盖率是用户定义的度量标准,用于度量已执行了多少spec(如测试计划中的功能所列举
的)。 它可以用来衡量对于spec的测试充分性。 它是用户定义的,不会自动生成。 它也不依赖于设计代码,因
为它是根据spec实现的

[357] 代码覆盖率有哪几种?

Statement/Line coverage:用于衡量在仿真测试期间测试了多少条语句(行)。 一般行覆盖率的目标是100%。


在下面的代码中,有4行或语句将在Statement/Line coverage中进行收集。

1 always @ (posedge clk) begin


2 if( A > B) begin //Line 1
3 Result = A - B; //Line 2
4 end else begin //Line 3
5 Result = A + B; //Line 4
6 end
7 end

Block coverage:在begin-end或if else或case语句之间或while循环或for循环之间的一组语句称为块。 块覆盖率


衡量的是在仿真过程中是否覆盖了这些类型的块码。 块覆盖范围看起来类似于语句覆盖范围,不同之处在于块
覆盖率包含了一组语句。 在下面的的示例代码中,有三个代码块

1 always @ (posedge clk) begin //always block


2 if( A > B) begin // if block
3 Result = A - B;
4 end else begin // else block
5 Result = A + B;
6 end
7 end

Branch/Decision coverage:分支覆盖率评估HDL代码中的条件,例如if-else,case语句和三元运算符(?:)
语句,并检测是否同时包含真假情况。 在上面的示例中,只有一个分支(if A> B),分支覆盖率会检查是否真
假两个分支都被触发了。
Conditional Coverage and Expression coverage:条件覆盖率会检查HDL中的所有布尔表达式,并计算该表达式
为真或假的次数。 表达式覆盖率检查语句的右侧,统计所有可能组成的真值表的覆盖程度。 以下是包含3个布
尔变量的表达式,它们决定了Result变量为true或false

1 Result = (A && B) || (C)

针对A,B和C的所有可能情况,如下创建真值表。 条件覆盖率可以衡量此真值表的所有行是否都被覆盖。
Toggle coverage:翻转覆盖率可衡量仿真运行期间设计中信号和端口的翻转率。 这有助于识别哪些信号一直没
有翻转。
FSM coverage:状态机覆盖衡量仿真期间是否所有状态机的状态都被覆盖到。

[358] 如果功能覆盖率接近100%而代码覆盖率不足60%,说明了什么?

仿真器会基于testcase提取代码覆盖率,而功能覆盖率则是用户定义的指标。低代码覆盖率表明并非设计代码的所有部
分都经过了测试。高功能覆盖率表明,用户从测试计划中捕获的所有功能都得到了测试。如果覆盖率指标显示低代码覆盖
率和高功能覆盖率,原因可能是:

1. 可能有许多设计代码未按照spec用于实现的功能。 (无效的代码)
2. 用户定义的功能覆盖率量中存在一些错误。测试计划未捕获所有设计功能/场景/边界,或者缺少功能覆盖率监视
器。代码覆盖率中未覆盖的设计代码可能会映射到这些功能上。
3. 在实现功能覆盖率监视器时可能存在潜在的错误,导致它们收集了错误的覆盖率。因此,在验证项目中,对用
户定义的功能覆盖率指标及其实现进行适当的检查很重要。

[359] 如果代码覆盖率接近100%而功能覆盖率不足60%,说明了什么?

1. 没有按照spec在设计中实现了所有功能。 因此,设计代码无法实现所有功能
2. 在功能覆盖率监视器中可能存在潜在的错误,即使设计代码实现了功能,也无法覆盖它们。
3. 功能正确,但是由于发送的激励不正确,对应的功能覆盖率无法收集。

[360] 覆盖组可以在类内部定义和使用吗??

是的,可以在类内部定义覆盖组。 这对于基于测试平台结构(例如事务,序列,检查器,监视器等)实现功能覆盖率
非常有用。

[361] 什么是covergroups和bins?

覆盖点(coverpoint)是用于指定需要收集覆盖率的目标。 Covergroup可以具有多个覆盖点以覆盖不同的表达式或变
量。 每个覆盖点还包括一组bin,这些bin是该覆盖点不同采样值。 bin可以由用户定义,也可以缺省自动创建。 在下面的
示例中,有两个变量a和b,covergroup有两个coverpoint,他们会检查a和b的值。 Coverpoint cp_a是用户定义的,bins
values_a检测a是否覆盖到特定的值。 Coverpoint cp_b是自动的,bin是自动生成的,会检测b是否覆盖到所有的可能性

1 bit [2:0] a;
2 bit [3:0] b;
3
4 covergroup cg @(posedge clk);
5 cp_a coverpoint a {
6 bins values_a = { [0,1,3,5,7 };
7 }
8 cp_b coverpoint b;
9 endgroup

[362] 下面的例子中coverpiont cp_a创建了多少个bin?

1 bit[3:0] var_a;
2
3 covergroup test_cg @(posedge clk);
4 cp_a : coverpoint var_a {
5 bins low_bins[] = {[0:3]};
6 bins med_bins = {[4:12]};
7 }
8 endgroup

lowbins[]创建了四个bin,对应检查是否覆盖到0,1,2,3,med_bins创建里一个仓,检查是否覆盖到4-12之间的
值。因此,一共创建了5个仓

[363] ignore bins 和 illegal bins的区别是什么?

ignore_bins用于指定与覆盖点关联的一组值或者翻转行为,这些值或者翻转行为可以明确从覆盖范围中排除。 例如,
以下将忽略变量a的所有采样值7和8。
1 coverpoint a {
2 ignore_bins ignore_vals = {7,8};
3 }

illegal_bins用于指定与覆盖点关联的一组值或者翻转行为,这些值或者翻转行为被标记为非法。 例如,以下会将所有
1、2、3采样值标记为非法。

1 covergroup cg3;
2 coverpoint b {
3 illegal_bins bad_vals = {1,2,3};
4 }
5 endgroup

当采样到illegal_bins时,仿真会报错,并且illegal_bins的优先级高于其他bin,即使其他bin和illegal_bins的范围有重
叠,也会导致报错。

[364] 如何编译一个coverpoint来覆盖一个翻转行为?

翻转覆盖率指定为“ value1 => value2”,其中value1和value2是在两个连续采样点上的表达式的采样值。 例如,在


coverpoint之下,在clk的三个连续正边缘中寻找变量v_a的值4、5和6的翻转行为。

1 covergroup cg @(posedge clk);


2 coverpoint v_a {
3 bins sa = (4 => 5 => 6),
4 }
5 endgroup

[365] 下面的语句覆盖了什么样的翻转行为?

1 coverpoint my_variable {
2 bins trans_bin[] = ( a,b,c => x, y);
3 }

a=>x, a=>y, b=>x, b=>y, c=>x, c=>y

[366] 下面的bin覆盖了哪些范围?

1 covergroup test_cg @(posedge clk);


2 coverpoint var_a {
3 bin hit_bin = { 3[*4]};
4 }
5 endgroup

[* N]指的是连续的重复操作。 因此,上面的bin覆盖的是连续4次采样都是3的翻转覆盖率

[367] 什么是wildcard bins?

wildcard bins可以让bin在定义时使用x、z和?作为0或者1的通配符。下面的例子中,并不关心低两位是多少,只要高
两位为11就在覆盖范围内。

1 coverpoint a[3:0] {
2 wildcard bins bin_12_to_15 = { 4'b11?? };
3 }

[368] 什么是cross coverage?何时使用它?


coverage可以指定两个或多个coverpoint或变量之间的cross coverage。 cross coverage使用cross进行指定的。交叉覆
盖率的仓数,等于交叉目标仓数的乘积,因为要覆盖到两者的所有可能组合。

1 bit [31:0] a_var;


2 bit [3:0] b_var;
3 covergroup cov3 @(posedge clk);
4 cp_a: coverpoint a_var {
5 bins yy[] = { [0:9] };
6 }
7 cp_b: coverpoint b_var;
8 cc_a_b : cross cp_b, cp_a;
9 endgroup

cp_a有10个bin,cp_b有16个bin,因此cc_a_b有160个bin。

交叉覆盖率通常用于不同功能或者事件同时发生的情况,去验证这些事件是否同时发生了。

[369] 下面的交叉覆盖率有多少个bin?

1 bit[1:0] cmd;
2 bit[3:0] sub_cmd;
3
4 covergroup abc_cg @(posedge clk);
5 a_cp: coverpoint cmd;
6 cmd_x_sub: cross cmd, sub_cmd;
7 endgroup

cmd和sub_cmd都是二值变量,a_cp有4个bin,sub_cmd默认有16个bin,因此,交叉覆盖率具有64个bin。

[370] 下面的覆盖率代码有什么错误?

1 int var_a;
2 covergroup test_cg @(posedge clk);
3 cp_a: coverpoint var_a {
4 bins low = {0,1};
5 bins other[] = default;
6 }
7 endgroup

代码对int类型进行覆盖率收集,low的bin数为2,而通过default所创建的数量为 个,数量十分巨大,这会导致
仿真器崩溃或者仿真速度下降。应该尽量避免使用default或者不要使用default。

[371] covergroup有几种采样方式?

有两种采样方式

定义covergroup时指定时钟事件,下面是一个例子

1 covergroup transaction_cg @(posedge clk)


2 coverpoint req_type;
3 endgroup

显式地调用sample()方法

1 class packet;
2 byte[4] dest_addr;
3 byte[4] src_addr;
4
5 covergroup pkt_cg;
6 coverpoint dest_addr;
7 endgroup
8
9 function new();
10 pkt_cg =new();
11 endfunction;
12 endclass
13
14 module test;
15 initial begin
16 packet pkt =new();
17 pkt.pkt_cg.sample();
18 end
19 endmodule

[372] 如何给covergroup传递参数,何时用它?

像定义方法一样,covergroup也可以通过类似的语法进行参数传递,主要使用ref,以便随时检测信号的变化。当我们
要对多个信号进行相同类型的覆盖率组定义时,我们可以通过定义参数传递的方法改变采样的信号,而覆盖率的定义只需
要进行一次即可。下面是一个例子。

1 module test;
2 covergroup xy_cg ( ref int x , ref int y, input string name);
3 cp_x: coverpoint x;
4 cp_y: coverpoint y;
5 cc_x_y: cross cp_x, cp_y;
6 endgroup
7
8 initial begin
9 xy_cg xy_cg_mod1 = new( top.mod1.x, top.mod1.y, "mod1_cvg");
10 xy_cg xy_cg_mod2 = new( top.mod2.x, top.mod2.y, "mod2_cvg");
11 end
12
13 endmodule

[373] covergroup可以引用DUT中的层次信号吗?

可以

[374] 能够对不同covergroup的coverpoint进行交叉覆盖率定义吗?

不可以,只能对当前covergroup的coverpoint定义交叉覆盖率

[375] per_instance和per_type的区别是什么?如何使用覆盖率选项控制它们?

Covergroup可以定义和实例化多次。 如果一个covergroup有多个实例,则默认情况下,SystemVerilog的coverage报
告是所有实例的累计coverage。 这就是默认的per_type。 但是,可以在covergroup中设置一个per_instance选项,那么
SystemVerilog将分别报告该Covergroup的每个实例的coverage。

1 covergroup test_cg @(posedge clk)


2 option.per_instance =1;
3 coverpoint var_a;
4 //and other coverpoints
5 endgroup

断言

[376] 什么是断言?在验证中使用断言的好处是什么?

断言是根据规范对设计属性的property,用于验证设计的行为。 如果在仿真中检查的property未按照规范运行,则断
言失败。 类似地,如果禁止在设计中发生的行为,而在仿真期间发生了,则断言也会失败。

使用断言的好处有:

断言在错误发生是会立刻捕获,改善了检测错误的能力
断言在设计中能够提供更好的可观察性,因此有助于更轻松地调试
断言既可以用于动态仿真,也可以用于设计的形式验证
断言还可以用于提供对输入激励的功能覆盖,并确保设计属性确实进行了验证。

[377] 有多少种断言?

systemvrilog中有两种断言,立即断言和并发断言

[378] 立即断言和并发断言的区别是什么?

立即断言使用表达式进行评估,并且像在过程块中的语句一样执行。,执行后会立即进行评估。 立即断言仅用于动态
仿真中。 以下是一个简单的立即断言的示例,该断言检查“a和b是否始终相等”:

1 always_comb begin a_eq_b:


2 assert (a==b) else $error ("A not equal b");
3 end

并发断言根据所涉及变量的采样值在时钟沿评估测试表达式。 它们与其他设计模块同时执行。 它们可以放置在模块或


接口中。 并发断言可以与动态仿真以及静态(形式)验证一起使用。 以下是一个并发断言的简单示例,该断言检查“如果c
在一个时钟周期内为高,则在下一个周期,a和b的值相等”:

1 ap_a_eq_b : assert property((@posedge clk) c |=> (a == b));

[379] 简单立即断言和延迟立即断言之间有什么区别?

延迟断言是立即断言的一种特殊类型。 简单立即断言立即求值,而无需等待其组合表达式中的变量稳定下来。 因此,


当组合表达式逐渐趋于稳定时,简单立即断言很容易出现小故障。 这可能导致断言多次触发,其中一些断言可能是错误
的。 为了避免这种情况,定义了延迟断言,仅在时间戳结束时,组合表达式中的变量稳定下来后,才评估这些断言。 这意
味着将在时间戳的reactive区域中对它们进行评估。

[380] 与使用过程式SystemVerilog代码编写检查程序相比,使用SVA(SystemVerilog断言)编
写checker有什么优势?

最好使用SVA而非程序代码编写某些类型的checker。 SVA具备sequence和property规范的丰富构造,这比使用过程代
码或编写基于类的检查器更容易。 另一个额外的好处是,相同的断言也可以在静态检查工具(如形式验证工具)中使用,
也可以用于提供功能覆盖率。

下面是一些推荐使用SVA的例子:

检查内部设计结构,例如FIFO的上溢或下溢。
使用设计中的嵌入式断言可以更轻松地检查模块之间的内部信号和接口
使用时间表达式也可以轻松开发标准接口协议(如PCIE,AMBA,以太网等)的checker。
仲裁,资源匮乏,协议死锁等检查是很多设计中形式验证的检查内容,因此编写断言将有助于它们在静态和动
态仿真中同时使用。

[381] 有几种方式为一个设计编写断言?

可以直接在module内部编写断言。 如果断言是由设计工程师为设计中的某些内部信号或接口编写的,则通常会
采用这种方式
断言也可以编写在单独的interface或module或program中,然后可以绑定到特定的module或实例,在断言中引
用来自该特定module或实例的信号。 这是使用SystemVerilog中的bind构造完成的。 如果断言是由验证工程师
编写的,会采用这种方式。

[382] SVA中的sequence的作用是什么?

sequence是编写property或断言的基本构建块。 sequence可以认为是在单个时钟边沿求值的简单布尔表达式,也可以
是在多个周期内求值的事件sequence。 property可能涉及检查在不同时间开始的一个或多个sequence行为。 因此,可以
使用逻辑或sequence组合的多个sequence来构造property。

sequence的基本语法是:
1 sequence name_of_sequence;
2 <boolean expression >
3 endsequence

例如,下面的sequence在检查每个周期的上升沿是否a和b始终相等

1 sequence s_a_eq_b;
2 @posedge(clk) (a ==b);
3 endsequence

[383] $rose(tst_signal)和@posedge(tst_signal)有什么区别?

@posedge(tst_signal)会在每个上升沿进行采样,检查tst_signal是否为1。 但是,$rose()是一个系统任务,它检查信号
的采样值在先前采样和当前采样之间(先前采样可能是0 / x / z)是否变为1。 因此,$rose()需要两个采样值进行判断,从
原来的非1变成1。 例如:在下面的sequence中,仅当信号“a”在时钟的两个正沿之间从0/x/ z的值变为1时,$rose(a)的计算
结果为true

1 sequence S1;
2 @(posedge clk) $rose(a)
3 endsequence

[384] 下面的sequence在以下那种情况下会捕获成功?

1 sequence S1;
2 @(posedge clk) $rose(a);
3 endsequence

1. 当信号a”从0变为1时。
2. 当信号“a”在clk的一个上升沿采样的值为“0”,而在下一个上升沿采样的值变为“1”。
3. 当信号“a”在clk的一个上升沿采样的值为“1”,而在下一个上升沿采样的值变为“0”。

第二种情况下会捕获成功,$rose()检查的是两次采样,第一次非1,第二次为1,@(posedge clk)表示在上升沿时采
样。

[385] sequence可以在下面那些地方定义?

1. Module
2. Interface
3. Program
4. Clocking Block
5. Package

以上所有地方都可以

[386] 在类中是否可以实现并发断言?

不能,并发断言不能再类中实现

[387] 下面的sequence会匹配事件?

1 req ##2 gnt ##1 !req

当gnt信号在req信号为高电平后的两个周期变为高电平,然后一个周期后req信号被置为零时,该sequence的值为
真。

[388] 什么是序列重复运算符? 有哪三种?


如果需要对一个sequence表达式进行一次以上的迭代求值,则可以使用重复运算符来构造一个更长的sequence,而不
是编写一个长sequence。 SVA支持三种类型的重复运算符:

连续重复([*const_or_range_expression]):如果sequence从一个迭代结束起以一个时钟单位进行有限次数的迭
代,可以使用连续重复运算符。在下面的例子中,描述的是如果a为高,一个周期后,b连续五个周期为高的事
件。

1 a ##1 b [*5]

跟随重复([->const_or_range_expression]):和连续重复类似,不一样的是,并不要求重复是连续的,间断的也
可以,并且要求在最后一次匹配时,立刻匹配后续表达式。下面的例子中,描述的是如果a为高,一个周期后b
连续至少2个至多10个周期为高,然后一个周期后c为高的事件

1 a ##1 b [->2:10] ##1 c

非连续重复([=const_or_range_expression] ):和跟随重复类似,但是重复匹配后,并不要求后续立刻跟上,只要
在之后能够匹配到即可。例如下面的例子中,可以在c有效匹配前一个时钟,并不需要b进行有效匹配。

1 a ##1 b [=2:10] ##1 c

[389] 下面的断言有什么错误?

1 module test (input clk, input a, input b);


2 assert_1: assert ( a && b);
3 endmodule;

立即断言只能在程序块中使用

[390] 写一个断言,检查信号最少2个最多6个周期内为高电平

1 property a_min_2_max_6:
2 @(posedge clk) $rose(a) |-> a[*2:6] ##1 (a==0)
3 endproperty
4
5 assert property (a_min_2_max_6);

[391] 什么是蕴含操作符?

蕴含操作符用于指定检查的先决条件,描述当先决条件发生后,再继续匹配。蕴含操作符只能再property这一级别使
用。蕴含操作符有两种

交叠蕴含操作符

1 assert property prop_name ( sequence_expr |-> property_expr )

非交叠蕴含操作符

1 assert property prop_name ( sequence_expr |=> property_expr )

当蕴含操作符左侧发生后,才会匹配右侧

[392]交叠蕴含操作符和非交叠蕴含操作符有什么不同?

交叠蕴含操作符,先决条件的起点和后继事件的起点是同一个时刻。例如下面的例子中,再时钟上升沿是,a要
为1,同时开始检查b是否为1,如果不是那么匹配不成功,不会进行后续的评估
1 assert property abc_overlap (@posedge clk (a==1) |-> b ##1 c )

非交叠运算符,先决条件的终点是后继事件的起点,在下一个周期才会匹配后续。例如下面的例子中,时钟上
升沿a为1,下个周期要匹配b为1,而不是同一时刻进行匹配。

1 assert property abc_overlap (@posedge clk (a==1) |=> b ##1 c )

[393] 蕴含操作符可以在sequence中使用吗?

不能,只能在property中使用

[394] 下面的两个断言是等效的吗?

1 1) @(posedge clk) req |=> ##2 $rose(ack);


2 2) @(posedge clk) req |-> ##3 $rose(ack);

是的,参考[392]

[395] SVA中允许嵌套蕴含吗?

允许,下面就是一个例子

1 a |=> b |=> c

匹配的就是a为高,下个周期b为高,再下个周期c为高

[396] 系统函数$past()的作用是什么?

这个系统函数能够从之前的时钟周期中获得信号

[397] 写一个断言,检查一个信号永远不会变成X

使用系统函数$isunknown(signal)可以进行此项检查。

1 assert property (@(posedge clk) ! $isunknown(mysignal));

[398] 写一个断言,检查一个变量保持独热码状态

使用系统函数$isonehot()或者$countones()可以进行此项检查

1 assert property (@(posedge clk) $isonehot(state));


2 assert property (@(posedge clk) $countones(state)==1);

[399] 写一个断言,检查主设备是否在发出有效请求后就在2到5个时钟周期内提供授权

1 property p_req_grant; @(posedge clk) $rose (req) |-> ##[2:5] $rose (gnt); endproperty

[400] 如何再复位期间禁止进行断言检查?

使用“disable iff”可以实现
1 assert property (@(posedge clk) disable iff (reset) a |=> b);

[401] systemverilog中的bind构造是什么意思?

SystemVerilog中的bind构造用于将checker于模块、模块的示例或者一个模块的多个示例进行绑定。通过绑定可以分
离断言和设计diamagnetic。下面是示例语法

1 bind <target module/instance> <module/interface to be instantiated> <instance name with port map>

当我们有下面的一个接口,并且内嵌断言

1 interface range (input clk, enable, int minval, expr);


2
3 property crange_en;
4 @(posedge clk) enable |-> (minval <= expr);
5 endproperty
6
7 range_chk: assert property (crange_en);
8
9 endinterface

绑定到模块

1 bind cr_unit range r1(c_clk,c_en,v_low,(in1&&in2));

绑定到模块的某个实例

1 bind cr_unit:cr_unit_1 range r1(c_clk,c_en,v_low,(in1&&in2));

[402] 如何关掉所有的断言?

使用$assertoff()系统函数可以实现,缺省情况下会关掉所有断言。也可以指定关闭哪些断言。

1 $assertoff[(levels[, list])]

第一个参数指定关闭哪个层次的断言,第二个参数指定关闭具体哪些property

[403] 有哪些方式为property指定时钟?

sequence中显式使用时钟,继承到property中

1 sequence seq1;
2 @(posedge clk) a ##1 b;
3 endsequence
4
5 property prop1;
6 not seq1;
7 endproperty
8
9 assert property (prop1);

property中显示使用时钟
1 property prop1;
2 @(posedge clk) not (a ##1 b);
3 endproperty
4
5 assert property (prop1);

从过程块中推断当前使用的时钟

1 always @(posedge clk) assert property (not (a ##1 b));

使用当前时钟块的时钟

1 clocking master_clk @(posedge clk);


2 property prop1;
3 not (a ##1 b);
4 endproperty
5 endclocking
6
7 assert property (master_clk.prop1);

直接定义默认时钟,在没有上述方法指定时钟的条件下会使用默认时钟。

1 default clocking master_clk ; // master clock as defined above


2
3 property p4;
4 not (a ##1 b);
5 endproperty
6
7 assert property (p4);

[404] 对于深度= 32的同步FIFO,为以下情况编写断言。 假设具有时钟信号(clk),写入和读


取使能信号,满标志和计数器信号。 1)如果计数器> 31,则设置FIFO已满标志。 2)如果计数器
为31,并且在没有同时读取的情况下发生了新的写操作,则FIFO满标志将置1。

1 assert property (@(posedge clk) disable iff (!rst_n) (wordcnt>31 |-> fifo_full));
2 assert property (@(posedge clk) disable iff (!rst_n) (wordcnt==31 && write_en && !read_en |=>
fifo_full));

注意第二条,使用的是非交叠蕴含操作符,满标志位要在下个周期拉高。

You might also like