数字电路基础知识
状态机
1. 翻译:读入电路的RTL级描述,并将语言描述翻译成相应的功能块以及功能块之间的拓扑结构。这一过程的结果是在综合器内部生成电路的布尔函数表达式,不做任何逻辑重组和优化。
2. 优化:根据所施加的时序和面积约束,按照一定的算法对翻译结果进行逻辑重组和优化。
3. 映射:根据所施加的时序和面积约束,从目标工艺库中搜索符合条件的单元来构成实际电路的逻辑网表。
第一 什么是流水线
流水线设计就是将组合逻辑系统地分割,并在各个部分(分级)之间插入寄存器,并暂存中间数据的方法。目的是将一个大操作分解成若干的小操作,每一步小操作的时间较小,所以能提高频率,各小操作能并行执行,所以能提高数据吞吐率(提高处理速度)。
第二 什么时候用流水线设计
使用流水线一般是时序比较紧张,对电路工作频率较高的时候。典型情况如下:
1)功能模块之间的流水线,用乒乓 buffer 来交互数据。代价是增加了 memory 的数量,但是和获得的巨大性能提升相比,可以忽略不计。
2) I/O 瓶颈,比如某个运算需要输入 8 个数据,而 memroy 只能同时提供 2 个数据,如果通过适当划分运算步骤,使用流水线反而会减少面积。
3)片内 sram 的读操作,因为 sram 的读操作本身就是两极流水线,除非下一步操作依赖读结果,否则使用流水线是自然而然的事情。
4)组合逻辑太长,比如(a+b)*c,那么在加法和乘法之间插入寄存器是比较稳妥的做法。
第三 使用流水线的优缺点
1)优点: 流水线缩短了在一个时钟周期内给的那个信号必须通过的通路长度,增加了数据吞吐量,从而可以提高时钟频率,但也导致了数据的延时。举例如下:
例如:一个 2 级组合逻辑,假定每级延迟相同为 Tpd,
1.无流水线的总延迟就是 2Tpd,可以在一个时钟周期完成,但是时钟周期受限制在 2Tpd;
2.流水线:每一级加入寄存器(延迟为 Tco)后,单级的延迟为 Tpd+Tco,每级消耗一个时钟周期,流水线需要 2 个时钟周期来获得第一个计算结果,称 为首次延迟,它要 2*( Tpd+Tco),但是执行重复操作时,只要一个时钟周期来获得最后的计算结果,称为吞吐延迟( Tpd+Tco)。可见只要 Tco 小于 Tpd,流水线就可以提高速度。 特别需要说明的是,流水线并不减小单次操作的时间,减小的是整个数据的操作时间,请大家认真体会。
2) 缺点: 功耗增加,面积增加,硬件复杂度增加,特别对于复杂逻辑如 cpu 的流水线而言,流水越深,发生
需要 hold 流水线或 reset 流水线的情况时,时间损失越大。 所以使用流水线并非有利无害,大家需权衡考虑。
(1)所有综合工具都支持的结构:always,assign,begin,end,case,wire,tri,aupply0,supply1,reg,integer,default,for,function,and,nand,or,nor,xor,xnor,buf,not,bufif0,bufif1,notif0,notif1,if,inout,input,instantitation,module,negedge,posedge,operators,output,parameter。
(2)所有综合工具都不支持的结构:time,defparam,$finish,fork,join,initial,delays,UDP,wait。
(3)有些工具支持有些工具不支持的结构:casex,casez,wand,triand,wor,trior,real,disable,forever,arrays,memories,repeat,task,while。
OCV(on-chip variation)是指在同一个芯片上, 由于制造工艺和环境等原因导致芯片上各部分特征不能完全一样,从而造成偏差,对时序分析造成影响。这些偏差对互联线和cell的延时都是有影响的。
在设计中引入OCV的目的在于从设计角度考虑芯片在实际生产中可能出现的各种差异(variation),从而适度增加设计余量(margin),减少不必要的设计悲观量(pessimism)。
https://zhuanlan.zhihu.com/p/40293737
芯片设计流程
制定规格-代码编写-仿真验证-逻辑综合-形式验证-sta-dft实现-布局布线-后仿
vcs verdi-dc-formatlity-pt-icc
STA 四种时序路径:
路径1:输入端口到寄存器的数据输入 D,
路径2:内部寄存器的时钟输入端到下一个寄存器的数据输入 D,
路径3:内部寄存器的时钟输入端到输出端口;
路径4:输入端口到输出端口。
每条时序路径包含 1 个起点和 1 个终点,
起点只能是设计的基本输入端口或内部寄存器的时钟输入端,
终点只能是内部寄存器的数据输入端或设计的基本输出端口。
https://blog.csdn.net/zgezi/article/details/108286253
低功耗设计方法
动态功耗 静态功耗
静态功耗
多阈值工艺(Multi-Vt Design)方法 电源门控(Power Gating)方法
动态功耗
多电压域 门控时钟 毛刺消除 逻辑优化
verilog延迟
https://blog.csdn.net/weixin_42369496/article/details/93066397
亚稳态
建立时间Tsu:时钟有效沿到来之前数据必须保持稳定的最小时间;
保持时间Th:时钟有效沿到来之后数据必须保持稳定的最小时间;
setup time 和 hold time 的和必须为正
1 | https://www.runoob.com/w3cnote/verilog2-setup-hold-time.html |
建立时间违例
降低时钟频率 时钟路径插入buffer 优化组合逻辑,采用流水线, 提高工作电压
保持时间违例
优化时钟路径,让时钟更早到达 数据通路上插入buffer 更换更大器件延迟的触发器
恢复时间(Recovery time):与同步电路中的建立时间类似,是指异步控制信号(如寄存器的异步清除和置位控制信号)在“下个时钟沿”来临之前变无效的最小时间长度。这个时间的意义是,异步控制信号在时钟上升沿来临Trecovery时间就要保持稳定,如果保证不了这个最小恢复时间,也就是说“下个时钟沿”来临时,这个异步控制信号不能保证正常执行。
去除时间(Removal time):与同步电路中的保持时间类似,是指异步控制信号(如寄存器的异步清除和置位控制信号)在“有效时钟沿”之后变无效的最小时间长度。这个时间的意义是,异步控制信号在时钟上升沿后仍需保持Tremoval的稳定时间,如果保证不了这个去除时间,也就是说这个异步控制信号的解除与“有效时钟沿”离得太近,那么依旧不能保证这一异步控制信号能正常执行。
亚稳态是指触发器无法在某个规定的时间段内到达一个可以确认的状态。即:如果触发器的输入电压采样时间过短,则触发器需要花很长时间来实现输出逻辑达到标准电平,在这段时间里输出端在高低电平之间处于振荡状态,而不是等于理想输出值。也就是说,电路处于中间态的时间变长,使得电路“反应”迟钝,这就是“亚稳态”。(例如输入信号在时钟有效沿的建立时间和保持时间之间改变了,导致不满足触发器的建立时间或保持时间,导致输出有一段时间的不稳定态,就是亚稳态。)
产生和消除
1、跨时钟域信号传输
产生:在跨时钟域信号传输时,由于源寄存器时钟和目的寄存器时钟相移未知,所以源寄存器数据发出数据,数据可能在任何时间到达异步时钟域的目的寄存器,所以无法保证满足目的寄存器Tsu和Th的要求,从而出现亚稳态。
消除:对异步信号进行同步处理;如添加两级D触发器、采用握手进行交互等。
2、异步信号采集
产生:在异步信号采集中,由于异步信号可以在任意时间点到达目的寄存器,所以也无法保证满足目的寄存器Tsu和Th的要求,从而出现亚稳态。
消除:采用FIFO对跨时钟域数据通信进行缓冲设计;
3、异步复位电路
产生:在异步复位电路中,复位信号的释放时间不定,难以保证满足恢复时间(Recovery Time)以及去除时间(Removal Time),从而出现亚稳态。
消除: 对复位电路采用异步复位、同步释放方式处理。详见:三种复位方式详解—-同步复位/异步复位/异步复位同步释放
避免
1 降低系统时钟(不常见,因为高速率正确处理才是目的)
2 用反应更快的触发器(工艺相关,受硬件制约)
3 引入同步机制,防止亚稳态传播(常见的处理方式,即通过一些机制,在现有硬件条件下,最大程度的减少亚稳态发生)
4 改善时钟质量,用边沿变化快速的时钟信号
跨时钟域
两级触发器同步
握手
异步fifo
dmux
fifo深度计算
异步复位与同步复位
同步复位:当时钟上升沿检测到复位信号,执行复位操作(有效的时钟沿是前提)。always @ ( posedge clk );
优点:
a、有利于仿真器的仿真;
b、可以使所设计的系统成为 100% 的同步时序电路,有利于时序分析,而且可综合出较高的 Fmax;
c、由于只在时钟有效电平到来时才有效,所以可以滤除高于时钟频率的复位毛刺。
缺点:
a、复位信号的有效时长必须大于时钟周期,才能真正被系统识别并完成复位任务。同时还要考虑诸如 clk skew 、组合逻辑路径延时 、复位延时等因素(所以复位信号有时需要脉冲展宽,用以保证时钟有效期间有足够的复位宽度);
b、由于大多数的逻辑器件的目标库内的 DFF 都只有异步复位端口,所以,倘若采用同步复位的话,综合器就会在寄存器的数据输入端口插入组合逻辑,这样就会一方面额外增加FPGA内部的逻辑资源,另一方面也增加了相应的组合逻辑门时延。
异步复位:它是指无论时钟沿是否到来,只要复位信号有效,就对系统进行复位。always @ ( posedge clk or negedge rst_n );
优点:
a、大多数目标器件库的 DFF 都有异步复位端口,那么该触发器的复位端口就不需要额外的组合逻辑,这样就可以节省资源;
b、设计相对简单;
c、异步复位信号识别方便(电路在任何情况下都能复位而不管是否有时钟出现)。
缺点:
a、最大的问题在于它属于异步逻辑,问题出现在复位释放时,而不是有效时,如果复位释放接近时钟有效沿,则触发器的输出可能进入亚稳态(此时 clk 检测到的 rst_n 的状态就会是一个亚稳态,即是0是1是不确定的),从而导致复位失败。
b、可能因为噪声或者毛刺造成虚假复位信号(注意:时钟端口、清零和置位端口对毛刺信号十分敏感,任何一点毛刺都可能会使系统出错,因此判断逻辑电路中是否存在冒险以及如何避免冒险是设计人员必须要考虑的问题);
c、静态定时分析比较困难。
d、对于 DFT (Design For Test可测性设计)设计,如果复位信号不是直接来自于 I/O 引脚,在 DFT 扫描和测试时,复位信号必须被禁止,因此需要额外的同步电路。
总结:推荐使用异步复位、同步释放的方式,并且复位信号为低电平有效。
这里:
一、电路解释:是指复位信号到来的有效与否与 clk 无关,而且复位信号的撤除也与 clk 无关,但是复位信号的撤除是在下一个 clk 来到后才起的作用。
二、电路目的:为了防止复位信号撤除时,可能产生的亚稳态。
三、电路详解:异步复位:复位信号 rst_sync_n 由高拉低时实现异步复位。同步释放:这个是关键,即当复位信号 rst_async_n 撤除时(由低拉高),由于双缓冲电路(双寄存器)的作用,rst_sync_n 不会随着 rst_async_n 的撤除而撤除。假设 rst_async_n 撤除时发生在 clk 上升沿,如果不加此电路则可能发生亚稳态事件,但是加上此电路以后,假设第一级 D 触发器 clk 上升沿时 rst_async_n 正好撤除,(第一个DFF 此时是出于亚稳态的;假设此时识别到高电平;若是识别到低电平,则增加一个 Delay)则 DFF1 输出高电平,此时第二级触发器也会更新输出,但是输出值为前一级触发器 clk 来之前时的 Q1 输出状态,显然 Q1 之前为低电平,所以第二级触发器输出保持复位低电平,直到下一个 clk 来之后,才随着变为高电平,即同步释放。
1 | always @ (posedge clk or negedge rst_async_n) begin |
DFT
DFT(Design for Test)可测性设计,为了测试而加入的设计,常见技术 :
(1)Scan Chain(扫描链),针对时序电路,测试寄存器(Flip-Flop)和组合逻辑;
2 内建自测试(BIST)设计技术通过在芯片的设计中加入一些额外的自测试电路,测试时只需要从外部施加必要的控制信号,通过运行内建的自测试硬件和软件,检查被测电路的缺陷或故障。和扫描设计不同的是,内建自测试的测试向量一般是内部生成的,而不是外部输入的。内建自测试可以简化测试步骤,而且无需昂贵的测试仪器和设备(如ATE设备),但它增加了芯片设计的复杂性。
(2)MBIST(Memory Bulit-in Self Test,内建自测试),测试芯片中存储资源, rom 和 ram,在设计中插入内建自测试逻辑;
(3)Boundary Scan(边界扫描),测试封装与 IO、芯片间互联,主要逻辑有 TAP Controller 和 Boundary Scanchain)、JTAG(JTAG 是boundary scan design中用到的一个基本结构)。
1 | 偶数分频 |
1 | module fifo #( |
验证基础知识
Ic验证能力的五个维度:
完备性,复用性,高效性,高产出,代码性能
定宽数组、动态数组、关联数组、队列各自特点和使用
队列的使用方法:insert,delete,push_back和pop_front
Push
插入,pop
取出Front
前边,back
后边
- 队列:队列结合了链表和数组的优点,可以在一个队列的任何位置进行增加或者删除元素;
- 定宽数组:属于静态数组,编译时便已经确定大小。其可以分为压缩定宽数组和非压缩定宽数组:压缩数组是定义在类型后面,名字前面;非压缩数组定义在名字后面。Bit [7:0] [3:0] name; bit[7:0] name [3:0];
- 动态数组:其内存空间在运行时才能够确定,使用前需要用new[]进行空间分配。
- 关联数组:其主要针对需要超大空间但又不是全部需要所有数据的时候使用,类似于
hash
,通过一个索引值和一个数据组成,索引值必须是唯一的。
但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。
【一、灵活性】
- 索引是连续的非负整数,采用定宽或动态数组
- 索引不规则,且稀疏分布,采用关联数组
- 元素数目变化很大数组,采用队列
【二、速度】
- 定宽和动态数组都是存放在连续的存储空间中,访问任何的元素时间都相同,与数组大小无关
- 队列在收尾存取数据几乎没有任何开销,但是在队列中间插入或删除元素需要对其他元素进行搬移以腾出空间。在很长的队列中间插入新的元素,会需要很长的时间
- 关联数组的存取速度是最慢的,因为过程中要有大量算法实现
【三、排序】
- 如果元素一次性全部加入,选择定宽或者动态数组,只需要对数组进行一次分配
- 如果元素一个一个加入,选择队列
- 如果数组的值不连续且互异,可以使用关联数组,把元素值本身作为索引
【四、存储空间】
- 小于1000个元素的数组,各种类型对存储器用量影响不大
- 1000 ~ 100万个元素的数组,定宽和动态数组具有最高的存储器使用效率
- 大于100万个元素的数组,重新检查算法是否有问题
- 对兆字节量级的存储器建模应该使用关联数组,但是注意指针带来的额外消耗,关联数组中每个元素站的空间可能会比定宽和动态数组大好几倍
多线程fork join/fork join_any/fork join_none的用法差异
Fork join
:内部begin end
块并行运行,直到所有线程运行完毕才会进入下一个阶段。Fork join_any
:内部begin end
块并行运行,任意一个begin end
块运行结束就可以进入下一个阶段。Fork join_none
:内部begin end
块并行运行,无需等待可以直接进入下一个阶段。wait fork
:会引起调用进程阻塞,直到它的所有子进程结束,一般用来确保所有子进程(调用进程产生的进程,也即一级子进程)执行都已经结束。disable fork
:用来终止调用进程 的所有活跃进程, 以及进程的所有子进程。
多线程的同步调度方法
多线程之间同步主要由mailbox、event、 semaphore
三种进行一个通信交互。
mailbox邮箱
:主要用于两个线程之间的数据通信,通过put函数和 get 函数还有peek函数进行数据的发送和获取。Event
:事件主要用于两个线程之间的一个同步运行,通过事件触发和事件等待进行两个线程间的运行同步。使用@(event)或者 wait(event.trigger)进行等待,->进行触发。Semaphore
:旗语主要是用于对资源访问的一个交互,通过key的获取和返回实现一个线程对资源的一个访问。使用put和 get函数获取返回key。一次可以多个。
Task和function的区别
- 函数能调用另一个函数,但不能调用任务,任务能调用另一个任务,也能调用另一个函数
- 函数总是在仿真时刻0就开始执行,任务可以在非零时刻执行
- 函数一定不能包含任何延迟、事件或者时序控制声明语句,任务可以包含延迟、事件或者时序控制声明语句
- 函数至少有一个输入变量,可以有多个输入变量,任务可以没有或者多个输入(input)、输出(output)和双向(inout)变量
- 函数只能返回一个值,函数不能有输出(output)或者双向(inout)变量,任务不返回任何值,任务可以通过输出(output)或者双向(inout)变量传递多个值
使用interface和clocking blocking的好处
Interface
是一组接口,用于对信号进行一个封装,捆扎起来。如果像verilog
中对各个信号进行连接,每一层我们都需要对接口信号进行定义,若信号过多,很容易出现人为错误,而且后期的可重用性不高。因此使用interface
接口进行连接,不仅可以简化代码,而且提高可重用性,除此之外,interface
内部提供了其他一些功能,用于测试平台与DUT之间的同步和避免竞争。Clocking block
:在interface
内部我们可以定义clocking
块,可以使得信号保持同步,对于接口的采样vrbg和驱动有详细的设置操作,从而避免TB
与DUT
的接口竞争,减少我们由于信号竞争导致的错误。采样提前,驱动落后,保证信号不会出现竞争。
OPP(面向对象)的特性?
封装、继承和多态
- 封装:通过将一些数据和使用这些数据的方法封装在一个集合里,成为一个类。
- 继承:允许通过现有类去得到一个新的类,且其可以共享现有类的属性和方法。现有类叫做基类,新类叫做派生类或扩展类。
- 多态:得到扩展类后,有时我们会使用基类句柄去调用扩展类对象,这时候调用的方法如何准确去判断是想要调用的方法呢?通过对类中方法进行
virtual
声明,这样当调用基类句柄指向扩展类时,方法会根据对象去识别,调用扩展类的方法,而不是基类中的。而基类和扩展类中方法有着同样的名字,但能够准确调用,叫做多态。
简述UVM的工厂机制 Callback
机制
Factory
机制也叫工厂机制,其存在的意义就是为了能够方便的替换TB中的实例或者已注册的类型。一般而言,在搭建完TB后,我们如果需要对TB进行更改配置或者相关的类信息,我们可以通过使用factory
机制进行覆盖,达到替换的效果,从而大大提高TB的可重用性和灵活性。- Factory的好处是便于创建模块化、可复用的测试平台。同时Factory使得类的重载与复用变得更加简单。
- 要使用factory机制先要进行:
- 将类注册到factory表中
- 创建对象,使用对应的语句 (type_id::create)
- 编写相应的类对基类进行覆盖。
通过工厂进行覆盖有什么要求?
- 无论是重载的类(
parrot
)还是被重载的类(bird
),都要在定义时注册到factory
机制中。 - 被重载的类(
bird
)在实例化时,要使用factory
机制式的实例化方式,而不能使用传统的new
方式。 - 最重要的是,重载的类(
parrot
)要与被重载的类(bird
)有派生关系。重载的类必须派生自被重载的类,被重载的类必须是重载类的父类。
Uvm_component_utils有什么作用
factory
机制的实现被集成在了一个宏中:uvm_component_utils
。- 这个宏最主要的任务是,将字符串登记在
UVM
内部的一张表中,这张表是factory
功能实现的基础。只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中。这样,factory
机制可以实现:根据一个字符串自动创建一个类的实例,并且调用其中的函数(function
)和任务(task
),这个类的main_phase
就会被自动调用。
Callback
机制其作用是提高TB
的可重用性,其还可进行特殊激励的产生等,与factory
类似,两者可以有机结合使用。与factory
不同之处在于 callback
的类还是原先的类,只是内部的callback
函数变了,而factory
是产生一个新的扩展类进行替换。
UVM
组件中内嵌callback
函数或者任务- 定义一个常见的
uvm_callbacks class
- 从
UVM callback
空壳类扩展uvm_callback
类 - 在验证环境中创建并登记
uvm_callback
field_automation机制和objection机制
field_automation
机制:可以自动实现copy、compare、print
等三个函数。当使用uvm_field
系列相关宏注册之后,可以直接调用以上三个函数,而无需自己定义。这极大的简化了验证平台的搭建,尤其是简化了driver
和monitor
,提高了效率。
UVM
中通过objection
机制来控制验证平台的关闭,需要在drop_objection
之前先raise_objection
。验证在进入到某一phase
时,UVM
会收集此phase
提出的所有objection
,并且实时监测所有objection
是否已经被撤销了,当发现所有都已经撤销后,那么就会关闭此phase
,开始进入下一个phase
。当所有的phase
都执行完毕后,就会调用$finish
来将整个验证平台关掉。如果UVM
发现此phase
没有提起任何objection
,那么将会直接跳转到 下一个phase
中。
UVM
的设计哲学就是全部由sequence
来控制激励生成,因此一般情况下只在sequence
中控制objection
。另外还需注意的是,raise_objection
语句必须在main_phase
中第一个消耗仿真时间的语句之前。
UVM从哪里启动,
UVM的启动 总结:
run_test来启动UVM验证平台
- 在导入uvm_pkg文件时,会自动创建UVM_root所例化的对象UVM_top,UVM顶层的类会提供run_test()方法充当UVM世界的核心角色,通过UVM_top调用run_test()方法.
- 在环境中输入run_test来启动UVM验证平台,run_test语句会创建一个my_case0的实例,得到正确的test_name
- 依次执行uvm_test容器中的各个component组件中的phase机制,按照顺序:
- build-phase(自顶向下构建UVM 树)
- connet_phase(自低向上连接各个组件)
- end_of_elaboration_phase
- start_of_simulation_phase
- run_phase() objection机制仿真挂起,通过start启动sequence(每个sequence都有一个body任务。当一个sequence启动后,会自动执行sequence的body任务),等到sequence发送完毕则关闭objection,结束run_phase()(UVM_objection提供component和sequence共享的计数器,当所有参与到objection机制中的组件都落下objection时,计数器counter才会清零,才满足run_phase()退出的条件)
- 执行后面的phase
接口怎么传递到环境中
a) 虽然SV可以通过层次化的interface的索引完成传递,但是这种传递方式不利于软件环境的封装和复用。通过使用uvm_config_db配置机制来传递接口,可以将接口的传递与获取彻底分离开。
b) 接口传递从硬件世界到UVM环境可以通过uvm_config_db来实现,在实现过程中应当注意:
c) 接口传递应发生在run_test()之前。这保证了在进入build_phase之前,virtual interface已经被传递到uvm_config_db中。
d) 用户应当把interface与virtual interface区分开来,在传递过程中的类型应当为virtual interface,即实际接口的句柄
如何在driver中使用interface,为什么
Interface
如果不进行virtual
声明的话是不能直接使用在dirver
中的,会报错,因为interface
声明的是一个实际的物理接口。一般在dirver
中使用virtual interface
进行申明接口,然后通过config_db
进行接口参数传递,这样我们可以从上层组件获得虚拟的interface
接口进行处理。Config_db
传递时只能传递virtual
接口,即interface
的句柄,否则传递的是一个实际的物理接口,这在driver
中是不能实现的,且这样的话不同组件中的接口一一对应一个物理接口,那么操作就没有意义了。
Config_db的作用,以及传递其使用时的参数含义
Config_db
机制主要作用就是传递参数使得TB
的可配置性高,更加灵活。Config_db
机制主要传递的有三种类型:
- 一种是
interface
虚拟接口,通过传递virtual interface
使得dirver
和monitor
能够与DUT
连接,并驱动接口和采集接口信号。 - 第二种是单一变量参数,如
int,string,enum
等,这些主要就是为了配置某些循环次数,id
号是多少等等。 - 第三种是
object
类,这种主要是当配置参数较多时,我们可以将其封装成一个object
类,去包含这些属性和相关的处理方法,这样传递起来就比较简单明朗,不易出错。
Config_db
的参数主要由四个参数组成,如下所示,第一个参数为父的根parent
,第二个参数为接下来的路径,对应的组件,第三个是传递时的名字(必须保持一致),第四个是变量名。uvm_config_db #(virtual interface) :: set(uvm_root:.get(),”uvm_test_top.c1”,’vif”,vif); uvm_config_db #(virtual interface) :: get(this,””,”vif”,vif);
如果环境中有两个config_db set,哪个有效?
UVM
更高的层次更接近用户,为了让用户少和底层组件打交道,所以层次越高优先级越高,高层次的set
会覆盖底层次的set
,如果是层次相同再看时间先后顺序,谁发生的晚谁有效,时间靠后的会覆盖之前的。
uvm是什么
UVM
对一些些重复性和重要的进行封装,在搭建测试平台时可直接调用,从而使帮助验证人员能够快速的搭建一个需要的测试平台,而且还可以提高验证平台的可重用性。但是UVM
又不仅仅是封装。
- 刚开始接触的时候,我认为
UVM
其实就是SV
的一个封装,将我们在搭建测试平台过程中的一些重复性和重要的工作进行封装,从而使我们能够快速的搭建一个需要的测试平台,并且可重用性还高。因此我当时觉得它就是一个库。 - 不过,随着学习的不断深入,当我深入理解
UVM
中各种机制和模型的构造和相互之间关系之后,我觉得其实UVM
方法学对于使用何种语言其实并不重要,重要的是他的思想,比如:在UVM
中有sequence
机制,以往如果我们使用SV
进行TB
搭建时,我们一般会采用driver
一个类进行数据的产生,转换,发送,或者使用generator
和driver
两个进行,这种方式可重用性很低,而且代码臃肿;但是在UVM中我们通过将sequence、sequencer、driver、sequence_item
拆开,相互独立而又有联系,因此我们只需关注每一个类需要做的工作就可以,可重用性高。我在学习sequence
时,我经常把sequence
比作蓄水池,sequence_item
就是水,sequencer
就是一个调度站,driver
就是总工厂,通过这种方式进行处理,我们的总工厂不需要管其他,只需处理运送过来的水资源就可以,而sequencer
只需要调度水资源,sequence
只需要产生不同的水资源。而这种处理方式和现实世界中的生产模式又是基本吻合的。除此之外,还有好多好多,其实UVM
方法学中很多思想就是来源于经验,来源于现实生活,而不在乎是何种语言。
uvm优缺点
- UVM的优点:UVM有各个机制、促进验证平台的标准化,UVM中
test sequence
和验证平台是隔离独立的,可以更好的控制激励而不需要重新设计agent
. 改变测试sequence
可以简单高效提高代码覆盖率。UVM
支持工业标准,这会促进验证平台标准化。此外,UVM
通过OOP
(面向对象编程)的特点(例如继承)以及使用覆盖组件提高了重复使用率。因此UVM环境方便移植,架构清晰,组件连接方便,有利于进行大规模的验证。 - UVM的缺点:代码冗余,工作量大,运行速度有缺失
UVM验证环境的组成
首先,UVM
测试平台基本是由object
和 component
组成的,其中 component
搭建了TB
的一个树形结构,其基本包含了driver、monitor、sequencer、agent、scoreboard、model、env、test、top
;然后object
一般包含sequence_item、config
和一些其他需要的类。各个组件相互独立,又通过TLM
事务级传输进行通信,除此之外,DUT
与driver
和 monitor
又通过interface
进行连接,实现驱动和采集,最后在top
层进行例化调用test
进行测试。
Sequencer
:负责将数据转给driver
driver
负责数据的发送;driver
有时钟/时序的概念。monitor 监测dut的输入输出
Agent
:其实只是简单的把driver
,monitor
和sequencer
封装在一起。scorboard 对dut的输出与期望输出进行对比
Env
:则相当于是一个特大的容器,将所有成员包含进去。除了driver、monitor、agent、model、scoreboard、env、test之外全部用uvm_object。
代码覆盖率、功能覆盖率和断言覆盖率的区别
- 代码覆盖率——是针对RTL设计代码的运行完备度的体现,包括行覆盖率、条件覆盖率、FSM覆盖率、跳转覆盖率、分支覆盖率,只要仿真就可以收集,可以看DUT的哪部分代码没有动,如果有一部分代码一直没动看一下是不是case没有写到。
- 功能覆盖率—-与
spec
比较来发现,design
是否行为正确,需要按verification plan
来比较进度。用来衡量哪些设计特征已经被测试程序测试过的一个指标
- 首要的选择是使用更多的种子来运行现有的测试程序;
- 其次是建立新的约束,只有在确实需要的时候才会求助于定向测试,改进功能覆盖率最简单的方法是仅仅增加仿真时间或者尝试新的随机种子。
- 验证的目的就是确保设计在实际环境中的行为正确。设计规范里详细说明了设备应该如何运行,而验证计划里则列出了相应的功能应该如何激励、验证和测量
断言覆盖率:用于检查几个信号之间的关系,常用在查找错误,主要是检查时序上的错误,测量断言被触发的频繁程度。
项目中会考虑哪些coverage
- 主要会考虑三个方面吧,代码覆盖率,功能覆盖率,断言覆盖率。
- 代码覆盖率,主要由行覆盖率、条件覆盖率、
fsm
覆盖率、跳转覆盖率、分支覆盖率,他们是否都是运行到的,比如fsm
,是否各个状态都运行到了,然后不同状态之间的跳转是否也都运行到了。 - 功能覆盖率的话主要是自己编写
covergroup
和coverpoint
去覆盖我们想要覆盖的数据和地址或者其他控制信号。 - 断言覆盖率主要检测我们的时序关系是否都运行到了,比如总线的地址数据读写时序关系是否都有实现。
Coverage一般不会直接达到100%,当你发现condition未cover到的时候,你该怎么做?
Condition
又称为条件覆盖率,当条件覆盖率未被覆盖时,我们需要通过查看覆盖率报告去定位哪些条件没有被覆盖到,是因为没有满足该条件的前提条件还是因为根本就遗漏了这些情况,根据这个我们去编写相应的case
,进而将其覆盖到。
Function coverage和 code coverage的区别,以及他们分别对项目的含义
- 功能覆盖率主要是针对
spec
文档中功能点的覆盖检测 -code
覆盖率主要是针对RTL
设计代码的运行完备度的体现,其包括行覆盖率、条件覆盖率、FSM
覆盖率、跳转覆盖率、分支覆盖率(只要仿真就可以,看看DUT
的哪些代码没有动,如果有一部分代码一直没动,看一下是不是case
没写到)。 - 功能覆盖率和代码覆盖率两者缺一不可,功能覆盖率表示着代设计是否具备这些功能,代码覆盖率表示我们的测试是否完备,代码是否冗余。当功能覆盖率高而代码覆盖率低时,表示
covergroup
是不是写少了,case
写少了;或者代码冗余。当功能覆盖率很低而代码覆盖率高时,表示代码设计是不是全面,功能点遗漏;covergroup
写的是不是冗余了。只有当两者覆盖率都高的时候才表明我们验证的大部分是可靠的。 - 代码覆盖率很难达到100%,一般情况下达到90%多已经非常不错了,如果有一部分代码没有被触动到,需要有经验的验证工程师去分析,如果确实没啥问题,就可以签字通过了
Find 队列和find index队列
find的队列应该是返回队列的值,一般的话是和with配合使用,find index应该是返回索引值
断言
https://zhuanlan.zhihu.com/p/399746842
https://blog.csdn.net/qq_38620826/article/details/116858564
并行断言
时序性的 property 与设计模块一同并行执行
并发断言基于时钟周期进行,可以放置于procedural block、module、interface及program中。在静态(形式)验证及动态仿真工具中均可以应用。
立即断言
非时序的、执行时如过程语句,可以在initial/always过程块或者task/function中使用
立即断言基于事件,必须放置在程序块中,只能用于动态仿真,没有时间的概念。立即断言与并发断言关键字区分在于“property”。简单的立即断言示例如下:
断言 and 和 和 intersect 区别
And
指的是两个序列具有相同的起始点,终点可以不同。Intersect
指的是两个序列具有相同的起始点和终点。Or
指的是两个序列只要满足一个就可以Throughout
指的是满足前面要求才能执行后面的序列
你写过assertion嘛,assertion分几种?简述一下assertion的用法
Assertion
可以分为立即断言和并发断言。- 立即断言的话就是和时序无关,比如我们在对激励随机化时,我们会使用立即断言,如果随机化出错我们就会触发断言报错。
- 并发断言的话主要是用来检测时序关系的,由于在很多模块或者总线中,单纯使用覆盖率或者事务
check
并不能完全检测多个时序信号之间的关系,但是并发断言却可以使用简洁的语言去监测,除此之外,还可以进行覆盖率检测。 - 并发断言的用法的话,主要是有三个层次:
- 序列
sequence
编写,将多个信号的关系用断言中特定的操作符进行表示; - 属性
property
的编写,它可以将多个sequence
和多个property
进行嵌套,外加上触发事件; assert
的编写,调用property
就可以。编写完断言后我们可以将它用在很多地方,比如DUT
内部,或者在top
层嵌入DUT
中,还可以在interface
处进行编写,基本能够检测到信号的地方都可以进行断言检测。
a[*3]、a[->3]和a[=3]区别
- a[*3]指的是:重复3次a,且其与前后其他序列不能有间隔,a中间也不可有间隔。
- a[->3]指的是:重复3次,其 a中间可以有间隔,但是其后面的序列与a之间不可以有间隔。
- a[=3]指的是:只要重复3次,中间可随意间隔。
形式验证
- 形式验证指从数学上完备地证明或验证电路的实现方案是否确实实现了电路设计所描述的功能。形式验证方法分为等价性验证、模型检验和定理证明等。
- 形式验证主要验证数字IC设计流程中的各个阶段的代码功能是否一致,包括综合前RTL代码和综合后网表的验证,因为如今IC设计的规模越来越大,如果对门级网表进行动态仿真,会花费较长的时间,而形式验证只用几个小时即可完成一个大型的验证。另外,因为版图后做了时钟树综合,时钟树的插入意味着进入布图工具的原来的网表已经被修改了,所以有必要验证与原来的网表是逻辑等价的
如何保证验证的完备性?
- 首先不可能百分百完全完备,即遍历所有信号的组合,这既不经济也不现实。
- 所以只能通过多种验证方法一起验证尽可能减少潜在风险,一般有这些验证流程:ip级验证、子系统级验证、soc级验证,除这些以外,还有upf验证、fpga原型验证等多种手段。
- 前端每走完一个阶段都需要跟设计以及系统一起
review
验证功能点,测试用例,以及特殊情况下的波形等。 - 芯片后端也会做一些检查,像sta、formality、DFM、DRC检查等,也会插入一些DFT逻辑供流片回来测试用。流片归来进行测试,有些bug可以软件规避,有些不能规避,只能重新投片
启动Sequence的方法
- 通过sequence.start的方式显示启动
- 通过default sequence来隐式启动
- 也可以通过‘uvm_do系列宏启动
面向对象编程的优势
- 易维护:采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
- 质量高:在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
- 效率高:在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
- 易扩展:由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
事件的触发
用来触发事件时,使用->;用来等待事件使用@或者wait。
约束的几种形式
- 权重约束 dist:有两种操作符::=n :/n 第一种表示每一个取值权重都是n,第二种表示每一个取值权重为n/num。
- 条件约束 if else 和->(case):if else 就是和正常使用一样;->通过前面条件满足后可以触发后面事件的发生。
- 范围约束inside:inside{[min:max]};范围操作符,也可以直接使用大于小于符号进行,但是不可以连续使用,如 min<wxm<max 这是错误的
- 通过
constraint_mode(0)
关闭默认范围的约束块 constraint_mode(1)
是打开约束- 可以用
soft
关键字修饰特定的约束语句,这样既可以让变量在一般的情况下取默认值,也可以直接给变量赋默认值范围外的取值。
get_next_item()和try_next_item()有什么区别
get_next_item()
是一个阻塞调用,直到存在可供驱动的sequence item
为止,并返回指向sequence item
的指针。try_next_item()
是非阻塞调用,如果没有可供驱动的sequence item
,则返回空指针。
Break;continue;return的含义,return之后,function里剩下的语句会执行吗
break
语句结束整个循环。continue
立即结束本次循环,继续执行下一次循环。return
语句会终止函数的执行并返回函数的值(如果有返回值的话)。return
之后,function
里剩下的语句不能执行,其是终止函数的执行,并返回函数的值。
组件之间的通信机制,analysis port和其它的区别
- 通信分为,单向通信,双向通信和多向通信
- 单向通信:指的是从
initiator
到target
之间的数据流向是单一方向的 - 双向通信:双向通信的两端也分为
initiator
和target
,但是数据流向在端对端之间是双向的 - 多向通信:仍然是两个组件之间的通信,是指
initiator
与target
之间的相同TLM端口数目超过一个时的处理解决办法。
- blocking阻塞传输的方法包含:
- Put():
initiator
先生成数据Tt
,同时将该数据传送至target
。 - Get():
initiator
从target
获取数据Tt
,而target
中的该数据Tt
则应消耗。 - Peek():
initiator
从target
获取数据Tt
,而target
中的该数据Tt
还应保留。
- 通信管道:
TLM FIFO
:可以进行数据缓存,功能类似于mailbox
,不同的地方在于uvm_tlm_fifo
提供了各种端口(put、get、peek)
供用户使用analysis port
:一端对多端,用于多个组件同时对一个数据进行处理,如果这个数据是从同一个源的TLM
端口发出到达不同组件,则要求该端口能够满足一端到多端,如果数据源端发生变化需要通知跟它关联的多个组件时,我们可以利用软件的设计模式之一观察者模式实现,即广播模式analysis TLM FIFO
a. 由于analysis
端口提出实现了一端到多端的TLM
数据传输,而一个新的数据缓存组件类uvm_tlm_analysis_fifo
为用户们提供了可以搭配uvm_analysis_port
端口uvm_analysis_imp
端口和write()
函数。
b.uvm_tlm_analysis_fifo
类继承于uvm_tlm_fifo
,这表明它本身具有面向单一TLM
端口的数据缓存特性,而同时该类又有一个uvm_analysis_imp
端口analysis_export
并且实现了write()
函数:
- request & response通信管道 双向通信端口
transport
,即通过在target
端实现transport()
方法可以在一次传输中既发送request
又可以接收response
。
UVM组件的通信方式TLM的接口分类和用法,peek和get的差异
UVM
中采用事务级传输机制进行组件间的通信,可以大大提高仿真的速度和使得我们简化组件间的数据传输,简化工作,TLM
独立于组件之外,降低组件间的依赖关系。UVM
接口主要由port、export、imp
;驱动这些接口方式有put、get、peek、transport、analysis
等。- 其中
peek
是查看端口内部的数据事务但是不删除,get
是获取后立即删除。我们一般会先使用peek
进行获取数据,但不删除(保证put
端不会立马又发送一个数据),处理完毕后再用get
删除。 - lmp只能作为终点接口,transport表示双向通信,analysis可以连接多个imp(类似于广播)。
Analysis port是否可以不连或者连多个impport
都可以。Analysis port
类似于广播,其可以同时对多个imp
进行事务通信,只需要在每一个对应的imp
端口申明write()
函数即可。对比 put,get,peek port,
他们都只能进行一对一传输,且也必须申明对应的函数如 put()、get()、peek()、can_put()/do_put()
等。Fifo
是可以不用申明操作函数的,其内部封装了很多的通信端口,如analysis_export
等,我们只需要将端口与其连接即可实现通信。
TLM怎么用
TLM
通信的步骤:
- 分辨出
initiator
和target,producer
和consumer
。 - 在
target
中实现tlm
通信方法。 - 在俩个对象中创建
tlm
端口。 - 在更高层次中将俩个对象进行连接。
- 端口类型有三种:
port
,一般是initiator
的发起端。export
,作为initiator
和target
的中间端口。imp
,只能作为target
接受request
的末端。- 多个
port
可以连接同一个export
或imp
,但是单个port
或export
不能连接多个imp
。
- 端口的连接:通过
connect
函数进行连接,例如A(initiator)
与B
进行连接,可以使用A.port.connect(B.export)
- uvm_*_imp#(T,IMP);IMP定义中第一个参数T是这个IMP传输的数据类型,第二个参数IMP是实现这个接口所在的
component
。
简述深拷贝和浅拷贝
- 浅拷贝可以使用列表自带的
copy()
函数(如list.copy()
),或者使用copy
模块的copy()
函数。深拷贝只能使用copy
模块的deepcopy()
,所以使用前要导入:from copy import deepcopy
- 如果拷贝的对象里的元素只有值,没有引用,那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象,对新对象里的值进行修改不会影响原有对象,新对象和原对象完全分离开。
- 如果拷贝的对象里的元素包含引用(像一个列表里储存着另一个列表,存的就是另一个列表的引用),那浅拷贝和深拷贝是不同的,浅拷贝虽然将原有对象复制一份,但是依然保存的是引用,所以对新对象里的引用里的值进行修改,依然会改变原对象里的列表的值,新对象和原对象完全分离开并没有完全分离开。而深拷贝则不同,它会将原对象里的引用也新创建一个,即新建一个列表,然后放的是新列表的引用,这样就可以将新对象和原对象完全分离开。
浅拷贝即句柄拷贝,不复制对象,只复制了句柄。只拷贝对象中的数据变量,而对于对象中的数据操作(一般为任务和函数)和其中定义的其他类的句柄,采用类似“引用”的方式,浅拷贝前后共用同一内存空间。
深拷贝即对象拷贝,对对象中的所有成员变量(包括数据变量、数据操作和其他句柄)统一分配新的内存空间。
类的public、protected和local的区别
- 如果没有指明访问类型,那么成员的默认类型是public,子类和外部均可以访问成员。
- 如果指明了访问类型是
protected
,那么只有该类或者子类可以访问成员,而外部无法访问。 - 如果指明了访问类型是
local
,那么只有该类可以访问成员,子类和外部均无法访问。
举例说明UVM组件中常用的方法,各种phase关系,phase机制作用
UVM
中有很多非常有趣的机制,例如factory
机制,field_automation
机制,phase
机制,打印机制,sequence
机制,config_db
机制等,这些机制使得我们搭建的UVM
能够有很好的可重用性和使得我们平台运行有秩序稳定。例如
phase
机制,phase
机制主要是使得UVM
的运行仿真层次化,使得各种例化先后次序正确。UVM
的phase
机制主要有9个,外加12个小phase
。主要的phase
有build phase、connect phase、run phase、report phase、final phase
等,其中除了run phase
是** task**
,其余都是function
,然后build phase
和final phase
都是自顶向下运行,其余都是自底向上运行。Run phase
和12个小phase
(reset phase、configure phase、main phase、shutdown phase
)是并行运行的,有这12个小phase
主要是进一步将run phase
中的事务划分到不同的phase
进行,简化代码。注意,run phase
和 12个小phase
最好不要同时使用。从运行上来看,9个phase
顺序执行,不同组件中的同一个phase
执行有顺序,build phase
为自顶向下,只有同一个phase
全部执行完毕才会执行下一个phase
。所有的
phase
按照以下顺序自上而下自动执行:(九大phase,其中run phase又分为12个小phase)build_paseconnect_phase
end_of_elaboration_phase
start_of_simulation_phase
run_pase
extract_phase
check_phase
report_phase
final_phase
其中,run_phase按照以下顺序自上而下执行:
pre_reset_phase
reset_phase
post_reset_phase
pre_configure_phase
configure_phase
post_configure_phase
pre_main_phase
main_phase
post_main_phase
pre_shutdown_phase
shutdown_phase
post_shutdown_phase
run_phase和main_phase之间的关系;
run_phase
和main phase
(动态运行)都是task phase
,且是并行运行的,后者称为动态运行(run-time
)的phase
。如果想执行一些耗费时间的代码,那么要在此
phase
下任意一个component
中至少提起一次objection
,这个结论只适用于12个run-time
的phase
。对于run_phase
则不适用,由于run_phase
与动态运行的phase
是并行运行的,如果12个动态运行的phase
有objection
被提起,那么run_phase
根本不需要raise_objection
就可以自动执行。main_phase要如何跳转到reset_phase;
在main_phase
执行过程中,突然遇到reset
信号被置起,可以用jump()
实现从mian_phase
到reset_phase
的跳转:
UVM中各个component之间是如何组织运行的,串行还是并行,通过什么机制进行调度的
Component
之间通过在new
函数创建时指定parent
参数指定子关系,通过这种方法来将TB
形成一个树形结构。UVM
中运行是通过Phase
机制进行层次化仿真的。从组件来看各个组件并行运行,从phase
上看是串行运行,有层次化的。Phase
机制的9个phase
是串行运行的,不同组件中的同一个phase
都运行完毕后才能进入下一个phase
运行,同一个phase
在不同组件中的运行也是由一定顺序的,build
和 final
是自顶向下。
phase中的domain概念
Domain
是用来组织不同组件,实现独立运行的概率。默认情况下,UVM
的9个phase
属于 common_domain
,12个小phase
属于uvm_domain
。例如,如果我们有两个dirver
类,默认情况下,两个driver
类中的复位phase
和 main phase
必须同时执行,但是我们可以设置两个driver
属于不同的domain
,这样两个dirver
就是独立运行的了,相当于处于不同的时钟域(只针对12个小phase
有效)。
Sequence和item(uvm_sequece,uvm_sequence_item)以及sequence的分类
item
是基于uvm_object
类,这表明了它具备UVM
核心基类所必要的数据操作方法,例如copy、 clone、compare、record
等。item
对象的生命应该开始于sequence
的body()
方法,而后经历了随机化并穿越sequencer
最终到达driver
,直到被driver
消化之后,它的生命一般来讲才会结束。- item与sequence的关系 一个
sequence
可以包含一些有序组织起来的item
实例,考虑到item
在创建后需要被随机化,sequence
在声明时也需要预留一些可供外部随机化的变量,这些随机变量一部分是用来通过层级传递约束来最终控制item
对象的随机变量,一部分是用来对item
对象之间加以组织和时序控制的。 - Sequence的分类:
- 扁平类
(flat sequence)
:这一类往往只用来组织更细小的粒度,即item实例构成的组织。 - 层次类
( hierarchical sequence)
:这一类是由更高层的sequence用来组织底层的sequence
,进而让这些sequence
或者按照顺序方式,或者按照并行方式,挂载到同一个sequencer
上。 - 虚拟类
(virtual sequence)
:这一类则是最终控制整个测试场景的方式,鉴于整个环境中往往存在不同种类的sequencer
和其对应的sequence
,我们需要一个虚拟的sequence
来协调顶层的测试场景。之所以称这个方式为virtual sequence
,是因为该序列本身并不会固定挂载于某一种sequencer
类型上,而是将其内部不同类型sequence
最终挂载到不同的目标sequencer
上面。这也是virtual sequence
不同于hierarchical sequence
的最大一点。
Sequence和sequencer的关系
sequence
机制用于产生激励,它是UVM
中最重要的机制之一。sequence
机制有两大组成部分:sequence
和sequencer
。- 在整个验证平台中
sequence
处于一个比较特殊的位置。sequence
不属于验证平台的任何一部分,但是它与sequencer
之间有着密切的关系。 - 只有在
sequencer
的帮助下,sequence
产生的transaction
才能最终送给driver
;同样,sequencer
只有在sequence
出现的情况下才能体现出其价值,如果没有sequence
,sequencer
几乎没有任何作用。 - 除此之外,
sequence
与sequencer
还有显著的区别。从本质上说,sequencer
是一个uvm_component
,而sequence
是一个uvm_object
。与my_transaction
一样,sequence
也有其生命周期。它的生命周期比my_transaction
要更长一点,其内部的transaction
全部发送完毕后,它的生命周期也就结束了。
Sequencer的仲裁特性(set_arbitration)及锁定机制(lock和grab)
仲裁特性
锁定机制
Virtual sequence和virtual sequencer中virtual含义
Virtual
含义就是其sequencer
并不需要传递item
,也不会与driver
连接,其只是一个去协调各个sequencer
的中央路由器。通过virtual sequencer
我们可以实现多个agent
的多个sequencer
他们的 sequence
的调度和可重用。Virtual sequence
可以组织不同sequencer
的sequence
群落。
为什么会有sequence、sequencer以及driver,为什么要分开实现,这样做的好处是什么?
- 在
UVM
中有sequence
机制,以往如果我们使用SV
进行TB
搭建时,我们一般会采用driver
一个类进行数据的参数,转换,发送,或者使用genetor
和driver
两个进行,这种方式可重用性很低,而且代码臃肿; - 但是在UVM中我们通过将
sequence、sequencer、driver、sequence_item
拆开,相互独立而又有联系,因此我们只需关注每一个类需要做的工作就可以,可重用性高。我在学习sequence
时,我经常把sequence
比作蓄水池,sequence_item
就是水,sequencer
就是一个调度站,driver
就是总工厂,通过这种方式进行处理,我们的总工厂不需要管其他,只需处理运送过来的水资源就可以,而sequencer
只需要调度水资源,sequence
只需要产生不同的水资源。
UVM如何启动一个sequence
- 启动
sequence
有很多的方法:常用的方法有使用default sequence
进行调用,其会将对应的sequence
与sequencer
绑定,当dirver
请求获得req
时,sequencer
就会调用对应的sequence
去运行body
函数,从而产生req
。 - 除此之外,还可以使用
start
函数进行,其参数主要就是对应的需要绑定的sequencer
和该类的上层sequence
。如此,就可以实现启动sequence
的功能。 - 注意:一般仿真开始结束会在
sequence
中raise objection
和drop objection
你所搭建的验证平台为什么要用RAL(寄存器)
- 首先,我们要了解寄存器对于设计的重要性,其是模块间交互的窗口,我们可以通过读寄存器值去观察模块的运行状态,通过写寄存器去控制模块的配置和功能改变。
- 然后,为什么我们需要RAL呢?由于前面寄存器的重要性,我们可以知道,如果我们不能先保证我们寄存器的读写正确,那么就不用谈后续 DUT是否正确了,因此,寄存器的验证是排在首要位置的。
- 那么我们应该用什么方法去读写和验证寄存器呢?采用RAL寄存器模型去测试验证,是目前最成功的方法吧,寄存器模型独立于
TB
之外,我们可以搭建一个测试寄存器的agent
,去通过前门或者后门访问去控制DUT
的寄存器,使得DUT
按照我们的要求去运行。 - 除此之外,
UVM
中内建了很多RAL
的sequence
,用于帮助我们去检测寄存器,除此之外,还有一些其他的类和变量去帮助我们搭建,以提高RAL
的可重用性和便捷性还有更全的覆盖率。
前门访问和后门访问的区别
- 前门访问和后门访问的比较
- 前门访问,顾名思义指的是在寄存器模型上做的读写操作,最终会通过总线UVC来实现总线上的物理时序访问,因此是真实的物理操作。
- 后门访问,指的是利用
UVM DPI (uvm_hdl_read()、uvm_hdl_deposit())
,将寄存器的操作直接作用到DUT内的寄存器变量,而不通过物理总线访问。 - 前门访问在使用时需要将
path
设置为UVM_FRONTDOOR
- 在进行后门访问时,用户首先需要确保寄存器模型在建立时,是否将各个寄存器映射到了
DUT
一侧的HDL
路径:使用add_hdl_path
- 从上面的差别可以看出,后门访问较前门访问更便捷更快一些,但如果单纯依赖后门访问也不能称之为“正道”。
实际上,利用寄存器模型的前门访问和后门访问混合方式,对寄存器验证的完备性更有帮助。
后门访问的路径怎么配置
如果寄存器的地址不匹配的错误怎么测试出来
在通过前门配置寄存器A之后,再通过后门访问来判断HDL地址映射的寄存器A变量值是否改变,最后通过前门访问来读取寄存器A的值。
寄存器模型的常规方法(期望值、镜像值、真实值)
1 | mirror、desired、actual value() |
- 我们在应用寄存器模型的时候,除了利用它的寄存器信息,也会利用它来跟踪寄存器的值。寄存器有很多域,每一个域都有两个值。
- 寄存器模型中的每一个寄存器,都应该有两个值,一个是镜像值(
mirrored value
) , 一个是期望值(desired value
) 。 - 期望值是先利用寄存器模型修改软件对象值,而后利用该值更新硬件值;镜像值是表示当前硬件的已知状态值。
- 镜像值往往由模型预测给出,即在前门访问时通过观察总线或者在后门访问时通过自动预测等方式来给出镜像值
- 镜像值有可能与硬件实际值不一致
Prediction的分类(自动预测和显式预测)
- UVM提供了两种用来跟踪寄存器值的方式,我们将其分为自动预测(
auto prediction
)和显式预测(explicit
)。 - 如果用户想使用自动预测的方式,还需要调用函数
uvm_reg_map::set_auto predict()
- 两种预测方式的显著差别在于,显式预测对寄存器数值预测更为准确,我们可以通过下面对两种模式的分析得出具体原因。自动预测
- 如果用户没有在环境中集成独立的
predictor
,而是利用寄存器的操作来自动记录每一次寄存器的读写数值,并在后台自动调用predict()
方法的话,这种方式被称之为自动预测。 - 这种方式简单有效,然而需要注意,如果出现了其它一些
sequence
直接在总线层面上对寄存器进行操作(跳过寄存器级别的write/read
操作,或者通过其它总线来访问寄存器等这些额外的情况,都无法自动得到寄存器的镜像值和预期值。显式预测 - 更为可靠的一种方式是在物理总线上通过监视器来捕捉总线事务,并将捕捉到的事务传递给外部例化的
predictor
,该predictor
由UVM
参数化类uvm_reg_predictor
例化并集成在顶层环境中。 - 在集成的过程中需要将
adapter
与map
的句柄也一并传递给predictor
,同时将monitor
采集的事务通过analysis port
接入到predictor
一侧。 - 这种集成关系可以使得,
monitor
一旦捕捉到有效事务,会发送给predictor
,再由其利用adapter
的桥接方法,实现事务信息转换,并将转化后的寄存器模型有关信息更新到map
中。 默认情况下,系统将采用显式预测的方式,这就要求集成到环境中的总线
UVC monitor
需要具备捕捉事务的功能和对应的analysis port
,以便于同predictor
连接。寄存器怎么配置,adapter怎么集成
你在做验证时的流程是怎么样的,你是怎么做的。
对于流程的话
- 首先第一步我会先去查看
spec
文档,将模块的功能和接口总线时序搞明白,尤其是工作的时序,这对于后续写TB
非常重要; - 第二步我会根据功能点去划分我的
TB
应该怎么搭建,我的case
大致会有哪些,这些功能点我应该如何去覆盖,时序应该如何去检查,总结列出这样的一个清单; - 第三步开始去搭建我们的
TB
,包括各种组件,和一些基础的sequence
还有test
,暂时先就写一两个基础的sequence
,然后还有一些环境配置参数的确定等,最后能够将TB
正常运行,保证无误; - 第四步就是根据清单去编写
sequence
和case
,然后去仿真,保证仿真正确性,收集覆盖率; - 第五步就是分析收集的覆盖率,然后查看覆盖率报告去分析还有哪些没有被覆盖,去写一些定向
case
,和更换不同的seed
去仿真; - 第六步就是回归测试
regression
,通过不同的seed
去跑,收集覆盖率和检测是否有其它bug
; - 第七步就是总结
验证流程,验证环境怎么搭
- 验证流程:
- 看
spec
文档和协议,将DUT
的功能和接口总线时序搞明白 - 制定验证计划和测试点分解
- 写
VIP
或者是用别人给的VIP
,搭建验证环境和TB
,包括各种组件,各个模块的pkg
,基础的sequence
还有test
,暂时先就写一两个基础的sequence
,然后还有一些环境配置参数的确定等,最后能够将TB
正常运行,保证无误; - 根据测试点编写
sequence
和case
,然后去仿真,保证仿真正确性,收集覆盖率; - 分析收集的覆盖率,然后查看覆盖率报告去分析还有哪些没有被覆盖,去写一些定向
case
,和更换不同的seed
去仿真; - 回归测试
regression
,通过不同的seed
去跑,收集覆盖率和检测是否有其它bug
; - 总结
- 验证环境的搭建:
driver
给 DUT
发送激励,montior
监测 DUT
输出的数据,参考模型( reference model
)能实现与 DUT
相同的功能,scoreboard
把 monitor
接受到的数据和 reference model
的输出数据进行比对,如果比对成功就表示 DUT
能完成设计的功能,
你的验证环境是什么?目录结构是什么样的
我是使用UVM
验证方法学搭建的TB
,然后在VCS
平台进行仿真的。目录结构的话:主要由RTL
文件、doc
文件、tb
文件、sim
文件、script
文件这几部分。
你在进行验证的过程中,碰到过什么难点,重点是什么呢?
- 刚开始的难点还是TB的搭建,想要搭建出一个可重用性很高的TB,配置灵活的TB还是有一定困难,对于哪些参数应该放在配置类,哪些参数应该放在事务类的抉择,哪些单独配置。
- 除此之外,还有就是时序的理解,这对于
driver
和monitor
还有sequence
和assertion
的编写至关重要,只有正确理解时序才能编写出正确的TB
。 - 最后就是实现覆盖率的尽可能高,这也是比较困难的,刚开始的
case
好写,也比较快就可以达到较高的覆盖率,但是那些边边角角的case
需要自己去琢磨,去分析还需要写什么case
。这些难点就是重点,还要能够自动化监测判断是否正确。
你发现过哪些验证过程中的 bug,如何发现的,怎么解决的?
这个问题面试的时候经常问,建议面试之前考虑一下,再做决定
IP怎么写?
- 阶段1(定义)。
- 功能特性提取
- 特性覆盖率创建及映射
- VIP的架构
- 阶段2(VIP基本搭建)
- driver,sequencer,monitor (少量特性实现)。
- 实现基本的端到端的sequence
- 阶段3(完成monitor与scoreboard)
- 完成monitor -100%实现(checkers,assertions)
- 完成scoreboard -100%实现(数据完整性检查)
- 在monitor中,完成监测到的transaction与function coverage实现映射。
- 为映射更多的基本功能覆盖率,创建其它sequences。
- 阶段4(扩充test和sequence阶段)
- 实现更多sequences,从而获得80%的功能覆盖率
- 阶段5(完成标准)
- Sequence最终可以实现100%的功能覆盖率。
- 回归测试结果和最终的总结报告。
fifo深度计算https://blog.csdn.net/u011412586/article/details/10241585
异步复位与同步复位https://blog.csdn.net/qq_41634276/article/details/101078454
低功耗https://blog.csdn.net/wangkai_2019/article/details/106854081
亚稳态https://blog.csdn.net/CLL_caicai/article/details/104625791
奇数分频https://zhuanlan.zhihu.com/p/30282654
(N-1)/2 N-1
https://blog.csdn.net/weixin_43755290/article/details/108734704
状态机
摩尔状态机 输出与输入变量无直接关系的时序逻辑电路
米利状态机 输出与输入变量直接相关的时序逻辑电路
LFSR
cache
上采样 低通抗混叠滤波器,滤掉高频镜像
二进制转格雷码
c语言编译存储空间
- (1)栈(stack):由编译器进行管理,自动分配和释放,存放函数调用过程中的各种参数、局部变量、返回值以及函数返回地址。操作方式类似数据结构中的栈。
- (2)堆(heap):用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的。正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则程序结束时系统自动回收。注意:这里的“堆”并不是数据结构中的“堆”。
- (3)全局(静态)存储区:分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。
- (4)文字常量区:存放常量字符串。程序结束后由系统释放。
- (5)程序代码区:存放程序的二进制代码 Text
sv
一、数据类型
1.1 内建数据类型
- 有符号类型:byte、shortint、int、longint、integer;
- 无符号类型:bit、logic、reg、net-type(eg.wire、tri)按照位数定义的类型都是无符号,reg[31:0]
1.1.1 逻辑(logic)类型
四值逻辑:0、1、x、z
integer、logic、reg 、net-type、time
system verilog 对 reg 数据类型进行优化,使其除了作为一个变量外,还可以被连续赋值、门单元和模块所驱动,这种数据类型称为logic。
任何使用线网的地方均可使用logic,但是不能有多个结构性的驱动,例如对双向总线建模的时候。logic只能有一个驱动。
1.1.2 双状态数据类型
只有 0,1 两个状态
数据类型 | 有无符号 | 位数 |
---|---|---|
bit | 无 | 1 |
byte | 有 | 8 |
shortint | 有 | 16 |
int | 有 | 32 |
longint | 有 | 64 |
1.2 定宽数组
1.2.1 数组的声明和初始化
数组以0为搜索下界,声明时可以只给出数组宽度。
1 | int lo_hi [0:15]; // 16 |
在变量后面指定维度的方式创建多维定宽数组。
1 | // 定义8行4列的数组 |
1 | int ascend [4] = `{0, 1, 2, 3}; // 初始化数组 |
如果从一个越界的地址中读取数据,那么将返回数组元素类型的缺省值,对于一个元素为四状态类型的数组,返回x,对于双状态类型的数组,返回0,
合并类型与非合并类型
1 | bit [3][7:0] b_pack; // 合并,占用一个字空间 一个字空间32位 |
四值逻辑的一位数据需要 2位 存储空间
1 | logic [3][7:0] l_pack; // 需要 24*2=48位 存储空间 |
1.2.2 基本数组操作
for 和 foreach
1 | bit [31:0] src[5], dst[5]; |
复制和比较
直接利用赋值符号”=” 进行复制,利用”==” “!=”进行比较
1 | bit [31:0] src [5] = `{0, 1, 2, 3, 4}, |
1.3 动态数组
1 | int dyn[], d2[]; // 声明动态数组 |
1.4 队列
队列声明 [$] ,队列元素的标号从0到$
队列不使用new[ ]创建空间, 使用队列的方法增减元素,一开始其空间为0
push_back( ) 和 pop_front( )
1 | int j - 1, q2[$] - {3, 4}, q[$] - {0, 2, 5}; // 队列赋值不使用` |
1.5 关联数组
1 | bit [63:0] assoc[int], idx = 1; |
1.6 结构体
struct 创建结构
和 typedef 创建新的类型,并用新类型声明更多变量
1 | struct {bit [7:0] r, g, b;} pixel; // 创建一个pixel的结构体 pixel是变量名 |
1.7 枚举类型
enum
1 | typedef enum {INIT, DECODE, IDLE} fsmstate_e; |
枚举类型可以直接给int赋值,int不能赋值给枚举类型(需要转换)
1.8 字符串
string
1 | string s; |
1.9接口类型
interface
可以在接口 声明modport 来约束不同模块连接时的信号方向,在模块例化时,可以选择连接到端口中具体的某一个modport,降低方向连接错误的可能,避免信号多驱动的情况。
1 | interface main_bus; |
二、随机化
三、覆盖率
代码覆盖率
断言覆盖率
功能覆盖率
真题
multicycle path
https://aijishu.com/a/1060000000206169
设置输入延迟(input delay)
https://blog.csdn.net/Reborn_Lee/article/details/85052053