第六章. 随机化
6.1 哪些对象需要随机化
随机时需考虑设计输入的各个方面,器件配置,环境配置,原始输入数据,封装后的输出数据,协议异常,延时,事务状态,错误和违例等情况。
6.2 SV中的随机化
//带有随机变量的简单类
class Packet;
rand bit [31:0] src, dst, data[8];
randc bit [7:0] kind;
//src的约束
constraint c {src > 10; src < 15;}
endclass
Packet p;
initial
begin
p = new();
assert (p.randmize());
else $fatal(0, "Packet::randomize failed");
transmit(p);
end
使用rand/randc
修饰符修饰的变量为随机变量,表示每次随机化这个类时(调用randmize()函数),这些变量都会被随机赋一个值(满足约束条件)。randc
表示周期性随机,即当所有可能值没有全部被取到前不会出现数据重复的问题。约束是一组用来确定变量的值的范围的关系表达式。约束表达式式放在括号中,而没有放在begin-end
块之间,这是由于这段代码是声明性质的,而不是程序性质的。随机化函数randomize()在遇到约束方面的问题时返回0。
不能在类的构造函数里随机化对象,因为在随机化前可能需要打开或者关闭约束,改变权重,添加新的约束。**构造函数是用来初始化对象的变量,不能调用randmize()函数。**类中的所有变量都应该设置为随机的(random)
和公有的(public)
,这样测试平台才能最大程度地控制DUT。
randomize()
函数为类里所有的rand
和randc
类型的随机变量赋一个随机值,并保证不违背所有有效的约束。当你的代码中有矛盾的约束时,随机化过程会失败,所以一定要检测随机化的结果(可以使用立即断言)。如果不检查,变量可能会赋未知的值,导致仿真的失败。
**约束表达式的求解是由SV的约束求解器完成的。**求解器能够选择满足约束的值,这个值由SV的PRNG(伪随机数生成器,即随机种子确定后生成随机数可通过公式计算出来,产生的每个随机数概率相同)从一个初始值(random seed)产生。如果SV仿真器每次使用相同的初始值,相同的测试平台,那么仿真的结果也是相同的。各种仿真器的求解器都是不同的,因此使用不同的仿真器时受约束的随机测试得到的结果也有可能不同,甚至同一个仿真器的不同版本的仿真记过也不相同。
6.2.2 哪些数据可以被随机化
可以随机化整型,随机只能产生二值数据类型(0/1),尽管随机变量类型为四值类型。可以使用整数和位矢量,但是不能使用随机化字符串,或在约束中指向句柄。
6.3 约束
每个表达式里至少有一个变量必须是rand
或randc
类型的随机变量。
//除非age随机化恰巧在允许的范围内,否则会报错
class Child;
bit [31:0] age;
constraint c_teenager { age > 12;
age < 20;}
endclass
在一个表达式中最多只能出现一个关系操作符(<,<=,==,>=,>)
。
class order;
rand bit [7:0] lo, med, hi;
constraint bad {lo < med < hi; } //错误约束
constraint good {lo < med; //正确约束
med < hi;}
endclass
此时求解器会按照从左至右的顺序分隔成两个关系吧表达式:((lo < med) < hi)。lo < med的结果是0或1,然后判断hi大于0或1。
因为约束块里只能包含表达式,所以不能在约束块里进行赋值(=)。应该使用关系运算符为随机变量赋一个固定的值,如len == 42;
6.3.1 权重分布
关键字dist,值或者权重可以是常数或者变量。值可以是一个值或值的范围,例如[lo:hi]。权重不用百分比表示,权重的和也不必是100。权重符号有:=
(表示值范围内的每一个值的权重都是相同的),:/
(表示权重要均分到范围内每一个值)。要产生带权重的分布,正确的做法是使用dist
操作符。
rand int src, dst;
constraint c_dist {
//0,1,2,3的权重依次为40/220,60/220,60/220,60/220
src dist {0:=40, [1:3]:=60};
//0,1,2,3的权重依次为40/100,20/100,20/100,20/100
dst dist {0:/40, [1:3]:/60};
}
6.3.2 集合set成员和inside运算符
使用inside
运算符产生一个值的集合。如果不存在其他约束,SV在值的集合里取随机值,各个值的选取机会是相等的。在集合里可以使用变量。
rand int c;
int lo, hi;
constraint c_range {
c inside {[lo:hi]}; //lo <= c 且 c <= hi
}
可选的范围由lo和hi决定,可以采用这种方法使约束参数化,实现不修改约束,测试平台就可以改变激励发生器的行为。如果lo>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 <= b <= 63
}
//如果想选择一个集合之外的值,只需要用取反操作符!对约束取反
constraint c_range {
! (c inside {[lo:hi]}); // c < lo || c > hi
}
6.3.3 在集合中使用数组
rand int f;
int fib[5] = '{1,2,3,5,8};
constraint c_fibonacci {
f inside fib;
}
集合中的每一个值取出的概率都是相同的,即使值在数组中出现多次。
class Weighted;
rand int val;
int array[] = '{1,2,3,3,5,8,8,8,8,8};
endclass
6.3.4 条件约束
SV支持两种关系操作符:->
和if-else
。
6.3.5 约束的双向性(并行性)
SV中的约束是双向的,约束块不像自上而下执行的程序性代码,他们是声明性的代码,是并行的,所有的约束表达式同时有效。
6.3.6 替换数学运算符来提高效率
约束求解器可以有效的处理简单的数学运算,如加,减,位移和移位。约束求解器对于32位数值的乘法,除法和取模运算的运算量是非常大的。SV中任何没有显式声明位宽的常数都是作为32位数值对待的。可以利用移位运算代替除法和取模运算,以提高约束求解器的效率。
6.4 解的概率
SV并不保证随机约束求解器能给出准确的解,但可以干预解的概率分布。
6.4.1 没有约束的类
class Unconstrained;
rand bit x;
rand bit [1:0] y;
endclass
经过千次随机化,x,y的每个值的取值概率是相同的。
6.4.2 关系操作
class Imp1;
rand bit x;
rand bit [1:0] y;
constraint c_xy {
(x==0) -> y==0;
}
endclass
x=0,y=0的概率为1/2,x=1,y=0,x=1,y=1,x=1,y=2,x=1,y=3的概率各为1/8。此求解器将x=0时,y为0,1,2,3的四种情况都归到了x=0,y=0的情况中。
6.4.3 关系操作和双向约束
class Imp2;
rand bit x;
rand bit [1:0] y;
constraint c_xy {
y > 0;
(x==0) -> y==0;
}
endclass
因约束是双向的,所以x的值为1,y值为1,2,3,每种组合的概率各为1/3。
6.4.4 Solve…Before
使用solve...before
可以改变值出现的概率,除非对某些值出现的概率不满意,否则不要使用solve...before
块,过度使用slove...before
会降低计算的速度,也会使你的约束让人难以理解。(不推荐使用)
6.5 控制多个约束块
一个类可以包含多个约束块,可以把不同的约束块用于不同的测试,例如一种约束用来限制数据的长度,用于产生小的事务,另一种约束用来产生大的事务。在运行期间,可以使用内建的constraint_mode()
函数打开或者关闭约束,可以用handle.constraint_mode()
控制一个约束块,用handle.constraint_mode()
控制对象的所有约束。
class Packet;
rand int length;
constraint c_short {length inside {[1:32]}; }
constraint c_long {length inside {[1000:1023]}; }
endclass
Packet P;
initial
begin
p = new();
//关闭c_short,产生长事务
p.c_short.constraint_mode(0);
assert (p.randomize());
transmit(p);
//打开c_short,产生短事务
p.constraint_mode(0);
p.c_short.constraint_mode(1);
assert (p.randomize());
transmit(p);
end
6.6 有效性约束
设置多个约束以保证随机激励的正确性是一种很好的随机化技术,它也称为“有效性约束”
。
class Transcation
rand enum {BYTE, WORD, LWRD, QWRD} length;
rand enum {READ, WRITE, RMW, INTR} opc;
constraint valid_RMW_LWRD {
(opc == RMW) -> length == LWRD;
}
endclass
6.7 内嵌约束
约束越来越多时,他们会互相作用,最终产生难以预测的结果。SV允许使用randomize() with
来增加额外的约束,这和在类里增加约束是等效的。
class Transaction;
rand bit [31:0] addr, data;
constraint c1 { addr inside{[0:100], [1000:2000]}; }
endclass
Transaction t;
initial begin
t = new();
assert(t.randomize() with {addr >= 50; addr <= 1500;
data < 10;});
driveBus(t);
arrser(t.randomize() with {addr == 2000; data > 10;});
driveBus(t);
end
此代码和在现有的约束上增加额外的约束是等效的。如果约束之间存在冲突,可以使用constraint_mode()函数禁止冲突的约束。**在with{}语句中,SV使用了类作用域,使用了addr而不是t.addr。**在使用randmize() with语句时常犯的错误是使用()包括内嵌的约束,而没有使用{}。约束块内应该使用{},所以内嵌约束也应该使用{},{}用于声明性的代码。
6.8 pre_randomize和post_randomize函数
有时需要在调用randmize()函数之前或之后立即执行一些操作,如在随机化之前设置类里的非随机变量(如上下限,权重),或者随机化之后需要计算随机数据的误差校正位。SV可以使用两个特殊的void类型的pre_randomize和post_randomize函数来完成这些功能。
6.9 随机数函数
下面是一些常用的随机数函数:
$random()
–平均分布,返回32位有符号随机数
$urandom()
–平均分布,返回32位无符号随机数
$urandom_range(low,upper)
–发挥[low, upper]范围(包括上下限)内的无符号数
$dist_exponential()
–指数衰落
$dist_uniform()
–平均分布
a = $urandom_range(3,10); //值的范围是3~10
a = $urandom_range(10,3); //值的范围是3~10
b = $urandom_range(5); //值的范围是0~5
6.10 约束的技巧和技术
6.10.1 使用变量的约束
//可以通过改变max_size的值来改变约束上限
class bounds;
rand int size;
int max_size = 100;
constraint c_size {
size inside {[1:max_size]};
}
endclass
//带有权重变量的dist约束
typedef enum {READ8, READ16, READ32} read_e;
class ReadCommands;
rand read_e read_cmd;
int read8_wt = 1, read16_wt = 1, read32_wt = 1;
constraint c_read {
read_cmd dist {READ8 := read8_wt,
READ16 := read16_wt,
READ32 := read32_wt };
}
endclass
6.10.2 使用非随机值
如果用一套约束在随机化的过程中已经产生了几乎所有想要的激励向量,但是还缺少几种激励向量,可以采用先调用randomize函数,然后再把随机变量的值设置为固定的期望值的方法来解决。
class Packet;
rand bit [7:0] length, payload[];
constraint c_valid {
length > 0;
payload.size == length;
}
endclass
Packet p;
initial
begin
p = new();
assert(p.randomize());
p.length.rand_mode(0);
p.length = 42;
assert (p.randomize());
end
6.10.3 用约束检查值的有效性
在随机化一个对象并改变它的变量的值后,可以通过检查值是否遵守约束来检查对象是否仍然有效,在调用handle.randomize(null)
函数时,SV会把所有的变量当作非随机变量,仅仅检查这些变量是否满足约束条件。
6.10.4 随机化个别变量
可以在调用randomize函数只传递变量的一个子集,这样就只会随机化类里的几个变量。只有参数列表里的变量才会被随机化,其他变量会被当作状态变量而不会被随机化。
class Rising;
byte low;
rand byte med, hi;
constraint up {
low < med;
med < hi;
}
endclass
initial
begin
Rising r;
r = new();
r.randomize(); //随机化med,hi,但不改变low
r.randomize(med); //随机化med
r.randomize(low); //随机化low
end
6.10.5 打开或关闭约束
可以使用constraint_mode
打开或关闭约束
class Instruction;
typedef enum {NOP, HALT, CLR, NOT} opcode_e;
rand opcode_e opcode;
...
constraint c_no_oprands {
opcode == NOP || opcode == HALT; }
constraint c_one_operand {
opcode == CLR || opcode == NOT; }
endclass
Instruction instr;
initial
begin
instr = new();
instr.constraint_mode(0); //关闭所有约束
instr.c_no_operands.constraint_mode(1); //打开c_no_operands约束
assert (instr.randomize());
instr.constraint_mode(0); //关闭所有约束
instr.c_one_operand.constraint_mode(1); //打开c_one_operands约束
assert (instr.randomize());
end
6.10.6 使用内嵌约束
如前问所述,SV可以使用randmize() with{}
内嵌约束来改变约束。
6.10.7 外部约束
函数的函数体可以在函数的外部定义,同样,约束的约束体也可以在类的外部定义,可以在一个文件里定义一个类,这个类只有一个空的约束,然后在每个不同的测试里定义这个约束的不同版本以产生不同的激励。
class Packet;
rand bit [7:0] length;
rand bit [7:0] payload[];
constraint c_valid {
length > 0;
payload.size() == length;
}
endclass
//类外部定义约束 test.sv文件
program automatinc test;
include "packet.sv" constraint Packet::c_external {length == 1;}
...
endprogram
外部约束和内嵌约束相比具有很多优点,外部约束可以放在另一个文件里,从而在不同的测试里可以复用外部约束。外部约束对类的所有实例都起作用,而内嵌约束仅仅影响一次randomize()调用。需要注意,外部约束只能增加约束,而不能改变已有的约束。和内嵌约束一样,因为外部约束可能分布在多个文件里,所以可能导致潜在的问题。
6.10.8 扩展类
通过拓展类(继承),可以在测试平台中使用一个已有的类,然后切换到增加了约束,子程序和变量的拓展类。
6.11 随机化的常见错误
除非必要,不要在随机约束里使用有符号类型。
class SignedVars;
rand byte pkt1_len, pk2_len;
constraint total_len {
pkt1_len+pk2_len == 64;
}
endclass
//为了避免得到负的包长这样无意义的值,应该使用无符号随机变量
class Vars32;
rand bit [7:0] pkt_1_len, pk2_len; //无符号类型
constraint total_len {
pkt1_len + pk2_len == 9'd64;
}
endclass
6.11.1 提高求解器性能的技巧
避免使用复杂的运算,例如除法,乘法和取模。
如果需要除以或乘以2的幂次方,使用右移或者左移操作。
如果需要进行这些操作,使用宽度小于32位的变量可以得到更高的运算性能。
6.12 迭代和数组约束
数组的大小可以用size()
函数进行约束,此函数可以约束动态数组和队列的元素个数。
//使用inside约束可以设置数组大小的上限和下限
class dyn_size;
rand logic [31:0] d[];
constraint d_size {d.size() inside {[1:10]}; }
endclass
6.12.1 元素的和
//可以用sum()函数约束随机数组只有四个位有效
parameter MAX_TRANSFER_LEN = 10;
class StrobePat;
rand bit strobe[MAX_TRANSFER_LEN];
constraint c_set_four { strobe.sum() == 4'h4}
endclass
6.12.2 对每个元素进行约束
SV中可以用foreach对数组的每一个元素进行约束,和直接写出对固定大小的数组的每一个元素的约束相比,使用foreach要更简洁。
class goo_sum5;
rand unit len[];
constraint c_len {
foreach(len[i])
len[i] inside {[1:255]};
len.sum < 1024;
len.size() inside {[1:8]};
}
endclass
//使用foreach产生递增的数组元素的值
class Ascend;
rand unit d[10];
constraint c {
foreach (d[i])
if(i>0)
d[i]>d[i-1]; //i=0时i-1=-1???
}
endclass
6.12.3 产生具有唯一元素值的数组
class UniqueSlow;
rand bit [7:0] ua[64];
constraint c {
foreach (ua[i])
foreach (ua[j])
if(i!=j)
ua[i] != ua[j];
}
endclass
//更好的办法是使用randc辅助类产生唯一的元素值
class randc8;
randc bit [7:0] val;
endclass
class LittleUniqueArray;
bit [7:0] ua [64];
function void pre_randomize;
randc8 rc8;
rc8 = new();
foreach(ua[i])
begin
assert(rc8.randomize());
ua[i] = rc8.val;
end
endfunction
endclass
6.12.4 随机化句柄数组
如果需要产生多个随机对象,那么你可能需要建立随机句柄数组。和整数数组不同,你需要在随机化前分配所有的元素,因为随机求解器不会创建对象。
parameter MAX_SIZE = 10;
class RandStuff;
rand int value;
endclass
class RandArray;
array = new [MAX_SIZE];
constraint c {array.sise() inside {[1:MAX_SIZE]}; }
function new();
array = new [MAX_SIZE];
foreach (array[i])
array[i] = new();
endfunction
RandArray ra;
initial
begin
ra = new();
assert (ra.randomize());
foreach (ra.array[i])
$display(ra.array[i].value);
end
endclass
6.13 产生原子激励和场景
产生事务序列的一个方法是SV的randsequence
结构。
initial
begin
for (int i = 0; i < 15; i++)
begin
randsequence(stream)
stream : cfg_read := 1 | io_read := 2 | mem_read := 5;
cfg_read : {cfg_read_task;} | {cfg_read_task} cfg_read;
mem_read : {meme_read_task;} | {mem_read_task;} mem_read;
io_read : {io_read_task;} | {io_read_task;} io_read;
endsequence
end
end
随机sequence序列会从三种操作中选取一种。
6.14 随机控制
6.14.1 使用randcase
和$urandom_range
的随机控制
initial
begin
int len;
randcase
1: len = $urandom_range(0, 2);
8: len = $urandom_range(3, 5);
1: len = $urandom_range(6, 7);
endcase
end
使用randcase
的代码会比随机约束的代码更难修改和重载。修改随机代码结果的位移方法是修改代码或使用权重变量。
6.14.2 可以使用randcase
建立决策树
initial
begin
//一层决策
randcase
one_write_wt: do_one_write();
one_read_wt: do_one_read();
seq_write_wt: do_seq_write();
seq_read_wt: do_seq_read();
endcase
//二层决策
task do_one_write;
randcase
mem_write_wt: do_mem_write();
io_write_wt: do_io_write();
cfg_write_wt: do_cfg_write();
endcase
endtask
end
6.15 随机数发生器
6.15.1 伪随机数发生器
V使用一种简单的PRNG(伪随机数发生器),通过$random
函数访问,这个发生器有一个内部状态,可以通过$random
的种子来设置。下面是一个简单的PRNG,它并不是SV使用的PRNG,这个PRNG有一个32位的内部状态,要计算下一个随机值,先计算出状态的64位平方值,取中间的32位数值,然后加上原来的32位数值。
reg [31:0] state = 32'h12345678;
function logic [31:0] my_random;
logic [63:0] s64;
s64 = state * state;
state = (s64>>16) + state;
endfunction
6.15.2 随机稳定性,多个随机发生器
V在整个仿真过程中使用一个PRNG,但如果SV仍然使用这种方案,测试平台通常会有几个激励发生器同时运行,为被测设计产生数据,如果两个码流共享一个PRNG,他们获得的都是随机数的一个子集。由于是从一个PRNG获取随机数据,所以改变其中一个类的随机数值个数会影响到另一个类获取的数值。在SV中,每个对象和线程都有一个独立的PRNG,改变一个对象不会影响其他对象获得的随机数。
6.15.3 随机稳定性和层次化种子
SV的每个对象都有自己的PRNG和独立的种子。当启动一个新的对象或线程时,**子PRNG的种子由父PRNG产生,**所以在仿真开始时的一个种子可以产生多个随机激励流,他们之间又是相互独立的。
6.16 随机器件配置
测试DUT的一个重要工作就是测试DUT内部设置和环绕DUT的系统的配置。如上所述,测试应该随机化环境,这样才能保证尽可能测试足够多的模式。
参考文献:
SystemVerilog验证 测试平台编写指南(原书第二版)张春 麦宋平 赵益新 译