SV学习笔记(四)

本文

芯片验证系列文章主要分为三部分:验证工程师与芯片验证、SV学习笔记、UVM学习笔记。此为 SV学习笔记 部分第四篇,主要介绍类随机约束和分布、约束块控制、随机函数、数组约束、随机控制。

版本 说明
0.1 初版发布

参考

名称 作者 来源
《芯片验证漫游指南》 刘斌 书籍
《SystemVerilog验证》 克里斯.斯皮尔 书籍

专业术语与缩略语

缩写 全称 说明
OCP Open Closed Principle 开闭原则

随机约束和分布

为什么需要随机?

  • 芯片复杂度越来越高,在20年前 定向测试 已经无法满足验证的需求,而 随机测试 的比例逐渐提高。
  • 定向测试能找到你认为可能存在的缺陷,而 随机测试可以找到连你都没有想到的缺陷
  • 随机测试的环境要求比定向测试复杂,它需要激励、参考模型和在线比较。上百次的仿真不再需要人为参与,以此来提高验证效率。
  • 随机测试相对于定向测试可以减少相当多的代码量,而产生的激励较定向测试也更多样。

为什么需要约束?

  • 如果随机没有约束,产生有效激励的同时,还 会产生大量的无效激励
  • 通过为随机添加约束,这种 随机自由是一种合法的随机 ,产生有效的测试激励。
  • 约束不是一成不变的,为了获取期望的测试范围或期待的数值范围,约束需要“变形”。
  • 随机的对象不只是一个数据,而是 有联系的变量集合 。通常这些变量集合会被封装在一个数据类里,同时需要类中声明数据之间的约束关系。因此,约束之后要产生一个随机数据的“求解器”,即在满足数据本身和数据之间约束关系的随机数值解。
  • 约束不但 可以指定数据的取值范围 ,还 可以指定各个数值的随机权重分布

我们需要随机什么?

  • 器件配置: 通过寄存器和系统信号。
  • 环境配置: 随机化环境,例如合理的时钟和外部反馈信号。
  • 原始输入数据: 例如MCDF数据包的长度、带宽,数据间的顺序。
  • 延时: 握手信号之间的时序关系,例如valid和ready,req和ack之间的时序关系。
  • 协议异常: 如果反馈信号给出异常,那么设计是否可以保持后续数据处理的稳定性。

声明随机变量的类

  • 随机化是为了产生更多可能的驱动,我们倾向于将相关数据有机整理在一个类的同时,也用“rand”关键词来表明它的随机属性。
  • “randc”关键词表示周期性随机,即所有可能的值都赋过值后随机才可能重复,也就好比54张扑克牌抽牌游戏,rand代表每抽完一张放回去才可以下次抽牌,randc代表没抽完一张不需要放回就抽取下一张,如果抽完了,那就全部放回再次同样规则抽取。
  • rand和randc,只能声明类的变量,硬件域以及软件域的局部变量都不可以。
  • 随机属性需要配合SV预定义的随机函数std::randomize()使用。即通过声明rand变量,并且在后期调用randomize()函数才可以随机化变量。
  • 约束constraint也同随机变量一起在class中声明。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class packet;
    rand bit [31:0] src, dst, data[8];
    randc bit [7:0] kind;

    constraint c {
        src >10;
        src <15;
    }
endclass

//----------------------------------

Packet p;
initial begin
    p = new();
    //assert语句保证randomize成功,否则会报fatal(如果约束冲突,如src>15 and src<10则会随机失败)
    assert (p.randomize()) else $fatal(0, "Packet::randomize failed");
    transmit(p);
end

什么是约束

  • 约束表达式的求解是有SV的约束求解器自动完成的。
  • 求解器能够选择满足约束的值,这个值是由SV的PRNG(伪随机数发生器)从一个初始值(seed)产生。只要改变种子的值,就可以改变CRT的行为。
  • SV标准定义了表达式的含义以及产生的合法值,但没有规定求解器计算约束的准确顺序。也就是,不同仿真器对于同一个约束类和种子求解出的数值可能不同。
  • 什么可以被约束?SV只能随机化二值数据类型,但数据位可以是二值或四值的,所以无法随机出x值和z值,也无法随机出字符串。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class date;
    rand bit [2:0] month; //note:
    rand bit [4:0] day;
    rand int year;

    constraint c_data {
        month inside {[1:12]};
        day inside {[1:31]};
        year inside {[2010:2030]};}
    }
