- 第6章 随机化
6.1 介绍
■ CRT(随机测试法);
■ CRT由两部分组成:使用随机的数据流为DUT产生输入的测试代码,以及伪随机数发生器(PRNG)的种子(seed),只要改变种子的值,就可以改变CRT的行为;
6.2 什么需要随机化
6.2.1 器件配置
6.2.2 环境配置
6.2.3 原始输入数据
■ 准备好相关的事务类;
6.2.4 封装后的输入数据
6.2.5 协议异常、错误(error)和违规(violation)
6.2.6 延时
■ 测试平台中的每一个测试都使用随机的、有效的延时,以便发现设计中的Bug;
6.3 SV中的随机化
■ 受约束的随机激励是在事务级产生的,通常不会一次只产生一个值;
6.3.1 带有随机变量的简单类
■ rand bit [31:0] src ,dst ,data[8] ;// rand 修饰符,表示每次随机化这个类时,这些变量都会赋一个值;(类似掷骰子,每掷一次,都会产生一个新的数字,或和现在一样的数字)
■ randc bit[7:0] kind ; // randc类型,表示周期随机性,即所有可能的值都赋过后随机值才可能重复;(类似发牌,洗牌后再按另一个顺序随机地抽出牌)
//src的约束
constraint c {src > 10 ;src < 15}
◆ 约束是一组用来确定变量的值的范围的关系表达式,表达式的值永远为真;
◆ 约束表达式是放在括号“{}”中,而没有放在begin ...end之间,这是由于这段代码是声明性质,而不是程序性质;
// assert (p. randmize( ));
◆ randmize()函数在遇到约束方面的问题时返回0;
◆ 使用断言(assert)来检查randomize()结果;
■ 不能在类class的构造函数里随机化对象,因为在随机化前可能需要打开或关闭约束、改变权重,甚至添加新的约束。
■ 构造函数用来初始化对象的变量,不能在这里调用randomize(函数);
■ 类里的所有变量都应该是随机的(random)和公有的(public),这样测试平台才能最大程度地控制DUT;
6.3.2 检查随机化(randomize)的结果
■ randomize( )函数为类class里所有的rand和randc类型的随机变量赋一个随机值,并保证不违背所有有效的约束;
6.3.3 约束求解
■ 约束表达式的求解是有SV的约束求解器完成的;
6.3.4 什么可以被随机化
■ SV可以随机化整型变量,即由位组成的变量,尽管只能随机化2值数据类型,但位也可以是2值或4值类型;
6.4 约束
■ 每个表达式里至少有一个变量必须是rand或randc类型的随机变量;
6.4.1 什么是约束
例 6.3 受约束的随机类
6.4.2 简单表达式
■ 变量可以在多个表达式里使用;
■ 在一个表达式中,最多只能使用一个关系操作符(<、<=、= =、>=、>)
例 6.4 不正确的排序约束
class order;
rand bit[7:0] lo ,me ,hi ;
constraint bad {lo<med<hi;} // 错误
endclass
例6.6 固定顺序的约束
class order;
rand bit[15:0] lo, med , hi ;
constraint good {lo<med ; med< hi;}
endclass
6.4.3 等效表达式
■ 在约束块里只能包含表达式,所以在约束块里不能进行赋值;
■ 相反,应该用关系运算符为随机变量赋一个固定的值,例如 len == 42;
■也可以在多个随机变量之间使用更复杂的关系表达式,例如len = =header.addr_mode * 4 + payload.size( );
6.4.4 权重分布
■ dist操作符允许产生权重分布,这样某些值的选取机会比其他值更大一些;
■ dist操作符带有一个值的列表以及相应的权重,中间用:=或:/分开;
■ 值或权重可以是常数或变量;
■ 值可以是一个值或值的范围,例如[lo:hi];
■ 权重不用百分比表示,权重的和也不必是100;
■ := 操作符表示值范围内的每一个值的权重是相同的, :/操作符表示权重要均分到值范围内的每一个值;
■ 可以使用权重变量来随时改变值的概率分布,甚至可以把权重设为0,从而删除一个值;
例6.8 动态改变权重
6.4.5 集合(set)成员和inside运算符
■ 可以使用inside运算符产生一个值的集合,SV在值的集合里随机取值时,各个值的选取机会是相等;在集合里也可以使用变量;
例6.9 随机值的集合
rand int c ; // 随机变量
int lo,hi; //作为上限和下限的非随机变量
constraint c_range {
c inside {[lo:hi]} ; //lo <=c 并且c<=hi
}
■ 可以使用$来代表取值范围里的最小值和最大值;
rand bit[6:0] b ;// 0< =b <=127
rand bit[5:0] e; //0 <=e <=63
constraint c_range {
b inside {[$:4],[20:$]}; //0<= b <=4 || 20<= b <=127
e inside {[$:4], [20:$]}; //0<=e <=4 || 20<=e<=63
}
■ 若想选择一个集合之外的值,只需要用取反操作符!对约束取反;
constraint c_range{
!(c inside {[lo:hi]}); //c<lo或c>hi
}
6.4.6 在集合里使用数组
■ 把集合里的值保存到数组里后,就可以使用这些值;
6.4.7 条件约束
■ SV支持两种关系操作:—> 和if-else
■ —> 操作符可以产生和case操作符效果类似的语句块,它可以用于枚举类型的表达式;
■ 在约束块中用{ }把多个表达式组成一个块,而在程序性代码里使用begin...end关键词;
6.4.8 双向约束
■ 约束块是声明性的代码,是并行的,所有的约束表达式同时有效;
■ SV的约束是双向的,即它会同时计算所有的随机变量的约束;
6.4.9 使用合适的数学运算来提高效率
6.5 解的概率
6.5.1 没有约束的类
■ 一个类class里面的随机数有很多个解,要想得到某一种类型的解,可能需要上千次的随机化;
6.5.2 关系操作
■ 某一变量的值依赖于另一变量的值;
6.5.3 关系操作和双向约束
6.5.4 使用solve...before 约束引导概率分布
■solve...before约束不会改变解的个数,只会改变各个值的概率分布;
■ 除非对某些值出现的概率不满意,否则不要使用solve...before;
6.6 控制多个约束块
■ 使用内建的constraint_mode( )函数打开或关闭约束;
■ handle.constrain.constrain_mode( )控制一个约束块,handle.constraint_mode()控制对象的所有约束;
eg:p.c_short.constraint_mode(0) ;//禁止c_short约束产生长包
p.constraint_mode(0); //禁所有的约束
6.7 有效性约束
■ 设置多个约束以保证随机激励的正确性,是一总很好的随机化技术,被称为“有效性约束”;
■ 例如,总线的“读-修改-写”命令只允许操作长字数据长度;
6.8 内嵌约束
■ SV允许使用randomize()with来增加额外的约束;
■ 在使用randomize()with语句时,常犯的错误是使用()括号内嵌的约束,而没有使用{},即,约束快应该使用{},所以内嵌的约束块也应该使用{ };
eg:Transaction t;
assert(t.randomize( )with {addr >=50;addr <= 1500;data< 10;});
6.9 pre_randomize和post_randomize函数
■ 用于在随机化之前做一些动作,比如设置类class里的一些非随机变量(例如上下限、权重),或者随机化之后需要计算随机数据的误差校正位;
■ pre_randomize和post_randomize是void类型函数,没有返回值,不消耗时间;如果想在pre_randomize和post_randomize函数里调用调试程序,则调试程序必须是函数类型;
6.9.1 构造鱼缸型分布
■ 某些应用中可能需要一些非线性的随机分布,如短包或长包比中等长度的包更容易发现缓冲器缓冲溢出类型的错误;
eg: $dist_exponential非线性分布函数;
6.9.2 关于void函数
■Pre_randomize和post_randomize函数,只能调用其他函数,不能调用消耗时间的任务,所以在执行randomize()函数的期间无法产生一段延时;
6.10 随机数函数
- $random( ) ---平均分布,返回32位有符号随机数;
- $urandom( )---平均分布,返回32位无符号随机数;
- $urandom_range( ) ---在指定范围内的平均分布;
- $dist_exponential( ) --指数衰落;
- $dist_normal ( ) ---钟型分布;
- $dist_poisson( ) ---钟型分布;
- $dist_uniform( )---平均分布;
例6.32 使用$urandom_range函数
a=$urandom_range(3,10); //值的范围是3~10
a=$urandom_range(10 , 3) ; //值的范围是3~10
b=$urandom_range(5); //值的范围是0~5
6.11 约束的技巧和技术
6.11.1 使用变量的约束
6.11.2 使用非随机值
■ 如果只有几个变量需要修改,可以使用rand_mode函数把这些变量设置为非随机变量;
6.11.3 用约束检查值的有效性
■ 调用handle.randomize(null)函数时,systemverilog会把所有的变量当作非随机变量,仅仅检查这些变量是否满足约束条件;
6.11.4 随机化个别变量
6.11.5 打开或关闭约束
■ 可以使用条件操作符(—>或if - else)来构建由非随机变量控制的约束;
■ 针对复杂的约束,可以对没一种指令建立一套独立的约束,在使用时关闭其他所有的约束;
6.11.6 在测试过程中使用内嵌约束
6.11.7 在测试过程中使用外部约束
■ 函数的函数体可以在函数的外部定义,同样,约束的约束体也可以在类的外部定义;
6.11.8 扩展类
■扩展类里定义的约束的名字与基类里的约束名字相同,那么扩展的约束将取代基类的约束;
6.12 随机化的常见错误
6.12.1 小心使用有符号变量
■ 除非必要,不要在随机约束里使用有符号类型;//bit
6.12.2 提高求解器性能的技巧
■ 如果需要除以或乘以2的幂次方,则使用右移或左移操作;
6.13 迭代和数组约束
■ 用foreach约束和一些数组函数,可以改变值的分布形态;
6.13.1 数组的大小
■ $size( )函数,可以约束动态数组或队列的元素个数;
■ 使用inside约束可以设置数组大小的下限和是上限;
6.13.2 元素的和
■ 例如,可以使用sum()函数约束随机数组只有四个位有效,从而产生在十个周期内发送四个数据的脉冲信号。
这里要注意数组元素个数的保存精度。因为但比特元素的和正常情况下也是单比特。
6.13.3 数组约束的问题
■ 数组求和约束的三种方法:bad_sum1、bad_sum2、bad_sum3、bad_sum4
6.13.4 约束数组和队列的每一个元素
■ 使用foreach约束动态数组和队列;
6.13.5 产生具有唯一元素值的数组
6.13.6 随机化句柄数组
6.14 产生原子激励和场景
6.15 随机控制
6.16 随机数发生器
6.16 随机器件配置
■ 测试DUT的一个重要工作是测试DUT内部设置和环绕DUT的系统配置,测试应该随机化环境,这样才能保证尽可能测试足够多的模式;
- 第七章 线程及线程间的通信
■ 时序逻辑通过时钟来激活,组合逻辑的输出则随着输入的变化而变化;
■ 所有的并发活动,在Verilog的寄存器传输级上是通过initial和always块语句、实例化和连续赋值语句来模拟的;
■ systemverilog的调度器通过选择下一个要运行的线程来完成这种同步。
■ 每个线程总是会跟相邻的线程通信,常见的线程间通信有标准的Verilog事件、事件控制、wait语句、sv信箱和旗语等;
■ 线程的量级比进程小,其代码和存储区可共享。
7.1 线程的使用
■ 测试平台隶属于程序块,代码总是以initial块启动,从时刻0开始执行;
■ Verilog对语句有两种分组方式---使用begin...end或fork...join;
◆ begin ...end以顺序方式执行;
◆ fork ...join中的语句则以并发方式执行,必须等fork...join内的所有语句都执行完之后,才能继续块内后续的处理;
■ SV引入新的创建线程的方法---使用fork...join_none和fork...join_any语句;
■ 测试平台通过已有的结构如事件、@事件控制、wait和disable语句,以及新的语句元素(如旗语和信箱),来实现线程间的通信、同步以及对线程的控制;
7.1.1 使用fork ... join和begin ... end
■ 由于fork...join块里的代码都是并发执行的,所以带短时延的语句执行的比带长延时的语句早;
■ fork...join里面的语句执行未完,join后面的语句被阻塞;
7.1.2 使用fork...join_none来产生线程
■ 块内语句并行执行,join_none后面的语句同该模块同时执行;
■ join_none块后的那个语句的执行早于fork...join_none内的任何语句;
7.1.3 使用fork...join_any来产生线程
■ 块内语句并行执行,当块内有一条语句执行完后,join_any后面语句开始执行;
7.1.4 在类中创建线程
7.1.5 动态线程
■ 在SV中,可以动态地创建线程,而且不用等到它们都执行完毕;
7.1.6 线程中的自动变量
■ 在并发线程中务必使用自动变量来保存数值;
例7.9 不良代码:在循环中内嵌fork...join_none
program no_auto;
initial begin
for(int j = 0 ;j <3 ; j++)
fork
$write(j); //漏洞--得到的是最终的索引值
join_none
# 0 $display(“\n”);
end
endprogram
7.1.7 等待所有衍生线程
■ 在SV中,当程序中的initial块全部执行完毕,仿真器就退出了;
■ 有些线程运行时间比较长,可以使用wait fork语句来等待所有子线程结束;
例 7.14 使用wait fork等待所有子线程结束
task run_threads;
... 创建一些事务
fork
check_trans(tr1) ; //产生第一个线程
check_trans(tr2) ; //产生第二个线程
check_trans(tr3) ; //产生第三个线程
join_none
.... //完成其他的工作
//在这里等待上述线程结束
wait fork;
endtask
7.1.8 在线程间共享变量
■ 在包含所有变量使用的最小范围内声明所有的变量;
■ 对索引变量的声明应放在for循环内部而不是在程序或整个作用域的层级上;
■ 尽可能使用foreach语句;
7.2 停止线程
■ Verilog中的disable语句可以用于停止SV中的线程;
7.2.1 停止单个线程
例 7.16 停止一个线程
fork:timeout_block
begin
wait (bus.cb.addr = tr.addr);
$display(“@ %0t:Addr match %d” , $time , tr.addr);
end
join_any
disable timeout_block;
end
7.2.2 停止多个线程
■ SV引入disable fork语句能够停止从当前线程中衍生出来的所有子线程;
■ 应该使用fork...join把目标代码包围起来以限制disable fork语句的作用范围;
例 7.17 限制disable fork的作用范围
initial begin
check_trans( tr0) ; //线程0
//创建一个线程来限制disable fork的作用范围
fork //线程1
begin
check_trans(tr1); //线程2
fork //线程3
check_trans(tr2); //线程4
join
// 停止线程1-4,单独保留线程0
# (TIME_OUT/2) disable fork;
end
join
end
例7.18 可以使用带标号的disable来停止线程
initial begin
check_trans(tr0) ; //线程0
fork
begin:threads_inner
check_trans(tr1);
check_trans(tr2);
end
//停止线程2和3,单独保留线程0
#(TIME_OUT/2) disable threads_inner;
join
end
7.2.3 禁止被多次调用的任务
7.3 线程间的通信
■ 所有数据交换和控制的同步被称为线程间的通信(IPC),在SV中可使用事件、旗语和信箱来完成;
7.4 事件
■ Verilog事件可以实现线程的同步,在Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着,等待事件的变化;
■ 在verilog中,其它线程可以通过—>操作符来触发事件,解除对第一个线程的阻塞;
■ 在Verilog中,当一个线程在一个事件上发生阻塞的同时,正好另一个线程触发了这个事件,则竞争的可能性便开始了;
■ 在SV中,事件event成为同步对象的句柄,可以传递给子程序;
■ sv引入triggered( )函数,用于查询某个事件是否已被触发,包括当前时刻,线程可以在等待这个函数的结果,而不用在@操作符上阻塞;
■ 事件的触发有两种方式,边沿触发以及电平触发。
边沿触发只在触发的那一瞬间有效。电平触发相当于只要事件触发后,triggered就一直有效果。
7.4.1 在事件的边沿阻塞
7.4.2 等待事件的触发
■ 可以使用电平敏感的wait(e1 .triggered( ))来替代边沿敏感的阻塞语句@e1;如果事件在当前时间步已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止;
7.4.3 在循环中使用事件
■ 可以使用事件来实现两个线程的同步;
■ 如果在循环中使用wait(handshake.triggered( )),一定确保在下次等待之前时间可以向前推进,否则代码将进入一个零时延循环,原因是wait会在单个事件触发器上反复执行;
◆ wait(handshake . triggered( ));
◆ @handshake;
■ 如果需要在同一时刻发送多个通告,则就不应该使用事件,而应该使用其他内嵌排队机制的线程通信(IPC)方法,如旗语和信箱;
7.4.4 传递事件
■ SV中的事件event可以像参数一样,传递给子程序;
function new(event done); //从测试平台中传来事件
7.4.5 等待多个事件
■ 在所有线程按顺序完成的情况下,可以在父线程中使用for循环来等待每个事件;
■ 线程不按顺序完成,则创建一个新线程并从中衍生子线程,然后保证每个线程阻塞在每个发生器的一个事件event上;(可以使用wait fork)
例 7.27 使用wait fork等待多个线程
例7.28 通过对触发事件进行计数来等待多个线程
例 7.29 使用线程计数来等待多个线程
7.5 旗语
■ 使用旗语可以实现对同一资源的访问控制;
■ 旗语可被视为一个互斥体,用于实现对同一资源的访问控制;
■ 当测试平台中存在一个资源,如一条总线,对应这多个请求方,而实际物理设计中又只允许单一驱动时,便可使用旗语;
■ 在SV中,一个线程如果请求“钥匙”而得不到,则会一直阻塞,多个阻塞的线程会以先进先出(FIFO)的方式进行排队;
7.5.1 旗语的操作
■ 旗语有三种基本操作;
◆ 使用new方法可以创建一个带单个或多个钥匙的旗语;
◆ 使用get可以获取一个或多个钥匙,而put则可以返回一个或多个钥匙;
◆ 如果试图获取一个旗语而希望不被阻塞,可以使用try_get( )函数,返回1表示有足够多的钥匙,而返回0则表示钥匙不够;
例7.30 用旗语实现对硬件资源的访问控制
program automatic test (bus_ifc . TB bus) ;
semaphore sem; //创建一个旗语
....
task sendTrans ;
sem.get(1); //获取总线钥匙
@bus.cb ; //把信号驱动到总线上
bus.cb.addr <= t.addr;
...
sem.put(1); //处理完成时把钥匙返回
endtask
endprogram
7.5.2 带多个钥匙的旗语
■ 多个不同的请求混在一起,可以自己编写一个类,这样谁取得优先权会比较清楚;
■ 旗语就是一个类。这个类应该是Driver封装好的。
7.6 信箱
■ 对信箱的最简单理解是把它看成一个具有源端和收端的FIFO,源端把数据放进信箱,收端则从信箱中获取数据;
■ 信箱可以有容量上的限制,也可以没有;
■ 信箱满放则阻塞,信箱空取则阻塞;
■ 信箱mailbox是一种对象,必须调用new函数来进行实例化,例化是有一个可选的参数size,用以限制信箱中的条目,如果size是0或者没有指定,则信箱是无线大的,可以容纳任意多的条目;
■ 使用put 任务task可以把数据放入信箱里,而使用get任务task,则可以移除数据。
■ 如果信箱为满,则put会阻塞,信箱为空,则get会阻塞;
■ peek 任务可以获取对信箱里数据的拷贝而不移除它;
■ 可以在信箱里放入句柄,但不能是对象;
■ 务必在一个信箱里只放一种类型的数据;
“每个句柄都指向了不同的对象”的发生器称为“蓝图模式”
■ 在访问信箱时,可以使用try_get( )和try_peek( )函数避免出现阻塞,如果函数执行成功, 则会返回一个非零值,否则返回0;
7.6.1 测试平台里的信箱
7.6.2 定容信箱
■ 缺省情况下,信箱类似容量不限的FIFO;
■ 任何大于0的容量便可创建一个“定容信箱”;
7.6.3 在异步线程间使用信箱通信
7.6.4 使用定容信箱和探视(peek)来实现线程的同步;
7.6.5 使用信箱和事件来实现线程的同步
7.6.6 使用两个信箱来实现线程的同步
■ 对两个线程进行同步另一种方式是再使用一个信箱,把消费方的完成信息发回给生产方;
7.6.7 其他的同步技术
■ 通过变量或旗语来阻塞线程,同样可以实现握手;
■ 事件是最简单的结构,其次是通过变量阻塞;
■ 旗语相当于第二个信箱,但没有信息交换;
7.7 构筑带线程并可实现线程间通信的测试程序
7.7.1 基本的事务处理器
7.7.2 配置类
■ 配置类允许在每次仿真时对系统的配置进行随机化;
7.7.3 环境类
7.7.4 测试程序
7.8 结束语
■ 线程间可以使用事件、旗语、信箱,以及经典的@事件控制和wait语句来实现通信和同步;
■ disable命令可以用来中止线程;
- 第八章 面对对象编程的高级技巧指南
■ 继承通过增加新的特性提供了可重用性,并且不需要修改基类;
8.1 继承简介
8.1.1 事务基类
源地址
目的地址
变量
八个数据字
CRC变量
事务基类 显示内功子程序
子程序
计算CRC子程序
例 8.1 事务(Transaction)基类
class Transaction;
rand bit[31:0] src ,dst ,data[8] ; //随机变量
bit[31:0] crc ; //计算得到的CRC值
virtual function void calc_crc;
crc = src^dst^data.xor;
endfunction
virtual function void display (input string prefix = “ ”);
$display (“%s Tr : src = %h , dst = %h , crc = %h”,prefix , src ,dst ,crc );
endfunction
endclass
8.1.2 Transaction类的扩展
例 8.2 扩展的Transaction类
class Badtr extends Transaction;
rand bit bad_crc;
virtual function void calc_crc;
super.calc_crc( ); //计算正确的CRC (通过使用super前缀调用基类中的calc_crc函数)
if(bad_crc)crc = ~crc; //产生错误的CRC位
endfunction
virtual function void display(input string prefix = “”);
$write (“%sBadTr:bad_crc = %b,”,prefix , bad_crc);
super.display( );
endfunction
■ 可以调用上一层类class的成员,但SV不允许用类似super.super.new方式进行多层调用;
■ 将类中的子程序定义成虚拟virtual的,这样就可以在扩展类中重定义。这一点适用于所有的任务task和函数function,除了new函数;
8.1.3 更多的OOP术语
■ OOP中类的变量称为属性(property);
■ OOP中的任务task或者函数function称为方法(method);
■ 基类是不从任何其他类派生得到的类;
■