第二章 SystemVerilog基础回顾:面向对象编程、随机化与约束、功能覆盖率、断言基础

各位同学,咱们直接进入正题。这一章我把它叫做“热身运动”。

你想想看,UVM 说白了就是 SystemVerilog 的一个应用框架。如果 SV 基础不牢,后面学 UVM 会非常痛苦。我自己带过不少新人,很多人一上来就啃 UVM 源码,结果被 uvm_component_utils 这些宏搞晕了。其实,底层全是面向对象那套东西。

所以,咱们花点时间,把 SV 里跟 UVM 最相关的四个核心点过一遍。嗯,这章内容不少,但都是干货。

2.1 面向对象编程(OOP)—— UVM 的骨架

UVM 整个框架就是建立在 OOP 之上的。你写的每一个 uvm_driver、uvm_monitor,本质上都是类的实例化。

2.1.1 类与对象

类就是模板,对象是实例。这个不难理解。我习惯把类比作“设计图纸”,对象就是“造出来的房子”。

class packet;
    rand bit [7:0] addr;
    rand bit [7:0] data;
    
    function void display();
        $display("addr=%0h, data=%0h", addr, data);
    endfunction
endclass

packet pkt;      // 声明句柄
pkt = new();     // 创建对象

个人习惯: 我每次写 new() 的时候,都会检查一下构造函数里有没有初始化所有成员变量。曾经有一次我忘了初始化一个队列,结果仿真跑了两小时才发现数据全是 X,血的教训。

2.1.2 继承与多态

继承是 UVM 的核心思想。你写的 my_driver extends uvm_driver,就是把父类的所有属性和方法拿过来,再添加自己的东西。

多态呢?说白了就是“同样的函数名,不同的行为”。UVM 里到处都是 build_phase、connect_phase,你重写它们,框架自动调用你的版本。

class base_test;
    virtual function void build();
        $display("base_test::build");
    endfunction
endclass

class my_test extends base_test;
    function void build();
        super.build();  // 调用父类
        $display("my_test::build");
    endfunction
endclass

避坑指南: 我曾经在重写 build_phase 时忘了调用 super.build(),结果父类里创建的子组件全没了。嗯,从那以后我每次重写都会先写 super.build(),养成肌肉记忆。

2.1.3 句柄与对象

这是新人最容易搞混的地方。句柄不是对象,它只是指向对象的“遥控器”。

packet pkt1, pkt2;
pkt1 = new();
pkt2 = pkt1;  // pkt2 和 pkt1 指向同一个对象
pkt1 = null;  // 对象还在,pkt2 还能用

2.2 随机化与约束——验证的精髓

验证的本质是什么?就是产生各种合法的、非法的输入,看 DUT 怎么反应。随机化就是干这个的。

2.2.1 随机化基础

SV 里用 rand 和 randc 关键字声明随机变量。rand 每次独立随机,randc 是循环随机,所有值轮完一遍才重复。

class packet;
    rand bit [7:0] addr;
    randc bit [3:0] id;  // 0~15 轮一遍
endclass

packet pkt = new();
repeat(10) begin
    assert(pkt.randomize());
    pkt.display();
end

注意: randomize() 返回 1 表示成功,0 表示失败。我建议每次都加 assert 检查,否则随机化失败了你都不知道,仿真结果全是错的。

2.2.2 约束的写法

约束就是给随机化加“规矩”。UVM 里最常用的就是 constraint 块。

class packet;
    rand bit [7:0] addr;
    rand bit [7:0] data;
    
    constraint addr_range { addr inside {[0:100], [200:255]}; }
    constraint data_valid { data != 8'h00; }
    constraint addr_data_corr { addr % 2 == 0 -> data[0] == 1'b1; }
endclass

我个人习惯把约束分成三类:

约束类型说明示例
范围约束限制取值范围addr inside {[0:100]}
关系约束变量间的关系addr % 2 == 0 -> data[0] == 1
分布约束控制概率分布dist {0:=1, [1:10]:/9}

避坑指南: 我曾经写过一个约束 addr > 10 && addr < 5,结果随机化永远失败。仿真器不会告诉你约束矛盾,你得自己排查。建议用 solve...before 控制求解顺序,避免约束冲突。

2.3 功能覆盖率——你到底测了多少?

功能覆盖率回答一个问题:“你的测试用例覆盖了所有功能点吗?”

2.3.1 覆盖组与覆盖点

covergroup 是 SV 里定义覆盖率的结构。一个覆盖组可以包含多个 coverpoint。

covergroup packet_cg @(posedge clk);
    addr_cp: coverpoint pkt.addr {
        bins low = {[0:50]};
        bins mid = {[51:150]};
        bins high = {[151:255]};
        bins zero = {0};
    }
    data_cp: coverpoint pkt.data;
    cross_cp: cross addr_cp, data_cp;
endgroup

packet_cg cg = new();
// 每次采样时调用 cg.sample();

为什么需要交叉覆盖率?因为单独看 addr 和 data 可能都覆盖了,但 addr=0 且 data=0 这个组合可能从来没出现过。交叉覆盖率就是抓这种“组合漏洞”的。

2.3.2 覆盖率驱动的验证

我习惯在仿真结束后看覆盖率报告。如果某个 bin 是 0%,说明这个场景没测到。这时候就要调整约束,或者写定向用例去覆盖。

个人经验: 覆盖率不是越高越好。我曾经为了把交叉覆盖率跑到 100%,写了 200 个定向用例,结果发现有些组合根本不可能出现(因为约束限制了)。所以,先看“合法组合”,再看覆盖率。

2.4 断言基础——让 DUT 自己说话

断言就是“自动检查器”。你告诉它“应该这样”,它帮你盯着,出错了就报。

2.4.1 立即断言与并发断言

立即断言用 assert,在过程块里用,检查当前时刻的值。

always_ff @(posedge clk) begin
    assert (data !== 8'hxx) else $error("data is X at time %0t", $time);
end

并发断言用 assert property,可以检查时序关系。

property req_ack;
    @(posedge clk) req |=> ##[1:3] ack;
endproperty
assert property (req_ack);

这个断言的意思是:如果 req 拉高,那么 1~3 个时钟周期后 ack 必须拉高。这就是典型的握手协议检查。

2.4.2 断言在 UVM 中的使用

UVM 里断言通常放在 uvm_monitor 或者 uvm_scoreboard 里。我习惯在 monitor 里写断言检查协议,在 scoreboard 里写断言检查数据一致性。

注意: 断言不要写得太复杂。我曾经见过一个断言写了 20 行,结果仿真速度慢了 30%。而且复杂断言 debug 起来非常痛苦。建议:一个断言只检查一件事。

知识体系总览

下面这张图是我自己整理的,把本章四个知识点的关系画出来了。你仔细看看,OOP 是基础,随机化产生激励,覆盖率衡量进度,断言保证正确性。四者缺一不可。

SystemVerilog 验证四大核心 面向对象编程 类与对象 继承与多态 句柄管理 随机化与约束 rand/randc constraint solve...before 功能覆盖率 covergroup coverpoint 交叉覆盖率 断言基础 立即断言 并发断言 property OOP 是骨架,随机化产生激励,覆盖率衡量进度,断言保证正确性

好了,这一章的内容就到这里。这四个知识点是 UVM 的基石,你花时间把它们吃透,后面学 UVM 会轻松很多。记住,验证不是写代码,而是设计场景。随机化给你无限场景,覆盖率告诉你场景够不够,断言帮你盯着场景对不对。

可可精品资料库