路科验证的个人空间 https://blog.eetop.cn/1561828 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

SV组件实现篇之三:激励器的随机化

已有 4647 次阅读| 2016-12-25 23:44 |个人分类:验证系统思想|系统分类:芯片设计

约束块
约束块的存在就是为了限制验证时的激烈,给出合理的边界。一个约束块是包含一些表达式用来限制一个随机成员的取值范围,或者是多个成员之间的关系。约束块可以同其它成员一样通过类来继承,这也使得通过不同类层次的继承实现约束的分层。

通常,我们将约束块定义在类中。如果一个类的随机成员没有其相应的约束块,那么这个随机成员就“无法无天”了吗?其实,除了类自身可以提供约束块(内部约束块),我们在随机化对象时仍然有机会再次提供额外的约束(外部约束块),通过这两种约束块我们可以灵活地控制成员的随机化。

关于约束块的分类,我们将其分为以下几种:
  • 集合成员约束:通过inside操作符产生一个值得集合,各个值的选取机会是相等的。
  • 权重分布约束:通过dist操作符产生权重分布,使得不同值选取机会按照权重值分布。
  • 唯一性约束:通过unique操作符使得在组内成员在随机化之后各不相同。
  • 条件约束:通过->if-else的关系操作符来使得只在某些条件下约束成立。
  • 迭代约束:通过foreach使得数组成员通过索引表达式和循环变量完成约束。

在这里,我们将省略具体约束块的语法使用情况,读者可以参考SV的标准手册或者语言入门参考书来学习和回顾约束块的部分。

