跳到主要内容
SystemVerilog

SystemVerilog 验证专用语法

SystemVerilog验证专用语法指南,涵盖动态数组、关联数组、队列、对象复制、乱序scoreboard、class、SVA、coverage与UVM

SV 验证

SystemVerilog 验证专用语法

本章语法主要用于 Testbench 和验证环境。class、coverage、UVM 和大多数断言不可综合;repeat、事件控制等是否可综合则取决于具体 RTL 写法和综合工具。


层级总览

第七层  验证语法   repeat, $random, $display格式符, @(posedge), #N延时
                   定长数组、动态数组、关联数组、队列与容器选型
        ↓            (Testbench专用, 验证工程师必会)
第八层  面向对象   class, extends, $cast, randomize, constraint
        ↓            (验证环境的基础)
第九层  断言       assert property, sequence, ##n
        ↓            (检查协议行为)
第十层  覆盖率     covergroup, coverpoint, cross
        ↓            (衡量验证进度)
第十一层 UVM框架   uvm_component, uvm_driver, uvm_monitor
                   (工业级验证方法学)

第七层:验证语法(Testbench专用)

7.1 repeat — 循环执行N次

// 在下降沿驱动,使信号在下一个上升沿前稳定
repeat(5) begin
    @(negedge clk);
    wr_en   = 1;
    wr_data = $urandom; // $urandom 是 SystemVerilog 内建无符号随机数函数
end
@(negedge clk);
wr_en = 0;

当循环体只需要固定重复执行,且不关心当前是第几次循环时,以上代码也可以写成:

for (int i = 0; i < 5; i++) begin
    @(negedge clk);
    wr_en   = 1;
    wr_data = $urandom;
end
@(negedge clk);
wr_en = 0;

在这个例子中,两者的执行效果相同:都在连续 5 个时钟周期准备随机写数据,信号会在随后的上升沿被 DUT 稳定采样。测试平台若在 posedge 与 DUT 同时驱动/采样,可能产生竞争(race)。

  • repeat(5) 强调“固定重复 5 次”,不提供循环计数变量。
  • for 提供计数变量 i,适合地址递增、按轮次生成数据或打印循环编号等场景。

因此,简单重复时 repeat 更直观;需要使用具体轮次或编号时,应使用 for

7.2 随机数生成

$random                 // 返回32位有符号随机整数(可能为负)
$urandom                // 返回32位无符号随机整数
$urandom_range(255, 0)  // 0~255随机数(推荐,含边界)
$urandom_range(16)      // 0~16随机数(含边界)

$urandom_range(max, min) 的参数顺序是“最大值、最小值”;省略 min 时默认为 0。若把两个参数反向书写,语言会自动交换它们,但建议仍按 max, min 的顺序书写。不要用 $random % 256 生成字节随机数:由于 $random 可能为负,结果也可能为负。

常用写法:

wr_data = $urandom_range(255, 0);    // 8位随机数据:0~255
address = $urandom_range(1023, 0);   // 10位随机地址:0~1023

7.3 格式化输出

$display("写入: %0d", wr_data);    // 十进制,不补空格
$display("地址: %h", addr);        // 十六进制
$display("二进制: %b", data);      // 二进制
$display("时间: %0t", $time);      // 仿真时间

