3、Gensys核心概念:事务级建模、序列器、驱动器、监视器、代理

各位同学,今天我们来聊聊Gensys验证方法学的几个核心概念。说实话,这些概念刚接触时容易混淆,但搞懂了它们,你的验证环境搭建就会顺手很多。

我个人习惯把Gensys的组件分成两类:一类是“干活的”,比如驱动器、监视器;另一类是“管事的”,比如代理、序列器。事务级建模则是它们之间沟通的语言。咱们一个一个来看。

3.1 事务级建模(Transaction-Level Modeling)

事务级建模,说白了就是把信号级别的交互抽象成“事务”。什么叫事务?一次读操作、一次写操作、一个数据包,这些都是事务。

我刚开始做验证时,习惯把每个信号都拉出来单独驱动。后来发现,一个简单的AXI写操作,要控制几十个信号,代码写得又臭又长。改用事务级建模后,一个write(addr, data)就搞定了。

核心思想:事务是验证组件之间传递的最小信息单元。它封装了协议操作的完整信息,但不包含具体的时序细节。

举个例子,一个读事务可以这样定义:

class read_transaction extends uvm_sequence_item;
    rand bit [31:0] addr;
    rand bit [31:0] data;
    rand int delay;  // 随机延迟
    
    `uvm_object_utils_begin(read_transaction)
        `uvm_field_int(addr, UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
        `uvm_field_int(delay, UVM_ALL_ON)
    `uvm_object_utils_end
endclass

你看,这个类里只有地址、数据、延迟三个字段。至于信号怎么跳变、时钟怎么对齐,那是驱动器的事,事务本身不关心。

我的经验:定义事务类时,尽量把字段声明为rand类型。这样序列器可以自动生成随机化的事务,覆盖更多场景。我曾经因为漏了rand关键字,导致一批测试用例的地址总是固定的,浪费了两天调试时间。

3.2 序列器(Sequencer)

序列器是事务的“调度员”。它从序列(sequence)那里接收事务请求,然后按顺序发给驱动器。

你想想看,如果没有序列器,多个序列同时产生事务时,谁先谁后?谁来仲裁?序列器就是干这个的。

序列器的典型用法是这样的:

class my_sequencer extends uvm_sequencer #(my_transaction);
    `uvm_component_utils(my_sequencer)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
endclass

注意那个参数#(my_transaction),它告诉序列器:我管理的是哪种事务类型。这样序列器就知道该转发什么类型的事务给驱动器。

避坑指南:我曾经犯过一个错误——在序列器里直接调用驱动器的任务。千万别这么做!序列器只负责转发事务,不负责驱动。驱动逻辑必须放在驱动器里,否则你的环境会变得一团糟,复用性极差。

3.3 驱动器(Driver)

驱动器是“干粗活”的。它从序列器拿到事务,然后把它翻译成具体的信号时序。

说白了,驱动器就是协议引擎。它知道时钟怎么跳、数据什么时候采样、握手信号怎么处理。

一个典型的驱动器实现:

class my_driver extends uvm_driver #(my_transaction);
    virtual my_if vif;
    
    `uvm_component_utils(my_driver)
    
    function void build_phase(uvm_phase phase);
        if(!uvm_config_db #(virtual my_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "Virtual interface not set")
    endfunction
    
    task run_phase(uvm_phase phase);
        forever begin
            seq_item_port.get_next_item(req);
            drive_transaction(req);
            seq_item_port.item_done();
        end
    endtask
    
    task drive_transaction(my_transaction tr);
        // 这里实现具体的信号驱动逻辑
        @(posedge vif.clk);
        vif.addr <= tr.addr;
        vif.data <= tr.data;
        // ... 其他信号
    endtask
endclass

注意那个get_next_itemitem_done的配对。这是驱动器与序列器握手的标准方式。少了任何一个,序列器都会卡住。

关键点:驱动器只做一件事——把事务变成信号。不要在里面做数据检查、覆盖率收集之类的事。那些是监视器和记分板的工作。

3.4 监视器(Monitor)

监视器是“偷听”的。它不驱动信号,只观察信号的变化,然后把观察到的信息打包成事务,发给其他组件(比如记分板、覆盖率收集器)。

我习惯把监视器分成两种:

  • 主动监视器:主动采样信号,比如在每个时钟上升沿采样数据总线
  • 被动监视器:等待事件触发,比如检测到有效信号时才采样

监视器的核心代码:

class my_monitor extends uvm_monitor;
    virtual my_if vif;
    uvm_analysis_port #(my_transaction) ap;
    
    `uvm_component_utils(my_monitor)
    
    function void build_phase(uvm_phase phase);
        ap = new("ap", this);
        // 获取vif...
    endfunction
    
    task run_phase(uvm_phase phase);
        my_transaction tr;
        forever begin
            @(posedge vif.clk);
            if(vif.valid) begin
                tr = my_transaction::type_id::create("tr");
                tr.addr = vif.addr;
                tr.data = vif.data;
                ap.write(tr);  // 通过分析端口发送事务
            end
        end
    endtask
endclass

看到那个uvm_analysis_port了吗?这是监视器对外通信的通道。它把观察到的事务广播出去,谁想听谁就订阅。

我的建议:监视器里尽量少做判断。只负责采样和打包,不做数据正确性检查。检查逻辑放到记分板里,这样职责清晰,调试也方便。

3.5 代理(Agent)

代理是“包工头”。它把序列器、驱动器、监视器打包在一起,形成一个完整的协议处理单元。

一个典型的代理结构:

class my_agent extends uvm_agent;
    my_sequencer sqr;
    my_driver drv;
    my_monitor mon;
    
    `uvm_component_utils(my_agent)
    
    function void build_phase(uvm_phase phase);
        sqr = my_sequencer::type_id::create("sqr", this);
        drv = my_driver::type_id::create("drv", this);
        mon = my_monitor::type_id::create("mon", this);
    endfunction
    
    function void connect_phase(uvm_phase phase);
        drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
endclass

代理有两种模式:

模式 包含组件 适用场景
主动模式(is_active = 1) 序列器 + 驱动器 + 监视器 需要驱动激励的场景
被动模式(is_active = 0) 仅监视器 只观察不驱动的场景

嗯,这里要注意:被动模式只包含监视器,没有序列器和驱动器。比如你要监控一个现成的IP核,不需要给它发激励,那就用被动模式。

我曾经踩过的坑:在同一个代理里同时使用主动和被动模式,结果驱动器在驱动信号,监视器也在采样同一个信号,导致时序冲突。后来我把它们拆成两个独立的代理,一个负责驱动,一个负责监视,问题就解决了。

3.6 核心关系图

为了让你更直观地理解这些组件之间的关系,我画了一张图:

序列 (Sequence) 序列器 (Sequencer) 驱动器 (Driver) 监视器 (Monitor) DUT 代理 (Agent) 事务请求 转发事务 信号驱动 信号采样

从这张图你可以看到:序列产生事务,通过序列器转发给驱动器;驱动器把事务变成信号驱动DUT;监视器观察DUT的信号变化,打包成事务发送出去。代理则把序列器、驱动器、监视器封装在一起,形成一个可复用的组件。

好了,这五个核心概念就讲到这里。记住一句话:事务是血液,序列器是心脏,驱动器是手,监视器是眼睛,代理是躯干。理解了它们的关系,Gensys验证环境你就掌握了七成。


公众号:蓝海资料掘金营,微信deep3321