我们这里需要另外点明的是,关于使用约束块的一些注意事项:
  • 求解器对于所有的约束块是同时解析并且求解的,并非按照从前到后的定义顺序。
  • 约束块内的关于变量之间的关系限定是双向的,譬如constraint c1 {foreach(arr[i]) arr[i].cmd == WR -> arr[i].cmd_addr < 'h10;}的限定中,指明了arr的每个成员,如果其cmd是WR,那么cmd_addr应该小于'h10;同时,如果cmd_addr小于'h10,那么cmd应该为WR。这表明了两个变量之间的限定是相互的,并不是只有cmd_addr依赖于cmd的取值。
  • 约束块最后的综合的结果不能过约束(over-constrained),即没有任何可以满足约束块的数对组合,否则求解器无法找到合适的解,即最终随机化会失败。
  • 约束块只支持2值的随机,即4值逻辑无法参与到约束块的限定中,同时4值逻辑的操作符例如===和!==也不能参与到约束块定义中。
  • 通过solve...before可以引导随机值概率的分布。不过我们不建议使用solve...before,除非你对某些值出现的概率不满意,而且也知道solve...before可以影响的概率最终结果。
  • 如果一些约束块内实现较复杂的约束限制,可以通过方法调用来实现复杂约束的计算

介绍完约束块使用的注意事项以后,我们再将约束块的使用进行如下分类并举例说明:
  • 内部约束和外部约束
  • 硬约束和软约束
  • 类约束和系统约束


内部约束和外部约束
与类中定义的约束块(内部约束)相对的是外部约束,或者也叫做内嵌约束(in-line constraint)。

我们将之前的类test_wr做了些修改:

class test_wr extends basic_test;

incacc acc;

task test(stm_ini ini);
super.test(ini);
acc = new();
assert(acc.randomize() with {arr.size() == 3;});
$display("test_wr::test");
ini.ts = new[acc.arr.size()];
ini.ts = acc.arr;
acc.print();
endtask

endclass

在对acc进行随机化时,通过randomize() with语句来添加额外的约束,我们可以称之为外部约束(相对于在类内部定义的约束)。则acc.arr的动态数组会被随机化为包含3个元素。

硬约束和软约束

如果我们再将上述的例子进行细微修改:

class test_wr extends basic_test;

incacc acc;

task test(stm_ini ini);
super.test(ini);
acc = new();
assert(acc.randomize() with {arr.size() == 5;});
$display("test_wr::test");
ini.ts = new[acc.arr.size()];
ini.ts = acc.arr;
acc.print();
endtask

endclass

要求acc.arr数组大小为5,但是这一外部限定与原先的约束incacc::c5 {arr.size() >= 2 && arr.size() <= 4;}是相冲突的。默认情况下,内部约束的定义均为“硬约束”,这种约束如果遇到外部约束或者其它约束与之发生冲突时,求解器会认为约束之间冲突,且没有优先级的差异,那么则无法找出满足条件的解。那么如果要引入约束之间的“优先级”,我们这里引入了“软约束”的概念。可以通过soft来标记一些约束,而后来如果一些约束与这些软约束发生冲突时,则以硬约束优于软约束的方式来求解,如果没有发生冲突,那么求解器会寻找一组满足硬约束和软约束的解。

所以,我们将incacc::c5修改为:

class incacc;
...

constraint c5 {soft arr.size() >= 2 && arr.size() <= 4;};

...
endclass

这样在将incacc::c5约束块修改为软约束之后,我们将可以生成一个大小为5的acc.arr数组。

类约束和系统约束
与在类中定义约束,并且通过对象调用随机化函数randomize()相对的是,一些细碎的场景并不需要像类那么全面支持随机约束的方式,而是需要一种更简单的机制来随机化一些在类之外的变量。于是,系统随机std::randomize()的方法(或者称之为域随机(scope randomize function)),可以让用户在当前的作用域中无需定义类和例化对象就可以完成对变量的随机化。

例如下面这个例子,我们可以将在模块tb中定义的变量arrsize通过系统随机和约束的方式来完成随机化:

module tb;

...

initial begin
int arrsize;
$display("arrsize = %0d before system randomization", arrsize);
std::randomize(arrsize) with {arrsize >= 2 && arrsize <= 4;};
$display("arrsize = %0d after system randomization", arrsize);
end

endmodule

输出结果:
# arrsize = 0 before system randomization
# arrsize = 3 after system randomization


随机化控制
在对对象进行随机化时,我们可以通过下面几种方法来实现随机化的控制
  • 随机化个别变量
  • 打开或者关闭随机属性
  • 打开或者关闭约束块

随机化个别变量
用户可以在对象调用randomize()时传递进入一些变量的子集,这样就会只随机在子集中的变量。只有在子集中的变量才会被随机,而不在其中的则不会被随机化,同时,在类中定义的约束块仍然起作用。譬如,下面通过acc.randomize(null)来告诉不对acc中所有的变量(即acc.arr)随机化,所以在acc随机化之后,其内的acc.arr并没有被随机化,即仍然保持空的状态,没有任何一个元素。

class test_wr extends basic_test;

incacc acc;

task test(stm_ini ini);
...
assert(acc.randomize(null));
...
endtask

endclass


打开或者关闭随机属性
同样的,如果不想对acc.arr随机化,也可以整体关闭对象的随机属性,或者关闭对象中某些变量的随机属性。当然,可以关闭,也就可以再次打开。

class test_wr extends basic_test;

incacc acc;

task test(stm_ini ini);
...
// acc.rand_mode(0); // 关闭对象acc内所有变量的随机属性
acc.arr.rand_mode(0); // 关闭对象acc的成员arr的随机属性
assert(acc.randomize());
...
endtask

endclass

这样通过rand_mode()可以灵活控制对于整个对象或者其个别成员的随机属性的控制。上面的例子仍然使得acc.arr在执行完随机化函数之后没有被随机化,因为其随机属性被关闭了。


打开或者关闭约束块
同“打开或者关闭随机属性”相似的是,SV也可以整体关闭对象的约束块,或者关闭对象中某些约束块。

class test_wr extends basic_test;

incacc acc;

task test(stm_ini ini);
...
acc.c6.constraint_mode(0);
assert(acc.randomize());
...
endtask

endclass

上面的例子由于在acc随机化之前关闭了acc.c6这个约束块,使得关于acc.arr内每个元素的cmd_addr递增的约束被关闭。这样,从输出结果来看,acc.arr其中各个元素的地址之间没有递增的关系了:

# test_wr::test
# arr[0].cmd_addr = 'h8
# arr[1].cmd_addr = 'h18
# arr[2].cmd_addr = 'h4
# arr[3].cmd_addr = 'h18


随机化的稳定性
对于随机化而言,最重要的一项要求便是在多次仿真运行中,同一个程序通过同一个随机种子(seed)是否可以产生相同的随机序列。如果这一项要求无法保证,那么在某次随机序列发生设计检查错误时,再要回溯该测试序列将非常困难。同时,我们在日常工作中,也可能会修改局部代码,但我们也并不希望小幅度的代码修改会大范围地影响程序生成的随机测试向量,而是尽可能地保持它的随机行为。

对于SV而言,每次产生的随机数实际上都是通过RNG(随机数生成器, random number generator)实现的。RNG每次通过一个随机种子(seed)来生成随机数,而且RGN生成随机数也是确定性的,是可以预测的。如果连续两次的随机过程中,RNG采取了相同的种子,那么生成的随机序列也是相同的。

这里我们需要考虑的是,在测试平台中,由于有多个线程和多个对象,它们可能会有各自的随机产生过程,而随机的稳定性就要保证这些线程和对象的整体稳定性,并且从下面这些地方来满足这一要求:
  • 初始RNG
  • 线程稳定性
  • 对象稳定性
  • 人工随机

初始RNG
每一个模块(module)实例、接口(interface)实例、程序(program)实例和包(package)都有自己的初始RNG,每个初始RNG接收被传入仿真的初始种子,进而来创建必要的线程。也就是说,每一个独立的模块、接口等,都有自己的初始RNG,而且这些RNG采用共同的初始种子。

例如下面的代码:
module m1;
initial $display("m1::proc1 randnum %0d", $urandom_range(0, 100));
initial $display("m1::proc2 randnum %0d", $urandom_range(0, 100));
endmodule

module m2;
initial $display("m2::proc1 randnum %0d", $urandom_range(0, 100));
initial $display("m2::proc2 randnum %0d", $urandom_range(0, 100));
endmodule


module top;
m1 i1();
m2 i2();
endmodule

如果在仿真时使用默认的种子即seed = 0,则输出结果是:
# m1::proc1 randnum 2
# m1::proc2 randnum 67
# m2::proc1 randnum 2
# m2::proc2 randnum 67

如果我们在仿真时将种子值修改为seed = 1,则输出结果是:
# m1::proc1 randnum 4
# m1::proc2 randnum 16
# m2::proc1 randnum 4
# m2::proc2 randnum 16

从这个例子可以看到,实例m1和实例m2使用的种子都是相同的,而且它们有着各自的初始RNG,通过相同的初始RNG,结合相同的种子,就产生了一致的随机数序列。

线程稳定性
对于上面提到的每一个独立的初始RNG,其内又会产生多个动态的线程,而父线程又会产生子线程。对于这些层次化的线程,它们遵循的原则是:
  • 子线程RNG的种子来源于父线程
  • 并处于一个父线程下得子线程之间,按照创建(而非执行)的先后顺序依次从父线程得到RNG种子

再看看下面这个例子:
module m1;
//initial begin: proc1
// $display("m1::proc1 randnum %0d", $urandom_range(0, 100));
//end
initial begin: proc2
//$display("m1::proc2.sub1 randnum %0d", $urandom_range(0, 100));
$display("m1::proc2.sub2 randnum %0d", $urandom_range(0, 100));
//$display("m1::proc2.sub3 randnum %0d", $urandom_range(0, 100));
end
//initial begin: proc3
// $display("m1::proc3 randnum %0d", $urandom_range(0, 100));
//end
endmodule

执行的结果是:
# m1::proc2.sub2 randnum 2

如果我们打开m1::proc3,那么输出的结果是:
# m1::proc2.sub2 randnum 2
# m1::proc3 randnum 67

如果再将m1::proc1打开,那么输出的结果是:
# m1::proc1 randnum 2
# m1::proc2.sub2 randnum 67
# m1::proc3 randnum 0

之所以产生这样的结果是因为, 首先proc1/proc2/proc3三个线程都并处于父线程即m1的初始RNG下,当先打开proc3之后,由于proc2先于proc3创建,所以依旧是最先从父线程拿到RNG的种子,由于种子没有变化,因此,proc2.sub2的输出结果依然没有变化。但是,当把proc1打开以后,那么proc1将先于proc2创建和取得RNG种子,所以,proc1的输出结果则变为之前proc2.sub2的输出结果(因为取得本该属于proc2.sub2的种子),而proc2.sub2的取得了本应属于proc3的种子,等到proc3时只能依次取得初始RNG产生的第三个种子。所以,这种对于同一个父类线程下子线程取得种子的方式和生成随机数的结果是确定可预测的。

接下来,我们将上面的proc1和proc3关闭,再单独来看proc2中的三个子句sub1/sub2/sub3。这三个子句段的随机数产生也类似于proc1/proc2/proc3创建和获取RNG种子的方式。如果只留下proc2.sub2,那么执行的结果依然是:
# m1::proc2.sub2 randnum 2

而当打开proc2.sub3时,输出的结果则是:
# m1::proc2.sub2 randnum 2
# m1::proc2.sub3 randnum 88

由于proc2.sub3于proc2.sub2之后创建,那么proc2.sub2的随机结果依然没有变化。当打开proc2.sub1后,则随机数产生的结果会发生改变:
# m1::proc2.sub1 randnum 2
# m1::proc2.sub2 randnum 88
# m1::proc2.sub3 randnum 2

pro2.sub1先于proc2.sub2得到RNG种子,即产生了本应由proc2.sub2产生的数;这个情况也同样适用于proc2.sub2先于proc2.sub3得到种子;最后,proc2.sub3只能得到父线程proc2的RNG产生的第三个种子。

对象稳定性
同父线程产生子线程,并且按照创建顺序给予种子类似的是,父线程也可以创建新的子对象,而子对象的种子也由父线程给予。它们遵循的规则是:
  • 子对象的RNG的种子来源于父线程
  • 并处于同一父线程的子对象,将按照其创建的先后顺序依次从父线程得到RNG种子

再来看看下面这个例子:
module m1;
//initial begin: proc1
// $display("m1::proc1 randnum %0d", $urandom_range(0, 100));
//end

initial begin: proc2
c1 i1;
//$display("m1::proc2.sub1 randnum %0d", $urandom_range(0, 100));
i1 = new();
assert(i1.randomize());
$display("m1::proc2.i1 randnum %0d", i1.randnum);
end

endmodule

无论是打开proc1,还是打开proc2.sub1,都将使得proc2.i1对象得到不同的RNG种子。所以,对此,我们的建议是应该尽量将新创建的对象置于之前创建的对象之后,或者将新的线程置于之前已经创建的线程之后。只有通过这种方式,才可以尽量保证之前创建的线程和对象得到没有变化的种子,进而产生没有变化的随机向量

人工随机
尽管我们可以小心地将新添加的代码放置到源代码线程或者对象的后面,但是这仍然不是一种可以确保万无一失的方法。如果读者确实需要保证每次产生的向量一致,那么我们可以通过srandom的方法来人工设置某个线程或者某个对象的RNG种子,进而确保它们产生的随机向量是一致的。

例如下面这段代码,无论是proc1还是proc2.i1,都可以通过srandom的设置来使得它们产生的的值是确定的:

class c1;
rand int randnum;
constraint cstr {randnum inside {[0:100]};};
function new(int seed);
srandom(seed);
endfunction
endclass


module m1;
initial begin: proc1
process::self.srandom(10);
$display("m1::proc1 randnum %0d", $urandom_range(0, 100));
end

initial begin: proc2
c1 i1;
i1 = new(10);
assert(i1.randomize());
$display("m1::proc2.i1 randnum %0d", i1.randnum);
end
endmodule

从这段代码可以看到,通过通过在线程内部设置自己的随机种子,或者通过对象自己来修改对象自己随机化的种子,这种人工设定随机种子的方式使得可以确保产生的激励向量是固定的。需要注意的是,对象的种子可以自己设定或者由其他线程来设定,而线程的种子必须在线程内部设定,例如上面的代码中,proc1中通过调用process::self.srandom()来为自己设定随机种子。


随机化的流程控制
在对某一个对象通过randomize()进行随机化时,伴随着随机化的次数,我们会发现随机化产生的数组对于覆盖整个可取值域的贡献逐渐下降,这是因为每次的随机化之间都是各自独立的。为了让这种随机生成对于值域覆盖的贡献率维持在高效率,我们期望后面随机化的生成可以从之前的结果中得到“启示”,进而指导接下来的随机生成。于此,我们可以在每次随机化后将生成的随机数装入一个历史“容器”中,让接下来的随机化有新的镜子可以对照,再考虑该生成哪些有意义的数据。

对于上面的这个案例,我们就可以在对象自己的随机前(pre_randomize())函数和随机后(post_randomize())函数中做一些处理。从执行顺序来看,是pre_randomize()先于randomize(),而randomize()先于post_randomize()。在每次调用randomize()函数时,对象会自动先后调用pre_randomize()和post_randomize()。

我们来看看下面这个例子:

class c1;
rand int randnum;
int hist[$];
constraint cstr1 {randnum inside {[0:10]};};
constraint cstr2 {!(randnum inside {hist});};
function void post_randomize();
hist.push_back(randnum);
endfunction
endclass


module m1;

initial begin
c1 i1;
i1 = new();
repeat(10) begin
assert(i1.randomize());
$display("m1::proc2.i1 randnum %0d", i1.randnum);
end
end
endmodule

通过c1::post_randomize()的定义,保证了每次生成的c1::randnum都被记录进历史容器c1::hist中,而在后续新的随机化过程中,c1::cstr2的限制使得每次生成的c1::randnum均不同于之前生成的数字。所以,上述代码的输出结果为:
# m1::proc2.i1 randnum 5
# m1::proc2.i1 randnum 9
# m1::proc2.i1 randnum 2
# m1::proc2.i1 randnum 1

点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 254

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

站长推荐 上一条 /1 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-5-13 02:15 , Processed in 0.021123 second(s), 12 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部