endclass

请问:month=10,day=31,year=2020此组随机值可以产生吗? 答案:不能,因为month的声明是3位,所以不可能出现数值10,这也是经常会犯的错误,当你约束数据时,一定要与声明数据的位数相匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class stim;
    const bit [31:0] CONGEST_ADDR = 42; //声明常数
    typedef enum {READ, WRITE, CONTROL} stim_e;
    randc stime_e kind;
    rand bit [31:0] len, src, dst;
    bit congestion_test;

    constraint c_stim {
        len < 1000;
        len > 0;

        if(congestion_test) (
            dst inside {[CONGEST_ADDR-100:CONGEST_ADDR+100]};
            src == CONGEST_ADDR;
        ) else (
            src inside {0, [10:20], [100:200]};
        )
    }
endclass

权重分布

  • 关键词dist可以在约束中用来产生随机数值的权重分布,这样某些值的选取机会要大于其他值。
  • dist操作符带有一个值的列表以及相应的权重,中间用 := 或 :/ 分开。值和权重可以是常数,也可以是变量。
  • 权重不要百分比表示,权重的和也不必是100。
  • := 操作符表示值的范围内的每一个值的权重是相同的, :/ 操作符表示权重要平均分到范围内的每一个值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
rand int src, dst;

constraint c_dist {
    src dist {0:=40, [1:3]:=60;}
    // src=1, weight=40/220
    // src=2, weight=60/220
    // src=3, weight=60/220
    // src=4, weight=60/220

    dst dist {0:/40, [1:3]:/60;}
    // dst=1, weight=40/100
    // dst=2, weight=20/100
    // dst=3, weight=20/100
    // dst=4, weight=20/100
}

集合成员和inside

  • inside是常见的约束运算符,表示变量属于某个值的集合,除非还存在其他约束 ,否则随机变量在集合里取值的概率是相等的(集合里也可以是变量)。
  • 可以使用 $ 符指定最大或最小值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
rand int c;
int lo, hi;
constraint c_range{
    c inside {[lo:hi]};
}

//-------------------------------

rand bit [6:0] b;
rand bit [5:0] e;
constraint c_range {
    b inside {[$:4], [20:$]};
    e inside {[$:4], [20:$]};
}

条件约束

  • 可以通过 -> 或者 if-else来让一个约束表达式在特定条件有效。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
constraint c_io {
    (i_space_mode) -> addr[31] == 1'b1; //i_space_mode!=0
}

//--------------------------------------

constraint c_io {
    if(i_space_mode) //i_space_mode!=0
        addr[31] == 1'b1;
    else;
}

双向约束

  • 约束块不是自上而下的程序代码,它们是声明性代码,是并行的,所有的约束同时有效。
  • 约束是双向的,这表示它会同时计算所有的随机变量的约束,增加或删除任何一个变量的约束都会直接或间接的影响所有相关的值的选取。
  • 约束块可以声明多个,但是它们仍旧是并行的,如果对同一变量进行约束,取两者约束的交集,也就是两个约束都会生效,与写在一个约束块效果相同。
  • 子类会继承父类的约束。

约束块控制

打开或关闭约束

  • 一个类可以包含多个约束块,可以把不同约束块用于不同测试。
  • 一般情况下,各个约束块之间的约束内容是相互协调不违背的,因此通过随机函数产生的随机数可以找到合适的解。
  • 对于其他情况,例如跟胡不同需求,来选择使能哪些约束块,禁止哪些约束块,可以使用内建函数constraint_mode()打开或者关闭约束。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class packet;
    rand int length;
    constraint c_short {length inside {[1:32];}}
    constraint c_long {length inside {[1000:1032];}}
