3、验证方法学入门:UVM是什么、UVM的核心思想、UVM的基本组件
做验证这行,你要是没听过UVM,那基本等于白干了。说实话,我刚入行那会儿,大家还在用Verilog写testbench,那叫一个痛苦。每次换个项目,验证环境基本重写一遍,代码复用率低得可怜。后来UVM一出来,整个行业都变了。
UVM全称是Universal Verification Methodology,通用验证方法学。说白了,它就是一套基于SystemVerilog的标准化验证框架。你想想看,如果每个公司、每个项目都自己搞一套验证环境,那得多乱?UVM就是要把这件事统一起来。
3.1 UVM到底是什么?
UVM不是工具,不是软件,它是一套类库和方法论。Accellera组织搞的,后来成了IEEE标准。我习惯把它理解成一套搭积木的规则——告诉你用什么积木、怎么搭、搭成什么样。
它的底层是SystemVerilog的面向对象特性。你写UVM验证环境,本质上就是在写SV代码,只不过用的是UVM提供的那些基类。比如uvm_component、uvm_sequence、uvm_driver这些。
核心要点:UVM = 标准化的验证组件 + 标准化的验证流程
我在项目中遇到过不少新人,一上来就问我:「UVM是不是很难学?」其实不然。你只要搞懂它的核心思想,剩下的就是熟悉那些API了。嗯,这里要注意,UVM的学习曲线确实有点陡,但一旦跨过那个坎,后面就顺了。
3.2 UVM的核心思想
UVM的核心思想,我总结下来就三条:
3.2.1 基于组件的架构
UVM把验证环境拆成一个个独立的组件。每个组件各司其职,互不干扰。比如driver只管驱动信号,monitor只管监测信号,scoreboard只管比对结果。
为什么要这么搞?你想想看,如果所有代码都揉在一起,改一个地方可能影响全局。拆开了,每个组件可以独立开发、独立调试、独立复用。我在一个项目里写好的driver,换个项目稍微改改就能用,省了多少事。
3.2.2 基于事务的验证
传统验证是信号级的——你盯着每个时钟沿,看每个信号对不对。UVM不一样,它是事务级的。什么叫事务?一次读操作、一次写操作、一个数据包,这些都是事务。
说白了,你不需要关心信号怎么跳变的,你只需要关心事务对不对。比如发一个读命令,你就等着读回来的数据对不对就行了。中间那些地址线、数据线怎么跳的,那是driver的事。
我的经验:事务级验证最大的好处是抽象层次高。你写测试用例的时候,不用再跟那些烦人的时序打交道了。我曾经在一个项目里,用事务级的方法,三天写完了原来要两周才能写完的测试用例。
3.2.3 可配置与可复用
UVM的组件都是可配置的。通过参数、配置数据库(uvm_config_db),你可以让同一个组件适应不同的场景。比如同一个driver,配置成AHB协议就是AHB driver,配置成AXI协议就是AXI driver。
复用性更是UVM的杀手锏。你写好的验证环境,换个项目,换个DUT,稍微改改就能用。我见过最夸张的,一个验证环境用了五个项目,每次只改改配置和约束。
3.3 UVM的基本组件
UVM的组件很多,但核心的就那么几个。我按它们在验证环境中的角色,给你捋一捋:
| 组件 | 作用 | 我常用的场景 |
|---|---|---|
| uvm_driver | 驱动事务到DUT接口 | 把sequence发来的事务转成信号时序 |
| uvm_monitor | 监测DUT接口上的活动 | 抓取输出信号,转成事务发给scoreboard |
| uvm_sequencer | 管理sequence的执行顺序 | 控制多个sequence的调度 |
| uvm_agent | 封装driver、monitor、sequencer | 一个agent对应一个协议接口 |
| uvm_env | 组织所有agent和组件 | 顶层环境,连接所有东西 |
| uvm_scoreboard | 比对期望值和实际值 | 检查DUT行为是否正确 |
| uvm_test | 定义测试用例 | 配置环境、启动sequence |
3.3.1 uvm_driver 和 uvm_sequencer
这两个是搭档。sequencer负责管sequence,driver负责干活。sequence生成事务,sequencer把它发给driver,driver再把它转成信号驱动到DUT上。
我习惯把sequencer想象成一个任务队列,driver就是那个干活的人。sequence就是任务描述。你往队列里塞任务,driver一个个取出来执行。
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
virtual 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
endclass
3.3.2 uvm_monitor
monitor就是偷看的。它不驱动信号,只监测。DUT的输入输出接口上发生了什么,它都记录下来,转成事务发给其他组件。
我曾经在一个项目里吃过亏——monitor没写好,抓到的数据老是少一拍。后来发现是采样时机不对。嗯,这里要注意,monitor的采样时序一定要跟DUT的时序对齐,不然数据对不上。
3.3.3 uvm_agent
agent是个容器。它把driver、monitor、sequencer打包在一起。一个agent对应一个协议接口。比如你有AHB接口,就搞一个AHB agent;有APB接口,就搞一个APB agent。
这样做的好处是模块化。你写好的AHB agent,拿到别的项目直接用。我手头就有个AHB agent,用了三年,改了七八个项目,基本没动过。
3.3.4 uvm_env 和 uvm_test
env是顶层容器,把所有agent和组件组织起来。test是测试用例的入口,负责配置env、启动sequence。
我一般会在env里把各个agent连好,把scoreboard挂上。然后在test里配置参数、选择要跑的sequence。这样env是通用的,test是灵活的。
避坑指南:我曾经犯过一个错误——把env写得太大,什么组件都往里塞。结果项目一换,env基本重写。后来我学乖了,env只做组织工作,具体功能交给子组件。这样复用性高得多。
3.4 一个小例子
说了这么多,不如看个实际的。假设你要验证一个简单的FIFO,UVM环境大概长这样:
class fifo_env extends uvm_env;
write_agent w_agent;
read_agent r_agent;
fifo_scoreboard sb;
function void build_phase(uvm_phase phase);
w_agent = write_agent::type_id::create("w_agent", this);
r_agent = read_agent::type_id::create("r_agent", this);
sb = fifo_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
w_agent.monitor.item_port.connect(sb.write_export);
r_agent.monitor.item_port.connect(sb.read_export);
endfunction
endclass
你看,写agent负责产生写事务,读agent负责产生读事务,scoreboard负责比对。各干各的,清清楚楚。
说实话,UVM刚出来那会儿,我也觉得它太复杂了。但用久了你会发现,它的设计是有道理的。那些看似繁琐的规则,其实都是为了让你少踩坑。我建议你从一个小项目开始,慢慢体会UVM的设计哲学。别一上来就想搞个大而全的环境,那只会让你崩溃。
好了,这一章就到这里。下一章我们聊聊UVM的phase机制——那个让很多人头疼的东西。其实搞懂了,也就那么回事。