格式符对比(假设 logic [7:0] value = 8'd255):

格式符含义典型输出
%d十进制,使用表达式位宽对应的默认字段宽度255
%0d十进制,取消默认字段宽度255
%h十六进制,通常保留表达式位宽对应的前导零ff
%0h十六进制紧凑显示,省略不必要的前导零ff
%b二进制,通常保留表达式位宽对应的前导零11111111
%0b二进制紧凑显示,省略不必要的前导零11111111
%t$timeformat 设置显示仿真时间,使用默认字段宽度取决于 $timeformat
%0t$timeformat 设置显示仿真时间,取消默认字段宽度12345

这里的 0 表示取消默认字段宽度,并不是“补零”。由于 8 位变量的值 255 已经占满所需宽度,上表中加不加 0 看不出区别;若值为 5,典型输出分别为 %d 5%0d5%h05%0h5%b00000101%0b101%t%0t 的时间单位、精度及后缀均由 $timeformat 决定。

7.6 线程与并发控制

什么是线程

SystemVerilog 中的线程(process)是一条可以独立执行、等待和结束的仿真流程。不同线程在同一段仿真时间内并发推进;一个线程执行 #10@(posedge clk)wait(...) 时,只会暂停自己,不会阻塞其他线程。

initialalways 各自会启动一个独立线程:

fork...join 创建子线程

fork...join 会把其中的每条顶层语句作为一个子线程并发启动:

fork
    drive_transactions();  // 子线程 1
    monitor_outputs();     // 子线程 2
    check_timeout();       // 子线程 3
join

三种结束方式的区别如下:

写法父线程何时继续子线程状态
fork...join所有子线程结束后全部已经结束
fork...join_any任意一个子线程结束后其他子线程继续运行
fork...join_none启动子线程后立即继续所有子线程在后台运行

join_any 不会自动停止尚未结束的子线程。若这些线程不再需要,可紧接着使用 disable fork 停止当前线程创建的剩余子线程:

fork
    send_packet();
    wait_for_timeout();
join_any
disable fork;

经典 Testbench:Driver、Monitor 与 Timeout Watchdog 并行

下面的简化示例同时运行激励、监测和超时监控线程(Timeout Watchdog)。这里的 Watchdog 属于验证环境,用于发现仿真卡死,不是 SoC 内部的硬件看门狗定时器(WDT)。为了让示例独立展示并发关系,使用 assign ready = rst_n 模拟 DUT 始终可以接收请求;实际项目中 ready 应由 DUT 驱动。

module tb_threads;
    timeunit 1ns;
    timeprecision 1ps;

    logic       clk = 0;
    logic       rst_n;
    logic       valid;
    logic       ready;
    logic [7:0] data;

    // 独立线程:持续产生时钟
    always #5ns clk = ~clk;

    // 仅用于简化示例;实际验证时 ready 来自 DUT
    assign ready = rst_n;

    // module 中的 task 默认使用静态存储。
    // 声明为 automatic 后,每次调用都有独立的参数和局部变量,
    // 便于任务今后被多个线程并行或重复调用。
    // 本例中的任务都只调用一次且没有局部变量,因此不写 automatic 也不影响当前行为。
    task automatic reset_dut();
        rst_n =  0;
        valid =  0;
        data  = '0;
        repeat (2) @(posedge clk);
        @(negedge clk);
        rst_n = 1;
    endtask

    task automatic drive_transactions();
        repeat (5) begin
            // 在下降沿驱动,保证信号在上升沿采样前稳定
            @(negedge clk);
            valid = 1;
            data  = $urandom_range(255, 0);

            // 本例 ready 始终为 1,因此在下一个上升沿完成握手
            @(posedge clk);
            @(negedge clk);
            valid = 0;
        end
    endtask

    task automatic monitor_transactions();
        forever begin
            @(posedge clk);
            if (valid && ready)
                $display("t=%0t:监测到 data=0x%0h", $time, data);
        end
    endtask

    task automatic timeout_watchdog();
        #1000ns;
        $fatal(1, "测试超时");
    endtask

    initial begin
        reset_dut();

        fork
            drive_transactions();   // 有限线程:发送 5 笔事务后结束
            monitor_transactions(); // 永久线程:持续监测接口
            timeout_watchdog();     // 超时监控线程:防止仿真卡死
        join_any

        // 正常情况下 Driver 最先结束,停止 Monitor 和 Timeout Watchdog
        disable fork;
        $finish;
    end
endmodule

这里不能直接使用 joinmonitor_transactions() 中有 forever,它不会主动返回,父线程会一直等待。采用 join_any 后,Driver 正常发送完 5 笔事务即可让父线程继续;随后 disable fork 清理 Monitor 和 Timeout Watchdog。这个结构体现了经典 Testbench 的并发分工:

Testbench
├── Driver             产生并发送激励
├── Monitor            持续采集 DUT 行为
└── Timeout Watchdog   检查仿真是否超时或卡死

需要注意,线程描述的是仿真程序的并发执行流程,不代表一定综合出硬件。fork...join、类任务和带时间控制的 Testbench 代码通常只用于验证。

7.7 系统函数速查

函数作用示例
$random32位有符号随机整数$random
$urandom_range指定闭区间内的无符号随机数$urandom_range(15, 0)
$display打印信息$display("data=%h", data)
$time仿真时间$display("t=%0t", $time)
$finish结束仿真$finish
$dumpfile指定波形文件$dumpfile("wave.vcd")
$dumpvars指定dump信号$dumpvars(0, tb)
$cast运行时检查并转换类型if ($cast(child, parent))

7.8 验证常用容器:数组与队列

验证环境需要保存 payload、采样数据、期望结果和未完成事务。选择容器时,先确定:数量是否固定、按位置还是按键访问、是否需要保持顺序

7.8.1 四种容器的区别

类型声明示例下标和大小常用操作验证应用
定长数组int a[4];本例下标为 0~3,元素数量固定a[i]foreach$size(a)固定 lane 数的计数器、固定长度采样窗口
动态数组int a[];下标为 0~size()-1,运行时确定数量new[N]size()delete()可变长度 packet payload、burst 数据
关联数组int a[int unsigned];按键查找,键可以不连续;也可使用字符串等类型作为键exists()num()delete(key)稀疏参考内存、按 ID 查找未完成事务
队列int a[$];下标为 0~size()-1,插入和删除时改变数量push_back()pop_front()size()顺序 scoreboard、事务缓存、历史记录

注意三种相似写法:a[4] 中的 4 是固定数量;a[int] 中的 int键的类型a[$] 表示队列。动态数组的 [] 则留空。

packed / unpacked 是另一个分类角度:

logic [31:0] word;          // packed:一个 32 位向量
logic [31:0] lanes[4];      // unpacked 定长数组,每个元素是 32 位向量
logic [31:0] payload[];     // unpacked 动态数组
logic [31:0] memory[longint unsigned]; // unpacked 关联数组
logic [31:0] history[$];    // unpacked 队列

变量名左侧的 [31:0] 描述元素的位宽,右侧的维度描述容器。多维数组也是另一个维度概念,可以与上述容器组合。这里说下标连续,并不规定仿真器底层如何分配物理内存。

下面带有 module ... endmodule 的基础示例可以分别作为独立 SV 文件运行;类和约束片段则需要放入相应的测试环境。

7.8.2 定长数组与动态数组:固定数量和运行时分配

module tb_dynamic_array;
    int fixed_a[4] = '{10, 20, 30, 40};
    int data_a[];
    int copied_a[];

    initial begin
        data_a = new[4];                 // 创建 4 个 int 元素,初值均为 0
        foreach (data_a[i])
            data_a[i] = fixed_a[i];

        data_a = new[6](data_a);         // 扩到 6 个元素,保留原有的前 4 个
        $display("size=%0d data=%p", data_a.size(), data_a);
        // size=6,内容为 10、20、30、40、0、0

        copied_a = data_a;               // 复制数组元素,并自动调整目标动态数组大小
        copied_a[0] = 99;
        $display("original=%0d copy=%0d", data_a[0], copied_a[0]);
        // original=10 copy=99:两个 int 数组独立

        data_a = new[2](data_a);         // 缩到 2 个,只保留前两个元素
        data_a = new[2];                 // 未提供旧数组:重新初始化为两个 0
        data_a.delete();                 // 清空,size() 变为 0
    end
endmodule
  • new[N] 创建或重新分配动态数组;new[N](old_array) 在分配时复制能放得下的旧元素。
  • data_a[1000] 不会自动扩容。必须先确保下标在当前有效范围内。
  • 动态数组的 delete() 清空整个数组,没有 delete(i) 这种删除单个位置的用法;频繁插入、删除元素时优先考虑队列。
  • 定长数组可以使用任意合法固定范围,例如 int a[8:5],所以并非所有定长数组都从 0 开始。遍历时使用 foreach 更方便。
  • 上例元素为 int,因此数组赋值后数据独立。如果元素是 class 句柄,数组赋值复制的仍是句柄,详见第 8.1 节的对象快照说明。

应用:可随机化的 packet payload。

class packet;
    rand byte unsigned payload[];

    constraint c_payload {
        payload.size() inside {[1:64]};
        foreach (payload[i])
            payload[i] inside {[0:255]};
    }
endclass

对该对象调用 randomize() 时,求解器会按 size() 约束决定随机动态数组的大小,再随机化元素,无需先手动 new[]。这里的字节范围约束用于展示 foreach,对 byte unsigned 本身是冗余的;实际项目可换成协议允许的取值或元素之间的关系。始终检查 randomize() 的返回值。

7.8.3 关联数组:按键查找与稀疏存储

module tb_associative_array;
    int expected[int unsigned];
    int count_by_name[string];

    initial begin
        expected[3]    = 10;
        expected[1000] = 20;
        $display("entries=%0d", expected.num()); // 2,不是 1001

        if (expected.exists(1000))
            $display("value=%0d", expected[1000]);

        expected[3] = 99;                       // 更新已有键,数量仍是 2
        foreach (expected[id])
            $display("id=%0d value=%0d", id, expected[id]);

        count_by_name["read"]  = 5;
        count_by_name["write"] = 2;

        expected.delete(3);                    // 只删除键 3
        expected.delete();                     // 删除所有条目
    end
endmodule

关联数组不需要 new[]。写入不存在的键会创建条目,但普通读取不存在的键不会自动创建条目;读取结果遵循元素的默认值或数组指定的默认值规则,仿真器还可能给出警告。不能用“读出来是 0”判断键是否存在,应使用 exists() 对 class 元素尤其要先检查存在性和非空句柄。

方法含义
aa.num() / aa.size()已存在条目的数量,不是最大键值加一
aa.exists(key)键是否存在,返回 0 或 1
aa.delete(key)删除指定键,不改变其他键的含义
aa.delete()清空整个关联数组
aa.first(key) / aa.last(key)把第一个/最后一个键写入参数,返回是否成功
aa.next(key) / aa.prev(key)从当前键寻找后一个/前一个键,返回是否成功

遍历整数键时按数值顺序,字符串键按字典序,并非按照写入先后顺序。需要记录先后顺序时应使用队列。优先写明键类型,如 [int unsigned][string],避免通配键 [*] 带来的遍历和类型限制。

应用:稀疏参考内存。

module tb_sparse_memory;
    logic [7:0] ref_mem[longint unsigned]; // 按字节地址保存期望值

    initial begin
        ref_mem[64'h0000_1000] = 8'hA5;
        ref_mem[64'h8000_1000] = 8'h5A;

        $display("stored bytes=%0d", ref_mem.num()); // 只记录两个字节地址
        if (ref_mem.exists(64'h8000_1000))
            $display("expected=%h", ref_mem[64'h8000_1000]);
    end
endmodule

参考模型还需按设计规格处理字节使能、地址映射、未写地址的初始值和并发读写顺序。稀疏内存记录“某地址当前应有的数据”;未完成事务表记录“某请求还在等待哪一个响应”,两者不能混为一张表。

7.8.4 队列:按顺序插入、取出和删除

module tb_queue;
    int q[$];
    int value;

    initial begin
        q.push_back(10);    // [10]
        q.push_back(20);    // [10, 20]
        q.push_front(5);    // [5, 10, 20]
        q.insert(1, 7);     // [5, 7, 10, 20]
        q.delete(2);        // [5, 7, 20],后续元素的位置会前移

        if (q.size() > 0) begin
            value = q.pop_front(); // value=5,q=[7, 20]
            $display("first=%0d remaining=%p", value, q);
        end

        if (q.size() > 0)
            value = q.pop_back();  // value=20,q=[7]

        q.delete();                // 清空
    end
endmodule
  • push_back() 配合 pop_front() 实现 FIFO;push_back() 配合 pop_back() 实现栈。
  • 队列也能用 q[i] 随机访问,但下标表示当前位置,不是稳定的事务 ID。删除队头后,其余元素的下标会改变。
  • pop_front() / pop_back() 不会阻塞等待数据。空队列时应先检查 size(),不能把返回的默认值当成有效事务。
  • int q[$:15] 的上限为 16 个元素。有界队列满了不会提供硬件 FIFO 式的反压或阻塞等待;应用需要显式处理容量,避免截断或丢弃数据。
  • 队列适合保存历史记录,但不是线程同步机制。需要生产者/消费者阻塞通信时,可用 mailbox 或 UVM TLM FIFO;它们传递 class 对象时同样需要约定对象所有权。

7.8.5 遍历、查找和排序

module tb_array_methods;
    int values[$] = '{30, 10, 20, 10};
    int indices[$];

    initial begin
        foreach (values[i])
            $display("index=%0d value=%0d", i, values[i]);

        indices = values.find_index() with (item == 10);
        $display("matching indices=%p", indices); // 两个位置:1、3

        values.sort();    // 原地排序:10、10、20、30
        values.reverse(); // 反转当前顺序:30、20、10、10
    end
endmodule

find_index() 返回的是匹配下标组成的队列;没有匹配时结果为空。find() 则返回匹配元素组成的队列。数组方法是否适用还取决于容器和元素类型,例如关联数组不支持上述 sort()reverse() 排序方法。

在 scoreboard 中,按明确 ID 查表通常比自行遍历所有未完成事务更直接。但不要声称所有仿真器的关联数组查询都固定为 O(1),底层实现不由这种声明保证。也不要先把实际事务排序或按 payload 随意寻找“能比对成功的一项”,这样可能掩盖 DUT 的非法乱序。

7.8.6 根据验证需求选容器

验证需求推荐容器选择原因
8 个固定端口的计数器定长数组端口数量固定,按端口号访问
长度运行时确定的数据包动态数组一次确定或随机化整个 payload 的大小
最近 N 笔采样记录队列+显式长度限制新记录入队,超出窗口后删除最旧记录
严格按请求顺序返回的结果一个期望队列实际结果与队头比较
未完成事务匹配键唯一,允许乱序返回关联数组根据响应键直接找到期望事务
不同 ID 可乱序,同 ID 内多笔事务保序关联数组,每个键对应一个队列按 ID 定位,再按该 ID 的先后顺序比较
大地址空间,只访问少量位置关联数组只为已记录的地址保存数据

乱序 scoreboard 的具体例子见第 11.6 节。声明为数组或队列只解决保存方式;匹配规则、合法顺序、超时和结束检查仍需要另外实现。

#1step#0

default input #1step output #0;
  • input #1step:在 clocking event 前一个最小仿真步长采样输入,得到边沿前稳定值;
  • output #0:在 clocking event 对应时刻驱动输出,不增加额外时间延迟;
  • 1step 不是一个时钟周期,也不是固定的 1ns,而是由仿真时间精度决定的最小步长。

通过明确采样和驱动偏移,可以减少 Testbench 与 DUT 在同一时间槽访问相同信号时产生的 race。

在 Testbench 中使用

fifo_if vif(clk);

// Driver进程:等待drv_cb的下降沿事件,然后通过时钟块驱动
initial begin
    @(vif.drv_cb);
    vif.drv_cb.wr_en   <= 1'b1;
    vif.drv_cb.wr_data <= 8'h55;

    @(vif.drv_cb);
    vif.drv_cb.wr_en   <= 1'b0;
end

// Monitor进程:每个mon_cb事件读取本次采样快照
always @(vif.mon_cb) begin
    $display("wr_en=%b wr_data=%h full=%b",
             vif.mon_cb.wr_en,
             vif.mon_cb.wr_data,
             vif.mon_cb.full);
end

Assertion 中的 default clocking

default clocking cb @(posedge clk);
endclocking

property p_not_both;
    !(full && empty);
endproperty

assert property (p_not_both);

default clocking 为当前作用域内没有显式写采样事件的 property 提供默认时钟,避免每条断言重复写 @(posedge clk)

是否必须使用


第八层:面向对象(class)

8.1 class 基础

什么是方法

方法 = 类能做的事情(操作数据的函数)

内存模型

句柄 txn          对象(堆内存)
┌────────┐       ┌─────────────────┐
│ 0x1000 │──────>│ addr = 0x100    │
└────────┘       │ data = 0xABCD   │
                 │ wr   = 1        │
                 └─────────────────┘

句柄 vs 对象

句柄(handle)= 对象的地址(类似指针)

句柄 txn          对象(内存中)
┌────────┐       ┌─────────────┐
│ 0x1000 │──────>│ addr = 0x100│
└────────┘       │ data = 0xFF │
                 └─────────────┘
transaction txn;       // 声明句柄(还没指向对象)
transaction txn2;

txn = new();           // 创建对象,句柄指向它
txn2 = txn;            // 两个句柄指向同一个对象

txn2.addr = 32'hFF;   // 修改txn.addr也会变(同一个对象)

重要区别:

操作含义
transaction txn;声明句柄(空的)
txn = new();创建对象,句柄指向它
txn2 = txn;两个句柄指向同一个对象

简单比喻:句柄是”遥控器”,对象是”电视”

遥控器A ──┐

遥控器B ──┼──> 同一台电视

遥控器C ──┘

一个对象可以有多个句柄。通过任意句柄修改对象成员,其他句柄都能看到变化;但给某个句柄重新赋值、让它指向另一个对象,不会改变其他句柄的指向。

容器里的句柄与对象快照

把 transaction 放进数组或队列,不会自动复制 transaction 对象。

class sample_item;
    int data;
endclass

module tb_handle_snapshot;
    sample_item q[$];
    sample_item source;
    sample_item snapshot;

    initial begin
        source = new();
        source.data = 10;
        q.push_back(source);       // 保存句柄,两者指向同一个对象

        source.data = 20;
        $display("queued=%0d", q[0].data); // 20,并非入队时的 10

        snapshot = new source;     // SV 浅拷贝:新建对象并复制其成员
        q.push_back(snapshot);
        source.data = 30;
        $display("shared=%0d snapshot=%0d", q[0].data, q[1].data);
        // shared=30 snapshot=20;本例只有 int 成员,所以快照独立
    end
endmodule
操作容器/外层对象内部 class 对象
b = a,a/b 是 class 句柄指向同一个对象共享
queue_b = queue_a复制队列元素,两个队列可分别增删若元素是句柄,所指对象仍共享
b = new a创建新对象并进行浅拷贝若成员是句柄,所指子对象仍共享
正确实现的递归复制创建独立快照按复制策略复制需要独立的子对象

动态数组、关联数组的整体赋值也要区分“复制元素”和“复制元素指向的对象”。在 UVM 中通常用 clone() 创建快照,或先创建目标对象再调用 copy();默认 clone() 通过 create()copy() 完成复制。需要通过字段宏或 do_copy() 正确实现成员复制,只有 factory 注册宏并不代表所有成员都会被复制。包含子对象时,还要检查它们的复制策略。

Scoreboard 应在接收事务、保存到容器时建立稳定快照,或者与发送方约定“发布后不再修改该对象”。第 11.6 节使用 clone() 保存快照。

当一个 class 把另一个 class 的句柄保存为成员,并负责创建和管理该对象时,这些成员句柄还可以形成**合成(composition)**关系。句柄是实现机制,合成是对象之间的组织关系,详见后面的“继承与合成的选择”。

8.2 继承(extends)

优势说明
代码复用父类5个属性不用重复写
易于维护修改父类,所有子类自动生效
扩展方便子类只需关注特有属性
统一接口所有transaction可以用同一个句柄

关键点

关键字作用
extends继承父类
super.new()调用父类构造函数(必须第一行)
virtual允许子类重写方法

virtual 关键字

// 父类方法不加virtual
class base;
    function void display();
        $display("base");
    endfunction
endclass

class child extends base;
    function void display();  // 这是隐藏,不是重写
        $display("child");
    endfunction
endclass

// 父类方法加virtual
class base;
    virtual function void display();  // 允许重写
        $display("base");
    endfunction
endclass

class child extends base;
    function void display();  // 这是重写
        $display("child");
    endfunction
endclass
情况行为
不加virtual隐藏(hiding)
virtual重写(override)

建议:只有需要通过父类句柄体现多态行为的方法才加 virtual。构造函数等不参与多态分派的方法无需声明为 virtual

继承关系

base_transaction          extended_transaction
┌──────────────┐          ┌──────────────────┐
│ addr         │          │ addr (继承)      │
└──────────────┘          │ data (新增)      │
                          │ wr   (新增)      │
                          └──────────────────┘

使用示例

extended_transaction ext_txn;

initial begin
    ext_txn = new(32'h100, 32'hABCD);
    
    // 可以访问父类和子类的属性
    $display("addr = %h", ext_txn.addr);   // 父类
    $display("data = %h", ext_txn.data);   // 子类
    $display("wr   = %b", ext_txn.wr);     // 子类
end

方法重写(override)

仅看类的定义不容易看出重写的效果。关键场景是:父类句柄指向子类对象,然后通过父类句柄调用方法

class base_transaction;
    logic [31:0] addr;

    virtual function void display();
        $display("base: addr=%h", addr);
    endfunction
endclass

class extended_transaction extends base_transaction;
    logic [31:0] data;

    function void display();  // 重写父类的虚方法
        $display("extended: addr=%h, data=%h", addr, data);
    endfunction
endclass

module tb;
    initial begin
        base_transaction     base_txn;  // 父类句柄
        extended_transaction ext_txn;   // 子类句柄

        ext_txn      = new();
        ext_txn.addr = 32'h1000;
        ext_txn.data = 32'hABCD;

        // 父类句柄指向子类对象
        base_txn = ext_txn;

        base_txn.display();
    end
endmodule

父类的 display() 带有 virtual,因此根据 base_txn 指向的实际对象类型调用子类版本:

extended: addr=00001000, data=0000abcd

如果删除父类方法前面的 virtual,其他代码保持不变:

function void display();
    $display("base: addr=%h", addr);
endfunction

再次执行同一条语句:

base_txn.display();

输出将变为:

base: addr=00001000
父类方法base_txn.display() 的结果判断依据
不加 virtual调用父类版本看句柄的声明类型
virtual调用子类版本看句柄指向的实际对象类型

**记忆:**没有 virtual 看句柄类型;有 virtual 看实际对象类型。

安全的向下转换:$cast

父类句柄可以直接接收子类句柄,这称为向上转换。反过来,父类句柄不一定真的指向目标子类对象,因此不能直接赋给子类句柄,需要用 $cast 在运行时检查:

base_transaction     base_txn;
extended_transaction ext_txn;
extended_transaction generated_txn;

generated_txn      = new();
generated_txn.addr = 32'h1000;
generated_txn.data = 32'hABCD;

base_txn = generated_txn;  // 向上转换:子类 -> 父类,隐式且安全

// 向下转换:父类 -> 子类,检查实际对象是否兼容
if ($cast(ext_txn, base_txn)) begin
    $display("data = %h", ext_txn.data);
end
else begin
    $error("base_txn实际指向的对象不是extended_transaction");
end

语法为 $cast(目标变量, 源表达式)目标变量的声明类型就是要转换到的类型。其工作过程如下:

  1. 仿真器查看源句柄当前指向对象的实际类型;
  2. 若该对象与目标类型兼容,将同一个对象的句柄赋给目标变量并返回 1
  3. 若不兼容,不修改目标变量并返回 0

$cast 只转换句柄,既不会调用 new(),也不会复制对象。转换成功后,base_txnext_txn 仍然指向同一个对象。

base_txn ──┐
           ├──> 同一个 extended_transaction 对象
ext_txn  ──┘

失败场景如下。普通父类对象不是子类对象,所以转换失败:

base_transaction     plain_txn;
extended_transaction ext_txn;

plain_txn = new();
ext_txn   = null;

if (!$cast(ext_txn, plain_txn)) begin
    $display("类型不兼容");  // ext_txn仍为null
end

常用写法有两种:

// 函数形式:主动处理成功或失败,验证代码中更常用
if (!$cast(ext_txn, base_txn))
    $fatal(1, "类型转换失败");

// 任务形式:忽略返回值;失败时由仿真器报告运行时错误
$cast(ext_txn, base_txn);
写法检查时机失败行为典型用途
target_type'(expr)不检查对象的运行时类型直接按目标类型解释或转换数值位宽、符号等静态转换
$cast(dst, src)仿真运行时检查返回 0,可自行报错或跳过父类句柄向子类句柄转换、UVM 基类接口取回具体对象

在 UVM 中,factory、clone()uvm_object 形参等经常返回或接收基类句柄;要访问具体 transaction 的字段时,通常先用 $cast 检查实际类型。不要在未检查返回值的情况下继续解引用目标句柄,否则类型错误会变成更难定位的空句柄或旧句柄问题。

继承与合成的选择

继承和合成都能复用已有代码,但表达的关系不同:

继承:A is-a B,A是一种B
合成:A has-a B,A拥有一个B

继承使用 extends。例如 write_transaction 是一种更具体的 base_transaction

class write_transaction extends base_transaction;
    logic [31:0] data;
endclass

合成通常通过 class 的成员句柄实现。例如一个 agent 拥有 driver 和 monitor:

class simple_driver;
endclass

class simple_monitor;
endclass

class simple_agent;
    // 成员句柄:声明后初始为null
    simple_driver  driver;
    simple_monitor monitor;

    function new();
        // agent负责创建和管理自己的组成对象
        driver  = new();
        monitor = new();
    endfunction
endclass

对应关系为:

simple_agent
├── driver
└── monitor

8.3 随机化(randomize)

基本语法

// 定义可随机化的transaction类
class random_transaction;
    rand logic [31:0] addr;   // rand: 可随机化(每次随机可能重复)
    rand logic [31:0] data;   // rand: 可随机化
    randc bit [7:0]   id;     // randc: 随机循环(不重复,遍历所有值后重来)

    // 约束:限制随机范围
    constraint c_addr {
        addr inside {[32'h0000:32'h00FF]};  // 地址范围:0x0000~0x00FF
    }

    constraint c_data {
        data != 0;  // 数据不能为0
    }
endclass

// 使用:生成随机激励
random_transaction txn;           // 声明句柄
initial begin
    txn = new();                  // 创建对象
    repeat(10) begin              // 重复10次
        assert(txn.randomize())   // 随机化成功返回1,失败返回0
            $display("addr=%h, data=%h", txn.addr, txn.data);  // 打印随机值
        else
            $error("随机化失败");  // 约束冲突时报错
    end
end

randomize() 返回值

// 返回1:随机化成功
// 返回0:随机化失败(约束冲突)
assert(txn.randomize())
    $display("成功");
else
    $error("失败");

内联约束(inline constraint)

// 只随机化某些字段
assert(txn.randomize() with {
    addr == 32'h100;      // 强制addr=0x100
    data inside {[0:100]}; // data在0~100
})

8.4 约束详解

class constrained_transaction;
    rand logic [7:0]  cmd;
    rand logic [31:0] addr;
    rand logic [31:0] data;
    rand bit [2:0]    burst_len;

    constraint c_cmd {
        cmd inside {8'h01, 8'h02, 8'h03};  // 枚举值
    }

    constraint c_addr {
        addr[1:0] == 2'b00;  // 地址对齐
        addr < 32'h1000;     // 地址范围
    }

    constraint c_burst {
        burst_len inside {[1:8]};  // 突发长度1~8
        soft data == 0;            // soft: 软约束(可被覆盖)
    }

    // 条件约束
    constraint c_cond {
        if (cmd == 8'h01)
            addr inside {[32'h000:32'h0FF]};
        else
            addr inside {[32'h100:32'h1FF]};
    }
endclass

条件约束

constraint c_cond {
    if (cmd == 8'h01)
        addr inside {[32'h000:32'h0FF]};
    else
        addr inside {[32'h100:32'h1FF]};
}

约束块命名

constraint c1 { ... }  // 命名约束块
constraint c2 { ... }  // 可以单独禁用/启用

地址对齐详解

什么是地址对齐?

地址对齐 = 地址必须是某个边界的整数倍

// 4字节对齐:地址必须是4的倍数
addr[1:0] == 2'b00;  // 低2位必须为00

// 8字节对齐:地址必须是8的倍数
addr[2:0] == 3'b000;  // 低3位必须为000

为什么需要对齐?

CPU、总线和存储器可能按字(word)访问数据。常见的 32 位访问一次为 4 字节,因此经常要求 4 字节对齐;实际对齐要求仍以 ISA、总线协议和存储器属性为准。

不对齐会怎样?

情况后果
对齐一次读取完成,效率高
不对齐需要读两次再拼接,效率低,甚至报错
地址0x0000(对齐):
  一次读取:[0x0000-0x0003] → 完成

地址0x0001(不对齐):
  读取1:[0x0000-0x0003]
  读取2:[0x0004-0x0007]
  拆分拼接 → 效率低

CPU位宽与对齐关系

CPU位宽对齐要求检查位一次读取
8位1字节对齐无要求1字节
16位2字节对齐addr[0]==02字节
32位4字节对齐addr[1:0]==004字节
64位8字节对齐addr[2:0]==0008字节

并非总是 4 的倍数:对齐要求取决于访问宽度和具体协议

// 16位CPU:2字节对齐
constraint c_align_16 {
    addr[0] == 1'b0;  // 低1位为0
}

// 32位CPU:4字节对齐
constraint c_align_32 {
    addr[1:0] == 2'b00;  // 低2位为0
}

// 64位CPU:8字节对齐
constraint c_align_64 {
    addr[2:0] == 3'b000;  // 低3位为0
}

二进制规律:低N位为0,就是2^N的倍数

十进制    二进制     低2位    低3位
0        0000      00 ✅    000 ✅  是4和8的倍数
4        0100      00 ✅    100 ❌  是4的倍数,不是8的
8        1000      00 ✅    000 ✅  是4和8的倍数
1        0001      01 ❌    001 ❌  都不是
2        0010      10 ❌    010 ❌  都不是
3        0011      11 ❌    011 ❌  都不是

约束写法


// 地址对齐 + 范围约束
constraint c_addr {
    addr[1:0] == 2'b00;      // 4字节对齐
    addr inside {[32'h000:32'h1FF]};  // 地址范围
}

// 用取模运算符写对齐约束(效果相同)
constraint c_align_mod {
    addr % 4 == 0;           // 等价于 addr[1:0] == 0
}

位操作 vs 取模,两种写法对比

// 写法1:位操作(推荐,表达清晰且通常更利于约束求解)
constraint c1 { addr[1:0] == 2'b00; }

// 写法2:取模运算(更直观)
constraint c2 { addr % 4 == 0; }

核心概念总结

概念本质目的
约束加条件限制随机值范围让随机值有意义,符合协议规范
随机自动生成测试向量提高测试效率和覆盖率
对齐让CPU一次读完数据提高访问效率,减少读取次数

示例4:scoreboard - 比较结果

// scoreboard类:比较预期结果和实际结果
class scoreboard;
    int pass_count = 0;          // 通过计数
    int fail_count = 0;          // 失败计数

    // 检查函数:比较expected和actual
    function void check(input logic [31:0] expected,   // 预期值
                        input logic [31:0] actual);     // 实际值
        if (expected === actual) begin                  // 全等比较
            $display("PASS: %h == %h", expected, actual);  // 打印通过
            pass_count++;                               // 通过计数+1
        end else begin
            $display("FAIL: %h != %h", expected, actual);  // 打印失败
            fail_count++;                               // 失败计数+1
        end
    endfunction
endclass

使用示例

scoreboard sb;
initial begin
    sb = new();
    sb.check(32'hDEAD_BEEF, actual_data);  // 比较
end

// 最后统计
$display("Pass: %0d, Fail: %0d", sb.pass_count, sb.fail_count);

示例5:parameterized class - 参数化类

class fifo #(parameter WIDTH = 8, DEPTH = 16);
    logic [WIDTH-1:0] queue[$];

    function bit push(input logic [WIDTH-1:0] data);
        if (queue.size() >= DEPTH) begin
            $error("FIFO model overflow");
            return 0;
        end
        queue.push_back(data);
        return 1;
    endfunction

    function bit pop(output logic [WIDTH-1:0] data);
        if (queue.size() == 0) begin
            $error("FIFO model underflow");
            return 0;
        end
        data = queue.pop_front();
        return 1;
    endfunction
endclass

// 使用
fifo #(8, 16)   fifo8;    // 8位宽,深度16
fifo #(32, 64)  fifo32;   // 32位宽,深度64

深度、地址位数、数据位宽、字节数详解

四个核心概念

概念定义示例
深度能存多少个数据16个
地址位数寻址深度需要几位4位(2^4=16)
数据位宽每个数据多少位8位(1字节)
字节数总容量16×1=16字节

关系公式

地址位数 = $clog2(深度)
字节数 = 深度 × 数据位宽 / 8

示例计算

fifo #(8, 16)   fifo8;
// 深度:16
// 地址位数:4位(2^4=16)
// 数据位宽:8位(1字节)
// 字节数:16 × 1 = 16字节

fifo #(32, 64)  fifo32;
// 深度:64
// 地址位数:6位(2^6=64)
// 数据位宽:32位(4字节)
// 字节数:64 × 4 = 256字节

常见配置表

深度地址位数数据位宽字节数
42位8位4字节
83位8位8字节
164位8位16字节
646位32位256字节
102410位32位4096字节

第九层:断言(SVA)

9.1 基础断言

// 简单断言
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> ##[1:3] ack
) else $error("req 后 1~3 个周期内未收到 ack");

// 含义:req 有效后,1~3 个周期内必须有 ack

9.2 序列(sequence)

// 定义序列
sequence s_wr;
    awvalid && awready;
endsequence

sequence s_data;
    wvalid && wready;
endsequence

// 使用序列
property p_wr_data;
    @(posedge clk) disable iff (!rst_n)
    s_wr |-> ##[0:1] s_data;
endproperty

assert property (p_wr_data);

这里仅演示 sequence 的定义和组合,不是完整 AXI 协议断言:AXI 的 AW 和 W 通道可独立握手,不能强制所有设计都满足上述先后关系。

稍复杂的例子:描述一个跨多个周期的完整事务

// 子序列1:发出 req 后,1~3 个周期内必须收到 grant
sequence s_req_grant;
    req ##[1:3] grant;
endsequence

// 子序列2:连续两个周期完成数据握手
sequence s_two_data_beats;
    (data_valid && data_ready)[*2];
endsequence

// 组合序列:
// 请求授权完成后的下一周期开始传输两个数据,
// 最后一个数据传输完成后的下一周期出现 done
sequence s_complete_transfer;
    s_req_grant ##1 s_two_data_beats ##1 done;
endsequence

property p_complete_transfer;
    @(posedge clk) disable iff (!rst_n)
    start |=> s_complete_transfer;
endproperty

assert property (p_complete_transfer)
    else $error("完整事务的时序不符合要求");

这里的 sequence 不再只是给单个条件起名字,而是描述并组合一段跨周期过程:

start
  → 下一周期出现 req
  → req 后 1~3 个周期内出现 grant
  → 下一周期开始连续完成两次数据握手
  → 下一周期出现 done

例如,下面是一种满足断言的时序:

周期             T0    T1    T2    T3    T4    T5    T6
start             1
req                     1
grant                               1
data_valid/ready                         1     1
done                                                   1

其中,##[1:3] 表示等待 1~3 个周期,[*2] 表示前面的条件必须连续匹配两个周期。s_complete_transfer 又把两个子序列组合成了一个可以复用的完整时序模式。

9.3 常用断言操作符

蕴含符号(Implication)

// |-> 重叠蕴含:前提成立的同一周期检查结论
assert property (@(posedge clk)
    req |-> ack
);
// 含义:如果 req 为真,那么同一周期 ack 必须为真

// |=> 非重叠蕴含:前提成立的下一周期开始检查结论
assert property (@(posedge clk)
    req |=> ack
);
// 含义:如果 req 为真,那么下一周期 ack 必须为真

// 对比时序图:
// clk:    ↑   ↑   ↑
// req:    1   0   0
// ack(|->): 1   0   0  ← 同一周期检查
// ack(|=>): 0   1   0  ← 下一周期检查

延迟操作符(##)

// ##n 延迟n个周期
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> ##3 ack
);
// 含义:req 为真后,恰好 3 个周期时 ack 必须为真

// ##[min:max] 延迟范围
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> ##[1:5] ack
);
// 含义:req 为真后,1~5个周期内必须有 ack

// ##0 同一周期(与 |-> 类似)
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> ##0 ack
);
// 含义:req 为真时,同一周期 ack 必须为真

// 无界延迟:表达“最终发生”,不能替代有上限的 timeout 检查
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> strong(##[1:$] ack)
);
// 含义:req 为真后,最终必须有 ack;工程中的 timeout 通常应写成有界范围

组合使用

// 多步延迟
assert property (@(posedge clk) disable iff (!rst_n)
    req |-> ##1 req_ack ##2 data_valid
);
// 含义:req → 1周期后 req_ack → 2周期后 data_valid

// 范围延迟 + 蕴含
assert property (@(posedge clk) disable iff (!rst_n)
    $rose(valid) |-> ##[1:3] ready && (data == expected)
);
// 含义:valid 上升沿后,1~3周期内 ready 为真且数据正确

系统函数

高频(必须掌握):

// $past(signal) - 前1个周期的值
assert property (@(posedge clk) disable iff (!rst_n)
    wr_data == $past(wr_data) + 1
);
// 含义:复位释放后,数据每周期递增1;disable iff 也避免仿真起始时 $past 无历史值

// $past(signal, n) - 前n个周期的值
assert property (@(posedge clk)
    $past(valid, 2) |-> ack
);
// 含义:2个周期前的 valid 为真时,当前 ack 必须为真

// $rose(signal) - 信号上升沿(从0变1)
assert property (@(posedge clk)
    $rose(valid) |-> ##[1:3] ready
);
// 含义:valid 上升沿后,1~3周期内 ready 必须为真

// $fell(signal) - 信号下降沿(从1变0)
assert property (@(posedge clk)
    $fell(valid) |-> !ready
);
// 含义:valid 下降沿时,ready 必须为假

// $stable(signal) - 信号稳定(没有变化)
assert property (@(posedge clk)
    $stable(data) |-> ack
);
// 含义:data 没变时,ack 必须为真

// $changed(signal) - 信号变化(有改变)
assert property (@(posedge clk)
    $changed(data) |-> ##1 valid
);
// 含义:data 变化后,下一周期 valid 必须为真

中频(偶尔使用):

// $countones(signal) - 统计1的个数
assert property (@(posedge clk)
    $countones(flags) == 4 |-> done
);
// 含义:flags 中有4个1时,done 必须为真

// $onehot(signal) - 只有1位为1
assert property (@(posedge clk)
    $onehot(sel) |-> valid
);
// 含义:sel 只有1位为1时,valid 必须为真

低频(很少使用):

// $onehot0(signal) - 最多1位为1(可以全0)
assert property (@(posedge clk)
    $onehot0(sel) |-> valid
);

// $isunknown(signal) - 是否有未知态X/Z
assert property (@(posedge clk)
    !$isunknown(data) |-> valid
);
// 含义:data 没有X/Z时,valid 必须为真

// $sampled(signal) - 取得并发断言当前采样值(常用于 assertion 的 action block)
assert property (@(posedge clk)
    $sampled(cmd) == 8'h01 |-> rd_en
);

其他操作符

// within:左侧序列必须完全落在右侧序列的匹配窗口内
sequence s_valid_burst;
    valid[*1:3];
endsequence
sequence s_transfer_window;
    $rose(start) ##[1:5] done;
endsequence
assert property (@(posedge clk) s_valid_burst within s_transfer_window);

// throughout:在右侧序列匹配的每个周期,左侧表达式都必须为真
sequence s_reset_active;
    !rst_n[*1:$];
endsequence
assert property (@(posedge clk) !wr_en throughout s_reset_active);

9.4 覆盖断言

// cover属性:统计场景出现次数
property p_cover_wr;
    @(posedge clk)
    wr_en && !full |=> wr_en && (wr_data == $past(wr_data) + 1);
endproperty

cover property (p_cover_wr);

// assume属性:假设条件(用于形式验证)
assume property (@(posedge clk)
    reset |=> !wr_en
);

第十层:覆盖率(covergroup)

10.1 基础covergroup

class transaction_coverage;
    rand logic [7:0]  cmd;
    rand logic [31:0] addr;
    rand logic [31:0] data;

    covergroup cg;
        // 命令覆盖点
        cp_cmd: coverpoint cmd {
            bins read  = {8'h01};
            bins write = {8'h02};
            bins reset = {8'h03};
        }

        // 地址覆盖点
        cp_addr: coverpoint addr {
            bins low  = {[0:32'hFF]};
            bins mid  = {[32'h100:32'h1FF]};
            bins high = {[32'h200:32'h2FF]};
        }

        // 交叉覆盖
        cx_cmd_addr: cross cp_cmd, cp_addr;
    endgroup

    // 类内 covergroup 在构造函数中实例化
    function new();
        cg = new();
    endfunction

    // 采样
    function void sample();
        cg.sample();
    endfunction
endclass

10.2 覆盖点详解

covergroup cg;
    // 基本覆盖点
    cp_data: coverpoint data {
        bins zero   = {0};
        bins small  = {[1:127]};
        bins large  = {[128:255]};
        bins others = default;  // 其他值
    }

    // 带权重
    cp_cmd: coverpoint cmd {
        bins low_weight  = {1} weight 1;
        bins high_weight = {2} weight 10;
    }

    // 自动分箱
    cp_auto: coverpoint addr {
        bins auto[] = {[0:255]};  // 自动分成N个bin
    }

    // 序列覆盖
    cp_seq: coverpoint data {
        bins seq1 = (0 => 1 => 2);
        bins seq2 = (3[*3]);  // 3连续出现3次
    }
endgroup

10.3 覆盖率目标

// 设置覆盖率目标
covergroup cg;
    cp_cmd: coverpoint cmd {
        bins read  = {8'h01};
        bins write = {8'h02};
    }

    // 全局目标
    option.goal = 100;  // 100%覆盖率

    // 单个覆盖点目标
    cp_cmd.option.goal = 90;
endgroup

// 在模块、program 或 interface 作用域中创建 covergroup 实例
cg cg_inst = new();

// 检查覆盖率
initial begin
    // 运行测试...
    #10000;

    if (cg_inst.get_coverage() >= 100)
        $display("覆盖率达标: %0f%%", cg_inst.get_coverage());
    else
        $warning("覆盖率不足: %0f%%", cg_inst.get_coverage());
end

第十一层:UVM框架

build_phase()connect_phase()run_phase() 本质上都是 UVM 父类中的虚方法。子类写出同名方法是在运用前面介绍的方法重写;UVM 通过父类句柄调用不同组件的 phase,是多态的实际应用。

11.3 接口与 transaction

import uvm_pkg::*;
`include "uvm_macros.svh"

// interface 把同一协议的信号集中在一起
interface my_bus_if(input logic clk);
    logic        wr;
    logic        valid;
    logic        ready;
    logic [31:0] addr;
    logic [31:0] data;
endinterface

// transaction 只描述“传什么”,不描述具体时钟时序
class my_transaction extends uvm_sequence_item;
    rand bit        wr;
    rand bit [31:0] addr;
    rand bit [31:0] data;

    // 注册到 UVM factory,并注册字段。
    // 注册字段后,可直接使用 print()、sprint()、copy()、compare() 等方法。
    `uvm_object_utils_begin(my_transaction)
        `uvm_field_int(wr,   UVM_ALL_ON)
        `uvm_field_int(addr, UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
    `uvm_object_utils_end

    function new(string name = "my_transaction");
        super.new(name);
    endfunction
endclass

关联前文知识:

  • extends:继承 uvm_sequence_item 已有的打印、复制和比较能力;
  • rand:允许 sequence 调用 randomize() 产生随机激励;
  • bit:transaction 通常只保存 0/1,使用二态类型可减少仿真开销;
  • interface:连接静态的模块世界和动态的 class 世界;
  • virtual interface:类不能直接用层次路径连接普通 interface,因此保存一个虚接口句柄。

宏与 factory 的关系:

`uvm_object_utils(my_transaction)
my_transaction::type_id::create("txn");

type_id::create() 会通过 factory 创建对象,使测试可以在不修改原组件源码的情况下进行类型替换(factory override)。它最终仍会调用类的 new() 构造函数。

11.4 Driver:把 transaction 转成引脚信号

class my_driver extends uvm_driver #(my_transaction);
    `uvm_component_utils(my_driver)

    // virtual 表示这是 interface 的句柄,不是新的硬件实例
    virtual my_bus_if vif;

    function new(string name = "my_driver", uvm_component parent = null);
        super.new(name, parent);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);

        // 从 UVM 配置数据库取得顶层 testbench 放入的接口句柄
        if (!uvm_config_db#(virtual my_bus_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "my_driver 未取得虚接口 vif")
    endfunction

    task run_phase(uvm_phase phase);
        my_transaction txn;

        // 先给输出信号确定的初始值
        vif.valid <= 1'b0;

        forever begin
            // 1. 阻塞等待 sequencer 提供下一个 transaction
            seq_item_port.get_next_item(txn);

            // 2. 在下降沿改变信号,避免与 DUT 在上升沿采样产生竞争
            @(negedge vif.clk);
            vif.wr    <= txn.wr;
            vif.addr  <= txn.addr;
            vif.data  <= txn.data;
            vif.valid <= 1'b1;

            // 3. valid && ready 表示一次传输在该上升沿完成
            do @(posedge vif.clk); while (!vif.ready);

            // 4. 撤销 valid,并通知 sequencer 当前 transaction 已完成
            @(negedge vif.clk);
            vif.valid <= 1'b0;
            seq_item_port.item_done();
        end
    endtask
endclass

uvm_driver #(my_transaction) 中的 #(...) 是参数化类,表示这个 driver 只接收 my_transaction 类型的数据。get_next_item()item_done() 必须成对使用,否则 sequence 可能一直等待。

11.5 Monitor:把引脚信号还原成 transaction

class my_monitor extends uvm_monitor;
    `uvm_component_utils(my_monitor)

    virtual my_bus_if vif;

    // analysis_port 是广播端口:一个 monitor 可以把数据发给
    // scoreboard、coverage collector 等多个订阅者。
    uvm_analysis_port #(my_transaction) ap;

    function new(string name = "my_monitor", uvm_component parent = null);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual my_bus_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "my_monitor 未取得虚接口 vif")
    endfunction

    task run_phase(uvm_phase phase);
        my_transaction txn;

        forever begin
            @(posedge vif.clk);

            // 只在真正握手成功的时钟沿采集一次
            if (vif.valid && vif.ready) begin
                txn = my_transaction::type_id::create("txn");
                txn.wr   = vif.wr;
                txn.addr = vif.addr;
                txn.data = vif.data;

                // write() 是非阻塞广播,不需要等待 scoreboard 返回
                ap.write(txn);
            end
        end
    endtask
endclass

Driver 是主动组件,会改变 DUT 输入;Monitor 是被动组件,只观察信号,不应驱动 DUT。

11.6 Scoreboard:比较预期结果和实际结果

下面先展示按顺序比较的单输入版本。它假设 DUT 的结果严格保序,而且每笔实际结果到达前,其期望结果已进入队列。Monitor 调用 ap.write(txn) 后,最终会调用 scoreboard 的 write()。允许乱序时,应使用后面的按 ID 匹配方案。

class my_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(my_scoreboard)

    // imp 是接收端;第二个类型参数表示 write() 在哪个类中实现
    uvm_analysis_imp #(my_transaction, my_scoreboard) actual_imp;

    // 队列适合按发送顺序保存预期事务
    my_transaction expected_q[$];

    function new(string name = "my_scoreboard",
                 uvm_component parent = null);
        super.new(name, parent);
        actual_imp = new("actual_imp", this);
    endfunction

    // 实际项目通常由 reference model/predictor 写入预期结果
    function void push_expected(my_transaction expected);
        my_transaction snapshot;
        if (expected == null) begin
            `uvm_fatal("SCOREBOARD", "不能保存空的期望事务")
            return;
        end
        if (!$cast(snapshot, expected.clone())) begin
            `uvm_fatal("SCOREBOARD", "期望事务 clone 类型错误")
            return;
        end
        expected_q.push_back(snapshot);
    endfunction

    // actual_imp 收到数据后,UVM 自动调用这个 write()
    function void write(my_transaction actual);
        my_transaction expected;

        if (actual == null) begin
            `uvm_error("SCOREBOARD", "收到空的实际事务")
            return;
        end

        if (expected_q.size() == 0) begin
            `uvm_error("SCOREBOARD", "收到实际事务,但预期队列为空")
            return;
        end

        expected = expected_q.pop_front();

        // compare() 来自 uvm_object;transaction 字段已通过宏注册
        if (!actual.compare(expected))
            `uvm_error("SCOREBOARD",
                       $sformatf("比较失败\nexpected:\n%sactual:\n%s",
                                 expected.sprint(), actual.sprint()))
    endfunction

    function void check_phase(uvm_phase phase);
        super.check_phase(phase);
        if (expected_q.size() != 0)
            `uvm_error("SCOREBOARD",
                       $sformatf("仍有 %0d 笔期望事务未匹配", expected_q.size()))
    endfunction
endclass

数据方向如下:

同一输入事务 ──> Reference Model/Predictor ──> expected_q ──┐
DUT 输出 ──────> Monitor ──> actual_imp ────────────────────┤

                                                     Scoreboard 比较
  • expected:参考模型根据同一输入事务和设计规格独立计算;
  • actual:Monitor 从 DUT 输出端采集;
  • Scoreboard:负责比较二者。

11.6.1 为什么乱序 scoreboard 适合关联数组

请求顺序:A(ID=3) → B(ID=8) → C(ID=5)
返回顺序:B(ID=8) → A(ID=3) → C(ID=5)

一个全局期望队列会先弹出 A,与第一个返回的 B 错配。关联数组允许按实际响应携带的 ID 查找期望事务:返回 B 时查键 8,返回 A 时查键 3。

前提是匹配键能从协议观察到,而且能够确定对应关系。 不应直接把 UVM 的 get_transaction_id() 当成总线上的 AXI ID;前者是验证框架中的事务标识,DUT 不会自动把它带回响应。也不能给 expected/actual 各自按到达顺序编号,再把编号当作共同匹配键。

协议和未完成事务情况保存方式比较规则
所有事务严格保序一个队列与队头比较
每个键最多一笔未完成事务,键之间可乱序transaction expected[key_t]按键取出对应对象;插入前检查键是否已存在
同 ID 有多笔未完成事务,同 ID 内保序transaction expected[key_t][$]按键定位队列,再与队头比较
同 ID 内也允许乱序更细的可观测键或协议规定的匹配机制不能简单按该 ID 的队头比较

单对象关联数组反复执行 expected[id] = txn 会覆盖同键的旧事务。因此“有 ID”不等于“每笔事务的 ID 都唯一”。

11.6.2 关联数组+队列:ID 之间乱序,ID 内保序

请求顺序:A(ID=3)、B(ID=8)、C(ID=3)

expected[3] → [A, C]
expected[8] → [B]

允许返回:B → A → C
不允许返回:C → A → B,因为同 ID=3 的顺序被颠倒

在普通 AXI 读响应/写响应的相应排序域内,同 ID 的响应按请求顺序返回,不同 ID 的响应可以乱序。读和写需要分别跟踪,多个源端口的同数值 ID 也不能直接视为同一来源。这种规则适合用“关联数组+队列”表示,具体仍以接口采用的 AXI 版本及功能为准。

下面用 typedef 把类型拆开,比直接写多个维度更容易理解:

// 示意声明;transaction 是工程中已有的事务类型
typedef int unsigned key_t;
typedef transaction txn_queue_t[$];
txn_queue_t expected[key_t]; // 外层按键查找,每个键的值是一整个队列

关联数组负责找到哪个 ID,队列负责找到该 ID 的第几笔事务。

11.6.3 UVM 示例:两路缓存,允许实际结果先到 scoreboard

参考模型与输出 Monitor 是两条独立的数据路径。即使 DUT 的请求先于响应发生,实际结果仍可能因为模型处理延迟或同一时间槽中的回调先后,先进入 scoreboard。下面同时缓存 expected 和 actual,两侧任何一侧到达都尝试匹配。

示例范围:一个来源、一个响应类别、8 位 ID;同 ID 内 expected 和 actual 各自按协议顺序提交;每个对象已经代表一笔完整响应。id/data/resp 为演示字段,完整协议还需添加相应字段和检查。两路各自的顺序必须来自 Monitor/参考模型对真实协议的观察,不能靠排序“修正”。

import uvm_pkg::*;
`include "uvm_macros.svh"

class ooo_item extends uvm_sequence_item;
    logic [7:0]  id;
    logic [31:0] data;
    logic [1:0]  resp;

    // 注册复制和比较需要的字段;保留四态,避免过早丢失 X/Z
    `uvm_object_utils_begin(ooo_item)
        `uvm_field_int(id,   UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
        `uvm_field_int(resp, UVM_ALL_ON)
    `uvm_object_utils_end

    function new(string name = "ooo_item");
        super.new(name);
    endfunction
endclass

// 在同一编译作用域中,每个后缀只声明一次
`uvm_analysis_imp_decl(_ooo_expected)
`uvm_analysis_imp_decl(_ooo_actual)

class ooo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(ooo_scoreboard)

    typedef bit [7:0] id_t;
    typedef ooo_item item_queue_t[$];

    item_queue_t expected_by_id[id_t];
    item_queue_t actual_by_id[id_t];

    uvm_analysis_imp_ooo_expected #(ooo_item, ooo_scoreboard) expected_imp;
    uvm_analysis_imp_ooo_actual   #(ooo_item, ooo_scoreboard) actual_imp;

    function new(string name = "ooo_scoreboard",
                 uvm_component parent = null);
        super.new(name, parent);
        expected_imp = new("expected_imp", this);
        actual_imp   = new("actual_imp", this);
    endfunction

    function ooo_item make_snapshot(ooo_item txn);
        ooo_item snapshot;
        if (txn == null) begin
            `uvm_fatal("OOO_NULL", "收到空事务")
            return null;
        end
        // 在转换为二态键以前检查,不能让未知 ID 悄悄变成合法 ID
        if ($isunknown(txn.id)) begin
            `uvm_error("OOO_ID", "事务 ID 含 X/Z")
            return null;
        end
        if (!$cast(snapshot, txn.clone())) begin
            `uvm_fatal("OOO_COPY", "clone 返回的对象类型不兼容")
            return null;
        end
        return snapshot;
    endfunction

    function void write_ooo_expected(ooo_item txn);
        ooo_item snapshot;
        id_t key;
        snapshot = make_snapshot(txn);
        if (snapshot == null) return;
        key = id_t'(snapshot.id);
        if (!expected_by_id.exists(key))
            expected_by_id[key] = {}; // 新键对应一个空队列
        expected_by_id[key].push_back(snapshot);
        try_match(key);
    endfunction

    function void write_ooo_actual(ooo_item txn);
        ooo_item snapshot;
        id_t key;
        snapshot = make_snapshot(txn);
        if (snapshot == null) return;
        key = id_t'(snapshot.id);
        if (!actual_by_id.exists(key))
            actual_by_id[key] = {};
        actual_by_id[key].push_back(snapshot);
        try_match(key);
    endfunction

    function void try_match(id_t key);
        ooo_item expected;
        ooo_item actual;
        if (!expected_by_id.exists(key) || !actual_by_id.exists(key))
            return;

        while (expected_by_id[key].size() > 0 &&
               actual_by_id[key].size() > 0) begin
            expected = expected_by_id[key].pop_front();
            actual   = actual_by_id[key].pop_front();
            if (!actual.compare(expected))
                `uvm_error("OOO_MISMATCH",
                    $sformatf("ID=%0d\nexpected:\n%sactual:\n%s",
                              key, expected.sprint(), actual.sprint()))
        end

        // 队列为空后删除键,使 num() 表示仍有记录的 ID 数量
        if (expected_by_id[key].size() == 0)
            expected_by_id.delete(key);
        if (actual_by_id[key].size() == 0)
            actual_by_id.delete(key);
    endfunction

    function void check_phase(uvm_phase phase);
        super.check_phase(phase);
        foreach (expected_by_id[key])
            `uvm_error("OOO_MISSING",
                $sformatf("ID=%0d 仍有 %0d 笔期望结果未匹配",
                          key, expected_by_id[key].size()))
        foreach (actual_by_id[key])
            `uvm_error("OOO_UNEXPECTED",
                $sformatf("ID=%0d 仍有 %0d 笔实际结果无对应期望",
                          key, actual_by_id[key].size()))
    endfunction
endclass

在 Env 的 connect_phase() 中,把 Predictor 的 uvm_analysis_port #(ooo_item) 连接到 expected_imp,输出 Monitor 的同类型端口连接到 actual_imp。示例展示比较器,不包含完整的 Driver、DUT 或协议参考模型。

如果 B 的 actual 先到,actual_by_id[8] 先缓存 B;B 的 expected 到来后才比较,比较后删除两侧已用条目。双路缓存解决的是scoreboard 接收先后问题,并未放宽协议要求的响应顺序。

11.6.4 完整工程需要补齐的检查

问题处理方式
多端口或读写共用 ID 数值分开实例/分表,或把来源、响应类别和 ID 组成键;跨互联观察时处理 ID 映射
同 ID 多笔未完成事务每个键存队列,不用单对象覆盖;不要为了通过比较而在该队列中任意搜索
burst 按 beat 到达先重建完整事务,或另存 beat 计数和期望长度,检查 LAST 后才结束该事务
Monitor 复用对象接收时建立快照;对象含子对象时检查 do_copy() 或字段宏的复制策略
漏响应、额外响应、重复响应跟踪请求/响应数量、未匹配记录和必要的生命周期信息,检查内容与顺序;仅有 ID 不能区分所有重复或过期响应
长时间不匹配用时间戳、周期计数和独立 watchdog 检查规范/测试定义的超时,避免一直缓存
复位、取消和 ID 重用按规格处理未完成事务,必要时记录 epoch/代次;若协议不可观察代次,还需约束重用时机或建立请求生命周期跟踪
仿真过早结束sequence 结束不等于所有响应到齐;测试结束前等待在途事务处理完或超时,再由 check_phase() 检查残留

expected_by_id.num() 统计的是键数,不是未完成事务总数;后者需要把各键对应队列的 size() 相加。上述示例包含快照、匹配和结束残留检查,超时、复位及具体协议规则需要按工程补齐。

11.6.5 适合用来验证 scoreboard 自身的场景

  • 不同 ID 按反向顺序返回,应正常匹配。
  • 同 ID 连续发两笔不同内容的事务,按顺序返回应通过,交换返回顺序应报错。
  • actual 先于 expected 进入 scoreboard,应先缓存并在另一侧到达后匹配。
  • 入队后修改发送方原对象,已保存的期望快照不应变化。
  • 漏掉一笔响应或多出一笔响应,超时/结束检查应报错。
  • burst 的 beat 数量或 LAST 位置错误,协议/重组检查应报错,不能只比较最终拼接出的 payload。

11.7 Sequencer、Agent 和 Env:创建并连接组件

11.8 Sequence:产生一组 transaction

class my_sequence extends uvm_sequence #(my_transaction);
    `uvm_object_utils(my_sequence)

    function new(string name = "my_sequence");
        super.new(name);
    endfunction

    task body();
        my_transaction txn;

        repeat (10) begin
            // 1. 通过 factory 创建 transaction
            txn = my_transaction::type_id::create("txn");

            // 2. 等待 sequencer 和 driver 准备好
            start_item(txn);

            // 3. 随机化;示例要求地址按 4 字节对齐
            if (!txn.randomize() with { addr[1:0] == 2'b00; })
                `uvm_fatal("RANDFAIL", "transaction 随机化失败")

            // 4. 把 transaction 交给 sequencer,等待 driver 完成
            finish_item(txn);
        end
    endtask
endclass

11.9 Test:创建环境并启动 sequence

class my_test extends uvm_test;
    `uvm_component_utils(my_test)

    my_env env;

    function new(string name = "my_test", uvm_component parent = null);
        super.new(name, parent);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = my_env::type_id::create("env", this);
    endfunction

    task run_phase(uvm_phase phase);
        my_sequence seq;

        // objection 大于 0 时,UVM 不会结束 run_phase
        phase.raise_objection(this);

        seq = my_sequence::type_id::create("seq");
        seq.start(env.agent.sequencer);

        // sequence 结束后撤销 objection,允许 UVM 结束测试
        phase.drop_objection(this);
    endtask
endclass

11.10 顶层模块:连接 DUT 世界与 UVM 世界

module tb_top;
    import uvm_pkg::*;

    logic clk = 0;
    always #5 clk = ~clk;

    // interface 是静态实例,实际项目中还要连接 DUT
    my_bus_if bus_if(clk);

    initial begin
        // 把静态 interface 的句柄存入 config_db。
        // 路径中的 * 表示 driver 和 monitor 等所有下层组件。
        uvm_config_db#(virtual my_bus_if)::set(null, "uvm_test_top.env.agent.*", "vif", bus_if);

        // 根据名字通过 factory 创建 my_test,并启动全部 UVM phase
        run_test("my_test");
    end
endmodule

config_db::set()get() 必须在以下三方面完全匹配:

  1. 类型匹配:都是 virtual my_bus_if
  2. 字段名匹配:都是 "vif"
  3. 路径匹配:设置路径必须覆盖 driver 和 monitor 的组件路径。

11.11 把前面知识串起来

前面学过的知识在 UVM 中的应用
class 与对象句柄所有 transaction 和 UVM 组件
继承 extends自定义类继承 UVM 基类
虚方法与重写重写 build_phase()connect_phase()run_phase()
多态UVM 用基类句柄统一调用各组件的 phase
动态数组transaction 的可变长度 payload、burst 数据
队列顺序 scoreboard、采样历史、同 ID 内的事务顺序
关联数组按 ID 查找乱序事务、稀疏参考内存
关联数组+队列多笔同 ID 未完成事务,以及 ID 之间乱序的匹配
对象复制与快照避免 Monitor/Predictor 后续修改已入队事务
随机化与约束sequence 产生受约束的 transaction
interface封装 DUT 引脚
virtual interfaceclass 访问静态 interface 实例
参数化类 #(...)规定 driver、sequencer、port 传递的 transaction 类型
$castfactory override 或基类/子类句柄转换时检查类型

最核心的数据转换只有两次:

transaction --Driver--> DUT 引脚信号
DUT 引脚信号 --Monitor--> transaction

其余 UVM 组件主要负责产生、调度、连接、检查和配置这两次转换。


验证流程

1. 编写Transaction(定义数据结构)
2. 编写Driver(驱动DUT)
3. 编写Monitor(采集信号)
4. 编写Scoreboard(比较结果)
5. 编写Coverage(统计覆盖率)
6. 编写Sequence(生成激励)
7. 编写Test(组织验证环境)
8. 运行仿真,分析覆盖率

附录:面试高频问题

模块常见问题
class静态属性vs动态属性?继承和多态?
数组与队列a[4]a[]a[int]a[$] 有什么区别?new[N](old) 如何保留数据?
对象复制容器赋值是否复制 class 对象?句柄赋值、浅拷贝与 UVM clone() 有何区别?
scoreboard何时用队列、关联数组或关联数组+队列?如何处理同 ID 多笔事务、两路到达顺序和残留检查?
randomize约束如何编写?如何覆盖约束?
SVA|->和|=>的区别?sequence如何定义?
coveragecoverpoint和cross的区别?如何提高覆盖率?
UVM组件层次结构?phase机制?factory机制?

数组与乱序匹配参考资料

最后更新: 2026-09-07