endclass

//------------------------

packet p;
initial begin
    p =new ();
    //create a long packet by disabling c_short
    p.c_short.constraint_mode(0);
    assert(p.randomize());
    transmit(p);
    //create a short packet by disabling all constraint and then enable only c_short
    p.constraint_mode(0);
    p.c_short.constraint_mode(1);
    assert(p.randomize());
    transmit(p);
end

内嵌约束

  • 伴随着复杂的约束,它们之间会相互作用,最终产生难以预测的结果。用来使能和禁止这些约束的代码也会增加测试的复杂性。
  • 经常增加或修改类的约束也可能会影响整个团队的工作,这需要考虑类的OCP原则(开放封闭原则,也就是哪些对外部开放,哪些不对外开放)。
  • SV允许使用 randomize() with来增加额外的约束,这和在类里增加约束是等效的,但同时要注意类内部约束和外部约束之间应该是协调的,如果出现违背,随机数会求解失败(求解失败,不同的工具报告形式不同,有的是error,有的是warning)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class packet;
    rand int length;
    constraint c_short {soft length inside {[1:32];}}
endclass

//------------------------

packet p;
initial begin
    p =new ();
    assert(p.randomize() with {
        length inside {[36:46];};
        length != 40;         }
    );
    transmit(p);
end

上述例子中randomize() with{}约束与c_short产生可冲突,那会不会报错呢?答案是不会,因为c_short约束前加了soft(软约束)关键字,意义就在于当外部或子类的约束发生冲突时,其优先级降低,不会影响外部或子类的约束。

随机函数

pre_randomize 和 post_randomize

  • 有时需要在调用randomize()之前或之后立即执行一些操作,例如在随机前设置一些非随机变量(上下限、条件值、权重),或者在随机化后需要计算数据的误差、分析和记录随机数据等。
  • SV提供两个预定义的void类型函数pre_randomize和post_randomize,用户可以类中定义这两个函数,分别在其中定义随机化前的行为和随机化后的行为。
  • 如果某个类中定义了pre_randomize或post_randomize,那么对象在执行randomize()之前或之后会分别执行这两个函数,所以pre_randomize和post_randomize可以看做是randomize函数的回调函数(callback function)。

系统随机数函数

SV提供了一些常用的系统随机函数,这些系统随机函数可以直接调用来返回随机数值:

  • $random()平均分布,返回32位有符号随机数。
  • $urandom()平均分布,返回32位无符号随机数。
  • $urandom_range()在指定范围内的平均分布。

随机化个别变量

  • 在调用randomize()时可以传递变量的一个子集,这样只会随机化类里的几个变量。
  • 只有参数列表里的变量才会被随机化,其他变量会被当做状态量而不会被随机化。
  • 所有的约束仍然保持有效。
  • 注意:类里所有没有被指定rand的变量也可以作为randomize()的参数而被随机化。
  • 注意:未进行随机化的变量默认初始值为0。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class rising;
    byte low;
    rand byte med, hi;
    constraint up {
        low<med; med<hi;
    }
endclass

//----------------------------------

initial begin
    rising r;
    r =new();
    r.randomize(); //随机化hi和med,不改变low
    r.randomize(med); //只随机化med
    r.randomize(low); //只随机化low
end

数组约束

约束数组的大小

  • 在约束随机标量的同时,我们也可以对随机化数组进行约束。
  • 多数情况下,数组的大小应该给定范围,防止生成过大体积的数组或空数组。
  • 此外,还可以在约束中结合数组的其他方法sum(), product(), and(), or(), 和xor()。
1
2
3
4
5
6
class dyn_size;
    rand logic [31:0] d[];
    constraint d_size {
        d,size() inside {[1:10];};
    }
endclass

约束数组的元素

  • SV可以利用foreach对数组每一个元素进行约束,和直接写出对固定大小数组的每一个元素相比,foreach更简洁。
  • 针对动态数组,foreach更适合于对非固定大小数组中每个元素的约束。
