文章目录
前言
随着设计变得越来越大,要产生一个完整得激励集来测试设计的功能变得很困难。定向测试集,功能项之间得关系使大多数错误的来源。
采用受约束的随机测试法(CRT)自动产生测试集。
产生有效的激励,测试感兴趣的功能项。
一、CRT?
定向测试集得环境: 只需要施加激励,然后人工检查输出结果。正确得输出结果以保存为标准日志文件;
CRT 环境: 不仅需要产生激励,还需要参考模型,传输函数或其他方法预测输出结果。
CRT由两部分组成: 使用随机的数据流为DUT产生输入得测试代码,伪随机数发生器(PRNG)得种子(seed); 改变种子的值就可以调整测试。
二、什么需要随机化
产生随机化得技术产生激励,产生随机化的数据,调用$random 函数,但是这种方法只能找到数据路径方面的Bug。该方法的本质还是基于定向测试的方法。
因此随机化使DUT的控制路径里的每一个分支都被测试:
考虑设计输入的各个方面:
- 器件配置; 2、环境配置;3. 原始输入数据;4. 封装后的输入数据;5. 协议异常;6. 延时;7. 事务状态;8. 错误和违规;
1. 器件配置
DUT 实际使用是其内部有各种配置的,而不是刚刚退出复位状态的实际,仅仅用一个固定的初始化向量使设计进入到确定的状态。
要测试这个器件, 验证工程师必须写很多行TCL代码来配置每个通道,他只可能验证少数几个通道的配置。
如果采用CRT方法, 他只需要写一个针对一个通道的参数随机化的测试平台,然后把它放在一个循环里去配置整个器件。
2. 环境配置
通常你设计的器件 在一个包含了若干器件的环境里工作。当验证DUT时,你应该随机化整个环境,包括对象的数量以及他们如何配置。
例如,验证一个I/O交换芯片,把很多PCI总线连接到内部存储器总线上。 他们随机选择PCI总线的个数(1-4个)、每个总线上器件的个数(1-8个)、每个器件的参数(主、从、CSR地址)。随机化的测试几乎可以覆盖所有这些组合情况。
3. 原始输入数据
只要准备相关的事务类,但需要设计协议的各个曾测以及故障注入。
4. 封装后的输入数据
很多器件会处理吉利的不同层次。如 一个器件可能会产生TCP流量,TCP数据随后随后被封装到IP协议里,最后被放到以太网包里发送出去。协议的每个层次都有自己的控制域,可以采用随机化的方法测试不同的组合。你需要编写约束以产生有效的控制域,同时还允许注入故障。
5. 协议异常、错误 和 违规
测试设计规范之外的行为 和 测试设计在设计规范边界处的行为。
确认设计 正确处理故障,不会死锁 或 进入不正确的状态。
6. 延时
许多通信协议定义了延时的范围,例如 总线允许信号在总线请求信号1-3个时钟周期后到来,存储器的数据在4-10个总线周期后有效。
测试平台应该在每一个测试里使用随机的、有效的延时,一边发现设计中的Bug。
一些设计会对时钟抖动非常敏感。 通过把时钟沿来回移动一格很小的步长,可以检查设计是否对时钟周期的微小变化异常敏感。
时钟发生器应该具有一些可配置的参数,例如 频率和相位。这些参数可以有测试平台在配置过程中设置。
查找功能错误。测试平台不应该厂是违反建立时间和保持时间的约束。
三、SV中的随机化
首先建立一个具有一组相关的随机变量的类。你可以用约束来限制这些随机值的方位,使它们使有效的值,也可以测试某些专用的功能。
1. 带有随机变量的简单类
constraint c { src > 10;
src < 15; }
这段代码是声明性质,而不是程序性质。
assert(p.randomize());
else $fatal(0,"Packet::randomize failed");
randomize()函数在遇到约束方面的问题时返回0。使用断言来检查randomize函数的结果。
构造函数:是用来初始化对象的变量,不能再这里调用randomize()函数。
类里的所有变量都应该时随机的(random)和公有的(public),这样才能最大程度地控制DUT。
如果忘记把变量设置成随机地,就只能通过编辑环境变量来实现,避免这样做!
2. 检查随机化(randomize)地结果
使断言能够 终止 仿真过程。
3. 约束求解
约束表达式地求解 是由 SV的约束求解器完成的。求解器从一个初始值(seed) 产生。
4. 什么可以被随机化
SV可以随机化整型变量,即由位组构成的变量。
在约束中指向句柄,只能随机化2值数据类型,但位也可以是2值或4值类型,不能使用随机字符串。
四、约束(固定顺序、只能用关系运算符)
有用的激励并不仅仅是随机值—各个变量之间有着相互关系。
class child;
bit [31:0] age; // 没有用rand 或 randc
constraint c_teenager { age > 12;
age < 20;}
endclass
randomize( ) 函数会 为 随机变量选取一个新的值,必须保证满足所有的约束条件。 因为没用rand ,randomize()仅仅检查age的值是否在c_teenager约束定义的范围里。如果正好age在13-19里,那么randomize函数是正确的。
1. 什么是约束
constraint 里的表达式 最多只能用一个关系操作符(<,>,==,<=,>=)
typedef enum {READ,WRITE,IDLE,CONTROL} stim_e;
rand stim_e kind; //枚举变量
rand bit [31:0] len, src, dst;
const bit [31:0] const_addr = 42;
bit congestion_test;
constraint c_stim {
len < 1000; //固定顺序的约束
len > 0; // 如使用 0 <len < 1000,这样做不对
if (congestion_test) {
dst inside {[const_addr-100, const_addr+100]}};
src == const_addr; //约束块里只能包含表达式,所以在约束块里不能进行赋值。用关系运算符==可以。 如:len == header.addr_mode * 4 + payload.size()
}
else
src inside {0, [2:10], [100:107]};
}
endclass
2. 权重分布
dist 操作符允许产生权重分布
constraint cst {
src dist {0:=40, [1:3]:=60}; // := 范围内的每一个值得权重是相同的
// src = 0, weight = 40/220
// src = 1, weight = 60/220
// src = 2, weight = 60/220
// src = 3, weight = 60/220
dst dist {0/=40,[1:3]/=60}; // /= 范围内表示权重要均分给每一个值
// dst=0, weight = 40/100
// dst=1, weight = 20/100
// dst=2, weight = 20/100
// dst=3, weight = 20/100
}
值和权重 可以是常量 或 变量。设置权重为0,从而删除一个值。
动态改编权重:
typedef enum {READ,WRITE,IDLE} length_e;
rand length_e len;
bit [31:0] w_read = 1, w_write = 3, w_idle = 5;
constraint c_len {
len dist { READ:= w_read;
WRITE:= w_write;
w_idle:= w_idle;}
}
3. 集合(set)成员和 inside 运算符
集合里得值选取机会是相等的。
rand bit [6:0] b; // 0 <= b <= 127
rand bit [5:0] e; // 0 <= e <= 63
constraint c {
b inside {[$,4],[20:$]};
e inside {[$,4],[20:$]};
}
随机集合约束得取反
constraint c_range {
! (c inside {[lo:hi]}); // c < lo 或 c > hi
}
4. 在集合里使用数组
集合里的每一个值取出的概率是相同的。
rand int f;
int array[5] = `{1,2,3,4,5};
int array[] = `{1,1,1,23,4,5};
constraint c {
f inside array;
}
or 等价的约束
constraint c {
(f == array[0]) ||
(f == array[1]) ||
(f == array[2]) ||
(f == array[3]) ||
(f == array[4]) ||
}
如果想动态地向集合里添加或删除值,要经过三思后才能使用inside操作符,它会影响仿真器的性能。
randc变量的计算非常块。
5. 条件约束(关系操作 -> 和 if-else)
怎样才能让一个约束表达式只在某些时候才有效呢?
例如:一条总线 支持 字节、字、长字 的读操作,只支持 长字的写操作。
- -> 可以产生 和 case 一样的效果, 可以用于枚举类型的表达式。
class Busop;
...
constraint c {
(io_space_mode) -> addr[31] = 1'b1;
}
约束: {(a ==1) -> (b ==0);} 和 { ! (a ==1) || (b ==0)} 约束等价的, 求解器并不是先计算 a1, 再令 b0; 而是 如下表所述:
a | b |
---|---|
1 | 0 |
0 | 1 |
- if-else 更适合真假类型的表达式
class Busop;
...
constraint c {
if (op == READ)
len inside {[BYTE:LWRD]};
else
len == LWRD
}
6. 双向约束
约束块中的 内容是 声明性的代码,是并行的。所有的约束表达式同时有效。 约束是双向的,会同时计算所有随机变量的约束。
rand logic [15:0] r,s,t;
constraint cst{
r < t;
s == r;
t < 30;
s > 25;
}
求解: 25 < s = r < t <30
6. 使用合适的数学运算来提高效率
约束求解器可以 有效地处理简单的数学运算:加、减、位提取、移位、
对于32位熟知的乘法、除法、和取模运算量 是非常大的。
位提取 代替 除法和取模运算
rand bit [31:0] addr;
constraint slow_near {
addr % 4096 inside {[0:20],[4075:4095]};
addr[11:0] inside {[0:20],[4075:4095]};
}
五、解的概率
SV 不能保证 随机约束求解器 能给出准确的解, 可以干预解的概率分布。
1. 关系操作和双向约束
(x == 0)-> (y 0), 当 x0时,则y=0;若y=0,则对x的值没有约束。若y不等于0,则x值为1.
2. 使用solve…before约束引导概率分布
rand bit x;
rnad bit [2] y;
constraint cstr {
(x == 0)-> (y ==0);
solve x before y;
}
X 为0或1的概率相同
当 x0时,则y=0;
当 x1时,则y为0,1,2,3的概率相等
若y=或不等于0,则对x的值没有约束
3. 控制多个约束块
用不同的约束块用于不同的测试
运行期间,使用内建的constraint_mode()函数打开或关闭约束。
class Packet;
rand int length;
constraint c_short {length inside [1:12];}
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);
// 禁止所有的约束,使能短句约束来产生短包
p.constraint_mode(0);
p.c_short.constraint_mode(1);
assert(p.randomize());
transmit(p);
end
4. 有效性约束
设置多个约束以保证随即激励的正确性是一种很好的随机化技术,也称为有效性约束
class transaction;
rand enum {BYTE,WORD,LWORD,QWRD} length;
rand enum {READ,WRITE,RMW,INTR} opc;
constraint valid_RWM_LWRD {
(opc == RMW) -> (length == LWRD);
}
endclass
5. 内嵌约束
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;});
这里约束与transaction类的约束是取交集的,这里addr 而不是t.addr,用了类的作用域,{}用于声明性代码
drive_bus(t);
end
六. pre_randomize 和 post_randomize 函数 void类型
pre_randomize 在设置类里的一些非随机变量(例如上下限、权重);
post_randomize 需要计算随机数据的误差校正位
若在pre_randomize 和 post_randomize 函数 调用 调试程序,则调试程序必须是函数类型。
1. 构造浴缸型分布(不用rand)
在 pre_randomize函数计算指数曲线上的一个点,然后随机地选择把这个点放在左边或右边的曲线上。
Verilog提供了很多非线性分布的函数,例如$dist_exponential,但没有浴缸型分布函数
构造浴缸型分布
class Bathtub;
int value; // 浴缸型分布的随机变量
int WIDTH=50, DEPTH=4, seed=1;
function void pre_randomize();
//计算函数曲线
value = $dist_exponential(seed, DEPTH);
if(value > WIDTH) value = WIDTH;
// 把这个点随机地放在左边或右边的曲线上
if($urandom_range(1))
value = WIDTH-value;
endfunction
endclass
value 的值在每次对象随机化的时候更新,经过多次随机化后,就可以得到预期的浴缸型非线性分布。
2. void 函数
randomize函数调用pre_randomize函数,不调用消耗时间的任务,如果随机化过程中出现的问题,可以调用预先准备好的void类型的显示程序来显示中间结果。
3. 随机函数
- $random() 平均分布,返回32位有符号随机数
- $urandom() 平均分布,返回32位无符号随机数
- $urandom_range() 在指定范围内的平均分布
- $dist_exponential() 指数衰退
- $dist_normal() 钟型分布
- $dist_poisson() 钟型分布
- $dist_uniform() 平均分布
六 约束的技巧和技术 demo
怎样编写易于修改的CRT?
DEMO:
1. 使用变量的约束,使用非随机值
可以用rand_mode(0)函数把变量设置为非随机变量.
产生变长负载的包
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.display("Simple randomize")
p.length.rand_mode(0); // 设置包长为非随机值
p.length = 42; // 设置包长为常数
assert(p.randomize()); //在随机化paypload,
注意包长不随机化
end
2. 用约束来检查值得有效性
调用 handle.randomize(null) 函数,SV会把所有的变量当作非随机变量,仅仅检查这些变量是否满足约束条件。
3. 随机化个别变量
randomize()函数只传递变量得一个子集,这样就只会随机化类里得几个变量,其他变量会被当作状态变量(非随机变量),所有的约束仍然保持有效。
class rising;
byte low; // 非随机变量
rand byte med, hi; // 随机变量
constraint c { low<med; med<hi;}
endclass
rising r;
initial begin
r = new();
r. randomize(); // 随机化med,hi,不改变low
r.randomize(med); // 随机化med
r.randomize(low); // 随机化low,非随机变量
end
4. 打开或关闭约束
- 约束得表达式越来愈多,常见得办法是对每一种指令建立一套独立的约束,在使用时关闭其他所有的约束。
class Packet;
rand int length;
constraint c_short {length inside [1:12];}
constraint c_long {length inside [1000:1023];}
endclass
Packet p;
initial begin
p = new();
p.constraint_mode(0); //禁止所有约束
p.c_short.constraint_mode(1);
assert (p.randomize());
transmit(p);
// 禁止所有的约束,使能长句约束来产生短包
p.constraint_mode(0);
p.c_long.constraint_mode(1);
assert(p.randomize());
transmit(p);
end
外部约束放在另一个文件里,在不同的测试里可以服用外部约束。
只能增加约束,不能改变已有的约束。
// packet.sv
class Packet;
rand bit [7:0] length;
rand bit [7:0] payload[];
constraint c_valid { length > 0;
payload.size() == length;}
constraint c_extern;
endclass
//test.sv
program automatic test;
include "packet.sv" constraint Packet::c_extern{ length ==1;}
...
endprogram
7. 扩展类
扩展类得约束与基类得约束得名字相同,那么扩展得约束将取代基类得约束。
七 随机化的常见错误
1. 小心使用有符号变量
除非必要,不要在随即约束里使用有符号类型。
byte 有符号,[-127,127]
bit 无符号,[0,127]
当然还有可能 bit1+bit2 =64 ,其中bit1和bit2均为8位位宽时,这样就可能导致 8位加8位是有可能9位的,这样写就不对,应该这样写:bit1+bit2 =9’d64 ;
2. 提高求解去性能的技巧
避免使用复杂的运算,如除法、乘法、取模
小于32位的变量可以得到更高的运算性能
3. 迭代和数组约束
之前都是约束标量类型的变量。在随机化数组时进行约束:用foreach和一些数组函数;
但是foreach会产生很多约束,影响仿真器的运行速度。最好用randc变量代替嵌套的foreach算法。
4. 约束动态数组的大小
class dyn_size;
rand logic [31:0] d[];
constraint d_size {d.size() inside{[1:10]};}
endclass
使用inside {[1:10]} 设置上下限,一般不希望数组尺寸为0,上限不要太高,产生成千上万个元素,导致求解器要很长的时间才能求解。
5. 元素的和
图中 是在十个周期内发送四个数据的脉冲信号示意图
随机化脉冲类
parameter MAX_TRANSFER_LEN = 10;
classs strobepat;
rand bit strobe[MAX_TRANSFER_LEN];
constraint c_stobe { strobe.sum() == 4'h4; }
用sum()函数约束随机数组元素个数最大是10
endclass
initial begin
strobepat sp;
sp = new();
int count = 0; // 数据数组的索引
assert(sp.randomize());
foreach (sp.strobe[i]) begin
@bus.cb;
bus.cb.strobe <= sp.strobe[i];
// 如果strobe信号有效,输出下一个数据
if(sp.strobe[i])
bus.cb.data <= data[count++];
end
end
6. 数组约束的问题
假设要产生1-8个随机事务,这些事务的总长度小于1024;
方法1和2:
class sum1024;
rand bit [7:0] len[]; // rand byte len[]; 和有时会为负,并且始终小于127
constraint c_len { len.sum < 1024;
len.size() inside {[1:8]};}
endclass
这个和不为负了,但是始终小于256. 原因是 8位数值的和 用 8位数值保存。
方法3:
class sum1024;
rand unit len[]; // 32 位
constraint c_len { len.sum < 1024;
len.size() inside {[1:8]};}
endclass
len的变量都超过8位,
方法4:
class good_sum4;
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 asecond;
rand unit d[10];
constraint c {
foreach (d[i]) //对数组的每个元素操作
if(i>0) //除了第一个元素
d[i]>d[i-1] //和前一个元素比较
}
6. 产生具有唯一元素值的数组
方法1
class uniqueslow;
rand bit [7:0] ua [64];
constraint c {
foreach (ua[i])
foreach (ua[j])
if(i != j)
ua[i] != ua[j];
}
endclass
方法2
class randc1;
randc bit [7:0] val;
endclass
class littleuniquearray;
bit [7:0] ua [64]; //每个元素具有唯一值的数组
function void pre_randomize;
randc1 rc;
rc = new();
foreach (ua[i]) begin
assert(rc.randomize());
ua[i] = rc.val;
end
endfunction
endclass
7. 随机化句柄数组
随即求解器不会创建对象。
产生随机数组的元素
parameter MAX_SIZE = 10;
class Randstuff;
rand int value;
endclass
class Randarray;
rand Randstuff array[]; // 不要忘记使用rand
constraint c {array.size inside {[1:MAX_SIZE]};}
function new();
array = new[MAX_SIZE]; // 按最大的容量分配
foreach (array[i])
array[i] = new();
endfunction
endclass
Randarray ra;
initial begin
ra = new(); //构造数组和所有的对象
assert(ra.randomize());
foreach(array[i])
$display(ra.array[i].value);
end
八 产生原子激励和场景
每次只产生一个事务无法模拟出这些场景
1. 和历史相关的原子发生器
产生事务流的最简单的办法是基于以前事物的随机值的原子发生器。
2. 随机序列
产生事务序列的另一个方法是使用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: { mem_read_task; } |
{ mem_read_task; } mem_read;
io_read: { io_read_task; } |
{ io_read_task; } io_read;
endsequence
end //for
end
task cfg_rea_task;
...
endtask
cfg_read 可以对cfg_read_task任务的一次调用, 后边跟着 cfg_read,所以后边还可以调用cfg_read_task,至少一次;
randsequence 优点:程序性代码,执行过程中可以逐步调试,增加$display语句。若调用对象的randomize()函数,无法知道其是否执行。
randsequence 缺点:修改一个序列,如增加一个新的分支或动作,你可能需要改变序列的原始代码,而不能通过扩展序列来实现。
3. 随机对象数组
产生随机序列的形式:随机化整个对象数组。可以建立之下该数组的前一个和后一个对象的约束。同时求解所有的约束。
4. 随机控制
使用randcase 和 $urandom_range的随机控制
initial begin
int len;
randcase
1: len = $urandom_range(0,2); // 10%: 0,1 or 2
8: len = $urandom_range(3,5); // 80%: 3,4 or 5
1: len = $urandom_range(6,7); // 10%: 6 or 7
endcase
$display("len = %0d", len);
end
$urandom_range(15),
则等价于$urandom_range(0,15)
class lendist;
rand int len;
constraint c { len dist{ [0:2]:= 1,
[3:5]:= 8,
[6:7]:= 1 }; }
randcase 和 constraint中的 dist是一样的功能,但是randcase想要修改代码比随机约束更难修改和重载。
5. 用randcase建立决策树
initial begin
// level 1
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
end
//level 2
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
task do_one_read;
randcase
mem_read_wt: do_mem_read;
io_read_wt: do_io_read;
cfg_read_wt: do_cfg_read;
endcase
endtask
5. 随机数发生器
- 简单的伪随机数发生器
reg [31:0] state = 32'h12345678;
function logic [32] my_random;
logic [64] s64;
s64 = state * state;
state = (s64>>16) + state;
my_random = state;
endfunction
简单的Verilog的PRNG,有一个32位的内部状态。要计算下一个随机值,先计算出状态的64位平方值,取中间的32位数值,然后加上原来的32位数值。
- 随机稳定性-------多个随机发生器
SV 测试平台总通常会有几个激励发生器同时运行。
SV 的每个第项都有自己的PRNG和独立的种子,当启动一个新的对象或线程时,子PRNG的种子由父PRNG产生。所以仿真开始时的一个种子可以产生多个随机激励流,他们之间又是相互独立的。 - 随机稳定性和层次化种子
测试平台首先创建了对象,然后再并行的线程里运行他们,
随后增加了一个新的发生器,并在新的线程里运行,新的对象在原来的对象之后创建,新的线程也是在原来的线程之后产生 - 随机器件配置
测试DUT的一个重要工作 是 测试DUT内部设置和环绕DUT的系统的配置。
如何建立测试平台配置,并在必要的时候在测试级改变配置。
以太网交换机配置类
class eth_cfg;
rand bit [3:0] in_use; //测试中使用的端口
rand bit [47:0] mac_addr[4]; // MAC地址
rand bit [3:0] is_100; // 100MB 模式
rand unit run_for_n_frames; // 测试中的帧数
// 在unicast模式时设置某些地址位
constraint local_unicast {
foreach(mac_addr[i])
mac_addr[i][41:40] = 2'b00;
}
constraint reasonable { //限制测试长度
run_for_n_frames inside {[1:100]};
}
endclass: eth_cfg
class Environment;
eth_cfg;
eth_src gen[4];
eth_mii drv[4];
function new();
cfg = new(); // 创建cfg
endfunction
function void gen_cfg;
assert(cfg.randomize()); //随机化cfg
endfunction
//使用随机配置建立环境
function void build();
foreach (gen[i])
if (cfg.in_use[i]) begin
gen[i] = new();
drv[i] = new();
if(cfg.is_100[i])
drv[i].set_speed(100);
end
endfunction
task run();
foreach (gen[i])
if (cfg.in_use[i]) begin //必须所有用到gen[i]的地方都先检测in_use[i],否则当测试平台访问到不存在的发生器时会崩溃。所以先检测
//启动测试平台的事务处理器
gen[i].run();
..
end
endtask
task wrap_up();
endtask
endclass:Environment
测试例化了环境类,然后一次运行。
program test;
Environment env;
initial begin
env = new(); // 创建环境
env.gen_cfg; // 建立随机配置
// 修改随机值-打开四个端口
env.cfg.in_use = 4'b1111;
env.build(); // 激励测试平台的环境
env.run(); // 运行测试
env.wrap_up();// 整理 & 产生报告
end
endprogram
总结
CRT 时产生验证复杂设计所需激励的唯一可行的方法。
测试必须是灵活的,允许你既可以使用产生的缺省值,也可以约束或修改。
建立测试平台前务必事先规划,流出足够的“钩子”,这样才能在不修改现有代码的情况下控制测试平台。