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_item和item_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 核心关系图
为了让你更直观地理解这些组件之间的关系,我画了一张图:
从这张图你可以看到:序列产生事务,通过序列器转发给驱动器;驱动器把事务变成信号驱动DUT;监视器观察DUT的信号变化,打包成事务发送出去。代理则把序列器、驱动器、监视器封装在一起,形成一个可复用的组件。
好了,这五个核心概念就讲到这里。记住一句话:事务是血液,序列器是心脏,驱动器是手,监视器是眼睛,代理是躯干。理解了它们的关系,Gensys验证环境你就掌握了七成。
公众号:蓝海资料掘金营,微信deep3321