1
2
3
4
5
6
7
8
class good_sum5;
    rand uint len[];
    constraint c_len{
        foreach (len[i]) len[i] inside {[1:255]};
        len.sum() < 1024;
        len.size() inside {[1:8]};
    }
endclass

产生唯一元素值的数组

  • 如果想要产生一个随机数组,它的每一个元素值都是唯一的,如果使用randc数组,嘛呢数组中的每一个元素只会独立的随机化,并不会按照我们期望的使得数组中的元素值是唯一的。
  • 解决方案1:
1
2
3
4
5
6
rand bit [7:0] data;
constraint c_data{
    foreach(data[i])
        foreach(data[j])
            if(i != j) data[i] != data[j];
}
  • 解决方案2:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class randc_data;
    randc bit [7:0] data[64];
endclass

class data_array;
    bit [7:0] data_array [64];

    function void pre_randomize();
        randc_data rcd;
        rcd = new();
        foreach (data_array[i]) begin
            assert(rcd.randomize());
            data_array[i] = rcd.val;
        end
    endfunction
endclass
  • 特别示例如下,首先“<=”代表小于等于,其次限定da.size为(3/4/5),实际不可能取到5,原因是da.size的约束体现在“da[i] <= da[i+1]”时,约束的是i和i+1为为(3/4/5)。
1
2
3
4
5
rand bit [7:0] da[];
constraint c_da {
    da.size() inside {[3:5]};
    foreach(da[i]) da[i] <= da[i+1];
}

随机化句柄数组

  • 随机句柄数组的功能是在调用其所在类的随机函数时,随机函数会随机化数组中的每一个句柄所指向的对象。因此随机句柄数组的声明一定要添加rand来表示其随机化的属性,同时在调用随机函数前要保证句柄数组中的每一个句柄元素都是非悬空的,这需要早随机化之前为每一个元素句柄构建对象。
  • 如果要产生多个随机对象,那么你可能需要建立随机句柄数组。和整数数组不同,你需要在随机化前分配所有的元素,因为在随机求解器不会创建对象。使用动态数组可以按照需要分配最大数量的元素,然后再使用约束减小数组的大小。在随机化时,动态句柄数组的大小可以保持不变或减小,但不能增加。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
parameter MAX_SIZE = 10;
class RandStuff;
    bit[1:0] value = 1;
endclass

class RandArray;
    rand RandStuff array[];
    constraint c_array {
        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(ra.array[i]) $display(ra.array[i].value);
end
  • 问题1:执行ra.randomize() with {array.size=2}时,array[0].value 和 array[0].value分别是多少? 答案都是1,首先value没有加rand,所以randomize不会随机value,仍然保持为1。
  • 问题2:为什么要分配最大容量? 答案是只有创建对象,并且分配最大容量,才能保证随机化时可能会碰到句柄数组悬空,无指向对象,随机会报错。
  • 总结:句柄数组的随机,首先查看句柄指向的对象内有没有rand变量,其次对句柄数组按最大容量进行例化。

随机控制

  • 产生事务序列的另一个方法是使用SV的randsequence结构。这对于随机安排组织原子(atomic)测试序列很有帮助。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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
end
  • 我们也可以使用randcase来建立随机决策树,但它带来的问题是没有变量可供追踪调试。
1
2
3
4
5
6
7
8
9
initial begin
    int len;
    randcase:
        1: len = $urandom_range(0,2); //10%
        8: len = $urandom_range(3,5); //80%
        1: len = $urandom_range(6,7); //10%
    endcase
    $display("len=%0d", len);
end
  • 总结:
    • randsequence和randcase是针对轻量级的随机控制的应用。而我们可以通过定义随机类取代上述随机控制的功能,并且由于类的继承性使得后期维护代码时更加方便。
    • randsequence的相关功能我们在协调激励组件和测试用例时,可能会用到。
    • randcase则对应着随机约束中的dist权重约束 + if-else条件约束的组合。

文章原创,可能存在部分错误,欢迎指正,联系邮箱 cao_arvin@163.com。