4、验证环境架构设计:UVM验证环境组件概览
好,咱们进入第四章。这一章我打算聊聊验证环境的骨架——架构设计。说白了,就是你的UVM环境里那些组件该怎么摆、怎么连、怎么分工。
我记得刚入行那会儿,带我的老工程师丢给我一个UVM环境,说“你照着这个写”。我一看,好家伙,agent里塞了三个driver,monitor挂在外面,sequence直接写在test里。能跑,但想复用?门儿都没有。后来我自己踩了不少坑,才慢慢摸清楚这套架构的门道。
4.1 UVM验证环境组件概览
先快速过一遍UVM里那些核心组件,咱们心里有个底。
| 组件 | 职责 | 我常说的比喻 |
|---|---|---|
| driver | 把sequence_item转成接口时序 | 翻译官,把“人话”转成“机器话” |
| monitor | 抓取接口信号,打包成transaction | 监听员,只看不说话 |
| sequencer | 管理sequence,给driver喂item | 调度员,排好队一个一个来 |
| agent | 把driver/monitor/sequencer打包 | 小团队,负责一个接口协议 |
| env | 组装所有agent和reference model | 总装车间,把零件拼成整机 |
| test | 配置env,启动sequence | 导演,喊“action”的那个人 |
嗯,这张表你最好存着。每次搭环境前看一眼,能少走不少弯路。
4.2 driver/monitor/sequencer 架构设计原则
这三个家伙是agent里的铁三角。我见过很多新手把driver和monitor的代码写成一团,结果换个接口协议就得重写。其实没那么复杂。
4.2.1 driver:只做一件事
driver的职责非常单一:从sequencer拿item,然后驱动到接口上。仅此而已。
我个人的习惯是:driver里绝对不放任何协议检查或数据处理的逻辑。那些东西应该放到monitor或者reference model里。
核心原则:driver只管“发”,不管“对不对”。
// 好的driver写法
class my_driver extends uvm_driver#(my_item);
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_item(req); // 只负责驱动时序
seq_item_port.item_done();
end
endtask
virtual task drive_item(my_item item);
// 这里只做时序转换,不做数据检查
@(posedge vif.clk);
vif.data <= item.data;
vif.valid <= 1'b1;
endtask
endclass
4.2.2 monitor:只抓不推
monitor是环境的眼睛。它默默观察接口上的信号变化,然后打包成transaction发给scoreboard。
避坑指南:我曾经在一个项目里把monitor和driver写成了双向通信,结果环境跑起来死锁了。monitor应该是完全被动的,它不驱动任何信号,也不等待任何握手。
注意:monitor里不要加任何“等待某个条件”的逻辑。它只负责“看到什么就报告什么”。
class my_monitor extends uvm_monitor;
virtual task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.valid) begin
my_item item = my_item::type_id::create("item");
item.data = vif.data;
item_port.write(item); // 直接扔出去,不判断对错
end
end
endtask
endclass
4.2.3 sequencer:当好调度员
sequencer其实是最省心的组件。UVM已经帮你写好了,你直接继承就行。除非你有特殊的仲裁需求,否则别动它。
你想想看,sequencer的核心能力是什么?是管理多个sequence的优先级和并发。这个UVM做得已经很好了,咱们别画蛇添足。
4.3 agent/env/test 架构设计原则
好,从单个组件上升到模块级别。这里我踩过的坑最多。
4.3.1 agent:按接口协议打包
agent的划分标准只有一个:按接口协议。一个agent对应一种接口协议。
举个例子,你的DUT有APB接口和AXI接口,那就应该有两个agent:apb_agent和axi_agent。千万别按功能模块来分,否则复用性会大打折扣。
小技巧:agent里要不要包含sequencer?看情况。如果是master agent(主动发起事务),需要sequencer。如果是slave agent(被动响应),可以不要sequencer,但monitor一定要有。
4.3.2 env:只做组装,不做逻辑
env是总装车间。它的任务就是把各个agent、reference model、scoreboard、coverage collector拼在一起,连好TLM端口。
我见过最糟糕的env:里面塞了各种function和task,甚至还有状态机。这完全违背了env的设计初衷。env应该是一个“配置和连接”的容器,不是逻辑处理的地方。
class my_env extends uvm_env;
my_agent agt;
my_model mdl;
my_scoreboard scb;
function void build_phase(uvm_phase phase);
agt = my_agent::type_id::create("agt", this);
mdl = my_model::type_id::create("mdl", this);
scb = my_scoreboard::type_id::create("scb", this);
endfunction
function void connect_phase(uvm_phase phase);
agt.monitor.item_port.connect(mdl.analysis_export);
mdl.result_port.connect(scb.expected_export);
endfunction
// 不要在这里加任何数据处理逻辑!
endclass
4.3.3 test:配置的起点
test是验证环境的入口。它的主要工作是:配置env的参数、设置verbosity、启动sequence。
说白了,test就是告诉环境“今天测什么”。不同的testcase应该通过不同的test类来实现,而不是在同一个test里加if-else。
4.4 可重用性设计
这一节是重点。我做了这么多年验证,最深的体会就是:可重用性不是写出来的,是设计出来的。
4.4.1 参数化你的组件
别把数据位宽、时钟周期这些硬编码在组件里。用parameter或者config_db传进去。
class my_driver #(type REQ = uvm_sequence_item) extends uvm_driver#(REQ);
int data_width;
// 通过config_db配置,而不是写死在代码里
endclass
4.4.2 用factory覆盖机制
UVM的factory机制是重用的利器。你想换个driver?不用改env,直接在test里override就行。
核心思想:写环境的时候,永远假设“将来有人会替换掉这个组件”。
4.4.3 分离协议层和应用层
这是我最想强调的一点。driver/monitor处理的是协议层(时序、握手),sequence处理的是应用层(数据内容、场景)。这两层一定要分开。
为什么?因为协议层变化少(AHB就是AHB,不会变),应用层变化多(今天测读,明天测写,后天测并发)。分开了,你的sequence就能在不同项目间复用。
4.4.4 我踩过的一个坑
曾经有个项目,我把协议检查和数据比对都塞在了monitor里。结果换了个DUT版本,接口时序微调,我不得不重写整个monitor。后来我学乖了:monitor只负责抓数据,检查逻辑放到独立的checker组件里。这样接口变了,我只需要改monitor的抓取逻辑,checker完全不用动。
4.5 小结
这一章的内容其实就一句话:各司其职,松耦合,高内聚。driver只管驱动,monitor只管监听,env只管组装,test只管配置。谁也别越界。
下一章咱们聊聊sequence和sequence_item的设计,那又是另一番天地了。
课后思考:如果你的DUT有多个相同类型的接口(比如两个SPI),你会怎么设计agent?是共用一个agent实例化两次,还是写两个不同的agent?